758 lines
21 KiB
TypeScript
758 lines
21 KiB
TypeScript
"use client";
|
|
|
|
import { useId, useState, useTransition, type ReactNode } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { toast } from "sonner";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from "@/components/ui/dialog";
|
|
import {
|
|
DAY_NAMES,
|
|
clockMinute,
|
|
minuteClock,
|
|
todayKey,
|
|
} from "@/lib/staff/calculations";
|
|
import { staffAction, type StaffActionResult } from "./actions";
|
|
|
|
const control =
|
|
"w-full rounded-lg border border-[var(--hairline)] bg-[var(--surface)] px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent-mid)]";
|
|
export function Field({
|
|
label,
|
|
name,
|
|
type = "text",
|
|
value,
|
|
required = true,
|
|
min,
|
|
max,
|
|
step,
|
|
}: {
|
|
label: string;
|
|
name: string;
|
|
type?: string;
|
|
value?: string | number;
|
|
required?: boolean;
|
|
min?: string | number;
|
|
max?: string | number;
|
|
step?: string | number;
|
|
}) {
|
|
const id = useId();
|
|
return (
|
|
<div className="grid gap-1.5">
|
|
<label htmlFor={id} className="text-sm font-medium">
|
|
{label}
|
|
</label>
|
|
<Input
|
|
id={id}
|
|
name={name}
|
|
type={type}
|
|
defaultValue={value}
|
|
required={required}
|
|
min={min}
|
|
max={max}
|
|
step={step}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
function transform(command: string, form: FormData) {
|
|
const text = (key: string) => String(form.get(key) ?? "");
|
|
const num = (key: string) => Number(text(key));
|
|
const hours = (key: string) => Math.round(num(key) * 60);
|
|
switch (command) {
|
|
case "setup":
|
|
return {
|
|
educatorId: text("educatorId"),
|
|
employmentStart: text("employmentStart"),
|
|
employmentEnd: text("employmentEnd") || null,
|
|
trackingStart: text("trackingStart"),
|
|
openingMinutes: hours("openingHours"),
|
|
reason: text("reason"),
|
|
};
|
|
case "invite":
|
|
return { email: text("email") };
|
|
case "employmentEnd":
|
|
return {
|
|
employmentEnd: text("employmentEnd") || null,
|
|
reason: text("reason"),
|
|
};
|
|
case "pattern":
|
|
return {
|
|
effectiveFrom: text("effectiveFrom"),
|
|
dailyMinutes: DAY_NAMES.map((_, i) => hours(`day${i}`)),
|
|
reason: text("reason"),
|
|
};
|
|
case "account":
|
|
return {
|
|
year: num("year"),
|
|
entitlementUnits: Math.round(num("entitlement") * 2),
|
|
carryUnits: Math.round(num("carry") * 2),
|
|
reason: text("reason"),
|
|
};
|
|
case "override":
|
|
return {
|
|
date: text("date"),
|
|
minutes: hours("hours"),
|
|
reason: text("reason"),
|
|
};
|
|
case "adjust":
|
|
return {
|
|
date: text("date"),
|
|
minutes: hours("hours"),
|
|
reason: text("reason"),
|
|
};
|
|
case "holiday":
|
|
return { date: text("date"), name: text("name") };
|
|
case "time":
|
|
return {
|
|
date: text("date"),
|
|
startMinute: clockMinute(text("start")),
|
|
endMinute: clockMinute(text("end")),
|
|
breakMinutes: num("breakMinutes"),
|
|
note: text("note"),
|
|
};
|
|
case "leave":
|
|
return {
|
|
kind: text("kind"),
|
|
startDate: text("startDate"),
|
|
endDate: text("endDate"),
|
|
halfDay: form.get("halfDay") === "on",
|
|
...(text("kind") === "TIME_OFF" ? { minutes: hours("hours") } : {}),
|
|
note: text("note"),
|
|
};
|
|
case "leaveReview":
|
|
case "weekReview":
|
|
return { reason: text("reason") };
|
|
default:
|
|
return {};
|
|
}
|
|
}
|
|
function StaffForm({
|
|
command,
|
|
profileId,
|
|
extra = {},
|
|
children,
|
|
label = "Speichern",
|
|
onSuccess,
|
|
}: {
|
|
command: string;
|
|
profileId?: string;
|
|
extra?: Record<string, unknown>;
|
|
children: ReactNode;
|
|
label?: string;
|
|
onSuccess?: (result: StaffActionResult) => void;
|
|
}) {
|
|
const [pending, start] = useTransition();
|
|
const [error, setError] = useState("");
|
|
const router = useRouter();
|
|
return (
|
|
<form
|
|
className="grid gap-4"
|
|
onSubmit={(event) => {
|
|
event.preventDefault();
|
|
const form = new FormData(event.currentTarget);
|
|
setError("");
|
|
start(async () => {
|
|
try {
|
|
const result = await staffAction(command, {
|
|
...transform(command, form),
|
|
...extra,
|
|
...(profileId ? { profileId } : {}),
|
|
});
|
|
if (result.error) {
|
|
setError(result.error);
|
|
return;
|
|
}
|
|
toast.success("Gespeichert.");
|
|
onSuccess?.(result);
|
|
router.refresh();
|
|
} catch (error) {
|
|
setError(
|
|
error instanceof Error
|
|
? error.message
|
|
: "Speichern fehlgeschlagen.",
|
|
);
|
|
}
|
|
});
|
|
}}
|
|
>
|
|
<fieldset disabled={pending} className="grid gap-4 disabled:opacity-60">
|
|
{children}
|
|
<div>
|
|
<Button type="submit">
|
|
{pending ? "Wird gespeichert …" : label}
|
|
</Button>
|
|
</div>
|
|
</fieldset>
|
|
{error && (
|
|
<p role="alert" className="text-sm text-[var(--danger)]">
|
|
{error}
|
|
</p>
|
|
)}
|
|
</form>
|
|
);
|
|
}
|
|
export function CommandButton({
|
|
command,
|
|
profileId,
|
|
data,
|
|
children,
|
|
variant = "outline",
|
|
}: {
|
|
command: string;
|
|
profileId: string;
|
|
data: Record<string, unknown>;
|
|
children: ReactNode;
|
|
variant?: "outline" | "ghost" | "default";
|
|
}) {
|
|
const [pending, start] = useTransition();
|
|
const router = useRouter();
|
|
return (
|
|
<Button
|
|
type="button"
|
|
variant={variant}
|
|
size="sm"
|
|
disabled={pending}
|
|
onClick={() =>
|
|
start(async () => {
|
|
const result = await staffAction(command, { ...data, profileId });
|
|
if (result.error) toast.error(result.error);
|
|
else {
|
|
toast.success("Gespeichert.");
|
|
router.refresh();
|
|
}
|
|
})
|
|
}
|
|
>
|
|
{pending ? "Bitte warten …" : children}
|
|
</Button>
|
|
);
|
|
}
|
|
export function ReviewButton({
|
|
command,
|
|
profileId,
|
|
data,
|
|
label,
|
|
approve,
|
|
}: {
|
|
command: "leaveReview" | "weekReview";
|
|
profileId: string;
|
|
data: Record<string, unknown>;
|
|
label: string;
|
|
approve: boolean;
|
|
}) {
|
|
const [open, setOpen] = useState(false);
|
|
return (
|
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button size="sm" variant={approve ? "default" : "outline"}>
|
|
{label}
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>{label}</DialogTitle>
|
|
<DialogDescription>
|
|
Die Entscheidung wird im Personalverlauf dokumentiert.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<StaffForm
|
|
command={command}
|
|
profileId={profileId}
|
|
extra={{ ...data, approve }}
|
|
label={label}
|
|
onSuccess={() => setOpen(false)}
|
|
>
|
|
<Field label="Begründung / Rückmeldung" name="reason" />
|
|
</StaffForm>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|
|
export function SetupForm({
|
|
educatorId,
|
|
values,
|
|
}: {
|
|
educatorId: string;
|
|
values?: {
|
|
employmentStart: string;
|
|
employmentEnd: string | null;
|
|
trackingStart: string;
|
|
openingMinutes: number;
|
|
};
|
|
}) {
|
|
return (
|
|
<StaffForm
|
|
command="setup"
|
|
extra={{ educatorId }}
|
|
label="Beschäftigungsprofil speichern"
|
|
>
|
|
<input type="hidden" name="educatorId" value={educatorId} />
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
<Field
|
|
label="Beschäftigung ab"
|
|
name="employmentStart"
|
|
type="date"
|
|
value={values?.employmentStart ?? todayKey()}
|
|
/>
|
|
<Field
|
|
label="Beschäftigung bis (optional)"
|
|
name="employmentEnd"
|
|
type="date"
|
|
value={values?.employmentEnd ?? ""}
|
|
required={false}
|
|
/>
|
|
<Field
|
|
label="Zeiterfassung ab"
|
|
name="trackingStart"
|
|
type="date"
|
|
value={values?.trackingStart ?? todayKey()}
|
|
/>
|
|
<Field
|
|
label="Anfangssaldo in Stunden"
|
|
name="openingHours"
|
|
type="number"
|
|
step="any"
|
|
value={(values?.openingMinutes ?? 0) / 60}
|
|
/>
|
|
</div>
|
|
<Field label="Begründung" name="reason" value="Ersteinrichtung" />
|
|
</StaffForm>
|
|
);
|
|
}
|
|
export function TimeForm({
|
|
profileId,
|
|
initial,
|
|
}: {
|
|
profileId: string;
|
|
initial?: {
|
|
id: string;
|
|
date: string;
|
|
startMinute: number;
|
|
endMinute: number;
|
|
breakMinutes: number;
|
|
note: string | null;
|
|
};
|
|
}) {
|
|
const [open, setOpen] = useState(false);
|
|
return (
|
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button
|
|
variant={initial ? "outline" : "default"}
|
|
size={initial ? "sm" : "default"}
|
|
>
|
|
{initial ? "Bearbeiten" : "+ Arbeitszeit erfassen"}
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>
|
|
{initial ? "Arbeitszeit bearbeiten" : "Arbeitszeit erfassen"}
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
Erfasse Beginn, Ende und die gesamte Pause dieses Arbeitsblocks.
|
|
Weitere Blöcke am selben Tag sind möglich.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<StaffForm
|
|
command="time"
|
|
profileId={profileId}
|
|
extra={initial ? { id: initial.id } : {}}
|
|
label="Arbeitszeit speichern"
|
|
onSuccess={() => setOpen(false)}
|
|
>
|
|
<Field
|
|
label="Datum"
|
|
name="date"
|
|
type="date"
|
|
value={initial?.date ?? todayKey()}
|
|
max={todayKey()}
|
|
/>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<Field
|
|
label="Beginn"
|
|
name="start"
|
|
type="time"
|
|
value={minuteClock(initial?.startMinute ?? 480)}
|
|
/>
|
|
<Field
|
|
label="Ende"
|
|
name="end"
|
|
type="time"
|
|
value={minuteClock(initial?.endMinute ?? 960)}
|
|
/>
|
|
</div>
|
|
<Field
|
|
label="Pause in Minuten"
|
|
name="breakMinutes"
|
|
type="number"
|
|
min={0}
|
|
step={1}
|
|
value={initial?.breakMinutes ?? 30}
|
|
/>
|
|
<Field
|
|
label="Notiz (optional)"
|
|
name="note"
|
|
value={initial?.note ?? ""}
|
|
required={false}
|
|
/>
|
|
</StaffForm>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|
|
export function LeaveForm({
|
|
profileId,
|
|
admin,
|
|
initial,
|
|
}: {
|
|
profileId: string;
|
|
admin: boolean;
|
|
initial?: {
|
|
id: string;
|
|
kind: string;
|
|
startDate: string;
|
|
endDate: string;
|
|
halfDay: boolean;
|
|
minutes: number | null;
|
|
note: string | null;
|
|
};
|
|
}) {
|
|
const [open, setOpen] = useState(false);
|
|
const [kind, setKind] = useState(initial?.kind ?? "VACATION");
|
|
const id = useId();
|
|
return (
|
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button size={initial ? "sm" : "default"} variant="outline">
|
|
{initial ? "Ändern" : "+ Abwesenheit beantragen"}
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>
|
|
{initial ? "Antrag ändern" : "Abwesenheit beantragen"}
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
Urlaub verbraucht nur deine vorgesehenen Arbeitstage.
|
|
Freizeitausgleich wird in Stunden für einen einzelnen Tag beantragt.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<StaffForm
|
|
command="leave"
|
|
profileId={profileId}
|
|
extra={initial ? { id: initial.id } : {}}
|
|
label={
|
|
kind === "SICKNESS" ? "Krankheit hinterlegen" : "Antrag speichern"
|
|
}
|
|
onSuccess={() => setOpen(false)}
|
|
>
|
|
<div className="grid gap-1.5">
|
|
<label htmlFor={`${id}-kind`} className="text-sm font-medium">
|
|
Art
|
|
</label>
|
|
<select
|
|
id={`${id}-kind`}
|
|
name="kind"
|
|
className={control}
|
|
value={kind}
|
|
onChange={(e) => setKind(e.target.value)}
|
|
>
|
|
<option value="VACATION">Urlaub</option>
|
|
<option value="TIME_OFF">Freizeitausgleich</option>
|
|
{admin && <option value="SICKNESS">Krankheit</option>}
|
|
</select>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<Field
|
|
label="Von"
|
|
name="startDate"
|
|
type="date"
|
|
value={initial?.startDate ?? todayKey()}
|
|
/>
|
|
<Field
|
|
label="Bis"
|
|
name="endDate"
|
|
type="date"
|
|
value={initial?.endDate ?? todayKey()}
|
|
/>
|
|
</div>
|
|
{kind === "VACATION" && (
|
|
<label className="flex items-center gap-2 text-sm">
|
|
<input
|
|
name="halfDay"
|
|
type="checkbox"
|
|
defaultChecked={initial?.halfDay}
|
|
/>
|
|
Halber Urlaubstag (Von = Bis)
|
|
</label>
|
|
)}
|
|
{kind === "TIME_OFF" && (
|
|
<Field
|
|
label="Freizeitausgleich in Stunden (Von = Bis)"
|
|
name="hours"
|
|
type="number"
|
|
min={1 / 60}
|
|
max={16}
|
|
step="any"
|
|
value={(initial?.minutes ?? 60) / 60}
|
|
/>
|
|
)}
|
|
<Field
|
|
label="Notiz (optional)"
|
|
name="note"
|
|
value={initial?.note ?? ""}
|
|
required={false}
|
|
/>
|
|
<p className="text-xs leading-5 text-[var(--ink-soft)]">
|
|
Der Antrag wird anhand deines Wochenmodells, der Tagesausnahmen und
|
|
der Feiertage geprüft. Ein halber Tag entspricht der Hälfte der
|
|
Sollzeit.
|
|
</p>
|
|
</StaffForm>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|
|
function SettingsSection({
|
|
title,
|
|
children,
|
|
}: {
|
|
title: string;
|
|
children: ReactNode;
|
|
}) {
|
|
return (
|
|
<details className="rounded-xl border border-[var(--hairline)] bg-[var(--surface)] p-5">
|
|
<summary className="cursor-pointer font-semibold">{title}</summary>
|
|
<div className="mt-5">{children}</div>
|
|
</details>
|
|
);
|
|
}
|
|
export function SettingsPanel({
|
|
profileId,
|
|
educatorId,
|
|
employment,
|
|
email,
|
|
verified,
|
|
pattern,
|
|
year,
|
|
account,
|
|
}: {
|
|
profileId: string;
|
|
educatorId: string;
|
|
employment: {
|
|
employmentStart: string;
|
|
employmentEnd: string | null;
|
|
trackingStart: string;
|
|
openingMinutes: number;
|
|
};
|
|
email?: string;
|
|
verified: boolean;
|
|
pattern?: { effectiveFrom: string; dailyMinutes: number[] };
|
|
year: number;
|
|
account?: { entitlementUnits: number; carryUnits: number };
|
|
}) {
|
|
const [invitePath, setInvitePath] = useState("");
|
|
return (
|
|
<div className="grid gap-3">
|
|
<SettingsSection title="Zugang zur Zeiterfassung">
|
|
{verified ? (
|
|
<p className="text-sm">Aktives Konto: {email}</p>
|
|
) : (
|
|
<StaffForm
|
|
command="invite"
|
|
profileId={profileId}
|
|
label="Aktivierungslink erstellen"
|
|
onSuccess={(r) => setInvitePath(r.invitePath ?? "")}
|
|
>
|
|
<Field
|
|
label="E-Mail-Adresse"
|
|
name="email"
|
|
type="email"
|
|
value={email ?? ""}
|
|
/>
|
|
<p className="text-sm text-[var(--ink-soft)]">
|
|
Erstellt oder verknüpft einen ErzieherInnen-Account. Der
|
|
persönliche Link ist sieben Tage gültig. Ein neuer Link ersetzt
|
|
den vorherigen.
|
|
</p>
|
|
</StaffForm>
|
|
)}
|
|
{invitePath && (
|
|
<div className="mt-4 grid gap-2 rounded-lg bg-[var(--accent-soft)] p-4">
|
|
<label htmlFor="activation-link" className="text-sm font-medium">
|
|
Persönlicher Aktivierungslink
|
|
</label>
|
|
<Input
|
|
id="activation-link"
|
|
readOnly
|
|
value={`${window.location.origin}${invitePath}`}
|
|
/>
|
|
<Button
|
|
variant="outline"
|
|
onClick={async () => {
|
|
await navigator.clipboard.writeText(
|
|
`${window.location.origin}${invitePath}`,
|
|
);
|
|
toast.success("Link kopiert.");
|
|
}}
|
|
>
|
|
Link kopieren
|
|
</Button>
|
|
<p className="text-xs">
|
|
Bitte persönlich an die Fachkraft weitergeben. Es wird keine
|
|
E-Mail versendet.
|
|
</p>
|
|
</div>
|
|
)}
|
|
</SettingsSection>
|
|
<SettingsSection title="Feste Wochentage & Sollstunden">
|
|
<StaffForm
|
|
command="pattern"
|
|
profileId={profileId}
|
|
label="Wochenmodell speichern"
|
|
>
|
|
<Field
|
|
label="Gültig ab"
|
|
name="effectiveFrom"
|
|
type="date"
|
|
value={pattern?.effectiveFrom ?? employment.trackingStart}
|
|
/>
|
|
<div className="grid gap-3 sm:grid-cols-2">
|
|
{DAY_NAMES.map((day, i) => (
|
|
<Field
|
|
key={day}
|
|
label={`${day} · Stunden`}
|
|
name={`day${i}`}
|
|
type="number"
|
|
min={0}
|
|
max={16}
|
|
step="any"
|
|
value={(pattern?.dailyMinutes[i] ?? 0) / 60}
|
|
/>
|
|
))}
|
|
</div>
|
|
<Field label="Begründung" name="reason" />
|
|
<p className="text-xs text-[var(--ink-soft)]">
|
|
0 Stunden = frei. Stunden sind Nettozeiten ohne Pause. Änderungen
|
|
gelten ab dem gewählten Datum; ältere Modelle bleiben erhalten.
|
|
</p>
|
|
</StaffForm>
|
|
</SettingsSection>
|
|
<SettingsSection title={`Urlaubsanspruch & Übertrag · ${year}`}>
|
|
<StaffForm command="account" profileId={profileId}>
|
|
<Field
|
|
label="Jahr"
|
|
name="year"
|
|
type="number"
|
|
min={2000}
|
|
max={2100}
|
|
value={year}
|
|
/>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<Field
|
|
label="Jahresanspruch in Tagen"
|
|
name="entitlement"
|
|
type="number"
|
|
min={0}
|
|
max={400}
|
|
step={0.5}
|
|
value={(account?.entitlementUnits ?? 0) / 2}
|
|
/>
|
|
<Field
|
|
label="Übertrag in Tagen"
|
|
name="carry"
|
|
type="number"
|
|
min={0}
|
|
max={400}
|
|
step={0.5}
|
|
value={(account?.carryUnits ?? 0) / 2}
|
|
/>
|
|
</div>
|
|
<Field label="Begründung" name="reason" />
|
|
<p className="text-xs text-[var(--ink-soft)]">
|
|
Anspruch individuell für die Arbeitstage dieser Person hinterlegen.
|
|
Der Übertrag wird ausdrücklich festgelegt.
|
|
</p>
|
|
</StaffForm>
|
|
</SettingsSection>
|
|
<SettingsSection title="Ausnahme für einen einzelnen Tag">
|
|
<StaffForm command="override" profileId={profileId}>
|
|
<Field label="Datum" name="date" type="date" value={todayKey()} />
|
|
<Field
|
|
label="Abweichende Sollstunden (0 = frei)"
|
|
name="hours"
|
|
type="number"
|
|
min={0}
|
|
max={16}
|
|
step="any"
|
|
value={0}
|
|
/>
|
|
<Field label="Begründung" name="reason" />
|
|
<p className="text-xs text-[var(--ink-soft)]">
|
|
Bei einem Tausch beide Tage erfassen: den ursprünglichen Tag auf 0
|
|
setzen und den neuen Arbeitstag mit Sollstunden hinterlegen.
|
|
</p>
|
|
</StaffForm>
|
|
</SettingsSection>
|
|
<SettingsSection title="Zeitkonto korrigieren">
|
|
<StaffForm command="adjust" profileId={profileId}>
|
|
<Field
|
|
label="Buchungsdatum"
|
|
name="date"
|
|
type="date"
|
|
value={todayKey()}
|
|
/>
|
|
<Field
|
|
label="Korrektur in Stunden (mit Vorzeichen)"
|
|
name="hours"
|
|
type="number"
|
|
step="any"
|
|
/>
|
|
<Field label="Begründung" name="reason" />
|
|
<p className="text-xs text-[var(--ink-soft)]">
|
|
Korrekturen bleiben im Verlauf erhalten. Falsche Buchungen durch
|
|
eine Gegenbuchung berichtigen.
|
|
</p>
|
|
</StaffForm>
|
|
</SettingsSection>
|
|
<SettingsSection title="Beschäftigungsbeginn & Anfangssaldo">
|
|
<SetupForm educatorId={educatorId} values={employment} />
|
|
<p className="mt-3 text-xs text-[var(--ink-soft)]">
|
|
Die Grunddaten sind geschützt, sobald Zeiten, Wochen oder Anträge
|
|
erfasst wurden.
|
|
</p>
|
|
</SettingsSection>
|
|
<SettingsSection title="Beschäftigungsende ändern">
|
|
<StaffForm command="employmentEnd" profileId={profileId}>
|
|
<Field
|
|
label="Beschäftigung bis (leer = unbefristet)"
|
|
name="employmentEnd"
|
|
type="date"
|
|
value={employment.employmentEnd ?? ""}
|
|
required={false}
|
|
/>
|
|
<Field label="Begründung" name="reason" />
|
|
</StaffForm>
|
|
</SettingsSection>
|
|
</div>
|
|
);
|
|
}
|
|
export function HolidayForm() {
|
|
return (
|
|
<StaffForm command="holiday" label="Feiertag speichern">
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
<Field label="Datum" name="date" type="date" />
|
|
<Field label="Bezeichnung" name="name" />
|
|
</div>
|
|
</StaffForm>
|
|
);
|
|
}
|