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

Production-grade specs for the attendance screens. The Marking Grid (S2) is the hero — full treatment first, then the remaining screens. Tokens: Sp = 00-shared/02 §3 spacing, Ty = typography §2, El = elevation §5, R = radius §4, Mo = motion §6. Components from 00-shared/03 and 07_Component_Library.md.


S2 — Marking Grid (hero)

1. Chrome & layout

AppBar: [back] "Grade 9 – B"  headlineMedium          [date picker] [history icon]
  subtitle bodyMedium: "Mon, 20 Jul 2026 · 23/40 marked" (live)
MarkedCountBar (sticky): 23 ✓ · 2 late · 1 half day · 1 holiday · 13 unmarked   [labelMedium chips]
StatusFilterChips (sticky, scrollable): All · Unmarked · Present · Absent · Late · Half day · Leave · Holiday
AttendanceGrid (primary scroll):
  header row: Roll No | Student (name + admission no) | Status (chip)
  rows 56 dp: [roll] [Avatar 32 + name + rollNumber] [StatusChip] [chevron]
  footer: "40 students · saved locally · 3 queued" bodySmall onSurfaceVariant
Batch action bar (appears on selection): [N selected] [Mark present/absent/late] [Clear]
FAB: "Mark all absent" (sweep) — hidden when selection active

Padding Sp 16 phone / Sp 24 tablet+; row height 56 (00-shared/04 §6); toolbar 64.

2. Status toggle semantics (the core interaction)

StatusDefault tap targetCycle order (tap)Visual
Present (default)1st tap → Absent; 2nd → Late; 3rd → back to Presentsuccess tint, check icon
Absent2nd tap→ Late → Presenterror tint, block icon
Late3rd tap→ Present → Absenttertiary/warning tint, clock icon
Half dayvia chip popover onlytertiary tint, half-circle
Leavevia chip popover onlyneutral surfaceVariant, vacation icon
Holidayvia chip popover only (bulk "mark holiday")neutral surfaceVariant, sun icon
  • Semantics: a row's status = the doc's status value (attendance.schema.ts:7-14). The chip shows exactly what will be persisted; no "pending" pseudo-status except during in-flight/queued states (see §7).
  • Default-present model: an unmarked student renders "Present" as the prospective state (teacher marks by exception). Only explicit taps create docs — unmarked rows send nothing. This matches upsert semantics: sending present would create docs for everyone; the UI must decide policy (see OQ-9 in 01: send-everyone vs send-changed-only). Decision: send-changed-only by default; "Mark all" sweep explicitly sends all.
  • Re-mark (overwrite): tapping an already-marked row overwrites the doc (attendance.repository.ts:23-29findOrCreate assigns new fields in place). The chip flips with Mo.m-fast; the prior status is shown as a transient label ("was: absent") for 2 s — no confirmation dialog for single rows (speed first).
  • checkIn/checkOut/remarks survive overwrites that don't include them: the upsert Object.assign only replaces keys present in the payload (attendance.repository.ts:25-27). The popover edit path must fetch the existing doc first (GET /attendance/:id) to prefill (OQ-6 notes PATCH quirks; for POST upsert, sending partial fields is safe).

3. Bulk selection & batch apply

  • Entry: long-press any row → multi-select mode (rows get checkboxes, batch bar slides up Mo.m-base); or "select all visible" via header checkbox; or FAB "Mark all absent" (one-tap sweep → confirm sheet).
  • Selection UX: selection counter "34 selected" labelLarge in batch bar; selected rows tint primaryContainer; deselect by tap again or Esc (desktop).
  • Apply flow: batch bar → status chip options → AppBottomSheet (S3) preview listing first 5 names + "+29 more", current statuses, target status; CTA "Apply to 34". On confirm → POST /attendance/bulk {records:[...]} (mark-attendance.dto.ts:51-54).
  • Conflict UI (remark warning): if selection includes rows already marked with a different status (present today), the sheet shows an amber AppBanner: "14 of 34 are already marked — they will be overwritten." (upsert semantics, attendance.repository.ts:23-29). Confirmation CTA changes to "Overwrite 34". Never silently overwrites without this banner — this is the one dialog in the grid flow.
  • Cross-class guard: if two selected students share the same studentId... impossible in one class; but if the same student appears under two class pickers on the same date, the unique index {tenantId, studentId, date} (attendance.schema.ts:59-62) keeps ONE doc; grid shows a surfaceVariant tooltip on first mark of the day (OQ-10: remark keeps original classId, attendance.repository.ts:29-33 — the row must display the class it was first marked under).
  • Sweep-all-absent: FAB → sheet "Mark all 40 as absent?" → bulk. Undo available for 4 s (UndoBar) — re-sends previous statuses via bulk.

