Gonçalo Rodrigues 6485f58f23 i18n(finance): translate all help tips and guided empty states
- Added [help.*] sections to en.toml and pt.toml covering all six
  tooltip popups (free_cash, savings_rate, net_worth, monthly_needed,
  at_current_rate, disposable_after) with title, body, and formula keys
- Added step1/2/3 keys to [goals.empty] in both locales
- Added empty_state_title/subtitle and empty_step1-3 keys to
  [transactions.table] in both locales
- Updated dashboard.html, goals.html, transactions.html to use T.Get
  for all previously hardcoded English strings in tips and empty states

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

315 lines
14 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.

{{define "content"}}
{{$d := .}}
{{if eq $d.Notice "all_duplicates"}}
<div style="background:rgba(245,158,11,0.1); border:1px solid rgba(245,158,11,0.4); border-radius:10px; padding:12px 16px; margin-bottom:20px; display:flex; align-items:center; gap:10px;">
<span style="font-size:1.1rem;"></span>
<span style="font-size:0.9rem;">{{$d.T.Get "transactions.warning_all_dupes"}}</span>
</div>
{{end}}
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:24px; flex-wrap:wrap; gap:12px;">
<h1>{{$d.T.Get "transactions.title"}}</h1>
<button class="btn btn-primary" onclick="openAddModal()">{{$d.T.Get "transactions.btn_add"}}</button>
</div>
<div class="card mb-16">
<form method="GET" class="flex flex-wrap" style="gap:10px; align-items:flex-end;">
<div class="form-group" style="margin-bottom:0; min-width:160px;">
<label>{{$d.T.Get "transactions.filter.label_category"}}</label>
<select name="category">
<option value="">{{$d.T.Get "transactions.filter.option_all_cats"}}</option>
{{range $d.Categories}}
<option value="{{.Name}}" {{if eq $.Cat .Name}}selected{{end}}>{{.Name}}</option>
{{end}}
</select>
</div>
<div class="form-group" style="margin-bottom:0; min-width:130px;">
<label>{{$d.T.Get "transactions.filter.label_period"}}</label>
<select name="days">
<option value="">{{$d.T.Get "transactions.filter.option_all_time"}}</option>
<option value="30" {{if eq $.Days "30"}}selected{{end}}>{{$d.T.Get "transactions.filter.option_30_days"}}</option>
<option value="90" {{if eq $.Days "90"}}selected{{end}}>{{$d.T.Get "transactions.filter.option_90_days"}}</option>
<option value="365" {{if eq $.Days "365"}}selected{{end}}>{{$d.T.Get "transactions.filter.option_1_year"}}</option>
</select>
</div>
<div class="form-group" style="margin-bottom:0; flex:1; min-width:200px;">
<label>{{$d.T.Get "transactions.filter.label_search"}}</label>
<input type="text" name="search" placeholder="{{$d.T.Get "transactions.filter.placeholder_search"}}" value="{{.Search}}">
</div>
<button type="submit" class="btn btn-primary">{{$d.T.Get "transactions.filter.btn_filter"}}</button>
{{if or $.Cat $.Search $.Days}}
<a href="/transactions" class="btn btn-outline">{{$d.T.Get "transactions.filter.btn_clear"}}</a>
{{end}}
</form>
</div>
<div class="card">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px;">
<span class="text-muted">{{len $d.Txns}} transaction{{if ne (len $d.Txns) 1}}s{{end}}</span>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>{{$d.T.Get "transactions.table.col_date"}}</th>
<th>{{$d.T.Get "transactions.table.col_description"}}</th>
<th>{{$d.T.Get "transactions.table.col_account"}}</th>
<th>{{$d.T.Get "transactions.table.col_category"}}</th>
<th class="text-right">{{$d.T.Get "transactions.table.col_amount"}}</th>
<th></th>
</tr>
</thead>
<tbody>
{{range $d.Txns}}
{{$color := index $d.CategoryColors .Category}}
<tr id="row-{{.ID}}">
<td style="white-space:nowrap; color:var(--text2);">{{dateShort .Date}}</td>
<td style="max-width:260px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{.Description}}</td>
<td style="font-size:12.5px; color:var(--text3);">
{{$name := index $d.AccountNames .AccountID}}
{{if $name}}{{$name}}{{else}}<span style="opacity:.35;"></span>{{end}}
</td>
<td>
<!-- badge shown normally -->
<span id="cat-{{.ID}}" class="badge" style="
background:{{if $color}}{{$color}}18{{else}}var(--bg3){{end}};
color:{{if $color}}{{$color}}{{else}}var(--text2){{end}};
border:1px solid {{if $color}}{{$color}}33{{else}}var(--border2){{end}};">
{{if $color}}<span class="category-dot" style="background:{{$color}};box-shadow:0 0 4px {{$color}}88;"></span>{{end}}
{{.Category}}
</span>
<!-- inline dropdown, hidden until pencil clicked -->
<select id="sel-{{.ID}}" style="display:none; font-size:12px; padding:4px 8px;
border:1px solid var(--border2); border-radius:6px; background:var(--bg2); color:var(--text);"
onchange="saveCategory('{{.ID}}', this.value)"
onblur="cancelEdit('{{.ID}}')">
{{range $d.Categories}}
<option value="{{.Name}}">{{.Name}}</option>
{{end}}
</select>
<button id="edit-btn-{{.ID}}" class="btn btn-outline btn-sm"
onclick="editCat('{{.ID}}')" title="{{$d.T.Get "transactions.table.btn_edit_category"}}"
style="margin-left:4px; padding:3px 7px;"></button>
</td>
<td class="cents {{if lt .AmountCents 0}}negative{{else}}positive{{end}}"
style="font-weight:600; white-space:nowrap;">
{{if lt .AmountCents 0}}{{else}}+{{end}}€{{cents (centsAbs .AmountCents)}}
</td>
<td>
<button class="btn btn-danger btn-sm" onclick="delTxn('{{.ID}}')" title="{{$d.T.Get "transactions.table.btn_delete"}}"></button>
</td>
</tr>
{{else}}
<tr>
<td colspan="6" style="padding:0;">
<div style="text-align:center; padding:40px 24px 48px;">
<div style="font-size:40px; margin-bottom:14px;">🧾</div>
<div style="font-size:16px; font-weight:600; color:var(--text2); margin-bottom:8px;">{{$d.T.Get "transactions.table.empty_state_title"}}</div>
<p style="font-size:13px; color:var(--text3); margin-bottom:28px;">{{$d.T.Get "transactions.table.empty_state_subtitle"}}</p>
<div class="setup-steps" style="margin-bottom:28px;">
<div class="setup-step">
<span class="setup-step-num">1</span>
<div>
<strong>{{$d.T.Get "transactions.table.empty_step1_title"}}</strong>
<p>{{$d.T.Get "transactions.table.empty_step1_body"}}</p>
</div>
</div>
<div class="setup-step">
<span class="setup-step-num">2</span>
<div>
<strong>{{$d.T.Get "transactions.table.empty_step2_title"}}</strong>
<p>{{$d.T.Get "transactions.table.empty_step2_body"}}</p>
</div>
</div>
<div class="setup-step">
<span class="setup-step-num">3</span>
<div>
<strong>{{$d.T.Get "transactions.table.empty_step3_title"}}</strong>
<p>{{$d.T.Get "transactions.table.empty_step3_body"}}</p>
</div>
</div>
</div>
<div style="display:flex; gap:10px; justify-content:center; flex-wrap:wrap;">
<a href="/import" class="btn btn-primary">{{$d.T.Get "transactions.table.empty_import_link"}}</a>
<button class="btn btn-outline" onclick="openAddModal()">{{$d.T.Get "transactions.table.empty_add_btn"}}</button>
</div>
</div>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
<!-- Add Transaction Modal -->
<div id="add-modal" style="display:none; position:fixed; inset:0;
background:rgba(0,0,0,0.55); backdrop-filter:blur(4px);
z-index:300; align-items:center; justify-content:center;">
<div class="card" style="width:440px; max-width:95vw; margin:0;
border:1px solid var(--border2); box-shadow:var(--shadow-lg);
animation:fadeUp 0.2s ease-out both;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
<span style="font-size:16px; font-weight:700;">{{$d.T.Get "transactions.modal_add.title"}}</span>
<button class="btn btn-outline btn-sm" onclick="closeAddModal()" style="padding:3px 8px;"></button>
</div>
<div class="form-group">
<label>{{$d.T.Get "transactions.modal_add.label_date"}}</label>
<input type="date" id="add-date" required>
</div>
<div class="form-group">
<label>{{$d.T.Get "transactions.modal_add.label_description"}}</label>
<input type="text" id="add-desc" placeholder="{{$d.T.Get "transactions.modal_add.placeholder_desc"}}" required>
</div>
<div class="form-group">
<label>{{$d.T.Get "transactions.modal_add.label_amount"}}</label>
<div style="display:flex; gap:8px;">
<select id="add-sign" style="width:110px; padding:9px 10px;
border:1px solid var(--border2); border-radius:var(--radius-sm);
background:var(--bg2); color:var(--text);">
<option value="-1">{{$d.T.Get "transactions.modal_add.option_expense"}}</option>
<option value="1">{{$d.T.Get "transactions.modal_add.option_income"}}</option>
</select>
<input type="number" id="add-amount" placeholder="{{$d.T.Get "transactions.modal_add.placeholder_amount"}}" step="0.01" min="0" style="flex:1;">
</div>
</div>
<div class="form-group">
<label>{{$d.T.Get "transactions.modal_add.label_category"}}</label>
<select id="add-cat">
{{range $d.Categories}}
<option value="{{.Name}}">{{.Name}}</option>
{{end}}
{{if not $d.Categories}}<option value="Others">Others</option>{{end}}
</select>
</div>
<div class="form-group">
<label>{{$d.T.Get "transactions.modal_add.label_account"}}</label>
<select id="add-account">
<option value="">{{$d.T.Get "transactions.modal_add.option_no_account"}}</option>
{{range $d.Accounts}}
<option value="{{.ID}}">{{.Name}} ({{.Type}})</option>
{{end}}
</select>
</div>
{{if $d.Goals}}
<div class="form-group">
<label>{{$d.T.Get "transactions.modal_add.label_goal"}}</label>
<select id="add-goal">
<option value="">{{$d.T.Get "transactions.modal_add.option_no_goal"}}</option>
{{range $d.Goals}}
<option value="{{.ID}}">{{.Name}}</option>
{{end}}
</select>
</div>
{{end}}
<div id="add-error" class="error" style="display:none;"></div>
<div style="display:flex; gap:8px; justify-content:flex-end; margin-top:4px;">
<button class="btn btn-outline" onclick="closeAddModal()">{{$d.T.Get "transactions.modal_add.btn_cancel"}}</button>
<button class="btn btn-primary" onclick="submitAdd()">{{$d.T.Get "transactions.modal_add.btn_save"}}</button>
</div>
</div>
</div>
<script>
const catColors = { {{range $k,$v := $d.CategoryColors}}"{{$k}}":"{{$v}}",{{end}} };
function editCat(id) {
document.getElementById('cat-' + id).style.display = 'none';
document.getElementById('edit-btn-' + id).style.display = 'none';
const sel = document.getElementById('sel-' + id);
sel.style.display = 'inline-block';
sel.focus();
}
function cancelEdit(id) {
setTimeout(() => {
document.getElementById('cat-' + id).style.display = 'inline-flex';
document.getElementById('edit-btn-' + id).style.display = 'inline-flex';
document.getElementById('sel-' + id).style.display = 'none';
}, 160);
}
function saveCategory(id, cat) {
fetch('/api/transactions/' + id, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({category: cat})
}).then(r => {
if (!r.ok) return;
const color = catColors[cat] || '';
const badge = document.getElementById('cat-' + id);
badge.style.background = color ? color + '18' : 'var(--bg3)';
badge.style.color = color || 'var(--text2)';
badge.style.borderColor = color ? color + '33' : 'var(--border2)';
badge.innerHTML = (color
? `<span class="category-dot" style="background:${color};box-shadow:0 0 4px ${color}88;"></span>`
: '') + cat;
cancelEdit(id);
});
}
function delTxn(id) {
if (!confirm('{{$d.T.Get "transactions.confirm.delete_msg"}}')) return;
fetch('/api/transactions/' + id, {method: 'DELETE'}).then(r => {
if (r.ok) document.getElementById('row-' + id).remove();
});
}
function openAddModal(preselectedGoalID) {
document.getElementById('add-modal').style.display = 'flex';
document.getElementById('add-date').value = new Date().toISOString().slice(0, 10);
if (preselectedGoalID) {
const goalSel = document.getElementById('add-goal');
if (goalSel) goalSel.value = preselectedGoalID;
const signSel = document.getElementById('add-sign');
if (signSel) signSel.value = '-1'; // goal contributions are expenses
}
document.getElementById('add-desc').focus();
}
function closeAddModal() {
document.getElementById('add-modal').style.display = 'none';
document.getElementById('add-error').style.display = 'none';
}
function submitAdd() {
const date = document.getElementById('add-date').value;
const desc = document.getElementById('add-desc').value.trim();
const sign = parseInt(document.getElementById('add-sign').value);
const amt = parseFloat(document.getElementById('add-amount').value);
const cat = document.getElementById('add-cat').value;
const acct = document.getElementById('add-account').value;
const goalEl = document.getElementById('add-goal');
const goalID = goalEl ? goalEl.value : '';
const errEl = document.getElementById('add-error');
if (!date || !desc || isNaN(amt) || amt <= 0) {
errEl.textContent = '{{$d.T.Get "transactions.modal_add.error_required"}}';
errEl.style.display = 'block';
return;
}
errEl.style.display = 'none';
const body = {account_id: acct, date, description: desc, amount_cents: Math.round(amt * 100) * sign, category: cat};
if (goalID) body.goal_id = goalID;
fetch('/api/transactions', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(body)
}).then(r => {
if (!r.ok) { errEl.textContent = '{{$d.T.Get "transactions.modal_add.error_save_failed"}}'; errEl.style.display = 'block'; return; }
closeAddModal();
location.reload();
});
}
// auto-open modal when redirected from goals page with ?fund_goal=<id>
(function() {
const params = new URLSearchParams(window.location.search);
const fundGoal = params.get('fund_goal');
if (fundGoal) openAddModal(fundGoal);
})();
document.getElementById('add-modal').addEventListener('click', e => { if (e.target === e.currentTarget) closeAddModal(); });
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeAddModal(); });
</script>
{{end}}