"use client"; import { useEffect, useMemo, useState } from "react"; import { trpc } from "~/lib/trpc/client.js"; type ScopeType = "COUNTRY" | "STATE" | "CITY"; type CalendarRow = { id: string; name: string; scopeType: ScopeType; stateCode: string | null; metroCityId: string | null; isActive: boolean; priority: number; country: { id: string; code: string; name: string }; metroCity: { id: string; name: string } | null; entries: Array<{ id: string; date: string | Date; name: string; isRecurringAnnual: boolean; source: string | null; }>; _count?: { entries: number }; }; type CountryRow = { id: string; code: string; name: string; metroCities: { id: string; name: string }[]; }; type HolidayPreviewDetail = { count: number; locationContext: { countryCode: string | null; stateCode: string | null; metroCity: string | null; year: number; }; summary: { byScope: Array<{ scope: string; count: number }>; bySourceType: Array<{ sourceType: string; count: number }>; byCalendar: Array<{ calendarName: string; count: number }>; }; holidays: Array<{ date: string; name: string; scope: string; calendarName: string; sourceType: string; }>; }; const SCOPE_LABELS: Record = { COUNTRY: "Land", STATE: "Bundesland/Region", CITY: "Stadt", }; function formatDate(value: string | Date): string { return new Date(value).toISOString().slice(0, 10); } export function HolidayCalendarEditor() { const utils = trpc.useUtils(); const [selectedCalendarId, setSelectedCalendarId] = useState(null); const [scopeType, setScopeType] = useState("COUNTRY"); const [countryId, setCountryId] = useState(""); const [stateCode, setStateCode] = useState(""); const [metroCityId, setMetroCityId] = useState(""); const [name, setName] = useState(""); const [priority, setPriority] = useState(0); const [entryDate, setEntryDate] = useState(""); const [entryName, setEntryName] = useState(""); const [entryRecurring, setEntryRecurring] = useState(false); const [entrySource, setEntrySource] = useState(""); const [previewYear, setPreviewYear] = useState(new Date().getFullYear()); const [error, setError] = useState(null); const [calendarDraft, setCalendarDraft] = useState({ name: "", priority: 0, stateCode: "", metroCityId: "", isActive: true, }); const [editingEntryId, setEditingEntryId] = useState(null); const [entryDraft, setEntryDraft] = useState({ date: "", name: "", isRecurringAnnual: false, source: "", }); const { data: countries } = trpc.country.list.useQuery(); const { data: calendars } = trpc.holidayCalendar.listCalendars.useQuery({ includeInactive: true }); const selectedCalendar = ((calendars ?? []) as unknown as CalendarRow[]).find((calendar) => calendar.id === selectedCalendarId) ?? null; const selectedCountry = useMemo(() => { const rows = (countries ?? []) as unknown as CountryRow[]; return rows.find((country) => country.id === countryId) ?? null; }, [countries, countryId]); const selectedCalendarCountry = useMemo(() => { const rows = (countries ?? []) as unknown as CountryRow[]; return rows.find((country) => country.id === selectedCalendar?.country.id) ?? null; }, [countries, selectedCalendar]); const previewQuery = (trpc.holidayCalendar.previewResolvedHolidaysDetail.useQuery as unknown as ( input: { countryId: string; year: number; stateCode?: string; metroCityId?: string; }, options: { enabled: boolean; staleTime: number }, ) => { data: HolidayPreviewDetail | undefined; isLoading: boolean })( { countryId: selectedCalendar?.country.id ?? countryId, year: previewYear, ...(selectedCalendar?.stateCode ? { stateCode: selectedCalendar.stateCode } : {}), ...(selectedCalendar?.metroCityId ? { metroCityId: selectedCalendar.metroCityId } : {}), }, { enabled: Boolean(selectedCalendar?.country.id ?? countryId), staleTime: 30_000, }, ); const invalidate = async () => { await Promise.all([ utils.holidayCalendar.listCalendars.invalidate(), utils.holidayCalendar.getCalendarById.invalidate(), utils.holidayCalendar.previewResolvedHolidays.invalidate(), utils.holidayCalendar.previewResolvedHolidaysDetail.invalidate(), ]); }; const createCalendar = trpc.holidayCalendar.createCalendar.useMutation({ onSuccess: async (calendar) => { await invalidate(); setSelectedCalendarId(calendar.id); setName(""); setStateCode(""); setMetroCityId(""); setPriority(0); setError(null); }, onError: (mutationError) => setError(mutationError.message), }); const updateCalendar = trpc.holidayCalendar.updateCalendar.useMutation({ onSuccess: async () => { await invalidate(); setError(null); }, onError: (mutationError) => setError(mutationError.message), }); const deleteCalendar = trpc.holidayCalendar.deleteCalendar.useMutation({ onSuccess: async () => { await invalidate(); setSelectedCalendarId(null); setError(null); }, onError: (mutationError) => setError(mutationError.message), }); const createEntry = trpc.holidayCalendar.createEntry.useMutation({ onSuccess: async () => { await invalidate(); setEntryDate(""); setEntryName(""); setEntryRecurring(false); setEntrySource(""); setError(null); }, onError: (mutationError) => setError(mutationError.message), }); const updateEntry = trpc.holidayCalendar.updateEntry.useMutation({ onSuccess: async () => { await invalidate(); setEditingEntryId(null); setError(null); }, onError: (mutationError) => setError(mutationError.message), }); const deleteEntry = trpc.holidayCalendar.deleteEntry.useMutation({ onSuccess: async () => { await invalidate(); setError(null); }, onError: (mutationError) => setError(mutationError.message), }); const countryRows = (countries ?? []) as unknown as CountryRow[]; const calendarRows = (calendars ?? []) as unknown as CalendarRow[]; const isCreateScopeValid = scopeType === "COUNTRY" ? Boolean(countryId && name.trim()) : scopeType === "STATE" ? Boolean(countryId && name.trim() && stateCode.trim()) : Boolean(countryId && name.trim() && metroCityId); const isCalendarDirty = selectedCalendar !== null && ( calendarDraft.name !== selectedCalendar.name || calendarDraft.priority !== selectedCalendar.priority || calendarDraft.isActive !== selectedCalendar.isActive || calendarDraft.stateCode !== (selectedCalendar.stateCode ?? "") || calendarDraft.metroCityId !== (selectedCalendar.metroCityId ?? "") ); useEffect(() => { if (!selectedCalendar) { setCalendarDraft({ name: "", priority: 0, stateCode: "", metroCityId: "", isActive: true, }); return; } setCalendarDraft({ name: selectedCalendar.name, priority: selectedCalendar.priority, stateCode: selectedCalendar.stateCode ?? "", metroCityId: selectedCalendar.metroCityId ?? "", isActive: selectedCalendar.isActive, }); setEditingEntryId(null); }, [selectedCalendar]); function handleCreateCalendar(e: React.FormEvent) { e.preventDefault(); setError(null); if (!isCreateScopeValid) { setError("Bitte alle Pflichtfelder fuer den gewaehlten Scope ausfuellen."); return; } createCalendar.mutate({ name: name.trim(), scopeType, countryId, ...(scopeType === "STATE" && stateCode.trim() ? { stateCode: stateCode.trim().toUpperCase() } : {}), ...(scopeType === "CITY" && metroCityId ? { metroCityId } : {}), priority, isActive: true, }); } function handleAddEntry(e: React.FormEvent) { e.preventDefault(); if (!selectedCalendarId) return; if (!entryDate || !entryName.trim()) { setError("Datum und Feiertagsname sind erforderlich."); return; } createEntry.mutate({ holidayCalendarId: selectedCalendarId, date: new Date(`${entryDate}T00:00:00.000Z`), name: entryName.trim(), isRecurringAnnual: entryRecurring, ...(entrySource.trim() ? { source: entrySource.trim() } : {}), }); } function resetCalendarDraft() { if (!selectedCalendar) { return; } setCalendarDraft({ name: selectedCalendar.name, priority: selectedCalendar.priority, stateCode: selectedCalendar.stateCode ?? "", metroCityId: selectedCalendar.metroCityId ?? "", isActive: selectedCalendar.isActive, }); } function handleUpdateCalendar(e: React.FormEvent) { e.preventDefault(); if (!selectedCalendar) { return; } setError(null); const normalizedStateCode = calendarDraft.stateCode.trim().toUpperCase(); if (selectedCalendar.scopeType === "STATE" && !normalizedStateCode) { setError("State-Kalender benoetigen einen Regionscode."); return; } if (selectedCalendar.scopeType === "CITY" && !calendarDraft.metroCityId) { setError("City-Kalender benoetigen eine Stadtzuordnung."); return; } updateCalendar.mutate({ id: selectedCalendar.id, data: { name: calendarDraft.name.trim(), priority: calendarDraft.priority, isActive: calendarDraft.isActive, ...(selectedCalendar.scopeType === "STATE" ? { stateCode: normalizedStateCode } : {}), ...(selectedCalendar.scopeType === "CITY" ? { metroCityId: calendarDraft.metroCityId } : {}), }, }); } function startEditingEntry(entry: CalendarRow["entries"][number]) { setEditingEntryId(entry.id); setEntryDraft({ date: formatDate(entry.date), name: entry.name, isRecurringAnnual: entry.isRecurringAnnual, source: entry.source ?? "", }); } function handleUpdateEntry(entryId: string) { if (!entryDraft.date || !entryDraft.name.trim()) { setError("Ein Feiertagseintrag braucht Datum und Name."); return; } setError(null); updateEntry.mutate({ id: entryId, data: { date: new Date(`${entryDraft.date}T00:00:00.000Z`), name: entryDraft.name.trim(), isRecurringAnnual: entryDraft.isRecurringAnnual, source: entryDraft.source.trim() || null, }, }); } function handleDeleteCalendar(calendar: CalendarRow) { if (deleteCalendar.isPending) { return; } const confirmed = globalThis.confirm( `Feiertagskalender "${calendar.name}" wirklich loeschen? Alle Eintraege gehen dabei verloren.`, ); if (!confirmed) { return; } setError(null); deleteCalendar.mutate({ id: calendar.id }); } function handleDeleteEntry(entry: CalendarRow["entries"][number]) { if (deleteEntry.isPending) { return; } const confirmed = globalThis.confirm( `Feiertag "${entry.name}" am ${formatDate(entry.date)} wirklich entfernen?`, ); if (!confirmed) { return; } setError(null); deleteEntry.mutate({ id: entry.id }); } return (

Holiday Calendar Editor

Pflege Feiertagskalender pro Land, Bundesland/Region oder Stadt. Die Vorschau zeigt den effektiv aufgeloesten Kalender fuer den gewaelten Scope.

{error && (
{error}
)}
{scopeType === "STATE" && ( )} {scopeType === "CITY" && ( )}
{calendarRows.length === 0 && ( )} {calendarRows.map((calendar) => ( setSelectedCalendarId(calendar.id)} > ))}
Kalender Scope Zuordnung Eintraege
Noch keine Feiertagskalender vorhanden.
{calendar.name}
{calendar.country.name}
{SCOPE_LABELS[calendar.scopeType]} {calendar.scopeType === "COUNTRY" && calendar.country.code} {calendar.scopeType === "STATE" && calendar.stateCode} {calendar.scopeType === "CITY" && calendar.metroCity?.name} {calendar._count?.entries ?? calendar.entries.length}
{selectedCalendar && (

{selectedCalendar.name}

{SCOPE_LABELS[selectedCalendar.scopeType]} · {selectedCalendar.country.name} {selectedCalendar.stateCode ? ` · ${selectedCalendar.stateCode}` : ""} {selectedCalendar.metroCity?.name ? ` · ${selectedCalendar.metroCity.name}` : ""}

{selectedCalendar.scopeType === "STATE" && ( )} {selectedCalendar.scopeType === "CITY" && ( )}
{selectedCalendar.entries.length === 0 && ( )} {selectedCalendar.entries.map((entry) => ( ))}
Datum Name Typ Quelle Aktion
Keine Eintraege vorhanden.
{editingEntryId === entry.id ? ( setEntryDraft((current) => ({ ...current, date: e.target.value }))} className="w-full rounded-lg border border-gray-300 bg-white px-2 py-1.5 text-sm dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100" /> ) : formatDate(entry.date)} {editingEntryId === entry.id ? ( setEntryDraft((current) => ({ ...current, name: e.target.value }))} className="w-full rounded-lg border border-gray-300 bg-white px-2 py-1.5 text-sm dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100" /> ) : entry.name} {editingEntryId === entry.id ? ( ) : entry.isRecurringAnnual ? "jaehrlich" : "fix"} {editingEntryId === entry.id ? ( setEntryDraft((current) => ({ ...current, source: e.target.value }))} className="w-full rounded-lg border border-gray-300 bg-white px-2 py-1.5 text-sm dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100" placeholder="Quelle" /> ) : entry.source ?? "System/ohne Quelle"}
{editingEntryId === entry.id ? ( <> ) : ( )}

Vorschau

Effektiv aufgeloeste Feiertage fuer den gewaehlten Scope.

setPreviewYear(parseInt(e.target.value, 10) || new Date().getFullYear())} className="w-24 rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100" />
{previewQuery.data && (
Basis: {[ previewQuery.data.locationContext.countryCode, previewQuery.data.locationContext.stateCode, previewQuery.data.locationContext.metroCity, ].filter(Boolean).join(" / ") || "n/a"}
Scope: {previewQuery.data.summary.byScope.map((item) => `${item.scope} ${item.count}`).join(" · ") || "none"}
Sources: {previewQuery.data.summary.bySourceType.map((item) => `${item.sourceType} ${item.count}`).join(" · ") || "none"}
)} {(!previewQuery.data || previewQuery.data.holidays.length === 0) && ( )} {(previewQuery.data?.holidays ?? []).map((entry) => ( ))}
Datum Name Scope Quelle
{previewQuery.isLoading ? "Laedt Vorschau..." : "Keine Feiertage fuer diese Auswahl vorhanden."}
{entry.date} {entry.name} {entry.scope} {entry.calendarName}
)}
); }