06/02 Modified transaction, transaction type switchable

This commit is contained in:
2026-06-02 10:30:47 -04:00
parent 04649530f7
commit ced33495ac
2 changed files with 95 additions and 14 deletions
+37 -11
View File
@@ -29,6 +29,15 @@ def _category_choices(cat_type):
return [('', '— None —')] + [(str(c.id), c.name) for c in cats] 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): class TransactionForm(FlaskForm):
transaction_type = HiddenField(default='expense') transaction_type = HiddenField(default='expense')
account_id = SelectField('Account', validators=[DataRequired()]) account_id = SelectField('Account', validators=[DataRequired()])
@@ -122,13 +131,18 @@ def new():
txn_type = 'expense' txn_type = 'expense'
form = TransactionForm() 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.account_id.choices = _account_choices()
form.category_id.choices = _category_choices(txn_type) form.category_id.choices = _category_choices(txn_type)
if form.validate_on_submit(): if form.validate_on_submit():
saved_type = form.transaction_type.data
txn = Transaction( txn = Transaction(
transaction_type=form.transaction_type.data, transaction_type=saved_type,
account_id=int(form.account_id.data), account_id=int(form.account_id.data),
category_id=int(form.category_id.data) if form.category_id.data else None, category_id=int(form.category_id.data) if form.category_id.data else None,
amount=form.amount.data, amount=form.amount.data,
@@ -139,15 +153,17 @@ def new():
db.session.add(txn) db.session.add(txn)
db.session.commit() db.session.commit()
calc_balance(txn.account_id) calc_balance(txn.account_id)
flash(f'{"Income" if txn_type == "income" else "Expense"} added.', 'success') flash(f'{"Income" if saved_type == "income" else "Expense"} added.', 'success')
if txn_type == 'expense': if saved_type == 'expense':
from app.services.alert_service import check_and_flash_budget_alerts from app.services.alert_service import check_and_flash_budget_alerts
check_and_flash_budget_alerts(flash) 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', return render_template('transactions/form.html',
form=form, form=form,
txn_type=txn_type, 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"}') title=f'New {"Income" if txn_type == "income" else "Expense"}')
@@ -155,18 +171,26 @@ def new():
@login_required @login_required
def edit(id): def edit(id):
txn = db.get_or_404(Transaction, 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': if request.method == 'GET':
form.transaction_type.data = txn.transaction_type
form.account_id.data = str(txn.account_id) form.account_id.data = str(txn.account_id)
form.category_id.data = str(txn.category_id) if txn.category_id else '' form.category_id.data = str(txn.category_id) if txn.category_id else ''
if form.validate_on_submit(): if form.validate_on_submit():
old_account_id = txn.account_id old_account_id = txn.account_id
txn.transaction_type = active_type
txn.account_id = int(form.account_id.data) txn.account_id = int(form.account_id.data)
txn.category_id = int(form.category_id.data) if form.category_id.data else None txn.category_id = int(form.category_id.data) if form.category_id.data else None
txn.amount = form.amount.data txn.amount = form.amount.data
@@ -185,7 +209,9 @@ def edit(id):
return render_template('transactions/form.html', return render_template('transactions/form.html',
form=form, form=form,
txn=txn, 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') title='Edit Transaction')
+58 -3
View File
@@ -57,11 +57,28 @@
</div> </div>
<!-- Transaction Form --> <!-- Transaction Form -->
<div class="pcard" style="border-top: 4px solid {% if txn_type=='income' %}var(--income){% else %}var(--expense){% endif %};"> <div class="pcard" id="txnCard" style="border-top: 4px solid {% if txn_type=='income' %}var(--income){% else %}var(--expense){% endif %};">
<form method="POST" novalidate id="txnForm"> <form method="POST" novalidate id="txnForm">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{{ form.transaction_type() }} {{ form.transaction_type() }}
<!-- Type toggle — only for income/expense (not transfer) -->
<div class="mb-3">
<label class="form-label fw-medium" style="font-size:13px;">Type</label>
<div class="d-flex gap-2">
<button type="button" id="toggleIncome" onclick="setTxnType('income')"
class="btn btn-sm flex-fill {% if txn_type=='income' %}btn-success{% else %}btn-outline-secondary{% endif %}"
style="font-size:13px;">
<i class="bi bi-arrow-down-circle me-1"></i>Income
</button>
<button type="button" id="toggleExpense" onclick="setTxnType('expense')"
class="btn btn-sm flex-fill {% if txn_type=='expense' %}btn-danger{% else %}btn-outline-secondary{% endif %}"
style="font-size:13px;">
<i class="bi bi-arrow-up-circle me-1"></i>Expense
</button>
</div>
</div>
<div class="mb-3"> <div class="mb-3">
{{ form.description.label(class="form-label fw-medium", style="font-size:13px;") }} {{ form.description.label(class="form-label fw-medium", style="font-size:13px;") }}
{{ form.description(class="form-control" + (" is-invalid" if form.description.errors else ""), {{ form.description(class="form-control" + (" is-invalid" if form.description.errors else ""),
@@ -103,10 +120,11 @@
</div> </div>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<button type="submit" class="btn {% if txn_type=='income' %}btn-success{% else %}btn-danger{% endif %}"> <button type="submit" id="submitBtn"
class="btn {% if txn_type=='income' %}btn-success{% else %}btn-danger{% endif %}">
Save {{ txn_type | title }} Save {{ txn_type | title }}
</button> </button>
<a href="{{ url_for('transactions.index', tab=txn_type) }}" class="btn btn-outline-secondary">Cancel</a> <a id="cancelLink" href="{{ url_for('transactions.index', tab=txn_type) }}" class="btn btn-outline-secondary">Cancel</a>
</div> </div>
</form> </form>
@@ -370,5 +388,42 @@ function autoOcrOnUpload(input) {
if (panel) { panel.style.display = 'block'; document.getElementById('ocrChevron').className = 'bi bi-chevron-up'; } if (panel) { panel.style.display = 'block'; document.getElementById('ocrChevron').className = 'bi bi-chevron-up'; }
runOcr(file); 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 = '<option value="">— None —</option>' +
cats.map(c => `<option value="${c.id}">${c.name}</option>`).join('');
if (prevVal && cats.some(c => c.id === prevVal)) {
sel.value = prevVal;
}
}
</script> </script>
{% endblock %} {% endblock %}