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 (Exams Module)

Production-grade specs for each Exams screen: chrome, states, components, micro-interactions, a11y, responsive, dark/light, keyboard, motion. Tokens: Sp = 00-shared/02 §3, Ty = typography §2, El = elevation §5, R = radius §4, Mo = motion §6. All components reference 00-shared/03 and 07 (module). Every data binding cites the exact schema/service field.


1. Exams List — /exams (S1)

Chrome: AppBar "Exams" (titleLarge, El-e1); AppFilterChip row below (status: draft|active|completed|published + type: midterm|final|unit_test|quarterly| other — enums examination.schema.ts:15-20, 28-33); body RefreshIndicator + ListView.builder; AppFAB.extended "New exam" (Icons.add, unique heroTag). Bottom: AppPaginationBar (page/limit, 00-shared/07 §5).

Row — ExamCard:

[type chip]                     [status badge: draft/active/completed/published]
Name                      titleMedium, 2 lines max
Term 2026-27 · Midterm          bodySmall onSurfaceVariant (academicYearId resolved)
15 Aug – 20 Aug · in 5d         bodyMedium; startDate/endDate (ISO) → local dates
Slots 5 · Marks 120/240         bodySmall (derived: slots + marked/total)

Data: GET /examinations?page&limit (examination.controller.ts:30-32); fields name, type, startDate, endDate, status, academicYearId (examination.schema.ts:9-36). Sorting: server ignores sort/q (OQ-7) — client sorts by startDate asc locally, filters chips client-side.

States:

  • Loading: AppSkeleton(list) 6 rows.
  • Success: cards; pagination meta from buildPaginationMeta (meta.page/limit/ totalItems/totalPages/hasNext/hasPrevious, pagination-query.dto.ts:41-55).
  • Empty: AppEmptyState icon assignment, "No exams yet" + subtitle "Plan your first exam for this academic year" + FAB remains; if empty and past academic year → "No exams in this term".
  • Error: AppErrorState; 5xx shows requestId; retry re-fetches.
  • Offline: AppOfflineBanner + cached list (TTL 24 h reference data, 00-shared/06 §3.3).
  • Pagination: infinite-scroll on scroll end (guard hasNext); "Loading more" footer row; end-of-list footer when hasNext: false.

A11y: card = Semantics(button) "{name}, {type}, status {status}, starts {date}"; badge read appended; focus order: filter chips → list → FAB; SwitchAccessible. Dark/light: token-only (00-shared/02); status badge published = tertiaryContainer/tertiary-tinted container with icon verified + label (never color-only, 00-shared/09). Tablet/desktop (≥840 dp): two-pane master–detail (list 360–400 dp + S2); N shortcut = new exam; hover tint on cards. Motion: rows fade-in Mo.fast (stagger 30 ms); FAB hero to S8.

2. Exam Detail — /exams/:examId (S2)

Chrome: AppBar title = exam name, trailing AppMenu (Edit / Delete — hidden when status: 'published', OQ-5). Body RefreshIndicator + CustomScrollView.

Header card (surfaceContainerLow, r-lg, e-1)
  name headlineMedium · type chip · status badge
  AppInfoRow: Academic year (resolved) · Term dates (startDate – endDate)
  AppInfoRow: Grading scheme (gradingSchemeId? — hidden when absent,
              examination.schema.ts:35-36)
Segmented control: [Schedule] [Coverage]
Schedule tab: ExamSubjectCard list (per slot, see 07)
Coverage tab: per-slot CoverageTile: subject, class, marked/total, progress bar,
              max/pass marks
Action bar (bottom, pinned):
  [OutlinedButton "Add subject"]   (exam.update)
  [FilledButton "Publish"]         (exam.publish; hidden when published)

Data: GET /examinations/:id (examination.controller.ts:33-35examination.service.ts:61-65); slots GET /examinations/:id/subjects (examination.controller.ts:51-53examination.service.ts:133-137); coverage per slot GET /results/exam-subject/:examSubjectId (result.controller.ts:21-25).

States:

  • Loading: header skeleton + 3 slot skeletons.
  • Empty (no slots): AppEmptyState "No subjects scheduled" + "Add subject" CTA; report-card entry on such exam → server 404 (result.service.ts:51-53).
  • Error 404 ("Examination not found.", examination.service.ts:63): full-screen AppErrorState "This exam was removed" + back; auto-pop if opened via stale list.
  • Publish in progress: action bar button → spinner, dialog (S5) shows progress; on success badge flips to published (server response-driven, never optimistic).
  • Offline: cached header (last-good) + "stale" chip on slots (TTL 5 min); actions disabled (online-only).
  • Disabled: when published → Publish hidden, menu Edit/Delete hidden, Add subject hidden (immutability contract, OQ-5); when completed → Publish still allowed (server allows; no guard, OQ-1).

A11y: status badge changes announced via Semantics(liveRegion: true); segmented control = radio semantics; publish button label includes exam name. Responsive: tablet adds a right rail: selected slot's coverage panel. Motion: header fade/slide Mo.base (200 ms); segmented switch Mo.fast; slot list stagger Mo.fast.

