Commit Graph

9 Commits

Author SHA1 Message Date
mAi
bcfde73815 feat(inbox): t-paliad-249 Slice A frontend — inbox dispatch + UI axes (m/paliad#80)
The /inbox surface drops "Genehmigungen" framing in favour of "Inbox"
and renders the unified feed.

- shape-list.ts: factor renderApprovalRow out of renderApprovalList so
  it can be reused alongside renderProjectEventInboxRow inside the new
  renderInboxList (row_action="inbox"). Project_event rows show a
  compact stream layout with an Öffnen link pointing at the right
  project tab (deadlines / appointments / notes).
- filter-bar gets two new axes: unread_only (binary chip cluster) +
  inbox_focus (4-chip coarse cluster: Alles / Genehmigungen / +Termine
  / +Fristen). Both round-trip via url-codec; inbox_focus translates
  to (sources, project_event.event_types, approval_request.entity_types)
  at the bar's resolve step (applyInboxFocusOverlay).
- FilterSpec gains a top-level unread_only flag; the bar writes it
  when the user toggles the chip; the server overlays the cursor.
- /inbox header: new "Alles als gelesen markieren" button POSTs
  /api/inbox/mark-all-seen with up_to=<newest visible row> for
  race-safety against a second tab.
- INBOX_AXES adds project + project_event_kind as advanced override
  chips so power users can still narrow per kind.
- i18n: inbox.title.feed / inbox.heading.feed / inbox.action.mark_all_seen
  / inbox.action.open / inbox.empty.feed / views.bar.unread_only.*  /
  views.bar.inbox_focus.* (DE + EN).
- url-codec round-trip tests for the two new axes.
2026-05-25 15:49:54 +02:00
mAi
f3b947e3ad feat(approvals): t-paliad-217 Slice C — approval-edit-modal full rewrite
Rewrite atop the unified openModal() primitive (Slice A). Drops the
per-modal ESC + focus + backdrop + close-button handlers — the
primitive owns them.

New three-section body per design §2:
  1. Editable fields. Every editable column on the entity, per m's Q1
     Reading A lock-in:
       deadline:    title, due_date, original_due_date, warning_date,
                    rule_code, description, notes, event_type_ids
                    (attached via the existing event-types picker).
       appointment: title, start_at, end_at, location, appointment_type,
                    description.
  2. Read-only context. Project title, requester, requested_at, current
     approval status. Renders as a definition-list with muted dt/dd
     pairs so the eye lands on the editable section first.
  3. Vorschlagskommentar (note). Always present, prominent.

Block labels matching /deadlines/new + views editor — reuses the
existing .form-field shapes for typography + spacing parity with the
rest of the app (m's Q6 lock-in).

inbox.ts gains projectTitle / requesterName / requestedAt hydration
from the per-row API response so the context section has data to
render. Falls back gracefully when missing.

Submit-button gate (in the openModal primary handler): refuses when no
field is dirty AND the note is empty. Mirrors the server's
ErrSuggestionRequiresChange.

CSS .approval-suggest-* classes added to global.css alongside the
modal primitive block (committed in Slice A).
2026-05-20 13:05:59 +02:00
mAi
0fd02bf033 feat(approvals): t-paliad-216 — wire suggest-changes click in /inbox
Suggest-changes branches off into handleSuggestChanges() instead of the
generic POST-with-prompt-note path. It:

  1. Fetches the full request payload + pre_image via GET
     /api/approval-requests/{id} — list payload may be stale.
  2. Opens approval-edit-modal with the entity_type-appropriate fields
     pre-populated.
  3. On submit, POSTs to /api/approval-requests/{id}/suggest-changes
     with {counter_payload, note}.
  4. On success, refreshes the filter bar (the OLD row flips to
     changes_requested, the NEW pending row appears) and the inbox badge.
  5. Stamps data-spawned-request-id on the OLD row's <li> so downstream
     code / tests can locate the counter without a re-query.

The error mapping (mapApprovalError) gains the two new codes
suggestion_requires_change + suggestion_lifecycle_invalid plus the
generic codes already handled. Body reads now go through `body.code`
preferentially with `body.error` fallback — the server uses {code,
message} envelopes.
2026-05-20 10:02:36 +02:00
m
4670cd660a feat(inbox): migrate to <FilterBar> — t-paliad-163 Slice 3
/inbox is the first surface to consume the universal FilterBar. The
two-tab UI collapses into the bar's approval_viewer_role chip cluster
(per Q4 lock-in 2026-05-08 21:47); status / entity_type / time chips
are new affordances; density toggle gives the activity-feed look the
brief asked for.

Changes:
- system_views.go: InboxSystemView + InboxRequesterSystemView render
  spec gains RowAction=approve so shape-list.ts knows which row
  layout to stamp (entity title + diff + approve/reject/revoke).
- shape-list.ts: row_action='approve' branch — stamps the inbox-row
  markup the surface owned today; surface attaches click handlers
  via data-attrs on .views-approval-action / .views-approval-row.
- inbox.tsx: tab row replaced with <div id='inbox-filter-bar'> +
  <div id='inbox-results'>. Heading + admin nudge unchanged.
- client/inbox.ts: shrunk to mountFilterBar with axes [time,
  approval_viewer_role, approval_status, approval_entity_type,
  density, sort]. Action handlers run via fetch + bar.refresh().
  Legacy ?tab=mine -> ?a_role=self_requested redirect on mount so
  bookmarks / sidebar bell still land on the right sub-view.

Build clean: bun run build + go build/vet/test all pass.
2026-05-08 21:59:44 +02:00
m
4ecea7a4bb feat(paliadin/agent-glyph): t-paliad-161 Slice E — alongside 👀
When a pending row was drafted by Paliadin (requester_kind='agent' on
its in-flight approval_request), surface a sparkle  next to the
existing eye-pill 👀. The two glyphs are orthogonal: 👀 = "needs
approval",  = "Paliadin drafted this". Either can change without the
other, so the visual taxonomy stays decomposable for any future
autopilot mode where 👀 disappears but  stays.

Read-path:

- DeadlineService.ListVisibleForUser + AppointmentService.ListVisibleForUser
  LEFT JOIN paliad.approval_requests on pending_request_id and project
  ar.requester_kind into the row. NULL when no request is pending.
- models.DeadlineWithProject + AppointmentWithProject grow
  RequesterKind *string. The list-projection helpers
  (projectDeadline / projectAppointment in event_service.go) carry it
  into EventListItem.
- /api/events response now includes requester_kind on every pending
  row; /api/inbox already does (Slice D extended approvalRequestViewColumns).

Render-path:

- frontend/src/client/events.ts — new AGENT_PILL_GLYPH constant (""),
  agentPill rendered into the title cell next to the existing
  pendingPill when item.approval_status='pending' AND
  item.requester_kind='agent'. EventListItem TS shape gains
  `requester_kind?: "user" | "agent"`.
- frontend/src/client/agenda.ts — same pattern, agendaItem TS shape
  + agentPill rendered next to pendingPill in the headline span.
- frontend/src/client/inbox.ts — ApprovalRequestView gains
  requester_kind + agent_turn_id; the meta line replaces the
  requester's plain name with "Anna  Paliadin" when the request was
  drafted by the agent.

CSS: new .approval-pill--agent modifier in global.css using only
existing tokens (--color-bg-lime-tint / --color-surface-2 /
--color-text), mirroring the .approval-pill--icon shape so the two
glyphs sit side-by-side at the same baseline.

i18n: 3 new keys × 2 langs (approvals.agent.label /
approvals.agent.byline / approvals.agent.suggestion_pending) — total
1966 → 1969.

Build clean (frontend + go), tests green.

Refs: docs/design-paliadin-inline-2026-05-08.md §8.
2026-05-08 20:04:10 +02:00
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
m
5df4285e1d feat(t-paliad-154) commit 5/5: inbox empty-state nudge + form-time hints
Three remaining surfaces from the locked design (Q9 + Q13):

/inbox empty-state admin nudge (Q9):
- New conditional block (.inbox-admin-nudge) revealed only when:
  * /api/me reports global_role='global_admin'
  * the inbox tab returned zero rows
  * /api/admin/approval-policies/seeded reports any=false (no policies firm-wide)
- Card links to /admin/approval-policies. Hidden in every other case so the
  ordinary post-rollout state (admins with active policies) sees nothing.

Form-time 4-eye hint on /projects/{id}/deadlines/new + /appointments/new (Q13):
- New .approval-hint container above the Speichern button on each form;
  hidden by default.
- Client TS fires GET /api/projects/{id}/approval-policies/effective on
  page load + on project change, reveals the hint when required_role is
  non-null and not 'none'. Renders role label + source attribution
  ('· Standard: Munich Lit') so the user knows where the rule comes from.
- Hides in every 'no policy applies' case (no candidates / 'none' suppression
  / project change to a project with no policy / fetch error).

i18n: 6 new keys × 2 langs (3 inbox-nudge keys + 2 form-hint keys + the
inbox-nudge title/body/cta wired in inbox.tsx). Total i18n keys: 1929.

Dynamic-key call sites use tDyn (admin-approval-policies.ts +
deadlines-new.ts + appointments-new.ts) so the typed t() barrier stays
intact for static keys.

Build: bun run build clean, go build + vet + test clean (no DB tests
require TEST_DATABASE_URL — those run in CI).
2026-05-08 02:31:35 +02:00
m
18faf81f58 fix(t-paliad-138): /inbox missing initSidebar() call — sidebar JS never ran on inbox page (pin restoration, hover-expand, badge polling all dead). One-liner add to inbox.ts 2026-05-06 16:50:21 +02:00
m
07a1c17861 feat(t-paliad-138): /inbox page + sidebar bell badge
Commit 5 of 8. End-user surface for the approval workflow:

- /inbox page (frontend/src/inbox.tsx + client/inbox.ts) with two tabs:
  "Zur Genehmigung" (requests I qualify to approve) and "Meine
  Anfragen" (requests I submitted). Each row shows the project, entity
  title, lifecycle event, requester name + age, the date-field diff
  (for update/complete/delete) and the relevant action buttons:
  approve + reject when on pending-mine, revoke when on mine.
  Historic rows render a status pill instead of buttons.
- Sidebar bell entry "Genehmigungen" (with sidebar-inbox-badge) under
  the Übersicht group. sidebar.ts polls /api/inbox/count every 60s and
  shows the count (or 9+ ceiling) when > 0.
- Server registration: GET /inbox → dist/inbox.html, gated by
  gateOnboarded. Already-registered API endpoints (commit 4) handle
  the data path.
- Bilingual (DE primary / EN secondary) i18n strings under
  approvals.* — labels, status names, lifecycle names, role names,
  decision-kind names, action verbs, error messages. ~50 new keys.
- Pending-state CSS classes: .approval-pill, .approval-pill--historic,
  .entity-row--pending-{create,update,complete,delete},
  #sidebar-inbox-badge. Soft-tint rows + amber pill so an approver
  can scan a list of pending entities at a glance. Used by commit 6
  (pending pills across surfaces) — no other surface picks them up
  yet, but the styles are wired and ready.
- Sidebar.tsx navItem signature gains an optional badgeID parameter
  so any future sidebar entry can host a count-badge with one extra
  argument (no per-entry custom rendering).
2026-05-06 16:00:17 +02:00