A round of interface corrections from use, plus one schema change behind them. The private email address is now optional. It was NOT NULL — the wrong default for a private detail: someone without one had to invent one, and invented data in a personnel file is worse than missing data. Both fields are relabelled to say whose they are, "Private E-Mail" and "Private Telefonnummer", because the company address does not exist until the person starts. Uniqueness stays; several NULLs coexist in a Postgres unique index, which is exactly what is wanted. The summary step still promised that "Personalnummer und Firmen-E-Mail-Adresse werden automatisch vergeben". Neither is true any more. Removed rather than reworded — the step lists what was entered, and a banner claiming otherwise is worse than no banner. Dependents move into the wizard as step three, optional. They can only be attached after the hire, because add_employee_dependent needs an id that does not exist while the form is open, so they are collected in the draft and written afterwards. That puts them outside the transaction the person is created in: if one fails the person still exists, so the message names who is missing instead of failing silently, and the SV number is checked in the step rather than after. The emergency contact gets its own step, second to last, and its relationship is a dropdown of the common ones rather than free text — otherwise "Gattin", "Ehefrau" and "Frau" end up side by side and nothing can be counted. "Sonstige" is there because a closed list would otherwise be presumptuous. On the master-data tab it now sits below the dependents rather than above: both are people around the employee, and this is the one you reach for in a hurry. Returning from a long absence: the choice read "unverändert", which made you open the file to find out what you were agreeing to. It now reads "Wie vor Abwesenheit (38,5 h)" with the hours actually worked, and the alternative is "Reduziert" — whose hours field starts empty on purpose. A number already filled in gets confirmed rather than read off the agreement it comes from. 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/supabase/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>
|
|
);
|
|
}
|