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

Cubit architecture per 00-shared/06. One cubit per screen; base LoadState (Initial/Loading/Success/Error(ApiException)), PaginatedListMixin, cache + SWR, optimistic updates, connectivity. All (proposed) client design.


1. Cubit map

CubitScreen(s)Data sourcesNotes
RoomListCubitS1E2 GET /rooms?page&limit (+ client filter (proposed))PaginatedListMixin<Room>; local filter/search state; cache invalidation on mutations
RoomDetailCubitS2E3 GET /rooms/:idTTL 5 min SWR; reconciles after edit; pops on 404
RoomEditorCubitS3 create/editE1 POST / E4 PATCHform model + validation; 409 inline; dirty tracking
(future) RoomAvailabilityCubitS2 availabilitybookings module (planned)(planned) — hidden until bookings lands
(future) RoomCheckinCubitQR scanQR endpoint (forward-looking)(forward-looking)

2. State machine (generic per 00-shared/06 §3.1)

stateDiagram-v2
    [*] --> Initial
    Initial --> Loading: Load
    Loading --> Success: load ok
    Loading --> Error: ApiException
    Error --> Loading: Retry
    Success --> Success: Refresh / mutate (reconcile)
    Success --> Error: mutation fails (delete/update)
    Success --> Loading: pull-to-refresh (bypass cache)

3. RoomListCubit

State: page (1), limit (20), items: List<Room>, hasNext, isLoadingMore, loadState, filters {types: Set<RoomType>, buildings: Set<String>, query} (proposed) — client-side until server params land (OQ-3).

Events: LoadFirst(), LoadMore(), Refresh(), SetTypeFilter(t), SetBuildingFilter(b), SetQuery(q), ClearFilters(), RoomChanged(room) (post-mutation reconcile), RoomDeleted(id).

Load flow: cache hit (sl:{tenant}:rooms:{page}:{limit}, TTL 5 min) → Success(stale:true)

  • background refetch; miss → Loading → Success/Error. On LoadMore while hasNext → append + dedupe by _id.
sequenceDiagram
    participant C as RoomListCubit
    participant R as RoomsRepository
    participant API as GET /rooms?page&limit
    C->>C: LoadFirst()
    C->>R: list(page:1, limit:20)
    API-->>R: data[] + meta {totalItems,hasNext,...}
    R-->>C: Success(items, meta)
    C->>C: scroll ≥80% && hasNext
    C->>R: list(page:2, limit:20)
    API-->>R: data[] + meta
    R-->>C: append items

4. RoomDetailCubit

State: room, stale (cache-served), loadState.

Events: LoadRoom(id), Refresh(), RoomUpdated(room) (reconcile from editor), Deleted().

Key reducer: Deleted() → emit terminal state; UI pops to list (route-level), list cubit removes row via RoomDeleted event. 404 → NotFound state (renders "Room not found" + back), not generic error — because cross-tenant and soft-deleted ids both 404 (rooms.service.ts:38,44,50).

stateDiagram-v2
    [*] --> Initial
    Initial --> Loading: LoadRoom(id)
    Loading --> Success: 200
    Loading --> NotFound: 404
    Loading --> Error: network/5xx
    Success --> Success: RoomUpdated / Refresh
    Success --> Deleted: Deleted() → pop to list
    NotFound --> Loading: Retry (id re-entered)

5. RoomEditorCubit

State: form: RoomFormModel (F1–F6), initial: RoomFormModel (dirty baseline), submitting, fieldErrors: Map<String,String>, duplicateHint: bool (proposed), saveResult.

Events: InitCreate(), InitEdit(room), FieldChanged(field, value), CodeChanged(code) (triggers debounced duplicate hint against list cache), Submit(), Discard().

Reducers: submit → submitting → on 201/200 emit RoomChanged(room) (via module bus) + pop + snackbar; on 409 → duplicateHint=true, inline error, no reset; on 400 → fieldErrors mapped from envelope details[]; on 404 (edit) → pop + "Room not found".

sequenceDiagram
    participant W as RoomEditorCubit
    participant R as RoomsRepository
    participant API as POST /rooms (or PATCH /rooms/:id)
    W->>W: Submit()
    W->>R: create(dto) / update(id, dto)
    alt 201/200
        API-->>R: room doc
        R-->>W: Success(room) → pop + snackbar
        W-->>Bus: RoomChanged(room)
    else 409
        R-->>W: ApiException(DUPLICATE_RESOURCE)
        W-->>UI: inline error under code
    else 400
        R-->>W: ApiException(VALIDATION_ERROR)
        W-->>UI: fieldErrors from details[]
    else 404
        W-->>UI: pop + "Room not found"
    end

6. Caching & staleness (module TTLs)

DataCache keyTTLNotes
Room list pagesl:{tenant}:rooms:{page}:{limit}5 min SWRinvalidated on E1/E4/E5 success
Room detailsl:{tenant}:rooms:{id}5 min SWRreconcile after edit; pop on 404
Duplicate-hint code set (proposed)derived from list cachenot authoritative; server 409 is
Buildings/type dictionary (proposed)derived from list cacherefresh on list refresh

7. Realtime & cross-cubit

  • No WS topics today; (forward-looking): subscribe rooms.updated → invalidate list + detail caches + refetch (multi-device admin edits).
  • Cross-cubit invalidation: after E1/E4 success → bump rooms:{id} + list keys; RoomDeleted → list row removal without refetch (delete is void — rooms.service.ts:48).
  • Module bus events: RoomChanged(room), RoomDeleted(id) consumed by list; editor listens to nothing (form state is local).
  • Future (planned): bookings availability state lives in the bookings module's cubits, not here.

8. Offline & connectivity

  • Reads: last-good cache + AppOfflineBanner; LoadMore blocked offline (no stale pages).
  • Writes: blocked in RoomEditorCubit.Submit() (emit error "You're offline", draft kept).
  • Reconnect: banner clears; user re-triggers.

9. Testing hooks

  • Pure-Dart cubits, mocked repositories; widget tests per state machine (Loading/Success/Error/NotFound/Empty) + 409-inline + delete-flow (00-shared/06 §6).
  • Golden: list with filters applied; editor with duplicate hint; typed-confirm disabled state.