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

Production-grade specs for each Academics screen: chrome, states, components, micro-interactions, a11y, responsive, dark/light, keyboard, motion. Tokens: Sp = 00-shared/02 §3, Ty = typography §2, El = elevation §5, R = radius §4, Mo = motion §6. Components reference 00-shared/03 and module components in 07_Component_Library.md. Wire facts: all reads return raw docs in data; lists paginated with meta{page,limit,totalItems,totalPages,hasNext,hasPrevious} (pagination-query.dto.ts:32-39).


0. Shared chrome & conventions (all screens)

  • Year switcher: AppDropdown chip in the AppBar (values = cached years, 13_State_Management.md §3); selection is a ReferenceScope in the cubit that re-scopes every academicYearId query; default = current year (isCurrent:true) or first year if none flagged.
  • Permission gating: all CTAs hidden for read-only roles (P4, 02); route guard (planned) for academics.* perms (OQ-1). Until then, hide by role set.
  • Status rendering: year status enum → badge (upcoming=outline, active=tonal/primary, archived=neutral) (academic-year.schema.ts:7-11); grade/section/class status is a free string — treat 'active' = on, anything else = archived badge (OQ-2 in 01).
  • Empty states: one AppEmptyState per entity with its primary CTA.
  • Loading: AppSkeleton(list) ≤ 200 ms to first frame; content ≤ 2 s (00-shared/10 §1).
  • Errors: AppErrorState with server code; 409/422 → inline banner; 5xx → generic + requestId hint; 401 → silent refresh → re-login.

1. Academic Year List /academics/years

Chrome: AppBar "Academic Years" + year switcher (hidden on this screen); FAB "Add year" (extended, Icons.add); padding Sp.16 (phone) / Sp.24.

Row (AppListTile):

  • Leading: year monogram avatar (Icons.calendar_month) — current variant primaryContainer.
  • Title: name (Ty.titleMedium).
  • Subtitle: startDate → endDate (locale Intl, dd MMM yyyy), bodySmall onSurfaceVariant.
  • Trailing: status badge + AppMenu (Set as current / Edit / Delete).

States:

  • idle → list from GET /academic-years (sort=-createdAt server default).
  • paginated: hasNext → infinite scroll footer spinner (Mo.m-instant).
  • pull-to-refresh: re-fetch page 1.
  • current-year row pinned first with a primaryContainer leading pill "Current" (client sort: isCurrent desc, then server order).

Conflict-free actions: "Set as current" → AppDialog confirm ("This will un-flag all other years") → PATCH /academic-years/:id/set-current (academic-year.service.ts:69-83) → row badges re-render; snackbar "Year 2026-2027 is now current". Optimistic: no (server flips multiple docs) — server-first, success snackbar.

Delete: destructive AppDialog (typed confirm of the year name — irreversible-ish soft delete, 00-shared/05 §5); on 200 row removed with AnimatedList exit.

A11y: row Semantics(button) + "current/upcoming/active/archived"; badge announced; pagination live region "Loaded N of M years".

Tablet/desktop: master-detail at ≥ 840 dp — list left, detail right (/academics/years/:id); keyboard N = new year; Ctrl+Enter = set current.

2. Academic Year Detail /academics/years/:id

Chrome: AppBar back + title name; header card.

Header card (AppCard tonal when isCurrent):

  • headlineMedium name; badge status + "Current" pill.
  • Meta rows (AppListTile-like): Start date, End date (bodyMedium), Created (bodySmall).
  • Actions row: TonalButton "Set as current" (hidden if already current); OutlinedButton "Edit" → form screen; icon menu → Delete.

Classes of this year section (titleMedium):

  • Feed = GET /classes/by-year/:academicYearId paginated (class.controller.ts:30-35).
  • Rows = class name + grade/section chips (client-joined), capacity.
  • Tap → /academics/classes/:id (master screen).
  • Empty → AppEmptyState compact "No classes yet for this year" + "Add class".

Edge: year status editable to archived even while isCurrent — client warns "Archiving the current year?" (server allows it — update-academic-year.dto.ts:20-27, academic-year.service.ts:59-67).

3. Grades List /academics/grades

Chrome: AppBar "Grades"; FAB "Add grade"; optional grade code chip.

