- Customer accounts (separate auth collection) with /konto area - Availability + AvailabilityOverrides collections driving real slot calculation - BookingRequests with race-safe confirmation (Postgres advisory lock + transaction) - Booking emails (request received, admin notify, confirmed, rejected, alternative proposed/accepted, cancelled) - Confirmed online bookings auto-create a private linked video-call Event - Custom Payload admin calendar view (month grid + day schedule) - Wired booking widget into offer pages and /termin-buchen
82 lines
2.5 KiB
TypeScript
82 lines
2.5 KiB
TypeScript
import type { Payload } from "payload";
|
|
import type { BookingRequest, Customer, Event, Offer } from "@/payload-types";
|
|
|
|
export type AdminCalendarItem = {
|
|
id: string;
|
|
kind: "booking" | "event";
|
|
title: string;
|
|
subtitle?: string;
|
|
date: string;
|
|
startTime: string;
|
|
endTime: string;
|
|
status: string; // booking status, or "public" for a standalone Event
|
|
appointmentType?: "onsite" | "online";
|
|
href: string;
|
|
};
|
|
|
|
/**
|
|
* Combines private BookingRequests with standalone public Events (excluding
|
|
* the auto-generated Events already represented by a confirmed online
|
|
* booking, so Anna never sees the same appointment listed twice) into one
|
|
* unified calendar feed for the admin view.
|
|
*/
|
|
export async function getAdminCalendarItems(
|
|
payload: Payload,
|
|
range: { from: Date; to: Date },
|
|
): Promise<AdminCalendarItem[]> {
|
|
const fromISO = range.from.toISOString();
|
|
const toISO = range.to.toISOString();
|
|
|
|
const [{ docs: bookings }, { docs: events }] = await Promise.all([
|
|
payload.find({
|
|
collection: "booking-requests",
|
|
where: { and: [{ date: { greater_than_equal: fromISO } }, { date: { less_than_equal: toISO } }] },
|
|
depth: 2,
|
|
limit: 500,
|
|
}),
|
|
payload.find({
|
|
collection: "events",
|
|
where: {
|
|
and: [
|
|
{ date: { greater_than_equal: fromISO } },
|
|
{ date: { less_than_equal: toISO } },
|
|
{ isPrivateBooking: { not_equals: true } },
|
|
],
|
|
},
|
|
depth: 0,
|
|
limit: 500,
|
|
}),
|
|
]);
|
|
|
|
const bookingItems: AdminCalendarItem[] = (bookings as BookingRequest[]).map((b) => {
|
|
const offer = typeof b.offer === "object" ? (b.offer as Offer) : null;
|
|
const customer = typeof b.user === "object" ? (b.user as Customer) : null;
|
|
return {
|
|
id: `booking-${b.id}`,
|
|
kind: "booking",
|
|
title: offer?.title ?? "Buchung",
|
|
subtitle: customer?.name,
|
|
date: b.date,
|
|
startTime: b.startTime,
|
|
endTime: b.endTime,
|
|
status: b.status,
|
|
appointmentType: b.appointmentType,
|
|
href: `/admin/collections/booking-requests/${b.id}`,
|
|
};
|
|
});
|
|
|
|
const eventItems: AdminCalendarItem[] = (events as Event[]).map((e) => ({
|
|
id: `event-${e.id}`,
|
|
kind: "event",
|
|
title: e.title,
|
|
date: e.date,
|
|
startTime: e.startTime ?? e.date,
|
|
endTime: e.endTime ?? e.date,
|
|
status: "public",
|
|
appointmentType: e.isOnline ? "online" : "onsite",
|
|
href: `/admin/collections/events/${e.id}`,
|
|
}));
|
|
|
|
return [...bookingItems, ...eventItems].sort((a, b) => new Date(a.startTime).getTime() - new Date(b.startTime).getTime());
|
|
}
|