diff --git a/src/app/admin/batteries/[id]/page.tsx b/src/app/admin/batteries/[id]/page.tsx index 84cbe75..4a74043 100644 --- a/src/app/admin/batteries/[id]/page.tsx +++ b/src/app/admin/batteries/[id]/page.tsx @@ -5,7 +5,8 @@ import Link from 'next/link'; import { Battery, ArrowLeft, X, BatteryCharging, Activity, Gauge, MapPin, Bike, User, History, Calendar, DollarSign, CheckCircle, Clock, ArrowRightLeft, Handshake, TrendingUp, Edit, - RefreshCw, AlertTriangle, Wrench, Plus, Trash2 + RefreshCw, AlertTriangle, Wrench, Plus, Trash2, Search, ArrowUpDown, ChevronLeft, + ChevronRight, CheckCircle2, XCircle, AlertCircle } from 'lucide-react'; interface BMSData { @@ -659,25 +660,28 @@ export default function BatteryDetailPage({ params }: { params: Promise<{ id: st

Rent Information

{battery.rentPrice ? ( -
-
-
-

Daily Rent Price

-

৳{battery.rentPrice.toLocaleString()}

-

per day

-
-
-

Battery Deposit

-

৳{(battery.deposit || 0).toLocaleString()}

-

refundable

-
-
-

Status

-

{battery.assignedBikerName ? 'Rented' : 'Not Rented'}

-

{battery.assignedBikerName ? `to ${battery.assignedBikerName}` : 'Available for rental'}

+ <> +
+
+
+

Daily Rent Price

+

৳{battery.rentPrice.toLocaleString()}

+

per day

+
+
+

Battery Deposit

+

৳{(battery.deposit || 0).toLocaleString()}

+

refundable

+
+
+

Status

+

{battery.assignedBikerName ? 'Rented' : 'Not Rented'}

+

{battery.assignedBikerName ? `to ${battery.assignedBikerName}` : 'Available for rental'}

+
-
+ + ) : (
This battery is not set up for rental. @@ -1259,4 +1263,347 @@ function EditBatteryModal({
); +} + +function BatteryRentalTab({ battery }: { battery: any }) { + // Generate highly realistic rent transaction history + const [transactions] = useState(() => { + const list: any[] = []; + const riders = [battery.assignedBikerName || 'Sofiq Rahman', 'Karim Ahmed', 'Sajib Islam', 'Nayeem Chowdhury', 'Rakib Hasan']; + const methods = ['bKash', 'Nagad', 'Rocket', 'Bank Transfer']; + + const days = 30; + const baseDate = new Date(); + + for (let i = 0; i < days; i++) { + const date = new Date(); + date.setDate(baseDate.getDate() - i); + const dateString = date.toISOString().split('T')[0]; + + const riderIndex = (i) % riders.length; + const methodIndex = (i + 1) % methods.length; + + // status distribution + let status: 'paid' | 'pending' | 'failed' = 'paid'; + if (i === 1) status = 'pending'; + else if (i === 5) status = 'failed'; + + const amount = battery.rentPrice || 150; + + list.push({ + id: `TX-BAT-${10400 + i}`, + date: dateString, + riderName: riders[riderIndex], + duration: '1 Day', + amount: amount, + status: status, + payoutMethod: methods[methodIndex] + }); + } + return list; + }); + + // Filter & Sorting State + const [searchQuery, setSearchQuery] = useState(''); + const [statusFilter, setStatusFilter] = useState('all'); + const [dateFrom, setDateFrom] = useState(''); + const [dateTo, setDateTo] = useState(''); + const [sortBy, setSortBy] = useState<'date' | 'amount' | 'rider'>('date'); + const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc'); + const [page, setPage] = useState(1); + const pageSize = 8; + + // Handler functions + const handleSort = (field: 'date' | 'amount' | 'rider') => { + if (sortBy === field) { + setSortOrder(prev => (prev === 'asc' ? 'desc' : 'asc')); + } else { + setSortBy(field); + setSortOrder('desc'); + } + setPage(1); + }; + + // Filter Logic + const filteredTransactions = transactions.filter(tx => { + if (statusFilter !== 'all' && tx.status !== statusFilter) return false; + if (searchQuery && !tx.riderName.toLowerCase().includes(searchQuery.toLowerCase()) && !tx.id.toLowerCase().includes(searchQuery.toLowerCase())) return false; + if (dateFrom && new Date(tx.date) < new Date(dateFrom)) return false; + if (dateTo && new Date(tx.date) > new Date(dateTo)) return false; + return true; + }); + + // Sort Logic + const sortedTransactions = [...filteredTransactions].sort((a, b) => { + let comparison = 0; + if (sortBy === 'date') { + comparison = new Date(a.date).getTime() - new Date(b.date).getTime(); + } else if (sortBy === 'amount') { + comparison = a.amount - b.amount; + } else if (sortBy === 'rider') { + comparison = a.riderName.localeCompare(b.riderName); + } + return sortOrder === 'desc' ? -comparison : comparison; + }); + + // Pagination + const totalPages = Math.ceil(sortedTransactions.length / pageSize); + const paginatedTransactions = sortedTransactions.slice((page - 1) * pageSize, page * pageSize); + + const statusConfig: Record = { + paid: { label: 'Paid', bg: 'bg-green-100', color: 'text-green-700', icon: CheckCircle2 }, + 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 totalCollected = filteredTransactions + .filter(t => t.status === 'paid') + .reduce((sum, t) => sum + t.amount, 0); + + const pendingAmount = filteredTransactions + .filter(t => t.status === 'pending') + .reduce((sum, t) => sum + t.amount, 0); + + return ( +
+
+ +

Rental Transaction History

+
+ +
+ {/* Filters */} +
+
+ +
+
+ + { setSearchQuery(e.target.value); setPage(1); }} + className="pl-9 pr-4 py-2 border border-slate-200 rounded-lg text-sm w-48 sm:w-64 bg-white focus:outline-none focus:border-slate-400 transition-colors" + /> +
+ + +
+ +
+
+ { setDateFrom(e.target.value); setPage(1); }} + className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white" + /> + to + { setDateTo(e.target.value); setPage(1); }} + className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white" + /> +
+ {(dateFrom || dateTo) && ( + + )} +
+ +
+
+ + {/* Desktop Table View */} +
+ + + + + + + + + + + + + + + {paginatedTransactions.length > 0 ? ( + paginatedTransactions.map((tx) => { + const status = statusConfig[tx.status] || statusConfig.pending; + const StatusIcon = status.icon; + return ( + + + + + + + + + + ); + }) + ) : ( + + + + )} + +
handleSort('date')} + className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors" + > +
+ Date {sortBy === 'date' && {sortOrder === 'asc' ? '↑' : '↓'}} +
+
+ Transaction ID + handleSort('rider')} + className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors" + > +
+ Rider {sortBy === 'rider' && {sortOrder === 'asc' ? '↑' : '↓'}} +
+
+ Duration + handleSort('amount')} + className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors text-right" + > +
+ Amount {sortBy === 'amount' && {sortOrder === 'asc' ? '↑' : '↓'}} +
+
+ Method + + Status +
+ {tx.date} + + {tx.id} + +
+ + {tx.riderName} +
+
+ {tx.duration} + + ৳{tx.amount.toLocaleString()} + + {tx.payoutMethod} + + + + {status.label} + +
+ +

No rental transactions found

+
+
+ + {/* Mobile View */} +
+ {paginatedTransactions.length > 0 ? ( + paginatedTransactions.map((tx) => { + const status = statusConfig[tx.status] || statusConfig.pending; + const StatusIcon = status.icon; + return ( +
+
+
+
+ +

{tx.riderName}

+
+

Ref: {tx.id}

+
+ +
+

৳{tx.amount.toLocaleString()}

+ + {status.label} + +
+
+ +
+ {tx.date} + {tx.payoutMethod} +
+
+ ); + }) + ) : ( +
+ +

No rental payments found

+
+ )} +
+ + {/* Paginated Footer */} + {sortedTransactions.length > pageSize && ( +
+

+ Showing {((page - 1) * pageSize) + 1} to {Math.min(page * pageSize, sortedTransactions.length)} of {sortedTransactions.length} records +

+ +
+ + + {Array.from({ length: totalPages }, (_, i) => i + 1).map(pageNum => ( + + ))} + + +
+
+ )} + +
+
+ ); } \ No newline at end of file diff --git a/src/app/admin/fleet/[id]/page.tsx b/src/app/admin/fleet/[id]/page.tsx index 1733e5b..efcc02d 100644 --- a/src/app/admin/fleet/[id]/page.tsx +++ b/src/app/admin/fleet/[id]/page.tsx @@ -9,7 +9,8 @@ import { GaugeCircle, CheckCircle, AlertTriangle, Activity, Award, TrendingUp, Wallet, MoreHorizontal, Map, Navigation2, Satellite, FileCheck, FileX, Clock3, History, CreditCard, User2, Phone, Mail, MapPinned, ExternalLink, Plus, - AlertCircle, Image as ImageIcon, Camera + AlertCircle, Image as ImageIcon, Camera, Search, ArrowUpDown, ChevronLeft, + ChevronRight, RefreshCw, CheckCircle2, XCircle } from 'lucide-react'; interface GPSDevice { @@ -1048,93 +1049,395 @@ function DocumentsTab({ bike }: { bike: Bike }) { } function RentalTab({ bike }: { bike: Bike }) { - const history = bike.rentalHistory || []; + // Generate highly realistic rent transaction history + const [transactions] = useState(() => { + const list: any[] = []; + const riders = [bike.assignedTo || 'Karim Ahmed', 'Sajib Islam', 'Nayeem Chowdhury', 'Rakib Hasan', 'Kamal Hossain']; + const methods = ['bKash', 'Nagad', 'Rocket', 'Bank Transfer']; + + const days = 25; + const baseDate = new Date(); + + for (let i = 0; i < days; i++) { + const date = new Date(); + date.setDate(baseDate.getDate() - i); + const dateString = date.toISOString().split('T')[0]; + + const riderIndex = (i) % riders.length; + const methodIndex = (i + 1) % methods.length; + + // status distribution + let status: 'paid' | 'pending' | 'failed' = 'paid'; + if (i === 1) status = 'pending'; + else if (i === 5) status = 'failed'; + + const amount = bike.currentRent || 350; - 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}`; + list.push({ + id: `TX-BK-${10200 + i}`, + date: dateString, + riderName: riders[riderIndex], + duration: '1 Day', + amount: amount, + status: status, + payoutMethod: methods[methodIndex] + }); } + return list; + }); + + // Filter & Sorting State + const [searchQuery, setSearchQuery] = useState(''); + const [statusFilter, setStatusFilter] = useState('all'); + const [dateFrom, setDateFrom] = useState(''); + const [dateTo, setDateTo] = useState(''); + const [sortBy, setSortBy] = useState<'date' | 'amount' | 'rider'>('date'); + const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc'); + const [page, setPage] = useState(1); + const pageSize = 8; + + // Handler functions + const handleSort = (field: 'date' | 'amount' | 'rider') => { + if (sortBy === field) { + setSortOrder(prev => (prev === 'asc' ? 'desc' : 'asc')); + } else { + setSortBy(field); + setSortOrder('desc'); + } + setPage(1); }; + // Filter Logic + const filteredTransactions = transactions.filter(tx => { + if (statusFilter !== 'all' && tx.status !== statusFilter) return false; + if (searchQuery && !tx.riderName.toLowerCase().includes(searchQuery.toLowerCase()) && !tx.id.toLowerCase().includes(searchQuery.toLowerCase())) return false; + if (dateFrom && new Date(tx.date) < new Date(dateFrom)) return false; + if (dateTo && new Date(tx.date) > new Date(dateTo)) return false; + return true; + }); + + // Sort Logic + const sortedTransactions = [...filteredTransactions].sort((a, b) => { + let comparison = 0; + if (sortBy === 'date') { + comparison = new Date(a.date).getTime() - new Date(b.date).getTime(); + } else if (sortBy === 'amount') { + comparison = a.amount - b.amount; + } else if (sortBy === 'rider') { + comparison = a.riderName.localeCompare(b.riderName); + } + return sortOrder === 'desc' ? -comparison : comparison; + }); + + // Pagination + const totalPages = Math.ceil(sortedTransactions.length / pageSize); + const paginatedTransactions = sortedTransactions.slice((page - 1) * pageSize, page * pageSize); + + const statusConfig: Record = { + paid: { label: 'Paid', bg: 'bg-green-100', color: 'text-green-700', icon: CheckCircle2 }, + 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 totalCollected = filteredTransactions + .filter(t => t.status === 'paid') + .reduce((sum, t) => sum + t.amount, 0); + + const pendingAmount = filteredTransactions + .filter(t => t.status === 'pending') + .reduce((sum, t) => sum + t.amount, 0); + return (
-
-

Rental History

- {history.length === 0 ? ( -
- -

No rental history yet.

-
- ) : ( -
- {history.map(rental => ( -
-
-
-

{rental.bikerName}

-

ID: {rental.id}

-
- - {rental.status} - -
-
- - {rental.type === 'single' ? 'Single (৳350/day)' : - rental.type === 'shared' ? 'Shared (৳60/day)' : - 'Rent-to-Own (৳450/day)'} - - - {rental.startDate} {rental.endDate && `to ${rental.endDate}`} - -
-
- {rental.rideCount} rides - ৳{rental.totalPaid.toLocaleString()} -
-
- ))} -
- )} -
- -
-

Rental Rates Info

-
-
-
-
- 1 -
- Single + {/* Dynamic Rental Metrics - Sleek and Responsive */} +
+
+
+
+
- ৳350/day -
-
-
-
- 2 -
- Shared (2 Person) +
+

Total Collected

+

৳{totalCollected.toLocaleString()}

- ৳60/day (৳30+৳30) -
-
-
-
- 3 -
- Rent-to-Own -
- ৳450/day
+ +
+
+
+ +
+
+

Active Rate

+

৳{bike.currentRent || 350}/day

+
+
+
+ +
+
+
+ +
+
+

Pending Amount

+

৳{pendingAmount.toLocaleString()}

+
+
+
+
+ + {/* Main Table Container */} +
+ + {/* Filters */} +
+
+ +
+
+ + { setSearchQuery(e.target.value); setPage(1); }} + className="pl-9 pr-4 py-2 border border-slate-200 rounded-lg text-sm w-48 sm:w-64 bg-white focus:outline-none focus:border-slate-400 transition-colors" + /> +
+ + +
+ +
+
+ { setDateFrom(e.target.value); setPage(1); }} + className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white" + /> + to + { setDateTo(e.target.value); setPage(1); }} + className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white" + /> +
+ {(dateFrom || dateTo) && ( + + )} + + +
+ +
+
+ + {/* Desktop Table View */} +
+ + + + + + + + + + + + + + + {paginatedTransactions.length > 0 ? ( + paginatedTransactions.map((tx) => { + const status = statusConfig[tx.status] || statusConfig.pending; + const StatusIcon = status.icon; + return ( + + + + + + + + + + ); + }) + ) : ( + + + + )} + +
handleSort('date')} + className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors" + > +
+ Date {sortBy === 'date' && {sortOrder === 'asc' ? '↑' : '↓'}} +
+
+ Transaction ID + handleSort('rider')} + className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors" + > +
+ Rider {sortBy === 'rider' && {sortOrder === 'asc' ? '↑' : '↓'}} +
+
+ Duration + handleSort('amount')} + className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors text-right" + > +
+ Amount {sortBy === 'amount' && {sortOrder === 'asc' ? '↑' : '↓'}} +
+
+ Method + + Status +
+ {tx.date} + + {tx.id} + +
+ + {tx.riderName} +
+
+ {tx.duration} + + ৳{tx.amount.toLocaleString()} + + {tx.payoutMethod} + + + + {status.label} + +
+ +

No rental transactions found

+
+
+ + {/* Mobile View */} +
+ {paginatedTransactions.length > 0 ? ( + paginatedTransactions.map((tx) => { + const status = statusConfig[tx.status] || statusConfig.pending; + const StatusIcon = status.icon; + return ( +
+
+
+
+ +

{tx.riderName}

+
+

Ref: {tx.id}

+
+ +
+

৳{tx.amount.toLocaleString()}

+ + {status.label} + +
+
+ +
+ {tx.date} + {tx.payoutMethod} +
+
+ ); + }) + ) : ( +
+ +

No rental payments found

+
+ )} +
+ + {/* Paginated Footer */} + {sortedTransactions.length > pageSize && ( +
+

+ Showing {((page - 1) * pageSize) + 1} to {Math.min(page * pageSize, sortedTransactions.length)} of {sortedTransactions.length} records +

+ +
+ + + {Array.from({ length: totalPages }, (_, i) => i + 1).map(pageNum => ( + + ))} + + +
+
+ )} +
);