Add Available to Budget envelope view and fix transaction/account delete bugs

- Envelopes view: "Available to Budget" card shows checking+savings balance
  minus total envelope allocations — turns red if over-allocated
- Income transactions in ManualEntry: envelope picker labeled clearly,
  green highlight, helper text explaining effect on Available to Budget,
  default option "Leave in Available to Budget"
- Fix account delete: now removes the account's transactions and
  recalculates envelope spending
- Fix transaction edit/delete: use functional state updater to avoid
  stale closure bug on accounts/envelopes recalculation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 4.6 committed 2026-07-14 10:55:41 -04:00
1 parent 77f571fbcf
commit 16ab65d4ec
2 files changed
+44 -11

No files matched your search

+36 -6
View File
@@ -422,8 +422,8 @@ export default function BudgetDashboard() {
const handleTransactionEdit = (updated: Transaction) => {
setTransactions(prev => {
const next = prev.map(t => t.id === updated.id ? updated : t);
setEnvelopes(calculateEnvelopeSpending(envelopes, next));
setAccounts(calculateAccountBalances(accounts, next));
setEnvelopes(e => calculateEnvelopeSpending(e, next));
setAccounts(a => calculateAccountBalances(a, next));
return next;
});
};
@@ -431,8 +431,8 @@ export default function BudgetDashboard() {
const handleTransactionDelete = (id: string) => {
setTransactions(prev => {
const next = prev.filter(t => t.id !== id);
setEnvelopes(calculateEnvelopeSpending(envelopes, next));
setAccounts(calculateAccountBalances(accounts, next));
setEnvelopes(e => calculateEnvelopeSpending(e, next));
setAccounts(a => calculateAccountBalances(a, next));
return next;
});
};
@@ -928,9 +928,32 @@ export default function BudgetDashboard() {
const incomeAlloc = transactions.filter(t => t.envelopeId === e.id && t.amount > 0).reduce((s, t) => s + t.amount, 0);
return e.allocated + incomeAlloc > 0 && e.spent > e.allocated + incomeAlloc;
});
// Available to budget = liquid assets (checking + savings) minus total envelope allocations
const liquidBalance = accounts
.filter(a => a.type === 'checking' || a.type === 'savings')
.reduce((s, a) => s + a.balance, 0);
const totalAllocated = envelopes.reduce((s, e) => s + e.allocated, 0);
const availableToBudget = liquidBalance - totalAllocated;
return (
<div className="px-4 py-6 sm:px-0">
<h2 className="text-2xl font-bold text-gray-900 mb-6">Envelopes</h2>
<h2 className="text-2xl font-bold text-gray-900 mb-4">Envelopes</h2>
{/* Available to Budget */}
<div className={`mb-6 rounded-xl border-2 p-5 flex items-center justify-between ${availableToBudget >= 0 ? 'bg-green-50 border-green-300' : 'bg-red-50 border-red-300'}`}>
<div>
<p className="text-sm font-medium text-gray-600">Available to Budget</p>
<p className={`text-3xl font-bold mt-0.5 ${availableToBudget >= 0 ? 'text-green-700' : 'text-red-700'}`}>
${Math.abs(availableToBudget).toFixed(2)}{availableToBudget < 0 ? ' over' : ''}
</p>
<p className="text-xs text-gray-500 mt-1">
${liquidBalance.toFixed(2)} in checking &amp; savings &minus; ${totalAllocated.toFixed(2)} allocated to envelopes
</p>
</div>
<div className="text-4xl">{availableToBudget >= 0 ? '💰' : '⚠️'}</div>
</div>
{overspentEnvs.length > 0 && (
<div className="mb-6 bg-red-50 border border-red-300 rounded-lg p-4 flex items-start gap-3">
<span className="text-red-500 text-xl flex-shrink-0">⚠️</span>
@@ -1117,7 +1140,14 @@ export default function BudgetDashboard() {
accounts={accounts}
onAccountAdd={handleAccountAdd}
onAccountUpdate={handleAccountUpdate}
onAccountDelete={id => setAccounts(prev => prev.filter(a => a.id !== id))}
onAccountDelete={id => {
setTransactions(prev => {
const next = prev.filter(t => t.accountId !== id);
setEnvelopes(e => calculateEnvelopeSpending(e, next));
return next;
});
setAccounts(prev => prev.filter(a => a.id !== id));
}}
/>
{sharedAccounts.length > 0 && (
<div className="mt-8">
+8 -5
View File
@@ -177,22 +177,25 @@ export default function ManualEntry({ onTransactionAdded, envelopes, accounts, t
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Assign to Envelope (Optional)
{isExpense ? 'Assign to Envelope (Optional)' : 'Assign Income to Envelope (Optional)'}
</label>
{!isExpense && (
<p className="text-xs text-green-700 bg-green-50 border border-green-200 rounded px-2 py-1 mb-2">
Assigning income to an envelope increases its available balance and reduces "Available to Budget".
</p>
)}
<select
value={selectedEnvelope}
onChange={(e) => setSelectedEnvelope(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white text-gray-900"
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 bg-white text-gray-900 ${!isExpense ? 'border-green-400 focus:ring-green-500' : 'border-gray-300 focus:ring-blue-500'}`}
>
<option value="">Select an envelope...</option>
<option value="">— Leave in Available to Budget —</option>
{envelopes.map((envelope) => {
// Calculate remaining balance
const incomeAllocated = transactions
.filter(t => t.envelopeId === envelope.id && t.amount > 0)
.reduce((sum, t) => sum + t.amount, 0);
const totalAllocated = (envelope.allocated || 0) + incomeAllocated;
const remaining = totalAllocated - (envelope.spent || 0);
return (
<option key={envelope.id} value={envelope.id}>
{envelope.name} — ${remaining.toFixed(2)} remaining