From 64203606a6fdd3b524bb51e27a8fb8482b522dae Mon Sep 17 00:00:00 2001 From: Spencer Twaddle <7374698+stwaddle@users.noreply.github.com> Date: Sat, 25 Apr 2026 07:59:40 -0500 Subject: [PATCH] Phase 7: Budget list, settings, and sharing UI - Implement BudgetsPage: list all budgets, create new budget, navigate to income view - Implement SettingsPage: rename budget, edit tax rate, manage shares table - Shares table shows permission dropdowns, pending status badge, revoke button - Add share form: email input, permission selector, add button - BudgetNav component used across all budget pages Co-Authored-By: Claude Sonnet 4.6 --- src/Budget.Client/src/pages/BudgetsPage.tsx | 46 +++++- src/Budget.Client/src/pages/SettingsPage.tsx | 141 ++++++++++++++++++- 2 files changed, 185 insertions(+), 2 deletions(-) diff --git a/src/Budget.Client/src/pages/BudgetsPage.tsx b/src/Budget.Client/src/pages/BudgetsPage.tsx index 13dfb11..2e89cf9 100644 --- a/src/Budget.Client/src/pages/BudgetsPage.tsx +++ b/src/Budget.Client/src/pages/BudgetsPage.tsx @@ -1,3 +1,47 @@ +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import type { BudgetDto } from '../types'; +import { api } from '../api/client'; + export function BudgetsPage() { - return
Budgets — coming soon
; + const [budgets, setBudgets] = useState([]); + const [newName, setNewName] = useState(''); + const navigate = useNavigate(); + + useEffect(() => { + api.get('/api/budgets').then(setBudgets); + }, []); + + const handleCreate = async () => { + if (!newName.trim()) return; + const created = await api.post('/api/budgets', { name: newName.trim() }); + setBudgets(prev => [...prev, created]); + setNewName(''); + navigate(`/budgets/${created.id}/income`); + }; + + return ( +
+

My Budgets

+ {budgets.length === 0 &&

No budgets yet. Create one below.

} +
    + {budgets.map(b => ( +
  • + +
  • + ))} +
+
+ setNewName(e.target.value)} + onKeyDown={e => e.key === 'Enter' && handleCreate()} + /> + +
+
+ ); } diff --git a/src/Budget.Client/src/pages/SettingsPage.tsx b/src/Budget.Client/src/pages/SettingsPage.tsx index 434454b..41da328 100644 --- a/src/Budget.Client/src/pages/SettingsPage.tsx +++ b/src/Budget.Client/src/pages/SettingsPage.tsx @@ -1,3 +1,142 @@ +import { useEffect, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import type { BudgetDto, ShareDto, SharePermission } from '../types'; +import { api } from '../api/client'; +import { BudgetNav } from '../components/BudgetNav'; + export function SettingsPage() { - return
Settings — coming soon
; + const { id: budgetId } = useParams<{ id: string }>(); + const [budget, setBudget] = useState(null); + const [shares, setShares] = useState([]); + const [nameInput, setNameInput] = useState(''); + const [taxInput, setTaxInput] = useState(''); + const [shareEmail, setShareEmail] = useState(''); + const [sharePermission, setSharePermission] = useState('View'); + const [saving, setSaving] = useState(false); + + useEffect(() => { + if (!budgetId) return; + api.get(`/api/budgets/${budgetId}`).then(b => { + setBudget(b); + setNameInput(b.name); + setTaxInput(String(Math.round(b.effectiveTaxRate * 100))); + }); + api.get(`/api/budgets/${budgetId}/shares`).then(setShares); + }, [budgetId]); + + const saveName = async () => { + if (!budgetId || !nameInput.trim()) return; + setSaving(true); + const updated = await api.put(`/api/budgets/${budgetId}`, { name: nameInput.trim() }); + setBudget(updated); + setSaving(false); + }; + + const saveTaxRate = async () => { + if (!budgetId) return; + setSaving(true); + const rate = parseFloat(taxInput) / 100; + await api.put(`/api/budgets/${budgetId}/summary/tax-rate`, { effectiveTaxRate: rate }); + const updated = await api.get(`/api/budgets/${budgetId}`); + setBudget(updated); + setSaving(false); + }; + + const addShare = async () => { + if (!budgetId || !shareEmail.trim()) return; + const share = await api.post(`/api/budgets/${budgetId}/shares`, { + email: shareEmail.trim(), + permission: sharePermission, + }); + setShares(prev => [...prev, share]); + setShareEmail(''); + }; + + const updatePermission = async (shareId: string, permission: SharePermission) => { + if (!budgetId) return; + const updated = await api.put(`/api/budgets/${budgetId}/shares/${shareId}`, { permission }); + setShares(prev => prev.map(s => s.id === shareId ? updated : s)); + }; + + const revokeShare = async (shareId: string) => { + if (!budgetId) return; + await api.delete(`/api/budgets/${budgetId}/shares/${shareId}`); + setShares(prev => prev.filter(s => s.id !== shareId)); + }; + + if (!budget) return
Loading...
; + + return ( +
+ +

Settings

+ +
+

Rename Budget

+ setNameInput(e.target.value)} /> + +
+ +
+

Effective Tax Rate

+ +
+ +
+

Sharing

+ + + + + + + + + + + {shares.map(s => ( + + + + + + + ))} + +
EmailPermissionStatus
{s.sharedWithEmail} + + {s.isPending ? Pending : 'Active'}
+ +
+ setShareEmail(e.target.value)} + /> + + +
+
+
+ ); }