"use client"; import { Download, Save, Trash2 } from "lucide-react"; import Link from "next/link"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { useState } from "react"; import { deleteReport, saveReport } from "@/actions/reports"; import { useToast } from "@/components/ui/Toast"; import { fmtDate } from "@/lib/format"; import { AVERAGE_MEASURES, DATE_SCOPED_MEASURES, GROUP_LABELS, MEASURE_LABELS, REPORT_PRESETS, type GroupDimension, type Measure, type ReportRow, } from "@/lib/reports"; const SPLIT_COLORS = ["bg-brand-500", "bg-info-text", "bg-purple-text", "bg-warning-text", "bg-success-text", "bg-danger-solid"]; type SavedReport = { id: string; name: string; config: Record }; type ReportsPageClientProps = { measure: Measure; group: GroupDimension; split: GroupDimension | ""; filters: { division: string; location: string; status: string; employment: string; from: string; to: string }; rows: ReportRow[]; total: number; recordCount: number; divisions: { id: string; name: string }[]; locations: { id: string; name: string }[]; savedReports: SavedReport[]; }; function formatValue(measure: Measure, value: number): string { if (["headcount", "hires", "exits"].includes(measure)) return String(Math.round(value)); if (measure === "fte") return value.toFixed(1); if (measure === "avg_salary") return new Intl.NumberFormat("de-AT", { style: "currency", currency: "EUR" }).format(value); if (measure === "parttime_rate" || measure === "female_share") return `${value.toFixed(1)}%`; if (measure === "avg_age" || measure === "avg_tenure") return `${value.toFixed(1)} Jahre`; return value.toFixed(1); } export function ReportsPageClient(props: ReportsPageClientProps) { const { measure, group, split, filters, rows, total, recordCount, divisions, locations, savedReports } = props; const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); const { showToast } = useToast(); const [expandedKey, setExpandedKey] = useState(null); function updateParams(patch: Record) { const sp = new URLSearchParams(searchParams.toString()); for (const [k, v] of Object.entries(patch)) { if (v) sp.set(k, v); else sp.delete(k); } router.push(`${pathname}?${sp.toString()}`); } function applyPreset(preset: (typeof REPORT_PRESETS)[number]) { router.push(`${pathname}?measure=${preset.measure}&group=${preset.group}${preset.split ? `&split=${preset.split}` : ""}`); } function applySavedReport(config: Record) { const sp = new URLSearchParams(); for (const [k, v] of Object.entries(config)) { if (typeof v === "string" && v) sp.set(k, v); } router.push(`${pathname}?${sp.toString()}`); } async function handleSaveReport() { const name = window.prompt("Name für diesen Bericht:"); if (!name) return; const result = await saveReport({ name, config: { measure, group, split, ...filters } }); if (result.success) { showToast("Bericht gespeichert."); router.refresh(); } else { showToast(result.error ?? "Fehler beim Speichern.", "error"); } } async function handleDeleteReport(id: string) { const result = await deleteReport(id); if (result.success) { showToast("Bericht gelöscht."); router.refresh(); } else { showToast(result.error ?? "Fehler beim Löschen.", "error"); } } function handleExportCsv() { const header = [GROUP_LABELS[group], MEASURE_LABELS[measure], "Anzahl", "Anteil (%)"]; const lines = [header.join(";")]; for (const row of rows) { const share = total > 0 ? ((row.value / total) * 100).toFixed(1) : "0"; lines.push([row.key, formatValue(measure, row.value), String(row.count), share].join(";")); } const blob = new Blob([lines.join("\n")], { type: "text/csv;charset=utf-8;" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `bericht-${measure}-${group}.csv`; a.click(); URL.revokeObjectURL(url); } const isAverage = AVERAGE_MEASURES.includes(measure); const isDateScoped = DATE_SCOPED_MEASURES.includes(measure); const maxValue = Math.max(1, ...rows.map((r) => r.value)); return (

Filter

{isDateScoped && (
updateParams({ from: e.target.value })} className="w-full rounded border border-border px-2 py-2 text-xs" /> updateParams({ to: e.target.value })} className="w-full rounded border border-border px-2 py-2 text-xs" />
)}

Vorlagen

{REPORT_PRESETS.map((preset) => ( ))}
{savedReports.length > 0 && (

Gespeicherte Berichte

    {savedReports.map((r) => (
  • ))}
)}

{MEASURE_LABELS[measure]} nach {GROUP_LABELS[group]}

{recordCount} Datensätze

{formatValue(measure, total)}

{split && (
{Array.from(new Set(rows.flatMap((r) => r.split?.map((s) => s.key) ?? []))).map((key, i) => ( {key} ))}
)}
{rows.map((row) => { const share = total > 0 ? (row.value / total) * 100 : 0; const isExpanded = expandedKey === row.key; return (
{isExpanded && (
    {row.people.slice(0, 12).map((p) => (
  • {p.name} {p.title} · {p.team} · {fmtDate(p.entry_date)}
  • ))}
{row.people.length > 12 &&

+{row.people.length - 12} weitere

}
)}
); })} {rows.length === 0 &&

Keine Daten für diese Auswahl.

}
); }