feat(instructor): add curriculum teaching view

CurriculumView.jsx — a new 'Curriculum' tab in the admin portal:
program structure (courses -> modules -> activities), mapped scenarios/
assessments, estimated teaching time, and instructor notes rendered per
activity. A student_id lookup shows one student's real per-activity
progress alongside the same structure, for spotting where someone is
stuck. Read-only, no WYSIWYG editor — course/module authoring stays
Git/migration-based this phase per docs/COURSE_CONTENT_MODEL.md.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
2026-08-25 23:29:50 -05:00
co-authored by Claude Sonnet 5
parent 8c09ef8f3e
commit db927b8081
3 changed files with 155 additions and 0 deletions
+3
View File
@@ -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' && <CohortsView role={instructor.role} />}
{view === 'students' && <StudentsView role={instructor.role} />}
{view === 'programs' && <ProgramsView role={instructor.role} />}
{view === 'curriculum' && <CurriculumView />}
{view === 'classroom' && <ClassroomView />}
{view === 'learning' && <LearningView />}
{view === 'assessments' && <AssessmentsView role={instructor.role} />}
+148
View File
@@ -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 <span className={`${styles.pill} ${cls}`}>{status}</span>
}
// 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 <div className={styles.errorBox}>{error}</div>
if (!programs) return <p className={styles.muted}>Loading…</p>
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 (
<>
<div className={styles.card}>
<h2>Curriculum</h2>
<select className={styles.input} value={programId || ''} onChange={(e) => { setProgramId(e.target.value); setOpenCourse(null); setOpenModule(null) }}>
{programs.map((p) => <option key={p.program_id} value={p.program_id}>{p.name}</option>)}
</select>
<form onSubmit={lookupStudent} style={{ marginTop: '1rem', display: 'flex', gap: '0.5rem' }}>
<input className={styles.input} placeholder="student_id — check where they're stuck" value={studentId} onChange={(e) => setStudentId(e.target.value)} />
<button className={styles.btnSecondary} disabled={!studentId.trim()}>Look up progress</button>
</form>
{studentError && <div className={styles.errorBox} style={{ marginTop: '0.5rem' }}>{studentError}</div>}
</div>
{!courses ? <p className={styles.muted}>Loading…</p> : mod ? (
<div className={styles.card}>
<button className={styles.linkBtn} onClick={() => setOpenModule(null)}>← {course.title}</button>
<h3 style={{ marginTop: '0.5rem' }}>{mod.title} {statusPill(mod.status)}</h3>
{mod.description && <p className={styles.muted}>{mod.description}</p>}
{mod.estimated_duration_minutes && <p className={styles.muted}>Estimated teaching time: ~{mod.estimated_duration_minutes} min</p>}
{mod.objectives?.length > 0 && <ul>{mod.objectives.map((o, i) => <li key={i}>{o}</li>)}</ul>}
<table>
<thead><tr><th>#</th><th>Type</th><th>Title</th><th>Mapped</th><th>Status</th>{studentCourses && <th>Student</th>}</tr></thead>
<tbody>
{mod.activities.map((a) => (
<tr key={a.activity_id}>
<td className={styles.muted}>{a.sequence}</td>
<td>{ACTIVITY_LABELS[a.activity_type] || a.activity_type}</td>
<td>{a.title}</td>
<td className={styles.muted}>{a.scenario_id || a.assessment_definition_id || '—'}</td>
<td>{statusPill(a.status)}</td>
{studentCourses && (
<td>{progressFor(a.activity_id)?.complete ? <span className={`${styles.pill} ${styles.pillOk}`}>done</span> : <span className={styles.muted}>—</span>}</td>
)}
</tr>
))}
</tbody>
</table>
{mod.activities.some((a) => a.instructor_notes) && (
<div style={{ marginTop: '1rem' }}>
<h3>Instructor notes</h3>
{mod.activities.filter((a) => a.instructor_notes).map((a) => (
<details key={a.activity_id} style={{ marginBottom: '0.5rem' }}>
<summary style={{ cursor: 'pointer' }}>{a.title}</summary>
<pre style={{ whiteSpace: 'pre-wrap', fontSize: '0.82rem', color: 'var(--text-2)' }}>{a.instructor_notes}</pre>
</details>
))}
</div>
)}
</div>
) : course ? (
<div className={styles.card}>
<button className={styles.linkBtn} onClick={() => setOpenCourse(null)}>← All courses</button>
<h3 style={{ marginTop: '0.5rem' }}>{course.title} {statusPill(course.status)}</h3>
{course.description && <p className={styles.muted}>{course.description}</p>}
{course.estimated_duration_hours && <p className={styles.muted}>Estimated: ~{course.estimated_duration_hours} hours</p>}
<table>
<thead><tr><th>#</th><th>Module</th><th>Activities</th><th>Est. min</th><th>Status</th></tr></thead>
<tbody>
{course.modules.length === 0 ? (
<tr><td colSpan={5} className={styles.muted}>No modules authored yet.</td></tr>
) : course.modules.map((m) => (
<tr key={m.module_id}>
<td className={styles.muted}>{m.sequence}</td>
<td><button className={styles.linkBtn} onClick={() => setOpenModule(m.module_id)}>{m.title}</button></td>
<td className={styles.muted}>{m.activities.length}</td>
<td className={styles.muted}>{m.estimated_duration_minutes || '—'}</td>
<td>{statusPill(m.status)}</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className={styles.card}>
<h3>ROOT_ program structure</h3>
<table>
<thead><tr><th>#</th><th>Course</th><th>Modules</th><th>Est. hours</th><th>Status</th></tr></thead>
<tbody>
{courses.map((c) => (
<tr key={c.course_id}>
<td className={styles.muted}>{c.sequence}</td>
<td><button className={styles.linkBtn} onClick={() => setOpenCourse(c.course_id)}>{c.title}</button></td>
<td className={styles.muted}>{c.modules.length}</td>
<td className={styles.muted}>{c.estimated_duration_hours || '—'}</td>
<td>{statusPill(c.status)}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</>
)
}
+4
View File
@@ -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)}`),
}