feat: implement automated double-entry accounting for investments and add transaction details modal

This commit is contained in:
sazzadulalambd
2026-04-26 14:56:12 +06:00
parent ae94ce0427
commit 7457b997ef
16 changed files with 8809 additions and 201 deletions

View 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>
);
}