From 3d6607e6c8585ca906177dd01ed0d7504f66344e Mon Sep 17 00:00:00 2001 From: root Date: Mon, 29 Jun 2026 08:54:41 -0400 Subject: [PATCH] make the booking in many steps --- .../api/src/modules/menu/menu.service.test.ts | 70 ++ .../app/(dashboard)/reservations/new/page.tsx | 836 +----------------- .../dashboard/src/components/I18nProvider.tsx | 21 +- .../layout/Sidebar.boundary.test.ts | 14 +- .../src/components/layout/Sidebar.tsx | 53 +- .../reservations/new/ReservationReview.tsx | 126 +++ .../reservations/new/ReservationStepper.tsx | 59 ++ .../reservations/new/ReservationWizard.tsx | 710 +++++++++++++++ .../new/reservationWizard.copy.ts | 269 ++++++ .../new/reservationWizard.reducer.ts | 163 ++++ .../new/reservationWizard.schemas.ts | 138 +++ .../new/reservationWizard.submit.test.ts | 83 ++ .../new/reservationWizard.submit.ts | 139 +++ .../new/reservationWizard.test.ts | 167 ++++ .../new/reservationWizard.types.ts | 91 ++ docker/scripts/dev-bootstrap.sh | 8 + docs/ProgressiveReservationCreation.md | 634 +++++++++++++ ...o_Menu_Subscription_Implementation_Plan.md | 775 ++++++++++++++++ package-lock.json | 120 +++ .../migration.sql | 254 ++++++ 20 files changed, 3894 insertions(+), 836 deletions(-) create mode 100644 apps/dashboard/src/components/reservations/new/ReservationReview.tsx create mode 100644 apps/dashboard/src/components/reservations/new/ReservationStepper.tsx create mode 100644 apps/dashboard/src/components/reservations/new/ReservationWizard.tsx create mode 100644 apps/dashboard/src/components/reservations/new/reservationWizard.copy.ts create mode 100644 apps/dashboard/src/components/reservations/new/reservationWizard.reducer.ts create mode 100644 apps/dashboard/src/components/reservations/new/reservationWizard.schemas.ts create mode 100644 apps/dashboard/src/components/reservations/new/reservationWizard.submit.test.ts create mode 100644 apps/dashboard/src/components/reservations/new/reservationWizard.submit.ts create mode 100644 apps/dashboard/src/components/reservations/new/reservationWizard.test.ts create mode 100644 apps/dashboard/src/components/reservations/new/reservationWizard.types.ts create mode 100644 docs/ProgressiveReservationCreation.md create mode 100644 docs/RentalDriveGo_Menu_Subscription_Implementation_Plan.md create mode 100644 packages/database/prisma/migrations/20260629110000_align_dashboard_menu_entitlements/migration.sql diff --git a/apps/api/src/modules/menu/menu.service.test.ts b/apps/api/src/modules/menu/menu.service.test.ts index e81f918..f726d4c 100644 --- a/apps/api/src/modules/menu/menu.service.test.ts +++ b/apps/api/src/modules/menu/menu.service.test.ts @@ -198,4 +198,74 @@ describe('menu.service', () => { }) expect(result.items.find((item) => item.label === 'Reports')?.reasons.join(' ')).toContain('STARTER does not include this menu item') }) + + it('returns the approved seven-item STARTER owner sidebar in order', async () => { + vi.mocked(prisma.employee.findUniqueOrThrow).mockResolvedValue({ + id: 'employee_1', + role: 'OWNER', + isActive: true, + companyId: 'company_1', + } as never) + + vi.mocked(prisma.company.findUniqueOrThrow).mockResolvedValue({ + id: 'company_1', + name: 'Atlas Cars', + status: 'ACTIVE', + subscription: { plan: 'STARTER', status: 'ACTIVE' }, + } as never) + + const item = ( + systemKey: string, + label: string, + routeOrUrl: string, + icon: string, + displayOrder: number, + plans: string[], + ) => ({ + id: `item_${systemKey}`, + systemKey, + label, + itemType: 'INTERNAL_PAGE', + routeOrUrl, + icon, + parentId: null, + openInNewTab: false, + isRequired: displayOrder <= 70, + isActive: true, + displayOrder, + roleVisibilities: [{ role: 'OWNER' }], + subscriptionAssignments: plans.map((plan) => ({ plan, displayOrder, isActive: true })), + companyAssignments: [], + }) + + vi.mocked(prisma.menuItem.findMany).mockResolvedValue([ + item('dashboard', 'Dashboard', '/', 'LayoutDashboard', 10, ['STARTER', 'GROWTH', 'PRO']), + item('reservations', 'Reservations', '/reservations', 'Calendar', 20, ['STARTER', 'GROWTH', 'PRO']), + item('fleet', 'Fleet', '/fleet', 'Car', 30, ['STARTER', 'GROWTH', 'PRO']), + item('customers', 'Customers', '/customers', 'Users', 40, ['STARTER', 'GROWTH', 'PRO']), + item('reports', 'Reports', '/reports', 'BarChart2', 50, ['STARTER', 'GROWTH', 'PRO']), + item('billing', 'Billing', '/billing', 'CreditCard', 60, ['STARTER', 'GROWTH', 'PRO']), + item('settings', 'Settings', '/settings', 'Settings', 70, ['STARTER', 'GROWTH', 'PRO']), + item('online-reservations', 'Online Reservations', '/online-reservations', 'Globe', 80, ['GROWTH', 'PRO']), + item('offers', 'Offers', '/offers', 'Tag', 90, ['GROWTH', 'PRO']), + item('team', 'Team', '/team', 'UserPlus', 100, ['GROWTH', 'PRO']), + item('contracts', 'Contracts', '/contracts', 'FileText', 110, ['GROWTH', 'PRO']), + item('notifications', 'Notifications', '/notifications', 'Bell', 120, ['GROWTH', 'PRO']), + item('reviews', 'Reviews', '/reviews', 'Star', 130, ['PRO']), + item('complaints', 'Complaints', '/complaints', 'AlertTriangle', 140, ['PRO']), + { ...item('subscription', 'Subscription', '/subscription', 'CreditCard', 999, []), isActive: false }, + ] as never) + + const result = await getEmployeeMenu('employee_1') + + expect(result.items.map((menuItem) => menuItem.systemKey)).toEqual([ + 'dashboard', + 'reservations', + 'fleet', + 'customers', + 'reports', + 'billing', + 'settings', + ]) + }) }) diff --git a/apps/dashboard/src/app/(dashboard)/reservations/new/page.tsx b/apps/dashboard/src/app/(dashboard)/reservations/new/page.tsx index 4a4e4d3..bc4b3fd 100644 --- a/apps/dashboard/src/app/(dashboard)/reservations/new/page.tsx +++ b/apps/dashboard/src/app/(dashboard)/reservations/new/page.tsx @@ -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 | 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([]) const [vehicles, setVehicles] = useState([]) const [loading, setLoading] = useState(true) - const [saving, setSaving] = useState(false) - const [error, setError] = useState(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(emptyBilingual()) - const [newCustomerLastName, setNewCustomerLastName] = useState(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(null) - const [licenseImageFile, setLicenseImageFile] = useState(null) - const [licenseImagePreviewUrl, setLicenseImagePreviewUrl] = useState(null) - const [additionalDriver, setAdditionalDriver] = useState({ - 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, - }, - fr: { - title: 'Réservation locale', - subtitle: 'Créez une réservation directement depuis l’espace, 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, - }, - 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, - }, - }[language] + const [customerLoadError, setCustomerLoadError] = useState(null) + const [vehicleLoadError, setVehicleLoadError] = useState(null) useEffect(() => { - Promise.all([ - apiFetch('/customers?pageSize=100'), - apiFetch('/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('/customers?pageSize=100'), + apiFetch('/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 ( +
+

{copy.loading}

+
) - })() - - 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(`/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('/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('/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 ( -
-
-
-

{copy.title}

-

{copy.subtitle}

-
-
- - {error ?
{error}
: null} - -
- {loading ? ( -

Loading…

- ) : ( - <> -
- - - -
- -
-

{copy.renterIdentity}

-
- - -
-
- - -
-