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

Extends 00-shared/11. Forward-looking: no client repo exists (shared ledger A1). Everything below derives from src/modules/timetable/**, src/modules/academics/**, src/modules/teachers/**, src/modules/rooms/**, src/modules/rbac/** (perms at permissions.constants.ts:44-45).


1. Module folder

lib/features/timetable/
├── data/
│   ├── dto/timetable_entry_dto.dart      # envelope payload mapper
│   ├── dto/entry_draft_dto.dart          # create payload (F1)
│   ├── models/timetable_entry.dart       # + DayOfWeek enum (6 values)
│   └── repositories/timetable_repository.dart
├── domain/
│   └── timetable_service.dart            # grid assembly + conflict derivation
└── presentation/
    ├── cubit/timetable_grid_cubit.dart
    ├── cubit/entry_form_cubit.dart
    ├── pages/timetable_grid_page.dart
    ├── widgets/timetable_grid.dart, timetable_slot_card.dart,
    │   day_header.dart, time_gutter.dart, grid_cell.dart,
    │   conflict_banner.dart, week_navigator.dart, entry_editor_sheet.dart

2. Domain models

enum DayOfWeek { monday, tuesday, wednesday, thursday, friday, saturday }
// parse via EnumByName; sunday → parse error → model invalid (server enum, timetable.schema.ts:7-14)

class TimetableEntry {
  final String id, classId, subjectId, teacherId;
  final String? roomId;
  final DayOfWeek dayOfWeek;
  final String startTime, endTime;        // zero-padded "HH:MM"
  final String academicYearId;
}
  • DTO→model: strict fromJson; times kept as strings (server contract — DateTime parse is display-only, never re-serialized; 08 F1).
  • Ref-name resolution: TimetableService.joinNames(entry, catalogs) — catalogs (classes/subjects/teachers/rooms/years) loaded once, cached 24 h (13 §3).

3. Repository

class TimetableRepository {
  TimetableRepository(this._dio);             // AppDio (00-shared/11 §5)
  Future<List<TimetableEntry>> byClass(String classId);     // GET /timetable?classId=
  Future<List<TimetableEntry>> byTeacher(String teacherId); // GET /timetable?teacherId=
  Future<TimetableEntry> create(EntryDraftDto dto);         // POST /timetable
}
  • Errors: interceptor maps envelope → ApiException(code, status, fieldDetails) (00-shared/11 §5); 409 exposes a conflict flavor (server message is generic — map via pre-flight data, 06 §S4).
  • Always send exactly one query param (both → classId wins; none → []; timetable.controller.ts:26-28).

4. Cubits

  • TimetableGridCubit — scope + weekOffset + entries; never re-sorts (server contract dayOfWeek, startTime, timetable.service.ts:48-60); derives conflicts list (same-grid teacher/room overlaps) for slot badges (OQ-1).
  • EntryFormCubit — mirrors EntryDraftDto; time-format guard (24 h HH:MM, end > start); pre-flight teacher/room clash check against cached grids; on 409 keeps form + sets clash.
  • RoomGridCubit (planned) — merges byClass responses per room usage.
  • WeekNavCubit — offset only (client concept; no server date dimension).

5. Router

// go_router additions (00-shared/05 §4 + 00-shared/11 §6)
GoRoute(path: '/academics/timetable', builder: TimetableGridPage.new), // ?classId=
GoRoute(path: '/academics/timetable/teacher/:teacherId', builder: TimetableGridPage.new),
// (planned) room view + edit/delete surfaces
GoRoute(path: '/academics/timetable/room/:roomId', builder: RoomGridPage.new),
GoRoute(path: '/my/schedule', builder: MySchedulePage.new),            // teacher self
  • Guards: permissionGuard('timetable.read') / ('timetable.create') client-side mirror of permissions.constants.ts:44-45 (server is JWT-only today — 01 §5).
  • Deep link: studylyon://timetable?classId=:id.

6. Theme & components

  • All tokens via AppTheme (00-shared/04); module components in 07; conflict colors only inside TimetableSlotCard conflict state + ConflictBanner (11 §7).
  • Times rendered mono + tabularFigures (02 §2) in gutter, cards, editor.

7. i18n keys

timetable.title, timetable.class, timetable.teacher, timetable.room,
timetable.scope.{class,teacher,room}, timetable.addSlot, timetable.slot.detail,
timetable.slot.duplicate, timetable.slot.edit, timetable.slot.delete,   // edit/delete (planned)
timetable.empty.{class,teacher,room}, timetable.conflict.teacher,
timetable.conflict.room, timetable.conflict.preflight, timetable.slot.doubleBooked,
timetable.saved, timetable.week.{prev,next,today}, timetable.drag.copy,
timetable.day.{monday,tuesday,wednesday,thursday,friday,saturday},
timetable.field.{class,subject,teacher,room,day,startTime,endTime,year},
errors.server, errors.rateLimited

8. Drag & drop (desktop) & long-press (mobile)

  • Desktop: TimetableSlotCard wrapped in Draggable<TimetableEntry> (feedback = card at e-4 + 0.95 scale); GridCell wraps DragTarget<TimetableEntry>: onAcceptWithDetails → open EntryEditorSheet prefilled (day/time from target cell, subject/teacher/room from dragged entry) — create semantics, no PATCH (10 §2, 09 §5; copy banner "This creates a new slot; original stays until delete (planned)").
  • Mobile: no drag — long-press opens the slot popover (10 §1); LongPressDraggable is deliberately unused on touch platforms.
  • Invalid target (occupied cell): reject + errorContainer outline; return animation m-base spring.

9. Grid table performance (hero component)

TechniqueWhy
Lazy rows: ListView.builder (or CustomScrollView) over day columns; cells built on demand6×N grid with ≥ 40 slots — no eager 60-widget build
Slot cards wrapped in RepaintBoundarydrag/hover repaints don't relayout the whole grid
TimeGutter + DayHeader pinned via sticky headers, single shared ScrollControllerconstant header during horizontal/vertical scroll
Cache-extent tuning + const where possiblejank-free 60 fps on low-end (14 QA-32)
Semantics merged per slot (one node)a11y tree stays small
Fonts: mono + tabularFigures timesno width jitter while scrolling
  • Grid assembly: Map<DayOfWeek, List<TimetableEntry>> from the server-sorted list (O(n) pass); row keys = ascending union of startTime; slot height = (end−start) / periodMinutes × rowHeight, min 1 row.

10. Tests

LayerCases
UnitTimetableGridCubit scope/week/conflict-derivation; EntryFormCubit 409 mapping, time-format guard (08 F1), pre-flight clash; DTO↔model mappers (sunday → invalid)
Widgetgrid 3-state (skeleton/error/empty), S4 409-banner, TimetableSlotCard golden ×2×2 (normal/conflict × light/dark), drop-target accept/reject
Integrationcreate → 409 → fix → grid shows slot; scope switch refetch; week nav no-refetch
E2EP0: full create/conflict journey + cross-tenant 404 + bare GET /timetable never sent (per 00-shared/10 §9)

11. Known backend gaps to coordinate (from 14-QA)

  • Class-overlap check or confirmed intent (QA-4/OQ-1); time HH:MM pattern + end > start validation (QA-10/12); room filter ?roomId= (QA-19)
  • Update/delete endpoints (QA-21/22), cascade/substitution for deleted teachers (QA-23), existence checks on create (QA-25)
  • RBAC metadata on endpoints (QA-30); TimetableEntryCreated routing (QA-31)