379 lines
16 KiB
TypeScript
379 lines
16 KiB
TypeScript
import React, { useState } 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";
|
||
|
||
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);
|
||
|
||
// Fetch booking details
|
||
const { data: booking, isLoading, error, refetch } = useQuery(
|
||
queryClient.cancellation.getBookingByToken.queryOptions({ input: { token } })
|
||
);
|
||
|
||
// 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 handleCancel = () => {
|
||
setIsCancelling(true);
|
||
setCancellationResult(null);
|
||
cancelMutation.mutate({ token });
|
||
};
|
||
|
||
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) {
|
||
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">Fehler</h2>
|
||
<p className="text-gray-600 mb-4">
|
||
{error?.message || "Der Link ist ungültig oder abgelaufen."}
|
||
</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 (!booking) {
|
||
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>
|
||
);
|
||
}
|
||
|
||
const statusInfo = getStatusInfo(booking.status);
|
||
|
||
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>
|
||
<div className="flex justify-between py-2 border-b border-gray-100">
|
||
<span className="text-gray-600">Behandlung:</span>
|
||
<span className="font-medium text-gray-900">{booking.treatmentName}</span>
|
||
</div>
|
||
<div className="flex justify-between py-2 border-b border-gray-100">
|
||
<span className="text-gray-600">Dauer:</span>
|
||
<span className="font-medium text-gray-900">{booking.treatmentDuration} Minuten</span>
|
||
</div>
|
||
{booking.treatmentPrice > 0 && (
|
||
<div className="flex justify-between py-2 border-b border-gray-100">
|
||
<span className="text-gray-600">Preis:</span>
|
||
<span className="font-medium text-gray-900">{booking.treatmentPrice.toFixed(2)} €</span>
|
||
</div>
|
||
)}
|
||
{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>
|
||
);
|
||
}
|
||
|