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