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.
267 lines
13 KiB
TypeScript
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 — 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ür Ihre Akten. Überfällig, heute, diese Woche, nächste Woche — 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">Überfä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ä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ä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ö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ügbar — 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ä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äge mit diesen Filtern.</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
<Footer />
|
|
<PaliadinWidget />
|
|
<script src="/assets/events.js"></script>
|
|
</body>
|
|
</html>
|
|
);
|
|
}
|