From a5d014ec4d22959b586191d4f0531e3de481f0c4 Mon Sep 17 00:00:00 2001
From: tomato6966 <chris.pre03@gmail.com>
Date: Mon, 23 Dec 2024 18:28:37 +0100
Subject: [PATCH] v1.1.0 - ability to edit savings plan, correct %performance
 calculation

---
 package-lock.json                             |  26 ++
 package.json                                  |   3 +-
 src/App.tsx                                   |   2 +
 src/components/InvestmentForm.tsx             |  20 +-
 src/components/Modals/AddAssetModal.tsx       |   9 +-
 src/components/Modals/EditInvestmentModal.tsx |  16 +-
 .../Modals/EditSavingsPlanModal.tsx           | 236 ++++++++++++++++++
 src/components/PortfolioTable.tsx             | 142 ++++++++---
 src/providers/PortfolioProvider.tsx           |   8 +-
 src/services/yahooFinanceService.ts           |   3 +
 src/utils/calculations/performance.ts         |  20 +-
 src/utils/calculations/portfolioValue.ts      |  24 +-
 12 files changed, 452 insertions(+), 57 deletions(-)
 create mode 100644 src/components/Modals/EditSavingsPlanModal.tsx

diff --git a/package-lock.json b/package-lock.json
index 8928217..f6632b1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,6 +14,7 @@
         "lucide-react": "^0.469.0",
         "react": "^19.0.0",
         "react-dom": "^19.0.0",
+        "react-hot-toast": "^2.4.1",
         "react-router-dom": "^7.1.0",
         "recharts": "^2.15.0",
         "use-debounce": "^10.0.4"
@@ -2861,6 +2862,15 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/goober": {
+      "version": "2.1.16",
+      "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz",
+      "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==",
+      "license": "MIT",
+      "peerDependencies": {
+        "csstype": "^3.0.10"
+      }
+    },
     "node_modules/graphemer": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
@@ -3735,6 +3745,22 @@
         "react": "^19.0.0"
       }
     },
