Files
alpenwerk-hr/app/(app)/reports/page.tsx
Maximilian Stubhan f96773da0f Reports/Export builder (CSV/XLSX), plus a security fix pass
Adds the Berichte export pipeline (/api/export/{report,events,employees})
with shared CSV/XLSX writers in lib/export.ts and lib/reports-data.ts.

Security pass alongside it: sanitize .or() search terms against PostgREST
filter injection, sanitize spreadsheet cells against CSV/Excel formula
injection, stop leaking raw DB error messages to clients, harden the
service-role client with server-only, add baseline security headers, and
bump the vulnerable nested postcss via an override.
2026-07-15 20:34:27 +02:00

101 lines
3.5 KiB
TypeScript

import { Suspense } from "react";
import { ReportsPageClient } from "@/components/reports/ReportsPageClient";
import { aggregateEvents, aggregateReport, sumValues, totalForRows, type EventGroupDimension, type GroupDimension, type Measure } from "@/lib/reports";
import { loadEventHistory, loadOrgLookups, loadSnapshotEmployees } from "@/lib/reports-data";
import { createClient } from "@/lib/supabase/server";
import type { HistoryEventType } from "@/lib/supabase/types";
type SearchParams = {
mode?: string;
measure?: string;
group?: string;
split?: string;
division?: string;
location?: string;
status?: string;
employment?: string;
asOf?: string;
eventType?: string;
from?: string;
to?: string;
};
export default async function ReportsPage({ searchParams }: { searchParams: Promise<SearchParams> }) {
const params = await searchParams;
const supabase = await createClient();
const mode = params.mode === "events" ? "events" : "snapshot";
const [{ lookups, divisions, locations }, { data: userRes }] = await Promise.all([loadOrgLookups(supabase), supabase.auth.getUser()]);
const user = userRes.user;
const { data: savedReports } = user
? await supabase.from("saved_reports").select("id, name, config").eq("created_by", user.id).order("created_at", { ascending: false })
: { data: [] };
if (mode === "events") {
const group = (params.group as EventGroupDimension) || "event_type";
const split = (params.split as EventGroupDimension) || undefined;
const eventType = (params.eventType as HistoryEventType) || undefined;
const events = await loadEventHistory(supabase, { eventType, division: params.division, location: params.location, from: params.from, to: params.to });
const rows = aggregateEvents(events, group, split ?? null, lookups);
const total = sumValues(rows);
return (
<Suspense>
<ReportsPageClient
mode="events"
eventGroup={group}
eventSplit={split ?? ""}
eventType={eventType ?? ""}
eventFilters={{ division: params.division ?? "", location: params.location ?? "", from: params.from ?? "", to: params.to ?? "" }}
rows={rows}
total={total}
recordCount={events.length}
divisions={divisions}
locations={locations}
savedReports={savedReports ?? []}
/>
</Suspense>
);
}
const measure = (params.measure as Measure) || "headcount";
const group = (params.group as GroupDimension) || "division";
const split = (params.split as GroupDimension) || undefined;
const asOf = params.asOf || undefined;
const employees = await loadSnapshotEmployees(supabase, {
division: params.division,
location: params.location,
status: params.status,
employment: params.employment,
asOf,
});
const rows = aggregateReport(employees, measure, group, split ?? null, lookups, asOf);
const total = totalForRows(rows, measure);
return (
<Suspense>
<ReportsPageClient
mode="snapshot"
measure={measure}
group={group}
split={split ?? ""}
asOf={asOf ?? ""}
filters={{
division: params.division ?? "",
location: params.location ?? "",
status: params.status ?? "",
employment: params.employment ?? "",
}}
rows={rows}
total={total}
recordCount={employees.length}
divisions={divisions}
locations={locations}
savedReports={savedReports ?? []}
/>
</Suspense>
);
}