Integrate LiveKit SFU and harden booking workflow
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user