Commit Graph

13 Commits

Author SHA1 Message Date
m
4bab520119 feat(approval-pill): icon-only eye pill on /deadlines + /appointments + /agenda
m's 2026-05-08 cosmetic ask: the "Wartet auf Genehmigung" badge ate
row width and read as a noisy block of text on every pending row.
Replace with a 22px eye-icon pill; the lifecycle label moves to the
hover tooltip (title attr + aria-label so screen readers still get
the full text).

Three pieces:

  - global.css — new .approval-pill--icon modifier sets the pill to
    a circular 22×22 hit target with a centered SVG. Base
    .approval-pill (text-pill behavior) and --historic (inbox status
    pill) stay untouched so the inbox surface keeps rendering the
    full status + decider name.
  - client/events.ts (the /deadlines + /appointments shell) and
    client/agenda.ts each get a tiny APPROVAL_PILL_EYE_SVG constant
    + the new --icon class on the pending pill. Two definitions
    (no shared icons module today; no other surfaces need this glyph
    yet) — the duplication is two lines, easier to read than yet
    another import.

What it looks like: 👁 in a soft amber circle, hovers to "Änderung
wartet auf Genehmigung" / "Erledigung wartet auf Genehmigung" / etc.
The lifecycle-specific label kept (no schema work) — Maria gated this
slice as pure-frontend; the richer "wartet auf Genehmigung von
<role>; angefragt am <date>" tooltip needs a backend join we're not
doing here.

Refs t-paliad-160 §C / m's 2026-05-08 18:15 batch Item B.
2026-05-08 18:18:16 +02:00
m
bc47d78d97 feat(t-paliad-138): pending pills on /events and /agenda
Commit 6 of 8. Renders the approval-pending warning pill on the two
busiest list surfaces:

- /events (deadline + appointment list): ⚠ pill next to the title +
  soft-tinted row via .entity-row--pending-update modifier.
- /agenda (timeline): ⚠ pill in the headline + same row tint.

Changes:

- internal/services/event_service.go: EventListItem gains
  ApprovalStatus *string; projectDeadline / projectAppointment
  populate it from the embedded model.
- internal/services/deadline_service.go ListVisibleForUser: SQL adds
  f.approval_status / pending_request_id / approved_by / approved_at
  to the SELECT so DeadlineWithProject hydrates them.
- internal/services/appointment_service.go ListVisibleForUser: same
  for appointments + completed_at.
- internal/services/agenda_service.go: AgendaItem gains
  ApprovalStatus; the per-source SQL queries select it; the
  loadDeadlines / loadAppointments projection sets it.
- frontend/src/client/events.ts renderRow: adds entity-row--pending-update
  modifier and an inline approval-pill on the title cell when status='pending'.
- frontend/src/client/agenda.ts renderItem: same treatment on the
  agenda-item headline.

Generic "pending update" label (approvals.pending_update.label) — not
lifecycle-specific. The inbox carries the lifecycle detail. Showing
just one pill keeps the visual signal clear; an approver scanning a
list of pending entities sees them at a glance via the row tint, then
clicks through to /inbox to see what's pending and act.

Detail pages (/deadlines/{id}, /appointments/{id}) and /dashboard
deadline rail — pill rendering for those surfaces deferred to a
follow-up to keep this commit focused. Rendered everywhere it
matters most for daily use.
2026-05-06 16:05:00 +02:00
m
9919e04657 feat(t-paliad-128): /events 'Nur persönliche' = items I created
Redefines the "Nur persönliche" filter on /events from "appointment with
NULL project_id" to "items where created_by = me", applied uniformly to
deadlines and appointments.

Before: client-side filter dropped every deadline row because the type
guard was `x.type === "appointment"`. m saw zero deadlines under "Nur
persönliche" even though he created plenty.

After:
- /api/events?personal_only=true (and /api/events/summary?personal_only=true)
  narrow BOTH rails to f.created_by / t.created_by = current user.
  ProjectID is ignored when personal_only is set (the two are
  contradictory).
- DeadlineService.ListFilter and AppointmentService.AppointmentListFilter
  gain CreatedBy *uuid.UUID — composes with existing visibility (AND), so
  a row created on a team the user has since left still won't leak.
- Frontend drops the client-side filter; sends personal_only=true when
  projectFilter === PERSONAL. URL ?personal_only=true also accepted on
  initial load (bookmark-friendly alias for ?project_id=__personal__).
  Personal option now shows for type=Fristen too — applies uniformly.