4. Mark-late workflow (row popover)

Tap row's StatusChip (not the row) → StatusChipPopover (half-height sheet):

  • Status selector (6 chips, current highlighted).
  • Optional: checkIn time (AppTimePicker, default now, Sp 8 label "Check-in"), checkOut time (optional), remarks (AppTextField).
  • CTA "Save" → POST upsert with only changed keys. checkIn sent as ISO date-time (mark-attendance.dto.ts:32-33 accepts IsDateString).
  • Late rows keep the chip at tertiary; the sheet marks which fields are new vs existing ("checkIn currently 09:12").

5. Grid states

StateRender
Initial loadAppSkeleton list rows (40 × row skeleton, shimmer Mo.m-slow)
Loadedgrid; existing marks overlaid from GET /attendance/class/:classId?date=; unmarked = default-present chip (dashed outline to signal "not saved")
Mark in-flightchip shows mini CircularProgressIndicator (16 dp) inside chip; row disabled
Mark savedchip settles + lightImpact haptic; MarkedCountBar animates AnimatedSwitcher
Mark error (4xx/5xx)chip reverts to previous state; row tint errorContainer; AppSnackbar(error) "Couldn't save — retry"; retry button in snackbar
Conflict (409 DUPLICATE_RESOURCE, 00-shared/06 §5)snackbar "Already marked — reloading" + grid refresh (server upsert normally prevents this; occurs on stale offline flush)
OfflineAppOfflineBanner top; marks go to local queue (chip shows dashed + queue icon); reads from last-good cache
Empty classAppEmptyState "No students in this class"
403/404AppErrorState with code; hidden if role-gated earlier

6. Fast-marking patterns (behaviour hooks)

  • Tap cycle (3 states) as §2; long-press row = multi-select, not menu (gesture priority, 00-shared/08 §1).
  • Sticky MarkedCountBar always visible — the teacher's "done?" signal.
  • Filter chips collapse the grid (e.g., "Unmarked") for quick scanning; filters are client-side (roster already loaded).
  • Auto-advance (proposed, off by default): after marking row N's chip, focus moves to row N+1 — power-user toggle in settings.
  • Consecutive same-status taps across rows: keep the chip in selection mode after apply ("marked 3 more") — mini-tutorial in empty state.

7. Motion & haptics

  • Chip state flip: Mo.m-fast (150 ms, easeOutCubic) color/size transition + AnimatedSwitcher icon morph.
  • Batch bar slide-up: Mo.m-base; sheet showModalBottomSheet default.
  • Sweep confirm dialog: Mo.m-base scale+fade; destructive tint only when overwriting (not for first-time marks — overwrite is the conflict path, §3).
  • Haptics: saved lightImpact; error mediumImpact; long-press selectionClick (00-shared/08 §3); none on desktop.
  • Reduced motion: m-instant fades only (00-shared/08 §4).

8. Accessibility

  • Grid = semantic table: header row announces "Roll, Student, Status"; each row announces "Aarav Sharma, absent — double-tap to change" (00-shared/09 §7).
  • Status chips: Semantics(toggled:) + label; never color-only — every chip has an icon + text label (00-shared/09 §9, 00-shared/09:76).
  • Live region on MarkedCountBar updates ("23 marked, 17 unmarked").
  • Touch targets: chips ≥ 48×48 in grid mode (dense row variant allows 44, 00-shared/02 §9); row ≥ 56 dp.
  • Focus order: chips left→right per row, then next row; Esc exits selection; Tab enters grid table.
  • Text scale 2×: chips wrap to two lines; grid stays usable (chips drop to icon+short label "Abs" pattern for a11y density toggle).

