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",
"private": true,
"version": "1.1.0",
"version": "1.1.1",
"type": "module",
"scripts": {
"dev": "vite",

View file

@ -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);
};

View file

@ -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>

View file

@ -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>