feat: add battery management tab with support for viewing, adding, editing, and deleting batteries in hub dashboard
This commit is contained in:
@@ -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);
|
||||||
@@ -155,8 +185,8 @@ export default function HubDetailPage() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('overview')}
|
onClick={() => setActiveTab('overview')}
|
||||||
className={`py-4 text-sm font-medium border-b-2 transition-colors ${activeTab === 'overview'
|
className={`py-4 text-sm font-medium border-b-2 transition-colors ${activeTab === 'overview'
|
||||||
? 'border-accent text-accent'
|
? 'border-accent text-accent'
|
||||||
: 'border-transparent text-slate-500 hover:text-slate-700'
|
: 'border-transparent text-slate-500 hover:text-slate-700'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Overview
|
Overview
|
||||||
@@ -164,17 +194,26 @@ export default function HubDetailPage() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('bikes')}
|
onClick={() => setActiveTab('bikes')}
|
||||||
className={`py-4 text-sm font-medium border-b-2 transition-colors ${activeTab === 'bikes'
|
className={`py-4 text-sm font-medium border-b-2 transition-colors ${activeTab === 'bikes'
|
||||||
? 'border-accent text-accent'
|
? 'border-accent text-accent'
|
||||||
: 'border-transparent text-slate-500 hover:text-slate-700'
|
: 'border-transparent text-slate-500 hover:text-slate-700'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
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'
|
||||||
? 'border-accent text-accent'
|
? 'border-accent text-accent'
|
||||||
: 'border-transparent text-slate-500 hover:text-slate-700'
|
: 'border-transparent text-slate-500 hover:text-slate-700'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Rentals ({rentals.length})
|
Rentals ({rentals.length})
|
||||||
@@ -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>
|
||||||
@@ -351,8 +390,8 @@ export default function HubDetailPage() {
|
|||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<Bike className="w-5 h-5 text-slate-400" />
|
<Bike className="w-5 h-5 text-slate-400" />
|
||||||
<span className={`text-xs font-medium px-2 py-1 rounded-full ${bike.status === 'available' ? 'bg-green-100 text-green-700' :
|
<span className={`text-xs font-medium px-2 py-1 rounded-full ${bike.status === 'available' ? 'bg-green-100 text-green-700' :
|
||||||
bike.status === 'rented' ? 'bg-amber-100 text-amber-700' :
|
bike.status === 'rented' ? 'bg-amber-100 text-amber-700' :
|
||||||
'bg-red-100 text-red-700'
|
'bg-red-100 text-red-700'
|
||||||
}`}>
|
}`}>
|
||||||
{bike.status}
|
{bike.status}
|
||||||
</span>
|
</span>
|
||||||
@@ -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,11 +522,10 @@ 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'
|
}`}>
|
||||||
}`}>
|
|
||||||
{rental.status}
|
{rental.status}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user