diff --git a/src/actions/intermediateTasks.js b/src/actions/intermediateTasks.js
new file mode 100644
index 0000000000..8521a9f9b0
--- /dev/null
+++ b/src/actions/intermediateTasks.js
@@ -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);
+ 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);
+ }
+};
+
+/**
+ * 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);
+ 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);
+ 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);
+ 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);
+ const errorMessage = error.response?.data?.error || error.message || 'Failed to mark sub-task as done';
+ toast.error(`Error: ${errorMessage}`);
+ throw error;
+ }
+ };
+};
+
diff --git a/src/components/EductionPortal/IntermediateTasks/IntermediateTaskForm.jsx b/src/components/EductionPortal/IntermediateTasks/IntermediateTaskForm.jsx
new file mode 100644
index 0000000000..e9510b3226
--- /dev/null
+++ b/src/components/EductionPortal/IntermediateTasks/IntermediateTaskForm.jsx
@@ -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 }) => {
+ const [formData, setFormData] = useState({
+ title: '',
+ description: '',
+ expectedHours: '',
+ dueDate: '',
+ status: 'pending',
+ });
+
+ useEffect(() => {
+ if (task) {
+ setFormData({
+ title: task.title || '',
+ description: task.description || '',
+ expectedHours: task.expectedHours || task.expected_hours || '',
+ dueDate:
+ task.dueDate || task.due_date
+ ? new Date(task.dueDate || task.due_date).toISOString().split('T')[0]
+ : '',
+ status: task.status || 'pending',
+ });
+ }
+ }, [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 (
+
+
+ {task ? 'Edit Intermediate Task' : 'Add Intermediate Task'}
+
+
+
+ );
+};
+
+export default IntermediateTaskForm;
diff --git a/src/components/EductionPortal/IntermediateTasks/IntermediateTaskList.jsx b/src/components/EductionPortal/IntermediateTasks/IntermediateTaskList.jsx
new file mode 100644
index 0000000000..32c2d08232
--- /dev/null
+++ b/src/components/EductionPortal/IntermediateTasks/IntermediateTaskList.jsx
@@ -0,0 +1,376 @@
+import React, { useState, useEffect } from 'react';
+import { Container, Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
+import { useSelector, useDispatch } from 'react-redux';
+import { toast } from 'react-toastify';
+import styles from './IntermediateTaskList.module.css';
+import IntermediateTaskForm from './IntermediateTaskForm';
+import {
+ fetchIntermediateTasks,
+ createIntermediateTask,
+ updateIntermediateTask,
+ deleteIntermediateTask,
+ markIntermediateTaskAsDone,
+} from '~/actions/intermediateTasks';
+import { fetchStudentTasks } from '~/actions/studentTasks';
+
+const IntermediateTaskList = () => {
+ const dispatch = useDispatch();
+ const authUser = useSelector(state => state.auth.user);
+ const { taskItems: parentTasks } = useSelector(state => state.studentTasks);
+
+ const [expandedTasks, setExpandedTasks] = useState({});
+ const [intermediateTasks, setIntermediateTasks] = useState({});
+ const [loadingTasks, setLoadingTasks] = useState({});
+ const [showForm, setShowForm] = useState(false);
+ const [editingTask, setEditingTask] = useState(null);
+ const [selectedParentTaskId, setSelectedParentTaskId] = useState(null);
+ const [isEducator, setIsEducator] = useState(false);
+ const [deleteModal, setDeleteModal] = useState({ isOpen: false, task: null, parentTaskId: null });
+
+ // Check if user is educator (you may need to adjust this based on your role system)
+ useEffect(() => {
+ // Check user role - adjust based on your role system
+ const userRole = authUser?.role;
+ setIsEducator(userRole === 'Administrator' || userRole === 'Owner' || userRole === 'Manager');
+ }, [authUser]);
+
+ // Fetch parent tasks on mount
+ useEffect(() => {
+ dispatch(fetchStudentTasks());
+ }, [dispatch]);
+
+ // Toggle expand/collapse for a task
+ const toggleTask = async taskId => {
+ const isExpanded = expandedTasks[taskId];
+
+ if (!isExpanded && !intermediateTasks[taskId]) {
+ // Fetch intermediate tasks when expanding
+ setLoadingTasks(prev => ({ ...prev, [taskId]: true }));
+ try {
+ const tasks = await dispatch(fetchIntermediateTasks(taskId));
+ setIntermediateTasks(prev => ({ ...prev, [taskId]: tasks || [] }));
+ } catch (error) {
+ console.error('Error fetching intermediate tasks:', error);
+ } finally {
+ setLoadingTasks(prev => ({ ...prev, [taskId]: false }));
+ }
+ }
+
+ setExpandedTasks(prev => ({
+ ...prev,
+ [taskId]: !isExpanded,
+ }));
+ };
+
+ // Handle add intermediate task
+ const handleAddTask = parentTaskId => {
+ setSelectedParentTaskId(parentTaskId);
+ setEditingTask(null);
+ setShowForm(true);
+ };
+
+ // Handle edit intermediate task
+ const handleEditTask = (task, parentTaskId) => {
+ setSelectedParentTaskId(parentTaskId);
+ // Ensure task has id field for consistency
+ const taskWithId = { ...task, id: task.id || task._id };
+ setEditingTask(taskWithId);
+ setShowForm(true);
+ };
+
+ // Handle delete intermediate task - open confirmation modal
+ const handleDeleteTask = (task, parentTaskId) => {
+ setDeleteModal({ isOpen: true, task, parentTaskId });
+ };
+
+ // Confirm deletion
+ const confirmDelete = async () => {
+ const { task, parentTaskId } = deleteModal;
+ try {
+ const taskId = task.id || task._id;
+ await dispatch(deleteIntermediateTask(taskId, parentTaskId));
+ // Refresh intermediate tasks for the parent
+ const tasks = await dispatch(fetchIntermediateTasks(parentTaskId));
+ setIntermediateTasks(prev => ({ ...prev, [parentTaskId]: tasks || [] }));
+ setDeleteModal({ isOpen: false, task: null, parentTaskId: null });
+ } catch (error) {
+ // Error is handled in the action
+ setDeleteModal({ isOpen: false, task: null, parentTaskId: null });
+ }
+ };
+
+ // Cancel deletion
+ const cancelDelete = () => {
+ setDeleteModal({ isOpen: false, task: null, parentTaskId: null });
+ };
+
+ // Handle mark as done (for students)
+ const handleMarkAsDone = async (task, parentTaskId) => {
+ try {
+ const taskId = task.id || task._id;
+ await dispatch(markIntermediateTaskAsDone(taskId));
+ // Refresh intermediate tasks for the parent
+ const tasks = await dispatch(fetchIntermediateTasks(parentTaskId));
+ setIntermediateTasks(prev => ({ ...prev, [parentTaskId]: tasks || [] }));
+ } catch (error) {
+ // Error is handled in the action
+ }
+ };
+
+ // Handle form submit
+ const handleFormSubmit = async formData => {
+ try {
+ if (editingTask) {
+ const taskId = editingTask.id || editingTask._id;
+ await dispatch(
+ updateIntermediateTask(taskId, {
+ ...formData,
+ parentTaskId: selectedParentTaskId,
+ }),
+ );
+ } else {
+ await dispatch(
+ createIntermediateTask({
+ ...formData,
+ parentTaskId: selectedParentTaskId,
+ }),
+ );
+ }
+
+ // Refresh intermediate tasks for the parent
+ const tasks = await dispatch(fetchIntermediateTasks(selectedParentTaskId));
+ setIntermediateTasks(prev => ({ ...prev, [selectedParentTaskId]: tasks || [] }));
+
+ setShowForm(false);
+ setEditingTask(null);
+ setSelectedParentTaskId(null);
+ } catch (error) {
+ // Error is handled in the action
+ }
+ };
+
+ // Calculate progress for a parent task based on intermediate tasks
+ const calculateProgress = parentTaskId => {
+ const tasks = intermediateTasks[parentTaskId] || [];
+ if (tasks.length === 0) return 0;
+ const completedCount = tasks.filter(t => t.status === 'completed').length;
+ return Math.round((completedCount / tasks.length) * 100);
+ };
+
+ return (
+
+
+
+
Intermediate Tasks
+
Manage sub-tasks for main tasks
+
+
+ {showForm && (
+ {
+ setShowForm(false);
+ setEditingTask(null);
+ setSelectedParentTaskId(null);
+ }}
+ />
+ )}
+
+
+ {!parentTasks || parentTasks.length === 0 ? (
+
+
No tasks found. Tasks will appear here once assigned.
+
+ ) : (
+ parentTasks.map(parentTask => {
+ const isExpanded = expandedTasks[parentTask.id];
+ const subTasks = intermediateTasks[parentTask.id] || [];
+ const isLoading = loadingTasks[parentTask.id];
+ const progress = calculateProgress(parentTask.id);
+
+ return (
+
+
+
+
+ {parentTask.course_name || parentTask.title}
+
+ {parentTask.subtitle && (
+
{parentTask.subtitle}
+ )}
+
+
+ Status: {parentTask.status || 'assigned'}
+
+ {subTasks.length > 0 && (
+
+ Progress: {progress}% (
+ {subTasks.filter(t => t.status === 'completed').length}/
+ {subTasks.length} completed)
+
+ )}
+
+
+
+ {isEducator && (
+
+ )}
+
+
+
+
+ {isExpanded && (
+
+ {isLoading ? (
+
+
+
Loading sub-tasks...
+
+ ) : subTasks.length === 0 ? (
+
+
+ No sub-tasks found.{' '}
+ {isEducator && 'Click "Add Intermediate Task" to create one.'}
+
+
+ ) : (
+
+ {subTasks.map(subTask => (
+
+
+
{subTask.title}
+ {subTask.description && (
+
{subTask.description}
+ )}
+
+
+ Expected: {subTask.expected_hours || 0}h
+
+ {subTask.due_date && (
+
+ Due: {new Date(subTask.due_date).toLocaleDateString()}
+
+ )}
+
+ {subTask.status || 'pending'}
+
+
+
+
+ {isEducator && (
+ <>
+
+
+ >
+ )}
+ {!isEducator && subTask.status !== 'completed' && (
+
+ )}
+ {!isEducator && subTask.status === 'completed' && (
+
Completed
+ )}
+
+
+ ))}
+
+ )}
+
+ )}
+
+ );
+ })
+ )}
+
+
+ {/* Delete Confirmation Modal */}
+
+ Confirm Deletion
+
+ Are you sure you want to delete this sub-task?
+ {deleteModal.task && (
+
+ {deleteModal.task.title}
+
+ )}
+ This action cannot be undone.
+
+
+
+
+
+
+
+
+ );
+};
+
+export default IntermediateTaskList;
diff --git a/src/components/EductionPortal/IntermediateTasks/IntermediateTaskList.module.css b/src/components/EductionPortal/IntermediateTasks/IntermediateTaskList.module.css
new file mode 100644
index 0000000000..02de19b1c1
--- /dev/null
+++ b/src/components/EductionPortal/IntermediateTasks/IntermediateTaskList.module.css
@@ -0,0 +1,366 @@
+.dashboard {
+ min-height: 100vh;
+ background-color: #f8f9fa;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
+ padding: 2rem 0;
+}
+
+.mainContainer {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 0 1rem;
+}
+
+.header {
+ text-align: center;
+ margin-bottom: 3rem;
+}
+
+.title {
+ font-size: 2.5rem;
+ font-weight: 700;
+ color: #1a1a1a;
+ margin-bottom: 0.5rem;
+ line-height: 1.2;
+}
+
+.subtitle {
+ font-size: 1.125rem;
+ color: #6b7280;
+ margin: 0;
+ font-weight: 400;
+}
+
+.tasksSection {
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+}
+
+.taskCard {
+ background-color: #ffffff;
+ border-radius: 12px;
+ padding: 1.5rem;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ border: 1px solid #e5e7eb;
+ transition: all 0.2s ease;
+}
+
+.taskCard:hover {
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+}
+
+.taskHeader {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: 1rem;
+}
+
+.taskHeaderContent {
+ flex: 1;
+}
+
+.taskTitle {
+ font-size: 1.25rem;
+ font-weight: 600;
+ color: #1a1a1a;
+ margin: 0 0 0.5rem 0;
+ line-height: 1.3;
+}
+
+.taskSubtitle {
+ font-size: 0.875rem;
+ color: #6b7280;
+ margin: 0 0 0.75rem 0;
+ line-height: 1.4;
+}
+
+.taskMeta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1rem;
+ font-size: 0.875rem;
+ color: #6b7280;
+}
+
+.taskStatus {
+ font-weight: 500;
+ color: #374151;
+}
+
+.progressInfo {
+ color: #3b82f6;
+ font-weight: 500;
+}
+
+.taskActions {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+}
+
+.addButton {
+ white-space: nowrap;
+}
+
+.expandButton {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 40px;
+ height: 40px;
+ border: 1px solid #d1d5db;
+ background-color: #ffffff;
+ border-radius: 8px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ color: #6b7280;
+}
+
+.expandButton:hover {
+ background-color: #f9fafb;
+ border-color: #9ca3af;
+ color: #374151;
+}
+
+.expandButton svg {
+ transition: transform 0.2s ease;
+}
+
+.expandButton.expanded svg {
+ transform: rotate(180deg);
+}
+
+.subTasksContainer {
+ margin-top: 1.5rem;
+ padding-top: 1.5rem;
+ border-top: 1px solid #e5e7eb;
+}
+
+.loadingState {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 2rem;
+ color: #6b7280;
+}
+
+.spinner {
+ width: 40px;
+ height: 40px;
+ border: 4px solid #e5e7eb;
+ border-top: 4px solid #3b82f6;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+ margin-bottom: 1rem;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+.emptySubTasks {
+ text-align: center;
+ padding: 2rem;
+ color: #6b7280;
+}
+
+.subTasksList {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.subTaskItem {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: 1rem;
+ padding: 1rem;
+ background-color: #f9fafb;
+ border-radius: 8px;
+ border: 1px solid #e5e7eb;
+ transition: all 0.2s ease;
+}
+
+.subTaskItem:hover {
+ background-color: #f3f4f6;
+ border-color: #d1d5db;
+}
+
+.subTaskContent {
+ flex: 1;
+}
+
+.subTaskTitle {
+ font-size: 1rem;
+ font-weight: 600;
+ color: #1a1a1a;
+ margin: 0 0 0.5rem 0;
+}
+
+.subTaskDescription {
+ font-size: 0.875rem;
+ color: #6b7280;
+ margin: 0 0 0.75rem 0;
+ line-height: 1.5;
+}
+
+.subTaskMeta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1rem;
+ font-size: 0.875rem;
+ color: #6b7280;
+}
+
+.subTaskHours {
+ font-weight: 500;
+ color: #374151;
+}
+
+.subTaskDueDate {
+ color: #6b7280;
+}
+
+.subTaskStatus {
+ padding: 0.25rem 0.75rem;
+ border-radius: 9999px;
+ font-size: 0.75rem;
+ font-weight: 500;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+.statuspending {
+ background-color: #e5e7eb;
+ color: #374151;
+}
+
+.statusin_progress {
+ background-color: #dbeafe;
+ color: #1e40af;
+}
+
+.statuscompleted {
+ background-color: #dcfce7;
+ color: #166534;
+}
+
+.subTaskActions {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.editButton,
+.deleteButton {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 36px;
+ height: 36px;
+ border: 1px solid #d1d5db;
+ background-color: #ffffff;
+ border-radius: 6px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ color: #6b7280;
+}
+
+.editButton:hover {
+ background-color: #eff6ff;
+ border-color: #3b82f6;
+ color: #3b82f6;
+}
+
+.deleteButton:hover {
+ background-color: #fef2f2;
+ border-color: #ef4444;
+ color: #ef4444;
+}
+
+.markDoneButton {
+ padding: 0.5rem 1rem;
+ border: none;
+ background-color: #3b82f6;
+ color: #ffffff;
+ border-radius: 6px;
+ font-size: 0.875rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.markDoneButton:hover {
+ background-color: #2563eb;
+}
+
+.completedBadge {
+ padding: 0.5rem 1rem;
+ background-color: #dcfce7;
+ color: #166534;
+ border-radius: 6px;
+ font-size: 0.875rem;
+ font-weight: 500;
+}
+
+.emptyState {
+ text-align: center;
+ padding: 4rem 2rem;
+ color: #6b7280;
+ background-color: #ffffff;
+ border-radius: 12px;
+ border: 1px solid #e5e7eb;
+}
+
+/* Form Modal Styles */
+.formModal {
+ z-index: 1050;
+}
+
+.formRow {
+ display: flex;
+ gap: 1rem;
+}
+
+.formGroupHalf {
+ flex: 1;
+}
+
+/* Responsive Design */
+@media (max-width: 768px) {
+ .mainContainer {
+ padding: 0 0.5rem;
+ }
+
+ .title {
+ font-size: 2rem;
+ }
+
+ .taskHeader {
+ flex-direction: column;
+ }
+
+ .taskActions {
+ width: 100%;
+ justify-content: space-between;
+ }
+
+ .subTaskItem {
+ flex-direction: column;
+ }
+
+ .subTaskActions {
+ width: 100%;
+ justify-content: flex-end;
+ }
+
+ .formRow {
+ flex-direction: column;
+ }
+}
+
diff --git a/src/components/EductionPortal/IntermediateTasks/index.js b/src/components/EductionPortal/IntermediateTasks/index.js
new file mode 100644
index 0000000000..476d30e8ff
--- /dev/null
+++ b/src/components/EductionPortal/IntermediateTasks/index.js
@@ -0,0 +1,2 @@
+export { default } from './IntermediateTaskList';
+export { default as IntermediateTaskForm } from './IntermediateTaskForm';
diff --git a/src/components/EductionPortal/StudentDashboard/IntermediateTasksList.jsx b/src/components/EductionPortal/StudentDashboard/IntermediateTasksList.jsx
new file mode 100644
index 0000000000..29d923f853
--- /dev/null
+++ b/src/components/EductionPortal/StudentDashboard/IntermediateTasksList.jsx
@@ -0,0 +1,73 @@
+import React from 'react';
+import { canMarkIntermediateTaskAsDone, getMarkIntermediateAsDoneTooltip } from './taskUtils';
+
+const IntermediateTasksList = ({ intermediateTasks, styles, onMarkIntermediateAsDone }) => {
+ if (intermediateTasks.length === 0) {
+ return No sub-tasks available
;
+ }
+
+ return intermediateTasks.map(subTask => {
+ const subTaskProgress = subTask.status === 'completed' ? 100 : 0;
+ const canMarkIntermediateDone = canMarkIntermediateTaskAsDone(subTask);
+ const intermediateTooltip = getMarkIntermediateAsDoneTooltip(subTask);
+
+ return (
+
+
+
{subTask.title}
+ {subTask.description && (
+
{subTask.description}
+ )}
+ {/* Progress Bar for Sub-task */}
+
+
+
+ {subTask.logged_hours || 0} / {subTask.expected_hours || 0}h
+
+ {subTask.due_date && (
+
+ Due: {new Date(subTask.due_date).toLocaleDateString()}
+
+ )}
+
+ {subTask.status || 'pending'}
+
+
+
+ {subTask.status !== 'completed' && (
+
+ )}
+
+ );
+ });
+};
+
+export default IntermediateTasksList;
diff --git a/src/components/EductionPortal/StudentDashboard/MarkAsDoneButton.jsx b/src/components/EductionPortal/StudentDashboard/MarkAsDoneButton.jsx
new file mode 100644
index 0000000000..5e95c72269
--- /dev/null
+++ b/src/components/EductionPortal/StudentDashboard/MarkAsDoneButton.jsx
@@ -0,0 +1,62 @@
+import React from 'react';
+
+const MarkAsDoneButton = ({
+ task,
+ intermediateTasks,
+ canMarkDone,
+ markAsDoneTooltip,
+ onMarkAsDone,
+ styles,
+ iconSize = '16',
+}) => {
+ const handleMarkAsDone = () => {
+ if (canMarkDone) {
+ onMarkAsDone(task.id);
+ }
+ };
+
+ // Show completed only if task is completed AND (no subtasks OR all subtasks are completed)
+ if (
+ task.is_completed &&
+ (intermediateTasks.length === 0 || intermediateTasks.every(t => t.status === 'completed'))
+ ) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+};
+
+export default MarkAsDoneButton;
diff --git a/src/components/EductionPortal/StudentDashboard/StudentDashboard.jsx b/src/components/EductionPortal/StudentDashboard/StudentDashboard.jsx
index 52e43d900f..c4c3a14b2a 100644
--- a/src/components/EductionPortal/StudentDashboard/StudentDashboard.jsx
+++ b/src/components/EductionPortal/StudentDashboard/StudentDashboard.jsx
@@ -8,6 +8,7 @@ import TaskListView from './TaskListView';
import NavigationBar from './NavigationBar';
import SummaryCards from './SummaryCards';
import { fetchStudentTasks, markStudentTaskAsDone } from '~/actions/studentTasks';
+import { fetchIntermediateTasks, markIntermediateTaskAsDone } from '~/actions/intermediateTasks';
const StudentDashboard = () => {
const [viewMode, setViewMode] = useState('card'); // 'card' or 'list'
@@ -17,6 +18,8 @@ const StudentDashboard = () => {
activeCourses: 0,
logEntries: 0,
});
+ const [intermediateTasks, setIntermediateTasks] = useState({});
+ const [expandedTasks, setExpandedTasks] = useState({});
const dispatch = useDispatch();
const authUser = useSelector(state => state.auth.user);
@@ -27,6 +30,31 @@ const StudentDashboard = () => {
dispatch(fetchStudentTasks());
}, [dispatch]);
+ // Fetch intermediate tasks for all parent tasks
+ useEffect(() => {
+ const fetchAllIntermediateTasks = async () => {
+ if (tasks && tasks.length > 0) {
+ const intermediateTasksData = {};
+
+ // Fetch intermediate tasks for each parent task
+ for (const task of tasks) {
+ try {
+ const subTasks = await dispatch(fetchIntermediateTasks(task.id));
+ if (subTasks && subTasks.length > 0) {
+ intermediateTasksData[task.id] = subTasks;
+ }
+ } catch (error) {
+ console.error(`Error fetching intermediate tasks for task ${task.id}:`, error);
+ }
+ }
+
+ setIntermediateTasks(intermediateTasksData);
+ }
+ };
+
+ fetchAllIntermediateTasks();
+ }, [tasks, dispatch]);
+
// Calculate summary data when tasks change
useEffect(() => {
if (tasks && tasks.length > 0) {
@@ -72,6 +100,29 @@ const StudentDashboard = () => {
dispatch(markStudentTaskAsDone(taskId));
};
+ // Handle mark intermediate task as done
+ const handleMarkIntermediateAsDone = async (intermediateTaskId, parentTaskId) => {
+ try {
+ await dispatch(markIntermediateTaskAsDone(intermediateTaskId));
+ // Refresh intermediate tasks for this parent
+ const tasks = await dispatch(fetchIntermediateTasks(parentTaskId));
+ setIntermediateTasks(prev => ({ ...prev, [parentTaskId]: tasks || [] }));
+ } catch (error) {
+ // Error is handled in the action
+ }
+ };
+
+ // Toggle expand/collapse intermediate tasks
+ const toggleIntermediateTasks = async taskId => {
+ const isExpanded = expandedTasks[taskId];
+
+ // Just toggle the expanded state (tasks are already loaded)
+ setExpandedTasks(prev => ({
+ ...prev,
+ [taskId]: !isExpanded,
+ }));
+ };
+
// Toggle view mode
const toggleViewMode = () => {
setViewMode(prev => (prev === 'card' ? 'list' : 'card'));
@@ -161,9 +212,23 @@ const StudentDashboard = () => {
{/* Task Views */}
{viewMode === 'card' ? (
-
+
) : (
-
+
)}
diff --git a/src/components/EductionPortal/StudentDashboard/TaskCard.jsx b/src/components/EductionPortal/StudentDashboard/TaskCard.jsx
index 9e359be1df..e2b90b1ff4 100644
--- a/src/components/EductionPortal/StudentDashboard/TaskCard.jsx
+++ b/src/components/EductionPortal/StudentDashboard/TaskCard.jsx
@@ -1,8 +1,17 @@
import React from 'react';
import styles from './TaskCard.module.css';
import { useTaskLogic } from './useTaskLogic';
+import MarkAsDoneButton from './MarkAsDoneButton';
+import IntermediateTasksList from './IntermediateTasksList';
-const TaskCard = ({ task, onMarkAsDone }) => {
+const TaskCard = ({
+ task,
+ onMarkAsDone,
+ intermediateTasks = [],
+ isExpanded = false,
+ onToggleIntermediateTasks,
+ onMarkIntermediateAsDone,
+}) => {
const {
progressPercentage,
canMarkDone,
@@ -10,11 +19,17 @@ const TaskCard = ({ task, onMarkAsDone }) => {
markAsDoneTooltip,
formattedTimeAndDate,
progressText,
- } = useTaskLogic(task, styles);
+ } = useTaskLogic(task, styles, intermediateTasks);
- const handleMarkAsDone = () => {
- if (canMarkDone) {
- onMarkAsDone(task.id);
+ const handleToggleIntermediateTasks = () => {
+ if (onToggleIntermediateTasks) {
+ onToggleIntermediateTasks(task.id);
+ }
+ };
+
+ const handleMarkIntermediateAsDone = intermediateTaskId => {
+ if (onMarkIntermediateAsDone) {
+ onMarkIntermediateAsDone(intermediateTaskId, task.id);
}
};
@@ -75,26 +90,23 @@ const TaskCard = ({ task, onMarkAsDone }) => {
- {task.is_completed ? (
-
- ) : (
+
+
+
+ {/* Intermediate Tasks Section */}
+ {onToggleIntermediateTasks && (
+
- )}
-
+
+ {isExpanded && (
+
+
+
+ )}
+
+ )}
);
diff --git a/src/components/EductionPortal/StudentDashboard/TaskCard.module.css b/src/components/EductionPortal/StudentDashboard/TaskCard.module.css
index ea08706134..2b1a43b061 100644
--- a/src/components/EductionPortal/StudentDashboard/TaskCard.module.css
+++ b/src/components/EductionPortal/StudentDashboard/TaskCard.module.css
@@ -180,19 +180,205 @@
cursor: default;
}
+/* Intermediate Tasks Styles */
+.intermediateTasksSection {
+ margin-top: 1rem;
+ padding-top: 1rem;
+ border-top: 1px solid #e5e7eb;
+}
+
+.toggleIntermediateButton {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.5rem;
+ background: transparent;
+ border: none;
+ color: #3b82f6;
+ font-size: 0.875rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ width: 100%;
+ justify-content: center;
+}
+
+.toggleIntermediateButton:hover {
+ color: #2563eb;
+ background-color: #eff6ff;
+ border-radius: 6px;
+}
+
+.expandedIcon {
+ transform: rotate(180deg);
+}
+
+.intermediateTasksList {
+ margin-top: 0.75rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.noIntermediateTasks {
+ text-align: center;
+ color: #9ca3af;
+ font-size: 0.875rem;
+ padding: 1rem;
+ margin: 0;
+}
+
+.intermediateTaskItem {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ padding: 0.75rem;
+ background-color: #f9fafb;
+ border-radius: 8px;
+ border: 1px solid #e5e7eb;
+ gap: 0.75rem;
+ transition: all 0.2s ease;
+}
+
+.intermediateTaskItem:hover {
+ background-color: #f3f4f6;
+}
+
+.intermediateTaskContent {
+ flex: 1;
+}
+
+.intermediateTaskTitle {
+ font-size: 0.9rem;
+ font-weight: 600;
+ color: #1f2937;
+ margin: 0 0 0.25rem 0;
+}
+
+.intermediateTaskDescription {
+ font-size: 0.8rem;
+ color: #6b7280;
+ margin: 0 0 0.5rem 0;
+ line-height: 1.4;
+}
+
+.subTaskProgressSection {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin-bottom: 0.5rem;
+}
+
+.subTaskProgressBar {
+ flex: 1;
+ height: 6px;
+ background-color: #e5e7eb;
+ border-radius: 3px;
+ overflow: hidden;
+}
+
+.subTaskProgressFill {
+ height: 100%;
+ background-color: #10b981;
+ border-radius: 3px;
+ transition: width 0.3s ease;
+}
+
+.subTaskProgressText {
+ font-size: 0.75rem;
+ font-weight: 600;
+ color: #6b7280;
+ min-width: 35px;
+ text-align: right;
+}
+
+.intermediateTaskMeta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.75rem;
+ font-size: 0.75rem;
+ color: #6b7280;
+}
+
+.intermediateTaskHours {
+ font-weight: 500;
+}
+
+.intermediateTaskStatus {
+ padding: 0.125rem 0.5rem;
+ border-radius: 9999px;
+ font-weight: 500;
+ text-transform: capitalize;
+ background-color: #e5e7eb;
+ color: #374151;
+}
+
+.intermediateTaskStatus.statuscompleted {
+ background-color: #dcfce7;
+ color: #166534;
+}
+
+.intermediateTaskStatus.statuspending {
+ background-color: #fef3c7;
+ color: #92400e;
+}
+
+.markIntermediateDoneButton {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 32px;
+ height: 32px;
+ border: 1px solid #d1d5db;
+ background-color: #ffffff;
+ border-radius: 6px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ color: #10b981;
+ flex-shrink: 0;
+}
+
+.markIntermediateDoneButton:hover:not(.disabled) {
+ background-color: #10b981;
+ border-color: #10b981;
+ color: #ffffff;
+}
+
+.markIntermediateDoneButton.disabled {
+ background-color: #f3f4f6;
+ border-color: #e5e7eb;
+ color: #d1d5db;
+ cursor: not-allowed;
+ opacity: 0.6;
+}
+
+.markIntermediateDoneButton.disabled:hover {
+ background-color: #f3f4f6;
+ border-color: #e5e7eb;
+ color: #d1d5db;
+}
+
/* Responsive Design */
@media (max-width: 768px) {
.taskCard {
padding: 1.25rem;
min-height: 260px;
}
-
+
.actionButtons {
flex-direction: column;
gap: 0.5rem;
}
-
+
.clockButton {
width: 100%;
}
+
+ .intermediateTaskItem {
+ flex-direction: column;
+ }
+
+ .markIntermediateDoneButton {
+ width: 100%;
+ }
}
diff --git a/src/components/EductionPortal/StudentDashboard/TaskCardView.jsx b/src/components/EductionPortal/StudentDashboard/TaskCardView.jsx
index 57b2f4ee70..6638d63fe4 100644
--- a/src/components/EductionPortal/StudentDashboard/TaskCardView.jsx
+++ b/src/components/EductionPortal/StudentDashboard/TaskCardView.jsx
@@ -2,7 +2,14 @@ import React from 'react';
import styles from './TaskCardView.module.css';
import TaskCard from './TaskCard';
-const TaskCardView = ({ tasks, onMarkAsDone }) => {
+const TaskCardView = ({
+ tasks,
+ onMarkAsDone,
+ intermediateTasks,
+ expandedTasks,
+ onToggleIntermediateTasks,
+ onMarkIntermediateAsDone,
+}) => {
if (!tasks || tasks.length === 0) {
return (
@@ -14,7 +21,15 @@ const TaskCardView = ({ tasks, onMarkAsDone }) => {
return (
{tasks.map(task => (
-
+
))}
);
diff --git a/src/components/EductionPortal/StudentDashboard/TaskListItem.jsx b/src/components/EductionPortal/StudentDashboard/TaskListItem.jsx
index 90769725b6..4347ab6e5b 100644
--- a/src/components/EductionPortal/StudentDashboard/TaskListItem.jsx
+++ b/src/components/EductionPortal/StudentDashboard/TaskListItem.jsx
@@ -1,8 +1,17 @@
import React from 'react';
import styles from './TaskListItem.module.css';
import { useTaskLogic } from './useTaskLogic';
+import MarkAsDoneButton from './MarkAsDoneButton';
+import IntermediateTasksList from './IntermediateTasksList';
-const TaskListItem = ({ task, onMarkAsDone }) => {
+const TaskListItem = ({
+ task,
+ onMarkAsDone,
+ intermediateTasks = [],
+ isExpanded = false,
+ onToggleIntermediateTasks,
+ onMarkIntermediateAsDone,
+}) => {
const {
progressPercentage,
canMarkDone,
@@ -10,11 +19,17 @@ const TaskListItem = ({ task, onMarkAsDone }) => {
markAsDoneTooltip,
formattedTimeAndDate,
progressText,
- } = useTaskLogic(task, styles);
+ } = useTaskLogic(task, styles, intermediateTasks);
- const handleMarkAsDone = () => {
- if (canMarkDone) {
- onMarkAsDone(task.id);
+ const handleToggleIntermediateTasks = () => {
+ if (onToggleIntermediateTasks) {
+ onToggleIntermediateTasks(task.id);
+ }
+ };
+
+ const handleMarkIntermediateAsDone = intermediateTaskId => {
+ if (onMarkIntermediateAsDone) {
+ onMarkIntermediateAsDone(intermediateTaskId, task.id);
}
};
@@ -65,25 +80,22 @@ const TaskListItem = ({ task, onMarkAsDone }) => {
- {task.is_completed ? (
-
- ) : (
+
+
+ {/* Toggle Intermediate Tasks */}
+ {onToggleIntermediateTasks && (
)}
+
+ {/* Intermediate Tasks */}
+ {isExpanded && onToggleIntermediateTasks && (
+
+ )}
);
};
diff --git a/src/components/EductionPortal/StudentDashboard/TaskListItem.module.css b/src/components/EductionPortal/StudentDashboard/TaskListItem.module.css
index c28a7ba3ab..b51b12d4a3 100644
--- a/src/components/EductionPortal/StudentDashboard/TaskListItem.module.css
+++ b/src/components/EductionPortal/StudentDashboard/TaskListItem.module.css
@@ -5,7 +5,8 @@
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border: 1px solid #e5e7eb;
display: flex;
- align-items: center;
+ flex-wrap: wrap;
+ align-items: flex-start;
gap: 1.5rem;
transition: all 0.2s ease;
}
@@ -170,6 +171,181 @@
cursor: default;
}
+.toggleIntermediateButton {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 40px;
+ height: 40px;
+ border: 1px solid #d1d5db;
+ background-color: #ffffff;
+ border-radius: 50%;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ color: #3b82f6;
+}
+
+.toggleIntermediateButton:hover {
+ background-color: #eff6ff;
+ border-color: #3b82f6;
+}
+
+.expandedIcon {
+ transform: rotate(180deg);
+}
+
+/* Intermediate Tasks Wrapper */
+.intermediateTasksWrapper {
+ width: 100%;
+ margin-top: 0.5rem;
+ padding-top: 1rem;
+ border-top: 1px solid #e5e7eb;
+}
+
+.intermediateTasksList {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.noIntermediateTasks {
+ text-align: center;
+ color: #9ca3af;
+ font-size: 0.875rem;
+ padding: 1rem;
+ margin: 0;
+}
+
+.intermediateTaskItem {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ padding: 0.75rem;
+ background-color: #f9fafb;
+ border-radius: 8px;
+ border: 1px solid #e5e7eb;
+ gap: 0.75rem;
+ transition: all 0.2s ease;
+}
+
+.intermediateTaskItem:hover {
+ background-color: #f3f4f6;
+}
+
+.intermediateTaskContent {
+ flex: 1;
+}
+
+.intermediateTaskTitle {
+ font-size: 0.9rem;
+ font-weight: 600;
+ color: #1f2937;
+ margin: 0 0 0.25rem 0;
+}
+
+.intermediateTaskDescription {
+ font-size: 0.8rem;
+ color: #6b7280;
+ margin: 0 0 0.5rem 0;
+ line-height: 1.4;
+}
+
+.subTaskProgressSection {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin-bottom: 0.5rem;
+}
+
+.subTaskProgressBar {
+ flex: 1;
+ height: 6px;
+ background-color: #e5e7eb;
+ border-radius: 3px;
+ overflow: hidden;
+}
+
+.subTaskProgressFill {
+ height: 100%;
+ background-color: #10b981;
+ border-radius: 3px;
+ transition: width 0.3s ease;
+}
+
+.subTaskProgressText {
+ font-size: 0.75rem;
+ font-weight: 600;
+ color: #6b7280;
+ min-width: 35px;
+ text-align: right;
+}
+
+.intermediateTaskMeta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.75rem;
+ font-size: 0.75rem;
+ color: #6b7280;
+}
+
+.intermediateTaskHours {
+ font-weight: 500;
+}
+
+.intermediateTaskStatus {
+ padding: 0.125rem 0.5rem;
+ border-radius: 9999px;
+ font-weight: 500;
+ text-transform: capitalize;
+ background-color: #e5e7eb;
+ color: #374151;
+}
+
+.intermediateTaskStatus.statuscompleted {
+ background-color: #dcfce7;
+ color: #166534;
+}
+
+.intermediateTaskStatus.statuspending {
+ background-color: #fef3c7;
+ color: #92400e;
+}
+
+.markIntermediateDoneButton {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 32px;
+ height: 32px;
+ border: 1px solid #d1d5db;
+ background-color: #ffffff;
+ border-radius: 6px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ color: #10b981;
+ flex-shrink: 0;
+}
+
+.markIntermediateDoneButton:hover:not(.disabled) {
+ background-color: #10b981;
+ border-color: #10b981;
+ color: #ffffff;
+}
+
+.markIntermediateDoneButton.disabled {
+ background-color: #f3f4f6;
+ border-color: #e5e7eb;
+ color: #d1d5db;
+ cursor: not-allowed;
+ opacity: 0.6;
+}
+
+.markIntermediateDoneButton.disabled:hover {
+ background-color: #f3f4f6;
+ border-color: #e5e7eb;
+ color: #d1d5db;
+}
+
/* Responsive Design */
@media (max-width: 768px) {
.taskListItem {
@@ -177,9 +353,17 @@
align-items: flex-start;
gap: 1rem;
}
-
+
.actionIcons {
flex-direction: row;
align-self: flex-end;
}
+
+ .intermediateTaskItem {
+ flex-direction: column;
+ }
+
+ .markIntermediateDoneButton {
+ width: 100%;
+ }
}
diff --git a/src/components/EductionPortal/StudentDashboard/TaskListView.jsx b/src/components/EductionPortal/StudentDashboard/TaskListView.jsx
index 7dddb5ad93..a0de5d30a8 100644
--- a/src/components/EductionPortal/StudentDashboard/TaskListView.jsx
+++ b/src/components/EductionPortal/StudentDashboard/TaskListView.jsx
@@ -2,7 +2,14 @@ import React from 'react';
import styles from './TaskListView.module.css';
import TaskListItem from './TaskListItem';
-const TaskListView = ({ tasks, onMarkAsDone }) => {
+const TaskListView = ({
+ tasks,
+ onMarkAsDone,
+ intermediateTasks,
+ expandedTasks,
+ onToggleIntermediateTasks,
+ onMarkIntermediateAsDone,
+}) => {
if (!tasks || tasks.length === 0) {
return (
@@ -14,7 +21,15 @@ const TaskListView = ({ tasks, onMarkAsDone }) => {
return (
{tasks.map(task => (
-
+
))}
);
diff --git a/src/components/EductionPortal/StudentDashboard/taskUtils.js b/src/components/EductionPortal/StudentDashboard/taskUtils.js
index 85e55374e0..f848846cad 100644
--- a/src/components/EductionPortal/StudentDashboard/taskUtils.js
+++ b/src/components/EductionPortal/StudentDashboard/taskUtils.js
@@ -3,11 +3,21 @@
*/
/**
- * Calculate progress percentage for a task
+ * Calculate progress percentage for a task based on intermediate tasks
* @param {Object} task - The task object
+ * @param {Array} intermediateTasks - Array of intermediate tasks (optional)
* @returns {number} Progress percentage (0-100)
*/
-export const calculateProgressPercentage = task => {
+export const calculateProgressPercentage = (task, intermediateTasks = []) => {
+ // If there are intermediate tasks, calculate progress based on number of completed tasks
+ if (intermediateTasks && intermediateTasks.length > 0) {
+ const totalTasks = intermediateTasks.length;
+ const completedTasks = intermediateTasks.filter(t => t.status === 'completed').length;
+
+ return totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0;
+ }
+
+ // Otherwise, use logged hours vs suggested hours
return task.suggested_total_hours > 0
? Math.round((task.logged_hours / task.suggested_total_hours) * 100)
: 0;
@@ -16,18 +26,22 @@ export const calculateProgressPercentage = task => {
/**
* Determine if a task can be marked as done
* @param {Object} task - The task object
+ * @param {Array} intermediateTasks - Array of intermediate tasks (optional)
* @returns {boolean} Whether the task can be marked as done
*/
-export const canMarkTaskAsDone = task => {
+export const canMarkTaskAsDone = (task, intermediateTasks = []) => {
if (task.is_completed) return false;
- // Only read tasks can be marked as complete manually
- // Must have logged hours >= suggested hours
+ // If there are intermediate tasks, ONLY check if all sub-tasks are completed
+ if (intermediateTasks && intermediateTasks.length > 0) {
+ return intermediateTasks.every(t => t.status === 'completed');
+ }
+
+ // Otherwise, use the original logic for tasks without sub-tasks
if (task.task_type === 'read') {
return task.logged_hours >= task.suggested_total_hours;
}
- // For other task types, cannot be marked done manually
return false;
};
@@ -35,10 +49,19 @@ export const canMarkTaskAsDone = task => {
* Get status badge information for a task
* @param {Object} task - The task object
* @param {Object} styles - CSS module styles object
+ * @param {Array} intermediateTasks - Array of intermediate tasks (optional)
* @returns {Object} Status badge info with text and className
*/
-export const getTaskStatusBadge = (task, styles) => {
- if (task.status === 'completed' || task.status === 'graded') {
+export const getTaskStatusBadge = (task, styles, intermediateTasks = []) => {
+ // Check if all sub-tasks are completed
+ if (intermediateTasks && intermediateTasks.length > 0) {
+ const allSubTasksCompleted = intermediateTasks.every(t => t.status === 'completed');
+ // Only show as completed if task is marked completed AND all subtasks are done
+ if (allSubTasksCompleted && (task.status === 'completed' || task.status === 'graded')) {
+ return { text: 'Completed', className: styles.completedBadge };
+ }
+ } else if (task.status === 'completed' || task.status === 'graded') {
+ // No subtasks, check task status only
return { text: 'Completed', className: styles.completedBadge };
}
@@ -64,13 +87,30 @@ export const getTaskStatusBadge = (task, styles) => {
/**
* Get tooltip text for mark as done button
* @param {Object} task - The task object
+ * @param {Array} intermediateTasks - Array of intermediate tasks (optional)
* @returns {string} Tooltip text
*/
-export const getMarkAsDoneTooltip = task => {
+export const getMarkAsDoneTooltip = (task, intermediateTasks = []) => {
if (task.is_completed) {
return 'Task is already completed';
}
+ // If there are intermediate tasks, ONLY consider sub-task completion
+ if (intermediateTasks && intermediateTasks.length > 0) {
+ const completedCount = intermediateTasks.filter(t => t.status === 'completed').length;
+ const totalCount = intermediateTasks.length;
+
+ if (completedCount === totalCount) {
+ return 'Mark as Done - All sub-tasks completed';
+ }
+
+ const remaining = totalCount - completedCount;
+ return `Cannot mark as done: Complete ${remaining} more sub-task${
+ remaining !== 1 ? 's' : ''
+ } (${completedCount}/${totalCount} done)`;
+ }
+
+ // Otherwise, use original logic for tasks without sub-tasks
if (task.task_type !== 'read') {
return `Cannot mark as done: Only read tasks can be completed manually (Current type: ${task.task_type})`;
}
@@ -112,9 +152,16 @@ export const formatDate = dateString => {
/**
* Get formatted time and date string for display
* @param {Object} task - The task object
+ * @param {Array} intermediateTasks - Array of intermediate tasks (optional)
* @returns {string} Formatted time and date string
*/
-export const getFormattedTimeAndDate = task => {
+export const getFormattedTimeAndDate = (task, intermediateTasks = []) => {
+ // If there are intermediate tasks, just show the date (no hours)
+ if (intermediateTasks && intermediateTasks.length > 0) {
+ return formatDate(task.last_logged_date || task.created_at);
+ }
+
+ // Otherwise, show logged hours
return `${formatTime(task.logged_hours || 0)} • ${formatDate(
task.last_logged_date || task.created_at,
)}`;
@@ -123,11 +170,58 @@ export const getFormattedTimeAndDate = task => {
/**
* Get progress text for display
* @param {Object} task - The task object
+ * @param {Array} intermediateTasks - Array of intermediate tasks (optional)
* @returns {string} Progress text
*/
-export const getProgressText = task => {
- const progressPercentage = calculateProgressPercentage(task);
+export const getProgressText = (task, intermediateTasks = []) => {
+ const progressPercentage = calculateProgressPercentage(task, intermediateTasks);
+
+ // If there are intermediate tasks, show number of completed tasks
+ if (intermediateTasks && intermediateTasks.length > 0) {
+ const totalTasks = intermediateTasks.length;
+ const completedTasks = intermediateTasks.filter(t => t.status === 'completed').length;
+
+ return `Progress: ${completedTasks} / ${totalTasks} tasks (${progressPercentage}%)`;
+ }
+
+ // Otherwise, show logged hours
return `Progress: ${task.logged_hours || 0} / ${
task.suggested_total_hours
} hrs (${progressPercentage}%)`;
};
+
+/**
+ * Determine if an intermediate task can be marked as done
+ * @param {Object} intermediateTask - The intermediate task object
+ * @returns {boolean} Whether the intermediate task can be marked as done
+ */
+export const canMarkIntermediateTaskAsDone = intermediateTask => {
+ // Already completed
+ if (intermediateTask.status === 'completed') return false;
+
+ // Check if logged hours meet or exceed expected hours
+ const loggedHours = intermediateTask.logged_hours || 0;
+ const expectedHours = intermediateTask.expected_hours || 0;
+
+ return loggedHours >= expectedHours;
+};
+
+/**
+ * Get tooltip text for mark intermediate task as done button
+ * @param {Object} intermediateTask - The intermediate task object
+ * @returns {string} Tooltip text
+ */
+export const getMarkIntermediateAsDoneTooltip = intermediateTask => {
+ if (intermediateTask.status === 'completed') {
+ return 'Sub-task is already completed';
+ }
+
+ const loggedHours = intermediateTask.logged_hours || 0;
+ const expectedHours = intermediateTask.expected_hours || 0;
+
+ if (loggedHours >= expectedHours) {
+ return 'Mark as Done - Hour requirement met';
+ }
+
+ return `Cannot mark as done: Insufficient hours logged (${loggedHours}/${expectedHours} hrs required)`;
+};
diff --git a/src/components/EductionPortal/StudentDashboard/useTaskLogic.js b/src/components/EductionPortal/StudentDashboard/useTaskLogic.js
index 5481acccce..715ab21b58 100644
--- a/src/components/EductionPortal/StudentDashboard/useTaskLogic.js
+++ b/src/components/EductionPortal/StudentDashboard/useTaskLogic.js
@@ -12,22 +12,25 @@ import {
* Custom hook for task-related logic
* @param {Object} task - The task object
* @param {Object} styles - CSS module styles object
+ * @param {Array} intermediateTasks - Array of intermediate tasks (optional)
* @returns {Object} Task logic and computed values
*/
-export const useTaskLogic = (task, styles) => {
- const progressPercentage = useMemo(() => calculateProgressPercentage(task), [
+export const useTaskLogic = (task, styles, intermediateTasks = []) => {
+ const progressPercentage = useMemo(() => calculateProgressPercentage(task, intermediateTasks), [
task.logged_hours,
task.suggested_total_hours,
+ intermediateTasks,
]);
- const canMarkDone = useMemo(() => canMarkTaskAsDone(task), [
+ const canMarkDone = useMemo(() => canMarkTaskAsDone(task, intermediateTasks), [
task.is_completed,
task.task_type,
task.logged_hours,
task.suggested_total_hours,
+ intermediateTasks,
]);
- const statusBadge = useMemo(() => getTaskStatusBadge(task, styles), [
+ const statusBadge = useMemo(() => getTaskStatusBadge(task, styles, intermediateTasks), [
task.status,
task.has_upload,
task.task_type,
@@ -35,24 +38,28 @@ export const useTaskLogic = (task, styles) => {
task.suggested_total_hours,
task.has_comments,
task.feedback,
+ intermediateTasks,
]);
- const markAsDoneTooltip = useMemo(() => getMarkAsDoneTooltip(task), [
+ const markAsDoneTooltip = useMemo(() => getMarkAsDoneTooltip(task, intermediateTasks), [
task.is_completed,
task.task_type,
task.logged_hours,
task.suggested_total_hours,
+ intermediateTasks,
]);
- const formattedTimeAndDate = useMemo(() => getFormattedTimeAndDate(task), [
+ const formattedTimeAndDate = useMemo(() => getFormattedTimeAndDate(task, intermediateTasks), [
task.logged_hours,
task.last_logged_date,
task.created_at,
+ intermediateTasks,
]);
- const progressText = useMemo(() => getProgressText(task), [
+ const progressText = useMemo(() => getProgressText(task, intermediateTasks), [
task.logged_hours,
task.suggested_total_hours,
+ intermediateTasks,
]);
return {
diff --git a/src/routes.jsx b/src/routes.jsx
index 39f880053a..dcf45a77ea 100644
--- a/src/routes.jsx
+++ b/src/routes.jsx
@@ -174,6 +174,7 @@ import ProtectedRoute from './components/common/ProtectedRoute';
import { UserRole } from './utils/enums';
import WriteTaskUpload from './components/EductionPortal/Tasks/WriteTaskUpload';
+import IntermediateTaskList from './components/EductionPortal/IntermediateTasks/IntermediateTaskList';
// Social Architecture
@@ -777,6 +778,11 @@ export default (
+
`${APIEndpoint}/tasknotification/${taskNotificationId}`,
-
-POPULARITY: (range, roles, start, end) => {
- let url = `${APIEndpoint}/popularity?`;
- if (range) url += `range=${range}&`;
- if (roles && roles.length > 0) {
- url += `roles=${encodeURIComponent(JSON.stringify(roles))}&`;
- }
- if (start) url += `start=${encodeURIComponent(start)}&`;
- if (end) url += `end=${encodeURIComponent(end)}&`;
- return url.slice(0, -1);
-},
-POPULARITY_ROLES: `${APIEndpoint}/popularity/roles`,
+
+ POPULARITY: (range, roles, start, end) => {
+ let url = `${APIEndpoint}/popularity?`;
+ if (range) url += `range=${range}&`;
+ if (roles && roles.length > 0) {
+ url += `roles=${encodeURIComponent(JSON.stringify(roles))}&`;
+ }
+ if (start) url += `start=${encodeURIComponent(start)}&`;
+ if (end) url += `end=${encodeURIComponent(end)}&`;
+ return url.slice(0, -1);
+ },
+ POPULARITY_ROLES: `${APIEndpoint}/popularity/roles`,
// titles endpoints
@@ -183,6 +183,11 @@ POPULARITY_ROLES: `${APIEndpoint}/popularity/roles`,
STUDENT_TASKS: () => `${APIEndpoint}/student/tasks`,
STUDENT_TASK_MARK_DONE: taskId => `${APIEndpoint}/student/tasks/${taskId}/mark-done`,
+ // Intermediate Tasks (Education Portal)
+ INTERMEDIATE_TASKS: () => `${APIEndpoint}/educator/intermediate-tasks`,
+ INTERMEDIATE_TASK_BY_ID: id => `${APIEndpoint}/educator/intermediate-tasks/${id}`,
+ INTERMEDIATE_TASKS_BY_PARENT: taskId => `${APIEndpoint}/educator/tasks/${taskId}/intermediate`,
+
TIMER_SERVICE: new URL('/timer-service', APIEndpoint.replace('http', 'ws')).toString(),
TIMEZONE_LOCATION: location => `${APIEndpoint}/timezone/${location}`,