465 lines
20 KiB
TypeScript
465 lines
20 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { useParams, useRouter } from 'next/navigation';
|
|
import Link from 'next/link';
|
|
import Image from 'next/image';
|
|
|
|
interface JobAction {
|
|
id: number;
|
|
action_type: string;
|
|
part_type: string | null;
|
|
part_id: string | null;
|
|
custom_price: number;
|
|
labour_fee: number;
|
|
created_at: string;
|
|
}
|
|
|
|
interface Vehicle {
|
|
id: string;
|
|
make: string;
|
|
model: string;
|
|
capacity: number | null;
|
|
yom: number | null;
|
|
owner: number | null;
|
|
}
|
|
|
|
interface ServiceRecord {
|
|
id: number;
|
|
created_at: string;
|
|
vehicle: string;
|
|
metadata: string | null;
|
|
status: number;
|
|
mileage: number | null;
|
|
Vehicles?: Vehicle;
|
|
}
|
|
|
|
export default function VehicleStatusPage() {
|
|
const router = useRouter();
|
|
const params = useParams();
|
|
const vehicleId = params?.vehicleId as string;
|
|
const [serviceRecords, setServiceRecords] = useState<ServiceRecord[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [selectedRecord, setSelectedRecord] = useState<ServiceRecord | null>(null);
|
|
const [jobActions, setJobActions] = useState<JobAction[]>([]);
|
|
const [loadingActions, setLoadingActions] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (vehicleId) {
|
|
loadServiceRecords();
|
|
}
|
|
}, [vehicleId]);
|
|
|
|
// Close modal when clicking outside or pressing Escape
|
|
useEffect(() => {
|
|
const handleEscape = (event: KeyboardEvent) => {
|
|
if (event.key === 'Escape' && selectedRecord) {
|
|
setSelectedRecord(null);
|
|
setJobActions([]);
|
|
}
|
|
};
|
|
|
|
if (selectedRecord) {
|
|
document.addEventListener('keydown', handleEscape);
|
|
}
|
|
|
|
return () => {
|
|
document.removeEventListener('keydown', handleEscape);
|
|
};
|
|
}, [selectedRecord]);
|
|
|
|
const loadServiceRecords = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const response = await fetch(`/api/vehicle/${encodeURIComponent(vehicleId)}/records`);
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
setServiceRecords(data);
|
|
} else {
|
|
console.error('Failed to load service records');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading service records:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const getStatusLabel = (status: number) => {
|
|
switch (status) {
|
|
case 0: return 'Pending';
|
|
case 1: return 'Active';
|
|
case 2: return 'Waiting for Customer Response';
|
|
case 3: return 'Waiting for Parts';
|
|
case 10: return 'Completed';
|
|
default: return 'Unknown';
|
|
}
|
|
};
|
|
|
|
const getStatusColor = (status: number) => {
|
|
switch (status) {
|
|
case 0: return 'text-white/60';
|
|
case 1: return 'text-cyan-400';
|
|
case 2: return 'text-yellow-400';
|
|
case 3: return 'text-orange-400';
|
|
case 10: return 'text-green-400';
|
|
default: return 'text-white/60';
|
|
}
|
|
};
|
|
|
|
const handleShowDetails = async (record: ServiceRecord) => {
|
|
setSelectedRecord(record);
|
|
setLoadingActions(true);
|
|
try {
|
|
const response = await fetch(`/api/vehicle/${encodeURIComponent(vehicleId)}/actions/${record.id}`);
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
setJobActions(data);
|
|
} else {
|
|
console.error('Failed to load job actions');
|
|
setJobActions([]);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading job actions:', error);
|
|
setJobActions([]);
|
|
} finally {
|
|
setLoadingActions(false);
|
|
}
|
|
};
|
|
|
|
const vehicle = serviceRecords[0]?.Vehicles;
|
|
|
|
return (
|
|
<div className="relative min-h-screen bg-black tech-grid scanlines">
|
|
{/* Animated background elements */}
|
|
<div className="absolute inset-0 overflow-hidden">
|
|
<div className="absolute top-1/4 left-1/4 h-96 w-96 rounded-full bg-white/5 blur-3xl animate-pulse"></div>
|
|
<div className="absolute bottom-1/4 right-1/4 h-96 w-96 rounded-full bg-white/5 blur-3xl animate-pulse delay-1000"></div>
|
|
</div>
|
|
|
|
<div className="relative mx-auto max-w-6xl px-4 py-12">
|
|
<div className="relative rounded-lg border-2 border-white/30 bg-black/80 backdrop-blur-sm p-8 neon-border-glow">
|
|
<div className="absolute inset-0 rounded-lg bg-gradient-to-br from-white/5 to-transparent"></div>
|
|
<div className="relative">
|
|
<div className="mb-6 flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<div className="h-2 w-2 rounded-full bg-white animate-pulse"></div>
|
|
<h1 className="text-3xl font-bold text-white uppercase tracking-wider neon-glow">
|
|
Service Status
|
|
</h1>
|
|
</div>
|
|
<Link
|
|
href="/"
|
|
className="text-sm font-medium text-white/60 hover:text-white transition-colors font-mono uppercase tracking-wider border border-white/20 px-3 py-1 rounded hover:border-white/40 hover:bg-white/10"
|
|
>
|
|
← Back to Home
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Vehicle Info */}
|
|
{vehicle && (
|
|
<div className="mb-8 rounded-lg border-2 border-white/20 bg-black/50 p-6">
|
|
<div className="mb-4 text-center">
|
|
<div className="mb-2 text-2xl font-bold text-white font-mono">
|
|
{vehicle.id}
|
|
</div>
|
|
<div className="text-white/80 font-mono">
|
|
{vehicle.make} {vehicle.model}
|
|
{vehicle.capacity && ` (${vehicle.capacity}cc)`}
|
|
{vehicle.yom && ` - ${vehicle.yom}`}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Service Records List */}
|
|
{loading ? (
|
|
<div className="text-center py-12">
|
|
<div className="h-2 w-2 rounded-full bg-white animate-pulse mx-auto mb-4"></div>
|
|
<p className="text-white/60 font-mono uppercase tracking-wider">Loading Service Records...</p>
|
|
</div>
|
|
) : serviceRecords.length === 0 ? (
|
|
<div className="text-center py-12">
|
|
<p className="text-white/60 font-mono uppercase tracking-wider">
|
|
No service records found for vehicle {vehicleId}
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{serviceRecords.map((record) => (
|
|
<div
|
|
key={record.id}
|
|
className="rounded-lg border-2 border-white/20 bg-black/50 p-6 transition-all hover:border-white/40 hover:bg-white/5 hover:shadow-[0_0_15px_rgba(255,255,255,0.2)]"
|
|
>
|
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
<div>
|
|
<div className="mb-1 text-xs text-white/60 font-mono uppercase tracking-wider">
|
|
Service Record ID
|
|
</div>
|
|
<div className="text-white font-mono font-semibold">
|
|
#{record.id}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div className="mb-1 text-xs text-white/60 font-mono uppercase tracking-wider">
|
|
Status
|
|
</div>
|
|
<div className={`font-mono font-semibold ${getStatusColor(record.status)}`}>
|
|
{getStatusLabel(record.status)}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div className="mb-1 text-xs text-white/60 font-mono uppercase tracking-wider">
|
|
Created At
|
|
</div>
|
|
<div className="text-white font-mono text-sm">
|
|
{new Date(record.created_at).toLocaleString()}
|
|
</div>
|
|
</div>
|
|
|
|
{record.mileage !== null && (
|
|
<div>
|
|
<div className="mb-1 text-xs text-white/60 font-mono uppercase tracking-wider">
|
|
Mileage
|
|
</div>
|
|
<div className="text-white font-mono text-sm">
|
|
{record.mileage.toLocaleString()} km
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{record.metadata && (
|
|
<div className="mt-4 pt-4 border-t border-white/10">
|
|
<div className="mb-1 text-xs text-white/60 font-mono uppercase tracking-wider">
|
|
Notes
|
|
</div>
|
|
<div className="text-white/80 font-mono text-sm">
|
|
{record.metadata}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="mt-4 pt-4 border-t border-white/10 flex items-center justify-between">
|
|
<div className="text-xs text-white/40 font-mono">
|
|
Record ID: {record.id}
|
|
</div>
|
|
<button
|
|
onClick={() => handleShowDetails(record)}
|
|
className="rounded-lg border border-cyan-500/50 bg-cyan-500/10 px-4 py-2 text-sm font-medium text-cyan-400 transition-all hover:bg-cyan-500/20 hover:border-cyan-500 hover:shadow-[0_0_15px_rgba(0,191,255,0.4)] font-mono uppercase tracking-wider"
|
|
>
|
|
Show Details
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{!loading && serviceRecords.length > 0 && (
|
|
<div className="mt-6 text-center">
|
|
<p className="text-white/60 font-mono text-sm uppercase tracking-wider">
|
|
{serviceRecords.length} service record{serviceRecords.length !== 1 ? 's' : ''} found
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Details Modal */}
|
|
{selectedRecord && (
|
|
<div
|
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/90 p-4 overflow-y-auto"
|
|
onClick={() => {
|
|
setSelectedRecord(null);
|
|
setJobActions([]);
|
|
}}
|
|
>
|
|
<div
|
|
className="relative w-full max-w-4xl rounded-lg border-2 border-white/30 bg-black/95 backdrop-blur-sm p-8 neon-border-glow my-8"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<div className="absolute inset-0 rounded-lg bg-gradient-to-br from-white/5 to-transparent"></div>
|
|
<div className="relative">
|
|
<div className="mb-6 flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<div className="h-2 w-2 rounded-full bg-white animate-pulse"></div>
|
|
<h2 className="text-2xl font-bold text-white uppercase tracking-wider neon-glow">
|
|
Service Record Details
|
|
</h2>
|
|
</div>
|
|
<button
|
|
onClick={() => {
|
|
setSelectedRecord(null);
|
|
setJobActions([]);
|
|
}}
|
|
className="text-white/60 hover:text-white text-2xl transition-colors"
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
|
|
{/* Service Record Details */}
|
|
<div className="mb-8 space-y-4">
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
<div>
|
|
<div className="mb-1 text-xs text-white/60 font-mono uppercase tracking-wider">
|
|
Service Record ID
|
|
</div>
|
|
<div className="text-white font-mono font-semibold">
|
|
#{selectedRecord.id}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div className="mb-1 text-xs text-white/60 font-mono uppercase tracking-wider">
|
|
Status
|
|
</div>
|
|
<div className={`font-mono font-semibold ${getStatusColor(selectedRecord.status)}`}>
|
|
{getStatusLabel(selectedRecord.status)}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div className="mb-1 text-xs text-white/60 font-mono uppercase tracking-wider">
|
|
Vehicle Number
|
|
</div>
|
|
<div className="text-white font-mono font-semibold">
|
|
{selectedRecord.Vehicles?.id || 'N/A'}
|
|
</div>
|
|
<div className="text-sm text-white/70 font-mono">
|
|
{selectedRecord.Vehicles?.make || 'N/A'} {selectedRecord.Vehicles?.model || ''}
|
|
{selectedRecord.Vehicles?.capacity && ` (${selectedRecord.Vehicles.capacity}cc)`}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div className="mb-1 text-xs text-white/60 font-mono uppercase tracking-wider">
|
|
Created At
|
|
</div>
|
|
<div className="text-white font-mono text-sm">
|
|
{new Date(selectedRecord.created_at).toLocaleString()}
|
|
</div>
|
|
</div>
|
|
|
|
{selectedRecord.mileage !== null && (
|
|
<div>
|
|
<div className="mb-1 text-xs text-white/60 font-mono uppercase tracking-wider">
|
|
Mileage
|
|
</div>
|
|
<div className="text-white font-mono text-sm">
|
|
{selectedRecord.mileage.toLocaleString()} km
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{selectedRecord.metadata && (
|
|
<div>
|
|
<div className="mb-1 text-xs text-white/60 font-mono uppercase tracking-wider">
|
|
Notes
|
|
</div>
|
|
<div className="text-white/80 font-mono text-sm bg-black/50 rounded-lg p-4 border border-white/10">
|
|
{selectedRecord.metadata}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Job Actions List */}
|
|
<div className="border-t border-white/20 pt-6">
|
|
<div className="mb-4 flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<div className="h-2 w-2 rounded-full bg-cyan-500"></div>
|
|
<h3 className="text-xl font-bold text-white uppercase tracking-wider">
|
|
Job Actions
|
|
</h3>
|
|
</div>
|
|
<div className="text-sm text-white/60 font-mono">
|
|
{jobActions.length} action{jobActions.length !== 1 ? 's' : ''}
|
|
</div>
|
|
</div>
|
|
|
|
{loadingActions ? (
|
|
<div className="text-center py-8">
|
|
<div className="h-2 w-2 rounded-full bg-white animate-pulse mx-auto mb-2"></div>
|
|
<p className="text-white/60 font-mono text-sm uppercase tracking-wider">
|
|
Loading Actions...
|
|
</p>
|
|
</div>
|
|
) : jobActions.length === 0 ? (
|
|
<div className="text-center py-8 rounded-lg border border-white/10 bg-black/50">
|
|
<p className="text-white/60 font-mono uppercase tracking-wider">
|
|
No job actions found
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="overflow-x-auto max-h-96 overflow-y-auto">
|
|
<table className="w-full border-collapse">
|
|
<thead className="sticky top-0 bg-black/95">
|
|
<tr className="border-b-2 border-white/20">
|
|
<th className="px-4 py-3 text-left text-xs font-bold text-white uppercase tracking-wider font-mono">Action Type</th>
|
|
<th className="px-4 py-3 text-left text-xs font-bold text-white uppercase tracking-wider font-mono">Part Type</th>
|
|
<th className="px-4 py-3 text-left text-xs font-bold text-white uppercase tracking-wider font-mono">Part ID</th>
|
|
<th className="px-4 py-3 text-right text-xs font-bold text-white uppercase tracking-wider font-mono">Labour Fee</th>
|
|
<th className="px-4 py-3 text-right text-xs font-bold text-white uppercase tracking-wider font-mono">Parts Price</th>
|
|
<th className="px-4 py-3 text-right text-xs font-bold text-white uppercase tracking-wider font-mono">Total</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{jobActions.map((action, index) => {
|
|
const partsPrice = action.custom_price > 0 && action.custom_price !== -1 ? action.custom_price : 0;
|
|
const rowTotal = action.labour_fee + partsPrice;
|
|
return (
|
|
<tr
|
|
key={action.id}
|
|
className={`border-b border-white/10 hover:bg-white/5 transition-colors ${index % 2 === 0 ? 'bg-black/30' : 'bg-black/50'}`}
|
|
>
|
|
<td className="px-4 py-3 text-white font-mono font-semibold">{action.action_type}</td>
|
|
<td className="px-4 py-3 text-white/80 font-mono">{action.part_type || '-'}</td>
|
|
<td className="px-4 py-3 text-white/80 font-mono">{action.part_id || '-'}</td>
|
|
<td className="px-4 py-3 text-right text-white font-mono">රු.{action.labour_fee || 0}</td>
|
|
<td className="px-4 py-3 text-right text-white font-mono">{partsPrice > 0 ? `රු.${partsPrice}` : '-'}</td>
|
|
<td className="px-4 py-3 text-right text-white font-mono font-semibold">රු.{rowTotal}</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
<tr className="border-t-2 border-white/30 bg-black/70 font-bold sticky bottom-0">
|
|
<td colSpan={5} className="px-4 py-4 text-right text-white font-mono uppercase tracking-wider">
|
|
Total Cost:
|
|
</td>
|
|
<td className="px-4 py-4 text-right text-white font-mono text-lg font-bold">
|
|
රු.{jobActions.reduce((sum, action) => {
|
|
const partsPrice = action.custom_price > 0 && action.custom_price !== -1 ? action.custom_price : 0;
|
|
return sum + action.labour_fee + partsPrice;
|
|
}, 0)}
|
|
</td>
|
|
</tr>
|
|
<tr className="bg-black/70 sticky bottom-0">
|
|
<td colSpan={6} className="px-4 pb-2 text-right text-white/60 font-mono text-xs">
|
|
<div className="flex justify-end gap-4">
|
|
<span>Labour: රු.{jobActions.reduce((sum, action) => sum + action.labour_fee, 0)}</span>
|
|
<span>Parts: රු.{jobActions.reduce((sum, action) => {
|
|
const partsPrice = action.custom_price > 0 && action.custom_price !== -1 ? action.custom_price : 0;
|
|
return sum + partsPrice;
|
|
}, 0)}</span>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|