diff --git a/src/pages/content-management/HumanLanguagePage.tsx b/src/pages/content-management/HumanLanguagePage.tsx index 6bcb976..cb3267b 100644 --- a/src/pages/content-management/HumanLanguagePage.tsx +++ b/src/pages/content-management/HumanLanguagePage.tsx @@ -25,6 +25,8 @@ export function HumanLanguagePage() { const [quickSearch, setQuickSearch] = useState("") const [quickCreating, setQuickCreating] = useState(false) const [deletingKey, setDeletingKey] = useState(null) + /** Course IDs whose path body is collapsed (headers stay visible). */ + const [collapsedPathIds, setCollapsedPathIds] = useState([]) const loadHierarchy = async () => { setLoading(true) @@ -62,13 +64,6 @@ export function HumanLanguagePage() { return filteredSubCategories.flatMap((s) => s.courses) }, [filteredSubCategories]) - /** Explicit course pick, or implicit when filters leave exactly one course (e.g. Course still "All" but only one row). */ - const resolvedCourseId = useMemo((): number | null => { - if (selectedCourseId !== "ALL") return selectedCourseId - if (availableCourses.length === 1) return availableCourses[0].course_id - return null - }, [selectedCourseId, availableCourses]) - const selectedCourses = useMemo( () => selectedCourseId === "ALL" @@ -77,15 +72,6 @@ export function HumanLanguagePage() { [availableCourses, selectedCourseId], ) - const levelsForSelectedCourse = useMemo(() => { - if (resolvedCourseId == null) return [] as string[] - const course = availableCourses.find((c) => c.course_id === resolvedCourseId) - if (!course) return [] - return course.levels - .filter((l) => (l.modules?.length ?? 0) > 0) - .map((l) => l.level.toUpperCase()) - }, [availableCourses, resolvedCourseId]) - /** A1 always; A2–C3 only after that level has at least one module (incremental UI). */ const visibleCefrLevels = useMemo(() => { if (availableCourses.length === 0) return [] as CefrLevel[] @@ -115,6 +101,15 @@ export function HumanLanguagePage() { setCollapsedLevels((prev) => (prev.includes(levelKey) ? prev.filter((l) => l !== levelKey) : [...prev, levelKey])) } + const togglePathCollapsed = (courseId: number) => { + setCollapsedPathIds((prev) => + prev.includes(courseId) ? prev.filter((id) => id !== courseId) : [...prev, courseId], + ) + } + + const levelsWithContentForCourse = (course: HumanLanguageCourseTree) => + course.levels.filter((l) => (l.modules?.length ?? 0) > 0).map((l) => l.level.toUpperCase()) + const parseModuleNumber = (title: string): number | null => { const match = title.match(/module-(\d+)/i) if (!match) return null @@ -212,22 +207,23 @@ export function HumanLanguagePage() { } } - const handleCreateNextLevel = async () => { - if (resolvedCourseId == null) { - toast.error("Select a specific course first (or narrow subcategory/course filters to a single course).") + const handleCreateNextLevelForCourse = async (courseId: number) => { + const course = availableCourses.find((c) => c.course_id === courseId) + if (!course) { + toast.error("Course not found") return } - const existing = new Set(levelsForSelectedCourse) + const existing = new Set(levelsWithContentForCourse(course)) const next = CEFR_LEVELS.find((level) => !existing.has(level)) if (!next) { - toast.error("All CEFR levels are already created") + toast.error("All CEFR levels (A1–C3) already have content for this path") return } - const key = `next-level-${resolvedCourseId}-${next}` + const key = `next-level-${courseId}-${next}` setCreatingKey(key) try { await createHumanLanguageLesson({ - course_id: resolvedCourseId, + course_id: courseId, cefr_level: next, title: "Module-1", description: `${next} Module-1`, @@ -347,35 +343,8 @@ export function HumanLanguagePage() { - -
-

- {resolvedCourseId == null - ? "Choose one course in the Course dropdown, or filter until only one course is listed—then you can add the next CEFR level (A1 first) and use remove actions." - : levelsForSelectedCourse.length >= CEFR_LEVELS.length - ? "All CEFR levels (A1–C3) already have content for this course." - : `Next level to add: ${CEFR_LEVELS.find((l) => !levelsForSelectedCourse.includes(l)) ?? "—"}`} -

- -
-
- {categoryId && resolvedCourseId != null ? ( -
- - - -
- ) : null} - {loading ? (
@@ -436,11 +405,54 @@ export function HumanLanguagePage() { return (node?.modules?.length ?? 0) > 0 }).filter((level) => selectedLevel === "ALL" || selectedLevel === level) + const pathCollapsed = collapsedPathIds.includes(course.course_id) + const levelsDone = levelsWithContentForCourse(course) + const nextCefrForPath = CEFR_LEVELS.find((l) => !levelsDone.includes(l)) + const pathNextLevelLoading = creatingKey?.startsWith(`next-level-${course.course_id}-`) ?? false + const pathLevelsFull = levelsDone.length >= CEFR_LEVELS.length + return ( -
-

{course.course_name}

+
+ +
+ {categoryId ? ( + + + + ) : null} + +
+ {!pathCollapsed ? ( {courseLevels.length === 0 ? (

No levels match the current level filter.

@@ -596,6 +608,7 @@ export function HumanLanguagePage() { }) )}
+ ) : null} ) })