3. Marks Entry — /exams/:examId/slots/:slotId/marks (S3)

Chrome: AppBar "Marks" + slot context in titleSmall; body SlotHeaderCard:

Subject name · Class name          titleMedium
Date 15 Aug · 09:00–12:00          bodyMedium (date + startTime/endTime strings,
                                   examination-subject.schema.ts:18-25)
Maximum 100 · Pass 33              bodySmall; "Passing" styled tertiary when
                                   passingMarks > maximumMarks (invalid, OQ-6)

Then MarksSummaryBar: marked/total · save-state (saved|queued|failed) with animated progress width. Roster ListView.builder of MarksRow (from Students module by slot classId; marks from GET /results/exam-subject/:examSubjectId).

Row:

[avatar initials]  Student name           bodyMedium
                   roll/admission no      bodySmall onSurfaceVariant
[ MarksTextField ]  [GradeChip optional]  [save status icon]
  • MarksTextField: numeric (digits only, FormattedNumberInput), max length = digits of maximumMarks; @Min(0) mirrors DTO (examination-subject.dto.ts:51-55).
  • Row-level save: submit on focus-loss or explicit save tap (see 10_Interaction_Specification.md §3 for cadence).

Save flow: POST /results/exam-subject/:examSubjectId/marks (result.controller.ts:26-31examination.service.ts:139-195). Upsert: existing (studentId, examinationSubjectId) doc updated in place (examination.service.ts:147-175); unique index {tenantId, studentId, examinationSubjectId} (examination-result.schema.ts:31-33). Payload {studentId, marksObtained, grade?, remarks?} (EnterMarksDto, examination-subject.dto.ts:47-66).

States:

  • Loading: roster skeleton rows.
  • Roster empty: AppEmptyState "No students in this class" (slot classId has no students).
  • Slot gone (404 "Exam subject not found.", examination.service.ts:144): banner + auto-pop to exam detail (refresh slots).
  • Per-row saving: inline spinner in save-status slot; optimistic green check on success (Mo.fast check animation); failure → row shake Mo.shake, "Retry" affordance; error toast for 404 "Marks cannot exceed maximum." (examination.service.ts:145-146) → open marks dialog clamped to maximumMarks.
  • Offline: row flagged queued (dashed border + cloud icon); offline queue flush on reconnect (13_State_Management.md §4); coverage bar shows queued count.
  • Disabled: after exam published, editing allowed server-side (OQ-5) but rows show "published" chip + confirm-before-edit sheet; publish-in-flight disables all fields.

A11y: row semantics "{student name}, {marks} of {maximum}"; keyboard: Tab between fields, Enter = save current row + focus next; Semantics(liveRegion) on coverage bar. Dark/light: token-only; failed row errorContainer tint. Responsive: tablet shows a right-side detail sheet (remarks/grade editor) instead of inline expansion. Motion: save check Mo.fast, error shake Mo.shake (00-shared/08), coverage bar width 200 ms ease.

4. Add Subject Slot — /exams/:examId/slots/new (S4)

Chrome: full-screen form page (> 3 fields rule, 00-shared/05 §5); AppBar "Add subject" + Save TextButton; body form list Sp.16 gaps, resizeToAvoidBottomInset: true. Field order (mirrors CreateExaminationSubjectDto, examination-subject.dto.ts:11-45):

1 subject    AppDropdown (Academics subjects; required; @IsMongoId)
2 class      AppDropdown (Academics classes; required; @IsMongoId)
3 date       AppDatePicker (required; @IsDateString; client min = exam startDate,
             max = exam endDate)
4 startTime  AppTextField "Start time" (HH:mm, keyboard=time; example '09:00')
5 endTime    AppTextField "End time" (HH:mm; client rule endTime > startTime)
6 maxMarks   AppTextField number (required; @Min(1); @IsNumber)
7 passMarks  AppTextField number (required; @Min(1); client rule ≤ maxMarks)

Client pre-validation (server lacks — OQ-2/OQ-3/OQ-6):

  • Overlap: same date + time window touching another existing slot of this exam → inline warning + AppDialog "Overlaps with Subject X 09:00–12:00 — add anyway?" (server has no conflict logic; confirm proceeds).
  • Duplicate: subject already added to this exam → warning "Subject already scheduled (date 15 Aug)" — proceed allowed (no server dedupe, OQ-3).
  • passingMarks > maximumMarks → inline error before submit.
  • Date outside exam window → inline error.

Submit: POST /examinations/:id/subjects (examination.controller.ts:45-50examination.service.ts:112-131) with examinationId taken from route (never typed). States: field errors (400 VALIDATION_ERROR → inline, focus first invalid); submitting spinner on Save; success → pop + new slot visible; 404 exam missing → pop with snackbar. A11y/other: as S8 (§8). Motion: inline error slide Mo.fast.

5. Publish Confirm — dialog on S2 (S5)

Chrome: AppDialog (00-shared/03), width ≤ 480 dp:

Icon warning_amber (large, tertiary)
"Publish results?"
"Results for {N} subjects will be released to students and parents.
 Unpublished marks stay hidden."   ← server truth: publish stamps publishedAt
                                   (examination-result.repository.ts:42-51)
