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

06 — Screen Specifications (Users Module)

Full production specifications for the screens in 05_Screen_Inventory.md. Every field/behaviour is derived from backend DTOs/schemas; layout and copy follow the shared tokens/components (00-shared/02, 03, 04, 09). Screens are (planned); backend behavior cited exactly.


SS1 — Users list (/users)

Layout (responsive)

Phone (<600dp)                          Tablet/Desktop (≥840dp)
┌──────────────────────────────┐        ┌───────────┬──────────────────────┐
│ AppBar: "Users"   [ImportCSV]│        │ AppBar: "Users"      [ImportCSV]│
│ SearchBar (debounce 300ms)   │        │ SearchBar ──────────┐            │
│ [Filter chips: Status ▾][Roles▾]      │ Filter chips        │  Detail    │
│ ┌─ ListTile row ────────────┐│        │ ┌─ Row 1 ─────────┐ │  pane      │
│ │ avatar  Name (displayName)││        │ │ ...             │ │  (S3)      │
│ │         email · status ▮ ││        │ └─────────────────┘ │            │
│ └───────────────────────────┘│        │ Row 2..N            │            │
│ [infinite scroll spinner]    │        │ [page controls]     │            │
│ FAB: "Add user"              │        │                     │            │
└──────────────────────────────┘        └─────────────────────┴────────────┘
  • Phone: list only; master-detail at ≥ 840 dp (selected row highlights, 00-shared/05 §3).
  • List rows: height ≥ 56 (00-shared/03 AppListTile); avatar 40; leading avatar, title = displayName (fallback firstName lastName — always set server-side, users.service.ts:62), subtitle = email; trailing = status badge + overflow menu.
  • Role chips: from GET /rbac/members joined client-side by userId (rbac.controller.ts:57-61); org_admin-gated data source — chips show "—" for non-admin viewers (04_IA §1).

States

StateRender
Initial/LoadingAppSkeleton list (8 rows)
Success+datarows; footer "end of list" at last page
Success+empty (q present)AppEmptyState "No users match 'query'"
Success+empty (no q)AppEmptyState "No users yet" + primary CTA "Add user"
ErrorAppErrorState per code: 429 → backoff copy; 5xx → generic + requestId (00-shared/06 §5)

Filter chips

  • Status chip: options map to UserStatus enum exactly (active, inactive, suspended, inviteduser.schema.ts:7-12).
  • Role chip: options from GET /rbac/roles (rbac.controller.ts:27-31).
  • Server has no status/role query params (users.service.ts:90-115); the chip filters the current client pages and is labeled "filtering loaded results" until server support lands ((planned), OQ-9).
  • Search q is server-side (regex $orusers.service.ts:92-99); clearing restores list.

Refresh/pagination

  • Pull-to-refresh resets to page 1, bypasses cache (00-shared/03 §F).
  • Infinite scroll appends while meta.hasNext (pagination-query.dto.ts:52); desktop uses explicit pager + jump-to-page.

A11y

  • Row = one Semantics(button) (avatar+name+status as summary); badge announced as "status: active" (00-shared/09 §7); live region announces "N results" after search (:44); filter chips announce selected state.

SS2 — Create user (/users/new)

Form structure (single full-screen page; >3 fields → page not sheet, 00-shared/05 §5)

Identity    firstName* | middleName? | lastName*        (one row on desktop, stacked on phone)
            displayName? (helper: defaults to "firstName lastName" — users.service.ts:62)
            email* (keyboardType email) | phone?
Demographic gender? (select: male|female|other|prefer_not_to_say — user.schema.ts:37-41)
            dateOfBirth? (AppDatePicker, max = today)
Locale      language (default en — user.schema.ts:49-50) | timezone (default UTC — :52-53)
Status      status (default active — create-user.dto.ts:44-47; invited shown but selectable only in invite flow (planned))
Membership  roles (multi-select chips from GET /rbac/roles)  ← optional step; POST /rbac/members
  • Field validation exactly mirrors CreateUserDto (create-user.dto.ts:5-62): required firstName, lastName, email (@IsEmail :24-26); gender is free string server-side (@IsString :33-38) but the client constrains to the schema enum; status enum-validated (:44-47).
  • Submit flow: POST /users → on success, if roles selected → POST /rbac/members {userId, roles} (rbac.service.ts:113-127). Membership failure → keep user, show persistent banner "Created without roles — retry" (retry calls the same member POST; idempotent enough — member unique per (tenantId, userId) index, organization-member.schema.ts:48).
  • Email conflict (409): inline under email field, focus it (users.service.ts:50-54); suggest "Search existing users for 'x'".
  • Phone conflict (409): same treatment (:56-60).
  • Anti-double-submit: button loading state replaces label (00-shared/08 §6); membership call is a second network round-trip — button stays "Creating…" through both.

A11y

  • Labels visible + autofill hints (name, email, tel — 00-shared/09 §10); on submit failure focus moves to first invalid field; error summary announced via live region.

SS3 — User detail (/users/:id)

Layout

Header: [avatar 64] displayName   [status badge]        [⋯ menu]
         email · phone · gender · DOB · language · timezone
         lastLoginAt · emailVerified/phoneVerified (read-only)
