From f5cd411a053536283437f5f23e11ffa98f227797 Mon Sep 17 00:00:00 2001 From: sazzadulalambd Date: Sat, 16 May 2026 12:10:49 +0600 Subject: [PATCH] feat: implement battery management module with list and detail views --- src/app/admin/batteries/[id]/page.tsx | 431 ++++++++ src/app/admin/batteries/page.tsx | 1312 +++++++++++++++++++++++++ src/components/Sidebar.tsx | 1 + 3 files changed, 1744 insertions(+) create mode 100644 src/app/admin/batteries/[id]/page.tsx create mode 100644 src/app/admin/batteries/page.tsx diff --git a/src/app/admin/batteries/[id]/page.tsx b/src/app/admin/batteries/[id]/page.tsx new file mode 100644 index 0000000..81e146e --- /dev/null +++ b/src/app/admin/batteries/[id]/page.tsx @@ -0,0 +1,431 @@ +'use client'; + +import { useState, use } from 'react'; +import Link from 'next/link'; +import { + Battery, ArrowLeft, X, BatteryCharging, Activity, Gauge, MapPin, Bike, User, History, + Calendar, DollarSign, CheckCircle, Clock, ArrowRightLeft, Handshake, TrendingUp +} from 'lucide-react'; + +interface BMSData { + voltage: number; + current: number; + soc: number; + temperature: number; + cycles: number; + health: number; + timestamp: string; +} + +interface OwnershipLog { + id: string; + batteryId: string; + fromOwner: string; + fromOwnerType: 'company' | 'biker' | 'hub' | 'swap-station'; + toOwner: string; + toOwnerType: 'company' | 'biker' | 'hub' | 'swap-station'; + fromBikeId?: string; + fromBikeModel?: string; + toBikeId?: string; + toBikeModel?: string; + fromHubId?: string; + fromHubName?: string; + toHubId?: string; + toHubName?: string; + fromStationId?: string; + fromStationName?: string; + toStationId?: string; + toStationName?: string; + action: 'assigned' | 'rented' | 'swapped' | 'returned' | 'retired'; + rentAmount?: number; + notes?: string; + timestamp: string; +} + +interface Battery { + id: string; + serialNumber: string; + brand: string; + model: string; + type: 'lithium-ion' | 'lifepo4' | 'lead-acid'; + capacity: number; + voltage: number; + purchaseDate: string; + purchasePrice: number; + warrantyExpiry: string; + status: 'available' | 'in-use' | 'maintenance' | 'retired' | 'charging'; + currentSoc: number; + health: number; + cycleCount: number; + assignedBikeId?: string; + assignedBikeModel?: string; + assignedBikePlate?: string; + assignedBikerId?: string; + assignedBikerName?: string; + assignedBikerPhone?: string; + currentHubId?: string; + currentHubName?: string; + currentStationId?: string; + currentStationName?: string; + lastMaintenance?: string; + nextMaintenance?: string; + bmsData?: BMSData; + monthlyRent?: number; + ownershipLogs: OwnershipLog[]; +} + +const mockBattery: Battery = { + id: 'BAT-001', + serialNumber: 'SN-2024-00001', + brand: 'EVE Energy', + model: 'Li-Ion 60V50Ah', + type: 'lithium-ion', + capacity: 50, + voltage: 60, + purchaseDate: '2024-01-15', + purchasePrice: 45000, + warrantyExpiry: '2027-01-15', + status: 'in-use', + currentSoc: 78, + health: 95, + cycleCount: 156, + assignedBikeId: 'EV001', + assignedBikeModel: 'Etron ET50', + assignedBikePlate: 'Dhaka Metro Cha-A-1234', + assignedBikerId: 'BK-001', + assignedBikerName: 'Rahim Ahmed', + assignedBikerPhone: '01712345678', + currentHubId: 'HUB-001', + currentHubName: 'JAIBEN Head Office', + monthlyRent: 1500, + lastMaintenance: '2024-03-01', + nextMaintenance: '2024-06-01', + bmsData: { voltage: 67.2, current: -2.5, soc: 78, temperature: 32, cycles: 156, health: 95, timestamp: '2024-03-28 12:00:00' }, + ownershipLogs: [ + { id: 'OL-001', batteryId: 'BAT-001', fromOwner: 'JAIBEN Hub', fromOwnerType: 'hub', toOwner: 'Rahim Ahmed', toOwnerType: 'biker', toBikeId: 'EV001', toBikeModel: 'Etron ET50', toHubId: 'HUB-001', toHubName: 'JAIBEN Head Office', action: 'rented', rentAmount: 1500, timestamp: '2024-03-15 08:30:00' }, + { id: 'OL-002', batteryId: 'BAT-001', fromOwner: 'Rahim Ahmed', fromOwnerType: 'biker', toOwner: 'JAIBEN Hub', toOwnerType: 'hub', fromBikeId: 'EV001', fromBikeModel: 'Etron ET50', fromHubId: 'HUB-001', fromHubName: 'JAIBEN Head Office', action: 'returned', timestamp: '2024-03-28 18:00:00', notes: 'Returned for charging' }, + { id: 'OL-003', batteryId: 'BAT-001', fromOwner: 'JAIBEN Hub', fromOwnerType: 'hub', toOwner: 'Rahim Ahmed', toOwnerType: 'biker', toBikeId: 'EV001', toBikeModel: 'Etron ET50', toHubId: 'HUB-001', toHubName: 'JAIBEN Head Office', action: 'rented', rentAmount: 1500, timestamp: '2024-03-28 18:30:00' }, + ] +}; + +const statusColors: Record = { + available: 'bg-green-100 text-green-700', + 'in-use': 'bg-blue-100 text-blue-700', + maintenance: 'bg-amber-100 text-amber-700', + retired: 'bg-slate-100 text-slate-500', + charging: 'bg-purple-100 text-purple-700', +}; + +const typeLabels: Record = { + 'lithium-ion': 'Lithium-Ion', + 'lifepo4': 'LiFePO4', + 'lead-acid': 'Lead Acid', +}; + +export default function BatteryDetailPage({ params }: { params: Promise<{ id: string }> }) { + const { id } = use(params); + const battery = mockBattery; + const [activeTab, setActiveTab] = useState<'info' | 'bms' | 'history' | 'rent'>('info'); + + return ( +
+
+ + + +
+

Battery Details

+

ID: {battery.id}

+
+
+ + + +
+
+
+
+
+ +
+
+

{battery.brand} {battery.model}

+

{battery.serialNumber}

+ + {battery.status} + +
+
+ +
+
+

SOC

+

50 ? 'text-green-600' : battery.currentSoc > 20 ? 'text-amber-600' : 'text-red-600'}`}>{battery.currentSoc}%

+
+
+

Health

+

80 ? 'text-green-600' : battery.health > 60 ? 'text-amber-600' : 'text-red-600'}`}>{battery.health}%

