Feature: Ausgaben-Formular redesigned — Kategorie-Kacheln, €-Prefix, Wiederholungs-Toggle, SW by-v607
This commit is contained in:
parent
c96e98917c
commit
dfd68f2a07
4 changed files with 173 additions and 40 deletions
|
|
@ -7435,3 +7435,84 @@ svg.empty-state-icon {
|
||||||
}
|
}
|
||||||
.exp-icon-btn:hover { color: var(--c-text); border-color: var(--c-text-muted); }
|
.exp-icon-btn:hover { color: var(--c-text); border-color: var(--c-text-muted); }
|
||||||
.exp-icon-btn--danger:hover { color: var(--c-danger); border-color: var(--c-danger); }
|
.exp-icon-btn--danger:hover { color: var(--c-danger); border-color: var(--c-danger); }
|
||||||
|
|
||||||
|
/* Ausgaben-Formular — Kategorie-Kacheln */
|
||||||
|
.exp-kat-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
.exp-kat-tile {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-1);
|
||||||
|
padding: var(--space-3) var(--space-2);
|
||||||
|
border: 1.5px solid var(--c-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color .15s, background .15s;
|
||||||
|
background: var(--c-surface);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.exp-kat-tile:hover { border-color: var(--c-primary); }
|
||||||
|
.exp-kat-tile--sel {
|
||||||
|
border-color: var(--c-primary);
|
||||||
|
background: var(--c-primary-subtle);
|
||||||
|
}
|
||||||
|
.exp-kat-tile-icon { font-size: 1.4rem; line-height: 1; }
|
||||||
|
.exp-kat-tile-label {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
font-weight: var(--weight-medium);
|
||||||
|
color: var(--c-text-secondary);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.exp-kat-tile--sel .exp-kat-tile-label { color: var(--c-primary); }
|
||||||
|
|
||||||
|
/* Betrag-Feld mit €-Prefix */
|
||||||
|
.exp-betrag-wrap {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.exp-betrag-prefix {
|
||||||
|
position: absolute;
|
||||||
|
left: var(--space-3);
|
||||||
|
color: var(--c-text-muted);
|
||||||
|
font-weight: var(--weight-semibold);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.exp-betrag-input { padding-left: calc(var(--space-3) + 14px + var(--space-2)) !important; }
|
||||||
|
|
||||||
|
/* Form-Label Hint */
|
||||||
|
.form-label-hint { color: var(--c-text-muted); font-weight: normal; font-size: var(--text-xs); }
|
||||||
|
|
||||||
|
/* Wiederholungs-Sektion */
|
||||||
|
.exp-repeat-section {
|
||||||
|
margin-top: var(--space-4);
|
||||||
|
padding-top: var(--space-4);
|
||||||
|
border-top: 1px solid var(--c-border-light);
|
||||||
|
}
|
||||||
|
.exp-repeat-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: var(--weight-medium);
|
||||||
|
color: var(--c-text);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.exp-repeat-toggle-box {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border: 1.5px solid var(--c-border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--c-surface);
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: background .15s, border-color .15s;
|
||||||
|
}
|
||||||
|
.exp-repeat-toggle input:checked ~ .exp-repeat-toggle-box {
|
||||||
|
background: var(--c-primary);
|
||||||
|
border-color: var(--c-primary);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Router, State-Management, Navigation, Initialisierung.
|
Router, State-Management, Navigation, Initialisierung.
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const APP_VER = '606'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
const APP_VER = '607'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||||
const APP_VERSION = '1.2.1'; // ← semantische Version, wird bei make release gesetzt
|
const APP_VERSION = '1.2.1'; // ← semantische Version, wird bei make release gesetzt
|
||||||
const IS_STAGING = location.hostname === 'staging.banyaro.app';
|
const IS_STAGING = location.hostname === 'staging.banyaro.app';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -641,64 +641,106 @@ window.Page_expenses = (() => {
|
||||||
const isEdit = !!entry;
|
const isEdit = !!entry;
|
||||||
const today = new Date().toISOString().split('T')[0];
|
const today = new Date().toISOString().split('T')[0];
|
||||||
const formId = 'exp-form';
|
const formId = 'exp-form';
|
||||||
|
const selKat = entry?.kategorie || 'sonstiges';
|
||||||
|
|
||||||
const dogOptions = (_appState.dogs || []).map(d =>
|
const dogOptions = (_appState.dogs || []).map(d =>
|
||||||
`<option value="${d.id}"${entry?.dog_id === d.id ? ' selected' : ''}>${_esc(d.name)}</option>`
|
`<option value="${d.id}"${entry?.dog_id === d.id ? ' selected' : ''}>${_esc(d.name)}</option>`
|
||||||
).join('');
|
).join('');
|
||||||
|
|
||||||
const katOptions = KATEGORIEN.map(k =>
|
// Kategorie-Kacheln statt Dropdown
|
||||||
`<option value="${k.id}"${(entry?.kategorie || 'sonstiges') === k.id ? ' selected' : ''}>
|
const katKacheln = KATEGORIEN.map(k => `
|
||||||
${k.label}
|
<label class="exp-kat-tile${selKat === k.id ? ' exp-kat-tile--sel' : ''}" data-kat="${k.id}">
|
||||||
</option>`
|
<input type="radio" name="kategorie" value="${k.id}" ${selKat === k.id ? 'checked' : ''} style="display:none">
|
||||||
).join('');
|
<span class="exp-kat-tile-icon" style="color:${k.color}">${UI.icon(k.icon)}</span>
|
||||||
|
<span class="exp-kat-tile-label">${k.label}</span>
|
||||||
|
</label>`).join('');
|
||||||
|
|
||||||
const body = `
|
const body = `
|
||||||
<form id="${formId}">
|
<form id="${formId}" autocomplete="off">
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Datum</label>
|
|
||||||
<input type="date" name="datum" class="form-input"
|
|
||||||
value="${entry?.datum || today}" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Kategorie</label>
|
<label class="form-label">Kategorie</label>
|
||||||
<select name="kategorie" class="form-input" required>
|
<div class="exp-kat-grid">${katKacheln}</div>
|
||||||
${katOptions}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Betrag (€)</label>
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
|
||||||
<input type="number" name="betrag" class="form-input"
|
<div class="form-group" style="margin-bottom:0">
|
||||||
value="${entry?.betrag || ''}"
|
<label class="form-label">Betrag</label>
|
||||||
min="0.01" step="0.01" placeholder="0,00" required>
|
<div class="exp-betrag-wrap">
|
||||||
|
<span class="exp-betrag-prefix">€</span>
|
||||||
|
<input type="number" name="betrag" class="form-control exp-betrag-input"
|
||||||
|
value="${entry?.betrag || ''}" min="0.01" step="0.01"
|
||||||
|
placeholder="0,00" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom:0">
|
||||||
|
<label class="form-label">Datum</label>
|
||||||
|
<input type="date" name="datum" class="form-control"
|
||||||
|
value="${entry?.datum || today}" required>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${dogOptions ? `
|
${dogOptions ? `
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Hund (optional)</label>
|
<label class="form-label">Hund <span class="form-label-hint">(optional)</span></label>
|
||||||
<select name="dog_id" class="form-input">
|
<select name="dog_id" class="form-control">
|
||||||
<option value="">— kein Hund zugeordnet —</option>
|
<option value="">— kein Hund —</option>${dogOptions}
|
||||||
${dogOptions}
|
|
||||||
</select>
|
</select>
|
||||||
</div>` : ''}
|
</div>` : ''}
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Notiz (optional)</label>
|
<label class="form-label">Notiz <span class="form-label-hint">(optional)</span></label>
|
||||||
<input type="text" name="notiz" class="form-input"
|
<input type="text" name="notiz" class="form-control"
|
||||||
value="${_esc(entry?.notiz || '')}"
|
value="${_esc(entry?.notiz || '')}"
|
||||||
placeholder="z. B. Impfung, Trockenfutter Vorrat …">
|
placeholder="z.B. Hundesteuer 2026, Allianz Haftpflicht …">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
${!isEdit ? `
|
||||||
|
<div class="exp-repeat-section">
|
||||||
|
<label class="exp-repeat-toggle">
|
||||||
|
<input type="checkbox" id="exp-wiederholen" name="wiederholen">
|
||||||
|
<span class="exp-repeat-toggle-box"></span>
|
||||||
|
<span>${UI.icon('arrows-clockwise')} Automatisch wiederholen</span>
|
||||||
|
</label>
|
||||||
|
<div id="exp-repeat-opts" style="display:none;margin-top:var(--space-3)">
|
||||||
|
<select name="haeufigkeit" class="form-control">
|
||||||
|
<option value="monatlich">Monatlich</option>
|
||||||
|
<option value="quartalsweise">Quartalsweise (alle 3 Monate)</option>
|
||||||
|
<option value="jaehrlich" selected>Jährlich</option>
|
||||||
|
</select>
|
||||||
|
<p style="font-size:var(--text-xs);color:var(--c-text-muted);margin:var(--space-2) 0 0">
|
||||||
|
Der Betrag wird automatisch zum Fälligkeitstermin eingetragen.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>` : ''}
|
||||||
|
|
||||||
</form>`;
|
</form>`;
|
||||||
|
|
||||||
const footer = isEdit ? `
|
const footer = isEdit ? `
|
||||||
<button type="button" class="btn btn-danger" id="exp-delete-btn">Löschen</button>
|
<button type="button" class="btn btn-ghost btn-sm" id="exp-delete-btn"
|
||||||
<button type="submit" form="${formId}" class="btn btn-primary">Speichern</button>
|
style="color:var(--c-danger);margin-right:auto">
|
||||||
` : `
|
${UI.icon('trash')}
|
||||||
|
</button>
|
||||||
<button type="button" class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
|
<button type="button" class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
|
||||||
<button type="submit" form="${formId}" class="btn btn-primary">Speichern</button>
|
<button type="submit" form="${formId}" class="btn btn-primary">Speichern</button>
|
||||||
|
` : `
|
||||||
|
<button type="button" class="btn btn-secondary flex-1" onclick="UI.modal.close()">Abbrechen</button>
|
||||||
|
<button type="submit" form="${formId}" class="btn btn-primary flex-1">Speichern</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const modal = UI.modal.open({
|
const modal = UI.modal.open({ title: isEdit ? 'Ausgabe bearbeiten' : 'Neue Ausgabe', body, footer });
|
||||||
title: isEdit ? 'Ausgabe bearbeiten' : 'Neue Ausgabe',
|
|
||||||
body,
|
// Kategorie-Kacheln interaktiv
|
||||||
footer,
|
modal.querySelectorAll('.exp-kat-tile').forEach(tile => {
|
||||||
|
tile.addEventListener('click', () => {
|
||||||
|
modal.querySelectorAll('.exp-kat-tile').forEach(t => t.classList.remove('exp-kat-tile--sel'));
|
||||||
|
tile.classList.add('exp-kat-tile--sel');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wiederholen-Toggle
|
||||||
|
modal.querySelector('#exp-wiederholen')?.addEventListener('change', e => {
|
||||||
|
modal.querySelector('#exp-repeat-opts').style.display = e.target.checked ? 'block' : 'none';
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
|
|
@ -718,8 +760,8 @@ window.Page_expenses = (() => {
|
||||||
|
|
||||||
modal.querySelector(`#${formId}`)?.addEventListener('submit', async (ev) => {
|
modal.querySelector(`#${formId}`)?.addEventListener('submit', async (ev) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
const fd = UI.formData(ev.target);
|
const fd = UI.formData(ev.target);
|
||||||
const body = {
|
const payload = {
|
||||||
kategorie: fd.kategorie,
|
kategorie: fd.kategorie,
|
||||||
betrag: parseFloat(fd.betrag),
|
betrag: parseFloat(fd.betrag),
|
||||||
datum: fd.datum,
|
datum: fd.datum,
|
||||||
|
|
@ -729,11 +771,21 @@ window.Page_expenses = (() => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
await API.patch(`/expenses/${entry.id}`, body);
|
await API.patch(`/expenses/${entry.id}`, payload);
|
||||||
UI.toast.success('Ausgabe aktualisiert.');
|
UI.toast.success('Ausgabe aktualisiert.');
|
||||||
} else {
|
} else {
|
||||||
await API.post('/expenses', body);
|
await API.post('/expenses', payload);
|
||||||
UI.toast.success('Ausgabe gespeichert.');
|
// Auch als Dauerauftrag anlegen wenn gewünscht
|
||||||
|
if (fd.wiederholen) {
|
||||||
|
await API.post('/expenses/recurring', {
|
||||||
|
...payload,
|
||||||
|
haeufigkeit: fd.haeufigkeit || 'jaehrlich',
|
||||||
|
startdatum: fd.datum,
|
||||||
|
});
|
||||||
|
UI.toast.success('Ausgabe + Dauerauftrag gespeichert.');
|
||||||
|
} else {
|
||||||
|
UI.toast.success('Ausgabe gespeichert.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
UI.modal.close();
|
UI.modal.close();
|
||||||
_invalidateCache();
|
_invalidateCache();
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Offline-Cache + Push Notifications + Tile-Cache
|
Offline-Cache + Push Notifications + Tile-Cache
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const CACHE_VERSION = 'by-v606';
|
const CACHE_VERSION = 'by-v607';
|
||||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||||
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
||||||
const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache
|
const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue