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

03 — User Journeys (Settings Module)

End-to-end journeys computed from settings.controller.ts + settings.service.ts + setting.repository.ts. Each journey: entry, intent, decision points, system responses, loading, failures, recovery, exit, back navigation, abandonment, timeout, session expiry, permission denial, offline. (planned) / (forward-looking) per global rules.


1. Browse settings (list + group filter)

entry: Settings section → "Settings" (admin), deep link, dashboard quick-link
intent: see the whole tenant config, grouped
sequenceDiagram
    actor U as Admin
    participant F as SettingsListPage
    participant R as SettingsRepository
    participant API as GET /settings
    U->>F: open Settings
    F->>R: load()
    R->>API: GET /api/v1/settings (Bearer)
    API-->>R: 200 data: [Setting...] (array, meta omitted — non-paginated)
    R-->>F: groupBy(group) + sort (server already sorts group,key)
    F-->>U: group tabs + key/value rows
    alt tap group chip
        U->>F: "Attendance"
        F->>R: load(group: 'attendance')
        R->>API: GET /api/v1/settings?group=attendance
        API-->>F: 200 filtered array
    else client search
        U->>F: type "grace"
        F-->>U: filtered rows (client-side only — API has no q param)
    end
  • Decision points: group chip (server filter) vs free search (client filter).
  • Loading: AppSkeleton(list); content ≤ 2 s budget (00-shared/10 §1).
  • Failure covers: 401 → silent refresh → session expiry; 5xx → AppErrorState + retry; offline → last-good cache + AppOfflineBanner.
  • Exit: row tap → setting detail; FAB none (edits happen in-line/detail).
  • Empty state: no settings at all → "No settings yet — create one" (AppEmptyState); group empty → "Nothing in this group".
  • Permission denial: client route guard rejects without settings.read → 403 screen (server does not 403 today — OQ-2).

2. Edit a setting (single upsert)

entry: settings list row tap → detail → edit
intent: change one value safely
sequenceDiagram
    actor U as Admin
    participant D as SettingDetailPage
    participant R as SettingsRepository
    participant API as PUT /settings
    U->>D: open "attendance.gracePeriod" (number editor)
    D-->>U: input type inferred from runtime value (number)
    U->>D: 10 → 15
    D->>R: save(key, value, group)
    R->>API: PUT /api/v1/settings {key, value:15, group:'attendance'}
    alt 200
        API-->>D: data: updated Setting doc
        D-->>U: "Saved" snackbar; value reflects server truth
    else 400 VALIDATION_ERROR
        API-->>D: details per field (group enum invalid etc.)
        D-->>U: field error under group picker
    else 5xx
        D-->>U: generic + requestId; form kept (retry safe — upsert idempotent)
    end
  • Save semantics (source): upsert = findOneAndUpdate({tenantId,key,isDeleted:false}, {$set:{value, group?}}, {upsert:true, new:true}) (setting.repository.ts:35-39) — the whole value is replaced, never merged. label/description from the DTO are dropped by the service (settings.service.ts:25).
  • Idempotency: PUT is safe to retry; last-write-wins per key.
  • Client validation: mirrors the DTO — key non-empty string, group ∈ enum (update-setting.dto.ts:6-15); value type-check is client-only (server stores any JSON).
  • Optimistic? No — value is config; server-confirm then reflect (consistent with 00-shared/06 §3.5 safe-mutation policy: write-once/semantic ops). In-line save on the list is the fast path (see 4).

3. Batch save (bulk update)

entry: settings list "Edit" mode or multi-select → Save all
intent: apply many config changes in one action
sequenceDiagram
    actor U as Admin
    participant F as SettingsListPage
    participant R as SettingsRepository
    participant API as PUT /settings/bulk
    U->>F: edit 5 values across groups
    F->>F: track dirty keys client-side
    U->>F: "Save all" (only dirty keys serialized)
    F->>R: saveAll([{key,value,group} x5])
    R->>API: PUT /api/v1/settings/bulk [ ...5 dtos ]
    loop each dto (server side)
        API->>API: repo.upsert(...) sequential, no transaction
    end
    API-->>R: 200 data: [Setting...] — all saved
    R-->>F: clear dirty set; "5 settings saved" snackbar
  • Server contract: loop of individual upserts, results returned as array (settings.service.ts:28-34). Not atomic — a mid-batch failure returns 5xx with earlier keys already saved (OQ-4).
  • Client countermeasure: only dirty keys are sent (no wasted writes); on failure show "Saved N of M" with per-key retry — the retry must re-send the full dirty set (upsert is idempotent, so re-sending saved keys is harmless).
  • Abandonment: leaving with dirty keys → "Discard changes?" dialog (client-side only; server has no draft).

4. Permission denied

entry: any settings route/action for a user without settings.* (today: client-side only)
sequenceDiagram
    actor U as User (no settings perms)
    participant R as Router
    participant G as RouteGuard
    U->>R: navigate /settings
    R->>G: guard check permissions.contains('settings.read')
    alt lacks permission
        G-->>U: redirect /settings/403 (shared 403 screen)
    else has permission
        G->>R: allow route
    end
    Note over U: server today returns 200 for any JWT (OQ-2); once @Permissions lands,<br/>403 PERMISSION_DENIED → same redirect via error code mapping
  • Failure: 403 from server (future) → shared 403 screen; hidden nav entries for users without perms (00-shared/05 §2); inline action 403 → snackbar + hide action (00-shared/06 §5).

5. Delete a setting

entry: detail screen or row menu → Delete
intent: remove a stale key
sequenceDiagram
    actor U as Admin
    participant D as SettingDetailPage
    participant API as DELETE /settings/:key
    U->>D: menu → "Delete"
    D->>D: AppDialog confirm (destructive)
    U->>D: confirm
    D->>API: DELETE /api/v1/settings/:key
    alt 200
        API-->>D: 200 (void data)
        D-->>U: snackbar "Setting deleted"; pop to list (row removed)
    else 404 RESOURCE_NOT_FOUND
        D-->>U: treat as already deleted; remove row
    end
    Note over U: soft delete — server keeps doc (isDeleted: true)<br/>(base.repository.ts:68-74). Recreating same key → E11000 → 500 (OQ-5)

6. Cross-cutting (shared rules)

AspectBehavior
Timeoutdio 15 s; retry on network failure (upsert safe)
Session expiry401 → silent refresh → sessionExpired → login; state preserved where safe
Offlinelist from last-good cache + banner; writes blocked with guidance (no offline queue for settings — 00-shared/07 §10)
Abandonmentdirty edits dropped on exit with confirm; no server draft
Deep links (forward-looking)studylyon://settings → list; studylyon://settings/:key → detail