Integrate LiveKit SFU and harden booking workflow

This commit is contained in:
2026-09-16 19:38:28 +02:00
parent f1cd07b3ad
commit d0864e2911
55 changed files with 1648 additions and 1234 deletions
+31 -16
View File
@@ -1,6 +1,8 @@
import type { AdminViewServerProps } from "payload";
import { getAdminCalendarItems, type AdminCalendarItem } from "@/lib/booking/adminCalendar";
import { berlinParts } from "@/lib/calendar/timezone";
import { CalendarFeedPanel } from "./CalendarFeedPanel";
import { AdminCallButton } from "./AdminCallButton";
import styles from "./AdminCalendarView.module.css";
const WEEKDAY_LABELS = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
@@ -27,7 +29,8 @@ const BADGE_CLASS: Record<string, string> = {
};
function dateKey(d: Date) {
return d.toISOString().slice(0, 10);
const { year, month, day } = berlinParts(d);
return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
}
function parseMonthParam(value: string | null): Date {
if (value && /^\d{4}-\d{2}$/.test(value)) {
@@ -40,7 +43,11 @@ function monthParam(d: Date) {
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
}
function fmtTime(iso: string) {
return new Date(iso).toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" });
return new Date(iso).toLocaleTimeString("de-DE", {
timeZone: "Europe/Berlin",
hour: "2-digit",
minute: "2-digit",
});
}
export async function AdminCalendarView({ initPageResult }: AdminViewServerProps) {
@@ -48,7 +55,9 @@ export async function AdminCalendarView({ initPageResult }: AdminViewServerProps
const searchParams = req.searchParams;
const month = parseMonthParam(searchParams.get("month"));
const selectedDayParam = searchParams.get("day");
const selectedDay = selectedDayParam ? new Date(`${selectedDayParam}T00:00:00`) : null;
// Noon UTC stays on the same Berlin calendar day even when the server runs
// in UTC and the selected date is around a DST transition.
const selectedDay = selectedDayParam ? new Date(`${selectedDayParam}T12:00:00Z`) : null;
const gridStart = new Date(month.getFullYear(), month.getMonth(), 1);
const firstWeekday = (gridStart.getDay() + 6) % 7;
@@ -82,7 +91,10 @@ export async function AdminCalendarView({ initPageResult }: AdminViewServerProps
const monthLabel = month.toLocaleDateString("de-DE", { month: "long", year: "numeric" });
const dayItems = selectedDay ? (itemsByDay.get(dateKey(selectedDay)) ?? []) : [];
const hours = Array.from({ length: 13 }, (_, i) => i + 8); // 08:00–20:00
const startHours = dayItems.length ? dayItems.map((item) => berlinParts(new Date(item.startTime)).hour) : [];
const firstHour = Math.min(8, ...startHours);
const lastHour = Math.max(20, ...startHours);
const hours = Array.from({ length: lastHour - firstHour + 1 }, (_, i) => i + firstHour);
return (
<div className={styles.page}>
@@ -156,23 +168,26 @@ export async function AdminCalendarView({ initPageResult }: AdminViewServerProps
{selectedDay &&
dayItems.length > 0 &&
hours.map((hour) => {
const hourEntries = dayItems.filter((item) => new Date(item.startTime).getHours() === hour);
const hourEntries = dayItems.filter((item) => berlinParts(new Date(item.startTime)).hour === hour);
return (
<div key={hour} className={styles.hourRow}>
<div className={styles.hourLabel}>{String(hour).padStart(2, "0")}:00</div>
<div className={styles.hourContent}>
{hourEntries.map((entry) => (
<a key={entry.id} href={entry.href} className={styles.entry}>
<span className={`${styles.badge} ${BADGE_CLASS[entry.status] ?? ""}`}>
{STATUS_LABEL[entry.status] ?? entry.status}
</span>
<strong>{entry.title}</strong>
<span>
{fmtTime(entry.startTime)}–{fmtTime(entry.endTime)}
</span>
{entry.subtitle && <span>· {entry.subtitle}</span>}
{entry.appointmentType && <span>· {entry.appointmentType === "online" ? "Online" : "Vor Ort"}</span>}
</a>
<div key={entry.id} className={styles.entry}>
<a href={entry.href} className={styles.entryLink}>
<span className={`${styles.badge} ${BADGE_CLASS[entry.status] ?? ""}`}>
{STATUS_LABEL[entry.status] ?? entry.status}
</span>
<strong>{entry.title}</strong>
<span>
{fmtTime(entry.startTime)}–{fmtTime(entry.endTime)}
</span>
{entry.subtitle && <span>· {entry.subtitle}</span>}
{entry.appointmentType && <span>· {entry.appointmentType === "online" ? "Online" : "Vor Ort"}</span>}
</a>
{entry.callEventSlug && <AdminCallButton eventSlug={entry.callEventSlug} />}
</div>
))}
</div>
</div>