2026-04-26 14:56:12 +06:00
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import { useState, useEffect } from 'react';
|
2026-05-10 01:22:17 +06:00
|
|
|
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';
|
2026-04-26 14:56:12 +06:00
|
|
|
import Link from 'next/link';
|
2026-05-10 01:22:17 +06:00
|
|
|
import {
|
|
|
|
|
canRentalCreate, canRentalCancel, canRentalEdit, canRentalLock, canRentalUnlock
|
|
|
|
|
} from '../../../lib/auth';
|
2026-04-26 14:56:12 +06:00
|
|
|
|
2026-05-10 01:22:17 +06:00
|
|
|
type RentalStatus = 'pending' | 'accepted' | 'active' | 'completed' | 'cancelled' | 'locked' | 'disputed';
|
2026-04-26 14:56:12 +06:00
|
|
|
type RentalType = 'single' | 'shared' | 'rent-to-own';
|
2026-05-10 01:22:17 +06:00
|
|
|
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;
|
|
|
|
|
}
|
2026-04-26 14:56:12 +06:00
|
|
|
|
|
|
|
|
interface Rental {
|
|
|
|
|
id: string;
|
|
|
|
|
bikeId: string;
|
|
|
|
|
userId: string;
|
2026-05-10 01:22:17 +06:00
|
|
|
userName: string;
|
|
|
|
|
userPhone: string;
|
|
|
|
|
bikeModel: string;
|
|
|
|
|
bikePlate: string;
|
|
|
|
|
bikeBattery: number;
|
2026-04-26 14:56:12 +06:00
|
|
|
type: RentalType;
|
|
|
|
|
status: RentalStatus;
|
|
|
|
|
startDate: string;
|
|
|
|
|
endDate?: string;
|
|
|
|
|
contractMonths?: number;
|
2026-05-10 01:22:17 +06:00
|
|
|
subscriptionType: 'daily' | 'weekly' | 'monthly';
|
2026-04-26 14:56:12 +06:00
|
|
|
deposit: number;
|
2026-05-10 01:22:17 +06:00
|
|
|
depositPaymentMethod: PaymentMethod;
|
|
|
|
|
depositPaid: boolean;
|
2026-04-26 14:56:12 +06:00
|
|
|
dailyRate: number;
|
2026-05-10 01:22:17 +06:00
|
|
|
weeklyRate: number;
|
|
|
|
|
monthlyRate: number;
|
2026-04-26 14:56:12 +06:00
|
|
|
totalPaid: number;
|
2026-05-10 01:22:17 +06:00
|
|
|
dueRental: number;
|
2026-05-10 01:31:22 +06:00
|
|
|
pendingRent?: number;
|
|
|
|
|
pendingRentDays?: number;
|
2026-05-10 01:22:17 +06:00
|
|
|
paymentStatus: PaymentStatus;
|
|
|
|
|
penaltyLevel: PenaltyLevel;
|
|
|
|
|
penaltyAmount: number;
|
2026-04-26 14:56:12 +06:00
|
|
|
lockedAt?: string;
|
|
|
|
|
lockedReason?: string;
|
2026-05-10 01:22:17 +06:00
|
|
|
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';
|
2026-04-26 14:56:12 +06:00
|
|
|
}
|
|
|
|
|
|
2026-05-10 01:22:17 +06:00
|
|
|
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' },
|
2026-04-26 14:56:12 +06:00
|
|
|
];
|
|
|
|
|
|
2026-05-10 01:22:17 +06:00
|
|
|
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 },
|
2026-04-26 14:56:12 +06:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
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' },
|
|
|
|
|
];
|
|
|
|
|
|
2026-05-10 01:22:17 +06:00
|
|
|
const rentalSettings = {
|
|
|
|
|
single: { deposit: 3000, contractMonths: [1, 3, 6, 12], dailyRate: 150, weeklyRate: 900, monthlyRate: 3500 },
|
|
|
|
|
shared: { deposit: 2000, contractMonths: [1, 3, 6], dailyRate: 100, weeklyRate: 600, monthlyRate: 2200 },
|
|
|
|
|
'rent-to-own': { deposit: 10000, contractMonths: [12, 18, 24, 36], dailyRate: 500, weeklyRate: 3000, monthlyRate: 12000 },
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-26 14:56:12 +06:00
|
|
|
const mockRentals: Rental[] = [
|
|
|
|
|
{
|
|
|
|
|
id: 'RNT-001',
|
|
|
|
|
bikeId: 'BIKE-001',
|
2026-05-10 01:22:17 +06:00
|
|
|
userId: 'USR-003',
|
|
|
|
|
userName: 'Jamal Uddin',
|
|
|
|
|
userPhone: '+8801912345678',
|
|
|
|
|
bikeModel: 'AIMA Lightning',
|
|
|
|
|
bikePlate: 'Dhaka Metro Cha-9012',
|
|
|
|
|
bikeBattery: 87,
|
2026-04-26 14:56:12 +06:00
|
|
|
type: 'single',
|
|
|
|
|
status: 'active',
|
|
|
|
|
startDate: '2024-01-15',
|
2026-05-10 01:22:17 +06:00
|
|
|
contractMonths: 12,
|
|
|
|
|
subscriptionType: 'monthly',
|
|
|
|
|
deposit: 3000,
|
|
|
|
|
depositPaymentMethod: 'cash',
|
|
|
|
|
depositPaid: true,
|
|
|
|
|
dailyRate: 150,
|
|
|
|
|
weeklyRate: 900,
|
|
|
|
|
monthlyRate: 3500,
|
|
|
|
|
totalPaid: 38500,
|
2026-04-26 14:56:12 +06:00
|
|
|
dueRental: 0,
|
2026-05-10 01:31:22 +06:00
|
|
|
pendingRent: 0,
|
|
|
|
|
pendingRentDays: 0,
|
2026-05-10 01:22:17 +06:00
|
|
|
paymentStatus: 'paid',
|
|
|
|
|
penaltyLevel: 'none',
|
|
|
|
|
penaltyAmount: 0,
|
2026-04-26 14:56:12 +06:00
|
|
|
hubId: 'HUB-001',
|
2026-05-10 01:22:17 +06:00
|
|
|
hubName: 'Gulshan Hub',
|
|
|
|
|
imagesApproved: true,
|
|
|
|
|
createdAt: '2024-01-15',
|
|
|
|
|
acceptedAt: '2024-01-15',
|
|
|
|
|
activatedAt: '2024-01-16',
|
2026-04-26 14:56:12 +06:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'RNT-002',
|
|
|
|
|
bikeId: 'BIKE-002',
|
|
|
|
|
userId: 'USR-002',
|
2026-05-10 01:22:17 +06:00
|
|
|
userName: 'Karim Hasan',
|
|
|
|
|
userPhone: '+8801812345678',
|
|
|
|
|
bikeModel: 'Yadea DT3',
|
|
|
|
|
bikePlate: 'Dhaka Metro Ba-5521',
|
|
|
|
|
bikeBattery: 65,
|
2026-04-26 14:56:12 +06:00
|
|
|
type: 'single',
|
|
|
|
|
status: 'pending',
|
|
|
|
|
startDate: '2024-02-10',
|
2026-05-10 01:22:17 +06:00
|
|
|
contractMonths: 3,
|
|
|
|
|
subscriptionType: 'monthly',
|
|
|
|
|
deposit: 3000,
|
|
|
|
|
depositPaymentMethod: 'bank',
|
|
|
|
|
depositPaid: true,
|
2026-04-26 14:56:12 +06:00
|
|
|
dailyRate: 150,
|
2026-05-10 01:22:17 +06:00
|
|
|
weeklyRate: 900,
|
|
|
|
|
monthlyRate: 3500,
|
|
|
|
|
totalPaid: 3000,
|
|
|
|
|
dueRental: 3500,
|
2026-05-10 01:31:22 +06:00
|
|
|
pendingRent: 3500,
|
|
|
|
|
pendingRentDays: 5,
|
2026-05-10 01:22:17 +06:00
|
|
|
paymentStatus: 'partial',
|
|
|
|
|
penaltyLevel: 'none',
|
|
|
|
|
penaltyAmount: 0,
|
2026-04-26 14:56:12 +06:00
|
|
|
hubId: 'HUB-002',
|
2026-05-10 01:22:17 +06:00
|
|
|
hubName: 'Banani Hub',
|
|
|
|
|
imagesApproved: false,
|
|
|
|
|
createdAt: '2024-02-10',
|
2026-04-26 14:56:12 +06:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'RNT-003',
|
|
|
|
|
bikeId: 'BIKE-003',
|
2026-05-10 01:22:17 +06:00
|
|
|
userId: 'USR-001',
|
|
|
|
|
userName: 'Rahim Ahmed',
|
|
|
|
|
userPhone: '+8801712345678',
|
|
|
|
|
bikeModel: 'AIMA EM5',
|
|
|
|
|
bikePlate: 'Dhaka Metro Ko-1234',
|
|
|
|
|
bikeBattery: 92,
|
2026-04-26 14:56:12 +06:00
|
|
|
type: 'rent-to-own',
|
|
|
|
|
status: 'completed',
|
|
|
|
|
startDate: '2023-06-01',
|
|
|
|
|
endDate: '2023-12-01',
|
2026-05-10 01:22:17 +06:00
|
|
|
contractMonths: 6,
|
|
|
|
|
subscriptionType: 'monthly',
|
2026-04-26 14:56:12 +06:00
|
|
|
deposit: 10000,
|
2026-05-10 01:22:17 +06:00
|
|
|
depositPaymentMethod: 'wallet',
|
|
|
|
|
depositPaid: true,
|
2026-04-26 14:56:12 +06:00
|
|
|
dailyRate: 500,
|
2026-05-10 01:22:17 +06:00
|
|
|
weeklyRate: 3000,
|
|
|
|
|
monthlyRate: 12000,
|
|
|
|
|
totalPaid: 82000,
|
|
|
|
|
dueRental: 0,
|
2026-05-10 01:31:22 +06:00
|
|
|
pendingRent: 0,
|
|
|
|
|
pendingRentDays: 0,
|
2026-05-10 01:22:17 +06:00
|
|
|
paymentStatus: 'paid',
|
|
|
|
|
penaltyLevel: 'none',
|
|
|
|
|
penaltyAmount: 0,
|
2026-04-26 14:56:12 +06:00
|
|
|
hubId: 'HUB-001',
|
2026-05-10 01:22:17 +06:00
|
|
|
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,
|
2026-05-10 01:31:22 +06:00
|
|
|
pendingRent: 600,
|
|
|
|
|
pendingRentDays: 3,
|
2026-05-10 01:22:17 +06:00
|
|
|
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',
|
|
|
|
|
},
|
2026-04-26 14:56:12 +06:00
|
|
|
];
|
2026-04-22 01:02:45 +06:00
|
|
|
|
|
|
|
|
export default function RentalsPage() {
|
2026-04-26 14:56:12 +06:00
|
|
|
const [rentals, setRentals] = useState<Rental[]>(mockRentals);
|
|
|
|
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
|
|
|
|
const [statusFilter, setStatusFilter] = useState<string>('all');
|
2026-05-10 01:22:17 +06:00
|
|
|
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 [createStep, setCreateStep] = useState(1);
|
|
|
|
|
const [newRental, setNewRental] = useState<{
|
|
|
|
|
userId: string;
|
|
|
|
|
type: RentalType;
|
|
|
|
|
subscriptionType: 'daily' | 'weekly' | 'monthly';
|
|
|
|
|
contractMonths: number;
|
|
|
|
|
bikeId: string;
|
|
|
|
|
startDate: string;
|
|
|
|
|
hubId: string;
|
|
|
|
|
depositPaymentMethod: PaymentMethod;
|
|
|
|
|
}>({
|
2026-04-26 14:56:12 +06:00
|
|
|
userId: '',
|
2026-05-10 01:22:17 +06:00
|
|
|
type: 'single',
|
|
|
|
|
subscriptionType: 'daily',
|
2026-04-26 14:56:12 +06:00
|
|
|
contractMonths: 0,
|
2026-05-10 01:22:17 +06:00
|
|
|
bikeId: '',
|
|
|
|
|
startDate: new Date().toISOString().split('T')[0],
|
2026-04-26 14:56:12 +06:00
|
|
|
hubId: '',
|
2026-05-10 01:22:17 +06:00
|
|
|
depositPaymentMethod: 'cash',
|
2026-04-26 14:56:12 +06:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const [showJournalPreview, setShowJournalPreview] = useState(false);
|
|
|
|
|
|
2026-05-10 01:22:17 +06:00
|
|
|
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 };
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-10 01:31:22 +06:00
|
|
|
const getPaymentStatusBadge = (status: PaymentStatus, pendingRent?: number, pendingRentDays?: number) => {
|
2026-05-10 01:22:17 +06:00
|
|
|
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',
|
|
|
|
|
};
|
2026-05-10 01:31:22 +06:00
|
|
|
return { style: styles[status], label: labels[status] || status, pendingRent: pendingRent || 0, pendingRentDays: pendingRentDays || 0 };
|
2026-05-10 01:22:17 +06:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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 };
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-26 14:56:12 +06:00
|
|
|
const filteredRentals = rentals.filter(r => {
|
|
|
|
|
if (statusFilter !== 'all' && r.status !== statusFilter) return false;
|
2026-05-10 01:22:17 +06:00
|
|
|
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)
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-04-26 14:56:12 +06:00
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-10 01:22:17 +06:00
|
|
|
const eligibleUsers = mockUsers.filter(u => u.kycStatus === 'approved' && !u.hasActiveRental);
|
|
|
|
|
const availableBikes = mockBikes.filter(b => b.status === 'available');
|
|
|
|
|
|
2026-04-26 14:56:12 +06:00
|
|
|
const stats = {
|
|
|
|
|
active: rentals.filter(r => r.status === 'active').length,
|
|
|
|
|
pending: rentals.filter(r => r.status === 'pending').length,
|
2026-05-10 01:22:17 +06:00
|
|
|
overdue: rentals.filter(r => r.paymentStatus === 'overdue' || r.paymentStatus === 'partial').length,
|
|
|
|
|
locked: rentals.filter(r => r.status === 'locked').length,
|
2026-04-26 14:56:12 +06:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleCreateRental = () => {
|
2026-05-10 01:22:17 +06:00
|
|
|
if (!newRental.userId || !newRental.bikeId || !newRental.hubId) return;
|
2026-04-26 14:56:12 +06:00
|
|
|
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);
|
2026-05-10 01:22:17 +06:00
|
|
|
const settings = rentalSettings[newRental.type];
|
|
|
|
|
|
2026-04-26 14:56:12 +06:00
|
|
|
const rental: Rental = {
|
|
|
|
|
id: `RNT-${String(rentals.length + 1).padStart(3, '0')}`,
|
|
|
|
|
bikeId: newRental.bikeId,
|
|
|
|
|
userId: newRental.userId,
|
2026-05-10 01:22:17 +06:00
|
|
|
userName: user?.name || '',
|
|
|
|
|
userPhone: user?.phone || '',
|
|
|
|
|
bikeModel: bike?.model || '',
|
|
|
|
|
bikePlate: bike?.plate || '',
|
|
|
|
|
bikeBattery: bike?.battery || 0,
|
2026-04-26 14:56:12 +06:00
|
|
|
type: newRental.type,
|
|
|
|
|
status: 'pending',
|
|
|
|
|
startDate: newRental.startDate,
|
2026-05-10 01:22:17 +06:00
|
|
|
endDate: newRental.contractMonths > 0
|
2026-04-26 14:56:12 +06:00
|
|
|
? new Date(new Date(newRental.startDate).setMonth(new Date(newRental.startDate).getMonth() + newRental.contractMonths)).toISOString().split('T')[0]
|
|
|
|
|
: undefined,
|
2026-05-10 01:22:17 +06:00
|
|
|
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,
|
2026-04-26 14:56:12 +06:00
|
|
|
dueRental: 0,
|
2026-05-10 01:22:17 +06:00
|
|
|
paymentStatus: settings.deposit > 0 ? 'paid' : 'unpaid',
|
|
|
|
|
penaltyLevel: 'none',
|
|
|
|
|
penaltyAmount: 0,
|
2026-04-26 14:56:12 +06:00
|
|
|
hubId: newRental.hubId,
|
2026-05-10 01:22:17 +06:00
|
|
|
hubName: hub?.name || '',
|
|
|
|
|
imagesApproved: false,
|
|
|
|
|
createdAt: new Date().toISOString().split('T')[0],
|
2026-04-26 14:56:12 +06:00
|
|
|
};
|
2026-05-10 01:22:17 +06:00
|
|
|
|
2026-04-26 14:56:12 +06:00
|
|
|
setRentals([...rentals, rental]);
|
|
|
|
|
setShowJournalPreview(false);
|
|
|
|
|
setShowCreateModal(false);
|
2026-05-10 01:22:17 +06:00
|
|
|
setCreateStep(1);
|
2026-04-26 14:56:12 +06:00
|
|
|
setNewRental({
|
|
|
|
|
userId: '',
|
|
|
|
|
type: 'single',
|
2026-05-10 01:22:17 +06:00
|
|
|
subscriptionType: 'daily',
|
2026-04-26 14:56:12 +06:00
|
|
|
contractMonths: 0,
|
2026-05-10 01:22:17 +06:00
|
|
|
bikeId: '',
|
|
|
|
|
startDate: new Date().toISOString().split('T')[0],
|
2026-04-26 14:56:12 +06:00
|
|
|
hubId: '',
|
2026-05-10 01:22:17 +06:00
|
|
|
depositPaymentMethod: 'cash',
|
2026-04-26 14:56:12 +06:00
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-10 01:22:17 +06:00
|
|
|
const selectedSettings = rentalSettings[newRental.type];
|
|
|
|
|
const selectedBike = mockBikes.find(b => b.id === newRental.bikeId);
|
|
|
|
|
const selectedUser = mockUsers.find(u => u.id === newRental.userId);
|
2026-04-26 14:56:12 +06:00
|
|
|
|
2026-04-22 01:02:45 +06:00
|
|
|
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>
|
2026-05-10 01:22:17 +06:00
|
|
|
<p className="text-sm text-slate-500 mt-1">Manage rental transactions</p>
|
2026-04-22 01:02:45 +06:00
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center gap-2">
|
2026-05-10 01:22:17 +06:00
|
|
|
{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
|
2026-04-22 01:02:45 +06:00
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-04-26 14:56:12 +06:00
|
|
|
<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">
|
2026-05-10 01:22:17 +06:00
|
|
|
<p className="text-2xl font-extrabold text-emerald-600">{stats.active}</p>
|
|
|
|
|
<p className="text-sm text-slate-500">Active</p>
|
2026-04-26 14:56:12 +06:00
|
|
|
</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">
|
2026-05-10 01:22:17 +06:00
|
|
|
<p className="text-2xl font-extrabold text-orange-600">{stats.overdue}</p>
|
|
|
|
|
<p className="text-sm text-slate-500">Payment Issues</p>
|
2026-04-26 14:56:12 +06:00
|
|
|
</div>
|
|
|
|
|
<div className="bg-white rounded-xl p-4 shadow-sm border border-slate-100">
|
2026-05-10 01:22:17 +06:00
|
|
|
<p className="text-2xl font-extrabold text-red-600">{stats.locked}</p>
|
|
|
|
|
<p className="text-sm text-slate-500">Locked</p>
|
2026-04-26 14:56:12 +06:00
|
|
|
</div>
|
2026-04-22 01:02:45 +06:00
|
|
|
</div>
|
|
|
|
|
|
2026-05-10 01:22:17 +06:00
|
|
|
<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>
|
|
|
|
|
|
2026-04-22 01:02:45 +06:00
|
|
|
<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>
|
2026-05-10 01:22:17 +06:00
|
|
|
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">ID</th>
|
2026-04-22 01:02:45 +06:00
|
|
|
<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>
|
2026-05-10 01:22:17 +06:00
|
|
|
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Subscription</th>
|
2026-04-26 14:56:12 +06:00
|
|
|
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Hub</th>
|
2026-04-22 01:02:45 +06:00
|
|
|
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Deposit</th>
|
2026-05-10 01:22:17 +06:00
|
|
|
<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> */}
|
2026-04-22 01:02:45 +06:00
|
|
|
<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">
|
2026-04-26 14:56:12 +06:00
|
|
|
{filteredRentals.map(rental => {
|
2026-05-10 01:22:17 +06:00
|
|
|
const statusBadge = getStatusBadge(rental.status);
|
2026-05-10 01:31:22 +06:00
|
|
|
const paymentBadge = getPaymentStatusBadge(rental.paymentStatus, rental.pendingRent, rental.pendingRentDays);
|
2026-05-10 01:22:17 +06:00
|
|
|
const penaltyBadge = getPenaltyBadge(rental.penaltyLevel);
|
|
|
|
|
const typeBadge = getTypeBadge(rental.type);
|
2026-04-22 01:02:45 +06:00
|
|
|
return (
|
|
|
|
|
<tr key={rental.id} className="hover:bg-slate-50 transition-colors">
|
|
|
|
|
<td className="px-4 py-3">
|
2026-05-10 01:22:17 +06:00
|
|
|
<Link href={`/admin/rentals/${rental.id}`} className="text-sm font-medium text-emerald-600 hover:text-emerald-700">
|
|
|
|
|
{rental.id}
|
|
|
|
|
</Link>
|
2026-04-22 01:02:45 +06:00
|
|
|
</td>
|
|
|
|
|
<td className="px-4 py-3">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<Bike className="w-4 h-4 text-slate-400" />
|
2026-04-26 14:56:12 +06:00
|
|
|
<div>
|
2026-05-10 01:22:17 +06:00
|
|
|
<span className="text-sm text-slate-600">{rental.bikeModel}</span>
|
|
|
|
|
<p className="text-xs text-slate-400">{rental.bikePlate}</p>
|
2026-04-26 14:56:12 +06:00
|
|
|
</div>
|
2026-04-22 01:02:45 +06:00
|
|
|
</div>
|
|
|
|
|
</td>
|
|
|
|
|
<td className="px-4 py-3">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<User className="w-4 h-4 text-slate-400" />
|
2026-04-26 14:56:12 +06:00
|
|
|
<div>
|
2026-05-10 01:22:17 +06:00
|
|
|
<span className="text-sm text-slate-600">{rental.userName}</span>
|
|
|
|
|
<p className="text-xs text-slate-400">{rental.userPhone}</p>
|
2026-04-26 14:56:12 +06:00
|
|
|
</div>
|
2026-04-22 01:02:45 +06:00
|
|
|
</div>
|
|
|
|
|
</td>
|
|
|
|
|
<td className="px-4 py-3">
|
2026-05-10 01:22:17 +06:00
|
|
|
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${typeBadge.style}`}>
|
|
|
|
|
{typeBadge.label}
|
|
|
|
|
</span>
|
2026-04-22 01:02:45 +06:00
|
|
|
</td>
|
2026-04-26 14:56:12 +06:00
|
|
|
<td className="px-4 py-3">
|
2026-05-10 01:22:17 +06:00
|
|
|
<span className="text-sm text-slate-600 capitalize">{rental.subscriptionType}</span>
|
2026-04-26 14:56:12 +06:00
|
|
|
</td>
|
2026-04-22 01:02:45 +06:00
|
|
|
<td className="px-4 py-3">
|
2026-05-10 01:22:17 +06:00
|
|
|
<span className="text-sm text-slate-600">{rental.hubName}</span>
|
2026-04-22 01:02:45 +06:00
|
|
|
</td>
|
|
|
|
|
<td className="px-4 py-3">
|
2026-05-10 01:22:17 +06:00
|
|
|
<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>
|
2026-04-26 14:56:12 +06:00
|
|
|
</td>
|
|
|
|
|
<td className="px-4 py-3">
|
2026-05-10 01:22:17 +06:00
|
|
|
{(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>
|
|
|
|
|
)}
|
2026-04-22 01:02:45 +06:00
|
|
|
</td>
|
2026-05-10 01:22:17 +06:00
|
|
|
{/* <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> */}
|
2026-04-22 01:02:45 +06:00
|
|
|
<td className="px-4 py-3">
|
2026-05-10 01:22:17 +06:00
|
|
|
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${statusBadge.style}`}>
|
|
|
|
|
{statusBadge.label}
|
|
|
|
|
</span>
|
2026-04-22 01:02:45 +06:00
|
|
|
</td>
|
|
|
|
|
<td className="px-4 py-3">
|
2026-04-26 14:56:12 +06:00
|
|
|
<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>
|
2026-05-10 01:22:17 +06:00
|
|
|
|
|
|
|
|
{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>
|
2026-04-26 14:56:12 +06:00
|
|
|
)}
|
2026-05-10 01:22:17 +06:00
|
|
|
{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>
|
2026-04-26 14:56:12 +06:00
|
|
|
</div>
|
2026-04-22 01:02:45 +06:00
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-04-26 14:56:12 +06:00
|
|
|
|
|
|
|
|
{showCreateModal && (
|
|
|
|
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
2026-05-10 01:22:17 +06:00
|
|
|
<div className="bg-white rounded-xl shadow-xl w-full max-w-2xl max-h-[90vh] overflow-y-auto">
|
2026-04-26 14:56:12 +06:00
|
|
|
<div className="p-4 border-b border-slate-100 flex justify-between items-center">
|
2026-05-10 01:22:17 +06:00
|
|
|
<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">
|
2026-04-26 14:56:12 +06:00
|
|
|
<X className="w-5 h-5" />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
2026-05-10 01:22:17 +06:00
|
|
|
|
2026-04-26 14:56:12 +06:00
|
|
|
<div className="p-4 space-y-4">
|
2026-05-10 01:22:17 +06:00
|
|
|
{createStep === 1 && (
|
2026-04-26 14:56:12 +06:00
|
|
|
<div>
|
2026-05-10 01:22:17 +06:00
|
|
|
<h4 className="font-medium text-slate-700 mb-3">Step 1: Select User</h4>
|
|
|
|
|
<p className="text-sm text-slate-500 mb-3">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>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{createStep === 2 && (
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<h4 className="font-medium text-slate-700 mb-3">Step 2: Rental Details</h4>
|
|
|
|
|
<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, subscriptionType: 'daily' })}
|
|
|
|
|
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
|
2026-04-26 14:56:12 +06:00
|
|
|
>
|
2026-05-10 01:22:17 +06:00
|
|
|
<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">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"
|
2026-04-26 14:56:12 +06:00
|
|
|
>
|
2026-05-10 01:22:17 +06:00
|
|
|
{selectedSettings.contractMonths.map(m => (
|
|
|
|
|
<option key={m} value={m}>{m} Months</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
</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-slate-50 p-3 rounded-lg">
|
|
|
|
|
<p className="text-sm text-slate-600">Deposit: ৳{selectedSettings.deposit.toLocaleString()}</p>
|
|
|
|
|
<p className="text-sm text-slate-600">
|
|
|
|
|
Rate: ৳{newRental.subscriptionType === 'daily' ? selectedSettings.dailyRate : newRental.subscriptionType === 'weekly' ? selectedSettings.weeklyRate : selectedSettings.monthlyRate}/
|
|
|
|
|
{newRental.subscriptionType === 'daily' ? 'day' : newRental.subscriptionType === 'weekly' ? 'week' : 'month'}
|
|
|
|
|
</p>
|
2026-04-26 14:56:12 +06:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-05-10 01:22:17 +06:00
|
|
|
{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>
|
2026-04-26 14:56:12 +06:00
|
|
|
)}
|
2026-05-10 01:22:17 +06:00
|
|
|
</div>
|
|
|
|
|
)}
|
2026-04-26 14:56:12 +06:00
|
|
|
|
2026-05-10 01:22:17 +06:00
|
|
|
{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: ৳{selectedSettings.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>
|
|
|
|
|
{selectedSettings.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>
|
|
|
|
|
)}
|
|
|
|
|
{selectedSettings.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">৳{selectedSettings.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">৳{selectedSettings.deposit}</td>
|
|
|
|
|
</tr>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-04-26 14:56:12 +06:00
|
|
|
</div>
|
2026-05-10 01:22:17 +06:00
|
|
|
|
2026-04-26 14:56:12 +06:00
|
|
|
<div className="p-4 border-t border-slate-100 flex justify-between">
|
|
|
|
|
<div className="flex gap-2">
|
2026-05-10 01:22:17 +06:00
|
|
|
{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>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
{selectedSettings.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>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
2026-04-26 14:56:12 +06:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-04-22 01:02:45 +06:00
|
|
|
</div>
|
|
|
|
|
);
|
2026-05-10 01:22:17 +06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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>
|
|
|
|
|
);
|
2026-04-22 01:02:45 +06:00
|
|
|
}
|