feat: add mobile-responsive rental table view and interactive status filter cards with lock/unlock actions

This commit is contained in:
sazzadulalambd
2026-05-12 19:04:26 +06:00
parent 63f689d1da
commit 8e6eadfac5

View File

@@ -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 && (
@@ -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>
); );
} }