Maximilian Stubhan 8282d7f581 Rename Karenz to Langzeitabwesenheit and record its type
Karenz was doing duty as the name for every kind of extended absence, but
the cases behave differently in payroll and reporting — Wochenhilfe, a
Präsenzdienst, a long sick leave and a sabbatical are not the same thing.
The concept is now called Langzeitabwesenheit and carries which kind it is.

- employees.absence_type, constrained to the thirteen kinds. start_karenz
  stores it on both paths (written straight away, or parked in the
  pending_org_changes payload when the absence starts later);
  record_karenz_return and the karenz_return branch of
  apply_due_pending_changes clear it, so a returned employee does not keep
  looking like they are still away. It also reaches employee_history, the
  audit log and the employee export.
- The status enum value stays 'Karenz'. Postgres can rename an enum value in
  place, but every stored function body that spells it would then reference
  a value that no longer exists — a dozen functions across fifteen
  migrations, rewritten for a label. The mapping lives in lib/absence.ts
  instead, which is the single place the UI reads the display name from.
- Where a kind is recorded the chip shows it — "Bildungskarenz" says more
  than "Langzeitabwesenheit". Absences predating the field have none and
  fall back to the generic name rather than to a guess, and a value outside
  the list is dropped rather than echoed into the UI.
- The export prints the display name, not the raw enum: a payroll hand-off
  reading "Karenz" for what the app calls Langzeitabwesenheit only causes
  questions. Audit filter options keep their stored values and change only
  their labels.
- The seed spreads the twelve absences across the kinds; all of them being
  Karenz would leave any breakdown by kind invisible.
2026-07-25 14:34:28 +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%