feat: implement persistent local storage state for batteries, bikes, and investors with dynamic patching logic
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -75,9 +75,49 @@ export default function InvestorDetailPage() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const investorId = params.id as string;
|
const investorId = params.id as string;
|
||||||
|
|
||||||
const [investors, setInvestors] = useState<Investor[]>(initialInvestors);
|
const [investors, setInvestors] = useState<Investor[]>(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const stored = localStorage.getItem('jaiben_investors');
|
||||||
|
if (stored) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(stored);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return initialInvestors;
|
||||||
|
});
|
||||||
const investor = investors.find(i => i.id === investorId);
|
const investor = investors.find(i => i.id === investorId);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.setItem('jaiben_investors', JSON.stringify(investors));
|
||||||
|
}
|
||||||
|
}, [investors]);
|
||||||
|
|
||||||
|
const [bikes, setBikes] = useState<any[]>(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const stored = localStorage.getItem('jaiben_bikes');
|
||||||
|
if (stored) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(stored);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return initialBikes;
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.setItem('jaiben_bikes', JSON.stringify(bikes));
|
||||||
|
}
|
||||||
|
}, [bikes]);
|
||||||
|
|
||||||
|
const assignedBikes = bikes.filter(b => b.investorId === investorId);
|
||||||
|
|
||||||
const [settings, setSettings] = useState<any>(null);
|
const [settings, setSettings] = useState<any>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -93,13 +133,23 @@ export default function InvestorDetailPage() {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const assignedBikes = initialBikes.filter(b => b.investorId === investorId);
|
|
||||||
// Investor transactions are filtered below
|
// Investor transactions are filtered below
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState('overview');
|
const [activeTab, setActiveTab] = useState('overview');
|
||||||
const [showEditModal, setShowEditModal] = useState(false);
|
const [showEditModal, setShowEditModal] = useState(false);
|
||||||
const [showAssignBikeModal, setShowAssignBikeModal] = useState(false);
|
const [showAssignBikeModal, setShowAssignBikeModal] = useState(false);
|
||||||
const [selectedBikeId, setSelectedBikeId] = useState('');
|
const [selectedBikeId, setSelectedBikeId] = useState('');
|
||||||
|
const [showRegisterBikeModal, setShowRegisterBikeModal] = useState(false);
|
||||||
|
const [registerBikeForm, setRegisterBikeForm] = useState({
|
||||||
|
plateNumber: '',
|
||||||
|
brand: 'Etron',
|
||||||
|
model: 'ET50',
|
||||||
|
currentRent: 150,
|
||||||
|
location: 'Banani',
|
||||||
|
purchasePrice: 200000,
|
||||||
|
rentalType: 'single_rent',
|
||||||
|
investmentId: ''
|
||||||
|
});
|
||||||
const [showCreateInvestmentModal, setShowCreateInvestmentModal] = useState(false);
|
const [showCreateInvestmentModal, setShowCreateInvestmentModal] = useState(false);
|
||||||
const [showInvestmentSuccessModal, setShowInvestmentSuccessModal] = useState(false);
|
const [showInvestmentSuccessModal, setShowInvestmentSuccessModal] = useState(false);
|
||||||
const [lastCreatedInvestment, setLastCreatedInvestment] = useState<any>(null);
|
const [lastCreatedInvestment, setLastCreatedInvestment] = useState<any>(null);
|
||||||
@@ -122,7 +172,18 @@ export default function InvestorDetailPage() {
|
|||||||
const [newDoc, setNewDoc] = useState({ type: 'nid', number: '', url: '' });
|
const [newDoc, setNewDoc] = useState({ type: 'nid', number: '', url: '' });
|
||||||
const [editingMobileIndex, setEditingMobileIndex] = useState<number | null>(null);
|
const [editingMobileIndex, setEditingMobileIndex] = useState<number | null>(null);
|
||||||
|
|
||||||
const [batteries, setBatteries] = useState<any[]>([
|
const [batteries, setBatteries] = useState<any[]>(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const stored = localStorage.getItem('jaiben_batteries');
|
||||||
|
if (stored) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(stored);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [
|
||||||
{
|
{
|
||||||
id: 'BAT-001',
|
id: 'BAT-001',
|
||||||
serialNumber: 'SN-2024-00001',
|
serialNumber: 'SN-2024-00001',
|
||||||
@@ -136,10 +197,10 @@ export default function InvestorDetailPage() {
|
|||||||
deposit: 5000,
|
deposit: 5000,
|
||||||
rentPrice: 150,
|
rentPrice: 150,
|
||||||
investorId: investorId,
|
investorId: investorId,
|
||||||
investorName: investor?.name || 'Md. Hasan Mahmud',
|
investorName: 'Md. Hasan Mahmud',
|
||||||
investorSharePercentage: 60,
|
investorSharePercentage: 100,
|
||||||
investedAmount: 45000,
|
investedAmount: 45000,
|
||||||
investmentId: 'ip1',
|
investmentId: 'ip3',
|
||||||
status: 'in-use',
|
status: 'in-use',
|
||||||
currentSoc: 78,
|
currentSoc: 78,
|
||||||
health: 95,
|
health: 95,
|
||||||
@@ -158,21 +219,185 @@ export default function InvestorDetailPage() {
|
|||||||
deposit: 4000,
|
deposit: 4000,
|
||||||
rentPrice: 120,
|
rentPrice: 120,
|
||||||
investorId: investorId,
|
investorId: investorId,
|
||||||
investorName: investor?.name || 'Md. Hasan Mahmud',
|
investorName: 'Md. Hasan Mahmud',
|
||||||
investorSharePercentage: 100,
|
investorSharePercentage: 100,
|
||||||
investedAmount: 38000,
|
investedAmount: 38000,
|
||||||
investmentId: 'ip2',
|
investmentId: 'ip3',
|
||||||
status: 'available',
|
status: 'available',
|
||||||
currentSoc: 92,
|
currentSoc: 92,
|
||||||
health: 98,
|
health: 98,
|
||||||
cycleCount: 45
|
cycleCount: 45
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'BAT-005',
|
||||||
|
serialNumber: 'SN-2024-00005',
|
||||||
|
brand: 'BYD',
|
||||||
|
model: 'Li-Ion 60V50Ah',
|
||||||
|
type: 'lithium-ion',
|
||||||
|
capacity: 50,
|
||||||
|
voltage: 60,
|
||||||
|
purchaseDate: '2024-02-15',
|
||||||
|
purchasePrice: 45000,
|
||||||
|
deposit: 5000,
|
||||||
|
rentPrice: 150,
|
||||||
|
investorId: investorId,
|
||||||
|
investorName: 'Md. Hasan Mahmud',
|
||||||
|
investorSharePercentage: 100,
|
||||||
|
investedAmount: 45000,
|
||||||
|
investmentId: 'ip3',
|
||||||
|
status: 'in-use',
|
||||||
|
currentSoc: 82,
|
||||||
|
health: 97,
|
||||||
|
cycleCount: 18
|
||||||
}
|
}
|
||||||
]);
|
];
|
||||||
|
});
|
||||||
|
|
||||||
const [unassignedBatteries, setUnassignedBatteries] = useState<any[]>([
|
const [unassignedBatteries, setUnassignedBatteries] = useState<any[]>(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const stored = localStorage.getItem('jaiben_unassigned_batteries');
|
||||||
|
if (stored) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(stored);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [
|
||||||
{ 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-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 }
|
{ 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 }
|
||||||
]);
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.setItem('jaiben_batteries', JSON.stringify(batteries));
|
||||||
|
}
|
||||||
|
}, [batteries]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.setItem('jaiben_unassigned_batteries', JSON.stringify(unassignedBatteries));
|
||||||
|
}
|
||||||
|
}, [unassignedBatteries]);
|
||||||
|
|
||||||
|
// Patch local storage state to make sure inv1 has the Standard Battery Plan (ip3) and correct assigned batteries
|
||||||
|
useEffect(() => {
|
||||||
|
if (!investor) return;
|
||||||
|
const hasIp3 = investor.investments?.some(inv => inv.id === 'ip3');
|
||||||
|
if (!hasIp3) {
|
||||||
|
const updated = investors.map(i => {
|
||||||
|
if (i.id === investorId) {
|
||||||
|
const investments = i.investments || [];
|
||||||
|
return {
|
||||||
|
...i,
|
||||||
|
totalInvested: 300000,
|
||||||
|
investments: [
|
||||||
|
...investments.filter(inv => inv.id !== 'ip3'),
|
||||||
|
{
|
||||||
|
id: 'ip3',
|
||||||
|
investorId: investorId,
|
||||||
|
planName: 'Standard Battery Plan',
|
||||||
|
planType: 'silver' as const,
|
||||||
|
assetType: 'battery' as const,
|
||||||
|
batteryIds: ['BAT-001', 'BAT-002', 'BAT-005'],
|
||||||
|
totalInvestment: 150000,
|
||||||
|
monthlyReturn: 4500,
|
||||||
|
expectedRoi: 16,
|
||||||
|
actualEarnings: 9000,
|
||||||
|
startDate: '2024-02-01',
|
||||||
|
endDate: '2025-02-01',
|
||||||
|
status: 'active' as const,
|
||||||
|
paymentMethod: 'bank' as const,
|
||||||
|
transactionId: 'invt3',
|
||||||
|
createdAt: '2024-02-01'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return i;
|
||||||
|
});
|
||||||
|
setInvestors(updated);
|
||||||
|
}
|
||||||
|
}, [investors, investorId, investor]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const hasBat5 = batteries.some(b => b.id === 'BAT-005');
|
||||||
|
const hasIp3Assignment = batteries.some(b => b.id === 'BAT-001' && b.investmentId === 'ip3');
|
||||||
|
if (!hasBat5 || !hasIp3Assignment) {
|
||||||
|
const updated = [
|
||||||
|
{
|
||||||
|
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: 'Md. Hasan Mahmud',
|
||||||
|
investorSharePercentage: 100,
|
||||||
|
investedAmount: 45000,
|
||||||
|
investmentId: 'ip3',
|
||||||
|
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: 'Md. Hasan Mahmud',
|
||||||
|
investorSharePercentage: 100,
|
||||||
|
investedAmount: 38000,
|
||||||
|
investmentId: 'ip3',
|
||||||
|
status: 'available',
|
||||||
|
currentSoc: 92,
|
||||||
|
health: 98,
|
||||||
|
cycleCount: 45
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'BAT-005',
|
||||||
|
serialNumber: 'SN-2024-00005',
|
||||||
|
brand: 'BYD',
|
||||||
|
model: 'Li-Ion 60V50Ah',
|
||||||
|
type: 'lithium-ion',
|
||||||
|
capacity: 50,
|
||||||
|
voltage: 60,
|
||||||
|
purchaseDate: '2024-02-15',
|
||||||
|
purchasePrice: 45000,
|
||||||
|
deposit: 5000,
|
||||||
|
rentPrice: 150,
|
||||||
|
investorId: investorId,
|
||||||
|
investorName: 'Md. Hasan Mahmud',
|
||||||
|
investorSharePercentage: 100,
|
||||||
|
investedAmount: 45000,
|
||||||
|
investmentId: 'ip3',
|
||||||
|
status: 'in-use',
|
||||||
|
currentSoc: 82,
|
||||||
|
health: 97,
|
||||||
|
cycleCount: 18
|
||||||
|
},
|
||||||
|
...batteries.filter(b => b.id !== 'BAT-001' && b.id !== 'BAT-002' && b.id !== 'BAT-005')
|
||||||
|
];
|
||||||
|
setBatteries(updated);
|
||||||
|
}
|
||||||
|
}, [batteries, investorId]);
|
||||||
|
|
||||||
const [showAssignBatteryModal, setShowAssignBatteryModal] = useState(false);
|
const [showAssignBatteryModal, setShowAssignBatteryModal] = useState(false);
|
||||||
const [showRegisterBatteryModal, setShowRegisterBatteryModal] = useState(false);
|
const [showRegisterBatteryModal, setShowRegisterBatteryModal] = useState(false);
|
||||||
@@ -269,14 +494,84 @@ export default function InvestorDetailPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const availableBikesForAssignment = initialBikes.filter(b => !b.investorId && b.status === 'available');
|
const availableBikesForAssignment = bikes.filter(b => !b.investorId && b.status === 'available');
|
||||||
|
|
||||||
const handleAssignBike = () => {
|
const handleAssignBike = () => {
|
||||||
alert('Bike assignment functionality - would update bike investorId here');
|
if (!selectedBikeId) {
|
||||||
|
toast.error('Please select a bike');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const bikeToAssign = bikes.find(b => b.id === selectedBikeId);
|
||||||
|
if (!bikeToAssign) return;
|
||||||
|
|
||||||
|
// Find first active EV Investment Plan of the investor to attach to
|
||||||
|
const evInv = investor.investments?.find((inv: any) => inv.assetType === 'bike' || inv.planName.toLowerCase().includes('ev') || inv.planName.toLowerCase().includes('bike')) || investor.investments?.[0];
|
||||||
|
|
||||||
|
setBikes(prev => prev.map(b => {
|
||||||
|
if (b.id === selectedBikeId) {
|
||||||
|
return {
|
||||||
|
...b,
|
||||||
|
investorId: investorId,
|
||||||
|
investorName: investor.name,
|
||||||
|
investmentId: evInv?.id || 'ip1',
|
||||||
|
status: 'rented',
|
||||||
|
totalEarnings: b.totalEarnings || 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return b;
|
||||||
|
}));
|
||||||
|
|
||||||
|
toast.success(`Bike ${bikeToAssign.model} (${bikeToAssign.plateNumber}) successfully assigned!`);
|
||||||
setShowAssignBikeModal(false);
|
setShowAssignBikeModal(false);
|
||||||
setSelectedBikeId('');
|
setSelectedBikeId('');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRegisterAndAssignBike = () => {
|
||||||
|
if (!registerBikeForm.plateNumber) {
|
||||||
|
toast.error('Please enter a plate number');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const evInv = investor.investments?.find((inv: any) => inv.assetType === 'bike' || inv.planName.toLowerCase().includes('ev') || inv.planName.toLowerCase().includes('bike')) || investor.investments?.[0];
|
||||||
|
|
||||||
|
const newBike = {
|
||||||
|
id: `BIKE-${Date.now()}`,
|
||||||
|
plateNumber: registerBikeForm.plateNumber,
|
||||||
|
brand: registerBikeForm.brand,
|
||||||
|
model: registerBikeForm.model,
|
||||||
|
currentRent: Number(registerBikeForm.currentRent),
|
||||||
|
location: registerBikeForm.location,
|
||||||
|
purchasePrice: Number(registerBikeForm.purchasePrice),
|
||||||
|
rentalType: registerBikeForm.rentalType,
|
||||||
|
investorId: investorId,
|
||||||
|
investorName: investor.name,
|
||||||
|
investmentId: registerBikeForm.investmentId || evInv?.id || 'ip1',
|
||||||
|
status: 'rented',
|
||||||
|
batteryLevel: 100,
|
||||||
|
totalEarnings: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
setBikes(prev => [...prev, newBike]);
|
||||||
|
setShowRegisterBikeModal(false);
|
||||||
|
toast.success(`New bike ${newBike.model} registered and assigned!`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUnassignBike = (bikeId: string) => {
|
||||||
|
setBikes(prev => prev.map(b => {
|
||||||
|
if (b.id === bikeId) {
|
||||||
|
return {
|
||||||
|
...b,
|
||||||
|
investorId: null,
|
||||||
|
investorName: null,
|
||||||
|
investmentId: null,
|
||||||
|
status: 'available'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return b;
|
||||||
|
}));
|
||||||
|
toast.success('Bike successfully unassigned from investor!');
|
||||||
|
};
|
||||||
|
|
||||||
const handleCreateInvestment = () => {
|
const handleCreateInvestment = () => {
|
||||||
const invId = `INV-${Date.now()}`;
|
const invId = `INV-${Date.now()}`;
|
||||||
const year = new Date().getFullYear();
|
const year = new Date().getFullYear();
|
||||||
@@ -1211,10 +1506,45 @@ export default function InvestorDetailPage() {
|
|||||||
|
|
||||||
{activeTab === 'bikes' && (
|
{activeTab === 'bikes' && (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold text-slate-800">Assigned Bikes</h3>
|
<h3 className="font-bold text-lg text-slate-800 flex items-center gap-2">
|
||||||
<p className="text-sm text-slate-500">{assignedBikes.length} bikes across {investor.investments?.length || 0} EV Investment Plans</p>
|
<Bike className="w-5 h-5 text-investor" />
|
||||||
|
Assigned Bikes
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-slate-500">
|
||||||
|
{assignedBikes.length} bikes across {investor.investments?.filter((inv: any) => inv.assetType === 'bike' || !inv.assetType).length || 0} EV Investment Plans
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedBikeId('');
|
||||||
|
setShowAssignBikeModal(true);
|
||||||
|
}}
|
||||||
|
className="px-4 py-2 bg-blue-50 text-blue-700 border border-blue-200 rounded-lg text-sm font-semibold hover:bg-blue-100 flex items-center gap-2 transition-colors"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4" /> Assign Existing
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const evInv = investor.investments?.find((inv: any) => inv.assetType === 'bike' || inv.planName.toLowerCase().includes('ev') || inv.planName.toLowerCase().includes('bike')) || investor.investments?.[0];
|
||||||
|
setRegisterBikeForm({
|
||||||
|
plateNumber: `DHAKA-METRO-HA-${Math.floor(1000 + Math.random() * 9000)}`,
|
||||||
|
brand: 'Etron',
|
||||||
|
model: 'ET50',
|
||||||
|
currentRent: 150,
|
||||||
|
location: 'Banani',
|
||||||
|
purchasePrice: 200000,
|
||||||
|
rentalType: 'single_rent',
|
||||||
|
investmentId: evInv?.id || ''
|
||||||
|
});
|
||||||
|
setShowRegisterBikeModal(true);
|
||||||
|
}}
|
||||||
|
className="px-4 py-2 bg-investor text-white rounded-lg text-sm font-semibold hover:bg-investor-dark flex items-center gap-2 shadow-sm transition-colors"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4" /> Register & Assign New
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
@@ -1249,15 +1579,14 @@ export default function InvestorDetailPage() {
|
|||||||
maintenance: { bg: 'bg-amber-100', color: 'text-amber-700' },
|
maintenance: { bg: 'bg-amber-100', color: 'text-amber-700' },
|
||||||
retired: { bg: 'bg-slate-100', color: 'text-slate-600' },
|
retired: { bg: 'bg-slate-100', color: 'text-slate-600' },
|
||||||
};
|
};
|
||||||
const rentalInfo = rentalTypes[bike.rentalType || 'single_rent'];
|
const rentalInfo = rentalTypes[bike.rentalType || 'single_rent'] || rentalTypes.single_rent;
|
||||||
const planType = investment?.planType || 'gold';
|
const planType = investment?.planType || 'gold';
|
||||||
const status = statusConfig[bike.status] || statusConfig.available;
|
const status = statusConfig[bike.status] || statusConfig.available;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<div
|
||||||
key={bike.id}
|
key={bike.id}
|
||||||
href={`/bikes?bike=${bike.id}`}
|
className={`block bg-white rounded-xl border ${planBg[planType]} overflow-hidden hover:shadow-lg transition-all group relative`}
|
||||||
className={`block bg-white rounded-xl border ${planBg[planType]} overflow-hidden hover:shadow-lg transition-all hover:scale-[1.02] cursor-pointer group`}
|
|
||||||
>
|
>
|
||||||
<div className={`h-2 bg-gradient-to-r ${planColors[planType]}`} />
|
<div className={`h-2 bg-gradient-to-r ${planColors[planType]}`} />
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
@@ -1309,8 +1638,24 @@ export default function InvestorDetailPage() {
|
|||||||
}`}>{bike.batteryLevel}%</span>
|
}`}>{bike.batteryLevel}%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<div className="mt-3 pt-3 border-t border-slate-100 flex justify-end gap-2">
|
||||||
|
<Link href={`/bikes?bike=${bike.id}`} className="text-xs text-slate-500 hover:text-slate-800 font-semibold transition-colors px-2 py-1 bg-slate-100 rounded">
|
||||||
|
View Details
|
||||||
</Link>
|
</Link>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (confirm(`Are you sure you want to unassign bike ${bike.model} (${bike.plateNumber})?`)) {
|
||||||
|
handleUnassignBike(bike.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="text-xs text-red-600 hover:text-red-800 font-semibold transition-colors px-2 py-1 bg-red-50 rounded"
|
||||||
|
>
|
||||||
|
Unassign
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{assignedBikes.length === 0 && (
|
{assignedBikes.length === 0 && (
|
||||||
@@ -3071,6 +3416,116 @@ export default function InvestorDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showRegisterBikeModal && (
|
||||||
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||||
|
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
|
||||||
|
<div className="p-5 border-b border-slate-100 flex items-center justify-between">
|
||||||
|
<h2 className="text-lg font-bold text-slate-800">Register & Assign New Bike</h2>
|
||||||
|
<button onClick={() => setShowRegisterBikeModal(false)} className="p-2 hover:bg-slate-100 rounded-lg">
|
||||||
|
<X className="w-5 h-5 text-slate-400" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-5 space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-slate-600 mb-1 block">Plate Number</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={registerBikeForm.plateNumber}
|
||||||
|
onChange={(e) => setRegisterBikeForm({ ...registerBikeForm, plateNumber: e.target.value })}
|
||||||
|
placeholder="DHAKA-METRO-HA-1234"
|
||||||
|
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-slate-600 mb-1 block">Brand</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={registerBikeForm.brand}
|
||||||
|
onChange={(e) => setRegisterBikeForm({ ...registerBikeForm, brand: e.target.value })}
|
||||||
|
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-slate-600 mb-1 block">Model</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={registerBikeForm.model}
|
||||||
|
onChange={(e) => setRegisterBikeForm({ ...registerBikeForm, model: e.target.value })}
|
||||||
|
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-slate-600 mb-1 block">Purchase Price (৳)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={registerBikeForm.purchasePrice}
|
||||||
|
onChange={(e) => setRegisterBikeForm({ ...registerBikeForm, purchasePrice: Number(e.target.value) })}
|
||||||
|
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-slate-600 mb-1 block">Daily Rent (৳)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={registerBikeForm.currentRent}
|
||||||
|
onChange={(e) => setRegisterBikeForm({ ...registerBikeForm, currentRent: Number(e.target.value) })}
|
||||||
|
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-slate-600 mb-1 block">Location</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={registerBikeForm.location}
|
||||||
|
onChange={(e) => setRegisterBikeForm({ ...registerBikeForm, location: e.target.value })}
|
||||||
|
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-slate-600 mb-1 block">Rental Type</label>
|
||||||
|
<select
|
||||||
|
value={registerBikeForm.rentalType}
|
||||||
|
onChange={(e) => setRegisterBikeForm({ ...registerBikeForm, rentalType: e.target.value })}
|
||||||
|
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
|
||||||
|
>
|
||||||
|
<option value="single_rent">Single Rent</option>
|
||||||
|
<option value="rent_to_own">Rent to Own</option>
|
||||||
|
<option value="share_ev">Share EV</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-slate-600 mb-1 block">Attach to Investment Plan</label>
|
||||||
|
<select
|
||||||
|
value={registerBikeForm.investmentId}
|
||||||
|
onChange={(e) => setRegisterBikeForm({ ...registerBikeForm, investmentId: e.target.value })}
|
||||||
|
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
|
||||||
|
>
|
||||||
|
<option value="">Select plan</option>
|
||||||
|
{investor.investments?.filter((inv: any) => inv.assetType === 'bike' || !inv.assetType).map((inv: any) => (
|
||||||
|
<option key={inv.id} value={inv.id}>{inv.planName} ({inv.id})</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-5 border-t border-slate-100 flex justify-end gap-3">
|
||||||
|
<button onClick={() => setShowRegisterBikeModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">Cancel</button>
|
||||||
|
<button onClick={handleRegisterAndAssignBike} className="px-4 py-2 bg-investor text-white rounded-lg text-sm hover:bg-investor-dark">Register & Assign</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{showCreateInvestmentModal && (
|
{showCreateInvestmentModal && (
|
||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-3xl max-h-[90vh] overflow-hidden flex flex-col">
|
<div className="bg-white rounded-xl shadow-xl w-full max-w-3xl max-h-[90vh] overflow-hidden flex flex-col">
|
||||||
|
|||||||
@@ -96,7 +96,9 @@ export interface InvestmentPlan {
|
|||||||
investorId: string;
|
investorId: string;
|
||||||
planName: string;
|
planName: string;
|
||||||
planType: 'silver' | 'gold' | 'platinum' | 'diamond';
|
planType: 'silver' | 'gold' | 'platinum' | 'diamond';
|
||||||
bikeIds: string[];
|
bikeIds?: string[];
|
||||||
|
batteryIds?: string[];
|
||||||
|
assetType?: 'bike' | 'battery';
|
||||||
totalInvestment: number;
|
totalInvestment: number;
|
||||||
monthlyReturn: number;
|
monthlyReturn: number;
|
||||||
expectedRoi: number;
|
expectedRoi: number;
|
||||||
@@ -363,7 +365,7 @@ export const investors: Investor[] = [
|
|||||||
emergencyContactName: 'Fatema Begum',
|
emergencyContactName: 'Fatema Begum',
|
||||||
emergencyContactRelation: 'Wife',
|
emergencyContactRelation: 'Wife',
|
||||||
emergencyContactPhone: '01712345679',
|
emergencyContactPhone: '01712345679',
|
||||||
totalInvested: 150000,
|
totalInvested: 300000,
|
||||||
totalEarnings: 114250,
|
totalEarnings: 114250,
|
||||||
activeBikes: 2,
|
activeBikes: 2,
|
||||||
withdrawalPending: 3000,
|
withdrawalPending: 3000,
|
||||||
@@ -387,7 +389,8 @@ export const investors: Investor[] = [
|
|||||||
referralEarnings: 2500,
|
referralEarnings: 2500,
|
||||||
investments: [
|
investments: [
|
||||||
{ id: 'ip1', investorId: 'inv1', planName: 'Gold EV Fleet 2024', planType: 'gold', bikeIds: ['b1'], totalInvestment: 85000, monthlyReturn: 2500, expectedRoi: 18, actualEarnings: 10000, startDate: '2024-01-15', endDate: '2025-01-14', status: 'active', paymentMethod: 'bank', transactionId: 'invt1', createdAt: '2024-01-15' },
|
{ id: 'ip1', investorId: 'inv1', planName: 'Gold EV Fleet 2024', planType: 'gold', bikeIds: ['b1'], totalInvestment: 85000, monthlyReturn: 2500, expectedRoi: 18, actualEarnings: 10000, startDate: '2024-01-15', endDate: '2025-01-14', status: 'active', paymentMethod: 'bank', transactionId: 'invt1', createdAt: '2024-01-15' },
|
||||||
{ id: 'ip2', investorId: 'inv1', planName: 'Gold City Commuter', planType: 'gold', bikeIds: ['b2'], totalInvestment: 65000, monthlyReturn: 2125, expectedRoi: 18, actualEarnings: 4250, startDate: '2024-01-20', endDate: '2025-01-19', status: 'active', paymentMethod: 'mobile', transactionId: 'invt2', createdAt: '2024-01-20' }
|
{ id: 'ip2', investorId: 'inv1', planName: 'Gold City Commuter', planType: 'gold', bikeIds: ['b2'], totalInvestment: 65000, monthlyReturn: 2125, expectedRoi: 18, actualEarnings: 4250, startDate: '2024-01-20', endDate: '2025-01-19', status: 'active', paymentMethod: 'mobile', transactionId: 'invt2', createdAt: '2024-01-20' },
|
||||||
|
{ id: 'ip3', investorId: 'inv1', planName: 'Standard Battery Plan', planType: 'silver', assetType: 'battery', batteryIds: ['BAT-001', 'BAT-002'], totalInvestment: 150000, monthlyReturn: 4500, expectedRoi: 16, actualEarnings: 9000, startDate: '2024-02-01', endDate: '2025-02-01', status: 'active', paymentMethod: 'bank', transactionId: 'invt3', createdAt: '2024-02-01' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user