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

13 — State Management (Timetable Module)

Extends 00-shared/06. Bloc/Cubit, one cubit per screen; repository layer only place touching HTTP; typed ApiException(code, status, fieldDetails).


1. Cubits

CubitScreenStateNotes
TimetableGridCubitS1/S2TimetableGridStatescope (class/teacher), week offset, entries
EntryFormCubitS4EntryFormStateF1 fields + conflict handling
RoomGridCubitS3 (planned)RoomGridStatemerge of class responses
WeekNavCubitS6WeekNavStateoffset only, shell-level (forward-looking)

2. State classes

// S1/S2
class TimetableGridState {
  LoadState load;                  // Initial | Loading | Success | Error
  GridScope scope;                 // class(scopeId) | teacher(scopeId)
  int weekOffset;                  // 0 = current week (client-only concept)
  List<TimetableEntry> entries;    // server-sorted dayOfWeek, startTime
  List<TimetableEntry> conflicts;  // client-derived same-grid overlaps (OQ-1)
  int requestToken;                // drop stale scope-switch responses
}

// S4
class EntryFormState {
  LoadState submit;                // idle | submitting | success | error
  EntryFormData data;              // field DTO mirror (F1)
  Map<String, String> fieldErrors; // from 400 details
  ConflictClash? clash;            // teacher | room, from 409 + pre-flight
  bool dirty;
}

// S3 (planned)
class RoomGridState {
  LoadState load;
  String roomId;
  List<TimetableEntry> merged;     // composed from per-class fetches
  Map<String, LoadState> perClass; // per-source load status
}

3. Repository & cache keys

class TimetableRepository {
  Future<List<TimetableEntry>> byClass(String classId);      // GET /timetable?classId=
  Future<List<TimetableEntry>> byTeacher(String teacherId);  // GET /timetable?teacherId=
  Future<TimetableEntry> create(EntryDraft dto);             // POST /timetable
}
Cache keyTTLPolicy
sl:{tenant}:timetable:class:{classId}5 minstale-while-revalidate (06 §3.3)
sl:{tenant}:timetable:teacher:{teacherId}5 minstale-while-revalidate
sl:{tenant}:timetable:refs:{classes|subjects|teachers|rooms|years}24 hreference data
detail / editornoneserver-fresh on every submit

RefreshIndicator always bypasses cache (06 §3.3).

4. Events

  • LoadGrid(scope), ChangeScope(scope), ChangeWeek(offset), RefreshGrid, GridCellTap(slot), SlotTap(entry)
  • SubmitEntry(draft), RetrySubmit, DismissConflict, Discard
  • PreflightChange(field, value) — runs local teacher/room clash check
  • Naming per 00-shared/06 §4.

5. Cubit flow (grid + editor)

flowchart TD
    A[TimetableGridCubit] -->|LoadGrid| B[Repository.byClass/byTeacher]
    B --> C[Success: entries → cells\n server-sorted, no re-sort]
    C --> D[EntryFormCubit.SubmitEntry]
    D --> E{Pre-flight clash?}
    E -->|warn| F[ConflictBanner advisory]
    E -->|none| G[POST /timetable]
    G -->|409| H[clash set → ConflictBanner]
    G -->|200| I[emit GridSlotAdded]
    I --> J[TimetableGridCubit inserts slot\n AnimatedList m-base]
    H --> K[keep form values]

6. Realtime / cross-cubit

  • notification.new (WS, 00-shared/07 §8) for own tenant → grid marks refresh-needed badge (planned); no live mutation (TimetableEntryCreated is emitted but unrouted — 01 §6).
  • Teachers module ScheduleTabCubit shares byTeacher cache — refetch on grid mutation while tab visible.
  • Dashboard KPIs (proposed): refresh on TimetableEntryCreated when dashboard visible.

7. Optimistic policy (module)

ActionOptimistic?
Scope switch, week offsetyes (local state)
Grid cell/slot tapsyes (local)
Entry createno — server-confirmed; insert after 200 only
Conflict banneryes (local pre-flight) but always server-verifiable
Delete (planned)no (soft-delete confirm, 00-shared/06 §3.5)

8. Error handling

  • 409 → ConflictClash from pre-flight data + server status (server sends only "Schedule conflict detected", timetable.service.ts:28); form values kept.
  • 400 → fieldErrors from error.details (00-shared/07 §3).
  • 404 → grid treats ref as "—" (catalog gone); entries themselves are only deleted by planned routes.
  • 401 → AuthCubit refresh; fail → session expiry.
  • 5xx → AppErrorState with requestId; retry re-emits event.

9. Testing hooks

  • TimetableGridCubit pure-Dart unit tests (mock TimetableRepository): scope switch, week offset, server-sort preservation, conflict derivation.
  • EntryFormCubit: 409 mapping, fieldErrors, time-format guard (08 F1).
  • Widget tests: grid 3-state (loading/error/empty), S4 409-banner, S5 banner announce, TimetableSlotCard golden ×2×2.
  • AppStateObserver transition logs dev-only (06 §6).