fix: harden booking and meeting flows
This commit is contained in:
@@ -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.");
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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}` },
|
||||
|
||||
Reference in New Issue
Block a user