/**
* 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'));
}
});
// ── 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));
}
// ── 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);
}
});
// ── 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);
});
});