06 — Screen Specifications (Results Module)
- S1 — Marks Entry Grid
- S2 — Exam-Subject Detail
- S3 — Student Results List
- S4 — Report Card View
- S5 — Grade Summary (completion & distribution)
The largest doc: full behaviour per screen. References shared specs by name: 02_Design_Tokens.md (tokens
T-), 03_Component_Library.md (componentsC-), 09_Accessibility_Baseline.md (a11y rules),06_State_Management.md(cubits), 07_API_Conventions.md (envelope/errors). Server facts cited withfile: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
| Field | Source |
|---|---|
Subject context: date, startTime, endTime, maximumMarks, passingMarks | examination-subject.schema.ts:18-31 |
Rows: studentId, marksObtained?, grade?, remarks?, publishedAt? | examination-result.schema.ts:8-25 |
Load: GET /api/v1/results/exam-subject/:examSubjectId | result.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 countn/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
| State | Trigger | UI |
|---|---|---|
| Loading | cubit fetch of S1-T + header | C-AppSkeleton rows (shimmer, 8 rows) |
| Loaded, clean | fetch ok, no edits | Table + C-AppSnackbar-free header chip Draft |
| Loaded, dirty | ≥1 field edited | Save bar active; per-row unsaved dot |
| Saving row | upsert in flight | Row spinner on that row only (per-row concurrency, examination.service.ts:147-175 upsert) |
| Saved | 2xx | Row clears dirty state; C-AppSnackbar success < 3 s |
| Row rejected | 404 RESOURCE_NOT_FOUND from examination.service.ts:145-146 (marks > max) | Inline field error: "Marks cannot exceed {maximumMarks}"; row keeps dirty state |
| Network fail | timeout/offline | C-AppOfflineBanner; edits preserved in offline draft (15_Flutter §Offline) |
| Empty | exam-subject has no students yet | C-AppEmptyState: "No students to grade yet" |
Published (server row has publishedAt, examination-result.schema.ts:24-25) | row-level | Row 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:onSurfaceVariantitalic; over-max error:error+errorContainerfield fill. - Publish chip:
secondaryContainer-stylesuccess; draft chip:surfaceVariant. - Numerals
mono14/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.
maximumMarkshint 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:
Semanticsheader row; each row labelled "Student {name}, marks {value} of {max}". - Marks field: label "Marks obtained — {student}" +
errorTextannounced viaSemanticsServiceon 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;
startTime–endTime;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
| State | Trigger | UI |
|---|---|---|
| Loading | subjects fetch | Hero C-AppSkeleton |
| Found | id in list | Full detail |
| Not found | id absent (or exam 404) | C-AppErrorState RESOURCE_NOT_FOUND + retry |
| Covered | count == total | Progress 100%, success tint |
| Partial | 0 < count < total | Progress partial, primary tint, "n remaining" hint |
| Zero | count == 0 | C-AppEmptyState compact, CTA "Enter marks" |
4–7. Components/Tokens/Responsive/A11y
- Same tokens as S1; info grid uses
bodyMediumonSurfaceVariantlabels. - Responsive: info grid 2 cols ≥ 600 px, 1 col below.
- A11y: coverage progress announces "marks entered for {n} of {total} students"; hero card single
Semanticsnode.
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 (
publishedAtset) / Unpublished / Grade (A+…F).
3. States
| State | Trigger | UI |
|---|---|---|
| Loading | fetch | C-AppSkeleton list |
| Empty | [] returned | C-AppEmptyState: "No results recorded" |
| Loaded | rows | Grouped list; unpublished items dimmed (onSurfaceVariant) |
| Published-only filter | filter chip | Only publishedAt != null groups (examination-result.schema.ts:24-25) |
| Error | 401/403/429 | C-AppErrorState per 07_API_Conventions.md codes |
4–7. Components/Tokens/Responsive/A11y
- Grade chip colors: A+/A
success, B+/Bprimary, Ctertiary, Dtertiary, Ferror(band mappingresult.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/:examId → ReportCard (result.service.ts:8-26):
| Field | Notes | Source |
|---|---|---|
studentId, examinationId | echo of path params | result.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 |
totalMarksObtained | sum incl. 0-filled missing | result.service.ts:65-71 |
totalMaximumMarks | sum of all subject maxima | result.service.ts:72 |
percentage | round(obtained/max × 100, 2) — computed server-side | result.service.ts:83-86 |
overallGrade | bands A+≥90, A≥80, B+≥70, B≥60, C≥50, D≥40, F<40 — computed server-side | result.service.ts:95, 130-138 |
generatedAt | ISO string, per-request | result.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
| State | Trigger | UI |
|---|---|---|
| Loading | fetch | C-AppSkeleton card + table |
| Success | 2xx | Full card |
| Exam has no subjects | 404 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-tenant | 404 (envelope per 07_API_Conventions.md §6) | C-AppErrorState RESOURCE_NOT_FOUND |
| Missing subject rows | marksObtained: 0, no grade | Row shows "0 / max" with "not entered" hint chip (data limitation: server cannot distinguish 0 from missing — result.service.ts:70) |
| Network | offline | C-AppOfflineBanner + cached card if previously loaded (15_Flutter §Cache) |
4–7. Components/Tokens/Responsive/A11y
- Grade hero:
displaySmall; totalsheadlineMediummono; 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.generatedAtinSemanticswith 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:
GET /api/v1/examinations/:id/subjects(examination.controller.ts:51-53)- per subject:
GET /api/v1/results/exam-subject/:examSubjectId(result.controller.ts:21-25) Then computes client-side: coverage %, grade distribution (applying server bandsresult.service.ts:130-138to the server-computed report-card percentages where possible; per-subject grades are client-supplied strings, so distribution uses storedgradestrings), pass/fail vspassingMarks(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-AppBannerwarning when any subject has < 100% coverage;successwhen complete. - Action: "Publish results" → confirm dialog →
POST /api/v1/examinations/:id/publish(examination.controller.ts:54-56).
3. States
| State | Trigger | UI |
|---|---|---|
| Aggregating | N subject fetches in flight | Skeleton bars |
| Ready | all fetched | Charts + banner |
| Empty | no subjects | C-AppEmptyState "No subjects added to this exam" |
| Published exam | status 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-AppBannerannounced viaSemanticsService(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-AppSnackbarerrors persist ≥ 4 s.