Maximilian Stubhan 4929252f45 Seed the OM model from scratch, and stop writing dates through UTC
Alle Daten gelöscht und neu aufgebaut: 60 Organisationseinheiten, 133 Jobs,
823 Planstellen, 852 Personen, 852 Besetzungen. Die Anmeldekonten bleiben
stehen — ein Seed, der sich selbst aus der Anwendung aussperrt, ist keiner.

Der Baum kommt aus buildOrg(); der Seed entscheidet nur noch, wer welche
Planstelle besetzt. Damit fällt die halbe Datei weg: keine division_id,
team_id, manager_id, org_level, is_lead mehr auf der Person.

Zwei Dinge, die das Altmodell nicht abbilden konnte, stehen jetzt bewusst in
den Daten:

  - Vakanz ist eine Planstelle ohne laufende Besetzung, keine eigene Tabelle.
    14 Planstellen sind heute unbesetzt, drei davon mit einem Eintritt in der
    Zukunft — die Besetzung beginnt später, die Planstelle existiert schon.
  - Ausgetretene sind Vorgänger:innen auf heute besetzten Planstellen, nicht
    Karteileichen an einem Team. Vorher liessen sie deren Planstellen als
    vakant erscheinen.

Drei Teamleitungen sind unbesetzt und zwei langzeitabwesend, damit die
Hochroll-Regel überhaupt Daten hat: 76 der 809 Berichtslinien weichen von der
formalen ab. Genau eine Person hat keine Vorgesetzte, die Geschäftsführung.

Beim ersten scharfen Lauf hat der SVNR-Trigger mitten im Einfügen abgebrochen,
mit bereits geleerter Datenbank. Ursache war nicht die Prüfziffer, sondern
isoDate(): es ging über toISOString(), während makeSvNummer die lokalen
Datumsteile liest. In Österreich verschiebt das jedes Datum um einen Tag — das
gespeicherte Geburtsdatum passte nicht mehr zu dem in der SV-Nummer codierten.
isoDate rechnet jetzt lokal, wie der Rest des Seeds auch.

Damit so etwas nicht wieder erst die Datenbank leerräumt: pruefeInvarianten()
läuft *vor* dem Löschen und prüft, was sonst erst die Unique-Indizes und
Trigger abfangen — doppelte Besetzungen, überlappende Historie, Ereignisse
nach dem Austritt, und jede SV-Nummer gegen ihr Geburtsdatum. Mit --dry-run
schreibt der Seed gar nichts und meldet nur, was entstehen würde.
2026-07-27 15:06:10 +02:00

Manner HR Master

Interne HR-Stammdatenverwaltung: Mitarbeiter:innen, Organisationsstruktur (Bereich/Abteilung/Team), Planstellen, Neueinstellungen, Versetzungen/ Beförderungen/Karenz, Reorganisationen und der zugehörige Audit-Trail.

Zweck

Die App ersetzt Excel-basierte HR-Stammdatenpflege durch ein Werkzeug mit verbindlichen Regeln (z. B. wirksame Daten statt sofortiger Änderungen, eindeutige Positions-/Org-Nummern, verpflichtende Historie) und einem lückenlosen Audit-Trail für jede Änderung.

Alle Mitarbeiterdaten in diesem System sind vertraulich — Stammdaten, Verträge, Sozialversicherungsnummern, Angehörige und Audit-Daten. Zugriff ist auf explizit aktivierte HR-Benutzer:innen beschränkt (siehe Sicherheitsprinzipien).

Tech-Stack

  • Next.js 16 (App Router) — Achtung: Next.js 16 hat Breaking Changes gegenüber älteren Versionen (u. a. proxy.ts statt middleware.ts). Vor Änderungen an Framework-nahen Dateien die lokalen Docs unter node_modules/next/dist/docs/ konsultieren; sie sind maßgeblich, ältere Anleitungen im Netz beschreiben teils überholte APIs.
  • React 19, TypeScript
  • Supabase (Postgres, Auth, RLS) — Datenhaltung liegt vollständig in Supabase, nicht im Next.js-Prozess.
  • Tailwind CSS v4
  • Vitest — Unit- (Node), Komponenten- (jsdom) und Integrationstests (gegen ein lokales Supabase)

Setup

npm install
cp .env.example .env.local   # Werte eintragen, siehe unten
npm run dev

Für lokale Supabase-Entwicklung (statt gegen ein Cloud-Projekt):

supabase start   # startet lokalen Postgres/Auth/Studio-Stack

supabase/config.toml und .env.test.local sind bereits auf die Standard-Ports der lokalen Supabase-CLI abgestimmt.

Umgebungsvariablen

Siehe .env.example für die vollständige, kommentierte Liste. Kurzfassung:

Variable Sichtbarkeit Zweck
NEXT_PUBLIC_SUPABASE_URL Browser + Server Supabase-Projekt-URL
NEXT_PUBLIC_SUPABASE_ANON_KEY Browser + Server Anon-Key, RLS-gebunden
SUPABASE_SERVICE_ROLE_KEY Nur Server Umgeht RLS vollständig — niemals im Browser-Bundle, niemals loggen
CRON_SECRET Nur Server Schützt /api/cron/apply-pending-changes

