PR: #313: [Story-1242] Team roles UI — settings catalog tab, member assignment, role over capability by @ankitmaheshwari-com
View PR →

Team roles UI

Story-1242 · pramaan-app · pairs with the pramaan-functions firm-roles catalog (Story-1241, PR #324).

Files changed

102 file(s), +5692 / -180 lines

Path Role + -
messages/en.jsonConfig+52-17
messages/hi.jsonConfig+52-17
openapi/app-api.jsonConfig+781-16
src/app/[locale]/(workspace)/settings/firm/team/page.tsxApp route+6-0
src/app/api/internal/firm-roles/[firmRoleId]/route.tsApp route+72-0
src/app/api/internal/firm-roles/route.tsApp route+34-0
src/app/api/internal/members/[userId]/firm-role/route.tsApp route+43-0
src/components/add-matter/__mocks__/team-draft.tsComponent+3-3
src/components/add-matter/lead-counsel-chip.tsxComponent+2-2
src/components/add-matter/step4-team-form.tsxComponent+1-1
src/components/add-matter/team-member-chip.tsxComponent+4-2
src/components/add-matter/team-types.tsComponent+5-2
src/components/add-matter/to-firm-member.tsComponent+41-0
src/components/matters/live-matters-list-page.tsxComponent+19-2
src/components/settings/team/{role-select.stories.tsx => capability-select.stories.tsx}Component+9-7
src/components/settings/team/{role-select.tsx => capability-select.tsx}Component+11-7
src/components/settings/team/firm-role-delete-dialog.tsxComponent+53-0
src/components/settings/team/firm-role-sheet.stories.tsxComponent+40-0
src/components/settings/team/firm-role-sheet.tsxComponent+112-0
src/components/settings/team/firm-role-table-row.tsxComponent+128-0
src/components/settings/team/firm-roles-table.stories.tsxComponent+34-0
src/components/settings/team/firm-roles-table.tsxComponent+64-0
src/components/settings/team/invite-member-fields.tsxComponent+3-3
src/components/settings/team/member-actions.stories.tsxComponent+7-3
src/components/settings/team/member-actions.tsxComponent+79-31
src/components/settings/team/member-table-row.stories.tsxComponent+7-3
src/components/settings/team/member-table-row.tsxComponent+17-7
src/components/settings/team/members-table.stories.tsxComponent+7-3
src/components/settings/team/members-table.tsxComponent+14-5
src/components/settings/team/pending-invite-row.tsxComponent+2-2
src/components/settings/team/presentation.tsComponent+36-12
src/components/settings/team/team-fixtures.tsComponent+54-0
src/components/settings/team/team-header-action.tsxComponent+38-0
src/components/settings/team/team-list-content.stories.tsxComponent+19-2
src/components/settings/team/team-list-content.tsxComponent+40-6
src/components/settings/team/team-list-page.tsxComponent+19-13
src/components/settings/team/team-list-toolbar.stories.tsxComponent+2-0
src/components/settings/team/team-list-toolbar.tsxComponent+10-5
src/components/settings/team/team-storybook-helpers.tsxComponent+19-1
src/components/settings/team/team-table-layout.tsComponent+8-1
src/components/settings/team/use-firm-role-actions.tsComponent+108-0
src/components/settings/team/use-firm-role-sheet.tsxComponent+117-0
src/components/settings/team/use-team-list-actions.tsComponent+17-4
src/components/settings/team/use-team-mutations.tsComponent+5-1
src/components/settings/team/use-team-roles-tab.tsxComponent+52-0
src/gen/client/appCreateFirmRole.tsGenerated contract+68-0
src/gen/client/appDeleteFirmRole.tsGenerated contract+68-0
src/gen/client/appListFirmRoles.tsGenerated contract+54-0
src/gen/client/appUpdateFirmRole.tsGenerated contract+75-0
src/gen/client/appUpdateMemberFirmRole.tsGenerated contract+73-0
src/gen/client/index.tsGenerated contract+5-0
src/gen/hooks/index.tsGenerated contract+24-0
src/gen/hooks/useAppCreateFirmRole.tsGenerated contract+156-0
src/gen/hooks/useAppDeleteFirmRole.tsGenerated contract+158-0
src/gen/hooks/useAppListFirmRoles.tsGenerated contract+114-0
src/gen/hooks/useAppListFirmRolesSuspense.tsGenerated contract+113-0
src/gen/hooks/useAppUpdateFirmRole.tsGenerated contract+162-0
src/gen/hooks/useAppUpdateMemberFirmRole.tsGenerated contract+156-0
src/gen/index.tsGenerated contract+169-0
src/gen/schemas/firmMemberFirmRoleUpdateRequest.jsonGenerated contract+15-0
src/gen/schemas/firmMemberListItem.jsonGenerated contract+27-1
src/gen/schemas/firmMemberListResponse.jsonGenerated contract+27-1
src/gen/schemas/firmMemberRoleRef.jsonGenerated contract+20-0
src/gen/schemas/firmRoleAvailability.jsonGenerated contract+7-0
src/gen/schemas/firmRoleCreateRequest.jsonGenerated contract+21-0
src/gen/schemas/firmRoleListResponse.jsonGenerated contract+92-0
src/gen/schemas/firmRoleUpdateRequest.jsonGenerated contract+21-0
src/gen/schemas/firmRoleView.jsonGenerated contract+79-0
src/gen/types/AppCreateFirmRole.tsGenerated contract+75-0
src/gen/types/AppDeleteFirmRole.tsGenerated contract+82-0
src/gen/types/AppListFirmRoles.tsGenerated contract+54-0
src/gen/types/AppUpdateFirmRole.tsGenerated contract+83-0
src/gen/types/AppUpdateMemberFirmRole.tsGenerated contract+78-0
src/gen/types/FirmMemberFirmRoleUpdateRequest.tsGenerated contract+16-0
src/gen/types/FirmMemberListItem.tsGenerated contract+5-0
src/gen/types/FirmMemberRoleRef.tsGenerated contract+22-0
src/gen/types/FirmRoleAvailability.tsGenerated contract+13-0
src/gen/types/FirmRoleCreateRequest.tsGenerated contract+22-0
src/gen/types/FirmRoleListResponse.tsGenerated contract+18-0
src/gen/types/FirmRoleUpdateRequest.tsGenerated contract+19-0
src/gen/types/FirmRoleView.tsGenerated contract+67-0
src/gen/types/index.tsGenerated contract+74-0
src/gen/zod/appCreateFirmRoleSchema.tsGenerated contract+97-0
src/gen/zod/appDeleteFirmRoleSchema.tsGenerated contract+107-0
src/gen/zod/appListFirmRolesSchema.tsGenerated contract+71-0
src/gen/zod/appUpdateFirmRoleSchema.tsGenerated contract+102-0
src/gen/zod/appUpdateMemberFirmRoleSchema.tsGenerated contract+96-0
src/gen/zod/firmMemberFirmRoleUpdateRequestSchema.tsGenerated contract+23-0
src/gen/zod/firmMemberListItemSchema.tsGenerated contract+9-0
src/gen/zod/firmMemberRoleRefSchema.tsGenerated contract+20-0
src/gen/zod/firmRoleAvailabilitySchema.tsGenerated contract+17-0
src/gen/zod/firmRoleCreateRequestSchema.tsGenerated contract+23-0
src/gen/zod/firmRoleListResponseSchema.tsGenerated contract+17-0
src/gen/zod/firmRoleUpdateRequestSchema.tsGenerated contract+21-0
src/gen/zod/firmRoleViewSchema.tsGenerated contract+44-0
src/gen/zod/index.tsGenerated contract+66-0
src/lib/firm-roles/gateway.tsLibrary+105-0
src/lib/kubb-client.tsLibrary+9-0
src/lib/members/gateway.tsLibrary+27-0
tests/unit/firm-roles-table.test.tsxTest+98-0
tests/unit/member-actions-firm-role.test.tsxTest+100-0
tests/unit/to-firm-member.test.tsTest+62-0

1 · The distinction this ships

Capability (admin / editor / member) is what a member may do — authorization, unchanged. Role (Partner / Senior Advocate / Member …) is the firm-defined professional designation — what a member is called. Before this story the UI showed capability wherever it said "Role"; a Partner read as "Admin". Now both exist side by side, correctly labelled.

2 · Settings → Team → "Team roles" tab

A third SegmentedFilter tab beside Members / Pending lists the firm-roles catalog: add (header PrimaryButton swaps to "Add role" on this tab), rename (name click or kebab → the same sheet, catalog-parity), reorder (kebab Move up / Move down, optimistic displayOrder swap), and soft-delete with a confirm dialog. canDelete from the API disables deleting the last role; the backend's 409 *.last_role conflict is surfaced as a specific toast if it races. Seeded defaults (Partner / Senior Advocate / Member) carry a "Default" hint.

3 · Member assignment + relabelled columns

The members table gains a Role column (member.firmRole.name, em-dash when unassigned) and the old "Role" header is relabelled Capability. The kebab now has two submenus: "Change role" (catalog picker + "No role" to clear, wired to PATCH /app/firm/members/{id}/firm-role) and "Change capability" (the previous admin/editor/member switch). A protected row (self / last admin) still allows designation changes — those carry no authorization weight — while capability and deactivation stay locked.

Invite-time role assignment is intentionally NOT shipped: invitations are Clerk-side objects and Story-1241 only extended the member-update path, so a role picked at invite time would have no persistence path. The invite sheet's capability section is relabelled "Capability"; role assignment happens from the members table once the member joins. Follow-up filed on the issue.

4 · Add-Matter wizard shows role, not capability

FirmMember gains role (designation); toFirmMember maps it from the members response, and the lead-counsel + team-member chips render it in the gray sub-line. This also closes the Bug-1240 stub: live-matters-list-page now feeds findMembers from useAppListMembers (active members) through filterFirmMembers instead of () => [].

5 · i18n split

Team.roles (which held capability labels) split into Team.capabilities / Team.capabilityDescriptions plus a real Team.roles namespace for the catalog tab. Every key exists in both messages/en.json and messages/hi.json; role names themselves come from the catalog data, never from i18n.

6 · Plumbing (ADR-095 / HR-21 / HR-22)

Piece Where
Generated hooks (Kubb, off the synced OpenAPI mirror) useAppListFirmRoles, useAppCreateFirmRole, useAppUpdateFirmRole, useAppDeleteFirmRole, useAppUpdateMemberFirmRole
Browser→server proxy /api/internal/firm-roles (+ [firmRoleId]), /api/internal/members/[userId]/firm-role, mapped in src/lib/kubb-client.ts
Server gateways (assertContract at the trust boundary) src/lib/firm-roles/gateway.ts, membersGateway.updateFirmRoleWithResponseHeaders
RSC prefetch → HydrationBoundary settings/firm/team/page.tsx now also prefetches GET /app/firm/roles

7 · Tests + Storybook

Surface Coverage
FirmRolesTable Render, default hints, rename via name, read-only, move gating, refuse-last delete, delete request (component test + stories).
MemberActions (assignment control) Assign catalog role, current-role disabled, clear designation, protected-row behaviour (component test + stories).
toFirmMember / filterFirmMembers Mapper + search filter unit tests.
FirmRoleSheet, capability select, toolbar, content Stories: add/edit/saving/conflict, roles tab states.

Verification

pnpm lint clean · pnpm test 496 passed · pnpm build compiles, new routes emitted · hardcoded-copy and color-literal scans no hits. Staging acceptance: defaults render on the Team roles tab, last role refuses deletion, member rows show & set role, wizard chips show role.