06 — Screen Specifications (Attendance Module)
- S2 — Marking Grid (hero)
- S3 — Batch Edit Sheet (half-height)
- S4 — History / Calendar View
- S7 — Student Attendance Profile (tab in /students/:id)
- S8 — Biometric Device Status
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)
| Status | Default tap target | Cycle order (tap) | Visual |
|---|---|---|---|
| Present (default) | — | 1st tap → Absent; 2nd → Late; 3rd → back to Present | success tint, check icon |
| Absent | 2nd tap | → Late → Present | error tint, block icon |
| Late | 3rd tap | → Present → Absent | tertiary/warning tint, clock icon |
| Half day | via chip popover only | — | tertiary tint, half-circle |
| Leave | via chip popover only | — | neutral surfaceVariant, vacation icon |
| Holiday | via chip popover only (bulk "mark holiday") | — | neutral surfaceVariant, sun icon |
- Semantics: a row's status = the doc's
statusvalue (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
presentwould create docs for everyone; the UI must decide policy (see OQ-9 in01: 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-29—findOrCreateassigns new fields in place). The chip flips withMo.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.assignonly 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"
labelLargein batch bar; selected rows tintprimaryContainer; deselect by tap again orEsc(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 asurfaceVarianttooltip on first mark of the day (OQ-10: remark keeps originalclassId,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:
checkIntime (AppTimePicker, default now,Sp 8label "Check-in"),checkOuttime (optional),remarks(AppTextField). - CTA "Save" → POST upsert with only changed keys.
checkInsent as ISO date-time (mark-attendance.dto.ts:32-33acceptsIsDateString). - Late rows keep the chip at
tertiary; the sheet marks which fields are new vs existing ("checkIn currently 09:12").
5. Grid states
| State | Render |
|---|---|
| Initial load | AppSkeleton list rows (40 × row skeleton, shimmer Mo.m-slow) |
| Loaded | grid; existing marks overlaid from GET /attendance/class/:classId?date=; unmarked = default-present chip (dashed outline to signal "not saved") |
| Mark in-flight | chip shows mini CircularProgressIndicator (16 dp) inside chip; row disabled |
| Mark saved | chip 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) |
| Offline | AppOfflineBanner top; marks go to local queue (chip shows dashed + queue icon); reads from last-good cache |
| Empty class | AppEmptyState "No students in this class" |
| 403/404 | AppErrorState 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 +AnimatedSwitchericon morph. - Batch bar slide-up:
Mo.m-base; sheetshowModalBottomSheetdefault. - Sweep confirm dialog:
Mo.m-basescale+fade; destructive tint only when overwriting (not for first-time marks — overwrite is the conflict path, §3). - Haptics: saved
lightImpact; errormediumImpact; long-pressselectionClick(00-shared/08 §3); none on desktop. - Reduced motion:
m-instantfades 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;
Escexits selection;Tabenters 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 shortcutsP/A/L/H/V/Don focused row, arrows navigate rows. - Dark mode: chip tints use dark token variants (
errorContainer #8C1D18etc.); dashed "unsaved" chip outlineoutlineVariantvisible 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 200data:[docs]→ grid reconciles each row bystudentId(docsare 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).CalendarHeatmapcells 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 in13_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 (roundedMath.round-style server formula mirrored: present/totaldashboard.service.ts:57-64), absent count, late count, half-day count for the selected month. CalendarHeatmapmonth; tap day → day detail (status + checkIn/checkOut + remarks + source badge —sourcefrom docattendance.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 (frombiometric_logsbydeviceId+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 aboutbiometric-synccron (scheduler.service.ts:71-76) having no worker. - Add/edit device:
(planned)form (name, deviceId unique per tenantbiometric-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).