176 lines
7.0 KiB
HTML
176 lines
7.0 KiB
HTML
{% extends "base.html" %}
|
||
{% block content %}
|
||
<div class="page-header">
|
||
<h1>Sinking Funds</h1>
|
||
</div>
|
||
|
||
<!-- Total savings banner -->
|
||
<div class="sinking-total-bar">
|
||
<div>
|
||
<div class="sinking-total-label">Total Across All Funds</div>
|
||
<div class="sinking-total-amount">${{ total | money }}</div>
|
||
</div>
|
||
<div class="sinking-total-note">Match this number to your savings account balance</div>
|
||
</div>
|
||
|
||
<!-- Fund cards -->
|
||
<div class="sinking-grid">
|
||
{% for f in funds %}
|
||
{% set color = fund_colors.get(f.name, '#C5BBA0') %}
|
||
<div class="sinking-card" style="border-top: 4px solid {{ color }};">
|
||
<div class="sinking-card-header">
|
||
<div class="sinking-card-name">{{ f.name }}</div>
|
||
<span class="sinking-type-badge {% if f.type == 'goal' %}badge-goal{% endif %}">
|
||
{{ 'Goal' if f.type == 'goal' else 'Ongoing' }}
|
||
</span>
|
||
</div>
|
||
|
||
{% if f.notes %}
|
||
<div class="sinking-notes">{{ f.notes }}</div>
|
||
{% endif %}
|
||
|
||
<div class="sinking-balance">${{ (f.balance or 0) | money }}</div>
|
||
|
||
{% if f.type == 'goal' and f.goal %}
|
||
{% set pct = f.pct or 0 %}
|
||
<div class="sinking-progress-wrap">
|
||
<div class="sinking-progress-bar">
|
||
<div class="sinking-progress-fill" style="width:{{ pct }}%;background:{{ color }};"></div>
|
||
</div>
|
||
<div class="sinking-progress-label">
|
||
${{ (f.balance or 0) | money }} of ${{ f.goal | money }} ({{ pct }}%)
|
||
</div>
|
||
</div>
|
||
{% if f.monthly_needed %}
|
||
<div class="sinking-monthly-needed">
|
||
Save <strong>${{ f.monthly_needed | money }}/mo</strong>
|
||
to reach goal
|
||
{% if f.months_left %} in {{ f.months_left }} months{% endif %}
|
||
</div>
|
||
{% endif %}
|
||
{% else %}
|
||
{% if f.monthly %}
|
||
<div class="sinking-monthly-needed">
|
||
Monthly contribution: <strong>${{ f.monthly | money }}</strong>
|
||
</div>
|
||
{% endif %}
|
||
{% endif %}
|
||
|
||
<!-- Contribute / Withdraw -->
|
||
<div class="sinking-actions">
|
||
<form method="POST" action="{{ url_for('sinking_contribute') }}" class="sinking-action-form">
|
||
<input type="hidden" name="name" value="{{ f.name }}">
|
||
<input type="number" name="amount" step="0.01" min="0" placeholder="Amount" class="sinking-amount-input">
|
||
<button type="submit" class="btn btn-accept">+ Add</button>
|
||
</form>
|
||
<form method="POST" action="{{ url_for('sinking_withdraw') }}" class="sinking-action-form">
|
||
<input type="hidden" name="name" value="{{ f.name }}">
|
||
<input type="number" name="amount" step="0.01" min="0" placeholder="Amount" class="sinking-amount-input">
|
||
<button type="submit" class="btn btn-ghost">− Use</button>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- Edit settings -->
|
||
<details class="sinking-edit-details">
|
||
<summary class="sinking-edit-toggle">Edit settings</summary>
|
||
<div class="sinking-edit-form">
|
||
<div class="form-group">
|
||
<label>Correct Balance <span class="optional">(override if something's off)</span></label>
|
||
<div style="display:flex;gap:0.5rem;align-items:center;">
|
||
<input type="number" step="0.01" id="balance-override-{{ loop.index }}" placeholder="{{ '%.2f' | format(f.balance or 0) }}" style="flex:1;">
|
||
<button type="button" class="btn btn-ghost" style="font-size:0.82rem;white-space:nowrap;"
|
||
onclick="setBalance('{{ f.name }}', document.getElementById('balance-override-{{ loop.index }}').value)">Set</button>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Type</label>
|
||
<select onchange="updateFund('{{ f.name }}', 'type', this.value)">
|
||
<option value="ongoing" {% if f.type != 'goal' %}selected{% endif %}>Ongoing</option>
|
||
<option value="goal" {% if f.type == 'goal' %}selected{% endif %}>Goal</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Monthly Contribution</label>
|
||
<input type="number" step="0.01" value="{{ f.monthly or '' }}" placeholder="0.00"
|
||
onchange="updateFund('{{ f.name }}', 'monthly', this.value)">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Goal Amount <span class="optional">(if goal type)</span></label>
|
||
<input type="number" step="0.01" value="{{ f.goal or '' }}" placeholder="0.00"
|
||
onchange="updateFund('{{ f.name }}', 'goal', this.value)">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Target Date <span class="optional">(if goal type)</span></label>
|
||
<input type="date" value="{{ f.target_date or '' }}"
|
||
onchange="updateFund('{{ f.name }}', 'target_date', this.value)">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Notes</label>
|
||
<input type="text" value="{{ f.notes or '' }}" placeholder="Optional note..."
|
||
onchange="updateFund('{{ f.name }}', 'notes', this.value)">
|
||
</div>
|
||
</div>
|
||
</details>
|
||
</div>
|
||
{% endfor %}
|
||
|
||
<!-- Add new fund card -->
|
||
<div class="sinking-card sinking-add-card">
|
||
<div class="sinking-card-name" style="margin-bottom:1rem;">+ New Fund</div>
|
||
<form method="POST" action="{{ url_for('sinking_add') }}">
|
||
<div class="form-group">
|
||
<label>Fund Name</label>
|
||
<input type="text" name="name" placeholder="e.g. New Car, Vacation" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Type</label>
|
||
<select name="type">
|
||
<option value="ongoing">Ongoing</option>
|
||
<option value="goal">Goal</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Starting Balance <span class="optional">(optional)</span></label>
|
||
<input type="number" name="balance" step="0.01" min="0" placeholder="0.00">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Monthly Contribution</label>
|
||
<input type="number" name="monthly" step="0.01" min="0" placeholder="0.00">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Goal Amount <span class="optional">(if goal)</span></label>
|
||
<input type="number" name="goal" step="0.01" min="0" placeholder="0.00">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Target Date <span class="optional">(if goal)</span></label>
|
||
<input type="date" name="target_date">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Notes <span class="optional">(optional)</span></label>
|
||
<input type="text" name="notes" placeholder="">
|
||
</div>
|
||
<button type="submit" class="btn btn-primary">Add Fund</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
function updateFund(name, field, value) {
|
||
fetch('/sinking/update', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||
body: new URLSearchParams({name, field, value})
|
||
});
|
||
}
|
||
|
||
function setBalance(name, value) {
|
||
if (value === '' || isNaN(parseFloat(value))) return;
|
||
fetch('/sinking/update', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||
body: new URLSearchParams({name, field: 'balance', value})
|
||
}).then(() => location.reload());
|
||
}
|
||
</script>
|
||
{% endblock %}
|