refactor: replace revenue and geofence pages with new hub management system
This commit is contained in:
@@ -100,7 +100,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;
|
||||
investorName?: string;
|
||||
@@ -126,7 +128,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', 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, 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 },
|
||||
@@ -157,7 +159,7 @@ const mockBikes: Bike[] = [
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'EV002', model: 'Yadea DT3', brand: 'Yadea', image: '', plateNumber: 'Dhaka Metro Cha-A-5678', status: 'available', batteryLevel: 95, location: 'Banani', investorId: 'inv1', investorName: 'Mr. Hasan (Investor)', purchaseDate: '2024-02-01', purchasePrice: 118000, totalRides: 89, totalDistance: 1567, totalEarnings: 31150, lastService: '2024-03-15', nextService: '2024-04-15', insuranceExpiry: '2025-02-01', registrationExpiry: '2026-02-01',
|
||||
id: 'EV002', model: 'Yadea DT3', brand: 'Yadea', image: '', plateNumber: 'Dhaka Metro Cha-A-5678', status: 'available', batteryLevel: 95, location: 'Banani', hubId: 'HUB-002', hubName: 'Banani Hub', investorId: 'inv1', investorName: 'Mr. Hasan (Investor)', purchaseDate: '2024-02-01', purchasePrice: 118000, totalRides: 89, totalDistance: 1567, totalEarnings: 31150, lastService: '2024-03-15', nextService: '2024-04-15', insuranceExpiry: '2025-02-01', registrationExpiry: '2026-02-01',
|
||||
gpsDevice: { id: 'GPS002', phone: '01712345602', imei: '861234567890124', lastActive: '2024-03-21 15:00', signal: 92, battery: 88 },
|
||||
documents: [
|
||||
{ type: 'registration', number: 'REG-EV002-2024', issueDate: '2024-02-01', expiryDate: '2026-02-01', verified: true },
|
||||
@@ -173,7 +175,7 @@ const mockBikes: Bike[] = [
|
||||
]
|
||||
},
|
||||
{
|
||||
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', investorName: 'Mr. Hasan (Investor)', purchaseDate: '2024-01-20', purchasePrice: 132000, currentRent: 400, totalRides: 203, totalDistance: 3890, totalEarnings: 71100, lastService: '2024-03-10', nextService: '2024-04-10', insuranceExpiry: '2025-01-20', registrationExpiry: '2026-01-20',
|
||||
id: 'EV003', model: 'AIMA Lightning', brand: 'AIMA', image: '', plateNumber: 'Dhaka Metro Cha-A-9012', status: 'rented', batteryLevel: 62, hubId: 'HUB-003', hubName: 'Uttara Hub', investorId: 'inv1', investorName: 'Mr. Hasan (Investor)', purchaseDate: '2024-01-20', purchasePrice: 132000, currentRent: 400, totalRides: 203, totalDistance: 3890, totalEarnings: 71100, lastService: '2024-03-10', nextService: '2024-04-10', insuranceExpiry: '2025-01-20', registrationExpiry: '2026-01-20',
|
||||
gpsDevice: { id: 'GPS003', phone: '01712345603', imei: '861234567890125', lastActive: '2024-03-21 14:45', signal: 78, battery: 55 },
|
||||
documents: [
|
||||
{ type: 'registration', number: 'REG-EV003-2024', issueDate: '2024-01-20', expiryDate: '2026-01-20', verified: true },
|
||||
@@ -184,7 +186,7 @@ const mockBikes: Bike[] = [
|
||||
activityLog: []
|
||||
},
|
||||
{
|
||||
id: 'EV004', model: 'TVS iQube', brand: 'TVS', image: '', plateNumber: 'Dhaka Metro Cha-A-3456', status: 'maintenance', batteryLevel: 45, location: 'Workshop - Banani', investorId: 'inv2', investorName: 'Mrs. Rita (Investor)', purchaseDate: '2023-12-10', purchasePrice: 145000, totalRides: 312, totalDistance: 5670, totalEarnings: 98000, lastService: '2024-03-20', nextService: '2024-03-25', insuranceExpiry: '2024-12-10', registrationExpiry: '2025-12-10', notes: 'Motor issue - awaiting parts',
|
||||
id: 'EV004', model: 'TVS iQube', brand: 'TVS', image: '', plateNumber: 'Dhaka Metro Cha-A-3456', status: 'maintenance', batteryLevel: 45, hubId: 'HUB-002', hubName: 'Banani Hub', investorId: 'inv2', investorName: 'Mrs. Rita (Investor)', purchaseDate: '2023-12-10', purchasePrice: 145000, totalRides: 312, totalDistance: 5670, totalEarnings: 98000, lastService: '2024-03-20', nextService: '2024-03-25', insuranceExpiry: '2024-12-10', registrationExpiry: '2025-12-10', notes: 'Motor issue - awaiting parts',
|
||||
gpsDevice: { id: 'GPS004', phone: '01712345604', imei: '861234567890126', lastActive: '2024-03-20 10:00', signal: 0, battery: 12 },
|
||||
documents: [
|
||||
{ type: 'registration', number: 'REG-EV004-2023', issueDate: '2023-12-10', expiryDate: '2025-12-10', verified: true },
|
||||
@@ -194,7 +196,7 @@ const mockBikes: Bike[] = [
|
||||
activityLog: []
|
||||
},
|
||||
{
|
||||
id: 'EV005', model: 'Bajaj Chetak', brand: 'Bajaj', image: '', plateNumber: 'Dhaka Metro Cha-A-7890', status: 'available', batteryLevel: 100, location: 'Dhanmondi', investorId: 'inv2', investorName: 'Mrs. Rita (Investor)', purchaseDate: '2024-02-15', purchasePrice: 138000, totalRides: 67, totalDistance: 890, totalEarnings: 23450, lastService: '2024-03-18', nextService: '2024-04-18', insuranceExpiry: '2025-02-15', registrationExpiry: '2026-02-15',
|
||||
id: 'EV005', model: 'Bajaj Chetak', brand: 'Bajaj', image: '', plateNumber: 'Dhaka Metro Cha-A-7890', status: 'available', batteryLevel: 100, hubId: 'HUB-001', hubName: 'JAIBEN Head Office', investorId: 'inv2', investorName: 'Mrs. Rita (Investor)', purchaseDate: '2024-02-15', purchasePrice: 138000, totalRides: 67, totalDistance: 890, totalEarnings: 23450, lastService: '2024-03-18', nextService: '2024-04-18', insuranceExpiry: '2025-02-15', registrationExpiry: '2026-02-15',
|
||||
gpsDevice: { id: 'GPS005', phone: '01712345605', imei: '861234567890127', lastActive: '2024-03-21 15:30', signal: 95, battery: 92 },
|
||||
documents: [
|
||||
{ type: 'registration', number: 'REG-EV005-2024', issueDate: '2024-02-15', expiryDate: '2026-02-15', verified: true },
|
||||
@@ -341,7 +343,7 @@ export default function FleetDetailPage({ params }: { params: Promise<{ id: stri
|
||||
|
||||
const tabs = [
|
||||
{ id: 'overview', label: 'Overview', icon: Bike },
|
||||
{ id: 'biker-assignment', label: 'Assign Bikers', icon: User },
|
||||
// { id: 'biker-assignment', label: 'Assign Bikers', icon: User },
|
||||
{ id: 'gps', label: 'GPS & Tracking', icon: Navigation2 },
|
||||
{ id: 'documents', label: 'Documents', icon: FileText },
|
||||
{ id: 'rental', label: 'Rental History', icon: History },
|
||||
@@ -372,8 +374,8 @@ export default function FleetDetailPage({ params }: { params: Promise<{ id: stri
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`px-3 py-2 rounded-lg text-sm font-medium whitespace-nowrap flex items-center gap-2 ${activeTab === tab.id
|
||||
? 'bg-accent text-white'
|
||||
: 'bg-white text-slate-600 border border-slate-200'
|
||||
? 'bg-accent text-white'
|
||||
: 'bg-white text-slate-600 border border-slate-200'
|
||||
}`}
|
||||
>
|
||||
<tab.icon className="w-4 h-4" />
|
||||
@@ -397,7 +399,7 @@ export default function FleetDetailPage({ params }: { params: Promise<{ id: stri
|
||||
<h3 className="font-semibold text-slate-700 flex items-center gap-2">
|
||||
<AlertTriangle className="w-5 h-5 text-accent" /> Damage History
|
||||
</h3>
|
||||
<button
|
||||
<button
|
||||
onClick={() => { setEditingDamage(null); setShowDamageModal(true); }}
|
||||
className="px-4 py-2 bg-accent text-white text-sm rounded-lg hover:bg-accent-dark flex items-center gap-2"
|
||||
>
|
||||
@@ -432,25 +434,24 @@ export default function FleetDetailPage({ params }: { params: Promise<{ id: stri
|
||||
<td className="px-4 py-3 text-sm text-slate-600">৳{damage.estimatedCost || 0}</td>
|
||||
<td className="px-4 py-3 text-sm font-medium text-slate-700">৳{damage.actualCost || '-'}</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${
|
||||
damage.status === 'repaired' ? 'bg-green-100 text-green-700' :
|
||||
damage.status === 'under_repair' ? 'bg-amber-100 text-amber-700' :
|
||||
damage.status === 'claim_rejected' ? 'bg-red-100 text-red-700' :
|
||||
'bg-slate-100 text-slate-700'
|
||||
}`}>
|
||||
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${damage.status === 'repaired' ? 'bg-green-100 text-green-700' :
|
||||
damage.status === 'under_repair' ? 'bg-amber-100 text-amber-700' :
|
||||
damage.status === 'claim_rejected' ? 'bg-red-100 text-red-700' :
|
||||
'bg-slate-100 text-slate-700'
|
||||
}`}>
|
||||
{damage.status.replace('_', ' ')}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
<button
|
||||
onClick={() => { setEditingDamage(damage); setShowDamageModal(true); }}
|
||||
className="p-2 hover:bg-slate-100 rounded-lg"
|
||||
title="Edit"
|
||||
>
|
||||
<Edit className="w-4 h-4 text-slate-400" />
|
||||
</button>
|
||||
<button
|
||||
<button
|
||||
onClick={() => handleDeleteDamage(damage.id)}
|
||||
className="p-2 hover:bg-red-50 rounded-lg"
|
||||
title="Delete"
|
||||
@@ -481,7 +482,7 @@ export default function FleetDetailPage({ params }: { params: Promise<{ id: stri
|
||||
<h3 className="font-semibold text-slate-700 flex items-center gap-2">
|
||||
<Wrench className="w-5 h-5 text-accent" /> Maintenance History
|
||||
</h3>
|
||||
<button
|
||||
<button
|
||||
onClick={() => { setEditingMaintenance(null); setShowMaintenanceModal(true); }}
|
||||
className="px-4 py-2 bg-accent text-white text-sm rounded-lg hover:bg-accent-dark flex items-center gap-2"
|
||||
>
|
||||
@@ -516,24 +517,23 @@ export default function FleetDetailPage({ params }: { params: Promise<{ id: stri
|
||||
<td className="px-4 py-3 text-sm font-medium text-slate-700">৳{maintenance.cost}</td>
|
||||
<td className="px-4 py-3 text-sm text-slate-600">{maintenance.nextDueDate || '-'}</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${
|
||||
maintenance.status === 'completed' ? 'bg-green-100 text-green-700' :
|
||||
maintenance.status === 'in_progress' ? 'bg-amber-100 text-amber-700' :
|
||||
'bg-slate-100 text-slate-700'
|
||||
}`}>
|
||||
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${maintenance.status === 'completed' ? 'bg-green-100 text-green-700' :
|
||||
maintenance.status === 'in_progress' ? 'bg-amber-100 text-amber-700' :
|
||||
'bg-slate-100 text-slate-700'
|
||||
}`}>
|
||||
{maintenance.status.replace('_', ' ')}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
<button
|
||||
onClick={() => { setEditingMaintenance(maintenance); setShowMaintenanceModal(true); }}
|
||||
className="p-2 hover:bg-slate-100 rounded-lg"
|
||||
title="Edit"
|
||||
>
|
||||
<Edit className="w-4 h-4 text-slate-400" />
|
||||
</button>
|
||||
<button
|
||||
<button
|
||||
onClick={() => handleDeleteMaintenance(maintenance.id)}
|
||||
className="p-2 hover:bg-red-50 rounded-lg"
|
||||
title="Delete"
|
||||
@@ -606,7 +606,7 @@ function OverviewTab({ bike }: { bike: Bike }) {
|
||||
{bike.status}
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full bg-slate-100 text-slate-600">
|
||||
<MapPin className="w-3 h-3" /> {bike.location}
|
||||
<MapPin className="w-3 h-3" /> {bike.hubName || 'Not Assigned'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -857,8 +857,8 @@ function RentalTab({ bike }: { bike: Bike }) {
|
||||
<p className="text-xs text-slate-500">ID: {rental.id}</p>
|
||||
</div>
|
||||
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${rental.status === 'active' ? 'bg-green-100 text-green-700' :
|
||||
rental.status === 'completed' ? 'bg-blue-100 text-blue-700' :
|
||||
'bg-red-100 text-red-700'
|
||||
rental.status === 'completed' ? 'bg-blue-100 text-blue-700' :
|
||||
'bg-red-100 text-red-700'
|
||||
}`}>
|
||||
{rental.status}
|
||||
</span>
|
||||
@@ -1098,8 +1098,8 @@ function BikerAssignmentTab({ bike }: { bike: Bike }) {
|
||||
key={key}
|
||||
onClick={() => { setRentalPlan(key as any); setAssignedBikers([]); }}
|
||||
className={`p-4 rounded-lg border text-left transition-all ${rentalPlan === key
|
||||
? 'border-accent bg-accent/5'
|
||||
: 'border-slate-200 hover:border-accent/50'
|
||||
? 'border-accent bg-accent/5'
|
||||
: 'border-slate-200 hover:border-accent/50'
|
||||
}`}
|
||||
>
|
||||
<p className="font-semibold text-slate-700">{plan.name}</p>
|
||||
@@ -1204,7 +1204,7 @@ function BikerAssignmentTab({ bike }: { bike: Bike }) {
|
||||
|
||||
<div className="p-5 border-t border-slate-100 flex justify-end gap-3">
|
||||
<button onClick={() => setShowAssignModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">Cancel</button>
|
||||
<button
|
||||
<button
|
||||
onClick={handleSubmitAssignment}
|
||||
disabled={assignedBikers.length === 0}
|
||||
className="px-4 py-2 bg-accent text-white rounded-lg text-sm hover:bg-accent-dark disabled:opacity-50"
|
||||
@@ -1239,7 +1239,7 @@ function InvestorTab({ bike }: { bike: Bike }) {
|
||||
<p className="font-semibold text-slate-700">{bike.investorName || 'Investor'}</p>
|
||||
<p className="text-sm text-slate-500">ID: {bike.investorId}</p>
|
||||
</div>
|
||||
<Link
|
||||
<Link
|
||||
href={`/admin/investors/${bike.investorId}`}
|
||||
className="px-4 py-2 bg-purple-600 text-white text-sm rounded-lg hover:bg-purple-700 flex items-center gap-2"
|
||||
>
|
||||
|
||||
@@ -15,7 +15,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;
|
||||
@@ -60,9 +62,10 @@ export default function FleetMapPage() {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedBike, setSelectedBike] = useState<Bike | null>(null);
|
||||
|
||||
// Generate unique coords for each bike based on its location string
|
||||
// Generate unique coords for each bike based on its hubName or location
|
||||
const bikesWithCoords = mockBikes.map((bike, index) => {
|
||||
const base = locations[bike.location] || { lat: 23.79, lng: 90.40 };
|
||||
const loc = bike.hubName || bike.location || 'Unknown';
|
||||
const base = locations[loc] || { lat: 23.79, lng: 90.40 };
|
||||
// Add jitter
|
||||
return {
|
||||
...bike,
|
||||
|
||||
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user