diff --git a/app/routes/transactions.py b/app/routes/transactions.py index 6d28c34..43b2cf6 100644 --- a/app/routes/transactions.py +++ b/app/routes/transactions.py @@ -29,6 +29,15 @@ def _category_choices(cat_type): return [('', '— None —')] + [(str(c.id), c.name) for c in cats] +def _category_choices_json(cat_type): + cats = Category.query.filter( + Category.category_type.in_([cat_type, 'both']), + Category.is_active == True, + Category.parent_id == None + ).order_by(Category.name).all() + return [{'id': str(c.id), 'name': c.name} for c in cats] + + class TransactionForm(FlaskForm): transaction_type = HiddenField(default='expense') account_id = SelectField('Account', validators=[DataRequired()]) @@ -122,13 +131,18 @@ def new(): txn_type = 'expense' form = TransactionForm() - form.transaction_type.data = txn_type + # On POST, honour the type the user selected in the toggle + if request.method == 'POST': + submitted = request.form.get('transaction_type', txn_type) + if submitted in ('income', 'expense'): + txn_type = submitted form.account_id.choices = _account_choices() form.category_id.choices = _category_choices(txn_type) if form.validate_on_submit(): + saved_type = form.transaction_type.data txn = Transaction( - transaction_type=form.transaction_type.data, + transaction_type=saved_type, account_id=int(form.account_id.data), category_id=int(form.category_id.data) if form.category_id.data else None, amount=form.amount.data, @@ -139,15 +153,17 @@ def new(): db.session.add(txn) db.session.commit() calc_balance(txn.account_id) - flash(f'{"Income" if txn_type == "income" else "Expense"} added.', 'success') - if txn_type == 'expense': + flash(f'{"Income" if saved_type == "income" else "Expense"} added.', 'success') + if saved_type == 'expense': from app.services.alert_service import check_and_flash_budget_alerts check_and_flash_budget_alerts(flash) - return redirect(url_for('transactions.index', tab=txn_type)) + return redirect(url_for('transactions.index', tab=saved_type)) return render_template('transactions/form.html', form=form, txn_type=txn_type, + income_cats=_category_choices_json('income'), + expense_cats=_category_choices_json('expense'), title=f'New {"Income" if txn_type == "income" else "Expense"}') @@ -155,18 +171,26 @@ def new(): @login_required def edit(id): txn = db.get_or_404(Transaction, id) - form = TransactionForm(obj=txn) - form.transaction_type.data = txn.transaction_type - form.account_id.choices = _account_choices() - form.category_id.choices = _category_choices(txn.transaction_type) - # Pre-populate foreign keys as strings for SelectField + # Determine active type: from POST toggle or existing record + if request.method == 'POST': + submitted_type = request.form.get('transaction_type', txn.transaction_type) + active_type = submitted_type if submitted_type in ('income', 'expense') else txn.transaction_type + else: + active_type = txn.transaction_type + + form = TransactionForm(obj=txn) + form.account_id.choices = _account_choices() + form.category_id.choices = _category_choices(active_type) + if request.method == 'GET': + form.transaction_type.data = txn.transaction_type form.account_id.data = str(txn.account_id) form.category_id.data = str(txn.category_id) if txn.category_id else '' if form.validate_on_submit(): old_account_id = txn.account_id + txn.transaction_type = active_type txn.account_id = int(form.account_id.data) txn.category_id = int(form.category_id.data) if form.category_id.data else None txn.amount = form.amount.data @@ -185,7 +209,9 @@ def edit(id): return render_template('transactions/form.html', form=form, txn=txn, - txn_type=txn.transaction_type, + txn_type=active_type, + income_cats=_category_choices_json('income'), + expense_cats=_category_choices_json('expense'), title='Edit Transaction') diff --git a/app/templates/transactions/form.html b/app/templates/transactions/form.html index f33b8fb..ad75002 100644 --- a/app/templates/transactions/form.html +++ b/app/templates/transactions/form.html @@ -57,11 +57,28 @@ -
+
{{ form.hidden_tag() }} {{ form.transaction_type() }} + +
+ +
+ + +
+
+
{{ form.description.label(class="form-label fw-medium", style="font-size:13px;") }} {{ form.description(class="form-control" + (" is-invalid" if form.description.errors else ""), @@ -103,10 +120,11 @@
- - Cancel + Cancel
@@ -370,5 +388,42 @@ function autoOcrOnUpload(input) { if (panel) { panel.style.display = 'block'; document.getElementById('ocrChevron').className = 'bi bi-chevron-up'; } runOcr(file); } + +// ── Transaction type toggle ─────────────────────────────────────────────────── +const incomeCats = {{ income_cats | tojson }}; +const expenseCats = {{ expense_cats | tojson }}; + +function setTxnType(type) { + // Update hidden field + document.querySelector('[name=transaction_type]').value = type; + + // Toggle button styles + const incBtn = document.getElementById('toggleIncome'); + const expBtn = document.getElementById('toggleExpense'); + incBtn.className = 'btn btn-sm flex-fill ' + (type === 'income' ? 'btn-success' : 'btn-outline-secondary'); + expBtn.className = 'btn btn-sm flex-fill ' + (type === 'expense' ? 'btn-danger' : 'btn-outline-secondary'); + + // Card border + submit button + document.getElementById('txnCard').style.borderTopColor = type === 'income' ? 'var(--income)' : 'var(--expense)'; + const submitBtn = document.getElementById('submitBtn'); + submitBtn.className = 'btn ' + (type === 'income' ? 'btn-success' : 'btn-danger'); + submitBtn.textContent = 'Save ' + (type === 'income' ? 'Income' : 'Expense'); + + // Update cancel link tab param + const cancelLink = document.getElementById('cancelLink'); + if (cancelLink) { + cancelLink.href = cancelLink.href.replace(/([\?&]tab=)[^&]*/, '$1' + type); + } + + // Swap category options, preserving current selection if still valid + const sel = document.getElementById('field_category'); + const prevVal = sel.value; + const cats = type === 'income' ? incomeCats : expenseCats; + sel.innerHTML = '' + + cats.map(c => ``).join(''); + if (prevVal && cats.some(c => c.id === prevVal)) { + sel.value = prevVal; + } +} {% endblock %}