refactor: replace revenue and geofence pages with new hub management system

This commit is contained in:
sazzadulalambd
2026-04-26 16:02:53 +06:00
parent f724a00453
commit 03062bfc48
8 changed files with 884 additions and 291 deletions

View File

@@ -17,7 +17,9 @@ interface Bike {
plateNumber: string;
status: 'available' | 'rented' | 'maintenance' | 'retired';
batteryLevel: number;
location: string;
location?: string; // deprecated - use hubId/hubName
hubId?: string;
hubName?: string;
assignedTo?: string;
investorId?: string;
purchaseDate?: string;
@@ -33,7 +35,7 @@ interface Bike {
}
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', investorId: 'inv1', purchaseDate: '2024-01-15', purchasePrice: 125000, currentRent: 350, totalRides: 156, totalDistance: 2340, 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, hubId: 'HUB-001', hubName: 'JAIBEN Head Office', assignedTo: 'Rahim Ahmed', investorId: 'inv1', purchaseDate: '2024-01-15', purchasePrice: 125000, currentRent: 350, totalRides: 156, totalDistance: 2340, lastService: '2024-03-01', nextService: '2024-04-01', insuranceExpiry: '2025-01-15', registrationExpiry: '2026-01-15' },
{ id: 'EV002', model: 'Yadea DT3', brand: 'Yadea', image: '', plateNumber: 'Dhaka Metro Cha-A-5678', status: 'available', batteryLevel: 95, location: 'Banani', purchaseDate: '2024-02-01', purchasePrice: 118000, totalRides: 89, totalDistance: 1567, lastService: '2024-03-15', nextService: '2024-04-15', insuranceExpiry: '2025-02-01', registrationExpiry: '2026-02-01' },
{ id: 'EV003', model: 'AIMA Lightning', brand: 'AIMA', image: '', plateNumber: 'Dhaka Metro Cha-A-9012', status: 'rented', batteryLevel: 62, location: 'Uttara', assignedTo: 'Karim Singh', investorId: 'inv1', purchaseDate: '2024-01-20', purchasePrice: 132000, currentRent: 400, totalRides: 203, totalDistance: 3890, lastService: '2024-03-10', nextService: '2024-04-10', insuranceExpiry: '2025-01-20', registrationExpiry: '2026-01-20' },
{ id: 'EV004', model: 'TVS iQube', brand: 'TVS', image: '', plateNumber: 'Dhaka Metro Cha-A-3456', status: 'maintenance', batteryLevel: 45, location: 'Workshop - Banani', purchaseDate: '2023-12-10', purchasePrice: 145000, totalRides: 312, totalDistance: 5670, lastService: '2024-03-20', nextService: '2024-03-25', insuranceExpiry: '2024-12-10', registrationExpiry: '2025-12-10', notes: 'Motor issue - awaiting parts' },
@@ -47,6 +49,13 @@ const mockBikes: Bike[] = [
{ id: 'EV012', model: 'Revolt RV400', brand: 'Revolt', image: '', plateNumber: 'Dhaka Metro Cha-C-5679', status: 'rented', batteryLevel: 55, location: 'Dhanmondi', assignedTo: 'Tashrif Islam', investorId: 'inv2', purchaseDate: '2024-01-25', purchasePrice: 150000, currentRent: 420, totalRides: 198, totalDistance: 3560, lastService: '2024-03-08', nextService: '2024-04-08', insuranceExpiry: '2025-01-25', registrationExpiry: '2026-01-25' },
];
const hubs = [
{ id: 'HUB-001', name: 'JAIBEN Head Office' },
{ id: 'HUB-002', name: 'Banani Hub' },
{ id: 'HUB-003', name: 'Uttara Hub' },
{ id: 'HUB-004', name: 'Mirpur Hub' },
];
const statusColors: Record<string, string> = {
available: 'bg-green-100 text-green-700',
rented: 'bg-blue-100 text-blue-700',
@@ -71,7 +80,7 @@ export default function FleetPage() {
const maintenanceCount = bikes.filter(b => b.status === 'maintenance').length;
const retiredCount = bikes.filter(b => b.status === 'retired').length;
const locations = [...new Set(bikes.map(b => b.location))];
const locations = [...new Set(bikes.map(b => b.hubName || b.location).filter(Boolean))];
const filteredBikes = bikes.filter(bike => {
const matchesSearch = bike.model.toLowerCase().includes(searchQuery.toLowerCase()) ||
@@ -79,7 +88,7 @@ export default function FleetPage() {
bike.plateNumber.toLowerCase().includes(searchQuery.toLowerCase()) ||
bike.id.toLowerCase().includes(searchQuery.toLowerCase());
const matchesStatus = statusFilter === 'all' || bike.status === statusFilter;
const matchesLocation = locationFilter === 'all' || bike.location === locationFilter;
const matchesLocation = locationFilter === 'all' || bike.hubName === locationFilter || bike.location === locationFilter;
return matchesSearch && matchesStatus && matchesLocation;
});
@@ -303,7 +312,7 @@ export default function FleetPage() {
<tr>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Bike</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Plate Number</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Location</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Hub</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Battery</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Metrics</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Status</th>
@@ -329,7 +338,7 @@ export default function FleetPage() {
</td>
<td className="px-4 py-3">
<p className="text-sm text-slate-600 flex items-center gap-1">
<MapPin className="w-3 h-3" /> {bike.location}
<MapPin className="w-3 h-3" /> {bike.hubName || bike.location || 'Not Assigned'}
</p>
</td>
<td className="px-4 py-3">
@@ -398,7 +407,7 @@ export default function FleetPage() {
</div>
<div className="flex items-center justify-between text-xs">
<span className="text-slate-500">Location</span>
<span className="font-medium text-slate-700">{bike.location}</span>
<span className="font-medium text-slate-700">{bike.hubName || bike.location || 'Not Assigned'}</span>
</div>
<div className="flex items-center justify-between text-xs">
<span className="text-slate-500">Battery</span>
@@ -479,7 +488,9 @@ function BikeForm({ bike, onSave, onCancel }: { bike: Bike | null; onSave: (bike
plateNumber: '',
status: 'available',
batteryLevel: 100,
location: '',
location: '', // deprecated
hubId: '',
hubName: '',
assignedTo: undefined,
investorId: undefined,
purchaseDate: new Date().toISOString().split('T')[0],
@@ -561,14 +572,21 @@ function BikeForm({ bike, onSave, onCancel }: { bike: Bike | null; onSave: (bike
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Location *</label>
<input
type="text"
value={formData.location}
onChange={(e) => handleChange('location', e.target.value)}
placeholder="Gulshan 1"
<label className="block text-sm font-medium text-slate-700 mb-1">Hub *</label>
<select
value={formData.hubId}
onChange={(e) => {
const hub = hubs.find(h => h.id === e.target.value);
handleChange('hubId', e.target.value);
handleChange('hubName', hub?.name || '');
}}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent"
/>
>
<option value="">Select Hub...</option>
{hubs.map(hub => (
<option key={hub.id} value={hub.id}>{hub.name}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Assigned To</label>
@@ -659,7 +677,7 @@ function BikeDetails({ bike }: { bike: Bike }) {
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500 mb-1">Location</p>
<p className="font-semibold text-slate-700">{bike.location}</p>
<p className="font-semibold text-slate-700">{bike.hubName || bike.location || 'Not Assigned'}</p>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500 mb-1">Assigned To</p>
@@ -729,7 +747,7 @@ function FleetMap({ bikes, onSelectBike, selectedBike, large }: { bikes: Bike[];
const locationCounts: Record<string, { bikes: Bike[]; lat: number; lng: number }> = {};
bikes.forEach(bike => {
const loc = bike.location;
const loc = bike.hubName || bike.location || 'Unassigned';
if (!locationCounts[loc]) {
const locations: Record<string, { lat: number; lng: number }> = {
'Gulshan 1': { lat: 23.7936, lng: 90.4061 },