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:
@@ -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} />}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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)}`),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user