# Partner Units — rename + admin management UI **Task:** t-paliad-070 **Inventor:** cronus (mai/cronus/partner-units-rename worktree) **Date:** 2026-04-29 **Status:** DESIGN v2 — m answered the open questions 21:44 Wed 29.04. Revised doc below; awaiting head greenlight before coder shift. ## m's answers (21:44 Wed 29.04.) summarised 1. **Naming**: `partner_unit` everywhere (snake_case for DB/JSON, `PartnerUnit` for Go types, `partner-unit(s)` for kebab-URLs). 2. **Rename API too**: `paliad.departments` → `paliad.partner_units`, `paliad.department_members` → `paliad.partner_unit_members`, `/api/departments/*` → `/api/partner-units/*`. Full consistency. 3. **Settings admin section**: remove (don't duplicate). 4. **Audit emit**: yes, in this PR. 5. **Free-text column drop**: yes — drop `users.dezernat` entirely instead of renaming. Phase 2 collapses into Phase 1. This dramatically expands the rename scope but produces a single coherent end-state (no transitional German names anywhere, no duplicate-state debt). Single PR is now even more important — splitting would leave the code in an unrunnable mid-rename state for any non-trivial duration. --- ## 1. The two concerns m wants: 1. The user-facing concept "Dezernate" renamed to **"Partner units"** everywhere. 2. The placeholder card on `/admin` ("Dezernate / Kommt bald") replaced with a real `/admin/departments` management surface. These two concerns share the same code surface, so this design treats them as one PR. --- ## 2. Live-state inventory (2026-04-29) What already exists: | Layer | Status | |---|---| | **DB tables** | `paliad.departments` and `paliad.department_members` already English (renamed in migrations 020 + 024). RLS policies, FKs, indexes already English. | | **DB column** | `paliad.users.dezernat` — German legacy, free-text `text` column added in migration 015. | | **Go service** | `internal/services/department_service.go` — full CRUD + member management. Admin-gated via `requireAdmin` (`global_role='global_admin'`). | | **Go handlers** | `internal/handlers/departments.go` — 8 routes registered under `/api/departments/*`. | | **Frontend admin CRUD** | Already shipped — but **inside `/settings?tab=dezernat`**, not on a dedicated admin page. Visible only to global_admin (gated client-side via `me.global_role`). | | **Admin landing** | `/admin` shows a "Geplant / Kommt bald" Dezernate card pointing nowhere. | | **Admin team page** | `/admin/team` has a "Dezernat" free-text column and edit input bound to `paliad.users.dezernat`. | | **Onboarding** | Asks for "Dezernat / Partner" as free text, persists to `users.dezernat`. | | **Settings profile tab** | Asks for "Dezernat oder Partner" free text. | | **Team directory** | `/team` groups colleagues by `users.dezernat` free-text fallback when `paliad.departments` membership is missing. | The duplicate-state debt is real: the same concept lives in two places — the structured `paliad.departments` registry (admin-managed) and the free-text `paliad.users.dezernat` column (user-typed). Migration 019 backfilled the former from the latter, but they have been drifting apart since. **Resolving that drift is out of scope for this task** — flagged as Phase 2. Counts (`grep -l`): - 7 Go files mention `dezernat` / `Dezernat` - 10 frontend files (`.ts` / `.tsx`) - 2 SQL migrations (015 = column add, 019 = seed function) - ~80 i18n strings --- ## 3. Naming decisions (per m) ### 3.1 User-facing label (cross-language) **"Partner unit" / "Partner units"** — same English phrase in DE and EN. Capitalised loanword in DE strings ("Partner Unit anlegen", "Partner Units verwalten"). ### 3.2 Internal names — full rename to `partner_unit` Per m's "lets fix departments even in api?!", everything Department-shaped on the structured side renames too. End state: | Surface | Before | After | |---|---|---| | Table | `paliad.departments` | `paliad.partner_units` | | Junction table | `paliad.department_members` | `paliad.partner_unit_members` | | FK column on junction | `department_id` | `partner_unit_id` | | Constraint names | `departments_*`, `department_members_*` | `partner_units_*`, `partner_unit_members_*` | | Index names | same prefix | same prefix | | RLS policy names | `departments_select` etc. | `partner_units_select` etc. | | Go type | `models.Department` | `models.PartnerUnit` | | Go type | `services.DepartmentMember` | `services.PartnerUnitMember` | | Go type | `services.DepartmentWithMembers` | `services.PartnerUnitWithMembers` | | Go service | `DepartmentService` (`Service.Department`) | `PartnerUnitService` (`Service.PartnerUnit`) | | Go file | `internal/services/department_service.go` | `internal/services/partner_unit_service.go` | | Go file | `internal/handlers/departments.go` | `internal/handlers/partner_units.go` | | API path | `/api/departments` | `/api/partner-units` | | API path | `/api/departments/{id}/members` | `/api/partner-units/{id}/members` | | Admin URL | `/admin/departments` | `/admin/partner-units` | | TSX file | (new) `admin-partner-units.tsx` | same | | Client TS | (new) `client/admin-partner-units.ts` | same | | JSON keys | `department_id`, `lead_user_id`, `members[]` | `partner_unit_id`, `lead_user_id`, `members[]` | | i18n keys | `dezernat.*` | `partner_unit.*` | | CSS classes | `.dezernat-*` | `.partner-unit-*` | | CSS classes | (none today) | `.partner-unit-*` | ### 3.3 The `users.dezernat` free-text column **Drop entirely** (per m's answer 5). Migration also re-runs migration 019's seed logic immediately before the drop, to capture any drift since 019 ran (users who edited their `dezernat` value via `/settings` after 019 won't have a corresponding `partner_unit_members` row). Idempotent `ON CONFLICT DO NOTHING`. This means **the onboarding form stops asking for a free-text Dezernat/ Partner field** and **the settings profile tab stops surfacing it**. Replacement UX (lightweight — same PR): - **Onboarding**: replace the free-text `dezernat` input with a ``. 13. Sign out, sign back in as a non-admin — confirm `/admin/partner-units` returns 302 to `/dashboard?forbidden=admin`, sidebar admin section is hidden. ### 9.3 Playwright (optional — confirm with head) If Playwright smoke is desired, mirror t-paliad-050's admin-team pattern: navigate, create, edit, delete, screenshot. Add an SQL assertion step that checks `partner_unit_events` row counts after each action. --- ## 10. PR strategy **Single PR, single merge to main.** Reasoning: - The rename touches the same files as the new admin page (admin.tsx, i18n.ts, settings.tsx, admin-team.tsx, sidebar.tsx, onboarding.tsx, team.tsx). Splitting forces ugly rebases. - The migration is multi-statement but single-tx — no risk of partial apply. - The user-facing label change is consistent only after the WHOLE diff lands. A split would land "internal rename" with old labels still saying "Dezernat", then "label change" — confusing during the gap. - Settings has a redirect dependency (`/settings?tab=dezernat` 404 fallback) that's only safe once the entire dezernat surface is gone. Branch already in place: `mai/cronus/partner-units-rename`. Estimated diff size: ~2200 lines net. Heavier than v1 because the structured-side rename (Department → PartnerUnit) cascades through service/handler/types/SQL/tests, plus onboarding form rebuild, plus audit table + emit plumbing. No new dependencies. --- ## 11. Out of scope (deferred) - **Hierarchical partner units** — flat list only. Per brief. - **Per-partner-unit branding** (logo, colour) — defer. - **Non-admin permission model** (lead manages own unit's members) — defer. - **Audit UI** (a viewer for `partner_unit_events`) — defer to t-paliad-071. Emission lands here; consumption + a unified events surface lands there. - **Other entities' audit emission** — only partner units in this PR. Projects already have `project_events`; deadlines/appointments already emit. No global cross-entity audit yet. - **Onboarding "create new partner unit" inline** — the new select offers existing units + "(noch keine zuordnung)". A user wanting a new unit asks an admin or self-promotes via `/admin/partner-units` post-onboarding (only global_admin sees that page). Inline create-during-onboarding is a small follow-up if friction surfaces. --- ## 12. Open questions — RESOLVED 21:44 Wed 29.04. (m's answers) | # | Question | m's answer | |---|---|---| | 1 | Column rename target | **partner_unit** (became "drop entirely" after Q5) | | 2 | API + URL rename | **yes — fix departments in api too** | | 3 | Settings admin section removal | **yes** ("you do you") | | 4 | Audit emit in this PR | **yes** ("sure why not") | | 5 | Drop free-text column | **yes** ("makes sense") | No remaining open questions. Design is now greenlit pending head's gate review of this v2 doc. --- ## 13. Files (final) ### New - `internal/db/migrations/026_rename_to_partner_units.up.sql` - `internal/db/migrations/026_rename_to_partner_units.down.sql` - `internal/services/partner_unit_service.go` (renamed from `department_service.go` via `git mv` so blame survives — content rewritten for type + SQL renames + audit emit) - `internal/handlers/partner_units.go` (renamed from `departments.go`) - `internal/handlers/admin_partner_units.go` — page-serve handler - `frontend/src/admin-partner-units.tsx` - `frontend/src/client/admin-partner-units.ts` ### Edit (Go) - `internal/services/services.go` — wire `PartnerUnit *PartnerUnitService`. - `internal/services/user_service.go` — drop `Dezernat` field from struct, drop dezernat from SQL columns, drop dezernat from CreateUserInput + UpdateUserInput, etc. - `internal/services/user_service_test.go` — drop dezernat assertions; add partner_unit_id + member-row assertions if onboarding/admin-create paths now insert membership. - `internal/models/models.go` — drop `User.Dezernat`; rename `Department` → `PartnerUnit`, `DepartmentMember` → `PartnerUnitMember`. - `internal/handlers/admin_users.go` — drop dezernat from admin create/update payloads. - `internal/handlers/handlers.go` — re-register `/api/partner-units/*`, add `GET /admin/partner-units`, drop `dbSvc.department` field, add `dbSvc.partnerUnit`. - `internal/handlers/redirects.go` — drop the `/dezernate` → `/departments` entry (the path is dead post-rename) OR keep for one cycle; flag in PR description. - `internal/handlers/appointments_pages.go` — drop the `"dezernat"` / `"department"` tab aliases entirely (tab is gone). Default fallback handles `/settings?tab=dezernat` gracefully. ### Edit (frontend) - `frontend/src/admin.tsx` — flip the Partner-Units card from "Geplant" to "Verfügbar". - `frontend/src/admin-team.tsx` — drop the "Dezernat" column and the add-form input. - `frontend/src/client/admin-team.ts` — drop dezernat from payload + render. - `frontend/src/onboarding.tsx` — replace free-text input with `