Skip to content
Merged
165 changes: 165 additions & 0 deletions src/actions/intermediateTasks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { toast } from 'react-toastify';
import { ENDPOINTS } from '~/utils/URL';
import httpService from '../services/httpService';
import { updateStudentTask } from './studentTasks';

/**
* Action types for intermediate tasks
*/
export const FETCH_INTERMEDIATE_TASKS_START = 'FETCH_INTERMEDIATE_TASKS_START';
export const FETCH_INTERMEDIATE_TASKS_SUCCESS = 'FETCH_INTERMEDIATE_TASKS_SUCCESS';
export const FETCH_INTERMEDIATE_TASKS_ERROR = 'FETCH_INTERMEDIATE_TASKS_ERROR';
export const CREATE_INTERMEDIATE_TASK_SUCCESS = 'CREATE_INTERMEDIATE_TASK_SUCCESS';
export const UPDATE_INTERMEDIATE_TASK_SUCCESS = 'UPDATE_INTERMEDIATE_TASK_SUCCESS';
export const DELETE_INTERMEDIATE_TASK_SUCCESS = 'DELETE_INTERMEDIATE_TASK_SUCCESS';
export const MARK_INTERMEDIATE_TASK_DONE = 'MARK_INTERMEDIATE_TASK_DONE';

/**
* Fetch intermediate tasks for a parent task
*/
export const fetchIntermediateTasks = (taskId) => {
return async (dispatch) => {
try {
const response = await httpService.get(ENDPOINTS.INTERMEDIATE_TASKS_BY_PARENT(taskId));
return response.data;
} catch (error) {
console.error('Error fetching intermediate tasks:', error);

Check warning on line 26 in src/actions/intermediateTasks.js

View workflow job for this annotation

GitHub Actions / test

Unexpected console statement
toast.error('Failed to fetch sub-tasks');
throw error;
}
};
};

/**
* Calculate total expected hours from intermediate tasks
*/
const calculateTotalExpectedHours = (intermediateTasks) => {
return intermediateTasks.reduce((total, task) => {
return total + (task.expected_hours || 0);
}, 0);
};

/**
* Update parent task's expected hours based on intermediate tasks
*/
const updateParentTaskExpectedHours = async (dispatch, getState, parentTaskId) => {
try {
// Fetch all intermediate tasks for this parent
const intermediateTasks = await dispatch(fetchIntermediateTasks(parentTaskId));

// Calculate total expected hours
const totalExpectedHours = calculateTotalExpectedHours(intermediateTasks);

// Get the parent task from state
const state = getState();
const parentTask = state.studentTasks.taskItems.find(t => t.id === parentTaskId);

if (parentTask) {
// Update the parent task with new expected hours
dispatch(updateStudentTask(parentTaskId, {
...parentTask,
suggested_total_hours: totalExpectedHours
}));
}
} catch (error) {
console.error('Error updating parent task expected hours:', error);

Check warning on line 65 in src/actions/intermediateTasks.js

View workflow job for this annotation

GitHub Actions / test

Unexpected console statement
}
};

/**
* Create a new intermediate task
*/
export const createIntermediateTask = (taskData) => {
return async (dispatch, getState) => {
try {
const response = await httpService.post(ENDPOINTS.INTERMEDIATE_TASKS(), taskData);
toast.success('Sub-task created successfully');

// Update parent task expected hours
if (taskData.parentTaskId) {
await updateParentTaskExpectedHours(dispatch, getState, taskData.parentTaskId);
}

return response.data;
} catch (error) {
console.error('Error creating intermediate task:', error);

Check warning on line 85 in src/actions/intermediateTasks.js

View workflow job for this annotation

GitHub Actions / test

Unexpected console statement
const errorMessage = error.response?.data?.error || error.message || 'Failed to create sub-task';
toast.error(`Error: ${errorMessage}`);
throw error;
}
};
};

/**
* Update an intermediate task
*/
export const updateIntermediateTask = (id, taskData) => {
return async (dispatch, getState) => {
try {
const response = await httpService.put(ENDPOINTS.INTERMEDIATE_TASK_BY_ID(id), taskData);
toast.success('Sub-task updated successfully');

// Update parent task expected hours
if (taskData.parentTaskId) {
await updateParentTaskExpectedHours(dispatch, getState, taskData.parentTaskId);
}

return response.data;
} catch (error) {
console.error('Error updating intermediate task:', error);

Check warning on line 109 in src/actions/intermediateTasks.js

View workflow job for this annotation

GitHub Actions / test

Unexpected console statement
const errorMessage = error.response?.data?.error || error.message || 'Failed to update sub-task';
toast.error(`Error: ${errorMessage}`);
throw error;
}
};
};

