mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
623 lines
28 KiB
React
623 lines
28 KiB
React
/***********************************************************************************************************************************************************************
|
|
* 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 (
|
|
<AdminTaskContext.Provider value={{
|
|
// ── Task List state ───────────────────────────────────────────────
|
|
taskLists, taskList,
|
|
// ── Task state ────────────────────────────────────────────────────
|
|
tasks, task,
|
|
// ── Task List Groups state ────────────────────────────────────────
|
|
taskListGroups, setTaskListGroups,
|
|
// ── Completion state ──────────────────────────────────────────────
|
|
completions, completion,
|
|
// ── Shared state ──────────────────────────────────────────────────
|
|
attributes, setAttributes,
|
|
pagination, setPagination,
|
|
loading,
|
|
// ── Completion-specific state ─────────────────────────────────────
|
|
completionPagination, setCompletionPagination,
|
|
completionLoading,
|
|
completionAttributes, setCompletionAttributes,
|
|
|
|
// ── Task List actions ─────────────────────────────────────────────
|
|
fetchTaskLists, fetchTaskList, fetchArchivedTaskLists,
|
|
createTaskList, updateTaskList,
|
|
archiveTaskList, restoreTaskList,
|
|
bulkArchiveTaskLists, bulkRestoreTaskLists,
|
|
permanentlyDeleteTaskList, bulkPermanentlyDeleteTaskLists,
|
|
fetchTaskListPermanentDeleteImpact,
|
|
fetchTaskListFieldValues,
|
|
|
|
// ── Task List Group actions ───────────────────────────────────────
|
|
fetchTaskListGroups,
|
|
assignGroups,
|
|
unassignGroups,
|
|
|
|
// ── Task actions ──────────────────────────────────────────────────
|
|
fetchTasks, fetchTask, fetchArchivedTasks,
|
|
createTask, updateTask,
|
|
archiveTask, restoreTask,
|
|
bulkArchiveTasks, bulkRestoreTasks,
|
|
permanentlyDeleteTask, bulkPermanentlyDeleteTasks,
|
|
fetchTaskFieldValues,
|
|
|
|
// ── Completion actions ────────────────────────────────────────────
|
|
fetchCompletions, fetchCompletion, fetchCompletionsByUser,
|
|
archiveCompletion, restoreCompletion,
|
|
bulkArchiveCompletions, bulkRestoreCompletions,
|
|
|
|
// ── Flat lists for RequirementBuilder ────────────────────────────
|
|
fetchCoursesFlat, fetchUnitsFlat, fetchLessonsFlat,
|
|
}}>
|
|
{children}
|
|
</AdminTaskContext.Provider>
|
|
);
|
|
} |