Initial commit — Moon Household Budget
This commit is contained in:
277
templates/income.html
Normal file
277
templates/income.html
Normal file
@@ -0,0 +1,277 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
<div class="page-header">
|
||||
<h1>Income — {{ month_name }} {{ year }}</h1>
|
||||
<div class="month-nav">
|
||||
<a href="{{ url_for('income_view', year=prev_year, month=prev_month) }}" class="btn btn-ghost">‹ Prev</a>
|
||||
<a href="{{ url_for('income_view', year=next_year, month=next_month) }}" class="btn btn-ghost">Next ›</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- YTD Summary Cards -->
|
||||
<div class="card-grid" style="margin-bottom:1.5rem;">
|
||||
<div class="card">
|
||||
<div class="card-label">{{ month_name }} Total</div>
|
||||
<div class="card-value">${{ month_total | money }}</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-label">{{ year }} YTD Total</div>
|
||||
<div class="card-value">${{ ytd_total | money }}</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-label">Monthly Average</div>
|
||||
<div class="card-value">
|
||||
{% set months_with_income = ytd_by_month | length %}
|
||||
${{ (ytd_total / months_with_income if months_with_income else 0) | money }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tax estimate banner -->
|
||||
<div style="background:#fff8e6;border:1px solid #e8d49a;border-radius:var(--radius);padding:1rem 1.25rem;margin-bottom:1.5rem;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:1rem;">
|
||||
<div>
|
||||
<div style="font-weight:600;margin-bottom:0.2rem;">🧾 {{ year }} Tax Estimate — based on ${{ ytd_total | money }} YTD income</div>
|
||||
<div style="font-size:0.82rem;color:var(--text-light);">
|
||||
~${{ tax_estimate.total | money }} owed ·
|
||||
${{ tax_set_aside | money }} set aside ·
|
||||
{% if tax_still_needed > 0 %}
|
||||
<span style="color:#c07070;font-weight:600;">${{ tax_still_needed | money }} still needed</span>
|
||||
{% else %}
|
||||
<span style="color:#5A9E68;font-weight:600;">fully covered ✓</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{ url_for('taxes_view') }}" class="btn btn-ghost" style="font-size:0.82rem;">View Tax Details →</a>
|
||||
</div>
|
||||
|
||||
<!-- This Month by Stream -->
|
||||
<div class="budget-section">
|
||||
<div class="budget-section-header" style="border-left-color: #A0C5A8;">
|
||||
<h3>{{ month_name }} {{ year }} — by Stream</h3>
|
||||
</div>
|
||||
<table class="budget-table">
|
||||
<thead>
|
||||
<tr><th>Stream</th><th>Owner</th><th>This Month</th><th>YTD</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for s in income_streams %}
|
||||
<tr>
|
||||
<td><strong>{{ s.name }}</strong></td>
|
||||
<td><span class="owner-tag" style="background:{{ owner_colors.get(s.owner,'#ddd') }};font-size:0.72rem;">{{ s.owner }}</span></td>
|
||||
<td class="amount">${{ by_stream.get(s.name, 0) | money }}</td>
|
||||
<td class="amount muted">${{ ytd_by_stream.get(s.name, 0) | money }}</td>
|
||||
<td>
|
||||
<button class="btn btn-ghost" style="font-size:0.75rem;padding:0.2rem 0.5rem;"
|
||||
onclick="openLogModal('{{ s.name }}')">+ Log</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="total-row">
|
||||
<td colspan="2">Total</td>
|
||||
<td class="amount">${{ month_total | money }}</td>
|
||||
<td class="amount muted">${{ ytd_total | money }}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- YTD by Month -->
|
||||
{% if ytd_by_month %}
|
||||
<div class="budget-section" style="margin-top:1.5rem;">
|
||||
<div class="budget-section-header" style="border-left-color: #C5BBA0;">
|
||||
<h3>{{ year }} — Month by Month</h3>
|
||||
</div>
|
||||
<div style="padding:1rem 0.5rem;">
|
||||
<canvas id="income-monthly-chart" height="80"></canvas>
|
||||
</div>
|
||||
<table class="budget-table" style="margin-top:0.5rem;">
|
||||
<thead><tr><th>Month</th><th>Total Income</th></tr></thead>
|
||||
<tbody>
|
||||
{% for mk in sorted_months %}
|
||||
{% set amt = ytd_by_month[mk] %}
|
||||
<tr>
|
||||
<td>{{ mk }}</td>
|
||||
<td class="amount">${{ amt | money }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="total-row">
|
||||
<td>YTD Total</td>
|
||||
<td class="amount">${{ ytd_total | money }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Log Entries for This Month -->
|
||||
{% if entries %}
|
||||
<div class="budget-section" style="margin-top:1.5rem;">
|
||||
<div class="budget-section-header" style="border-left-color: #B0CDD6;">
|
||||
<h3>{{ month_name }} Entries</h3>
|
||||
</div>
|
||||
<table class="budget-table">
|
||||
<thead><tr><th>Stream</th><th>Amount</th><th>Date Logged</th><th>Note</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{% for e in entries | sort(attribute='date', reverse=True) %}
|
||||
<tr>
|
||||
<td>{{ e.stream }}</td>
|
||||
<td class="amount">${{ e.amount | money }}</td>
|
||||
<td class="muted">{{ e.date }}</td>
|
||||
<td class="muted">{{ e.note or '—' }}</td>
|
||||
<td>
|
||||
<form method="POST" action="/income/delete/{{ e.id }}" style="display:inline;">
|
||||
<input type="hidden" name="year" value="{{ year }}">
|
||||
<input type="hidden" name="month" value="{{ month }}">
|
||||
<button type="submit" class="btn btn-ghost" style="font-size:0.72rem;padding:0.15rem 0.4rem;color:#c07070;"
|
||||
onclick="return confirm('Delete this entry?')">✕</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Log Income Modal -->
|
||||
<div id="income-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.3);z-index:100;align-items:center;justify-content:center;">
|
||||
<div style="background:#fff;border-radius:12px;padding:1.5rem;min-width:320px;max-width:420px;width:90%;box-shadow:0 8px 32px rgba(0,0,0,0.18);">
|
||||
<h3 style="margin:0 0 1rem;">Log Income</h3>
|
||||
<form method="POST" action="/income/log">
|
||||
<input type="hidden" name="month_str" value="{{ '%04d-%02d' | format(year, month) }}">
|
||||
<div style="margin-bottom:0.75rem;">
|
||||
<label style="display:block;font-size:0.85rem;margin-bottom:0.25rem;">Stream</label>
|
||||
<select name="stream" id="modal-stream" class="form-input" style="width:100%;">
|
||||
{% for s in income_streams %}
|
||||
<option value="{{ s.name }}">{{ s.name }} ({{ s.owner }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div style="margin-bottom:0.75rem;">
|
||||
<label style="display:block;font-size:0.85rem;margin-bottom:0.25rem;">Amount</label>
|
||||
<input type="number" name="amount" step="0.01" min="0" class="form-input budget-input" style="width:100%;" placeholder="0.00" required>
|
||||
</div>
|
||||
<div style="margin-bottom:1rem;">
|
||||
<label style="display:block;font-size:0.85rem;margin-bottom:0.25rem;">Note (optional)</label>
|
||||
<input type="text" name="note" class="form-input budget-input" style="width:100%;" placeholder="e.g. paycheck, client payment…">
|
||||
</div>
|
||||
<div style="display:flex;gap:0.75rem;justify-content:flex-end;">
|
||||
<button type="button" class="btn btn-ghost" onclick="closeModal()">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openLogModal(stream) {
|
||||
const modal = document.getElementById('income-modal');
|
||||
modal.style.display = 'flex';
|
||||
if (stream) {
|
||||
document.getElementById('modal-stream').value = stream;
|
||||
}
|
||||
}
|
||||
function closeModal() {
|
||||
document.getElementById('income-modal').style.display = 'none';
|
||||
}
|
||||
document.getElementById('income-modal').addEventListener('click', function(e) {
|
||||
if (e.target === this) closeModal();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div style="margin-top:1.5rem;">
|
||||
<button class="btn btn-primary" onclick="openLogModal('')">+ Log Income</button>
|
||||
</div>
|
||||
|
||||
<!-- Bulk Backfill -->
|
||||
<div class="budget-section" style="margin-top:2rem;">
|
||||
<div class="budget-section-header" style="border-left-color:#C5BBA0;">
|
||||
<h3>Backfill Past Months</h3>
|
||||
</div>
|
||||
<p class="muted" style="padding:0.5rem 0 1rem;">Enter totals for any month you haven't logged yet. Leave blank to skip. Existing entries won't be duplicated.</p>
|
||||
<form method="POST" action="/income/backfill">
|
||||
<input type="hidden" name="year" value="{{ year }}">
|
||||
<div style="overflow-x:auto;">
|
||||
<table class="budget-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Month</th>
|
||||
{% for s in income_streams %}
|
||||
<th style="min-width:110px;">
|
||||
{{ s.name }}<br>
|
||||
<span class="owner-tag" style="background:{{ owner_colors.get(s.owner,'#ddd') }};font-size:0.68rem;">{{ s.owner }}</span>
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for m in range(1, 13) %}
|
||||
{% set mk = '%04d-%02d' | format(year, m) %}
|
||||
{% set mn = month_names[m] %}
|
||||
{% set is_current = (m == month) %}
|
||||
<tr{% if is_current %} style="background:#f5f2ee;"{% endif %}>
|
||||
<td>
|
||||
<strong>{{ mn }}</strong>
|
||||
{% if is_current %}<span class="muted" style="font-size:0.75rem;"> (current)</span>{% endif %}
|
||||
{% if ytd_by_month.get(mk) %}
|
||||
<span style="font-size:0.72rem;color:#7A9E78;margin-left:0.25rem;">✓ ${{ "%.0f"|format(ytd_by_month[mk]) }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% for s in income_streams %}
|
||||
<td>
|
||||
<input type="number" step="0.01" min="0"
|
||||
name="amt_{{ m }}_{{ s.name | replace(' ','_') }}"
|
||||
class="budget-input" style="width:100px;"
|
||||
placeholder="0.00">
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div style="margin-top:1rem;">
|
||||
<button type="submit" class="btn btn-primary">Save All</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
{% if ytd_by_month %}
|
||||
const incomeLabels = {{ sorted_months | tojson }};
|
||||
const incomeValues = [{% for mk in sorted_months %}{{ ytd_by_month[mk] }},{% endfor %}];
|
||||
|
||||
new Chart(document.getElementById('income-monthly-chart'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: incomeLabels,
|
||||
datasets: [{
|
||||
label: 'Income',
|
||||
data: incomeValues,
|
||||
backgroundColor: '#7AB87A',
|
||||
borderRadius: 6,
|
||||
borderSkipped: false,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: { callbacks: { label: ctx => ' $' + ctx.parsed.y.toFixed(2) } }
|
||||
},
|
||||
scales: {
|
||||
x: { grid: { display: false } },
|
||||
y: { grid: { color: '#f0ece6' }, ticks: { callback: v => '$' + v.toLocaleString() } }
|
||||
},
|
||||
animation: { duration: 400 },
|
||||
}
|
||||
});
|
||||
{% endif %}
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user