diff --git a/src/contexts/AdminCoursesContext.jsx b/src/contexts/AdminCoursesContext.jsx
index cee9906..1371104 100644
--- a/src/contexts/AdminCoursesContext.jsx
+++ b/src/contexts/AdminCoursesContext.jsx
@@ -23,6 +23,22 @@ const PAGINATION_INIT = {
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 }) {
@@ -288,7 +304,7 @@ export function CoursesProvider({ children }) {
const fetchUnit = useCallback(
(courseId, unitId) =>
request(async () => {
- const { data } = await api.get(`${BASE}/${courseId}/units/${unitId}`);
+ const { data } = await api.get(`${unitBase(courseId, unitId)}`);
const result = data?.data?.data ?? null;
setUnit(result);
setLessons(result?.lessons ?? []);
@@ -315,7 +331,7 @@ export function CoursesProvider({ children }) {
const updateUnit = useCallback(
(courseId, unitId, payload) =>
request(async () => {
- const { data } = await api.put(`${BASE}/${courseId}/units/${unitId}`, payload);
+ 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)));
@@ -330,7 +346,7 @@ export function CoursesProvider({ children }) {
const archiveUnit = useCallback(
(courseId, unitId) =>
request(async () => {
- const { data } = await api.delete(`${BASE}/${courseId}/units/${unitId}`);
+ 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.");
@@ -372,7 +388,7 @@ export function CoursesProvider({ children }) {
const restoreUnit = useCallback(
(courseId, unitId) =>
request(async () => {
- const { data } = await api.patch(`${BASE}/${courseId}/units/${unitId}/restore`);
+ 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));
@@ -397,7 +413,7 @@ export function CoursesProvider({ children }) {
const permanentlyDeleteUnit = useCallback(
(courseId, unitId) =>
request(async () => {
- const { data } = await api.delete(`${BASE}/${courseId}/units/${unitId}/permanent`);
+ 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.");
@@ -420,7 +436,7 @@ export function CoursesProvider({ children }) {
const fetchUnitPermanentDeleteImpact = useCallback(
(courseId, unitId) =>
request(async () => {
- const { data } = await api.get(`${BASE}/${courseId}/units/${unitId}/permanent-delete-impact`);
+ const { data } = await api.get(`${unitBase(courseId, unitId)}/permanent-delete-impact`);
const { lessonCount } = data?.data ?? {};
return [{ label: "lesson(s)", count: lessonCount ?? 0 }];
}),
@@ -434,7 +450,7 @@ export function CoursesProvider({ children }) {
const fetchQuiz = useCallback(
(courseId, unitId) =>
request(async () => {
- const { data } = await api.get(`${BASE}/${courseId}/units/${unitId}/quiz`);
+ const { data } = await api.get(`${unitBase(courseId, unitId)}/quiz`);
const result = data?.data?.data ?? null;
setQuiz(result);
setQuestions(result?.questions ?? []);
@@ -446,7 +462,7 @@ export function CoursesProvider({ children }) {
const createQuiz = useCallback(
(courseId, unitId, payload) =>
request(async () => {
- const { data } = await api.post(`${BASE}/${courseId}/units/${unitId}/quiz`, payload);
+ const { data } = await api.post(`${unitBase(courseId, unitId)}/quiz`, payload);
const result = data?.data?.data ?? null;
if (result) {
setQuiz(result);
@@ -460,7 +476,7 @@ export function CoursesProvider({ children }) {
const updateQuiz = useCallback(
(courseId, unitId, quizId, payload) =>
request(async () => {
- const { data } = await api.patch(`${BASE}/${courseId}/units/${unitId}/quiz/${quizId}`, payload);
+ const { data } = await api.patch(`${unitBase(courseId, unitId)}/quiz/${quizId}`, payload);
const result = data?.data?.data ?? null;
if (result) {
setQuiz(result);
@@ -474,7 +490,7 @@ export function CoursesProvider({ children }) {
const deleteQuiz = useCallback(
(courseId, unitId, quizId, deletedBy) =>
request(async () => {
- const { data } = await api.delete(`${BASE}/${courseId}/units/${unitId}/quiz/${quizId}`, { data: { deletedBy } });
+ const { data } = await api.delete(`${unitBase(courseId, unitId)}/quiz/${quizId}`, { data: { deletedBy } });
setQuiz(null);
setQuestions([]);
toast("Quiz archived.");
@@ -488,7 +504,7 @@ export function CoursesProvider({ children }) {
const fetchArchivedQuiz = useCallback(
(courseId, unitId) =>
request(async () => {
- const { data } = await api.get(`${BASE}/${courseId}/units/${unitId}/quiz/archives`);
+ const { data } = await api.get(`${unitBase(courseId, unitId)}/quiz/archives`);
const result = data?.data?.data ?? null;
setQuiz(result);
return data;
@@ -499,7 +515,7 @@ export function CoursesProvider({ children }) {
const restoreQuiz = useCallback(
(courseId, unitId, quizId, restoredBy) =>
request(async () => {
- const { data } = await api.patch(`${BASE}/${courseId}/units/${unitId}/quiz/${quizId}/restore`, { restoredBy });
+ const { data } = await api.patch(`${unitBase(courseId, unitId)}/quiz/${quizId}/restore`, { restoredBy });
const result = data?.data?.data ?? null;
if (result) {
setQuiz(result);
@@ -517,7 +533,7 @@ export function CoursesProvider({ children }) {
const fetchQuizQuestions = useCallback(
(courseId, unitId, quizId) =>
request(async () => {
- const { data } = await api.get(`${BASE}/${courseId}/units/${unitId}/quiz/${quizId}/questions`);
+ const { data } = await api.get(`${unitBase(courseId, unitId)}/quiz/${quizId}/questions`);
const result = data?.data?.data ?? [];
setQuestions(result);
return data;
@@ -528,7 +544,7 @@ export function CoursesProvider({ children }) {
const createQuizQuestion = useCallback(
(courseId, unitId, quizId, payload) =>
request(async () => {
- const { data } = await api.post(`${BASE}/${courseId}/units/${unitId}/quiz/${quizId}/questions`, payload);
+ const { data } = await api.post(`${unitBase(courseId, unitId)}/quiz/${quizId}/questions`, payload);
const result = data?.data?.data ?? null;
if (result) {
setQuestions((prev) => [...prev, result]);
@@ -542,7 +558,7 @@ export function CoursesProvider({ children }) {
const updateQuizQuestion = useCallback(
(courseId, unitId, quizId, questionId, payload) =>
request(async () => {
- const { data } = await api.patch(`${BASE}/${courseId}/units/${unitId}/quiz/${quizId}/questions/${questionId}`, payload);
+ 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)));
@@ -556,7 +572,7 @@ export function CoursesProvider({ children }) {
const bulkSyncQuizQuestions = useCallback(
(courseId, unitId, quizId, questions, updatedBy) =>
request(async () => {
- const { data } = await api.put(`${BASE}/${courseId}/units/${unitId}/quiz/${quizId}/questions/bulk-sync`, { questions, updatedBy });
+ 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.");
@@ -568,7 +584,7 @@ export function CoursesProvider({ children }) {
const deleteQuizQuestion = useCallback(
(courseId, unitId, quizId, questionId, deletedBy) =>
request(async () => {
- const { data } = await api.delete(`${BASE}/${courseId}/units/${unitId}/quiz/${quizId}/questions/${questionId}`, { data: { deletedBy } });
+ 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;
@@ -579,7 +595,7 @@ export function CoursesProvider({ children }) {
const bulkArchiveQuizQuestions = useCallback(
(courseId, unitId, quizId, ids, deletedBy) =>
request(async () => {
- const { data } = await api.delete(`${BASE}/${courseId}/units/${unitId}/quiz/${quizId}/questions/bulk`, { data: { ids, deletedBy } });
+ 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;
@@ -592,7 +608,7 @@ export function CoursesProvider({ children }) {
const fetchArchivedQuizQuestion = useCallback(
(courseId, unitId, quizId, questionId) =>
request(async () => {
- const { data } = await api.get(`${BASE}/${courseId}/units/${unitId}/quiz/${quizId}/questions/archives/${questionId}`);
+ const { data } = await api.get(`${unitBase(courseId, unitId)}/quiz/${quizId}/questions/archives/${questionId}`);
return data;
}),
[request],
@@ -601,7 +617,7 @@ export function CoursesProvider({ children }) {
const restoreQuizQuestion = useCallback(
(courseId, unitId, quizId, questionId, restoredBy) =>
request(async () => {
- const { data } = await api.patch(`${BASE}/${courseId}/units/${unitId}/quiz/${quizId}/questions/${questionId}/restore`, { restoredBy });
+ const { data } = await api.patch(`${unitBase(courseId, unitId)}/quiz/${quizId}/questions/${questionId}/restore`, { restoredBy });
toast("Question restored.");
return data;
}),
@@ -611,7 +627,7 @@ export function CoursesProvider({ children }) {
const bulkRestoreQuizQuestions = useCallback(
(courseId, unitId, quizId, ids, restoredBy) =>
request(async () => {
- const { data } = await api.patch(`${BASE}/${courseId}/units/${unitId}/quiz/${quizId}/questions/restore/bulk`, { ids, restoredBy });
+ const { data } = await api.patch(`${unitBase(courseId, unitId)}/quiz/${quizId}/questions/restore/bulk`, { ids, restoredBy });
toast("Questions restored.");
return data;
}),
@@ -624,14 +640,14 @@ export function CoursesProvider({ children }) {
const fetchLessons = useCallback(
(courseId, unitId, params = {}) =>
- paginatedGet(`${BASE}/${courseId}/units/${unitId}/lessons`, setLessons, params),
+ paginatedGet(`${unitBase(courseId, unitId)}/lessons`, setLessons, params),
[paginatedGet],
);
const fetchLesson = useCallback(
(courseId, unitId, lessonId) =>
request(async () => {
- const { data } = await api.get(`${BASE}/${courseId}/units/${unitId}/lessons/${lessonId}`);
+ const { data } = await api.get(`${lessonBase(courseId, unitId, lessonId)}`);
const result = data?.data?.data ?? null;
setLesson(result);
setLessonPage(result?.page ?? null);
@@ -643,7 +659,7 @@ export function CoursesProvider({ children }) {
const createLesson = useCallback(
(courseId, unitId, payload) =>
request(async () => {
- const { data } = await api.post(`${BASE}/${courseId}/units/${unitId}/lessons`, payload);
+ const { data } = await api.post(`${unitBase(courseId, unitId)}/lessons`, payload);
const lesson = data?.data?.data ?? null;
if (lesson) {
setLessons((prev) => [...prev, lesson]);
@@ -657,7 +673,7 @@ export function CoursesProvider({ children }) {
const updateLesson = useCallback(
(courseId, unitId, lessonId, payload) =>
request(async () => {
- const { data } = await api.put(`${BASE}/${courseId}/units/${unitId}/lessons/${lessonId}`, payload);
+ 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)));
@@ -672,7 +688,7 @@ export function CoursesProvider({ children }) {
const archiveLesson = useCallback(
(courseId, unitId, lessonId) =>
request(async () => {
- const { data } = await api.delete(`${BASE}/${courseId}/units/${unitId}/lessons/${lessonId}`);
+ 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.");
@@ -684,7 +700,7 @@ export function CoursesProvider({ children }) {
const archiveLessons = useCallback(
(courseId, unitId, { ids }) =>
request(async () => {
- const { data } = await api.delete(`${BASE}/${courseId}/units/${unitId}/lessons/bulk`, { data: { ids } });
+ 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;
@@ -696,14 +712,14 @@ export function CoursesProvider({ children }) {
const fetchArchivedLessons = useCallback(
(courseId, unitId, params = {}) =>
- paginatedGet(`${BASE}/${courseId}/units/${unitId}/lessons/archives`, setLessons, params),
+ paginatedGet(`${unitBase(courseId, unitId)}/lessons/archives`, setLessons, params),
[paginatedGet],
);
const fetchArchivedLesson = useCallback(
(courseId, unitId, lessonId) =>
request(async () => {
- const { data } = await api.get(`${BASE}/${courseId}/units/${unitId}/lessons/archives/${lessonId}`);
+ const { data } = await api.get(`${unitBase(courseId, unitId)}/lessons/archives/${lessonId}`);
const result = data?.data?.data ?? null;
setLesson(result);
return data;
@@ -714,7 +730,7 @@ export function CoursesProvider({ children }) {
const restoreLesson = useCallback(
(courseId, unitId, lessonId) =>
request(async () => {
- const { data } = await api.patch(`${BASE}/${courseId}/units/${unitId}/lessons/${lessonId}/restore`);
+ 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));
@@ -728,7 +744,7 @@ export function CoursesProvider({ children }) {
const restoreLessons = useCallback(
(courseId, unitId, { ids }) =>
request(async () => {
- const { data } = await api.patch(`${BASE}/${courseId}/units/${unitId}/lessons/restore/bulk`, { ids });
+ 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;
@@ -739,7 +755,7 @@ export function CoursesProvider({ children }) {
const permanentlyDeleteLesson = useCallback(
(courseId, unitId, lessonId) =>
request(async () => {
- const { data } = await api.delete(`${BASE}/${courseId}/units/${unitId}/lessons/${lessonId}/permanent`);
+ 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.");
@@ -751,7 +767,7 @@ export function CoursesProvider({ children }) {
const permanentlyDeleteLessons = useCallback(
(courseId, unitId, { ids }) =>
request(async () => {
- const { data } = await api.delete(`${BASE}/${courseId}/units/${unitId}/lessons/bulk/permanent`, { data: { ids } });
+ 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;
@@ -766,7 +782,7 @@ export function CoursesProvider({ children }) {
const fetchLessonPage = useCallback(
(courseId, unitId, lessonId) =>
request(async () => {
- const { data } = await api.get(`${BASE}/${courseId}/units/${unitId}/lessons/${lessonId}/page`);
+ const { data } = await api.get(`${lessonBase(courseId, unitId, lessonId)}/page`);
const page = data?.data?.data ?? null;
setLessonPage(page);
return data;
@@ -777,7 +793,7 @@ export function CoursesProvider({ children }) {
const saveLessonPage = useCallback(
(courseId, unitId, lessonId, payload) =>
request(async () => {
- const { data } = await api.put(`${BASE}/${courseId}/units/${unitId}/lessons/${lessonId}/page`, payload);
+ const { data } = await api.put(`${lessonBase(courseId, unitId, lessonId)}/page`, payload);
const page = data?.data?.data ?? null;
if (page) {
setLessonPage(page);
@@ -987,7 +1003,7 @@ export function CoursesProvider({ children }) {
(courseId, unitId, quizId) =>
request(async () => {
setCompletions(null);
- const { data } = await api.get(`${BASE}/${courseId}/units/${unitId}/quiz/${quizId}/completions`);
+ const { data } = await api.get(`${unitBase(courseId, unitId)}/quiz/${quizId}/completions`);
setCompletions(data?.data ?? null);
return data;
}),
@@ -1109,7 +1125,7 @@ export function CoursesProvider({ children }) {
const fetchLessonFieldValues = useCallback(
(courseId, unitId, field) =>
request(async () => {
- const { data } = await api.get(`${BASE}/${courseId}/units/${unitId}/field-values`, { params: { field } });
+ const { data } = await api.get(`${unitBase(courseId, unitId)}/field-values`, { params: { field } });
const result = data?.data ?? [];
return result;
}),
diff --git a/src/contexts/AdminLibraryContext.jsx b/src/contexts/AdminLibraryContext.jsx
new file mode 100644
index 0000000..5551fb1
--- /dev/null
+++ b/src/contexts/AdminLibraryContext.jsx
@@ -0,0 +1,423 @@
+// ─── AdminLibraryContext.jsx ───────────────────────────────────────────────────
+// Standalone Units & Lessons libraries (junction revamp).
+// Units and Lessons run independently of Courses:
+// /admin/units → unit library CRUD + lesson attach/detach/reorder + quiz
+// /admin/lessons → lesson library CRUD + page content
+// Course membership stays under /admin/courses/:courseId/units (AdminCoursesContext).
+
+import { createContext, useCallback, useContext, useState } from "react";
+import api from "@/utils/api.util";
+import { toast } from "sonner";
+
+const LibraryContext = createContext(null);
+
+export function useLibrary() {
+ const ctx = useContext(LibraryContext);
+ if (!ctx) throw new Error("useLibrary must be used within a LibraryProvider");
+ return ctx;
+}
+
+const PAGINATION_INIT = {
+ page: 1,
+ limit: 10,
+ totalRecords: 0,
+ totalPages: 0,
+ hasPrevPage: false,
+ hasNextPage: false,
+};
+
+const UNITS_BASE = "/admin/units";
+const LESSONS_BASE = "/admin/lessons";
+
+export function LibraryProvider({ children }) {
+
+ // ─── State ────────────────────────────────────────────────────────────────
+ const [units, setUnits] = useState([]);
+ const [unit, setUnit] = useState(null);
+ const [unitsFlat, setUnitsFlat] = useState([]);
+ const [lessons, setLessons] = useState([]);
+ const [lesson, setLesson] = useState(null);
+ const [lessonsFlat, setLessonsFlat] = useState([]);
+ 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.";
+ 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],
+ );
+
+ // =========================================================================
+ // UNIT LIBRARY
+ // =========================================================================
+
+ const fetchUnits = useCallback(
+ (params = {}) => paginatedGet(UNITS_BASE, setUnits, params),
+ [paginatedGet],
+ );
+
+ const fetchUnitsFlat = useCallback(
+ () =>
+ request(async () => {
+ const { data } = await api.get(`${UNITS_BASE}/flat`);
+ setUnitsFlat(data?.data ?? []);
+ return data;
+ }),
+ [request],
+ );
+
+ const fetchUnit = useCallback(
+ (unitId) =>
+ request(async () => {
+ const { data } = await api.get(`${UNITS_BASE}/${unitId}`);
+ const result = data?.data?.data ?? null;
+ setUnit(result);
+ return data;
+ }),
+ [request],
+ );
+
+ const createUnit = useCallback(
+ (payload) =>
+ request(async () => {
+ const { data } = await api.post(UNITS_BASE, payload);
+ const created = data?.data?.data ?? null;
+ if (created) toast("Unit created successfully.");
+ return data;
+ }),
+ [request],
+ );
+
+ const updateUnit = useCallback(
+ (unitId, payload) =>
+ request(async () => {
+ const { data } = await api.put(`${UNITS_BASE}/${unitId}`, payload);
+ const updated = data?.data?.data ?? null;
+ if (updated) {
+ setUnit(updated);
+ toast("Unit updated successfully.");
+ }
+ return data;
+ }),
+ [request],
+ );
+
+ const archiveUnit = useCallback(
+ (unitId) =>
+ request(async () => {
+ const { data } = await api.delete(`${UNITS_BASE}/${unitId}`);
+ toast("Unit archived.");
+ return data;
+ }),
+ [request],
+ );
+
+ const archiveUnits = useCallback(
+ (ids) =>
+ request(async () => {
+ const { data } = await api.delete(`${UNITS_BASE}/bulk`, { data: { ids } });
+ toast(data?.message ?? "Units archived.");
+ return data;
+ }),
+ [request],
+ );
+
+ const fetchArchivedUnits = useCallback(
+ (params = {}) => paginatedGet(`${UNITS_BASE}/archives`, setUnits, params),
+ [paginatedGet],
+ );
+
+ const restoreUnit = useCallback(
+ (unitId) =>
+ request(async () => {
+ const { data } = await api.patch(`${UNITS_BASE}/${unitId}/restore`, {});
+ toast("Unit restored.");
+ return data;
+ }),
+ [request],
+ );
+
+ const restoreUnits = useCallback(
+ (ids) =>
+ request(async () => {
+ const { data } = await api.patch(`${UNITS_BASE}/restore/bulk`, { ids });
+ toast(data?.message ?? "Units restored.");
+ return data;
+ }),
+ [request],
+ );
+
+ const permanentlyDeleteUnit = useCallback(
+ (unitId) =>
+ request(async () => {
+ const { data } = await api.delete(`${UNITS_BASE}/${unitId}/permanent`);
+ toast("Unit permanently deleted.");
+ return data;
+ }),
+ [request],
+ );
+
+ const permanentlyDeleteUnits = useCallback(
+ (ids) =>
+ request(async () => {
+ const { data } = await api.delete(`${UNITS_BASE}/bulk/permanent`, { data: { ids } });
+ toast(data?.message ?? "Units permanently deleted.");
+ return data;
+ }),
+ [request],
+ );
+
+ const fetchUnitArchiveImpact = useCallback(
+ (unitId) =>
+ request(async () => {
+ const { data } = await api.get(`${UNITS_BASE}/${unitId}/archive-impact`);
+ return data?.data ?? null;
+ }),
+ [request],
+ );
+
+ const fetchUnitPermanentDeleteImpact = useCallback(
+ (unitId) =>
+ request(async () => {
+ const { data } = await api.get(`${UNITS_BASE}/${unitId}/permanent-delete-impact`);
+ return data?.data ?? null;
+ }),
+ [request],
+ );
+
+ const fetchUnitFieldValues = useCallback(
+ (field) =>
+ request(async () => {
+ const { data } = await api.get(`${UNITS_BASE}/field-values`, { params: { field } });
+ return data;
+ }),
+ [request],
+ );
+
+ // ── Unit ⇄ Lesson membership ────────────────────────────────────────────
+
+ const attachLessonsToUnit = useCallback(
+ (unitId, lessonIds) =>
+ request(async () => {
+ const { data } = await api.post(`${UNITS_BASE}/${unitId}/lessons`, { lesson_ids: lessonIds });
+ toast(data?.message ?? "Lessons attached.");
+ return data;
+ }),
+ [request],
+ );
+
+ const detachLessonFromUnit = useCallback(
+ (unitId, lessonId) =>
+ request(async () => {
+ const { data } = await api.delete(`${UNITS_BASE}/${unitId}/lessons/${lessonId}`);
+ toast("Lesson detached.");
+ return data;
+ }),
+ [request],
+ );
+
+ const reorderUnitLessons = useCallback(
+ (unitId, lessonIds) =>
+ request(async () => {
+ const { data } = await api.put(`${UNITS_BASE}/${unitId}/lessons/order`, { lesson_ids: lessonIds });
+ toast("Lesson order updated.");
+ return data;
+ }),
+ [request],
+ );
+
+ // =========================================================================
+ // LESSON LIBRARY
+ // =========================================================================
+
+ const fetchLessons = useCallback(
+ (params = {}) => paginatedGet(LESSONS_BASE, setLessons, params),
+ [paginatedGet],
+ );
+
+ const fetchLessonsFlat = useCallback(
+ () =>
+ request(async () => {
+ const { data } = await api.get(`${LESSONS_BASE}/flat`);
+ setLessonsFlat(data?.data ?? []);
+ return data;
+ }),
+ [request],
+ );
+
+ const fetchLesson = useCallback(
+ (lessonId) =>
+ request(async () => {
+ const { data } = await api.get(`${LESSONS_BASE}/${lessonId}`);
+ const result = data?.data?.data ?? null;
+ setLesson(result);
+ return data;
+ }),
+ [request],
+ );
+
+ const createLesson = useCallback(
+ (payload) =>
+ request(async () => {
+ const { data } = await api.post(LESSONS_BASE, payload);
+ const created = data?.data?.data ?? null;
+ if (created) toast("Lesson created successfully.");
+ return data;
+ }),
+ [request],
+ );
+
+ const updateLesson = useCallback(
+ (lessonId, payload) =>
+ request(async () => {
+ const { data } = await api.put(`${LESSONS_BASE}/${lessonId}`, payload);
+ const updated = data?.data?.data ?? null;
+ if (updated) {
+ setLesson(updated);
+ toast("Lesson updated successfully.");
+ }
+ return data;
+ }),
+ [request],
+ );
+
+ const archiveLesson = useCallback(
+ (lessonId) =>
+ request(async () => {
+ const { data } = await api.delete(`${LESSONS_BASE}/${lessonId}`);
+ toast("Lesson archived.");
+ return data;
+ }),
+ [request],
+ );
+
+ const archiveLessons = useCallback(
+ (ids) =>
+ request(async () => {
+ const { data } = await api.delete(`${LESSONS_BASE}/bulk`, { data: { ids } });
+ toast(data?.message ?? "Lessons archived.");
+ return data;
+ }),
+ [request],
+ );
+
+ const fetchArchivedLessons = useCallback(
+ (params = {}) => paginatedGet(`${LESSONS_BASE}/archives`, setLessons, params),
+ [paginatedGet],
+ );
+
+ const restoreLesson = useCallback(
+ (lessonId) =>
+ request(async () => {
+ const { data } = await api.patch(`${LESSONS_BASE}/${lessonId}/restore`, {});
+ toast("Lesson restored.");
+ return data;
+ }),
+ [request],
+ );
+
+ const restoreLessons = useCallback(
+ (ids) =>
+ request(async () => {
+ const { data } = await api.patch(`${LESSONS_BASE}/restore/bulk`, { ids });
+ toast(data?.message ?? "Lessons restored.");
+ return data;
+ }),
+ [request],
+ );
+
+ const permanentlyDeleteLesson = useCallback(
+ (lessonId) =>
+ request(async () => {
+ const { data } = await api.delete(`${LESSONS_BASE}/${lessonId}/permanent`);
+ toast("Lesson permanently deleted.");
+ return data;
+ }),
+ [request],
+ );
+
+ const permanentlyDeleteLessons = useCallback(
+ (ids) =>
+ request(async () => {
+ const { data } = await api.delete(`${LESSONS_BASE}/bulk/permanent`, { data: { ids } });
+ toast(data?.message ?? "Lessons permanently deleted.");
+ return data;
+ }),
+ [request],
+ );
+
+ const fetchLessonPermanentDeleteImpact = useCallback(
+ (lessonId) =>
+ request(async () => {
+ const { data } = await api.get(`${LESSONS_BASE}/${lessonId}/permanent-delete-impact`);
+ return data?.data ?? null;
+ }),
+ [request],
+ );
+
+ const fetchLessonFieldValues = useCallback(
+ (field) =>
+ request(async () => {
+ const { data } = await api.get(`${LESSONS_BASE}/field-values`, { params: { field } });
+ return data;
+ }),
+ [request],
+ );
+
+ // ─── Value ────────────────────────────────────────────────────────────────
+ const value = {
+ // shared table state
+ attributes, pagination, setPagination, loading,
+
+ // unit library
+ units, unit, unitsFlat,
+ fetchUnits, fetchUnitsFlat, fetchUnit, createUnit, updateUnit,
+ archiveUnit, archiveUnits, fetchArchivedUnits,
+ restoreUnit, restoreUnits,
+ permanentlyDeleteUnit, permanentlyDeleteUnits,
+ fetchUnitArchiveImpact, fetchUnitPermanentDeleteImpact,
+ fetchUnitFieldValues,
+ attachLessonsToUnit, detachLessonFromUnit, reorderUnitLessons,
+
+ // lesson library
+ lessons, lesson, lessonsFlat,
+ fetchLessons, fetchLessonsFlat, fetchLesson, createLesson, updateLesson,
+ archiveLesson, archiveLessons, fetchArchivedLessons,
+ restoreLesson, restoreLessons,
+ permanentlyDeleteLesson, permanentlyDeleteLessons,
+ fetchLessonPermanentDeleteImpact,
+ fetchLessonFieldValues,
+ };
+
+ return {children};
+}
diff --git a/src/contexts/ClientLibraryContext.jsx b/src/contexts/ClientLibraryContext.jsx
new file mode 100644
index 0000000..0f50b9c
--- /dev/null
+++ b/src/contexts/ClientLibraryContext.jsx
@@ -0,0 +1,182 @@
+// ─── ClientLibraryContext.jsx ───────────────────────────────────────────────
+// Standalone Unit / Lesson browsing (junction revamp). Units run independently
+// of Courses — this context drives the /units grid, /units/:uuid detail page,
+// and /units/:uuid/read reader.
+
+import { createContext, useCallback, useContext, useState } from "react";
+import api from "@/utils/api.util";
+import { toast } from "sonner";
+
+const ClientLibraryContext = createContext(null);
+
+export function useLibrary() {
+ const ctx = useContext(ClientLibraryContext);
+ if (!ctx) throw new Error("useLibrary must be used within a ClientLibraryProvider");
+ return ctx;
+}
+
+export function ClientLibraryProvider({ children }) {
+ // ── List state
+ const [units, setUnits] = useState([]);
+ const [unitsLoading, setUnitsLoading] = useState(false);
+
+ // ── Detail state — GET /client/units/:uuid/lessons returns unit + lessons + quiz in one call
+ const [unitDetail, setUnitDetail] = useState(null);
+ const [unitDetailLoading, setUnitDetailLoading] = useState(false);
+ const [unitBlocked, setUnitBlocked] = useState(false);
+ const [unitBlockedInfo, setUnitBlockedInfo] = useState(null); // { message, course }
+
+ // ── Lesson state (standalone reader)
+ const [lesson, setLesson] = useState(null);
+ const [lessonLoading, setLessonLoading] = useState(false);
+
+ // ── Quiz state
+ const [quiz, setQuiz] = useState(null);
+ const [quizLoading, setQuizLoading] = useState(false);
+
+ // ─── Actions ────────────────────────────────────────────────────────────
+
+ const getUnits = useCallback(async () => {
+ setUnitsLoading(true);
+ try {
+ const { data } = await api.get("/client/units");
+ setUnits(data.data ?? []);
+ } catch (err) {
+ toast(err?.response?.data?.message ?? "Could not load units.");
+ } finally {
+ setUnitsLoading(false);
+ }
+ }, []);
+
+ const getUnitDetail = useCallback(async (uuid) => {
+ setUnitDetailLoading(true);
+ setUnitBlocked(false);
+ setUnitBlockedInfo(null);
+ try {
+ const { data } = await api.get(`/client/units/${uuid}/lessons`);
+ setUnitDetail(data.data ?? null);
+ } catch (err) {
+ if (err?.response?.status === 403) {
+ setUnitBlocked(true);
+ setUnitBlockedInfo(err.response.data ?? null);
+ } else {
+ toast(err?.response?.data?.message ?? "Could not load unit.");
+ }
+ } finally {
+ setUnitDetailLoading(false);
+ }
+ }, []);
+
+ const getLesson = useCallback(async (uuid) => {
+ setLessonLoading(true);
+ try {
+ const { data } = await api.get(`/client/lessons/${uuid}`);
+ setLesson(data.data ?? null);
+ } catch (err) {
+ if (err?.response?.status === 403) {
+ setUnitBlocked(true);
+ setUnitBlockedInfo(err.response.data ?? null);
+ } else {
+ toast(err?.response?.data?.message ?? "Could not load lesson.");
+ }
+ } finally {
+ setLessonLoading(false);
+ }
+ }, []);
+
+ const getUnitQuiz = useCallback(async (uuid) => {
+ setQuizLoading(true);
+ try {
+ const { data } = await api.get(`/client/units/${uuid}/quiz`);
+ setQuiz(data.data ?? null);
+ } catch (err) {
+ toast(err?.response?.data?.message ?? "Could not load quiz.");
+ } finally {
+ setQuizLoading(false);
+ }
+ }, []);
+
+ const submitUnitQuiz = useCallback(async (uuid, quizId, answers) => {
+ try {
+ const { data } = await api.post(`/client/units/${uuid}/quiz/${quizId}/submit`, { answers });
+ return data.data ?? null;
+ } catch (err) {
+ toast(err?.response?.data?.message ?? "Could not submit quiz.");
+ return null;
+ }
+ }, []);
+
+ const saveUnitQuizDraft = useCallback(async (uuid, quizId, answers) => {
+ try {
+ await api.patch(`/client/units/${uuid}/quiz/${quizId}/draft`, { answers });
+ } catch { /* silent — draft saves are best-effort */ }
+ }, []);
+
+ // Writes lesson progress, then locally patches unitDetail so the reader/detail
+ // page updates without a full refetch (matched by numeric lesson_id — the
+ // response is keyed by id, not uuid).
+ const upsertLessonProgress = useCallback(async (lessonUuid, status, unitUuid) => {
+ try {
+ const { data } = await api.post(`/client/lessons/${lessonUuid}/progress`, {
+ status,
+ ...(unitUuid ? { unit_uuid: unitUuid } : {}),
+ });
+ const result = data.data ?? null;
+
+ setUnitDetail((prev) => {
+ if (!prev || !result?.lesson) return prev;
+ const nextLessons = prev.lessons.map((l) =>
+ l.lesson_id === result.lesson.lesson_id
+ ? { ...l, status: result.lesson.status, completed_at: status === "completed" ? new Date().toISOString() : l.completed_at }
+ : l
+ );
+ const is_completed = nextLessons.length > 0 && nextLessons.every((l) => l.status === "completed");
+ return { ...prev, lessons: nextLessons, is_completed };
+ });
+
+ return result;
+ } catch (err) {
+ toast(err?.response?.data?.message ?? "Could not update progress.");
+ return null;
+ }
+ }, []);
+
+ // ─── Resets ─────────────────────────────────────────────────────────────
+
+ const resetUnitDetail = useCallback(() => {
+ setUnitDetail(null);
+ setUnitBlocked(false);
+ setUnitBlockedInfo(null);
+ }, []);
+ const resetLesson = useCallback(() => setLesson(null), []);
+ const resetQuiz = useCallback(() => setQuiz(null), []);
+
+ // ─── Value ──────────────────────────────────────────────────────────────
+
+ const value = {
+ units, unitsLoading,
+ unitDetail, unitDetailLoading, unitBlocked, unitBlockedInfo,
+ lesson, lessonLoading,
+ quiz, quizLoading,
+
+ getUnits,
+ getUnitDetail,
+ getLesson,
+ getUnitQuiz,
+ submitUnitQuiz,
+ saveUnitQuizDraft,
+ upsertLessonProgress,
+
+ resetUnitDetail,
+ resetLesson,
+ resetQuiz,
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+export default ClientLibraryContext;
diff --git a/src/contexts/provider/AdminProvider.jsx b/src/contexts/provider/AdminProvider.jsx
index 3dfc0c4..ffbd07e 100644
--- a/src/contexts/provider/AdminProvider.jsx
+++ b/src/contexts/provider/AdminProvider.jsx
@@ -4,6 +4,7 @@ import { AdminDashboardProvider } from "../AdminDashboardContext"
import { UserProvider } from "../AdminUserContext";
import { UserGroupProvider } from "../AdminUserGroupContext";
import { CoursesProvider } from "../AdminCoursesContext";
+import { LibraryProvider } from "../AdminLibraryContext";
import { AdminTaskProvider } from "../AdminTaskContext";
import { AdminTiersProvider } from "../AdminTiersContext";
import { AdminCategoriesProvider } from "../AdminCategoriesContext";
@@ -26,11 +27,13 @@ export const AdminProvider = ({ children }) => {
-
-
- {children}
-
-
+
+
+
+ {children}
+
+
+
diff --git a/src/contexts/provider/ClientProvider.jsx b/src/contexts/provider/ClientProvider.jsx
index f4ebd02..96bbd3d 100644
--- a/src/contexts/provider/ClientProvider.jsx
+++ b/src/contexts/provider/ClientProvider.jsx
@@ -1,4 +1,5 @@
import { ClientCoursesProvider } from "../ClientCoursesContext"
+import { ClientLibraryProvider } from "../ClientLibraryContext"
import { ClientTiersProvider } from "../ClientTiersProvider"
import { ProfileProvider } from "../ProfileProvider"
import { TaskProgressProvider } from "../ClientTaskProgressContext"
@@ -15,17 +16,19 @@ export const ClientProvider = ({ children }) => {
-
-
-
-
-
- {children}
-
-
-
-
-
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
diff --git a/src/data/adminTiles.data.js b/src/data/adminTiles.data.js
index f554c13..3b1527e 100644
--- a/src/data/adminTiles.data.js
+++ b/src/data/adminTiles.data.js
@@ -1,4 +1,4 @@
-import { Users, GitFork, FolderOpen, BookText, ListCheck, ShieldCheck, Megaphone, Bell, Trophy } from "lucide-react";
+import { Users, GitFork, FolderOpen, BookText, BookCheck, FileText, ListCheck, ShieldCheck, Megaphone, Bell, Trophy } from "lucide-react";
export const ADMIN_SECTIONS = [
{
@@ -28,6 +28,8 @@ export const ADMIN_SECTIONS = [
description: "Manage tasks and courses",
tiles: [
{ key: "courses", label: "Courses", icon: BookText, link: "/admin/courses" },
+ { key: "units", label: "Units", icon: BookCheck, link: "/admin/units" },
+ { key: "lessons", label: "Lessons", icon: FileText, link: "/admin/lessons" },
{ key: "tasks", label: "Tasks", icon: ListCheck, link: "/admin/taskList" },
{ key: "tiers", label: "Tier Plans", icon: ShieldCheck, link: "/admin/tiers/plans" },
],
diff --git a/src/modules/admin/components/AdminSideTabs.jsx b/src/modules/admin/components/AdminSideTabs.jsx
index 49ed3e6..726f9e1 100644
--- a/src/modules/admin/components/AdminSideTabs.jsx
+++ b/src/modules/admin/components/AdminSideTabs.jsx
@@ -12,7 +12,7 @@ export default function AdminSideTabs() {
const tabItems = [
{ value: "tab-1", label: "Dashboard", paths: ["/admin"] },
{ value: "tab-2", label: "User Management", paths: ["users"] },
- { value: "tab-3", label: "Content Management", paths: ["content", "courses"] },
+ { value: "tab-3", label: "Content Management", paths: ["content", "courses", "units", "lessons"] },
{ value: "tab-5", label: "Site Content", paths: ["contents"] },
]
diff --git a/src/modules/admin/components/courses/LessonsTable.jsx b/src/modules/admin/components/courses/LessonsTable.jsx
index 49234a1..8dd159e 100644
--- a/src/modules/admin/components/courses/LessonsTable.jsx
+++ b/src/modules/admin/components/courses/LessonsTable.jsx
@@ -3,9 +3,12 @@ import { useNavigate } from "react-router-dom";
import { useCourses } from "@/contexts/AdminCoursesContext";
+import api from "@/utils/api.util";
import DataTable from "@/components/generic/Table/DataTable";
import { FilterSheet } from "@/components/generic/Sheet/FilterSheet";
import { ArchiveDialog } from "@/components/generic/Dialogs/ArchiveDialog";
+import AttachLessonsDialog from "../library/AttachLessonsDialog";
+import { Link2 } from "lucide-react";
import { buildDataColumns, columnPinning } from "../../config/courses/lessons/columns.config";
import { buildToolbarActions } from "../../config/courses/lessons/toolbar.config";
@@ -17,6 +20,7 @@ import { getTimestamp } from "@/utils/timestamp.util";
export default function LessonsTable({ courseId, unitId }) {
const [archiveTarget, setArchiveTarget] = useState(null);
const [archiveIds, setArchiveIds] = useState(null);
+ const [attachOpen, setAttachOpen] = useState(false);
const tableRefsRef = useRef({
getFilters: () => [],
@@ -55,17 +59,35 @@ export default function LessonsTable({ courseId, unitId }) {
onArchive: (row) => setArchiveTarget(row),
}), [courseId, unitId]);
- const toolbarActions = buildToolbarActions({
- fetchLessons: (params) => fetchLessons(courseId, unitId, params),
- pagination,
- exportConfig,
- navigate,
- courseId,
- unitId,
- getFilters: () => tableRefsRef.current.getFilters(),
- getSort: () => tableRefsRef.current.getSort(),
- getTableInstance: () => tableRefsRef.current.tableInstance,
- });
+ const toolbarActions = [
+ ...buildToolbarActions({
+ fetchLessons: (params) => fetchLessons(courseId, unitId, params),
+ pagination,
+ exportConfig,
+ navigate,
+ courseId,
+ unitId,
+ getFilters: () => tableRefsRef.current.getFilters(),
+ getSort: () => tableRefsRef.current.getSort(),
+ getTableInstance: () => tableRefsRef.current.tableInstance,
+ }),
+ // Junction revamp — lessons live standalone in the library; attach without re-creating
+ {
+ key: "attach-existing",
+ type: "button",
+ label: "Attach Existing",
+ icon: ,
+ variant: "outline",
+ onClick: () => setAttachOpen(true),
+ },
+ ];
+
+ const handleAttachLessons = async (lessonIds) => {
+ for (const lesson_id of lessonIds) {
+ await api.post(`/admin/courses/${courseId}/units/${unitId}/lessons`, { lesson_id });
+ }
+ fetchLessons(courseId, unitId, { page: 1, limit: pagination?.limit ?? 10 });
+ };
const selectionActions = buildSelectionActions({
exportConfig,
@@ -138,6 +160,15 @@ export default function LessonsTable({ courseId, unitId }) {
loading={loading}
onSuccess={handleArchiveSuccess}
/>
+
+ {/* ── Attach existing library lessons ── */}
+ l.lesson_id)}
+ onAttach={handleAttachLessons}
+ loading={loading}
+ />
>
);
}
diff --git a/src/modules/admin/components/courses/UnitsTable.jsx b/src/modules/admin/components/courses/UnitsTable.jsx
index a3206e2..ebfd47a 100644
--- a/src/modules/admin/components/courses/UnitsTable.jsx
+++ b/src/modules/admin/components/courses/UnitsTable.jsx
@@ -8,6 +8,8 @@ import { useAuth } from "@/contexts/AuthContext";
import DataTable from "@/components/generic/Table/DataTable";
import { FilterSheet } from "@/components/generic/Sheet/FilterSheet";
import { ArchiveDialog } from "@/components/generic/Dialogs/ArchiveDialog";
+import AttachUnitsDialog from "../library/AttachUnitsDialog";
+import { Link2 } from "lucide-react";
import { buildDataColumns, columnPinning } from "../../config/courses/units/columns.config";
import { buildToolbarActions } from "../../config/courses/units/toolbar.config";
@@ -19,6 +21,7 @@ import { getTimestamp } from "@/utils/timestamp.util";
export default function UnitsTable({ courseId }) {
const [archiveTarget, setArchiveTarget] = useState(null);
const [archiveIds, setArchiveIds] = useState(null);
+ const [attachOpen, setAttachOpen] = useState(false);
const tableRefsRef = useRef({
getFilters: () => [],
@@ -58,16 +61,32 @@ export default function UnitsTable({ courseId }) {
onArchive: (row) => setArchiveTarget(row),
}), [courseId]);
- const toolbarActions = buildToolbarActions({
- fetchUnits: (params) => fetchUnits(courseId, params),
- pagination,
- exportConfig,
- navigate,
- courseId,
- getFilters: () => tableRefsRef.current.getFilters(),
- getSort: () => tableRefsRef.current.getSort(),
- getTableInstance: () => tableRefsRef.current.tableInstance,
- });
+ const toolbarActions = [
+ ...buildToolbarActions({
+ fetchUnits: (params) => fetchUnits(courseId, params),
+ pagination,
+ exportConfig,
+ navigate,
+ courseId,
+ getFilters: () => tableRefsRef.current.getFilters(),
+ getSort: () => tableRefsRef.current.getSort(),
+ getTableInstance: () => tableRefsRef.current.tableInstance,
+ }),
+ // Junction revamp — units live standalone in the library; attach without re-creating
+ {
+ key: "attach-existing",
+ type: "button",
+ label: "Attach Existing",
+ icon: ,
+ variant: "outline",
+ onClick: () => setAttachOpen(true),
+ },
+ ];
+
+ const handleAttachUnits = async (unitIds) => {
+ await api.post(`/admin/courses/${courseId}/units/attach`, { unit_ids: unitIds });
+ fetchUnits(courseId, { page: 1, limit: pagination?.limit ?? 10 });
+ };
const selectionActions = buildSelectionActions({
exportConfig,
@@ -150,6 +169,15 @@ export default function UnitsTable({ courseId }) {
loading={loading}
onSuccess={handleArchiveSuccess}
/>
+
+ {/* ── Attach existing library units ── */}
+ u.unit_id)}
+ onAttach={handleAttachUnits}
+ loading={loading}
+ />
>
);
}
diff --git a/src/modules/admin/components/library/ArchivedLessonLibraryTable.jsx b/src/modules/admin/components/library/ArchivedLessonLibraryTable.jsx
new file mode 100644
index 0000000..74961b3
--- /dev/null
+++ b/src/modules/admin/components/library/ArchivedLessonLibraryTable.jsx
@@ -0,0 +1,171 @@
+import { useMemo, useRef, useState, useCallback } from "react";
+import { useNavigate } from "react-router-dom";
+
+import { useLibrary } from "@/contexts/AdminLibraryContext";
+
+import DataTable from "@/components/generic/Table/DataTable";
+import { FilterSheet } from "@/components/generic/Sheet/FilterSheet";
+import { RestoreDialog } from "@/components/generic/Dialogs/RestoreDialog";
+import { PermanentDeleteDialog } from "@/components/generic/Dialogs/PermanentDeleteDialog";
+
+import { buildDataColumns, columnPinning } from "../../config/library/lessons/columns.config";
+import { buildArchivedToolbarActions } from "../../config/library/lessons/toolbar.config";
+import { buildArchivedSelectionActions } from "../../config/library/lessons/selection.config";
+import { buildArchivedRowActions } from "../../config/library/lessons/rowActions.config";
+
+import { getTimestamp } from "@/utils/timestamp.util";
+
+export default function ArchivedLessonLibraryTable() {
+ const [restoreTarget, setRestoreTarget] = useState(null);
+ const [restoreIds, setRestoreIds] = useState(null);
+ const [deleteTarget, setDeleteTarget] = useState(null);
+ const [deleteIds, setDeleteIds] = useState(null);
+
+ const tableRefsRef = useRef({
+ getFilters: () => [],
+ getSort: () => [],
+ resetSelection: () => { },
+ setFilters: () => { },
+ });
+
+ const navigate = useNavigate();
+
+ const {
+ lessons, attributes, pagination, setPagination, loading,
+ fetchArchivedLessons, restoreLesson, restoreLessons,
+ permanentlyDeleteLesson, permanentlyDeleteLessons,
+ fetchLessonFieldValues, fetchLessonPermanentDeleteImpact,
+ } = useLibrary();
+
+ const handleRefsReady = (refs) => {
+ tableRefsRef.current = refs;
+ };
+
+ const exportConfig = {
+ allData: lessons,
+ attributes,
+ filename: `${getTimestamp()}_ArchivedLessonLibrary`,
+ sheetName: "Archived Lessons",
+ };
+
+ const handleFetch = useCallback((params) => fetchArchivedLessons(params), [fetchArchivedLessons]);
+
+ const rowActions = useMemo(() => buildArchivedRowActions({
+ onRestore: (row) => setRestoreTarget(row),
+ onDelete: (row) => setDeleteTarget(row),
+ }), []);
+
+ const toolbarActions = buildArchivedToolbarActions({
+ fetchArchivedLessons,
+ pagination,
+ exportConfig,
+ navigate,
+ getFilters: () => tableRefsRef.current.getFilters(),
+ getSort: () => tableRefsRef.current.getSort(),
+ getTableInstance: () => tableRefsRef.current.tableInstance,
+ });
+
+ const selectionActions = buildArchivedSelectionActions({
+ exportConfig,
+ onRestore: (row) => setRestoreTarget(row),
+ onRestoreMany: (ids) => setRestoreIds(ids),
+ onDelete: (row) => setDeleteTarget(row),
+ onDeleteMany: (ids) => setDeleteIds(ids),
+ getTableInstance: () => tableRefsRef.current.tableInstance,
+ });
+
+ const columns = useMemo(
+ () => buildDataColumns(attributes, rowActions),
+ [attributes, rowActions]
+ );
+
+ const refresh = () => {
+ setRestoreTarget(null);
+ setRestoreIds(null);
+ setDeleteTarget(null);
+ setDeleteIds(null);
+ tableRefsRef.current.resetSelection?.();
+ fetchArchivedLessons({ page: 1, limit: pagination?.limit ?? 10 });
+ };
+
+ return (
+ <>
+ fetchLessonFieldValues(field)}
+ onRefsReady={handleRefsReady}
+ renderFilterSheet={({ open, onOpenChange, column, attr, data, loading }) => (
+
+ )}
+ columnPinning={columnPinning}
+ toolbarActions={toolbarActions}
+ selectionActions={selectionActions}
+ recordLabel="lesson"
+ emptyMessage="No archived lessons."
+ />
+
+ {/* ── Restore ── */}
+ !v && setRestoreTarget(null)}
+ entity={restoreTarget}
+ entityLabel="Lesson"
+ getName={(l) => l?.title}
+ onRestore={(l) => restoreLesson(l?.lesson_id)}
+ loading={loading}
+ onSuccess={refresh}
+ />
+ !v && setRestoreIds(null)}
+ ids={restoreIds ?? []}
+ entityLabel="Lesson"
+ onRestore={(ids) => restoreLessons(ids)}
+ loading={loading}
+ onSuccess={refresh}
+ />
+
+ {/* ── Permanent delete ── */}
+ !v && setDeleteTarget(null)}
+ entity={deleteTarget}
+ entityLabel="Lesson"
+ getName={(l) => l?.title}
+ onDelete={(l) => permanentlyDeleteLesson(l?.lesson_id)}
+ loading={loading}
+ onSuccess={refresh}
+ onImpactCheck={async () => {
+ const impact = await fetchLessonPermanentDeleteImpact(deleteTarget?.lesson_id);
+ const { unitCount } = impact?.data ?? impact ?? {};
+ return [
+ { label: "unit attachment(s) will be removed", count: unitCount ?? 0 },
+ ];
+ }}
+ />
+ !v && setDeleteIds(null)}
+ ids={deleteIds ?? []}
+ entityLabel="Lesson"
+ onDelete={(ids) => permanentlyDeleteLessons(ids)}
+ loading={loading}
+ onSuccess={refresh}
+ />
+ >
+ );
+}
diff --git a/src/modules/admin/components/library/ArchivedUnitLibraryTable.jsx b/src/modules/admin/components/library/ArchivedUnitLibraryTable.jsx
new file mode 100644
index 0000000..e8f4329
--- /dev/null
+++ b/src/modules/admin/components/library/ArchivedUnitLibraryTable.jsx
@@ -0,0 +1,172 @@
+import { useMemo, useRef, useState, useCallback } from "react";
+import { useNavigate } from "react-router-dom";
+
+import { useLibrary } from "@/contexts/AdminLibraryContext";
+
+import DataTable from "@/components/generic/Table/DataTable";
+import { FilterSheet } from "@/components/generic/Sheet/FilterSheet";
+import { RestoreDialog } from "@/components/generic/Dialogs/RestoreDialog";
+import { PermanentDeleteDialog } from "@/components/generic/Dialogs/PermanentDeleteDialog";
+
+import { buildDataColumns, columnPinning } from "../../config/library/units/columns.config";
+import { buildArchivedToolbarActions } from "../../config/library/units/toolbar.config";
+import { buildArchivedSelectionActions } from "../../config/library/units/selection.config";
+import { buildArchivedRowActions } from "../../config/library/units/rowActions.config";
+
+import { getTimestamp } from "@/utils/timestamp.util";
+
+export default function ArchivedUnitLibraryTable() {
+ const [restoreTarget, setRestoreTarget] = useState(null);
+ const [restoreIds, setRestoreIds] = useState(null);
+ const [deleteTarget, setDeleteTarget] = useState(null);
+ const [deleteIds, setDeleteIds] = useState(null);
+
+ const tableRefsRef = useRef({
+ getFilters: () => [],
+ getSort: () => [],
+ resetSelection: () => { },
+ setFilters: () => { },
+ });
+
+ const navigate = useNavigate();
+
+ const {
+ units, attributes, pagination, setPagination, loading,
+ fetchArchivedUnits, restoreUnit, restoreUnits,
+ permanentlyDeleteUnit, permanentlyDeleteUnits,
+ fetchUnitFieldValues, fetchUnitPermanentDeleteImpact,
+ } = useLibrary();
+
+ const handleRefsReady = (refs) => {
+ tableRefsRef.current = refs;
+ };
+
+ const exportConfig = {
+ allData: units,
+ attributes,
+ filename: `${getTimestamp()}_ArchivedUnitLibrary`,
+ sheetName: "Archived Units",
+ };
+
+ const handleFetch = useCallback((params) => fetchArchivedUnits(params), [fetchArchivedUnits]);
+
+ const rowActions = useMemo(() => buildArchivedRowActions({
+ onRestore: (row) => setRestoreTarget(row),
+ onDelete: (row) => setDeleteTarget(row),
+ }), []);
+
+ const toolbarActions = buildArchivedToolbarActions({
+ fetchArchivedUnits,
+ pagination,
+ exportConfig,
+ navigate,
+ getFilters: () => tableRefsRef.current.getFilters(),
+ getSort: () => tableRefsRef.current.getSort(),
+ getTableInstance: () => tableRefsRef.current.tableInstance,
+ });
+
+ const selectionActions = buildArchivedSelectionActions({
+ exportConfig,
+ onRestore: (row) => setRestoreTarget(row),
+ onRestoreMany: (ids) => setRestoreIds(ids),
+ onDelete: (row) => setDeleteTarget(row),
+ onDeleteMany: (ids) => setDeleteIds(ids),
+ getTableInstance: () => tableRefsRef.current.tableInstance,
+ });
+
+ const columns = useMemo(
+ () => buildDataColumns(attributes, rowActions),
+ [attributes, rowActions]
+ );
+
+ const refresh = () => {
+ setRestoreTarget(null);
+ setRestoreIds(null);
+ setDeleteTarget(null);
+ setDeleteIds(null);
+ tableRefsRef.current.resetSelection?.();
+ fetchArchivedUnits({ page: 1, limit: pagination?.limit ?? 10 });
+ };
+
+ return (
+ <>
+ fetchUnitFieldValues(field)}
+ onRefsReady={handleRefsReady}
+ renderFilterSheet={({ open, onOpenChange, column, attr, data, loading }) => (
+
+ )}
+ columnPinning={columnPinning}
+ toolbarActions={toolbarActions}
+ selectionActions={selectionActions}
+ recordLabel="unit"
+ emptyMessage="No archived units."
+ />
+
+ {/* ── Restore ── */}
+ !v && setRestoreTarget(null)}
+ entity={restoreTarget}
+ entityLabel="Unit"
+ getName={(u) => u?.title}
+ onRestore={(u) => restoreUnit(u?.unit_id)}
+ loading={loading}
+ onSuccess={refresh}
+ />
+ !v && setRestoreIds(null)}
+ ids={restoreIds ?? []}
+ entityLabel="Unit"
+ onRestore={(ids) => restoreUnits(ids)}
+ loading={loading}
+ onSuccess={refresh}
+ />
+
+ {/* ── Permanent delete ── */}
+ !v && setDeleteTarget(null)}
+ entity={deleteTarget}
+ entityLabel="Unit"
+ getName={(u) => u?.title}
+ onDelete={(u) => permanentlyDeleteUnit(u?.unit_id)}
+ loading={loading}
+ onSuccess={refresh}
+ onImpactCheck={async () => {
+ const impact = await fetchUnitPermanentDeleteImpact(deleteTarget?.unit_id);
+ const { lessonCount, courseCount } = impact?.data ?? impact ?? {};
+ return [
+ { label: "lesson attachment(s) will be removed", count: lessonCount ?? 0 },
+ { label: "course attachment(s) will be removed", count: courseCount ?? 0 },
+ ];
+ }}
+ />
+ !v && setDeleteIds(null)}
+ ids={deleteIds ?? []}
+ entityLabel="Unit"
+ onDelete={(ids) => permanentlyDeleteUnits(ids)}
+ loading={loading}
+ onSuccess={refresh}
+ />
+ >
+ );
+}
diff --git a/src/modules/admin/components/library/AttachLessonsDialog.jsx b/src/modules/admin/components/library/AttachLessonsDialog.jsx
new file mode 100644
index 0000000..b9d4bb5
--- /dev/null
+++ b/src/modules/admin/components/library/AttachLessonsDialog.jsx
@@ -0,0 +1,129 @@
+// AttachLessonsDialog — pick existing library Lessons and attach them to a Unit.
+// Junction revamp: attaching creates unit_lessons rows; the Lessons stay standalone.
+
+import { useEffect, useMemo, useState } from "react";
+import { Search, Link2 } from "lucide-react";
+
+import { useLibrary } from "@/contexts/AdminLibraryContext";
+import {
+ Dialog, DialogContent, DialogDescription, DialogFooter,
+ DialogHeader, DialogTitle,
+} from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Badge } from "@/components/ui/badge";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import { Spinner } from "@/components/ui/spinner";
+import { formatDuration } from "@/utils/timestamp.util";
+
+export default function AttachLessonsDialog({ open, onOpenChange, attachedLessonIds = [], onAttach, loading }) {
+ const { lessonsFlat, fetchLessonsFlat, loading: libraryLoading } = useLibrary();
+ const [query, setQuery] = useState("");
+ const [selected, setSelected] = useState([]);
+
+ useEffect(() => {
+ if (open) {
+ setSelected([]);
+ setQuery("");
+ fetchLessonsFlat();
+ }
+ }, [open, fetchLessonsFlat]);
+
+ const attachedSet = useMemo(
+ () => new Set(attachedLessonIds.map(String)),
+ [attachedLessonIds]
+ );
+
+ const candidates = useMemo(() => {
+ const q = query.trim().toLowerCase();
+ return (lessonsFlat ?? [])
+ .filter((l) => !attachedSet.has(String(l.lesson_id)))
+ .filter((l) => !q || l.title?.toLowerCase().includes(q));
+ }, [lessonsFlat, attachedSet, query]);
+
+ const toggle = (lessonId) =>
+ setSelected((prev) =>
+ prev.includes(lessonId) ? prev.filter((id) => id !== lessonId) : [...prev, lessonId]
+ );
+
+ const handleAttach = async () => {
+ if (!selected.length) return;
+ await onAttach(selected);
+ onOpenChange(false);
+ };
+
+ return (
+
+ );
+}
diff --git a/src/modules/admin/components/library/AttachUnitsDialog.jsx b/src/modules/admin/components/library/AttachUnitsDialog.jsx
new file mode 100644
index 0000000..c796e46
--- /dev/null
+++ b/src/modules/admin/components/library/AttachUnitsDialog.jsx
@@ -0,0 +1,129 @@
+// AttachUnitsDialog — pick existing library Units and attach them to a Course.
+// Junction revamp: attaching creates course_units rows; the Units stay standalone.
+
+import { useEffect, useMemo, useState } from "react";
+import { Search, Link2 } from "lucide-react";
+
+import { useLibrary } from "@/contexts/AdminLibraryContext";
+import {
+ Dialog, DialogContent, DialogDescription, DialogFooter,
+ DialogHeader, DialogTitle,
+} from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Badge } from "@/components/ui/badge";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import { Spinner } from "@/components/ui/spinner";
+import { formatDuration } from "@/utils/timestamp.util";
+
+export default function AttachUnitsDialog({ open, onOpenChange, attachedUnitIds = [], onAttach, loading }) {
+ const { unitsFlat, fetchUnitsFlat, loading: libraryLoading } = useLibrary();
+ const [query, setQuery] = useState("");
+ const [selected, setSelected] = useState([]);
+
+ useEffect(() => {
+ if (open) {
+ setSelected([]);
+ setQuery("");
+ fetchUnitsFlat();
+ }
+ }, [open, fetchUnitsFlat]);
+
+ const attachedSet = useMemo(
+ () => new Set(attachedUnitIds.map(String)),
+ [attachedUnitIds]
+ );
+
+ const candidates = useMemo(() => {
+ const q = query.trim().toLowerCase();
+ return (unitsFlat ?? [])
+ .filter((u) => !attachedSet.has(String(u.unit_id)))
+ .filter((u) => !q || u.title?.toLowerCase().includes(q));
+ }, [unitsFlat, attachedSet, query]);
+
+ const toggle = (unitId) =>
+ setSelected((prev) =>
+ prev.includes(unitId) ? prev.filter((id) => id !== unitId) : [...prev, unitId]
+ );
+
+ const handleAttach = async () => {
+ if (!selected.length) return;
+ await onAttach(selected);
+ onOpenChange(false);
+ };
+
+ return (
+
+ );
+}
diff --git a/src/modules/admin/components/library/LessonLibraryTable.jsx b/src/modules/admin/components/library/LessonLibraryTable.jsx
new file mode 100644
index 0000000..290ca46
--- /dev/null
+++ b/src/modules/admin/components/library/LessonLibraryTable.jsx
@@ -0,0 +1,140 @@
+import { useMemo, useRef, useState, useCallback } from "react";
+import { useNavigate } from "react-router-dom";
+
+import { useLibrary } from "@/contexts/AdminLibraryContext";
+
+import DataTable from "@/components/generic/Table/DataTable";
+import { FilterSheet } from "@/components/generic/Sheet/FilterSheet";
+import { ArchiveDialog } from "@/components/generic/Dialogs/ArchiveDialog";
+
+import { buildDataColumns, columnPinning } from "../../config/library/lessons/columns.config";
+import { buildToolbarActions } from "../../config/library/lessons/toolbar.config";
+import { buildSelectionActions } from "../../config/library/lessons/selection.config";
+import { buildRowActions } from "../../config/library/lessons/rowActions.config";
+
+import { getTimestamp } from "@/utils/timestamp.util";
+
+export default function LessonLibraryTable() {
+ const [archiveTarget, setArchiveTarget] = useState(null);
+ const [archiveIds, setArchiveIds] = useState(null);
+
+ const tableRefsRef = useRef({
+ getFilters: () => [],
+ getSort: () => [],
+ resetSelection: () => { },
+ setFilters: () => { },
+ });
+
+ const navigate = useNavigate();
+
+ const {
+ lessons, attributes, pagination, setPagination, loading,
+ fetchLessons, archiveLesson, archiveLessons,
+ fetchLessonFieldValues,
+ } = useLibrary();
+
+ const handleRefsReady = (refs) => {
+ tableRefsRef.current = refs;
+ };
+
+ const exportConfig = {
+ allData: lessons,
+ attributes,
+ filename: `${getTimestamp()}_LessonLibrary`,
+ sheetName: "Lessons",
+ };
+
+ const handleFetch = useCallback((params) => fetchLessons(params), [fetchLessons]);
+
+ const rowActions = useMemo(() => buildRowActions({
+ onView: (row) => navigate(`/admin/lessons/${row.lesson_id}/view`),
+ onEdit: (row) => navigate(`/admin/lessons/${row.lesson_id}/edit`),
+ onBuildPage: (row) => navigate(`/admin/lessons/${row.lesson_id}/page`),
+ onViewPage: (row) => navigate(`/admin/lessons/${row.lesson_id}/page/view`),
+ onArchive: (row) => setArchiveTarget(row),
+ }), [navigate]);
+
+ const toolbarActions = buildToolbarActions({
+ fetchLessons,
+ pagination,
+ exportConfig,
+ navigate,
+ getFilters: () => tableRefsRef.current.getFilters(),
+ getSort: () => tableRefsRef.current.getSort(),
+ getTableInstance: () => tableRefsRef.current.tableInstance,
+ });
+
+ const selectionActions = buildSelectionActions({
+ exportConfig,
+ onArchive: (row) => setArchiveTarget(row),
+ onArchiveMany: (ids) => setArchiveIds(ids),
+ getTableInstance: () => tableRefsRef.current.tableInstance,
+ });
+
+ const columns = useMemo(
+ () => buildDataColumns(attributes, rowActions),
+ [attributes, rowActions]
+ );
+
+ const handleArchiveSuccess = () => {
+ setArchiveTarget(null);
+ setArchiveIds(null);
+ tableRefsRef.current.resetSelection?.();
+ fetchLessons({ page: 1, limit: pagination?.limit ?? 10 });
+ };
+
+ return (
+ <>
+ fetchLessonFieldValues(field)}
+ onRefsReady={handleRefsReady}
+ renderFilterSheet={({ open, onOpenChange, column, attr, data, loading }) => (
+
+ )}
+ columnPinning={columnPinning}
+ toolbarActions={toolbarActions}
+ selectionActions={selectionActions}
+ recordLabel="lesson"
+ emptyMessage="No lessons in the library yet."
+ />
+
+ {/* ── Single archive ── */}
+ !v && setArchiveTarget(null)}
+ entity={archiveTarget}
+ entityLabel="Lesson"
+ getName={(l) => l?.title}
+ onArchive={(l) => archiveLesson(l?.lesson_id)}
+ loading={loading}
+ onSuccess={handleArchiveSuccess}
+ />
+
+ {/* ── Bulk archive ── */}
+ !v && setArchiveIds(null)}
+ ids={archiveIds ?? []}
+ entityLabel="Lesson"
+ onArchive={(ids) => archiveLessons(ids)}
+ loading={loading}
+ onSuccess={handleArchiveSuccess}
+ />
+ >
+ );
+}
diff --git a/src/modules/admin/components/library/UnitLibraryTable.jsx b/src/modules/admin/components/library/UnitLibraryTable.jsx
new file mode 100644
index 0000000..68cb0e7
--- /dev/null
+++ b/src/modules/admin/components/library/UnitLibraryTable.jsx
@@ -0,0 +1,150 @@
+import { useMemo, useRef, useState, useCallback } from "react";
+import { useNavigate } from "react-router-dom";
+
+import { useLibrary } from "@/contexts/AdminLibraryContext";
+
+import DataTable from "@/components/generic/Table/DataTable";
+import { FilterSheet } from "@/components/generic/Sheet/FilterSheet";
+import { ArchiveDialog } from "@/components/generic/Dialogs/ArchiveDialog";
+
+import { buildDataColumns, columnPinning } from "../../config/library/units/columns.config";
+import { buildToolbarActions } from "../../config/library/units/toolbar.config";
+import { buildSelectionActions } from "../../config/library/units/selection.config";
+import { buildRowActions } from "../../config/library/units/rowActions.config";
+
+import { getTimestamp } from "@/utils/timestamp.util";
+
+export default function UnitLibraryTable() {
+ const [archiveTarget, setArchiveTarget] = useState(null);
+ const [archiveIds, setArchiveIds] = useState(null);
+
+ const tableRefsRef = useRef({
+ getFilters: () => [],
+ getSort: () => [],
+ resetSelection: () => { },
+ setFilters: () => { },
+ });
+
+ const navigate = useNavigate();
+
+ const {
+ units, attributes, pagination, setPagination, loading,
+ fetchUnits, archiveUnit, archiveUnits,
+ fetchUnitFieldValues, fetchUnitArchiveImpact,
+ } = useLibrary();
+
+ const handleRefsReady = (refs) => {
+ tableRefsRef.current = refs;
+ };
+
+ const exportConfig = {
+ allData: units,
+ attributes,
+ filename: `${getTimestamp()}_UnitLibrary`,
+ sheetName: "Units",
+ };
+
+ const handleFetch = useCallback((params) => fetchUnits(params), [fetchUnits]);
+
+ const rowActions = useMemo(() => buildRowActions({
+ onView: (row) => navigate(`/admin/units/${row.unit_id}/view`),
+ onEdit: (row) => navigate(`/admin/units/${row.unit_id}/edit`),
+ onManageLessons: (row) => navigate(`/admin/units/${row.unit_id}/view`),
+ onQuiz: (row) => navigate(`/admin/units/${row.unit_id}/quiz/edit`),
+ onViewQuiz: (row) => navigate(`/admin/units/${row.unit_id}/quiz/view`),
+ onArchive: (row) => setArchiveTarget(row),
+ }), [navigate]);
+
+ const toolbarActions = buildToolbarActions({
+ fetchUnits,
+ pagination,
+ exportConfig,
+ navigate,
+ getFilters: () => tableRefsRef.current.getFilters(),
+ getSort: () => tableRefsRef.current.getSort(),
+ getTableInstance: () => tableRefsRef.current.tableInstance,
+ });
+
+ const selectionActions = buildSelectionActions({
+ exportConfig,
+ onArchive: (row) => setArchiveTarget(row),
+ onArchiveMany: (ids) => setArchiveIds(ids),
+ getTableInstance: () => tableRefsRef.current.tableInstance,
+ });
+
+ const columns = useMemo(
+ () => buildDataColumns(attributes, rowActions),
+ [attributes, rowActions]
+ );
+
+ const handleArchiveSuccess = () => {
+ setArchiveTarget(null);
+ setArchiveIds(null);
+ tableRefsRef.current.resetSelection?.();
+ fetchUnits({ page: 1, limit: pagination?.limit ?? 10 });
+ };
+
+ return (
+ <>
+ fetchUnitFieldValues(field)}
+ onRefsReady={handleRefsReady}
+ renderFilterSheet={({ open, onOpenChange, column, attr, data, loading }) => (
+
+ )}
+ columnPinning={columnPinning}
+ toolbarActions={toolbarActions}
+ selectionActions={selectionActions}
+ recordLabel="unit"
+ emptyMessage="No units in the library yet."
+ />
+
+ {/* ── Single archive ── */}
+ !v && setArchiveTarget(null)}
+ entity={archiveTarget}
+ entityLabel="Unit"
+ getName={(u) => u?.title}
+ onArchive={(u) => archiveUnit(u?.unit_id)}
+ loading={loading}
+ onSuccess={handleArchiveSuccess}
+ onImpactCheck={async () => {
+ const impact = await fetchUnitArchiveImpact(archiveTarget?.unit_id);
+ const { completionCount, progressCount, courseCount } = impact?.data ?? impact ?? {};
+ return [
+ { label: "student(s) have completed this unit", count: completionCount ?? 0 },
+ { label: "student(s) have reading progress in this unit", count: progressCount ?? 0 },
+ { label: "course(s) currently include this unit", count: courseCount ?? 0 },
+ ];
+ }}
+ />
+
+ {/* ── Bulk archive ── */}
+ !v && setArchiveIds(null)}
+ ids={archiveIds ?? []}
+ entityLabel="Unit"
+ onArchive={(ids) => archiveUnits(ids)}
+ loading={loading}
+ onSuccess={handleArchiveSuccess}
+ />
+ >
+ );
+}
diff --git a/src/modules/admin/config/library/lessons/columns.config.jsx b/src/modules/admin/config/library/lessons/columns.config.jsx
new file mode 100644
index 0000000..34d97dc
--- /dev/null
+++ b/src/modules/admin/config/library/lessons/columns.config.jsx
@@ -0,0 +1,48 @@
+// config/library/lessons/columns.config.jsx
+// Column definitions and pinning for the standalone Lesson Library table.
+
+import { Badge } from "@/components/ui/badge";
+import { Clock } from "lucide-react";
+import { buildColumns } from "@/utils/table.util";
+import { buildSelectionColumn } from "@/components/generic/Table/buildSelectionColumn";
+import { buildRowActionsColumn } from "@/components/generic/Table/buildRowActionsColumn";
+import { formatDuration } from "@/utils/timestamp.util";
+
+export const columnPinning = {
+ right: ["actions"],
+ left: [],
+};
+
+const cellOverrides = {
+ duration_seconds: (info) => {
+ const seconds = parseInt(info.getValue() ?? 0, 10);
+ return (
+
+
+
+ {formatDuration(seconds)}
+
+
+ );
+ },
+ unit_count: (info) => {
+ const n = parseInt(info.getValue() ?? 0, 10);
+ return n > 0 ? (
+
+ in {n} unit{n === 1 ? "" : "s"}
+
+ ) : (
+ standalone
+ );
+ },
+};
+
+export function buildDataColumns(attributes, rowActions) {
+ const visibleAttributes = attributes.filter((a) => !a.hidden);
+
+ return [
+ buildSelectionColumn(),
+ ...buildColumns(visibleAttributes, { cellOverrides }),
+ buildRowActionsColumn(rowActions, { dropdownLabel: "Lesson Actions" }),
+ ];
+}
diff --git a/src/modules/admin/config/library/lessons/rowActions.config.jsx b/src/modules/admin/config/library/lessons/rowActions.config.jsx
new file mode 100644
index 0000000..8bcb376
--- /dev/null
+++ b/src/modules/admin/config/library/lessons/rowActions.config.jsx
@@ -0,0 +1,66 @@
+// config/library/lessons/rowActions.config.jsx
+// Row actions for the Lesson Library (active + archived variants).
+
+import {
+ Eye, Archive, Pencil, FileText, LayoutTemplate,
+ ArchiveRestore, Trash2,
+} from "lucide-react";
+
+export function buildRowActions({ onView, onEdit, onBuildPage, onViewPage, onArchive }) {
+ return [
+ {
+ key: "view",
+ label: "View Info",
+ icon: ,
+ onClick: (row) => onView(row),
+ },
+ {
+ key: "edit",
+ label: "Edit Lesson",
+ icon: ,
+ onClick: (row) => onEdit(row),
+ },
+ {
+ key: "build_page",
+ label: "Page Builder",
+ icon: ,
+ className: "text-sky-700 hover:text-sky-600",
+ onClick: (row) => onBuildPage(row),
+ separator: true,
+ },
+ {
+ key: "view_page",
+ label: "View Page",
+ icon: ,
+ className: "text-sky-700 hover:text-sky-600",
+ onClick: (row) => onViewPage(row),
+ },
+ {
+ key: "archive",
+ label: "Archive",
+ icon: ,
+ className: "text-destructive",
+ onClick: (row) => onArchive(row),
+ separator: true,
+ },
+ ];
+}
+
+export function buildArchivedRowActions({ onRestore, onDelete }) {
+ return [
+ {
+ key: "restore",
+ label: "Restore",
+ icon: ,
+ onClick: (row) => onRestore(row),
+ },
+ {
+ key: "delete",
+ label: "Delete Permanently",
+ icon: ,
+ className: "text-destructive",
+ onClick: (row) => onDelete(row),
+ separator: true,
+ },
+ ];
+}
diff --git a/src/modules/admin/config/library/lessons/selection.config.jsx b/src/modules/admin/config/library/lessons/selection.config.jsx
new file mode 100644
index 0000000..efea486
--- /dev/null
+++ b/src/modules/admin/config/library/lessons/selection.config.jsx
@@ -0,0 +1,66 @@
+// config/library/lessons/selection.config.jsx
+// Bulk selection actions for the Lesson Library (active + archived variants).
+
+import { Download, Archive, ArchiveRestore, Trash2 } from "lucide-react";
+import { exportTableToExcel } from "@/utils/excel.util";
+
+export function buildSelectionActions({ exportConfig, onArchive, onArchiveMany, getTableInstance }) {
+ return [
+ {
+ key: "export-selected",
+ label: "Export",
+ icon: ,
+ onClick: (rows, table) =>
+ exportTableToExcel({
+ ...exportConfig,
+ selectedRows: rows,
+ tableInstance: table ?? getTableInstance(),
+ }),
+ },
+ {
+ key: "archive-selected",
+ label: "Archive",
+ icon: ,
+ className: "text-destructive border-destructive/40 hover:bg-destructive/10 hover:text-destructive",
+ onClick: (rows) => {
+ const ids = rows.map((r) => r.lesson_id);
+ ids.length === 1 ? onArchive(rows[0]) : onArchiveMany(ids);
+ },
+ },
+ ];
+}
+
+export function buildArchivedSelectionActions({ exportConfig, onRestore, onRestoreMany, onDelete, onDeleteMany, getTableInstance }) {
+ return [
+ {
+ key: "export-selected",
+ label: "Export",
+ icon: ,
+ onClick: (rows, table) =>
+ exportTableToExcel({
+ ...exportConfig,
+ selectedRows: rows,
+ tableInstance: table ?? getTableInstance(),
+ }),
+ },
+ {
+ key: "restore-selected",
+ label: "Restore",
+ icon: ,
+ onClick: (rows) => {
+ const ids = rows.map((r) => r.lesson_id);
+ ids.length === 1 ? onRestore(rows[0]) : onRestoreMany(ids);
+ },
+ },
+ {
+ key: "delete-selected",
+ label: "Delete Permanently",
+ icon: ,
+ className: "text-destructive border-destructive/40 hover:bg-destructive/10 hover:text-destructive",
+ onClick: (rows) => {
+ const ids = rows.map((r) => r.lesson_id);
+ ids.length === 1 ? onDelete(rows[0]) : onDeleteMany(ids);
+ },
+ },
+ ];
+}
diff --git a/src/modules/admin/config/library/lessons/toolbar.config.jsx b/src/modules/admin/config/library/lessons/toolbar.config.jsx
new file mode 100644
index 0000000..b07d641
--- /dev/null
+++ b/src/modules/admin/config/library/lessons/toolbar.config.jsx
@@ -0,0 +1,105 @@
+// config/library/lessons/toolbar.config.jsx
+// Toolbar actions for the Lesson Library (active + archived variants).
+
+import { Plus, RefreshCw, Download, Archive, ArrowLeft } from "lucide-react";
+import { exportTableToExcel } from "@/utils/excel.util";
+
+export function buildToolbarActions({
+ fetchLessons,
+ pagination,
+ exportConfig,
+ navigate,
+ getFilters,
+ getSort,
+ getTableInstance,
+}) {
+ return [
+ {
+ key: "refresh",
+ type: "button",
+ label: "Refresh",
+ icon: ,
+ variant: "outline",
+ onClick: () => fetchLessons({
+ page: 1,
+ limit: pagination?.limit ?? 10,
+ filters: getFilters(),
+ sort: getSort(),
+ }),
+ },
+ {
+ key: "export",
+ type: "button",
+ label: "Export",
+ icon: ,
+ variant: "outline",
+ onClick: () => exportTableToExcel({
+ ...exportConfig,
+ tableInstance: getTableInstance(),
+ }),
+ },
+ {
+ key: "create",
+ type: "button",
+ label: "New Lesson",
+ icon: ,
+ variant: "default",
+ onClick: () => navigate("/admin/lessons/add"),
+ },
+ {
+ key: "archived-lessons",
+ type: "button",
+ icon: ,
+ label: "Archived Lessons",
+ variant: "secondary",
+ className: "border border-border",
+ onClick: () => navigate("/admin/lessons/archived"),
+ },
+ ];
+}
+
+export function buildArchivedToolbarActions({
+ fetchArchivedLessons,
+ pagination,
+ exportConfig,
+ navigate,
+ getFilters,
+ getSort,
+ getTableInstance,
+}) {
+ return [
+ {
+ key: "back",
+ type: "button",
+ label: "Back to Lessons",
+ icon: ,
+ variant: "secondary",
+ className: "border border-border",
+ onClick: () => navigate("/admin/lessons"),
+ },
+ {
+ key: "refresh",
+ type: "button",
+ label: "Refresh",
+ icon: ,
+ variant: "outline",
+ onClick: () => fetchArchivedLessons({
+ page: 1,
+ limit: pagination?.limit ?? 10,
+ filters: getFilters(),
+ sort: getSort(),
+ }),
+ },
+ {
+ key: "export",
+ type: "button",
+ label: "Export",
+ icon: ,
+ variant: "outline",
+ onClick: () => exportTableToExcel({
+ ...exportConfig,
+ tableInstance: getTableInstance(),
+ }),
+ },
+ ];
+}
diff --git a/src/modules/admin/config/library/units/columns.config.jsx b/src/modules/admin/config/library/units/columns.config.jsx
new file mode 100644
index 0000000..6a70c85
--- /dev/null
+++ b/src/modules/admin/config/library/units/columns.config.jsx
@@ -0,0 +1,53 @@
+// config/library/units/columns.config.jsx
+// Column definitions and pinning for the standalone Unit Library table.
+
+import { Badge } from "@/components/ui/badge";
+import { Clock } from "lucide-react";
+import { buildColumns } from "@/utils/table.util";
+import { buildSelectionColumn } from "@/components/generic/Table/buildSelectionColumn";
+import { buildRowActionsColumn } from "@/components/generic/Table/buildRowActionsColumn";
+import { formatDuration } from "@/utils/timestamp.util";
+
+export const columnPinning = {
+ right: ["actions"],
+ left: [],
+};
+
+const cellOverrides = {
+ duration_seconds: (info) => {
+ const seconds = parseInt(info.getValue() ?? 0, 10);
+ return (
+
+
+
+ {formatDuration(seconds)}
+
+
+ );
+ },
+ lesson_count: (info) => (
+
+ {info.getValue() ?? 0} lesson{(info.getValue() ?? 0) === 1 ? "" : "s"}
+
+ ),
+ course_count: (info) => {
+ const n = parseInt(info.getValue() ?? 0, 10);
+ return n > 0 ? (
+
+ in {n} course{n === 1 ? "" : "s"}
+
+ ) : (
+ standalone
+ );
+ },
+};
+
+export function buildDataColumns(attributes, rowActions) {
+ const visibleAttributes = attributes.filter((a) => !a.hidden);
+
+ return [
+ buildSelectionColumn(),
+ ...buildColumns(visibleAttributes, { cellOverrides }),
+ buildRowActionsColumn(rowActions, { dropdownLabel: "Unit Actions" }),
+ ];
+}
diff --git a/src/modules/admin/config/library/units/rowActions.config.jsx b/src/modules/admin/config/library/units/rowActions.config.jsx
new file mode 100644
index 0000000..0e3ead1
--- /dev/null
+++ b/src/modules/admin/config/library/units/rowActions.config.jsx
@@ -0,0 +1,85 @@
+// config/library/units/rowActions.config.jsx
+// Row actions for the Unit Library (active + archived variants).
+
+import {
+ Eye, Archive, BookCheck, NotebookPen, ClipboardList, PlusCircle,
+ Pencil, ArchiveRestore, Trash2,
+} from "lucide-react";
+
+export function buildRowActions({ onView, onEdit, onManageLessons, onArchive, onQuiz, onViewQuiz }) {
+ return [
+ {
+ key: "view",
+ label: "View Info",
+ icon: ,
+ onClick: (row) => onView(row),
+ },
+ {
+ key: "edit",
+ label: "Edit Unit",
+ icon: ,
+ onClick: (row) => onEdit(row),
+ },
+ {
+ key: "manage_lessons",
+ label: "Manage Lessons",
+ icon: ,
+ className: "text-sky-700 hover:text-sky-600",
+ onClick: (row) => onManageLessons(row),
+ separator: true,
+ },
+ {
+ key: "create_quiz",
+ label: "Create Quiz",
+ icon: ,
+ className: "text-purple-700 hover:text-purple-600",
+ onClick: (row) => onQuiz(row),
+ hidden: (row) => !!(row.quiz_id || row.quiz),
+ separator: true,
+ },
+ {
+ key: "view_quiz",
+ label: "View Quiz",
+ icon: ,
+ className: "text-purple-700 hover:text-purple-600",
+ onClick: (row) => onViewQuiz(row),
+ hidden: (row) => !(row.quiz_id || row.quiz),
+ separator: true,
+ },
+ {
+ key: "modify_quiz",
+ label: "Modify Quiz",
+ icon: ,
+ className: "text-purple-700 hover:text-purple-600",
+ onClick: (row) => onQuiz(row),
+ hidden: (row) => !(row.quiz_id || row.quiz),
+ },
+ {
+ key: "archive",
+ label: "Archive",
+ icon: ,
+ className: "text-destructive",
+ onClick: (row) => onArchive(row),
+ separator: true,
+ },
+ ];
+}
+
+export function buildArchivedRowActions({ onRestore, onDelete }) {
+ return [
+ {
+ key: "restore",
+ label: "Restore",
+ icon: ,
+ onClick: (row) => onRestore(row),
+ },
+ {
+ key: "delete",
+ label: "Delete Permanently",
+ icon: ,
+ className: "text-destructive",
+ onClick: (row) => onDelete(row),
+ separator: true,
+ },
+ ];
+}
diff --git a/src/modules/admin/config/library/units/selection.config.jsx b/src/modules/admin/config/library/units/selection.config.jsx
new file mode 100644
index 0000000..e7e1100
--- /dev/null
+++ b/src/modules/admin/config/library/units/selection.config.jsx
@@ -0,0 +1,66 @@
+// config/library/units/selection.config.jsx
+// Bulk selection actions for the Unit Library (active + archived variants).
+
+import { Download, Archive, ArchiveRestore, Trash2 } from "lucide-react";
+import { exportTableToExcel } from "@/utils/excel.util";
+
+export function buildSelectionActions({ exportConfig, onArchive, onArchiveMany, getTableInstance }) {
+ return [
+ {
+ key: "export-selected",
+ label: "Export",
+ icon: ,
+ onClick: (rows, table) =>
+ exportTableToExcel({
+ ...exportConfig,
+ selectedRows: rows,
+ tableInstance: table ?? getTableInstance(),
+ }),
+ },
+ {
+ key: "archive-selected",
+ label: "Archive",
+ icon: ,
+ className: "text-destructive border-destructive/40 hover:bg-destructive/10 hover:text-destructive",
+ onClick: (rows) => {
+ const ids = rows.map((r) => r.unit_id);
+ ids.length === 1 ? onArchive(rows[0]) : onArchiveMany(ids);
+ },
+ },
+ ];
+}
+
+export function buildArchivedSelectionActions({ exportConfig, onRestore, onRestoreMany, onDelete, onDeleteMany, getTableInstance }) {
+ return [
+ {
+ key: "export-selected",
+ label: "Export",
+ icon: ,
+ onClick: (rows, table) =>
+ exportTableToExcel({
+ ...exportConfig,
+ selectedRows: rows,
+ tableInstance: table ?? getTableInstance(),
+ }),
+ },
+ {
+ key: "restore-selected",
+ label: "Restore",
+ icon: ,
+ onClick: (rows) => {
+ const ids = rows.map((r) => r.unit_id);
+ ids.length === 1 ? onRestore(rows[0]) : onRestoreMany(ids);
+ },
+ },
+ {
+ key: "delete-selected",
+ label: "Delete Permanently",
+ icon: ,
+ className: "text-destructive border-destructive/40 hover:bg-destructive/10 hover:text-destructive",
+ onClick: (rows) => {
+ const ids = rows.map((r) => r.unit_id);
+ ids.length === 1 ? onDelete(rows[0]) : onDeleteMany(ids);
+ },
+ },
+ ];
+}
diff --git a/src/modules/admin/config/library/units/toolbar.config.jsx b/src/modules/admin/config/library/units/toolbar.config.jsx
new file mode 100644
index 0000000..ce4b414
--- /dev/null
+++ b/src/modules/admin/config/library/units/toolbar.config.jsx
@@ -0,0 +1,105 @@
+// config/library/units/toolbar.config.jsx
+// Toolbar actions for the Unit Library (active + archived variants).
+
+import { Plus, RefreshCw, Download, Archive, ArrowLeft } from "lucide-react";
+import { exportTableToExcel } from "@/utils/excel.util";
+
+export function buildToolbarActions({
+ fetchUnits,
+ pagination,
+ exportConfig,
+ navigate,
+ getFilters,
+ getSort,
+ getTableInstance,
+}) {
+ return [
+ {
+ key: "refresh",
+ type: "button",
+ label: "Refresh",
+ icon: ,
+ variant: "outline",
+ onClick: () => fetchUnits({
+ page: 1,
+ limit: pagination?.limit ?? 10,
+ filters: getFilters(),
+ sort: getSort(),
+ }),
+ },
+ {
+ key: "export",
+ type: "button",
+ label: "Export",
+ icon: ,
+ variant: "outline",
+ onClick: () => exportTableToExcel({
+ ...exportConfig,
+ tableInstance: getTableInstance(),
+ }),
+ },
+ {
+ key: "create",
+ type: "button",
+ label: "New Unit",
+ icon: ,
+ variant: "default",
+ onClick: () => navigate("/admin/units/add"),
+ },
+ {
+ key: "archived-units",
+ type: "button",
+ icon: ,
+ label: "Archived Units",
+ variant: "secondary",
+ className: "border border-border",
+ onClick: () => navigate("/admin/units/archived"),
+ },
+ ];
+}
+
+export function buildArchivedToolbarActions({
+ fetchArchivedUnits,
+ pagination,
+ exportConfig,
+ navigate,
+ getFilters,
+ getSort,
+ getTableInstance,
+}) {
+ return [
+ {
+ key: "back",
+ type: "button",
+ label: "Back to Units",
+ icon: ,
+ variant: "secondary",
+ className: "border border-border",
+ onClick: () => navigate("/admin/units"),
+ },
+ {
+ key: "refresh",
+ type: "button",
+ label: "Refresh",
+ icon: ,
+ variant: "outline",
+ onClick: () => fetchArchivedUnits({
+ page: 1,
+ limit: pagination?.limit ?? 10,
+ filters: getFilters(),
+ sort: getSort(),
+ }),
+ },
+ {
+ key: "export",
+ type: "button",
+ label: "Export",
+ icon: ,
+ variant: "outline",
+ onClick: () => exportTableToExcel({
+ ...exportConfig,
+ tableInstance: getTableInstance(),
+ }),
+ },
+ ];
+}
diff --git a/src/modules/admin/pages/courses/CourseList.jsx b/src/modules/admin/pages/courses/CourseList.jsx
index 3b7965b..cad1c84 100644
--- a/src/modules/admin/pages/courses/CourseList.jsx
+++ b/src/modules/admin/pages/courses/CourseList.jsx
@@ -26,6 +26,15 @@ export default function CourseList() {
Manage Tier Plans
+ {" "}· Units and Lessons now run independently — build them once in the{" "}
+
+ Units Library
+
+ {" "}and{" "}
+
+ Lessons Library
+
+ , then attach them to any course.
diff --git a/src/modules/admin/pages/courses/lessons/ViewLessonPage.jsx b/src/modules/admin/pages/courses/lessons/ViewLessonPage.jsx
index 78c1ecc..cc5b1a2 100644
--- a/src/modules/admin/pages/courses/lessons/ViewLessonPage.jsx
+++ b/src/modules/admin/pages/courses/lessons/ViewLessonPage.jsx
@@ -11,6 +11,11 @@ import { PreviewContent, PreviewChrome } from "../../../components/courses/Lesso
export default function ViewLessonPage() {
const navigate = useNavigate();
const { courseId, unitId, lessonId } = useParams();
+
+ // Junction revamp — runs course-scoped AND from the standalone Lesson Library.
+ const builderPath = unitId
+ ? `/admin/courses/${courseId}/units/${unitId}/lessons/${lessonId}/page`
+ : `/admin/lessons/${lessonId}/page`;
const { fetchLesson, lesson, lessonPage } = useCourses();
const [initializing, setInitializing] = useState(true);
@@ -43,7 +48,7 @@ export default function ViewLessonPage() {
@@ -73,7 +78,7 @@ export default function ViewLessonPage() {
type="button"
variant="outline"
size="sm"
- onClick={() => navigate(`/admin/courses/${courseId}/units/${unitId}/lessons/${lessonId}/page`)}
+ onClick={() => navigate(builderPath)}
>
Go to Page Builder
diff --git a/src/modules/admin/pages/courses/units/ModifyQuiz.jsx b/src/modules/admin/pages/courses/units/ModifyQuiz.jsx
index a8c5b16..1ab411d 100644
--- a/src/modules/admin/pages/courses/units/ModifyQuiz.jsx
+++ b/src/modules/admin/pages/courses/units/ModifyQuiz.jsx
@@ -223,19 +223,32 @@ export default function ModifyQuiz() {
const headerRef = useRef(null);
const initialSnapshot = useRef(null);
- const breadcrumbItems = [
- { label: "Home", icon: , to: "/admin" },
- { label: "Courses", to: "/admin/courses" },
- { label: course?.title ?? "…", to: `/admin/courses/${courseId}` },
- { label: unit?.title ?? "…", to: `/admin/courses/${courseId}/units/${unitId}` },
- { label: "Quiz" },
- ];
+ // Junction revamp — this builder runs course-scoped AND from the standalone
+ // Unit Library (/admin/units/:unitId/quiz/edit, no :courseId param).
+ const scopeBase = courseId
+ ? `/admin/courses/${courseId}/units/${unitId}`
+ : `/admin/units/${unitId}`;
+
+ const breadcrumbItems = courseId
+ ? [
+ { label: "Home", icon: , to: "/admin" },
+ { label: "Courses", to: "/admin/courses" },
+ { label: course?.title ?? "…", to: `/admin/courses/${courseId}` },
+ { label: unit?.title ?? "…", to: `/admin/courses/${courseId}/units/${unitId}` },
+ { label: "Quiz" },
+ ]
+ : [
+ { label: "Home", icon: , to: "/admin" },
+ { label: "Units Library", to: "/admin/units" },
+ { label: unit?.title ?? "…", to: `/admin/units/${unitId}/view` },
+ { label: "Quiz" },
+ ];
// ── Fetch — silently treat 404 as "no quiz yet" (create mode) ─────────────
useEffect(() => {
(async () => {
try {
- const { data } = await api.get(`/admin/courses/${courseId}/units/${unitId}/quiz`);
+ const { data } = await api.get(`${scopeBase}/quiz`);
const result = data?.data?.data ?? null;
setLocalQuiz(result);
} catch (err) {
diff --git a/src/modules/admin/pages/courses/units/ViewUnitQuiz.jsx b/src/modules/admin/pages/courses/units/ViewUnitQuiz.jsx
index fc0a2e0..de57f3c 100644
--- a/src/modules/admin/pages/courses/units/ViewUnitQuiz.jsx
+++ b/src/modules/admin/pages/courses/units/ViewUnitQuiz.jsx
@@ -245,6 +245,11 @@ export default function ViewUnitQuiz() {
const navigate = useNavigate();
const { courseId, unitId } = useParams();
+ // Junction revamp — runs course-scoped AND from the standalone Unit Library.
+ const scopeBase = courseId
+ ? `/admin/courses/${courseId}/units/${unitId}`
+ : `/admin/units/${unitId}`;
+
const {
fetchQuiz, quiz,
fetchQuizCompletions, completions,
@@ -293,7 +298,7 @@ export default function ViewUnitQuiz() {