Merge Phase G: Dashboard landing

# Conflicts:
#	frontend/build.ts
#	frontend/src/styles/global.css
This commit is contained in:
m
2026-04-16 17:30:26 +02:00
12 changed files with 1411 additions and 5 deletions

View File

@@ -18,6 +18,7 @@ import { renderFristen } from "./src/fristen";
import { renderFristenNeu } from "./src/fristen-neu";
import { renderFristenDetail } from "./src/fristen-detail";
import { renderFristenKalender } from "./src/fristen-kalender";
import { renderDashboard } from "./src/dashboard";
const DIST = join(import.meta.dir, "dist");
@@ -47,6 +48,7 @@ async function build() {
join(import.meta.dir, "src/client/fristen-neu.ts"),
join(import.meta.dir, "src/client/fristen-detail.ts"),
join(import.meta.dir, "src/client/fristen-kalender.ts"),
join(import.meta.dir, "src/client/dashboard.ts"),
],
outdir: join(DIST, "assets"),
naming: "[name].js",
@@ -86,6 +88,7 @@ async function build() {
await Bun.write(join(DIST, "fristen-neu.html"), renderFristenNeu());
await Bun.write(join(DIST, "fristen-detail.html"), renderFristenDetail());
await Bun.write(join(DIST, "fristen-kalender.html"), renderFristenKalender());
await Bun.write(join(DIST, "dashboard.html"), renderDashboard());
console.log("Build complete \u2192 dist/");
}

View File

