Skip to content

Commit 38e1319

Browse files
Merge pull request #4454 from OneCommunityGlobal/shashank-madan-intermediate-tasks-frontend
Shashank Madan - Phase 4 – Teacher-Created Intermediate Tasks (Frontend)
2 parents 9f689fe + 5b71b71 commit 38e1319

18 files changed

Lines changed: 1936 additions & 93 deletions

src/actions/intermediateTasks.js

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import { toast } from 'react-toastify';
2+
import { ENDPOINTS } from '~/utils/URL';
3+
import httpService from '../services/httpService';
4+
import { updateStudentTask } from './studentTasks';
5+
6+
/**
7+
* Action types for intermediate tasks
8+
*/
9+
export const FETCH_INTERMEDIATE_TASKS_START = 'FETCH_INTERMEDIATE_TASKS_START';
10+
export const FETCH_INTERMEDIATE_TASKS_SUCCESS = 'FETCH_INTERMEDIATE_TASKS_SUCCESS';
11+
export const FETCH_INTERMEDIATE_TASKS_ERROR = 'FETCH_INTERMEDIATE_TASKS_ERROR';
12+
export const CREATE_INTERMEDIATE_TASK_SUCCESS = 'CREATE_INTERMEDIATE_TASK_SUCCESS';
13+
export const UPDATE_INTERMEDIATE_TASK_SUCCESS = 'UPDATE_INTERMEDIATE_TASK_SUCCESS';
14+
export const DELETE_INTERMEDIATE_TASK_SUCCESS = 'DELETE_INTERMEDIATE_TASK_SUCCESS';
15+
export const MARK_INTERMEDIATE_TASK_DONE = 'MARK_INTERMEDIATE_TASK_DONE';
16+
17+
/**
18+
* Fetch intermediate tasks for a parent task
19+
*/
20+
export const fetchIntermediateTasks = (taskId) => {
21+
return async (dispatch) => {
22+
try {
23+
const response = await httpService.get(ENDPOINTS.INTERMEDIATE_TASKS_BY_PARENT(taskId));
24+
return response.data;
25+
} catch (error) {
26+
console.error('Error fetching intermediate tasks:', error);
27+
toast.error('Failed to fetch sub-tasks');
28+
throw error;
29+
}
30+
};
31+
};
32+
33+
/**
34+
* Calculate total expected hours from intermediate tasks
35+
*/
36+
const calculateTotalExpectedHours = (intermediateTasks) => {
37+
return intermediateTasks.reduce((total, task) => {
38+
return total + (task.expected_hours || 0);
39+
}, 0);
40+
};
41+
42+
/**
43+
* Update parent task's expected hours based on intermediate tasks
44+
*/
45+
const updateParentTaskExpectedHours = async (dispatch, getState, parentTaskId) => {
46+
try {
47+
// Fetch all intermediate tasks for this parent
48+
const intermediateTasks = await dispatch(fetchIntermediateTasks(parentTaskId));
49+
50+
// Calculate total expected hours
51+
const totalExpectedHours = calculateTotalExpectedHours(intermediateTasks);
52+
53+
// Get the parent task from state
54+
const state = getState();
55+
const parentTask = state.studentTasks.taskItems.find(t => t.id === parentTaskId);
56+
57+
if (parentTask) {
58+
// Update the parent task with new expected hours
59+
dispatch(updateStudentTask(parentTaskId, {
60+
...parentTask,
61+
suggested_total_hours: totalExpectedHours
62+
}));
63+
}
64+
} catch (error) {
65+
console.error('Error updating parent task expected hours:', error);
66+
}
67+
};
68+
69+
/**
70+
* Create a new intermediate task
71+
*/
72+
export const createIntermediateTask = (taskData) => {
73+
return async (dispatch, getState) => {
74+
try {
75+
const response = await httpService.post(ENDPOINTS.INTERMEDIATE_TASKS(), taskData);
76+
toast.success('Sub-task created successfully');
77+
78+
// Update parent task expected hours
79+
if (taskData.parentTaskId) {
80+
await updateParentTaskExpectedHours(dispatch, getState, taskData.parentTaskId);
81+
}
82+
83+
return response.data;
84+
} catch (error) {
85+
console.error('Error creating intermediate task:', error);
86+
const errorMessage = error.response?.data?.error || error.message || 'Failed to create sub-task';
87+
toast.error(`Error: ${errorMessage}`);
88+
throw error;
89+
}
90+
};
91+
};
92+
93+
/**
94+
* Update an intermediate task
95+
*/
96+
export const updateIntermediateTask = (id, taskData) => {
97+
return async (dispatch, getState) => {
98+
try {
99+
const response = await httpService.put(ENDPOINTS.INTERMEDIATE_TASK_BY_ID(id), taskData);
100+
toast.success('Sub-task updated successfully');
101+
102+
// Update parent task expected hours
103+
if (taskData.parentTaskId) {
104+
await updateParentTaskExpectedHours(dispatch, getState, taskData.parentTaskId);
105+
}
106+
107+
return response.data;
108+
} catch (error) {
109+
console.error('Error updating intermediate task:', error);
110+
const errorMessage = error.response?.data?.error || error.message || 'Failed to update sub-task';
111+
toast.error(`Error: ${errorMessage}`);
112+
throw error;
113+
}
114+
};
115+
};
116+
117+
/**
118+
* Delete an intermediate task
119+
*/
120+
export const deleteIntermediateTask = (id, parentTaskId = null) => {
121+
return async (dispatch, getState) => {
122+
try {
123+
await httpService.delete(ENDPOINTS.INTERMEDIATE_TASK_BY_ID(id));
124+
toast.success('Sub-task deleted successfully');
125+
126+
// Update parent task expected hours
127+
if (parentTaskId) {
128+
await updateParentTaskExpectedHours(dispatch, getState, parentTaskId);
129+
}
130+
131+
return true;
132+
} catch (error) {
133+
console.error('Error deleting intermediate task:', error);
134+
const errorMessage = error.response?.data?.error || error.message || 'Failed to delete sub-task';
135+
toast.error(`Error: ${errorMessage}`);
136+
throw error;
137+
}
138+
};
139+
};
140+
141+
/**
142+
* Mark an intermediate task as done (for students)
143+
*/
144+
export const markIntermediateTaskAsDone = (id, parentTaskId) => {
145+
return async (dispatch) => {
146+
try {
147+
// First, fetch the current task data
148+
const currentTask = await httpService.get(ENDPOINTS.INTERMEDIATE_TASK_BY_ID(id));
149+
150+
// Update with the completed status while preserving all required fields
151+
const response = await httpService.put(ENDPOINTS.INTERMEDIATE_TASK_BY_ID(id), {
152+
...currentTask.data,
153+
status: 'completed'
154+
});
155+
toast.success('Sub-task marked as done');
156+
return response.data;
157+
} catch (error) {
158+
console.error('Error marking intermediate task as done:', error);
159+
const errorMessage = error.response?.data?.error || error.message || 'Failed to mark sub-task as done';
160+
toast.error(`Error: ${errorMessage}`);
161+
throw error;
162+
}
163+
};
164+
};
165+
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import React, { useState, useEffect } from 'react';
2+
import {
3+
Button,
4+
Form,
5+
FormGroup,
6+
Label,
7+
Input,
8+
Modal,
9+
ModalHeader,
10+
ModalBody,
11+
ModalFooter,
12+
} from 'reactstrap';
13+
import styles from './IntermediateTaskList.module.css';
14+
15+
const IntermediateTaskForm = ({ task, onSubmit, onCancel }) => {
16+
const [formData, setFormData] = useState({
17+
title: '',
18+
description: '',
19+
expectedHours: '',
20+
dueDate: '',
21+
status: 'pending',
22+
});
23+
24+
useEffect(() => {
25+
if (task) {
26+
setFormData({
27+
title: task.title || '',
28+
description: task.description || '',
29+
expectedHours: task.expectedHours || task.expected_hours || '',
30+
dueDate:
31+
task.dueDate || task.due_date
32+
? new Date(task.dueDate || task.due_date).toISOString().split('T')[0]
33+
: '',
34+
status: task.status || 'pending',
35+
});
36+
}
37+
}, [task]);
38+
39+
const handleChange = e => {
40+
const { name, value } = e.target;
41+
setFormData(prev => ({
42+
...prev,
43+
[name]: value,
44+
}));
45+
};
46+
47+
const handleSubmit = e => {
48+
e.preventDefault();
49+
50+
// Validation
51+
if (!formData.title.trim()) {
52+
alert('Title is required');
53+
return;
54+
}
55+
56+
if (formData.expectedHours && (isNaN(formData.expectedHours) || formData.expectedHours < 0)) {
57+
alert('Expected hours must be a positive number');
58+
return;
59+
}
60+
61+
// Convert to backend format
62+
const submitData = {
63+
title: formData.title,
64+
description: formData.description,
65+
expectedHours: formData.expectedHours ? parseFloat(formData.expectedHours) : 0,
66+
status: formData.status,
67+
};
68+
69+
// Only set logged_hours to 0 when creating a new task (not editing)
70+
if (!task) {
71+
submitData.loggedHours = 0;
72+
}
73+
74+
// Only include dueDate if it's set
75+
if (formData.dueDate) {
76+
submitData.dueDate = new Date(formData.dueDate).toISOString();
77+
}
78+
79+
onSubmit(submitData);
80+
};
81+
82+
return (
83+
<Modal isOpen={true} toggle={onCancel} size="lg" className={styles.formModal}>
84+
<ModalHeader toggle={onCancel}>
85+
{task ? 'Edit Intermediate Task' : 'Add Intermediate Task'}
86+
</ModalHeader>
87+
<Form onSubmit={handleSubmit}>
88+
<ModalBody>
89+
<FormGroup>
90+
<Label for="title">Title *</Label>
91+
<Input
92+
type="text"
93+
name="title"
94+
id="title"
95+
value={formData.title}
96+
onChange={handleChange}
97+
required
98+
placeholder="Enter task title"
99+
/>
100+
</FormGroup>
101+
102+
<FormGroup>
103+
<Label for="description">Description</Label>
104+
<Input
105+
type="textarea"
106+
name="description"
107+
id="description"
108+
rows="4"
109+
value={formData.description}
110+
onChange={handleChange}
111+
placeholder="Enter task description"
112+
/>
113+
</FormGroup>
114+
115+
<div className={styles.formRow}>
116+
<FormGroup className={styles.formGroupHalf}>
117+
<Label for="expectedHours">Expected Hours</Label>
118+
<Input
119+
type="number"
120+
name="expectedHours"
121+
id="expectedHours"
122+
min="0"
123+
step="0.5"
124+
value={formData.expectedHours}
125+
onChange={handleChange}
126+
placeholder="0"
127+
/>
128+
</FormGroup>
129+
130+
<FormGroup className={styles.formGroupHalf}>
131+
<Label for="dueDate">Due Date</Label>
132+
<Input
133+
type="date"
134+
name="dueDate"
135+
id="dueDate"
136+
value={formData.dueDate}
137+
onChange={handleChange}
138+
/>
139+
</FormGroup>
140+
</div>
141+
142+
<FormGroup>
143+
<Label for="status">Status</Label>
144+
<Input
145+
type="select"
146+
name="status"
147+
id="status"
148+
value={formData.status}
149+
onChange={handleChange}
150+
>
151+
<option value="pending">Pending</option>
152+
<option value="in_progress">In Progress</option>
153+
{task && <option value="completed">Completed</option>}
154+
</Input>
155+
</FormGroup>
156+
</ModalBody>
157+
<ModalFooter>
158+
<Button color="secondary" onClick={onCancel}>
159+
Cancel
160+
</Button>
161+
<Button color="primary" type="submit">
162+
{task ? 'Update' : 'Create'} Task
163+
</Button>
164+
</ModalFooter>
165+
</Form>
166+
</Modal>
167+
);
168+
};
169+
170+
export default IntermediateTaskForm;

0 commit comments

Comments
 (0)