diff --git a/frontend/src/admin/AdminPortal.jsx b/frontend/src/admin/AdminPortal.jsx index 9d286ad..ca4e494 100644 --- a/frontend/src/admin/AdminPortal.jsx +++ b/frontend/src/admin/AdminPortal.jsx @@ -11,6 +11,7 @@ import AssessmentsView from './AssessmentsView' import ClassroomView from './ClassroomView' import ProgramsView from './ProgramsView' import NotificationsView from './NotificationsView' +import CurriculumView from './CurriculumView' import styles from './Admin.module.css' // Instructor/Admin portal — clearly separate from the student experience (/admin route, @@ -24,6 +25,7 @@ const NAV = [ { key: 'cohorts', label: 'Cohorts' }, { key: 'students', label: 'Students' }, { key: 'programs', label: 'Programs' }, + { key: 'curriculum', label: 'Curriculum' }, { key: 'classroom', label: 'Classroom' }, { key: 'learning', label: 'Learning' }, { key: 'assessments', label: 'Assessments' }, @@ -66,6 +68,7 @@ export default function AdminPortal() { {view === 'cohorts' && } {view === 'students' && } {view === 'programs' && } + {view === 'curriculum' && } {view === 'classroom' && } {view === 'learning' && } {view === 'assessments' && } diff --git a/frontend/src/admin/CurriculumView.jsx b/frontend/src/admin/CurriculumView.jsx new file mode 100644 index 0000000..17bba3f --- /dev/null +++ b/frontend/src/admin/CurriculumView.jsx @@ -0,0 +1,148 @@ +import { useState, useEffect, useCallback } from 'react' +import { adminApi } from './api' +import styles from './Admin.module.css' + +const ACTIVITY_LABELS = { + lesson: 'Lesson', demonstration: 'Demonstration', knowledge_check: 'Knowledge Check', + guided_exercise: 'Guided Exercise', lab: 'Lab', reflection: 'Reflection', + assessment: 'Assessment', capstone: 'Capstone', +} + +function statusPill(status) { + const cls = status === 'ready' ? styles.pillOk : status === 'partial' ? styles.pillWarn : styles.pillMuted + return {status} +} + +// Phase 6A §7 — instructor curriculum visibility: program structure with instructor notes, +// mapped scenarios/assessments, and estimated teaching time, plus an optional per-student +// progress lookup so a teacher can see where someone is stuck. Read-only — authoring stays +// Git/migration-based this phase (docs/COURSE_CONTENT_MODEL.md), so there is no edit form here. +export default function CurriculumView() { + const [programs, setPrograms] = useState(null) + const [programId, setProgramId] = useState(null) + const [courses, setCourses] = useState(null) + const [error, setError] = useState(null) + const [studentId, setStudentId] = useState('') + const [studentCourses, setStudentCourses] = useState(null) + const [studentError, setStudentError] = useState(null) + const [openCourse, setOpenCourse] = useState(null) + const [openModule, setOpenModule] = useState(null) + + useEffect(() => { adminApi.programs().then((p) => { setPrograms(p); if (p[0]) setProgramId(p[0].program_id) }).catch((e) => setError(e.message)) }, []) + + const loadCourses = useCallback(() => { + if (!programId) return + adminApi.programCourses(programId).then(setCourses).catch((e) => setError(e.message)) + }, [programId]) + useEffect(loadCourses, [loadCourses]) + + const lookupStudent = async (e) => { + e.preventDefault() + setStudentError(null); setStudentCourses(null) + try { setStudentCourses(await adminApi.studentCourses(studentId.trim(), programId)) } + catch (err) { setStudentError(err.message) } + } + + if (error) return
{error}
+ if (!programs) return

Loading…

+ + const course = openCourse && courses?.find((c) => c.course_id === openCourse) + const mod = course && openModule && course.modules.find((m) => m.module_id === openModule) + const progressFor = (activityId) => studentCourses?.flatMap((c) => c.modules).flatMap((m) => m.activities).find((a) => a.activity_id === activityId) + + return ( + <> +
+

Curriculum

+ + +
+ setStudentId(e.target.value)} /> + +
+ {studentError &&
{studentError}
} +
+ + {!courses ?

Loading…

: mod ? ( +
+ +

{mod.title} {statusPill(mod.status)}

+ {mod.description &&

{mod.description}

} + {mod.estimated_duration_minutes &&

Estimated teaching time: ~{mod.estimated_duration_minutes} min

} + {mod.objectives?.length > 0 &&
    {mod.objectives.map((o, i) =>
  • {o}
  • )}
} + + {studentCourses && } + + {mod.activities.map((a) => ( + + + + + + + {studentCourses && ( + + )} + + ))} + +
#TypeTitleMappedStatusStudent
{a.sequence}{ACTIVITY_LABELS[a.activity_type] || a.activity_type}{a.title}{a.scenario_id || a.assessment_definition_id || '—'}{statusPill(a.status)}{progressFor(a.activity_id)?.complete ? done : —}
+ {mod.activities.some((a) => a.instructor_notes) && ( +
+

Instructor notes

+ {mod.activities.filter((a) => a.instructor_notes).map((a) => ( +
+ {a.title} +
{a.instructor_notes}
+
+ ))} +
+ )} +
+ ) : course ? ( +
+ +

{course.title} {statusPill(course.status)}

+ {course.description &&

{course.description}

} + {course.estimated_duration_hours &&

Estimated: ~{course.estimated_duration_hours} hours

} + + + + {course.modules.length === 0 ? ( + + ) : course.modules.map((m) => ( + + + + + + + + ))} + +
#ModuleActivitiesEst. minStatus
No modules authored yet.
{m.sequence}{m.activities.length}{m.estimated_duration_minutes || '—'}{statusPill(m.status)}
+
+ ) : ( +
+

ROOT_ program structure

+ + + + {courses.map((c) => ( + + + + + + + + ))} + +
#CourseModulesEst. hoursStatus
{c.sequence}{c.modules.length}{c.estimated_duration_hours || '—'}{statusPill(c.status)}
+
+ )} + + ) +} diff --git a/frontend/src/admin/api.js b/frontend/src/admin/api.js index 89af425..8b7e5a0 100644 --- a/frontend/src/admin/api.js +++ b/frontend/src/admin/api.js @@ -134,4 +134,8 @@ export const adminApi = { studentNotifications: (studentId) => get(`/api/admin/students/${studentId}/notifications`), resendActivation: (studentId) => post(`/api/admin/students/${studentId}/resend-activation`), sendEnrollmentConfirmation: (studentId, enrollmentId) => post(`/api/admin/students/${studentId}/send-enrollment-confirmation`, enrollmentId ? { enrollment_id: enrollmentId } : undefined), + + // ── Phase 6A §7: course/module/activity curriculum view ────────────────────────────────── + programCourses: (programId) => get(`/api/admin/programs/${programId}/courses`), + studentCourses: (studentId, programId) => get(`/api/admin/students/${studentId}/courses?program_id=${encodeURIComponent(programId)}`), }