+
+
+

Cycles

+

{battery.cycleCount}

+
+
+

Price

+

৳{battery.purchasePrice.toLocaleString()}

+
+
+
+
+ + {battery.bmsData && ( +
+
+ + Live BMS Data + Real-time +
+
+
+

Voltage

+

{battery.bmsData.voltage}V

+
+
+

Current

+

{battery.bmsData.current}A

+
+
+

SOC

+

{battery.bmsData.soc}%

+
+
+

Temp

+

{battery.bmsData.temperature}°C

+
+
+

Cycles

+

{battery.bmsData.cycles}

+
+
+

Health

+

80 ? 'text-green-600' : 'text-amber-600'}`}>{battery.bmsData.health}%

+
+
+
+ )} + +
+
+ + + + +
+
+ +
+ {activeTab === 'info' && ( +
+
+

Brand

+

{battery.brand}

+
+
+

Model

+

{battery.model}

+
+
+

Serial Number

+

{battery.serialNumber}

+
+
+

Type

+

{typeLabels[battery.type]}

+
+
+

Capacity

+

{battery.capacity} Ah

+
+
+

Voltage

+

{battery.voltage} V

+
+
+

Purchase Date

+

{battery.purchaseDate}

+
+
+

Warranty Expiry

+

{battery.warrantyExpiry}

+
+
+

Last Maintenance

+

{battery.lastMaintenance || 'N/A'}

+
+
+

Next Maintenance

+

{battery.nextMaintenance || 'N/A'}

+
+
+

Purchase Price

+

৳{battery.purchasePrice.toLocaleString()}

+
+ {battery.monthlyRent && ( +
+

Monthly Rent

+

৳{battery.monthlyRent}/month

+
+ )} +
+ )} + + {activeTab === 'bms' && battery.bmsData && ( +
+
+
+ +

BMS Real-time Data

+
+ Updated: {battery.bmsData.timestamp} +
+
+
+

Voltage

+

{battery.bmsData.voltage}V

+
+
+

Current

+

{battery.bmsData.current}A

+
+
+

SOC

+

{battery.bmsData.soc}%

+
+
+

Temperature

+

{battery.bmsData.temperature}°C

+
+
+

Cycles

+

{battery.bmsData.cycles}

+
+
+

Health

+

80 ? 'text-green-600' : 'text-amber-600'}`}>{battery.bmsData.health}%

