feat: add support for battery swapping status and bike image gallery management in fleet details

This commit is contained in:
sazzadulalambd
2026-05-16 20:54:17 +06:00
parent de9499b567
commit 4c25990e70

View File

@@ -9,7 +9,7 @@ import {
GaugeCircle, CheckCircle, AlertTriangle, Activity, Award, TrendingUp, Wallet, GaugeCircle, CheckCircle, AlertTriangle, Activity, Award, TrendingUp, Wallet,
MoreHorizontal, Map, Navigation2, Satellite, FileCheck, FileX, Clock3, MoreHorizontal, Map, Navigation2, Satellite, FileCheck, FileX, Clock3,
History, CreditCard, User2, Phone, Mail, MapPinned, ExternalLink, Plus, History, CreditCard, User2, Phone, Mail, MapPinned, ExternalLink, Plus,
AlertCircle, Image as ImageIcon AlertCircle, Image as ImageIcon, Camera
} from 'lucide-react'; } from 'lucide-react';
interface GPSDevice { interface GPSDevice {
@@ -104,7 +104,8 @@ interface BatteryHistory {
serialNumber: string; serialNumber: string;
assignedDate: string; assignedDate: string;
returnedDate?: string; returnedDate?: string;
status: 'active' | 'returned'; swappedToBatteryId?: string;
status: 'active' | 'returned' | 'swapped';
socStart: number; socStart: number;
socEnd?: number; socEnd?: number;
monthlyRent?: number; monthlyRent?: number;
@@ -152,6 +153,12 @@ interface Bike {
damageHistory?: DamageRecord[]; damageHistory?: DamageRecord[];
maintenanceHistory?: MaintenanceRecord[]; maintenanceHistory?: MaintenanceRecord[];
batteryHistory?: BatteryHistory[]; batteryHistory?: BatteryHistory[];
bikeImages?: {
front?: string;
back?: string;
left?: string;
right?: string;
};
} }
const mockBikes: Bike[] = [ const mockBikes: Bike[] = [
@@ -188,8 +195,8 @@ const mockBikes: Bike[] = [
batteryHistory: [ batteryHistory: [
{ id: 'BH001', batteryId: 'BAT-001', brand: 'EVE Energy', model: 'Li-Ion 60V50Ah', serialNumber: 'SN-2024-00001', assignedDate: '2024-03-15', status: 'active', socStart: 85, monthlyRent: 1500 }, { id: 'BH001', batteryId: 'BAT-001', brand: 'EVE Energy', model: 'Li-Ion 60V50Ah', serialNumber: 'SN-2024-00001', assignedDate: '2024-03-15', status: 'active', socStart: 85, monthlyRent: 1500 },
{ id: 'BH002', batteryId: 'BAT-005', brand: 'Samsung SDI', model: 'Li-Ion 60V45Ah', serialNumber: 'SN-2023-00045', assignedDate: '2024-01-10', returnedDate: '2024-03-14', status: 'returned', socStart: 80, socEnd: 45, monthlyRent: 1200 }, { id: 'BH002', batteryId: 'BAT-005', brand: 'Samsung SDI', model: 'Li-Ion 60V45Ah', serialNumber: 'SN-2023-00045', assignedDate: '2024-01-10', returnedDate: '2024-03-14', status: 'returned', socStart: 80, socEnd: 45, monthlyRent: 1200 },
{ id: 'BH003', batteryId: 'BAT-008', brand: 'LG Chem', model: 'Li-Ion 48V40Ah', serialNumber: 'SN-2023-00012', assignedDate: '2023-11-05', returnedDate: '2024-01-09', status: 'returned', socStart: 90, socEnd: 60, monthlyRent: 1000 }, { id: 'BH003', batteryId: 'BAT-008', brand: 'LG Chem', model: 'Li-Ion 48V40Ah', serialNumber: 'SN-2023-00012', assignedDate: '2023-11-05', returnedDate: '2024-01-09', status: 'swapped', socStart: 90, socEnd: 60, monthlyRent: 1000 },
{ id: 'BH004', batteryId: 'BAT-003', brand: 'Panasonic', model: 'Li-Ion 60V50Ah', serialNumber: 'SN-2023-00078', assignedDate: '2023-08-20', returnedDate: '2023-11-04', status: 'returned', socStart: 88, socEnd: 55, monthlyRent: 1500 }, { id: 'BH004', batteryId: 'BAT-003', brand: 'Panasonic', model: 'Li-Ion 60V50Ah', serialNumber: 'SN-2023-00078', assignedDate: '2023-08-20', returnedDate: '2023-11-04', status: 'swapped', socStart: 88, socEnd: 55, monthlyRent: 1500 },
{ id: 'BH005', batteryId: 'BAT-002', brand: 'Sony', model: 'Li-Ion 48V35Ah', serialNumber: 'SN-2023-00034', assignedDate: '2023-05-15', returnedDate: '2023-08-19', status: 'returned', socStart: 75, socEnd: 40, monthlyRent: 900 }, { id: 'BH005', batteryId: 'BAT-002', brand: 'Sony', model: 'Li-Ion 48V35Ah', serialNumber: 'SN-2023-00034', assignedDate: '2023-05-15', returnedDate: '2023-08-19', status: 'returned', socStart: 75, socEnd: 40, monthlyRent: 900 },
{ id: 'BH006', batteryId: 'BAT-012', brand: 'BYD', model: 'LiFePO4 60V40Ah', serialNumber: 'SN-2023-00056', assignedDate: '2024-04-20', returnedDate: '2024-06-15', status: 'returned', socStart: 92, socEnd: 35, monthlyRent: 1300 }, { id: 'BH006', batteryId: 'BAT-012', brand: 'BYD', model: 'LiFePO4 60V40Ah', serialNumber: 'SN-2023-00056', assignedDate: '2024-04-20', returnedDate: '2024-06-15', status: 'returned', socStart: 92, socEnd: 35, monthlyRent: 1300 },
{ id: 'BH007', batteryId: 'BAT-015', brand: 'CATL', model: 'Li-Ion 72V50Ah', serialNumber: 'SN-2024-00089', assignedDate: '2024-06-20', status: 'active', socStart: 88, monthlyRent: 1800 }, { id: 'BH007', batteryId: 'BAT-015', brand: 'CATL', model: 'Li-Ion 72V50Ah', serialNumber: 'SN-2024-00089', assignedDate: '2024-06-20', status: 'active', socStart: 88, monthlyRent: 1800 },
@@ -198,7 +205,15 @@ const mockBikes: Bike[] = [
{ id: 'BH010', batteryId: 'BAT-007', brand: 'Kexin', model: 'Li-Ion 48V36Ah', serialNumber: 'SN-2022-00045', assignedDate: '2022-12-05', returnedDate: '2023-02-08', status: 'returned', socStart: 85, socEnd: 55, monthlyRent: 850 }, { id: 'BH010', batteryId: 'BAT-007', brand: 'Kexin', model: 'Li-Ion 48V36Ah', serialNumber: 'SN-2022-00045', assignedDate: '2022-12-05', returnedDate: '2023-02-08', status: 'returned', socStart: 85, socEnd: 55, monthlyRent: 850 },
{ id: 'BH011', batteryId: 'BAT-004', brand: 'Faraday', model: 'LiFePO4 48V42Ah', serialNumber: 'SN-2022-00089', assignedDate: '2022-09-15', returnedDate: '2022-12-04', status: 'returned', socStart: 90, socEnd: 48, monthlyRent: 1200 }, { id: 'BH011', batteryId: 'BAT-004', brand: 'Faraday', model: 'LiFePO4 48V42Ah', serialNumber: 'SN-2022-00089', assignedDate: '2022-09-15', returnedDate: '2022-12-04', status: 'returned', socStart: 90, socEnd: 48, monthlyRent: 1200 },
{ id: 'BH012', batteryId: 'BAT-006', brand: 'Reliance', model: 'Lead Acid 48V32Ah', serialNumber: 'SN-2022-00034', assignedDate: '2022-06-20', returnedDate: '2022-09-14', status: 'returned', socStart: 95, socEnd: 30, monthlyRent: 600 }, { id: 'BH012', batteryId: 'BAT-006', brand: 'Reliance', model: 'Lead Acid 48V32Ah', serialNumber: 'SN-2022-00034', assignedDate: '2022-06-20', returnedDate: '2022-09-14', status: 'returned', socStart: 95, socEnd: 30, monthlyRent: 600 },
] { id: 'BH013', batteryId: 'BAT-020', brand: 'Maxell', model: 'Li-Ion 60V45Ah', serialNumber: 'SN-2024-00123', assignedDate: '2024-07-10', swappedToBatteryId: 'BAT-025', status: 'swapped', socStart: 75, socEnd: 65, monthlyRent: 1400 },
{ id: 'BH014', batteryId: 'BAT-018', brand: 'Nikola', model: 'LiFePO4 48V40Ah', serialNumber: 'SN-2023-00078', assignedDate: '2024-05-05', swappedToBatteryId: 'BAT-022', status: 'swapped', socStart: 82, socEnd: 55, monthlyRent: 1150 },
],
bikeImages: {
front: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400&h=300&fit=crop',
back: 'https://images.unsplash.com/photo-1591637333184-19aa84de3fbd?w=400&h=300&fit=crop',
left: 'https://images.unsplash.com/photo-1622185135505-2d795043906a?w=400&h=300&fit=crop',
right: 'https://images.unsplash.com/photo-1609630875171-b1321377ee53?w=400&h=300&fit=crop',
}
}, },
{ {
id: 'EV002', model: 'Yadea DT3', brand: 'Yadea', image: '', plateNumber: 'Dhaka Metro Cha-A-5678', status: 'available', batteryLevel: 95, location: 'Banani', hubId: 'HUB-002', hubName: 'Banani Hub', investorId: 'inv1', investorName: 'Mr. Hasan (Investor)', purchaseDate: '2024-02-01', purchasePrice: 118000, totalRides: 89, totalDistance: 1567, totalEarnings: 31150, lastService: '2024-03-15', nextService: '2024-04-15', insuranceExpiry: '2025-02-01', registrationExpiry: '2026-02-01', id: 'EV002', model: 'Yadea DT3', brand: 'Yadea', image: '', plateNumber: 'Dhaka Metro Cha-A-5678', status: 'available', batteryLevel: 95, location: 'Banani', hubId: 'HUB-002', hubName: 'Banani Hub', investorId: 'inv1', investorName: 'Mr. Hasan (Investor)', purchaseDate: '2024-02-01', purchasePrice: 118000, totalRides: 89, totalDistance: 1567, totalEarnings: 31150, lastService: '2024-03-15', nextService: '2024-04-15', insuranceExpiry: '2025-02-01', registrationExpiry: '2026-02-01',
@@ -773,7 +788,7 @@ function OverviewTab({ bike }: { bike: Bike }) {
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Battery ID</th> <th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Battery ID</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Brand/Model</th> <th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Brand/Model</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Assigned Date</th> <th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Assigned Date</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Returned Date</th> <th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Returned/Swapped</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Start SOC</th> <th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Start SOC</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">End SOC</th> <th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">End SOC</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Monthly Rent</th> <th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Monthly Rent</th>
@@ -786,13 +801,20 @@ function OverviewTab({ bike }: { bike: Bike }) {
<td className="px-3 py-2 text-sm text-slate-700 font-medium">{bh.batteryId}</td> <td className="px-3 py-2 text-sm text-slate-700 font-medium">{bh.batteryId}</td>
<td className="px-3 py-2 text-sm text-slate-600">{bh.brand} {bh.model}</td> <td className="px-3 py-2 text-sm text-slate-600">{bh.brand} {bh.model}</td>
<td className="px-3 py-2 text-sm text-slate-600">{bh.assignedDate}</td> <td className="px-3 py-2 text-sm text-slate-600">{bh.assignedDate}</td>
<td className="px-3 py-2 text-sm text-slate-600">{bh.returnedDate || '-'}</td> <td className="px-3 py-2 text-sm text-slate-600">
{bh.status === 'swapped' ? (
<>
<span className="text-blue-600 block">Swapped to {bh.swappedToBatteryId}</span>
<span className="text-blue-600 text-[10px] block opacity-75">{bh.returnedDate}</span>
</>
) : bh.returnedDate || '-'}
</td>
<td className="px-3 py-2 text-sm text-slate-600">{bh.socStart}%</td> <td className="px-3 py-2 text-sm text-slate-600">{bh.socStart}%</td>
<td className="px-3 py-2 text-sm text-slate-600">{bh.socEnd ? `${bh.socEnd}%` : '-'}</td> <td className="px-3 py-2 text-sm text-slate-600">{bh.socEnd ? `${bh.socEnd}%` : '-'}</td>
<td className="px-3 py-2 text-sm text-green-600">{bh.monthlyRent}</td> <td className="px-3 py-2 text-sm text-green-600">{bh.monthlyRent}</td>
<td className="px-3 py-2"> <td className="px-3 py-2">
<span className={`text-xs font-medium px-2 py-1 rounded-full ${bh.status === 'active' ? 'bg-green-100 text-green-700' : 'bg-slate-100 text-slate-500'}`}> <span className={`text-xs font-medium px-2 py-1 rounded-full ${bh.status === 'active' ? 'bg-green-100 text-green-700' : bh.status === 'swapped' ? 'bg-blue-100 text-blue-700' : 'bg-slate-100 text-slate-500'}`}>
{bh.status === 'active' ? 'Active' : 'Returned'} {bh.status === 'active' ? 'Active' : bh.status === 'swapped' ? 'Swapped' : 'Returned'}
</span> </span>
</td> </td>
</tr> </tr>
@@ -926,9 +948,50 @@ function GPSTab({ bike }: { bike: Bike }) {
function DocumentsTab({ bike }: { bike: Bike }) { function DocumentsTab({ bike }: { bike: Bike }) {
const docs = bike.documents || []; const docs = bike.documents || [];
const [images, setImages] = useState(bike.bikeImages || { front: '', back: '', left: '', right: '' });
const [uploading, setUploading] = useState<string | null>(null);
const handleImageUpload = (view: 'front' | 'back' | 'left' | 'right', e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
setUploading(view);
const reader = new FileReader();
reader.onloadend = () => {
setImages(prev => ({ ...prev, [view]: reader.result as string }));
setUploading(null);
};
reader.readAsDataURL(file);
}
};
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-slate-100">
<h3 className="font-semibold text-slate-700 mb-4">Bike Images</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{(['front', 'back', 'left', 'right'] as const).map(view => (
<div key={view} className="space-y-2">
<p className="text-xs font-medium text-slate-500 capitalize">{view} View</p>
<div className="relative aspect-video bg-slate-100 rounded-lg overflow-hidden border border-slate-200">
{images[view] ? (
<img src={images[view]} alt={`${view} view`} className="w-full h-full object-cover" />
) : (
<div className="w-full h-full flex items-center justify-center">
<Camera className="w-8 h-8 text-slate-300" />
</div>
)}
<label className="absolute inset-0 cursor-pointer hover:bg-black/20 flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity">
<span className="bg-white px-3 py-1 rounded-full text-xs font-medium text-slate-700 shadow">
{uploading === view ? 'Uploading...' : 'Upload'}
</span>
<input type="file" accept="image/*" className="hidden" onChange={(e) => handleImageUpload(view, e)} />
</label>
</div>
</div>
))}
</div>
</div>
<div className="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-slate-100"> <div className="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-slate-100">
<h3 className="font-semibold text-slate-700 mb-4">Bike Documents</h3> <h3 className="font-semibold text-slate-700 mb-4">Bike Documents</h3>
{docs.length === 0 ? ( {docs.length === 0 ? (