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

Production-grade specs for all six screens (S1–S6, 05_Screen_Inventory.md). Tokens: Sp = 00-shared/02 §3, Ty = §2, El = §5, R = §4, Mo = §6. Components reference 00-shared/03 and 07_Component_Library.md (this module). The permission matrix (§3, S3) is the module's hero component and gets the deepest spec.


S1 — Roles List /settings/roles

Chrome: AppBar (title "Roles & Permissions", back); headlineMedium; list area.

Layout (phone):

AppBar "Roles & Permissions"
SectionHeader "System roles"      (titleSmall, onSurfaceVariant)
RoleRow × 7 (locked)              [AppAvatar shield] Name · slug (mono)   [System][N perms] ⋮
SectionHeader "Custom roles"
RoleRow × N                       name · description (1 line)            [N perms] ⋮
FAB "Create role" (extended)

Elements & behaviour:

  • RoleRow: leading AppAvatar (initials of role name, primaryContainer); title = role name (role.schema.ts:69-70); subtitle = slug in mono + description (role.schema.ts:75-76) ellipsized; trailing = AppBadge "System" (locked, outline) or perm count AppBadge "{n} perms"; AppMenu: custom → Edit / Delete; system → disabled (tooltip "System role").
  • Sections split by isSystem (role.schema.ts:78-79); ordering priority desc (rbac.service.ts:76) within each section — Platform Admin (1000) then Org Admin (100) then Teacher (50)… in system; custom roles sorted by their priority.
  • Search: AppSearchBar client-filters name/slug (list is small; server has no q).
  • Delete: AppDialog confirm ("Delete role '{name}'? N members hold this role — they will lose its permissions within 5 minutes.") → DELETE /rbac/roles/:id (soft delete, rbac.service.ts:106) → optimistic remove + snackbar.
  • FAB: "Create role" → /settings/roles/new.

States: loading AppSkeleton(list); error AppErrorState (UNAUTHENTICATED → refresh flow; PERMISSION_DENIED → 403 screen); empty → only system section renders, empty-state card "No custom roles yet".

A11y: system badge read as "System role — locked"; row semantics button; menu actions labelled ("Edit role", "Delete role").

Tablet/desktop (≥ 840 dp): master-detail — list left (width 360), detail pane right (go_router split). Hover states; keyboard ↑↓ moves selection, Enter opens detail.


S2 — Role Detail /settings/roles/:id

Chrome: AppBar (role name, back); scroll body; SafeArea bottom action bar.

Layout:

AppBar "{name}"
[Banner: system role → "System roles can't be edited." (info)]      (only if isSystem)
Identity card (AppCard e-1):
   name            titleLarge
   slug            mono
   description     bodyMedium onSurfaceVariant
   priority        InfoRow "Priority" value
   system badge    AppBadge System / Custom
"Permissions · {n} of 95"   SectionHeader
PermissionGroupSection × N (read-only): group label + granted chips only
[proposed] "Members with this role" — client-joined list (OQ-R7)
Bottom bar: [Edit permissions] FilledButton (hidden if isSystem)

Behaviour:

  • Data = role doc from the list fetch (client holds the loaded list; no per-id GET endpoint exists — rbac.controller.ts exposes only GET /rbac/roles). Deep-linked :id → load full list, pick by id; missing → AppErrorState 404 semantics.
  • Edit → /settings/roles/:id/edit (S3). Save → detail refreshes from the returned doc (rbac.service.ts:96 returns updated doc).
  • Members-with-this-role: (proposed) — computed client-side by intersecting GET /rbac/members with the role slug; no server endpoint (OQ-R7).

A11y: group headers Semantics(headers:); chips readable in one pass; contrast for locked badge ≥ 3:1.


S3 — Role Editor (permission matrix) /settings/roles/new · /settings/roles/:id/edit

The hero. One screen for create and edit; create = same form with empty matrix.

Chrome: AppBar ("Create role" / "Edit role"), subtitle slug; body = form fields (Section A, 08 §1) + matrix; bottom sticky bar.

Layout (phone portrait):

AppBar "Create role"
name          AppTextField (autofocus on create)
slug          AppTextField (mono, helper "Used in API & membership")
description   AppTextField (multiline, 2)
priority      AppTextField (number; default 0, role.schema.ts:82)
───────────── divider
Row: "Permissions"  titleMedium   •  AppBadge "{sel} of 95"  (live)
AppSearchBar  hint "Search permissions…" (filters perms + groups)
[Matrix] PermissionGroupSection × 26 (scrollable, grouped)
─────────────
Bottom bar: [Cancel] OutlinedButton | [Save role] FilledButton (loading)

