Initial commit — Moon Household Budget
This commit is contained in:
774
templates/debts.html
Normal file
774
templates/debts.html
Normal file
@@ -0,0 +1,774 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>Debt Tracker — {{ month_name }} {{ year }}</h1>
|
||||
<div class="month-nav">
|
||||
<a href="{{ url_for('debts_view', year=prev_year, month=prev_month) }}" class="btn btn-ghost">‹ Prev</a>
|
||||
<a href="{{ url_for('debts_view', year=next_year, month=next_month) }}" class="btn btn-ghost">Next ›</a>
|
||||
<a href="{{ url_for('debt_strategy') }}" class="btn" style="background:#C47A4A;color:#fff;border:none;">📊 Debt Payoff Strategy</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Forms -->
|
||||
<div style="display:flex;gap:0.75rem;margin-bottom:1.5rem;flex-wrap:wrap;">
|
||||
|
||||
<details class="add-debt-details">
|
||||
<summary class="btn btn-primary" style="display:inline-block;cursor:pointer;">+ Add Credit Card</summary>
|
||||
<div class="card-wide" style="margin-top:0.75rem;">
|
||||
<form method="POST" action="{{ url_for('debt_add') }}" class="bill-form">
|
||||
<input type="hidden" name="debt_category" value="Credit Cards">
|
||||
<div class="bill-form-grid">
|
||||
<div class="form-group">
|
||||
<label>Card Name</label>
|
||||
<input type="text" name="name" placeholder="e.g. Discover, Chase, Indigo" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Owner</label>
|
||||
<select name="owner">
|
||||
<option value="Bonna">Bonna</option>
|
||||
<option value="Tony">Tony</option>
|
||||
<option value="Both">Both</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Current Balance</label>
|
||||
<input type="number" name="current_balance" step="0.01" min="0" placeholder="0.00" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>APR % <span class="optional">(leave blank if unknown)</span></label>
|
||||
<input type="number" name="apr" step="0.01" min="0" max="100" placeholder="e.g. 24.99">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Minimum Payment</label>
|
||||
<input type="number" name="minimum" step="0.01" min="0" placeholder="0.00">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Account Name in App <span class="optional">(to match transactions)</span></label>
|
||||
<input type="text" name="account_match" placeholder="e.g. Discover">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Notes <span class="optional">(optional)</span></label>
|
||||
<input type="text" name="notes" placeholder="e.g. balance transfer, 0% promo ends Aug">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Add Debt</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="add-debt-details">
|
||||
<summary class="btn btn-dusty-rose" style="display:inline-block;cursor:pointer;">+ Add Medical Debt</summary>
|
||||
<div class="card-wide" style="margin-top:0.75rem;">
|
||||
<form method="POST" action="{{ url_for('debt_add') }}" class="bill-form">
|
||||
<input type="hidden" name="debt_category" value="Medical Payment Plans">
|
||||
<div class="bill-form-grid">
|
||||
<div class="form-group">
|
||||
<label>Provider / Institution</label>
|
||||
<input type="text" name="name" placeholder="e.g. Marshfield Clinic, Oakleaf" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Owner</label>
|
||||
<select name="owner">
|
||||
<option value="Bonna">Bonna</option>
|
||||
<option value="Tony">Tony</option>
|
||||
<option value="Both">Both</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Current Balance</label>
|
||||
<input type="number" name="current_balance" step="0.01" min="0" placeholder="0.00" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>APR % <span class="optional">(0% if interest-free plan)</span></label>
|
||||
<input type="number" name="apr" step="0.01" min="0" max="100" placeholder="0.00">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Monthly Payment</label>
|
||||
<input type="number" name="minimum" step="0.01" min="0" placeholder="0.00">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Account Name in App <span class="optional">(optional)</span></label>
|
||||
<input type="text" name="account_match" placeholder="e.g. Marshfield">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Notes <span class="optional">(optional)</span></label>
|
||||
<input type="text" name="notes" placeholder="e.g. interest-free, started Jan 2026">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Add Medical Debt</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="add-debt-details">
|
||||
<summary class="btn btn-powder-blue" style="display:inline-block;cursor:pointer;">+ Add Affirm / Klarna / BNPL Plan</summary>
|
||||
<div class="card-wide" style="margin-top:0.75rem;">
|
||||
<form method="POST" action="{{ url_for('bnpl_add') }}" class="bill-form">
|
||||
<div class="bill-form-grid">
|
||||
<div class="form-group">
|
||||
<label>Provider</label>
|
||||
<select name="bnpl_provider">
|
||||
<option>Affirm</option>
|
||||
<option>Klarna</option>
|
||||
<option>Afterpay</option>
|
||||
<option>Zip</option>
|
||||
<option>Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>What was it for?</label>
|
||||
<input type="text" name="description" placeholder="e.g. Living room couch" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Merchant / Store</label>
|
||||
<input type="text" name="merchant" placeholder="e.g. Wayfair, Amazon">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Owner</label>
|
||||
<select name="owner">
|
||||
<option value="Bonna">Bonna</option>
|
||||
<option value="Tony">Tony</option>
|
||||
<option value="Both">Both</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Original Purchase Amount</label>
|
||||
<input type="number" name="original_amount" step="0.01" min="0" placeholder="0.00">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Payment Amount</label>
|
||||
<input type="number" name="payment_amount" step="0.01" min="0" placeholder="0.00" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Total Number of Payments</label>
|
||||
<input type="number" name="total_payments" min="1" placeholder="e.g. 4 or 6 or 12" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Payments Already Made</label>
|
||||
<input type="number" name="payments_made" min="0" value="0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Payment Frequency</label>
|
||||
<select name="frequency">
|
||||
<option value="biweekly">Every 2 weeks</option>
|
||||
<option value="monthly">Monthly</option>
|
||||
<option value="4-payment">4 payments (every 2 weeks)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Next Payment Date</label>
|
||||
<input type="date" name="next_payment_date" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Notes <span class="optional">(optional)</span></label>
|
||||
<input type="text" name="notes" placeholder="e.g. for bedroom, holiday gift">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Add BNPL Plan</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="add-debt-details">
|
||||
<summary class="btn btn-moss" style="display:inline-block;cursor:pointer;">📋 Add Card by Pasting Statement</summary>
|
||||
<div class="card-wide" style="margin-top:0.75rem;">
|
||||
<p class="muted" style="font-size:0.85rem;margin-bottom:1rem;">Fill in the card name and owner, paste your statement text, and we'll create the card and fill in all the numbers at once.</p>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-bottom:1rem;">
|
||||
<div class="form-group">
|
||||
<label>Card Name</label>
|
||||
<input type="text" id="paste-card-name" placeholder="e.g. Self Visa, Discover" class="budget-input" style="width:100%;">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Owner</label>
|
||||
<select id="paste-card-owner" class="budget-input" style="width:100%;">
|
||||
<option value="Bonna">Bonna</option>
|
||||
<option value="Tony">Tony</option>
|
||||
<option value="Both">Both</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Paste Statement Text</label>
|
||||
<textarea id="paste-new-statement" style="width:100%;height:160px;border:1px solid var(--border);border-radius:8px;padding:0.75rem;font-size:0.82rem;resize:vertical;" placeholder="Copy all the text from your statement and paste it here…"></textarea>
|
||||
</div>
|
||||
<div id="paste-new-result" style="font-size:0.85rem;margin-bottom:0.75rem;"></div>
|
||||
<button class="btn btn-primary" onclick="addCardFromStatement()">Add Card & Import Statement</button>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<!-- Debt Summary Breakdown -->
|
||||
<div class="debt-summary-block">
|
||||
<table class="debt-summary-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Category</th>
|
||||
<th>Balance</th>
|
||||
<th>Minimums</th>
|
||||
<th>Paid This Month</th>
|
||||
<th>Extra</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for cat in debt_categories %}
|
||||
{% set ct = totals[cat] %}
|
||||
{% if by_category[cat] %}
|
||||
<tr>
|
||||
<td>{{ cat }}</td>
|
||||
<td class="amount">${{ ct.balance | money }}</td>
|
||||
<td class="amount">${{ ct.minimum | money }}</td>
|
||||
<td class="amount">${{ ct.paid | money }}</td>
|
||||
<td class="amount accent">${{ ct.extra | money }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="total-row">
|
||||
<td><strong>Grand Total</strong></td>
|
||||
<td class="amount"><strong>${{ grand.balance | money }}</strong></td>
|
||||
<td class="amount"><strong>${{ grand.minimum | money }}</strong></td>
|
||||
<td class="amount"><strong>${{ grand.paid | money }}</strong></td>
|
||||
<td class="amount accent"><strong>${{ grand.extra | money }}</strong></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Debt-to-Income Ratio -->
|
||||
{% if dti is not none %}
|
||||
{% if dti <= 20 %}
|
||||
{% set dti_color = "#5A9E68" %}{% set dti_label = "Healthy" %}{% set dti_tip = "Under 20% is excellent." %}
|
||||
{% elif dti <= 36 %}
|
||||
{% set dti_color = "#D4A96A" %}{% set dti_label = "Manageable" %}{% set dti_tip = "20–36% is average. Keep an eye on it." %}
|
||||
{% elif dti <= 50 %}
|
||||
{% set dti_color = "#E8956A" %}{% set dti_label = "High" %}{% set dti_tip = "36–50% is high. Prioritize paying down debt." %}
|
||||
{% else %}
|
||||
{% set dti_color = "#C57A7A" %}{% set dti_label = "Critical" %}{% set dti_tip = "Over 50% is a red flag. Focus on reducing debt ASAP." %}
|
||||
{% endif %}
|
||||
<div style="display:flex;align-items:center;gap:1.5rem;background:#fff;border:1px solid var(--border);border-radius:var(--radius);padding:1rem 1.25rem;margin-bottom:1rem;flex-wrap:wrap;">
|
||||
<div>
|
||||
<div style="font-size:0.75rem;text-transform:uppercase;letter-spacing:0.06em;color:var(--text-light);margin-bottom:0.2rem;">Debt-to-Income Ratio</div>
|
||||
<div style="font-size:1.8rem;font-weight:700;color:{{ dti_color }};">{{ dti }}%</div>
|
||||
</div>
|
||||
<div style="flex:1;min-width:180px;">
|
||||
<div style="display:flex;justify-content:space-between;font-size:0.78rem;margin-bottom:0.3rem;">
|
||||
<span style="color:{{ dti_color }};font-weight:600;">{{ dti_label }}</span>
|
||||
<span class="muted">${{ "%.0f" | format(grand.minimum) }} min / ${{ "%.0f" | format(monthly_income) }} income</span>
|
||||
</div>
|
||||
<div style="background:var(--border);border-radius:99px;height:8px;overflow:hidden;">
|
||||
<div style="height:100%;width:{{ [dti, 100] | min }}%;background:{{ dti_color }};border-radius:99px;transition:width 0.3s;"></div>
|
||||
</div>
|
||||
<div style="font-size:0.75rem;color:var(--text-light);margin-top:0.3rem;">{{ dti_tip }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div style="background:#fff;border:1px solid var(--border);border-radius:var(--radius);padding:0.75rem 1.25rem;margin-bottom:1rem;font-size:0.85rem;color:var(--text-light);">
|
||||
DTI ratio unavailable — <a href="{{ url_for('income_view') }}">log this month's income</a> to see your debt-to-income ratio.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Search -->
|
||||
<div style="margin-bottom:1rem;">
|
||||
<input type="text" id="debt-search" placeholder="Search debts…" class="budget-input" style="width:240px;" oninput="filterDebts(this.value)">
|
||||
</div>
|
||||
|
||||
<!-- By Category -->
|
||||
{% for cat in debt_categories %}
|
||||
{% set debts = by_category[cat] %}
|
||||
{% set cat_totals = totals[cat] %}
|
||||
{% if debts %}
|
||||
<section class="section">
|
||||
<div class="debt-cat-header" onclick="toggleDebtSection('debt-section-{{ loop.index }}')" style="cursor:pointer;user-select:none;">
|
||||
<div style="display:flex;align-items:center;gap:0.5rem;">
|
||||
<span class="debt-collapse-arrow" id="arrow-debt-section-{{ loop.index }}">▾</span>
|
||||
<h2 style="margin:0;">{{ cat }}</h2>
|
||||
</div>
|
||||
<div class="debt-cat-totals">
|
||||
<span>Balance: <strong>${{ cat_totals.balance | money }}</strong></span>
|
||||
<span>Minimums: <strong>${{ cat_totals.minimum | money }}</strong></span>
|
||||
<span>Paid: <strong>${{ cat_totals.paid | money }}</strong></span>
|
||||
<span class="accent">Extra: <strong>${{ cat_totals.extra | money }}</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="debt-cards" id="debt-section-{{ loop.index }}">
|
||||
{% for d in debts %}
|
||||
<div class="debt-card">
|
||||
<div class="debt-card-top">
|
||||
<div>
|
||||
<div class="debt-card-name">{{ d.name }}</div>
|
||||
{% if d.notes %}<div class="debt-card-notes">{{ d.notes }}</div>{% endif %}
|
||||
</div>
|
||||
<div class="debt-card-badges">
|
||||
<span class="owner-tag" style="background: {{ owner_colors.get(d.owner, '#ddd') }};">{{ d.owner }}</span>
|
||||
{% if d.apr %}<span class="apr-tag">{{ d.apr }}% APR</span>{% endif %}
|
||||
{% if d.get('is_bnpl') %}
|
||||
<button class="edit-link" style="background:none;border:none;cursor:pointer;font-size:0.78rem;"
|
||||
onclick="openEditBNPL('{{ d.id }}','{{ d.name }}','{{ d.merchant or '' }}','{{ d.bnpl_provider or 'Affirm' }}','{{ d.owner }}','{{ d.original_amount or 0 }}','{{ d.payment_amount or 0 }}','{{ d.total_payments or 0 }}','{{ d.payments_made or 0 }}','{{ d.frequency or 'biweekly' }}','{{ d.next_payment_date or '' }}','{{ d.notes or '' }}')">
|
||||
✏️ Edit
|
||||
</button>
|
||||
{% else %}
|
||||
<button class="edit-link" style="background:none;border:none;cursor:pointer;font-size:0.78rem;"
|
||||
onclick="openEditDebt('{{ d.id }}','{{ d.name }}','{{ d.owner }}','{{ d.apr or 0 }}','{{ d.minimum or 0 }}','{{ d.current_balance or 0 }}','{{ d.credit_limit or 0 }}','{{ d.notes or '' }}')">
|
||||
✏️ Edit
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="debt-card-balance">
|
||||
<span class="debt-balance-label">Balance</span>
|
||||
<span class="debt-balance-value" id="balance-{{ d.id }}">${{ d.ending | money }}</span>
|
||||
</div>
|
||||
{% if d.credit_limit %}
|
||||
{% set available = d.credit_limit - d.ending %}
|
||||
{% set util_pct = [(d.ending / d.credit_limit * 100), 100] | min | int %}
|
||||
<div style="margin-bottom:0.75rem;">
|
||||
<div style="display:flex;justify-content:space-between;font-size:0.8rem;margin-bottom:0.3rem;">
|
||||
<span style="color:var(--text-light);">Available credit</span>
|
||||
<span style="font-weight:600;color:{% if available < 0 %}#c07070{% elif util_pct > 80 %}#c07070{% elif util_pct > 50 %}#b8943f{% else %}#5A9E68{% endif %};">
|
||||
${{ available | money }} of ${{ d.credit_limit | money }}
|
||||
</span>
|
||||
</div>
|
||||
<div style="background:#EDE8E2;border-radius:4px;height:6px;">
|
||||
<div style="background:{% if util_pct > 80 %}#c07070{% elif util_pct > 50 %}#d4a843{% else %}#5A9E68{% endif %};border-radius:4px;height:6px;width:{{ util_pct }}%;"></div>
|
||||
</div>
|
||||
<div style="font-size:0.73rem;color:var(--text-light);margin-top:0.2rem;">{{ util_pct }}% utilized</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if d.payoff_str and not d.get('is_bnpl') %}
|
||||
<div style="margin-bottom:0.75rem;">
|
||||
<div style="display:flex;justify-content:space-between;font-size:0.8rem;margin-bottom:0.3rem;">
|
||||
<span style="color:var(--text-light);">Payoff projection</span>
|
||||
<span style="font-weight:600;color:#5A9E68;">{{ d.payoff_str }}</span>
|
||||
</div>
|
||||
{% if d.months_left and d.months_left <= 36 %}
|
||||
{% set payoff_pct = [100 - (d.months_left / 36 * 100), 5] | max | int %}
|
||||
<div style="background:#EDE8E2;border-radius:4px;height:6px;">
|
||||
<div style="background:#5A9E68;border-radius:4px;height:6px;width:{{ payoff_pct }}%;"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div style="font-size:0.73rem;color:var(--text-light);margin-top:0.2rem;">
|
||||
{{ d.months_left }} month{{ 's' if d.months_left != 1 }} at minimum payments
|
||||
{% if d.apr %} · {{ d.apr }}% APR{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Statement pending toggle -->
|
||||
{% if not d.get('is_bnpl') %}
|
||||
<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.75rem;">
|
||||
<button class="toggle-btn {% if d.get('statement_pending') %}toggle-on{% endif %}"
|
||||
onclick="togglePending('{{ d.id }}', this)"
|
||||
style="font-size:0.75rem;padding:0.2rem 0.6rem;">
|
||||
{{ '⏳ Statement Pending' if d.get('statement_pending') else 'Statement closed' }}
|
||||
</button>
|
||||
{% if d.get('statement_pending') %}
|
||||
<span style="font-size:0.75rem;color:var(--text-light);">numbers may not reflect current statement</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if d.get('is_bnpl') %}
|
||||
{% set pct = ((d.payments_made / d.total_payments) * 100) | int if d.total_payments else 0 %}
|
||||
{% set remaining_payments = d.total_payments - d.payments_made %}
|
||||
{% set payoff_str, _ = bnpl_payoff_date(d.payments_made, d.total_payments, d.frequency_days) %}
|
||||
<div class="bnpl-progress-wrap">
|
||||
<div class="bnpl-progress-bar">
|
||||
<div class="bnpl-progress-fill" style="width: {{ pct }}%;"></div>
|
||||
</div>
|
||||
<div class="bnpl-progress-label">{{ d.payments_made }} of {{ d.total_payments }} payments made</div>
|
||||
</div>
|
||||
<table class="debt-month-table">
|
||||
<tr><td>Payment Amount</td><td class="amount">${{ d.payment_amount | money }}</td></tr>
|
||||
<tr><td>Payments Remaining</td><td class="amount">{{ remaining_payments }}</td></tr>
|
||||
<tr><td>Still Owed</td><td class="amount">${{ d.current_balance | money }}</td></tr>
|
||||
{% if payoff_str %}<tr><td>Payoff Date</td><td class="amount accent">{{ payoff_str }}</td></tr>{% endif %}
|
||||
</table>
|
||||
<form method="POST" action="{{ url_for('bnpl_payment', debt_id=d.id) }}" style="margin-bottom:0.5rem;">
|
||||
<button type="submit" class="btn btn-accept" onclick="return confirm('Mark one payment as made?')">
|
||||
✓ Mark Payment Made
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<table class="debt-month-table">
|
||||
<tr>
|
||||
<td>Starting Balance</td>
|
||||
<td><input type="number" step="0.01" class="budget-input"
|
||||
value="{{ d.starting }}"
|
||||
onchange="logDebt('{{ d.id }}', {{ year }}, {{ month }}, 'starting_balance', this.value, '{{ d.id }}')"
|
||||
placeholder="0.00"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Interest Charged</td>
|
||||
<td><input type="number" step="0.01" class="budget-input"
|
||||
value="{{ d.interest }}"
|
||||
onchange="logDebt('{{ d.id }}', {{ year }}, {{ month }}, 'interest', this.value, '{{ d.id }}')"
|
||||
placeholder="auto-calculated"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>New Charges</td>
|
||||
<td><input type="number" step="0.01" class="budget-input"
|
||||
value="{{ d.new_charges or '' }}"
|
||||
onchange="logDebt('{{ d.id }}', {{ year }}, {{ month }}, 'new_charges', this.value, '{{ d.id }}')"
|
||||
placeholder="0.00"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Minimum Due</td>
|
||||
<td class="amount">${{ d.minimum | money }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Payment Made</td>
|
||||
<td><input type="number" step="0.01" class="budget-input"
|
||||
value="{{ d.payment or '' }}"
|
||||
onchange="logDebt('{{ d.id }}', {{ year }}, {{ month }}, 'payment', this.value, '{{ d.id }}')"
|
||||
placeholder="0.00"></td>
|
||||
</tr>
|
||||
<tr class="debt-extra-row">
|
||||
{% set underpaid = d.payment > 0 and d.payment < d.minimum and not d.get('statement_pending') %}
|
||||
<td>{% if underpaid %}⚠️ Underpaid Minimum{% else %}Extra Paid Above Minimum{% endif %}</td>
|
||||
<td class="amount {% if underpaid %}{% else %}accent{% endif %}" id="extra-{{ d.id }}"
|
||||
{% if underpaid %}style="color:#c07070;"{% endif %}>
|
||||
{% if underpaid %}
|
||||
-${{ d.minimum - d.payment | money }} short
|
||||
{% elif d.get('statement_pending') %}
|
||||
<span class="muted">—</span>
|
||||
{% else %}
|
||||
${{ d.extra | money }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
{% if not d.get('is_bnpl') %}
|
||||
<button class="btn btn-ghost" style="font-size:0.78rem;margin-bottom:0.5rem;width:100%;"
|
||||
onclick="openStatementModal('{{ d.id }}', '{{ d.name }}')">📋 Paste Statement</button>
|
||||
{% endif %}
|
||||
|
||||
<form method="POST" action="{{ url_for('debt_delete', debt_id=d.id) }}"
|
||||
onsubmit="return confirm('Remove {{ d.name }} from debt tracker?');">
|
||||
<button type="submit" class="debt-delete-btn">Remove</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if not (by_category.values() | map('list') | sum(start=[])) %}
|
||||
<div class="empty-state">
|
||||
<p>No debts added yet. Click "+ Add a Debt" above to get started.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
function toggleDebtSection(id) {
|
||||
const el = document.getElementById(id);
|
||||
const arrow = document.getElementById('arrow-' + id);
|
||||
if (!el) return;
|
||||
const collapsed = el.style.display === 'none';
|
||||
el.style.display = collapsed ? '' : 'none';
|
||||
if (arrow) arrow.textContent = collapsed ? '▾' : '▸';
|
||||
}
|
||||
|
||||
function togglePending(debtId, btn) {
|
||||
fetch(`/debts/toggle-pending/${debtId}`, {method: 'POST'})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
btn.textContent = data.pending ? '⏳ Statement Pending' : 'Statement closed';
|
||||
btn.classList.toggle('toggle-on', data.pending);
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
|
||||
function filterDebts(q) {
|
||||
q = q.toLowerCase();
|
||||
document.querySelectorAll('.debt-card').forEach(card => {
|
||||
card.style.display = card.textContent.toLowerCase().includes(q) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
function logDebt(debtId, year, month, field, value, cardId) {
|
||||
const body = new URLSearchParams({debt_id: debtId, year, month, field, value});
|
||||
fetch('/debts/log', {method: 'POST', body})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.ending !== undefined) {
|
||||
const bal = document.getElementById('balance-' + cardId);
|
||||
if (bal) bal.textContent = '$' + data.ending.toFixed(2);
|
||||
}
|
||||
if (data.extra !== undefined) {
|
||||
const ext = document.getElementById('extra-' + cardId);
|
||||
if (ext) ext.textContent = '$' + data.extra.toFixed(2);
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Edit Debt Modal -->
|
||||
<div id="edit-debt-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.3);z-index:200;align-items:center;justify-content:center;">
|
||||
<div style="background:#fff;border-radius:12px;padding:1.5rem;width:90%;max-width:480px;box-shadow:0 8px 32px rgba(0,0,0,0.18);">
|
||||
<h3 style="margin:0 0 1rem;" id="edit-debt-title">Edit Debt</h3>
|
||||
<form method="POST" id="edit-debt-form" action="">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0.75rem;">
|
||||
<div class="form-group">
|
||||
<label style="font-size:0.85rem;">Card / Debt Name</label>
|
||||
<input type="text" name="name" id="edit-name" class="budget-input" style="width:100%;" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label style="font-size:0.85rem;">Owner</label>
|
||||
<select name="owner" id="edit-owner" class="budget-input" style="width:100%;">
|
||||
<option value="Bonna">Bonna</option>
|
||||
<option value="Tony">Tony</option>
|
||||
<option value="Both">Both</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label style="font-size:0.85rem;">Current Balance</label>
|
||||
<input type="number" name="current_balance" id="edit-balance" step="0.01" min="0" class="budget-input" style="width:100%;">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label style="font-size:0.85rem;">APR %</label>
|
||||
<input type="number" name="apr" id="edit-apr" step="0.01" min="0" class="budget-input" style="width:100%;">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label style="font-size:0.85rem;">Minimum Payment</label>
|
||||
<input type="number" name="minimum" id="edit-minimum" step="0.01" min="0" class="budget-input" style="width:100%;">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label style="font-size:0.85rem;">Credit Limit</label>
|
||||
<input type="number" name="credit_limit" id="edit-credit-limit" step="0.01" min="0" class="budget-input" style="width:100%;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" style="margin-top:0.5rem;">
|
||||
<label style="font-size:0.85rem;">Notes</label>
|
||||
<input type="text" name="notes" id="edit-notes" class="budget-input" style="width:100%;" placeholder="optional">
|
||||
</div>
|
||||
<div style="display:flex;gap:0.75rem;justify-content:flex-end;margin-top:1rem;">
|
||||
<button type="button" class="btn btn-ghost" onclick="closeEditDebt()">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit BNPL Modal -->
|
||||
<div id="edit-bnpl-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.3);z-index:200;align-items:center;justify-content:center;">
|
||||
<div style="background:#fff;border-radius:12px;padding:1.5rem;width:90%;max-width:520px;box-shadow:0 8px 32px rgba(0,0,0,0.18);max-height:90vh;overflow-y:auto;">
|
||||
<h3 style="margin:0 0 1rem;" id="edit-bnpl-title">Edit BNPL Plan</h3>
|
||||
<form method="POST" id="edit-bnpl-form" action="">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0.75rem;">
|
||||
<div class="form-group">
|
||||
<label style="font-size:0.85rem;">What is it for?</label>
|
||||
<input type="text" name="description" id="ebnpl-description" class="budget-input" style="width:100%;" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label style="font-size:0.85rem;">Merchant / Store</label>
|
||||
<input type="text" name="merchant" id="ebnpl-merchant" class="budget-input" style="width:100%;">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label style="font-size:0.85rem;">Provider</label>
|
||||
<select name="bnpl_provider" id="ebnpl-provider" class="budget-input" style="width:100%;">
|
||||
<option>Affirm</option>
|
||||
<option>Klarna</option>
|
||||
<option>Afterpay</option>
|
||||
<option>Zip</option>
|
||||
<option>Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label style="font-size:0.85rem;">Owner</label>
|
||||
<select name="owner" id="ebnpl-owner" class="budget-input" style="width:100%;">
|
||||
<option value="Bonna">Bonna</option>
|
||||
<option value="Tony">Tony</option>
|
||||
<option value="Both">Both</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label style="font-size:0.85rem;">Original Purchase Amount</label>
|
||||
<input type="number" step="0.01" name="original_amount" id="ebnpl-original" class="budget-input" style="width:100%;" placeholder="0.00">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label style="font-size:0.85rem;">Payment Amount</label>
|
||||
<input type="number" step="0.01" name="payment_amount" id="ebnpl-payment" class="budget-input" style="width:100%;" placeholder="0.00">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label style="font-size:0.85rem;">Total Payments</label>
|
||||
<input type="number" min="1" name="total_payments" id="ebnpl-total" class="budget-input" style="width:100%;">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label style="font-size:0.85rem;">Payments Already Made</label>
|
||||
<input type="number" min="0" name="payments_made" id="ebnpl-made" class="budget-input" style="width:100%;">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label style="font-size:0.85rem;">Frequency</label>
|
||||
<select name="frequency" id="ebnpl-frequency" class="budget-input" style="width:100%;">
|
||||
<option value="biweekly">Every 2 weeks</option>
|
||||
<option value="monthly">Monthly</option>
|
||||
<option value="4-payment">4 payments (every 2 weeks)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label style="font-size:0.85rem;">Next Payment Date</label>
|
||||
<input type="date" name="next_payment_date" id="ebnpl-next-date" class="budget-input" style="width:100%;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" style="margin-top:0.5rem;">
|
||||
<label style="font-size:0.85rem;">Notes</label>
|
||||
<input type="text" name="notes" id="ebnpl-notes" class="budget-input" style="width:100%;" placeholder="optional">
|
||||
</div>
|
||||
<p class="muted" style="font-size:0.78rem;margin-top:0.75rem;">Saving will delete any unpaid calendar entries for this plan and regenerate them based on the updated dates.</p>
|
||||
<div style="display:flex;gap:0.75rem;justify-content:flex-end;margin-top:1rem;">
|
||||
<button type="button" class="btn btn-ghost" onclick="closeEditBNPL()">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Paste Statement Modal -->
|
||||
<div id="statement-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.3);z-index:200;align-items:center;justify-content:center;">
|
||||
<div style="background:#fff;border-radius:12px;padding:1.5rem;width:90%;max-width:540px;box-shadow:0 8px 32px rgba(0,0,0,0.18);">
|
||||
<h3 style="margin:0 0 0.25rem;" id="statement-modal-title">Paste Statement</h3>
|
||||
<p class="muted" style="font-size:0.82rem;margin-bottom:1rem;">Copy all the text from your statement and paste it below. The app will extract the numbers automatically.</p>
|
||||
<textarea id="statement-text" style="width:100%;height:180px;border:1px solid var(--border);border-radius:8px;padding:0.75rem;font-size:0.82rem;resize:vertical;" placeholder="Paste statement text here…"></textarea>
|
||||
<div id="statement-result" style="margin:0.75rem 0;font-size:0.85rem;"></div>
|
||||
<div style="display:flex;gap:0.75rem;justify-content:flex-end;margin-top:0.75rem;">
|
||||
<button class="btn btn-ghost" onclick="closeStatementModal()">Cancel</button>
|
||||
<button class="btn btn-primary" onclick="parseStatement()">Import</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let activeDebtId = null;
|
||||
|
||||
function addCardFromStatement() {
|
||||
const name = document.getElementById('paste-card-name').value.trim();
|
||||
const owner = document.getElementById('paste-card-owner').value;
|
||||
const text = document.getElementById('paste-new-statement').value.trim();
|
||||
const resultEl = document.getElementById('paste-new-result');
|
||||
|
||||
if (!name) { resultEl.style.color='#c07070'; resultEl.textContent='Please enter a card name.'; return; }
|
||||
if (!text) { resultEl.style.color='#c07070'; resultEl.textContent='Please paste your statement text.'; return; }
|
||||
|
||||
resultEl.style.color='inherit';
|
||||
resultEl.textContent='Creating card and importing…';
|
||||
|
||||
fetch('/debts/add-from-statement', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: new URLSearchParams({name, owner, text, year: {{ year }}, month: {{ month }}})
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.error) { resultEl.style.color='#c07070'; resultEl.textContent=data.error; return; }
|
||||
resultEl.style.color='#5A9E68';
|
||||
resultEl.textContent='✓ Card added and statement imported!';
|
||||
setTimeout(() => location.reload(), 1200);
|
||||
});
|
||||
}
|
||||
|
||||
function openStatementModal(debtId, debtName) {
|
||||
activeDebtId = debtId;
|
||||
document.getElementById('statement-modal-title').textContent = 'Paste Statement — ' + debtName;
|
||||
document.getElementById('statement-text').value = '';
|
||||
document.getElementById('statement-result').textContent = '';
|
||||
document.getElementById('statement-modal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeStatementModal() {
|
||||
document.getElementById('statement-modal').style.display = 'none';
|
||||
activeDebtId = null;
|
||||
}
|
||||
|
||||
function parseStatement() {
|
||||
const text = document.getElementById('statement-text').value;
|
||||
if (!text.trim()) return;
|
||||
const resultEl = document.getElementById('statement-result');
|
||||
resultEl.textContent = 'Parsing…';
|
||||
|
||||
fetch('/debts/parse-statement', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: new URLSearchParams({text, debt_id: activeDebtId, year: {{ year }}, month: {{ month }}})
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
resultEl.style.color = '#c07070';
|
||||
resultEl.textContent = 'Could not parse: ' + data.error;
|
||||
return;
|
||||
}
|
||||
resultEl.style.color = '#5A9E68';
|
||||
resultEl.innerHTML = `
|
||||
✓ Imported:<br>
|
||||
Previous balance: <strong>$${data.previous_balance}</strong>
|
||||
Purchases: <strong>$${data.purchases}</strong>
|
||||
Interest: <strong>$${data.interest}</strong>
|
||||
Payment: <strong>$${data.payment}</strong>
|
||||
Minimum due: <strong>$${data.minimum}</strong>
|
||||
New balance: <strong>$${data.new_balance}</strong>
|
||||
${data.credit_limit ? ' Credit limit: <strong>$' + data.credit_limit + '</strong>' : ''}
|
||||
${data.available_credit ? ' Available: <strong>$' + data.available_credit + '</strong>' : ''}
|
||||
`;
|
||||
// Reload to show updated values
|
||||
setTimeout(() => { closeStatementModal(); location.reload(); }, 1800);
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('statement-modal').addEventListener('click', function(e) {
|
||||
if (e.target === this) closeStatementModal();
|
||||
});
|
||||
|
||||
function openEditDebt(id, name, owner, apr, minimum, balance, creditLimit, notes) {
|
||||
document.getElementById('edit-debt-title').textContent = 'Edit — ' + name;
|
||||
document.getElementById('edit-debt-form').action = '/debts/edit/' + id;
|
||||
document.getElementById('edit-name').value = name;
|
||||
document.getElementById('edit-owner').value = owner;
|
||||
document.getElementById('edit-apr').value = apr != '0' ? apr : '';
|
||||
document.getElementById('edit-minimum').value = minimum != '0' ? minimum : '';
|
||||
document.getElementById('edit-balance').value = balance != '0' ? balance : '';
|
||||
document.getElementById('edit-credit-limit').value = creditLimit != '0' ? creditLimit : '';
|
||||
document.getElementById('edit-notes').value = notes;
|
||||
document.getElementById('edit-debt-modal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeEditDebt() {
|
||||
document.getElementById('edit-debt-modal').style.display = 'none';
|
||||
}
|
||||
|
||||
document.getElementById('edit-debt-modal').addEventListener('click', function(e) {
|
||||
if (e.target === this) closeEditDebt();
|
||||
});
|
||||
|
||||
function openEditBNPL(id, description, merchant, provider, owner, original, payment, total, made, frequency, nextDate, notes) {
|
||||
document.getElementById('edit-bnpl-title').textContent = 'Edit — ' + description;
|
||||
document.getElementById('edit-bnpl-form').action = '/debts/bnpl-edit/' + id;
|
||||
document.getElementById('ebnpl-description').value = description;
|
||||
document.getElementById('ebnpl-merchant').value = merchant;
|
||||
document.getElementById('ebnpl-provider').value = provider;
|
||||
document.getElementById('ebnpl-owner').value = owner;
|
||||
document.getElementById('ebnpl-original').value = original != '0' ? original : '';
|
||||
document.getElementById('ebnpl-payment').value = payment != '0' ? payment : '';
|
||||
document.getElementById('ebnpl-total').value = total;
|
||||
document.getElementById('ebnpl-made').value = made;
|
||||
document.getElementById('ebnpl-frequency').value = frequency;
|
||||
document.getElementById('ebnpl-next-date').value = nextDate;
|
||||
document.getElementById('ebnpl-notes').value = notes;
|
||||
document.getElementById('edit-bnpl-modal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeEditBNPL() {
|
||||
document.getElementById('edit-bnpl-modal').style.display = 'none';
|
||||
}
|
||||
|
||||
document.getElementById('edit-bnpl-modal').addEventListener('click', function(e) {
|
||||
if (e.target === this) closeEditBNPL();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user