feat: implement automated double-entry accounting for investments and add transaction details modal
This commit is contained in:
250
src/app/rental/[id]/page.tsx
Normal file
250
src/app/rental/[id]/page.tsx
Normal file
@@ -0,0 +1,250 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { Bike, User, Calendar, DollarSign, MapPin, Shield, Clock, CheckCircle } from 'lucide-react';
|
||||
|
||||
type RentalStatus = 'active' | 'pending' | 'completed' | 'disputed' | 'cancelled' | 'locked';
|
||||
type RentalType = 'single' | 'shared' | 'rent-to-own';
|
||||
|
||||
interface Rental {
|
||||
id: string;
|
||||
bikeId: string;
|
||||
userId: string;
|
||||
type: RentalType;
|
||||
status: RentalStatus;
|
||||
startDate: string;
|
||||
endDate?: string;
|
||||
contractMonths?: number;
|
||||
deposit: number;
|
||||
dailyRate: number;
|
||||
totalPaid: number;
|
||||
dueRental?: number;
|
||||
hubId?: string;
|
||||
hubName?: string;
|
||||
}
|
||||
|
||||
interface BikeInfo {
|
||||
id: string;
|
||||
model: string;
|
||||
plate: string;
|
||||
}
|
||||
|
||||
interface UserInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
phone: string;
|
||||
membership: string;
|
||||
}
|
||||
|
||||
const mockRentals: Rental[] = [
|
||||
{
|
||||
id: 'RNT-001',
|
||||
bikeId: 'BIKE-001',
|
||||
userId: 'USR-001',
|
||||
type: 'single',
|
||||
status: 'active',
|
||||
startDate: '2024-01-15',
|
||||
contractMonths: 6,
|
||||
endDate: '2024-07-15',
|
||||
deposit: 5000,
|
||||
dailyRate: 300,
|
||||
totalPaid: 81900,
|
||||
dueRental: 0,
|
||||
hubId: 'HUB-001',
|
||||
hubName: 'Gulshan Hub'
|
||||
},
|
||||
{
|
||||
id: 'RNT-002',
|
||||
bikeId: 'BIKE-002',
|
||||
userId: 'USR-002',
|
||||
type: 'single',
|
||||
status: 'pending',
|
||||
startDate: '2024-02-10',
|
||||
contractMonths: 12,
|
||||
endDate: '2025-02-10',
|
||||
deposit: 3000,
|
||||
dailyRate: 150,
|
||||
totalPaid: 3000,
|
||||
hubId: 'HUB-002',
|
||||
hubName: 'Banani Hub'
|
||||
}
|
||||
];
|
||||
|
||||
const mockBikes: Record<string, BikeInfo> = {
|
||||
'BIKE-001': { id: 'BIKE-001', model: 'AIMA Lightning', plate: 'Dhaka Metro Cha-9012' },
|
||||
'BIKE-002': { id: 'BIKE-002', model: 'Yadea DT3', plate: 'Dhaka Metro Ba-5521' },
|
||||
'BIKE-003': { id: 'BIKE-003', model: 'AIMA EM5', plate: 'Dhaka Metro Ko-1234' },
|
||||
};
|
||||
|
||||
const mockUsers: Record<string, UserInfo> = {
|
||||
'USR-001': { id: 'USR-001', name: 'Rahim Ahmed', phone: '+8801712345678', membership: 'vip' },
|
||||
'USR-002': { id: 'USR-002', name: 'Karim Hasan', phone: '+8801812345678', membership: 'standard' },
|
||||
'USR-003': { id: 'USR-003', name: 'Jamal Uddin', phone: '+8801912345678', membership: 'premium' },
|
||||
};
|
||||
|
||||
export default function PublicRentalPage() {
|
||||
const params = useParams();
|
||||
const id = params.id as string;
|
||||
|
||||
const [rental, setRental] = useState<Rental | null>(null);
|
||||
const [bike, setBike] = useState<BikeInfo | null>(null);
|
||||
const [user, setUser] = useState<UserInfo | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
const found = mockRentals.find(r => r.id === id);
|
||||
if (found) {
|
||||
setRental(found);
|
||||
setBike(mockBikes[found.bikeId] || null);
|
||||
setUser(mockUsers[found.userId] || null);
|
||||
}
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
if (!rental) {
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-100 flex items-center justify-center p-4">
|
||||
<div className="bg-white rounded-2xl shadow-xl p-8 max-w-md w-full text-center">
|
||||
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<Clock className="w-8 h-8 text-red-500" />
|
||||
</div>
|
||||
<h1 className="text-xl font-bold text-slate-800 mb-2">Rental Not Found</h1>
|
||||
<p className="text-slate-500">This rental ID is not valid or has been removed.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const statusColors = {
|
||||
active: 'bg-green-500',
|
||||
pending: 'bg-amber-500',
|
||||
completed: 'bg-blue-500',
|
||||
disputed: 'bg-red-500',
|
||||
cancelled: 'bg-slate-400',
|
||||
locked: 'bg-red-600',
|
||||
};
|
||||
|
||||
const isExpired = rental.endDate && new Date(rental.endDate) < new Date();
|
||||
const isActive = rental.status === 'active' && !isExpired;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-green-50 to-emerald-100 p-4">
|
||||
<div className="max-w-lg mx-auto">
|
||||
<div className="bg-white rounded-2xl shadow-xl overflow-hidden">
|
||||
<div className={`${statusColors[rental.status]} text-white p-6 text-center`}>
|
||||
<div className="flex items-center justify-center gap-2 mb-2">
|
||||
<CheckCircle className="w-6 h-6" />
|
||||
<span className="text-lg font-semibold uppercase">{rental.status}</span>
|
||||
</div>
|
||||
<p className="text-3xl font-bold">RENTAL ID: {rental.id}</p>
|
||||
{isExpired && <p className="text-sm mt-2 opacity-80">EXPIRED</p>}
|
||||
</div>
|
||||
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="bg-blue-50 rounded-xl p-4">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||
<Bike className="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-blue-600">Bike</p>
|
||||
<p className="font-semibold text-blue-800">{bike?.model}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-blue-600">Plate</span>
|
||||
<span className="font-medium text-blue-800">{bike?.plate}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-purple-50 rounded-xl p-4">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center">
|
||||
<User className="w-5 h-5 text-purple-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-purple-600">Renter</p>
|
||||
<p className="font-semibold text-purple-800">{user?.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-purple-600">Phone</span>
|
||||
<span className="font-medium text-purple-800">{user?.phone}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm mt-1">
|
||||
<span className="text-purple-600">Membership</span>
|
||||
<span className="font-medium text-purple-800 uppercase">{user?.membership}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 rounded-xl p-4">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center">
|
||||
<Calendar className="w-5 h-5 text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-green-600">Contract Period</p>
|
||||
<p className="font-semibold text-green-800">
|
||||
{rental.contractMonths ? `${rental.contractMonths} Months` : 'Daily Basis'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-green-600">Start Date</span>
|
||||
<span className="font-medium text-green-800">{rental.startDate}</span>
|
||||
</div>
|
||||
{rental.endDate && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-green-600">End Date</span>
|
||||
<span className="font-medium text-green-800">{rental.endDate}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between text-sm mt-2 pt-2 border-t border-green-200">
|
||||
<span className="text-green-600 font-medium">Daily Rate</span>
|
||||
<span className="font-bold text-green-700">৳{rental.dailyRate}/day</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-amber-50 rounded-xl p-4">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="w-10 h-10 bg-amber-100 rounded-lg flex items-center justify-center">
|
||||
<DollarSign className="w-5 h-5 text-amber-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-amber-600">Payment</p>
|
||||
<p className="font-semibold text-amber-800">Total Paid: ৳{rental.totalPaid.toLocaleString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-amber-600">Deposit Paid</span>
|
||||
<span className="font-medium text-amber-800">৳{rental.deposit.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-amber-600">Due</span>
|
||||
<span className="font-medium text-amber-800">৳{(rental.dueRental || 0).toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-50 rounded-xl p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-slate-200 rounded-lg flex items-center justify-center">
|
||||
<MapPin className="w-5 h-5 text-slate-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-slate-600">Hub</p>
|
||||
<p className="font-semibold text-slate-800">{rental.hubName || 'N/A'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t pt-4 text-center">
|
||||
<p className="text-xs text-slate-400">Verified by JAIBEN Mobility Ltd</p>
|
||||
<p className="text-xs text-slate-400">Generated from JAIBEN Rental System</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user