The definitive per-screen spec. Wire contract per 00-shared/07 . All field names and
rules cite the DTO/schema they come from. (planned) = backend/roadmap gap.
AppBar: "Teachers" | [Search icon → /search]
─────────────────────────────────────────────
AppSearchBar (q) [Filter chips ▾]
[Filter chips row: status · department · designation]
─────────────────────────────────────────────
[AppListTile × N] avatar Name (titleMedium)
Employee #TCH001 · Dept (bodyMedium)
[StatusChip] [›]
─────────────────────────────────────────────
[infinite scroll loader / end-of-list footer]
FAB: "Add teacher"
Breakpoints: phone single column list; tablet 2-column grid of AppCards; desktop
master-detail (tap row → detail in right pane, 05_Global_Information_Architecture.md:58).
State UI Source
Initial/Loading 8 × AppSkeleton rows 06_State_Management.md §3.1
Success + data list, status chips colored —
Success + empty (no filters) AppEmptyState "No teachers yet" + CTA add—
Success + empty (filters) AppEmptyState "No teachers match" + clear filters—
Error AppErrorState(code, message, retry)envelope error.code
Fetch: GET /teachers?page=&limit=20 (teacher.controller.ts:27-29); meta
drives hasNext (buildPaginationMeta, pagination-query.dto.ts:41-55).
q/sort params: sent but ignored by TeacherService.find
(teacher.service.ts:66-78) — client filters cached list by q locally; (planned)
server search (OQ-3).
Filter chips: status (4 enum values, teacher.schema.ts:7-12), department,
designation — no server filter params exist → client-side filtering of loaded
pages (OQ-3); document as limitation.
Row tap → S2. Overflow menu: Edit → S6, Deactivate → S8.
Pull-to-refresh (RefreshIndicator) bypasses cache; FAB scroll-hide behaviour.
Chips: FilterChip with selected state semantics (09_Accessibility_Baseline.md §5).
Status never color-only: chip = icon + label (status enum text) (09 §9).
Live region announces search result count.
AppBar: "{displayName}" [Edit] [⋮ menu]
─────────────────────────────────────────────
Header card (AppCard, tonal):
[Avatar 48] Name (titleLarge) StatusChip (labelLarge)
Employee #TCH001 (mono)
─────────────────────────────────────────────
Info section (AppSectionHeader "Employment"):
InfoRow Department "Science"
InfoRow Designation "Teacher" (designations examples,
InfoRow Joining date "12 Aug 2024" COLLECTIONS.md:1560-1574)
InfoRow Qualification "M.Sc. Physics"
InfoRow Experience "6 years"
AppSectionHeader "Subjects taught": [SubjectBadge × N]
AppSectionHeader "Class teacher for":[ClassBadge × N]
AppSectionHeader "Metadata" (only if non-empty): key/value rows
GET /teachers/:id (teacher.controller.ts:30-32). 404 → AppErrorState
RESOURCE_NOT_FOUND + "back to list" (treat as removed/soft-deleted).
Reference names resolved from cached catalogs; missing ref id → "—".
Empty subjects[]/classTeacherFor[] → "Not set" bodyMedium text, no empty-state.
Edit → S6 (prefill from current doc).
Menu → Deactivate (S8). If employmentStatus === 'terminated' → deactivate hidden.
If on_leave → AppBanner info: "On leave since {updatedAt}".
AppSectionHeader "Assignments" [Academic year ▾] [+ Add]
─────────────────────────────────────────────────────
[AssignmentCard × N]
subject name (titleMedium) [⋮ remove]
class name · academic year (bodyMedium)
─────────────────────────────────────────────────────
empty: AppEmptyState "No assignments for {year}" + CTA
GET /subject-assignments/by-teacher/:teacherId?academicYearId=
(subject-assignment.controller.ts:30-35); academicYearId required in practice
(subject-assignment.service.ts:26-31 returns [] when missing).
Year picker from GET /academic-years (academic-year.controller.ts:27-30),
default = isCurrent (academic-year.schema.ts:31-32).
Loading: 3 skeleton cards. Error: inline AppErrorState(compact) + retry.
Remove: confirm AppDialog → DELETE /subject-assignments/:id
(subject-assignment.controller.ts:36-38); 404 → treat as already removed.
AppSectionHeader "Weekly schedule" [Week ▾ (current week)]
Grid: 7 columns (Mon..Sun) × time rows; period cards:
"10:00–10:45 · Mathematics · 10-A"
GET /timetable?teacherId=<id> (timetable.controller.ts:21-27), server-sorted
dayOfWeek, startTime (timetable.service.ts:55-60).
Empty → "No classes scheduled." Stale-year handling: show current term by default.
AppBar "Add teacher" [Cancel] [Save]
─────────────────────────────────────────
1. Identity user picker* (from Users list, email searchable)
2. Employment employeeNumber* (mono, e.g. TCH001)
3. Org placement department ▾ · designation ▾ (pickers from staff catalogs)
4. Timeline joiningDate (date picker) · employmentStatus ▾ (default active)
5. Qualifications qualification (multiline) · experienceYears (number)
6. Assignments subjects multi-select (chips) · classTeacherFor multi-select
[Save] sticky bottom on phone; top-right on desktop
Field Server (create-teacher.dto.ts) Client rules
userId required @IsMongoId (line 6-7) required; from user picker
employeeNumber required @IsString (10-11) required; trim; unique check against loaded roster (server 409 = race-safe)
departmentId optional @IsMongoId (15-16) picker
designationId optional @IsMongoId (20-21) picker
joiningDate optional @IsDateString (25-26) date picker; ≤ today (proposed)
employmentStatus optional string, enum hint ['active','inactive','on_leave','terminated'] (28-33) default active; server does not enum-validate → client restricts to 4 values
qualification optional @IsString (37-38) free text
experienceYears optional number, no validator (41-42) client: int ≥ 0 (proposed)
subjects optional string[], no array/MongoId validator (45-46) client: array of valid ObjectIds
classTeacherFor optional string[], no validator (48-52) client: array of valid ObjectIds
POST /teachers → 200 data = teacher doc (unwrapped by envelope interceptor).
409 → inline AppBanner(error) per field ("A teacher profile already exists for this
user." / "Employee number … already exists.", teacher.service.ts:29-37).
400 → map error.details[].field to form fields (00-shared/07 §3).
Double-submit guard (anti-double-submit, 08_Interaction_&_Motion.md §6).
Offline → blocked with banner (no offline write queue for teachers, 00-shared/06 §3.7).
Same spec as S5 with: userId field hidden & immutable (update-teacher.dto.ts:4-8
lacks userId), metadata editor (proposed) simple key/value rows
(update-teacher.dto.ts:51-53), prefilled values from detail doc.
Submit PATCH /teachers/:id → 200 updated doc; 404 → leave form + list.
metadata is Record<string, unknown> — client sends only primitive values
(proposed).
Sheet "Assign {teacherName}" [Add]
Subject ▾ (required, searchable picker, GET /subjects)
Class ▾ (required, year-scoped, GET /classes)
Academic year ▾ (default isCurrent)
[Add] disabled until all three set
All four ids @IsMongoId required (create-subject-assignment.dto.ts:5-19);
teacherId prefilled from context (S3) or user-selected.
Client duplicate guard: exact (teacher, subject, class, academicYear) already in
matrix → disable Add + inline error "Already assigned" (server gap, OQ-2).
Submit POST /subject-assignments → 200 doc appended to matrix; snackbar "Assigned".
Errors: 400 → inline; 500 → generic + requestId; no optimistic insert (server
confirms only, 00-shared/06 §3.5).
Aspect Spec
Title "Deactivate {displayName}?"
Body "{employeeNumber} will be hidden from all lists. Historical assignments and audit records are kept. This action cannot be undone in the app."
Actions [Cancel] [Deactivate — destructive AppButton, error colors]
Flow DELETE /teachers/:id → 200 → snackbar "Teacher deactivated" → pop to list; 404 → snackbar "Already deactivated" → pop
Focus trapped in dialog; ESC/back cancels (03_Component_Library.md AppDialog)
A11y Semantics destructive-action label; haptic heavyImpact() on confirm (08 §3)
Spec deferred to backend support for self-resolution (OQ-2); wire behaviour per S3/S4
with read-only treatment (no add/remove for teacher role).
Own-profile banner when on_leave: "Your profile is on leave — schedule may be
reassigned."