diff --git a/apps/api/src/modules/companies/company.schemas.edge.test.ts b/apps/api/src/modules/companies/company.schemas.edge.test.ts index 8200d10..bc546ed 100644 --- a/apps/api/src/modules/companies/company.schemas.edge.test.ts +++ b/apps/api/src/modules/companies/company.schemas.edge.test.ts @@ -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) diff --git a/apps/api/src/modules/companies/company.schemas.ts b/apps/api/src/modules/companies/company.schemas.ts index 1123681..32cbecc 100644 --- a/apps/api/src/modules/companies/company.schemas.ts +++ b/apps/api/src/modules/companies/company.schemas.ts @@ -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(), diff --git a/apps/api/src/modules/companies/company.service.ts b/apps/api/src/modules/companies/company.service.ts index 8a947bd..a6b73c5 100644 --- a/apps/api/src/modules/companies/company.service.ts +++ b/apps/api/src/modules/companies/company.service.ts @@ -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) } diff --git a/apps/api/src/modules/companies/settingsEntitlements.ts b/apps/api/src/modules/companies/settingsEntitlements.ts index 0ec62ab..6452e1d 100644 --- a/apps/api/src/modules/companies/settingsEntitlements.ts +++ b/apps/api/src/modules/companies/settingsEntitlements.ts @@ -105,7 +105,7 @@ const FEATURE_PLANS: Record { 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', diff --git a/apps/dashboard/next-env.d.ts b/apps/dashboard/next-env.d.ts index c4b7818..9edff1c 100644 --- a/apps/dashboard/next-env.d.ts +++ b/apps/dashboard/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -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. diff --git a/apps/dashboard/src/app/(dashboard)/billing/page.tsx b/apps/dashboard/src/app/(dashboard)/billing/page.tsx index 36f2b9c..ea79980 100644 --- a/apps/dashboard/src/app/(dashboard)/billing/page.tsx +++ b/apps/dashboard/src/app/(dashboard)/billing/page.tsx @@ -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 = { 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(null) const [invoices, setInvoices] = useState(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) diff --git a/apps/dashboard/src/app/(dashboard)/contracts/[id]/page.tsx b/apps/dashboard/src/app/(dashboard)/contracts/[id]/page.tsx index 262f460..365f788 100644 --- a/apps/dashboard/src/app/(dashboard)/contracts/[id]/page.tsx +++ b/apps/dashboard/src/app/(dashboard)/contracts/[id]/page.tsx @@ -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" /> + ([]) const [search, setSearch] = useState('') const [error, setError] = useState(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 diff --git a/apps/dashboard/src/app/(dashboard)/customers/page.tsx b/apps/dashboard/src/app/(dashboard)/customers/page.tsx index d93bfa3..b2f722a 100644 --- a/apps/dashboard/src/app/(dashboard)/customers/page.tsx +++ b/apps/dashboard/src/app/(dashboard)/customers/page.tsx @@ -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([]) const [error, setError] = useState(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 (
@@ -91,7 +103,7 @@ export default function CustomersPage() { - {rows.map((row) => ( + {filteredRows.map((row) => ( {row.firstName} {row.lastName} @@ -109,7 +121,7 @@ export default function CustomersPage() { {row.flagged ? {copy.flagged} : {copy.clear}} ))} - {rows.length === 0 && ( + {filteredRows.length === 0 && ( {copy.empty} diff --git a/apps/dashboard/src/app/(dashboard)/fleet/[id]/page.tsx b/apps/dashboard/src/app/(dashboard)/fleet/[id]/page.tsx index d3f1a99..82e24ee 100644 --- a/apps/dashboard/src/app/(dashboard)/fleet/[id]/page.tsx +++ b/apps/dashboard/src/app/(dashboard)/fleet/[id]/page.tsx @@ -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(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
{error &&

{error}

} -
+
setForm({ ...form, performedAt: e.target.value })} required />
-
- - setForm({ ...form, mileage: e.target.value })} /> -
+ {usesMileage && ( +
+ + setForm({ ...form, mileage: e.target.value })} /> +
+ )}
-
+
setForm({ ...form, nextDueAt: e.target.value })} />
-
- - setForm({ ...form, nextDueMileage: e.target.value })} /> -
+ {usesMileage && ( +
+ + setForm({ ...form, nextDueMileage: e.target.value })} /> +
+ )}
@@ -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 ( +
+
+

{vd.carMaintenance}

+ +
+ +
+ ) +} + +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 (
@@ -475,10 +541,7 @@ export default function FleetDetailPage() {

{vehicle.make} {vehicle.model}

- {vehicle.year} · {vehicle.licensePlate} - {vehicle.vin ? ` · ${vehicle.vin}` : ''} - {' · '} - {statusLabel} + {vehicle.year} · {statusLabel}

@@ -505,7 +568,7 @@ export default function FleetDetailPage() { {/* Tab bar */}
- {(['details', 'maintenance', 'calendar', 'pricing'] as Tab[]).map((tab) => ( + {(['calendar', 'carTax', 'details', 'insurance', 'maintenance', 'pricing', 'technicalInspection'] as Tab[]).map((tab) => ( @@ -598,7 +664,6 @@ export default function FleetDetailPage() {
{vd.labelCategory}
{categoryLabel}
{vd.labelDailyRate}
{formatCurrency(vehicle.dailyRate, 'MAD')}
{vd.labelStatus}
{statusLabel}
-
{vd.vinLabel}
{vehicle.vin || '—'}
{vd.labelSeats}
{vehicle.seats}
{vd.labelTransmission}
{transLabel}
{vd.labelFuelType}
{fuelLabel}
@@ -678,20 +743,10 @@ export default function FleetDetailPage() {
- {/* Daily Rate, License Plate & VIN */} -
-
- - setForm({ ...form, dailyRate: e.target.value })} /> -
-
- - setForm({ ...form, licensePlate: e.target.value })} /> -
-
+ {/* Daily Rate */}
- - setForm({ ...form, vin: e.target.value })} /> + + setForm({ ...form, dailyRate: e.target.value })} />
{/* Status */} @@ -806,25 +861,54 @@ export default function FleetDetailPage() { {activeTab === 'maintenance' && (

{vd.maintenanceIntro}

- {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 ( - - ) - })} +
)} + {activeTab === 'technicalInspection' && ( + log.type === TECHNICAL_INSPECTION_TYPE.key) + .sort((a, b) => new Date(b.performedAt).getTime() - new Date(a.performedAt).getTime())[0]} + onLogged={fetchMaintenance} + /> + )} + + {activeTab === 'carTax' && ( + log.type === CAR_TAX_TYPE.key) + .sort((a, b) => new Date(b.performedAt).getTime() - new Date(a.performedAt).getTime())[0]} + onLogged={fetchMaintenance} + /> + )} + + {activeTab === 'insurance' && ( + log.type === INSURANCE_TYPE.key) + .sort((a, b) => new Date(b.performedAt).getTime() - new Date(a.performedAt).getTime())[0]} + onLogged={fetchMaintenance} + /> + )} + {activeTab === 'calendar' && ( )} diff --git a/apps/dashboard/src/app/(dashboard)/fleet/page.tsx b/apps/dashboard/src/app/(dashboard)/fleet/page.tsx index 1094adc..2c8509a 100644 --- a/apps/dashboard/src/app/(dashboard)/fleet/page.tsx +++ b/apps/dashboard/src/app/(dashboard)/fleet/page.tsx @@ -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) diff --git a/apps/dashboard/src/app/(dashboard)/reservations/page.tsx b/apps/dashboard/src/app/(dashboard)/reservations/page.tsx index 5ecace9..f27eca4 100644 --- a/apps/dashboard/src/app/(dashboard)/reservations/page.tsx +++ b/apps/dashboard/src/app/(dashboard)/reservations/page.tsx @@ -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 (
@@ -140,7 +153,7 @@ export default function ReservationsPage() { - {rows.map((row) => ( + {filteredRows.map((row) => ( @@ -158,7 +171,7 @@ export default function ReservationsPage() { {formatCurrency(row.totalAmount, 'MAD')} ))} - {rows.length === 0 && ( + {filteredRows.length === 0 && ( {r.noReservations} diff --git a/apps/dashboard/src/app/(dashboard)/settings/page.tsx b/apps/dashboard/src/app/(dashboard)/settings/page.tsx index b4f409f..3d3ccc1 100644 --- a/apps/dashboard/src/app/(dashboard)/settings/page.tsx +++ b/apps/dashboard/src/app/(dashboard)/settings/page.tsx @@ -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. L’accè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 l’abonnement', requiredPlan: 'Requiert', lockedTitle: 'Cette section n’est 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 d’accè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: 'L’envoi 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('/companies/me/brand')) } if (activeSection === 'rental-policies' && !contractSettings) { - setContractSettings(await apiFetch('/companies/me/contract-settings') ?? { - fuelPolicyType: 'FULL_TO_FULL', fuelPolicyNote: '', additionalDriverCharge: 'FREE', - additionalDriverDailyRate: 0, additionalDriverFlatRate: 0, damagePolicy: '', - }) + setContractSettings(withRentalPolicyDefaults(await apiFetch('/companies/me/contract-settings'), language)) } if (activeSection === 'insurance' && isAvailable('settings.insurance_policies') && insurancePolicies.length === 0) { setInsurancePolicies(await apiFetch('/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 && ( +
+ +
setContractSettings({ ...contractSettings, additionalDriverCharge: v as ContractSettings['additionalDriverCharge'] })} /> - {contractSettings.additionalDriverCharge === 'PER_DAY' && setContractSettings({ ...contractSettings, additionalDriverDailyRate: Math.max(0, Number(v)) })} />} - {contractSettings.additionalDriverCharge === 'FLAT' && setContractSettings({ ...contractSettings, additionalDriverFlatRate: Math.max(0, Number(v)) })} />} + setContractSettings({ ...contractSettings, additionalDriverDailyRate: Math.max(0, Number(v)) })} />} + {contractSettings.additionalDriverCharge === 'FLAT' && setContractSettings({ ...contractSettings, additionalDriverFlatRate: Math.max(0, Number(v)) })} />} +