Files
paliad/frontend/src/events.tsx
mAi d0f732d0ec refactor(events): t-paliad-224 — fold Kalender tab into mountCalendar()
The /events Kalender view now mounts the canonical mountCalendar()
module from frontend/src/client/calendar/ — same renderer Custom
Views uses for shape=calendar. Drops the events-page-specific
month-grid + popup code path entirely.

What replaces what
- renderCalendar() / openCalPopup() / calDotClass / fmtMonthYear /
  isoDate / itemDateISO and the calYear/calMonth module state →
  one mountCalendar() handle (lazy, urlState=true).
- events-cal-prev / events-cal-next / events-cal-today buttons →
  toolbar in mountCalendar (includes its own 'Heute' button).
- modal popup on cell click → drill-down to day view (matches
  /views; head decision §11 Q2).
- @media min-height shrink on .frist-cal-cell → views-calendar-*
  responsive surface (CSS unchanged from /views).

Behavioural deltas vs pre-refactor
- /events Kalender now persists view+anchor in ?cal_view + ?cal_date
  (head decision §11 Q3) — refresh / share-link safe.
- Pills are kind-coded (deadline / appointment) rather than urgency-
  coded; matches /views (head decision §11 Q4 — drop subtype dot
  colouring, file as follow-up).
- Empty-month message gone; the per-day no-entries state from the
  day-view replaces it (head decision §11 Q8 — drop dead i18n).

Adapter: toCalendarItem() preserves the pre-refactor bucketing rule
— deadlines bucket on due_date, appointments on start_at, both fall
back to event_date.

events.tsx: 31-line calendar subtree (toolbar + grid + modal +
empty hint) reduces to a single host div. mountCalendar fills it
when the user picks Kalender.
2026-05-20 15:23:28 +02:00

267 lines
13 KiB
TypeScript

