223 lines
8.1 KiB
TypeScript
223 lines
8.1 KiB
TypeScript
'use client';
|
||
|
||
import { useEffect, useState, useRef } from 'react';
|
||
import { useParams, useRouter } from 'next/navigation';
|
||
import { XCircle, Loader2, CheckCircle, Truck, MessageSquare } from 'lucide-react';
|
||
|
||
export default function CarrierRejectPage() {
|
||
const params = useParams();
|
||
const router = useRouter();
|
||
const token = params.token as string;
|
||
|
||
const [loading, setLoading] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [bookingId, setBookingId] = useState<string | null>(null);
|
||
const [isNewAccount, setIsNewAccount] = useState(false);
|
||
const [showSuccess, setShowSuccess] = useState(false);
|
||
const [reason, setReason] = useState('');
|
||
|
||
// Prevent double API calls (React 18 StrictMode issue)
|
||
const hasCalledApi = useRef(false);
|
||
|
||
const handleReject = async () => {
|
||
// Protection contre les doubles appels
|
||
if (hasCalledApi.current) {
|
||
return;
|
||
}
|
||
hasCalledApi.current = true;
|
||
if (!token) {
|
||
setError('Token manquant');
|
||
return;
|
||
}
|
||
|
||
setLoading(true);
|
||
setError(null);
|
||
|
||
try {
|
||
// Construire l'URL avec la raison en query param si fournie
|
||
const url = new URL(`http://localhost:4000/api/v1/csv-booking-actions/reject/${token}`);
|
||
if (reason.trim()) {
|
||
url.searchParams.append('reason', reason.trim());
|
||
}
|
||
|
||
const response = await fetch(url.toString(), {
|
||
method: 'GET',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorData = await response.json();
|
||
|
||
// Messages d'erreur personnalisés
|
||
let errorMessage = errorData.message || 'Erreur lors du refus du booking';
|
||
|
||
if (errorMessage.includes('status REJECTED')) {
|
||
errorMessage = 'Ce booking a déjà été refusé. Vous ne pouvez pas le refuser à nouveau.';
|
||
} else if (errorMessage.includes('status ACCEPTED')) {
|
||
errorMessage = 'Ce booking a déjà été accepté. Vous ne pouvez plus le refuser.';
|
||
} else if (errorMessage.includes('not found')) {
|
||
errorMessage = 'Booking introuvable. Le lien peut avoir expiré.';
|
||
}
|
||
|
||
throw new Error(errorMessage);
|
||
}
|
||
|
||
const data = await response.json();
|
||
|
||
// Stocker le token JWT pour l'auto-login
|
||
if (data.autoLoginToken) {
|
||
localStorage.setItem('carrier_access_token', data.autoLoginToken);
|
||
}
|
||
|
||
setBookingId(data.bookingId);
|
||
setIsNewAccount(data.isNewAccount);
|
||
setShowSuccess(true);
|
||
setLoading(false);
|
||
|
||
// Redirection manuelle - plus de redirection automatique
|
||
} catch (err) {
|
||
console.error('Error rejecting booking:', err);
|
||
setError(err instanceof Error ? err.message : 'Erreur lors du refus');
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-red-50 to-orange-50">
|
||
<div className="bg-white p-8 rounded-lg shadow-lg max-w-md w-full text-center">
|
||
<Loader2 className="w-16 h-16 text-red-600 mx-auto mb-4 animate-spin" />
|
||
<h1 className="text-2xl font-bold text-gray-900 mb-4">
|
||
Traitement en cours...
|
||
</h1>
|
||
<p className="text-gray-600">
|
||
Nous traitons votre refus de la réservation.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (showSuccess) {
|
||
return (
|
||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-red-50 to-orange-50">
|
||
<div className="bg-white p-8 rounded-lg shadow-lg max-w-md w-full text-center">
|
||
<CheckCircle className="w-16 h-16 text-orange-500 mx-auto mb-4" />
|
||
<h1 className="text-3xl font-bold text-gray-900 mb-4">
|
||
Réservation Refusée
|
||
</h1>
|
||
|
||
<div className="bg-orange-50 border-2 border-orange-200 rounded-lg p-4 mb-6">
|
||
<p className="text-orange-800 font-medium">
|
||
❌ La réservation a été refusée
|
||
</p>
|
||
{reason && (
|
||
<p className="text-sm text-orange-700 mt-2">
|
||
Raison : {reason}
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
{isNewAccount && (
|
||
<div className="bg-blue-50 border-2 border-blue-200 rounded-lg p-4 mb-6">
|
||
<p className="text-blue-800 font-medium mb-2">
|
||
🎉 Compte transporteur créé !
|
||
</p>
|
||
<p className="text-sm text-blue-700">
|
||
Un email avec vos identifiants de connexion vous a été envoyé.
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
<button
|
||
onClick={() => router.push(`/carrier/dashboard/bookings/${bookingId}`)}
|
||
className="w-full px-6 py-3 bg-orange-600 text-white rounded-lg hover:bg-orange-700 font-semibold transition-colors flex items-center justify-center"
|
||
>
|
||
<Truck className="w-5 h-5 mr-2" />
|
||
Voir les détails de la réservation
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (error) {
|
||
return (
|
||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-red-50 to-orange-50">
|
||
<div className="bg-white p-8 rounded-lg shadow-lg max-w-md w-full text-center">
|
||
<XCircle className="w-16 h-16 text-red-500 mx-auto mb-4" />
|
||
<h1 className="text-2xl font-bold text-gray-900 mb-4">Erreur</h1>
|
||
<p className="text-gray-600 mb-6">{error}</p>
|
||
<button
|
||
onClick={() => router.push('/carrier/login')}
|
||
className="w-full px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700"
|
||
>
|
||
Retour à la connexion
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// Formulaire de refus
|
||
return (
|
||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-red-50 to-orange-50 p-4">
|
||
<div className="bg-white p-8 rounded-lg shadow-lg max-w-md w-full">
|
||
<div className="text-center mb-6">
|
||
<XCircle className="w-16 h-16 text-red-500 mx-auto mb-4" />
|
||
<h1 className="text-3xl font-bold text-gray-900 mb-2">
|
||
Refuser la Réservation
|
||
</h1>
|
||
<p className="text-gray-600">
|
||
Vous êtes sur le point de refuser cette demande de réservation.
|
||
</p>
|
||
</div>
|
||
|
||
<div className="mb-6">
|
||
<label className="flex items-center text-sm font-medium text-gray-700 mb-2">
|
||
<MessageSquare className="w-4 h-4 mr-2" />
|
||
Raison du refus (optionnel)
|
||
</label>
|
||
<textarea
|
||
value={reason}
|
||
onChange={(e) => setReason(e.target.value)}
|
||
placeholder="Ex: Capacité insuffisante, date non disponible..."
|
||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent resize-none"
|
||
rows={4}
|
||
maxLength={500}
|
||
/>
|
||
<p className="text-xs text-gray-500 mt-1">
|
||
{reason.length}/500 caractères
|
||
</p>
|
||
</div>
|
||
|
||
<div className="space-y-3">
|
||
<button
|
||
onClick={handleReject}
|
||
disabled={loading}
|
||
className="w-full px-6 py-3 bg-red-600 text-white rounded-lg hover:bg-red-700 font-semibold disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||
>
|
||
Confirmer le Refus
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => router.back()}
|
||
disabled={loading}
|
||
className="w-full px-6 py-3 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 font-semibold disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||
>
|
||
Annuler
|
||
</button>
|
||
</div>
|
||
|
||
<div className="mt-6 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||
<p className="text-sm text-yellow-800">
|
||
⚠️ Le client sera notifié de votre refus{reason.trim() ? ' avec la raison fournie' : ''}.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|