Files
starr-philproperties/src/contexts/AdminTaskContext.jsx
T
2026-07-07 13:40:07 +08:00

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>
);
}