diff --git a/src/modules/admin/components/courses/CreateUnitDialog.jsx b/src/modules/admin/components/courses/CreateUnitDialog.jsx index c8c90a8..7b5fce6 100644 --- a/src/modules/admin/components/courses/CreateUnitDialog.jsx +++ b/src/modules/admin/components/courses/CreateUnitDialog.jsx @@ -1,22 +1,27 @@ // modules/admin/components/courses/CreateUnitDialog.jsx // Lightweight "create a brand-new unit and attach it to this course" dialog — // the create-new counterpart to AttachUnitsDialog's attach-existing flow. +// Two steps (Details -> Completion Requirements) to match the other two +// create-unit surfaces (AddUnit.jsx, AddLibraryUnit.jsx) — same capability, +// just condensed into a dialog since this one runs inline in a wizard. -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; +import { ChevronLeft } from "lucide-react"; import { useCourses } from "@/contexts/AdminCoursesContext"; import { useAuth } from "@/contexts/AuthContext"; import { - Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogClose, + Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Spinner } from "@/components/ui/spinner"; +import DraftRequirementsEditor from "./DraftRequirementsEditor"; const schema = z.object({ title: z.string().min(1, "Title is required."), @@ -25,19 +30,33 @@ const schema = z.object({ }); export default function CreateUnitDialog({ open, onOpenChange, courseId, nextOrder = 0, onCreated, draftMode = false }) { - const { createUnit, loading } = useCourses(); + const { createUnit, syncUnitRequirements, loading } = useCourses(); const { user } = useAuth(); - const { register, handleSubmit, reset, formState: { errors } } = useForm({ + const [step, setStep] = useState(0); + const [requirements, setRequirements] = useState([]); + + const { register, handleSubmit, trigger, reset, formState: { errors } } = useForm({ resolver: zodResolver(schema), defaultValues: { title: "", description: "", order: nextOrder }, }); useEffect(() => { - if (open) reset({ title: "", description: "", order: nextOrder }); + if (open) { + reset({ title: "", description: "", order: nextOrder }); + setStep(0); + setRequirements([]); + } }, [open, nextOrder, reset]); + const handleNext = async () => { + const valid = await trigger(); + if (valid) setStep(1); + }; + const onValid = async (values) => { + const clean = requirements.map(({ _key, ...r }) => r); + if (draftMode) { onCreated?.({ unit_id: null, @@ -46,6 +65,7 @@ export default function CreateUnitDialog({ open, onOpenChange, courseId, nextOrd description: values.description, order_index: values.order, lessons: [], + requirements: clean, }); onOpenChange(false); return; @@ -54,6 +74,11 @@ export default function CreateUnitDialog({ open, onOpenChange, courseId, nextOrd const result = await createUnit(courseId, { ...values, createdBy: user?.user_id }); const unit = result?.data?.data ?? null; if (!unit) return; + + if (clean.length > 0) { + await syncUnitRequirements(courseId, unit.unit_id, clean); + } + onCreated?.(unit); onOpenChange(false); }; @@ -63,35 +88,63 @@ export default function CreateUnitDialog({ open, onOpenChange, courseId, nextOrd New Unit + + Creates a new unit in the shared Units Library and attaches it to this course. + - - - Title * - - - {errors.title && {errors.title.message}} - + {step === 0 && ( + <> + + + Title * + + + {errors.title && {errors.title.message}} + - - Description - - + + Description + + - - Order - - + + Order + + + > + )} + + {step === 1 && ( + + + Configure how learners complete this unit — optional, sensible defaults apply automatically. + + + + )} - - Cancel - - - {loading && } - Create Unit - + {step === 0 ? ( + <> + + Cancel + + Next + > + ) : ( + <> + setStep(0)} disabled={loading}> + + Back + + + {loading && } + Create Unit + + > + )} diff --git a/src/modules/admin/components/courses/UnitsTable.jsx b/src/modules/admin/components/courses/UnitsTable.jsx index ebfd47a..88ebf0d 100644 --- a/src/modules/admin/components/courses/UnitsTable.jsx +++ b/src/modules/admin/components/courses/UnitsTable.jsx @@ -18,7 +18,7 @@ import { buildRowActions } from "../../config/courses/units/rowActions.config"; import { getTimestamp } from "@/utils/timestamp.util"; -export default function UnitsTable({ courseId }) { +export default function UnitsTable({ courseId, returnTo }) { const [archiveTarget, setArchiveTarget] = useState(null); const [archiveIds, setArchiveIds] = useState(null); const [attachOpen, setAttachOpen] = useState(false); @@ -68,6 +68,7 @@ export default function UnitsTable({ courseId }) { exportConfig, navigate, courseId, + returnTo, getFilters: () => tableRefsRef.current.getFilters(), getSort: () => tableRefsRef.current.getSort(), getTableInstance: () => tableRefsRef.current.tableInstance, diff --git a/src/modules/admin/config/courses/units/toolbar.config.jsx b/src/modules/admin/config/courses/units/toolbar.config.jsx index 2d6ee65..a54302b 100644 --- a/src/modules/admin/config/courses/units/toolbar.config.jsx +++ b/src/modules/admin/config/courses/units/toolbar.config.jsx @@ -7,6 +7,7 @@ export function buildToolbarActions({ exportConfig, navigate, courseId, + returnTo, getFilters, getSort, getTableInstance, @@ -42,7 +43,7 @@ export function buildToolbarActions({ label: "New Unit", icon: , variant: "default", - onClick: () => navigate(`/admin/courses/${courseId}/units/add`), + onClick: () => navigate(`/admin/courses/${courseId}/units/add`, returnTo ? { state: { returnTo } } : undefined), }, { key: "archived-units", diff --git a/src/modules/admin/pages/courses/AddCourse.jsx b/src/modules/admin/pages/courses/AddCourse.jsx index 2a7893e..148d405 100644 --- a/src/modules/admin/pages/courses/AddCourse.jsx +++ b/src/modules/admin/pages/courses/AddCourse.jsx @@ -271,6 +271,7 @@ export default function AddCourse() { unit_id: u.unit_id, title: u.title, description: u.description, + requirements: u.requirements ?? [], lessons: u.lessons.map((l) => ({ lesson_id: l.lesson_id, title: l.title, diff --git a/src/modules/admin/pages/courses/EditCourse.jsx b/src/modules/admin/pages/courses/EditCourse.jsx index 0bfb37f..a5a6e31 100644 --- a/src/modules/admin/pages/courses/EditCourse.jsx +++ b/src/modules/admin/pages/courses/EditCourse.jsx @@ -1,5 +1,5 @@ import { useEffect, useState, useCallback } from "react"; -import { useNavigate, useParams } from "react-router-dom"; +import { useNavigate, useParams, useSearchParams } from "react-router-dom"; import { useForm, useFieldArray, useWatch } from "react-hook-form"; import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -14,6 +14,7 @@ import { TIER_COLOR_OPTIONS } from "@/utils/tierColors"; import { AssetPickerSheet } from "@/components/generic/AssetPickerSheet"; import { PageMeta } from "@/contexts/MetadataContext"; import CourseInstructorPicker from "@/modules/admin/components/courses/CourseInstructorPicker"; +import UnitsTable from "@/modules/admin/components/courses/UnitsTable"; import CoursePrerequisiteBuilder from "@/modules/admin/components/courses/CoursePrerequisiteBuilder"; import { useCategories } from "@/contexts/AdminCategoriesContext"; import { useAuth } from "@/contexts/AuthContext"; @@ -61,6 +62,7 @@ const schema = z.object({ const STEPS = [ { label: "Basic Info", description: "Title, level & objectives" }, + { label: "Units", description: "Roadmap for this course" }, { label: "Categories", description: "Tags & instructors" }, { label: "Rewards", description: "Badge & achievements" }, { label: "Requirements", description: "Prerequisites & completion" }, @@ -157,7 +159,12 @@ export default function EditCourse() { const { categories: allCategories, fetchCategories } = useCategories(); const { user } = useAuth(); - const [currentStep, setCurrentStep] = useState(0); + const [searchParams] = useSearchParams(); + const initialStep = Math.min( + Math.max(Number(searchParams.get("step")) || 0, 0), + STEPS.length - 1, + ); + const [currentStep, setCurrentStep] = useState(initialStep); // ─── Tier categories ────────────────────────────────────────────────────── const [tierCategories, setTierCategories] = useState([]); @@ -751,8 +758,18 @@ export default function EditCourse() { > )} - {/* ── Step 1: Categories & Instructors ── */} + {/* ── Step 1: Units ── */} {currentStep === 1 && ( + + + + )} + + {/* ── Step 2: Categories & Instructors ── */} + {currentStep === 2 && ( <> {selectedCategoryIds.length > 0 && ( @@ -900,8 +917,8 @@ export default function EditCourse() { > )} - {/* ── Step 2: Rewards ── */} - {currentStep === 2 && ( + {/* ── Step 3: Rewards ── */} + {currentStep === 3 && ( )} - {/* ── Step 3: Prerequisites & Completion Requirements ── */} - {currentStep === 3 && ( + {/* ── Step 4: Prerequisites & Completion Requirements ── */} + {currentStep === 4 && ( <> )} - {/* ── Step 4: Pricing ── */} - {currentStep === 4 && ( + {/* ── Step 5: Pricing ── */} + {currentStep === 5 && ( (step === 0 ? navigate(`/admin/courses/${courseId}/units`) : setStep(0))} + onClick={() => (step === 0 ? navigate(backTarget) : setStep(0))} > Create Unit - Add a new unit to this course. + + Creates a new unit in the shared Units Library and attaches it to this course. + @@ -169,7 +174,7 @@ export default function AddUnit() { (step === 0 ? navigate(`/admin/courses/${courseId}/units`) : setStep(0))} + onClick={() => (step === 0 ? navigate(backTarget) : setStep(0))} disabled={loading} >
{errors.title.message}
+ Configure how learners complete this unit — optional, sensible defaults apply automatically. +
Add a new unit to this course.
+ Creates a new unit in the shared Units Library and attaches it to this course. +