refactor: replace revenue and geofence pages with new hub management system
This commit is contained in:
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user