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

@@ -1,7 +1,15 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'images.unsplash.com',
pathname: '/**',
},
],
},
};
export default nextConfig;

10
package-lock.json generated
View File

@@ -8,6 +8,7 @@
"name": "jaiben-ui",
"version": "0.1.0",
"dependencies": {
"lucide-react": "^1.8.0",
"next": "16.2.4",
"react": "19.2.4",
"react-dom": "19.2.4"
@@ -4858,6 +4859,15 @@
"yallist": "^3.0.2"
}
},
"node_modules/lucide-react": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.8.0.tgz",
"integrity": "sha512-WuvlsjngSk7TnTBJ1hsCy3ql9V9VOdcPkd3PKcSmM34vJD8KG6molxz7m7zbYFgICwsanQWmJ13JlYs4Zp7Arw==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",

View File

@@ -9,6 +9,7 @@
"lint": "eslint"
},
"dependencies": {
"lucide-react": "^1.8.0",
"next": "16.2.4",
"react": "19.2.4",
"react-dom": "19.2.4"

View File

@@ -0,0 +1,505 @@
'use client';
import { useState, use } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import {
User, Phone, Mail, MapPin, Calendar, Heart, Briefcase, Car, Navigation,
FileText, Clock, TrendingUp, CreditCard, Shield, Award, Star, Activity,
Eye, Edit, Trash2, ArrowLeft, PhoneCall, MessageCircle, CheckCircle, XCircle,
AlertTriangle, DollarSign, Wallet, Bike as BikeIcon, Wrench, Ban, MoreHorizontal, Copy,
ExternalLink, Download, Upload, Bell, MessageSquare, Send, RefreshCcw
} from 'lucide-react';
interface DrivingLicense {
number: string;
issueDate: string;
expiryDate: string;
class: string;
status: 'valid' | 'expired' | 'suspended';
}
interface Document {
type: 'nid' | 'passport' | 'driving_license' | 'other';
number: string;
verified: boolean;
}
interface Biker {
id: string;
name: string;
email: string;
phone: string;
alternatePhone?: string;
status: 'active' | 'pending' | 'inactive' | 'blocked';
createdAt: string;
location: string;
address: string;
dateOfBirth: string;
gender: 'male' | 'female' | 'other';
bloodGroup: string;
occupation: string;
emergencyContact: string;
emergencyPhone: string;
emergencyRelation: string;
drivingLicense: DrivingLicense;
documents: Document[];
gpsDeviceId?: string;
gpsPhone?: string;
totalRides: number;
totalDistance: number;
totalRideHours: number;
totalSpent: number;
currentBike?: string;
bikePlate?: string;
depositPaid: number;
walletBalance: number;
rating: number;
totalRatings: number;
responseTime: number;
cancellationRate: number;
kycStatus: 'verified' | 'pending' | 'rejected';
membershipType: 'free' | 'basic' | 'premium' | 'vip';
insuranceStatus: 'active' | 'expired' | 'none';
insuranceExpiry?: string;
notes?: string;
lastRideAt?: string;
firstRideAt?: string;
joinedFrom: string;
}
const mockBikers: Biker[] = [
{
id: 'B001', name: 'Rahim Ahmed', email: 'rahim@email.com', phone: '01712345678', alternatePhone: '01912345678',
status: 'active', createdAt: '2024-01-15', location: 'Gulshan, Dhaka',
address: 'House 12, Road 5, Gulshan 1, Dhaka 1212', dateOfBirth: '1995-03-15', gender: 'male', bloodGroup: 'O+',
occupation: 'Student', emergencyContact: 'Karim Ahmed', emergencyPhone: '01712345679', emergencyRelation: 'Brother',
drivingLicense: { number: 'DL2024001234', issueDate: '2023-01-15', expiryDate: '2033-01-14', class: 'M', status: 'valid' },
documents: [{ type: 'nid', number: '1234567890', verified: true }],
gpsDeviceId: 'GP-001234', gpsPhone: '01712345678',
totalRides: 156, totalDistance: 2340, totalRideHours: 468, totalSpent: 54500,
currentBike: 'Etron ET50', bikePlate: 'Dhaka Metro Cha-1234', depositPaid: 5000, walletBalance: 1250,
rating: 4.8, totalRatings: 156, responseTime: 2.5, cancellationRate: 2.1,
kycStatus: 'verified', membershipType: 'premium', insuranceStatus: 'active', insuranceExpiry: '2025-01-14',
notes: 'Reliable rider, frequently uses premium service', lastRideAt: '2024-03-21', firstRideAt: '2024-01-15', joinedFrom: 'App Store',
},
{
id: 'B002', name: 'Karim Hasan', email: 'karim@email.com', phone: '01712345679',
status: 'active', createdAt: '2024-02-20', location: 'Banani, Dhaka',
address: 'House 5, Road 11, Banani, Dhaka 1213', dateOfBirth: '1990-07-22', gender: 'male', bloodGroup: 'B+',
occupation: 'Business', emergencyContact: 'Rahim Hasan', emergencyPhone: '01712345678', emergencyRelation: 'Friend',
drivingLicense: { number: 'DL2024005678', issueDate: '2023-06-20', expiryDate: '2033-06-19', class: 'M', status: 'valid' },
documents: [{ type: 'nid', number: '1234567891', verified: true }],
gpsDeviceId: 'GP-001235',
totalRides: 89, totalDistance: 1335, totalRideHours: 267, totalSpent: 31200,
currentBike: 'Yadea DT3', bikePlate: 'Dhaka Metro Cha-5678', depositPaid: 5000, walletBalance: 800,
rating: 4.5, totalRatings: 89, responseTime: 3.2, cancellationRate: 4.5,
kycStatus: 'verified', membershipType: 'basic', insuranceStatus: 'active', insuranceExpiry: '2025-02-19', joinedFrom: 'Website',
},
{
id: 'B003', name: 'Jamal Mahmud', email: 'jamal@email.com', phone: '01712345680',
status: 'pending', createdAt: '2024-03-18', location: 'Uttara, Dhaka',
address: 'Sector 10, Uttara, Dhaka 1230', dateOfBirth: '1988-11-05', gender: 'male', bloodGroup: 'A+',
occupation: 'Job Holder', emergencyContact: 'Mahmud Ali', emergencyPhone: '01712345681', emergencyRelation: 'Father',
drivingLicense: { number: '', issueDate: '', expiryDate: '', class: 'M', status: 'valid' },
documents: [{ type: 'nid', number: '1234567892', verified: false }],
totalRides: 0, totalDistance: 0, totalRideHours: 0, totalSpent: 0,
depositPaid: 0, walletBalance: 0, rating: 0, totalRatings: 0, responseTime: 0, cancellationRate: 0,
kycStatus: 'pending', membershipType: 'free', insuranceStatus: 'none', joinedFrom: 'Referral',
},
{
id: 'B004', name: 'Ali Rahman', email: 'ali@email.com', phone: '01712345681',
status: 'active', createdAt: '2023-12-01', location: 'Dhanmondi, Dhaka',
address: 'House 27, Road 8, Dhanmondi, Dhaka 1205', dateOfBirth: '1992-06-10', gender: 'male', bloodGroup: 'AB+',
occupation: 'Engineer', emergencyContact: 'Rahman Ali', emergencyPhone: '01712345682', emergencyRelation: 'Brother',
drivingLicense: { number: 'DL202301234', issueDate: '2023-05-10', expiryDate: '2033-05-09', class: 'M', status: 'valid' },
documents: [{ type: 'nid', number: '1234567893', verified: true }],
gpsDeviceId: 'GP-001236',
totalRides: 234, totalDistance: 3510, totalRideHours: 702, totalSpent: 81900,
currentBike: 'AIMA Lightning', bikePlate: 'Dhaka Metro Cha-9012', depositPaid: 5000, walletBalance: 2100,
rating: 4.9, totalRatings: 234, responseTime: 1.8, cancellationRate: 1.2,
kycStatus: 'verified', membershipType: 'vip', insuranceStatus: 'active', insuranceExpiry: '2024-12-01', joinedFrom: 'Facebook',
},
{
id: 'B005', name: 'Mostafa Kamal', email: 'mostafa@email.com', phone: '01712345682',
status: 'inactive', createdAt: '2023-08-15', location: 'Mirpur, Dhaka',
address: 'Mirpur 1, Dhaka 1216', dateOfBirth: '1997-02-28', gender: 'male', bloodGroup: 'O-',
occupation: 'Teacher', emergencyContact: 'Kamal Mostafa', emergencyPhone: '01712345683', emergencyRelation: 'Father',
drivingLicense: { number: 'DL2022009876', issueDate: '2022-08-15', expiryDate: '2024-08-14', class: 'M', status: 'expired' },
documents: [{ type: 'nid', number: '1234567894', verified: true }],
totalRides: 45, totalDistance: 675, totalRideHours: 135, totalSpent: 15750,
depositPaid: 5000, walletBalance: 0, rating: 3.8, totalRatings: 45, responseTime: 4.5, cancellationRate: 8.9,
kycStatus: 'verified', membershipType: 'free', insuranceStatus: 'expired', insuranceExpiry: '2024-01-14', joinedFrom: 'App Store',
},
];
const statusColors: Record<string, string> = {
active: 'bg-green-100 text-green-700',
pending: 'bg-amber-100 text-amber-700',
inactive: 'bg-slate-100 text-slate-500',
blocked: 'bg-red-100 text-red-700',
};
const kycColors: Record<string, string> = {
verified: 'bg-green-100 text-green-700',
pending: 'bg-amber-100 text-amber-700',
rejected: 'bg-red-100 text-red-700',
};
const membershipColors: Record<string, string> = {
free: 'bg-slate-100 text-slate-600',
basic: 'bg-blue-100 text-blue-700',
premium: 'bg-purple-100 text-purple-700',
vip: 'bg-amber-100 text-amber-700',
};
interface PageProps {
params: Promise<{ id: string }>;
}
export default function BikerDetailPage({ params }: PageProps) {
const resolvedParams = use(params);
const router = useRouter();
const [activeTab, setActiveTab] = useState('personal');
const biker = mockBikers.find(b => b.id === resolvedParams.id) || mockBikers[0];
const tabs = [
{ id: 'personal', label: 'Personal', icon: User },
{ id: 'license', label: 'License & GPS', icon: Car },
{ id: 'documents', label: 'Documents', icon: FileText },
{ id: 'reviews', label: 'Reviews', icon: MessageCircle },
{ id: 'stats', label: 'Statistics', icon: TrendingUp },
{ id: 'account', label: 'Account', icon: CreditCard },
{ id: 'activity', label: 'Activity', icon: Activity },
];
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/bikers" 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">Biker Details</h1>
<p className="text-sm text-slate-500">View and manage biker profile</p>
</div>
<div className="flex items-center gap-2">
<button className="p-2 border border-slate-200 rounded-lg hover:bg-slate-50">
<Bell className="w-5 h-5 text-slate-600" />
</button>
<button className="p-2 border border-slate-200 rounded-lg hover:bg-slate-50">
<MessageSquare className="w-5 h-5 text-slate-600" />
</button>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden mb-4">
<div className="p-4 lg:p-6 flex flex-col lg:flex-row lg:items-center gap-4">
<div className="w-20 h-20 rounded-full bg-blue-100 flex items-center justify-center">
<span className="text-3xl font-bold text-blue-600">{biker.name.charAt(0)}</span>
</div>
<div className="flex-1">
<div className="flex items-center gap-2">
<h2 className="text-xl lg:text-2xl font-bold text-slate-800">{biker.name}</h2>
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${statusColors[biker.status]}`}>
{biker.status === 'active' && <Activity className="w-3 h-3" />}
{biker.status === 'pending' && <Clock className="w-3 h-3" />}
{biker.status === 'blocked' && <Ban className="w-3 h-3" />}
{biker.status}
</span>
</div>
<p className="text-sm text-slate-500">ID: {biker.id} {biker.location}</p>
<div className="flex flex-wrap items-center gap-2 mt-2">
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${kycColors[biker.kycStatus]}`}>
{biker.kycStatus === 'verified' && <CheckCircle className="w-3 h-3" />}
{biker.kycStatus === 'pending' && <Clock className="w-3 h-3" />}
KYC: {biker.kycStatus}
</span>
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${membershipColors[biker.membershipType]}`}>
<Award className="w-3 h-3" /> {biker.membershipType}
</span>
{biker.rating > 0 && (
<span className="inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full bg-amber-100 text-amber-700">
<Star className="w-3 h-3" /> {biker.rating} ({biker.totalRatings})
</span>
)}
</div>
</div>
<div className="flex gap-2">
<button className="flex-1 lg:flex-none px-4 py-2 bg-blue-600 text-white rounded-lg font-semibold text-sm hover:bg-blue-700 flex items-center justify-center gap-2">
<PhoneCall className="w-4 h-4" />
<span className="hidden lg:inline">Call</span>
</button>
<button className="flex-1 lg:flex-none px-4 py-2 bg-green-600 text-white rounded-lg font-semibold text-sm hover:bg-green-700 flex items-center justify-center gap-2">
<MessageCircle className="w-4 h-4" />
<span className="hidden lg:inline">Message</span>
</button>
</div>
</div>
<div className="border-t border-slate-100 overflow-x-auto">
<nav className="flex whitespace-nowrap">
{tabs.map(tab => {
const Icon = tab.icon;
return (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 transition-colors ${activeTab === tab.id
? 'border-accent text-accent'
: 'border-transparent text-slate-500 hover:text-slate-700'
}`}
>
<Icon className="w-4 h-4 inline mr-1" />
{tab.label}
</button>
);
})}
</nav>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden mb-4">
{activeTab === 'personal' && (
<div className="p-4 lg:p-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="space-y-4">
<h3 className="font-semibold text-slate-800 flex items-center gap-2">
<User className="w-5 h-5 text-accent" /> Personal Information
</h3>
<div className="grid grid-cols-2 gap-4">
<InfoCard label="Full Name" value={biker.name} />
<InfoCard label="Email" value={biker.email} />
<InfoCard label="Phone" value={biker.phone} />
<InfoCard label="Alternate Phone" value={biker.alternatePhone || 'Not provided'} />
<InfoCard label="Date of Birth" value={biker.dateOfBirth} />
<InfoCard label="Gender" value={biker.gender} />
<InfoCard label="Blood Group" value={biker.bloodGroup} />
<InfoCard label="Occupation" value={biker.occupation} />
</div>
<div>
<p className="text-xs text-slate-500 mb-1">Address</p>
<p className="text-sm text-slate-700 bg-slate-50 p-3 rounded-lg">{biker.address}</p>
</div>
</div>
<div className="space-y-4">
<h3 className="font-semibold text-slate-800 flex items-center gap-2">
<PhoneCall className="w-5 h-5 text-accent" /> Emergency Contact
</h3>
<div className="bg-slate-50 p-4 rounded-lg space-y-3">
<InfoCard label="Contact Name" value={biker.emergencyContact} />
<InfoCard label="Relation" value={biker.emergencyRelation} />
<InfoCard label="Phone" value={biker.emergencyPhone} />
</div>
</div>
</div>
</div>
)}
{activeTab === 'license' && (
<div className="p-4 lg:p-6 space-y-6">
<div>
<h3 className="font-semibold text-slate-800 mb-3 flex items-center gap-2">
<Car className="w-5 h-5 text-accent" /> Driving License
</h3>
<div className="grid grid-cols-2 lg:grid-cols-3 gap-4">
<InfoCard label="License Number" value={biker.drivingLicense.number || 'Not provided'} />
<InfoCard label="Class" value={biker.drivingLicense.class} />
<InfoCard label="Status" value={biker.drivingLicense.status} highlight={biker.drivingLicense.status === 'valid' ? 'green' : 'red'} />
<InfoCard label="Issue Date" value={biker.drivingLicense.issueDate || 'N/A'} />
<InfoCard label="Expiry Date" value={biker.drivingLicense.expiryDate || 'N/A'} />
</div>
</div>
<div>
<h3 className="font-semibold text-slate-800 mb-3 flex items-center gap-2">
<Navigation className="w-5 h-5 text-accent" /> GPS Tracking Device
</h3>
<div className="grid grid-cols-2 gap-4">
<InfoCard label="Device ID" value={biker.gpsDeviceId || 'Not assigned'} />
<InfoCard label="Linked Phone" value={biker.gpsPhone || 'Not linked'} />
</div>
</div>
</div>
)}
{activeTab === 'documents' && (
<div className="p-4 lg:p-6">
<h3 className="font-semibold text-slate-800 mb-3 flex items-center gap-2">
<FileText className="w-5 h-5 text-accent" /> Uploaded Documents
</h3>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{biker.documents.map((doc, idx) => (
<div key={idx} className="p-4 border border-slate-200 rounded-lg">
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-slate-700 uppercase">{doc.type}</p>
<p className="text-sm text-slate-500">Number: {doc.number || 'N/A'}</p>
</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" /> : <Clock className="w-3 h-3" />}
{doc.verified ? 'Verified' : 'Pending'}
</span>
</div>
</div>
))}
</div>
</div>
)}
{activeTab === 'reviews' && (
<div className="p-4 lg:p-6">
<h3 className="font-semibold text-slate-800 mb-3 flex items-center gap-2">
<Star className="w-5 h-5 text-amber-500" /> Biker Reviews
</h3>
<div className="space-y-3">
{[
{ rider: 'Tashrif', rating: 5, comment: 'Great service! Very polite and quick.', date: '2024-03-20', rideId: 'R001' },
{ rider: 'Mahir', rating: 4, comment: 'Good ride, arrived on time.', date: '2024-03-19', rideId: 'R002' },
{ rider: 'Raisa', rating: 5, comment: 'Excellent experience. Would recommend!', date: '2024-03-18', rideId: 'R003' },
{ rider: 'Anika', rating: 3, comment: 'Ride was okay but a bit slow.', date: '2024-03-17', rideId: 'R004' },
{ rider: 'Ovi', rating: 5, comment: 'Best biker ever! Helped with luggage.', date: '2024-03-16', rideId: 'R005' },
].map((review, idx) => (
<div key={idx} className="p-4 border border-slate-200 rounded-lg">
<div className="flex items-start justify-between mb-2">
<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">{review.rider.charAt(0)}</span>
</div>
<div>
<p className="text-sm font-medium text-slate-700">{review.rider}</p>
<p className="text-xs text-slate-400">Ride: {review.rideId}</p>
</div>
</div>
<span className="flex items-center gap-1 text-amber-500 font-bold text-sm">
{Array.from({ length: review.rating }).map((_, i) => (
<Star key={i} className="w-4 h-4 fill-current" />
))}
</span>
</div>
<p className="text-sm text-slate-600 mb-2">{review.comment}</p>
<p className="text-xs text-slate-400">{review.date}</p>
</div>
))}
</div>
</div>
)}
{activeTab === 'stats' && (
<div className="p-4 lg:p-6">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<StatCard label="Total Rides" value={biker.totalRides.toString()} icon={BikeIcon} color="text-blue-600" />
<StatCard label="Distance" value={`${biker.totalDistance.toLocaleString()} km`} icon={Navigation} color="text-green-600" />
<StatCard label="Ride Hours" value={biker.totalRideHours.toString()} icon={Clock} color="text-purple-600" />
<StatCard label="Rating" value={`${biker.rating}`} icon={Star} color="text-amber-600" />
</div>
<div className="grid grid-cols-2 lg:grid-cols-3 gap-4">
<InfoCard label="Avg Response Time" value={`${biker.responseTime} min`} />
<InfoCard label="Cancellation Rate" value={`${biker.cancellationRate}%`} highlight={biker.cancellationRate > 5 ? 'red' : ''} />
<InfoCard label="Current Bike" value={biker.currentBike || 'None'} />
<InfoCard label="Plate Number" value={biker.bikePlate || 'N/A'} />
</div>
</div>
)}
{activeTab === 'account' && (
<div className="p-4 lg:p-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="space-y-4">
<h3 className="font-semibold text-slate-800 flex items-center gap-2">
<CreditCard className="w-5 h-5 text-accent" /> Financial
</h3>
<div className="grid grid-cols-2 gap-4">
<InfoCard label="Total Spent" value={`${biker.totalSpent.toLocaleString()}`} highlight="green" />
<InfoCard label="Wallet Balance" value={`${biker.walletBalance}`} highlight="blue" />
<InfoCard label="Deposit Paid" value={`${biker.depositPaid}`} />
<InfoCard label="Membership" value={biker.membershipType} highlight={biker.membershipType === 'vip' ? 'amber' : ''} />
</div>
</div>
<div className="space-y-4">
<h3 className="font-semibold text-slate-800 flex items-center gap-2">
<Shield className="w-5 h-5 text-accent" /> Verification & Insurance
</h3>
<div className="grid grid-cols-2 gap-4">
<InfoCard label="KYC Status" value={biker.kycStatus} highlight={biker.kycStatus === 'verified' ? 'green' : 'amber'} />
<InfoCard label="Insurance" value={biker.insuranceStatus} highlight={biker.insuranceStatus === 'active' ? 'green' : biker.insuranceStatus === 'expired' ? 'red' : ''} />
<InfoCard label="Insurance Expiry" value={biker.insuranceExpiry || 'N/A'} />
<InfoCard label="Joined From" value={biker.joinedFrom} />
</div>
</div>
</div>
</div>
)}
{activeTab === 'activity' && (
<div className="p-4 lg:p-6">
<h3 className="font-semibold text-slate-800 mb-3 flex items-center gap-2">
<Activity className="w-5 h-5 text-accent" /> Activity Log
</h3>
<div className="space-y-3">
{[
{ action: 'Profile Updated', date: '2024-03-21', icon: Edit },
{ action: 'Ride Completed', date: '2024-03-21', icon: BikeIcon },
{ action: 'Wallet Top Up - ৳2000', date: '2024-03-20', icon: Wallet },
{ action: 'KYC Verified', date: '2024-01-15', icon: CheckCircle },
].map((log, idx) => {
const Icon = log.icon;
return (
<div key={idx} className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg">
<Icon className="w-5 h-5 text-slate-400" />
<div className="flex-1">
<p className="text-sm font-medium text-slate-700">{log.action}</p>
<p className="text-xs text-slate-400">{log.date}</p>
</div>
</div>
);
})}
</div>
</div>
)}
</div>
{/* <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 Biker</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 InfoCard({ label, value, highlight }: { label: string; value: string; highlight?: string }) {
return (
<div>
<p className="text-xs text-slate-500 mb-1">{label}</p>
<p className={`text-sm font-medium ${highlight === 'green' ? 'text-green-600' :
highlight === 'red' ? 'text-red-600' :
highlight === 'blue' ? 'text-blue-600' :
highlight === 'amber' ? 'text-amber-600' :
'text-slate-700'
}`}>{value}</p>
</div>
);
}
function StatCard({ label, value, icon: Icon, color }: { label: string; value: string; icon: any; color: string }) {
return (
<div className="p-4 bg-slate-50 rounded-lg">
<Icon className={`w-5 h-5 ${color} mb-2`} />
<p className="text-lg font-bold text-slate-800">{value}</p>
<p className="text-xs text-slate-500">{label}</p>
</div>
);
}

File diff suppressed because it is too large Load Diff

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

View File

@@ -0,0 +1,108 @@
import { MapPin, Plus, Search, MoreVertical, Eye, Edit, Trash2 } from 'lucide-react';
const geofences = [
{ id: 'gf1', name: 'Gulshan Zone A', type: 'Restricted', bikes: 12, status: 'active' },
{ id: 'gf2', name: 'Banani Area', type: 'Allowed', bikes: 8, status: 'active' },
{ id: 'gf3', name: 'Uttara Sector', type: 'Allowed', bikes: 15, status: 'active' },
{ id: 'gf4', name: 'Dhanmondi Zone', type: 'Slow Zone', bikes: 6, status: 'active' },
];
export default function GeofencePage() {
return (
<div className="p-4 lg:p-6">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6">
<div>
<h1 className="text-2xl lg:text-3xl font-extrabold text-slate-800">Geofences</h1>
<p className="text-sm text-slate-500 mt-1">Manage geo-fenced areas</p>
</div>
<button className="py-2.5 px-4 bg-accent text-white rounded-lg font-semibold text-sm hover:bg-accent-dark transition-colors flex items-center gap-2">
<Plus className="w-4 h-4" /> Add New Zone
</button>
</div>
<div className="grid grid-cols-3 gap-4 mb-6">
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<p className="text-2xl font-extrabold text-slate-800">{geofences.length}</p>
<p className="text-sm text-slate-500">Total Zones</p>
</div>
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<p className="text-2xl font-extrabold text-slate-800">41</p>
<p className="text-sm text-slate-500">Bikes in Zones</p>
</div>
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<p className="text-2xl font-extrabold text-slate-800">4</p>
<p className="text-sm text-slate-500">Active Zones</p>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-100">
<div className="p-4 border-b border-slate-100">
<div className="relative max-w-md">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<input
type="text"
placeholder="Search geofences..."
className="w-full pl-10 pr-4 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
/>
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-50">
<tr>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Zone Name</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Type</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Bikes</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Status</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{geofences.map(zone => (
<tr key={zone.id} className="hover:bg-slate-50 transition-colors">
<td className="px-4 py-3">
<div className="flex items-center gap-3">
<MapPin className="w-5 h-5 text-accent" />
<span className="text-sm font-medium text-slate-700">{zone.name}</span>
</div>
</td>
<td className="px-4 py-3">
<span className={`text-xs font-medium px-2.5 py-1 rounded-full ${
zone.type === 'Restricted' ? 'bg-red-100 text-red-700' :
zone.type === 'Slow Zone' ? 'bg-amber-100 text-amber-700' :
'bg-green-100 text-green-700'
}`}>
{zone.type}
</span>
</td>
<td className="px-4 py-3">
<span className="text-sm text-slate-600">{zone.bikes} bikes</span>
</td>
<td className="px-4 py-3">
<span className="text-xs font-medium px-2.5 py-1 rounded-full bg-green-100 text-green-700">
{zone.status}
</span>
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-1">
<button className="p-2 hover:bg-slate-100 rounded-lg">
<Eye className="w-4 h-4 text-slate-400" />
</button>
<button className="p-2 hover:bg-slate-100 rounded-lg">
<Edit className="w-4 h-4 text-slate-400" />
</button>
<button className="p-2 hover:bg-red-50 rounded-lg">
<Trash2 className="w-4 h-4 text-red-400" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,669 @@
'use client';
import { useState } from 'react';
import Link from 'next/link';
import { useParams } from 'next/navigation';
import { investors as initialInvestors, bikes as initialBikes, transactions as initialTransactions } from '@/data/mockData';
import type { Investor } from '@/data/mockData';
import {
ArrowLeft, Wallet, TrendingUp, Banknote, Calendar, Phone, Mail, MapPin, Edit, Trash2, Plus, X, Bike,
User, FileText, CreditCard, DollarSign, Clock, ChevronDown, ExternalLink, Download, Upload,
AlertTriangle, Shield, Star, CheckCircle, XCircle, Search, Filter
} from 'lucide-react';
const statusColors: Record<string, string> = {
active: 'bg-green-100 text-green-700',
pending: 'bg-amber-100 text-amber-700',
inactive: 'bg-slate-100 text-slate-500',
suspended: 'bg-red-100 text-red-700',
};
const planColors: Record<string, string> = {
silver: 'bg-slate-200 text-slate-700',
gold: 'bg-yellow-100 text-yellow-700',
platinum: 'bg-purple-100 text-purple-700',
diamond: 'bg-blue-100 text-blue-700',
};
const kycColors: Record<string, string> = {
verified: 'bg-green-100 text-green-700',
pending: 'bg-amber-100 text-amber-700',
rejected: 'bg-red-100 text-red-700',
not_submitted: 'bg-slate-100 text-slate-500',
};
const bikeStatusColors: Record<string, string> = {
available: 'bg-blue-100 text-blue-700',
rented: 'bg-green-100 text-green-700',
maintenance: 'bg-amber-100 text-amber-700',
retired: 'bg-slate-100 text-slate-500',
};
export default function InvestorDetailPage() {
const params = useParams();
const investorId = params.id as string;
const [investors] = useState<Investor[]>(initialInvestors);
const investor = investors.find(i => i.id === investorId);
const assignedBikes = initialBikes.filter(b => b.investorId === investorId);
const investorTransactions = initialTransactions.filter(t => t.userId === investor?.userId);
const [activeTab, setActiveTab] = useState('overview');
const [showEditModal, setShowEditModal] = useState(false);
const [showAssignBikeModal, setShowAssignBikeModal] = useState(false);
const [selectedBikeId, setSelectedBikeId] = useState('');
if (!investor) {
return (
<div className="p-4 lg:p-6">
<div className="text-center py-12">
<h2 className="text-xl font-bold text-slate-800">Investor Not Found</h2>
<p className="text-slate-500 mt-2">The investor you're looking for doesn't exist.</p>
<Link href="/admin/investors" className="mt-4 inline-flex items-center gap-2 text-investor hover:underline">
<ArrowLeft className="w-4 h-4" /> Back to Investors
</Link>
</div>
</div>
);
}
const availableBikesForAssignment = initialBikes.filter(b => !b.investorId && b.status === 'available');
const handleAssignBike = () => {
alert('Bike assignment functionality - would update bike investorId here');
setShowAssignBikeModal(false);
setSelectedBikeId('');
};
return (
<div className="p-4 lg:p-6">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-4">
<Link href="/admin/investors" className="p-2 hover:bg-slate-100 rounded-lg">
<ArrowLeft className="w-5 h-5 text-slate-600" />
</Link>
<div>
<h1 className="text-2xl lg:text-3xl font-extrabold text-slate-800">{investor.name}</h1>
<p className="text-sm text-slate-500">{investor.id} {investor.email}</p>
</div>
</div>
<div className="flex items-center gap-2">
<button onClick={() => setShowEditModal(true)} className="py-2.5 px-4 border border-slate-200 text-slate-600 rounded-lg font-semibold text-sm hover:bg-slate-50 flex items-center gap-2">
<Edit className="w-4 h-4" /> Edit
</button>
<button className="py-2.5 px-4 bg-red-500 text-white rounded-lg font-semibold text-sm hover:bg-red-600 flex items-center gap-2">
<Trash2 className="w-4 h-4" /> Delete
</button>
</div>
</div>
<div className="flex flex-wrap gap-2 mb-6">
<span className={`inline-flex items-center gap-1 text-sm font-medium px-3 py-1.5 rounded-full ${statusColors[investor.status]}`}>
{investor.status}
</span>
{investor.investments && investor.investments.length > 0 && (
<span className={`inline-flex items-center gap-1 text-sm font-medium px-3 py-1.5 rounded-full ${planColors[investor.investments[0].planType]}`}>
{investor.investments.length} Investment{investor.investments.length > 1 ? 's' : ''}
</span>
)}
<span className={`inline-flex items-center gap-1 text-sm font-medium px-3 py-1.5 rounded-full ${kycColors[investor.kycStatus]}`}>
<Shield className="w-4 h-4" /> KYC {investor.kycStatus}
</span>
<span className={`inline-flex items-center gap-1 text-sm font-medium px-3 py-1.5 rounded-full ${investor.riskLevel === 'low' ? 'bg-green-100 text-green-700' : investor.riskLevel === 'medium' ? 'bg-amber-100 text-amber-700' : 'bg-red-100 text-red-700'}`}>
Risk: {investor.riskLevel}
</span>
</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-purple-50 flex items-center justify-center">
<Wallet className="w-6 h-6 text-purple-600" />
</div>
<div>
<p className="text-2xl font-extrabold text-slate-800">{investor.totalInvested.toLocaleString()}</p>
<p className="text-sm text-slate-500">Total Invested</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-green-50 flex items-center justify-center">
<TrendingUp className="w-6 h-6 text-green-600" />
</div>
<div>
<p className="text-2xl font-extrabold text-slate-800">{investor.totalEarnings.toLocaleString()}</p>
<p className="text-sm text-slate-500">Total Earnings</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">
<Bike className="w-6 h-6 text-blue-600" />
</div>
<div>
<p className="text-2xl font-extrabold text-slate-800">{investor.activeBikes}</p>
<p className="text-sm text-slate-500">Active Bikes</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">
<Calendar className="w-6 h-6 text-amber-600" />
</div>
<div>
<p className="text-2xl font-extrabold text-slate-800">{investor.roi}%</p>
<p className="text-sm text-slate-500">ROI</p>
</div>
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-100 mb-6">
<div className="border-b border-slate-100 flex overflow-x-auto">
<button
onClick={() => setActiveTab('overview')}
className={`px-4 py-3 text-sm font-medium whitespace-nowrap ${activeTab === 'overview' ? 'border-b-2 border-investor text-investor' : 'text-slate-500'}`}
>
<User className="w-4 h-4 inline mr-1" /> Overview
</button>
<button
onClick={() => setActiveTab('investments')}
className={`px-4 py-3 text-sm font-medium whitespace-nowrap ${activeTab === 'investments' ? 'border-b-2 border-investor text-investor' : 'text-slate-500'}`}
>
<TrendingUp className="w-4 h-4 inline mr-1" /> Investments ({investor.investments?.length || 0})
</button>
<button
onClick={() => setActiveTab('bikes')}
className={`px-4 py-3 text-sm font-medium whitespace-nowrap ${activeTab === 'bikes' ? 'border-b-2 border-investor text-investor' : 'text-slate-500'}`}
>
<Bike className="w-4 h-4 inline mr-1" /> Bikes ({assignedBikes.length})
</button>
<button
onClick={() => setActiveTab('financial')}
className={`px-4 py-3 text-sm font-medium whitespace-nowrap ${activeTab === 'financial' ? 'border-b-2 border-investor text-investor' : 'text-slate-500'}`}
>
<Banknote className="w-4 h-4 inline mr-1" /> Financial
</button>
<button
onClick={() => setActiveTab('transactions')}
className={`px-4 py-3 text-sm font-medium whitespace-nowrap ${activeTab === 'transactions' ? 'border-b-2 border-investor text-investor' : 'text-slate-500'}`}
>
<DollarSign className="w-4 h-4 inline mr-1" /> Transactions
</button>
<button
onClick={() => setActiveTab('documents')}
className={`px-4 py-3 text-sm font-medium whitespace-nowrap ${activeTab === 'documents' ? 'border-b-2 border-investor text-investor' : 'text-slate-500'}`}
>
<FileText className="w-4 h-4 inline mr-1" /> Documents
</button>
</div>
<div className="p-5">
{activeTab === 'overview' && (
<div className="space-y-6">
<div className="grid lg:grid-cols-2 gap-6">
<div>
<h3 className="font-semibold text-slate-800 mb-4">Personal Information</h3>
<div className="space-y-3">
<div className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg">
<User className="w-5 h-5 text-slate-400" />
<div>
<p className="text-xs text-slate-500">Full Name</p>
<p className="font-medium text-slate-700">{investor.name}</p>
</div>
</div>
<div className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg">
<Phone className="w-5 h-5 text-slate-400" />
<div>
<p className="text-xs text-slate-500">Phone</p>
<p className="font-medium text-slate-700">{investor.phone}</p>
</div>
</div>
<div className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg">
<Mail className="w-5 h-5 text-slate-400" />
<div>
<p className="text-xs text-slate-500">Email</p>
<p className="font-medium text-slate-700">{investor.email}</p>
</div>
</div>
<div className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg">
<MapPin className="w-5 h-5 text-slate-400" />
<div>
<p className="text-xs text-slate-500">Address</p>
<p className="font-medium text-slate-700">{investor.address}</p>
</div>
</div>
{investor.dateOfBirth && (
<div className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg">
<Calendar className="w-5 h-5 text-slate-400" />
<div>
<p className="text-xs text-slate-500">Date of Birth</p>
<p className="font-medium text-slate-700">{investor.dateOfBirth}</p>
</div>
</div>
)}
{investor.nidNumber && (
<div className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg">
<Shield className="w-5 h-5 text-slate-400" />
<div>
<p className="text-xs text-slate-500">NID Number</p>
<p className="font-medium text-slate-700">{investor.nidNumber}</p>
</div>
</div>
)}
</div>
</div>
<div>
<h3 className="font-semibold text-slate-800 mb-4">Emergency Contact</h3>
<div className="space-y-3">
{investor.emergencyContactName && (
<div className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg">
<Phone className="w-5 h-5 text-slate-400" />
<div>
<p className="text-xs text-slate-500">Contact</p>
<p className="font-medium text-slate-700">{investor.emergencyContactName}</p>
<p className="text-xs text-slate-400">{investor.emergencyContactRelation} {investor.emergencyContactPhone}</p>
</div>
</div>
)}
</div>
<h3 className="font-semibold text-slate-800 mb-4 mt-6">Investment Details</h3>
<div className="space-y-3">
<div className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg">
<Calendar className="w-5 h-5 text-slate-400" />
<div>
<p className="text-xs text-slate-500">Total Investments</p>
<p className="font-medium text-slate-700">
{investor.investments?.length || 0} active investments
</p>
</div>
</div>
<div className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg">
<FileText className="w-5 h-5 text-slate-400" />
<div>
<p className="text-xs text-slate-500">Overall Status</p>
<p className="font-medium text-slate-700 capitalize">{investor.status}</p>
</div>
</div>
{investor.referralCode && (
<div className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg">
<Star className="w-5 h-5 text-slate-400" />
<div>
<p className="text-xs text-slate-500">Referral Code</p>
<p className="font-medium text-slate-700">{investor.referralCode}</p>
<p className="text-xs text-slate-400">Referrals: {investor.totalReferrals} Earnings: {investor.referralEarnings}</p>
</div>
</div>
)}
</div>
</div>
</div>
{investor.notes && (
<div className="mt-6">
<h3 className="font-semibold text-slate-800 mb-2">Notes</h3>
<p className="text-sm text-slate-600 bg-slate-50 p-3 rounded-lg">{investor.notes}</p>
</div>
)}
</div>
)}
{activeTab === 'bikes' && (
<div>
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-slate-800">Assigned Bikes</h3>
<button
onClick={() => setShowAssignBikeModal(true)}
className="py-2 px-3 bg-investor text-white rounded-lg text-sm font-medium hover:bg-investor-dark flex items-center gap-1"
>
<Plus className="w-4 h-4" /> Assign Bike
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{assignedBikes.map(bike => (
<div key={bike.id} className="bg-slate-50 rounded-xl p-4">
<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}</p>
</div>
</div>
<span className={`text-xs font-medium px-2.5 py-1 rounded-full ${bikeStatusColors[bike.status]}`}>
{bike.status}
</span>
</div>
<div className="space-y-2 text-xs">
<div className="flex justify-between">
<span className="text-slate-500">Plate</span>
<span className="font-medium">{bike.plateNumber}</span>
</div>
<div className="flex justify-between">
<span className="text-slate-500">Location</span>
<span className="font-medium">{bike.location}</span>
</div>
<div className="flex justify-between">
<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 justify-between">
<span className="text-slate-500">Purchase Price</span>
<span className="font-medium text-purple-600">{bike.purchasePrice?.toLocaleString() || 0}</span>
</div>
<div className="flex justify-between">
<span className="text-slate-500">Total Earnings</span>
<span className="font-medium text-green-600">{bike.totalEarnings?.toLocaleString() || 0}</span>
</div>
</div>
</div>
))}
{assignedBikes.length === 0 && (
<div className="col-span-full text-center py-8 text-slate-400">
<Bike className="w-12 h-12 mx-auto mb-2 opacity-50" />
<p>No bikes assigned yet</p>
</div>
)}
</div>
</div>
)}
{activeTab === 'investments' && (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h3 className="font-semibold text-slate-800">Investment Plans</h3>
<button className="py-2 px-3 bg-investor text-white rounded-lg text-sm font-medium hover:bg-investor-dark flex items-center gap-1">
<Plus className="w-4 h-4" /> Add Investment
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{investor.investments?.map((inv) => (
<div key={inv.id} className="bg-slate-50 rounded-xl p-4">
<div className="flex items-start justify-between mb-3">
<div>
<p className="font-semibold text-slate-700">{inv.planName}</p>
<p className="text-xs text-slate-400">{inv.planType} Plan</p>
</div>
<span className={`text-xs font-medium px-2.5 py-1 rounded-full ${planColors[inv.planType]}`}>
{inv.status}
</span>
</div>
<div className="grid grid-cols-2 gap-2 text-xs mb-3">
<div className="bg-white rounded-lg p-2">
<p className="text-slate-400">Investment</p>
<p className="font-medium text-purple-600">{inv.totalInvestment.toLocaleString()}</p>
</div>
<div className="bg-white rounded-lg p-2">
<p className="text-slate-400">Monthly Return</p>
<p className="font-medium text-green-600">{inv.monthlyReturn.toLocaleString()}</p>
</div>
<div className="bg-white rounded-lg p-2">
<p className="text-slate-400">Expected ROI</p>
<p className="font-medium text-slate-700">{inv.expectedRoi}%</p>
</div>
<div className="bg-white rounded-lg p-2">
<p className="text-slate-400">Actual Earned</p>
<p className="font-medium text-green-600">{inv.actualEarnings.toLocaleString()}</p>
</div>
</div>
<div className="flex items-center justify-between text-xs">
<span className="text-slate-400">{inv.startDate} to {inv.endDate || 'Ongoing'}</span>
<span className="capitalize">{inv.paymentMethod}</span>
</div>
</div>
))}
{(!investor.investments || investor.investments.length === 0) && (
<div className="col-span-full text-center py-8 text-slate-400">
<TrendingUp className="w-12 h-12 mx-auto mb-2 opacity-50" />
<p>No investments yet</p>
<button className="mt-2 px-4 py-2 bg-investor text-white rounded-lg text-sm hover:bg-investor-dark">
Create First Investment
</button>
</div>
)}
</div>
</div>
)}
{activeTab === 'financial' && (
<div className="space-y-6">
<div className="grid lg:grid-cols-2 gap-6">
<div>
<h3 className="font-semibold text-slate-800 mb-4">Bank Details</h3>
<div className="space-y-3">
{investor.bankName && (
<div className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg">
<Banknote className="w-5 h-5 text-slate-400" />
<div>
<p className="text-xs text-slate-500">Bank</p>
<p className="font-medium text-slate-700">{investor.bankName}</p>
<p className="text-xs text-slate-400">{investor.bankBranch}</p>
</div>
</div>
)}
{investor.bankAccountNumber && (
<div className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg">
<CreditCard className="w-5 h-5 text-slate-400" />
<div>
<p className="text-xs text-slate-500">Account</p>
<p className="font-medium text-slate-700">{investor.bankAccountName}</p>
<p className="text-xs text-slate-400">{investor.bankAccountNumber}</p>
</div>
</div>
)}
</div>
</div>
<div>
<h3 className="font-semibold text-slate-800 mb-4">Mobile Banking</h3>
<div className="space-y-3">
{investor.mobileBanking && (
<div className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg">
<Phone className="w-5 h-5 text-slate-400" />
<div>
<p className="text-xs text-slate-500">{investor.mobileBanking} (Primary)</p>
<p className="font-medium text-slate-700">{investor.mobileBankingNumber}</p>
</div>
</div>
)}
{investor.additionalMobileBanking?.map((mb, idx) => (
<div key={idx} className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg">
<Phone className="w-5 h-5 text-slate-400" />
<div className="flex-1">
<p className="text-xs text-slate-500">{mb.provider}</p>
<p className="font-medium text-slate-700">{mb.number}</p>
</div>
{mb.verified && (
<span className="text-xs bg-green-100 text-green-700 px-2 py-1 rounded-full">Verified</span>
)}
</div>
))}
</div>
<h3 className="font-semibold text-slate-800 mb-4 mt-6">Tax Information</h3>
<div className="space-y-3">
{investor.tinNumber && (
<div className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg">
<FileText className="w-5 h-5 text-slate-400" />
<div>
<p className="text-xs text-slate-500">TIN</p>
<p className="font-medium text-slate-700">{investor.tinNumber}</p>
</div>
</div>
)}
{investor.passportNumber && (
<div className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg">
<FileText className="w-5 h-5 text-slate-400" />
<div>
<p className="text-xs text-slate-500">Passport</p>
<p className="font-medium text-slate-700">{investor.passportNumber}</p>
</div>
</div>
)}
</div>
</div>
</div>
<div>
<h3 className="font-semibold text-slate-800 mb-4">Investment Summary</h3>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<div className="bg-purple-50 rounded-lg p-4">
<p className="text-sm text-purple-600">Total Invested</p>
<p className="text-xl font-bold text-purple-700">{investor.totalInvested.toLocaleString()}</p>
</div>
<div className="bg-green-50 rounded-lg p-4">
<p className="text-sm text-green-600">Total Earnings</p>
<p className="text-xl font-bold text-green-700">{investor.totalEarnings.toLocaleString()}</p>
</div>
<div className="bg-amber-50 rounded-lg p-4">
<p className="text-sm text-amber-600">Pending Earnings</p>
<p className="text-xl font-bold text-amber-700">{investor.pendingEarnings.toLocaleString()}</p>
</div>
<div className="bg-slate-50 rounded-lg p-4">
<p className="text-sm text-slate-600">Total Withdrawn</p>
<p className="text-xl font-bold text-slate-700">{investor.totalWithdrawn.toLocaleString()}</p>
</div>
</div>
</div>
</div>
)}
{activeTab === 'transactions' && (
<div>
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-slate-800">Transactions</h3>
<button className="py-2 px-3 border border-slate-200 text-slate-600 rounded-lg text-sm font-medium hover:bg-slate-50 flex items-center gap-1">
<Download className="w-4 h-4" /> Export
</button>
</div>
<div className="space-y-2">
{investorTransactions.map(tx => (
<div key={tx.id} className="flex items-center justify-between p-3 bg-slate-50 rounded-lg">
<div className="flex items-center gap-3">
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${
tx.type === 'earning' ? 'bg-green-100' :
tx.type === 'withdrawal' ? 'bg-red-100' : 'bg-blue-100'
}`}>
<DollarSign className={`w-5 h-5 ${
tx.type === 'earning' ? 'text-green-600' :
tx.type === 'withdrawal' ? 'text-red-600' : 'text-blue-600'
}`} />
</div>
<div>
<p className="text-sm font-medium text-slate-700">{tx.description}</p>
<p className="text-xs text-slate-400">{tx.createdAt}</p>
</div>
</div>
<div className="text-right">
<p className={`text-sm font-medium ${
tx.type === 'earning' ? 'text-green-600' :
tx.type === 'withdrawal' ? 'text-red-600' : 'text-blue-600'
}`}>
{tx.type === 'earning' ? '+' : tx.type === 'withdrawal' ? '-' : '+'}{tx.amount.toLocaleString()}
</p>
<span className={`text-xs font-medium px-2 py-0.5 rounded-full ${
tx.status === 'completed' ? 'bg-green-100 text-green-700' :
tx.status === 'pending' ? 'bg-amber-100 text-amber-700' : 'bg-red-100 text-red-700'
}`}>
{tx.status}
</span>
</div>
</div>
))}
{investorTransactions.length === 0 && (
<div className="text-center py-8 text-slate-400">
<DollarSign className="w-12 h-12 mx-auto mb-2 opacity-50" />
<p>No transactions yet</p>
</div>
)}
</div>
</div>
)}
{activeTab === 'documents' && (
<div>
<h3 className="font-semibold text-slate-800 mb-4">Uploaded Documents</h3>
<div className="space-y-2">
{investor.kycDocuments?.map((doc, idx) => (
<div key={idx} className="flex items-center justify-between p-3 bg-slate-50 rounded-lg">
<div className="flex items-center gap-3">
<FileText className="w-5 h-5 text-slate-400" />
<div>
<p className="text-sm font-medium text-slate-700 capitalize">{doc.type.replace('_', ' ')}</p>
<p className="text-xs text-slate-400">{doc.number || 'No number'}</p>
</div>
</div>
<div className="flex items-center gap-2">
{doc.verified ? (
<span className="flex items-center gap-1 text-xs font-medium px-2 py-1 rounded-full bg-green-100 text-green-700">
<CheckCircle className="w-3 h-3" /> Verified
</span>
) : (
<span className="flex items-center gap-1 text-xs font-medium px-2 py-1 rounded-full bg-amber-100 text-amber-700">
<Clock className="w-3 h-3" /> Pending
</span>
)}
</div>
</div>
))}
{(!investor.kycDocuments || investor.kycDocuments.length === 0) && (
<div className="text-center py-8 text-slate-400">
<FileText className="w-12 h-12 mx-auto mb-2 opacity-50" />
<p>No documents uploaded</p>
</div>
)}
</div>
<div className="mt-4 border-2 border-dashed border-slate-200 rounded-lg p-8 text-center">
<FileText className="w-12 h-12 text-slate-300 mx-auto mb-2" />
<p className="text-sm text-slate-500">Drag and drop files here, or click to browse</p>
<button className="mt-2 px-4 py-2 bg-slate-100 text-slate-600 rounded-lg text-sm hover:bg-slate-200">Browse Files</button>
</div>
</div>
)}
</div>
</div>
{showAssignBikeModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
<div className="p-5 border-b border-slate-100 flex items-center justify-between">
<h2 className="text-lg font-bold text-slate-800">Assign Bike to Investor</h2>
<button onClick={() => setShowAssignBikeModal(false)} className="p-2 hover:bg-slate-100 rounded-lg">
<X className="w-5 h-5 text-slate-400" />
</button>
</div>
<div className="p-5">
<label className="text-sm font-medium text-slate-600 mb-1 block">Select Bike</label>
<select
value={selectedBikeId}
onChange={(e) => setSelectedBikeId(e.target.value)}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
>
<option value="">Select a bike</option>
{availableBikesForAssignment.map(bike => (
<option key={bike.id} value={bike.id}>
{bike.model} - {bike.plateNumber} ({bike.purchasePrice?.toLocaleString() || 0})
</option>
))}
</select>
</div>
<div className="p-5 border-t border-slate-100 flex justify-end gap-3">
<button onClick={() => setShowAssignBikeModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">Cancel</button>
<button onClick={handleAssignBike} disabled={!selectedBikeId} className="px-4 py-2 bg-investor text-white rounded-lg text-sm hover:bg-investor-dark disabled:opacity-50">Assign Bike</button>
</div>
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,875 @@
'use client';
import { useState } from 'react';
import Link from 'next/link';
import { investors as initialInvestors, bikes as initialBikes, transactions } from '@/data/mockData';
import type { Investor } from '@/data/mockData';
import { Plus, Search, Filter, Download, Upload, Eye, Edit, Trash2, X, LayoutGrid, List, Phone, Mail, MapPin, Calendar, Banknote, TrendingUp, Wallet, AlertTriangle, User, FileText, CreditCard, Smartphone, Shield, Star, ExternalLink } from 'lucide-react';
const statusColors: Record<string, string> = {
active: 'bg-green-100 text-green-700',
pending: 'bg-amber-100 text-amber-700',
inactive: 'bg-slate-100 text-slate-500',
suspended: 'bg-red-100 text-red-700',
};
const planColors: Record<string, string> = {
silver: 'bg-slate-200 text-slate-700',
gold: 'bg-yellow-100 text-yellow-700',
platinum: 'bg-purple-100 text-purple-700',
diamond: 'bg-blue-100 text-blue-700',
};
const kycColors: Record<string, string> = {
verified: 'bg-green-100 text-green-700',
pending: 'bg-amber-100 text-amber-700',
rejected: 'bg-red-100 text-red-700',
not_submitted: 'bg-slate-100 text-slate-500',
};
export default function InvestorsPage() {
const [investors, setInvestors] = useState<Investor[]>(initialInvestors);
const [bikes, setBikes] = useState(initialBikes);
const [searchQuery, setSearchQuery] = useState('');
const [statusFilter, setStatusFilter] = useState('all');
const [planFilter, setPlanFilter] = useState('all');
const [selectedInvestor, setSelectedInvestor] = useState<Investor | null>(null);
const [showModal, setShowModal] = useState(false);
const [showDetailsModal, setShowDetailsModal] = useState(false);
const [editingInvestor, setEditingInvestor] = useState<Investor | null>(null);
const [activeTab, setActiveTab] = useState('personal');
const [sortField, setSortField] = useState<string>('name');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc');
const [viewMode, setViewMode] = useState<'table' | 'cards'>('table');
const [showAssignBikeModal, setShowAssignBikeModal] = useState(false);
const [selectedBikeId, setSelectedBikeId] = useState('');
const filteredInvestors = investors.filter(inv => {
const matchesSearch = inv.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
inv.email.toLowerCase().includes(searchQuery.toLowerCase()) ||
inv.phone.includes(searchQuery) ||
inv.id.toLowerCase().includes(searchQuery.toLowerCase());
const matchesStatus = statusFilter === 'all' || inv.status === statusFilter;
const matchesPlan = planFilter === 'all' || (inv.investments?.some(inv => inv.planType === planFilter) ?? false);
return matchesSearch && matchesStatus && matchesPlan;
});
const sortedInvestors = [...filteredInvestors].sort((a, b) => {
const aVal = String(a[sortField as keyof Investor] || '');
const bVal = String(b[sortField as keyof Investor] || '');
if (sortOrder === 'asc') return aVal.localeCompare(bVal);
return bVal.localeCompare(aVal);
});
const handleSort = (field: string) => {
if (sortField === field) {
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
} else {
setSortField(field);
setSortOrder('asc');
}
};
const handleAddInvestor = () => {
setEditingInvestor({
id: `INV${String(investors.length + 1).padStart(3, '0')}`,
userId: `u${100 + investors.length + 1}`,
name: '',
email: '',
phone: '',
address: '',
totalInvested: 0,
totalEarnings: 0,
activeBikes: 0,
withdrawalPending: 0,
totalWithdrawn: 0,
pendingEarnings: 0,
roi: 0,
status: 'pending',
createdAt: new Date().toISOString().split('T')[0],
kycStatus: 'not_submitted',
riskLevel: 'low',
totalReferrals: 0,
referralEarnings: 0,
investments: [],
});
setShowModal(true);
};
const handleEditInvestor = (inv: Investor) => {
setEditingInvestor(JSON.parse(JSON.stringify(inv)));
setShowModal(true);
};
const handleDeleteInvestor = (id: string) => {
if (confirm('Are you sure you want to delete this investor?')) {
setInvestors(investors.filter(i => i.id !== id));
}
};
const handleSaveInvestor = () => {
if (editingInvestor) {
const existingIndex = investors.findIndex(i => i.id === editingInvestor.id);
if (existingIndex >= 0) {
const newInvestors = [...investors];
newInvestors[existingIndex] = editingInvestor;
setInvestors(newInvestors);
} else {
setInvestors([...investors, editingInvestor]);
}
}
setShowModal(false);
setEditingInvestor(null);
};
const handleAssignBike = () => {
if (selectedInvestor && selectedBikeId) {
const bikeIndex = bikes.findIndex(b => b.id === selectedBikeId);
if (bikeIndex >= 0) {
const updatedBikes = [...bikes];
updatedBikes[bikeIndex] = { ...updatedBikes[bikeIndex], investorId: selectedInvestor.id };
setBikes(updatedBikes);
const investorIndex = investors.findIndex(i => i.id === selectedInvestor.id);
if (investorIndex >= 0) {
const updatedInvestors = [...investors];
updatedInvestors[investorIndex] = {
...updatedInvestors[investorIndex],
activeBikes: updatedInvestors[investorIndex].activeBikes + 1,
totalInvested: updatedInvestors[investorIndex].totalInvested + (bikes[bikeIndex].purchasePrice || 0),
};
setInvestors(updatedInvestors);
}
}
setShowAssignBikeModal(false);
setSelectedBikeId('');
}
};
const handleUnassignBike = (bikeId: string) => {
if (selectedInvestor && confirm('Are you sure you want to unassign this bike from the investor?')) {
const bikeIndex = bikes.findIndex(b => b.id === bikeId);
if (bikeIndex >= 0) {
const bike = bikes[bikeIndex];
const updatedBikes = [...bikes];
updatedBikes[bikeIndex] = { ...updatedBikes[bikeIndex], investorId: undefined };
setBikes(updatedBikes);
const investorIndex = investors.findIndex(i => i.id === selectedInvestor.id);
if (investorIndex >= 0) {
const updatedInvestors = [...investors];
updatedInvestors[investorIndex] = {
...updatedInvestors[investorIndex],
activeBikes: Math.max(0, updatedInvestors[investorIndex].activeBikes - 1),
totalInvested: updatedInvestors[investorIndex].totalInvested - (bike.purchasePrice || 0),
};
setInvestors(updatedInvestors);
}
}
}
};
const availableBikesForAssignment = bikes.filter(b => !b.investorId && b.status === 'available');
const assignedBikes = bikes.filter(b => b.investorId === selectedInvestor?.id);
return (
<div className="p-4 lg:p-6">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6">
<div>
<h1 className="text-2xl lg:text-3xl font-extrabold text-slate-800">Investor Management</h1>
<p className="text-sm text-slate-500 mt-1">Manage investor accounts and investments</p>
</div>
<div className="flex items-center gap-2">
<button onClick={handleAddInvestor} className="py-2.5 px-4 bg-investor text-white rounded-lg font-semibold text-sm hover:bg-investor-dark transition-colors flex items-center gap-2">
<Plus className="w-4 h-4" /> Add Investor
</button>
</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-purple-50 flex items-center justify-center">
<Wallet className="w-6 h-6 text-purple-600" />
</div>
<div>
<p className="text-2xl font-extrabold text-slate-800">{investors.reduce((sum, i) => sum + i.totalInvested, 0).toLocaleString()}</p>
<p className="text-sm text-slate-500">Total Invested</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-green-50 flex items-center justify-center">
<TrendingUp className="w-6 h-6 text-green-600" />
</div>
<div>
<p className="text-2xl font-extrabold text-slate-800">{investors.reduce((sum, i) => sum + i.totalEarnings, 0).toLocaleString()}</p>
<p className="text-sm text-slate-500">Total Earnings</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">
<Banknote className="w-6 h-6 text-blue-600" />
</div>
<div>
<p className="text-2xl font-extrabold text-slate-800">{investors.reduce((sum, i) => sum + i.activeBikes, 0)}</p>
<p className="text-sm text-slate-500">Active Bikes</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">
<Calendar className="w-6 h-6 text-amber-600" />
</div>
<div>
<p className="text-2xl font-extrabold text-slate-800">{investors.length}</p>
<p className="text-sm text-slate-500">Total Investors</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 investors by name, email, phone..."
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="active">Active</option>
<option value="pending">Pending</option>
<option value="inactive">Inactive</option>
<option value="suspended">Suspended</option>
</select>
<select
value={planFilter}
onChange={(e) => setPlanFilter(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 Plans</option>
<option value="silver">Silver</option>
<option value="gold">Gold</option>
<option value="platinum">Platinum</option>
<option value="diamond">Diamond</option>
</select>
<button
onClick={() => setViewMode(viewMode === 'table' ? 'cards' : 'table')}
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"
>
{viewMode === 'table' ? <LayoutGrid className="w-4 h-4" /> : <List className="w-4 h-4" />}
{viewMode === 'table' ? 'Cards' : 'Table'}
</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">
<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">Investor</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Contact</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Investment</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Earnings</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Bikes</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Plan</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">KYC</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">
{sortedInvestors.map(inv => (
<tr key={inv.id} className="hover:bg-slate-50 transition-colors">
<td className="px-4 py-3">
<Link href={`/admin/investors/${inv.id}`} className="flex items-center gap-3 hover:bg-slate-50 -m-2 p-2 rounded-lg">
<div className="w-10 h-10 rounded-full bg-purple-100 flex items-center justify-center">
<span className="text-sm font-bold text-purple-600">{inv.name.charAt(0)}</span>
</div>
<div>
<p className="text-sm font-medium text-slate-700">{inv.name}</p>
<p className="text-xs text-slate-400">{inv.id}</p>
</div>
</Link>
</td>
<td className="px-4 py-3">
<div className="text-sm text-slate-600">{inv.phone}</div>
<div className="text-xs text-slate-400">{inv.email}</div>
</td>
<td className="px-4 py-3">
<div className="text-sm font-medium text-purple-600">{inv.totalInvested.toLocaleString()}</div>
</td>
<td className="px-4 py-3">
<div className="text-sm font-medium text-green-600">{inv.totalEarnings.toLocaleString()}</div>
<div className="text-xs text-slate-400">ROI: {inv.roi}%</div>
</td>
<td className="px-4 py-3">
<div className="text-sm font-medium text-slate-700">{inv.activeBikes}</div>
</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 capitalize ${planColors[inv.investments?.[0]?.planType || 'silver']}`}>
{inv.investments?.length || 0} Plan{inv.investments?.length !== 1 ? 's' : ''}
</span>
</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 ${kycColors[inv.kycStatus]}`}>
{inv.kycStatus}
</span>
</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[inv.status]}`}>
{inv.status}
</span>
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-1">
<button onClick={() => { setSelectedInvestor(inv); setShowDetailsModal(true); }} className="p-2 hover:bg-slate-100 rounded-lg" title="View Details">
<Eye className="w-4 h-4 text-blue-500" />
</button>
<button onClick={() => handleEditInvestor(inv)} className="p-2 hover:bg-slate-100 rounded-lg" title="Edit">
<Edit className="w-4 h-4 text-slate-400" />
</button>
<button onClick={() => handleDeleteInvestor(inv.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">
{sortedInvestors.map(inv => (
<Link key={inv.id} href={`/admin/investors/${inv.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">
<span className="text-lg font-bold text-purple-600">{inv.name.charAt(0)}</span>
</div>
<div>
<p className="font-semibold text-slate-700">{inv.name}</p>
<p className="text-xs text-slate-400">{inv.id}</p>
</div>
</div>
<button onClick={(e) => { e.preventDefault(); setSelectedInvestor(inv); setShowDetailsModal(true); }} className="p-1.5 hover:bg-white rounded-lg">
<Eye className="w-4 h-4 text-blue-500" />
</button>
</div>
<div className="flex flex-wrap gap-2 mb-3">
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${statusColors[inv.status]}`}>
{inv.status}
</span>
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${planColors[inv.investments?.[0]?.planType || 'silver']}`}>
{inv.investments?.length || 0} Plans
</span>
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${kycColors[inv.kycStatus]}`}>
{inv.kycStatus}
</span>
</div>
<div className="grid grid-cols-2 gap-2 text-xs">
<div className="bg-white rounded-lg p-2">
<p className="text-slate-400">Invested</p>
<p className="font-medium text-purple-600">{inv.totalInvested.toLocaleString()}</p>
</div>
<div className="bg-white rounded-lg p-2">
<p className="text-slate-400">Earnings</p>
<p className="font-medium text-green-600">{inv.totalEarnings.toLocaleString()}</p>
</div>
<div className="bg-white rounded-lg p-2">
<p className="text-slate-400">Bikes</p>
<p className="font-medium text-slate-700">{inv.activeBikes}</p>
</div>
<div className="bg-white rounded-lg p-2">
<p className="text-slate-400">ROI</p>
<p className="font-medium text-slate-700">{inv.roi}%</p>
</div>
</div>
</Link>
))}
</div>
)}
</div>
{showModal && editingInvestor && (
<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-4xl 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">
{investors.find(i => i.id === editingInvestor.id) ? 'Edit Investor' : 'Add New Investor'}
</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>
<div className="border-b border-slate-100 flex overflow-x-auto">
<button
onClick={() => setActiveTab('personal')}
className={`px-4 py-3 text-sm font-medium whitespace-nowrap ${activeTab === 'personal' ? 'border-b-2 border-accent text-accent' : 'text-slate-500'}`}
>
<User className="w-4 h-4 inline mr-1" /> Personal
</button>
<button
onClick={() => setActiveTab('financial')}
className={`px-4 py-3 text-sm font-medium whitespace-nowrap ${activeTab === 'financial' ? 'border-b-2 border-accent text-accent' : 'text-slate-500'}`}
>
<Banknote className="w-4 h-4 inline mr-1" /> Financial
</button>
<button
onClick={() => setActiveTab('payment')}
className={`px-4 py-3 text-sm font-medium whitespace-nowrap ${activeTab === 'payment' ? 'border-b-2 border-accent text-accent' : 'text-slate-500'}`}
>
<CreditCard className="w-4 h-4 inline mr-1" /> Payment
</button>
<button
onClick={() => setActiveTab('documents')}
className={`px-4 py-3 text-sm font-medium whitespace-nowrap ${activeTab === 'documents' ? 'border-b-2 border-accent text-accent' : 'text-slate-500'}`}
>
<FileText className="w-4 h-4 inline mr-1" /> Documents
</button>
<button
onClick={() => setActiveTab('investment')}
className={`px-4 py-3 text-sm font-medium whitespace-nowrap ${activeTab === 'investment' ? 'border-b-2 border-accent text-accent' : 'text-slate-500'}`}
>
<TrendingUp className="w-4 h-4 inline mr-1" /> Investment
</button>
</div>
<div className="p-5 overflow-y-auto flex-1">
{activeTab === 'personal' && (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Full Name *</label>
<input type="text" value={editingInvestor.name} onChange={(e) => setEditingInvestor({ ...editingInvestor, name: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Enter full name" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Phone Number *</label>
<input type="text" value={editingInvestor.phone} onChange={(e) => setEditingInvestor({ ...editingInvestor, phone: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="01XXXXXXXXX" />
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Alternate Phone</label>
<input type="text" value={editingInvestor.phoneAlt || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, phoneAlt: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Optional" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Email *</label>
<input type="email" value={editingInvestor.email} onChange={(e) => setEditingInvestor({ ...editingInvestor, email: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="email@example.com" />
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Date of Birth</label>
<input type="date" value={editingInvestor.dateOfBirth || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, dateOfBirth: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">NID Number</label>
<input type="text" value={editingInvestor.nidNumber || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, nidNumber: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="NID Number" />
</div>
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Address</label>
<textarea value={editingInvestor.address} onChange={(e) => setEditingInvestor({ ...editingInvestor, address: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" rows={2} placeholder="Enter full address" />
</div>
<div className="grid grid-cols-3 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Emergency Contact</label>
<input type="text" value={editingInvestor.emergencyContactName || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, emergencyContactName: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Contact Name" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Relation</label>
<input type="text" value={editingInvestor.emergencyContactRelation || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, emergencyContactRelation: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Relation" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Phone</label>
<input type="text" value={editingInvestor.emergencyContactPhone || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, emergencyContactPhone: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Phone" />
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Status</label>
<select value={editingInvestor.status} onChange={(e) => setEditingInvestor({ ...editingInvestor, status: e.target.value as any })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
<option value="active">Active</option>
<option value="pending">Pending</option>
<option value="inactive">Inactive</option>
<option value="suspended">Suspended</option>
</select>
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">KYC Status</label>
<select value={editingInvestor.kycStatus} onChange={(e) => setEditingInvestor({ ...editingInvestor, kycStatus: e.target.value as any })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
<option value="verified">Verified</option>
<option value="pending">Pending</option>
<option value="rejected">Rejected</option>
<option value="not_submitted">Not Submitted</option>
</select>
</div>
</div>
</div>
)}
{activeTab === 'financial' && (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Total Invested ()</label>
<input type="number" value={editingInvestor.totalInvested} onChange={(e) => setEditingInvestor({ ...editingInvestor, totalInvested: Number(e.target.value) })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Total Earnings ()</label>
<input type="number" value={editingInvestor.totalEarnings} onChange={(e) => setEditingInvestor({ ...editingInvestor, totalEarnings: Number(e.target.value) })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Total Withdrawn ()</label>
<input type="number" value={editingInvestor.totalWithdrawn} onChange={(e) => setEditingInvestor({ ...editingInvestor, totalWithdrawn: Number(e.target.value) })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Pending Earnings ()</label>
<input type="number" value={editingInvestor.pendingEarnings} onChange={(e) => setEditingInvestor({ ...editingInvestor, pendingEarnings: Number(e.target.value) })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Withdrawal Pending ()</label>
<input type="number" value={editingInvestor.withdrawalPending} onChange={(e) => setEditingInvestor({ ...editingInvestor, withdrawalPending: Number(e.target.value) })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">ROI (%)</label>
<input type="number" value={editingInvestor.roi} onChange={(e) => setEditingInvestor({ ...editingInvestor, roi: Number(e.target.value) })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Referral Earnings ()</label>
<input type="number" value={editingInvestor.referralEarnings} onChange={(e) => setEditingInvestor({ ...editingInvestor, referralEarnings: Number(e.target.value) })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Total Referrals</label>
<input type="number" value={editingInvestor.totalReferrals} onChange={(e) => setEditingInvestor({ ...editingInvestor, totalReferrals: Number(e.target.value) })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
</div>
</div>
)}
{activeTab === 'payment' && (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Bank Name</label>
<input type="text" value={editingInvestor.bankName || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, bankName: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Bank Name" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Bank Branch</label>
<input type="text" value={editingInvestor.bankBranch || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, bankBranch: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Branch" />
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Account Name</label>
<input type="text" value={editingInvestor.bankAccountName || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, bankAccountName: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Account Name" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Account Number</label>
<input type="text" value={editingInvestor.bankAccountNumber || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, bankAccountNumber: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Account Number" />
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Mobile Banking</label>
<select value={editingInvestor.mobileBanking || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, mobileBanking: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
<option value="">Select</option>
<option value="Bkash">Bkash</option>
<option value="Nagad">Nagad</option>
<option value="Rocket">Rocket</option>
<option value="Upay">Upay</option>
</select>
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Mobile Number</label>
<input type="text" value={editingInvestor.mobileBankingNumber || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, mobileBankingNumber: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Mobile Number" />
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">TIN Number</label>
<input type="text" value={editingInvestor.tinNumber || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, tinNumber: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="TIN Number" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Passport Number</label>
<input type="text" value={editingInvestor.passportNumber || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, passportNumber: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Passport Number" />
</div>
</div>
</div>
)}
{activeTab === 'investment' && (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Active Bikes</label>
<input type="number" value={editingInvestor.activeBikes} onChange={(e) => setEditingInvestor({ ...editingInvestor, activeBikes: Number(e.target.value) })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Risk Level</label>
<select value={editingInvestor.riskLevel} onChange={(e) => setEditingInvestor({ ...editingInvestor, riskLevel: e.target.value as any })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</div>
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-2 block">Investments ({editingInvestor.investments?.length || 0})</label>
<div className="space-y-2">
{editingInvestor.investments?.map((inv, idx) => (
<div key={idx} className="p-3 bg-slate-50 rounded-lg">
<div className="flex items-center justify-between">
<span className="font-medium text-sm">{inv.planName}</span>
<span className={`text-xs px-2 py-1 rounded-full ${planColors[inv.planType]}`}>{inv.planType}</span>
</div>
<div className="grid grid-cols-2 gap-2 mt-2 text-xs">
<div>
<span className="text-slate-400">Investment:</span>
<span className="font-medium ml-1">{inv.totalInvestment.toLocaleString()}</span>
</div>
<div>
<span className="text-slate-400">Monthly:</span>
<span className="font-medium ml-1">{inv.monthlyReturn.toLocaleString()}</span>
</div>
</div>
</div>
))}
<p className="text-sm text-slate-500 text-center py-2">Add investments from the single investor page</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Referral Code</label>
<input type="text" value={editingInvestor.referralCode || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, referralCode: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Referral Code" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Referred By</label>
<input type="text" value={editingInvestor.referredBy || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, referredBy: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Referrer ID" />
</div>
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Notes</label>
<textarea value={editingInvestor.notes || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, notes: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" rows={3} placeholder="Additional notes..." />
</div>
</div>
)}
{activeTab === 'documents' && (
<div className="space-y-4">
<div className="border-2 border-dashed border-slate-200 rounded-lg p-8 text-center">
<FileText className="w-12 h-12 text-slate-300 mx-auto mb-2" />
<p className="text-sm text-slate-500">Drag and drop files here, or click to browse</p>
<button className="mt-2 px-4 py-2 bg-slate-100 text-slate-600 rounded-lg text-sm hover:bg-slate-200">Browse Files</button>
</div>
<div className="space-y-2">
{editingInvestor.kycDocuments?.map((doc, idx) => (
<div key={idx} className="flex items-center justify-between p-3 bg-slate-50 rounded-lg">
<div className="flex items-center gap-3">
<FileText className="w-5 h-5 text-slate-400" />
<div>
<p className="text-sm font-medium text-slate-700 capitalize">{doc.type.replace('_', ' ')}</p>
<p className="text-xs text-slate-400">{doc.number || 'No number'}</p>
</div>
</div>
<span className={`text-xs font-medium px-2 py-1 rounded-full ${doc.verified ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700'}`}>
{doc.verified ? 'Verified' : 'Pending'}
</span>
</div>
))}
</div>
</div>
)}
</div>
<div className="p-5 border-t border-slate-100 flex justify-end gap-3">
<button onClick={() => setShowModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">Cancel</button>
<button onClick={handleSaveInvestor} className="px-4 py-2 bg-investor text-white rounded-lg text-sm hover:bg-investor-dark">Save Investor</button>
</div>
</div>
</div>
)}
{showDetailsModal && selectedInvestor && (
<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">
<div className="p-5 border-b border-slate-100 flex items-center justify-between">
<h2 className="text-lg font-bold text-slate-800">Investor 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>
<div className="p-5 overflow-y-auto max-h-[60vh]">
<div className="flex items-center gap-4 mb-6">
<div className="w-16 h-16 rounded-full bg-purple-100 flex items-center justify-center">
<span className="text-2xl font-bold text-purple-600">{selectedInvestor.name.charAt(0)}</span>
</div>
<div>
<h3 className="text-xl font-bold text-slate-800">{selectedInvestor.name}</h3>
<p className="text-sm text-slate-500">{selectedInvestor.id}</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4 mb-6">
<div className="bg-slate-50 rounded-lg p-4">
<p className="text-sm text-slate-500 mb-1">Total Invested</p>
<p className="text-xl font-bold text-purple-600">{selectedInvestor.totalInvested.toLocaleString()}</p>
</div>
<div className="bg-slate-50 rounded-lg p-4">
<p className="text-sm text-slate-500 mb-1">Total Earnings</p>
<p className="text-xl font-bold text-green-600">{selectedInvestor.totalEarnings.toLocaleString()}</p>
</div>
<div className="bg-slate-50 rounded-lg p-4">
<p className="text-sm text-slate-500 mb-1">Active Bikes</p>
<p className="text-xl font-bold text-slate-800">{selectedInvestor.activeBikes}</p>
</div>
<div className="bg-slate-50 rounded-lg p-4">
<p className="text-sm text-slate-500 mb-1">ROI</p>
<p className="text-xl font-bold text-slate-800">{selectedInvestor.roi}%</p>
</div>
</div>
<div className="space-y-3">
<div className="flex justify-between text-sm">
<span className="text-slate-500">Phone</span>
<span className="font-medium">{selectedInvestor.phone}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-slate-500">Email</span>
<span className="font-medium">{selectedInvestor.email}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-slate-500">Investment Plans</span>
<span className="font-medium capitalize">{selectedInvestor.investments?.length || 0} Plans</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-slate-500">Status</span>
<span className={`font-medium capitalize ${selectedInvestor.status === 'active' ? 'text-green-600' : 'text-amber-600'}`}>{selectedInvestor.status}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-slate-500">KYC</span>
<span className={`font-medium capitalize ${selectedInvestor.kycStatus === 'verified' ? 'text-green-600' : 'text-amber-600'}`}>{selectedInvestor.kycStatus}</span>
</div>
</div>
<div className="mt-6 pt-4 border-t border-slate-100">
<div className="flex items-center justify-between mb-3">
<h4 className="font-semibold text-slate-700">Assigned Bikes ({assignedBikes.length})</h4>
<button
onClick={() => setShowAssignBikeModal(true)}
className="text-sm text-investor hover:underline flex items-center gap-1"
>
<Plus className="w-4 h-4" /> Add Bike
</button>
</div>
<div className="space-y-2">
{assignedBikes.map(bike => (
<div key={bike.id} className="flex items-center justify-between p-3 bg-slate-50 rounded-lg">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-white flex items-center justify-center">
<Banknote className="w-5 h-5 text-purple-600" />
</div>
<div>
<p className="text-sm font-medium text-slate-700">{bike.model}</p>
<p className="text-xs text-slate-400">{bike.plateNumber}</p>
</div>
</div>
<div className="flex items-center gap-2">
<span className={`text-xs font-medium px-2 py-1 rounded-full ${bike.status === 'rented' ? 'bg-green-100 text-green-700' : 'bg-blue-100 text-blue-700'}`}>
{bike.status}
</span>
<button onClick={() => handleUnassignBike(bike.id)} className="p-1 hover:bg-red-50 rounded-lg text-red-400">
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
))}
{assignedBikes.length === 0 && (
<p className="text-center text-slate-400 py-4">No bikes assigned</p>
)}
</div>
</div>
</div>
<div className="p-5 border-t border-slate-100 flex justify-between">
<button onClick={() => handleDeleteInvestor(selectedInvestor.id)} className="px-4 py-2 border border-red-200 text-red-600 rounded-lg text-sm hover:bg-red-50">Delete</button>
<div className="flex gap-2">
<button onClick={() => { handleEditInvestor(selectedInvestor); setShowDetailsModal(false); }} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">Edit</button>
<Link href={`/admin/investors/${selectedInvestor.id}`} className="px-4 py-2 bg-investor text-white rounded-lg text-sm hover:bg-investor-dark flex items-center gap-1">
View Full Page <ExternalLink className="w-4 h-4" />
</Link>
</div>
</div>
</div>
</div>
)}
{showAssignBikeModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
<div className="p-5 border-b border-slate-100 flex items-center justify-between">
<h2 className="text-lg font-bold text-slate-800">Assign Bike to Investor</h2>
<button onClick={() => setShowAssignBikeModal(false)} className="p-2 hover:bg-slate-100 rounded-lg">
<X className="w-5 h-5 text-slate-400" />
</button>
</div>
<div className="p-5">
<label className="text-sm font-medium text-slate-600 mb-1 block">Select Bike</label>
<select
value={selectedBikeId}
onChange={(e) => setSelectedBikeId(e.target.value)}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
>
<option value="">Select a bike</option>
{availableBikesForAssignment.map(bike => (
<option key={bike.id} value={bike.id}>
{bike.model} - {bike.plateNumber} ({bike.purchasePrice?.toLocaleString() || 0})
</option>
))}
</select>
</div>
<div className="p-5 border-t border-slate-100 flex justify-end gap-3">
<button onClick={() => setShowAssignBikeModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">Cancel</button>
<button onClick={handleAssignBike} disabled={!selectedBikeId} className="px-4 py-2 bg-investor text-white rounded-lg text-sm hover:bg-investor-dark disabled:opacity-50">Assign Bike</button>
</div>
</div>
</div>
)}
</div>
);
}

128
src/app/admin/kyc/page.tsx Normal file
View File

@@ -0,0 +1,128 @@
import { Shield, Search, Filter, Check, X, Clock, User, Phone, MapPin, FileText, Eye, Download } from 'lucide-react';
import { kycRequests } from '@/data/mockData';
const statusColors: Record<string, string> = {
pending: 'bg-amber-100 text-amber-700',
approved: 'bg-green-100 text-green-700',
rejected: 'bg-red-100 text-red-700',
};
export default function KYCReviewsPage() {
const pending = kycRequests.filter(k => k.status === 'pending');
const approved = kycRequests.filter(k => k.status === 'approved');
const rejected = kycRequests.filter(k => k.status === 'rejected');
return (
<div className="p-4 lg:p-6">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6">
<div>
<h1 className="text-2xl lg:text-3xl font-extrabold text-slate-800">KYC Verification</h1>
<p className="text-sm text-slate-500 mt-1">Review and verify biker identity documents</p>
</div>
<div className="flex items-center gap-2">
<button className="py-2 px-4 border border-slate-200 rounded-lg text-sm font-medium text-slate-600 hover:bg-slate-50 flex items-center gap-2">
<Download className="w-4 h-4" /> Export
</button>
</div>
</div>
<div className="grid grid-cols-3 gap-4 mb-6">
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-xl bg-amber-50 flex items-center justify-center">
<Clock className="w-6 h-6 text-amber-600" />
</div>
<div>
<p className="text-2xl font-extrabold text-slate-800">{pending.length}</p>
<p className="text-sm text-slate-500">Pending Review</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-green-50 flex items-center justify-center">
<Check className="w-6 h-6 text-green-600" />
</div>
<div>
<p className="text-2xl font-extrabold text-slate-800">{approved.length}</p>
<p className="text-sm text-slate-500">Approved</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-red-50 flex items-center justify-center">
<X className="w-6 h-6 text-red-600" />
</div>
<div>
<p className="text-2xl font-extrabold text-slate-800">{rejected.length}</p>
<p className="text-sm text-slate-500">Rejected</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">
<div className="flex items-center gap-2">
<button className="px-3 py-1.5 bg-slate-800 text-white rounded-lg text-sm font-medium">All</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">Pending</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">Approved</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">Rejected</button>
</div>
</div>
<div className="divide-y divide-slate-50">
{kycRequests.map(kyc => (
<div key={kyc.id} className="p-5 hover:bg-slate-50 transition-colors">
<div className="flex flex-col lg:flex-row lg:items-start gap-4">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center">
<User className="w-6 h-6 text-slate-600" />
</div>
<div>
<p className="font-semibold text-slate-800">{kyc.userName}</p>
<p className="text-sm text-slate-500 flex items-center gap-1">
<Phone className="w-3 h-3" /> {kyc.phone}
</p>
</div>
</div>
<div className="flex-1">
<div className="flex flex-wrap gap-4 text-sm text-slate-600">
<p className="flex items-center gap-1">
<Clock className="w-3 h-3" /> Submitted: {kyc.submittedAt}
</p>
<p className="flex items-center gap-1">
<MapPin className="w-3 h-3" /> Dhaka, Bangladesh
</p>
<p className="flex items-center gap-1">
<FileText className="w-3 h-3" /> NID: 1234567890
</p>
</div>
</div>
<div className="flex items-center gap-3">
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${statusColors[kyc.status]}`}>
{kyc.status}
</span>
{kyc.status === 'pending' && (
<div className="flex gap-2">
<button className="py-2 px-4 bg-green-600 text-white text-sm font-semibold rounded-lg hover:bg-green-700 flex items-center gap-1">
<Check className="w-4 h-4" /> Approve
</button>
<button className="py-2 px-4 border border-red-200 text-red-600 text-sm font-semibold rounded-lg hover:bg-red-50 flex items-center gap-1">
<X className="w-4 h-4" /> Reject
</button>
<button className="py-2 px-3 border border-slate-200 text-slate-600 text-sm font-semibold rounded-lg hover:bg-slate-50 flex items-center gap-1">
<Eye className="w-4 h-4" /> View Docs
</button>
</div>
)}
</div>
</div>
</div>
))}
</div>
</div>
</div>
);
}

211
src/app/admin/page.tsx Normal file
View File

@@ -0,0 +1,211 @@
import {
Users,
Bike,
DollarSign,
TrendingUp,
Activity,
ArrowUpRight,
ArrowDownRight,
Clock,
Shield,
AlertTriangle,
CheckCircle,
XCircle
} from 'lucide-react';
import { kycRequests, bikes, rentals, transactions } from '@/data/mockData';
const stats = [
{ label: 'Total Bikers', value: '156', change: '+12%', trend: 'up', icon: Users, color: 'text-blue-600', bg: 'bg-blue-50' },
{ label: 'Active Rentals', value: '89', change: '+8%', trend: 'up', icon: Bike, color: 'text-green-600', bg: 'bg-green-50' },
{ label: 'Daily Revenue', value: '৳45.6k', change: '+23%', trend: 'up', icon: DollarSign, color: 'text-purple-600', bg: 'bg-purple-50' },
{ label: 'Fleet Utilization', value: '78%', change: '-2%', trend: 'down', icon: TrendingUp, color: 'text-amber-600', bg: 'bg-amber-50' },
];
const pendingKYCs = kycRequests.filter(k => k.status === 'pending');
const activeRentals = rentals.filter(r => r.status === 'active');
export default function AdminDashboard() {
return (
<div className="p-4 lg:p-6">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6">
<div>
<h1 className="text-2xl lg:text-3xl font-extrabold text-slate-800">Admin Dashboard</h1>
<p className="text-sm text-slate-500 mt-1">Manage fleet, users, and operations</p>
</div>
<div className="flex items-center gap-2">
<span className="text-sm text-slate-500">Last updated:</span>
<span className="text-sm font-medium text-slate-700">Just now</span>
</div>
</div>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
{stats.map((stat, index) => {
const Icon = stat.icon;
return (
<div key={index} className="bg-white rounded-xl p-5 shadow-sm border border-slate-100 hover:shadow-md transition-shadow">
<div className="flex items-start justify-between mb-3">
<div className={`w-12 h-12 rounded-xl ${stat.bg} flex items-center justify-center`}>
<Icon className={`w-6 h-6 ${stat.color}`} />
</div>
<span className={`text-xs font-semibold px-2 py-1 rounded-full flex items-center gap-1 ${
stat.trend === 'up' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'
}`}>
{stat.trend === 'up' ? <ArrowUpRight className="w-3 h-3" /> : <ArrowDownRight className="w-3 h-3" />}
{stat.change}
</span>
</div>
<p className="text-2xl font-extrabold text-slate-800">{stat.value}</p>
<p className="text-sm text-slate-500 mt-1">{stat.label}</p>
</div>
);
})}
</div>
<div className="grid lg:grid-cols-3 gap-6 mb-8">
<div className="lg:col-span-2 bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden">
<div className="px-5 py-4 border-b border-slate-100 flex items-center justify-between">
<h2 className="font-bold text-slate-800">Recent Rentals</h2>
<button className="text-sm text-blue-600 font-medium hover:underline">View All</button>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-50">
<tr>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Rental ID</th>
<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">User</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Type</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Status</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Daily Rate</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{activeRentals.map(rental => {
const bike = bikes.find(b => b.id === rental.bikeId);
return (
<tr key={rental.id} className="hover:bg-slate-50 transition-colors">
<td className="px-4 py-3">
<span className="text-sm font-medium text-slate-700">{rental.id}</span>
</td>
<td className="px-4 py-3">
<span className="text-sm text-slate-600">{bike?.model}</span>
<span className="text-xs text-slate-400 block">{bike?.plateNumber}</span>
</td>
<td className="px-4 py-3">
<span className="text-sm text-slate-600">{rental.userId}</span>
</td>
<td className="px-4 py-3">
<span className="text-sm text-slate-600 capitalize">{rental.type}</span>
</td>
<td className="px-4 py-3">
<span className={`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 === 'pending' ? 'bg-amber-100 text-amber-700' :
'bg-slate-100 text-slate-500'
}`}>
<Activity className="w-3 h-3" />
{rental.status}
</span>
</td>
<td className="px-4 py-3">
<span className="text-sm font-semibold text-slate-700">{rental.dailyRate}</span>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden">
<div className="px-5 py-4 border-b border-slate-100 flex items-center justify-between">
<h2 className="font-bold text-slate-800">KYC Requests</h2>
<span className="text-xs font-semibold px-2 py-1 bg-amber-100 text-amber-700 rounded-full">
{pendingKYCs.length} pending
</span>
</div>
<div className="divide-y divide-slate-50">
{pendingKYCs.map(kyc => (
<div key={kyc.id} className="p-4 hover:bg-slate-50 transition-colors">
<div className="flex items-start justify-between mb-3">
<div>
<p className="font-medium text-slate-700">{kyc.userName}</p>
<p className="text-sm text-slate-400">{kyc.phone}</p>
<p className="text-xs text-slate-400 mt-1 flex items-center gap-1">
<Clock className="w-3 h-3" /> {kyc.submittedAt}
</p>
</div>
<span className="p-1.5 bg-amber-100 rounded-lg">
<Shield className="w-4 h-4 text-amber-600" />
</span>
</div>
<div className="flex gap-2">
<button className="flex-1 py-2 bg-green-600 text-white text-sm font-semibold rounded-lg hover:bg-green-700 flex items-center justify-center gap-1">
<CheckCircle className="w-4 h-4" /> Approve
</button>
<button className="flex-1 py-2 border border-red-200 text-red-600 text-sm font-semibold rounded-lg hover:bg-red-50 flex items-center justify-center gap-1">
<XCircle className="w-4 h-4" /> Reject
</button>
</div>
</div>
))}
{pendingKYCs.length === 0 && (
<div className="p-8 text-center">
<CheckCircle className="w-12 h-12 text-green-500 mx-auto mb-2" />
<p className="text-sm text-slate-500">No pending KYC requests</p>
</div>
)}
</div>
</div>
</div>
<div className="grid lg:grid-cols-2 gap-6">
<div className="bg-white rounded-xl shadow-sm border border-slate-100 p-5">
<h2 className="font-bold text-slate-800 mb-4">Quick Actions</h2>
<div className="grid grid-cols-2 gap-3">
<button className="py-3 px-4 bg-accent text-white rounded-lg font-semibold text-sm hover:bg-accent-dark transition-colors">
+ Add New Bike
</button>
<button className="py-3 px-4 bg-blue-600 text-white rounded-lg font-semibold text-sm hover:bg-blue-700 transition-colors">
Manage Users
</button>
<button className="py-3 px-4 bg-purple-600 text-white rounded-lg font-semibold text-sm hover:bg-purple-700 transition-colors">
View Reports
</button>
<button className="py-3 px-4 border border-slate-200 text-slate-600 rounded-lg font-semibold text-sm hover:bg-slate-50 transition-colors">
Settings
</button>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-100 p-5">
<h2 className="font-bold text-slate-800 mb-4">System Alerts</h2>
<div className="space-y-3">
<div className="flex items-center gap-3 p-3 bg-red-50 rounded-lg border border-red-100">
<AlertTriangle className="w-5 h-5 text-red-500" />
<div>
<p className="text-sm font-medium text-red-700">3 bikes overdue maintenance</p>
<p className="text-xs text-red-500">Action required</p>
</div>
</div>
<div className="flex items-center gap-3 p-3 bg-amber-50 rounded-lg border border-amber-100">
<Clock className="w-5 h-5 text-amber-500" />
<div>
<p className="text-sm font-medium text-amber-700">5 pending KYC verifications</p>
<p className="text-xs text-amber-500">Review needed</p>
</div>
</div>
<div className="flex items-center gap-3 p-3 bg-green-50 rounded-lg border border-green-100">
<CheckCircle className="w-5 h-5 text-green-500" />
<div>
<p className="text-sm font-medium text-green-700">System running smoothly</p>
<p className="text-xs text-green-500">All services operational</p>
</div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,108 @@
import { FileText, Search, Filter, Bike, User, Calendar, DollarSign, Clock, MoreVertical, Eye } from 'lucide-react';
import { rentals, bikes, users } from '@/data/mockData';
export default function RentalsPage() {
return (
<div className="p-4 lg:p-6">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6">
<div>
<h1 className="text-2xl lg:text-3xl font-extrabold text-slate-800">Rentals</h1>
<p className="text-sm text-slate-500 mt-1">View and manage all rental transactions</p>
</div>
<div className="flex items-center gap-2">
<button className="py-2 px-4 border border-slate-200 rounded-lg text-sm font-medium text-slate-600 hover:bg-slate-50 flex items-center gap-2">
<Filter className="w-4 h-4" /> Filter
</button>
<button className="py-2 px-4 bg-accent text-white rounded-lg text-sm font-semibold hover:bg-accent-dark">
Export
</button>
</div>
</div>
<div className="grid grid-cols-4 gap-4 mb-6">
{['Active', 'Pending', 'Completed', 'Disputed'].map(status => {
const count = rentals.filter(r => r.status === status.toLowerCase()).length;
return (
<div key={status} className="bg-white rounded-xl p-4 shadow-sm border border-slate-100">
<p className="text-2xl font-extrabold text-slate-800">{count}</p>
<p className="text-sm text-slate-500">{status} Rentals</p>
</div>
);
})}
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden">
<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">Rental ID</th>
<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">User</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Type</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Start Date</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Deposit</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Total Paid</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">
{rentals.map(rental => {
const bike = bikes.find(b => b.id === rental.bikeId);
return (
<tr key={rental.id} className="hover:bg-slate-50 transition-colors">
<td className="px-4 py-3">
<span className="text-sm font-medium text-slate-700">{rental.id}</span>
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<Bike className="w-4 h-4 text-slate-400" />
<span className="text-sm text-slate-600">{bike?.model}</span>
</div>
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<User className="w-4 h-4 text-slate-400" />
<span className="text-sm text-slate-600">{rental.userId}</span>
</div>
</td>
<td className="px-4 py-3">
<span className="text-sm text-slate-600 capitalize">{rental.type}</span>
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-1 text-sm text-slate-600">
<Calendar className="w-3 h-3" /> {rental.startDate}
</div>
</td>
<td className="px-4 py-3">
<span className="text-sm font-medium text-slate-700">{rental.deposit}</span>
</td>
<td className="px-4 py-3">
<span className="text-sm font-semibold text-green-600">{rental.totalPaid}</span>
</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 ${
rental.status === 'active' ? 'bg-green-100 text-green-700' :
rental.status === 'pending' ? 'bg-amber-100 text-amber-700' :
rental.status === 'completed' ? 'bg-blue-100 text-blue-700' :
'bg-red-100 text-red-700'
}`}>
{rental.status}
</span>
</td>
<td className="px-4 py-3">
<button className="p-2 hover:bg-slate-100 rounded-lg">
<Eye className="w-4 h-4 text-slate-400" />
</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,75 @@
import { FileText, Download, Calendar, BarChart3, PieChart, TrendingUp, Bike, Users } from 'lucide-react';
export default function ReportsPage() {
return (
<div className="p-4 lg:p-6">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6">
<div>
<h1 className="text-2xl lg:text-3xl font-extrabold text-slate-800">Reports</h1>
<p className="text-sm text-slate-500 mt-1">Analytics and insights</p>
</div>
<button className="py-2 px-4 bg-accent text-white rounded-lg text-sm font-semibold hover:bg-accent-dark flex items-center gap-2">
<Download className="w-4 h-4" /> Export All Reports
</button>
</div>
<div className="grid lg:grid-cols-3 gap-6 mb-8">
<div className="bg-white rounded-xl shadow-sm border border-slate-100 p-5">
<div className="flex items-center gap-3 mb-4">
<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>
<h3 className="font-semibold text-slate-800">Fleet Utilization</h3>
</div>
<p className="text-3xl font-extrabold text-slate-800 mb-2">78%</p>
<p className="text-sm text-green-600">+5% from last month</p>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-100 p-5">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-lg bg-green-50 flex items-center justify-center">
<Users className="w-5 h-5 text-green-600" />
</div>
<h3 className="font-semibold text-slate-800">Active Bikers</h3>
</div>
<p className="text-3xl font-extrabold text-slate-800 mb-2">89</p>
<p className="text-sm text-green-600">+12 new this month</p>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-100 p-5">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-lg bg-purple-50 flex items-center justify-center">
<TrendingUp className="w-5 h-5 text-purple-600" />
</div>
<h3 className="font-semibold text-slate-800">Avg. Daily Revenue</h3>
</div>
<p className="text-3xl font-extrabold text-slate-800 mb-2">45.6k</p>
<p className="text-sm text-green-600">+23% from last month</p>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-100 p-5 mb-6">
<h2 className="font-bold text-slate-800 mb-4">Available Reports</h2>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-4">
{[
{ title: 'Revenue Report', desc: 'Daily, weekly, monthly revenue', icon: BarChart3 },
{ title: 'Fleet Utilization', desc: 'Bike usage and availability', icon: PieChart },
{ title: 'Biker Activity', desc: 'Rides, hours, distance', icon: TrendingUp },
{ title: 'Damage Report', desc: 'Incidents and claims', icon: FileText },
{ title: 'KYC Summary', desc: 'Verification stats', icon: Users },
{ title: 'Investment ROI', desc: 'Investor returns', icon: TrendingUp },
].map((report, index) => {
const Icon = report.icon;
return (
<div key={index} className="p-4 border border-slate-200 rounded-xl hover:border-accent hover:shadow-md transition-all cursor-pointer">
<Icon className="w-6 h-6 text-accent mb-2" />
<h3 className="font-semibold text-slate-800">{report.title}</h3>
<p className="text-sm text-slate-500">{report.desc}</p>
</div>
);
})}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,128 @@
import { DollarSign, TrendingUp, TrendingDown, Calendar, Download, ArrowUpRight, ArrowDownRight } from 'lucide-react';
export default function RevenuePage() {
return (
<div className="p-4 lg:p-6">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6">
<div>
<h1 className="text-2xl lg:text-3xl font-extrabold text-slate-800">Revenue</h1>
<p className="text-sm text-slate-500 mt-1">Track earnings and financial performance</p>
</div>
<div className="flex items-center gap-2">
<button className="py-2 px-4 border border-slate-200 rounded-lg text-sm font-medium text-slate-600 hover:bg-slate-50 flex items-center gap-2">
<Calendar className="w-4 h-4" /> Select Date Range
</button>
<button className="py-2 px-4 bg-accent text-white rounded-lg text-sm font-semibold hover:bg-accent-dark flex items-center gap-2">
<Download className="w-4 h-4" /> Export Report
</button>
</div>
</div>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
{[
{ label: 'Today', value: '৳45,600', change: '+23%', trend: 'up' },
{ label: 'This Week', value: '৳312,400', change: '+15%', trend: 'up' },
{ label: 'This Month', value: '৳1,245,000', change: '+8%', trend: 'up' },
{ label: 'This Year', value: '৳8,450,000', change: '+12%', trend: 'up' },
].map((item, index) => (
<div key={index} className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<div className="flex items-start justify-between mb-2">
<span className={`text-xs font-semibold px-2 py-1 rounded-full flex items-center gap-1 ${
item.trend === 'up' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'
}`}>
{item.trend === 'up' ? <ArrowUpRight className="w-3 h-3" /> : <ArrowDownRight className="w-3 h-3" />}
{item.change}
</span>
</div>
<p className="text-2xl font-extrabold text-slate-800">{item.value}</p>
<p className="text-sm text-slate-500">{item.label}</p>
</div>
))}
</div>
<div className="grid lg:grid-cols-2 gap-6 mb-8">
<div className="bg-white rounded-xl shadow-sm border border-slate-100 p-5">
<h2 className="font-bold text-slate-800 mb-4">Revenue Breakdown</h2>
<div className="space-y-4">
{[
{ label: 'Daily Rentals', value: '৳32,400', percent: 71 },
{ label: 'Weekly Rentals', value: '৳8,200', percent: 18 },
{ label: 'Deposits', value: '৳5,000', percent: 11 },
].map((item, index) => (
<div key={index}>
<div className="flex items-center justify-between mb-1">
<span className="text-sm text-slate-600">{item.label}</span>
<span className="text-sm font-semibold text-slate-700">{item.value}</span>
</div>
<div className="w-full bg-slate-100 rounded-full h-2">
<div className="bg-accent h-2 rounded-full" style={{ width: `${item.percent}%` }} />
</div>
</div>
))}
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-100 p-5">
<h2 className="font-bold text-slate-800 mb-4">Top Performing Bikes</h2>
<div className="space-y-3">
{[
{ model: 'Etron ET50', earnings: '৳12,400', rides: 45 },
{ model: 'AIMA Lightning', earnings: '৳10,200', rides: 38 },
{ model: 'Yadea DT3', earnings: '৳8,800', rides: 32 },
].map((bike, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-slate-50 rounded-lg">
<div>
<p className="text-sm font-medium text-slate-700">{bike.model}</p>
<p className="text-xs text-slate-400">{bike.rides} rides</p>
</div>
<div className="text-right">
<p className="text-sm font-semibold text-green-600">{bike.earnings}</p>
<p className="text-xs text-slate-400">earnings</p>
</div>
</div>
))}
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden">
<div className="px-5 py-4 border-b border-slate-100">
<h2 className="font-bold text-slate-800">Recent Transactions</h2>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-50">
<tr>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Date</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Description</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Type</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Amount</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{[
{ date: '2024-03-21', desc: 'Rental Payment - r1', type: 'Rental', amount: '৳350', status: 'Completed' },
{ date: '2024-03-21', desc: 'Deposit - r1', type: 'Deposit', amount: '৳5,000', status: 'Completed' },
{ date: '2024-03-20', desc: 'Rental Payment - r2', type: 'Rental', amount: '৳450', status: 'Completed' },
{ date: '2024-03-20', desc: 'Top Up - u1', type: 'Top Up', amount: '৳2,000', status: 'Completed' },
].map((tx, index) => (
<tr key={index} className="hover:bg-slate-50">
<td className="px-4 py-3 text-sm text-slate-600">{tx.date}</td>
<td className="px-4 py-3 text-sm font-medium text-slate-700">{tx.desc}</td>
<td className="px-4 py-3 text-sm text-slate-600">{tx.type}</td>
<td className="px-4 py-3 text-sm font-semibold text-green-600">{tx.amount}</td>
<td className="px-4 py-3">
<span className="text-xs font-medium px-2.5 py-1 rounded-full bg-green-100 text-green-700">
{tx.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
}

62
src/app/bikes/page.tsx Normal file
View File

@@ -0,0 +1,62 @@
'use client';
import { useState } from 'react';
import BikeCard from '@/components/BikeCard';
import { availableBikes } from '@/data/mockData';
import { Search, Filter } from 'lucide-react';
export default function BikesPage() {
const [filter, setFilter] = useState<string>('all');
const [sort, setSort] = useState<string>('location');
return (
<div className="p-4 lg:p-6">
<div className="mb-6">
<h1 className="text-xl lg:text-2xl font-extrabold text-slate-800">Browse EVs</h1>
<p className="text-sm text-slate-500">Find the perfect bike for your needs</p>
</div>
<div className="flex flex-wrap gap-3 mb-6">
<select
value={filter}
onChange={(e) => setFilter(e.target.value)}
className="px-4 py-2 bg-white border border-slate-200 rounded-lg text-sm font-medium"
>
<option value="all">All Locations</option>
<option value="gulshan">Gulshan</option>
<option value="banani">Banani</option>
<option value="uttara">Uttara</option>
<option value="dhanmondi">Dhanmondi</option>
<option value="mirpur">Mirpur</option>
</select>
<select
value={sort}
onChange={(e) => setSort(e.target.value)}
className="px-4 py-2 bg-white border border-slate-200 rounded-lg text-sm font-medium"
>
<option value="location">Sort by Location</option>
<option value="battery">Sort by Battery</option>
<option value="model">Sort by Model</option>
</select>
<button className="px-4 py-2 bg-accent text-white rounded-lg text-sm font-medium hover:bg-accent-dark flex items-center gap-2">
<Search className="w-4 h-4" /> Search
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{availableBikes.map(bike => (
<BikeCard key={bike.id} bike={bike} />
))}
</div>
{availableBikes.length === 0 && (
<div className="text-center py-12">
<p className="text-slate-400 text-lg">No bikes available</p>
<p className="text-slate-400 text-sm">Check back later for availability</p>
</div>
)}
</div>
);
}

View File

@@ -1,26 +1,66 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
--background: #F8FAFC;
--foreground: #1E293B;
--accent: #065F46;
--accent-dark: #064e25;
--accent-light: #ecfdf5;
--secondary: #1D4ED8;
--secondary-light: #DBEAFE;
--biker: #1D4ED8;
--biker-light: #DBEAFE;
--shop: #DC2626;
--shop-light: #FEE2E2;
--investor: #7C3AED;
--investor-light: #EDE9FE;
--admin: #059669;
--admin-light: #D1FAE5;
--accountant: #0E7490;
--accountant-light: #CFFAFE;
--staff: #D97706;
--staff-light: #FEF3C7;
--manager: #EA580C;
--manager-light: #FFEDD5;
--merchant: #BE185D;
--merchant-light: #FCE7F3;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
--color-accent: var(--accent);
--color-accent-dark: var(--accent-dark);
--color-accent-light: var(--accent-light);
--color-secondary: var(--secondary);
--color-secondary-light: var(--secondary-light);
--color-biker: var(--biker);
--color-biker-light: var(--biker-light);
--color-shop: var(--shop);
--color-shop-light: var(--shop-light);
--color-investor: var(--investor);
--color-investor-light: var(--investor-light);
--color-admin: var(--admin);
--color-admin-light: var(--admin-light);
--color-accountant: var(--accountant);
--color-accountant-light: var(--accountant-light);
--color-staff: var(--staff);
--color-staff-light: var(--staff-light);
--color-manager: var(--manager);
--color-manager-light: var(--manager-light);
--color-merchant: var(--merchant);
--color-merchant-light: var(--merchant-light);
--font-sans: Inter, system-ui, sans-serif;
--radius-sm: 6px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-xl: 16px;
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
font-family: Inter, system-ui, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

76
src/app/investor/page.tsx Normal file
View File

@@ -0,0 +1,76 @@
import StatCard from '@/components/StatCard';
import TransactionList from '@/components/TransactionList';
import { investors, bikes, transactions } from '@/data/mockData';
import { Wallet, TrendingUp, Bike, Target, DollarSign, FileText, Phone, BarChart3, Send, ArrowDownToLine } from 'lucide-react';
export default function InvestorPage() {
const investor = investors[0];
const investorBikes = bikes.filter(b => b.investorId === investor?.id || b.status === 'rented');
return (
<div className="p-4 lg:p-6">
<div className="mb-6">
<h1 className="text-xl lg:text-2xl font-extrabold text-slate-800">Investor Dashboard</h1>
<p className="text-sm text-slate-500">Track your investments and earnings</p>
</div>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<StatCard label="Total Invested" value={`${(investor.totalInvested / 1000).toFixed(0)}k`} icon={Wallet} color="text-investor" />
<StatCard label="Total Earnings" value={`${(investor.totalEarnings / 1000).toFixed(1)}k`} icon={TrendingUp} color="text-green-600" trend="+2.3%" trendUp />
<StatCard label="Active Bikes" value={investor.activeBikes} icon={Bike} color="text-biker" />
<StatCard label="ROI" value={`${investor.roi}%`} icon={Target} color="text-purple-600" />
</div>
<div className="grid lg:grid-cols-2 gap-6 mb-6">
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<div className="flex items-center justify-between mb-4">
<h2 className="font-bold text-slate-800">My Portfolio</h2>
<span className="text-xs font-semibold px-2 py-1 bg-purple-100 text-purple-700 rounded-full">
{investorBikes.length} bikes
</span>
</div>
<div className="space-y-3">
{investorBikes.map(bike => (
<div key={bike.id} className="flex items-center justify-between p-3 bg-slate-50 rounded-lg">
<div>
<p className="font-medium text-slate-700">{bike.model}</p>
<p className="text-xs text-slate-400">{bike.plateNumber}</p>
</div>
<span className={`text-xs font-medium px-2 py-1 rounded-full ${
bike.status === 'rented' ? 'bg-green-100 text-green-700' :
bike.status === 'available' ? 'bg-blue-100 text-blue-700' :
'bg-amber-100 text-amber-700'
}`}>
{bike.status}
</span>
</div>
))}
</div>
</div>
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<h2 className="font-bold text-slate-800 mb-4">Quick Actions</h2>
<div className="grid grid-cols-2 gap-3">
<button className="py-3 bg-investor text-white rounded-lg font-semibold text-sm hover:bg-investor-dark flex items-center justify-center gap-2">
<DollarSign className="w-4 h-4" /> Withdraw
</button>
<button className="py-3 bg-purple-600 text-white rounded-lg font-semibold text-sm hover:bg-purple-700 flex items-center justify-center gap-2">
<FileText className="w-4 h-4" /> Statement
</button>
<button className="py-3 border border-slate-200 text-slate-600 rounded-lg font-semibold text-sm hover:bg-slate-50 flex items-center justify-center gap-2">
<Phone className="w-4 h-4" /> Support
</button>
<button className="py-3 border border-slate-200 text-slate-600 rounded-lg font-semibold text-sm hover:bg-slate-50 flex items-center justify-center gap-2">
<BarChart3 className="w-4 h-4" /> Analytics
</button>
</div>
</div>
</div>
<div>
<h2 className="text-lg font-bold text-slate-800 mb-3">Transaction History</h2>
<TransactionList transactions={transactions.filter(t => t.userId === 'u7')} />
</div>
</div>
);
}

View File

@@ -0,0 +1,98 @@
import { Wallet, TrendingUp, Bike, DollarSign, Calendar, Download, ArrowUpRight } from 'lucide-react';
import { investors, bikes } from '@/data/mockData';
const investor = investors[0];
const investorBikes = bikes.filter(b => b.investorId === investor?.id);
export default function InvestorPortfolioPage() {
return (
<div className="p-4 lg:p-6">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6">
<div>
<h1 className="text-2xl lg:text-3xl font-extrabold text-slate-800">My Portfolio</h1>
<p className="text-sm text-slate-500 mt-1">Track your investments</p>
</div>
<button className="py-2 px-4 bg-accent text-white rounded-lg text-sm font-semibold hover:bg-accent-dark flex items-center gap-2">
<Download className="w-4 h-4" /> Export Statement
</button>
</div>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<div className="flex items-center gap-3 mb-2">
<Wallet className="w-5 h-5 text-purple-600" />
<span className="text-sm text-slate-500">Invested</span>
</div>
<p className="text-2xl font-extrabold text-slate-800">{investor.totalInvested.toLocaleString()}</p>
</div>
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<div className="flex items-center gap-3 mb-2">
<TrendingUp className="w-5 h-5 text-green-600" />
<span className="text-sm text-slate-500">Earnings</span>
</div>
<p className="text-2xl font-extrabold text-green-600">{investor.totalEarnings.toLocaleString()}</p>
<span className="text-xs text-green-600 flex items-center gap-1 mt-1">
<ArrowUpRight className="w-3 h-3" /> +2.3%
</span>
</div>
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<div className="flex items-center gap-3 mb-2">
<Bike className="w-5 h-5 text-blue-600" />
<span className="text-sm text-slate-500">Active Bikes</span>
</div>
<p className="text-2xl font-extrabold text-slate-800">{investor.activeBikes}</p>
</div>
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<div className="flex items-center gap-3 mb-2">
<DollarSign className="w-5 h-5 text-amber-600" />
<span className="text-sm text-slate-500">ROI</span>
</div>
<p className="text-2xl font-extrabold text-slate-800">{investor.roi}%</p>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden">
<div className="px-5 py-4 border-b border-slate-100">
<h2 className="font-bold text-slate-800">My Bikes</h2>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-50">
<tr>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Bike</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Plate</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Location</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Status</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Earnings</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{investorBikes.map(bike => (
<tr key={bike.id} className="hover:bg-slate-50">
<td className="px-4 py-3">
<div className="flex items-center gap-3">
<Bike className="w-5 h-5 text-slate-400" />
<span className="text-sm font-medium text-slate-700">{bike.model}</span>
</div>
</td>
<td className="px-4 py-3 text-sm text-slate-600">{bike.plateNumber}</td>
<td className="px-4 py-3 text-sm text-slate-600">{bike.location}</td>
<td className="px-4 py-3">
<span className={`text-xs font-medium px-2.5 py-1 rounded-full ${
bike.status === 'rented' ? 'bg-green-100 text-green-700' :
bike.status === 'available' ? 'bg-blue-100 text-blue-700' :
'bg-amber-100 text-amber-700'
}`}>
{bike.status}
</span>
</td>
<td className="px-4 py-3 text-sm font-semibold text-green-600">2,500</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,123 @@
import { DollarSign, Banknote, Clock, Check, X, AlertCircle } from 'lucide-react';
import { investors } from '@/data/mockData';
const investor = investors[0];
const withdrawalHistory = [
{ id: 'w1', amount: 3000, method: 'bKash', status: 'pending', date: '2024-03-20' },
{ id: 'w2', amount: 5000, method: 'Bank Transfer', status: 'completed', date: '2024-03-15' },
{ id: 'w3', amount: 2500, method: 'bKash', status: 'completed', date: '2024-02-28' },
];
export default function WithdrawPage() {
return (
<div className="p-4 lg:p-6">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6">
<div>
<h1 className="text-2xl lg:text-3xl font-extrabold text-slate-800">Withdraw Earnings</h1>
<p className="text-sm text-slate-500 mt-1">Request withdrawals to your bank or bKash</p>
</div>
</div>
<div className="grid lg:grid-cols-3 gap-6 mb-8">
<div className="lg:col-span-2 bg-white rounded-xl shadow-sm border border-slate-100 p-6">
<h2 className="font-bold text-slate-800 mb-4">Request Withdrawal</h2>
<div className="mb-4">
<label className="text-sm font-medium text-slate-600 mb-2 block">Available Balance</label>
<p className="text-3xl font-extrabold text-green-600">{(investor.totalEarnings - investor.withdrawalPending).toLocaleString()}</p>
</div>
<div className="mb-4">
<label className="text-sm font-medium text-slate-600 mb-2 block">Amount</label>
<input
type="number"
placeholder="Enter amount"
className="w-full px-4 py-3 border border-slate-200 rounded-lg text-lg focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
/>
</div>
<div className="mb-4">
<label className="text-sm font-medium text-slate-600 mb-2 block">Withdrawal Method</label>
<div className="grid grid-cols-2 gap-3">
<button className="py-3 px-4 border-2 border-accent bg-accent-light rounded-lg font-medium text-accent">
bKash
</button>
<button className="py-3 px-4 border border-slate-200 rounded-lg font-medium text-slate-600 hover:bg-slate-50">
Bank Transfer
</button>
</div>
</div>
<div className="mb-4">
<label className="text-sm font-medium text-slate-600 mb-2 block">Account Number</label>
<input
type="text"
placeholder="01XXXXXXXXX"
className="w-full px-4 py-3 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
/>
</div>
<button className="w-full py-3 bg-accent text-white rounded-lg font-semibold hover:bg-accent-dark">
Request Withdrawal
</button>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-100 p-5">
<h2 className="font-bold text-slate-800 mb-4">Quick Withdraw</h2>
<div className="space-y-2 mb-4">
<button className="w-full py-2 px-4 border border-slate-200 rounded-lg text-sm text-left hover:bg-slate-50">
1,000
</button>
<button className="w-full py-2 px-4 border border-slate-200 rounded-lg text-sm text-left hover:bg-slate-50">
2,500
</button>
<button className="w-full py-2 px-4 border border-slate-200 rounded-lg text-sm text-left hover:bg-slate-50">
5,000
</button>
<button className="w-full py-2 px-4 border border-slate-200 rounded-lg text-sm text-left hover:bg-slate-50">
All Available
</button>
</div>
<div className="p-3 bg-blue-50 rounded-lg">
<p className="text-sm text-blue-700 flex items-center gap-2">
<AlertCircle className="w-4 h-4" />
Processing time: 1-3 business days
</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden">
<div className="px-5 py-4 border-b border-slate-100">
<h2 className="font-bold text-slate-800">Withdrawal History</h2>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-50">
<tr>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Date</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Method</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Amount</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{withdrawalHistory.map(w => (
<tr key={w.id} className="hover:bg-slate-50">
<td className="px-4 py-3 text-sm text-slate-600">{w.date}</td>
<td className="px-4 py-3 text-sm text-slate-600">{w.method}</td>
<td className="px-4 py-3 text-sm font-semibold text-slate-700">{w.amount}</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 ${
w.status === 'completed' ? 'bg-green-100 text-green-700' :
'bg-amber-100 text-amber-700'
}`}>
{w.status === 'completed' ? <Check className="w-3 h-3" /> : <Clock className="w-3 h-3" />}
{w.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
}

View File

@@ -1,20 +1,16 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Inter } from "next/font/google";
import "./globals.css";
import Sidebar from "@/components/Sidebar";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
const inter = Inter({
variable: "--font-inter",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "JAIBEN Mobility - EV Rental Platform",
description: "JAIBEN Mobility Ltd - EV Rental, Rent-to-Own, Share EV, FOCO Investor",
};
export default function RootLayout({
@@ -23,11 +19,13 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html
lang="en"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
>
<body className="min-h-full flex flex-col">{children}</body>
<html lang="en" className={inter.variable}>
<body className="min-h-screen bg-slate-50 antialiased">
<Sidebar />
<main className="lg:ml-64 min-h-screen">
{children}
</main>
</body>
</html>
);
}

View File

@@ -1,65 +1,80 @@
import Image from "next/image";
import Image from 'next/image';
import StatCard from '@/components/StatCard';
import BikeCard from '@/components/BikeCard';
import TransactionList from '@/components/TransactionList';
import { bikes, rentals, transactions, stats, users } from '@/data/mockData';
import { Bike, Wallet, Footprints, Clock, Zap, RotateCcw, CreditCard, FileText, Share2 } from 'lucide-react';
export default function Home() {
const activeRental = rentals.find(r => r.status === 'active');
const currentBike = bikes.find(b => b.id === activeRental?.bikeId);
const currentUser = users[0];
return (
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
<div className="p-4 lg:p-6">
<div className="mb-6">
<div className="flex items-center gap-4">
<div className="w-14 h-14 rounded-full bg-biker-light flex items-center justify-center text-2xl font-bold text-biker">
{currentUser.name.charAt(0)}
</div>
<div>
<h1 className="text-xl lg:text-2xl font-extrabold text-slate-800">Welcome back, {currentUser.name.split(' ')[0]}!</h1>
<p className="text-sm text-slate-500">Here's your rental status</p>
</div>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</div>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<StatCard label="Active Rental" value={stats.biker.activeRental} icon={Bike} color="text-biker" />
<StatCard label="Wallet Balance" value={`৳${stats.biker.walletBalance}`} icon={Wallet} color="text-green-600" />
<StatCard label="Total Rides" value={stats.biker.totalRides} icon={Footprints} color="text-purple-600" />
<StatCard label="Days Remaining" value={stats.biker.daysRemaining} icon={Clock} color="text-amber-600" />
</div>
{currentBike && (
<div className="mb-6">
<h2 className="text-lg font-bold text-slate-800 mb-3">Current Bike</h2>
<BikeCard bike={currentBike} />
</div>
</main>
)}
<div className="grid lg:grid-cols-2 gap-6 mb-6">
<div>
<h2 className="text-lg font-bold text-slate-800 mb-3">Quick Actions</h2>
<div className="grid grid-cols-2 gap-3">
<button className="py-3 bg-accent text-white rounded-lg font-semibold hover:bg-accent-dark transition-colors flex items-center justify-center gap-2">
<Zap className="w-4 h-4" /> Rent Bike
</button>
<button className="py-3 bg-biker text-white rounded-lg font-semibold hover:bg-biker-dark transition-colors flex items-center justify-center gap-2">
<RotateCcw className="w-4 h-4" /> Extend Rental
</button>
<button className="py-3 bg-green-600 text-white rounded-lg font-semibold hover:bg-green-700 transition-colors flex items-center justify-center gap-2">
<CreditCard className="w-4 h-4" /> Top Up
</button>
<button className="py-3 border-2 border-slate-200 text-slate-600 rounded-lg font-semibold hover:bg-slate-50 transition-colors flex items-center justify-center gap-2">
<FileText className="w-4 h-4" /> End Rental
</button>
</div>
</div>
<div>
<h2 className="text-lg font-bold text-slate-800 mb-3">Recent Transactions</h2>
<TransactionList transactions={transactions.filter(t => t.userId === 'u1')} compact />
</div>
</div>
<div className="bg-gradient-to-r from-accent to-accent-dark rounded-xl p-6 text-white">
<div className="flex items-center justify-between">
<div>
<p className="text-sm opacity-80">Refer a friend</p>
<h3 className="text-xl font-bold mt-1">Earn 500 Credit</h3>
<p className="text-xs opacity-80 mt-2">Share your referral link and earn free rides</p>
</div>
<button className="px-4 py-2 bg-white text-accent rounded-lg font-semibold text-sm flex items-center gap-2">
<Share2 className="w-4 h-4" /> Share Link
</button>
</div>
</div>
</div>
);
}

110
src/app/rent/page.tsx Normal file
View File

@@ -0,0 +1,110 @@
'use client';
import { useState } from 'react';
import Image from 'next/image';
import { availableBikes } from '@/data/mockData';
import { Check, Battery, MapPin, ArrowRight, Zap } from 'lucide-react';
export default function RentPage() {
const [selectedBike, setSelectedBike] = useState<string | null>(null);
const [rentalType, setRentalType] = useState<'single' | 'shared' | 'rent-to-own'>('single');
const plans = [
{ id: 'single', name: 'Standard Rental', price: 350, badge: 'demo=50tk', features: ['Single rider', 'Daily/Weekly/Monthly', 'Full insurance'] },
{ id: 'shared', name: 'Shared (2 Person)', price: 600, badge: 'demo=60tk', features: ['2 riders', 'Split payments', 'Shared deposit'] },
{ id: 'rent-to-own', name: 'Rent-to-Own', price: 450, badge: 'demo=45tk', features: ['Ownership potential', 'Flexible terms', 'Transfer ready'] },
];
return (
<div className="p-4 lg:p-6">
<div className="mb-6">
<h1 className="text-xl lg:text-2xl font-extrabold text-slate-800">Rent a Bike</h1>
<p className="text-sm text-slate-500">Choose your plan and bike</p>
</div>
<div className="mb-6">
<h2 className="text-lg font-bold text-slate-800 mb-3">Select Plan</h2>
<div className="grid grid-cols-3 gap-3">
{plans.map(plan => (
<button
key={plan.id}
onClick={() => setRentalType(plan.id as 'single' | 'shared' | 'rent-to-own')}
className={`p-4 rounded-xl border-2 text-left transition-all ${
rentalType === plan.id
? 'border-accent bg-accent-light shadow-md'
: 'border-slate-200 bg-white hover:border-accent/50'
}`}
>
<div className="flex items-center gap-2 mb-2">
<span className="text-sm font-bold text-slate-800">{plan.name}</span>
<span className="text-[10px] font-semibold px-1.5 py-0.5 bg-accent text-white rounded">{plan.badge}</span>
</div>
<p className="text-xl font-extrabold text-accent">{plan.price}<span className="text-xs font-normal text-slate-500">/day</span></p>
<div className="mt-3 space-y-1">
{plan.features.map((f, i) => (
<p key={i} className="text-xs text-slate-500 flex items-center gap-1">
<Check className="w-3 h-3 text-green-500" /> {f}
</p>
))}
</div>
</button>
))}
</div>
</div>
<div className="mb-6">
<h2 className="text-lg font-bold text-slate-800 mb-3">Available Bikes</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{availableBikes.map(bike => (
<div
key={bike.id}
onClick={() => setSelectedBike(bike.id)}
className={`bg-white rounded-xl overflow-hidden shadow-sm border-2 cursor-pointer transition-all hover:shadow-md ${
selectedBike === bike.id ? 'border-accent shadow-md' : 'border-slate-100'
}`}
>
<div className="h-32 bg-slate-100 relative">
<Image src={bike.image} alt={bike.model} fill className="object-cover" />
{selectedBike === bike.id && (
<div className="absolute top-2 right-2 w-6 h-6 bg-accent rounded-full flex items-center justify-center">
<Check className="w-4 h-4 text-white" />
</div>
)}
<div className="absolute bottom-2 left-2 bg-white/90 backdrop-blur-sm px-2 py-1 rounded-md flex items-center gap-1">
<Battery className="w-3 h-3 text-green-600" />
<p className="text-xs font-semibold text-slate-700">{bike.batteryLevel}%</p>
</div>
</div>
<div className="p-4">
<h3 className="font-bold text-slate-800">{bike.model}</h3>
<p className="text-sm text-slate-500">{bike.brand} {bike.plateNumber}</p>
<p className="text-xs text-slate-400 mt-1 flex items-center gap-1"><MapPin className="w-3 h-3" /> {bike.location}</p>
</div>
</div>
))}
</div>
</div>
<div className="fixed bottom-0 left-0 right-0 p-4 bg-white border-t border-slate-200 lg:relative">
<div className="flex items-center justify-between max-w-4xl mx-auto">
<div>
<p className="text-sm text-slate-500">Selected Bike</p>
<p className="font-bold text-slate-800">
{selectedBike ? availableBikes.find(b => b.id === selectedBike)?.model : 'None'}
</p>
</div>
<button
disabled={!selectedBike}
className={`px-6 py-3 rounded-lg font-bold text-white transition-colors flex items-center gap-2 ${
selectedBike
? 'bg-accent hover:bg-accent-dark'
: 'bg-slate-300 cursor-not-allowed'
}`}
>
Continue <ArrowRight className="w-4 h-4" />
</button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,104 @@
import { Package, Truck, MapPin, Clock, Check, Play, Phone } from 'lucide-react';
import { deliveries } from '@/data/mockData';
const statusColors: Record<string, string> = {
pending: 'bg-amber-100 text-amber-700',
in_progress: 'bg-blue-100 text-blue-700',
completed: 'bg-green-100 text-green-700',
};
export default function ShopDeliveriesPage() {
const pending = deliveries.filter(d => d.status === 'pending').length;
const inProgress = deliveries.filter(d => d.status === 'in_progress').length;
const completed = deliveries.filter(d => d.status === 'completed').length;
return (
<div className="p-4 lg:p-6">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6">
<div>
<h1 className="text-2xl lg:text-3xl font-extrabold text-slate-800">Deliveries</h1>
<p className="text-sm text-slate-500 mt-1">Track and manage deliveries</p>
</div>
</div>
<div className="grid grid-cols-3 gap-4 mb-6">
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-xl bg-amber-50 flex items-center justify-center">
<Clock className="w-6 h-6 text-amber-600" />
</div>
<div>
<p className="text-2xl font-extrabold text-slate-800">{pending}</p>
<p className="text-sm text-slate-500">Pending</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">
<Play className="w-6 h-6 text-blue-600" />
</div>
<div>
<p className="text-2xl font-extrabold text-slate-800">{inProgress}</p>
<p className="text-sm text-slate-500">In Progress</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-green-50 flex items-center justify-center">
<Check className="w-6 h-6 text-green-600" />
</div>
<div>
<p className="text-2xl font-extrabold text-slate-800">{completed}</p>
<p className="text-sm text-slate-500">Completed</p>
</div>
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden">
<div className="px-5 py-4 border-b border-slate-100">
<h2 className="font-bold text-slate-800">All Deliveries</h2>
</div>
<div className="divide-y divide-slate-50">
{deliveries.map(delivery => (
<div key={delivery.id} className="p-5 hover:bg-slate-50 transition-colors">
<div className="flex flex-col lg:flex-row lg:items-center gap-4">
<div className="flex items-center gap-4 lg:w-48">
<div className="w-10 h-10 rounded-full bg-slate-100 flex items-center justify-center">
<Truck className="w-5 h-5 text-slate-600" />
</div>
<div>
<p className="font-semibold text-slate-800">{delivery.id}</p>
<p className="text-xs text-slate-400">{delivery.createdAt}</p>
</div>
</div>
<div className="flex-1">
<div className="flex items-center gap-2 text-sm text-slate-600">
<MapPin className="w-4 h-4 text-green-500" />
<span>{delivery.pickupLocation}</span>
</div>
<div className="flex items-center gap-2 text-sm text-slate-400 mt-1">
<span></span>
<span>{delivery.dropoffLocation}</span>
</div>
</div>
<div className="flex items-center gap-3">
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${statusColors[delivery.status]}`}>
{delivery.status.replace('_', ' ')}
</span>
{delivery.status !== 'completed' && (
<button className="p-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
<Phone className="w-4 h-4" />
</button>
)}
</div>
</div>
</div>
))}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,93 @@
import { Bike, MapPin, Battery, User, MoreVertical } from 'lucide-react';
import { bikes } from '@/data/mockData';
const rentedBikes = bikes.filter(b => b.status === 'rented');
export default function ShopFleetPage() {
return (
<div className="p-4 lg:p-6">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6">
<div>
<h1 className="text-2xl lg:text-3xl font-extrabold text-slate-800">Fleet Status</h1>
<p className="text-sm text-slate-500 mt-1">View assigned bikes and their status</p>
</div>
</div>
<div className="grid grid-cols-3 gap-4 mb-6">
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<p className="text-2xl font-extrabold text-slate-800">{rentedBikes.length}</p>
<p className="text-sm text-slate-500">Rented Out</p>
</div>
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<p className="text-2xl font-extrabold text-slate-800">
{rentedBikes.filter(b => b.batteryLevel > 50).length}
</p>
<p className="text-sm text-slate-500">Good Battery</p>
</div>
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<p className="text-2xl font-extrabold text-slate-800">
{rentedBikes.filter(b => b.batteryLevel <= 50).length}
</p>
<p className="text-sm text-slate-500">Low Battery</p>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden">
<div className="px-5 py-4 border-b border-slate-100">
<h2 className="font-bold text-slate-800">Assigned Bikes</h2>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-50">
<tr>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Bike</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Plate</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Rider</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Location</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Battery</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{rentedBikes.map(bike => (
<tr key={bike.id} className="hover:bg-slate-50">
<td className="px-4 py-3">
<div className="flex items-center gap-3">
<Bike className="w-5 h-5 text-slate-400" />
<span className="text-sm font-medium text-slate-700">{bike.model}</span>
</div>
</td>
<td className="px-4 py-3 text-sm text-slate-600">{bike.plateNumber}</td>
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<User className="w-4 h-4 text-slate-400" />
<span className="text-sm text-slate-600">{bike.assignedTo}</span>
</div>
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-1 text-sm text-slate-600">
<MapPin className="w-3 h-3" /> {bike.location}
</div>
</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' : 'text-amber-600'}`} />
<span className={`text-sm font-medium ${bike.batteryLevel > 50 ? 'text-green-600' : 'text-amber-600'}`}>
{bike.batteryLevel}%
</span>
</div>
</td>
<td className="px-4 py-3">
<button className="p-2 hover:bg-slate-100 rounded-lg">
<MoreVertical className="w-4 h-4 text-slate-400" />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
}

110
src/app/shop/page.tsx Normal file
View File

@@ -0,0 +1,110 @@
import StatCard from '@/components/StatCard';
import { stats, deliveries, bikes } from '@/data/mockData';
import { Package, Bike, Play, CheckCircle, MapPin, Battery, Phone, Settings } from 'lucide-react';
export default function ShopPage() {
const activeBikes = bikes.filter(b => b.status === 'rented');
const pendingDeliveries = deliveries.filter(d => d.status === 'pending');
const inProgressDeliveries = deliveries.filter(d => d.status === 'in_progress');
const completedDeliveries = deliveries.filter(d => d.status === 'completed');
return (
<div className="p-4 lg:p-6">
<div className="mb-6">
<h1 className="text-xl lg:text-2xl font-extrabold text-slate-800">Shop Dashboard</h1>
<p className="text-sm text-slate-500">Manage deliveries and fleet</p>
</div>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<StatCard label="Today's Deliveries" value={stats.shop.todayDeliveries} icon={Package} color="text-shop" />
<StatCard label="Active Riders" value={stats.shop.activeRiders} icon={Bike} color="text-green-600" />
<StatCard label="In Progress" value={inProgressDeliveries.length} icon={Play} color="text-amber-600" />
<StatCard label="Completed Today" value={completedDeliveries.length} icon={CheckCircle} color="text-purple-600" />
</div>
<div className="grid lg:grid-cols-2 gap-6 mb-6">
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<div className="flex items-center justify-between mb-4">
<h2 className="font-bold text-slate-800">Active Deliveries</h2>
<span className="text-xs font-semibold px-2 py-1 bg-amber-100 text-amber-700 rounded-full">
{inProgressDeliveries.length + pendingDeliveries.length} active
</span>
</div>
<div className="space-y-3">
{deliveries.filter(d => d.status !== 'completed').map(delivery => (
<div key={delivery.id} className="p-3 bg-slate-50 rounded-lg">
<div className="flex items-center justify-between mb-2">
<span className="text-xs font-semibold text-slate-500">{delivery.id}</span>
<span className={`text-xs font-medium px-2 py-0.5 rounded-full ${
delivery.status === 'in_progress' ? 'bg-blue-100 text-blue-700' :
'bg-amber-100 text-amber-700'
}`}>
{delivery.status.replace('_', ' ')}
</span>
</div>
<p className="text-sm text-slate-700 flex items-center gap-1"><MapPin className="w-3 h-3" /> {delivery.pickupLocation}</p>
<p className="text-xs text-slate-400 flex items-center gap-1 ml-4"> {delivery.dropoffLocation}</p>
</div>
))}
</div>
</div>
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<h2 className="font-bold text-slate-800 mb-4">Fleet Status</h2>
<div className="space-y-3">
{activeBikes.map(bike => (
<div key={bike.id} className="flex items-center justify-between p-3 bg-slate-50 rounded-lg">
<div>
<p className="font-medium text-slate-700">{bike.model}</p>
<p className="text-xs text-slate-400">{bike.plateNumber}</p>
</div>
<div className="text-right">
<p className="text-xs font-medium text-green-600 flex items-center gap-1"><Battery className="w-3 h-3" /> {bike.batteryLevel}%</p>
<p className="text-xs text-slate-400 flex items-center gap-1"><MapPin className="w-3 h-3" /> {bike.location}</p>
</div>
</div>
))}
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden">
<div className="px-5 py-3 border-b border-slate-100">
<h2 className="font-bold text-slate-800">Delivery History</h2>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-50">
<tr>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Delivery ID</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Rider</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Pickup</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Dropoff</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{deliveries.map(delivery => (
<tr key={delivery.id} className="hover:bg-slate-50">
<td className="px-4 py-3 text-sm font-medium text-slate-700">{delivery.id}</td>
<td className="px-4 py-3 text-sm text-slate-600">{delivery.riderId}</td>
<td className="px-4 py-3 text-sm text-slate-600">{delivery.pickupLocation}</td>
<td className="px-4 py-3 text-sm text-slate-600">{delivery.dropoffLocation}</td>
<td className="px-4 py-3">
<span className={`text-xs font-medium px-2 py-1 rounded-full ${
delivery.status === 'completed' ? 'bg-green-100 text-green-700' :
delivery.status === 'in_progress' ? 'bg-blue-100 text-blue-700' :
'bg-amber-100 text-amber-700'
}`}>
{delivery.status.replace('_', ' ')}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,65 @@
import Image from 'next/image';
import { Bike } from '@/data/mockData';
interface BikeCardProps {
bike: Bike;
onRent?: (bikeId: string) => void;
compact?: boolean;
}
export default function BikeCard({ bike, onRent, compact }: BikeCardProps) {
const statusColors = {
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',
};
if (compact) {
return (
<div className="flex items-center gap-3 p-3 bg-white rounded-lg border border-slate-100 hover:border-accent/30 transition-colors">
<div className="w-12 h-12 rounded-lg bg-slate-100 overflow-hidden relative">
<Image src={bike.image} alt={bike.model} fill className="object-cover" />
</div>
<div className="flex-1 min-w-0">
<p className="font-semibold text-sm text-slate-800 truncate">{bike.model}</p>
<p className="text-xs text-slate-500">{bike.plateNumber}</p>
</div>
<span className={`text-xs font-medium px-2 py-0.5 rounded-full ${statusColors[bike.status]}`}>
{bike.status}
</span>
</div>
);
}
return (
<div className="bg-white rounded-xl overflow-hidden shadow-sm border border-slate-100 hover:shadow-md transition-shadow">
<div className="h-36 bg-slate-100 relative">
<Image src={bike.image} alt={bike.model} fill className="object-cover" />
<div className="absolute top-2 right-2">
<span className={`text-xs font-semibold px-2.5 py-1 rounded-full ${statusColors[bike.status]}`}>
{bike.status}
</span>
</div>
<div className="absolute bottom-2 left-2 bg-white/90 backdrop-blur-sm px-2 py-1 rounded-md">
<p className="text-xs font-semibold text-slate-700">🔋 {bike.batteryLevel}%</p>
</div>
</div>
<div className="p-4">
<h3 className="font-bold text-slate-800">{bike.model}</h3>
<p className="text-sm text-slate-500">{bike.brand} {bike.plateNumber}</p>
<div className="flex items-center gap-2 mt-2 text-xs text-slate-400">
<span>📍 {bike.location}</span>
</div>
{onRent && bike.status === 'available' && (
<button
onClick={() => onRent(bike.id)}
className="w-full mt-4 py-2.5 bg-accent text-white rounded-lg font-semibold text-sm hover:bg-accent-dark transition-colors"
>
Rent This Bike
</button>
)}
</div>
</div>
);
}

151
src/components/Sidebar.tsx Normal file
View File

@@ -0,0 +1,151 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useState } from 'react';
import {
Bike,
Settings,
Wallet,
Store,
Zap,
Battery,
Menu,
X,
Users,
FileText,
BarChart3,
CreditCard,
MapPin,
Shield,
Truck,
ChevronDown,
LogOut
} from 'lucide-react';
const adminNavItems = [
{ label: 'Dashboard', href: '/admin', icon: BarChart3 },
{ label: 'Bikers', href: '/admin/bikers', icon: Users },
{ label: 'Investors', href: '/admin/investors', icon: Wallet },
{ label: 'Fleet Management', href: '/admin/fleet', icon: Bike },
{ label: 'KYC Requests', href: '/admin/kyc', icon: Shield },
{ label: 'Rentals', href: '/admin/rentals', icon: FileText },
{ label: 'Revenue', href: '/admin/revenue', icon: CreditCard },
{ label: 'Reports', href: '/admin/reports', icon: BarChart3 },
{ label: 'Geofences', href: '/admin/geofence', icon: MapPin },
];
const bikerNavItems = [
{ label: 'Biker Dashboard', href: '/', icon: Bike },
{ label: 'Rent Bike', href: '/rent', icon: Zap },
{ label: 'Browse EVs', href: '/bikes', icon: Battery },
];
const investorNavItems = [
{ label: 'Dashboard', href: '/investor', icon: Wallet },
{ label: 'Portfolio', href: '/investor/portfolio', icon: BarChart3 },
{ label: 'Withdraw', href: '/investor/withdraw', icon: CreditCard },
];
const shopNavItems = [
{ label: 'Dashboard', href: '/shop', icon: Store },
{ label: 'Deliveries', href: '/shop/deliveries', icon: Truck },
{ label: 'Fleet', href: '/shop/fleet', icon: Bike },
];
export default function Sidebar() {
const pathname = usePathname();
const [mobileOpen, setMobileOpen] = useState(false);
const [expandedMenu, setExpandedMenu] = useState<string | null>(null);
const isAdmin = pathname.startsWith('/admin');
const isInvestor = pathname.startsWith('/investor');
const isShop = pathname.startsWith('/shop');
const navItems = isAdmin ? adminNavItems :
isInvestor ? investorNavItems :
isShop ? shopNavItems : bikerNavItems;
const toggleMenu = (label: string) => {
setExpandedMenu(expandedMenu === label ? null : label);
};
return (
<>
<button
onClick={() => setMobileOpen(!mobileOpen)}
className="lg:hidden fixed top-4 left-4 z-50 p-2 bg-white rounded-lg shadow-md"
>
{mobileOpen ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
</button>
<aside className={`
fixed left-0 top-0 h-screen w-64 bg-white border-r border-slate-200 shadow-sm z-40
transform transition-transform duration-300 ease-in-out
${mobileOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}
`}>
<div className="p-4 border-b border-slate-100">
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-extrabold text-accent">JAIBEN</h1>
<p className="text-xs text-slate-500">Mobility Ltd</p>
</div>
<div className="px-2 py-1 bg-accent-light rounded text-xs font-semibold text-accent">
{isAdmin ? 'Admin' : isInvestor ? 'Investor' : isShop ? 'Shop' : 'Biker'}
</div>
</div>
</div>
<nav className="p-3 space-y-1 overflow-y-auto h-[calc(100vh-140px)]">
{navItems.map((item) => {
const isActive = pathname === item.href || (item.href !== '/' && pathname.startsWith(item.href));
const Icon = item.icon;
return (
<Link
key={item.href}
href={item.href}
onClick={() => setMobileOpen(false)}
className={`
flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200
${isActive
? 'bg-accent text-white shadow-sm'
: 'text-slate-600 hover:bg-slate-50 hover:text-slate-900'
}
`}
>
<Icon className={`w-5 h-5 ${isActive ? 'text-white' : ''}`} />
<span>{item.label}</span>
</Link>
);
})}
</nav>
<div className="absolute bottom-0 left-0 right-0 p-3 border-t border-slate-100 bg-white">
<div className="flex items-center gap-3 px-3 py-2">
<div className="w-8 h-8 rounded-full bg-accent-light flex items-center justify-center">
<span className="text-sm font-bold text-accent">A</span>
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-slate-700 truncate">Admin User</p>
<p className="text-xs text-slate-400">admin@jaiben.com</p>
</div>
<button className="p-1.5 hover:bg-slate-100 rounded-lg">
<LogOut className="w-4 h-4 text-slate-400" />
</button>
</div>
<div className="mt-2 text-xs text-slate-400 text-center">
<p>Phase 1 - Core EV Rental</p>
<p className="mt-1">v1.0.0</p>
</div>
</div>
</aside>
{mobileOpen && (
<div
className="lg:hidden fixed inset-0 bg-black/30 z-30"
onClick={() => setMobileOpen(false)}
/>
)}
</>
);
}

View File

@@ -0,0 +1,33 @@
import { LucideIcon } from 'lucide-react';
interface StatCardProps {
label: string;
value: string | number;
icon: LucideIcon;
color?: string;
trend?: string;
trendUp?: boolean;
}
export default function StatCard({ label, value, icon: Icon, color = 'text-accent', trend, trendUp }: StatCardProps) {
const bgColor = color.replace('text-', 'bg-');
return (
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100 hover:shadow-md transition-shadow">
<div className="flex items-start justify-between">
<div className={`w-12 h-12 rounded-xl ${bgColor}/10 flex items-center justify-center`}>
<Icon className={`w-6 h-6 ${color}`} />
</div>
{trend && (
<span className={`text-xs font-semibold px-2 py-1 rounded-full ${trendUp ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}`}>
{trend}
</span>
)}
</div>
<div className="mt-4">
<p className="text-2xl font-extrabold text-slate-800">{value}</p>
<p className="text-sm text-slate-500 mt-1">{label}</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,88 @@
import { Transaction } from '@/data/mockData';
import { Bike, Banknote, Send, CreditCard, TrendingUp, ArrowUpDown } from 'lucide-react';
import { LucideIcon } from 'lucide-react';
interface TransactionListProps {
transactions: Transaction[];
compact?: boolean;
}
const typeIcons: Record<string, LucideIcon> = {
rental_payment: Bike,
deposit: Banknote,
withdrawal: Send,
topup: CreditCard,
earning: TrendingUp,
refund: ArrowUpDown,
};
const typeColors: Record<string, string> = {
rental_payment: 'text-blue-600',
deposit: 'text-green-600',
withdrawal: 'text-amber-600',
topup: 'text-blue-600',
earning: 'text-green-600',
refund: 'text-purple-600',
};
export default function TransactionList({ transactions, compact }: TransactionListProps) {
if (compact) {
return (
<div className="space-y-2">
{transactions.slice(0, 5).map((tx) => {
const Icon = typeIcons[tx.type] || Bike;
return (
<div key={tx.id} className="flex items-center justify-between py-2 border-b border-slate-50 last:border-0">
<div className="flex items-center gap-3">
<Icon className={`w-5 h-5 ${typeColors[tx.type]}`} />
<div>
<p className="text-sm font-medium text-slate-700">{tx.description}</p>
<p className="text-xs text-slate-400">{tx.createdAt}</p>
</div>
</div>
<span className={`font-semibold ${typeColors[tx.type]}`}>
{tx.type === 'withdrawal' ? '-' : '+'}{tx.amount.toLocaleString()}
</span>
</div>
);
})}
</div>
);
}
return (
<div className="bg-white rounded-xl overflow-hidden shadow-sm border border-slate-100">
<div className="px-5 py-3 border-b border-slate-100">
<h3 className="font-semibold text-slate-800">Recent Transactions</h3>
</div>
<div className="divide-y divide-slate-50">
{transactions.map((tx) => {
const Icon = typeIcons[tx.type] || Bike;
return (
<div key={tx.id} className="flex items-center justify-between px-5 py-3 hover:bg-slate-50 transition-colors">
<div className="flex items-center gap-3">
<Icon className={`w-5 h-5 ${typeColors[tx.type]}`} />
<div>
<p className="font-medium text-slate-700">{tx.description}</p>
<p className="text-xs text-slate-400">{tx.createdAt}</p>
</div>
</div>
<div className="text-right">
<p className={`font-bold ${typeColors[tx.type]}`}>
{tx.type === 'withdrawal' ? '-' : '+'}{tx.amount.toLocaleString()}
</p>
<span className={`text-xs px-2 py-0.5 rounded-full ${
tx.status === 'completed' ? 'bg-green-100 text-green-700' :
tx.status === 'pending' ? 'bg-amber-100 text-amber-700' :
'bg-red-100 text-red-700'
}`}>
{tx.status}
</span>
</div>
</div>
);
})}
</div>
</div>
);
}

433
src/data/mockData.ts Normal file
View File

@@ -0,0 +1,433 @@
export interface User {
id: string;
name: string;
email: string;
phone: string;
role: 'biker' | 'admin' | 'manager' | 'staff' | 'accountant' | 'investor' | 'shop' | 'merchant';
avatar?: string;
status: 'active' | 'pending' | 'inactive';
createdAt: string;
}
export 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;
purchasePrice?: number;
purchaseDate?: string;
currentRent?: number;
totalEarnings?: number;
}
export interface Rental {
id: string;
bikeId: string;
userId: string;
type: 'single' | 'shared' | 'rent-to-own';
status: 'active' | 'pending' | 'completed' | 'disputed';
startDate: string;
endDate?: string;
deposit: number;
dailyRate: number;
totalPaid: number;
}
export interface Transaction {
id: string;
userId?: string;
investorId?: string;
bikeId?: string;
type: 'rental_payment' | 'deposit' | 'withdrawal' | 'topup' | 'earning' | 'refund' | 'investment' | 'investment_return' | 'bike_earning' | 'referral_bonus' | 'penalty' | 'adjustment';
amount: number;
status: 'completed' | 'pending' | 'failed' | 'cancelled';
description: string;
paymentMethod?: 'bank' | 'mobile' | 'cash' | 'cheque';
referenceNumber?: string;
createdAt: string;
processedAt?: string;
}
export interface InvestmentPlan {
id: string;
investorId: string;
planName: string;
planType: 'silver' | 'gold' | 'platinum' | 'diamond';
bikeIds: string[];
totalInvestment: number;
monthlyReturn: number;
expectedRoi: number;
actualEarnings: number;
startDate: string;
endDate?: string;
status: 'active' | 'completed' | 'cancelled' | 'pending';
paymentMethod: 'bank' | 'mobile' | 'cash' | 'cheque';
transactionId?: string;
notes?: string;
createdAt: string;
}
export interface Investor {
id: string;
userId: string;
name: string;
email: string;
phone: string;
phoneAlt?: string;
address: string;
dateOfBirth?: string;
gender?: string;
occupation?: string;
nidNumber?: string;
nidFront?: string;
nidBack?: string;
passportNumber?: string;
tinNumber?: string;
bankName?: string;
bankAccountName?: string;
bankAccountNumber?: string;
bankBranch?: string;
bankRouting?: string;
mobileBanking?: string;
mobileBankingNumber?: string;
additionalMobileBanking?: { provider: string; number: string; verified: boolean }[];
emergencyContactName?: string;
emergencyContactRelation?: string;
emergencyContactPhone?: string;
totalInvested: number;
totalEarnings: number;
activeBikes: number;
withdrawalPending: number;
totalWithdrawn: number;
pendingEarnings: number;
roi: number;
status: 'active' | 'pending' | 'inactive' | 'suspended';
notes?: string;
createdAt: string;
updatedAt?: string;
kycStatus: 'verified' | 'pending' | 'rejected' | 'not_submitted';
kycSubmittedAt?: string;
kycDocuments?: { type: string; number?: string; url?: string; verified: boolean; uploadedAt?: string }[];
riskLevel: 'low' | 'medium' | 'high';
referralCode?: string;
referredBy?: string;
totalReferrals: number;
referralEarnings: number;
investments: InvestmentPlan[];
}
export interface KYCRequest {
id: string;
userId: string;
userName: string;
phone: string;
submittedAt: string;
status: 'pending' | 'approved' | 'rejected';
}
export interface Delivery {
id: string;
shopId: string;
riderId: string;
status: 'pending' | 'in_progress' | 'completed';
pickupLocation: string;
dropoffLocation: string;
createdAt: string;
}
export const users: User[] = [
{ id: 'u1', name: 'Rahim Ahmed', email: 'rahim@email.com', phone: '01712345678', role: 'biker', status: 'active', createdAt: '2024-01-15' },
{ id: 'u2', name: 'Karim Hasan', email: 'karim@email.com', phone: '01712345679', role: 'biker', status: 'active', createdAt: '2024-02-20' },
{ id: 'u3', name: 'Admin User', email: 'admin@jaiben.com', phone: '01710000001', role: 'admin', status: 'active', createdAt: '2023-06-01' },
{ id: 'u4', name: 'Manager User', email: 'manager@jaiben.com', phone: '01710000002', role: 'manager', status: 'active', createdAt: '2023-06-01' },
{ id: 'u5', name: 'Staff User', email: 'staff@jaiben.com', phone: '01710000003', role: 'staff', status: 'active', createdAt: '2023-07-01' },
{ id: 'u6', name: 'Accountant User', email: 'accountant@jaiben.com', phone: '01710000004', role: 'accountant', status: 'active', createdAt: '2023-07-01' },
{ id: 'u7', name: 'Investor User', email: 'investor@email.com', phone: '01720000001', role: 'investor', status: 'active', createdAt: '2023-08-01' },
{ id: 'u8', name: 'Shop Owner', email: 'shop@email.com', phone: '01730000001', role: 'shop', status: 'active', createdAt: '2023-09-01' },
{ id: 'u9', name: 'Merchant User', email: 'merchant@email.com', phone: '01740000001', role: 'merchant', status: 'active', createdAt: '2023-10-01' },
];
export const bikes: Bike[] = [
{ id: 'b1', model: 'Etron ET50', brand: 'Etron', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400', plateNumber: ' Dhaka Metro Cha-1234', status: 'rented', batteryLevel: 78, location: 'Gulshan 1', assignedTo: 'u1', investorId: 'inv1', purchasePrice: 85000, purchaseDate: '2024-01-15', currentRent: 350, totalEarnings: 14250 },
{ id: 'b2', model: 'Yadea DT3', brand: 'Yadea', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400', plateNumber: 'Dhaka Metro Cha-5678', status: 'available', batteryLevel: 95, location: 'Banani', investorId: 'inv1', purchasePrice: 65000, purchaseDate: '2024-01-20', currentRent: 0, totalEarnings: 12750 },
{ id: 'b3', model: 'AIMA Lightning', brand: 'AIMA', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400', plateNumber: 'Dhaka Metro Cha-9012', status: 'rented', batteryLevel: 62, location: 'Uttara', assignedTo: 'u2', investorId: 'inv2', purchasePrice: 95000, purchaseDate: '2023-11-05', currentRent: 450, totalEarnings: 22500 },
{ id: 'b4', model: 'TVS iQube', brand: 'TVS', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400', plateNumber: 'Dhaka Metro Cha-3456', status: 'maintenance', batteryLevel: 45, location: 'Workshop', investorId: 'inv2', purchasePrice: 72000, purchaseDate: '2023-12-01', currentRent: 0, totalEarnings: 8500 },
{ id: 'b5', model: 'Bajaj Chetak', brand: 'Bajaj', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400', plateNumber: 'Dhaka Metro Cha-7890', status: 'available', batteryLevel: 100, location: 'Dhanmondi', investorId: 'inv2', purchasePrice: 68000, purchaseDate: '2023-11-10', currentRent: 0, totalEarnings: 20000 },
{ id: 'b6', model: 'Hero Photon', brand: 'Hero', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400', plateNumber: 'Dhaka Metro Cha-1111', status: 'rented', batteryLevel: 88, location: 'Mirpur', investorId: 'inv2', purchasePrice: 55000, purchaseDate: '2024-02-01', currentRent: 300, totalEarnings: 4500 },
{ id: 'b7', model: 'Okinawa Praise', brand: 'Okinawa', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400', plateNumber: 'Dhaka Metro Cha-2222', status: 'available', batteryLevel: 92, location: 'Gulshan 2', investorId: 'inv3', purchasePrice: 75000, purchaseDate: '2024-02-10', currentRent: 0, totalEarnings: 9500 },
{ id: 'b8', model: 'Bajaj Chetak', brand: 'Bajaj', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400', plateNumber: 'Dhaka Metro Cha-3333', status: 'available', batteryLevel: 100, location: 'Dhanmondi', purchasePrice: 70000, purchaseDate: '2024-03-01', currentRent: 0, totalEarnings: 0 },
{ id: 'b9', model: 'TVS iQube', brand: 'TVS', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400', plateNumber: 'Dhaka Metro Cha-4444', status: 'rented', batteryLevel: 75, location: 'Banani', purchasePrice: 75000, purchaseDate: '2024-03-05', currentRent: 380, totalEarnings: 1140 },
{ id: 'b10', model: 'Yadea DT3', brand: 'Yadea', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400', plateNumber: 'Dhaka Metro Cha-5555', status: 'available', batteryLevel: 90, location: 'Uttara', purchasePrice: 68000, purchaseDate: '2024-03-10', currentRent: 0, totalEarnings: 0 },
];
export const rentals: Rental[] = [
{ id: 'r1', bikeId: 'b1', userId: 'u1', type: 'single', status: 'active', startDate: '2024-03-01', deposit: 5000, dailyRate: 350, totalPaid: 10500 },
{ id: 'r2', bikeId: 'b3', userId: 'u2', type: 'rent-to-own', status: 'active', startDate: '2024-02-15', deposit: 8000, dailyRate: 450, totalPaid: 18000 },
{ id: 'r3', bikeId: 'b2', userId: 'u1', type: 'single', status: 'completed', startDate: '2024-01-10', endDate: '2024-01-25', deposit: 5000, dailyRate: 350, totalPaid: 5250 },
];
export const transactions: Transaction[] = [
// Biker transactions
{ id: 't1', userId: 'u1', type: 'rental_payment', amount: 350, status: 'completed', description: 'Daily Rental - Mar 21', createdAt: '2024-03-21' },
{ id: 't2', userId: 'u1', type: 'rental_payment', amount: 350, status: 'completed', description: 'Daily Rental - Mar 20', createdAt: '2024-03-20' },
{ id: 't3', userId: 'u1', type: 'deposit', amount: 5000, status: 'completed', description: 'Security Deposit Paid', createdAt: '2024-03-01' },
{ id: 't4', userId: 'u1', type: 'topup', amount: 2000, status: 'completed', description: 'Wallet Top Up', createdAt: '2024-03-15' },
// Investor 1 (inv1) - Hasan Mahmud - Multiple investment transactions
{ id: 'invt1', investorId: 'inv1', type: 'investment', amount: 85000, status: 'completed', description: 'Investment - Etron ET50 (Gold Plan)', paymentMethod: 'bank', referenceNumber: 'INV/2024/001', createdAt: '2024-01-15', processedAt: '2024-01-15' },
{ id: 'invt2', investorId: 'inv1', type: 'investment', amount: 65000, status: 'completed', description: 'Investment - Yadea DT3 (Gold Plan)', paymentMethod: 'mobile', referenceNumber: 'INV/2024/002', createdAt: '2024-01-20', processedAt: '2024-01-20' },
{ id: 'invt3', investorId: 'inv1', type: 'bike_earning', bikeId: 'b1', amount: 2500, status: 'completed', description: 'Monthly Earnings - Bike b1 (Etron ET50)', createdAt: '2024-02-15', processedAt: '2024-02-15' },
{ id: 'invt4', investorId: 'inv1', type: 'bike_earning', bikeId: 'b2', amount: 2125, status: 'completed', description: 'Monthly Earnings - Bike b2 (Yadea DT3)', createdAt: '2024-02-15', processedAt: '2024-02-15' },
{ id: 'invt5', investorId: 'inv1', type: 'bike_earning', bikeId: 'b1', amount: 2500, status: 'completed', description: 'Monthly Earnings - Bike b1 (Etron ET50)', createdAt: '2024-03-15', processedAt: '2024-03-15' },
{ id: 'invt6', investorId: 'inv1', type: 'bike_earning', bikeId: 'b2', amount: 2125, status: 'completed', description: 'Monthly Earnings - Bike b2 (Yadea DT3)', createdAt: '2024-03-15', processedAt: '2024-03-15' },
{ id: 'invt7', investorId: 'inv1', type: 'withdrawal', amount: 15000, status: 'completed', description: 'Withdrawal via Bank Transfer', paymentMethod: 'bank', referenceNumber: 'WDL/2024/015', createdAt: '2024-02-20', processedAt: '2024-02-21' },
{ id: 'invt8', investorId: 'inv1', type: 'withdrawal', amount: 10000, status: 'completed', description: 'Withdrawal via Bkash', paymentMethod: 'mobile', referenceNumber: 'WDL/2024/022', createdAt: '2024-03-10', processedAt: '2024-03-10' },
{ id: 'invt9', investorId: 'inv1', type: 'withdrawal', amount: 20000, status: 'completed', description: 'Withdrawal via Bank Transfer', paymentMethod: 'bank', referenceNumber: 'WDL/2024/028', createdAt: '2024-03-18', processedAt: '2024-03-19' },
{ id: 'invt10', investorId: 'inv1', type: 'withdrawal', amount: 3000, status: 'pending', description: 'Withdrawal Request - Bkash', paymentMethod: 'mobile', referenceNumber: 'WDL/2024/035', createdAt: '2024-03-20' },
{ id: 'invt11', investorId: 'inv1', type: 'referral_bonus', amount: 1500, status: 'completed', description: 'Referral Bonus - New Investor Signup', createdAt: '2024-02-01', processedAt: '2024-02-01' },
{ id: 'invt12', investorId: 'inv1', type: 'referral_bonus', amount: 1000, status: 'completed', description: 'Referral Bonus - New Investor Signup', createdAt: '2024-03-05', processedAt: '2024-03-05' },
// Investor 2 (inv2) - Ahmed Khan - Platinum Plan
{ id: 'invt13', investorId: 'inv2', type: 'investment', amount: 95000, status: 'completed', description: 'Investment - AIMA Lightning (Platinum Plan)', paymentMethod: 'bank', referenceNumber: 'INV/2023/089', createdAt: '2023-11-05', processedAt: '2023-11-05' },
{ id: 'invt14', investorId: 'inv2', type: 'investment', amount: 72000, status: 'completed', description: 'Investment - TVS iQube (Platinum Plan)', paymentMethod: 'bank', referenceNumber: 'INV/2023/095', createdAt: '2023-12-01', processedAt: '2023-12-01' },
{ id: 'invt15', investorId: 'inv2', type: 'investment', amount: 68000, status: 'completed', description: 'Investment - Bajaj Chetak (Platinum Plan)', paymentMethod: 'mobile', referenceNumber: 'INV/2023/102', createdAt: '2023-11-10', processedAt: '2023-11-10' },
{ id: 'invt16', investorId: 'inv2', type: 'investment', amount: 55000, status: 'completed', description: 'Investment - Hero Photon (Platinum Plan)', paymentMethod: 'bank', referenceNumber: 'INV/2024/008', createdAt: '2024-02-01', processedAt: '2024-02-01' },
{ id: 'invt17', investorId: 'inv2', type: 'bike_earning', bikeId: 'b3', amount: 3750, status: 'completed', description: 'Monthly Earnings - Bike b3 (AIMA Lightning)', createdAt: '2023-12-15', processedAt: '2023-12-15' },
{ id: 'invt18', investorId: 'inv2', type: 'bike_earning', bikeId: 'b4', amount: 1415, status: 'completed', description: 'Monthly Earnings - Bike b4 (TVS iQube)', createdAt: '2023-12-15', processedAt: '2023-12-15' },
{ id: 'invt19', investorId: 'inv2', type: 'bike_earning', bikeId: 'b5', amount: 3335, status: 'completed', description: 'Monthly Earnings - Bike b5 (Bajaj Chetak)', createdAt: '2023-12-15', processedAt: '2023-12-15' },
{ id: 'invt20', investorId: 'inv2', type: 'bike_earning', bikeId: 'b3', amount: 3750, status: 'completed', description: 'Monthly Earnings - Bike b3 (AIMA Lightning)', createdAt: '2024-01-15', processedAt: '2024-01-15' },
{ id: 'invt21', investorId: 'inv2', type: 'bike_earning', bikeId: 'b4', amount: 1415, status: 'completed', description: 'Monthly Earnings - Bike b4 (TVS iQube)', createdAt: '2024-01-15', processedAt: '2024-01-15' },
{ id: 'invt22', investorId: 'inv2', type: 'bike_earning', bikeId: 'b5', amount: 3335, status: 'completed', description: 'Monthly Earnings - Bike b5 (Bajaj Chetak)', createdAt: '2024-01-15', processedAt: '2024-01-15' },
{ id: 'invt23', investorId: 'inv2', type: 'bike_earning', bikeId: 'b6', amount: 750, status: 'completed', description: 'Monthly Earnings - Bike b6 (Hero Photon)', createdAt: '2024-03-15', processedAt: '2024-03-15' },
{ id: 'invt24', investorId: 'inv2', type: 'bike_earning', bikeId: 'b3', amount: 3750, status: 'completed', description: 'Monthly Earnings - Bike b3 (AIMA Lightning)', createdAt: '2024-02-15', processedAt: '2024-02-15' },
{ id: 'invt25', investorId: 'inv2', type: 'bike_earning', bikeId: 'b4', amount: 1415, status: 'completed', description: 'Monthly Earnings - Bike b4 (TVS iQube)', createdAt: '2024-02-15', processedAt: '2024-02-15' },
{ id: 'invt26', investorId: 'inv2', type: 'bike_earning', bikeId: 'b5', amount: 3335, status: 'completed', description: 'Monthly Earnings - Bike b5 (Bajaj Chetak)', createdAt: '2024-02-15', processedAt: '2024-02-15' },
{ id: 'invt27', investorId: 'inv2', type: 'bike_earning', bikeId: 'b3', amount: 3750, status: 'completed', description: 'Monthly Earnings - Bike b3 (AIMA Lightning)', createdAt: '2024-03-15', processedAt: '2024-03-15' },
{ id: 'invt28', investorId: 'inv2', type: 'bike_earning', bikeId: 'b4', amount: 1415, status: 'completed', description: 'Monthly Earnings - Bike b4 (TVS iQube)', createdAt: '2024-03-15', processedAt: '2024-03-15' },
{ id: 'invt29', investorId: 'inv2', type: 'bike_earning', bikeId: 'b5', amount: 3335, status: 'completed', description: 'Monthly Earnings - Bike b5 (Bajaj Chetak)', createdAt: '2024-03-15', processedAt: '2024-03-15' },
{ id: 'invt30', investorId: 'inv2', type: 'withdrawal', amount: 25000, status: 'completed', description: 'Withdrawal via Bank Transfer', paymentMethod: 'bank', referenceNumber: 'WDL/2024/010', createdAt: '2024-01-20', processedAt: '2024-01-21' },
{ id: 'invt31', investorId: 'inv2', type: 'withdrawal', amount: 25000, status: 'completed', description: 'Withdrawal via Bank Transfer', paymentMethod: 'bank', referenceNumber: 'WDL/2024/018', createdAt: '2024-02-15', processedAt: '2024-02-16' },
{ id: 'invt32', investorId: 'inv2', type: 'withdrawal', amount: 25000, status: 'completed', description: 'Withdrawal via Nagad', paymentMethod: 'mobile', referenceNumber: 'WDL/2024/025', createdAt: '2024-03-12', processedAt: '2024-03-12' },
{ id: 'invt33', investorId: 'inv2', type: 'referral_bonus', amount: 2500, status: 'completed', description: 'Referral Bonus - Investor Referral Program', createdAt: '2024-01-10', processedAt: '2024-01-10' },
{ id: 'invt34', investorId: 'inv2', type: 'referral_bonus', amount: 5000, status: 'completed', description: 'Referral Bonus - Multiple Referrals Bonus', createdAt: '2024-02-20', processedAt: '2024-02-20' },
// Investor 3 (inv3) - Sadia Islam - Silver Plan
{ id: 'invt35', investorId: 'inv3', type: 'investment', amount: 75000, status: 'completed', description: 'Investment - Okinawa Praise (Silver Plan)', paymentMethod: 'bank', referenceNumber: 'INV/2024/012', createdAt: '2024-02-10', processedAt: '2024-02-10' },
{ id: 'invt36', investorId: 'inv3', type: 'bike_earning', bikeId: 'b7', amount: 1585, status: 'completed', description: 'Monthly Earnings - Bike b7 (Okinawa Praise)', createdAt: '2024-03-15', processedAt: '2024-03-15' },
{ id: 'invt37', investorId: 'inv3', type: 'investment_return', amount: 4500, status: 'completed', description: 'Investment Return - Q1 2024', paymentMethod: 'bank', referenceNumber: 'RET/2024/Q1/001', createdAt: '2024-03-31', processedAt: '2024-03-31' },
{ id: 'invt38', investorId: 'inv3', type: 'withdrawal', amount: 5000, status: 'pending', description: 'Withdrawal Request - Bkash', paymentMethod: 'mobile', referenceNumber: 'WDL/2024/032', createdAt: '2024-03-19' },
{ id: 'invt39', investorId: 'inv3', type: 'withdrawal', amount: 15000, status: 'completed', description: 'Withdrawal via Bank Transfer', paymentMethod: 'bank', referenceNumber: 'WDL/2024/008', createdAt: '2024-03-05', processedAt: '2024-03-06' },
// Adjustment transactions
{ id: 'adj1', investorId: 'inv1', type: 'adjustment', amount: -500, status: 'completed', description: 'Bike Maintenance Deduction', createdAt: '2024-02-10', processedAt: '2024-02-10' },
{ id: 'adj2', investorId: 'inv2', type: 'adjustment', amount: -300, status: 'completed', description: 'Service Charge Deduction', createdAt: '2024-01-20', processedAt: '2024-01-20' },
{ id: 'adj3', investorId: 'inv2', type: 'penalty', amount: -1000, status: 'completed', description: 'Late Payment Penalty', createdAt: '2024-02-25', processedAt: '2024-02-25' },
];
export const investors: Investor[] = [
{
id: 'inv1',
userId: 'u7',
name: 'Md. Hasan Mahmud',
email: 'hasan.mahmud@email.com',
phone: '01712345678',
phoneAlt: '01812345678',
address: 'House 12, Road 5, Dhanmondi, Dhaka 1205',
dateOfBirth: '1985-03-15',
gender: 'male',
occupation: 'Business',
nidNumber: '1234567890',
bankName: 'Islami Bank Bangladesh Ltd',
bankAccountName: 'Hasan Mahmud',
bankAccountNumber: '2050 1500 2345',
bankBranch: 'Dhanmondi Branch',
bankRouting: '140',
mobileBanking: 'Bkash',
mobileBankingNumber: '01712345678',
additionalMobileBanking: [
{ provider: 'Nagad', number: '01712345679', verified: true }
],
emergencyContactName: 'Fatema Begum',
emergencyContactRelation: 'Wife',
emergencyContactPhone: '01712345679',
totalInvested: 150000,
totalEarnings: 14250,
activeBikes: 2,
withdrawalPending: 3000,
totalWithdrawn: 45000,
pendingEarnings: 4625,
roi: 19,
status: 'active',
notes: 'Long-term investor, reliable payment history',
createdAt: '2024-01-15',
updatedAt: '2024-03-20',
kycStatus: 'verified',
kycSubmittedAt: '2024-01-10',
kycDocuments: [
{ type: 'nid', number: '1234567890', verified: true, uploadedAt: '2024-01-10' },
{ type: 'bank_statement', verified: true, uploadedAt: '2024-01-10' }
],
riskLevel: 'low',
referralCode: 'INV2024H1',
referredBy: undefined,
totalReferrals: 2,
referralEarnings: 2500,
investments: [
{ id: 'ip1', investorId: 'inv1', planName: 'Gold EV Fleet 2024', planType: 'gold', bikeIds: ['b1'], totalInvestment: 85000, monthlyReturn: 2500, expectedRoi: 18, actualEarnings: 10000, startDate: '2024-01-15', endDate: '2025-01-14', status: 'active', paymentMethod: 'bank', transactionId: 'invt1', createdAt: '2024-01-15' },
{ id: 'ip2', investorId: 'inv1', planName: 'Gold City Commuter', planType: 'gold', bikeIds: ['b2'], totalInvestment: 65000, monthlyReturn: 2125, expectedRoi: 18, actualEarnings: 4250, startDate: '2024-01-20', endDate: '2025-01-19', status: 'active', paymentMethod: 'mobile', transactionId: 'invt2', createdAt: '2024-01-20' }
]
},
{
id: 'inv2',
userId: 'u8',
name: 'Ahmed Khan',
email: 'ahmed.khan@email.com',
phone: '01722345678',
phoneAlt: '01922345678',
address: 'Flat 3B, House 45, Gulshan Avenue, Dhaka 1212',
dateOfBirth: '1978-07-22',
gender: 'male',
occupation: 'Real Estate',
nidNumber: '2345678901',
bankName: 'Standard Chartered Bank',
bankAccountName: 'Ahmed Khan',
bankAccountNumber: '2150 5678 9012',
bankBranch: 'Gulshan Branch',
bankRouting: '270',
mobileBanking: 'Nagad',
mobileBankingNumber: '01722345678',
additionalMobileBanking: [
{ provider: 'Bkash', number: '01722345679', verified: true },
{ provider: 'Rocket', number: '01722345680', verified: false }
],
emergencyContactName: 'Rahima Khan',
emergencyContactRelation: 'Wife',
emergencyContactPhone: '01722345679',
totalInvested: 290000,
totalEarnings: 36250,
activeBikes: 4,
withdrawalPending: 0,
totalWithdrawn: 75000,
pendingEarnings: 10840,
roi: 17,
status: 'active',
notes: 'Premium investor with multiple bike investments',
createdAt: '2023-11-01',
updatedAt: '2024-03-18',
kycStatus: 'verified',
kycSubmittedAt: '2023-10-25',
kycDocuments: [
{ type: 'nid', number: '2345678901', verified: true, uploadedAt: '2023-10-25' },
{ type: 'bank_statement', verified: true, uploadedAt: '2023-10-25' },
{ type: 'tin_certificate', number: '234567890', verified: true, uploadedAt: '2023-10-25' }
],
riskLevel: 'low',
referralCode: 'INV2024A2',
totalReferrals: 5,
referralEarnings: 7500,
investments: [
{ id: 'ip3', investorId: 'inv2', planName: 'Platinum Premium Fleet', planType: 'platinum', bikeIds: ['b3'], totalInvestment: 95000, monthlyReturn: 3750, expectedRoi: 20, actualEarnings: 15000, startDate: '2023-11-05', endDate: '2024-11-04', status: 'active', paymentMethod: 'bank', transactionId: 'invt13', createdAt: '2023-11-05' },
{ id: 'ip4', investorId: 'inv2', planName: 'Platinum Urban Ride', planType: 'platinum', bikeIds: ['b4'], totalInvestment: 72000, monthlyReturn: 1415, expectedRoi: 18, actualEarnings: 5660, startDate: '2023-12-01', endDate: '2024-11-30', status: 'active', paymentMethod: 'bank', transactionId: 'invt14', createdAt: '2023-12-01' },
{ id: 'ip5', investorId: 'inv2', planName: 'Platinum City Electric', planType: 'platinum', bikeIds: ['b5'], totalInvestment: 68000, monthlyReturn: 3335, expectedRoi: 20, actualEarnings: 13340, startDate: '2023-11-10', endDate: '2024-11-09', status: 'active', paymentMethod: 'mobile', transactionId: 'invt15', createdAt: '2023-11-10' },
{ id: 'ip6', investorId: 'inv2', planName: 'Platinum New Entry', planType: 'platinum', bikeIds: ['b6'], totalInvestment: 55000, monthlyReturn: 750, expectedRoi: 16, actualEarnings: 750, startDate: '2024-02-01', endDate: '2025-01-31', status: 'active', paymentMethod: 'bank', transactionId: 'invt16', createdAt: '2024-02-01' }
]
},
{
id: 'inv3',
userId: 'u9',
name: 'Sadia Islam',
email: 'sadia.islam@email.com',
phone: '01732345678',
phoneAlt: '01632345678',
address: 'Road 8, House 22, Baridhara, Dhaka 1212',
dateOfBirth: '1990-12-05',
gender: 'female',
occupation: 'Engineer',
nidNumber: '3456789012',
bankName: 'BRAC Bank',
bankAccountName: 'Sadia Islam',
bankAccountNumber: '1510 8901 2345',
bankBranch: 'Baridhara Branch',
bankRouting: '180',
mobileBanking: 'Rocket',
mobileBankingNumber: '01732345678',
additionalMobileBanking: [],
emergencyContactName: 'Abul Hasan',
emergencyContactRelation: 'Brother',
emergencyContactPhone: '01732345679',
totalInvested: 75000,
totalEarnings: 6085,
activeBikes: 1,
withdrawalPending: 5000,
totalWithdrawn: 15000,
pendingEarnings: 1585,
roi: 12,
status: 'pending',
notes: 'New investor, first investment',
createdAt: '2024-02-01',
updatedAt: '2024-03-15',
kycStatus: 'pending',
kycSubmittedAt: '2024-02-01',
kycDocuments: [
{ type: 'nid', number: '3456789012', verified: false, uploadedAt: '2024-02-01' }
],
riskLevel: 'medium',
referralCode: 'INV2024S3',
totalReferrals: 0,
referralEarnings: 0,
investments: [
{ id: 'ip7', investorId: 'inv3', planName: 'Silver Starter Pack', planType: 'silver', bikeIds: ['b7'], totalInvestment: 75000, monthlyReturn: 1585, expectedRoi: 12, actualEarnings: 6085, startDate: '2024-02-10', status: 'active', paymentMethod: 'bank', transactionId: 'invt35', createdAt: '2024-02-10' }
]
},
];
export const kycRequests: KYCRequest[] = [
{ id: 'kyc1', userId: 'u10', userName: 'New Biker Rahim', phone: '01712345670', submittedAt: '2024-03-20', status: 'pending' },
{ id: 'kyc2', userId: 'u11', userName: 'New Biker Karim', phone: '01712345671', submittedAt: '2024-03-19', status: 'pending' },
{ id: 'kyc3', userId: 'u12', userName: 'New Biker Jamal', phone: '01712345672', submittedAt: '2024-03-18', status: 'approved' },
];
export const deliveries: Delivery[] = [
{ id: 'd1', shopId: 'u8', riderId: 'u1', status: 'in_progress', pickupLocation: 'Gulshan 1', dropoffLocation: 'Banani 11', createdAt: '2024-03-21' },
{ id: 'd2', shopId: 'u8', riderId: 'u2', status: 'pending', pickupLocation: 'Gulshan 2', dropoffLocation: 'Uttara', createdAt: '2024-03-21' },
{ id: 'd3', shopId: 'u8', riderId: 'u1', status: 'completed', pickupLocation: 'Dhanmondi', dropoffLocation: 'Mirpur 1', createdAt: '2024-03-20' },
];
export const stats = {
biker: {
activeRental: 1,
walletBalance: 1250,
totalRides: 45,
daysRemaining: 14,
},
admin: {
totalBikers: 156,
activeRentals: 89,
dailyRevenue: 45600,
monthlyRevenue: 1250000,
fleetUtilization: 78,
},
investor: {
totalInvested: 150000,
totalEarnings: 28500,
activeBikes: 2,
},
shop: {
todayDeliveries: 23,
activeRiders: 8,
completedToday: 19,
},
};
export const availableBikes: Bike[] = [
{ id: 'b2', model: 'Yadea DT3', brand: 'Yadea', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400', plateNumber: 'Dhaka Metro Cha-5678', status: 'available', batteryLevel: 95, location: 'Banani' },
{ id: 'b5', model: 'Bajaj Chetak', brand: 'Bajaj', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400', plateNumber: 'Dhaka Metro Cha-7890', status: 'available', batteryLevel: 100, location: 'Dhanmondi' },
{ id: 'b6', model: 'Hero Photon', brand: 'Hero', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400', plateNumber: 'Dhaka Metro Cha-1111', status: 'available', batteryLevel: 88, location: 'Mirpur' },
{ id: 'b7', model: 'Okinawa Praise', brand: 'Okinawa', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400', plateNumber: 'Dhaka Metro Cha-2222', status: 'available', batteryLevel: 92, location: 'Gulshan 2' },
];