"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 (
); } 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; children: ReactNode; label?: string; onSuccess?: (result: StaffActionResult) => void; }) { const [pending, start] = useTransition(); const [error, setError] = useState(""); const router = useRouter(); return (
{ 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.", ); } }); }} >
{children}
{error && (

{error}

)}
); } export function CommandButton({ command, profileId, data, children, variant = "outline", }: { command: string; profileId: string; data: Record; children: ReactNode; variant?: "outline" | "ghost" | "default"; }) { const [pending, start] = useTransition(); const router = useRouter(); return ( ); } export function ReviewButton({ command, profileId, data, label, approve, }: { command: "leaveReview" | "weekReview"; profileId: string; data: Record; label: string; approve: boolean; }) { const [open, setOpen] = useState(false); return ( {label} Die Entscheidung wird im Personalverlauf dokumentiert. setOpen(false)} > ); } export function SetupForm({ educatorId, values, }: { educatorId: string; values?: { employmentStart: string; employmentEnd: string | null; trackingStart: string; openingMinutes: number; }; }) { return (
); } 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 ( {initial ? "Arbeitszeit bearbeiten" : "Arbeitszeit erfassen"} Erfasse Beginn, Ende und die gesamte Pause dieses Arbeitsblocks. Weitere Blöcke am selben Tag sind möglich. setOpen(false)} >
); } 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 ( {initial ? "Antrag ändern" : "Abwesenheit beantragen"} Urlaub verbraucht nur deine vorgesehenen Arbeitstage. Freizeitausgleich wird in Stunden für einen einzelnen Tag beantragt. setOpen(false)} >
{kind === "VACATION" && ( )} {kind === "TIME_OFF" && ( )}

Der Antrag wird anhand deines Wochenmodells, der Tagesausnahmen und der Feiertage geprüft. Ein halber Tag entspricht der Hälfte der Sollzeit.

); } function SettingsSection({ title, children, }: { title: string; children: ReactNode; }) { return (
{title}
{children}
); } 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 (
{verified ? (

Aktives Konto: {email}

) : ( setInvitePath(r.invitePath ?? "")} >

Erstellt oder verknüpft einen ErzieherInnen-Account. Der persönliche Link ist sieben Tage gültig. Ein neuer Link ersetzt den vorherigen.

)} {invitePath && (

Bitte persönlich an die Fachkraft weitergeben. Es wird keine E-Mail versendet.

)}
{DAY_NAMES.map((day, i) => ( ))}

0 Stunden = frei. Stunden sind Nettozeiten ohne Pause. Änderungen gelten ab dem gewählten Datum; ältere Modelle bleiben erhalten.

Anspruch individuell für die Arbeitstage dieser Person hinterlegen. Der Übertrag wird ausdrücklich festgelegt.

Bei einem Tausch beide Tage erfassen: den ursprünglichen Tag auf 0 setzen und den neuen Arbeitstag mit Sollstunden hinterlegen.

Korrekturen bleiben im Verlauf erhalten. Falsche Buchungen durch eine Gegenbuchung berichtigen.

Die Grunddaten sind geschützt, sobald Zeiten, Wochen oder Anträge erfasst wurden.

); } export function HolidayForm() { return (
); }