06 — Screen Specifications (Academics Module)
- 0. Shared chrome & conventions (all screens)
- 1. Academic Year List
/academics/years - 2. Academic Year Detail
/academics/years/:id - 3. Grades List
/academics/grades - 4. Grade Detail
/academics/grades/:id - 5. Classes List
/academics/classes - 6. Class Detail — MASTER
/academics/classes/:id - 7. Section Detail
/academics/sections/:id - 8. Sections List
/academics/sections - 9. Subjects List
/academics/subjects - 10. Subject Detail
/academics/subjects/:id - 11. Subject Assignment Matrix (full screen)
/academics/classes/:id/assign - 12. Hierarchy Explorer
/academics/explore - 13. Teacher Roster
/academics/teachers/:teacherId - Interaction & motion summary (module)
- Keyboard (desktop)
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 indata; lists paginated withmeta{page,limit,totalItems,totalPages,hasNext,hasPrevious}(pagination-query.dto.ts:32-39).
0. Shared chrome & conventions (all screens)
- Year switcher:
AppDropdownchip in the AppBar (values = cached years, 13_State_Management.md §3); selection is aReferenceScopein the cubit that re-scopes everyacademicYearIdquery; 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)foracademics.*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
AppEmptyStateper entity with its primary CTA. - Loading:
AppSkeleton(list)≤ 200 ms to first frame; content ≤ 2 s (00-shared/10 §1). - Errors:
AppErrorStatewith servercode; 409/422 → inline banner; 5xx → generic +requestIdhint; 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) —currentvariantprimaryContainer. - Title:
name(Ty.titleMedium). - Subtitle:
startDate → endDate(localeIntl,dd MMM yyyy),bodySmall onSurfaceVariant. - Trailing: status badge +
AppMenu(Set as current / Edit / Delete).
States:
- idle → list from
GET /academic-years(sort=-createdAtserver default). - paginated:
hasNext→ infinite scroll footer spinner (Mo.m-instant). - pull-to-refresh: re-fetch page 1.
- current-year row pinned first with a
primaryContainerleading pill "Current" (client sort:isCurrentdesc, 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):
headlineMediumname; 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/:academicYearIdpaginated (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:codechip (mono) if set; status badge (non-activeonly);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.groupsavatar. - 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 —classTeacherIdis a user ObjectId percreate-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, trailingAppMenu(Replace / Remove). - Conflict surface: duplicate subject rows →
AppBanner(warning)"Same subject assigned twice" + row highlightedtertiary; 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 → 404Assignment 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:codemono+shortNameif 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
SubjectAssignmentRowas §6). - "Add" opens
AppBottomSheet(2/3 height):AppDropdownSubject (searchable) — pre-loaded catalog; entries already assigned to this class are disabled with "Already assigned" note.AppDropdownTeacher (searchable) — user pickers filtered to teacher roles (Users module);(planned)server-side teachers list.- CTA "Assign" →
POST /subject-assignments{teacherId, subjectId, classId, academicYearId}(allIsMongoId,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
tertiarydot + "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)
| Concern | Spec |
|---|---|
| Screen enter | Mo.m-base slide; list→detail uses Hero on row leading avatar → header card (Mo.m-slow) |
| FAB actions | scale-in Mo.m-entrance; hide on scroll-down |
| Chip toggles (filters, year switcher) | Mo.m-fast fill/outline swap |
| Conflict banner | slide-down + fade Mo.m-base; live region |
| Row remove | AnimatedList exit Mo.m-fast; snackbar confirm |
| Reduced motion | all 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).