Initial commit — Moon Household Budget

This commit is contained in:
Bonna
2026-06-06 14:22:57 -05:00
commit fa3c030b36
44 changed files with 10860 additions and 0 deletions

175
templates/sinking.html Normal file
View File

@@ -0,0 +1,175 @@
{% 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 %}