153 lines
7.6 KiB
HTML
153 lines
7.6 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Split Transaction{% endblock %}
|
|
{% block page_title %}Split Transaction{% endblock %}
|
|
|
|
{% block topbar_actions %}
|
|
<a href="{{ url_for('transactions.index', tab=txn.transaction_type) }}" class="btn btn-sm btn-outline-secondary" style="font-size:12px;">
|
|
<i class="bi bi-arrow-left me-1"></i>Back
|
|
</a>
|
|
{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
.split-row { display:grid; grid-template-columns:1fr 2fr 110px 36px; gap:8px; align-items:center; }
|
|
@media (max-width:575px) { .split-row { grid-template-columns:1fr 1fr 90px 28px; } }
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="row justify-content-center">
|
|
<div class="col-12 col-md-8 col-lg-7">
|
|
|
|
<!-- Original transaction summary -->
|
|
<div class="pcard mb-3" style="border-left:4px solid {% if txn.transaction_type=='income' %}var(--income){% else %}var(--expense){% endif %};">
|
|
<div style="font-size:12px;color:var(--muted);margin-bottom:4px;">Original transaction</div>
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<div style="font-weight:600;font-size:14px;">{{ txn.description }}</div>
|
|
<div style="font-size:12px;color:var(--muted);">{{ txn.date.strftime('%b %d, %Y') }} · {{ txn.account.name if txn.account else '—' }}</div>
|
|
</div>
|
|
<div class="mono {% if txn.transaction_type=='income' %}text-income{% else %}text-expense{% endif %}" style="font-size:18px;font-weight:700;">
|
|
{{ txn.amount | currency }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Split form -->
|
|
<div class="pcard">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h6 class="mb-0" style="font-size:14px;font-weight:600;">Split into parts</h6>
|
|
<div style="font-size:12px;color:var(--muted);">
|
|
Remaining: <span id="remaining" class="mono fw-bold">{{ txn.amount | currency }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<form method="POST" id="splitForm">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
|
|
<!-- Column headers -->
|
|
<div class="split-row mb-2" style="font-size:11px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;">
|
|
<span>Category</span>
|
|
<span>Description</span>
|
|
<span>Amount</span>
|
|
<span></span>
|
|
</div>
|
|
|
|
<div id="split-rows">
|
|
<!-- Two default rows -->
|
|
<div class="split-row mb-2 split-entry">
|
|
<select name="split_category" class="form-select form-select-sm">
|
|
<option value="">— None —</option>
|
|
{% for cat in categories %}
|
|
<option value="{{ cat.id }}"{% if txn.category_id == cat.id %} selected{% endif %}>{{ cat.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
<input type="text" name="split_description" class="form-control form-control-sm" placeholder="{{ txn.description }}" value="{{ txn.description }}">
|
|
<input type="number" name="split_amount" class="form-control form-control-sm split-amt" placeholder="0.00" step="0.01" min="0.01" oninput="updateRemaining()">
|
|
<button type="button" onclick="removeRow(this)" class="btn btn-sm btn-outline-danger" style="padding:2px 8px;"><i class="bi bi-x-lg"></i></button>
|
|
</div>
|
|
<div class="split-row mb-2 split-entry">
|
|
<select name="split_category" class="form-select form-select-sm">
|
|
<option value="">— None —</option>
|
|
{% for cat in categories %}
|
|
<option value="{{ cat.id }}">{{ cat.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
<input type="text" name="split_description" class="form-control form-control-sm" placeholder="{{ txn.description }}" value="{{ txn.description }}">
|
|
<input type="number" name="split_amount" class="form-control form-control-sm split-amt" placeholder="0.00" step="0.01" min="0.01" oninput="updateRemaining()">
|
|
<button type="button" onclick="removeRow(this)" class="btn btn-sm btn-outline-danger" style="padding:2px 8px;"><i class="bi bi-x-lg"></i></button>
|
|
</div>
|
|
</div>
|
|
|
|
<button type="button" onclick="addRow()" class="btn btn-sm btn-outline-secondary mb-3" style="font-size:12px;">
|
|
<i class="bi bi-plus-lg me-1"></i>Add row
|
|
</button>
|
|
|
|
<!-- Total mismatch warning -->
|
|
<div id="total-warn" style="display:none;background:#fee2e2;border:1px solid #fca5a5;border-radius:6px;padding:8px 12px;font-size:12px;color:#991b1b;margin-bottom:12px;">
|
|
<i class="bi bi-exclamation-triangle-fill me-1"></i>
|
|
Split total must equal <strong>{{ txn.amount | currency }}</strong>.
|
|
</div>
|
|
|
|
<div class="d-flex gap-2">
|
|
<button type="submit" id="splitBtn" class="btn btn-primary" style="font-size:13px;">
|
|
<i class="bi bi-scissors me-1"></i>Confirm Split
|
|
</button>
|
|
<a href="{{ url_for('transactions.index', tab=txn.transaction_type) }}" class="btn btn-outline-secondary" style="font-size:13px;">Cancel</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
const ORIGINAL = {{ txn.amount | float }};
|
|
const CSRF = '{{ csrf_token() }}';
|
|
|
|
const catOptions = `{% for cat in categories %}<option value="{{ cat.id }}">{{ cat.name }}</option>{% endfor %}`;
|
|
const defaultDesc = {{ txn.description | tojson }};
|
|
|
|
function updateRemaining() {
|
|
const amts = [...document.querySelectorAll('.split-amt')].map(i => parseFloat(i.value) || 0);
|
|
const total = amts.reduce((a, b) => a + b, 0);
|
|
const rem = ORIGINAL - total;
|
|
const el = document.getElementById('remaining');
|
|
el.textContent = (rem < 0 ? '-' : '') + Math.abs(rem).toLocaleString('en-US', {minimumFractionDigits:2, maximumFractionDigits:2});
|
|
el.style.color = Math.abs(rem) < 0.005 ? '#10b981' : (rem < 0 ? '#ef4444' : 'var(--text)');
|
|
document.getElementById('total-warn').style.display = Math.abs(total - ORIGINAL) > 0.005 && total > 0 ? 'block' : 'none';
|
|
}
|
|
|
|
function addRow() {
|
|
const container = document.getElementById('split-rows');
|
|
const div = document.createElement('div');
|
|
div.className = 'split-row mb-2 split-entry';
|
|
div.innerHTML = `
|
|
<select name="split_category" class="form-select form-select-sm">
|
|
<option value="">— None —</option>${catOptions}
|
|
</select>
|
|
<input type="text" name="split_description" class="form-control form-control-sm" placeholder="${defaultDesc}" value="${defaultDesc}">
|
|
<input type="number" name="split_amount" class="form-control form-control-sm split-amt" placeholder="0.00" step="0.01" min="0.01" oninput="updateRemaining()">
|
|
<button type="button" onclick="removeRow(this)" class="btn btn-sm btn-outline-danger" style="padding:2px 8px;"><i class="bi bi-x-lg"></i></button>`;
|
|
container.appendChild(div);
|
|
}
|
|
|
|
function removeRow(btn) {
|
|
const rows = document.querySelectorAll('.split-entry');
|
|
if (rows.length <= 2) { alert('Keep at least 2 rows.'); return; }
|
|
btn.closest('.split-entry').remove();
|
|
updateRemaining();
|
|
}
|
|
|
|
document.getElementById('splitForm').addEventListener('submit', function(e) {
|
|
const amts = [...document.querySelectorAll('.split-amt')].map(i => parseFloat(i.value) || 0);
|
|
const total = amts.reduce((a, b) => a + b, 0);
|
|
if (Math.abs(total - ORIGINAL) > 0.005) {
|
|
e.preventDefault();
|
|
document.getElementById('total-warn').style.display = 'block';
|
|
document.getElementById('total-warn').scrollIntoView({behavior:'smooth', block:'nearest'});
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|