feat: initialize CalBook project with comprehensive scheduling, admin, and deployment infrastructure
This commit is contained in:
218
app/api/admin/letzte-buchungen/route.ts
Normal file
218
app/api/admin/letzte-buchungen/route.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user