|
| 1 | +import axios from 'axios'; |
| 2 | +import { toast } from 'react-toastify'; |
| 3 | +import * as types from '../constants/studentTasks'; |
| 4 | +import { ENDPOINTS } from '~/utils/URL'; |
| 5 | +import { mockTasks } from '../components/EductionPortal/StudentDashboard/mockData'; |
| 6 | +import httpService from '../services/httpService'; |
| 7 | + |
| 8 | +/** |
| 9 | + * Set a flag that fetching Student Tasks |
| 10 | + */ |
| 11 | +export const setStudentTasksStart = () => { |
| 12 | + return { |
| 13 | + type: types.FETCH_STUDENT_TASKS_START, |
| 14 | + }; |
| 15 | +}; |
| 16 | + |
| 17 | +/** |
| 18 | + * Set Student Tasks in store |
| 19 | + * @param payload : Student Task [] |
| 20 | + */ |
| 21 | +export const setStudentTasks = (taskItems) => { |
| 22 | + return { |
| 23 | + type: types.RECEIVE_STUDENT_TASKS, |
| 24 | + taskItems, |
| 25 | + }; |
| 26 | +}; |
| 27 | + |
| 28 | +/** |
| 29 | + * Error when fetching student tasks |
| 30 | + * @param payload : error status code |
| 31 | + */ |
| 32 | +export const setStudentTasksError = (err) => { |
| 33 | + return { |
| 34 | + type: types.FETCH_STUDENT_TASKS_ERROR, |
| 35 | + err, |
| 36 | + }; |
| 37 | +}; |
| 38 | + |
| 39 | +/** |
| 40 | + * Update a specific student task |
| 41 | + */ |
| 42 | +export const updateStudentTask = (taskId, updatedTask) => { |
| 43 | + return { |
| 44 | + type: types.UPDATE_STUDENT_TASK, |
| 45 | + taskId, |
| 46 | + updatedTask, |
| 47 | + }; |
| 48 | +}; |
| 49 | + |
| 50 | +/** |
| 51 | + * Transform a single task to flat format |
| 52 | + * @param {Object} task - The task object |
| 53 | + * @param {Object} subjectData - The subject data containing subject info |
| 54 | + * @param {string} subjectKey - The subject key |
| 55 | + * @returns {Object} Transformed task in flat format |
| 56 | + */ |
| 57 | +const transformTaskToFlatFormat = (task, subjectData, subjectKey) => { |
| 58 | + return { |
| 59 | + id: task._id, // Use _id as the primary id for React keys |
| 60 | + course_name: subjectData.subject?.name || subjectKey || 'Unknown Subject', |
| 61 | + subtitle: task.lessonPlan?.title || task.atom?.name || 'No Description', |
| 62 | + task_type: task.type || 'read', |
| 63 | + logged_hours: task.loggedHours || 0, |
| 64 | + suggested_total_hours: task.suggestedTotalHours || 0, |
| 65 | + last_logged_date: task.completedAt || task.assignedAt, |
| 66 | + created_at: task.assignedAt, |
| 67 | + is_completed: task.status === 'completed' || task.status === 'graded', |
| 68 | + has_upload: task.uploadUrls && task.uploadUrls.length > 0, |
| 69 | + has_comments: task.feedback && task.feedback.length > 0, |
| 70 | + status: task.status || 'assigned', |
| 71 | + _id: task._id, |
| 72 | + grade: task.grade, |
| 73 | + feedback: task.feedback, |
| 74 | + dueAt: task.dueAt, |
| 75 | + lessonPlan: task.lessonPlan, |
| 76 | + subject: task.subject, |
| 77 | + atom: task.atom, |
| 78 | + color_level: task.color_level, |
| 79 | + difficulty_level: task.difficulty_level, |
| 80 | + activity_group: task.activity_group, |
| 81 | + }; |
| 82 | +}; |
| 83 | + |
| 84 | +/** |
| 85 | + * Flatten grouped tasks structure to individual tasks array |
| 86 | + * @param {Object} groupedTasks - The grouped tasks from API response |
| 87 | + * @returns {Array} Array of flattened, deduplicated tasks |
| 88 | + */ |
| 89 | +const flattenGroupedTasks = (groupedTasks) => { |
| 90 | + const taskMap = new Map(); // Use Map to deduplicate tasks by _id |
| 91 | + |
| 92 | + // Flatten the grouped structure to get individual tasks |
| 93 | + Object.entries(groupedTasks).forEach(([subjectKey, subjectData]) => { |
| 94 | + Object.values(subjectData.colorLevels).forEach(colorLevel => { |
| 95 | + Object.values(colorLevel.activityGroups).forEach(activityGroup => { |
| 96 | + activityGroup.tasks.forEach(task => { |
| 97 | + // Only add task if it hasn't been seen before (deduplication) |
| 98 | + if (!taskMap.has(task._id)) { |
| 99 | + const transformedTask = transformTaskToFlatFormat(task, subjectData, subjectKey); |
| 100 | + taskMap.set(task._id, transformedTask); |
| 101 | + } |
| 102 | + }); |
| 103 | + }); |
| 104 | + }); |
| 105 | + }); |
| 106 | + |
| 107 | + // Convert Map values to array and add final deduplication as safety measure |
| 108 | + const flattenedTasks = Array.from(taskMap.values()); |
| 109 | + |
| 110 | + // Final deduplication by _id as a safety measure |
| 111 | + const uniqueTasks = flattenedTasks.filter((task, index, self) => |
| 112 | + index === self.findIndex(t => t._id === task._id) |
| 113 | + ); |
| 114 | + |
| 115 | + return uniqueTasks; |
| 116 | +}; |
| 117 | + |
| 118 | +/** |
| 119 | + * Fetch tasks from the primary API endpoint |
| 120 | + * @returns {Promise<Array>} Array of flattened tasks |
| 121 | + */ |
| 122 | +const fetchTasksFromPrimaryEndpoint = async () => { |
| 123 | + console.log('Making API call to:', ENDPOINTS.STUDENT_TASKS()); |
| 124 | + |
| 125 | + const response = await httpService.get(ENDPOINTS.STUDENT_TASKS()); |
| 126 | + console.log('API response:', response.data); |
| 127 | + |
| 128 | + // The API returns grouped tasks, we need to flatten them for our UI |
| 129 | + const groupedTasks = response.data.tasks; |
| 130 | + const uniqueTasks = flattenGroupedTasks(groupedTasks); |
| 131 | + |
| 132 | + console.log(`Processed ${uniqueTasks.length} unique tasks from API response`); |
| 133 | + return uniqueTasks; |
| 134 | +}; |
| 135 | + |
| 136 | +/** |
| 137 | + * Handle API error and try fallback options |
| 138 | + * @param {Error} apiError - The API error |
| 139 | + * @param {Function} dispatch - Redux dispatch function |
| 140 | + * @returns {Promise<Array>} Array of tasks (from fallback or mock data) |
| 141 | + */ |
| 142 | +const handleApiError = async (apiError, dispatch) => { |
| 143 | + console.error('Student tasks API error:', apiError); |
| 144 | + console.error('Error response:', apiError.response?.data); |
| 145 | + console.error('Error status:', apiError.response?.status); |
| 146 | + console.error('Error config:', apiError.config); |
| 147 | + |
| 148 | + // Try alternative endpoint if the first one fails |
| 149 | + if (apiError.response?.status === 404) { |
| 150 | + console.log('Trying alternative endpoint...'); |
| 151 | + try { |
| 152 | + const altResponse = await httpService.post(`${ENDPOINTS.APIEndpoint()}/student-tasks`); |
| 153 | + console.log('Alternative endpoint response:', altResponse.data); |
| 154 | + return altResponse.data.tasks || []; |
| 155 | + } catch (altError) { |
| 156 | + console.error('Alternative endpoint also failed:', altError); |
| 157 | + } |
| 158 | + } |
| 159 | + |
| 160 | + console.warn('Student tasks API not available, using mock data:', apiError.message); |
| 161 | + toast.info('Using demo data. Student tasks API is not yet available.'); |
| 162 | + return mockTasks; |
| 163 | +}; |
| 164 | + |
| 165 | +/** |
| 166 | + * Fetch all student tasks for the logged-in user |
| 167 | + */ |
| 168 | +export const fetchStudentTasks = () => { |
| 169 | + return async (dispatch, getState) => { |
| 170 | + dispatch(setStudentTasksStart()); |
| 171 | + |
| 172 | + try { |
| 173 | + const state = getState(); |
| 174 | + const userId = state.auth.user.userid; |
| 175 | + |
| 176 | + if (!userId) { |
| 177 | + console.error('No user ID found in auth state'); |
| 178 | + dispatch(setStudentTasksError('User not authenticated')); |
| 179 | + return; |
| 180 | + } |
| 181 | + |
| 182 | + try { |
| 183 | + const tasks = await fetchTasksFromPrimaryEndpoint(); |
| 184 | + dispatch(setStudentTasks(tasks)); |
| 185 | + } catch (apiError) { |
| 186 | + const fallbackTasks = await handleApiError(apiError, dispatch); |
| 187 | + dispatch(setStudentTasks(fallbackTasks)); |
| 188 | + } |
| 189 | + } catch (err) { |
| 190 | + console.error('Error fetching student tasks:', err); |
| 191 | + dispatch(setStudentTasksError(err.message || 'Failed to fetch student tasks')); |
| 192 | + toast.error('Failed to fetch student tasks. Please try again later.'); |
| 193 | + } |
| 194 | + }; |
| 195 | +}; |
| 196 | + |
| 197 | +/** |
| 198 | + * Validate if a task can be marked as completed |
| 199 | + * @param {Object} task - The task to validate |
| 200 | + * @returns {Object} Validation result with valid flag and optional error message |
| 201 | + */ |
| 202 | +const validateTaskCompletion = (task) => { |
| 203 | + if (task.is_completed) { |
| 204 | + return { valid: false, errorMessage: 'Task is already completed' }; |
| 205 | + } |
| 206 | + |
| 207 | + if (task.task_type !== 'read') { |
| 208 | + return { valid: false, errorMessage: 'Only read tasks can be marked as complete manually' }; |
| 209 | + } |
| 210 | + |
| 211 | + if (task.logged_hours < task.suggested_total_hours) { |
| 212 | + return { |
| 213 | + valid: false, |
| 214 | + errorMessage: `Insufficient hours logged. Required: ${task.suggested_total_hours}, Logged: ${task.logged_hours}` |
| 215 | + }; |
| 216 | + } |
| 217 | + |
| 218 | + return { valid: true }; |
| 219 | +}; |
| 220 | + |
| 221 | +/** |
| 222 | + * Call the mark-complete API endpoint |
| 223 | + * @param {string} taskId - The task ID |
| 224 | + * @param {string} userId - The user ID |
| 225 | + * @returns {Promise<void>} |
| 226 | + */ |
| 227 | +const callMarkCompleteAPI = async (taskId, userId) => { |
| 228 | + await httpService.post(`${ENDPOINTS.APIEndpoint()}/education-tasks/student/mark-complete`, { |
| 229 | + taskId: taskId, |
| 230 | + studentId: userId, |
| 231 | + requestor: { |
| 232 | + requestorId: userId |
| 233 | + } |
| 234 | + }); |
| 235 | +}; |
| 236 | + |
| 237 | +/** |
| 238 | + * Mark a student task as done |
| 239 | + */ |
| 240 | +export const markStudentTaskAsDone = (taskId) => { |
| 241 | + return async (dispatch, getState) => { |
| 242 | + try { |
| 243 | + const state = getState(); |
| 244 | + const task = state.studentTasks.taskItems.find(t => t.id === taskId); |
| 245 | + |
| 246 | + if (!task) { |
| 247 | + throw new Error('Task not found'); |
| 248 | + } |
| 249 | + |
| 250 | + // Validate task can be marked as done |
| 251 | + const validation = validateTaskCompletion(task); |
| 252 | + if (!validation.valid) { |
| 253 | + if (task.is_completed) { |
| 254 | + toast.warning(validation.errorMessage); |
| 255 | + } else { |
| 256 | + toast.error(validation.errorMessage); |
| 257 | + } |
| 258 | + return; |
| 259 | + } |
| 260 | + |
| 261 | + try { |
| 262 | + // Call the student mark-complete API endpoint |
| 263 | + await callMarkCompleteAPI(taskId, state.auth.user.userid); |
| 264 | + |
| 265 | + // Only update local state if API call succeeds |
| 266 | + dispatch(updateStudentTask(taskId, { |
| 267 | + ...task, |
| 268 | + is_completed: true, |
| 269 | + status: 'completed' |
| 270 | + })); |
| 271 | + |
| 272 | + toast.success('Task marked as completed successfully!'); |
| 273 | + } catch (apiError) { |
| 274 | + // Show error toast if API fails |
| 275 | + console.error('Student task mark complete API error:', apiError); |
| 276 | + console.error('API Error:', apiError.response?.data || apiError.message); |
| 277 | + |
| 278 | + const errorMessage = apiError.response?.data?.error || apiError.message || 'Failed to mark task as complete'; |
| 279 | + toast.error(`Error: ${errorMessage}`); |
| 280 | + } |
| 281 | + } catch (err) { |
| 282 | + console.error('Error marking task as done:', err); |
| 283 | + toast.error('Failed to mark task as done. Please try again.'); |
| 284 | + } |
| 285 | + }; |
| 286 | +}; |
0 commit comments