"use client"; import { useState, useMemo, useTransition } from "react"; import { TerminType, TerminStatus, } from "@prisma/client"; import { format, startOfMonth, endOfMonth, startOfWeek, endOfWeek, eachDayOfInterval, isSameMonth, isSameDay, addMonths, subMonths, } from "date-fns"; import { de } from "date-fns/locale"; import { Search, ListFilter, ChevronLeft, ChevronRight, Calendar as CalendarIcon, Clock, MapPin, AlertTriangle, WashingMachine, Trash2, } from "lucide-react"; import { RsvpDialog, RsvpForm } from "@/components/rsvp-form"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { toast } from "sonner"; import { CalendarListItemDto, NotdienstSwapDialog, NotdienstSwapOptionDto, TerminList, } from "./termin-list"; import { MitbringselList } from "./mitbringsel-list"; import { AdminTerminModal } from "./admin-termin-modal"; import { acceptNotdienstSwapRequest, cancelNotdienstSwapRequest, declineNotdienstSwapRequest, deleteTerminAdmin, } from "../actions"; const categories = [ { label: "Schließtag", type: TerminType.SCHLIESSTAG, color: "var(--danger)" }, { label: "Teamtag", type: TerminType.TEAMTAG, color: "var(--danger)" }, { label: "Elternabend", type: TerminType.ELTERNABEND, color: "oklch(0.50 0.10 280)" }, { label: "Fest", type: TerminType.KITA_FEST, color: "var(--good)" }, { label: "Geburtstag", type: TerminType.GEBURTSTAG_INTERN, color: "oklch(0.50 0.10 320)" }, { label: "Mein Elterndienst", type: "mein-elterndienst", color: "oklch(0.60 0.16 160)" }, { label: "Elterndienst", type: "elterndienst", color: "oklch(0.75 0.08 160)" }, { label: "Mein Notdienst", type: "mein-notdienst", color: "oklch(0.60 0.18 45)" }, { label: "Notdienst", type: "notdienst", color: "oklch(0.75 0.10 50)" }, { label: "Sonstiges", type: TerminType.SONSTIGES, color: "var(--ink-muted)" }, ]; export type NotdienstSwapRequestDto = { id: string; direction: "incoming" | "outgoing"; targetDate: Date; targetFamilyName: string; targetChildName: string; offeredDate: Date; offeredFamilyName: string; offeredChildName: string; message: string | null; }; export function CalendarOverviewClient({ termine, userId, isAdmin, notdienstSwapOptions = [], pendingSwapRequests = [], }: { termine: CalendarListItemDto[]; userId: string; isAdmin: boolean; notdienstSwapOptions?: NotdienstSwapOptionDto[]; pendingSwapRequests?: NotdienstSwapRequestDto[]; }) { const [view, setView] = useState<"liste" | "monat" | "agenda">("monat"); const [searchQuery, setSearchQuery] = useState(""); const [activeTypes, setActiveTypes] = useState>(new Set()); const [filterScope, setFilterScope] = useState<"own" | "all">(isAdmin ? "all" : "own"); // Month navigation state const [currentMonthDate, setCurrentMonthDate] = useState(new Date()); // Dialog state for day clicking in Month view const [selectedDay, setSelectedDay] = useState(null); // Delete event states const [deleteEventId, setDeleteEventId] = useState(null); const [deleteEventTitle, setDeleteEventTitle] = useState(""); const [isDeleting, startDeleteTransition] = useTransition(); const toggleType = (type: string) => { setActiveTypes((prev) => { const next = new Set(prev); if (next.has(type)) { next.delete(type); } else { next.add(type); } return next; }); }; // Filter logic const filteredEvents = useMemo(() => { return termine.filter((item) => { // 0. Filter by scope (own vs all) if (filterScope === "own") { if (item.kind === "duty" && !item.isOwn) return false; if (item.kind === "notdienst" && !item.isOwn) return false; } // 1. Search Query filter const query = searchQuery.trim().toLowerCase(); if (query) { const titleMatch = item.title.toLowerCase().includes(query); const descMatch = item.kind === "termin" && item.description ? item.description.toLowerCase().includes(query) : false; const familyMatch = (item.kind === "duty" || item.kind === "notdienst") && item.familyName ? item.familyName.toLowerCase().includes(query) : false; const childMatch = item.kind === "notdienst" && item.childName ? item.childName.toLowerCase().includes(query) : false; if (!titleMatch && !descMatch && !familyMatch && !childMatch) return false; } // 2. Category filter if (activeTypes.size > 0) { const itemType = item.kind === "duty" ? item.isOwn ? "mein-elterndienst" : "elterndienst" : item.kind === "notdienst" ? item.isOwn ? "mein-notdienst" : "notdienst" : item.type; if (!activeTypes.has(itemType)) return false; } return true; }); }, [termine, searchQuery, activeTypes, filterScope]); // Check if an event falls on a specific day const isEventOnDay = (item: CalendarListItemDto, day: Date) => { const eventStart = new Date(item.startDate); const eventEnd = new Date(item.endDate); const dayTime = new Date(day.getFullYear(), day.getMonth(), day.getDate()).getTime(); const startTime = new Date(eventStart.getFullYear(), eventStart.getMonth(), eventStart.getDate()).getTime(); const endTime = new Date(eventEnd.getFullYear(), eventEnd.getMonth(), eventEnd.getDate()).getTime(); return dayTime >= startTime && dayTime <= endTime; }; // Get events for selected day (using all filtered events or all events? We use filtered to keep visual filters consistent!) const selectedDayEvents = useMemo(() => { if (!selectedDay) return []; return filteredEvents.filter((item) => isEventOnDay(item, selectedDay)); }, [selectedDay, filteredEvents]); // Handle event delete const handleDeleteConfirm = () => { if (!deleteEventId) return; startDeleteTransition(async () => { const res = await deleteTerminAdmin(deleteEventId); if (res.error) { toast.error(res.error); } else { toast.success("Termin wurde erfolgreich gelöscht."); setDeleteEventId(null); // If deleting from the day dialog, update selectedDay if it becomes empty or keep it open } }); }; return (
{pendingSwapRequests.length > 0 && ( )} {/* Search and Filters Header */}
{(["liste", "monat", "agenda"] as const).map((v) => ( ))}
{!isAdmin && (
)}
{/* Category chips */}
Filter {categories.map((cat) => { const isActive = activeTypes.has(cat.type); return ( ); })}
setSearchQuery(e.target.value)} />
{/* Main View Contents */} {view === "liste" && ( )} {view === "monat" && ( setSelectedDay(day)} /> )} {view === "agenda" && ( { setDeleteEventId(id); setDeleteEventTitle(title); }} /> )} {/* Day details Dialog for Month View */} !open && setSelectedDay(null)}> {selectedDay && format(selectedDay, "EEEE, d. MMMM yyyy", { locale: de })} Übersicht aller Zuteilungen, Feste und Einträge an diesem Tag.
{selectedDayEvents.length === 0 ? (

Keine Termine oder Dienste an diesem Tag.

) : ( selectedDayEvents.map((item) => (
{item.kind === "duty" ? (

{item.title}

{item.isOwn ? "Mein Elterndienst" : "Elterndienst"}

Familie: {item.familyName}

{format(new Date(item.startDate), "dd.MM.")} -{" "} {format(new Date(item.endDate), "dd.MM.yyyy")}

{isAdmin && ( )}
) : item.kind === "notdienst" ? (

{item.title}

{item.isOwn ? "Mein Notdienst" : "Notdienst"}

Kind: {item.childName} (Familie: {item.familyName})

{format(new Date(item.startDate), "dd.MM.yyyy")}

{isAdmin && ( )} {item.canRequestSwap && (
Tausch anfragen } />
)}
) : (

{item.title}

{getTypeLabel(item.type)}

{item.allDay ? "Ganztägig" : `${format(new Date(item.startDate), "HH:mm")} - ${format( new Date(item.endDate), "HH:mm", )}`}

{item.description && (

{item.description}

)} {item.rsvpEvent ?
Eure Rückmeldung
: null} {item.participantInfo && item.showParticipantInfo && (
Hinweis für Teilnehmer

{item.participantInfo}

)} {item.mitbringselListEnabled && (
)} {isAdmin && (
)}
)}
)) )}
{/* Delete Confirmation Dialog */} !open && setDeleteEventId(null)}> Termin unwiderruflich löschen? Möchtest du den Termin “{deleteEventTitle}” wirklich löschen? Alle zugehörigen Teilnahmen und Mitbringsel-Einträge gehen verloren.
); } function NotdienstSwapRequestsPanel({ requests, }: { requests: NotdienstSwapRequestDto[]; }) { return (

Offene Notdienst-Tausche

{requests.map((request) => ( ))}
); } function NotdienstSwapRequestRow({ request, }: { request: NotdienstSwapRequestDto; }) { const [isPending, startTransition] = useTransition(); const runAction = ( action: (requestId: string) => Promise<{ success?: boolean; error?: string }>, successMessage: string, ) => { startTransition(async () => { const result = await action(request.id); if (result.error) { toast.error(result.error); return; } toast.success(successMessage); }); }; return (

{request.direction === "incoming" ? "Anfrage an dich" : "Deine Anfrage"}

{format(request.targetDate, "dd.MM.yyyy", { locale: de })}{" "} ({request.targetFamilyName}, {request.targetChildName}) gegen{" "} {format(request.offeredDate, "dd.MM.yyyy", { locale: de })}{" "} ({request.offeredFamilyName}, {request.offeredChildName})

{request.message && (

{request.message}

)}
{request.direction === "incoming" ? ( <> ) : ( )}
); } // ===================================================================== // Month Grid Component // ===================================================================== function MonthView({ currentMonthDate, setCurrentMonthDate, events, isEventOnDay, onDayClick, }: { currentMonthDate: Date; setCurrentMonthDate: (date: Date) => void; events: CalendarListItemDto[]; isEventOnDay: (item: CalendarListItemDto, day: Date) => boolean; onDayClick: (day: Date) => void; }) { const monthStart = startOfMonth(currentMonthDate); const monthEnd = endOfMonth(monthStart); const startDate = startOfWeek(monthStart, { weekStartsOn: 1 }); const endDate = endOfWeek(monthEnd, { weekStartsOn: 1 }); const days = useMemo(() => { return eachDayOfInterval({ start: startDate, end: endDate }); }, [startDate, endDate]); const weekdays = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"]; return (

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

