export type Measure = | "headcount" | "fte" | "hires" | "exits" | "avg_salary" | "parttime_rate" | "avg_age" | "avg_tenure" | "female_share"; export type GroupDimension = | "division" | "department" | "team" | "location" | "status" | "employment_type" | "contract_type" | "entry_year" | "source" | "paygrade"; export const MEASURE_LABELS: Record = { headcount: "Headcount", fte: "FTE", hires: "Eintritte", exits: "Austritte", avg_salary: "Ø Bruttogehalt", parttime_rate: "Teilzeitquote", avg_age: "Ø Alter", avg_tenure: "Ø Zugehörigkeit", female_share: "Frauenanteil", }; export const GROUP_LABELS: Record = { division: "Bereich", department: "Abteilung", team: "Team", location: "Standort", status: "Status", employment_type: "Beschäftigung", contract_type: "Vertragsart", entry_year: "Eintrittsjahr", source: "Intern/Extern", paygrade: "Paygrade", }; export const AVERAGE_MEASURES: Measure[] = ["avg_salary", "parttime_rate", "avg_age", "avg_tenure", "female_share"]; export const DATE_SCOPED_MEASURES: Measure[] = ["hires", "exits"]; export type ReportEmployee = { id: string; first_name: string; last_name: string; job_title: string; division_id: string; team_id: string | null; location_id: string; status: string; employment_type: string; contract_type: string; entry_date: string; exit_date: string | null; weekly_hours: number; monthly_salary_gross: number | null; source: string; paygrade: string; birth_date: string; gender: string; }; export type OrgLookups = { divisionName: Map; departmentNameByTeam: Map; teamName: Map; locationName: Map; }; function ageFromBirthDate(birthDate: string): number { const d = new Date(birthDate); const today = new Date(); let age = today.getFullYear() - d.getFullYear(); if (today.getMonth() < d.getMonth() || (today.getMonth() === d.getMonth() && today.getDate() < d.getDate())) age -= 1; return age; } function tenureYears(entryDate: string, exitDate: string | null): number { const start = new Date(entryDate); const end = exitDate ? new Date(exitDate) : new Date(); return Math.max(0, (end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24 * 365.25)); } export function groupKeyFor(e: ReportEmployee, dim: GroupDimension, lookups: OrgLookups): string { switch (dim) { case "division": return lookups.divisionName.get(e.division_id) ?? "Unbekannt"; case "department": return e.team_id ? (lookups.departmentNameByTeam.get(e.team_id) ?? "Unbekannt") : "–"; case "team": return e.team_id ? (lookups.teamName.get(e.team_id) ?? "Unbekannt") : "–"; case "location": return lookups.locationName.get(e.location_id) ?? "Unbekannt"; case "status": return e.status; case "employment_type": return e.employment_type; case "contract_type": return e.contract_type; case "entry_year": return String(new Date(e.entry_date).getFullYear()); case "source": return e.source; case "paygrade": return e.paygrade; default: return "Unbekannt"; } } export function measureValue(rows: ReportEmployee[], measure: Measure): number { if (rows.length === 0) return 0; switch (measure) { case "headcount": case "hires": case "exits": return rows.length; case "fte": return rows.reduce((s, e) => s + e.weekly_hours / 38.5, 0); case "avg_salary": { const withSalary = rows.filter((e) => e.monthly_salary_gross != null); return withSalary.length ? withSalary.reduce((s, e) => s + (e.monthly_salary_gross ?? 0), 0) / withSalary.length : 0; } case "parttime_rate": return (rows.filter((e) => e.employment_type === "Teilzeit").length / rows.length) * 100; case "avg_age": return rows.reduce((s, e) => s + ageFromBirthDate(e.birth_date), 0) / rows.length; case "avg_tenure": return rows.reduce((s, e) => s + tenureYears(e.entry_date, e.exit_date), 0) / rows.length; case "female_share": return (rows.filter((e) => e.gender === "w").length / rows.length) * 100; default: return 0; } } export type ReportPerson = { id: string; name: string; title: string; team: string; entry_date: string }; export type ReportSplitRow = { key: string; value: number; count: number }; export type ReportRow = { key: string; value: number; count: number; people: ReportPerson[]; split?: ReportSplitRow[] }; export function aggregateReport( employees: ReportEmployee[], measure: Measure, group: GroupDimension, split: GroupDimension | null, lookups: OrgLookups ): ReportRow[] { const byGroup = new Map(); for (const e of employees) { const key = groupKeyFor(e, group, lookups); if (!byGroup.has(key)) byGroup.set(key, []); byGroup.get(key)!.push(e); } const rows: ReportRow[] = []; for (const [key, rowsForGroup] of byGroup) { const value = measureValue(rowsForGroup, measure); const people: ReportPerson[] = rowsForGroup.map((e) => ({ id: e.id, name: `${e.first_name} ${e.last_name}`, title: e.job_title, team: e.team_id ? (lookups.teamName.get(e.team_id) ?? "–") : "–", entry_date: e.entry_date, })); const row: ReportRow = { key, value, count: rowsForGroup.length, people }; if (split) { const bySplit = new Map(); for (const e of rowsForGroup) { const sKey = groupKeyFor(e, split, lookups); if (!bySplit.has(sKey)) bySplit.set(sKey, []); bySplit.get(sKey)!.push(e); } row.split = Array.from(bySplit.entries()).map(([sKey, sRows]) => ({ key: sKey, value: measureValue(sRows, measure), count: sRows.length, })); } rows.push(row); } return rows.sort((a, b) => b.value - a.value); } export const REPORT_PRESETS: { name: string; measure: Measure; group: GroupDimension; split?: GroupDimension }[] = [ { name: "Headcount nach Bereich", measure: "headcount", group: "division" }, { name: "Frauenanteil nach Bereich", measure: "female_share", group: "division" }, { name: "Ø Gehalt nach Paygrade", measure: "avg_salary", group: "paygrade" }, { name: "Teilzeitquote nach Standort", measure: "parttime_rate", group: "location" }, { name: "Eintritte nach Bereich", measure: "hires", group: "division" }, { name: "Austritte nach Abteilung", measure: "exits", group: "department" }, ];