Add exclude from budget tracking option per account

Accounts (e.g. mortgage) can be toggled to exclude from budget tracking:
- Their transactions don't reduce envelope spending
- Their balance is excluded from Available to Budget
- Account card shows "excluded from budget" badge when enabled

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 4.6 committed 2026-07-14 11:33:36 -04:00
1 parent f38fceefba
commit a4cde09d48
2 files changed
+27 -6

No files matched your search

+9 -6
View File
@@ -30,6 +30,7 @@ interface Account {
accountNumber?: string; accountNumber?: string;
color: string; color: string;
isActive: boolean; isActive: boolean;
excludeFromBudget?: boolean;
defaultPaycheckAmount?: number; defaultPaycheckAmount?: number;
} }
@@ -144,13 +145,15 @@ export default function BudgetDashboard() {
// ── Derived helpers ────────────────────────────────────────────────────────── // ── Derived helpers ──────────────────────────────────────────────────────────
const calculateEnvelopeSpending = useCallback((envs: Envelope[], txns: Transaction[]): Envelope[] => const calculateEnvelopeSpending = useCallback((envs: Envelope[], txns: Transaction[], accs?: Account[]): Envelope[] => {
envs.map(envelope => { const excludedIds = new Set((accs ?? accounts).filter(a => a.excludeFromBudget).map(a => a.id));
return envs.map(envelope => {
const spent = txns const spent = txns
.filter(t => t.envelopeId === envelope.id && t.amount < 0) .filter(t => t.envelopeId === envelope.id && t.amount < 0 && !excludedIds.has(t.accountId))
.reduce((sum, t) => sum + Math.abs(t.amount), 0); .reduce((sum, t) => sum + Math.abs(t.amount), 0);
return { ...envelope, spent }; return { ...envelope, spent };
}), []); });
}, [accounts]);
const calculateAccountBalances = useCallback((accs: Account[], txns: Transaction[]): Account[] => { const calculateAccountBalances = useCallback((accs: Account[], txns: Transaction[]): Account[] => {
const map = new Map<string, number>(); const map = new Map<string, number>();
@@ -929,10 +932,10 @@ export default function BudgetDashboard() {
return e.allocated + incomeAlloc > 0 && e.spent > e.allocated + incomeAlloc; return e.allocated + incomeAlloc > 0 && e.spent > e.allocated + incomeAlloc;
}); });
// Available to budget = all asset accounts minus total envelope allocations // Available to budget = all asset accounts (excluding budget-excluded ones) minus total envelope allocations
const LIABILITY_TYPES = ['credit_card', 'mortgage', 'loan']; const LIABILITY_TYPES = ['credit_card', 'mortgage', 'loan'];
const liquidBalance = accounts const liquidBalance = accounts
.filter(a => !LIABILITY_TYPES.includes(a.type)) .filter(a => !LIABILITY_TYPES.includes(a.type) && !a.excludeFromBudget)
.reduce((s, a) => s + a.balance, 0); .reduce((s, a) => s + a.balance, 0);
const totalAllocated = envelopes.reduce((s, e) => s + e.allocated, 0); const totalAllocated = envelopes.reduce((s, e) => s + e.allocated, 0);
const availableToBudget = liquidBalance - totalAllocated; const availableToBudget = liquidBalance - totalAllocated;
+18
View File
@@ -12,6 +12,7 @@ interface Account {
accountNumber?: string; accountNumber?: string;
color: string; color: string;
isActive: boolean; isActive: boolean;
excludeFromBudget?: boolean;
} }
interface AccountManagementProps { interface AccountManagementProps {
@@ -187,6 +188,9 @@ export default function AccountManagement({ accounts, onAccountAdd, onAccountUpd
<p className={`text-sm ${account.isActive ? 'text-green-600' : 'text-gray-500'}`}> <p className={`text-sm ${account.isActive ? 'text-green-600' : 'text-gray-500'}`}>
{account.isActive ? 'Active' : 'Inactive'} {account.isActive ? 'Active' : 'Inactive'}
</p> </p>
{account.excludeFromBudget && (
<span className="text-xs bg-gray-100 text-gray-500 px-1.5 py-0.5 rounded">excluded from budget</span>
)}
</div> </div>
<div className="flex space-x-2"> <div className="flex space-x-2">
@@ -316,6 +320,20 @@ export default function AccountManagement({ accounts, onAccountAdd, onAccountUpd
<label htmlFor="isActive" className="text-sm text-gray-700">Active Account</label> <label htmlFor="isActive" className="text-sm text-gray-700">Active Account</label>
</div> </div>
<div className="flex items-start gap-2">
<input
type="checkbox"
id="excludeFromBudget"
checked={formData.excludeFromBudget ?? false}
onChange={(e) => setFormData(prev => ({ ...prev, excludeFromBudget: e.target.checked }))}
className="mr-2 mt-0.5"
/>
<label htmlFor="excludeFromBudget" className="text-sm text-gray-700">
Exclude from budget tracking
<span className="block text-xs text-gray-400">Transactions on this account won't count against envelopes or expenses (e.g. mortgage payments)</span>
</label>
</div>
<div className="flex justify-end space-x-3 pt-4"> <div className="flex justify-end space-x-3 pt-4">
<button <button
type="button" type="button"