feat: add InvestorNotification component and integrate it across investor dashboard pages
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
ChevronLeft, ChevronRight, CheckCircle, XCircle, AlertCircle, Calendar
|
||||
} from 'lucide-react';
|
||||
import { investors, bikes, rentalPayments } from '@/data/mockData';
|
||||
import InvestorNotification from '@/components/InvestorNotification';
|
||||
|
||||
export default function RentalHistoryPage() {
|
||||
const investor = investors.find(i => i.id === 'inv1') || investors[0];
|
||||
@@ -64,311 +65,314 @@ export default function RentalHistoryPage() {
|
||||
};
|
||||
|
||||
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 className="min-h-screen">
|
||||
<InvestorNotification isMobile />
|
||||
<div className="pt-18 lg:pt-0 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>
|
||||
<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" />
|
||||
{/* 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>
|
||||
<p className="text-xs text-slate-500">Total Collected</p>
|
||||
<p className="text-lg font-bold text-green-600">৳{totalCollected.toLocaleString()}</p>
|
||||
</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>
|
||||
<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); }}
|
||||
{/* 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"
|
||||
/>
|
||||
<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>
|
||||
)}
|
||||
<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>
|
||||
</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>
|
||||
{/* 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>
|
||||
<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 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="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 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="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 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 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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user