fix: harden booking and meeting flows

This commit is contained in:
2026-09-17 02:42:27 +02:00
parent cb30ecade9
commit 66710fc65c
42 changed files with 683 additions and 608 deletions
+2 -1
View File
@@ -4,6 +4,7 @@ import { useState, type FormEvent } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { ResendVerificationButton } from "./ResendVerificationButton";
import { TextField, FormError } from "@/components/FormField";
import { safeLoginRedirect } from "@/lib/auth/redirect";
export function LoginForm() {
const router = useRouter();
@@ -32,7 +33,7 @@ export function LoginForm() {
setLoading(false);
return;
}
router.push(searchParams.get("next") || "/konto");
router.push(safeLoginRedirect(searchParams.get("next")));
router.refresh();
} catch {
setError("Verbindung fehlgeschlagen. Bitte versuche es erneut.");
+5 -5
View File
@@ -3,18 +3,18 @@
import { useState } from "react";
import { useRouter } from "next/navigation";
import { BookingStatusBadge, resolveDisplayStatus } from "./BookingStatusBadge";
import type { BookingRequest, Event, Offer } from "@/payload-types";
import type { CustomerBooking } from "@/lib/booking/customerView";
import { formatBerlinDate, formatBerlinTime } from "@/lib/calendar/timezone";
import type { MeetingStatus } from "@/lib/meeting/status";
export function BookingCard({ booking, meetingStatus }: { booking: BookingRequest; meetingStatus?: MeetingStatus | null }) {
export function BookingCard({ booking, meetingStatus }: { booking: CustomerBooking; meetingStatus?: MeetingStatus | null }) {
const router = useRouter();
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const offer = typeof booking.offer === "object" ? (booking.offer as Offer) : null;
const event = typeof booking.linkedEvent === "object" ? (booking.linkedEvent as Event) : null;
const displayStatus = resolveDisplayStatus(booking.status, booking.date);
const offer = booking.offer;
const event = booking.linkedEvent;
const displayStatus = resolveDisplayStatus(booking.status, booking.endTime);
const hasAlternative = booking.status === "pending" && Boolean(booking.proposedAlternative?.date);
async function callAction(path: string) {
+9 -4
View File
@@ -27,6 +27,7 @@ export function BookingWidget({
}) {
const [slotsByDay, setSlotsByDay] = useState<SlotsResponse["slots"]>({});
const [loading, setLoading] = useState(true);
const [loadError, setLoadError] = useState(false);
const [selectedDay, setSelectedDay] = useState<Date | null>(null);
const [selectedSlot, setSelectedSlot] = useState<string | null>(null);
const [appointmentType, setAppointmentType] = useState<"onsite" | "online">("onsite");
@@ -35,12 +36,15 @@ export function BookingWidget({
const [result, setResult] = useState<{ ok: boolean; error?: string } | null>(null);
useEffect(() => {
const controller = new AbortController();
const from = new Date();
const to = new Date(from.getTime() + 28 * 24 * 60 * 60 * 1000);
fetch(`/api/booking/slots?offer=${encodeURIComponent(offerSlug)}&from=${dateKey(from)}&to=${dateKey(to)}`)
.then((res) => res.json())
fetch(`/api/booking/slots?offer=${encodeURIComponent(offerSlug)}&from=${dateKey(from)}&to=${dateKey(to)}`, { signal: controller.signal })
.then((res) => { if (!res.ok) throw new Error("Slots unavailable"); return res.json(); })
.then((data: SlotsResponse) => setSlotsByDay(data.slots ?? {}))
.finally(() => setLoading(false));
.catch(() => { if (!controller.signal.aborted) setLoadError(true); })
.finally(() => { if (!controller.signal.aborted) setLoading(false); });
return () => controller.abort();
}, [offerSlug]);
if (result?.ok) {
@@ -126,11 +130,12 @@ export function BookingWidget({
if (loading) {
return <p className="text-sm text-anouma-plum/70">Verfügbare Termine werden geladen …</p>;
}
if (loadError) return <FormError>Verfügbare Termine konnten nicht geladen werden. Bitte lade die Seite erneut.</FormError>;
return (
<div className="space-y-6">
<div className="grid gap-6 lg:grid-cols-[minmax(0,340px)_1fr]">
<MonthCalendar markers={markers} onSelectDay={setSelectedDay} selectedDay={selectedDay} />
<MonthCalendar markers={markers} onSelectDay={(day) => { setSelectedDay(day); setSelectedSlot(null); }} selectedDay={selectedDay} />
<div>
{!selectedDay && <p className="text-sm text-anouma-plum/70">Wähle einen markierten Tag, um freie Zeiten zu sehen.</p>}
{selectedDay && daySlots.length === 0 && (
+4 -3
View File
@@ -4,16 +4,17 @@ import { useState } from "react";
import { MonthCalendar, type CalendarMarker } from "./MonthCalendar";
import { BookingCard } from "./BookingCard";
import { resolveDisplayStatus } from "./BookingStatusBadge";
import type { BookingRequest, MeetingSetting } from "@/payload-types";
import type { MeetingSetting } from "@/payload-types";
import type { CustomerBooking } from "@/lib/booking/customerView";
import { berlinDateKey } from "@/lib/calendar/timezone";
import { getBookingMeetingStatus } from "@/lib/booking/meetingAccess";
export function PersonalCalendar({ bookings, meetingSettings }: { bookings: BookingRequest[]; meetingSettings: MeetingSetting }) {
export function PersonalCalendar({ bookings, meetingSettings }: { bookings: CustomerBooking[]; meetingSettings: MeetingSetting }) {
const [selectedDay, setSelectedDay] = useState<Date | null>(null);
const markers: CalendarMarker[] = bookings.map((b) => ({
date: new Date(b.date),
status: resolveDisplayStatus(b.status, b.date) as CalendarMarker["status"],
status: resolveDisplayStatus(b.status, b.endTime) as CalendarMarker["status"],
}));
const selectedBookings = selectedDay
+3 -3
View File
@@ -171,9 +171,9 @@ export function CallRoom({ slug }: { slug: string }) {
localTracksRef.current = previewTracks;
try {
let serverUrl = session.serverUrl;
let participantToken = session.participantToken;
if (!serverUrl || !participantToken) {
let serverUrl: string;
let participantToken: string;
{
const response = await fetch(`/api/meetings/${encodeURIComponent(slug)}/connect`, {
method: "POST",
headers: { Authorization: `Bearer ${session.token}` },