- 3 new live subtests covering personal_only across type=deadline /
  appointment / all, with mixed-creator + multi-project + null-project
  fixtures.
2026-05-04 19:49:37 +02:00
m
4d7c74994a feat(t-paliad-125): sort project pickers by tree path with depth indent
The /events Project filter dropdown was sorted by `updated_at DESC`, so a
recently-touched Case appeared above its parent Client and cousins
interleaved unrelated branches — m's report (2026-05-04): "Siemens cases
come directly after 'mandant vs Gegner' and are not under 'Siemens-AG'".

Backend: switch ProjectService.List to ORDER BY p.path so every
descendant immediately follows its ancestor — the same ordering BuildTree
produces. Both callers (handleListProjects, searchProjects) gain a
stable, hierarchical default that matches user expectation.

Frontend: add project-indent.ts shared helper and apply NBSP indent
prefix in every <select> picker fed by /api/projects: events filter,
/deadlines/new, /appointments/new, checklist new-instance modal,
Fristenrechner save modal. NBSP avoids browser whitespace collapse
inside <option> labels. Multi-parent repetition is out of scope (data
model has singular parent_id today).

Tests: project_list_order_test pins the path-order contract against a
seeded mixed-recency tree.
2026-05-04 19:30:37 +02:00
m
1bba9cb3ce feat(t-paliad-123): apply date-bucket Status filter to appointments
Until now, /events hid the Status dropdown when Type=Termine. The
date-bucket filters (Heute, Diese Woche, Nächste Woche, Später) only
worked on the deadline rail — m wanted them on appointments too, even
without a "completed" dimension.

Frontend (events.ts):
- New populateStatusFilter() rebuilds the Status <select> options based
  on currentType: deadlines get the full 8-option set, appointments
  narrow to 5 (Alle + 4 buckets). The "completed/pending/overdue"
  options drop because they have no appointment analogue.
- applyTypeVisibility() no longer hides the Status filter for
  appointments; it calls the populator instead. The populator runs on
  type-chip click and on language change so labels translate live.
- When switching type while a now-invalid status is selected (e.g.
  Termine + status=completed via URL), the populator falls back to the
  per-type default (deadline → pending, appointment → all) and updates
  URL params.
- syncURLParams + isFilterPristine + initFilters use a per-type default
  so the appointment view treats `all` as pristine and stays out of the
  URL until the user picks a bucket.
- loadList always sends `status` to /api/events; backend already
  applies bucket-aware appointment filtering via
  bucketAppointmentWindow().

events.tsx:
- The static <option> list collapses to a single placeholder; the
  populator owns the option set at hydration.

i18n:
- New `events.filter.status.all` ("Alle"/"All") for the appointment-only
  case — `deadlines.filter.all` says "Alle (offen & erledigt)" which is
  wrong for appointments (they don't have a completed/pending state).

Backend (event_service_test.go):
- Three new live subtests confirming type=appointment + status=today
  narrows to today's appointments, status=later narrows to far-future,
  and status=completed collapses the appointment rail (defensive vs.
  URL-hacking — the dropdown excludes that value for appointments).
2026-05-04 18:56:25 +02:00
m
5875a62aba fix(t-paliad-117): /events filter polish — i18n leak, label position, panel anchoring
Three small polish bugs on the unified /events filter row.

A) i18n leak on lang change. The static [data-i18n] options in the appointment-
type select are retranslated by initI18n's applyTranslations(), but the project
select is rebuilt at runtime via populateProjectFilter() and the multi-select
trigger label is set via t() inside attachEventTypeMultiSelectFilter. Neither
re-ran on lang change, so DE→EN→DE left "All matters" / "All types" stuck on
the page. Wire onLangChange:
  - events.ts: re-call populateProjectFilter() inside the existing handler.
  - event-types.ts: attachEventTypeMultiSelectFilter now subscribes to
    onLangChange itself and re-renders updateLabel() + (when open) renderPanel().
    Self-contained side effect — also fixes /agenda's multi-select for free.

B) Filter caption above the field. The pre-existing .filter-row + .filter-group
pattern (already used on /projects) had .filter-group laid out horizontally
(label-left, select-right). Wrapped each events.tsx label+control pair in
.filter-group and switched .filter-group to flex-direction: column with a 4px
gap, so the caption sits above the select / button trigger. The whole filter-
row stays a horizontal flex-wrap of column cells. Mobile (≤480px) still falls
back to a single-column stack. /projects gets the same polish since it already
used the same wrapper pattern.