+    "node_modules/react-hot-toast": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz",
+      "integrity": "sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==",
+      "license": "MIT",
+      "dependencies": {
+        "goober": "^2.1.10"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "peerDependencies": {
+        "react": ">=16",
+        "react-dom": ">=16"
+      }
+    },
     "node_modules/react-is": {
       "version": "18.3.1",
       "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
diff --git a/package.json b/package.json
index e1905e7..c35ea0b 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "investment-portfolio-tracker",
   "private": true,
-  "version": "1.0.0",
+  "version": "1.1.0",
   "type": "module",
   "scripts": {
     "dev": "vite",
@@ -16,6 +16,7 @@
     "lucide-react": "^0.469.0",
     "react": "^19.0.0",
     "react-dom": "^19.0.0",
+    "react-hot-toast": "^2.4.1",
     "react-router-dom": "^7.1.0",
     "recharts": "^2.15.0",
     "use-debounce": "^10.0.4"
diff --git a/src/App.tsx b/src/App.tsx
index 1886c6f..2c124ce 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,4 +1,5 @@
 import { lazy, Suspense, useState } from "react";
+import { Toaster } from "react-hot-toast";
 
 import { AppShell } from "./components/Landing/AppShell";
 import { LoadingPlaceholder } from "./components/utils/LoadingPlaceholder";
@@ -19,6 +20,7 @@ export default function App() {
                     />
                 </Suspense>
             </AppShell>
+            <Toaster position="bottom-right" />
         </PortfolioProvider>
     );
 }
diff --git a/src/components/InvestmentForm.tsx b/src/components/InvestmentForm.tsx
index ebcfb84..d3c503a 100644
--- a/src/components/InvestmentForm.tsx
+++ b/src/components/InvestmentForm.tsx
@@ -1,5 +1,6 @@
 import { Loader2 } from "lucide-react";
 import React, { useState } from "react";
+import toast from "react-hot-toast";
 
 import { usePortfolioSelector } from "../hooks/usePortfolio";
 import { generatePeriodicInvestments } from "../utils/calculations/assetValue";
@@ -81,9 +82,16 @@ const InvestmentForm = ({ assetId, clearSelectedAsset }: { assetId: string, clea
 
     const handleSubmit = async (e: React.FormEvent) => {
         e.preventDefault();
+        e.stopPropagation();
+
+        console.log("submitting")
+        console.time('generatePeriodicInvestments');
+        console.timeLog('generatePeriodicInvestments', "1");
+
         setIsSubmitting(true);
 
         setTimeout(async () => {
+            console.log("timeout")
             try {
                 if (type === "single") {
                     const investment = {
@@ -94,6 +102,7 @@ const InvestmentForm = ({ assetId, clearSelectedAsset }: { assetId: string, clea
                         date
                     };
                     addInvestment(assetId, investment);
+                    toast.success('Investment added successfully');
                 } else {
                     const periodicSettings = {
                         startDate: date,
@@ -108,18 +117,23 @@ const InvestmentForm = ({ assetId, clearSelectedAsset }: { assetId: string, clea
                             },
                         } : undefined),
                     };
+                    console.timeLog('generatePeriodicInvestments', "2");
 
                     const investments = generatePeriodicInvestments(
                         periodicSettings,
                         dateRange.endDate,
                         assetId
                     );
+                    console.timeLog('generatePeriodicInvestments', "3");
+                    addInvestment(assetId, investments);
 
-                    for (const investment of investments) {
-                        addInvestment(assetId, investment);
-                    }
+                    toast.success('Periodic investment plan created successfully');
                 }
+            } catch (error:any) {
+                toast.error('Failed to add investment. Please try again.' + String(error?.message || error));
             } finally {
+                console.timeLog('generatePeriodicInvestments', "4");
+                console.timeEnd('generatePeriodicInvestments');
                 setIsSubmitting(false);
                 setAmount('');
                 clearSelectedAsset();
diff --git a/src/components/Modals/AddAssetModal.tsx b/src/components/Modals/AddAssetModal.tsx
index 6b2eaa3..53140c7 100644
--- a/src/components/Modals/AddAssetModal.tsx
+++ b/src/components/Modals/AddAssetModal.tsx
@@ -1,5 +1,6 @@
 import { Loader2, Search, X } from "lucide-react";
 import { useState } from "react";
+import toast from "react-hot-toast";
 import { useDebouncedCallback } from "use-debounce";
 
 import { usePortfolioSelector } from "../../hooks/usePortfolio";
@@ -43,17 +44,23 @@ export default function AddAssetModal({ onClose }: { onClose: () => void }) {
                     dateRange.endDate
                 );
 
+                if (historicalData.length === 0) {
+                    toast.error(`No historical data available for ${asset.name}`);
+                    return;
+                }
+
                 const assetWithHistory = {
                     ...asset,
-                    // override name with the fetched long Name if available
                     name: longName || asset.name,
                     historicalData,
                 };
 
                 addAsset(assetWithHistory);
+                toast.success(`Successfully added ${assetWithHistory.name}`);
                 onClose();
             } catch (error) {
                 console.error('Error fetching historical data:', error);
+                toast.error(`Failed to add ${asset.name}. Please try again.`);
             } finally {
                 setLoading(null);
             }
diff --git a/src/components/Modals/EditInvestmentModal.tsx b/src/components/Modals/EditInvestmentModal.tsx
index 1727a6b..a871f99 100644
--- a/src/components/Modals/EditInvestmentModal.tsx
+++ b/src/components/Modals/EditInvestmentModal.tsx
@@ -1,5 +1,6 @@
 import { X } from "lucide-react";
 import { useState } from "react";
+import toast from "react-hot-toast";
 
 import { usePortfolioSelector } from "../../hooks/usePortfolio";
 import { Investment } from "../../types";
@@ -18,11 +19,16 @@ export const EditInvestmentModal = ({ investment, assetId, onClose }: EditInvest
 
     const handleSubmit = (e: React.FormEvent) => {
         e.preventDefault();
-        updateInvestment(assetId, investment.id, {
-            ...investment,
-            amount: parseFloat(amount),
-        });
-        onClose();
+        try {
+            updateInvestment(assetId, investment.id, {
+                ...investment,
+                amount: parseFloat(amount),
+            });
+            toast.success('Investment updated successfully');
+            onClose();
+        } catch (error:any) {
+            toast.error('Failed to update investment' + String(error?.message || error));
+        }
     };
 
     return (
diff --git a/src/components/Modals/EditSavingsPlanModal.tsx b/src/components/Modals/EditSavingsPlanModal.tsx
new file mode 100644
index 0000000..3816f41
--- /dev/null
+++ b/src/components/Modals/EditSavingsPlanModal.tsx
@@ -0,0 +1,236 @@
+import { Loader2, X } from "lucide-react";
+import { useState } from "react";
+import toast from "react-hot-toast";
+
+import { usePortfolioSelector } from "../../hooks/usePortfolio";
+import { generatePeriodicInvestments } from "../../utils/calculations/assetValue";
+
+interface EditSavingsPlanModalProps {
+    assetId: string;
+    groupId: string;
+    amount: number;
+    dayOfMonth: number;
+    interval: number;
+    dynamic?: {
+        type: 'percentage' | 'fixed';
+        value: number;
+        yearInterval: number;
+    };
+    onClose: () => void;
+}
+
+export const EditSavingsPlanModal = ({
+    assetId,
+    groupId,
+    amount: initialAmount,
+    dayOfMonth: initialDayOfMonth,
+    interval: initialInterval,
+    dynamic: initialDynamic,
+    onClose
+}: EditSavingsPlanModalProps) => {
+    const [amount, setAmount] = useState(initialAmount.toString());
+    const [dayOfMonth, setDayOfMonth] = useState(initialDayOfMonth.toString());
+    const [interval, setInterval] = useState(initialInterval.toString());
+    const [isDynamic, setIsDynamic] = useState(!!initialDynamic);
+    const [dynamicType, setDynamicType] = useState<'percentage' | 'fixed'>(initialDynamic?.type || 'percentage');
+    const [dynamicValue, setDynamicValue] = useState(initialDynamic?.value.toString() || '');
+    const [yearInterval, setYearInterval] = useState(initialDynamic?.yearInterval.toString() || '1');
+    const [isSubmitting, setIsSubmitting] = useState(false);
+
+    const { dateRange, addInvestment, removeInvestment, assets } = usePortfolioSelector((state) => ({
+        dateRange: state.dateRange,
+        addInvestment: state.addInvestment,
+        removeInvestment: state.removeInvestment,
+        assets: state.assets,
+    }));
+
+    const handleSubmit = async (e: React.FormEvent) => {
+        e.preventDefault();
+        e.stopPropagation();
+        setIsSubmitting(true);
+
+        setTimeout(async () => {
+            try {
+                // First, remove all existing investments for this savings plan
+                const asset = assets.find(a => a.id === assetId)!;
+                const investments = asset.investments.filter(inv => inv.periodicGroupId === groupId);
+                const startDate = investments[0].date!; // Keep original start date
+
+                investments.forEach(inv => {
+                    removeInvestment(assetId, inv.id);
+                });
+
+                // Generate and add new investments
+                const periodicSettings = {
+                    startDate,
+                    dayOfMonth: parseInt(dayOfMonth),
+                    interval: parseInt(interval),
+                    amount: parseFloat(amount),
+                    ...(isDynamic ? {
+                        dynamic: {
+                            type: dynamicType,
+                            value: parseFloat(dynamicValue),
+                            yearInterval: parseInt(yearInterval),
+                        },
+                    } : undefined),
+                };
+
+                const newInvestments = generatePeriodicInvestments(
+                    periodicSettings,
+                    dateRange.endDate,
+                    assetId
+                );
+
+                addInvestment(assetId, newInvestments);
+                toast.success('Savings plan updated successfully');
+                onClose();
+            } catch (error:any) {
+                toast.error('Failed to update savings plan: ' + String(error?.message || error));
+            } finally {
+                setIsSubmitting(false);
+            }
+        }, 10);
+    };
+
+    return (
+        <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
+            <div className="bg-white dark:bg-slate-800 rounded-lg p-6 w-full max-w-lg">
+                <div className="flex justify-between items-center mb-4">
+                    <h2 className="text-xl font-bold dark:text-gray-200">Edit Savings Plan</h2>
+                    <button onClick={onClose} className="p-2">
+                        <X className="w-6 h-6 dark:text-gray-200" />
+                    </button>
+                </div>
+
+                <form onSubmit={handleSubmit} className="space-y-4">
+                    <div>
+                        <label className="block text-sm font-medium mb-1 dark:text-gray-200">
+                            Investment Amount
+                        </label>
+                        <input
+                            type="number"
+                            value={amount}
+                            onChange={(e) => setAmount(e.target.value)}
+                            className="w-full p-2 border rounded dark:bg-slate-800 dark:border-slate-700 dark:outline-none dark:text-gray-300"
+                            step="0.01"
+                            min="0"
+                            required
+                        />
+                    </div>
+
+                    <div>
+                        <label className="block text-sm font-medium mb-1 dark:text-gray-200">
+                            Day of Month
+                        </label>
+                        <input
+                            type="number"
+                            value={dayOfMonth}
+                            onChange={(e) => setDayOfMonth(e.target.value)}
+                            className="w-full p-2 border rounded dark:bg-slate-800 dark:border-slate-700 dark:outline-none dark:text-gray-300"
+                            min="1"
+                            max="31"
+                            required
+                        />
+                    </div>
+
+                    <div>
+                        <label className="block text-sm font-medium mb-1 dark:text-gray-200">
+                            Interval (days)
+                        </label>
+                        <input
+                            type="number"
+                            value={interval}
+                            onChange={(e) => setInterval(e.target.value)}
+                            className="w-full p-2 border rounded dark:bg-slate-800 dark:border-slate-700 dark:outline-none dark:text-gray-300"
+                            min="1"
+                            required
+                        />
+                    </div>
+
+                    <div>
+                        <label className="flex items-center gap-2 dark:text-gray-200">
+                            <input
+                                type="checkbox"
+                                checked={isDynamic}
+                                onChange={(e) => setIsDynamic(e.target.checked)}
+                                className="rounded"
+                            />
+                            <span className="text-sm font-medium">Dynamic Investment Growth</span>
+                        </label>
+                    </div>
+
+                    {isDynamic && (
+                        <>
+                            <div>
+                                <label className="block text-sm font-medium mb-1 dark:text-gray-200">
+                                    Growth Type
+                                </label>
+                                <select
+                                    value={dynamicType}
+                                    onChange={(e) => setDynamicType(e.target.value as 'percentage' | 'fixed')}
+                                    className="w-full p-2 border rounded dark:bg-slate-800 dark:border-slate-700 dark:outline-none dark:text-gray-300"
+                                >
+                                    <option value="percentage">Percentage</option>
+                                    <option value="fixed">Fixed Amount</option>
+                                </select>
+                            </div>
+
+                            <div>
+                                <label className="block text-sm font-medium mb-1 dark:text-gray-200">
+                                    Increase Value
+                                </label>
+                                <input
+                                    type="number"
+                                    value={dynamicValue}
+                                    onChange={(e) => setDynamicValue(e.target.value)}
+                                    className="w-full p-2 border rounded dark:bg-slate-800 dark:border-slate-700 dark:outline-none dark:text-gray-300"
+                                    min="0"
+                                    step={dynamicType === 'percentage' ? '0.1' : '1'}
+                                    required
+                                />
+                            </div>
+
+                            <div>
+                                <label className="block text-sm font-medium mb-1 dark:text-gray-200">
+                                    Year Interval for Increase
+                                </label>
+                                <input
+                                    type="number"
+                                    value={yearInterval}
+                                    onChange={(e) => setYearInterval(e.target.value)}
+                                    className="w-full p-2 border rounded dark:bg-slate-800 dark:border-slate-700 dark:outline-none dark:text-gray-300"
+                                    min="1"
+                                    required
+                                />
+                            </div>
+                        </>
+                    )}
+
+                    <div className="flex justify-end gap-2">
+                        <button
+                            type="button"
+                            onClick={onClose}
+                            className="px-4 py-2 border rounded hover:bg-gray-100 dark:hover:bg-slate-700 dark:border-slate-600 dark:text-gray-200"
+                        >
+                            Cancel
+                        </button>
+                        <button
+                            type="submit"
+                            disabled={isSubmitting}
+                            className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
+                        >
+                            {isSubmitting ? (
+                                <>
+                                    <Loader2 className="animate-spin" size={16} />
+                                    Updating...
+                                </>
+                            ) : (
+                                'Update Plan'
+                            )}
+                        </button>
+                    </div>
+                </form>
+            </div>
+        </div>
+    );
+};
diff --git a/src/components/PortfolioTable.tsx b/src/components/PortfolioTable.tsx
index e2e312a..9d01de6 100644
--- a/src/components/PortfolioTable.tsx
+++ b/src/components/PortfolioTable.tsx
@@ -3,12 +3,14 @@ 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";
 
@@ -23,8 +25,20 @@ export default function PortfolioTable() {
         investment: Investment;
         assetId: string;
     } | null>(null);
-    const [showSavingsPlans, setShowSavingsPlans] = useState(true);
     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]);
 
@@ -34,13 +48,23 @@ export default function PortfolioTable() {
 
     const handleDelete = useCallback((investmentId: string, assetId: string) => {
         if (window.confirm("Are you sure you want to delete this investment?")) {
-            removeInvestment(assetId, investmentId);
+            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?")) {
-            clearInvestments();
+            try {
+                clearInvestments();
+                toast.success('All investments cleared successfully');
+            } catch (error:any) {
+                toast.error('Failed to clear investments' + String(error?.message || error));
+            }
         }
     }, [clearInvestments]);
 
@@ -113,11 +137,39 @@ export default function PortfolioTable() {
                 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">
@@ -140,15 +192,6 @@ export default function PortfolioTable() {
                             Future Projection
                         </button>
 
-                        <button
-                            onClick={() => setShowSavingsPlans(prev => !prev)}
-                            disabled={isSavingsPlanOverviewDisabled}
-                            className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 flex items-center gap-2 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50"
-                        >
-                            <RefreshCw size={16} />
-                            {showSavingsPlans ? 'Hide' : 'Show'} Savings Plans Performance
-                        </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"
@@ -164,7 +207,7 @@ export default function PortfolioTable() {
                     </div>
                 </div>
 
-                {!isSavingsPlanOverviewDisabled && showSavingsPlans && savingsPlansPerformance.length > 0 && (
+                {!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>
@@ -184,20 +227,57 @@ export default function PortfolioTable() {
                                     <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 last:rounded-tr-lg">Performance (p.a.)</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>
-                                {savingsPlansPerformance.map((plan) => (
-                                    <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.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>
-                                    </tr>
-                                ))}
+                                {savingsPlansPerformance.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}</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: parseInt(firstInvestment.date!.split('-')[2]),
+                                                            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>
@@ -276,10 +356,10 @@ export default function PortfolioTable() {
                                             <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-xs text-gray-500 dark:text-gray-400"> (best p.a. {performance.summary.bestPerformancePerAnno?.[0]?.percentage?.toFixed(2) || "0.00"}% {performance.summary.bestPerformancePerAnno?.[0]?.year || "N/A"})</li>
-                                                    <li className="text-xs text-gray-500 dark:text-gray-400"> (worst p.a. {performance.summary.worstPerformancePerAnno?.[0]?.percentage?.toFixed(2) || "0.00"}% {performance.summary.worstPerformancePerAnno?.[0]?.year || "N/A"})</li>
+                                                    <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>
@@ -366,6 +446,12 @@ export default function PortfolioTable() {
                     onClose={() => setShowProjection(false)}
                 />
             )}
+            {editingSavingsPlan && (
+                <EditSavingsPlanModal
+                    {...editingSavingsPlan}
+                    onClose={() => setEditingSavingsPlan(null)}
+                />
+            )}
         </div>
     );
 };
diff --git a/src/providers/PortfolioProvider.tsx b/src/providers/PortfolioProvider.tsx
index db88119..9bb69a3 100644
--- a/src/providers/PortfolioProvider.tsx
+++ b/src/providers/PortfolioProvider.tsx
@@ -16,7 +16,7 @@ type PortfolioAction =
     | { type: 'ADD_ASSET'; payload: Asset }
     | { type: 'REMOVE_ASSET'; payload: string }
     | { type: 'CLEAR_ASSETS' }
-    | { type: 'ADD_INVESTMENT'; payload: { assetId: string; investment: Investment } }
+    | { type: 'ADD_INVESTMENT'; payload: { assetId: string; investment: Investment | Investment[] } }
     | { type: 'REMOVE_INVESTMENT'; payload: { assetId: string; investmentId: string } }
     | { type: 'UPDATE_DATE_RANGE'; payload: DateRange }
     | { type: 'UPDATE_ASSET_HISTORICAL_DATA'; payload: { assetId: string; historicalData: HistoricalData[]; longName?: string } }
@@ -57,7 +57,7 @@ const portfolioReducer = (state: PortfolioState, action: PortfolioAction): Portf
                 ...state,
                 assets: state.assets.map(asset =>
                     asset.id === action.payload.assetId
-                        ? { ...asset, investments: [...asset.investments, action.payload.investment] }
+                        ? { ...asset, investments: [...asset.investments, ...(Array.isArray(action.payload.investment) ? action.payload.investment : [action.payload.investment])] }
                         : asset
                 )
             };
@@ -127,7 +127,7 @@ export interface PortfolioContextType extends PortfolioState {
     addAsset: (asset: Asset) => void;
     removeAsset: (assetId: string) => void;
     clearAssets: () => void;
-    addInvestment: (assetId: string, investment: Investment) => void;
+    addInvestment: (assetId: string, investment: Investment | Investment[]) => void;
     removeInvestment: (assetId: string, investmentId: string) => void;
     updateDateRange: (dateRange: DateRange) => void;
     updateAssetHistoricalData: (assetId: string, historicalData: HistoricalData[], longName?: string) => void;
@@ -148,7 +148,7 @@ export const PortfolioProvider = ({ children }: { children: React.ReactNode }) =
         addAsset: (asset: Asset) => dispatch({ type: 'ADD_ASSET', payload: asset }),
         removeAsset: (assetId: string) => dispatch({ type: 'REMOVE_ASSET', payload: assetId }),
         clearAssets: () => dispatch({ type: 'CLEAR_ASSETS' }),
-        addInvestment: (assetId: string, investment: Investment) =>
+        addInvestment: (assetId: string, investment: Investment | Investment[]) =>
             dispatch({ type: 'ADD_INVESTMENT', payload: { assetId, investment } }),
         removeInvestment: (assetId: string, investmentId: string) =>
             dispatch({ type: 'REMOVE_INVESTMENT', payload: { assetId, investmentId } }),
diff --git a/src/services/yahooFinanceService.ts b/src/services/yahooFinanceService.ts
index 515e3f7..a0b107f 100644
--- a/src/services/yahooFinanceService.ts
+++ b/src/services/yahooFinanceService.ts
@@ -1,4 +1,5 @@
 import type { Asset, YahooSearchResponse, YahooChartResult } from "../types";
+import toast from "react-hot-toast";
 
 // this is only needed when hosted staticly without a proxy server or smt
 // TODO change it to use the proxy server
@@ -49,6 +50,7 @@ export const searchAssets = async (query: string): Promise<Asset[]> => {
             }));
     } catch (error) {
         console.error('Error searching assets:', error);
+        toast.error('Failed to search assets. Please try again later.');
         return [];
     }
 };
@@ -81,6 +83,7 @@ export const getHistoricalData = async (symbol: string, startDate: string, endDa
         }
     } catch (error) {
         console.error('Error fetching historical data:', error);
+        toast.error(`Failed to fetch historical data for ${symbol}. Please try again later.`);
         return { historicalData: [], longName: '' };
     }
 };
diff --git a/src/utils/calculations/performance.ts b/src/utils/calculations/performance.ts
index f436c90..cd2e04b 100644
--- a/src/utils/calculations/performance.ts
+++ b/src/utils/calculations/performance.ts
@@ -52,7 +52,7 @@ export const calculateInvestmentPerformance = (assets: Asset[]): PortfolioPerfor
         const yearStart = new Date(year, 0, 1); // 1. Januar
         const yearEnd = year === endYear ? new Date(year, now.getMonth(), now.getDate()) : new Date(year, 11, 31); // Aktuelles Datum oder 31. Dez.
 
-        const investmentsPerformances:number[] = [];
+        const yearInvestments: { percent: number; weight: number }[] = [];
 
         for (const asset of assets) {
             // Get prices for the start and end of the year
@@ -67,7 +67,7 @@ export const calculateInvestmentPerformance = (assets: Asset[]): PortfolioPerfor
 
             if (startPrice === 0 || endPrice === 0) {
                 console.warn(`Skipping asset for year ${year} due to missing start or end price`);
-                continue; // Überspringe, wenn keine Daten vorhanden
+                continue;
             }
 
             // Get all investments made before or during this year
@@ -88,19 +88,23 @@ export const calculateInvestmentPerformance = (assets: Asset[]): PortfolioPerfor
                     (data) => isAfter(new Date(data.date), new Date(investment.date!))
                 ).find((v) => v.price !== 0)?.price || 0;
 
-
                 if (buyInPrice > 0) {
-                    const shares = investment.amount / buyInPrice; // Berechne Anzahl der Shares
+                    const shares = investment.amount / buyInPrice;
                     const endValue = shares * endPrice;
                     const startValue = shares * startPrice;
-                    investmentsPerformances.push((endValue - startValue) / startValue * 100);
+                    yearInvestments.push({
+                        percent: ((endValue - startValue) / startValue) * 100,
+                        weight: startValue
+                    });
                 }
             }
         }
 
-        // Calculate performance for the year
-        if (investmentsPerformances.length > 0) {
-            const percentage = investmentsPerformances.reduce((acc, curr) => acc + curr, 0) / investmentsPerformances.length;
+        // Calculate weighted average performance for the year
+        if (yearInvestments.length > 0) {
+            const totalWeight = yearInvestments.reduce((sum, inv) => sum + inv.weight, 0);
+            const percentage = yearInvestments.reduce((sum, inv) =>
+                sum + (inv.percent * (inv.weight / totalWeight)), 0);
 
             if (!isNaN(percentage)) {
                 annualPerformances.push({ year, percentage });
diff --git a/src/utils/calculations/portfolioValue.ts b/src/utils/calculations/portfolioValue.ts
index 29fbf08..b7f0f52 100644
--- a/src/utils/calculations/portfolioValue.ts
+++ b/src/utils/calculations/portfolioValue.ts
@@ -21,8 +21,12 @@ export const calculatePortfolioValue = (assets: Asset[], dateRange: DateRange) =
             percentageChange: 0,
             assets: {},
         };
-        // this should contain the percentage gain of all investments till now
-        const pPercents: number[] = [];
+
+        interface WeightedPercent {
+            percent: number;
+            weight: number;
+        }
+        const weightedPercents: WeightedPercent[] = [];
 
         for (const asset of assets) {
             // calculate the invested kapital
@@ -49,14 +53,20 @@ export const calculatePortfolioValue = (assets: Asset[], dateRange: DateRange) =
                 dayData.assets[asset.id] = currentValueOfAsset;
 
                 const percent = ((currentValueOfAsset - avgBuyIn) / avgBuyIn) * 100;
-                if (!Number.isNaN(percent)) pPercents.push(percent);
+                if (!Number.isNaN(percent) && investedValue && investedValue > 0) {
+                    weightedPercents.push({
+                        percent,
+                        weight: investedValue
+                    });
+                }
             }
         }
 
-
-        // Calculate average percentage change if percentages array is not empty
-        if (pPercents.length > 0) {
-            dayData.percentageChange = pPercents.reduce((a, b) => a + b, 0) / pPercents.length;
+        // Calculate weighted average percentage change
+        if (weightedPercents.length > 0) {
+            const totalWeight = weightedPercents.reduce((sum, wp) => sum + wp.weight, 0);
+            dayData.percentageChange = weightedPercents.reduce((sum, wp) =>
+                sum + (wp.percent * (wp.weight / totalWeight)), 0);
         } else {
             dayData.percentageChange = 0;
         }