fixing platform admin
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { apiFetch } from '@/lib/api'
|
||||
|
||||
type InspectionType = 'CHECKIN' | 'CHECKOUT'
|
||||
type FuelLevel = 'FULL' | 'SEVEN_EIGHTHS' | 'THREE_QUARTERS' | 'FIVE_EIGHTHS' | 'HALF' | 'THREE_EIGHTHS' | 'QUARTER' | 'ONE_EIGHTH' | 'EMPTY'
|
||||
type DamageType = 'SCRATCH' | 'DENT' | 'CRACK' | 'CHIP' | 'MISSING' | 'STAIN' | 'OTHER'
|
||||
type DamageSeverity = 'MINOR' | 'MODERATE' | 'MAJOR'
|
||||
|
||||
export interface DamagePoint {
|
||||
id?: string
|
||||
viewType: 'TOP' | 'FRONT' | 'REAR' | 'LEFT' | 'RIGHT'
|
||||
x: number
|
||||
y: number
|
||||
damageType: DamageType
|
||||
severity: DamageSeverity
|
||||
description?: string | null
|
||||
isPreExisting: boolean
|
||||
}
|
||||
|
||||
export interface DamageInspection {
|
||||
id: string
|
||||
type: InspectionType
|
||||
mileage: number | null
|
||||
fuelLevel: FuelLevel
|
||||
fuelCharge: number | null
|
||||
generalCondition: string | null
|
||||
employeeNotes: string | null
|
||||
customerAgreed: boolean
|
||||
damagePoints: DamagePoint[]
|
||||
}
|
||||
|
||||
const fuelLevels: FuelLevel[] = ['FULL', 'SEVEN_EIGHTHS', 'THREE_QUARTERS', 'FIVE_EIGHTHS', 'HALF', 'THREE_EIGHTHS', 'QUARTER', 'ONE_EIGHTH', 'EMPTY']
|
||||
const damageTypes: DamageType[] = ['SCRATCH', 'DENT', 'CRACK', 'CHIP', 'MISSING', 'STAIN', 'OTHER']
|
||||
const severityColors: Record<DamageSeverity, string> = {
|
||||
MINOR: '#f59e0b',
|
||||
MODERATE: '#f97316',
|
||||
MAJOR: '#dc2626',
|
||||
}
|
||||
|
||||
export default function DamageInspectionCard({
|
||||
reservationId,
|
||||
type,
|
||||
initialInspection,
|
||||
onSaved,
|
||||
}: {
|
||||
reservationId: string
|
||||
type: InspectionType
|
||||
initialInspection?: DamageInspection | null
|
||||
onSaved: (inspection: DamageInspection) => void
|
||||
}) {
|
||||
const [inspection, setInspection] = useState<DamageInspection>({
|
||||
id: initialInspection?.id ?? `${type.toLowerCase()}-draft`,
|
||||
type,
|
||||
mileage: initialInspection?.mileage ?? null,
|
||||
fuelLevel: initialInspection?.fuelLevel ?? 'FULL',
|
||||
fuelCharge: initialInspection?.fuelCharge ?? null,
|
||||
generalCondition: initialInspection?.generalCondition ?? '',
|
||||
employeeNotes: initialInspection?.employeeNotes ?? '',
|
||||
customerAgreed: initialInspection?.customerAgreed ?? false,
|
||||
damagePoints: initialInspection?.damagePoints ?? [],
|
||||
})
|
||||
const [damageType, setDamageType] = useState<DamageType>('SCRATCH')
|
||||
const [severity, setSeverity] = useState<DamageSeverity>('MINOR')
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
async function saveInspection() {
|
||||
setSaving(true)
|
||||
setError(null)
|
||||
try {
|
||||
const result = await apiFetch<DamageInspection>(`/reservations/${reservationId}/inspections/${type.toLowerCase()}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
mileage: inspection.mileage,
|
||||
fuelLevel: inspection.fuelLevel,
|
||||
fuelCharge: inspection.fuelCharge,
|
||||
generalCondition: inspection.generalCondition,
|
||||
employeeNotes: inspection.employeeNotes,
|
||||
customerAgreed: inspection.customerAgreed,
|
||||
damagePoints: inspection.damagePoints.map(({ viewType, x, y, damageType, severity, description, isPreExisting }) => ({
|
||||
viewType,
|
||||
x,
|
||||
y,
|
||||
damageType,
|
||||
severity,
|
||||
description,
|
||||
isPreExisting,
|
||||
})),
|
||||
}),
|
||||
})
|
||||
setInspection(result)
|
||||
onSaved(result)
|
||||
} catch (err: any) {
|
||||
setError(err.message ?? 'Failed to save inspection')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
function addDamagePoint(event: React.MouseEvent<SVGSVGElement>) {
|
||||
const rect = event.currentTarget.getBoundingClientRect()
|
||||
const x = ((event.clientX - rect.left) / rect.width) * 100
|
||||
const y = ((event.clientY - rect.top) / rect.height) * 180
|
||||
setInspection((current) => ({
|
||||
...current,
|
||||
damagePoints: [
|
||||
...current.damagePoints,
|
||||
{
|
||||
viewType: 'TOP',
|
||||
x,
|
||||
y,
|
||||
damageType,
|
||||
severity,
|
||||
description: '',
|
||||
isPreExisting: type === 'CHECKIN',
|
||||
},
|
||||
],
|
||||
}))
|
||||
}
|
||||
|
||||
function removePoint(index: number) {
|
||||
setInspection((current) => ({
|
||||
...current,
|
||||
damagePoints: current.damagePoints.filter((_, pointIndex) => pointIndex !== index),
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card p-6">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-slate-900">{type === 'CHECKIN' ? 'Check-in inspection' : 'Check-out inspection'}</h3>
|
||||
<p className="mt-1 text-sm text-slate-500">Mark existing and newly discovered damage directly on the vehicle diagram.</p>
|
||||
</div>
|
||||
<button onClick={saveInspection} disabled={saving} className="btn-primary">
|
||||
{saving ? 'Saving…' : 'Save inspection'}
|
||||
</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>}
|
||||
|
||||
<div className="mt-5 grid gap-6 xl:grid-cols-[1.1fr_0.9fr]">
|
||||
<div className="rounded-2xl border border-slate-200 bg-slate-50 p-4">
|
||||
<div className="mb-3 flex flex-wrap items-center gap-2 text-xs">
|
||||
{damageTypes.map((option) => (
|
||||
<button
|
||||
key={option}
|
||||
type="button"
|
||||
onClick={() => setDamageType(option)}
|
||||
className={`rounded-full px-3 py-1.5 font-semibold ${damageType === option ? 'bg-slate-900 text-white' : 'bg-white text-slate-600'}`}
|
||||
>
|
||||
{option}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="mb-4 flex flex-wrap items-center gap-2 text-xs">
|
||||
{(Object.keys(severityColors) as DamageSeverity[]).map((option) => (
|
||||
<button
|
||||
key={option}
|
||||
type="button"
|
||||
onClick={() => setSeverity(option)}
|
||||
className="rounded-full px-3 py-1.5 font-semibold text-white"
|
||||
style={{ backgroundColor: severityColors[option], opacity: severity === option ? 1 : 0.55 }}
|
||||
>
|
||||
{option}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<svg viewBox="0 0 100 180" className="w-full cursor-crosshair rounded-2xl border border-slate-200 bg-white" onClick={addDamagePoint}>
|
||||
<rect x="30" y="12" width="40" height="156" rx="18" fill="#e2e8f0" stroke="#94a3b8" />
|
||||
<rect x="36" y="22" width="28" height="24" rx="8" fill="#cbd5e1" />
|
||||
<rect x="36" y="54" width="28" height="70" rx="8" fill="#f8fafc" stroke="#cbd5e1" />
|
||||
<rect x="36" y="132" width="28" height="20" rx="8" fill="#cbd5e1" />
|
||||
<rect x="18" y="38" width="12" height="28" rx="5" fill="#1e293b" />
|
||||
<rect x="70" y="38" width="12" height="28" rx="5" fill="#1e293b" />
|
||||
<rect x="18" y="114" width="12" height="28" rx="5" fill="#1e293b" />
|
||||
<rect x="70" y="114" width="12" height="28" rx="5" fill="#1e293b" />
|
||||
{inspection.damagePoints.map((point, index) => (
|
||||
<g key={`${point.x}-${point.y}-${index}`}>
|
||||
<circle cx={point.x} cy={point.y} r="3.8" fill={severityColors[point.severity]} stroke="white" strokeWidth="1.5" />
|
||||
</g>
|
||||
))}
|
||||
</svg>
|
||||
<p className="mt-2 text-xs text-slate-500">Click anywhere on the diagram to add a damage marker with the selected type and severity.</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label className="mb-1.5 block text-sm font-medium text-slate-700">Mileage</label>
|
||||
<input
|
||||
type="number"
|
||||
className="input-field"
|
||||
value={inspection.mileage ?? ''}
|
||||
onChange={(event) => setInspection((current) => ({ ...current, mileage: event.target.value ? Number(event.target.value) : null }))}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1.5 block text-sm font-medium text-slate-700">Fuel level</label>
|
||||
<select className="input-field" value={inspection.fuelLevel} onChange={(event) => setInspection((current) => ({ ...current, fuelLevel: event.target.value as FuelLevel }))}>
|
||||
{fuelLevels.map((fuelLevel) => <option key={fuelLevel} value={fuelLevel}>{fuelLevel}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1.5 block text-sm font-medium text-slate-700">General condition</label>
|
||||
<textarea className="input-field min-h-24" value={inspection.generalCondition ?? ''} onChange={(event) => setInspection((current) => ({ ...current, generalCondition: event.target.value }))} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1.5 block text-sm font-medium text-slate-700">Employee notes</label>
|
||||
<textarea className="input-field min-h-24" value={inspection.employeeNotes ?? ''} onChange={(event) => setInspection((current) => ({ ...current, employeeNotes: event.target.value }))} />
|
||||
</div>
|
||||
|
||||
<label className="flex items-center gap-3 text-sm font-medium text-slate-700">
|
||||
<input type="checkbox" checked={inspection.customerAgreed} onChange={(event) => setInspection((current) => ({ ...current, customerAgreed: event.target.checked }))} />
|
||||
Customer acknowledged this inspection
|
||||
</label>
|
||||
|
||||
<div className="rounded-2xl border border-slate-200">
|
||||
<div className="border-b border-slate-200 px-4 py-3">
|
||||
<p className="text-sm font-semibold text-slate-900">Damage markers</p>
|
||||
</div>
|
||||
<div className="divide-y divide-slate-100">
|
||||
{inspection.damagePoints.map((point, index) => (
|
||||
<div key={`${point.x}-${point.y}-${index}`} className="flex items-center justify-between gap-3 px-4 py-3 text-sm">
|
||||
<div>
|
||||
<p className="font-medium text-slate-900">{point.damageType} · {point.severity}</p>
|
||||
<p className="text-xs text-slate-500">x {point.x.toFixed(1)} · y {point.y.toFixed(1)}</p>
|
||||
</div>
|
||||
<button type="button" onClick={() => removePoint(index)} className="rounded-full border border-slate-300 px-3 py-1 text-xs font-semibold text-slate-600">
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
{inspection.damagePoints.length === 0 && (
|
||||
<div className="px-4 py-6 text-sm text-slate-400">No damage markers saved yet.</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user