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({
)
}
@@ -243,9 +244,9 @@ export default function DashboardPage() {
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() {
<>