@@ -0,0 +1,318 @@
import { initI18n, onLangChange, t, getLang } from "./i18n";
import { initSidebar } from "./sidebar";
interface DashboardUser {
id: string;
email: string;
display_name: string;
office: string;
role: string;
}
interface DeadlineSummary {
overdue: number;
this_week: number;
upcoming: number;
completed_this_week: number;
}
interface MatterSummary {
active: number;
archived: number;
total: number;
}
interface UpcomingDeadline {
id: string;
title: string;
due_date: string;
akte_id: string;
akte_title: string;
akte_ref: string;
urgency: "overdue" | "today" | "urgent" | "soon";
}
interface UpcomingAppointment {
id: string;
title: string;
start_at: string;
end_at: string | null;
type: string | null;
akte_id: string | null;
akte_title: string | null;
akte_ref: string | null;
}
interface ActivityEntry {
timestamp: string;
actor_email: string | null;
actor_name: string | null;
akte_id: string;
akte_title: string;
akte_ref: string;
action: string | null;
details: string;
description: string | null;
}
interface DashboardData {
user: DashboardUser | null;
deadline_summary: DeadlineSummary;
matter_summary: MatterSummary;
upcoming_deadlines: UpcomingDeadline[];
upcoming_appointments: UpcomingAppointment[];
recent_activity: ActivityEntry[];
}
declare global {
interface Window {
__PALIAD_DASHBOARD__?: DashboardData | null;
}
}
const POLL_INTERVAL_MS = 60_000;
let data: DashboardData | null = null;
async function loadDashboard(): Promise<void> {
const unavailable = document.getElementById("dashboard-unavailable")!;
try {
const resp = await fetch("/api/dashboard");
if (resp.status === 503) {
unavailable.style.display = "block";
return;
}
if (!resp.ok) {
unavailable.style.display = "block";
return;
}
data = await resp.json();
render();
} catch {
unavailable.style.display = "block";
}
}
function render(): void {
if (!data) return;
renderGreeting(data.user);
renderSummary(data.deadline_summary);
renderMatters(data.matter_summary);
renderDeadlines(data.upcoming_deadlines);
renderAppointments(data.upcoming_appointments);
renderActivity(data.recent_activity);
toggleOnboardingHint(data.user);
}
function renderGreeting(user: DashboardUser | null): void {
const nameEl = document.getElementById("dashboard-greeting-name")!;
const chip = document.getElementById("dashboard-office-chip")!;
const dateEl = document.getElementById("dashboard-date")!;
if (user) {
nameEl.textContent = user.display_name ? `, ${user.display_name}` : "";
const officeLabel = t(`office.${user.office}`) || user.office;
chip.textContent = officeLabel;
chip.className = `dashboard-office-chip akten-office-chip akten-office-${user.office}`;
chip.style.display = "inline-block";
} else {
nameEl.textContent = "";
chip.style.display = "none";
}
const now = new Date();
dateEl.textContent = now.toLocaleDateString(
getLang() === "de" ? "de-DE" : "en-GB",
{ weekday: "long", year: "numeric", month: "long", day: "numeric" },
);
}
function renderSummary(s: DeadlineSummary): void {
setCount("dashboard-count-overdue", s.overdue);
setCount("dashboard-count-this-week", s.this_week);
setCount("dashboard-count-upcoming", s.upcoming);
setCount("dashboard-count-completed", s.completed_this_week);
// Tone down the red card when there's nothing overdue — reduces alarm
// fatigue when the user has a clean slate.
const overdueCard = document.getElementById("dashboard-card-overdue")!;
overdueCard.classList.toggle("dashboard-card-quiet", s.overdue === 0);
}
function renderMatters(s: MatterSummary): void {
setCount("dashboard-matter-active", s.active);
setCount("dashboard-matter-archived", s.archived);
setCount("dashboard-matter-total", s.total);
}
function renderDeadlines(items: UpcomingDeadline[]): void {
const list = document.getElementById("dashboard-deadlines-list")!;
const empty = document.getElementById("dashboard-deadlines-empty")!;
if (!items.length) {
list.innerHTML = "";
list.style.display = "none";
empty.style.display = "block";
return;
}
list.style.display = "";
empty.style.display = "none";
list.innerHTML = items.map((d) => {
const urgencyClass = `dashboard-urgency-${d.urgency}`;
const urgencyLabel = t(`dashboard.urgency.${d.urgency}`);
return `<li class="dashboard-list-item">
<a href="/akten/${esc(d.akte_id)}/fristen" class="dashboard-list-link">
<div class="dashboard-list-main">
<span class="dashboard-list-title">${esc(d.title)}</span>
<span class="dashboard-list-ref">${esc(d.akte_ref)} &middot; ${esc(d.akte_title)}</span>
</div>
<div class="dashboard-list-meta">
<span class="dashboard-urgency-badge ${urgencyClass}" title="${escAttr(urgencyLabel)}">${esc(formatRelative(d.due_date))}</span>
</div>
</a>
</li>`;
}).join("");
}
function renderAppointments(items: UpcomingAppointment[]): void {
const list = document.getElementById("dashboard-appointments-list")!;
const empty = document.getElementById("dashboard-appointments-empty")!;
if (!items.length) {
list.innerHTML = "";
list.style.display = "none";
empty.style.display = "block";
return;
}
list.style.display = "";
empty.style.display = "none";
list.innerHTML = items.map((a) => {
const dot = a.type
? `<span class="dashboard-termin-dot dashboard-termin-${esc(a.type)}" aria-hidden="true"></span>`
: `<span class="dashboard-termin-dot" aria-hidden="true"></span>`;
const href = a.akte_id ? `/akten/${esc(a.akte_id)}/termine` : "#";
const tag = a.akte_id ? "a" : "div";
const akteLine = a.akte_ref && a.akte_title
? `<span class="dashboard-list-ref">${esc(a.akte_ref)} &middot; ${esc(a.akte_title)}</span>`
: "";
return `<li class="dashboard-list-item">
<${tag} href="${href}" class="dashboard-list-link">
<div class="dashboard-list-main">
<span class="dashboard-list-title">${dot}${esc(a.title)}</span>
${akteLine}
</div>
<div class="dashboard-list-meta">
<span class="dashboard-appt-time">${esc(formatDateTime(a.start_at))}</span>
</div>
</${tag}>
</li>`;
}).join("");
}
function renderActivity(items: ActivityEntry[]): void {
const list = document.getElementById("dashboard-activity-list")!;
const empty = document.getElementById("dashboard-activity-empty")!;
if (!items.length) {
list.innerHTML = "";
list.style.display = "none";
empty.style.display = "block";
return;
}
list.style.display = "";
empty.style.display = "none";
list.innerHTML = items.map((e) => {
const actor = e.actor_name || e.actor_email || t("dashboard.activity.system");
const actionLabel = e.action
? (t(`dashboard.action.${e.action}`) || e.action)
: t("dashboard.activity.event");
return `<li class="dashboard-activity-item">
<span class="dashboard-activity-time">${esc(formatDateTime(e.timestamp))}</span>
<span class="dashboard-activity-body">
<span class="dashboard-activity-actor">${esc(actor)}</span>
<span class="dashboard-activity-action">${esc(actionLabel)}</span>
<a href="/akten/${esc(e.akte_id)}" class="dashboard-activity-akte">${esc(e.akte_ref)}</a>
<span class="dashboard-activity-details">${esc(e.details)}</span>
</span>
</li>`;
}).join("");
}
function toggleOnboardingHint(user: DashboardUser | null): void {
const onboarding = document.getElementById("dashboard-onboarding")!;
onboarding.style.display = user ? "none" : "block";
}
function setCount(id: string, n: number): void {
const el = document.getElementById(id);
if (el) el.textContent = String(n);
}
function formatRelative(isoDate: string): string {
const due = new Date(isoDate + "T00:00:00");
if (isNaN(due.getTime())) return isoDate;
const today = new Date();
today.setHours(0, 0, 0, 0);
const diffDays = Math.round((due.getTime() - today.getTime()) / 86400000);
const lang = getLang();
if (diffDays < 0) {
const n = Math.abs(diffDays);
return lang === "de"
? (n === 1 ? "vor 1 Tag" : `vor ${n} Tagen`)
: (n === 1 ? "1 day ago" : `${n} days ago`);
}
if (diffDays === 0) return t("dashboard.when.today");
if (diffDays === 1) return t("dashboard.when.tomorrow");
return lang === "de" ? `in ${diffDays} Tagen` : `in ${diffDays} days`;
}
function formatDateTime(iso: string): string {
const d = new Date(iso);
if (isNaN(d.getTime())) return iso;
const locale = getLang() === "de" ? "de-DE" : "en-GB";
return d.toLocaleString(locale, {
day: "2-digit",
month: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
}
function esc(s: string): string {
const d = document.createElement("div");
d.textContent = s ?? "";
return d.innerHTML;
}
function escAttr(s: string): string {
return (s ?? "").replace(/&/g, "&amp;").replace(/"/g, "&quot;");
}
function schedulePolling(): void {
// Refresh the payload every minute so open dashboards stay current when
// teammates create Akten/Fristen. Uses the JSON endpoint — no page reload.
window.setInterval(() => {
void loadDashboard();
}, POLL_INTERVAL_MS);
}
document.addEventListener("DOMContentLoaded", () => {
initI18n();
initSidebar();
onLangChange(render);
const inlined = window.__PALIAD_DASHBOARD__;
if (inlined !== undefined) {
// Server-side hydration path: the handler spliced the payload directly
// into the HTML. Render synchronously, then start polling.
if (inlined === null) {
document.getElementById("dashboard-unavailable")!.style.display = "block";
} else {
data = inlined;
render();
}
schedulePolling();
return;
}
// Fallback for dev or when the handler couldn't splice (no DB, etc.).
void loadDashboard().then(schedulePolling);
});

View File

@@ -24,6 +24,8 @@ const translations: Record<Lang, Record<string, string>> = {
"nav.akten": "Akten",
"nav.fristen": "Fristen",
"nav.termine": "Termine",
"nav.dashboard": "Dashboard",
"nav.group.uebersicht": "\u00dcbersicht",
"nav.group.arbeit": "Arbeit",
"nav.group.werkzeuge": "Werkzeuge",
"nav.group.wissen": "Wissen",
@@ -582,6 +584,42 @@ const translations: Record<Lang, Record<string, string>> = {
"office.london": "London",
"office.paris": "Paris",
"office.milan": "Mailand",
// Dashboard (logged-in landing)
"dashboard.title": "Dashboard \u2014 Paliad",
"dashboard.greeting.prefix": "Guten Tag",
"dashboard.unavailable": "Dashboard ben\u00f6tigt die Datenbank \u2014 bitte Administrator kontaktieren.",
"dashboard.onboarding": "Bitte schlie\u00dfen Sie das Onboarding ab, damit Ihnen Fristen und Mandate angezeigt werden k\u00f6nnen.",
"dashboard.summary.heading": "Fristen auf einen Blick",
"dashboard.summary.overdue": "\u00dcberf\u00e4llig",
"dashboard.summary.this_week": "Diese Woche",
"dashboard.summary.upcoming": "Kommend",
"dashboard.summary.completed": "Abgeschlossen (7\u202fT.)",
"dashboard.matters.heading": "Meine Mandate",
"dashboard.matters.active": "Aktiv",
"dashboard.matters.archived": "Archiviert",
"dashboard.matters.total": "Gesamt",
"dashboard.deadlines.heading": "Kommende Fristen",
"dashboard.deadlines.empty": "Keine Fristen in den n\u00e4chsten 7 Tagen.",
"dashboard.appointments.heading": "Kommende Termine",
"dashboard.appointments.empty": "Keine Termine in den n\u00e4chsten 7 Tagen.",
"dashboard.activity.heading": "Letzte Aktivit\u00e4t",
"dashboard.activity.empty": "Noch keine Aktivit\u00e4t erfasst.",
"dashboard.activity.system": "System",
"dashboard.activity.event": "Ereignis",
"dashboard.urgency.overdue": "\u00dcberf\u00e4llig",
"dashboard.urgency.today": "Heute",
"dashboard.urgency.urgent": "Dringend",
"dashboard.urgency.soon": "Bald",
"dashboard.when.today": "heute",
"dashboard.when.tomorrow": "morgen",
"dashboard.action.akte_created": "legte Akte an",
"dashboard.action.akte_archived": "archivierte Akte",
"dashboard.action.status_changed": "\u00e4nderte Status",
"dashboard.action.visibility_changed": "\u00e4nderte Sichtbarkeit",
"dashboard.action.collaborators_updated": "aktualisierte Bearbeiter",
"dashboard.action.partei_added": "f\u00fcgte Partei hinzu",
"dashboard.action.partei_removed": "entfernte Partei",
},
en: {
@@ -599,6 +637,8 @@ const translations: Record<Lang, Record<string, string>> = {
"nav.akten": "Matters",
"nav.fristen": "Deadlines",
"nav.termine": "Appointments",
"nav.dashboard": "Dashboard",
"nav.group.uebersicht": "Overview",
"nav.group.arbeit": "Work",
"nav.group.werkzeuge": "Tools",
"nav.group.wissen": "Knowledge",
@@ -1157,6 +1197,42 @@ const translations: Record<Lang, Record<string, string>> = {
"office.london": "London",
"office.paris": "Paris",
"office.milan": "Milan",
// Dashboard (logged-in landing)
"dashboard.title": "Dashboard \u2014 Paliad",
"dashboard.greeting.prefix": "Good day",
"dashboard.unavailable": "Dashboard requires the database \u2014 contact an administrator.",
"dashboard.onboarding": "Please complete onboarding before deadlines and matters are shown.",
"dashboard.summary.heading": "Deadlines at a glance",
"dashboard.summary.overdue": "Overdue",
"dashboard.summary.this_week": "This week",
"dashboard.summary.upcoming": "Upcoming",
"dashboard.summary.completed": "Completed (7d)",
"dashboard.matters.heading": "My matters",
"dashboard.matters.active": "Active",
"dashboard.matters.archived": "Archived",
"dashboard.matters.total": "Total",
"dashboard.deadlines.heading": "Upcoming deadlines",
"dashboard.deadlines.empty": "No deadlines in the next 7 days.",
"dashboard.appointments.heading": "Upcoming appointments",
"dashboard.appointments.empty": "No appointments in the next 7 days.",
"dashboard.activity.heading": "Recent activity",
"dashboard.activity.empty": "No activity recorded yet.",
"dashboard.activity.system": "System",
"dashboard.activity.event": "event",
"dashboard.urgency.overdue": "Overdue",
"dashboard.urgency.today": "Today",
"dashboard.urgency.urgent": "Urgent",
"dashboard.urgency.soon": "Soon",
"dashboard.when.today": "today",
"dashboard.when.tomorrow": "tomorrow",
"dashboard.action.akte_created": "created matter",
"dashboard.action.akte_archived": "archived matter",
"dashboard.action.status_changed": "changed status",
"dashboard.action.visibility_changed": "changed visibility",
"dashboard.action.collaborators_updated": "updated collaborators",
"dashboard.action.partei_added": "added party",
"dashboard.action.partei_removed": "removed party",
},
};

View File

@@ -15,6 +15,7 @@ const ICON_PIN = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" str
const ICON_MENU = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="18" x2="20" y2="18"/></svg>';
const ICON_FOLDER = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>';
const ICON_CALENDAR = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>';
const ICON_GAUGE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 14l3.5-3.5"/><path d="M3 12a9 9 0 0 1 18 0"/><path d="M12 3v2"/><path d="M3 12H5"/><path d="M19 12h2"/><path d="M5.6 5.6l1.4 1.4"/><path d="M17 7l1.4-1.4"/></svg>';
interface SidebarProps {
currentPath: string;
@@ -69,6 +70,10 @@ export function Sidebar({ currentPath }: SidebarProps): string {
<nav className="sidebar-nav">
{navItem("/", ICON_HOME, "nav.home", "Home", currentPath)}
{group("nav.group.uebersicht", "\u00DCbersicht",
navItem("/dashboard", ICON_GAUGE, "nav.dashboard", "Dashboard", currentPath),
)}
{group("nav.group.arbeit", "Arbeit",
navItem("/akten", ICON_FOLDER, "nav.akten", "Akten", currentPath) +
navItem("/fristen", ICON_CLOCK, "nav.fristen", "Fristen", currentPath) +

138
frontend/src/dashboard.tsx Normal file
View File

@@ -0,0 +1,138 @@
import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar";
import { Footer } from "./components/Footer";
// The /* __PALIAD_DASHBOARD_DATA__ */ token below is replaced at request time
// by the Go handler (internal/handlers/dashboard_shell.go) with a JSON blob
// assigned to window.__PALIAD_DASHBOARD__. Keep the token intact and exactly
// once in the output.
const HYDRATION_SCRIPT =
"/*__PALIAD_DASHBOARD_DATA__*/";
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.0" />
<title data-i18n="dashboard.title">Dashboard &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
<script dangerouslySetInnerHTML={{ __html: HYDRATION_SCRIPT }} />
</head>
<body className="has-sidebar">
<Sidebar 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>
</div>
<div id="dashboard-unavailable" className="dashboard-unavailable" style="display:none">
<p data-i18n="dashboard.unavailable">
Dashboard ben&ouml;tigt die Datenbank &mdash; bitte Administrator kontaktieren.
</p>
</div>
<div id="dashboard-onboarding" className="dashboard-unavailable" style="display:none">
<p data-i18n="dashboard.onboarding">
Bitte schlie&szlig;en Sie das Onboarding ab, damit Ihnen Fristen und Mandate angezeigt werden k&ouml;nnen.
</p>
</div>
{/* Traffic-light deadline summary */}
<section className="dashboard-summary" aria-labelledby="dashboard-summary-heading">
<h2 id="dashboard-summary-heading" className="dashboard-section-heading" data-i18n="dashboard.summary.heading">
Fristen auf einen Blick
</h2>
<div className="dashboard-summary-grid">
<a href="/fristen?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">&Uuml;berf&auml;llig</div>
</a>
<a href="/fristen?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="/fristen?status=upcoming" className="dashboard-card dashboard-card-green" id="dashboard-card-upcoming">
<div className="dashboard-card-count" id="dashboard-count-upcoming">0</div>
<div className="dashboard-card-label" data-i18n="dashboard.summary.upcoming">Kommend</div>
</a>
<a href="/fristen?status=completed" className="dashboard-card dashboard-card-done" id="dashboard-card-completed">
<div className="dashboard-card-count" id="dashboard-count-completed">0</div>
<div className="dashboard-card-label" data-i18n="dashboard.summary.completed">Abgeschlossen (7&#8239;T.)</div>
</a>
</div>
</section>
{/* Matter summary card */}
<section className="dashboard-matters">
<a href="/akten" className="dashboard-matter-card">
<div className="dashboard-matter-header">
<h3 data-i18n="dashboard.matters.heading">Meine Mandate</h3>
<span className="dashboard-matter-arrow" aria-hidden="true">&rarr;</span>
</div>
<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>
</section>
{/* Two-column lists */}
<div className="dashboard-columns">
<section className="dashboard-col">
<h3 className="dashboard-section-heading" data-i18n="dashboard.deadlines.heading">Kommende Fristen</h3>
<ul className="dashboard-list" id="dashboard-deadlines-list"></ul>
<p className="dashboard-empty" id="dashboard-deadlines-empty" style="display:none" data-i18n="dashboard.deadlines.empty">
Keine Fristen in den n&auml;chsten 7 Tagen.
</p>
</section>
<section className="dashboard-col">
<h3 className="dashboard-section-heading" data-i18n="dashboard.appointments.heading">Kommende Termine</h3>
<ul className="dashboard-list" id="dashboard-appointments-list"></ul>
<p className="dashboard-empty" id="dashboard-appointments-empty" style="display:none" data-i18n="dashboard.appointments.empty">
Keine Termine in den n&auml;chsten 7 Tagen.
</p>
</section>
</div>
{/* Activity feed */}
<section className="dashboard-activity">
<h3 className="dashboard-section-heading" data-i18n="dashboard.activity.heading">Letzte Aktivit&auml;t</h3>
<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&auml;t erfasst.
</p>
</section>
</div>
</section>
</main>
<Footer />
<script src="/assets/dashboard.js"></script>
</body>
</html>
);
}

View File

@@ -4547,3 +4547,386 @@ input[type="range"]::-moz-range-thumb {
min-height: 64px;
}
}
/* ========================================================================
Dashboard (Phase G — logged-in landing)
======================================================================== */
.dashboard-header {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.dashboard-greeting {
font-size: 1.6rem;
font-weight: 600;
letter-spacing: -0.02em;
margin: 0;
}
.dashboard-greeting-name {
color: var(--color-accent);
}
.dashboard-subline {
margin-top: 0.4rem;
display: flex;
align-items: center;
gap: 0.75rem;
color: var(--color-text-muted);
font-size: 0.88rem;
}
.dashboard-office-chip {
margin-right: 0.25rem;
}
.dashboard-date {
text-transform: capitalize;
}
.dashboard-section-heading {
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--color-text-muted);
margin-bottom: 0.75rem;
font-weight: 600;
}
.dashboard-unavailable {
padding: 1rem 1.25rem;
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: #fff8e6;
color: #70520b;
margin-bottom: 1.5rem;
font-size: 0.92rem;
}
/* --- Traffic-light summary cards --- */
.dashboard-summary {
margin-bottom: 2rem;
}
.dashboard-summary-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1rem;
}
.dashboard-card {
display: block;
padding: 1.25rem 1.4rem;
border-radius: var(--radius);
background: var(--color-surface);
border: 1px solid var(--color-border);
text-decoration: none;
color: var(--color-text);
transition: transform 0.12s ease, box-shadow 0.12s ease, border-color 0.12s ease;
box-shadow: var(--shadow);
}
.dashboard-card:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
.dashboard-card-count {
font-family: var(--font-mono);
font-size: 2.25rem;
font-weight: 700;
line-height: 1.1;
letter-spacing: -0.03em;
}
.dashboard-card-label {
margin-top: 0.4rem;
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: 600;
color: var(--color-text-muted);
}
.dashboard-card-red .dashboard-card-count { color: #b91c1c; }
.dashboard-card-red { border-left: 3px solid #b91c1c; }
.dashboard-card-amber .dashboard-card-count { color: #b45309; }
.dashboard-card-amber { border-left: 3px solid #f59e0b; }
.dashboard-card-green .dashboard-card-count { color: #15803d; }
.dashboard-card-green { border-left: 3px solid #65a30d; }
.dashboard-card-done .dashboard-card-count { color: #475569; }
.dashboard-card-done { border-left: 3px solid #94a3b8; }
.dashboard-card-quiet .dashboard-card-count { color: var(--color-text-muted); }
.dashboard-card-quiet { border-left-color: var(--color-border); }
/* --- Matter summary card --- */
.dashboard-matters {
margin-bottom: 2rem;
}
.dashboard-matter-card {
display: block;
padding: 1.25rem 1.5rem;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
text-decoration: none;
color: var(--color-text);
box-shadow: var(--shadow);
transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
.dashboard-matter-card:hover {
border-color: var(--color-accent-light);
box-shadow: var(--shadow-md);
}
.dashboard-matter-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.9rem;
}
.dashboard-matter-header h3 {
font-size: 0.95rem;
font-weight: 600;
margin: 0;
}
.dashboard-matter-arrow {
color: var(--color-accent);
font-size: 1.1rem;
}
.dashboard-matter-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
}
.dashboard-matter-num {
font-family: var(--font-mono);
font-size: 1.5rem;
font-weight: 700;
line-height: 1.1;
color: var(--color-text);
}
.dashboard-matter-lbl {
margin-top: 0.25rem;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-text-muted);
}
/* --- Two-column lists --- */
.dashboard-columns {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
margin-bottom: 2.5rem;
}
.dashboard-col {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
padding: 1.25rem 1.4rem;
box-shadow: var(--shadow);
}
.dashboard-list {
list-style: none;
margin: 0;
padding: 0;
}
.dashboard-list-item {
border-bottom: 1px solid var(--color-border);
}
.dashboard-list-item:last-child {
border-bottom: none;
}
.dashboard-list-link {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.75rem 0;
text-decoration: none;
color: var(--color-text);
transition: color 0.1s ease;
}
.dashboard-list-link:hover {
color: var(--color-accent);
}
.dashboard-list-main {
display: flex;
flex-direction: column;
gap: 0.15rem;
min-width: 0;
}
.dashboard-list-title {
font-weight: 600;
font-size: 0.92rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dashboard-list-ref {
font-size: 0.78rem;
color: var(--color-text-muted);
font-family: var(--font-mono);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dashboard-list-meta {
flex-shrink: 0;
}
.dashboard-urgency-badge {
display: inline-block;
padding: 0.2rem 0.6rem;
border-radius: 999px;
font-size: 0.72rem;
font-weight: 600;
white-space: nowrap;
}
.dashboard-urgency-overdue { background: #fee2e2; color: #b91c1c; }
.dashboard-urgency-today { background: #fef3c7; color: #92400e; }
.dashboard-urgency-urgent { background: #fef3c7; color: #b45309; }
.dashboard-urgency-soon { background: #ecfccb; color: #365314; }
.dashboard-appt-time {
font-family: var(--font-mono);
font-size: 0.82rem;
color: var(--color-text-muted);
white-space: nowrap;
}
.dashboard-termin-dot {
display: inline-block;
width: 0.55rem;
height: 0.55rem;
border-radius: 999px;
background: #94a3b8;
margin-right: 0.5rem;
vertical-align: middle;
}
.dashboard-termin-hearing { background: #b91c1c; }
.dashboard-termin-meeting { background: #2563eb; }
.dashboard-termin-consultation { background: #65a30d; }
.dashboard-termin-deadline_hearing { background: #b45309; }
.dashboard-empty {
padding: 1.5rem 0.5rem;
color: var(--color-text-muted);
font-size: 0.88rem;
font-style: italic;
}
/* --- Activity feed --- */
.dashboard-activity {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
padding: 1.25rem 1.4rem;
box-shadow: var(--shadow);
margin-bottom: 3rem;
}
.dashboard-activity-list {
list-style: none;
margin: 0;
padding: 0;
}
.dashboard-activity-item {
display: grid;
grid-template-columns: 8.5rem 1fr;
gap: 0.75rem;
padding: 0.6rem 0;
border-bottom: 1px solid var(--color-border);
font-size: 0.88rem;
}
.dashboard-activity-item:last-child {
border-bottom: none;
}
.dashboard-activity-time {
font-family: var(--font-mono);
font-size: 0.78rem;
color: var(--color-text-muted);
white-space: nowrap;
}
.dashboard-activity-body {
display: inline;
}
.dashboard-activity-actor {
font-weight: 600;
margin-right: 0.3rem;
}
.dashboard-activity-action {
color: var(--color-text-muted);
margin-right: 0.3rem;
}
.dashboard-activity-akte {
font-family: var(--font-mono);
font-size: 0.82rem;
color: var(--color-accent);
text-decoration: none;
margin-right: 0.3rem;
}
.dashboard-activity-akte:hover {
text-decoration: underline;
}
.dashboard-activity-details {
color: var(--color-text-muted);
}
/* --- Responsive --- */
@media (max-width: 900px) {
.dashboard-summary-grid {
grid-template-columns: repeat(2, 1fr);
}
.dashboard-columns {
grid-template-columns: 1fr;
}
.dashboard-matter-stats {
grid-template-columns: repeat(3, 1fr);
}
.dashboard-activity-item {
grid-template-columns: 1fr;
gap: 0.2rem;
}
}