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

159
templates/bills.html Normal file
View File

@@ -0,0 +1,159 @@
{% extends "base.html" %}
{% block content %}
<div class="page-header">
<h1>Manage Bills</h1>
<a href="{{ url_for('bill_calendar') }}" class="btn btn-ghost">View Calendar</a>
</div>
<div class="card-wide">
<h2>Add a Bill</h2>
<form method="POST" action="{{ url_for('bill_add') }}" class="bill-form">
<div class="bill-form-grid">
<div class="form-group">
<label>Bill Name</label>
<input type="text" name="name" placeholder="e.g. Verizon, Affirm (home)" required>
</div>
<div class="form-group">
<label>Amount</label>
<input type="number" name="amount" step="0.01" min="0" placeholder="0.00">
</div>
<div class="form-group">
<label>Due Day of Month</label>
<input type="number" name="due_day" min="1" max="31" placeholder="131" required>
</div>
<div class="form-group">
<label>Category</label>
<select name="category" id="add-category" onchange="updateAddSubcats()">
<option value="">— select —</option>
{% for cat in categories %}
<option value="{{ cat }}">{{ cat }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label>Subcategory <span class="optional">(optional)</span></label>
<select name="subcategory" id="add-subcategory">
<option value="">— none —</option>
</select>
</div>
<div class="form-group">
<label>Notes <span class="optional">(optional)</span></label>
<input type="text" name="notes" placeholder="e.g. T=Theresa, B=both">
</div>
<div class="form-group" style="display:flex;align-items:center;gap:0.5rem;padding-top:0.5rem;">
<input type="checkbox" name="autopay" id="autopay-new">
<label for="autopay-new" style="margin:0;font-weight:400;">On autopay (no reminder needed)</label>
</div>
</div>
<button type="submit" class="btn btn-primary">Add Bill</button>
</form>
</div>
{% if bills %}
<section class="section">
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:0.75rem;margin-bottom:0.75rem;">
<h2>Your Bills <span class="muted">({{ bills | length }} total)</span></h2>
<input type="text" id="bills-search" placeholder="Search bills…" class="budget-input" style="width:220px;" oninput="filterBills(this.value)">
</div>
<table class="txn-table txn-table-full">
<thead>
<tr>
<th>Due Day</th>
<th>Name</th>
<th>Amount</th>
<th>Category</th>
<th>Autopay</th>
<th>Remind Me</th>
<th>Notes</th>
<th></th>
</tr>
</thead>
<tbody>
{% for b in bills | sort(attribute='due_day') %}
<tr>
<td class="amount">{{ b.due_day }}</td>
<td>{{ b.name }}</td>
<td class="amount">{% if b.amount %}${{ b.amount | money }}{% else %}<span class="muted">varies</span>{% endif %}</td>
<td>
{% if b.category %}
<span class="tag" style="background: {{ colors.get(b.category, '#ddd') }};">
{{ b.category }}{% if b.subcategory %} {{ b.subcategory }}{% endif %}
</span>
{% endif %}
</td>
<td>
<button class="toggle-btn {% if b.get('autopay') %}toggle-on{% endif %}"
onclick="toggle('{{ b.id }}', 'autopay', this)">
{{ '✓ Yes' if b.get('autopay') else 'No' }}
</button>
</td>
<td>
<button class="toggle-btn {% if b.get('remind', not b.get('autopay')) %}toggle-on{% endif %}"
onclick="toggle('{{ b.id }}', 'remind', this)">
{{ '🔔 Yes' if b.get('remind', not b.get('autopay')) else 'Off' }}
</button>
</td>
<td class="muted">{{ b.notes }}</td>
<td>
<form method="POST" action="{{ url_for('bill_delete', bill_id=b.id) }}"
onsubmit="return confirm('Delete {{ b.name }}?');" style="display:inline;">
<button type="submit" class="edit-link" style="background:none;border:none;cursor:pointer;color:#C5A0A0;">Delete</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</section>
{% else %}
<div class="empty-state">
<p>No bills yet. Add your first one above.</p>
</div>
{% endif %}
<div class="reminder-bar">
<span>Run reminders now to test your email & Mac notifications:</span>
<button class="btn btn-ghost" onclick="runReminders(this)">Send Test Reminder</button>
<span class="reminder-result" id="reminder-result"></span>
</div>
<script>
const subcats = {{ categories | tojson }};
function updateAddSubcats() {
const cat = document.getElementById('add-category').value;
const sel = document.getElementById('add-subcategory');
sel.innerHTML = '<option value="">— none —</option>';
(subcats[cat] || []).forEach(s => {
const o = document.createElement('option');
o.value = s; o.textContent = s;
sel.appendChild(o);
});
}
function toggle(billId, field, btn) {
fetch(`/bills/toggle/${billId}/${field}`, { method: 'POST' })
.then(r => r.json())
.then(() => location.reload());
}
function filterBills(q) {
q = q.toLowerCase();
document.querySelectorAll('.txn-table tbody tr').forEach(row => {
row.style.display = row.textContent.toLowerCase().includes(q) ? '' : 'none';
});
}
function runReminders(btn) {
btn.textContent = 'Sending…';
btn.disabled = true;
fetch('/reminders/run', { method: 'POST' })
.then(r => r.json())
.then(data => {
document.getElementById('reminder-result').textContent = data.message;
btn.textContent = 'Send Test Reminder';
btn.disabled = false;
});
}
</script>
{% endblock %}