ae28abdb3e
- 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
103 lines
3.5 KiB
TypeScript
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>
|
|
);
|
|
}
|