Commit Graph

172 Commits

Author SHA1 Message Date
m
04d034af81 feat(t-paliad-126): Fristenrechner auto-calc on tab open + input change
The Verfahrensablauf and "Was kommt nach" tabs now render results
immediately, without requiring a click on "Fristen berechnen". The
button stays as a manual force-recalc affordance.

- Pre-select the first proceeding type on load so step 3 has data
  out of the box.
- Pre-select the first trigger event on first event-tab activation
  (or right after the list loads if the tab was already active).
- Auto-recalc on date / proceeding-type / condition-flag change.
- Debounce input events to 200ms so spam-edits coalesce into one
  request, with a per-mode sequence counter so a stale fetch result
  can never overwrite a fresher one.
2026-05-04 19:33:47 +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
0587fc2296 Merge: t-paliad-119 — Fristenrechner shift-reason explainer (UPC vacation, holiday name, weekend) 2026-05-04 18:37:17 +02:00
m
d688ebde90 feat(t-paliad-119): explain WHY a Fristenrechner deadline was shifted
The current "Wochenende/Feiertag" / "weekend/holiday" label hides the cause
of long shifts — m's reproduction had a deadline jump from 4.8.2026 to
31.8.2026 (+27 calendar days) across UPC Summer Vacation, and the UI made
it look like a bug. The math was correct; the explanation was lying.

Backend:
- AdjustForNonWorkingDaysWithReason returns an AdjustmentReason alongside
  the adjusted date. Walks the same 60-iter loop, classifies the dominant
  cause (vacation > public_holiday > weekend), collects every named
  holiday hit, and for vacations scans outward to report the contiguous
  block boundary (27.7.–28.8., not the 25 individual rows).
- AdjustForNonWorkingDays now wraps the new method, preserving its
  3-tuple signature for existing callers (deadline_calculator,
  event_deadline_service).
- UIDeadline gains an AdjustmentReason field; FristenrechnerService
  populates it on every shifted deadline.
- Date fields serialise as YYYY-MM-DD strings (HolidayDTO + string
  vacation span) — the Fristenrechner client already speaks that format.

Frontend:
- AdjustmentReason → human-readable phrase via renderAdjustmentReason:
    vacation       → "{vacation_name} ({span})"
    public_holiday → "Feiertag ({first_holiday_name})" / "{name} holiday"
    weekend        → "Wochenende" / localised weekday
- Surrounding format becomes "Verschoben wegen X: A → B" (DE) or
  "Shifted (X): A → B" (EN). Falls back to the legacy reason string
  when the backend hasn't sent a structured reason.
- Vacation names render verbatim from paliad.holidays — no hardcoded
  i18n mapping for individual closures (those rotate via the seed, not
  via i18n.ts).

Tests cover the three Kind paths plus the no-shift case; UPC vacation
test injects the migration-010 seed into the cache so the assertion
runs without a live DB.

Out of scope (raised in conversation, deferred):
- Whether "UPC Summer Vacation" / "UPC Winter Vacation" are the right
  names for the seeded rows, and whether the winter block belongs in
  paliad.holidays at all (m flagged this as BS while reviewing the
  task — needs a data-side decision before renaming/removing).
- holidays.country isn't filtered by proceeding-type jurisdiction, so
  UPC vacation currently shifts EP_GRANT / EPA_APP / German national
  deadlines too. Bigger fix; flagged for a follow-up issue.
2026-05-04 18:31:55 +02:00
m
f79dbdba4a fix(t-paliad-120): /events Termin-Typ chip — dark-mode text invisible
Add dark-mode rules for `.termin-type-chip.termin-type-XYZ`. Without them
the chip inherited the bare `.termin-type-XYZ` swatch rule (line 8407+),
which intentionally paints text the same colour as the background for
the dot/border-left swatch use case → text invisible in the chip context.

Mirror the existing badge dark rules (lines 8413-8415) so chip and badge
share the same colour family. Adds the missing `consultation` variant for
the chip (badge consultation dark is also missing but out of scope).
2026-05-04 18:25:59 +02:00
m
ecfd62e330 Merge: t-paliad-118 — /admin/email-templates Verfügbare Variablen single-column stack 2026-05-04 18:13:12 +02:00
m
e9e445fddf fix(t-paliad-118): /admin/email-templates Verfügbare Variablen — single-column stack
The `.admin-et-variables-list` was a 4-track CSS grid designed for flat
name/type/desc/sample siblings. The renderer wraps each variable in a
`.admin-et-variable-row` div, so each variable became a single grid item —
and 4 rows packed into one visual row, producing the wide multi-column
overflow.

Switch the outer container to `flex-direction: column` (one variable per row)
and the row to a baseline-aligned wrapping flex (name, type pill, desc, sample
left-to-right). `margin-left: auto` pushes the sample right when there's room
and lets it wrap below on narrow widths.

Pre-existing dark-mode issue (`.admin-et-variable-name` hardcoded to #1c1917,
unreadable on dark background) is unchanged by this fix and predates the
layout regression — flagged separately, not in scope here.
2026-05-04 18:08:06 +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
4e1213fbd1 fix(t-paliad-116): event_deadlines i18n follow-up — title_de backfill + notes_en
Two adjacent i18n leaks in /tools/fristenrechner "Was kommt nach…" mode,
same pattern as t-paliad-112's deadline_rules fix but on event_deadlines:

A) title_de empty for all 70 rows. The DTO already falls back to title
   silently, so DE locale rendered English titles ("Statement of Defence",
   "Decision of the EPO", …). Backfilled via mig 035 with UPC RoP DE
   terminology that matches the trigger_events.name_de translations from
   mig 033, so the picker label and the deadline row read the same.

