fix setting and add rental policies
Test / Type Check (all packages) (push) Has been cancelled
Test / API Unit Tests (push) Has been cancelled
Test / Homepage Unit Tests (push) Has been cancelled
Test / Carplace Unit Tests (push) Has been cancelled
Test / Admin Unit Tests (push) Has been cancelled
Test / Dashboard Unit Tests (push) Has been cancelled
Test / API Integration Tests (push) Has been cancelled
Build & Push / Pipeline Tests (push) Waiting to run
Build & Push / Build & Push Docker Image (push) Blocked by required conditions

This commit is contained in:
root
2026-07-20 22:47:52 -04:00
parent d13ae2205f
commit 8f5ca1df22
24 changed files with 584 additions and 106 deletions
@@ -19,7 +19,17 @@ describe('company schemas edge cases', () => {
})
it('keeps contract, accounting, insurance, and pricing settings inside known enums', () => {
expect(contractSettingsSchema.parse({ fuelPolicyType: 'FULL_TO_FULL', additionalDriverCharge: 'PER_DAY', taxRate: 20 })).toMatchObject({ fuelPolicyType: 'FULL_TO_FULL' })
expect(contractSettingsSchema.parse({
fuelPolicy: 'Return with the same fuel level.',
fuelPolicyType: 'FULL_TO_FULL',
additionalDriverPolicy: 'Additional drivers must be approved before pickup.',
additionalDriverCharge: 'PER_DAY',
taxRate: 20,
})).toMatchObject({
fuelPolicy: 'Return with the same fuel level.',
fuelPolicyType: 'FULL_TO_FULL',
additionalDriverPolicy: 'Additional drivers must be approved before pickup.',
})
expect(contractSettingsSchema.safeParse({ fuelPolicyType: 'CHAOS' }).success).toBe(false)
expect(accountingSettingsSchema.parse({ reportingPeriod: 'MONTHLY', fiscalYearStart: 1, currency: 'MAD' })).toMatchObject({ fiscalYearStart: 1 })
expect(accountingSettingsSchema.safeParse({ fiscalYearStart: 13 }).success).toBe(false)
@@ -106,6 +106,7 @@ export const contractSettingsSchema = z.object({
depositPolicy: z.string().optional(),
lateFeePolicy: z.string().optional(),
damagePolicy: z.string().optional(),
additionalDriverPolicy: z.string().optional(),
contractFooterNote: z.string().optional(),
invoiceFooterNote: z.string().optional(),
signatureRequired: z.boolean().optional(),
@@ -105,13 +105,6 @@ export async function getContractSettings(companyId: string) {
export async function updateContractSettings(companyId: string, data: any) {
await assertSettingsFeature(companyId, 'settings.rental_policies_basic')
if (
data.additionalDriverCharge ||
data.additionalDriverDailyRate !== undefined ||
data.additionalDriverFlatRate !== undefined
) {
await assertSettingsFeature(companyId, 'settings.additional_driver_fees')
}
return repo.upsertContractSettings(companyId, data)
}
@@ -105,7 +105,7 @@ const FEATURE_PLANS: Record<SettingsFeatureKey, { plans: Plan[]; requiredPlan: P
'settings.branding_hero': { plans: GROWTH_PLUS, requiredPlan: 'GROWTH' },
'settings.renter_payments': { plans: GROWTH_PLUS, requiredPlan: 'GROWTH' },
'settings.rental_policies_basic': { plans: ALL_PLANS, requiredPlan: null },
'settings.additional_driver_fees': { plans: GROWTH_PLUS, requiredPlan: 'GROWTH' },
'settings.additional_driver_fees': { plans: ALL_PLANS, requiredPlan: null },
'settings.insurance_policies': { plans: GROWTH_PLUS, requiredPlan: 'GROWTH' },
'settings.pricing_rules': { plans: GROWTH_PLUS, requiredPlan: 'GROWTH' },
'settings.accounting_defaults': { plans: GROWTH_PLUS, requiredPlan: 'GROWTH' },
@@ -209,6 +209,7 @@ export async function getContract(id: string, companyId: string) {
depositPolicy: contractSettings.depositPolicy,
lateFeePolicy: contractSettings.lateFeePolicy,
damagePolicy: contractSettings.damagePolicy,
additionalDriverPolicy: contractSettings.additionalDriverPolicy,
footerNote: contractSettings.contractFooterNote,
signatureRequired: contractSettings.signatureRequired,
},
@@ -70,6 +70,7 @@ describe('company configuration API contracts', () => {
vi.mocked(companyService.setCustomDomain).mockResolvedValue({ id: 'brand_1', customDomain: 'cars.example.com' } as never)
vi.mocked(companyService.createInsurancePolicy).mockResolvedValue({ id: 'policy_1' } as never)
vi.mocked(companyService.createPricingRule).mockResolvedValue({ id: 'rule_1' } as never)
vi.mocked(companyService.updateContractSettings).mockResolvedValue({ id: 'contract_settings_1' } as never)
vi.mocked(companyService.updateAccountingSettings).mockResolvedValue({ id: 'accounting_1' } as never)
})
@@ -126,6 +127,25 @@ describe('company configuration API contracts', () => {
}))
})
it('passes rental policy text settings through to the service', async () => {
const res = await request(app).patch('/api/v1/companies/me/contract-settings').send({
fuelPolicy: 'Return with the same fuel level shown at pickup.',
fuelPolicyType: 'SAME_TO_SAME',
fuelPolicyNote: 'Charge refueling when fuel is lower at return.',
additionalDriverPolicy: 'Only approved additional drivers may operate the vehicle.',
damagePolicy: 'Damage is assessed at return inspection.',
})
expect(res.status).toBe(200)
expect(companyService.updateContractSettings).toHaveBeenCalledWith('company_1', {
fuelPolicy: 'Return with the same fuel level shown at pickup.',
fuelPolicyType: 'SAME_TO_SAME',
fuelPolicyNote: 'Charge refueling when fuel is lower at return.',
additionalDriverPolicy: 'Only approved additional drivers may operate the vehicle.',
damagePolicy: 'Damage is assessed at return inspection.',
})
})
it('rejects malformed pricing rules before service execution', async () => {
const res = await request(app).post('/api/v1/companies/me/pricing-rules').send({
name: 'Young driver fee',
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -2,6 +2,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import Link from 'next/link'
import { useSearchParams } from 'next/navigation'
import { formatCurrency, SupportedCurrency } from '@rentaldrivego/types'
import { apiFetch } from '@/lib/api'
import { useDashboardI18n } from '@/components/I18nProvider'
@@ -77,7 +78,9 @@ const STATUS_BADGE: Record<string, string> = {
export default function BillingPage() {
const { language } = useDashboardI18n()
const searchParams = useSearchParams()
const localeCode = language === 'fr' ? 'fr-FR' : language === 'ar' ? 'ar-MA' : 'en-US'
const querySearch = searchParams.get('search') ?? ''
const [summary, setSummary] = useState<BillingSummary | null>(null)
const [invoices, setInvoices] = useState<BillingInvoiceResponse | null>(null)
const [search, setSearch] = useState('')
@@ -262,6 +265,12 @@ export default function BillingPage() {
},
}[language]), [language])
useEffect(() => {
setSearch(querySearch)
setSubmittedSearch(querySearch)
setPage(1)
}, [querySearch])
const loadBilling = useCallback(async () => {
setLoading(true)
setError(null)
@@ -116,6 +116,7 @@ type ContractPayload = {
depositPolicy: string | null
lateFeePolicy: string | null
damagePolicy: string | null
additionalDriverPolicy: string | null
footerNote: string | null
signatureRequired: boolean
}
@@ -453,6 +454,7 @@ export default function ContractDetailPage() {
deposit: 'Deposit policy',
lateFees: 'Late fee policy',
damage: 'Damage policy',
additionalDriverPolicy: 'Driver policy',
termsLabel: 'Terms',
signature: 'Signature required',
customerDamage: 'Check-in inspection',
@@ -584,6 +586,7 @@ export default function ContractDetailPage() {
deposit: 'Politique de dépôt',
lateFees: 'Politique de retard',
damage: 'Politique dommages',
additionalDriverPolicy: 'Politique conducteur',
termsLabel: 'Conditions',
signature: 'Signature requise',
customerDamage: 'Inspection de départ',
@@ -715,6 +718,7 @@ export default function ContractDetailPage() {
deposit: 'سياسة العربون',
lateFees: 'سياسة التأخير',
damage: 'سياسة الأضرار',
additionalDriverPolicy: 'سياسة السائق',
termsLabel: 'الشروط',
signature: 'التوقيع مطلوب',
customerDamage: 'فحص الاستلام',
@@ -982,6 +986,7 @@ export default function ContractDetailPage() {
className="bg-white"
/>
<PaperField label={bl(copy.fuel, arCopy.fuel)} value={contract.terms.fuelPolicy ?? copy.none} className="bg-white" />
<PaperField label={bl(copy.additionalDriverPolicy, arCopy.additionalDriverPolicy)} value={contract.terms.additionalDriverPolicy ?? copy.none} className="bg-white" />
<PaperField label={bl(copy.damage, arCopy.damage)} value={contract.terms.damagePolicy ?? copy.none} className="bg-white" />
<PaperField
label={bl(copy.checkInCondition, arCopy.checkInCondition)}
@@ -2,6 +2,7 @@
import { useEffect, useMemo, useState } from 'react'
import Link from 'next/link'
import { useSearchParams } from 'next/navigation'
import { formatCurrency } from '@rentaldrivego/types'
import { apiFetch } from '@/lib/api'
import { useDashboardI18n } from '@/components/I18nProvider'
@@ -21,6 +22,7 @@ type ReservationRow = {
export default function ContractsPage() {
const { language } = useDashboardI18n()
const searchParams = useSearchParams()
const [rows, setRows] = useState<ReservationRow[]>([])
const [search, setSearch] = useState('')
const [error, setError] = useState<string | null>(null)
@@ -85,6 +87,10 @@ export default function ContractsPage() {
.catch((err) => setError(err.message ?? 'Failed to load contracts'))
}, [])
useEffect(() => {
setSearch(searchParams.get('search') ?? '')
}, [searchParams])
const filteredRows = useMemo(() => {
const q = search.trim().toLowerCase()
if (!q) return rows
@@ -1,6 +1,7 @@
'use client'
import { useEffect, useState } from 'react'
import { useSearchParams } from 'next/navigation'
import { apiFetch } from '@/lib/api'
import { useDashboardI18n } from '@/components/I18nProvider'
@@ -17,6 +18,8 @@ interface CustomerRow {
export default function CustomersPage() {
const { language } = useDashboardI18n()
const searchParams = useSearchParams()
const search = (searchParams.get('search') ?? '').trim().toLowerCase()
const [rows, setRows] = useState<CustomerRow[]>([])
const [error, setError] = useState<string | null>(null)
const copy = {
@@ -70,6 +73,15 @@ export default function CustomersPage() {
.catch((err) => setError(err.message))
}, [])
const filteredRows = search
? rows.filter((row) =>
`${row.firstName} ${row.lastName}`.toLowerCase().includes(search) ||
row.email.toLowerCase().includes(search) ||
(row.phone ?? '').toLowerCase().includes(search) ||
row.licenseValidationStatus.toLowerCase().includes(search),
)
: rows
return (
<div className="space-y-6">
<div>
@@ -91,7 +103,7 @@ export default function CustomersPage() {
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{rows.map((row) => (
{filteredRows.map((row) => (
<tr key={row.id}>
<td className="px-6 py-4 text-sm font-semibold text-slate-900">{row.firstName} {row.lastName}</td>
<td className="px-6 py-4">
@@ -109,7 +121,7 @@ export default function CustomersPage() {
<td className="px-6 py-4">{row.flagged ? <span className="badge-red">{copy.flagged}</span> : <span className="badge-green">{copy.clear}</span>}</td>
</tr>
))}
{rows.length === 0 && (
{filteredRows.length === 0 && (
<tr>
<td colSpan={4} className="px-6 py-10 text-center text-sm text-slate-400">{copy.empty}</td>
</tr>
@@ -96,11 +96,8 @@ interface MaintenanceLog {
nextDueMileage: number | null
}
const ROUTINE_TYPES = [
const CAR_MAINTENANCE_TYPES = [
{ key: 'Oil Change', intervalMonths: 6 },
{ key: 'Technical Inspection', intervalMonths: 12 },
{ key: 'Vehicle Registration', intervalMonths: 12 },
{ key: 'Car Tax', intervalMonths: 12 },
{ key: 'Tire Rotation', intervalMonths: 6 },
{ key: 'Brake Inspection', intervalMonths: 12 },
{ key: 'Air Filter', intervalMonths: 12 },
@@ -109,8 +106,27 @@ const ROUTINE_TYPES = [
{ key: 'Belt / Chain Service', intervalMonths: 24 },
{ key: 'Coolant Flush', intervalMonths: 24 },
{ key: 'Transmission Service', intervalMonths: 24 },
{ key: 'Wheel Alignment', intervalMonths: 12 },
{ key: 'Wheel Balancing', intervalMonths: 12 },
{ key: 'Spark Plugs', intervalMonths: 24 },
{ key: 'Fuel Filter', intervalMonths: 24 },
{ key: 'Wiper Blades', intervalMonths: 12 },
{ key: 'AC Service', intervalMonths: 12 },
{ key: 'Suspension Check', intervalMonths: 12 },
{ key: 'Engine Diagnostic', intervalMonths: 12 },
{ key: 'Brake Pads', intervalMonths: 12 },
]
const ROUTINE_TYPES = [
{ key: 'Technical Inspection', intervalMonths: 12 },
{ key: 'Car Tax', intervalMonths: 12 },
{ key: 'Insurance', intervalMonths: 12 },
]
const TECHNICAL_INSPECTION_TYPE = ROUTINE_TYPES[0]
const CAR_TAX_TYPE = ROUTINE_TYPES[1]
const INSURANCE_TYPE = ROUTINE_TYPES[2]
function serviceStatus(log: MaintenanceLog | undefined, currentMileage: number | null): 'none' | 'overdue' | 'due-soon' | 'ok' {
if (!log) return 'none'
const now = new Date()
@@ -163,6 +179,7 @@ function MaintenanceRow({ vehicleId, typeKey, intervalMonths, currentMileage, lo
})
const [saving, setSaving] = useState(false)
const [error, setError] = useState<string | null>(null)
const usesMileage = typeKey !== 'Car Tax' && typeKey !== 'Insurance'
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
@@ -176,10 +193,10 @@ function MaintenanceRow({ vehicleId, typeKey, intervalMonths, currentMileage, lo
type: typeKey,
description: form.description || undefined,
cost: form.cost ? Math.round(parseFloat(form.cost) * 100) : undefined,
mileage: form.mileage ? parseInt(form.mileage) : undefined,
mileage: usesMileage && form.mileage ? parseInt(form.mileage) : undefined,
performedAt: new Date(form.performedAt).toISOString(),
nextDueAt: form.nextDueAt ? new Date(form.nextDueAt).toISOString() : undefined,
nextDueMileage: form.nextDueMileage ? parseInt(form.nextDueMileage) : undefined,
nextDueMileage: usesMileage && form.nextDueMileage ? parseInt(form.nextDueMileage) : undefined,
}),
})
setForm({ description: '', cost: '', mileage: '', performedAt: today, nextDueAt: defaultNextDue, nextDueMileage: '' })
@@ -192,18 +209,18 @@ function MaintenanceRow({ vehicleId, typeKey, intervalMonths, currentMileage, lo
}
}
const status = serviceStatus(log, currentMileage)
const status = serviceStatus(log, usesMileage ? currentMileage : null)
const displayLabel = vd.routineTypes[typeKey] ?? typeKey
const nextDueText = (() => {
if (!log) return vd.noRecord
const parts: string[] = [`${vd.lastPrefix} ${new Date(log.performedAt).toLocaleDateString()}`]
if (log.mileage) parts.push(vd.atKm(log.mileage.toLocaleString()))
if (usesMileage && log.mileage) parts.push(vd.atKm(log.mileage.toLocaleString()))
const dueParts: string[] = []
if (log.nextDueAt) dueParts.push(new Date(log.nextDueAt).toLocaleDateString())
if (log.nextDueMileage != null) dueParts.push(`${log.nextDueMileage.toLocaleString()} km`)
if (usesMileage && log.nextDueMileage != null) dueParts.push(`${log.nextDueMileage.toLocaleString()} km`)
if (dueParts.length) parts.push(`${vd.duePrefix} ${dueParts.join(` ${vd.orSep} `)}`)
if (currentMileage != null && log.nextDueMileage != null) {
if (usesMileage && currentMileage != null && log.nextDueMileage != null) {
const kmLeft = log.nextDueMileage - currentMileage
parts.push(`(${kmLeft > 0 ? vd.kmLeft(kmLeft.toLocaleString()) : vd.overdueByKm})`)
}
@@ -237,26 +254,30 @@ function MaintenanceRow({ vehicleId, typeKey, intervalMonths, currentMileage, lo
<form onSubmit={handleSubmit} className="border-t border-slate-100 bg-slate-50 px-4 py-4 space-y-3">
{error && <p className="text-xs text-red-600">{error}</p>}
<div className="grid grid-cols-2 gap-3">
<div className={usesMileage ? 'grid grid-cols-2 gap-3' : ''}>
<div>
<label className="block text-xs font-medium text-slate-500 mb-1">{vd.serviceDateLabel}</label>
<input type="date" className="input-field text-sm" value={form.performedAt} onChange={(e) => setForm({ ...form, performedAt: e.target.value })} required />
</div>
<div>
<label className="block text-xs font-medium text-slate-500 mb-1">{vd.odometerAtService}</label>
<input type="number" className="input-field text-sm" placeholder={currentMileage ? String(currentMileage) : 'e.g. 52000'} min="0" value={form.mileage} onChange={(e) => setForm({ ...form, mileage: e.target.value })} />
</div>
{usesMileage && (
<div>
<label className="block text-xs font-medium text-slate-500 mb-1">{vd.odometerAtService}</label>
<input type="number" className="input-field text-sm" placeholder={currentMileage ? String(currentMileage) : 'e.g. 52000'} min="0" value={form.mileage} onChange={(e) => setForm({ ...form, mileage: e.target.value })} />
</div>
)}
</div>
<div className="grid grid-cols-2 gap-3">
<div className={usesMileage ? 'grid grid-cols-2 gap-3' : ''}>
<div>
<label className="block text-xs font-medium text-slate-500 mb-1">{vd.nextDueDateLabel}</label>
<input type="date" className="input-field text-sm" value={form.nextDueAt} onChange={(e) => setForm({ ...form, nextDueAt: e.target.value })} />
</div>
<div>
<label className="block text-xs font-medium text-slate-500 mb-1">{vd.nextDueKmLabel}</label>
<input type="number" className="input-field text-sm" placeholder="e.g. 60000" min="0" value={form.nextDueMileage} onChange={(e) => setForm({ ...form, nextDueMileage: e.target.value })} />
</div>
{usesMileage && (
<div>
<label className="block text-xs font-medium text-slate-500 mb-1">{vd.nextDueKmLabel}</label>
<input type="number" className="input-field text-sm" placeholder="e.g. 60000" min="0" value={form.nextDueMileage} onChange={(e) => setForm({ ...form, nextDueMileage: e.target.value })} />
</div>
)}
</div>
<div className="grid grid-cols-2 gap-3">
@@ -280,7 +301,50 @@ function MaintenanceRow({ vehicleId, typeKey, intervalMonths, currentMileage, lo
)
}
type Tab = 'details' | 'maintenance' | 'calendar' | 'pricing'
function CarMaintenanceGroup({ vehicleId, currentMileage, logs, onLogged }: {
vehicleId: string
currentMileage: number | null
logs: MaintenanceLog[]
onLogged: () => void
}) {
const { dict } = useDashboardI18n()
const vd = dict.vehicleDetail
const [selectedKey, setSelectedKey] = useState(CAR_MAINTENANCE_TYPES[0].key)
const selectedType = CAR_MAINTENANCE_TYPES.find((type) => type.key === selectedKey) ?? CAR_MAINTENANCE_TYPES[0]
const latest = logs
.filter((log) => log.type === selectedType.key)
.sort((a, b) => new Date(b.performedAt).getTime() - new Date(a.performedAt).getTime())[0]
return (
<div className="rounded-xl border border-slate-100 bg-white p-4">
<div className="mb-3 flex flex-wrap items-center justify-between gap-3">
<h3 className="text-sm font-semibold text-slate-900">{vd.carMaintenance}</h3>
<select
className="input-field w-full text-sm sm:w-72"
value={selectedKey}
onChange={(event) => setSelectedKey(event.target.value)}
>
{CAR_MAINTENANCE_TYPES.map((type) => (
<option key={type.key} value={type.key}>
{vd.routineTypes[type.key] ?? type.key}
</option>
))}
</select>
</div>
<MaintenanceRow
key={selectedType.key}
vehicleId={vehicleId}
typeKey={selectedType.key}
intervalMonths={selectedType.intervalMonths}
currentMileage={currentMileage}
log={latest}
onLogged={onLogged}
/>
</div>
)
}
type Tab = 'details' | 'maintenance' | 'technicalInspection' | 'carTax' | 'insurance' | 'calendar' | 'pricing'
export default function FleetDetailPage() {
const params = useParams<{ id: string }>()
@@ -373,7 +437,6 @@ export default function FleetDetailPage() {
if (!form || !vehicle) return
if (!form.make) { setSaveError(vd.makeRequired); return }
if (!form.model) { setSaveError(vd.modelRequired); return }
if (!form.licensePlate) { setSaveError(vd.plateRequired); return }
const rate = parseFloat(form.dailyRate)
if (isNaN(rate) || rate < 0) { setSaveError(vd.rateInvalid); return }
if (form.allowDifferentDropoff && parseLocationInput(form.dropoffLocations).length === 0) {
@@ -463,6 +526,9 @@ export default function FleetDetailPage() {
const fuelLabel = fl.fuelTypeLabels[vehicle.fuelType as keyof typeof fl.fuelTypeLabels] ?? vehicle.fuelType
const transLabel = vehicle.transmission === 'MANUAL' ? fl.manual : fl.automatic
const pricingTabLabel = language === 'fr' ? 'Tarifs' : language === 'ar' ? 'التسعير' : 'Pricing'
const technicalInspectionTabLabel = vd.routineTypes[TECHNICAL_INSPECTION_TYPE.key] ?? TECHNICAL_INSPECTION_TYPE.key
const carTaxTabLabel = vd.routineTypes[CAR_TAX_TYPE.key] ?? CAR_TAX_TYPE.key
const insuranceTabLabel = vd.routineTypes[INSURANCE_TYPE.key] ?? INSURANCE_TYPE.key
return (
<div className="space-y-6">
@@ -475,10 +541,7 @@ export default function FleetDetailPage() {
<div>
<h2 className="text-xl font-semibold text-slate-900">{vehicle.make} {vehicle.model}</h2>
<p className="text-sm text-slate-500 mt-0.5">
{vehicle.year} · {vehicle.licensePlate}
{vehicle.vin ? ` · ${vehicle.vin}` : ''}
{' · '}
{statusLabel}
{vehicle.year} · {statusLabel}
</p>
</div>
</div>
@@ -505,7 +568,7 @@ export default function FleetDetailPage() {
{/* Tab bar */}
<div className="flex gap-1 border-b border-slate-200 dark:border-slate-800">
{(['details', 'maintenance', 'calendar', 'pricing'] as Tab[]).map((tab) => (
{(['calendar', 'carTax', 'details', 'insurance', 'maintenance', 'pricing', 'technicalInspection'] as Tab[]).map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
@@ -523,6 +586,9 @@ export default function FleetDetailPage() {
{maintenanceLogs.length === 0 && <span className="ml-1 inline-flex h-4 w-4 items-center justify-center rounded-full bg-orange-100 text-orange-700 text-[10px] font-bold">!</span>}
</>
)}
{tab === 'technicalInspection' && <><Wrench className="w-4 h-4" /> {technicalInspectionTabLabel}</>}
{tab === 'carTax' && <><DollarSign className="w-4 h-4" /> {carTaxTabLabel}</>}
{tab === 'insurance' && <><Info className="w-4 h-4" /> {insuranceTabLabel}</>}
{tab === 'calendar' && <><CalendarDays className="w-4 h-4" /> {vd.tabCalendar}</>}
{tab === 'pricing' && <><DollarSign className="w-4 h-4" /> {pricingTabLabel}</>}
</button>
@@ -598,7 +664,6 @@ export default function FleetDetailPage() {
<div><dt className="text-slate-500">{vd.labelCategory}</dt><dd className="text-slate-900">{categoryLabel}</dd></div>
<div><dt className="text-slate-500">{vd.labelDailyRate}</dt><dd className="text-slate-900">{formatCurrency(vehicle.dailyRate, 'MAD')}</dd></div>
<div><dt className="text-slate-500">{vd.labelStatus}</dt><dd className="text-slate-900">{statusLabel}</dd></div>
<div><dt className="text-slate-500">{vd.vinLabel}</dt><dd className="text-slate-900">{vehicle.vin || '—'}</dd></div>
<div><dt className="text-slate-500">{vd.labelSeats}</dt><dd className="text-slate-900">{vehicle.seats}</dd></div>
<div><dt className="text-slate-500">{vd.labelTransmission}</dt><dd className="text-slate-900">{transLabel}</dd></div>
<div><dt className="text-slate-500">{vd.labelFuelType}</dt><dd className="text-slate-900">{fuelLabel}</dd></div>
@@ -678,20 +743,10 @@ export default function FleetDetailPage() {
</div>
</div>
{/* Daily Rate, License Plate & VIN */}
<div className="grid gap-3 sm:grid-cols-2">
<div>
<label className="block text-xs font-medium text-slate-500 mb-1.5">{vd.dailyRateLabel}</label>
<input type="number" className="input-field" min="0" step="0.01" value={form.dailyRate} onChange={(e) => setForm({ ...form, dailyRate: e.target.value })} />
</div>
<div>
<label className="block text-xs font-medium text-slate-500 mb-1.5">{vd.licensePlateLabel}</label>
<input className="input-field" value={form.licensePlate} onChange={(e) => setForm({ ...form, licensePlate: e.target.value })} />
</div>
</div>
{/* Daily Rate */}
<div>
<label className="block text-xs font-medium text-slate-500 mb-1.5">{vd.vinLabel}</label>
<input className="input-field" placeholder="1HGCM82633A123456" value={form.vin} onChange={(e) => setForm({ ...form, vin: e.target.value })} />
<label className="block text-xs font-medium text-slate-500 mb-1.5">{vd.dailyRateLabel}</label>
<input type="number" className="input-field" min="0" step="0.01" value={form.dailyRate} onChange={(e) => setForm({ ...form, dailyRate: e.target.value })} />
</div>
{/* Status */}
@@ -806,25 +861,54 @@ export default function FleetDetailPage() {
{activeTab === 'maintenance' && (
<div className="space-y-3">
<p className="text-sm text-slate-500">{vd.maintenanceIntro}</p>
{ROUTINE_TYPES.map(({ key, intervalMonths }) => {
const latest = maintenanceLogs
.filter((l) => l.type === key)
.sort((a, b) => new Date(b.performedAt).getTime() - new Date(a.performedAt).getTime())[0]
return (
<MaintenanceRow
key={key}
vehicleId={params.id}
typeKey={key}
intervalMonths={intervalMonths}
currentMileage={vehicle.mileage}
log={latest}
onLogged={fetchMaintenance}
/>
)
})}
<CarMaintenanceGroup
vehicleId={params.id}
currentMileage={vehicle.mileage}
logs={maintenanceLogs}
onLogged={fetchMaintenance}
/>
</div>
)}
{activeTab === 'technicalInspection' && (
<MaintenanceRow
vehicleId={params.id}
typeKey={TECHNICAL_INSPECTION_TYPE.key}
intervalMonths={TECHNICAL_INSPECTION_TYPE.intervalMonths}
currentMileage={vehicle.mileage}
log={maintenanceLogs
.filter((log) => log.type === TECHNICAL_INSPECTION_TYPE.key)
.sort((a, b) => new Date(b.performedAt).getTime() - new Date(a.performedAt).getTime())[0]}
onLogged={fetchMaintenance}
/>
)}
{activeTab === 'carTax' && (
<MaintenanceRow
vehicleId={params.id}
typeKey={CAR_TAX_TYPE.key}
intervalMonths={CAR_TAX_TYPE.intervalMonths}
currentMileage={vehicle.mileage}
log={maintenanceLogs
.filter((log) => log.type === CAR_TAX_TYPE.key)
.sort((a, b) => new Date(b.performedAt).getTime() - new Date(a.performedAt).getTime())[0]}
onLogged={fetchMaintenance}
/>
)}
{activeTab === 'insurance' && (
<MaintenanceRow
vehicleId={params.id}
typeKey={INSURANCE_TYPE.key}
intervalMonths={INSURANCE_TYPE.intervalMonths}
currentMileage={vehicle.mileage}
log={maintenanceLogs
.filter((log) => log.type === INSURANCE_TYPE.key)
.sort((a, b) => new Date(b.performedAt).getTime() - new Date(a.performedAt).getTime())[0]}
onLogged={fetchMaintenance}
/>
)}
{activeTab === 'calendar' && (
<VehicleCalendar vehicleId={params.id} />
)}
@@ -601,6 +601,10 @@ export default function FleetPage() {
return () => { cancelled = true }
}, [])
useEffect(() => {
setSearch(searchParams.get('search') ?? '')
}, [searchParams])
useEffect(() => {
if (searchParams.get('modal') === 'add-car') {
setShowAddModal(true)
@@ -2,7 +2,7 @@
import { useEffect, useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { useRouter, useSearchParams } from 'next/navigation'
import { formatCurrency } from '@rentaldrivego/types'
import { apiFetch } from '@/lib/api'
import { useDashboardI18n } from '@/components/I18nProvider'
@@ -33,6 +33,8 @@ interface VehicleRow {
export default function ReservationsPage() {
const { dict, language } = useDashboardI18n()
const router = useRouter()
const searchParams = useSearchParams()
const search = (searchParams.get('search') ?? '').trim().toLowerCase()
const r = dict.reservations
const localeCode = language === 'fr' ? 'fr-FR' : language === 'ar' ? 'ar-MA' : 'en-US'
@@ -96,6 +98,17 @@ export default function ReservationsPage() {
router.push('/fleet?modal=add-car')
}
const filteredRows = search
? rows.filter((row) =>
`${row.customer.firstName} ${row.customer.lastName}`.toLowerCase().includes(search) ||
row.customer.email.toLowerCase().includes(search) ||
`${row.vehicle.make} ${row.vehicle.model}`.toLowerCase().includes(search) ||
row.status.toLowerCase().includes(search) ||
row.source.toLowerCase().includes(search) ||
(row.contractNumber ?? '').toLowerCase().includes(search),
)
: rows
return (
<div className="space-y-6">
<div className="flex items-start justify-between gap-3">
@@ -140,7 +153,7 @@ export default function ReservationsPage() {
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{rows.map((row) => (
{filteredRows.map((row) => (
<tr key={row.id}>
<td className="px-6 py-4">
<Link href={`/reservations/${row.id}`} className="text-sm font-semibold text-slate-900 hover:text-blue-700">
@@ -158,7 +171,7 @@ export default function ReservationsPage() {
<td className="px-6 py-4 text-right text-sm font-semibold text-slate-900">{formatCurrency(row.totalAmount, 'MAD')}</td>
</tr>
))}
{rows.length === 0 && (
{filteredRows.length === 0 && (
<tr>
<td colSpan={6} className="px-6 py-10 text-center text-sm text-slate-400">{r.noReservations}</td>
</tr>
@@ -86,11 +86,13 @@ interface BrandSettings {
}
interface ContractSettings {
fuelPolicy: string
fuelPolicyType: string
fuelPolicyNote: string | null
additionalDriverCharge: 'FREE' | 'PER_DAY' | 'FLAT'
additionalDriverDailyRate: number
additionalDriverFlatRate: number
additionalDriverPolicy: string
damagePolicy: string
}
@@ -126,6 +128,49 @@ interface AccountingSettings {
const emptyInsurance = { name: '', type: 'BASIC', chargeType: 'PER_DAY', chargeValue: 0, isRequired: false, isActive: true }
const emptyRule = { name: '', type: 'SURCHARGE', condition: 'AGE_LESS_THAN', conditionValue: 25, adjustmentType: 'PERCENTAGE', adjustmentValue: 0, isActive: true }
const rentalPolicyTemplates = {
en: {
fuelPolicy: 'The vehicle will be provided with the fuel level or electric battery charge percentage recorded on the rental agreement. The renter must return the vehicle with the same fuel level or, for electric vehicles, the same battery charge percentage recorded at pickup.\n\nIf the vehicle is returned with less fuel or a lower electric battery charge percentage, the renter will be charged for the missing fuel or charging shortfall at the applicable refueling or charging rate, plus any configured service fee. Charges are based on the vehicle fuel gauge or battery charge percentage and the levels recorded at the start and end of the rental.\n\nFuel or charging purchased during the rental is the renter responsibility and is not refundable. The renter should retain fuel or charging receipts until the vehicle has been returned and the rental agreement has been closed.\n\nThe renter must use the correct fuel type or charging method specified for the vehicle. Any damage caused by using the wrong fuel, charger, connector, or charging method will be charged to the renter.',
fuelPolicyNote: 'Please return the vehicle with the same fuel level provided at collection. Electric vehicles must be returned with the same battery charge percentage recorded at pickup. Vehicles returned with less fuel or a lower battery charge percentage will be subject to a refueling or charging charge and service fee. Use only the fuel type or charging method specified for the vehicle.',
additionalDriverPolicy: 'Only drivers listed and approved on the rental agreement are permitted to drive the vehicle.\n\nThis driver policy applies to the principal renter and every additional driver. Every driver must meet the minimum age requirement, present a valid driving licence accepted in the rental location, provide any additional identification reasonably requested, and comply with all terms of the rental agreement.\n\nDrivers aged 25 or older qualify for the normal rental rate. Drivers aged 18 to 24 may be accepted subject to an additional young driver fee. The fee may apply to the principal renter or to any additional driver in this age range, according to the selected charge setting and pricing rules.\n\nAn additional driver fee may apply per day or per rental according to the selected charge setting. The fee and any applicable taxes will be confirmed before the rental begins.\n\nThe primary renter remains responsible for the vehicle and for all charges, damage, fines, penalties, and breaches of the rental agreement, regardless of which approved driver was operating the vehicle.\n\nAllowing an unauthorised person to drive the vehicle is a breach of the rental agreement and may invalidate any damage waiver, protection package, or insurance coverage.',
damagePolicy: 'The renter is responsible for inspecting the vehicle at collection and confirming that all existing damage is recorded on the rental agreement or vehicle condition report. Any unrecorded damage should be reported before the vehicle is driven.\n\nThe vehicle must be returned in the same condition in which it was provided, allowing for reasonable wear from normal use.\n\nThe renter may be responsible for new bodywork, paint, glass, tyre, wheel, or interior damage; damage caused by collision, misuse, negligence, or improper driving; damage resulting from driving on unsuitable roads or in prohibited areas; damage caused by an unauthorised driver; damage caused by incorrect fuel, lost keys, or failure to secure the vehicle; and towing, recovery, assessment, repair, administration, and loss-of-use charges where applicable.\n\nAny damage waiver or protection package is subject to its stated exclusions and excess or deductible. It does not automatically remove all financial responsibility.\n\nThe renter must report any accident, theft, vandalism, or damage as soon as reasonably possible and must follow the company reporting instructions. Where required, the renter must also contact the police and obtain an official report or reference number.\n\nThe renter must not arrange or authorise repairs without prior written approval from the rental company, except where necessary to protect personal safety or prevent further immediate damage.\n\nDamage charges will be supported by available inspection records, photographs, repair estimates, invoices, or other reasonable evidence. Any security deposit may be held or applied toward valid charges under the rental agreement.',
},
fr: {
fuelPolicy: 'Le vehicule est fourni avec le niveau de carburant ou le pourcentage de charge de batterie electrique indique sur le contrat de location. Le locataire doit restituer le vehicule avec le meme niveau de carburant ou, pour les vehicules electriques, le meme pourcentage de charge de batterie releve a la prise en charge.\n\nSi le vehicule est restitue avec moins de carburant ou un pourcentage de charge electrique inferieur, le carburant manquant ou l ecart de charge sera facture au tarif de ravitaillement ou de recharge applicable, avec les frais de service configures le cas echeant. Les frais sont calcules selon la jauge de carburant ou le pourcentage de batterie du vehicule et les niveaux releves au debut et a la fin de la location.\n\nLe carburant ou la recharge achete pendant la location reste a la charge du locataire et n est pas remboursable. Le locataire doit conserver les recus de carburant ou de recharge jusqu a la restitution du vehicule et la cloture du contrat de location.\n\nLe locataire doit utiliser le type de carburant ou la methode de recharge corrects indiques pour le vehicule. Tout dommage cause par l utilisation d un mauvais carburant, chargeur, connecteur ou mode de recharge sera facture au locataire.',
fuelPolicyNote: 'Veuillez restituer le vehicule avec le meme niveau de carburant qu a la prise en charge. Les vehicules electriques doivent etre restitues avec le meme pourcentage de charge de batterie releve a la prise en charge. Les vehicules restitues avec moins de carburant ou une charge de batterie inferieure feront l objet de frais de ravitaillement ou de recharge et de service. Utilisez uniquement le type de carburant ou la methode de recharge indiques pour le vehicule.',
additionalDriverPolicy: 'Seuls les conducteurs inscrits et approuves sur le contrat de location sont autorises a conduire le vehicule.\n\nCette politique conducteur s applique au locataire principal et a chaque conducteur supplementaire. Chaque conducteur doit respecter l age minimum requis, presenter un permis de conduire valide accepte sur le lieu de location, fournir toute piece d identification supplementaire raisonnablement demandee et respecter toutes les conditions du contrat de location.\n\nLes conducteurs ages de 25 ans ou plus beneficient du tarif normal. Les conducteurs ages de 18 a 24 ans peuvent etre acceptes avec des frais supplementaires jeune conducteur. Ces frais peuvent s appliquer au locataire principal ou a tout conducteur supplementaire dans cette tranche d age, selon le mode de facturation et les regles de tarification selectionnes.\n\nDes frais de conducteur supplementaire peuvent s appliquer par jour ou par location selon le mode de facturation selectionne. Les frais et les taxes applicables seront confirmes avant le debut de la location.\n\nLe locataire principal reste responsable du vehicule ainsi que de tous les frais, dommages, amendes, penalites et manquements au contrat de location, quel que soit le conducteur approuve qui utilisait le vehicule.\n\nAutoriser une personne non approuvee a conduire le vehicule constitue une violation du contrat de location et peut annuler toute franchise, protection ou couverture d assurance.',
damagePolicy: 'Le locataire est responsable de l inspection du vehicule lors de la prise en charge et doit confirmer que tous les dommages existants sont indiques sur le contrat de location ou le rapport d etat du vehicule. Tout dommage non enregistre doit etre signale avant que le vehicule ne soit conduit.\n\nLe vehicule doit etre restitue dans le meme etat que celui dans lequel il a ete fourni, sous reserve de l usure raisonnable liee a une utilisation normale.\n\nLe locataire peut etre responsable des nouveaux dommages de carrosserie, peinture, vitrage, pneus, roues ou interieur; des dommages causes par collision, mauvaise utilisation, negligence ou conduite inappropriee; des dommages resultant d une conduite sur routes inadaptees ou dans des zones interdites; des dommages causes par un conducteur non autorise; des dommages causes par un mauvais carburant, la perte des cles ou le defaut de securisation du vehicule; ainsi que des frais de remorquage, recuperation, evaluation, reparation, administration et immobilisation le cas echeant.\n\nToute franchise, garantie ou protection est soumise a ses exclusions, limites et franchises indiquees. Elle ne supprime pas automatiquement toute responsabilite financiere.\n\nLe locataire doit signaler tout accident, vol, vandalisme ou dommage des que raisonnablement possible et suivre les instructions de declaration de la societe. Lorsque cela est requis, le locataire doit egalement contacter la police et obtenir un rapport officiel ou un numero de reference.\n\nLe locataire ne doit pas organiser ni autoriser des reparations sans accord ecrit prealable de la societe de location, sauf si cela est necessaire pour proteger la securite des personnes ou empecher un dommage immediat supplementaire.\n\nLes frais de dommage seront justifies par les elements disponibles, notamment rapports d inspection, photos, devis, factures ou autres preuves raisonnables. Tout depot de garantie peut etre retenu ou impute sur les frais valables prevus au contrat de location.',
},
ar: {
fuelPolicy: 'يتم تسليم المركبة بمستوى الوقود أو نسبة شحن البطارية الكهربائية المسجلة في عقد الإيجار. يجب على المستأجر إعادة المركبة بنفس مستوى الوقود أو، بالنسبة للمركبات الكهربائية، بنفس نسبة شحن البطارية المسجلة عند الاستلام.\n\nإذا أعيدت المركبة بمستوى وقود أقل أو بنسبة شحن بطارية كهربائية أقل، يتحمل المستأجر تكلفة الوقود الناقص أو فرق الشحن وفق سعر التزويد أو الشحن المعتمد، إضافة إلى أي رسوم خدمة محددة. تعتمد الرسوم على عداد الوقود أو نسبة شحن البطارية والمستويات المسجلة عند بداية ونهاية الإيجار.\n\nالوقود أو الشحن الذي يتم شراؤه أثناء فترة الإيجار يبقى على مسؤولية المستأجر ولا يكون قابلا للاسترداد. يجب على المستأجر الاحتفاظ بإيصالات الوقود أو الشحن إلى حين إعادة المركبة وإغلاق عقد الإيجار.\n\nيجب على المستأجر استخدام نوع الوقود أو طريقة الشحن الصحيحة المحددة للمركبة. أي ضرر ناتج عن استخدام وقود أو شاحن أو موصل أو طريقة شحن غير صحيحة يتم تحميله على المستأجر.',
fuelPolicyNote: 'يرجى إعادة المركبة بنفس مستوى الوقود المقدم عند الاستلام. يجب إعادة المركبات الكهربائية بنفس نسبة شحن البطارية المسجلة عند الاستلام. المركبات التي تعاد بوقود أقل أو بنسبة شحن أقل تخضع لرسوم تزويد أو شحن ورسوم خدمة. استخدم فقط نوع الوقود أو طريقة الشحن المحددة للمركبة.',
additionalDriverPolicy: 'يسمح بقيادة المركبة فقط للسائقين المدرجين والمعتمدين في عقد الإيجار.\n\nتنطبق سياسة السائق هذه على المستأجر الرئيسي وكل سائق إضافي. يجب على كل سائق استيفاء شرط السن الأدنى، وتقديم رخصة قيادة صالحة ومقبولة في موقع الإيجار، وتقديم أي وثائق تعريف إضافية مطلوبة بشكل معقول، والالتزام بجميع شروط عقد الإيجار.\n\nالسائقون بعمر 25 سنة أو أكثر يخضعون للسعر العادي. السائقون من 18 إلى 24 سنة يمكن قبولهم مع تطبيق رسوم إضافية للسائق الشاب. قد تطبق هذه الرسوم على المستأجر الرئيسي أو على أي سائق إضافي ضمن هذه الفئة العمرية، حسب إعداد الرسوم وقواعد التسعير المحددة.\n\nقد تطبق رسوم السائق الإضافي يوميا أو لكل إيجار حسب إعداد الرسوم المحدد. يتم تأكيد الرسوم وأي ضرائب مطبقة قبل بداية الإيجار.\n\nيبقى المستأجر الرئيسي مسؤولا عن المركبة وعن جميع الرسوم والأضرار والغرامات والمخالفات وأي إخلال بعقد الإيجار، بغض النظر عن السائق المعتمد الذي كان يقود المركبة.\n\nالسماح لشخص غير مصرح له بقيادة المركبة يعد إخلالا بعقد الإيجار وقد يؤدي إلى إبطال أي إعفاء من الضرر أو باقة حماية أو تغطية تأمينية.',
damagePolicy: 'يتحمل المستأجر مسؤولية فحص المركبة عند الاستلام والتأكد من تسجيل جميع الأضرار الموجودة في عقد الإيجار أو تقرير حالة المركبة. يجب الإبلاغ عن أي ضرر غير مسجل قبل قيادة المركبة.\n\nيجب إعادة المركبة بنفس الحالة التي سلمت بها، مع السماح بالاستهلاك المعقول الناتج عن الاستخدام العادي.\n\nقد يتحمل المستأجر مسؤولية الأضرار الجديدة في الهيكل أو الطلاء أو الزجاج أو الإطارات أو العجلات أو المقصورة الداخلية؛ والأضرار الناتجة عن التصادم أو سوء الاستخدام أو الإهمال أو القيادة غير السليمة؛ والأضرار الناتجة عن القيادة في طرق غير مناسبة أو مناطق محظورة؛ والأضرار التي يسببها سائق غير مصرح له؛ والأضرار الناتجة عن استخدام وقود غير صحيح أو فقدان المفاتيح أو عدم تأمين المركبة؛ ورسوم السحب والاسترجاع والتقييم والإصلاح والإدارة وفقدان الاستخدام متى كانت مطبقة.\n\nأي إعفاء من الضرر أو باقة حماية يخضع للاستثناءات والحدود ومبلغ التحمل المحدد له. ولا يلغي ذلك تلقائيا جميع المسؤوليات المالية.\n\nيجب على المستأجر الإبلاغ عن أي حادث أو سرقة أو تخريب أو ضرر في أقرب وقت ممكن بشكل معقول واتباع تعليمات الشركة الخاصة بالإبلاغ. وعند الاقتضاء، يجب على المستأجر أيضا التواصل مع الشرطة والحصول على تقرير رسمي أو رقم مرجعي.\n\nلا يجوز للمستأجر ترتيب أو السماح بأي إصلاحات دون موافقة كتابية مسبقة من شركة التأجير، إلا عندما يكون ذلك ضروريا لحماية السلامة الشخصية أو منع ضرر فوري إضافي.\n\nتدعم رسوم الأضرار بسجلات الفحص والصور وتقديرات الإصلاح والفواتير أو أي أدلة معقولة أخرى متاحة. يجوز حجز مبلغ الضمان أو استخدامه لتغطية الرسوم الصحيحة بموجب عقد الإيجار.',
},
} as const
type RentalPolicyTemplateKey = keyof typeof rentalPolicyTemplates.en
function shouldUseRentalPolicyTemplate(value: string | null | undefined, key: RentalPolicyTemplateKey): boolean {
const text = value?.trim()
if (!text) return true
return Object.values(rentalPolicyTemplates).some((template) => template[key] === value)
}
function withRentalPolicyDefaults(settings: ContractSettings | null, language: keyof typeof rentalPolicyTemplates): ContractSettings {
const policyTemplate = rentalPolicyTemplates[language]
return {
fuelPolicy: shouldUseRentalPolicyTemplate(settings?.fuelPolicy, 'fuelPolicy') ? policyTemplate.fuelPolicy : settings!.fuelPolicy,
fuelPolicyType: settings?.fuelPolicyType || 'FULL_TO_FULL',
fuelPolicyNote: shouldUseRentalPolicyTemplate(settings?.fuelPolicyNote, 'fuelPolicyNote') ? policyTemplate.fuelPolicyNote : settings!.fuelPolicyNote,
additionalDriverCharge: settings?.additionalDriverCharge || 'FREE',
additionalDriverDailyRate: settings?.additionalDriverDailyRate ?? 0,
additionalDriverFlatRate: settings?.additionalDriverFlatRate ?? 0,
additionalDriverPolicy: shouldUseRentalPolicyTemplate(settings?.additionalDriverPolicy, 'additionalDriverPolicy') ? policyTemplate.additionalDriverPolicy : settings!.additionalDriverPolicy,
damagePolicy: shouldUseRentalPolicyTemplate(settings?.damagePolicy, 'damagePolicy') ? policyTemplate.damagePolicy : settings!.damagePolicy,
}
}
const sectionIcons = {
building: Building2,
palette: Palette,
@@ -146,60 +191,66 @@ function SettingsPageContent() {
en: {
title: 'Settings', subtitle: 'Manage one settings area at a time. Access is resolved from your subscription and role.',
loading: 'Loading settings...', save: 'Saving...', saved: 'Saved.', saveSection: 'Save section', currentPlan: 'Current plan',
applyPolicyTemplate: 'Apply language template',
subscriptionStatus: 'Subscription status', manageSubscription: 'Manage subscription', requiredPlan: 'Requires',
lockedTitle: 'This settings area is not included in your current plan.', lockedBody: 'Your saved configuration is preserved and becomes editable again after upgrade.',
readOnly: 'This section is read-only while your subscription access is restricted.', companyHint: 'Default language affects Carplace text and generated contracts when available.',
publicProfile: 'Public profile', carplaceBasics: 'Carplace basics', premiumBranding: 'Available on GROWTH',
paymentsBody: 'Configure renter payment providers. Secret values are never shown after saving.',
policies: 'Fuel and damage policies', additionalDriver: 'Additional-driver automation', insuranceNew: 'New insurance policy',
policies: 'Fuel, driver, and damage policies', additionalDriver: 'Additional-driver automation', insuranceNew: 'New insurance policy',
pricingNew: 'New pricing rule', accountingSchedule: 'Scheduled delivery is available on PRO.', noItems: 'No records configured yet.',
carplace: 'Listed on Carplace', logo: 'Logo', hero: 'Hero image', upload: 'Upload', active: 'Active', inactive: 'Inactive',
labels: {
displayName: 'Display name', tagline: 'Tagline', publicEmail: 'Public email', publicPhone: 'Public phone', whatsapp: 'WhatsApp number',
city: 'City', country: 'Country', websiteUrl: 'Website URL', defaultLocale: 'Default language', defaultCurrency: 'Default currency',
primaryColor: 'Primary color', accentColor: 'Accent color', amanpayMerchantId: 'AmanPay merchant ID', amanpaySecretKey: 'AmanPay secret key',
paypalEmail: 'PayPal business email', fuelPolicyType: 'Fuel policy type', fuelPolicyNote: 'Fuel policy note', damagePolicy: 'Damage policy',
additionalDriverCharge: 'Additional driver charge', dailyRate: 'Daily rate', flatRate: 'Flat rate', accountantName: 'Accountant name',
paypalEmail: 'PayPal business email', fuelPolicy: 'Fuel policy', fuelPolicyType: 'Fuel policy type', fuelPolicyNote: 'Fuel policy note',
damagePolicy: 'Damage policy', additionalDriverPolicy: 'Driver policy', additionalDriverCharge: 'Additional driver charge',
dailyRate: 'Daily rate', flatRate: 'Flat rate', accountantName: 'Accountant name',
accountantEmail: 'Accountant email', reportingPeriod: 'Reporting period', reportFormat: 'Report format', autoSend: 'Auto-send reports',
},
},
fr: {
title: 'Paramètres', subtitle: 'Gérez une section à la fois. Laccès vient de votre abonnement et de votre rôle.',
loading: 'Chargement des paramètres...', save: 'Enregistrement...', saved: 'Enregistré.', saveSection: 'Enregistrer', currentPlan: 'Plan actuel',
applyPolicyTemplate: 'Appliquer le modèle',
subscriptionStatus: 'Statut abonnement', manageSubscription: 'Gérer labonnement', requiredPlan: 'Requiert',
lockedTitle: 'Cette section nest pas incluse dans votre plan actuel.', lockedBody: 'La configuration enregistrée est conservée et redevient modifiable après mise à niveau.',
readOnly: 'Cette section est en lecture seule pendant la restriction daccès.', companyHint: 'La langue par défaut affecte la vitrine et les contrats générés si disponibles.',
publicProfile: 'Profil public', carplaceBasics: 'Paramètres Carplace', premiumBranding: 'Disponible avec GROWTH',
paymentsBody: 'Configurez les prestataires de paiement. Les secrets ne sont jamais affichés après enregistrement.',
policies: 'Carburant et dommages', additionalDriver: 'Automatisation conducteur additionnel', insuranceNew: 'Nouvelle police',
policies: 'Carburant, conducteur et dommages', additionalDriver: 'Automatisation conducteur additionnel', insuranceNew: 'Nouvelle police',
pricingNew: 'Nouvelle règle', accountingSchedule: 'Lenvoi planifié est disponible avec PRO.', noItems: 'Aucun enregistrement.',
carplace: 'Publié sur Carplace', logo: 'Logo', hero: 'Image principale', upload: 'Téléverser', active: 'Actif', inactive: 'Inactif',
labels: {
displayName: 'Nom affiché', tagline: 'Slogan', publicEmail: 'E-mail public', publicPhone: 'Téléphone public', whatsapp: 'Numéro WhatsApp',
city: 'Ville', country: 'Pays', websiteUrl: 'Site web', defaultLocale: 'Langue par défaut', defaultCurrency: 'Devise par défaut',
primaryColor: 'Couleur principale', accentColor: 'Couleur secondaire', amanpayMerchantId: 'ID marchand AmanPay', amanpaySecretKey: 'Clé secrète AmanPay',
paypalEmail: 'Email PayPal business', fuelPolicyType: 'Type de politique carburant', fuelPolicyNote: 'Note carburant', damagePolicy: 'Politique dommages',
additionalDriverCharge: 'Supplément conducteur', dailyRate: 'Tarif journalier', flatRate: 'Tarif fixe', accountantName: 'Nom du comptable',
paypalEmail: 'Email PayPal business', fuelPolicy: 'Politique carburant', fuelPolicyType: 'Type de politique carburant', fuelPolicyNote: 'Note carburant',
damagePolicy: 'Politique dommages', additionalDriverPolicy: 'Politique conducteur', additionalDriverCharge: 'Supplément conducteur',
dailyRate: 'Tarif journalier', flatRate: 'Tarif fixe', accountantName: 'Nom du comptable',
accountantEmail: 'E-mail du comptable', reportingPeriod: 'Période', reportFormat: 'Format', autoSend: 'Envoi automatique',
},
},
ar: {
title: 'الإعدادات', subtitle: 'إدارة قسم واحد في كل مرة. يتم تحديد الوصول من الاشتراك والدور.',
loading: 'جارٍ تحميل الإعدادات...', save: 'جارٍ الحفظ...', saved: 'تم الحفظ.', saveSection: 'حفظ القسم', currentPlan: 'الخطة الحالية',
applyPolicyTemplate: 'تطبيق نموذج اللغة',
subscriptionStatus: 'حالة الاشتراك', manageSubscription: 'إدارة الاشتراك', requiredPlan: 'يتطلب',
lockedTitle: 'هذا القسم غير مشمول في خطتك الحالية.', lockedBody: 'يتم الاحتفاظ بالإعدادات المحفوظة وتعود قابلة للتعديل بعد الترقية.',
readOnly: 'هذا القسم للقراءة فقط أثناء تقييد الوصول.', companyHint: 'تؤثر اللغة الافتراضية على الواجهة والعقود عند توفرها.',
publicProfile: 'الملف العام', carplaceBasics: 'أساسيات الواجهة', premiumBranding: 'متاح في GROWTH',
paymentsBody: 'إعداد مزودي دفع المستأجرين. لا يتم عرض المفاتيح السرية بعد الحفظ.',
policies: 'سياسات الوقود والأضرار', additionalDriver: 'أتمتة السائق الإضافي', insuranceNew: 'سياسة تأمين جديدة',
policies: 'سياسات الوقود والسائق والأضرار', additionalDriver: 'أتمتة السائق الإضافي', insuranceNew: 'سياسة تأمين جديدة',
pricingNew: 'قاعدة تسعير جديدة', accountingSchedule: 'الإرسال المجدول متاح في PRO.', noItems: 'لا توجد سجلات بعد.',
carplace: 'مدرج على Carplace', logo: 'الشعار', hero: 'صورة الواجهة', upload: 'رفع', active: 'نشط', inactive: 'غير نشط',
labels: {
displayName: 'اسم العرض', tagline: 'الشعار', publicEmail: 'البريد العام', publicPhone: 'الهاتف العام', whatsapp: 'رقم واتساب',
city: 'المدينة', country: 'الدولة', websiteUrl: 'الموقع', defaultLocale: 'اللغة الافتراضية', defaultCurrency: 'العملة الافتراضية',
primaryColor: 'اللون الأساسي', accentColor: 'لون التمييز', amanpayMerchantId: 'معرف تاجر AmanPay', amanpaySecretKey: 'مفتاح AmanPay السري',
paypalEmail: 'بريد PayPal التجاري', fuelPolicyType: 'سياسة الوقود', fuelPolicyNote: 'ملاحظة الوقود', damagePolicy: 'سياسة الأضرار',
additionalDriverCharge: 'رسوم السائق الإضافي', dailyRate: 'السعر اليومي', flatRate: 'السعر الثابت', accountantName: 'اسم المحاسب',
paypalEmail: 'بريد PayPal التجاري', fuelPolicy: 'سياسة الوقود', fuelPolicyType: 'نوع سياسة الوقود', fuelPolicyNote: 'ملاحظة الوقود',
damagePolicy: 'سياسة الأضرار', additionalDriverPolicy: 'سياسة السائق', additionalDriverCharge: 'رسوم السائق الإضافي',
dailyRate: 'السعر اليومي', flatRate: 'السعر الثابت', accountantName: 'اسم المحاسب',
accountantEmail: 'بريد المحاسب', reportingPeriod: 'فترة التقرير', reportFormat: 'تنسيق التقرير', autoSend: 'إرسال تلقائي',
},
},
@@ -257,10 +308,7 @@ function SettingsPageContent() {
setBrand(await apiFetch<BrandSettings | null>('/companies/me/brand'))
}
if (activeSection === 'rental-policies' && !contractSettings) {
setContractSettings(await apiFetch<ContractSettings | null>('/companies/me/contract-settings') ?? {
fuelPolicyType: 'FULL_TO_FULL', fuelPolicyNote: '', additionalDriverCharge: 'FREE',
additionalDriverDailyRate: 0, additionalDriverFlatRate: 0, damagePolicy: '',
})
setContractSettings(withRentalPolicyDefaults(await apiFetch<ContractSettings | null>('/companies/me/contract-settings'), language))
}
if (activeSection === 'insurance' && isAvailable('settings.insurance_policies') && insurancePolicies.length === 0) {
setInsurancePolicies(await apiFetch<InsurancePolicy[]>('/companies/me/insurance-policies'))
@@ -278,7 +326,12 @@ function SettingsPageContent() {
}
}
loadSection()
}, [activeSection, accountingSettings, brand, contractSettings, insurancePolicies.length, pricingRules.length, entitlements])
}, [activeSection, accountingSettings, brand, contractSettings, insurancePolicies.length, pricingRules.length, entitlements, language])
useEffect(() => {
if (activeSection !== 'rental-policies') return
setContractSettings((current) => current ? withRentalPolicyDefaults(current, language) : current)
}, [activeSection, language])
async function saveBrand() {
if (!brand) return
@@ -327,14 +380,7 @@ function SettingsPageContent() {
if (!contractSettings) return
setSaving(true); setError(null); setMessage(null)
try {
const body = canEdit('settings.additional_driver_fees')
? contractSettings
: {
fuelPolicyType: contractSettings.fuelPolicyType,
fuelPolicyNote: contractSettings.fuelPolicyNote,
damagePolicy: contractSettings.damagePolicy,
}
await apiFetch('/companies/me/contract-settings', { method: 'PATCH', body: JSON.stringify(body) })
await apiFetch('/companies/me/contract-settings', { method: 'PATCH', body: JSON.stringify(contractSettings) })
setMessage(copy.saved)
} catch (err: any) {
setError(err.message ?? 'Failed to save contract settings')
@@ -343,6 +389,18 @@ function SettingsPageContent() {
}
}
function applyRentalPolicyTemplate() {
if (!contractSettings) return
const policyTemplate = rentalPolicyTemplates[language]
setContractSettings({
...contractSettings,
fuelPolicy: policyTemplate.fuelPolicy,
fuelPolicyNote: policyTemplate.fuelPolicyNote,
additionalDriverPolicy: policyTemplate.additionalDriverPolicy,
damagePolicy: policyTemplate.damagePolicy,
})
}
async function saveAccountingSettings() {
if (!accountingSettings) return
setSaving(true); setError(null); setMessage(null)
@@ -500,15 +558,19 @@ function SettingsPageContent() {
{activeSection === 'rental-policies' && contractSettings && (
<SectionCard onSave={saveContractSettings} saving={saving} copy={copy}>
<div className="mb-4 flex justify-end">
<button type="button" className="btn-secondary text-xs" disabled={!canEdit('settings.rental_policies_basic')} onClick={applyRentalPolicyTemplate}>{copy.applyPolicyTemplate}</button>
</div>
<div className="grid gap-4 lg:grid-cols-2">
<Select label={copy.labels.fuelPolicyType} value={contractSettings.fuelPolicyType} disabled={!canEdit('settings.rental_policies_basic')} options={['FULL_TO_FULL', 'FULL_TO_EMPTY', 'SAME_TO_SAME', 'PREPAID', 'FREE']} onChange={(v) => setContractSettings({ ...contractSettings, fuelPolicyType: v })} />
<Select label={copy.labels.additionalDriverCharge} value={contractSettings.additionalDriverCharge} disabled={!canEdit('settings.additional_driver_fees')} options={['FREE', 'PER_DAY', 'FLAT']} onChange={(v) => setContractSettings({ ...contractSettings, additionalDriverCharge: v as ContractSettings['additionalDriverCharge'] })} />
{contractSettings.additionalDriverCharge === 'PER_DAY' && <Input label={copy.labels.dailyRate} type="number" value={contractSettings.additionalDriverDailyRate} disabled={!canEdit('settings.additional_driver_fees')} onChange={(v) => setContractSettings({ ...contractSettings, additionalDriverDailyRate: Math.max(0, Number(v)) })} />}
{contractSettings.additionalDriverCharge === 'FLAT' && <Input label={copy.labels.flatRate} type="number" value={contractSettings.additionalDriverFlatRate} disabled={!canEdit('settings.additional_driver_fees')} onChange={(v) => setContractSettings({ ...contractSettings, additionalDriverFlatRate: Math.max(0, Number(v)) })} />}
<Select label={copy.labels.additionalDriverCharge} value={contractSettings.additionalDriverCharge} disabled={!canEdit('settings.rental_policies_basic')} options={['FREE', 'PER_DAY', 'FLAT']} onChange={(v) => setContractSettings({ ...contractSettings, additionalDriverCharge: v as ContractSettings['additionalDriverCharge'] })} />
{contractSettings.additionalDriverCharge === 'PER_DAY' && <Input label={copy.labels.dailyRate} type="number" value={contractSettings.additionalDriverDailyRate} disabled={!canEdit('settings.rental_policies_basic')} onChange={(v) => setContractSettings({ ...contractSettings, additionalDriverDailyRate: Math.max(0, Number(v)) })} />}
{contractSettings.additionalDriverCharge === 'FLAT' && <Input label={copy.labels.flatRate} type="number" value={contractSettings.additionalDriverFlatRate} disabled={!canEdit('settings.rental_policies_basic')} onChange={(v) => setContractSettings({ ...contractSettings, additionalDriverFlatRate: Math.max(0, Number(v)) })} />}
<Textarea label={copy.labels.fuelPolicy} value={contractSettings.fuelPolicy ?? ''} disabled={!canEdit('settings.rental_policies_basic')} onChange={(v) => setContractSettings({ ...contractSettings, fuelPolicy: v })} />
<Textarea label={copy.labels.fuelPolicyNote} value={contractSettings.fuelPolicyNote ?? ''} disabled={!canEdit('settings.rental_policies_basic')} onChange={(v) => setContractSettings({ ...contractSettings, fuelPolicyNote: v })} />
<Textarea label={copy.labels.additionalDriverPolicy} value={contractSettings.additionalDriverPolicy ?? ''} disabled={!canEdit('settings.rental_policies_basic')} onChange={(v) => setContractSettings({ ...contractSettings, additionalDriverPolicy: v })} />
<Textarea label={copy.labels.damagePolicy} value={contractSettings.damagePolicy ?? ''} disabled={!canEdit('settings.rental_policies_basic')} onChange={(v) => setContractSettings({ ...contractSettings, damagePolicy: v })} />
</div>
{!isAvailable('settings.additional_driver_fees') && <UpgradePanel title={copy.additionalDriver} />}
</SectionCard>
)}
@@ -294,6 +294,7 @@ type VehicleDetailDict = {
saveLabel: string
savingLabel: string
dateRequired: string
carMaintenance: string
routineTypes: Record<string, string>
}
@@ -532,11 +533,13 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
saveLabel: 'Save',
savingLabel: 'Saving…',
dateRequired: 'Service date is required.',
carMaintenance: 'Car Maintenance',
routineTypes: {
'Oil Change': 'Oil Change',
'Technical Inspection': 'Technical Inspection',
'Vehicle Registration': 'Vehicle Registration',
'Car Tax': 'Car Tax',
'Insurance': 'Insurance',
'Tire Rotation': 'Tire Rotation',
'Brake Inspection': 'Brake Inspection',
'Air Filter': 'Air Filter',
@@ -545,6 +548,15 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
'Belt / Chain Service': 'Belt / Chain Service',
'Coolant Flush': 'Coolant Flush',
'Transmission Service': 'Transmission Service',
'Wheel Alignment': 'Wheel Alignment',
'Wheel Balancing': 'Wheel Balancing',
'Spark Plugs': 'Spark Plugs',
'Fuel Filter': 'Fuel Filter',
'Wiper Blades': 'Wiper Blades',
'AC Service': 'AC Service',
'Suspension Check': 'Suspension Check',
'Engine Diagnostic': 'Engine Diagnostic',
'Brake Pads': 'Brake Pads',
},
},
calendar: {
@@ -890,11 +902,13 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
saveLabel: 'Enregistrer',
savingLabel: 'Enregistrement…',
dateRequired: 'La date du service est requise.',
carMaintenance: 'Entretien voiture',
routineTypes: {
'Oil Change': "Vidange d'huile",
'Technical Inspection': 'Contrôle technique',
'Vehicle Registration': 'Carte grise',
'Car Tax': 'Vignette automobile',
'Insurance': 'Assurance',
'Tire Rotation': 'Rotation des pneus',
'Brake Inspection': 'Contrôle des freins',
'Air Filter': 'Filtre à air',
@@ -903,6 +917,15 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
'Belt / Chain Service': 'Courroie / Chaîne',
'Coolant Flush': 'Liquide de refroidissement',
'Transmission Service': 'Entretien transmission',
'Wheel Alignment': 'Parallélisme',
'Wheel Balancing': 'Équilibrage des roues',
'Spark Plugs': 'Bougies',
'Fuel Filter': 'Filtre à carburant',
'Wiper Blades': "Balais d'essuie-glace",
'AC Service': 'Climatisation',
'Suspension Check': 'Contrôle suspension',
'Engine Diagnostic': 'Diagnostic moteur',
'Brake Pads': 'Plaquettes de frein',
},
},
calendar: {
@@ -1248,11 +1271,13 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
saveLabel: 'حفظ',
savingLabel: 'جارٍ الحفظ…',
dateRequired: 'تاريخ الخدمة مطلوب.',
carMaintenance: 'صيانة السيارة',
routineTypes: {
'Oil Change': 'تغيير الزيت',
'Technical Inspection': 'الفحص التقني',
'Vehicle Registration': 'تسجيل المركبة',
'Car Tax': 'الضريبة السنوية',
'Insurance': 'التأمين',
'Tire Rotation': 'تدوير الإطارات',
'Brake Inspection': 'فحص الفرامل',
'Air Filter': 'فلتر الهواء',
@@ -1261,6 +1286,15 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
'Belt / Chain Service': 'خدمة السير / السلسلة',
'Coolant Flush': 'تغيير سائل التبريد',
'Transmission Service': 'خدمة ناقل الحركة',
'Wheel Alignment': 'ضبط زوايا العجلات',
'Wheel Balancing': 'ترصيص العجلات',
'Spark Plugs': 'شمعات الإشعال',
'Fuel Filter': 'فلتر الوقود',
'Wiper Blades': 'مساحات الزجاج',
'AC Service': 'صيانة المكيف',
'Suspension Check': 'فحص نظام التعليق',
'Engine Diagnostic': 'تشخيص المحرك',
'Brake Pads': 'صفائح الفرامل',
},
},
calendar: {
@@ -2,7 +2,7 @@
import Link from 'next/link'
import { Bell, Search, Settings } from 'lucide-react'
import { usePathname, useRouter } from 'next/navigation'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import { useState, useEffect } from 'react'
import { io } from 'socket.io-client'
import { EMPLOYEE_PROFILE_KEY, apiFetch } from '@/lib/api'
@@ -33,11 +33,20 @@ function resolveSocketOrigin(): string | null {
return window.location.origin
}
const SEARCHABLE_ROUTES = ['/reservations', '/fleet', '/customers', '/contracts', '/billing']
function getSearchTarget(appPath: string): string {
const match = SEARCHABLE_ROUTES.find((route) => appPath === route || appPath.startsWith(`${route}/`))
return match ?? '/reservations'
}
export default function TopBar() {
const { dict } = useDashboardI18n()
const pathname = usePathname()
const appPath = toDashboardAppPath(pathname)
const router = useRouter()
const searchParams = useSearchParams()
const [globalSearch, setGlobalSearch] = useState('')
const [unreadCount, setUnreadCount] = useState(0)
const [showNotifs, setShowNotifs] = useState(false)
const [userInitials, setUserInitials] = useState('W')
@@ -54,6 +63,10 @@ export default function TopBar() {
const [mounted, setMounted] = useState(false)
useEffect(() => { setMounted(true) }, [])
useEffect(() => {
setGlobalSearch(searchParams.get('search') ?? '')
}, [searchParams])
const title = (() => {
if (!mounted) return dict.titles['/']
if (dict.titles[appPath]) return dict.titles[appPath]
@@ -193,18 +206,35 @@ export default function TopBar() {
router.push('/notifications')
}
function submitSearch(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
const query = globalSearch.trim()
const target = getSearchTarget(appPath)
const params = new URLSearchParams()
if (query) {
params.set('search', query)
router.push(`${target}?${params.toString()}`)
} else {
router.push(target)
}
}
return (
<header className="relative z-[70] flex h-16 items-center justify-between border-b border-blue-200/70 bg-white/82 px-6 text-blue-950 backdrop-blur-xl transition-colors dark:border-blue-400/10 dark:bg-[#0a0f1a]/90 dark:text-slate-100">
<div className="flex items-center gap-4 min-w-0">
<h1 className="shrink-0 text-lg font-semibold text-blue-950 dark:text-slate-50">{title}</h1>
<div className="relative hidden w-[min(380px,38vw)] sm:block">
<form className="relative hidden w-[min(380px,38vw)] sm:block" onSubmit={submitSearch}>
<Search className="pointer-events-none absolute left-3.5 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-500" />
<input
type="search"
placeholder="Search vehicles, bookings, customers..."
value={globalSearch}
onChange={(event) => setGlobalSearch(event.target.value)}
className="h-10 w-full rounded-lg border border-blue-200/80 bg-blue-50/70 pl-10 pr-4 text-sm text-blue-950 outline-none transition-all placeholder:text-slate-500 focus:border-blue-400/45 focus:bg-white focus:ring-4 focus:ring-blue-500/10 dark:border-blue-400/15 dark:bg-blue-500/[0.06] dark:text-slate-100 dark:focus:bg-blue-500/10"
/>
</div>
</form>
<div className="hidden items-center gap-2 text-sm text-slate-400 xl:flex">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-400 shadow-[0_0_0_5px_rgba(16,185,129,0.10)]" />
System operational
@@ -0,0 +1,2 @@
ALTER TABLE "contract_settings"
ADD COLUMN "additionalDriverPolicy" TEXT NOT NULL DEFAULT 'Only authorized additional drivers listed on the rental agreement may operate the vehicle.';
@@ -0,0 +1,76 @@
ALTER TABLE "contract_settings"
ALTER COLUMN "fuelPolicy" SET DEFAULT 'The vehicle will be provided with the fuel level recorded on the rental agreement. The renter must return the vehicle with the same fuel level.
If the vehicle is returned with less fuel, the renter will be charged for the missing fuel at the applicable refueling rate, plus any configured refueling service fee. Fuel charges are based on the vehicle fuel gauge and the level recorded at the start and end of the rental.
Fuel purchased during the rental is the renter responsibility and is not refundable. The renter should retain fuel receipts until the vehicle has been returned and the rental agreement has been closed.
The renter must use the correct fuel type specified for the vehicle. Any damage caused by using the wrong fuel will be charged to the renter.',
ALTER COLUMN "damagePolicy" SET DEFAULT 'The renter is responsible for inspecting the vehicle at collection and confirming that all existing damage is recorded on the rental agreement or vehicle condition report. Any unrecorded damage should be reported before the vehicle is driven.
The vehicle must be returned in the same condition in which it was provided, allowing for reasonable wear from normal use.
The renter may be responsible for new bodywork, paint, glass, tyre, wheel, or interior damage; damage caused by collision, misuse, negligence, or improper driving; damage resulting from driving on unsuitable roads or in prohibited areas; damage caused by an unauthorised driver; damage caused by incorrect fuel, lost keys, or failure to secure the vehicle; and towing, recovery, assessment, repair, administration, and loss-of-use charges where applicable.
Any damage waiver or protection package is subject to its stated exclusions and excess or deductible. It does not automatically remove all financial responsibility.
The renter must report any accident, theft, vandalism, or damage as soon as reasonably possible and must follow the company reporting instructions. Where required, the renter must also contact the police and obtain an official report or reference number.
The renter must not arrange or authorise repairs without prior written approval from the rental company, except where necessary to protect personal safety or prevent further immediate damage.
Damage charges will be supported by available inspection records, photographs, repair estimates, invoices, or other reasonable evidence. Any security deposit may be held or applied toward valid charges under the rental agreement.',
ALTER COLUMN "additionalDriverPolicy" SET DEFAULT 'Only drivers listed and approved on the rental agreement are permitted to drive the vehicle.
Each additional driver must meet the minimum age requirement, present a valid driving licence accepted in the rental location, provide any additional identification reasonably requested, and comply with all terms of the rental agreement.
An additional driver fee may apply per day or per rental according to the selected charge setting. The fee and any applicable taxes will be confirmed before the rental begins.
The primary renter remains responsible for the vehicle and for all charges, damage, fines, penalties, and breaches of the rental agreement, regardless of which approved driver was operating the vehicle.
Allowing an unauthorised person to drive the vehicle is a breach of the rental agreement and may invalidate any damage waiver, protection package, or insurance coverage.';
UPDATE "contract_settings"
SET "fuelPolicy" = 'The vehicle will be provided with the fuel level recorded on the rental agreement. The renter must return the vehicle with the same fuel level.
If the vehicle is returned with less fuel, the renter will be charged for the missing fuel at the applicable refueling rate, plus any configured refueling service fee. Fuel charges are based on the vehicle fuel gauge and the level recorded at the start and end of the rental.
Fuel purchased during the rental is the renter responsibility and is not refundable. The renter should retain fuel receipts until the vehicle has been returned and the rental agreement has been closed.
The renter must use the correct fuel type specified for the vehicle. Any damage caused by using the wrong fuel will be charged to the renter.'
WHERE "fuelPolicy" = 'The vehicle must be returned with the same fuel level as at pickup.'
OR "fuelPolicy" = '';
UPDATE "contract_settings"
SET "fuelPolicyNote" = 'Please return the vehicle with the same fuel level provided at collection. Vehicles returned with less fuel will be subject to a refueling charge and service fee. Use only the fuel type specified for the vehicle.'
WHERE "fuelPolicyNote" IS NULL
OR "fuelPolicyNote" = '';
UPDATE "contract_settings"
SET "additionalDriverPolicy" = 'Only drivers listed and approved on the rental agreement are permitted to drive the vehicle.
Each additional driver must meet the minimum age requirement, present a valid driving licence accepted in the rental location, provide any additional identification reasonably requested, and comply with all terms of the rental agreement.
An additional driver fee may apply per day or per rental according to the selected charge setting. The fee and any applicable taxes will be confirmed before the rental begins.
The primary renter remains responsible for the vehicle and for all charges, damage, fines, penalties, and breaches of the rental agreement, regardless of which approved driver was operating the vehicle.
Allowing an unauthorised person to drive the vehicle is a breach of the rental agreement and may invalidate any damage waiver, protection package, or insurance coverage.'
WHERE "additionalDriverPolicy" = 'Only authorized additional drivers listed on the rental agreement may operate the vehicle.'
OR "additionalDriverPolicy" = '';
UPDATE "contract_settings"
SET "damagePolicy" = 'The renter is responsible for inspecting the vehicle at collection and confirming that all existing damage is recorded on the rental agreement or vehicle condition report. Any unrecorded damage should be reported before the vehicle is driven.
The vehicle must be returned in the same condition in which it was provided, allowing for reasonable wear from normal use.
The renter may be responsible for new bodywork, paint, glass, tyre, wheel, or interior damage; damage caused by collision, misuse, negligence, or improper driving; damage resulting from driving on unsuitable roads or in prohibited areas; damage caused by an unauthorised driver; damage caused by incorrect fuel, lost keys, or failure to secure the vehicle; and towing, recovery, assessment, repair, administration, and loss-of-use charges where applicable.
Any damage waiver or protection package is subject to its stated exclusions and excess or deductible. It does not automatically remove all financial responsibility.
The renter must report any accident, theft, vandalism, or damage as soon as reasonably possible and must follow the company reporting instructions. Where required, the renter must also contact the police and obtain an official report or reference number.
The renter must not arrange or authorise repairs without prior written approval from the rental company, except where necessary to protect personal safety or prevent further immediate damage.
Damage charges will be supported by available inspection records, photographs, repair estimates, invoices, or other reasonable evidence. Any security deposit may be held or applied toward valid charges under the rental agreement.'
WHERE "damagePolicy" = 'The renter is liable for any damage not covered by insurance.'
OR "damagePolicy" = '';
@@ -0,0 +1,36 @@
ALTER TABLE "contract_settings"
ALTER COLUMN "additionalDriverPolicy" SET DEFAULT 'Only drivers listed and approved on the rental agreement are permitted to drive the vehicle.
The principal renter and every additional driver must meet the minimum age requirement, present a valid driving licence accepted in the rental location, provide any additional identification reasonably requested, and comply with all terms of the rental agreement.
Drivers aged 25 or older qualify for the normal rental rate. Drivers aged 18 to 24 may be accepted subject to an additional young driver fee. The fee may apply to the principal renter or to any additional driver in this age range, according to the selected charge setting and pricing rules.
An additional driver fee may apply per day or per rental according to the selected charge setting. The fee and any applicable taxes will be confirmed before the rental begins.
The primary renter remains responsible for the vehicle and for all charges, damage, fines, penalties, and breaches of the rental agreement, regardless of which approved driver was operating the vehicle.
Allowing an unauthorised person to drive the vehicle is a breach of the rental agreement and may invalidate any damage waiver, protection package, or insurance coverage.';
UPDATE "contract_settings"
SET "additionalDriverPolicy" = 'Only drivers listed and approved on the rental agreement are permitted to drive the vehicle.
The principal renter and every additional driver must meet the minimum age requirement, present a valid driving licence accepted in the rental location, provide any additional identification reasonably requested, and comply with all terms of the rental agreement.
Drivers aged 25 or older qualify for the normal rental rate. Drivers aged 18 to 24 may be accepted subject to an additional young driver fee. The fee may apply to the principal renter or to any additional driver in this age range, according to the selected charge setting and pricing rules.
An additional driver fee may apply per day or per rental according to the selected charge setting. The fee and any applicable taxes will be confirmed before the rental begins.
The primary renter remains responsible for the vehicle and for all charges, damage, fines, penalties, and breaches of the rental agreement, regardless of which approved driver was operating the vehicle.
Allowing an unauthorised person to drive the vehicle is a breach of the rental agreement and may invalidate any damage waiver, protection package, or insurance coverage.'
WHERE "additionalDriverPolicy" = 'Only drivers listed and approved on the rental agreement are permitted to drive the vehicle.
Each additional driver must meet the minimum age requirement, present a valid driving licence accepted in the rental location, provide any additional identification reasonably requested, and comply with all terms of the rental agreement.
An additional driver fee may apply per day or per rental according to the selected charge setting. The fee and any applicable taxes will be confirmed before the rental begins.
The primary renter remains responsible for the vehicle and for all charges, damage, fines, penalties, and breaches of the rental agreement, regardless of which approved driver was operating the vehicle.
Allowing an unauthorised person to drive the vehicle is a breach of the rental agreement and may invalidate any damage waiver, protection package, or insurance coverage.'
OR "additionalDriverPolicy" = 'Only authorized additional drivers listed on the rental agreement may operate the vehicle.'
OR "additionalDriverPolicy" = '';
@@ -0,0 +1,32 @@
ALTER TABLE "contract_settings"
ALTER COLUMN "fuelPolicy" SET DEFAULT 'The vehicle will be provided with the fuel level or electric battery charge percentage recorded on the rental agreement. The renter must return the vehicle with the same fuel level or, for electric vehicles, the same battery charge percentage recorded at pickup.
If the vehicle is returned with less fuel or a lower electric battery charge percentage, the renter will be charged for the missing fuel or charging shortfall at the applicable refueling or charging rate, plus any configured service fee. Charges are based on the vehicle fuel gauge or battery charge percentage and the levels recorded at the start and end of the rental.
Fuel or charging purchased during the rental is the renter responsibility and is not refundable. The renter should retain fuel or charging receipts until the vehicle has been returned and the rental agreement has been closed.
The renter must use the correct fuel type or charging method specified for the vehicle. Any damage caused by using the wrong fuel, charger, connector, or charging method will be charged to the renter.';
UPDATE "contract_settings"
SET "fuelPolicy" = 'The vehicle will be provided with the fuel level or electric battery charge percentage recorded on the rental agreement. The renter must return the vehicle with the same fuel level or, for electric vehicles, the same battery charge percentage recorded at pickup.
If the vehicle is returned with less fuel or a lower electric battery charge percentage, the renter will be charged for the missing fuel or charging shortfall at the applicable refueling or charging rate, plus any configured service fee. Charges are based on the vehicle fuel gauge or battery charge percentage and the levels recorded at the start and end of the rental.
Fuel or charging purchased during the rental is the renter responsibility and is not refundable. The renter should retain fuel or charging receipts until the vehicle has been returned and the rental agreement has been closed.
The renter must use the correct fuel type or charging method specified for the vehicle. Any damage caused by using the wrong fuel, charger, connector, or charging method will be charged to the renter.'
WHERE "fuelPolicy" = 'The vehicle will be provided with the fuel level recorded on the rental agreement. The renter must return the vehicle with the same fuel level.
If the vehicle is returned with less fuel, the renter will be charged for the missing fuel at the applicable refueling rate, plus any configured refueling service fee. Fuel charges are based on the vehicle fuel gauge and the level recorded at the start and end of the rental.
Fuel purchased during the rental is the renter responsibility and is not refundable. The renter should retain fuel receipts until the vehicle has been returned and the rental agreement has been closed.
The renter must use the correct fuel type specified for the vehicle. Any damage caused by using the wrong fuel will be charged to the renter.'
OR "fuelPolicy" = 'The vehicle must be returned with the same fuel level as at pickup.'
OR "fuelPolicy" = '';
UPDATE "contract_settings"
SET "fuelPolicyNote" = 'Please return the vehicle with the same fuel level provided at collection. Electric vehicles must be returned with the same battery charge percentage recorded at pickup. Vehicles returned with less fuel or a lower battery charge percentage will be subject to a refueling or charging charge and service fee. Use only the fuel type or charging method specified for the vehicle.'
WHERE "fuelPolicyNote" = 'Please return the vehicle with the same fuel level provided at collection. Vehicles returned with less fuel will be subject to a refueling charge and service fee. Use only the fuel type specified for the vehicle.'
OR "fuelPolicyNote" IS NULL
OR "fuelPolicyNote" = '';
@@ -0,0 +1,47 @@
ALTER TABLE "contract_settings"
ALTER COLUMN "additionalDriverPolicy" SET DEFAULT 'Only drivers listed and approved on the rental agreement are permitted to drive the vehicle.
This driver policy applies to the principal renter and every additional driver. Every driver must meet the minimum age requirement, present a valid driving licence accepted in the rental location, provide any additional identification reasonably requested, and comply with all terms of the rental agreement.
Drivers aged 25 or older qualify for the normal rental rate. Drivers aged 18 to 24 may be accepted subject to an additional young driver fee. The fee may apply to the principal renter or to any additional driver in this age range, according to the selected charge setting and pricing rules.
An additional driver fee may apply per day or per rental according to the selected charge setting. The fee and any applicable taxes will be confirmed before the rental begins.
The primary renter remains responsible for the vehicle and for all charges, damage, fines, penalties, and breaches of the rental agreement, regardless of which approved driver was operating the vehicle.
Allowing an unauthorised person to drive the vehicle is a breach of the rental agreement and may invalidate any damage waiver, protection package, or insurance coverage.';
UPDATE "contract_settings"
SET "additionalDriverPolicy" = 'Only drivers listed and approved on the rental agreement are permitted to drive the vehicle.
This driver policy applies to the principal renter and every additional driver. Every driver must meet the minimum age requirement, present a valid driving licence accepted in the rental location, provide any additional identification reasonably requested, and comply with all terms of the rental agreement.
Drivers aged 25 or older qualify for the normal rental rate. Drivers aged 18 to 24 may be accepted subject to an additional young driver fee. The fee may apply to the principal renter or to any additional driver in this age range, according to the selected charge setting and pricing rules.
An additional driver fee may apply per day or per rental according to the selected charge setting. The fee and any applicable taxes will be confirmed before the rental begins.
The primary renter remains responsible for the vehicle and for all charges, damage, fines, penalties, and breaches of the rental agreement, regardless of which approved driver was operating the vehicle.
Allowing an unauthorised person to drive the vehicle is a breach of the rental agreement and may invalidate any damage waiver, protection package, or insurance coverage.'
WHERE "additionalDriverPolicy" = 'Only drivers listed and approved on the rental agreement are permitted to drive the vehicle.
The principal renter and every additional driver must meet the minimum age requirement, present a valid driving licence accepted in the rental location, provide any additional identification reasonably requested, and comply with all terms of the rental agreement.
Drivers aged 25 or older qualify for the normal rental rate. Drivers aged 18 to 24 may be accepted subject to an additional young driver fee. The fee may apply to the principal renter or to any additional driver in this age range, according to the selected charge setting and pricing rules.
An additional driver fee may apply per day or per rental according to the selected charge setting. The fee and any applicable taxes will be confirmed before the rental begins.
The primary renter remains responsible for the vehicle and for all charges, damage, fines, penalties, and breaches of the rental agreement, regardless of which approved driver was operating the vehicle.
Allowing an unauthorised person to drive the vehicle is a breach of the rental agreement and may invalidate any damage waiver, protection package, or insurance coverage.'
OR "additionalDriverPolicy" = 'Only drivers listed and approved on the rental agreement are permitted to drive the vehicle.
Each additional driver must meet the minimum age requirement, present a valid driving licence accepted in the rental location, provide any additional identification reasonably requested, and comply with all terms of the rental agreement.
An additional driver fee may apply per day or per rental according to the selected charge setting. The fee and any applicable taxes will be confirmed before the rental begins.
The primary renter remains responsible for the vehicle and for all charges, damage, fines, penalties, and breaches of the rental agreement, regardless of which approved driver was operating the vehicle.
Allowing an unauthorised person to drive the vehicle is a breach of the rental agreement and may invalidate any damage waiver, protection package, or insurance coverage.'
OR "additionalDriverPolicy" = 'Only authorized additional drivers listed on the rental agreement may operate the vehicle.'
OR "additionalDriverPolicy" = '';
+3 -2
View File
@@ -1591,10 +1591,11 @@ model ContractSettings {
registrationNumber String?
taxId String?
terms String @default("")
fuelPolicy String @default("The vehicle must be returned with the same fuel level as at pickup.")
fuelPolicy String @default("The vehicle will be provided with the fuel level or electric battery charge percentage recorded on the rental agreement. The renter must return the vehicle with the same fuel level or, for electric vehicles, the same battery charge percentage recorded at pickup.\n\nIf the vehicle is returned with less fuel or a lower electric battery charge percentage, the renter will be charged for the missing fuel or charging shortfall at the applicable refueling or charging rate, plus any configured service fee. Charges are based on the vehicle fuel gauge or battery charge percentage and the levels recorded at the start and end of the rental.\n\nFuel or charging purchased during the rental is the renter responsibility and is not refundable. The renter should retain fuel or charging receipts until the vehicle has been returned and the rental agreement has been closed.\n\nThe renter must use the correct fuel type or charging method specified for the vehicle. Any damage caused by using the wrong fuel, charger, connector, or charging method will be charged to the renter.")
depositPolicy String @default("The security deposit is refundable within 7 days after return, subject to vehicle inspection.")
lateFeePolicy String @default("Late returns will incur a charge of 1 additional day rate per hour of delay.")
damagePolicy String @default("The renter is liable for any damage not covered by insurance.")
damagePolicy String @default("The renter is responsible for inspecting the vehicle at collection and confirming that all existing damage is recorded on the rental agreement or vehicle condition report. Any unrecorded damage should be reported before the vehicle is driven.\n\nThe vehicle must be returned in the same condition in which it was provided, allowing for reasonable wear from normal use.\n\nThe renter may be responsible for new bodywork, paint, glass, tyre, wheel, or interior damage; damage caused by collision, misuse, negligence, or improper driving; damage resulting from driving on unsuitable roads or in prohibited areas; damage caused by an unauthorised driver; damage caused by incorrect fuel, lost keys, or failure to secure the vehicle; and towing, recovery, assessment, repair, administration, and loss-of-use charges where applicable.\n\nAny damage waiver or protection package is subject to its stated exclusions and excess or deductible. It does not automatically remove all financial responsibility.\n\nThe renter must report any accident, theft, vandalism, or damage as soon as reasonably possible and must follow the company reporting instructions. Where required, the renter must also contact the police and obtain an official report or reference number.\n\nThe renter must not arrange or authorise repairs without prior written approval from the rental company, except where necessary to protect personal safety or prevent further immediate damage.\n\nDamage charges will be supported by available inspection records, photographs, repair estimates, invoices, or other reasonable evidence. Any security deposit may be held or applied toward valid charges under the rental agreement.")
additionalDriverPolicy String @default("Only drivers listed and approved on the rental agreement are permitted to drive the vehicle.\n\nThis driver policy applies to the principal renter and every additional driver. Every driver must meet the minimum age requirement, present a valid driving licence accepted in the rental location, provide any additional identification reasonably requested, and comply with all terms of the rental agreement.\n\nDrivers aged 25 or older qualify for the normal rental rate. Drivers aged 18 to 24 may be accepted subject to an additional young driver fee. The fee may apply to the principal renter or to any additional driver in this age range, according to the selected charge setting and pricing rules.\n\nAn additional driver fee may apply per day or per rental according to the selected charge setting. The fee and any applicable taxes will be confirmed before the rental begins.\n\nThe primary renter remains responsible for the vehicle and for all charges, damage, fines, penalties, and breaches of the rental agreement, regardless of which approved driver was operating the vehicle.\n\nAllowing an unauthorised person to drive the vehicle is a breach of the rental agreement and may invalidate any damage waiver, protection package, or insurance coverage.")
additionalClauses String[] @default([])
signatureRequired Boolean @default(true)
contractFooterNote String?
+3 -3
View File
@@ -12,9 +12,9 @@ export const FUEL_POLICY_LABELS: Record<FuelPolicyType, Record<'en' | 'fr' | 'ar
ar: 'ممتلئ إلى فارغ: تُسلَّم السيارة بخزان ممتلئ. لا استرداد للوقود غير المستخدم.',
},
SAME_TO_SAME: {
en: 'Same-to-Same: Return vehicle with the same fuel level as at pickup.',
fr: "Même niveau : retournez le véhicule avec le même niveau de carburant qu'au départ.",
ar: 'نفس المستوى: أعد السيارة بنفس مستوى الوقود عند الاستلام.',
en: 'Same-to-Same: Return vehicle with the same fuel level, or EV battery charge percentage, as at pickup.',
fr: "Même niveau : retournez le véhicule avec le même niveau de carburant ou, pour un véhicule électrique, le même pourcentage de charge qu'au départ.",
ar: 'نفس المستوى: أعد السيارة بنفس مستوى الوقود أو، للمركبة الكهربائية، بنفس نسبة شحن البطارية عند الاستلام.',
},
PREPAID: {
en: 'Prepaid Fuel: You pre-purchase a full tank at a fixed rate. Return at any fuel level.',