diff --git a/src/app/admin/investors/[id]/page.tsx b/src/app/admin/investors/[id]/page.tsx index af3ad23..48abb48 100644 --- a/src/app/admin/investors/[id]/page.tsx +++ b/src/app/admin/investors/[id]/page.tsx @@ -1,16 +1,16 @@ 'use client'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import Link from 'next/link'; import { useParams, useRouter } from 'next/navigation'; import { investors as initialInvestors, bikes as initialBikes, transactions as initialTransactions, rentalPayments as initialRentalPayments } from '@/data/mockData'; import type { Investor } from '@/data/mockData'; import toast from 'react-hot-toast'; import { - ArrowLeft, Wallet, TrendingUp, Banknote, Calendar, Phone, Mail, MapPin, Edit, Trash2, Plus, X, Bike, + ArrowLeft, Wallet, TrendingUp, Banknote, Calendar, Phone, Mail, MapPin, Edit, Trash2, Plus, X, Bike, Battery, Unlink, Eye, ShieldAlert, Cpu, User, FileText, CreditCard, DollarSign, Clock, ChevronDown, ExternalLink, Download, Upload, AlertTriangle, Shield, Star, CheckCircle, XCircle, Search, Filter, BookOpen, ArrowRight, Printer, - UserCircle, Home, Briefcase, CreditCardIcon, Heart, PhoneCall, PhoneOutgoing, MessageSquare, Save, + UserCircle, Home, Briefcase, Heart, PhoneCall, PhoneOutgoing, MessageSquare, Save, ShieldCheck, Building2, Users, Check, AlertOctagon, Activity, Award, Camera, History, Settings } from 'lucide-react'; @@ -75,9 +75,24 @@ export default function InvestorDetailPage() { const router = useRouter(); const investorId = params.id as string; - const [investors] = useState(initialInvestors); + const [investors, setInvestors] = useState(initialInvestors); const investor = investors.find(i => i.id === investorId); + const [settings, setSettings] = useState(null); + + useEffect(() => { + if (typeof window !== 'undefined') { + const stored = localStorage.getItem('companySettings'); + if (stored) { + try { + setSettings(JSON.parse(stored)); + } catch (e) { + console.error(e); + } + } + } + }, []); + const assignedBikes = initialBikes.filter(b => b.investorId === investorId); // Investor transactions are filtered below @@ -106,6 +121,89 @@ export default function InvestorDetailPage() { const [editingTax, setEditingTax] = useState({ tinNumber: '', passportNumber: '' }); const [newDoc, setNewDoc] = useState({ type: 'nid', number: '', url: '' }); const [editingMobileIndex, setEditingMobileIndex] = useState(null); + + const [batteries, setBatteries] = useState([ + { + id: 'BAT-001', + serialNumber: 'SN-2024-00001', + brand: 'EVE Energy', + model: 'Li-Ion 60V50Ah', + type: 'lithium-ion', + capacity: 50, + voltage: 60, + purchaseDate: '2024-01-15', + purchasePrice: 45000, + deposit: 5000, + rentPrice: 150, + investorId: investorId, + investorName: investor?.name || 'Md. Hasan Mahmud', + investorSharePercentage: 60, + investedAmount: 45000, + investmentId: 'ip1', + status: 'in-use', + currentSoc: 78, + health: 95, + cycleCount: 156 + }, + { + id: 'BAT-002', + serialNumber: 'SN-2024-00002', + brand: 'CATL', + model: 'LiFePO4 48V40Ah', + type: 'lifepo4', + capacity: 40, + voltage: 48, + purchaseDate: '2024-02-10', + purchasePrice: 38000, + deposit: 4000, + rentPrice: 120, + investorId: investorId, + investorName: investor?.name || 'Md. Hasan Mahmud', + investorSharePercentage: 100, + investedAmount: 38000, + investmentId: 'ip2', + status: 'available', + currentSoc: 92, + health: 98, + cycleCount: 45 + } + ]); + + const [unassignedBatteries, setUnassignedBatteries] = useState([ + { id: 'BAT-003', serialNumber: 'SN-2024-00003', brand: 'BYD', model: 'Li-Ion 72V60Ah', type: 'lithium-ion', capacity: 60, voltage: 72, purchasePrice: 52000, deposit: 6000, rentPrice: 180, status: 'available', currentSoc: 85, health: 97, cycleCount: 12 }, + { id: 'BAT-004', serialNumber: 'SN-2024-00004', brand: 'Panasonic', model: 'Li-Ion 60V40Ah', type: 'lithium-ion', capacity: 40, voltage: 60, purchasePrice: 41000, deposit: 4500, rentPrice: 130, status: 'available', currentSoc: 90, health: 99, cycleCount: 8 } + ]); + + const [showAssignBatteryModal, setShowAssignBatteryModal] = useState(false); + const [showRegisterBatteryModal, setShowRegisterBatteryModal] = useState(false); + const [showEditBatteryModal, setShowEditBatteryModal] = useState(false); + + const [selectedBatteryToAssign, setSelectedBatteryToAssign] = useState(null); + const [selectedBatteryToEdit, setSelectedBatteryToEdit] = useState(null); + + const [assignForm, setAssignForm] = useState({ + batteryId: '', + investmentId: '', + deposit: 5000, + rentPrice: 150, + investorShare: 60, + investedAmount: 45000 + }); + + const [registerForm, setRegisterForm] = useState({ + serialNumber: '', + brand: 'BYD', + model: 'Li-Ion 60V50Ah', + type: 'lithium-ion', + capacity: 50, + voltage: 60, + purchasePrice: 45000, + deposit: 5000, + rentPrice: 150, + investorShare: 100, + investedAmount: 45000, + investmentId: '' + }); const investorTransactions = initialTransactions.filter(t => t.investorId === investorId); const investorRentalPayments = initialRentalPayments.filter(p => p.investorId === investorId).sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); const [rentalPage, setRentalPage] = useState(1); @@ -131,7 +229,9 @@ export default function InvestorDetailPage() { const [newInvestment, setNewInvestment] = useState({ planName: '', planType: 'gold' as 'silver' | 'gold' | 'platinum' | 'diamond', + assetType: 'bike' as 'bike' | 'battery', selectedBikeIds: [] as string[], + selectedBatteryIds: [] as string[], totalInvestment: 0, paidAmount: 0, paymentType: 'full' as 'full' | 'partial', @@ -211,26 +311,42 @@ export default function InvestorDetailPage() { paymentMethod: newInvestment.paymentMethod }; - setInvestorJournals([journalEntry, ...investorJournals]); - - setLastCreatedInvestment({ + const createdInv = { id: invId, investorId: investor.id, ...newInvestment, + bikeIds: [], + batteryIds: [], actualEarnings: 0, - status: 'active', + status: 'active' as const, transactionId: transactionRef, createdAt: new Date().toISOString(), debitAccount, journalEntry - }); + }; + + setInvestorJournals([journalEntry, ...investorJournals]); + setLastCreatedInvestment(createdInv); + + setInvestors(prev => prev.map(inv => { + if (inv.id === investor.id) { + return { + ...inv, + investments: [...(inv.investments || []), createdInv], + totalInvested: inv.totalInvested + newInvestment.totalInvestment + }; + } + return inv; + })); setShowCreateInvestmentModal(false); setShowInvestmentSuccessModal(true); setNewInvestment({ planName: '', planType: 'gold', + assetType: 'bike', selectedBikeIds: [], + selectedBatteryIds: [], totalInvestment: 0, paidAmount: 0, paymentType: 'full', @@ -244,6 +360,149 @@ export default function InvestorDetailPage() { }); }; + const handleUnassignBattery = (batteryId: string) => { + const batteryToUnassign = batteries.find(b => b.id === batteryId); + if (!batteryToUnassign) return; + + setBatteries(prev => prev.filter(b => b.id !== batteryId)); + setUnassignedBatteries(prev => [ + ...prev, + { + ...batteryToUnassign, + investorId: null, + investorName: null, + investorSharePercentage: null, + investedAmount: null, + investmentId: null, + status: 'available' + } + ]); + toast.success('Battery successfully unassigned from investor!'); + }; + + const handleAssignBattery = () => { + if (!assignForm.batteryId) { + toast.error('Please select a battery'); + return; + } + const batteryToAssign = unassignedBatteries.find(b => b.id === assignForm.batteryId); + if (!batteryToAssign) return; + + const assignedObj = { + ...batteryToAssign, + investorId: investorId, + investorName: investor.name, + investorSharePercentage: Number(assignForm.investorShare), + investedAmount: Number(assignForm.investedAmount), + investmentId: assignForm.investmentId, + deposit: Number(assignForm.deposit), + rentPrice: Number(assignForm.rentPrice), + status: 'in-use' + }; + + setBatteries(prev => [...prev, assignedObj]); + setUnassignedBatteries(prev => prev.filter(b => b.id !== assignForm.batteryId)); + setShowAssignBatteryModal(false); + toast.success('Battery assigned to investment plan!'); + }; + + const handleRegisterAndAssignBattery = () => { + if (!registerForm.serialNumber) { + toast.error('Please enter a serial number'); + return; + } + + const newBat = { + id: `BAT-${Date.now()}`, + serialNumber: registerForm.serialNumber, + brand: registerForm.brand, + model: registerForm.model, + type: registerForm.type, + capacity: Number(registerForm.capacity), + voltage: Number(registerForm.voltage), + purchaseDate: new Date().toISOString().split('T')[0], + purchasePrice: Number(registerForm.purchasePrice), + deposit: Number(registerForm.deposit), + rentPrice: Number(registerForm.rentPrice), + investorId: investorId, + investorName: investor.name, + investorSharePercentage: Number(registerForm.investorShare), + investedAmount: Number(registerForm.investedAmount), + investmentId: registerForm.investmentId, + status: 'in-use', + currentSoc: 100, + health: 100, + cycleCount: 0 + }; + + setBatteries(prev => [...prev, newBat]); + setShowRegisterBatteryModal(false); + toast.success('New battery registered and assigned!'); + }; + + const handleSaveEditBattery = () => { + if (!selectedBatteryToEdit) return; + + setBatteries(prev => prev.map(b => { + if (b.id === selectedBatteryToEdit.id) { + return { + ...b, + deposit: Number(assignForm.deposit), + rentPrice: Number(assignForm.rentPrice), + investorSharePercentage: Number(assignForm.investorShare), + investedAmount: Number(assignForm.investedAmount), + investmentId: assignForm.investmentId + }; + } + return b; + })); + + setShowEditBatteryModal(false); + setSelectedBatteryToEdit(null); + toast.success('Battery assignment updated successfully!'); + }; + + const bikeTemplates = settings?.plans?.investment?.map((plan: any) => ({ + id: plan.id, + name: plan.name, + tier: plan.tier || 'Standard', + evBasePrice: plan.evBasePrice || 200000, + minQuantity: plan.minQuantity || 1, + duration: plan.durationMonths || 12, + maxInvestment: plan.maxInvestment || 1000000, + lockIn: plan.lockInMonths || 3, + exitPenalty: plan.earlyExitPenalty || 10, + profitShareSingle: plan.ficoSingleRent || 45, + profitShareOwn: plan.ficoRentToOwn || 55, + profitShareEV: plan.ficoShareEv || 60 + })) || [ + { id: 'inv_demo_1', name: '1 Bike Plan', tier: 'Economy', evBasePrice: 200000, minQuantity: 1, duration: 12, maxInvestment: 1000000, lockIn: 3, exitPenalty: 10, profitShareSingle: 45, profitShareOwn: 55, profitShareEV: 60 }, + { id: 'inv_demo_2', name: '5 Bike Plan', tier: 'Standard', evBasePrice: 180000, minQuantity: 5, duration: 24, maxInvestment: 5000000, lockIn: 6, exitPenalty: 15, profitShareSingle: 50, profitShareOwn: 60, profitShareEV: 65 } + ]; + + const batteryTemplates = settings?.plans?.batteryInvestment?.map((plan: any) => ({ + id: plan.id, + name: plan.name, + tier: plan.tier || 'Standard', + evBasePrice: plan.batteryBasePrice || 15000, + minQuantity: plan.minQuantity || 10, + duration: plan.durationMonths || 12, + maxInvestment: plan.maxInvestment || 500000, + lockIn: plan.lockInMonths || 3, + exitPenalty: plan.earlyExitPenalty || 10, + profitSharePercent: plan.profitSharePercent || 40, + profitShareSingle: plan.profitSharePercent || 40, + profitShareOwn: plan.profitSharePercent || 40, + profitShareEV: plan.profitSharePercent || 40 + })) || [ + { id: 'bat_demo_1', name: '1 Battery Pack Plan', tier: 'Silver', evBasePrice: 45000, minQuantity: 1, duration: 12, maxInvestment: 500000, lockIn: 3, exitPenalty: 8, profitSharePercent: 40, profitShareSingle: 40, profitShareOwn: 40, profitShareEV: 40 }, + { id: 'bat_demo_2', name: '5 Battery Pack Plan', tier: 'Gold', evBasePrice: 42000, minQuantity: 5, duration: 18, maxInvestment: 2000000, lockIn: 4, exitPenalty: 10, profitSharePercent: 48, profitShareSingle: 48, profitShareOwn: 48, profitShareEV: 48 }, + { id: 'bat_demo_3', name: '10 Battery Pack Fleet', tier: 'Platinum', evBasePrice: 40000, minQuantity: 10, duration: 24, maxInvestment: 5000000, lockIn: 6, exitPenalty: 12, profitSharePercent: 55, profitShareSingle: 55, profitShareOwn: 55, profitShareEV: 55 } + ]; + + const allTemplates = [...bikeTemplates, ...batteryTemplates]; + const activeTemplate = allTemplates.find(t => t.name === newInvestment.planName) || (newInvestment.assetType === 'battery' ? batteryTemplates[0] : bikeTemplates[0]); + return (
+
)} + {activeTab === 'batteries' && ( +
+
+
+

+ + Assigned Batteries +

+

+ {batteries.filter(b => b.investorId === investorId).length} battery packs assigned to this partner +

+
+
+ + +
+
+ +
+ {batteries.filter(b => b.investorId === investorId).map(battery => { + const investment = investor.investments?.find((inv: any) => inv.id === battery.investmentId); + const dailyPayout = Math.floor(battery.rentPrice * (battery.investorSharePercentage / 100)); + + const statusConfig: Record = { + 'in-use': { bg: 'bg-green-50 text-green-700 border-green-200', text: 'In Use', dot: 'bg-green-500' }, + 'available': { bg: 'bg-blue-50 text-blue-700 border-blue-200', text: 'Available', dot: 'bg-blue-500' }, + 'maintenance': { bg: 'bg-amber-50 text-amber-700 border-amber-200', text: 'Maintenance', dot: 'bg-amber-500' }, + }; + const status = statusConfig[battery.status] || { bg: 'bg-slate-50 text-slate-700 border-slate-200', text: battery.status || 'Unknown', dot: 'bg-slate-500' }; + + return ( +
+
+
+ {/* Card Header */} +
+
+ + {battery.serialNumber} + + + + {status.text} + +
+

{battery.brand} • {battery.model}

+

ID: {battery.id}

+
+ + Plan: {investment?.planName || 'Unlinked Plan'} + +
+
+ + {/* Co-ownership Payout details */} +
+
+ Refundable Deposit + ৳{battery.deposit?.toLocaleString()} +
+
+ Daily Rent Price + ৳{battery.rentPrice}/day +
+
+ Your Share % + {battery.investorSharePercentage}% +
+
+ Capital Contribution + ৳{battery.investedAmount?.toLocaleString()} +
+
+ Your Daily Payout + ৳{dailyPayout}/day +
+
+ + {/* BMS parameters */} +
+
+

BMS SOC

+
+ + {battery.currentSoc}% +
+
+
+

SOH Health

+

{battery.health}%

+
+
+

Cycles

+

{battery.cycleCount}

+
+
+ + {/* Actions block */} +
+ +
+ + +
+
+
+
+ ); + })} + + {batteries.filter(b => b.investorId === investorId).length === 0 && ( +
+ +

No Batteries Assigned

+

Link high-yield battery pack assets to this partner

+ +
+ )} +
+
+ )} + {activeTab === 'investments' && (
@@ -2170,6 +2642,398 @@ export default function InvestorDetailPage() {
)} + {showAssignBatteryModal && ( +
+
+
+

+ + Assign Battery to Partner +

+ +
+ +
+
+ + +
+ +
+ + +
+ +
+
+ + setAssignForm({ ...assignForm, deposit: Number(e.target.value) })} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" + /> +
+
+ + setAssignForm({ ...assignForm, rentPrice: Number(e.target.value) })} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" + /> +
+
+ +
+
+ + setAssignForm({ ...assignForm, investorShare: Number(e.target.value) })} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" + /> +
+
+ + setAssignForm({ ...assignForm, investedAmount: Number(e.target.value) })} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" + /> +
+
+
+ +
+ + +
+
+
+ )} + + {showRegisterBatteryModal && ( +
+
+
+

+ + Register & Assign New Battery +

+ +
+ +
+
+
+ + setRegisterForm({ ...registerForm, serialNumber: e.target.value })} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm font-mono font-semibold" + /> +
+
+ + +
+
+ +
+
+ + setRegisterForm({ ...registerForm, model: e.target.value })} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" + /> +
+
+ + +
+
+ +
+
+ + setRegisterForm({ ...registerForm, capacity: Number(e.target.value) })} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" + /> +
+
+ + setRegisterForm({ ...registerForm, voltage: Number(e.target.value) })} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" + /> +
+
+ + setRegisterForm({ ...registerForm, purchasePrice: Number(e.target.value), investedAmount: Number(e.target.value) })} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" + /> +
+
+ +
+ +
+
+ + +
+ +
+
+ + setRegisterForm({ ...registerForm, deposit: Number(e.target.value) })} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" + /> +
+
+ + setRegisterForm({ ...registerForm, rentPrice: Number(e.target.value) })} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" + /> +
+
+ +
+
+ + setRegisterForm({ ...registerForm, investorShare: Number(e.target.value) })} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" + /> +
+
+ + setRegisterForm({ ...registerForm, investedAmount: Number(e.target.value) })} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" + /> +
+
+
+
+
+ +
+ + +
+
+
+ )} + + {showEditBatteryModal && ( +
+
+
+

+ + Edit Battery Assignment +

+ +
+ +
+
+

SELECTED ASSET

+

+ {selectedBatteryToEdit?.brand} {selectedBatteryToEdit?.model} ({selectedBatteryToEdit?.serialNumber}) +

+

ID: {selectedBatteryToEdit?.id}

+
+ +
+ + +
+ +
+
+ + setAssignForm({ ...assignForm, deposit: Number(e.target.value) })} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" + /> +
+
+ + setAssignForm({ ...assignForm, rentPrice: Number(e.target.value) })} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" + /> +
+
+ +
+
+ + setAssignForm({ ...assignForm, investorShare: Number(e.target.value) })} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" + /> +
+
+ + setAssignForm({ ...assignForm, investedAmount: Number(e.target.value) })} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" + /> +
+
+
+ +
+ + +
+
+
+ )} + {showAssignBikeModal && (
@@ -2218,13 +3082,55 @@ export default function InvestorDetailPage() {
+ {/* Asset Type Selector */} +
+ +
+ + +
+
+
-
- {[ - { id: 'inv_demo_1', name: '1 Bike Plan', tier: 'Economy', evBasePrice: 200000, minQuantity: 1, duration: 12, maxInvestment: 1000000 }, - { id: 'inv_demo_2', name: '5 Bike Plan', tier: 'Standard', evBasePrice: 180000, minQuantity: 5, duration: 24, maxInvestment: 5000000 }, - ].map(plan => ( +
+ {(newInvestment.assetType === 'battery' ? batteryTemplates : bikeTemplates).map((plan: any) => ( ))} @@ -2255,12 +3161,12 @@ export default function InvestorDetailPage() { type="text" value={newInvestment.planName} onChange={(e) => setNewInvestment({ ...newInvestment, planName: e.target.value })} - className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm font-semibold" placeholder="Plan name" />
- + + +
- - + +
- -

= Qty × Base Price

+ +

= Qty × Base Price

@@ -2301,55 +3207,60 @@ export default function InvestorDetailPage() { setNewInvestment({ ...newInvestment, totalInvestment: val, - paidAmount: newInvestment.paymentType === 'full' ? val : Math.max(val * 0.5, newInvestment.paidAmount) + paidAmount: newInvestment.assetType === 'battery' ? val : (newInvestment.paymentType === 'full' ? val : Math.max(val * 0.5, newInvestment.paidAmount)) }); }} - className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm bg-yellow-50" + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm bg-yellow-50 font-bold" />
- - + +
- +
- +
- +
- - -
-

+
+

- FICO Share - Jaiben's Profit per Ride + {newInvestment.assetType === 'battery' ? 'Partner Profit Sharing Percentage' : 'FICO Share - Partner Yield Sharing Percentages'}

-

Profit sharing when bikes are rented to end customers

-
-
- - +

Profit sharing ratio when {newInvestment.assetType}s are utilized

+ {newInvestment.assetType === 'battery' ? ( +
+ Profit Share Percent (%) + {activeTemplate.profitSharePercent || activeTemplate.profitShareSingle}%
-
- - + ) : ( +
+
+ + +
+
+ + +
+
+ + +
-
- - -
-
+ )}
@@ -2376,54 +3287,38 @@ export default function InvestorDetailPage() {

Payment Options

-
-