mirror of
https://github.com/Tomato6966/investment-portfolio-simulator.git
synced 2025-04-07 12:00:35 +02:00
v1.1.1 finalize design and feature, allow savingsplans to edit and deleted with allocation overview
This commit is contained in:
parent
a5d014ec4d
commit
5f9b4f0797
4 changed files with 93 additions and 13 deletions
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "investment-portfolio-tracker",
|
||||
"private": true,
|
||||
"version": "1.1.0",
|
||||
"version": "1.1.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
|
|
@ -54,7 +54,7 @@ export default function InvestmentFormWrapper() {
|
|||
selectedAsset && (
|
||||
<div className="flex-1 h-[calc(100%-120px)] overflow-hidden">
|
||||
<div className="p-6 pr-3 pt-0">
|
||||
<InvestmentForm assetId={selectedAsset} clearSelectedAsset={() => setSelectedAsset(null)} />
|
||||
<InvestmentForm assetId={selectedAsset} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -63,7 +63,7 @@ export default function InvestmentFormWrapper() {
|
|||
);
|
||||
}
|
||||
|
||||
const InvestmentForm = ({ assetId, clearSelectedAsset }: { assetId: string, clearSelectedAsset: () => void }) => {
|
||||
const InvestmentForm = ({ assetId }: { assetId: string }) => {
|
||||
const [type, setType] = useState<'single' | 'periodic'>('single');
|
||||
const [amount, setAmount] = useState('');
|
||||
const [date, setDate] = useState('');
|
||||
|
@ -80,7 +80,7 @@ const InvestmentForm = ({ assetId, clearSelectedAsset }: { assetId: string, clea
|
|||
addInvestment: state.addInvestment,
|
||||
}));
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
|
@ -90,7 +90,7 @@ const InvestmentForm = ({ assetId, clearSelectedAsset }: { assetId: string, clea
|
|||
|
||||
setIsSubmitting(true);
|
||||
|
||||
setTimeout(async () => {
|
||||
setTimeout(() => {
|
||||
console.log("timeout")
|
||||
try {
|
||||
if (type === "single") {
|
||||
|
@ -136,7 +136,6 @@ const InvestmentForm = ({ assetId, clearSelectedAsset }: { assetId: string, clea
|
|||
console.timeEnd('generatePeriodicInvestments');
|
||||
setIsSubmitting(false);
|
||||
setAmount('');
|
||||
clearSelectedAsset();
|
||||
}
|
||||
}, 10);
|
||||
};
|
||||
|
|
|
@ -282,20 +282,47 @@ export const FutureProjectionModal = ({
|
|||
const renderScenarioDescription = () => {
|
||||
if (!scenarios.best.projection.length) return null;
|
||||
|
||||
const getLastValue = (projection: ProjectionData[]) => {
|
||||
const lastPoint = projection[projection.length - 1];
|
||||
return {
|
||||
value: lastPoint.value,
|
||||
invested: lastPoint.invested,
|
||||
returnPercentage: ((lastPoint.value - lastPoint.invested) / lastPoint.invested) * 100
|
||||
};
|
||||
};
|
||||
|
||||
const baseCase = getLastValue(projectionData);
|
||||
const bestCase = getLastValue(scenarios.best.projection);
|
||||
const worstCase = getLastValue(scenarios.worst.projection);
|
||||
|
||||
return (
|
||||
<div className="mb-4 p-4 bg-gray-50 dark:bg-slate-800/50 rounded-lg text-sm">
|
||||
<h4 className="font-semibold mb-2 dark:text-gray-200">Scenario Calculations</h4>
|
||||
<ul className="space-y-2 text-gray-600 dark:text-gray-400">
|
||||
<li>
|
||||
<span className="font-medium text-indigo-600 dark:text-indigo-400">Avg. Base Case:</span> Using historical average return of <span className="font-bold underline">{performancePerAnno.toFixed(2)}%</span>
|
||||
<span className="font-medium text-indigo-600 dark:text-indigo-400">Avg. Base Case:</span> Using historical average return of{' '}
|
||||
<span className="font-bold underline">{performancePerAnno.toFixed(2)}%</span>
|
||||
<i className="block text-gray-300 dark:text-gray-500">
|
||||
After {years} years you'd have{' '}
|
||||
<span className="font-bold">{formatCurrency(baseCase.value)}</span> from {formatCurrency(baseCase.invested)} invested,{' '}
|
||||
that's a total return of <span className="font-bold">{baseCase.returnPercentage.toFixed(2)}%</span>
|
||||
</i>
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-medium text-green-600 dark:text-green-400">Best Case:</span> Average of top 50% performing years ({scenarios.best.avaragedAmount} years) at {scenarios.best.percentage.toFixed(2)}%,
|
||||
averaged with base case to <span className="font-semibold underline">{scenarios.best.percentageAveraged.toFixed(2)}%</span>
|
||||
averaged with base case to <span className="font-semibold underline">{scenarios.best.percentageAveraged.toFixed(2)}%</span>.{' '}
|
||||
<i className="block text-gray-300 dark:text-gray-500">
|
||||
After {years} years you'd have <span className="font-bold">{formatCurrency(bestCase.value)}</span> from {formatCurrency(bestCase.invested)} invested,{' '}
|
||||
that's a total return of <span className="font-bold">{bestCase.returnPercentage.toFixed(2)}%</span>
|
||||
</i>
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-medium text-red-600 dark:text-red-400">Worst Case:</span> Average of bottom 50% performing years ({scenarios.worst.avaragedAmount} years) at {scenarios.worst.percentage.toFixed(2)}%,
|
||||
averaged with base case to <span className="font-semibold underline">{scenarios.worst.percentageAveraged.toFixed(2)}%</span>
|
||||
averaged with base case to <span className="font-semibold underline">{scenarios.worst.percentageAveraged.toFixed(2)}%</span>.{' '}
|
||||
<i className="block text-gray-300 dark:text-gray-500">
|
||||
After {years} years you'd have <span className="font-bold">{formatCurrency(worstCase.value)}</span> from {formatCurrency(worstCase.invested)} invested,{' '}
|
||||
that's a total return of <span className="font-bold">{worstCase.returnPercentage.toFixed(2)}%</span>
|
||||
</i>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -14,6 +14,16 @@ 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,
|
||||
|
@ -110,9 +120,17 @@ export default function PortfolioTable() {
|
|||
|
||||
const savingsPlansPerformance = useMemo(() => {
|
||||
if(isSavingsPlanOverviewDisabled) return [];
|
||||
const performance = [];
|
||||
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,
|
||||
|
@ -121,13 +139,35 @@ export default function PortfolioTable() {
|
|||
performance.push({
|
||||
assetName: asset.name,
|
||||
amount: savingsPlans[0].amount,
|
||||
...assetPerformance.summary
|
||||
...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 {
|
||||
|
@ -224,6 +264,7 @@ export default function PortfolioTable() {
|
|||
<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>
|
||||
|
@ -232,7 +273,19 @@ export default function PortfolioTable() {
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{savingsPlansPerformance.map((plan) => {
|
||||
{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!;
|
||||
|
@ -240,7 +293,8 @@ export default function PortfolioTable() {
|
|||
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}</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>
|
||||
|
|
Loading…
Add table
Reference in a new issue