Row:

  • Leading: rank badge displayOrder (mono, Ty.labelMedium, circled).
  • Title: name (Ty.titleMedium); trailing: code chip (mono) if set; status badge (non-active only); AppMenu (Edit / Delete).
  • Subtitle: "N sections" (count from client cache of by-grade) + "N classes".

Ordering: server default displayOrder:1 (grade.service.ts:39) — no client re-sort; reorder = edit displayOrder (form field, Min(0)).

Create (screen 14.1): full-screen form (5+ fields) per 08 §1.

Conflict: server 409 Grade "Grade 1" already exists. → inline AppBanner (error) at top + autofocus name field (grade.service.ts:23).

Delete: confirm dialog warns "Sections and classes of this grade will remain but become orphaned" — honest copy; cascade (planned) (OQ-3).

4. Grade Detail /academics/grades/:id

Header: name + code + displayOrder + status; actions Edit/Delete. Sections section: SectionChips (module component) from GET /sections/by-grade/:gradeId — tap chip → section detail; "Add section" → form (section picker pre-filled with this grade). Classes section: client-joined rows from classes cache where gradeId matches; shows which year each belongs to (badge) since grades may float across years (grade.schema.ts:9-10).

5. Classes List /academics/classes

Chrome: AppBar "Classes" + year switcher + filter chips (AppChips): All / This year / Active only (client filter on status).

Row:

  • Leading: Icons.groups avatar.
  • Title: name (Ty.titleMedium) — e.g. "Grade 10 - A" (create-class.dto.ts:22).
  • Subtitle: joined chips "Grade 10 · Section A" + year name; capacity right-aligned mono "40".
  • Trailing: status badge; AppMenu (Edit / Delete).

Feed: GET /classes (all) or GET /classes/by-year/:academicYearId when a year is selected (server-filtered, paginated). Both unsorted → preserve order.

Conflicts: duplicate class names per (year, grade, section) are possible (class.service.ts:16-18) — client warns with a tertiary "Duplicate" chip on rows sharing (gradeId, sectionId, name) (OQ-4).

Create (screen 14.3): cascading pickers Year → Grade → Section (13 §2); name auto-suggested "Grade {name} - {section}" (editable).

6. Class Detail — MASTER /academics/classes/:id

Chrome: AppBar back + class name; two tabs: Overview · Subjects & teachers; FAB: on Overview tab = Edit, on roster tab = Add subject+teacher (small FAB morph via AnimatedSwitcher, Mo.m-fast).

Overview tab:

  • Header card: name headlineMedium, grade/section/year chips, status badge.
  • Meta AppCard: Capacity (mono), Class teacher (name resolved from users cache — classTeacherId is a user ObjectId per create-class.dto.ts:32-35), Room (roomId, free string — render as text, OQ-6), Campus (campusId, opaque id — hide unless resolved, OQ-6).
  • Links section (AppListTiles): "Timetable" → /timetable?classId= (timetable.controller.ts:22-29); "Attendance" → attendance module; "Students" → students module ((planned) count from class_enrollments).

Subjects & teachers tab (assignment matrix — module core):

  • Roster = GET /subject-assignments/by-class/:classId?academicYearId= (bare array, subject-assignment.controller.ts:24-29).
  • Rows: SubjectAssignmentRow — subject name + code chip + shortName, teacher name, trailing AppMenu (Replace / Remove).
  • Conflict surface: duplicate subject rows → AppBanner(warning) "Same subject assigned twice" + row highlighted tertiary; empty roster → AppEmptyState compact "No subjects assigned yet" + "Add subject".
  • Replace = delete + create (no update endpoint, subject-assignment.controller.ts:19-39); Remove = destructive confirm → DELETE /subject-assignments/:id; stale id → 404 Assignment not found. (subject-assignment.service.ts:34) → treat as already-removed, refresh.

Tablet/desktop (≥ 840 dp): detail pane shows tabs side-by-side with the list pane; roster tab renders as a table (columns: Subject | Code | Teacher | Actions) with column headers announced (00-shared/09 §7).

A11y: tab semantics; each roster row = one Semantics(button) group with action hints; conflict banner in a live region.

7. Section Detail /academics/sections/:id

