06 — Screen Specifications (Students Module)
- 1. Students List (Roster)
- 2. Student Detail
- 3. Create Student Wizard (
/students/add) - 4. Transfer Form (
/students/:id/transfer) - 5. Enroll Sheet (detail → "Enroll")
- 6. Documents Upload Sheet
- 7. Bulk Import Wizard (
/students/import) - 8. Link Parent Sheet (detail → Parents tab → +)
- 9. Graduate / Archive / Restore / Delete dialogs
- Shared layout rules applied
Production-grade layout specs for the core screens. Wire-accurate to the API (12_API_Mapping.md); tokens from 00-shared/02; components from 00-shared/03 and 07_Component_Library.md. Grid: 4 dp. Breakpoints: phone < 600, tablet 600–839, landscape 840–1199, desktop ≥ 1200 (00-shared/02 §8).
1. Students List (Roster)
Layout (phone)
┌──────────────────────────────────────────────┐
│ AppBar: "Students" [search] [menu] │ 64
├──────────────────────────────────────────────┤
│ AppSearchBar (debounce 300 ms) │ 56 (sticky)
│ Filter chips row (horizontal scroll) │ 40
│ [All][Active][Graduated][Archived] [Class▾][Year▾]
├──────────────────────────────────────────────┤
│ result count line (bodySmall, onSurfaceVariant) │ 24
│ ┌──────────────────────────────────────────┐ │
│ │ StudentListTile │ │ 72 (avatar 40, name titleMedium,
│ │ avatar name · ADM-2026-001 [Active]│ │ admission number mono bodySmall,
│ │ Grade 5 · A · 2025-26 chevron │ │ class+year bodySmall, StatusBadge)
│ └──────────────────────────────────────────┘ │
│ … (n rows; infinite scroll → bottom spinner) │
│ [end of list footer] │
├──────────────────────────────────────────────┤
│ FAB: + Add student (extended, heroTag unique)│
└──────────────────────────────────────────────┘
- Padding: horizontal 16 (phone) / 24 (tablet+). Row min height 56; avatar 40.
- Sticky header group: search + chips scroll away or stick? — stick search bar,
chips scroll with content (desktop keeps all sticky,
e-2). - Tablet 600–839: 2-column list (cards,
surfaceContainerLow, r-lg). - Desktop ≥ 1200: master-detail — list left (320–400 dp), detail pane right;
row tap selects (indicator
secondaryContainer).
Behaviour rules
- Search:
AppSearchBardebounce 300 ms → client filter over loaded pages (serverqignored — OQ-2). While searching, chips disabled except Status. - Infinite scroll: load next page when last tile visible; append;
meta.hasNextguard (metashape frompagination-query.dto.ts:32-39). - Pull-to-refresh: reload page 1 + reset.
- Row menu visibility by status:
active→ View, Edit, Transfer, Graduate, Archive, Deletegraduated→ View, Edit, Restore* (no server un-graduate; Restore = archive→active only — omit; graduated only Edit, Delete), Archive, Deletearchived→ View, Edit, Restore, Delete- (see
01 §12 OQ-5/6— graduated is terminal; no action maps to it)
- Empty states: no data →
AppEmptyState"No students yet — Add or Import"; filtered empty → "No students match — clear filters" withClearaction. - Error:
AppErrorStatecode+retry; offline: banner + cached rows.
States matrix
| State | Render |
|---|---|
| initial/loading | 6 × AppSkeleton list rows |
| success+data | rows + meta count + infinite scroll |
| success+empty | AppEmptyState |
| error | AppErrorState(code, onRetry) |
| loadingMore | bottom LinearProgressIndicator (indeterminate, m-instant fade) |
| offline | rows from cache + AppOfflineBanner; FAB disabled? no — create blocked with snackbar |
Accessibility
- Live region: "24 students" / "0 results".
- Status never color-only: badge = icon + text (✓ Active etc. —
00-shared/09 §9). - Search field label "Search students".
2. Student Detail
Layout (phone, tabbed)
┌──────────────────────────────────────────────┐
│ AppBar: [←] Meera Sharma [⋯ overflow] │ 64
├──────────────────────────────────────────────┤
│ Header card (e-1, padding 16) │
│ avatar 64 Meera Sharma (titleLarge) │
│ ADM-2026-0142 [✓ Active] │
│ Grade 6 · B · 2025-26 (chips) │
│ campus: Main · house: Red │
│ actions: [Edit][Transfer] (filledTonal) │
│ [Graduate][Archive][Delete] (menu) │
├──────────────────────────────────────────────┤
│ TabBar: Profile|Attendance|Fees|Results| │
│ Documents|History (scrollable) │
├──────────────────────────────────────────────┤
│ Tab content (lazy-load per tab) │
└──────────────────────────────────────────────┘
- Tablet/desktop: header card + tabs in right pane of master-detail; two-column info grid.
- Overflow menu per status (same rules as list row menu).
Tab: Profile
Profile
├─ Identity (from linked user — read-only here)
│ name, email, phone, DOB, gender, avatar (AppInfoRow rows)
│ edit hint: "Identity is managed under User profile" (bodySmall)
├─ Academic
│ admission number (mono), roll number, admission type chip
│ admission date, class, grade, section, academic year, campus, house
├─ Flags
│ transport required ✓/✗, hostel required ✓/✗ (AppSwitch read-only)
├─ Medical notes (card, bodyMedium)
└─ Metadata (expandable, mono JSON — admin only, `(proposed)`)
Tab: Documents
Documents
├─ [+ Upload] (button top-right of tab, or FAB when tab active)
├─ Attachment tiles (createdAt desc, `student.service.ts:273-279`)
│ icon by mimeType (pdf/image/other), fileName (titleMedium),
│ "2.4 MB · category: birth_certificate · by Priya · Aug 2" (bodySmall)
│ actions: Preview `(planned)`, Share `(planned)` — blocked by OQ-9
└─ empty: "No documents yet — upload a birth certificate, transfer papers…"
Tab: History
History (GET /students/:id/academic-history — joinedAt desc)
├─ Timeline (AppAcademicHistoryTimeline)
│ ▸ 2025-26 · Grade 6-B joined Aug 2, 2025 [ACTIVE]
│ ▸ 2024-25 · Grade 5-A joined Apr 1, 2025 [transferred]
│ left Aug 1, 2025
│ each node: year/class title, joinedAt/leftAt dates, EnrollmentStatus badge
└─ Note: current active enrollment also surfaces via /enrollments (active filter)
Tab: Attendance / Fees / Results (forward-looking)
Owned by their modules; here: embedded lazy tab shells that call those modules'
read APIs (per PLAN.md 5.4/7.x/8.x flows). If module API not implemented →
tab shows (planned) empty-state placeholder. Do not block rendering of other tabs.
Behaviour rules
- Tabs lazy-load on first activation; state kept alive (
TabBarView+ keep-alive). - After any action (edit/transfer/enroll/upload/graduate/archive/restore) refresh profile + history + badges, snackbar confirmation.
- 404 on load →
AppErrorState"Student not found" + back; treat as deleted.
3. Create Student Wizard (/students/add)
Step model (AppStepper — horizontal phone, vertical desktop)
Step 1 — Identity
Identity (source: POST /users — create-student.dto via users module)
├─ firstName* (text, autofill given-name)
├─ middleName? (text)
├─ lastName* (text, autofill family-name)
├─ email* (email keyboard, autofill email) — unique per tenant
├─ phone? (tel)
└─ "Find existing user" — search over GET /users?q= (pick → skips creation)
Step 2 — Academics
Academics (source: cascading GET /academic-years → /grades → /sections/by-grade/:gradeId → /classes?by-year)
├─ academic year* (AppDropdown — only ACTIVE/UPCOMING shown by status field)
├─ grade* (AppDropdown)
├─ section* (AppDropdown — filtered by grade)
├─ class* (AppDropdown — filtered by grade+section+year; shows capacity "Grade 5 · A (32/40)")
├─ roll number? (text, mono)
└─ admission type? (segmented: new | transfer | promoted — matches enum student.schema.ts:47)
Step 3 — Extras
Extras
├─ admission date? (AppDatePicker — default today; max = today; server defaults new Date() student.service.ts:66-68)
├─ campus? (AppDropdown)
├─ house? (AppDropdown)
├─ transport required? (AppSwitch)
├─ hostel required? (AppSwitch)
└─ medical notes? (multiline)
Step 4 — Review & submit
Review
├─ summary card (identity, class path, flags)
├─ note: "Student is created as ACTIVE and auto-enrolled in Grade 5 · A" (banner)
└─ CTA: "Create student" (loading spinner)
Submission sequence (exact)
POST /users {firstName,lastName,email,phone?}→ 200user._id(or 409 → "email already registered" → switch to search-existing step).POST /students {userId, admissionNumber, rollNumber?, academicYearId, campusId?, gradeId, sectionId, classId, houseId?, admissionDate?, admissionType?, transportRequired?, hostelRequired?, medicalNotes?}→ 200 student (auto-enrolled).- Navigate
/students/:id+ snackbar "Student created".
Error mapping
| Error | UI |
|---|---|
| 400 validation | field errors (first invalid field focused) |
| 409 duplicate admission | admission-number field inline "already exists" + suggest search |
| 409 duplicate email (users) | step 1 inline, offer reuse |
| 429 | countdown banner, disable CTA |
| offline | block submit, banner |
States
step validated on Next; back preserves form; submit = step 4 loading; failure stays on step 4 with error summary. Interrupted flow (app kill) → no draft persistence (YAGNI; re-enter).
4. Transfer Form (/students/:id/transfer)
Transfer — Meera Sharma (Grade 6-B, active)
Banner: "Current enrollment will be marked as transferred. History is preserved."
├─ class* (AppDropdown — classes for chosen year)
├─ academic year* (AppDropdown)
├─ grade? (AppDropdown — default: target class's grade) transfer-student.dto.ts:13-19
├─ section? (AppDropdown — default: target class's section) transfer-student.dto.ts:20-25
└─ roll number? (text)
CTA: [Transfer student] (confirm dialog: "Transfer to Grade 7-A?")
- Pre-submit validation client-side: target must differ from current
(
classId/academicYearIdboth equal current → block with inline message). - Server guard: status ≠
active→ 409Cannot transfer a student with status "x".→ screen switches to read-only + status banner (student.service.ts:175-179). - Success → detail refresh; history tab shows old enrollment
transferred+leftAt(class-enrollment.schema.ts:30-31).
5. Enroll Sheet (detail → "Enroll")
AppBottomSheet (half, drag handle)
Enroll in class
Banner: "Any currently active enrollment becomes transferred."
├─ class* (AppDropdown — filterable by year)
├─ academic year* (AppDropdown)
└─ roll number? (text)
CTA: [Enroll] → POST /students/:id/enroll → snackbar + refresh
6. Documents Upload Sheet
AppBottomSheet
Upload document — Meera Sharma
├─ file picker row (name + size + type after selection; camera option phone)
├─ category? (AppDropdown with suggestions: birth_certificate, transfer_certificate,
│ report_card, medical, id_proof, other — free text allowed, server accepts any string)
└─ CTA: [Upload] → multipart POST /students/:id/documents (field "file")
- Client-side guards (server has none — OQ-9): size ≤ 10 MB
(proposed), mime allowlist pdf/jpg/png/webp(proposed); violation → inline error before upload. - Upload progress: determinate bar; on 200 → tile appears at top (createdAt desc);
on network loss mid-upload → error + retry tile (
00-shared/10 §2).
7. Bulk Import Wizard (/students/import)
Step 1 — Template
Import students from CSV
├─ info card: required columns — firstName*, lastName*, email*,
│ admissionNumber*, grade*, section*, academicYear*;
│ optional — rollNumber (students-import.adapter.ts:17-26)
├─ [Download template] → GET /bulk/export/students (text/csv attachment,
│ filename "students.csv" — bulk.controller.ts:50-60) — exported columns differ:
│ admissionNumber, rollNumber, status, admissionDate (adapter exportRows 87-98)
└─ [Next]
Step 2 — File
├─ file picker (.csv only)
├─ client pre-parse (isolate — 00-shared/11 §13): header check + column presence;
│ warn on missing required columns BEFORE upload
└─ [Import CSV]
Step 3 — Progress (uploading)
Uploading… (indeterminate bar; request synchronous server-side — OQ-12)
"Processing N rows — this may take a minute for large files"
Step 4 — Report
Import report
┌──────────────────────────────┐
│ Imported Failed Total │ (ImportReportCard: 3 stat tiles)
│ 997 3 1000 │
└──────────────────────────────┘
Failed rows (grouped list):
Row 42 — Missing required column "section".
Row 87 — Email "x@y" already registered.
Row 120 — No class found for grade "5" section "A".
(every error string server-side: students-import.adapter.ts:43-57,116-141)
CTA: [Done] → back to list (refresh) · [Fix and re-import] → step 2
- Partial success is the normal state — never show "failed" as an error page.
8. Link Parent Sheet (detail → Parents tab → +)
Link guardian — Meera Sharma
├─ Guardian: [search existing parent] or [create new]
│ create new: (user) firstName*, lastName*, email*, phone?
│ (parent) occupation?, company?, annualIncome?, relationshipNotes?,
│ pickupAuthorization? (create-parent.dto.ts)
├─ relationship* (AppDropdown — required, enum: mother|father|guardian|
│ grandparent|relative|foster_parent — student-parent-link.schema.ts:7-14)
├─ primary guardian? (AppSwitch — isPrimaryGuardian)
├─ financial responsibility? (AppSwitch)
├─ pickup allowed? (AppSwitch — default TRUE, link dto default)
└─ emergency priority? (AppSlider 0–10, default 0)
CTA: [Link] → POST /parents/link/:studentId
- New-parent branch sequence:
POST /users→POST /parents(409 if parent profile exists for user) → link. - Unlink: row menu → confirm →
DELETE /parents/link/:linkId(404 → treat removed).
9. Graduate / Archive / Restore / Delete dialogs
| Dialog | Copy (title / body) | Confirm label | Endpoint |
|---|---|---|---|
| Graduate | "Graduate Meera Sharma?" / "Marks the student as graduated. This cannot be undone by the UI (no un-graduate endpoint)." | Graduate (destructive) | POST /students/:id/graduate |
| Archive | "Archive Meera Sharma?" / "Hidden from the roster. You can restore anytime." | Archive | POST /students/:id/archive |
| Restore | "Restore Meera Sharma?" / "Returns the student to active status." | Restore | POST /students/:id/restore |
| Delete | "Delete Meera Sharma?" / "Soft-deletes the record — excluded from all lists. Documents and history remain." | Delete (destructive) | DELETE /students/:id |
409 handling: graduate twice / archive twice / restore twice → server 409
"Student is already graduated/archived/active." → snackbar, state refresh
(student.service.ts:207-209,228-230).
Shared layout rules applied
- One
headlineMediumper screen title (00-shared/09 §11). - All lists
ListView.builder; infinite scroll with end-of-list footer. - Keyboard: phone bottom-inset avoidance on all forms.
- Motion: screen enter
m-baseslide; sheetm-basescale+fade; success snackbar < 3 s; error ≥ 4 s (00-shared/03 A). - Dark mode: every surface tokenized; badges keep icon+text (no color-only).