mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
course,tasklist,task and completed validation
Signed-off-by: rgrgogu <obsequio.rus@gmail.com>
This commit is contained in:
@@ -24,6 +24,7 @@ import {
|
||||
import CourseBadge from "@/modules/admin/components/courses/CourseBadge";
|
||||
import RoadmapBuilder from "@/modules/admin/components/courses/RoadmapBuilder";
|
||||
import AchievementsBuilder from "@/modules/admin/components/courses/AchievementsBuilder";
|
||||
import CoursePrerequisiteBuilder from "@/modules/admin/components/courses/CoursePrerequisiteBuilder";
|
||||
import { TIER_COLOR_OPTIONS } from "@/utils/tierColors";
|
||||
import { AssetPickerSheet } from "@/components/generic/AssetPickerSheet";
|
||||
import { useUnsavedChangesGuard } from "@/hooks/useUnsavedChangesGuard";
|
||||
@@ -40,6 +41,7 @@ const schema = z.object({
|
||||
status: z.enum(["draft", "published", "unpublished"]).default("draft"),
|
||||
objectives: z.array(z.object({ text: z.string().min(1, "Objective cannot be empty.") }))
|
||||
.min(1, "At least one learning objective is required."),
|
||||
roles: z.array(z.object({ text: z.string().min(1, "Role cannot be empty.") })).default([]),
|
||||
achievement_keys: z.array(z.string()).max(1).default([]),
|
||||
});
|
||||
|
||||
@@ -47,7 +49,7 @@ const schema = z.object({
|
||||
|
||||
const STEPS = [
|
||||
{ label: "Basic Info", description: "Title, level & objectives" },
|
||||
{ label: "Roadmap", description: "Units & lessons" },
|
||||
{ label: "Roadmap", description: "Units, lessons & prerequisites" },
|
||||
{ label: "Rewards", description: "Badge & achievements" },
|
||||
{ label: "Review", description: "Confirm & create" },
|
||||
];
|
||||
@@ -127,11 +129,12 @@ function StepIndicator({ steps, current, onStepClick }) {
|
||||
|
||||
export default function AddCourse() {
|
||||
const navigate = useNavigate();
|
||||
const { createCourseFull, loading } = useCourses();
|
||||
const { createCourseFull, loading, fetchCoursesFlat, fetchUnitsFlat, fetchLessonsFlat } = useCourses();
|
||||
const { user } = useAuth();
|
||||
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [roadmapUnits, setRoadmapUnits] = useState([]);
|
||||
const [prerequisites, setPrerequisites] = useState([]);
|
||||
const [tierCategories, setTierCategories] = useState([]);
|
||||
useEffect(() => {
|
||||
api.get("/admin/tiers/categories")
|
||||
@@ -139,6 +142,18 @@ export default function AddCourse() {
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const [flatCourses, setFlatCourses] = useState([]);
|
||||
const [flatUnits, setFlatUnits] = useState([]);
|
||||
const [flatLessons, setFlatLessons] = useState([]);
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const [c, u, l] = await Promise.all([fetchCoursesFlat(), fetchUnitsFlat(), fetchLessonsFlat()]);
|
||||
setFlatCourses(c ?? []);
|
||||
setFlatUnits(u ?? []);
|
||||
setFlatLessons(l ?? []);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const [badgeColor, setBadgeColor] = useState("purple");
|
||||
const [badgeImageUrl, setBadgeImageUrl] = useState(null);
|
||||
const [badgeAssetId, setBadgeAssetId] = useState(null);
|
||||
@@ -163,12 +178,15 @@ export default function AddCourse() {
|
||||
subscription: "free",
|
||||
status: "draft",
|
||||
objectives: [],
|
||||
roles: [],
|
||||
achievement_keys: [],
|
||||
},
|
||||
});
|
||||
|
||||
const { fields: objectiveFields, append: appendObjective, remove: removeObjective } =
|
||||
useFieldArray({ control, name: "objectives" });
|
||||
const { fields: roleFields, append: appendRole, remove: removeRole } =
|
||||
useFieldArray({ control, name: "roles" });
|
||||
|
||||
const currentAchKeys = useWatch({ control, name: "achievement_keys" });
|
||||
const watchedTitle = useWatch({ control, name: "title" });
|
||||
@@ -179,6 +197,7 @@ export default function AddCourse() {
|
||||
const watchedSubscr = useWatch({ control, name: "subscription" });
|
||||
const watchedStatus = useWatch({ control, name: "status" });
|
||||
const watchedObjectives = useWatch({ control, name: "objectives" });
|
||||
const watchedRoles = useWatch({ control, name: "roles" });
|
||||
|
||||
const [achievementRegistry, setAchievementRegistry] = useState([]);
|
||||
useEffect(() => {
|
||||
@@ -198,6 +217,7 @@ export default function AddCourse() {
|
||||
const hasUnsavedChanges =
|
||||
isDirty ||
|
||||
roadmapUnits.length > 0 ||
|
||||
prerequisites.length > 0 ||
|
||||
currentAchKeys.length > 0 ||
|
||||
!!badgeImageUrl ||
|
||||
badgeColor !== "purple";
|
||||
@@ -237,6 +257,12 @@ export default function AddCourse() {
|
||||
subscription: values.subscription,
|
||||
status: values.status,
|
||||
objectives: values.objectives.map((o) => o.text),
|
||||
roles: values.roles.map((r) => r.text),
|
||||
// Drop rows where a type was picked but no item was actually selected —
|
||||
// sending an empty ref_id fails at the DB level.
|
||||
prerequisites: prerequisites
|
||||
.filter((p) => p.ref_id !== "" && p.ref_id != null)
|
||||
.map(({ ref_type, ref_id }) => ({ ref_type, ref_id })),
|
||||
achievement_keys: currentAchKeys,
|
||||
badge_color: badgeColor,
|
||||
badge_asset_id: badgeAssetId ?? null,
|
||||
@@ -426,12 +452,66 @@ export default function AddCourse() {
|
||||
</Button>
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard
|
||||
title="Course Roles"
|
||||
description="Who is this course intended for? e.g. Sales Agent, Property Manager."
|
||||
>
|
||||
<div className="space-y-2">
|
||||
{roleFields.map((field, index) => (
|
||||
<div key={field.id} className="flex items-start gap-2">
|
||||
<div className="flex-1 space-y-1">
|
||||
<Input
|
||||
placeholder={`Role ${index + 1}`}
|
||||
{...register(`roles.${index}.text`)}
|
||||
/>
|
||||
<FieldError message={errors.roles?.[index]?.text?.message} />
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="mt-0.5 text-muted-foreground hover:text-destructive"
|
||||
onClick={() => removeRole(index)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full mt-1"
|
||||
onClick={() => appendRole({ text: "" })}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Role
|
||||
</Button>
|
||||
</div>
|
||||
</SectionCard>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ── Step 1: Roadmap ── */}
|
||||
{currentStep === 1 && (
|
||||
<RoadmapBuilder units={roadmapUnits} onUnitsChange={setRoadmapUnits} />
|
||||
<>
|
||||
<RoadmapBuilder units={roadmapUnits} onUnitsChange={setRoadmapUnits} />
|
||||
|
||||
<SectionCard
|
||||
title="Prerequisites"
|
||||
description="What a learner must complete before starting this course."
|
||||
>
|
||||
<CoursePrerequisiteBuilder
|
||||
value={prerequisites}
|
||||
onChange={setPrerequisites}
|
||||
courses={flatCourses}
|
||||
units={flatUnits}
|
||||
lessons={flatLessons}
|
||||
/>
|
||||
</SectionCard>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ── Step 2: Rewards ── */}
|
||||
@@ -587,11 +667,26 @@ export default function AddCourse() {
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="border-t pt-4">
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2">
|
||||
Course Roles
|
||||
</p>
|
||||
{!watchedRoles?.length ? (
|
||||
<p className="text-sm text-muted-foreground">None added.</p>
|
||||
) : (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{watchedRoles.map((r, i) => (
|
||||
<Badge key={i} variant="secondary">{r.text}</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard
|
||||
title="Roadmap"
|
||||
description={`${roadmapUnits.length} unit${roadmapUnits.length === 1 ? "" : "s"} · ${totalLessons} lesson${totalLessons === 1 ? "" : "s"} added.`}
|
||||
description={`${roadmapUnits.length} unit${roadmapUnits.length === 1 ? "" : "s"} · ${totalLessons} lesson${totalLessons === 1 ? "" : "s"} added · ${prerequisites.length} prerequisite${prerequisites.length === 1 ? "" : "s"}.`}
|
||||
>
|
||||
{roadmapUnits.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">No units added.</p>
|
||||
@@ -614,6 +709,21 @@ export default function AddCourse() {
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{prerequisites.length > 0 && (
|
||||
<div className="border-t pt-3 mt-1">
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2">
|
||||
Prerequisites
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{prerequisites.map((p, i) => (
|
||||
<Badge key={i} variant="outline" className="capitalize gap-1">
|
||||
{p.ref_type}: {p.title || `#${p.ref_id}`}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard title="Rewards">
|
||||
|
||||
Reference in New Issue
Block a user