Coverage recap: 3 slots · 118/120 marks entered      (from slot result counts)
[Cancel  TextButton] [Publish  FilledButton (tertiary)]

States: initial; submitting → button spinner, dialog non-dismissable, barrierDismissible: false; success → close, badge published, snackbar "Results published"; error 404/5xx → reopen dialog with error line + requestId, Publish re-enabled. Note: server allows re-publish and post-publish edits (OQ-5) — the client enforces immutability; dialog is the single confirm gate. A11y: focus trap, Escape = cancel, confirm label "Publish". Motion: scale-in Mo.base, success check Mo.fast.

6. Student Results — /results (S6)

Chrome: AppBar "My Results" (+ parent: "Name's Results"); body RefreshIndicator + grouped list by exam (GET /results/student/:studentId, result.controller.ts:16-20result.service.ts:36-38).

Row — ResultRowCard: exam name (resolved via exam id? — payload only carries examinationSubjectId; client groups by slot's exam; subject names resolved client-side from slot ids — OQ-9), subjectName · marksObtained/maximumMarks · grade chip, publishedAt formatted ("Published 15 Aug"), remarks preview.

States: loading; empty "No results yet — check after your next exam"; error; offline banner + cache (TTL 5 min). Note: the endpoint returns unpaginated arrays — client slices by exam client-side (OQ-7 pattern). A11y: each row one Semantics label "{subject}, {marks} of {max}, grade {g}". Responsive: tablet = table layout (columns subject/marks/grade/remarks). Motion: row stagger Mo.fast.

7. Report Card — /results/:examId/report-card (S7)

Chrome: AppBar "Report Card" + share action (Icons.share → image share, (proposed)). Body card-stack on surfaceContainerLow:

Header: exam name · generatedAt (ISO → "Generated 15 Aug 14:30")
Overall card (primaryContainer, r-lg):
  percentage  displaySmall mono tabular ("82.5%")
  overallGrade  GradeBadge "A" + threshold caption ("80–89%")
  totals row: totalMarksObtained / totalMaximumMarks
Subject table (ReportCardTable, tablet=real columns / phone=rows):
  subjectName (resolved, OQ-9) · marksObtained/maximumMarks ·
  grade (client-supplied, may be empty) · remarks (may be empty)

Data: GET /results/report-card/:studentId/:examId (result.controller.ts:32-37result.service.ts:46-98): subjects[], totalMarksObtained, totalMaximumMarks, percentage (2 dp), overallGrade (A+/A/B+/B/C/D/F at 90/80/70/60/50/40 — result.service.ts:130-138), generatedAt.

States: loading skeleton; empty/404 ("No subjects found for this examination.", result.service.ts:51-53) → AppEmptyState "No subjects scheduled for this exam"; error; offline (cached card, publishedAt-stamped cards are cacheable 24 h). Edge rendering: subject with no result → row shows 0/max + muted "not marked" hint (payload cannot distinguish 0 vs absent — result.service.ts:70); empty grade → "—"; remarks overflow → 2-line clamp + expand. If all subjects are unmarked → percentage 0, grade F — show explanatory note, not an error. A11y: table headers announced; GradeBadge label "A, 80 to 89 percent". Responsive: phone = card rows, ≥840 dp = real table; landscape keeps table. Motion: rows fade Mo.fast; badge pop Mo.base.

8. Create / Edit Exam — /exams/new · /exams/:examId/edit (S8 / S8a)

Chrome: full-screen form; AppBar "New exam" / "Edit exam" + Save; body Sp.16 gaps. Fields mirror CreateExaminationDto (examination.dto.ts:4-26) / UpdateExaminationDto (examination.dto.ts:28-53):

1 academicYear  AppDropdown (Academic years; create only; required; @IsMongoId)
2 name          AppTextField (required; @IsString; maxLength 100 client)
3 type          AppDropdown midterm|final|unit_test|quarterly|other
                (required; schema enum examination.schema.ts:15-20; DTO is IsString
                 only — client constrains, OQ note)
4 startDate     AppDatePicker (required; @IsDateString)
5 endDate       AppDatePicker (required; client rule endDate ≥ startDate — OQ-6)
6 status (edit) read-only chip (draft|active|completed|published) — client never
                sends status on create; edit sends only explicit transitions (OQ-1)

Submit: create → POST /examinations → 201 doc, status: 'draft' forced (examination.service.ts:48); edit → PATCH /examinations/:id partial (examination.service.ts:81-97), $set only sent fields, version +1 (base.repository.ts:57-66). Edit prefill from GET /examinations/:id. States: field validation; submitting spinner; success → pop to detail + snackbar; 400 field errors inline; 404 (edit of deleted exam, examination.service.ts:87) → pop + "Exam no longer exists"; status edit rule: chips for draft → active only (free-string status risk, OQ-5); after published the edit route is unreachable from UI. A11y/keyboard: labeled fields, error summaries announced, autofocus name; submit via keyboard done. Responsive: single column ≤ 600 dp, two-column grid ≥ 840 dp. Motion: inline error slide Mo.fast.