m's 2026-05-21 14:20 report: dashboard "Diese Woche" card linked to /deadlines?status=this_week but the 301 to /events?type=deadline dropped the query string, landing on the default Pending filter instead of the This-Week bucket. Two-part fix: 1. handleDeadlinesListRedirect now appends r.URL.RawQuery to the target so any filter (status, project_id, event_type, …) survives the redirect. Regression test pins all three shapes (no query, single param, multi param). 2. Dashboard summary cards point at the canonical /events?type=deadline&status=… URL directly — saves the 301 bounce and matches the URL the events page itself reads on load. The five card values (overdue/today/this_week/next_week/later) are all in STATUS_OPTIONS_DEADLINE in frontend/src/client/events.ts, so the events page filter chip picks them up natively.
293 lines
17 KiB
TypeScript
293 lines
17 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";
|
|
|
|
// The three /* __PALIAD_DASHBOARD_*__ */ tokens below are replaced at
|
|
// request time by the Go handler (internal/handlers/dashboard_shell.go)
|
|
// with JSON blobs assigned to window.__PALIAD_DASHBOARD__,
|
|
// window.__PALIAD_DASHBOARD_LAYOUT__, and window.__PALIAD_DASHBOARD_CATALOG__.
|
|
// Keep each token intact and exactly once in the output. The latter two
|
|
// power the per-user configurable layout (t-paliad-219).
|
|
const HYDRATION_SCRIPT =
|
|
"/*__PALIAD_DASHBOARD_DATA__*//*__PALIAD_DASHBOARD_LAYOUT__*//*__PALIAD_DASHBOARD_CATALOG__*/";
|
|
|
|
// Chevron used as the collapsible-section disclosure indicator. CSS rotates
|
|
// it 90deg clockwise when the section is open via the
|
|
// .dashboard-section[aria-expanded="true"] selector — see global.css.
|
|
const ICON_CHEVRON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 6 15 12 9 18"/></svg>';
|
|
|
|
// Render a collapsible dashboard section. The toggle button is the entire
|
|
// header row so the heading text doubles as the affordance. State is
|
|
// hydrated client-side from localStorage by client/dashboard.ts; SSR
|
|
// renders all sections expanded so unstyled fallback is sensible.
|
|
function CollapsibleSection(props: {
|
|
id: string;
|
|
widgetKey: string;
|
|
headingI18n: string;
|
|
headingDe: string;
|
|
children: any;
|
|
}): string {
|
|
return (
|
|
<section className="dashboard-section" data-collapse-key={props.id} data-widget-key={props.widgetKey} aria-expanded="true">
|
|
<button type="button" className="dashboard-section-toggle" aria-expanded="true">
|
|
<h3 className="dashboard-section-heading" data-i18n={props.headingI18n}>{props.headingDe}</h3>
|
|
<span className="dashboard-section-chevron" aria-hidden="true"
|
|
dangerouslySetInnerHTML={{ __html: ICON_CHEVRON }} />
|
|
</button>
|
|
<div className="dashboard-section-body">
|
|
{props.children}
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
export function renderDashboard(): string {
|
|
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 data-i18n="dashboard.title">Dashboard — Paliad</title>
|
|
<link rel="stylesheet" href="/assets/global.css" />
|
|
<script dangerouslySetInnerHTML={{ __html: HYDRATION_SCRIPT }} />
|
|
</head>
|
|
<body className="has-sidebar">
|
|
<Sidebar currentPath="/dashboard" />
|
|
<BottomNav currentPath="/dashboard" />
|
|
|
|
<main>
|
|
<section className="tool-page">
|
|
<div className="container">
|
|
<div className="dashboard-header">
|
|
<div>
|
|
<h1 className="dashboard-greeting">
|
|
<span data-i18n="dashboard.greeting.prefix">Guten Tag</span>
|
|
<span className="dashboard-greeting-name" id="dashboard-greeting-name"></span>
|
|
</h1>
|
|
<p className="dashboard-subline">
|
|
<span className="dashboard-office-chip" id="dashboard-office-chip" style="display:none"></span>
|
|
<span className="dashboard-date" id="dashboard-date"></span>
|
|
</p>
|
|
</div>
|
|
{/* "Anpassen" toggle (t-paliad-219 Slice B). Off by
|
|
default — when on, body.dashboard-editing reveals
|
|
drag handles / ↑↓ / x / ⚙ chrome on each widget plus
|
|
the edit-footer below the widget stack. */}
|
|
<button
|
|
type="button"
|
|
id="dashboard-edit-toggle"
|
|
className="btn btn-ghost dashboard-edit-toggle"
|
|
aria-pressed="false"
|
|
data-i18n="dashboard.edit.toggle"
|
|
>Anpassen</button>
|
|
</div>
|
|
|
|
<div id="dashboard-unavailable" className="dashboard-unavailable" style="display:none">
|
|
<p data-i18n="dashboard.unavailable">
|
|
Dashboard benötigt die Datenbank — bitte Administrator kontaktieren.
|
|
</p>
|
|
</div>
|
|
|
|
<div id="dashboard-onboarding" className="dashboard-unavailable" style="display:none">
|
|
<p data-i18n="dashboard.onboarding">
|
|
Bitte schließen Sie das Onboarding ab, damit Ihnen Fristen und Akten angezeigt werden können.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Configurable widget grid (t-paliad-227 overhaul). All
|
|
widgets live as direct children of the single
|
|
.dashboard-grid container so applyLayout can place them
|
|
via grid-column/grid-row inline styles. Pre-overhaul
|
|
this stack had nested wrappers (.dashboard-columns,
|
|
standalone <section>s) that fought the layout engine
|
|
and made cross-row drags appear to fail. */}
|
|
<div className="dashboard-grid" id="dashboard-grid">
|
|
{/* Traffic-light deadline summary (4+1: Überfällig conditional + 4 universal — t-paliad-110) */}
|
|
<CollapsibleSection id="summary" widgetKey="deadline-summary" headingI18n="dashboard.summary.heading" headingDe="Fristen auf einen Blick">
|
|
<div className="dashboard-summary-grid">
|
|
<a href="/events?type=deadline&status=overdue" className="dashboard-card dashboard-card-red" id="dashboard-card-overdue">
|
|
<div className="dashboard-card-count" id="dashboard-count-overdue">0</div>
|
|
<div className="dashboard-card-label" data-i18n="dashboard.summary.overdue">Überfällig</div>
|
|
</a>
|
|
<a href="/events?type=deadline&status=today" className="dashboard-card dashboard-card-today" id="dashboard-card-today">
|
|
<div className="dashboard-card-count" id="dashboard-count-today">0</div>
|
|
<div className="dashboard-card-label" data-i18n="dashboard.summary.today">Heute</div>
|
|
</a>
|
|
<a href="/events?type=deadline&status=this_week" className="dashboard-card dashboard-card-amber" id="dashboard-card-thisweek">
|
|
<div className="dashboard-card-count" id="dashboard-count-this-week">0</div>
|
|
<div className="dashboard-card-label" data-i18n="dashboard.summary.this_week">Diese Woche</div>
|
|
</a>
|
|
<a href="/events?type=deadline&status=next_week" className="dashboard-card dashboard-card-green" id="dashboard-card-nextweek">
|
|
<div className="dashboard-card-count" id="dashboard-count-next-week">0</div>
|
|
<div className="dashboard-card-label" data-i18n="dashboard.summary.next_week">Nächste Woche</div>
|
|
</a>
|
|
<a href="/events?type=deadline&status=later" className="dashboard-card dashboard-card-later" id="dashboard-card-later">
|
|
<div className="dashboard-card-count" id="dashboard-count-later">0</div>
|
|
<div className="dashboard-card-label" data-i18n="dashboard.summary.later">Später</div>
|
|
</a>
|
|
</div>
|
|
</CollapsibleSection>
|
|
|
|
{/* Matter summary — uses CollapsibleSection now so it
|
|
participates in the grid like every other widget. The
|
|
inner card heading was redundant with the section
|
|
heading; we keep the stats grid + the projects link. */}
|
|
<CollapsibleSection id="matters" widgetKey="matter-summary" headingI18n="dashboard.matters.heading" headingDe="Meine Akten">
|
|
<a href="/projects" className="dashboard-matter-card">
|
|
<div className="dashboard-matter-stats">
|
|
<div>
|
|
<div className="dashboard-matter-num" id="dashboard-matter-active">0</div>
|
|
<div className="dashboard-matter-lbl" data-i18n="dashboard.matters.active">Aktiv</div>
|
|
</div>
|
|
<div>
|
|
<div className="dashboard-matter-num" id="dashboard-matter-archived">0</div>
|
|
<div className="dashboard-matter-lbl" data-i18n="dashboard.matters.archived">Archiviert</div>
|
|
</div>
|
|
<div>
|
|
<div className="dashboard-matter-num" id="dashboard-matter-total">0</div>
|
|
<div className="dashboard-matter-lbl" data-i18n="dashboard.matters.total">Gesamt</div>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
</CollapsibleSection>
|
|
|
|
<CollapsibleSection id="deadlines" widgetKey="upcoming-deadlines" headingI18n="dashboard.deadlines.heading" headingDe="Kommende Fristen">
|
|
<ul className="dashboard-list" id="dashboard-deadlines-list"></ul>
|
|
<div className="dashboard-calendar" id="dashboard-deadlines-calendar" style="display:none"></div>
|
|
<p className="dashboard-empty" id="dashboard-deadlines-empty" style="display:none" data-i18n="dashboard.deadlines.empty">
|
|
Keine Fristen in den nächsten 7 Tagen.
|
|
</p>
|
|
</CollapsibleSection>
|
|
|
|
<CollapsibleSection id="appointments" widgetKey="upcoming-appointments" headingI18n="dashboard.appointments.heading" headingDe="Kommende Termine">
|
|
<ul className="dashboard-list" id="dashboard-appointments-list"></ul>
|
|
<div className="dashboard-calendar" id="dashboard-appointments-calendar" style="display:none"></div>
|
|
<p className="dashboard-empty" id="dashboard-appointments-empty" style="display:none" data-i18n="dashboard.appointments.empty">
|
|
Keine Termine in den nächsten 7 Tagen.
|
|
</p>
|
|
</CollapsibleSection>
|
|
|
|
{/* Inline Agenda (t-paliad-162). Same item shape as the
|
|
standalone /agenda page, rendered via the shared
|
|
agenda-render module. The dashboard variant is read-only:
|
|
no chip filters, no URL state — a 30-day window of
|
|
upcoming items grouped by day. The standalone /agenda
|
|
route is unchanged for direct-link compatibility. */}
|
|
<CollapsibleSection id="agenda" widgetKey="inline-agenda" headingI18n="dashboard.agenda.heading" headingDe="Agenda">
|
|
<div className="dashboard-agenda">
|
|
<div className="agenda-timeline" id="dashboard-agenda-timeline" />
|
|
<ul className="dashboard-list" id="dashboard-agenda-list" style="display:none"></ul>
|
|
<p className="dashboard-empty" id="dashboard-agenda-empty" style="display:none" data-i18n="dashboard.agenda.empty">
|
|
Keine Fälligkeiten in den nächsten 30 Tagen.
|
|
</p>
|
|
<p className="dashboard-agenda-link">
|
|
<a href="/agenda" data-i18n="dashboard.agenda.full_link">Vollständige Agenda öffnen →</a>
|
|
</p>
|
|
</div>
|
|
</CollapsibleSection>
|
|
|
|
{/* Inbox-approvals widget (t-paliad-219 — new in v1). The
|
|
list mirrors /inbox's "Approver" axis but capped at the
|
|
widget's count setting. Renders the empty state when
|
|
the user has no open approvals to review. */}
|
|
<CollapsibleSection id="inbox-approvals" widgetKey="inbox-approvals" headingI18n="dashboard.inbox.heading" headingDe="Offene Freigaben">
|
|
<div className="dashboard-inbox">
|
|
<p className="dashboard-inbox-summary" id="dashboard-inbox-summary" style="display:none"></p>
|
|
<ul className="dashboard-list" id="dashboard-inbox-list"></ul>
|
|
<p className="dashboard-empty" id="dashboard-inbox-empty" style="display:none" data-i18n="dashboard.inbox.empty">
|
|
Keine offenen Freigaben.
|
|
</p>
|
|
<p className="dashboard-agenda-link">
|
|
<a href="/inbox" data-i18n="dashboard.inbox.full_link">Vollständigen Posteingang öffnen →</a>
|
|
</p>
|
|
</div>
|
|
</CollapsibleSection>
|
|
|
|
{/* Activity feed — moved under Agenda per m's design call
|
|
(t-paliad-162). */}
|
|
<CollapsibleSection id="activity" widgetKey="recent-activity" headingI18n="dashboard.activity.heading" headingDe="Letzte Aktivität">
|
|
<ul className="dashboard-activity-list" id="dashboard-activity-list"></ul>
|
|
<p className="dashboard-empty" id="dashboard-activity-empty" style="display:none" data-i18n="dashboard.activity.empty">
|
|
Noch keine Aktivität erfasst.
|
|
</p>
|
|
</CollapsibleSection>
|
|
|
|
{/* Pinned-projects widget (t-paliad-219 Slice C). Reads
|
|
PinService via DashboardData.pinned_projects (server-
|
|
joined to titles + refs). Default-hidden — users opt
|
|
in via the picker. */}
|
|
<CollapsibleSection id="pinned-projects" widgetKey="pinned-projects" headingI18n="dashboard.pinned.heading" headingDe="Angepinnte Akten">
|
|
<ul className="dashboard-list" id="dashboard-pinned-list"></ul>
|
|
<p className="dashboard-empty" id="dashboard-pinned-empty" style="display:none" data-i18n="dashboard.pinned.empty">
|
|
Noch keine Akten angepinnt.
|
|
</p>
|
|
<p className="dashboard-agenda-link">
|
|
<a href="/projects" data-i18n="dashboard.pinned.full_link">Alle Akten öffnen →</a>
|
|
</p>
|
|
</CollapsibleSection>
|
|
|
|
{/* Quick-actions widget (t-paliad-219 Slice C). Pure UI;
|
|
no backend data path. Default-hidden — surfaced via the
|
|
picker. */}
|
|
<CollapsibleSection id="quick-actions" widgetKey="quick-actions" headingI18n="dashboard.quick.heading" headingDe="Schnellzugriff">
|
|
<div className="dashboard-quick-actions">
|
|
<a href="/projects/new" className="btn btn-primary dashboard-quick-btn" data-i18n="dashboard.quick.new_project">+ Akte</a>
|
|
<a href="/deadlines/new" className="btn btn-secondary dashboard-quick-btn" data-i18n="dashboard.quick.new_deadline">+ Frist</a>
|
|
<a href="/appointments/new" className="btn btn-secondary dashboard-quick-btn" data-i18n="dashboard.quick.new_appointment">+ Termin</a>
|
|
</div>
|
|
</CollapsibleSection>
|
|
</div>
|
|
|
|
{/* Edit-mode footer (t-paliad-219 Slice B). Hidden via CSS
|
|
unless body.dashboard-editing — see dashboard.ts.
|
|
Slice C added the admin "Promote to firm default"
|
|
button — it stays hidden unless data.user.global_role
|
|
is 'global_admin'; dashboard.ts toggles it. */}
|
|
<div id="dashboard-edit-footer" className="dashboard-edit-footer">
|
|
<button
|
|
type="button"
|
|
id="dashboard-edit-add"
|
|
className="btn btn-secondary dashboard-edit-add"
|
|
data-i18n="dashboard.edit.add_widget"
|
|
>Widget hinzufügen</button>
|
|
<button
|
|
type="button"
|
|
id="dashboard-edit-promote"
|
|
className="btn btn-ghost dashboard-edit-promote"
|
|
style="display:none"
|
|
data-i18n="dashboard.edit.promote"
|
|
>Als Firmen-Standard speichern</button>
|
|
<button
|
|
type="button"
|
|
id="dashboard-edit-reset"
|
|
className="dashboard-edit-reset-link"
|
|
data-i18n="dashboard.edit.reset"
|
|
>Auf Standard zurücksetzen</button>
|
|
</div>
|
|
|
|
{/* Save toast slot — managed by dashboard.ts. */}
|
|
<div
|
|
id="dashboard-save-toast"
|
|
className="dashboard-save-toast"
|
|
role="status"
|
|
aria-live="polite"
|
|
></div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
<Footer />
|
|
<PaliadinWidget />
|
|
<script src="/assets/dashboard.js"></script>
|
|
</body>
|
|
</html>
|
|
);
|
|
}
|