refactor: dynamic display of asset details and investment data based on investment type

This commit is contained in:
sazzadulalambd
2026-05-19 18:55:50 +06:00
parent f0d92f31ff
commit 646068dbe3
3 changed files with 173 additions and 76 deletions

View File

@@ -34,13 +34,6 @@ export default function InvestorInvestmentDetailPage({ params }: { params: Promi
const [activeTab, setActiveTab] = useState('overview'); const [activeTab, setActiveTab] = useState('overview');
const [showPaymentModal, setShowPaymentModal] = useState(false); const [showPaymentModal, setShowPaymentModal] = useState(false);
const paymentHistory: PaymentRecord[] = [
{ id: 'pay1', date: '2024-01-15', amount: Math.round((investment?.totalInvestment || 0) * 0.5), installmentNo: 1, type: 'installment', method: 'Bank Transfer', status: 'completed' },
{ id: 'pay2', date: '2024-02-15', amount: Math.round((investment?.totalInvestment || 0) * 0.25), installmentNo: null, type: 'partial', method: 'bKash', status: 'completed' },
];
if (!investment) { if (!investment) {
return ( return (
<div className="min-h-screen pb-20 lg:pb-0"> <div className="min-h-screen pb-20 lg:pb-0">
@@ -58,6 +51,20 @@ export default function InvestorInvestmentDetailPage({ params }: { params: Promi
); );
} }
const isBattery = investment.assetType === 'battery' || investment.planName?.toLowerCase().includes('battery');
const paymentHistory: PaymentRecord[] = [
{
id: `pay_${investment.id}`,
date: investment.startDate || '2024-02-01',
amount: investment.totalInvestment,
installmentNo: null,
type: 'full',
method: investment.paymentMethod === 'bank' ? 'Bank Transfer' : 'bKash',
status: 'completed'
}
];
const totalPaid = paymentHistory.reduce((sum, p) => p.status === 'completed' ? sum + p.amount : sum, 0); const totalPaid = paymentHistory.reduce((sum, p) => p.status === 'completed' ? sum + p.amount : sum, 0);
const dueAmount = investment.totalInvestment - totalPaid; const dueAmount = investment.totalInvestment - totalPaid;
@@ -69,24 +76,32 @@ export default function InvestorInvestmentDetailPage({ params }: { params: Promi
}; };
const style = planConfig[investment.planType] || planConfig.gold; const style = planConfig[investment.planType] || planConfig.gold;
const demoBikes = [ const bikeIds = investment.bikeIds || (investmentId === 'ip1' ? ['b1'] : investmentId === 'ip2' ? ['b2'] : []);
{ id: 'b1', model: 'Yadea DT3', brand: 'Yadea', plateNumber: 'AB-1234', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400&h=300&fit=crop', status: 'rented', currentRent: 450, totalEarnings: 45000, batteryLevel: 85, range: 60, location: 'Dhaka Central Hub' }, const batteryIds = investment.batteryIds || (isBattery ? ['BAT-001', 'BAT-002', 'BAT-005'] : []);
{ id: 'b2', model: 'Apex E-Bike Pro', brand: 'Apex', plateNumber: 'CD-5678', image: 'https://images.unsplash.com/photo-1622185135505-2d795043906a?w=400&h=300&fit=crop', status: 'rented', currentRent: 500, totalEarnings: 52000, batteryLevel: 72, range: 55, location: 'Gulshan Area' },
{ id: 'b3', model: 'Niu NQi Sport', brand: 'Niu', plateNumber: 'EF-9012', image: 'https://images.unsplash.com/photo-1591353230407-2b14a8e4c8d3?w=400&h=300&fit=crop', status: 'available', currentRent: 0, totalEarnings: 28000, batteryLevel: 95, range: 70, location: 'Mirpur Depot' },
{ id: 'b4', model: 'Yadea DT3', brand: 'Yadea', plateNumber: 'GH-3456', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400&h=300&fit=crop', status: 'maintenance', currentRent: 0, totalEarnings: 15000, batteryLevel: 0, range: 0, location: 'Service Center' },
];
const demoPnl = { grossRevenue: 185000, platformFee: 83250, insurance: 15000, maintenance: 8500, netProfit: 78250 }; const assignedBikes = [
{ id: 'b1', model: 'Etron ET50', brand: 'Etron', plateNumber: 'Dhaka Metro Cha-1234', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400&h=300&fit=crop', status: 'rented', currentRent: 350, totalEarnings: 114250, batteryLevel: 78, range: 60, location: 'Gulshan 1' },
{ id: 'b2', model: 'Yadea DT3', brand: 'Yadea', plateNumber: 'Dhaka Metro Cha-5678', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400&h=300&fit=crop', status: 'available', currentRent: 300, totalEarnings: 12750, batteryLevel: 95, range: 75, location: 'Banani' }
].filter(bike => bikeIds.includes(bike.id));
const demoTransactions = [ const assignedBatteries = [
{ id: 'tx1', date: '2024-05-15', description: 'Rental Income - Bike AB-1234', amount: 450, status: 'completed' }, { id: 'BAT-001', serialNumber: 'SN-2024-00001', brand: 'EVE Energy', model: 'Li-Ion 60V50Ah', status: 'In-Use', cycleCount: 156, soc: '78% / 95%', earnings: 4500, batteryLevel: 78, location: 'Dhaka Central Hub' },
{ id: 'tx2', date: '2024-05-14', description: 'Rental Income - Bike CD-5678', amount: 500, status: 'completed' }, { id: 'BAT-002', serialNumber: 'SN-2024-00002', brand: 'EVE Energy', model: 'Li-Ion 60V50Ah', status: 'In-Use', cycleCount: 142, soc: '82% / 96%', earnings: 4500, batteryLevel: 82, location: 'Dhaka Central Hub' },
{ id: 'tx3', date: '2024-05-13', description: 'Rental Income - Bike AB-1234', amount: 450, status: 'completed' }, { id: 'BAT-005', serialNumber: 'SN-2024-00005', brand: 'EVE Energy', model: 'Li-Ion 60V50Ah', status: 'Available', cycleCount: 88, soc: '91% / 98%', earnings: 0, batteryLevel: 91, location: 'Dhaka Central Hub' }
{ id: 'tx4', date: '2024-05-12', description: 'Rental Income - Bike EF-9012', amount: 350, status: 'completed' }, ].filter(bat => batteryIds.includes(bat.id));
{ id: 'tx5', date: '2024-05-11', description: 'Rental Income - Bike CD-5678', amount: 500, status: 'completed' },
{ id: 'tx6', date: '2024-05-10', description: 'Withdrawal to Bank', amount: -10000, status: 'completed' }, const demoPnl = isBattery ? { grossRevenue: 22500, platformFee: 13500, insurance: 0, maintenance: 0, netProfit: 9000 } : { grossRevenue: 185000, platformFee: 83250, insurance: 15000, maintenance: 8500, netProfit: 78250 };
{ id: 'tx7', date: '2024-05-09', description: 'Rental Income - Bike AB-1234', amount: 450, status: 'completed' },
{ id: 'tx8', date: '2024-05-08', description: 'Rental Income - Bike CD-5678', amount: 500, status: 'completed' }, const demoTransactions = isBattery ? [
{ id: 'tx-bat-1', date: '2024-03-01', description: 'Monthly Yield Share - BAT-001', amount: 4500, status: 'completed' },
{ id: 'tx-bat-2', date: '2024-02-15', description: 'Monthly Yield Share - BAT-002', amount: 4500, status: 'completed' },
{ id: 'tx-bat-funded', date: investment.startDate || '2024-02-01', description: 'Investment Funded - Standard Battery Plan', amount: investment.totalInvestment, status: 'completed' }
] : [
{ id: 'tx1', date: '2024-05-15', description: 'Rental Income - Bike Dhaka Metro Cha-1234', amount: 350, status: 'completed' },
{ id: 'tx2', date: '2024-05-14', description: 'Rental Income - Bike Dhaka Metro Cha-5678', amount: 300, status: 'completed' },
{ id: 'tx3', date: '2024-05-13', description: 'Rental Income - Bike Dhaka Metro Cha-1234', amount: 350, status: 'completed' },
{ id: 'tx4', date: '2024-05-12', description: 'Rental Income - Bike Dhaka Metro Cha-5678', amount: 300, status: 'completed' },
{ id: 'tx-funded', date: investment.startDate || '2024-01-15', description: `Investment Funded - ${investment.planName}`, amount: investment.totalInvestment, status: 'completed' }
]; ];
const handlePaymentSubmit = () => { const handlePaymentSubmit = () => {
@@ -174,12 +189,18 @@ export default function InvestorInvestmentDetailPage({ params }: { params: Promi
<div className="bg-white rounded-xl p-4 lg:p-5 border border-slate-200 shadow-sm"> <div className="bg-white rounded-xl p-4 lg:p-5 border border-slate-200 shadow-sm">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<div className="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center"> <div className={`w-8 h-8 rounded-lg flex items-center justify-center ${isBattery ? 'bg-emerald-100' : 'bg-blue-100'}`}>
<Bike className="w-4 h-4 text-blue-600" /> {isBattery ? (
<Battery className="w-4 h-4 text-emerald-600" />
) : (
<Bike className="w-4 h-4 text-blue-600" />
)}
</div> </div>
<p className="text-xs text-slate-500 font-medium">Bikes</p> <p className="text-xs text-slate-500 font-medium">{isBattery ? 'Batteries' : 'Bikes'}</p>
</div> </div>
<p className="text-xl lg:text-2xl font-bold text-blue-600">{demoBikes.length}</p> <p className={`text-xl lg:text-2xl font-bold ${isBattery ? 'text-emerald-600' : 'text-blue-600'}`}>
{isBattery ? assignedBatteries.length : assignedBikes.length}
</p>
</div> </div>
</div> </div>
@@ -188,7 +209,7 @@ export default function InvestorInvestmentDetailPage({ params }: { params: Promi
<div className="flex overflow-x-auto border-b border-slate-100 justify-between sm:justify-start px-4 lg:px-0"> <div className="flex overflow-x-auto border-b border-slate-100 justify-between sm:justify-start px-4 lg:px-0">
{[ {[
{ key: 'overview', label: 'Overview', icon: FileText, count: null }, { key: 'overview', label: 'Overview', icon: FileText, count: null },
{ key: 'bikes', label: 'Bikes', icon: Bike, count: demoBikes.length }, { key: 'bikes', label: isBattery ? 'Batteries' : 'Bikes', icon: isBattery ? Battery : Bike, count: isBattery ? assignedBatteries.length : assignedBikes.length },
{ key: 'transactions', label: 'Transactions', icon: CreditCard, count: demoTransactions.length }, { key: 'transactions', label: 'Transactions', icon: CreditCard, count: demoTransactions.length },
{ key: 'statement', label: 'Statement', icon: Receipt, count: null }, { key: 'statement', label: 'Statement', icon: Receipt, count: null },
].map((tab) => { ].map((tab) => {
@@ -257,59 +278,103 @@ export default function InvestorInvestmentDetailPage({ params }: { params: Promi
</div> </div>
</div> </div>
<div className="bg-amber-50 border border-amber-200 rounded-xl p-5"> {isBattery ? (
<h4 className="font-semibold text-amber-800 mb-4 flex items-center gap-2"> <div className="bg-emerald-50 border border-emerald-200 rounded-xl p-5">
<Percent className="w-4 h-4 text-amber-600" /> Profit Sharing <h4 className="font-semibold text-emerald-800 mb-4 flex items-center gap-2">
</h4> <Percent className="w-4 h-4 text-emerald-600" /> Profit Sharing
<p className="text-xs text-amber-600 mb-4 font-medium">Your share based on rental model</p> </h4>
<div className="grid grid-cols-3 gap-3"> <p className="text-xs text-emerald-600 mb-4 font-medium">Profit sharing ratio when batteries are utilized</p>
<div className="bg-white rounded-lg p-3 text-center border border-amber-200"> <div className="max-w-xs">
<p className="text-xs text-slate-500 mb-1 font-medium">Single Rent</p> <div className="bg-white rounded-lg p-3 text-center border border-emerald-200">
<p className="text-xl font-bold text-slate-800">55%</p> <p className="text-xs text-slate-500 mb-1 font-medium">Profit Share</p>
</div> <p className="text-xl font-bold text-slate-800">40%</p>
<div className="bg-white rounded-lg p-3 text-center border border-amber-200"> </div>
<p className="text-xs text-slate-500 mb-1 font-medium">Rent to Own</p>
<p className="text-xl font-bold text-slate-800">45%</p>
</div>
<div className="bg-white rounded-lg p-3 text-center border border-amber-200">
<p className="text-xs text-slate-500 mb-1 font-medium">Share EV</p>
<p className="text-xl font-bold text-slate-800">40%</p>
</div> </div>
</div> </div>
</div> ) : (
<div className="bg-amber-50 border border-amber-200 rounded-xl p-5">
<h4 className="font-semibold text-amber-800 mb-4 flex items-center gap-2">
<Percent className="w-4 h-4 text-amber-600" /> Profit Sharing
</h4>
<p className="text-xs text-amber-600 mb-4 font-medium">Your share based on rental model</p>
<div className="grid grid-cols-3 gap-3">
<div className="bg-white rounded-lg p-3 text-center border border-amber-200">
<p className="text-xs text-slate-500 mb-1 font-medium">Single Rent</p>
<p className="text-xl font-bold text-slate-800">40%</p>
</div>
<div className="bg-white rounded-lg p-3 text-center border border-amber-200">
<p className="text-xs text-slate-500 mb-1 font-medium">Rent to Own</p>
<p className="text-xl font-bold text-slate-800">50%</p>
</div>
<div className="bg-white rounded-lg p-3 text-center border border-amber-200">
<p className="text-xs text-slate-500 mb-1 font-medium">Share EV</p>
<p className="text-xl font-bold text-slate-800">55%</p>
</div>
</div>
</div>
)}
</div> </div>
)} )}
{activeTab === 'bikes' && ( {activeTab === 'bikes' && (
<div className="space-y-4"> isBattery ? (
<p className="text-sm text-slate-500">{demoBikes.length} bikes assigned to this investment</p> <div className="space-y-4">
{demoBikes.map((bike) => ( <p className="text-sm text-slate-500">{assignedBatteries.length} battery pack{assignedBatteries.length !== 1 ? 's' : ''} assigned to this investment</p>
<div key={bike.id} className="p-4 bg-white rounded-xl border border-slate-200 flex flex-col md:flex-row items-start gap-4 hover:border-investor/30 transition-colors"> {assignedBatteries.map((battery) => (
<div className="w-24 h-20 bg-slate-100 rounded-lg overflow-hidden shrink-0"> <div key={battery.id} className="p-4 bg-white rounded-xl border border-slate-200 flex flex-col md:flex-row items-start gap-4 hover:border-emerald-500/30 transition-colors">
<img src={bike.image} alt={bike.model} className="w-full h-full object-cover" /> <div className="w-16 h-16 bg-emerald-50 rounded-xl flex items-center justify-center shrink-0 border border-emerald-100">
</div> <Battery className="w-8 h-8 text-emerald-600 animate-pulse" />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h5 className="font-semibold text-slate-800">{bike.model}</h5>
<span className={`px-2 py-0.5 rounded text-[10px] font-bold uppercase ${bike.status === 'rented' ? 'bg-green-100 text-green-700' : bike.status === 'available' ? 'bg-blue-100 text-blue-700' : 'bg-red-100 text-red-700'}`}>{bike.status}</span>
</div> </div>
<p className="text-sm text-slate-500">{bike.plateNumber} {bike.brand}</p> <div className="flex-1 min-w-0">
<div className="mt-2 flex flex-wrap gap-3 text-xs text-slate-500"> <div className="flex items-center gap-2 mb-1">
<span className="flex items-center gap-1"><Battery className="w-3 h-3" /> {bike.batteryLevel}%</span> <h5 className="font-semibold text-slate-800">{battery.model}</h5>
<span className="flex items-center gap-1"><Gauge className="w-3 h-3" /> {bike.range} km</span> <span className={`px-2 py-0.5 rounded text-[10px] font-bold uppercase ${battery.status === 'In-Use' ? 'bg-green-100 text-green-700' : 'bg-blue-100 text-blue-700'}`}>{battery.status}</span>
<span className="flex items-center gap-1"><MapPin className="w-3 h-3" /> {bike.location}</span> </div>
<p className="text-sm text-slate-500">{battery.serialNumber} {battery.brand}</p>
<div className="mt-2 flex flex-wrap gap-3 text-xs text-slate-500">
<span className="flex items-center gap-1">Cycle Count: {battery.cycleCount}</span>
<span className="flex items-center gap-1">SoC / Health: {battery.soc}</span>
<span className="flex items-center gap-1"><MapPin className="w-3 h-3" /> {battery.location}</span>
</div>
</div>
<div className="text-right shrink-0">
<p className="text-xs text-slate-500 mb-1">Est. Monthly Return</p>
<p className="text-lg font-bold text-slate-800">{(investment.monthlyReturn / assignedBatteries.length).toLocaleString()}</p>
<p className="text-xs text-slate-400 mt-1">Total: {battery.earnings.toLocaleString()}</p>
</div> </div>
</div> </div>
<div className="text-right shrink-0"> ))}
<p className="text-xs text-slate-500 mb-1">Daily Rent</p> </div>
<p className="text-lg font-bold text-slate-800">{bike.currentRent}</p> ) : (
<p className="text-xs text-slate-400 mt-1">Total: {bike.totalEarnings.toLocaleString()}</p> <div className="space-y-4">
<p className="text-sm text-slate-500">{assignedBikes.length} bike{assignedBikes.length !== 1 ? 's' : ''} assigned to this investment</p>
{assignedBikes.map((bike) => (
<div key={bike.id} className="p-4 bg-white rounded-xl border border-slate-200 flex flex-col md:flex-row items-start gap-4 hover:border-investor/30 transition-colors">
<div className="w-24 h-20 bg-slate-100 rounded-lg overflow-hidden shrink-0">
<img src={bike.image} alt={bike.model} className="w-full h-full object-cover" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h5 className="font-semibold text-slate-800">{bike.model}</h5>
<span className={`px-2 py-0.5 rounded text-[10px] font-bold uppercase ${bike.status === 'rented' ? 'bg-green-100 text-green-700' : bike.status === 'available' ? 'bg-blue-100 text-blue-700' : 'bg-red-100 text-red-700'}`}>{bike.status}</span>
</div>
<p className="text-sm text-slate-500">{bike.plateNumber} {bike.brand}</p>
<div className="mt-2 flex flex-wrap gap-3 text-xs text-slate-500">
<span className="flex items-center gap-1"><Battery className="w-3 h-3" /> {bike.batteryLevel}%</span>
<span className="flex items-center gap-1"><MapPin className="w-3 h-3" /> {bike.location}</span>
</div>
</div>
<div className="text-right shrink-0">
<p className="text-xs text-slate-500 mb-1">Est. Monthly Return</p>
<p className="text-lg font-bold text-slate-800">{bike.currentRent}</p>
<p className="text-xs text-slate-400 mt-1">Total: {bike.totalEarnings.toLocaleString()}</p>
</div>
</div> </div>
</div> ))}
))} </div>
</div> )
)} )}
{activeTab === 'statement' && ( {activeTab === 'statement' && (
@@ -322,12 +387,21 @@ export default function InvestorInvestmentDetailPage({ params }: { params: Promi
<span className="text-xl font-bold text-slate-800">{demoPnl.grossRevenue.toLocaleString()}</span> <span className="text-xl font-bold text-slate-800">{demoPnl.grossRevenue.toLocaleString()}</span>
</div> </div>
<div className="space-y-2 py-2"> <div className="space-y-2 py-2">
<div className="flex justify-between text-sm"><span className="text-slate-500">Platform Fee (45%)</span><span className="font-medium text-red-500">-{demoPnl.platformFee.toLocaleString()}</span></div> <div className="flex justify-between text-sm">
<div className="flex justify-between text-sm"><span className="text-slate-500">Insurance Coverage</span><span className="font-medium text-slate-600">-{demoPnl.insurance.toLocaleString()}</span></div> <span className="text-slate-500">Platform Fee {isBattery ? '(60%)' : '(45%)'}</span>
<div className="flex justify-between text-sm"><span className="text-slate-500">Maintenance</span><span className="font-medium text-slate-600">-{demoPnl.maintenance.toLocaleString()}</span></div> <span className="font-medium text-red-500">-{demoPnl.platformFee.toLocaleString()}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-slate-500">Insurance Coverage</span>
<span className="font-medium text-slate-600">-{demoPnl.insurance.toLocaleString()}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-slate-500">Maintenance</span>
<span className="font-medium text-slate-600">-{demoPnl.maintenance.toLocaleString()}</span>
</div>
</div> </div>
<div className="flex justify-between items-center pt-4 border-t-2 border-slate-300"> <div className="flex justify-between items-center pt-4 border-t-2 border-slate-300">
<span className="text-lg font-semibold text-slate-800">Your Share (55%)</span> <span className="text-lg font-semibold text-slate-800">Your Share {isBattery ? '(40%)' : '(55%)'}</span>
<span className="text-2xl font-bold text-green-600">{demoPnl.netProfit.toLocaleString()}</span> <span className="text-2xl font-bold text-green-600">{demoPnl.netProfit.toLocaleString()}</span>
</div> </div>
</div> </div>

View File

@@ -117,17 +117,37 @@ export default function MyInvestmentsPage() {
</div> </div>
</div> </div>
<div className="bg-slate-50 rounded-lg p-3 space-y-1.5"> <div className="bg-slate-50 rounded-lg p-3 space-y-1.5">
<div className="flex justify-between text-xs">
<span className="text-slate-400">Assigned Assets</span>
<span className="font-semibold text-slate-800">
{inv.planName.toLowerCase().includes('battery') || inv.assetType === 'battery' ? (
`${inv.batteryIds?.length || 3} Batteries`
) : (
`${inv.bikeIds?.length || 1} Bike${(inv.bikeIds?.length || 1) > 1 ? 's' : ''}`
)}
</span>
</div>
<div className="flex justify-between text-xs"> <div className="flex justify-between text-xs">
<span className="text-slate-400">Duration</span> <span className="text-slate-400">Duration</span>
<span className="font-medium">12 months</span> <span className="font-medium">{inv.durationMonths || 12} months</span>
</div> </div>
<div className="flex justify-between text-xs"> <div className="flex justify-between text-xs">
<span className="text-slate-400">Lock-in Period</span> <span className="text-slate-400">Lock-in Period</span>
<span className="font-medium">3 months</span> <span className="font-medium">{inv.lockInMonths || 3} months</span>
</div> </div>
<div className="flex justify-between text-xs"> <div className="flex justify-between text-xs">
<span className="text-slate-400">Early Exit Penalty</span> <span className="text-slate-400">Early Exit Penalty</span>
<span className="font-medium text-red-500">10%</span> <span className="font-medium text-red-500">{inv.exitPenaltyPercent || 10}%</span>
</div>
<div className="flex justify-between text-xs border-t border-slate-200/60 pt-1.5">
<span className="text-slate-400">Profit Sharing</span>
<span className="font-semibold text-emerald-600">
{inv.planName.toLowerCase().includes('battery') || inv.assetType === 'battery' ? (
'40%'
) : (
'40% / 50% / 55%'
)}
</span>
</div> </div>
</div> </div>
<div className="flex items-center justify-between text-sm"> <div className="flex items-center justify-between text-sm">

View File

@@ -109,6 +109,9 @@ export interface InvestmentPlan {
paymentMethod: 'bank' | 'mobile' | 'cash' | 'cheque'; paymentMethod: 'bank' | 'mobile' | 'cash' | 'cheque';
transactionId?: string; transactionId?: string;
notes?: string; notes?: string;
durationMonths?: number;
lockInMonths?: number;
exitPenaltyPercent?: number;
createdAt: string; createdAt: string;
} }