'use client' import { useEffect, useMemo, useReducer, useRef, useState } from 'react' import { useRouter } from 'next/navigation' import { BilingualInput, bilingualPrimary } from '@/components/ui/BilingualInput' import { getReservationWizardCopy, wizardSteps } from './reservationWizard.copy' import { ReservationReview } from './ReservationReview' import { ReservationStepper } from './ReservationStepper' import { createInitialDraft, reservationDraftReducer } from './reservationWizard.reducer' import { validateCompleteReservation, validateCustomerStep, validateIdentityStep, validateLicenseStep, validatePaymentExtrasStep, validateRentalStep, } from './reservationWizard.schemas' import { checkVehicleAvailability, submitReservationDraft } from './reservationWizard.submit' import type { AdditionalDriverDraft, Customer, FieldErrors, ReservationDraft, Vehicle, WizardStepId } from './reservationWizard.types' import type { BilingualField } from '@/components/ui/BilingualInput' type Language = 'en' | 'fr' | 'ar' const paymentModes = ['BANK_TRANSFER', 'CHECK'] as const export function ReservationWizard({ customers, vehicles, language, }: { customers: Customer[] vehicles: Vehicle[] language: Language }) { const router = useRouter() const copy = useMemo(() => getReservationWizardCopy(language), [language]) const localeCode = language === 'fr' ? 'fr-FR' : language === 'ar' ? 'ar-MA' : 'en-US' const [draft, dispatch] = useReducer(reservationDraftReducer, undefined, createInitialDraft) const [currentStep, setCurrentStep] = useState('customer') const [completedSteps, setCompletedSteps] = useState>(new Set()) const [errors, setErrors] = useState({}) const [submitError, setSubmitError] = useState(null) const [customerSearch, setCustomerSearch] = useState('') const [saving, setSaving] = useState(false) const [licenseImagePreviewUrl, setLicenseImagePreviewUrl] = useState(null) const headingRef = useRef(null) const dirtyRef = useRef(false) const pendingErrorFocusRef = useRef(null) const stepIndex = wizardSteps.findIndex((step) => step.id === currentStep) const highestReachableIndex = Math.min(completedSteps.size, wizardSteps.length - 1) const availableVehicles = vehicles.filter((vehicle) => vehicle.status === 'AVAILABLE') const filteredCustomers = useMemo(() => { const q = customerSearch.trim().toLowerCase() if (!q) return customers return customers.filter((customer) => `${customer.firstName} ${customer.lastName}`.toLowerCase().includes(q) || customer.email.toLowerCase().includes(q), ) }, [customerSearch, customers]) useEffect(() => { dirtyRef.current = JSON.stringify(draft) !== JSON.stringify(createInitialDraft()) }, [draft]) useEffect(() => { const onBeforeUnload = (event: BeforeUnloadEvent) => { if (!dirtyRef.current) return event.preventDefault() event.returnValue = copy.leaveWarning } window.addEventListener('beforeunload', onBeforeUnload) return () => window.removeEventListener('beforeunload', onBeforeUnload) }, [copy.leaveWarning]) useEffect(() => { headingRef.current?.focus() }, [currentStep]) useEffect(() => { if (!pendingErrorFocusRef.current) return const field = pendingErrorFocusRef.current requestAnimationFrame(() => { const target = document.querySelector(`[data-field="${field}"]`) if (target) { target.focus() pendingErrorFocusRef.current = null } }) }, [currentStep, errors]) useEffect(() => { if (!draft.license.newImageFile) { setLicenseImagePreviewUrl(null) return } const objectUrl = URL.createObjectURL(draft.license.newImageFile) setLicenseImagePreviewUrl(objectUrl) return () => URL.revokeObjectURL(objectUrl) }, [draft.license.newImageFile]) function setFieldErrors(nextErrors: FieldErrors) { setErrors(nextErrors) if (Object.keys(nextErrors).length === 0) return pendingErrorFocusRef.current = Object.keys(nextErrors)[0] requestAnimationFrame(() => { if (!pendingErrorFocusRef.current) return const target = document.querySelector(`[data-field="${pendingErrorFocusRef.current}"]`) if (target) { target.focus() pendingErrorFocusRef.current = null } }) } function updateCustomer(field: keyof ReservationDraft['customer'], value: any) { dispatch({ type: 'updateCustomer', field, value }) clearFieldError(`customer.${field}`) } function clearFieldError(field: string) { setErrors((current) => { if (!current[field]) return current const next = { ...current } delete next[field] return next }) } function updateIdentity(field: keyof ReservationDraft['identity'], value: string) { dispatch({ type: 'updateIdentity', field, value }) clearFieldError(`identity.${field}`) } function updateLicense(field: keyof ReservationDraft['license'], value: any) { dispatch({ type: 'updateLicense', field, value }) clearFieldError(`license.${field}`) } function updateRental(field: keyof ReservationDraft['rental'], value: string) { dispatch({ type: 'updateRental', field, value }) clearFieldError(`rental.${field}`) } function updatePayment(field: keyof ReservationDraft['payment'], value: string | boolean) { dispatch({ type: 'updatePayment', field, value }) clearFieldError(`payment.${field}`) } function selectCustomer(customerId: string) { if (!customerId) return const customer = customers.find((item) => item.id === customerId) if (!customer) return if (draft.customer.hydratedFromCustomerId && draft.customer.hydratedFromCustomerId !== customer.id) { const confirmed = window.confirm(copy.confirmReplaceCustomer) if (!confirmed) return } dispatch({ type: 'selectExistingCustomer', customer }) setCustomerSearch(`${customer.firstName} ${customer.lastName}`) setErrors({}) } function validateStep(step: WizardStepId) { if (step === 'customer') return validateCustomerStep(draft, copy) if (step === 'identity') return validateIdentityStep(draft, copy) if (step === 'license') return validateLicenseStep(draft, copy) if (step === 'rental') return validateRentalStep(draft, copy) if (step === 'payment') return validatePaymentExtrasStep(draft, copy) return validateCompleteReservation(draft, copy) } async function handleContinue() { setSubmitError(null) const nextErrors = validateStep(currentStep) if (Object.keys(nextErrors).length > 0) { setFieldErrors(nextErrors) return } if (currentStep === 'rental') { try { const availability = await checkVehicleAvailability(draft) if (!availability.available) { setFieldErrors({ 'rental.vehicleId': copy.availabilityConflict }) return } } catch (err: any) { setFieldErrors({ 'rental.vehicleId': err.message ?? copy.availabilityConflict }) return } } const nextCompleted = new Set(completedSteps) nextCompleted.add(currentStep) setCompletedSteps(nextCompleted) const nextStep = wizardSteps[Math.min(stepIndex + 1, wizardSteps.length - 1)] setErrors({}) setCurrentStep(nextStep.id) } function handleBack() { if (stepIndex === 0) { handleCancel() return } setErrors({}) setCurrentStep(wizardSteps[stepIndex - 1].id) } function handleCancel() { router.push('/reservations') } function handleStepClick(step: WizardStepId) { const targetIndex = wizardSteps.findIndex((item) => item.id === step) if (targetIndex > highestReachableIndex) return setErrors({}) setCurrentStep(step) } async function handleSubmit() { if (saving) { setSubmitError(copy.duplicateGuard) return } const nextErrors = validateCompleteReservation(draft, copy) if (Object.keys(nextErrors).length > 0) { const firstStepWithError = wizardSteps.find((step) => Object.keys(nextErrors).some((field) => { if (step.id === 'customer') return field.startsWith('customer.') if (step.id === 'identity') return field.startsWith('identity.') if (step.id === 'license') return field.startsWith('license.') if (step.id === 'rental') return field.startsWith('rental.') if (step.id === 'payment') return field.startsWith('payment.') || field.startsWith('additionalDrivers.') return false }), ) if (firstStepWithError) setCurrentStep(firstStepWithError.id) setFieldErrors(nextErrors) return } setSaving(true) setSubmitError(null) try { const result = await submitReservationDraft(draft, { onCustomerIdResolved: (customerId) => dispatch({ type: 'setCreatedCustomerId', customerId }), }) dirtyRef.current = false dispatch({ type: 'reset' }) router.push(`/reservations/${result.reservation.id}`) } catch (err: any) { setSubmitError(err.code === 'vehicle_unavailable' ? copy.availabilityConflict : err.message ?? copy.submitFailed) } finally { setSaving(false) } } const currentLabel = copy[wizardSteps[stepIndex].labelKey] as string return (

{copy.title}

{copy.subtitle}

{submitError ?
{submitError}
: null}
{ event.preventDefault() if (currentStep === 'review') void handleSubmit() }} >

{currentLabel}

{currentStep === 'customer' ? ( { dispatch({ type: 'setCustomerMode', mode }) setErrors({}) }} onCustomerChange={updateCustomer} /> ) : null} {currentStep === 'identity' ? ( ) : null} {currentStep === 'license' ? ( ) : null} {currentStep === 'rental' ? ( ) : null} {currentStep === 'payment' ? ( dispatch({ type: 'addAdditionalDriver' })} onRemoveDriver={(id: string) => dispatch({ type: 'removeAdditionalDriver', id })} onDriverChange={(id: string, field: keyof AdditionalDriverDraft, value: any) => { dispatch({ type: 'updateAdditionalDriver', id, field, value }) setErrors((current) => { const index = draft.additionalDrivers.findIndex((driver) => driver.id === id) const key = `additionalDrivers.${index}.${field}` if (!current[key]) return current const next = { ...current } delete next[key] return next }) }} /> ) : null} {currentStep === 'review' ? ( ) : null}
{currentStep === 'review' ? ( ) : null}
{currentStep === 'review' ? ( ) : ( )}
) } function FieldError({ id, errors }: { id: string; errors: FieldErrors }) { return errors[id] ?

{errors[id]}

: null } function inputProps(field: string, errors: FieldErrors) { return { 'data-field': field, 'aria-invalid': Boolean(errors[field]), 'aria-describedby': errors[field] ? `${field}-error` : undefined, } } function CustomerStep({ copy, draft, customers, customerSearch, errors, onSearchChange, onSelectCustomer, onModeChange, onCustomerChange, }: { copy: ReturnType draft: ReservationDraft customers: Customer[] customerSearch: string errors: FieldErrors onSearchChange: (value: string) => void onSelectCustomer: (customerId: string) => void onModeChange: (mode: 'existing' | 'new') => void onCustomerChange: (field: keyof ReservationDraft['customer'], value: any) => void }) { return (
{(['existing', 'new'] as const).map((mode) => ( ))}
{draft.customer.mode === 'existing' ? (
) : (
onCustomerChange('firstName', value)} />
onCustomerChange('lastName', value)} />
)}
) } function IdentityStep({ copy, draft, errors, onChange, }: { copy: ReturnType draft: ReservationDraft errors: FieldErrors onChange: (field: keyof ReservationDraft['identity'], value: string) => void }) { return (
{draft.customer.hydratedFromCustomerId ?

{copy.loadedCustomerData}

: null}
onChange('dateOfBirth', value)} /> onChange('nationality', value)} />
onChange('identityDocumentNumber', value)} /> onChange('internationalLicenseNumber', value)} />