B) notes column carries English text on rows 50, 52, 70 (DE-named column
   was DE-only in spec, but seeds slipped EN strings through). Mig 036
   adds a parallel notes_en column following the t-112 mig 032 pattern,
   copies the existing English into notes_en, and replaces notes with
   proper DE for those three rows.

Render path:
- service: select notes_en, plumb through EventDeadlineResult.NotesEN
- frontend: getLang() === "en" ? (notesEN || notes) : notes (mirrors the
  proceeding-tree timeline branch already in fristenrechner.ts)
2026-05-04 17:03:58 +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
eb6e194684 Merge: t-paliad-115 PR-2 — canonical /events URL + redirect old paths 2026-05-04 14:40:57 +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
b88afe8ddd Merge: t-paliad-112 — i18n bug bundle (deadline_notes_en, trigger-event DE, Checkliste PROJECT) 2026-05-04 14:36:57 +02:00
m
341fa6c26f fix(t-paliad-112): i18n leaks — deadline_notes_en, trigger-event DE, Checkliste header
Three i18n bugs from the t-paliad-101 QA sweep, fixed together:

B2 — Fristenrechner deadline notes leaked German into the EN locale.
Migration 032 adds paliad.deadline_rules.deadline_notes_en (TEXT NULL)
and backfills English translations for all 30 rules that carry a
deadline_notes value (UPC RoP / EPC / ZPO terminology). The frontend
prefers _en when locale=EN and falls back to deadline_notes (DE) when
the column is NULL, so future seeds without an EN translation render
in DE rather than empty. UIDeadline DTO gains notesEN. The bulk
"Als Frist(en) speichern" CTA now stores the locale-matched note text
so EN users get an EN note alongside the EN title.

B8 — trigger-event picker labels were English-only when DE locale was
active (102 rows, name_de defaulted to '' in 028, frontend already had
the locale switch but no data). Migration 033 backfills name_de for
all 102 trigger events using standard German UPC RoP terminology
(Klageschrift, Klageerwiderung, Replik, Duplik, Nichtigkeitswiderklage,
Verletzungswiderklage, Berufungsschrift/-begründung, Anschlussberufung,
Schutzschrift, Beweissicherung, etc.).

S3 — frontend/src/client/checklists-instance.ts:154 had a hardcoded
"Project" label in both branches of the locale ternary; the DE branch
now reads "Projekt", matching the surrounding meta-item labels' pattern
(Court / Authority → Gericht / Behörde, Reference → Rechtsgrundlage).
2026-05-04 14:36:50 +02:00
m
45979caf81 Merge: t-paliad-115 PR-1 — events view ⊥ filter + unified calendar view 2026-05-04 14:34:52 +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
2c346672bb fix(t-paliad-113): bug-bundle polish — save modal court-set + project prefix, negative Streitwert, EPO en label, ISO en dates
5 small polish bugs from the t-paliad-101 QA sweep, bundled as one PR.

B4 — Fristenrechner save modal pre-checked court-determined entries:
treat dl.party === "court" as court-determined alongside dl.isCourtSet
so Zwischenverfahren / Mündliche Verhandlung / Entscheidung pre-uncheck
+ disable in the modal. Their meta now reads "vom Gericht bestimmt"
(deadlines.court.set) instead of the urgency placeholder.

B5 — Save modal project dropdown empty-code prefix: render "(reference)
— title" only when reference is non-empty; bare title otherwise. No
more leading "— Title" rows.

B7 — Negative Streitwert in Kostenrechner: blur handler now snaps
negative entries by clearing the input + resetting the slider to its
min and re-running calc, so a "-50000" entry no longer leaves stale
positive results onscreen. Input handler also drops the slider to min
on negatives so the visual state stays in sync mid-typing.

S2 — Courts EN page filter chip "EPA" → "EPO": added
gerichte.filter.epa i18n key (DE: EPA, EN: EPO) and wired it on the
courts.tsx pill.

S4 — Fristenrechner EN date format: switch from en-GB (dd/mm/yyyy,
ambiguous for US readers) to ISO ("Tue, 2026-09-01"). DE format
unchanged.
2026-05-04 14:31:43 +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
57237a55a3 feat(t-paliad-110): refactor Dashboard rails — drop Erledigt card, add Später + Termine rail
PR-4 of the Fristen+Termine unification, closing out t-paliad-110.

Fristen rail (was 5 cards):
- Erledigt card removed (status=completed stays reachable via the EventsPage
  filter dropdown — no card on the rail per the new model)
- Später card added (pending deadlines past Mon-week-after, click filters
  to /deadlines?status=later)
- 4+1 final shape: Überfällig (conditional alarm) · Heute · Diese Woche ·
  Nächste Woche · Später

Termine rail (new): 3 cards — Heute · Diese Woche · Später. No Überfällig
(past appointments aren't urgent), no Nächste Woche (low-value distinction
for appointments per the design rationale). Cards click through to
/appointments?status=… so users land in the matching EventsPage view.

Backend (DashboardService.loadSummary):
- DeadlineSummary.CompletedThisWeek dropped, .Later added
- AppointmentSummary added (Today / ThisWeek / Later)
- One CTE-based query computes both rails alongside MatterSummary; bucket
  cutoffs share computeDeadlineBucketBounds with /api/events/summary +
  /api/deadlines/summary so all three surfaces stay in lockstep

Frontend:
- dashboard.tsx: Erledigt card removed, Später card + Termine section added
- client/dashboard.ts: types updated, renderAppointmentSummary added
- 4 new i18n keys (DE+EN): dashboard.summary.later +
  dashboard.appointment_summary.heading