{weekdays.map((day) => (
{day}
))}
{days.map((day, idx) => { const isCurrentMonth = isSameMonth(day, currentMonthDate); const isToday = isSameDay(day, new Date()); const dayEvents = events.filter((item) => isEventOnDay(item, day)); return (
onDayClick(day)} className={[ "min-h-[105px] bg-[var(--surface)] p-2 transition-colors hover:bg-[var(--surface-2)] cursor-pointer flex flex-col justify-between", !isCurrentMonth ? "bg-[var(--surface-3)]/30 text-[var(--ink-muted)] opacity-60" : "text-[var(--ink)]", ].join(" ")} > {/* Day number cell top */}
{format(day, "d")}
{/* Event indicators/snippets */}
{dayEvents.slice(0, 3).map((item) => { const color = item.kind === "duty" ? item.isOwn ? "oklch(0.60 0.16 160)" : "oklch(0.75 0.08 160)" : item.kind === "notdienst" ? item.isOwn ? "oklch(0.60 0.18 45)" : "oklch(0.75 0.10 50)" : getCategoryColor(item.type); return (
{item.title}
); })} {dayEvents.length > 3 && (
+ {dayEvents.length - 3} weitere
)}
); })}
); } // ===================================================================== // Agenda Timeline View Component // ===================================================================== function AgendaView({ events, isAdmin, userId, onDeleteTermin, }: { events: CalendarListItemDto[]; isAdmin: boolean; userId: string; onDeleteTermin: (id: string, title: string) => void; }) { const grouped = useMemo(() => { const groups = new Map(); events.forEach((item) => { const key = format(new Date(item.startDate), "yyyy-MM-dd"); const group = groups.get(key) ?? { date: new Date(item.startDate), items: [] }; group.items.push(item); groups.set(key, group); }); return Array.from(groups.values()).sort((a, b) => a.date.getTime() - b.date.getTime()); }, [events]); if (events.length === 0) { return (

