Files
2026-06-06 14:22:57 -05:00

397 lines
21 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% block content %}
<div class="page-header">
<h1>Review Transactions</h1>
{% if remaining %}<span class="badge">{{ remaining }} remaining</span>{% endif %}
</div>
{% if remaining %}
<div style="background:var(--warm-white);border:1px solid var(--border);border-radius:var(--radius);padding:0.85rem 1.25rem;margin-bottom:1.25rem;display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap;">
<span style="font-size:0.88rem;color:var(--text-mid);">Need to bulk-clear old transactions?</span>
<form method="POST" action="{{ url_for('bulk_ignore_before') }}" onsubmit="return confirm('Ignore all unreviewed transactions before this date?')">
<div style="display:flex;align-items:center;gap:0.5rem;">
<label style="font-size:0.85rem;">Ignore everything before</label>
<input type="date" name="before_date" required class="budget-input" style="width:150px;">
<button type="submit" class="btn btn-ghost" style="color:var(--text-light);font-size:0.85rem;">Bulk ignore</button>
</div>
</form>
</div>
{% endif %}
{% if not transaction %}
<div class="empty-state card-wide">
<div class="empty-icon"></div>
<h2>All caught up!</h2>
<p>Every transaction has been reviewed.</p>
<a href="{{ url_for('transactions_view') }}" class="btn btn-primary">View All Transactions</a>
</div>
{% else %}
<div class="review-card">
<div class="review-meta">
<span class="review-date">{{ transaction.date }}</span>
<span class="review-account">{{ transaction.account }}</span>
</div>
<div class="review-description">{{ transaction.description }}</div>
<div class="review-amount">${{ transaction.amount | money }}</div>
{% if bnpl_lender and bnpl_matches %}
<div style="background:#f0f5ff;border:1.5px solid #b0c4ef;border-radius:var(--radius);padding:1rem 1.25rem;margin-bottom:1rem;">
<div style="font-weight:600;margin-bottom:0.5rem;">💳 Looks like a {{ bnpl_lender | title }} payment — which loan is this?</div>
<div style="display:flex;flex-direction:column;gap:0.5rem;">
{% for loan in bnpl_matches %}
<form method="POST" action="{{ url_for('match_bnpl') }}">
<input type="hidden" name="txn_id" value="{{ transaction.id }}">
<input type="hidden" name="debt_id" value="{{ loan.id }}">
<input type="hidden" name="category" value="Debt">
<button type="submit" style="width:100%;text-align:left;background:{% if loan.amount_match %}#e8f0fe{% else %}#fff{% endif %};border:1px solid {% if loan.amount_match %}#7aacf0{% else %}var(--border){% endif %};border-radius:6px;padding:0.6rem 0.85rem;cursor:pointer;display:flex;justify-content:space-between;align-items:center;gap:1rem;">
<span>
<strong>{{ loan.name }}</strong>
<span style="font-size:0.78rem;color:var(--text-light);margin-left:0.5rem;">{{ loan.payments_made }}/{{ loan.total_payments }} payments made</span>
</span>
<span style="white-space:nowrap;">
${{ loan.payment_amount | money }}
{% if loan.amount_match %}<span style="color:#5A9E68;font-size:0.78rem;margin-left:0.4rem;">✓ amount matches</span>{% endif %}
</span>
</button>
</form>
{% endfor %}
</div>
<div style="margin-top:0.6rem;font-size:0.8rem;color:var(--text-light);">Selecting a loan will mark one payment as made and categorize this as Debt. Or ignore this and categorize manually below.</div>
</div>
{% elif bnpl_lender %}
<div style="background:#fff8e6;border:1px solid #e8d49a;border-radius:var(--radius);padding:1rem 1.25rem;margin-bottom:1rem;">
<div style="font-weight:600;margin-bottom:0.25rem;">💳 Looks like a {{ bnpl_lender | title }} payment — no active loans found</div>
<div style="font-size:0.83rem;color:var(--text-light);margin-bottom:0.75rem;">Want to set up this loan now so future payments get tracked automatically?</div>
<button type="button" class="btn btn-ghost" style="font-size:0.85rem;" onclick="document.getElementById('bnpl-quick-add').style.display='block';this.style.display='none';">
+ Add {{ bnpl_lender | title }} loan now
</button>
<div id="bnpl-quick-add" style="display:none;margin-top:0.75rem;">
<form method="POST" action="{{ url_for('bnpl_add') }}">
<input type="hidden" name="bnpl_provider" value="{{ bnpl_lender | title }}">
<input type="hidden" name="redirect_to" value="review">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0.75rem;margin-bottom:0.75rem;">
<div>
<label style="font-size:0.8rem;display:block;margin-bottom:0.2rem;">What did you buy?</label>
<input type="text" name="description" class="budget-input" style="width:100%;" placeholder="e.g. Nike Shoes" required>
</div>
<div>
<label style="font-size:0.8rem;display:block;margin-bottom:0.2rem;">Merchant (optional)</label>
<input type="text" name="merchant" class="budget-input" style="width:100%;" placeholder="e.g. Nike">
</div>
<div>
<label style="font-size:0.8rem;display:block;margin-bottom:0.2rem;">Payment Amount</label>
<input type="number" name="payment_amount" step="0.01" min="0" class="budget-input" style="width:100%;" value="{{ transaction.amount }}" required>
</div>
<div>
<label style="font-size:0.8rem;display:block;margin-bottom:0.2rem;">Total # of Payments</label>
<input type="number" name="total_payments" min="1" class="budget-input" style="width:100%;" placeholder="e.g. 4" required>
</div>
<div>
<label style="font-size:0.8rem;display:block;margin-bottom:0.2rem;">Frequency</label>
<select name="frequency" class="budget-input" style="width:100%;">
<option value="biweekly">Every 2 weeks</option>
<option value="monthly">Monthly</option>
<option value="weekly">Weekly</option>
</select>
</div>
<div>
<label style="font-size:0.8rem;display:block;margin-bottom:0.2rem;">Next Payment Date</label>
<input type="date" name="next_payment_date" class="budget-input" style="width:100%;">
</div>
<div>
<label style="font-size:0.8rem;display:block;margin-bottom:0.2rem;">Who's it for?</label>
<select name="owner" class="budget-input" style="width:100%;">
<option value="Shared">Shared</option>
<option value="Bonna">Bonna</option>
<option value="Tony">Tony</option>
</select>
</div>
<div>
<label style="font-size:0.8rem;display:block;margin-bottom:0.2rem;">Payments already made</label>
<input type="number" name="payments_made" min="0" value="1" class="budget-input" style="width:100%;">
</div>
</div>
<div style="display:flex;gap:0.75rem;align-items:center;">
<button type="submit" class="btn btn-primary">Save Loan & Continue</button>
<button type="button" class="btn btn-ghost" onclick="document.getElementById('bnpl-quick-add').style.display='none';this.closest('div').previousElementSibling.style.display='inline-block';">Cancel</button>
<span style="font-size:0.78rem;color:var(--text-light);">This transaction will still need to be categorized on the next screen</span>
</div>
</form>
</div>
</div>
{% endif %}
{% if suggestion %}
<div class="suggestion-banner">
<span>Suggested: <strong>{{ suggestion.category }}{% if suggestion.subcategory %} {{ suggestion.subcategory }}{% endif %}</strong></span>
<form method="POST" action="{{ url_for('categorize') }}" style="display:inline;">
<input type="hidden" name="id" value="{{ transaction.id }}">
<input type="hidden" name="category" value="{{ suggestion.category }}">
<input type="hidden" name="subcategory" value="{{ suggestion.subcategory or '' }}">
<button type="submit" class="btn btn-accept">Accept</button>
</form>
</div>
{% endif %}
<form method="POST" action="{{ url_for('categorize') }}" class="categorize-form">
<input type="hidden" name="id" value="{{ transaction.id }}">
<div class="form-group">
<label>Category</label>
<div class="category-grid">
{% for cat, subcats in categories.items() %}
<label class="cat-option" style="--cat-color: {{ colors.get(cat, '#ddd') }};">
<input type="radio" name="category" value="{{ cat }}"
{% if suggestion and suggestion.category == cat %}checked{% endif %}
onchange="updateSubcats(this.value)">
<span>{{ cat }}</span>
</label>
{% endfor %}
</div>
</div>
<div class="form-group" id="subcat-group">
<label>Subcategory <span class="optional">(optional)</span></label>
<select name="subcategory" id="subcat-select">
<option value="">— none —</option>
</select>
</div>
<!-- Sinking fund picker — shown when Transfer > Sinking Fund is selected -->
<div class="form-group" id="sinking-fund-prompt" style="display:none;">
<label>Which fund? <span class="optional">(will auto-add to that fund's balance)</span></label>
<select name="sinking_fund_name" id="sinking-fund-select" class="budget-input">
<option value="">— select fund —</option>
{% for f in sinking_funds %}
<option value="{{ f.name }}">{{ f.name }} (current: ${{ (f.balance or 0) | money }})</option>
{% endfor %}
</select>
</div>
<!-- Car prompt — shown when Gas subcategory is selected -->
<div class="form-group" id="car-prompt" style="display:none;">
<label>Which car? <span class="optional">(will auto-log to that car's profile)</span></label>
<select name="car_id" id="car-select">
<option value="">— select car —</option>
{% for car in personal_cars %}
<option value="{{ car.id }}">{{ car.display_name }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label>Notes <span class="optional">(optional)</span></label>
<input type="text" name="notes" placeholder="e.g. Costco run, birthday gift...">
</div>
<div class="form-group checkbox-group">
<label>
<input type="checkbox" name="remember" checked>
Remember this for future transactions from this merchant
</label>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Save & Next</button>
<button type="button" class="btn btn-ghost" onclick="toggleSplit()">Split transaction</button>
</div>
</form>
<!-- Quick-action buttons — must be outside the main form -->
<div style="display:flex;flex-wrap:wrap;gap:0.5rem;margin-top:0.75rem;">
<form method="POST" action="{{ url_for('ignore_transaction') }}">
<input type="hidden" name="id" value="{{ transaction.id }}">
<button type="submit" class="btn btn-ghost" style="color:var(--text-light);font-size:0.82rem;"
title="Marks as reviewed but excludes from all totals — use for bill payments, debt payments, income deposits">
✓ Already tracked — ignore
</button>
</form>
<button type="button" class="btn btn-ghost" style="background:#e8f5e9;color:#3a7d44;border:1px solid #a5d6a7;font-size:0.82rem;" onclick="document.getElementById('review-sinking-picker').style.display='block';this.style.display='none';">🏦 Sinking Fund Transfer</button>
<div id="review-sinking-picker" style="display:none;background:#e8f5e9;border:1px solid #a5d6a7;border-radius:var(--radius);padding:0.75rem 1rem;width:100%;">
<form method="POST" action="{{ url_for('categorize_sinking_transfer') }}">
<input type="hidden" name="id" value="{{ transaction.id }}">
<input type="hidden" name="redirect" value="review">
<label style="font-size:0.82rem;font-weight:600;display:block;margin-bottom:0.4rem;">Which sinking fund?</label>
<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap;">
<select name="sinking_fund_name" class="budget-input" style="min-width:180px;" required>
<option value="">— select fund —</option>
{% for f in sinking_funds %}
<option value="{{ f.name }}">{{ f.name }} (${{ (f.balance or 0) | money }})</option>
{% endfor %}
</select>
<button type="submit" class="btn btn-primary" style="font-size:0.82rem;">Save & Next</button>
<button type="button" class="btn btn-ghost" style="font-size:0.82rem;" onclick="document.getElementById('review-sinking-picker').style.display='none';this.closest('div').previousElementSibling.style.display='inline-block';">Cancel</button>
</div>
</form>
</div>
<form method="POST" action="{{ url_for('ignore_transaction') }}">
<input type="hidden" name="id" value="{{ transaction.id }}">
<input type="hidden" name="label" value="Investment Transfer">
<button type="submit" class="btn btn-ghost" style="background:#e3f2fd;color:#1565c0;border:1px solid #90caf9;font-size:0.82rem;">📈 Investment Transfer</button>
</form>
<form method="POST" action="{{ url_for('ignore_transaction') }}">
<input type="hidden" name="id" value="{{ transaction.id }}">
<input type="hidden" name="label" value="Savings Transfer">
<button type="submit" class="btn btn-ghost" style="background:#f3e5f5;color:#6a1b9a;border:1px solid #ce93d8;font-size:0.82rem;">💰 Savings Transfer</button>
</form>
<form method="POST" action="{{ url_for('skip') }}">
<input type="hidden" name="id" value="{{ transaction.id }}">
<button type="submit" class="btn btn-ghost" style="font-size:0.82rem;">Skip for now</button>
</form>
</div>
<!-- Split transaction form -->
<div id="split-panel" style="display:none;margin-top:1.5rem;">
<div class="budget-section">
<div class="budget-section-header" style="border-left-color:#D4A96A;">
<h3>Split Transaction — ${{ transaction.amount | money }} total</h3>
</div>
<p class="muted" style="margin-bottom:1rem;font-size:0.85rem;">Divide this charge across multiple categories. Amounts must add up to ${{ transaction.amount | money }}.</p>
<form method="POST" action="{{ url_for('split_transaction') }}">
<input type="hidden" name="id" value="{{ transaction.id }}">
<table class="budget-table" id="split-table">
<thead><tr><th>Category</th><th>Subcategory</th><th>Amount</th><th>Note</th><th></th></tr></thead>
<tbody id="split-rows">
<tr class="split-row">
<td>
<select name="split_cat_0" class="budget-input split-cat" onchange="updateSplitSubcat(this, 0)" style="width:130px;">
<option value="">— pick —</option>
{% for cat in categories %}<option value="{{ cat }}">{{ cat }}</option>{% endfor %}
</select>
</td>
<td><select name="split_subcat_0" class="budget-input split-subcat" style="width:130px;"><option value="">— none —</option></select></td>
<td><input type="number" name="split_amt_0" step="0.01" min="0" class="budget-input split-amt" style="width:90px;" placeholder="0.00" oninput="updateRemaining()"></td>
<td><input type="text" name="split_note_0" class="budget-input" style="width:110px;" placeholder="optional"></td>
<td></td>
</tr>
<tr class="split-row">
<td>
<select name="split_cat_1" class="budget-input split-cat" onchange="updateSplitSubcat(this, 1)" style="width:130px;">
<option value="">— pick —</option>
{% for cat in categories %}<option value="{{ cat }}">{{ cat }}</option>{% endfor %}
</select>
</td>
<td><select name="split_subcat_1" class="budget-input split-subcat" style="width:130px;"><option value="">— none —</option></select></td>
<td><input type="number" name="split_amt_1" step="0.01" min="0" class="budget-input split-amt" style="width:90px;" placeholder="0.00" oninput="updateRemaining()"></td>
<td><input type="text" name="split_note_1" class="budget-input" style="width:110px;" placeholder="optional"></td>
<td></td>
</tr>
</tbody>
</table>
<div style="margin:0.75rem 0;font-size:0.85rem;">
Remaining: <strong id="split-remaining">${{ transaction.amount | money }}</strong>
<button type="button" class="btn btn-ghost" style="margin-left:1rem;font-size:0.78rem;" onclick="addSplitRow()">+ Add row</button>
</div>
<div style="display:flex;gap:0.75rem;">
<button type="submit" class="btn btn-primary">Save Split & Next</button>
<button type="button" class="btn btn-ghost" onclick="toggleSplit()">Cancel</button>
</div>
</form>
</div>
</div>
</div>
<script>
const subcats = {{ categories | tojson }};
const suggestion = {{ suggestion | tojson if suggestion else 'null' }};
function updateSubcats(cat) {
const select = document.getElementById('subcat-select');
const group = document.getElementById('subcat-group');
const list = subcats[cat] || [];
select.innerHTML = '<option value="">— none —</option>';
list.forEach(s => {
const opt = document.createElement('option');
opt.value = s;
opt.textContent = s;
if (suggestion && suggestion.subcategory === s) opt.selected = true;
select.appendChild(opt);
});
group.style.display = list.length ? 'block' : 'none';
checkCarPrompt(cat, select.value);
}
function checkCarPrompt(cat, subcat) {
const carPrompt = document.getElementById('car-prompt');
if (cat === 'Car' && subcat === 'Gas') {
carPrompt.style.display = 'block';
} else {
carPrompt.style.display = 'none';
document.getElementById('car-select').value = '';
}
const sinkingPrompt = document.getElementById('sinking-fund-prompt');
if (cat === 'Transfer' && subcat === 'Sinking Fund') {
sinkingPrompt.style.display = 'block';
} else {
sinkingPrompt.style.display = 'none';
document.getElementById('sinking-fund-select').value = '';
}
}
document.getElementById('subcat-select').addEventListener('change', function() {
const cat = document.querySelector('input[name="category"]:checked');
checkCarPrompt(cat ? cat.value : '', this.value);
});
// Initialize on load
const checked = document.querySelector('input[name="category"]:checked');
if (checked) updateSubcats(checked.value);
document.querySelectorAll('input[name="category"]').forEach(r => {
r.addEventListener('change', () => updateSubcats(r.value));
});
// Split transaction
const totalAmount = {{ transaction.amount }};
const allSubcats = {{ categories | tojson }};
let splitRowCount = 2;
function toggleSplit() {
const panel = document.getElementById('split-panel');
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
}
function updateSplitSubcat(selectEl, idx) {
const cat = selectEl.value;
const subcatSel = document.querySelector(`select[name="split_subcat_${idx}"]`);
const list = allSubcats[cat] || [];
subcatSel.innerHTML = '<option value="">— none —</option>';
list.forEach(s => {
const opt = document.createElement('option');
opt.value = s; opt.textContent = s;
subcatSel.appendChild(opt);
});
}
function updateRemaining() {
const inputs = document.querySelectorAll('.split-amt');
let used = 0;
inputs.forEach(el => used += parseFloat(el.value) || 0);
const remaining = Math.round((totalAmount - used) * 100) / 100;
const el = document.getElementById('split-remaining');
el.textContent = '$' + remaining.toFixed(2);
el.style.color = Math.abs(remaining) < 0.01 ? '#5A9E68' : (remaining < 0 ? '#c07070' : 'inherit');
}
function addSplitRow() {
const idx = splitRowCount++;
const tbody = document.getElementById('split-rows');
const row = document.createElement('tr');
row.className = 'split-row';
row.innerHTML = `
<td><select name="split_cat_${idx}" class="budget-input split-cat" onchange="updateSplitSubcat(this,${idx})" style="width:130px;">
<option value="">— pick —</option>
${Object.keys(allSubcats).map(c => `<option value="${c}">${c}</option>`).join('')}
</select></td>
<td><select name="split_subcat_${idx}" class="budget-input split-subcat" style="width:130px;"><option value="">— none —</option></select></td>
<td><input type="number" name="split_amt_${idx}" step="0.01" min="0" class="budget-input split-amt" style="width:90px;" placeholder="0.00" oninput="updateRemaining()"></td>
<td><input type="text" name="split_note_${idx}" class="budget-input" style="width:110px;" placeholder="optional"></td>
<td><button type="button" onclick="this.closest('tr').remove();updateRemaining();" style="background:none;border:none;color:#c07070;cursor:pointer;font-size:1rem;">✕</button></td>
`;
tbody.appendChild(row);
}
</script>
{% endif %}
{% endblock %}