// Seeds ~800 realistic Austrian/DACH employees + org structure + a handful of // open positions, per spec §5. // // Run with: node --env-file=.env.local supabase/seed.ts // Uses the service-role key over the Supabase REST API (bypasses RLS) — no // direct Postgres connection needed. All row ids are generated client-side // so parent/child references never require a round-trip. import { createClient } from "@supabase/supabase-js"; import { randomUUID } from "node:crypto"; const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL; const SERVICE_ROLE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY; if (!SUPABASE_URL || !SERVICE_ROLE_KEY) { throw new Error("Missing NEXT_PUBLIC_SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY in the environment"); } const ADMIN_EMAIL = "m.stubhan@loudspring.at"; const MANAGER_TEST_EMAIL = "manager-test@test.manner.at"; const supabase = createClient(SUPABASE_URL, SERVICE_ROLE_KEY, { auth: { autoRefreshToken: false, persistSession: false }, }); // ── RNG helpers ────────────────────────────────────────────── function randInt(min: number, max: number): number { return Math.floor(Math.random() * (max - min + 1)) + min; } function pick(arr: readonly T[]): T { return arr[randInt(0, arr.length - 1)]; } function chance(probability: number): boolean { return Math.random() < probability; } function weightedPick(entries: readonly (readonly [T, number])[]): T { const total = entries.reduce((sum, [, w]) => sum + w, 0); let r = Math.random() * total; for (const [value, w] of entries) { r -= w; if (r <= 0) return value; } return entries[entries.length - 1][0]; } function addDays(d: Date, days: number): Date { const r = new Date(d); r.setDate(r.getDate() + days); return r; } function isoDate(d: Date): string { return d.toISOString().slice(0, 10); } function randomDateBetween(start: Date, end: Date): Date { const t = start.getTime() + Math.random() * (end.getTime() - start.getTime()); return new Date(t); } function slugify(s: string): string { return s .toLowerCase() .replace(/ä/g, "ae") .replace(/ö/g, "oe") .replace(/ü/g, "ue") .replace(/ß/g, "ss") .replace(/[^a-z0-9]+/g, ""); } const TODAY = new Date(); // ── Name pools ─────────────────────────────────────────────── const MALE_FIRST_NAMES = [ "Michael", "Andreas", "Thomas", "Stefan", "Christian", "Martin", "Markus", "Daniel", "Christoph", "Alexander", "Wolfgang", "Peter", "Josef", "Franz", "Johann", "Georg", "Bernhard", "Florian", "Manuel", "Philipp", "Sebastian", "Patrick", "Dominik", "Simon", "Lukas", "David", "Matthias", "Robert", "Gerhard", "Helmut", "Karl", "Anton", "Rudolf", "Herbert", "Kurt", "Werner", "Erwin", "Hannes", "Fabian", "Julian", ]; const FEMALE_FIRST_NAMES = [ "Maria", "Anna", "Sabine", "Andrea", "Claudia", "Petra", "Julia", "Sarah", "Lisa", "Nicole", "Christine", "Elisabeth", "Monika", "Barbara", "Karin", "Silvia", "Martina", "Susanne", "Katharina", "Eva", "Michaela", "Stephanie", "Verena", "Melanie", "Sandra", "Birgit", "Ingrid", "Renate", "Gabriele", "Brigitte", "Theresa", "Laura", "Hannah", "Johanna", "Magdalena", "Carina", "Vanessa", "Nadine", "Bettina", "Ursula", ]; const LAST_NAMES = [ "Gruber", "Huber", "Bauer", "Wagner", "Müller", "Pichler", "Steiner", "Moser", "Mayer", "Hofer", "Leitner", "Berger", "Fuchs", "Eder", "Fischer", "Schmid", "Winkler", "Weber", "Schwarz", "Maier", "Schneider", "Reiter", "Mayr", "Wolf", "Aigner", "Lang", "Baumgartner", "Auer", "Brunner", "Wallner", "Wimmer", "Egger", "Binder", "Wieser", "Höller", "Schmidt", "Riegler", "Kaiser", "Lechner", "Kogler", "Peer", "Lehner", "Zimmermann", "Pöll", "Haas", "Novak", "Horvat", "Vukovic", "Yilmaz", "Demir", "Kovac", "Nemec", "Simic", "Kralj", "Toth", "Szabo", ]; const NATIONALITIES: readonly (readonly [string, number])[] = [ ["Österreich", 75], ["Deutschland", 5], ["Tschechien", 3], ["Slowenien", 2], ["Türkei", 5], ["Serbien", 3], ["Kroatien", 3], ["Bosnien", 2], ["Ungarn", 2], ]; function addressCountryFor(nationality: string): string { return ["Österreich", "Deutschland", "Tschechien", "Slowenien"].includes(nationality) ? nationality : "Andere"; } const STREETS = ["Hauptstraße", "Bahnhofstraße", "Schulgasse", "Kirchenplatz", "Gartenweg", "Industriestraße", "Ringstraße", "Feldweg"]; const EXIT_REASONS = [ "Einvernehmliche Auflösung", "Kündigung AN", "Kündigung AG", "Befristungsablauf", "Pensionierung", "Entlassung", ]; // ── Locations ──────────────────────────────────────────────── const LOCATIONS = [ { id: randomUUID(), name: "Wien-Hernals", country: "Österreich" }, { id: randomUUID(), name: "Wolkersdorf", country: "Österreich" }, { id: randomUUID(), name: "Köln", country: "Deutschland" }, { id: randomUUID(), name: "Brünn", country: "Tschechien" }, { id: randomUUID(), name: "Ljubljana", country: "Slowenien" }, ] as const; const LOCATION_WEIGHTS: readonly (readonly [(typeof LOCATIONS)[number], number])[] = [ [LOCATIONS[0], 55], [LOCATIONS[1], 20], [LOCATIONS[2], 10], [LOCATIONS[3], 10], [LOCATIONS[4], 5], ]; // ── Org structure ──────────────────────────────────────────── type TeamDef = { name: string; leadTitle: string; icTitles: string[]; baseSize: number }; type DeptDef = { name: string; teams: TeamDef[] }; type DivisionDef = { name: string; headTitle: string; departments: DeptDef[] }; const SCALE = 1.44; // brings the ~556-person base roster up to ~800 const DIVISIONS: DivisionDef[] = [ { name: "Produktion", headTitle: "Bereichsleitung Produktion", departments: [ { name: "Fertigung", teams: [ { name: "Montage", leadTitle: "Teamleitung Montage", icTitles: ["Maschinenbediener:in", "Montagemitarbeiter:in", "Anlagenführer:in"], baseSize: 45 }, { name: "CNC-Fertigung", leadTitle: "Teamleitung CNC-Fertigung", icTitles: ["CNC-Fräser:in", "CNC-Dreher:in", "Zerspanungstechniker:in"], baseSize: 35 }, { name: "Qualitätssicherung Fertigung", leadTitle: "Teamleitung Qualitätssicherung Fertigung", icTitles: ["Qualitätsprüfer:in", "Messtechniker:in"], baseSize: 22 }, ], }, { name: "Instandhaltung", teams: [ { name: "Elektrotechnik", leadTitle: "Teamleitung Elektrotechnik", icTitles: ["Elektrotechniker:in", "Automatisierungstechniker:in"], baseSize: 24 }, { name: "Mechanik", leadTitle: "Teamleitung Mechanik", icTitles: ["Industriemechaniker:in", "Schlosser:in"], baseSize: 22 }, ], }, ], }, { name: "Logistik & Einkauf", headTitle: "Bereichsleitung Logistik & Einkauf", departments: [ { name: "Logistik", teams: [ { name: "Lager", leadTitle: "Teamleitung Lager", icTitles: ["Lagerlogistiker:in", "Staplerfahrer:in", "Kommissionierer:in"], baseSize: 30 }, { name: "Versand", leadTitle: "Teamleitung Versand", icTitles: ["Versandmitarbeiter:in", "Speditionskaufmann/-frau"], baseSize: 20 }, { name: "Fuhrpark", leadTitle: "Teamleitung Fuhrpark", icTitles: ["Berufskraftfahrer:in", "Fuhrparkdisponent:in"], baseSize: 16 }, ], }, { name: "Einkauf", teams: [ { name: "Strategischer Einkauf", leadTitle: "Teamleitung Strategischer Einkauf", icTitles: ["Einkäufer:in", "Category Manager:in"], baseSize: 14 }, { name: "Operativer Einkauf", leadTitle: "Teamleitung Operativer Einkauf", icTitles: ["Operative:r Einkäufer:in", "Bestelldisponent:in"], baseSize: 14 }, ], }, ], }, { name: "Vertrieb & Marketing", headTitle: "Bereichsleitung Vertrieb & Marketing", departments: [ { name: "Vertrieb", teams: [ { name: "Key Account Management", leadTitle: "Teamleitung Key Account Management", icTitles: ["Key Account Manager:in", "Sales Manager:in"], baseSize: 16 }, { name: "Außendienst", leadTitle: "Teamleitung Außendienst", icTitles: ["Außendienstmitarbeiter:in", "Gebietsverkaufsleiter:in"], baseSize: 22 }, { name: "Vertriebsinnendienst", leadTitle: "Teamleitung Vertriebsinnendienst", icTitles: ["Vertriebsinnendienstmitarbeiter:in", "Auftragssachbearbeiter:in"], baseSize: 18 }, ], }, { name: "Marketing", teams: [ { name: "Brand Marketing", leadTitle: "Teamleitung Brand Marketing", icTitles: ["Brand Manager:in", "Produktmanager:in"], baseSize: 12 }, { name: "Digital Marketing", leadTitle: "Teamleitung Digital Marketing", icTitles: ["Digital Marketing Manager:in", "Social-Media-Manager:in"], baseSize: 12 }, ], }, ], }, { name: "Forschung & Entwicklung", headTitle: "Bereichsleitung Forschung & Entwicklung", departments: [ { name: "Produktentwicklung", teams: [ { name: "Rezeptur & Sensorik", leadTitle: "Teamleitung Rezeptur & Sensorik", icTitles: ["Lebensmitteltechniker:in", "Sensoriker:in"], baseSize: 16 }, { name: "Verpackungsentwicklung", leadTitle: "Teamleitung Verpackungsentwicklung", icTitles: ["Verpackungstechniker:in", "Packmittelentwickler:in"], baseSize: 12 }, ], }, { name: "Verfahrenstechnik", teams: [ { name: "Prozessoptimierung", leadTitle: "Teamleitung Prozessoptimierung", icTitles: ["Verfahrenstechniker:in", "Prozessingenieur:in"], baseSize: 14 }, { name: "Anlagentechnik", leadTitle: "Teamleitung Anlagentechnik", icTitles: ["Anlagentechniker:in", "Projektingenieur:in"], baseSize: 12 }, ], }, ], }, { name: "Qualitätsmanagement", headTitle: "Bereichsleitung Qualitätsmanagement", departments: [ { name: "Qualitätssicherung", teams: [ { name: "Wareneingangsprüfung", leadTitle: "Teamleitung Wareneingangsprüfung", icTitles: ["Qualitätsprüfer:in", "Wareneingangskontrolleur:in"], baseSize: 14 }, { name: "Prozessaudit", leadTitle: "Teamleitung Prozessaudit", icTitles: ["Qualitätsauditor:in", "QM-Beauftragte:r"], baseSize: 10 }, ], }, { name: "Lebensmittelsicherheit", teams: [ { name: "Hygienemanagement", leadTitle: "Teamleitung Hygienemanagement", icTitles: ["Hygienebeauftragte:r", "Lebensmittelsicherheitsbeauftragte:r"], baseSize: 12 }, { name: "Zertifizierung", leadTitle: "Teamleitung Zertifizierung", icTitles: ["Zertifizierungsmanager:in", "QM-Sachbearbeiter:in"], baseSize: 10 }, ], }, ], }, { name: "IT", headTitle: "Bereichsleitung IT", departments: [ { name: "Business Applications", teams: [ { name: "SAP-Team", leadTitle: "Teamleitung SAP-Team", icTitles: ["SAP-Consultant", "SAP-Entwickler:in"], baseSize: 12 }, { name: "Power Platform & Automatisierung", leadTitle: "Teamleitung Power Platform & Automatisierung", icTitles: ["Power Platform Developer:in", "Prozessautomatisierer:in"], baseSize: 10 }, ], }, { name: "Infrastruktur", teams: [ { name: "Netzwerk & Security", leadTitle: "Teamleitung Netzwerk & Security", icTitles: ["Netzwerktechniker:in", "IT-Security-Spezialist:in"], baseSize: 12 }, { name: "IT-Support", leadTitle: "Teamleitung IT-Support", icTitles: ["IT-Support-Mitarbeiter:in", "Systemadministrator:in"], baseSize: 14 }, ], }, ], }, { name: "Finanzen & Controlling", headTitle: "Bereichsleitung Finanzen & Controlling", departments: [ { name: "Finanzen", teams: [ { name: "Buchhaltung", leadTitle: "Teamleitung Buchhaltung", icTitles: ["Buchhalter:in", "Bilanzbuchhalter:in"], baseSize: 16 }, { name: "Treasury", leadTitle: "Teamleitung Treasury", icTitles: ["Treasury-Manager:in", "Finanzanalyst:in"], baseSize: 10 }, ], }, { name: "Controlling", teams: [ { name: "Konzerncontrolling", leadTitle: "Teamleitung Konzerncontrolling", icTitles: ["Controller:in", "Financial Analyst:in"], baseSize: 12 }, { name: "Werkscontrolling", leadTitle: "Teamleitung Werkscontrolling", icTitles: ["Werkscontroller:in", "Kostenrechner:in"], baseSize: 12 }, ], }, ], }, { name: "Human Resources", headTitle: "Bereichsleitung Human Resources", departments: [ { name: "HR Business Partner", teams: [ { name: "Recruiting", leadTitle: "Teamleitung Recruiting", icTitles: ["Recruiter:in", "Talent Acquisition Manager:in"], baseSize: 10 }, { name: "Personalentwicklung", leadTitle: "Teamleitung Personalentwicklung", icTitles: ["Personalentwickler:in", "Trainer:in"], baseSize: 8 }, ], }, { name: "Personaladministration", teams: [ { name: "Gehaltsabrechnung", leadTitle: "Teamleitung Gehaltsabrechnung", icTitles: ["Payroll-Spezialist:in", "Personalverrechner:in"], baseSize: 10 }, { name: "HR-Systeme", leadTitle: "Teamleitung HR-Systeme", icTitles: ["HR-IT-Spezialist:in", "HRIS Manager:in"], baseSize: 8 }, ], }, ], }, ]; // ── Employee generation ────────────────────────────────────── type EmployeeRow = { id: string; first_name: string; last_name: string; gender: "m" | "w"; birth_date: string; sv_nummer: string; nationality: string; address: string; address_country: string; email: string; phone: string; team_id: string | null; division_id: string; job_title: string; location_id: string; manager_id: string | null; org_level: number; is_lead: boolean; employment_type: "Vollzeit" | "Teilzeit"; weekly_hours: number; monthly_salary_gross: number; contract_type: "unbefristet" | "befristet"; contract_end_date: string | null; paygrade: "A" | "B" | "C" | "D" | "E" | "F"; source: "Intern" | "Extern"; status: "Aktiv" | "Karenz" | "Geplant" | "Ausgetreten"; entry_date: string; exit_date: string | null; exit_reason: string | null; karenz_return_date: string | null; }; type HistoryRow = { employee_id: string; event_date: string; event_type: string; description: string; }; const usedEmails = new Set(); function makeEmail(firstName: string, lastName: string): string { const base = `${slugify(firstName)}.${slugify(lastName)}`; let email = `${base}@test.manner.at`; let n = 2; while (usedEmails.has(email)) { email = `${base}${n}@test.manner.at`; n += 1; } usedEmails.add(email); return email; } function makeSvNummer(birthDate: Date): string { const dd = String(birthDate.getDate()).padStart(2, "0"); const mm = String(birthDate.getMonth() + 1).padStart(2, "0"); const yy = String(birthDate.getFullYear()).slice(-2); const prefix = String(randInt(1000, 9999)); return `${prefix} ${dd}${mm}${yy}`; } function birthDateForAge(age: number): Date { const year = TODAY.getFullYear() - age; return new Date(year, randInt(0, 11), randInt(1, 28)); } function paygradeAndSalaryForIc(): { paygrade: EmployeeRow["paygrade"]; salary: number } { const grade = weightedPick([ ["A", 15], ["B", 35], ["C", 30], ["D", 20], ]); const ranges: Record = { A: [2200, 2700], B: [2600, 3300], C: [3200, 4100], D: [4000, 5200], }; const [min, max] = ranges[grade]; return { paygrade: grade, salary: randInt(min, max) }; } function newHireBase(jobTitle: string, orgLevel: number, isLead: boolean, teamId: string | null, divisionId: string, managerId: string | null) { const gender: "m" | "w" = chance(0.48) ? "m" : "w"; const firstName = pick(gender === "m" ? MALE_FIRST_NAMES : FEMALE_FIRST_NAMES); const lastName = pick(LAST_NAMES); const nationality = weightedPick(NATIONALITIES); const location = weightedPick(LOCATION_WEIGHTS); return { id: randomUUID(), first_name: firstName, last_name: lastName, gender, nationality, address: `${pick(STREETS)} ${randInt(1, 90)}, ${randInt(1010, 2500)} Wien`, address_country: addressCountryFor(nationality), email: makeEmail(firstName, lastName), phone: `+43 664 ${randInt(1000000, 9999999)}`, team_id: teamId, division_id: divisionId, job_title: jobTitle, location_id: location.id, manager_id: managerId, org_level: orgLevel, is_lead: isLead, }; } const employees: EmployeeRow[] = []; const history: HistoryRow[] = []; const icPoolForStatusAssignment: EmployeeRow[] = []; function finalizeEmployee(base: ReturnType, opts: { salary: number; paygrade: EmployeeRow["paygrade"] }): EmployeeRow { const age = randInt(22, 60); const birthDate = birthDateForAge(age); const maxTenureYears = Math.min(15, age - 20); const entryDate = randomDateBetween(addDays(TODAY, -maxTenureYears * 365), addDays(TODAY, -30)); const employmentType: "Vollzeit" | "Teilzeit" = chance(0.8) ? "Vollzeit" : "Teilzeit"; const weeklyHours = employmentType === "Vollzeit" ? 38.5 : pick([15, 18, 20, 25, 28, 30, 32, 35]); const isBefristet = chance(0.1) && entryDate > addDays(TODAY, -540); const contractEndDate = isBefristet ? addDays(TODAY, randInt(90, 540)) : null; const row: EmployeeRow = { ...base, birth_date: isoDate(birthDate), sv_nummer: makeSvNummer(birthDate), employment_type: employmentType, weekly_hours: weeklyHours, monthly_salary_gross: opts.salary, contract_type: isBefristet ? "befristet" : "unbefristet", contract_end_date: contractEndDate ? isoDate(contractEndDate) : null, paygrade: opts.paygrade, source: chance(0.15) ? "Intern" : "Extern", status: "Aktiv", entry_date: isoDate(entryDate), exit_date: null, exit_reason: null, karenz_return_date: null, }; history.push({ employee_id: row.id, event_date: row.entry_date, event_type: "Eintritt", description: `Eintritt als ${row.job_title}`, }); if (row.status === "Aktiv" && entryDate < addDays(TODAY, -2 * 365) && chance(0.06)) { const promoDate = randomDateBetween(addDays(entryDate, 365), addDays(TODAY, -30)); history.push({ employee_id: row.id, event_date: isoDate(promoDate), event_type: "Beförderung", description: `Beförderung im Rahmen der Laufbahnentwicklung, neue Position: ${row.job_title}`, }); } if (chance(0.08)) { const adjDate = randomDateBetween(addDays(entryDate, 180), TODAY); history.push({ employee_id: row.id, event_date: isoDate(adjDate), event_type: "Gehaltsanpassung", description: "Jährliche Gehaltsanpassung im Rahmen der Kollektivvertragsrunde", }); } return row; } type TeamRef = { id: string; org_number: string; name: string; department_id: string }; type DeptRef = { id: string; org_number: string; name: string; division_id: string }; type DivisionRef = { id: string; org_number: string; name: string }; const divisionRows: DivisionRef[] = []; const departmentRows: DeptRef[] = []; const teamRows: TeamRef[] = []; // Geschäftsführung: small division, no departments/teams — CEO and their // assistant sit directly under it (§5). const gfDivisionId = randomUUID(); divisionRows.push({ id: gfDivisionId, org_number: "20900000", name: "Geschäftsführung" }); const ceo = finalizeEmployee( newHireBase("Geschäftsführer:in", 0, true, null, gfDivisionId, null), { salary: 15500, paygrade: "F" } ); const gfAssistant = finalizeEmployee( newHireBase("Assistenz der Geschäftsführung", 3, false, null, gfDivisionId, ceo.id), { salary: 3900, paygrade: "C" } ); employees.push(ceo, gfAssistant); let divisionCounter = 0; let deptCounter = 0; let teamCounter = 0; for (const div of DIVISIONS) { divisionCounter += 1; const divisionId = randomUUID(); const divisionOrgNumber = `20${String(divisionCounter * 100000).padStart(6, "0")}`; divisionRows.push({ id: divisionId, org_number: divisionOrgNumber, name: div.name }); const divisionHead = finalizeEmployee( newHireBase(div.headTitle, 1, true, null, divisionId, ceo.id), { salary: randInt(9000, 11500), paygrade: "F" } ); employees.push(divisionHead); for (const dept of div.departments) { deptCounter += 1; const departmentId = randomUUID(); const deptOrgNumber = `21${String(deptCounter * 10000).padStart(6, "0")}`; departmentRows.push({ id: departmentId, org_number: deptOrgNumber, name: dept.name, division_id: divisionId }); for (const team of dept.teams) { teamCounter += 1; const teamId = randomUUID(); const teamOrgNumber = `22${String(teamCounter * 1000).padStart(6, "0")}`; teamRows.push({ id: teamId, org_number: teamOrgNumber, name: team.name, department_id: departmentId }); const size = Math.max(2, Math.round(team.baseSize * SCALE)); const teamLead = finalizeEmployee( newHireBase(team.leadTitle, 2, true, teamId, divisionId, divisionHead.id), { salary: randInt(5000, 6800), paygrade: "E" } ); employees.push(teamLead); for (let i = 0; i < size - 1; i++) { const jobTitle = pick(team.icTitles); const { paygrade, salary } = paygradeAndSalaryForIc(); const ic = finalizeEmployee( newHireBase(jobTitle, 3, false, teamId, divisionId, teamLead.id), { salary, paygrade } ); employees.push(ic); icPoolForStatusAssignment.push(ic); } } } } // ── Apply the target status distribution (§5) across the IC pool ──────── function shuffle(arr: T[]): T[] { const a = [...arr]; for (let i = a.length - 1; i > 0; i--) { const j = randInt(0, i); [a[i], a[j]] = [a[j], a[i]]; } return a; } const shuffledIcs = shuffle(icPoolForStatusAssignment); let cursor = 0; // ~40 Ausgetreten for (let i = 0; i < 40 && cursor < shuffledIcs.length; i++, cursor++) { const e = shuffledIcs[cursor]; const entryDate = new Date(e.entry_date); const exitDate = randomDateBetween(addDays(entryDate, 90), TODAY); e.status = "Ausgetreten"; e.exit_date = isoDate(exitDate); e.exit_reason = pick(EXIT_REASONS); history.push({ employee_id: e.id, event_date: e.exit_date, event_type: "Austritt", description: `Austritt (${e.exit_reason})` }); } // ~12 Karenz for (let i = 0; i < 12 && cursor < shuffledIcs.length; i++, cursor++) { const e = shuffledIcs[cursor]; const entryDate = new Date(e.entry_date); const karenzStart = randomDateBetween(addDays(entryDate, 180), addDays(TODAY, -10)); const returnDate = addDays(TODAY, randInt(10, 300)); e.status = "Karenz"; e.karenz_return_date = isoDate(returnDate); history.push({ employee_id: e.id, event_date: isoDate(karenzStart), event_type: "Karenz", description: `Karenzantritt, geplante Rückkehr am ${isoDate(returnDate)}`, }); } // ~3 Geplant (future entry — overwrite entry_date/history) for (let i = 0; i < 3 && cursor < shuffledIcs.length; i++, cursor++) { const e = shuffledIcs[cursor]; const futureEntry = addDays(TODAY, randInt(10, 90)); e.status = "Geplant"; e.entry_date = isoDate(futureEntry); const historyEntry = history.find((h) => h.employee_id === e.id && h.event_type === "Eintritt"); if (historyEntry) historyEntry.event_date = e.entry_date; } // ~3 planned future exits (still Aktiv until the exit date arrives) for (let i = 0; i < 3 && cursor < shuffledIcs.length; i++, cursor++) { const e = shuffledIcs[cursor]; const futureExit = addDays(TODAY, randInt(10, 90)); e.exit_date = isoDate(futureExit); e.exit_reason = pick(EXIT_REASONS); } // ── Open positions (§4.6 / §5) ─────────────────────────────── type PositionRow = { title: string; team_id: string; is_lead: boolean; reports_to_employee_id: string | null; status: "open"; created_at: string; }; const positions: PositionRow[] = []; { const pool = shuffle([...teamRows]); for (let i = 0; i < 8; i++) { const team = pool[i % pool.length]; const leadOfTeam = employees.find((e) => e.team_id === team.id && e.is_lead); const isLeadPosition = i < 2; // first two are leadership requisitions const reportsTo = isLeadPosition ? (employees.find((e) => e.division_id === leadOfTeam?.division_id && e.org_level === 1)?.id ?? null) : (leadOfTeam?.id ?? null); positions.push({ title: isLeadPosition ? `Teamleitung ${team.name}` : "Neue Position", team_id: team.id, is_lead: isLeadPosition, reports_to_employee_id: reportsTo, status: "open", created_at: new Date(addDays(TODAY, -randInt(1, 45))).toISOString(), }); } } // ── Insert helpers ─────────────────────────────────────────── async function insertInChunks(table: string, rows: Record[], chunkSize = 200) { for (let i = 0; i < rows.length; i += chunkSize) { const chunk = rows.slice(i, i + chunkSize); const { error } = await supabase.from(table).insert(chunk); if (error) throw new Error(`Insert into ${table} failed: ${error.message}`); } console.log(` inserted ${rows.length} row(s) into ${table}`); } async function main() { console.log("Seeding locations..."); await insertInChunks("locations", LOCATIONS.map((l) => ({ ...l }))); console.log("Seeding divisions..."); await insertInChunks("divisions", divisionRows); console.log("Seeding departments..."); await insertInChunks("departments", departmentRows); console.log("Seeding teams..."); await insertInChunks("teams", teamRows); console.log(`Seeding ${employees.length} employees...`); await insertInChunks("employees", employees); console.log(`Seeding ${history.length} employee_history rows...`); await insertInChunks("employee_history", history); console.log(`Seeding ${positions.length} open positions...`); await insertInChunks("positions", positions); console.log("Creating hr_admin account..."); const adminPassword = randomUUID().slice(0, 12) + "!Aa1"; const { data: adminUser, error: adminErr } = await supabase.auth.admin.createUser({ email: ADMIN_EMAIL, password: adminPassword, email_confirm: true, }); if (adminErr) throw new Error(`Creating admin user failed: ${adminErr.message}`); await supabase.from("profiles").insert({ id: adminUser.user.id, email: ADMIN_EMAIL, full_name: "Maximilian Stubhan", role: "hr_admin", }); console.log("Creating manager test account..."); const managerPassword = randomUUID().slice(0, 12) + "!Bb2"; const { data: managerUser, error: managerErr } = await supabase.auth.admin.createUser({ email: MANAGER_TEST_EMAIL, password: managerPassword, email_confirm: true, }); if (managerErr) throw new Error(`Creating manager test user failed: ${managerErr.message}`); await supabase.from("profiles").insert({ id: managerUser.user.id, email: MANAGER_TEST_EMAIL, full_name: "Test Manager", role: "manager", }); console.log("\nDone."); console.log(`hr_admin login: ${ADMIN_EMAIL} / ${adminPassword}`); console.log(`manager login: ${MANAGER_TEST_EMAIL} / ${managerPassword}`); console.log("(Passwords are shown once here only — store them somewhere safe.)"); } main().catch((err) => { console.error(err); process.exit(1); });