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.
101 lines
3.5 KiB
TypeScript
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>
|
|
);
|
|
}
|