From d4f7de576238ecb9652699b505f37d3f1b7024c7 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 19 May 2026 21:25:38 -0400 Subject: [PATCH] dashboard fix --- .../c2c92f69332b855e21ffeb95229a4add.jpg | Bin 0 -> 23274 bytes apps/api/src/routes/analytics.ts | 3 +- apps/api/src/routes/auth.company.ts | 2 + apps/api/src/routes/auth.employee.ts | 2 + apps/api/src/routes/companies.ts | 27 +- apps/api/src/routes/customers.ts | 6 +- apps/api/src/routes/offers.ts | 11 +- apps/api/src/routes/payments.ts | 9 +- apps/api/src/routes/subscriptions.ts | 9 +- apps/api/src/routes/vehicles.ts | 19 +- .../src/app/(dashboard)/dashboard/page.tsx | 9 +- .../dashboard/reservations/new/page.tsx | 105 ++++---- .../dashboard/subscription/page.tsx | 8 +- .../src/app/sign-in/[[...sign-in]]/page.tsx | 141 +++------- .../src/app/sign-up/[[...sign-up]]/page.tsx | 135 ++++++---- .../dashboard/src/components/I18nProvider.tsx | 40 ++- .../src/components/layout/Sidebar.tsx | 49 ++-- .../src/components/layout/TopBar.tsx | 4 + .../src/components/ui/BilingualInput.tsx | 66 +++++ .../src/app/explore/ExploreVehicleGrid.tsx | 19 +- .../app/explore/[slug]/vehicles/[id]/page.tsx | 104 ++++---- apps/marketplace/src/app/explore/page.tsx | 6 +- .../src/components/BookingForm.tsx | 246 ++++++++++++++++++ .../src/components/MarketplaceShell.tsx | 13 + .../migration.sql | 1 + packages/database/prisma/schema.prisma | 1 + scripts/cronjobs.md | 6 + 27 files changed, 705 insertions(+), 336 deletions(-) create mode 100644 apps/api/src/lib/storage/companies/cmpbosg0o0001bh8tc433zxt2/vehicles/c2c92f69332b855e21ffeb95229a4add.jpg create mode 100644 apps/dashboard/src/components/ui/BilingualInput.tsx create mode 100644 apps/marketplace/src/components/BookingForm.tsx create mode 100644 packages/database/prisma/migrations/20260518000000_add_employee_preferred_language/migration.sql create mode 100644 scripts/cronjobs.md 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 0000000000000000000000000000000000000000..bbc749a21c3659b929705c22ca3baf911144cb3d GIT binary patch literal 23274 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 Xo6W^3N>Y@iFGb~eUg}@yb=m*f_bQgi literal 0 HcmV?d00001 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() { <>