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.
151 lines
6.6 KiB
Markdown
151 lines
6.6 KiB
Markdown
# 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](#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
|
|
```
|
|
|
|
Für lokale Supabase-Entwicklung (statt gegen ein Cloud-Projekt):
|
|
|
|
```bash
|
|
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`](.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`](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.
|