Tabs:  Profile | Membership | Preferences | (Audit (planned))
  • Profile tab: all UpdateUserDto fields read-only except through Edit (S5); metadata shown as JSON (proposed) collapsible — arbitrary object (user.schema.ts:68-69).
  • Membership tab: member status (MemberStatusorganization-member.schema.ts:7-11), roles (editable chips → PATCH /rbac/members/:id, rbac.controller.ts:69-73), joinedAt, invitedBy, acceptedAt, lastActiveAt (:30-40); remove membership → DELETE /rbac/members/:id (:75-79).
  • Preferences tab: S6 embedded.
  • Status change menu (header): active→inactive|suspended etc. — all four enum values selectable (user.schema.ts:7-12); confirm dialog for suspended with consequence copy; the invited value selectable only via (planned) invite flow.
  • Delete / GDPR erase: top-bar overflow → S8 dialogs.

States

ConditionRender
404 (cross-tenant/erased/deleted id)"User not found" empty state — do not leak existence (00-shared/07 §3)
Erased user visited post-purgesame 404
No membership recordMembership tab empty-state "Not a member — add roles"

SS4 — Bulk import wizard (/users/import)

Step 1 — Upload

  • Drop zone (desktop) / file picker (phone); accept text/csv, .csv; size guard: warn > 2 MB (server has no explicit limit in code — upload timeout 120 s client-side, 00-shared/11 §5; OQ-10).
  • "Download template" → client-generated CSV with header email,firstname,lastname,phone,gender,language,timezone (column names exactly as parsed: lowercased headers — users.service.ts:244-247; aliases first_name/last_name supported — :265-266).
  • Malformed file (binary, wrong ext) → inline error before upload.

Step 2 — Preview (client-side, isolate)

  • Parse mirrors server semantics for preview fidelity:
    • header row = first non-empty line, lowercase/trim (users.service.ts:244-247);
    • rows = subsequent non-empty lines (:237); naive , split (:251);
    • required: email; name aliases optional with fallback Unknown (:265-266); language/timezone defaults en/UTC (:270-271).
  • Show: column map, row count, first 5 rows; warnings: missing optional columns; quotes present (server will split mid-quote — OQ-7); duplicate email within file (server rejects row 2+ — :260-263).
  • Client-side duplicate check against current list is advisory only (server re-checks at import time).

Step 3 — Result

  • Counts: imported / errors.length (exact fields of response — users.service.ts:281); progress bar 100% on response.
  • Error table: row number + message verbatim (Row N: …:256-278); paginate error list if > 50 (client-side).
  • Buttons: "Download errors (CSV)" (client-side export of row number + message), "Import more" (back to step 1), "Done" → list refresh.
  • No cancel/rollback: server commits per-row (:250-279); copy states "Already-imported rows are kept" on leaving.

Failure handling

  • 400 envelope: response data may be absent; the endpoint returns {imported:0, errors:[…]} for empty files rather than a 400 (users.service.ts:238-243) — treat as result, not error.
  • Network loss mid-request: retry is safe (server dedups emails — :260-263); inform "retry won't duplicate imported rows".

A11y

  • Progress announced (00-shared/09 §7 "Importing users: 12%"); result summary in live region; error table rows readable by screen reader with row-number prefix.

SS5 — Edit user (/users/:id/edit)

  • Same form as SS2 minus Membership; DTO = UpdateUserDto (update-user.dto.ts:5-70) — all optional; submit sends only changed fields.
  • Email/phone change → server re-checks uniqueness (users.service.ts:120-134); 409 inline.
  • After save: displayName may change server-side (:136-139) — detail header refreshes from response.
  • avatarFileId field exists in DTO (update-user.dto.ts:38-39) — not surfaced as a text field (managed via avatar upload S7/S3).

SS6 — Preferences

  • Notifications: three switches email, push, sms (update-user-preferences.dto.ts:8-12).
  • Theme: mode radio light | dark | system + language field (:16-20).
  • Save: full merged object (see contract warning in 03_User_Journey.md §3); switches optimistic with rollback; "Save" persists merged state.
  • Load: GET /users/:id/preferencesuser.preferences ?? {} (users.service.ts:168-171) — never null; empty state = defaults off.

SS7 — Self profile

  • Read-only identity + avatar + links; :id from JWT sub (auth.service.ts:461); guard (planned) server-side.
  • Avatar flow: pick → crop (square) → POST /users/:id/avatar multipart file (users.controller.ts:95-102) → response {avatarFileId} (users.service.ts:230); old file deleted best-effort server-side (:227-229). Optimistic thumb while uploading; error → revert + snackbar.
  • Show emailVerified/phoneVerified read-only — flagged OQ-4.

SS8 — Confirm dialogs

DialogTriggerContentConfirm
Deactivaterow menu / header"Set status to inactive? They can still sign in if they have credentials."button "Deactivate"
Suspendheader"They will see a suspension notice on login." (planned) copy — backend has no suspension-block copy (login rejects only lockout auth.service.ts:133-135)"Suspend"
Deleterow menu / header"User will be hidden immediately and hard-deleted after 30 days."typed delete
GDPR eraseoverflow"PII is anonymized immediately; the account is permanently deleted. This cannot be undone."typed ERASE

SS9 — Import result / error report

  • Standalone reuse of SS4 step 3 with server-only response (no preview): e.g. API clients pasting a direct multipart result.
  • Render errors flat strings; count badge error role (00-shared/03 AppBadge).