feat: implement comprehensive admin CRUD interfaces for swap stations, users, merchants, and roles with sidebar navigation updates
This commit is contained in:
373
src/app/admin/swap-stations/[id]/page.tsx
Normal file
373
src/app/admin/swap-stations/[id]/page.tsx
Normal file
@@ -0,0 +1,373 @@
|
||||
'use client';
|
||||
|
||||
import { useState, use } from 'react';
|
||||
import { Zap, ArrowLeft, Phone, Mail, MapPin, Battery, Clock, CheckCircle, XCircle, Edit, Trash2, RefreshCw, ZapOff } from 'lucide-react';
|
||||
|
||||
interface BatterySlot {
|
||||
slotId: string;
|
||||
batteryId: string | null;
|
||||
charge: number;
|
||||
status: 'empty' | 'charging' | 'ready' | 'in-use';
|
||||
lastUpdate: string;
|
||||
}
|
||||
|
||||
interface SwapStation {
|
||||
id: string;
|
||||
name: string;
|
||||
address: string;
|
||||
city: string;
|
||||
phone: string;
|
||||
status: 'active' | 'inactive' | 'maintenance';
|
||||
lastRestock: string;
|
||||
hubId?: string;
|
||||
}
|
||||
|
||||
interface SwapRequest {
|
||||
id: string;
|
||||
stationId: string;
|
||||
bikerName: string;
|
||||
bikerPhone: string;
|
||||
bikeModel: string;
|
||||
currentBattery: number;
|
||||
requestedBattery: number;
|
||||
status: 'pending' | 'approved' | 'rejected' | 'completed';
|
||||
requestedAt: string;
|
||||
processedAt?: string;
|
||||
}
|
||||
|
||||
const generateSlots = (total: number) => {
|
||||
const slots: BatterySlot[] = [];
|
||||
const statuses: BatterySlot['status'][] = ['ready', 'ready', 'ready', 'charging', 'charging', 'in-use'];
|
||||
|
||||
for (let i = 1; i <= total; i++) {
|
||||
const status = i <= 12 ? 'ready' : i <= 17 ? 'charging' : 'in-use';
|
||||
slots.push({
|
||||
slotId: `SL-${String(i).padStart(2, '0')}`,
|
||||
batteryId: `BAT-${String(1000 + i).padStart(4, '0')}`,
|
||||
charge: status === 'ready' ? 85 + Math.floor(Math.random() * 15) : status === 'charging' ? 45 + Math.floor(Math.random() * 35) : 30 + Math.floor(Math.random() * 30),
|
||||
status,
|
||||
lastUpdate: '2024-02-16 10:00'
|
||||
});
|
||||
}
|
||||
return slots;
|
||||
};
|
||||
|
||||
const mockStation: SwapStation = {
|
||||
id: 'SS-001',
|
||||
name: 'JAIBEN Swap Hub - Gulshan',
|
||||
address: 'House 45, Road 13, Gulshan 1',
|
||||
city: 'Dhaka',
|
||||
phone: '+8801712345670',
|
||||
status: 'active',
|
||||
lastRestock: '2024-02-15',
|
||||
hubId: 'HUB-001'
|
||||
};
|
||||
|
||||
const mockRequests: SwapRequest[] = [
|
||||
{ id: 'SWR-001', stationId: 'SS-001', bikerName: 'Karim Khan', bikerPhone: '+8801712345678', bikeModel: 'Yamaha NMAX', currentBattery: 15, requestedBattery: 100, status: 'pending', requestedAt: '2024-02-16 10:30:00' },
|
||||
{ id: 'SWR-002', stationId: 'SS-001', bikerName: 'Jamal Mia', bikerPhone: '+8801812345678', bikeModel: 'Honda PCX', currentBattery: 20, requestedBattery: 100, status: 'approved', requestedAt: '2024-02-16 09:15:00' },
|
||||
{ id: 'SWR-003', stationId: 'SS-001', bikerName: 'Rahim Ali', bikerPhone: '+8801912345678', bikeModel: 'TVS Ntorq', currentBattery: 10, requestedBattery: 100, status: 'pending', requestedAt: '2024-02-16 08:45:00' },
|
||||
{ id: 'SWR-004', stationId: 'SS-001', bikerName: 'Rashid Khan', bikerPhone: '+8801512345678', bikeModel: 'Suzuki Burgman', currentBattery: 25, requestedBattery: 100, status: 'rejected', requestedAt: '2024-02-15 18:30:00' },
|
||||
{ id: 'SWR-005', stationId: 'SS-001', bikerName: 'Saif Ahmed', bikerPhone: '+8801612345678', bikeModel: 'Yamaha NMAX', currentBattery: 18, requestedBattery: 100, status: 'completed', requestedAt: '2024-02-15 14:20:00', processedAt: '2024-02-15 14:25:00' },
|
||||
];
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
active: 'bg-green-100 text-green-700',
|
||||
inactive: 'bg-slate-100 text-slate-700',
|
||||
maintenance: 'bg-amber-100 text-amber-700'
|
||||
};
|
||||
|
||||
const slotStatusColors: Record<string, string> = {
|
||||
ready: 'bg-green-100 border-green-300',
|
||||
charging: 'bg-amber-100 border-amber-300',
|
||||
'in-use': 'bg-blue-100 border-blue-300',
|
||||
empty: 'bg-slate-100 border-slate-300'
|
||||
};
|
||||
|
||||
const requestStatusColors: Record<string, string> = {
|
||||
pending: 'bg-amber-100 text-amber-700',
|
||||
approved: 'bg-blue-100 text-blue-700',
|
||||
rejected: 'bg-red-100 text-red-700',
|
||||
completed: 'bg-green-100 text-green-700'
|
||||
};
|
||||
|
||||
const requestStatusLabels: Record<string, string> = {
|
||||
pending: 'Pending',
|
||||
approved: 'Approved',
|
||||
rejected: 'Rejected',
|
||||
completed: 'Completed'
|
||||
};
|
||||
|
||||
export default function SwapStationDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = use(params);
|
||||
const [station] = useState<SwapStation>(mockStation);
|
||||
const [slots, setSlots] = useState<BatterySlot[]>(generateSlots(20));
|
||||
const [requests, setRequests] = useState<SwapRequest[]>(mockRequests);
|
||||
const [activeTab, setActiveTab] = useState<'slots' | 'requests' | 'transactions'>('slots');
|
||||
|
||||
const readyCount = slots.filter(s => s.status === 'ready').length;
|
||||
const chargingCount = slots.filter(s => s.status === 'charging').length;
|
||||
const inUseCount = slots.filter(s => s.status === 'in-use').length;
|
||||
const emptyCount = slots.filter(s => s.status === 'empty').length;
|
||||
|
||||
const handleApprove = (requestId: string) => {
|
||||
setRequests(requests.map(r => r.id === requestId ? { ...r, status: 'approved' as const, processedAt: new Date().toISOString().replace('T', ' ').split('.')[0] } : r));
|
||||
};
|
||||
|
||||
const handleReject = (requestId: string) => {
|
||||
setRequests(requests.map(r => r.id === requestId ? { ...r, status: 'rejected' as const, processedAt: new Date().toISOString().replace('T', ' ').split('.')[0] } : r));
|
||||
};
|
||||
|
||||
const handleComplete = (requestId: string) => {
|
||||
setRequests(requests.map(r => r.id === requestId ? { ...r, status: 'completed' as const, processedAt: new Date().toISOString().replace('T', ' ').split('.')[0] } : r));
|
||||
};
|
||||
|
||||
const pendingRequests = requests.filter(r => r.status === 'pending');
|
||||
const completedTransactions = requests.filter(r => r.status === 'completed');
|
||||
|
||||
const getChargeColor = (charge: number) => {
|
||||
if (charge >= 80) return 'text-green-600';
|
||||
if (charge >= 50) return 'text-amber-600';
|
||||
return 'text-red-600';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 lg:p-6">
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<a href="/admin/swap-stations" className="p-2 hover:bg-slate-100 rounded-lg">
|
||||
<ArrowLeft className="w-5 h-5 text-slate-600" />
|
||||
</a>
|
||||
<div className="flex-1">
|
||||
<h1 className="text-xl lg:text-2xl font-extrabold text-slate-800">{station.name}</h1>
|
||||
<p className="text-sm text-slate-500">{station.id} • Last restock: {station.lastRestock}</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button className="py-2 px-4 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>
|
||||
<button className="py-2 px-4 bg-red-500 text-white rounded-lg text-sm hover:bg-red-600 flex items-center gap-2">
|
||||
<Trash2 className="w-4 h-4" /> Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 lg:grid-cols-5 gap-4 mb-6">
|
||||
<div className="bg-white rounded-xl p-4 shadow-sm border border-slate-100">
|
||||
<p className="text-xl lg:text-2xl font-bold text-green-600">{readyCount}</p>
|
||||
<p className="text-sm text-slate-500">Ready to Swap</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 shadow-sm border border-slate-100">
|
||||
<p className="text-xl lg:text-2xl font-bold text-amber-600">{chargingCount}</p>
|
||||
<p className="text-sm text-slate-500">Charging</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 shadow-sm border border-slate-100">
|
||||
<p className="text-xl lg:text-2xl font-bold text-blue-600">{inUseCount}</p>
|
||||
<p className="text-sm text-slate-500">In Use</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 shadow-sm border border-slate-100">
|
||||
<p className="text-xl lg:text-2xl font-bold text-amber-600">{pendingRequests.length}</p>
|
||||
<p className="text-sm text-slate-500">Pending Requests</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 shadow-sm border border-slate-100">
|
||||
<p className="text-xl lg:text-2xl font-bold text-blue-600">{completedTransactions.length}</p>
|
||||
<p className="text-sm text-slate-500">Swaps Today</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid lg:grid-cols-2 gap-6 mb-6">
|
||||
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
|
||||
<h3 className="font-semibold text-slate-800 mb-4">Station Information</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Zap className="w-4 h-4 text-amber-500" />
|
||||
<span className="text-sm text-slate-600">{station.name}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs px-2 py-1 rounded-full bg-slate-50 text-slate-600">Status</span>
|
||||
<span className={`text-xs px-2 py-1 rounded-full ${statusColors[station.status]}`}>
|
||||
{station.status.toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Phone className="w-4 h-4 text-slate-400" />
|
||||
<span className="text-sm text-slate-600">{station.phone}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<MapPin className="w-4 h-4 text-slate-400" />
|
||||
<span className="text-sm text-slate-600">{station.address}, {station.city}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
|
||||
<h3 className="font-semibold text-slate-800 mb-4">Quick Actions</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-slate-600">Total Slots</span>
|
||||
<span className="font-medium text-slate-800">20</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-slate-600">Ready</span>
|
||||
<span className="font-medium text-green-600">{readyCount}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-slate-600">Charging</span>
|
||||
<span className="font-medium text-amber-600">{chargingCount}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-slate-600">In Use</span>
|
||||
<span className="font-medium text-blue-600">{inUseCount}</span>
|
||||
</div>
|
||||
<button className="w-full mt-2 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50 flex items-center justify-center gap-2">
|
||||
<RefreshCw className="w-4 h-4" /> Restock Batteries
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-100 mb-6">
|
||||
<div className="flex gap-6 p-4 border-b border-slate-100 overflow-x-auto">
|
||||
{(['slots', 'requests', 'transactions'] as const).map(tab => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className={`text-sm font-medium pb-2 border-b-2 whitespace-nowrap transition-colors ${
|
||||
activeTab === tab
|
||||
? 'text-accent border-accent'
|
||||
: 'text-slate-500 border-transparent hover:text-slate-700'
|
||||
}`}
|
||||
>
|
||||
{tab === 'slots' ? 'Battery Slots' : tab === 'requests' ? 'Swap Requests' : 'Transactions'}
|
||||
{tab === 'requests' && pendingRequests.length > 0 && (
|
||||
<span className="ml-2 px-1.5 py-0.5 bg-amber-500 text-white text-xs rounded-full">{pendingRequests.length}</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{activeTab === 'slots' && (
|
||||
<div className="p-4">
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-green-500" />
|
||||
<span className="text-xs text-slate-600">Ready ({readyCount})</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-amber-500" />
|
||||
<span className="text-xs text-slate-600">Charging ({chargingCount})</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-blue-500" />
|
||||
<span className="text-xs text-slate-600">In Use ({inUseCount})</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-slate-300" />
|
||||
<span className="text-xs text-slate-600">Empty ({emptyCount})</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 lg:grid-cols-10 gap-3">
|
||||
{slots.map(slot => (
|
||||
<div
|
||||
key={slot.slotId}
|
||||
className={`
|
||||
aspect-square rounded-lg border-2 p-2 flex flex-col justify-between
|
||||
${slotStatusColors[slot.status]}
|
||||
${slot.status === 'empty' ? 'opacity-50' : ''}
|
||||
`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium text-slate-600">{slot.slotId}</span>
|
||||
{slot.status === 'charging' && (
|
||||
<div className="w-2 h-2 rounded-full bg-amber-500 animate-pulse" />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{slot.batteryId ? (
|
||||
<>
|
||||
<p className="text-[10px] text-slate-500 truncate">{slot.batteryId}</p>
|
||||
<p className={`text-lg font-bold ${getChargeColor(slot.charge)}`}>{slot.charge}%</p>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<ZapOff className="w-5 h-5 text-slate-400" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'requests' && (
|
||||
<div className="p-4">
|
||||
<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">Request</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Biker</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">Current %</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Time</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Status</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-50">
|
||||
{requests.map(request => (
|
||||
<tr key={request.id} className="hover:bg-slate-50">
|
||||
<td className="px-4 py-3 text-sm font-medium text-slate-700">{request.id}</td>
|
||||
<td className="px-4 py-3">
|
||||
<p className="text-sm text-slate-600">{request.bikerName}</p>
|
||||
<p className="text-xs text-slate-400">{request.bikerPhone}</p>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-slate-600">{request.bikeModel}</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Battery className="w-4 h-4 text-green-500" />
|
||||
<span className={`text-sm font-medium ${request.currentBattery <= 20 ? 'text-red-600' : 'text-green-600'}`}>
|
||||
{request.currentBattery}%
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-slate-500">{request.requestedAt}</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`text-xs px-2 py-1 rounded-full ${requestStatusColors[request.status]}`}>
|
||||
{requestStatusLabels[request.status]}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{request.status === 'pending' && (
|
||||
<div className="flex gap-1">
|
||||
<button onClick={() => handleApprove(request.id)} className="px-2 py-1 text-xs bg-blue-500 text-white rounded hover:bg-blue-600">Approve</button>
|
||||
<button onClick={() => handleReject(request.id)} className="px-2 py-1 text-xs bg-red-500 text-white rounded hover:bg-red-600">Reject</button>
|
||||
</div>
|
||||
)}
|
||||
{request.status === 'approved' && (
|
||||
<button onClick={() => handleComplete(request.id)} className="px-2 py-1 text-xs bg-green-500 text-white rounded hover:bg-green-600">Complete</button>
|
||||
)}
|
||||
{request.status === 'completed' && <span className="text-xs text-slate-400">Done</span>}
|
||||
{request.status === 'rejected' && <span className="text-xs text-red-500">Rejected</span>}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'transactions' && (
|
||||
<div className="p-4">
|
||||
<div className="text-center py-12 text-slate-500">
|
||||
<Zap className="w-12 h-12 mx-auto mb-4 text-amber-300" />
|
||||
<p className="text-sm">Select a request tab to view completed transactions</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
409
src/app/admin/swap-stations/page.tsx
Normal file
409
src/app/admin/swap-stations/page.tsx
Normal file
@@ -0,0 +1,409 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Zap, Search, Plus, X, MapPin, Phone, Battery, Clock, CheckCircle, XCircle, AlertCircle, Eye, Edit, Trash2, Filter } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface SwapStation {
|
||||
id: string;
|
||||
name: string;
|
||||
address: string;
|
||||
city: string;
|
||||
phone: string;
|
||||
status: 'active' | 'inactive' | 'maintenance';
|
||||
totalSlots: number;
|
||||
charging: number;
|
||||
readyToSwap: number;
|
||||
inUse: number;
|
||||
lastRestock: string;
|
||||
hubId?: string;
|
||||
}
|
||||
|
||||
const mockStations: SwapStation[] = [
|
||||
{
|
||||
id: 'SS-001',
|
||||
name: 'JAIBEN Swap Hub - Gulshan',
|
||||
address: 'House 45, Road 13, Gulshan 1',
|
||||
city: 'Dhaka',
|
||||
phone: '+8801712345670',
|
||||
status: 'active',
|
||||
totalSlots: 20,
|
||||
charging: 3,
|
||||
readyToSwap: 12,
|
||||
inUse: 5,
|
||||
lastRestock: '2024-02-15',
|
||||
hubId: 'HUB-001'
|
||||
},
|
||||
{
|
||||
id: 'SS-002',
|
||||
name: 'JAIBEN Swap Hub - Banani',
|
||||
address: 'House 8, Road 11, Banani',
|
||||
city: 'Dhaka',
|
||||
phone: '+8801712345671',
|
||||
status: 'active',
|
||||
totalSlots: 15,
|
||||
charging: 2,
|
||||
readyToSwap: 8,
|
||||
inUse: 5,
|
||||
lastRestock: '2024-02-14',
|
||||
hubId: 'HUB-001'
|
||||
},
|
||||
{
|
||||
id: 'SS-003',
|
||||
name: 'JAIBEN Swap Hub - Uttara',
|
||||
address: 'Sector 10, Road 2, Uttara',
|
||||
city: 'Dhaka',
|
||||
phone: '+8801712345672',
|
||||
status: 'maintenance',
|
||||
totalSlots: 10,
|
||||
charging: 0,
|
||||
readyToSwap: 0,
|
||||
inUse: 0,
|
||||
lastRestock: '2024-02-10'
|
||||
},
|
||||
{
|
||||
id: 'SS-004',
|
||||
name: 'JAIBEN Swap Hub - Mirpur',
|
||||
address: 'Section 10, Mirpur',
|
||||
city: 'Dhaka',
|
||||
phone: '+8801712345673',
|
||||
status: 'active',
|
||||
totalSlots: 25,
|
||||
charging: 5,
|
||||
readyToSwap: 15,
|
||||
inUse: 5,
|
||||
lastRestock: '2024-02-16'
|
||||
}
|
||||
];
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
active: 'bg-green-100 text-green-700',
|
||||
inactive: 'bg-slate-100 text-slate-700',
|
||||
maintenance: 'bg-amber-100 text-amber-700'
|
||||
};
|
||||
|
||||
const statusLabels: Record<string, string> = {
|
||||
active: 'Active',
|
||||
inactive: 'Inactive',
|
||||
maintenance: 'Maintenance'
|
||||
};
|
||||
|
||||
export default function SwapStationsPage() {
|
||||
const [stations, setStations] = useState<SwapStation[]>(mockStations);
|
||||
const [search, setSearch] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState('all');
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [editingStation, setEditingStation] = useState<SwapStation | null>(null);
|
||||
const [selectedStation, setSelectedStation] = useState<SwapStation | null>(null);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
address: '',
|
||||
city: 'Dhaka',
|
||||
phone: '',
|
||||
status: 'active' as SwapStation['status'],
|
||||
totalSlots: 10,
|
||||
hubId: 'HUB-001'
|
||||
});
|
||||
|
||||
const filteredStations = stations.filter(s => {
|
||||
const matchesSearch = s.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
s.address.toLowerCase().includes(search.toLowerCase()) ||
|
||||
s.id.toLowerCase().includes(search.toLowerCase());
|
||||
const matchesStatus = statusFilter === 'all' || s.status === statusFilter;
|
||||
return matchesSearch && matchesStatus;
|
||||
});
|
||||
|
||||
const stats = {
|
||||
total: stations.length,
|
||||
active: stations.filter(s => s.status === 'active').length,
|
||||
totalSlots: stations.reduce((a, s) => a + s.totalSlots, 0),
|
||||
readyToSwap: stations.reduce((a, s) => a + s.readyToSwap, 0),
|
||||
charging: stations.reduce((a, s) => a + s.charging, 0)
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (!formData.name || !formData.address || !formData.phone) return;
|
||||
|
||||
if (editingStation) {
|
||||
setStations(stations.map(s => s.id === editingStation.id ? { ...s, ...formData } : s));
|
||||
} else {
|
||||
const newStation: SwapStation = {
|
||||
id: `SS-${String(stations.length + 1).padStart(3, '0')}`,
|
||||
...formData,
|
||||
charging: 2,
|
||||
readyToSwap: Math.floor(formData.totalSlots * 0.6),
|
||||
inUse: formData.totalSlots - Math.floor(formData.totalSlots * 0.6) - 2,
|
||||
lastRestock: new Date().toISOString().split('T')[0]
|
||||
};
|
||||
setStations([...stations, newStation]);
|
||||
}
|
||||
|
||||
setShowCreateModal(false);
|
||||
setEditingStation(null);
|
||||
setFormData({ name: '', address: '', city: 'Dhaka', phone: '', status: 'active', totalSlots: 10, hubId: 'HUB-001' });
|
||||
};
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
if (confirm('Are you sure you want to delete this station?')) {
|
||||
setStations(stations.filter(s => s.id !== id));
|
||||
}
|
||||
setSelectedStation(null);
|
||||
};
|
||||
|
||||
const openEdit = (station: SwapStation) => {
|
||||
setEditingStation(station);
|
||||
setFormData({
|
||||
name: station.name,
|
||||
address: station.address,
|
||||
city: station.city,
|
||||
phone: station.phone,
|
||||
status: station.status,
|
||||
totalSlots: station.totalSlots,
|
||||
hubId: station.hubId || 'HUB-001'
|
||||
});
|
||||
setShowCreateModal(true);
|
||||
setSelectedStation(null);
|
||||
};
|
||||
|
||||
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">Swap Stations (P3)</h1>
|
||||
<p className="text-sm text-slate-500 mt-1">Manage battery swap stations across Dhaka</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditingStation(null);
|
||||
setFormData({ name: '', address: '', city: 'Dhaka', phone: '', status: 'active', totalSlots: 10, hubId: 'HUB-001' });
|
||||
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 Station
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 lg:grid-cols-5 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">{stats.total}</p>
|
||||
<p className="text-sm text-slate-500">Total Stations</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">{stats.active}</p>
|
||||
<p className="text-sm text-slate-500">Active</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">{stats.totalSlots}</p>
|
||||
<p className="text-sm text-slate-500">Total Slots</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
|
||||
<p className="text-2xl font-extrabold text-purple-600">{stats.readyToSwap}</p>
|
||||
<p className="text-sm text-slate-500">Ready to Swap</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">{stats.charging}</p>
|
||||
<p className="text-sm text-slate-500">Charging</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="flex flex-col lg:flex-row gap-4">
|
||||
<div className="relative flex-1 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 stations..."
|
||||
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>
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="py-2 px-4 border border-slate-200 rounded-lg text-sm"
|
||||
>
|
||||
<option value="all">All Status</option>
|
||||
<option value="active">Active</option>
|
||||
<option value="inactive">Inactive</option>
|
||||
<option value="maintenance">Maintenance</option>
|
||||
</select>
|
||||
</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">Station</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Location</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Slots</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Batteries</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Status</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-50">
|
||||
{filteredStations.map(station => (
|
||||
<tr key={station.id} className="hover:bg-slate-50 transition-colors">
|
||||
<td className="px-4 py-3">
|
||||
<Link
|
||||
href={`/admin/swap-stations/${station.id.toLowerCase()}`}
|
||||
className="text-left hover:text-accent"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Zap className="w-4 h-4 text-amber-500" />
|
||||
<span className="text-sm font-medium text-slate-700">{station.name}</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-400 pl-6">{station.id}</p>
|
||||
</Link>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<p className="text-sm text-slate-600">{station.address}</p>
|
||||
<p className="text-xs text-slate-400">{station.city}</p>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Battery className="w-4 h-4 text-blue-400" />
|
||||
<span className="text-sm font-medium text-slate-700">
|
||||
{station.readyToSwap}/{station.totalSlots}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-400">{station.charging} charging</p>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<p className="text-sm font-medium text-green-600">{station.inUse}</p>
|
||||
<p className="text-xs text-slate-400">in use</p>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`text-xs font-medium px-2 py-1 rounded-full ${statusColors[station.status]}`}>
|
||||
{statusLabels[station.status]}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<Link
|
||||
href={`/admin/swap-stations/${station.id.toLowerCase()}`}
|
||||
className="p-2 hover:bg-blue-100 rounded-lg"
|
||||
>
|
||||
<Eye className="w-4 h-4 text-blue-400" />
|
||||
</Link>
|
||||
<button onClick={() => openEdit(station)} className="p-2 hover:bg-slate-100 rounded-lg">
|
||||
<Edit className="w-4 h-4 text-slate-400" />
|
||||
</button>
|
||||
<button onClick={() => handleDelete(station.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-lg max-h-[90vh] overflow-y-auto">
|
||||
<div className="p-4 border-b border-slate-100 flex justify-between items-center sticky top-0 bg-white">
|
||||
<h3 className="font-semibold text-slate-800">{editingStation ? 'Edit Station' : 'Add New Station'}</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">Station 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="e.g., JAIBEN Swap Hub - Gulshan"
|
||||
/>
|
||||
</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 className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm text-slate-600">City</label>
|
||||
<select
|
||||
value={formData.city}
|
||||
onChange={(e) => setFormData({ ...formData, city: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
|
||||
>
|
||||
<option value="Dhaka">Dhaka</option>
|
||||
<option value="Chittagong">Chittagong</option>
|
||||
<option value="Sylhet">Sylhet</option>
|
||||
<option value="Khulna">Khulna</option>
|
||||
</select>
|
||||
</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>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm text-slate-600">Total Slots</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.totalSlots}
|
||||
onChange={(e) => setFormData({ ...formData, totalSlots: parseInt(e.target.value) || 10 })}
|
||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
|
||||
min={1}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-slate-600">Status</label>
|
||||
<select
|
||||
value={formData.status}
|
||||
onChange={(e) => setFormData({ ...formData, status: e.target.value as SwapStation['status'] })}
|
||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
|
||||
>
|
||||
<option value="active">Active</option>
|
||||
<option value="inactive">Inactive</option>
|
||||
<option value="maintenance">Maintenance</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 border-t border-slate-100 flex justify-end gap-2 sticky bottom-0 bg-white">
|
||||
<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 || !formData.phone}
|
||||
className="px-4 py-2 bg-accent text-white rounded-lg text-sm disabled:opacity-50"
|
||||
>
|
||||
{editingStation ? 'Update' : 'Create'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user