v1.1.1 finalize design and feature, allow savingsplans to edit and deleted with allocation overview

This commit is contained in:
tomato6966 2024-12-23 19:05:20 +01:00
parent a5d014ec4d
commit 5f9b4f0797
4 changed files with 93 additions and 13 deletions

View file

@ -1,7 +1,7 @@
{ {
"name": "investment-portfolio-tracker", "name": "investment-portfolio-tracker",
"private": true, "private": true,
"version": "1.1.0", "version": "1.1.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View file

@ -54,7 +54,7 @@ export default function InvestmentFormWrapper() {
selectedAsset && ( selectedAsset && (
<div className="flex-1 h-[calc(100%-120px)] overflow-hidden"> <div className="flex-1 h-[calc(100%-120px)] overflow-hidden">
<div className="p-6 pr-3 pt-0"> <div className="p-6 pr-3 pt-0">
<InvestmentForm assetId={selectedAsset} clearSelectedAsset={() => setSelectedAsset(null)} /> <InvestmentForm assetId={selectedAsset} />
</div> </div>
</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 [type, setType] = useState<'single' | 'periodic'>('single');
const [amount, setAmount] = useState(''); const [amount, setAmount] = useState('');
const [date, setDate] = useState(''); const [date, setDate] = useState('');
@ -80,7 +80,7 @@ const InvestmentForm = ({ assetId, clearSelectedAsset }: { assetId: string, clea
addInvestment: state.addInvestment, addInvestment: state.addInvestment,
})); }));
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -90,7 +90,7 @@ const InvestmentForm = ({ assetId, clearSelectedAsset }: { assetId: string, clea
setIsSubmitting(true); setIsSubmitting(true);
setTimeout(async () => { setTimeout(() => {
console.log("timeout") console.log("timeout")
try { try {
if (type === "single") { if (type === "single") {
@ -136,7 +136,6 @@ const InvestmentForm = ({ assetId, clearSelectedAsset }: { assetId: string, clea
console.timeEnd('generatePeriodicInvestments'); console.timeEnd('generatePeriodicInvestments');
setIsSubmitting(false); setIsSubmitting(false);
setAmount(''); setAmount('');
clearSelectedAsset();
} }
}, 10); }, 10);
}; };

View file

@ -282,20 +282,47 @@ export const FutureProjectionModal = ({
const renderScenarioDescription = () => { const renderScenarioDescription = () => {
if (!scenarios.best.projection.length) return null; 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 ( return (
<div className="mb-4 p-4 bg-gray-50 dark:bg-slate-800/50 rounded-lg text-sm"> <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> <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"> <ul className="space-y-2 text-gray-600 dark:text-gray-400">
<li> <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>
<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)}%, <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>
<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)}%, <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> </li>
</ul> </ul>
</div> </div>

View file

@ -14,6 +14,16 @@ import { EditSavingsPlanModal } from "./Modals/EditSavingsPlanModal";
import { FutureProjectionModal } from "./Modals/FutureProjectionModal"; import { FutureProjectionModal } from "./Modals/FutureProjectionModal";
import { Tooltip } from "./utils/ToolTip"; 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() { export default function PortfolioTable() {
const { assets, removeInvestment, clearInvestments } = usePortfolioSelector((state) => ({ const { assets, removeInvestment, clearInvestments } = usePortfolioSelector((state) => ({
assets: state.assets, assets: state.assets,
@ -110,9 +120,17 @@ export default function PortfolioTable() {
const savingsPlansPerformance = useMemo(() => { const savingsPlansPerformance = useMemo(() => {
if(isSavingsPlanOverviewDisabled) return []; 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) { for (const asset of assets) {
const savingsPlans = asset.investments.filter(inv => inv.type === 'periodic'); const savingsPlans = asset.investments.filter(inv => inv.type === 'periodic');
const amount = savingsPlans.reduce((sum, inv) => sum + inv.amount, 0);
if (savingsPlans.length > 0) { if (savingsPlans.length > 0) {
const assetPerformance = calculateInvestmentPerformance([{ const assetPerformance = calculateInvestmentPerformance([{
...asset, ...asset,
@ -121,13 +139,35 @@ export default function PortfolioTable() {
performance.push({ performance.push({
assetName: asset.name, assetName: asset.name,
amount: savingsPlans[0].amount, amount: savingsPlans[0].amount,
...assetPerformance.summary ...assetPerformance.summary,
allocation: amount / totalSavingsPlansAmount * 100
}); });
} }
} }
return performance; return performance;
}, [assets, isSavingsPlanOverviewDisabled]); }, [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 () => { const handleGeneratePDF = async () => {
setIsGeneratingPDF(true); setIsGeneratingPDF(true);
try { try {
@ -224,6 +264,7 @@ export default function PortfolioTable() {
<tr className="bg-gray-100 dark:bg-slate-700 text-left"> <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 first:rounded-tl-lg">Asset</th>
<th className="px-4 py-2">Interval Amount</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">Total Invested</th>
<th className="px-4 py-2">Current Value</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 (%)</th>
@ -232,7 +273,19 @@ export default function PortfolioTable() {
</tr> </tr>
</thead> </thead>
<tbody> <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 asset = assets.find(a => a.name === plan.assetName)!;
const firstInvestment = asset.investments.find(inv => inv.type === 'periodic')!; const firstInvestment = asset.investments.find(inv => inv.type === 'periodic')!;
const groupId = firstInvestment.periodicGroupId!; const groupId = firstInvestment.periodicGroupId!;
@ -240,7 +293,8 @@ export default function PortfolioTable() {
return ( return (
<tr key={plan.assetName} className="border-t border-gray-200 dark:border-slate-600"> <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.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.totalInvested.toFixed(2)}</td>
<td className="px-4 py-2">{plan.currentValue.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.performancePercentage.toFixed(2)}%</td>