Matrix component spec (hero):

  • Source of truth: GET /rbac/permissions (rbac.service.ts:79-81 returns ALL_PERMISSIONS, permissions.constants.ts:1-97) fetched on editor open; role's current perms = its permissions[] (role.schema.ts:84-85).
  • Grouping (forward-looking client matrix): 26 groups derived from the domain prefix of each permission. Full table (order = source order within group):
GroupPermissions
Organizationorganization.read, organization.update, organization.delete, organization.settings.update
Usersuser.read, user.create, user.update, user.delete, user.import
Roles & Accessrbac.role.read, rbac.role.create, rbac.role.update, rbac.role.delete, rbac.member.read, rbac.member.create, rbac.member.update, rbac.member.delete
Staffstaff.read, staff.create, staff.update, staff.delete, department.manage, designation.manage
Studentsstudent.read, student.create, student.update, student.delete
Attendanceattendance.mark, attendance.edit
Feesfees.collect
Notificationsnotification.read, notification.update
CRMcrm.read, crm.lead.manage, crm.campaign.manage
Dashboarddashboard.read, dashboard.widget.manage
Reportsreport.generate, report.read
Biometricbiometric.log.create, biometric.log.read, biometric.device.manage
Timetabletimetable.read, timetable.create
Houseshouses.read, houses.create, houses.update, houses.delete
Roomsrooms.read, rooms.create, rooms.update, rooms.delete
Auditaudit.read
Librarybooks.read, books.create, books.update, books.delete, books.issue, books.return
Finesfines.pay
Transporttransport.vehicle.read, transport.vehicle.create, transport.vehicle.update, transport.vehicle.delete, transport.route.read, transport.route.create, transport.route.update, transport.route.delete, transport.driver.read, transport.driver.create, transport.driver.update, transport.driver.delete, transport.assign
Settingssettings.read, settings.update, settings.delete
Feature Flagsfeature-flags.read, feature-flags.update, feature-flags.delete
Paymentspayments.read, payments.process, payments.refund, payments.reconcile, receipts.read
Filesfile.read, file.upload, file.delete
Webhookswebhook.create, webhook.read, webhook.update, webhook.delete
Searchsearch
Schedulerscheduler.read, scheduler.create, scheduler.delete
  • PermissionGroupSection: group header = label + Checkbox (tri-state: none/some/all)
    • "{k}/{n}"; body = Wrap of PermissionChips (FilterChip-style, label = full perm string in mono-ish small type, tooltip = humanized name (proposed)).
  • Toggle semantics: chip tap toggles; group checkbox toggles all in group; search filters to matched group sections (sections with 0 matches collapse to "no results").
  • Live count badge updates on every toggle; no server call until Save (one PATCH, rbac.service.ts:96 — no per-cell endpoint exists).
  • Forward-looking notes: the matrix renders only the 95 permissions returned by the server; coaching perms (batch.manage etc.) and roles (batch_coordinator…, docs/IMPLEMENTATION_PLAN.md:713-761) are (planned) and must not be rendered until the server ships them.

Behaviour & rules:

  • Create: sends {name, slug, description?, priority?, isSystem:false, permissions}isSystem:false is forced (OQ-R2; server default true locks the role, role.schema.ts:78-79).
  • Save → POST /rbac/roles or PATCH /rbac/roles/:id; 409 duplicate slug → inline under slug field, focus it; 400 (system role — should be unreachable) → banner.
  • Dirty state: back navigation with unsaved changes → AppDialog "Discard changes?".
  • Empty selection on create → warning banner "Role with no permissions — members will only see the base shell" (allowed server-side; e.g. student role is []).
  • Priority field hint: "Higher number lists the role first" (rbac.service.ts:76).

States: loading (catalog + role) skeleton; saving → CTA spinner, matrix disabled, anti-double-submit; success → snackbar + pop to detail.

A11y: every chip = toggle with Semantics(checked:) + label + group context; search results announced (liveRegion "3 groups match"); checkbox tri-state announced ("none / some / all selected").

Tablet/desktop: matrix in a 2-column grid (groups wrap); sticky group header on scroll; Ctrl+F focuses search; tab order = fields → matrix rows.


S4 — Members List /settings/members

Chrome: AppBar "Members"; headlineMedium; list; FAB.

Layout:

