Data model - employee_assignments records org placement over time (valid_from/valid_to), written by a trigger on `employees` rather than inside each RPC: ~70 `update employees` statements spread over fifteen migrations mean per-call bookkeeping would miss paths today and again with every future RPC. A partial unique index enforces the one-open-interval invariant the trigger relies on when closing the current row. - The Organigramm gains a Stichtag (default today). Membership comes from entry/exit/karenz, past placement from the new history, future placement projected from pending_org_changes. Placements predating the migration are backfilled with today's values and flagged as such in the UI, since employee_history only ever stored free text and cannot be reconstructed. Correctness - Reports and exports silently truncated at PostgREST's 1000-row cap (db.max_rows); employee_history is already past it at ~800 staff. Every whole-table read now pages explicitly. - XLSX date cells were a day early: ExcelJS converts a Date to an Excel serial straight off getTime(), so a Date built at local midnight lands on the previous day's serial in any positive-offset zone. - Date handling is pinned to Europe/Vienna throughout, and date-only strings are formatted without a Date round-trip. The dashboard's YTD window was built by round-tripping a local Date through toISOString(), which shifted it a day early and dropped 31 December entirely. - Export routes parsed measure/group/split/eventType with unchecked `as` casts, so an unknown value reached column headers as `undefined` and the Content-Disposition filename. Parsed against the label maps now, with the filename slugged as a backstop. - toXlsx keyed columns by header text, silently dropping the second of any two columns sharing a name — split columns take their header from data. - The org chart tree walks had no cycle guard; nothing in the schema forbids a manager_id cycle, and one would hang the tab rather than misreport. - The login page reflected ?error= verbatim, letting anyone put arbitrary text on the real sign-in screen; messages are looked up by code now. - React Flow needs elementsSelectable on, or it sets pointer-events:none on the whole node and the expand control stops responding. UI - Mobile: the shell was unusable below lg — a fixed 236px margin pushed content off-screen with no mobile navigation at all. The sidebar is now a drawer, dvh replaces vh, safe-area insets are honoured, inputs are 16px so iOS stops zooming on focus, and form grids stack. - Org chart nodes redesigned: per-kind accent stripes and icons, vacant roles called out, expand control moved to the bottom edge carrying the child count. - Pagination is windowed; it previously rendered one link per page (54 for the employee list, unbounded for the audit log). - Positions page reduced to open positions with a single "Besetzen" action. - The employee Organisation tab links into the org chart focused on that person, reusing the chart's existing search-match highlighting. Also included, uncommitted until now - Dependants, HR notes, academic titles, split address fields, position validity and role/employment fields, with their migrations and UI. - Docker/compose deployment setup, data-model and security-review docs.
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.tsstattmiddleware.ts, sieheAGENTS.md). Vor Änderungen an Framework-nahen Dateien die lokalen Docs unternode_modules/next/dist/docs/konsultieren. - React 19, TypeScript
- Supabase (Postgres, Auth, RLS) — Datenhaltung liegt vollständig in Supabase, nicht im Next.js-Prozess.
- Tailwind CSS v4
- Vitest (Unit + Integrationstests), Playwright (E2E)
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-Funktionis_hr_user(). Kein Sub-Rollensystem — siehedocs/data-model.md. - Service-Role-Key ist server-only. Einzige Verwendung:
lib/supabase/admin.ts, geschützt durchimport "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 siehedocs/security-review.md. - Historie ist append-only (
employee_history,audit_log) — RLS erlaubt keinupdate/delete. Korrekturen sind kompensierende Einträge.
Cron-Konfiguration
/api/cron/apply-pending-changes wendet wirksam gewordene, zukunftsdatierte
Änderungen an (pending_org_changes → apply_due_pending_changes()).
- Auf Vercel:
vercel.jsondefiniert den täglichen Schedule; Vercel Cron sendetAuthorization: Bearer <CRON_SECRET>automatisch, wennCRON_SECRETin den Projekt-Env-Vars gesetzt ist. - Außerhalb von Vercel (Docker): kein Vercel Cron verfügbar — siehe
DEPLOYMENT.mdfür den Cron-Sidecar-Container, der denselben Endpoint mit demselben Schema aufruft. - Fehlt
CRON_SECREToder stimmt der Header nicht, antwortet die Route mit401(nicht500— bewusst, siehetests/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.tsund.env.test.localsind 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 (siehetests/integration/authorization.test.tsfü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.tssetzt 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.gitignoreausgeschlossen; 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.