Remove Supabase
Some checks failed
CI / Lint, Typen, Tests, Build (push) Failing after 5m40s
CI / Migrationen auf leerer Datenbank (push) Has been cancelled

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:
2026-09-07 10:43:22 +02:00
parent 5c310c3a58
commit b87c8ad64c
155 changed files with 386 additions and 4014 deletions

View File

@@ -15,6 +15,3 @@ npm-debug.log*
*.tsbuildinfo *.tsbuildinfo
.scratch_* .scratch_*
.scratch_shots .scratch_shots
supabase/.branches
supabase/.temp
supabase/snippets

View File

@@ -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.

View File

@@ -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
View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);

View File

@@ -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() {

View File

@@ -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

View File

@@ -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.

View File

@@ -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.
// //

View File

@@ -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"];

View File

@@ -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"];

View File

@@ -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";

View File

@@ -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.
// //

View File

@@ -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"];

View File

@@ -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"];

View File

@@ -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";

View File

@@ -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"];

View File

@@ -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"];

View File

@@ -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"];

View File

@@ -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"];

View File

@@ -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"];

View File

@@ -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"];

View File

@@ -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"];

View File

@@ -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"];

View File

@@ -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"];

View File

@@ -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.

View File

@@ -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 }[] = [

View File

@@ -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

View File

@@ -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[];

View File

@@ -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

View File

@@ -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