/**
* Delete an intermediate task
*/
export const deleteIntermediateTask = (id, parentTaskId = null) => {
return async (dispatch, getState) => {
try {
await httpService.delete(ENDPOINTS.INTERMEDIATE_TASK_BY_ID(id));
toast.success('Sub-task deleted successfully');

// Update parent task expected hours
if (parentTaskId) {
await updateParentTaskExpectedHours(dispatch, getState, parentTaskId);
}

return true;
} catch (error) {
console.error('Error deleting intermediate task:', error);

Check warning on line 133 in src/actions/intermediateTasks.js

View workflow job for this annotation

GitHub Actions / test

Unexpected console statement
const errorMessage = error.response?.data?.error || error.message || 'Failed to delete sub-task';
toast.error(`Error: ${errorMessage}`);
throw error;
}
};
};

/**
* Mark an intermediate task as done (for students)
*/
export const markIntermediateTaskAsDone = (id, parentTaskId) => {
return async (dispatch) => {
try {
// First, fetch the current task data
const currentTask = await httpService.get(ENDPOINTS.INTERMEDIATE_TASK_BY_ID(id));

// Update with the completed status while preserving all required fields
const response = await httpService.put(ENDPOINTS.INTERMEDIATE_TASK_BY_ID(id), {
...currentTask.data,
status: 'completed'
});
toast.success('Sub-task marked as done');
return response.data;
} catch (error) {
console.error('Error marking intermediate task as done:', error);

Check warning on line 158 in src/actions/intermediateTasks.js

View workflow job for this annotation

GitHub Actions / test

Unexpected console statement
const errorMessage = error.response?.data?.error || error.message || 'Failed to mark sub-task as done';
toast.error(`Error: ${errorMessage}`);
throw error;
}
};
};

Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import React, { useState, useEffect } from 'react';
import {
Button,
Form,
FormGroup,
Label,
Input,
Modal,
ModalHeader,
ModalBody,
ModalFooter,
} from 'reactstrap';
import styles from './IntermediateTaskList.module.css';

const IntermediateTaskForm = ({ task, onSubmit, onCancel }) => {

Check warning on line 15 in src/components/EductionPortal/IntermediateTasks/IntermediateTaskForm.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'task' is missing in props validation

See more on https://sonarcloud.io/project/issues?id=OneCommunityGlobal_HighestGoodNetworkApp&issues=AZq40ZRCkIFRl7cH34V1&open=AZq40ZRCkIFRl7cH34V1&pullRequest=4454

Check warning on line 15 in src/components/EductionPortal/IntermediateTasks/IntermediateTaskForm.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'onSubmit' is missing in props validation

See more on https://sonarcloud.io/project/issues?id=OneCommunityGlobal_HighestGoodNetworkApp&issues=AZq40ZRCkIFRl7cH34V2&open=AZq40ZRCkIFRl7cH34V2&pullRequest=4454
const [formData, setFormData] = useState({
title: '',
description: '',
expectedHours: '',
dueDate: '',
status: 'pending',
});

useEffect(() => {
if (task) {
setFormData({
title: task.title || '',

Check warning on line 27 in src/components/EductionPortal/IntermediateTasks/IntermediateTaskForm.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'task.title' is missing in props validation

See more on https://sonarcloud.io/project/issues?id=OneCommunityGlobal_HighestGoodNetworkApp&issues=AZq40ZRCkIFRl7cH34V4&open=AZq40ZRCkIFRl7cH34V4&pullRequest=4454
description: task.description || '',

Check warning on line 28 in src/components/EductionPortal/IntermediateTasks/IntermediateTaskForm.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'task.description' is missing in props validation

See more on https://sonarcloud.io/project/issues?id=OneCommunityGlobal_HighestGoodNetworkApp&issues=AZq40ZRCkIFRl7cH34V5&open=AZq40ZRCkIFRl7cH34V5&pullRequest=4454
expectedHours: task.expectedHours || task.expected_hours || '',

Check warning on line 29 in src/components/EductionPortal/IntermediateTasks/IntermediateTaskForm.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'task.expectedHours' is missing in props validation

See more on https://sonarcloud.io/project/issues?id=OneCommunityGlobal_HighestGoodNetworkApp&issues=AZq40ZRCkIFRl7cH34V6&open=AZq40ZRCkIFRl7cH34V6&pullRequest=4454

Check warning on line 29 in src/components/EductionPortal/IntermediateTasks/IntermediateTaskForm.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'task.expected_hours' is missing in props validation

See more on https://sonarcloud.io/project/issues?id=OneCommunityGlobal_HighestGoodNetworkApp&issues=AZq40ZRCkIFRl7cH34V7&open=AZq40ZRCkIFRl7cH34V7&pullRequest=4454
dueDate:
task.dueDate || task.due_date

Check warning on line 31 in src/components/EductionPortal/IntermediateTasks/IntermediateTaskForm.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'task.due_date' is missing in props validation

See more on https://sonarcloud.io/project/issues?id=OneCommunityGlobal_HighestGoodNetworkApp&issues=AZq40ZRCkIFRl7cH34V9&open=AZq40ZRCkIFRl7cH34V9&pullRequest=4454
? new Date(task.dueDate || task.due_date).toISOString().split('T')[0]

Check warning on line 32 in src/components/EductionPortal/IntermediateTasks/IntermediateTaskForm.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'task.dueDate' is missing in props validation

See more on https://sonarcloud.io/project/issues?id=OneCommunityGlobal_HighestGoodNetworkApp&issues=AZq40ZRCkIFRl7cH34V-&open=AZq40ZRCkIFRl7cH34V-&pullRequest=4454
: '',
status: task.status || 'pending',

Check warning on line 34 in src/components/EductionPortal/IntermediateTasks/IntermediateTaskForm.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'task.status' is missing in props validation

See more on https://sonarcloud.io/project/issues?id=OneCommunityGlobal_HighestGoodNetworkApp&issues=AZq40ZRCkIFRl7cH34WA&open=AZq40ZRCkIFRl7cH34WA&pullRequest=4454
});
}
}, [task]);

const handleChange = e => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value,
}));
};