9. Responsive & dark/light

  • Phone: full-width grid, chips right-aligned, MarkedCountBar horizontal scroll.
  • Tablet (≥ 840 dp): master-detail — S4 heatmap left pane, S2 right; grid columns student-name wider.
  • Desktop: hover states on chips (MouseRegion), click = tap, right-click = chip popover, keyboard shortcuts P/A/L/H/V/D on focused row, arrows navigate rows.
  • Dark mode: chip tints use dark token variants (errorContainer #8C1D18 etc.); dashed "unsaved" chip outline outlineVariant visible in both modes.

S3 — Batch Edit Sheet (half-height)

  • Trigger: multi-select → batch bar → status chip. Content: preview list (first 5 + N), current-vs-target statuses, conflict banner (§2.3), CTA "Apply to N" / "Overwrite N".
  • Submit: POST /attendance/bulk → on 200 data:[docs] → grid reconciles each row by studentId (docs are full docs with _id, attendance.service.ts:48-54).
  • Failure: snackbar "9 of 34 failed — retry" (retry re-sends only failed rows; safe — upsert). Server may throw on first bad record (sequential loop) — client should pre-validate all records client-side (valid enum + MongoIds) to avoid partial writes (OQ-2).

S4 — History / Calendar View

  • Month header: ‹ July 2026 › (desktop: arrows; phone: swipe month). CalendarHeatmap cells 40×40 dp (min), colored by day's dominant status; tap cell → DaySummaryCard (counts per status from that day's docs).
  • Data: per-day GET /attendance/class/:classId?date= fetches (30 calls/month). Cache strategy in 13_State_Management.md §3; month switch reuses cached days.
  • Unmarked day: neutral cell with "0 marked" tooltip. Future days: disabled.
  • Status filter chips above heatmap highlight matching days (e.g., only days with ≥1 absent).
  • Drill: cell tap → S2 read-only mode for that date (no FAB, chips disabled unless attendance.mark).

S7 — Student Attendance Profile (tab in /students/:id)

  • Header: student name + class, AppStatTiles: % present (rounded Math.round-style server formula mirrored: present/total dashboard.service.ts:57-64), absent count, late count, half-day count for the selected month.
  • CalendarHeatmap month; tap day → day detail (status + checkIn/checkOut + remarks + source badge — source from doc attendance.schema.ts:46-51, e.g. "via biometric").
  • Data: GET /attendance/student/:studentId?startDate&endDate (returns array, client groups by date; OQ-4 unpaginated).
  • Teacher with attendance.mark: day detail offers "Correct status" → PATCH flow (PATCH /attendance/:id, attendance.service.ts:81-101).
  • Parent: read-only; AbsenceAlertCard ((planned) dispatch, OQ-1) shows when the child has ≥3 absences in the visible window — copy says "3 absences" (never "consecutive", attendance.worker.ts:70-74).

S8 — Biometric Device Status

  • Device list: BiometricDeviceTile — name, model, location, status badge (active/inactive/offline, biometric-device.schema.ts:7-11), last-punch time, today's ingest count (from biometric_logs by deviceId+timestamp, biometric-log.schema.ts:26).
  • Status freshness rule: device active + no punch in 24 h → warn chip "no punches".
  • Pipeline banner (persistent until OQ-3): "Punches are captured — automatic sync to attendance is (planned)." — honest about biometric-sync cron (scheduler.service.ts:71-76) having no worker.
  • Add/edit device: (planned) form (name, deviceId unique per tenant biometric-device.schema.ts:35-36, model, location, config JSON).
  • Ingest log viewer: filter by device/student/day; fields studentId, deviceId, timestamp, mode, rawData (biometric-log.schema.ts:9-23).