diff --git a/docs/ux-overhaul/production-route-migration.md b/docs/ux-overhaul/production-route-migration.md
index 3d1e75b..ecd3a79 100644
--- a/docs/ux-overhaul/production-route-migration.md
+++ b/docs/ux-overhaul/production-route-migration.md
@@ -1,10 +1,17 @@
# Production Route Migration
-Phase 1 preserves route compatibility and establishes canonical role destinations without deleting deep links. Phase 2 begins the student `/learn` migration while keeping legacy student deep links available.
+Phase 1 preserves route compatibility and establishes canonical role destinations without deleting deep links. Phase 2 begins the student `/learn` migration while keeping legacy student deep links available. Phase 3 adds canonical teacher `/teach` routes while preserving `/home` and workspace cohort deep links.
| Current route | Canonical route | Role | Redirect behavior | Guard behavior | Migration phase | Removal criteria | Deep-link risk |
| --- | --- | --- | --- | --- | --- | --- | --- |
-| `/home` | `/home` for teacher/admin, transitional for other roles | teacher, instructor, admin, super_admin-compatible | No redirect | `HomeSessionGuard`; child guards unchanged | Phase 1 | Only after all role dashboards have route-level replacements and analytics show low usage | High |
+| `/home` | `/teach` for teacher/instructor; `/home` remains admin transitional | teacher, instructor, admin, super_admin-compatible | No redirect | `HomeSessionGuard`; child guards unchanged | Phase 3 teacher alias added | Remove teacher reliance only after `/teach` smoke tests and usage validation | High |
+| `/teach` | `/teach` | teacher, instructor | No redirect; role landing now points here | `HomeSessionGuard`; child guards unchanged | Phase 3 | None planned | Low |
+| `/teach/classes` | `/teach/classes` | teacher, instructor, org_admin | No redirect | `HomeSessionGuard` plus `RoleGuard` section roles | Phase 3 | None planned | Low |
+| `/teach/classes/:id` | `/teach/classes/:id` | teacher, instructor, org_admin | No redirect | `HomeSessionGuard` plus `RoleGuard` section roles; frontend verifies route section is in authorized section list | Phase 3 | None planned | Low |
+| `/teach/curriculum` | `/teach/curriculum` | teacher, instructor | No redirect | `HomeSessionGuard` plus `RoleGuard` teacher/instructor | Phase 3 | None planned | Low |
+| `/teach/courses/:courseId/preview` | `/teach/courses/:courseId/preview` | teacher, instructor | No redirect | `HomeSessionGuard` plus `RoleGuard` teacher/instructor; preview uses course load only | Phase 3 | None planned | Low |
+| `/teach/assignments` | `/teach/classes` class-first assignment entry | teacher, instructor, org_admin | No redirect; currently renders class list to choose assignment context | `HomeSessionGuard` plus `RoleGuard` section roles | Phase 3 | Replace only if global assignment API is added | Low |
+| `/teach/learners/:learnerId` | `/teach/learners/:learnerId?sectionId=` | teacher, instructor, org_admin | No redirect | `HomeSessionGuard` plus `RoleGuard`; frontend requires section context and roster membership | Phase 3 | None planned | Low |
| `/learn` | `/learn` | student | Login routes students here after ready bootstrap | `HomeSessionGuard`; learner child routes unchanged | Phase 1 | None planned | Low |
| `/learn/products` | `/learn/products` visible as Courses, with course path links to `/learn/courses/:courseId` | student | No redirect | `HomeSessionGuard`; existing learner-product API still used | Phase 2 | Keep as transitional browse route until learner-product and course catalog surfaces are unified | Low |
| `/learn/courses/:courseId` | `/learn/courses/:courseId` | student | New canonical course overview; no redirect | `HomeSessionGuard`; course access remains enforced by backend endpoints and governed start behavior | Phase 2 | None planned; direct route should remain | Low |
@@ -18,7 +25,8 @@ Phase 1 preserves route compatibility and establishes canonical role destination
| `/home/admin/users` | `/home/admin/users` | admin | No redirect | Existing `RoleGuard` admin-only | Phase 1 | Dedicated Admin IA route exists and old path is aliased | High |
| `/home/admin/courses` | `/home/admin/courses` | admin | No redirect | Existing `RoleGuard` admin-only | Phase 1 | Dedicated Admin IA route exists and old path is aliased | High |
| `/home/admin/badges` | `/home/admin/badges` | admin | No redirect | Existing `RoleGuard` admin-only | Phase 1 | Dedicated Admin IA route exists and old path is aliased | High |
-| `/home/sections` | `/home/sections` | teacher, instructor, org_admin | No redirect | Existing `RoleGuard` section roles | Phase 1 | Class/cohort IA aliases are implemented and smoke tested | High |
+| `/home/sections` | `/teach/classes` | teacher, instructor, org_admin | No redirect | Existing `RoleGuard` section roles | Phase 3 alias added | Remove only after teacher bookmarks and smoke tests move to `/teach/classes` | High |
+| `/home/sections/:id` | `/teach/classes/:id` | teacher, instructor, org_admin | No redirect | Existing `RoleGuard` section roles | Phase 3 alias added | Remove only after class detail deep links have compatibility redirects | High |
| `/home/courses` | `/home/courses` | teacher, instructor, student transitional | No redirect | Existing child route, no added guard | Phase 1 | Course library split is implemented and tested | Medium |
| `/home/programs` | `/home/programs` | teacher, instructor, student transitional | No redirect | Existing child route, no added guard | Phase 1 | Learning path route aliases are implemented and tested | Medium |
| `/home/certifications` | `/learn/certificates` for student-origin actions | student transitional | No redirect | Existing child route, no added guard | Phase 2 partial | Existing links aliased and achievements page redesigned | Medium |
@@ -28,8 +36,8 @@ Phase 1 preserves route compatibility and establishes canonical role destination
## Canonical Login Destinations
- `student`: `/learn`
-- `teacher`: `/home`
-- `instructor`: `/home`
+- `teacher`: `/teach`
+- `instructor`: `/teach`
- `content_admin`: `/workspace`
- `org_admin`: `/workspace/learners`
- `admin`: `/home`
@@ -43,3 +51,5 @@ Phase 1 preserves route compatibility and establishes canonical role destination
- Phase 2 updates student-origin actions from the legacy student dashboard to navigate into `/learn/**` while preserving `/home/**` deep links.
- Phase 2 adds `/learn/courses/:courseId` as the canonical student course overview and keeps `/learn/products` as a transitional browse route.
- No dedicated unit route was added because existing APIs do not expose unit-level student progress or direct-link authorization.
+- Phase 3 adds canonical Teach routes without deleting `/home/sections`, `/home/sections/:id`, or `/workspace/learners/cohorts` compatibility routes.
+- Teacher course preview intentionally avoids `/api/start-course`; it uses `/api/courses/{id}` only.
diff --git a/docs/ux-overhaul/shared-component-status.md b/docs/ux-overhaul/shared-component-status.md
index ac85d09..a7dea39 100644
--- a/docs/ux-overhaul/shared-component-status.md
+++ b/docs/ux-overhaul/shared-component-status.md
@@ -23,6 +23,17 @@
| State panel | `frontend/src/app/components/echo-state-panel/` | Production active | Adds `permission` and `success` variants and real icon rendering. |
| Access denied | `frontend/src/app/pages/access-denied/` | Migrated | Uses shared permission state. |
| Icon registry | `frontend/src/app/shared/icon/icon.module.ts` | Extended | Adds `Lock` and `CheckCircle` paths. |
+| Student curriculum | `frontend/src/app/components/student-curriculum/student-curriculum.component.ts` | Reused in teacher preview | Teacher course preview renders the same unit/lesson hierarchy in preview-only mode without starting learner progress. |
+| Confirmation dialog | `frontend/src/app/components/echo-confirmation-dialog/` | Connected to teacher assignment creation | Class assignments require review confirmation before calling the existing section assignment API. |
+| State panel/loading state | `frontend/src/app/components/echo-state-panel/`, `frontend/src/app/components/echo-loading-state/` | Connected to Teach routes | Teacher overview, classes, class detail, curriculum, course preview, and learner detail use shared loading/error/empty/permission states. |
+
+## Added In Teacher Phase
+
+| Component/screen | Path | Status | Notes |
+| --- | --- | --- | --- |
+| Teacher curriculum page | `frontend/src/app/pages/teacher-curriculum/teacher-curriculum.component.ts` | Production active | Course search and preview/assign entry over `/api/courses`. |
+| Teacher course preview | `frontend/src/app/pages/teacher-curriculum/teacher-course-preview.component.ts` | Production active | Preview-only course hierarchy over `/api/courses/{id}`. |
+| Teacher learner detail | `frontend/src/app/pages/teacher-learner-detail/teacher-learner-detail.component.ts` | Production active | Requires class `sectionId` context and roster verification. |
## Still Legacy
@@ -31,6 +42,8 @@
- Destructive actions still need page-by-page adoption of `EchoConfirmationDialogComponent`.
- Form controls, tables, filter controls, page-specific buttons, dashboard cards, and internal page layouts are still mixed Tailwind/SCSS implementations.
- Activity controls delegated to canvas, map, and storybook components still need full accessibility and responsive QA.
+- Global teacher assignment list is still a class-first alias because no global assignment API was verified.
+- Teacher feedback panels remain deferred because no feedback persistence endpoint was verified.
## Required Future Adoption
diff --git a/docs/ux-overhaul/teacher-accessibility-review.md b/docs/ux-overhaul/teacher-accessibility-review.md
new file mode 100644
index 0000000..42022a8
--- /dev/null
+++ b/docs/ux-overhaul/teacher-accessibility-review.md
@@ -0,0 +1,21 @@
+# Teacher Accessibility Review
+
+Date: 2026-07-12
+
+## Addressed
+
+- One visible `h1` per new teacher screen.
+- Logical heading hierarchy for overview, class, curriculum, preview, and learner detail.
+- Keyboard-accessible links, buttons, forms, and tab controls.
+- Visible focus styles on custom teacher controls.
+- Loading states use shared status semantics.
+- Error and permission states use shared state panels.
+- Assignment confirmation uses shared dialog with focus management.
+- Required assignment/class fields have visible labels and inline errors.
+- Progress values include text, not color-only bars.
+- No essential row action is hover-only.
+- Course preview clearly announces that no progress is recorded.
+
+## Remaining Manual QA
+
+Manual screen-reader and keyboard traversal should be performed in a running browser at 1440, 1280, 768, and 390 px once local frontend/backend infrastructure is available.
diff --git a/docs/ux-overhaul/teacher-assignment-data-contract.md b/docs/ux-overhaul/teacher-assignment-data-contract.md
new file mode 100644
index 0000000..87a1f5b
--- /dev/null
+++ b/docs/ux-overhaul/teacher-assignment-data-contract.md
@@ -0,0 +1,35 @@
+# Teacher Assignment Data Contract
+
+Date: 2026-07-12
+
+## Existing Assignment Model
+
+| Field | Source | Supported in UI | Notes |
+| --- | --- | --- | --- |
+| Assignment ID | `AssignmentResponse.id` | Display/list only | UUID, not exposed as primary UI label. |
+| Section/class target | URL section ID and `section_id` | Yes | Class-first workflow. |
+| Target type | `target_type` | Yes | UI supports `course`, `unit`, and `lesson`; backend enum must accept the chosen value. |
+| Target ID | `target_id` | Yes | Course picker is available for `course`; unit/lesson still require IDs because no picker endpoint was verified. |
+| Due date | `due_at` | Yes | Uses `datetime-local`; backend receives ISO-like browser value through Angular binding. |
+| Instructions | `instructions` | Yes | Preserved after recoverable failures. |
+| Created by | `created_by` | Not shown | Internal teacher identifier not exposed. |
+| Created at | `created_at` | Available | Used for ordering only when needed. |
+| Assigned learners | Not in current assignment create response | No | Class assignment endpoint does not support individual learner selection. |
+| Status/publishing state | Not exposed | No | UI does not invent draft/published states. |
+| Completion/review state | Submission model exists separately | Limited | No teacher-facing per-learner completion endpoint verified. |
+| Retry/reassignment | Not exposed | No | Not implemented. |
+| Edit/delete/archive | No endpoint verified | No | UI does not show destructive assignment actions. |
+| Notifications | No endpoint verified | No | UI does not claim notifications are sent. |
+
+## Creation Behavior
+
+The class detail assignment tab uses:
+
+- Required target validation.
+- Optional due date and instructions.
+- Confirmation dialog before assignment creation.
+- Duplicate-submit prevention while saving.
+- Persistent inline error if creation fails.
+- Success toast only after API confirmation.
+
+Known limitation: the API creates section-wide assignments only; the UI states that individual learner selection is not supported by the current endpoint.
diff --git a/docs/ux-overhaul/teacher-privacy-review.md b/docs/ux-overhaul/teacher-privacy-review.md
new file mode 100644
index 0000000..d9c1b09
--- /dev/null
+++ b/docs/ux-overhaul/teacher-privacy-review.md
@@ -0,0 +1,18 @@
+# Teacher Privacy Review
+
+Date: 2026-07-12
+
+## Decisions
+
+- Canonical learner detail requires `sectionId` and verifies the learner appears in that section roster before showing profile context.
+- Roster rows show learner name only when the authorized student list includes that profile.
+- Emails, internal organization IDs, raw learner IDs, demographic data, and global learner search are not shown in teacher UI.
+- Deep-link learner failures use neutral permission language and do not reveal whether a learner exists outside the teacher's authorized class context.
+- Course preview does not call `startCourse()` and does not create teacher enrollment, learner enrollment, lesson sessions, or progress records.
+- Unsupported discussion moderation is not exposed because thread/post APIs are not class-scoped or permission-scoped.
+
+## Remaining Backend Privacy Gaps
+
+- Section roster/enrollment/assignment APIs should verify the target section belongs to the active organization and the educator's authorized scope.
+- Assignment listing should require an educator or authorized learner dependency.
+- Thread/post APIs should require authentication and class/community scope before Teach moderation is exposed.
diff --git a/docs/ux-overhaul/teacher-production-audit.md b/docs/ux-overhaul/teacher-production-audit.md
new file mode 100644
index 0000000..7208de0
--- /dev/null
+++ b/docs/ux-overhaul/teacher-production-audit.md
@@ -0,0 +1,37 @@
+# Teacher Production Audit
+
+Date: 2026-07-12
+
+## Summary
+
+The audited teacher/instructor production surface uses the existing FastAPI auth, organization-role, section, assignment, course, learner-progress, assessment, and discussion APIs. No backend files or database schema changes were required for this phase.
+
+## Routes Audited
+
+| Route | User goal | Role access | API dependencies | Current UX problems found | Privacy/accessibility/responsive risks | Migration recommendation |
+| --- | --- | --- | --- | --- | --- | --- |
+| `/home` | Teacher landing through role selector | `teacher`; now `instructor` compatible in role selector | `/api/courses`, `/api/users/students`, `/api/analytics/teacher-summary`, governance summary | Old dashboard mixed demo/governance/course-authoring language with teacher work | Progress rows are not class-scoped; dense legacy tables; demo copy | Canonical `/teach`; keep `/home` legacy |
+| `/teach` | Teacher Teach overview | `teacher`, `instructor` through shell/bootstrap | Same as `/home` teacher view | New canonical route added | Must not imply unsupported review/feedback counts | Canonical Phase 3 route |
+| `/home/sections` | List/create sections | `teacher`, `instructor`, `org_admin` | `/api/sections`, `/api/sections/{id}/analytics/summary` | Thin CRUD, exposed internal course version ID without explanation | No loading/error states; no responsive data-list | Keep as legacy alias; canonical `/teach/classes` |
+| `/teach/classes` | List/create classes | `teacher`, `instructor`, `org_admin` | Same as `/home/sections` | New production class list | Course version ID is still technical due to backend contract | Canonical Phase 3 route |
+| `/home/sections/:id` | Manage section assignments | `teacher`, `instructor`, `org_admin` | `/api/sections`, `/api/sections/{id}/roster`, `/api/sections/{id}/assignments`, `/api/sections/{id}/analytics/summary`, `/api/users/students`, `/api/courses` | Old page only created assignment by raw target ID | Roster endpoint returns no learner profile fields; section ownership check is incomplete in backend | Keep as legacy alias; canonical `/teach/classes/:id` |
+| `/teach/classes/:id` | Class overview, roster, assignments, progress, discussion note | `teacher`, `instructor`, `org_admin` | Same as section detail | New production class detail | Frontend verifies selected section is in authorized section list; backend remains authoritative | Canonical Phase 3 route |
+| `/teach/curriculum` | Browse curriculum for assignment | `teacher`, `instructor` | `/api/courses` | New route | Client-side search only; no standards/duration invented | Canonical Phase 3 route |
+| `/teach/courses/:courseId/preview` | Preview learner course hierarchy | `teacher`, `instructor` | `/api/courses/{id}` | New route | Does not call `/api/start-course`; no progress is recorded | Canonical Phase 3 route |
+| `/teach/assignments` | Assignment entry point | `teacher`, `instructor`, `org_admin` | `/api/sections`, section assignment APIs after class selection | New alias to class list | Global assignment list endpoint does not exist | Keep as class-first assignment entry |
+| `/teach/learners/:learnerId?sectionId=` | Learner detail with class context | `teacher`, `instructor`, `org_admin` | `/api/sections`, `/api/sections/{id}/roster`, `/api/users/students` | New route | Requires section context and roster membership; no global learner profile exposure | Canonical Phase 3 scoped learner detail |
+| `/workspace/learners/cohorts` | Legacy class alias under workspace | creator roles; cohort child uses section roles | `/api/sections` | Commercial/workspace mental model for teachers | Route name does not match teacher task | Preserve legacy alias, do not promote |
+| `/home/courses` | Legacy mixed course catalog | unguarded child under authenticated home | student/teacher transitional | Student and teacher goals mixed | Possible wrong mental model | Teacher navigation now uses `/teach/curriculum` |
+| `/home/assessments/:id` | Assessment detail/submission | authenticated child; submit requires student | `/api/assessments/{id}`, `/api/assessments/{id}/attempts` | Student-oriented submit flow | Teachers can view assessment questions but no review/feedback workflow | Use as assessment preview only; no teacher grading |
+| `/workspace/review-center` | Content review center | creator roles | V2 review center APIs | Content/studio review, not student work review | Could confuse teacher feedback | Not exposed in Teach nav |
+| `/threads`, `/posts` APIs | Discussion primitives | No route guards in audited backend modules | `/api/threads`, `/api/posts` | No class scope or moderation permission model | High privacy risk if exposed as teacher moderation | Document gap; do not expose class feed |
+
+## Backend Authorization Risks Found
+
+- `GET /api/sections/{id}/roster` requires a teacher/org/instructor org role, but does not verify the section belongs to that organization.
+- `POST /api/sections/{id}/enrollments` has the same section-scope risk.
+- `GET /api/sections/{id}/assignments` currently has no explicit role dependency.
+- `GET /api/analytics/teacher-summary` supports `admin` and `teacher`, but not `instructor`.
+- Thread/post endpoints are not authenticated or class-scoped in the audited files.
+
+Frontend work keeps navigation and views scoped, but these backend risks remain documented gaps.
diff --git a/docs/ux-overhaul/teacher-production-implementation.md b/docs/ux-overhaul/teacher-production-implementation.md
new file mode 100644
index 0000000..53258d0
--- /dev/null
+++ b/docs/ux-overhaul/teacher-production-implementation.md
@@ -0,0 +1,54 @@
+# Teacher Production Implementation
+
+Date: 2026-07-12
+
+## Implemented
+
+- Canonical Teach route family:
+ - `/teach`
+ - `/teach/classes`
+ - `/teach/classes/:id`
+ - `/teach/curriculum`
+ - `/teach/courses/:courseId/preview`
+ - `/teach/assignments`
+ - `/teach/learners/:learnerId`
+- Teacher/instructor navigation now points to Teach routes.
+- Legacy `/home/sections`, `/home/sections/:id`, and `/workspace/learners/cohorts` routes remain available.
+- Teacher home reframed around today, classes, learner progress, curriculum preview, and honest review/discussion limitations.
+- Classes list adds loading, empty, error, search, responsive cards, and explicit course-version contract copy.
+- Class detail adds overview, roster, assignments, progress, and discussion tabs over existing APIs.
+- Assignment creation uses confirmation dialog and preserves values on failure.
+- Curriculum browse and course preview use existing course APIs.
+- Learner detail is scoped to class roster context.
+
+## Backend Changes
+
+None.
+
+## APIs Used
+
+- `/api/courses`
+- `/api/courses/{id}`
+- `/api/users/students`
+- `/api/analytics/teacher-summary`
+- `/api/analytics/educator-runtime-support`
+- `/api/sections`
+- `/api/sections/{id}/roster`
+- `/api/sections/{id}/assignments`
+- `/api/sections/{id}/analytics/summary`
+
+## Known Limitations
+
+- Instructor-compatible UI exists, but some analytics endpoints still require backend support for `instructor`.
+- Per-learner assignment state, feedback history, manual review, and return-work workflows are not verified.
+- Unit/lesson assignment target pickers need richer APIs before they can avoid raw IDs.
+- Discussion moderation remains deferred until thread/post APIs are scoped and authorized.
+
+## Verification
+
+- App TypeScript compile: `npx tsc -p tsconfig.app.json --noEmit` passed.
+- Production build: `npm run build` passed with existing Sass deprecation warnings in global typography.
+- Frontend unit tests: `npx ng test --watch=false --browsers=ChromeHeadless` passed with `233 SUCCESS`.
+- Playwright discovery: `npx playwright test --list` passed; 1 demo smoke test discovered.
+- Strict OpenSpec validation: `openspec validate overhaul-role-based-ui-ux-experience --strict` passed.
+- Browser execution remains blocked because local TCP checks failed for frontend `127.0.0.1:4200`, backend `127.0.0.1:8000`, and PostgreSQL `127.0.0.1:5432`.
diff --git a/docs/ux-overhaul/teacher-responsive-review.md b/docs/ux-overhaul/teacher-responsive-review.md
new file mode 100644
index 0000000..bf6df62
--- /dev/null
+++ b/docs/ux-overhaul/teacher-responsive-review.md
@@ -0,0 +1,21 @@
+# Teacher Responsive Review
+
+Date: 2026-07-12
+
+## Implemented Responsive Patterns
+
+- Teach overview stacks priorities, data lists, and next actions at tablet/mobile widths.
+- Classes list uses responsive cards rather than a desktop table.
+- Class detail tabs are horizontally scrollable and class data rows collapse to single-column cards.
+- Assignment form collapses to a single column on narrow screens.
+- Curriculum cards move from two columns to one column.
+- Course preview changes from two-column header to one column.
+- Learner detail metrics collapse to one column.
+
+## Static Checks
+
+CSS uses constrained grids, wrapping actions, 44px minimum control height, and no hover-only action affordances.
+
+## Remaining Browser QA
+
+Seeded browser execution still depends on local frontend/backend/database availability. Visual verification at 1440, 1280, 768, and 390 px remains a final manual/browser task if infrastructure is unavailable.
diff --git a/docs/ux-overhaul/teacher-review-behavior.md b/docs/ux-overhaul/teacher-review-behavior.md
new file mode 100644
index 0000000..40bfdc2
--- /dev/null
+++ b/docs/ux-overhaul/teacher-review-behavior.md
@@ -0,0 +1,22 @@
+# Teacher Review Behavior
+
+Date: 2026-07-12
+
+## Assessment and Work Review Findings
+
+| Behavior | Verified support | Evidence |
+| --- | --- | --- |
+| Teachers can load assessment details | Yes | `GET /api/assessments/{id}` includes questions for staff users. |
+| Students can submit assessment attempts | Yes | `POST /api/assessments/{id}/attempts` requires `student`. |
+| Auto-scoring | Yes | Assessment submit endpoint scores answers and records events. |
+| Teachers can list work needing review | Not verified | No teacher review queue endpoint found for student assignment submissions. |
+| Teachers can manually score | Not verified | No score mutation endpoint for teachers found. |
+| Teachers can comment/save feedback | Not verified | No feedback persistence endpoint found. |
+| Teachers can publish/return feedback | Not verified | No return-work endpoint found. |
+| Retries can be granted | Not verified | Assessment max attempts exist, but no teacher retry grant endpoint found. |
+| Correct answers visible to staff | Partial | Staff assessment serialization includes questions; answer visibility is model-dependent. |
+| Rubric support | Not verified | No rubric endpoint surfaced. |
+
+## UI Decision
+
+Teach surfaces describe work review and feedback as unavailable unless the current API confirms a supported action. No toast-only confirmation, manual grading state, returned-work state, or retry state was invented.
diff --git a/docs/ux-overhaul/teacher-role-capability-map.md b/docs/ux-overhaul/teacher-role-capability-map.md
new file mode 100644
index 0000000..772ff7f
--- /dev/null
+++ b/docs/ux-overhaul/teacher-role-capability-map.md
@@ -0,0 +1,18 @@
+# Teacher Role Capability Map
+
+Date: 2026-07-12
+
+| Capability | Teacher | Instructor | Org admin in class routes | Notes |
+| --- | --- | --- | --- | --- |
+| Teach overview | Yes | Yes | No primary nav | Uses existing teacher dashboard component. Instructor compatibility is UI-level; some analytics endpoints still require backend expansion. |
+| Classes list | Yes | Yes | Yes | Uses `/api/sections`. |
+| Create class | Yes | Yes | Yes | Requires current course version ID contract. |
+| Class roster | Yes | Yes | Yes | Uses `/api/sections/{id}/roster`; learner names are matched from `/api/users/students` where available. |
+| Create class assignment | Yes | Yes | Yes | Uses `/api/sections/{id}/assignments`; no individual learner targeting in this endpoint. |
+| Curriculum browse | Yes | Yes | No | Uses `/api/courses`. |
+| Course preview | Yes | Yes | No | Uses `/api/courses/{id}` only; does not create learner progress. |
+| Learner detail | Yes, class scoped | Yes, class scoped | Yes, class scoped | Requires `sectionId` and roster membership. |
+| Progress monitoring | Partial | Partial | Partial | Section summary and teacher summary are supported; class-scoped learner detail is not fully supported. |
+| Assessment/work review | Read-only/limited | Read-only/limited | Read-only/limited | Auto-scored attempts exist; manual grading/feedback persistence is not verified. |
+| Discussion moderation | Deferred | Deferred | Deferred | Threads/posts lack verified class scope and moderation permissions. |
+| Studio/admin links | No | No | Org admin may have organization/studio permissions separately | Teach nav does not expose Studio/Admin for teacher/instructor roles. |
diff --git a/frontend/src/app/app.routes.spec.ts b/frontend/src/app/app.routes.spec.ts
index c520a07..1b28b3d 100644
--- a/frontend/src/app/app.routes.spec.ts
+++ b/frontend/src/app/app.routes.spec.ts
@@ -10,6 +10,11 @@ import { DemoReadinessComponent } from './pages/demo-readiness/demo-readiness.co
import { PublicProductsComponent } from './pages/public-products/public-products.component';
import { PublicProductDetailComponent } from './pages/public-products/public-product-detail.component';
import { RoleGuard } from './guards/role.guard';
+import { SectionsComponent } from './pages/sections/sections.component';
+import { SectionDetailComponent } from './pages/section-detail/section-detail.component';
+import { TeacherCurriculumComponent } from './pages/teacher-curriculum/teacher-curriculum.component';
+import { TeacherCoursePreviewComponent } from './pages/teacher-curriculum/teacher-course-preview.component';
+import { TeacherLearnerDetailComponent } from './pages/teacher-learner-detail/teacher-learner-detail.component';
function findRoute(routeList: Routes, path: string): Route | undefined {
const exactRoute = routeList.find(candidate => candidate.path === path);
@@ -90,6 +95,31 @@ describe('app routes', () => {
].forEach(path => expect(findRoute(routes, path)).withContext(path).toBeTruthy());
});
+ it('adds canonical Teach aliases while preserving legacy teacher routes', () => {
+ [
+ 'teach',
+ 'teach/',
+ 'teach/classes',
+ 'teach/classes/:id',
+ 'teach/curriculum',
+ 'teach/courses/:courseId/preview',
+ 'teach/assignments',
+ 'teach/learners/:learnerId',
+ 'home/sections',
+ 'home/sections/:id',
+ 'workspace/learners/cohorts',
+ 'workspace/learners/cohorts/:id',
+ ].forEach(path => expect(findRoute(routes, path)).withContext(path).toBeTruthy());
+
+ expect(findRoute(routes, 'teach/classes')?.component).toBe(SectionsComponent);
+ expect(findRoute(routes, 'teach/classes/:id')?.component).toBe(SectionDetailComponent);
+ expect(findRoute(routes, 'teach/curriculum')?.component).toBe(TeacherCurriculumComponent);
+ expect(findRoute(routes, 'teach/courses/:courseId/preview')?.component).toBe(TeacherCoursePreviewComponent);
+ expect(findRoute(routes, 'teach/learners/:learnerId')?.component).toBe(TeacherLearnerDetailComponent);
+ expect(findRoute(routes, 'teach/classes')?.canActivate).toContain(RoleGuard);
+ expect(findRoute(routes, 'teach/curriculum')?.canActivate).toContain(RoleGuard);
+ });
+
it('maps Learner Portal V2 routes to V2 pages while reusing lesson runtime', () => {
const learnRoute = findRoute(routes, 'learn');
const learnerShell = learnRoute?.children?.find(route => route.path === '');
diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts
index 6c0c417..303aea7 100644
--- a/frontend/src/app/app.routes.ts
+++ b/frontend/src/app/app.routes.ts
@@ -42,6 +42,9 @@ import { DemoReadinessComponent } from './pages/demo-readiness/demo-readiness.co
import { PublicProductsComponent } from './pages/public-products/public-products.component';
import { PublicProductDetailComponent } from './pages/public-products/public-product-detail.component';
import { CommercialDashboardComponent } from './pages/commercial-dashboard/commercial-dashboard.component';
+import { TeacherCurriculumComponent } from './pages/teacher-curriculum/teacher-curriculum.component';
+import { TeacherCoursePreviewComponent } from './pages/teacher-curriculum/teacher-course-preview.component';
+import { TeacherLearnerDetailComponent } from './pages/teacher-learner-detail/teacher-learner-detail.component';
const creatorRoles = ['admin', 'teacher', 'content_admin', 'org_admin', 'instructor'];
const studioRoles = ['content_admin', 'org_admin'];
@@ -55,6 +58,57 @@ export const routes: Routes = [
{ path: 'registration', component: RegistrationComponent },
{ path: 'onboarding/organization', component: OnboardingOrganizationComponent },
{ path: 'access-denied', component: AccessDeniedComponent },
+ {
+ path: 'teach',
+ component: HomeComponent,
+ canActivate: [HomeSessionGuard],
+ children: [
+ {
+ path: '',
+ component: UserDashboardComponent,
+ children: [
+ { path: '', component: EchoedRoleSelectorComponent },
+ {
+ path: 'classes',
+ component: SectionsComponent,
+ canActivate: [RoleGuard],
+ data: { roles: ['teacher', 'instructor', 'org_admin'] }
+ },
+ {
+ path: 'classes/:id',
+ component: SectionDetailComponent,
+ canActivate: [RoleGuard],
+ data: { roles: ['teacher', 'instructor', 'org_admin'] }
+ },
+ {
+ path: 'curriculum',
+ component: TeacherCurriculumComponent,
+ canActivate: [RoleGuard],
+ data: { roles: ['teacher', 'instructor'] }
+ },
+ {
+ path: 'courses/:courseId/preview',
+ component: TeacherCoursePreviewComponent,
+ canActivate: [RoleGuard],
+ data: { roles: ['teacher', 'instructor'] }
+ },
+ {
+ path: 'assignments',
+ component: SectionsComponent,
+ canActivate: [RoleGuard],
+ data: { roles: ['teacher', 'instructor', 'org_admin'] }
+ },
+ {
+ path: 'learners/:learnerId',
+ component: TeacherLearnerDetailComponent,
+ canActivate: [RoleGuard],
+ data: { roles: ['teacher', 'instructor', 'org_admin'] }
+ },
+ { path: 'settings', component: PreferencesComponent },
+ ]
+ }
+ ]
+ },
{
path: 'home',
component: HomeComponent,
diff --git a/frontend/src/app/models/section.ts b/frontend/src/app/models/section.ts
index d080fe0..2bbed8c 100644
--- a/frontend/src/app/models/section.ts
+++ b/frontend/src/app/models/section.ts
@@ -19,3 +19,21 @@ export interface Assignment {
created_by: string;
created_at: string;
}
+
+export interface Enrollment {
+ id: string;
+ section_id: string;
+ user_id: string;
+ role_in_section: string;
+ status: string;
+ enrolled_at: string;
+}
+
+export interface SectionSummary {
+ section_id: string;
+ totals: {
+ enrolled: number;
+ lessons_completed: number;
+ units_completed: number;
+ };
+}
diff --git a/frontend/src/app/pages/section-detail/section-detail.component.html b/frontend/src/app/pages/section-detail/section-detail.component.html
index a86fb51..f61c5ee 100644
--- a/frontend/src/app/pages/section-detail/section-detail.component.html
+++ b/frontend/src/app/pages/section-detail/section-detail.component.html
@@ -1,35 +1,210 @@
-
-
-
Section detail
-
Manage assignments and run live lesson sessions.
-
-
-
-
-
-
-
Assignments
- Start lesson session
-
-
No assignments yet.
-
0">
-
-
{{ assignment.target_type | titlecase }} ยท {{ assignment.target_id }}
-
Due: {{ assignment.due_at || 'Flexible' }}
-
{{ assignment.instructions }}
+
+ Back to classes
+
+
+
+
+
+
+
+
+
+
+ {{ tab.label }}
+
+
+
+
+ Today
+ Class status
+
+
+ Learners
+ {{ rosterCount }}
+
+
+ Assignments
+ {{ assignments.length }}
+
+
+ Lessons complete
+ {{ completedLessons }}
+
+
+ Units complete
+ {{ completedUnits }}
+
+
+
+
+
Current assignments
+
No assignments have been created for this class.
+
0" class="class-detail__list">
+
+ {{ assignmentTargetLabel(assignment) }}
+ {{ assignment.due_at ? ('Due ' + (assignment.due_at | date:'MMM d, y, h:mm a')) : 'No due date' }}
+
+
+
+
+
Work review
+
+ Existing assignment APIs expose assignment creation and student submission status, but no teacher review queue,
+ manual scoring, feedback save, or return-work endpoint is verified.
+
+
+
+
+
+
+ Roster
+ Learners in this class
+
+ 0" class="class-detail__data-list" data-educator-responsive="class-roster">
+
+
+
{{ learnerName(enrollment) }}
+
{{ enrollment.status | titlecase }} enrollment
+
+
+
+
Role
+ {{ enrollment.role_in_section | titlecase }}
+
+
+
Joined
+ {{ enrollment.enrolled_at | date:'MMM d, y' }}
+
+
+ View learner
+
+
+
+
+
+ Assignments
+ Assign learning
+
+ Assignments apply to this class section. The current API does not support selecting individual learners during class assignment creation.
+ {{ assignmentError }}
+
+
+
+
+
{{ assignmentTargetLabel(assignment) }}
+
{{ assignment.instructions || 'No teacher instructions provided.' }}
+
+
+
+
Target
+ {{ assignment.target_type | titlecase }}
+
+
+
Due
+ {{ assignment.due_at ? (assignment.due_at | date:'MMM d, y, h:mm a') : 'Flexible' }}
+
+
+
-
-
-
+
+
+
+ Progress
+ Supported class progress
+ This class view uses the section analytics summary endpoint. Learner-by-learner lesson status is available only through broader teacher summary APIs and may not be class-scoped.
+
+
+ Learners enrolled
+ {{ rosterCount }}
+
+
+ Lessons complete
+ {{ completedLessons }}
+
+
+ Units complete
+ {{ completedUnits }}
+
+
+
+
+
+ Discussion
+ Class discussion entry points
+ Thread and post APIs exist, but they are not currently class-scoped or permission-scoped for teacher moderation. No class discussion feed is shown here until that backend scope is verified.
+
+
+
+
+
diff --git a/frontend/src/app/pages/section-detail/section-detail.component.scss b/frontend/src/app/pages/section-detail/section-detail.component.scss
index 5d4e87f..d99669b 100644
--- a/frontend/src/app/pages/section-detail/section-detail.component.scss
+++ b/frontend/src/app/pages/section-detail/section-detail.component.scss
@@ -1,3 +1,277 @@
:host {
display: block;
}
+
+.class-detail {
+ color: var(--ee-text, #102033);
+ display: grid;
+ gap: 1rem;
+ padding: clamp(1rem, 3vw, 2rem);
+}
+
+.class-detail__back,
+.class-detail a,
+.class-detail button {
+ border-radius: var(--ee-radius-control-default, 8px);
+ font-weight: 850;
+ min-height: 2.75rem;
+}
+
+.class-detail__back,
+.class-detail a {
+ align-items: center;
+ color: var(--ee-action-primary, #0f766e);
+ display: inline-flex;
+ text-decoration: none;
+}
+
+.class-detail__back {
+ text-decoration: underline;
+ text-underline-offset: 0.2em;
+}
+
+.class-detail__header,
+.class-detail__tabs,
+.class-detail__panel,
+.class-detail__row,
+.class-detail__metrics article {
+ background: var(--ee-surface, #fffdf8);
+ border: 1px solid var(--ee-border, #dccdb7);
+ border-radius: var(--ee-radius-lg, 8px);
+ box-shadow: var(--ee-shadow-soft, 0 12px 30px rgba(16, 32, 51, 0.08));
+}
+
+.class-detail__header {
+ align-items: start;
+ display: flex;
+ gap: 1rem;
+ justify-content: space-between;
+ padding: clamp(1rem, 3vw, 1.5rem);
+}
+
+.class-detail__header > a,
+.class-detail__row > a {
+ background: var(--ee-color-action-secondary-background, #fffdf8);
+ border: 1px solid var(--ee-color-action-secondary-border, #cdbda3);
+ padding: 0.65rem 0.9rem;
+}
+
+.class-detail__tabs {
+ display: flex;
+ gap: 0.5rem;
+ overflow-x: auto;
+ padding: 0.5rem;
+}
+
+.class-detail__tabs button {
+ background: transparent;
+ border: 1px solid transparent;
+ color: var(--ee-text-soft, #40546b);
+ cursor: pointer;
+ padding: 0.65rem 0.9rem;
+ white-space: nowrap;
+}
+
+.class-detail__tabs .class-detail__tab--active {
+ background: var(--ee-action-primary, #0f766e);
+ border-color: var(--ee-action-primary, #0f766e);
+ color: #fff;
+}
+
+.class-detail__panel {
+ display: grid;
+ gap: 1rem;
+ padding: clamp(1rem, 3vw, 1.5rem);
+}
+
+.class-detail__eyebrow,
+.class-detail__metrics span,
+dt,
+label span {
+ color: var(--ee-text-muted, #5f6f7f);
+ font-size: 0.76rem;
+ font-weight: 900;
+ letter-spacing: 0.08em;
+ margin: 0;
+ text-transform: uppercase;
+}
+
+h1,
+h2,
+h3,
+p {
+ letter-spacing: 0;
+ margin: 0;
+}
+
+h1 {
+ font-size: clamp(2rem, 5vw, 3rem);
+ line-height: 1;
+}
+
+.class-detail__header p,
+.class-detail__muted,
+.class-detail__row p,
+.class-detail__row span {
+ color: var(--ee-text-soft, #40546b);
+ line-height: 1.6;
+}
+
+.class-detail__metrics {
+ display: grid;
+ gap: 1rem;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+}
+
+.class-detail__metrics article {
+ display: grid;
+ gap: 0.35rem;
+ padding: 1rem;
+}
+
+.class-detail__metrics strong {
+ color: var(--ee-action-primary, #0f766e);
+ font-size: 2rem;
+ line-height: 1;
+}
+
+.class-detail__two-column {
+ display: grid;
+ gap: 1rem;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+
+.class-detail__list,
+.class-detail__data-list {
+ display: grid;
+ gap: 0.75rem;
+}
+
+.class-detail__list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.class-detail__list li {
+ background: var(--ee-surface-muted, #f8fafc);
+ border: 1px solid var(--ee-border, #dccdb7);
+ border-radius: 8px;
+ display: grid;
+ gap: 0.25rem;
+ padding: 0.75rem;
+}
+
+.class-detail__row {
+ align-items: start;
+ display: grid;
+ gap: 1rem;
+ grid-template-columns: minmax(0, 1fr) minmax(14rem, 0.5fr) auto;
+ padding: 1rem;
+}
+
+dl {
+ display: grid;
+ gap: 0.75rem;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ margin: 0;
+}
+
+dd {
+ margin: 0;
+}
+
+.class-detail__assignment-form {
+ display: grid;
+ gap: 0.85rem;
+ grid-template-columns: minmax(10rem, 0.35fr) minmax(0, 1fr) minmax(12rem, 0.45fr);
+}
+
+.class-detail__wide {
+ grid-column: 1 / -1;
+}
+
+label {
+ display: grid;
+ gap: 0.35rem;
+}
+
+input,
+select,
+textarea {
+ background: #fff;
+ border: 1px solid var(--ee-border, #dccdb7);
+ border-radius: var(--ee-radius-control-default, 8px);
+ color: var(--ee-text, #102033);
+ min-height: 2.75rem;
+ padding: 0.65rem 0.75rem;
+}
+
+textarea {
+ resize: vertical;
+}
+
+.class-detail__assignment-form button {
+ align-self: end;
+ background: var(--ee-action-primary, #0f766e);
+ border: 1px solid var(--ee-action-primary, #0f766e);
+ color: #fff;
+ cursor: pointer;
+ padding: 0.65rem 0.9rem;
+}
+
+.class-detail__assignment-form button:disabled {
+ cursor: wait;
+ opacity: 0.68;
+}
+
+.class-detail__error {
+ background: var(--ee-color-status-danger-background, #fff1f3);
+ border: 1px solid var(--ee-color-status-danger-border, #ffd9df);
+ border-radius: 8px;
+ color: var(--ee-color-status-danger-text, #9f1239);
+ padding: 0.75rem;
+}
+
+a:focus-visible,
+button:focus-visible,
+input:focus-visible,
+select:focus-visible,
+textarea:focus-visible {
+ outline: 3px solid var(--ee-focus-ring, #0f766e);
+ outline-offset: 3px;
+}
+
+@media (max-width: 980px) {
+ .class-detail__header,
+ .class-detail__two-column {
+ display: grid;
+ }
+
+ .class-detail__metrics {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+
+ .class-detail__row,
+ .class-detail__assignment-form {
+ grid-template-columns: 1fr;
+ }
+
+ .class-detail__header > a,
+ .class-detail__row > a,
+ .class-detail__assignment-form button {
+ justify-content: center;
+ width: 100%;
+ }
+}
+
+@media (max-width: 520px) {
+ .class-detail {
+ padding: 0.75rem;
+ }
+
+ .class-detail__metrics,
+ dl {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/frontend/src/app/pages/section-detail/section-detail.component.ts b/frontend/src/app/pages/section-detail/section-detail.component.ts
index 54e1aa2..ff1b208 100644
--- a/frontend/src/app/pages/section-detail/section-detail.component.ts
+++ b/frontend/src/app/pages/section-detail/section-detail.component.ts
@@ -1,61 +1,201 @@
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
-import { ActivatedRoute } from '@angular/router';
+import { ActivatedRoute, RouterLink } from '@angular/router';
+import { forkJoin, of } from 'rxjs';
+import { catchError, finalize } from 'rxjs/operators';
+
+import { EchoConfirmationDialogComponent } from '../../components/echo-confirmation-dialog/echo-confirmation-dialog.component';
+import { EchoLoadingStateComponent } from '../../components/echo-loading-state/echo-loading-state.component';
+import { EchoStatePanelComponent } from '../../components/echo-state-panel/echo-state-panel.component';
+import { Course } from '../../models/course';
+import { Assignment, Enrollment, Section, SectionSummary } from '../../models/section';
+import { User } from '../../models/user';
+import { CoursesService } from '../../services/courses.service';
import { SectionsService } from '../../services/sections.service';
-import { Assignment } from '../../models/section';
import { ToastService } from '../../services/toast.service';
+import { UsersService } from '../../services/users.service';
+
+type ClassTab = 'overview' | 'roster' | 'assignments' | 'progress' | 'discussion';
@Component({
selector: 'app-section-detail',
standalone: true,
- imports: [CommonModule, FormsModule],
+ imports: [
+ CommonModule,
+ FormsModule,
+ RouterLink,
+ EchoConfirmationDialogComponent,
+ EchoLoadingStateComponent,
+ EchoStatePanelComponent,
+ ],
templateUrl: './section-detail.component.html',
styleUrl: './section-detail.component.scss'
})
export class SectionDetailComponent implements OnInit {
sectionId = '';
+ section?: Section;
assignments: Assignment[] = [];
+ roster: Enrollment[] = [];
+ students: User[] = [];
+ courses: Course[] = [];
+ summary?: SectionSummary;
+ activeTab: ClassTab = 'overview';
targetType = 'lesson';
targetId = '';
dueAt = '';
instructions = '';
+ loading = true;
+ savingAssignment = false;
+ loadError = '';
+ assignmentError = '';
+ confirmationOpen = false;
+
+ readonly tabs: { id: ClassTab; label: string }[] = [
+ { id: 'overview', label: 'Overview' },
+ { id: 'roster', label: 'Roster' },
+ { id: 'assignments', label: 'Assignments' },
+ { id: 'progress', label: 'Progress' },
+ { id: 'discussion', label: 'Discussion' },
+ ];
constructor(
private route: ActivatedRoute,
private sectionsService: SectionsService,
+ private coursesService: CoursesService,
+ private usersService: UsersService,
private toastService: ToastService
) {}
ngOnInit(): void {
this.sectionId = this.route.snapshot.paramMap.get('id') || '';
- this.loadAssignments();
+ this.loadClass();
+ }
+
+ get className(): string {
+ return this.section?.name || 'Class detail';
+ }
+
+ get rosterCount(): number {
+ return this.summary?.totals.enrolled ?? this.roster.length;
+ }
+
+ get completedLessons(): number {
+ return this.summary?.totals.lessons_completed ?? 0;
+ }
+
+ get completedUnits(): number {
+ return this.summary?.totals.units_completed ?? 0;
+ }
+
+ get upcomingAssignments(): Assignment[] {
+ return [...this.assignments].sort((left, right) => {
+ const leftDate = left.due_at ? new Date(left.due_at).getTime() : Number.MAX_SAFE_INTEGER;
+ const rightDate = right.due_at ? new Date(right.due_at).getTime() : Number.MAX_SAFE_INTEGER;
+ return leftDate - rightDate;
+ });
}
- loadAssignments(): void {
+ loadClass(): void {
if (!this.sectionId) {
+ this.loading = false;
+ this.loadError = 'This class link is missing a section id.';
return;
}
- this.sectionsService.getAssignments(this.sectionId).subscribe(assignments => {
- this.assignments = assignments;
+
+ this.loading = true;
+ this.loadError = '';
+ forkJoin({
+ sections: this.sectionsService.listSections(),
+ assignments: this.sectionsService.getAssignments(this.sectionId).pipe(catchError(() => of([] as Assignment[]))),
+ roster: this.sectionsService.getRoster(this.sectionId).pipe(catchError(() => of([] as Enrollment[]))),
+ summary: this.sectionsService.getSectionSummary(this.sectionId).pipe(catchError(() => of(undefined))),
+ students: this.usersService.getStudents().pipe(catchError(() => of([] as User[]))),
+ courses: this.coursesService.getCourses().pipe(catchError(() => of([] as Course[]))),
+ }).subscribe({
+ next: ({ sections, assignments, roster, summary, students, courses }) => {
+ this.section = sections.find(section => section.id === this.sectionId);
+ this.assignments = assignments;
+ this.roster = roster;
+ this.summary = summary;
+ this.students = students;
+ this.courses = courses;
+ this.loading = false;
+ if (!this.section) {
+ this.loadError = 'This class is not available in your authorized class list.';
+ }
+ },
+ error: () => {
+ this.loading = false;
+ this.loadError = 'We could not load this class. No roster or assignment data was changed.';
+ },
});
}
+ setTab(tab: ClassTab): void {
+ this.activeTab = tab;
+ }
+
+ learnerName(enrollment: Enrollment): string {
+ const student = this.students.find(row => row.id === enrollment.user_id);
+ if (!student) {
+ return 'Learner profile unavailable';
+ }
+ return `${student.firstname || ''} ${student.lastname || ''}`.trim() || student.username || 'Learner';
+ }
+
+ assignmentTargetLabel(assignment: Assignment): string {
+ const course = this.courses.find(row => row.id === assignment.target_id);
+ if (course) {
+ return course.title;
+ }
+ return `${this.titleCase(assignment.target_type)} ${assignment.target_id}`;
+ }
+
+ requestAssignmentCreate(): void {
+ this.assignmentError = '';
+ if (!this.targetId.trim()) {
+ this.assignmentError = 'Choose or enter the learning target before assigning.';
+ return;
+ }
+ this.confirmationOpen = true;
+ }
+
+ cancelAssignmentCreate(): void {
+ if (!this.savingAssignment) {
+ this.confirmationOpen = false;
+ }
+ }
+
createAssignment(): void {
- if (!this.sectionId || !this.targetId) {
+ if (!this.sectionId || !this.targetId.trim()) {
return;
}
+ this.savingAssignment = true;
+ this.assignmentError = '';
this.sectionsService.createAssignment(this.sectionId, {
target_type: this.targetType,
- target_id: this.targetId,
+ target_id: this.targetId.trim(),
due_at: this.dueAt || null,
- instructions: this.instructions
- }).subscribe(() => {
- this.toastService.show('Assignment created!', 'success');
- this.targetId = '';
- this.dueAt = '';
- this.instructions = '';
- this.loadAssignments();
+ instructions: this.instructions.trim() || null,
+ }).pipe(finalize(() => {
+ this.savingAssignment = false;
+ })).subscribe({
+ next: () => {
+ this.toastService.show('Assignment created.', 'success');
+ this.confirmationOpen = false;
+ this.targetId = '';
+ this.dueAt = '';
+ this.instructions = '';
+ this.loadClass();
+ },
+ error: (error) => {
+ this.assignmentError = error?.error?.detail || 'We could not create this assignment. No learners were assigned.';
+ },
});
}
+
+ private titleCase(value: string): string {
+ return value.replace(/[_-]/g, ' ').replace(/\b\w/g, char => char.toUpperCase());
+ }
}
diff --git a/frontend/src/app/pages/sections/sections.component.html b/frontend/src/app/pages/sections/sections.component.html
index 6763cb9..a871e43 100644
--- a/frontend/src/app/pages/sections/sections.component.html
+++ b/frontend/src/app/pages/sections/sections.component.html
@@ -1,32 +1,102 @@
-
-
-
Sections & classrooms
-
Create cohorts, assign lessons, and monitor progress.
-
+
+
+
+
+
+
Create class
+
New class section
+
The current backend creates a class from a course version ID. This does not enroll learners automatically.
+
+
+
+ Class name *
+
+
+
+ Course version ID *
+
+
+
+ Class mode
+
+ In person
+ Remote
+ Hybrid
+ Home
+
+
+ {{ creating ? 'Creating...' : 'Create class' }}
+
+ Use a course version that already exists in the current organization context.
+ {{ formError }}
+
-
-
New section
-
-
-
-
- In-person
- Remote
- Hybrid
- Home
-
-
Create section
+
+
+
+
Authorized classes
+
Class list
+
+
+ Search classes
+
+
-
-
-
My sections
-
No sections yet.
-
-
+
+
diff --git a/frontend/src/app/pages/sections/sections.component.scss b/frontend/src/app/pages/sections/sections.component.scss
index 5d4e87f..fc8c4bc 100644
--- a/frontend/src/app/pages/sections/sections.component.scss
+++ b/frontend/src/app/pages/sections/sections.component.scss
@@ -1,3 +1,205 @@
:host {
display: block;
}
+
+.teach-classes {
+ color: var(--ee-text, #102033);
+ display: grid;
+ gap: 1rem;
+ padding: clamp(1rem, 3vw, 2rem);
+}
+
+.teach-classes__header,
+.teach-classes__panel,
+.teach-classes__card {
+ background: var(--ee-surface, #fffdf8);
+ border: 1px solid var(--ee-border, #dccdb7);
+ border-radius: var(--ee-radius-lg, 8px);
+ box-shadow: var(--ee-shadow-soft, 0 12px 30px rgba(16, 32, 51, 0.08));
+}
+
+.teach-classes__header,
+.teach-classes__panel-heading {
+ align-items: start;
+ display: flex;
+ gap: 1rem;
+ justify-content: space-between;
+}
+
+.teach-classes__header,
+.teach-classes__panel {
+ padding: clamp(1rem, 3vw, 1.5rem);
+}
+
+.teach-classes__panel {
+ display: grid;
+ gap: 1rem;
+}
+
+.teach-classes__eyebrow {
+ color: var(--ee-text-muted, #5f6f7f);
+ font-size: 0.76rem;
+ font-weight: 900;
+ letter-spacing: 0.12em;
+ margin: 0;
+ text-transform: uppercase;
+}
+
+h1,
+h2,
+h3,
+p {
+ letter-spacing: 0;
+ margin: 0;
+}
+
+h1 {
+ font-size: clamp(2rem, 5vw, 3rem);
+ line-height: 1;
+}
+
+.teach-classes__header p,
+.teach-classes__panel p,
+.teach-classes__muted {
+ color: var(--ee-text-soft, #40546b);
+ line-height: 1.6;
+}
+
+.teach-classes a,
+.teach-classes button {
+ border-radius: var(--ee-radius-control-default, 8px);
+ font-weight: 850;
+ min-height: 2.75rem;
+}
+
+.teach-classes__header > a,
+.teach-classes__card {
+ color: var(--ee-action-primary, #0f766e);
+ text-decoration: none;
+}
+
+.teach-classes__header > a {
+ align-items: center;
+ background: var(--ee-color-action-secondary-background, #fffdf8);
+ border: 1px solid var(--ee-color-action-secondary-border, #cdbda3);
+ display: inline-flex;
+ padding: 0.65rem 0.9rem;
+}
+
+.teach-classes__form {
+ display: grid;
+ gap: 0.85rem;
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(10rem, 0.35fr) auto;
+}
+
+label {
+ display: grid;
+ gap: 0.35rem;
+}
+
+label span,
+dt {
+ color: var(--ee-text-muted, #5f6f7f);
+ font-size: 0.8rem;
+ font-weight: 850;
+}
+
+input,
+select {
+ background: #fff;
+ border: 1px solid var(--ee-border, #dccdb7);
+ border-radius: var(--ee-radius-control-default, 8px);
+ color: var(--ee-text, #102033);
+ min-height: 2.75rem;
+ padding: 0.65rem 0.75rem;
+}
+
+button {
+ align-self: end;
+ background: var(--ee-action-primary, #0f766e);
+ border: 1px solid var(--ee-action-primary, #0f766e);
+ color: #fff;
+ cursor: pointer;
+ padding: 0.65rem 0.9rem;
+}
+
+button:disabled {
+ cursor: wait;
+ opacity: 0.68;
+}
+
+input:focus-visible,
+select:focus-visible,
+button:focus-visible,
+a:focus-visible {
+ outline: 3px solid var(--ee-focus-ring, #0f766e);
+ outline-offset: 3px;
+}
+
+.teach-classes__error {
+ background: var(--ee-color-status-danger-background, #fff1f3);
+ border: 1px solid var(--ee-color-status-danger-border, #ffd9df);
+ border-radius: 8px;
+ color: var(--ee-color-status-danger-text, #9f1239);
+ padding: 0.75rem;
+}
+
+.teach-classes__search {
+ min-width: min(20rem, 100%);
+}
+
+.teach-classes__grid {
+ display: grid;
+ gap: 1rem;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+
+.teach-classes__card {
+ display: grid;
+ gap: 0.75rem;
+ padding: 1rem;
+}
+
+.teach-classes__card > span {
+ color: var(--ee-text-muted, #5f6f7f);
+ font-size: 0.8rem;
+ font-weight: 850;
+}
+
+dl {
+ display: grid;
+ gap: 0.75rem;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ margin: 0;
+}
+
+dd {
+ color: var(--ee-action-primary, #0f766e);
+ font-size: 1.4rem;
+ font-weight: 900;
+ margin: 0;
+}
+
+@media (max-width: 900px) {
+ .teach-classes__header,
+ .teach-classes__panel-heading {
+ display: grid;
+ }
+
+ .teach-classes__form,
+ .teach-classes__grid {
+ grid-template-columns: 1fr;
+ }
+
+ button,
+ .teach-classes__header > a {
+ justify-content: center;
+ width: 100%;
+ }
+}
+
+@media (max-width: 520px) {
+ .teach-classes {
+ padding: 0.75rem;
+ }
+}
diff --git a/frontend/src/app/pages/sections/sections.component.ts b/frontend/src/app/pages/sections/sections.component.ts
index 983b2e5..22df78e 100644
--- a/frontend/src/app/pages/sections/sections.component.ts
+++ b/frontend/src/app/pages/sections/sections.component.ts
@@ -1,23 +1,34 @@
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
-import { SectionsService } from '../../services/sections.service';
-import { Section } from '../../models/section';
import { RouterModule } from '@angular/router';
+import { forkJoin, of } from 'rxjs';
+import { catchError } from 'rxjs/operators';
+
+import { EchoLoadingStateComponent } from '../../components/echo-loading-state/echo-loading-state.component';
+import { EchoStatePanelComponent } from '../../components/echo-state-panel/echo-state-panel.component';
+import { Section, SectionSummary } from '../../models/section';
+import { SectionsService } from '../../services/sections.service';
import { ToastService } from '../../services/toast.service';
@Component({
selector: 'app-sections',
standalone: true,
- imports: [CommonModule, FormsModule, RouterModule],
+ imports: [CommonModule, FormsModule, RouterModule, EchoLoadingStateComponent, EchoStatePanelComponent],
templateUrl: './sections.component.html',
styleUrl: './sections.component.scss'
})
export class SectionsComponent implements OnInit {
sections: Section[] = [];
+ summaries: Record
= {};
name = '';
courseVersionId = '';
mode = 'remote';
+ searchTerm = '';
+ loading = true;
+ creating = false;
+ loadError = '';
+ formError = '';
constructor(private sectionsService: SectionsService, private toastService: ToastService) {}
@@ -25,21 +36,88 @@ export class SectionsComponent implements OnInit {
this.loadSections();
}
+ get filteredSections(): Section[] {
+ const search = this.searchTerm.trim().toLowerCase();
+ if (!search) {
+ return this.sections;
+ }
+ return this.sections.filter(section =>
+ section.name.toLowerCase().includes(search) ||
+ section.mode.toLowerCase().includes(search)
+ );
+ }
+
loadSections(): void {
- this.sectionsService.listSections().subscribe(sections => {
- this.sections = sections;
+ this.loading = true;
+ this.loadError = '';
+ this.sectionsService.listSections().subscribe({
+ next: (sections) => {
+ this.sections = sections;
+ this.loading = false;
+ this.loadSummaries();
+ },
+ error: () => {
+ this.sections = [];
+ this.summaries = {};
+ this.loading = false;
+ this.loadError = 'We could not load your classes. No class data was changed.';
+ },
});
}
createSection(): void {
- if (!this.name || !this.courseVersionId) {
+ this.formError = '';
+ if (!this.name.trim() || !this.courseVersionId.trim()) {
+ this.formError = 'Class name and course version ID are required by the current section API.';
return;
}
- this.sectionsService.createSection({ name: this.name, course_version_id: this.courseVersionId, mode: this.mode }).subscribe(() => {
- this.toastService.show('Section created!', 'success');
- this.name = '';
- this.courseVersionId = '';
- this.loadSections();
+
+ this.creating = true;
+ this.sectionsService.createSection({
+ name: this.name.trim(),
+ course_version_id: this.courseVersionId.trim(),
+ mode: this.mode,
+ }).subscribe({
+ next: () => {
+ this.toastService.show('Class created.', 'success');
+ this.name = '';
+ this.courseVersionId = '';
+ this.mode = 'remote';
+ this.creating = false;
+ this.loadSections();
+ },
+ error: (error) => {
+ this.creating = false;
+ this.formError = error?.error?.detail || 'We could not create this class. No learners were enrolled.';
+ },
+ });
+ }
+
+ learnerCount(section: Section): number {
+ return this.summaries[section.id]?.totals.enrolled ?? 0;
+ }
+
+ lessonCount(section: Section): number {
+ return this.summaries[section.id]?.totals.lessons_completed ?? 0;
+ }
+
+ private loadSummaries(): void {
+ if (this.sections.length === 0) {
+ this.summaries = {};
+ return;
+ }
+
+ forkJoin(
+ this.sections.map(section =>
+ this.sectionsService.getSectionSummary(section.id).pipe(catchError(() => of(undefined)))
+ )
+ ).subscribe((summaries) => {
+ this.summaries = summaries.reduce((map, summary) => {
+ if (summary) {
+ map[summary.section_id] = summary;
+ }
+ return map;
+ }, {} as Record);
});
}
}
diff --git a/frontend/src/app/pages/teacher-curriculum/teacher-course-preview.component.scss b/frontend/src/app/pages/teacher-curriculum/teacher-course-preview.component.scss
new file mode 100644
index 0000000..0b65fc8
--- /dev/null
+++ b/frontend/src/app/pages/teacher-curriculum/teacher-course-preview.component.scss
@@ -0,0 +1,104 @@
+:host {
+ display: block;
+}
+
+.teacher-preview {
+ color: var(--ee-text, #102033);
+ display: grid;
+ gap: 1rem;
+ padding: clamp(1rem, 3vw, 2rem);
+}
+
+.teacher-preview__back {
+ color: var(--ee-action-primary, #0f766e);
+ font-weight: 850;
+ text-decoration: underline;
+ text-underline-offset: 0.2em;
+}
+
+.teacher-preview__header,
+.teacher-preview__panel {
+ background: var(--ee-surface, #fffdf8);
+ border: 1px solid var(--ee-border, #dccdb7);
+ border-radius: var(--ee-radius-lg, 8px);
+ box-shadow: var(--ee-shadow-soft, 0 12px 30px rgba(16, 32, 51, 0.08));
+ padding: clamp(1rem, 3vw, 1.5rem);
+}
+
+.teacher-preview__header {
+ display: grid;
+ gap: 1rem;
+ grid-template-columns: minmax(0, 1fr) minmax(18rem, 0.38fr);
+}
+
+.teacher-preview__header aside {
+ background: var(--ee-surface-muted, #f8fafc);
+ border: 1px solid var(--ee-border, #dccdb7);
+ border-radius: 8px;
+ display: grid;
+ gap: 0.5rem;
+ padding: 1rem;
+}
+
+.teacher-preview__panel {
+ display: grid;
+ gap: 0.75rem;
+}
+
+.teacher-preview__eyebrow,
+dt {
+ color: var(--ee-text-muted, #5f6f7f);
+ font-size: 0.76rem;
+ font-weight: 900;
+ letter-spacing: 0.1em;
+ margin: 0;
+ text-transform: uppercase;
+}
+
+h1,
+h2,
+p {
+ letter-spacing: 0;
+ margin: 0;
+}
+
+h1 {
+ font-size: clamp(2rem, 5vw, 3.4rem);
+ line-height: 1;
+}
+
+.teacher-preview p {
+ color: var(--ee-text-soft, #40546b);
+ line-height: 1.6;
+}
+
+dl {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1rem;
+ margin: 0;
+}
+
+dd {
+ color: var(--ee-action-primary, #0f766e);
+ font-size: 1.4rem;
+ font-weight: 900;
+ margin: 0;
+}
+
+a:focus-visible {
+ outline: 3px solid var(--ee-focus-ring, #0f766e);
+ outline-offset: 3px;
+}
+
+@media (max-width: 900px) {
+ .teacher-preview__header {
+ grid-template-columns: 1fr;
+ }
+}
+
+@media (max-width: 520px) {
+ .teacher-preview {
+ padding: 0.75rem;
+ }
+}
diff --git a/frontend/src/app/pages/teacher-curriculum/teacher-course-preview.component.ts b/frontend/src/app/pages/teacher-curriculum/teacher-course-preview.component.ts
new file mode 100644
index 0000000..ddfe30b
--- /dev/null
+++ b/frontend/src/app/pages/teacher-curriculum/teacher-course-preview.component.ts
@@ -0,0 +1,190 @@
+import { CommonModule } from '@angular/common';
+import { Component, OnInit } from '@angular/core';
+import { ActivatedRoute, RouterLink } from '@angular/router';
+
+import {
+ StudentCurriculumComponent,
+ StudentCurriculumLesson,
+ StudentCurriculumUnit,
+} from '../../components/student-curriculum/student-curriculum.component';
+import { EchoLoadingStateComponent } from '../../components/echo-loading-state/echo-loading-state.component';
+import { EchoStatePanelComponent } from '../../components/echo-state-panel/echo-state-panel.component';
+import { Course } from '../../models/course';
+import { Lesson } from '../../models/lesson';
+import { Unit } from '../../models/unit';
+import { CoursesService } from '../../services/courses.service';
+
+@Component({
+ selector: 'app-teacher-course-preview',
+ standalone: true,
+ imports: [CommonModule, RouterLink, StudentCurriculumComponent, EchoLoadingStateComponent, EchoStatePanelComponent],
+ template: `
+
+ Back to curriculum
+
+
+
+
+
+
+
+
+ Course purpose
+ What learners will see
+ {{ course.learning_objectives || 'Learning objectives are not available from the current course data.' }}
+
+
+
Units
+ {{ unitCount }}
+
+
+
Lessons
+ {{ lessonCount }}
+
+
+
+
+
+
+
+ Assessments and resources
+ Supported preview detail
+
+ Lesson activities, discussion prompts, teacher notes, sources, and assessment references appear where they are included in the current course and lesson payloads.
+ This page does not call the student start-course API.
+
+ {{ previewMessage }}
+
+
+
+ `,
+ styleUrl: './teacher-course-preview.component.scss',
+})
+export class TeacherCoursePreviewComponent implements OnInit {
+ courseId = '';
+ course?: Course;
+ loading = true;
+ loadError = '';
+ previewMessage = '';
+ curriculumUnits: StudentCurriculumUnit[] = [];
+
+ constructor(
+ private route: ActivatedRoute,
+ private coursesService: CoursesService,
+ ) {}
+
+ ngOnInit(): void {
+ this.courseId = this.route.snapshot.paramMap.get('courseId') || '';
+ this.loadCourse();
+ }
+
+ get unitCount(): number {
+ return this.orderedUnits.length;
+ }
+
+ get lessonCount(): number {
+ return this.orderedUnits.reduce((count, unit) => count + this.orderedLessons(unit).length, 0);
+ }
+
+ get curriculumSummaryLabel(): string {
+ return `${this.unitCount} unit${this.unitCount === 1 ? '' : 's'}, ${this.lessonCount} lesson${this.lessonCount === 1 ? '' : 's'} in preview`;
+ }
+
+ loadCourse(): void {
+ if (!this.courseId) {
+ this.loading = false;
+ this.loadError = 'This preview link is missing a course id.';
+ return;
+ }
+ this.loading = true;
+ this.loadError = '';
+ this.previewMessage = '';
+ this.coursesService.getCourseById(this.courseId).subscribe({
+ next: (course) => {
+ this.course = course as unknown as Course;
+ this.curriculumUnits = this.buildCurriculumUnits();
+ this.loading = false;
+ },
+ error: (error) => {
+ this.course = undefined;
+ this.curriculumUnits = [];
+ this.loading = false;
+ this.loadError = error?.status === 404
+ ? 'This course could not be found.'
+ : 'We could not load the course preview. No learner progress was changed.';
+ },
+ });
+ }
+
+ notePreviewOnly(lesson: StudentCurriculumLesson): void {
+ this.previewMessage = `${lesson.title} is shown in teacher preview only. Open the lesson runtime only through supported learner or staff lesson routes.`;
+ }
+
+ private get orderedUnits(): Unit[] {
+ return [...(this.course?.units || [])].sort((left, right) => {
+ const leftOrder = left.order ?? Number.MAX_SAFE_INTEGER;
+ const rightOrder = right.order ?? Number.MAX_SAFE_INTEGER;
+ return leftOrder === rightOrder ? left.title.localeCompare(right.title) : leftOrder - rightOrder;
+ });
+ }
+
+ private buildCurriculumUnits(): StudentCurriculumUnit[] {
+ return this.orderedUnits.map((unit, index) => ({
+ id: unit.id,
+ title: unit.title,
+ order: unit.order ?? index + 1,
+ progressLabel: 'Preview',
+ lessons: this.orderedLessons(unit).map(lesson => this.mapLesson(lesson)),
+ }));
+ }
+
+ private mapLesson(lesson: Lesson): StudentCurriculumLesson {
+ return {
+ id: lesson.id,
+ title: lesson.title,
+ objective: lesson.objective || lesson.learning_objectives,
+ durationMinutes: lesson.duration_minutes,
+ activityCount: lesson.activities?.length || 0,
+ state: 'included',
+ stateLabel: 'Preview',
+ stateDescription: 'Teacher preview only. No learner progress will be recorded.',
+ canOpen: true,
+ };
+ }
+
+ private orderedLessons(unit: Unit): Lesson[] {
+ return [...(unit.lessons || [])].sort((left, right) => {
+ const leftOrder = left.order ?? Number.MAX_SAFE_INTEGER;
+ const rightOrder = right.order ?? Number.MAX_SAFE_INTEGER;
+ return leftOrder === rightOrder ? left.title.localeCompare(right.title) : leftOrder - rightOrder;
+ });
+ }
+}
diff --git a/frontend/src/app/pages/teacher-curriculum/teacher-curriculum.component.scss b/frontend/src/app/pages/teacher-curriculum/teacher-curriculum.component.scss
new file mode 100644
index 0000000..7c67cb4
--- /dev/null
+++ b/frontend/src/app/pages/teacher-curriculum/teacher-curriculum.component.scss
@@ -0,0 +1,141 @@
+:host {
+ display: block;
+}
+
+.teacher-curriculum {
+ color: var(--ee-text, #102033);
+ display: grid;
+ gap: 1rem;
+ padding: clamp(1rem, 3vw, 2rem);
+}
+
+.teacher-curriculum__header,
+.teacher-curriculum__panel,
+.teacher-curriculum__card {
+ background: var(--ee-surface, #fffdf8);
+ border: 1px solid var(--ee-border, #dccdb7);
+ border-radius: var(--ee-radius-lg, 8px);
+ box-shadow: var(--ee-shadow-soft, 0 12px 30px rgba(16, 32, 51, 0.08));
+ padding: clamp(1rem, 3vw, 1.5rem);
+}
+
+.teacher-curriculum__header {
+ align-items: start;
+ display: flex;
+ gap: 1rem;
+ justify-content: space-between;
+}
+
+.teacher-curriculum__eyebrow,
+label span,
+dt {
+ color: var(--ee-text-muted, #5f6f7f);
+ font-size: 0.76rem;
+ font-weight: 900;
+ letter-spacing: 0.1em;
+ margin: 0;
+ text-transform: uppercase;
+}
+
+h1,
+h2,
+p {
+ letter-spacing: 0;
+ margin: 0;
+}
+
+h1 {
+ font-size: clamp(2rem, 5vw, 3rem);
+ line-height: 1;
+}
+
+.teacher-curriculum__header p,
+.teacher-curriculum__card p {
+ color: var(--ee-text-soft, #40546b);
+ line-height: 1.6;
+}
+
+a {
+ align-items: center;
+ background: var(--ee-color-action-secondary-background, #fffdf8);
+ border: 1px solid var(--ee-color-action-secondary-border, #cdbda3);
+ border-radius: var(--ee-radius-control-default, 8px);
+ color: var(--ee-action-primary, #0f766e);
+ display: inline-flex;
+ font-weight: 850;
+ min-height: 2.75rem;
+ padding: 0.65rem 0.9rem;
+ text-decoration: none;
+}
+
+label {
+ display: grid;
+ gap: 0.35rem;
+}
+
+input {
+ background: #fff;
+ border: 1px solid var(--ee-border, #dccdb7);
+ border-radius: var(--ee-radius-control-default, 8px);
+ color: var(--ee-text, #102033);
+ min-height: 2.75rem;
+ padding: 0.65rem 0.75rem;
+}
+
+a:focus-visible,
+input:focus-visible {
+ outline: 3px solid var(--ee-focus-ring, #0f766e);
+ outline-offset: 3px;
+}
+
+.teacher-curriculum__grid {
+ display: grid;
+ gap: 1rem;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+
+.teacher-curriculum__card {
+ display: grid;
+ gap: 0.85rem;
+}
+
+dl {
+ display: grid;
+ gap: 0.75rem;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ margin: 0;
+}
+
+dd {
+ color: var(--ee-action-primary, #0f766e);
+ font-size: 1.4rem;
+ font-weight: 900;
+ margin: 0;
+}
+
+.teacher-curriculum__actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.6rem;
+}
+
+@media (max-width: 900px) {
+ .teacher-curriculum__header {
+ display: grid;
+ }
+
+ .teacher-curriculum__grid {
+ grid-template-columns: 1fr;
+ }
+}
+
+@media (max-width: 520px) {
+ .teacher-curriculum {
+ padding: 0.75rem;
+ }
+
+ a {
+ justify-content: center;
+ width: 100%;
+ }
+}
diff --git a/frontend/src/app/pages/teacher-curriculum/teacher-curriculum.component.ts b/frontend/src/app/pages/teacher-curriculum/teacher-curriculum.component.ts
new file mode 100644
index 0000000..b91d54e
--- /dev/null
+++ b/frontend/src/app/pages/teacher-curriculum/teacher-curriculum.component.ts
@@ -0,0 +1,137 @@
+import { CommonModule } from '@angular/common';
+import { Component, OnInit } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { RouterLink } from '@angular/router';
+
+import { EchoLoadingStateComponent } from '../../components/echo-loading-state/echo-loading-state.component';
+import { EchoStatePanelComponent } from '../../components/echo-state-panel/echo-state-panel.component';
+import { Course } from '../../models/course';
+import { CoursesService } from '../../services/courses.service';
+
+@Component({
+ selector: 'app-teacher-curriculum',
+ standalone: true,
+ imports: [CommonModule, FormsModule, RouterLink, EchoLoadingStateComponent, EchoStatePanelComponent],
+ template: `
+
+
+
+
+
+
Filter
+
Search available courses
+
+
+ Search courses
+
+
+
+
+
+
+
+ 0 && filteredCourses.length === 0"
+ variant="empty"
+ eyebrow="No matching courses"
+ title="No course matches your search"
+ body="Clear the search to return to the full curriculum list."
+ >
+
+ 0" class="teacher-curriculum__grid">
+
+ Course
+ {{ course.title }}
+ {{ course.description || 'No description available.' }}
+
+
+
Units
+ {{ course.units?.length || 0 }}
+
+
+
Lessons
+ {{ lessonCount(course) }}
+
+
+ {{ course.learning_objectives }}
+
+
+
+
+ `,
+ styleUrl: './teacher-curriculum.component.scss',
+})
+export class TeacherCurriculumComponent implements OnInit {
+ courses: Course[] = [];
+ searchTerm = '';
+ loading = true;
+ loadError = '';
+
+ constructor(private coursesService: CoursesService) {}
+
+ ngOnInit(): void {
+ this.loadCourses();
+ }
+
+ get filteredCourses(): Course[] {
+ const search = this.searchTerm.trim().toLowerCase();
+ if (!search) {
+ return this.courses;
+ }
+ return this.courses.filter(course =>
+ course.title.toLowerCase().includes(search) ||
+ (course.description || '').toLowerCase().includes(search) ||
+ (course.learning_objectives || '').toLowerCase().includes(search)
+ );
+ }
+
+ loadCourses(): void {
+ this.loading = true;
+ this.loadError = '';
+ this.coursesService.getCourses().subscribe({
+ next: (courses) => {
+ this.courses = courses;
+ this.loading = false;
+ },
+ error: () => {
+ this.courses = [];
+ this.loading = false;
+ this.loadError = 'We could not load curriculum. No course or learner progress was changed.';
+ },
+ });
+ }
+
+ lessonCount(course: Course): number {
+ return (course.units || []).reduce((count, unit) => count + (unit.lessons?.length || 0), 0);
+ }
+}
diff --git a/frontend/src/app/pages/teacher-learner-detail/teacher-learner-detail.component.scss b/frontend/src/app/pages/teacher-learner-detail/teacher-learner-detail.component.scss
new file mode 100644
index 0000000..5531d82
--- /dev/null
+++ b/frontend/src/app/pages/teacher-learner-detail/teacher-learner-detail.component.scss
@@ -0,0 +1,93 @@
+:host {
+ display: block;
+}
+
+.teacher-learner {
+ color: var(--ee-text, #102033);
+ display: grid;
+ gap: 1rem;
+ padding: clamp(1rem, 3vw, 2rem);
+}
+
+.teacher-learner__back {
+ color: var(--ee-action-primary, #0f766e);
+ font-weight: 850;
+ text-decoration: underline;
+ text-underline-offset: 0.2em;
+}
+
+.teacher-learner__header,
+.teacher-learner__panel,
+.teacher-learner__states article {
+ background: var(--ee-surface, #fffdf8);
+ border: 1px solid var(--ee-border, #dccdb7);
+ border-radius: var(--ee-radius-lg, 8px);
+ box-shadow: var(--ee-shadow-soft, 0 12px 30px rgba(16, 32, 51, 0.08));
+ padding: clamp(1rem, 3vw, 1.5rem);
+}
+
+.teacher-learner__panel {
+ display: grid;
+ gap: 1rem;
+}
+
+.teacher-learner__eyebrow,
+dt {
+ color: var(--ee-text-muted, #5f6f7f);
+ font-size: 0.76rem;
+ font-weight: 900;
+ letter-spacing: 0.1em;
+ margin: 0;
+ text-transform: uppercase;
+}
+
+h1,
+h2,
+p {
+ letter-spacing: 0;
+ margin: 0;
+}
+
+h1 {
+ font-size: clamp(2rem, 5vw, 3rem);
+ line-height: 1;
+}
+
+.teacher-learner p {
+ color: var(--ee-text-soft, #40546b);
+ line-height: 1.6;
+}
+
+dl,
+.teacher-learner__states {
+ display: grid;
+ gap: 1rem;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ margin: 0;
+}
+
+.teacher-learner__states {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+
+dd {
+ margin: 0;
+}
+
+a:focus-visible {
+ outline: 3px solid var(--ee-focus-ring, #0f766e);
+ outline-offset: 3px;
+}
+
+@media (max-width: 820px) {
+ dl,
+ .teacher-learner__states {
+ grid-template-columns: 1fr;
+ }
+}
+
+@media (max-width: 520px) {
+ .teacher-learner {
+ padding: 0.75rem;
+ }
+}
diff --git a/frontend/src/app/pages/teacher-learner-detail/teacher-learner-detail.component.ts b/frontend/src/app/pages/teacher-learner-detail/teacher-learner-detail.component.ts
new file mode 100644
index 0000000..fd73c67
--- /dev/null
+++ b/frontend/src/app/pages/teacher-learner-detail/teacher-learner-detail.component.ts
@@ -0,0 +1,153 @@
+import { CommonModule } from '@angular/common';
+import { Component, OnInit } from '@angular/core';
+import { ActivatedRoute, RouterLink } from '@angular/router';
+import { forkJoin, of } from 'rxjs';
+import { catchError } from 'rxjs/operators';
+
+import { EchoLoadingStateComponent } from '../../components/echo-loading-state/echo-loading-state.component';
+import { EchoStatePanelComponent } from '../../components/echo-state-panel/echo-state-panel.component';
+import { Enrollment, Section } from '../../models/section';
+import { User } from '../../models/user';
+import { SectionsService } from '../../services/sections.service';
+import { UsersService } from '../../services/users.service';
+
+@Component({
+ selector: 'app-teacher-learner-detail',
+ standalone: true,
+ imports: [CommonModule, RouterLink, EchoLoadingStateComponent, EchoStatePanelComponent],
+ template: `
+
+ Back to class
+
+
+
+
+
+
+
+
+ Current learning
+ Class-scoped information
+
+
+
Enrollment status
+ {{ enrollment?.status | titlecase }}
+
+
+
Role in class
+ {{ enrollment?.role_in_section | titlecase }}
+
+
+
Joined
+ {{ enrollment?.enrolled_at ? (enrollment?.enrolled_at | date:'MMM d, y') : 'Unavailable' }}
+
+
+
+
+
+ Progress and assessment
+ Supported states
+
+
+ Assignment status
+ Unavailable in this detail view. The current assignment API does not expose per-learner assignment state to teachers.
+
+
+ Course progress
+ Use the Teach overview for existing teacher summary rows. A class-scoped learner progress endpoint is not verified.
+
+
+ Assessment results
+ Auto-scored assessment reporting endpoints exist, but this page does not infer assessment scope without a course, unit, lesson, or program target.
+
+
+ Feedback history
+ Feedback unavailable. No teacher feedback persistence endpoint is verified.
+
+
+
+
+
+ `,
+ styleUrl: './teacher-learner-detail.component.scss',
+})
+export class TeacherLearnerDetailComponent implements OnInit {
+ learnerId = '';
+ sectionId = '';
+ learner?: User;
+ section?: Section;
+ enrollment?: Enrollment;
+ loading = true;
+ loadError = '';
+
+ constructor(
+ private route: ActivatedRoute,
+ private sectionsService: SectionsService,
+ private usersService: UsersService,
+ ) {}
+
+ ngOnInit(): void {
+ this.learnerId = this.route.snapshot.paramMap.get('learnerId') || '';
+ this.sectionId = this.route.snapshot.queryParamMap.get('sectionId') || '';
+ this.loadLearner();
+ }
+
+ get learnerName(): string {
+ if (!this.learner) {
+ return 'Learner';
+ }
+ return `${this.learner.firstname || ''} ${this.learner.lastname || ''}`.trim() || this.learner.username || 'Learner';
+ }
+
+ loadLearner(): void {
+ if (!this.learnerId || !this.sectionId) {
+ this.loading = false;
+ this.loadError = 'Open a learner from a class roster so class authorization can be verified.';
+ return;
+ }
+
+ this.loading = true;
+ this.loadError = '';
+ forkJoin({
+ sections: this.sectionsService.listSections(),
+ roster: this.sectionsService.getRoster(this.sectionId),
+ students: this.usersService.getStudents().pipe(catchError(() => of([] as User[]))),
+ }).subscribe({
+ next: ({ sections, roster, students }) => {
+ this.section = sections.find(section => section.id === this.sectionId);
+ this.enrollment = roster.find(row => row.user_id === this.learnerId);
+ this.learner = students.find(student => student.id === this.learnerId);
+ this.loading = false;
+ if (!this.section || !this.enrollment) {
+ this.loadError = 'This learner is not available in the selected authorized class roster.';
+ return;
+ }
+ if (!this.learner) {
+ this.loadError = 'The learner enrollment exists, but the authorized student profile list did not include profile details.';
+ }
+ },
+ error: () => {
+ this.loading = false;
+ this.loadError = 'We could not verify this learner against the class roster.';
+ },
+ });
+ }
+}
diff --git a/frontend/src/app/pages/user-dashboard/echoed-role-selector/echoed-role-selector.component.html b/frontend/src/app/pages/user-dashboard/echoed-role-selector/echoed-role-selector.component.html
index e6cc2d9..2099b4c 100644
--- a/frontend/src/app/pages/user-dashboard/echoed-role-selector/echoed-role-selector.component.html
+++ b/frontend/src/app/pages/user-dashboard/echoed-role-selector/echoed-role-selector.component.html
@@ -1,8 +1,8 @@
-
+
Loading user information...
-
\ No newline at end of file
+
diff --git a/frontend/src/app/pages/user-dashboard/teacher-view/teacher-view.component.html b/frontend/src/app/pages/user-dashboard/teacher-view/teacher-view.component.html
index b9da936..5448a8c 100644
--- a/frontend/src/app/pages/user-dashboard/teacher-view/teacher-view.component.html
+++ b/frontend/src/app/pages/user-dashboard/teacher-view/teacher-view.component.html
@@ -1,546 +1,54 @@
-
-
-
-
-
-
-
Teacher Control Center
-
Welcome, {{userInfo.fullname}}
-
- Monitor the flagship class story, review governance signals, and keep the live demo on a stable path.
-
-
-
Demo ready
-
-
-
-
-
Active courses
-
{{ courses.length }}
-
-
-
In progress lessons
-
{{ inProgressLessons }}
-
-
-
Average completion
-
{{ averageCompletion }}%
-
-
-
-
-
-
-
-
Pilot demonstration focus
-
{{ flagshipCourse?.title || 'Introduction to Africa flagship pathway' }}
-
-
- {{ flagshipCourse ? 'Flagship visible' : 'Read-only mode' }}
-
-
-
- Keep the walkthrough centered on course visibility, learner progress, and governed runtime context. Destructive or off-script actions remain secondary.
-
-
-
-
Flagship runtime
-
{{ flagshipCourse ? 'Available for narration' : 'Waiting for flagship course data' }}
-
-
-
Teacher confidence
-
Loading, retry, and empty states stay bounded and presentation-safe.
-
-
-
+
+
+
+
+
+ Classes
+ {{ students.length }}
+ Authorized learner profiles available for assignment and roster matching.
+
+
+ Active learning
+ {{ inProgressLessons }}
+ Learner course rows currently reported as active by the teacher summary API.
+
+
+ Average progress
+ {{ averageCompletion }}%
+ Simple average of supported course progress rows. This is not an urgency score.
+
-
-
-
-
-
-
Course Management
- Demo-safe view
-
-
- The recommended live demo stays on read-only course visibility and governance context. Off-script catalog actions are intentionally de-emphasized here.
-
-
-
-
-
-
-
- 0">
-
-
-
-
- Title
- Status
- Actions
-
-
-
-
- {{ course.title }}
-
-
- {{ flagshipCourse?.id === course.id ? 'Flagship course' : 'Visible in teacher view' }}
-
-
-
- Manage
-
-
-
-
-
-
-
-
-
-
Course
-
{{ course.title }}
-
-
- {{ flagshipCourse?.id === course.id ? 'Flagship course' : 'Visible in teacher view' }}
-
-
-
- Manage
-
-
-
- visibleCount" class="teacher-dashboard__text-link mt-4 inline-flex cursor-pointer items-center gap-2 text-sm text-primary" (click)="viewAllCourses()">View More
-
-
-
-
-
Publish readiness
-
Read-only readiness checks for the visible educator course set.
-
-
-
-
-
-
-
0" class="space-y-3">
-
-
-
-
{{ readiness.course_title }}
-
- {{ readiness.blocking_issue_count }} blocking issues
- • {{ readiness.warning_count }} warnings
-
-
-
- {{ readinessStateLabel(readiness.is_ready) }}
-
-
-
- 0" class="mt-3">
-
Blocking issues
-
-
- {{ issue.entity_title }} : {{ issue.message }}
-
-
-
-
- 0" class="mt-3">
-
Warnings
-
-
- {{ warning.entity_title }} : {{ warning.message }}
-
-
-
-
-
-
-
-
-
-
Safe publish validation
-
Read-only learner-safety checks for the visible educator course set.
-
-
-
-
-
-
-
0" class="space-y-3">
-
-
-
-
{{ validation.course_title }}
-
- {{ validation.blocking_issue_count }} blocking issues
- • {{ validation.warning_count }} warnings
-
-
-
- {{ safePublishStateLabel(validation.is_safe) }}
-
-
-
- 0" class="mt-3">
-
Blocking issues
-
-
- {{ issue.entity_title }} : {{ issue.message }}
-
-
-
-
- 0" class="mt-3">
-
Warnings
-
-
- {{ warning.entity_title }} : {{ warning.message }}
-
-
-
-
-
-
-
-
-
-
Competency evidence integrity
-
Read-only mastery explainability checks for the visible educator course set.
-
-
-
-
-
-
-
0" class="space-y-3">
-
-
-
-
{{ integrity.course_title }}
-
- {{ integrity.blocking_issue_count }} blocking issues
- • {{ integrity.warning_count }} warnings
-
-
-
-
- {{ competencyIntegrityStateLabel(integrity.is_valid) }}
-
-
- {{ competencyExplainabilityLabel(integrity.is_explainable) }}
-
-
-
-
- 0" class="mt-3 rounded-xl border border-slate-200 bg-slate-50 p-3">
-
Affected competencies
-
{{ integrity.affected_competency_identifiers.join(', ') }}
-
-
- 0" class="mt-3">
-
Affected assessments
-
-
- {{ assessment.assessment_title }}
- 0">
- : {{ assessment.competency_identifiers.join(', ') }}
-
-
-
-
-
- 0" class="mt-3">
-
Blocking issues
-
-
- {{ issue.entity_title }} : {{ issue.message }}
-
-
-
-
- 0" class="mt-3">
-
Warnings
-
-
- {{ warning.entity_title }} : {{ warning.message }}
-
-
-
-
-
-
-
-
-
-
Runtime intervention recommendations
-
Course-level recommendation detail for each learner, including evidence basis, confidence, and caution flags.
-
-
-
-
-
-
-
0" class="space-y-4">
-
-
-
-
{{ group.courseTitle }}
-
- {{ group.recommendations.length }} learner recommendations
-
-
-
- Flagship course
-
-
-
-
-
-
-
-
{{ recommendation.student_name }}
-
{{ recommendation.summary }}
-
-
-
- {{ formatRuntimeLabel(recommendation.recommendation_state) }}
-
-
- {{ formatRuntimeLabel(recommendation.educator_attention_level) }} attention
-
-
-
-
-
-
-
Confidence
-
{{ formatRuntimeLabel(recommendation.confidence_level) }}
-
-
-
Learner-safe message
-
{{ recommendation.learner_safe_message }}
-
-
-
- 0" class="mt-3">
-
Evidence basis
-
-
- {{ formatRuntimeLabel(basis.source) }} : {{ basis.detail }}
-
- ({{ basis.assessment_title }})
-
- 0">
- - {{ basis.competency_identifiers.join(', ') }}
-
-
-
-
-
- 0" class="mt-3 rounded-xl border border-amber-200 bg-amber-50 p-3">
-
Caution flags
-
{{ formatRuntimeLabels(recommendation.caution_flags) }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Advanced actions
-
- Useful outside the scripted demo path. Keep focus on governance and runtime visibility during the live walkthrough.
-
-
-
Off-script
-
-
-
-
-
-
-
-
-
-
Assign a course
-
Prepare a learner demo path quickly without leaving the dashboard.
-
-
-
- Select student
- {{ student.firstname }} {{ student.lastname }}
-
-
- Select course
- {{ course.title }}
-
- Assign Course
-
-
-
-
-
Teacher runtime confidence
-
Use these secondary controls only when you need to narrate the broader educator surface beyond the flagship walkthrough.
-
- Start Live Lesson
- View Reports
-
-
-
-
-
-
-
-
-
-
-
Student Progress
-
A classroom snapshot of who has started, who is moving steadily, and who may need live support during the pilot walkthrough.
+
+
+
+
+
Needs attention today
+
Learner support and review signals
+
Refresh
- 0">
-
-
-
-
- Student
- Course
- Progress
- Last Active
-
-
-
-
- {{ row.student_name }}
- {{ row.course_title }}
-
-
-
- {{ row.progress }}%
- {{ row.status }}
-
-
-
-
- {{ row.last_active ? (row.last_active | date: 'MMM d, y') : 'Not started' }}
-
-
-
-
-
-
-
-
-
{{ row.student_name }}
-
{{ row.course_title }}
-
-
{{ row.status }}
-
-
-
-
Progress
-
{{ row.progress }}%
-
-
-
-
-
Last active
-
{{ row.last_active ? (row.last_active | date: 'MMM d, y') : 'Not started' }}
-
-
-
-
-
-
-
-
-
-
-
-
Runtime Support
-
Flagship spotlight for fast live-demo narration about who needs support, who is ready for extension, and who is moving steadily.
-
-
- Flagship
-
-
+
0" class="teach-home__data-list" data-educator-responsive="teacher-summary">
+
+
+
{{ row.student_name }}
+
{{ row.course_title }}
-
-
-
-
-
-
-
0" class="space-y-3">
-
-
-
-
{{ row.student_name }}
-
{{ row.course_title }}
-
-
- {{ runtimeSupportStateLabel[row.support_state] || runtimeSupportStateLabel['unknown'] }}
-
+
+
{{ row.progress }}%
+
{{ row.status === 'active' ? 'In progress' : (row.status | titlecase) }}
+
+
+
{{ row.last_active ? (row.last_active | date:'MMM d, y') : 'No recent activity available' }}
+
+
+
- {{ row.support_summary }}
+
+ Recommended next actions
+ Keep instruction moving
+
+
+ Feedback, manual scoring, and discussion moderation are shown only where current APIs support them.
+
+
+
-
0" class="mt-3 rounded-xl border border-slate-200 bg-white p-3">
-
Context
-
{{ row.context_lesson_title }}
-
0" class="mt-1 text-sm text-slate-600">
- Focus concepts: {{ row.context_key_concepts.join(', ') }}
-
-
0" class="mt-2 space-y-2 text-sm text-slate-700">
-
- {{ prompt }}
-
-
-
+
+
+
+
Curriculum shortcuts
+
Courses ready to preview
+
+
Browse all
+
-
- Latest evidence: {{ row.last_evidence_at | date: 'MMM d, y' }}
-
-
-
-
+
+
+
+
+
0" class="teach-home__course-grid" data-educator-responsive="teacher-courses">
+
+ Course
+ {{ course.title }}
+ {{ course.description || 'No course description available.' }}
+
+
+
+
-
-
-
-
Upcoming Events
-
A small credibility surface for pilot walkthroughs so the dashboard feels like an active teacher workspace.
-
-
-
- Parent-Teacher Conference - March 15
- Live project showcase - March 20
- Course feedback survey - March 30
-
-
+
+ Discussions and feedback
+ Supported review surfaces
+
+
Work review: assessment attempts are auto-scored by the existing assessment API. Manual grading and return-work workflows are not exposed because no teacher feedback persistence endpoint is verified.
+
Discussions: thread and post APIs exist, but they are not scoped to classes or moderation permissions. Teach links keep discussion moderation out of the main workflow until backend scope is verified.
-
-
+
+
diff --git a/frontend/src/app/pages/user-dashboard/teacher-view/teacher-view.component.scss b/frontend/src/app/pages/user-dashboard/teacher-view/teacher-view.component.scss
index 797df6d..75f154d 100644
--- a/frontend/src/app/pages/user-dashboard/teacher-view/teacher-view.component.scss
+++ b/frontend/src/app/pages/user-dashboard/teacher-view/teacher-view.component.scss
@@ -2,130 +2,219 @@
display: block;
}
-.teacher-dashboard {
- color: var(--echo-text, #0f172a);
+.teach-home {
+ color: var(--ee-text, #102033);
+ display: grid;
+ gap: clamp(1rem, 2vw, 1.5rem);
+ padding: clamp(1rem, 3vw, 2rem);
}
-.teacher-dashboard__hero-badge,
-.teacher-dashboard__hero-panel {
- backdrop-filter: blur(16px);
+.teach-home__header,
+.teach-home__panel,
+.teach-home__priority,
+.teach-home__course {
+ background: var(--ee-surface, #fffdf8);
+ border: 1px solid var(--ee-border, #dccdb7);
+ border-radius: var(--ee-radius-lg, 8px);
+ box-shadow: var(--ee-shadow-soft, 0 12px 30px rgba(16, 32, 51, 0.08));
}
-.teacher-dashboard__section-heading,
-.teacher-dashboard__subsection-heading {
- position: relative;
+.teach-home__header {
+ align-items: start;
+ display: grid;
+ gap: 1rem;
+ grid-template-columns: minmax(0, 1fr) auto;
+ padding: clamp(1rem, 3vw, 1.5rem);
}
-.teacher-dashboard__section-heading::after {
- content: '';
- display: block;
- margin-top: 0.9rem;
- height: 1px;
- width: 100%;
- background: linear-gradient(90deg, rgba(15, 23, 42, 0.12), rgba(15, 23, 42, 0));
+.teach-home__eyebrow,
+.teach-home__priority span {
+ color: var(--ee-text-muted, #5f6f7f);
+ font-size: 0.76rem;
+ font-weight: 900;
+ letter-spacing: 0.12em;
+ margin: 0;
+ text-transform: uppercase;
+}
+
+h1,
+h2,
+h3,
+p {
+ letter-spacing: 0;
+ margin: 0;
+}
+
+h1 {
+ font-size: clamp(2rem, 5vw, 3.5rem);
+ line-height: 1;
}
-.teacher-dashboard__subsection-heading h4,
-.teacher-dashboard__section-heading h3 {
- letter-spacing: -0.01em;
+h2 {
+ font-size: clamp(1.2rem, 3vw, 1.55rem);
}
-.teacher-dashboard__link-button,
-.teacher-dashboard__text-link,
-.teacher-dashboard__primary-button,
-.teacher-dashboard__secondary-button,
-.teacher-dashboard__control {
- transition:
- background-color 140ms ease,
- border-color 140ms ease,
- color 140ms ease,
- box-shadow 140ms ease,
- transform 140ms ease;
+h3 {
+ font-size: 1rem;
}
-.teacher-dashboard__link-button {
- border-radius: 9999px;
- padding: 0.45rem 0.85rem;
+.teach-home__header p,
+.teach-home__row p,
+.teach-home__course p,
+.teach-home__note,
+.teach-home__limits,
+.teach-home__muted {
+ color: var(--ee-text-soft, #40546b);
+ line-height: 1.6;
}
-.teacher-dashboard__link-button:hover,
-.teacher-dashboard__link-button:focus-visible {
- background: rgba(14, 116, 144, 0.08);
+.teach-home__quick-nav,
+.teach-home__actions,
+.teach-home__course-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.6rem;
}
-.teacher-dashboard__text-link {
- font-weight: 600;
+.teach-home a,
+.teach-home button {
+ border-radius: var(--ee-radius-control-default, 8px);
+ font-weight: 850;
+ min-height: 2.75rem;
+}
+
+.teach-home a {
+ align-items: center;
+ background: var(--ee-color-action-secondary-background, #fffdf8);
+ border: 1px solid var(--ee-color-action-secondary-border, #cdbda3);
+ color: var(--ee-action-primary, #0f766e);
+ display: inline-flex;
+ padding: 0.65rem 0.9rem;
text-decoration: none;
}
-.teacher-dashboard__text-link:hover,
-.teacher-dashboard__text-link:focus-visible {
- text-decoration: underline;
- text-underline-offset: 0.2rem;
+.teach-home button {
+ background: var(--ee-surface-muted, #f4eadc);
+ border: 1px solid var(--ee-border, #dccdb7);
+ color: var(--ee-action-primary, #0f766e);
+ cursor: pointer;
+ padding: 0.65rem 0.9rem;
}
-.teacher-dashboard__control {
- color: #0f172a;
+.teach-home a:focus-visible,
+.teach-home button:focus-visible {
+ outline: 3px solid var(--ee-focus-ring, #0f766e);
+ outline-offset: 3px;
}
-.teacher-dashboard__control:hover {
- border-color: rgba(14, 116, 144, 0.32);
+.teach-home__priority-grid,
+.teach-home__course-grid {
+ display: grid;
+ gap: 1rem;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
}
-.teacher-dashboard__control:focus-visible,
-.teacher-dashboard__link-button:focus-visible,
-.teacher-dashboard__text-link:focus-visible,
-.teacher-dashboard__primary-button:focus-visible,
-.teacher-dashboard__secondary-button:focus-visible {
- outline: none;
- box-shadow: 0 0 0 3px rgba(14, 116, 144, 0.18);
+.teach-home__priority,
+.teach-home__course,
+.teach-home__panel {
+ display: grid;
+ gap: 0.85rem;
+ padding: clamp(1rem, 2vw, 1.25rem);
}
-.teacher-dashboard__primary-button:hover,
-.teacher-dashboard__secondary-button:hover {
- transform: translateY(-1px);
+.teach-home__priority strong {
+ color: var(--ee-action-primary, #0f766e);
+ font-size: 2rem;
+ line-height: 1;
}
-.teacher-dashboard__primary-button {
- background: linear-gradient(135deg, var(--color-primary, #0e7490), #155e75);
+.teach-home__layout {
+ display: grid;
+ gap: 1rem;
+ grid-template-columns: minmax(0, 1.55fr) minmax(18rem, 0.75fr);
}
-.teacher-dashboard__primary-button:hover {
- filter: brightness(1.02);
+.teach-home__panel-heading,
+.teach-home__row {
+ align-items: start;
+ display: flex;
+ gap: 1rem;
+ justify-content: space-between;
}
-.teacher-dashboard__secondary-button {
- border: 1px solid rgba(14, 116, 144, 0.2);
- background: #fff;
+.teach-home__data-list {
+ display: grid;
+ gap: 0.75rem;
}
-.teacher-dashboard__secondary-button:hover {
- border-color: rgba(14, 116, 144, 0.32);
- background: rgba(14, 116, 144, 0.04);
+.teach-home__row {
+ background: var(--ee-surface-muted, #f8fafc);
+ border: 1px solid var(--ee-border, #dccdb7);
+ border-radius: var(--ee-radius, 6px);
+ padding: 0.9rem;
}
-.teacher-dashboard__secondary-button--accent {
- border-color: rgba(245, 158, 11, 0.2);
- background: linear-gradient(135deg, rgba(252, 211, 77, 0.75), rgba(254, 240, 138, 0.82));
+.teach-home__progress {
+ display: grid;
+ gap: 0.35rem;
+ min-width: 11rem;
}
-.teacher-dashboard__secondary-button--accent:hover {
- border-color: rgba(217, 119, 6, 0.35);
- background: linear-gradient(135deg, rgba(252, 211, 77, 0.92), rgba(254, 240, 138, 0.96));
+.teach-home__progress span:first-child {
+ color: var(--ee-action-primary, #0f766e);
+ font-weight: 900;
}
-@media (max-width: 1023px) {
- .teacher-dashboard__section-heading::after {
- margin-top: 0.75rem;
+.teach-home__progress span:nth-child(2) {
+ color: var(--ee-text-muted, #5f6f7f);
+ font-size: 0.8rem;
+ font-weight: 800;
+}
+
+.teach-home__track {
+ background: var(--ee-border, #dccdb7);
+ border-radius: 999px;
+ height: 0.55rem;
+ overflow: hidden;
+}
+
+.teach-home__track div {
+ background: var(--ee-action-primary, #0f766e);
+ height: 100%;
+}
+
+.teach-home__limits {
+ display: grid;
+ gap: 0.7rem;
+}
+
+@media (max-width: 980px) {
+ .teach-home__header,
+ .teach-home__layout {
+ grid-template-columns: 1fr;
+ }
+
+ .teach-home__priority-grid,
+ .teach-home__course-grid {
+ grid-template-columns: 1fr;
}
}
-@media (max-width: 767px) {
- .teacher-dashboard__hero-panel {
- padding: 1rem;
+@media (max-width: 520px) {
+ .teach-home {
+ padding: 0.75rem;
+ }
+
+ .teach-home__panel-heading,
+ .teach-home__row {
+ display: grid;
}
- .teacher-dashboard__link-button {
- padding-inline: 0;
+ .teach-home__quick-nav a,
+ .teach-home__actions a,
+ .teach-home__course-actions a,
+ .teach-home button {
+ justify-content: center;
+ width: 100%;
}
}
diff --git a/frontend/src/app/pages/user-dashboard/teacher-view/teacher-view.component.spec.ts b/frontend/src/app/pages/user-dashboard/teacher-view/teacher-view.component.spec.ts
index af08cf0..c182c20 100644
--- a/frontend/src/app/pages/user-dashboard/teacher-view/teacher-view.component.spec.ts
+++ b/frontend/src/app/pages/user-dashboard/teacher-view/teacher-view.component.spec.ts
@@ -11,44 +11,13 @@ import { ToastService } from '../../../services/toast.service';
class MockCoursesService {
coursesResponse = of
([]);
- getCourses = jasmine.createSpy('getCourses').and.callFake(() => this.coursesResponse);
- publishReadinessResponse = of({
- course_id: 'course-1',
- course_title: 'Introduction to Africa',
- is_ready: true,
- blocking_issue_count: 0,
- warning_count: 0,
- blocking_issues: [],
- warnings: [],
- });
- getCoursePublishReadiness = jasmine.createSpy('getCoursePublishReadiness').and.callFake(() => this.publishReadinessResponse);
- safePublishResponse = of({
- course_id: 'course-1',
- course_title: 'Introduction to Africa',
- is_safe: true,
- blocking_issue_count: 0,
- warning_count: 0,
- blocking_issues: [],
- warnings: [],
- });
- getCourseSafePublishValidation = jasmine.createSpy('getCourseSafePublishValidation').and.callFake(() => this.safePublishResponse);
- competencyIntegrityResponse = of({
- course_id: 'course-1',
- course_title: 'Introduction to Africa',
- is_valid: true,
- is_explainable: true,
- blocking_issue_count: 0,
- warning_count: 0,
- blocking_issues: [],
- warnings: [],
- affected_assessments: [],
- affected_competency_identifiers: [],
- });
- getCourseCompetencyEvidenceIntegrity = jasmine.createSpy('getCourseCompetencyEvidenceIntegrity').and.callFake(() => this.competencyIntegrityResponse);
- runtimeInterventionResponse = of([]);
- getCourseRuntimeInterventionRecommendations = jasmine.createSpy('getCourseRuntimeInterventionRecommendations').and.callFake(() => this.runtimeInterventionResponse);
governanceSummaryResponse = of(buildGovernanceSummary());
+ getCourses = jasmine.createSpy('getCourses').and.callFake(() => this.coursesResponse);
getCourseGovernanceSummary = jasmine.createSpy('getCourseGovernanceSummary').and.callFake(() => this.governanceSummaryResponse);
+ getCoursePublishReadiness = jasmine.createSpy('getCoursePublishReadiness').and.returnValue(of({}));
+ getCourseSafePublishValidation = jasmine.createSpy('getCourseSafePublishValidation').and.returnValue(of({}));
+ getCourseCompetencyEvidenceIntegrity = jasmine.createSpy('getCourseCompetencyEvidenceIntegrity').and.returnValue(of({}));
+ getCourseRuntimeInterventionRecommendations = jasmine.createSpy('getCourseRuntimeInterventionRecommendations').and.returnValue(of([]));
deleteCourse = jasmine.createSpy('deleteCourse').and.returnValue(of({}));
assignCourseToStudent = jasmine.createSpy('assignCourseToStudent').and.returnValue(of({}));
}
@@ -71,53 +40,14 @@ class MockToastService {
function buildGovernanceSummary(overrides: Partial = {}) {
return {
- course_id: overrides['course_id'] || 'course-1',
- course_title: overrides['course_title'] || 'Introduction to Africa',
- publish_readiness: {
- course_id: 'course-1',
- course_title: 'Introduction to Africa',
- is_ready: true,
- blocking_issue_count: 0,
- warning_count: 0,
- blocking_issues: [],
- warnings: [],
- ...(overrides['publish_readiness'] || {}),
- },
- safe_publish_validation: {
- course_id: 'course-1',
- course_title: 'Introduction to Africa',
- is_safe: true,
- blocking_issue_count: 0,
- warning_count: 0,
- blocking_issues: [],
- warnings: [],
- ...(overrides['safe_publish_validation'] || {}),
- },
- lineage_safety_visibility: {
- course_id: 'course-1',
- course_title: 'Introduction to Africa',
- is_coherent: true,
- is_safe: true,
- blocking_issue_count: 0,
- warning_count: 0,
- blocking_issues: [],
- warnings: [],
- ...(overrides['lineage_safety_visibility'] || {}),
- },
- competency_evidence_integrity: {
- course_id: 'course-1',
- course_title: 'Introduction to Africa',
- is_valid: true,
- is_explainable: true,
- blocking_issue_count: 0,
- warning_count: 0,
- blocking_issues: [],
- warnings: [],
- affected_assessments: [],
- affected_competency_identifiers: [],
- ...(overrides['competency_evidence_integrity'] || {}),
- },
- runtime_intervention_recommendations: overrides['runtime_intervention_recommendations'] || [],
+ course_id: 'course-1',
+ course_title: 'Introduction to Africa',
+ publish_readiness: { course_id: 'course-1', course_title: 'Introduction to Africa', is_ready: true, blocking_issue_count: 0, warning_count: 0, blocking_issues: [], warnings: [] },
+ safe_publish_validation: { course_id: 'course-1', course_title: 'Introduction to Africa', is_safe: true, blocking_issue_count: 0, warning_count: 0, blocking_issues: [], warnings: [] },
+ lineage_safety_visibility: { course_id: 'course-1', course_title: 'Introduction to Africa', is_coherent: true, is_safe: true, blocking_issue_count: 0, warning_count: 0, blocking_issues: [], warnings: [] },
+ competency_evidence_integrity: { course_id: 'course-1', course_title: 'Introduction to Africa', is_valid: true, is_explainable: true, blocking_issue_count: 0, warning_count: 0, blocking_issues: [], warnings: [], affected_assessments: [], affected_competency_identifiers: [] },
+ runtime_intervention_recommendations: [],
+ ...overrides,
};
}
@@ -127,8 +57,9 @@ describe('TeacherViewComponent', () => {
let coursesService: MockCoursesService;
let analyticsService: MockAnalyticsService;
let usersService: MockUsersService;
+ let toastService: MockToastService;
- async function createComponent() {
+ async function createComponent(role = 'teacher') {
await TestBed.configureTestingModule({
imports: [RouterTestingModule, HttpClientTestingModule, TeacherViewComponent],
providers: [
@@ -141,514 +72,170 @@ describe('TeacherViewComponent', () => {
fixture = TestBed.createComponent(TeacherViewComponent);
component = fixture.componentInstance;
- component.userInfo = { fullname: 'Teacher Test', role: 'teacher' } as any;
+ component.userInfo = { fullname: 'Teacher Test', role } as any;
coursesService = TestBed.inject(CoursesService) as unknown as MockCoursesService;
analyticsService = TestBed.inject(AnalyticsService) as unknown as MockAnalyticsService;
usersService = TestBed.inject(UsersService) as unknown as MockUsersService;
+ toastService = TestBed.inject(ToastService) as unknown as MockToastService;
}
beforeEach(async () => {
await createComponent();
});
- it('uses EchoLoadingState while teacher data is loading', () => {
- const pendingCourses = new Subject();
- const pendingStudents = new Subject();
- const pendingSummary = new Subject();
- const pendingRuntimeSupport = new Subject();
- const pendingGovernanceSummary = new Subject();
- coursesService.coursesResponse = pendingCourses.asObservable();
- coursesService.governanceSummaryResponse = pendingGovernanceSummary.asObservable();
- usersService.studentsResponse = pendingStudents.asObservable();
- analyticsService.summaryResponse = pendingSummary.asObservable();
- analyticsService.runtimeSupportResponse = pendingRuntimeSupport.asObservable();
-
- fixture.detectChanges();
-
- const compiled = fixture.nativeElement as HTMLElement;
- expect(compiled.querySelector('[data-echo-loading="section"]')).not.toBeNull();
- expect(compiled.textContent).toContain('Loading course management');
- expect(compiled.textContent).toContain('Loading student progress');
- expect(compiled.textContent).toContain('Loading runtime support');
- expect(compiled.textContent).toContain('Loading publish readiness');
- expect(compiled.textContent).toContain('Loading safe publish validation');
- expect(compiled.textContent).toContain('Loading competency evidence integrity');
- expect(compiled.textContent).toContain('Loading runtime intervention recommendations');
- });
-
- it('uses EchoStatePanel for teacher empty states', () => {
- coursesService.coursesResponse = of([]);
- fixture.detectChanges();
-
- const compiled = fixture.nativeElement as HTMLElement;
- const emptyStates = compiled.querySelectorAll('[data-echo-state="empty"]');
- expect(emptyStates.length).toBeGreaterThan(0);
- expect(compiled.textContent).toContain('Course management will appear here');
- expect(compiled.textContent).toContain('Student progress will appear here');
- expect(compiled.textContent).toContain('Flagship runtime support is not available yet');
- expect(compiled.textContent).toContain('Publish readiness will appear here');
- expect(compiled.textContent).toContain('Safe publish validation will appear here');
- expect(compiled.textContent).toContain('Competency evidence integrity will appear here');
- expect(compiled.textContent).toContain('Runtime intervention recommendations will appear here');
- });
-
- it('uses EchoStatePanel for teacher error states and retries data loads', () => {
- coursesService.coursesResponse = throwError(() => new Error('courses failed'));
- usersService.studentsResponse = throwError(() => new Error('students failed'));
- analyticsService.summaryResponse = throwError(() => new Error('summary failed'));
- analyticsService.runtimeSupportResponse = throwError(() => new Error('runtime support failed'));
-
- fixture.detectChanges();
- expect(coursesService.getCourses).toHaveBeenCalledTimes(1);
- expect(usersService.getStudents).toHaveBeenCalledTimes(1);
- expect(analyticsService.getTeacherSummary).toHaveBeenCalledTimes(1);
- expect(analyticsService.getEducatorRuntimeSupport).not.toHaveBeenCalled();
- expect(coursesService.getCourseGovernanceSummary).not.toHaveBeenCalled();
-
- coursesService.coursesResponse = of([{ id: 'course-1', title: 'Introduction to Africa', standards_metadata: { pathway_key: 'introduction-to-africa' } }]);
- coursesService.governanceSummaryResponse = of(buildGovernanceSummary());
- usersService.studentsResponse = of([{ id: 'student-1', firstname: 'Ada', lastname: 'Lovelace' }]);
- analyticsService.summaryResponse = of([{ student_name: 'Ada Lovelace', course_title: 'Algebra I', progress: 50, status: 'active', last_active: '2026-05-01T00:00:00.000Z' }]);
- analyticsService.runtimeSupportResponse = of([]);
+ function text(): string {
+ return (fixture.nativeElement as HTMLElement).textContent || '';
+ }
- const buttons = Array.from(fixture.nativeElement.querySelectorAll('button')) as HTMLButtonElement[];
- buttons.filter((button) => button.textContent?.trim() === 'Retry').forEach((button) => button.click());
+ it('renders the canonical Teach overview for teacher state', () => {
fixture.detectChanges();
-
- expect(coursesService.getCourses).toHaveBeenCalledTimes(3);
- expect(usersService.getStudents).toHaveBeenCalledTimes(2);
- expect(analyticsService.getTeacherSummary).toHaveBeenCalledTimes(2);
- expect(analyticsService.getEducatorRuntimeSupport).toHaveBeenCalledTimes(2);
- expect(coursesService.getCourseGovernanceSummary).toHaveBeenCalledWith('course-1');
- expect(coursesService.getCoursePublishReadiness).not.toHaveBeenCalled();
- expect(coursesService.getCourseSafePublishValidation).not.toHaveBeenCalled();
- expect(coursesService.getCourseCompetencyEvidenceIntegrity).not.toHaveBeenCalled();
- expect(coursesService.getCourseRuntimeInterventionRecommendations).not.toHaveBeenCalled();
+ expect(text()).toContain('EchoEd Teach');
+ expect(text()).toContain('Today in Teach');
+ expect(text()).toContain('My classes');
+ expect(text()).toContain('Browse curriculum');
+ expect(text()).toContain('Assignments');
});
- it('demotes off-script teacher actions while keeping the dashboard usable', () => {
- coursesService.coursesResponse = of([{ id: 'course-1', title: 'Introduction to Africa', standards_metadata: { pathway_key: 'introduction-to-africa' } }]);
- usersService.studentsResponse = of([{ id: 'student-1', firstname: 'Ada', lastname: 'Lovelace' }]);
- analyticsService.summaryResponse = of([{ student_name: 'Ada Lovelace', course_title: 'Algebra I', progress: 50, status: 'active', last_active: '2026-05-01T00:00:00.000Z' }]);
- analyticsService.runtimeSupportResponse = of([]);
-
+ it('renders the same Teach overview for instructor-compatible state', async () => {
+ TestBed.resetTestingModule();
+ await createComponent('instructor');
fixture.detectChanges();
- const compiled = fixture.nativeElement as HTMLElement;
- expect(compiled.textContent).toContain('Demo-safe view');
- expect(compiled.textContent).toContain('Advanced actions');
- expect(compiled.textContent).toContain('Off-script');
- expect(compiled.textContent).toContain('Assign Course');
- expect(compiled.textContent).toContain('Start Live Lesson');
- expect(compiled.textContent).toContain('View Reports');
- expect(compiled.textContent).not.toContain('Add Course');
- expect(compiled.textContent).not.toContain('Delete');
- expect(compiled.textContent).not.toContain('Publish Course');
+ expect(text()).toContain('Today in Teach');
+ expect(component.userInfo.role).toBe('instructor');
});
- it('gives runtime intervention and runtime support distinct framing', () => {
- coursesService.coursesResponse = of([{ id: 'course-1', title: 'Introduction to Africa', standards_metadata: { pathway_key: 'introduction-to-africa' } }]);
- usersService.studentsResponse = of([{ id: 'student-1', firstname: 'Ada', lastname: 'Lovelace' }]);
- analyticsService.summaryResponse = of([]);
- analyticsService.runtimeSupportResponse = of([]);
- coursesService.governanceSummaryResponse = of(buildGovernanceSummary());
-
+ it('uses shared loading states while teacher data is loading', () => {
+ coursesService.coursesResponse = new Subject().asObservable();
+ usersService.studentsResponse = new Subject().asObservable();
+ analyticsService.summaryResponse = new Subject().asObservable();
fixture.detectChanges();
-
- const compiled = fixture.nativeElement as HTMLElement;
- expect(compiled.textContent).toContain('Course-level recommendation detail for each learner, including evidence basis, confidence, and caution flags.');
- expect(compiled.textContent).toContain('Flagship spotlight for fast live-demo narration about who needs support, who is ready for extension, and who is moving steadily.');
+ expect(text()).toContain('Loading learner progress');
+ expect(text()).toContain('Loading curriculum');
});
- it('renders responsive fallback markup for educator tables', () => {
- coursesService.coursesResponse = of([{ id: 'course-1', title: 'Introduction to Africa', standards_metadata: { pathway_key: 'introduction-to-africa' } }]);
- usersService.studentsResponse = of([{ id: 'student-1', firstname: 'Ada', lastname: 'Lovelace' }]);
- analyticsService.summaryResponse = of([{ student_name: 'Ada Lovelace', course_title: 'Algebra I', progress: 50, status: 'active', last_active: '2026-05-01T00:00:00.000Z' }]);
- analyticsService.runtimeSupportResponse = of([]);
-
+ it('renders no classes and no assignments entry state without inventing counts', () => {
fixture.detectChanges();
- const compiled = fixture.nativeElement as HTMLElement;
- expect(compiled.querySelector('[data-educator-responsive="teacher-courses"]')).not.toBeNull();
- expect(compiled.querySelector('[data-educator-responsive="teacher-summary"]')).not.toBeNull();
+ expect(text()).toContain('No progress yet');
+ expect(text()).toContain('No curriculum is available');
+ expect(text()).toContain('Create or inspect assignments');
});
- it('renders publish readiness ready state in the teacher staff surface', () => {
- coursesService.coursesResponse = of([{ id: 'course-1', title: 'Introduction to Africa', standards_metadata: { pathway_key: 'introduction-to-africa' } }]);
- coursesService.governanceSummaryResponse = of(buildGovernanceSummary({
- publish_readiness: {
- warning_count: 1,
- warnings: [{ entity_type: 'unit', entity_id: 'unit-1', entity_title: 'Unit One', code: 'empty_unit', message: 'Unit does not contain any lessons yet.' }],
- },
- }));
- usersService.studentsResponse = of([{ id: 'student-1', firstname: 'Ada', lastname: 'Lovelace' }]);
- analyticsService.summaryResponse = of([]);
- analyticsService.runtimeSupportResponse = of([]);
-
+ it('renders needs-review limitations from supported data only', () => {
fixture.detectChanges();
-
- expect(coursesService.getCourseGovernanceSummary).toHaveBeenCalledWith('course-1');
- expect(coursesService.getCoursePublishReadiness).not.toHaveBeenCalled();
- const compiled = fixture.nativeElement as HTMLElement;
- expect(compiled.textContent).toContain('Publish readiness');
- expect(compiled.textContent).toContain('Introduction to Africa');
- expect(compiled.textContent).toContain('Ready');
- expect(compiled.textContent).toContain('1 warning');
+ expect(text()).toContain('Feedback, manual scoring, and discussion moderation are shown only where current APIs support them.');
+ expect(text()).toContain('thread and post APIs exist, but they are not scoped to classes or moderation permissions.');
});
- it('renders blocked publish readiness issues with retry and no mutation actions', () => {
- coursesService.coursesResponse = of([{ id: 'course-1', title: 'Introduction to Africa', standards_metadata: { pathway_key: 'introduction-to-africa' } }]);
- coursesService.governanceSummaryResponse = throwError(() => new Error('readiness failed'));
- usersService.studentsResponse = of([{ id: 'student-1', firstname: 'Ada', lastname: 'Lovelace' }]);
- analyticsService.summaryResponse = of([]);
- analyticsService.runtimeSupportResponse = of([]);
-
- fixture.detectChanges();
- let compiled = fixture.nativeElement as HTMLElement;
- expect(compiled.textContent).toContain('We could not load course publish readiness');
-
- coursesService.governanceSummaryResponse = of(buildGovernanceSummary({
- publish_readiness: {
- is_ready: false,
- blocking_issue_count: 2,
- warning_count: 0,
- blocking_issues: [
- { entity_type: 'lesson', entity_id: 'lesson-1', entity_title: 'Draft Lesson', code: 'review_status_not_approved', message: 'Lesson must be approved before it is publish-ready for learners.' },
- { entity_type: 'lesson', entity_id: 'lesson-1', entity_title: 'Draft Lesson', code: 'missing_readiness_field', message: 'Lesson is missing required publish-readiness field: sources.' },
- ],
- warnings: [],
- },
- }));
-
- const retryButton = Array.from(compiled.querySelectorAll('button')).find(
- (button) => button.textContent?.trim() === 'Retry',
- ) as HTMLButtonElement;
- retryButton.click();
+ it('renders API failure states and retry actions', () => {
+ coursesService.coursesResponse = throwError(() => new Error('courses failed'));
+ analyticsService.summaryResponse = throwError(() => new Error('summary failed'));
fixture.detectChanges();
-
- compiled = fixture.nativeElement as HTMLElement;
- expect(coursesService.getCourseGovernanceSummary).toHaveBeenCalledTimes(2);
- expect(coursesService.getCoursePublishReadiness).not.toHaveBeenCalled();
- expect(compiled.textContent).toContain('Not ready');
- expect(compiled.textContent).toContain('Draft Lesson');
- expect(coursesService.assignCourseToStudent).not.toHaveBeenCalled();
- expect(coursesService.deleteCourse).not.toHaveBeenCalled();
+ expect(text()).toContain('We could not load learner progress');
+ expect(text()).toContain('We could not load courses');
+ expect((fixture.nativeElement as HTMLElement).querySelectorAll('button').length).toBeGreaterThan(0);
});
- it('renders safe-publish safe state in the teacher staff surface', () => {
- coursesService.coursesResponse = of([{ id: 'course-1', title: 'Introduction to Africa', standards_metadata: { pathway_key: 'introduction-to-africa' } }]);
- coursesService.governanceSummaryResponse = of(buildGovernanceSummary({
- safe_publish_validation: {
- warning_count: 1,
- warnings: [
- { entity_type: 'course', entity_id: 'course-1', entity_title: 'Introduction to Africa', code: 'draft_has_published_at', message: 'Draft revisions should not already have a published timestamp.' },
- ],
- },
- }));
- usersService.studentsResponse = of([{ id: 'student-1', firstname: 'Ada', lastname: 'Lovelace' }]);
- analyticsService.summaryResponse = of([]);
- analyticsService.runtimeSupportResponse = of([]);
-
+ it('keeps unauthorized/admin and Studio links out of teacher navigation content', () => {
fixture.detectChanges();
-
- expect(coursesService.getCourseGovernanceSummary).toHaveBeenCalledWith('course-1');
- expect(coursesService.getCourseSafePublishValidation).not.toHaveBeenCalled();
- const compiled = fixture.nativeElement as HTMLElement;
- expect(compiled.textContent).toContain('Safe publish validation');
- expect(compiled.textContent).toContain('Safe');
- expect(compiled.textContent).toContain('1 warning');
- expect(compiled.querySelector('[data-safe-publish-row]')).not.toBeNull();
+ expect(text()).not.toContain('Admin Overview');
+ expect(text()).not.toContain('Content Studio');
+ expect(text()).not.toContain('Product Studio');
});
- it('renders competency integrity valid and explainable state in the teacher staff surface', () => {
- coursesService.coursesResponse = of([{ id: 'course-1', title: 'Introduction to Africa', standards_metadata: { pathway_key: 'introduction-to-africa' } }]);
- coursesService.governanceSummaryResponse = of(buildGovernanceSummary({
- competency_evidence_integrity: {
- warning_count: 1,
- warnings: [
- { entity_type: 'assessment', entity_id: 'assessment-2', entity_title: 'Practice Quiz', code: 'unaligned_assessment_mastery_evidence', message: 'Assessment evidence is not aligned to a competency.' },
- ],
- },
- }));
- usersService.studentsResponse = of([{ id: 'student-1', firstname: 'Ada', lastname: 'Lovelace' }]);
- analyticsService.summaryResponse = of([]);
- analyticsService.runtimeSupportResponse = of([]);
-
+ it('renders learner progress rows with consistent state language', () => {
+ analyticsService.summaryResponse = of([{ student_name: 'Ada Lovelace', course_title: 'Introduction to Africa', progress: 45, status: 'active', last_active: '2026-05-01T00:00:00.000Z' }]);
fixture.detectChanges();
-
- expect(coursesService.getCourseGovernanceSummary).toHaveBeenCalledWith('course-1');
- expect(coursesService.getCourseCompetencyEvidenceIntegrity).not.toHaveBeenCalled();
- const compiled = fixture.nativeElement as HTMLElement;
- expect(compiled.textContent).toContain('Competency evidence integrity');
- expect(compiled.textContent).toContain('Valid');
- expect(compiled.textContent).toContain('Explainable');
- expect(compiled.textContent).toContain('1 warning');
- expect(compiled.querySelector('[data-competency-integrity-row]')).not.toBeNull();
+ expect(text()).toContain('Ada Lovelace');
+ expect(text()).toContain('Introduction to Africa');
+ expect(text()).toContain('45%');
+ expect(text()).toContain('In progress');
});
- it('renders competency integrity affected assessments, competencies, and issues with retry and no mutation actions', () => {
- coursesService.coursesResponse = of([{ id: 'course-1', title: 'Introduction to Africa', standards_metadata: { pathway_key: 'introduction-to-africa' } }]);
- coursesService.governanceSummaryResponse = throwError(() => new Error('integrity failed'));
- usersService.studentsResponse = of([{ id: 'student-1', firstname: 'Ada', lastname: 'Lovelace' }]);
- analyticsService.summaryResponse = of([]);
- analyticsService.runtimeSupportResponse = of([]);
-
- fixture.detectChanges();
- let compiled = fixture.nativeElement as HTMLElement;
- expect(compiled.textContent).toContain('We could not load competency evidence integrity');
-
- coursesService.governanceSummaryResponse = of(buildGovernanceSummary({
- competency_evidence_integrity: {
- is_valid: false,
- is_explainable: false,
- blocking_issue_count: 1,
- warning_count: 1,
- blocking_issues: [
- { entity_type: 'assessment', entity_id: 'assessment-1', entity_title: 'Final Assessment', code: 'missing_attempt_event_for_mastery_evidence', message: 'Attempt evidence is missing an authoritative event.' },
- ],
- warnings: [
- { entity_type: 'assessment', entity_id: 'assessment-2', entity_title: 'Practice Quiz', code: 'unaligned_assessment_mastery_evidence', message: 'Assessment evidence is not aligned to a competency.' },
- ],
- affected_assessments: [
- { assessment_id: 'assessment-1', assessment_title: 'Final Assessment', competency_identifiers: ['evidence', 'analysis'] },
- { assessment_id: 'assessment-2', assessment_title: 'Practice Quiz', competency_identifiers: [] },
- ],
- affected_competency_identifiers: ['evidence', 'analysis'],
- },
- }));
-
- const retryButtons = Array.from(compiled.querySelectorAll('button')).filter(
- (button) => button.textContent?.trim() === 'Retry',
- ) as HTMLButtonElement[];
- retryButtons[0]?.click();
+ it('renders not-started language for missing activity dates', () => {
+ analyticsService.summaryResponse = of([{ student_name: 'Biko Student', course_title: 'History', progress: 0, status: 'pending', last_active: null }]);
fixture.detectChanges();
-
- compiled = fixture.nativeElement as HTMLElement;
- expect(coursesService.getCourseGovernanceSummary).toHaveBeenCalledTimes(2);
- expect(coursesService.getCourseCompetencyEvidenceIntegrity).not.toHaveBeenCalled();
- expect(compiled.textContent).toContain('Not valid');
- expect(compiled.textContent).toContain('Not explainable');
- expect(compiled.textContent).toContain('Affected competencies');
- expect(compiled.textContent).toContain('evidence, analysis');
- expect(compiled.textContent).toContain('Final Assessment');
- expect(compiled.textContent).toContain('Practice Quiz');
- expect(compiled.textContent).toContain('Attempt evidence is missing an authoritative event.');
- expect(coursesService.assignCourseToStudent).not.toHaveBeenCalled();
- expect(coursesService.deleteCourse).not.toHaveBeenCalled();
+ expect(text()).toContain('No recent activity available');
+ expect(text()).toContain('Pending');
});
- it('renders runtime intervention recommendation states, evidence basis, and caution flags in the teacher staff surface', () => {
- coursesService.coursesResponse = of([{ id: 'course-1', title: 'Introduction to Africa', standards_metadata: { pathway_key: 'introduction-to-africa' } }]);
- coursesService.governanceSummaryResponse = of(buildGovernanceSummary({
- runtime_intervention_recommendations: [
- {
- student_id: 'student-1',
- student_name: 'Ada Lovelace',
- student_course_id: 'student-course-1',
- course_id: 'course-1',
- course_title: 'Introduction to Africa',
- recommendation_state: 'reteach',
- educator_attention_level: 'high',
- summary: 'Recent evidence suggests a brief reteach cycle may help.',
- evidence_basis: [
- {
- source: 'attempt_events',
- detail: 'Recent attempts show repeated weak evidence.',
- assessment_id: 'assessment-1',
- assessment_title: 'Exit Check',
- competency_identifiers: ['analysis'],
- },
- ],
- confidence_level: 'high',
- caution_flags: ['incomplete_evidence'],
- learner_safe_message: 'Rebuild understanding with calm support and one step at a time.',
- },
- ],
- }));
- usersService.studentsResponse = of([{ id: 'student-1', firstname: 'Ada', lastname: 'Lovelace' }]);
- analyticsService.summaryResponse = of([]);
- analyticsService.runtimeSupportResponse = of([]);
-
+ it('calculates active learning and average completion from teacher summary rows', () => {
+ analyticsService.summaryResponse = of([
+ { student_name: 'One', course_title: 'Course', progress: 50, status: 'active', last_active: null },
+ { student_name: 'Two', course_title: 'Course', progress: 100, status: 'completed', last_active: null },
+ ]);
fixture.detectChanges();
-
- expect(coursesService.getCourseGovernanceSummary).toHaveBeenCalledWith('course-1');
- expect(coursesService.getCourseRuntimeInterventionRecommendations).not.toHaveBeenCalled();
- const compiled = fixture.nativeElement as HTMLElement;
- expect(compiled.textContent).toContain('Runtime intervention recommendations');
- expect(compiled.textContent).toContain('Ada Lovelace');
- expect(compiled.textContent).toContain('Reteach');
- expect(compiled.textContent).toContain('High attention');
- expect(compiled.textContent).toContain('Evidence basis');
- expect(compiled.textContent).toContain('Attempt Events');
- expect(compiled.textContent).toContain('Exit Check');
- expect(compiled.textContent).toContain('Caution flags');
- expect(compiled.textContent).toContain('Incomplete Evidence');
- expect(compiled.querySelector('[data-runtime-intervention-row]')).not.toBeNull();
- const runtimeButtons = compiled.querySelectorAll('[data-runtime-intervention-row] button');
- expect(runtimeButtons.length).toBe(0);
+ expect(text()).toContain('Active learning1');
+ expect(text()).toContain('Average progress75%');
});
- it('renders runtime intervention error state and retries read-only loading in the teacher staff surface', () => {
- coursesService.coursesResponse = of([{ id: 'course-1', title: 'Introduction to Africa', standards_metadata: { pathway_key: 'introduction-to-africa' } }]);
- coursesService.governanceSummaryResponse = throwError(() => new Error('runtime intervention failed'));
- usersService.studentsResponse = of([{ id: 'student-1', firstname: 'Ada', lastname: 'Lovelace' }]);
- analyticsService.summaryResponse = of([]);
- analyticsService.runtimeSupportResponse = of([]);
-
+ it('renders curriculum cards with preview and assign actions', () => {
+ coursesService.coursesResponse = of([{ id: 'course-1', title: 'Introduction to Africa', description: 'Course description', standards_metadata: { pathway_key: 'introduction-to-africa' } }]);
fixture.detectChanges();
- let compiled = fixture.nativeElement as HTMLElement;
- expect(compiled.textContent).toContain('We could not load runtime intervention recommendations');
-
- coursesService.governanceSummaryResponse = of(buildGovernanceSummary());
- const retryButtons = Array.from(compiled.querySelectorAll('button')).filter(
- (button) => button.textContent?.trim() === 'Retry',
- ) as HTMLButtonElement[];
- retryButtons[0]?.click();
- fixture.detectChanges();
-
- compiled = fixture.nativeElement as HTMLElement;
- expect(coursesService.getCourseGovernanceSummary).toHaveBeenCalledTimes(2);
- expect(coursesService.getCourseRuntimeInterventionRecommendations).not.toHaveBeenCalled();
- expect(coursesService.assignCourseToStudent).not.toHaveBeenCalled();
- expect(coursesService.deleteCourse).not.toHaveBeenCalled();
- expect(compiled.textContent).toContain('Runtime intervention recommendations will appear here');
+ expect(text()).toContain('Introduction to Africa');
+ expect(text()).toContain('Course description');
+ expect(text()).toContain('Preview course');
+ expect(text()).toContain('Assign learning');
});
- it('renders blocked safe-publish issues with retry and no mutation actions', () => {
+ it('does not call student progress creation APIs from the overview', () => {
coursesService.coursesResponse = of([{ id: 'course-1', title: 'Introduction to Africa', standards_metadata: { pathway_key: 'introduction-to-africa' } }]);
- coursesService.governanceSummaryResponse = throwError(() => new Error('safe publish failed'));
- usersService.studentsResponse = of([{ id: 'student-1', firstname: 'Ada', lastname: 'Lovelace' }]);
- analyticsService.summaryResponse = of([]);
- analyticsService.runtimeSupportResponse = of([]);
-
- fixture.detectChanges();
- let compiled = fixture.nativeElement as HTMLElement;
- expect(compiled.textContent).toContain('We could not load course safe-publish validation');
-
- coursesService.governanceSummaryResponse = of(buildGovernanceSummary({
- safe_publish_validation: {
- is_safe: false,
- blocking_issue_count: 1,
- warning_count: 1,
- blocking_issues: [
- { entity_type: 'course', entity_id: 'course-1', entity_title: 'Introduction to Africa', code: 'blocked_revision_status', message: 'Deprecated or archived content is not safe to republish for learners.' },
- ],
- warnings: [
- { entity_type: 'course', entity_id: 'course-1', entity_title: 'Introduction to Africa', code: 'draft_has_published_at', message: 'Draft revisions should not already have a published timestamp.' },
- ],
- },
- }));
-
- const retryButtons = Array.from(compiled.querySelectorAll('button')).filter(
- (button) => button.textContent?.trim() === 'Retry',
- ) as HTMLButtonElement[];
- retryButtons[0]?.click();
fixture.detectChanges();
-
- compiled = fixture.nativeElement as HTMLElement;
- expect(coursesService.getCourseGovernanceSummary).toHaveBeenCalledTimes(2);
- expect(coursesService.getCourseSafePublishValidation).not.toHaveBeenCalled();
- expect(compiled.textContent).toContain('Not safe');
- expect(compiled.textContent).toContain('Deprecated or archived content is not safe to republish for learners.');
+ expect(coursesService.getCourses).toHaveBeenCalled();
expect(coursesService.assignCourseToStudent).not.toHaveBeenCalled();
expect(coursesService.deleteCourse).not.toHaveBeenCalled();
});
- it('loads runtime support for the flagship course and renders remediation context', () => {
+ it('still loads flagship runtime support for supported educator guidance data', () => {
coursesService.coursesResponse = of([{ id: 'course-1', title: 'Introduction to Africa', standards_metadata: { pathway_key: 'introduction-to-africa' } }]);
- usersService.studentsResponse = of([{ id: 'student-1', firstname: 'Ada', lastname: 'Lovelace' }]);
- analyticsService.summaryResponse = of([{ student_name: 'Ada Lovelace', course_title: 'Introduction to Africa', progress: 50, status: 'active', last_active: '2026-05-01T00:00:00.000Z' }]);
- analyticsService.runtimeSupportResponse = of([{
- student_id: 'student-1',
- student_name: 'Ada Lovelace',
- student_course_id: 'student-course-1',
- course_id: 'course-1',
- course_title: 'Introduction to Africa',
- support_state: 'remediation',
- support_summary: 'Recent evidence suggests the learner may benefit from brief review in Introduction to Africa.',
- evidence_source: 'recent_assessment_retry',
- recommended_action: 'review-and-continue',
- last_evidence_at: '2026-05-01T00:00:00.000Z',
- context_lesson_id: 'lesson-1',
- context_lesson_title: 'Introduction to Africa',
- context_key_concepts: ['continent', 'diversity'],
- context_prompts: ['Revisit one idea from Introduction to Africa.'],
- educator_intervention_hint: 'Offer a brief guided discussion.',
- }]);
-
fixture.detectChanges();
-
expect(analyticsService.getEducatorRuntimeSupport).toHaveBeenCalledWith('course-1');
- const compiled = fixture.nativeElement as HTMLElement;
- expect(compiled.textContent).toContain('Runtime Support');
- expect(compiled.textContent).toContain('Needs review support');
- expect(compiled.textContent).toContain('Ada Lovelace');
- expect(compiled.textContent).toContain('Introduction to Africa');
- expect(compiled.textContent).toContain('Revisit one idea from Introduction to Africa.');
- expect(compiled.textContent).not.toContain('review-and-continue');
});
- it('renders enrichment context with educator-safe summary', () => {
+ it('handles course governance summary failures without exposing governance UI', () => {
coursesService.coursesResponse = of([{ id: 'course-1', title: 'Introduction to Africa', standards_metadata: { pathway_key: 'introduction-to-africa' } }]);
- usersService.studentsResponse = of([{ id: 'student-1', firstname: 'Biko', lastname: 'Extension' }]);
- analyticsService.summaryResponse = of([{ student_name: 'Biko Extension', course_title: 'Introduction to Africa', progress: 95, status: 'active', last_active: '2026-05-02T00:00:00.000Z' }]);
- analyticsService.runtimeSupportResponse = of([{
- student_id: 'student-1',
- student_name: 'Biko Extension',
- student_course_id: 'student-course-1',
- course_id: 'course-1',
- course_title: 'Introduction to Africa',
- support_state: 'enrichment',
- support_summary: 'Recent mastery evidence is strong enough to support optional enrichment.',
- evidence_source: 'strong_mastery_signal',
- recommended_action: 'continue-with-enrichment',
- last_evidence_at: '2026-05-02T00:00:00.000Z',
- context_lesson_id: 'lesson-1',
- context_lesson_title: 'Introduction to Africa',
- context_key_concepts: ['curiosity'],
- context_prompts: ['Look a little deeper at Introduction to Africa.'],
- educator_intervention_hint: null,
- }]);
-
+ coursesService.governanceSummaryResponse = throwError(() => new Error('governance failed'));
fixture.detectChanges();
-
- const compiled = fixture.nativeElement as HTMLElement;
- expect(compiled.textContent).toContain('Ready for enrichment');
- expect(compiled.textContent).toContain('Recent mastery evidence is strong enough to support optional enrichment.');
- expect(compiled.textContent).toContain('Look a little deeper at Introduction to Africa.');
+ expect(coursesService.getCourseGovernanceSummary).toHaveBeenCalledWith('course-1');
+ expect(text()).not.toContain('Safe publish validation');
});
- it('renders runtime support empty state when the flagship course has no support rows', () => {
- coursesService.coursesResponse = of([{ id: 'course-1', title: 'Introduction to Africa', standards_metadata: { pathway_key: 'introduction-to-africa' } }]);
- usersService.studentsResponse = of([{ id: 'student-1', firstname: 'Ada', lastname: 'Lovelace' }]);
- analyticsService.summaryResponse = of([]);
- analyticsService.runtimeSupportResponse = of([]);
-
+ it('uses responsive educator data-list markers for progress and courses', () => {
+ coursesService.coursesResponse = of([{ id: 'course-1', title: 'Introduction to Africa' }]);
+ analyticsService.summaryResponse = of([{ student_name: 'Ada Lovelace', course_title: 'Course', progress: 40, status: 'active', last_active: null }]);
fixture.detectChanges();
-
const compiled = fixture.nativeElement as HTMLElement;
- expect(compiled.textContent).toContain('Runtime support will appear here');
+ expect(compiled.querySelector('[data-educator-responsive="teacher-summary"]')).not.toBeNull();
+ expect(compiled.querySelector('[data-educator-responsive="teacher-courses"]')).not.toBeNull();
});
- it('renders runtime support error state and retries without introducing mutations', () => {
- coursesService.coursesResponse = of([{ id: 'course-1', title: 'Introduction to Africa', standards_metadata: { pathway_key: 'introduction-to-africa' } }]);
- usersService.studentsResponse = of([]);
- analyticsService.summaryResponse = of([]);
- analyticsService.runtimeSupportResponse = throwError(() => new Error('runtime failed'));
-
+ it('refreshes teacher summary from the visible Refresh control', () => {
fixture.detectChanges();
+ const refresh = Array.from((fixture.nativeElement as HTMLElement).querySelectorAll('button')).find(button => button.textContent?.trim() === 'Refresh') as HTMLButtonElement;
+ refresh.click();
+ expect(analyticsService.getTeacherSummary).toHaveBeenCalledTimes(2);
+ });
- expect(analyticsService.getEducatorRuntimeSupport).toHaveBeenCalledTimes(1);
- const compiled = fixture.nativeElement as HTMLElement;
- expect(compiled.textContent).toContain('We could not load learner support visibility');
+ it('validates quick assignment action without duplicate submission', () => {
+ component.assignCourse();
+ expect(toastService.show).toHaveBeenCalledWith('Select a student and a course to assign.', 'info');
+ expect(coursesService.assignCourseToStudent).not.toHaveBeenCalled();
+ });
- analyticsService.runtimeSupportResponse = of([]);
- const retryButton = Array.from(compiled.querySelectorAll('button')).find(
- (button) => button.textContent?.trim() === 'Retry'
- ) as HTMLButtonElement;
- retryButton.click();
- fixture.detectChanges();
+ it('assigns a course only when a student and course are selected', () => {
+ component.selectedCourseId = 'course-1';
+ component.selectedStudentId = 'student-1';
+ component.assignCourse();
+ expect(coursesService.assignCourseToStudent).toHaveBeenCalledWith('course-1', 'student-1');
+ expect(analyticsService.getTeacherSummary).toHaveBeenCalled();
+ });
- expect(analyticsService.getEducatorRuntimeSupport).toHaveBeenCalledTimes(2);
- expect(coursesService.assignCourseToStudent).not.toHaveBeenCalled();
- expect(coursesService.deleteCourse).not.toHaveBeenCalled();
+ it('reports assignment API failure without clearing selected values first', () => {
+ coursesService.assignCourseToStudent.and.returnValue(throwError(() => ({ error: { detail: 'Assignment failed.' } })));
+ component.selectedCourseId = 'course-1';
+ component.selectedStudentId = 'student-1';
+ component.assignCourse();
+ expect(toastService.show).toHaveBeenCalledWith('Assignment failed.', 'error');
+ expect(component.selectedCourseId).toBe('course-1');
+ expect(component.selectedStudentId).toBe('student-1');
});
});
diff --git a/frontend/src/app/pages/user-dashboard/teacher-view/teacher-view.component.ts b/frontend/src/app/pages/user-dashboard/teacher-view/teacher-view.component.ts
index 4d2d5af..cb635fd 100644
--- a/frontend/src/app/pages/user-dashboard/teacher-view/teacher-view.component.ts
+++ b/frontend/src/app/pages/user-dashboard/teacher-view/teacher-view.component.ts
@@ -2,6 +2,7 @@ import { Component, Input, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Router } from '@angular/router';
+import { RouterLink } from '@angular/router';
import { UserInfo } from '../../../models/user-info';
import { CoursesService } from '../../../services/courses.service';
import { Course } from '../../../models/course';
@@ -34,7 +35,7 @@ interface RuntimeInterventionCourseGroup {
@Component({
selector: 'echoed-teacher-view',
standalone: true,
- imports: [CommonModule, FormsModule, IconModule, EchoStatePanelComponent, EchoLoadingStateComponent],
+ imports: [CommonModule, FormsModule, RouterLink, IconModule, EchoStatePanelComponent, EchoLoadingStateComponent],
templateUrl: './teacher-view.component.html',
styleUrl: './teacher-view.component.scss'
})
diff --git a/frontend/src/app/services/sections.service.ts b/frontend/src/app/services/sections.service.ts
index 8524f56..bd0948f 100644
--- a/frontend/src/app/services/sections.service.ts
+++ b/frontend/src/app/services/sections.service.ts
@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from '../../environments/environment';
-import { Assignment, Section } from '../models/section';
+import { Assignment, Enrollment, Section, SectionSummary } from '../models/section';
@Injectable({
providedIn: 'root'
@@ -20,6 +20,14 @@ export class SectionsService {
return this.http.post(`${this.apiUrl}/sections`, payload);
}
+ getRoster(sectionId: string): Observable {
+ return this.http.get(`${this.apiUrl}/sections/${sectionId}/roster`);
+ }
+
+ getSectionSummary(sectionId: string): Observable {
+ return this.http.get(`${this.apiUrl}/sections/${sectionId}/analytics/summary`);
+ }
+
getAssignments(sectionId: string): Observable {
return this.http.get(`${this.apiUrl}/sections/${sectionId}/assignments`);
}
diff --git a/frontend/src/app/services/shell-navigation.service.ts b/frontend/src/app/services/shell-navigation.service.ts
index 7a43b73..e2ccff3 100644
--- a/frontend/src/app/services/shell-navigation.service.ts
+++ b/frontend/src/app/services/shell-navigation.service.ts
@@ -54,12 +54,13 @@ export class ShellNavigationService {
title: 'Teach',
space: 'teach',
items: [
- { label: 'Teaching Overview', route: '/home', icon: 'Home', roles: ['teacher', 'instructor'], exact: true },
- { label: 'Classes', route: '/home/sections', icon: 'Users', roles: ['teacher', 'instructor', 'org_admin'] },
- { label: 'Curriculum', route: '/home/courses', icon: 'BookOpen', roles: ['teacher', 'instructor'] },
+ { label: 'Teaching Overview', route: '/teach', icon: 'Home', roles: ['teacher', 'instructor'], exact: true },
+ { label: 'Classes', route: '/teach/classes', icon: 'Users', roles: ['teacher', 'instructor', 'org_admin'] },
+ { label: 'Assignments', route: '/teach/assignments', icon: 'ClipboardList', roles: ['teacher', 'instructor', 'org_admin'] },
+ { label: 'Curriculum', route: '/teach/curriculum', icon: 'BookOpen', roles: ['teacher', 'instructor'] },
{ label: 'Learning Paths', route: '/home/programs', icon: 'ClipboardList', roles: ['teacher', 'instructor'] },
- { label: 'Learner Progress', route: '/workspace/learners', icon: 'SlidersHorizontal', roles: ['teacher', 'instructor'] },
- { label: 'Settings', route: '/home/me/preferences', icon: 'Settings', roles: ['teacher', 'instructor'] },
+ { label: 'Learner Progress', route: '/teach', icon: 'SlidersHorizontal', roles: ['teacher', 'instructor'] },
+ { label: 'Settings', route: '/teach/settings', icon: 'Settings', roles: ['teacher', 'instructor'] },
],
},
{
diff --git a/openspec/changes/overhaul-role-based-ui-ux-experience/tasks.md b/openspec/changes/overhaul-role-based-ui-ux-experience/tasks.md
index 66840e1..0d7261f 100644
--- a/openspec/changes/overhaul-role-based-ui-ux-experience/tasks.md
+++ b/openspec/changes/overhaul-role-based-ui-ux-experience/tasks.md
@@ -39,10 +39,10 @@
## 6. Teacher Experience
-- [ ] 6.1 Redesign teacher Today dashboard around learner support and classes; affected screens: teacher dashboard; APIs: `/api/courses`, `/api/users/students`, `/api/analytics/teacher-summary`, governance summary; tests: teacher specs; a11y: data list summaries; responsive: priority stack; frontend-only: yes; rollback: old teacher view.
-- [ ] 6.2 Redesign classes/sections and class detail; affected screens: `/home/sections`, `/home/sections/:id`, cohort aliases; APIs: sections, roster, assignments, section analytics; tests: section specs and Playwright teacher flow; a11y: tabs/list keyboard support; responsive: tabs/cards; frontend-only: yes; rollback: old templates.
-- [ ] 6.3 Add guided assignment workflow over existing assignment/assign-course APIs; affected screens: class detail assignment flow; APIs: `/api/sections/{id}/assignments`, `/api/assign-course`; tests: workflow specs; a11y: stepper and validation; responsive: mobile wizard; frontend-only: yes; rollback: current inline assignment controls.
-- [ ] 6.4 Redesign curriculum preview, learner progress, review/feedback, and discussion/moderation surfaces where existing APIs support them; affected screens: curriculum, assessment, discussion; APIs: courses, assessments, posts/threads; tests: targeted specs; a11y: form/table semantics; responsive: data cards; frontend-only: yes unless feedback/moderation persistence is selected; rollback: hide unsupported surfaces.
+- [x] 6.1 Redesign teacher Today dashboard around learner support and classes; affected screens: teacher dashboard; APIs: `/api/courses`, `/api/users/students`, `/api/analytics/teacher-summary`, governance summary; tests: teacher specs; a11y: data list summaries; responsive: priority stack; frontend-only: yes; rollback: old teacher view.
+- [x] 6.2 Redesign classes/sections and class detail; affected screens: `/home/sections`, `/home/sections/:id`, cohort aliases; APIs: sections, roster, assignments, section analytics; tests: section specs and Playwright teacher flow; a11y: tabs/list keyboard support; responsive: tabs/cards; frontend-only: yes; rollback: old templates.
+- [x] 6.3 Add guided assignment workflow over existing assignment/assign-course APIs; affected screens: class detail assignment flow; APIs: `/api/sections/{id}/assignments`, `/api/assign-course`; tests: workflow specs; a11y: stepper and validation; responsive: mobile wizard; frontend-only: yes; rollback: current inline assignment controls.
+- [x] 6.4 Redesign curriculum preview, learner progress, review/feedback, and discussion/moderation surfaces where existing APIs support them; affected screens: curriculum, assessment, discussion; APIs: courses, assessments, posts/threads; tests: targeted specs; a11y: form/table semantics; responsive: data cards; frontend-only: yes unless feedback/moderation persistence is selected; rollback: hide unsupported surfaces.
## 7. Administrator Experience