Initial commit — Moon Household Budget
This commit is contained in:
234
templates/edit.html
Normal file
234
templates/edit.html
Normal file
@@ -0,0 +1,234 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>Edit Transaction</h1>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<form method="POST" class="categorize-form">
|
||||
<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 transaction.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>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Notes <span class="optional">(optional)</span></label>
|
||||
<input type="text" name="notes" value="{{ transaction.notes }}" placeholder="e.g. Costco run, birthday gift...">
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||
<button type="button" class="btn btn-ghost" onclick="toggleSplit()">Split transaction</button>
|
||||
<a href="{{ url_for('transactions_view') }}" class="btn btn-ghost">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Split transaction panel -->
|
||||
<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 }}">
|
||||
<input type="hidden" name="redirect" value="transactions">
|
||||
<table class="budget-table">
|
||||
<thead><tr><th>Category</th><th>Subcategory</th><th>Amount</th><th>Note</th><th></th></tr></thead>
|
||||
<tbody id="split-rows">
|
||||
{% if transaction.splits %}
|
||||
{% for s in transaction.splits %}
|
||||
<tr class="split-row">
|
||||
<td>
|
||||
<select name="split_cat_{{ loop.index0 }}" class="budget-input split-cat" onchange="updateSplitSubcat(this, {{ loop.index0 }})" style="width:130px;">
|
||||
<option value="">— pick —</option>
|
||||
{% for cat in categories %}<option value="{{ cat }}" {% if s.category == cat %}selected{% endif %}>{{ cat }}</option>{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
<td><select name="split_subcat_{{ loop.index0 }}" class="budget-input split-subcat" style="width:130px;"><option value="">— none —</option></select></td>
|
||||
<td><input type="number" name="split_amt_{{ loop.index0 }}" step="0.01" min="0" class="budget-input split-amt" style="width:90px;" value="{{ s.amount }}" oninput="updateRemaining()"></td>
|
||||
<td><input type="text" name="split_note_{{ loop.index0 }}" class="budget-input" style="width:110px;" value="{{ s.note or '' }}"></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>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<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>
|
||||
{% endif %}
|
||||
</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</button>
|
||||
<button type="button" class="btn btn-ghost" onclick="toggleSplit()">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:1.25rem;padding-top:1.25rem;border-top:1px solid var(--border);display:flex;gap:0.5rem;flex-wrap:wrap;">
|
||||
<form method="POST" action="{{ url_for('ignore_transaction') }}">
|
||||
<input type="hidden" name="id" value="{{ transaction.id }}">
|
||||
<input type="hidden" name="redirect" value="transactions">
|
||||
<button type="submit" class="btn btn-ghost" style="color:var(--text-light);font-size:0.82rem;">✓ 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('sinking-picker').style.display='block';this.style.display='none';">🏦 Sinking Fund Transfer</button>
|
||||
<div id="sinking-picker" style="display:none;background:#e8f5e9;border:1px solid #a5d6a7;border-radius:var(--radius);padding:0.75rem 1rem;width:100%;margin-top:0.25rem;">
|
||||
<form method="POST" action="{{ url_for('categorize_sinking_transfer') }}">
|
||||
<input type="hidden" name="id" value="{{ transaction.id }}">
|
||||
<input type="hidden" name="redirect" value="transactions">
|
||||
<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</button>
|
||||
<button type="button" class="btn btn-ghost" style="font-size:0.82rem;" onclick="document.getElementById('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">
|
||||
<input type="hidden" name="redirect" value="transactions">
|
||||
<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">
|
||||
<input type="hidden" name="redirect" value="transactions">
|
||||
<button type="submit" class="btn btn-ghost" style="background:#f3e5f5;color:#6a1b9a;border:1px solid #ce93d8;font-size:0.82rem;">💰 Savings Transfer</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const subcats = {{ categories | tojson }};
|
||||
const current = {{ transaction | tojson }};
|
||||
|
||||
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 (current.subcategory === s) opt.selected = true;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
group.style.display = list.length ? 'block' : 'none';
|
||||
}
|
||||
|
||||
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 = {{ transaction.splits | length if transaction.splits else 2 }};
|
||||
|
||||
function toggleSplit() {
|
||||
const panel = document.getElementById('split-panel');
|
||||
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
|
||||
if (panel.style.display === 'block') updateRemaining();
|
||||
}
|
||||
|
||||
function updateSplitSubcat(selectEl, idx) {
|
||||
const cat = selectEl.value;
|
||||
const subcatSel = document.querySelector(`select[name="split_subcat_${idx}"]`);
|
||||
const list = allSubcats[cat] || [];
|
||||
const current = subcatSel ? subcatSel.dataset.current || '' : '';
|
||||
subcatSel.innerHTML = '<option value="">— none —</option>';
|
||||
list.forEach(s => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = s; opt.textContent = s;
|
||||
if (s === current) opt.selected = true;
|
||||
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);
|
||||
}
|
||||
|
||||
// Pre-populate subcats for existing splits
|
||||
document.querySelectorAll('.split-cat').forEach((sel, idx) => {
|
||||
if (sel.value) updateSplitSubcat(sel, idx);
|
||||
});
|
||||
|
||||
// Open split panel automatically if transaction already has splits
|
||||
{% if transaction.splits %}
|
||||
toggleSplit();
|
||||
{% endif %}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user