- 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.
734 lines
32 KiB
TypeScript
734 lines
32 KiB
TypeScript
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>
|
||
);
|
||
}
|
||
|