/** * 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 nur bei exaktem Kontonummer-Treffer; sonst Select leeren // (verhindert, dass direkte Kontonummer-Eingabe durch einen Partial-Match überschrieben wird) if (q.length > 0) { const exact = [...select.options].find( opt => opt.value !== '' && opt.dataset.search && opt.dataset.search.split(' ')[0] === q ); select.value = exact ? exact.value : ''; select.dispatchEvent(new Event('change')); } }); // ── 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')); } }); // ── Klickbare Tabellenzeilen (Navigation) ────────────────────── document.addEventListener('click', (e) => { const row = e.target.closest('tr[data-row-href]'); if (!row || e.target.closest('[data-no-row-click]')) return; window.location = row.dataset.rowHref; }); // ── 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 / Überschrift hinzufügen ─────────────────── document.addEventListener('click', (e) => { const el = e.target.closest('[data-action]'); if (!el) return; switch (el.dataset.action) { case 'show-add-line-dialog': showAddLineDialog(el); break; case 'add-invoice-line': document.getElementById('add-line-popup')?.remove(); addInvoiceLine(); break; case 'add-invoice-heading': document.getElementById('add-line-popup')?.remove(); addInvoiceHeading(); break; case 'remove-invoice-line': removeInvoiceLine(el); break; } }); function showAddLineDialog(btn) { document.getElementById('add-line-popup')?.remove(); const popup = document.createElement('div'); popup.id = 'add-line-popup'; popup.style.cssText = 'position:absolute;z-index:200;background:#fff;border:1px solid #ccc;border-radius:5px;box-shadow:0 2px 8px rgba(0,0,0,.12);overflow:hidden;font-size:.875rem'; popup.innerHTML = ` `; document.body.appendChild(popup); const r = btn.getBoundingClientRect(); popup.style.top = (window.scrollY + r.bottom + 3) + 'px'; popup.style.left = r.left + 'px'; popup.style.minWidth = r.width + 'px'; setTimeout(() => { document.addEventListener('click', function close(e) { if (!popup.contains(e.target) && e.target !== btn) { popup.remove(); document.removeEventListener('click', close); } }); }, 0); } function _nextLineIdx() { const tbody = document.getElementById('invoice-lines-body'); return tbody ? tbody.querySelectorAll('tr').length : 0; } function addInvoiceLine() { const tbody = document.getElementById('invoice-lines-body'); if (!tbody) return; const idx = _nextLineIdx(); const row = document.createElement('tr'); row.className = 'invoice-line-row'; row.innerHTML = ` 0,00 € 0,00 € `; tbody.appendChild(row); renumberLines(); calcInvoiceTotals(); const ta = row.querySelector('.line-bez-input'); if (ta) { initBezTextarea(ta); ta.focus(); } } function addInvoiceHeading() { const tbody = document.getElementById('invoice-lines-body'); if (!tbody) return; const idx = _nextLineIdx(); const row = document.createElement('tr'); row.className = 'invoice-heading-row'; row.innerHTML = ` `; tbody.appendChild(row); renumberLines(); const ta = row.querySelector('.line-bez-input'); if (ta) { initBezTextarea(ta); ta.focus(); } } function removeInvoiceLine(btn) { btn.closest('tr').remove(); renumberLines(); calcInvoiceTotals(); } function renumberLines() { const tbody = document.getElementById('invoice-lines-body'); if (!tbody) return; let posNum = 0; tbody.querySelectorAll('tr').forEach((row, i) => { // Alle name-Attribute auf neuen Array-Index setzen row.querySelectorAll('[name]').forEach(el => { el.name = el.name.replace(/lines\[\d+\]/, `lines[${i}]`); }); // Positions-Nummer nur bei echten Zeilen hochzählen const posEl = row.querySelector('.line-pos'); if (posEl) posEl.textContent = ++posNum; }); } // 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)'; // Kontodaten nur als Text einfügen (Fix #213) const nr = document.createElement('strong'); nr.textContent = item.kontonummer; const art = document.createElement('small'); art.className = 'text-muted'; art.textContent = `(${item.kontenart})`; li.append(nr, ` – ${item.bezeichnung} `, art); 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)); } // ── Inline-Edit (Belegdatum / Rechnungsnr. in der Info-Bar) ────── document.addEventListener('click', (e) => { const trigger = e.target.closest('.inline-edit-trigger'); if (!trigger || trigger.querySelector('input')) return; const bar = trigger.closest('[data-beleg-id]'); if (!bar) return; const field = trigger.dataset.field; const type = trigger.dataset.type || 'text'; const rawValue = trigger.dataset.raw || ''; const input = document.createElement('input'); input.type = type; input.value = rawValue; input.style.cssText = 'font-size:inherit;padding:2px 5px;border:1px solid var(--color-border);border-radius:var(--radius);background:#fff;min-width:' + (type === 'date' ? '140px' : '180px'); if (field === 'supplier_name') { input.setAttribute('list', 'supplier-datalist'); input.setAttribute('autocomplete', 'off'); } const originalHTML = trigger.innerHTML; trigger.innerHTML = ''; trigger.appendChild(input); input.focus(); if (type === 'text') input.select(); let cancelled = false; async function save() { if (cancelled) return; const newValue = input.value.trim(); const body = new URLSearchParams({ rechnungs_nr: field === 'rechnungs_nr' ? newValue : bar.dataset.rechnungsNr || '', belegtyp: bar.dataset.belegtyp || '', beleg_datum: field === 'beleg_datum' ? newValue : bar.dataset.belegDatum || '', betrag_brutto: bar.dataset.betragBrutto || '', supplier_name: field === 'supplier_name' ? newValue : bar.dataset.supplierName || '', }); try { const res = await fetch(`/belege/${bar.dataset.belegId}/meta`, { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/x-www-form-urlencoded' }, body: body.toString(), }); if (!res.ok) throw new Error(); // Update stored data-attribute if (field === 'beleg_datum') { bar.dataset.belegDatum = newValue; trigger.dataset.raw = newValue; if (newValue) { const [y, m, d] = newValue.split('-'); trigger.textContent = `${d}.${m}.${y}`; } else { trigger.innerHTML = '– setzen –'; } } else if (field === 'supplier_name') { bar.dataset.supplierName = newValue; trigger.dataset.raw = newValue; trigger.textContent = newValue || ''; if (!newValue) trigger.innerHTML = '– setzen –'; } else { bar.dataset.rechnungsNr = newValue; trigger.dataset.raw = newValue; trigger.textContent = newValue || ''; if (!newValue) trigger.innerHTML = '– setzen –'; } } catch (_) { trigger.innerHTML = originalHTML; } } input.addEventListener('blur', save); input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); input.blur(); } if (e.key === 'Escape') { cancelled = true; trigger.innerHTML = originalHTML; } }); }); // ── Konto-Select: Doppelklick / Enter → Kontonr. ins Filterfeld ── // selectedIndex ist beim click-Event garantiert korrekt; beim dblclick-Event // kann er in manchen Browsern bei List-Boxes zurückgesetzt sein (kein Prior-Select), // daher wird er beim click gecacht und als Fallback genutzt. const _kontoNrCache = {}; document.addEventListener('click', (e) => { const sel = e.target.closest('select#kategorie_konto_id, select#zahlungskonto_id'); if (!sel) return; const opt = sel.options[sel.selectedIndex]; if (opt && opt.value) _kontoNrCache[sel.id] = opt.textContent.split(' – ')[0].trim(); }); function transferKontoNr(select) { const opt = select.options[select.selectedIndex]; const kontoNr = (opt && opt.value) ? opt.textContent.split(' – ')[0].trim() : (_kontoNrCache[select.id] ?? null); if (!kontoNr) return; const filterInput = document.querySelector(`.konto-filter[data-target="${select.id}"]`); if (filterInput) filterInput.value = kontoNr; } document.addEventListener('dblclick', (e) => { const select = e.target.closest('select#kategorie_konto_id, select#zahlungskonto_id'); if (select) transferKontoNr(select); }); document.addEventListener('keydown', (e) => { if (e.key !== 'Enter') return; const select = e.target.closest('select#kategorie_konto_id, select#zahlungskonto_id'); if (select) { e.preventDefault(); transferKontoNr(select); } }); // ── Rechnungsformular: Enter nicht als Submit, Textarea auto-resize ─────────── function initBezTextarea(el) { el.style.height = 'auto'; el.style.height = el.scrollHeight + 'px'; } document.addEventListener('input', (e) => { if (e.target.classList.contains('line-bez-input')) initBezTextarea(e.target); if (e.target.classList.contains('autosize-textarea')) initBezTextarea(e.target); }); document.addEventListener('keydown', (e) => { if (e.key !== 'Enter') return; if (e.target.tagName === 'TEXTAREA') return; const form = e.target.closest('form.invoice-form'); if (form) e.preventDefault(); }, true); document.addEventListener('DOMContentLoaded', () => { document.querySelectorAll('.line-bez-input').forEach(initBezTextarea); }); // ── Flash Message Auto-dismiss ──────────────────────────────── document.addEventListener('DOMContentLoaded', () => { document.querySelectorAll('.flash').forEach(el => { if (el.closest('#ki-badge')) return; // KI-Badge nie auto-dismissen if (el.classList.contains('flash-persistent')) return; // z.B. Bewirtungshinweis setTimeout(() => { el.style.transition = 'opacity .4s'; el.style.opacity = '0'; setTimeout(() => el.remove(), 400); }, 20000); }); }); // ── Buchungsvorlagen ────────────────────────────────────────── // Vorlage auswählen → Modal per AJAX laden document.addEventListener('change', function(e) { const sel = e.target.closest('#vorlage-select'); if (!sel) return; const vorlageId = sel.value; if (!vorlageId) return; const belegId = sel.dataset.belegId || ''; const bankTxId = sel.dataset.bankTx || ''; let url = `/api/buchungsvorlagen/${vorlageId}/modal?beleg_id=${belegId}`; if (bankTxId) url += `&bank_tx=${bankTxId}`; fetch(url) .then(r => r.ok ? r.text() : Promise.reject(r.status)) .then(html => { const container = document.getElementById('vorlage-modal-container'); if (!container) return; container.innerHTML = html; // CSRF-Token in das dynamisch geladene Formular injizieren const token = (document.querySelector('meta[name="csrf-token"]') || {}).content || ''; container.querySelectorAll('form').forEach(function(form) { if (token && !form.querySelector('[name="csrf_token"]')) { var inp = document.createElement('input'); inp.type = 'hidden'; inp.name = 'csrf_token'; inp.value = token; form.appendChild(inp); } }); }) .catch(() => { // Selektion zurücksetzen bei Fehler sel.value = ''; }); }); // Modal schließen → Vorlage-Select zurücksetzen document.addEventListener('click', function(e) { const closeBtn = e.target.closest('[data-modal-close]'); if (!closeBtn) return; const overlay = closeBtn.closest('.modal-overlay'); if (overlay && overlay.id === 'vorlage-modal') { const sel = document.getElementById('vorlage-select'); if (sel) sel.value = ''; const container = document.getElementById('vorlage-modal-container'); if (container) setTimeout(() => { container.innerHTML = ''; }, 300); } }); // Live-Berechnungen in USt-Zeilen des Vorlage-Modals document.addEventListener('input', function(e) { const inp = e.target; if (!inp.matches('#vorlage-modal .vorlage-netto, #vorlage-modal .vorlage-brutto')) return; const row = inp.closest('.vorlage-ust-row'); if (!row) return; const pDec = v => parseFloat(String(v).replace(',', '.')) || 0; const satz = parseFloat(row.dataset.ustSatz) / 100; const netto = row.querySelector('.vorlage-netto'); const brutto = row.querySelector('.vorlage-brutto'); const ust = row.querySelector('.vorlage-ust'); const ustAnzeige = row.querySelector('.vorlage-ust-anzeige'); if (inp.classList.contains('vorlage-netto')) { const n = pDec(netto.value); brutto.value = (n * (1 + satz)).toFixed(2); ust.value = (n * satz).toFixed(2); } else { const b = pDec(brutto.value); const n = b / (1 + satz); netto.value = n.toFixed(2); ust.value = (b - n).toFixed(2); } if (ustAnzeige) ustAnzeige.textContent = parseFloat(ust.value).toFixed(2).replace('.', ','); vorlageUpdateBruttoGesamt(); }); function vorlageUpdateBruttoGesamt() { const modal = document.getElementById('vorlage-modal'); if (!modal) return; const bruttoFelder = modal.querySelectorAll('.vorlage-brutto'); const summe = Array.from(bruttoFelder).reduce((s, el) => s + (parseFloat(String(el.value).replace(',', '.')) || 0), 0); const anzeige = modal.querySelector('[data-feldname="brutto_gesamt"]'); if (anzeige) anzeige.textContent = summe.toFixed(2).replace('.', ',') + ' €'; } // Zahldatum-Feld Änderung → buchungs_datum-Hidden synchronisieren document.addEventListener('change', function(e) { if (!e.target.matches('#vorlage-modal #vorlage-zahldatum')) return; const hidden = document.getElementById('vorlage-buchungs-datum'); if (hidden) hidden.value = e.target.value; });