add pricing feature
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user