C) Multi-panel anchoring. The event-type popover was rendered as a sibling of
the trigger inside .filter-row and absolute-positioned with auto/auto, so it
landed wherever the wrapping flex layout put it (~226px above the trigger in
practice). Wrapped trigger+panel in <div class="multi-anchor"> with
position: relative and pinned the panel via top: 100%; left: 0 — only when
inside .multi-anchor, so /agenda's existing column-flex auto-positioning is
untouched.

toggleFilterPair simplified to hide the wrapping .filter-group via closest()
instead of toggling the label and control separately.
2026-05-04 18:02:26 +02:00
m
0ead001811 fix(dashboard,events): drop "Termine auf einen Blick" rail + fix multi-panel overflow leak
m's call (2026-05-04): the Dashboard's secondary "Termine auf einen
Blick" rail (3 cards) was redundant — the upcoming-Termine list lives
right below it on the same page, and the Fristen rail above is what
matters for the at-a-glance read. Drop the cards.

frontend/src/dashboard.tsx: remove the <section
aria-labelledby="dashboard-appointment-summary-heading"> and its 3 cards.

frontend/src/client/dashboard.ts: drop renderAppointmentSummary +
the AppointmentSummary type + the appointment_summary field on
DashboardData (kept the API-side payload — other consumers may use it
later; just stop wiring the dashboard to it).

Also two related event-page styling bugs:

- frontend/src/client/events.ts:721 was force-stamping
  `style.display = "block"` on the event-type multi-panel popup whenever
  the type filter was anything but appointment. The panel is supposed
  to be a hidden flyout owned by the trigger button via `panel.hidden`;
  the inline display:block trumped the `.multi-panel[hidden]` CSS rule
  and left it visible on larger screens (m flagged the
  `<div ... hidden="" style="display: block;">` artefact). Fix: never
  set inline display from this code path; force-close the panel only
  when switching to appointment view.

- frontend/src/styles/global.css `.multi-list` + `.multi-option`: long
  event-type labels overflowed the 22rem panel horizontally because
  `.multi-list` only had `overflow-y: auto` and the flex options had no
  `min-width: 0` / overflow-wrap rules. Add `overflow-x: hidden` +
  `min-width: 0` on the list and `overflow-wrap: anywhere` on options.
2026-05-04 17:08:34 +02:00
m
c554e865eb Merge: t-paliad-111 — bug bundle correctness (UPC GESAMTKOSTEN, court-set dates, REGEL save) 2026-05-04 14:42:51 +02:00
m
0be2dfb5a0 fix(t-paliad-111): bug bundle (correctness) — UPC GESAMTKOSTEN, court-set dates, REGEL save flow
Three correctness bugs from the t-paliad-101 QA sweep, fixed together since
they all change displayed/saved numbers users rely on.

B1 — Kostenrechner UPC GESAMTKOSTEN double-count
  ComputeUPCInstance was setting InstanceTotal = effectiveCourtFee +
  recoverableCeiling. The R.152 recoverable-cost cap is the OPPOSING
  side's worst-case loss-of-suit liability, not the user's own cost —
  folding it into GESAMTKOSTEN inflated the UPC total under a label
  that means "your outlay," and the DE LG/OLG/BGH branches don't add
  any opponent estimate. Drop it from InstanceTotal; the ceiling
  still surfaces as its own RecoverableCeiling line item.

  Live pre-fix on paliad.de (Streitwert 100k, UPC 1. Instanz only):
    instanceTotal = 52600 = 14600 court fee + 38000 R.152 ceiling
  Post-fix:
    instanceTotal = 14600 (court fee only); RecoverableCeiling stays 38000

B3 — Court-determined Termine emit trigger date as a real-looking date
  Zwischenverfahren / Mündliche Verhandlung / Entscheidung all live in
  paliad.deadline_rules with duration_value=0 and parent_id=NULL, so
  Calculate() classified them as IsRootEvent and emitted the trigger
  date as their own DueDate. Worse, RoP.151 "Antrag auf Kostenentscheidung"
  parents off inf.decision and chained 1 month off the placeholder ->
  bogus deadline that the UI rendered as real.

  Fix: classify a zero-duration rule as IsCourtSet (not IsRootEvent)
  when primary_party = 'court' or event_type ∈ {hearing, decision,
  order}. Track court-set rule IDs and propagate IsCourtSet downstream
  to any rule whose parent is court-set, so RoP.151 also surfaces as
  court-set rather than a fabricated date. Save-modal already greys
  out IsCourtSet rows so the "Gerichtsbestimmte Termine ohne Datum
  werden übersprungen" footnote becomes truthful again.

  Live pre-fix on paliad.de (UPC_INF, trigger 2026-04-29):
    Zwischenverfahren / Oral / Entscheidung -> dueDate 2026-04-29
    Antrag auf Kostenentscheidung -> 2026-05-29 (bogus, +1mo from trigger)