Keine Termine oder Dienste gefunden

Passe deine Filter an oder suche nach einem anderen Begriff.

); } return (
{grouped.map((group) => (
{/* Timeline marker */}
{/* Date Label on left */}
{format(group.date, "d. MMMM", { locale: de })}
{format(group.date, "EEEE", { locale: de })}
{/* Items stack on right */}
{group.items.map((item) => { const color = item.kind === "termin" ? getCategoryColor(item.type) : item.kind === "notdienst" ? item.isOwn ? "oklch(0.60 0.18 45)" : "oklch(0.75 0.10 50)" : item.kind === "duty" ? item.isOwn ? "oklch(0.60 0.16 160)" : "oklch(0.75 0.08 160)" : "oklch(0.60 0.15 160)"; return (

{item.title}

{item.kind === "termin" ? ( {getTypeLabel(item.type)} ) : item.kind === "notdienst" ? ( {item.isOwn ? "Mein Notdienst" : "Notdienst"} ) : ( Elterndienst )} {item.kind === "termin" && item.status === TerminStatus.PENDING && ( Ausstehend )}

{item.kind === "termin" ? item.allDay ? "Ganztägig" : `${format(new Date(item.startDate), "HH:mm")} - ${format( new Date(item.endDate), "HH:mm", )}` : item.kind === "notdienst" ? `${format(new Date(item.startDate), "dd.MM.yyyy")} (ganztägig)` : `${format(new Date(item.startDate), "dd.MM.")} - ${format( new Date(item.endDate), "dd.MM.yyyy", )}`} {item.kind === "termin" && } {item.kind === "termin" ? "Kita" : item.kind === "notdienst" ? `Kind: ${item.childName} · Familie: ${item.familyName}` : `Familie: ${item.familyName}`}

{item.kind === "termin" && item.description && (

{item.description}

)}
{item.kind === "termin" && item.rsvpEvent ? : null} {/* Actions for admins */} {isAdmin && item.kind === "termin" && (
)}
); })}
))}
); } // ===================================================================== // Helper functions for categories/labels // ===================================================================== function getCategoryColor(type: TerminType): string { switch (type) { case TerminType.KITA_FEST: case TerminType.MITMACH_TAG: return "var(--good)"; case TerminType.SCHLIESSTAG: case TerminType.TEAMTAG: return "var(--danger)"; case TerminType.GEBURTSTAG_INTERN: case TerminType.GEBURTSTAG_EXTERN: return "oklch(0.50 0.10 320)"; case TerminType.ELTERNABEND: case TerminType.MITGLIEDERVERSAMMLUNG: case TerminType.ELTERNCAFE: return "oklch(0.50 0.10 280)"; default: return "var(--ink-muted)"; } } function getTypeLabel(type: TerminType): string { switch (type) { case TerminType.KITA_FEST: return "Kita-Fest"; case TerminType.MITMACH_TAG: return "Mitmach-Tag"; case TerminType.SCHLIESSTAG: return "Schließtag"; case TerminType.TEAMTAG: return "Teamtag"; case TerminType.ELTERNABEND: return "Elternabend"; case TerminType.MITGLIEDERVERSAMMLUNG: return "Mitgliederversammlung"; case TerminType.ELTERNCAFE: return "Elterncafe"; case TerminType.GEBURTSTAG_INTERN: return "Geburtstag"; case TerminType.GEBURTSTAG_EXTERN: return "Raumanfrage"; default: return "Sonstiges"; } }