/*********************************************************************************************************************************************************************** * File Name : AdminTaskContext.jsx * Type : Context / Provider * Description : Admin task management context. * Covers: task lists (list, get, create, update, archive, restore, bulk archive, bulk restore) * tasks (list, get, create, update, archive, restore, bulk archive, bulk restore) * task list groups (list assigned, assign, unassign) * task completions (list, get, list by user, archive, restore, bulk archive, bulk restore) ***********************************************************************************************************************************************************************/ import { createContext, useCallback, useContext, useState } from 'react'; import api from '@/utils/api.util'; import { toast } from 'sonner'; const BASE = '/admin/task-lists'; // ─── Context ────────────────────────────────────────────────────────────────── const AdminTaskContext = createContext(null); export function useAdminTask() { const ctx = useContext(AdminTaskContext); if (!ctx) throw new Error('useAdminTask must be used within an AdminTaskProvider'); return ctx; } // ─── Provider ───────────────────────────────────────────────────────────────── export function AdminTaskProvider({ children }) { // ── Task List state ─────────────────────────────────────────────────────── const [taskLists, setTaskLists] = useState([]); const [taskList, setTaskList] = useState(null); // ── Task state ──────────────────────────────────────────────────────────── const [tasks, setTasks] = useState([]); const [task, setTask] = useState(null); // ── Task List Groups state ──────────────────────────────────────────────── const [taskListGroups, setTaskListGroups] = useState([]); // ── Completion state ────────────────────────────────────────────────────── const [completions, setCompletions] = useState([]); const [completion, setCompletion] = useState(null); // ── Shared (task lists + tasks) ─────────────────────────────────────────── const [attributes, setAttributes] = useState([]); const [pagination, setPagination] = useState({ page: 1, limit: 10, totalRecords: 0, totalPages: 1 }); const [loading, setLoading] = useState(false); // ── Completion-specific (separate to avoid conflicts) ───────────────────── const [completionPagination, setCompletionPagination] = useState({ page: 1, limit: 10, totalRecords: 0, totalPages: 1 }); const [completionLoading, setCompletionLoading] = useState(false); const [completionAttributes, setCompletionAttributes] = useState([]); // ─── Generic request wrappers ───────────────────────────────────────────── const request = useCallback(async (fn) => { setLoading(true); try { return await fn(); } catch (err) { const message = err?.response?.data?.message ?? 'Something went wrong.'; toast(message); return null; } finally { setLoading(false); } }, []); const completionRequest = useCallback(async (fn) => { setCompletionLoading(true); try { return await fn(); } catch (err) { const message = err?.response?.data?.message ?? 'Something went wrong.'; toast(message); return null; } finally { setCompletionLoading(false); } }, []); // ══════════════════════════════════════════════════════════════════════════ // TASK LISTS // ══════════════════════════════════════════════════════════════════════════ const fetchTaskLists = useCallback( ({ page = 1, limit = 10, filters = [], sort = [] } = {}) => request(async () => { const res = await api.get(BASE, { params: { page, limit, filters: JSON.stringify(filters), sort: JSON.stringify(sort), }, }); const { data, attributes: attrs, pagination: pg } = res.data?.data ?? {}; setTaskLists(data ?? []); setAttributes(attrs ?? []); setPagination(pg ?? { page: 1, limit: 10, totalRecords: 0, totalPages: 1 }); }), [request] ); const fetchTaskList = useCallback( (taskListId) => request(async () => { const res = await api.get(`${BASE}/${taskListId}`); const taskListData = res.data?.data ?? null; setTaskList(taskListData); if (taskListData?.groups) setTaskListGroups(taskListData.groups); return taskListData; }), [request] ); const fetchArchivedTaskLists = useCallback( ({ page = 1, limit = 10, filters = [], sort = [] } = {}) => request(async () => { const res = await api.get(`${BASE}/archived`, { params: { page, limit, filters: JSON.stringify(filters), sort: JSON.stringify(sort), }, }); const { data, attributes: attrs, pagination: pg } = res.data?.data ?? {}; setTaskLists(data ?? []); setAttributes(attrs ?? []); setPagination(pg ?? { page: 1, limit: 10, totalRecords: 0, totalPages: 1 }); }), [request] ); const createTaskList = useCallback( (payload) => request(async () => { const res = await api.post(BASE, payload); toast('Task list created.'); return res.data?.data ?? null; }), [request] ); const updateTaskList = useCallback( (taskListId, payload) => request(async () => { const res = await api.patch(`${BASE}/${taskListId}`, payload); toast('Task list updated.'); return res.data?.data ?? null; }), [request] ); const archiveTaskList = useCallback( (taskListId) => request(async () => { await api.delete(`${BASE}/${taskListId}`); toast('Task list archived.'); return true; }), [request] ); const restoreTaskList = useCallback( (taskListId) => request(async () => { await api.patch(`${BASE}/${taskListId}/restore`); toast('Task list restored.'); return true; }), [request] ); const bulkArchiveTaskLists = useCallback( (ids) => request(async () => { await api.post(`${BASE}/bulk-archive`, { ids }); toast(`${ids.length} task list(s) archived.`); return true; }), [request] ); const bulkRestoreTaskLists = useCallback( (ids) => request(async () => { await api.post(`${BASE}/bulk-restore`, { ids }); toast(`${ids.length} task list(s) restored.`); return true; }), [request] ); const permanentlyDeleteTaskList = useCallback( (taskListId) => request(async () => { await api.delete(`${BASE}/${taskListId}/permanent`); toast('Task list permanently deleted.'); return true; }), [request] ); const bulkPermanentlyDeleteTaskLists = useCallback( (ids) => request(async () => { await api.post(`${BASE}/bulk-delete`, { ids }); toast(`${ids.length} task list(s) permanently deleted.`); return true; }), [request] ); const fetchTaskListPermanentDeleteImpact = useCallback( (taskListId) => request(async () => { const res = await api.get(`${BASE}/${taskListId}/permanent-delete-impact`); const { taskCount, completionCount } = res.data?.data ?? {}; return [ { label: "task(s)", count: taskCount ?? 0 }, { label: "task completion record(s)", count: completionCount ?? 0 }, ]; }), [request] ); const fetchTaskListFieldValues = useCallback( (field) => request(async () => { const res = await api.get(`${BASE}/field-values`, { params: { field } }); return res.data?.data ?? []; }), [request] ); // ══════════════════════════════════════════════════════════════════════════ // TASK LIST GROUPS // ══════════════════════════════════════════════════════════════════════════ const fetchTaskListGroups = useCallback( (taskListId) => request(async () => { const res = await api.get(`${BASE}/${taskListId}/groups`); const groups = res.data?.data ?? []; setTaskListGroups(groups); return groups; }), [request] ); const assignGroups = useCallback( (taskListId, groupIds) => request(async () => { const res = await api.post(`${BASE}/${taskListId}/groups/assign`, { group_ids: groupIds, }); const result = res.data?.data ?? {}; if (result.assigned_ids?.length) { toast(`${result.assigned_ids.length} group(s) assigned.`); } else { toast('All selected groups were already assigned.'); } return result; }), [request] ); const unassignGroups = useCallback( (taskListId, groupIds) => request(async () => { const res = await api.post(`${BASE}/${taskListId}/groups/unassign`, { group_ids: groupIds, }); const result = res.data?.data ?? {}; toast(`${result.unassigned_ids?.length ?? 0} group(s) unassigned.`); return result; }), [request] ); // ══════════════════════════════════════════════════════════════════════════ // TASKS // ══════════════════════════════════════════════════════════════════════════ const fetchTasks = useCallback( (taskListId, { page = 1, limit = 10, filters = [], sort = [] } = {}) => request(async () => { const res = await api.get(`${BASE}/${taskListId}/tasks`, { params: { page, limit, filters: JSON.stringify(filters), sort: JSON.stringify(sort), }, }); const { data, attributes: attrs, pagination: pg } = res.data?.data ?? {}; setTasks(data ?? []); setAttributes(attrs ?? []); setPagination(pg ?? { page: 1, limit: 10, totalRecords: 0, totalPages: 1 }); }), [request] ); const fetchArchivedTasks = useCallback( (taskListId, { page = 1, limit = 10, filters = [], sort = [] } = {}) => request(async () => { const res = await api.get(`${BASE}/${taskListId}/tasks/archived`, { params: { page, limit, filters: JSON.stringify(filters), sort: JSON.stringify(sort), }, }); const { data, attributes: attrs, pagination: pg } = res.data?.data ?? {}; setTasks(data ?? []); setAttributes(attrs ?? []); setPagination(pg ?? { page: 1, limit: 10, totalRecords: 0, totalPages: 1 }); }), [request] ); const fetchTask = useCallback( (taskListId, taskId) => request(async () => { const res = await api.get(`${BASE}/${taskListId}/tasks/${taskId}`); const taskData = res.data?.data ?? null; setTask(taskData); return taskData; }), [request] ); const createTask = useCallback( (taskListId, payload) => request(async () => { const res = await api.post(`${BASE}/${taskListId}/tasks`, payload); toast('Task created.'); return res.data?.data?.data ?? null; }), [request] ); const updateTask = useCallback( (taskListId, taskId, payload) => request(async () => { const res = await api.patch(`${BASE}/${taskListId}/tasks/${taskId}`, payload); toast('Task updated.'); return res.data?.data?.data ?? null; }), [request] ); const archiveTask = useCallback( (taskListId, taskId) => request(async () => { await api.delete(`${BASE}/${taskListId}/tasks/${taskId}`); toast('Task archived.'); return true; }), [request] ); const restoreTask = useCallback( (taskListId, taskId) => request(async () => { await api.patch(`${BASE}/${taskListId}/tasks/${taskId}/restore`); toast('Task restored.'); return true; }), [request] ); const bulkArchiveTasks = useCallback( (taskListId, ids) => request(async () => { await api.post(`${BASE}/${taskListId}/tasks/bulk-archive`, { ids }); toast(`${ids.length} task(s) archived.`); return true; }), [request] ); const bulkRestoreTasks = useCallback( (taskListId, ids) => request(async () => { await api.post(`${BASE}/${taskListId}/tasks/bulk-restore`, { ids }); toast(`${ids.length} task(s) restored.`); return true; }), [request] ); const permanentlyDeleteTask = useCallback( (taskListId, taskId) => request(async () => { await api.delete(`${BASE}/${taskListId}/tasks/${taskId}/permanent`); toast('Task permanently deleted.'); return true; }), [request] ); const bulkPermanentlyDeleteTasks = useCallback( (taskListId, ids) => request(async () => { await api.post(`${BASE}/${taskListId}/tasks/bulk-delete`, { ids }); toast(`${ids.length} task(s) permanently deleted.`); return true; }), [request] ); const fetchTaskFieldValues = useCallback( (taskListId, field) => request(async () => { const res = await api.get(`${BASE}/${taskListId}/tasks/field-values`, { params: { field }, }); return res.data?.data ?? []; }), [request] ); // ── Flat lists for RequirementBuilder course/unit/lesson selectors ──────── const fetchCoursesFlat = useCallback( () => request(async () => { const res = await api.get('/admin/courses/flat'); return res.data?.data ?? []; }), [request] ); const fetchUnitsFlat = useCallback( () => request(async () => { const res = await api.get('/admin/courses/units-flat'); const raw = res.data?.data ?? []; return raw.map((u) => ({ ...u, _search: `${u.course_title} unit ${u.order_index + 1} ${u.title}`.toLowerCase(), })); }), [request] ); const fetchLessonsFlat = useCallback( () => request(async () => { const res = await api.get('/admin/courses/lessons-flat'); const raw = res.data?.data ?? []; return raw.map((l) => ({ ...l, _search: `${l.course_title} unit ${l.unit_order + 1} ${l.unit_title} lesson ${l.order_index + 1} ${l.title}`.toLowerCase(), })); }), [request] ); // ══════════════════════════════════════════════════════════════════════════ // TASK COMPLETIONS // ══════════════════════════════════════════════════════════════════════════ // ─── GET ALL ────────────────────────────────────────────────────────────── // GET /admin/task-lists/:taskListId/tasks/:taskId/completions const fetchCompletions = useCallback( (taskListId, taskId, { page = 1, limit = 10, filters = [], sort = [] } = {}) => completionRequest(async () => { const res = await api.get(`${BASE}/${taskListId}/tasks/${taskId}/completions`, { params: { page, limit, filters: JSON.stringify(filters), sort: JSON.stringify(sort), }, }); const { data, attributes: attrs, pagination: pg } = res.data?.data ?? {}; setCompletions(data ?? []); setCompletionAttributes(attrs ?? []); setCompletionPagination(pg ?? { page: 1, limit: 10, totalRecords: 0, totalPages: 1 }); }), [completionRequest] ); // ─── GET ONE ────────────────────────────────────────────────────────────── // GET /admin/task-lists/:taskListId/tasks/:taskId/completions/:completionId const fetchCompletion = useCallback( (taskListId, taskId, completionId) => completionRequest(async () => { const res = await api.get( `${BASE}/${taskListId}/tasks/${taskId}/completions/${completionId}` ); const data = res.data?.data ?? null; setCompletion(data); return data; }), [completionRequest] ); // ─── GET BY USER ────────────────────────────────────────────────────────── // GET /admin/task-lists/:taskListId/tasks/:taskId/completions/user/:userId const fetchCompletionsByUser = useCallback( (taskListId, taskId, userId, { page = 1, limit = 10, filters = [], sort = [] } = {}) => completionRequest(async () => { const res = await api.get( `${BASE}/${taskListId}/tasks/${taskId}/completions/user/${userId}`, { params: { page, limit, filters: JSON.stringify(filters), sort: JSON.stringify(sort), }, } ); const { data, attributes: attrs, pagination: pg } = res.data?.data ?? {}; setCompletions(data ?? []); setCompletionAttributes(attrs ?? []); setCompletionPagination(pg ?? { page: 1, limit: 10, totalRecords: 0, totalPages: 1 }); }), [completionRequest] ); // ─── ARCHIVE ────────────────────────────────────────────────────────────── const archiveCompletion = useCallback( (taskListId, taskId, completionId) => completionRequest(async () => { await api.delete( `${BASE}/${taskListId}/tasks/${taskId}/completions/${completionId}` ); toast('Completion archived.'); return true; }), [completionRequest] ); // ─── RESTORE ────────────────────────────────────────────────────────────── const restoreCompletion = useCallback( (taskListId, taskId, completionId) => completionRequest(async () => { await api.patch( `${BASE}/${taskListId}/tasks/${taskId}/completions/${completionId}/restore` ); toast('Completion restored.'); return true; }), [completionRequest] ); // ─── BULK ARCHIVE ───────────────────────────────────────────────────────── const bulkArchiveCompletions = useCallback( (taskListId, taskId, ids) => completionRequest(async () => { await api.post( `${BASE}/${taskListId}/tasks/${taskId}/completions/bulk-archive`, { ids } ); toast(`${ids.length} completion(s) archived.`); return true; }), [completionRequest] ); // ─── BULK RESTORE ───────────────────────────────────────────────────────── const bulkRestoreCompletions = useCallback( (taskListId, taskId, ids) => completionRequest(async () => { await api.post( `${BASE}/${taskListId}/tasks/${taskId}/completions/bulk-restore`, { ids } ); toast(`${ids.length} completion(s) restored.`); return true; }), [completionRequest] ); // ───────────────────────────────────────────────────────────────────────── return ( {children} ); }