Files
anouma/components/booking/BookingCard.tsx
T

122 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { BookingStatusBadge, resolveDisplayStatus } from "./BookingStatusBadge";
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: CustomerBooking; meetingStatus?: MeetingStatus | null }) {
const router = useRouter();
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
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) {
setBusy(true);
setError(null);
try {
const res = await fetch(path, { method: "POST", credentials: "include" });
const data = await res.json().catch(() => ({}));
if (!res.ok) {
setError(data.error || "Aktion fehlgeschlagen.");
setBusy(false);
return;
}
router.refresh();
} catch {
setError("Verbindung fehlgeschlagen.");
setBusy(false);
}
}
return (
<div className="rounded-3xl bg-white p-6 shadow-sm">
<div className="flex flex-wrap items-start justify-between gap-3">
<div>
<h3 className="font-serif text-xl font-medium text-anouma-plum">{offer?.title ?? "Termin"}</h3>
<p className="mt-1 text-sm text-anouma-plum/80">
{formatBerlinDate(new Date(booking.date), { weekday: "long", day: "2-digit", month: "long", year: "numeric" })} · {formatBerlinTime(new Date(booking.startTime), { hour: "2-digit", minute: "2-digit" })} – {formatBerlinTime(new Date(booking.endTime), { hour: "2-digit", minute: "2-digit" })}
</p>
</div>
<BookingStatusBadge status={displayStatus} />
</div>
{booking.status === "pending" && !hasAlternative && (
<p className="mt-4 rounded-2xl bg-anouma-cream-light p-4 text-sm leading-relaxed text-anouma-plum">
Der Termin ist noch nicht verbindlich. Du erhältst eine E-Mail, sobald Anna den Termin bestätigt.
</p>
)}
{hasAlternative && booking.proposedAlternative && (
<div className="mt-4 rounded-2xl bg-anouma-cream-light p-4">
<p className="text-xs font-medium uppercase tracking-wide text-anouma-plum/70">Alternativer Termin</p>
<p className="mt-1 text-base text-anouma-plum">
{formatBerlinDate(new Date(booking.proposedAlternative.date!), { weekday: "long", day: "2-digit", month: "long", year: "numeric" })} · {formatBerlinTime(new Date(booking.proposedAlternative.startTime!), { hour: "2-digit", minute: "2-digit" })} –{" "}
{formatBerlinTime(new Date(booking.proposedAlternative.endTime!), { hour: "2-digit", minute: "2-digit" })}
</p>
<div className="mt-3 flex flex-wrap gap-3">
<button
type="button"
disabled={busy}
onClick={() => callAction(`/api/booking/${booking.id}/accept-alternative`)}
className="rounded-full bg-anouma-mauve-dark px-5 py-2.5 text-sm font-medium text-white hover:bg-anouma-plum disabled:opacity-60"
>
Termin annehmen
</button>
<button
type="button"
disabled={busy}
onClick={() => callAction(`/api/booking/${booking.id}/cancel`)}
className="rounded-full border border-anouma-mauve-dark/40 px-5 py-2.5 text-sm font-medium text-anouma-plum hover:bg-anouma-cream-light disabled:opacity-60"
>
Anderen Termin anfragen
</button>
</div>
</div>
)}
{booking.status === "confirmed" && (
<div className="mt-4 space-y-3">
{booking.appointmentType === "online" && event && meetingStatus !== "ended" && meetingStatus !== "scheduled" && (
<a href={`/termine/${event.slug}/beitreten`} className="inline-flex rounded-full bg-anouma-mauve-dark px-5 py-2.5 text-sm font-medium text-white hover:bg-anouma-plum">
Video-Call betreten
</a>
)}
{booking.appointmentType === "online" && event && meetingStatus === "scheduled" && (
<p className="text-sm text-anouma-plum/75">Der Video-Call wird 10 Minuten vor Beginn freigeschaltet.</p>
)}
{booking.appointmentType === "online" && event && meetingStatus === "ended" && (
<p className="text-sm text-anouma-plum/75">Dieser Video-Call ist bereits beendet.</p>
)}
{booking.appointmentType === "onsite" && (
<p className="text-sm text-anouma-plum/80">Vor Ort – Details siehe Bestätigungs-E-Mail.</p>
)}
</div>
)}
{(booking.status === "pending" || booking.status === "confirmed") && displayStatus !== "past" && !hasAlternative && (
<button
type="button"
disabled={busy}
onClick={() => callAction(`/api/booking/${booking.id}/cancel`)}
className="-ml-2 mt-4 inline-flex min-h-11 items-center rounded-full px-2 text-sm font-medium text-anouma-plum/60 underline underline-offset-4 transition-colors hover:bg-anouma-cream-light hover:text-red-700"
>
Termin stornieren
</button>
)}
{error && (
<p role="alert" className="mt-3 text-sm text-red-700">
{error}
</p>
)}
</div>
);
}