219 lines
5.6 KiB
TypeScript
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);
|
|
}
|
|
}
|