diff --git a/apps/api/src/lib/storage/companies/cmpbosg0o0001bh8tc433zxt2/vehicles/c2c92f69332b855e21ffeb95229a4add.jpg b/apps/api/src/lib/storage/companies/cmpbosg0o0001bh8tc433zxt2/vehicles/c2c92f69332b855e21ffeb95229a4add.jpg new file mode 100644 index 0000000..bbc749a Binary files /dev/null and b/apps/api/src/lib/storage/companies/cmpbosg0o0001bh8tc433zxt2/vehicles/c2c92f69332b855e21ffeb95229a4add.jpg differ diff --git a/apps/api/src/routes/analytics.ts b/apps/api/src/routes/analytics.ts index 3e2dc3b..0821aa7 100644 --- a/apps/api/src/routes/analytics.ts +++ b/apps/api/src/routes/analytics.ts @@ -3,10 +3,11 @@ import { prisma } from '../lib/prisma' import { requireCompanyAuth } from '../middleware/requireCompanyAuth' import { requireTenant } from '../middleware/requireTenant' import { requireSubscription } from '../middleware/requireSubscription' +import { requireRole } from '../middleware/requireRole' import { generateFinancialReport, toCsv } from '../services/financialReportService' const router = Router() -router.use(requireCompanyAuth, requireTenant, requireSubscription) +router.use(requireCompanyAuth, requireTenant, requireSubscription, requireRole('MANAGER')) function getRangeFromPeriod(period: string) { const now = new Date() diff --git a/apps/api/src/routes/auth.company.ts b/apps/api/src/routes/auth.company.ts index 2890441..a41365c 100644 --- a/apps/api/src/routes/auth.company.ts +++ b/apps/api/src/routes/auth.company.ts @@ -21,6 +21,7 @@ const signupSchema = z.object({ companyPhone: z.string().min(1).max(80), fax: z.string().max(80).optional(), yearsActive: z.string().min(1).max(80), + preferredLanguage: z.enum(['en', 'fr', 'ar']).default('en'), plan: z.enum(['STARTER', 'GROWTH', 'PRO']), billingPeriod: z.enum(['MONTHLY', 'ANNUAL']), currency: z.enum(['MAD', 'USD', 'EUR']), @@ -196,6 +197,7 @@ router.post('/signup', async (req, res, next) => { phone: body.companyPhone, passwordHash, role: 'OWNER', + preferredLanguage: body.preferredLanguage, isActive: true, }, }) diff --git a/apps/api/src/routes/auth.employee.ts b/apps/api/src/routes/auth.employee.ts index 7ae6491..35497d2 100644 --- a/apps/api/src/routes/auth.employee.ts +++ b/apps/api/src/routes/auth.employee.ts @@ -97,6 +97,7 @@ router.get('/me', async (req, res, next) => { firstName: employee.firstName, lastName: employee.lastName, role: employee.role, + preferredLanguage: employee.preferredLanguage, companyId: employee.companyId, companyName: employee.company.name, companySlug: employee.company.slug, @@ -142,6 +143,7 @@ router.post('/login', async (req, res, next) => { firstName: employee.firstName, lastName: employee.lastName, role: employee.role, + preferredLanguage: employee.preferredLanguage, companyId: employee.companyId, companyName: employee.company.name, companySlug: employee.company.slug, diff --git a/apps/api/src/routes/companies.ts b/apps/api/src/routes/companies.ts index cd24da0..9e9a013 100644 --- a/apps/api/src/routes/companies.ts +++ b/apps/api/src/routes/companies.ts @@ -6,6 +6,7 @@ import { uploadImage } from '../lib/storage' import { requireCompanyAuth } from '../middleware/requireCompanyAuth' import { requireTenant } from '../middleware/requireTenant' import { requireSubscription } from '../middleware/requireSubscription' +import { requireRole } from '../middleware/requireRole' const router = Router() const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 10 * 1024 * 1024 } }) @@ -212,7 +213,7 @@ router.get('/me', async (req, res, next) => { } catch (err) { next(err) } }) -router.patch('/me', async (req, res, next) => { +router.patch('/me', requireRole('OWNER'), async (req, res, next) => { try { const body = companySchema.parse(req.body) const company = await prisma.company.update({ @@ -232,7 +233,7 @@ router.get('/me/brand', async (req, res, next) => { } catch (err) { next(err) } }) -router.patch('/me/brand', async (req, res, next) => { +router.patch('/me/brand', requireRole('OWNER'), async (req, res, next) => { try { const body = brandSchema.parse(req.body) const current = await prisma.brandSettings.findUnique({ where: { companyId: req.companyId } }) @@ -256,7 +257,7 @@ router.patch('/me/brand', async (req, res, next) => { } catch (err) { next(err) } }) -router.post('/me/brand/logo', upload.single('file'), async (req, res, next) => { +router.post('/me/brand/logo', requireRole('OWNER'), upload.single('file'), async (req, res, next) => { try { if (!req.file) { return res.status(400).json({ error: 'missing_file', message: 'A logo file is required', statusCode: 400 }) @@ -276,7 +277,7 @@ router.post('/me/brand/logo', upload.single('file'), async (req, res, next) => { } catch (err) { next(err) } }) -router.post('/me/brand/hero', upload.single('file'), async (req, res, next) => { +router.post('/me/brand/hero', requireRole('OWNER'), upload.single('file'), async (req, res, next) => { try { if (!req.file) { return res.status(400).json({ error: 'missing_file', message: 'A hero image file is required', statusCode: 400 }) @@ -296,7 +297,7 @@ router.post('/me/brand/hero', upload.single('file'), async (req, res, next) => { } catch (err) { next(err) } }) -router.post('/me/brand/subdomain/check', async (req, res, next) => { +router.post('/me/brand/subdomain/check', requireRole('OWNER'), async (req, res, next) => { try { const { subdomain } = z.object({ subdomain: z.string().min(3) }).parse(req.body) const existing = await prisma.brandSettings.findFirst({ @@ -306,7 +307,7 @@ router.post('/me/brand/subdomain/check', async (req, res, next) => { } catch (err) { next(err) } }) -router.post('/me/brand/custom-domain', async (req, res, next) => { +router.post('/me/brand/custom-domain', requireRole('OWNER'), async (req, res, next) => { try { const { customDomain } = z.object({ customDomain: z.string().min(3) }).parse(req.body) const normalized = customDomain.toLowerCase().trim() @@ -351,7 +352,7 @@ router.get('/me/brand/custom-domain/status', async (req, res, next) => { } catch (err) { next(err) } }) -router.delete('/me/brand/custom-domain', async (req, res, next) => { +router.delete('/me/brand/custom-domain', requireRole('OWNER'), async (req, res, next) => { try { const brand = await prisma.brandSettings.updateMany({ where: { companyId: req.companyId }, @@ -368,7 +369,7 @@ router.get('/me/contract-settings', async (req, res, next) => { } catch (err) { next(err) } }) -router.patch('/me/contract-settings', async (req, res, next) => { +router.patch('/me/contract-settings', requireRole('MANAGER'), async (req, res, next) => { try { const body = contractSettingsSchema.parse(req.body) const settings = await prisma.contractSettings.upsert({ @@ -390,7 +391,7 @@ router.get('/me/insurance-policies', async (req, res, next) => { } catch (err) { next(err) } }) -router.post('/me/insurance-policies', async (req, res, next) => { +router.post('/me/insurance-policies', requireRole('MANAGER'), async (req, res, next) => { try { const body = insurancePolicySchema.parse(req.body) const policy = await prisma.insurancePolicy.create({ data: { companyId: req.companyId, ...body } }) @@ -398,7 +399,7 @@ router.post('/me/insurance-policies', async (req, res, next) => { } catch (err) { next(err) } }) -router.patch('/me/insurance-policies/:id', async (req, res, next) => { +router.patch('/me/insurance-policies/:id', requireRole('MANAGER'), async (req, res, next) => { try { const body = insurancePolicySchema.partial().parse(req.body) const policy = await prisma.insurancePolicy.updateMany({ @@ -411,7 +412,7 @@ router.patch('/me/insurance-policies/:id', async (req, res, next) => { } catch (err) { next(err) } }) -router.delete('/me/insurance-policies/:id', async (req, res, next) => { +router.delete('/me/insurance-policies/:id', requireRole('MANAGER'), async (req, res, next) => { try { const deleted = await prisma.insurancePolicy.deleteMany({ where: { id: req.params.id, companyId: req.companyId } }) if (deleted.count === 0) return res.status(404).json({ error: 'not_found', message: 'Insurance policy not found', statusCode: 404 }) @@ -429,7 +430,7 @@ router.get('/me/pricing-rules', async (req, res, next) => { } catch (err) { next(err) } }) -router.post('/me/pricing-rules', async (req, res, next) => { +router.post('/me/pricing-rules', requireRole('MANAGER'), async (req, res, next) => { try { const body = pricingRuleSchema.parse(req.body) const rule = await prisma.pricingRule.create({ data: { companyId: req.companyId, ...body } }) @@ -437,7 +438,7 @@ router.post('/me/pricing-rules', async (req, res, next) => { } catch (err) { next(err) } }) -router.patch('/me/pricing-rules/:id', async (req, res, next) => { +router.patch('/me/pricing-rules/:id', requireRole('MANAGER'), async (req, res, next) => { try { const body = pricingRuleSchema.partial().parse(req.body) const updated = await prisma.pricingRule.updateMany({ diff --git a/apps/api/src/routes/customers.ts b/apps/api/src/routes/customers.ts index 17d1851..0807286 100644 --- a/apps/api/src/routes/customers.ts +++ b/apps/api/src/routes/customers.ts @@ -78,7 +78,7 @@ router.get('/:id', async (req, res, next) => { } catch (err) { next(err) } }) -router.patch('/:id', async (req, res, next) => { +router.patch('/:id', requireRole('MANAGER'), async (req, res, next) => { try { const body = customerSchema.partial().parse(req.body) const updated = await prisma.customer.updateMany({ where: { id: req.params.id, companyId: req.companyId }, data: { ...body, dateOfBirth: body.dateOfBirth ? new Date(body.dateOfBirth) : undefined, licenseExpiry: body.licenseExpiry ? new Date(body.licenseExpiry) : undefined, licenseIssuedAt: body.licenseIssuedAt ? new Date(body.licenseIssuedAt) : undefined } as any }) @@ -89,7 +89,7 @@ router.patch('/:id', async (req, res, next) => { } catch (err) { next(err) } }) -router.post('/:id/flag', async (req, res, next) => { +router.post('/:id/flag', requireRole('MANAGER'), async (req, res, next) => { try { const { reason } = z.object({ reason: z.string().optional() }).parse(req.body) await prisma.customer.updateMany({ where: { id: req.params.id, companyId: req.companyId }, data: { flagged: true, flagReason: reason ?? null } }) @@ -97,7 +97,7 @@ router.post('/:id/flag', async (req, res, next) => { } catch (err) { next(err) } }) -router.delete('/:id/flag', async (req, res, next) => { +router.delete('/:id/flag', requireRole('MANAGER'), async (req, res, next) => { try { await prisma.customer.updateMany({ where: { id: req.params.id, companyId: req.companyId }, data: { flagged: false, flagReason: null } }) res.json({ data: { success: true } }) diff --git a/apps/api/src/routes/offers.ts b/apps/api/src/routes/offers.ts index 5c03615..e06b41d 100644 --- a/apps/api/src/routes/offers.ts +++ b/apps/api/src/routes/offers.ts @@ -4,6 +4,7 @@ import { prisma } from '../lib/prisma' import { requireCompanyAuth } from '../middleware/requireCompanyAuth' import { requireTenant } from '../middleware/requireTenant' import { requireSubscription } from '../middleware/requireSubscription' +import { requireRole } from '../middleware/requireRole' const router = Router() router.use(requireCompanyAuth, requireTenant, requireSubscription) @@ -40,7 +41,7 @@ router.get('/', async (req, res, next) => { } catch (err) { next(err) } }) -router.post('/', async (req, res, next) => { +router.post('/', requireRole('MANAGER'), async (req, res, next) => { try { const { vehicleIds, ...body } = offerSchema.parse(req.body) const offer = await prisma.offer.create({ @@ -58,7 +59,7 @@ router.get('/:id', async (req, res, next) => { } catch (err) { next(err) } }) -router.patch('/:id', async (req, res, next) => { +router.patch('/:id', requireRole('MANAGER'), async (req, res, next) => { try { const { vehicleIds, ...body } = offerSchema.partial().parse(req.body) const existing = await prisma.offer.findFirst({ where: { id: req.params.id, companyId: req.companyId } }) @@ -77,21 +78,21 @@ router.patch('/:id', async (req, res, next) => { } catch (err) { next(err) } }) -router.delete('/:id', async (req, res, next) => { +router.delete('/:id', requireRole('MANAGER'), async (req, res, next) => { try { await prisma.offer.deleteMany({ where: { id: req.params.id, companyId: req.companyId } }) res.json({ data: { success: true } }) } catch (err) { next(err) } }) -router.post('/:id/activate', async (req, res, next) => { +router.post('/:id/activate', requireRole('MANAGER'), async (req, res, next) => { try { await prisma.offer.updateMany({ where: { id: req.params.id, companyId: req.companyId }, data: { isActive: true } }) res.json({ data: { success: true } }) } catch (err) { next(err) } }) -router.post('/:id/deactivate', async (req, res, next) => { +router.post('/:id/deactivate', requireRole('MANAGER'), async (req, res, next) => { try { await prisma.offer.updateMany({ where: { id: req.params.id, companyId: req.companyId }, data: { isActive: false } }) res.json({ data: { success: true } }) diff --git a/apps/api/src/routes/payments.ts b/apps/api/src/routes/payments.ts index 16fedf7..6d8e94d 100644 --- a/apps/api/src/routes/payments.ts +++ b/apps/api/src/routes/payments.ts @@ -4,6 +4,7 @@ import { prisma } from '../lib/prisma' import { requireCompanyAuth } from '../middleware/requireCompanyAuth' import { requireTenant } from '../middleware/requireTenant' import { requireSubscription } from '../middleware/requireSubscription' +import { requireRole } from '../middleware/requireRole' import * as amanpay from '../services/amanpayService' import * as paypal from '../services/paypalService' @@ -135,7 +136,7 @@ router.get('/reservations/:id', async (req, res, next) => { } catch (err) { next(err) } }) -router.post('/reservations/:id/charge', async (req, res, next) => { +router.post('/reservations/:id/charge', requireRole('MANAGER'), async (req, res, next) => { try { const { provider, type, currency, successUrl, failureUrl } = chargeSchema.parse(req.body) @@ -208,7 +209,7 @@ router.post('/reservations/:id/charge', async (req, res, next) => { } catch (err) { next(err) } }) -router.post('/reservations/:id/capture-paypal', async (req, res, next) => { +router.post('/reservations/:id/capture-paypal', requireRole('MANAGER'), async (req, res, next) => { try { const { paypalOrderId } = z.object({ paypalOrderId: z.string() }).parse(req.body) @@ -233,7 +234,7 @@ router.post('/reservations/:id/capture-paypal', async (req, res, next) => { } catch (err) { next(err) } }) -router.post('/reservations/:id/manual', async (req, res, next) => { +router.post('/reservations/:id/manual', requireRole('MANAGER'), async (req, res, next) => { try { const { amount, currency, type, paymentMethod } = manualPaymentSchema.parse(req.body) @@ -278,7 +279,7 @@ router.post('/reservations/:id/manual', async (req, res, next) => { } catch (err) { next(err) } }) -router.post('/reservations/:reservationId/payments/:paymentId/refund', async (req, res, next) => { +router.post('/reservations/:reservationId/payments/:paymentId/refund', requireRole('MANAGER'), async (req, res, next) => { try { const { amount, reason } = z.object({ amount: z.number().int().positive().optional(), diff --git a/apps/api/src/routes/subscriptions.ts b/apps/api/src/routes/subscriptions.ts index 3c850b8..6a23fd4 100644 --- a/apps/api/src/routes/subscriptions.ts +++ b/apps/api/src/routes/subscriptions.ts @@ -4,6 +4,7 @@ import { PLAN_PRICES } from '@rentaldrivego/types' import { prisma } from '../lib/prisma' import { requireCompanyAuth } from '../middleware/requireCompanyAuth' import { requireTenant } from '../middleware/requireTenant' +import { requireRole } from '../middleware/requireRole' import * as amanpay from '../services/amanpayService' import * as paypal from '../services/paypalService' @@ -164,7 +165,7 @@ const checkoutSchema = z.object({ failureUrl: z.string().url(), }) -router.post('/checkout', async (req, res, next) => { +router.post('/checkout', requireRole('OWNER'), async (req, res, next) => { try { const body = checkoutSchema.parse(req.body) const prices = PLAN_PRICES[body.plan]?.[body.billingPeriod] @@ -245,7 +246,7 @@ router.post('/checkout', async (req, res, next) => { } catch (err) { next(err) } }) -router.post('/change-plan', async (req, res, next) => { +router.post('/change-plan', requireRole('OWNER'), async (req, res, next) => { try { const { plan, billingPeriod, currency } = z.object({ plan: z.enum(['STARTER', 'GROWTH', 'PRO']), @@ -262,7 +263,7 @@ router.post('/change-plan', async (req, res, next) => { } catch (err) { next(err) } }) -router.post('/cancel', async (req, res, next) => { +router.post('/cancel', requireRole('OWNER'), async (req, res, next) => { try { const updated = await prisma.subscription.update({ where: { companyId: req.companyId }, @@ -272,7 +273,7 @@ router.post('/cancel', async (req, res, next) => { } catch (err) { next(err) } }) -router.post('/resume', async (req, res, next) => { +router.post('/resume', requireRole('OWNER'), async (req, res, next) => { try { const updated = await prisma.subscription.update({ where: { companyId: req.companyId }, diff --git a/apps/api/src/routes/vehicles.ts b/apps/api/src/routes/vehicles.ts index 072e2d1..cedeee3 100644 --- a/apps/api/src/routes/vehicles.ts +++ b/apps/api/src/routes/vehicles.ts @@ -6,6 +6,7 @@ import { uploadImage } from '../lib/storage' import { requireCompanyAuth } from '../middleware/requireCompanyAuth' import { requireTenant } from '../middleware/requireTenant' import { requireSubscription } from '../middleware/requireSubscription' +import { requireRole } from '../middleware/requireRole' const router = Router() const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 10 * 1024 * 1024 } }) @@ -48,7 +49,7 @@ router.get('/', async (req, res, next) => { } catch (err) { next(err) } }) -router.post('/', async (req, res, next) => { +router.post('/', requireRole('MANAGER'), async (req, res, next) => { try { const body = vehicleSchema.parse(req.body) const vehicle = await prisma.vehicle.create({ data: { ...body, companyId: req.companyId } }) @@ -63,7 +64,7 @@ router.get('/:id', async (req, res, next) => { } catch (err) { next(err) } }) -router.patch('/:id', async (req, res, next) => { +router.patch('/:id', requireRole('MANAGER'), async (req, res, next) => { try { const body = vehicleSchema.partial().parse(req.body) if (body.status === 'MAINTENANCE' || body.status === 'OUT_OF_SERVICE') { @@ -78,14 +79,14 @@ router.patch('/:id', async (req, res, next) => { } catch (err) { next(err) } }) -router.delete('/:id', async (req, res, next) => { +router.delete('/:id', requireRole('MANAGER'), async (req, res, next) => { try { await prisma.vehicle.updateMany({ where: { id: req.params.id, companyId: req.companyId }, data: { status: 'OUT_OF_SERVICE', isPublished: false } }) res.json({ data: { success: true } }) } catch (err) { next(err) } }) -router.post('/:id/photos', upload.array('photos', 10), async (req, res, next) => { +router.post('/:id/photos', requireRole('MANAGER'), upload.array('photos', 10), async (req, res, next) => { try { const vehicle = await prisma.vehicle.findFirstOrThrow({ where: { id: req.params.id, companyId: req.companyId } }) const files = req.files as Express.Multer.File[] @@ -95,7 +96,7 @@ router.post('/:id/photos', upload.array('photos', 10), async (req, res, next) => } catch (err) { next(err) } }) -router.delete('/:id/photos/:idx', async (req, res, next) => { +router.delete('/:id/photos/:idx', requireRole('MANAGER'), async (req, res, next) => { try { const vehicle = await prisma.vehicle.findFirstOrThrow({ where: { id: req.params.id, companyId: req.companyId } }) const idx = parseInt(req.params.idx) @@ -105,7 +106,7 @@ router.delete('/:id/photos/:idx', async (req, res, next) => { } catch (err) { next(err) } }) -router.patch('/:id/publish', async (req, res, next) => { +router.patch('/:id/publish', requireRole('MANAGER'), async (req, res, next) => { try { const { isPublished } = z.object({ isPublished: z.boolean() }).parse(req.body) await prisma.vehicle.updateMany({ where: { id: req.params.id, companyId: req.companyId }, data: { isPublished } }) @@ -214,7 +215,7 @@ router.get('/:id/calendar', async (req, res, next) => { } catch (err) { next(err) } }) -router.post('/:id/calendar/blocks', async (req, res, next) => { +router.post('/:id/calendar/blocks', requireRole('MANAGER'), async (req, res, next) => { try { const body = z.object({ startDate: z.string().datetime({ offset: true }).or(z.string().regex(/^\d{4}-\d{2}-\d{2}$/)), @@ -242,7 +243,7 @@ router.post('/:id/calendar/blocks', async (req, res, next) => { } catch (err) { next(err) } }) -router.delete('/:id/calendar/blocks/:blockId', async (req, res, next) => { +router.delete('/:id/calendar/blocks/:blockId', requireRole('MANAGER'), async (req, res, next) => { try { await prisma.vehicle.findFirstOrThrow({ where: { id: req.params.id, companyId: req.companyId } }) await prisma.vehicleCalendarBlock.deleteMany({ @@ -259,7 +260,7 @@ router.get('/:id/maintenance', async (req, res, next) => { } catch (err) { next(err) } }) -router.post('/:id/maintenance', async (req, res, next) => { +router.post('/:id/maintenance', requireRole('MANAGER'), async (req, res, next) => { try { const body = z.object({ type: z.string().min(1), diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/page.tsx b/apps/dashboard/src/app/(dashboard)/dashboard/page.tsx index 7267e38..2c4eb22 100644 --- a/apps/dashboard/src/app/(dashboard)/dashboard/page.tsx +++ b/apps/dashboard/src/app/(dashboard)/dashboard/page.tsx @@ -1,5 +1,6 @@ 'use client' +import Link from 'next/link' import { useEffect, useState } from 'react' import { Calendar, Car, Users, DollarSign, AlertTriangle, Clock, XCircle } from 'lucide-react' import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts' @@ -93,9 +94,9 @@ function SubscriptionBanner({
{config.icon}

{config.message}

- + {manageBillingLabel} - +
) } @@ -243,9 +244,9 @@ export default function DashboardPage() {

{d.recentReservations}

- + {d.viewAll} - +
diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/reservations/new/page.tsx b/apps/dashboard/src/app/(dashboard)/dashboard/reservations/new/page.tsx index 783ec49..aa82936 100644 --- a/apps/dashboard/src/app/(dashboard)/dashboard/reservations/new/page.tsx +++ b/apps/dashboard/src/app/(dashboard)/dashboard/reservations/new/page.tsx @@ -4,6 +4,7 @@ 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 @@ -19,15 +20,15 @@ type Customer = { type Vehicle = { id: string; make: string; model: string; licensePlate: string; status: string } type CreatedCustomer = { id: string; firstName: string; lastName: string; email: string } type AdditionalDriverForm = { - firstName: string - lastName: string + firstName: BilingualField + lastName: BilingualField email: string phone: string driverLicense: string licenseExpiry: string licenseIssuedAt: string dateOfBirth: string - nationality: string + nationality: BilingualField } type CreatedReservation = { id: string } @@ -55,8 +56,8 @@ export default function NewReservationPage() { const [showAddCustomer, setShowAddCustomer] = useState(false) const [savingCustomer, setSavingCustomer] = useState(false) const [includeAdditionalDriver, setIncludeAdditionalDriver] = useState(false) - const [newCustomerFirstName, setNewCustomerFirstName] = useState('') - const [newCustomerLastName, setNewCustomerLastName] = useState('') + const [newCustomerFirstName, setNewCustomerFirstName] = useState(emptyBilingual()) + const [newCustomerLastName, setNewCustomerLastName] = useState(emptyBilingual()) const [newCustomerEmail, setNewCustomerEmail] = useState('') const [newCustomerPhone, setNewCustomerPhone] = useState('') const [driverLicense, setDriverLicense] = useState('') @@ -65,15 +66,15 @@ export default function NewReservationPage() { const [licenseCountry, setLicenseCountry] = useState('') const [licenseCategory, setLicenseCategory] = useState('') const [additionalDriver, setAdditionalDriver] = useState({ - firstName: '', - lastName: '', + firstName: emptyBilingual(), + lastName: emptyBilingual(), email: '', phone: '', driverLicense: '', licenseExpiry: '', licenseIssuedAt: '', dateOfBirth: '', - nationality: '', + nationality: emptyBilingual(), }) const copy = { @@ -113,7 +114,7 @@ export default function NewReservationPage() { firstName: 'First name', lastName: 'Last name', email: 'Email', - phone: 'Phone (optional)', + phone: 'Phone', saveCustomer: 'Save customer', savingCustomer: 'Saving customer…', selectVehicle: 'Select vehicle…', @@ -162,7 +163,7 @@ export default function NewReservationPage() { firstName: 'Prénom', lastName: 'Nom', email: 'Email', - phone: 'Téléphone (optionnel)', + phone: 'Téléphone', saveCustomer: 'Enregistrer le client', savingCustomer: 'Enregistrement du client…', selectVehicle: 'Sélectionner un véhicule…', @@ -211,7 +212,7 @@ export default function NewReservationPage() { firstName: 'الاسم الأول', lastName: 'اسم العائلة', email: 'البريد الإلكتروني', - phone: 'الهاتف (اختياري)', + phone: 'الهاتف', saveCustomer: 'حفظ العميل', savingCustomer: 'جارٍ حفظ العميل…', selectVehicle: 'اختر مركبة…', @@ -264,7 +265,7 @@ export default function NewReservationPage() { async function addCustomer() { setError(null) - if (!newCustomerFirstName.trim() || !newCustomerLastName.trim() || !newCustomerEmail.trim()) { + if (!bilingualPrimary(newCustomerFirstName).trim() || !bilingualPrimary(newCustomerLastName).trim() || !newCustomerEmail.trim() || !newCustomerPhone.trim()) { setError(copy.required) return } @@ -273,10 +274,12 @@ export default function NewReservationPage() { const created = await apiFetch('/customers', { method: 'POST', body: JSON.stringify({ - firstName: newCustomerFirstName.trim(), - lastName: newCustomerLastName.trim(), + firstName: bilingualPrimary(newCustomerFirstName).trim(), + firstNameAr: newCustomerFirstName.ar.trim() || undefined, + lastName: bilingualPrimary(newCustomerLastName).trim(), + lastNameAr: newCustomerLastName.ar.trim() || undefined, email: newCustomerEmail.trim(), - phone: newCustomerPhone.trim() || undefined, + phone: newCustomerPhone.trim(), driverLicense: driverLicense.trim() || undefined, licenseExpiry: licenseExpiry ? new Date(licenseExpiry).toISOString() : undefined, licenseIssuedAt: licenseIssuedAt ? new Date(licenseIssuedAt).toISOString() : undefined, @@ -286,10 +289,10 @@ export default function NewReservationPage() { }) setCustomers((prev) => [created, ...prev]) setCustomerId(created.id) - setCustomerSearch(`${created.firstName} ${created.lastName}`) + setCustomerSearch(`${bilingualPrimary(newCustomerFirstName)} ${bilingualPrimary(newCustomerLastName)}`) setShowAddCustomer(false) - setNewCustomerFirstName('') - setNewCustomerLastName('') + setNewCustomerFirstName(emptyBilingual()) + setNewCustomerLastName(emptyBilingual()) setNewCustomerEmail('') setNewCustomerPhone('') } catch (err: any) { @@ -311,7 +314,7 @@ export default function NewReservationPage() { setError(copy.invalidDates) return } - if (includeAdditionalDriver && (!additionalDriver.firstName.trim() || !additionalDriver.lastName.trim() || !additionalDriver.driverLicense.trim())) { + if (includeAdditionalDriver && (!bilingualPrimary(additionalDriver.firstName).trim() || !bilingualPrimary(additionalDriver.lastName).trim() || !additionalDriver.driverLicense.trim())) { setError(copy.required) return } @@ -341,15 +344,18 @@ export default function NewReservationPage() { depositAmount: Number.isFinite(Number(depositAmount)) ? Math.max(0, Math.round(Number(depositAmount))) : 0, paymentMode, additionalDrivers: includeAdditionalDriver ? [{ - firstName: additionalDriver.firstName.trim(), - lastName: additionalDriver.lastName.trim(), + 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: additionalDriver.nationality.trim() || undefined, + nationality: bilingualPrimary(additionalDriver.nationality).trim() || undefined, + nationalityAr: additionalDriver.nationality.ar.trim() || undefined, }] : [], notes: notes || undefined, }), @@ -380,7 +386,7 @@ export default function NewReservationPage() { <>