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" />
|
||||
@@ -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 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>
|
||||
@@ -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 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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
438
src/app/admin/hub/[id]/page.tsx
Normal file
438
src/app/admin/hub/[id]/page.tsx
Normal 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
371
src/app/admin/hub/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -34,9 +34,8 @@ const adminNavItems = [
|
||||
{ label: 'Fleet Management', href: '/admin/fleet', icon: Bike },
|
||||
{ label: 'Damage & Maintenance', href: '/admin/maintenance', icon: Wrench },
|
||||
{ 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: 'Geofences', href: '/admin/geofence', icon: MapPin },
|
||||
];
|
||||
|
||||
const bikerNavItems = [
|
||||
|
||||
Reference in New Issue
Block a user