Files
JML/src/app/admin/rentals/page.tsx

993 lines
44 KiB
TypeScript
Raw Normal View History

'use client';
import { useState, useEffect } from 'react';
import {
FileText, Search, Filter, Bike, User, Calendar, DollarSign, Clock, MoreVertical,
Eye, Plus, Phone, MessageCircle, X, CreditCard, Wallet, Building, Download,
Printer, ChevronLeft, ChevronRight, CheckCircle, AlertTriangle
} from 'lucide-react';
import Link from 'next/link';
import {
canRentalCreate, canRentalCancel, canRentalEdit, canRentalLock, canRentalUnlock
} from '../../../lib/auth';
type RentalStatus = 'pending' | 'accepted' | 'active' | 'completed' | 'cancelled' | 'locked' | 'disputed';
type RentalType = 'single' | 'shared' | 'rent-to-own';
type PaymentMethod = 'cash' | 'bank' | 'wallet';
type PaymentStatus = 'paid' | 'partial' | 'overdue' | 'unpaid';
type PenaltyLevel = 'none' | 'day1' | 'day2' | 'day3';
interface BikeImage {
id: string;
url: string;
type: 'front' | 'back' | 'left' | 'right' | 'battery';
approved?: boolean;
}
interface Rental {
id: string;
bikeId: string;
userId: string;
userName: string;
userPhone: string;
bikeModel: string;
bikePlate: string;
bikeBattery: number;
type: RentalType;
status: RentalStatus;
startDate: string;
endDate?: string;
contractMonths?: number;
subscriptionType: 'daily' | 'weekly' | 'monthly';
deposit: number;
depositPaymentMethod: PaymentMethod;
depositPaid: boolean;
dailyRate: number;
weeklyRate: number;
monthlyRate: number;
totalPaid: number;
dueRental: number;
pendingRent?: number;
pendingRentDays?: number;
paymentStatus: PaymentStatus;
penaltyLevel: PenaltyLevel;
penaltyAmount: number;
lockedAt?: string;
lockedReason?: string;
hubId: string;
hubName: string;
initialImages?: BikeImage[];
imagesApproved: boolean;
bikerNote?: string;
rejectNote?: string;
createdAt: string;
acceptedAt?: string;
activatedAt?: string;
}
interface Bike {
id: string;
model: string;
plate: string;
battery: number;
status: 'available' | 'rented' | 'maintenance';
}
interface UserData {
id: string;
name: string;
phone: string;
kycStatus: 'approved' | 'pending' | 'rejected';
hasActiveRental: boolean;
}
const mockBikes: Bike[] = [
{ id: 'BIKE-001', model: 'AIMA Lightning', plate: 'Dhaka Metro Cha-9012', battery: 87, status: 'available' },
{ id: 'BIKE-002', model: 'Yadea DT3', plate: 'Dhaka Metro Ba-5521', battery: 65, status: 'available' },
{ id: 'BIKE-003', model: 'AIMA EM5', plate: 'Dhaka Metro Ko-1234', battery: 92, status: 'available' },
{ id: 'BIKE-005', model: 'Yadea G5', plate: 'Dhaka Metro Ha-5678', battery: 45, status: 'available' },
];
const mockUsers: UserData[] = [
{ id: 'USR-001', name: 'Rahim Ahmed', phone: '+8801712345678', kycStatus: 'approved', hasActiveRental: false },
{ id: 'USR-002', name: 'Karim Hasan', phone: '+8801812345678', kycStatus: 'approved', hasActiveRental: false },
{ id: 'USR-003', name: 'Jamal Uddin', phone: '+8801912345678', kycStatus: 'approved', hasActiveRental: true },
{ id: 'USR-004', name: 'Rafiq Islam', phone: '+8801512345678', kycStatus: 'pending', hasActiveRental: false },
{ id: 'USR-005', name: 'Farid Ahmed', phone: '+8801612345678', kycStatus: 'approved', hasActiveRental: false },
];
const mockHubs = [
{ id: 'HUB-001', name: 'Gulshan Hub' },
{ id: 'HUB-002', name: 'Banani Hub' },
{ id: 'HUB-003', name: 'Uttara Hub' },
{ id: 'HUB-004', name: 'Mirpur Hub' },
];
const rentalSettings = {
single: {
Premium: { deposit: 5000, contractMonths: [1, 3, 6, 12], dailyRate: 200, weeklyRate: 1200, monthlyRate: 5000 },
Standard: { deposit: 3000, contractMonths: [1, 3, 6, 12], dailyRate: 150, weeklyRate: 900, monthlyRate: 3500 },
Economy: { deposit: 2000, contractMonths: [1, 3, 6, 12], dailyRate: 100, weeklyRate: 600, monthlyRate: 2500 },
},
shared: {
Premium: { deposit: 4000, contractMonths: [1, 3, 6], dailyRate: 150, weeklyRate: 900, monthlyRate: 3500 },
Standard: { deposit: 2500, contractMonths: [1, 3, 6], dailyRate: 100, weeklyRate: 600, monthlyRate: 2200 },
Economy: { deposit: 2000, contractMonths: [1, 3, 6], dailyRate: 80, weeklyRate: 500, monthlyRate: 1800 },
},
'rent-to-own': {
Premium: { deposit: 15000, contractMonths: [12, 18, 24, 36], dailyRate: 350, weeklyRate: 2450, monthlyRate: 10500 },
Standard: { deposit: 12000, contractMonths: [12, 18, 24, 36], dailyRate: 250, weeklyRate: 1750, monthlyRate: 7000 },
Economy: { deposit: 10000, contractMonths: [12, 18, 24, 36], dailyRate: 200, weeklyRate: 1400, monthlyRate: 6000 },
},
};
const mockRentals: Rental[] = [
{
id: 'RNT-001',
bikeId: 'BIKE-001',
userId: 'USR-003',
userName: 'Jamal Uddin',
userPhone: '+8801912345678',
bikeModel: 'AIMA Lightning',
bikePlate: 'Dhaka Metro Cha-9012',
bikeBattery: 87,
type: 'single',
status: 'active',
startDate: '2024-01-15',
contractMonths: 12,
subscriptionType: 'monthly',
deposit: 3000,
depositPaymentMethod: 'cash',
depositPaid: true,
dailyRate: 150,
weeklyRate: 900,
monthlyRate: 3500,
totalPaid: 38500,
dueRental: 0,
pendingRent: 0,
pendingRentDays: 0,
paymentStatus: 'paid',
penaltyLevel: 'none',
penaltyAmount: 0,
hubId: 'HUB-001',
hubName: 'Gulshan Hub',
imagesApproved: true,
createdAt: '2024-01-15',
acceptedAt: '2024-01-15',
activatedAt: '2024-01-16',
},
{
id: 'RNT-002',
bikeId: 'BIKE-002',
userId: 'USR-002',
userName: 'Karim Hasan',
userPhone: '+8801812345678',
bikeModel: 'Yadea DT3',
bikePlate: 'Dhaka Metro Ba-5521',
bikeBattery: 65,
type: 'single',
status: 'pending',
startDate: '2024-02-10',
contractMonths: 3,
subscriptionType: 'monthly',
deposit: 3000,
depositPaymentMethod: 'bank',
depositPaid: true,
dailyRate: 150,
weeklyRate: 900,
monthlyRate: 3500,
totalPaid: 3000,
dueRental: 3500,
pendingRent: 3500,
pendingRentDays: 5,
paymentStatus: 'partial',
penaltyLevel: 'none',
penaltyAmount: 0,
hubId: 'HUB-002',
hubName: 'Banani Hub',
imagesApproved: false,
createdAt: '2024-02-10',
},
{
id: 'RNT-003',
bikeId: 'BIKE-003',
userId: 'USR-001',
userName: 'Rahim Ahmed',
userPhone: '+8801712345678',
bikeModel: 'AIMA EM5',
bikePlate: 'Dhaka Metro Ko-1234',
bikeBattery: 92,
type: 'rent-to-own',
status: 'completed',
startDate: '2023-06-01',
endDate: '2023-12-01',
contractMonths: 6,
subscriptionType: 'monthly',
deposit: 10000,
depositPaymentMethod: 'wallet',
depositPaid: true,
dailyRate: 500,
weeklyRate: 3000,
monthlyRate: 12000,
totalPaid: 82000,
dueRental: 0,
pendingRent: 0,
pendingRentDays: 0,
paymentStatus: 'paid',
penaltyLevel: 'none',
penaltyAmount: 0,
hubId: 'HUB-001',
hubName: 'Gulshan Hub',
imagesApproved: true,
createdAt: '2023-06-01',
acceptedAt: '2023-06-01',
activatedAt: '2023-06-02',
},
{
id: 'RNT-004',
bikeId: 'BIKE-005',
userId: 'USR-005',
userName: 'Farid Ahmed',
userPhone: '+8801612345678',
bikeModel: 'Yadea G5',
bikePlate: 'Dhaka Metro Ha-5678',
bikeBattery: 45,
type: 'shared',
status: 'locked',
startDate: '2024-01-20',
contractMonths: 1,
subscriptionType: 'weekly',
deposit: 2000,
depositPaymentMethod: 'cash',
depositPaid: true,
dailyRate: 100,
weeklyRate: 600,
monthlyRate: 2200,
totalPaid: 2600,
dueRental: 600,
pendingRent: 600,
pendingRentDays: 3,
paymentStatus: 'overdue',
penaltyLevel: 'day3',
penaltyAmount: 1000,
lockedAt: '2024-02-05',
lockedReason: 'Payment overdue - bike locked',
hubId: 'HUB-003',
hubName: 'Uttara Hub',
imagesApproved: true,
createdAt: '2024-01-20',
acceptedAt: '2024-01-20',
activatedAt: '2024-01-21',
},
];
export default function RentalsPage() {
const [rentals, setRentals] = useState<Rental[]>(mockRentals);
const [showCreateModal, setShowCreateModal] = useState(false);
const [statusFilter, setStatusFilter] = useState<string>('all');
const [searchQuery, setSearchQuery] = useState('');
const [hubFilter, setHubFilter] = useState<string>('all');
const [createPermission, setCreatePermission] = useState(false);
const [cancelPermission, setCancelPermission] = useState(false);
const [editPermission, setEditPermission] = useState(false);
const [lockPermission, setLockPermission] = useState(false);
const [unlockPermission, setUnlockPermission] = useState(false);
const [planConditions, setPlanConditions] = useState<{
single: { name: string; deposit: number; dailyRate: number; weeklyRate: number; monthlyRate: number; contractMonths: number[] }[];
shared: { name: string; deposit: number; dailyRate: number; weeklyRate: number; monthlyRate: number; contractMonths: number[] }[];
'rent-to-own': { name: string; deposit: number; dailyRate: number; weeklyRate: number; monthlyRate: number; contractMonths: number[] }[];
}>({
single: [
{ name: 'Single Rent - Premium', deposit: 25000, dailyRate: 400, weeklyRate: 2800, monthlyRate: 12000, contractMonths: [1, 3, 6, 12] },
{ name: 'Single Rent - Standard', deposit: 20000, dailyRate: 300, weeklyRate: 2100, monthlyRate: 9000, contractMonths: [1, 3, 6, 12] },
{ name: 'Single Rent - Economy', deposit: 15000, dailyRate: 250, weeklyRate: 1750, monthlyRate: 7500, contractMonths: [1, 3, 6, 12] },
],
shared: [
{ name: 'Share an EV - Premium', deposit: 20000, dailyRate: 300, weeklyRate: 2100, monthlyRate: 8400, contractMonths: [1, 3, 6, 12] },
{ name: 'Share an EV - Standard', deposit: 15000, dailyRate: 200, weeklyRate: 1400, monthlyRate: 5600, contractMonths: [1, 3, 6, 12] },
{ name: 'Share an EV - Economy', deposit: 12000, dailyRate: 150, weeklyRate: 1050, monthlyRate: 4200, contractMonths: [1, 3, 6, 12] },
],
'rent-to-own': [
{ name: 'Rent to Own - Premium', deposit: 25000, dailyRate: 350, weeklyRate: 2450, monthlyRate: 10500, contractMonths: [12, 18, 24, 36] },
{ name: 'Rent to Own - Standard', deposit: 18000, dailyRate: 250, weeklyRate: 1750, monthlyRate: 7000, contractMonths: [12, 18, 24, 36] },
{ name: 'Rent to Own - Economy', deposit: 15000, dailyRate: 200, weeklyRate: 1400, monthlyRate: 6000, contractMonths: [12, 18, 24, 36] },
],
});
const [createStep, setCreateStep] = useState(1);
const [newRental, setNewRental] = useState<{
userId: string;
type: RentalType;
planConditionName: string;
subscriptionType: 'daily' | 'weekly' | 'monthly';
contractMonths: number;
bikeId: string;
startDate: string;
hubId: string;
depositPaymentMethod: PaymentMethod;
}>({
userId: '',
type: 'single',
planConditionName: '',
subscriptionType: 'daily',
contractMonths: 0,
bikeId: '',
startDate: new Date().toISOString().split('T')[0],
hubId: '',
depositPaymentMethod: 'cash',
});
const [showJournalPreview, setShowJournalPreview] = useState(false);
useEffect(() => {
setCreatePermission(canRentalCreate());
setCancelPermission(canRentalCancel());
setEditPermission(canRentalEdit());
setLockPermission(canRentalLock());
setUnlockPermission(canRentalUnlock());
}, []);
const getStatusBadge = (status: RentalStatus) => {
const styles: Record<RentalStatus, string> = {
active: 'bg-green-100 text-green-700',
pending: 'bg-amber-100 text-amber-700',
accepted: 'bg-blue-100 text-blue-700',
completed: 'bg-indigo-100 text-indigo-700',
cancelled: 'bg-slate-100 text-slate-600',
locked: 'bg-red-100 text-red-700',
disputed: 'bg-orange-100 text-orange-700',
};
const labels: Record<RentalStatus, string> = {
active: 'Active',
pending: 'Pending',
accepted: 'Accepted',
completed: 'Completed',
cancelled: 'Cancelled',
locked: 'Locked',
disputed: 'Disputed',
};
return { style: styles[status], label: labels[status] || status };
};
const getPaymentStatusBadge = (status: PaymentStatus, pendingRent?: number, pendingRentDays?: number) => {
const styles: Record<PaymentStatus, string> = {
paid: 'bg-green-100 text-green-700',
partial: 'bg-amber-100 text-amber-700',
overdue: 'bg-red-100 text-red-700',
unpaid: 'bg-slate-100 text-slate-600',
};
const labels: Record<PaymentStatus, string> = {
paid: 'Paid',
partial: 'Partial',
overdue: 'Overdue',
unpaid: 'Unpaid',
};
return { style: styles[status], label: labels[status] || status, pendingRent: pendingRent || 0, pendingRentDays: pendingRentDays || 0 };
};
const getPenaltyBadge = (level: PenaltyLevel) => {
const styles: Record<PenaltyLevel, string> = {
none: 'bg-slate-100 text-slate-500',
day1: 'bg-amber-100 text-amber-700',
day2: 'bg-orange-100 text-orange-700',
day3: 'bg-red-100 text-red-700',
};
const labels: Record<PenaltyLevel, string> = {
none: 'None',
day1: '1st Day',
day2: '2nd Day',
day3: 'Bike Lock',
};
return { style: styles[level], label: labels[level] || level };
};
const getTypeBadge = (type: RentalType) => {
const styles: Record<RentalType, string> = {
single: 'bg-blue-100 text-blue-700',
shared: 'bg-purple-100 text-purple-700',
'rent-to-own': 'bg-emerald-100 text-emerald-700',
};
const labels: Record<RentalType, string> = {
single: 'Single Rent',
shared: 'Share EV',
'rent-to-own': 'Rent to Own',
};
return { style: styles[type], label: labels[type] || type };
};
const filteredRentals = rentals.filter(r => {
if (statusFilter !== 'all' && r.status !== statusFilter) return false;
if (hubFilter !== 'all' && r.hubId !== hubFilter) return false;
if (searchQuery) {
const query = searchQuery.toLowerCase();
return (
r.id.toLowerCase().includes(query) ||
r.userName.toLowerCase().includes(query) ||
r.userPhone.includes(query) ||
r.bikeModel.toLowerCase().includes(query) ||
r.bikePlate.toLowerCase().includes(query)
);
}
return true;
});
const eligibleUsers = mockUsers.filter(u => u.kycStatus === 'approved' && !u.hasActiveRental);
const availableBikes = mockBikes.filter(b => b.status === 'available');
const selectedPlan = planConditions[newRental.type]?.find(p => p.name === newRental.planConditionName) || planConditions[newRental.type]?.[0];
const stats = {
active: rentals.filter(r => r.status === 'active').length,
pending: rentals.filter(r => r.status === 'pending').length,
overdue: rentals.filter(r => r.paymentStatus === 'overdue' || r.paymentStatus === 'partial').length,
locked: rentals.filter(r => r.status === 'locked').length,
};
const handleCreateRental = () => {
if (!newRental.userId || !newRental.bikeId || !newRental.hubId) return;
setShowJournalPreview(true);
};
const confirmCreateRental = () => {
const bike = mockBikes.find(b => b.id === newRental.bikeId);
const user = mockUsers.find(u => u.id === newRental.userId);
const hub = mockHubs.find(h => h.id === newRental.hubId);
const settings = planConditions[newRental.type]?.find(p => p.name === newRental.planConditionName) || planConditions[newRental.type]?.[0];
const rental: Rental = {
id: `RNT-${String(rentals.length + 1).padStart(3, '0')}`,
bikeId: newRental.bikeId,
userId: newRental.userId,
userName: user?.name || '',
userPhone: user?.phone || '',
bikeModel: bike?.model || '',
bikePlate: bike?.plate || '',
bikeBattery: bike?.battery || 0,
type: newRental.type,
status: 'pending',
startDate: newRental.startDate,
endDate: newRental.contractMonths > 0
? new Date(new Date(newRental.startDate).setMonth(new Date(newRental.startDate).getMonth() + newRental.contractMonths)).toISOString().split('T')[0]
: undefined,
contractMonths: newRental.contractMonths > 0 ? newRental.contractMonths : undefined,
subscriptionType: newRental.subscriptionType,
deposit: settings.deposit,
depositPaymentMethod: newRental.depositPaymentMethod,
depositPaid: settings.deposit > 0,
dailyRate: settings.dailyRate,
weeklyRate: settings.weeklyRate,
monthlyRate: settings.monthlyRate,
totalPaid: settings.deposit,
dueRental: 0,
paymentStatus: settings.deposit > 0 ? 'paid' : 'unpaid',
penaltyLevel: 'none',
penaltyAmount: 0,
hubId: newRental.hubId,
hubName: hub?.name || '',
imagesApproved: false,
createdAt: new Date().toISOString().split('T')[0],
};
setRentals([...rentals, rental]);
setShowJournalPreview(false);
setShowCreateModal(false);
setCreateStep(1);
setNewRental({
userId: '',
type: 'single',
planConditionName: planConditions.single[1]?.name || '',
subscriptionType: 'daily',
contractMonths: 0,
bikeId: '',
startDate: new Date().toISOString().split('T')[0],
hubId: '',
depositPaymentMethod: 'cash',
});
};
const selectedBike = mockBikes.find(b => b.id === newRental.bikeId);
const selectedUser = mockUsers.find(u => u.id === newRental.userId);
return (
<div className="p-4 lg:p-6">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6">
<div>
<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>
</div>
<div className="flex items-center gap-2">
{createPermission && (
<button
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"
>
<Plus className="w-4 h-4" /> New Rental
</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">
<Download className="w-4 h-4" /> Export
</button>
</div>
</div>
<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">
<p className="text-2xl font-extrabold text-emerald-600">{stats.active}</p>
<p className="text-sm text-slate-500">Active</p>
</div>
<div className="bg-white rounded-xl p-4 shadow-sm border border-slate-100">
<p className="text-2xl font-extrabold text-amber-600">{stats.pending}</p>
<p className="text-sm text-slate-500">Pending</p>
</div>
<div className="bg-white rounded-xl p-4 shadow-sm border border-slate-100">
<p className="text-2xl font-extrabold text-orange-600">{stats.overdue}</p>
<p className="text-sm text-slate-500">Payment Issues</p>
</div>
<div className="bg-white rounded-xl p-4 shadow-sm border border-slate-100">
<p className="text-2xl font-extrabold text-red-600">{stats.locked}</p>
<p className="text-sm text-slate-500">Locked</p>
</div>
</div>
<div className="flex flex-col lg:flex-row gap-3 mb-4">
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="py-2 px-4 border border-slate-200 rounded-lg text-sm font-medium text-slate-600 hover:bg-slate-50"
>
<option value="all">All Status</option>
<option value="pending">Pending</option>
<option value="accepted">Accepted</option>
<option value="active">Active</option>
<option value="completed">Completed</option>
<option value="cancelled">Cancelled</option>
<option value="locked">Locked</option>
<option value="disputed">Disputed</option>
</select>
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<input
type="text"
placeholder="Search by ID, user, bike..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-slate-200 rounded-lg text-sm text-slate-600"
/>
</div>
<select
value={hubFilter}
onChange={(e) => setHubFilter(e.target.value)}
className="py-2 px-4 border border-slate-200 rounded-lg text-sm font-medium text-slate-600 hover:bg-slate-50"
>
<option value="all">All Hubs</option>
{mockHubs.map(hub => (
<option key={hub.id} value={hub.id}>{hub.name}</option>
))}
</select>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-50">
<tr>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">ID</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Bike</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">User</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Type</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Subscription</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">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">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{filteredRentals.map(rental => {
const statusBadge = getStatusBadge(rental.status);
const paymentBadge = getPaymentStatusBadge(rental.paymentStatus, rental.pendingRent, rental.pendingRentDays);
const penaltyBadge = getPenaltyBadge(rental.penaltyLevel);
const typeBadge = getTypeBadge(rental.type);
return (
<tr key={rental.id} className="hover:bg-slate-50 transition-colors">
<td className="px-4 py-3">
<Link href={`/admin/rentals/${rental.id}`} className="text-sm font-medium text-emerald-600 hover:text-emerald-700">
{rental.id}
</Link>
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<Bike className="w-4 h-4 text-slate-400" />
<div>
<span className="text-sm text-slate-600">{rental.bikeModel}</span>
<p className="text-xs text-slate-400">{rental.bikePlate}</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" />
<div>
<span className="text-sm text-slate-600">{rental.userName}</span>
<p className="text-xs text-slate-400">{rental.userPhone}</p>
</div>
</div>
</td>
<td className="px-4 py-3">
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${typeBadge.style}`}>
{typeBadge.label}
</span>
</td>
<td className="px-4 py-3">
<span className="text-sm text-slate-600 capitalize">{rental.subscriptionType}</span>
</td>
<td className="px-4 py-3">
<span className="text-sm text-slate-600">{rental.hubName}</span>
</td>
<td className="px-4 py-3">
<div>
<span className="text-sm font-medium text-slate-700">{rental.deposit.toLocaleString()}</span>
<span className={`ml-2 text-xs px-1.5 py-0.5 rounded ${rental.depositPaid ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}`}>
{rental.depositPaid ? 'Paid' : 'Unpaid'}
</span>
</div>
</td>
<td className="px-4 py-3">
{(paymentBadge.pendingRent && paymentBadge.pendingRent > 0) ? (
<div>
<span className="inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full bg-amber-100 text-amber-700">
<AlertTriangle className="w-3 h-3" /> Pending
</span>
<p className="text-xs text-amber-600 mt-1">{paymentBadge.pendingRent} ({paymentBadge.pendingRentDays}d)</p>
</div>
) : (
<span className="inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full bg-green-100 text-green-700">
<CheckCircle className="w-3 h-3" /> Clear
</span>
)}
</td>
{/* <td className="px-4 py-3">
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${penaltyBadge.style}`}>
{penaltyBadge.label}
</span>
</td> */}
<td className="px-4 py-3">
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${statusBadge.style}`}>
{statusBadge.label}
</span>
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-1">
<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" />
</Link>
{lockPermission && rental.status === 'active' && (
<button className="p-1.5 hover:bg-red-100 rounded-lg" title="Lock">
<Lock className="w-4 h-4 text-red-500" />
</button>
)}
{unlockPermission && rental.status === 'locked' && (
<button className="p-1.5 hover:bg-green-100 rounded-lg" title="Unlock">
<Unlock className="w-4 h-4 text-green-500" />
</button>
)}
<a href={`tel:${rental.userPhone}`} className="p-1.5 hover:bg-green-100 rounded-lg" title="Call">
<Phone className="w-4 h-4 text-green-500" />
</a>
<a href={`sms:${rental.userPhone}`} className="p-1.5 hover:bg-blue-100 rounded-lg" title="Message">
<MessageCircle className="w-4 h-4 text-blue-500" />
</a>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
{showCreateModal && (
<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-2xl max-h-[90vh] overflow-y-auto">
<div className="p-4 border-b border-slate-100 flex justify-between items-center">
<h3 className="font-semibold text-slate-800">Create New Rental - Step {createStep} of 5</h3>
<button onClick={() => { setShowCreateModal(false); setCreateStep(1); }} className="text-slate-400 hover:text-slate-600">
<X className="w-5 h-5" />
</button>
</div>
<div className="p-4 space-y-4">
{createStep === 1 && (
<div className="space-y-4">
<h4 className="font-medium text-slate-700 mb-3">Step 1: Start Date & Hub</h4>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1.5 block">Start Date</label>
<input type="date" value={newRental.startDate} onChange={(e) => setNewRental({ ...newRental, startDate: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1.5 block">Hub</label>
<select value={newRental.hubId} onChange={(e) => setNewRental({ ...newRental, hubId: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
<option value="">Select Hub...</option>
{mockHubs.map(hub => (
<option key={hub.id} value={hub.id}>{hub.name}</option>
))}
</select>
</div>
</div>
<div>
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1.5 block">Select User</label>
<p className="text-xs text-slate-400 mb-2">Only KYC-approved users without active rentals are shown.</p>
<select value={newRental.userId} onChange={(e) => setNewRental({ ...newRental, userId: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
<option value="">Select User...</option>
{eligibleUsers.map(user => (
<option key={user.id} value={user.id}>{user.name} ({user.phone})</option>
))}
</select>
</div>
</div>
)}
{createStep === 2 && (
<div className="space-y-4">
<div>
<label className="text-sm text-slate-600">Rental Type</label>
<select
value={newRental.type}
onChange={(e) => setNewRental({ ...newRental, type: e.target.value as RentalType, planConditionName: planConditions[e.target.value as RentalType]?.[1]?.name || planConditions[e.target.value as RentalType]?.[0]?.name || '', subscriptionType: 'daily', contractMonths: 0 })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
>
<option value="single">Single Rent</option>
<option value="shared">Share EV</option>
<option value="rent-to-own">Rent to Own</option>
</select>
</div>
<div>
<label className="text-sm text-slate-600">Plan Condition</label>
<select
value={newRental.planConditionName}
onChange={(e) => setNewRental({ ...newRental, planConditionName: e.target.value, contractMonths: 0, subscriptionType: 'daily' })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
>
<option value="">Select Plan...</option>
{planConditions[newRental.type]?.map(plan => (
<option key={plan.name} value={plan.name}>{plan.name}</option>
))}
</select>
</div>
<div>
<label className="text-sm text-slate-600">Contract Duration</label>
<select
value={newRental.contractMonths}
onChange={(e) => setNewRental({ ...newRental, contractMonths: Number(e.target.value) })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
>
<option value={0}>Daily Basis</option>
{selectedPlan?.contractMonths.map(m => (
<option key={m} value={m}>{m} Months</option>
))}
</select>
{newRental.contractMonths > 0 && (
<p className="text-xs text-emerald-600 mt-1">
End Date: {new Date(new Date(newRental.startDate).setMonth(new Date(newRental.startDate).getMonth() + newRental.contractMonths)).toISOString().split('T')[0]}
</p>
)}
</div>
<div>
<label className="text-sm text-slate-600 mb-2 block">Subscription Type</label>
<div className="flex gap-2">
{(['daily', 'weekly', 'monthly'] as const).map(sub => (
<button
key={sub}
type="button"
onClick={() => setNewRental({ ...newRental, subscriptionType: sub })}
className={`flex-1 py-2 px-3 rounded-lg text-sm border ${newRental.subscriptionType === sub
? 'bg-emerald-100 border-emerald-300 text-emerald-700'
: 'border-slate-200 text-slate-600 hover:bg-slate-50'
}`}
>
{sub === 'daily' ? 'Daily' : sub === 'weekly' ? 'Weekly' : 'Monthly'}
</button>
))}
</div>
</div>
<div className="bg-emerald-50 p-3 rounded-lg border border-emerald-100">
<div className="flex items-center justify-between mb-1">
<span className="text-xs text-emerald-600 font-medium uppercase tracking-wide">{newRental.planConditionName}</span>
</div>
<p className="text-sm text-slate-600">Deposit: <span className="font-semibold text-slate-800">{selectedPlan?.deposit.toLocaleString()}</span></p>
<p className="text-sm text-slate-600">
Rate: <span className="font-semibold text-slate-800">{newRental.subscriptionType === 'daily' ? selectedPlan?.dailyRate : newRental.subscriptionType === 'weekly' ? selectedPlan?.weeklyRate : selectedPlan?.monthlyRate}</span>
<span className="text-slate-500">/{newRental.subscriptionType === 'daily' ? 'day' : newRental.subscriptionType === 'weekly' ? 'week' : 'month'}</span>
</p>
</div>
</div>
)}
{createStep === 3 && (
<div>
<h4 className="font-medium text-slate-700 mb-3">Step 3: Select Bike</h4>
<select
value={newRental.bikeId}
onChange={(e) => setNewRental({ ...newRental, bikeId: e.target.value })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
>
<option value="">Select Bike...</option>
{availableBikes.map(bike => (
<option key={bike.id} value={bike.id}>{bike.model} - {bike.plate}</option>
))}
</select>
{selectedBike && (
<div className="mt-3 flex items-center gap-2">
<span className="text-sm text-slate-600">Battery:</span>
<span className={`text-sm px-2 py-1 rounded-full ${selectedBike.battery > 70 ? 'bg-green-100 text-green-700' : selectedBike.battery > 40 ? 'bg-amber-100 text-amber-700' : 'bg-red-100 text-red-700'}`}>
{selectedBike.battery}%
</span>
</div>
)}
</div>
)}
{createStep === 4 && (
<div className="space-y-4">
<h4 className="font-medium text-slate-700 mb-3">Step 4: Start Date & Hub</h4>
<div>
<label className="text-sm text-slate-600">Start Date</label>
<input
type="date"
value={newRental.startDate}
onChange={(e) => setNewRental({ ...newRental, startDate: e.target.value })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
/>
</div>
<div>
<label className="text-sm text-slate-600">Hub</label>
<select
value={newRental.hubId}
onChange={(e) => setNewRental({ ...newRental, hubId: e.target.value })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
>
<option value="">Select Hub...</option>
{mockHubs.map(hub => (
<option key={hub.id} value={hub.id}>{hub.name}</option>
))}
</select>
</div>
</div>
)}
{createStep === 5 && (
<div className="space-y-4">
<h4 className="font-medium text-slate-700 mb-3">Step 5: Deposit Payment</h4>
<div className="bg-slate-50 p-3 rounded-lg">
<p className="text-sm text-slate-600">Deposit Amount: {selectedPlan?.deposit.toLocaleString()}</p>
<p className="text-sm text-slate-600">User: {selectedUser?.name}</p>
<p className="text-sm text-slate-600">Bike: {selectedBike?.model} ({selectedBike?.plate})</p>
<p className="text-sm text-slate-600">Hub: {mockHubs.find(h => h.id === newRental.hubId)?.name}</p>
</div>
{selectedPlan.deposit > 0 && (
<div>
<label className="text-sm text-slate-600 mb-2 block">Payment Method</label>
<div className="flex gap-2">
{(['cash', 'bank', 'wallet'] as const).map(method => (
<button
key={method}
type="button"
onClick={() => setNewRental({ ...newRental, depositPaymentMethod: method })}
className={`flex-1 py-2 px-3 rounded-lg text-sm border flex items-center justify-center gap-2 ${newRental.depositPaymentMethod === method
? 'bg-emerald-100 border-emerald-300 text-emerald-700'
: 'border-slate-200 text-slate-600 hover:bg-slate-50'
}`}
>
{method === 'cash' && <Wallet className="w-4 h-4" />}
{method === 'bank' && <Building className="w-4 h-4" />}
{method === 'wallet' && <CreditCard className="w-4 h-4" />}
{method === 'cash' ? 'Cash' : method === 'bank' ? 'Bank' : 'Wallet'}
</button>
))}
</div>
</div>
)}
{selectedPlan.deposit > 0 && (
<div className="bg-blue-50 p-3 rounded-lg">
<p className="text-sm font-medium text-blue-700 mb-2">Journal Preview</p>
<table className="w-full text-xs">
<thead className="bg-blue-100">
<tr>
<th className="px-2 py-1 text-left">Account</th>
<th className="px-2 py-1 text-right">Debit</th>
<th className="px-2 py-1 text-right">Credit</th>
</tr>
</thead>
<tbody>
<tr>
<td className="px-2 py-1">
{newRental.depositPaymentMethod === 'cash' && '1000 - Cash'}
{newRental.depositPaymentMethod === 'bank' && '1100 - Bank'}
{newRental.depositPaymentMethod === 'wallet' && '1200 - Biker Wallet'}
</td>
<td className="px-2 py-1 text-right">{selectedPlan.deposit}</td>
<td className="px-2 py-1 text-right">-</td>
</tr>
<tr>
<td className="px-2 py-1">2100 - Deposit Received</td>
<td className="px-2 py-1 text-right">-</td>
<td className="px-2 py-1 text-right">{selectedPlan.deposit}</td>
</tr>
</tbody>
</table>
</div>
)}
</div>
)}
</div>
<div className="p-4 border-t border-slate-100 flex justify-between">
<div className="flex gap-2">
{createStep > 1 && (
<button
onClick={() => setCreateStep(createStep - 1)}
className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm flex items-center gap-2"
>
<ChevronLeft className="w-4 h-4" /> Back
</button>
)}
</div>
<div className="flex gap-2">
{createStep < 5 ? (
<button
onClick={() => setCreateStep(createStep + 1)}
disabled={
(createStep === 1 && !newRental.userId) ||
(createStep === 3 && !newRental.bikeId) ||
(createStep === 4 && !newRental.hubId)
}
className="px-4 py-2 bg-emerald-600 text-white rounded-lg text-sm disabled:opacity-50 flex items-center gap-2"
>
Next <ChevronRight className="w-4 h-4" />
</button>
) : (
<>
{selectedPlan.deposit > 0 && (
<button className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm flex items-center gap-2">
<Printer className="w-4 h-4" /> Print Invoice
</button>
)}
<button
onClick={confirmCreateRental}
className="px-4 py-2 bg-emerald-600 text-white rounded-lg text-sm hover:bg-emerald-700"
>
Confirm & Create
</button>
</>
)}
</div>
</div>
</div>
</div>
)}
</div>
);
}
function Lock({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
</svg>
);
}
function Unlock({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
<path d="M7 11V7a5 5 0 0 1 9.9-1" />
</svg>
);
}