// Baut den Organisationsbaum im SAP-OM-Modell aus der fachlichen // Bereichsdefinition des Seeds. // // Bewusst frei von Zufall, Datenbank und IDs aus der Umgebung: die // Konstruktion ist die Stelle, an der sich Nummernkreise, Leitungsplanstellen // und die Elternbeziehung falsch verdrahten lassen, ohne dass es jemandem // auffällt — ein Team unter dem falschen Bereich sieht im Organigramm // plausibel aus. Deshalb ist sie eine reine Funktion mit Tests. export type OrgUnitType = "Gesellschaft" | "Bereich" | "Abteilung" | "Team"; export type TeamDef = { name: string; leadTitle: string; icTitles: string[]; baseSize: number }; export type DeptDef = { name: string; leadTitle: string; teams: TeamDef[] }; export type DivisionDef = { name: string; headTitle: string; departments: DeptDef[] }; export type BuiltUnit = { id: string; org_number: string; name: string; parent_id: string | null; unit_type: OrgUnitType; }; export type BuiltJob = { id: string; code: string; title: string }; export type BuiltPosition = { id: string; position_number: string; org_unit_id: string; job_id: string; is_chief: boolean; }; export type BuiltOrg = { units: BuiltUnit[]; jobs: BuiltJob[]; positions: BuiltPosition[]; /** Für jedes Team die Planstellen der Mitarbeitenden, in Reihenfolge. */ icPositionsByTeam: Map; }; // Nummernkreise wie im Altmodell, damit die Nummern der Einheiten über den // Umstieg hinweg wiedererkennbar bleiben. const PREFIX: Record = { Gesellschaft: "10", Bereich: "20", Abteilung: "21", Team: "22", }; function orgNumber(type: OrgUnitType, counter: number): string { return `${PREFIX[type]}${String(counter).padStart(6, "0")}`; } /** Planstellennummern folgen dem bestehenden Muster ^6\d{7}$. */ function positionNumber(counter: number): string { return `6${String(counter).padStart(7, "0")}`; } function jobCode(counter: number): string { return `J${String(counter).padStart(4, "0")}`; } /** * `newId` wird hereingereicht, damit die Funktion in Tests deterministisch * bleibt und im Seed randomUUID benutzt. */ export function buildOrg( companyName: string, divisions: DivisionDef[], newId: () => string ): BuiltOrg { const units: BuiltUnit[] = []; const positions: BuiltPosition[] = []; const icPositionsByTeam = new Map(); // Job-Katalog: gleiche Tätigkeit, ein Eintrag. Vorher war job_title // Freitext je Person, weshalb sich Tätigkeiten nicht auswerten liessen. const jobIdByTitle = new Map(); const jobs: BuiltJob[] = []; function jobFor(title: string): string { const existing = jobIdByTitle.get(title); if (existing) return existing; const job = { id: newId(), code: jobCode(jobs.length + 1), title }; jobs.push(job); jobIdByTitle.set(title, job.id); return job.id; } let unitCounter = { Gesellschaft: 0, Bereich: 0, Abteilung: 0, Team: 0 }; let positionCounter = 0; function addUnit(name: string, type: OrgUnitType, parentId: string | null): BuiltUnit { unitCounter = { ...unitCounter, [type]: unitCounter[type] + 1 }; const unit: BuiltUnit = { id: newId(), org_number: orgNumber(type, unitCounter[type] * (type === "Team" ? 1000 : type === "Abteilung" ? 10000 : 100000)), name, parent_id: parentId, unit_type: type, }; units.push(unit); return unit; } function addPosition(unitId: string, title: string, isChief: boolean): BuiltPosition { positionCounter += 1; const position: BuiltPosition = { id: newId(), position_number: positionNumber(positionCounter), org_unit_id: unitId, job_id: jobFor(title), is_chief: isChief, }; positions.push(position); return position; } const company = addUnit(companyName, "Gesellschaft", null); addPosition(company.id, "Geschäftsführer:in", true); // Die Assistenz hängt an der Gesellschaft, führt sie aber nicht — genau // die Unterscheidung, die es im Altmodell nicht gab. addPosition(company.id, "Assistenz der Geschäftsführung", false); for (const div of divisions) { const bereich = addUnit(div.name, "Bereich", company.id); addPosition(bereich.id, div.headTitle, true); for (const dept of div.departments) { const abteilung = addUnit(dept.name, "Abteilung", bereich.id); // Die Ebene, die im Altmodell gefehlt hat: eine Abteilung hat jetzt // eine eigene Leitungsplanstelle. addPosition(abteilung.id, dept.leadTitle, true); for (const team of dept.teams) { const teamUnit = addUnit(team.name, "Team", abteilung.id); addPosition(teamUnit.id, team.leadTitle, true); const size = Math.max(1, team.baseSize); const icPositions = Array.from({ length: size }, (_, i) => addPosition(teamUnit.id, team.icTitles[i % team.icTitles.length], false) ); icPositionsByTeam.set(teamUnit.id, icPositions); } } } return { units, jobs, positions, icPositionsByTeam }; }