Gonçalo Rodrigues 1c2bac1d5f feat: implement Tax Summary, Household Mode, and Auto Import
- Tax Summary (/tax): annual gross income from Income transactions,
  FIFO capital gains/losses from trades, expenses by category, CSV export
- Household Mode (/household): link a partner account, combined
  income/expense/disposable view for current month, shared goals list
- Auto Import (/auto-import): schedule recurring CSV imports with
  account, format and optional source URL; delete schedules; webhook docs
- New store methods for households and import_schedules collections
- storeIface extended; mockStore updated; all tests still pass

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 17:28:22 +01:00

131 lines
5.0 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{{template "base" .}}
{{define "content"}}
{{$d := .}}
<style>
.tax-hero { display:flex; gap:16px; flex-wrap:wrap; margin-bottom:24px; }
.tax-card { background:var(--card); border:1px solid var(--border); border-radius:12px; padding:20px 24px; flex:1; min-width:180px; }
.tax-card h3 { margin:0 0 4px; font-size:0.8rem; color:var(--muted); text-transform:uppercase; letter-spacing:.05em; }
.tax-card .val { font-size:1.6rem; font-weight:700; }
.gain { color:#4caf50; }
.loss { color:#f44336; }
.neutral { color:var(--text); }
.year-form { display:flex; align-items:center; gap:8px; margin-bottom:24px; }
.year-form select { padding:6px 10px; border:1px solid var(--border); border-radius:8px; background:var(--card); color:var(--text); }
table { width:100%; border-collapse:collapse; font-size:0.9rem; }
th { text-align:left; padding:8px 12px; color:var(--muted); font-weight:600; border-bottom:2px solid var(--border); }
td { padding:8px 12px; border-bottom:1px solid var(--border); }
tr:last-child td { border-bottom:none; }
.section-title { font-size:1rem; font-weight:700; margin:24px 0 12px; }
.export-btn { display:inline-block; padding:8px 18px; background:var(--accent); color:#fff; border-radius:8px; text-decoration:none; font-size:0.85rem; font-weight:600; }
</style>
<div style="display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:12px; margin-bottom:8px;">
<h1 style="margin:0;">Tax Summary</h1>
<a href="/tax/export.csv?year={{$d.Year}}" class="export-btn">Export CSV</a>
</div>
<form class="year-form" method="get" action="/tax">
<label for="year-sel" style="font-size:0.85rem; color:var(--muted);">Tax year:</label>
<select id="year-sel" name="year" onchange="this.form.submit()">
{{range $d.AvailableYears}}
<option value="{{.}}" {{if eq . $d.Year}}selected{{end}}>{{.}}</option>
{{end}}
</select>
</form>
<div class="tax-hero">
<div class="tax-card">
<h3>Gross Income</h3>
<div class="val gain">€{{cents $d.GrossIncomeCents}}</div>
<div style="font-size:0.78rem; color:var(--muted); margin-top:4px;">from Income transactions</div>
</div>
<div class="tax-card">
<h3>Total Expenses</h3>
<div class="val neutral">€{{cents $d.TotalDeductCents}}</div>
<div style="font-size:0.78rem; color:var(--muted); margin-top:4px;">across all categories</div>
</div>
<div class="tax-card">
<h3>Capital Gains</h3>
<div class="val gain">€{{cents $d.CapitalGainsCents}}</div>
<div style="font-size:0.78rem; color:var(--muted); margin-top:4px;">realized this year</div>
</div>
<div class="tax-card">
<h3>Capital Losses</h3>
<div class="val loss">€{{cents $d.CapitalLossesCents}}</div>
<div style="font-size:0.78rem; color:var(--muted); margin-top:4px;">realized this year</div>
</div>
<div class="tax-card">
<h3>Net Capital</h3>
{{if ge $d.NetCapitalCents 0}}
<div class="val gain">€{{cents $d.NetCapitalCents}}</div>
{{else}}
<div class="val loss">-€{{cents (centsAbs $d.NetCapitalCents)}}</div>
{{end}}
<div style="font-size:0.78rem; color:var(--muted); margin-top:4px;">gains losses</div>
</div>
</div>
{{if $d.CapitalEntries}}
<div class="section-title">Realized Capital Events</div>
<div style="background:var(--card); border:1px solid var(--border); border-radius:12px; overflow:hidden; margin-bottom:24px;">
<table>
<thead>
<tr>
<th>Name</th>
<th>ISIN</th>
<th style="text-align:right">Cost Basis</th>
<th style="text-align:right">Proceeds</th>
<th style="text-align:right">Gain/Loss</th>
<th style="text-align:right">%</th>
</tr>
</thead>
<tbody>
{{range $d.CapitalEntries}}
<tr>
<td>{{.Name}}</td>
<td style="font-family:monospace; font-size:0.8rem; color:var(--muted);">{{.ISIN}}</td>
<td style="text-align:right">€{{cents .BuyCents}}</td>
<td style="text-align:right">€{{cents .SellCents}}</td>
<td style="text-align:right; {{if ge .GainCents 0}}color:#4caf50{{else}}color:#f44336{{end}}">
{{if ge .GainCents 0}}+{{end}}€{{cents .GainCents}}
</td>
<td style="text-align:right; {{if ge .GainPct 0.0}}color:#4caf50{{else}}color:#f44336{{end}}">
{{pctSign .GainPct}}{{printf "%.1f" .GainPct}}%
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}
<div class="section-title">Expenses by Category</div>
<div style="background:var(--card); border:1px solid var(--border); border-radius:12px; overflow:hidden;">
{{if $d.Deductibles}}
<table>
<thead>
<tr>
<th>Category</th>
<th style="text-align:right">Total Spent</th>
</tr>
</thead>
<tbody>
{{range $d.Deductibles}}
<tr>
<td>{{.Category}}</td>
<td style="text-align:right">€{{cents .TotalCents}}</td>
</tr>
{{end}}
<tr style="font-weight:700; background:var(--bg);">
<td>Total</td>
<td style="text-align:right">€{{cents $d.TotalDeductCents}}</td>
</tr>
</tbody>
</table>
{{else}}
<div style="padding:32px; text-align:center; color:var(--muted);">No expense transactions for {{$d.Year}}</div>
{{end}}
</div>
{{end}}