feat: add service center management module with CRUD functionality and status filtering

This commit is contained in:
sazzadulalambd
2026-05-20 14:48:29 +06:00
parent 8669da78d6
commit 989221f953
3 changed files with 1910 additions and 1 deletions

View File

@@ -0,0 +1,801 @@
'use client';
import { useState, useEffect } from 'react';
import { useParams, useRouter } from 'next/navigation';
import {
ArrowLeft, Building2, Star, Phone, Mail, MapPin, Activity, Wrench,
Battery, Bike, DollarSign, CheckCircle2, Clock, AlertTriangle, Search,
SlidersHorizontal, ArrowUpDown, User, Calendar, Shield, Tag, Plus, Eye,
BarChart3, Percent, ChevronRight, ExternalLink
} from 'lucide-react';
import Link from 'next/link';
import { ServiceCenter } from '../page';
// Interface for Maintenance History Record
interface HistoryRecord {
id: string;
date: string;
assetId: string;
assetType: 'EV Bike' | 'Battery';
serviceType: 'Damage' | 'Repair' | 'Service' | 'Battery Swap' | 'Inspection';
description: string;
severity: 'critical' | 'major' | 'minor' | 'cosmetic';
status: 'completed' | 'in_progress' | 'parts_ordered';
estimatedCost: number;
actualCost: number;
partsUsed: { name: string; qty: number; price: number }[];
laborCost: number;
technician: string;
}
// Generate realistic mock history data based on Center ID/Name
const getMockHistoryData = (centerName: string): HistoryRecord[] => {
const baseHistory: HistoryRecord[] = [
{
id: 'MNT-101',
date: '2024-03-21',
assetId: 'EV-004',
assetType: 'EV Bike',
serviceType: 'Damage',
description: 'Front fender shattered in traffic collision. Replaced brackets and front wheel.',
severity: 'major',
status: 'in_progress',
estimatedCost: 3500,
actualCost: 3200,
partsUsed: [
{ name: 'Front fender', qty: 1, price: 1500 },
{ name: 'Mounting brackets', qty: 2, price: 800 },
{ name: 'Brake pads', qty: 1, price: 600 }
],
laborCost: 1200,
technician: 'Sabbir Ahmed'
},
{
id: 'MNT-102',
date: '2024-03-18',
assetId: 'BAT-044',
assetType: 'Battery',
serviceType: 'Battery Swap',
description: 'Internal diagnostic showing rapid voltage degradation. Replaced cells and recalibrated BMS.',
severity: 'critical',
status: 'completed',
estimatedCost: 12000,
actualCost: 11500,
partsUsed: [
{ name: 'Battery 60V cell pack', qty: 1, price: 9500 },
{ name: 'BMS Controller Board', qty: 1, price: 2000 }
],
laborCost: 2500,
technician: 'Kamrul Hasan'
},
{
id: 'MNT-103',
date: '2024-03-15',
assetId: 'EV-012',
assetType: 'EV Bike',
serviceType: 'Service',
description: 'Routine 5,000km periodic maintenance. Calibrated drum brakes and greased chassis bearings.',
severity: 'minor',
status: 'completed',
estimatedCost: 1500,
actualCost: 1450,
partsUsed: [
{ name: 'Brake Cable', qty: 1, price: 250 },
{ name: 'Sprocket kit', qty: 1, price: 450 }
],
laborCost: 750,
technician: 'Sabbir Ahmed'
},
{
id: 'MNT-104',
date: '2024-03-10',
assetId: 'BAT-021',
assetType: 'Battery',
serviceType: 'Inspection',
description: 'Thermal warning flag during hyper-charging cycle. Terminals cleaned and thermal gel reapplied.',
severity: 'cosmetic',
status: 'completed',
estimatedCost: 500,
actualCost: 400,
partsUsed: [
{ name: 'Thermal paste', qty: 1, price: 150 }
],
laborCost: 250,
technician: 'Kamrul Hasan'
},
{
id: 'MNT-105',
date: '2024-03-05',
assetId: 'EV-009',
assetType: 'EV Bike',
serviceType: 'Repair',
description: 'Throttle failure reported by delivery driver. Replaced magnetic sensor assembly.',
severity: 'major',
status: 'completed',
estimatedCost: 1800,
actualCost: 2100,
partsUsed: [
{ name: 'Throttle control assembly', qty: 1, price: 800 },
{ name: 'Wiring loom adapter', qty: 1, price: 450 }
],
laborCost: 850,
technician: 'Rafiqul Islam'
},
{
id: 'MNT-106',
date: '2024-02-28',
assetId: 'EV-017',
assetType: 'EV Bike',
serviceType: 'Damage',
description: 'Rear tire blowout due to road debris. Replacement and alignment completed.',
severity: 'minor',
status: 'completed',
estimatedCost: 2800,
actualCost: 2750,
partsUsed: [
{ name: 'Rear Tire tubeless', qty: 1, price: 2200 },
{ name: 'Chain replacement', qty: 1, price: 400 }
],
laborCost: 500,
technician: 'Sabbir Ahmed'
},
{
id: 'MNT-107',
date: '2024-02-20',
assetId: 'BAT-089',
assetType: 'Battery',
serviceType: 'Battery Swap',
description: 'Dead module replacement under premium warranty. Replaced sub-assemblies.',
severity: 'critical',
status: 'parts_ordered',
estimatedCost: 15000,
actualCost: 0,
partsUsed: [
{ name: 'Battery 48V cell pack', qty: 1, price: 8000 }
],
laborCost: 1500,
technician: 'Kamrul Hasan'
}
];
// Variations in records based on Center's specialty & size to make data dynamic
if (centerName.includes('Gulshan') || centerName.includes('Center A')) {
return baseHistory;
} else if (centerName.includes('Banani') || centerName.includes('Center B')) {
return baseHistory.filter(h => h.serviceType === 'Battery Swap' || h.serviceType === 'Service' || h.serviceType === 'Inspection').map(h => ({
...h,
id: h.id.replace('10', '20'),
technician: 'Tanvir Rahman'
}));
} else {
// Uttara / Authorized
return baseHistory.filter(h => h.serviceType === 'Inspection' || h.serviceType === 'Repair').map(h => ({
...h,
id: h.id.replace('10', '30'),
technician: 'Arif Chowdhury'
}));
}
};
export default function ServiceCenterDetailsPage() {
const params = useParams();
const router = useRouter();
const id = params.id as string;
const [isMounted, setIsMounted] = useState(false);
const [center, setCenter] = useState<ServiceCenter | null>(null);
const [history, setHistory] = useState<HistoryRecord[]>([]);
// Filtering / Sorting / Search states for history
const [searchQuery, setSearchQuery] = useState('');
const [assetFilter, setAssetFilter] = useState('all');
const [typeFilter, setTypeFilter] = useState('all');
const [severityFilter, setSeverityFilter] = useState('all');
const [statusFilter, setStatusFilter] = useState('all');
const [sortBy, setSortBy] = useState<'date-desc' | 'date-asc' | 'cost-desc' | 'cost-asc' | 'urgency-desc' | 'urgency-asc'>('date-desc');
// Map Interactive detail popup state
const [mapPopup, setMapPopup] = useState<string | null>(null);
useEffect(() => {
setIsMounted(true);
// Load Service Centers from localStorage
const stored = localStorage.getItem('jaiben_service_centers');
let foundCenter: ServiceCenter | null = null;
if (stored) {
try {
const centers: ServiceCenter[] = JSON.parse(stored);
foundCenter = centers.find(c => c.id === id) || null;
} catch (e) {}
}
if (foundCenter) {
setCenter(foundCenter);
setHistory(getMockHistoryData(foundCenter.name));
} else {
router.push('/admin/service-centers');
}
}, [id, router]);
if (!isMounted || !center) return null;
// Filter History records
const filteredHistory = history.filter(h => {
const matchesSearch = h.id.toLowerCase().includes(searchQuery.toLowerCase()) ||
h.assetId.toLowerCase().includes(searchQuery.toLowerCase()) ||
h.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
h.technician.toLowerCase().includes(searchQuery.toLowerCase());
const matchesAsset = assetFilter === 'all' || h.assetType === assetFilter;
const matchesType = typeFilter === 'all' || h.serviceType === typeFilter;
const matchesSeverity = severityFilter === 'all' || h.severity === severityFilter;
const matchesStatus = statusFilter === 'all' || h.status === statusFilter;
return matchesSearch && matchesAsset && matchesType && matchesSeverity && matchesStatus;
});
// Sort History records
const severityWeights = { cosmetic: 1, minor: 2, major: 3, critical: 4 };
const sortedHistory = [...filteredHistory].sort((a, b) => {
switch (sortBy) {
case 'date-desc':
return new Date(b.date).getTime() - new Date(a.date).getTime();
case 'date-asc':
return new Date(a.date).getTime() - new Date(b.date).getTime();
case 'cost-desc':
return (b.actualCost || b.estimatedCost) - (a.actualCost || a.estimatedCost);
case 'cost-asc':
return (a.actualCost || a.estimatedCost) - (b.actualCost || b.estimatedCost);
case 'urgency-desc':
return severityWeights[b.severity] - severityWeights[a.severity];
case 'urgency-asc':
return severityWeights[a.severity] - severityWeights[b.severity];
default:
return 0;
}
});
// Financial & Aggregate calculations
const totalRepairs = history.length;
const completedRepairs = history.filter(h => h.status === 'completed');
const totalEstimatedCost = completedRepairs.reduce((sum, h) => sum + h.estimatedCost, 0);
const totalActualCost = completedRepairs.reduce((sum, h) => sum + h.actualCost, 0);
const costVariance = totalActualCost - totalEstimatedCost;
const totalPartsCost = completedRepairs.reduce((sum, h) => sum + h.partsUsed.reduce((s, p) => s + (p.price * p.qty), 0), 0);
const totalLaborCost = completedRepairs.reduce((sum, h) => sum + h.laborCost, 0);
const totalSpend = totalPartsCost + totalLaborCost;
// Aggregate Parts Utilized Log
const partsAggregated: { name: string; totalQty: number; totalCost: number }[] = [];
history.forEach(h => {
h.partsUsed.forEach(part => {
const existing = partsAggregated.find(p => p.name === part.name);
if (existing) {
existing.totalQty += part.qty;
existing.totalCost += part.price * part.qty;
} else {
partsAggregated.push({
name: part.name,
totalQty: part.qty,
totalCost: part.price * part.qty
});
}
});
});
const topParts = partsAggregated.sort((a, b) => b.totalQty - a.totalQty).slice(0, 5);
const statusColors = {
active: 'bg-emerald-100 text-emerald-700',
busy: 'bg-amber-100 text-amber-700',
inactive: 'bg-slate-100 text-slate-700'
};
const severityColors = {
critical: 'bg-red-100 text-red-700',
major: 'bg-orange-100 text-orange-700',
minor: 'bg-amber-100 text-amber-700',
cosmetic: 'bg-slate-100 text-slate-700'
};
const statusHistoryColors = {
completed: 'bg-emerald-100 text-emerald-700',
in_progress: 'bg-blue-100 text-blue-700',
parts_ordered: 'bg-purple-100 text-purple-700'
};
return (
<div className="p-4 lg:p-6 mb-6 lg:mb-0 space-y-6 max-w-8xl mx-auto">
{/* Navigation Top - standard layout of other detail profiles */}
<div className="flex items-center justify-between border-b border-slate-100 pb-4 mb-4">
<button
onClick={() => router.push('/admin/service-centers')}
className="flex items-center gap-2 text-slate-600 hover:text-slate-900 text-sm font-semibold transition-colors duration-200"
>
<ArrowLeft className="w-4 h-4 text-slate-400" /> Back to Service Centers
</button>
<span className="text-xs font-bold text-slate-400">
Node Registry: {center.id}
</span>
</div>
{/* Main Profile Info Header - rounded-xl alignment matching maintenance page */}
<div className="bg-white rounded-xl p-6 border border-slate-100 shadow-sm space-y-6 relative overflow-hidden">
{/* Glow effect decorative */}
<div className="absolute top-0 right-0 w-64 h-64 bg-accent/5 rounded-full blur-3xl -mr-16 -mt-16 pointer-events-none" />
<div className="flex flex-col lg:flex-row lg:items-start justify-between gap-6 relative z-10">
<div className="space-y-4 flex-1">
<div className="flex flex-wrap items-center gap-3">
<div className="w-14 h-14 rounded-xl bg-slate-50 border border-slate-100 flex items-center justify-center flex-shrink-0">
<Building2 className="w-7 h-7 text-slate-600" />
</div>
<div>
<h1 className="text-2xl lg:text-3xl font-extrabold text-slate-800 leading-tight">{center.name}</h1>
<div className="flex flex-wrap items-center gap-2 mt-1">
<span className={`px-2.5 py-0.5 text-xs font-semibold rounded-full border border-transparent ${statusColors[center.status]}`}>
{center.status}
</span>
<div className="flex items-center gap-1 text-yellow-500 font-bold text-sm bg-yellow-50 px-2 py-0.5 rounded border border-yellow-100">
<Star className="w-3.5 h-3.5 fill-yellow-500" />
<span>{center.rating.toFixed(1)}</span>
</div>
</div>
</div>
</div>
{/* Profile items - clean, consistent spacing */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-y-3 gap-x-6 text-sm text-slate-600 pt-2">
<div className="flex items-center gap-2">
<MapPin className="w-4 h-4 text-slate-400" />
<div className="flex flex-wrap items-center gap-x-2">
<span>{center.address}</span>
{center.googleMapLink && (
<a
href={center.googleMapLink}
target="_blank"
rel="noopener noreferrer"
className="text-accent hover:underline flex items-center gap-0.5 font-bold"
>
Map Link <ExternalLink className="w-3 h-3" />
</a>
)}
</div>
</div>
<div className="flex items-center gap-2">
<Phone className="w-4 h-4 text-slate-400" />
<span>{center.phone}</span>
</div>
<div className="flex items-center gap-2">
<Mail className="w-4 h-4 text-slate-400" />
<span>{center.email}</span>
</div>
<div className="flex items-center gap-2">
<Shield className="w-4 h-4 text-slate-400" />
<span>Staff: <strong className="font-semibold text-slate-800">{center.staffCount} technicians</strong></span>
</div>
<div className="flex items-center gap-2">
<Wrench className="w-4 h-4 text-slate-400" />
<span>Capacity: <strong className="font-semibold text-slate-800">{center.capacity}</strong> total slots</span>
</div>
</div>
{/* Specialization List Header */}
<div className="space-y-1.5 pt-2">
<span className="text-[10px] font-extrabold uppercase text-slate-400 tracking-wider">Node Specializations</span>
<div className="flex flex-wrap gap-1.5">
{center.specialization.map(spec => (
<span key={spec} className="px-2.5 py-1 bg-slate-50 border border-slate-100 rounded-lg text-xs font-bold text-slate-600">
{spec}
</span>
))}
</div>
</div>
</div>
{/* Quick Stats Header Summary - simplified matching maintenance specs, occupancy not needed */}
<div className="grid grid-cols-2 gap-4 bg-slate-50 p-5 rounded-xl border border-slate-100 w-full lg:max-w-xs flex-shrink-0">
<div className="space-y-1">
<p className="text-xs font-bold text-slate-400 uppercase tracking-wide">Repairs Logs</p>
<p className="text-2xl font-extrabold text-slate-800">{totalRepairs}</p>
<p className="text-[10px] text-slate-500">{completedRepairs.length} completed</p>
</div>
<div className="space-y-1">
<p className="text-xs font-bold text-slate-400 uppercase tracking-wide">Capacity</p>
<p className="text-2xl font-extrabold text-slate-800">{center.capacity}</p>
<p className="text-[10px] text-slate-500">Service slots registered</p>
</div>
</div>
</div>
</div>
{/* Analytics: Map Mockup & Cost Breakdown Grid */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* INTERACTIVE STYLIZED MAP CONTAINER - aligned clean white rounded-xl styles */}
<div className="bg-white rounded-xl p-6 border border-slate-100 shadow-sm space-y-4 flex flex-col justify-between lg:col-span-1 min-h-[380px]">
<div className="space-y-1">
<h3 className="font-extrabold text-slate-800 text-lg">Node Location Map</h3>
<p className="text-xs text-slate-400">Dhaka city arterial coverage grid mockup</p>
</div>
{/* Map canvas container */}
<div className="flex-1 bg-slate-900 rounded-xl relative overflow-hidden border border-slate-850 shadow-inner flex items-center justify-center min-h-[220px]">
{/* Pulsating target coordinate representing the Center */}
<div
className="absolute w-8 h-8 flex items-center justify-center cursor-pointer group z-20"
style={{ top: '45%', left: '50%', transform: 'translate(-50%, -50%)' }}
onClick={() => setMapPopup(center.name)}
>
<span className="absolute inline-flex h-full w-full rounded-full bg-accent opacity-75 animate-ping" />
<div className="relative w-4 h-4 bg-accent border-2 border-white rounded-full flex items-center justify-center shadow-lg group-hover:scale-125 transition-transform">
<div className="w-1.5 h-1.5 bg-white rounded-full" />
</div>
</div>
{/* Stylized Dhaka grids & landmarks using SVGs */}
<svg className="w-full h-full absolute inset-0 opacity-40 select-none pointer-events-none" viewBox="0 0 100 100" preserveAspectRatio="none">
{/* Arterial Highways */}
<line x1="20" y1="0" x2="20" y2="100" stroke="#475569" strokeWidth="0.75" strokeDasharray="2" />
<line x1="50" y1="0" x2="50" y2="100" stroke="#475569" strokeWidth="1.5" />
<line x1="80" y1="0" x2="80" y2="100" stroke="#475569" strokeWidth="0.75" />
<line x1="0" y1="30" x2="100" y2="30" stroke="#475569" strokeWidth="0.75" />
<line x1="0" y1="50" x2="100" y2="50" stroke="#475569" strokeWidth="1.5" />
<line x1="0" y1="80" x2="100" y2="80" stroke="#475569" strokeWidth="0.75" />
{/* Waterway (Gulshan Lake) */}
<path d="M 50,0 Q 52,25 48,50 T 54,100" fill="none" stroke="#1e3a8a" strokeWidth="3" opacity="0.3" />
<path d="M 47,40 Q 70,45 80,42" fill="none" stroke="#1e3a8a" strokeWidth="2.5" opacity="0.3" />
{/* Other hubs mockup dots */}
<circle cx="20" cy="30" r="1.5" fill="#4f46e5" />
<circle cx="80" cy="30" r="1.5" fill="#4f46e5" />
<circle cx="20" cy="80" r="1.5" fill="#4f46e5" />
<circle cx="80" cy="80" r="1.5" fill="#4f46e5" />
</svg>
{/* Scale watermark */}
<div className="absolute bottom-2 left-2 text-[9px] text-slate-500 font-bold bg-slate-950/75 px-1.5 py-0.5 rounded border border-slate-800">
GPS: {center.latitude.toFixed(4)}°N, {center.longitude.toFixed(4)}°E
</div>
{/* Stylized popup when clicked */}
{mapPopup && (
<div className="absolute top-2 right-2 left-2 bg-slate-950/90 border border-slate-800 rounded-lg p-2.5 text-xs text-white z-30 animate-fadeIn space-y-1">
<div className="flex items-center justify-between">
<span className="font-extrabold text-accent">{center.name}</span>
<button onClick={() => setMapPopup(null)} className="text-slate-400 hover:text-white font-bold">×</button>
</div>
<p className="text-[10px] text-slate-400">{center.address}</p>
<div className="flex justify-between pt-1 border-t border-slate-800 text-[9px] text-slate-400 font-bold">
<span>Capacity: {center.capacity} slots</span>
<span>Rating: {center.rating.toFixed(1)}</span>
</div>
</div>
)}
{/* Custom street labels */}
<div className="absolute top-4 left-[53%] text-[8px] font-bold text-slate-600 tracking-widest uppercase origin-center rotate-90 select-none">
Gulshan Lake Road
</div>
<div className="absolute top-[52%] left-4 text-[8px] font-bold text-slate-600 tracking-widest uppercase select-none">
Tejgaon-Gulshan Link Road
</div>
</div>
<div className="text-center text-[10px] font-bold text-slate-400 uppercase tracking-widest pt-1">
📍 Click GPS coordinate node for telemetry details
</div>
</div>
{/* FINANCIAL PERFORMANCE & EXPENSE TRACKING */}
<div className="bg-white rounded-xl p-6 border border-slate-100 shadow-sm space-y-6 lg:col-span-2">
<div className="space-y-1">
<h3 className="font-extrabold text-slate-800 text-lg">Financial Performance & Cost Margins</h3>
<p className="text-xs text-slate-400">Aggregated historical metrics from completed maintenance invoices</p>
</div>
{/* Financial details panel */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-slate-50 border border-slate-100 p-4 rounded-xl flex flex-col justify-between">
<span className="text-xs font-bold text-slate-400 uppercase tracking-wider block">Estimated Spend</span>
<div className="mt-2">
<p className="text-2xl font-extrabold text-slate-800">{totalEstimatedCost.toLocaleString()}</p>
<p className="text-[10px] text-slate-500 mt-0.5">Budgeted repairs cost</p>
</div>
</div>
<div className="bg-indigo-50/50 border border-indigo-100/50 p-4 rounded-xl flex flex-col justify-between">
<span className="text-xs font-bold text-slate-400 uppercase tracking-wider block">Actual Invoice Spend</span>
<div className="mt-2">
<p className="text-2xl font-extrabold text-indigo-700">{totalActualCost.toLocaleString()}</p>
<p className="text-[10px] text-indigo-500 mt-0.5">Billed repair totals</p>
</div>
</div>
<div className={`p-4 rounded-xl border flex flex-col justify-between ${costVariance > 0 ? 'bg-rose-50 border-rose-100' : 'bg-emerald-50 border-emerald-100'}`}>
<span className="text-xs font-bold text-slate-400 uppercase tracking-wider block">Cost Variance</span>
<div className="mt-2">
<p className={`text-2xl font-extrabold ${costVariance > 0 ? 'text-rose-700' : 'text-emerald-700'}`}>
{costVariance > 0 ? `+৳${costVariance.toLocaleString()}` : `-৳${Math.abs(costVariance).toLocaleString()}`}
</p>
<p className="text-[10px] text-slate-500 mt-0.5">
{costVariance > 0 ? 'Over budget invoices' : 'Under budget savings!'}
</p>
</div>
</div>
</div>
{/* Parts Used Aggregates & Labor Breakdown */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 pt-2">
{/* Margins */}
<div className="space-y-4">
<h4 className="text-xs font-extrabold uppercase text-slate-400 tracking-wider">Expense Margin Distribution</h4>
<div className="space-y-3">
{/* Parts Spend Bar */}
<div className="space-y-1">
<div className="flex items-center justify-between text-xs font-bold text-slate-600">
<span className="flex items-center gap-1"><Battery className="w-3.5 h-3.5 text-indigo-500" /> Spare Parts Cost</span>
<span>{totalPartsCost.toLocaleString()} ({totalSpend > 0 ? Math.round((totalPartsCost/totalSpend)*100) : 0}%)</span>
</div>
<div className="w-full bg-slate-100 rounded-full h-2">
<div
className="bg-indigo-650 h-full rounded-full"
style={{ width: `${totalSpend > 0 ? (totalPartsCost/totalSpend)*100 : 0}%` }}
/>
</div>
</div>
{/* Labor Spend Bar */}
<div className="space-y-1">
<div className="flex items-center justify-between text-xs font-bold text-slate-600">
<span className="flex items-center gap-1"><Wrench className="w-3.5 h-3.5 text-emerald-500" /> Labor Costs</span>
<span>{totalLaborCost.toLocaleString()} ({totalSpend > 0 ? Math.round((totalLaborCost/totalSpend)*100) : 0}%)</span>
</div>
<div className="w-full bg-slate-100 rounded-full h-2">
<div
className="bg-emerald-550 h-full rounded-full"
style={{ width: `${totalSpend > 0 ? (totalLaborCost/totalSpend)*100 : 0}%` }}
/>
</div>
</div>
</div>
{/* General Health Tip */}
<div className="p-3 bg-slate-50 border border-slate-100 rounded-lg text-xs text-slate-500 flex items-start gap-2">
<AlertTriangle className="w-4 h-4 text-amber-500 flex-shrink-0 mt-0.5" />
<span>
<strong>Cost Ratio Notice:</strong> This node maintains a healthy parts-to-labor ratio of {totalSpend > 0 ? Math.round((totalPartsCost/totalSpend)*100) : 0}:{totalSpend > 0 ? Math.round((totalLaborCost/totalSpend)*100) : 0}. Lower labor ratios reflect technician efficiency.
</span>
</div>
</div>
{/* Parts utilized list */}
<div className="space-y-3">
<h4 className="text-xs font-extrabold uppercase text-slate-400 tracking-wider">Top Spare Parts Log</h4>
<div className="divide-y divide-slate-100 border border-slate-100 rounded-xl overflow-hidden bg-slate-50/50">
{topParts.length === 0 ? (
<p className="text-xs text-slate-400 p-4 text-center">No spare parts recorded yet</p>
) : topParts.map(part => (
<div key={part.name} className="p-2.5 flex items-center justify-between text-xs text-slate-600">
<span className="font-semibold text-slate-700">{part.name}</span>
<div className="flex items-center gap-4 text-right">
<span className="font-bold text-slate-500">Qty: {part.totalQty}</span>
<span className="font-extrabold text-slate-800">{part.totalCost.toLocaleString()}</span>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</div>
{/* Interactive History Log - aligned standard filters and table headers */}
<div className="bg-white rounded-xl p-6 border border-slate-100 shadow-sm space-y-6">
{/* Section title */}
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4 border-b border-slate-100 pb-4">
<div className="space-y-1">
<h3 className="font-extrabold text-slate-800 text-lg flex items-center gap-2">
<Activity className="w-5 h-5 text-indigo-500" />
<span>Serviced Fleet History Log</span>
</h3>
<p className="text-xs text-slate-400">Integrated audit list for EV bikes and Battery Swap maintenance nodes</p>
</div>
{/* Quick Counter */}
<span className="px-3 py-1 bg-indigo-50 border border-indigo-100/50 rounded-lg text-xs font-extrabold text-indigo-700 self-start lg:self-auto">
{sortedHistory.length} Matches Found
</span>
</div>
{/* Filter Controls Panel */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-3 bg-slate-50 p-4 rounded-lg border border-slate-100">
{/* Search bar */}
<div className="relative col-span-1 lg:col-span-2">
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
<input
type="text"
placeholder="Search by ID, tech, description..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-9 pr-3 py-2 border border-slate-200 rounded-lg text-xs focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all bg-white"
/>
</div>
{/* Filter 1: Asset Type */}
<select
value={assetFilter}
onChange={(e) => setAssetFilter(e.target.value)}
className="py-2 px-3 border border-slate-200 rounded-lg text-xs font-semibold text-slate-600 bg-white focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all"
>
<option value="all">All Asset Types</option>
<option value="EV Bike">EV Bike</option>
<option value="Battery">Battery</option>
</select>
{/* Filter 2: Service Type */}
<select
value={typeFilter}
onChange={(e) => setTypeFilter(e.target.value)}
className="py-2 px-3 border border-slate-200 rounded-lg text-xs font-semibold text-slate-600 bg-white focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all"
>
<option value="all">All Service Types</option>
<option value="Damage">Damage</option>
<option value="Repair">Repair</option>
<option value="Service">Service</option>
<option value="Battery Swap">Battery Swap</option>
<option value="Inspection">Inspection</option>
</select>
{/* Sorting Dropdown */}
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as any)}
className="py-2 px-3 border border-slate-200 rounded-lg text-xs font-bold text-indigo-600 bg-white focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all"
>
<option value="date-desc">📆 Date: Newest First</option>
<option value="date-asc">📆 Date: Oldest First</option>
<option value="cost-desc"> Cost: Highest First</option>
<option value="cost-asc"> Cost: Lowest First</option>
<option value="urgency-desc"> Severity: Critical First</option>
<option value="urgency-asc"> Severity: Cosmetic First</option>
</select>
</div>
{/* Table / List representation */}
{sortedHistory.length === 0 ? (
<div className="text-center py-12 bg-slate-50 rounded-lg border border-dashed border-slate-200">
<Activity className="w-12 h-12 text-slate-300 mx-auto mb-3" />
<p className="text-sm font-semibold text-slate-500">No matching service records</p>
<p className="text-xs text-slate-400 mt-0.5">Try clearing filters or search variables</p>
</div>
) : (
<div className="overflow-x-auto border border-slate-100 rounded-lg">
<table className="w-full text-left border-collapse">
<thead>
<tr className="bg-slate-50 border-b border-slate-100 text-xs font-semibold uppercase tracking-wider text-slate-500">
<th className="py-3 px-4">Record ID</th>
<th className="py-3 px-4">Asset Code</th>
<th className="py-3 px-4">Service Type</th>
<th className="py-3 px-4">Description</th>
<th className="py-3 px-4 text-center">Severity</th>
<th className="py-3 px-4 text-center">Status</th>
<th className="py-3 px-4 text-right">Invoice cost</th>
<th className="py-3 px-4 text-right">Details</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100 text-sm text-slate-700">
{sortedHistory.map(record => (
<tr key={record.id} className="hover:bg-slate-50/50 transition-colors">
<td className="py-4 px-4 font-bold text-slate-400">{record.id}</td>
<td className="py-4 px-4">
<div className="flex items-center gap-1.5">
{record.assetType === 'EV Bike' ? (
<Bike className="w-4 h-4 text-purple-600 flex-shrink-0" />
) : (
<Battery className="w-4 h-4 text-green-600 flex-shrink-0" />
)}
<div className="space-y-0.5">
<span className="font-extrabold text-slate-800">{record.assetId}</span>
<span className="text-[10px] text-slate-400 uppercase font-semibold block">{record.assetType}</span>
</div>
</div>
</td>
<td className="py-4 px-4 font-semibold text-slate-700">{record.serviceType}</td>
<td className="py-4 px-4 max-w-sm">
<div className="space-y-1">
<p className="text-xs text-slate-600 line-clamp-2 leading-relaxed">{record.description}</p>
<div className="flex items-center gap-3 text-[10px] text-slate-400 font-semibold">
<span className="flex items-center gap-0.5"><User className="w-3 h-3" /> Tech: {record.technician}</span>
<span className="flex items-center gap-0.5"><Calendar className="w-3 h-3" /> {record.date}</span>
</div>
</div>
</td>
<td className="py-4 px-4 text-center">
<span className={`px-2 py-0.5 text-xs font-semibold rounded-full border inline-block ${severityColors[record.severity]}`}>
{record.severity}
</span>
</td>
<td className="py-4 px-4 text-center">
<span className={`px-2.5 py-0.5 text-xs font-semibold rounded-full border inline-block ${statusHistoryColors[record.status]}`}>
{record.status.replace('_', ' ')}
</span>
</td>
<td className="py-4 px-4 text-right font-extrabold text-slate-800">
{record.actualCost > 0 ? (
<span>{record.actualCost.toLocaleString()}</span>
) : (
<span className="text-slate-400 font-normal italic text-xs">Pending invoice</span>
)}
</td>
<td className="py-4 px-4 text-right">
{/* Deep link details */}
<Link
href={`/admin/maintenance/${record.id}`}
className="p-1.5 hover:bg-slate-100 rounded-lg text-slate-400 hover:text-slate-700 transition-colors inline-block"
title="Open full maintenance record"
>
<Eye className="w-4 h-4" />
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
);
}