AppBar "Members"
AppSearchBar (name / email filter — merged profile, OQ-R7)
MemberTile × N:
  AppAvatar 48 (photo or initials)   displayName    (titleMedium)
                                     email · joined {date} (bodySmall)
                                     [role chips ×k]  (FilterChip, read-only, tap = no-op
                                                      w/ tooltip "Managed in role editor")
                                     [status badge: active/suspended/invited]  (render-only)
                                     ⋮ (Edit roles / Remove — admin only)
FAB "Add member" (extended)

Behaviour:

  • Data: GET /rbac/members (rbac.service.ts:109-111, joinedAt desc) + profile merge via user.read (batch GET /api/v1/users, OQ-R7); merge failures render userId in mono fallback.
  • Roles per member = roles[] (organization-member.schema.ts:22) resolved to role docs for labels/priority; unknown slugs (role deleted) render as plain slug chips.
  • Status badge from status (organization-member.schema.ts:27); today always active (OQ-R3); suspended/invited render-only until server produces them.
  • Row menu (admin): "Edit roles" → S5 sheet in edit mode (PATCH /rbac/members/:id, DTO requires userId but service ignores it — client may send the same userId from the row, rbac.controller.ts:71-72); "Remove member" → AppDialog destructive confirm ("Remove {name}? Their access ends within 5 minutes.") → DELETE /rbac/members/:id (rbac.service.ts:138-140 soft delete; silent 200 even if already removed) → optimistic removal.
  • Self-guard: Priya removing her own org_admin membership → extra typed confirm (type the role slug pattern, 05 §5) + warning "You will lose access to this screen immediately on next refresh."

States: skeleton / error / empty ("No members yet — add your first member"); pull to refresh.


S5 — Add Member /settings/members/add (bottom sheet)

Full-height AppBottomSheet (Sp-16 padding, r-xl top):

DragHandle  "Add member"  titleLarge
Step 1 — Person:
AppSearchBar "Search users…" (debounced, user.read; selectable results = AppListTile:
  avatar + displayName + email)
[selected user tile] AppCard: avatar, name, email, [change]
Step 2 — Roles:
SectionHeader "Roles"
FilterChip × roles (GET /rbac/roles, priority order; system + custom; multi-select)
helper: "Roles control permissions. Members can hold several."
[Add member] FilledButton fullWidth (disabled until user + ≥1 role)

Rules (derived):

  • Body: {userId, roles}@IsMongoId (add-member.dto.ts:6-7), @IsArray @IsString({each}) (add-member.dto.ts:9-12).
  • Duplicate pre-check client-side against loaded member list (unique index organization-member.schema.ts:48; server would 500 — OQ-R5) → inline error "Already a member".
  • Success → 200/201 doc (status:"active", joinedAt now — rbac.service.ts:122-123); sheet closes, row appears top of S4, snackbar "Member added".
  • Edit mode (from S4 menu): same sheet, title "Edit roles", user tile read-only, chips preselect current roles, CTA "Save roles".

A11y: sheet traps focus; role chips Semantics(selected:); user results liveRegion count.


S6 — Permission Audit View /settings/access-audit

Chrome: AppBar "Access audit"; filter bar; paginated list.

Layout:

AppBar "Access audit"
Filter row (Wrap of 3 AppDropdown + clear):
  action      (free-text suggestions: role.create, member.delete, user.login…)
  entityType  (roles, organization_members, users…)
  actorId     (person picker, user.read)
[AppBanner info: "Shows the last 90 days. RBAC writes appear here." (proposed copy)]
AuditRow × 20:
  action    (labelLarge, monospace accent for RBAC actions)
  entityType · actorId (mono)
  timestamp (bodySmall, relative)
Infinite scroll + "end of list" footer (meta.totalItems — paginated, 07 §2)

Behaviour:

  • Query GET /audit-logs?page=&limit=&action=&entityType=&actorId= (audit.controller.ts:17-34); default limit 50 (audit.controller.ts:20); pagination envelope with meta ({page,limit,totalItems,totalPages,hasNext,hasPrevious}, 07 §2).
  • RBAC focus: presets in the action dropdown ((proposed) convenience): "Role created", "Role updated", "Role deleted", "Member added", "Member updated", "Member removed" — mapped to whatever action strings the audit module writes (audit-write queue, PLAN.md:189-191; exact strings (planned), OQ-R8).
  • Row tap → no detail screen (no server detail endpoint) — copies requestId ((proposed)).
  • 401/403 handling standard; server currently gates with JwtAuthGuard only (audit.controller.ts:9) — client still requires audit.read (mismatch flagged, Phase-5 (planned)).

Tablet/desktop: filters in the AppBar row; table view (columns: time, action, entity, actor, requestId) instead of list tiles.