/** * Steuerkram – App JavaScript * Kein Framework. Nur native DOM-APIs. */ 'use strict'; // ── Kontenart-Badge neben Label anzeigen ───────────────────── function showKontoArt(selectId, badgeId, sel) { const badge = document.getElementById(badgeId); if (!badge) return; const opt = sel.options[sel.selectedIndex]; const art = opt?.dataset?.art || ''; const classes = { 'Aufwand': 'badge-red', 'Ertrag': 'badge-green', 'Aktiv': 'badge-blue', 'Passiv': 'badge-yellow', 'Neutral': 'badge-gray', }; badge.textContent = art; badge.className = 'badge ' + (classes[art] || 'badge-gray'); badge.style.display = art ? '' : 'none'; } // ── Konto-Filter (Select-Filterung per Sucheingabe) ─────────── document.addEventListener('input', (e) => { if (!e.target.classList.contains('konto-filter')) return; const q = e.target.value.toLowerCase(); const select = document.getElementById(e.target.dataset.target); if (!select) return; for (const opt of select.options) { if (opt.value === '') { opt.hidden = false; continue; } opt.hidden = !opt.dataset.search.toLowerCase().includes(q); } // Auto-select first visible match if (q.length > 0) { for (const opt of select.options) { if (!opt.hidden && opt.value !== '') { select.value = opt.value; break; } } } }); // ── Modals ──────────────────────────────────────────────────── document.addEventListener('click', (e) => { // Open modal const openBtn = e.target.closest('[data-modal-open]'); if (openBtn) { const id = openBtn.dataset.modalOpen; document.getElementById(id)?.classList.add('open'); return; } // Close modal via backdrop click or close button const closeBtn = e.target.closest('[data-modal-close]'); if (closeBtn) { closeBtn.closest('.modal-overlay')?.classList.remove('open'); return; } if (e.target.classList.contains('modal-overlay')) { e.target.classList.remove('open'); } }); // Close modal on Escape key document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { document.querySelectorAll('.modal-overlay.open').forEach(m => m.classList.remove('open')); } }); // ── Confirm Delete ──────────────────────────────────────────── document.addEventListener('submit', (e) => { const form = e.target; const msg = form.dataset.confirm; if (msg && !confirm(msg)) { e.preventDefault(); } }); // ── Rechnungszeilen – Live-Kalkulation ──────────────────────── function calcInvoiceLine(row) { const menge = parseFloat(row.querySelector('[name$="[menge]"]')?.value) || 0; const preis = parseFloat(row.querySelector('[name$="[einzelpreis]"]')?.value) || 0; const ustSatz = parseFloat(row.querySelector('[name$="[ust_satz]"]')?.value) || 0; const netto = menge * preis; const ust = netto * ustSatz / 100; const brutto = netto + ust; const fmt = (v) => v.toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); const nettoEl = row.querySelector('.line-netto'); const bruttoEl = row.querySelector('.line-brutto'); if (nettoEl) nettoEl.textContent = fmt(netto) + ' €'; if (bruttoEl) bruttoEl.textContent = fmt(brutto) + ' €'; } function calcInvoiceTotals() { let totalNetto = 0; let totalUst = 0; let totalBrutto = 0; document.querySelectorAll('.invoice-line-row').forEach(row => { const menge = parseFloat(row.querySelector('[name$="[menge]"]')?.value) || 0; const preis = parseFloat(row.querySelector('[name$="[einzelpreis]"]')?.value) || 0; const ustSatz = parseFloat(row.querySelector('[name$="[ust_satz]"]')?.value) || 0; const netto = menge * preis; const ust = netto * ustSatz / 100; totalNetto += netto; totalUst += ust; totalBrutto += netto + ust; calcInvoiceLine(row); }); const fmt = (v) => v.toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' €'; const elNetto = document.getElementById('total-netto'); const elUst = document.getElementById('total-ust'); const elBrutto = document.getElementById('total-brutto'); if (elNetto) elNetto.textContent = fmt(totalNetto); if (elUst) elUst.textContent = fmt(totalUst); if (elBrutto) elBrutto.textContent = fmt(totalBrutto); } // Live-Update on input change in invoice form document.addEventListener('input', (e) => { if (e.target.closest('.invoice-line-row')) { calcInvoiceTotals(); } if (e.target.closest('.invoice-line-row') && e.target.matches('[name$="[ust_satz]"]')) { calcInvoiceTotals(); } }); // ── Rechnungszeile hinzufügen ───────────────────────────────── let lineCounter = 0; function addInvoiceLine() { lineCounter++; const tbody = document.getElementById('invoice-lines-body'); if (!tbody) return; const idx = tbody.querySelectorAll('.invoice-line-row').length; const row = document.createElement('tr'); row.className = 'invoice-line-row'; row.innerHTML = ` ${idx + 1} 0,00 € 0,00 € `; tbody.appendChild(row); calcInvoiceTotals(); } function removeInvoiceLine(btn) { btn.closest('.invoice-line-row').remove(); // Renumber positions document.querySelectorAll('.invoice-line-row').forEach((row, i) => { const posEl = row.querySelector('.line-pos'); if (posEl) posEl.textContent = i + 1; // Update indices in name attributes row.querySelectorAll('[name]').forEach(el => { el.name = el.name.replace(/lines\[\d+\]/, `lines[${i}]`); }); }); calcInvoiceTotals(); } // Init totals on page load document.addEventListener('DOMContentLoaded', () => { if (document.getElementById('invoice-lines-body')) { calcInvoiceTotals(); } }); // ── Konto-Suche (Typeahead) ─────────────────────────────────── function setupAccountSearch(inputId, hiddenId) { const input = document.getElementById(inputId); const hidden = document.getElementById(hiddenId); if (!input || !hidden) return; let dropdown = null; let debounce = null; input.addEventListener('input', () => { clearTimeout(debounce); const q = input.value.trim(); if (q.length < 1) { closeDropdown(); return; } debounce = setTimeout(async () => { const res = await fetch(`/accounts/search?q=${encodeURIComponent(q)}`); const data = await res.json(); renderDropdown(data); }, 200); }); function renderDropdown(items) { closeDropdown(); if (!items.length) return; dropdown = document.createElement('ul'); dropdown.className = 'account-dropdown'; dropdown.style.cssText = ` position:absolute; z-index:50; background:#fff; border:1px solid var(--color-border); border-radius:var(--radius); width:100%; max-height:220px; overflow-y:auto; list-style:none; padding:0; margin:0; box-shadow:var(--shadow); `; items.forEach(item => { const li = document.createElement('li'); li.style.cssText = 'padding:.5rem .75rem; cursor:pointer; font-size:.875rem; border-bottom:1px solid var(--color-border)'; li.innerHTML = `${item.kontonummer} – ${item.bezeichnung} (${item.kontenart})`; li.addEventListener('mousedown', () => { input.value = `${item.kontonummer} – ${item.bezeichnung}`; hidden.value = item.id; closeDropdown(); }); dropdown.appendChild(li); }); const wrapper = input.closest('.account-search-wrap') || input.parentElement; wrapper.style.position = 'relative'; wrapper.appendChild(dropdown); } function closeDropdown() { dropdown?.remove(); dropdown = null; } input.addEventListener('blur', () => setTimeout(closeDropdown, 150)); } // ── Flash Message Auto-dismiss ──────────────────────────────── document.addEventListener('DOMContentLoaded', () => { document.querySelectorAll('.flash').forEach(el => { setTimeout(() => { el.style.transition = 'opacity .4s'; el.style.opacity = '0'; setTimeout(() => el.remove(), 400); }, 4000); }); });