Remove Supabase
The database moved to a container of our own; the platform is gone. This takes out what was left of it — and, where the leftovers were load bearing, moves rather than deletes. Moved, not deleted: supabase/migrations/ -> db/migrations/ the schema's source of truth supabase/build-org.ts -> scripts/build-org.ts lib/supabase/types.ts -> lib/types.ts 52 import sites repointed The bookkeeping needed care. It lived in `supabase_migrations.schema_migrations`, and simply renaming the schema would have left the runner facing an empty table: it would have called all 67 migrations pending and replayed them against a database that is long since current. So the runner now creates `migrationen.schema_migrations` and, once, copies the old rows across — guarded so a second run does nothing and a fresh database skips it entirely. Only then does migration 20260907100000 drop the old schema. Deleted: the CLI config, the seed, the historical schema/function dumps (nothing read them), scripts/umzug-von-supabase.sh (the move is done), and both Supabase packages plus the CLI. Nothing in the application imported them — the build now succeeds with no environment variables at all, which is the proof. Integration tests: six of them signed in through Supabase Auth and asserted against the anon key and the service role. That model is gone, so the tests were not portable — they are deleted. session-context and employee-status-filter already ran on pg and are untouched; om-reporting is ported to a direct connection because it guards a real risk (the reporting line rule exists twice, once in SQL and once in TypeScript). CI: the integration job started a Supabase stack. It now runs a postgres service, applies deploy/db-init and every migration to an empty database — that was the valuable part, and it still holds — then checks that a second run is a no-op, which is what proves the bookkeeping works. Docs: security-review.md audited a service-role key, a cookie adapter and auth.users, none of which exist. Restating findings about removed components would suggest today's system had been reviewed; it has not. It now records what was removed and says a fresh review is due. data-model.md was already marked obsolete and described the pre-OM schema; azure-migration.md was a plan for a route not taken. Both deleted. Verified: npm ci, typecheck, lint, 445 tests, build — all clean without the packages. Integration tests skip cleanly with no database. Migration SQL and the runner are reviewed but NOT executed: no Docker here, and the old instance no longer resolves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,3 @@ npm-debug.log*
|
|||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
.scratch_*
|
.scratch_*
|
||||||
.scratch_shots
|
.scratch_shots
|
||||||
supabase/.branches
|
|
||||||
supabase/.temp
|
|
||||||
supabase/snippets
|
|
||||||
|
|||||||
101
.github/workflows/ci.yml
vendored
101
.github/workflows/ci.yml
vendored
@@ -29,25 +29,45 @@ jobs:
|
|||||||
- name: Typecheck
|
- name: Typecheck
|
||||||
run: npm run typecheck
|
run: npm run typecheck
|
||||||
|
|
||||||
# Catches the drift that `tsc` cannot: lib/supabase/types.ts is
|
# Fängt die Abweichung, die `tsc` nicht sehen kann: lib/types.ts wird von
|
||||||
# hand-written, so a migration adding a column leaves it silently stale.
|
# Hand gepflegt, eine Migration mit einer neuen Spalte lässt sie also
|
||||||
|
# stillschweigend veralten.
|
||||||
- name: Schema/Typen-Abgleich
|
- name: Schema/Typen-Abgleich
|
||||||
run: npm run types:check
|
run: npm run types:check
|
||||||
|
|
||||||
- name: Unit- und Komponententests
|
- name: Unit- und Komponententests
|
||||||
run: npm test
|
run: npm test
|
||||||
|
|
||||||
|
# Ohne Umgebungsvariablen: seit dem Wegfall der Browser-Anbindung wird
|
||||||
|
# zur Bauzeit nichts mehr aus der Umgebung gelesen, und das Abbild ist
|
||||||
|
# für jede Umgebung dasselbe.
|
||||||
- name: Build
|
- name: Build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
env:
|
|
||||||
# Read at module scope by the Supabase browser client, so the build
|
|
||||||
# needs them present — never the real project's values.
|
|
||||||
NEXT_PUBLIC_SUPABASE_URL: http://127.0.0.1:54321
|
|
||||||
NEXT_PUBLIC_SUPABASE_ANON_KEY: build-time-placeholder
|
|
||||||
|
|
||||||
integration:
|
migrationen:
|
||||||
name: Integrationstests (echtes Postgres)
|
name: Migrationen auf leerer Datenbank
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
# Derselbe Stand wie im Betrieb. Ein eigener Dienst statt einer fremden
|
||||||
|
# Plattform — die Datenbank gehört seit dem Umzug zum Projekt.
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:17-alpine
|
||||||
|
env:
|
||||||
|
POSTGRES_PASSWORD: ci
|
||||||
|
POSTGRES_DB: alpenwerk
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
options: >-
|
||||||
|
--health-cmd "pg_isready -U postgres"
|
||||||
|
--health-interval 5s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 20
|
||||||
|
|
||||||
|
env:
|
||||||
|
MIGRATE_DATABASE_URL: postgresql://postgres:ci@127.0.0.1:5432/alpenwerk
|
||||||
|
DATABASE_SSL: "false"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
@@ -58,40 +78,37 @@ jobs:
|
|||||||
|
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
|
|
||||||
- uses: supabase/setup-cli@v1
|
# Was das Schema von der Umgebung erwartet: die Anwendungsrolle ohne
|
||||||
with:
|
# BYPASSRLS, zwei Erweiterungen und die Attrappe des `auth`-Schemas, ohne
|
||||||
version: latest
|
# die sich die Migrationen von Juni 2026 nicht abspielen lassen.
|
||||||
|
- name: Rollen, Erweiterungen, auth-Attrappe
|
||||||
# Applies every migration to a fresh database — which also means a
|
env:
|
||||||
# migration that cannot be replayed from scratch fails here rather than
|
PGPASSWORD: ci
|
||||||
# on a restore or a new environment.
|
APP_DB_PASSWORD: ci-anwendungsrolle
|
||||||
- name: Supabase starten
|
|
||||||
run: supabase start
|
|
||||||
|
|
||||||
# `-o env` emits API_URL / ANON_KEY / SERVICE_ROLE_KEY / DB_URL; the app
|
|
||||||
# expects them under its own names. DATABASE_URL ist der direkte
|
|
||||||
# Postgres-Zugang — den braucht die neue Zugriffsschicht (lib/db) und
|
|
||||||
# vor allem der Nachweis zum Sitzungskontext.
|
|
||||||
- name: Testumgebung schreiben
|
|
||||||
run: |
|
run: |
|
||||||
supabase status -o env \
|
for f in deploy/db-init/*.sql; do
|
||||||
--override-name api.url=NEXT_PUBLIC_SUPABASE_URL \
|
echo "── $f"
|
||||||
--override-name auth.anon_key=NEXT_PUBLIC_SUPABASE_ANON_KEY \
|
psql -v ON_ERROR_STOP=1 -h 127.0.0.1 -U postgres -d alpenwerk -f "$f"
|
||||||
--override-name auth.service_role_key=SUPABASE_SERVICE_ROLE_KEY \
|
done
|
||||||
--override-name db.url=DATABASE_URL \
|
|
||||||
| grep -E '^(NEXT_PUBLIC_SUPABASE_URL|NEXT_PUBLIC_SUPABASE_ANON_KEY|SUPABASE_SERVICE_ROLE_KEY|DATABASE_URL)=' \
|
|
||||||
| tr -d '"' > .env.test.local
|
|
||||||
# Lokal läuft Postgres ohne TLS; ohne das versucht `pg` es trotzdem.
|
|
||||||
echo "DATABASE_SSL=false" >> .env.test.local
|
|
||||||
grep -q '^DATABASE_URL=' .env.test.local \
|
|
||||||
|| { echo "DATABASE_URL wurde nicht geschrieben — der Sitzungskontext-Nachweis liefe ins Leere."; exit 1; }
|
|
||||||
|
|
||||||
- name: Seed
|
# Der eigentliche Zweck dieses Jobs: jede Migration muss sich auf einer
|
||||||
run: node --env-file=.env.test.local supabase/seed.ts
|
# leeren Datenbank abspielen lassen. Eine, die das nicht kann, fällt hier
|
||||||
|
# auf — und nicht beim Wiederherstellen einer Sicherung oder beim
|
||||||
|
# Aufsetzen einer neuen Umgebung.
|
||||||
|
- name: Migrationen einspielen
|
||||||
|
run: node scripts/migrate.mjs
|
||||||
|
|
||||||
- name: Integrationstests
|
# Zweiter Lauf: die Buchführung muss greifen. Meldet er etwas anderes als
|
||||||
run: npm run test:integration
|
# „nichts anzuwenden", verbucht der Läufer nicht richtig — und ein
|
||||||
|
# Ausrollen spielte Migrationen doppelt ein.
|
||||||
|
- name: Zweiter Lauf ist ein Nichts
|
||||||
|
run: |
|
||||||
|
ausgabe=$(node scripts/migrate.mjs)
|
||||||
|
echo "$ausgabe"
|
||||||
|
echo "$ausgabe" | grep -q "^Nichts anzuwenden" \
|
||||||
|
|| { echo "Der zweite Lauf wollte erneut anwenden — die Buchführung greift nicht."; exit 1; }
|
||||||
|
|
||||||
- name: Supabase-Logs bei Fehlschlag
|
# Die Integrationstests brauchen einen befüllten Bestand; der Seed lag
|
||||||
if: failure()
|
# in der abgelösten Umgebung und ist noch nicht nachgezogen (siehe
|
||||||
run: supabase status && docker ps -a
|
# README, offene Punkte). Bis dahin laufen sie gegen eine erreichbare
|
||||||
|
# Datenbank von Hand, nicht hier.
|
||||||
|
|||||||
8
.github/workflows/deploy.yml
vendored
8
.github/workflows/deploy.yml
vendored
@@ -96,7 +96,7 @@ jobs:
|
|||||||
- name: .env schreiben
|
- name: .env schreiben
|
||||||
env:
|
env:
|
||||||
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
||||||
SUPABASE_DB_URL: ${{ secrets.SUPABASE_DB_URL }}
|
MIGRATE_DATABASE_URL: ${{ secrets.MIGRATE_DATABASE_URL }}
|
||||||
AUTH_SECRET: ${{ secrets.AUTH_SECRET }}
|
AUTH_SECRET: ${{ secrets.AUTH_SECRET }}
|
||||||
AUTH_URL: ${{ secrets.AUTH_URL }}
|
AUTH_URL: ${{ secrets.AUTH_URL }}
|
||||||
AUTH_MICROSOFT_ENTRA_ID_ID: ${{ secrets.AUTH_MICROSOFT_ENTRA_ID_ID }}
|
AUTH_MICROSOFT_ENTRA_ID_ID: ${{ secrets.AUTH_MICROSOFT_ENTRA_ID_ID }}
|
||||||
@@ -106,7 +106,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
fehlend=""
|
fehlend=""
|
||||||
for name in DATABASE_URL SUPABASE_DB_URL AUTH_SECRET AUTH_URL \
|
for name in DATABASE_URL MIGRATE_DATABASE_URL AUTH_SECRET AUTH_URL \
|
||||||
AUTH_MICROSOFT_ENTRA_ID_ID AUTH_MICROSOFT_ENTRA_ID_SECRET \
|
AUTH_MICROSOFT_ENTRA_ID_ID AUTH_MICROSOFT_ENTRA_ID_SECRET \
|
||||||
AUTH_MICROSOFT_ENTRA_ID_ISSUER CRON_SECRET; do
|
AUTH_MICROSOFT_ENTRA_ID_ISSUER CRON_SECRET; do
|
||||||
eval "wert=\${$name:-}"
|
eval "wert=\${$name:-}"
|
||||||
@@ -138,12 +138,12 @@ jobs:
|
|||||||
# wenn danach etwas schiefgeht.
|
# wenn danach etwas schiefgeht.
|
||||||
- name: Ausstehende Migrationen zeigen
|
- name: Ausstehende Migrationen zeigen
|
||||||
env:
|
env:
|
||||||
SUPABASE_DB_URL: ${{ secrets.SUPABASE_DB_URL }}
|
MIGRATE_DATABASE_URL: ${{ secrets.MIGRATE_DATABASE_URL }}
|
||||||
run: node scripts/migrate.mjs --dry-run
|
run: node scripts/migrate.mjs --dry-run
|
||||||
|
|
||||||
- name: Migrationen anwenden
|
- name: Migrationen anwenden
|
||||||
env:
|
env:
|
||||||
SUPABASE_DB_URL: ${{ secrets.SUPABASE_DB_URL }}
|
MIGRATE_DATABASE_URL: ${{ secrets.MIGRATE_DATABASE_URL }}
|
||||||
run: node scripts/migrate.mjs
|
run: node scripts/migrate.mjs
|
||||||
|
|
||||||
# ── Container ───────────────────────────────────────────────────
|
# ── Container ───────────────────────────────────────────────────
|
||||||
|
|||||||
14
.gitignore
vendored
14
.gitignore
vendored
@@ -38,17 +38,9 @@ yarn-error.log*
|
|||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
# supabase CLI local dev (generated, project-machine-specific)
|
|
||||||
/supabase/.branches
|
|
||||||
/supabase/.temp
|
|
||||||
/supabase/snippets
|
|
||||||
|
|
||||||
# scratch output of `npm run types:generate`, for comparing against the
|
# Kladden und Bildschirmfotos aus der Handprüfung — können Personendaten
|
||||||
# hand-written lib/supabase/types.ts — never itself imported
|
# oder Zugangsdaten enthalten; nie committen.
|
||||||
/lib/supabase/types.generated.ts
|
|
||||||
|
|
||||||
# local scratch scripts/screenshots (ad-hoc verification against a real or
|
|
||||||
# seeded DB — can carry HR data or use SUPABASE_SERVICE_ROLE_KEY; never commit)
|
|
||||||
.scratch_*
|
.scratch_*
|
||||||
/.scratch_shots/
|
/.scratch_shots/
|
||||||
|
|
||||||
@@ -56,5 +48,3 @@ next-env.d.ts
|
|||||||
.backups/
|
.backups/
|
||||||
|
|
||||||
# Datenabzuege aus dem Umzug – enthalten Personendaten
|
# Datenabzuege aus dem Umzug – enthalten Personendaten
|
||||||
supabase-daten-*.sql
|
|
||||||
supabase-voll-*.sql
|
|
||||||
|
|||||||
@@ -15,12 +15,10 @@ eine `profiles`-Zeile braucht (siehe [docs/entra-sso.md](docs/entra-sso.md)).
|
|||||||
- **Containerisiert:** die Next.js-App (`Dockerfile`) **und die Datenbank**
|
- **Containerisiert:** die Next.js-App (`Dockerfile`) **und die Datenbank**
|
||||||
(Dienst `db`, `postgres:17-alpine`). Beide zusammen in
|
(Dienst `db`, `postgres:17-alpine`). Beide zusammen in
|
||||||
`docker-compose.yml`; die Daten liegen im benannten Volume `db-daten`.
|
`docker-compose.yml`; die Daten liegen im benannten Volume `db-daten`.
|
||||||
- **Nicht mehr Supabase.** Die Anwendung sprach ohnehin unmittelbar mit
|
- **Die Datenbank gehört zum Projekt.** Was eine gehostete Plattform sonst
|
||||||
PostgreSQL — Supabase war nur der Betreiber. Was die Plattform beisteuerte,
|
beisteuert, bringt `deploy/db-init/` mit: die Anwendungsrolle ohne
|
||||||
bringt jetzt `deploy/db-init/` mit: die Anwendungsrolle ohne BYPASSRLS, die
|
BYPASSRLS, die zwei benutzten Erweiterungen und eine Attrappe des
|
||||||
zwei benutzten Erweiterungen und eine Attrappe des `auth`-Schemas, die nur
|
`auth`-Schemas, die nur die Migrationen von Juni 2026 brauchen.
|
||||||
die alten Migrationen von Juni brauchen. Der Umzug steht in
|
|
||||||
[Abschnitt 3a](#3a-umzug-von-supabase).
|
|
||||||
- Läuft die Datenbank woanders (Azure Flexible Server, RDS, eigenes Blech),
|
- Läuft die Datenbank woanders (Azure Flexible Server, RDS, eigenes Blech),
|
||||||
genügt es, `DATABASE_URL` dorthin zeigen zu lassen und den `db`-Dienst nicht
|
genügt es, `DATABASE_URL` dorthin zeigen zu lassen und den `db`-Dienst nicht
|
||||||
zu starten. Die Anwendung merkt keinen Unterschied.
|
zu starten. Die Anwendung merkt keinen Unterschied.
|
||||||
@@ -152,41 +150,6 @@ docker compose up -d --build
|
|||||||
`migrate` steht im Profil `tools` und läuft bei `docker compose up` nicht mit.
|
`migrate` steht im Profil `tools` und läuft bei `docker compose up` nicht mit.
|
||||||
Auf dem Host wird dafür weder Node noch psql gebraucht — nur Docker.
|
Auf dem Host wird dafür weder Node noch psql gebraucht — nur Docker.
|
||||||
|
|
||||||
## 3a. Umzug von Supabase
|
|
||||||
|
|
||||||
Einmalig, wenn der Bestand noch bei Supabase liegt:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
SUPABASE_DB_URL='postgresql://postgres:<passwort>@<projekt>.supabase.com:5432/postgres' \
|
|
||||||
./scripts/umzug-von-supabase.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Was das Skript tut und warum:
|
|
||||||
|
|
||||||
1. **Schema aus den Migrationen**, nicht aus einem Abzug. Nachgewiesen ist,
|
|
||||||
dass alle Migrationen auf einer leeren Datenbank durchlaufen und dabei
|
|
||||||
Spalte für Spalte, Index für Index, Policy für Policy dasselbe ergeben wie
|
|
||||||
die gewachsene Produktion. Der Weg hat zwei Vorteile: die Buchführung
|
|
||||||
stimmt danach von selbst, und Supabase-eigene Rechte und Eigentümer kommen
|
|
||||||
gar nicht erst mit.
|
|
||||||
2. **Nur die Daten** werden abgezogen (`pg_dump --data-only
|
|
||||||
--disable-triggers`). Ohne `--disable-triggers` stolpert jede
|
|
||||||
Fremdschlüsselprüfung über die Ladereihenfolge. RLS steht nicht im Weg —
|
|
||||||
keine der 19 Tabellen hat `FORCE ROW LEVEL SECURITY`, der Eigentümer
|
|
||||||
schreibt also durch.
|
|
||||||
3. Am Ende werden die Zeilenzahlen ausgegeben. **Mit denen bei Supabase
|
|
||||||
vergleichen**, bevor irgendetwas abgeschaltet wird.
|
|
||||||
|
|
||||||
Der Abzug bleibt als Datei liegen. Erst löschen, wenn die Anwendung gegen die
|
|
||||||
neue Datenbank nachweislich läuft.
|
|
||||||
|
|
||||||
Danach in `.env`:
|
|
||||||
|
|
||||||
```
|
|
||||||
DATABASE_URL=postgresql://alpenwerk_app:<APP_DB_PASSWORD>@db:5432/alpenwerk
|
|
||||||
DATABASE_SSL=false
|
|
||||||
```
|
|
||||||
|
|
||||||
### Was am `auth`-Schema übrigbleibt
|
### Was am `auth`-Schema übrigbleibt
|
||||||
|
|
||||||
`deploy/db-init/01-auth-attrappe.sql` legt ein leeres `auth`-Schema an. Es wird
|
`deploy/db-init/01-auth-attrappe.sql` legt ein leeres `auth`-Schema an. Es wird
|
||||||
@@ -283,8 +246,8 @@ und löscht sie danach wieder.
|
|||||||
|
|
||||||
| Secret | Anmerkung |
|
| Secret | Anmerkung |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `DATABASE_URL` | Transaktions-Pooler (Supabase: 6543) — den benutzt die Anwendung |
|
| `DATABASE_URL` | Verbindung der Anwendungsrolle — die benutzt die Anwendung |
|
||||||
| `SUPABASE_DB_URL` | **Direkter** Zugang (Supabase: 5432) — nur für die Migrationen |
|
| `MIGRATE_DATABASE_URL` | Zugang als Verwalter — nur für die Migrationen |
|
||||||
| `AUTH_SECRET` | |
|
| `AUTH_SECRET` | |
|
||||||
| `AUTH_URL` | z. B. `https://hr.elycon.solutions` |
|
| `AUTH_URL` | z. B. `https://hr.elycon.solutions` |
|
||||||
| `AUTH_MICROSOFT_ENTRA_ID_ID` | |
|
| `AUTH_MICROSOFT_ENTRA_ID_ID` | |
|
||||||
@@ -332,9 +295,9 @@ node --env-file=.env scripts/migrate.mjs --dry-run # was stünde an
|
|||||||
node --env-file=.env scripts/migrate.mjs # anwenden
|
node --env-file=.env scripts/migrate.mjs # anwenden
|
||||||
```
|
```
|
||||||
|
|
||||||
Buch geführt wird in `supabase_migrations.schema_migrations`, derselben Tabelle
|
Buch geführt wird in `migrationen.schema_migrations`: eine Zeile je
|
||||||
in derselben Form, die die Supabase-CLI benutzt — `supabase db push` von der
|
angewendeter Datei, mit ihrem Text. Der Läufer wendet nur an, was dort
|
||||||
Arbeitsstation bleibt damit möglich und überspringt, was hier schon lief.
|
fehlt.
|
||||||
|
|
||||||
> **Einmalig, im August 2026 bereits erledigt:** Die Migrationen wurden bis
|
> **Einmalig, im August 2026 bereits erledigt:** Die Migrationen wurden bis
|
||||||
> dahin von Hand eingespielt, die Buchführungstabelle existierte gar nicht. Ein
|
> dahin von Hand eingespielt, die Buchführungstabelle existierte gar nicht. Ein
|
||||||
@@ -409,9 +372,6 @@ Single-Instance-Compose-Konfiguration ist das nicht nötig.
|
|||||||
- **Cron läuft nicht:** `docker compose logs cron` – prüft, ob
|
- **Cron läuft nicht:** `docker compose logs cron` – prüft, ob
|
||||||
`/etc/crontabs/root` korrekt geschrieben wurde und ob `CRON_SECRET` in
|
`/etc/crontabs/root` korrekt geschrieben wurde und ob `CRON_SECRET` in
|
||||||
`.env` gesetzt ist (leer/fehlend führt serverseitig zu `401`).
|
`.env` gesetzt ist (leer/fehlend führt serverseitig zu `401`).
|
||||||
- **`max clients reached in session mode`:** der Verbindungsstring zeigt auf
|
|
||||||
den Sitzungs-Modus des Poolers. Auf den Transaktions-Modus wechseln (bei
|
|
||||||
Supabase Port 6543).
|
|
||||||
- **Healthcheck rot:** `docker compose logs app` – meist `DATABASE_URL`
|
- **Healthcheck rot:** `docker compose logs app` – meist `DATABASE_URL`
|
||||||
fehlend oder nicht erreichbar. Der Pool baut die Verbindung erst beim
|
fehlend oder nicht erreichbar. Der Pool baut die Verbindung erst beim
|
||||||
ersten Zugriff auf, der Fehler steht deshalb im Log der Anfrage, nicht im
|
ersten Zugriff auf, der Fehler steht deshalb im Log der Anfrage, nicht im
|
||||||
|
|||||||
63
README.md
63
README.md
@@ -24,11 +24,13 @@ auf explizit aktivierte HR-Benutzer:innen beschränkt (siehe
|
|||||||
`node_modules/next/dist/docs/` konsultieren; sie sind maßgeblich, ältere
|
`node_modules/next/dist/docs/` konsultieren; sie sind maßgeblich, ältere
|
||||||
Anleitungen im Netz beschreiben teils überholte APIs.
|
Anleitungen im Netz beschreiben teils überholte APIs.
|
||||||
- React 19, TypeScript
|
- React 19, TypeScript
|
||||||
- Supabase (Postgres, Auth, RLS) — Datenhaltung liegt vollständig in
|
- PostgreSQL, angesprochen über Kysely und `pg` — die Datenhaltung liegt in
|
||||||
Supabase, nicht im Next.js-Prozess.
|
der Datenbank, nicht im Next.js-Prozess. Der Zugriff läuft ausschliesslich
|
||||||
|
über `withUser()` (`lib/db/`), das den Sitzungskontext setzt.
|
||||||
|
- Auth.js gegen Microsoft Entra ID — keine eigenen Passwörter.
|
||||||
- Tailwind CSS v4
|
- Tailwind CSS v4
|
||||||
- Vitest — Unit- (Node), Komponenten- (jsdom) und Integrationstests
|
- Vitest — Unit- (Node), Komponenten- (jsdom) und Integrationstests
|
||||||
(gegen ein lokales Supabase)
|
(gegen eine erreichbare Datenbank)
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
@@ -38,14 +40,14 @@ cp .env.example .env.local # Werte eintragen, siehe unten
|
|||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Für lokale Supabase-Entwicklung (statt gegen ein Cloud-Projekt):
|
Für eine lokale Datenbank genügt der Container aus `docker-compose.yml`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
supabase start # startet lokalen Postgres/Auth/Studio-Stack
|
docker compose up -d db
|
||||||
|
docker compose run --rm migrate # spielt db/migrations/ ein
|
||||||
```
|
```
|
||||||
|
|
||||||
`supabase/config.toml` und `.env.test.local` sind bereits auf die
|
Danach zeigt `DATABASE_URL` in `.env.local` auf diese Datenbank.
|
||||||
Standard-Ports der lokalen Supabase-CLI abgestimmt.
|
|
||||||
|
|
||||||
## Umgebungsvariablen
|
## Umgebungsvariablen
|
||||||
|
|
||||||
@@ -77,8 +79,9 @@ selbst spricht. Ein Docker-Abbild ist damit umgebungsneutral: einmal gebaut,
|
|||||||
| `npm run lint` | ESLint (`eslint-config-next`, Flat Config) |
|
| `npm run lint` | ESLint (`eslint-config-next`, Flat Config) |
|
||||||
| `npm run typecheck` | `tsc --noEmit` |
|
| `npm run typecheck` | `tsc --noEmit` |
|
||||||
| `npm run test` | Vitest, Unit-Tests (`tests/unit/**`) |
|
| `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:integration` | Vitest gegen eine erreichbare Datenbank; überspringt sich ohne `DATABASE_URL` |
|
||||||
| `npm run test:e2e` | Playwright |
|
| `npm run test:e2e` | Playwright |
|
||||||
|
| `npm run migrate` | Ausstehende Migrationen einspielen (`--dry-run`, `--baseline`) |
|
||||||
| `npm run check` | lint + typecheck + test + build in Folge |
|
| `npm run check` | lint + typecheck + test + build in Folge |
|
||||||
|
|
||||||
## Sicherheitsprinzipien
|
## Sicherheitsprinzipien
|
||||||
@@ -115,23 +118,24 @@ selbst spricht. Ein Docker-Abbild ist damit umgebungsneutral: einmal gebaut,
|
|||||||
- Fehlt `CRON_SECRET` oder stimmt der Header nicht, antwortet die Route mit
|
- Fehlt `CRON_SECRET` oder stimmt der Header nicht, antwortet die Route mit
|
||||||
`401` (nicht `500` — bewusst, siehe `tests/unit/security.test.ts`).
|
`401` (nicht `500` — bewusst, siehe `tests/unit/security.test.ts`).
|
||||||
|
|
||||||
## Supabase-Hinweise
|
## Datenbank
|
||||||
|
|
||||||
- Schema-Quelle der Wahrheit: `supabase/migrations/`. Menschlich lesbare
|
- Schema-Quelle der Wahrheit: `db/migrations/`. Menschlich lesbare Fassung,
|
||||||
Fassung, aus der laufenden Datenbank erzeugt:
|
aus der laufenden Datenbank erzeugt:
|
||||||
[`docs/datenkatalog.md`](docs/datenkatalog.md).
|
[`docs/datenkatalog.md`](docs/datenkatalog.md).
|
||||||
- Migrationen einspielen: `supabase db push` (gegen das verlinkte Projekt)
|
- Migrationen einspielen: `npm run migrate`. Der Läufer wendet nur die
|
||||||
bzw. `supabase start` + automatische Anwendung für lokale Entwicklung.
|
fehlenden Dateien an, jede in ihrer eigenen Transaktion, und führt darüber
|
||||||
- `supabase/seed.ts` und `.env.test.local` sind nur für lokale
|
Buch in `migrationen.schema_migrations`.
|
||||||
Entwicklung/Tests gedacht, nie für ein Produktivprojekt verwenden.
|
- `lib/types.ts` wird von Hand gepflegt. `npm run types:check` hält sie
|
||||||
|
Spalte für Spalte gegen die Migrationen.
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
- `npm run test` — schnell, keine externen Abhängigkeiten, läuft in CI.
|
- `npm run test` — schnell, keine externen Abhängigkeiten, läuft in CI.
|
||||||
- `npm run test:integration` — braucht eine laufende lokale Supabase-Instanz
|
- `npm run test:integration` — braucht eine erreichbare Datenbank
|
||||||
(`supabase start`) und `.env.test.local`; prüft RLS-Verhalten end-to-end
|
(`DATABASE_URL`). Geprüft werden Regeln, die es zweimal gibt: einmal als
|
||||||
(siehe `tests/integration/authorization.test.ts` für das HR-Only-Zugriffs-
|
SQL, einmal als TypeScript. Ohne `DATABASE_URL` überspringen sich die
|
||||||
modell).
|
Dateien, statt mit einem Verbindungsfehler abzubrechen.
|
||||||
- `npm run test:e2e` — Playwright gegen einen laufenden Dev-/Preview-Server.
|
- `npm run test:e2e` — Playwright gegen einen laufenden Dev-/Preview-Server.
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
@@ -141,15 +145,18 @@ Reverse-Proxy/TLS, der nächtliche Lauf, Migrationen und Updates.
|
|||||||
|
|
||||||
## Known TODOs vor Produktivbetrieb
|
## Known TODOs vor Produktivbetrieb
|
||||||
|
|
||||||
- **Content-Security-Policy fehlt noch** (`next.config.ts` setzt bewusst
|
- **Content-Security-Policy läuft im Nur-Bericht-Modus** (`next.config.ts`).
|
||||||
keine CSP — Skript-/Style-/Connect-Quellen sind noch nicht vollständig
|
Erzwungen wird sie erst, wenn die Meldungen sauber sind — eine geratene,
|
||||||
inventarisiert; ungeprüft geraten zu setzen riskiert, Hydration oder den
|
erzwungene Richtlinie blendet die Anwendung für alle aus.
|
||||||
Supabase-Client stillschweigend zu brechen).
|
- **Seed für die Integrationstests fehlt.** Er lag in der abgelösten
|
||||||
- **Lokale Scratch-Artefakte** (`.scratch_*`, `.scratch_shots/`) enthalten
|
Umgebung. Zwei der drei Integrationstests vergleichen Regeln über den
|
||||||
Screenshots/Hilfsskripte aus einer früheren manuellen Verifikation und
|
gesamten Bestand und brauchen dafür Daten; bis der Seed nachgezogen ist,
|
||||||
liegen noch im Arbeitsverzeichnis. Sie sind jetzt über `.gitignore`
|
laufen sie nur gegen eine bereits befüllte Datenbank, nicht in der CI.
|
||||||
ausgeschlossen; vor einem Produktiv-Handover sollten sie durchgesehen und
|
- **Sicherheitsprüfung des heutigen Aufbaus steht aus** — siehe
|
||||||
bei Bedarf gelöscht werden.
|
[`docs/security-review.md`](docs/security-review.md).
|
||||||
|
- **Bildschirmfotos unter `.scratch_shots/`** stammen aus einer früheren
|
||||||
|
Handprüfung. Sie sind über `.gitignore` ausgeschlossen; vor der Übergabe
|
||||||
|
durchsehen und löschen.
|
||||||
- **Kein granulareres Rollenmodell** — aktuell HR-only (alles-oder-nichts).
|
- **Kein granulareres Rollenmodell** — aktuell HR-only (alles-oder-nichts).
|
||||||
Falls z. B. eine reine Lese-Rolle künftig gebraucht wird, gehört die
|
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
|
Erweiterung in eine neue Migration (`is_hr_user()`/RLS-Policies), nicht in
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { withUser } from "@/lib/db";
|
|||||||
import { callFunction, runMutation, type ActionResult, type MutationFn } from "@/lib/db/rpc";
|
import { callFunction, runMutation, type ActionResult, type MutationFn } from "@/lib/db/rpc";
|
||||||
import { OFFBOARDING_PUNKTE } from "@/lib/offboarding";
|
import { OFFBOARDING_PUNKTE } from "@/lib/offboarding";
|
||||||
import { ONBOARDING_PUNKTE } from "@/lib/onboarding";
|
import { ONBOARDING_PUNKTE } from "@/lib/onboarding";
|
||||||
import type { CollectiveAgreement, DienstwagenArt, NoteCategory, RelationshipType, Weekday, WorkerType } from "@/lib/supabase/types";
|
import type { CollectiveAgreement, DienstwagenArt, NoteCategory, RelationshipType, Weekday, WorkerType } from "@/lib/types";
|
||||||
|
|
||||||
async function callRpc(fn: MutationFn, payload: Record<string, unknown>, revalidate: string[]): Promise<ActionResult> {
|
async function callRpc(fn: MutationFn, payload: Record<string, unknown>, revalidate: string[]): Promise<ActionResult> {
|
||||||
const result = await runMutation(await currentUserId(), fn, payload);
|
const result = await runMutation(await currentUserId(), fn, payload);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { derivedStatusFilter } from "@/lib/employee-status-filter";
|
|||||||
import { fmtDate, fmtName, todayIso } from "@/lib/format";
|
import { fmtDate, fmtName, todayIso } from "@/lib/format";
|
||||||
import { breadcrumbLabel, divisionOf, loadOrgMaps, subtreeOf, unitOf, type OrgEb } from "@/lib/org";
|
import { breadcrumbLabel, divisionOf, loadOrgMaps, subtreeOf, unitOf, type OrgEb } from "@/lib/org";
|
||||||
import { loadPlacements } from "@/lib/placement";
|
import { loadPlacements } from "@/lib/placement";
|
||||||
import type { EmploymentStatus } from "@/lib/supabase/types";
|
import type { EmploymentStatus } from "@/lib/types";
|
||||||
|
|
||||||
const PAGE_SIZE = 15;
|
const PAGE_SIZE = 15;
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Button, LINK_BUTTON_CLASS } from "@/components/ui/Button";
|
|||||||
// Without this file a failed render drops the user on Next.js's own error
|
// Without this file a failed render drops the user on Next.js's own error
|
||||||
// screen — no navigation, no way back, and in production just "a client-side
|
// screen — no navigation, no way back, and in production just "a client-side
|
||||||
// exception occurred". `reset()` re-renders the segment, which is enough for
|
// exception occurred". `reset()` re-renders the segment, which is enough for
|
||||||
// the common case of a transient Supabase timeout.
|
// the common case of a transient database timeout.
|
||||||
export default function AppError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
|
export default function AppError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.error("Route error:", error);
|
console.error("Route error:", error);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Every page in this group is server-rendered per request (they all read
|
// Every page in this group is server-rendered per request (they all read
|
||||||
// from Supabase), so without this the browser sits on the previous page with
|
// from the database), so without this the browser sits on the previous page with
|
||||||
// no feedback until the server answers — on the employee list, long enough
|
// no feedback until the server answers — on the employee list, long enough
|
||||||
// to look broken.
|
// to look broken.
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { callFunction } from "@/lib/db/rpc";
|
|||||||
|
|
||||||
// Applies effective-dated changes (Versetzung/Beförderung/Karenz/Reorg/Daten
|
// Applies effective-dated changes (Versetzung/Beförderung/Karenz/Reorg/Daten
|
||||||
// ändern with a future "Wirksam ab" date) once their date has arrived — see
|
// ändern with a future "Wirksam ab" date) once their date has arrived — see
|
||||||
// apply_due_pending_changes() in supabase/migrations.
|
// apply_due_pending_changes() in db/migrations.
|
||||||
//
|
//
|
||||||
// Gerufen wird das vom `cron`-Dienst aus docker-compose.yml, täglich um 03:00.
|
// Gerufen wird das vom `cron`-Dienst aus docker-compose.yml, täglich um 03:00.
|
||||||
// Nicht im Namen einer HR-Person: es gibt keine angemeldete Sitzung, deshalb
|
// Nicht im Namen einer HR-Person: es gibt keine angemeldete Sitzung, deshalb
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { deriveStatusAsOf, parseIsoDateParam, parseStatuses, type OrgLookups } f
|
|||||||
import { applyCriteria, loadDependentsCounts, loadOrgLookups, type ReportFilters } from "@/lib/reports-data";
|
import { applyCriteria, loadDependentsCounts, loadOrgLookups, type ReportFilters } from "@/lib/reports-data";
|
||||||
import { requireHrUser } from "@/lib/auth/require-hr";
|
import { requireHrUser } from "@/lib/auth/require-hr";
|
||||||
import { withUser } from "@/lib/db";
|
import { withUser } from "@/lib/db";
|
||||||
import type { Database, Weekday } from "@/lib/supabase/types";
|
import type { Database, Weekday } from "@/lib/types";
|
||||||
|
|
||||||
// Die Rohzeile plus die Einordnung, die nicht mehr auf ihr steht: sie kommt
|
// Die Rohzeile plus die Einordnung, die nicht mehr auf ihr steht: sie kommt
|
||||||
// über die Planstelle und die abgeleitete Berichtslinie.
|
// über die Planstelle und die abgeleitete Berichtslinie.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useState } from "react";
|
|||||||
import { AenderungsTabelle } from "@/components/ui/AenderungsTabelle";
|
import { AenderungsTabelle } from "@/components/ui/AenderungsTabelle";
|
||||||
import { SlideOver } from "@/components/ui/SlideOver";
|
import { SlideOver } from "@/components/ui/SlideOver";
|
||||||
import { actionBadgeStyle } from "@/lib/colors";
|
import { actionBadgeStyle } from "@/lib/colors";
|
||||||
import type { AuditChange } from "@/lib/supabase/types";
|
import type { AuditChange } from "@/lib/types";
|
||||||
|
|
||||||
// Eine Protokollzeile zum Aufklappen.
|
// Eine Protokollzeile zum Aufklappen.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { SelectField, TextField } from "@/components/ui/Field";
|
|||||||
import { Modal } from "@/components/ui/Modal";
|
import { Modal } from "@/components/ui/Modal";
|
||||||
import { useToast } from "@/components/ui/Toast";
|
import { useToast } from "@/components/ui/Toast";
|
||||||
import { todayIso } from "@/lib/format";
|
import { todayIso } from "@/lib/format";
|
||||||
import type { RelationshipType } from "@/lib/supabase/types";
|
import type { RelationshipType } from "@/lib/types";
|
||||||
|
|
||||||
const RELATIONSHIPS: RelationshipType[] = ["Ehepartner:in", "Lebenspartner:in", "Kind", "Sonstige"];
|
const RELATIONSHIPS: RelationshipType[] = ["Ehepartner:in", "Lebenspartner:in", "Kind", "Sonstige"];
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { deleteEmployeeDependent } from "@/actions/employees";
|
|||||||
import { Button } from "@/components/ui/Button";
|
import { Button } from "@/components/ui/Button";
|
||||||
import { useToast } from "@/components/ui/Toast";
|
import { useToast } from "@/components/ui/Toast";
|
||||||
import { fmtDate, todayIso } from "@/lib/format";
|
import { fmtDate, todayIso } from "@/lib/format";
|
||||||
import type { Database } from "@/lib/supabase/types";
|
import type { Database } from "@/lib/types";
|
||||||
import { AddDependentModal } from "./AddDependentModal";
|
import { AddDependentModal } from "./AddDependentModal";
|
||||||
|
|
||||||
type Dependent = Database["public"]["Tables"]["employee_dependents"]["Row"];
|
type Dependent = Database["public"]["Tables"]["employee_dependents"]["Row"];
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { fmtFullName, tenure } from "@/lib/format";
|
|||||||
import { fortschritt as fortschrittOffboarding, gehoertOffboarding } from "@/lib/offboarding";
|
import { fortschritt as fortschrittOffboarding, gehoertOffboarding } from "@/lib/offboarding";
|
||||||
import { fortschritt as fortschrittOnboarding } from "@/lib/onboarding";
|
import { fortschritt as fortschrittOnboarding } from "@/lib/onboarding";
|
||||||
import type { OpenPositionResolved } from "@/lib/positions";
|
import type { OpenPositionResolved } from "@/lib/positions";
|
||||||
import type { Database } from "@/lib/supabase/types";
|
import type { Database } from "@/lib/types";
|
||||||
import { DatenAendernPanel } from "./panels/DatenAendernPanel";
|
import { DatenAendernPanel } from "./panels/DatenAendernPanel";
|
||||||
import { KarenzPanel } from "./panels/KarenzPanel";
|
import { KarenzPanel } from "./panels/KarenzPanel";
|
||||||
import { PromotePanel } from "./panels/PromotePanel";
|
import { PromotePanel } from "./panels/PromotePanel";
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { TextField } from "@/components/ui/Field";
|
|||||||
import { Modal } from "@/components/ui/Modal";
|
import { Modal } from "@/components/ui/Modal";
|
||||||
import { useToast } from "@/components/ui/Toast";
|
import { useToast } from "@/components/ui/Toast";
|
||||||
import { fmtDate } from "@/lib/format";
|
import { fmtDate } from "@/lib/format";
|
||||||
import type { AuditChange } from "@/lib/supabase/types";
|
import type { AuditChange } from "@/lib/types";
|
||||||
|
|
||||||
// Berichtigen, nicht neu erfassen.
|
// Berichtigen, nicht neu erfassen.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { SelectField, TextField } from "@/components/ui/Field";
|
import { SelectField, TextField } from "@/components/ui/Field";
|
||||||
import type { CollectiveAgreement, DienstwagenArt, Weekday, WorkerType } from "@/lib/supabase/types";
|
import type { CollectiveAgreement, DienstwagenArt, Weekday, WorkerType } from "@/lib/types";
|
||||||
|
|
||||||
const WEEKDAYS: Weekday[] = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
|
const WEEKDAYS: Weekday[] = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { brauchtAufenthaltstitel, UN_COUNTRIES } from "@/lib/countries";
|
|||||||
import { STUNDEN_GRUENDE } from "@/lib/absence";
|
import { STUNDEN_GRUENDE } from "@/lib/absence";
|
||||||
import { fmtFullName, todayIso } from "@/lib/format";
|
import { fmtFullName, todayIso } from "@/lib/format";
|
||||||
import { isValidSvnr, requiresAustrianSvnr } from "@/lib/svnr";
|
import { isValidSvnr, requiresAustrianSvnr } from "@/lib/svnr";
|
||||||
import { EMERGENCY_RELATIONS, type ContractType, type Database, type EmploymentType, type GenderType } from "@/lib/supabase/types";
|
import { EMERGENCY_RELATIONS, type ContractType, type Database, type EmploymentType, type GenderType } from "@/lib/types";
|
||||||
|
|
||||||
type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"];
|
type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"];
|
||||||
type Dependent = Database["public"]["Tables"]["employee_dependents"]["Row"];
|
type Dependent = Database["public"]["Tables"]["employee_dependents"]["Row"];
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { SlideOver } from "@/components/ui/SlideOver";
|
|||||||
import { useToast } from "@/components/ui/Toast";
|
import { useToast } from "@/components/ui/Toast";
|
||||||
import { ABSENCE_TYPES, absenceLabel, RUECKKEHR_GRUENDE } from "@/lib/absence";
|
import { ABSENCE_TYPES, absenceLabel, RUECKKEHR_GRUENDE } from "@/lib/absence";
|
||||||
import { fmtDate, fmtName } from "@/lib/format";
|
import { fmtDate, fmtName } from "@/lib/format";
|
||||||
import type { Database } from "@/lib/supabase/types";
|
import type { Database } from "@/lib/types";
|
||||||
|
|
||||||
type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"];
|
type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"];
|
||||||
type Mode = "adjust" | "return";
|
type Mode = "adjust" | "return";
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Button } from "@/components/ui/Button";
|
|||||||
import { SelectField, TextField } from "@/components/ui/Field";
|
import { SelectField, TextField } from "@/components/ui/Field";
|
||||||
import { SlideOver } from "@/components/ui/SlideOver";
|
import { SlideOver } from "@/components/ui/SlideOver";
|
||||||
import { useToast } from "@/components/ui/Toast";
|
import { useToast } from "@/components/ui/Toast";
|
||||||
import type { Database, PaygradeType } from "@/lib/supabase/types";
|
import type { Database, PaygradeType } from "@/lib/types";
|
||||||
import { fmtName } from "@/lib/format";
|
import { fmtName } from "@/lib/format";
|
||||||
|
|
||||||
type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"];
|
type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"];
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { SlideOver } from "@/components/ui/SlideOver";
|
|||||||
import { useToast } from "@/components/ui/Toast";
|
import { useToast } from "@/components/ui/Toast";
|
||||||
import { fmtDate, fmtName } from "@/lib/format";
|
import { fmtDate, fmtName } from "@/lib/format";
|
||||||
import type { OpenPositionResolved } from "@/lib/positions";
|
import type { OpenPositionResolved } from "@/lib/positions";
|
||||||
import type { Database } from "@/lib/supabase/types";
|
import type { Database } from "@/lib/types";
|
||||||
|
|
||||||
type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"];
|
type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"];
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Button } from "@/components/ui/Button";
|
|||||||
import { SelectField, TextField, TextareaField } from "@/components/ui/Field";
|
import { SelectField, TextField, TextareaField } from "@/components/ui/Field";
|
||||||
import { SlideOver } from "@/components/ui/SlideOver";
|
import { SlideOver } from "@/components/ui/SlideOver";
|
||||||
import { useToast } from "@/components/ui/Toast";
|
import { useToast } from "@/components/ui/Toast";
|
||||||
import type { Database } from "@/lib/supabase/types";
|
import type { Database } from "@/lib/types";
|
||||||
import { fmtDate, fmtName } from "@/lib/format";
|
import { fmtDate, fmtName } from "@/lib/format";
|
||||||
|
|
||||||
type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"];
|
type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"];
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { SelectField, TextField } from "@/components/ui/Field";
|
|||||||
import { SlideOver } from "@/components/ui/SlideOver";
|
import { SlideOver } from "@/components/ui/SlideOver";
|
||||||
import { useToast } from "@/components/ui/Toast";
|
import { useToast } from "@/components/ui/Toast";
|
||||||
import type { OpenPositionResolved } from "@/lib/positions";
|
import type { OpenPositionResolved } from "@/lib/positions";
|
||||||
import type { Database } from "@/lib/supabase/types";
|
import type { Database } from "@/lib/types";
|
||||||
import { fmtName } from "@/lib/format";
|
import { fmtName } from "@/lib/format";
|
||||||
|
|
||||||
type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"];
|
type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"];
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { SegmentedControl } from "@/components/ui/SegmentedControl";
|
|||||||
import { actionBadgeStyle } from "@/lib/colors";
|
import { actionBadgeStyle } from "@/lib/colors";
|
||||||
import { fmtDate, todayIso } from "@/lib/format";
|
import { fmtDate, todayIso } from "@/lib/format";
|
||||||
import { darfBearbeitetWerden, darfKorrigiertWerden, loeschVorschau } from "@/lib/history";
|
import { darfBearbeitetWerden, darfKorrigiertWerden, loeschVorschau } from "@/lib/history";
|
||||||
import type { Database } from "@/lib/supabase/types";
|
import type { Database } from "@/lib/types";
|
||||||
|
|
||||||
type HistoryRow = Database["public"]["Tables"]["employee_history"]["Row"];
|
type HistoryRow = Database["public"]["Tables"]["employee_history"]["Row"];
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { SelectField, TextField, TextareaField } from "@/components/ui/Field";
|
|||||||
import { useToast } from "@/components/ui/Toast";
|
import { useToast } from "@/components/ui/Toast";
|
||||||
import { NOTE_CATEGORY_STYLES } from "@/lib/colors";
|
import { NOTE_CATEGORY_STYLES } from "@/lib/colors";
|
||||||
import { fmtDate } from "@/lib/format";
|
import { fmtDate } from "@/lib/format";
|
||||||
import type { Database, NoteCategory } from "@/lib/supabase/types";
|
import type { Database, NoteCategory } from "@/lib/types";
|
||||||
|
|
||||||
type Note = Database["public"]["Tables"]["employee_notes"]["Row"];
|
type Note = Database["public"]["Tables"]["employee_notes"]["Row"];
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { AngehoerigeSection } from "@/components/employees/AngehoerigeSection";
|
import { AngehoerigeSection } from "@/components/employees/AngehoerigeSection";
|
||||||
import { brauchtAufenthaltstitel } from "@/lib/countries";
|
import { brauchtAufenthaltstitel } from "@/lib/countries";
|
||||||
import { fmtAge, fmtDate } from "@/lib/format";
|
import { fmtAge, fmtDate } from "@/lib/format";
|
||||||
import type { Database } from "@/lib/supabase/types";
|
import type { Database } from "@/lib/types";
|
||||||
|
|
||||||
type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"];
|
type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"];
|
||||||
type Location = Database["public"]["Tables"]["locations"]["Row"];
|
type Location = Database["public"]["Tables"]["locations"]["Row"];
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { dienstwagenLabel } from "@/lib/dienstwagen";
|
import { dienstwagenLabel } from "@/lib/dienstwagen";
|
||||||
import { fmtDate } from "@/lib/format";
|
import { fmtDate } from "@/lib/format";
|
||||||
import type { Database } from "@/lib/supabase/types";
|
import type { Database } from "@/lib/types";
|
||||||
|
|
||||||
type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"];
|
type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"];
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Button } from "@/components/ui/Button";
|
|||||||
import { SelectField, TextField } from "@/components/ui/Field";
|
import { SelectField, TextField } from "@/components/ui/Field";
|
||||||
import { fmtDate } from "@/lib/format";
|
import { fmtDate } from "@/lib/format";
|
||||||
import { formatSvnr, svnrErrorMessage, validateSvnr } from "@/lib/svnr";
|
import { formatSvnr, svnrErrorMessage, validateSvnr } from "@/lib/svnr";
|
||||||
import type { RelationshipType } from "@/lib/supabase/types";
|
import type { RelationshipType } from "@/lib/types";
|
||||||
import type { HireDraftAngehoerige, HireDraftData } from "./types";
|
import type { HireDraftAngehoerige, HireDraftData } from "./types";
|
||||||
|
|
||||||
const VERHAELTNIS: RelationshipType[] = ["Ehepartner:in", "Lebenspartner:in", "Kind", "Sonstige"];
|
const VERHAELTNIS: RelationshipType[] = ["Ehepartner:in", "Lebenspartner:in", "Kind", "Sonstige"];
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { SelectField, TextField } from "@/components/ui/Field";
|
import { SelectField, TextField } from "@/components/ui/Field";
|
||||||
import { EMERGENCY_RELATIONS } from "@/lib/supabase/types";
|
import { EMERGENCY_RELATIONS } from "@/lib/types";
|
||||||
import type { HireDraftData } from "./types";
|
import type { HireDraftData } from "./types";
|
||||||
|
|
||||||
// Eigener Schritt, kurz vor der Zusammenfassung.
|
// Eigener Schritt, kurz vor der Zusammenfassung.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { RoleEmploymentFields } from "@/components/employees/RoleEmploymentFields";
|
import { RoleEmploymentFields } from "@/components/employees/RoleEmploymentFields";
|
||||||
import { SelectField, TextField } from "@/components/ui/Field";
|
import { SelectField, TextField } from "@/components/ui/Field";
|
||||||
import type { PaygradeType } from "@/lib/supabase/types";
|
import type { PaygradeType } from "@/lib/types";
|
||||||
import type { HireDraftData } from "./types";
|
import type { HireDraftData } from "./types";
|
||||||
|
|
||||||
const PAYGRADES: { value: PaygradeType; label: string; description: string }[] = [
|
const PAYGRADES: { value: PaygradeType; label: string; description: string }[] = [
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { CollectiveAgreement, ContractType, DienstwagenArt, EmploymentType, GenderType, PaygradeType, RelationshipType, Weekday, WorkerType } from "@/lib/supabase/types";
|
import type { CollectiveAgreement, ContractType, DienstwagenArt, EmploymentType, GenderType, PaygradeType, RelationshipType, Weekday, WorkerType } from "@/lib/types";
|
||||||
|
|
||||||
// The spec's hire wizard field list (§4.4) omits Geschlecht and Standort even
|
// The spec's hire wizard field list (§4.4) omits Geschlecht and Standort even
|
||||||
// though both are NOT NULL on employees — added here (defaults keep them
|
// though both are NOT NULL on employees — added here (defaults keep them
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import {
|
|||||||
type ReportRow,
|
type ReportRow,
|
||||||
type UnitOption,
|
type UnitOption,
|
||||||
} from "@/lib/reports";
|
} from "@/lib/reports";
|
||||||
import type { HistoryEventType } from "@/lib/supabase/types";
|
import type { HistoryEventType } from "@/lib/types";
|
||||||
|
|
||||||
const SPLIT_COLORS = ["bg-brand-500", "bg-info-text", "bg-purple-text", "bg-warning-text", "bg-success-text", "bg-danger-solid"];
|
const SPLIT_COLORS = ["bg-brand-500", "bg-info-text", "bg-purple-text", "bg-warning-text", "bg-success-text", "bg-danger-solid"];
|
||||||
const EVENT_TYPES = Object.keys(EVENT_TYPE_LABELS) as HistoryEventType[];
|
const EVENT_TYPES = Object.keys(EVENT_TYPE_LABELS) as HistoryEventType[];
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { AuditChange } from "@/lib/supabase/types";
|
import type { AuditChange } from "@/lib/types";
|
||||||
|
|
||||||
// Was sich geändert hat, feldweise — im Protokoll und in der Historie einer
|
// Was sich geändert hat, feldweise — im Protokoll und in der Historie einer
|
||||||
// Person dieselbe Darstellung. Zwei Ansichten derselben Sache verschieden zu
|
// Person dieselbe Darstellung. Zwei Ansichten derselben Sache verschieden zu
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { absenceLabel } from "@/lib/absence";
|
import { absenceLabel } from "@/lib/absence";
|
||||||
import { STATUS_STYLES } from "@/lib/colors";
|
import { STATUS_STYLES } from "@/lib/colors";
|
||||||
import { fmtDate } from "@/lib/format";
|
import { fmtDate } from "@/lib/format";
|
||||||
import type { EmploymentStatus } from "@/lib/supabase/types";
|
import type { EmploymentStatus } from "@/lib/types";
|
||||||
|
|
||||||
type StatusChipProps = {
|
type StatusChipProps = {
|
||||||
status: EmploymentStatus;
|
status: EmploymentStatus;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user