"use client"; import type { ReactNode } from "react"; import { useState, useTransition } from "react"; import { DutyAssignmentStatus, RsvpMode, TerminStatus, TerminType, } from "@prisma/client"; import { format } from "date-fns"; import { de } from "date-fns/locale"; import { AlertTriangle, Calendar, Clock, MapPin, MoreHorizontal, WashingMachine, } from "lucide-react"; import { toast } from "sonner"; import { RsvpForm } from "@/components/rsvp-form"; import type { RsvpEventDto } from "@/lib/rsvp"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, DialogFooter, } from "@/components/ui/dialog"; import { AdminTerminModal } from "./admin-termin-modal"; import { TerminRequestModal } from "./termin-request-modal"; import { MitbringselList } from "./mitbringsel-list"; import { createNotdienstSwapRequest, deleteTerminAdmin } from "../actions"; export type TerminListItemDto = { kind: "termin"; id: string; title: string; description: string | null; type: TerminType; status: TerminStatus; startDate: Date; endDate: Date; allDay: boolean; mitbringselListEnabled: boolean; requiresRsvp: boolean; rsvpMode: RsvpMode; rsvpDeadline: Date | null; hasRsvpResponses: boolean; rsvpEvent?: RsvpEventDto; participantInfo: string | null; showParticipantInfo: boolean; mitbringselItems?: { id: string; userId: string; content: string; user: { firstName: string; lastName: string }; }[]; }; export type DutyCalendarItemDto = { kind: "duty"; id: string; title: string; familyName: string; status: DutyAssignmentStatus; startDate: Date; endDate: Date; isOwn: boolean; }; export type NotdienstCalendarItemDto = { kind: "notdienst"; id: string; planId: string; planYear: number; planMonth: number; title: string; familyId: string; familyName: string; childName: string; isOwn: boolean; canRequestSwap: boolean; startDate: Date; endDate: Date; }; export type NotdienstSwapOptionDto = { id: string; date: Date; childName: string; familyName: string; }; export type CalendarListItemDto = | TerminListItemDto | DutyCalendarItemDto | NotdienstCalendarItemDto; export function TerminList({ termine, userId, isAdmin, notdienstSwapOptions = [], }: { termine: CalendarListItemDto[]; userId: string; isAdmin: boolean; notdienstSwapOptions?: NotdienstSwapOptionDto[]; }) { if (termine.length === 0) { return (

Keine Termine in diesem Zeitraum

Lege den ersten Termin an, damit Eltern planen können.

{isAdmin ? ( ) : ( )}
); } const groups = groupByMonth(termine); return (
{groups.map((group) => (

{group.label}

{group.items.length}{" "} {group.items.length === 1 ? "Termin" : "Termine"}
{group.items.map((item) => item.kind === "duty" ? ( ) : item.kind === "notdienst" ? ( ) : ( ), )}
))}
); } function DutyRow({ duty, isAdmin, }: { duty: DutyCalendarItemDto; isAdmin: boolean; }) { const color = duty.isOwn ? "oklch(0.60 0.16 160)" : "oklch(0.75 0.08 160)"; const [menuOpen, setMenuOpen] = useState(false); return (

{duty.title}

{format(duty.startDate, "dd.MM.", { locale: de })} -{" "} {format(duty.endDate, "dd.MM.yyyy", { locale: de })} {duty.familyName}

{duty.isOwn ? "Mein Elterndienst" : "Elterndienst"} {isAdmin ? (
{menuOpen && ( <> ) : (
)}
); } function TerminRow({ termin, userId, isAdmin, }: { termin: TerminListItemDto; userId: string; isAdmin: boolean; }) { const color = getTypeColor(termin.type); const [menuOpen, setMenuOpen] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false); const [isDeleting, startDeleteTransition] = useTransition(); const handleDeleteConfirm = () => { startDeleteTransition(async () => { const res = await deleteTerminAdmin(termin.id); if (res.error) { toast.error(res.error); } else { toast.success("Termin wurde erfolgreich gelöscht."); setDeleteOpen(false); } }); }; return ( <>

{termin.title}

{termin.status === TerminStatus.PENDING && ( Ausstehend )}

{formatTerminTime(termin)} Kita

{getTypeLabel(termin.type)} {isAdmin ? (
{menuOpen && ( <>
setMenuOpen(false)} />
)}
) : (
)}
{termin.title} {format(termin.startDate, "PP", { locale: de })} {!termin.allDay && ` · ${format(termin.startDate, "p", { locale: de })} - ${format( termin.endDate, "p", { locale: de }, )}`}
{getTypeLabel(termin.type)}
{termin.description ? (

{termin.description}

) : (

Keine Beschreibung hinterlegt.

)} {termin.rsvpEvent ? (

Eure Rückmeldung

) : null} {termin.participantInfo && termin.showParticipantInfo && (
Wichtiger Hinweis für Teilnehmer

{termin.participantInfo}

)} {termin.mitbringselListEnabled && (
)}
{/* Delete Confirmation Dialog */} Termin unwiderruflich löschen? Möchtest du den Termin “{termin.title}” wirklich löschen? Alle zugehörigen Teilnahmen und Mitbringsel-Einträge gehen verloren. ); } function DateTile({ date }: { date: Date }) { return (
{format(date, "d", { locale: de })} {format(date, "EEE", { locale: de })}
); } function CategoryPill({ color, children, }: { color: string; children: React.ReactNode; }) { return ( {children} ); } function groupByMonth(items: CalendarListItemDto[]) { const groups = new Map< string, { key: string; label: string; items: CalendarListItemDto[] } >(); items.forEach((item) => { const key = format(item.startDate, "yyyy-MM"); const label = format(item.startDate, "MMMM yyyy", { locale: de }); const group = groups.get(key) ?? { key, label, items: [] }; group.items.push(item); groups.set(key, group); }); return Array.from(groups.values()); } function formatTerminTime(termin: TerminListItemDto) { if (termin.allDay) return "ganztägig"; return `${format(termin.startDate, "HH:mm", { locale: de })} - ${format( termin.endDate, "HH:mm", { locale: de }, )}`; } function getTypeColor(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"; } } export function NotdienstSwapDialog({ target, options, trigger, }: { target: NotdienstCalendarItemDto; options: NotdienstSwapOptionDto[]; trigger: ReactNode; }) { const selectableOptions = options.filter((option) => option.id !== target.id); const [open, setOpen] = useState(false); const [offeredAssignmentId, setOfferedAssignmentId] = useState( selectableOptions[0]?.id ?? "", ); const [message, setMessage] = useState(""); const [isPending, startTransition] = useTransition(); const handleSubmit = () => { if (!offeredAssignmentId) { toast.error("Bitte wähle einen eigenen Notdienst als Tausch-Angebot."); return; } startTransition(async () => { const result = await createNotdienstSwapRequest({ targetAssignmentId: target.id, offeredAssignmentId, message, }); if ("error" in result && result.error) { toast.error(result.error); return; } toast.success("Tauschanfrage wurde versendet."); setOpen(false); setMessage(""); }); }; return ( {trigger} Notdienst tauschen Frage Familie {target.familyName}, ob ihr den Notdienst am{" "} {format(target.startDate, "dd.MM.yyyy", { locale: de })} tauschen könnt.