feat: add rental history dashboard for investors and update navigation menu

This commit is contained in:
sazzadulalambd
2026-05-15 02:29:41 +06:00
parent 4909826c24
commit 16e08c930a
4 changed files with 406 additions and 27 deletions

View File

@@ -11,7 +11,7 @@ export default function InvestorDashboardPage() {
const availableBalance = investor.totalEarnings - investor.totalWithdrawn - investor.withdrawalPending;
return (
<div className="p-4 lg:p-6 max-w-6xl mx-auto mb-20 lg:mb-0">
<div className="p-4 lg:p-6 max-w-8xl mx-auto mb-20 lg:mb-0">
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4 mb-6">
<div>
<h1 className="text-xl lg:text-2xl font-extrabold text-slate-800">Welcome back, {investor.name.split(' ')[0]} 👋</h1>
@@ -110,10 +110,9 @@ export default function InvestorDashboardPage() {
<p className="text-[10px] text-slate-400 uppercase">Daily Rent</p>
</div>
<div>
<span className={`inline-flex px-2 py-1 rounded text-[10px] font-bold uppercase ${
bike.status === 'rented' ? 'bg-green-100 text-green-700' :
<span className={`inline-flex px-2 py-1 rounded text-[10px] font-bold uppercase ${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>
@@ -121,13 +120,13 @@ export default function InvestorDashboardPage() {
))}
</div>
) : (
<div className="h-full flex flex-col items-center justify-center text-center py-8">
<div className="w-12 h-12 bg-slate-50 rounded-full flex items-center justify-center mb-3">
<Bike className="w-6 h-6 text-slate-300" />
</div>
<p className="text-sm font-semibold text-slate-700">No bikes assigned yet</p>
<p className="text-xs text-slate-500 mt-1 max-w-[200px]">Once you make an investment, assigned bikes will appear here.</p>
</div>
<div className="h-full flex flex-col items-center justify-center text-center py-8">
<div className="w-12 h-12 bg-slate-50 rounded-full flex items-center justify-center mb-3">
<Bike className="w-6 h-6 text-slate-300" />
</div>
<p className="text-sm font-semibold text-slate-700">No bikes assigned yet</p>
<p className="text-xs text-slate-500 mt-1 max-w-[200px]">Once you make an investment, assigned bikes will appear here.</p>
</div>
)}
</div>
</div>

View File

@@ -0,0 +1,375 @@
'use client';
import { useState } from 'react';
import {
History, Bike, DollarSign, Clock, User, Download, Search,
ChevronLeft, ChevronRight, CheckCircle, XCircle, AlertCircle, Calendar
} from 'lucide-react';
import { investors, bikes, rentalPayments } from '@/data/mockData';
export default function RentalHistoryPage() {
const investor = investors.find(i => i.id === 'inv1') || investors[0];
const investorBikes = bikes.filter(b => b.investorId === investor.id);
const investorPayments = rentalPayments.filter(p => p.investorId === investor.id);
const [bikeFilter, setBikeFilter] = useState('all');
const [statusFilter, setStatusFilter] = useState('all');
const [sortBy, setSortBy] = useState<'date' | 'amount'>('date');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
const [page, setPage] = useState(1);
const [searchQuery, setSearchQuery] = useState('');
const [dateFrom, setDateFrom] = useState('');
const [dateTo, setDateTo] = useState('');
const pageSize = 10;
const filteredPayments = investorPayments.filter(p => {
if (bikeFilter !== 'all' && p.bikeId !== bikeFilter) return false;
if (statusFilter !== 'all' && p.status !== statusFilter) return false;
if (searchQuery && !p.bikerName.toLowerCase().includes(searchQuery.toLowerCase()) &&
!p.bikeModel.toLowerCase().includes(searchQuery.toLowerCase()) &&
!p.plateNumber.toLowerCase().includes(searchQuery.toLowerCase())) return false;
if (dateFrom && new Date(p.date) < new Date(dateFrom)) return false;
if (dateTo && new Date(p.date) > new Date(dateTo)) return false;
return true;
});
const sortedPayments = [...filteredPayments].sort((a, b) => {
if (sortBy === 'date') {
return sortOrder === 'asc'
? new Date(a.date).getTime() - new Date(b.date).getTime()
: new Date(b.date).getTime() - new Date(a.date).getTime();
} else {
return sortOrder === 'asc' ? a.amount - b.amount : b.amount - a.amount;
}
});
const totalPages = Math.ceil(sortedPayments.length / pageSize);
const paginatedPayments = sortedPayments.slice((page - 1) * pageSize, page * pageSize);
const totalCollected = investorPayments.filter(p => p.status === 'paid').reduce((sum, p) => sum + p.amount, 0);
const totalPending = investorPayments.filter(p => p.status === 'pending').reduce((sum, p) => sum + p.amount, 0);
const totalFailed = investorPayments.filter(p => p.status === 'failed').reduce((sum, p) => sum + p.amount, 0);
const activeRentals = new Set(investorPayments.filter(p => p.status === 'paid').map(p => p.bikeId)).size;
const statusConfig: Record<string, { label: string; bg: string; color: string; icon: any }> = {
paid: { label: 'Paid', bg: 'bg-green-100', color: 'text-green-700', icon: CheckCircle },
pending: { label: 'Pending', bg: 'bg-amber-100', color: 'text-amber-700', icon: Clock },
failed: { label: 'Failed', bg: 'bg-red-100', color: 'text-red-700', icon: XCircle },
};
const planConfig: Record<string, { label: string; bg: string; color: string }> = {
single: { label: 'Single Rent', bg: 'bg-green-100', color: 'text-green-700' },
'rent-to-own': { label: 'Rent to Own', bg: 'bg-blue-100', color: 'text-blue-700' },
share_ev: { label: 'Share EV', bg: 'bg-purple-100', color: 'text-purple-700' },
};
return (
<div className="p-4 sm:p-6 max-w-8xl mx-auto">
{/* Header */}
<div className="mb-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-xl sm:text-2xl font-bold text-slate-800 flex items-center gap-2">
<History className="w-5 h-5 sm:w-6 sm:h-6 text-investor" /> Rental History
</h1>
<p className="text-sm text-slate-500 mt-1">Track daily rental payments from your bikes</p>
</div>
<button className="px-4 py-2.5 bg-investor text-white rounded-lg text-sm font-medium hover:bg-investor-dark flex items-center gap-2 shadow-sm w-fit">
<Download className="w-4 h-4" /> Export
</button>
</div>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4 mb-6">
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center shrink-0">
<DollarSign className="w-5 h-5 text-green-600" />
</div>
<div>
<p className="text-xs text-slate-500">Total Collected</p>
<p className="text-lg font-bold text-green-600">{totalCollected.toLocaleString()}</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center shrink-0">
<Bike className="w-5 h-5 text-blue-600" />
</div>
<div>
<p className="text-xs text-slate-500">Active Rentals</p>
<p className="text-lg font-bold text-slate-800">{activeRentals}</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-amber-100 rounded-lg flex items-center justify-center shrink-0">
<Clock className="w-5 h-5 text-amber-600" />
</div>
<div>
<p className="text-xs text-slate-500">Pending</p>
<p className="text-lg font-bold text-amber-600">{totalPending.toLocaleString()}</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-red-100 rounded-lg flex items-center justify-center shrink-0">
<AlertCircle className="w-5 h-5 text-red-600" />
</div>
<div>
<p className="text-xs text-slate-500">Failed</p>
<p className="text-lg font-bold text-red-600">{totalFailed.toLocaleString()}</p>
</div>
</div>
</div>
</div>
{/* Main Table Card */}
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
{/* Filters */}
<div className="p-4 border-b border-slate-100">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-3">
<div className="flex flex-wrap items-center gap-2">
<div className="relative">
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
<input
type="text"
placeholder="Search biker, bike..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9 pr-4 py-2 border border-slate-200 rounded-lg text-sm w-48 sm:w-64"
/>
</div>
<select
value={bikeFilter}
onChange={(e) => { setBikeFilter(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"
>
<option value="all">All Bikes</option>
{investorBikes.map(bike => (
<option key={bike.id} value={bike.id}>{bike.model}</option>
))}
</select>
<select
value={statusFilter}
onChange={(e) => { setStatusFilter(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"
>
<option value="all">All Status</option>
<option value="paid">Paid</option>
<option value="pending">Pending</option>
<option value="failed">Failed</option>
</select>
</div>
<div className="flex flex-wrap items-center gap-2">
<div className="flex items-center gap-1 text-xs text-slate-500 font-medium">
<Calendar className="w-4 h-4" /> Date Range
</div>
<div className="flex items-center gap-1">
<input
type="date"
value={dateFrom}
onChange={(e) => { setDateFrom(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"
/>
<span className="text-slate-400">to</span>
<input
type="date"
value={dateTo}
onChange={(e) => { setDateTo(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"
/>
</div>
{(dateFrom || dateTo) && (
<button
onClick={() => { setDateFrom(''); setDateTo(''); setPage(1); }}
className="px-2 py-1 text-xs text-red-500 hover:bg-red-50 rounded"
>
Clear
</button>
)}
</div>
</div>
</div>
{/* Card View - Mobile/Tablet */}
<div className="lg:hidden divide-y divide-slate-100">
{paginatedPayments.length > 0 ? paginatedPayments.map((payment) => {
const status = statusConfig[payment.status] || statusConfig.pending;
const plan = planConfig[payment.planType] || planConfig.single;
const StatusIcon = status.icon;
return (
<div key={payment.id} className="p-4 hover:bg-slate-50">
<div className="flex items-start justify-between gap-3 mb-3">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<Bike className="w-4 h-4 text-slate-400 shrink-0" />
<p className="text-sm font-semibold text-slate-800 truncate">{payment.bikeModel}</p>
</div>
<p className="text-xs text-slate-400 ml-6">{payment.plateNumber}</p>
<div className="flex items-center gap-2 mt-1 ml-6">
<User className="w-3 h-3 text-slate-400" />
<p className="text-xs text-slate-600">{payment.bikerName}</p>
</div>
</div>
<div className="text-right shrink-0">
<p className="text-base font-bold text-slate-800">{payment.amount.toLocaleString()}</p>
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium mt-1 ${status.bg} ${status.color}`}>
<StatusIcon className="w-3 h-3" /> {status.label}
</span>
</div>
</div>
<div className="flex items-center justify-between ml-6 text-xs text-slate-400">
<span>{payment.date}</span>
<span className={`px-2 py-0.5 rounded ${plan.bg} ${plan.color}`}>{plan.label}</span>
</div>
</div>
);
}) : (
<div className="p-8 text-center text-slate-500">
<History className="w-10 h-10 mx-auto mb-2 text-slate-300" />
<p>No rental payments found</p>
</div>
)}
</div>
{/* Table View - Desktop */}
<div className="hidden lg:block overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-50">
<tr>
<th
onClick={() => {
if (sortBy === 'date') setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
else { setSortBy('date'); setSortOrder('desc'); }
}}
className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase cursor-pointer hover:bg-slate-100"
>
<div className="flex items-center gap-1">
Date {sortBy === 'date' && <span className="text-investor">{sortOrder === 'asc' ? '↑' : '↓'}</span>}
</div>
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Bike</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Biker</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Plan</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Duration</th>
<th
onClick={() => {
if (sortBy === 'amount') setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
else { setSortBy('amount'); setSortOrder('desc'); }
}}
className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase cursor-pointer hover:bg-slate-100"
>
<div className="flex items-center gap-1">
Amount {sortBy === 'amount' && <span className="text-investor">{sortOrder === 'asc' ? '↑' : '↓'}</span>}
</div>
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Method</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{paginatedPayments.length > 0 ? paginatedPayments.map((payment) => {
const status = statusConfig[payment.status] || statusConfig.pending;
const plan = planConfig[payment.planType] || planConfig.single;
const StatusIcon = status.icon;
return (
<tr key={payment.id} className="hover:bg-slate-50">
<td className="px-4 py-3">
<div>
<p className="text-sm font-medium text-slate-800">{payment.date}</p>
<p className="text-xs text-slate-400">{payment.transactionId || payment.id}</p>
</div>
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<Bike className="w-4 h-4 text-slate-400" />
<div>
<p className="text-sm font-medium text-slate-800">{payment.bikeModel}</p>
<p className="text-xs text-slate-400">{payment.plateNumber}</p>
</div>
</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-700">{payment.bikerName}</span>
</div>
</td>
<td className="px-4 py-3">
<span className={`px-2 py-1 rounded-full text-xs font-medium ${plan.bg} ${plan.color}`}>
{plan.label}
</span>
</td>
<td className="px-4 py-3">
<span className="text-sm text-slate-600">{payment.duration}</span>
</td>
<td className="px-4 py-3">
<p className="text-sm font-bold text-slate-800">{payment.amount.toLocaleString()}</p>
</td>
<td className="px-4 py-3">
<span className="text-sm text-slate-600 capitalize">{payment.paymentMethod}</span>
</td>
<td className="px-4 py-3">
<span className={`inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium ${status.bg} ${status.color}`}>
<StatusIcon className="w-3 h-3" /> {status.label}
</span>
</td>
</tr>
);
}) : (
<tr>
<td colSpan={8} className="px-4 py-12 text-center text-slate-500">
<History className="w-12 h-12 mx-auto mb-2 text-slate-300" />
<p>No rental payments found</p>
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Pagination */}
{sortedPayments.length > pageSize && (
<div className="p-4 border-t border-slate-100 flex flex-col sm:flex-row items-center justify-between gap-3">
<p className="text-xs sm:text-sm text-slate-500">
Showing {((page - 1) * pageSize) + 1} to {Math.min(page * pageSize, sortedPayments.length)} of {sortedPayments.length}
</p>
<div className="flex items-center gap-1">
<button
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page === 1}
className="p-2 border border-slate-200 rounded-lg disabled:opacity-50 hover:bg-slate-50"
>
<ChevronLeft className="w-4 h-4" />
</button>
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
const pageNum = totalPages <= 5 ? i + 1 : page <= 3 ? i + 1 : page >= totalPages - 2 ? totalPages - 4 + i : page - 2 + i;
return (
<button
key={pageNum}
onClick={() => setPage(pageNum)}
className={`w-8 h-8 rounded-lg text-sm font-medium ${page === pageNum ? 'bg-investor text-white' : 'border border-slate-200 hover:bg-slate-50'}`}
>
{pageNum}
</button>
);
})}
<button
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
disabled={page === totalPages}
className="p-2 border border-slate-200 rounded-lg disabled:opacity-50 hover:bg-slate-50"
>
<ChevronRight className="w-4 h-4" />
</button>
</div>
</div>
)}
</div>
</div>
);
}

View File

@@ -107,7 +107,7 @@ export default function InvestorWithdrawPage() {
};
return (
<div className="p-4 lg:p-6 max-w-7xl mx-auto">
<div className="p-4 lg:p-6 max-w-8xl mx-auto">
{/* Header */}
<div className="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>

View File

@@ -67,6 +67,7 @@ const bikerNavItems = [
const investorNavItems = [
{ label: 'Dashboard', href: '/investor', icon: BarChart3 },
{ label: 'My Investments', href: '/investor/plans', icon: Target },
{ label: 'Rental History', href: '/investor/rental-history', icon: FileText },
{ label: 'Withdraw', href: '/investor/withdraw', icon: CreditCard },
{ label: 'My Profile', href: '/investor/profile', icon: User },
];
@@ -106,7 +107,9 @@ export default function Sidebar() {
] : isInvestor ? [
{ label: 'Home', href: '/investor', icon: BarChart3 },
{ label: 'Investments', href: '/investor/plans', icon: Target },
{ label: 'History', href: '/investor/rental-history', icon: FileText },
{ label: 'Withdraw', href: '/investor/withdraw', icon: CreditCard },
{ label: 'Profile', href: '/investor/profile', icon: User },
] : isShop ? [
{ label: 'Home', href: '/shop', icon: Store },
{ label: 'Deliveries', href: '/shop/deliveries', icon: Truck },
@@ -194,8 +197,8 @@ export default function Sidebar() {
</div>
</aside>
{/* Bottom Navigation for Mobile */}
<nav className="lg:hidden fixed bottom-0 left-0 right-0 bg-white border-t border-slate-200 flex justify-around items-center h-16 z-30 pb-safe shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.05)]">
{/* Bottom Navigation for Mobile */}
<nav className="lg:hidden fixed bottom-0 left-0 right-0 bg-white border-t border-slate-200 flex items-center h-16 z-30 shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.05)]">
{bottomNavItems.map((item) => {
const isActive = pathname === item.href || (item.href !== '/' && pathname.startsWith(item.href));
const Icon = item.icon;
@@ -204,7 +207,7 @@ export default function Sidebar() {
key={item.href}
href={item.href}
className={`
flex flex-col items-center justify-center w-full h-full gap-1 transition-colors
flex flex-col items-center justify-center flex-1 h-full gap-1 transition-colors
${isActive ? 'text-accent' : 'text-slate-500 hover:text-slate-900'}
`}
>
@@ -213,13 +216,15 @@ export default function Sidebar() {
</Link>
);
})}
<button
onClick={() => setMobileOpen(true)}
className="flex flex-col items-center justify-center w-full h-full gap-1 text-slate-500 hover:text-slate-900 transition-colors"
>
<Menu className="w-5 h-5" />
<span className="text-[10px] font-medium">Menu</span>
</button>
{isAdmin && (
<button
onClick={() => setMobileOpen(true)}
className="flex flex-col items-center justify-center flex-1 h-full gap-1 text-slate-500 hover:text-slate-900 transition-colors"
>
<Menu className="w-5 h-5" />
<span className="text-[10px] font-medium">Menu</span>
</button>
)}
</nav>
{mobileOpen && (