Some checks failed
CI/CD Pipeline / Discord Notification (Failure) (push) Blocked by required conditions
CI/CD Pipeline / Integration Tests (push) Blocked by required conditions
CI/CD Pipeline / Deployment Summary (push) Blocked by required conditions
CI/CD Pipeline / Deploy to Portainer (push) Blocked by required conditions
CI/CD Pipeline / Discord Notification (Success) (push) Blocked by required conditions
CI/CD Pipeline / Backend - Build, Test & Push (push) Failing after 1m20s
CI/CD Pipeline / Frontend - Build, Test & Push (push) Has been cancelled
298 lines
11 KiB
TypeScript
298 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, useRouter } from 'next/navigation';
|
|
import { acceptCsvBooking, type CsvBookingResponse } from '@/lib/api/bookings';
|
|
|
|
export default function BookingConfirmPage() {
|
|
const params = useParams();
|
|
const router = useRouter();
|
|
const token = params.token as string;
|
|
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [booking, setBooking] = useState<CsvBookingResponse | null>(null);
|
|
const [isAccepting, setIsAccepting] = useState(false);
|
|
|
|
const handleAccept = useCallback(async () => {
|
|
setIsAccepting(true);
|
|
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('Une erreur est survenue lors de l\'acceptation');
|
|
}
|
|
} finally {
|
|
setIsLoading(false);
|
|
setIsAccepting(false);
|
|
}
|
|
}, [token]);
|
|
|
|
useEffect(() => {
|
|
if (!token) {
|
|
setError('Token de confirmation invalide');
|
|
setIsLoading(false);
|
|
return;
|
|
}
|
|
|
|
// Auto-accept the booking
|
|
handleAccept();
|
|
}, [token, handleAccept]);
|
|
|
|
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">Confirmation en cours...</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">
|
|
Erreur de confirmation
|
|
</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>Raisons possibles :</strong>
|
|
</p>
|
|
<ul className="text-sm text-red-700 mt-2 space-y-1 list-disc list-inside">
|
|
<li>Le lien a expiré</li>
|
|
<li>La demande a déjà été acceptée ou refusée</li>
|
|
<li>Le token de confirmation est invalide</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<p className="text-sm text-gray-500 text-center">
|
|
Si vous pensez qu'il s'agit d'une erreur, veuillez contacter le client directement.
|
|
</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>
|
|
{/* Animated rings */}
|
|
<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">
|
|
Demande acceptée !
|
|
</h1>
|
|
<p className="text-lg text-gray-600 mb-2">
|
|
Merci d'avoir accepté cette demande de transport.
|
|
</p>
|
|
<p className="text-gray-500">
|
|
Le client a été notifié par email.
|
|
</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">
|
|
Récapitulatif de la réservation
|
|
</h2>
|
|
|
|
<div className="space-y-3">
|
|
<div className="flex justify-between py-2 border-b border-gray-200">
|
|
<span className="text-gray-600">ID Réservation</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">Trajet</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">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">Poids</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">Palettes</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">Type de conteneur</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">Temps de transit</span>
|
|
<span className="font-semibold text-gray-900">{booking.transitDays} jours</span>
|
|
</div>
|
|
|
|
<div className="flex justify-between py-3">
|
|
<span className="text-gray-600 text-lg">Prix</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">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>
|
|
Prochaines étapes
|
|
</h3>
|
|
<ul className="text-sm text-blue-800 space-y-1 list-disc list-inside">
|
|
<li>Le client va finaliser les détails du conteneur</li>
|
|
<li>Vous recevrez un email avec les documents nécessaires</li>
|
|
<li>Le paiement sera traité selon vos conditions habituelles</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">Documents fournis</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élécharger
|
|
</a>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Contact Info */}
|
|
<div className="text-center text-sm text-gray-500">
|
|
<p>Pour toute question, contactez-nous à</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>
|
|
);
|
|
}
|