Files
alpenwerk-hr/components/employees/tabs/StammdatenTab.tsx
Maximilian Stubhan b87c8ad64c
Some checks failed
CI / Lint, Typen, Tests, Build (push) Failing after 5m40s
CI / Migrationen auf leerer Datenbank (push) Has been cancelled
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>
2026-09-07 10:43:22 +02:00

114 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { AngehoerigeSection } from "@/components/employees/AngehoerigeSection";
import { brauchtAufenthaltstitel } from "@/lib/countries";
import { fmtAge, fmtDate } from "@/lib/format";
import type { Database } from "@/lib/types";
type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"];
type Location = Database["public"]["Tables"]["locations"]["Row"];
type Dependent = Database["public"]["Tables"]["employee_dependents"]["Row"];
export function StammdatenTab({ employee, location, dependents }: { employee: EmployeeRow; location?: Location; dependents: Dependent[] }) {
// Defensive against a DB that hasn't received the title_prefix/title_suffix
// migration yet — select("*") simply omits unknown columns, so these can
// be undefined rather than the empty array the column default implies.
const prefixe = employee.title_prefix ?? [];
const suffixe = employee.title_suffix ?? [];
// Feld für Feld dieselbe Liste wie im Abschnitt „Person" von „Daten
// ändern", in derselben Reihenfolge.
//
// Vorher fasste die Anzeige zusammen: Titel in einer Zeile, Adresse mit
// Postleitzahl und Ort verschmolzen, Vor- und Nachname gar nicht — die
// standen nur in der Kopfzeile. Wer eine Angabe prüfen wollte, musste den
// Änderungsdialog öffnen, um sie überhaupt zu sehen, und stand dann schon
// in einem Formular. Was sich ändern lässt, soll sich auch ansehen lassen.
const rows: [string, string][] = [
["Personalnummer", String(employee.personnel_number)],
["Vorname", employee.first_name],
["Nachname", employee.last_name],
["Titel (vorangestellt)", prefixe.length > 0 ? prefixe.join(", ") : "–"],
["Titel (nachgestellt)", suffixe.length > 0 ? suffixe.join(", ") : "–"],
["Geschlecht", employee.gender === "m" ? "männlich" : "weiblich"],
["Geburtsdatum", `${fmtDate(employee.birth_date)} (${fmtAge(employee.birth_date)} Jahre)`],
["SV-Nummer", employee.sv_nummer ?? "–"],
["Staatsbürgerschaft", employee.nationality],
// Nur, wo er verlangt ist. Bei einer österreichischen Staatsbürger-
// schaft wäre die Zeile „Aufenthaltstitel: Nein" keine Auskunft,
// sondern eine Frage, die sich nicht stellt.
...(brauchtAufenthaltstitel(employee.nationality)
? ([
[
"Aufenthaltstitel",
employee.hat_aufenthaltstitel
? employee.aufenthaltstitel_bis
? `Ja, bis ${fmtDate(employee.aufenthaltstitel_bis)}`
: "Ja (unbefristet oder nicht erfasst)"
: "Nein",
],
] as [string, string][])
: []),
["Adresse", employee.address ?? "–"],
["Postleitzahl", employee.postal_code ?? "–"],
["Ort", employee.city ?? "–"],
["Land", employee.address_country ?? "–"],
["Private E-Mail", employee.email ?? "–"],
["Private Telefonnummer", employee.phone ?? "–"],
// Der Standort ist keine Angabe zur Person, sondern die Betriebsstätte —
// er steht deshalb am Ende und nicht zwischen Adresse und Land, wo man
// ihn für den Wohnort halten könnte.
["Standort", location ? `${location.name} (${location.country})` : "–"],
];
return (
<div className="flex flex-col gap-6">
<dl className="grid grid-cols-1 gap-x-8 gap-y-4 sm:grid-cols-2 lg:grid-cols-3">
{rows.map(([label, value]) => (
<div key={label}>
<dt className="text-xs font-semibold uppercase tracking-wide text-ink-muted">{label}</dt>
<dd className="mt-1 text-sm text-ink">{value}</dd>
</div>
))}
</dl>
<AngehoerigeSection employeeId={employee.id} dependents={dependents} />
{/* Eigener Abschnitt statt einer Zelle im Raster, und unterhalb der
Angehörigen: beides sind Personen im Umfeld, und der Notfallkontakt
ist die Ausnahme davon — deshalb steht er zuletzt, nicht dazwischen.
Im Ernstfall greift jemand in Eile danach; dann muss die Nummer
sofort zu finden sein und wählbar. */}
<div className="border-t border-border pt-6">
<h3 className="mb-3 text-xs font-bold uppercase tracking-wide text-brand-700">Notfallkontakt</h3>
{employee.emergency_contact_name ? (
<dl className="grid grid-cols-1 gap-x-8 gap-y-4 sm:grid-cols-2 lg:grid-cols-3">
<div>
<dt className="text-xs font-semibold uppercase tracking-wide text-ink-muted">Name</dt>
<dd className="mt-1 text-sm text-ink">{employee.emergency_contact_name}</dd>
</div>
<div>
<dt className="text-xs font-semibold uppercase tracking-wide text-ink-muted">Telefon</dt>
<dd className="mt-1 text-sm font-semibold text-ink">
{employee.emergency_contact_phone ? (
<a
href={`tel:${employee.emergency_contact_phone.replace(/\s/g, "")}`}
className="rounded hover:text-brand-700 hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-500"
>
{employee.emergency_contact_phone}
</a>
) : (
"–"
)}
</dd>
</div>
<div>
<dt className="text-xs font-semibold uppercase tracking-wide text-ink-muted">Verhältnis</dt>
<dd className="mt-1 text-sm text-ink">{employee.emergency_contact_relation || "–"}</dd>
</div>
</dl>
) : (
<p className="text-sm text-ink-muted">Kein Notfallkontakt hinterlegt.</p>
)}
</div>
</div>
);
}