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

09 — User Behaviour (Academics Module)

Expected behaviour derived from server semantics (src/modules/academics/services/*)

  • shared interaction rules (00-shared/08), the common mistakes users make with structure data, and the UX countermeasures. Purpose: a playbook of what the user does so the flows never surprise.

1. Expected behaviour (server-contract-driven)

  • Duplicates are rejected by name/code, never silently merged: creating a year or grade with an existing name, or a subject with an existing code, returns 409 DUPLICATE_RESOURCE with exact server copy ("Academic year "X" already exists.", academic-year.service.ts:25-29; grade.service.ts:21-23; subject.service.ts:21-25). The client must never pre-empt with its own copy — render the server message verbatim.
  • set-current is a global, one-way-ish flip: PATCH /academic-years/:id/set-current clears isCurrent on every other year, then sets the target isCurrent:true and status:"active" (academic-year.service.ts:69-83). The old current year does not go to archived automatically — it stays active minus the flag. Users expect "old year becomes archived"; the UI must show the real result.
  • Delete is soft and silent to children: grade/section/class/assignment delete = softDelete (base.repository.ts:68-74); lists simply stop showing the row. Nothing cascades — sections/classes/assignments pointing at a deleted grade remain in the DB and keep appearing in other lists (OQ-3).
  • Correction of an assignment is delete + recreate: there is no assignment update endpoint (subject-assignment.controller.ts:19-39); a wrong teacher is fixed by removing the row and adding a new one.
  • Ordering is server-given, not user-sortable: grades render in displayOrder asc (grade.service.ts:37-39); years newest-first (createdAt:-1, academic-year.service.ts:45-47); classes/sections/subjects in insertion order (class.service.ts:41-53, section.service.ts:41-48, subject.service.ts:35-47). Reordering grades = editing displayOrder (int ≥ 0), not drag-and-drop (until a reorder endpoint exists — (planned)).
  • q (global search) is accepted but ignored: PaginationQueryDto.q (pagination-query.dto.ts:26-29) is parsed on every list endpoint but no academics service filters on it (services filter {}, e.g. grade.service.ts:41-43). Users will type in a search box and get full lists — client search must be local.
  • Grade/section/class status is an open string (default 'active', grade.schema.ts:21-22, section.schema.ts:24-25, class.schema.ts:33-34); only academic-year status is an enum (upcoming|active|archived, academic-year.schema.ts:7-11). Treat any non-active value as inactive.

2. Common mistakes & countermeasures

MistakeRelatedUX countermeasure
Creating "Grade 1" twice ("1" vs "One" vs "Grade-01")409 name checkpre-check against loaded cache with fuzzy hint ("A grade named similar exists"); still render server 409
Year name format drift ("2026-2027" vs "2026-27" vs "2026/27")409fixed format helper + example in the field; paste-tolerant parser (proposed)
Subject code case drift ("math101" vs "MATH101") — server matches exact stringunique code (subject.service.ts:21-25, index subject.schema.ts:38)auto-uppercase formatter + warning when cache shows a case-variant
Tapping "Set as current" on an archived/upcoming yearsetCurrent forces active (academic-year.service.ts:78-80)confirm dialog: "This year will become active"; show resulting status
Deleting a grade that still has sections/classesno cascade (OQ-3)destructive dialog with honest copy "Sections and classes will remain but become orphaned"; suggest archiving instead
Double-assigning the same subject to a classno server duplicate check (OQ-4, subject-assignment.service.ts:13-17)picker disables already-assigned (subject, teacher) pairs; row-level warning if duplicates exist from another device
Fixing an assignment by "editing" the rowno update endpoint"Replace" flow (delete + recreate) with single confirm, not two steps
Typing passingMarks > maximumMarksno cross-field validation server-side (OQ-5)client-side block + inline error; keep server copy as fallback
Expecting search to filter server-sideq unused (OQ-7)label the search "Filter (on this device)" — or hide until server search lands
Reordering classes/sections by draginsertion order server-sideno drag UI; order = creation order; document in empty-state hint

3. Power & new users

  • Power user (admin/coordinator, desktop): keyboard N = new entity on list screens, Ctrl+Enter = submit; year switcher keyboard shortcut; bulk-skip pattern in ladder setup (create Grade 1 → "Save & new" stays in form, displayOrder auto-incremented).
  • New user (fresh tenant): first-run empty states chain — create year → create grades → create sections → create classes → assign subjects. Each AppEmptyState CTA navigates the next step; a mini checklist ("Step 2 of 5") helps structure-first onboarding (proposed).

4. Mobile & a11y users

  • One-thumb phone: all admin forms are full-screen with the primary CTA at the bottom (thumb zone); the year switcher and filter chips stay within the top ⅓.
  • TalkBack/VoiceOver: tree nodes announce level + counts ("Grade 10, expanded, 4 sections"); chips announce full labels ("Section A, capacity 40"); conflict banners read in a live region.
  • Dynamic type 200%: chips and status badges wrap (no fixed widths); SectionChips overflow falls back to "+N more" (07 §2).
  • Reduced motion: no shake on validation; errors = color + icon + text (00-shared/08 §4).

5. Interrupted sessions & retries

  • App killed mid-create form: form state lost (memory only) → return = fresh form; cached lists still available (13 §3) so the user can re-check duplicates.
  • Server 5xx on create: generic error + requestId; form values preserved; suggest retry. Duplicate re-submit risk: a 409 "already exists" then means the first attempt actually succeeded — the client should offer "Open existing".
  • 401 mid-flow: silent refresh → replay once → sessionExpired → re-login, in-progress form preserved in memory.
  • 429: countdown banner, no auto-retry (00-shared/07 §4).

6. Abandonment

Where people quitWhat to do
Year/grade ladder setup mid-way (forms)no partial writes (single-doc creates); re-entry = empty form; keep "resume" via cached lists
Assignment matrix after adding 2 of 5 subjectsno batch API — client memoizes in-progress sheet state; offer "finish later" row on class detail (proposed)
Delete confirm dialogssafe: nothing deleted until confirm; back = cancel

7. Frustration points → UX treatments (summary)

FrustrationTreatment
409 on a name that "looks unique" (spacing/case)show server message verbatim + the conflicting existing row (client lookup by name)
Duplicate rows appearing from another devicerefresh on screen focus; conflict banner + merge/remove action
Long class lists with no orderingyear filter chip defaults "This year"; client group-by-grade view toggle (proposed)
Orphaned structure after a delete elsewhereorphan detection runs client-side (loaded cache); banner + "reparent or archive" action
Marks math confusing (theory+practical vs max)live summary line under the form: "80 + 20 = 100 ≤ max 100"

8. Cross-device & conflict

  • Two admins editing the same structure: last-write-wins — every doc carries a version counter incremented per update (base.repository.ts:57-66) but the client does not send it (PATCH bodies are plain $set DTOs, update-class.dto.ts:4-50); no 409/412 optimistic-concurrency errors surface. UI: refresh lists on focus; treat remote changes as truth.
  • set-current raced from two devices: both PATCHes run sequentially server-side; the later one wins — the client re-reads the years list after the call to render the true current year.

9. Adoption path (holistic)

P0 (backend already complete): structure browse + create/edit for admin. P1: teacher roster, student browse, conflict UX. P2 (planned): server-side search (q), conflict/duplicate validation, cascade jobs (RELATIONSHIPS.md:134-138), coaching extension (docs/IMPLEMENTATION_PLAN.md §6.2.1).