B6 — Fristenrechner save flow stored rule code in TITLE
  Frontend was concatenating "RoP.023 — Klageerwiderung" into the
  title because deadlines had nowhere else to put the citation, and
  the /deadlines REGEL column ended up showing "—". Add migration 032
  with a paliad.deadlines.rule_code text column, plumb it through
  CreateDeadlineInput / insertTx, drop the now-redundant r.code AS
  rule_code JOIN alias on the list query (the deadline owns its
  citation), and render f.rule_code on the project-detail deadlines
  table + /deadlines events list + deadline-detail page.

Build, vet, and tests all clean. New unit test
TestIsCourtDeterminedRule pins the B3 discriminator across the
event_type / primary_party combinations seen in migrations 012 + 031.

Repro creds: tester@hlc.de
2026-05-04 14:42:29 +02:00
m
56522adffe feat(t-paliad-115): canonicalise list URL on /events; redirect old paths
PR-2 of t-paliad-115. The unified Fristen + Termine surface now lives at
/events. Old /deadlines and /appointments list URLs 301-redirect to
/events?type=deadline and /events?type=appointment so existing bookmarks
still land on the right view. Detail pages (/deadlines/{id},
/appointments/{id}) stay type-specific.

Backend (Go).
- New `GET /events` route → handleEventsListPage serves dist/events.html.
- `GET /deadlines` → handleDeadlinesListRedirect (301 → /events?type=deadline).
- `GET /appointments` → handleAppointmentsListRedirect (301 → /events?type=appointment).
- /deadlines/new, /deadlines/calendar, /deadlines/{id}, /appointments/new,
  /appointments/calendar, /appointments/{id} unchanged — type-specific
  detail / form / legacy-calendar surfaces stay where they are.

Frontend.
- build.ts now emits ONE events.html (not events-deadlines /
  events-appointments) with defaultType="all" baked in. The page reads
  ?type=… and ?view=… on hydration, so /events?type=deadline lands on
  the Fristen-only Cards view, /events?view=calendar opens the calendar,
  and bare /events shows the Beides view.
- Sidebar Fristen / Termine entries point at /events?type=deadline and
  /events?type=appointment. The SSR active-state matches exactly via
  href === currentPath, so detail/new/calendar pages that pass
  currentPath="/events?type=deadline" (resp. appointment) still
  highlight the right entry.
- events.ts hydration adds applySidebarTypeHighlight(): on bare /events
  the sidebar SSRs with neither entry lit, and we re-highlight the
  matching entry whenever the in-page chip toggle changes the active
  type. Sidebar stays in sync without a server round-trip.
- Updated every list-target reference: palette-actions.ts (Cmd-K
  navigation), deadlines-detail.ts + appointments-detail.ts (post-delete
  redirect), and the back-link / cancel hrefs in the *-new + *-detail +
  *-calendar TSX templates. Detail-page Sidebar/BottomNav currentPath
  also moved from "/deadlines" → "/events?type=deadline" so the new
  highlight contract holds end-to-end.

Out of scope (per task brief).
- A third "Ereignisse / Alle Events" sidebar entry pointing at /events
  bare. m's call: keep two entries; defer until signal.
- Removing /deadlines/calendar + /appointments/calendar standalone
  pages. The new /events?view=calendar covers the same need but the
  legacy URLs stay live for one cycle.

Build clean: `cd frontend && bun run build` + `go build/vet/test ./...`.
2026-05-04 14:40:53 +02:00
m
1dad1c7371 feat(t-paliad-115): events view ⊥ filter — view selector + unified calendar view
PR-1 of t-paliad-115. Separates the view axis (cards / list / calendar)
from the filter axis (event type, status, project) on EventsPage. Closes
the duplicate "Kalenderansicht" button that t-paliad-110 left behind on
the Beides view.

Bug source: frontend/src/events.tsx:53-68 rendered TWO separate
"Kalenderansicht" anchors (events-action-deadline-cal +
events-action-appointment-cal) and frontend/src/client/events.ts:533-534
unhid both when type=all. Reproduced live on paliad.de — screenshot at
.playwright-mcp/paliad-115-duplicate-kalenderansicht-before.png.

