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>
114 lines
5.0 KiB
TypeScript
114 lines
5.0 KiB
TypeScript
import { Plus, Trash2 } from "lucide-react";
|
|
import { Button } from "@/components/ui/Button";
|
|
import { SelectField, TextField } from "@/components/ui/Field";
|
|
import { fmtDate } from "@/lib/format";
|
|
import { formatSvnr, svnrErrorMessage, validateSvnr } from "@/lib/svnr";
|
|
import type { RelationshipType } from "@/lib/types";
|
|
import type { HireDraftAngehoerige, HireDraftData } from "./types";
|
|
|
|
const VERHAELTNIS: RelationshipType[] = ["Ehepartner:in", "Lebenspartner:in", "Kind", "Sonstige"];
|
|
|
|
// Angehörige im Assistenten, obwohl es die Person noch nicht gibt.
|
|
//
|
|
// Sie werden hier gesammelt und erst nach dem Anlegen angehängt — die
|
|
// Datenbankfunktion braucht eine Kennung, und die entsteht mit der
|
|
// Einstellung. Der Preis dafür steht in HireWizard: schlägt eine der
|
|
// Ergänzungen fehl, ist die Person trotzdem angelegt, und die Meldung sagt
|
|
// das dann auch.
|
|
//
|
|
// Freiwillig: die meisten Einstellungen kommen ohne aus, und wer später
|
|
// etwas nachträgt, findet denselben Dialog in der Personalakte.
|
|
export function StepAngehoerige({
|
|
draft,
|
|
update,
|
|
}: {
|
|
draft: HireDraftData;
|
|
update: (patch: Partial<HireDraftData>) => void;
|
|
}) {
|
|
const liste = draft.angehoerige;
|
|
|
|
function setze(index: number, patch: Partial<HireDraftAngehoerige>) {
|
|
update({ angehoerige: liste.map((a, i) => (i === index ? { ...a, ...patch } : a)) });
|
|
}
|
|
|
|
function hinzufuegen() {
|
|
update({
|
|
angehoerige: [...liste, { firstName: "", lastName: draft.lastName, relationship: "Kind", birthDate: "", svNummer: "" }],
|
|
});
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col gap-4">
|
|
<p className="max-w-prose text-sm text-ink-muted">
|
|
Angehörige sind freiwillig und lassen sich jederzeit in der Personalakte nachtragen. Der Nachname ist mit dem
|
|
der einzustellenden Person vorbelegt — überschreibbar.
|
|
</p>
|
|
|
|
{liste.length === 0 ? (
|
|
<p className="text-sm text-ink-muted">Keine Angehörigen erfasst.</p>
|
|
) : (
|
|
<div className="flex flex-col gap-3">
|
|
{liste.map((a, i) => {
|
|
// Dieselbe Prüfung wie bei der Person selbst: Prüfziffer und
|
|
// Geburtsdatum müssen zusammenpassen. Hier schon, damit der
|
|
// Fehler nicht erst nach dem Anlegen auftaucht — dann existiert
|
|
// die Person bereits und die Angehörige fehlt.
|
|
const svFehler = a.svNummer.trim() ? validateSvnr(a.svNummer, a.birthDate || null) : null;
|
|
return (
|
|
<fieldset key={i} className="rounded-md border border-border p-3">
|
|
<legend className="flex items-center gap-2 px-1 text-xs font-semibold uppercase tracking-wide text-ink-muted">
|
|
{a.firstName || a.lastName ? `${a.firstName} ${a.lastName}`.trim() : `Angehörige:r ${i + 1}`}
|
|
{a.birthDate && <span className="font-normal normal-case">· {fmtDate(a.birthDate)}</span>}
|
|
</legend>
|
|
|
|
<div className="flex flex-col gap-3">
|
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
|
<TextField label="Vorname" dense required value={a.firstName} onChange={(v) => setze(i, { firstName: v })} />
|
|
<TextField label="Nachname" dense required value={a.lastName} onChange={(v) => setze(i, { lastName: v })} />
|
|
</div>
|
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
|
<SelectField
|
|
label="Verhältnis"
|
|
dense
|
|
required
|
|
value={a.relationship}
|
|
onChange={(v) => setze(i, { relationship: v as RelationshipType })}
|
|
options={VERHAELTNIS.map((r) => ({ value: r, label: r }))}
|
|
/>
|
|
<TextField label="Geburtsdatum" dense required type="date" value={a.birthDate} onChange={(v) => setze(i, { birthDate: v })} />
|
|
</div>
|
|
<TextField
|
|
label="SV-Nummer"
|
|
dense
|
|
value={a.svNummer}
|
|
onChange={(v) => setze(i, { svNummer: v })}
|
|
error={svFehler ? svnrErrorMessage(svFehler) : undefined}
|
|
hint={!svFehler && a.svNummer.trim() ? formatSvnr(a.svNummer) : undefined}
|
|
/>
|
|
</div>
|
|
|
|
<div className="mt-2 flex justify-end">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => update({ angehoerige: liste.filter((_, j) => j !== i) })}
|
|
className="!px-1 text-danger-text hover:!bg-transparent hover:underline"
|
|
>
|
|
<Trash2 className="h-3.5 w-3.5" /> Entfernen
|
|
</Button>
|
|
</div>
|
|
</fieldset>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
<div>
|
|
<Button variant="secondary" size="sm" onClick={hinzufuegen}>
|
|
<Plus className="h-4 w-4" /> Angehörige:n hinzufügen
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|