Die Blaetter kamen aus `root.children`, also aus den unmittelbaren Kindern der Gesellschaft. Das stimmte, solange die Bereiche dort hingen. Beim Kunden liegt "CEO" dazwischen und ist selbst ein Bereich: die Auswahl bot einen einzigen Eintrag an, und aus 784 Personen wurde die Uebersicht plus ein Blatt fuer CEO. Genommen wird jetzt jeder Bereich, unter dem kein weiterer liegt -- nach dem Etikett und nicht nach der Stellung im Baum. Nur nach dem Etikett zu gehen genuegte nicht: CEO kaeme als eigenes Blatt dazu und enthielte die sechs anderen noch einmal. Die Uebersicht haengte die gewaehlten Bereiche unmittelbar unter die Gesellschaft und haette CEO damit stillschweigend uebersprungen. Sie zeichnet jetzt den echten Weg von oben, beschnitten auf die Aeste mit einem gewaehlten Bereich, und geht so tief, wie dieser Baum reicht. Das Ab- und Anwaehlen wirkt weiterhin bis in die Uebersicht. Bei der Gesellschaft entfaellt "Leitung unbesetzt": sie wird nicht gefuehrt, sondern ist das Ganze. Ueberall sonst bleibt der Vermerk.
176 lines
6.3 KiB
TypeScript
176 lines
6.3 KiB
TypeScript
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<string, PrintUnit>();
|
|
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,
|
|
},
|
|
};
|
|
}
|