438 lines
17 KiB
TypeScript
438 lines
17 KiB
TypeScript
/**
|
||
* 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 1–3 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
|
||
“J'ai effectué le virement”.
|
||
</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'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>
|
||
);
|
||
}
|