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

The largest doc: full behaviour per screen. References shared specs by name: 02_Design_Tokens.md (tokens T-), 03_Component_Library.md (components C-), 09_Accessibility_Baseline.md (a11y rules), 06_State_Management.md (cubits), 07_API_Conventions.md (envelope/errors). Server facts cited with file:line. All screens (forward-looking) Flutter client design.


S1 — Marks Entry Grid

Route: /examinations/:examId/subjects/:examSubjectId/marks Purpose: enter/update marks for every student for one exam-subject.

1. Data contract

FieldSource
Subject context: date, startTime, endTime, maximumMarks, passingMarksexamination-subject.schema.ts:18-31
Rows: studentId, marksObtained?, grade?, remarks?, publishedAt?examination-result.schema.ts:8-25
Load: GET /api/v1/results/exam-subject/:examSubjectIdresult.controller.ts:21-25
Save: POST /api/v1/results/exam-subject/:examSubjectId/marks body {studentId, marksObtained, grade?, remarks?}result.controller.ts:26-31, examination-subject.dto.ts:47-65

2. Layout

  • Header block (S1-H): exam name + subject + class; chips: date, time range, maximumMarks, passingMarks, entry count n/students.
  • Table (S1-T): columns — student (name, id), marks obtained, grade, remarks, status. Rows sorted by student name (client-side).
  • Bottom action bar (S1-A): "Save all pending" with pending count; "Sync" for offline draft.
  • Status strip (S1-S): unsaved-changes count; last-sync time.

3. States

StateTriggerUI
Loadingcubit fetch of S1-T + headerC-AppSkeleton rows (shimmer, 8 rows)
Loaded, cleanfetch ok, no editsTable + C-AppSnackbar-free header chip Draft
Loaded, dirty≥1 field editedSave bar active; per-row unsaved dot
Saving rowupsert in flightRow spinner on that row only (per-row concurrency, examination.service.ts:147-175 upsert)
Saved2xxRow clears dirty state; C-AppSnackbar success < 3 s
Row rejected404 RESOURCE_NOT_FOUND from examination.service.ts:145-146 (marks > max)Inline field error: "Marks cannot exceed {maximumMarks}"; row keeps dirty state
Network failtimeout/offlineC-AppOfflineBanner; edits preserved in offline draft (15_Flutter §Offline)
Emptyexam-subject has no students yetC-AppEmptyState: "No students to grade yet"
Published (server row has publishedAt, examination-result.schema.ts:24-25)row-levelRow locked + "Published" chip; UI-only lock — server still accepts writes (examination.service.ts:139-195 has no published check, (planned))

4. Components (from 03_Component_Library.md)

C-AppTextField (numeric, tabularFigures() per 02_Design_Tokens.md §2), C-AppSelect (grade), C-AppSnackbar, C-AppOfflineBanner, C-AppEmptyState, C-AppSkeleton, C-StatusChip (module: draft/saved/published), C-DataTable (module: virtualised, see 15_Flutter §Table), C-FilterChip (only-entered/only-missing toggle).

5. Tokens (02_Design_Tokens.md)

  • Table header row: surfaceContainerHigh; zebra: surface / surfaceContainerLow.
  • Entered marks: onSurface; missing row: onSurfaceVariant italic; over-max error: error + errorContainer field fill.
  • Publish chip: secondaryContainer-style success; draft chip: surfaceVariant.
  • Numerals mono 14/400/20 + FontFeature.tabularFigures().

6. Dark/light

