Files
Calbook/app/api/admin/letzte-buchungen/route.ts

219 lines
5.6 KiB
TypeScript

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<typeof sortSchema>) {
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<string, GroupedBooking>();
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);
}
}