diff --git a/actions/employees.ts b/actions/employees.ts index 72997bb..36f1d1c 100644 --- a/actions/employees.ts +++ b/actions/employees.ts @@ -345,6 +345,25 @@ export async function addEmployeeDependent(payload: { return callRpc("add_employee_dependent", payload, [`/employees/${payload.employee_id}`]); } +/** + * Eine Angabe berichtigen — ohne Stichtag. + * + * Hinzufügen und Entfernen tragen einen: dort passiert etwas zu einem + * Zeitpunkt. Eine Berichtigung nicht — der Wert war schon vorher falsch, und + * ein „wirksam ab" hiesse, die Person habe bis dahin anders geheissen. + */ +export async function updateEmployeeDependent(payload: { + dependent_id: string; + employee_id: string; + first_name: string; + last_name: string; + relationship: RelationshipType; + sv_nummer?: string; + birth_date: string; +}): Promise { + return callRpc("update_employee_dependent", payload, [`/employees/${payload.employee_id}`]); +} + export async function deleteEmployeeDependent(payload: { dependent_id: string; employee_id: string; diff --git a/components/employees/AddDependentModal.tsx b/components/employees/AddDependentModal.tsx index f34fcae..096d740 100644 --- a/components/employees/AddDependentModal.tsx +++ b/components/employees/AddDependentModal.tsx @@ -2,34 +2,48 @@ import { useRouter } from "next/navigation"; import { useState } from "react"; -import { addEmployeeDependent } from "@/actions/employees"; +import { addEmployeeDependent, updateEmployeeDependent } from "@/actions/employees"; import { Button } from "@/components/ui/Button"; import { SelectField, TextField } from "@/components/ui/Field"; import { Modal } from "@/components/ui/Modal"; import { useToast } from "@/components/ui/Toast"; import { todayIso } from "@/lib/format"; -import type { RelationshipType } from "@/lib/types"; +import type { Database, RelationshipType } from "@/lib/types"; const RELATIONSHIPS: RelationshipType[] = ["Ehepartner:in", "Lebenspartner:in", "Kind", "Sonstige"]; +type Dependent = Database["public"]["Tables"]["employee_dependents"]["Row"]; + +// Dasselbe Fenster für Anlegen und Berichtigen. +// +// Der Unterschied ist nicht die Maske, sondern der Stichtag: beim Anlegen +// passiert etwas zu einem Zeitpunkt, beim Berichtigen war die Angabe schon +// vorher falsch. Deshalb steht „Wirksam ab" nur im ersten Fall — ein Datum +// bei einer Korrektur hiesse, die Person habe bis dahin anders geheissen. export function AddDependentModal({ open, onClose, employeeId, defaultEffectiveDate, + dependent, }: { open: boolean; onClose: () => void; employeeId: string; defaultEffectiveDate?: string; + /** Gesetzt: berichtigen statt anlegen. */ + dependent?: Dependent; }) { const { showToast } = useToast(); const router = useRouter(); - const [firstName, setFirstName] = useState(""); - const [lastName, setLastName] = useState(""); - const [svNummer, setSvNummer] = useState(""); - const [birthDate, setBirthDate] = useState(""); - const [relationship, setRelationship] = useState("Kind"); + const bearbeiten = Boolean(dependent); + const [firstName, setFirstName] = useState(dependent?.first_name ?? ""); + const [lastName, setLastName] = useState(dependent?.last_name ?? ""); + const [svNummer, setSvNummer] = useState(dependent?.sv_nummer ?? ""); + const [birthDate, setBirthDate] = useState(dependent?.birth_date ?? ""); + const [relationship, setRelationship] = useState( + (dependent?.relationship as RelationshipType) ?? "Kind" + ); const [effectiveDate, setEffectiveDate] = useState(defaultEffectiveDate ?? todayIso()); const [prevDefaultEffectiveDate, setPrevDefaultEffectiveDate] = useState(defaultEffectiveDate); const [pending, setPending] = useState(false); @@ -53,26 +67,36 @@ export function AddDependentModal({ } async function handleSubmit() { - if (!firstName || !lastName || !birthDate || !effectiveDate) { + if (!firstName || !lastName || !birthDate || (!bearbeiten && !effectiveDate)) { showToast("Bitte alle Pflichtfelder ausfüllen.", "error"); return; } setPending(true); - const result = await addEmployeeDependent({ - employee_id: employeeId, - first_name: firstName, - last_name: lastName, - relationship, - sv_nummer: svNummer || undefined, - birth_date: birthDate, - effective_date: effectiveDate, - }); + const result = bearbeiten + ? await updateEmployeeDependent({ + dependent_id: dependent!.id, + employee_id: employeeId, + first_name: firstName, + last_name: lastName, + relationship, + sv_nummer: svNummer, + birth_date: birthDate, + }) + : await addEmployeeDependent({ + employee_id: employeeId, + first_name: firstName, + last_name: lastName, + relationship, + sv_nummer: svNummer || undefined, + birth_date: birthDate, + effective_date: effectiveDate, + }); setPending(false); if (result.success) { - showToast("Angehörige:r hinzugefügt."); + showToast(bearbeiten ? "Angaben berichtigt." : "Angehörige:r hinzugefügt."); router.refresh(); onClose(); - reset(); + if (!bearbeiten) reset(); } else { showToast(result.error ?? "Fehler beim Speichern.", "error"); } @@ -82,20 +106,22 @@ export function AddDependentModal({ } >
- + {!bearbeiten && ( + + )}
diff --git a/components/employees/AngehoerigeSection.tsx b/components/employees/AngehoerigeSection.tsx index 7b9461f..5c018ea 100644 --- a/components/employees/AngehoerigeSection.tsx +++ b/components/employees/AngehoerigeSection.tsx @@ -1,6 +1,6 @@ "use client"; -import { Plus, Trash2 } from "lucide-react"; +import { Pencil, Plus, Trash2 } from "lucide-react"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { deleteEmployeeDependent } from "@/actions/employees"; @@ -19,6 +19,7 @@ export function AngehoerigeSection({ employeeId, dependents, effectiveDate }: { const { showToast } = useToast(); const router = useRouter(); const [modalOpen, setModalOpen] = useState(false); + const [bearbeitet, setBearbeitet] = useState(null); const [deletingId, setDeletingId] = useState(null); const resolvedEffectiveDate = effectiveDate || todayIso(); @@ -70,6 +71,18 @@ export function AngehoerigeSection({ employeeId, dependents, effectiveDate }: { {d.sv_nummer ?? "–"} {fmtDate(d.birth_date)} + {/* Berichtigen statt löschen-und-neu-anlegen: ein + Tippfehler hinterliess sonst zwei Einträge in der + Akte, und der erste sagte „entfernt", was nicht + stimmte. */} +
); } diff --git a/components/employees/NotfallkontaktModal.tsx b/components/employees/NotfallkontaktModal.tsx index b336901..a4f505d 100644 --- a/components/employees/NotfallkontaktModal.tsx +++ b/components/employees/NotfallkontaktModal.tsx @@ -31,11 +31,14 @@ export function NotfallkontaktModal({ onClose, employeeId, kontakt, + effectiveDate, }: { open: boolean; onClose: () => void; employeeId: string; kontakt: Notfallkontakt; + /** Aus „Daten ändern"; ohne Angabe gilt heute. */ + effectiveDate?: string; }) { const { showToast } = useToast(); const router = useRouter(); @@ -57,7 +60,7 @@ export function NotfallkontaktModal({ setPending(true); const result = await changeEmployeeData({ employee_id: employeeId, - effective_date: todayIso(), + effective_date: effectiveDate || todayIso(), person: { emergency_contact_name: name.trim(), emergency_contact_phone: telefon.trim(), diff --git a/components/employees/NotfallkontaktSection.tsx b/components/employees/NotfallkontaktSection.tsx index 1ff4c4a..17c98e9 100644 --- a/components/employees/NotfallkontaktSection.tsx +++ b/components/employees/NotfallkontaktSection.tsx @@ -19,11 +19,25 @@ import { NotfallkontaktModal, type Notfallkontakt } from "./NotfallkontaktModal" // ihn führte der einzige Weg über „Daten ändern" — ein Formular über alle // Stammdaten, um eine Telefonnummer einzutragen. -export function NotfallkontaktSection({ employeeId, kontakt }: { employeeId: string; kontakt: Notfallkontakt }) { +export function NotfallkontaktSection({ + employeeId, + kontakt, + effectiveDate, +}: { + employeeId: string; + kontakt: Notfallkontakt; + /** + * Nur, wenn der Abschnitt in „Daten ändern" steht: dann treibt dessen + * „Wirksam ab" auch diesen Vorgang — wie bei den Angehörigen darüber. + * Ohne Angabe gilt heute. + */ + effectiveDate?: string; +}) { const { showToast } = useToast(); const router = useRouter(); const [modalOpen, setModalOpen] = useState(false); const [entfernt, setEntfernt] = useState(false); + const stichtag = effectiveDate || todayIso(); const vorhanden = Boolean(kontakt.name); @@ -33,7 +47,7 @@ export function NotfallkontaktSection({ employeeId, kontakt }: { employeeId: str // daraus null. Würden die Schlüssel fehlen, bliebe der Kontakt stehen. const result = await changeEmployeeData({ employee_id: employeeId, - effective_date: todayIso(), + effective_date: stichtag, person: { emergency_contact_name: "", emergency_contact_phone: "", emergency_contact_relation: "" }, contract: {}, role: {}, @@ -135,6 +149,7 @@ export function NotfallkontaktSection({ employeeId, kontakt }: { employeeId: str onClose={() => setModalOpen(false)} employeeId={employeeId} kontakt={kontakt} + effectiveDate={stichtag} />
); diff --git a/components/employees/panels/DatenAendernPanel.tsx b/components/employees/panels/DatenAendernPanel.tsx index 4536c37..597b1eb 100644 --- a/components/employees/panels/DatenAendernPanel.tsx +++ b/components/employees/panels/DatenAendernPanel.tsx @@ -18,7 +18,8 @@ import { GRUND_BEGUENSTIGT_BEHINDERT } from "@/lib/kuendigungsschutz"; import { MITARBEITERART_STANDARD } from "@/lib/mitarbeiterart"; import { fmtFullName, todayIso } from "@/lib/format"; import { isValidSvnr, requiresAustrianSvnr } from "@/lib/svnr"; -import { EMERGENCY_RELATIONS, type ContractType, type Database, type EmploymentType, type GenderType } from "@/lib/types"; +import { NotfallkontaktSection } from "@/components/employees/NotfallkontaktSection"; +import type { ContractType, Database, EmploymentType, GenderType } from "@/lib/types"; type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"]; type Dependent = Database["public"]["Tables"]["employee_dependents"]["Row"]; @@ -66,9 +67,6 @@ export function DatenAendernPanel({ const [email, setEmail] = useState(employee.email ?? ""); const [companyEmail, setCompanyEmail] = useState(employee.company_email ?? ""); const [phone, setPhone] = useState(employee.phone ?? ""); - const [notfallName, setNotfallName] = useState(employee.emergency_contact_name ?? ""); - const [notfallTelefon, setNotfallTelefon] = useState(employee.emergency_contact_phone ?? ""); - const [notfallVerhaeltnis, setNotfallVerhaeltnis] = useState(employee.emergency_contact_relation ?? ""); const [employmentType, setEmploymentType] = useState(employee.employment_type); const [weeklyHours, setWeeklyHours] = useState(String(employee.weekly_hours)); @@ -136,12 +134,6 @@ export function DatenAendernPanel({ showToast("Bitte ein Wirksam-ab-Datum angeben.", "error"); return; } - // Name und Nummer gehören zusammen — die Datenbank weist eines ohne das - // andere ab, und die Meldung dort erklärt es nicht. - if (Boolean(notfallName.trim()) !== Boolean(notfallTelefon.trim())) { - showToast("Beim Notfallkontakt braucht es Name und Telefonnummer — oder beides leer.", "error"); - return; - } setPending(true); const result = await changeEmployeeData({ employee_id: employee.id, @@ -172,9 +164,10 @@ export function DatenAendernPanel({ email: email.trim(), company_email: companyEmail.trim(), phone: phone.trim(), - emergency_contact_name: notfallName.trim(), - emergency_contact_phone: notfallTelefon.trim(), - emergency_contact_relation: notfallVerhaeltnis.trim(), + // Der Notfallkontakt läuft über seinen eigenen Abschnitt weiter unten + // — wie die Angehörigen. Stünde er zusätzlich hier, schrieben zwei + // Stellen dasselbe Feld, und welche gewinnt, hinge an der + // Reihenfolge. }, contract: { employment_type: employmentType, @@ -386,27 +379,21 @@ export function DatenAendernPanel({ - {/* Unterhalb der Angehörigen und ganz am Schluss — dieselbe Reihenfolge - wie im Reiter „Stammdaten". Aufgebaut wie „Person", „Vertrag" und - „Rolle & Anstellung" darüber: eine Überschrift, darunter die - Felder. Hier stand ein umrahmtes fieldset mit eigener - Beschriftung; das hob den Abschnitt gegenüber allen anderen - heraus, ohne dass er wichtiger wäre. */} -
-

Notfallkontakt

-
- - - ({ value: r, label: r }))} - /> -
-
+ {/* Derselbe Abschnitt wie im Reiter „Stammdaten", unmittelbar unter den + Angehörigen — beides Personen im Umfeld, beides dieselbe Tabelle + mit denselben Knöpfen. Hier standen drei Eingabefelder: fachlich + dasselbe, aber daneben eine zweite Bauform für denselben Zweck. + Beide Abschnitte laufen jetzt über das „Wirksam ab" dieses + Formulars. */} + ); diff --git a/db/migrations/20260924160000_angehoerige_bearbeiten.sql b/db/migrations/20260924160000_angehoerige_bearbeiten.sql new file mode 100644 index 0000000..81fc49c --- /dev/null +++ b/db/migrations/20260924160000_angehoerige_bearbeiten.sql @@ -0,0 +1,93 @@ +-- Angehörige lassen sich bearbeiten. +-- +-- Bisher gab es nur Hinzufügen und Entfernen. Ein Tippfehler im Namen oder +-- ein falsches Geburtsdatum liess sich nur beheben, indem man die Person +-- löschte und neu anlegte — zwei Einträge in der Akte für eine Korrektur, und +-- der erste sagt „entfernt", was nicht stimmt. +-- +-- Anders als Hinzufügen und Entfernen **ohne Stichtag**: eine Berichtigung +-- gilt rückwirkend, denn der Wert war schon vorher falsch. Ein „wirksam ab" +-- hiesse, die Person habe bis dahin anders geheissen. Wer eine tatsächliche +-- Änderung zum Stichtag abbilden will — jemand heiratet und heisst fortan +-- anders —, entfernt und legt neu an; dafür sind die beiden Vorgänge da. +-- +-- Der Eintrag in der Akte nennt deshalb Vorher und Nachher, nicht ein Datum. + +create or replace function update_employee_dependent(payload jsonb) +returns void +language plpgsql +set search_path to 'public', 'pg_temp' +as $$ +declare + v_alt employee_dependents%rowtype; + v_employee_name text; + v_neu_name text := (payload->>'first_name') || ' ' || (payload->>'last_name'); + v_alt_name text; +begin + perform require_hr_admin(); + + select * into v_alt from employee_dependents where id = (payload->>'dependent_id')::uuid; + if not found then + raise exception 'Angehörige:r nicht gefunden.'; + end if; + v_alt_name := v_alt.first_name || ' ' || v_alt.last_name; + + select first_name || ' ' || last_name into v_employee_name + from employees where id = v_alt.employee_id; + + -- Nur die Felder, die mitkommen. Die SV-Nummer ist freiwillig und muss + -- sich leeren lassen, deshalb `case when ? then nullif` statt coalesce — + -- dieselbe Form wie bei den freiwilligen Feldern der Person + -- (20260923160000). + update employee_dependents set + first_name = coalesce(payload->>'first_name', first_name), + last_name = coalesce(payload->>'last_name', last_name), + relationship = coalesce(payload->>'relationship', relationship), + birth_date = coalesce((payload->>'birth_date')::date, birth_date), + sv_nummer = case when payload ? 'sv_nummer' then nullif(payload->>'sv_nummer', '') else sv_nummer end + where id = v_alt.id; + + insert into employee_history (employee_id, event_date, event_type, description) + values ( + v_alt.employee_id, current_date, 'Stammdatenänderung', + 'Angehörige:r berichtigt: ' || v_alt_name || ' (' || v_alt.relationship || ') → ' + || v_neu_name || ' (' || (payload->>'relationship') || ')' + ); + + insert into audit_log (actor_user_id, actor_name, action, target_label, target_employee_id, details, changes) + values ( + app_current_user_id(), current_actor_name(), 'Angehörige:r berichtigt', v_employee_name, v_alt.employee_id, + v_alt_name || ' → ' || v_neu_name, + app_aenderung( + app_aenderung( + app_aenderung( + app_aenderung('[]'::jsonb, 'Name', v_alt_name, v_neu_name), + 'Verhältnis', v_alt.relationship, payload->>'relationship'), + 'Geburtsdatum', v_alt.birth_date::text, payload->>'birth_date'), + 'SV-Nummer', v_alt.sv_nummer, nullif(payload->>'sv_nummer', '')) + ); +end; +$$; + +grant execute on function public.update_employee_dependent(payload jsonb) to alpenwerk_app; + + +-- Selbstprüfung. +do $$ +declare + v_def text := pg_get_functiondef('public.update_employee_dependent(jsonb)'::regprocedure); +begin + if v_def not like '%require_hr_admin%' then + raise exception 'update_employee_dependent prueft die Rechte nicht.'; + end if; + if position('SET search_path' in v_def) = 0 then + raise exception 'update_employee_dependent hat keinen festen search_path.'; + end if; + if v_def like '%auth.uid()%' then + raise exception 'update_employee_dependent ruft auth.uid().'; + end if; + if v_def not like '%insert into audit_log%' then + raise exception 'update_employee_dependent schreibt kein Protokoll.'; + end if; +end +$$; diff --git a/lib/types.ts b/lib/types.ts index 685545d..4fd84af 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -712,6 +712,9 @@ export type Database = { change_employee_data: { Args: { payload: Record }; Returns: void }; rehire_employee: { Args: { payload: Record }; Returns: void }; add_employee_dependent: { Args: { payload: Record }; Returns: void }; + // Berichtigen, nicht ändern zum Stichtag: ein falscher Name war schon + // vorher falsch. Deshalb ohne `effective_date` (20260924160000). + update_employee_dependent: { Args: { payload: Record }; Returns: void }; delete_employee_dependent: { Args: { payload: Record }; Returns: void }; add_employee_note: { Args: { payload: Record }; Returns: string }; complete_employee_note: { Args: { payload: Record }; Returns: void };