diff --git a/app/src/api/savings.ts b/app/src/api/savings.ts new file mode 100644 index 000000000..8085e9d65 --- /dev/null +++ b/app/src/api/savings.ts @@ -0,0 +1,47 @@ +import { api } from './client'; + +export type SavingsGoal = { + id: number; + title: string; + target: number; + current: number; + currency: string; + deadline: string | null; + monthlyTarget: number; + status: 'on-track' | 'ahead' | 'behind' | 'completed'; + created_at?: string; +}; + +export type SavingsGoalCreate = { + title: string; + target_amount: number; + current_amount?: number; + currency?: string; + deadline?: string | null; +}; + +export type SavingsGoalUpdate = Partial; + +export async function listSavingsGoals(): Promise { + return api('/savings'); +} + +export async function createSavingsGoal(payload: SavingsGoalCreate): Promise { + return api('/savings', { method: 'POST', body: payload }); +} + +export async function getSavingsGoal(id: number): Promise { + return api(`/savings/${id}`); +} + +export async function updateSavingsGoal(id: number, payload: SavingsGoalUpdate): Promise { + return api(`/savings/${id}`, { method: 'PATCH', body: payload }); +} + +export async function contributeSavingsGoal(id: number, amount: number): Promise { + return api(`/savings/${id}/contribute`, { method: 'POST', body: { amount } }); +} + +export async function deleteSavingsGoal(id: number): Promise<{ message: string }> { + return api(`/savings/${id}`, { method: 'DELETE' }); +} diff --git a/app/src/pages/Budgets.tsx b/app/src/pages/Budgets.tsx index ec687baa5..b2cdb084b 100644 --- a/app/src/pages/Budgets.tsx +++ b/app/src/pages/Budgets.tsx @@ -1,8 +1,9 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { FinancialCard, FinancialCardContent, FinancialCardDescription, FinancialCardFooter, FinancialCardHeader, FinancialCardTitle } from '@/components/ui/financial-card'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; -import { Calendar, DollarSign, Plus, PieChart, TrendingDown, TrendingUp, Target, AlertCircle, Settings } from 'lucide-react'; +import { Calendar, DollarSign, Plus, PieChart, TrendingDown, TrendingUp, Target, AlertCircle, Settings, Loader2 } from 'lucide-react'; +import { listSavingsGoals, type SavingsGoal } from '@/api/savings'; const budgetCategories = [ { @@ -67,43 +68,41 @@ const budgetCategories = [ } ]; -const budgetGoals = [ - { - id: 1, - title: 'Emergency Fund', - target: 10000, - current: 7250, - deadline: 'Dec 2025', - monthlyTarget: 458, - status: 'on-track' - }, - { - id: 2, - title: 'Vacation Fund', - target: 3000, - current: 1850, - deadline: 'Jun 2025', - monthlyTarget: 383, - status: 'behind' - }, - { - id: 3, - title: 'New Car', - target: 25000, - current: 15600, - deadline: 'Mar 2026', - monthlyTarget: 625, - status: 'ahead' - } -]; - export function Budgets() { const [selectedPeriod] = useState('monthly'); - + const [savingsGoals, setSavingsGoals] = useState([]); + const [goalsLoading, setGoalsLoading] = useState(true); + + useEffect(() => { + listSavingsGoals() + .then(setSavingsGoals) + .catch(() => setSavingsGoals([])) + .finally(() => setGoalsLoading(false)); + }, []); + const totalAllocated = budgetCategories.reduce((sum, cat) => sum + cat.allocated, 0); const totalSpent = budgetCategories.reduce((sum, cat) => sum + cat.spent, 0); const totalRemaining = totalAllocated - totalSpent; + const statusLabel = (status: SavingsGoal['status']) => { + if (status === 'on-track') return 'On Track'; + if (status === 'ahead') return 'Ahead'; + if (status === 'completed') return 'Completed'; + return 'Behind'; + }; + + const statusVariant = (status: SavingsGoal['status']): 'default' | 'secondary' | 'destructive' => { + if (status === 'on-track') return 'default'; + if (status === 'ahead' || status === 'completed') return 'secondary'; + return 'destructive'; + }; + + const formatDeadline = (deadline: string | null) => { + if (!deadline) return '—'; + const d = new Date(deadline); + return d.toLocaleDateString('en-US', { month: 'short', year: 'numeric' }); + }; + return (
@@ -211,7 +210,7 @@ export function Budgets() { {budgetCategories.map((category) => { const percentage = (category.spent / category.allocated) * 100; const isOverBudget = category.remaining < 0; - + return (
@@ -278,48 +277,58 @@ export function Budgets() { -
- {budgetGoals.map((goal) => { - const percentage = (goal.current / goal.target) * 100; - - return ( -
-
-
- {goal.title} -
- - {goal.status === 'on-track' ? 'On Track' : - goal.status === 'ahead' ? 'Ahead' : 'Behind'} - -
-
-
- - ${goal.current.toLocaleString()} / ${goal.target.toLocaleString()} - - - {percentage.toFixed(0)}% - -
-
-
+ {goalsLoading ? ( +
+ +
+ ) : savingsGoals.length === 0 ? ( +
+ No savings goals yet. Add one to get started! +
+ ) : ( +
+ {savingsGoals.map((goal) => { + const percentage = Math.min((goal.current / goal.target) * 100, 100); + return ( +
+
+
+ {goal.title} +
+ + {statusLabel(goal.status)} +
-
- Target: {goal.deadline} - ${goal.monthlyTarget}/mo +
+
+ + {goal.currency} {goal.current.toLocaleString()} / {goal.target.toLocaleString()} + + + {percentage.toFixed(0)}% + +
+
+
+
+
+ Target: {formatDeadline(goal.deadline)} + {goal.monthlyTarget > 0 && ( + {goal.currency} {goal.monthlyTarget.toLocaleString()}/mo + )} +
-
- ); - })} -
+ ); + })} +
+ )}