feat: add mobile-responsive rental table view and interactive status filter cards with lock/unlock actions
This commit is contained in:
@@ -272,6 +272,7 @@ export default function RentalsPage() {
|
|||||||
const [editPermission, setEditPermission] = useState(false);
|
const [editPermission, setEditPermission] = useState(false);
|
||||||
const [lockPermission, setLockPermission] = useState(false);
|
const [lockPermission, setLockPermission] = useState(false);
|
||||||
const [unlockPermission, setUnlockPermission] = useState(false);
|
const [unlockPermission, setUnlockPermission] = useState(false);
|
||||||
|
const [lockUnlockRental, setLockUnlockRental] = useState<{ id: string; action: 'lock' | 'unlock' } | null>(null);
|
||||||
const [planConditions, setPlanConditions] = useState<{
|
const [planConditions, setPlanConditions] = useState<{
|
||||||
single: { name: string; deposit: number; dailyRate: number; weeklyRate: number; monthlyRate: number; contractMonths: number[]; evModels: string[] }[];
|
single: { name: string; deposit: number; dailyRate: number; weeklyRate: number; monthlyRate: number; contractMonths: number[]; evModels: string[] }[];
|
||||||
shared: { name: string; deposit: number; dailyRate: number; weeklyRate: number; monthlyRate: number; contractMonths: number[]; evModels: string[] }[];
|
shared: { name: string; deposit: number; dailyRate: number; weeklyRate: number; monthlyRate: number; contractMonths: number[]; evModels: string[] }[];
|
||||||
@@ -503,38 +504,38 @@ export default function RentalsPage() {
|
|||||||
<h1 className="text-2xl lg:text-3xl font-extrabold text-slate-800">Rentals</h1>
|
<h1 className="text-2xl lg:text-3xl font-extrabold text-slate-800">Rentals</h1>
|
||||||
<p className="text-sm text-slate-500 mt-1">Manage rental transactions</p>
|
<p className="text-sm text-slate-500 mt-1">Manage rental transactions</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="grid grid-cols-2 gap-2 sm:flex sm:items-center sm:gap-2">
|
||||||
{createPermission && (
|
{createPermission && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowCreateModal(true)}
|
onClick={() => setShowCreateModal(true)}
|
||||||
className="py-2 px-4 bg-emerald-600 text-white rounded-lg text-sm font-semibold hover:bg-emerald-700 flex items-center gap-2"
|
className="py-2 px-4 bg-emerald-600 text-white rounded-lg text-sm font-semibold hover:bg-emerald-700 flex items-center justify-center gap-2"
|
||||||
>
|
>
|
||||||
<Plus className="w-4 h-4" /> New Rental
|
<Plus className="w-4 h-4" /> New Rental
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<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">
|
<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 justify-center gap-2">
|
||||||
<Download className="w-4 h-4" /> Export
|
<Download className="w-4 h-4" /> Export
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||||
<div className="bg-white rounded-xl p-4 shadow-sm border border-slate-100">
|
<button onClick={() => setStatusFilter(statusFilter === 'active' ? 'all' : 'active')} className={`text-left bg-white rounded-xl p-4 shadow-sm border transition-all ${statusFilter === 'active' ? 'border-emerald-500 ring-2 ring-emerald-100' : 'border-slate-100 hover:border-emerald-200'}`}>
|
||||||
<p className="text-2xl font-extrabold text-emerald-600">{stats.active}</p>
|
<p className="text-2xl font-extrabold text-emerald-600">{stats.active}</p>
|
||||||
<p className="text-sm text-slate-500">Active</p>
|
<p className="text-sm text-slate-500">Active Rentals</p>
|
||||||
</div>
|
</button>
|
||||||
<div className="bg-white rounded-xl p-4 shadow-sm border border-slate-100">
|
<button onClick={() => setStatusFilter(statusFilter === 'pending' ? 'all' : 'pending')} className={`text-left bg-white rounded-xl p-4 shadow-sm border transition-all ${statusFilter === 'pending' ? 'border-amber-500 ring-2 ring-amber-100' : 'border-slate-100 hover:border-amber-200'}`}>
|
||||||
<p className="text-2xl font-extrabold text-amber-600">{stats.pending}</p>
|
<p className="text-2xl font-extrabold text-amber-600">{stats.pending}</p>
|
||||||
<p className="text-sm text-slate-500">Pending</p>
|
<p className="text-sm text-slate-500">Payment Due (Day1)</p>
|
||||||
</div>
|
</button>
|
||||||
<div className="bg-white rounded-xl p-4 shadow-sm border border-slate-100">
|
<button onClick={() => setStatusFilter(statusFilter === 'overdue' ? 'all' : 'overdue')} className={`text-left bg-white rounded-xl p-4 shadow-sm border transition-all ${statusFilter === 'overdue' ? 'border-orange-500 ring-2 ring-orange-100' : 'border-slate-100 hover:border-orange-200'}`}>
|
||||||
<p className="text-2xl font-extrabold text-orange-600">{stats.overdue}</p>
|
<p className="text-2xl font-extrabold text-orange-600">{stats.overdue}</p>
|
||||||
<p className="text-sm text-slate-500">Payment Issues</p>
|
<p className="text-sm text-slate-500">Payment Due (Day2)</p>
|
||||||
</div>
|
</button>
|
||||||
<div className="bg-white rounded-xl p-4 shadow-sm border border-slate-100">
|
<button onClick={() => setStatusFilter(statusFilter === 'locked' ? 'all' : 'locked')} className={`text-left bg-white rounded-xl p-4 shadow-sm border transition-all ${statusFilter === 'locked' ? 'border-red-500 ring-2 ring-red-100' : 'border-slate-100 hover:border-red-200'}`}>
|
||||||
<p className="text-2xl font-extrabold text-red-600">{stats.locked}</p>
|
<p className="text-2xl font-extrabold text-red-600">{stats.locked}</p>
|
||||||
<p className="text-sm text-slate-500">Locked</p>
|
<p className="text-sm text-slate-500">EV Locked</p>
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col lg:flex-row gap-3 mb-4">
|
<div className="flex flex-col lg:flex-row gap-3 mb-4">
|
||||||
@@ -575,7 +576,7 @@ export default function RentalsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden">
|
<div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden">
|
||||||
<div className="overflow-x-auto">
|
<div className="hidden md:block overflow-x-auto">
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead className="bg-slate-50">
|
<thead className="bg-slate-50">
|
||||||
<tr>
|
<tr>
|
||||||
@@ -587,7 +588,6 @@ export default function RentalsPage() {
|
|||||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Hub</th>
|
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Hub</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">Deposit</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Rent Payment</th>
|
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Rent Payment</th>
|
||||||
{/* <th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Penalty</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">Status</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Actions</th>
|
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -596,7 +596,6 @@ export default function RentalsPage() {
|
|||||||
{filteredRentals.map(rental => {
|
{filteredRentals.map(rental => {
|
||||||
const statusBadge = getStatusBadge(rental.status);
|
const statusBadge = getStatusBadge(rental.status);
|
||||||
const paymentBadge = getPaymentStatusBadge(rental.paymentStatus, rental.pendingRent, rental.pendingRentDays);
|
const paymentBadge = getPaymentStatusBadge(rental.paymentStatus, rental.pendingRent, rental.pendingRentDays);
|
||||||
const penaltyBadge = getPenaltyBadge(rental.penaltyLevel);
|
|
||||||
const typeBadge = getTypeBadge(rental.type);
|
const typeBadge = getTypeBadge(rental.type);
|
||||||
return (
|
return (
|
||||||
<tr key={rental.id} className="hover:bg-slate-50 transition-colors">
|
<tr key={rental.id} className="hover:bg-slate-50 transition-colors">
|
||||||
@@ -671,14 +670,13 @@ export default function RentalsPage() {
|
|||||||
<Link href={`/admin/rentals/${rental.id}`} className="p-1.5 hover:bg-slate-100 rounded-lg" title="View">
|
<Link href={`/admin/rentals/${rental.id}`} className="p-1.5 hover:bg-slate-100 rounded-lg" title="View">
|
||||||
<Eye className="w-4 h-4 text-slate-500" />
|
<Eye className="w-4 h-4 text-slate-500" />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{lockPermission && rental.status === 'active' && (
|
{lockPermission && rental.status === 'active' && (
|
||||||
<button className="p-1.5 hover:bg-red-100 rounded-lg" title="Lock">
|
<button onClick={() => setLockUnlockRental({ id: rental.id, action: 'lock' })} className="p-1.5 hover:bg-red-100 rounded-lg" title="Lock">
|
||||||
<Lock className="w-4 h-4 text-red-500" />
|
<Lock className="w-4 h-4 text-red-500" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{unlockPermission && rental.status === 'locked' && (
|
{unlockPermission && rental.status === 'locked' && (
|
||||||
<button className="p-1.5 hover:bg-green-100 rounded-lg" title="Unlock">
|
<button onClick={() => setLockUnlockRental({ id: rental.id, action: 'unlock' })} className="p-1.5 hover:bg-green-100 rounded-lg" title="Unlock">
|
||||||
<Unlock className="w-4 h-4 text-green-500" />
|
<Unlock className="w-4 h-4 text-green-500" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@@ -696,6 +694,92 @@ export default function RentalsPage() {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="md:hidden p-3 space-y-3">
|
||||||
|
{filteredRentals.map(rental => {
|
||||||
|
const statusBadge = getStatusBadge(rental.status);
|
||||||
|
const paymentBadge = getPaymentStatusBadge(rental.paymentStatus, rental.pendingRent, rental.pendingRentDays);
|
||||||
|
const typeBadge = getTypeBadge(rental.type);
|
||||||
|
return (
|
||||||
|
<div key={rental.id} className="bg-slate-50 rounded-lg p-3 space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Link href={`/admin/rentals/${rental.id}`} className="text-sm font-medium text-emerald-600">
|
||||||
|
{rental.id}
|
||||||
|
</Link>
|
||||||
|
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2 py-0.5 rounded-full ${statusBadge.style}`}>
|
||||||
|
{statusBadge.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Bike className="w-4 h-4 text-slate-400" />
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm text-slate-700 font-medium truncate">{rental.bikeModel}</p>
|
||||||
|
<p className="text-xs text-slate-400">{rental.bikePlate}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<User className="w-4 h-4 text-slate-400" />
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm text-slate-700 truncate">{rental.userName}</p>
|
||||||
|
<p className="text-xs text-slate-400">{rental.userPhone}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2 py-0.5 rounded-full ${typeBadge.style}`}>
|
||||||
|
{typeBadge.label}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-slate-500 capitalize">{rental.subscriptionType}</span>
|
||||||
|
<span className="text-xs text-slate-400">•</span>
|
||||||
|
<span className="text-xs text-slate-500 truncate">{rental.hubName}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between pt-1 border-t border-slate-100">
|
||||||
|
<div>
|
||||||
|
<span className="text-xs text-slate-500">Deposit: </span>
|
||||||
|
<span className="text-sm font-medium text-slate-700">৳{rental.deposit.toLocaleString()}</span>
|
||||||
|
<span className={`ml-1.5 text-xs px-1 py-0.5 rounded ${rental.depositPaid ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}`}>
|
||||||
|
{rental.depositPaid ? 'Paid' : 'Unpaid'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{(paymentBadge.pendingRent && paymentBadge.pendingRent > 0) ? (
|
||||||
|
<span className="inline-flex items-center gap-1 text-xs font-medium px-2 py-0.5 rounded-full bg-amber-100 text-amber-700">
|
||||||
|
<AlertTriangle className="w-3 h-3" /> ৳{paymentBadge.pendingRent}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="inline-flex items-center gap-1 text-xs font-medium px-2 py-0.5 rounded-full bg-green-100 text-green-700">
|
||||||
|
<CheckCircle className="w-3 h-3" /> Clear
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1.5 pt-1">
|
||||||
|
<Link href={`/admin/rentals/${rental.id}`} className="flex-1 flex items-center justify-center gap-1 p-2 bg-white hover:bg-slate-100 rounded-lg text-xs font-medium text-slate-600">
|
||||||
|
<Eye className="w-3.5 h-3.5" /> View
|
||||||
|
</Link>
|
||||||
|
{lockPermission && rental.status === 'active' && (
|
||||||
|
<button onClick={() => setLockUnlockRental({ id: rental.id, action: 'lock' })} className="flex-1 flex items-center justify-center gap-1 p-2 bg-white hover:bg-red-50 rounded-lg text-xs font-medium text-red-600">
|
||||||
|
<Lock className="w-3.5 h-3.5" /> Lock
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{unlockPermission && rental.status === 'locked' && (
|
||||||
|
<button onClick={() => setLockUnlockRental({ id: rental.id, action: 'unlock' })} className="flex-1 flex items-center justify-center gap-1 p-2 bg-white hover:bg-green-50 rounded-lg text-xs font-medium text-green-600">
|
||||||
|
<Unlock className="w-3.5 h-3.5" /> Unlock
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<a href={`tel:${rental.userPhone}`} className="flex-1 flex items-center justify-center gap-1 p-2 bg-white hover:bg-green-50 rounded-lg text-xs font-medium text-green-600">
|
||||||
|
<Phone className="w-3.5 h-3.5" /> Call
|
||||||
|
</a>
|
||||||
|
<a href={`sms:${rental.userPhone}`} className="flex-1 flex items-center justify-center gap-1 p-2 bg-white hover:bg-blue-50 rounded-lg text-xs font-medium text-blue-600">
|
||||||
|
<MessageCircle className="w-3.5 h-3.5" /> SMS
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showCreateModal && (
|
{showCreateModal && (
|
||||||
@@ -916,7 +1000,7 @@ export default function RentalsPage() {
|
|||||||
{newRental.depositPaymentMethod === 'bank' && '1100 - Bank'}
|
{newRental.depositPaymentMethod === 'bank' && '1100 - Bank'}
|
||||||
{newRental.depositPaymentMethod === 'wallet' && '1200 - Biker Wallet'}
|
{newRental.depositPaymentMethod === 'wallet' && '1200 - Biker Wallet'}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2 text-right">{newRental.depositAmount > 0 ? newRental.depositAmount.toLocaleString() : selectedPlan?.deposit.toLocaleString()}</td>
|
<td className="px-3 py-2 text-right">{newRental.depositAmount > 0 ? newRental.depositAmount.toLocaleString() : selectedPlan?.deposit.toLocaleString()}</td>
|
||||||
<td className="px-3 py-2 text-right">-</td>
|
<td className="px-3 py-2 text-right">-</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr className="border-t border-slate-100">
|
<tr className="border-t border-slate-100">
|
||||||
@@ -982,6 +1066,43 @@ export default function RentalsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{lockUnlockRental && (
|
||||||
|
<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-sm">
|
||||||
|
<div className="p-6 text-center">
|
||||||
|
<div className={`w-16 h-16 mx-auto mb-4 rounded-full flex items-center justify-center ${lockUnlockRental.action === 'lock' ? 'bg-red-100' : 'bg-green-100'}`}>
|
||||||
|
{lockUnlockRental.action === 'lock' ? <Lock className="w-8 h-8 text-red-600" /> : <Unlock className="w-8 h-8 text-green-600" />}
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold text-slate-800 mb-2">
|
||||||
|
{lockUnlockRental.action === 'lock' ? 'Lock EV?' : 'Unlock EV?'}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-slate-500 mb-6">
|
||||||
|
{lockUnlockRental.action === 'lock'
|
||||||
|
? 'This will lock the EV and prevent the rider from starting it. Are you sure?'
|
||||||
|
: 'This will unlock the EV and allow the rider to start it. Are you sure?'}
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => setLockUnlockRental(null)}
|
||||||
|
className="flex-1 py-2.5 px-4 border border-slate-200 rounded-lg text-sm font-medium text-slate-600 hover:bg-slate-50"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setRentals(prev => prev.map(r => r.id === lockUnlockRental.id ? { ...r, status: lockUnlockRental.action === 'lock' ? 'locked' : 'active' } : r));
|
||||||
|
setLockUnlockRental(null);
|
||||||
|
}}
|
||||||
|
className={`flex-1 py-2.5 px-4 rounded-lg text-sm font-semibold text-white ${lockUnlockRental.action === 'lock' ? 'bg-red-600 hover:bg-red-700' : 'bg-green-600 hover:bg-green-700'}`}
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user