Files
beauty-bookings/src/client/components/booking-status-page.tsx
elpatron 63384aa209 Fix: TypeScript-Fehler für Multi-Treatment-Migration beheben
- admin-calendar.tsx: getTreatmentNames für treatments[] angepasst
- admin-calendar.tsx: getAvailableTimes für treatmentIds[] umgestellt
- admin-calendar.tsx: createManualBooking sendet treatments[] statt treatmentId
- cancellation.ts: treatmentId optional behandeln (Rückwärtskompatibilität)
- review-submission-page.tsx: treatmentName durch treatments[] ersetzt
- booking-status-page.tsx: proposed date/time als optional markiert

Docker-Build erfolgreich getestet.
2025-10-08 19:57:10 +02:00

734 lines
32 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

import React, { useState, useEffect } from "react";
import { useQuery, useMutation } from "@tanstack/react-query";
import { queryClient } from "@/client/rpc-client";
interface BookingStatusPageProps {
token: string;
}
type BookingStatus = "pending" | "confirmed" | "cancelled" | "completed";
interface Treatment {
id: string;
name: string;
duration: number;
price: number;
}
interface BookingDetails {
id: string;
customerName: string;
customerEmail?: string;
customerPhone?: string;
appointmentDate: string;
appointmentTime: string;
treatments: Treatment[];
totalDuration: number;
totalPrice: number;
status: BookingStatus;
notes?: string;
formattedDate: string;
createdAt: string;
canCancel: boolean;
hoursUntilAppointment: number;
}
interface RescheduleProposalDetails {
booking: {
id: string;
customerName: string;
customerEmail?: string;
customerPhone?: string;
status: BookingStatus;
treatments: Treatment[];
totalDuration: number;
totalPrice: number;
};
original: { date: string; time: string };
proposed: { date?: string; time?: string };
expiresAt: string;
hoursUntilExpiry: number;
isExpired: boolean;
canRespond: boolean;
}
function getStatusInfo(status: BookingStatus) {
switch (status) {
case "pending":
return {
label: "Wartet auf Bestätigung",
color: "yellow",
icon: "⏳",
bgColor: "bg-yellow-50",
borderColor: "border-yellow-200",
textColor: "text-yellow-800",
badgeColor: "bg-yellow-100 text-yellow-800",
};
case "confirmed":
return {
label: "Bestätigt",
color: "green",
icon: "✓",
bgColor: "bg-green-50",
borderColor: "border-green-200",
textColor: "text-green-800",
badgeColor: "bg-green-100 text-green-800",
};
case "cancelled":
return {
label: "Storniert",
color: "red",
icon: "✕",
bgColor: "bg-red-50",
borderColor: "border-red-200",
textColor: "text-red-800",
badgeColor: "bg-red-100 text-red-800",
};
case "completed":
return {
label: "Abgeschlossen",
color: "gray",
icon: "✓",
bgColor: "bg-gray-50",
borderColor: "border-gray-200",
textColor: "text-gray-800",
badgeColor: "bg-gray-100 text-gray-800",
};
}
}
export default function BookingStatusPage({ token }: BookingStatusPageProps) {
const [showCancelConfirm, setShowCancelConfirm] = useState(false);
const [isCancelling, setIsCancelling] = useState(false);
const [cancellationResult, setCancellationResult] = useState<{ success: boolean; message: string; formattedDate?: string } | null>(null);
const [rescheduleProposal, setRescheduleProposal] = useState<RescheduleProposalDetails | null>(null);
const [rescheduleResult, setRescheduleResult] = useState<{ success: boolean; message: string } | null>(null);
const [isAccepting, setIsAccepting] = useState(false);
const [isDeclining, setIsDeclining] = useState(false);
const [showDeclineConfirm, setShowDeclineConfirm] = useState(false);
const [oneClickAction, setOneClickAction] = useState<string | null>(null);
const [oneClickLoading, setOneClickLoading] = useState(false);
// Fetch booking details
const { data: booking, isLoading, error, refetch, error: bookingError } = useQuery(
queryClient.cancellation.getBookingByToken.queryOptions({ input: { token } })
);
// Try fetching reschedule proposal if booking not found or error
const rescheduleQuery = useQuery<RescheduleProposalDetails>({
...queryClient.cancellation.getRescheduleProposal.queryOptions({ input: { token } }),
enabled: !!token && (!!bookingError || !booking),
});
// Handle reschedule proposal data
useEffect(() => {
if (rescheduleQuery.data) {
setRescheduleProposal(rescheduleQuery.data);
} else if (rescheduleQuery.error) {
setRescheduleProposal(null);
}
}, [rescheduleQuery.data, rescheduleQuery.error]);
// Cancellation mutation
const cancelMutation = useMutation({
...queryClient.cancellation.cancelByToken.mutationOptions(),
onSuccess: (result: any) => {
setCancellationResult({
success: true,
message: result.message,
formattedDate: result.formattedDate,
});
setIsCancelling(false);
setShowCancelConfirm(false);
refetch(); // Refresh booking data
},
onError: (error: any) => {
setCancellationResult({
success: false,
message: error?.message || "Ein Fehler ist aufgetreten.",
});
setIsCancelling(false);
},
});
const acceptMutation = useMutation({
...queryClient.bookings.acceptReschedule.mutationOptions(),
onSuccess: (result: any) => {
setRescheduleResult({ success: true, message: result.message });
setIsAccepting(false);
setShowDeclineConfirm(false);
refetch();
},
onError: (error: any) => {
setRescheduleResult({ success: false, message: error?.message || 'Ein Fehler ist aufgetreten.' });
setIsAccepting(false);
}
});
const declineMutation = useMutation({
...queryClient.bookings.declineReschedule.mutationOptions(),
onSuccess: (result: any) => {
setRescheduleResult({ success: true, message: result.message });
setIsDeclining(false);
setShowDeclineConfirm(false);
},
onError: (error: any) => {
setRescheduleResult({ success: false, message: error?.message || 'Ein Fehler ist aufgetreten.' });
setIsDeclining(false);
}
});
const handleCancel = () => {
setIsCancelling(true);
setCancellationResult(null);
cancelMutation.mutate({ token });
};
// Handle one-click actions from URL parameters
useEffect(() => {
if (rescheduleProposal && !oneClickAction) {
const urlParams = new URLSearchParams(window.location.search);
const action = urlParams.get('action');
if (action === 'accept' || action === 'decline') {
setOneClickAction(action);
}
}
}, [rescheduleProposal, oneClickAction]);
// Auto-execute one-click action
useEffect(() => {
if (oneClickAction && rescheduleProposal && !oneClickLoading && !rescheduleResult) {
setOneClickLoading(true);
if (oneClickAction === 'accept') {
const confirmAccept = window.confirm(
`Möchtest du den neuen Termin am ${rescheduleProposal.proposed.date || 'TBD'} um ${rescheduleProposal.proposed.time || 'TBD'} Uhr akzeptieren?`
);
if (confirmAccept) {
acceptMutation.mutate({ token });
} else {
setOneClickLoading(false);
setOneClickAction(null);
}
} else if (oneClickAction === 'decline') {
const confirmDecline = window.confirm(
`Möchtest du den Vorschlag ablehnen? Dein ursprünglicher Termin am ${rescheduleProposal.original.date} um ${rescheduleProposal.original.time} Uhr bleibt dann bestehen.`
);
if (confirmDecline) {
declineMutation.mutate({ token });
} else {
setOneClickLoading(false);
setOneClickAction(null);
}
}
}
}, [oneClickAction, rescheduleProposal, oneClickLoading, rescheduleResult, acceptMutation, declineMutation, token]);
// Reset one-click loading when mutations complete
useEffect(() => {
if (rescheduleResult) {
setOneClickLoading(false);
setOneClickAction(null);
}
}, [rescheduleResult]);
if (isLoading) {
return (
<div className="min-h-screen bg-gradient-to-br from-pink-50 to-purple-50 flex items-center justify-center p-4">
<div className="bg-white p-8 rounded-lg shadow-lg max-w-2xl w-full">
<div className="flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-pink-500"></div>
<span className="ml-3 text-gray-600">Buchung wird geladen...</span>
</div>
</div>
</div>
);
}
if (error && !rescheduleProposal) {
return (
<div className="min-h-screen bg-gradient-to-br from-pink-50 to-purple-50 flex items-center justify-center p-4">
<div className="bg-white p-8 rounded-lg shadow-lg max-w-md w-full">
<div className="text-center">
<div className="w-16 h-16 mx-auto mb-4 bg-red-100 rounded-full flex items-center justify-center">
<svg className="w-8 h-8 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
<h2 className="text-xl font-bold text-gray-900 mb-2">Link nicht verfügbar</h2>
<p className="text-gray-600 mb-4">
Dieser Buchungslink ist nicht mehr verfügbar. Mögliche Gründe:
</p>
<ul className="text-sm text-gray-600 text-left mb-6 space-y-2">
<li> Der Link ist abgelaufen</li>
<li> Die Buchung wurde bereits storniert</li>
<li> Der Link wurde bereits verwendet</li>
</ul>
<div className="space-y-3">
<a
href="/"
className="inline-flex items-center px-4 py-2 bg-pink-600 text-white rounded-lg hover:bg-pink-700 transition-colors"
>
Neue Buchung erstellen
</a>
<p className="text-sm text-gray-500">
Bei Fragen wende dich direkt an uns.
</p>
</div>
</div>
</div>
</div>
);
}
if (!booking && !rescheduleProposal) {
return (
<div className="min-h-screen bg-gradient-to-br from-pink-50 to-purple-50 flex items-center justify-center p-4">
<div className="bg-white p-8 rounded-lg shadow-lg max-w-md w-full">
<div className="text-center">
<h2 className="text-xl font-bold text-gray-900 mb-2">Buchung nicht gefunden</h2>
<p className="text-gray-600 mb-4">
Die angeforderte Buchung konnte nicht gefunden werden.
</p>
<a
href="/"
className="inline-flex items-center px-4 py-2 bg-pink-600 text-white rounded-lg hover:bg-pink-700 transition-colors"
>
Zur Startseite
</a>
</div>
</div>
</div>
);
}
if (rescheduleProposal) {
const isExpired = rescheduleProposal.isExpired;
const handleAccept = () => {
setIsAccepting(true);
setRescheduleResult(null);
acceptMutation.mutate({ token });
};
const handleDecline = () => {
setIsDeclining(true);
setRescheduleResult(null);
declineMutation.mutate({ token });
};
return (
<div className="min-h-screen bg-gradient-to-br from-pink-50 to-purple-50 py-8 px-4">
<div className="max-w-2xl mx-auto">
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
<div className="flex items-center justify-between mb-4">
<img src="/assets/stargilnails_logo_transparent_112.png" alt="Stargil Nails Logo" className="w-16 h-16 object-contain" />
<span className={`px-4 py-2 rounded-full text-sm font-semibold bg-orange-100 text-orange-800`}>
Terminänderung vorgeschlagen
</span>
</div>
<h1 className="text-2xl font-bold text-gray-900">Vorschlag zur Terminänderung</h1>
<p className="text-gray-600 mt-1">Bitte bestätige, ob der neue Termin für dich passt.</p>
</div>
{oneClickLoading && (
<div className="mb-6 p-4 rounded-lg bg-blue-50 border border-blue-200">
<div className="flex items-center">
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-blue-500 mr-3"></div>
<p className="text-blue-800">
{oneClickAction === 'accept' ? 'Akzeptiere Termin...' : 'Lehne Termin ab...'}
</p>
</div>
</div>
)}
{rescheduleResult && (
<div className={`mb-6 p-4 rounded-lg ${rescheduleResult.success ? 'bg-green-50 border border-green-200' : 'bg-red-50 border border-red-200'}`}>
<p className={rescheduleResult.success ? 'text-green-800' : 'text-red-800'}>
{rescheduleResult.message}
</p>
</div>
)}
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Vergleich</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="border rounded-lg p-4 bg-gray-50">
<div className="text-sm text-gray-500 font-semibold mb-1">Aktueller Termin</div>
<div className="text-gray-900 font-medium">{rescheduleProposal.original.date} um {rescheduleProposal.original.time} Uhr</div>
<div className="text-gray-700 text-sm mt-2">
{rescheduleProposal.booking.treatments && rescheduleProposal.booking.treatments.length > 0 ? (
<>
{rescheduleProposal.booking.treatments.length <= 2 ? (
rescheduleProposal.booking.treatments.map((t, i) => (
<div key={i}>{t.name}</div>
))
) : (
<>
{rescheduleProposal.booking.treatments.slice(0, 2).map((t, i) => (
<div key={i}>{t.name}</div>
))}
<div className="text-gray-500 italic">+{rescheduleProposal.booking.treatments.length - 2} weitere</div>
</>
)}
<div className="text-gray-600 mt-1 text-xs">
{rescheduleProposal.booking.totalDuration} Min
</div>
</>
) : (
<span className="text-gray-400 italic">Keine Behandlungen</span>
)}
</div>
</div>
<div className="border rounded-lg p-4 bg-orange-50">
<div className="text-sm text-orange-700 font-semibold mb-1">Neuer Vorschlag</div>
<div className="text-gray-900 font-medium">{rescheduleProposal.proposed.date || 'TBD'} um {rescheduleProposal.proposed.time || 'TBD'} Uhr</div>
<div className="text-gray-700 text-sm mt-2">
{rescheduleProposal.booking.treatments && rescheduleProposal.booking.treatments.length > 0 ? (
<>
{rescheduleProposal.booking.treatments.length <= 2 ? (
rescheduleProposal.booking.treatments.map((t, i) => (
<div key={i}>{t.name}</div>
))
) : (
<>
{rescheduleProposal.booking.treatments.slice(0, 2).map((t, i) => (
<div key={i}>{t.name}</div>
))}
<div className="text-gray-500 italic">+{rescheduleProposal.booking.treatments.length - 2} weitere</div>
</>
)}
<div className="text-gray-600 mt-1 text-xs">
{rescheduleProposal.booking.totalDuration} Min
</div>
</>
) : (
<span className="text-gray-400 italic">Keine Behandlungen</span>
)}
</div>
</div>
</div>
<div className="mt-4 bg-yellow-50 border border-yellow-200 rounded-lg p-3 text-sm text-yellow-800">
Bitte antworte bis {new Date(rescheduleProposal.expiresAt).toLocaleDateString('de-DE')} {new Date(rescheduleProposal.expiresAt).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })} ({rescheduleProposal.hoursUntilExpiry} Stunden).
</div>
</div>
{!isExpired && !rescheduleResult && !oneClickLoading && (
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
<div className="flex flex-col sm:flex-row gap-3">
<button
onClick={handleAccept}
disabled={isAccepting}
className="flex-1 bg-green-600 text-white py-3 px-4 rounded-lg font-medium hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{isAccepting ? 'Akzeptiere...' : 'Neuen Termin akzeptieren'}
</button>
<button
onClick={() => setShowDeclineConfirm(true)}
disabled={isDeclining}
className="flex-1 bg-red-600 text-white py-3 px-4 rounded-lg font-medium hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
Vorschlag ablehnen
</button>
</div>
<div className="mt-3 text-sm text-gray-600">Wenn du ablehnst, bleibt dein ursprünglicher Termin bestehen.</div>
</div>
)}
{isExpired && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
<p className="text-yellow-800 text-sm">
Diese Terminänderung ist abgelaufen. Dein ursprünglicher Termin bleibt bestehen. Bei Fragen kontaktiere uns bitte.
</p>
</div>
)}
{showDeclineConfirm && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl p-6 w-full max-w-md">
<h4 className="text-lg font-semibold text-gray-900 mb-2">Vorschlag ablehnen?</h4>
<p className="text-sm text-gray-700 mb-4">
Bist du sicher, dass du den neuen Terminvorschlag ablehnen möchtest?<br />
Dein ursprünglicher Termin am {rescheduleProposal.original.date} um {rescheduleProposal.original.time} bleibt dann bestehen.
</p>
<div className="flex gap-3">
<button
onClick={() => setShowDeclineConfirm(false)}
className="flex-1 bg-gray-100 text-gray-700 py-2 rounded-lg hover:bg-gray-200"
>
Abbrechen
</button>
<button
onClick={handleDecline}
disabled={isDeclining}
className="flex-1 bg-red-600 text-white py-2 rounded-lg hover:bg-red-700 disabled:opacity-50"
>
{isDeclining ? 'Lehne ab...' : 'Ja, ablehnen'}
</button>
</div>
</div>
</div>
)}
<div className="text-center">
<a href="/" className="inline-flex items-center text-pink-600 hover:text-pink-700 font-medium">
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
Zurück zur Startseite
</a>
</div>
</div>
</div>
);
}
const statusInfo = getStatusInfo(booking?.status || "pending");
return (
<div className="min-h-screen bg-gradient-to-br from-pink-50 to-purple-50 py-8 px-4">
<div className="max-w-2xl mx-auto">
{/* Header */}
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
<div className="flex items-center justify-between mb-4">
<img
src="/assets/stargilnails_logo_transparent_112.png"
alt="Stargil Nails Logo"
className="w-16 h-16 object-contain"
/>
<span className={`px-4 py-2 rounded-full text-sm font-semibold ${statusInfo.badgeColor}`}>
{statusInfo.icon} {statusInfo.label}
</span>
</div>
<h1 className="text-2xl font-bold text-gray-900">Buchungsübersicht</h1>
<p className="text-gray-600 mt-1">Hier findest du alle Details zu deinem Termin</p>
</div>
{/* Cancellation Result */}
{cancellationResult && (
<div className={`mb-6 p-4 rounded-lg ${
cancellationResult.success ? 'bg-green-50 border border-green-200' : 'bg-red-50 border border-red-200'
}`}>
<p className={cancellationResult.success ? 'text-green-800' : 'text-red-800'}>
{cancellationResult.message}
{cancellationResult.formattedDate && (
<><br />Stornierter Termin: {cancellationResult.formattedDate}</>
)}
</p>
</div>
)}
{/* Status Banner */}
<div className={`${statusInfo.bgColor} border ${statusInfo.borderColor} rounded-lg p-6 mb-6`}>
<div className="flex items-start">
<div className={`text-4xl mr-4 ${statusInfo.textColor}`}>{statusInfo.icon}</div>
<div className="flex-1">
<h2 className={`text-xl font-bold ${statusInfo.textColor} mb-2`}>
Status: {statusInfo.label}
</h2>
{booking?.status === "pending" && (
<p className={statusInfo.textColor}>
Wir haben deine Terminanfrage erhalten und werden sie in Kürze prüfen. Du erhältst eine E-Mail, sobald dein Termin bestätigt wurde.
</p>
)}
{booking?.status === "confirmed" && (
<p className={statusInfo.textColor}>
Dein Termin wurde bestätigt! Wir freuen uns auf dich. Du hast eine Bestätigungs-E-Mail mit Kalendereintrag erhalten.
</p>
)}
{booking?.status === "cancelled" && (
<p className={statusInfo.textColor}>
Dieser Termin wurde storniert. Du kannst jederzeit einen neuen Termin buchen.
</p>
)}
{booking?.status === "completed" && (
<p className={statusInfo.textColor}>
Dieser Termin wurde erfolgreich abgeschlossen. Vielen Dank für deinen Besuch!
</p>
)}
</div>
</div>
</div>
{/* Appointment Details */}
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<svg className="w-5 h-5 mr-2 text-pink-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
Termin-Details
</h2>
<div className="space-y-3">
<div className="flex justify-between py-2 border-b border-gray-100">
<span className="text-gray-600">Datum:</span>
<span className="font-medium text-gray-900">{booking?.formattedDate}</span>
</div>
<div className="flex justify-between py-2 border-b border-gray-100">
<span className="text-gray-600">Uhrzeit:</span>
<span className="font-medium text-gray-900">{booking?.appointmentTime} Uhr</span>
</div>
{/* Treatments List */}
<div className="py-2 border-b border-gray-100">
<div className="text-gray-600 mb-2">Behandlungen:</div>
{booking?.treatments && booking.treatments.length > 0 ? (
<div className="bg-gray-50 rounded-lg p-3 space-y-2">
{booking.treatments.map((treatment, index) => (
<div key={index} className="flex justify-between items-center text-sm">
<span className="font-medium text-gray-900"> {treatment.name}</span>
<span className="text-gray-600">
{treatment.duration} Min - {treatment.price.toFixed(2)}
</span>
</div>
))}
<div className="flex justify-between items-center pt-2 mt-2 border-t border-gray-200 font-semibold">
<span className="text-gray-900">Gesamt:</span>
<span className="text-gray-900">
{booking.totalDuration} Min - {booking.totalPrice.toFixed(2)}
</span>
</div>
</div>
) : (
<div className="space-y-2">
<span className="text-gray-400 text-sm italic">Keine Behandlungen angegeben</span>
{((booking?.totalDuration ?? 0) > 0 || (booking?.totalPrice ?? 0) > 0) && (
<div className="bg-gray-50 rounded-lg p-3">
<div className="flex justify-between items-center font-semibold text-sm">
<span className="text-gray-900">Gesamt:</span>
<span className="text-gray-900">
{booking?.totalDuration ?? 0} Min - {(booking?.totalPrice ?? 0).toFixed(2)}
</span>
</div>
</div>
)}
</div>
)}
</div>
{booking?.hoursUntilAppointment && booking.hoursUntilAppointment > 0 && booking.status !== "cancelled" && booking.status !== "completed" && (
<div className="flex justify-between py-2">
<span className="text-gray-600">Verbleibende Zeit:</span>
<span className="font-medium text-pink-600">
{booking.hoursUntilAppointment} Stunde{booking.hoursUntilAppointment !== 1 ? 'n' : ''}
</span>
</div>
)}
</div>
</div>
{/* Customer Details */}
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<svg className="w-5 h-5 mr-2 text-pink-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
Deine Daten
</h2>
<div className="space-y-3">
<div className="flex justify-between py-2 border-b border-gray-100">
<span className="text-gray-600">Name:</span>
<span className="font-medium text-gray-900">{booking?.customerName}</span>
</div>
<div className="flex justify-between py-2 border-b border-gray-100">
<span className="text-gray-600">E-Mail:</span>
<span className="font-medium text-gray-900">{booking?.customerEmail || '—'}</span>
</div>
<div className="flex justify-between py-2">
<span className="text-gray-600">Telefon:</span>
<span className="font-medium text-gray-900">{booking?.customerPhone || '—'}</span>
</div>
</div>
{booking?.notes && (
<div className="mt-4 pt-4 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-700 mb-2">Notizen:</h3>
<p className="text-gray-600 text-sm">{booking.notes}</p>
</div>
)}
</div>
{/* Cancellation Section */}
{booking?.canCancel && !cancellationResult?.success && (
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<svg className="w-5 h-5 mr-2 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
Termin stornieren
</h2>
{!showCancelConfirm ? (
<div>
<p className="text-gray-600 mb-4">
Du kannst diesen Termin noch bis {parseInt(process.env.MIN_STORNO_TIMESPAN || "24")} Stunden vor dem Termin kostenlos stornieren.
</p>
<button
onClick={() => setShowCancelConfirm(true)}
className="w-full bg-red-600 text-white py-3 px-4 rounded-lg font-medium hover:bg-red-700 transition-colors flex items-center justify-center"
>
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
Termin stornieren
</button>
</div>
) : (
<div>
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-4">
<p className="text-red-800 font-semibold mb-2">Bist du sicher?</p>
<p className="text-red-700 text-sm">
Diese Aktion kann nicht rückgängig gemacht werden. Der Termin wird storniert und der Slot wird wieder für andere Kunden verfügbar.
</p>
</div>
<div className="flex gap-3">
<button
onClick={handleCancel}
disabled={isCancelling}
className="flex-1 bg-red-600 text-white py-3 px-4 rounded-lg font-medium hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center justify-center"
>
{isCancelling ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Storniere...
</>
) : (
<>Ja, stornieren</>
)}
</button>
<button
onClick={() => setShowCancelConfirm(false)}
disabled={isCancelling}
className="flex-1 bg-gray-200 text-gray-800 py-3 px-4 rounded-lg font-medium hover:bg-gray-300 disabled:opacity-50 transition-colors"
>
Abbrechen
</button>
</div>
</div>
)}
</div>
)}
{!booking?.canCancel && booking?.status !== "cancelled" && booking?.status !== "completed" && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
<p className="text-yellow-800 text-sm">
<strong> Stornierungsfrist abgelaufen:</strong> Dieser Termin liegt weniger als {parseInt(process.env.MIN_STORNO_TIMESPAN || "24")} Stunden in der Zukunft und kann nicht mehr online storniert werden. Bitte kontaktiere uns direkt.
</p>
</div>
)}
{/* Footer */}
<div className="text-center">
<a
href="/"
className="inline-flex items-center text-pink-600 hover:text-pink-700 font-medium"
>
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
Zurück zur Startseite
</a>
</div>
</div>
</div>
);
}