Add full FOCO investor management system with CRUD, investments, and transactions

This commit is contained in:
sazzadulalambd
2026-04-22 01:02:45 +06:00
parent 5338038ea2
commit dab0c11b15
32 changed files with 7673 additions and 89 deletions

View File

@@ -0,0 +1,689 @@
'use client';
import { useState, use } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import {
Bike, MapPin, Battery, User, Wrench, Eye, Edit, Trash2, X, ArrowLeft, PhoneCall,
MessageCircle, Calendar, DollarSign, Clock, Navigation, Car, FileText, Shield, Zap,
GaugeCircle, CheckCircle, AlertTriangle, Activity, Award, TrendingUp, Wallet,
MoreHorizontal, Map, Navigation2, Satellite, FileCheck, FileX, Clock3,
History, CreditCard, User2, Phone, Mail, MapPinned
} from 'lucide-react';
interface GPSDevice {
id: string;
phone: string;
imei: string;
lastActive: string;
signal: number;
battery: number;
}
interface BikeDocument {
type: 'registration' | 'insurance' | 'fitness' | 'permit' | 'other';
number: string;
issueDate: string;
expiryDate: string;
verified: boolean;
}
interface RentalHistory {
id: string;
bikerId: string;
bikerName: string;
type: 'single' | 'shared' | 'rent-to-own';
status: 'active' | 'completed' | 'disputed';
startDate: string;
endDate?: string;
dailyRate: number;
totalPaid: number;
rideCount: number;
}
interface ActivityLog {
id: string;
action: string;
details: string;
date: string;
by: string;
}
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;
investorName?: string;
purchaseDate?: string;
purchasePrice?: number;
currentRent?: number;
totalRides?: number;
totalDistance?: number;
totalEarnings?: number;
lastService?: string;
nextService?: string;
insuranceExpiry?: string;
registrationExpiry?: string;
notes?: string;
gpsDevice?: GPSDevice;
documents?: BikeDocument[];
rentalHistory?: RentalHistory[];
activityLog?: ActivityLog[];
}
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',
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 },
{ type: 'insurance', number: 'INS-EV001-2024', issueDate: '2024-01-15', expiryDate: '2025-01-15', verified: true },
{ type: 'fitness', number: 'FIT-EV001-2024', issueDate: '2024-01-15', expiryDate: '2025-01-15', verified: true },
{ type: 'permit', number: 'PMT-EV001-2024', issueDate: '2024-01-15', expiryDate: '2026-01-15', verified: true },
],
rentalHistory: [
{ id: 'R001', bikerId: 'B001', bikerName: 'Rahim Ahmed', type: 'single', status: 'active', startDate: '2024-03-01', dailyRate: 350, totalPaid: 7350, rideCount: 21 },
{ id: 'R002', bikerId: 'B003', bikerName: 'Jamal Khan', type: 'rent-to-own', status: 'completed', startDate: '2024-02-01', endDate: '2024-02-28', dailyRate: 400, totalPaid: 11200, rideCount: 28 },
{ id: 'R003', bikerId: 'B005', bikerName: 'Mahir Islam', type: 'shared', status: 'completed', startDate: '2024-01-15', endDate: '2024-01-31', dailyRate: 60, totalPaid: 2700, rideCount: 45 },
],
activityLog: [
{ id: 'A001', action: 'Rental Started', details: 'Single rental by Rahim Ahmed', date: '2024-03-01 08:00', by: 'System' },
{ id: 'A002', action: 'Service', details: 'Regular maintenance completed', date: '2024-03-01', by: 'Admin' },
{ id: 'A003', action: 'GPS Update', details: 'New GPS device installed', date: '2024-02-15', by: 'Admin' },
{ id: 'A004', action: 'Rental Completed', details: 'Rent-to-own by Jamal Khan', date: '2024-02-28 23:59', by: 'System' },
{ id: 'A005', action: 'Insurance Renewed', details: 'Insurance renewed for 1 year', date: '2024-01-15', by: 'Admin' },
]
},
{
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',
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 },
{ type: 'insurance', number: 'INS-EV002-2024', issueDate: '2024-02-01', expiryDate: '2025-02-01', verified: true },
{ type: 'fitness', number: 'FIT-EV002-2024', issueDate: '2024-02-01', expiryDate: '2025-02-01', verified: true },
],
rentalHistory: [
{ id: 'R004', bikerId: 'B002', bikerName: 'Karim Singh', type: 'single', status: 'completed', startDate: '2024-02-15', endDate: '2024-02-28', dailyRate: 350, totalPaid: 4900, rideCount: 14 },
],
activityLog: [
{ id: 'A006', action: 'Service', details: 'Regular maintenance', date: '2024-03-15', by: 'Admin' },
{ id: 'A007', action: 'Added to Fleet', details: 'Bike registered in system', date: '2024-02-01', by: 'Admin' },
]
},
{
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',
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 },
{ type: 'insurance', number: 'INS-EV003-2024', issueDate: '2024-01-20', expiryDate: '2025-01-20', verified: true },
{ type: 'fitness', number: 'FIT-EV003-2024', issueDate: '2024-01-20', expiryDate: '2025-01-20', verified: true },
],
rentalHistory: [],
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',
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 },
{ type: 'insurance', number: 'INS-EV004-2023', issueDate: '2023-12-10', expiryDate: '2024-12-10', verified: true },
],
rentalHistory: [],
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',
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 },
{ type: 'insurance', number: 'INS-EV005-2024', issueDate: '2024-02-15', expiryDate: '2025-02-15', verified: true },
],
rentalHistory: [],
activityLog: []
},
];
function getBikeById(id: string): Bike | undefined {
return mockBikes.find(b => b.id === id);
}
const statusColors: Record<string, string> = {
available: 'bg-green-100 text-green-700',
rented: 'bg-blue-100 text-blue-700',
maintenance: 'bg-amber-100 text-amber-700',
retired: 'bg-slate-100 text-slate-500',
};
const docTypeLabels: Record<string, string> = {
registration: 'Registration Certificate',
insurance: 'Insurance',
fitness: 'Fitness Certificate',
permit: 'Road Permit',
other: 'Other',
};
export default function FleetDetailPage({ params }: { params: Promise<{ id: string }> }) {
const resolvedParams = use(params);
const router = useRouter();
const bikeData = getBikeById(resolvedParams.id);
const [activeTab, setActiveTab] = useState('overview');
if (!bikeData) {
return (
<div className="p-4 lg:p-6">
<div className="bg-white rounded-xl p-8 text-center">
<Bike className="w-12 h-12 text-slate-300 mx-auto mb-4" />
<h2 className="text-lg font-bold text-slate-700">Bike Not Found</h2>
<p className="text-sm text-slate-500 mb-4">The bike ID "{resolvedParams.id}" was not found.</p>
<Link href="/admin/fleet" className="text-accent hover:underline">Back to Fleet</Link>
</div>
</div>
);
}
const bike = bikeData;
const tabs = [
{ id: 'overview', label: 'Overview', icon: Bike },
{ id: 'gps', label: 'GPS & Tracking', icon: Navigation2 },
{ id: 'documents', label: 'Documents', icon: FileText },
{ id: 'rental', label: 'Rental History', icon: History },
{ id: 'activity', label: 'Activity Log', icon: Clock3 },
{ id: 'investor', label: 'Investor Info', icon: User2 },
];
return (
<div className="p-4 lg:p-6 min-h-screen">
<div className="flex items-center gap-3 mb-4">
<button onClick={() => router.back()} className="p-2 hover:bg-slate-100 rounded-lg lg:hidden">
<ArrowLeft className="w-5 h-5 text-slate-600" />
</button>
<Link href="/admin/fleet" className="p-2 hover:bg-slate-100 rounded-lg hidden lg:block">
<ArrowLeft className="w-5 h-5 text-slate-600" />
</Link>
<div className="flex-1">
<h1 className="text-xl lg:text-2xl font-extrabold text-slate-800">Bike Details</h1>
<p className="text-sm text-slate-500">ID: {bike.id}</p>
</div>
</div>
<div className="flex flex-wrap gap-2 mb-4 overflow-x-auto pb-2">
{tabs.map(tab => (
<button
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'
}`}
>
<tab.icon className="w-4 h-4" />
{tab.label}
</button>
))}
</div>
{activeTab === 'overview' && <OverviewTab bike={bike} />}
{activeTab === 'gps' && <GPSTab bike={bike} />}
{activeTab === 'documents' && <DocumentsTab bike={bike} />}
{activeTab === 'rental' && <RentalTab bike={bike} />}
{activeTab === 'activity' && <ActivityTab bike={bike} />}
{activeTab === 'investor' && <InvestorTab bike={bike} />}
<div className="fixed bottom-0 left-0 right-0 p-4 bg-white border-t border-slate-200 lg:relative lg:bg-transparent lg:border-0 lg:p-0 z-50">
<div className="flex gap-2 max-w-2xl mx-auto">
<button className="flex-1 py-2 px-4 bg-accent text-white rounded-lg font-semibold text-sm flex items-center justify-center gap-2 hover:bg-accent-dark transition-colors">
<Edit className="w-4 h-4" />
<span>Edit Bike</span>
</button>
<button className="flex-1 py-2 px-4 border border-red-200 text-red-600 rounded-lg font-semibold text-sm flex items-center justify-center gap-2 hover:bg-red-50 transition-colors">
<Trash2 className="w-4 h-4" />
<span>Delete</span>
</button>
</div>
</div>
</div>
);
}
function OverviewTab({ bike }: { bike: Bike }) {
return (
<div className="space-y-4">
<div className="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-slate-100">
<div className="flex items-start gap-4 mb-4">
<div className="w-16 h-16 rounded-xl bg-blue-50 flex items-center justify-center">
<Bike className="w-8 h-8 text-blue-600" />
</div>
<div className="flex-1">
<h2 className="text-xl font-bold text-slate-800">{bike.model}</h2>
<p className="text-sm text-slate-500">{bike.brand} {bike.id}</p>
<div className="flex flex-wrap gap-2 mt-2">
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${statusColors[bike.status]}`}>
{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}
</span>
</div>
</div>
</div>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Plate Number</p>
<p className="font-semibold text-slate-700">{bike.plateNumber}</p>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Battery</p>
<p className={`font-semibold ${bike.batteryLevel > 50 ? 'text-green-600' : bike.batteryLevel > 20 ? 'text-amber-600' : 'text-red-600'}`}>{bike.batteryLevel}%</p>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Current Renter</p>
<p className="font-semibold text-slate-700">{bike.assignedTo || 'Available'}</p>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Daily Rate</p>
<p className="font-semibold text-green-600">{bike.currentRent || 0}</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-slate-100">
<h3 className="font-semibold text-slate-700 mb-3">Performance Metrics</h3>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Total Rides</p>
<p className="text-xl font-bold text-slate-700">{bike.totalRides || 0}</p>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Distance</p>
<p className="text-xl font-bold text-slate-700">{(bike.totalDistance || 0).toLocaleString()} km</p>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Total Earnings</p>
<p className="text-xl font-bold text-green-600">{bike.totalEarnings?.toLocaleString() || 0}</p>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Purchase Price</p>
<p className="text-xl font-bold text-slate-700">{bike.purchasePrice?.toLocaleString() || 0}</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-slate-100">
<h3 className="font-semibold text-slate-700 mb-3">Maintenance</h3>
<div className="grid grid-cols-2 gap-3">
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Last Service</p>
<p className="font-medium text-slate-700">{bike.lastService || 'N/A'}</p>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Next Service</p>
<p className="font-medium text-slate-700">{bike.nextService || 'N/A'}</p>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Insurance Expiry</p>
<p className="font-medium text-slate-700">{bike.insuranceExpiry || 'N/A'}</p>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Registration Expiry</p>
<p className="font-medium text-slate-700">{bike.registrationExpiry || 'N/A'}</p>
</div>
</div>
{bike.notes && (
<div className="mt-3 p-3 bg-amber-50 rounded-lg">
<p className="text-xs text-amber-600 font-medium">Note</p>
<p className="text-sm text-amber-800">{bike.notes}</p>
</div>
)}
</div>
</div>
);
}
function GPSTab({ bike }: { bike: Bike }) {
const gps = bike.gpsDevice;
if (!gps) {
return (
<div className="bg-white rounded-xl p-6 shadow-sm border border-slate-100 text-center">
<Satellite className="w-12 h-12 text-slate-300 mx-auto mb-4" />
<h3 className="font-semibold text-slate-700">No GPS Device</h3>
<p className="text-sm text-slate-500">This bike doesn't have a GPS device installed.</p>
</div>
);
}
return (
<div className="space-y-4">
<div className="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-slate-100">
<div className="flex items-center gap-3 mb-4">
<div className="w-12 h-12 rounded-xl bg-purple-50 flex items-center justify-center">
<Satellite className="w-6 h-6 text-purple-600" />
</div>
<div>
<h3 className="font-semibold text-slate-700">GPS Device</h3>
<p className="text-sm text-slate-500">ID: {gps.id}</p>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Phone Number</p>
<p className="font-semibold text-slate-700 flex items-center gap-2">
<Phone className="w-4 h-4 text-slate-400" /> {gps.phone}
</p>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">IMEI</p>
<p className="font-semibold text-slate-700 font-mono text-sm">{gps.imei}</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-slate-100">
<h3 className="font-semibold text-slate-700 mb-3">Status</h3>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Last Active</p>
<p className="font-medium text-slate-700">{gps.lastActive}</p>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Signal Strength</p>
<p className={`font-semibold ${gps.signal > 70 ? 'text-green-600' : gps.signal > 40 ? 'text-amber-600' : 'text-red-600'}`}>
{gps.signal}%
</p>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">GPS Battery</p>
<p className={`font-semibold ${gps.battery > 50 ? 'text-green-600' : gps.battery > 20 ? 'text-amber-600' : 'text-red-600'}`}>
{gps.battery}%
</p>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Status</p>
<p className="font-semibold text-green-600 flex items-center gap-1">
<CheckCircle className="w-4 h-4" /> Active
</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-slate-100">
<h3 className="font-semibold text-slate-700 mb-3">Live Location</h3>
<div className="bg-slate-100 rounded-lg h-48 flex items-center justify-center">
<div className="text-center">
<MapPinned className="w-8 h-8 text-slate-400 mx-auto mb-2" />
<p className="text-sm text-slate-500">{bike.location}</p>
</div>
</div>
</div>
</div>
);
}
function DocumentsTab({ bike }: { bike: Bike }) {
const docs = bike.documents || [];
return (
<div className="space-y-4">
<div className="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-slate-100">
<h3 className="font-semibold text-slate-700 mb-4">Bike Documents</h3>
{docs.length === 0 ? (
<div className="text-center py-8">
<FileText className="w-12 h-12 text-slate-300 mx-auto mb-4" />
<p className="text-sm text-slate-500">No documents uploaded.</p>
</div>
) : (
<div className="space-y-3">
{docs.map((doc, idx) => (
<div key={idx} className="flex items-center justify-between p-4 border border-slate-200 rounded-lg">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-blue-50 flex items-center justify-center">
<FileText className="w-5 h-5 text-blue-600" />
</div>
<div>
<p className="font-medium text-slate-700">{docTypeLabels[doc.type]}</p>
<p className="text-xs text-slate-500">Number: {doc.number}</p>
<p className="text-xs text-slate-400">Issued: {doc.issueDate} • Expires: {doc.expiryDate}</p>
</div>
</div>
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${
doc.verified ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700'
}`}>
{doc.verified ? <CheckCircle className="w-3 h-3" /> : <Clock3 className="w-3 h-3" />}
{doc.verified ? 'Verified' : 'Pending'}
</span>
</div>
))}
</div>
)}
</div>
<div className="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-slate-100">
<h3 className="font-semibold text-slate-700 mb-3">Add New Document</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<select className="px-3 py-2 border border-slate-200 rounded-lg text-sm">
<option value="">Select Document Type</option>
<option value="registration">Registration Certificate</option>
<option value="insurance">Insurance</option>
<option value="fitness">Fitness Certificate</option>
<option value="permit">Road Permit</option>
<option value="other">Other</option>
</select>
<input type="text" placeholder="Document Number" className="px-3 py-2 border border-slate-200 rounded-lg text-sm" />
<input type="date" placeholder="Issue Date" className="px-3 py-2 border border-slate-200 rounded-lg text-sm" />
<input type="date" placeholder="Expiry Date" className="px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
<button className="w-full mt-3 py-2 bg-accent text-white rounded-lg font-semibold text-sm hover:bg-accent-dark">
Upload Document
</button>
</div>
</div>
);
}
function RentalTab({ bike }: { bike: Bike }) {
const history = bike.rentalHistory || [];
const getRateDisplay = (type: string, rate: number) => {
switch (type) {
case 'single': return `৳${rate}/day`;
case 'shared': return `৳${rate/2}+${rate/2} (2 person)`;
case 'rent-to-own': return `৳${rate}/day`;
default: return `৳${rate}`;
}
};
return (
<div className="space-y-4">
<div className="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-slate-100">
<h3 className="font-semibold text-slate-700 mb-3">Rental History</h3>
{history.length === 0 ? (
<div className="text-center py-8">
<History className="w-12 h-12 text-slate-300 mx-auto mb-4" />
<p className="text-sm text-slate-500">No rental history yet.</p>
</div>
) : (
<div className="space-y-3">
{history.map(rental => (
<div key={rental.id} className="p-4 border border-slate-200 rounded-lg">
<div className="flex items-start justify-between mb-2">
<div>
<p className="font-medium text-slate-700">{rental.bikerName}</p>
<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}
</span>
</div>
<div className="flex flex-wrap gap-3 text-xs">
<span className="bg-slate-100 px-2 py-1 rounded text-slate-600">
{rental.type === 'single' ? 'Single (350/day)' :
rental.type === 'shared' ? 'Shared (60/day)' :
'Rent-to-Own (450/day)'}
</span>
<span className="text-slate-500">
{rental.startDate} {rental.endDate && `to ${rental.endDate}`}
</span>
</div>
<div className="flex justify-between mt-2 pt-2 border-t border-slate-100">
<span className="text-xs text-slate-500">{rental.rideCount} rides</span>
<span className="text-sm font-semibold text-green-600">৳{rental.totalPaid.toLocaleString()}</span>
</div>
</div>
))}
</div>
)}
</div>
<div className="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-slate-100">
<h3 className="font-semibold text-slate-700 mb-3">Rental Rates Info</h3>
<div className="space-y-2">
<div className="flex items-center justify-between p-3 bg-green-50 rounded-lg">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-full bg-green-100 flex items-center justify-center">
<span className="text-xs font-bold text-green-600">1</span>
</div>
<span className="font-medium text-slate-700">Single</span>
</div>
<span className="font-semibold text-green-600">৳350/day</span>
</div>
<div className="flex items-center justify-between p-3 bg-blue-50 rounded-lg">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center">
<span className="text-xs font-bold text-blue-600">2</span>
</div>
<span className="font-medium text-slate-700">Shared (2 Person)</span>
</div>
<span className="font-semibold text-green-600">৳60/day (৳30+৳30)</span>
</div>
<div className="flex items-center justify-between p-3 bg-purple-50 rounded-lg">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-full bg-purple-100 flex items-center justify-center">
<span className="text-xs font-bold text-purple-600">3</span>
</div>
<span className="font-medium text-slate-700">Rent-to-Own</span>
</div>
<span className="font-semibold text-green-600">৳450/day</span>
</div>
</div>
</div>
</div>
);
}
function ActivityTab({ bike }: { bike: Bike }) {
const logs = bike.activityLog || [];
return (
<div className="space-y-4">
<div className="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-slate-100">
<h3 className="font-semibold text-slate-700 mb-3">Activity Log</h3>
{logs.length === 0 ? (
<div className="text-center py-8">
<Clock3 className="w-12 h-12 text-slate-300 mx-auto mb-4" />
<p className="text-sm text-slate-500">No activity yet.</p>
</div>
) : (
<div className="space-y-3">
{logs.map(log => (
<div key={log.id} className="flex items-start gap-3 p-3 bg-slate-50 rounded-lg">
<div className="w-8 h-8 rounded-full bg-white flex items-center justify-center">
<Activity className="w-4 h-4 text-slate-400" />
</div>
<div className="flex-1">
<p className="text-sm font-medium text-slate-700">{log.action}</p>
<p className="text-xs text-slate-500">{log.details}</p>
<p className="text-xs text-slate-400">{log.date} by {log.by}</p>
</div>
</div>
))}
</div>
)}
</div>
</div>
);
}
function InvestorTab({ bike }: { bike: Bike }) {
return (
<div className="space-y-4">
<div className="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-slate-100">
<h3 className="font-semibold text-slate-700 mb-3">Investor Information</h3>
{!bike.investorId ? (
<div className="text-center py-8">
<User2 className="w-12 h-12 text-slate-300 mx-auto mb-4" />
<p className="text-sm text-slate-500">No investor assigned.</p>
</div>
) : (
<div className="space-y-4">
<div className="flex items-center gap-4 p-4 bg-purple-50 rounded-lg">
<div className="w-12 h-12 rounded-full bg-purple-100 flex items-center justify-center">
<User2 className="w-6 h-6 text-purple-600" />
</div>
<div>
<p className="font-semibold text-slate-700">{bike.investorName || 'Investor'}</p>
<p className="text-sm text-slate-500">ID: {bike.investorId}</p>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Purchase Date</p>
<p className="font-semibold text-slate-700">{bike.purchaseDate || 'N/A'}</p>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Purchase Price</p>
<p className="font-semibold text-green-600">{bike.purchasePrice?.toLocaleString() || 0}</p>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Total Earnings</p>
<p className="font-semibold text-green-600">{bike.totalEarnings?.toLocaleString() || 0}</p>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">ROI</p>
<p className="font-semibold text-amber-600">
{bike.purchasePrice && bike.totalEarnings
? ((bike.totalEarnings / bike.purchasePrice) * 100).toFixed(1)
: 0}%
</p>
</div>
</div>
</div>
)}
</div>
<div className="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-slate-100">
<h3 className="font-semibold text-slate-700 mb-3">Payment Summary</h3>
<div className="space-y-2">
<div className="flex items-center justify-between p-3 bg-slate-50 rounded-lg">
<span className="text-sm text-slate-600">Total Paid by Renters</span>
<span className="font-semibold text-green-600">{bike.totalEarnings?.toLocaleString() || 0}</span>
</div>
<div className="flex items-center justify-between p-3 bg-slate-50 rounded-lg">
<span className="text-sm text-slate-600">Investor Share (80%)</span>
<span className="font-semibold text-purple-600">{Math.round((bike.totalEarnings || 0) * 0.8).toLocaleString()}</span>
</div>
<div className="flex items-center justify-between p-3 bg-slate-50 rounded-lg">
<span className="text-sm text-slate-600">Company Share (20%)</span>
<span className="font-semibold text-blue-600">{Math.round((bike.totalEarnings || 0) * 0.2).toLocaleString()}</span>
</div>
</div>
</div>
</div>
);
}

View 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>
);
}

View File

@@ -0,0 +1,865 @@
'use client';
import { useState, use } from 'react';
import Link from 'next/link';
import {
Bike, Search, Filter, Plus, MoreVertical, MapPin, Battery, User, Wrench,
Eye, Edit, Trash2, X, Download, Upload, MoreHorizontal, CheckCircle, XCircle,
AlertTriangle, Calendar, DollarSign, Clock, Navigation, Car, LayoutGrid, List,
Gauge, FileText, Shield, Zap, GaugeCircle, Map
} 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', notes: 'Motor issue - awaiting parts' },
{ 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', insuranceExpiry: '2025-02-15', registrationExpiry: '2026-02-15' },
{ 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', insuranceExpiry: '2025-01-05', registrationExpiry: '2026-01-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', insuranceExpiry: '2025-02-20', registrationExpiry: '2026-02-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', insuranceExpiry: '2024-11-01', registrationExpiry: '2025-11-01', notes: 'Battery replacement needed' },
{ id: 'EV009', model: 'JME Victory', brand: 'JME', image: '', plateNumber: 'Dhaka Metro Cha-B-7788', status: 'retired', batteryLevel: 0, location: 'Warehouse', purchaseDate: '2023-06-15', purchasePrice: 145000, totalRides: 567, totalDistance: 8900, lastService: '2024-01-10', insuranceExpiry: '2024-06-15', registrationExpiry: '2025-06-15', notes: 'Old vehicle - for scrap' },
{ 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', insuranceExpiry: '2025-02-10', registrationExpiry: '2026-02-10' },
{ 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', insuranceExpiry: '2025-03-01', registrationExpiry: '2026-03-01' },
{ 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 statusColors: Record<string, string> = {
available: 'bg-green-100 text-green-700',
rented: 'bg-blue-100 text-blue-700',
maintenance: 'bg-amber-100 text-amber-700',
retired: 'bg-slate-100 text-slate-500',
};
export default function FleetPage() {
const [bikes, setBikes] = useState<Bike[]>(mockBikes);
const [searchQuery, setSearchQuery] = useState('');
const [statusFilter, setStatusFilter] = useState('all');
const [locationFilter, setLocationFilter] = useState('all');
const [selectedBike, setSelectedBike] = useState<Bike | null>(null);
const [showModal, setShowModal] = useState(false);
const [showDetailsModal, setShowDetailsModal] = useState(false);
const [editingBike, setEditingBike] = useState<Bike | null>(null);
const [viewMode, setViewMode] = useState<'table' | 'cards' | 'map'>('table');
const [selectedMapBike, setSelectedMapBike] = useState<Bike | null>(null);
const availableCount = bikes.filter(b => b.status === 'available').length;
const rentedCount = bikes.filter(b => b.status === 'rented').length;
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 filteredBikes = bikes.filter(bike => {
const matchesSearch = bike.model.toLowerCase().includes(searchQuery.toLowerCase()) ||
bike.brand.toLowerCase().includes(searchQuery.toLowerCase()) ||
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;
return matchesSearch && matchesStatus && matchesLocation;
});
const handleAddBike = () => {
setEditingBike(null);
setShowModal(true);
};
const handleEditBike = (bike: Bike) => {
setEditingBike(bike);
setShowModal(true);
};
const handleViewDetails = (bike: Bike) => {
setSelectedBike(bike);
setShowDetailsModal(true);
};
const handleDeleteBike = (id: string) => {
if (confirm('Are you sure you want to delete this bike?')) {
setBikes(bikes.filter(b => b.id !== id));
}
};
const handleSaveBike = (bike: Bike) => {
if (editingBike) {
setBikes(bikes.map(b => b.id === editingBike.id ? bike : b));
} else {
setBikes([...bikes, { ...bike, id: `EV${String(bikes.length + 1).padStart(3, '0')}` }]);
}
setShowModal(false);
};
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 className="flex items-center gap-4">
<div>
<h1 className="text-2xl lg:text-3xl font-extrabold text-slate-800">Fleet Management</h1>
<p className="text-sm text-slate-500 mt-1">Manage your EV bike inventory</p>
</div>
{/* <div className="hidden lg:block w-80 h-32 rounded-xl overflow-hidden border border-slate-200 relative">
<FleetMap bikes={filteredBikes} onSelectBike={setSelectedMapBike} selectedBike={selectedMapBike} />
</div> */}
</div>
<div className="flex items-center gap-2">
<Link
href="/admin/fleet/map"
className="py-2.5 px-4 border border-slate-200 text-slate-700 bg-white rounded-lg font-semibold text-sm hover:bg-slate-50 transition-colors flex items-center gap-2"
>
<Map className="w-4 h-4" /> Map View
</Link>
<button onClick={handleAddBike} 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" /> Register New Bike
</button>
</div>
</div>
{selectedMapBike && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4" onClick={() => setSelectedMapBike(null)}>
<div className="bg-white rounded-xl shadow-xl w-full max-w-sm" onClick={e => e.stopPropagation()}>
<div className="p-4">
<div className="flex items-start gap-3 mb-4">
<div className="w-12 h-12 rounded-xl bg-blue-50 flex items-center justify-center">
<Bike className="w-6 h-6 text-blue-600" />
</div>
<div className="flex-1">
<p className="font-semibold text-slate-700">{selectedMapBike.model}</p>
<p className="text-xs text-slate-500">{selectedMapBike.brand} {selectedMapBike.id}</p>
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2 py-0.5 rounded-full mt-1 ${selectedMapBike.status === 'available' ? 'bg-green-100 text-green-700' :
selectedMapBike.status === 'rented' ? 'bg-blue-100 text-blue-700' :
selectedMapBike.status === 'maintenance' ? 'bg-amber-100 text-amber-700' :
'bg-slate-100 text-slate-500'
}`}>
{selectedMapBike.status}
</span>
</div>
<button onClick={() => setSelectedMapBike(null)} className="p-1 hover:bg-slate-100 rounded-lg">
<X className="w-4 h-4 text-slate-400" />
</button>
</div>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-slate-500">Plate</span>
<span className="font-medium">{selectedMapBike.plateNumber}</span>
</div>
<div className="flex justify-between">
<span className="text-slate-500">Location</span>
<span className="font-medium">{selectedMapBike.location}</span>
</div>
<div className="flex justify-between">
<span className="text-slate-500">Battery</span>
<span className={`font-medium ${selectedMapBike.batteryLevel > 50 ? 'text-green-600' :
selectedMapBike.batteryLevel > 20 ? 'text-amber-600' : 'text-red-600'
}`}>{selectedMapBike.batteryLevel}%</span>
</div>
<div className="flex justify-between">
<span className="text-slate-500">Daily Rate</span>
<span className="font-medium text-green-600">{selectedMapBike.currentRent || 0}</span>
</div>
</div>
<Link
href={`/admin/fleet/${selectedMapBike.id}`}
className="block w-full mt-4 py-2 bg-accent text-white rounded-lg font-semibold text-sm text-center hover:bg-accent-dark"
>
View Details
</Link>
</div>
</div>
</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">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-xl bg-green-50 flex items-center justify-center">
<Bike className="w-6 h-6 text-green-600" />
</div>
<div>
<p className="text-2xl font-extrabold text-slate-800">{availableCount}</p>
<p className="text-sm text-slate-500">Available</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-xl bg-blue-50 flex items-center justify-center">
<User className="w-6 h-6 text-blue-600" />
</div>
<div>
<p className="text-2xl font-extrabold text-slate-800">{rentedCount}</p>
<p className="text-sm text-slate-500">Rented</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-xl bg-amber-50 flex items-center justify-center">
<Wrench className="w-6 h-6 text-amber-600" />
</div>
<div>
<p className="text-2xl font-extrabold text-slate-800">{maintenanceCount}</p>
<p className="text-sm text-slate-500">Maintenance</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-xl bg-slate-100 flex items-center justify-center">
<GaugeCircle className="w-6 h-6 text-slate-600" />
</div>
<div>
<p className="text-2xl font-extrabold text-slate-800">{retiredCount}</p>
<p className="text-sm text-slate-500">Retired</p>
</div>
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-100">
<div className="p-4 border-b border-slate-100 flex flex-col lg:flex-row lg:items-center gap-3">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<input
type="text"
placeholder="Search bikes by model, plate, ID..."
value={searchQuery}
onChange={(e) => setSearchQuery(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 className="flex items-center gap-2 flex-wrap">
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="py-2 px-3 border border-slate-200 rounded-lg text-sm font-medium text-slate-600 bg-white"
>
<option value="all">All Status</option>
<option value="available">Available</option>
<option value="rented">Rented</option>
<option value="maintenance">Maintenance</option>
<option value="retired">Retired</option>
</select>
<select
value={locationFilter}
onChange={(e) => setLocationFilter(e.target.value)}
className="py-2 px-3 border border-slate-200 rounded-lg text-sm font-medium text-slate-600 bg-white"
>
<option value="all">All Locations</option>
{locations.map(loc => (
<option key={loc} value={loc}>{loc}</option>
))}
</select>
<div className="flex items-center bg-slate-100 p-1 rounded-lg">
<button
onClick={() => setViewMode('table')}
className={`py-1.5 px-3 rounded-md text-sm font-medium transition-all flex items-center gap-2 ${viewMode === 'table' ? 'bg-white text-slate-800 shadow-sm' : 'text-slate-500 hover:text-slate-700'}`}
>
<List className="w-4 h-4" /> Table
</button>
<button
onClick={() => setViewMode('cards')}
className={`py-1.5 px-3 rounded-md text-sm font-medium transition-all flex items-center gap-2 ${viewMode === 'cards' ? 'bg-white text-slate-800 shadow-sm' : 'text-slate-500 hover:text-slate-700'}`}
>
<LayoutGrid className="w-4 h-4" /> Cards
</button>
</div>
<button className="py-2 px-3 border border-slate-200 rounded-lg text-sm font-medium text-slate-600 hover:bg-slate-50 flex items-center gap-2 ml-auto">
<Download className="w-4 h-4" /> Export
</button>
<button className="py-2 px-3 border border-slate-200 rounded-lg text-sm font-medium text-slate-600 hover:bg-slate-50 flex items-center gap-2">
<Upload className="w-4 h-4" /> Import
</button>
</div>
</div>
{viewMode === 'table' ? (
<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">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">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>
<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">
{filteredBikes.map(bike => (
<tr key={bike.id} className="hover:bg-slate-50 transition-colors">
<td className="px-4 py-3">
<Link href={`/admin/fleet/${bike.id}`} className="flex items-center gap-3 hover:bg-slate-50 -m-2 p-2 rounded-lg">
<div className="w-10 h-10 rounded-lg bg-blue-50 flex items-center justify-center">
<Bike className="w-5 h-5 text-blue-600" />
</div>
<div>
<p className="text-sm font-medium text-slate-700">{bike.model}</p>
<p className="text-xs text-slate-400">{bike.brand} {bike.id}</p>
</div>
</Link>
</td>
<td className="px-4 py-3">
<span className="text-sm font-medium text-slate-600">{bike.plateNumber}</span>
</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}
</p>
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<Battery className={`w-4 h-4 ${bike.batteryLevel > 50 ? 'text-green-600' : bike.batteryLevel > 20 ? 'text-amber-600' : 'text-red-600'}`} />
<span className={`text-sm font-medium ${bike.batteryLevel > 50 ? 'text-green-600' : bike.batteryLevel > 20 ? 'text-amber-600' : 'text-red-600'}`}>{bike.batteryLevel}%</span>
</div>
</td>
<td className="px-4 py-3">
<p className="text-xs text-slate-600">{bike.totalRides || 0} rides</p>
<p className="text-xs text-slate-400">{(bike.totalDistance || 0).toLocaleString()} km</p>
</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 ${statusColors[bike.status]}`}>
{bike.status}
</span>
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-1">
<button onClick={() => handleViewDetails(bike)} className="p-2 hover:bg-slate-100 rounded-lg" title="View Details">
<Eye className="w-4 h-4 text-blue-500" />
</button>
<button onClick={() => handleEditBike(bike)} className="p-2 hover:bg-slate-100 rounded-lg" title="Edit">
<Edit className="w-4 h-4 text-slate-400" />
</button>
<button onClick={() => handleDeleteBike(bike.id)} className="p-2 hover:bg-red-50 rounded-lg" title="Delete">
<Trash2 className="w-4 h-4 text-red-400" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 p-4">
{filteredBikes.map(bike => (
<Link key={bike.id} href={`/admin/fleet/${bike.id}`} className="block bg-slate-50 rounded-xl p-4 hover:shadow-md transition-shadow">
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-xl bg-white flex items-center justify-center shadow-sm">
<Bike className="w-6 h-6 text-blue-600" />
</div>
<div>
<p className="font-semibold text-slate-700">{bike.model}</p>
<p className="text-xs text-slate-400">{bike.brand} {bike.id}</p>
</div>
</div>
<div className="flex items-center gap-1">
<button onClick={(e) => { e.preventDefault(); handleViewDetails(bike); }} className="p-1.5 hover:bg-white rounded-lg">
<Eye className="w-4 h-4 text-blue-500" />
</button>
<button onClick={(e) => { e.preventDefault(); handleEditBike(bike); }} className="p-1.5 hover:bg-white rounded-lg">
<Edit className="w-4 h-4 text-slate-400" />
</button>
<button onClick={(e) => { e.preventDefault(); handleDeleteBike(bike.id); }} className="p-1.5 hover:bg-red-50 rounded-lg">
<Trash2 className="w-4 h-4 text-red-400" />
</button>
</div>
</div>
<div className="space-y-2 mb-3">
<div className="flex items-center justify-between text-xs">
<span className="text-slate-500">Plate</span>
<span className="font-medium text-slate-700">{bike.plateNumber}</span>
</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>
</div>
<div className="flex items-center justify-between text-xs">
<span className="text-slate-500">Battery</span>
<span className={`font-medium ${bike.batteryLevel > 50 ? 'text-green-600' : bike.batteryLevel > 20 ? 'text-amber-600' : 'text-red-600'}`}>{bike.batteryLevel}%</span>
</div>
<div className="flex items-center justify-between text-xs">
<span className="text-slate-500">Total Rides</span>
<span className="font-medium text-slate-700">{bike.totalRides || 0}</span>
</div>
</div>
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full w-full justify-center ${statusColors[bike.status]}`}>
{bike.status}
</span>
</Link>
))}
</div>
)}
<div className="p-4 border-t border-slate-100 flex items-center justify-between">
<p className="text-sm text-slate-500">
Showing <span className="font-medium">1</span> to <span className="font-medium">{filteredBikes.length}</span> of <span className="font-medium">{bikes.length}</span> bikes
</p>
<div className="flex items-center gap-2">
<button className="px-3 py-1.5 border border-slate-200 rounded-lg text-sm font-medium text-slate-600 hover:bg-slate-50">Previous</button>
<button className="px-3 py-1.5 bg-accent text-white rounded-lg text-sm font-medium">1</button>
<button className="px-3 py-1.5 border border-slate-200 rounded-lg text-sm font-medium text-slate-600 hover:bg-slate-50">Next</button>
</div>
</div>
</div>
{/* Add/Edit Modal */}
{showModal && (
<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-2xl max-h-[90vh] overflow-hidden flex flex-col">
<div className="p-5 border-b border-slate-100 flex items-center justify-between">
<h2 className="text-lg font-bold text-slate-800">
{editingBike ? 'Edit Bike' : 'Register New Bike'}
</h2>
<button onClick={() => setShowModal(false)} className="p-2 hover:bg-slate-100 rounded-lg">
<X className="w-5 h-5 text-slate-400" />
</button>
</div>
<BikeForm
bike={editingBike}
onSave={handleSaveBike}
onCancel={() => setShowModal(false)}
/>
</div>
</div>
)}
{/* Details Modal */}
{showDetailsModal && selectedBike && (
<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-2xl max-h-[90vh] overflow-hidden flex flex-col">
<div className="p-5 border-b border-slate-100 flex items-center justify-between">
<h2 className="text-lg font-bold text-slate-800">Bike Details</h2>
<button onClick={() => setShowDetailsModal(false)} className="p-2 hover:bg-slate-100 rounded-lg">
<X className="w-5 h-5 text-slate-400" />
</button>
</div>
<BikeDetails bike={selectedBike} />
</div>
</div>
)}
</div>
);
}
function BikeForm({ bike, onSave, onCancel }: { bike: Bike | null; onSave: (bike: Bike) => void; onCancel: () => void }) {
const [formData, setFormData] = useState<Bike>(bike || {
id: '',
model: '',
brand: '',
image: '',
plateNumber: '',
status: 'available',
batteryLevel: 100,
location: '',
assignedTo: undefined,
investorId: undefined,
purchaseDate: new Date().toISOString().split('T')[0],
purchasePrice: 0,
totalRides: 0,
totalDistance: 0,
insuranceExpiry: '',
registrationExpiry: '',
});
const handleChange = (field: keyof Bike, value: any) => {
setFormData({ ...formData, [field]: value });
};
return (
<div className="p-5 overflow-y-auto flex-1">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Bike ID *</label>
<input
type="text"
value={formData.id}
onChange={(e) => handleChange('id', e.target.value)}
placeholder="EV001"
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent"
disabled={!!bike}
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Brand *</label>
<input
type="text"
value={formData.brand}
onChange={(e) => handleChange('brand', e.target.value)}
placeholder="Etron"
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Model *</label>
<input
type="text"
value={formData.model}
onChange={(e) => handleChange('model', e.target.value)}
placeholder="ET50"
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Plate Number *</label>
<input
type="text"
value={formData.plateNumber}
onChange={(e) => handleChange('plateNumber', e.target.value)}
placeholder="Dhaka Metro Cha-A-1234"
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Status *</label>
<select
value={formData.status}
onChange={(e) => handleChange('status', e.target.value)}
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="available">Available</option>
<option value="rented">Rented</option>
<option value="maintenance">Maintenance</option>
<option value="retired">Retired</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Battery Level (%)</label>
<input
type="number"
value={formData.batteryLevel}
onChange={(e) => handleChange('batteryLevel', parseInt(e.target.value))}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent"
/>
</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"
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Assigned To</label>
<input
type="text"
value={formData.assignedTo || ''}
onChange={(e) => handleChange('assignedTo', e.target.value)}
placeholder="Biker 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"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Purchase Date</label>
<input
type="date"
value={formData.purchaseDate}
onChange={(e) => handleChange('purchaseDate', e.target.value)}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Purchase Price ()</label>
<input
type="number"
value={formData.purchasePrice}
onChange={(e) => handleChange('purchasePrice', parseInt(e.target.value))}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Insurance Expiry</label>
<input
type="date"
value={formData.insuranceExpiry}
onChange={(e) => handleChange('insuranceExpiry', e.target.value)}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Registration Expiry</label>
<input
type="date"
value={formData.registrationExpiry}
onChange={(e) => handleChange('registrationExpiry', e.target.value)}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent"
/>
</div>
</div>
<div className="flex gap-3 mt-6">
<button onClick={onCancel} className="flex-1 py-2.5 px-4 border border-slate-200 rounded-lg font-semibold text-sm hover:bg-slate-50">
Cancel
</button>
<button
onClick={() => onSave(formData)}
className="flex-1 py-2.5 px-4 bg-accent text-white rounded-lg font-semibold text-sm hover:bg-accent-dark"
>
{bike ? 'Update Bike' : 'Register Bike'}
</button>
</div>
</div>
);
}
function BikeDetails({ bike }: { bike: Bike }) {
return (
<div className="p-5 overflow-y-auto flex-1">
<div className="flex items-center gap-4 mb-6">
<div className="w-16 h-16 rounded-xl bg-blue-50 flex items-center justify-center">
<Bike className="w-8 h-8 text-blue-600" />
</div>
<div>
<h3 className="text-xl font-bold text-slate-800">{bike.model}</h3>
<p className="text-sm text-slate-500">{bike.brand} {bike.id}</p>
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full mt-1 ${statusColors[bike.status]}`}>
{bike.status}
</span>
</div>
</div>
<div className="grid grid-cols-2 gap-4 mb-6">
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500 mb-1">Plate Number</p>
<p className="font-semibold text-slate-700">{bike.plateNumber}</p>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500 mb-1">Battery Level</p>
<p className={`font-semibold ${bike.batteryLevel > 50 ? 'text-green-600' : bike.batteryLevel > 20 ? 'text-amber-600' : 'text-red-600'}`}>{bike.batteryLevel}%</p>
</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>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500 mb-1">Assigned To</p>
<p className="font-semibold text-slate-700">{bike.assignedTo || 'Unassigned'}</p>
</div>
</div>
<div className="border-t border-slate-100 pt-4 mb-4">
<h4 className="font-semibold text-slate-700 mb-3">Performance Metrics</h4>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Total Rides</p>
<p className="text-lg font-bold text-slate-700">{bike.totalRides || 0}</p>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Distance</p>
<p className="text-lg font-bold text-slate-700">{(bike.totalDistance || 0).toLocaleString()} km</p>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Daily Rent</p>
<p className="text-lg font-bold text-green-600">{bike.currentRent || 0}</p>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Purchase Price</p>
<p className="text-lg font-bold text-slate-700">{bike.purchasePrice?.toLocaleString() || 0}</p>
</div>
</div>
</div>
<div className="border-t border-slate-100 pt-4 mb-4">
<h4 className="font-semibold text-slate-700 mb-3">Maintenance Info</h4>
<div className="grid grid-cols-2 gap-3">
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Last Service</p>
<p className="font-medium text-slate-700">{bike.lastService || 'N/A'}</p>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Next Service</p>
<p className="font-medium text-slate-700">{bike.nextService || 'N/A'}</p>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Insurance Expiry</p>
<p className="font-medium text-slate-700">{bike.insuranceExpiry || 'N/A'}</p>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Registration Expiry</p>
<p className="font-medium text-slate-700">{bike.registrationExpiry || 'N/A'}</p>
</div>
</div>
</div>
{bike.notes && (
<div className="border-t border-slate-100 pt-4">
<h4 className="font-semibold text-slate-700 mb-2">Notes</h4>
<p className="text-sm text-slate-600 bg-slate-50 rounded-lg p-3">{bike.notes}</p>
</div>
)}
<button onClick={() => window.location.reload()} className="w-full mt-4 py-2.5 px-4 bg-slate-100 text-slate-600 rounded-lg font-semibold text-sm hover:bg-slate-200">
Close
</button>
</div>
);
}
function FleetMap({ bikes, onSelectBike, selectedBike, large }: { bikes: Bike[]; onSelectBike: (bike: Bike) => void; selectedBike: Bike | null, large?: boolean }) {
const locationCounts: Record<string, { bikes: Bike[]; lat: number; lng: number }> = {};
bikes.forEach(bike => {
const loc = bike.location;
if (!locationCounts[loc]) {
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 },
};
locationCounts[loc] = { bikes: [], lat: locations[loc]?.lat || 23.7936, lng: locations[loc]?.lng || 90.4061 };
}
locationCounts[loc].bikes.push(bike);
});
// Simple coordinate mapper for Dhaka-ish area
const getCoords = (lat: number, lng: number) => {
// Zoom factor based on whether it's the large view
const xMultiplier = large ? 700 : 500;
const yMultiplier = large ? 600 : 400;
const x = (lng - 90.35) * xMultiplier;
const y = (23.88 - lat) * yMultiplier;
return { x, y };
};
return (
<div className={`w-full h-full bg-slate-50 relative ${large ? 'cursor-grab active:cursor-grabbing' : ''}`}>
<div className="absolute inset-0 overflow-hidden">
<svg viewBox={large ? "0 0 100 100" : "0 0 100 100"} className="w-full h-full" preserveAspectRatio="xMidYMid slice">
<defs>
<pattern id="gridLarge" width="10" height="10" patternUnits="userSpaceOnUse">
<path d="M 10 0 L 0 0 0 10" fill="none" stroke="#e2e8f0" strokeWidth="0.2" />
</pattern>
<filter id="shadow">
<feDropShadow dx="0" dy="1" stdDeviation="0.5" floodOpacity="0.2" />
</filter>
</defs>
{/* Subtle map pattern */}
<rect width="1000" height="1000" fill="url(#gridLarge)" />
{/* Simple river-like shapes for "map" look */}
<path d="M-20,40 Q30,45 60,35 T120,45" fill="none" stroke="#e0f2fe" strokeWidth="8" strokeLinecap="round" />
<path d="M40,-20 Q45,30 35,60 T45,120" fill="none" stroke="#f1f5f9" strokeWidth="12" strokeLinecap="round" />
{(Object.entries(locationCounts) as [string, { bikes: Bike[]; lat: number; lng: number }][]).map(([loc, data]) => {
const { x, y } = getCoords(data.lat, data.lng);
const isSelected = data.bikes.some(b => b.id === selectedBike?.id);
return (
<g key={loc} transform={`translate(${x}, ${y})`} onClick={() => onSelectBike(data.bikes[0])} className="cursor-pointer">
{/* Glow for selected/multiple */}
<circle
r={large ? 4 : 3}
className={`${isSelected ? 'fill-accent animate-pulse' : 'fill-slate-300'} opacity-20`}
/>
{/* Marker body */}
<path
d={large ? "M0,0 L-3,-6 A3.5,3.5 0 1,1 3,-6 L0,0 Z" : "M0,0 L-2,-4 A2.5,2.5 0 1,1 2,-4 L0,0 Z"}
className={`${data.bikes[0].status === 'available' ? 'fill-green-500' :
data.bikes[0].status === 'rented' ? 'fill-blue-500' :
data.bikes[0].status === 'maintenance' ? 'fill-amber-500' :
'fill-slate-400'
}`}
filter="url(#shadow)"
/>
{/* Center dot or number */}
<circle
cx="0"
cy={large ? -6 : -4}
r={large ? 1.5 : 1}
fill="white"
/>
{data.bikes.length > 1 && (
<g transform={`translate(${large ? 3 : 2}, ${large ? -8 : -5})`}>
<circle r={large ? 2.5 : 1.8} className="fill-slate-800" />
<text
textAnchor="middle"
dominantBaseline="middle"
className="fill-white text-[2.5px] font-bold pointer-events-none"
>
{data.bikes.length}
</text>
</g>
)}
{large && (
<text
y="4"
textAnchor="middle"
className="fill-slate-400 text-[1.5px] font-medium tracking-tight pointer-events-none"
>
{loc}
</text>
)}
</g>
);
})}
</svg>
</div>
{!large && (
<div className="absolute bottom-2 left-2 flex gap-2">
<div className="flex items-center gap-1 bg-white/90 rounded px-2 py-1 text-[10px] shadow-sm border border-slate-100">
<div className="w-1.5 h-1.5 rounded-full bg-green-500" />
<span className="text-slate-600 font-medium">{bikes.filter(b => b.status === 'available').length}</span>
</div>
<div className="flex items-center gap-1 bg-white/90 rounded px-2 py-1 text-[10px] shadow-sm border border-slate-100">
<div className="w-1.5 h-1.5 rounded-full bg-blue-500" />
<span className="text-slate-600 font-medium">{bikes.filter(b => b.status === 'rented').length}</span>
</div>
</div>
)}
{large && (
<div className="absolute bottom-4 right-4 flex flex-col gap-2">
<button className="w-8 h-8 bg-white rounded-lg shadow-md border border-slate-200 flex items-center justify-center text-slate-600 hover:bg-slate-50">
<Plus className="w-4 h-4" />
</button>
<button className="w-8 h-8 bg-white rounded-lg shadow-md border border-slate-200 flex items-center justify-center text-slate-600 hover:bg-slate-50">
<div className="w-4 h-0.5 bg-slate-400 rounded-full" />
</button>
</div>
)}
</div>
);
}