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>
163 lines
6.2 KiB
TypeScript
163 lines
6.2 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { buildOrg, type DivisionDef } from "@/scripts/build-org";
|
|
|
|
// Die Konstruktion des Org-Baums ist die Stelle, an der sich Elternbezüge
|
|
// und Leitungsplanstellen falsch verdrahten lassen, ohne dass es auffällt:
|
|
// ein Team unter dem falschen Bereich sieht im Organigramm plausibel aus.
|
|
|
|
const DIVISIONS: DivisionDef[] = [
|
|
{
|
|
name: "Produktion",
|
|
headTitle: "Bereichsleitung Produktion",
|
|
departments: [
|
|
{
|
|
name: "Fertigung",
|
|
leadTitle: "Abteilungsleitung Fertigung",
|
|
teams: [
|
|
{ name: "Montage", leadTitle: "Teamleitung Montage", icTitles: ["Monteur:in", "Anlagenführer:in"], baseSize: 3 },
|
|
{ name: "CNC", leadTitle: "Teamleitung CNC", icTitles: ["CNC-Fräser:in"], baseSize: 2 },
|
|
],
|
|
},
|
|
{
|
|
name: "Instandhaltung",
|
|
leadTitle: "Abteilungsleitung Instandhaltung",
|
|
teams: [{ name: "Mechanik", leadTitle: "Teamleitung Mechanik", icTitles: ["Schlosser:in"], baseSize: 2 }],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "IT",
|
|
headTitle: "Bereichsleitung IT",
|
|
departments: [
|
|
{
|
|
name: "Infrastruktur",
|
|
leadTitle: "Abteilungsleitung Infrastruktur",
|
|
teams: [{ name: "IT-Support", leadTitle: "Teamleitung IT-Support", icTitles: ["Systemadministrator:in"], baseSize: 2 }],
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
function build() {
|
|
let n = 0;
|
|
return buildOrg("Alpenwerk Industrie GmbH", DIVISIONS, () => `id-${++n}`);
|
|
}
|
|
|
|
describe("Struktur", () => {
|
|
it("hat genau eine Wurzel, und das ist die Gesellschaft", () => {
|
|
const { units } = build();
|
|
const roots = units.filter((u) => u.parent_id === null);
|
|
expect(roots).toHaveLength(1);
|
|
expect(roots[0].unit_type).toBe("Gesellschaft");
|
|
});
|
|
|
|
it("hängt jede Einheit unter den richtigen Typ", () => {
|
|
const { units } = build();
|
|
const byId = new Map(units.map((u) => [u.id, u]));
|
|
const erwartetesElternteil = { Bereich: "Gesellschaft", Abteilung: "Bereich", Team: "Abteilung" } as const;
|
|
|
|
for (const u of units) {
|
|
if (u.unit_type === "Gesellschaft") continue;
|
|
const parent = byId.get(u.parent_id!);
|
|
expect(parent?.unit_type, `${u.name} hängt falsch`).toBe(erwartetesElternteil[u.unit_type]);
|
|
}
|
|
});
|
|
|
|
it("baut die Ebenen vollständig auf", () => {
|
|
const { units } = build();
|
|
const zahl = (t: string) => units.filter((u) => u.unit_type === t).length;
|
|
expect(zahl("Gesellschaft")).toBe(1);
|
|
expect(zahl("Bereich")).toBe(2);
|
|
expect(zahl("Abteilung")).toBe(3);
|
|
expect(zahl("Team")).toBe(4);
|
|
});
|
|
|
|
it("führt jede Einheit über parent_id auf die Wurzel zurück", () => {
|
|
// Ein abgehängter Teilbaum würde im Organigramm nie gerendert.
|
|
const { units } = build();
|
|
const byId = new Map(units.map((u) => [u.id, u]));
|
|
for (const start of units) {
|
|
const gesehen = new Set<string>();
|
|
let cur = start;
|
|
while (cur.parent_id && !gesehen.has(cur.id)) {
|
|
gesehen.add(cur.id);
|
|
cur = byId.get(cur.parent_id)!;
|
|
}
|
|
expect(cur.parent_id, `${start.name} erreicht die Wurzel nicht`).toBeNull();
|
|
}
|
|
});
|
|
|
|
it("vergibt eindeutige Org-Nummern im richtigen Nummernkreis", () => {
|
|
const { units } = build();
|
|
expect(new Set(units.map((u) => u.org_number)).size).toBe(units.length);
|
|
const prefix = { Gesellschaft: "10", Bereich: "20", Abteilung: "21", Team: "22" } as const;
|
|
for (const u of units) expect(u.org_number.startsWith(prefix[u.unit_type]), u.name).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("Planstellen", () => {
|
|
it("gibt jeder Einheit genau eine Leitungsplanstelle", () => {
|
|
// Der Unique-Index in der Datenbank erzwingt das ebenfalls; hier soll
|
|
// der Seed gar nicht erst dagegenlaufen.
|
|
const { units, positions } = build();
|
|
for (const u of units) {
|
|
const chiefs = positions.filter((p) => p.org_unit_id === u.id && p.is_chief);
|
|
expect(chiefs, `${u.name}`).toHaveLength(1);
|
|
}
|
|
});
|
|
|
|
it("legt auch für die Abteilung eine Leitung an", () => {
|
|
// Die Ebene, die im Altmodell gefehlt hat.
|
|
const { units, positions } = build();
|
|
const abteilungen = units.filter((u) => u.unit_type === "Abteilung");
|
|
expect(abteilungen.length).toBeGreaterThan(0);
|
|
for (const a of abteilungen) {
|
|
expect(positions.some((p) => p.org_unit_id === a.id && p.is_chief)).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("erzeugt für jedes Team so viele Mitarbeiter-Planstellen wie vorgesehen", () => {
|
|
const { units, icPositionsByTeam } = build();
|
|
const montage = units.find((u) => u.name === "Montage")!;
|
|
expect(icPositionsByTeam.get(montage.id)).toHaveLength(3);
|
|
expect(icPositionsByTeam.get(montage.id)!.every((p) => !p.is_chief)).toBe(true);
|
|
});
|
|
|
|
it("vergibt eindeutige Planstellennummern nach dem bestehenden Muster", () => {
|
|
const { positions } = build();
|
|
expect(new Set(positions.map((p) => p.position_number)).size).toBe(positions.length);
|
|
for (const p of positions) expect(p.position_number).toMatch(/^6\d{7}$/);
|
|
});
|
|
|
|
it("hängt jede Planstelle an eine existierende Einheit", () => {
|
|
const { units, positions } = build();
|
|
const ids = new Set(units.map((u) => u.id));
|
|
for (const p of positions) expect(ids.has(p.org_unit_id), p.position_number).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("Job-Katalog", () => {
|
|
it("führt jede Tätigkeit genau einmal", () => {
|
|
// "Schlosser:in" darf nicht als zwei Einträge existieren, sonst ist eine
|
|
// Auswertung nach Tätigkeit wertlos.
|
|
const { jobs } = build();
|
|
expect(new Set(jobs.map((j) => j.title)).size).toBe(jobs.length);
|
|
expect(new Set(jobs.map((j) => j.code)).size).toBe(jobs.length);
|
|
});
|
|
|
|
it("teilt denselben Job über mehrere Planstellen", () => {
|
|
const { positions, jobs } = build();
|
|
const jobById = new Map(jobs.map((j) => [j.id, j]));
|
|
const monteur = jobs.find((j) => j.title === "Monteur:in")!;
|
|
const stellen = positions.filter((p) => p.job_id === monteur.id);
|
|
expect(stellen.length).toBeGreaterThan(1);
|
|
expect(jobById.get(stellen[0].job_id)!.title).toBe("Monteur:in");
|
|
});
|
|
|
|
it("verweist jede Planstelle auf einen existierenden Job", () => {
|
|
const { positions, jobs } = build();
|
|
const ids = new Set(jobs.map((j) => j.id));
|
|
for (const p of positions) expect(ids.has(p.job_id), p.position_number).toBe(true);
|
|
});
|
|
});
|