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:
1 parent
77f571fbcf
commit
16ab65d4ec
2 files changed
+44
-11
No files matched your search
+36
-6
@@ -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 & savings − ${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">
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user