import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar";
import { PaliadinWidget } from "./components/PaliadinWidget";
import { BottomNav } from "./components/BottomNav";
import { Footer } from "./components/Footer";
import { PWAHead } from "./components/PWAHead";
// EventsPage is the shared shell that lives at /events (t-paliad-115).
// One HTML output: `currentPath` is "/events" so the sidebar Fristen /
// Termine entries (which point at /events?type=…) don't SSR-highlight on
// the bare URL — events.ts re-highlights at hydration based on the
// active type. The defaultType is "all" so a visit to bare /events
// shows the unified Beides view; visits via sidebar carry ?type=… in
// the URL and override.
export function renderEvents(): string {
const hydration = `window.__PALIAD_EVENTS__=${JSON.stringify({ defaultType: "all" })};`;
const currentPath = "/events";
return "<!DOCTYPE html>" + (
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#BFF355" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<PWAHead />
<title id="events-title" data-i18n="deadlines.list.title">Fristen &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
<script>{hydration}</script>
</head>
<body className="has-sidebar">
<Sidebar currentPath={currentPath} />
<BottomNav currentPath={currentPath} />
<main>
<section className="tool-page">
<div className="container">
<div className="tool-header">
<div className="entity-header-row">
<div>
<h1 id="events-heading" data-i18n="deadlines.list.heading">Fristen</h1>
<p
className="tool-subtitle"
id="events-subtitle"
data-i18n="deadlines.list.subtitle"
>
Persistente Fristen f&uuml;r Ihre Akten. &Uuml;berf&auml;llig, heute, diese Woche, n&auml;chste Woche &mdash; auf einen Blick.
</p>
</div>
<div className="fristen-header-actions" id="events-actions">
<a
href="/deadlines/new"
className="btn-primary btn-cta-lime"
id="events-action-new-deadline"
data-i18n="deadlines.list.new"
>
Neue Frist
</a>
<a
href="/appointments/new"
className="btn-primary btn-cta-lime"
id="events-action-new-appointment"
data-i18n="appointments.list.new"
>
Neuer Termin
</a>
</div>
</div>
</div>
<div className="events-axis-row">
<div
className="agenda-chip-row event-type-chip-row"
role="tablist"
id="events-type-chips"
aria-label="Typ"
>
<button
type="button"
className="agenda-chip"
data-event-type="deadline"
data-i18n="events.toggle.deadline"
role="tab"
>
Fristen
</button>
<button
type="button"
className="agenda-chip"
data-event-type="appointment"
data-i18n="events.toggle.appointment"
role="tab"
>
Termine
</button>
<button
type="button"
className="agenda-chip"
data-event-type="all"
data-i18n="events.toggle.all"
role="tab"
>
Beides
</button>
</div>
<div
className="events-view-selector"
role="tablist"
id="events-view-selector"
aria-label="Ansicht"
>
<button
type="button"
className="events-view-btn"
data-event-view="cards"
data-i18n="events.view.cards"
role="tab"
>
Karten
</button>
<button
type="button"
className="events-view-btn"
data-event-view="list"
data-i18n="events.view.list"
role="tab"
>
Liste
</button>
<button
type="button"
className="events-view-btn"
data-event-view="calendar"
data-i18n="events.view.calendar"
role="tab"
>
Kalender
</button>
</div>
</div>
<div className="frist-summary-cards" id="events-summary">
<button type="button" className="frist-summary-card frist-card-overdue" data-bucket="overdue">
<span className="frist-summary-dot" />
<span className="frist-summary-count" id="events-sum-overdue">0</span>
<span className="frist-summary-label" data-i18n="deadlines.summary.overdue">&Uuml;berf&auml;llig</span>
</button>
<button type="button" className="frist-summary-card frist-card-today" data-bucket="today">
<span className="frist-summary-dot" />
<span className="frist-summary-count" id="events-sum-today">0</span>
<span className="frist-summary-label" data-i18n="deadlines.summary.today">Heute</span>
</button>
<button type="button" className="frist-summary-card frist-card-week" data-bucket="this_week">
<span className="frist-summary-dot" />
<span className="frist-summary-count" id="events-sum-week">0</span>
<span className="frist-summary-label" data-i18n="deadlines.summary.thisweek">Diese Woche</span>
</button>
<button type="button" className="frist-summary-card frist-card-next-week" data-bucket="next_week">
<span className="frist-summary-dot" />
<span className="frist-summary-count" id="events-sum-next-week">0</span>
<span className="frist-summary-label" data-i18n="deadlines.summary.nextweek">N&auml;chste Woche</span>
</button>
<button type="button" className="frist-summary-card frist-card-later" data-bucket="later">
<span className="frist-summary-dot" />
<span className="frist-summary-count" id="events-sum-later">0</span>
<span className="frist-summary-label" data-i18n="deadlines.summary.later">Sp&auml;ter</span>
</button>
</div>
<div className="entity-controls">
<div className="filter-row">
<div className="filter-group">
<label className="filter-label" htmlFor="events-filter-status" data-i18n="deadlines.filter.status">Status</label>
<select id="events-filter-status" className="entity-select">
{/* Options are populated at hydration by populateStatusFilter */}
{/* in client/events.ts — the active set depends on the */}
{/* current Type chip (deadlines vs appointments). The static */}
{/* placeholder keeps the select non-empty before JS runs. */}
<option value="pending" data-i18n="deadlines.filter.pending">Alle offenen</option>
</select>
</div>
<div className="filter-group" id="events-filter-project-group">
<label className="filter-label" htmlFor="events-filter-project" data-i18n="deadlines.filter.akte">Projekt</label>
<select id="events-filter-project" className="entity-select">
<option value="" data-i18n="deadlines.filter.akte.all">Alle Projekte</option>
<option value="__personal__" data-i18n="appointments.filter.akte.personal">Nur pers&ouml;nliche</option>
</select>
</div>
<div className="filter-group" id="events-filter-event-type-group">
<label className="filter-label" htmlFor="events-filter-event-type" id="events-filter-event-type-label" data-i18n="deadlines.filter.event_type">Typ</label>
<div className="multi-anchor">
<button type="button" id="events-filter-event-type" className="entity-select multi-trigger" aria-haspopup="listbox" />
<div id="events-filter-event-type-panel" className="multi-panel" hidden />
</div>
</div>
<div className="filter-group" id="events-filter-appointment-type-group">
<label className="filter-label" htmlFor="events-filter-appointment-type" id="events-filter-appointment-type-label" data-i18n="appointments.filter.type">Typ</label>
<select id="events-filter-appointment-type" className="entity-select">
<option value="" data-i18n="appointments.filter.type.all">Alle Typen</option>
<option value="hearing" data-i18n="appointments.type.hearing">Verhandlung</option>
<option value="meeting" data-i18n="appointments.type.meeting">Besprechung</option>
<option value="consultation" data-i18n="appointments.type.consultation">Beratung</option>
<option value="deadline_hearing" data-i18n="appointments.type.deadline_hearing">Fristverhandlung</option>
</select>
</div>
</div>
</div>
<div id="events-unavailable" className="entity-unavailable" style="display:none">
<p data-i18n="events.unavailable">
Termin- und Fristenverwaltung zurzeit nicht verf&uuml;gbar &mdash; bitte Administrator kontaktieren.
</p>
</div>
<div className="entity-table-wrap" id="events-table-wrap">
<table className="entity-table fristen-table events-table" id="events-table">
<thead>
<tr>
<th />
<th className="events-col-row-type" />
<th data-i18n="events.col.date">Datum</th>
<th data-i18n="deadlines.col.title">Titel</th>
<th data-i18n="deadlines.col.akte">Projekt</th>
<th className="events-col-rule" data-i18n="deadlines.col.rule">Regel</th>
<th className="entity-col-event-type" data-i18n="deadlines.col.event_type">Typ</th>
<th className="events-col-location" data-i18n="events.col.location">Ort</th>
<th className="events-col-appointment-type" data-i18n="events.col.appointment_type">Termin-Typ</th>
<th className="entity-col-status" data-i18n="deadlines.col.status">Status</th>
</tr>
</thead>
<tbody id="events-body" />
</table>
</div>
{/* Calendar host — mountCalendar() (t-paliad-224) builds the
month/week/day grid + toolbar into this container when
the Kalender view chip is active. Empty until then. */}
<div id="events-calendar-wrap" className="events-calendar-wrap" hidden />
<div className="entity-empty" id="events-empty" style="display:none">
<h2 data-i18n="events.empty.title">Keine Eintr&auml;ge vorhanden</h2>
<p data-i18n="events.empty.hint">
Sobald Fristen oder Termine angelegt werden, erscheinen sie hier.
</p>
<a href="/deadlines/new" className="btn-primary btn-cta-lime" id="events-empty-cta-deadline" data-i18n="deadlines.list.new">Neue Frist</a>
<a href="/appointments/new" className="btn-primary btn-cta-lime" id="events-empty-cta-appointment" data-i18n="appointments.list.new">Neuer Termin</a>
</div>
<div className="entity-empty entity-empty-filtered" id="events-empty-filtered" style="display:none">
<p data-i18n="events.empty.filtered">Keine Eintr&auml;ge mit diesen Filtern.</p>
</div>
</div>
</section>
</main>
<Footer />
<PaliadinWidget />
<script src="/assets/events.js"></script>
</body>
</html>
);
}