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
661 lines
26 KiB
TypeScript
661 lines
26 KiB
TypeScript
/**
|
||
* CSV Booking Creation Page
|
||
*
|
||
* Multi-step form for creating a CSV-based booking request
|
||
*/
|
||
|
||
'use client';
|
||
|
||
import { useState, useEffect, Suspense } from 'react';
|
||
import { useRouter, useSearchParams } from 'next/navigation';
|
||
import type { CsvRateSearchResult } from '@/types/rates';
|
||
import { createCsvBooking } from '@/lib/api/bookings';
|
||
|
||
interface BookingForm {
|
||
// Rate data (pre-filled from search results)
|
||
carrierName: string;
|
||
carrierEmail: string;
|
||
origin: string;
|
||
destination: string;
|
||
volumeCBM: number;
|
||
weightKG: number;
|
||
palletCount: number;
|
||
priceUSD: number;
|
||
priceEUR: number;
|
||
primaryCurrency: 'USD' | 'EUR';
|
||
transitDays: number;
|
||
containerType: string;
|
||
|
||
// Documents to upload
|
||
documents: File[];
|
||
|
||
// Optional notes
|
||
notes?: string;
|
||
}
|
||
|
||
const DOCUMENT_TYPES = [
|
||
{ value: 'BILL_OF_LADING', label: 'Bill of Lading (Connaissement)' },
|
||
{ value: 'PACKING_LIST', label: 'Packing List (Liste de colisage)' },
|
||
{ value: 'COMMERCIAL_INVOICE', label: 'Commercial Invoice (Facture commerciale)' },
|
||
{ value: 'CERTIFICATE_OF_ORIGIN', label: 'Certificate of Origin (Certificat d\'origine)' },
|
||
{ value: 'OTHER', label: 'Autre document' },
|
||
];
|
||
|
||
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
|
||
const ACCEPTED_FILE_TYPES = ['.pdf', '.doc', '.docx', '.jpg', '.jpeg', '.png'];
|
||
|
||
function NewBookingPageContent() {
|
||
const router = useRouter();
|
||
const searchParams = useSearchParams();
|
||
const [currentStep, setCurrentStep] = useState(1);
|
||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
const [formData, setFormData] = useState<BookingForm>({
|
||
carrierName: '',
|
||
carrierEmail: '',
|
||
origin: '',
|
||
destination: '',
|
||
volumeCBM: 0,
|
||
weightKG: 0,
|
||
palletCount: 0,
|
||
priceUSD: 0,
|
||
priceEUR: 0,
|
||
primaryCurrency: 'EUR',
|
||
transitDays: 0,
|
||
containerType: '',
|
||
documents: [],
|
||
notes: '',
|
||
});
|
||
|
||
// Load rate data from URL params
|
||
useEffect(() => {
|
||
const rateDataParam = searchParams.get('rateData');
|
||
if (rateDataParam) {
|
||
try {
|
||
const rateData: CsvRateSearchResult = JSON.parse(decodeURIComponent(rateDataParam));
|
||
setFormData(prev => ({
|
||
...prev,
|
||
carrierName: rateData.companyName,
|
||
carrierEmail: rateData.companyEmail,
|
||
origin: rateData.origin,
|
||
destination: rateData.destination,
|
||
volumeCBM: parseFloat(searchParams.get('volumeCBM') || '0'),
|
||
weightKG: parseFloat(searchParams.get('weightKG') || '0'),
|
||
palletCount: parseInt(searchParams.get('palletCount') || '0'),
|
||
priceUSD: rateData.priceUSD,
|
||
priceEUR: rateData.priceEUR,
|
||
primaryCurrency: rateData.primaryCurrency as 'USD' | 'EUR',
|
||
transitDays: rateData.transitDays,
|
||
containerType: rateData.containerType,
|
||
}));
|
||
} catch (err) {
|
||
console.error('Failed to parse rate data:', err);
|
||
setError('Données de tarif invalides');
|
||
}
|
||
} else {
|
||
// No rate data - redirect back to search
|
||
router.push('/dashboard/search-advanced');
|
||
}
|
||
}, [searchParams, router]);
|
||
|
||
const handleFileChange = (files: FileList | null) => {
|
||
if (!files) return;
|
||
|
||
const newFiles: File[] = [];
|
||
const errors: string[] = [];
|
||
|
||
Array.from(files).forEach(file => {
|
||
// Check file size
|
||
if (file.size > MAX_FILE_SIZE) {
|
||
errors.push(`${file.name}: Fichier trop volumineux (max 5MB)`);
|
||
return;
|
||
}
|
||
|
||
// Check file type
|
||
const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase();
|
||
if (!ACCEPTED_FILE_TYPES.includes(fileExtension)) {
|
||
errors.push(`${file.name}: Type de fichier non accepté`);
|
||
return;
|
||
}
|
||
|
||
newFiles.push(file);
|
||
});
|
||
|
||
if (errors.length > 0) {
|
||
setError(errors.join('\n'));
|
||
} else {
|
||
setError(null);
|
||
}
|
||
|
||
setFormData(prev => ({
|
||
...prev,
|
||
documents: [...prev.documents, ...newFiles],
|
||
}));
|
||
};
|
||
|
||
const removeDocument = (index: number) => {
|
||
setFormData(prev => ({
|
||
...prev,
|
||
documents: prev.documents.filter((_, i) => i !== index),
|
||
}));
|
||
};
|
||
|
||
const handleSubmit = async () => {
|
||
setIsSubmitting(true);
|
||
setError(null);
|
||
|
||
try {
|
||
// Create FormData for multipart upload
|
||
const formDataToSend = new FormData();
|
||
|
||
// Append all booking data
|
||
formDataToSend.append('carrierName', formData.carrierName);
|
||
formDataToSend.append('carrierEmail', formData.carrierEmail);
|
||
formDataToSend.append('origin', formData.origin);
|
||
formDataToSend.append('destination', formData.destination);
|
||
formDataToSend.append('volumeCBM', formData.volumeCBM.toString());
|
||
formDataToSend.append('weightKG', formData.weightKG.toString());
|
||
formDataToSend.append('palletCount', formData.palletCount.toString());
|
||
formDataToSend.append('priceUSD', formData.priceUSD.toString());
|
||
formDataToSend.append('priceEUR', formData.priceEUR.toString());
|
||
formDataToSend.append('primaryCurrency', formData.primaryCurrency);
|
||
formDataToSend.append('transitDays', formData.transitDays.toString());
|
||
formDataToSend.append('containerType', formData.containerType);
|
||
|
||
if (formData.notes) {
|
||
formDataToSend.append('notes', formData.notes);
|
||
}
|
||
|
||
// Append documents
|
||
formData.documents.forEach((file) => {
|
||
formDataToSend.append('documents', file);
|
||
});
|
||
|
||
// Send to API using client function
|
||
const result = await createCsvBooking(formDataToSend);
|
||
|
||
// Redirect to success page
|
||
router.push(`/dashboard/bookings?success=true&id=${result.id}`);
|
||
} catch (err) {
|
||
console.error('Booking creation error:', err);
|
||
setError(err instanceof Error ? err.message : 'Une erreur est survenue');
|
||
} finally {
|
||
setIsSubmitting(false);
|
||
}
|
||
};
|
||
|
||
const canProceedToStep2 = formData.carrierName && formData.origin && formData.destination;
|
||
const canProceedToStep3 = formData.documents.length >= 1;
|
||
const canSubmit = canProceedToStep3;
|
||
|
||
const formatPrice = (price: number, currency: string) => {
|
||
return new Intl.NumberFormat('fr-FR', {
|
||
style: 'currency',
|
||
currency: currency,
|
||
}).format(price);
|
||
};
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 py-12 px-4">
|
||
<div className="max-w-4xl mx-auto">
|
||
{/* Header */}
|
||
<div className="mb-8">
|
||
<button
|
||
onClick={() => router.back()}
|
||
className="mb-4 flex items-center text-blue-600 hover:text-blue-800 font-medium"
|
||
>
|
||
← Retour aux résultats
|
||
</button>
|
||
|
||
<div className="bg-white rounded-lg shadow-md p-6">
|
||
<h1 className="text-3xl font-bold text-gray-900 mb-2">Nouvelle demande de réservation</h1>
|
||
<p className="text-gray-600">
|
||
Envoyez une demande de réservation directement au transporteur
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Progress Steps */}
|
||
<div className="mb-8">
|
||
<div className="flex items-center justify-between">
|
||
{[1, 2, 3].map(step => (
|
||
<div key={step} className="flex items-center flex-1">
|
||
<div className="flex items-center">
|
||
<div
|
||
className={`w-10 h-10 rounded-full flex items-center justify-center font-semibold ${
|
||
currentStep >= step
|
||
? 'bg-blue-600 text-white'
|
||
: 'bg-gray-200 text-gray-600'
|
||
}`}
|
||
>
|
||
{step}
|
||
</div>
|
||
<div className="ml-3">
|
||
<p
|
||
className={`text-sm font-medium ${
|
||
currentStep >= step ? 'text-blue-600' : 'text-gray-500'
|
||
}`}
|
||
>
|
||
{step === 1 && 'Détails'}
|
||
{step === 2 && 'Documents'}
|
||
{step === 3 && 'Révision'}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
{step < 3 && (
|
||
<div
|
||
className={`flex-1 h-1 mx-4 ${
|
||
currentStep > step ? 'bg-blue-600' : 'bg-gray-200'
|
||
}`}
|
||
/>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Error Message */}
|
||
{error && (
|
||
<div className="mb-6 bg-red-50 border-2 border-red-200 rounded-lg p-4">
|
||
<div className="flex items-start">
|
||
<span className="text-2xl mr-3">⚠️</span>
|
||
<div>
|
||
<h4 className="font-semibold text-red-900 mb-1">Erreur</h4>
|
||
<p className="text-red-700 whitespace-pre-line">{error}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Step Content */}
|
||
<div className="bg-white rounded-lg shadow-lg p-8">
|
||
{/* Step 1: Transport Details (Read-only) */}
|
||
{currentStep === 1 && (
|
||
<div>
|
||
<h2 className="text-2xl font-bold text-gray-900 mb-6">
|
||
Détails du transport
|
||
</h2>
|
||
|
||
<div className="space-y-6">
|
||
{/* Carrier Info */}
|
||
<div className="bg-blue-50 border-2 border-blue-200 rounded-lg p-6">
|
||
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||
Transporteur
|
||
</h3>
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<p className="text-sm text-gray-600">Nom</p>
|
||
<p className="font-semibold text-gray-900">{formData.carrierName}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-sm text-gray-600">Email</p>
|
||
<p className="font-semibold text-gray-900">{formData.carrierEmail}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Route Info */}
|
||
<div className="bg-gray-50 rounded-lg p-6">
|
||
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||
Trajet
|
||
</h3>
|
||
<div className="flex items-center justify-between">
|
||
<div className="text-center">
|
||
<p className="text-sm text-gray-600 mb-1">Origine</p>
|
||
<p className="text-xl font-bold text-blue-600">{formData.origin}</p>
|
||
</div>
|
||
<div className="flex-1 mx-8">
|
||
<div className="border-t-2 border-dashed border-gray-300 relative">
|
||
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-white px-3">
|
||
<span className="text-sm text-gray-600">{formData.transitDays} jours</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="text-center">
|
||
<p className="text-sm text-gray-600 mb-1">Destination</p>
|
||
<p className="text-xl font-bold text-blue-600">{formData.destination}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Shipment Details */}
|
||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||
<div className="bg-gray-50 rounded-lg p-4">
|
||
<p className="text-xs text-gray-600 mb-1">Volume</p>
|
||
<p className="text-lg font-bold text-gray-900">{formData.volumeCBM} CBM</p>
|
||
</div>
|
||
<div className="bg-gray-50 rounded-lg p-4">
|
||
<p className="text-xs text-gray-600 mb-1">Poids</p>
|
||
<p className="text-lg font-bold text-gray-900">{formData.weightKG} kg</p>
|
||
</div>
|
||
<div className="bg-gray-50 rounded-lg p-4">
|
||
<p className="text-xs text-gray-600 mb-1">Palettes</p>
|
||
<p className="text-lg font-bold text-gray-900">{formData.palletCount || 'N/A'}</p>
|
||
</div>
|
||
<div className="bg-gray-50 rounded-lg p-4">
|
||
<p className="text-xs text-gray-600 mb-1">Type</p>
|
||
<p className="text-lg font-bold text-gray-900">{formData.containerType}</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Pricing */}
|
||
<div className="bg-green-50 border-2 border-green-200 rounded-lg p-6">
|
||
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||
Prix estimé
|
||
</h3>
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm text-gray-600">Prix en EUR</p>
|
||
<p className="text-3xl font-bold text-green-600">
|
||
{formatPrice(formData.priceEUR, 'EUR')}
|
||
</p>
|
||
</div>
|
||
{formData.priceUSD > 0 && (
|
||
<div className="text-right">
|
||
<p className="text-sm text-gray-600">Prix en USD</p>
|
||
<p className="text-xl font-semibold text-gray-700">
|
||
{formatPrice(formData.priceUSD, 'USD')}
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mt-8 flex justify-end">
|
||
<button
|
||
onClick={() => setCurrentStep(2)}
|
||
disabled={!canProceedToStep2}
|
||
className="px-8 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed font-semibold"
|
||
>
|
||
Continuer →
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Step 2: Document Upload */}
|
||
{currentStep === 2 && (
|
||
<div>
|
||
<h2 className="text-2xl font-bold text-gray-900 mb-6">
|
||
Documents requis
|
||
</h2>
|
||
|
||
<div className="mb-6 bg-yellow-50 border-2 border-yellow-200 rounded-lg p-4">
|
||
<div className="flex items-start">
|
||
<span className="text-2xl mr-3">📋</span>
|
||
<div>
|
||
<h4 className="font-semibold text-yellow-900 mb-1">Information importante</h4>
|
||
<p className="text-sm text-yellow-800">
|
||
Veuillez télécharger au moins <strong>1 document</strong> pour continuer.
|
||
Formats acceptés: PDF, DOC, DOCX, JPG, PNG (max 5MB par fichier)
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* File Upload */}
|
||
<div className="space-y-4 mb-8">
|
||
<div className="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center hover:border-blue-400 transition-colors">
|
||
<input
|
||
type="file"
|
||
id="file-upload"
|
||
multiple
|
||
accept={ACCEPTED_FILE_TYPES.join(',')}
|
||
onChange={(e) => handleFileChange(e.target.files)}
|
||
className="hidden"
|
||
/>
|
||
<label
|
||
htmlFor="file-upload"
|
||
className="cursor-pointer flex flex-col items-center"
|
||
>
|
||
<svg
|
||
className="w-16 h-16 text-gray-400 mb-4"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<path
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
strokeWidth={2}
|
||
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
|
||
/>
|
||
</svg>
|
||
<p className="text-lg font-semibold text-gray-700 mb-2">
|
||
Cliquez pour sélectionner des fichiers
|
||
</p>
|
||
<p className="text-sm text-gray-500">
|
||
ou glissez-déposez vos documents ici
|
||
</p>
|
||
</label>
|
||
</div>
|
||
|
||
{/* Document Type Suggestions */}
|
||
<div className="bg-gray-50 rounded-lg p-4">
|
||
<p className="text-sm font-semibold text-gray-700 mb-2">
|
||
Documents recommandés :
|
||
</p>
|
||
<ul className="text-sm text-gray-600 space-y-1">
|
||
{DOCUMENT_TYPES.map(type => (
|
||
<li key={type.value}>• {type.label}</li>
|
||
))}
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Uploaded Files List */}
|
||
{formData.documents.length > 0 && (
|
||
<div className="mb-8">
|
||
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||
Documents sélectionnés ({formData.documents.length})
|
||
</h3>
|
||
<div className="space-y-2">
|
||
{formData.documents.map((file, index) => (
|
||
<div
|
||
key={index}
|
||
className="flex items-center justify-between bg-gray-50 rounded-lg p-4 border border-gray-200"
|
||
>
|
||
<div className="flex items-center space-x-3">
|
||
<svg
|
||
className="w-6 h-6 text-blue-600"
|
||
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="font-medium text-gray-900">{file.name}</p>
|
||
<p className="text-xs text-gray-500">
|
||
{(file.size / 1024 / 1024).toFixed(2)} MB
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<button
|
||
onClick={() => removeDocument(index)}
|
||
className="text-red-600 hover:text-red-800 font-medium text-sm"
|
||
>
|
||
Supprimer
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Optional Notes */}
|
||
<div className="mb-8">
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
Notes ou instructions spéciales (optionnel)
|
||
</label>
|
||
<textarea
|
||
value={formData.notes || ''}
|
||
onChange={(e) => setFormData(prev => ({ ...prev, notes: e.target.value }))}
|
||
rows={4}
|
||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||
placeholder="Ajoutez des instructions spéciales pour le transporteur..."
|
||
/>
|
||
</div>
|
||
|
||
<div className="flex justify-between">
|
||
<button
|
||
onClick={() => setCurrentStep(1)}
|
||
className="px-8 py-3 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 font-semibold"
|
||
>
|
||
← Retour
|
||
</button>
|
||
<button
|
||
onClick={() => setCurrentStep(3)}
|
||
disabled={!canProceedToStep3}
|
||
className="px-8 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed font-semibold"
|
||
>
|
||
Continuer →
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Step 3: Review & Submit */}
|
||
{currentStep === 3 && (
|
||
<div>
|
||
<h2 className="text-2xl font-bold text-gray-900 mb-6">
|
||
Révision et envoi
|
||
</h2>
|
||
|
||
<div className="space-y-6 mb-8">
|
||
{/* Summary */}
|
||
<div className="bg-blue-50 border-2 border-blue-200 rounded-lg p-6">
|
||
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||
Récapitulatif
|
||
</h3>
|
||
<div className="space-y-3">
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-600">Transporteur :</span>
|
||
<span className="font-semibold text-gray-900">{formData.carrierName}</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-600">Trajet :</span>
|
||
<span className="font-semibold text-gray-900">
|
||
{formData.origin} → {formData.destination}
|
||
</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-600">Volume :</span>
|
||
<span className="font-semibold text-gray-900">{formData.volumeCBM} CBM</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-600">Poids :</span>
|
||
<span className="font-semibold text-gray-900">{formData.weightKG} kg</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-600">Documents :</span>
|
||
<span className="font-semibold text-gray-900">
|
||
{formData.documents.length} fichier(s)
|
||
</span>
|
||
</div>
|
||
<div className="flex justify-between border-t pt-3 mt-3">
|
||
<span className="text-gray-900 font-semibold">Prix total :</span>
|
||
<span className="text-2xl font-bold text-green-600">
|
||
{formatPrice(formData.priceEUR, 'EUR')}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* What happens next */}
|
||
<div className="bg-yellow-50 border-2 border-yellow-200 rounded-lg p-6">
|
||
<h3 className="text-lg font-semibold text-gray-900 mb-3">
|
||
📧 Que se passe-t-il ensuite ?
|
||
</h3>
|
||
<ul className="space-y-2 text-sm text-gray-700">
|
||
<li className="flex items-start">
|
||
<span className="mr-2">1.</span>
|
||
<span>
|
||
Votre demande sera <strong>envoyée par email</strong> au transporteur ({formData.carrierEmail})
|
||
</span>
|
||
</li>
|
||
<li className="flex items-start">
|
||
<span className="mr-2">2.</span>
|
||
<span>
|
||
Le transporteur recevra tous vos documents et détails de transport
|
||
</span>
|
||
</li>
|
||
<li className="flex items-start">
|
||
<span className="mr-2">3.</span>
|
||
<span>
|
||
Il pourra <strong>accepter ou refuser</strong> la demande directement depuis son email
|
||
</span>
|
||
</li>
|
||
<li className="flex items-start">
|
||
<span className="mr-2">4.</span>
|
||
<span>
|
||
Vous recevrez une <strong>notification</strong> dès que le transporteur répond (sous 7 jours maximum)
|
||
</span>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
|
||
{/* Terms */}
|
||
<div className="bg-gray-50 rounded-lg p-4">
|
||
<label className="flex items-start cursor-pointer">
|
||
<input
|
||
type="checkbox"
|
||
className="mt-1 h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||
required
|
||
/>
|
||
<span className="ml-3 text-sm text-gray-700">
|
||
Je confirme que les informations fournies sont exactes et que j'accepte les{' '}
|
||
<a href="#" className="text-blue-600 hover:underline">
|
||
conditions générales
|
||
</a>
|
||
</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex justify-between">
|
||
<button
|
||
onClick={() => setCurrentStep(2)}
|
||
disabled={isSubmitting}
|
||
className="px-8 py-3 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 font-semibold disabled:opacity-50"
|
||
>
|
||
← Retour
|
||
</button>
|
||
<button
|
||
onClick={handleSubmit}
|
||
disabled={!canSubmit || isSubmitting}
|
||
className="px-8 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:bg-gray-400 disabled:cursor-not-allowed font-semibold"
|
||
>
|
||
{isSubmitting ? (
|
||
<span className="flex items-center">
|
||
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2"></div>
|
||
Envoi en cours...
|
||
</span>
|
||
) : (
|
||
'✓ Envoyer la demande'
|
||
)}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default function NewBookingPage() {
|
||
return (
|
||
<Suspense fallback={<div className="min-h-screen flex items-center justify-center">Chargement...</div>}>
|
||
<NewBookingPageContent />
|
||
</Suspense>
|
||
);
|
||
}
|