06 — Screen Specifications (Reports Module)
- S1 — Report Catalog (
/reports) - S2 — Generate Report Form (
/reports/new) - S3 — Job List (
/reports/jobs) - S4 — Job Detail / Progress (
/reports/jobs/:jobId) - S5 — Result View (
/reports/jobs/:jobId/result) - S6 — Scheduled Reports Admin
(planned)
Detailed, per-state specifications for the five screens. Wire contracts in 12_API_Mapping.md; shared components in 00-shared/03; tokens in 00-shared/02 (M3, proposed). All server quotes from
src/modules/reports/**unless noted.
S1 — Report Catalog (/reports)
Purpose
Pick a report template. Static content derived from the server enum
(report-job.schema.ts:7-11).
Layout (mobile → tablet)
- AppBar: "Reports" (title), optional filter menu
(forward-looking). - List (AppListTile × 3):
- Report card — "Per-student marksheet with grades (A+…D)". Badge: needs student + exam.
- Attendance summary — "Counts per status for a class/period". Badge: can be scheduled daily/weekly.
- Fee summary — "Collections: paid / pending / overdue". Badge: no params.
- Below list: muted row "Result delivery: PDF/CSV coming soon"
(planned).
Data
Static table (no API). Sources: report-job.schema.ts:7-11,
generate-report.dto.ts:6-33, reports.service.ts:85-160.
States
| State | Render |
|---|---|
| idle | 3 tiles |
| permission-gap | module hidden (no report.read) (forward-looking) |
| offline | AppOfflineBanner; tiles still render (static) |
Permissions / events
- View:
report.read(permissions.constants.ts:40). reports.catalog.select(type)(proposed).
Accessibility / motion
- Tiles: min 48 dp, semantics label per tile;
m-fastpress ripple; page enterm-basefade-slide (00-shared/08).
S2 — Generate Report Form (/reports/new)
Purpose
Collect params per 08_Form_Specifications.md; POST and hand off to job detail.
Layout
- AppBar: "New report" + back.
- Fixed select (non-editable): report type from catalog selection.
- Param fields per type (08):
report_card: student picker + exam picker (both required by service —reports.service.ts:91).attendance_summary: class picker (optional), start date, end date (optional;generate-report.dto.ts:17-33).fee_summary: no fields; info card "No parameters needed".
- Summary row: params → chips (
reports.service.ts:31-33echoesparams). - Primary CTA: "Generate report" full-width
AppButton.
Submission contract
POST /reports/generate body = GenerateReportDto (generate-report.dto.ts:5-34):
{ "type": "attendance_summary", "classId": "…", "startDate": "2026-08-01", "endDate": "2026-08-31" }
Response: {jobId, status: "queued"} (reports.service.ts:43).
States
| State | Render |
|---|---|
| idle | form |
| validating | inline errors (presence for report_card — DTO does NOT enforce presence, only Mongo-Id format: generate-report.dto.ts:11-13,22-23) |
| submitting | button spinner, fields disabled (prevents duplicate job — server has no idempotency key) |
| success | navigate S4 with jobId |
| error 400 | inline per field (invalid type enum, malformed MongoId) |
| error 429 | countdown, no auto-retry (00-shared/10) |
| error 5xx | AppSnackbar + form preserved (re-POST is safe — new job per POST) |
| offline | AppOfflineBanner; submit blocked |
Behavior rules
- Presence validation for
studentId/examIdonreport_cardBEFORE POST — the server fails the job (async), not the request:"studentId and examId required"(reports.service.ts:91). Client must not ship that UX. - No double-submit while in flight.
- Date fields sent as strings (
generate-report.dto.ts:27-33), no format validation server-side — client sends ISOYYYY-MM-DD; server builds Mongo$gte/$lte(reports.service.ts:120-122).
Permissions / analytics
- Submit:
report.generate(permissions.constants.ts:39) — server does not enforce today (reports.controller.ts:9only JWT)(forward-looking). reports.generate.submit|success|failure(proposed).
S3 — Job List (/reports/jobs)
Purpose
Recent jobs, newest first.
Data — known client gap
No GET /reports endpoint exists. Server only has GET /reports/:jobId
(reports.controller.ts:20-24). The {tenantId, createdAt:-1} index
(report-job.schema.ts:48) implies list queries are intended. Client strategy:
- Local persisted history of submitted jobIds (max 50, oldest evicted);
- Poll only rows currently visible;
(forward-looking): addGET /reports?status=queued|processing|completed|failedfor full history + scheduled jobs visibility.
Layout
- AppBar "Report jobs".
- Filter chips
(forward-looking): All / Running / Completed / Failed. - List rows (AppListTile):
- Leading: status icon (queue/gear/check/cross).
- Title: type label.
- Subtitle:
createdAt(relative),jobIdshort. - Trailing: status
AppBadge(queuedgray,processingamber,completedgreen,failedred — enumreport-job.schema.ts:13-18); source badge "Scheduled" when from scheduler (local heuristic(planned)— no actorId stored on job doc).
- Pull-to-refresh: re-poll visible rows.
States
| State | Render |
|---|---|
| loading | AppSkeleton rows |
| empty | AppEmptyState "No reports yet" + CTA → catalog |
| error | banner + retry (poll failures do not clear list) |
Behavior
- Tap row → S4.
- Rows in terminal state are never re-polled in-session.
- Retry action on failed rows: re-POST original
paramsfrom job doc (report-job.schema.ts:33) → new jobId, row prepended.
Permissions / analytics
report.read(forward-looking);reports.job.list(proposed).
S4 — Job Detail / Progress (/reports/jobs/:jobId)
Purpose
Live lifecycle view + result entry. The polling hub (10_Interaction_Specification).
Data
GET /reports/:jobId (reports.controller.ts:20-24) → full job doc
(report-job.schema.ts:21-43):
{
"_id": "…", "tenantId": "…", "type": "fee_summary", "status": "completed",
"params": {"type":"fee_summary"}, "result": {"totalInvoices": 3, "totalCollected": 1500,
"totalPending": 400, "totalOverdue": 100}, "completedAt": "2026-08-03T…",
"createdAt": "…", "updatedAt": "…", "version": 0
}
Layout
- Header card: type label + status badge + createdAt + short jobId.
- Params card:
paramsas chips (empty → "No parameters"). - Status card (status-dependent, see table).
- Result card (completed only).
- Actions: Retry (failed), "New report from these params" (completed),
Download/Share
(planned).
Status-dependent UI
| Status | UI | Polling |
|---|---|---|
queued | "Waiting in queue" + indeterminate progress row | every 2 s |
processing | "Generating…" + indeterminate progress row + elapsed timer | every 2 s |
completed | success icon; result card; CTAs | stop |
failed | error icon; error text from error field (report-job.schema.ts:39); Retry | stop |
Notes:
- No progress % — server has none; indeterminate only.
- Elapsed timer computed client-side from
createdAt. - Terminal state is sticky: no re-poll, cached doc until screen exit.
States
| State | Render |
|---|---|
| loading | full skeleton |
| 404 | "Report not found" empty state + back (reports.service.ts:48) |
| network fail | banner; polling paused; manual "Retry check" |
| offline | banner + last-good doc |
| cross-tenant 404 | same "not found" (server scopes by tenant via BaseRepository — no leak, base.repository.ts:21-23) |
Permissions / analytics
report.read(forward-looking).reports.job.status(jobId,status),reports.job.retry(jobId)(proposed).
S5 — Result View (/reports/jobs/:jobId/result)
Purpose
Render completed result per type; export (planned).
Result renderers (from reports.service.ts:96-160)
Report card (reports.service.ts:96-107):
- Header:
studentName(⚠ currently admission number,:98),examId,generatedAt. - Subject table: per row
subjects[](name, marks, max, grade — shape fromresult.service.ts:68-91). - Footer metrics:
totalMarksObtained/totalMaximumMarks,percentage,gradebadge (bands A+…D,result.service.ts:130-136).
Attendance summary (reports.service.ts:131-136):
- Metric cards:
total+ per-status counts (summarykeyed by status — present/late/absent/etc. as stored by attendance module). - Period line:
classId,period.startDate/endDate(nulls → "All time").
Fee summary (reports.service.ts:154-158):
- 4 metric cards:
totalInvoices,totalCollected,totalPending,totalOverdue(numbers frominvoiceRepo.find({})+ paid/due math:142-152). - Derived: collection rate = collected ÷ totalInvoices
(client-derived).
States
| State | Render |
|---|---|
| ready | renderer above |
| completed but result missing | empty state (e.g. zero attendance) — result may be {} (zero-record summaries still complete, :131-136) |
| not completed | guard: redirect to S4 (result only reachable when status == completed) |
| offline | last-good cached result + banner |
Export (planned)
- "Download CSV"/"Share PDF" — NO endpoint today. Blueprint:
GET /reports/:jobId/download(04-Modules/Reports.md:26), streamed (:49), files undersl/{tenantId}/reports/{uuid}TTL (:50). - Until then: hide export CTAs or show "coming soon" tooltip.
Permissions / analytics
report.read;reports.result.open|download(proposed).
S6 — Scheduled Reports Admin (planned)
- Scheduler enqueues attendance reports daily/weekly today
(
attendance-report.job.ts:13-24), but there is no UI to configure. - Future screen: schedule list + CRUD (perm
scheduler.read/create/delete,permissions.constants.ts:94-96); result surfaced through job list.