+
+
+
+ )} + + {activeTab === 'history' && ( +
+
+ +

Ownership History

+
+ {battery.ownershipLogs.length === 0 ? ( +
No history records
+ ) : ( +
+ {battery.ownershipLogs.map((log, index) => ( +
+
+
+
+ + {log.action} + + {log.timestamp} +
+
+
+

From

+

{log.fromOwner}

+
+
+

To

+

{log.toOwner}

+
+
+ {log.rentAmount && ( +

Rent: ৳{log.rentAmount}/month

+ )} + {log.notes && ( +

{log.notes}

+ )} +
+
+ ))} +
+ )} +
+ )} + + {activeTab === 'rent' && ( +
+
+ +

Rent Information

+
+ {battery.monthlyRent ? ( +
+
+
+

Monthly Rent Amount

+

৳{battery.monthlyRent}

+

per month

+
+
+

Status

+

{battery.assignedBikerName ? 'Rented' : 'Not Rented'}

+

{battery.assignedBikerName ? `to ${battery.assignedBikerName}` : 'Available for rental'}

+
+
+
+ ) : ( +
+ This battery is not set up for rental. +
+ )} +
+ )} +
+
+ + {battery.status === 'in-use' && ( +
+
+
+ +
+
+

Currently Rented to Biker

+ Active Rental +
+
+ +
+
+

Biker

+

{battery.assignedBikerName}

+

{battery.assignedBikerPhone}

+
+
+

Bike

+

{battery.assignedBikeModel}

+

{battery.assignedBikePlate}

+
+
+

Hub/Station

+

{battery.currentHubName || battery.currentStationName || 'Not Assigned'}

+
+
+

Monthly Rent

+

৳{battery.monthlyRent}/month

+
+
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/app/admin/batteries/page.tsx b/src/app/admin/batteries/page.tsx new file mode 100644 index 0000000..7b5a9e3 --- /dev/null +++ b/src/app/admin/batteries/page.tsx @@ -0,0 +1,1312 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import Link from 'next/link'; +import { + Battery, Plus, Search, Filter, X, Download, Upload, MoreHorizontal, + BatteryCharging, BatteryWarning, BatteryFull, Zap, Thermometer, + Activity, Clock, MapPin, Bike, User, History, TrendingUp, DollarSign, + Eye, Edit, Trash2, RefreshCw, CheckCircle, AlertTriangle, ChevronRight, + ArrowUpDown, Calendar, Gauge, Timer +} from 'lucide-react'; + +interface BMSData { + voltage: number; + current: number; + soc: number; + temperature: number; + cycles: number; + health: number; + timestamp: string; +} + +interface BatteryHistory { + id: string; + batteryId: string; + bikeId?: string; + bikeModel?: string; + bikerId?: string; + bikerName?: string; + stationId?: string; + stationName?: string; + action: 'assigned' | 'swapped' | 'maintenance' | 'retired'; + timestamp: string; + notes?: string; +} + +interface Battery { + id: string; + serialNumber: string; + brand: string; + model: string; + type: 'lithium-ion' | 'lifepo4' | 'lead-acid'; + capacity: number; + voltage: number; + purchaseDate: string; + purchasePrice: number; + warrantyExpiry: string; + status: 'available' | 'in-use' | 'maintenance' | 'retired' | 'charging'; + currentSoc: number; + health: number; + cycleCount: number; + assignedBikeId?: string; + assignedBikeModel?: string; + assignedBikerId?: string; + assignedBikerName?: string; + currentStationId?: string; + currentStationName?: string; + lastMaintenance?: string; + nextMaintenance?: string; + bmsData?: BMSData; + history: BatteryHistory[]; +} + +interface Bike { + id: string; + model: string; + brand: string; + plateNumber: string; + status: 'available' | 'rented' | 'maintenance' | 'retired'; +} + +interface Biker { + id: string; + name: string; + phone: string; + stationId?: string; + stationName?: string; +} + +interface SwapStation { + id: string; + name: string; + location: string; +} + +const mockBatteries: Battery[] = [ + { + id: 'BAT-001', + serialNumber: 'SN-2024-00001', + brand: 'EVE Energy', + model: 'Li-Ion 60V50Ah', + type: 'lithium-ion', + capacity: 50, + voltage: 60, + purchaseDate: '2024-01-15', + purchasePrice: 45000, + warrantyExpiry: '2027-01-15', + status: 'in-use', + currentSoc: 78, + health: 95, + cycleCount: 156, + assignedBikeId: 'EV001', + assignedBikeModel: 'Etron ET50', + assignedBikerId: 'BK-001', + assignedBikerName: 'Rahim Ahmed', + currentStationId: 'SS-001', + currentStationName: 'Banani Swap Station', + lastMaintenance: '2024-03-01', + nextMaintenance: '2024-06-01', + history: [ + { id: 'H1', batteryId: 'BAT-001', bikeId: 'EV001', bikeModel: 'Etron ET50', bikerId: 'BK-001', bikerName: 'Rahim Ahmed', stationId: 'SS-001', stationName: 'Banani Swap Station', action: 'assigned', timestamp: '2024-03-15 08:30:00' }, + { id: 'H2', batteryId: 'BAT-001', stationId: 'SS-002', stationName: 'Gulshan Swap Station', action: 'swapped', timestamp: '2024-03-20 14:22:00' }, + { id: 'H3', batteryId: 'BAT-001', stationId: 'SS-001', stationName: 'Banani Swap Station', action: 'swapped', timestamp: '2024-03-25 10:15:00' }, + ], + bmsData: { voltage: 67.2, current: -2.5, soc: 78, temperature: 32, cycles: 156, health: 95, timestamp: '2024-03-28 12:00:00' } + }, + { + id: 'BAT-002', + serialNumber: 'SN-2024-00002', + brand: 'CATL', + model: 'LiFePO4 48V40Ah', + type: 'lifepo4', + capacity: 40, + voltage: 48, + purchaseDate: '2024-02-01', + purchasePrice: 38000, + warrantyExpiry: '2027-02-01', + status: 'available', + currentSoc: 100, + health: 98, + cycleCount: 45, + lastMaintenance: '2024-03-15', + nextMaintenance: '2024-06-15', + history: [ + { id: 'H4', batteryId: 'BAT-002', action: 'retired', timestamp: '2024-03-10 09:00:00', notes: 'Retired from active service' }, + ] + }, + { + id: 'BAT-003', + serialNumber: 'SN-2024-00003', + brand: 'Panasonic', + model: 'Li-Ion 60V45Ah', + type: 'lithium-ion', + capacity: 45, + voltage: 60, + purchaseDate: '2024-01-20', + purchasePrice: 42000, + warrantyExpiry: '2027-01-20', + status: 'maintenance', + currentSoc: 15, + health: 72, + cycleCount: 234, + lastMaintenance: '2024-03-28', + nextMaintenance: '2024-04-01', + history: [ + { id: 'H5', batteryId: 'BAT-003', bikeId: 'EV003', bikeModel: 'AIMA Lightning', bikerId: 'BK-002', bikerName: 'Karim Singh', stationId: 'SS-003', stationName: 'Uttara Swap Station', action: 'assigned', timestamp: '2024-02-10 11:00:00' }, + { id: 'H6', batteryId: 'BAT-003', action: 'maintenance', timestamp: '2024-03-28 16:00:00', notes: 'Battery degradation - needs replacement' }, + ] + }, + { + id: 'BAT-004', + serialNumber: 'SN-2024-00004', + brand: 'LG Chem', + model: 'Li-Ion 60V50Ah', + type: 'lithium-ion', + capacity: 50, + voltage: 60, + purchaseDate: '2024-02-15', + purchasePrice: 46000, + warrantyExpiry: '2027-02-15', + status: 'in-use', + currentSoc: 92, + health: 96, + cycleCount: 89, + assignedBikeId: 'EV006', + assignedBikeModel: 'Hero Eddy', + assignedBikerId: 'BK-003', + assignedBikerName: 'Mahir Khan', + currentStationId: 'SS-002', + currentStationName: 'Gulshan Swap Station', + lastMaintenance: '2024-03-05', + nextMaintenance: '2024-06-05', + history: [ + { id: 'H7', batteryId: 'BAT-004', bikeId: 'EV006', bikeModel: 'Hero Eddy', bikerId: 'BK-003', bikerName: 'Mahir Khan', stationId: 'SS-002', stationName: 'Gulshan Swap Station', action: 'assigned', timestamp: '2024-03-05 07:45:00' }, + ], + bmsData: { voltage: 68.4, current: -1.2, soc: 92, temperature: 28, cycles: 89, health: 96, timestamp: '2024-03-28 12:00:00' } + }, + { + id: 'BAT-005', + serialNumber: 'SN-2024-00005', + brand: 'Samsung SDI', + model: 'Li-Ion 60V55Ah', + type: 'lithium-ion', + capacity: 55, + voltage: 60, + purchaseDate: '2024-03-01', + purchasePrice: 50000, + warrantyExpiry: '2027-03-01', + status: 'charging', + currentSoc: 85, + health: 99, + cycleCount: 12, + currentStationId: 'SS-001', + currentStationName: 'Banani Swap Station', + lastMaintenance: '2024-03-20', + nextMaintenance: '2024-06-20', + history: [] + }, + { + id: 'BAT-006', + serialNumber: 'SN-2023-00089', + brand: 'BYD', + model: 'LiFePO4 48V30Ah', + type: 'lifepo4', + capacity: 30, + voltage: 48, + purchaseDate: '2023-06-15', + purchasePrice: 28000, + warrantyExpiry: '2026-06-15', + status: 'retired', + currentSoc: 0, + health: 45, + cycleCount: 450, + lastMaintenance: '2024-01-10', + history: [ + { id: 'H8', batteryId: 'BAT-006', bikeId: 'EV009', bikeModel: 'JME Victory', action: 'retired', timestamp: '2024-01-10 10:00:00', notes: 'Health below 50% - retired' }, + ] + }, +]; + +const mockBikes: Bike[] = [ + { id: 'EV001', model: 'Etron ET50', brand: 'Etron', plateNumber: 'Dhaka Metro Cha-A-1234', status: 'rented' }, + { id: 'EV002', model: 'Yadea DT3', brand: 'Yadea', plateNumber: 'Dhaka Metro Cha-A-5678', status: 'available' }, + { id: 'EV003', model: 'AIMA Lightning', brand: 'AIMA', plateNumber: 'Dhaka Metro Cha-A-9012', status: 'rented' }, + { id: 'EV006', model: 'Hero Eddy', brand: 'Hero', plateNumber: 'Dhaka Metro Cha-B-1122', status: 'rented' }, + { id: 'EV010', model: 'Benling Aura', brand: 'Benling', plateNumber: 'Dhaka Metro Cha-C-9900', status: 'rented' }, +]; + +const mockBikers: Biker[] = [ + { id: 'BK-001', name: 'Rahim Ahmed', phone: '01712345678', stationId: 'SS-001', stationName: 'Banani Swap Station' }, + { id: 'BK-002', name: 'Karim Singh', phone: '01712345679', stationId: 'SS-003', stationName: 'Uttara Swap Station' }, + { id: 'BK-003', name: 'Mahir Khan', phone: '01712345680', stationId: 'SS-002', stationName: 'Gulshan Swap Station' }, + { id: 'BK-004', name: 'Ovi Rahman', phone: '01712345681', stationId: 'SS-001', stationName: 'Banani Swap Station' }, +]; + +const mockStations: SwapStation[] = [ + { id: 'SS-001', name: 'Banani Swap Station', location: 'Block A, Road 3, Banani' }, + { id: 'SS-002', name: 'Gulshan Swap Station', location: 'Circle, Gulshan 2' }, + { id: 'SS-003', name: 'Uttara Swap Station', location: 'Sector 11, Uttara' }, + { id: 'SS-004', name: 'Dhanmondi Swap Station', location: 'Jashore Road, Dhanmondi' }, + { id: 'SS-005', name: 'Mirpur Swap Station', location: 'Mirpur 1, Section 2' }, +]; + +const statusColors: Record = { + available: 'bg-green-100 text-green-700', + 'in-use': 'bg-blue-100 text-blue-700', + maintenance: 'bg-amber-100 text-amber-700', + retired: 'bg-slate-100 text-slate-500', + charging: 'bg-purple-100 text-purple-700', +}; + +const typeLabels: Record = { + 'lithium-ion': 'Lithium-Ion', + 'lifepo4': 'LiFePO4', + 'lead-acid': 'Lead Acid', +}; + +export default function BatteriesPage() { + const [batteries, setBatteries] = useState(mockBatteries); + const [searchQuery, setSearchQuery] = useState(''); + const [statusFilter, setStatusFilter] = useState('all'); + const [typeFilter, setTypeFilter] = useState('all'); + const [selectedBattery, setSelectedBattery] = useState(null); + const [showModal, setShowModal] = useState(false); + const [showDetailsModal, setShowDetailsModal] = useState(false); + const [showHistoryModal, setShowHistoryModal] = useState(false); + const [showBMSModal, setShowBMSModal] = useState(false); + const [editingBattery, setEditingBattery] = useState(null); + const [viewMode, setViewMode] = useState<'table' | 'cards'>('table'); + + const availableCount = batteries.filter(b => b.status === 'available').length; + const inUseCount = batteries.filter(b => b.status === 'in-use').length; + const maintenanceCount = batteries.filter(b => b.status === 'maintenance').length; + const chargingCount = batteries.filter(b => b.status === 'charging').length; + const totalValue = batteries.reduce((sum, b) => sum + b.purchasePrice, 0); + + const filteredBatteries = batteries.filter(battery => { + const matchesSearch = battery.serialNumber.toLowerCase().includes(searchQuery.toLowerCase()) || + battery.brand.toLowerCase().includes(searchQuery.toLowerCase()) || + battery.model.toLowerCase().includes(searchQuery.toLowerCase()) || + battery.id.toLowerCase().includes(searchQuery.toLowerCase()); + const matchesStatus = statusFilter === 'all' || battery.status === statusFilter; + const matchesType = typeFilter === 'all' || battery.type === typeFilter; + return matchesSearch && matchesStatus && matchesType; + }); + + const handleAddBattery = () => { + setEditingBattery(null); + setShowModal(true); + }; + + const handleEditBattery = (battery: Battery) => { + setEditingBattery(battery); + setShowModal(true); + }; + + const handleViewDetails = (battery: Battery) => { + setSelectedBattery(battery); + setShowDetailsModal(true); + }; + + const handleViewHistory = (battery: Battery) => { + setSelectedBattery(battery); + setShowHistoryModal(true); + }; + + const handleReceiveBMSData = (battery: Battery) => { + setSelectedBattery(battery); + setShowBMSModal(true); + }; + + const handleDeleteBattery = (id: string) => { + if (confirm('Are you sure you want to delete this battery?')) { + setBatteries(batteries.filter(b => b.id !== id)); + } + }; + + const handleSaveBattery = (battery: Battery) => { + if (editingBattery) { + setBatteries(batteries.map(b => b.id === editingBattery.id ? battery : b)); + } else { + const newId = `BAT-${String(batteries.length + 1).padStart(3, '0')}`; + setBatteries([...batteries, { ...battery, id: newId, history: [] }]); + } + setShowModal(false); + }; + + const handleAssignToBike = (batteryId: string, bikeId: string, bikerId: string, stationId: string) => { + const bike = mockBikes.find(b => b.id === bikeId); + const biker = mockBikers.find(b => b.id === bikerId); + const station = mockStations.find(s => s.id === stationId); + + const newHistory: BatteryHistory = { + id: `H-${Date.now()}`, + batteryId, + bikeId, + bikeModel: bike?.model, + bikerId, + bikerName: biker?.name, + stationId, + stationName: station?.name, + action: 'assigned', + timestamp: new Date().toISOString().replace('T', ' ').substring(0, 19), + }; + + setBatteries(batteries.map(b => { + if (b.id === batteryId) { + return { + ...b, + status: 'in-use', + assignedBikeId: bikeId, + assignedBikeModel: bike?.model, + assignedBikerId: bikerId, + assignedBikerName: biker?.name, + currentStationId: stationId, + currentStationName: station?.name, + history: [newHistory, ...b.history], + }; + } + return b; + })); + setShowDetailsModal(false); + }; + + return ( +
+
+
+

Battery Management

+

Manage battery inventory, track BMS data, and assignment history

+
+
+ + +
+
+ +
+
+
+
+ +
+
+

{availableCount}

+

Available

+
+
+
+
+
+
+ +
+
+

{inUseCount}

+

In Use

+
+
+
+
+
+
+ +
+
+

{chargingCount}

+

Charging

+
+
+
+
+
+
+ +
+
+

{maintenanceCount}

+

Maintenance

+
+
+
+
+
+
+ +
+
+

৳{totalValue.toLocaleString()}

+

Total Value

+
+
+
+
+ +
+
+
+ + setSearchQuery(e.target.value)} + className="w-full pl-10 pr-4 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent" + /> +
+
+ + +
+ + +
+ +
+
+ + {viewMode === 'table' ? ( +
+ + + + + + + + + + + + + + {filteredBatteries.map(battery => ( + + + + + + + + + + ))} + +
BatterySpecsCurrent StatusAssignmentHealthPriceActions
+ +
+ +
+
+

{battery.brand} {battery.model}

+

{battery.serialNumber}

+
+ +
+

{battery.capacity}Ah / {battery.voltage}V

+

{typeLabels[battery.type]}

+
+
+ + {battery.status === 'in-use' ? : battery.status === 'charging' ? : null} + {battery.status} + + 50 ? 'text-green-600' : battery.currentSoc > 20 ? 'text-amber-600' : 'text-red-600'}`}> + {battery.currentSoc}% + +
+
+ {battery.assignedBikeId ? ( +
+

+ {battery.assignedBikeModel} +

+

+ {battery.assignedBikerName} +

+

+ {battery.currentStationName} +

+
+ ) : battery.currentStationName ? ( +

+ {battery.currentStationName} +

+ ) : ( + Unassigned + )} +
+
+
+
80 ? 'bg-green-500' : battery.health > 60 ? 'bg-amber-500' : 'bg-red-500'}`} + style={{ width: `${battery.health}%` }} + /> +
+ 80 ? 'text-green-600' : battery.health > 60 ? 'text-amber-600' : 'text-red-600'}`}> + {battery.health}% + +
+

{battery.cycleCount} cycles

+
+ ৳{battery.purchasePrice.toLocaleString()} + +
+ + + {battery.bmsData && ( + + )} + + +
+
+
+ ) : ( +
+ {filteredBatteries.map(battery => ( + +
+
+
+ +
+
+

{battery.brand} {battery.model}

+

{battery.serialNumber}

+
+
+ + {battery.status} + +
+ +
+
+ Capacity + {battery.capacity}Ah / {battery.voltage}V +
+
+ SOC + 50 ? 'text-green-600' : battery.currentSoc > 20 ? 'text-amber-600' : 'text-red-600'}`}>{battery.currentSoc}% +
+
+ Health + 80 ? 'text-green-600' : battery.health > 60 ? 'text-amber-600' : 'text-red-600'}`}>{battery.health}% +
+
+ Cycles + {battery.cycleCount} +
+
+ Price + ৳{battery.purchasePrice.toLocaleString()} +
+
+ + {battery.assignedBikeId && ( +
+

Current Assignment

+

{battery.assignedBikeModel}

+

{battery.assignedBikerName} • {battery.currentStationName}

+
+ )} + +
+ + + +
+ + ))} +
+ )} + +
+

+ Showing 1 to {filteredBatteries.length} of {batteries.length} batteries +

+
+ + + +
+
+
+ + {/* Add/Edit Modal */} + {showModal && ( +
+
+
+

+ {editingBattery ? 'Edit Battery' : 'Register New Battery'} +

+ +
+ setShowModal(false)} + /> +
+
+ )} + + {/* Details Modal */} + {showDetailsModal && selectedBattery && ( +
+
+
+

Battery Details

+ +
+ setShowDetailsModal(false)} + /> +
+
+ )} + + {/* History Modal */} + {showHistoryModal && selectedBattery && ( +
+
+
+

Battery History

+ +
+ setShowHistoryModal(false)} /> +
+
+ )} + + {/* BMS Data Modal */} + {showBMSModal && ( +
+
+
+

Receive BMS Data

+ +
+ { + setBatteries(batteries.map(b => + b.id === batteryId ? { ...b, bmsData } : b + )); + setShowBMSModal(false); + }} + onCancel={() => setShowBMSModal(false)} + /> +
+
+ )} +
+ ); +} + +function BatteryForm({ battery, onSave, onCancel }: { battery: Battery | null; onSave: (battery: Battery) => void; onCancel: () => void }) { + const [formData, setFormData] = useState(battery || { + id: '', + serialNumber: '', + brand: '', + model: '', + type: 'lithium-ion', + capacity: 50, + voltage: 60, + purchaseDate: new Date().toISOString().split('T')[0], + purchasePrice: 0, + warrantyExpiry: '', + status: 'available', + currentSoc: 100, + health: 100, + cycleCount: 0, + history: [], + }); + + const handleChange = (field: keyof Battery, value: any) => { + setFormData({ ...formData, [field]: value }); + }; + + return ( +
+
+
+ + handleChange('serialNumber', e.target.value)} + placeholder="SN-2024-00001" + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent" + /> +
+
+ + handleChange('brand', e.target.value)} + placeholder="EVE Energy" + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent" + /> +
+
+ + handleChange('model', e.target.value)} + placeholder="Li-Ion 60V50Ah" + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent" + /> +
+
+ + +
+
+ + handleChange('capacity', parseInt(e.target.value))} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent" + /> +
+
+ + handleChange('voltage', parseInt(e.target.value))} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent" + /> +
+
+ + handleChange('purchaseDate', e.target.value)} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent" + /> +
+
+ + handleChange('purchasePrice', parseInt(e.target.value))} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent" + /> +
+
+ + handleChange('warrantyExpiry', e.target.value)} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent" + /> +
+
+ + handleChange('currentSoc', parseInt(e.target.value))} + max={100} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent" + /> +
+
+ + +
+
+
+ + +
+
+ ); +} + +function BatteryDetails({ + battery, + bikes, + bikers, + stations, + onAssign, + onClose +}: { + battery: Battery; + bikes: Bike[]; + bikers: Biker[]; + stations: SwapStation[]; + onAssign: (batteryId: string, bikeId: string, bikerId: string, stationId: string) => void; + onClose: () => void; +}) { + const [selectedBike, setSelectedBike] = useState(battery.assignedBikeId || ''); + const [selectedBiker, setSelectedBiker] = useState(battery.assignedBikerId || ''); + const [selectedStation, setSelectedStation] = useState(battery.currentStationId || ''); + + const handleAssign = () => { + if (selectedBike && selectedBiker && selectedStation) { + onAssign(battery.id, selectedBike, selectedBiker, selectedStation); + } + }; + + const availableBikes = bikes.filter(b => b.status === 'rented' || b.status === 'available'); + const stationBikers = bikers.filter(b => b.stationId === selectedStation); + + return ( +
+
+
+ +
+
+

{battery.brand} {battery.model}

+

{battery.serialNumber}

+ + {battery.status} + +
+
+ +
+
+

Capacity

+

{battery.capacity}Ah / {battery.voltage}V

+
+
+

Type

+

{typeLabels[battery.type]}

+
+
+

Current SOC

+

50 ? 'text-green-600' : battery.currentSoc > 20 ? 'text-amber-600' : 'text-red-600'}`}>{battery.currentSoc}%

