From 8ac7b08f861ea9d0f3ca36750a8c59f2a944f502 Mon Sep 17 00:00:00 2001 From: rgrgogu Date: Thu, 21 May 2026 11:48:50 +0800 Subject: [PATCH] Adjusted --- src/contexts/AdminCoursesContext.jsx | 50 +++++-- .../components/courses/ArchivedUnitsTable.jsx | 140 ++++++++++++++++++ .../admin/components/courses/UnitsTable.jsx | 31 +++- .../courses/archive/rowActions.config.jsx | 1 - .../units/archive/rowActions.config.jsx | 60 +++----- .../units/archive/selection.config.jsx | 21 +-- .../courses/units/archive/toolbar.config.jsx | 22 +-- .../config/courses/units/selection.config.jsx | 2 +- .../config/courses/units/toolbar.config.jsx | 2 +- .../pages/courses/units/ArchivedUnitsList.jsx | 57 +++++++ .../admin/pages/courses/units/UnitsList.jsx | 2 +- src/modules/admin/routes/AdminRoutes.jsx | 2 + 12 files changed, 297 insertions(+), 93 deletions(-) create mode 100644 src/modules/admin/pages/courses/units/ArchivedUnitsList.jsx diff --git a/src/contexts/AdminCoursesContext.jsx b/src/contexts/AdminCoursesContext.jsx index 6a79c4e..18c0a75 100644 --- a/src/contexts/AdminCoursesContext.jsx +++ b/src/contexts/AdminCoursesContext.jsx @@ -275,10 +275,10 @@ export function CoursesProvider({ children }) { [request], ); - const deleteUnit = useCallback( - (courseId, unitId, deletedBy) => + const archiveUnit = useCallback( + (courseId, unitId) => request(async () => { - const { data } = await api.delete(`${BASE}/${courseId}/units/${unitId}`, { data: { deletedBy } }); + const { data } = await api.delete(`${BASE}/${courseId}/units/${unitId}`); setUnits((prev) => prev.filter((u) => u.unit_id !== unitId)); setUnit((prev) => (prev?.unit_id === unitId ? null : prev)); toast.success("Unit archived."); @@ -287,10 +287,10 @@ export function CoursesProvider({ children }) { [request], ); - const bulkArchiveUnits = useCallback( - (courseId, ids, deletedBy) => + const archiveUnits = useCallback( + (courseId, { ids }) => request(async () => { - const { data } = await api.delete(`${BASE}/${courseId}/units/bulk`, { data: { ids, deletedBy } }); + const { data } = await api.delete(`${BASE}/${courseId}/units/bulk`, { data: { ids } }); setUnits((prev) => prev.filter((u) => !ids.includes(u.unit_id))); toast.success("Units archived."); return data; @@ -318,9 +318,9 @@ export function CoursesProvider({ children }) { ); const restoreUnit = useCallback( - (courseId, unitId, restoredBy) => + (courseId, unitId) => request(async () => { - const { data } = await api.patch(`${BASE}/${courseId}/units/${unitId}/restore`, { restoredBy }); + const { data } = await api.patch(`${BASE}/${courseId}/units/${unitId}/restore`); const result = data?.data?.data ?? null; if (result) { setUnits((prev) => prev.filter((u) => u.unit_id !== unitId)); @@ -331,10 +331,10 @@ export function CoursesProvider({ children }) { [request], ); - const bulkRestoreUnits = useCallback( - (courseId, ids, restoredBy) => + const restoreUnits = useCallback( + (courseId, { ids }) => request(async () => { - const { data } = await api.patch(`${BASE}/${courseId}/units/restore/bulk`, { ids, restoredBy }); + const { data } = await api.patch(`${BASE}/${courseId}/units/restore/bulk`, { ids } ); setUnits((prev) => prev.filter((u) => !ids.includes(u.unit_id))); toast.success("Units restored."); return data; @@ -857,6 +857,26 @@ export function CoursesProvider({ children }) { [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(`${BASE}/${courseId}/units/${unitId}/field-values`, { params: { field } }); + const result = data?.data ?? []; + return result; + }), + [request], + ); + // ─── Provider value ─────────────────────────────────────────────────────── return ( {children} diff --git a/src/modules/admin/components/courses/ArchivedUnitsTable.jsx b/src/modules/admin/components/courses/ArchivedUnitsTable.jsx index e69de29..1a66446 100644 --- a/src/modules/admin/components/courses/ArchivedUnitsTable.jsx +++ b/src/modules/admin/components/courses/ArchivedUnitsTable.jsx @@ -0,0 +1,140 @@ +import { useMemo, useRef, useState, useCallback } from "react"; +import { useNavigate } from "react-router-dom"; + +import { useCourses } from "@/contexts/AdminCoursesContext"; + +import DataTable from "@/components/generic/Table/DataTable"; +import { FilterSheet } from "@/components/generic/Sheet/FilterSheet"; +import { RestoreDialog } from "@/components/generic/Dialogs/RestoreDialog"; + +import { buildDataColumns, columnPinning } from "../../config/courses/units/archive/columns.config"; +import { buildToolbarActions } from "../../config/courses/units/archive/toolbar.config"; +import { buildSelectionActions } from "../../config/courses/units/archive/selection.config"; +import { buildRowActions } from "../../config/courses/units/archive/rowActions.config"; + +import { getTimestamp } from "@/utils/timestamp.util"; + +export default function ArchivedUnitsTable({ courseId }) { + const [restoreTarget, setRestoreTarget] = useState(null); + const [restoreIds, setRestoreIds] = useState(null); + + const tableRefsRef = useRef({ + getFilters: () => [], + getSort: () => [], + resetSelection: () => { }, + setFilters: () => { }, + }); + + const navigate = useNavigate(); + + const { + course, units, attributes, pagination, setPagination, loading, + fetchArchivedUnits, restoreUnit, restoreUnits, fetchUnitFieldValues + } = useCourses(); + + const handleRefsReady = (refs) => { + tableRefsRef.current = refs; + }; + + const exportConfig = { + allData: units, + attributes, + filename: `${getTimestamp()}_${course?.title}_ArchivedUnits`, + sheetName: "Archived Units", + }; + + const handleFetch = useCallback( + (params) => fetchArchivedUnits(courseId, params), + [courseId] + ); + + const rowActions = useMemo(() => buildRowActions({ + onRestore: (row) => setRestoreTarget(row) + }), [courseId]); + + const toolbarActions = buildToolbarActions({ + fetchUnits: (params) => fetchArchivedUnits(courseId, params), + pagination, + exportConfig, + navigate, + courseId, + getFilters: () => tableRefsRef.current.getFilters(), + getSort: () => tableRefsRef.current.getSort(), + getTableInstance: () => tableRefsRef.current.tableInstance, + }); + + const selectionActions = buildSelectionActions({ + exportConfig, + restoreCourse: (row) => setRestoreTarget(row), // single + restoreCourses: (ids) => setRestoreIds(ids), // bulk + getTableInstance: () => tableRefsRef.current.tableInstance, + }); + + + const columns = useMemo( + () => buildDataColumns(attributes, rowActions), + [attributes] + ); + + const handleRestoreSuccess = () => { + setRestoreTarget(null); + setRestoreIds(null); + tableRefsRef.current.resetSelection?.(); + fetchArchivedUnits(courseId, { page: 1, limit: pagination?.limit ?? 10 }); + }; + + return ( + <> + fetchUnitFieldValues(courseId, field)} + onRefsReady={handleRefsReady} + renderFilterSheet={({ open, onOpenChange, column, attr, data, loading }) => ( + + )} + columnPinning={columnPinning} + toolbarActions={toolbarActions} + selectionActions={selectionActions} + recordLabel="unit" + emptyMessage="No units found." + /> + + {/* Single restore */} + !v && setRestoreTarget(null)} + entity={restoreTarget} + entityLabel="Unit" + getName={(c) => c?.title} + onRestore={(c) => restoreUnit(courseId, c?.unit_id)} + loading={loading} + onSuccess={handleRestoreSuccess} + /> + + {/* Bulk restore */} + !v && setRestoreIds(null)} + ids={restoreIds ?? []} + entityLabel="Unit" + onRestore={(ids) => restoreUnits(courseId, ids)} + loading={loading} + onSuccess={handleRestoreSuccess} + /> + + ); +} diff --git a/src/modules/admin/components/courses/UnitsTable.jsx b/src/modules/admin/components/courses/UnitsTable.jsx index 7f2251f..bd9ccaf 100644 --- a/src/modules/admin/components/courses/UnitsTable.jsx +++ b/src/modules/admin/components/courses/UnitsTable.jsx @@ -10,12 +10,14 @@ import { ArchiveDialog } from "@/components/generic/Dialogs/ArchiveDialog"; import { buildDataColumns, columnPinning } from "../../config/courses/units/columns.config"; import { buildToolbarActions } from "../../config/courses/units/toolbar.config"; +import { buildSelectionActions } from "../../config/courses/units/selection.config"; import { buildRowActions } from "../../config/courses/units/rowActions.config"; import { getTimestamp } from "@/utils/timestamp.util"; export default function UnitsTable({ courseId }) { const [archiveTarget, setArchiveTarget] = useState(null); + const [archiveIds, setArchiveIds] = useState(null); const tableRefsRef = useRef({ getFilters: () => [], @@ -28,7 +30,7 @@ export default function UnitsTable({ courseId }) { const { units, attributes, pagination, setPagination, loading, - fetchUnits, deleteUnit, + fetchUnits, archiveUnit, archiveUnits, fetchUnitFieldValues } = useCourses(); const handleRefsReady = (refs) => { @@ -66,6 +68,13 @@ export default function UnitsTable({ courseId }) { 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] @@ -73,6 +82,7 @@ export default function UnitsTable({ courseId }) { const handleArchiveSuccess = () => { setArchiveTarget(null); + setArchiveIds(null); tableRefsRef.current.resetSelection?.(); fetchUnits(courseId, { page: 1, limit: pagination?.limit ?? 10 }); }; @@ -88,7 +98,7 @@ export default function UnitsTable({ courseId }) { setPagination={setPagination} loading={loading} onFetch={handleFetch} - onFetchFilterData={() => []} + onFetchFilterData={(field) => fetchUnitFieldValues(courseId, field)} onRefsReady={handleRefsReady} renderFilterSheet={({ open, onOpenChange, column, attr, data, loading }) => ( + {/* ── Single archive ── */} !v && setArchiveTarget(null)} entity={archiveTarget} entityLabel="Unit" - getName={(u) => u?.title} - onArchive={(u) => deleteUnit(courseId, u?.unit_id)} + getName={(c) => c?.title} + onArchive={(c) => archiveUnit(courseId, c?.unit_id)} + loading={loading} + onSuccess={handleArchiveSuccess} + /> + + {/* ── Bulk archive ── */} + !v && setArchiveIds(null)} + ids={archiveIds ?? []} + entityLabel="Unit" + onArchive={(ids) => archiveUnits(courseId, ids)} loading={loading} onSuccess={handleArchiveSuccess} /> diff --git a/src/modules/admin/config/courses/archive/rowActions.config.jsx b/src/modules/admin/config/courses/archive/rowActions.config.jsx index 03112a9..9d44130 100644 --- a/src/modules/admin/config/courses/archive/rowActions.config.jsx +++ b/src/modules/admin/config/courses/archive/rowActions.config.jsx @@ -15,7 +15,6 @@ export function buildRowActions({ onRestore }) { className: "text-emerald-600 focus:text-emerald-600", icon: , onClick: (row) => onRestore(row), - hidden: (row) => row.is_active, }, ]; } \ No newline at end of file diff --git a/src/modules/admin/config/courses/units/archive/rowActions.config.jsx b/src/modules/admin/config/courses/units/archive/rowActions.config.jsx index 6a92abc..9d44130 100644 --- a/src/modules/admin/config/courses/units/archive/rowActions.config.jsx +++ b/src/modules/admin/config/courses/units/archive/rowActions.config.jsx @@ -1,42 +1,20 @@ -import { Eye, Pencil, Archive, BookCheck, NotebookPen } from "lucide-react"; +// modules/admin/config/assets/rowActions.config.jsx +import { RotateCcw } from "lucide-react"; -export function buildRowActions({ onViewLessons, onView, onEdit, onArchive, onQuiz }) { - return [ - { - key: "view", - label: "View Info", - icon: , - onClick: (row) => onView(row), - }, - { - key: "edit", - label: "Edit Info", - icon: , - onClick: (row) => onEdit(row), - }, - { - key: "view_units", - label: "View Lessons", - icon: , - className: "text-sky-700 hover:text-sky-600", - onClick: (row) => onViewLessons(row), - separator: true - }, - { - key: "modify_quiz", - label: "Modify Quiz", - icon: , - className: "text-purple-700 hover:text-purple-600", - onClick: (row) => onQuiz(row), - separator: true - }, - { - key: "archive", - label: "Archive", - icon: , - className: "text-destructive", - onClick: (row) => onArchive(row), - separator: true - }, - ] -} +/** + * @param {Object} deps + * @param {Function} deps.onView (row) → void — navigate to view page + * @param {Function} deps.onEdit (row) → void — navigate to edit page + * @param {Function} deps.onArchive (row) → void — open archive dialog + */ +export function buildRowActions({ onRestore }) { + return [ + { + key: "restore", + label: "Restore", + className: "text-emerald-600 focus:text-emerald-600", + icon: , + onClick: (row) => onRestore(row), + }, + ]; +} \ No newline at end of file diff --git a/src/modules/admin/config/courses/units/archive/selection.config.jsx b/src/modules/admin/config/courses/units/archive/selection.config.jsx index 6cc4c31..8ca78fe 100644 --- a/src/modules/admin/config/courses/units/archive/selection.config.jsx +++ b/src/modules/admin/config/courses/units/archive/selection.config.jsx @@ -1,7 +1,8 @@ -import { Download, Archive } from "lucide-react"; +// config/assets/archive/selection.config.jsx +import { Download, ArchiveRestore } from "lucide-react"; import { exportTableToExcel } from "@/utils/excel.util"; -export function buildSelectionActions({ exportConfig, onArchive, onArchiveMany, getTableInstance }) { +export function buildSelectionActions({ exportConfig, restoreCourse, restoreCourses, getTableInstance }) { return [ { key: "export-selected", @@ -15,16 +16,16 @@ export function buildSelectionActions({ exportConfig, onArchive, onArchiveMany, }), }, { - key: "archive-selected", - label: "Archive", - icon: , - className: "text-destructive border-destructive/40 hover:bg-destructive/10 hover:text-destructive", + key: "restore-selected", + label: "Restore", + icon: , + className: "text-emerald-600 border-emerald-600/40 hover:bg-emerald-600/10 hover:text-emerald-700", onClick: (rows) => { - const ids = rows.map((r) => r.asset_id); + const ids = rows.map((r) => r.unit_id); ids.length === 1 - ? onArchive(rows[0]) - : onArchiveMany(ids); + ? restoreCourse(rows[0]) + : restoreCourses(ids); }, }, ]; -} +} \ No newline at end of file diff --git a/src/modules/admin/config/courses/units/archive/toolbar.config.jsx b/src/modules/admin/config/courses/units/archive/toolbar.config.jsx index 0f5ef69..2f9c594 100644 --- a/src/modules/admin/config/courses/units/archive/toolbar.config.jsx +++ b/src/modules/admin/config/courses/units/archive/toolbar.config.jsx @@ -2,11 +2,10 @@ import { Plus, RefreshCw, Download, Archive } from "lucide-react"; import { exportTableToExcel } from "@/utils/excel.util"; export function buildToolbarActions({ - fetchUnits, + fetchArchivedCourses, pagination, exportConfig, navigate, - courseId, getFilters, getSort, getTableInstance, @@ -18,7 +17,7 @@ export function buildToolbarActions({ label: "Refresh", icon: , variant: "outline", - onClick: () => fetchUnits({ + onClick: () => fetchArchivedCourses({ page: 1, limit: pagination?.limit ?? 10, filters: getFilters(), @@ -36,22 +35,5 @@ export function buildToolbarActions({ tableInstance: getTableInstance(), }), }, - { - key: "create", - type: "button", - label: "New Unit", - icon: , - variant: "default", - onClick: () => navigate(`/admin/courses/${courseId}/units/add`), - }, - { - key: "archived-units", - type: "button", - icon: , - label: "Archived Units", - variant: "secondary", - className: "border border-border", - onClick: () => navigate("/admin/units/archived"), - }, ]; } diff --git a/src/modules/admin/config/courses/units/selection.config.jsx b/src/modules/admin/config/courses/units/selection.config.jsx index 6cc4c31..f157213 100644 --- a/src/modules/admin/config/courses/units/selection.config.jsx +++ b/src/modules/admin/config/courses/units/selection.config.jsx @@ -20,7 +20,7 @@ export function buildSelectionActions({ exportConfig, onArchive, onArchiveMany, icon: , className: "text-destructive border-destructive/40 hover:bg-destructive/10 hover:text-destructive", onClick: (rows) => { - const ids = rows.map((r) => r.asset_id); + const ids = rows.map((r) => r.unit_id); ids.length === 1 ? onArchive(rows[0]) : onArchiveMany(ids); diff --git a/src/modules/admin/config/courses/units/toolbar.config.jsx b/src/modules/admin/config/courses/units/toolbar.config.jsx index 0f5ef69..2d6ee65 100644 --- a/src/modules/admin/config/courses/units/toolbar.config.jsx +++ b/src/modules/admin/config/courses/units/toolbar.config.jsx @@ -51,7 +51,7 @@ export function buildToolbarActions({ label: "Archived Units", variant: "secondary", className: "border border-border", - onClick: () => navigate("/admin/units/archived"), + onClick: () => navigate(`/admin/courses/${courseId}/units/archived`), }, ]; } diff --git a/src/modules/admin/pages/courses/units/ArchivedUnitsList.jsx b/src/modules/admin/pages/courses/units/ArchivedUnitsList.jsx new file mode 100644 index 0000000..6bec44a --- /dev/null +++ b/src/modules/admin/pages/courses/units/ArchivedUnitsList.jsx @@ -0,0 +1,57 @@ +import { useEffect, useState } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { ArrowLeft, House, Plus } from "lucide-react"; + +import { useCourses } from "@/contexts/AdminCoursesContext"; +import AppBreadcrumb from "@/components/generic/Breadcrumb/AppBreadcrumb"; +import { Spinner } from "@/components/ui/spinner"; +import ArchivedUnitsTable from "../../../components/courses/ArchivedUnitsTable"; +import { Badge } from "@/components/ui/badge"; + +export default function ArchivedUnitsList() { + const navigate = useNavigate(); + const { courseId } = useParams(); + const { fetchCourse, course, loading } = useCourses(); + const [initializing, setInitializing] = useState(true); + + useEffect(() => { + (async () => { + await fetchCourse(courseId); + setInitializing(false); + })(); + }, [courseId]); + + const items = [ + { label: "Home", icon: , to: "/admin" }, + { label: "Courses", to: "/admin/courses" }, + { label: course?.title ?? "...", to: `/admin/courses/${course?.course_id ?? course?.course_id}/units` }, + { label: "Archived" } + ]; + + // Replace the loading check + if (initializing) { + return ( +
+ +
+ ); + } + + return ( +
+
+
+ +
+ +
+ + + {/* ── Units ── */} + + +
+
+
+ ); +} diff --git a/src/modules/admin/pages/courses/units/UnitsList.jsx b/src/modules/admin/pages/courses/units/UnitsList.jsx index 0754c47..3a8c401 100644 --- a/src/modules/admin/pages/courses/units/UnitsList.jsx +++ b/src/modules/admin/pages/courses/units/UnitsList.jsx @@ -8,7 +8,7 @@ import { Spinner } from "@/components/ui/spinner"; import UnitsTable from "../../../components/courses/UnitsTable"; import { Badge } from "@/components/ui/badge"; -export default function CourseDetail() { +export default function UnitsList() { const navigate = useNavigate(); const { courseId } = useParams(); const { fetchCourse, course, loading } = useCourses(); diff --git a/src/modules/admin/routes/AdminRoutes.jsx b/src/modules/admin/routes/AdminRoutes.jsx index 04436e6..b43b52a 100644 --- a/src/modules/admin/routes/AdminRoutes.jsx +++ b/src/modules/admin/routes/AdminRoutes.jsx @@ -42,6 +42,7 @@ import UnitsList from '../pages/courses/units/UnitsList' import AddUnit from '../pages/courses/units/AddUnit' import ViewUnit from '../pages/courses/units/ViewUnit' import EditUnit from '../pages/courses/units/EditUnit' +import ArchivedUnitsList from '../pages/courses/units/ArchivedUnitsList' // Lessons import LessonsList from '../pages/courses/lessons/LessonsList' @@ -136,6 +137,7 @@ export const AdminRoutes = { element: , children: [ { index: true, element: }, + { path: 'archived', element: }, { path: 'add', element: }, { path: ':unitId/view', element: }, { path: ':unitId/edit', element: },