"use client"; import { useEffect, useState } from "react"; import Pagination from "./Pagination"; type EventItem = { id: string; title: string; startAt: string; endAt: string; status: string; location?: string | null; locationPlaceId?: string | null; locationLat?: number | null; locationLng?: number | null; description?: string | null; publicOverride?: boolean | null; category?: { id: string; name: string; isPublic?: boolean } | null; createdBy?: { name?: string | null; email?: string | null } | null; }; type CategoryItem = { id: string; name: string; isPublic: boolean; }; export default function AdminPanel() { const [events, setEvents] = useState([]); const [allEvents, setAllEvents] = useState([]); const [error, setError] = useState(null); const [categories, setCategories] = useState([]); const [categoryError, setCategoryError] = useState(null); const [categoryStatus, setCategoryStatus] = useState(null); const [categoryModalOpen, setCategoryModalOpen] = useState(false); const [categoryModalError, setCategoryModalError] = useState(null); const [categoryModalStatus, setCategoryModalStatus] = useState(null); const [editingCategory, setEditingCategory] = useState(null); const [editEvent, setEditEvent] = useState(null); const [editStatus, setEditStatus] = useState(null); const [editError, setEditError] = useState(null); const [isEditOpen, setIsEditOpen] = useState(false); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(20); const [sortKey, setSortKey] = useState<"startAt" | "title" | "category" | "status">( "startAt" ); const [sortDir, setSortDir] = useState<"asc" | "desc">("asc"); const [importFile, setImportFile] = useState(null); const [importCategoryId, setImportCategoryId] = useState(""); const [importStatus, setImportStatus] = useState(null); const [importError, setImportError] = useState(null); const [publicAccessEnabled, setPublicAccessEnabled] = useState(null); const load = async () => { try { const response = await fetch("/api/events?status=PENDING"); if (!response.ok) { throw new Error("Vorschläge konnten nicht geladen werden."); } setEvents(await response.json()); } catch (err) { setError((err as Error).message); } }; const loadAllEvents = async () => { try { const response = await fetch("/api/events"); if (!response.ok) { throw new Error("Termine konnten nicht geladen werden."); } setAllEvents(await response.json()); } catch (err) { setError((err as Error).message); } }; const loadCategories = async () => { try { const response = await fetch("/api/categories"); if (!response.ok) { throw new Error("Kategorien konnten nicht geladen werden."); } setCategories(await response.json()); } catch (err) { setCategoryError((err as Error).message); } }; const loadSystemSettings = async () => { try { const response = await fetch("/api/settings/system"); if (!response.ok) return; const payload = await response.json(); setPublicAccessEnabled(payload.publicAccessEnabled !== false); } catch { // ignore } }; useEffect(() => { load(); loadCategories(); loadAllEvents(); loadSystemSettings(); }, []); useEffect(() => { setPage(1); }, [allEvents.length]); useEffect(() => { setPage(1); }, [pageSize]); const totalPages = Math.max(1, Math.ceil(allEvents.length / pageSize)); const showPublicControls = publicAccessEnabled === true; const sortedEvents = [...allEvents].sort((a, b) => { const dir = sortDir === "asc" ? 1 : -1; if (sortKey === "title") { return a.title.localeCompare(b.title) * dir; } if (sortKey === "category") { const aCat = a.category?.name || "Ohne Kategorie"; const bCat = b.category?.name || "Ohne Kategorie"; return aCat.localeCompare(bCat) * dir; } if (sortKey === "status") { return a.status.localeCompare(b.status) * dir; } const aDate = new Date(a.startAt).getTime(); const bDate = new Date(b.startAt).getTime(); return (aDate - bDate) * dir; }); const toggleSort = (nextKey: "startAt" | "title" | "category" | "status") => { if (sortKey === nextKey) { setSortDir((prev) => (prev === "asc" ? "desc" : "asc")); return; } setSortKey(nextKey); setSortDir("asc"); }; const updateStatus = async (id: string, status: "APPROVED" | "REJECTED") => { await fetch(`/api/events/${id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status }) }); load(); loadAllEvents(); }; const deleteEvent = async (id: string) => { const ok = window.confirm("Termin wirklich löschen?"); if (!ok) return; await fetch(`/api/events/${id}`, { method: "DELETE" }); load(); loadAllEvents(); }; const formatLocalDateTime = (value?: string | null) => { if (!value) return ""; const date = new Date(value); const offset = date.getTimezoneOffset() * 60000; return new Date(date.getTime() - offset).toISOString().slice(0, 16); }; const toIsoString = (value: FormDataEntryValue | null) => { if (!value) return null; const raw = String(value); if (!raw) return null; const date = new Date(raw); if (Number.isNaN(date.getTime())) return null; return date.toISOString(); }; const parsePublicOverride = (value: FormDataEntryValue | null) => { if (!value) return null; const raw = String(value); if (raw === "public") return true; if (raw === "private") return false; return null; }; const updateEvent = async (event: React.FormEvent) => { event.preventDefault(); setEditStatus(null); setEditError(null); if (!editEvent) return; const formData = new FormData(event.currentTarget); const payload: Record = { title: formData.get("title"), description: formData.get("description"), location: formData.get("location"), locationPlaceId: formData.get("locationPlaceId"), locationLat: formData.get("locationLat"), locationLng: formData.get("locationLng"), startAt: toIsoString(formData.get("startAt")), endAt: toIsoString(formData.get("endAt")), categoryId: formData.get("categoryId") }; if (showPublicControls) { payload.publicOverride = parsePublicOverride( formData.get("publicOverride") ); } const response = await fetch(`/api/events/${editEvent.id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }); if (!response.ok) { const data = await response.json(); setEditError(data.error || "Termin konnte nicht aktualisiert werden."); return; } const updated = await response.json(); setEditStatus("Termin aktualisiert."); setEditEvent(updated); load(); loadAllEvents(); setIsEditOpen(false); }; const createCategory = async (event: React.FormEvent) => { event.preventDefault(); setCategoryError(null); setCategoryStatus(null); const formData = new FormData(event.currentTarget); const rawName = String(formData.get("name") || "").trim(); const isPublic = formData.get("isPublic") === "on"; if (!rawName) { setCategoryError("Name erforderlich."); return; } const payload: Record = { name: rawName }; if (showPublicControls) { payload.isPublic = isPublic; } const response = await fetch("/api/categories", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }); if (!response.ok) { const data = await response.json(); setCategoryError(data.error || "Kategorie konnte nicht angelegt werden."); return; } const created = await response.json(); setCategories((prev) => { if (prev.some((item) => item.id === created.id)) return prev; return [...prev, created].sort((a, b) => a.name.localeCompare(b.name)); }); event.currentTarget.reset(); setCategoryStatus("Kategorie angelegt."); }; const openCategoryModal = (category: CategoryItem) => { setEditingCategory(category); setCategoryModalError(null); setCategoryModalStatus(null); setCategoryModalOpen(true); }; const closeCategoryModal = () => { setEditingCategory(null); setCategoryModalOpen(false); }; const updateCategory = async (event: React.FormEvent) => { event.preventDefault(); if (!editingCategory) return; setCategoryModalError(null); setCategoryModalStatus(null); const formData = new FormData(event.currentTarget); const name = String(formData.get("name") || "").trim(); const isPublic = formData.get("isPublic") === "on"; if (!name) { setCategoryModalError("Name erforderlich."); return; } const payload: Record = { id: editingCategory.id, name }; if (showPublicControls) { payload.isPublic = isPublic; } const response = await fetch("/api/categories", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }); if (!response.ok) { const data = await response.json(); setCategoryModalError(data.error || "Kategorie konnte nicht aktualisiert werden."); return; } const updated = await response.json(); setCategories((prev) => prev .map((item) => (item.id === updated.id ? updated : item)) .sort((a, b) => a.name.localeCompare(b.name)) ); setCategoryModalStatus("Kategorie aktualisiert."); setCategoryModalOpen(false); }; const deleteCategory = async (categoryId: string) => { const ok = window.confirm("Kategorie wirklich löschen? Zugeordnete Termine bleiben erhalten."); if (!ok) return; const response = await fetch(`/api/categories?id=${categoryId}`, { method: "DELETE" }); if (!response.ok) { const data = await response.json(); setCategoryError(data.error || "Kategorie konnte nicht gelöscht werden."); return; } setCategories((prev) => prev.filter((item) => item.id !== categoryId)); setCategoryStatus("Kategorie gelöscht."); }; const importIcal = async (event: React.FormEvent) => { event.preventDefault(); setImportStatus(null); setImportError(null); if (!importFile) { setImportError("Bitte eine iCal-Datei auswählen."); return; } if (!importCategoryId) { setImportError("Bitte eine Kategorie auswählen."); return; } const formData = new FormData(); formData.append("file", importFile); formData.append("categoryId", importCategoryId); const response = await fetch("/api/ical/import", { method: "POST", body: formData }); if (!response.ok) { const data = await response.json(); setImportError(data.error || "Import fehlgeschlagen."); return; } const data = await response.json(); const details = [ `${data.created || 0} importiert`, `${data.duplicates || 0} doppelt`, `${data.skipped || 0} übersprungen` ]; if (data.recurringSkipped) { details.push(`${data.recurringSkipped} wiederkehrend`); } setImportStatus(`Import abgeschlossen: ${details.join(", ")}.`); setImportFile(null); setImportCategoryId(""); load(); loadAllEvents(); }; return (

Vorschläge

Terminvorschläge

{error &&

{error}

} {events.length === 0 ? (

Keine offenen Vorschläge.

) : (
{events.map((event) => (

{event.title}

{new Date(event.startAt).toLocaleString()} - {new Date(event.endAt).toLocaleString()}

{event.createdBy && (

Vorschlag von {event.createdBy.name || event.createdBy.email || "Unbekannt"}

)} {event.location && (

Ort: {event.location}

)}
{event.description && (

{event.description}

)}
))}
)}

Kategorien

Kategorien verwalten

{showPublicControls && ( )}
{categoryStatus && (

{categoryStatus}

)} {categoryError && (

{categoryError}

)}
{categories.length === 0 ? ( Noch keine Kategorien. ) : (
{categories.map((category) => (
{category.name} {showPublicControls && category.isPublic && ( Öffentlich )}
))}
)}
{categoryModalOpen && editingCategory && (

Kategorie bearbeiten

{showPublicControls && ( )}
{categoryModalStatus && (

{categoryModalStatus}

)} {categoryModalError && (

{categoryModalError}

)}
)}

iCal

iCal-Import

Lade eine iCal-Datei hoch, um Termine direkt zu importieren.

{importFile ? importFile.name : "Keine Datei ausgewählt"}
{importStatus &&

{importStatus}

} {importError &&

{importError}

}

Termine

Alle Termine verwalten

{allEvents.length > 0 ? ( ) : (

Keine Termine vorhanden.

)} {isEditOpen && editEvent && (

Termin bearbeiten

{showPublicControls && (

Öffentlich

)}