feat: add support for battery swapping status and bike image gallery management in fleet details
This commit is contained in:
@@ -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',
|
||||||
@@ -486,9 +501,9 @@ export default function FleetDetailPage({ params }: { params: Promise<{ id: stri
|
|||||||
<td className="px-4 py-3 text-sm font-medium text-slate-700">৳{damage.actualCost || '-'}</td>
|
<td className="px-4 py-3 text-sm font-medium text-slate-700">৳{damage.actualCost || '-'}</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${damage.status === 'repaired' ? 'bg-green-100 text-green-700' :
|
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${damage.status === 'repaired' ? 'bg-green-100 text-green-700' :
|
||||||
damage.status === 'under_repair' ? 'bg-amber-100 text-amber-700' :
|
damage.status === 'under_repair' ? 'bg-amber-100 text-amber-700' :
|
||||||
damage.status === 'claim_rejected' ? 'bg-red-100 text-red-700' :
|
damage.status === 'claim_rejected' ? 'bg-red-100 text-red-700' :
|
||||||
'bg-slate-100 text-slate-700'
|
'bg-slate-100 text-slate-700'
|
||||||
}`}>
|
}`}>
|
||||||
{damage.status.replace('_', ' ')}
|
{damage.status.replace('_', ' ')}
|
||||||
</span>
|
</span>
|
||||||
@@ -571,8 +586,8 @@ export default function FleetDetailPage({ params }: { params: Promise<{ id: stri
|
|||||||
<td className="px-4 py-3 text-sm text-slate-600">{maintenance.nextDueDate || '-'}</td>
|
<td className="px-4 py-3 text-sm text-slate-600">{maintenance.nextDueDate || '-'}</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${maintenance.status === 'completed' ? 'bg-green-100 text-green-700' :
|
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${maintenance.status === 'completed' ? 'bg-green-100 text-green-700' :
|
||||||
maintenance.status === 'in_progress' ? 'bg-amber-100 text-amber-700' :
|
maintenance.status === 'in_progress' ? 'bg-amber-100 text-amber-700' :
|
||||||
'bg-slate-100 text-slate-700'
|
'bg-slate-100 text-slate-700'
|
||||||
}`}>
|
}`}>
|
||||||
{maintenance.status.replace('_', ' ')}
|
{maintenance.status.replace('_', ' ')}
|
||||||
</span>
|
</span>
|
||||||
@@ -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 ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user