feat: add battery management tab with support for viewing, adding, editing, and deleting batteries in hub dashboard

This commit is contained in:
sazzadulalambd
2026-05-17 20:24:47 +06:00
parent 89300a457e
commit a4ff86b953

View File

@@ -4,7 +4,7 @@ import { useState, useEffect } from 'react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { import {
ArrowLeft, MapPin, Phone, Clock, Bike, Plus, X, Edit, Save, Trash2, ArrowLeft, MapPin, Phone, Clock, Bike, Plus, X, Edit, Save, Trash2,
Navigation, User, Wallet, DollarSign, CheckCircle, AlertTriangle Navigation, User, Wallet, DollarSign, CheckCircle, AlertTriangle, Battery
} from 'lucide-react'; } from 'lucide-react';
interface Hub { interface Hub {
@@ -29,6 +29,17 @@ interface BikeInfo {
status: 'available' | 'rented' | 'maintenance'; status: 'available' | 'rented' | 'maintenance';
} }
interface BatteryInfo {
id: string;
brand: string;
model: string;
serialNumber: string;
status: 'available' | 'charging' | 'deployed' | 'maintenance';
chargeLevel: number;
assignedBike?: string;
assignedDate?: string;
}
const mockHub: Hub = { const mockHub: Hub = {
id: 'HUB-001', id: 'HUB-001',
name: 'JAIBEN Head Office', name: 'JAIBEN Head Office',
@@ -52,6 +63,15 @@ const mockHubBikes: BikeInfo[] = [
{ id: 'BIKE-005', model: 'Yadea G5', plate: 'Dhaka Metro Ha-5678', status: 'maintenance' }, { id: 'BIKE-005', model: 'Yadea G5', plate: 'Dhaka Metro Ha-5678', status: 'maintenance' },
]; ];
const mockHubBatteries: BatteryInfo[] = [
{ id: 'BAT-001', brand: 'Lithium', model: '60V/30Ah', serialNumber: 'LTH-2024-001', status: 'available', chargeLevel: 95 },
{ id: 'BAT-002', brand: 'Lithium', model: '60V/30Ah', serialNumber: 'LTH-2024-002', status: 'charging', chargeLevel: 75 },
{ id: 'BAT-003', brand: 'Lithium', model: '60V/40Ah', serialNumber: 'LTH-2024-003', status: 'deployed', chargeLevel: 45, assignedBike: 'BIKE-002', assignedDate: '2024-03-15' },
{ id: 'BAT-004', brand: 'Lithium', model: '60V/30Ah', serialNumber: 'LTH-2024-004', status: 'available', chargeLevel: 88 },
{ id: 'BAT-005', brand: 'Lithium', model: '48V/25Ah', serialNumber: 'LTH-2024-005', status: 'maintenance', chargeLevel: 0 },
{ id: 'BAT-006', brand: 'Lithium', model: '60V/30Ah', serialNumber: 'LTH-2024-006', status: 'deployed', chargeLevel: 62, assignedBike: 'BIKE-004', assignedDate: '2024-03-18' },
];
interface RentalInfo { interface RentalInfo {
id: string; id: string;
userName: string; userName: string;
@@ -77,10 +97,20 @@ export default function HubDetailPage() {
const [hub, setHub] = useState<Hub>(mockHub); const [hub, setHub] = useState<Hub>(mockHub);
const [bikes, setBikes] = useState<BikeInfo[]>(mockHubBikes); const [bikes, setBikes] = useState<BikeInfo[]>(mockHubBikes);
const [batteries, setBatteries] = useState<BatteryInfo[]>(mockHubBatteries);
const [rentals, setRentals] = useState<RentalInfo[]>(mockHubRentals); const [rentals, setRentals] = useState<RentalInfo[]>(mockHubRentals);
const [editMode, setEditMode] = useState(false); const [editMode, setEditMode] = useState(false);
const [editForm, setEditForm] = useState(hub); const [editForm, setEditForm] = useState(hub);
const [activeTab, setActiveTab] = useState<'overview' | 'bikes' | 'rentals'>('overview'); const [activeTab, setActiveTab] = useState<'overview' | 'bikes' | 'batteries' | 'rentals'>('overview');
const [assignModal, setAssignModal] = useState<BatteryInfo | null>(null);
const [selectedBike, setSelectedBike] = useState('');
const [addBikeModal, setAddBikeModal] = useState(false);
const [addBatteryModal, setAddBatteryModal] = useState(false);
const [editingBike, setEditingBike] = useState<BikeInfo | null>(null);
const [editingBattery, setEditingBattery] = useState<BatteryInfo | null>(null);
const [bikeForm, setBikeForm] = useState<{ model: string; plate: string; status: 'available' | 'rented' | 'maintenance' }>({ model: '', plate: '', status: 'available' });
const [batteryForm, setBatteryForm] = useState<{ brand: string; model: string; serialNumber: string; chargeLevel: number; status: 'available' | 'charging' | 'deployed' | 'maintenance' }>({ brand: '', model: '', serialNumber: '', chargeLevel: 100, status: 'available' });
const [deleteModal, setDeleteModal] = useState<{ type: 'bike' | 'battery'; item: BikeInfo | BatteryInfo } | null>(null);
const handleSaveEdit = () => { const handleSaveEdit = () => {
setHub(editForm); setHub(editForm);
@@ -170,6 +200,15 @@ export default function HubDetailPage() {
> >
Bikes ({bikes.length}) Bikes ({bikes.length})
</button> </button>
<button
onClick={() => setActiveTab('batteries')}
className={`py-4 text-sm font-medium border-b-2 transition-colors ${activeTab === 'batteries'
? 'border-accent text-accent'
: 'border-transparent text-slate-500 hover:text-slate-700'
}`}
>
Batteries ({batteries.length})
</button>
<button <button
onClick={() => setActiveTab('rentals')} onClick={() => setActiveTab('rentals')}
className={`py-4 text-sm font-medium border-b-2 transition-colors ${activeTab === 'rentals' className={`py-4 text-sm font-medium border-b-2 transition-colors ${activeTab === 'rentals'
@@ -341,7 +380,7 @@ export default function HubDetailPage() {
<div> <div>
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-slate-800">Hub Bikes ({bikes.length})</h3> <h3 className="font-semibold text-slate-800">Hub Bikes ({bikes.length})</h3>
<button className="px-4 py-2 bg-accent text-white rounded-lg text-sm flex items-center gap-2"> <button onClick={() => { setBikeForm({ model: '', plate: '', status: 'available' }); setAddBikeModal(true); }} className="px-4 py-2 bg-accent text-white rounded-lg text-sm flex items-center gap-2">
<Plus className="w-4 h-4" /> Add Bike <Plus className="w-4 h-4" /> Add Bike
</button> </button>
</div> </div>
@@ -360,6 +399,73 @@ export default function HubDetailPage() {
<p className="font-medium text-slate-800">{bike.model}</p> <p className="font-medium text-slate-800">{bike.model}</p>
<p className="text-sm text-slate-500">{bike.plate}</p> <p className="text-sm text-slate-500">{bike.plate}</p>
<p className="text-xs text-slate-400 mt-2">ID: {bike.id}</p> <p className="text-xs text-slate-400 mt-2">ID: {bike.id}</p>
<div className="flex gap-2 mt-3">
<button onClick={() => { setEditingBike(bike); setBikeForm({ model: bike.model, plate: bike.plate, status: bike.status }); setAddBikeModal(true); }} className="flex-1 py-1.5 text-xs font-medium text-blue-600 border border-blue-200 rounded-lg hover:bg-blue-50">Edit</button>
<button onClick={() => setDeleteModal({ type: 'bike', item: bike })} className="flex-1 py-1.5 text-xs font-medium text-red-600 border border-red-200 rounded-lg hover:bg-red-50">Delete</button>
</div>
</div>
))}
</div>
</div>
)}
{activeTab === 'batteries' && (
<div>
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-slate-800">Hub Batteries ({batteries.length})</h3>
<button onClick={() => { setBatteryForm({ brand: '', model: '', serialNumber: '', chargeLevel: 100, status: 'available' }); setAddBatteryModal(true); }} className="px-4 py-2 bg-accent text-white rounded-lg text-sm flex items-center gap-2">
<Plus className="w-4 h-4" /> Add Battery
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{batteries.map(battery => (
<div key={battery.id} className="bg-slate-50 p-4 rounded-xl border border-slate-100">
<div className="flex items-center justify-between mb-2">
<Battery className="w-5 h-5 text-slate-400" />
<span className={`text-xs font-medium px-2 py-1 rounded-full ${battery.status === 'available' ? 'bg-green-100 text-green-700' :
battery.status === 'charging' ? 'bg-blue-100 text-blue-700' :
battery.status === 'deployed' ? 'bg-amber-100 text-amber-700' :
'bg-red-100 text-red-700'
}`}>
{battery.status}
</span>
</div>
<p className="font-medium text-slate-800">{battery.brand} {battery.model}</p>
<p className="text-sm text-slate-500">SN: {battery.serialNumber}</p>
<div className="mt-2">
<div className="flex items-center justify-between text-xs mb-1">
<span className="text-slate-500">Charge</span>
<span className={`font-medium ${battery.chargeLevel > 50 ? 'text-green-600' : battery.chargeLevel > 20 ? 'text-amber-600' : 'text-red-600'}`}>
{battery.chargeLevel}%
</span>
</div>
<div className="h-1.5 bg-slate-200 rounded-full overflow-hidden">
<div className={`h-full rounded-full ${battery.chargeLevel > 50 ? 'bg-green-500' : battery.chargeLevel > 20 ? 'bg-amber-500' : 'bg-red-500'}`} style={{ width: `${battery.chargeLevel}%` }} />
</div>
</div>
{battery.assignedBike && (
<p className="text-xs text-slate-400 mt-2">Assigned to: {battery.assignedBike}</p>
)}
<div className="mt-2 flex gap-2">
<button
onClick={() => { setAssignModal(battery); setSelectedBike(battery.assignedBike || ''); }}
className="flex-1 py-1.5 text-xs font-medium text-accent border border-accent rounded-lg hover:bg-accent hover:text-white transition-colors"
>
{battery.assignedBike ? 'Reassign' : 'Assign'}
</button>
{battery.assignedBike && (
<button
onClick={() => { setBatteries(prev => prev.map(b => b.id === battery.id ? { ...b, assignedBike: undefined, assignedDate: undefined, status: 'available' as const } : b)); }}
className="py-1.5 px-2 text-xs font-medium text-orange-600 border border-orange-200 rounded-lg hover:bg-orange-50"
>
Unassign
</button>
)}
</div>
<div className="flex gap-2 mt-2">
<button onClick={() => { setEditingBattery(battery); setBatteryForm({ brand: battery.brand, model: battery.model, serialNumber: battery.serialNumber, chargeLevel: battery.chargeLevel, status: battery.status }); setAddBatteryModal(true); }} className="flex-1 py-1.5 text-xs font-medium text-blue-600 border border-blue-200 rounded-lg hover:bg-blue-50">Edit</button>
<button onClick={() => setDeleteModal({ type: 'battery', item: battery })} className="flex-1 py-1.5 text-xs font-medium text-red-600 border border-red-200 rounded-lg hover:bg-red-50">Delete</button>
</div>
</div> </div>
))} ))}
</div> </div>
@@ -370,9 +476,9 @@ export default function HubDetailPage() {
<div> <div>
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-slate-800">Hub Rentals ({rentals.length})</h3> <h3 className="font-semibold text-slate-800">Hub Rentals ({rentals.length})</h3>
<button className="px-4 py-2 bg-accent text-white rounded-lg text-sm flex items-center gap-2"> {/* <button className="px-4 py-2 bg-accent text-white rounded-lg text-sm flex items-center gap-2">
<Plus className="w-4 h-4" /> New Rental <Plus className="w-4 h-4" /> New Rental
</button> </button> */}
</div> </div>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full"> <table className="w-full">
@@ -416,8 +522,7 @@ export default function HubDetailPage() {
<span className="text-sm font-medium text-green-600">৳{rental.totalPaid.toLocaleString()}</span> <span className="text-sm font-medium text-green-600">৳{rental.totalPaid.toLocaleString()}</span>
</td> </td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<span className={`text-xs font-medium px-2.5 py-1 rounded-full ${ <span className={`text-xs font-medium px-2.5 py-1 rounded-full ${rental.status === 'active' ? 'bg-green-100 text-green-700' :
rental.status === 'active' ? 'bg-green-100 text-green-700' :
rental.status === 'pending' ? 'bg-amber-100 text-amber-700' : rental.status === 'pending' ? 'bg-amber-100 text-amber-700' :
'bg-blue-100 text-blue-700' 'bg-blue-100 text-blue-700'
}`}> }`}>
@@ -433,6 +538,213 @@ export default function HubDetailPage() {
)} )}
</div> </div>
</div> </div>
{assignModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
<div className="p-4 border-b border-slate-100 flex justify-between items-center">
<h3 className="font-semibold text-slate-800">Assign Battery to Bike</h3>
<button onClick={() => setAssignModal(null)} className="text-slate-400 hover:text-slate-600">
<X className="w-5 h-5" />
</button>
</div>
<div className="p-4 space-y-4">
<div className="bg-slate-50 p-3 rounded-lg">
<p className="text-sm text-slate-500">Battery</p>
<p className="font-medium text-slate-800">{assignModal.brand} {assignModal.model}</p>
<p className="text-xs text-slate-500">SN: {assignModal.serialNumber}</p>
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Select Bike</label>
<select
value={selectedBike}
onChange={(e) => setSelectedBike(e.target.value)}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
>
<option value="">-- Select a bike --</option>
{bikes.filter(b => b.status !== 'maintenance').map(bike => (
<option key={bike.id} value={bike.id}>
{bike.model} - {bike.plate}
</option>
))}
</select>
</div>
{selectedBike && (
<div className="bg-green-50 p-3 rounded-lg">
<p className="text-sm text-green-600">Battery will be assigned to:</p>
<p className="font-medium text-green-800">
{bikes.find(b => b.id === selectedBike)?.model} ({bikes.find(b => b.id === selectedBike)?.plate})
</p>
</div>
)}
</div>
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
<button onClick={() => setAssignModal(null)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">Cancel</button>
<button
onClick={() => {
setBatteries(prev => prev.map(b => b.id === assignModal.id ? {
...b,
assignedBike: selectedBike || undefined,
assignedDate: selectedBike ? new Date().toISOString().split('T')[0] : undefined,
status: selectedBike ? 'deployed' as const : 'available' as const
} : b));
setAssignModal(null);
}}
className="px-4 py-2 bg-accent text-white rounded-lg text-sm hover:bg-accent-dark"
>
{assignModal.assignedBike ? 'Update Assignment' : 'Assign Battery'}
</button>
</div>
</div>
</div>
)}
{addBikeModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
<div className="p-4 border-b border-slate-100 flex justify-between items-center">
<h3 className="font-semibold text-slate-800">{editingBike ? 'Edit Bike' : 'Add New Bike'}</h3>
<button onClick={() => { setAddBikeModal(false); setEditingBike(null); }} className="text-slate-400 hover:text-slate-600">
<X className="w-5 h-5" />
</button>
</div>
<div className="p-4 space-y-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Model</label>
<input type="text" value={bikeForm.model} onChange={(e) => setBikeForm(f => ({ ...f, model: e.target.value }))} placeholder="e.g. AIMA Lightning" disabled={!!editingBike} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm disabled:bg-slate-100 disabled:text-slate-500" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">License Plate</label>
<input type="text" value={bikeForm.plate} onChange={(e) => setBikeForm(f => ({ ...f, plate: e.target.value }))} placeholder="e.g. Dhaka Metro Cha-1234" disabled={!!editingBike} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm disabled:bg-slate-100 disabled:text-slate-500" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Status</label>
<select value={bikeForm.status} onChange={(e) => setBikeForm(f => ({ ...f, status: e.target.value as any }))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
<option value="available">Available</option>
<option value="rented">Rented</option>
<option value="maintenance">Maintenance</option>
</select>
</div>
</div>
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
<button onClick={() => { setAddBikeModal(false); setEditingBike(null); }} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">Cancel</button>
<button
onClick={() => {
if (editingBike) {
setBikes(prev => prev.map(b => b.id === editingBike.id ? { ...b, ...bikeForm } : b));
} else {
const newBike: BikeInfo = { id: `BIKE-${Date.now()}`, ...bikeForm };
setBikes(prev => [...prev, newBike]);
}
setAddBikeModal(false);
setEditingBike(null);
}}
disabled={!bikeForm.model || !bikeForm.plate}
className="px-4 py-2 bg-accent text-white rounded-lg text-sm hover:bg-accent-dark disabled:opacity-50"
>
{editingBike ? 'Update Bike' : 'Add Bike'}
</button>
</div>
</div>
</div>
)}
{addBatteryModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
<div className="p-4 border-b border-slate-100 flex justify-between items-center">
<h3 className="font-semibold text-slate-800">{editingBattery ? 'Edit Battery' : 'Add New Battery'}</h3>
<button onClick={() => { setAddBatteryModal(false); setEditingBattery(null); }} className="text-slate-400 hover:text-slate-600">
<X className="w-5 h-5" />
</button>
</div>
<div className="p-4 space-y-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Brand</label>
<input type="text" value={batteryForm.brand} onChange={(e) => setBatteryForm(f => ({ ...f, brand: e.target.value }))} placeholder="e.g. Lithium" disabled={!!editingBattery} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm disabled:bg-slate-100 disabled:text-slate-500" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Model</label>
<input type="text" value={batteryForm.model} onChange={(e) => setBatteryForm(f => ({ ...f, model: e.target.value }))} placeholder="e.g. 60V/30Ah" disabled={!!editingBattery} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm disabled:bg-slate-100 disabled:text-slate-500" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Serial Number</label>
<input type="text" value={batteryForm.serialNumber} onChange={(e) => setBatteryForm(f => ({ ...f, serialNumber: e.target.value }))} placeholder="e.g. LTH-2024-001" disabled={!!editingBattery} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm disabled:bg-slate-100 disabled:text-slate-500" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Charge Level (%)</label>
<input type="number" min="0" max="100" value={batteryForm.chargeLevel} onChange={(e) => setBatteryForm(f => ({ ...f, chargeLevel: parseInt(e.target.value) || 0 }))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Status</label>
<select value={batteryForm.status} onChange={(e) => setBatteryForm(f => ({ ...f, status: e.target.value as any }))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
<option value="available">Available</option>
<option value="charging">Charging</option>
<option value="deployed">Deployed</option>
<option value="maintenance">Maintenance</option>
</select>
</div>
</div>
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
<button onClick={() => { setAddBatteryModal(false); setEditingBattery(null); }} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">Cancel</button>
<button
onClick={() => {
if (editingBattery) {
setBatteries(prev => prev.map(b => b.id === editingBattery.id ? { ...b, ...batteryForm } : b));
} else {
const newBattery: BatteryInfo = { id: `BAT-${Date.now()}`, ...batteryForm, assignedBike: undefined, assignedDate: undefined };
setBatteries(prev => [...prev, newBattery]);
}
setAddBatteryModal(false);
setEditingBattery(null);
}}
disabled={!batteryForm.brand || !batteryForm.model || !batteryForm.serialNumber}
className="px-4 py-2 bg-accent text-white rounded-lg text-sm hover:bg-accent-dark disabled:opacity-50"
>
{editingBattery ? 'Update Battery' : 'Add Battery'}
</button>
</div>
</div>
</div>
)}
{deleteModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-xl w-full max-w-sm">
<div className="p-6 text-center">
<div className="w-12 h-12 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
<AlertTriangle className="w-6 h-6 text-red-600" />
</div>
<h3 className="font-semibold text-slate-800 mb-2">Confirm Delete</h3>
<p className="text-sm text-slate-500">
Are you sure you want to delete this {deleteModal.type === 'bike' ? 'bike' : 'battery'}?
{deleteModal.type === 'bike' && (
<span className="block mt-1 font-medium text-slate-700">{(deleteModal.item as BikeInfo).model} - {(deleteModal.item as BikeInfo).plate}</span>
)}
{deleteModal.type === 'battery' && (
<span className="block mt-1 font-medium text-slate-700">{(deleteModal.item as BatteryInfo).brand} {(deleteModal.item as BatteryInfo).model}</span>
)}
</p>
</div>
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
<button onClick={() => setDeleteModal(null)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">Cancel</button>
<button
onClick={() => {
if (deleteModal.type === 'bike') {
setBikes(prev => prev.filter(b => b.id !== (deleteModal.item as BikeInfo).id));
} else {
setBatteries(prev => prev.filter(b => b.id !== (deleteModal.item as BatteryInfo).id));
}
setDeleteModal(null);
}}
className="px-4 py-2 bg-red-600 text-white rounded-lg text-sm hover:bg-red-700"
>
Delete
</button>
</div>
</div>
</div>
)}
</div> </div>
); );
} }