Reports (§4.8): - lib/reports.ts: generic server-side aggregation engine over 9 measures (Headcount, FTE, Eintritte, Austritte, Ø Bruttogehalt, Teilzeitquote, Ø Alter, Ø Zugehoerigkeit, Frauenanteil) x 10 group-by dimensions, with an optional second-dimension split (disabled for average-type measures) and per-row drill-down data. - app/(app)/reports/page.tsx: reads filters from the URL, fetches the matching employees_directory rows server-side, aggregates in Node (not shipped raw to the client), computes the total. - ReportsPageClient: measure/group/split/filter controls, 6 preset chips, saved-reports list (actions/reports.ts), CSV export (client-side blob download), stacked bars with a color-keyed legend when split is active, and click-to-drill-down into the underlying people (capped at 12, "+N weitere", linking to /employees/[id]). Audit log (§4.9): - app/(app)/audit/page.tsx + AuditFilters: search (target/details/actor) + action-type filter, paginated table with colored action badges, row links to the affected employee when target_employee_id is set, and the required "unveraenderbar" footer note. This completes every route from the spec's information architecture: Dashboard, Mitarbeiter:innen (list+detail), Organigramm (3 views), Positionen & Bereiche, Berichte, Audit-Log, plus the Hire wizard and all 6 action panels reachable from them. Final verification: clean npm run build + tsc --noEmit, then a full browser walkthrough of all 6 authenticated routes as hr_admin (zero console errors, zero 5xx responses) and a role-check pass as the manager test account confirming action buttons and the "+ Neueinstellung" button are hidden, and salary is masked as "... (ausgeblendet)" on the Vertrag & Gehalt tab. Swept the database for leftover test data from the debugging sessions above - none found, seed data is clean.
77 lines
2.3 KiB
TypeScript
77 lines
2.3 KiB
TypeScript
"use client";
|
|
|
|
import { Search } from "lucide-react";
|
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
|
import { useEffect, useState } from "react";
|
|
|
|
const ACTIONS = [
|
|
"Neueinstellung",
|
|
"Wiedereinstellung",
|
|
"Rückkehr",
|
|
"Austritt",
|
|
"Versetzung",
|
|
"Ausschreibung",
|
|
"Interne Besetzung",
|
|
"Beförderung",
|
|
"Reorganisation",
|
|
"Reorganisation rückgängig",
|
|
"Karenz",
|
|
"Vertragsänderung",
|
|
"Stammdatenänderung",
|
|
"Gehaltsanpassung",
|
|
];
|
|
|
|
export function AuditFilters() {
|
|
const router = useRouter();
|
|
const pathname = usePathname();
|
|
const searchParams = useSearchParams();
|
|
const [q, setQ] = useState(searchParams.get("q") ?? "");
|
|
|
|
useEffect(() => {
|
|
const handle = setTimeout(() => {
|
|
const current = new URLSearchParams(searchParams.toString());
|
|
if (q) current.set("q", q);
|
|
else current.delete("q");
|
|
current.delete("page");
|
|
const next = current.toString();
|
|
if (next !== searchParams.toString()) router.push(`${pathname}?${next}`);
|
|
}, 300);
|
|
return () => clearTimeout(handle);
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [q]);
|
|
|
|
function updateParam(key: string, value: string) {
|
|
const params = new URLSearchParams(searchParams.toString());
|
|
if (value) params.set(key, value);
|
|
else params.delete(key);
|
|
params.delete("page");
|
|
router.push(`${pathname}?${params.toString()}`);
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
<div className="flex min-w-[240px] flex-1 items-center gap-2 rounded border border-border bg-white px-3 py-2">
|
|
<Search className="h-4 w-4 shrink-0 text-ink-muted" />
|
|
<input
|
|
value={q}
|
|
onChange={(e) => setQ(e.target.value)}
|
|
placeholder="Objekt, Details, Benutzer:in…"
|
|
className="w-full text-sm text-ink outline-none placeholder:text-ink-muted"
|
|
/>
|
|
</div>
|
|
<select
|
|
defaultValue={searchParams.get("action") ?? ""}
|
|
onChange={(e) => updateParam("action", e.target.value)}
|
|
className="rounded border border-border bg-white px-3 py-2 text-sm text-ink"
|
|
>
|
|
<option value="">Alle Aktionen</option>
|
|
{ACTIONS.map((a) => (
|
|
<option key={a} value={a}>
|
|
{a}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
);
|
|
}
|