- CSS: .dashboard-card-later (muted blue) + 3 .dashboard-card-appt-* rules
  reusing the existing --bucket-* tokens

go build/vet/test ./... clean. bun run build clean (1396 keys).
2026-05-04 13:52:49 +02:00
m
50ac065c7d feat(t-paliad-110): mount unified EventsPage on /deadlines + /appointments
PR-3 of the Fristen+Termine unification. Both routes now serve the shared
shell built by renderEvents() — the per-type pages (deadlines.tsx /
appointments.tsx and their client bundles) are deleted entirely.

Hydration is baked at build time, not at request time: build.ts emits
events-deadlines.html and events-appointments.html, each carrying an
inline `window.__PALIAD_EVENTS__={"defaultType":"…"}` script in <head>.
The Go handlers ServeFile the matching artefact, no placeholder swap
needed (cleaner than the dashboard pattern for a single static flag).

Sidebar entries unchanged — "Fristen" still points at /deadlines,
"Termine" at /appointments. Both highlight correctly because each
artefact passes the matching currentPath into <Sidebar />.

Detail / new / calendar pages stay type-specific (out of scope per
task brief). Old endpoints /api/deadlines + /api/appointments remain
live for the calendars, project-detail panes, and CalDAV consumers.

Net: -981 lines (drops the duplicated chrome of the two old pages
in favour of one shared shell).

go build/vet/test ./... clean. bun run build clean.
2026-05-04 13:48:53 +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
m
db09f6e7d5 feat(t-paliad-108): bucket card palette — Heute orange, Diese Woche yellow
5-bucket urgency cards (Dashboard "Fristen auf einen Blick" + /deadlines
summary) now share a single source of truth via new --bucket-* tokens:

  Überfällig red → Heute orange → Diese Woche yellow → Nächste Woche green → Erledigt grey

Light values pass WCAG AA-large (3:1+) on the white surface for the
colored count (1.6–2.25rem, 600–700 weight = large text):
  red #ef4444 · orange #c2410c · yellow #a16207 · green #16a34a · grey #6b7280
Dark mode lifts to bright pastels for readability on midnight:
  red #fca5a5 · orange #fb923c · yellow #fbbf24 · green #4ade80 · grey #9ca3af

Both surfaces (.dashboard-card-* and .frist-card-*) now wire through the
same tokens, so the visual hierarchy reads identically. The Überfällig
alarm pulse (saturated red bg + white text, t-paliad-105/106) remains
unchanged. The dashboard-card-green border previously used --color-accent
(brand lime); it now uses --bucket-next-week to match the Fristen page.
2026-05-04 13:02:12 +02:00
m
de03f3ddcb feat(t-paliad-107): add Alle anzeigen browse-all modal to event-type picker
The picker on /deadlines/new and /deadlines/{id} edit was search-only —
users had to know what to type. Add a third affordance: a "Alle anzeigen"
button next to search and "+ Neuer Typ" that opens a modal listing every
available event type grouped by category, with sticky search and
multi-select checkboxes pre-populated from the picker's current
selection. Apply replaces; Cancel discards.

Modal a11y: focus-trap (Tab cycles in modal), Esc to close, click-outside
to dismiss. Each row shows the localized label, optional jurisdiction
badge, and a checkbox. Empty-search state renders a muted message.

Side effect: added a base `.modal` rule giving every `<div class="modal">`
a proper card surface (background, padding, radius, shadow). The existing
.event-type-add-modal previously had only width — the latent gap meant
its form fields rendered floating over the dim overlay. Now both modals
share the scaffold.

Files:
- frontend/src/client/event-types.ts — openBrowseEventTypesModal +
  browse button on the picker.
- frontend/src/styles/global.css — .modal base + .event-type-browse-*
  + flex-wrap on .event-type-search-row to accommodate three children.
- frontend/src/client/i18n.ts — DE/EN keys: event_types.picker.browse_all,
  event_types.browse.{title,search,empty,apply,cancel,selected_count,
  jurisdiction.none}.