NEXT_PUBLIC_*-Werte werden beim Build in das Client-Bundle eingebacken — eine Änderung erfordert einen Rebuild, nicht nur einen Neustart (relevant für Docker-Deployments, siehe unten).

Scripts

Befehl Zweck
npm run dev Lokaler Dev-Server
npm run build Produktions-Build
npm run start Produktions-Server (nach build)
npm run lint ESLint (eslint-config-next, Flat Config)
npm run typecheck tsc --noEmit
npm run test Vitest, Unit-Tests (tests/unit/**)
npm run test:integration Vitest gegen eine echte (lokale) Supabase-Instanz — braucht supabase start und .env.test.local
npm run test:e2e Playwright
npm run check lint + typecheck + test + build in Folge

Sicherheitsprinzipien

  • RLS ist die eigentliche Schranke, nicht die UI. Jede Tabelle hat Row Level Security aktiv; proxy.ts (App-Ebene) ist Defense-in-Depth, keine Ersatzkontrolle.
  • Ein Rollenmodell: profiles.role = 'hr' + profiles.is_active = true, geprüft über die SQL-Funktion is_hr_user(). Kein Sub-Rollensystem — siehe docs/data-model.md.
  • Service-Role-Key ist server-only. Einzige Verwendung: lib/supabase/admin.ts, geschützt durch import "server-only" (macht einen versehentlichen Client-Import zu einem Build-Fehler statt einem Laufzeitproblem).
  • Audit-Log ist transaktional in der Datenbank, nicht im App-Code: jede mutierende SQL-Funktion schreibt ihren audit_log-Eintrag in derselben Transaktion wie die Änderung selbst. Details und Prüfung siehe docs/security-review.md.
  • Historie ist append-only (employee_history, audit_log) — RLS erlaubt kein update/delete. Korrekturen sind kompensierende Einträge.

Cron-Konfiguration

/api/cron/apply-pending-changes wendet wirksam gewordene, zukunftsdatierte Änderungen an (pending_org_changesapply_due_pending_changes()).

  • Auf Vercel: vercel.json definiert den täglichen Schedule; Vercel Cron sendet Authorization: Bearer <CRON_SECRET> automatisch, wenn CRON_SECRET in den Projekt-Env-Vars gesetzt ist.
  • Außerhalb von Vercel (Docker): kein Vercel Cron verfügbar — siehe DEPLOYMENT.md für den Cron-Sidecar-Container, der denselben Endpoint mit demselben Schema aufruft.
  • Fehlt CRON_SECRET oder stimmt der Header nicht, antwortet die Route mit 401 (nicht 500 — bewusst, siehe tests/unit/security.test.ts).

Supabase-Hinweise

  • Schema-Quelle der Wahrheit: supabase/migrations/. Menschlich lesbare Zusammenfassung: docs/data-model.md.
  • Migrationen einspielen: supabase db push (gegen das verlinkte Projekt) bzw. supabase start + automatische Anwendung für lokale Entwicklung.
  • supabase/seed.ts und .env.test.local sind nur für lokale Entwicklung/Tests gedacht, nie für ein Produktivprojekt verwenden.

Testing

  • npm run test — schnell, keine externen Abhängigkeiten, läuft in CI.
  • npm run test:integration — braucht eine laufende lokale Supabase-Instanz (supabase start) und .env.test.local; prüft RLS-Verhalten end-to-end (siehe tests/integration/authorization.test.ts für das HR-Only-Zugriffs- modell).
  • npm run test:e2e — Playwright gegen einen laufenden Dev-/Preview-Server.

Deployment

Siehe DEPLOYMENT.md für Docker-basiertes Deployment (Dockerfile, docker-compose.yml, Reverse-Proxy/TLS, Cron-Ersatz, Updates). Für Vercel: vercel.json ist bereits vorhanden; Env-Vars im Vercel-Projekt setzen (siehe oben).

Known TODOs vor Produktivbetrieb

  • Content-Security-Policy fehlt noch (next.config.ts setzt bewusst keine CSP — Skript-/Style-/Connect-Quellen sind noch nicht vollständig inventarisiert; ungeprüft geraten zu setzen riskiert, Hydration oder den Supabase-Client stillschweigend zu brechen).
  • Lokale Scratch-Artefakte (.scratch_*, .scratch_shots/) enthalten Screenshots/Hilfsskripte aus einer früheren manuellen Verifikation und liegen noch im Arbeitsverzeichnis. Sie sind jetzt über .gitignore ausgeschlossen; vor einem Produktiv-Handover sollten sie durchgesehen und bei Bedarf gelöscht werden.
  • Kein granulareres Rollenmodell — aktuell HR-only (alles-oder-nichts). Falls z. B. eine reine Lese-Rolle künftig gebraucht wird, gehört die Erweiterung in eine neue Migration (is_hr_user()/RLS-Policies), nicht in App-seitigen Code.
Description
No description provided
Readme 938 KiB
Languages
TypeScript 67.3%
PLpgSQL 31.2%
JavaScript 0.8%
CSS 0.5%
Dockerfile 0.2%