+
+
+

Health

+

80 ? 'text-green-600' : battery.health > 60 ? 'text-amber-600' : 'text-red-600'}`}>{battery.health}%

+
+
+

Cycle Count

+

{battery.cycleCount} cycles

+
+
+

Purchase Price

+

৳{battery.purchasePrice.toLocaleString()}

+
+
+ + {battery.bmsData && ( +
+

+ Live BMS Data +

+
+
+

Voltage

+

{battery.bmsData.voltage}V

+
+
+

Current

+

{battery.bmsData.current}A

+
+
+

Temperature

+

{battery.bmsData.temperature}°C

+
+
+

SOC

+

{battery.bmsData.soc}%

+
+
+

Cycles

+

{battery.bmsData.cycles}

+
+
+

Health

+

80 ? 'text-green-600' : 'text-amber-600'}`}>{battery.bmsData.health}%

+
+
+
+ )} + + {battery.status !== 'retired' && ( +
+

+ Assign to Bike & Biker +

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ )} + + +
+ ); +} + +function BatteryHistoryView({ battery, onClose }: { battery: Battery; onClose: () => void }) { + return ( +
+
+
+ +
+
+

{battery.brand} {battery.model}

+

{battery.serialNumber}

+
+
+ + {battery.history.length === 0 ? ( +
+ +

No history records found

+
+ ) : ( +
+ {battery.history.map((record, index) => ( +
+
+ {index > 0 &&
} +
+
+ + {record.action === 'assigned' && } + {record.action === 'swapped' && } + {record.action === 'maintenance' && } + {record.action === 'retired' && } + {record.action} + + {record.timestamp} +
+ {record.bikeModel && ( +

+ {record.bikeModel} +

+ )} + {record.bikerName && ( +

+ {record.bikerName} +

+ )} + {record.stationName && ( +

+ {record.stationName} +

+ )} + {record.notes && ( +

{record.notes}

+ )} +
+
+ ))} +
+ )} + + +
+ ); +} + +function BMSDataReceiver({ + batteries, + onReceive, + onCancel +}: { + batteries: Battery[]; + onReceive: (batteryId: string, bmsData: BMSData) => void; + onCancel: () => void; +}) { + const [selectedBatteryId, setSelectedBatteryId] = useState(''); + const [isReceiving, setIsReceiving] = useState(false); + const [receivedData, setReceivedData] = useState(null); + + const simulateBMSData = (): BMSData => { + const baseVoltage = 60 + Math.random() * 10; + const baseSoc = Math.floor(Math.random() * 40) + 60; + const baseTemp = 25 + Math.floor(Math.random() * 15); + const baseCurrent = -3 + Math.random() * 4; + const baseCycles = Math.floor(Math.random() * 300); + const baseHealth = 70 + Math.floor(Math.random() * 30); + + return { + voltage: Math.round(baseVoltage * 10) / 10, + current: Math.round(baseCurrent * 10) / 10, + soc: baseSoc, + temperature: baseTemp, + cycles: baseCycles, + health: baseHealth, + timestamp: new Date().toISOString().replace('T', ' ').substring(0, 19), + }; + }; + + const handleReceiveData = () => { + if (!selectedBatteryId) return; + setIsReceiving(true); + setTimeout(() => { + const data = simulateBMSData(); + setReceivedData(data); + setIsReceiving(false); + }, 1500); + }; + + const handleSaveData = () => { + if (selectedBatteryId && receivedData) { + onReceive(selectedBatteryId, receivedData); + } + }; + + return ( +
+
+
+ +
+

BMS Data Integration

+

Connect to digital BMS to receive real-time battery data. Data will be processed and stored for monitoring.

+
+
+
+ +
+
+ + +
+ + + + {receivedData && ( +
+

+ Received Data +

+
+
+

Voltage

+

{receivedData.voltage}V

+
+
+

Current

+

{receivedData.current}A

+
+
+

SOC

+

{receivedData.soc}%

+
+
+

Temperature

+

{receivedData.temperature}°C

+
+
+

Cycles

+

{receivedData.cycles}

+
+
+

Health

+

80 ? 'text-green-600' : 'text-amber-600'}`}>{receivedData.health}%

+
+
+

Received at: {receivedData.timestamp}

+
+ )} +
+ +
+ + {receivedData && ( + + )} +
+
+ ); +} + +function Wrench({ className }: { className?: string }) { + return ( + + + + ); +} \ No newline at end of file diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 712e74b..138afca 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -47,6 +47,7 @@ const adminNavItems: NavItem[] = [ { label: 'Bikers', href: '/admin/bikers', icon: Users }, { label: 'Investors', href: '/admin/investors', icon: Wallet }, { label: 'Fleet Management', href: '/admin/fleet', icon: Bike }, + { label: 'Battery Management', href: '/admin/batteries', icon: Battery }, { label: 'Merchants (P2)', href: '/admin/merchants', icon: Store }, { label: 'Swap Stations (P3)', href: '/admin/swap-stations', icon: Zap },