Architectural change.
- Removed the per-type calendar buttons from the page header.
- Added a 3-button segmented view selector (Karten / Liste / Kalender)
  next to the type chips. The two controls now sit on a shared
  events-axis-row that flexes side-by-side and stacks on narrow viewports.
- View state lives in `currentView`, defaults to "cards", reads from
  ?view= on init, persists to URL on change. Default ("cards") stays
  out of the URL so existing bookmarks don't change.
- Cards view = original (5-card summary + table). List view = table
  only (cards hidden). Calendar view = month grid; cards + table both
  hidden.

Calendar view.
- Plots both deadlines (due_date) and appointments (start_at local
  date) on a Mo–So month grid. Reuses the existing .frist-cal-* CSS
  scaffold from /deadlines/calendar; only new addition is
  .events-cal-dot-appointment for the appointment hue (--bucket-next-week).
- Inherits the page's filters — `?view=calendar&type=appointment` shows
  appointment-only; `?view=calendar&status=all` shows everything; etc.
  Status, type, project, event-type filters apply orthogonally to view,
  matching the spec's "two axes combine" requirement.
- Click a day with items → existing modal pattern lists them with type
  chip + project ref; clicking an item navigates to its detail page.
- Month nav (prev / next / today) is purely client-side — no refetch,
  cheap pagination over the already-loaded items.

Out of scope (per task brief).
- Standalone /deadlines/calendar + /appointments/calendar pages stay
  untouched. PR-2 (URL canonicalisation) handles that surface.
- Custom time-window controls — future iteration per t-110 spec.

Build clean: `cd frontend && bun run build` + `go build/vet/test ./...`.
2026-05-04 14:34:44 +02:00
m
82421b3c86 fix(t-paliad-110): hide Überfällig card on Beides view
Spec is explicit that Überfällig is deadline-view-only — only showing
it on type=deadline matches the rationale that 'overdue' is a deadline-
specific concept (appointments don't go overdue, they happen). The
previous logic also surfaced the card on Beides whenever the
deadline-side overdue count > 0, which would have rendered an alarm
card on a mixed view.

Caught during live verification on paliad.de.
2026-05-04 13:57:40 +02:00
m
fe9c1b7de2 feat(t-paliad-110): add shared EventsPage component + bucket-aware backend tweaks
PR-2 of the Fristen+Termine unification. Pure additive change — the existing
deadlines.tsx + appointments.tsx pages stay live; this PR introduces the new
events.tsx shell + client/events.ts runtime that PR-3 will mount onto the
two routes.

Frontend (new):
- frontend/src/events.tsx — shared shell with the 3-chip type toggle
  (Fristen / Termine / Beides), the 5-card summary row (Überfällig
  conditional + 4 universal cards), the union filter row, and the unified
  table that renders a discriminated row per type. Two header CTAs ("Neue
  Frist" + "Neuer Termin") collapse to the relevant one in single-type mode.
- frontend/src/client/events.ts — runtime. Reads window.__PALIAD_EVENTS__
  (PR-3 will inject defaultType from the Go handler), derives the rest from
  ?type query param. Card click sets status filter; the events endpoint
  takes care of bucket-aware appointment-side date windowing so both rails
  stay in sync in Beides mode. Hide-on-uniform pattern applied per column
  (rule, event_type, location, appointment_type, status, row-type chip).
- frontend/build.ts — emits events-deadlines.html + events-appointments.html
  from one renderEvents(currentPath) so each output gets the right Sidebar
  highlight; client/events.ts bundle added.
- 16 i18n keys (DE+EN): events.toggle.*, events.summary.later,
  events.col.*, events.row.type.*, events.empty.*, events.unavailable plus
  the new deadlines.summary.later / deadlines.filter.later pair for the
  Später bucket.
- CSS: --bucket-later (#1d4ed8 light / #60a5fa dark) for the Später card,
  matching events-table--hide-* column hiders, .events-row-type-chip
  styling, .event-type-chip-row spacing.

Backend tweaks (small):
- DeadlineFilterLater (`later`): pending deadlines past Mon-week-after.
  Click-target for the Später card.
- EventService.ListVisibleForUser now derives an appointment-side date
  window from a bucket-style status (today/this_week/next_week/later) so
  card clicks filter both rails consistently. Overdue/Completed exclude
  appointments entirely (no appointment analogue).
- pickLater / pickEarlier helpers intersect the bucket-derived window with
  any caller-supplied from/to.

go build/vet/test ./... clean. bun run build clean (1394 keys, IIFE prologue
guard passes).
2026-05-04 13:46:33 +02:00