Files
carmanagement/apps/marketplace/src/components/BookingForm.tsx
T
2026-06-11 15:35:25 -04:00

722 lines
31 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 { useEffect, useRef, useState } from 'react'
import { formatCurrency } from '@rentaldrivego/types'
import { MarketplaceApiError, marketplaceFetch, marketplacePost } from '@/lib/api'
import { useMarketplacePreferences } from '@/components/MarketplaceShell'
type Props = {
vehicleId: string
companySlug: string
dailyRate: number
pickupLocations: string[]
allowDifferentDropoff: boolean
dropoffLocations: string[]
}
type DropoffMode = 'same' | 'different'
type BookingFields = {
firstName: string
lastName: string
email: string
phone: string
pickupLocation: string
dropoffMode: DropoffMode
returnLocation: string
startDate: string
endDate: string
notes: string
dateOfBirth: string
nationality: string
identityDocumentNumber: string
fullAddress: string
driverLicense: string
licenseIssuedAt: string
licenseExpiry: string
licenseCountry: string
licenseCategory: string
internationalLicenseNumber: string
}
type AvailabilityState = {
status: 'idle' | 'checking' | 'available' | 'unavailable' | 'error'
nextAvailableAt: string | null
}
type AvailabilityResponse = {
available: boolean
availabilityStatus: string
nextAvailableAt: string | null
blockingReason: string | null
}
type BookingResponse = {
reservationId: string
bookingReference?: string | null
companyName?: string
vehicleName?: string
startDate?: string
endDate?: string
status?: string
message?: string
}
type FunnelEventName =
| 'booking_form_viewed'
| 'trip_dates_selected'
| 'contact_details_started'
| 'driver_details_expanded'
| 'booking_request_submitted'
| 'booking_request_failed'
| 'booking_request_success'
const copy = {
en: {
title: 'Request this car in 1 minute',
subtitle: 'Send the essentials now. Driver documents can be added later if the company needs them.',
tripDetails: 'Trip details',
contactDetails: 'Contact details',
optionalDetails: 'Driver details, optional now',
optionalDetailsHint: 'Skip this unless you want to speed up final approval. The company can collect it after confirming availability.',
showDriverDetails: 'Add driver details now',
hideDriverDetails: 'Hide driver details',
firstName: 'First name',
lastName: 'Last name',
email: 'Email',
phone: 'Phone',
dateOfBirth: 'Date of birth',
nationality: 'Nationality',
identityDocumentNumber: 'CIN / Passport number',
fullAddress: 'Full address',
driverLicense: 'License number',
licenseIssuedAt: 'Issue date',
licenseExpiry: 'Expiry date',
licenseCountry: 'Country of issue',
licenseCategory: 'License category',
internationalLicenseNumber: 'International permit number',
pickupLocation: 'Pick-up location',
returnLocation: 'Drop-off location',
sameDropoff: 'Same drop-off',
differentDropoff: 'Different drop-off',
sameAsPickup: 'Return to the same location',
startDate: 'Pick-up date',
endDate: 'Return date',
notes: 'Anything the company should know? Optional.',
submit: 'Send booking request',
submitting: 'Sending…',
successTitle: 'Request sent.',
successBody: 'This first step is a request, not an instant booking. Here is what happens next.',
bookingReference: 'Booking reference',
pendingStatus: 'Pending',
invalidDates: 'Return date must be after pick-up date.',
required: 'Add your trip dates, name, email, and phone.',
perDay: '/ day',
estimatedTotal: 'Estimated total',
days: 'day(s)',
genericError: 'Something went wrong. Please try again.',
locationRequired: 'Select the required pick-up and drop-off locations.',
checkingAvailability: 'Checking availability for these dates…',
likelyAvailable: 'Likely available for these dates. Final confirmation still depends on the rental company.',
unavailableWithDate: 'Unavailable for these dates. Next likely availability: {date}.',
unavailableWithoutDate: 'Unavailable for these dates. Choose different trip dates to continue.',
availabilityError: 'Availability could not be checked right now. You can still try again in a moment.',
nextStepRequestSent: 'Request sent',
nextStepRequestSentBody: 'The company receives your dates and contact details first.',
nextStepAvailability: 'Company confirms availability',
nextStepAvailabilityBody: 'They review the request and accept it if the car is still open.',
nextStepDocuments: 'Driver documents if needed',
nextStepDocumentsBody: 'License and identity details can be collected after the company accepts the request.',
nextStepPayment: 'Payment or deposit',
nextStepPaymentBody: 'Any required payment is handled only after availability is confirmed.',
nextStepConfirmed: 'Booking confirmed',
nextStepConfirmedBody: 'The booking is final once availability, documents, and payment are all cleared.',
},
fr: {
title: 'Demander cette voiture en 1 minute',
subtitle: 'Envoyez lessentiel maintenant. Les documents conducteur peuvent être ajoutés plus tard si lagence les demande.',
tripDetails: 'Détails du trajet',
contactDetails: 'Coordonnées',
optionalDetails: 'Détails conducteur, optionnels maintenant',
optionalDetailsHint: 'Ignorez cette partie sauf si vous voulez accélérer la validation finale. Lagence pourra les demander après confirmation de disponibilité.',
showDriverDetails: 'Ajouter les détails conducteur',
hideDriverDetails: 'Masquer les détails conducteur',
firstName: 'Prénom',
lastName: 'Nom',
email: 'E-mail',
phone: 'Téléphone',
dateOfBirth: 'Date de naissance',
nationality: 'Nationalité',
identityDocumentNumber: 'N° CIN / passeport',
fullAddress: 'Adresse complète',
driverLicense: 'Numéro du permis',
licenseIssuedAt: 'Date de délivrance',
licenseExpiry: 'Date dexpiration',
licenseCountry: 'Pays de délivrance',
licenseCategory: 'Catégorie du permis',
internationalLicenseNumber: 'N° de permis international',
pickupLocation: 'Lieu de départ',
returnLocation: 'Lieu de retour',
sameDropoff: 'Même retour',
differentDropoff: 'Retour différent',
sameAsPickup: 'Retour au même lieu',
startDate: 'Date de départ',
endDate: 'Date de retour',
notes: 'Une information utile pour lagence ? Optionnel.',
submit: 'Envoyer la demande',
submitting: 'Envoi…',
successTitle: 'Demande envoyée.',
successBody: 'Cette première étape reste une demande, pas une réservation instantanée. Voici la suite.',
bookingReference: 'Référence de réservation',
pendingStatus: 'En attente',
invalidDates: 'La date de retour doit être après la date de départ.',
required: 'Ajoutez vos dates, votre nom, votre e-mail et votre téléphone.',
perDay: '/ jour',
estimatedTotal: 'Total estimé',
days: 'jour(s)',
genericError: 'Une erreur est survenue. Veuillez réessayer.',
locationRequired: 'Sélectionnez les lieux de départ et de retour requis.',
checkingAvailability: 'Vérification de la disponibilité pour ces dates…',
likelyAvailable: 'Probablement disponible pour ces dates. La confirmation finale dépend encore de lagence.',
unavailableWithDate: 'Indisponible pour ces dates. Prochaine disponibilité probable : {date}.',
unavailableWithoutDate: 'Indisponible pour ces dates. Choisissez dautres dates pour continuer.',
availabilityError: 'La disponibilité ne peut pas être vérifiée pour le moment. Réessayez dans un instant.',
nextStepRequestSent: 'Demande envoyée',
nextStepRequestSentBody: 'Lagence reçoit dabord vos dates et vos coordonnées.',
nextStepAvailability: 'Confirmation de disponibilité',
nextStepAvailabilityBody: 'Lagence vérifie la demande et laccepte si la voiture est toujours libre.',
nextStepDocuments: 'Documents conducteur si nécessaires',
nextStepDocumentsBody: 'Le permis et les pièces didentité peuvent être demandés après lacceptation de la demande.',
nextStepPayment: 'Paiement ou dépôt',
nextStepPaymentBody: 'Le paiement demandé intervient seulement après la confirmation de disponibilité.',
nextStepConfirmed: 'Réservation confirmée',
nextStepConfirmedBody: 'La réservation devient finale une fois la disponibilité, les documents et le paiement validés.',
},
ar: {
title: 'اطلب هذه السيارة خلال دقيقة',
subtitle: 'أرسل المعلومات الأساسية الآن. يمكن إضافة وثائق السائق لاحقاً إذا احتاجت الشركة إليها.',
tripDetails: 'تفاصيل الرحلة',
contactDetails: 'بيانات التواصل',
optionalDetails: 'بيانات السائق، اختيارية الآن',
optionalDetailsHint: 'تجاوز هذا الجزء إلا إذا أردت تسريع الموافقة النهائية. يمكن للشركة طلبه بعد تأكيد التوفر.',
showDriverDetails: 'إضافة بيانات السائق الآن',
hideDriverDetails: 'إخفاء بيانات السائق',
firstName: 'الاسم الأول',
lastName: 'اسم العائلة',
email: 'البريد الإلكتروني',
phone: 'الهاتف',
dateOfBirth: 'تاريخ الميلاد',
nationality: 'الجنسية',
identityDocumentNumber: 'رقم البطاقة الوطنية / جواز السفر',
fullAddress: 'العنوان الكامل',
driverLicense: 'رقم الرخصة',
licenseIssuedAt: 'تاريخ الإصدار',
licenseExpiry: 'تاريخ الانتهاء',
licenseCountry: 'بلد الإصدار',
licenseCategory: 'فئة الرخصة',
internationalLicenseNumber: 'رقم الرخصة الدولية',
pickupLocation: 'موقع الاستلام',
returnLocation: 'موقع الإرجاع',
sameDropoff: 'نفس موقع الإرجاع',
differentDropoff: 'موقع إرجاع مختلف',
sameAsPickup: 'الإرجاع في نفس موقع الاستلام',
startDate: 'تاريخ الاستلام',
endDate: 'تاريخ الإرجاع',
notes: 'أي معلومة تريد إبلاغ الشركة بها؟ اختياري.',
submit: 'إرسال طلب الحجز',
submitting: 'جارٍ الإرسال…',
successTitle: 'تم إرسال الطلب.',
successBody: 'هذه الخطوة الأولى عبارة عن طلب وليست حجزاً فورياً. هذه هي الخطوات التالية.',
bookingReference: 'مرجع الحجز',
pendingStatus: 'قيد المراجعة',
invalidDates: 'يجب أن يكون تاريخ الإرجاع بعد تاريخ الاستلام.',
required: 'أضف تواريخ الرحلة والاسم والبريد والهاتف.',
perDay: '/ يوم',
estimatedTotal: 'الإجمالي التقديري',
days: 'يوم',
genericError: 'حدث خطأ ما. يرجى المحاولة مرة أخرى.',
locationRequired: 'اختر مواقع الاستلام والإرجاع المطلوبة.',
checkingAvailability: 'جارٍ التحقق من التوفر لهذه التواريخ…',
likelyAvailable: 'السيارة متاحة غالباً لهذه التواريخ، لكن التأكيد النهائي يعود إلى شركة التأجير.',
unavailableWithDate: 'غير متاحة لهذه التواريخ. أقرب توفر متوقع: {date}.',
unavailableWithoutDate: 'غير متاحة لهذه التواريخ. اختر تواريخ مختلفة للمتابعة.',
availabilityError: 'تعذر التحقق من التوفر الآن. حاول مرة أخرى بعد قليل.',
nextStepRequestSent: 'تم إرسال الطلب',
nextStepRequestSentBody: 'تتلقى الشركة أولاً التواريخ وبيانات التواصل.',
nextStepAvailability: 'تأكيد التوفر',
nextStepAvailabilityBody: 'تراجع الشركة الطلب وتقبله إذا كانت السيارة ما زالت متاحة.',
nextStepDocuments: 'وثائق السائق عند الحاجة',
nextStepDocumentsBody: 'يمكن طلب الرخصة ووثائق الهوية بعد قبول الطلب.',
nextStepPayment: 'الدفع أو العربون',
nextStepPaymentBody: 'أي دفعة مطلوبة تتم فقط بعد تأكيد التوفر.',
nextStepConfirmed: 'تأكيد الحجز',
nextStepConfirmedBody: 'يصبح الحجز نهائياً بعد اعتماد التوفر والوثائق والدفع.',
},
} as const
const emptyFields: BookingFields = {
firstName: '',
lastName: '',
email: '',
phone: '',
pickupLocation: '',
dropoffMode: 'same',
returnLocation: '',
startDate: '',
endDate: '',
notes: '',
dateOfBirth: '',
nationality: '',
identityDocumentNumber: '',
fullAddress: '',
driverLicense: '',
licenseIssuedAt: '',
licenseExpiry: '',
licenseCountry: '',
licenseCategory: '',
internationalLicenseNumber: '',
}
export function calculateBookingTotalDays(startDate: string, endDate: string): number {
if (!startDate || !endDate) return 0
return Math.max(0, Math.ceil((new Date(endDate).getTime() - new Date(startDate).getTime()) / 86_400_000))
}
export function resolveReturnLocation({
dropoffMode,
returnLocation,
pickupLocation,
}: {
dropoffMode: DropoffMode
returnLocation: string
pickupLocation: string
}): string | undefined {
if (dropoffMode === 'different') return returnLocation || undefined
return pickupLocation || undefined
}
export function normalizeOptionalText(value: string): string | undefined {
const trimmed = value.trim()
return trimmed || undefined
}
function interpolate(template: string, replacements: Record<string, string>) {
return Object.entries(replacements).reduce(
(current, [key, value]) => current.replace(`{${key}}`, value),
template,
)
}
function formatLocalizedDate(value: string, language: 'en' | 'fr' | 'ar') {
return new Intl.DateTimeFormat(language, { dateStyle: 'medium' }).format(new Date(value))
}
function getBookingSessionId() {
if (typeof window === 'undefined') return 'server'
const existing = window.sessionStorage.getItem('marketplace-booking-session-id')
if (existing) return existing
const created =
typeof window.crypto?.randomUUID === 'function'
? window.crypto.randomUUID()
: `booking-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
window.sessionStorage.setItem('marketplace-booking-session-id', created)
return created
}
function RequiredMark() {
return <span className="text-red-500">*</span>
}
type FieldProps = {
label: string
value: string
onChange: (value: string) => void
required?: boolean
type?: string
textarea?: boolean
}
function Field({ label, value, onChange, required = false, type = 'text', textarea = false }: FieldProps) {
const className = 'w-full rounded-xl border border-stone-200 bg-white px-3 py-2 text-sm text-stone-900 outline-none focus:border-stone-400 dark:border-blue-800 dark:bg-blue-900/30 dark:text-stone-100 dark:focus:border-stone-500'
return (
<div className="space-y-1">
<label className="text-xs font-medium text-stone-600 dark:text-stone-400">
{label} {required ? <RequiredMark /> : null}
</label>
{textarea ? (
<textarea value={value} onChange={(e) => onChange(e.target.value)} rows={3} className={className} />
) : (
<input required={required} type={type} value={value} onChange={(e) => onChange(e.target.value)} className={className} />
)}
</div>
)
}
export default function BookingForm({
vehicleId,
companySlug,
dailyRate,
pickupLocations,
allowDifferentDropoff,
dropoffLocations,
}: Props) {
const { language } = useMarketplacePreferences()
const t = copy[language]
const pickupOptions = Array.isArray(pickupLocations) ? pickupLocations : []
const dropoffOptions = Array.isArray(dropoffLocations) ? dropoffLocations : []
const [fields, setFields] = useState<BookingFields>({
...emptyFields,
pickupLocation: pickupOptions[0] ?? '',
returnLocation: dropoffOptions[0] ?? '',
})
const [submitting, setSubmitting] = useState(false)
const [error, setError] = useState<string | null>(null)
const [success, setSuccess] = useState(false)
const [successBooking, setSuccessBooking] = useState<BookingResponse | null>(null)
const [availability, setAvailability] = useState<AvailabilityState>({ status: 'idle', nextAvailableAt: null })
const totalDays = calculateBookingTotalDays(fields.startDate, fields.endDate)
const sessionIdRef = useRef<string>('')
const startedAtRef = useRef<number>(Date.now())
const trackedRef = useRef({
viewed: false,
tripDates: false,
contactStarted: false,
})
if (!sessionIdRef.current) {
sessionIdRef.current = getBookingSessionId()
}
function buildUnavailableMessage(nextAvailableAt: string | null) {
return nextAvailableAt
? interpolate(t.unavailableWithDate, { date: formatLocalizedDate(nextAvailableAt, language) })
: t.unavailableWithoutDate
}
async function trackEvent(eventName: FunnelEventName, metadata?: Record<string, string | number | boolean | null>) {
await marketplacePost('/marketplace/events', {
eventName,
companySlug,
vehicleId,
sessionId: sessionIdRef.current,
path: typeof window === 'undefined' ? undefined : window.location.pathname,
metadata,
}).catch(() => null)
}
async function checkAvailability(startDate: string, endDate: string, signal?: AbortSignal) {
const start = new Date(startDate)
const end = new Date(endDate)
const result = await marketplaceFetch<AvailabilityResponse>(
`/marketplace/${companySlug}/vehicles/${vehicleId}/availability?startDate=${encodeURIComponent(start.toISOString())}&endDate=${encodeURIComponent(end.toISOString())}`,
signal ? { signal } : undefined,
)
setAvailability({
status: result.available ? 'available' : 'unavailable',
nextAvailableAt: result.nextAvailableAt,
})
return result
}
function update<K extends keyof BookingFields>(key: K, value: BookingFields[K]) {
setFields((current) => ({ ...current, [key]: value }))
}
useEffect(() => {
if (trackedRef.current.viewed) return
trackedRef.current.viewed = true
void trackEvent('booking_form_viewed', {
allowDifferentDropoff,
hasPickupLocations: pickupOptions.length > 0,
hasDropoffLocations: dropoffOptions.length > 0,
})
}, [allowDifferentDropoff, dropoffOptions.length, pickupOptions.length])
useEffect(() => {
if (trackedRef.current.contactStarted) return
if (!fields.firstName.trim() && !fields.lastName.trim() && !fields.email.trim() && !fields.phone.trim()) return
trackedRef.current.contactStarted = true
void trackEvent('contact_details_started')
}, [fields.email, fields.firstName, fields.lastName, fields.phone])
useEffect(() => {
if (!fields.startDate || !fields.endDate) {
setAvailability({ status: 'idle', nextAvailableAt: null })
return
}
const start = new Date(fields.startDate)
const end = new Date(fields.endDate)
if (end <= start) {
setAvailability({ status: 'idle', nextAvailableAt: null })
return
}
if (!trackedRef.current.tripDates) {
trackedRef.current.tripDates = true
void trackEvent('trip_dates_selected', { totalDays: calculateBookingTotalDays(fields.startDate, fields.endDate) })
}
const controller = new AbortController()
setAvailability((current) => ({ ...current, status: 'checking' }))
void checkAvailability(fields.startDate, fields.endDate, controller.signal).catch((err) => {
if (controller.signal.aborted) return
if (err instanceof MarketplaceApiError && err.code === 'invalid_dates') {
setAvailability({ status: 'idle', nextAvailableAt: null })
return
}
setAvailability({ status: 'error', nextAvailableAt: null })
})
return () => controller.abort()
}, [companySlug, fields.endDate, fields.startDate, vehicleId])
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
setError(null)
if (!fields.firstName.trim() || !fields.lastName.trim() || !fields.email.trim() || !fields.phone.trim() || !fields.startDate || !fields.endDate) {
setError(t.required)
return
}
if ((pickupOptions.length > 0 && !fields.pickupLocation) || (fields.dropoffMode === 'different' && dropoffOptions.length > 0 && !fields.returnLocation)) {
setError(t.locationRequired)
return
}
const start = new Date(fields.startDate)
const end = new Date(fields.endDate)
if (end <= start) {
setError(t.invalidDates)
return
}
setSubmitting(true)
try {
void trackEvent('booking_request_submitted', {
totalDays,
})
const availabilityResult = await checkAvailability(fields.startDate, fields.endDate)
if (!availabilityResult.available) {
const message = buildUnavailableMessage(availabilityResult.nextAvailableAt)
setError(message)
void trackEvent('booking_request_failed', {
reason: 'unavailable',
nextAvailableAt: availabilityResult.nextAvailableAt,
})
return
}
const booking = await marketplacePost<BookingResponse>('/marketplace/reservations', {
vehicleId,
companySlug,
firstName: fields.firstName.trim(),
lastName: fields.lastName.trim(),
email: fields.email.trim(),
phone: fields.phone.trim(),
startDate: start.toISOString(),
endDate: end.toISOString(),
pickupLocation: fields.pickupLocation || undefined,
returnLocation: resolveReturnLocation({
dropoffMode: fields.dropoffMode,
returnLocation: fields.returnLocation,
pickupLocation: fields.pickupLocation,
}),
notes: normalizeOptionalText(fields.notes),
language,
})
setSuccessBooking(booking)
setSuccess(true)
void trackEvent('booking_request_success', {
totalDays,
elapsedMs: Date.now() - startedAtRef.current,
})
} catch (err: unknown) {
if (err instanceof MarketplaceApiError && err.code === 'unavailable') {
setAvailability({ status: 'unavailable', nextAvailableAt: err.nextAvailableAt ?? null })
setError(buildUnavailableMessage(err.nextAvailableAt ?? null))
} else {
setError(err instanceof Error ? err.message : t.genericError)
}
void trackEvent('booking_request_failed', {
reason: err instanceof MarketplaceApiError ? err.code ?? 'request_failed' : 'request_failed',
})
} finally {
setSubmitting(false)
}
}
if (success) {
return (
<div className="rounded-2xl border border-green-200 bg-green-50 p-6 dark:border-green-800 dark:bg-green-950/40">
<p className="text-base font-semibold text-green-800 dark:text-green-300">{t.successTitle}</p>
<p className="mt-2 text-sm text-green-700 dark:text-green-400">{t.successBody}</p>
{successBooking?.bookingReference ? (
<div className="mt-4 rounded-2xl border border-green-200 bg-white/70 px-4 py-3 text-sm text-green-900 dark:border-green-800 dark:bg-green-950/20 dark:text-green-200">
<p className="font-semibold">{t.bookingReference}: {successBooking.bookingReference}</p>
<p className="mt-1 text-xs text-green-700 dark:text-green-400">
{successBooking.status ?? t.pendingStatus}
</p>
</div>
) : null}
<div className="mt-4 space-y-3">
{[
{ title: t.nextStepRequestSent, body: t.nextStepRequestSentBody, active: true },
{ title: t.nextStepAvailability, body: t.nextStepAvailabilityBody },
{ title: t.nextStepDocuments, body: t.nextStepDocumentsBody },
{ title: t.nextStepPayment, body: t.nextStepPaymentBody },
{ title: t.nextStepConfirmed, body: t.nextStepConfirmedBody },
].map((step, index) => (
<div
key={step.title}
className={`rounded-2xl border px-4 py-3 ${step.active ? 'border-green-300 bg-white/80 dark:border-green-700 dark:bg-green-950/20' : 'border-green-100 bg-white/50 dark:border-green-900/60 dark:bg-transparent'}`}
>
<p className="text-sm font-semibold text-green-900 dark:text-green-200">{index + 1}. {step.title}</p>
<p className="mt-1 text-xs leading-5 text-green-700 dark:text-green-400">{step.body}</p>
</div>
))}
</div>
</div>
)
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<p className="text-sm font-semibold text-stone-900 dark:text-stone-100">{t.title}</p>
<p className="mt-1 text-xs leading-5 text-stone-500 dark:text-stone-400">{t.subtitle}</p>
</div>
{error ? (
<p className="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-800 dark:bg-red-950/40 dark:text-red-400">
{error}
</p>
) : null}
<section className="space-y-3 rounded-2xl border border-stone-200 bg-stone-50 p-4 dark:border-blue-800 dark:bg-blue-950">
<p className="text-sm font-semibold text-stone-900 dark:text-stone-100">{t.tripDetails}</p>
{pickupOptions.length > 0 ? (
<div className="space-y-1">
<label className="text-xs font-medium text-stone-600 dark:text-stone-400">{t.pickupLocation} <RequiredMark /></label>
<select
value={fields.pickupLocation}
onChange={(e) => update('pickupLocation', e.target.value)}
className="w-full rounded-xl border border-stone-200 bg-white px-3 py-2 text-sm text-stone-900 outline-none focus:border-stone-400 dark:border-blue-800 dark:bg-blue-900/30 dark:text-stone-100 dark:focus:border-stone-500"
>
{pickupOptions.map((location) => <option key={location} value={location}>{location}</option>)}
</select>
</div>
) : null}
{allowDifferentDropoff ? (
<div className="space-y-3">
<div className="inline-flex rounded-full border border-stone-200 bg-white p-1 dark:border-blue-800 dark:bg-blue-900/30">
<button
type="button"
onClick={() => update('dropoffMode', 'same')}
className={`rounded-full px-3 py-1.5 text-xs font-semibold transition ${fields.dropoffMode === 'same' ? 'bg-blue-900 text-white dark:bg-orange-400' : 'text-stone-500 dark:text-stone-400'}`}
>
{t.sameDropoff}
</button>
<button
type="button"
onClick={() => update('dropoffMode', 'different')}
className={`rounded-full px-3 py-1.5 text-xs font-semibold transition ${fields.dropoffMode === 'different' ? 'bg-blue-900 text-white dark:bg-orange-400' : 'text-stone-500 dark:text-stone-400'}`}
>
{t.differentDropoff}
</button>
</div>
{fields.dropoffMode === 'different' && dropoffOptions.length > 0 ? (
<div className="space-y-1">
<label className="text-xs font-medium text-stone-600 dark:text-stone-400">{t.returnLocation} <RequiredMark /></label>
<select
value={fields.returnLocation}
onChange={(e) => update('returnLocation', e.target.value)}
className="w-full rounded-xl border border-stone-200 bg-white px-3 py-2 text-sm text-stone-900 outline-none focus:border-stone-400 dark:border-blue-800 dark:bg-blue-900/30 dark:text-stone-100 dark:focus:border-stone-500"
>
{dropoffOptions.map((location) => <option key={location} value={location}>{location}</option>)}
</select>
</div>
) : (
<p className="text-sm text-stone-500 dark:text-stone-400">{fields.pickupLocation || t.sameAsPickup}</p>
)}
</div>
) : null}
<div className="grid grid-cols-2 gap-3">
<Field label={t.startDate} required type="date" value={fields.startDate} onChange={(value) => update('startDate', value)} />
<Field label={t.endDate} required type="date" value={fields.endDate} onChange={(value) => update('endDate', value)} />
</div>
{totalDays > 0 ? (
<p className="text-sm text-stone-600 dark:text-stone-400">
{t.estimatedTotal}:{' '}
<strong className="text-stone-900 dark:text-stone-100">{formatCurrency(dailyRate * totalDays, 'MAD', language)}</strong>{' '}
({totalDays} {t.days})
</p>
) : null}
{availability.status === 'checking' ? (
<p className="rounded-xl border border-stone-200 bg-white px-4 py-3 text-sm text-stone-600 dark:border-blue-800 dark:bg-blue-900/30 dark:text-stone-300">
{t.checkingAvailability}
</p>
) : null}
{availability.status === 'available' ? (
<p className="rounded-xl border border-green-200 bg-green-50 px-4 py-3 text-sm text-green-700 dark:border-green-800 dark:bg-green-950/40 dark:text-green-300">
{t.likelyAvailable}
</p>
) : null}
{availability.status === 'unavailable' ? (
<p className="rounded-xl border border-orange-200 bg-orange-50 px-4 py-3 text-sm text-orange-800 dark:border-orange-800 dark:bg-orange-950/40 dark:text-orange-300">
{buildUnavailableMessage(availability.nextAvailableAt)}
</p>
) : null}
{availability.status === 'error' ? (
<p className="rounded-xl border border-stone-200 bg-white px-4 py-3 text-sm text-stone-600 dark:border-blue-800 dark:bg-blue-900/30 dark:text-stone-300">
{t.availabilityError}
</p>
) : null}
</section>
<section className="space-y-3 rounded-2xl border border-stone-200 bg-stone-50 p-4 dark:border-blue-800 dark:bg-blue-950">
<p className="text-sm font-semibold text-stone-900 dark:text-stone-100">{t.contactDetails}</p>
<div className="grid grid-cols-2 gap-3">
<Field label={t.firstName} required value={fields.firstName} onChange={(value) => update('firstName', value)} />
<Field label={t.lastName} required value={fields.lastName} onChange={(value) => update('lastName', value)} />
</div>
<Field label={t.email} required type="email" value={fields.email} onChange={(value) => update('email', value)} />
<Field label={t.phone} required type="tel" value={fields.phone} onChange={(value) => update('phone', value)} />
<Field label={t.notes} textarea value={fields.notes} onChange={(value) => update('notes', value)} />
</section>
<button
type="submit"
disabled={submitting || availability.status === 'checking'}
className="w-full rounded-full bg-orange-600 px-6 py-3 text-center text-sm font-semibold text-white transition hover:bg-orange-700 disabled:opacity-60 dark:bg-orange-400 dark:text-white dark:hover:bg-orange-300"
>
{submitting ? t.submitting : t.submit}
</button>
</form>
)
}