Compute a sha256 fingerprint (date|description|amount|account_id, first 16 hex chars) for every CSV row and store it as bank_ref. At preview time, existing fingerprints are fetched and matching rows are shown greyed out with a "duplicate" label. At confirm time, those rows are silently skipped — only truly new transactions are inserted. If every row is a duplicate the user is redirected with ?notice=all_duplicates instead of inserting an empty batch. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
151 lines
6.0 KiB
HTML
151 lines
6.0 KiB
HTML
{{define "content"}}
|
||
{{$d := .}}
|
||
<h1 style="margin-bottom:24px;">Import</h1>
|
||
|
||
{{if $d.Error}}
|
||
<div class="card" style="border-color:var(--red); background:var(--red-dim);">
|
||
<span style="color:var(--red); font-size:13.5px;">⚠ {{$d.Error}}</span>
|
||
</div>
|
||
{{end}}
|
||
|
||
{{if $d.Preview}}
|
||
<!-- Preview table -->
|
||
<div class="card animate-on-scroll">
|
||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:16px; flex-wrap:wrap; gap:8px;">
|
||
<div>
|
||
<h2 style="font-size:15px; font-weight:700; color:var(--text); text-transform:none; letter-spacing:0;">Preview</h2>
|
||
<p class="text-muted" style="margin-top:3px;">
|
||
{{$d.Preview.Total}} rows
|
||
{{if $d.DuplicateCount}}
|
||
— <span style="color:var(--yellow, #f59e0b); font-weight:600;">{{$d.DuplicateCount}} already imported</span> (shown greyed out, will be skipped)
|
||
{{end}}
|
||
</p>
|
||
</div>
|
||
<a href="/import" class="btn btn-outline btn-sm">← Back</a>
|
||
</div>
|
||
|
||
<form method="POST" action="/import/confirm">
|
||
<input type="hidden" name="account_id" value="{{$d.Preview.AccountID}}">
|
||
<input type="hidden" name="format" value="{{$d.SelectedFormat}}">
|
||
<input type="hidden" name="raw_data" value="{{$d.RawData}}">
|
||
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Date</th>
|
||
<th>Description</th>
|
||
<th class="text-right">Amount</th>
|
||
<th>Category</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{{range $i, $row := $d.Preview.Rows}}
|
||
<tr {{if $row.Duplicate}}style="opacity:0.4;"{{end}}>
|
||
<td style="white-space:nowrap; color:var(--text2);">{{$row.Date}}</td>
|
||
<td style="max-width:280px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">
|
||
{{$row.Description}}
|
||
{{if $row.Duplicate}}<span style="font-size:11px; font-weight:600; color:var(--muted); margin-left:6px;">duplicate</span>{{end}}
|
||
</td>
|
||
<td class="cents {{if lt $row.AmountCents 0}}negative{{else}}positive{{end}}" style="font-weight:600; white-space:nowrap;">
|
||
{{if lt $row.AmountCents 0}}−{{else}}+{{end}}€{{cents (centsAbs $row.AmountCents)}}
|
||
</td>
|
||
<td>
|
||
{{if $row.Duplicate}}
|
||
<span style="font-size:12px; color:var(--muted);">—</span>
|
||
{{else}}
|
||
<select name="categories" class="cat-select" data-selected="{{$row.Category}}"
|
||
style="font-size:12.5px; padding:5px 8px; border:1.5px solid var(--border2);
|
||
border-radius:6px; background:var(--bg2); color:var(--text);
|
||
border-left-width:4px; cursor:pointer; min-width:130px;">
|
||
{{range $cat := $d.Categories}}
|
||
<option value="{{$cat}}" {{if eq $cat $row.Category}}selected{{end}}>{{$cat}}</option>
|
||
{{end}}
|
||
</select>
|
||
{{end}}
|
||
</td>
|
||
</tr>
|
||
{{end}}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div style="display:flex; gap:10px; margin-top:18px; flex-wrap:wrap;">
|
||
<button type="submit" class="btn btn-primary">✓ Confirm Import</button>
|
||
<a href="/import" class="btn btn-outline">Cancel</a>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<script>
|
||
const catColors = { {{range $k,$v := $d.CategoryColors}}"{{$k}}":"{{$v}}",{{end}} };
|
||
document.querySelectorAll('.cat-select').forEach(sel => {
|
||
function paint() {
|
||
const color = catColors[sel.value] || '';
|
||
sel.style.borderLeftColor = color || 'var(--border2)';
|
||
}
|
||
sel.addEventListener('change', paint);
|
||
paint();
|
||
});
|
||
</script>
|
||
|
||
{{else}}
|
||
<!-- Upload forms -->
|
||
<div class="grid-2" style="align-items:start;">
|
||
<div class="card animate-on-scroll">
|
||
<h2 style="font-size:15px; font-weight:700; color:var(--text); text-transform:none; letter-spacing:0; margin-bottom:18px;">
|
||
Bank Transactions
|
||
</h2>
|
||
<form method="POST" action="/import/preview" enctype="multipart/form-data">
|
||
<div class="form-group">
|
||
<label>Account</label>
|
||
<select name="account_id" required>
|
||
<option value="">Select account…</option>
|
||
{{range $d.Accounts}}
|
||
<option value="{{.ID}}">{{.Name}} ({{.Type}})</option>
|
||
{{end}}
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Bank / Format</label>
|
||
<select name="format">
|
||
<option value="cgd">Caixa Geral de Depósitos (CGD)</option>
|
||
<option value="traderepublic">Trade Republic Card</option>
|
||
<option value="generic">Generic CSV</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>CSV File</label>
|
||
<input type="file" name="file" accept=".csv,.txt" required
|
||
style="padding:10px; cursor:pointer; font-size:13px;">
|
||
</div>
|
||
<button type="submit" class="btn btn-primary" style="width:100%;">Preview Import</button>
|
||
</form>
|
||
</div>
|
||
|
||
<div class="card animate-on-scroll">
|
||
<h2 style="font-size:15px; font-weight:700; color:var(--text); text-transform:none; letter-spacing:0; margin-bottom:18px;">
|
||
Securities Trades
|
||
</h2>
|
||
<p class="text-muted" style="margin-bottom:16px; font-size:13px; line-height:1.6;">
|
||
Upload your Trade Republic securities CSV to import buy/sell trades into your portfolio.
|
||
</p>
|
||
<form method="POST" action="/import/securities" enctype="multipart/form-data">
|
||
<div class="form-group">
|
||
<label>Trade Republic Securities CSV</label>
|
||
<input type="file" name="file" accept=".csv,.txt" required
|
||
style="padding:10px; cursor:pointer; font-size:13px;">
|
||
</div>
|
||
<button type="submit" class="btn btn-primary" style="width:100%;">Import Trades</button>
|
||
</form>
|
||
|
||
<div style="margin-top:20px; padding-top:16px; border-top:1px solid var(--border);">
|
||
<p class="text-muted" style="font-size:12px; line-height:1.6;">
|
||
After importing, visit <a href="/portfolio" style="color:var(--accent);">Portfolio</a> to see live prices and P&L.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{{end}}
|
||
{{end}}
|