export const dynamic = "force-dynamic"; import { z } from "zod"; import { requireAdmin } from "@/lib/auth/session"; import { fail, handleAuthError, ok } from "@/lib/api"; import { prisma } from "@/lib/prisma"; import { getSetting, setSettings } from "@/lib/settings"; import { SETTING_KEYS } from "@/lib/constants"; import { readJsonBody, validateMutationRequestOrigin } from "@/lib/security/request"; const sortSchema = z.enum([ "date_desc", "date_asc", "customer_asc", "customer_desc", "person_asc", "person_desc" ]); const actionSchema = z.object({ id: z.string().min(1), action: z.enum(["archive", "delete"]) }); type GroupedBooking = { key: string; id: string; customerFirstName: string; customerLastName: string; customerEmail: string; startAt: Date; staffNames: string[]; staffCount: number; }; function parseArchivedKeys(raw: string) { try { const parsed = JSON.parse(raw); if (!Array.isArray(parsed)) return []; return parsed .map((value) => (typeof value === "string" ? value.trim() : "")) .filter((value) => value.length > 0); } catch { return []; } } function bookingKey(row: { id: string; bookingGroupId: string | null }) { return row.bookingGroupId ?? row.id; } function sortBookings(items: GroupedBooking[], sort: z.infer) { const sorted = [...items]; sorted.sort((a, b) => { if (sort === "date_desc") { return b.startAt.getTime() - a.startAt.getTime(); } if (sort === "date_asc") { return a.startAt.getTime() - b.startAt.getTime(); } if (sort === "customer_asc") { return `${a.customerLastName} ${a.customerFirstName}`.localeCompare( `${b.customerLastName} ${b.customerFirstName}` ); } if (sort === "customer_desc") { return `${b.customerLastName} ${b.customerFirstName}`.localeCompare( `${a.customerLastName} ${a.customerFirstName}` ); } if (sort === "person_asc") { return (a.staffNames[0] ?? "").localeCompare(b.staffNames[0] ?? ""); } return (b.staffNames[0] ?? "").localeCompare(a.staffNames[0] ?? ""); }); return sorted; } export async function GET(req: Request) { try { await requireAdmin(); const url = new URL(req.url); const parsedSort = sortSchema.safeParse(url.searchParams.get("sort") ?? "date_desc"); if (!parsedSort.success) { return fail("Ungültige Sortierung", 400, parsedSort.error.flatten()); } const archivedRaw = await getSetting(SETTING_KEYS.LATEST_BOOKINGS_ARCHIVED_KEYS); const archivedSet = new Set(parseArchivedKeys(archivedRaw)); const rows = await prisma.appointment.findMany({ where: { status: "CONFIRMED" }, include: { staff: { select: { name: true } } }, orderBy: { startAt: "desc" }, take: 300 }); const grouped = new Map(); for (const row of rows) { const key = bookingKey(row); if (archivedSet.has(key)) continue; const existing = grouped.get(key); if (!existing) { grouped.set(key, { key, id: row.id, customerFirstName: row.customerFirstName, customerLastName: row.customerLastName, customerEmail: row.customerEmail, startAt: row.startAt, staffNames: [row.staff.name], staffCount: 1 }); continue; } if (!existing.staffNames.includes(row.staff.name)) { existing.staffNames.push(row.staff.name); existing.staffCount = existing.staffNames.length; } } const sorted = sortBookings(Array.from(grouped.values()), parsedSort.data).slice(0, 50); return ok({ bookings: sorted }); } catch (error) { return handleAuthError(error); } } export async function PATCH(req: Request) { try { const originError = validateMutationRequestOrigin(req); if (originError) return originError; await requireAdmin(); const bodyResult = await readJsonBody(req, { maxBytes: 16 * 1024 }); if (!bodyResult.ok) return bodyResult.response; const parsed = actionSchema.safeParse(bodyResult.data); if (!parsed.success) { return fail("Ungültige Aktion", 400, parsed.error.flatten()); } const target = await prisma.appointment.findUnique({ where: { id: parsed.data.id }, select: { id: true, bookingGroupId: true } }); if (!target) { return fail("Buchung nicht gefunden", 404); } const key = bookingKey(target); if (parsed.data.action === "archive") { const archivedRaw = await getSetting(SETTING_KEYS.LATEST_BOOKINGS_ARCHIVED_KEYS); const archived = new Set(parseArchivedKeys(archivedRaw)); archived.add(key); await setSettings({ [SETTING_KEYS.LATEST_BOOKINGS_ARCHIVED_KEYS]: JSON.stringify(Array.from(archived)) }); return ok({ message: "Buchung archiviert." }); } if (target.bookingGroupId) { await prisma.appointment.deleteMany({ where: { bookingGroupId: target.bookingGroupId } }); } else { await prisma.appointment.delete({ where: { id: target.id } }); } const archivedRaw = await getSetting(SETTING_KEYS.LATEST_BOOKINGS_ARCHIVED_KEYS); const archived = parseArchivedKeys(archivedRaw).filter((entry) => entry !== key); await setSettings({ [SETTING_KEYS.LATEST_BOOKINGS_ARCHIVED_KEYS]: JSON.stringify(archived) }); return ok({ message: "Buchung gelöscht." }); } catch (error) { return handleAuthError(error); } }