xpeditis2.0/apps/frontend/app/[locale]/booking/confirm/[token]/page.tsx
2026-05-12 21:01:52 +02:00

301 lines
11 KiB
TypeScript

/**
* Public Booking Confirmation Page
*
* Allows carriers to accept booking requests via email link
* Route: /booking/confirm/:token
*/
'use client';
import { useState, useEffect, useCallback } from 'react';
import { useParams } from 'next/navigation';
import { useTranslations } from 'next-intl';
import { acceptCsvBooking, type CsvBookingResponse } from '@/lib/api/bookings';
export default function BookingConfirmPage() {
const params = useParams();
const token = params.token as string;
const t = useTranslations('bookingPortal.confirm');
const tCommon = useTranslations('bookingPortal.common');
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [booking, setBooking] = useState<CsvBookingResponse | null>(null);
const handleAccept = useCallback(async () => {
setError(null);
try {
const result = await acceptCsvBooking(token);
setBooking(result);
} catch (err) {
console.error('Acceptance error:', err);
if (err instanceof Error) {
setError(err.message);
} else {
setError(t('errorGeneric'));
}
} finally {
setIsLoading(false);
}
}, [token, t]);
useEffect(() => {
if (!token) {
setError(t('tokenInvalid'));
setIsLoading(false);
return;
}
handleAccept();
}, [token, handleAccept, t]);
if (isLoading) {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-blue-50 flex items-center justify-center p-4">
<div className="bg-white rounded-2xl shadow-xl p-8 max-w-md w-full text-center">
<div className="animate-spin rounded-full h-16 w-16 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-600">{t('loading')}</p>
</div>
</div>
);
}
if (error) {
return (
<div className="min-h-screen bg-gradient-to-br from-red-50 via-white to-red-50 flex items-center justify-center p-4">
<div className="bg-white rounded-2xl shadow-xl p-8 max-w-md w-full">
<div className="text-center mb-6">
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg
className="w-8 h-8 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>
</div>
<h1 className="text-2xl font-bold text-gray-900 mb-2">{t('errorTitle')}</h1>
<p className="text-gray-600">{error}</p>
</div>
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
<p className="text-sm text-red-800">
<strong>{t('errorReasonsTitle')}</strong>
</p>
<ul className="text-sm text-red-700 mt-2 space-y-1 list-disc list-inside">
<li>{t('errorReason1')}</li>
<li>{t('errorReason2')}</li>
<li>{t('errorReason3')}</li>
</ul>
</div>
<p className="text-sm text-gray-500 text-center">{t('errorContact')}</p>
</div>
</div>
);
}
if (!booking) {
return null;
}
return (
<div className="min-h-screen bg-gradient-to-br from-green-50 via-white to-green-50 flex items-center justify-center p-4">
<div className="bg-white rounded-2xl shadow-xl p-8 max-w-2xl w-full">
{/* Success Icon with Animation */}
<div className="text-center mb-8">
<div className="relative inline-block">
<div className="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4 animate-scale-in">
<svg
className="w-10 h-10 text-green-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
</div>
<div className="absolute inset-0 rounded-full border-4 border-green-200 animate-ping opacity-20"></div>
</div>
<h1 className="text-3xl font-bold text-gray-900 mb-3">{t('successTitle')}</h1>
<p className="text-lg text-gray-600 mb-2">{t('successHeadline')}</p>
<p className="text-gray-500">{t('successBody')}</p>
</div>
{/* Booking Summary */}
<div className="bg-gray-50 rounded-xl p-6 mb-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">{t('summaryTitle')}</h2>
<div className="space-y-3">
<div className="flex justify-between py-2 border-b border-gray-200">
<span className="text-gray-600">{t('labels.bookingId')}</span>
<span className="font-semibold text-gray-900">{booking.bookingId}</span>
</div>
<div className="flex justify-between py-2 border-b border-gray-200">
<span className="text-gray-600">{t('labels.route')}</span>
<span className="font-semibold text-gray-900">
{booking.origin} {booking.destination}
</span>
</div>
<div className="flex justify-between py-2 border-b border-gray-200">
<span className="text-gray-600">{t('labels.volume')}</span>
<span className="font-semibold text-gray-900">{booking.volumeCBM} CBM</span>
</div>
<div className="flex justify-between py-2 border-b border-gray-200">
<span className="text-gray-600">{t('labels.weight')}</span>
<span className="font-semibold text-gray-900">{booking.weightKG} kg</span>
</div>
<div className="flex justify-between py-2 border-b border-gray-200">
<span className="text-gray-600">{t('labels.pallets')}</span>
<span className="font-semibold text-gray-900">{booking.palletCount}</span>
</div>
<div className="flex justify-between py-2 border-b border-gray-200">
<span className="text-gray-600">{t('labels.containerType')}</span>
<span className="font-semibold text-gray-900">{booking.containerType}</span>
</div>
<div className="flex justify-between py-2 border-b border-gray-200">
<span className="text-gray-600">{t('labels.transitDays')}</span>
<span className="font-semibold text-gray-900">
{t('transitDaysValue', { count: booking.transitDays })}
</span>
</div>
<div className="flex justify-between py-3">
<span className="text-gray-600 text-lg">{t('labels.price')}</span>
<div className="text-right">
<div className="font-bold text-xl text-green-600">
{booking.primaryCurrency === 'USD'
? `$${booking.priceUSD.toLocaleString()}`
: `${booking.priceEUR.toLocaleString()}`}
</div>
<div className="text-sm text-gray-500">
{booking.primaryCurrency === 'USD'
? `(€${booking.priceEUR.toLocaleString()})`
: `($${booking.priceUSD.toLocaleString()})`}
</div>
</div>
</div>
</div>
{booking.notes && (
<div className="mt-4 pt-4 border-t border-gray-200">
<p className="text-sm text-gray-600 mb-1">{t('labels.notes')}</p>
<p className="text-gray-800">{booking.notes}</p>
</div>
)}
</div>
{/* Next Steps */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
<h3 className="font-semibold text-blue-900 mb-2 flex items-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="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
{t('nextStepsTitle')}
</h3>
<ul className="text-sm text-blue-800 space-y-1 list-disc list-inside">
<li>{t('nextStep1')}</li>
<li>{t('nextStep2')}</li>
<li>{t('nextStep3')}</li>
</ul>
</div>
{/* Documents Section */}
{booking.documents && booking.documents.length > 0 && (
<div className="bg-gray-50 rounded-lg p-4 mb-6">
<h3 className="font-semibold text-gray-900 mb-3">{t('labels.documents')}</h3>
<div className="space-y-2">
{booking.documents.map((doc, index) => (
<div
key={index}
className="flex items-center justify-between p-3 bg-white rounded border border-gray-200"
>
<div className="flex items-center">
<svg
className="w-5 h-5 text-gray-400 mr-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
<div>
<p className="text-sm font-medium text-gray-900">{doc.fileName}</p>
<p className="text-xs text-gray-500">{doc.type}</p>
</div>
</div>
<a
href={doc.url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-700 text-sm font-medium"
>
{t('labels.download')}
</a>
</div>
))}
</div>
</div>
)}
{/* Contact Info */}
<div className="text-center text-sm text-gray-500">
<p>{tCommon('supportPrompt')}</p>
<a href="mailto:support@xpeditis.com" className="text-blue-600 hover:underline">
support@xpeditis.com
</a>
</div>
</div>
<style jsx>{`
@keyframes scale-in {
0% {
transform: scale(0);
opacity: 0;
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
opacity: 1;
}
}
.animate-scale-in {
animation: scale-in 0.5s ease-out;
}
`}</style>
</div>
);
}