Initial commit — Moon Household Budget
This commit is contained in:
484
templates/weekly.html
Normal file
484
templates/weekly.html
Normal file
@@ -0,0 +1,484 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>{{ month_name }} {{ year }} — Weekly Overview</h1>
|
||||
<div class="month-nav">
|
||||
<a href="{{ url_for('weekly_overview', year=prev_year, month=prev_month) }}" class="btn btn-ghost">‹ Prev</a>
|
||||
<a href="{{ url_for('weekly_overview', year=next_year, month=next_month) }}" class="btn btn-ghost">Next ›</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="week-tabs">
|
||||
{% for w in weeks %}
|
||||
<button class="week-tab" onclick="showWeek({{ w.num }})" id="tab-{{ w.num }}">
|
||||
Week {{ w.num }}<br><span>{{ w.start_label }} – {{ w.end_label }}</span>
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% for w in weeks %}
|
||||
<div class="week-panel" id="week-{{ w.num }}">
|
||||
|
||||
<!-- Week summary -->
|
||||
{% if w.summary %}
|
||||
<div style="background:var(--warm-white);border:1px solid var(--border);border-radius:var(--radius);padding:0.85rem 1.1rem;margin-bottom:1rem;display:flex;flex-wrap:wrap;gap:0.5rem 1.5rem;">
|
||||
<span style="font-size:0.75rem;font-weight:600;text-transform:uppercase;letter-spacing:0.06em;color:var(--text-light);width:100%;">How's this week looking?</span>
|
||||
{% for line in w.summary %}
|
||||
<span style="font-size:0.88rem;color:var(--text-dark);">{{ line | bold_md }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Starting Balance -->
|
||||
<div style="display:flex;align-items:center;gap:1rem;background:var(--warm-white);border:1px solid var(--border);border-radius:var(--radius);padding:0.75rem 1rem;margin-bottom:1rem;">
|
||||
<label style="font-weight:600;white-space:nowrap;">Checking Balance at Start of Week</label>
|
||||
<input type="number" step="0.01" class="budget-input" style="max-width:160px;"
|
||||
id="starting-balance-{{ w.num }}"
|
||||
value="{{ w.starting_balance }}"
|
||||
onchange="saveStartingBalance({{ year }}, {{ month }}, '{{ w.num }}', this.value)"
|
||||
placeholder="0.00">
|
||||
<span style="font-size:0.82rem;color:var(--text-light);">Enter what's in checking when the week begins</span>
|
||||
</div>
|
||||
|
||||
<!-- 1. Income -->
|
||||
<div class="budget-section">
|
||||
<div class="budget-section-header weekly-collapsible" onclick="toggleWeeklySection(this)" style="border-left-color: {{ colors.get('Income', '#A0C5A8') }};cursor:pointer;">
|
||||
<h3><span class="weekly-arrow">▾</span> Income</h3>
|
||||
</div>
|
||||
<div class="weekly-section-body">
|
||||
<table class="budget-table">
|
||||
<thead><tr><th>Source</th><th>Budgeted</th><th>Actual</th></tr></thead>
|
||||
<tbody>
|
||||
{% for src in w.income_sources %}
|
||||
{% set inc = w.income.get(src, {}) %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ src }}
|
||||
{% if src == 'Auto King' %}
|
||||
<span class="income-friday-tag">Fridays</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><input type="number" step="0.01" class="budget-input"
|
||||
value="{{ inc.get('budgeted', '') }}"
|
||||
onchange="saveField({{ year }}, {{ month }}, '{{ w.num }}', 'income', '{{ src }}', 'budgeted', this.value)"
|
||||
placeholder="expected"></td>
|
||||
<td>
|
||||
<div class="income-actual-wrap">
|
||||
<input type="number" step="0.01" class="budget-input income-actual-input"
|
||||
id="income-actual-{{ w.num }}-{{ src | replace(' ','_') }}"
|
||||
value="{{ inc.get('actual', '') }}"
|
||||
onchange="logIncome({{ year }}, {{ month }}, '{{ w.num }}', '{{ src }}', this.value)"
|
||||
placeholder="enter when received">
|
||||
{% if inc.get('actual') %}
|
||||
<span class="income-logged-badge">✓ logged</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="total-row">
|
||||
<td>Total</td>
|
||||
<td class="amount">$<span id="w{{ w.num }}-income-budget-total">{{ (w.income.values() | map(attribute='budgeted') | select | map('float') | sum if w.income else 0) | money }}</span></td>
|
||||
<td class="amount">$<span id="w{{ w.num }}-income-actual-total">{{ w.income_total | money }}</span></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. Bills -->
|
||||
<div class="budget-section">
|
||||
<div class="budget-section-header weekly-collapsible" onclick="toggleWeeklySection(this)" style="border-left-color: {{ colors.get('Debt', '#C5A0A0') }};cursor:pointer;">
|
||||
<h3><span class="weekly-arrow">▾</span> Bills Due This Week</h3>
|
||||
<a href="{{ url_for('bills_list') }}" class="edit-link" onclick="event.stopPropagation()">manage bills</a>
|
||||
</div>
|
||||
<div class="weekly-section-body">
|
||||
{% if w.bills %}
|
||||
<table class="budget-table">
|
||||
<thead><tr><th>Bill</th><th>Due</th><th>Amount</th><th>Category</th></tr></thead>
|
||||
<tbody>
|
||||
{% for b in w.bills %}
|
||||
<tr>
|
||||
<td>{{ b.name }}</td>
|
||||
<td class="muted">{{ b.due_date.strftime('%b %-d') if b.due_date else '' }}</td>
|
||||
<td class="amount">{% if b.amount %}${{ b.amount | money }}{% else %}<span class="muted">—</span>{% endif %}</td>
|
||||
<td>
|
||||
{% if b.category %}
|
||||
<span class="tag" style="background: {{ colors.get(b.category, '#ddd') }};">{{ b.category }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="total-row">
|
||||
<td colspan="2">Bills Total</td>
|
||||
<td class="amount">${{ w.bills_total | money }}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="muted" style="padding: 0.75rem 0;">No bills due this week.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. Budget Categories -->
|
||||
<div class="budget-section">
|
||||
<div class="budget-section-header weekly-collapsible" onclick="toggleWeeklySection(this)" style="border-left-color: {{ colors.get('Food', '#A8C5A0') }};cursor:pointer;">
|
||||
<h3><span class="weekly-arrow">▾</span> Budget Categories</h3>
|
||||
</div>
|
||||
<div class="weekly-section-body">
|
||||
{% set skip = ['Income', 'Debt', 'Transfer'] %}
|
||||
{% set total_budgeted = namespace(v=0) %}
|
||||
{% set total_actual = namespace(v=0) %}
|
||||
<table class="budget-table">
|
||||
<thead><tr><th>Category</th><th>Budgeted</th><th>Spent</th><th>Left to Spend</th></tr></thead>
|
||||
<tbody>
|
||||
{% for cat in categories if cat not in skip %}
|
||||
{% set cb = w.cat_budget.get(cat, {}) %}
|
||||
{% set actual = w.cat_actuals.get(cat, 0) %}
|
||||
{% set budgeted = cb.get('budgeted', '') | float %}
|
||||
{% set left = budgeted - actual %}
|
||||
{% set over = budgeted > 0 and actual > budgeted %}
|
||||
{% set total_budgeted.v = total_budgeted.v + budgeted %}
|
||||
{% set total_actual.v = total_actual.v + actual %}
|
||||
<tr{% if over %} style="background:#fdf0f0;"{% endif %}>
|
||||
<td>
|
||||
<span class="tag" style="background: {{ colors.get(cat, '#ddd') }};">{{ cat }}</span>
|
||||
</td>
|
||||
<td><input type="number" step="0.01" class="budget-input"
|
||||
value="{{ cb.get('budgeted', '') }}"
|
||||
onchange="saveField({{ year }}, {{ month }}, '{{ w.num }}', 'categories', '{{ cat }}', 'budgeted', this.value)"
|
||||
placeholder="0.00"></td>
|
||||
<td class="amount{% if actual == 0 %} muted{% endif %}">
|
||||
${{ actual | money }}
|
||||
</td>
|
||||
<td class="amount">
|
||||
{% if budgeted %}
|
||||
{% if over %}
|
||||
<span style="color:#c07070;">-${{ (actual - budgeted) | money }}</span>
|
||||
{% else %}
|
||||
<span style="color:#5A9E68;">${{ left | money }}</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
{% set total_left = total_budgeted.v - total_actual.v %}
|
||||
<tr class="total-row">
|
||||
<td>Totals</td>
|
||||
<td class="amount">${{ total_budgeted.v | money }}</td>
|
||||
<td class="amount">${{ total_actual.v | money }}</td>
|
||||
<td class="amount">
|
||||
{% if total_budgeted.v > 0 %}
|
||||
{% if total_left < 0 %}
|
||||
<span style="color:#c07070;font-weight:700;">-${{ total_left | abs | money }} over</span>
|
||||
{% else %}
|
||||
<span style="color:#5A9E68;font-weight:700;">${{ total_left | money }} left</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4. Sinking Funds -->
|
||||
<div class="budget-section">
|
||||
<div class="budget-section-header weekly-collapsible weekly-collapsed" onclick="toggleWeeklySection(this)" style="border-left-color: {{ colors.get('Travel', '#C5BBA0') }};cursor:pointer;">
|
||||
<h3><span class="weekly-arrow">▸</span> Sinking Funds</h3>
|
||||
<a href="{{ url_for('sinking_view') }}" class="edit-link" onclick="event.stopPropagation()">manage funds</a>
|
||||
</div>
|
||||
<div class="weekly-section-body" style="display:none;">
|
||||
<table class="budget-table">
|
||||
<thead><tr><th>Fund</th><th>Current Balance</th><th>Add This Week</th></tr></thead>
|
||||
<tbody>
|
||||
{% for f in sinking_funds %}
|
||||
{% set wkey = 'sinking_' ~ w.num ~ '_' ~ f.name %}
|
||||
{% set sinking_entry = w.sinking.get(f.name, {}) %}
|
||||
{% set contributed = sinking_entry.get('contributed', '') %}
|
||||
{% set from_txn = sinking_entry.get('from_transaction') %}
|
||||
<tr>
|
||||
<td>
|
||||
<span class="tag" style="background: {{ fund_colors.get(f.name, '#C5BBA0') }};">{{ f.name }}</span>
|
||||
</td>
|
||||
<td class="amount">${{ (f.balance or 0) | money }}</td>
|
||||
<td>
|
||||
<div class="income-actual-wrap">
|
||||
<input type="number" step="0.01" class="budget-input"
|
||||
value="{{ contributed }}"
|
||||
onchange="contributeSinking('{{ f.name }}', this.value, {{ year }}, {{ month }}, '{{ w.num }}')"
|
||||
placeholder="0.00"
|
||||
{% if from_txn %}title="Logged automatically from a categorized transaction"{% endif %}>
|
||||
{% if from_txn %}
|
||||
<span class="income-logged-badge" title="This was recorded when you categorized a transaction">✓ from transaction</span>
|
||||
{% elif contributed %}
|
||||
<span class="income-logged-badge">✓ added</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="total-row">
|
||||
<td colspan="2">Total Contributed This Week</td>
|
||||
<td class="amount">${{ (w.sinking.values() | selectattr('contributed') | map(attribute='contributed') | map('float') | sum) | money }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 5. Extra Debt Payments -->
|
||||
<div class="budget-section">
|
||||
<div class="budget-section-header weekly-collapsible weekly-collapsed" onclick="toggleWeeklySection(this)" style="border-left-color: {{ colors.get('Debt', '#C5A0A0') }};cursor:pointer;">
|
||||
<h3><span class="weekly-arrow">▸</span> Extra Debt Payments</h3>
|
||||
<a href="{{ url_for('debts_view') }}" class="edit-link" onclick="event.stopPropagation()">manage debts</a>
|
||||
</div>
|
||||
<div class="weekly-section-body" style="display:none;">
|
||||
{% if active_debts %}
|
||||
<table class="budget-table">
|
||||
<thead><tr><th>Debt</th><th>Owner</th><th>Minimum</th><th>Extra Payment</th></tr></thead>
|
||||
<tbody>
|
||||
{% for d in active_debts %}
|
||||
{% set extra_paid = w.extra_debt.get(d.id, {}).get('extra', '') %}
|
||||
<tr>
|
||||
<td>{{ d.name }}</td>
|
||||
<td><span class="owner-tag" style="background:{{ owner_colors.get(d.owner,'#ddd') }};font-size:0.72rem;">{{ d.owner }}</span></td>
|
||||
<td class="amount muted">${{ (d.minimum or 0) | money }}</td>
|
||||
<td>
|
||||
<div class="income-actual-wrap">
|
||||
<input type="number" step="0.01" class="budget-input"
|
||||
value="{{ extra_paid }}"
|
||||
onchange="logExtraDebt('{{ d.id }}', this.value, {{ year }}, {{ month }}, '{{ w.num }}')"
|
||||
placeholder="0.00">
|
||||
{% if extra_paid %}
|
||||
<span class="income-logged-badge">✓ logged</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="total-row">
|
||||
<td colspan="3">Total Extra This Week</td>
|
||||
<td class="amount accent">${{ (w.extra_debt.values() | map(attribute='extra') | select | map('float') | sum) | money }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="muted" style="padding:0.75rem 0;">No debts added yet. <a href="{{ url_for('debts_view') }}">Add debts</a> to track extra payments here.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 6. Transactions -->
|
||||
<div class="budget-section">
|
||||
<div class="budget-section-header weekly-collapsible weekly-collapsed" onclick="toggleWeeklySection(this)" style="border-left-color:#D4A96A;cursor:pointer;">
|
||||
<h3><span class="weekly-arrow">▸</span> Transactions This Week{% if w.transactions %} <span style="font-weight:400;font-size:0.82rem;color:var(--text-light);">({{ w.transactions | length }})</span>{% endif %}</h3>
|
||||
</div>
|
||||
<div class="weekly-section-body" style="display:none;">
|
||||
|
||||
<!-- Inline import -->
|
||||
<details style="margin-bottom:1rem;">
|
||||
<summary class="btn" style="display:inline-block;cursor:pointer;font-size:0.85rem;background:#7A9E7E;color:#fff;border:none;">⬆️ Import Statement</summary>
|
||||
<div style="margin-top:0.75rem;background:var(--cream);border:1px solid var(--border);border-radius:var(--radius);padding:1rem;">
|
||||
<form method="POST" action="{{ url_for('import_transactions') }}" enctype="multipart/form-data">
|
||||
<input type="hidden" name="redirect_to" value="weekly">
|
||||
<div style="display:flex;gap:0.75rem;flex-wrap:wrap;align-items:flex-end;">
|
||||
<div>
|
||||
<label style="font-size:0.8rem;display:block;margin-bottom:0.2rem;">Account Name</label>
|
||||
<input type="text" name="account" placeholder="e.g. WestConsin Checking" class="budget-input" style="width:200px;" required>
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:0.8rem;display:block;margin-bottom:0.2rem;">Statement File (PDF or CSV)</label>
|
||||
<input type="file" name="statement" accept=".pdf,.csv" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" style="font-size:0.85rem;">Import</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{% if w.transactions %}
|
||||
{% set txns_by_cat = {} %}
|
||||
{% for t in w.transactions %}
|
||||
{% set cat = t.category or 'Uncategorized' %}
|
||||
{% if cat not in txns_by_cat %}{% set _ = txns_by_cat.update({cat: []}) %}{% endif %}
|
||||
{% set _ = txns_by_cat[cat].append(t) %}
|
||||
{% endfor %}
|
||||
<table class="budget-table">
|
||||
<thead><tr><th>Date</th><th>Description</th><th>Category</th><th>Amount</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{% for t in w.transactions | sort(attribute='date', reverse=True) %}
|
||||
<tr>
|
||||
<td class="muted" style="white-space:nowrap;font-size:0.82rem;">{{ t.date }}</td>
|
||||
<td style="font-size:0.85rem;">{{ t.description }}</td>
|
||||
<td>
|
||||
{% if t.category %}
|
||||
<span class="tag" style="background:{{ colors.get(t.category,'#ddd') }};font-size:0.75rem;">{{ t.category }}{% if t.subcategory %} › {{ t.subcategory }}{% endif %}</span>
|
||||
{% else %}
|
||||
<span class="muted" style="font-size:0.78rem;">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="amount" style="font-size:0.85rem;">${{ t.amount | money }}</td>
|
||||
<td><a href="{{ url_for('edit_transaction', txn_id=t.id) }}" class="edit-link" style="font-size:0.78rem;">Edit</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="total-row">
|
||||
<td colspan="3">Total</td>
|
||||
<td class="amount">${{ (w.transactions | map(attribute='amount') | map('float') | sum) | money }}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="muted" style="padding:0.5rem 0;">No reviewed transactions for this week yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Leftover -->
|
||||
<div class="leftover-bar {% if w.leftover < 0 %}leftover-negative{% endif %}" id="leftover-bar-{{ w.num }}">
|
||||
<div>
|
||||
<div style="font-weight:600;">Week {{ w.num }} — What's Left</div>
|
||||
<div style="font-size:0.78rem;color:var(--text-light);margin-top:0.2rem;line-height:1.7;">
|
||||
$<span id="lo-start-{{ w.num }}">{{ w.starting_balance | money }}</span> starting
|
||||
+ $<span id="lo-income-{{ w.num }}">{{ w.income_total | money }}</span> income
|
||||
− $<span id="lo-spending-{{ w.num }}">{{ w.spending_total | money }}</span> spending
|
||||
− $<span id="lo-sinking-{{ w.num }}">{{ w.sinking_total | money }}</span> savings
|
||||
− $<span id="lo-debt-{{ w.num }}">{{ w.extra_debt_total | money }}</span> extra debt
|
||||
</div>
|
||||
</div>
|
||||
<span class="leftover-amount" id="leftover-amount-{{ w.num }}"
|
||||
data-spending="{{ w.spending_total }}"
|
||||
data-sinking="{{ w.sinking_total }}"
|
||||
data-debt="{{ w.extra_debt_total }}"
|
||||
data-start="{{ w.starting_balance }}"
|
||||
>${{ w.leftover | money }}</span>
|
||||
</div>
|
||||
|
||||
</div><!-- end week-panel -->
|
||||
{% endfor %}
|
||||
|
||||
<script>
|
||||
function showWeek(num) {
|
||||
document.querySelectorAll('.week-panel').forEach(p => p.classList.remove('active'));
|
||||
document.querySelectorAll('.week-tab').forEach(t => t.classList.remove('active'));
|
||||
document.getElementById('week-' + num).classList.add('active');
|
||||
document.getElementById('tab-' + num).classList.add('active');
|
||||
}
|
||||
|
||||
function saveField(year, month, week, field, key, subkey, value) {
|
||||
fetch('/weekly/save', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: new URLSearchParams({year, month, week, field, key, subkey, value})
|
||||
}).then(() => updateLeftover(week));
|
||||
}
|
||||
|
||||
function logIncome(year, month, week, source, value) {
|
||||
fetch('/weekly/save', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: new URLSearchParams({year, month, week, field: 'income', key: source, subkey: 'actual', value})
|
||||
}).then(r => r.json()).then(data => {
|
||||
updateLeftover(week);
|
||||
// show ✓ badge
|
||||
const id = 'income-actual-' + week + '-' + source.replace(/ /g,'_');
|
||||
const input = document.getElementById(id);
|
||||
if (input && parseFloat(value) > 0) {
|
||||
let badge = input.parentElement.querySelector('.income-logged-badge');
|
||||
if (!badge) {
|
||||
badge = document.createElement('span');
|
||||
badge.className = 'income-logged-badge';
|
||||
badge.textContent = '✓ logged';
|
||||
input.parentElement.appendChild(badge);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateLeftover(week) {
|
||||
// Recalculate income total from inputs
|
||||
let incomeTotal = 0;
|
||||
document.querySelectorAll(`#week-${week} .income-actual-input`).forEach(el => {
|
||||
incomeTotal += parseFloat(el.value) || 0;
|
||||
});
|
||||
const totalEl = document.getElementById(`w${week}-income-actual-total`);
|
||||
if (totalEl) totalEl.textContent = incomeTotal.toFixed(2);
|
||||
|
||||
const loIncomeEl = document.getElementById(`lo-income-${week}`);
|
||||
if (loIncomeEl) loIncomeEl.textContent = incomeTotal.toFixed(2);
|
||||
|
||||
// Update leftover bar using all deductions from data attributes
|
||||
const leftoverEl = document.getElementById(`leftover-amount-${week}`);
|
||||
if (leftoverEl) {
|
||||
const startBal = parseFloat(document.getElementById(`starting-balance-${week}`)?.value) || 0;
|
||||
const spending = parseFloat(leftoverEl.dataset.spending) || 0;
|
||||
const sinking = parseFloat(leftoverEl.dataset.sinking) || 0;
|
||||
const debt = parseFloat(leftoverEl.dataset.debt) || 0;
|
||||
const leftover = startBal + incomeTotal - spending - sinking - debt;
|
||||
leftoverEl.textContent = '$' + leftover.toFixed(2);
|
||||
const bar = document.getElementById(`leftover-bar-${week}`);
|
||||
if (bar) bar.classList.toggle('leftover-negative', leftover < 0);
|
||||
}
|
||||
}
|
||||
|
||||
function logExtraDebt(debtId, value, year, month, week) {
|
||||
fetch('/weekly/extra-debt', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: new URLSearchParams({debt_id: debtId, amount: value, year, month, week})
|
||||
});
|
||||
}
|
||||
|
||||
function saveStartingBalance(year, month, week, value) {
|
||||
fetch('/weekly/save', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: new URLSearchParams({year, month, week, field: 'starting_balance', key: 'amount', subkey: 'value', value})
|
||||
}).then(() => {
|
||||
const loStart = document.getElementById(`lo-start-${week}`);
|
||||
if (loStart) loStart.textContent = parseFloat(value || 0).toFixed(2);
|
||||
updateLeftover(week);
|
||||
});
|
||||
}
|
||||
|
||||
function contributeSinking(fundName, value, year, month, week) {
|
||||
fetch('/sinking/contribute-weekly', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: new URLSearchParams({name: fundName, amount: value, year, month, week})
|
||||
}).then(r => r.json()).then(data => {
|
||||
// update the balance cell
|
||||
document.querySelectorAll('.sinking-weekly-balance-' + fundName.replace(/ /g,'_'))
|
||||
.forEach(el => el.textContent = '$' + (data.new_balance || 0).toFixed(2));
|
||||
});
|
||||
}
|
||||
|
||||
function toggleWeeklySection(header) {
|
||||
const body = header.nextElementSibling;
|
||||
const arrow = header.querySelector('.weekly-arrow');
|
||||
const collapsed = body.style.display === 'none';
|
||||
body.style.display = collapsed ? '' : 'none';
|
||||
arrow.textContent = collapsed ? '▾' : '▸';
|
||||
}
|
||||
|
||||
// Show current week by default
|
||||
showWeek({{ current_week }});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user