Den Notfallkontakt an Ort und Stelle anlegen statt ueber "Daten aendern"

Bei den Angehoerigen steht ein "+ Hinzufuegen", beim Notfallkontakt stand
nichts: der einzige Weg zu einer Telefonnummer fuehrte ueber ein Formular
ueber saemtliche Stammdaten. Jetzt steht der Knopf an derselben Stelle,
mit einem Fenster fuer Name, Telefonnummer und Verhaeltnis; ist ein
Kontakt hinterlegt, heisst er "Aendern" und daneben laesst er sich
entfernen.

Geschrieben wird ueber dieselbe Funktion wie in "Daten aendern" --
change_employee_data mit nur den drei Feldern. Der Eintrag landet damit
in der Historie und im Protokoll wie jede andere Stammdatenaenderung. Die
Datenbank fasst nur die Felder an, die im Aufruf stehen: sie prueft auf
das Vorhandensein des Schluessels, nicht auf seinen Wert (Migration
20260917120000, Zeilen 209-211). Adresse, E-Mail und der Rest bleiben
unberuehrt.
This commit is contained in:
2026-09-23 10:41:24 +02:00
parent 1baf8c2867
commit 1816ab68ad
3 changed files with 244 additions and 37 deletions

View File

@@ -0,0 +1,108 @@
"use client";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { changeEmployeeData } from "@/actions/employees";
import { Button } from "@/components/ui/Button";
import { TextField } from "@/components/ui/Field";
import { Modal } from "@/components/ui/Modal";
import { useToast } from "@/components/ui/Toast";
import { todayIso } from "@/lib/format";
// Der Notfallkontakt, ohne den Umweg über „Daten ändern".
//
// Geschrieben wird über dieselbe Funktion wie dort: change_employee_data mit
// **nur** den drei Feldern im Abschnitt `person`. Das ist keine Abkürzung,
// sondern die Bedingung dafür, dass der Eintrag in der Historie und im
// Protokoll landet wie jede andere Stammdatenänderung. Die Datenbank fasst
// ausschliesslich die Felder an, die im Aufruf stehen — sie prüft auf das
// Vorhandensein des Schlüssels, nicht auf seinen Wert (Migration
// 20260917120000). Adresse, E-Mail und der Rest bleiben deshalb unberührt.
export type Notfallkontakt = {
name: string | null;
telefon: string | null;
verhaeltnis: string | null;
};
export function NotfallkontaktModal({
open,
onClose,
employeeId,
kontakt,
}: {
open: boolean;
onClose: () => void;
employeeId: string;
kontakt: Notfallkontakt;
}) {
const { showToast } = useToast();
const router = useRouter();
const [name, setName] = useState(kontakt.name ?? "");
const [telefon, setTelefon] = useState(kontakt.telefon ?? "");
const [verhaeltnis, setVerhaeltnis] = useState(kontakt.verhaeltnis ?? "");
const [effectiveDate, setEffectiveDate] = useState(todayIso());
const [pending, setPending] = useState(false);
const vorhanden = Boolean(kontakt.name);
async function handleSubmit() {
// Name und Telefonnummer gehören zusammen: ein Name ohne Nummer hilft im
// Ernstfall niemandem, eine Nummer ohne Namen sagt nicht, wen man dran
// hat.
if (!name.trim() || !telefon.trim()) {
showToast("Name und Telefonnummer sind nötig.", "error");
return;
}
setPending(true);
const result = await changeEmployeeData({
employee_id: employeeId,
effective_date: effectiveDate,
person: {
emergency_contact_name: name.trim(),
emergency_contact_phone: telefon.trim(),
emergency_contact_relation: verhaeltnis.trim(),
},
contract: {},
role: {},
});
setPending(false);
if (result.success) {
showToast(vorhanden ? "Notfallkontakt geändert." : "Notfallkontakt hinterlegt.");
router.refresh();
onClose();
} else {
showToast(result.error ?? "Fehler beim Speichern.", "error");
}
}
return (
<Modal
open={open}
onClose={onClose}
title={vorhanden ? "Notfallkontakt ändern" : "Notfallkontakt hinterlegen"}
footer={
<>
<Button variant="ghost" onClick={onClose}>
Abbrechen
</Button>
<Button onClick={handleSubmit} pending={pending}>
Speichern
</Button>
</>
}
>
<div className="flex flex-col gap-4">
<TextField label="Wirksam ab" required type="date" value={effectiveDate} onChange={setEffectiveDate} />
<TextField label="Name" required value={name} onChange={setName} />
<TextField label="Telefonnummer" required type="tel" value={telefon} onChange={setTelefon} />
<TextField
label="Verhältnis"
value={verhaeltnis}
onChange={setVerhaeltnis}
hint="Zum Beispiel Gattin, Bruder, Freundin."
/>
</div>
</Modal>
);
}

