mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
1291 lines
44 KiB
React
1291 lines
44 KiB
React
import { createContext, useCallback, useContext, useState } from "react";
|
|
import api from "@/utils/api.util";
|
|
import { toast } from "sonner";
|
|
|
|
const CoursesContext = createContext(null);
|
|
|
|
export function useCourses() {
|
|
const ctx = useContext(CoursesContext);
|
|
if (!ctx) throw new Error("useCourses must be used within a CoursesProvider");
|
|
return ctx;
|
|
}
|
|
|
|
// ─── Initial States ───────────────────────────────────────────────────────────
|
|
|
|
const PAGINATION_INIT = {
|
|
page: 1,
|
|
limit: 10,
|
|
totalRecords: 0,
|
|
totalPages: 0,
|
|
hasPrevPage: false,
|
|
hasNextPage: false,
|
|
};
|
|
|
|
const BASE = "/admin/courses";
|
|
|
|
// Junction revamp — unit-scoped endpoints resolve either through a course
|
|
// (/admin/courses/:courseId/units/:unitId) or straight against the standalone
|
|
// unit library (/admin/units/:unitId) when no courseId is in scope. This lets
|
|
// the quiz builder and unit pages run identically in both contexts.
|
|
const unitBase = (courseId, unitId) =>
|
|
courseId != null && courseId !== ""
|
|
? `${BASE}/${courseId}/units/${unitId}`
|
|
: `/admin/units/${unitId}`;
|
|
|
|
// Lesson-scoped endpoints resolve through a unit when one is in scope,
|
|
// otherwise straight against the standalone lesson library (/admin/lessons/:id).
|
|
const lessonBase = (courseId, unitId, lessonId) =>
|
|
unitId != null && unitId !== ""
|
|
? `${unitBase(courseId, unitId)}/lessons/${lessonId}`
|
|
: `/admin/lessons/${lessonId}`;
|
|
|
|
// ─── Provider ─────────────────────────────────────────────────────────────────
|
|
|
|
export function CoursesProvider({ children }) {
|
|
|
|
// ─── State ────────────────────────────────────────────────────────────────
|
|
const [courses, setCourses] = useState([]);
|
|
const [course, setCourse] = useState(null);
|
|
const [units, setUnits] = useState([]);
|
|
const [unit, setUnit] = useState(null);
|
|
const [lessons, setLessons] = useState([]);
|
|
const [lesson, setLesson] = useState(null);
|
|
const [lessonPage, setLessonPage] = useState(null);
|
|
const [prerequisites, setPrerequisites] = useState([]);
|
|
const [quiz, setQuiz] = useState(null);
|
|
const [questions, setQuestions] = useState([]);
|
|
const [assessment, setAssessment] = useState(null);
|
|
const [completions, setCompletions] = useState(null); // { summary, completions[] }
|
|
const [sessions, setSessions] = useState(null); // { summary, sessions[] }
|
|
const [attributes, setAttributes] = useState([]);
|
|
const [pagination, setPagination] = useState(PAGINATION_INIT);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
// ─── Request wrapper ──────────────────────────────────────────────────────
|
|
const request = useCallback(async (fn) => {
|
|
setLoading(true);
|
|
try {
|
|
return await fn();
|
|
} catch (err) {
|
|
const message = err?.response?.data?.message ?? "Something went wrong.";
|
|
if (err.status === 404 && message) toast(message);
|
|
else toast(message);
|
|
return null;
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
// ─── Paginated GET helper ─────────────────────────────────────────────────
|
|
const paginatedGet = useCallback(
|
|
(url, setter, params = {}) =>
|
|
request(async () => {
|
|
const { filters, sort, ...rest } = params;
|
|
const { data } = await api.get(url, {
|
|
params: {
|
|
...rest,
|
|
...(filters?.length ? { filters: JSON.stringify(filters) } : {}),
|
|
...(sort?.length ? { sort: JSON.stringify(sort) } : {}),
|
|
},
|
|
});
|
|
const final_data = data?.data;
|
|
setter(final_data?.data ?? []);
|
|
setPagination(final_data?.pagination ?? PAGINATION_INIT);
|
|
if (final_data?.attributes?.length) setAttributes(final_data.attributes);
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
// =========================================================================
|
|
// COURSES
|
|
// =========================================================================
|
|
|
|
const fetchCourses = useCallback(
|
|
(params = {}) => paginatedGet(BASE, setCourses, params),
|
|
[paginatedGet],
|
|
);
|
|
|
|
const fetchCourse = useCallback(
|
|
(courseId) =>
|
|
request(async () => {
|
|
const { data } = await api.get(`${BASE}/${courseId}`);
|
|
const result = data?.data?.data ?? null;
|
|
setCourse(result);
|
|
setUnits(result?.units ?? []);
|
|
setPrerequisites(result?.prerequisites ?? []);
|
|
setAssessment(result?.assessment ?? null);
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const createCourse = useCallback(
|
|
(payload) =>
|
|
request(async () => {
|
|
const { data } = await api.post(BASE, payload);
|
|
const course = data?.data?.data ?? null;
|
|
if (course) {
|
|
setCourses((prev) => [course, ...prev]);
|
|
toast("Course created successfully.");
|
|
}
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const updateCourse = useCallback(
|
|
(courseId, payload) =>
|
|
request(async () => {
|
|
const { data } = await api.put(`${BASE}/${courseId}`, payload);
|
|
const course = data?.data?.data ?? null;
|
|
if (course) {
|
|
setCourses((prev) => prev.map((c) => (c.course_id === courseId ? course : c)));
|
|
setCourse(course);
|
|
toast("Course updated successfully.");
|
|
}
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const archiveCourse = useCallback(
|
|
(courseId) =>
|
|
request(async () => {
|
|
const { data } = await api.delete(`${BASE}/${courseId}`);
|
|
setCourses((prev) => prev.filter((c) => c.course_id !== courseId));
|
|
setCourse((prev) => (prev?.course_id === courseId ? null : prev));
|
|
toast("Course archived.");
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const archiveCourses = useCallback(
|
|
({ ids }) =>
|
|
request(async () => {
|
|
const { data } = await api.delete(`${BASE}/bulk`, { data: { ids } });
|
|
setCourses((prev) => prev.filter((c) => !ids.includes(c.course_id)));
|
|
toast("Courses archived.");
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
// ── Course Archives & Restore ─────────────────────────────────────────────
|
|
|
|
const fetchArchivedCourses = useCallback(
|
|
(params = {}) => paginatedGet(`${BASE}/archives`, setCourses, params),
|
|
[paginatedGet],
|
|
);
|
|
|
|
const fetchArchivedCourse = useCallback(
|
|
(courseId) =>
|
|
request(async () => {
|
|
const { data } = await api.get(`${BASE}/archives/${courseId}`);
|
|
const result = data?.data?.data ?? null;
|
|
setCourse(result);
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const restoreCourse = useCallback(
|
|
(courseId) =>
|
|
request(async () => {
|
|
const { data } = await api.patch(`${BASE}/${courseId}/restore`);
|
|
const result = data?.data?.data ?? null;
|
|
if (result) {
|
|
setCourses((prev) => prev.filter((c) => c.course_id !== courseId));
|
|
toast("Course restored.");
|
|
}
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const restoreCourses = useCallback(
|
|
({ ids }) =>
|
|
request(async () => {
|
|
const { data } = await api.patch(`${BASE}/restore/bulk`, { ids });
|
|
setCourses((prev) => prev.filter((c) => !ids.includes(c.course_id)));
|
|
toast("Courses restored.");
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const permanentlyDeleteCourse = useCallback(
|
|
(courseId) =>
|
|
request(async () => {
|
|
const { data } = await api.delete(`${BASE}/${courseId}/permanent`);
|
|
setCourses((prev) => prev.filter((c) => c.course_id !== courseId));
|
|
setCourse((prev) => (prev?.course_id === courseId ? null : prev));
|
|
toast("Course permanently deleted.");
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const permanentlyDeleteCourses = useCallback(
|
|
({ ids }) =>
|
|
request(async () => {
|
|
const { data } = await api.delete(`${BASE}/bulk/permanent`, { data: { ids } });
|
|
setCourses((prev) => prev.filter((c) => !ids.includes(c.course_id)));
|
|
toast("Courses permanently deleted.");
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const fetchCoursePermanentDeleteImpact = useCallback(
|
|
(courseId) =>
|
|
request(async () => {
|
|
const { data } = await api.get(`${BASE}/${courseId}/permanent-delete-impact`);
|
|
const { unitCount, lessonCount } = data?.data ?? {};
|
|
return [
|
|
{ label: "unit(s)", count: unitCount ?? 0 },
|
|
{ label: "lesson(s)", count: lessonCount ?? 0 },
|
|
];
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
// =========================================================================
|
|
// PREREQUISITES
|
|
// =========================================================================
|
|
|
|
const fetchPrerequisites = useCallback(
|
|
(courseId) =>
|
|
request(async () => {
|
|
const { data } = await api.get(`${BASE}/${courseId}/prerequisites`);
|
|
const result = data?.data?.data ?? [];
|
|
setPrerequisites(result);
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const syncPrerequisites = useCallback(
|
|
(courseId, prerequisites) =>
|
|
request(async () => {
|
|
const { data } = await api.put(`${BASE}/${courseId}/prerequisites`, { prerequisites });
|
|
setPrerequisites(prerequisites);
|
|
toast("Prerequisites updated.");
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
// =========================================================================
|
|
// UNITS
|
|
// =========================================================================
|
|
|
|
const fetchUnits = useCallback(
|
|
(courseId, params = {}) => {
|
|
const mergeQuiz = (incoming) =>
|
|
setUnits((prev) => {
|
|
if (!prev.length) return incoming;
|
|
const prevMap = Object.fromEntries(prev.map((u) => [u.unit_id, u]));
|
|
return incoming.map((u) => {
|
|
const old = prevMap[u.unit_id];
|
|
return {
|
|
...u,
|
|
quiz: u.quiz ?? old?.quiz ?? null,
|
|
quiz_id: u.quiz_id ?? old?.quiz_id ?? null,
|
|
};
|
|
});
|
|
});
|
|
return paginatedGet(`${BASE}/${courseId}/units`, mergeQuiz, params);
|
|
},
|
|
[paginatedGet],
|
|
);
|
|
|
|
const fetchUnit = useCallback(
|
|
(courseId, unitId) =>
|
|
request(async () => {
|
|
const { data } = await api.get(`${unitBase(courseId, unitId)}`);
|
|
const result = data?.data?.data ?? null;
|
|
setUnit(result);
|
|
setLessons(result?.lessons ?? []);
|
|
setQuiz(result?.quiz ?? null);
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const createUnit = useCallback(
|
|
(courseId, payload) =>
|
|
request(async () => {
|
|
const { data } = await api.post(`${BASE}/${courseId}/units`, payload);
|
|
const unit = data?.data?.data ?? null;
|
|
if (unit) {
|
|
setUnits((prev) => [...prev, unit]);
|
|
toast("Unit created successfully.");
|
|
}
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const updateUnit = useCallback(
|
|
(courseId, unitId, payload) =>
|
|
request(async () => {
|
|
const { data } = await api.put(`${unitBase(courseId, unitId)}`, payload);
|
|
const unit = data?.data?.data ?? null;
|
|
if (unit) {
|
|
setUnits((prev) => prev.map((u) => (u.unit_id === unitId ? unit : u)));
|
|
setUnit(unit);
|
|
toast("Unit updated successfully.");
|
|
}
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const archiveUnit = useCallback(
|
|
(courseId, unitId) =>
|
|
request(async () => {
|
|
const { data } = await api.delete(`${unitBase(courseId, unitId)}`);
|
|
setUnits((prev) => prev.filter((u) => u.unit_id !== unitId));
|
|
setUnit((prev) => (prev?.unit_id === unitId ? null : prev));
|
|
toast("Unit archived.");
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const archiveUnits = useCallback(
|
|
(courseId, { ids }) =>
|
|
request(async () => {
|
|
const { data } = await api.delete(`${BASE}/${courseId}/units/bulk`, { data: { ids } });
|
|
setUnits((prev) => prev.filter((u) => !ids.includes(u.unit_id)));
|
|
toast("Units archived.");
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
// ── Unit Archives & Restore ───────────────────────────────────────────────
|
|
|
|
const fetchArchivedUnits = useCallback(
|
|
(courseId, params = {}) =>
|
|
paginatedGet(`${BASE}/${courseId}/units/archives`, setUnits, params),
|
|
[paginatedGet],
|
|
);
|
|
|
|
const fetchArchivedUnit = useCallback(
|
|
(courseId, unitId) =>
|
|
request(async () => {
|
|
const { data } = await api.get(`${BASE}/${courseId}/units/archives/${unitId}`);
|
|
const result = data?.data?.data ?? null;
|
|
setUnit(result);
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const restoreUnit = useCallback(
|
|
(courseId, unitId) =>
|
|
request(async () => {
|
|
const { data } = await api.patch(`${unitBase(courseId, unitId)}/restore`);
|
|
const result = data?.data?.data ?? null;
|
|
if (result) {
|
|
setUnits((prev) => prev.filter((u) => u.unit_id !== unitId));
|
|
toast("Unit restored.");
|
|
}
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const restoreUnits = useCallback(
|
|
(courseId, { ids }) =>
|
|
request(async () => {
|
|
const { data } = await api.patch(`${BASE}/${courseId}/units/restore/bulk`, { ids } );
|
|
setUnits((prev) => prev.filter((u) => !ids.includes(u.unit_id)));
|
|
toast("Units restored.");
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const permanentlyDeleteUnit = useCallback(
|
|
(courseId, unitId) =>
|
|
request(async () => {
|
|
const { data } = await api.delete(`${unitBase(courseId, unitId)}/permanent`);
|
|
setUnits((prev) => prev.filter((u) => u.unit_id !== unitId));
|
|
setUnit((prev) => (prev?.unit_id === unitId ? null : prev));
|
|
toast("Unit permanently deleted.");
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const permanentlyDeleteUnits = useCallback(
|
|
(courseId, { ids }) =>
|
|
request(async () => {
|
|
const { data } = await api.delete(`${BASE}/${courseId}/units/bulk/permanent`, { data: { ids } });
|
|
setUnits((prev) => prev.filter((u) => !ids.includes(u.unit_id)));
|
|
toast("Units permanently deleted.");
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const fetchUnitPermanentDeleteImpact = useCallback(
|
|
(courseId, unitId) =>
|
|
request(async () => {
|
|
const { data } = await api.get(`${unitBase(courseId, unitId)}/permanent-delete-impact`);
|
|
const { lessonCount } = data?.data ?? {};
|
|
return [{ label: "lesson(s)", count: lessonCount ?? 0 }];
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
// =========================================================================
|
|
// UNIT QUIZ
|
|
// =========================================================================
|
|
|
|
const fetchQuiz = useCallback(
|
|
(courseId, unitId) =>
|
|
request(async () => {
|
|
const { data } = await api.get(`${unitBase(courseId, unitId)}/quiz`);
|
|
const result = data?.data?.data ?? null;
|
|
setQuiz(result);
|
|
setQuestions(result?.questions ?? []);
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const createQuiz = useCallback(
|
|
(courseId, unitId, payload) =>
|
|
request(async () => {
|
|
const { data } = await api.post(`${unitBase(courseId, unitId)}/quiz`, payload);
|
|
const result = data?.data?.data ?? null;
|
|
if (result) {
|
|
setQuiz(result);
|
|
toast("Quiz created.");
|
|
}
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const updateQuiz = useCallback(
|
|
(courseId, unitId, quizId, payload) =>
|
|
request(async () => {
|
|
const { data } = await api.patch(`${unitBase(courseId, unitId)}/quiz/${quizId}`, payload);
|
|
const result = data?.data?.data ?? null;
|
|
if (result) {
|
|
setQuiz(result);
|
|
toast("Quiz updated.");
|
|
}
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const deleteQuiz = useCallback(
|
|
(courseId, unitId, quizId, deletedBy) =>
|
|
request(async () => {
|
|
const { data } = await api.delete(`${unitBase(courseId, unitId)}/quiz/${quizId}`, { data: { deletedBy } });
|
|
setQuiz(null);
|
|
setQuestions([]);
|
|
toast("Quiz archived.");
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
// ── Quiz Archives & Restore ───────────────────────────────────────────────
|
|
|
|
const fetchArchivedQuiz = useCallback(
|
|
(courseId, unitId) =>
|
|
request(async () => {
|
|
const { data } = await api.get(`${unitBase(courseId, unitId)}/quiz/archives`);
|
|
const result = data?.data?.data ?? null;
|
|
setQuiz(result);
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const restoreQuiz = useCallback(
|
|
(courseId, unitId, quizId, restoredBy) =>
|
|
request(async () => {
|
|
const { data } = await api.patch(`${unitBase(courseId, unitId)}/quiz/${quizId}/restore`, { restoredBy });
|
|
const result = data?.data?.data ?? null;
|
|
if (result) {
|
|
setQuiz(result);
|
|
toast("Quiz restored.");
|
|
}
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
// =========================================================================
|
|
// QUIZ QUESTIONS
|
|
// =========================================================================
|
|
|
|
const fetchQuizQuestions = useCallback(
|
|
(courseId, unitId, quizId) =>
|
|
request(async () => {
|
|
const { data } = await api.get(`${unitBase(courseId, unitId)}/quiz/${quizId}/questions`);
|
|
const result = data?.data?.data ?? [];
|
|
setQuestions(result);
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const createQuizQuestion = useCallback(
|
|
(courseId, unitId, quizId, payload) =>
|
|
request(async () => {
|
|
const { data } = await api.post(`${unitBase(courseId, unitId)}/quiz/${quizId}/questions`, payload);
|
|
const result = data?.data?.data ?? null;
|
|
if (result) {
|
|
setQuestions((prev) => [...prev, result]);
|
|
toast("Question added.");
|
|
}
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const updateQuizQuestion = useCallback(
|
|
(courseId, unitId, quizId, questionId, payload) =>
|
|
request(async () => {
|
|
const { data } = await api.patch(`${unitBase(courseId, unitId)}/quiz/${quizId}/questions/${questionId}`, payload);
|
|
const result = data?.data?.data ?? null;
|
|
if (result) {
|
|
setQuestions((prev) => prev.map((q) => (q.question_id === questionId ? result : q)));
|
|
toast("Question updated.");
|
|
}
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const bulkSyncQuizQuestions = useCallback(
|
|
(courseId, unitId, quizId, questions, updatedBy) =>
|
|
request(async () => {
|
|
const { data } = await api.put(`${unitBase(courseId, unitId)}/quiz/${quizId}/questions/bulk-sync`, { questions, updatedBy });
|
|
const result = data?.data?.data ?? [];
|
|
setQuestions(result);
|
|
toast("Quiz saved.");
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const deleteQuizQuestion = useCallback(
|
|
(courseId, unitId, quizId, questionId, deletedBy) =>
|
|
request(async () => {
|
|
const { data } = await api.delete(`${unitBase(courseId, unitId)}/quiz/${quizId}/questions/${questionId}`, { data: { deletedBy } });
|
|
setQuestions((prev) => prev.filter((q) => q.question_id !== questionId));
|
|
toast("Question archived.");
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const bulkArchiveQuizQuestions = useCallback(
|
|
(courseId, unitId, quizId, ids, deletedBy) =>
|
|
request(async () => {
|
|
const { data } = await api.delete(`${unitBase(courseId, unitId)}/quiz/${quizId}/questions/bulk`, { data: { ids, deletedBy } });
|
|
setQuestions((prev) => prev.filter((q) => !ids.includes(q.question_id)));
|
|
toast("Questions archived.");
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
// ── Quiz Question Archives & Restore ─────────────────────────────────────
|
|
|
|
const fetchArchivedQuizQuestion = useCallback(
|
|
(courseId, unitId, quizId, questionId) =>
|
|
request(async () => {
|
|
const { data } = await api.get(`${unitBase(courseId, unitId)}/quiz/${quizId}/questions/archives/${questionId}`);
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const restoreQuizQuestion = useCallback(
|
|
(courseId, unitId, quizId, questionId, restoredBy) =>
|
|
request(async () => {
|
|
const { data } = await api.patch(`${unitBase(courseId, unitId)}/quiz/${quizId}/questions/${questionId}/restore`, { restoredBy });
|
|
toast("Question restored.");
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const bulkRestoreQuizQuestions = useCallback(
|
|
(courseId, unitId, quizId, ids, restoredBy) =>
|
|
request(async () => {
|
|
const { data } = await api.patch(`${unitBase(courseId, unitId)}/quiz/${quizId}/questions/restore/bulk`, { ids, restoredBy });
|
|
toast("Questions restored.");
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
// =========================================================================
|
|
// LESSONS
|
|
// =========================================================================
|
|
|
|
const fetchLessons = useCallback(
|
|
(courseId, unitId, params = {}) =>
|
|
paginatedGet(`${unitBase(courseId, unitId)}/lessons`, setLessons, params),
|
|
[paginatedGet],
|
|
);
|
|
|
|
const fetchLesson = useCallback(
|
|
(courseId, unitId, lessonId) =>
|
|
request(async () => {
|
|
const { data } = await api.get(`${lessonBase(courseId, unitId, lessonId)}`);
|
|
const result = data?.data?.data ?? null;
|
|
setLesson(result);
|
|
setLessonPage(result?.page ?? null);
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const createLesson = useCallback(
|
|
(courseId, unitId, payload) =>
|
|
request(async () => {
|
|
const { data } = await api.post(`${unitBase(courseId, unitId)}/lessons`, payload);
|
|
const lesson = data?.data?.data ?? null;
|
|
if (lesson) {
|
|
setLessons((prev) => [...prev, lesson]);
|
|
toast("Lesson created successfully.");
|
|
}
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const updateLesson = useCallback(
|
|
(courseId, unitId, lessonId, payload) =>
|
|
request(async () => {
|
|
const { data } = await api.put(`${lessonBase(courseId, unitId, lessonId)}`, payload);
|
|
const lesson = data?.data?.data ?? null;
|
|
if (lesson) {
|
|
setLessons((prev) => prev.map((l) => (l.lesson_id === lessonId ? lesson : l)));
|
|
setLesson(lesson);
|
|
toast("Lesson updated successfully.");
|
|
}
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const archiveLesson = useCallback(
|
|
(courseId, unitId, lessonId) =>
|
|
request(async () => {
|
|
const { data } = await api.delete(`${lessonBase(courseId, unitId, lessonId)}`);
|
|
setLessons((prev) => prev.filter((l) => l.lesson_id !== lessonId));
|
|
setLesson((prev) => (prev?.lesson_id === lessonId ? null : prev));
|
|
toast("Lesson archived.");
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const archiveLessons = useCallback(
|
|
(courseId, unitId, { ids }) =>
|
|
request(async () => {
|
|
const { data } = await api.delete(`${unitBase(courseId, unitId)}/lessons/bulk`, { data: { ids } });
|
|
setLessons((prev) => prev.filter((l) => !ids.includes(l.lesson_id)));
|
|
toast("Lessons archived.");
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
// ── Lesson Archives & Restore ─────────────────────────────────────────────
|
|
|
|
const fetchArchivedLessons = useCallback(
|
|
(courseId, unitId, params = {}) =>
|
|
paginatedGet(`${unitBase(courseId, unitId)}/lessons/archives`, setLessons, params),
|
|
[paginatedGet],
|
|
);
|
|
|
|
const fetchArchivedLesson = useCallback(
|
|
(courseId, unitId, lessonId) =>
|
|
request(async () => {
|
|
const { data } = await api.get(`${unitBase(courseId, unitId)}/lessons/archives/${lessonId}`);
|
|
const result = data?.data?.data ?? null;
|
|
setLesson(result);
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const restoreLesson = useCallback(
|
|
(courseId, unitId, lessonId) =>
|
|
request(async () => {
|
|
const { data } = await api.patch(`${lessonBase(courseId, unitId, lessonId)}/restore`);
|
|
const result = data?.data?.data ?? null;
|
|
if (result) {
|
|
setLessons((prev) => prev.filter((l) => l.lesson_id !== lessonId));
|
|
toast("Lesson restored.");
|
|
}
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const restoreLessons = useCallback(
|
|
(courseId, unitId, { ids }) =>
|
|
request(async () => {
|
|
const { data } = await api.patch(`${unitBase(courseId, unitId)}/lessons/restore/bulk`, { ids });
|
|
setLessons((prev) => prev.filter((l) => !ids.includes(l.lesson_id)));
|
|
toast("Lessons restored.");
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const permanentlyDeleteLesson = useCallback(
|
|
(courseId, unitId, lessonId) =>
|
|
request(async () => {
|
|
const { data } = await api.delete(`${lessonBase(courseId, unitId, lessonId)}/permanent`);
|
|
setLessons((prev) => prev.filter((l) => l.lesson_id !== lessonId));
|
|
setLesson((prev) => (prev?.lesson_id === lessonId ? null : prev));
|
|
toast("Lesson permanently deleted.");
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const permanentlyDeleteLessons = useCallback(
|
|
(courseId, unitId, { ids }) =>
|
|
request(async () => {
|
|
const { data } = await api.delete(`${unitBase(courseId, unitId)}/lessons/bulk/permanent`, { data: { ids } });
|
|
setLessons((prev) => prev.filter((l) => !ids.includes(l.lesson_id)));
|
|
toast("Lessons permanently deleted.");
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
// =========================================================================
|
|
// LESSON PAGE
|
|
// =========================================================================
|
|
|
|
const fetchLessonPage = useCallback(
|
|
(courseId, unitId, lessonId) =>
|
|
request(async () => {
|
|
const { data } = await api.get(`${lessonBase(courseId, unitId, lessonId)}/page`);
|
|
const page = data?.data?.data ?? null;
|
|
setLessonPage(page);
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const saveLessonPage = useCallback(
|
|
(courseId, unitId, lessonId, payload) =>
|
|
request(async () => {
|
|
const { data } = await api.put(`${lessonBase(courseId, unitId, lessonId)}/page`, payload);
|
|
const page = data?.data?.data ?? null;
|
|
if (page) {
|
|
setLessonPage(page);
|
|
toast("Lesson page saved.");
|
|
}
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
// =========================================================================
|
|
// COURSE ASSESSMENT
|
|
// =========================================================================
|
|
|
|
const fetchAssessment = useCallback(
|
|
(courseId) =>
|
|
request(async () => {
|
|
const { data } = await api.get(`${BASE}/${courseId}/assessment`);
|
|
const result = data?.data?.data ?? null;
|
|
setAssessment(result);
|
|
setQuestions(result?.questions ?? []);
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const createAssessment = useCallback(
|
|
(courseId, payload) =>
|
|
request(async () => {
|
|
const { data } = await api.post(`${BASE}/${courseId}/assessment`, payload);
|
|
const result = data?.data?.data ?? null;
|
|
if (result) {
|
|
setAssessment(result);
|
|
toast("Assessment created.");
|
|
}
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const updateAssessment = useCallback(
|
|
(courseId, assessmentId, payload) =>
|
|
request(async () => {
|
|
const { data } = await api.patch(`${BASE}/${courseId}/assessment/${assessmentId}`, payload);
|
|
const result = data?.data?.data ?? null;
|
|
if (result) {
|
|
setAssessment(result);
|
|
toast("Assessment updated.");
|
|
}
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const deleteAssessment = useCallback(
|
|
(courseId, assessmentId, deletedBy) =>
|
|
request(async () => {
|
|
const { data } = await api.delete(`${BASE}/${courseId}/assessment/${assessmentId}`, { data: { deletedBy } });
|
|
setAssessment(null);
|
|
setQuestions([]);
|
|
toast("Assessment archived.");
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
// ── Assessment Archives & Restore ─────────────────────────────────────────
|
|
|
|
const fetchArchivedAssessment = useCallback(
|
|
(courseId) =>
|
|
request(async () => {
|
|
const { data } = await api.get(`${BASE}/${courseId}/assessment/archives`);
|
|
const result = data?.data?.data ?? null;
|
|
setAssessment(result);
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const restoreAssessment = useCallback(
|
|
(courseId, assessmentId, restoredBy) =>
|
|
request(async () => {
|
|
const { data } = await api.patch(`${BASE}/${courseId}/assessment/${assessmentId}/restore`, { restoredBy });
|
|
const result = data?.data?.data ?? null;
|
|
if (result) {
|
|
setAssessment(result);
|
|
toast("Assessment restored.");
|
|
}
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
// =========================================================================
|
|
// ASSESSMENT QUESTIONS
|
|
// =========================================================================
|
|
|
|
const fetchAssessmentQuestions = useCallback(
|
|
(courseId, assessmentId) =>
|
|
request(async () => {
|
|
const { data } = await api.get(`${BASE}/${courseId}/assessment/${assessmentId}/questions`);
|
|
const result = data?.data?.data ?? [];
|
|
setQuestions(result);
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const createAssessmentQuestion = useCallback(
|
|
(courseId, assessmentId, payload) =>
|
|
request(async () => {
|
|
const { data } = await api.post(`${BASE}/${courseId}/assessment/${assessmentId}/questions`, payload);
|
|
const result = data?.data?.data ?? null;
|
|
if (result) {
|
|
setQuestions((prev) => [...prev, result]);
|
|
toast("Question added.");
|
|
}
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const updateAssessmentQuestion = useCallback(
|
|
(courseId, assessmentId, questionId, payload) =>
|
|
request(async () => {
|
|
const { data } = await api.patch(`${BASE}/${courseId}/assessment/${assessmentId}/questions/${questionId}`, payload);
|
|
const result = data?.data?.data ?? null;
|
|
if (result) {
|
|
setQuestions((prev) => prev.map((q) => (q.question_id === questionId ? result : q)));
|
|
toast("Question updated.");
|
|
}
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const bulkSyncAssessmentQuestions = useCallback(
|
|
(courseId, assessmentId, questions, updatedBy) =>
|
|
request(async () => {
|
|
const { data } = await api.put(`${BASE}/${courseId}/assessment/${assessmentId}/questions/bulk-sync`, { questions, updatedBy });
|
|
const result = data?.data?.data ?? [];
|
|
setQuestions(result);
|
|
toast("Assessment saved.");
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const deleteAssessmentQuestion = useCallback(
|
|
(courseId, assessmentId, questionId, deletedBy) =>
|
|
request(async () => {
|
|
const { data } = await api.delete(`${BASE}/${courseId}/assessment/${assessmentId}/questions/${questionId}`, { data: { deletedBy } });
|
|
setQuestions((prev) => prev.filter((q) => q.question_id !== questionId));
|
|
toast("Question archived.");
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const bulkArchiveAssessmentQuestions = useCallback(
|
|
(courseId, assessmentId, ids, deletedBy) =>
|
|
request(async () => {
|
|
const { data } = await api.delete(`${BASE}/${courseId}/assessment/${assessmentId}/questions/bulk`, { data: { ids, deletedBy } });
|
|
setQuestions((prev) => prev.filter((q) => !ids.includes(q.question_id)));
|
|
toast("Questions archived.");
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
// ── Assessment Question Archives & Restore ────────────────────────────────
|
|
|
|
const fetchArchivedAssessmentQuestion = useCallback(
|
|
(courseId, assessmentId, questionId) =>
|
|
request(async () => {
|
|
const { data } = await api.get(`${BASE}/${courseId}/assessment/${assessmentId}/questions/archives/${questionId}`);
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const restoreAssessmentQuestion = useCallback(
|
|
(courseId, assessmentId, questionId, restoredBy) =>
|
|
request(async () => {
|
|
const { data } = await api.patch(`${BASE}/${courseId}/assessment/${assessmentId}/questions/${questionId}/restore`, { restoredBy });
|
|
toast("Question restored.");
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const bulkRestoreAssessmentQuestions = useCallback(
|
|
(courseId, assessmentId, ids, restoredBy) =>
|
|
request(async () => {
|
|
const { data } = await api.patch(`${BASE}/${courseId}/assessment/${assessmentId}/questions/restore/bulk`, { ids, restoredBy });
|
|
toast("Questions restored.");
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
// =========================================================================
|
|
// COMPLETIONS & SESSIONS
|
|
// =========================================================================
|
|
|
|
const fetchQuizCompletions = useCallback(
|
|
(courseId, unitId, quizId) =>
|
|
request(async () => {
|
|
setCompletions(null);
|
|
const { data } = await api.get(`${unitBase(courseId, unitId)}/quiz/${quizId}/completions`);
|
|
setCompletions(data?.data ?? null);
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const fetchAssessmentCompletions = useCallback(
|
|
(courseId, assessmentId) =>
|
|
request(async () => {
|
|
setCompletions(null);
|
|
const { data } = await api.get(`${BASE}/${courseId}/assessment/${assessmentId}/completions`);
|
|
setCompletions(data?.data ?? null);
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const fetchAssessmentSessions = useCallback(
|
|
(courseId, assessmentId) =>
|
|
request(async () => {
|
|
setSessions(null);
|
|
const { data } = await api.get(`${BASE}/${courseId}/assessment/${assessmentId}/sessions`);
|
|
setSessions(data?.data ?? null);
|
|
return data;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
// =========================================================================
|
|
// COURSE PRODUCT & CATEGORIES
|
|
// =========================================================================
|
|
|
|
const fetchCourseProduct = useCallback(
|
|
(courseId) => request(async () => {
|
|
const { data } = await api.get(`/admin/products/courses/${courseId}/product`);
|
|
return data.data ?? null;
|
|
}), [request],
|
|
);
|
|
|
|
const saveCourseProduct = useCallback(
|
|
(courseId, payload) => request(async () => {
|
|
const { data } = await api.put(`/admin/products/courses/${courseId}/product`, payload);
|
|
toast("Product listing saved.");
|
|
return data.data ?? null;
|
|
}), [request],
|
|
);
|
|
|
|
const removeCourseProduct = useCallback(
|
|
(courseId) => request(async () => {
|
|
await api.delete(`/admin/products/courses/${courseId}/product`);
|
|
toast("Product listing removed.");
|
|
return true;
|
|
}), [request],
|
|
);
|
|
|
|
const fetchCourseCategories = useCallback(
|
|
(courseId) => request(async () => {
|
|
const { data } = await api.get(`/admin/products/courses/${courseId}/categories`);
|
|
return data.data ?? [];
|
|
}), [request],
|
|
);
|
|
|
|
const syncCourseCategories = useCallback(
|
|
(courseId, categoryIds) => request(async () => {
|
|
const { data } = await api.post(`/admin/products/courses/${courseId}/categories`, { category_ids: categoryIds });
|
|
toast("Categories updated.");
|
|
return data.data ?? [];
|
|
}), [request],
|
|
);
|
|
|
|
const fetchInstructors = useCallback(
|
|
(courseId) => request(async () => {
|
|
const { data } = await api.get(`${BASE}/${courseId}/instructors`);
|
|
return data.data ?? [];
|
|
}), [request],
|
|
);
|
|
|
|
const syncInstructors = useCallback(
|
|
(courseId, instructors) => request(async () => {
|
|
await api.put(`${BASE}/${courseId}/instructors`, { instructors });
|
|
toast("Instructors updated.");
|
|
}), [request],
|
|
);
|
|
|
|
const fetchCourseAchievements = useCallback(
|
|
(courseId) => request(async () => {
|
|
const { data } = await api.get(`${BASE}/${courseId}/achievements`);
|
|
return (data?.data?.data ?? []).map((r) => r.achievement_key);
|
|
}), [request],
|
|
);
|
|
|
|
const syncCourseAchievements = useCallback(
|
|
(courseId, achievement_keys) => request(async () => {
|
|
await api.put(`${BASE}/${courseId}/achievements`, { achievement_keys });
|
|
toast("Rewards updated.");
|
|
}), [request],
|
|
);
|
|
|
|
const fetchCourseFieldValues = useCallback(
|
|
(field) =>
|
|
request(async () => {
|
|
const { data } = await api.get(`${BASE}/field-values`, { params: { field } });
|
|
const result = data?.data ?? [];
|
|
return result;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const fetchUnitFieldValues = useCallback(
|
|
(courseId, field) =>
|
|
request(async () => {
|
|
const { data } = await api.get(`${BASE}/${courseId}/field-values`, { params: { field } });
|
|
const result = data?.data ?? [];
|
|
return result;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
const fetchLessonFieldValues = useCallback(
|
|
(courseId, unitId, field) =>
|
|
request(async () => {
|
|
const { data } = await api.get(`${unitBase(courseId, unitId)}/field-values`, { params: { field } });
|
|
const result = data?.data ?? [];
|
|
return result;
|
|
}),
|
|
[request],
|
|
);
|
|
|
|
// ─── Provider value ───────────────────────────────────────────────────────
|
|
return (
|
|
<CoursesContext.Provider value={{
|
|
// ── state ──────────────────────────────────────────────────────────────
|
|
courses, course,
|
|
units, unit,
|
|
lessons, lesson,
|
|
lessonPage,
|
|
prerequisites,
|
|
quiz,
|
|
questions,
|
|
assessment,
|
|
attributes,
|
|
pagination,
|
|
loading,
|
|
|
|
// ── setters ────────────────────────────────────────────────────────────
|
|
setCourse,
|
|
setUnit,
|
|
setLesson,
|
|
setLessonPage,
|
|
setPrerequisites,
|
|
setQuiz,
|
|
setQuestions,
|
|
setAssessment,
|
|
setPagination,
|
|
|
|
// ── courses ────────────────────────────────────────────────────────────
|
|
fetchCourses,
|
|
fetchCourse,
|
|
createCourse,
|
|
updateCourse,
|
|
archiveCourse,
|
|
archiveCourses,
|
|
|
|
// ── course archives & restore ──────────────────────────────────────────
|
|
fetchArchivedCourses,
|
|
fetchArchivedCourse,
|
|
restoreCourse,
|
|
restoreCourses,
|
|
permanentlyDeleteCourse,
|
|
permanentlyDeleteCourses,
|
|
fetchCoursePermanentDeleteImpact,
|
|
|
|
// ── prerequisites ──────────────────────────────────────────────────────
|
|
fetchPrerequisites,
|
|
syncPrerequisites,
|
|
|
|
// ── units ──────────────────────────────────────────────────────────────
|
|
fetchUnits,
|
|
fetchUnit,
|
|
createUnit,
|
|
updateUnit,
|
|
archiveUnit,
|
|
archiveUnits,
|
|
|
|
// ── unit archives & restore ────────────────────────────────────────────
|
|
fetchArchivedUnits,
|
|
fetchArchivedUnit,
|
|
restoreUnit,
|
|
restoreUnits,
|
|
permanentlyDeleteUnit,
|
|
permanentlyDeleteUnits,
|
|
fetchUnitPermanentDeleteImpact,
|
|
|
|
// ── unit quiz ──────────────────────────────────────────────────────────
|
|
fetchQuiz,
|
|
createQuiz,
|
|
updateQuiz,
|
|
deleteQuiz,
|
|
|
|
// ── quiz archives & restore ────────────────────────────────────────────
|
|
fetchArchivedQuiz,
|
|
restoreQuiz,
|
|
|
|
// ── quiz questions ─────────────────────────────────────────────────────
|
|
fetchQuizQuestions,
|
|
createQuizQuestion,
|
|
updateQuizQuestion,
|
|
deleteQuizQuestion,
|
|
bulkArchiveQuizQuestions,
|
|
bulkSyncQuizQuestions,
|
|
|
|
// ── quiz question archives & restore ───────────────────────────────────
|
|
fetchArchivedQuizQuestion,
|
|
restoreQuizQuestion,
|
|
bulkRestoreQuizQuestions,
|
|
|
|
// ── lessons ────────────────────────────────────────────────────────────
|
|
fetchLessons,
|
|
fetchLesson,
|
|
createLesson,
|
|
updateLesson,
|
|
archiveLesson,
|
|
archiveLessons,
|
|
|
|
// ── lesson archives & restore ──────────────────────────────────────────
|
|
fetchArchivedLessons,
|
|
fetchArchivedLesson,
|
|
restoreLesson,
|
|
restoreLessons,
|
|
permanentlyDeleteLesson,
|
|
permanentlyDeleteLessons,
|
|
|
|
// ── lesson page ────────────────────────────────────────────────────────
|
|
fetchLessonPage,
|
|
saveLessonPage,
|
|
|
|
// ── completions & sessions ─────────────────────────────────────────────
|
|
completions, sessions,
|
|
fetchQuizCompletions,
|
|
fetchAssessmentCompletions,
|
|
fetchAssessmentSessions,
|
|
|
|
// ── assessment ─────────────────────────────────────────────────────────
|
|
fetchAssessment,
|
|
createAssessment,
|
|
updateAssessment,
|
|
deleteAssessment,
|
|
|
|
// ── assessment archives & restore ──────────────────────────────────────
|
|
fetchArchivedAssessment,
|
|
restoreAssessment,
|
|
|
|
// ── assessment questions ───────────────────────────────────────────────
|
|
fetchAssessmentQuestions,
|
|
createAssessmentQuestion,
|
|
updateAssessmentQuestion,
|
|
deleteAssessmentQuestion,
|
|
bulkArchiveAssessmentQuestions,
|
|
bulkSyncAssessmentQuestions,
|
|
|
|
// ── assessment question archives & restore ─────────────────────────────
|
|
fetchArchivedAssessmentQuestion,
|
|
restoreAssessmentQuestion,
|
|
bulkRestoreAssessmentQuestions,
|
|
|
|
// ── fetch field values ─────────────────────────────
|
|
fetchCourseFieldValues,
|
|
fetchUnitFieldValues,
|
|
fetchLessonFieldValues,
|
|
|
|
// ── course product & categories ────────────────────
|
|
fetchCourseProduct,
|
|
saveCourseProduct,
|
|
removeCourseProduct,
|
|
fetchCourseCategories,
|
|
syncCourseCategories,
|
|
fetchInstructors,
|
|
syncInstructors,
|
|
fetchCourseAchievements,
|
|
syncCourseAchievements,
|
|
}}>
|
|
{children}
|
|
</CoursesContext.Provider>
|
|
);
|
|
} |