Header: name headlineMedium, grade chip (joined), status badge. Meta card: Capacity, Class teacher, Room (same resolution rules as class). Classes using this section: client-joined rows (from classes cache where sectionId matches) with year badges. Actions: Edit / Delete (orphan warning on delete — sections are referenced by classes with required sectionId, class.schema.ts:18-19).

8. Sections List /academics/sections

Simple list; rows = section name + grade label chip + capacity; filter chips (All / By grade via GET /sections/by-grade/:gradeId). FAB "Add section" (grade picker first).

9. Subjects List /academics/subjects

Chrome: AppBar "Subjects"; FAB "Add subject"; search via AppSearchBar client-side only (server q unused — OQ-7 in 01).

Row:

  • Leading: Icons.menu_book.
  • Title: name; subtitle: code mono + shortName if set.
  • Trailing: marks summary bodySmall "100 max · pass 33" (admin/coordinator only — hidden for P4) + status badge + AppMenu.

Ordering: insertion order (subject.service.ts:35-47); no reorder field exists — client preserves.

10. Subject Detail /academics/subjects/:id

Header: name + code chip + shortName + status. Marks card (AppCard): grid of four stat tiles — Maximum, Passing, Theory, Practical (Ty.titleMedium numerals with tabularFigures(), 00-shared/02 §2); values from subject.schema.ts:21-32. Edit → form without theory/practical fields (not updatable — update-subject.dto.ts:4-42; flagged, OQ-5). Usage section: "Assigned to N classes" — client-joined count from cached rosters; rows link to class detail.

11. Subject Assignment Matrix (full screen) /academics/classes/:id/assign

Full-screen sheet (≥ 3 fields, 00-shared/05 §5):

  • Header: class name + year.
  • Existing rows (same SubjectAssignmentRow as §6).
  • "Add" opens AppBottomSheet (2/3 height):
    • AppDropdown Subject (searchable) — pre-loaded catalog; entries already assigned to this class are disabled with "Already assigned" note.
    • AppDropdown Teacher (searchable) — user pickers filtered to teacher roles (Users module); (planned) server-side teachers list.
    • CTA "Assign" → POST /subject-assignments {teacherId, subjectId, classId, academicYearId} (all IsMongoId, create-subject-assignment.dto.ts:5-19).
  • Conflict: if server someday returns 409/422 → banner; today server never rejects duplicates (OQ-4) — client guard only.

12. Hierarchy Explorer /academics/explore

Chrome: AppBar "Structure"; year switcher; search box (client filter on names). Layout:

  • HierarchyTree (module component, 07 §1): root = Year → children = Grades → Section → Class leaves; each node shows counts (sections/classes/subjects).
  • Expand/collapse with AnimatedSize (Mo.m-base); default: years expanded, grades collapsed.
  • Leaf tap → class master screen; node context menu (Edit / Add child) for admin/coordinator. Gap spotting (coordinator): nodes with 0 children get a tertiary dot + "empty" label; class nodes without subjects get a warning badge. Data: composed client-side from the 4 list endpoints (no tree endpoint exists) — see 15_Flutter_Implementation_Guide.md §4.

13. Teacher Roster /academics/teachers/:teacherId

Chrome: AppBar teacher name; year switcher (query academicYearId). Rows: class name + subject (+ code), grouped by class (List.groupBy, 00-shared/11 §8). Conflict: same subject in ≥ 2 classes → AppBanner(warning)

  • row highlights. Empty: "No teaching load this year". Data: GET /subject-assignments/by-teacher/:teacherId?academicYearId= (subject-assignment.controller.ts:30-35).

Interaction & motion summary (module)

ConcernSpec
Screen enterMo.m-base slide; list→detail uses Hero on row leading avatar → header card (Mo.m-slow)
FAB actionsscale-in Mo.m-entrance; hide on scroll-down
Chip toggles (filters, year switcher)Mo.m-fast fill/outline swap
Conflict bannerslide-down + fade Mo.m-base; live region
Row removeAnimatedList exit Mo.m-fast; snackbar confirm
Reduced motionall fade ≤ Mo.m-instant (00-shared/08 §4)

Keyboard (desktop)

N = new on list screens; Ctrl+Enter = submit forms / set-current; Esc = close sheets/dialogs; Tab logical order Year→Grade→Section→Class→Subject (00-shared/08 §2).