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

15 — Flutter Implementation Guide (Academics Module)

How to build the Academics feature in the Flutter client on top of 00-shared/11. Forward-looking spec; no client repo exists yet. All DTO/API facts are exact from src/modules/academics/** (12_API_Mapping.md).


1. Folder structure

features/academics/
├── domain/
│   ├── models/
│   │   ├── academic_year.dart       # id, name, startDate, endDate, status, isCurrent
│   │   ├── grade.dart               # id, academicYearId?, name, code?, displayOrder, status
│   │   ├── section.dart             # id, gradeId, name, capacity, classTeacherId?, roomId?, status
│   │   ├── school_class.dart        # id, academicYearId, campusId?, gradeId, sectionId,
│   │   │                            #   name, capacity, classTeacherId?, roomId?, status
│   │   ├── subject.dart             # id, code, name, shortName?, credits, maximumMarks,
│   │   │                            #   passingMarks, theoryMarks, practicalMarks, status
│   │   └── subject_assignment.dart  # id, teacherId, subjectId, classId, academicYearId
│   └── exceptions/academics_exceptions.dart
├── data/
│   ├── dto/
│   │   ├── create_academic_year_dto.dart … create_subject_dto.dart
│   │   ├── update_*_dto.dart (5 entities)
│   │   └── create_subject_assignment_dto.dart
│   └── repositories/
│       ├── academic_year_repository.dart
│       ├── grade_repository.dart
│       ├── section_repository.dart
│       ├── class_repository.dart
│       ├── subject_repository.dart
│       ├── subject_assignment_repository.dart
│       └── reference_repository.dart   # aggregated cache loader
└── presentation/
    ├── cubit/
    │   ├── reference_cubit.dart        # §2 (13_State_Management §3)
    │   ├── cascading_picker_cubit.dart
    │   ├── year_list_cubit.dart, grade_list_cubit.dart, class_list_cubit.dart,
    │   │   section_list_cubit.dart, subject_list_cubit.dart
    │   ├── class_detail_cubit.dart, assignment_matrix_cubit.dart,
    │   │   roster_cubit.dart, explorer_cubit.dart
    │   └── year_form_cubit.dart, grade_form_cubit.dart, section_form_cubit.dart,
    │       class_form_cubit.dart, subject_form_cubit.dart
    ├── pages/
    │   ├── year_list_page.dart, year_detail_page.dart
    │   ├── grade_list_page.dart, grade_detail_page.dart
    │   ├── class_list_page.dart, class_detail_page.dart, class_assign_page.dart
    │   ├── section_list_page.dart, section_detail_page.dart
    │   ├── subject_list_page.dart, subject_detail_page.dart
    │   ├── explorer_page.dart, teacher_roster_page.dart
    │   └── forms/ (year_form_page, grade_form_page, section_form_page,
    │              class_form_page, subject_form_page)
    └── widgets/
        ├── hierarchy_tree.dart, section_chips.dart, subject_assignment_row.dart,
        │   year_status_badge.dart, entity_status_badge.dart,
        │   cascading_entity_picker.dart, marks_summary_tiles.dart,
        │   roster_table.dart, conflict_banner.dart, reference_cache_provider.dart
        └── joiners.dart   # label resolution (§5)

2. Dependencies

flutter_bloc, dio (AppDio with refresh/error interceptors, 00-shared/11 §5), go_router, get_it, hive (reference cache persistence) or SharedPreferences + in-memory index, intl, collection (groupBy). No tree package — HierarchyTree is a custom AnimatedSize list (07 §1).

3. Cubits

  • ReferenceCubit: singleton per tenant (recreated on tenant switch, 13 §10); state {years, grades, sections, classes, subjects, selectedYearId, ttl}; emits ReferenceChanged on any write/refresh (13 §5).
  • List cubits mix PaginationCubit (00-shared/06 §3.2); year-scoped variants hold Map<String, PageState> per year.
  • Form cubits: single Submit(dto); map 409 → duplicate state with server message; 400 → field errors from error.details keys (DTO names).
  • AssignmentMatrixCubit: Add guards against already-assigned pairs using the loaded roster (client-only guard, OQ-4); Replace = Remove + Add sequence.
  • ExplorerCubit: builds the tree from ReferenceCubit cache; never refetches per node (no tree endpoint).

4. Repositories + the tree join

class ReferenceRepository {
  // single-flight per collection; caches pages until TTL (24 h)
  Future<List<AcademicYear>> years();          // GET /academic-years (all pages)
  Future<List<Grade>> grades();                // GET /grades (all pages)
  Future<List<Section>> sections({String? gradeId}); // GET /sections[/by-grade/:id]
  Future<List<SchoolClass>> classes({String? academicYearId}); // GET /classes[/by-year/:id]
  Future<List<Subject>> subjects();            // GET /subjects (all pages)
  Future<List<SubjectAssignment>> rosterByClass(String classId, String yearId); // by-class
  Future<List<SubjectAssignment>> rosterByTeacher(String teacherId, String yearId); // by-teacher
}

Hierarchy join (no server tree): build nested nodes from cached lists:

  • classes → schoolClass.gradeId → grade; schoolClass.sectionId → section; grades optionally bound to grade.academicYearId (floaters shown in all years, grade.schema.ts:9-10); classes grouped under their year via class.academicYearId.
  • Each class leaf's subject count = roster cache [classId] length (fetched lazily per expanded class, by-class).

5. Extensions / joiners (shared 00-shared/11 §8 + module)

  • Grade.section(sections) → label; SchoolClass.labels(grades, sections, years)(gradeName, sectionName, yearName) for chips — never guessed server-side.
  • SubjectAssignment.classLabel() / .subjectLabel() resolution; missing ref → "Unknown (deleted)" chip (OQ-3 orphan handling).
  • String.toTitle() for section names; String.upperCode() for subject codes.
  • DateTime.toIsoDate() (submit YYYY-MM-DD only — IsDateString, create-academic-year.dto.ts:9-15); DateTime.fromIso() for display.
  • YearStatusBadge mapping status enum (academic-year.schema.ts:7-11).
  • Numerals: FontFeature.tabularFigures() on capacity/marks/orders.

6. Navigation

go_router GoRoutes per 04 §3 (prefix /academics); route guards authGuard + role check (00-shared/05 §9); academics.* permission guard (planned) — OQ-1. Year switcher is a shell-level control bound to ReferenceCubit.selectedYearId, not per-page state. Deep links: /academics/classes/:id, /academics/explore?yearId= (10 §7).

7. Theme

Global AppTheme unchanged; module adds no tokens. Patterns from 11_Design_System_Mapping.md: conflict tertiaryContainer, current primaryContainer, status badge maps.

8. Localization keys

academics.*: years.*, grades.*, sections.*, classes.*, subjects.*, assignments.*, explorer.*, forms.*, conflicts.*. Server business messages (409 copies) rendered verbatim via a serverMessage key with fallback to the raw message (00-shared/09 policy in 14 §11).

9. Storage

  • Reference cache → Hive boxes academics.years/grades/sections/classes/subjects (non-sensitive, 00-shared/11 §11), TTL 24 h (13 §3).
  • No secure-storage usage in this module (no tokens/secrets).

10. Testing

  • Unit: cascade picker transitions; pagination per-year maps; duplicate pre-checks (year/grade/subject code); marks cross-validation (theory+practical ≤ maximum, passing ≤ maximum — OQ-5); DTO → model mappers.
  • Widget: class form cascade disabled-states; roster conflict banner; explorer expand ≤ 50 nodes; SectionChips overflow.
  • Golden: HierarchyTree, SectionChips, SubjectAssignmentRow, YearStatusBadge, CascadingEntityPicker light/dark × 3 sizes.
  • Integration: mock-server flows per 14 §QA scripts.
  • E2E (P0): fresh tenant full ladder setup → timetable/attendance reflect new class; multi-device set-current correction.

11. Performance

  • Cache-first render: screens paint from Hive before network revalidation (no blank spinners on warm start).
  • HierarchyTree: lazy children, ≤ 50 expanded nodes, ListView.builder (07 §1).
  • Roster rows cached per class (LRU 20, 13 §1); by-class fetched on class detail open only.
  • const constructors; no whole-page rebuild on filter chips (scoped BlocBuilders).
  • Pagination: infinite scroll fetches next page only when hasNext (12 §9).

12. Proposals flagged to the team

  1. RBAC first (OQ-1): today any authenticated user can write structure — ship UI gating + flag backend perms (academics.read/.create/.update/.delete per entity) before student-facing read mode goes live.
  2. When server adds duplicate/conflict validation (OQ-4), remove client-only guards and render server 409/422 messages instead.
  3. When q search lands server-side (OQ-7), swap local filters for query params.
  4. When campus/room modules land (OQ-6), replace hidden campusId field + free roomId text with real pickers.
  5. Coaching extension (docs/IMPLEMENTATION_PLAN.md:314-317,402-429): sections → batches, subjectCategory/classType/batchId — gate behind FeatureFlagsCubit; models gain optional fields, UI unchanged until enabled.
  6. Analytics wiring waits shared AnalyticsService interface (00-shared/10 §8).