xpeditis2.0/apps/frontend/app/dashboard/booking/[id]/pay/page.tsx
2026-03-19 19:04:31 +01:00

438 lines
17 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Commission Payment Page
*
* 2-column layout:
* - Left: payment method selector + action
* - Right: booking summary
*/
'use client';
import { useState, useEffect } from 'react';
import { useRouter, useParams } from 'next/navigation';
import {
CreditCard,
Building2,
ArrowLeft,
Loader2,
AlertTriangle,
CheckCircle,
Copy,
Clock,
} from 'lucide-react';
import { getCsvBooking, payBookingCommission, declareBankTransfer } from '@/lib/api/bookings';
interface BookingData {
id: string;
bookingNumber?: string;
carrierName: string;
carrierEmail: string;
origin: string;
destination: string;
volumeCBM: number;
weightKG: number;
palletCount: number;
priceEUR: number;
priceUSD: number;
primaryCurrency: string;
transitDays: number;
containerType: string;
status: string;
commissionRate?: number;
commissionAmountEur?: number;
}
type PaymentMethod = 'card' | 'transfer' | null;
const BANK_DETAILS = {
beneficiary: 'XPEDITIS SAS',
iban: 'FR76 XXXX XXXX XXXX XXXX XXXX XXX',
bic: 'XXXXXXXX',
};
export default function PayCommissionPage() {
const router = useRouter();
const params = useParams();
const bookingId = params.id as string;
const [booking, setBooking] = useState<BookingData | null>(null);
const [loading, setLoading] = useState(true);
const [paying, setPaying] = useState(false);
const [declaring, setDeclaring] = useState(false);
const [error, setError] = useState<string | null>(null);
const [selectedMethod, setSelectedMethod] = useState<PaymentMethod>(null);
const [copied, setCopied] = useState<string | null>(null);
useEffect(() => {
async function fetchBooking() {
try {
const data = await getCsvBooking(bookingId);
setBooking(data as any);
if (data.status !== 'PENDING_PAYMENT') {
router.replace('/dashboard/bookings');
}
} catch (err) {
setError('Impossible de charger les détails du booking');
} finally {
setLoading(false);
}
}
if (bookingId) fetchBooking();
}, [bookingId, router]);
const handlePayByCard = async () => {
setPaying(true);
setError(null);
try {
const result = await payBookingCommission(bookingId);
window.location.href = result.sessionUrl;
} catch (err) {
setError(err instanceof Error ? err.message : 'Erreur lors de la création du paiement');
setPaying(false);
}
};
const handleDeclareTransfer = async () => {
setDeclaring(true);
setError(null);
try {
await declareBankTransfer(bookingId);
router.push('/dashboard/bookings?transfer=declared');
} catch (err) {
setError(err instanceof Error ? err.message : 'Erreur lors de la déclaration du virement');
setDeclaring(false);
}
};
const copyToClipboard = (value: string, key: string) => {
navigator.clipboard.writeText(value);
setCopied(key);
setTimeout(() => setCopied(null), 2000);
};
const formatPrice = (price: number, currency: string) =>
new Intl.NumberFormat('fr-FR', { style: 'currency', currency }).format(price);
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 to-blue-50">
<div className="flex items-center space-x-3">
<Loader2 className="h-6 w-6 animate-spin text-blue-600" />
<span className="text-gray-600">Chargement...</span>
</div>
</div>
);
}
if (error && !booking) {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 to-blue-50">
<div className="bg-white rounded-xl shadow-md p-8 max-w-md">
<AlertTriangle className="h-12 w-12 text-red-500 mx-auto mb-4" />
<p className="text-center text-gray-700">{error}</p>
<button
onClick={() => router.push('/dashboard/bookings')}
className="mt-4 w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Retour aux bookings
</button>
</div>
</div>
);
}
if (!booking) return null;
const commissionAmount = booking.commissionAmountEur || 0;
const commissionRate = booking.commissionRate || 0;
const reference = booking.bookingNumber || booking.id.slice(0, 8).toUpperCase();
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-blue-50 py-10 px-4">
<div className="max-w-5xl mx-auto">
{/* Back button */}
<button
onClick={() => router.push('/dashboard/bookings')}
className="mb-6 flex items-center text-blue-600 hover:text-blue-800 font-medium"
>
<ArrowLeft className="h-4 w-4 mr-2" />
Retour aux bookings
</button>
<h1 className="text-2xl font-bold text-gray-900 mb-1">Paiement de la commission</h1>
<p className="text-gray-500 mb-8">
Finalisez votre booking en réglant la commission de service
</p>
{error && (
<div className="mb-6 bg-red-50 border border-red-200 rounded-lg p-4 flex items-start space-x-3">
<AlertTriangle className="h-5 w-5 text-red-500 flex-shrink-0 mt-0.5" />
<p className="text-red-700 text-sm">{error}</p>
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6">
{/* LEFT — Payment method selector */}
<div className="lg:col-span-3 space-y-4">
<h2 className="text-base font-semibold text-gray-700 uppercase tracking-wide">
Choisir le mode de paiement
</h2>
{/* Card option */}
<button
onClick={() => setSelectedMethod('card')}
className={`w-full text-left rounded-xl border-2 p-5 transition-all ${
selectedMethod === 'card'
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 bg-white hover:border-gray-300'
}`}
>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div
className={`w-10 h-10 rounded-full flex items-center justify-center ${
selectedMethod === 'card' ? 'bg-blue-100' : 'bg-gray-100'
}`}
>
<CreditCard
className={`h-5 w-5 ${
selectedMethod === 'card' ? 'text-blue-600' : 'text-gray-500'
}`}
/>
</div>
<div>
<p className="font-semibold text-gray-900">Carte bancaire</p>
<p className="text-sm text-gray-500">Paiement immédiat via Stripe</p>
</div>
</div>
<div
className={`w-5 h-5 rounded-full border-2 flex items-center justify-center ${
selectedMethod === 'card' ? 'border-blue-500 bg-blue-500' : 'border-gray-300'
}`}
>
{selectedMethod === 'card' && (
<div className="w-2 h-2 rounded-full bg-white" />
)}
</div>
</div>
</button>
{/* Transfer option */}
<button
onClick={() => setSelectedMethod('transfer')}
className={`w-full text-left rounded-xl border-2 p-5 transition-all ${
selectedMethod === 'transfer'
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 bg-white hover:border-gray-300'
}`}
>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div
className={`w-10 h-10 rounded-full flex items-center justify-center ${
selectedMethod === 'transfer' ? 'bg-blue-100' : 'bg-gray-100'
}`}
>
<Building2
className={`h-5 w-5 ${
selectedMethod === 'transfer' ? 'text-blue-600' : 'text-gray-500'
}`}
/>
</div>
<div>
<p className="font-semibold text-gray-900">Virement bancaire</p>
<p className="text-sm text-gray-500">Validation sous 13 jours ouvrables</p>
</div>
</div>
<div
className={`w-5 h-5 rounded-full border-2 flex items-center justify-center ${
selectedMethod === 'transfer'
? 'border-blue-500 bg-blue-500'
: 'border-gray-300'
}`}
>
{selectedMethod === 'transfer' && (
<div className="w-2 h-2 rounded-full bg-white" />
)}
</div>
</div>
</button>
{/* Card action */}
{selectedMethod === 'card' && (
<div className="bg-white rounded-xl border border-gray-200 p-5">
<p className="text-sm text-gray-600 mb-4">
Vous serez redirigé vers Stripe pour finaliser votre paiement en toute sécurité.
</p>
<button
onClick={handlePayByCard}
disabled={paying}
className="w-full py-3 bg-blue-600 text-white rounded-lg font-semibold hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed flex items-center justify-center space-x-2 transition-colors"
>
{paying ? (
<>
<Loader2 className="h-5 w-5 animate-spin" />
<span>Redirection vers Stripe...</span>
</>
) : (
<>
<CreditCard className="h-5 w-5" />
<span>Payer {formatPrice(commissionAmount, 'EUR')} par carte</span>
</>
)}
</button>
</div>
)}
{/* Transfer action */}
{selectedMethod === 'transfer' && (
<div className="bg-white rounded-xl border border-gray-200 p-5 space-y-4">
<p className="text-sm text-gray-600">
Effectuez le virement avec les coordonnées ci-dessous, puis cliquez sur
&ldquo;J&apos;ai effectué le virement&rdquo;.
</p>
{/* Bank details */}
<div className="bg-gray-50 rounded-lg divide-y divide-gray-200 text-sm">
{[
{ label: 'Bénéficiaire', value: BANK_DETAILS.beneficiary, key: 'beneficiary' },
{ label: 'IBAN', value: BANK_DETAILS.iban, key: 'iban', mono: true },
{ label: 'BIC / SWIFT', value: BANK_DETAILS.bic, key: 'bic', mono: true },
{
label: 'Montant',
value: formatPrice(commissionAmount, 'EUR'),
key: 'amount',
bold: true,
},
{ label: 'Référence', value: reference, key: 'ref', mono: true },
].map(({ label, value, key, mono, bold }) => (
<div key={key} className="flex items-center justify-between px-4 py-3">
<span className="text-gray-500">{label}</span>
<div className="flex items-center space-x-2">
<span
className={`${mono ? 'font-mono' : ''} ${bold ? 'font-bold text-gray-900' : 'text-gray-800'}`}
>
{value}
</span>
{key !== 'amount' && (
<button
onClick={() => copyToClipboard(value, key)}
className="text-gray-400 hover:text-blue-600 transition-colors"
title="Copier"
>
{copied === key ? (
<CheckCircle className="h-4 w-4 text-green-500" />
) : (
<Copy className="h-4 w-4" />
)}
</button>
)}
</div>
</div>
))}
</div>
<div className="flex items-start space-x-2 text-xs text-amber-700 bg-amber-50 rounded-lg p-3">
<Clock className="h-4 w-4 flex-shrink-0 mt-0.5" />
<span>
Mentionnez impérativement la référence <strong>{reference}</strong> dans le
libellé du virement.
</span>
</div>
<button
onClick={handleDeclareTransfer}
disabled={declaring}
className="w-full py-3 bg-green-600 text-white rounded-lg font-semibold hover:bg-green-700 disabled:bg-gray-400 disabled:cursor-not-allowed flex items-center justify-center space-x-2 transition-colors"
>
{declaring ? (
<>
<Loader2 className="h-5 w-5 animate-spin" />
<span>Enregistrement...</span>
</>
) : (
<>
<CheckCircle className="h-5 w-5" />
<span>J&apos;ai effectué le virement</span>
</>
)}
</button>
</div>
)}
{/* Placeholder when no method selected */}
{selectedMethod === null && (
<div className="bg-white rounded-xl border-2 border-dashed border-gray-200 p-6 text-center text-gray-400 text-sm">
Sélectionnez un mode de paiement ci-dessus
</div>
)}
</div>
{/* RIGHT — Booking summary */}
<div className="lg:col-span-2 space-y-4">
<h2 className="text-base font-semibold text-gray-700 uppercase tracking-wide">
Récapitulatif
</h2>
<div className="bg-white rounded-xl border border-gray-200 p-5 space-y-4">
{booking.bookingNumber && (
<div className="flex justify-between text-sm">
<span className="text-gray-500">Numéro</span>
<span className="font-semibold text-gray-900">{booking.bookingNumber}</span>
</div>
)}
<div className="flex justify-between text-sm">
<span className="text-gray-500">Transporteur</span>
<span className="font-semibold text-gray-900 text-right max-w-[55%]">
{booking.carrierName}
</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-500">Trajet</span>
<span className="font-semibold text-gray-900">
{booking.origin} {booking.destination}
</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-500">Volume / Poids</span>
<span className="font-semibold text-gray-900">
{booking.volumeCBM} CBM · {booking.weightKG} kg
</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-500">Transit</span>
<span className="font-semibold text-gray-900">{booking.transitDays} jours</span>
</div>
<div className="border-t pt-3 flex justify-between text-sm">
<span className="text-gray-500">Prix transport</span>
<span className="font-bold text-gray-900">
{formatPrice(booking.priceEUR, 'EUR')}
</span>
</div>
</div>
{/* Commission box */}
<div className="bg-blue-600 rounded-xl p-5 text-white">
<p className="text-sm text-blue-100 mb-1">
Commission ({commissionRate}% du prix transport)
</p>
<p className="text-3xl font-bold">{formatPrice(commissionAmount, 'EUR')}</p>
<p className="text-xs text-blue-200 mt-1">
{formatPrice(booking.priceEUR, 'EUR')} × {commissionRate}%
</p>
</div>
<div className="bg-white rounded-xl border border-gray-200 p-4 flex items-start space-x-3">
<CheckCircle className="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
<p className="text-xs text-gray-500">
Après validation du paiement, votre demande est envoyée au transporteur (
{booking.carrierEmail}). Vous serez notifié de sa réponse.
</p>
</div>
</div>
</div>
</div>
</div>
);
}