xpeditis2.0/apps/frontend/app/booking/reject/[token]/page.tsx
David 890bc189ee
Some checks failed
CI/CD Pipeline - Xpeditis PreProd / Frontend - Build & Test (push) Failing after 5m31s
CI/CD Pipeline - Xpeditis PreProd / Frontend - Docker Build & Push (push) Has been skipped
CI/CD Pipeline - Xpeditis PreProd / Backend - Build & Test (push) Failing after 5m42s
CI/CD Pipeline - Xpeditis PreProd / Backend - Docker Build & Push (push) Has been skipped
CI/CD Pipeline - Xpeditis PreProd / Deploy to PreProd Server (push) Has been skipped
CI/CD Pipeline - Xpeditis PreProd / Run Smoke Tests (push) Has been skipped
fix v0.2
2025-11-12 18:00:33 +01:00

363 lines
14 KiB
TypeScript

/**
* Public Booking Rejection Page
*
* Allows carriers to reject booking requests via email link
* Route: /booking/reject/:token
*/
'use client';
import { useState, useEffect } from 'react';
import { useParams } from 'next/navigation';
import { rejectCsvBooking, type CsvBookingResponse } from '@/lib/api/bookings';
export default function BookingRejectPage() {
const params = useParams();
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 [isRejecting, setIsRejecting] = useState(false);
const [hasRejected, setHasRejected] = useState(false);
const [reason, setReason] = useState('');
const [showReasonField, setShowReasonField] = useState(false);
useEffect(() => {
if (!token) {
setError('Token de refus invalide');
setIsLoading(false);
return;
}
// Just validate the token exists, don't auto-reject
setIsLoading(false);
}, [token]);
const handleReject = async () => {
if (!token) return;
setIsRejecting(true);
setError(null);
try {
const result = await rejectCsvBooking(token, reason || undefined);
setBooking(result);
setHasRejected(true);
} catch (err) {
console.error('Rejection error:', err);
if (err instanceof Error) {
setError(err.message);
} else {
setError('Une erreur est survenue lors du refus');
}
} finally {
setIsRejecting(false);
}
};
if (isLoading) {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 via-white to-gray-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-gray-600 mx-auto mb-4"></div>
<p className="text-gray-600">Chargement...</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 refus
</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à é acceptée ou refusée</li>
<li>Le token 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>
);
}
// After successful rejection
if (hasRejected && booking) {
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-2xl w-full">
{/* Rejection Icon with Animation */}
<div className="text-center mb-8">
<div className="relative inline-block">
<div className="w-20 h-20 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4 animate-scale-in">
<svg
className="w-10 h-10 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>
</div>
<h1 className="text-3xl font-bold text-gray-900 mb-3">
Demande refusée
</h1>
<p className="text-lg text-gray-600 mb-2">
Vous avez refusé 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 demande refusée
</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">
<span className="text-gray-600">Prix proposé</span>
<span className="font-semibold text-gray-900">
{booking.primaryCurrency === 'USD'
? `$${booking.priceUSD.toLocaleString()}`
: `${booking.priceEUR.toLocaleString()}`
}
</span>
</div>
</div>
{reason && (
<div className="mt-4 pt-4 border-t border-gray-200">
<p className="text-sm text-gray-600 mb-1">Raison du refus :</p>
<p className="text-gray-800 bg-white p-3 rounded border border-gray-200">
{reason}
</p>
</div>
)}
</div>
{/* Info Message */}
<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>
Information
</h3>
<p className="text-sm text-blue-800">
Le client pourra soumettre une nouvelle demande avec des conditions différentes si nécessaire.
</p>
</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>
);
}
// Initial rejection form
return (
<div className="min-h-screen bg-gradient-to-br from-orange-50 via-white to-orange-50 flex items-center justify-center p-4">
<div className="bg-white rounded-2xl shadow-xl p-8 max-w-md w-full">
{/* Warning Icon */}
<div className="text-center mb-6">
<div className="w-16 h-16 bg-orange-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg
className="w-8 h-8 text-orange-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
</div>
<h1 className="text-2xl font-bold text-gray-900 mb-2">
Refuser cette demande
</h1>
<p className="text-gray-600">
Vous êtes sur le point de refuser cette demande de transport.
</p>
</div>
{/* Optional Reason Field */}
<div className="mb-6">
{!showReasonField ? (
<button
onClick={() => setShowReasonField(true)}
className="w-full text-left px-4 py-3 bg-gray-50 hover:bg-gray-100 border border-gray-200 rounded-lg transition-colors"
>
<div className="flex items-center justify-between">
<span className="text-gray-700">Ajouter une raison (optionnel)</span>
<svg className="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</div>
</button>
) : (
<div>
<label htmlFor="reason" className="block text-sm font-medium text-gray-700 mb-2">
Raison du refus (optionnel)
</label>
<textarea
id="reason"
rows={4}
value={reason}
onChange={(e) => setReason(e.target.value)}
placeholder="Ex: Prix trop élevé, délais trop courts, itinéraire non disponible..."
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-transparent resize-none"
maxLength={500}
/>
<div className="mt-1 flex items-center justify-between">
<p className="text-xs text-gray-500">
Cette information sera communiquée au client
</p>
<span className="text-xs text-gray-400">
{reason.length}/500
</span>
</div>
</div>
)}
</div>
{/* Warning Message */}
<div className="bg-orange-50 border border-orange-200 rounded-lg p-4 mb-6">
<p className="text-sm text-orange-800">
<strong>Attention :</strong> Cette action est irréversible. Le client sera immédiatement notifié par email de votre refus.
</p>
</div>
{/* Action Buttons */}
<div className="space-y-3">
<button
onClick={handleReject}
disabled={isRejecting}
className="w-full px-6 py-3 bg-red-600 hover:bg-red-700 disabled:bg-red-400 text-white font-semibold rounded-lg transition-colors flex items-center justify-center"
>
{isRejecting ? (
<>
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Refus en cours...
</>
) : (
<>
<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>
Confirmer le refus
</>
)}
</button>
<a
href="mailto:support@xpeditis.com"
className="block w-full px-6 py-3 bg-white hover:bg-gray-50 border border-gray-300 text-gray-700 font-semibold rounded-lg transition-colors text-center"
>
Contacter le support
</a>
</div>
{/* Help Text */}
<p className="mt-6 text-xs text-center text-gray-500">
Si vous avez des questions avant de refuser, contactez-nous par email.
</p>
</div>
</div>
);
}