Implementiere Stornierungssystem und E-Mail-Links zur Hauptseite
- Neues Stornierungssystem mit sicheren Token-basierten Links - Stornierungsfrist konfigurierbar über MIN_STORNO_TIMESPAN (24h Standard) - Stornierungs-Seite mit Buchungsdetails und Ein-Klick-Stornierung - Automatische Slot-Freigabe bei Stornierung - Stornierungs-Link in Bestätigungs-E-Mails integriert - Alle E-Mails enthalten jetzt Links zur Hauptseite (DOMAIN Variable) - Schöne HTML-Buttons und Text-Links in allen E-Mail-Templates - Vollständige Validierung: Vergangenheits-Check, Token-Ablauf, Stornierungsfrist - Responsive Stornierungs-Seite mit Loading-States und Fehlerbehandlung - Dokumentation in README.md aktualisiert
This commit is contained in:
247
src/client/components/cancellation-page.tsx
Normal file
247
src/client/components/cancellation-page.tsx
Normal file
@@ -0,0 +1,247 @@
|
||||
import React, { useState } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { queryClient } from "@/client/rpc-client";
|
||||
|
||||
interface CancellationPageProps {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export default function CancellationPage({ token }: CancellationPageProps) {
|
||||
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 } = useQuery({
|
||||
queryKey: ["cancellation", "booking", token],
|
||||
queryFn: () => queryClient.cancellation.getBookingByToken({ token }),
|
||||
retry: false,
|
||||
});
|
||||
|
||||
// Cancellation mutation
|
||||
const cancelMutation = useMutation({
|
||||
mutationFn: () => queryClient.cancellation.cancelByToken({ token }),
|
||||
onSuccess: (result) => {
|
||||
setCancellationResult({
|
||||
success: true,
|
||||
message: result.message,
|
||||
formattedDate: result.formattedDate,
|
||||
});
|
||||
setIsCancelling(false);
|
||||
},
|
||||
onError: (error: any) => {
|
||||
setCancellationResult({
|
||||
success: false,
|
||||
message: error?.message || "Ein Fehler ist aufgetreten.",
|
||||
});
|
||||
setIsCancelling(false);
|
||||
},
|
||||
});
|
||||
|
||||
const handleCancel = () => {
|
||||
setIsCancelling(true);
|
||||
setCancellationResult(null);
|
||||
cancelMutation.mutate();
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-pink-50 to-purple-50 flex items-center justify-center">
|
||||
<div className="bg-white p-8 rounded-lg shadow-lg max-w-md 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">Termin 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">
|
||||
<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 Stornierungs-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 (cancellationResult) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-pink-50 to-purple-50 flex items-center justify-center">
|
||||
<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 rounded-full flex items-center justify-center ${
|
||||
cancellationResult.success ? 'bg-green-100' : 'bg-red-100'
|
||||
}`}>
|
||||
{cancellationResult.success ? (
|
||||
<svg className="w-8 h-8 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
) : (
|
||||
<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 mb-2 ${
|
||||
cancellationResult.success ? 'text-green-700' : 'text-red-700'
|
||||
}`}>
|
||||
{cancellationResult.success ? 'Termin storniert' : 'Stornierung fehlgeschlagen'}
|
||||
</h2>
|
||||
<p className="text-gray-600 mb-4">
|
||||
{cancellationResult.message}
|
||||
{cancellationResult.formattedDate && (
|
||||
<><br />Stornierter Termin: {cancellationResult.formattedDate}</>
|
||||
)}
|
||||
</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">
|
||||
<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">Termin nicht gefunden</h2>
|
||||
<p className="text-gray-600 mb-4">
|
||||
Der angeforderte Termin 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>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-pink-50 to-purple-50 flex items-center justify-center">
|
||||
<div className="bg-white p-8 rounded-lg shadow-lg max-w-md w-full">
|
||||
<div className="text-center mb-6">
|
||||
<img
|
||||
src="/assets/stargilnails_logo_transparent_112.png"
|
||||
alt="Stargil Nails Logo"
|
||||
className="w-16 h-16 mx-auto mb-4 object-contain"
|
||||
/>
|
||||
<h1 className="text-2xl font-bold text-gray-900">Termin stornieren</h1>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 rounded-lg p-6 mb-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">Termin-Details</h2>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">Name:</span>
|
||||
<span className="font-medium text-gray-900">{booking.customerName}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">Datum:</span>
|
||||
<span className="font-medium text-gray-900">{booking.formattedDate}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">Uhrzeit:</span>
|
||||
<span className="font-medium text-gray-900">{booking.appointmentTime}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">Behandlung:</span>
|
||||
<span className="font-medium text-gray-900">{(booking as any).treatmentName || 'Unbekannte Behandlung'}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">Status:</span>
|
||||
<span className={`font-medium ${
|
||||
booking.status === 'confirmed' ? 'text-green-600' :
|
||||
booking.status === 'pending' ? 'text-yellow-600' :
|
||||
'text-gray-600'
|
||||
}`}>
|
||||
{booking.status === 'confirmed' ? 'Bestätigt' :
|
||||
booking.status === 'pending' ? 'Ausstehend' :
|
||||
booking.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{booking.status === 'cancelled' ? (
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
|
||||
<p className="text-yellow-800 text-center">
|
||||
Dieser Termin wurde bereits storniert.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<p className="text-blue-800 text-sm">
|
||||
<strong>ℹ️ Stornierungsfrist:</strong> Termine können nur bis zu einer bestimmten Zeit vor dem Termin storniert werden.
|
||||
Falls die Stornierung nicht möglich ist, erhältst du eine entsprechende Meldung.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||
<p className="text-red-800 text-sm">
|
||||
<strong>Hinweis:</strong> Nach der Stornierung wird der Termin-Slot wieder für andere Kunden verfügbar.
|
||||
Eine erneute Buchung ist jederzeit möglich.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleCancel}
|
||||
disabled={isCancelling}
|
||||
className="w-full 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...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<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>
|
||||
Ich möchte diesen Termin stornieren
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-center mt-6">
|
||||
<a
|
||||
href="/"
|
||||
className="text-pink-600 hover:text-pink-700 text-sm"
|
||||
>
|
||||
Zurück zur Startseite
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user