Files
paliad/internal
m 073af975f7 feat(approvals/t-paliad-160 slice2): admin UI flip + badge + withdraw + inbox visibility hardening
A3 — admin/approval-policies 2-control flip:
  Each cell becomes [✓] requires_approval checkbox + role select + clear
  button. The "none" option in the role dropdown is gone — the checkbox
  replaces it. Role select is greyed when the checkbox is off (gate
  closed). Clear button explicitly drops the cell back to inheritance.
  Project matrix surfaces inherited "no approval" state with its own
  attribution chip ("Geerbt · keine Genehmigung") so admins can tell a
  silently-inherited off-state from a never-authored cell.

  PUT /api/.../approval-policies/{entity}/{lifecycle} accepts the new
  shape `{requires_approval: bool, min_role: string|null}` while still
  honouring the legacy `{required_role: "..."}` body during the M1
  dual-read window (decodePolicyBody routes to UpsertProjectPolicySplit
  vs UpsertProjectPolicy accordingly).

C+E — Pending-approval badge + Withdraw button:
  deadlines-detail + appointments-detail surface a "Wartet auf
  Genehmigung" badge when approval_status='pending'. Hover-tooltip
  carries requested_at + required_role + requester_name. Action
  controls (Complete, Edit, Delete) freeze while pending — caller
  would get a 409 anyway, no point letting them try.

  Withdraw button visible only to the requester (me.id ===
  pending_request.requested_by). Click → POST /api/approval-requests/
  {id}/revoke (existing endpoint, no new server route). On success,
  the entity flips back to approval_status='approved' and the page
  re-renders with normal controls.

  Complete button now handles 409 from the server gracefully:
  surfaces the new mapApprovalError body's `message` instead of
  silently disabling itself.

D — /inbox "Meine Anfragen" visibility hardening:
  Three defence-in-depth fixes for the "tab shows empty" report:
    1. handlers force `[]` (not Go-nil → JSON null) on every inbox
       endpoint so the frontend never trips on `rows.length` of null.
    2. parseInboxFilter validates ?status= against an allowlist
       (pending|approved|rejected|revoked|superseded). Anything else
       is silently dropped — a stray ?status=foo from a stale
       frontend build can no longer shadow rows out of the result.
       entity_type filter same treatment (deadline|appointment).
    3. Frontend inbox.ts coerces null body → [] so older / cached
       builds talking to the new server still don't crash.

  Test coverage: TestParseInboxFilter_DropsUnknownStatus +
  TestApprovalService_ListSubmittedByUser_PendingVisible (live-DB,
  skipped without TEST_DATABASE_URL).

Build clean: bun build OK, go test ./... OK.

Defers: M2 (drop required_role column) — only fires once all
in-tree writers are confirmed off the legacy column path.
2026-05-08 17:07:46 +02:00
..