const handleSubmit = e => {
e.preventDefault();

// Validation
if (!formData.title.trim()) {
alert('Title is required');
return;
}

if (formData.expectedHours && (isNaN(formData.expectedHours) || formData.expectedHours < 0)) {
alert('Expected hours must be a positive number');
return;
}

// Convert to backend format
const submitData = {
title: formData.title,
description: formData.description,
expectedHours: formData.expectedHours ? parseFloat(formData.expectedHours) : 0,
status: formData.status,
};

// Only set logged_hours to 0 when creating a new task (not editing)
if (!task) {
submitData.loggedHours = 0;
}

// Only include dueDate if it's set
if (formData.dueDate) {
submitData.dueDate = new Date(formData.dueDate).toISOString();
}

onSubmit(submitData);
};

return (
<Modal isOpen={true} toggle={onCancel} size="lg" className={styles.formModal}>
<ModalHeader toggle={onCancel}>
{task ? 'Edit Intermediate Task' : 'Add Intermediate Task'}
</ModalHeader>
<Form onSubmit={handleSubmit}>
<ModalBody>
<FormGroup>
<Label for="title">Title *</Label>
<Input
type="text"
name="title"
id="title"
value={formData.title}
onChange={handleChange}
required
placeholder="Enter task title"
/>
</FormGroup>

<FormGroup>
<Label for="description">Description</Label>
<Input
type="textarea"
name="description"
id="description"
rows="4"
value={formData.description}
onChange={handleChange}
placeholder="Enter task description"
/>
</FormGroup>

<div className={styles.formRow}>
<FormGroup className={styles.formGroupHalf}>
<Label for="expectedHours">Expected Hours</Label>
<Input
type="number"
name="expectedHours"
id="expectedHours"
min="0"
step="0.5"
value={formData.expectedHours}
onChange={handleChange}
placeholder="0"
/>
</FormGroup>

<FormGroup className={styles.formGroupHalf}>
<Label for="dueDate">Due Date</Label>
<Input
type="date"
name="dueDate"
id="dueDate"
value={formData.dueDate}
onChange={handleChange}
/>
</FormGroup>
</div>

<FormGroup>
<Label for="status">Status</Label>
<Input
type="select"
name="status"
id="status"
value={formData.status}
onChange={handleChange}
>
<option value="pending">Pending</option>
<option value="in_progress">In Progress</option>
{task && <option value="completed">Completed</option>}
</Input>
</FormGroup>
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={onCancel}>
Cancel
</Button>
<Button color="primary" type="submit">
{task ? 'Update' : 'Create'} Task
</Button>
</ModalFooter>
</Form>
</Modal>
);
};

export default IntermediateTaskForm;
Loading
Loading