PR: #469: feat(settings): firm profile page — edit firm name [Story-1862] by @ankitmaheshwari-com
View PR →
Story-1862

Firm profile: rename your firm from a single field

A firm admin can now change the firm's display name themselves. Settings → Firm configuration → Firm profile shows the current name in one input; edit, save, done — the new name appears across the app (starting with the settings sidebar header) immediately, no reload. No support ticket, and no forcing the firm to re-enter an address it never gave us. The save rides the partial-update PATCH /app/firm contract that functions Story-1861 landed: a name-only request leaves every other firm column untouched (RFD-FIRM-002).

What changed

Single-field page

A new FirmProfileRoute renders one react-hook-form + zod field (z.string().trim().min(1).max(255)). Save is disabled until the name actually changes (trim-aware dirty-check against the form baseline), so a no-op PATCH is never sent and a whitespace-only edit never enables Save.

Confidentiality (ADR-099)

The settings layout has no admin guard, so a read-only member reaches the page. The RSC passes only the display name + canWrite to the client — never the full FirmProfile (plan / seats / CSM / address). Because me.firm_name already IS the display name, no extra over-fetching gateway read is made at all.

Header refresh, no reload

The firm name renders in the sidebar header, which the settings layout (not the page) builds from loadAppMe().firm_name. The server action calls revalidatePath(`/${locale}/settings`, "layout") so the header reflects the new name without a reload (RFD §5.5 F5).

Honest failure UX

Success → localized toast.success. Validation → localized field error + focus-to-field. 401/403/5xx → localized fallback toast — the raw backend error.message is never rendered. The typed value is retained on failure.

Contract status (verified against merged functions main)

Where it lives

Observability

No new instrumentation (pramaan-docs ADR-104 / ADR-065 / ADR-124). The save traverses src/lib/gateway.ts, which already mints/forwards traceparent + x-pramaan-correlation-id — the trace propagates for free. PATCH /app/firm is an existing instrumented domain operation; this Story only changes the request shape. No OTel SDK, spans, or ad-hoc headers added.

Verify

pnpm lint
pnpm typecheck
pnpm test
pnpm build
pnpm check:routes

Files changed

55 file(s), +3348 / -217 lines

Path Role + -
messages/en.jsonConfig+14-30
messages/hi.jsonConfig+14-30
openapi/app-api.jsonConfig+636-37
openapi/permission-map.jsonConfig+28-0
src/app/[locale]/(workspace)/settings/firm/profile/actions.tsApp route+58-0
src/app/[locale]/(workspace)/settings/firm/profile/page.tsxApp route+47-0
src/components/settings/firm-profile/firm-profile-page.stories.tsxComponent+68-0
src/components/settings/firm-profile/firm-profile-page.tsxComponent+172-0
src/components/settings/settings-sidebar.tsxComponent+8-0
src/gen/client/appCreateCreditTopup.tsGenerated contract+68-0
src/gen/client/appGetChatCredits.tsGenerated contract+65-0
src/gen/client/appGetFirmCredits.tsGenerated contract+58-0
src/gen/client/index.tsGenerated contract+3-0
src/gen/hooks/index.tsGenerated contract+20-0
src/gen/hooks/useAppCreateCreditTopup.tsGenerated contract+156-0
src/gen/hooks/useAppGetChatCredits.tsGenerated contract+134-0
src/gen/hooks/useAppGetChatCreditsSuspense.tsGenerated contract+137-0
src/gen/hooks/useAppGetFirmCredits.tsGenerated contract+122-0
src/gen/hooks/useAppGetFirmCreditsSuspense.tsGenerated contract+121-0
src/gen/index.tsGenerated contract+108-0
src/gen/schemas/chatCreditUsageResponse.jsonGenerated contract+19-0
src/gen/schemas/creditTopupOrderRequest.jsonGenerated contract+18-0
src/gen/schemas/creditTopupOrderResponse.jsonGenerated contract+8-0
src/gen/schemas/firmCreditsResponse.jsonGenerated contract+64-0
src/gen/schemas/firmProfileUpdateRequest.jsonGenerated contract+44-41
src/gen/schemas/firmProfileUpdateResponse.jsonGenerated contract+49-0
src/gen/schemas/lawyerCreditUsageView.jsonGenerated contract+15-0
src/gen/types/AppCreateCreditTopup.tsGenerated contract+75-0
src/gen/types/AppGetChatCredits.tsGenerated contract+76-0
src/gen/types/AppGetFirmCredits.tsGenerated contract+68-0
src/gen/types/AppUpdateProfile.tsGenerated contract+3-3
src/gen/types/ChatCreditUsageResponse.tsGenerated contract+21-0
src/gen/types/CreditTopupOrderRequest.tsGenerated contract+25-0
src/gen/types/CreditTopupOrderResponse.tsGenerated contract+16-0
src/gen/types/FirmCreditsResponse.tsGenerated contract+51-0
src/gen/types/FirmProfileUpdateRequest.tsGenerated contract+15-32
src/gen/types/FirmProfileUpdateResponse.tsGenerated contract+45-0
src/gen/types/LawyerCreditUsageView.tsGenerated contract+21-0
src/gen/types/index.tsGenerated contract+44-0
src/gen/zod/appCreateCreditTopupSchema.tsGenerated contract+101-0
src/gen/zod/appGetChatCreditsSchema.tsGenerated contract+98-0
src/gen/zod/appGetFirmCreditsSchema.tsGenerated contract+93-0
src/gen/zod/appUpdateProfileSchema.tsGenerated contract+4-4
src/gen/zod/chatCreditUsageResponseSchema.tsGenerated contract+20-0
src/gen/zod/creditTopupOrderRequestSchema.tsGenerated contract+21-0
src/gen/zod/creditTopupOrderResponseSchema.tsGenerated contract+19-0
src/gen/zod/firmCreditsResponseSchema.tsGenerated contract+35-0
src/gen/zod/firmProfileUpdateRequestSchema.tsGenerated contract+39-30
src/gen/zod/firmProfileUpdateResponseSchema.tsGenerated contract+46-0
src/gen/zod/index.tsGenerated contract+41-0
src/gen/zod/lawyerCreditUsageViewSchema.tsGenerated contract+20-0
src/lib/firm-profile/gateway.tsLibrary+7-2
src/lib/surface-registry/page-card/catalog.tsLibrary+1-0
src/lib/surface-registry/route-registry.tsLibrary+12-2
tests/unit/firm-profile-contract.test.tsTest+77-6