mirror of
https://github.com/Tomato6966/investment-portfolio-simulator.git
synced 2025-04-17 22:38:43 +02:00
511 lines
31 KiB
TypeScript
511 lines
31 KiB
TypeScript
import { format, isBefore } from "date-fns";
|
|
import {
|
|
Download, FileDown, LineChart, Loader2, Pencil, RefreshCw, ShoppingBag, Trash2
|
|
} from "lucide-react";
|
|
import { useCallback, useMemo, useState } from "react";
|
|
import toast from "react-hot-toast";
|
|
|
|
import { usePortfolioSelector } from "../hooks/usePortfolio";
|
|
import { Investment } from "../types";
|
|
import { calculateInvestmentPerformance } from "../utils/calculations/performance";
|
|
import { downloadTableAsCSV, generatePortfolioPDF } from "../utils/export";
|
|
import { EditInvestmentModal } from "./Modals/EditInvestmentModal";
|
|
import { EditSavingsPlanModal } from "./Modals/EditSavingsPlanModal";
|
|
import { FutureProjectionModal } from "./Modals/FutureProjectionModal";
|
|
import { Tooltip } from "./utils/ToolTip";
|
|
|
|
interface SavingsPlanPerformance {
|
|
assetName: string;
|
|
amount: number;
|
|
totalInvested: number;
|
|
currentValue: number;
|
|
performancePercentage: number;
|
|
performancePerAnnoPerformance: number;
|
|
allocation?: number;
|
|
}
|
|
|
|
export default function PortfolioTable() {
|
|
const { assets, removeInvestment, clearInvestments } = usePortfolioSelector((state) => ({
|
|
assets: state.assets,
|
|
removeInvestment: state.removeInvestment,
|
|
clearInvestments: state.clearInvestments,
|
|
}));
|
|
|
|
const [editingInvestment, setEditingInvestment] = useState<{
|
|
investment: Investment;
|
|
assetId: string;
|
|
} | null>(null);
|
|
const [isGeneratingPDF, setIsGeneratingPDF] = useState(false);
|
|
const [isUpdatingSavingsPlan, setIsUpdatingSavingsPlan] = useState(false);
|
|
const [editingSavingsPlan, setEditingSavingsPlan] = useState<{
|
|
assetId: string;
|
|
groupId: string;
|
|
amount: number;
|
|
dayOfMonth: number;
|
|
interval: number;
|
|
dynamic?: {
|
|
type: 'percentage' | 'fixed';
|
|
value: number;
|
|
yearInterval: number;
|
|
};
|
|
} | null>(null);
|
|
|
|
const performance = useMemo(() => calculateInvestmentPerformance(assets), [assets]);
|
|
|
|
const averagePerformance = useMemo(() => {
|
|
return ((performance.investments.reduce((sum, inv) => sum + inv.performancePercentage, 0) / performance.investments.length) || 0).toFixed(2);
|
|
}, [performance.investments]);
|
|
|
|
const handleDelete = useCallback((investmentId: string, assetId: string) => {
|
|
if (window.confirm("Are you sure you want to delete this investment?")) {
|
|
try {
|
|
removeInvestment(assetId, investmentId);
|
|
toast.success('Investment deleted successfully');
|
|
} catch (error:any) {
|
|
toast.error('Failed to delete investment' + String(error?.message || error));
|
|
}
|
|
}
|
|
}, [removeInvestment]);
|
|
|
|
const handleClearAll = useCallback(() => {
|
|
if (window.confirm("Are you sure you want to clear all investments?")) {
|
|
try {
|
|
clearInvestments();
|
|
toast.success('All investments cleared successfully');
|
|
} catch (error:any) {
|
|
toast.error('Failed to clear investments' + String(error?.message || error));
|
|
}
|
|
}
|
|
}, [clearInvestments]);
|
|
|
|
const performanceTooltip = useMemo(() => (
|
|
<div className="space-y-2">
|
|
<p>The performance of your portfolio is {performance.summary.performancePercentage.toFixed(2)}%</p>
|
|
<p>The average (acc.) performance of all positions is {averagePerformance}%</p>
|
|
<p>The average (p.a.) performance of every year is {performance.summary.performancePerAnnoPerformance.toFixed(2)}%</p>
|
|
<p>Best p.a.: {performance.summary.bestPerformancePerAnno?.[0]?.percentage?.toFixed(2) || "0.00"}% ({performance.summary.bestPerformancePerAnno?.[0]?.year || "N/A"})</p>
|
|
<p>Worst p.a.: {performance.summary.worstPerformancePerAnno?.[0]?.percentage?.toFixed(2) || "0.00"}% ({performance.summary.worstPerformancePerAnno?.[0]?.year || "N/A"})</p>
|
|
<p className="text-xs mt-2">
|
|
Note: An average performance of positions doesn't always match your entire portfolio's average,
|
|
especially with single investments or investments on different time ranges.
|
|
</p>
|
|
</div>
|
|
), [performance.summary.performancePercentage, averagePerformance, performance.summary.performancePerAnnoPerformance, performance.summary.bestPerformancePerAnno, performance.summary.worstPerformancePerAnno]);
|
|
|
|
const buyInTooltip = useMemo(() => (
|
|
<div className="space-y-2">
|
|
<p>"Buy-in" shows the asset's price when that position was bought.</p>
|
|
<p>"Avg" shows the average buy-in price across all positions for that asset.</p>
|
|
</div>
|
|
), []);
|
|
|
|
const currentAmountTooltip = useMemo(() => (
|
|
"The current value of your investment based on the latest market price."
|
|
), []);
|
|
|
|
const ttworTooltip = useMemo(() => (
|
|
<div className="space-y-2">
|
|
<p>Time Travel Without Risk (TTWOR) shows how your portfolio would have performed if all investments had been made at the beginning of the period.</p>
|
|
<p className="text-xs mt-2">
|
|
It helps to evaluate the impact of your investment timing strategy compared to a single early investment.
|
|
</p>
|
|
</div>
|
|
), []);
|
|
|
|
const [showProjection, setShowProjection] = useState(false);
|
|
|
|
const isSavingsPlanOverviewDisabled = useMemo(() => {
|
|
return !assets.some(asset => asset.investments.some(inv => inv.type === 'periodic'));
|
|
}, [assets]);
|
|
|
|
const savingsPlansPerformance = useMemo(() => {
|
|
if(isSavingsPlanOverviewDisabled) return [];
|
|
const performance: SavingsPlanPerformance[] = [];
|
|
const totalSavingsPlansAmount = assets
|
|
.map(v => v.investments)
|
|
.flat()
|
|
.filter(inv => inv.type === 'periodic')
|
|
.reduce((sum, inv) => sum + inv.amount, 0);
|
|
|
|
// Second pass to calculate individual performances with allocation
|
|
for (const asset of assets) {
|
|
const savingsPlans = asset.investments.filter(inv => inv.type === 'periodic');
|
|
const amount = savingsPlans.reduce((sum, inv) => sum + inv.amount, 0);
|
|
if (savingsPlans.length > 0) {
|
|
const assetPerformance = calculateInvestmentPerformance([{
|
|
...asset,
|
|
investments: savingsPlans
|
|
}]);
|
|
performance.push({
|
|
assetName: asset.name,
|
|
amount: savingsPlans[0].amount,
|
|
...assetPerformance.summary,
|
|
allocation: amount / totalSavingsPlansAmount * 100
|
|
});
|
|
}
|
|
}
|
|
return performance;
|
|
}, [assets, isSavingsPlanOverviewDisabled]);
|
|
|
|
const savingsPlansSummary = useMemo(() => {
|
|
if (savingsPlansPerformance.length === 0) return null;
|
|
|
|
const totalCurrentValue = savingsPlansPerformance.reduce((sum, plan) => sum + plan.currentValue, 0);
|
|
const totalInvested = savingsPlansPerformance.reduce((sum, plan) => sum + plan.totalInvested, 0);
|
|
const weightedPerformance = savingsPlansPerformance.reduce((sum, plan) => {
|
|
return sum + (plan.performancePercentage * (plan.currentValue / totalCurrentValue));
|
|
}, 0);
|
|
const weightedPerformancePA = savingsPlansPerformance.reduce((sum, plan) => {
|
|
return sum + (plan.performancePerAnnoPerformance * (plan.currentValue / totalCurrentValue));
|
|
}, 0);
|
|
|
|
return {
|
|
totalAmount: savingsPlansPerformance.reduce((sum, plan) => sum + plan.amount, 0),
|
|
totalInvested,
|
|
totalCurrentValue,
|
|
weightedPerformance,
|
|
weightedPerformancePA,
|
|
};
|
|
}, [savingsPlansPerformance]);
|
|
|
|
const handleGeneratePDF = async () => {
|
|
setIsGeneratingPDF(true);
|
|
try {
|
|
await generatePortfolioPDF(
|
|
assets,
|
|
performance,
|
|
savingsPlansPerformance,
|
|
performance.summary.performancePerAnnoPerformance
|
|
);
|
|
toast.success('PDF generated successfully');
|
|
} catch (error:any) {
|
|
toast.error('Failed to generate PDF' + String(error?.message || error));
|
|
} finally {
|
|
setIsGeneratingPDF(false);
|
|
}
|
|
};
|
|
|
|
const handleDeleteSavingsPlan = useCallback((assetId: string, groupId: string) => {
|
|
if (window.confirm("Are you sure you want to delete this savings plan? All related investments will be removed.")) {
|
|
try {
|
|
setIsUpdatingSavingsPlan(true);
|
|
setTimeout(() => {
|
|
try {
|
|
const asset = assets.find(a => a.id === assetId);
|
|
if (!asset) throw new Error('Asset not found');
|
|
const investments = asset.investments.filter(inv => inv.periodicGroupId === groupId);
|
|
investments.forEach(inv => {
|
|
removeInvestment(assetId, inv.id);
|
|
});
|
|
toast.success('Savings plan deleted successfully');
|
|
} catch (error:any) {
|
|
toast.error('Failed to delete savings plan: ' + String(error?.message || error));
|
|
} finally {
|
|
setIsUpdatingSavingsPlan(false);
|
|
}
|
|
}, 10);
|
|
} catch (error:any) {
|
|
toast.error('Failed to delete savings plan: ' + String(error?.message || error));
|
|
}
|
|
}
|
|
}, [assets, removeInvestment]);
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="overflow-x-auto min-h-[500px] dark:text-gray-300 p-4 border-gray-300 dark:border-slate-800 rounded-lg bg-white dark:bg-slate-800 shadow-lg dark:shadow-black/60">
|
|
<div className="flex flex-wrap justify-between items-center mb-4">
|
|
<h2 className="text-xl font-bold dark:text-gray-100">Portfolio's <u>Positions</u> Overview</h2>
|
|
<div className="flex flex-wrap gap-2">
|
|
<button
|
|
onClick={handleClearAll}
|
|
disabled={performance.investments.length === 0}
|
|
className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
Clear All Investments
|
|
</button>
|
|
<button
|
|
onClick={() => setShowProjection(true)}
|
|
disabled={performance.investments.length === 0}
|
|
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<LineChart size={16} />
|
|
Future Projection
|
|
</button>
|
|
|
|
<button
|
|
onClick={handleGeneratePDF}
|
|
className="px-4 py-2 bg-purple-500 text-white rounded hover:bg-purple-600 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
disabled={performance.investments.length === 0 || isGeneratingPDF}
|
|
>
|
|
{isGeneratingPDF ? (
|
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
) : (
|
|
<FileDown size={16} />
|
|
)}
|
|
{isGeneratingPDF ? 'Generating...' : 'Save Analysis'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{!isSavingsPlanOverviewDisabled && savingsPlansPerformance.length > 0 && (
|
|
<div className="overflow-x-auto mb-4 dark:text-gray-300 p-4 border-gray-300 dark:border-slate-800 rounded-lg bg-white dark:bg-slate-800 shadow-lg dark:shadow-black/60">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h3 className="text-lg font-bold">Savings Plans Performance</h3>
|
|
<button
|
|
onClick={() => downloadTableAsCSV(savingsPlansPerformance, 'savings-plans-performance')}
|
|
className="p-2 hover:bg-gray-100 dark:hover:bg-slate-700 rounded transition-colors"
|
|
title="Download CSV"
|
|
>
|
|
<Download size={16} />
|
|
</button>
|
|
</div>
|
|
<table className="min-w-full bg-white dark:bg-slate-800">
|
|
<thead>
|
|
<tr className="bg-gray-100 dark:bg-slate-700 text-left">
|
|
<th className="px-4 py-2 first:rounded-tl-lg">Asset</th>
|
|
<th className="px-4 py-2">Interval Amount</th>
|
|
<th className="px-4 py-2">Allocation</th>
|
|
<th className="px-4 py-2">Total Invested</th>
|
|
<th className="px-4 py-2">Current Value</th>
|
|
<th className="px-4 py-2">Performance (%)</th>
|
|
<th className="px-4 py-2">Performance (p.a.)</th>
|
|
<th className="px-4 py-2 last:rounded-tr-lg">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{savingsPlansSummary && (
|
|
<tr className="font-bold bg-gray-50 dark:bg-slate-700 border-t border-gray-200 dark:border-slate-600">
|
|
<td className="px-4 py-2">Total</td>
|
|
<td className="px-4 py-2">€{savingsPlansSummary.totalAmount.toFixed(2)}</td>
|
|
<td className="px-4 py-2">100%</td>
|
|
<td className="px-4 py-2">€{savingsPlansSummary.totalInvested.toFixed(2)}</td>
|
|
<td className="px-4 py-2">€{savingsPlansSummary.totalCurrentValue.toFixed(2)}</td>
|
|
<td className="px-4 py-2">{savingsPlansSummary.weightedPerformance.toFixed(2)}%</td>
|
|
<td className="px-4 py-2">{savingsPlansSummary.weightedPerformancePA.toFixed(2)}%</td>
|
|
<td className="px-4 py-2"></td>
|
|
</tr>
|
|
)}
|
|
{savingsPlansPerformance.sort((a, b) => Number(b.allocation || 0) - Number(a.allocation || 0)).map((plan) => {
|
|
const asset = assets.find(a => a.name === plan.assetName)!;
|
|
const firstInvestment = asset.investments.find(inv => inv.type === 'periodic')!;
|
|
const groupId = firstInvestment.periodicGroupId!;
|
|
|
|
return (
|
|
<tr key={plan.assetName} className="border-t border-gray-200 dark:border-slate-600">
|
|
<td className="px-4 py-2">{plan.assetName}</td>
|
|
<td className="px-4 py-2">€{plan.amount.toFixed(2)}</td>
|
|
<td className="px-4 py-2">{plan.allocation?.toFixed(2)}%</td>
|
|
<td className="px-4 py-2">€{plan.totalInvested.toFixed(2)}</td>
|
|
<td className="px-4 py-2">€{plan.currentValue.toFixed(2)}</td>
|
|
<td className="px-4 py-2">{plan.performancePercentage.toFixed(2)}%</td>
|
|
<td className="px-4 py-2">{plan.performancePerAnnoPerformance.toFixed(2)}%</td>
|
|
<td className="px-4 py-2">
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={() => setEditingSavingsPlan({
|
|
assetId: asset.id,
|
|
groupId,
|
|
amount: firstInvestment.amount,
|
|
dayOfMonth: firstInvestment.date?.getDate() || 0,
|
|
interval: 30, // You might want to store this in the investment object
|
|
// Add dynamic settings if available
|
|
})}
|
|
className="p-1 hover:bg-gray-100 dark:hover:bg-slate-700 rounded transition-colors"
|
|
>
|
|
{isUpdatingSavingsPlan || editingSavingsPlan ? (
|
|
<Loader2 className="animate-spin" size={16} />
|
|
) : (<Pencil className="w-4 h-4" />
|
|
)}
|
|
</button>
|
|
<button
|
|
onClick={() => handleDeleteSavingsPlan(asset.id, groupId)}
|
|
className="p-1 hover:bg-gray-100 dark:hover:bg-slate-700 rounded text-red-500 transition-colors"
|
|
>
|
|
{isUpdatingSavingsPlan || editingSavingsPlan ? (
|
|
<Loader2 className="animate-spin" size={16} />
|
|
) : (
|
|
<Trash2 className="w-4 h-4" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
|
|
<div className="overflow-x-auto dark:text-gray-300 p-4 border-gray-300 dark:border-slate-800 rounded-lg bg-white dark:bg-slate-800 shadow-lg dark:shadow-black/60">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h3 className="text-lg font-bold">Positions Overview</h3>
|
|
<button
|
|
onClick={() => downloadTableAsCSV([
|
|
{
|
|
id: "",
|
|
assetName: "Total Portfolio",
|
|
date: "",
|
|
investedAmount: performance.summary.totalInvested.toFixed(2),
|
|
investedAtPrice: "",
|
|
currentValue: performance.summary.currentValue.toFixed(2),
|
|
performancePercentage: `${performance.summary.performancePercentage.toFixed(2)}% (avg. acc. ${averagePerformance}%) (avg. p.a. ${performance.summary.performancePerAnnoPerformance.toFixed(2)}%)`,
|
|
periodicGroupId: "",
|
|
},
|
|
{
|
|
id: "",
|
|
assetName: "TTWOR",
|
|
date: "",
|
|
investedAmount: performance.summary.totalInvested.toFixed(2),
|
|
investedAtPrice: "",
|
|
currentValue: performance.summary.ttworValue.toFixed(2),
|
|
performancePercentage: `${performance.summary.ttworPercentage.toFixed(2)}%`,
|
|
periodicGroupId: "",
|
|
},
|
|
...performance.investments
|
|
], 'portfolio-positions')}
|
|
className="p-2 hover:bg-gray-100 dark:hover:bg-slate-700 rounded transition-colors"
|
|
title="Download CSV"
|
|
>
|
|
<Download size={16} />
|
|
</button>
|
|
</div>
|
|
<div className="rounded-lg">
|
|
<table className="min-w-full bg-white dark:bg-slate-800">
|
|
<thead>
|
|
<tr className="bg-gray-100 dark:bg-slate-700 text-left">
|
|
<th className="px-4 py-2 first:rounded-tl-lg">Asset</th>
|
|
<th className="px-4 py-2">Type</th>
|
|
<th className="px-4 py-2">Date</th>
|
|
<th className="px-4 py-2">Invested Amount</th>
|
|
<th className="px-4 py-2">
|
|
<Tooltip content={currentAmountTooltip}>
|
|
Current Amount
|
|
</Tooltip>
|
|
</th>
|
|
<th className="px-4 py-2">
|
|
<Tooltip content={buyInTooltip}>
|
|
Buy-In (avg)
|
|
</Tooltip>
|
|
</th>
|
|
<th className="px-4 py-2">
|
|
<Tooltip content={performanceTooltip}>
|
|
Performance (%)
|
|
</Tooltip>
|
|
</th>
|
|
<th className="px-4 py-2 last:rounded-tr-lg">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{performance.summary && (
|
|
<>
|
|
|
|
<tr className="font-bold bg-gray-50 dark:bg-slate-700 border-t border-gray-200 dark:border-slate-600">
|
|
<td className="px-4 py-2">Total Portfolio</td>
|
|
<td className="px-4 py-2"></td>
|
|
<td className="px-4 py-2"></td>
|
|
<td className="px-4 py-2">€{performance.summary.totalInvested.toFixed(2)}</td>
|
|
<td className="px-4 py-2">€{performance.summary.currentValue.toFixed(2)}</td>
|
|
<td className="px-4 py-2"></td>
|
|
<td className="px-4 py-2">
|
|
{performance.summary.performancePercentage.toFixed(2)}%
|
|
<ul>
|
|
<li className="text-xs text-gray-500 dark:text-gray-400">(avg. acc. {averagePerformance}%)</li>
|
|
<li className="text-xs text-gray-500 dark:text-gray-400">(avg. p.a. {performance.summary.performancePerAnnoPerformance.toFixed(2)}%)</li>
|
|
<li className="text-[10px] text-gray-500 dark:text-gray-400 italic">(best p.a. {performance.summary.bestPerformancePerAnno?.[0]?.percentage?.toFixed(2) || "0.00"}% {performance.summary.bestPerformancePerAnno?.[0]?.year || "N/A"})</li>
|
|
<li className="text-[10px] text-gray-500 dark:text-gray-400 italic">(worst p.a. {performance.summary.worstPerformancePerAnno?.[0]?.percentage?.toFixed(2) || "0.00"}% {performance.summary.worstPerformancePerAnno?.[0]?.year || "N/A"})</li>
|
|
</ul>
|
|
</td>
|
|
<td className="px-4 py-2"></td>
|
|
</tr>
|
|
|
|
|
|
<tr className="italic dark:text-gray-500 border-t border-gray-200 dark:border-slate-600 ">
|
|
<td className="px-4 py-2">TTWOR</td>
|
|
<td className="px-4 py-2"></td>
|
|
<td className="px-4 py-2">{new Date(performance.investments[0]?.date).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })}</td>
|
|
<td className="px-4 py-2">€{performance.summary.totalInvested.toFixed(2)}</td>
|
|
<td className="px-4 py-2">€{performance.summary.ttworValue.toFixed(2)}</td>
|
|
<td className="px-4 py-2"></td>
|
|
<td className="px-4 py-2"><Tooltip content={ttworTooltip}>{performance.summary.ttworPercentage.toFixed(2)}%</Tooltip></td>
|
|
<td className="px-4 py-2"></td>
|
|
</tr>
|
|
</>
|
|
)}
|
|
{performance.investments.sort((a, b) => isBefore(a.date, b.date) ? -1 : 1).map((inv, index) => {
|
|
const asset = assets.find(a => a.name === inv.assetName)!;
|
|
const investment = asset.investments.find(i => i.id === inv.id)! || inv;
|
|
const filtered = performance.investments.filter(v => v.assetName === inv.assetName);
|
|
const avgBuyIn = filtered.reduce((acc, curr) => acc + curr.investedAtPrice, 0) / filtered.length;
|
|
const isLast = index === performance.investments.length - 1;
|
|
|
|
return (
|
|
<tr key={inv.id} className={`border-t border-gray-200 dark:border-slate-600 ${isLast ? 'last:rounded-b-lg' : ''}`}>
|
|
<td className={`px-4 py-2 ${isLast ? 'first:rounded-bl-lg' : ''}`}>{inv.assetName}</td>
|
|
<td className="px-4 py-2">
|
|
{investment?.type === 'periodic' ? (
|
|
<span className="inline-flex items-center px-2 py-1 bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200 rounded-full">
|
|
<RefreshCw className="w-4 h-4 mr-1" />
|
|
SavingsPlan
|
|
</span>
|
|
) : (
|
|
<span className="inline-flex items-center px-2 py-1 bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 rounded-full">
|
|
<ShoppingBag className="w-4 h-4 mr-1" />
|
|
OneTime
|
|
</span>
|
|
)}
|
|
</td>
|
|
<td className="px-4 py-2">{format(new Date(inv.date), 'dd.MM.yyyy')}</td>
|
|
<td className="px-4 py-2">€{inv.investedAmount.toFixed(2)}</td>
|
|
<td className="px-4 py-2">€{inv.currentValue.toFixed(2)}</td>
|
|
<td className="px-4 py-2">€{inv.investedAtPrice.toFixed(2)} (€{avgBuyIn.toFixed(2)})</td>
|
|
<td className="px-4 py-2">{inv.performancePercentage.toFixed(2)}%</td>
|
|
<td className={`px-4 py-2 ${isLast ? 'last:rounded-br-lg' : ''}`}>
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={() => setEditingInvestment({ investment, assetId: asset.id })}
|
|
className="p-1 hover:bg-gray-100 dark:hover:bg-slate-700 rounded transition-colors"
|
|
>
|
|
<Pencil className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
onClick={() => handleDelete(inv.id, asset.id)}
|
|
className="p-1 hover:bg-gray-100 dark:hover:bg-slate-700 rounded text-red-500 transition-colors"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{editingInvestment && (
|
|
<EditInvestmentModal
|
|
investment={editingInvestment.investment}
|
|
assetId={editingInvestment.assetId}
|
|
onClose={() => setEditingInvestment(null)}
|
|
/>
|
|
)}
|
|
{showProjection && (
|
|
<FutureProjectionModal
|
|
performancePerAnno={performance.summary.performancePerAnnoPerformance}
|
|
bestPerformancePerAnno={performance.summary.bestPerformancePerAnno}
|
|
worstPerformancePerAnno={performance.summary.worstPerformancePerAnno}
|
|
onClose={() => setShowProjection(false)}
|
|
/>
|
|
)}
|
|
{editingSavingsPlan && (
|
|
<EditSavingsPlanModal
|
|
{...editingSavingsPlan}
|
|
onClose={() => setEditingSavingsPlan(null)}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|