PR: #321: [Story-1245] Capability derived from firm role + role-sheet capability selector (ADR-112) by @ankitmaheshwari-com
View PR →

Capability is derived from the firm role

Story-1245 · pramaan-app (frontend half of pramaan-docs ADR-112) · pairs with pramaan-functions Story-1244 (PR #328): firm roles now carry a capability column and propagate it to membership.

Files changed

45 file(s), +274 / -777 lines

Path Role + -
messages/en.jsonConfig+5-4
messages/hi.jsonConfig+5-4
openapi/app-api.jsonConfig+33-128
src/app/api/internal/members/[userId]/role/route.tsApp route+0-43
src/components/settings/matter-types/matter-type-sheet-parts.tsxComponent+4-0
src/components/settings/team/capability-select.tsxComponent+9-3
src/components/settings/team/firm-role-sheet.stories.tsxComponent+13-1
src/components/settings/team/firm-role-sheet.tsxComponent+36-4
src/components/settings/team/firm-role-table-row.tsxComponent+4-1
src/components/settings/team/firm-roles-table.tsxComponent+1-0
src/components/settings/team/member-table-row.tsxComponent+8-2
src/components/settings/team/presentation.tsComponent+0-6
src/components/settings/team/team-fixtures.tsComponent+3-0
src/components/settings/team/team-storybook-helpers.tsxComponent+0-1
src/components/settings/team/team-table-layout.tsComponent+1-1
src/components/settings/team/use-firm-role-sheet.tsxComponent+14-2
src/components/settings/team/use-team-list-actions.tsComponent+0-14
src/components/settings/team/use-team-mutations.tsComponent+0-4
src/gen/client/appUpdateMemberRole.tsGenerated contract+0-73
src/gen/client/index.tsGenerated contract+0-1
src/gen/hooks/index.tsGenerated contract+0-4
src/gen/hooks/useAppUpdateMemberRole.tsGenerated contract+0-156
src/gen/index.tsGenerated contract+5-32
src/gen/schemas/firmMemberRoleUpdateRequest.jsonGenerated contract+0-17
src/gen/schemas/firmRoleCapability.jsonGenerated contract+7-0
src/gen/schemas/firmRoleCreateRequest.jsonGenerated contract+8-1
src/gen/schemas/firmRoleListResponse.jsonGenerated contract+9-1
src/gen/schemas/firmRoleUpdateRequest.jsonGenerated contract+13-0
src/gen/schemas/firmRoleView.jsonGenerated contract+9-1
src/gen/types/AppUpdateMemberRole.tsGenerated contract+0-76
src/gen/types/FirmMemberRoleUpdateRequest.tsGenerated contract+0-19
src/gen/types/FirmRoleCapability.tsGenerated contract+16-0
src/gen/types/FirmRoleCreateRequest.tsGenerated contract+7-0
src/gen/types/FirmRoleUpdateRequest.tsGenerated contract+6-0
src/gen/types/FirmRoleView.tsGenerated contract+7-1
src/gen/types/index.tsGenerated contract+4-14
src/gen/zod/appUpdateMemberRoleSchema.tsGenerated contract+0-96
src/gen/zod/firmMemberRoleUpdateRequestSchema.tsGenerated contract+0-24
src/gen/zod/firmRoleCapabilitySchema.tsGenerated contract+17-0
src/gen/zod/firmRoleCreateRequestSchema.tsGenerated contract+6-0
src/gen/zod/firmRoleUpdateRequestSchema.tsGenerated contract+9-0
src/gen/zod/firmRoleViewSchema.tsGenerated contract+7-1
src/gen/zod/index.tsGenerated contract+1-13
src/lib/members/gateway.tsLibrary+0-27
tests/unit/firm-roles-table.test.tsxTest+7-2

1 · The inversion this ships

Before ADR-112, capability (Admin / Editor / Member) was set per member and the firm role (designation) was a separate, decorative label. ADR-112 inverts the relationship: the firm role is the single assignable, and capability is derived from it. A firm authors the role→capability binding once (Partner → Admin); assigning a member that role sets their access automatically. The standalone "Change capability" control is therefore retired — there is nothing left to set directly.

2 · Contract sync (the one allowed codegen)

The vendored openapi/app-api.json was synced to pramaan-functions main and pnpm codegen regenerated src/gen (first commit). Cascade absorbed:

Contract change App-side fix
appUpdateMemberRole (PATCH …/role) removed Deleted the /api/internal/members/[userId]/role proxy, membersGateway.updateRoleWithResponseHeaders, the useAppUpdateMemberRole mutation, and every updateCapability / updateMemberCapability caller. Designation is the only member mutation — PATCH …/firm-role.
FirmRoleView / FirmRoleCreateRequest / FirmRoleUpdateRequest gained required capability; new FirmRoleCapability enum (firm_admin | firm_editor | firm_member) Role create/edit sheet authors it; the three firmRoleFixtures carry it; the roles table shows it.

3 · Members page — capability read-only, derived

The member "⋯" menu keeps only Change role (designation catalog picker + "No role" to clear) — no capability action. The Role column now renders the firm role name with the derived capability as a muted meta line beneath it (member.role → Team.capabilities.* → Admin / Editor / Member). A member with no firm role shows the firm_member default ("Member") with an em-dash designation. Because capability is propagated server-side, the meta line updates the moment the role changes — no separate write.

4 · Firm-role sheet — where the binding is authored

FirmRoleSheet gains a capability selector (reusing the existing CapabilitySelect radio cards, now generalised with ariaLabel / idPrefix props). Create and rename send capability in the payload. System defaults (Partner / Senior Advocate / Member) are rename-only — ADR-112 locks their capability — so the selector is disabled and the PATCH omits capability for them, with a hint explaining why.

5 · Firm configuration IA + Team roles page

The Settings "Firm configuration" group and the standalone Team roles page (promoted out of the Members tab strip) landed earlier in Story-1251 (#317), which is on this branch's base. This story completes ADR-112 on top of that IA: the Team roles page's roles table now carries the read-only Access column, and the sheet behind it authors the binding.

6 · i18n

Dead keys removed (Team.members.changeCapability, columnCapability, capabilityUpdatedToast). New keys added to both messages/en.json and messages/hi.json: Team.roles.columnCapability, capabilityLabel, capabilityHint, capabilityLockedHint. Per ux-guidelines §17 the raw word "capability" never reaches counsel — it renders as Admin / Editor / Member (column header "Access").

7 · Tests + Storybook

Surface Coverage
FirmRolesTable Updated to assert the derived Access column (Partner → Admin, Senior Advocate → Editor; "Member" appears as both name and tier).
MemberActions Existing test confirms no "Change capability" menuitem; role picker unaffected.
FirmRoleSheet Stories: add, edit (editable selector), DefaultRoleCapabilityLocked (locked tier), saving, name-conflict.

Verification

pnpm lint clean (pre-existing warnings only) · pnpm test 495 passed · pnpm build compiles; the retired …/role route is gone, …/firm-role remains · tsc --noEmit clean · hardcoded-copy and color-literal scans no hits · quality CI green (no src/gen drift after the mirror sync).