Out of scope per brief: /deadlines + /agenda multi-select filter (already
has its own browse-style listbox-panel), admin moderation panel,
appointment forms (don't carry event types).
2026-05-04 12:06:00 +02:00
m
37a925d3b2 feat(t-paliad-106): harmonize deadline summary — 5 disjoint buckets across Dashboard + Fristen
Both surfaces now show the same buckets with the same labels and the same
cutoffs: Überfällig (conditional, alarming) · Heute · Diese Woche ·
Nächste Woche · Erledigt. Single-source bucket math via
computeDeadlineBucketBounds — Heute = today, Diese Woche = tomorrow
through the upcoming Sunday inclusive, Nächste Woche = next Monday
through next Sunday inclusive, all disjoint. Items past next Sunday
are visible only via "All open"/"Upcoming" filters; the Überfällig
card stays hidden when count == 0 and switches to a saturated red
pulse + bold white text when count > 0.

Filter dropdown on /deadlines gains today / next_week entries; old
"upcoming" filter still works as a back-compat alias for everything
pending past this Sunday so legacy bookmarks don't 4xx.

Tests: 8 deterministic table cases for the bucket pivots (every
weekday + a 21-day disjointness walk).
2026-05-04 12:03:56 +02:00
m
16eb73bf44 Merge: t-paliad-105 — Überfällig hide-when-zero / alarm-when-nonzero 2026-05-04 11:52:45 +02:00
m
2bbbe562d7 feat(t-paliad-105): hide Überfällig card at zero, alarm at >0
Überfällig is an emergency category — never a normal-state tile. Replace the
prior `.dashboard-card-quiet` dim-but-visible behavior with two states:

- overdue === 0 → card removed from the layout (`*-overdue-hidden`).
  The Dashboard summary grid and the Fristen summary cards now use
  `repeat(auto-fit, minmax(180px, 1fr))` so the row re-flows to 3 cards
  instead of leaving an empty 4th column.
- overdue > 0 → saturated red surface, white text, soft pulsing red ring
  via `paliad-alarm-pulse`. Honors `prefers-reduced-motion`. Dark theme
  uses a slightly lighter red so the alarm still pops on midnight.

Applied on both surfaces (`#dashboard-card-overdue` and the Fristen
`.frist-summary-card[data-status="overdue"]`).
2026-05-04 11:52:35 +02:00
m
e6369bc4c2 feat(t-paliad-104): harmonize light-theme sidebar with cream body
Reverses the t-paliad-083 carve-out that kept the sidebar on midnight in
both themes. m's feedback: in light mode the dark midnight column read as
a separate panel bolted onto the cream body. The brand lime stripe on the
active item is preserved (decoupled from --sidebar-text-active onto
--color-accent) so the active cue stays anchored across themes.

Light mode: --sidebar-bg = --color-bg-subtle (one step up from body
cream), --sidebar-text/-muted track the body palette, --sidebar-text-active
= midnight (full contrast against muted-grey inactive items),
hover/input/scrollbar tints switch to midnight-channel alphas.

Dark mode: original midnight + cream + lime palette restored inside the
:root[data-theme="dark"] override block. No regression there.
2026-05-04 11:51:40 +02:00
m
73d108d878 fix(t-paliad-103): replace Verlauf ::before block-link with row-click handler
The t-102 ::before overlay (`inset: 0` on `.entity-event-link`) made the whole
card clickable but also captured pointer events on the title and description
text — users couldn't select-to-copy. Same trap noted in brunel's t-102
debrief: when text-selection matters, switch to a row-level click handler that
skips inner <a>/<button>, matching the .entity-table pattern from t-098/099.

CSS (global.css):
  - drop `.entity-event-link::before` overlay
  - drop `position: relative` from `.entity-event` and `position: static` from
    `.entity-event-link` (no longer anchoring an absolute pseudo-element)
  - keep cursor: pointer + hover-lift on `.entity-event:has(.entity-event-link)`
    so the affordance still telegraphs "clickable"
  - card hover-lift now keys off the card itself, not the link's :hover, so the
    lift triggers from anywhere on the card (matching the new click surface)
  - mirror `.dashboard-activity-item`: cursor + lime-tint hover row-highlight

TS:
  - projects-detail.ts:renderEvents: after innerHTML, attach a row-level click
    handler that reads `.entity-event-link.href` and skips clicks on inner
    <a>/<button>. Cards without a link have no `.entity-event-link` and stay
    non-clickable (cursor stays default via the `:has()` selector).
  - dashboard.ts:renderActivity: same handler reading `.dashboard-activity-project.href`.

Acceptance:
  - Title and description text on /projects/{id} → Verlauf cards and on
    /dashboard activity rows is selectable again.
  - Click anywhere on a card still navigates (no regression from t-102).
  - Title link still navigates; Cmd-/Ctrl-click opens in new tab; keyboard
    tabbing still hits the inner link.
  - `cd frontend && bun run build` clean; `go build/vet/test ./...` clean.
2026-05-04 10:34:45 +02:00
m
95a6df5b49 feat(t-paliad-102): link Verlauf entries to deadlines/appointments/notes
Extends the t-paliad-097 metadata pattern from checklist_* events to the
remaining audit families. Project Verlauf and Dashboard activity feed now
deep-link each event to its originating entity:

  - deadline_{created,updated,completed,reopened} → /deadlines/{id}
  - appointment_{created,updated} → /appointments/{id}
  - note_created → /appointments/{id} | /deadlines/{id} | /projects/{id}
    (most-specific parent — notes have no standalone page)

Backend (Go):
  - deadline_service.go / appointment_service.go: switch single-entity
    mutation events from insertProjectEvent to insertProjectEventWithMeta
    carrying {"deadline_id"|"appointment_id": uuid}.
  - note_service.go:insertWithAudit: derive metadata from noteParent so
    the audit row records {note_id, deadline_id|appointment_id|project_id}.

Frontend (TS):
  - projects-detail.ts: extract eventDetailHref(); wrapEventTitleLink
    delegates to it. Comment block lists every wired event family.
  - dashboard.ts:activityHref: same routing rules as the project Verlauf.
  - global.css: .entity-event becomes position:relative; the
    .entity-event-link::before pseudo expands the link's hit area to the
    full card so a click anywhere on the row navigates (matches what m
    expected from "die Karte ist verlinkt"). Hover lifts border + shadow.

Excluded by design (mirrors checklist_deleted exclusion):
  - *_deleted events — entity is gone.
  - deadlines_imported — bulk event with no single deadline_id; would
    need an aggregate target the product doesn't have today.

Pre-metadata rows stay non-clickable (no backfill — same precedent as
t-paliad-097).
2026-05-03 18:39:06 +02:00
m
2d46a86c50 docs(t-paliad-099): document .entity-table row-click contract
Anchor the convention surfaced by t-paliad-098/099 so the next
hand on .entity-table sees it before adding a new table:

- frontend/src/styles/global.css: contract comment block above the
  default cursor:pointer rule explaining the navigate-or-readonly choice
- .claude/CLAUDE.md: new "Frontend conventions" section pointing at
  the CSS and the row-handler pattern in client/checklists.ts +
  client/projects-detail.ts

No code changes; pure docs.
2026-05-02 11:47:57 +02:00
m
19e4bfacfb fix(t-paliad-099): cursor only invites click where rows actually navigate
Audit follow-up to t-paliad-098. The global `.entity-table tbody tr`
rule set `cursor: pointer` and a hover background on every row, but six
tables across the admin and settings surfaces don't navigate on row
click — actions live in inline buttons (admin-team, admin-event-types,
admin-partner-units) or the rows are pure read-only summaries (admin
audit log, CalDAV sync log). The cursor lied and the hover invite was
empty.

- Add `.entity-table--readonly` modifier in global.css that resets
  cursor and neutralises the hover background, including a dark-theme
  override since the existing `:root[data-theme="dark"] .entity-table
  tbody tr:hover` rule outranks the base modifier on specificity.
- Apply the modifier to the six table instances that don't navigate.

The eight tables that DO navigate (projects, deadlines, appointments,
checklists templates+instances, project-detail's deadlines/appointments
/checklists) already have row click handlers and keep the default
clickable affordance.
2026-05-02 11:17:00 +02:00
m
215d4a465b fix(t-paliad-098): row-level click navigates to checklist instance
The .entity-table tbody tr CSS rule sets cursor: pointer on every row,
but the three checklist-instance tables only wired navigation to the
name cell's <a>. Clicks on Vorlage / Fortschritt / Angelegt cells looked
clickable yet did nothing.

Added row-level click handlers (skipping inner <a>/button so the
project-link cell and delete button still work) in:
- checklists.ts (Vorhandene Instanzen tab)
- projects-detail.ts (Checklisten tab on project detail)
- checklists-detail.ts (instance list on template detail)

Search-result anchors (handlers/search.go) already worked since they
are real <a> elements, no row handler needed.
2026-05-01 10:55:30 +02:00
m
df321acb63 feat(t-paliad-097): clickable checklist references + Vorhandene Instanzen tab
Two related checklist UX gaps:

1. Checklist events in a project's Verlauf tab were unclickable — and
   nothing in the project_events row carried the originating instance ID.
   Add an `insertProjectEventWithMeta` helper, write
   {"checklist_instance_id": <uuid>} as project_events.metadata for
   checklist_created / _renamed / _linked / _unlinked / _reset (skipped
   for _deleted — instance is gone). Surface metadata on
   /api/projects/{id}/events and on dashboard recent_activity. The
   Verlauf renderer wraps the title in <a href="/checklists/instances/{id}">
   when metadata.checklist_instance_id is present, and the dashboard's
   activity feed deep-links the project ref to the instance directly for
   checklist_* events. Existing rows (metadata `{}`) stay non-clickable —
   no migration backfill needed.

2. /checklists previously demanded a template pick before any existing
   instance was reachable. Add a tab nav (Vorlagen / Vorhandene Instanzen)
   using the existing entity-tab pattern. New endpoint
   GET /api/checklist-instances and ChecklistInstanceService.ListAllVisible
   return every visible instance across templates + projects, joined with
   project ref/title and sorted by created_at DESC. Rows show template,
   instance name (linked), project link (or "Persönlich"), progress bar,
   and created date. URL state (?tab=instances) keeps the active tab
   shareable. EN + DE i18n covered for tab labels and column headers.

Also adds event.title.checklist_* localizations for the Verlauf header
that translateEvent looks up.
2026-05-01 09:48:25 +02:00
m
fd6517d53a fix(t-paliad-096): checkbox row-alignment in form-field contexts
`.form-field input` set `width: 100%` and `padding: 0.55rem 0.75rem` on
all inputs — that includes checkboxes and radios. The visual checkbox
visibly stretched to fill the form column on /settings (Benachrichtigungen
tab) and inside the Fristenrechner "Fristen übernehmen" save modal,
pushing the label text out of place.

Add a targeted override that restores natural sizing for type=checkbox
and type=radio inside `.form-field`. Also bump `.caldav-toggle-label`
specificity (selector → `label.caldav-toggle-label`) so its
`inline-flex; align-items: center; gap: 0.5rem` actually wins over the
generic `.form-field label { display: block }` rule — without that the
checkbox + label kiss with no gap.

Surfaces verified via Playwright on paliad.de:
- /settings?tab=benachrichtigungen — Frist-Erinnerungen master + 3 sub-toggles
- /tools/fristenrechner — "Fristen übernehmen" save modal rows
2026-04-30 21:45:09 +02:00
m
d14c8111eb Merge: t-paliad-093 — CSS class rename .akten-* → English equivalents (F-7 of t-074) 2026-04-30 16:52:25 +02:00
m
c6872f94b0 refactor(t-paliad-093): rename .akten-* CSS classes to English equivalents
F-7 of the t-paliad-074 architecture audit. Sweeps the last German-named
CSS leftovers — purely a class-name change, no behaviour or styling
delta. 466 references across global.css and ~30 TSX/TS files.

Naming rules applied:
- Generic table/tabs/form/empty/controls/detail/events/status/type/
  suggestion/chip/col/ref/search-wrap/select/soon/loading/muted/
  unavailable/row/header-row/title-input -> .entity-*
- Truly generic widgets dropped the prefix: .multi-* (multi-select
  panel), .filter-*, .collab-* (collaborator picker; bare class is
  now .collab-picker), .firmwide-*, .office-*, .back-link
- Project-specific names kept specific: .party-form/-controls/-table
  (parties on a project), .checklists-hint, .netdocs-link
- Page-scoped IDs in projects.tsx -> projects-search/-count/-body;
  projects-new.tsx -> project-new-form/-msg
- German content "akten-bezogen" tightened to "aktenbezogen" (one-word
  form is also valid German) so the strict grep stays clean
2026-04-30 16:52:10 +02:00
m
460736ad1e refactor(t-paliad-092): rename Go module path patholo → paliad
F-6 from t-paliad-074 architecture audit. The Gitea repo was renamed
m/patholo → mAi/paliad → m/paliad, but go.mod still declared
`mgit.msbls.de/m/patholo` and every internal import echoed the
pre-rebrand name.

Sweep:
- go.mod: module path → mgit.msbls.de/m/paliad
- All *.go files: imports rewritten via sed
- README.md, docs/design-kanzlai-integration.md: mAi/paliad → m/paliad
- Frontend issue-reference comments (mAi/paliad#N → m/paliad#N) in
  i18n.ts, theme.ts, sidebar.ts, app.ts, Sidebar.tsx, PWAHead.tsx,
  global.css

Verified: go build/vet/test ./... clean, bun run build clean,
no remaining mgit.msbls.de/m/patholo or mAi/paliad references
outside docs that intentionally describe the rename history.
2026-04-30 16:46:31 +02:00
m
bbd46f658b Merge: t-paliad-089 — Admin Event-Type moderation panel (bulk archive, merge, promote, restore) 2026-04-30 16:43:09 +02:00
m
90b2f935c2 feat(t-paliad-094): Tier 2 Fristenrechner rule ports — damages, cost-appeals, cross-appeal, lay-open, leave/discretion
Per audit recommendations §6.2 (rec 5-9). Ports 14 RoP rules from
youpc's deadline calc into paliad.deadline_rules so they're surfaceable
in the timeline (course-of-proceedings) Fristenrechner mode in addition
to the existing trigger-event mode.

Adds 4 new proceeding types and extends 2 existing trees:

- UPC_DAMAGES (new) — Schadensbemessung (R.137.2, R.139 reply, R.139 rejoin)
- UPC_DISCOVERY (new) — Bucheinsicht (R.142.2, R.142.3 reply, R.142.3 rejoin)
- UPC_COST_APPEAL (new) — Berufung Kostenentscheidung (R.221.1)
- UPC_APP_ORDERS (new) — 15-day order-flavor (R.220.2, R.220.3, R.237 b, R.238.2)
- UPC_INF extended — R.151 chained off inf.decision
- UPC_APP extended — decision-flavor cross-appeal pair (R.237 a, R.238.1)

Total: 14 RoP rules across 5 families. New types appear in the
proceeding-type picker via deadlines.upc_damages / upc_discovery /
upc_cost_appeal / upc_app_orders i18n keys (DE + EN).

Design notes in the migration explain why some rules live in their own
proceeding type (when the legal anchor differs from UPC_INF/UPC_APP's
trigger date) vs being chained off existing rules.
2026-04-30 16:41:38 +02:00
m
fca7143244 feat(t-paliad-089): admin Event-Type moderation panel
Q6 of t-paliad-088 left firm-wide event_type creation open to any user; this
ships the moderation surface admins use to dedupe and clean up the resulting
drift.

Service layer (internal/services/event_type_service.go):
- ListAllForAdmin(filter) — firm-wide rows with usage_count and
  author_display_name, optionally including archived (single query, scalar
  subquery + LEFT JOIN paliad.users). Sorted live-first, then category +
  label_de.
- ListPrivatePendingPromotion — every private non-archived row across all
  users, sorted by usage_count DESC.
- ArchiveBulk(ids) — UPDATE archived_at=now() WHERE is_firm_wide AND NULL.
- Promote(id) — flip is_firm_wide=true; surfaces ErrEventTypeSlugTaken on
  collision so the admin can merge instead.
- Restore(id) — flip archived_at back to NULL; same slug-collision surface.
- MergeIDs(winner, losers) — tx-scoped INSERT … SELECT … ON CONFLICT
  redirect of deadline_event_types from losers → winner, then DELETE on the
  loser junction rows, then archive the losers. Refuses if the winner is
  archived or private. Junction PK does the dedup.
- requireAdmin gate runs at every method (defence-in-depth on top of the
  handler-level RequireAdminFunc).

Handlers (internal/handlers/admin_event_types.go):
- GET /api/admin/event-types[?include_archived=1]
- GET /api/admin/event-types/private
- POST /api/admin/event-types/archive { ids:[…] }
- POST /api/admin/event-types/merge { winner_id, loser_ids:[…] }
- POST /api/admin/event-types/{id}/promote
- POST /api/admin/event-types/{id}/restore
- GET /admin/event-types page shell.
All wrapped behind auth.RequireAdminFunc at registration time.

Frontend:
- New /admin/event-types SPA (admin-event-types.tsx + client/admin-event-types.ts):
  search, "Archivierte anzeigen" toggle, per-row archive/restore, bulk
  archive, merge modal (winner picker defaults to highest-usage row),
  separate table for private types pending promotion.
- Sidebar entry under Verwaltung; admin landing card.
- ~50 i18n keys DE+EN under admin.event_types.* + nav.admin.event_types.
- CSS for archived badge, merge option list, bulk-actions bar.

Out of scope (deferred): public "merge request" workflow for non-admins.
2026-04-30 16:37:48 +02:00
m
8ddc8277d0 feat(t-paliad-088): Event Types frontend — picker, multi-select filter, add modal (PR-2)
Shared client module client/event-types.ts exposes three surfaces:

1. attachEventTypePicker — multi-tag chip cluster with typeahead suggest
   and an inline "+ Neuen Typ hinzufügen…" affordance. Mounted on
   /deadlines/new and the /deadlines/{id} edit modal.
2. attachEventTypeMultiSelectFilter — listbox-panel filter (search + Alle
   + Ohne Typ + grouped checkbox list, click-outside / Escape dismiss).
   Mounted on /deadlines and /agenda. Trigger styled like the existing
   <select>s; serialises to ?event_type=<uuid>,<uuid>,none.
3. openAddEventTypeModal — modal with label_de/label_en/category/
   jurisdiction/firm_wide. Live duplicate-warning fed by
   /api/event-types/suggest (Q6 mitigation). Firm-wide checkbox is
   only rendered for global_admin (per the design's permission model).

Added Typ column on /deadlines (hidden when no visible row carries an
event_type — matches the t-paliad-073 hide-on-uniform pattern).
Added Typ display + edit on /deadlines/{id}; PATCH now sends
event_type_ids when the picker is mounted.

i18n: 36 new keys (DE+EN) under event_types.* + deadlines.field/col/
filter.event_type + agenda.filter.event_type + common.cancel.

CSS in global.css: .event-type-picker / .event-type-chip /
.akten-multi-trigger / .akten-multi-panel / .akten-event-type-pill /
.event-type-add-modal. Mobile (<640px) collapses the panel into a
bottom sheet.

bun run build clean (1302 i18n keys regenerated, data-i18n scan clean).
go build / go vet / go test ./... clean (PR-1 still green after rebase).
2026-04-30 12:57:53 +02:00
m
f721d7eccd fix(t-paliad-087): also fix --color-bg-muted in admin email templates
The first PR caught all `var(--color-bg-muted, #fallback)` sites. This catches
the 5 remaining `var(--color-bg-muted)` sites *without* fallback in the admin
email templates page (.admin-et-card-key, .admin-et-card-lang-btn:hover,
.admin-et-variable-type, .admin-et-preview-subject, .admin-et-version-row:hover).

Without fallback, an undefined custom property resolves to `unset` →
`transparent`, so these elements rendered with no visible background in
either mode (rather than light-grey in light mode like the fallback-form
variant). Same root cause though: the `--color-bg-muted` token name was
never defined anywhere.

All 10 sites (5 with hex fallback + 5 without) now use `--color-surface-muted`.

Build clean.
2026-04-30 12:08:00 +02:00
m
721560074b fix(t-paliad-087): theme-aware count pills + sweep of hardcoded light-grey BGs
`/team` count pills (member counts per office / per partner unit) rendered with
`var(--color-bg-muted, #f4f4f7)` — but `--color-bg-muted` was never defined, so
the literal `#f4f4f7` always won and the pills stayed light-grey in dark mode.

Sweep across `frontend/src/styles/global.css`:

- 5x `--color-bg-muted, #f3f4f6|#f4f4f7` → `--color-surface-muted` (the actual
  themed chip-bg token; light: #f3f4f6, dark: 5% cream over midnight).
  Sites: `.team-group-count`, `.team-dept-tag`, `.admin-soon-badge`,
  `.admin-audit-event`, `.admin-audit-source`.

- Trigger-event Fristenrechner block (added in PR-2 / t-paliad-086) used a
  parallel set of fictional tokens (`--surface-color`, `--surface-soft`,
  `--accent-soft`, `--text-color`, `--text-muted`, `--border-color`,
  `--accent`, `--accent-text`, `--border-light`) that were never defined,
  so the entire panel rendered in fallback hex literals — white card,
  light-grey duration chip, pale-lime rule-code, dark `#111` body text.
  In dark mode the bg/border/divider stayed light while text stayed dark,
  on a midnight body — unreadable.
  Re-pointed all 27 sites onto the project's `--color-*` token system.

- `rgba(0, 0, 0, 0.0X)` literal overlays (hover/active states for
  `.search-result`, `.palette-action`, `.quick-add-row/-cancel`,
  `.pwa-install-dismiss`, `.termin-type-chip/-badge`, `.termin-personal-tag`,
  `.caldav-status-card`) → `--color-overlay-faint|subtle` (the existing
  tokens that flip to white-channel alpha in dark mode).

- Removed redundant hex fallbacks on already-themed tokens
  (`var(--color-surface, #ffffff)` → `var(--color-surface)` etc) — the
  `:root[data-theme="dark"]` block already defines all of them.

Acceptance:

- `cd frontend && bun run build` → clean.
- Sweep-greps from the task brief now return 0 hits (excl. one comment).
- No new tokens introduced — reuses the t-paliad-083 / t-paliad-082 palette.

Refs t-paliad-087.
2026-04-30 12:01:51 +02:00
m
97ea393fe9 Merge: t-paliad-086 PR-3 — Tier 1 Fristenrechner bug fixes (CCR adaptive, UPC_APP grounds, EP_GRANT priority, rule-code normalisation, holiday-cap fix) 2026-04-30 11:11:53 +02:00
m
d00974424f fix(t-paliad-086): Tier 1 Fristenrechner bug fixes — PR-3
Implements the four audit recommendations from §6.1 of
docs/audit-fristenrechner-completeness-2026-04-30.md plus a holiday-
adjustment cap fix surfaced by PR-2's smoke test.

(1) UPC_INF CCR-conditional rejoinder
   Public Fristenrechner now flips inf.reply (RoP.029.b → RoP.029.a) and
   inf.rejoin (1mo / RoP.029.c → 2mo / RoP.029.d) when the user ticks
   "Mit Widerklage auf Nichtigkeit." Implemented via a new
   `condition_flag` column on paliad.deadline_rules: when the rule names
   a flag and the API request's flags array contains it, the calculator
   substitutes alt_duration_value/unit and alt_rule_code. Independent of
   the existing `condition_rule_id` mechanism (which references a real
   rule in the same proceeding tree — only useful for matter-attached
   trees that already seed the CCR rule).

(2) UPC_APP / internal APP grounds anchoring
   `app.grounds` is now anchored on the trigger date (the appealed
   decision) with a 4-month duration, not chained 2mo after `app.notice`.
   Per RoP 220.1 the legal rule is "4 months from notification of the
   decision," independent of when the notice itself was filed. The chain
   only happened to give the right answer when both legs landed on a
   working day; under holiday rollover (e.g. notice deadline pushed to
   Monday) the grounds deadline drifted off the 4mo legal target.

(3) EP_GRANT publish anchor on priority date
   New `anchor_alt` column on paliad.deadline_rules. ep_grant.publish
   carries `anchor_alt='priority_date'`. The Fristenrechner UI surfaces
   an optional "Prioritätstag" input (visible only when EP_GRANT is
   selected) that, when populated, anchors the publish-A1 calculation on
   the priority date instead of the filing. Falls back to filing date
   when the priority field is empty (the case for purely-EP applications
   with no foreign priority claim).

(4) Rule-code format normalisation
   Migration 029 normalises 'RoP 23' → 'RoP.023', 'RoP 29b' / 'RoP.029b'
   → 'RoP.029.b', 'RoP 220.1' → 'RoP.220.1', etc. across deadline_rules.
   Matches the canonical youpc format already used by the PR-1 imported
   event-deadline rule codes.

(+) AdjustForNonWorkingDays cap bumped 30 → 60
   Surfaced by the PR-2 smoke test: SoD on 2026-04-30 (3mo from trigger)
   landed on Sat 2026-08-29 instead of Mon 2026-08-31. The 30-iteration
   safety bound on AdjustForNonWorkingDays cannot walk past the 33-day
   UPC summer vacation plus flanking weekends. Bumped to 60. Pure-Go
   one-liner, locked by a follow-up production smoke (real
   paliad.holidays seed has the UPC vacation).

Schema (migration 029): two new nullable text columns on
paliad.deadline_rules — `condition_flag` and `anchor_alt`. Both ignored
by every existing rule; only the rows updated above carry values.

Models: DeadlineRule gains ConditionFlag + AnchorAlt (nilable strings).

Service: FristenrechnerService.Calculate now takes a CalcOptions struct
(PriorityDateStr, Flags). API handler accepts optional priorityDate and
flags fields on POST /api/tools/fristenrechner.

Frontend: TSX surfaces the priority-date row + CCR checkbox conditionally
on selectedType (only EP_GRANT / UPC_INF respectively). Client TS reads
them and threads through the API call. New i18n keys for both DE+EN.

Migration 029 dry-run validated on prod Supabase (BEGIN/ROLLBACK):
schema + UPDATEs apply cleanly, rule states match expected post-fix
shape. Tests + go build/vet + bun build all clean.
2026-04-30 11:11:47 +02:00
m
29143e15fd Merge: t-paliad-086 PR-2 — trigger-event Fristenrechner mode + working_days primitive 2026-04-30 11:04:38 +02:00
m
d78f20be8a feat(t-paliad-086): trigger-event Fristenrechner mode + working_days primitive — PR-2
Adds the second Fristenrechner mode (mirrored from youpc.org's deadline
calc): pick a UPC trigger event + date, see all deadlines that flow
from it. Coexists with the existing course-of-proceedings timeline mode
via a tab toggle on /tools/fristenrechner.

Backend:
- internal/services/event_deadline_service.go — EventDeadlineService.
  ListTriggerEvents (alphabetical), Calculate (resolves all deadlines
  flowing from a trigger). Routes through HolidayService for weekend +
  holiday rollover. Honours the new working_days unit. Resolves
  composite rules (alt_* + combine_op) by computing both legs and
  picking max/min. Used by R.198/R.213 ("31d OR 20wd, whichever is
  longer") imported in PR-1.
- internal/services/event_deadline_service_test.go — covers
  addWorkingDays (forward, backward, zero, holiday-skip), composite
  rule semantics, before-timing.
- internal/handlers/fristenrechner.go — two new endpoints:
  GET /api/tools/trigger-events, POST /api/tools/event-deadlines.
- handlers.Services / dbServices: new EventDeadline / eventDeadline
  field; wired in cmd/server/main.go from the same HolidayService.

Frontend:
- frontend/src/fristenrechner.tsx — tab strip + second wizard panel
  (3 steps: trigger picker → date → flat result list).
- frontend/src/client/fristenrechner.ts — initEventMode wiring,
  typeahead filter over the 102 trigger events, Calculate flow,
  bilingual rendering, composite-rule labels, lang-change refresh.
- frontend/src/client/i18n.ts — 27 new keys (DE+EN) under
  deadlines.mode.* and deadlines.event.* (incl. units, timing).
- frontend/src/styles/global.css — fristen-mode-tabs, mode-panel,
  event-list, event-result-row visual style.

Working-day arithmetic detail: the new addWorkingDays helper steps
one day at a time and skips runs of non-working days (Sat/Sun + DE
federal + UPC vacations seeded via paliad.holidays). Day-zero is the
caller's job — addWorkingDays(0) returns the input unchanged so
callers can decide whether to roll forward via AdjustForNonWorkingDays.

Composite-rule resolution: when a row carries alt_duration_value +
alt_duration_unit + combine_op, Calculate computes both legs,
picks max/min, and surfaces a compositeNote like
"max(31 days, 20 working_days) → working_days leg" so the UI can
explain which leg won.

PR-3 will land Tier 1 bug fixes from the audit (CCR adaptive,
UPC_APP grounds anchoring, EP_GRANT priority, rule-code normalisation).
2026-04-30 11:04:32 +02:00