Files
budget/src/Budget.Client/src/pages/SummaryPage.tsx
T
Spencer Twaddle ae28abdb3e Phase 12: Add TanStack React Query
- Install @tanstack/react-query
- Create queryClient with MutationCache for global toast on success/error
- QueryToastBridge component bridges module-level handlers to ToastProvider
- Wrap App in QueryClientProvider
- Create domain hook files: budgets, incomes, outgos, shares, summary
- Update all 5 pages to use hooks; remove inline useEffect/useState fetch logic
- DnD reorder pages use local displayItems state synced from query data
2026-05-02 16:48:00 -05:00

103 lines
3.5 KiB
TypeScript

import { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { MoneyDisplay } from '../components/MoneyDisplay';
import { BudgetNav } from '../components/BudgetNav';
import { useSummary, useUpdateTaxRate } from '../api/summary';
const fmt = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });
export function SummaryPage() {
const { id: budgetId } = useParams<{ id: string }>();
const { data: summary } = useSummary(budgetId!);
const [taxRateInput, setTaxRateInput] = useState('');
const updateTaxRate = useUpdateTaxRate(budgetId!);
useEffect(() => {
if (summary) {
setTaxRateInput(String(Math.round(summary.preTaxIncome.effectiveTaxRate * 100)));
}
}, [summary]);
const saveTaxRate = () => {
updateTaxRate.mutate(parseFloat(taxRateInput) / 100);
};
if (!summary) return <div>Loading...</div>;
return (
<div>
<BudgetNav />
<h1>Summary</h1>
<div style={{ display: 'flex', gap: '2rem', marginBottom: '1.5rem' }}>
<div>
<div style={{ fontSize: '0.85rem', color: '#666' }}>Monthly Income</div>
<div style={{ fontSize: '1.5rem', fontWeight: 'bold' }}>
<MoneyDisplay value={summary.monthlyIncome} />
</div>
</div>
<div>
<div style={{ fontSize: '0.85rem', color: '#666' }}>Annual Income</div>
<div style={{ fontSize: '1.5rem', fontWeight: 'bold' }}>
<MoneyDisplay value={summary.annualIncome} />
</div>
</div>
</div>
<table>
<thead>
<tr>
<th>Type</th>
<th>Monthly Total</th>
<th>Annual Total</th>
<th>% of Income</th>
<th>Max Amount (target%)</th>
<th>Remaining</th>
</tr>
</thead>
<tbody>
{summary.breakdown.map(row => (
<tr key={row.type}>
<td>
{row.type}
{row.targetPercent != null && (
<span title={`Target: ${row.targetPercent}%`} style={{ marginLeft: '4px', cursor: 'help', color: '#888' }}></span>
)}
</td>
<td><MoneyDisplay value={row.total} /></td>
<td><MoneyDisplay value={row.annually} /></td>
<td>{row.percent.toFixed(1)}%</td>
<td>{row.maxAmount != null ? fmt.format(row.maxAmount) : '—'}</td>
<td>{row.remaining != null ? fmt.format(row.remaining) : '—'}</td>
</tr>
))}
</tbody>
</table>
<div style={{ marginTop: '2rem' }}>
<h2>Pre-Tax Income</h2>
<div>
<label>
Effective Tax Rate (%){' '}
<input
type="number"
min="0"
max="100"
value={taxRateInput}
onChange={e => setTaxRateInput(e.target.value)}
style={{ width: '60px' }}
/>
<button onClick={saveTaxRate} disabled={updateTaxRate.isPending} style={{ marginLeft: '8px' }}>
{updateTaxRate.isPending ? 'Saving…' : 'Save'}
</button>
</label>
</div>
<div style={{ marginTop: '0.75rem' }}>
<div>Minimum Annual Gross: <strong><MoneyDisplay value={summary.preTaxIncome.minimumAnnualGross} /></strong></div>
<div>Minimum Monthly Gross: <strong><MoneyDisplay value={summary.preTaxIncome.minimumMonthlyGross} /></strong></div>
</div>
</div>
</div>
);
}