add pricing feature

This commit is contained in:
root
2026-06-02 01:15:04 -04:00
parent 05d7b1bf8c
commit 0df950f2b1
18 changed files with 2364 additions and 29 deletions
@@ -1,12 +1,13 @@
'use client'
import { useEffect, useRef, useState } from 'react'
import { useParams, useRouter } from 'next/navigation'
import { useParams, useRouter, useSearchParams } from 'next/navigation'
import Image from 'next/image'
import { ArrowLeft, Edit, X, Check, Upload, Trash2, CalendarDays, Info, Wrench, Plus, ChevronDown, ChevronUp } from 'lucide-react'
import { ArrowLeft, Edit, X, Check, Upload, Trash2, CalendarDays, DollarSign, Info, Wrench, Plus, ChevronDown, ChevronUp } from 'lucide-react'
import { formatCurrency } from '@rentaldrivego/types'
import { apiFetch } from '@/lib/api'
import VehicleCalendar from '@/components/VehicleCalendar'
import VehiclePricingManager from '@/components/VehiclePricingManager'
import { useDashboardI18n } from '@/components/I18nProvider'
interface VehicleDetail {
@@ -23,6 +24,7 @@ interface VehicleDetail {
seats: number
mileage: number | null
licensePlate: string
vin: string | null
photos: string[]
notes: string | null
isPublished: boolean
@@ -276,12 +278,13 @@ function MaintenanceRow({ vehicleId, typeKey, intervalMonths, currentMileage, lo
)
}
type Tab = 'details' | 'maintenance' | 'calendar'
type Tab = 'details' | 'maintenance' | 'calendar' | 'pricing'
export default function FleetDetailPage() {
const params = useParams<{ id: string }>()
const router = useRouter()
const { dict } = useDashboardI18n()
const searchParams = useSearchParams()
const { dict, language } = useDashboardI18n()
const vd = dict.vehicleDetail
const fl = dict.fleet
@@ -294,7 +297,7 @@ export default function FleetDetailPage() {
const [form, setForm] = useState<{
make: string; model: string; year: number; category: string
dailyRate: string; licensePlate: string; color: string
dailyRate: string; licensePlate: string; vin: string; color: string
seats: number; transmission: string; fuelType: string
status: string; notes: string; mileage: string
pickupLocations: string; allowDifferentDropoff: boolean; dropoffLocations: string
@@ -325,6 +328,12 @@ export default function FleetDetailPage() {
.catch((err) => setError(err.message))
}, [params.id])
useEffect(() => {
if (searchParams.get('tab') === 'pricing') {
setActiveTab('pricing')
}
}, [searchParams])
const startEdit = () => {
if (!vehicle) return
setForm({
@@ -332,6 +341,7 @@ export default function FleetDetailPage() {
category: vehicle.category,
dailyRate: (vehicle.dailyRate / 100).toFixed(2),
licensePlate: vehicle.licensePlate,
vin: vehicle.vin ?? '',
color: vehicle.color ?? '', seats: vehicle.seats,
transmission: vehicle.transmission, fuelType: vehicle.fuelType,
status: vehicle.status, notes: vehicle.notes ?? '',
@@ -376,7 +386,7 @@ export default function FleetDetailPage() {
body: JSON.stringify({
make: form.make, model: form.model, year: Number(form.year),
category: form.category, dailyRate: Math.round(rate * 100),
licensePlate: form.licensePlate, color: form.color,
licensePlate: form.licensePlate, vin: form.vin.trim() || undefined, color: form.color,
seats: Number(form.seats), transmission: form.transmission,
fuelType: form.fuelType, status: form.status,
notes: form.notes || undefined,
@@ -449,6 +459,7 @@ export default function FleetDetailPage() {
const statusLabel = fl.statusLabels[vehicle.status as keyof typeof fl.statusLabels] ?? vehicle.status
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'
return (
<div className="space-y-6">
@@ -460,7 +471,12 @@ export default function FleetDetailPage() {
</button>
<div>
<h2 className="text-xl font-semibold text-slate-900">{vehicle.make} {vehicle.model}</h2>
<p className="text-sm text-slate-500 mt-0.5">{vehicle.year} · {vehicle.licensePlate} · {statusLabel}</p>
<p className="text-sm text-slate-500 mt-0.5">
{vehicle.year} · {vehicle.licensePlate}
{vehicle.vin ? ` · ${vehicle.vin}` : ''}
{' · '}
{statusLabel}
</p>
</div>
</div>
{!editing ? (
@@ -486,7 +502,7 @@ export default function FleetDetailPage() {
{/* Tab bar */}
<div className="flex gap-1 border-b border-slate-200 dark:border-slate-800">
{(['details', 'maintenance', 'calendar'] as Tab[]).map((tab) => (
{(['details', 'maintenance', 'calendar', 'pricing'] as Tab[]).map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
@@ -505,6 +521,7 @@ export default function FleetDetailPage() {
</>
)}
{tab === 'calendar' && <><CalendarDays className="w-4 h-4" /> {vd.tabCalendar}</>}
{tab === 'pricing' && <><DollarSign className="w-4 h-4" /> {pricingTabLabel}</>}
</button>
))}
</div>
@@ -578,6 +595,7 @@ export default function FleetDetailPage() {
<div><dt className="text-slate-500">{vd.labelCategory}</dt><dd className="text-slate-900">{categoryLabel}</dd></div>
<div><dt className="text-slate-500">{vd.labelDailyRate}</dt><dd className="text-slate-900">{formatCurrency(vehicle.dailyRate, 'MAD')}</dd></div>
<div><dt className="text-slate-500">{vd.labelStatus}</dt><dd className="text-slate-900">{statusLabel}</dd></div>
<div><dt className="text-slate-500">{vd.vinLabel}</dt><dd className="text-slate-900">{vehicle.vin || '—'}</dd></div>
<div><dt className="text-slate-500">{vd.labelSeats}</dt><dd className="text-slate-900">{vehicle.seats}</dd></div>
<div><dt className="text-slate-500">{vd.labelTransmission}</dt><dd className="text-slate-900">{transLabel}</dd></div>
<div><dt className="text-slate-500">{vd.labelFuelType}</dt><dd className="text-slate-900">{fuelLabel}</dd></div>
@@ -657,8 +675,8 @@ export default function FleetDetailPage() {
</div>
</div>
{/* Daily Rate & License Plate */}
<div className="grid grid-cols-2 gap-3">
{/* Daily Rate, License Plate & VIN */}
<div className="grid gap-3 sm:grid-cols-2">
<div>
<label className="block text-xs font-medium text-slate-500 mb-1.5">{vd.dailyRateLabel}</label>
<input type="number" className="input-field" min="0" step="0.01" value={form.dailyRate} onChange={(e) => setForm({ ...form, dailyRate: e.target.value })} />
@@ -668,6 +686,10 @@ export default function FleetDetailPage() {
<input className="input-field" value={form.licensePlate} onChange={(e) => setForm({ ...form, licensePlate: e.target.value })} />
</div>
</div>
<div>
<label className="block text-xs font-medium text-slate-500 mb-1.5">{vd.vinLabel}</label>
<input className="input-field" placeholder="1HGCM82633A123456" value={form.vin} onChange={(e) => setForm({ ...form, vin: e.target.value })} />
</div>
{/* Status */}
<div>
@@ -807,6 +829,10 @@ export default function FleetDetailPage() {
{activeTab === 'calendar' && (
<VehicleCalendar vehicleId={params.id} />
)}
{activeTab === 'pricing' && (
<VehiclePricingManager vehicleId={params.id} />
)}
</div>
)
}
@@ -1,7 +1,7 @@
'use client'
import { useEffect, useRef, useState } from 'react'
import { Plus, Edit, Eye, Search, Upload, X, ChevronDown } from 'lucide-react'
import { Plus, Edit, Eye, Search, Upload, X, ChevronDown, DollarSign } from 'lucide-react'
import Image from 'next/image'
import Link from 'next/link'
import { apiFetch } from '@/lib/api'
@@ -19,6 +19,7 @@ interface Vehicle {
isPublished: boolean
primaryPhoto: string | null
licensePlate: string
vin?: string | null
pickupLocations?: string[]
allowDifferentDropoff?: boolean
dropoffLocations?: string[]
@@ -279,7 +280,7 @@ function AddVehicleModal({ open, onClose, onSaved }: AddVehicleModalProps) {
const f = dict.fleet
const [form, setForm] = useState({
make: '', model: '', year: new Date().getFullYear(), category: 'ECONOMY',
dailyRate: '', licensePlate: '', color: '', seats: 5, transmission: 'MANUAL',
licensePlate: '', vin: '', color: '', seats: 5, transmission: 'MANUAL',
fuelType: 'GASOLINE', mileage: '', pickupLocations: '', allowDifferentDropoff: false, dropoffLocations: '',
})
const [colorSelect, setColorSelect] = useState('')
@@ -311,7 +312,7 @@ function AddVehicleModal({ open, onClose, onSaved }: AddVehicleModalProps) {
const reset = () => {
setForm({
make: '', model: '', year: new Date().getFullYear(), category: 'ECONOMY',
dailyRate: '', licensePlate: '', color: '', seats: 5, transmission: 'MANUAL',
licensePlate: '', vin: '', color: '', seats: 5, transmission: 'MANUAL',
fuelType: 'GASOLINE', mileage: '', pickupLocations: '', allowDifferentDropoff: false, dropoffLocations: '',
})
setColorSelect('')
@@ -330,7 +331,6 @@ function AddVehicleModal({ open, onClose, onSaved }: AddVehicleModalProps) {
if (!form.make) { setError(f.makeMissing); return }
if (!form.model) { setError(f.modelMissing); return }
if (!form.licensePlate) { setError(f.plateMissing); return }
if (!form.dailyRate || isNaN(parseFloat(form.dailyRate))) { setError(f.rateMissing); return }
if (form.allowDifferentDropoff && parseLocationInput(form.dropoffLocations).length === 0) {
setError(f.dropoffLocationsRequired)
return
@@ -342,9 +342,9 @@ function AddVehicleModal({ open, onClose, onSaved }: AddVehicleModalProps) {
method: 'POST',
body: JSON.stringify({
...form,
dailyRate: Math.round(parseFloat(form.dailyRate) * 100),
year: Number(form.year),
seats: Number(form.seats),
vin: form.vin.trim() || undefined,
mileage: form.mileage ? Number(form.mileage) : undefined,
pickupLocations: parseLocationInput(form.pickupLocations),
allowDifferentDropoff: form.allowDifferentDropoff,
@@ -442,16 +442,16 @@ function AddVehicleModal({ open, onClose, onSaved }: AddVehicleModalProps) {
</div>
</div>
{/* Daily Rate & License Plate */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-medium text-slate-500 mb-1.5">{f.dailyRate}</label>
<input type="number" className="input-field" placeholder="350.00" min="0" step="0.01" required value={form.dailyRate} onChange={(e) => setForm({ ...form, dailyRate: e.target.value })} />
</div>
{/* License Plate & VIN */}
<div className="grid gap-4 sm:grid-cols-2">
<div>
<label className="block text-xs font-medium text-slate-500 mb-1.5">{f.licensePlate}</label>
<input className="input-field" placeholder="AB-1234-CD" required value={form.licensePlate} onChange={(e) => setForm({ ...form, licensePlate: e.target.value })} />
</div>
<div>
<label className="block text-xs font-medium text-slate-500 mb-1.5">{f.vin}</label>
<input className="input-field" placeholder="1HGCM82633A123456" value={form.vin} onChange={(e) => setForm({ ...form, vin: e.target.value })} />
</div>
</div>
{/* Color, Seats, Transmission */}
@@ -649,7 +649,7 @@ export default function FleetPage() {
}
const filtered = vehicles.filter((v) => {
if (search && !`${v.make} ${v.model} ${v.licensePlate}`.toLowerCase().includes(search.toLowerCase())) return false
if (search && !`${v.make} ${v.model} ${v.licensePlate} ${v.vin ?? ''}`.toLowerCase().includes(search.toLowerCase())) return false
if (statusFilter && v.status !== statusFilter) return false
if (categoryFilter && v.category !== categoryFilter) return false
if (publishedFilter === 'published' && !v.isPublished) return false
@@ -729,7 +729,10 @@ export default function FleetPage() {
<td className="px-6 py-4">
<div>
<p className="text-sm font-semibold text-slate-900">{vehicle.make} {vehicle.model}</p>
<p className="text-xs text-slate-400">{vehicle.year} · {vehicle.licensePlate}</p>
<p className="text-xs text-slate-400">
{vehicle.year} · {vehicle.licensePlate}
{vehicle.vin ? ` · ${vehicle.vin}` : ''}
</p>
</div>
</td>
<td className="px-6 py-4">
@@ -761,6 +764,9 @@ export default function FleetPage() {
</td>
<td className="px-6 py-4">
<div className="flex items-center justify-end gap-2 rtl:justify-start">
<Link href={`/fleet/${vehicle.id}?tab=pricing`} title="Manage pricing" aria-label="Manage pricing" className="p-1.5 text-slate-400 hover:text-slate-700 hover:bg-slate-100 rounded-lg transition-colors">
<DollarSign className="w-4 h-4" />
</Link>
<Link href={`/fleet/${vehicle.id}`} className="p-1.5 text-slate-400 hover:text-slate-700 hover:bg-slate-100 rounded-lg transition-colors">
<Eye className="w-4 h-4" />
</Link>
@@ -41,6 +41,7 @@ type FleetDict = {
category: string
dailyRate: string
licensePlate: string
vin: string
color: string
select: string
typeColor: string
@@ -253,6 +254,7 @@ type VehicleDetailDict = {
yearLabel: string
dailyRateLabel: string
licensePlateLabel: string
vinLabel: string
statusLabel: string
colorLabel: string
selectColor: string
@@ -393,6 +395,7 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
category: 'Category',
dailyRate: 'Daily Rate (MAD) *',
licensePlate: 'License Plate *',
vin: 'VIN',
color: 'Color',
select: 'Select…',
typeColor: 'Type color…',
@@ -487,6 +490,7 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
yearLabel: 'Year *',
dailyRateLabel: 'Daily Rate (MAD) *',
licensePlateLabel: 'License Plate *',
vinLabel: 'VIN',
statusLabel: 'Status',
colorLabel: 'Color',
selectColor: 'Select color…',
@@ -747,6 +751,7 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
category: 'Catégorie',
dailyRate: 'Tarif journalier (MAD) *',
licensePlate: "Plaque d'immatriculation *",
vin: 'VIN',
color: 'Couleur',
select: 'Sélectionner…',
typeColor: 'Saisir la couleur…',
@@ -841,6 +846,7 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
yearLabel: 'Année *',
dailyRateLabel: 'Tarif journalier (MAD) *',
licensePlateLabel: "Plaque d'immatriculation *",
vinLabel: 'VIN',
statusLabel: 'Statut',
colorLabel: 'Couleur',
selectColor: 'Sélectionner la couleur…',
@@ -1101,6 +1107,7 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
category: 'الفئة',
dailyRate: 'السعر اليومي (درهم) *',
licensePlate: 'رقم اللوحة *',
vin: 'رقم الهيكل',
color: 'اللون',
select: 'اختر…',
typeColor: 'اكتب اللون…',
@@ -1195,6 +1202,7 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
yearLabel: 'السنة *',
dailyRateLabel: 'السعر اليومي (درهم) *',
licensePlateLabel: 'رقم اللوحة *',
vinLabel: 'رقم الهيكل',
statusLabel: 'الحالة',
colorLabel: 'اللون',
selectColor: 'اختر اللون…',
@@ -0,0 +1,848 @@
'use client'
import { useEffect, useState } from 'react'
import { CalendarRange, History, PlusCircle, Save, Sparkles, Trash2 } from 'lucide-react'
import { formatCurrency } from '@rentaldrivego/types'
import { apiFetch } from '@/lib/api'
import { useDashboardI18n } from '@/components/I18nProvider'
interface PricingConfiguration {
id: string
vehicleId: string
pricingMode: 'MANUAL' | 'AUTOMATIC'
baseDailyRate: number
weeklyRate: number | null
weekendRate: number | null
holidayRate: number | null
monthlyRate: number | null
longTermDailyRate: number | null
minimumDailyRate: number | null
maximumDailyRate: number | null
maxDailyPriceMovementPct: number
automaticPricingEnabled: boolean
approvalRequired: boolean
createdAt: string
updatedAt: string
}
interface PricingRule {
id: string
name: string
ruleType: 'DATE_RANGE' | 'SEASONAL' | 'HOLIDAY' | 'WEEKEND' | 'SPECIAL_EVENT' | 'MANUAL_OVERRIDE'
startDate: string
endDate: string
dailyRate: number | null
weeklyRate: number | null
weekendRate: number | null
monthlyRate: number | null
minDailyRate: number | null
maxDailyRate: number | null
automaticAdjustmentPct: number | null
priceAdjustment: number | null
isActive: boolean
sortOrder: number
createdAt: string
updatedAt: string
}
interface PricingHistoryEntry {
id: string
source: 'CONFIG_UPDATE' | 'RULE_CREATED' | 'RULE_UPDATED' | 'RULE_DELETED' | 'MANUAL_OVERRIDE'
changedByEmployeeId: string | null
previousDailyRate: number | null
nextDailyRate: number | null
previousWeeklyRate: number | null
nextWeeklyRate: number | null
note: string | null
effectiveFrom: string | null
createdAt: string
}
interface PricingPreviewItem {
date: string
dailyRate: number
pricingType: 'MANUAL_BASE' | 'AUTOMATIC_BASE' | 'WEEKEND_RATE' | 'RULE_RATE' | 'AUTOMATIC_RULE'
matchedRuleName: string | null
}
interface PricingResponse {
configuration: PricingConfiguration
rules: PricingRule[]
history: PricingHistoryEntry[]
preview: {
today: PricingPreviewItem
next14Days: PricingPreviewItem[]
}
}
interface ConfigForm {
pricingMode: 'MANUAL' | 'AUTOMATIC'
baseDailyRate: string
weeklyRate: string
weekendRate: string
holidayRate: string
monthlyRate: string
longTermDailyRate: string
minimumDailyRate: string
maximumDailyRate: string
maxDailyPriceMovementPct: string
automaticPricingEnabled: boolean
approvalRequired: boolean
}
interface RuleForm {
id?: string
name: string
ruleType: PricingRule['ruleType']
startDate: string
endDate: string
dailyRate: string
weeklyRate: string
weekendRate: string
monthlyRate: string
minDailyRate: string
maxDailyRate: string
automaticAdjustmentPct: string
priceAdjustment: string
isActive: boolean
sortOrder: string
}
function centsToInput(value: number | null | undefined) {
return value == null ? '' : (value / 100).toFixed(2)
}
function inputToCents(value: string) {
if (!value.trim()) return null
const parsed = Number.parseFloat(value)
if (!Number.isFinite(parsed)) return null
return Math.round(parsed * 100)
}
function numericInputToInt(value: string) {
if (!value.trim()) return null
const parsed = Number.parseInt(value, 10)
return Number.isFinite(parsed) ? parsed : null
}
function toDateInput(value: string) {
return value.slice(0, 10)
}
function buildConfigForm(configuration: PricingConfiguration): ConfigForm {
return {
pricingMode: configuration.pricingMode,
baseDailyRate: centsToInput(configuration.baseDailyRate),
weeklyRate: centsToInput(configuration.weeklyRate),
weekendRate: centsToInput(configuration.weekendRate),
holidayRate: centsToInput(configuration.holidayRate),
monthlyRate: centsToInput(configuration.monthlyRate),
longTermDailyRate: centsToInput(configuration.longTermDailyRate),
minimumDailyRate: centsToInput(configuration.minimumDailyRate),
maximumDailyRate: centsToInput(configuration.maximumDailyRate),
maxDailyPriceMovementPct: String(configuration.maxDailyPriceMovementPct),
automaticPricingEnabled: configuration.automaticPricingEnabled,
approvalRequired: configuration.approvalRequired,
}
}
function buildRuleForm(rule?: PricingRule): RuleForm {
const today = new Date().toISOString().slice(0, 10)
const nextWeek = new Date(Date.now() + 6 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10)
return {
id: rule?.id,
name: rule?.name ?? '',
ruleType: rule?.ruleType ?? 'DATE_RANGE',
startDate: rule ? toDateInput(rule.startDate) : today,
endDate: rule ? toDateInput(rule.endDate) : nextWeek,
dailyRate: centsToInput(rule?.dailyRate),
weeklyRate: centsToInput(rule?.weeklyRate),
weekendRate: centsToInput(rule?.weekendRate),
monthlyRate: centsToInput(rule?.monthlyRate),
minDailyRate: centsToInput(rule?.minDailyRate),
maxDailyRate: centsToInput(rule?.maxDailyRate),
automaticAdjustmentPct: rule?.automaticAdjustmentPct != null ? String(rule.automaticAdjustmentPct) : '',
priceAdjustment: centsToInput(rule?.priceAdjustment),
isActive: rule?.isActive ?? true,
sortOrder: rule?.sortOrder != null ? String(rule.sortOrder) : '0',
}
}
export default function VehiclePricingManager({ vehicleId }: { vehicleId: string }) {
const { language } = useDashboardI18n()
const copy = {
en: {
title: 'Pricing management',
subtitle: 'Manage fixed rates, automatic pricing bounds, seasonal rules, and pricing history for this vehicle.',
savePricing: 'Save pricing',
saving: 'Saving…',
loading: 'Loading pricing settings…',
mode: 'Pricing method',
manual: 'Manual fixed pricing',
automatic: 'Automatic dynamic pricing',
baseDailyRate: 'Base daily rate (MAD)',
weeklyRate: 'Weekly rate (MAD)',
weekendRate: 'Weekend rate (MAD)',
holidayRate: 'Holiday rate (MAD)',
monthlyRate: 'Monthly rate (MAD)',
longTermDailyRate: 'Long-term daily rate (MAD)',
minimumDailyRate: 'Minimum daily rate (MAD)',
maximumDailyRate: 'Maximum daily rate (MAD)',
automaticPricingEnabled: 'Automatic pricing enabled',
approvalRequired: 'Approve automatic price changes before they go live',
maxDailyMovement: 'Max daily price movement (%)',
currentPreview: 'Current pricing preview',
next14Days: 'Next 14 days',
matchedRule: 'Matched rule',
noRule: 'No rule',
rules: 'Seasonal and manual rules',
addRule: 'Add rule',
createRule: 'Create rule',
updateRule: 'Save rule',
deleting: 'Deleting…',
deleteRule: 'Delete',
noRules: 'No vehicle-specific pricing rules yet.',
history: 'Pricing history',
noHistory: 'No pricing changes recorded yet.',
active: 'Active',
inactive: 'Inactive',
startDate: 'Start date',
endDate: 'End date',
name: 'Rule name',
ruleType: 'Rule type',
adjustmentPct: 'Auto adjustment (%)',
priceAdjustment: 'Flat adjustment (MAD)',
sortOrder: 'Priority',
saveErrorBase: 'Base daily rate is required.',
saveErrorBounds: 'Minimum daily rate cannot be greater than maximum daily rate.',
saveErrorRule: 'Rules need a name plus at least one rate, bound, or adjustment.',
configSaved: 'Pricing settings saved.',
ruleSaved: 'Pricing rule saved.',
ruleCreated: 'Pricing rule created.',
ruleDeleted: 'Pricing rule deleted.',
previewLabels: {
MANUAL_BASE: 'Base manual price',
AUTOMATIC_BASE: 'Automatic base price',
WEEKEND_RATE: 'Weekend price',
RULE_RATE: 'Manual rule',
AUTOMATIC_RULE: 'Automatic rule',
} as Record<PricingPreviewItem['pricingType'], string>,
ruleTypeLabels: {
DATE_RANGE: 'Date range',
SEASONAL: 'Season',
HOLIDAY: 'Holiday',
WEEKEND: 'Weekend',
SPECIAL_EVENT: 'Special event',
MANUAL_OVERRIDE: 'Manual override',
} as Record<PricingRule['ruleType'], string>,
historyLabels: {
CONFIG_UPDATE: 'Configuration updated',
RULE_CREATED: 'Rule created',
RULE_UPDATED: 'Rule updated',
RULE_DELETED: 'Rule deleted',
MANUAL_OVERRIDE: 'Manual override',
} as Record<PricingHistoryEntry['source'], string>,
ruleHint: 'Use direct rates for fixed pricing or adjustments and bounds to guide automatic pricing.',
},
fr: {
title: 'Gestion des tarifs',
subtitle: 'Gérez les tarifs fixes, les limites automatiques, les saisons et lhistorique des prix de ce véhicule.',
savePricing: 'Enregistrer les tarifs',
saving: 'Enregistrement…',
loading: 'Chargement des paramètres tarifaires…',
mode: 'Méthode tarifaire',
manual: 'Tarification fixe manuelle',
automatic: 'Tarification dynamique automatique',
baseDailyRate: 'Tarif journalier de base (MAD)',
weeklyRate: 'Tarif hebdomadaire (MAD)',
weekendRate: 'Tarif week-end (MAD)',
holidayRate: 'Tarif jours fériés (MAD)',
monthlyRate: 'Tarif mensuel (MAD)',
longTermDailyRate: 'Tarif long séjour / jour (MAD)',
minimumDailyRate: 'Tarif journalier minimum (MAD)',
maximumDailyRate: 'Tarif journalier maximum (MAD)',
automaticPricingEnabled: 'Tarification automatique activée',
approvalRequired: 'Valider les changements automatiques avant publication',
maxDailyMovement: 'Variation maximale par jour (%)',
currentPreview: 'Aperçu des tarifs',
next14Days: '14 prochains jours',
matchedRule: 'Règle appliquée',
noRule: 'Aucune règle',
rules: 'Règles saisonnières et manuelles',
addRule: 'Ajouter une règle',
createRule: 'Créer la règle',
updateRule: 'Enregistrer la règle',
deleting: 'Suppression…',
deleteRule: 'Supprimer',
noRules: 'Aucune règle spécifique au véhicule pour le moment.',
history: 'Historique tarifaire',
noHistory: 'Aucun changement tarifaire enregistré.',
active: 'Active',
inactive: 'Inactive',
startDate: 'Date de début',
endDate: 'Date de fin',
name: 'Nom de la règle',
ruleType: 'Type de règle',
adjustmentPct: 'Ajustement auto (%)',
priceAdjustment: 'Ajustement fixe (MAD)',
sortOrder: 'Priorité',
saveErrorBase: 'Le tarif journalier de base est requis.',
saveErrorBounds: 'Le tarif minimum ne peut pas dépasser le maximum.',
saveErrorRule: 'Les règles nécessitent un nom et au moins un tarif, une limite ou un ajustement.',
configSaved: 'Paramètres tarifaires enregistrés.',
ruleSaved: 'Règle tarifaire enregistrée.',
ruleCreated: 'Règle tarifaire créée.',
ruleDeleted: 'Règle tarifaire supprimée.',
previewLabels: {
MANUAL_BASE: 'Tarif manuel de base',
AUTOMATIC_BASE: 'Base automatique',
WEEKEND_RATE: 'Tarif week-end',
RULE_RATE: 'Règle manuelle',
AUTOMATIC_RULE: 'Règle automatique',
} as Record<PricingPreviewItem['pricingType'], string>,
ruleTypeLabels: {
DATE_RANGE: 'Plage de dates',
SEASONAL: 'Saison',
HOLIDAY: 'Jour férié',
WEEKEND: 'Week-end',
SPECIAL_EVENT: 'Événement spécial',
MANUAL_OVERRIDE: 'Forçage manuel',
} as Record<PricingRule['ruleType'], string>,
historyLabels: {
CONFIG_UPDATE: 'Configuration mise à jour',
RULE_CREATED: 'Règle créée',
RULE_UPDATED: 'Règle mise à jour',
RULE_DELETED: 'Règle supprimée',
MANUAL_OVERRIDE: 'Forçage manuel',
} as Record<PricingHistoryEntry['source'], string>,
ruleHint: 'Utilisez des tarifs directs pour le fixe ou des ajustements et limites pour guider lautomatique.',
},
ar: {
title: 'إدارة التسعير',
subtitle: 'إدارة الأسعار الثابتة وحدود التسعير التلقائي والقواعد الموسمية وسجل تغييرات السعر لهذه السيارة.',
savePricing: 'حفظ التسعير',
saving: 'جارٍ الحفظ…',
loading: 'جارٍ تحميل إعدادات التسعير…',
mode: 'طريقة التسعير',
manual: 'تسعير ثابت يدوي',
automatic: 'تسعير ديناميكي تلقائي',
baseDailyRate: 'السعر اليومي الأساسي (درهم)',
weeklyRate: 'السعر الأسبوعي (درهم)',
weekendRate: 'سعر عطلة نهاية الأسبوع (درهم)',
holidayRate: 'سعر العطل (درهم)',
monthlyRate: 'السعر الشهري (درهم)',
longTermDailyRate: 'سعر الإيجار الطويل / يوم (درهم)',
minimumDailyRate: 'الحد الأدنى للسعر اليومي (درهم)',
maximumDailyRate: 'الحد الأقصى للسعر اليومي (درهم)',
automaticPricingEnabled: 'تفعيل التسعير التلقائي',
approvalRequired: 'اعتماد تغييرات السعر التلقائية قبل نشرها',
maxDailyMovement: 'أقصى تغيير يومي (%)',
currentPreview: 'معاينة التسعير الحالية',
next14Days: 'الأيام الـ 14 القادمة',
matchedRule: 'القاعدة المطبقة',
noRule: 'لا توجد قاعدة',
rules: 'القواعد الموسمية واليدوية',
addRule: 'إضافة قاعدة',
createRule: 'إنشاء القاعدة',
updateRule: 'حفظ القاعدة',
deleting: 'جارٍ الحذف…',
deleteRule: 'حذف',
noRules: 'لا توجد قواعد تسعير خاصة بهذه السيارة بعد.',
history: 'سجل التسعير',
noHistory: 'لا توجد تغييرات تسعير مسجلة بعد.',
active: 'نشطة',
inactive: 'غير نشطة',
startDate: 'تاريخ البدء',
endDate: 'تاريخ الانتهاء',
name: 'اسم القاعدة',
ruleType: 'نوع القاعدة',
adjustmentPct: 'تعديل تلقائي (%)',
priceAdjustment: 'تعديل ثابت (درهم)',
sortOrder: 'الأولوية',
saveErrorBase: 'السعر اليومي الأساسي مطلوب.',
saveErrorBounds: 'لا يمكن أن يكون الحد الأدنى أعلى من الحد الأقصى.',
saveErrorRule: 'تحتاج القواعد إلى اسم وإلى سعر أو حد أو تعديل واحد على الأقل.',
configSaved: 'تم حفظ إعدادات التسعير.',
ruleSaved: 'تم حفظ قاعدة التسعير.',
ruleCreated: 'تم إنشاء قاعدة التسعير.',
ruleDeleted: 'تم حذف قاعدة التسعير.',
previewLabels: {
MANUAL_BASE: 'السعر اليدوي الأساسي',
AUTOMATIC_BASE: 'السعر الأساسي التلقائي',
WEEKEND_RATE: 'سعر عطلة نهاية الأسبوع',
RULE_RATE: 'قاعدة يدوية',
AUTOMATIC_RULE: 'قاعدة تلقائية',
} as Record<PricingPreviewItem['pricingType'], string>,
ruleTypeLabels: {
DATE_RANGE: 'نطاق تاريخ',
SEASONAL: 'موسم',
HOLIDAY: 'عطلة',
WEEKEND: 'نهاية أسبوع',
SPECIAL_EVENT: 'مناسبة خاصة',
MANUAL_OVERRIDE: 'تجاوز يدوي',
} as Record<PricingRule['ruleType'], string>,
historyLabels: {
CONFIG_UPDATE: 'تم تحديث الإعدادات',
RULE_CREATED: 'تم إنشاء قاعدة',
RULE_UPDATED: 'تم تحديث قاعدة',
RULE_DELETED: 'تم حذف قاعدة',
MANUAL_OVERRIDE: 'تجاوز يدوي',
} as Record<PricingHistoryEntry['source'], string>,
ruleHint: 'استخدم أسعاراً مباشرة للتسعير الثابت أو تعديلات وحدوداً لتوجيه التسعير التلقائي.',
},
}[language]
const [loading, setLoading] = useState(true)
const [savingConfig, setSavingConfig] = useState(false)
const [savingRuleId, setSavingRuleId] = useState<string | null>(null)
const [deletingRuleId, setDeletingRuleId] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
const [success, setSuccess] = useState<string | null>(null)
const [configForm, setConfigForm] = useState<ConfigForm | null>(null)
const [rules, setRules] = useState<RuleForm[]>([])
const [newRule, setNewRule] = useState<RuleForm>(buildRuleForm())
const [history, setHistory] = useState<PricingHistoryEntry[]>([])
const [preview, setPreview] = useState<PricingResponse['preview'] | null>(null)
function applyResponse(data: PricingResponse) {
setConfigForm(buildConfigForm(data.configuration))
setRules(data.rules.map((rule) => buildRuleForm(rule)))
setHistory(data.history)
setPreview(data.preview)
}
async function load() {
setLoading(true)
try {
const data = await apiFetch<PricingResponse>(`/vehicles/${vehicleId}/pricing`)
applyResponse(data)
setError(null)
} catch (err: any) {
setError(err.message ?? 'Failed to load vehicle pricing')
} finally {
setLoading(false)
}
}
useEffect(() => {
load()
}, [vehicleId])
async function saveConfiguration() {
if (!configForm) return
const baseDailyRate = inputToCents(configForm.baseDailyRate)
const minimumDailyRate = inputToCents(configForm.minimumDailyRate)
const maximumDailyRate = inputToCents(configForm.maximumDailyRate)
if (baseDailyRate == null) {
setError(copy.saveErrorBase)
return
}
if (minimumDailyRate != null && maximumDailyRate != null && minimumDailyRate > maximumDailyRate) {
setError(copy.saveErrorBounds)
return
}
setSavingConfig(true)
setError(null)
setSuccess(null)
try {
const data = await apiFetch<PricingResponse>(`/vehicles/${vehicleId}/pricing`, {
method: 'PUT',
body: JSON.stringify({
pricingMode: configForm.pricingMode,
baseDailyRate,
weeklyRate: inputToCents(configForm.weeklyRate),
weekendRate: inputToCents(configForm.weekendRate),
holidayRate: inputToCents(configForm.holidayRate),
monthlyRate: inputToCents(configForm.monthlyRate),
longTermDailyRate: inputToCents(configForm.longTermDailyRate),
minimumDailyRate,
maximumDailyRate,
maxDailyPriceMovementPct: numericInputToInt(configForm.maxDailyPriceMovementPct) ?? 10,
automaticPricingEnabled: configForm.automaticPricingEnabled,
approvalRequired: configForm.approvalRequired,
note: copy.configSaved,
}),
})
applyResponse(data)
setSuccess(copy.configSaved)
} catch (err: any) {
setError(err.message ?? copy.saveErrorBase)
} finally {
setSavingConfig(false)
}
}
async function saveRule(rule: RuleForm, create = false) {
const hasPricingEffect = [
inputToCents(rule.dailyRate),
inputToCents(rule.weeklyRate),
inputToCents(rule.weekendRate),
inputToCents(rule.monthlyRate),
inputToCents(rule.minDailyRate),
inputToCents(rule.maxDailyRate),
numericInputToInt(rule.automaticAdjustmentPct),
inputToCents(rule.priceAdjustment),
].some((value) => value != null)
if (!rule.name.trim() || !hasPricingEffect) {
setError(copy.saveErrorRule)
return
}
const payload = {
name: rule.name.trim(),
ruleType: rule.ruleType,
startDate: rule.startDate,
endDate: rule.endDate,
dailyRate: inputToCents(rule.dailyRate),
weeklyRate: inputToCents(rule.weeklyRate),
weekendRate: inputToCents(rule.weekendRate),
monthlyRate: inputToCents(rule.monthlyRate),
minDailyRate: inputToCents(rule.minDailyRate),
maxDailyRate: inputToCents(rule.maxDailyRate),
automaticAdjustmentPct: numericInputToInt(rule.automaticAdjustmentPct),
priceAdjustment: inputToCents(rule.priceAdjustment),
isActive: rule.isActive,
sortOrder: numericInputToInt(rule.sortOrder) ?? 0,
}
setSavingRuleId(rule.id ?? 'new')
setError(null)
setSuccess(null)
try {
const data = create
? await apiFetch<PricingResponse>(`/vehicles/${vehicleId}/pricing/rules`, { method: 'POST', body: JSON.stringify(payload) })
: await apiFetch<PricingResponse>(`/vehicles/${vehicleId}/pricing/rules/${rule.id}`, { method: 'PATCH', body: JSON.stringify(payload) })
applyResponse(data)
if (create) {
setNewRule(buildRuleForm())
setSuccess(copy.ruleCreated)
} else {
setSuccess(copy.ruleSaved)
}
} catch (err: any) {
setError(err.message ?? copy.saveErrorRule)
} finally {
setSavingRuleId(null)
}
}
async function deleteRule(ruleId: string) {
setDeletingRuleId(ruleId)
setError(null)
setSuccess(null)
try {
const data = await apiFetch<PricingResponse>(`/vehicles/${vehicleId}/pricing/rules/${ruleId}`, { method: 'DELETE' })
applyResponse(data)
setSuccess(copy.ruleDeleted)
} catch (err: any) {
setError(err.message ?? copy.ruleDeleted)
} finally {
setDeletingRuleId(null)
}
}
if (loading || !configForm || !preview) {
return <div className="card p-6 text-sm text-slate-500">{copy.loading}</div>
}
return (
<div className="space-y-6">
<div className="card p-6">
<div className="flex flex-wrap items-start justify-between gap-4">
<div>
<h3 className="text-base font-semibold text-slate-900">{copy.title}</h3>
<p className="mt-1 max-w-3xl text-sm text-slate-500">{copy.subtitle}</p>
</div>
<button onClick={saveConfiguration} disabled={savingConfig} className="btn-primary">
<Save className="h-4 w-4" />
{savingConfig ? copy.saving : copy.savePricing}
</button>
</div>
{error && <div className="mt-4 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{error}</div>}
{success && <div className="mt-4 rounded-xl border border-green-200 bg-green-50 px-4 py-3 text-sm text-green-700">{success}</div>}
<div className="mt-6 grid gap-6 xl:grid-cols-[1.2fr_0.8fr]">
<div className="grid gap-4 rounded-2xl border border-slate-200 bg-slate-50 p-4 md:grid-cols-2">
<div className="md:col-span-2">
<label className="mb-1.5 block text-sm font-medium text-slate-700">{copy.mode}</label>
<select className="input-field" value={configForm.pricingMode} onChange={(event) => setConfigForm((current) => current ? { ...current, pricingMode: event.target.value as ConfigForm['pricingMode'] } : current)}>
<option value="MANUAL">{copy.manual}</option>
<option value="AUTOMATIC">{copy.automatic}</option>
</select>
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{copy.baseDailyRate}</label>
<input className="input-field" type="number" min="0" step="0.01" value={configForm.baseDailyRate} onChange={(event) => setConfigForm((current) => current ? { ...current, baseDailyRate: event.target.value } : current)} />
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{copy.weeklyRate}</label>
<input className="input-field" type="number" min="0" step="0.01" value={configForm.weeklyRate} onChange={(event) => setConfigForm((current) => current ? { ...current, weeklyRate: event.target.value } : current)} />
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{copy.weekendRate}</label>
<input className="input-field" type="number" min="0" step="0.01" value={configForm.weekendRate} onChange={(event) => setConfigForm((current) => current ? { ...current, weekendRate: event.target.value } : current)} />
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{copy.holidayRate}</label>
<input className="input-field" type="number" min="0" step="0.01" value={configForm.holidayRate} onChange={(event) => setConfigForm((current) => current ? { ...current, holidayRate: event.target.value } : current)} />
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{copy.monthlyRate}</label>
<input className="input-field" type="number" min="0" step="0.01" value={configForm.monthlyRate} onChange={(event) => setConfigForm((current) => current ? { ...current, monthlyRate: event.target.value } : current)} />
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{copy.longTermDailyRate}</label>
<input className="input-field" type="number" min="0" step="0.01" value={configForm.longTermDailyRate} onChange={(event) => setConfigForm((current) => current ? { ...current, longTermDailyRate: event.target.value } : current)} />
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{copy.minimumDailyRate}</label>
<input className="input-field" type="number" min="0" step="0.01" value={configForm.minimumDailyRate} onChange={(event) => setConfigForm((current) => current ? { ...current, minimumDailyRate: event.target.value } : current)} />
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{copy.maximumDailyRate}</label>
<input className="input-field" type="number" min="0" step="0.01" value={configForm.maximumDailyRate} onChange={(event) => setConfigForm((current) => current ? { ...current, maximumDailyRate: event.target.value } : current)} />
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{copy.maxDailyMovement}</label>
<input className="input-field" type="number" min="0" max="100" step="1" value={configForm.maxDailyPriceMovementPct} onChange={(event) => setConfigForm((current) => current ? { ...current, maxDailyPriceMovementPct: event.target.value } : current)} />
</div>
<div className="md:col-span-2 flex flex-col gap-3 rounded-2xl border border-slate-200 bg-white p-4">
<label className="flex items-center gap-3 text-sm font-medium text-slate-700">
<input type="checkbox" checked={configForm.automaticPricingEnabled} onChange={(event) => setConfigForm((current) => current ? { ...current, automaticPricingEnabled: event.target.checked } : current)} />
{copy.automaticPricingEnabled}
</label>
<label className="flex items-center gap-3 text-sm font-medium text-slate-700">
<input type="checkbox" checked={configForm.approvalRequired} onChange={(event) => setConfigForm((current) => current ? { ...current, approvalRequired: event.target.checked } : current)} />
{copy.approvalRequired}
</label>
</div>
</div>
<div className="rounded-2xl border border-slate-200 bg-white p-5">
<div className="flex items-center gap-2">
<Sparkles className="h-4 w-4 text-blue-600" />
<h4 className="text-sm font-semibold text-slate-900">{copy.currentPreview}</h4>
</div>
<div className="mt-4 rounded-2xl bg-slate-50 p-4">
<p className="text-xs uppercase tracking-wide text-slate-500">{preview.today.date}</p>
<p className="mt-2 text-2xl font-semibold text-slate-900">{formatCurrency(preview.today.dailyRate, 'MAD')}</p>
<p className="mt-1 text-sm text-slate-500">{copy.previewLabels[preview.today.pricingType]}</p>
<p className="mt-2 text-xs text-slate-400">{copy.matchedRule}: {preview.today.matchedRuleName ?? copy.noRule}</p>
</div>
<div className="mt-4">
<div className="mb-3 flex items-center gap-2">
<CalendarRange className="h-4 w-4 text-slate-400" />
<p className="text-sm font-semibold text-slate-900">{copy.next14Days}</p>
</div>
<div className="space-y-2">
{preview.next14Days.map((item) => (
<div key={item.date} className="flex items-center justify-between rounded-xl border border-slate-100 px-3 py-2 text-sm">
<div>
<p className="font-medium text-slate-900">{item.date}</p>
<p className="text-xs text-slate-500">{copy.previewLabels[item.pricingType]}</p>
</div>
<div className="text-right">
<p className="font-semibold text-slate-900">{formatCurrency(item.dailyRate, 'MAD')}</p>
<p className="text-xs text-slate-400">{item.matchedRuleName ?? copy.noRule}</p>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</div>
<div className="card p-6">
<h3 className="text-base font-semibold text-slate-900">{copy.rules}</h3>
<p className="mt-1 text-sm text-slate-500">{copy.ruleHint}</p>
<div className="mt-5 space-y-4">
{rules.map((rule, index) => (
<div key={rule.id} className="rounded-2xl border border-slate-200 bg-slate-50 p-4">
<div className="grid gap-4 lg:grid-cols-2">
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{copy.name}</label>
<input className="input-field" value={rule.name} onChange={(event) => setRules((current) => current.map((entry, entryIndex) => entryIndex === index ? { ...entry, name: event.target.value } : entry))} />
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{copy.ruleType}</label>
<select className="input-field" value={rule.ruleType} onChange={(event) => setRules((current) => current.map((entry, entryIndex) => entryIndex === index ? { ...entry, ruleType: event.target.value as RuleForm['ruleType'] } : entry))}>
{Object.entries(copy.ruleTypeLabels).map(([value, label]) => (
<option key={value} value={value}>{label}</option>
))}
</select>
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{copy.startDate}</label>
<input className="input-field" type="date" value={rule.startDate} onChange={(event) => setRules((current) => current.map((entry, entryIndex) => entryIndex === index ? { ...entry, startDate: event.target.value } : entry))} />
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{copy.endDate}</label>
<input className="input-field" type="date" value={rule.endDate} onChange={(event) => setRules((current) => current.map((entry, entryIndex) => entryIndex === index ? { ...entry, endDate: event.target.value } : entry))} />
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{copy.baseDailyRate}</label>
<input className="input-field" type="number" min="0" step="0.01" value={rule.dailyRate} onChange={(event) => setRules((current) => current.map((entry, entryIndex) => entryIndex === index ? { ...entry, dailyRate: event.target.value } : entry))} />
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{copy.weeklyRate}</label>
<input className="input-field" type="number" min="0" step="0.01" value={rule.weeklyRate} onChange={(event) => setRules((current) => current.map((entry, entryIndex) => entryIndex === index ? { ...entry, weeklyRate: event.target.value } : entry))} />
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{copy.weekendRate}</label>
<input className="input-field" type="number" min="0" step="0.01" value={rule.weekendRate} onChange={(event) => setRules((current) => current.map((entry, entryIndex) => entryIndex === index ? { ...entry, weekendRate: event.target.value } : entry))} />
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{copy.monthlyRate}</label>
<input className="input-field" type="number" min="0" step="0.01" value={rule.monthlyRate} onChange={(event) => setRules((current) => current.map((entry, entryIndex) => entryIndex === index ? { ...entry, monthlyRate: event.target.value } : entry))} />
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{copy.minimumDailyRate}</label>
<input className="input-field" type="number" min="0" step="0.01" value={rule.minDailyRate} onChange={(event) => setRules((current) => current.map((entry, entryIndex) => entryIndex === index ? { ...entry, minDailyRate: event.target.value } : entry))} />
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{copy.maximumDailyRate}</label>
<input className="input-field" type="number" min="0" step="0.01" value={rule.maxDailyRate} onChange={(event) => setRules((current) => current.map((entry, entryIndex) => entryIndex === index ? { ...entry, maxDailyRate: event.target.value } : entry))} />
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{copy.adjustmentPct}</label>
<input className="input-field" type="number" step="1" value={rule.automaticAdjustmentPct} onChange={(event) => setRules((current) => current.map((entry, entryIndex) => entryIndex === index ? { ...entry, automaticAdjustmentPct: event.target.value } : entry))} />
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{copy.priceAdjustment}</label>
<input className="input-field" type="number" step="0.01" value={rule.priceAdjustment} onChange={(event) => setRules((current) => current.map((entry, entryIndex) => entryIndex === index ? { ...entry, priceAdjustment: event.target.value } : entry))} />
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{copy.sortOrder}</label>
<input className="input-field" type="number" min="0" step="1" value={rule.sortOrder} onChange={(event) => setRules((current) => current.map((entry, entryIndex) => entryIndex === index ? { ...entry, sortOrder: event.target.value } : entry))} />
</div>
<div className="flex items-end">
<label className="flex items-center gap-3 text-sm font-medium text-slate-700">
<input type="checkbox" checked={rule.isActive} onChange={(event) => setRules((current) => current.map((entry, entryIndex) => entryIndex === index ? { ...entry, isActive: event.target.checked } : entry))} />
{rule.isActive ? copy.active : copy.inactive}
</label>
</div>
</div>
<div className="mt-4 flex flex-wrap items-center justify-end gap-2">
<button onClick={() => deleteRule(rule.id!)} disabled={deletingRuleId === rule.id} className="btn-secondary text-red-600">
<Trash2 className="h-4 w-4" />
{deletingRuleId === rule.id ? copy.deleting : copy.deleteRule}
</button>
<button onClick={() => saveRule(rule)} disabled={savingRuleId === rule.id} className="btn-primary">
<Save className="h-4 w-4" />
{savingRuleId === rule.id ? copy.saving : copy.updateRule}
</button>
</div>
</div>
))}
{rules.length === 0 && <div className="rounded-2xl border border-dashed border-slate-200 px-4 py-6 text-sm text-slate-400">{copy.noRules}</div>}
</div>
<div className="mt-6 rounded-2xl border border-blue-100 bg-blue-50/60 p-4">
<div className="mb-4 flex items-center gap-2">
<PlusCircle className="h-4 w-4 text-blue-600" />
<h4 className="text-sm font-semibold text-slate-900">{copy.addRule}</h4>
</div>
<div className="grid gap-4 lg:grid-cols-2">
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{copy.name}</label>
<input className="input-field" value={newRule.name} onChange={(event) => setNewRule((current) => ({ ...current, name: event.target.value }))} />
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{copy.ruleType}</label>
<select className="input-field" value={newRule.ruleType} onChange={(event) => setNewRule((current) => ({ ...current, ruleType: event.target.value as RuleForm['ruleType'] }))}>
{Object.entries(copy.ruleTypeLabels).map(([value, label]) => (
<option key={value} value={value}>{label}</option>
))}
</select>
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{copy.startDate}</label>
<input className="input-field" type="date" value={newRule.startDate} onChange={(event) => setNewRule((current) => ({ ...current, startDate: event.target.value }))} />
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{copy.endDate}</label>
<input className="input-field" type="date" value={newRule.endDate} onChange={(event) => setNewRule((current) => ({ ...current, endDate: event.target.value }))} />
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{copy.baseDailyRate}</label>
<input className="input-field" type="number" min="0" step="0.01" value={newRule.dailyRate} onChange={(event) => setNewRule((current) => ({ ...current, dailyRate: event.target.value }))} />
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{copy.weeklyRate}</label>
<input className="input-field" type="number" min="0" step="0.01" value={newRule.weeklyRate} onChange={(event) => setNewRule((current) => ({ ...current, weeklyRate: event.target.value }))} />
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{copy.weekendRate}</label>
<input className="input-field" type="number" min="0" step="0.01" value={newRule.weekendRate} onChange={(event) => setNewRule((current) => ({ ...current, weekendRate: event.target.value }))} />
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{copy.monthlyRate}</label>
<input className="input-field" type="number" min="0" step="0.01" value={newRule.monthlyRate} onChange={(event) => setNewRule((current) => ({ ...current, monthlyRate: event.target.value }))} />
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{copy.minimumDailyRate}</label>
<input className="input-field" type="number" min="0" step="0.01" value={newRule.minDailyRate} onChange={(event) => setNewRule((current) => ({ ...current, minDailyRate: event.target.value }))} />
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{copy.maximumDailyRate}</label>
<input className="input-field" type="number" min="0" step="0.01" value={newRule.maxDailyRate} onChange={(event) => setNewRule((current) => ({ ...current, maxDailyRate: event.target.value }))} />
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{copy.adjustmentPct}</label>
<input className="input-field" type="number" step="1" value={newRule.automaticAdjustmentPct} onChange={(event) => setNewRule((current) => ({ ...current, automaticAdjustmentPct: event.target.value }))} />
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{copy.priceAdjustment}</label>
<input className="input-field" type="number" step="0.01" value={newRule.priceAdjustment} onChange={(event) => setNewRule((current) => ({ ...current, priceAdjustment: event.target.value }))} />
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{copy.sortOrder}</label>
<input className="input-field" type="number" min="0" step="1" value={newRule.sortOrder} onChange={(event) => setNewRule((current) => ({ ...current, sortOrder: event.target.value }))} />
</div>
</div>
<div className="mt-4 flex justify-end">
<button onClick={() => saveRule(newRule, true)} disabled={savingRuleId === 'new'} className="btn-primary">
<PlusCircle className="h-4 w-4" />
{savingRuleId === 'new' ? copy.saving : copy.createRule}
</button>
</div>
</div>
</div>
<div className="card p-6">
<div className="flex items-center gap-2">
<History className="h-4 w-4 text-slate-400" />
<h3 className="text-base font-semibold text-slate-900">{copy.history}</h3>
</div>
<div className="mt-5 space-y-3">
{history.map((entry) => (
<div key={entry.id} className="rounded-2xl border border-slate-200 px-4 py-3 text-sm">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<p className="font-medium text-slate-900">{copy.historyLabels[entry.source]}</p>
<p className="text-slate-500">{new Date(entry.createdAt).toLocaleString()}</p>
</div>
<div className="text-right">
{entry.nextDailyRate != null && <p className="font-semibold text-slate-900">{formatCurrency(entry.nextDailyRate, 'MAD')}</p>}
{entry.previousDailyRate != null && entry.nextDailyRate != null && (
<p className="text-xs text-slate-400">{formatCurrency(entry.previousDailyRate, 'MAD')} {formatCurrency(entry.nextDailyRate, 'MAD')}</p>
)}
</div>
</div>
{entry.note && <p className="mt-2 text-sm text-slate-600">{entry.note}</p>}
</div>
))}
{history.length === 0 && <div className="rounded-2xl border border-dashed border-slate-200 px-4 py-6 text-sm text-slate-400">{copy.noHistory}</div>}
</div>
</div>
</div>
)
}