Org assignment history, mobile support, and a correctness pass

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.
This commit is contained in:
2026-07-24 23:38:10 +02:00
parent f96773da0f
commit 79f0e19bf8
101 changed files with 6120 additions and 700 deletions

154
README.md
View File

@@ -1,36 +1,150 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
# Manner HR Master
## Getting Started
Interne HR-Stammdatenverwaltung: Mitarbeiter:innen, Organisationsstruktur
(Bereich/Abteilung/Team), Planstellen, Neueinstellungen, Versetzungen/
Beförderungen/Karenz, Reorganisationen und der zugehörige Audit-Trail.
First, run the development server:
## 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](#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`, siehe
`AGENTS.md`). Vor Änderungen an Framework-nahen Dateien die lokalen Docs
unter `node_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
```bash
npm install
cp .env.example .env.local # Werte eintragen, siehe unten
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
Für lokale Supabase-Entwicklung (statt gegen ein Cloud-Projekt):
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
```bash
supabase start # startet lokalen Postgres/Auth/Studio-Stack
```
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
`supabase/config.toml` und `.env.test.local` sind bereits auf die
Standard-Ports der lokalen Supabase-CLI abgestimmt.
## Learn More
## Umgebungsvariablen
To learn more about Next.js, take a look at the following resources:
Siehe [`.env.example`](.env.example) für die vollständige, kommentierte
Liste. Kurzfassung:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
| 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` |
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
`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).
## Deploy on Vercel
## Scripts
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
| 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 |
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
## 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`](docs/data-model.md#zugriffsmodell).
- **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`](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_changes``apply_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`](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`](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`](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.