Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

06 — Screen Specifications (Students Module)

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: AppSearchBar debounce 300 ms → client filter over loaded pages (server q ignored — OQ-2). While searching, chips disabled except Status.
  • Infinite scroll: load next page when last tile visible; append; meta.hasNext guard (meta shape from pagination-query.dto.ts:32-39).
  • Pull-to-refresh: reload page 1 + reset.
  • Row menu visibility by status:
    • active → View, Edit, Transfer, Graduate, Archive, Delete
    • graduated → View, Edit, Restore* (no server un-graduate; Restore = archive→active only — omit; graduated only Edit, Delete), Archive, Delete
    • archived → 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" with Clear action.
  • Error: AppErrorState code+retry; offline: banner + cached rows.

States matrix

StateRender
initial/loading6 × AppSkeleton list rows
success+datarows + meta count + infinite scroll
success+emptyAppEmptyState
errorAppErrorState(code, onRetry)
loadingMorebottom LinearProgressIndicator (indeterminate, m-instant fade)
offlinerows 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)

  1. POST /users {firstName,lastName,email,phone?} → 200 user._id (or 409 → "email already registered" → switch to search-existing step).
  2. POST /students {userId, admissionNumber, rollNumber?, academicYearId, campusId?, gradeId, sectionId, classId, houseId?, admissionDate?, admissionType?, transportRequired?, hostelRequired?, medicalNotes?} → 200 student (auto-enrolled).
  3. Navigate /students/:id + snackbar "Student created".

Error mapping

ErrorUI
400 validationfield errors (first invalid field focused)
409 duplicate admissionadmission-number field inline "already exists" + suggest search
409 duplicate email (users)step 1 inline, offer reuse
429countdown banner, disable CTA
offlineblock 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/academicYearId both equal current → block with inline message).
  • Server guard: status ≠ active → 409 Cannot 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.
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 /usersPOST /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

DialogCopy (title / body)Confirm labelEndpoint
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."ArchivePOST /students/:id/archive
Restore"Restore Meera Sharma?" / "Returns the student to active status."RestorePOST /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 headlineMedium per 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-base slide; sheet m-base scale+fade; success snackbar < 3 s; error ≥ 4 s (00-shared/03 A).
  • Dark mode: every surface tokenized; badges keep icon+text (no color-only).