make the booking in many steps
Build & Deploy / Build & Push Docker Image (push) Successful in 8m27s
Test / Type Check (all packages) (push) Successful in 3m34s
Build & Deploy / Deploy to VPS (push) Successful in 2s
Test / API Unit Tests (push) Failing after 3m16s
Test / Homepage Unit Tests (push) Failing after 2m40s
Test / Storefront Unit Tests (push) Failing after 24s
Test / Admin Unit Tests (push) Failing after 22s
Test / Dashboard Unit Tests (push) Failing after 24s
Test / API Integration Tests (push) Failing after 33s

This commit is contained in:
root
2026-06-29 08:54:41 -04:00
parent ae10f5272f
commit 3d6607e6c8
20 changed files with 3894 additions and 836 deletions
@@ -1,808 +1,70 @@
'use client'
import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import { apiFetch } from '@/lib/api'
import { useDashboardI18n } from '@/components/I18nProvider'
import { BilingualField, BilingualInput, bilingualPrimary, emptyBilingual } from '@/components/ui/BilingualInput'
type Customer = {
id: string
firstName: string
lastName: string
email: string
phone?: string | null
driverLicense?: string | null
dateOfBirth?: string | null
nationality?: string | null
address?: Record<string, unknown> | null
licenseExpiry?: string | null
licenseIssuedAt?: string | null
licenseCountry?: string | null
licenseNumber?: string | null
licenseCategory?: string | null
licenseImageUrl?: string | null
}
type Vehicle = { id: string; make: string; model: string; licensePlate: string; status: string }
type AdditionalDriverForm = {
firstName: BilingualField
lastName: BilingualField
email: string
phone: string
driverLicense: string
licenseExpiry: string
licenseIssuedAt: string
dateOfBirth: string
nationality: BilingualField
}
type CreatedReservation = { id: string }
import { ReservationWizard } from '@/components/reservations/new/ReservationWizard'
import { getReservationWizardCopy } from '@/components/reservations/new/reservationWizard.copy'
import type { Customer, Vehicle } from '@/components/reservations/new/reservationWizard.types'
export default function NewReservationPage() {
const { language } = useDashboardI18n()
const router = useRouter()
const copy = getReservationWizardCopy(language)
const [customers, setCustomers] = useState<Customer[]>([])
const [vehicles, setVehicles] = useState<Vehicle[]>([])
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [error, setError] = useState<string | null>(null)
const [customerId, setCustomerId] = useState('')
const [customerSearch, setCustomerSearch] = useState('')
const [vehicleId, setVehicleId] = useState('')
const [startDate, setStartDate] = useState('')
const [endDate, setEndDate] = useState('')
const [pickupLocation, setPickupLocation] = useState('')
const [returnLocation, setReturnLocation] = useState('')
const [depositAmount, setDepositAmount] = useState('0')
const [paymentMode, setPaymentMode] = useState('CASH')
const [notes, setNotes] = useState('')
const [showAddCustomer, setShowAddCustomer] = useState(false)
const [savingCustomer, setSavingCustomer] = useState(false)
const [includeAdditionalDriver, setIncludeAdditionalDriver] = useState(false)
const [newCustomerFirstName, setNewCustomerFirstName] = useState<BilingualField>(emptyBilingual())
const [newCustomerLastName, setNewCustomerLastName] = useState<BilingualField>(emptyBilingual())
const [newCustomerEmail, setNewCustomerEmail] = useState('')
const [newCustomerPhone, setNewCustomerPhone] = useState('')
const [driverLicense, setDriverLicense] = useState('')
const [customerDateOfBirth, setCustomerDateOfBirth] = useState('')
const [customerNationality, setCustomerNationality] = useState('')
const [customerFullAddress, setCustomerFullAddress] = useState('')
const [customerIdentityDocumentNumber, setCustomerIdentityDocumentNumber] = useState('')
const [customerInternationalLicenseNumber, setCustomerInternationalLicenseNumber] = useState('')
const [licenseExpiry, setLicenseExpiry] = useState('')
const [licenseIssuedAt, setLicenseIssuedAt] = useState('')
const [licenseCountry, setLicenseCountry] = useState('')
const [licenseCategory, setLicenseCategory] = useState('')
const [licenseImageUrl, setLicenseImageUrl] = useState<string | null>(null)
const [licenseImageFile, setLicenseImageFile] = useState<File | null>(null)
const [licenseImagePreviewUrl, setLicenseImagePreviewUrl] = useState<string | null>(null)
const [additionalDriver, setAdditionalDriver] = useState<AdditionalDriverForm>({
firstName: emptyBilingual(),
lastName: emptyBilingual(),
email: '',
phone: '',
driverLicense: '',
licenseExpiry: '',
licenseIssuedAt: '',
dateOfBirth: '',
nationality: emptyBilingual(),
})
const copy = {
en: {
title: 'Local Booking',
subtitle: 'Create a reservation directly from workspace without a previous online reservation.',
customer: 'Customer',
searchCustomer: 'Search previous customer',
vehicle: 'Vehicle',
startDate: 'Start date & time',
endDate: 'End date & time',
pickup: 'Pickup location',
return: 'Return location',
deposit: 'Deposit amount (MAD)',
paymentMode: 'Payment mode',
renterIdentity: 'Renter identity',
driverLicenseInfo: 'Driver license information',
driverLicense: 'Driver license number',
licenseExpiry: 'License expiry',
licenseIssuedAt: 'License issued at',
licenseCountry: 'License country',
licenseCategory: 'License category',
fullAddress: 'Full address',
identityDocumentNumber: 'CIN / Passport number',
internationalLicenseNumber: 'International permit number',
licenseImage: 'Driver license image',
licenseImageHint: 'Upload a photo or scan of the primary driver license.',
licenseImageSelected: 'Selected file',
licenseImageCurrent: 'Current image',
noLicenseImage: 'No license image uploaded yet.',
addAdditionalDriver: 'Add additional driver',
additionalDriverInfo: 'Additional driver',
dateOfBirth: 'Date of birth',
nationality: 'Nationality',
notes: 'Notes',
notesPlaceholder: 'Optional notes…',
create: 'Create booking',
creating: 'Creating…',
cancel: 'Cancel',
loadFailed: 'Failed to load customers/vehicles.',
invalidDates: 'End date must be after start date.',
required: 'Please fill all required fields.',
selectCustomer: 'Select customer…',
addCustomer: 'Add customer',
addCustomerTitle: 'Add new customer',
firstName: 'First name',
lastName: 'Last name',
email: 'Email',
phone: 'Phone',
saveCustomer: 'Save customer',
savingCustomer: 'Saving customer…',
selectVehicle: 'Select vehicle…',
noVehicles: 'No available vehicles.',
paymentModes: {
CASH: 'Cash',
CARD: 'Card',
BANK_TRANSFER: 'Bank transfer',
AMANPAY: 'AmanPay',
PAYPAL: 'PayPal',
} as Record<string, string>,
},
fr: {
title: 'Réservation locale',
subtitle: 'Créez une réservation directement depuis lespace, sans réservation en ligne préalable.',
customer: 'Client',
searchCustomer: 'Rechercher un client existant',
vehicle: 'Véhicule',
startDate: 'Date et heure de départ',
endDate: 'Date et heure de retour',
pickup: 'Lieu de départ',
return: 'Lieu de retour',
deposit: 'Montant du dépôt (MAD)',
paymentMode: 'Mode de paiement',
renterIdentity: 'Identité du locataire',
driverLicenseInfo: 'Informations du permis',
driverLicense: 'Numéro du permis',
licenseExpiry: 'Expiration du permis',
licenseIssuedAt: 'Permis délivré le',
licenseCountry: 'Pays du permis',
licenseCategory: 'Catégorie du permis',
fullAddress: 'Adresse complète',
identityDocumentNumber: 'N° CIN / passeport',
internationalLicenseNumber: 'N° de permis international',
licenseImage: 'Image du permis',
licenseImageHint: 'Téléversez une photo ou un scan du permis du conducteur principal.',
licenseImageSelected: 'Fichier sélectionné',
licenseImageCurrent: 'Image actuelle',
noLicenseImage: 'Aucune image de permis téléversée.',
addAdditionalDriver: 'Ajouter un conducteur supplémentaire',
additionalDriverInfo: 'Conducteur supplémentaire',
dateOfBirth: 'Date de naissance',
nationality: 'Nationalité',
notes: 'Notes',
notesPlaceholder: 'Notes optionnelles…',
create: 'Créer la réservation',
creating: 'Création…',
cancel: 'Annuler',
loadFailed: 'Échec du chargement des clients/véhicules.',
invalidDates: 'La date de fin doit être après la date de début.',
required: 'Veuillez remplir tous les champs requis.',
selectCustomer: 'Sélectionner un client…',
addCustomer: 'Ajouter un client',
addCustomerTitle: 'Ajouter un nouveau client',
firstName: 'Prénom',
lastName: 'Nom',
email: 'Email',
phone: 'Téléphone',
saveCustomer: 'Enregistrer le client',
savingCustomer: 'Enregistrement du client…',
selectVehicle: 'Sélectionner un véhicule…',
noVehicles: 'Aucun véhicule disponible.',
paymentModes: {
CASH: 'Espèces',
CARD: 'Carte',
BANK_TRANSFER: 'Virement',
AMANPAY: 'AmanPay',
PAYPAL: 'PayPal',
} as Record<string, string>,
},
ar: {
title: 'حجز محلي',
subtitle: 'أنشئ حجزًا مباشرة من مساحة العمل بدون حجز إلكتروني مسبق.',
customer: 'العميل',
searchCustomer: 'ابحث عن عميل سابق',
vehicle: 'المركبة',
startDate: 'تاريخ ووقت البداية',
endDate: 'تاريخ ووقت النهاية',
pickup: 'موقع الاستلام',
return: 'موقع التسليم',
deposit: 'مبلغ العربون (MAD)',
paymentMode: 'طريقة الدفع',
renterIdentity: 'بيانات المستأجر',
driverLicenseInfo: 'معلومات رخصة القيادة',
driverLicense: 'رقم رخصة القيادة',
licenseExpiry: 'تاريخ انتهاء الرخصة',
licenseIssuedAt: 'تاريخ إصدار الرخصة',
licenseCountry: 'بلد الرخصة',
licenseCategory: 'فئة الرخصة',
fullAddress: 'العنوان الكامل',
identityDocumentNumber: 'رقم البطاقة الوطنية / جواز السفر',
internationalLicenseNumber: 'رقم الرخصة الدولية',
licenseImage: 'صورة رخصة القيادة',
licenseImageHint: 'ارفع صورة أو مسحًا لرخصة السائق الأساسي.',
licenseImageSelected: 'الملف المحدد',
licenseImageCurrent: 'الصورة الحالية',
noLicenseImage: 'لم يتم رفع صورة الرخصة بعد.',
addAdditionalDriver: 'إضافة سائق إضافي',
additionalDriverInfo: 'السائق الإضافي',
dateOfBirth: 'تاريخ الميلاد',
nationality: 'الجنسية',
notes: 'ملاحظات',
notesPlaceholder: 'ملاحظات اختيارية…',
create: 'إنشاء الحجز',
creating: 'جارٍ الإنشاء…',
cancel: 'إلغاء',
loadFailed: 'فشل تحميل العملاء/المركبات.',
invalidDates: 'يجب أن يكون تاريخ النهاية بعد تاريخ البداية.',
required: 'يرجى تعبئة جميع الحقول المطلوبة.',
selectCustomer: 'اختر عميلًا…',
addCustomer: 'إضافة عميل',
addCustomerTitle: 'إضافة عميل جديد',
firstName: 'الاسم الأول',
lastName: 'اسم العائلة',
email: 'البريد الإلكتروني',
phone: 'الهاتف',
saveCustomer: 'حفظ العميل',
savingCustomer: 'جارٍ حفظ العميل…',
selectVehicle: 'اختر مركبة…',
noVehicles: 'لا توجد مركبات متاحة.',
paymentModes: {
CASH: 'نقدا',
CARD: 'بطاقة',
BANK_TRANSFER: 'تحويل بنكي',
AMANPAY: 'AmanPay',
PAYPAL: 'PayPal',
} as Record<string, string>,
},
}[language]
const [customerLoadError, setCustomerLoadError] = useState<string | null>(null)
const [vehicleLoadError, setVehicleLoadError] = useState<string | null>(null)
useEffect(() => {
Promise.all([
apiFetch<Customer[]>('/customers?pageSize=100'),
apiFetch<Vehicle[]>('/vehicles?pageSize=100'),
])
.then(([c, v]) => {
setCustomers(c ?? [])
setVehicles(v ?? [])
})
.catch((err) => setError(err.message ?? copy.loadFailed))
.finally(() => setLoading(false))
}, [])
let cancelled = false
const filteredCustomers = (() => {
const q = customerSearch.trim().toLowerCase()
if (!q) return customers
return customers.filter((c) =>
`${c.firstName} ${c.lastName}`.toLowerCase().includes(q) ||
c.email.toLowerCase().includes(q),
async function loadData() {
setLoading(true)
const [customerResult, vehicleResult] = await Promise.allSettled([
apiFetch<Customer[]>('/customers?pageSize=100'),
apiFetch<Vehicle[]>('/vehicles?pageSize=100'),
])
if (cancelled) return
if (customerResult.status === 'fulfilled') {
setCustomers(customerResult.value ?? [])
setCustomerLoadError(null)
} else {
setCustomerLoadError(customerResult.reason?.message ?? copy.loadFailed)
}
if (vehicleResult.status === 'fulfilled') {
setVehicles(vehicleResult.value ?? [])
setVehicleLoadError(null)
} else {
setVehicleLoadError(vehicleResult.reason?.message ?? copy.loadFailed)
}
setLoading(false)
}
void loadData()
return () => {
cancelled = true
}
}, [copy.loadFailed])
if (loading) {
return (
<div className="card max-w-3xl p-6">
<p className="text-sm text-slate-500">{copy.loading}</p>
</div>
)
})()
const availableVehicles = vehicles.filter((v) => v.status === 'AVAILABLE')
const canSubmit = !!customerId && !!vehicleId && !!startDate && !!endDate
function readCustomerAddressValue(customer: Customer | undefined, key: string) {
const address = customer?.address
if (!address || typeof address !== 'object' || Array.isArray(address)) return ''
const value = address[key]
return typeof value === 'string' ? value : ''
}
useEffect(() => {
const selected = customers.find((customer) => customer.id === customerId)
if (!selected) return
setDriverLicense(selected.driverLicense ?? selected.licenseNumber ?? '')
setCustomerDateOfBirth(selected.dateOfBirth ? selected.dateOfBirth.slice(0, 10) : '')
setCustomerNationality(selected.nationality ?? '')
setCustomerFullAddress(readCustomerAddressValue(selected, 'fullAddress'))
setCustomerIdentityDocumentNumber(readCustomerAddressValue(selected, 'identityDocumentNumber'))
setCustomerInternationalLicenseNumber(readCustomerAddressValue(selected, 'internationalLicenseNumber'))
setLicenseExpiry(selected.licenseExpiry ? selected.licenseExpiry.slice(0, 10) : '')
setLicenseIssuedAt(selected.licenseIssuedAt ? selected.licenseIssuedAt.slice(0, 10) : '')
setLicenseCountry(selected.licenseCountry ?? '')
setLicenseCategory(selected.licenseCategory ?? '')
setLicenseImageUrl(selected.licenseImageUrl ?? null)
setLicenseImageFile(null)
}, [customerId, customers])
useEffect(() => {
if (!licenseImageFile) {
setLicenseImagePreviewUrl(null)
return
}
const objectUrl = URL.createObjectURL(licenseImageFile)
setLicenseImagePreviewUrl(objectUrl)
return () => URL.revokeObjectURL(objectUrl)
}, [licenseImageFile])
async function uploadLicenseImage(customerIdToUpdate: string) {
if (!licenseImageFile) return null
const formData = new FormData()
formData.append('file', licenseImageFile)
const updated = await apiFetch<Customer>(`/customers/${customerIdToUpdate}/license-image`, {
method: 'POST',
body: formData,
})
setCustomers((prev) => prev.map((customer) => customer.id === updated.id ? { ...customer, ...updated } : customer))
setLicenseImageUrl(updated.licenseImageUrl ?? null)
setLicenseImageFile(null)
return updated.licenseImageUrl ?? null
}
async function addCustomer() {
setError(null)
if (!bilingualPrimary(newCustomerFirstName).trim() || !bilingualPrimary(newCustomerLastName).trim() || !newCustomerEmail.trim() || !newCustomerPhone.trim()) {
setError(copy.required)
return
}
if (
!driverLicense.trim() ||
!customerDateOfBirth ||
!customerNationality.trim() ||
!customerFullAddress.trim() ||
!customerIdentityDocumentNumber.trim() ||
!licenseExpiry ||
!licenseIssuedAt ||
!licenseCountry.trim() ||
!licenseCategory.trim()
) {
setError(copy.required)
return
}
setSavingCustomer(true)
try {
const created = await apiFetch<Customer>('/customers', {
method: 'POST',
body: JSON.stringify({
firstName: bilingualPrimary(newCustomerFirstName).trim(),
firstNameAr: newCustomerFirstName.ar.trim() || undefined,
lastName: bilingualPrimary(newCustomerLastName).trim(),
lastNameAr: newCustomerLastName.ar.trim() || undefined,
email: newCustomerEmail.trim(),
phone: newCustomerPhone.trim(),
driverLicense: driverLicense.trim(),
dateOfBirth: new Date(customerDateOfBirth).toISOString(),
nationality: customerNationality.trim(),
address: {
fullAddress: customerFullAddress.trim(),
identityDocumentNumber: customerIdentityDocumentNumber.trim(),
internationalLicenseNumber: customerInternationalLicenseNumber.trim() || undefined,
},
licenseExpiry: new Date(licenseExpiry).toISOString(),
licenseIssuedAt: new Date(licenseIssuedAt).toISOString(),
licenseCountry: licenseCountry.trim(),
licenseNumber: driverLicense.trim(),
licenseCategory: licenseCategory.trim(),
}),
})
setCustomers((prev) => [created, ...prev])
setCustomerId(created.id)
setCustomerSearch(`${bilingualPrimary(newCustomerFirstName)} ${bilingualPrimary(newCustomerLastName)}`)
setShowAddCustomer(false)
setNewCustomerFirstName(emptyBilingual())
setNewCustomerLastName(emptyBilingual())
setNewCustomerEmail('')
setNewCustomerPhone('')
setLicenseImageUrl(created.licenseImageUrl ?? null)
if (licenseImageFile) {
await uploadLicenseImage(created.id)
}
} catch (err: any) {
setError(err.message)
} finally {
setSavingCustomer(false)
}
}
async function submit() {
setError(null)
if (!canSubmit) {
setError(copy.required)
return
}
const start = new Date(startDate)
const end = new Date(endDate)
if (end <= start) {
setError(copy.invalidDates)
return
}
if (
!driverLicense.trim() ||
!customerDateOfBirth ||
!customerNationality.trim() ||
!customerFullAddress.trim() ||
!customerIdentityDocumentNumber.trim() ||
!licenseExpiry ||
!licenseIssuedAt ||
!licenseCountry.trim() ||
!licenseCategory.trim()
) {
setError(copy.required)
return
}
if (includeAdditionalDriver && (!bilingualPrimary(additionalDriver.firstName).trim() || !bilingualPrimary(additionalDriver.lastName).trim() || !additionalDriver.driverLicense.trim())) {
setError(copy.required)
return
}
setSaving(true)
try {
await apiFetch(`/customers/${customerId}`, {
method: 'PATCH',
body: JSON.stringify({
driverLicense: driverLicense.trim(),
dateOfBirth: new Date(customerDateOfBirth).toISOString(),
nationality: customerNationality.trim(),
address: {
fullAddress: customerFullAddress.trim(),
identityDocumentNumber: customerIdentityDocumentNumber.trim(),
internationalLicenseNumber: customerInternationalLicenseNumber.trim() || undefined,
},
licenseExpiry: new Date(licenseExpiry).toISOString(),
licenseIssuedAt: new Date(licenseIssuedAt).toISOString(),
licenseCountry: licenseCountry.trim(),
licenseNumber: driverLicense.trim(),
licenseCategory: licenseCategory.trim(),
}),
})
if (licenseImageFile) {
await uploadLicenseImage(customerId)
}
const created = await apiFetch<CreatedReservation>('/reservations', {
method: 'POST',
body: JSON.stringify({
customerId,
vehicleId,
startDate: start.toISOString(),
endDate: end.toISOString(),
pickupLocation: pickupLocation || undefined,
returnLocation: returnLocation || undefined,
depositAmount: Number.isFinite(Number(depositAmount)) ? Math.max(0, Math.round(Number(depositAmount))) : 0,
paymentMode,
additionalDrivers: includeAdditionalDriver ? [{
firstName: bilingualPrimary(additionalDriver.firstName).trim(),
firstNameAr: additionalDriver.firstName.ar.trim() || undefined,
lastName: bilingualPrimary(additionalDriver.lastName).trim(),
lastNameAr: additionalDriver.lastName.ar.trim() || undefined,
email: additionalDriver.email.trim() || undefined,
phone: additionalDriver.phone.trim() || undefined,
driverLicense: additionalDriver.driverLicense.trim(),
licenseExpiry: additionalDriver.licenseExpiry ? new Date(additionalDriver.licenseExpiry).toISOString() : undefined,
licenseIssuedAt: additionalDriver.licenseIssuedAt ? new Date(additionalDriver.licenseIssuedAt).toISOString() : undefined,
dateOfBirth: additionalDriver.dateOfBirth ? new Date(additionalDriver.dateOfBirth).toISOString() : undefined,
nationality: bilingualPrimary(additionalDriver.nationality).trim() || undefined,
nationalityAr: additionalDriver.nationality.ar.trim() || undefined,
}] : [],
notes: notes || undefined,
}),
})
router.push(`/reservations/${created.id}`)
} catch (err: any) {
setError(err.message)
} finally {
setSaving(false)
}
}
return (
<div className="space-y-6 max-w-3xl">
<div className="flex items-start justify-between gap-3">
<div>
<h2 className="text-xl font-semibold text-slate-900">{copy.title}</h2>
<p className="text-sm text-slate-500 mt-1">{copy.subtitle}</p>
</div>
</div>
{error ? <div className="card p-4 text-sm text-red-700">{error}</div> : null}
<div className="card p-6 space-y-4">
{loading ? (
<p className="text-sm text-slate-500">Loading</p>
) : (
<>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<label className="space-y-1">
<span className="text-sm font-medium text-slate-700">{copy.customer} <span className="text-red-600">*</span></span>
<input
value={customerSearch}
onChange={(e) => setCustomerSearch(e.target.value)}
placeholder={copy.searchCustomer}
className="input-field mb-2"
/>
<select value={customerId} onChange={(e) => setCustomerId(e.target.value)} className="input-field">
<option value="">{copy.selectCustomer}</option>
{filteredCustomers.map((c) => (
<option key={c.id} value={c.id}>{c.firstName} {c.lastName} · {c.email}</option>
))}
</select>
<button
type="button"
className="text-xs font-semibold text-blue-700 hover:underline mt-1"
onClick={() => setShowAddCustomer((v) => !v)}
>
{copy.addCustomer}
</button>
{showAddCustomer ? (
<div className="mt-2 rounded-lg border border-slate-200 p-3 space-y-2 bg-slate-50">
<p className="text-xs font-semibold text-slate-700">{copy.addCustomerTitle}</p>
<BilingualInput label={copy.firstName} required value={newCustomerFirstName} onChange={setNewCustomerFirstName} />
<BilingualInput label={copy.lastName} required value={newCustomerLastName} onChange={setNewCustomerLastName} />
<div className="space-y-1">
<span className="text-sm font-medium text-slate-700">{copy.email} <span className="text-red-600">*</span></span>
<input value={newCustomerEmail} onChange={(e) => setNewCustomerEmail(e.target.value)} className="input-field" />
</div>
<div className="space-y-1">
<span className="text-sm font-medium text-slate-700">{copy.phone} <span className="text-red-600">*</span></span>
<input required value={newCustomerPhone} onChange={(e) => setNewCustomerPhone(e.target.value)} className="input-field" />
</div>
<div className="flex justify-end">
<button type="button" className="btn-secondary" onClick={addCustomer} disabled={savingCustomer}>
{savingCustomer ? copy.savingCustomer : copy.saveCustomer}
</button>
</div>
</div>
) : null}
</label>
<label className="space-y-1">
<span className="text-sm font-medium text-slate-700">{copy.vehicle} <span className="text-red-600">*</span></span>
<select value={vehicleId} onChange={(e) => setVehicleId(e.target.value)} className="input-field">
<option value="">{copy.selectVehicle}</option>
{availableVehicles.map((v) => (
<option key={v.id} value={v.id}>{v.make} {v.model} · {v.licensePlate}</option>
))}
</select>
{vehicles.length === 0 ? (
<span className="text-xs text-orange-700">{copy.noVehicles}</span>
) : availableVehicles.length === 0 ? (
<span className="text-xs text-orange-700">
{language === 'fr'
? 'Des véhicules existent dans la flotte, mais aucun nest marqué Disponible.'
: language === 'ar'
? 'توجد مركبات في الأسطول، لكن لا توجد مركبة بحالة متاحة.'
: 'Vehicles exist in fleet, but none are marked Available.'}
</span>
) : null}
</label>
</div>
<div className="rounded-xl border border-slate-200 p-4 space-y-4">
<p className="text-sm font-semibold text-slate-900">{copy.renterIdentity}</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<label className="space-y-1">
<span className="text-sm font-medium text-slate-700">{copy.dateOfBirth} <span className="text-red-600">*</span></span>
<input type="date" value={customerDateOfBirth} onChange={(e) => setCustomerDateOfBirth(e.target.value)} className="input-field" />
</label>
<label className="space-y-1">
<span className="text-sm font-medium text-slate-700">{copy.nationality} <span className="text-red-600">*</span></span>
<input value={customerNationality} onChange={(e) => setCustomerNationality(e.target.value)} className="input-field" />
</label>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<label className="space-y-1">
<span className="text-sm font-medium text-slate-700">{copy.identityDocumentNumber} <span className="text-red-600">*</span></span>
<input value={customerIdentityDocumentNumber} onChange={(e) => setCustomerIdentityDocumentNumber(e.target.value)} className="input-field" />
</label>
<label className="space-y-1">
<span className="text-sm font-medium text-slate-700">{copy.internationalLicenseNumber}</span>
<input value={customerInternationalLicenseNumber} onChange={(e) => setCustomerInternationalLicenseNumber(e.target.value)} className="input-field" />
</label>
</div>
<label className="space-y-1 block">
<span className="text-sm font-medium text-slate-700">{copy.fullAddress} <span className="text-red-600">*</span></span>
<textarea value={customerFullAddress} onChange={(e) => setCustomerFullAddress(e.target.value)} className="input-field min-h-[88px]" />
</label>
</div>
<div className="rounded-xl border border-slate-200 p-4 space-y-4">
<p className="text-sm font-semibold text-slate-900">{copy.driverLicenseInfo}</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<label className="space-y-1">
<span className="text-sm font-medium text-slate-700">{copy.driverLicense} <span className="text-red-600">*</span></span>
<input value={driverLicense} onChange={(e) => setDriverLicense(e.target.value)} className="input-field" />
</label>
<label className="space-y-1">
<span className="text-sm font-medium text-slate-700">{copy.licenseCountry} <span className="text-red-600">*</span></span>
<input value={licenseCountry} onChange={(e) => setLicenseCountry(e.target.value)} className="input-field" />
</label>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<label className="space-y-1">
<span className="text-sm font-medium text-slate-700">{copy.licenseIssuedAt} <span className="text-red-600">*</span></span>
<input type="date" value={licenseIssuedAt} onChange={(e) => setLicenseIssuedAt(e.target.value)} className="input-field" />
</label>
<label className="space-y-1">
<span className="text-sm font-medium text-slate-700">{copy.licenseExpiry} <span className="text-red-600">*</span></span>
<input type="date" value={licenseExpiry} onChange={(e) => setLicenseExpiry(e.target.value)} className="input-field" />
</label>
<label className="space-y-1">
<span className="text-sm font-medium text-slate-700">{copy.licenseCategory} <span className="text-red-600">*</span></span>
<input value={licenseCategory} onChange={(e) => setLicenseCategory(e.target.value)} className="input-field" />
</label>
</div>
<div className="space-y-2">
<label className="space-y-1 block">
<span className="text-sm font-medium text-slate-700">{copy.licenseImage}</span>
<input
type="file"
accept="image/*"
onChange={(e) => setLicenseImageFile(e.target.files?.[0] ?? null)}
className="input-field"
/>
</label>
<p className="text-xs text-slate-500">{copy.licenseImageHint}</p>
{licenseImageFile ? (
<p className="text-xs font-medium text-slate-700">{copy.licenseImageSelected}: {licenseImageFile.name}</p>
) : licenseImageUrl ? (
<p className="text-xs font-medium text-slate-700">{copy.licenseImageCurrent}</p>
) : (
<p className="text-xs text-slate-500">{copy.noLicenseImage}</p>
)}
{licenseImagePreviewUrl || licenseImageUrl ? (
<img
src={licenseImagePreviewUrl ?? licenseImageUrl ?? ''}
alt="Driver license preview"
className="h-40 w-full max-w-sm rounded-xl border border-slate-200 object-cover"
/>
) : null}
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<label className="space-y-1">
<span className="text-sm font-medium text-slate-700">{copy.startDate} <span className="text-red-600">*</span></span>
<input type="datetime-local" value={startDate} onChange={(e) => setStartDate(e.target.value)} className="input-field" />
</label>
<label className="space-y-1">
<span className="text-sm font-medium text-slate-700">{copy.endDate} <span className="text-red-600">*</span></span>
<input type="datetime-local" value={endDate} onChange={(e) => setEndDate(e.target.value)} className="input-field" />
</label>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<label className="space-y-1">
<span className="text-sm font-medium text-slate-700">{copy.pickup}</span>
<input value={pickupLocation} onChange={(e) => setPickupLocation(e.target.value)} className="input-field" />
</label>
<label className="space-y-1">
<span className="text-sm font-medium text-slate-700">{copy.return}</span>
<input value={returnLocation} onChange={(e) => setReturnLocation(e.target.value)} className="input-field" />
</label>
</div>
<label className="space-y-1 block">
<span className="text-sm font-medium text-slate-700">{copy.deposit}</span>
<input type="number" min={0} value={depositAmount} onChange={(e) => setDepositAmount(e.target.value)} className="input-field" />
</label>
<label className="space-y-1 block">
<span className="text-sm font-medium text-slate-700">{copy.paymentMode}</span>
<select value={paymentMode} onChange={(e) => setPaymentMode(e.target.value)} className="input-field">
{(['CASH', 'CARD', 'BANK_TRANSFER', 'AMANPAY', 'PAYPAL'] as const).map((mode) => (
<option key={mode} value={mode}>{copy.paymentModes[mode]}</option>
))}
</select>
</label>
<div className="rounded-xl border border-slate-200 p-4 space-y-4">
<label className="flex items-center gap-3">
<input
type="checkbox"
checked={includeAdditionalDriver}
onChange={(e) => setIncludeAdditionalDriver(e.target.checked)}
className="h-4 w-4 rounded border-slate-300 text-blue-600"
/>
<span className="text-sm font-medium text-slate-900">{copy.addAdditionalDriver}</span>
</label>
{includeAdditionalDriver ? (
<div className="space-y-4">
<p className="text-sm font-semibold text-slate-900">{copy.additionalDriverInfo}</p>
<BilingualInput label={copy.firstName} required value={additionalDriver.firstName} onChange={(v) => setAdditionalDriver((cur) => ({ ...cur, firstName: v }))} />
<BilingualInput label={copy.lastName} required value={additionalDriver.lastName} onChange={(v) => setAdditionalDriver((cur) => ({ ...cur, lastName: v }))} />
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<label className="space-y-1">
<span className="text-sm font-medium text-slate-700">{copy.email}</span>
<input
value={additionalDriver.email}
onChange={(e) => setAdditionalDriver((current) => ({ ...current, email: e.target.value }))}
className="input-field"
/>
</label>
<label className="space-y-1">
<span className="text-sm font-medium text-slate-700">{copy.phone}</span>
<input
value={additionalDriver.phone}
onChange={(e) => setAdditionalDriver((current) => ({ ...current, phone: e.target.value }))}
className="input-field"
/>
</label>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<label className="space-y-1">
<span className="text-sm font-medium text-slate-700">{copy.driverLicense} <span className="text-red-600">*</span></span>
<input
value={additionalDriver.driverLicense}
onChange={(e) => setAdditionalDriver((current) => ({ ...current, driverLicense: e.target.value }))}
className="input-field"
/>
</label>
<div className="space-y-1">
<BilingualInput label={copy.nationality} value={additionalDriver.nationality} onChange={(v) => setAdditionalDriver((cur) => ({ ...cur, nationality: v }))} />
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<label className="space-y-1">
<span className="text-sm font-medium text-slate-700">{copy.dateOfBirth}</span>
<input
type="date"
value={additionalDriver.dateOfBirth}
onChange={(e) => setAdditionalDriver((current) => ({ ...current, dateOfBirth: e.target.value }))}
className="input-field"
/>
</label>
<label className="space-y-1">
<span className="text-sm font-medium text-slate-700">{copy.licenseIssuedAt}</span>
<input
type="date"
value={additionalDriver.licenseIssuedAt}
onChange={(e) => setAdditionalDriver((current) => ({ ...current, licenseIssuedAt: e.target.value }))}
className="input-field"
/>
</label>
<label className="space-y-1">
<span className="text-sm font-medium text-slate-700">{copy.licenseExpiry}</span>
<input
type="date"
value={additionalDriver.licenseExpiry}
onChange={(e) => setAdditionalDriver((current) => ({ ...current, licenseExpiry: e.target.value }))}
className="input-field"
/>
</label>
</div>
</div>
) : null}
</div>
<label className="space-y-1 block">
<span className="text-sm font-medium text-slate-700">{copy.notes}</span>
<textarea value={notes} onChange={(e) => setNotes(e.target.value)} className="input-field min-h-[96px]" placeholder={copy.notesPlaceholder} />
</label>
<div className="pt-2 flex items-center justify-end gap-3">
<button type="button" className="btn-secondary" onClick={() => router.push('/reservations')}>
{copy.cancel}
</button>
<button type="button" className="btn-primary" onClick={submit} disabled={saving || !canSubmit}>
{saving ? copy.creating : copy.create}
</button>
</div>
</>
)}
</div>
<div className="space-y-4">
{customerLoadError ? <div className="card max-w-3xl p-4 text-sm text-red-700">{customerLoadError}</div> : null}
{vehicleLoadError ? <div className="card max-w-3xl p-4 text-sm text-red-700">{vehicleLoadError}</div> : null}
<ReservationWizard customers={customers} vehicles={vehicles} language={language} />
</div>
)
}