feat: enhance bike overview with detailed battery tracking and rental subscription data

This commit is contained in:
sazzadulalambd
2026-05-16 20:44:19 +06:00
parent ec487f6d27
commit de9499b567

View File

@@ -96,6 +96,20 @@ interface MaintenanceRecord {
notes?: string;
}
interface BatteryHistory {
id: string;
batteryId: string;
brand: string;
model: string;
serialNumber: string;
assignedDate: string;
returnedDate?: string;
status: 'active' | 'returned';
socStart: number;
socEnd?: number;
monthlyRent?: number;
}
interface Bike {
id: string;
model: string;
@@ -104,10 +118,19 @@ interface Bike {
plateNumber: string;
status: 'available' | 'rented' | 'maintenance' | 'retired';
batteryLevel: number;
location?: string; // deprecated - use hubId/hubName
currentBatteryId?: string;
currentBatteryBrand?: string;
currentBatteryModel?: string;
location?: string;
hubId?: string;
hubName?: string;
assignedTo?: string;
renterPhone?: string;
renterNid?: string;
rentalStartDate?: string;
subscriptionType?: 'daily' | 'weekly' | 'monthly';
weeklyRent?: number;
monthlyRent?: number;
investorId?: string;
investorName?: string;
purchaseDate?: string;
@@ -128,11 +151,12 @@ interface Bike {
assignmentHistory?: BikeAssignment[];
damageHistory?: DamageRecord[];
maintenanceHistory?: MaintenanceRecord[];
batteryHistory?: BatteryHistory[];
}
const mockBikes: Bike[] = [
{
id: 'EV001', model: 'Etron ET50', brand: 'Etron', image: '', plateNumber: 'Dhaka Metro Cha-A-1234', status: 'rented', batteryLevel: 78, location: 'Gulshan 1', assignedTo: 'Rahim Ahmed', hubId: 'HUB-001', hubName: 'JAIBEN Head Office', investorId: 'inv1', investorName: 'Mr. Hasan (Investor)', purchaseDate: '2024-01-15', purchasePrice: 125000, currentRent: 350, totalRides: 156, totalDistance: 2340, totalEarnings: 54600, lastService: '2024-03-01', nextService: '2024-04-01', insuranceExpiry: '2025-01-15', registrationExpiry: '2026-01-15',
id: 'EV001', model: 'Etron ET50', brand: 'Etron', image: '', plateNumber: 'Dhaka Metro Cha-A-1234', status: 'rented', batteryLevel: 78, currentBatteryId: 'BAT-001', currentBatteryBrand: 'EVE Energy', currentBatteryModel: 'Li-Ion 60V50Ah', location: 'Gulshan 1', assignedTo: 'Rahim Ahmed', renterPhone: '01712345678', renterNid: '1234567890', rentalStartDate: '2024-03-01', subscriptionType: 'weekly', weeklyRent: 2400, location: 'Gulshan 1', assignedTo: 'Rahim Ahmed', hubId: 'HUB-001', hubName: 'JAIBEN Head Office', investorId: 'inv1', investorName: 'Mr. Hasan (Investor)', purchaseDate: '2024-01-15', purchasePrice: 125000, currentRent: 350, totalRides: 156, totalDistance: 2340, totalEarnings: 54600, lastService: '2024-03-01', nextService: '2024-04-01', insuranceExpiry: '2025-01-15', registrationExpiry: '2026-01-15',
gpsDevice: { id: 'GPS001', phone: '01712345601', imei: '861234567890123', lastActive: '2024-03-21 14:30', signal: 85, battery: 72 },
documents: [
{ type: 'registration', number: 'REG-EV001-2024', issueDate: '2024-01-15', expiryDate: '2026-01-15', verified: true },
@@ -160,6 +184,20 @@ const mockBikes: Bike[] = [
{ id: 'MNT001', date: '2024-03-01', type: 'routine', description: 'Full service - oil change, brake check, tire rotation', performedBy: 'Service Center', cost: 1500, nextDueDate: '2024-04-01', status: 'completed', hubId: 'HUB-001', hubName: 'Gulshan Hub' },
{ id: 'MNT002', date: '2024-02-15', type: 'battery', description: 'Battery health check and terminal cleaning', performedBy: 'Service Center', cost: 500, nextDueDate: '2024-05-15', status: 'completed', hubId: 'HUB-003', hubName: 'Uttara Hub' },
{ id: 'MNT003', date: '2024-01-20', type: 'tire', description: 'Tire pressure check and inflation', performedBy: 'Service Center', cost: 300, nextDueDate: '2024-04-20', status: 'completed', hubId: 'HUB-004', hubName: 'Mirpur Hub' },
],
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: '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: '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: '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: '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: 'BH008', batteryId: 'BAT-009', brand: 'Tongsheng', model: 'Li-Ion 48V45Ah', serialNumber: 'SN-2023-00023', assignedDate: '2023-02-10', returnedDate: '2023-05-14', status: 'returned', socStart: 82, socEnd: 50, monthlyRent: 1100 },
{ id: 'BH009', batteryId: 'BAT-011', brand: 'Binek', model: 'Li-Ion 60V48Ah', serialNumber: 'SN-2023-00067', assignedDate: '2024-02-01', returnedDate: '2024-04-18', status: 'returned', socStart: 78, socEnd: 42, monthlyRent: 1400 },
{ 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: '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 },
]
},
{
@@ -606,6 +644,15 @@ export default function FleetDetailPage({ params }: { params: Promise<{ id: stri
}
function OverviewTab({ bike }: { bike: Bike }) {
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 5;
const batteryHistory = bike.batteryHistory || [];
const totalPages = Math.ceil(batteryHistory.length / itemsPerPage);
const paginatedHistory = batteryHistory.slice(
(currentPage - 1) * itemsPerPage,
currentPage * itemsPerPage
);
return (
<div className="space-y-4">
<div className="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-slate-100">
@@ -629,20 +676,38 @@ function OverviewTab({ bike }: { bike: Bike }) {
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Plate Number</p>
<p className="font-semibold text-slate-700">{bike.plateNumber}</p>
<p className="text-xs text-slate-500">Bike</p>
<p className="font-semibold text-slate-700">{bike.brand} {bike.model}</p>
<p className="text-xs text-slate-500 mt-1">{bike.plateNumber}</p>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Battery</p>
<p className={`font-semibold ${bike.batteryLevel > 50 ? 'text-green-600' : bike.batteryLevel > 20 ? 'text-amber-600' : 'text-red-600'}`}>{bike.batteryLevel}%</p>
<p className={`font-semibold text-lg ${bike.batteryLevel > 50 ? 'text-green-600' : bike.batteryLevel > 20 ? 'text-amber-600' : 'text-red-600'}`}>{bike.batteryLevel}%</p>
{bike.currentBatteryId && (
<p className="text-xs font-medium text-slate-700 mt-1">{bike.currentBatteryId}</p>
)}
{bike.currentBatteryBrand && (
<p className="text-xs text-slate-500">{bike.currentBatteryBrand} {bike.currentBatteryModel}</p>
)}
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Current Renter</p>
<p className="font-semibold text-slate-700">{bike.assignedTo || 'Available'}</p>
{bike.renterPhone && <p className="text-xs text-slate-500 mt-1">{bike.renterPhone}</p>}
{bike.rentalStartDate && <p className="text-xs text-slate-400 mt-1">Since: {bike.rentalStartDate}</p>}
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Daily Rate</p>
<p className="font-semibold text-green-600">{bike.currentRent || 0}</p>
<p className="text-xs text-slate-500">Subscription</p>
<p className="font-semibold text-slate-700 capitalize">{bike.subscriptionType || 'Daily'}</p>
{bike.subscriptionType === 'weekly' && bike.weeklyRent && (
<p className="text-xs text-green-600 mt-1">{bike.weeklyRent}/week</p>
)}
{bike.subscriptionType === 'monthly' && bike.monthlyRent && (
<p className="text-xs text-green-600 mt-1">{bike.monthlyRent}/month</p>
)}
{bike.subscriptionType === 'daily' && (
<p className="text-xs text-green-600 mt-1">{bike.currentRent || 0}/day</p>
)}
</div>
</div>
</div>
@@ -696,6 +761,84 @@ function OverviewTab({ bike }: { bike: Bike }) {
</div>
)}
</div>
<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">Battery History</h3>
{batteryHistory.length > 0 ? (
<>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-50">
<tr>
<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">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">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">Monthly Rent</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{paginatedHistory.map(bh => (
<tr key={bh.id} className="hover:bg-slate-50">
<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.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.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-green-600">{bh.monthlyRent}</td>
<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'}`}>
{bh.status === 'active' ? 'Active' : 'Returned'}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
{totalPages > 1 && (
<div className="flex items-center justify-between mt-4 pt-4 border-t border-slate-100">
<p className="text-sm text-slate-500">
Showing {(currentPage - 1) * itemsPerPage + 1} to {Math.min(currentPage * itemsPerPage, batteryHistory.length)} of {batteryHistory.length} batteries
</p>
<div className="flex items-center gap-2">
<button
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage === 1}
className="px-3 py-1 text-sm border border-slate-200 rounded-lg hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Previous
</button>
{Array.from({ length: totalPages }, (_, i) => i + 1).map(page => (
<button
key={page}
onClick={() => setCurrentPage(page)}
className={`px-3 py-1 text-sm rounded-lg ${currentPage === page ? 'bg-accent text-white' : 'border border-slate-200 hover:bg-slate-50'}`}
>
{page}
</button>
))}
<button
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages}
className="px-3 py-1 text-sm border border-slate-200 rounded-lg hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Next
</button>
</div>
</div>
)}
</>
) : (
<div className="text-center py-8 text-slate-500">
No battery history found
</div>
)}
</div>
</div>
);
}