Admin UI: manage firm office list (firm-agnostic) #43

Open
opened 2026-05-20 07:53:08 +00:00 by mAi · 0 comments
Collaborator

Context

The firm's office list is hard-coded in two places that must stay in sync:

  • internal/offices/offices.go — the Go-side source of truth (All []Office slice).
  • DB CHECK constraints on paliad.users.office (mig 002) and paliad.partner_units.office (mig 018, renamed via mig 024 + mig 027).

Adding a new office (e.g. Madrid, m's 2026-05-20 ask, mig 106 / commit e035512) currently means: edit the Go slice + update the test + write a migration to extend both CHECK constraints. Three files, one PR per office. Not sustainable as the firm grows or as Paliad gets re-branded to firms with different office footprints.

Goal

Admin area page where a global_admin can add, rename, reorder, or retire an office without touching code or shipping a migration.

Design considerations (not prescriptive)

  • Schema shift: move from CHECK (office IN ('...', '...')) to a proper paliad.offices table with FK references from users.office_key and partner_units.office_key. Drops the CHECK; adds a referential integrity guarantee that's enforceable in the UI.
  • i18n: each office row carries label_de + label_en (the current Office struct shape). Editable in the admin UI.
  • Display order: explicit sort_order column (integer, gaps fine) — admin can drag to reorder.
  • Soft retire, not delete: an office that's already FK'd from existing users / partner_units can't be hard-deleted. Add retired_at timestamptz NULL. Retired offices don't appear in the new-user/team-creation dropdowns but stay valid on existing rows.
  • Backfill: migration seeds the table with the current 8 (munich/duesseldorf/hamburg/amsterdam/london/paris/milan/madrid), in display order, with the existing labels. Existing rows on users / partner_units re-key against the new table.
  • Go-side: the offices package becomes a service-loaded cache (refresh on admin write), not a compile-time slice. The IsValid / Keys API can stay; implementation reads from DB.
  • Frontend: the existing /api/offices endpoint stays — admin UI just adds CRUD on it. Settings / onboarding / admin-team / admin-partner-units dropdowns pick up new offices automatically (they already read from the API).
  • Permissions: editing the office list is global_admin only. Reading is anyone authenticated (it's already a public read via /api/offices).

Out of scope

  • Office-level RLS / visibility rules (already handled separately via team membership).
  • Per-office branding / language defaults (firm-wide FIRM_NAME covers that).
  • Renaming an office key (the key column should be immutable once any row references it — labels are editable, keys are not).

Acceptance

  • A global_admin can navigate to /admin/offices (or wherever it lives in the admin tree), see the current list, add a new office, edit a label, reorder, and retire.
  • New offices appear in the user/team/partner-unit dropdowns within one page load.
  • Retiring an office removes it from new-user dropdowns but doesn't break existing rows referencing it.
  • The Go-side offices package serves the live DB list (not the hard-coded slice).
  • mig 106 (Madrid) and the hard-coded slice both go away after the table is the source of truth.

Role recommendation

inventor — schema shift + migration plan + admin UI flow deserve a design pass before implementation. Then coder shift for the build.

## Context The firm's office list is hard-coded in two places that must stay in sync: - `internal/offices/offices.go` — the Go-side source of truth (`All []Office` slice). - DB CHECK constraints on `paliad.users.office` (mig 002) and `paliad.partner_units.office` (mig 018, renamed via mig 024 + mig 027). Adding a new office (e.g. Madrid, m's 2026-05-20 ask, mig 106 / commit e035512) currently means: edit the Go slice + update the test + write a migration to extend both CHECK constraints. Three files, one PR per office. Not sustainable as the firm grows or as Paliad gets re-branded to firms with different office footprints. ## Goal Admin area page where a global_admin can add, rename, reorder, or retire an office without touching code or shipping a migration. ## Design considerations (not prescriptive) - **Schema shift**: move from `CHECK (office IN ('...', '...'))` to a proper `paliad.offices` table with FK references from `users.office_key` and `partner_units.office_key`. Drops the CHECK; adds a referential integrity guarantee that's enforceable in the UI. - **i18n**: each office row carries `label_de` + `label_en` (the current `Office` struct shape). Editable in the admin UI. - **Display order**: explicit `sort_order` column (integer, gaps fine) — admin can drag to reorder. - **Soft retire, not delete**: an office that's already FK'd from existing users / partner_units can't be hard-deleted. Add `retired_at timestamptz NULL`. Retired offices don't appear in the new-user/team-creation dropdowns but stay valid on existing rows. - **Backfill**: migration seeds the table with the current 8 (munich/duesseldorf/hamburg/amsterdam/london/paris/milan/madrid), in display order, with the existing labels. Existing rows on users / partner_units re-key against the new table. - **Go-side**: the `offices` package becomes a service-loaded cache (refresh on admin write), not a compile-time slice. The `IsValid` / `Keys` API can stay; implementation reads from DB. - **Frontend**: the existing `/api/offices` endpoint stays — admin UI just adds CRUD on it. Settings / onboarding / admin-team / admin-partner-units dropdowns pick up new offices automatically (they already read from the API). - **Permissions**: editing the office list is global_admin only. Reading is anyone authenticated (it's already a public read via `/api/offices`). ## Out of scope - Office-level RLS / visibility rules (already handled separately via team membership). - Per-office branding / language defaults (firm-wide `FIRM_NAME` covers that). - Renaming an office key (the `key` column should be immutable once any row references it — labels are editable, keys are not). ## Acceptance - A global_admin can navigate to /admin/offices (or wherever it lives in the admin tree), see the current list, add a new office, edit a label, reorder, and retire. - New offices appear in the user/team/partner-unit dropdowns within one page load. - Retiring an office removes it from new-user dropdowns but doesn't break existing rows referencing it. - The Go-side `offices` package serves the live DB list (not the hard-coded slice). - mig 106 (Madrid) and the hard-coded slice both go away after the table is the source of truth. ## Role recommendation **inventor** — schema shift + migration plan + admin UI flow deserve a design pass before implementation. Then coder shift for the build.
mAi self-assigned this 2026-05-20 07:53:08 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: m/paliad#43
No description provided.