"use client"; import { useMemo, useState, useTransition } from "react"; import { useRouter } from "next/navigation"; import { Check, ChevronLeft, ChevronRight, Clock3, Info, Loader2, LockKeyhole, X, } from "lucide-react"; import { eachDayOfInterval, endOfMonth, endOfWeek, format, isSameMonth, startOfDay, startOfMonth, startOfWeek, } from "date-fns"; import { de } from "date-fns/locale"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; import { formatDateKey, formatUtcDateKey } from "@/lib/date-utils"; import { getNotdienstDays } from "@/lib/notdienst-days"; import { saveNotdienstAvailabilities } from "./actions"; export const notdienstEventCategories = { closure: { label: "Schließtag", dot: "oklch(0.62 0.10 35)", bg: "oklch(0.96 0.04 35)", border: "oklch(0.88 0.04 35)", text: "oklch(0.38 0.08 35)", }, team: { label: "Teamtag", dot: "oklch(0.62 0.10 70)", bg: "oklch(0.96 0.04 70)", border: "oklch(0.88 0.04 70)", text: "oklch(0.38 0.08 70)", }, parents: { label: "Elternabend", dot: "oklch(0.62 0.10 280)", bg: "oklch(0.96 0.04 280)", border: "oklch(0.88 0.04 280)", text: "oklch(0.38 0.08 280)", }, social: { label: "Fest", dot: "oklch(0.62 0.10 155)", bg: "oklch(0.96 0.04 155)", border: "oklch(0.88 0.04 155)", text: "oklch(0.38 0.08 155)", }, birthday: { label: "Geburtstag", dot: "oklch(0.62 0.10 320)", bg: "oklch(0.96 0.04 320)", border: "oklch(0.88 0.04 320)", text: "oklch(0.38 0.08 320)", }, } as const; type NotdienstEventCategory = keyof typeof notdienstEventCategories; export type NotdienstCalendarItem = { id: string; title: string; category: NotdienstEventCategory; startDate: Date; endDate: Date; allDay: boolean; note: string | null; }; interface NotdienstFormProps { targetYear: number; targetMonth: number; isLocked: boolean; requiredDaysTotal: number; initialSelectedDates: string[]; childrenIds: string[]; calendarItems: NotdienstCalendarItem[]; } export function NotdienstForm({ targetYear, targetMonth, isLocked, requiredDaysTotal, initialSelectedDates, childrenIds, calendarItems, }: NotdienstFormProps) { const router = useRouter(); // Lokaler State für die Auswahl. Ein Datum liegt als stabiler yyyy-MM-dd-Schlüssel vor. const [selection, setSelectedDates] = useState>( new Set(initialSelectedDates), ); const [isPending, startTransition] = useTransition(); const selectableDateKeys = useMemo( () => new Set(getNotdienstDays(targetYear, targetMonth, calendarItems.filter((item) => item.category === "closure" || item.category === "team"), ).map(formatUtcDateKey)), [calendarItems, targetMonth, targetYear], ); const selectedDates = useMemo( () => new Set(Array.from(selection).filter((key) => selectableDateKeys.has(key))), [selection, selectableDateKeys], ); const monthStart = startOfMonth(new Date(targetYear, targetMonth - 1)); const selectedCount = selectedDates.size; const hasMinimumRequirement = requiredDaysTotal > 0; const isFulfilled = !hasMinimumRequirement || selectedCount >= requiredDaysTotal; const selectedLabel = selectedCount === 1 ? "1 Tag markiert" : `${selectedCount} Tage markiert`; function handleChange(nextDates: Set) { if (isLocked || isPending) return; setSelectedDates(nextDates); } function handleSave() { if (isLocked) return; if (hasMinimumRequirement && !isFulfilled) { toast.error( `Bitte wähle noch ${requiredDaysTotal - selectedCount} weitere Termine aus.`, ); return; } startTransition(async () => { const payload = { dates: Array.from(selectedDates), childrenIds, }; const result = await saveNotdienstAvailabilities(payload); if (result?.error) { toast.error(result.error); } else { setSelectedDates(new Set(result.dates)); toast.success("Verfügbarkeiten erfolgreich gespeichert."); router.refresh(); } }); } return (
{isLocked ? (

Die Frist für diesen Monat ist abgelaufen. Änderungen sind nicht mehr möglich.

) : null}

{hasMinimumRequirement ? `Du hast ${selectedCount} von ${requiredDaysTotal} benötigten Tagen markiert.` : `${selectedLabel}. Für diese Kita ist aktuell keine Mindestanzahl hinterlegt.`}

); } type NotdienstCalendarProps = { month: Date; events: NotdienstCalendarItem[]; selected: Set; requiredDays: number; disabled?: boolean; selectableDateKeys: Set; isSaving?: boolean; onChange: (selected: Set) => void; onSave: () => void; }; export function NotdienstCalendar({ month, events, selected, requiredDays, disabled = false, selectableDateKeys, isSaving = false, onChange, onSave, }: NotdienstCalendarProps) { const [visibleMonth, setVisibleMonth] = useState(month); const [activeEvent, setActiveEvent] = useState( null, ); const visibleMonthStart = startOfMonth(visibleMonth); const visibleMonthEnd = endOfMonth(visibleMonthStart); const calendarStart = startOfWeek(visibleMonthStart, { weekStartsOn: 1 }); const calendarEnd = endOfWeek(visibleMonthEnd, { weekStartsOn: 1 }); const calendarDays = eachDayOfInterval({ start: calendarStart, end: calendarEnd, }).map((day) => startOfDay(day)); const selectedCount = selected.size; const extraCount = Math.max(0, selectedCount - requiredDays); const statusText = requiredDays > 0 && selectedCount >= requiredDays ? `Fertig — alle ${requiredDays} Tage markiert` : `${selectedCount} von ${requiredDays} Tagen`; function toggleDate(dateKey: string) { if (disabled || !selectableDateKeys.has(dateKey)) return; const nextSelected = new Set(selected); if (nextSelected.has(dateKey)) { nextSelected.delete(dateKey); } else { nextSelected.add(dateKey); } onChange(nextSelected); } return (

NOTDIENST · {format(visibleMonthStart, "MMMM yyyy", { locale: de })}

Wann magst du{" "} helfen ?

{Array.from({ length: Math.max(requiredDays, 1) }).map( (_, index) => { const isMarked = index < selectedCount; return ( ); }, )} {extraCount > 0 ? ( +{extraCount} ) : null}
{statusText}

{format(visibleMonthStart, "MMMM yyyy", { locale: de })}

{[ "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag", ].map((weekday) => (
{weekday}
))}
{calendarDays.map((day) => { const dateKey = formatDateKey(day); const isInTargetMonth = isSameMonth(day, visibleMonthStart); const isSelected = selected.has(dateKey); const dayEvents = getCalendarItemsForDay(events, day); const isWeekend = day.getDay() === 0 || day.getDay() === 6; const hasClosure = isInTargetMonth && !isWeekend && !selectableDateKeys.has(dateKey); const isSelectable = isInTargetMonth && selectableDateKeys.has(dateKey); return ( toggleDate(dateKey)} onOpenEvent={setActiveEvent} /> ); })}
{activeEvent ? ( setActiveEvent(null)} /> ) : null}
); } function CalendarTile({ day, events, isInTargetMonth, isSelected, isSelectable, isWeekend, hasClosure, disabled, onToggle, onOpenEvent, }: { day: Date; events: NotdienstCalendarItem[]; isInTargetMonth: boolean; isSelected: boolean; isSelectable: boolean; isWeekend: boolean; hasClosure: boolean; disabled: boolean; onToggle: () => void; onOpenEvent: (event: NotdienstCalendarItem) => void; }) { return (
{format(day, "d")} {isSelected ? ( ) : null} {hasClosure ? ( ) : null}
{events.slice(0, 2).map((event) => ( ))} {events.length > 2 ? (

+{events.length - 2} weitere

) : null}
); } function CalendarLegend() { return (
Dein Notdienst {Object.entries(notdienstEventCategories).map(([key, category]) => ( {category.label} ))}
); } function EventDialog({ event, onClose, }: { event: NotdienstCalendarItem; onClose: () => void; }) { const category = notdienstEventCategories[event.category]; return (
eventClick.stopPropagation()} >
{category.label}

{event.title}

{formatEventDate(event)}

{event.note ? (

{event.note}

) : null}
); } function getCalendarItemsForDay( items: NotdienstCalendarItem[], day: Date, ) { const dayStart = startOfDay(day).getTime(); return items.filter((item) => { const itemStart = startOfDay(item.startDate).getTime(); const itemEnd = startOfDay(item.endDate).getTime(); return itemStart <= dayStart && itemEnd >= dayStart; }); } function formatEventDate(event: NotdienstCalendarItem) { const date = format(event.startDate, "EEEE, d. MMMM yyyy", { locale: de }); return event.allDay ? `${date} · Ganztägig` : `${date} · ${formatEventTime(event)}`; } function formatEventTime(event: NotdienstCalendarItem) { if (event.allDay) return "Ganztägig"; return `${format(event.startDate, "HH:mm")} - ${format( event.endDate, "HH:mm", )}`; }