dcd62f35ac
- Remove 'medium' theme from dashboard and marketplace; keep only light/dark - Marketplace: move public pages into (public)/ route group so header/footer persist across navigations without re-rendering (eliminates usePathname) - MarketplaceShell is now a pure context provider; chrome lives in (public)/layout.tsx which Next.js holds stable across navigations - WorkspaceFrame: remove history.replaceState and standalone-route logic - Docker API: bypass npm run dev to avoid node --env-file in containers - Add scripts/run-with-env-file.cjs for Node.js < 20.6 compatibility; update apps/api/package.json dev script to use it Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1576 lines
61 KiB
TypeScript
1576 lines
61 KiB
TypeScript
'use client'
|
||
|
||
import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||
import { SHARED_LANGUAGE_COOKIE, SHARED_LANGUAGE_KEY, SHARED_THEME_KEY, readCurrentUserScopedPreference, readScopedPreference, writeScopedPreference } from '@/lib/preferences'
|
||
import { apiFetch } from '@/lib/api'
|
||
|
||
export type DashboardLanguage = 'en' | 'fr' | 'ar'
|
||
export type DashboardTheme = 'light' | 'dark'
|
||
|
||
type FleetDict = {
|
||
statusLabels: Record<'AVAILABLE' | 'RENTED' | 'MAINTENANCE' | 'OUT_OF_SERVICE', string>
|
||
logMaintenance: string
|
||
maintenanceSubtitle: (name: string) => string
|
||
maintenanceTypeRequired: string
|
||
failedToSave: string
|
||
typeLabel: string
|
||
typePlaceholder: string
|
||
description: string
|
||
descriptionPlaceholder: string
|
||
date: string
|
||
nextDueDate: string
|
||
cost: string
|
||
odometer: string
|
||
skip: string
|
||
saveLog: string
|
||
saving: string
|
||
addNewVehicle: string
|
||
makeMissing: string
|
||
modelMissing: string
|
||
plateMissing: string
|
||
rateMissing: string
|
||
failedToAdd: string
|
||
make: string
|
||
selectMake: string
|
||
other: string
|
||
typeMake: string
|
||
model: string
|
||
selectModel: string
|
||
typeModel: string
|
||
year: string
|
||
category: string
|
||
dailyRate: string
|
||
licensePlate: string
|
||
color: string
|
||
select: string
|
||
typeColor: string
|
||
seats: string
|
||
transmission: string
|
||
manual: string
|
||
automatic: string
|
||
fuelType: string
|
||
photosLabel: string
|
||
clickToAddPhotos: string
|
||
addMorePhotos: string
|
||
cancel: string
|
||
adding: string
|
||
addVehicle: string
|
||
heading: string
|
||
vehicleCount: (n: number) => string
|
||
searchPlaceholder: string
|
||
categoryLabels: Record<'ECONOMY' | 'COMPACT' | 'MIDSIZE' | 'FULLSIZE' | 'SUV' | 'LUXURY' | 'VAN' | 'TRUCK', string>
|
||
fuelTypeLabels: Record<'GASOLINE' | 'DIESEL' | 'ELECTRIC' | 'HYBRID', string>
|
||
allStatuses: string
|
||
allCategories: string
|
||
allVisibility: string
|
||
published: string
|
||
unpublished: string
|
||
colVehicle: string
|
||
colCategory: string
|
||
colStatus: string
|
||
colDailyRate: string
|
||
colPublished: string
|
||
colActions: string
|
||
noPhoto: string
|
||
perDay: string
|
||
noVehicles: string
|
||
noMatch: string
|
||
}
|
||
|
||
type ReservationsDict = {
|
||
heading: string
|
||
subtitle: string
|
||
colCustomer: string
|
||
colVehicle: string
|
||
colDates: string
|
||
colSource: string
|
||
colStatus: string
|
||
colTotal: string
|
||
noReservations: string
|
||
loadingReservation: string
|
||
failedToLoad: string
|
||
confirmReservation: string
|
||
checkInVehicle: string
|
||
checkOutVehicle: string
|
||
working: string
|
||
failedConfirm: string
|
||
failedCheckIn: string
|
||
failedCheckOut: string
|
||
sectionCustomer: string
|
||
noPhone: string
|
||
licenseLabel: string
|
||
notCaptured: string
|
||
flaggedBadge: string
|
||
sectionVehicle: string
|
||
sectionCharges: string
|
||
chargeDiscount: string
|
||
chargeInsurance: string
|
||
chargeAdditionalDrivers: string
|
||
chargePricingAdjustments: string
|
||
chargeGrandTotal: string
|
||
appliedInsurance: string
|
||
pricingRulesApplied: string
|
||
sectionAdditionalDrivers: string
|
||
driverLicenseLabel: string
|
||
driverChargeLabel: string
|
||
approveBtn: string
|
||
approvedBadge: string
|
||
noApprovalNeeded: string
|
||
noAdditionalDrivers: string
|
||
failedApproveDriver: string
|
||
sectionInspectionSummary: string
|
||
checkInInspectionLabel: string
|
||
checkOutInspectionLabel: string
|
||
savedBadge: string
|
||
pendingBadge: string
|
||
inspectionCardTitle: (type: 'CHECKIN' | 'CHECKOUT') => string
|
||
inspectionSubtitle: string
|
||
saveInspection: string
|
||
savingInspection: string
|
||
failedSaveInspection: string
|
||
diagramHelp: string
|
||
mileageLabel: string
|
||
fuelLevelLabel: string
|
||
fuelLevels: Record<'FULL' | 'SEVEN_EIGHTHS' | 'THREE_QUARTERS' | 'FIVE_EIGHTHS' | 'HALF' | 'THREE_EIGHTHS' | 'QUARTER' | 'ONE_EIGHTH' | 'EMPTY', string>
|
||
generalConditionLabel: string
|
||
employeeNotesLabel: string
|
||
customerAcknowledged: string
|
||
damageMarkersHeading: string
|
||
removeMarker: string
|
||
noMarkers: string
|
||
damageTypeLabels: Record<'SCRATCH' | 'DENT' | 'CRACK' | 'CHIP' | 'MISSING' | 'STAIN' | 'OTHER', string>
|
||
severityLabels: Record<'MINOR' | 'MODERATE' | 'MAJOR', string>
|
||
}
|
||
|
||
type DashboardPageDict = {
|
||
failedToLoad: string
|
||
forbidden: string
|
||
kpiTotalBookings: string
|
||
kpiActiveVehicles: string
|
||
kpiMonthlyRevenue: string
|
||
kpiTotalCustomers: string
|
||
vsLastMonth: string
|
||
bookingSources: string
|
||
barBookings: string
|
||
barRevenue: string
|
||
noBookingData: string
|
||
quickStats: string
|
||
noData: string
|
||
recentReservations: string
|
||
viewAll: string
|
||
colBookingNum: string
|
||
colCustomer: string
|
||
colVehicle: string
|
||
colDates: string
|
||
colStatus: string
|
||
colTotal: string
|
||
noReservations: string
|
||
trialEnds: (date: string) => string
|
||
trialActive: string
|
||
pastDueMsg: string
|
||
suspendedMsg: string
|
||
manageBilling: string
|
||
statusLabels: Record<'CONFIRMED' | 'PENDING' | 'ACTIVE' | 'COMPLETED' | 'CANCELLED', string>
|
||
}
|
||
|
||
type CalendarDict = {
|
||
heading: string
|
||
blockDates: string
|
||
clickSecondDay: (selected: string) => string
|
||
cancelSelection: string
|
||
legendReserved: string
|
||
legendMaintenance: string
|
||
legendBlocked: string
|
||
legendAvailable: string
|
||
failedLoad: string
|
||
moreDays: (n: number) => string
|
||
reservationsThisMonth: string
|
||
blocksAndMaintenance: string
|
||
removeBlockConfirm: string
|
||
failedDeleteBlock: string
|
||
modalTitle: string
|
||
blockTypeLabel: string
|
||
manualBlock: string
|
||
maintenanceBlock: string
|
||
startDate: string
|
||
endDate: string
|
||
reasonLabel: string
|
||
optional: string
|
||
reasonPlaceholderManual: string
|
||
reasonPlaceholderMaintenance: string
|
||
datesRequired: string
|
||
endAfterStart: string
|
||
failedCreateBlock: string
|
||
cancel: string
|
||
saveBlock: string
|
||
saving: string
|
||
removeBlock: string
|
||
dayHeaders: [string, string, string, string, string, string, string]
|
||
}
|
||
|
||
type VehicleDetailDict = {
|
||
tabDetails: string
|
||
tabMaintenance: string
|
||
tabCalendar: string
|
||
editBtn: string
|
||
cancelBtn: string
|
||
saveBtn: string
|
||
savingBtn: string
|
||
uploadingPhotosBtn: string
|
||
loadingVehicle: string
|
||
photosHeading: string
|
||
noPhotosYet: string
|
||
clickToAddPhotos: string
|
||
addMorePhotos: (current: number) => string
|
||
maxPhotosReached: string
|
||
newPhotoBadge: string
|
||
vehicleDetailsHeading: string
|
||
labelCategory: string
|
||
labelDailyRate: string
|
||
labelStatus: string
|
||
labelSeats: string
|
||
labelTransmission: string
|
||
labelFuelType: string
|
||
labelColor: string
|
||
labelOdometer: string
|
||
labelNotes: string
|
||
makeLabel: string
|
||
selectMake: string
|
||
other: string
|
||
typeMake: string
|
||
modelLabel: string
|
||
selectModel: string
|
||
typeModel: string
|
||
yearLabel: string
|
||
dailyRateLabel: string
|
||
licensePlateLabel: string
|
||
statusLabel: string
|
||
colorLabel: string
|
||
selectColor: string
|
||
typeColor: string
|
||
seatsLabel: string
|
||
transmissionLabel: string
|
||
fuelTypeLabel: string
|
||
odometerLabel: string
|
||
notesLabel: string
|
||
optionalNotes: string
|
||
optional: string
|
||
makeRequired: string
|
||
modelRequired: string
|
||
plateRequired: string
|
||
rateInvalid: string
|
||
failedSave: string
|
||
maintenanceIntro: string
|
||
logBtn: string
|
||
statusNone: string
|
||
statusOverdue: string
|
||
statusDueSoon: string
|
||
statusOk: string
|
||
noRecord: string
|
||
lastPrefix: string
|
||
atKm: (km: string) => string
|
||
duePrefix: string
|
||
orSep: string
|
||
kmLeft: (km: string) => string
|
||
overdueByKm: string
|
||
serviceDateLabel: string
|
||
odometerAtService: string
|
||
nextDueDateLabel: string
|
||
nextDueKmLabel: string
|
||
costLabel: string
|
||
maintenanceNotesLabel: string
|
||
saveLabel: string
|
||
savingLabel: string
|
||
dateRequired: string
|
||
routineTypes: Record<string, string>
|
||
}
|
||
|
||
type DashboardDictionary = {
|
||
nav: Record<string, string>
|
||
titles: Record<string, string>
|
||
notifications: string
|
||
noNewNotifications: string
|
||
unreadNotifications: (count: number) => string
|
||
signOut: string
|
||
workspaceUser: string
|
||
localAuth: string
|
||
language: string
|
||
theme: string
|
||
light: string
|
||
dark: string
|
||
fleet: FleetDict
|
||
vehicleDetail: VehicleDetailDict
|
||
calendar: CalendarDict
|
||
dashboardPage: DashboardPageDict
|
||
reservations: ReservationsDict
|
||
}
|
||
|
||
const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
|
||
en: {
|
||
nav: {
|
||
dashboard: 'Dashboard',
|
||
fleet: 'Fleet',
|
||
reservations: 'Booking',
|
||
onlineReservations: 'Online Reservations',
|
||
customers: 'Customers',
|
||
offers: 'Offers',
|
||
team: 'Team',
|
||
reports: 'Reports',
|
||
subscription: 'Subscription',
|
||
billing: 'Customer Billing',
|
||
contracts: 'Contracts',
|
||
notifications: 'Notifications',
|
||
settings: 'Settings',
|
||
},
|
||
titles: {
|
||
'/dashboard': 'Dashboard',
|
||
'/dashboard/fleet': 'Fleet Management',
|
||
'/dashboard/reservations': 'Booking',
|
||
'/dashboard/reservations/new': 'Book Car',
|
||
'/dashboard/online-reservations': 'Online Reservations',
|
||
'/dashboard/customers': 'Customers',
|
||
'/dashboard/offers': 'Offers',
|
||
'/dashboard/team': 'Team',
|
||
'/dashboard/reports': 'Reports',
|
||
'/dashboard/subscription': 'Subscription',
|
||
'/dashboard/billing': 'Customer Billing',
|
||
'/dashboard/contracts': 'Contracts',
|
||
'/dashboard/notifications': 'Notifications',
|
||
'/dashboard/settings': 'Settings',
|
||
},
|
||
notifications: 'Notifications',
|
||
noNewNotifications: 'No new notifications',
|
||
unreadNotifications: (count) => `${count} unread notification${count > 1 ? 's' : ''}`,
|
||
signOut: 'Sign out',
|
||
workspaceUser: 'Workspace user',
|
||
localAuth: 'Local authentication',
|
||
language: 'Language',
|
||
theme: 'Theme',
|
||
light: 'Light',
|
||
dark: 'Dark',
|
||
fleet: {
|
||
statusLabels: { AVAILABLE: 'Available', RENTED: 'Rented', MAINTENANCE: 'Maintenance', OUT_OF_SERVICE: 'Out of Service' },
|
||
logMaintenance: 'Log Maintenance',
|
||
maintenanceSubtitle: (name) => `${name} has been set to maintenance. Add details below.`,
|
||
maintenanceTypeRequired: 'Please enter a maintenance type.',
|
||
failedToSave: 'Failed to save maintenance log.',
|
||
typeLabel: 'Type *',
|
||
typePlaceholder: 'e.g. Oil change, Tire rotation, Brake service…',
|
||
description: 'Description',
|
||
descriptionPlaceholder: 'Optional details…',
|
||
date: 'Date *',
|
||
nextDueDate: 'Next Due Date',
|
||
cost: 'Cost (MAD)',
|
||
odometer: 'Odometer (km)',
|
||
skip: 'Skip',
|
||
saveLog: 'Save Log',
|
||
saving: 'Saving…',
|
||
addNewVehicle: 'Add New Vehicle',
|
||
makeMissing: 'Please select or enter a make.',
|
||
modelMissing: 'Please select or enter a model.',
|
||
plateMissing: 'Please enter a license plate.',
|
||
rateMissing: 'Please enter a valid daily rate.',
|
||
failedToAdd: 'Failed to add vehicle. Please check all fields and try again.',
|
||
make: 'Make *',
|
||
selectMake: 'Select make…',
|
||
other: 'Other…',
|
||
typeMake: 'Type make…',
|
||
model: 'Model *',
|
||
selectModel: 'Select model…',
|
||
typeModel: 'Type model…',
|
||
year: 'Year *',
|
||
category: 'Category',
|
||
dailyRate: 'Daily Rate (MAD) *',
|
||
licensePlate: 'License Plate *',
|
||
color: 'Color',
|
||
select: 'Select…',
|
||
typeColor: 'Type color…',
|
||
seats: 'Seats',
|
||
transmission: 'Transmission',
|
||
manual: 'Manual',
|
||
automatic: 'Automatic',
|
||
fuelType: 'Fuel Type',
|
||
photosLabel: 'Photos (up to 10)',
|
||
clickToAddPhotos: 'Click to add photos',
|
||
addMorePhotos: 'Add more photos',
|
||
cancel: 'Cancel',
|
||
adding: 'Adding…',
|
||
addVehicle: 'Add Vehicle',
|
||
heading: 'Fleet',
|
||
vehicleCount: (n) => `${n} vehicle${n !== 1 ? 's' : ''} total`,
|
||
searchPlaceholder: 'Search make, model, plate…',
|
||
categoryLabels: {
|
||
ECONOMY: 'Economy',
|
||
COMPACT: 'Compact',
|
||
MIDSIZE: 'Midsize',
|
||
FULLSIZE: 'Full-size',
|
||
SUV: 'SUV',
|
||
LUXURY: 'Luxury',
|
||
VAN: 'Van',
|
||
TRUCK: 'Truck',
|
||
},
|
||
fuelTypeLabels: {
|
||
GASOLINE: 'Gasoline',
|
||
DIESEL: 'Diesel',
|
||
ELECTRIC: 'Electric',
|
||
HYBRID: 'Hybrid',
|
||
},
|
||
allStatuses: 'All statuses',
|
||
allCategories: 'All categories',
|
||
allVisibility: 'All visibility',
|
||
published: 'Published',
|
||
unpublished: 'Unpublished',
|
||
colVehicle: 'Vehicle',
|
||
colCategory: 'Category',
|
||
colStatus: 'Status',
|
||
colDailyRate: 'Daily Rate',
|
||
colPublished: 'Published',
|
||
colActions: 'Actions',
|
||
noPhoto: 'No photo',
|
||
perDay: '/day',
|
||
noVehicles: 'No vehicles yet. Add your first vehicle to get started.',
|
||
noMatch: 'No vehicles match your filters.',
|
||
},
|
||
vehicleDetail: {
|
||
tabDetails: 'Details',
|
||
tabMaintenance: 'Maintenance',
|
||
tabCalendar: 'Calendar',
|
||
editBtn: 'Edit',
|
||
cancelBtn: 'Cancel',
|
||
saveBtn: 'Save',
|
||
savingBtn: 'Saving…',
|
||
uploadingPhotosBtn: 'Uploading photos…',
|
||
loadingVehicle: 'Loading vehicle…',
|
||
photosHeading: 'Photos',
|
||
noPhotosYet: 'No photos uploaded yet.',
|
||
clickToAddPhotos: 'Click to add photos',
|
||
addMorePhotos: (n) => `Add more photos (${n}/10)`,
|
||
maxPhotosReached: 'Maximum 10 photos reached.',
|
||
newPhotoBadge: 'New',
|
||
vehicleDetailsHeading: 'Vehicle details',
|
||
labelCategory: 'Category',
|
||
labelDailyRate: 'Daily rate',
|
||
labelStatus: 'Status',
|
||
labelSeats: 'Seats',
|
||
labelTransmission: 'Transmission',
|
||
labelFuelType: 'Fuel type',
|
||
labelColor: 'Color',
|
||
labelOdometer: 'Odometer',
|
||
labelNotes: 'Notes',
|
||
makeLabel: 'Make *',
|
||
selectMake: 'Select make…',
|
||
other: 'Other…',
|
||
typeMake: 'Type make…',
|
||
modelLabel: 'Model *',
|
||
selectModel: 'Select model…',
|
||
typeModel: 'Type model…',
|
||
yearLabel: 'Year *',
|
||
dailyRateLabel: 'Daily Rate (MAD) *',
|
||
licensePlateLabel: 'License Plate *',
|
||
statusLabel: 'Status',
|
||
colorLabel: 'Color',
|
||
selectColor: 'Select color…',
|
||
typeColor: 'Type color…',
|
||
seatsLabel: 'Seats',
|
||
transmissionLabel: 'Transmission',
|
||
fuelTypeLabel: 'Fuel Type',
|
||
odometerLabel: 'Odometer (km)',
|
||
notesLabel: 'Notes',
|
||
optionalNotes: 'Optional notes…',
|
||
optional: 'Optional…',
|
||
makeRequired: 'Make is required.',
|
||
modelRequired: 'Model is required.',
|
||
plateRequired: 'License plate is required.',
|
||
rateInvalid: 'Please enter a valid daily rate.',
|
||
failedSave: 'Failed to save changes.',
|
||
maintenanceIntro: 'Track routine maintenance for this vehicle. Click Log on any item to record a service.',
|
||
logBtn: 'Log',
|
||
statusNone: 'Not logged',
|
||
statusOverdue: 'Overdue',
|
||
statusDueSoon: 'Due soon',
|
||
statusOk: 'Up to date',
|
||
noRecord: 'No record yet',
|
||
lastPrefix: 'Last:',
|
||
atKm: (km) => `at ${km} km`,
|
||
duePrefix: '· Due:',
|
||
orSep: 'or',
|
||
kmLeft: (km) => `${km} km left`,
|
||
overdueByKm: 'overdue by km',
|
||
serviceDateLabel: 'Service Date *',
|
||
odometerAtService: 'Odometer at service (km)',
|
||
nextDueDateLabel: 'Next due date',
|
||
nextDueKmLabel: 'Next due at (km)',
|
||
costLabel: 'Cost (MAD)',
|
||
maintenanceNotesLabel: 'Notes',
|
||
saveLabel: 'Save',
|
||
savingLabel: 'Saving…',
|
||
dateRequired: 'Service date is required.',
|
||
routineTypes: {
|
||
'Oil Change': 'Oil Change',
|
||
'Technical Inspection': 'Technical Inspection',
|
||
'Vehicle Registration': 'Vehicle Registration',
|
||
'Car Tax': 'Car Tax',
|
||
'Tire Rotation': 'Tire Rotation',
|
||
'Brake Inspection': 'Brake Inspection',
|
||
'Air Filter': 'Air Filter',
|
||
'Cabin Filter': 'Cabin Filter',
|
||
'Battery Check': 'Battery Check',
|
||
'Belt / Chain Service': 'Belt / Chain Service',
|
||
'Coolant Flush': 'Coolant Flush',
|
||
'Transmission Service': 'Transmission Service',
|
||
},
|
||
},
|
||
calendar: {
|
||
heading: 'Availability Calendar',
|
||
blockDates: 'Block dates',
|
||
clickSecondDay: (selected) => `Click a second day to set the block end date (selected: ${selected})`,
|
||
cancelSelection: 'cancel',
|
||
legendReserved: 'Reserved',
|
||
legendMaintenance: 'Maintenance',
|
||
legendBlocked: 'Blocked',
|
||
legendAvailable: 'Available',
|
||
failedLoad: 'Failed to load calendar',
|
||
moreDays: (n) => `+${n} more`,
|
||
reservationsThisMonth: 'Reservations this month',
|
||
blocksAndMaintenance: 'Blocks & maintenance',
|
||
removeBlockConfirm: 'Remove this block?',
|
||
failedDeleteBlock: 'Failed to delete block',
|
||
modalTitle: 'Block vehicle dates',
|
||
blockTypeLabel: 'Block type',
|
||
manualBlock: 'Manual block',
|
||
maintenanceBlock: 'Maintenance',
|
||
startDate: 'Start date',
|
||
endDate: 'End date (exclusive)',
|
||
reasonLabel: 'Reason',
|
||
optional: '(optional)',
|
||
reasonPlaceholderManual: 'e.g. Reserved for VIP, company event…',
|
||
reasonPlaceholderMaintenance: 'e.g. Oil change, tire rotation…',
|
||
datesRequired: 'Start and end dates are required.',
|
||
endAfterStart: 'End date must be after start date.',
|
||
failedCreateBlock: 'Failed to create block',
|
||
cancel: 'Cancel',
|
||
saveBlock: 'Save block',
|
||
saving: 'Saving…',
|
||
removeBlock: 'Remove block',
|
||
dayHeaders: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
|
||
},
|
||
dashboardPage: {
|
||
failedToLoad: 'Failed to load dashboard',
|
||
forbidden: 'You do not have permission to access this page. A Manager role or higher is required.',
|
||
kpiTotalBookings: 'Total Bookings',
|
||
kpiActiveVehicles: 'Active Vehicles',
|
||
kpiMonthlyRevenue: 'Monthly Revenue',
|
||
kpiTotalCustomers: 'Total Customers',
|
||
vsLastMonth: 'vs last month',
|
||
bookingSources: 'Booking Sources',
|
||
barBookings: 'Bookings',
|
||
barRevenue: 'Revenue (MAD)',
|
||
noBookingData: 'No booking data available yet',
|
||
quickStats: 'Quick Stats',
|
||
noData: 'No data yet',
|
||
recentReservations: 'Recent Reservations',
|
||
viewAll: 'View all →',
|
||
colBookingNum: 'Booking #',
|
||
colCustomer: 'Customer',
|
||
colVehicle: 'Vehicle',
|
||
colDates: 'Dates',
|
||
colStatus: 'Status',
|
||
colTotal: 'Total',
|
||
noReservations: "No reservations yet. Once customers book vehicles, they'll appear here.",
|
||
trialEnds: (date) => `You're on a free trial. Trial ends ${date}.`,
|
||
trialActive: "You're on a free trial.",
|
||
pastDueMsg: 'Your payment is past due. Please update your billing information.',
|
||
suspendedMsg: 'Your account has been suspended. Please contact support or renew your subscription.',
|
||
manageBilling: 'Manage subscription →',
|
||
statusLabels: { CONFIRMED: 'Confirmed', PENDING: 'Pending', ACTIVE: 'Active', COMPLETED: 'Completed', CANCELLED: 'Cancelled' },
|
||
},
|
||
reservations: {
|
||
heading: 'Booking',
|
||
subtitle: 'All booking sources, including dashboard, public site, and marketplace.',
|
||
colCustomer: 'Customer',
|
||
colVehicle: 'Vehicle',
|
||
colDates: 'Dates',
|
||
colSource: 'Source',
|
||
colStatus: 'Status',
|
||
colTotal: 'Total',
|
||
noReservations: 'No reservations yet.',
|
||
loadingReservation: 'Loading reservation…',
|
||
failedToLoad: 'Failed to load reservation',
|
||
confirmReservation: 'Confirm reservation',
|
||
checkInVehicle: 'Check in vehicle',
|
||
checkOutVehicle: 'Check out vehicle',
|
||
working: 'Working…',
|
||
failedConfirm: 'Failed to confirm reservation',
|
||
failedCheckIn: 'Failed to check in reservation',
|
||
failedCheckOut: 'Failed to check out reservation',
|
||
sectionCustomer: 'Customer',
|
||
noPhone: 'No phone provided',
|
||
licenseLabel: 'License:',
|
||
notCaptured: 'Not captured',
|
||
flaggedBadge: 'Flagged',
|
||
sectionVehicle: 'Vehicle',
|
||
sectionCharges: 'Charges',
|
||
chargeDiscount: 'Discount',
|
||
chargeInsurance: 'Insurance',
|
||
chargeAdditionalDrivers: 'Additional drivers',
|
||
chargePricingAdjustments: 'Pricing adjustments',
|
||
chargeGrandTotal: 'Grand total',
|
||
appliedInsurance: 'Applied insurance',
|
||
pricingRulesApplied: 'Pricing rules applied',
|
||
sectionAdditionalDrivers: 'Additional drivers',
|
||
driverLicenseLabel: 'License:',
|
||
driverChargeLabel: 'Charge:',
|
||
approveBtn: 'Approve',
|
||
approvedBadge: 'Approved',
|
||
noApprovalNeeded: 'No approval needed',
|
||
noAdditionalDrivers: 'No additional drivers were added to this reservation.',
|
||
failedApproveDriver: 'Failed to approve additional driver',
|
||
sectionInspectionSummary: 'Inspection summary',
|
||
checkInInspectionLabel: 'Check-in inspection',
|
||
checkOutInspectionLabel: 'Check-out inspection',
|
||
savedBadge: 'Saved',
|
||
pendingBadge: 'Pending',
|
||
inspectionCardTitle: (type) => type === 'CHECKIN' ? 'Check-in inspection' : 'Check-out inspection',
|
||
inspectionSubtitle: 'Mark existing and newly discovered damage directly on the vehicle diagram.',
|
||
saveInspection: 'Save inspection',
|
||
savingInspection: 'Saving…',
|
||
failedSaveInspection: 'Failed to save inspection',
|
||
diagramHelp: 'Click anywhere on the diagram to add a damage marker with the selected type and severity.',
|
||
mileageLabel: 'Mileage',
|
||
fuelLevelLabel: 'Fuel level',
|
||
fuelLevels: { FULL: 'Full', SEVEN_EIGHTHS: '7/8', THREE_QUARTERS: '3/4', FIVE_EIGHTHS: '5/8', HALF: '1/2', THREE_EIGHTHS: '3/8', QUARTER: '1/4', ONE_EIGHTH: '1/8', EMPTY: 'Empty' },
|
||
generalConditionLabel: 'General condition',
|
||
employeeNotesLabel: 'Employee notes',
|
||
customerAcknowledged: 'Customer acknowledged this inspection',
|
||
damageMarkersHeading: 'Damage markers',
|
||
removeMarker: 'Remove',
|
||
noMarkers: 'No damage markers saved yet.',
|
||
damageTypeLabels: { SCRATCH: 'Scratch', DENT: 'Dent', CRACK: 'Crack', CHIP: 'Chip', MISSING: 'Missing', STAIN: 'Stain', OTHER: 'Other' },
|
||
severityLabels: { MINOR: 'Minor', MODERATE: 'Moderate', MAJOR: 'Major' },
|
||
},
|
||
},
|
||
fr: {
|
||
nav: {
|
||
dashboard: 'Tableau de bord',
|
||
fleet: 'Flotte',
|
||
reservations: 'Réservations',
|
||
onlineReservations: 'Réservations en ligne',
|
||
customers: 'Clients',
|
||
offers: 'Offres',
|
||
team: 'Équipe',
|
||
reports: 'Rapports',
|
||
subscription: 'Abonnement',
|
||
billing: 'Facturation clients',
|
||
contracts: 'Contrats',
|
||
notifications: 'Notifications',
|
||
settings: 'Paramètres',
|
||
},
|
||
titles: {
|
||
'/dashboard': 'Tableau de bord',
|
||
'/dashboard/fleet': 'Gestion de flotte',
|
||
'/dashboard/reservations': 'Réservations',
|
||
'/dashboard/reservations/new': 'Réserver une voiture',
|
||
'/dashboard/online-reservations': 'Réservations en ligne',
|
||
'/dashboard/customers': 'Clients',
|
||
'/dashboard/offers': 'Offres',
|
||
'/dashboard/team': 'Équipe',
|
||
'/dashboard/reports': 'Rapports',
|
||
'/dashboard/subscription': 'Abonnement',
|
||
'/dashboard/billing': 'Facturation clients',
|
||
'/dashboard/contracts': 'Contrats',
|
||
'/dashboard/notifications': 'Notifications',
|
||
'/dashboard/settings': 'Paramètres',
|
||
},
|
||
notifications: 'Notifications',
|
||
noNewNotifications: 'Aucune nouvelle notification',
|
||
unreadNotifications: (count) => `${count} notification${count > 1 ? 's' : ''} non lue${count > 1 ? 's' : ''}`,
|
||
signOut: 'Déconnexion',
|
||
workspaceUser: 'Utilisateur espace',
|
||
localAuth: 'Authentification locale',
|
||
language: 'Langue',
|
||
theme: 'Mode',
|
||
light: 'Clair',
|
||
dark: 'Sombre',
|
||
fleet: {
|
||
statusLabels: { AVAILABLE: 'Disponible', RENTED: 'Loué', MAINTENANCE: 'Maintenance', OUT_OF_SERVICE: 'Hors service' },
|
||
logMaintenance: 'Journal de maintenance',
|
||
maintenanceSubtitle: (name) => `${name} a été mis en maintenance. Ajoutez les détails ci-dessous.`,
|
||
maintenanceTypeRequired: 'Veuillez saisir un type de maintenance.',
|
||
failedToSave: 'Échec de la sauvegarde du journal de maintenance.',
|
||
typeLabel: 'Type *',
|
||
typePlaceholder: 'ex. Vidange, Rotation des pneus, Freins…',
|
||
description: 'Description',
|
||
descriptionPlaceholder: 'Détails optionnels…',
|
||
date: 'Date *',
|
||
nextDueDate: 'Prochaine échéance',
|
||
cost: 'Coût (MAD)',
|
||
odometer: 'Kilométrage (km)',
|
||
skip: 'Ignorer',
|
||
saveLog: 'Enregistrer',
|
||
saving: 'Enregistrement…',
|
||
addNewVehicle: 'Ajouter un véhicule',
|
||
makeMissing: 'Veuillez sélectionner ou saisir une marque.',
|
||
modelMissing: 'Veuillez sélectionner ou saisir un modèle.',
|
||
plateMissing: 'Veuillez saisir un numéro de plaque.',
|
||
rateMissing: 'Veuillez saisir un tarif journalier valide.',
|
||
failedToAdd: "Échec de l'ajout du véhicule. Vérifiez tous les champs.",
|
||
make: 'Marque *',
|
||
selectMake: 'Sélectionner la marque…',
|
||
other: 'Autre…',
|
||
typeMake: 'Saisir la marque…',
|
||
model: 'Modèle *',
|
||
selectModel: 'Sélectionner le modèle…',
|
||
typeModel: 'Saisir le modèle…',
|
||
year: 'Année *',
|
||
category: 'Catégorie',
|
||
dailyRate: 'Tarif journalier (MAD) *',
|
||
licensePlate: "Plaque d'immatriculation *",
|
||
color: 'Couleur',
|
||
select: 'Sélectionner…',
|
||
typeColor: 'Saisir la couleur…',
|
||
seats: 'Places',
|
||
transmission: 'Transmission',
|
||
manual: 'Manuelle',
|
||
automatic: 'Automatique',
|
||
fuelType: 'Carburant',
|
||
photosLabel: "Photos (jusqu'à 10)",
|
||
clickToAddPhotos: 'Cliquer pour ajouter des photos',
|
||
addMorePhotos: "Ajouter d'autres photos",
|
||
cancel: 'Annuler',
|
||
adding: 'Ajout…',
|
||
addVehicle: 'Ajouter le véhicule',
|
||
heading: 'Flotte',
|
||
vehicleCount: (n) => `${n} véhicule${n !== 1 ? 's' : ''} au total`,
|
||
searchPlaceholder: 'Rechercher marque, modèle, plaque…',
|
||
categoryLabels: {
|
||
ECONOMY: 'Économique',
|
||
COMPACT: 'Compacte',
|
||
MIDSIZE: 'Intermédiaire',
|
||
FULLSIZE: 'Grande berline',
|
||
SUV: 'SUV',
|
||
LUXURY: 'Luxe',
|
||
VAN: 'Van',
|
||
TRUCK: 'Camionnette',
|
||
},
|
||
fuelTypeLabels: {
|
||
GASOLINE: 'Essence',
|
||
DIESEL: 'Diesel',
|
||
ELECTRIC: 'Électrique',
|
||
HYBRID: 'Hybride',
|
||
},
|
||
allStatuses: 'Tous les statuts',
|
||
allCategories: 'Toutes les catégories',
|
||
allVisibility: 'Toute visibilité',
|
||
published: 'Publié',
|
||
unpublished: 'Non publié',
|
||
colVehicle: 'Véhicule',
|
||
colCategory: 'Catégorie',
|
||
colStatus: 'Statut',
|
||
colDailyRate: 'Tarif journalier',
|
||
colPublished: 'Publié',
|
||
colActions: 'Actions',
|
||
noPhoto: 'Pas de photo',
|
||
perDay: '/jour',
|
||
noVehicles: 'Aucun véhicule. Ajoutez votre premier véhicule pour commencer.',
|
||
noMatch: 'Aucun véhicule ne correspond aux filtres.',
|
||
},
|
||
vehicleDetail: {
|
||
tabDetails: 'Détails',
|
||
tabMaintenance: 'Entretien',
|
||
tabCalendar: 'Calendrier',
|
||
editBtn: 'Modifier',
|
||
cancelBtn: 'Annuler',
|
||
saveBtn: 'Enregistrer',
|
||
savingBtn: 'Enregistrement…',
|
||
uploadingPhotosBtn: 'Envoi des photos…',
|
||
loadingVehicle: 'Chargement du véhicule…',
|
||
photosHeading: 'Photos',
|
||
noPhotosYet: 'Aucune photo ajoutée.',
|
||
clickToAddPhotos: 'Cliquer pour ajouter des photos',
|
||
addMorePhotos: (n) => `Ajouter des photos (${n}/10)`,
|
||
maxPhotosReached: '10 photos maximum atteintes.',
|
||
newPhotoBadge: 'Nouveau',
|
||
vehicleDetailsHeading: 'Détails du véhicule',
|
||
labelCategory: 'Catégorie',
|
||
labelDailyRate: 'Tarif journalier',
|
||
labelStatus: 'Statut',
|
||
labelSeats: 'Places',
|
||
labelTransmission: 'Transmission',
|
||
labelFuelType: 'Carburant',
|
||
labelColor: 'Couleur',
|
||
labelOdometer: 'Kilométrage',
|
||
labelNotes: 'Notes',
|
||
makeLabel: 'Marque *',
|
||
selectMake: 'Sélectionner la marque…',
|
||
other: 'Autre…',
|
||
typeMake: 'Saisir la marque…',
|
||
modelLabel: 'Modèle *',
|
||
selectModel: 'Sélectionner le modèle…',
|
||
typeModel: 'Saisir le modèle…',
|
||
yearLabel: 'Année *',
|
||
dailyRateLabel: 'Tarif journalier (MAD) *',
|
||
licensePlateLabel: "Plaque d'immatriculation *",
|
||
statusLabel: 'Statut',
|
||
colorLabel: 'Couleur',
|
||
selectColor: 'Sélectionner la couleur…',
|
||
typeColor: 'Saisir la couleur…',
|
||
seatsLabel: 'Places',
|
||
transmissionLabel: 'Transmission',
|
||
fuelTypeLabel: 'Carburant',
|
||
odometerLabel: 'Kilométrage (km)',
|
||
notesLabel: 'Notes',
|
||
optionalNotes: 'Notes optionnelles…',
|
||
optional: 'Optionnel…',
|
||
makeRequired: 'La marque est requise.',
|
||
modelRequired: 'Le modèle est requis.',
|
||
plateRequired: "La plaque d'immatriculation est requise.",
|
||
rateInvalid: 'Veuillez saisir un tarif journalier valide.',
|
||
failedSave: "Échec de l'enregistrement.",
|
||
maintenanceIntro: "Suivez l'entretien de ce véhicule. Cliquez sur Saisir pour enregistrer une intervention.",
|
||
logBtn: 'Saisir',
|
||
statusNone: 'Non enregistré',
|
||
statusOverdue: 'En retard',
|
||
statusDueSoon: 'Bientôt dû',
|
||
statusOk: 'À jour',
|
||
noRecord: 'Aucune intervention',
|
||
lastPrefix: 'Dernier :',
|
||
atKm: (km) => `à ${km} km`,
|
||
duePrefix: '· Échéance :',
|
||
orSep: 'ou',
|
||
kmLeft: (km) => `${km} km restants`,
|
||
overdueByKm: 'dépassé (km)',
|
||
serviceDateLabel: 'Date du service *',
|
||
odometerAtService: 'Kilométrage lors du service (km)',
|
||
nextDueDateLabel: 'Prochaine échéance (date)',
|
||
nextDueKmLabel: 'Prochaine échéance (km)',
|
||
costLabel: 'Coût (MAD)',
|
||
maintenanceNotesLabel: 'Notes',
|
||
saveLabel: 'Enregistrer',
|
||
savingLabel: 'Enregistrement…',
|
||
dateRequired: 'La date du service est requise.',
|
||
routineTypes: {
|
||
'Oil Change': "Vidange d'huile",
|
||
'Technical Inspection': 'Contrôle technique',
|
||
'Vehicle Registration': 'Carte grise',
|
||
'Car Tax': 'Vignette automobile',
|
||
'Tire Rotation': 'Rotation des pneus',
|
||
'Brake Inspection': 'Contrôle des freins',
|
||
'Air Filter': 'Filtre à air',
|
||
'Cabin Filter': 'Filtre habitacle',
|
||
'Battery Check': 'Vérification batterie',
|
||
'Belt / Chain Service': 'Courroie / Chaîne',
|
||
'Coolant Flush': 'Liquide de refroidissement',
|
||
'Transmission Service': 'Entretien transmission',
|
||
},
|
||
},
|
||
calendar: {
|
||
heading: 'Calendrier de disponibilité',
|
||
blockDates: 'Bloquer des dates',
|
||
clickSecondDay: (selected) => `Cliquez sur un second jour pour définir la fin du blocage (sélectionné : ${selected})`,
|
||
cancelSelection: 'annuler',
|
||
legendReserved: 'Réservé',
|
||
legendMaintenance: 'Maintenance',
|
||
legendBlocked: 'Bloqué',
|
||
legendAvailable: 'Disponible',
|
||
failedLoad: 'Échec du chargement du calendrier',
|
||
moreDays: (n) => `+${n} de plus`,
|
||
reservationsThisMonth: 'Réservations ce mois-ci',
|
||
blocksAndMaintenance: 'Blocages & maintenance',
|
||
removeBlockConfirm: 'Supprimer ce blocage ?',
|
||
failedDeleteBlock: 'Échec de la suppression du blocage',
|
||
modalTitle: 'Bloquer les dates du véhicule',
|
||
blockTypeLabel: 'Type de blocage',
|
||
manualBlock: 'Blocage manuel',
|
||
maintenanceBlock: 'Maintenance',
|
||
startDate: 'Date de début',
|
||
endDate: 'Date de fin (exclusive)',
|
||
reasonLabel: 'Raison',
|
||
optional: '(optionnel)',
|
||
reasonPlaceholderManual: 'ex. Réservé pour VIP, événement…',
|
||
reasonPlaceholderMaintenance: 'ex. Vidange, rotation des pneus…',
|
||
datesRequired: 'Les dates de début et de fin sont requises.',
|
||
endAfterStart: 'La date de fin doit être après la date de début.',
|
||
failedCreateBlock: 'Échec de la création du blocage',
|
||
cancel: 'Annuler',
|
||
saveBlock: 'Enregistrer',
|
||
saving: 'Enregistrement…',
|
||
removeBlock: 'Supprimer le blocage',
|
||
dayHeaders: ['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam'],
|
||
},
|
||
dashboardPage: {
|
||
failedToLoad: 'Échec du chargement du tableau de bord',
|
||
forbidden: "Vous n'avez pas la permission d'accéder à cette page. Un rôle Gestionnaire ou supérieur est requis.",
|
||
kpiTotalBookings: 'Total des réservations',
|
||
kpiActiveVehicles: 'Véhicules actifs',
|
||
kpiMonthlyRevenue: 'Revenus du mois',
|
||
kpiTotalCustomers: 'Total des clients',
|
||
vsLastMonth: 'vs mois dernier',
|
||
bookingSources: 'Sources de réservation',
|
||
barBookings: 'Réservations',
|
||
barRevenue: 'Revenus (MAD)',
|
||
noBookingData: 'Aucune donnée de réservation disponible',
|
||
quickStats: 'Statistiques rapides',
|
||
noData: 'Aucune donnée',
|
||
recentReservations: 'Réservations récentes',
|
||
viewAll: 'Voir tout →',
|
||
colBookingNum: 'Réservation #',
|
||
colCustomer: 'Client',
|
||
colVehicle: 'Véhicule',
|
||
colDates: 'Dates',
|
||
colStatus: 'Statut',
|
||
colTotal: 'Total',
|
||
noReservations: "Aucune réservation pour l'instant. Les réservations clients apparaîtront ici.",
|
||
trialEnds: (date) => `Vous êtes en période d'essai. Fin de l'essai le ${date}.`,
|
||
trialActive: "Vous êtes en période d'essai.",
|
||
pastDueMsg: 'Votre paiement est en retard. Veuillez mettre à jour vos informations de facturation.',
|
||
suspendedMsg: 'Votre compte a été suspendu. Veuillez contacter le support ou renouveler votre abonnement.',
|
||
manageBilling: 'Gérer l’abonnement →',
|
||
statusLabels: { CONFIRMED: 'Confirmé', PENDING: 'En attente', ACTIVE: 'Actif', COMPLETED: 'Terminé', CANCELLED: 'Annulé' },
|
||
},
|
||
reservations: {
|
||
heading: 'Réservations',
|
||
subtitle: 'Toutes les sources de réservation : tableau de bord, site public et marketplace.',
|
||
colCustomer: 'Client',
|
||
colVehicle: 'Véhicule',
|
||
colDates: 'Dates',
|
||
colSource: 'Source',
|
||
colStatus: 'Statut',
|
||
colTotal: 'Total',
|
||
noReservations: 'Aucune réservation pour le moment.',
|
||
loadingReservation: 'Chargement de la réservation…',
|
||
failedToLoad: 'Échec du chargement de la réservation',
|
||
confirmReservation: 'Confirmer la réservation',
|
||
checkInVehicle: 'Enregistrer le départ',
|
||
checkOutVehicle: 'Enregistrer le retour',
|
||
working: 'En cours…',
|
||
failedConfirm: 'Échec de la confirmation de la réservation',
|
||
failedCheckIn: 'Échec de l\'enregistrement du départ',
|
||
failedCheckOut: 'Échec de l\'enregistrement du retour',
|
||
sectionCustomer: 'Client',
|
||
noPhone: 'Aucun téléphone renseigné',
|
||
licenseLabel: 'Permis :',
|
||
notCaptured: 'Non saisi',
|
||
flaggedBadge: 'Signalé',
|
||
sectionVehicle: 'Véhicule',
|
||
sectionCharges: 'Facturation',
|
||
chargeDiscount: 'Remise',
|
||
chargeInsurance: 'Assurance',
|
||
chargeAdditionalDrivers: 'Conducteurs supplémentaires',
|
||
chargePricingAdjustments: 'Ajustements tarifaires',
|
||
chargeGrandTotal: 'Total général',
|
||
appliedInsurance: 'Assurance appliquée',
|
||
pricingRulesApplied: 'Règles tarifaires appliquées',
|
||
sectionAdditionalDrivers: 'Conducteurs supplémentaires',
|
||
driverLicenseLabel: 'Permis :',
|
||
driverChargeLabel: 'Frais :',
|
||
approveBtn: 'Approuver',
|
||
approvedBadge: 'Approuvé',
|
||
noApprovalNeeded: 'Pas d\'approbation requise',
|
||
noAdditionalDrivers: 'Aucun conducteur supplémentaire ajouté à cette réservation.',
|
||
failedApproveDriver: 'Échec de l\'approbation du conducteur supplémentaire',
|
||
sectionInspectionSummary: 'Résumé des inspections',
|
||
checkInInspectionLabel: 'Inspection de départ',
|
||
checkOutInspectionLabel: 'Inspection de retour',
|
||
savedBadge: 'Enregistré',
|
||
pendingBadge: 'En attente',
|
||
inspectionCardTitle: (type) => type === 'CHECKIN' ? 'Inspection de départ' : 'Inspection de retour',
|
||
inspectionSubtitle: 'Signalez les dommages existants et nouvellement découverts sur le schéma du véhicule.',
|
||
saveInspection: 'Enregistrer l\'inspection',
|
||
savingInspection: 'Enregistrement…',
|
||
failedSaveInspection: 'Échec de l\'enregistrement de l\'inspection',
|
||
diagramHelp: 'Cliquez n\'importe où sur le schéma pour ajouter un marqueur de dommage avec le type et la gravité sélectionnés.',
|
||
mileageLabel: 'Kilométrage',
|
||
fuelLevelLabel: 'Niveau de carburant',
|
||
fuelLevels: { FULL: 'Plein', SEVEN_EIGHTHS: '7/8', THREE_QUARTERS: '3/4', FIVE_EIGHTHS: '5/8', HALF: '1/2', THREE_EIGHTHS: '3/8', QUARTER: '1/4', ONE_EIGHTH: '1/8', EMPTY: 'Vide' },
|
||
generalConditionLabel: 'État général',
|
||
employeeNotesLabel: 'Notes de l\'employé',
|
||
customerAcknowledged: 'Le client a reconnu cette inspection',
|
||
damageMarkersHeading: 'Marqueurs de dommages',
|
||
removeMarker: 'Supprimer',
|
||
noMarkers: 'Aucun marqueur de dommage enregistré.',
|
||
damageTypeLabels: { SCRATCH: 'Rayure', DENT: 'Bosse', CRACK: 'Fissure', CHIP: 'Éclat', MISSING: 'Manquant', STAIN: 'Tache', OTHER: 'Autre' },
|
||
severityLabels: { MINOR: 'Léger', MODERATE: 'Modéré', MAJOR: 'Majeur' },
|
||
},
|
||
},
|
||
ar: {
|
||
nav: {
|
||
dashboard: 'لوحة التحكم',
|
||
fleet: 'الأسطول',
|
||
reservations: 'الحجوزات',
|
||
onlineReservations: 'الحجوزات الإلكترونية',
|
||
customers: 'العملاء',
|
||
offers: 'العروض',
|
||
team: 'الفريق',
|
||
reports: 'التقارير',
|
||
subscription: 'الاشتراك',
|
||
billing: 'فوترة العملاء',
|
||
contracts: 'العقود',
|
||
notifications: 'الإشعارات',
|
||
settings: 'الإعدادات',
|
||
},
|
||
titles: {
|
||
'/dashboard': 'لوحة التحكم',
|
||
'/dashboard/fleet': 'إدارة الأسطول',
|
||
'/dashboard/reservations': 'الحجوزات',
|
||
'/dashboard/reservations/new': 'حجز سيارة',
|
||
'/dashboard/online-reservations': 'الحجوزات الإلكترونية',
|
||
'/dashboard/customers': 'العملاء',
|
||
'/dashboard/offers': 'العروض',
|
||
'/dashboard/team': 'الفريق',
|
||
'/dashboard/reports': 'التقارير',
|
||
'/dashboard/subscription': 'الاشتراك',
|
||
'/dashboard/billing': 'فوترة العملاء',
|
||
'/dashboard/contracts': 'العقود',
|
||
'/dashboard/notifications': 'الإشعارات',
|
||
'/dashboard/settings': 'الإعدادات',
|
||
},
|
||
notifications: 'الإشعارات',
|
||
noNewNotifications: 'لا توجد إشعارات جديدة',
|
||
unreadNotifications: (count) => `${count} إشعار غير مقروء`,
|
||
signOut: 'تسجيل الخروج',
|
||
workspaceUser: 'مستخدم مساحة العمل',
|
||
localAuth: 'مصادقة محلية',
|
||
language: 'اللغة',
|
||
theme: 'الوضع',
|
||
light: 'فاتح',
|
||
dark: 'داكن',
|
||
fleet: {
|
||
statusLabels: { AVAILABLE: 'متاح', RENTED: 'مؤجر', MAINTENANCE: 'صيانة', OUT_OF_SERVICE: 'خارج الخدمة' },
|
||
logMaintenance: 'تسجيل الصيانة',
|
||
maintenanceSubtitle: (name) => `تم تعيين ${name} في وضع الصيانة. أضف التفاصيل أدناه.`,
|
||
maintenanceTypeRequired: 'الرجاء إدخال نوع الصيانة.',
|
||
failedToSave: 'فشل حفظ سجل الصيانة.',
|
||
typeLabel: 'النوع *',
|
||
typePlaceholder: 'مثال: تغيير الزيت، تدوير الإطارات، خدمة الفرامل…',
|
||
description: 'الوصف',
|
||
descriptionPlaceholder: 'تفاصيل اختيارية…',
|
||
date: 'التاريخ *',
|
||
nextDueDate: 'تاريخ الاستحقاق التالي',
|
||
cost: 'التكلفة (درهم)',
|
||
odometer: 'عداد المسافة (كم)',
|
||
skip: 'تخطي',
|
||
saveLog: 'حفظ السجل',
|
||
saving: 'جارٍ الحفظ…',
|
||
addNewVehicle: 'إضافة مركبة جديدة',
|
||
makeMissing: 'الرجاء اختيار أو إدخال الماركة.',
|
||
modelMissing: 'الرجاء اختيار أو إدخال الطراز.',
|
||
plateMissing: 'الرجاء إدخال رقم اللوحة.',
|
||
rateMissing: 'الرجاء إدخال سعر يومي صحيح.',
|
||
failedToAdd: 'فشل إضافة المركبة. يرجى التحقق من جميع الحقول.',
|
||
make: 'الماركة *',
|
||
selectMake: 'اختر الماركة…',
|
||
other: 'أخرى…',
|
||
typeMake: 'اكتب الماركة…',
|
||
model: 'الطراز *',
|
||
selectModel: 'اختر الطراز…',
|
||
typeModel: 'اكتب الطراز…',
|
||
year: 'السنة *',
|
||
category: 'الفئة',
|
||
dailyRate: 'السعر اليومي (درهم) *',
|
||
licensePlate: 'رقم اللوحة *',
|
||
color: 'اللون',
|
||
select: 'اختر…',
|
||
typeColor: 'اكتب اللون…',
|
||
seats: 'المقاعد',
|
||
transmission: 'ناقل الحركة',
|
||
manual: 'يدوي',
|
||
automatic: 'أوتوماتيكي',
|
||
fuelType: 'نوع الوقود',
|
||
photosLabel: 'الصور (حتى 10)',
|
||
clickToAddPhotos: 'انقر لإضافة صور',
|
||
addMorePhotos: 'إضافة المزيد من الصور',
|
||
cancel: 'إلغاء',
|
||
adding: 'جارٍ الإضافة…',
|
||
addVehicle: 'إضافة المركبة',
|
||
heading: 'الأسطول',
|
||
vehicleCount: (n) => `${n} مركبة إجمالاً`,
|
||
searchPlaceholder: 'بحث عن الماركة، الطراز، اللوحة…',
|
||
categoryLabels: {
|
||
ECONOMY: 'اقتصادية',
|
||
COMPACT: 'مدمجة',
|
||
MIDSIZE: 'متوسطة',
|
||
FULLSIZE: 'كبيرة',
|
||
SUV: 'دفع رباعي',
|
||
LUXURY: 'فاخرة',
|
||
VAN: 'فان',
|
||
TRUCK: 'شاحنة',
|
||
},
|
||
fuelTypeLabels: {
|
||
GASOLINE: 'بنزين',
|
||
DIESEL: 'ديزل',
|
||
ELECTRIC: 'كهربائي',
|
||
HYBRID: 'هجين',
|
||
},
|
||
allStatuses: 'جميع الحالات',
|
||
allCategories: 'جميع الفئات',
|
||
allVisibility: 'جميع الرؤية',
|
||
published: 'منشور',
|
||
unpublished: 'غير منشور',
|
||
colVehicle: 'المركبة',
|
||
colCategory: 'الفئة',
|
||
colStatus: 'الحالة',
|
||
colDailyRate: 'السعر اليومي',
|
||
colPublished: 'النشر',
|
||
colActions: 'الإجراءات',
|
||
noPhoto: 'لا توجد صورة',
|
||
perDay: '/يوم',
|
||
noVehicles: 'لا توجد مركبات. أضف مركبتك الأولى للبدء.',
|
||
noMatch: 'لا توجد مركبات تطابق المرشحات.',
|
||
},
|
||
vehicleDetail: {
|
||
tabDetails: 'التفاصيل',
|
||
tabMaintenance: 'الصيانة',
|
||
tabCalendar: 'التقويم',
|
||
editBtn: 'تعديل',
|
||
cancelBtn: 'إلغاء',
|
||
saveBtn: 'حفظ',
|
||
savingBtn: 'جارٍ الحفظ…',
|
||
uploadingPhotosBtn: 'جارٍ رفع الصور…',
|
||
loadingVehicle: 'جارٍ تحميل المركبة…',
|
||
photosHeading: 'الصور',
|
||
noPhotosYet: 'لم يتم إضافة أي صور.',
|
||
clickToAddPhotos: 'انقر لإضافة صور',
|
||
addMorePhotos: (n) => `إضافة المزيد من الصور (${n}/10)`,
|
||
maxPhotosReached: 'تم الوصول إلى الحد الأقصى (10 صور).',
|
||
newPhotoBadge: 'جديد',
|
||
vehicleDetailsHeading: 'تفاصيل المركبة',
|
||
labelCategory: 'الفئة',
|
||
labelDailyRate: 'السعر اليومي',
|
||
labelStatus: 'الحالة',
|
||
labelSeats: 'المقاعد',
|
||
labelTransmission: 'ناقل الحركة',
|
||
labelFuelType: 'نوع الوقود',
|
||
labelColor: 'اللون',
|
||
labelOdometer: 'عداد المسافة',
|
||
labelNotes: 'الملاحظات',
|
||
makeLabel: 'الماركة *',
|
||
selectMake: 'اختر الماركة…',
|
||
other: 'أخرى…',
|
||
typeMake: 'اكتب الماركة…',
|
||
modelLabel: 'الطراز *',
|
||
selectModel: 'اختر الطراز…',
|
||
typeModel: 'اكتب الطراز…',
|
||
yearLabel: 'السنة *',
|
||
dailyRateLabel: 'السعر اليومي (درهم) *',
|
||
licensePlateLabel: 'رقم اللوحة *',
|
||
statusLabel: 'الحالة',
|
||
colorLabel: 'اللون',
|
||
selectColor: 'اختر اللون…',
|
||
typeColor: 'اكتب اللون…',
|
||
seatsLabel: 'المقاعد',
|
||
transmissionLabel: 'ناقل الحركة',
|
||
fuelTypeLabel: 'نوع الوقود',
|
||
odometerLabel: 'عداد المسافة (كم)',
|
||
notesLabel: 'الملاحظات',
|
||
optionalNotes: 'ملاحظات اختيارية…',
|
||
optional: 'اختياري…',
|
||
makeRequired: 'الماركة مطلوبة.',
|
||
modelRequired: 'الطراز مطلوب.',
|
||
plateRequired: 'رقم اللوحة مطلوب.',
|
||
rateInvalid: 'يرجى إدخال سعر يومي صحيح.',
|
||
failedSave: 'فشل الحفظ.',
|
||
maintenanceIntro: 'تتبع صيانة هذه المركبة. انقر على سجّل لأي عنصر لتسجيل الخدمة.',
|
||
logBtn: 'سجّل',
|
||
statusNone: 'غير مسجل',
|
||
statusOverdue: 'متأخر',
|
||
statusDueSoon: 'قريباً',
|
||
statusOk: 'محدّث',
|
||
noRecord: 'لا يوجد سجل',
|
||
lastPrefix: 'آخر:',
|
||
atKm: (km) => `عند ${km} كم`,
|
||
duePrefix: '· الموعد:',
|
||
orSep: 'أو',
|
||
kmLeft: (km) => `${km} كم متبقية`,
|
||
overdueByKm: 'تجاوز المسافة',
|
||
serviceDateLabel: 'تاريخ الخدمة *',
|
||
odometerAtService: 'قراءة العداد عند الخدمة (كم)',
|
||
nextDueDateLabel: 'تاريخ الخدمة القادمة',
|
||
nextDueKmLabel: 'المسافة عند الخدمة القادمة (كم)',
|
||
costLabel: 'التكلفة (درهم)',
|
||
maintenanceNotesLabel: 'الملاحظات',
|
||
saveLabel: 'حفظ',
|
||
savingLabel: 'جارٍ الحفظ…',
|
||
dateRequired: 'تاريخ الخدمة مطلوب.',
|
||
routineTypes: {
|
||
'Oil Change': 'تغيير الزيت',
|
||
'Technical Inspection': 'الفحص التقني',
|
||
'Vehicle Registration': 'تسجيل المركبة',
|
||
'Car Tax': 'الضريبة السنوية',
|
||
'Tire Rotation': 'تدوير الإطارات',
|
||
'Brake Inspection': 'فحص الفرامل',
|
||
'Air Filter': 'فلتر الهواء',
|
||
'Cabin Filter': 'فلتر المقصورة',
|
||
'Battery Check': 'فحص البطارية',
|
||
'Belt / Chain Service': 'خدمة السير / السلسلة',
|
||
'Coolant Flush': 'تغيير سائل التبريد',
|
||
'Transmission Service': 'خدمة ناقل الحركة',
|
||
},
|
||
},
|
||
calendar: {
|
||
heading: 'تقويم التوفر',
|
||
blockDates: 'حجب التواريخ',
|
||
clickSecondDay: (selected) => `انقر على يوم ثانٍ لتحديد نهاية الحجب (المختار: ${selected})`,
|
||
cancelSelection: 'إلغاء',
|
||
legendReserved: 'محجوز',
|
||
legendMaintenance: 'صيانة',
|
||
legendBlocked: 'محجوب',
|
||
legendAvailable: 'متاح',
|
||
failedLoad: 'فشل تحميل التقويم',
|
||
moreDays: (n) => `+${n} أكثر`,
|
||
reservationsThisMonth: 'الحجوزات هذا الشهر',
|
||
blocksAndMaintenance: 'الحجب والصيانة',
|
||
removeBlockConfirm: 'حذف هذا الحجب؟',
|
||
failedDeleteBlock: 'فشل حذف الحجب',
|
||
modalTitle: 'حجب تواريخ المركبة',
|
||
blockTypeLabel: 'نوع الحجب',
|
||
manualBlock: 'حجب يدوي',
|
||
maintenanceBlock: 'صيانة',
|
||
startDate: 'تاريخ البداية',
|
||
endDate: 'تاريخ النهاية (غير شامل)',
|
||
reasonLabel: 'السبب',
|
||
optional: '(اختياري)',
|
||
reasonPlaceholderManual: 'مثال: محجوز لشخص مميز، فعالية…',
|
||
reasonPlaceholderMaintenance: 'مثال: تغيير الزيت، تدوير الإطارات…',
|
||
datesRequired: 'تواريخ البداية والنهاية مطلوبة.',
|
||
endAfterStart: 'يجب أن يكون تاريخ النهاية بعد تاريخ البداية.',
|
||
failedCreateBlock: 'فشل إنشاء الحجب',
|
||
cancel: 'إلغاء',
|
||
saveBlock: 'حفظ',
|
||
saving: 'جارٍ الحفظ…',
|
||
removeBlock: 'حذف الحجب',
|
||
dayHeaders: ['أحد', 'اثن', 'ثلا', 'أرب', 'خمي', 'جمع', 'سبت'],
|
||
},
|
||
dashboardPage: {
|
||
failedToLoad: 'فشل تحميل لوحة التحكم',
|
||
forbidden: 'ليس لديك صلاحية الوصول إلى هذه الصفحة. يلزم دور المدير أو أعلى.',
|
||
kpiTotalBookings: 'إجمالي الحجوزات',
|
||
kpiActiveVehicles: 'المركبات النشطة',
|
||
kpiMonthlyRevenue: 'الإيرادات الشهرية',
|
||
kpiTotalCustomers: 'إجمالي العملاء',
|
||
vsLastMonth: 'مقارنة بالشهر الماضي',
|
||
bookingSources: 'مصادر الحجز',
|
||
barBookings: 'الحجوزات',
|
||
barRevenue: 'الإيرادات (درهم)',
|
||
noBookingData: 'لا توجد بيانات حجز متاحة بعد',
|
||
quickStats: 'إحصائيات سريعة',
|
||
noData: 'لا توجد بيانات',
|
||
recentReservations: 'الحجوزات الأخيرة',
|
||
viewAll: 'عرض الكل ←',
|
||
colBookingNum: 'الحجز #',
|
||
colCustomer: 'العميل',
|
||
colVehicle: 'المركبة',
|
||
colDates: 'التواريخ',
|
||
colStatus: 'الحالة',
|
||
colTotal: 'الإجمالي',
|
||
noReservations: 'لا توجد حجوزات بعد. ستظهر الحجوزات هنا عند قيام العملاء بالحجز.',
|
||
trialEnds: (date) => `أنت في فترة تجريبية. تنتهي في ${date}.`,
|
||
trialActive: 'أنت في فترة تجريبية مجانية.',
|
||
pastDueMsg: 'دفعتك متأخرة. يرجى تحديث معلومات الفوترة.',
|
||
suspendedMsg: 'تم تعليق حسابك. يرجى التواصل مع الدعم أو تجديد اشتراكك.',
|
||
manageBilling: 'إدارة الاشتراك ←',
|
||
statusLabels: { CONFIRMED: 'مؤكد', PENDING: 'قيد الانتظار', ACTIVE: 'نشط', COMPLETED: 'مكتمل', CANCELLED: 'ملغى' },
|
||
},
|
||
reservations: {
|
||
heading: 'الحجوزات',
|
||
subtitle: 'جميع مصادر الحجز: لوحة التحكم، الموقع العام، والسوق الإلكتروني.',
|
||
colCustomer: 'العميل',
|
||
colVehicle: 'المركبة',
|
||
colDates: 'التواريخ',
|
||
colSource: 'المصدر',
|
||
colStatus: 'الحالة',
|
||
colTotal: 'الإجمالي',
|
||
noReservations: 'لا توجد حجوزات بعد.',
|
||
loadingReservation: 'جارٍ تحميل الحجز…',
|
||
failedToLoad: 'فشل تحميل الحجز',
|
||
confirmReservation: 'تأكيد الحجز',
|
||
checkInVehicle: 'تسجيل الاستلام',
|
||
checkOutVehicle: 'تسجيل الإرجاع',
|
||
working: 'جارٍ التنفيذ…',
|
||
failedConfirm: 'فشل تأكيد الحجز',
|
||
failedCheckIn: 'فشل تسجيل الاستلام',
|
||
failedCheckOut: 'فشل تسجيل الإرجاع',
|
||
sectionCustomer: 'العميل',
|
||
noPhone: 'لم يُدخل رقم هاتف',
|
||
licenseLabel: 'الرخصة:',
|
||
notCaptured: 'غير مُسجّل',
|
||
flaggedBadge: 'مُبلَّغ عنه',
|
||
sectionVehicle: 'المركبة',
|
||
sectionCharges: 'الرسوم',
|
||
chargeDiscount: 'الخصم',
|
||
chargeInsurance: 'التأمين',
|
||
chargeAdditionalDrivers: 'السائقون الإضافيون',
|
||
chargePricingAdjustments: 'تعديلات التسعير',
|
||
chargeGrandTotal: 'الإجمالي الكلي',
|
||
appliedInsurance: 'التأمين المطبق',
|
||
pricingRulesApplied: 'قواعد التسعير المطبقة',
|
||
sectionAdditionalDrivers: 'السائقون الإضافيون',
|
||
driverLicenseLabel: 'الرخصة:',
|
||
driverChargeLabel: 'الرسوم:',
|
||
approveBtn: 'موافقة',
|
||
approvedBadge: 'مُوافَق عليه',
|
||
noApprovalNeeded: 'لا يحتاج موافقة',
|
||
noAdditionalDrivers: 'لم يتم إضافة سائقين إضافيين لهذا الحجز.',
|
||
failedApproveDriver: 'فشلت الموافقة على السائق الإضافي',
|
||
sectionInspectionSummary: 'ملخص الفحص',
|
||
checkInInspectionLabel: 'فحص الاستلام',
|
||
checkOutInspectionLabel: 'فحص الإرجاع',
|
||
savedBadge: 'محفوظ',
|
||
pendingBadge: 'معلّق',
|
||
inspectionCardTitle: (type) => type === 'CHECKIN' ? 'فحص الاستلام' : 'فحص الإرجاع',
|
||
inspectionSubtitle: 'حدد الأضرار الموجودة والمكتشفة حديثاً مباشرة على مخطط المركبة.',
|
||
saveInspection: 'حفظ الفحص',
|
||
savingInspection: 'جارٍ الحفظ…',
|
||
failedSaveInspection: 'فشل حفظ الفحص',
|
||
diagramHelp: 'انقر في أي مكان على المخطط لإضافة علامة ضرر بالنوع والخطورة المحددين.',
|
||
mileageLabel: 'عداد المسافة',
|
||
fuelLevelLabel: 'مستوى الوقود',
|
||
fuelLevels: { FULL: 'ممتلئ', SEVEN_EIGHTHS: '7/8', THREE_QUARTERS: '3/4', FIVE_EIGHTHS: '5/8', HALF: '1/2', THREE_EIGHTHS: '3/8', QUARTER: '1/4', ONE_EIGHTH: '1/8', EMPTY: 'فارغ' },
|
||
generalConditionLabel: 'الحالة العامة',
|
||
employeeNotesLabel: 'ملاحظات الموظف',
|
||
customerAcknowledged: 'وافق العميل على هذا الفحص',
|
||
damageMarkersHeading: 'علامات الضرر',
|
||
removeMarker: 'حذف',
|
||
noMarkers: 'لا توجد علامات ضرر محفوظة.',
|
||
damageTypeLabels: { SCRATCH: 'خدش', DENT: 'انبعاج', CRACK: 'تشقق', CHIP: 'تقشر', MISSING: 'مفقود', STAIN: 'بقعة', OTHER: 'أخرى' },
|
||
severityLabels: { MINOR: 'طفيف', MODERATE: 'متوسط', MAJOR: 'شديد' },
|
||
},
|
||
},
|
||
}
|
||
|
||
type I18nContextValue = {
|
||
language: DashboardLanguage
|
||
setLanguage: (value: DashboardLanguage) => void
|
||
theme: DashboardTheme
|
||
setTheme: (value: DashboardTheme) => void
|
||
dict: DashboardDictionary
|
||
}
|
||
|
||
const I18nContext = createContext<I18nContextValue | null>(null)
|
||
const DASHBOARD_LANGUAGE_KEY = 'dashboard-language'
|
||
|
||
function normalizeLanguage(value: string | null | undefined): DashboardLanguage {
|
||
return value === 'fr' || value === 'ar' || value === 'en' ? value : 'en'
|
||
}
|
||
|
||
function detectBrowserLanguage(): DashboardLanguage | null {
|
||
if (typeof navigator === 'undefined') return null
|
||
const langs = Array.from(navigator.languages ?? [navigator.language])
|
||
for (const lang of langs) {
|
||
const code = lang.split('-')[0].toLowerCase()
|
||
if (code === 'fr' || code === 'ar' || code === 'en') return code as DashboardLanguage
|
||
}
|
||
return null
|
||
}
|
||
|
||
export function DashboardI18nProvider({
|
||
children,
|
||
initialLanguage = 'en',
|
||
}: {
|
||
children: React.ReactNode
|
||
initialLanguage?: DashboardLanguage
|
||
}) {
|
||
const [language, setLanguageState] = useState<DashboardLanguage>(initialLanguage)
|
||
const [theme, setThemeState] = useState<DashboardTheme>('light')
|
||
const themeInitialized = useRef(false)
|
||
// Skip the very first write so we don't overwrite a stored preference with the
|
||
// server-resolved initialLanguage before the read effect has a chance to apply it.
|
||
const skipFirstLangWrite = useRef(true)
|
||
|
||
function applyLanguage(nextLanguage: DashboardLanguage) {
|
||
if (nextLanguage === language) return
|
||
|
||
if (typeof document !== 'undefined') {
|
||
document.documentElement.lang = nextLanguage
|
||
document.documentElement.dir = nextLanguage === 'ar' ? 'rtl' : 'ltr'
|
||
writeScopedPreference(SHARED_LANGUAGE_KEY, nextLanguage, [DASHBOARD_LANGUAGE_KEY])
|
||
document.cookie = `${SHARED_LANGUAGE_COOKIE}=${nextLanguage}; path=/; max-age=31536000; samesite=lax`
|
||
document.cookie = `${DASHBOARD_LANGUAGE_KEY}=${nextLanguage}; path=/; max-age=31536000; samesite=lax`
|
||
}
|
||
|
||
setLanguageState(nextLanguage)
|
||
}
|
||
|
||
function applyTheme(nextTheme: DashboardTheme) {
|
||
if (nextTheme === theme) return
|
||
|
||
if (typeof document !== 'undefined') {
|
||
document.documentElement.classList.remove('light', 'dark')
|
||
document.documentElement.classList.add(nextTheme === 'light' ? 'light' : 'dark')
|
||
document.documentElement.style.colorScheme = nextTheme === 'light' ? 'light' : 'dark'
|
||
document.body.dataset.theme = nextTheme
|
||
writeScopedPreference(SHARED_THEME_KEY, nextTheme, ['dashboard-theme'])
|
||
}
|
||
|
||
setThemeState(nextTheme)
|
||
}
|
||
|
||
useEffect(() => {
|
||
const stored = readScopedPreference(SHARED_LANGUAGE_KEY, [DASHBOARD_LANGUAGE_KEY])
|
||
if (stored !== null) {
|
||
const next = normalizeLanguage(stored)
|
||
if (next !== language) setLanguageState(next)
|
||
} else {
|
||
const detected = detectBrowserLanguage()
|
||
if (detected && detected !== language) setLanguageState(detected)
|
||
}
|
||
}, [])
|
||
|
||
useEffect(() => {
|
||
const storedTheme = readScopedPreference(SHARED_THEME_KEY, ['dashboard-theme'])
|
||
if (storedTheme === 'light' || storedTheme === 'dark') {
|
||
if (storedTheme !== theme) setThemeState(storedTheme)
|
||
return
|
||
}
|
||
|
||
if (window.matchMedia('(prefers-color-scheme: dark)').matches && theme !== 'dark') {
|
||
setThemeState('dark')
|
||
}
|
||
}, [])
|
||
|
||
useEffect(() => {
|
||
document.documentElement.lang = language
|
||
document.documentElement.dir = language === 'ar' ? 'rtl' : 'ltr'
|
||
if (skipFirstLangWrite.current) {
|
||
skipFirstLangWrite.current = false
|
||
return
|
||
}
|
||
writeScopedPreference(SHARED_LANGUAGE_KEY, language, [DASHBOARD_LANGUAGE_KEY])
|
||
document.cookie = `${SHARED_LANGUAGE_COOKIE}=${language}; path=/; max-age=31536000; samesite=lax`
|
||
document.cookie = `${DASHBOARD_LANGUAGE_KEY}=${language}; path=/; max-age=31536000; samesite=lax`
|
||
}, [language])
|
||
|
||
useEffect(() => {
|
||
if (!themeInitialized.current) {
|
||
// Skip first run: inline script already applied the correct class; writing 'light' here would flash and corrupt storage.
|
||
themeInitialized.current = true
|
||
return
|
||
}
|
||
document.documentElement.classList.remove('light', 'dark')
|
||
document.documentElement.classList.add(theme === 'light' ? 'light' : 'dark')
|
||
document.documentElement.style.colorScheme = theme === 'light' ? 'light' : 'dark'
|
||
document.body.dataset.theme = theme
|
||
writeScopedPreference(SHARED_THEME_KEY, theme, ['dashboard-theme'])
|
||
}, [theme])
|
||
|
||
useEffect(() => {
|
||
function handleAuthChanged() {
|
||
const scopedLanguage = readCurrentUserScopedPreference(SHARED_LANGUAGE_KEY)
|
||
const scopedTheme = readCurrentUserScopedPreference(SHARED_THEME_KEY)
|
||
|
||
if (scopedLanguage) {
|
||
// Employee has a stored preference — apply it.
|
||
const next = normalizeLanguage(scopedLanguage)
|
||
if (next !== language) setLanguageState(next)
|
||
} else {
|
||
// No scoped pref yet (new login). Promote any base preference that was set
|
||
// before login (e.g., language selected on the sign-up page) to the scoped key.
|
||
const baseLang = readScopedPreference(SHARED_LANGUAGE_KEY, [DASHBOARD_LANGUAGE_KEY])
|
||
const langToWrite = baseLang ? normalizeLanguage(baseLang) : language
|
||
if (langToWrite !== language) setLanguageState(langToWrite)
|
||
writeScopedPreference(SHARED_LANGUAGE_KEY, langToWrite, [DASHBOARD_LANGUAGE_KEY])
|
||
}
|
||
|
||
if (scopedTheme === 'light' || scopedTheme === 'dark') {
|
||
if (scopedTheme !== theme) setThemeState(scopedTheme)
|
||
} else {
|
||
writeScopedPreference(SHARED_THEME_KEY, theme, ['dashboard-theme'])
|
||
}
|
||
}
|
||
|
||
window.addEventListener('rentaldrivego:auth-changed', handleAuthChanged as EventListener)
|
||
return () => window.removeEventListener('rentaldrivego:auth-changed', handleAuthChanged as EventListener)
|
||
}, [language, theme])
|
||
|
||
const value = useMemo(
|
||
() => ({ language, setLanguage: applyLanguage, theme, setTheme: applyTheme, dict: dictionaries[language] }),
|
||
[language, theme],
|
||
)
|
||
return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>
|
||
}
|
||
|
||
export function useDashboardI18n() {
|
||
const context = useContext(I18nContext)
|
||
if (!context) throw new Error('useDashboardI18n must be used within DashboardI18nProvider')
|
||
return context
|
||
}
|
||
|
||
export function DashboardLanguageSwitcher() {
|
||
const { language, setLanguage, dict } = useDashboardI18n()
|
||
const [embedded, setEmbedded] = useState(false)
|
||
|
||
useEffect(() => {
|
||
setEmbedded(window.self !== window.top)
|
||
}, [])
|
||
|
||
if (embedded) return null
|
||
|
||
function handleLanguageChange(value: DashboardLanguage) {
|
||
setLanguage(value)
|
||
apiFetch('/auth/employee/me/language', {
|
||
method: 'PATCH',
|
||
body: JSON.stringify({ language: value }),
|
||
}).catch(() => {})
|
||
}
|
||
|
||
return (
|
||
<div className="flex items-center gap-1 rounded-full border border-slate-200 bg-white px-2 py-1 shadow-sm transition-colors dark:border-slate-700 dark:bg-slate-900">
|
||
<span className="px-2 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">
|
||
{dict.language}
|
||
</span>
|
||
{(['en', 'fr', 'ar'] as DashboardLanguage[]).map((value) => {
|
||
const active = value === language
|
||
return (
|
||
<button
|
||
key={value}
|
||
type="button"
|
||
onClick={() => handleLanguageChange(value)}
|
||
className={`rounded-full px-3 py-1.5 text-xs font-semibold transition ${
|
||
active
|
||
? 'bg-slate-900 text-white dark:bg-slate-100 dark:text-slate-950'
|
||
: 'text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800'
|
||
}`}
|
||
>
|
||
{value.toUpperCase()}
|
||
</button>
|
||
)
|
||
})}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export function DashboardThemeSwitcher() {
|
||
const { theme, setTheme, dict } = useDashboardI18n()
|
||
const [embedded, setEmbedded] = useState(false)
|
||
|
||
useEffect(() => {
|
||
setEmbedded(window.self !== window.top)
|
||
}, [])
|
||
|
||
if (embedded) return null
|
||
|
||
return (
|
||
<div className="flex items-center gap-1 rounded-full border border-slate-200 bg-white px-2 py-1 shadow-sm transition-colors dark:border-slate-700 dark:bg-slate-900">
|
||
<span className="px-2 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500 dark:text-slate-400">
|
||
{dict.theme}
|
||
</span>
|
||
{(['light', 'dark'] as DashboardTheme[]).map((value) => {
|
||
const active = value === theme
|
||
return (
|
||
<button
|
||
key={value}
|
||
type="button"
|
||
onClick={() => setTheme(value)}
|
||
className={`rounded-full px-3 py-1.5 text-xs font-semibold transition ${
|
||
active
|
||
? 'bg-slate-900 text-white dark:bg-slate-100 dark:text-slate-950'
|
||
: 'text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800'
|
||
}`}
|
||
>
|
||
{value === 'light' ? dict.light : dict.dark}
|
||
</button>
|
||
)
|
||
})}
|
||
</div>
|
||
)
|
||
}
|