Files
carmanagement/apps/public-site/src/app/book/BookClient.tsx
T
2026-05-06 22:58:23 -04:00

755 lines
37 KiB
TypeScript
Raw 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.
'use client'
import { useRouter, useSearchParams } from 'next/navigation'
import { useEffect, useState } from 'react'
import { DEFAULT_COMPANY_SLUG } from '@/lib/api'
import { getPublicSiteDictionary, type PublicSiteLanguage } from '@/lib/i18n'
interface VehicleDetail {
id: string
make: string
model: string
year: number
category: string
dailyRate: number
seats: number
transmission: string
photos: string[]
}
interface PromoOffer {
id: string
title: string
discountValue: number
}
interface InsurancePolicy {
id: string
name: string
type: string
chargeType: 'PER_DAY' | 'PER_RENTAL' | 'PERCENTAGE_OF_RENTAL'
chargeValue: number
isRequired: boolean
}
interface ContractSettings {
fuelPolicyType: string
fuelPolicyNote: string | null
additionalDriverCharge: 'FREE' | 'PER_DAY' | 'FLAT'
additionalDriverDailyRate: number
additionalDriverFlatRate: number
}
interface BookingOptions {
insurancePolicies: InsurancePolicy[]
contractSettings: ContractSettings | null
}
interface Step1 {
startDate: string
endDate: string
}
interface Step2 {
firstName: string
lastName: string
email: string
phone: string
driverLicense: string
dateOfBirth: string
licenseExpiry: string
licenseIssuedAt: string
nationality: string
promoCode: string
}
interface AdditionalDriverForm {
firstName: string
lastName: string
email: string
phone: string
driverLicense: string
dateOfBirth: string
licenseExpiry: string
licenseIssuedAt: string
nationality: string
}
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:4000/api/v1'
function diffDays(start: string, end: string): number {
if (!start || !end) return 0
const ms = new Date(end).getTime() - new Date(start).getTime()
return Math.max(1, Math.ceil(ms / 86400000))
}
function fmt(n: number) {
return n.toLocaleString('fr-MA', { style: 'currency', currency: 'MAD', maximumFractionDigits: 0 })
}
function inputClass(error?: string) {
return [
'w-full rounded-xl border px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-sky-500',
error ? 'border-red-400 bg-red-50' : 'border-slate-300 bg-white',
].join(' ')
}
function insuranceCharge(policy: InsurancePolicy, days: number, baseTotal: number) {
if (policy.chargeType === 'PER_DAY') return policy.chargeValue * days
if (policy.chargeType === 'PERCENTAGE_OF_RENTAL') return Math.round(baseTotal * policy.chargeValue / 100)
return policy.chargeValue
}
function additionalDriverCharge(settings: ContractSettings | null, count: number, days: number) {
if (!settings || count === 0) return 0
if (settings.additionalDriverCharge === 'PER_DAY') return settings.additionalDriverDailyRate * count * days
if (settings.additionalDriverCharge === 'FLAT') return settings.additionalDriverFlatRate * count
return 0
}
function getDayLabel(language: PublicSiteLanguage, days: number) {
if (language === 'fr') return days === 1 ? 'jour' : 'jours'
if (language === 'ar') return days === 1 ? 'يوم' : 'أيام'
return days === 1 ? 'day' : 'days'
}
function StepDots({
step,
steps,
}: {
step: number
steps: {
selectDates: string
driverDetails: string
reviewBooking: string
payment: string
}
}) {
return (
<div className="mb-6 flex items-center gap-2">
{[1, 2, 3, 4].map((n) => (
<div key={n} className="flex items-center gap-2">
<div
className={[
'flex h-8 w-8 items-center justify-center rounded-full text-sm font-bold',
n === step ? 'bg-slate-900 text-white' : n < step ? 'bg-sky-600 text-white' : 'bg-slate-200 text-slate-500',
].join(' ')}
>
{n < step ? '✓' : n}
</div>
{n < 4 && <div className={['h-0.5 w-8 rounded', n < step ? 'bg-sky-600' : 'bg-slate-200'].join(' ')} />}
</div>
))}
<span className="ml-2 text-sm text-slate-500">
{step === 1 && steps.selectDates}
{step === 2 && steps.driverDetails}
{step === 3 && steps.reviewBooking}
{step === 4 && steps.payment}
</span>
</div>
)
}
function SummaryRow({
label,
value,
valueClass = 'text-slate-900',
}: {
label: string
value: string
valueClass?: string
}) {
return (
<div className="flex justify-between gap-4 px-4 py-3">
<span className="flex-shrink-0 text-slate-500">{label}</span>
<span className={`text-right font-medium ${valueClass}`}>{value}</span>
</div>
)
}
export default function BookClient({ language }: { language: PublicSiteLanguage }) {
const dict = getPublicSiteDictionary(language)
const params = useSearchParams()
const router = useRouter()
const vehicleId = params.get('vehicleId') ?? ''
const offerId = params.get('offerId') ?? undefined
const ref = params.get('ref') ?? 'direct'
const slug = params.get('slug') ?? DEFAULT_COMPANY_SLUG
const [step, setStep] = useState(1)
const [vehicle, setVehicle] = useState<VehicleDetail | null>(null)
const [vehicleLoading, setVehicleLoading] = useState(!!vehicleId)
const [vehicleError, setVehicleError] = useState('')
const [bookingOptions, setBookingOptions] = useState<BookingOptions | null>(null)
const [bookingOptionsError, setBookingOptionsError] = useState('')
const [step1, setStep1] = useState<Step1>({ startDate: '', endDate: '' })
const [step1Errors, setStep1Errors] = useState<Partial<Step1>>({})
const [step2, setStep2] = useState<Step2>({
firstName: '',
lastName: '',
email: '',
phone: '',
driverLicense: '',
dateOfBirth: '',
licenseExpiry: '',
licenseIssuedAt: '',
nationality: '',
promoCode: '',
})
const [step2Errors, setStep2Errors] = useState<Record<string, string>>({})
const [additionalDriverEnabled, setAdditionalDriverEnabled] = useState(false)
const [additionalDriver, setAdditionalDriver] = useState<AdditionalDriverForm>({
firstName: '',
lastName: '',
email: '',
phone: '',
driverLicense: '',
dateOfBirth: '',
licenseExpiry: '',
licenseIssuedAt: '',
nationality: '',
})
const [selectedInsurancePolicyIds, setSelectedInsurancePolicyIds] = useState<string[]>([])
const [promoApplied, setPromoApplied] = useState<PromoOffer | null>(null)
const [promoLoading, setPromoLoading] = useState(false)
const [promoError, setPromoError] = useState('')
const [submitting, setSubmitting] = useState(false)
const [submitError, setSubmitError] = useState('')
const [reservationId, setReservationId] = useState<string | null>(null)
const [requiresManualApproval, setRequiresManualApproval] = useState(false)
const [paymentProvider, setPaymentProvider] = useState<'AMANPAY' | 'PAYPAL' | 'CASH'>('AMANPAY')
const [paymentCurrency, setPaymentCurrency] = useState<'MAD' | 'USD' | 'EUR'>('MAD')
const [paymentLoading, setPaymentLoading] = useState(false)
const [paymentError, setPaymentError] = useState('')
useEffect(() => {
if (!vehicleId) return
setVehicleLoading(true)
fetch(`${API_BASE}/site/${slug}/vehicles/${vehicleId}`)
.then((r) => r.json())
.then((j) => {
if (j?.data) setVehicle(j.data as VehicleDetail)
else setVehicleError(dict.booking.vehicleNotFound)
})
.catch(() => setVehicleError(dict.booking.vehicleLoadError))
.finally(() => setVehicleLoading(false))
}, [dict.booking.vehicleLoadError, dict.booking.vehicleNotFound, vehicleId, slug])
useEffect(() => {
fetch(`${API_BASE}/site/${slug}/booking-options`)
.then((r) => r.json())
.then((j) => setBookingOptions(j.data as BookingOptions))
.catch(() => setBookingOptionsError(dict.booking.bookingOptionsLoadError))
}, [dict.booking.bookingOptionsLoadError, slug])
const days = diffDays(step1.startDate, step1.endDate)
const baseTotal = vehicle ? vehicle.dailyRate * days : 0
const discount = promoApplied ? Math.round(baseTotal * (promoApplied.discountValue / 100)) : 0
const requiredPolicies = (bookingOptions?.insurancePolicies ?? []).filter((policy) => policy.isRequired)
const optionalPolicies = (bookingOptions?.insurancePolicies ?? []).filter((policy) => !policy.isRequired)
const selectedPolicies = [
...requiredPolicies,
...optionalPolicies.filter((policy) => selectedInsurancePolicyIds.includes(policy.id)),
]
const insuranceTotal = selectedPolicies.reduce((sum, policy) => sum + insuranceCharge(policy, days, baseTotal), 0)
const additionalDriverTotal = additionalDriverCharge(bookingOptions?.contractSettings ?? null, additionalDriverEnabled ? 1 : 0, days)
const grandTotal = baseTotal - discount + insuranceTotal + additionalDriverTotal
const dayLabel = getDayLabel(language, days)
function validateStep1() {
const errs: Partial<Step1> = {}
if (!step1.startDate) errs.startDate = dict.booking.validation.pickStartDate
if (!step1.endDate) errs.endDate = dict.booking.validation.pickEndDate
if (step1.startDate && step1.endDate && new Date(step1.endDate) <= new Date(step1.startDate)) errs.endDate = dict.booking.validation.endDateAfterStart
setStep1Errors(errs)
return Object.keys(errs).length === 0
}
function validateStep2() {
const errs: Record<string, string> = {}
if (!step2.firstName.trim()) errs.firstName = dict.booking.validation.firstNameRequired
if (!step2.lastName.trim()) errs.lastName = dict.booking.validation.lastNameRequired
if (!step2.email.trim() || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(step2.email)) errs.email = dict.booking.validation.validEmailRequired
if (!step2.driverLicense.trim()) errs.driverLicense = dict.booking.validation.driverLicenseRequired
if (!step2.dateOfBirth) errs.dateOfBirth = dict.booking.validation.dateOfBirthRequired
if (!step2.licenseExpiry) errs.licenseExpiry = dict.booking.validation.licenseExpiryRequired
if (!step2.licenseIssuedAt) errs.licenseIssuedAt = dict.booking.validation.licenseIssueDateRequired
if (additionalDriverEnabled) {
if (!additionalDriver.firstName.trim()) errs.additionalDriverFirstName = dict.booking.validation.additionalDriverFirstNameRequired
if (!additionalDriver.lastName.trim()) errs.additionalDriverLastName = dict.booking.validation.additionalDriverLastNameRequired
if (!additionalDriver.driverLicense.trim()) errs.additionalDriverLicense = dict.booking.validation.additionalDriverLicenseRequired
}
setStep2Errors(errs)
return Object.keys(errs).length === 0
}
async function applyPromo() {
if (!step2.promoCode.trim()) return
setPromoLoading(true)
setPromoError('')
setPromoApplied(null)
try {
const res = await fetch(`${API_BASE}/site/${slug}/book/validate-code`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code: step2.promoCode.trim() }),
})
const json = await res.json()
if (!res.ok) setPromoError(json?.message ?? dict.booking.promo.invalid)
else setPromoApplied(json.data as PromoOffer)
} catch {
setPromoError(dict.booking.promo.unavailable)
} finally {
setPromoLoading(false)
}
}
async function handleSubmit() {
setSubmitting(true)
setSubmitError('')
try {
const res = await fetch(`${API_BASE}/site/${slug}/book`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
vehicleId,
offerId: promoApplied?.id ?? offerId,
promoCodeUsed: promoApplied ? step2.promoCode.trim() : undefined,
source: ref === 'marketplace' ? 'MARKETPLACE' : 'PUBLIC_SITE',
startDate: new Date(step1.startDate).toISOString(),
endDate: new Date(step1.endDate).toISOString(),
firstName: step2.firstName.trim(),
lastName: step2.lastName.trim(),
email: step2.email.trim(),
phone: step2.phone.trim() || undefined,
driverLicense: step2.driverLicense.trim(),
dateOfBirth: new Date(step2.dateOfBirth).toISOString(),
licenseExpiry: new Date(step2.licenseExpiry).toISOString(),
licenseIssuedAt: new Date(step2.licenseIssuedAt).toISOString(),
nationality: step2.nationality.trim() || undefined,
selectedInsurancePolicyIds,
additionalDrivers: additionalDriverEnabled ? [{
firstName: additionalDriver.firstName.trim(),
lastName: additionalDriver.lastName.trim(),
email: additionalDriver.email.trim() || undefined,
phone: additionalDriver.phone.trim() || undefined,
driverLicense: additionalDriver.driverLicense.trim(),
dateOfBirth: additionalDriver.dateOfBirth ? new Date(additionalDriver.dateOfBirth).toISOString() : undefined,
licenseExpiry: additionalDriver.licenseExpiry ? new Date(additionalDriver.licenseExpiry).toISOString() : undefined,
licenseIssuedAt: additionalDriver.licenseIssuedAt ? new Date(additionalDriver.licenseIssuedAt).toISOString() : undefined,
nationality: additionalDriver.nationality.trim() || undefined,
}] : [],
}),
})
const json = await res.json()
if (!res.ok) {
setSubmitError(json?.message ?? dict.booking.submit.failed)
return
}
setReservationId(json.data.id)
setRequiresManualApproval(Boolean(json.data.requiresManualApproval))
setStep(4)
if (json.data.requiresManualApproval) setPaymentProvider('CASH')
} catch {
setSubmitError(dict.booking.submit.network)
} finally {
setSubmitting(false)
}
}
async function handlePayOnline() {
if (!reservationId) return
setPaymentLoading(true)
setPaymentError('')
try {
const base = window.location.origin
const res = await fetch(`${API_BASE}/site/${slug}/booking/${reservationId}/pay`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
provider: paymentProvider,
currency: paymentCurrency,
successUrl: `${base}/book/confirmation?id=${reservationId}&payment=success`,
failureUrl: `${base}/book?vehicleId=${vehicleId}&payment=failed`,
}),
})
const json = await res.json()
if (!res.ok) {
setPaymentError(json?.message ?? dict.booking.payment.initiateFailed)
return
}
window.location.href = json.data.checkoutUrl
} catch {
setPaymentError(dict.booking.payment.network)
} finally {
setPaymentLoading(false)
}
}
function handlePayAtPickup() {
if (!reservationId) return
router.push(`/book/confirmation?id=${reservationId}`)
}
return (
<div className="card p-8">
<p className="text-sm font-semibold uppercase tracking-[0.18em] text-sky-700">{dict.booking.eyebrow}</p>
<h1 className="mt-2 text-3xl font-black text-slate-900">{dict.booking.title}</h1>
<div className="mt-6">
<StepDots step={step} steps={dict.booking.steps} />
</div>
{vehicleLoading && <div className="mb-6 rounded-xl bg-slate-100 px-4 py-3 text-sm text-slate-500">{dict.booking.loadingVehicle}</div>}
{vehicleError && <div className="mb-6 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{vehicleError}</div>}
{vehicle && (
<div className="mb-6 flex items-center gap-4 rounded-xl border border-slate-200 bg-slate-50 p-4">
{vehicle.photos[0] && (
// eslint-disable-next-line @next/next/no-img-element
<img src={vehicle.photos[0]} alt={`${vehicle.make} ${vehicle.model}`} className="h-16 w-24 rounded-lg object-cover flex-shrink-0" />
)}
<div>
<p className="font-bold text-slate-900">
{vehicle.make} {vehicle.model} <span className="font-normal text-slate-500">({vehicle.year})</span>
</p>
<p className="text-sm text-slate-500">{vehicle.category} · {vehicle.transmission} · {dict.vehicles.seats(vehicle.seats)}</p>
<p className="mt-0.5 text-sm font-semibold text-slate-800">{fmt(vehicle.dailyRate)} {dict.vehicles.detail.perDay}</p>
</div>
</div>
)}
{step === 1 && (
<div className="space-y-5">
<div className="grid gap-5 sm:grid-cols-2">
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{dict.booking.startDate}</label>
<input type="date" value={step1.startDate} min={new Date().toISOString().slice(0, 10)} onChange={(e) => setStep1((s) => ({ ...s, startDate: e.target.value }))} className={inputClass(step1Errors.startDate)} />
{step1Errors.startDate && <p className="mt-1 text-xs text-red-600">{step1Errors.startDate}</p>}
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{dict.booking.endDate}</label>
<input type="date" value={step1.endDate} min={step1.startDate || new Date().toISOString().slice(0, 10)} onChange={(e) => setStep1((s) => ({ ...s, endDate: e.target.value }))} className={inputClass(step1Errors.endDate)} />
{step1Errors.endDate && <p className="mt-1 text-xs text-red-600">{step1Errors.endDate}</p>}
</div>
</div>
{days > 0 && vehicle && (
<div className="rounded-xl border border-sky-200 bg-sky-50 px-4 py-3 text-sm text-sky-800">
<span className="font-semibold">{days} {dayLabel}</span> × {fmt(vehicle.dailyRate)} = <span className="font-bold">{fmt(baseTotal)}</span>
</div>
)}
<button onClick={() => validateStep1() && setStep(2)} className="w-full rounded-full bg-slate-900 py-3 text-sm font-semibold text-white transition-colors hover:bg-slate-700">
{dict.booking.continueToDriverDetails}
</button>
</div>
)}
{step === 2 && (
<div className="space-y-5">
<div className="grid gap-4 sm:grid-cols-2">
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{dict.booking.firstName}</label>
<input value={step2.firstName} onChange={(e) => setStep2((s) => ({ ...s, firstName: e.target.value }))} placeholder={dict.booking.firstName} className={inputClass(step2Errors.firstName)} />
{step2Errors.firstName && <p className="mt-1 text-xs text-red-600">{step2Errors.firstName}</p>}
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{dict.booking.lastName}</label>
<input value={step2.lastName} onChange={(e) => setStep2((s) => ({ ...s, lastName: e.target.value }))} placeholder={dict.booking.lastName} className={inputClass(step2Errors.lastName)} />
{step2Errors.lastName && <p className="mt-1 text-xs text-red-600">{step2Errors.lastName}</p>}
</div>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{dict.booking.email}</label>
<input type="email" value={step2.email} onChange={(e) => setStep2((s) => ({ ...s, email: e.target.value }))} placeholder="you@example.com" className={inputClass(step2Errors.email)} />
{step2Errors.email && <p className="mt-1 text-xs text-red-600">{step2Errors.email}</p>}
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{dict.booking.phone}</label>
<input value={step2.phone} onChange={(e) => setStep2((s) => ({ ...s, phone: e.target.value }))} placeholder="+212 6xx xxx xxx" className={inputClass()} />
</div>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{dict.booking.driverLicense}</label>
<input value={step2.driverLicense} onChange={(e) => setStep2((s) => ({ ...s, driverLicense: e.target.value }))} placeholder={dict.booking.driverLicense} className={inputClass(step2Errors.driverLicense)} />
{step2Errors.driverLicense && <p className="mt-1 text-xs text-red-600">{step2Errors.driverLicense}</p>}
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{dict.booking.nationality}</label>
<input value={step2.nationality} onChange={(e) => setStep2((s) => ({ ...s, nationality: e.target.value }))} placeholder={dict.booking.nationality} className={inputClass()} />
</div>
</div>
<div className="grid gap-4 sm:grid-cols-3">
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{dict.booking.dateOfBirth}</label>
<input type="date" value={step2.dateOfBirth} onChange={(e) => setStep2((s) => ({ ...s, dateOfBirth: e.target.value }))} className={inputClass(step2Errors.dateOfBirth)} />
{step2Errors.dateOfBirth && <p className="mt-1 text-xs text-red-600">{step2Errors.dateOfBirth}</p>}
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{dict.booking.licenseIssued}</label>
<input type="date" value={step2.licenseIssuedAt} onChange={(e) => setStep2((s) => ({ ...s, licenseIssuedAt: e.target.value }))} className={inputClass(step2Errors.licenseIssuedAt)} />
{step2Errors.licenseIssuedAt && <p className="mt-1 text-xs text-red-600">{step2Errors.licenseIssuedAt}</p>}
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{dict.booking.licenseExpiry}</label>
<input type="date" value={step2.licenseExpiry} onChange={(e) => setStep2((s) => ({ ...s, licenseExpiry: e.target.value }))} className={inputClass(step2Errors.licenseExpiry)} />
{step2Errors.licenseExpiry && <p className="mt-1 text-xs text-red-600">{step2Errors.licenseExpiry}</p>}
</div>
</div>
{bookingOptionsError && <div className="rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">{bookingOptionsError}</div>}
{bookingOptions && (
<div className="space-y-4 rounded-xl border border-slate-200 bg-slate-50 p-4">
<div>
<p className="text-sm font-semibold text-slate-900">{dict.booking.insuranceAndPickupPolicies}</p>
<p className="mt-1 text-xs text-slate-500">
{bookingOptions.contractSettings?.fuelPolicyNote || dict.booking.insuranceHint}
</p>
</div>
{selectedPolicies.length > 0 && (
<div className="rounded-lg border border-slate-200 bg-white px-3 py-2 text-xs text-slate-600">
{dict.booking.requiredCoverage}: {requiredPolicies.map((policy) => policy.name).join(', ') || dict.booking.none}
</div>
)}
{optionalPolicies.length > 0 ? (
<div className="space-y-2">
{optionalPolicies.map((policy) => {
const selected = selectedInsurancePolicyIds.includes(policy.id)
return (
<label key={policy.id} className="flex items-center justify-between gap-3 rounded-xl border border-slate-200 bg-white px-4 py-3">
<div>
<p className="text-sm font-medium text-slate-900">{policy.name}</p>
<p className="text-xs text-slate-500">{policy.type}</p>
</div>
<div className="flex items-center gap-3">
<span className="text-sm font-semibold text-slate-800">{fmt(insuranceCharge(policy, days, baseTotal))}</span>
<input
type="checkbox"
checked={selected}
onChange={(e) => setSelectedInsurancePolicyIds((current) => e.target.checked ? [...current, policy.id] : current.filter((id) => id !== policy.id))}
/>
</div>
</label>
)
})}
</div>
) : (
<p className="text-xs text-slate-500">{dict.booking.noOptionalInsurance}</p>
)}
<div className="rounded-xl border border-slate-200 bg-white p-4">
<label className="flex items-center justify-between gap-3">
<div>
<p className="text-sm font-semibold text-slate-900">{dict.booking.additionalDriver}</p>
<p className="text-xs text-slate-500">{dict.booking.charge}: {fmt(additionalDriverCharge(bookingOptions.contractSettings, 1, days))}</p>
</div>
<input type="checkbox" checked={additionalDriverEnabled} onChange={(e) => setAdditionalDriverEnabled(e.target.checked)} />
</label>
{additionalDriverEnabled && (
<div className="mt-4 grid gap-4 sm:grid-cols-2">
<div>
<input value={additionalDriver.firstName} onChange={(e) => setAdditionalDriver((current) => ({ ...current, firstName: e.target.value }))} placeholder={`${dict.booking.additionalDriver} ${dict.booking.firstName.toLowerCase()}`} className={inputClass(step2Errors.additionalDriverFirstName)} />
{step2Errors.additionalDriverFirstName && <p className="mt-1 text-xs text-red-600">{step2Errors.additionalDriverFirstName}</p>}
</div>
<div>
<input value={additionalDriver.lastName} onChange={(e) => setAdditionalDriver((current) => ({ ...current, lastName: e.target.value }))} placeholder={`${dict.booking.additionalDriver} ${dict.booking.lastName.toLowerCase()}`} className={inputClass(step2Errors.additionalDriverLastName)} />
{step2Errors.additionalDriverLastName && <p className="mt-1 text-xs text-red-600">{step2Errors.additionalDriverLastName}</p>}
</div>
<input value={additionalDriver.email} onChange={(e) => setAdditionalDriver((current) => ({ ...current, email: e.target.value }))} placeholder={`${dict.booking.additionalDriver} ${dict.booking.email.toLowerCase()}`} className={inputClass()} />
<input value={additionalDriver.phone} onChange={(e) => setAdditionalDriver((current) => ({ ...current, phone: e.target.value }))} placeholder={`${dict.booking.additionalDriver} ${dict.booking.phone.toLowerCase()}`} className={inputClass()} />
<div>
<input value={additionalDriver.driverLicense} onChange={(e) => setAdditionalDriver((current) => ({ ...current, driverLicense: e.target.value }))} placeholder={`${dict.booking.additionalDriver} ${dict.booking.driverLicense.toLowerCase()}`} className={inputClass(step2Errors.additionalDriverLicense)} />
{step2Errors.additionalDriverLicense && <p className="mt-1 text-xs text-red-600">{step2Errors.additionalDriverLicense}</p>}
</div>
<input value={additionalDriver.nationality} onChange={(e) => setAdditionalDriver((current) => ({ ...current, nationality: e.target.value }))} placeholder={`${dict.booking.additionalDriver} ${dict.booking.nationality.toLowerCase()}`} className={inputClass()} />
<input type="date" value={additionalDriver.dateOfBirth} onChange={(e) => setAdditionalDriver((current) => ({ ...current, dateOfBirth: e.target.value }))} className={inputClass()} />
<input type="date" value={additionalDriver.licenseIssuedAt} onChange={(e) => setAdditionalDriver((current) => ({ ...current, licenseIssuedAt: e.target.value }))} className={inputClass()} />
<input type="date" value={additionalDriver.licenseExpiry} onChange={(e) => setAdditionalDriver((current) => ({ ...current, licenseExpiry: e.target.value }))} className={inputClass()} />
</div>
)}
</div>
</div>
)}
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{dict.booking.promoCode}</label>
<div className="flex gap-2">
<input
value={step2.promoCode}
onChange={(e) => {
setStep2((s) => ({ ...s, promoCode: e.target.value }))
setPromoApplied(null)
setPromoError('')
}}
placeholder={dict.booking.enterCode}
className={`flex-1 ${inputClass(promoError || undefined)}`}
/>
<button type="button" onClick={applyPromo} disabled={promoLoading || !step2.promoCode.trim()} className="rounded-xl border border-slate-300 px-5 py-3 text-sm font-semibold text-slate-700 transition-colors hover:bg-slate-50 disabled:opacity-50">
{promoLoading ? '...' : dict.booking.apply}
</button>
</div>
{promoError && <p className="mt-1 text-xs text-red-600">{promoError}</p>}
{promoApplied && <p className="mt-1 text-xs font-medium text-emerald-700">{dict.booking.appliedPromo(promoApplied.title, promoApplied.discountValue)}</p>}
</div>
<div className="flex gap-3 pt-2">
<button onClick={() => setStep(1)} className="rounded-full border border-slate-300 px-6 py-3 text-sm font-semibold text-slate-700 transition-colors hover:bg-slate-50">{dict.booking.back}</button>
<button onClick={() => validateStep2() && setStep(3)} className="flex-1 rounded-full bg-slate-900 py-3 text-sm font-semibold text-white transition-colors hover:bg-slate-700">
{dict.booking.reviewBookingCta}
</button>
</div>
</div>
)}
{step === 3 && (
<div className="space-y-6">
<div className="overflow-hidden rounded-xl border border-slate-200 divide-y divide-slate-100 text-sm">
<SummaryRow label={dict.booking.summary.vehicle} value={vehicle ? `${vehicle.make} ${vehicle.model}` : vehicleId} />
<SummaryRow label={dict.booking.summary.dates} value={`${step1.startDate} -> ${step1.endDate} (${days} ${dayLabel})`} />
<SummaryRow label={dict.booking.summary.primaryDriver} value={`${step2.firstName} ${step2.lastName}`} />
<SummaryRow label={dict.booking.summary.email} value={step2.email} />
<SummaryRow label={dict.booking.summary.driverLicense} value={step2.driverLicense} />
<SummaryRow label={dict.booking.summary.licenseExpiry} value={step2.licenseExpiry} />
{additionalDriverEnabled && <SummaryRow label={dict.booking.summary.additionalDriver} value={`${additionalDriver.firstName} ${additionalDriver.lastName}`} />}
{promoApplied && <SummaryRow label={dict.booking.summary.promoCode} value={`${step2.promoCode} (-${promoApplied.discountValue}%)`} valueClass="font-semibold text-emerald-700" />}
</div>
<div className="space-y-2 rounded-xl border border-slate-200 bg-slate-50 p-4 text-sm">
<div className="flex justify-between text-slate-600">
<span>{days} {dayLabel} × {fmt(vehicle?.dailyRate ?? 0)}</span>
<span>{fmt(baseTotal)}</span>
</div>
{discount > 0 && (
<div className="flex justify-between text-emerald-700">
<span>{dict.booking.summary.discount(promoApplied?.discountValue ?? 0)}</span>
<span>-{fmt(discount)}</span>
</div>
)}
{selectedPolicies.map((policy) => (
<div key={policy.id} className="flex justify-between text-slate-600">
<span>{policy.name}</span>
<span>{fmt(insuranceCharge(policy, days, baseTotal))}</span>
</div>
))}
{additionalDriverEnabled && (
<div className="flex justify-between text-slate-600">
<span>{dict.booking.additionalDriver}</span>
<span>{fmt(additionalDriverTotal)}</span>
</div>
)}
<div className="flex justify-between border-t border-slate-200 pt-2 text-base font-bold text-slate-900">
<span>{dict.booking.summary.total}</span>
<span>{fmt(grandTotal)}</span>
</div>
</div>
{submitError && <div className="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{submitError}</div>}
<div className="flex gap-3">
<button onClick={() => setStep(2)} className="rounded-full border border-slate-300 px-6 py-3 text-sm font-semibold text-slate-700 transition-colors hover:bg-slate-50">{dict.booking.back}</button>
<button onClick={handleSubmit} disabled={submitting} className="flex-1 rounded-full bg-slate-900 py-3 text-sm font-semibold text-white transition-colors hover:bg-slate-700 disabled:opacity-60">
{submitting ? dict.booking.processing : dict.booking.confirmReservation}
</button>
</div>
</div>
)}
{step === 4 && (
<div className="space-y-6">
<div className="rounded-xl border border-green-200 bg-green-50 px-4 py-3 text-sm font-medium text-green-800">
{dict.booking.reservationCreated}
</div>
{requiresManualApproval && (
<div className="rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">
{dict.booking.manualApprovalRequired}
</div>
)}
<div className="flex items-center justify-between rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm">
<span className="text-slate-600">{dict.booking.totalDue}</span>
<span className="text-base font-black text-slate-900">{fmt(grandTotal)}</span>
</div>
<div>
<p className="mb-2 text-sm font-medium text-slate-700">{dict.booking.currency}</p>
<div className="flex gap-2">
{(['MAD', 'USD', 'EUR'] as const).map((currency) => (
<button
key={currency}
onClick={() => setPaymentCurrency(currency)}
className={`rounded-lg border px-4 py-1.5 text-sm font-medium transition-colors ${
paymentCurrency === currency ? 'border-sky-500 bg-sky-50 text-sky-700' : 'border-slate-200 text-slate-600 hover:border-slate-300'
}`}
>
{currency}
</button>
))}
</div>
</div>
<div>
<p className="mb-3 text-sm font-medium text-slate-700">{dict.booking.paymentMethod}</p>
<div className="space-y-2">
{(['AMANPAY', 'PAYPAL', 'CASH'] as const).map((provider) => {
const disabled = requiresManualApproval && provider !== 'CASH'
return (
<button
key={provider}
onClick={() => !disabled && setPaymentProvider(provider)}
className={`w-full rounded-xl border-2 p-4 text-left transition-all ${
paymentProvider === provider ? 'border-slate-900 bg-slate-50' : 'border-slate-200 hover:border-slate-300'
} ${disabled ? 'opacity-50' : ''}`}
>
<div className="flex items-center gap-4">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-slate-100 text-lg">
{provider === 'AMANPAY' ? '🏦' : provider === 'PAYPAL' ? '🔵' : '💵'}
</div>
<div>
<p className="text-sm font-semibold text-slate-900">
{provider === 'AMANPAY' ? dict.booking.paymentMethods.amanpay.title : provider === 'PAYPAL' ? dict.booking.paymentMethods.paypal.title : dict.booking.paymentMethods.cash.title}
</p>
<p className="mt-0.5 text-xs text-slate-500">
{provider === 'AMANPAY' && dict.booking.paymentMethods.amanpay.description}
{provider === 'PAYPAL' && dict.booking.paymentMethods.paypal.description}
{provider === 'CASH' && dict.booking.paymentMethods.cash.description}
</p>
</div>
</div>
</button>
)
})}
</div>
</div>
{paymentError && <div className="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{paymentError}</div>}
<button
onClick={paymentProvider === 'CASH' ? handlePayAtPickup : handlePayOnline}
disabled={paymentLoading || (requiresManualApproval && paymentProvider !== 'CASH')}
className="w-full rounded-full bg-slate-900 py-3.5 text-sm font-semibold text-white transition-colors hover:bg-slate-700 disabled:opacity-60"
>
{paymentLoading
? dict.booking.redirectingToPayment
: paymentProvider === 'CASH'
? dict.booking.confirmPayAtPickup
: dict.booking.payWith(paymentProvider === 'AMANPAY' ? dict.booking.paymentMethods.amanpay.title : dict.booking.paymentMethods.paypal.title)}
</button>
</div>
)}
</div>
)
}