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

@@ -100,7 +100,9 @@ interface Bike {
plateNumber: string; plateNumber: string;
status: 'available' | 'rented' | 'maintenance' | 'retired'; status: 'available' | 'rented' | 'maintenance' | 'retired';
batteryLevel: number; batteryLevel: number;
location: string; location?: string; // deprecated - use hubId/hubName
hubId?: string;
hubName?: string;
assignedTo?: string; assignedTo?: string;
investorId?: string; investorId?: string;
investorName?: string; investorName?: string;
@@ -126,7 +128,7 @@ interface Bike {
const mockBikes: 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 }, gpsDevice: { id: 'GPS001', phone: '01712345601', imei: '861234567890123', lastActive: '2024-03-21 14:30', signal: 85, battery: 72 },
documents: [ documents: [
{ type: 'registration', number: 'REG-EV001-2024', issueDate: '2024-01-15', expiryDate: '2026-01-15', verified: true }, { 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 }, gpsDevice: { id: 'GPS002', phone: '01712345602', imei: '861234567890124', lastActive: '2024-03-21 15:00', signal: 92, battery: 88 },
documents: [ documents: [
{ type: 'registration', number: 'REG-EV002-2024', issueDate: '2024-02-01', expiryDate: '2026-02-01', verified: true }, { 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 }, gpsDevice: { id: 'GPS003', phone: '01712345603', imei: '861234567890125', lastActive: '2024-03-21 14:45', signal: 78, battery: 55 },
documents: [ documents: [
{ type: 'registration', number: 'REG-EV003-2024', issueDate: '2024-01-20', expiryDate: '2026-01-20', verified: true }, { type: 'registration', number: 'REG-EV003-2024', issueDate: '2024-01-20', expiryDate: '2026-01-20', verified: true },
@@ -184,7 +186,7 @@ const mockBikes: Bike[] = [
activityLog: [] 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 }, gpsDevice: { id: 'GPS004', phone: '01712345604', imei: '861234567890126', lastActive: '2024-03-20 10:00', signal: 0, battery: 12 },
documents: [ documents: [
{ type: 'registration', number: 'REG-EV004-2023', issueDate: '2023-12-10', expiryDate: '2025-12-10', verified: true }, { type: 'registration', number: 'REG-EV004-2023', issueDate: '2023-12-10', expiryDate: '2025-12-10', verified: true },
@@ -194,7 +196,7 @@ const mockBikes: Bike[] = [
activityLog: [] 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 }, gpsDevice: { id: 'GPS005', phone: '01712345605', imei: '861234567890127', lastActive: '2024-03-21 15:30', signal: 95, battery: 92 },
documents: [ documents: [
{ type: 'registration', number: 'REG-EV005-2024', issueDate: '2024-02-15', expiryDate: '2026-02-15', verified: true }, { 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 = [ const tabs = [
{ id: 'overview', label: 'Overview', icon: Bike }, { 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: 'gps', label: 'GPS & Tracking', icon: Navigation2 },
{ id: 'documents', label: 'Documents', icon: FileText }, { id: 'documents', label: 'Documents', icon: FileText },
{ id: 'rental', label: 'Rental History', icon: History }, { id: 'rental', label: 'Rental History', icon: History },
@@ -372,8 +374,8 @@ export default function FleetDetailPage({ params }: { params: Promise<{ id: stri
key={tab.id} key={tab.id}
onClick={() => setActiveTab(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 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-accent text-white'
: 'bg-white text-slate-600 border border-slate-200' : 'bg-white text-slate-600 border border-slate-200'
}`} }`}
> >
<tab.icon className="w-4 h-4" /> <tab.icon className="w-4 h-4" />
@@ -432,12 +434,11 @@ 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 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 text-sm font-medium text-slate-700">{damage.actualCost || '-'}</td>
<td className="px-4 py-3"> <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 ${ <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 === 'repaired' ? 'bg-green-100 text-green-700' : damage.status === 'under_repair' ? 'bg-amber-100 text-amber-700' :
damage.status === 'under_repair' ? 'bg-amber-100 text-amber-700' : damage.status === 'claim_rejected' ? 'bg-red-100 text-red-700' :
damage.status === 'claim_rejected' ? 'bg-red-100 text-red-700' : 'bg-slate-100 text-slate-700'
'bg-slate-100 text-slate-700' }`}>
}`}>
{damage.status.replace('_', ' ')} {damage.status.replace('_', ' ')}
</span> </span>
</td> </td>
@@ -516,11 +517,10 @@ 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 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 text-sm text-slate-600">{maintenance.nextDueDate || '-'}</td>
<td className="px-4 py-3"> <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 ${ <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 === 'completed' ? 'bg-green-100 text-green-700' : maintenance.status === 'in_progress' ? 'bg-amber-100 text-amber-700' :
maintenance.status === 'in_progress' ? 'bg-amber-100 text-amber-700' : 'bg-slate-100 text-slate-700'
'bg-slate-100 text-slate-700' }`}>
}`}>
{maintenance.status.replace('_', ' ')} {maintenance.status.replace('_', ' ')}
</span> </span>
</td> </td>
@@ -606,7 +606,7 @@ function OverviewTab({ bike }: { bike: Bike }) {
{bike.status} {bike.status}
</span> </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"> <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> </span>
</div> </div>
</div> </div>
@@ -857,8 +857,8 @@ function RentalTab({ bike }: { bike: Bike }) {
<p className="text-xs text-slate-500">ID: {rental.id}</p> <p className="text-xs text-slate-500">ID: {rental.id}</p>
</div> </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' : <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' : rental.status === 'completed' ? 'bg-blue-100 text-blue-700' :
'bg-red-100 text-red-700' 'bg-red-100 text-red-700'
}`}> }`}>
{rental.status} {rental.status}
</span> </span>
@@ -1098,8 +1098,8 @@ function BikerAssignmentTab({ bike }: { bike: Bike }) {
key={key} key={key}
onClick={() => { setRentalPlan(key as any); setAssignedBikers([]); }} onClick={() => { setRentalPlan(key as any); setAssignedBikers([]); }}
className={`p-4 rounded-lg border text-left transition-all ${rentalPlan === key className={`p-4 rounded-lg border text-left transition-all ${rentalPlan === key
? 'border-accent bg-accent/5' ? 'border-accent bg-accent/5'
: 'border-slate-200 hover:border-accent/50' : 'border-slate-200 hover:border-accent/50'
}`} }`}
> >
<p className="font-semibold text-slate-700">{plan.name}</p> <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"> <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 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} onClick={handleSubmitAssignment}
disabled={assignedBikers.length === 0} disabled={assignedBikers.length === 0}
className="px-4 py-2 bg-accent text-white rounded-lg text-sm hover:bg-accent-dark disabled:opacity-50" className="px-4 py-2 bg-accent text-white rounded-lg text-sm hover:bg-accent-dark disabled:opacity-50"

View File

@@ -15,7 +15,9 @@ interface Bike {
plateNumber: string; plateNumber: string;
status: 'available' | 'rented' | 'maintenance' | 'retired'; status: 'available' | 'rented' | 'maintenance' | 'retired';
batteryLevel: number; batteryLevel: number;
location: string; location?: string; // deprecated - use hubId/hubName
hubId?: string;
hubName?: string;
assignedTo?: string; assignedTo?: string;
investorId?: string; investorId?: string;
purchaseDate?: string; purchaseDate?: string;
@@ -60,9 +62,10 @@ export default function FleetMapPage() {
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [selectedBike, setSelectedBike] = useState<Bike | null>(null); 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 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 // Add jitter
return { return {
...bike, ...bike,

View File

@@ -17,7 +17,9 @@ interface Bike {
plateNumber: string; plateNumber: string;
status: 'available' | 'rented' | 'maintenance' | 'retired'; status: 'available' | 'rented' | 'maintenance' | 'retired';
batteryLevel: number; batteryLevel: number;
location: string; location?: string; // deprecated - use hubId/hubName
hubId?: string;
hubName?: string;
assignedTo?: string; assignedTo?: string;
investorId?: string; investorId?: string;
purchaseDate?: string; purchaseDate?: string;
@@ -33,7 +35,7 @@ interface Bike {
} }
const mockBikes: 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: '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: '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' }, { 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' }, { 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> = { const statusColors: Record<string, string> = {
available: 'bg-green-100 text-green-700', available: 'bg-green-100 text-green-700',
rented: 'bg-blue-100 text-blue-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 maintenanceCount = bikes.filter(b => b.status === 'maintenance').length;
const retiredCount = bikes.filter(b => b.status === 'retired').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 filteredBikes = bikes.filter(bike => {
const matchesSearch = bike.model.toLowerCase().includes(searchQuery.toLowerCase()) || const matchesSearch = bike.model.toLowerCase().includes(searchQuery.toLowerCase()) ||
@@ -79,7 +88,7 @@ export default function FleetPage() {
bike.plateNumber.toLowerCase().includes(searchQuery.toLowerCase()) || bike.plateNumber.toLowerCase().includes(searchQuery.toLowerCase()) ||
bike.id.toLowerCase().includes(searchQuery.toLowerCase()); bike.id.toLowerCase().includes(searchQuery.toLowerCase());
const matchesStatus = statusFilter === 'all' || bike.status === statusFilter; 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; return matchesSearch && matchesStatus && matchesLocation;
}); });
@@ -303,7 +312,7 @@ export default function FleetPage() {
<tr> <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">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">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">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">Metrics</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Status</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>
<td className="px-4 py-3"> <td className="px-4 py-3">
<p className="text-sm text-slate-600 flex items-center gap-1"> <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> </p>
</td> </td>
<td className="px-4 py-3"> <td className="px-4 py-3">
@@ -398,7 +407,7 @@ export default function FleetPage() {
</div> </div>
<div className="flex items-center justify-between text-xs"> <div className="flex items-center justify-between text-xs">
<span className="text-slate-500">Location</span> <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>
<div className="flex items-center justify-between text-xs"> <div className="flex items-center justify-between text-xs">
<span className="text-slate-500">Battery</span> <span className="text-slate-500">Battery</span>
@@ -479,7 +488,9 @@ function BikeForm({ bike, onSave, onCancel }: { bike: Bike | null; onSave: (bike
plateNumber: '', plateNumber: '',
status: 'available', status: 'available',
batteryLevel: 100, batteryLevel: 100,
location: '', location: '', // deprecated
hubId: '',
hubName: '',
assignedTo: undefined, assignedTo: undefined,
investorId: undefined, investorId: undefined,
purchaseDate: new Date().toISOString().split('T')[0], purchaseDate: new Date().toISOString().split('T')[0],
@@ -561,14 +572,21 @@ function BikeForm({ bike, onSave, onCancel }: { bike: Bike | null; onSave: (bike
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-slate-700 mb-1">Location *</label> <label className="block text-sm font-medium text-slate-700 mb-1">Hub *</label>
<input <select
type="text" value={formData.hubId}
value={formData.location} onChange={(e) => {
onChange={(e) => handleChange('location', e.target.value)} const hub = hubs.find(h => h.id === e.target.value);
placeholder="Gulshan 1" 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" 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>
<div> <div>
<label className="block text-sm font-medium text-slate-700 mb-1">Assigned To</label> <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>
<div className="bg-slate-50 rounded-lg p-3"> <div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500 mb-1">Location</p> <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>
<div className="bg-slate-50 rounded-lg p-3"> <div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500 mb-1">Assigned To</p> <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 }> = {}; const locationCounts: Record<string, { bikes: Bike[]; lat: number; lng: number }> = {};
bikes.forEach(bike => { bikes.forEach(bike => {
const loc = bike.location; const loc = bike.hubName || bike.location || 'Unassigned';
if (!locationCounts[loc]) { if (!locationCounts[loc]) {
const locations: Record<string, { lat: number; lng: number }> = { const locations: Record<string, { lat: number; lng: number }> = {
'Gulshan 1': { lat: 23.7936, lng: 90.4061 }, 'Gulshan 1': { lat: 23.7936, lng: 90.4061 },

View File

@@ -1,108 +0,0 @@
import { MapPin, Plus, Search, MoreVertical, Eye, Edit, Trash2 } from 'lucide-react';
const geofences = [
{ id: 'gf1', name: 'Gulshan Zone A', type: 'Restricted', bikes: 12, status: 'active' },
{ id: 'gf2', name: 'Banani Area', type: 'Allowed', bikes: 8, status: 'active' },
{ id: 'gf3', name: 'Uttara Sector', type: 'Allowed', bikes: 15, status: 'active' },
{ id: 'gf4', name: 'Dhanmondi Zone', type: 'Slow Zone', bikes: 6, status: 'active' },
];
export default function GeofencePage() {
return (
<div className="p-4 lg:p-6">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6">
<div>
<h1 className="text-2xl lg:text-3xl font-extrabold text-slate-800">Geofences</h1>
<p className="text-sm text-slate-500 mt-1">Manage geo-fenced areas</p>
</div>
<button className="py-2.5 px-4 bg-accent text-white rounded-lg font-semibold text-sm hover:bg-accent-dark transition-colors flex items-center gap-2">
<Plus className="w-4 h-4" /> Add New Zone
</button>
</div>
<div className="grid grid-cols-3 gap-4 mb-6">
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<p className="text-2xl font-extrabold text-slate-800">{geofences.length}</p>
<p className="text-sm text-slate-500">Total Zones</p>
</div>
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<p className="text-2xl font-extrabold text-slate-800">41</p>
<p className="text-sm text-slate-500">Bikes in Zones</p>
</div>
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<p className="text-2xl font-extrabold text-slate-800">4</p>
<p className="text-sm text-slate-500">Active Zones</p>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-100">
<div className="p-4 border-b border-slate-100">
<div className="relative max-w-md">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<input
type="text"
placeholder="Search geofences..."
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"
/>
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-50">
<tr>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Zone Name</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Type</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Bikes</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Status</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{geofences.map(zone => (
<tr key={zone.id} className="hover:bg-slate-50 transition-colors">
<td className="px-4 py-3">
<div className="flex items-center gap-3">
<MapPin className="w-5 h-5 text-accent" />
<span className="text-sm font-medium text-slate-700">{zone.name}</span>
</div>
</td>
<td className="px-4 py-3">
<span className={`text-xs font-medium px-2.5 py-1 rounded-full ${
zone.type === 'Restricted' ? 'bg-red-100 text-red-700' :
zone.type === 'Slow Zone' ? 'bg-amber-100 text-amber-700' :
'bg-green-100 text-green-700'
}`}>
{zone.type}
</span>
</td>
<td className="px-4 py-3">
<span className="text-sm text-slate-600">{zone.bikes} bikes</span>
</td>
<td className="px-4 py-3">
<span className="text-xs font-medium px-2.5 py-1 rounded-full bg-green-100 text-green-700">
{zone.status}
</span>
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-1">
<button className="p-2 hover:bg-slate-100 rounded-lg">
<Eye className="w-4 h-4 text-slate-400" />
</button>
<button className="p-2 hover:bg-slate-100 rounded-lg">
<Edit className="w-4 h-4 text-slate-400" />
</button>
<button className="p-2 hover:bg-red-50 rounded-lg">
<Trash2 className="w-4 h-4 text-red-400" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,438 @@
'use client';
import { useState, useEffect } from 'react';
import { useParams, useRouter } from 'next/navigation';
import {
ArrowLeft, MapPin, Phone, Clock, Bike, Plus, X, Edit, Save, Trash2,
Navigation, User, Wallet, DollarSign, CheckCircle, AlertTriangle
} from 'lucide-react';
interface Hub {
id: string;
name: string;
address: string;
phone: string;
managerName?: string;
bikeCount: number;
activeRentals: number;
status: 'active' | 'inactive';
coordinates?: { lat: number; lng: number };
openTime: string;
closeTime: string;
isHeadOffice?: boolean;
}
interface BikeInfo {
id: string;
model: string;
plate: string;
status: 'available' | 'rented' | 'maintenance';
}
const mockHub: Hub = {
id: 'HUB-001',
name: 'JAIBEN Head Office',
address: 'House 12, Road 17, Gulshan 1, Dhaka',
phone: '+8801712345678',
managerName: 'Rahim Ahmed',
bikeCount: 25,
activeRentals: 12,
status: 'active',
coordinates: { lat: 23.7925, lng: 90.4174 },
openTime: '06:00',
closeTime: '23:00',
isHeadOffice: true
};
const mockHubBikes: BikeInfo[] = [
{ id: 'BIKE-001', model: 'AIMA Lightning', plate: 'Dhaka Metro Cha-9012', status: 'available' },
{ id: 'BIKE-002', model: 'Yadea DT3', plate: 'Dhaka Metro Ba-5521', status: 'rented' },
{ id: 'BIKE-003', model: 'AIMA EM5', plate: 'Dhaka Metro Ko-1234', status: 'available' },
{ id: 'BIKE-004', model: 'AIMA Lightning', plate: 'Dhaka Metro Cha-9013', status: 'rented' },
{ id: 'BIKE-005', model: 'Yadea G5', plate: 'Dhaka Metro Ha-5678', status: 'maintenance' },
];
interface RentalInfo {
id: string;
userName: string;
bike: string;
plate: string;
startDate: string;
type: string;
status: 'active' | 'pending' | 'completed';
dailyRate: number;
totalPaid: number;
}
const mockHubRentals: RentalInfo[] = [
{ id: 'RNT-001', userName: 'Rahim Ahmed', bike: 'AIMA Lightning', plate: 'Dhaka Metro Cha-9012', startDate: '2024-01-15', type: 'single', status: 'active', dailyRate: 300, totalPaid: 81900 },
{ id: 'RNT-002', userName: 'Karim Hasan', bike: 'Yadea DT3', plate: 'Dhaka Metro Ba-5521', startDate: '2024-01-20', type: 'single', status: 'active', dailyRate: 200, totalPaid: 12400 },
{ id: 'RNT-003', userName: 'Jamal Uddin', bike: 'AIMA EM5', plate: 'Dhaka Metro Ko-1234', startDate: '2024-02-01', type: 'shared', status: 'pending', dailyRate: 150, totalPaid: 450 },
];
export default function HubDetailPage() {
const params = useParams();
const router = useRouter();
const id = params.id as string;
const [hub, setHub] = useState<Hub>(mockHub);
const [bikes, setBikes] = useState<BikeInfo[]>(mockHubBikes);
const [rentals, setRentals] = useState<RentalInfo[]>(mockHubRentals);
const [editMode, setEditMode] = useState(false);
const [editForm, setEditForm] = useState(hub);
const [activeTab, setActiveTab] = useState<'overview' | 'bikes' | 'rentals'>('overview');
const handleSaveEdit = () => {
setHub(editForm);
setEditMode(false);
};
if (!id || id !== 'HUB-001') {
return (
<div className="p-6 flex items-center justify-center min-h-[50vh]">
<div className="text-center">
<MapPin className="w-16 h-16 text-slate-300 mx-auto mb-4" />
<p className="text-slate-500">Hub not found</p>
<button
onClick={() => router.push('/admin/hub')}
className="mt-4 px-4 py-2 bg-accent text-white rounded-lg text-sm"
>
Back to Hubs
</button>
</div>
</div>
);
}
return (
<div className="p-4 lg:p-6 max-w-8xl mx-auto">
<button
onClick={() => router.push('/admin/hub')}
className="flex items-center gap-2 text-slate-600 hover:text-slate-800 mb-4"
>
<ArrowLeft className="w-4 h-4" /> Back to Hubs
</button>
<div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden">
<div className="p-6 border-b border-slate-100">
<div className="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-4">
<div>
<div className="flex items-center gap-3">
<h1 className="text-2xl font-extrabold text-slate-800">{hub.name}</h1>
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${hub.status === 'active' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'
}`}>
{hub.status}
</span>
{hub.isHeadOffice && (
<span className="text-xs font-medium px-2 py-1 rounded-full bg-purple-100 text-purple-700">
Head Office
</span>
)}
</div>
<p className="text-slate-500 mt-1">{hub.address}</p>
</div>
<div className="flex gap-2">
{editMode ? (
<>
<button onClick={handleSaveEdit} className="px-4 py-2 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700 flex items-center gap-2">
<Save className="w-4 h-4" /> Save
</button>
<button onClick={() => { setEditForm(hub); setEditMode(false); }} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">
Cancel
</button>
</>
) : (
<button onClick={() => setEditMode(true)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50 flex items-center gap-2">
<Edit className="w-4 h-4" /> Edit
</button>
)}
</div>
</div>
</div>
<div className="border-b border-slate-100">
<nav className="flex gap-6 px-6">
<button
onClick={() => setActiveTab('overview')}
className={`py-4 text-sm font-medium border-b-2 transition-colors ${activeTab === 'overview'
? 'border-accent text-accent'
: 'border-transparent text-slate-500 hover:text-slate-700'
}`}
>
Overview
</button>
<button
onClick={() => setActiveTab('bikes')}
className={`py-4 text-sm font-medium border-b-2 transition-colors ${activeTab === 'bikes'
? 'border-accent text-accent'
: 'border-transparent text-slate-500 hover:text-slate-700'
}`}
>
Bikes ({bikes.length})
</button>
<button
onClick={() => setActiveTab('rentals')}
className={`py-4 text-sm font-medium border-b-2 transition-colors ${activeTab === 'rentals'
? 'border-accent text-accent'
: 'border-transparent text-slate-500 hover:text-slate-700'
}`}
>
Rentals ({rentals.length})
</button>
</nav>
</div>
<div className="p-6">
{activeTab === 'overview' && (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="space-y-4">
<div className="bg-blue-50 p-4 rounded-xl border border-blue-100">
<h3 className="font-semibold text-blue-800 mb-3 flex items-center gap-2">
<MapPin className="w-5 h-5" /> Location Info
</h3>
{editMode ? (
<div className="space-y-3">
<input
type="text"
value={editForm.address}
onChange={(e) => setEditForm({ ...editForm, address: e.target.value })}
className="w-full px-3 py-2 border border-blue-200 rounded-lg text-sm"
placeholder="Address"
/>
</div>
) : (
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-sm text-blue-600">Address</span>
<span className="text-sm font-medium text-blue-800">{hub.address}</span>
</div>
{hub.coordinates && (
<div className="flex justify-between">
<span className="text-sm text-blue-600">Coordinates</span>
<span className="text-sm font-medium text-blue-800">{hub.coordinates.lat}, {hub.coordinates.lng}</span>
</div>
)}
</div>
)}
</div>
<div className="bg-purple-50 p-4 rounded-xl border border-purple-100">
<h3 className="font-semibold text-purple-800 mb-3 flex items-center gap-2">
<Phone className="w-5 h-5" /> Contact Info
</h3>
{editMode ? (
<div className="space-y-3">
<input
type="text"
value={editForm.phone}
onChange={(e) => setEditForm({ ...editForm, phone: e.target.value })}
className="w-full px-3 py-2 border border-purple-200 rounded-lg text-sm"
placeholder="Phone"
/>
<input
type="text"
value={editForm.managerName || ''}
onChange={(e) => setEditForm({ ...editForm, managerName: e.target.value })}
className="w-full px-3 py-2 border border-purple-200 rounded-lg text-sm"
placeholder="Manager Name"
/>
</div>
) : (
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-sm text-purple-600">Phone</span>
<span className="text-sm font-medium text-purple-800">{hub.phone}</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-purple-600">Manager</span>
<span className="text-sm font-medium text-purple-800">{hub.managerName || '-'}</span>
</div>
</div>
)}
</div>
</div>
<div className="space-y-4">
<div className="bg-green-50 p-4 rounded-xl border border-green-100">
<h3 className="font-semibold text-green-800 mb-3 flex items-center gap-2">
<Clock className="w-5 h-5" /> Operating Hours
</h3>
{editMode ? (
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-sm text-green-600">Open</label>
<input
type="time"
value={editForm.openTime}
onChange={(e) => setEditForm({ ...editForm, openTime: e.target.value })}
className="w-full px-3 py-2 border border-green-200 rounded-lg text-sm mt-1"
/>
</div>
<div>
<label className="text-sm text-green-600">Close</label>
<input
type="time"
value={editForm.closeTime}
onChange={(e) => setEditForm({ ...editForm, closeTime: e.target.value })}
className="w-full px-3 py-2 border border-green-200 rounded-lg text-sm mt-1"
/>
</div>
</div>
) : (
<div className="flex justify-between">
<span className="text-sm text-green-600">Hours</span>
<span className="text-sm font-medium text-green-800">{hub.openTime} - {hub.closeTime}</span>
</div>
)}
</div>
<div className="bg-amber-50 p-4 rounded-xl border border-amber-100">
<h3 className="font-semibold text-amber-800 mb-3 flex items-center gap-2">
<DollarSign className="w-5 h-5" /> Today's Earnings
</h3>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-sm text-amber-600">Rentals</span>
<span className="text-sm font-medium text-amber-800">{hub.activeRentals}</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-amber-600">Revenue</span>
<span className="text-sm font-medium text-amber-800">৳{hub.activeRentals * 300}</span>
</div>
</div>
</div>
</div>
<div className="space-y-4">
<div className="bg-slate-50 p-4 rounded-xl border border-slate-100">
<h3 className="font-semibold text-slate-800 mb-3 flex items-center gap-2">
<Bike className="w-5 h-5" /> Bike Statistics
</h3>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-sm text-slate-600">Total Bikes</span>
<span className="text-sm font-medium text-slate-800">{hub.bikeCount}</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-slate-600">Available</span>
<span className="text-sm font-medium text-green-700">
{bikes.filter(b => b.status === 'available').length}
</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-slate-600">Rented</span>
<span className="text-sm font-medium text-amber-700">
{bikes.filter(b => b.status === 'rented').length}
</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-slate-600">Maintenance</span>
<span className="text-sm font-medium text-red-700">
{bikes.filter(b => b.status === 'maintenance').length}
</span>
</div>
</div>
</div>
</div>
</div>
)}
{activeTab === 'bikes' && (
<div>
<div className="flex items-center justify-between mb-4">
<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">
<Plus className="w-4 h-4" /> Add Bike
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{bikes.map(bike => (
<div key={bike.id} className="bg-slate-50 p-4 rounded-xl border border-slate-100">
<div className="flex items-center justify-between mb-2">
<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' :
bike.status === 'rented' ? 'bg-amber-100 text-amber-700' :
'bg-red-100 text-red-700'
}`}>
{bike.status}
</span>
</div>
<p className="font-medium text-slate-800">{bike.model}</p>
<p className="text-sm text-slate-500">{bike.plate}</p>
<p className="text-xs text-slate-400 mt-2">ID: {bike.id}</p>
</div>
))}
</div>
</div>
)}
{activeTab === 'rentals' && (
<div>
<div className="flex items-center justify-between mb-4">
<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">
<Plus className="w-4 h-4" /> New Rental
</button>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-50">
<tr>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Rental ID</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">User</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Bike</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Start Date</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Type</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Daily Rate</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Total Paid</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{rentals.map(rental => (
<tr key={rental.id} className="hover:bg-slate-50">
<td className="px-4 py-3">
<span className="text-sm font-medium text-slate-700">{rental.id}</span>
</td>
<td className="px-4 py-3">
<span className="text-sm text-slate-600">{rental.userName}</span>
</td>
<td className="px-4 py-3">
<div>
<span className="text-sm text-slate-600">{rental.bike}</span>
<p className="text-xs text-slate-400">{rental.plate}</p>
</div>
</td>
<td className="px-4 py-3">
<span className="text-sm text-slate-600">{rental.startDate}</span>
</td>
<td className="px-4 py-3">
<span className="text-sm text-slate-600 capitalize">{rental.type}</span>
</td>
<td className="px-4 py-3">
<span className="text-sm text-slate-600">৳{rental.dailyRate}</span>
</td>
<td className="px-4 py-3">
<span className="text-sm font-medium text-green-600">৳{rental.totalPaid.toLocaleString()}</span>
</td>
<td className="px-4 py-3">
<span className={`text-xs font-medium px-2.5 py-1 rounded-full ${
rental.status === 'active' ? 'bg-green-100 text-green-700' :
rental.status === 'pending' ? 'bg-amber-100 text-amber-700' :
'bg-blue-100 text-blue-700'
}`}>
{rental.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div>
</div>
</div>
);
}

371
src/app/admin/hub/page.tsx Normal file
View File

@@ -0,0 +1,371 @@
'use client';
import { useState } from 'react';
import { MapPin, Plus, Search, Eye, Edit, Trash2, Bike, X, Navigation, Phone, Clock } from 'lucide-react';
import Link from 'next/link';
interface Hub {
id: string;
name: string;
address: string;
phone: string;
managerName?: string;
bikeCount: number;
activeRentals: number;
status: 'active' | 'inactive';
coordinates?: { lat: number; lng: number };
openTime: string;
closeTime: string;
isHeadOffice?: boolean;
}
const mockHubs: Hub[] = [
{
id: 'HUB-001',
name: 'JAIBEN Head Office',
address: 'House 12, Road 17, Gulshan 1, Dhaka',
phone: '+8801712345678',
managerName: 'Rahim Ahmed',
bikeCount: 25,
activeRentals: 12,
status: 'active',
coordinates: { lat: 23.7925, lng: 90.4174 },
openTime: '06:00',
closeTime: '23:00',
isHeadOffice: true
},
{
id: 'HUB-002',
name: 'Banani Hub',
address: 'House 5, Road 11, Banani, Dhaka',
phone: '+8801812345678',
managerName: 'Karim Hasan',
bikeCount: 18,
activeRentals: 8,
status: 'active',
coordinates: { lat: 23.7785, lng: 90.4190 },
openTime: '06:00',
closeTime: '23:00'
},
{
id: 'HUB-003',
name: 'Uttara Hub',
address: 'Sector 11, Uttara, Dhaka',
phone: '+8801912345678',
managerName: 'Jamal Uddin',
bikeCount: 30,
activeRentals: 15,
status: 'active',
coordinates: { lat: 23.8657, lng: 90.4027 },
openTime: '06:00',
closeTime: '23:00'
},
{
id: 'HUB-004',
name: 'Mirpur Hub',
address: 'Section 10, Mirpur, Dhaka',
phone: '+8801512345678',
bikeCount: 0,
activeRentals: 0,
status: 'inactive',
openTime: '06:00',
closeTime: '23:00'
}
];
export default function HubsPage() {
const [hubs, setHubs] = useState<Hub[]>(mockHubs);
const [search, setSearch] = useState('');
const [showCreateModal, setShowCreateModal] = useState(false);
const [editingHub, setEditingHub] = useState<Hub | null>(null);
const [formData, setFormData] = useState({
name: '',
address: '',
phone: '',
managerName: '',
openTime: '06:00',
closeTime: '23:00',
isHeadOffice: false,
});
const filteredHubs = hubs.filter(h =>
h.name.toLowerCase().includes(search.toLowerCase()) ||
h.address.toLowerCase().includes(search.toLowerCase())
);
const handleSave = () => {
if (!formData.name || !formData.address) return;
if (editingHub) {
setHubs(hubs.map(h => h.id === editingHub.id ? {
...h,
...formData,
status: editingHub.status,
bikeCount: editingHub.bikeCount,
activeRentals: editingHub.activeRentals,
} : h));
} else {
const newHub: Hub = {
id: `HUB-${String(hubs.length + 1).padStart(3, '0')}`,
...formData,
bikeCount: 0,
activeRentals: 0,
status: 'active',
};
setHubs([...hubs, newHub]);
}
setShowCreateModal(false);
setEditingHub(null);
setFormData({ name: '', address: '', phone: '', managerName: '', openTime: '06:00', closeTime: '23:00', isHeadOffice: false });
};
const handleDelete = (id: string) => {
if (confirm('Are you sure you want to delete this hub?')) {
setHubs(hubs.filter(h => h.id !== id));
}
};
const openEdit = (hub: Hub) => {
setEditingHub(hub);
setFormData({
name: hub.name,
address: hub.address,
phone: hub.phone,
managerName: hub.managerName || '',
openTime: hub.openTime,
closeTime: hub.closeTime,
isHeadOffice: hub.isHeadOffice || false,
});
setShowCreateModal(true);
};
return (
<div className="p-4 lg:p-6">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6">
<div>
<h1 className="text-2xl lg:text-3xl font-extrabold text-slate-800">Hubs</h1>
<p className="text-sm text-slate-500 mt-1">Manage hub locations and branches</p>
</div>
<button
onClick={() => {
setEditingHub(null);
setFormData({ name: '', address: '', phone: '', managerName: '', openTime: '06:00', closeTime: '23:00', isHeadOffice: false });
setShowCreateModal(true);
}}
className="py-2.5 px-4 bg-accent text-white rounded-lg font-semibold text-sm hover:bg-accent-dark transition-colors flex items-center gap-2"
>
<Plus className="w-4 h-4" /> Add New Hub
</button>
</div>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<p className="text-2xl font-extrabold text-slate-800">{hubs.length}</p>
<p className="text-sm text-slate-500">Total Hubs</p>
</div>
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<p className="text-2xl font-extrabold text-green-600">{hubs.filter(h => h.status === 'active').length}</p>
<p className="text-sm text-slate-500">Active Hubs</p>
</div>
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<p className="text-2xl font-extrabold text-blue-600">{hubs.reduce((a, h) => a + h.bikeCount, 0)}</p>
<p className="text-sm text-slate-500">Total Bikes</p>
</div>
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<p className="text-2xl font-extrabold text-amber-600">{hubs.reduce((a, h) => a + h.activeRentals, 0)}</p>
<p className="text-sm text-slate-500">Active Rentals</p>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-100">
<div className="p-4 border-b border-slate-100">
<div className="relative max-w-md">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<input
type="text"
placeholder="Search hubs..."
value={search}
onChange={(e) => setSearch(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"
/>
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-50">
<tr>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Hub Name</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Address</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Phone</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Manager</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Bikes</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Rentals</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Hours</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Status</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{filteredHubs.map(hub => (
<tr key={hub.id} className="hover:bg-slate-50 transition-colors">
<td className="px-4 py-3">
<Link href={`/admin/hub/${hub.id}`} className="flex items-center gap-3 hover:text-accent">
<MapPin className="w-5 h-5 text-accent" />
<span className="text-sm font-medium text-slate-700">{hub.name}</span>
</Link>
</td>
<td className="px-4 py-3">
<span className="text-sm text-slate-600 max-w-[200px] block truncate">{hub.address}</span>
</td>
<td className="px-4 py-3">
<span className="text-sm text-slate-600">{hub.phone}</span>
</td>
<td className="px-4 py-3">
<span className="text-sm text-slate-600">{hub.managerName || '-'}</span>
</td>
<td className="px-4 py-3">
<span className="text-sm font-medium text-slate-700">{hub.bikeCount}</span>
</td>
<td className="px-4 py-3">
<span className="text-sm font-medium text-amber-700">{hub.activeRentals}</span>
</td>
<td className="px-4 py-3">
<span className="text-sm text-slate-600">{hub.openTime} - {hub.closeTime}</span>
</td>
<td className="px-4 py-3">
<span className={`text-xs font-medium px-2.5 py-1 rounded-full ${
hub.status === 'active' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'
}`}>
{hub.status}
</span>
{hub.isHeadOffice && (
<span className="ml-2 text-xs font-medium px-2 py-1 rounded-full bg-purple-100 text-purple-700">
Head Office
</span>
)}
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-1">
<Link href={`/admin/hub/${hub.id}`} className="p-2 hover:bg-slate-100 rounded-lg">
<Eye className="w-4 h-4 text-slate-400" />
</Link>
<button onClick={() => openEdit(hub)} className="p-2 hover:bg-slate-100 rounded-lg">
<Edit className="w-4 h-4 text-slate-400" />
</button>
<button onClick={() => handleDelete(hub.id)} className="p-2 hover:bg-red-50 rounded-lg">
<Trash2 className="w-4 h-4 text-red-400" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{showCreateModal && (
<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">{editingHub ? 'Edit Hub' : 'Add New Hub'}</h3>
<button onClick={() => setShowCreateModal(false)} 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 text-slate-600">Hub Name *</label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
placeholder="Gulshan Hub"
/>
</div>
<div>
<label className="text-sm text-slate-600">Address *</label>
<textarea
value={formData.address}
onChange={(e) => setFormData({ ...formData, address: e.target.value })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
rows={2}
placeholder="Full address"
/>
</div>
<div>
<label className="text-sm text-slate-600">Phone</label>
<input
type="text"
value={formData.phone}
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
placeholder="+8801xxxxxxxxx"
/>
</div>
<div>
<label className="text-sm text-slate-600">Manager Name</label>
<input
type="text"
value={formData.managerName}
onChange={(e) => setFormData({ ...formData, managerName: e.target.value })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
placeholder="Manager name"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm text-slate-600">Open Time</label>
<input
type="time"
value={formData.openTime}
onChange={(e) => setFormData({ ...formData, openTime: e.target.value })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
/>
</div>
<div>
<label className="text-sm text-slate-600">Close Time</label>
<input
type="time"
value={formData.closeTime}
onChange={(e) => setFormData({ ...formData, closeTime: e.target.value })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
/>
</div>
<div className="flex items-center gap-2 mt-3">
<input
type="checkbox"
id="isHeadOffice"
checked={formData.isHeadOffice}
onChange={(e) => setFormData({ ...formData, isHeadOffice: e.target.checked })}
className="w-4 h-4 text-accent rounded"
/>
<label htmlFor="isHeadOffice" className="text-sm text-slate-600">Set as Head Office</label>
</div>
</div>
</div>
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
<button
onClick={() => setShowCreateModal(false)}
className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm"
>
Cancel
</button>
<button
onClick={handleSave}
disabled={!formData.name || !formData.address}
className="px-4 py-2 bg-accent text-white rounded-lg text-sm disabled:opacity-50"
>
{editingHub ? 'Update' : 'Create'}
</button>
</div>
</div>
</div>
)}
</div>
);
}

View File

@@ -1,128 +0,0 @@
import { DollarSign, TrendingUp, TrendingDown, Calendar, Download, ArrowUpRight, ArrowDownRight } from 'lucide-react';
export default function RevenuePage() {
return (
<div className="p-4 lg:p-6">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6">
<div>
<h1 className="text-2xl lg:text-3xl font-extrabold text-slate-800">Revenue</h1>
<p className="text-sm text-slate-500 mt-1">Track earnings and financial performance</p>
</div>
<div className="flex items-center gap-2">
<button className="py-2 px-4 border border-slate-200 rounded-lg text-sm font-medium text-slate-600 hover:bg-slate-50 flex items-center gap-2">
<Calendar className="w-4 h-4" /> Select Date Range
</button>
<button className="py-2 px-4 bg-accent text-white rounded-lg text-sm font-semibold hover:bg-accent-dark flex items-center gap-2">
<Download className="w-4 h-4" /> Export Report
</button>
</div>
</div>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
{[
{ label: 'Today', value: '৳45,600', change: '+23%', trend: 'up' },
{ label: 'This Week', value: '৳312,400', change: '+15%', trend: 'up' },
{ label: 'This Month', value: '৳1,245,000', change: '+8%', trend: 'up' },
{ label: 'This Year', value: '৳8,450,000', change: '+12%', trend: 'up' },
].map((item, index) => (
<div key={index} className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<div className="flex items-start justify-between mb-2">
<span className={`text-xs font-semibold px-2 py-1 rounded-full flex items-center gap-1 ${
item.trend === 'up' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'
}`}>
{item.trend === 'up' ? <ArrowUpRight className="w-3 h-3" /> : <ArrowDownRight className="w-3 h-3" />}
{item.change}
</span>
</div>
<p className="text-2xl font-extrabold text-slate-800">{item.value}</p>
<p className="text-sm text-slate-500">{item.label}</p>
</div>
))}
</div>
<div className="grid lg:grid-cols-2 gap-6 mb-8">
<div className="bg-white rounded-xl shadow-sm border border-slate-100 p-5">
<h2 className="font-bold text-slate-800 mb-4">Revenue Breakdown</h2>
<div className="space-y-4">
{[
{ label: 'Daily Rentals', value: '৳32,400', percent: 71 },
{ label: 'Weekly Rentals', value: '৳8,200', percent: 18 },
{ label: 'Deposits', value: '৳5,000', percent: 11 },
].map((item, index) => (
<div key={index}>
<div className="flex items-center justify-between mb-1">
<span className="text-sm text-slate-600">{item.label}</span>
<span className="text-sm font-semibold text-slate-700">{item.value}</span>
</div>
<div className="w-full bg-slate-100 rounded-full h-2">
<div className="bg-accent h-2 rounded-full" style={{ width: `${item.percent}%` }} />
</div>
</div>
))}
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-100 p-5">
<h2 className="font-bold text-slate-800 mb-4">Top Performing Bikes</h2>
<div className="space-y-3">
{[
{ model: 'Etron ET50', earnings: '৳12,400', rides: 45 },
{ model: 'AIMA Lightning', earnings: '৳10,200', rides: 38 },
{ model: 'Yadea DT3', earnings: '৳8,800', rides: 32 },
].map((bike, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-slate-50 rounded-lg">
<div>
<p className="text-sm font-medium text-slate-700">{bike.model}</p>
<p className="text-xs text-slate-400">{bike.rides} rides</p>
</div>
<div className="text-right">
<p className="text-sm font-semibold text-green-600">{bike.earnings}</p>
<p className="text-xs text-slate-400">earnings</p>
</div>
</div>
))}
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden">
<div className="px-5 py-4 border-b border-slate-100">
<h2 className="font-bold text-slate-800">Recent Transactions</h2>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-50">
<tr>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Date</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Description</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Type</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Amount</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{[
{ date: '2024-03-21', desc: 'Rental Payment - r1', type: 'Rental', amount: '৳350', status: 'Completed' },
{ date: '2024-03-21', desc: 'Deposit - r1', type: 'Deposit', amount: '৳5,000', status: 'Completed' },
{ date: '2024-03-20', desc: 'Rental Payment - r2', type: 'Rental', amount: '৳450', status: 'Completed' },
{ date: '2024-03-20', desc: 'Top Up - u1', type: 'Top Up', amount: '৳2,000', status: 'Completed' },
].map((tx, index) => (
<tr key={index} className="hover:bg-slate-50">
<td className="px-4 py-3 text-sm text-slate-600">{tx.date}</td>
<td className="px-4 py-3 text-sm font-medium text-slate-700">{tx.desc}</td>
<td className="px-4 py-3 text-sm text-slate-600">{tx.type}</td>
<td className="px-4 py-3 text-sm font-semibold text-green-600">{tx.amount}</td>
<td className="px-4 py-3">
<span className="text-xs font-medium px-2.5 py-1 rounded-full bg-green-100 text-green-700">
{tx.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
}

View File

@@ -34,9 +34,8 @@ const adminNavItems = [
{ label: 'Fleet Management', href: '/admin/fleet', icon: Bike }, { label: 'Fleet Management', href: '/admin/fleet', icon: Bike },
{ label: 'Damage & Maintenance', href: '/admin/maintenance', icon: Wrench }, { label: 'Damage & Maintenance', href: '/admin/maintenance', icon: Wrench },
{ label: 'Accounting', href: '/admin/accounting', icon: Calculator }, { label: 'Accounting', href: '/admin/accounting', icon: Calculator },
{ label: 'Revenue', href: '/admin/revenue', icon: CreditCard }, { label: 'Hubs', href: '/admin/hub', icon: MapPin },
{ label: 'Reports', href: '/admin/reports', icon: BarChart3 }, { label: 'Reports', href: '/admin/reports', icon: BarChart3 },
{ label: 'Geofences', href: '/admin/geofence', icon: MapPin },
]; ];
const bikerNavItems = [ const bikerNavItems = [