The database moved to a container of our own; the platform is gone. This takes out what was left of it — and, where the leftovers were load bearing, moves rather than deletes. Moved, not deleted: supabase/migrations/ -> db/migrations/ the schema's source of truth supabase/build-org.ts -> scripts/build-org.ts lib/supabase/types.ts -> lib/types.ts 52 import sites repointed The bookkeeping needed care. It lived in `supabase_migrations.schema_migrations`, and simply renaming the schema would have left the runner facing an empty table: it would have called all 67 migrations pending and replayed them against a database that is long since current. So the runner now creates `migrationen.schema_migrations` and, once, copies the old rows across — guarded so a second run does nothing and a fresh database skips it entirely. Only then does migration 20260907100000 drop the old schema. Deleted: the CLI config, the seed, the historical schema/function dumps (nothing read them), scripts/umzug-von-supabase.sh (the move is done), and both Supabase packages plus the CLI. Nothing in the application imported them — the build now succeeds with no environment variables at all, which is the proof. Integration tests: six of them signed in through Supabase Auth and asserted against the anon key and the service role. That model is gone, so the tests were not portable — they are deleted. session-context and employee-status-filter already ran on pg and are untouched; om-reporting is ported to a direct connection because it guards a real risk (the reporting line rule exists twice, once in SQL and once in TypeScript). CI: the integration job started a Supabase stack. It now runs a postgres service, applies deploy/db-init and every migration to an empty database — that was the valuable part, and it still holds — then checks that a second run is a no-op, which is what proves the bookkeeping works. Docs: security-review.md audited a service-role key, a cookie adapter and auth.users, none of which exist. Restating findings about removed components would suggest today's system had been reviewed; it has not. It now records what was removed and says a fresh review is due. data-model.md was already marked obsolete and described the pre-OM schema; azure-migration.md was a plan for a route not taken. Both deleted. Verified: npm ci, typecheck, lint, 445 tests, build — all clean without the packages. Integration tests skip cleanly with no database. Migration SQL and the runner are reviewed but NOT executed: no Docker here, and the old instance no longer resolves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
150 lines
5.0 KiB
TypeScript
150 lines
5.0 KiB
TypeScript
// 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<string, BuiltPosition[]>;
|
|
};
|
|
|
|
// Nummernkreise wie im Altmodell, damit die Nummern der Einheiten über den
|
|
// Umstieg hinweg wiedererkennbar bleiben.
|
|
const PREFIX: Record<OrgUnitType, string> = {
|
|
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<string, BuiltPosition[]>();
|
|
|
|
// 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<string, string>();
|
|
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 };
|
|
}
|