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.
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).
Status chip: options map to UserStatus enum exactly (active, inactive, suspended, invited — user.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).
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.
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.
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.
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.
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".
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.
"Set status to inactive? They can still sign in if they have credentials."
button "Deactivate"
Suspend
header
"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"
Delete
row menu / header
"User will be hidden immediately and hard-deleted after 30 days."
typed delete
GDPR erase
overflow
"PII is anonymized immediately; the account is permanently deleted. This cannot be undone."