Both modes identical structure; only roles swap per 02_Design_Tokens.md §1 (surfaceContainerHigh #ECE6E0 light / #2B2929 dark, etc.). Grid lines use outlineVariant in both.

7. Responsive

  • ≥ 720 px: full table, columns as above.
  • < 720 px: table → cards; per-student card: name header, marks field, grade dropdown, remarks field, status chip. maximumMarks hint beside marks field in both.
  • Portrait phones: save bar fixed bottom (respect safe-area), sticky header block with subject chips scrollable horizontally.

8. Accessibility (09_Accessibility_Baseline.md)

  • Table: Semantics header row; each row labelled "Student {name}, marks {value} of {max}".
  • Marks field: label "Marks obtained — {student}" + errorText announced via SemanticsService on 404-reject.
  • Keyboard: Tab order = marks → grade → remarks per row; Enter saves row.
  • Color not the only signal: published/missing states carry icon + text, not just chip color.
  • Charts/tables a11y (see S5 §8): table content is real Semantics, not image.
  • Focus visible on save bar; reduced motion: no per-row spinner animation (static progress).

S2 — Exam-Subject Detail

Route: /examinations/:examId/subjects/:examSubjectId

1. Data contract

Single ExaminationSubject (examination-subject.schema.ts:8-31): examinationId, subjectId, classId, date, startTime, endTime, maximumMarks, passingMarks. Source endpoint: GET /api/v1/examinations/:id/subjects (examination.controller.ts:51-53) — client picks the row by id (no single-row GET exists).

2. Layout

  • Hero card: subject name (client-mapped from subjectId), class.
  • Info grid: date; startTimeendTime; maximumMarks; passingMarks.
  • Coverage card: entered count / total students + linear progress (C-ProgressIndicator); computed client-side from S1 rows.
  • Primary action: "Enter marks" → S1. Secondary: "Grade summary" → S5 (filtered to this subject).

3. States

StateTriggerUI
Loadingsubjects fetchHero C-AppSkeleton
Foundid in listFull detail
Not foundid absent (or exam 404)C-AppErrorState RESOURCE_NOT_FOUND + retry
Coveredcount == totalProgress 100%, success tint
Partial0 < count < totalProgress partial, primary tint, "n remaining" hint
Zerocount == 0C-AppEmptyState compact, CTA "Enter marks"

4–7. Components/Tokens/Responsive/A11y

  • Same tokens as S1; info grid uses bodyMedium onSurfaceVariant labels.
  • Responsive: info grid 2 cols ≥ 600 px, 1 col below.
  • A11y: coverage progress announces "marks entered for {n} of {total} students"; hero card single Semantics node.

S3 — Student Results List

Route: /students/:studentId/results

1. Data contract

GET /api/v1/results/student/:studentId → array of ExaminationResult rows (examination-result.repository.ts:26-28): studentId, examinationSubjectId, marksObtained?, grade?, remarks?, publishedAt?, timestamps.

Flat rows, not grouped by exam — client groups by examinationSubjectId → exam lookup (GET /api/v1/examinations/:id, examination.controller.ts:33-35) to build the list. (planned) server-side grouping would remove N lookups.

2. Layout

  • Grouped list (S3-L): exam group headers (name + status chip); items per subject: subject name, marks obtained/max, grade chip, remarks.
  • FAB / header action: "Report card" per exam → S4 (needs both ids — result.controller.ts:32-37).
  • Filter chip row: All / Published (publishedAt set) / Unpublished / Grade (A+…F).

3. States

StateTriggerUI
LoadingfetchC-AppSkeleton list
Empty[] returnedC-AppEmptyState: "No results recorded"
LoadedrowsGrouped list; unpublished items dimmed (onSurfaceVariant)
Published-only filterfilter chipOnly publishedAt != null groups (examination-result.schema.ts:24-25)
Error401/403/429C-AppErrorState per 07_API_Conventions.md codes

4–7. Components/Tokens/Responsive/A11y

  • Grade chip colors: A+/A success, B+/B primary, C tertiary, D tertiary, F error (band mapping result.service.ts:130-138).
  • Responsive: single-column list everywhere; filters scroll horizontally.
  • A11y: each item announces "subject, marks of maximum, grade"; group headers titleSmall + Semantics(header: true).

S4 — Report Card View

Route: /students/:studentId/results/:examId

1. Data contract

GET /api/v1/results/report-card/:studentId/:examIdReportCard (result.service.ts:8-26):

FieldNotesSource
studentId, examinationIdecho of path paramsresult.service.ts:18-19
subjects[]subjectId, subjectName (= raw subject ID — client must map!), marksObtained (0 when missing, result.service.ts:70), maximumMarks, grade?, remarks?result.service.ts:68-81
totalMarksObtainedsum incl. 0-filled missingresult.service.ts:65-71
totalMaximumMarkssum of all subject maximaresult.service.ts:72
percentageround(obtained/max × 100, 2) — computed server-sideresult.service.ts:83-86
overallGradebands A+≥90, A≥80, B+≥70, B≥60, C≥50, D≥40, F<40 — computed server-sideresult.service.ts:95, 130-138
generatedAtISO string, per-requestresult.service.ts:96

2. Layout

  • Card header: exam name, student name, generatedAt (formatted local), overall grade hero chip.
  • Totals bar: totalMarksObtained / totalMaximumMarks + percentage (mono, 2 decimals).
  • Subject table (S4-T): subject (mapped name), marks, max, grade chip, remarks.
  • Actions: Share (web share — (forward-looking)); Print/PDF — (planned) (Results.md:57, Reports module).

3. States

StateTriggerUI
LoadingfetchC-AppSkeleton card + table
Success2xxFull card
Exam has no subjects404 No subjects found for this examination. (result.service.ts:51-52)C-AppErrorState → guided empty: "No subjects configured for this exam"
Student not found / cross-tenant404 (envelope per 07_API_Conventions.md §6)C-AppErrorState RESOURCE_NOT_FOUND
Missing subject rowsmarksObtained: 0, no gradeRow shows "0 / max" with "not entered" hint chip (data limitation: server cannot distinguish 0 from missing — result.service.ts:70)
NetworkofflineC-AppOfflineBanner + cached card if previously loaded (15_Flutter §Cache)

4–7. Components/Tokens/Responsive/A11y

  • Grade hero: displaySmall; totals headlineMedium mono; subject table per S1 §5.
  • Dark/light: card surface surfaceContainerLow; hero chip uses grade role colors (see S3).
  • Responsive: ≥ 600 px two-column (totals bar + grade hero side by side); below → stacked; table → stacked rows with labels.
  • A11y: hero Semantics(label: "Overall grade A plus") — never read "A+" as literal; table rows announced "Subject, marks x of y, grade z"; totals bar announced as one summary sentence. generatedAt in Semantics with full date. Contrast of grade chips ≥ 4.5:1 (09_Accessibility_Baseline.md).

S5 — Grade Summary (completion & distribution)

Route: /examinations/:examId/grades

1. Data contract

No dedicated endpoint (proposed) — blueprint GET /api/v1/results/analytics (Results.md:28) unimplemented. Client aggregates:

  1. GET /api/v1/examinations/:id/subjects (examination.controller.ts:51-53)
  2. per subject: GET /api/v1/results/exam-subject/:examSubjectId (result.controller.ts:21-25) Then computes client-side: coverage %, grade distribution (applying server bands result.service.ts:130-138 to the server-computed report-card percentages where possible; per-subject grades are client-supplied strings, so distribution uses stored grade strings), pass/fail vs passingMarks (examination-subject.schema.ts:30-31).

2. Layout

  • Class filter (from subject classId, examination-subject.schema.ts:15-16).
  • Coverage row: per-subject progress bars (entered / total).
  • Distribution chart: horizontal bar per grade (A+…F) — counts of stored grades; ungraded bucket.
  • Publish readiness banner: C-AppBanner warning when any subject has < 100% coverage; success when complete.
  • Action: "Publish results" → confirm dialog → POST /api/v1/examinations/:id/publish (examination.controller.ts:54-56).

3. States

StateTriggerUI
AggregatingN subject fetches in flightSkeleton bars
Readyall fetchedCharts + banner
Emptyno subjectsC-AppEmptyState "No subjects added to this exam"
Published examstatus published (examination.schema.ts:28-33)Banner → "Results published on {date}"; publish action disabled

4–7. Components/Tokens/Responsive/A11y

  • Charts: module-specific simple bar chart (no chart dependency, see 07_Component_Library.md + 15_Flutter §Charts). Bars use grade role colors; ungraded = surfaceVariant.
  • Tokens per S3 §5.
  • Responsive: bars stack vertically < 600 px; chart has scrollable horizontal axis.
  • A11y (charts/tables): every bar is a Semantics-labelled node "Grade A, 12 students (24%)"; raw counts also rendered as text (never chart-only); C-AppBanner announced via SemanticsService (09_Accessibility_Baseline.md §charts).

8. Cross-cutting a11y notes (all screens)

  • Charts and tables always ship a text equivalent; no pure-image data.
  • All error surfaces map envelope codes → copy (07_API_Conventions.md §3); 5xx → generic + requestId.
  • Focus order linear; C-AppSnackbar errors persist ≥ 4 s.