diff --git a/frontend/src/components/ItemsSection.tsx b/frontend/src/components/ItemsSection.tsx index 12f38ee..889986d 100644 --- a/frontend/src/components/ItemsSection.tsx +++ b/frontend/src/components/ItemsSection.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { Plus, Trash2, Edit2, Check, X, Eye, EyeOff } from "lucide-react"; +import { Plus, Trash2, Edit2, Check, X } from "lucide-react"; import { ItemWithCategory, BudgetCategory, api } from "../api/client"; import { Card } from "./ui/Card"; import { Input } from "./ui/Input"; @@ -29,8 +29,6 @@ export function ItemsSection({ const [amount, setAmount] = useState(""); const [categoryId, setCategoryId] = useState(""); const [spentOn, setSpentOn] = useState(new Date().toISOString().split("T")[0]); - const [savingsDestination, setSavingsDestination] = useState("none"); - const [showDestination, setShowDestination] = useState(true); const handleAdd = async () => { if (!description || !amount || !categoryId) return; @@ -39,7 +37,7 @@ export function ItemsSection({ amount: parseFloat(amount), category_id: parseInt(categoryId), spent_on: spentOn, - savings_destination: savingsDestination, + savings_destination: "none", }); resetForm(); await onUpdate(); @@ -52,7 +50,7 @@ export function ItemsSection({ amount: parseFloat(amount), category_id: parseInt(categoryId), spent_on: spentOn, - savings_destination: savingsDestination, + savings_destination: "none", }); resetForm(); await onUpdate(); @@ -69,7 +67,6 @@ export function ItemsSection({ setAmount(item.amount.toString()); setCategoryId(item.category_id.toString()); setSpentOn(item.spent_on); - setSavingsDestination(item.savings_destination); }; const resetForm = () => { @@ -78,11 +75,11 @@ export function ItemsSection({ setAmount(""); setCategoryId(""); setSpentOn(new Date().toISOString().split("T")[0]); - setSavingsDestination("none"); setIsAdding(false); }; const categoryOptions = categories.map((c) => ({ value: c.id, label: c.label })); + const spendingItems = items.filter((item) => item.savings_destination === "none"); return ( @@ -147,23 +144,7 @@ export function ItemsSection({ onChange={(e) => setSpentOn(e.target.value)} /> -
-
- - setSavingsDestination(e.target.value)} - className="text-xs" - /> - - )}
+ )} +
+ + {isAdding && ( +
+
+ setDescription(e.target.value)} + /> + setAmount(e.target.value)} + /> + setSpentOn(e.target.value)} + /> +
+
+
+ + setSpentOn(e.target.value)} + className="text-xs" + /> + + + setDescription(e.target.value)} + className="text-xs" + /> + + + setAmount(e.target.value)} + className="text-xs text-right" + /> + + +
+ + +
+ + + ) : ( + <> + + {item.spent_on} + {item.spent_on.slice(5)} + + +
+ {item.description} +
+ + + {item.savings_destination === "savings" && ( + + Savings + + )} + {item.savings_destination === "retirement_savings" && ( + + Retirement + + )} + + + → {formatCurrency(item.amount)} + + {!isReadOnly && transfersEnabled && ( + +
+ + +
+ + )} + + )} + + ))} + + + + {transferItems.length === 0 && ( +
+ No transfers +
+ )} +
+ + ); +} diff --git a/frontend/src/context/UIPreferencesContext.tsx b/frontend/src/context/UIPreferencesContext.tsx new file mode 100644 index 0000000..0ae9459 --- /dev/null +++ b/frontend/src/context/UIPreferencesContext.tsx @@ -0,0 +1,46 @@ +import { createContext, useContext, useState, ReactNode } from "react"; + +interface UIPreferencesContextType { + transfersEnabled: boolean; + setTransfersEnabled: (enabled: boolean) => void; +} + +const UIPreferencesContext = createContext(undefined); + +const STORAGE_KEY = "uiPreferences"; + +export function UIPreferencesProvider({ children }: { children: ReactNode }) { + const [transfersEnabled, setTransfersEnabledState] = useState(() => { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored) { + try { + const prefs = JSON.parse(stored); + return prefs.transfersEnabled ?? true; + } catch { + return true; + } + } + return true; // Default: transfers enabled + }); + + const setTransfersEnabled = (enabled: boolean) => { + setTransfersEnabledState(enabled); + const stored = localStorage.getItem(STORAGE_KEY); + const prefs = stored ? JSON.parse(stored) : {}; + localStorage.setItem(STORAGE_KEY, JSON.stringify({ ...prefs, transfersEnabled: enabled })); + }; + + return ( + + {children} + + ); +} + +export function useUIPreferences() { + const context = useContext(UIPreferencesContext); + if (context === undefined) { + throw new Error("useUIPreferences must be used within UIPreferencesProvider"); + } + return context; +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index fda57bc..c260e6c 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -5,14 +5,17 @@ import App from "./App"; import { ThemeProvider } from "./context/ThemeContext"; import { AuthProvider } from "./context/AuthContext"; import { CurrencyProvider } from "./context/CurrencyContext"; +import { UIPreferencesProvider } from "./context/UIPreferencesContext"; createRoot(document.getElementById("root")!).render( - - - + + + + + diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index d7ffd2f..ba85918 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -5,6 +5,7 @@ import { Summary } from "../components/Summary"; import { SavingsCard } from "../components/SavingsCard"; import { RetirementSavingsCard } from "../components/RetirementSavingsCard"; import { CustomSavingsGoals } from "../components/CustomSavingsGoals"; +import { TransfersCard } from "../components/TransfersCard"; import { VarianceModal } from "../components/VarianceModal"; import { IncomeSection } from "../components/IncomeSection"; import { FixedExpenses } from "../components/FixedExpenses"; @@ -12,6 +13,7 @@ import { BudgetSection } from "../components/BudgetSection"; import { ItemsSection } from "../components/ItemsSection"; import { Stats } from "../components/Stats"; import { useMonth } from "../hooks/useMonth"; +import { useUIPreferences } from "../context/UIPreferencesContext"; import { Loader2 } from "lucide-react"; interface DashboardProps { @@ -20,6 +22,7 @@ interface DashboardProps { export function Dashboard({ onSettingsClick }: DashboardProps) { const [showVarianceModal, setShowVarianceModal] = useState(false); + const { transfersEnabled } = useUIPreferences(); const { summary, months, @@ -114,6 +117,16 @@ export function Dashboard({ onSettingsClick }: DashboardProps) { isReadOnly={isReadOnly} onUpdate={refresh} /> + + {(transfersEnabled || summary.items.some(item => item.savings_destination === "savings" || item.savings_destination === "retirement_savings")) && ( + + )}
diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index 3f2c58b..212a8a0 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -6,8 +6,9 @@ import { Select } from "../components/ui/Select"; import { Modal } from "../components/ui/Modal"; import { useAuth } from "../context/AuthContext"; import { useCurrency, SUPPORTED_CURRENCIES } from "../context/CurrencyContext"; +import { useUIPreferences } from "../context/UIPreferencesContext"; import { api } from "../api/client"; -import { ArrowLeft } from "lucide-react"; +import { ArrowLeft, Info } from "lucide-react"; interface SettingsProps { onBack: () => void; @@ -16,6 +17,7 @@ interface SettingsProps { export function Settings({ onBack }: SettingsProps) { const { user, logout, updateUsername } = useAuth(); const { currency, setCurrency, formatCurrency } = useCurrency(); + const { transfersEnabled, setTransfersEnabled } = useUIPreferences(); const [newUsername, setNewUsername] = useState(user?.username || ""); const [currentPassword, setCurrentPassword] = useState(""); const [newPassword, setNewPassword] = useState(""); @@ -32,6 +34,8 @@ export function Settings({ onBack }: SettingsProps) { const [passwordSuccess, setPasswordSuccess] = useState(false); const [currencySuccess, setCurrencySuccess] = useState(false); const [selectedCurrency, setSelectedCurrency] = useState(currency.code); + const [showTransfersModal, setShowTransfersModal] = useState(false); + const handleChangeUsername = async (e: React.FormEvent) => { e.preventDefault(); @@ -153,6 +157,47 @@ export function Settings({ onBack }: SettingsProps) {
+
+

+ Transferred Items +

+
+
+
+
+ + +
+

+ Allow adding, editing, and deleting transferred items +

+
+ +
+
+
+

Change Username @@ -286,6 +331,58 @@ export function Settings({ onBack }: SettingsProps) {

+ + setShowTransfersModal(false)}> +
+

+ How to Use Transferred Items +

+ +
+
+

What are transfers?

+

Track portions of your budgeted spending that you plan to transfer to savings or retirement accounts instead of spending.

+
+ +
+

Enable/Disable behavior:

+
    +
  • When enabled: You can add, edit, and delete transfers.
  • +
  • When disabled: View only. Card hides once all transfers are deleted.
  • +
+
+ +
+

When to use transfers:

+
    +
  • You have extra budget left and want to save it
  • +
  • You want to contribute to retirement beyond regular deductions
  • +
  • You want to track discretionary savings separately
  • +
+
+ +
+

How to add a transfer:

+
    +
  1. Open the "Transferred Items" card on the dashboard
  2. +
  3. Click the + button to create a transfer
  4. +
  5. Fill in the description, amount, and date
  6. +
  7. Choose the destination: Savings or Retirement
  8. +
  9. Click confirm
  10. +
+
+
+ +
+ +
+
+
); }