+ );
+ }
+
+ if (type === "video") {
+ if (!content.url) {
+ return (
+
;
+ }
+
+ if (type === "text-video") {
+ const vidLeft = content.video_position === "left";
+ return (
+
+ {vidLeft &&
}
+
Empty text" }}
+ />
+ {!vidLeft &&
}
+
+ );
+ }
+
+ return null;
+}
+
+export function PreviewContent({ lesson, blocks, empty = "No content yet." }) {
+ return (
+ <>
+
+ {blocks.length === 0 ? (
+
+ ) : (
+ blocks.map((block) => (
+
+ ))
+ )}
+ >
+ );
+}
+
+export function PreviewChrome({ title, children }) {
+ return (
+
+
+
+
+
+
+
+ {title ?? "Lesson Preview"}
+
+
+
+ {children}
+
+ );
+}
\ No newline at end of file
diff --git a/src/modules/admin/components/courses/LessonsTable.jsx b/src/modules/admin/components/courses/LessonsTable.jsx
index de5004f..700c143 100644
--- a/src/modules/admin/components/courses/LessonsTable.jsx
+++ b/src/modules/admin/components/courses/LessonsTable.jsx
@@ -47,7 +47,9 @@ export default function LessonsTable({ courseId, unitId }) {
);
const rowActions = useMemo(() => buildRowActions({
- onView: (row) => navigate(`/admin/courses/${courseId}/units/${unitId}/lessons/${row.lesson_id}`),
+ onViewPage: (row) => navigate(`/admin/courses/${courseId}/units/${unitId}/lessons/${row.lesson_id}/page/view`),
+ onCreatePage: (row) => navigate(`/admin/courses/${courseId}/units/${unitId}/lessons/${row.lesson_id}/page`),
+ onView: (row) => navigate(`/admin/courses/${courseId}/units/${unitId}/lessons/${row.lesson_id}/view`),
onEdit: (row) => navigate(`/admin/courses/${courseId}/units/${unitId}/lessons/${row.lesson_id}/edit`),
onArchive: (row) => setArchiveTarget(row),
}), [courseId, unitId]);
diff --git a/src/modules/admin/components/courses/QuestionEditor.jsx b/src/modules/admin/components/courses/QuestionEditor.jsx
new file mode 100644
index 0000000..b39fc3f
--- /dev/null
+++ b/src/modules/admin/components/courses/QuestionEditor.jsx
@@ -0,0 +1,274 @@
+import { useState } from "react";
+import { Plus, Trash2, GripVertical, CheckCircle2, Circle } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Textarea } from "@/components/ui/textarea";
+import { Badge } from "@/components/ui/badge";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { cn } from "@/lib/utils";
+
+const QUESTION_TYPES = [
+ { value: "true_false", label: "True / False" },
+ { value: "multiple_choice", label: "Multiple Choice (1 correct)" },
+ { value: "multi_select", label: "Multi Select (multiple correct)" },
+];
+
+function FieldError({ message }) {
+ if (!message) return null;
+ return
{message}
;
+}
+
+// ── Single Option Row ─────────────────────────────────────────────────────────
+function OptionRow({ option, index, questionType, onUpdate, onRemove, onToggleCorrect }) {
+ const isCorrect = option.is_correct;
+
+ return (
+
+
+
+ {/* Correct toggle */}
+
+
+ onUpdate(index, { ...option, text: e.target.value })}
+ placeholder={`Option ${index + 1}`}
+ className="flex-1 border-0 shadow-none focus-visible:ring-0 p-0 h-auto text-sm"
+ />
+
+
+
+ );
+}
+
+// ── Question Card ─────────────────────────────────────────────────────────────
+export function QuestionCard({ question, index, onChange, onRemove, error }) {
+ const updateField = (field, value) => onChange({ ...question, [field]: value });
+
+ const updateOption = (i, updated) => {
+ const options = [...question.options];
+ options[i] = updated;
+ onChange({ ...question, options });
+ };
+
+ const toggleCorrect = (i) => {
+ const options = question.options.map((o, idx) => {
+ if (question.type === "multiple_choice" || question.type === "true_false") {
+ // single correct — deselect all others
+ return { ...o, is_correct: idx === i };
+ }
+ // multi_select — toggle individual
+ return idx === i ? { ...o, is_correct: !o.is_correct } : o;
+ });
+ onChange({ ...question, options });
+ };
+
+ const addOption = () => {
+ onChange({
+ ...question,
+ options: [
+ ...question.options,
+ { text: "", is_correct: false, order_index: question.options.length },
+ ],
+ });
+ };
+
+ const removeOption = (i) => {
+ onChange({
+ ...question,
+ options: question.options.filter((_, idx) => idx !== i),
+ });
+ };
+
+ const handleTypeChange = (type) => {
+ // Reset options based on type
+ const defaultOptions =
+ type === "true_false"
+ ? [
+ { text: "True", is_correct: true, order_index: 0 },
+ { text: "False", is_correct: false, order_index: 1 },
+ ]
+ : question.options.map((o) => ({ ...o, is_correct: false }));
+
+ onChange({ ...question, type, options: defaultOptions });
+ };
+
+ const correctCount = question.options.filter((o) => o.is_correct).length;
+
+ return (
+
+ {/* Card header */}
+
+
+
+ {index + 1}
+
+
+
+ {question.points ?? 1} pt{(question.points ?? 1) !== 1 ? "s" : ""}
+
+ {correctCount > 0 && (
+
+ {correctCount} correct
+
+ )}
+
+
+
+
+
+ {/* Question text */}
+
+
+
+
+ {/* Options */}
+
+
+
+ {question.type !== "true_false" && (
+
+ )}
+
+
+
+ {question.options.map((option, i) => (
+
+ ))}
+
+
+
+
+ {/* Points + Explanation row */}
+
+
+
+ );
+}
+
+// ── Make a blank question ─────────────────────────────────────────────────────
+export function makeQuestion(type = "multiple_choice") {
+ const defaultOptions = {
+ true_false: [
+ { text: "True", is_correct: true, order_index: 0 },
+ { text: "False", is_correct: false, order_index: 1 },
+ ],
+ multiple_choice: [
+ { text: "", is_correct: true, order_index: 0 },
+ { text: "", is_correct: false, order_index: 1 },
+ { text: "", is_correct: false, order_index: 2 },
+ ],
+ multi_select: [
+ { text: "", is_correct: true, order_index: 0 },
+ { text: "", is_correct: true, order_index: 1 },
+ { text: "", is_correct: false, order_index: 2 },
+ ],
+ };
+
+ return {
+ _tempId: crypto.randomUUID(),
+ type,
+ question: "",
+ explanation: "",
+ points: 1,
+ order_index: 0,
+ options: defaultOptions[type] ?? [],
+ };
+}
\ No newline at end of file
diff --git a/src/modules/admin/components/courses/UnitsTable.jsx b/src/modules/admin/components/courses/UnitsTable.jsx
index 4013619..7f2251f 100644
--- a/src/modules/admin/components/courses/UnitsTable.jsx
+++ b/src/modules/admin/components/courses/UnitsTable.jsx
@@ -48,7 +48,9 @@ export default function UnitsTable({ courseId }) {
);
const rowActions = useMemo(() => buildRowActions({
- onView: (row) => navigate(`/admin/courses/${courseId}/units/${row.unit_id}`),
+ onQuiz: (row) => navigate(`/admin/courses/${courseId}/units/${row.unit_id}/quiz`),
+ onViewLessons: (row) => navigate(`/admin/courses/${courseId}/units/${row.unit_id}/lessons`),
+ onView: (row) => navigate(`/admin/courses/${courseId}/units/${row.unit_id}/view`),
onEdit: (row) => navigate(`/admin/courses/${courseId}/units/${row.unit_id}/edit`),
onArchive: (row) => setArchiveTarget(row),
}), [courseId]);
diff --git a/src/modules/admin/config/assets/rowActions.config.jsx b/src/modules/admin/config/assets/rowActions.config.jsx
index e336d20..bd474a4 100644
--- a/src/modules/admin/config/assets/rowActions.config.jsx
+++ b/src/modules/admin/config/assets/rowActions.config.jsx
@@ -12,13 +12,13 @@ export function buildRowActions({ onView, onEdit, onArchive }) {
return [
{
key: "view",
- label: "View",
+ label: "View Info",
icon:
,
onClick: (row) => onView(row),
},
{
key: "edit",
- label: "Edit",
+ label: "Edit Info",
icon:
,
onClick: (row) => onEdit(row),
},
diff --git a/src/modules/admin/config/courses/archive/columns.config.jsx b/src/modules/admin/config/courses/archive/columns.config.jsx
new file mode 100644
index 0000000..25a1d56
--- /dev/null
+++ b/src/modules/admin/config/courses/archive/columns.config.jsx
@@ -0,0 +1,70 @@
+// config/columns.config.jsx
+// Column definitions and pinning config for the Users table.
+
+import { buildColumns } from "@/utils/table.util";
+import { buildSelectionColumn } from "@/components/generic/Table/buildSelectionColumn";
+import { buildRowActionsColumn } from "@/components/generic/Table/buildRowActionsColumn";
+import { Badge } from "@/components/ui/badge";
+import { Book, BookOpenCheck, Clock } from "lucide-react";
+import { formatDuration } from "@/utils/timestamp.util";
+
+export const columnPinning = {
+ right: ["actions"],
+ left: [],
+};
+
+
+// ─── Custom cell overrides ────────────────────────────────────────────────────
+const cellOverrides = {
+ unitCount: (info) => {
+ const count = parseInt(info.getValue() ?? 0, 10);
+ return (
+
+
+
+ {count} {count === 1 ? "unit" : "units"}
+
+
+ );
+ },
+ lessonCount: (info) => {
+ const count = parseInt(info.getValue() ?? 0, 10);
+ return (
+
+
+
+ {count} {count === 1 ? "lesson" : "lessons"}
+
+
+ );
+ },
+ duration_seconds: (info) => {
+ const seconds = parseInt(info.getValue() ?? 0, 10);
+
+ return (
+
+
+
+ {formatDuration(seconds)}
+
+
+ );
+ },
+};
+
+/**
+ * Builds the full column array for the Users table.
+ *
+ * @param {Array} attributes Field definitions from the server (drives data columns)
+ * @param {Array} rowActions Row-level kebab action definitions
+ * @returns {Array} TanStack column definitions
+ */
+export function buildDataColumns(attributes, rowActions) {
+ const visibleAttributes = attributes.filter((a) => !a.hidden);
+
+ return [
+ buildSelectionColumn(),
+ ...buildColumns(visibleAttributes, { cellOverrides }),
+ buildRowActionsColumn(rowActions, { dropdownLabel: "Course Actions" }),
+ ];
+}
\ No newline at end of file
diff --git a/src/modules/admin/config/courses/archive/rowActions.config.jsx b/src/modules/admin/config/courses/archive/rowActions.config.jsx
new file mode 100644
index 0000000..03112a9
--- /dev/null
+++ b/src/modules/admin/config/courses/archive/rowActions.config.jsx
@@ -0,0 +1,21 @@
+// modules/admin/config/assets/rowActions.config.jsx
+import { RotateCcw } from "lucide-react";
+
+/**
+ * @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),
+ hidden: (row) => row.is_active,
+ },
+ ];
+}
\ No newline at end of file
diff --git a/src/modules/admin/config/courses/archive/selection.config.jsx b/src/modules/admin/config/courses/archive/selection.config.jsx
new file mode 100644
index 0000000..4635601
--- /dev/null
+++ b/src/modules/admin/config/courses/archive/selection.config.jsx
@@ -0,0 +1,31 @@
+// config/assets/archive/selection.config.jsx
+import { Download, ArchiveRestore } from "lucide-react";
+import { exportTableToExcel } from "@/utils/excel.util";
+
+export function buildSelectionActions({ exportConfig, restoreCourse, restoreCourses, getTableInstance }) {
+ return [
+ {
+ key: "export-selected",
+ label: "Export",
+ icon:
,
+ onClick: (rows, table) =>
+ exportTableToExcel({
+ ...exportConfig,
+ selectedRows: rows,
+ tableInstance: table ?? getTableInstance(),
+ }),
+ },
+ {
+ 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.course_id);
+ ids.length === 1
+ ? restoreCourse(rows[0])
+ : restoreCourses(ids);
+ },
+ },
+ ];
+}
\ No newline at end of file
diff --git a/src/modules/admin/config/courses/archive/toolbar.config.jsx b/src/modules/admin/config/courses/archive/toolbar.config.jsx
new file mode 100644
index 0000000..2f9c594
--- /dev/null
+++ b/src/modules/admin/config/courses/archive/toolbar.config.jsx
@@ -0,0 +1,39 @@
+import { Plus, RefreshCw, Download, Archive } from "lucide-react";
+import { exportTableToExcel } from "@/utils/excel.util";
+
+export function buildToolbarActions({
+ fetchArchivedCourses,
+ pagination,
+ exportConfig,
+ navigate,
+ getFilters,
+ getSort,
+ getTableInstance,
+}) {
+ return [
+ {
+ key: "refresh",
+ type: "button",
+ label: "Refresh",
+ icon:
,
+ variant: "outline",
+ onClick: () => fetchArchivedCourses({
+ 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/courses/columns.config.jsx b/src/modules/admin/config/courses/columns.config.jsx
index db75d6a..25a1d56 100644
--- a/src/modules/admin/config/courses/columns.config.jsx
+++ b/src/modules/admin/config/courses/columns.config.jsx
@@ -1,38 +1,70 @@
+// config/columns.config.jsx
+// Column definitions and pinning config for the Users table.
+
+import { buildColumns } from "@/utils/table.util";
+import { buildSelectionColumn } from "@/components/generic/Table/buildSelectionColumn";
+import { buildRowActionsColumn } from "@/components/generic/Table/buildRowActionsColumn";
import { Badge } from "@/components/ui/badge";
+import { Book, BookOpenCheck, Clock } from "lucide-react";
+import { formatDuration } from "@/utils/timestamp.util";
export const columnPinning = {
- left: ["select", "title"],
right: ["actions"],
+ left: [],
};
-export function buildDataColumns(attributes, rowActions) {
- const base = [
- {
- accessorKey: "title",
- header: "Title",
- cell: ({ row }) => (
-
{row.original.title}
- ),
- },
- {
- accessorKey: "description",
- header: "Description",
- cell: ({ row }) => (
-
- {row.original.description ?? "—"}
-
- ),
- },
- {
- accessorKey: "order",
- header: "Order",
- cell: ({ row }) => (
-
- {row.original.order}
-
- ),
- },
- ];
- return rowActions ? [...base, rowActions] : base;
-}
+// ─── Custom cell overrides ────────────────────────────────────────────────────
+const cellOverrides = {
+ unitCount: (info) => {
+ const count = parseInt(info.getValue() ?? 0, 10);
+ return (
+
+
+
+ {count} {count === 1 ? "unit" : "units"}
+
+
+ );
+ },
+ lessonCount: (info) => {
+ const count = parseInt(info.getValue() ?? 0, 10);
+ return (
+
+
+
+ {count} {count === 1 ? "lesson" : "lessons"}
+
+
+ );
+ },
+ duration_seconds: (info) => {
+ const seconds = parseInt(info.getValue() ?? 0, 10);
+
+ return (
+
+
+
+ {formatDuration(seconds)}
+
+
+ );
+ },
+};
+
+/**
+ * Builds the full column array for the Users table.
+ *
+ * @param {Array} attributes Field definitions from the server (drives data columns)
+ * @param {Array} rowActions Row-level kebab action definitions
+ * @returns {Array} TanStack column definitions
+ */
+export function buildDataColumns(attributes, rowActions) {
+ const visibleAttributes = attributes.filter((a) => !a.hidden);
+
+ return [
+ buildSelectionColumn(),
+ ...buildColumns(visibleAttributes, { cellOverrides }),
+ buildRowActionsColumn(rowActions, { dropdownLabel: "Course Actions" }),
+ ];
+}
\ No newline at end of file
diff --git a/src/modules/admin/config/courses/lessons/archive/columns.config.jsx b/src/modules/admin/config/courses/lessons/archive/columns.config.jsx
new file mode 100644
index 0000000..8f51e87
--- /dev/null
+++ b/src/modules/admin/config/courses/lessons/archive/columns.config.jsx
@@ -0,0 +1,47 @@
+// config/columns.config.jsx
+// Column definitions and pinning config for the Users 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: [],
+};
+
+// ─── Custom cell overrides ────────────────────────────────────────────────────
+const cellOverrides = {
+ duration_seconds: (info) => {
+ const seconds = parseInt(info.getValue() ?? 0, 10);
+
+ return (
+
+
+
+ {formatDuration(seconds)}
+
+
+ );
+ },
+};
+
+/**
+ * Builds the full column array for the Users table.
+ *
+ * @param {Array} attributes Field definitions from the server (drives data columns)
+ * @param {Array} rowActions Row-level kebab action definitions
+ * @returns {Array} TanStack column definitions
+ */
+export function buildDataColumns(attributes, rowActions) {
+ const visibleAttributes = attributes.filter((a) => !a.hidden);
+
+ return [
+ buildSelectionColumn(),
+ ...buildColumns(visibleAttributes, { cellOverrides }),
+ buildRowActionsColumn(rowActions, { dropdownLabel: "Lesson Actions" }),
+ ];
+}
\ No newline at end of file
diff --git a/src/modules/admin/config/courses/lessons/archive/rowActions.config.jsx b/src/modules/admin/config/courses/lessons/archive/rowActions.config.jsx
new file mode 100644
index 0000000..e2952b6
--- /dev/null
+++ b/src/modules/admin/config/courses/lessons/archive/rowActions.config.jsx
@@ -0,0 +1,41 @@
+import { Eye, Pencil, Archive, SquarePlus, SquareChartGantt } from "lucide-react";
+
+export function buildRowActions({ onCreatePage, onViewPage, onView, onEdit, onArchive }) {
+ return [
+ {
+ key: "view",
+ label: "View Lesson",
+ icon:
,
+ onClick: (row) => onView(row),
+ },
+ {
+ key: "edit",
+ label: "Edit Lesson",
+ icon:
,
+ onClick: (row) => onEdit(row),
+ },
+ {
+ key: "edit",
+ label: "View Content",
+ icon:
,
+ className: "text-sky-700 hover:text-sky-600",
+ onClick: (row) => onViewPage(row),
+ separator: true,
+ },
+ {
+ key: "edit",
+ label: "Modify Content",
+ icon:
,
+ className: "text-sky-700 hover:text-sky-600",
+ onClick: (row) => onCreatePage(row),
+ },
+ {
+ key: "archive",
+ label: "Archive",
+ icon:
,
+ className: "text-destructive",
+ onClick: (row) => onArchive(row),
+ separator: true
+ },
+ ]
+}
diff --git a/src/modules/admin/config/courses/lessons/archive/selection.config.jsx b/src/modules/admin/config/courses/lessons/archive/selection.config.jsx
new file mode 100644
index 0000000..6cc4c31
--- /dev/null
+++ b/src/modules/admin/config/courses/lessons/archive/selection.config.jsx
@@ -0,0 +1,30 @@
+import { Download, Archive } 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.asset_id);
+ ids.length === 1
+ ? onArchive(rows[0])
+ : onArchiveMany(ids);
+ },
+ },
+ ];
+}
diff --git a/src/modules/admin/config/courses/lessons/archive/toolbar.config.jsx b/src/modules/admin/config/courses/lessons/archive/toolbar.config.jsx
new file mode 100644
index 0000000..369d195
--- /dev/null
+++ b/src/modules/admin/config/courses/lessons/archive/toolbar.config.jsx
@@ -0,0 +1,62 @@
+// config/courses/lessons/toolbar.config.jsx
+
+import { Plus, RefreshCw, Download, Archive } from "lucide-react";
+import { exportTableToExcel } from "@/utils/excel.util";
+
+export function buildToolbarActions({
+ fetchLessons,
+ pagination,
+ exportConfig,
+ navigate,
+ courseId,
+ unitId,
+ 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/courses/${courseId}/units/${unitId}/lessons/add`
+ ),
+ },
+ {
+ key: "archived-lessons",
+ type: "button",
+ icon:
,
+ label: "Archived Lessons",
+ variant: "secondary",
+ className: "border border-border",
+ onClick: () => navigate("/admin/lessons/archived"),
+ },
+ ];
+}
diff --git a/src/modules/admin/config/courses/lessons/columns.config.jsx b/src/modules/admin/config/courses/lessons/columns.config.jsx
index 9096867..8f51e87 100644
--- a/src/modules/admin/config/courses/lessons/columns.config.jsx
+++ b/src/modules/admin/config/courses/lessons/columns.config.jsx
@@ -1,52 +1,47 @@
+// config/columns.config.jsx
+// Column definitions and pinning config for the Users 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 = {
- left: ["select", "title"],
right: ["actions"],
+ left: [],
};
-export function buildDataColumns(attributes, rowActions) {
- const base = [
- {
- accessorKey: "title",
- header: "Title",
- cell: ({ row }) => (
-
{row.original.title}
- ),
- },
- {
- accessorKey: "description",
- header: "Description",
- cell: ({ row }) => (
-
- {row.original.description ?? "—"}
-
- ),
- },
- {
- accessorKey: "order",
- header: "Order",
- cell: ({ row }) => (
-
- {row.original.order}
-
- ),
- },
- {
- accessorKey: "page",
- header: "Content",
- cell: ({ row }) => {
- const blocks = row.original.page?.blocks ?? [];
- return blocks.length ? (
-
- {blocks.length} block{blocks.length !== 1 ? "s" : ""}
-
- ) : (
-
No content
- );
- },
- },
- ];
+// ─── Custom cell overrides ────────────────────────────────────────────────────
+const cellOverrides = {
+ duration_seconds: (info) => {
+ const seconds = parseInt(info.getValue() ?? 0, 10);
- return rowActions ? [...base, rowActions] : base;
-}
+ return (
+
+
+
+ {formatDuration(seconds)}
+
+
+ );
+ },
+};
+
+/**
+ * Builds the full column array for the Users table.
+ *
+ * @param {Array} attributes Field definitions from the server (drives data columns)
+ * @param {Array} rowActions Row-level kebab action definitions
+ * @returns {Array} TanStack column definitions
+ */
+export function buildDataColumns(attributes, rowActions) {
+ const visibleAttributes = attributes.filter((a) => !a.hidden);
+
+ return [
+ buildSelectionColumn(),
+ ...buildColumns(visibleAttributes, { cellOverrides }),
+ buildRowActionsColumn(rowActions, { dropdownLabel: "Lesson Actions" }),
+ ];
+}
\ No newline at end of file
diff --git a/src/modules/admin/config/courses/lessons/rowActions.config.jsx b/src/modules/admin/config/courses/lessons/rowActions.config.jsx
index 48ce7b4..e2952b6 100644
--- a/src/modules/admin/config/courses/lessons/rowActions.config.jsx
+++ b/src/modules/admin/config/courses/lessons/rowActions.config.jsx
@@ -1,35 +1,41 @@
-// config/courses/lessons/rowActions.config.jsx
+import { Eye, Pencil, Archive, SquarePlus, SquareChartGantt } from "lucide-react";
-import { Eye, Pencil, Trash2 } from "lucide-react";
-
-export function buildRowActions({ onView, onEdit, onArchive }) {
- return {
- id: "actions",
- header: "",
- cell: ({ row }) => (
-
-
-
-
-
- ),
- };
+export function buildRowActions({ onCreatePage, onViewPage, onView, onEdit, onArchive }) {
+ return [
+ {
+ key: "view",
+ label: "View Lesson",
+ icon:
,
+ onClick: (row) => onView(row),
+ },
+ {
+ key: "edit",
+ label: "Edit Lesson",
+ icon:
,
+ onClick: (row) => onEdit(row),
+ },
+ {
+ key: "edit",
+ label: "View Content",
+ icon:
,
+ className: "text-sky-700 hover:text-sky-600",
+ onClick: (row) => onViewPage(row),
+ separator: true,
+ },
+ {
+ key: "edit",
+ label: "Modify Content",
+ icon:
,
+ className: "text-sky-700 hover:text-sky-600",
+ onClick: (row) => onCreatePage(row),
+ },
+ {
+ key: "archive",
+ label: "Archive",
+ icon:
,
+ className: "text-destructive",
+ onClick: (row) => onArchive(row),
+ separator: true
+ },
+ ]
}
diff --git a/src/modules/admin/config/courses/lessons/selection.config.jsx b/src/modules/admin/config/courses/lessons/selection.config.jsx
new file mode 100644
index 0000000..6cc4c31
--- /dev/null
+++ b/src/modules/admin/config/courses/lessons/selection.config.jsx
@@ -0,0 +1,30 @@
+import { Download, Archive } 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.asset_id);
+ ids.length === 1
+ ? onArchive(rows[0])
+ : onArchiveMany(ids);
+ },
+ },
+ ];
+}
diff --git a/src/modules/admin/config/courses/lessons/toolbar.config.jsx b/src/modules/admin/config/courses/lessons/toolbar.config.jsx
index 88db67e..369d195 100644
--- a/src/modules/admin/config/courses/lessons/toolbar.config.jsx
+++ b/src/modules/admin/config/courses/lessons/toolbar.config.jsx
@@ -1,6 +1,6 @@
// config/courses/lessons/toolbar.config.jsx
-import { Plus, RefreshCw, Download } from "lucide-react";
+import { Plus, RefreshCw, Download, Archive } from "lucide-react";
import { exportTableToExcel } from "@/utils/excel.util";
export function buildToolbarActions({
@@ -46,8 +46,17 @@ export function buildToolbarActions({
icon:
,
variant: "default",
onClick: () => navigate(
- `/admin/courses/${courseId}/units/${unitId}/lessons/create`
+ `/admin/courses/${courseId}/units/${unitId}/lessons/add`
),
},
+ {
+ key: "archived-lessons",
+ type: "button",
+ icon:
,
+ label: "Archived Lessons",
+ variant: "secondary",
+ className: "border border-border",
+ onClick: () => navigate("/admin/lessons/archived"),
+ },
];
}
diff --git a/src/modules/admin/config/courses/rowActions.config.jsx b/src/modules/admin/config/courses/rowActions.config.jsx
index ddbb004..cfdf3b7 100644
--- a/src/modules/admin/config/courses/rowActions.config.jsx
+++ b/src/modules/admin/config/courses/rowActions.config.jsx
@@ -1,33 +1,42 @@
-import { Eye, Pencil, Trash2 } from "lucide-react";
+import { Eye, Pencil, Archive, ShelvingUnit, NotebookPen } from "lucide-react";
-export function buildRowActions({ onView, onEdit, onArchive }) {
- return {
- id: "actions",
- header: "",
- cell: ({ row }) => (
-
-
-
-
-
- ),
- };
+export function buildRowActions({ onViewUnits, onView, onEdit, onArchive, onAssessment }) {
+ 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 Units",
+ icon:
,
+ className: "text-sky-700 hover:text-sky-600",
+ onClick: (row) => onViewUnits(row),
+ separator: true
+ },
+ {
+ key: "modify_assessment",
+ label: "Modify Assessment",
+ icon:
,
+ className: "text-purple-700 hover:text-purple-600",
+ onClick: (row) => onAssessment(row),
+ separator: true,
+ },
+ {
+ key: "archive",
+ label: "Archive Course",
+ icon:
,
+ className: "text-destructive",
+ onClick: (row) => onArchive(row),
+ separator: true
+ },
+ ]
}
diff --git a/src/modules/admin/config/courses/selection.config.jsx b/src/modules/admin/config/courses/selection.config.jsx
index 8be6485..47e1fad 100644
--- a/src/modules/admin/config/courses/selection.config.jsx
+++ b/src/modules/admin/config/courses/selection.config.jsx
@@ -1,7 +1,7 @@
-import { Download } from "lucide-react";
+import { Download, Archive } from "lucide-react";
import { exportTableToExcel } from "@/utils/excel.util";
-export function buildSelectionActions({ exportConfig, getTableInstance }) {
+export function buildSelectionActions({ exportConfig, onArchive, onArchiveMany, getTableInstance }) {
return [
{
key: "export-selected",
@@ -14,5 +14,17 @@ export function buildSelectionActions({ exportConfig, getTableInstance }) {
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.course_id);
+ ids.length === 1
+ ? onArchive(rows[0])
+ : onArchiveMany(ids);
+ },
+ },
];
}
diff --git a/src/modules/admin/config/courses/toolbar.config.jsx b/src/modules/admin/config/courses/toolbar.config.jsx
index 96a88f3..a0290b5 100644
--- a/src/modules/admin/config/courses/toolbar.config.jsx
+++ b/src/modules/admin/config/courses/toolbar.config.jsx
@@ -1,4 +1,4 @@
-import { Plus, RefreshCw, Download } from "lucide-react";
+import { Plus, RefreshCw, Download, Archive } from "lucide-react";
import { exportTableToExcel } from "@/utils/excel.util";
export function buildToolbarActions({
@@ -41,7 +41,16 @@ export function buildToolbarActions({
label: "New Course",
icon:
,
variant: "default",
- onClick: () => navigate("/admin/courses/create"),
+ onClick: () => navigate("/admin/courses/add"),
+ },
+ {
+ key: "archived-courses",
+ type: "button",
+ icon:
,
+ label: "Archived Courses",
+ variant: "secondary",
+ className: "border border-border",
+ onClick: () => navigate("/admin/courses/archived"),
},
];
}
diff --git a/src/modules/admin/config/courses/units/archive/columns.config.jsx b/src/modules/admin/config/courses/units/archive/columns.config.jsx
new file mode 100644
index 0000000..1edd8bf
--- /dev/null
+++ b/src/modules/admin/config/courses/units/archive/columns.config.jsx
@@ -0,0 +1,47 @@
+// config/columns.config.jsx
+// Column definitions and pinning config for the Users 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: [],
+};
+
+// ─── Custom cell overrides ────────────────────────────────────────────────────
+const cellOverrides = {
+ duration_seconds: (info) => {
+ const seconds = parseInt(info.getValue() ?? 0, 10);
+
+ return (
+
+
+
+ {formatDuration(seconds)}
+
+
+ );
+ },
+};
+
+/**
+ * Builds the full column array for the Users table.
+ *
+ * @param {Array} attributes Field definitions from the server (drives data columns)
+ * @param {Array} rowActions Row-level kebab action definitions
+ * @returns {Array} TanStack column definitions
+ */
+export function buildDataColumns(attributes, rowActions) {
+ const visibleAttributes = attributes.filter((a) => !a.hidden);
+
+ return [
+ buildSelectionColumn(),
+ ...buildColumns(visibleAttributes, { cellOverrides }),
+ buildRowActionsColumn(rowActions, { dropdownLabel: "Unit Actions" }),
+ ];
+}
\ 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
new file mode 100644
index 0000000..6a92abc
--- /dev/null
+++ b/src/modules/admin/config/courses/units/archive/rowActions.config.jsx
@@ -0,0 +1,42 @@
+import { Eye, Pencil, Archive, BookCheck, NotebookPen } 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
+ },
+ ]
+}
diff --git a/src/modules/admin/config/courses/units/archive/selection.config.jsx b/src/modules/admin/config/courses/units/archive/selection.config.jsx
new file mode 100644
index 0000000..6cc4c31
--- /dev/null
+++ b/src/modules/admin/config/courses/units/archive/selection.config.jsx
@@ -0,0 +1,30 @@
+import { Download, Archive } 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.asset_id);
+ ids.length === 1
+ ? onArchive(rows[0])
+ : onArchiveMany(ids);
+ },
+ },
+ ];
+}
diff --git a/src/modules/admin/config/courses/units/archive/toolbar.config.jsx b/src/modules/admin/config/courses/units/archive/toolbar.config.jsx
new file mode 100644
index 0000000..0f5ef69
--- /dev/null
+++ b/src/modules/admin/config/courses/units/archive/toolbar.config.jsx
@@ -0,0 +1,57 @@
+import { Plus, RefreshCw, Download, Archive } from "lucide-react";
+import { exportTableToExcel } from "@/utils/excel.util";
+
+export function buildToolbarActions({
+ fetchUnits,
+ pagination,
+ exportConfig,
+ navigate,
+ courseId,
+ 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/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/columns.config.jsx b/src/modules/admin/config/courses/units/columns.config.jsx
index c5af0ee..1edd8bf 100644
--- a/src/modules/admin/config/courses/units/columns.config.jsx
+++ b/src/modules/admin/config/courses/units/columns.config.jsx
@@ -1,40 +1,47 @@
-// config/courses/units/columns.config.jsx
+// config/columns.config.jsx
+// Column definitions and pinning config for the Users 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 = {
- left: ["select", "title"],
right: ["actions"],
+ left: [],
};
-export function buildDataColumns(attributes, rowActions) {
- const base = [
- {
- accessorKey: "title",
- header: "Title",
- cell: ({ row }) => (
-
{row.original.title}
- ),
- },
- {
- accessorKey: "description",
- header: "Description",
- cell: ({ row }) => (
-
- {row.original.description ?? "—"}
-
- ),
- },
- {
- accessorKey: "order",
- header: "Order",
- cell: ({ row }) => (
-
- {row.original.order}
-
- ),
- },
- ];
+// ─── Custom cell overrides ────────────────────────────────────────────────────
+const cellOverrides = {
+ duration_seconds: (info) => {
+ const seconds = parseInt(info.getValue() ?? 0, 10);
- return rowActions ? [...base, rowActions] : base;
-}
+ return (
+
+
+
+ {formatDuration(seconds)}
+
+
+ );
+ },
+};
+
+/**
+ * Builds the full column array for the Users table.
+ *
+ * @param {Array} attributes Field definitions from the server (drives data columns)
+ * @param {Array} rowActions Row-level kebab action definitions
+ * @returns {Array} TanStack column definitions
+ */
+export function buildDataColumns(attributes, rowActions) {
+ const visibleAttributes = attributes.filter((a) => !a.hidden);
+
+ return [
+ buildSelectionColumn(),
+ ...buildColumns(visibleAttributes, { cellOverrides }),
+ buildRowActionsColumn(rowActions, { dropdownLabel: "Unit Actions" }),
+ ];
+}
\ No newline at end of file
diff --git a/src/modules/admin/config/courses/units/rowActions.config.jsx b/src/modules/admin/config/courses/units/rowActions.config.jsx
index ddbb004..6a92abc 100644
--- a/src/modules/admin/config/courses/units/rowActions.config.jsx
+++ b/src/modules/admin/config/courses/units/rowActions.config.jsx
@@ -1,33 +1,42 @@
-import { Eye, Pencil, Trash2 } from "lucide-react";
+import { Eye, Pencil, Archive, BookCheck, NotebookPen } from "lucide-react";
-export function buildRowActions({ onView, onEdit, onArchive }) {
- return {
- id: "actions",
- header: "",
- cell: ({ row }) => (
-
-
-
-
-
- ),
- };
+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
+ },
+ ]
}
diff --git a/src/modules/admin/config/courses/units/selection.config.jsx b/src/modules/admin/config/courses/units/selection.config.jsx
new file mode 100644
index 0000000..6cc4c31
--- /dev/null
+++ b/src/modules/admin/config/courses/units/selection.config.jsx
@@ -0,0 +1,30 @@
+import { Download, Archive } 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.asset_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 efe3b54..0f5ef69 100644
--- a/src/modules/admin/config/courses/units/toolbar.config.jsx
+++ b/src/modules/admin/config/courses/units/toolbar.config.jsx
@@ -1,4 +1,4 @@
-import { Plus, RefreshCw, Download } from "lucide-react";
+import { Plus, RefreshCw, Download, Archive } from "lucide-react";
import { exportTableToExcel } from "@/utils/excel.util";
export function buildToolbarActions({
@@ -42,7 +42,16 @@ export function buildToolbarActions({
label: "New Unit",
icon:
,
variant: "default",
- onClick: () => navigate(`/admin/courses/${courseId}/units/create`),
+ 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/user_groups/rowActions.config.jsx b/src/modules/admin/config/user_groups/rowActions.config.jsx
index 040afe4..9c57b01 100644
--- a/src/modules/admin/config/user_groups/rowActions.config.jsx
+++ b/src/modules/admin/config/user_groups/rowActions.config.jsx
@@ -15,13 +15,13 @@ export function buildRowActions({ navigate, onEdit, onArchive }) {
return [
{
key: "view",
- label: "View details",
+ label: "View Info",
icon:
,
onClick: (row) => navigate(`view/${row.group_id}`),
},
{
key: "edit",
- label: "Edit details",
+ label: "Edit Info",
icon:
,
onClick: (row) => onEdit(row),
},
diff --git a/src/modules/admin/config/users/rowActions.config.jsx b/src/modules/admin/config/users/rowActions.config.jsx
index 66aff06..ec93ab9 100644
--- a/src/modules/admin/config/users/rowActions.config.jsx
+++ b/src/modules/admin/config/users/rowActions.config.jsx
@@ -15,13 +15,13 @@ export function buildRowActions({ navigate, onArchive }) {
return [
{
key: "view",
- label: "View details",
+ label: "View Info",
icon:
,
onClick: (row) => navigate(`view/${row.user_id}`),
},
{
key: "edit",
- label: "Edit details",
+ label: "Edit Info",
icon:
,
onClick: (row) => navigate(`edit/${row.user_id}`),
disabled: (row) => row.role === "super_admin",
diff --git a/src/modules/admin/layouts/AdminLayout.jsx b/src/modules/admin/layouts/AdminLayout.jsx
index 8bec925..b287da9 100644
--- a/src/modules/admin/layouts/AdminLayout.jsx
+++ b/src/modules/admin/layouts/AdminLayout.jsx
@@ -42,12 +42,12 @@ const AdminLayout = () => {
}
return (
-
+
-
+
-
navigate(`/admin/${id}`)}>
+
navigate(`/admin`)}>
@@ -81,11 +81,16 @@ const AdminLayout = () => {
{/* ─── All admin contexts live here, scoped to admin routes only ── */}
-
+
+
+ {/* Footer sits outside AdminProvider, at the bottom of the flex column */}
+
)
diff --git a/src/modules/admin/pages/AdminDashboard.jsx b/src/modules/admin/pages/AdminDashboard.jsx
index 4629a3f..5cce363 100644
--- a/src/modules/admin/pages/AdminDashboard.jsx
+++ b/src/modules/admin/pages/AdminDashboard.jsx
@@ -13,7 +13,7 @@ export default function AdminDashboard() {
return (
{/* ── Sticky tab bar — driven by the same array ── */}
-
+
@@ -38,7 +38,7 @@ export default function AdminDashboard() {
{/* ── Sections — same array, one DashboardGrid per entry ── */}
{ADMIN_SECTIONS.map((s) => (
-
+
{s.tiles.length > 0 ? (
) : (
diff --git a/src/modules/admin/pages/courses/AddCourse.jsx b/src/modules/admin/pages/courses/AddCourse.jsx
new file mode 100644
index 0000000..5f8477d
--- /dev/null
+++ b/src/modules/admin/pages/courses/AddCourse.jsx
@@ -0,0 +1,251 @@
+import { useNavigate } from "react-router-dom";
+import { useForm, useFieldArray } from "react-hook-form";
+import { z } from "zod";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { ArrowLeft, House, Plus, Trash2 } from "lucide-react";
+
+import { useCourses } from "@/contexts/AdminCoursesContext";
+import { useAuth } from "@/contexts/AuthContext";
+import AppBreadcrumb from "@/components/generic/Breadcrumb/AppBreadcrumb";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Textarea } from "@/components/ui/textarea";
+import { Spinner } from "@/components/ui/spinner";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+
+// ─── Schema ───────────────────────────────────────────────────────────────────
+
+const schema = z.object({
+ title: z.string().min(1, "Title is required."),
+ description: z.string().optional(),
+ course_code: z.string().optional(),
+ order_index: z.coerce.number().min(0).default(0),
+ level: z.enum(["beginner", "intermediate", "advanced"]).optional(),
+ subscription: z.enum(["free", "premium"]).default("free"),
+ objectives: z.array(z.object({ text: z.string().min(1, "Objective cannot be empty.") })).default([]),
+});
+
+// ─── Helpers ──────────────────────────────────────────────────────────────────
+
+function FieldError({ message }) {
+ if (!message) return null;
+ return
{message}
;
+}
+
+function SectionCard({ title, description, children }) {
+ return (
+
+ {(title || description) && (
+
+ {title &&
{title}
}
+ {description &&
{description}
}
+
+ )}
+ {children}
+
+ );
+}
+
+// ─── Page ─────────────────────────────────────────────────────────────────────
+
+export default function AddCourse() {
+ const navigate = useNavigate();
+ const { createCourse, loading } = useCourses();
+ const { user } = useAuth();
+
+ const {
+ register,
+ handleSubmit,
+ control,
+ setValue,
+ watch,
+ formState: { errors },
+ } = useForm({
+ resolver: zodResolver(schema),
+ defaultValues: {
+ title: "",
+ description: "",
+ course_code: "",
+ order_index: 0,
+ level: "beginner",
+ subscription: "free",
+ objectives: [],
+ },
+ });
+
+ const { fields: objectiveFields, append: appendObjective, remove: removeObjective } =
+ useFieldArray({ control, name: "objectives" });
+
+ const onSubmit = async (values) => {
+ const payload = {
+ ...values,
+ objectives: values.objectives.map((o) => o.text),
+ level: values.level || null,
+ course_code: values.course_code || null,
+ createdBy: user?.user_id ?? null,
+ };
+
+ const result = await createCourse(payload);
+ if (!result) return;
+ navigate("/admin/courses");
+ };
+
+ return (
+
+
+
+
+
+
+
+
Course Details
+
View course information.
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/modules/admin/pages/courses/ArchivedCourseList.jsx b/src/modules/admin/pages/courses/ArchivedCourseList.jsx
new file mode 100644
index 0000000..c755797
--- /dev/null
+++ b/src/modules/admin/pages/courses/ArchivedCourseList.jsx
@@ -0,0 +1,24 @@
+import { House } from "lucide-react";
+import AppBreadcrumb from "@/components/generic/Breadcrumb/AppBreadcrumb";
+import ArchivedCoursesTable from "../../components/courses/ArchivedCourseTable";
+
+export default function ArchivedCourseList() {
+ const items = [
+ { label: "Home", icon:
, to: "/admin" },
+ { label: "Courses", to: "/admin/courses" },
+ { label: "Archived" },
+ ];
+
+ return (
+
+ );
+}
diff --git a/src/modules/admin/pages/courses/CourseAssessment.jsx b/src/modules/admin/pages/courses/CourseAssessment.jsx
new file mode 100644
index 0000000..6d2a812
--- /dev/null
+++ b/src/modules/admin/pages/courses/CourseAssessment.jsx
@@ -0,0 +1,560 @@
+import { useEffect, useRef, useState } from "react";
+import { useNavigate, useParams } from "react-router-dom";
+import { ArrowLeft, House, Plus, Save, ClipboardList, ChevronUp, ChevronDown } from "lucide-react";
+
+import { useCourses } from "@/contexts/AdminCoursesContext";
+import { useAuth } from "@/contexts/AuthContext";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Spinner } from "@/components/ui/spinner";
+import { Checkbox } from "@/components/ui/checkbox";
+import { cn } from "@/lib/utils";
+import { QuestionCard, makeQuestion } from "../../components/courses/QuestionEditor";
+
+// ── Validation ────────────────────────────────────────────────────────────────
+
+function validate(questions) {
+ const errors = {};
+ questions.forEach((q, i) => {
+ const qErr = {};
+ if (!q.question?.trim()) qErr.question = "Question text is required.";
+ const correctCount = q.options.filter((o) => o.is_correct).length;
+ if (correctCount === 0) qErr.options = "At least one correct answer is required.";
+ if (q.options.some((o) => !o.text?.trim())) qErr.options = "All option texts are required.";
+ if (Object.keys(qErr).length) errors[i] = qErr;
+ });
+ return errors;
+}
+
+// ── Jump to input ─────────────────────────────────────────────────────────────
+
+function JumpToInput({ max, onJump }) {
+ const [val, setVal] = useState("");
+
+ const handleJump = () => {
+ const n = parseInt(val, 10);
+ if (!isNaN(n) && n >= 1 && n <= max) {
+ onJump(n - 1);
+ setVal("");
+ }
+ };
+
+ return (
+
+ setVal(e.target.value)}
+ onKeyDown={(e) => e.key === "Enter" && handleJump()}
+ placeholder={`1–${max}`}
+ className="h-7 text-xs"
+ />
+
+
+ );
+}
+
+// ── Question Navigator ────────────────────────────────────────────────────────
+
+const TYPE_LABEL = {
+ multiple_choice: "MC",
+ multi_select: "MS",
+ true_false: "TF",
+};
+
+function QuestionNavigator({ questions, activeIndex, onJump, onMove, navItemRefs, errors }) {
+ return (
+
+
+ {/* Header */}
+
+
+ Questions
+
+
+ {questions.length} total
+
+
+
+ {/* Scrollable list */}
+
+ {questions.length === 0 ? (
+
+ No questions yet.
+
+ ) : (
+
+ {questions.map((q, i) => {
+ const isActive = i === activeIndex;
+ const hasError = !!errors?.[i];
+ const typeLabel = TYPE_LABEL[q.type] ?? "Q";
+
+ return (
+
(navItemRefs.current[i] = el)}
+ onClick={() => onJump(i)}
+ className={cn(
+ "group flex items-center gap-1.5 rounded-md px-2 py-1.5 cursor-pointer transition-colors",
+ isActive
+ ? "bg-primary/10 text-primary"
+ : hasError
+ ? "bg-destructive/10 text-destructive hover:bg-destructive/20"
+ : "hover:bg-muted text-foreground"
+ )}
+ >
+ {/* Number badge */}
+
+ {i + 1}
+
+
+ {/* Type */}
+
+ {typeLabel}
+
+
+ {/* Question preview */}
+
+ {q.question?.trim()
+ ? q.question.trim()
+ : Untitled
+ }
+
+
+ {/* Move buttons — show on hover */}
+
+
+
+
+
+ );
+ })}
+
+ )}
+
+
+ {/* Jump to — only when enough questions */}
+ {questions.length > 5 && (
+
+ )}
+
+
+ );
+}
+
+// ── Main Page ─────────────────────────────────────────────────────────────────
+
+export default function CourseAssessment() {
+ const navigate = useNavigate();
+ const { courseId } = useParams();
+ const {
+ fetchAssessment, createAssessment, updateAssessment,
+ createAssessmentQuestion, updateAssessmentQuestion,
+ course, assessment, loading,
+ } = useCourses();
+ const { user } = useAuth();
+
+ const [initializing, setInitializing] = useState(true);
+ const [questions, setQuestions] = useState([]);
+ const [errors, setErrors] = useState({});
+ const [activeIndex, setActiveIndex] = useState(0);
+
+ const [title, setTitle] = useState("");
+ const [passingScore, setPassingScore] = useState(70);
+ const [timeLimit, setTimeLimit] = useState("");
+ const [isRequired, setIsRequired] = useState(false);
+ const [maxQuestions, setMaxQuestions] = useState("");
+
+ const questionRefs = useRef([]);
+ const navItemRefs = useRef([]);
+ const headerRef = useRef(null);
+
+ // ── Fetch ──────────────────────────────────────────────────────────────────
+ useEffect(() => {
+ (async () => {
+ await fetchAssessment(courseId);
+ setInitializing(false);
+ })();
+ }, [courseId]);
+
+ // ── Seed ──────────────────────────────────────────────────────────────────
+ useEffect(() => {
+ if (!assessment) return;
+ setTitle(assessment.title ?? "");
+ setPassingScore(assessment.passing_score ?? 70);
+ setTimeLimit(assessment.time_limit_minutes ?? "");
+ setIsRequired(assessment.is_required === true || assessment.is_required === 1);
+ setMaxQuestions(assessment.max_questions ?? "");
+ setQuestions(
+ (assessment.questions ?? []).map((q) => ({
+ ...q,
+ _tempId: q.question_id,
+ options: q.options ?? [],
+ }))
+ );
+ }, [assessment]);
+
+ // ── Measure sticky header → --assessment-h ────────────────────────────────
+ useEffect(() => {
+ if (!headerRef.current) return;
+ const update = () => {
+ document.documentElement.style.setProperty(
+ "--assessment-h",
+ `${headerRef.current.offsetHeight}px`
+ );
+ };
+ update();
+ window.addEventListener("resize", update);
+ return () => window.removeEventListener("resize", update);
+ }, []);
+
+ // ── Scroll to keep active nav item visible ─────────────────────────────────
+ useEffect(() => {
+ navItemRefs.current[activeIndex]?.scrollIntoView({
+ behavior: "smooth",
+ block: "nearest",
+ });
+ }, [activeIndex]);
+
+ // ── IntersectionObserver — highlight nav as user scrolls ──────────────────
+ useEffect(() => {
+ if (!questions.length) return;
+ const observers = [];
+
+ questionRefs.current.forEach((el, i) => {
+ if (!el) return;
+ const observer = new IntersectionObserver(
+ ([entry]) => { if (entry.isIntersecting) setActiveIndex(i); },
+ { rootMargin: "-20% 0px -70% 0px", threshold: 0 }
+ );
+ observer.observe(el);
+ observers.push(observer);
+ });
+
+ return () => observers.forEach((o) => o.disconnect());
+ }, [questions.length]);
+
+ // ── Scroll helper with sticky offset ──────────────────────────────────────
+ const scrollToQuestion = (index) => {
+ const el = questionRefs.current[index];
+ if (!el) return;
+ const navbarH = parseInt(getComputedStyle(document.documentElement).getPropertyValue("--navbar-h") || "0", 10);
+ const assessmentH = parseInt(getComputedStyle(document.documentElement).getPropertyValue("--assessment-h") || "0", 10);
+ const offset = navbarH + assessmentH + 16;
+ const top = el.getBoundingClientRect().top + window.scrollY - offset;
+ window.scrollTo({ top, behavior: "smooth" });
+ };
+
+ // ── Question actions ───────────────────────────────────────────────────────
+ const addQuestion = (type = "multiple_choice") => {
+ setQuestions((prev) => {
+ const next = [...prev, { ...makeQuestion(type), order_index: prev.length }];
+ setTimeout(() => {
+ const idx = next.length - 1;
+ setActiveIndex(idx);
+ scrollToQuestion(idx);
+ }, 50);
+ return next;
+ });
+ };
+
+ const updateQuestion = (index, updated) => {
+ setQuestions((prev) => prev.map((q, i) => i === index ? updated : q));
+ setErrors((prev) => { const e = { ...prev }; delete e[index]; return e; });
+ };
+
+ const removeQuestion = (index) => {
+ setQuestions((prev) => prev.filter((_, i) => i !== index));
+ setActiveIndex((prev) => Math.max(0, prev >= index ? prev - 1 : prev));
+ };
+
+ const moveQuestion = (index, direction) => {
+ setQuestions((prev) => {
+ const next = [...prev];
+ const swapIndex = direction === "up" ? index - 1 : index + 1;
+ if (swapIndex < 0 || swapIndex >= next.length) return prev;
+ [next[index], next[swapIndex]] = [next[swapIndex], next[index]];
+ return next;
+ });
+ const newIndex = direction === "up" ? index - 1 : index + 1;
+ setActiveIndex(newIndex);
+ setTimeout(() => scrollToQuestion(newIndex), 50);
+ };
+
+ const jumpTo = (index) => {
+ setActiveIndex(index);
+ scrollToQuestion(index);
+ };
+
+ const totalPoints = questions.reduce((sum, q) => sum + (q.points ?? 1), 0);
+
+ // ── Save ───────────────────────────────────────────────────────────────────
+ const handleSave = async () => {
+ const errs = validate(questions);
+ if (Object.keys(errs).length) {
+ setErrors(errs);
+ // Jump to first error
+ const firstErr = parseInt(Object.keys(errs)[0], 10);
+ jumpTo(firstErr);
+ return;
+ }
+
+ let assessmentId = assessment?.assessment_id;
+ const meta = {
+ title: title || "Course Assessment",
+ passing_score: passingScore,
+ time_limit_minutes: timeLimit ? parseInt(timeLimit) : null,
+ is_required: isRequired,
+ max_questions: maxQuestions ? parseInt(maxQuestions) : null,
+ updatedBy: user?.user_id,
+ createdBy: user?.user_id,
+ };
+
+ if (!assessmentId) {
+ const res = await createAssessment(courseId, meta);
+ assessmentId = res?.data?.data?.data?.assessment_id;
+ if (!assessmentId) return;
+ } else {
+ await updateAssessment(courseId, assessmentId, meta);
+ }
+
+ for (let i = 0; i < questions.length; i++) {
+ const q = { ...questions[i], order_index: i, updatedBy: user?.user_id };
+ if (q.question_id) {
+ await updateAssessmentQuestion(courseId, assessmentId, q.question_id, q);
+ } else {
+ await createAssessmentQuestion(courseId, assessmentId, q);
+ }
+ }
+
+ navigate(-1);
+ };
+
+ // ── Render ─────────────────────────────────────────────────────────────────
+ return (
+
+
+ {/* ── Sticky header ── */}
+
+
+
+
+
+
+
+ Course Assessment
+
+
+ {questions.length} question{questions.length !== 1 ? "s" : ""} · {totalPoints} total point{totalPoints !== 1 ? "s" : ""}
+
+
+
+
+
+
+
+ {/* ── Split layout ── */}
+
+
+ {/* LEFT — Navigator (desktop only) */}
+
+
+
+
+ {/* RIGHT — Main content */}
+
+ {initializing ? (
+
+
+
+ ) : (
+
+
+ {/* ── Settings ── */}
+
+
Settings
+
+
+
+ setTitle(e.target.value)}
+ placeholder="Course Assessment"
+ />
+
+
+
+
+
+ setIsRequired(val)}
+ />
+
+
+
+
+ {maxQuestions && parseInt(maxQuestions) < questions.length && (
+
+ Takers will see {maxQuestions} randomly selected questions
+ out of {questions.length} in the pool.
+
+ )}
+
+ {/* ── Questions ── */}
+
+ {questions.length === 0 ? (
+
+
+
+ No questions yet. Add one below.
+
+
+ ) : (
+ questions.map((q, i) => (
+
(questionRefs.current[i] = el)}
+ onClick={() => setActiveIndex(i)}
+ >
+ updateQuestion(i, updated)}
+ onRemove={() => removeQuestion(i)}
+ error={errors[i]}
+ />
+
+ ))
+ )}
+
+
+ {/* ── Add question ── */}
+
+
+ Add Question
+
+
+ {[
+ { type: "multiple_choice", label: "Multiple Choice" },
+ { type: "multi_select", label: "Multi Select" },
+ { type: "true_false", label: "True / False" },
+ ].map(({ type, label }) => (
+
+ ))}
+
+
+
+
+ )}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/modules/admin/pages/courses/CourseDetail.jsx b/src/modules/admin/pages/courses/CourseDetail.jsx
deleted file mode 100644
index 3de1d7d..0000000
--- a/src/modules/admin/pages/courses/CourseDetail.jsx
+++ /dev/null
@@ -1,73 +0,0 @@
-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 { Button } from "@/components/ui/button";
-import { Spinner } from "@/components/ui/spinner";
-import UnitsTable from "../../components/courses/UnitsTable";
-
-export default function CourseDetail() {
- 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 ?? "..." },
- ];
-
- // Replace the loading check
- if (initializing) {
- return (
-
-
-
- );
- }
-
- return (
-
-
-
-
-
-
- {/* ── Course header ── */}
-
-
-
-
-
{course?.title}
- {course?.description && (
-
{course.description}
- )}
-
-
-
-
-
- {/* ── Units ── */}
-
-
-
-
-
- );
-}
diff --git a/src/modules/admin/pages/courses/CreateCourse.jsx b/src/modules/admin/pages/courses/CreateCourse.jsx
deleted file mode 100644
index 1094c5e..0000000
--- a/src/modules/admin/pages/courses/CreateCourse.jsx
+++ /dev/null
@@ -1,103 +0,0 @@
-import { useNavigate } from "react-router-dom";
-import { useForm } from "react-hook-form";
-import { z } from "zod";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { ArrowLeft } from "lucide-react";
-import { House } from "lucide-react";
-
-import { useCourses } from "@/contexts/AdminCoursesContext";
-import { useAuth } from "@/contexts/AuthContext";
-import AppBreadcrumb from "@/components/generic/Breadcrumb/AppBreadcrumb";
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
-import { Textarea } from "@/components/ui/textarea";
-import { Spinner } from "@/components/ui/spinner";
-
-const schema = z.object({
- title: z.string().min(1, "Title is required."),
- description: z.string().optional(),
- order: z.coerce.number().min(0).default(0),
-});
-
-function FieldError({ message }) {
- if (!message) return null;
- return
{message}
;
-}
-
-export default function CreateCourse() {
- const navigate = useNavigate();
- const { createCourse, loading } = useCourses();
- const { user } = useAuth();
-
- const { register, handleSubmit, formState: { errors } } = useForm({
- resolver: zodResolver(schema),
- defaultValues: { title: "", description: "", order: 0 },
- });
-
- const items = [
- { label: "Home", icon:
, to: "/admin" },
- { label: "Courses", to: "/admin/courses" },
- { label: "Create" },
- ];
-
- const onSubmit = async (data) => {
- const result = await createCourse({ ...data, createdBy: user?.user_id });
- if (!result) return;
- navigate("/admin/courses");
- };
-
- return (
-
-
-
-
-
-
-
-
-
Create Course
-
Add a new course.
-
-
-
-
-
-
-
- );
-}
diff --git a/src/modules/admin/pages/courses/EditCourse.jsx b/src/modules/admin/pages/courses/EditCourse.jsx
index da50267..28f7fe2 100644
--- a/src/modules/admin/pages/courses/EditCourse.jsx
+++ b/src/modules/admin/pages/courses/EditCourse.jsx
@@ -1,9 +1,9 @@
import { useEffect } from "react";
import { useNavigate, useParams } from "react-router-dom";
-import { useForm } from "react-hook-form";
+import { useForm, useFieldArray } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
-import { ArrowLeft, House } from "lucide-react";
+import { ArrowLeft, House, Plus, Trash2 } from "lucide-react";
import { useCourses } from "@/contexts/AdminCoursesContext";
import { useAuth } from "@/contexts/AuthContext";
@@ -13,63 +13,129 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Spinner } from "@/components/ui/spinner";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+
+// ─── Schema ───────────────────────────────────────────────────────────────────
const schema = z.object({
title: z.string().min(1, "Title is required."),
description: z.string().optional(),
- order: z.coerce.number().min(0).default(0),
+ course_code: z.string().optional(),
+ order_index: z.coerce.number().min(0).default(0),
+ level: z.enum(["beginner", "intermediate", "advanced"]).optional(),
+ subscription: z.enum(["free", "premium"]).default("free"),
+ objectives: z.array(z.object({ text: z.string().min(1, "Objective cannot be empty.") })).default([]),
});
+// ─── Helpers ──────────────────────────────────────────────────────────────────
+
function FieldError({ message }) {
if (!message) return null;
return
{message}
;
}
+function SectionCard({ title, description, children }) {
+ return (
+
+ {(title || description) && (
+
+ {title &&
{title}
}
+ {description &&
{description}
}
+
+ )}
+ {children}
+
+ );
+}
+
+// ─── Page ─────────────────────────────────────────────────────────────────────
+
export default function EditCourse() {
const navigate = useNavigate();
const { courseId } = useParams();
const { fetchCourse, updateCourse, loading } = useCourses();
const { user } = useAuth();
- const { register, handleSubmit, reset, formState: { errors, isDirty } } = useForm({
+ const {
+ register,
+ handleSubmit,
+ reset,
+ control,
+ setValue,
+ watch,
+ formState: { errors, isDirty },
+ } = useForm({
resolver: zodResolver(schema),
- defaultValues: { title: "", description: "", order: 0 },
+ defaultValues: {
+ title: "",
+ description: "",
+ course_code: "",
+ order_index: 0,
+ level: undefined,
+ subscription: "free",
+ objectives: [],
+ },
});
- const items = [
- { label: "Home", icon:
, to: "/admin" },
- { label: "Courses", to: "/admin/courses" },
- { label: "Edit" },
- ];
+ const { fields: objectiveFields, append: appendObjective, remove: removeObjective } =
+ useFieldArray({ control, name: "objectives" });
+ // ─── Load existing course data ────────────────────────────────────────────
useEffect(() => {
(async () => {
const res = await fetchCourse(courseId);
- const course = res?.data?.data ?? null;
- if (!course) return;
+ const c = res?.data?.data ?? null;
+ if (!c) return;
+
reset({
- title: course.title ?? "",
- description: course.description ?? "",
- order: course.order ?? 0,
+ title: c.title ?? "",
+ description: c.description ?? "",
+ course_code: c.course_code ?? "",
+ order_index: c.order_index ?? 0,
+ level: c.level ?? undefined,
+ subscription: c.subscription ?? "free",
+ objectives: (c.objectives ?? []).map((o) => ({
+ objective_id: o.objective_id ?? null, // ← carry the id
+ value: o.text ?? "", // ← form field is "value"
+ })),
});
})();
}, [courseId]);
- const onSubmit = async (data) => {
+ const onSubmit = async (values) => {
if (!isDirty) return navigate(-1);
- const result = await updateCourse(courseId, { ...data, updatedBy: user?.user_id });
+
+ console.log(values)
+ const payload = {
+ ...values,
+ objectives: values.objectives?.map((o, i) => ({
+ objective_id: o.objective_id ?? null,
+ text: o.text,
+ order_index: i,
+ })) ?? [],
+ level: values.level || null,
+ course_code: values.course_code || null,
+ updatedBy: user?.user_id ?? null,
+ };
+
+ const result = await updateCourse(courseId, payload);
if (!result) return;
navigate(-1);
};
return (
-
-
-
+
+
+
+ {/* ── Header ── */}
-