m's UX bug (2026-05-17, paliad.de prod): clicking Genehmigen/Ablehnen/
Zurückziehen on a row the viewer can't act on alerted ("Eigengenehmigung
nicht zulässig.", "Sie haben nicht die erforderliche Rolle.") after the
POST round-trip. m's ask: "approval that i cannot grant should have the
'Genehmigen' button greyed out... that would be better than showing an
error when I try."
Backend (internal/services/approval_service.go):
- ApprovalRequestView gains viewer_can_approve + viewer_is_requester
booleans. Resolved server-side per caller — false on self-authored rows
(caller == requester), true when the eligibility predicate matches.
- Extract the eligibility EXISTS-block into approvalEligibilitySQL const
and reuse it in ListPendingForApprover (WHERE), PendingCountForUser
(WHERE), and the new viewer_can_approve SELECT expression. Single
source of truth for the gate, identical to canApprove.
- ListPendingForApprover, ListSubmittedByUser, and GetRequest all bind
$1 = callerID so the SELECT computes the flags inline (one query, no
N+1). GetRequest's signature grows a callerID arg; the handler passes
the authenticated user.
Frontend (frontend/src/client/views/shape-list.ts):
- ApprovalDetail picks up the two booleans (optional — falsy is safe:
it disables, never falsely enables).
- approvalActionBtn renders the button as before but flips
btn.disabled + sets a tooltip via disabledReasonFor: approve/reject
share the viewer_can_approve gate (self → self_approval tooltip;
unauthorized → not_authorized); revoke needs viewer_is_requester.
- All three buttons still render on every pending row so users see
what's possible — the disabled+tooltip combo explains what's not.
i18n + CSS:
- 3 new keys × DE/EN: approvals.disabled.{self_approval,
not_authorized,revoke_not_requester}.
- .inbox-row-action:disabled neutralises the .btn-primary/danger/
secondary variant via opacity + not-allowed + muted tokens.
Tests:
- internal/services/approval_service_test.go::TestApprovalService_ViewerFlags
is a 4-case table-driven live-DB test (skips without TEST_DATABASE_URL):
self-authored (false/true), eligible peer (true/false), non-eligible
viewer (false/false), global_admin (true/false). Also asserts the flags
on ListPendingForApprover + ListSubmittedByUser rows.
Defence-in-depth preserved: server still rejects illegal POSTs with the
same error contract, and the alert path stays in inbox.ts for the race
where state changes between render and click.