import type { OrgEmployee, OrgUnitNode, OrgVacancy } from "@/components/orgchart/types"; import { fmtName } from "@/lib/format"; // Das Modell für den Druck. // // Der Bildschirm zeigt das Organigramm als Fläche, in die man hineinzoomt. // Papier kann das nicht: es hat eine feste Grösse, und was darauf nicht // lesbar ist, ist verloren. Ein Ausdruck der Leinwand — 850 Personen auf eine // Seite skaliert — ergibt Kästchen von zwei Millimetern. // // Deshalb wird für den Druck **umgebaut statt verkleinert**: eine Übersicht // über die obersten Ebenen, und danach ein Bereich je Seite als eigener, // vollständiger Baum. Jede Seite ist für sich verständlich und trägt ihre // Überschrift — auch wenn jemand nur Blatt sieben aus dem Drucker nimmt. // // Der Baum bleibt hier ein Baum: `children` und `totalPeople` sind das, woraus // die Auswahl („welche Bereiche, wie tief") und die gezeichnete Hierarchie // gleichermassen entstehen. export type PrintPerson = { id: string; /** Getrennt mitgeführt, weil danach sortiert wird — siehe nachName. */ lastName: string; name: string; jobTitle: string; personnelNumber: number; isChief: boolean; absent: boolean; absenceType: string | null; }; export type PrintUnit = { id: string; name: string; unitType: OrgUnitNode["unit_type"]; orgNumber: string; /** Die Leitung dieser Einheit, falls besetzt. */ chief: PrintPerson | null; /** Alle übrigen Personen, die unmittelbar in dieser Einheit sitzen. */ members: PrintPerson[]; /** Unbesetzte Planstellen dieser Einheit. */ vacancies: { positionNumber: string; jobTitle: string; isChief: boolean }[]; children: PrintUnit[]; /** Personen in dieser Einheit und allem darunter — für die Übersicht. */ totalPeople: number; }; export type PrintModel = { root: PrintUnit | null; /** Je Bereich eine Seite. Bei nur einer Ebene bleibt es bei der Übersicht. */ divisions: PrintUnit[]; totals: { people: number; vacancies: number; units: number }; }; /** Liegt unterhalb dieser Einheit noch ein Bereich? */ function enthaeltBereich(unit: PrintUnit): boolean { return unit.children.some((k) => k.unitType === "Bereich" || enthaeltBereich(k)); } function personFrom(e: OrgEmployee): PrintPerson { return { id: e.id, lastName: e.last_name, name: fmtName(e.first_name, e.last_name), jobTitle: e.job_title, personnelNumber: e.personnel_number, isChief: e.is_chief, absent: e.absent, absenceType: e.absence_type, }; } /** * Nachname, dann Vorname — wie in jeder Liste, die jemand durchsucht. * * Der Nachname kommt aus dem eigenen Feld, nicht aus dem zusammengesetzten * Namen. Vorher wurde das letzte Wort genommen, was bei „Hannah Winkler" * zufällig stimmte; seit die Anzeige „Winkler, Hannah" lautet, wäre das * letzte Wort der Vorname, und die Liste stünde nach Vornamen sortiert da. */ function nachName(a: PrintPerson, b: PrintPerson): number { return a.lastName.localeCompare(b.lastName, "de") || a.name.localeCompare(b.name, "de"); } export function buildPrintModel( units: OrgUnitNode[], employees: OrgEmployee[], vacancies: OrgVacancy[] ): PrintModel { const byUnit = new Map(); for (const u of units) { byUnit.set(u.id, { id: u.id, name: u.name, unitType: u.unit_type, orgNumber: u.org_number, chief: null, members: [], vacancies: [], children: [], totalPeople: 0, }); } for (const e of employees) { const unit = byUnit.get(e.org_unit_id); if (!unit) continue; const p = personFrom(e); // Die Leitung steht oben und nicht in der Namensliste — sie ist die // Antwort auf „wer führt das hier", und die soll man nicht suchen. if (p.isChief && !unit.chief) unit.chief = p; else unit.members.push(p); } for (const v of vacancies) { const unit = byUnit.get(v.org_unit_id); if (!unit) continue; unit.vacancies.push({ positionNumber: v.position_number, jobTitle: v.job_title, isChief: v.is_chief }); } // Bäume verknüpfen, in der Reihenfolge der Orgnummern — dieselbe wie // überall sonst in der Anwendung. const sortiert = [...units].sort((a, b) => a.org_number.localeCompare(b.org_number, "de")); let root: PrintUnit | null = null; for (const u of sortiert) { const node = byUnit.get(u.id)!; if (u.parent_id && byUnit.has(u.parent_id)) byUnit.get(u.parent_id)!.children.push(node); else if (u.unit_type === "Gesellschaft") root = node; } for (const unit of byUnit.values()) { unit.members.sort(nachName); unit.vacancies.sort((a, b) => a.jobTitle.localeCompare(b.jobTitle, "de")); } // Summen von unten nach oben, über beliebig viele Ebenen. `unit_type` ist // ein Etikett und begrenzt die Tiefe nicht — hier stand einmal das // Gegenteil. function zaehle(unit: PrintUnit): number { const eigene = (unit.chief ? 1 : 0) + unit.members.length; unit.totalPeople = eigene + unit.children.reduce((s, c) => s + zaehle(c), 0); return unit.totalPeople; } if (root) zaehle(root); else for (const u of byUnit.values()) if (!units.find((x) => x.id === u.id)?.parent_id) zaehle(u); // Ein Blatt je Bereich — und zwar je Bereich, unter dem kein weiterer mehr // liegt. // // Hier stand `root.children`, also „die unmittelbaren Kinder der // Gesellschaft". Das stimmte, solange die Bereiche unmittelbar darunter // hingen. Bei Manner liegt „CEO" dazwischen und ist selbst ein Bereich: die // Auswahl bot genau einen Eintrag an, und der Ausdruck bestand aus der // Übersicht plus einem einzigen Blatt für CEO. // // Nach dem Etikett zu gehen allein genügt nicht — dann käme CEO als siebtes // Blatt dazu und enthielte die sechs anderen noch einmal. Deshalb bleiben // nur die untersten Bereiche übrig. Im alten Modell ändert das nichts: dort // liegt unter einem Bereich nie ein zweiter. const bereiche = [...byUnit.values()] .filter((u) => u.unitType === "Bereich" && !enthaeltBereich(u)) .sort((a, b) => a.orgNumber.localeCompare(b.orgNumber, "de")); // Ohne jedes Etikett „Bereich" bliebe sonst nichts zum Drucken übrig. const divisions = bereiche.length > 0 ? bereiche : root ? root.children : []; return { root, divisions, totals: { people: employees.length, vacancies: vacancies.length, units: units.length, }, }; }