Add full FOCO investor management system with CRUD, investments, and transactions
This commit is contained in:
307
src/app/admin/fleet/map/page.tsx
Normal file
307
src/app/admin/fleet/map/page.tsx
Normal file
@@ -0,0 +1,307 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
Bike as BikeIcon, ArrowLeft, Search, Filter, MapPin,
|
||||
Battery, User, Wrench, X, Map, MoreHorizontal, Eye, Plus
|
||||
} from 'lucide-react';
|
||||
|
||||
interface Bike {
|
||||
id: string;
|
||||
model: string;
|
||||
brand: string;
|
||||
image: string;
|
||||
plateNumber: string;
|
||||
status: 'available' | 'rented' | 'maintenance' | 'retired';
|
||||
batteryLevel: number;
|
||||
location: string;
|
||||
assignedTo?: string;
|
||||
investorId?: string;
|
||||
purchaseDate?: string;
|
||||
purchasePrice?: number;
|
||||
currentRent?: number;
|
||||
totalRides?: number;
|
||||
totalDistance?: number;
|
||||
lastService?: string;
|
||||
nextService?: string;
|
||||
insuranceExpiry?: string;
|
||||
registrationExpiry?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
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: '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' },
|
||||
{ id: 'EV005', model: 'Bajaj Chetak', brand: 'Bajaj', image: '', plateNumber: 'Dhaka Metro Cha-A-7890', status: 'available', batteryLevel: 100, location: 'Dhanmondi', purchaseDate: '2024-02-15', purchasePrice: 138000, totalRides: 67, totalDistance: 890, lastService: '2024-03-18', nextService: '2024-04-18' },
|
||||
{ id: 'EV006', model: 'Hero Eddy', brand: 'Hero', image: '', plateNumber: 'Dhaka Metro Cha-B-1122', status: 'rented', batteryLevel: 88, location: 'Mirpur 1', assignedTo: 'Mahir Khan', investorId: 'inv2', purchaseDate: '2024-01-05', purchasePrice: 115000, currentRent: 320, totalRides: 178, totalDistance: 2890, lastService: '2024-03-05', nextService: '2024-04-05' },
|
||||
{ id: 'EV007', model: 'Okinawa Ridge', brand: 'Okinawa', image: '', plateNumber: 'Dhaka Metro Cha-B-3344', status: 'available', batteryLevel: 92, location: 'Gulshan 2', purchaseDate: '2024-02-20', purchasePrice: 122000, totalRides: 45, totalDistance: 567, lastService: '2024-03-20', nextService: '2024-04-20' },
|
||||
{ id: 'EV008', model: 'Ampere Magnus', brand: 'Ampere', image: '', plateNumber: 'Dhaka Metro Cha-B-5566', status: 'maintenance', batteryLevel: 15, location: 'Workshop - Dhanmondi', purchaseDate: '2023-11-01', purchasePrice: 98000, totalRides: 234, totalDistance: 4120, lastService: '2024-03-22', nextService: '2024-03-27' },
|
||||
{ id: 'EV010', model: 'Benling Aura', brand: 'Benling', image: '', plateNumber: 'Dhaka Metro Cha-C-9900', status: 'rented', batteryLevel: 71, location: 'Banani', assignedTo: 'Ovi Rahman', investorId: 'inv1', purchaseDate: '2024-02-10', purchasePrice: 128000, currentRent: 380, totalRides: 112, totalDistance: 1890, lastService: '2024-03-12', nextService: '2024-04-12' },
|
||||
{ id: 'EV011', model: 'Lectrix LXS', brand: 'Lectrix', image: '', plateNumber: 'Dhaka Metro Cha-C-1235', status: 'available', batteryLevel: 98, location: 'Uttara 11', purchaseDate: '2024-03-01', purchasePrice: 135000, totalRides: 23, totalDistance: 345, lastService: '2024-03-21', nextService: '2024-04-21' },
|
||||
];
|
||||
|
||||
const locations: Record<string, { lat: number; lng: number }> = {
|
||||
'Gulshan 1': { lat: 23.7936, lng: 90.4061 },
|
||||
'Banani': { lat: 23.7983, lng: 90.4071 },
|
||||
'Uttara': { lat: 23.8304, lng: 90.4034 },
|
||||
'Uttara 11': { lat: 23.8547, lng: 90.4016 },
|
||||
'Dhanmondi': { lat: 23.7465, lng: 90.3762 },
|
||||
'Mirpur 1': { lat: 23.8090, lng: 90.3706 },
|
||||
'Gulshan 2': { lat: 23.7917, lng: 90.4175 },
|
||||
'Workshop - Banani': { lat: 23.7965, lng: 90.4050 },
|
||||
'Workshop - Dhanmondi': { lat: 23.7438, lng: 90.3738 },
|
||||
'Warehouse': { lat: 23.7880, lng: 90.3900 },
|
||||
};
|
||||
|
||||
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
|
||||
const bikesWithCoords = mockBikes.map((bike, index) => {
|
||||
const base = locations[bike.location] || { lat: 23.79, lng: 90.40 };
|
||||
// Add jitter
|
||||
return {
|
||||
...bike,
|
||||
lat: base.lat + (Math.sin(index * 12.3) * 0.005),
|
||||
lng: base.lng + (Math.cos(index * 15.7) * 0.005),
|
||||
};
|
||||
});
|
||||
|
||||
const filteredBikes = bikesWithCoords.filter(bike =>
|
||||
bike.model.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
bike.id.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
bike.plateNumber.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
const getCoords = (lat: number, lng: number) => {
|
||||
const x = (lng - 90.35) * 800;
|
||||
const y = (23.90 - lat) * 700;
|
||||
return { x, y };
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen overflow-hidden bg-white">
|
||||
{/* Header */}
|
||||
<div className="p-4 border-b border-slate-100 flex items-center justify-between z-10 bg-white">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/admin/fleet" className="p-2 hover:bg-slate-100 rounded-lg text-slate-500 transition-colors">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-slate-800">Fleet Real-time Map</h1>
|
||||
<p className="text-xs text-slate-500">Live locations of all EV bikes</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search bike ID, model..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 pr-4 py-2 border border-slate-200 rounded-lg text-sm w-64 focus:outline-none focus:ring-2 focus:ring-accent"
|
||||
/>
|
||||
</div>
|
||||
<button className="p-2 border border-slate-200 rounded-lg text-slate-500 hover:bg-slate-50">
|
||||
<Filter className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 relative flex">
|
||||
{/* Map Area */}
|
||||
<div className="flex-1 bg-slate-50 relative overflow-hidden cursor-grab active:cursor-grabbing border-r border-slate-100">
|
||||
<svg viewBox="0 0 100 100" className="w-full h-full absolute inset-0" preserveAspectRatio="xMidYMid slice">
|
||||
<defs>
|
||||
<pattern id="grid" width="10" height="10" patternUnits="userSpaceOnUse">
|
||||
<path d="M 10 0 L 0 0 0 10" fill="none" stroke="#e2e8f0" strokeWidth="0.2" />
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100" height="100" fill="url(#grid)" />
|
||||
|
||||
{/* Roads visualization */}
|
||||
<path d="M0,45 Q50,48 100,42" fill="none" stroke="#f1f5f9" strokeWidth="6" strokeLinecap="round" />
|
||||
<path d="M40,0 Q42,50 38,100" fill="none" stroke="#f1f5f9" strokeWidth="8" strokeLinecap="round" />
|
||||
</svg>
|
||||
|
||||
{/* Markers Layer (HTML) */}
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
{filteredBikes.map((bike) => {
|
||||
const { x, y } = getCoords(bike.lat, bike.lng);
|
||||
const isSelected = selectedBike?.id === bike.id;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={bike.id}
|
||||
style={{ left: `${x}%`, top: `${y}%` }}
|
||||
className="absolute -translate-x-1/2 -translate-y-full pointer-events-auto group"
|
||||
onClick={() => setSelectedBike(bike)}
|
||||
>
|
||||
<div className="relative flex flex-col items-center">
|
||||
{/* Bike Marker */}
|
||||
<div className={`relative w-10 h-12 flex flex-col items-center justify-center transition-transform hover:scale-110 ${isSelected ? 'scale-110 z-20' : 'z-10'}`}>
|
||||
{/* Marker Tip */}
|
||||
<div className="absolute bottom-0 w-1 h-3 bg-slate-400 rotate-180 rounded-full" />
|
||||
|
||||
{/* Marker Body */}
|
||||
<div className={`w-10 h-10 rounded-full border-2 border-white shadow-lg flex items-center justify-center ${
|
||||
bike.status === 'available' ? 'bg-green-500' :
|
||||
bike.status === 'rented' ? 'bg-blue-500' :
|
||||
bike.status === 'maintenance' ? 'bg-amber-500' :
|
||||
'bg-slate-400'
|
||||
}`}>
|
||||
<BikeIcon className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
|
||||
{/* ID Tag (on hover) */}
|
||||
<div className="absolute -top-8 left-1/2 -translate-x-1/2 opacity-0 group-hover:opacity-100 transition-opacity bg-slate-800 text-white text-[10px] px-2 py-0.5 rounded whitespace-nowrap font-bold shadow-sm pointer-events-none">
|
||||
{bike.id}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Selection Pulse */}
|
||||
{isSelected && (
|
||||
<div className="absolute bottom-0 w-8 h-2 bg-accent/20 rounded-[100%] blur-sm animate-pulse" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Quick Info Overlay */}
|
||||
<div className="absolute top-4 left-4 space-y-2 pointer-events-none">
|
||||
<div className="bg-white/90 backdrop-blur-sm p-4 rounded-xl shadow-xl border border-white/50 w-64 pointer-events-auto">
|
||||
<h3 className="font-bold text-slate-800 mb-3 flex items-center gap-2">
|
||||
<MapPin className="w-4 h-4 text-accent" /> Fleet Distribution
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-slate-500 flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-green-500" /> Available
|
||||
</span>
|
||||
<span className="font-bold text-slate-700">{mockBikes.filter(b => b.status === 'available').length}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-slate-500 flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-blue-500" /> Rented
|
||||
</span>
|
||||
<span className="font-bold text-slate-700">{mockBikes.filter(b => b.status === 'rented').length}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-slate-500 flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-amber-500" /> Maintenance
|
||||
</span>
|
||||
<span className="font-bold text-slate-700">{mockBikes.filter(b => b.status === 'maintenance').length}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Battery Warning */}
|
||||
<div className="bg-amber-50/90 backdrop-blur-sm p-3 rounded-lg border border-amber-100 shadow-sm w-64 pointer-events-auto">
|
||||
<p className="text-xs font-bold text-amber-800 mb-1 flex items-center gap-1">
|
||||
<Battery className="w-3 h-3" /> Low Battery Alert
|
||||
</p>
|
||||
<p className="text-[10px] text-amber-700">3 bikes are below 20% charge.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Floating Controls */}
|
||||
<div className="absolute bottom-6 right-6 flex flex-col gap-2">
|
||||
<button className="w-10 h-10 bg-white rounded-xl shadow-lg border border-slate-200 flex items-center justify-center text-slate-600 hover:bg-slate-50 transition-colors">
|
||||
<Plus className="w-5 h-5" />
|
||||
</button>
|
||||
<button className="w-10 h-10 bg-white rounded-xl shadow-lg border border-slate-200 flex items-center justify-center text-slate-600 hover:bg-slate-50 transition-colors">
|
||||
<div className="w-5 h-0.5 bg-slate-400 rounded-full" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Selected Bike Panel */}
|
||||
{selectedBike && (
|
||||
<div className="w-80 border-l border-slate-100 bg-white flex flex-col animate-in slide-in-from-right duration-300">
|
||||
<div className="p-4 border-b border-slate-100 flex items-center justify-between bg-slate-50/50">
|
||||
<h3 className="font-bold text-slate-800">Bike Details</h3>
|
||||
<button
|
||||
onClick={() => setSelectedBike(null)}
|
||||
className="p-1 hover:bg-slate-200 rounded-lg transition-colors"
|
||||
>
|
||||
<X className="w-4 h-4 text-slate-500" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<div className="w-16 h-16 rounded-2xl bg-blue-50 flex items-center justify-center relative shadow-sm">
|
||||
<BikeIcon className="w-8 h-8 text-blue-600" />
|
||||
<div className={`absolute -bottom-1 -right-1 w-5 h-5 border-2 border-white rounded-full ${
|
||||
selectedBike.status === 'available' ? 'bg-green-500' :
|
||||
selectedBike.status === 'rented' ? 'bg-blue-500' : 'bg-amber-500'
|
||||
}`} />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-lg font-extrabold text-slate-800">{selectedBike.model}</h4>
|
||||
<p className="text-xs text-slate-400 font-medium">{selectedBike.brand} • {selectedBike.id}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 mb-6">
|
||||
<div className="bg-slate-50 rounded-xl p-3 border border-slate-100">
|
||||
<p className="text-[10px] uppercase font-bold text-slate-400 mb-1">Battery</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<Battery className={`w-4 h-4 ${selectedBike.batteryLevel > 50 ? 'text-green-600' : 'text-amber-600'}`} />
|
||||
<span className="font-extrabold text-slate-700">{selectedBike.batteryLevel}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-slate-50 rounded-xl p-3 border border-slate-100">
|
||||
<p className="text-[10px] uppercase font-bold text-slate-400 mb-1">Status</p>
|
||||
<p className="font-extrabold text-slate-800 capitalize leading-none pt-1">{selectedBike.status}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between p-3 rounded-xl border border-slate-100">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-slate-100 rounded-lg"><MapPin className="w-4 h-4 text-slate-600" /></div>
|
||||
<span className="text-sm font-medium text-slate-600">Location</span>
|
||||
</div>
|
||||
<span className="text-sm font-bold text-slate-800">{selectedBike.location}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3 rounded-xl border border-slate-100">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-slate-100 rounded-lg"><User className="w-4 h-4 text-slate-600" /></div>
|
||||
<span className="text-sm font-medium text-slate-600">Assigned To</span>
|
||||
</div>
|
||||
<span className="text-sm font-bold text-slate-800">{selectedBike.assignedTo || 'Unassigned'}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3 rounded-xl border border-slate-100">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-slate-100 rounded-lg"><Wrench className="w-4 h-4 text-slate-600" /></div>
|
||||
<span className="text-sm font-medium text-slate-600">Last Service</span>
|
||||
</div>
|
||||
<span className="text-sm font-bold text-slate-800">{selectedBike.lastService}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 space-y-3">
|
||||
<Link
|
||||
href={`/admin/fleet/${selectedBike.id}`}
|
||||
className="w-full py-3 bg-accent text-white rounded-xl font-bold text-sm flex items-center justify-center gap-2 hover:bg-accent-dark shadow-lg shadow-accent/20 transition-all"
|
||||
>
|
||||
<Eye className="w-4 h-4" /> Full Details
|
||||
</Link>
|
||||
<button className="w-full py-3 bg-white border border-slate-200 text-slate-600 rounded-xl font-bold text-sm hover:bg-slate-50 transition-all flex items-center justify-center gap-2">
|
||||
<MoreHorizontal className="w-4 h-4" /> More Actions
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user