View File

@@ -0,0 +1,127 @@
"use client";
import { Pencil, Plus, Trash2 } from "lucide-react";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { changeEmployeeData } from "@/actions/employees";
import { Button } from "@/components/ui/Button";
import { useToast } from "@/components/ui/Toast";
import { todayIso } from "@/lib/format";
import { NotfallkontaktModal, type Notfallkontakt } from "./NotfallkontaktModal";
// 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.
//
// Der Knopf steht hier, weil er bei den Angehörigen darüber auch steht. Ohne
// 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 }) {
const { showToast } = useToast();
const router = useRouter();
const [modalOpen, setModalOpen] = useState(false);
const [entfernt, setEntfernt] = useState(false);
const vorhanden = Boolean(kontakt.name);
async function handleEntfernen() {
setEntfernt(true);
// Leere Zeichenketten, nicht ausgelassene Felder: die Datenbank macht
// daraus null. Würden die Schlüssel fehlen, bliebe der Kontakt stehen.
const result = await changeEmployeeData({
employee_id: employeeId,
effective_date: todayIso(),
person: { emergency_contact_name: "", emergency_contact_phone: "", emergency_contact_relation: "" },
contract: {},
role: {},
});
setEntfernt(false);
if (result.success) {
showToast("Notfallkontakt entfernt.");
router.refresh();
} else {
showToast(result.error ?? "Fehler beim Entfernen.", "error");
}
}
return (
<div className="border-t border-border pt-6">
<div className="mb-3 flex items-center justify-between">
<h3 className="text-xs font-bold uppercase tracking-wide text-brand-700">Notfallkontakt</h3>
<div className="flex items-center gap-4">
<Button
variant="ghost"
size="sm"
onClick={() => setModalOpen(true)}
className="!px-1 text-brand-700 hover:!bg-transparent hover:underline"
>
{vorhanden ? (
<>
<Pencil className="h-3.5 w-3.5" /> Ändern
</>
) : (
<>
<Plus className="h-3.5 w-3.5" /> Hinzufügen
</>
)}
</Button>
{vorhanden && (
<Button
variant="icon"
onClick={handleEntfernen}
pending={entfernt}
aria-label="Notfallkontakt entfernen"
className="hover:!text-danger-solid"
>
<Trash2 className="h-3.5 w-3.5" />
</Button>
)}
</div>
</div>
{vorhanden ? (
<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">{kontakt.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">
{kontakt.telefon ? (
<a
href={`tel:${kontakt.telefon.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"
>
{kontakt.telefon}
</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">{kontakt.verhaeltnis || "–"}</dd>
</div>
</dl>
) : (
<p className="text-sm text-ink-muted">Kein Notfallkontakt hinterlegt.</p>
)}
{/* `key` setzt die Felder auf den gespeicherten Stand zurück, sobald
sich der Kontakt geändert hat — das Fenster bleibt sonst montiert
und zeigte beim zweiten Öffnen den alten Entwurf. */}
<NotfallkontaktModal
key={`${kontakt.name ?? ""}|${kontakt.telefon ?? ""}|${kontakt.verhaeltnis ?? ""}`}
open={modalOpen}
onClose={() => setModalOpen(false)}
employeeId={employeeId}
kontakt={kontakt}
/>
</div>
);
}

View File

@@ -1,4 +1,5 @@
import { AngehoerigeSection } from "@/components/employees/AngehoerigeSection"; import { AngehoerigeSection } from "@/components/employees/AngehoerigeSection";
import { NotfallkontaktSection } from "@/components/employees/NotfallkontaktSection";
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/types"; import type { Database } from "@/lib/types";
@@ -71,43 +72,14 @@ export function StammdatenTab({ employee, location, dependents }: { employee: Em
<AngehoerigeSection employeeId={employee.id} dependents={dependents} /> <AngehoerigeSection employeeId={employee.id} dependents={dependents} />
{/* Eigener Abschnitt statt einer Zelle im Raster, und unterhalb der <NotfallkontaktSection
Angehörigen: beides sind Personen im Umfeld, und der Notfallkontakt employeeId={employee.id}
ist die Ausnahme davon — deshalb steht er zuletzt, nicht dazwischen. kontakt={{
Im Ernstfall greift jemand in Eile danach; dann muss die Nummer name: employee.emergency_contact_name,
sofort zu finden sein und wählbar. */} telefon: employee.emergency_contact_phone,
<div className="border-t border-border pt-6"> verhaeltnis: employee.emergency_contact_relation,
<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> </div>
); );
} }