feat: i18n notes, contacts, budget, settings pages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+53
-39
@@ -8,6 +8,7 @@
|
||||
import { api } from '/api.js';
|
||||
import { openModal as openSharedModal, closeModal } from '/components/modal.js';
|
||||
import { stagger, vibrate } from '/utils/ux.js';
|
||||
import { t } from '/i18n.js';
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Konstanten
|
||||
@@ -18,6 +19,18 @@ const CATEGORIES = [
|
||||
'Freizeit', 'Kleidung', 'Gesundheit', 'Bildung', 'Sonstiges',
|
||||
];
|
||||
|
||||
const CATEGORY_LABELS = () => ({
|
||||
'Lebensmittel': t('budget.catFood'),
|
||||
'Miete': t('budget.catRent'),
|
||||
'Versicherung': t('budget.catInsurance'),
|
||||
'Mobilität': t('budget.catMobility'),
|
||||
'Freizeit': t('budget.catLeisure'),
|
||||
'Kleidung': t('budget.catClothing'),
|
||||
'Gesundheit': t('budget.catHealth'),
|
||||
'Bildung': t('budget.catEducation'),
|
||||
'Sonstiges': t('budget.catMisc'),
|
||||
});
|
||||
|
||||
const MONTH_NAMES = ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
|
||||
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'];
|
||||
|
||||
@@ -74,7 +87,7 @@ async function loadMonth(month) {
|
||||
state.entries = [];
|
||||
state.summary = { income: 0, expenses: 0, balance: 0, byCategory: [] };
|
||||
state.prevSummary = null;
|
||||
window.oikos?.showToast('Budget konnte nicht geladen werden.', 'danger');
|
||||
window.oikos?.showToast(t('budget.loadError'), 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,24 +102,24 @@ export async function render(container, { user }) {
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="budget-page">
|
||||
<h1 class="sr-only">Budget</h1>
|
||||
<h1 class="sr-only">${t('budget.title')}</h1>
|
||||
<div class="budget-nav">
|
||||
<button class="btn btn--icon" id="budget-prev" aria-label="Vorheriger Monat">
|
||||
<button class="btn btn--icon" id="budget-prev" aria-label="${t('budget.prevMonth')}">
|
||||
<i data-lucide="chevron-left" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button class="budget-nav__today" id="budget-today">Aktuell</button>
|
||||
<button class="budget-nav__today" id="budget-today">${t('budget.currentMonth')}</button>
|
||||
<span class="budget-nav__label" id="budget-label"></span>
|
||||
<button class="btn btn--primary btn--icon" id="budget-add" aria-label="Eintrag hinzufügen">
|
||||
<button class="btn btn--primary btn--icon" id="budget-add" aria-label="${t('budget.addEntryLabel')}">
|
||||
<i data-lucide="plus" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button class="btn btn--icon" id="budget-next" aria-label="Nächster Monat">
|
||||
<button class="btn btn--icon" id="budget-next" aria-label="${t('budget.nextMonth')}">
|
||||
<i data-lucide="chevron-right" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div id="budget-body" style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
|
||||
<div style="padding:2rem;text-align:center;color:var(--color-text-disabled);">Lade…</div>
|
||||
<div style="padding:2rem;text-align:center;color:var(--color-text-disabled);">${t('budget.loadingIndicator')}</div>
|
||||
</div>
|
||||
<button class="page-fab" id="fab-new-budget" aria-label="Neuer Eintrag">
|
||||
<button class="page-fab" id="fab-new-budget" aria-label="${t('budget.newEntryFabLabel')}">
|
||||
<i data-lucide="plus" style="width:24px;height:24px" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -171,17 +184,17 @@ function renderBody() {
|
||||
<!-- Zusammenfassung -->
|
||||
<div class="budget-summary">
|
||||
<div class="budget-summary-card budget-summary-card--income">
|
||||
<div class="budget-summary-card__label">Einnahmen</div>
|
||||
<div class="budget-summary-card__label">${t('budget.income')}</div>
|
||||
<div class="budget-summary-card__amount">${formatAmount(s.income)}</div>
|
||||
${p ? renderTrend(s.income, p.income, prevLabel) : ''}
|
||||
</div>
|
||||
<div class="budget-summary-card budget-summary-card--expenses">
|
||||
<div class="budget-summary-card__label">Ausgaben</div>
|
||||
<div class="budget-summary-card__label">${t('budget.expenses')}</div>
|
||||
<div class="budget-summary-card__amount">${formatAmount(Math.abs(s.expenses))}</div>
|
||||
${p ? renderTrend(s.expenses, p.expenses, prevLabel) : ''}
|
||||
</div>
|
||||
<div class="budget-summary-card ${balanceClass}">
|
||||
<div class="budget-summary-card__label">Saldo</div>
|
||||
<div class="budget-summary-card__label">${t('budget.balance')}</div>
|
||||
<div class="budget-summary-card__amount">${formatAmount(s.balance)}</div>
|
||||
${p ? renderTrend(s.balance, p.balance, prevLabel) : ''}
|
||||
</div>
|
||||
@@ -190,7 +203,7 @@ function renderBody() {
|
||||
<!-- Kategorie-Balken -->
|
||||
${s.byCategory.length ? `
|
||||
<div class="budget-chart-section">
|
||||
<div class="budget-chart-section__title">Nach Kategorie</div>
|
||||
<div class="budget-chart-section__title">${t('budget.byCategory')}</div>
|
||||
<div class="budget-chart">
|
||||
${renderCategoryBars(s.byCategory)}
|
||||
</div>
|
||||
@@ -199,7 +212,7 @@ function renderBody() {
|
||||
<!-- Transaktionsliste -->
|
||||
<div class="budget-list-section">
|
||||
<div class="budget-list-header">
|
||||
<span class="budget-list-header__title">Transaktionen</span>
|
||||
<span class="budget-list-header__title">${t('budget.transactions')}</span>
|
||||
${state.entries.length ? `
|
||||
<a href="/api/v1/budget/export?month=${state.month}" class="btn btn--secondary"
|
||||
style="font-size:var(--text-sm);padding:var(--space-1) var(--space-3);">
|
||||
@@ -256,8 +269,8 @@ function renderEntries() {
|
||||
<line x1="12" y1="1" x2="12" y2="23"/>
|
||||
<path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>
|
||||
</svg>
|
||||
<div class="empty-state__title">Keine Einträge diesen Monat</div>
|
||||
<div class="empty-state__description">Budget-Einträge über den + Button hinzufügen.</div>
|
||||
<div class="empty-state__title">${t('budget.emptyTitle')}</div>
|
||||
<div class="empty-state__description">${t('budget.emptyDescription')}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -277,7 +290,7 @@ function renderEntries() {
|
||||
<div class="budget-entry__meta">${date} · ${escHtml(e.category)}${recurTag}</div>
|
||||
</div>
|
||||
<div class="budget-entry__amount ${amtClass}">${sign}${formatAmount(e.amount)}</div>
|
||||
<button class="budget-entry__delete" data-action="delete" data-id="${e.id}" aria-label="Eintrag löschen">
|
||||
<button class="budget-entry__delete" data-action="delete" data-id="${e.id}" aria-label="${t('budget.deleteLabel')}">
|
||||
<i data-lucide="trash-2" style="width:14px;height:14px;" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -298,7 +311,7 @@ function renderEntries() {
|
||||
function renderTrend(current, prev, prevLabel) {
|
||||
const delta = current - prev;
|
||||
if (Math.abs(delta) < 0.005) {
|
||||
return `<div class="budget-summary-card__trend budget-summary-card__trend--neutral">— wie ${prevLabel}</div>`;
|
||||
return `<div class="budget-summary-card__trend budget-summary-card__trend--neutral">${t('budget.trendNeutral', { month: prevLabel })}</div>`;
|
||||
}
|
||||
const positive = delta > 0;
|
||||
const arrow = positive ? '▲' : '▼';
|
||||
@@ -323,38 +336,39 @@ function openBudgetModal({ mode, entry = null }) {
|
||||
const isExpense = isEdit ? entry.amount < 0 : true;
|
||||
const absAmount = isEdit ? Math.abs(entry.amount).toFixed(2) : '';
|
||||
|
||||
const catLabels = CATEGORY_LABELS();
|
||||
const catOpts = CATEGORIES.map((c) =>
|
||||
`<option value="${c}" ${isEdit && entry.category === c ? 'selected' : ''}>${c}</option>`
|
||||
`<option value="${c}" ${isEdit && entry.category === c ? 'selected' : ''}>${catLabels[c] || c}</option>`
|
||||
).join('');
|
||||
|
||||
const content = `
|
||||
<div class="amount-type-toggle">
|
||||
<button class="amount-type-btn amount-type-btn--expenses ${isExpense ? 'amount-type-btn--active' : ''}"
|
||||
id="type-expense" type="button">Ausgabe</button>
|
||||
id="type-expense" type="button">${t('budget.typeExpense')}</button>
|
||||
<button class="amount-type-btn amount-type-btn--income ${!isExpense ? 'amount-type-btn--active' : ''}"
|
||||
id="type-income" type="button">Einnahme</button>
|
||||
id="type-income" type="button">${t('budget.typeIncome')}</button>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="bm-title">Titel *</label>
|
||||
<label class="form-label" for="bm-title">${t('budget.titleLabel')}</label>
|
||||
<input type="text" class="form-input" id="bm-title"
|
||||
placeholder="z.B. REWE Einkauf" value="${escHtml(isEdit ? entry.title : '')}">
|
||||
placeholder="${t('budget.titlePlaceholder')}" value="${escHtml(isEdit ? entry.title : '')}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="bm-amount">Betrag (€) *</label>
|
||||
<label class="form-label" for="bm-amount">${t('budget.amountLabel')}</label>
|
||||
<input type="number" class="form-input" id="bm-amount"
|
||||
placeholder="0,00" step="0.01" min="0"
|
||||
placeholder="${t('budget.amountPlaceholder')}" step="0.01" min="0"
|
||||
value="${absAmount}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="bm-category">Kategorie</label>
|
||||
<label class="form-label" for="bm-category">${t('budget.categoryLabel')}</label>
|
||||
<select class="form-input" id="bm-category">${catOpts}</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="bm-date">Datum *</label>
|
||||
<label class="form-label" for="bm-date">${t('budget.dateLabel')}</label>
|
||||
<input type="date" class="form-input" id="bm-date"
|
||||
value="${isEdit ? entry.date : today}">
|
||||
</div>
|
||||
@@ -362,22 +376,22 @@ function openBudgetModal({ mode, entry = null }) {
|
||||
<div class="form-group">
|
||||
<label class="allday-toggle">
|
||||
<input type="checkbox" id="bm-recurring" ${isEdit && entry.is_recurring ? 'checked' : ''}>
|
||||
<span class="allday-toggle__label">Wiederkehrend</span>
|
||||
<span class="allday-toggle__label">${t('budget.recurringLabel')}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="modal-panel__footer" style="border:none;padding:0;margin-top:var(--space-4)">
|
||||
${isEdit ? `<button class="btn btn--danger btn--icon" id="bm-delete" aria-label="Eintrag löschen">
|
||||
${isEdit ? `<button class="btn btn--danger btn--icon" id="bm-delete" aria-label="${t('budget.deleteLabel')}">
|
||||
<i data-lucide="trash-2" style="width:16px;height:16px;" aria-hidden="true"></i>
|
||||
</button>` : '<div></div>'}
|
||||
<div style="display:flex;gap:var(--space-3)">
|
||||
<button class="btn btn--secondary" id="bm-cancel">Abbrechen</button>
|
||||
<button class="btn btn--primary" id="bm-save">${isEdit ? 'Speichern' : 'Hinzufügen'}</button>
|
||||
<button class="btn btn--secondary" id="bm-cancel">${t('common.cancel')}</button>
|
||||
<button class="btn btn--primary" id="bm-save">${isEdit ? t('common.save') : t('common.add')}</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
openSharedModal({
|
||||
title: isEdit ? 'Eintrag bearbeiten' : 'Neuer Eintrag',
|
||||
title: isEdit ? t('budget.editEntry') : t('budget.newEntry'),
|
||||
content,
|
||||
size: 'sm',
|
||||
onSave(panel) {
|
||||
@@ -397,7 +411,7 @@ function openBudgetModal({ mode, entry = null }) {
|
||||
panel.querySelector('#bm-cancel').addEventListener('click', closeModal);
|
||||
|
||||
panel.querySelector('#bm-delete')?.addEventListener('click', async () => {
|
||||
if (!confirm(`"${entry.title}" wirklich löschen?`)) return;
|
||||
if (!confirm(t('budget.deletePersonConfirm', { title: entry.title }))) return;
|
||||
closeModal();
|
||||
await deleteEntry(entry.id);
|
||||
});
|
||||
@@ -410,9 +424,9 @@ function openBudgetModal({ mode, entry = null }) {
|
||||
const date = panel.querySelector('#bm-date').value;
|
||||
const recurring = panel.querySelector('#bm-recurring').checked ? 1 : 0;
|
||||
|
||||
if (!title) { window.oikos?.showToast('Titel ist erforderlich', 'error'); return; }
|
||||
if (isNaN(absVal) || absVal <= 0) { window.oikos?.showToast('Gültigen Betrag eingeben', 'error'); return; }
|
||||
if (!date) { window.oikos?.showToast('Datum ist erforderlich', 'error'); return; }
|
||||
if (!title) { window.oikos?.showToast(t('common.titleRequired'), 'error'); return; }
|
||||
if (isNaN(absVal) || absVal <= 0) { window.oikos?.showToast(t('budget.validAmountRequired'), 'error'); return; }
|
||||
if (!date) { window.oikos?.showToast(t('budget.dateRequired'), 'error'); return; }
|
||||
|
||||
const amount = currentType === 'expense' ? -absVal : absVal;
|
||||
|
||||
@@ -434,11 +448,11 @@ function openBudgetModal({ mode, entry = null }) {
|
||||
|
||||
closeModal();
|
||||
renderBody();
|
||||
window.oikos?.showToast(mode === 'create' ? 'Eintrag hinzugefügt' : 'Eintrag gespeichert', 'success');
|
||||
window.oikos?.showToast(mode === 'create' ? t('budget.addedToast') : t('budget.savedToast'), 'success');
|
||||
} catch (err) {
|
||||
window.oikos?.showToast(err.data?.error ?? 'Fehler', 'error');
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.textContent = isEdit ? 'Speichern' : 'Hinzufügen';
|
||||
saveBtn.textContent = isEdit ? t('common.save') : t('common.add');
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -450,7 +464,7 @@ function openBudgetModal({ mode, entry = null }) {
|
||||
// --------------------------------------------------------
|
||||
|
||||
async function deleteEntry(id) {
|
||||
if (!confirm('Eintrag wirklich löschen?')) return;
|
||||
if (!confirm(t('budget.deleteConfirm'))) return;
|
||||
try {
|
||||
await api.delete(`/budget/${id}`);
|
||||
state.entries = state.entries.filter((e) => e.id !== id);
|
||||
@@ -458,7 +472,7 @@ async function deleteEntry(id) {
|
||||
state.summary = sumRes.data;
|
||||
renderBody();
|
||||
vibrate([30, 50, 30]);
|
||||
window.oikos?.showToast('Eintrag gelöscht', 'success');
|
||||
window.oikos?.showToast(t('budget.deletedToast'), 'success');
|
||||
} catch (err) {
|
||||
window.oikos?.showToast(err.data?.error ?? 'Fehler', 'error');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user