Finanzmanager/public/static/app.js
fds2610-bot 20b2406219 Budgetplaner: drei Ansichten und Verteilen über das Zahnrad
Abgelaufene Monate werden nicht mehr zusammengefasst – alle zwölf Monate
stehen einzeln da und bleiben beplanbar. Statt Verlauf und alle Monate
gibt es Alles, Nur Budget und Nur IST; die Budgetspalte ist überall dort
editierbar, wo sie steht.

Das Zahnrad vor jedem Feld verteilt dessen Wert auf das ganze oder das
restliche Jahr. Es arbeitet auf den Eingabefeldern statt auf der
Datenbank: Das Ergebnis ist vor dem Speichern zu sehen und geht mit
demselben Knopf weg wie jede andere Änderung.

Nebenbei: app.js war doppelt eingebunden, jeder Handler lief zweimal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 08:22:35 +02:00

161 lines
5.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* Finanzmanager – Frontend-Hilfen.
*
* Bewusst schlank und ohne Framework.
*
* Bestätigungen hängen an <form data-bestaetigen="…"> und nicht als onsubmit
* im HTML: Die Content-Security-Policy erlaubt keine Inline-Skripte, ein
* onsubmit-Attribut würde stillschweigend verworfen – und dann löschte der
* Knopf ohne Rückfrage.
*/
(function () {
'use strict';
// Formulare mit data-bestaetigen fragen vor dem Absenden nach.
// In der Capture-Phase, damit die Rückfrage auch dann greift, wenn ein
// anderer Handler das Ereignis unterwegs stoppt.
document.addEventListener('submit', function (ereignis) {
const formular = ereignis.target;
if (!(formular instanceof HTMLFormElement)) {
return;
}
const frage = formular.dataset.bestaetigen;
if (frage && !window.confirm(frage)) {
ereignis.preventDefault();
ereignis.stopPropagation();
}
}, true);
// Kategoriebaum im Budgetplaner auf- und zuklappen. Die Zeilen kennen ihre
// Elternkategorie über data-eltern; von dort aus lässt sich der Teilbaum
// einsammeln, ohne ihn im HTML zu verschachteln (in einer Tabelle ginge das
// ohnehin nicht).
document.querySelectorAll('.klappknopf').forEach(function (knopf) {
knopf.setAttribute('aria-expanded', 'true');
knopf.addEventListener('click', function () {
const wurzel = knopf.dataset.klappt;
const offen = knopf.getAttribute('aria-expanded') !== 'false';
knopf.setAttribute('aria-expanded', offen ? 'false' : 'true');
const zeilen = Array.from(document.querySelectorAll('tr.kategoriezeile'));
const betroffen = new Set([wurzel]);
zeilen.forEach(function (zeile) {
const eltern = zeile.dataset.eltern;
if (eltern && betroffen.has(eltern)) {
betroffen.add(zeile.dataset.kategorie);
zeile.classList.toggle('zugeklappt', offen);
}
});
});
});
// Zahnrad im Budgetplaner: verteilt den eingegebenen Wert über die Zeile,
// wahlweise auf das ganze oder nur auf das restliche Jahr. Gearbeitet wird
// auf den Eingabefeldern, nicht auf der Datenbank – so ist das Ergebnis vor
// dem Speichern zu sehen und geht mit demselben Knopf weg wie alles andere.
//
// Das Menü gibt es nur einmal und wandert in die angeklickte Zelle: Ein
// eigenes Menü je Feld wären zwölf Stück pro Kategoriezeile, und ein frei
// positioniertes bräuchte ein style-Attribut, das die
// Content-Security-Policy verwirft.
const planer = document.querySelector('table.budgetplaner');
if (planer) {
const menue = document.createElement('div');
menue.className = 'zahnradmenue';
let feld = null;
let zahnrad = null;
function schliessen() {
if (menue.parentNode) {
menue.parentNode.removeChild(menue);
}
if (zahnrad) {
zahnrad.setAttribute('aria-expanded', 'false');
}
feld = null;
zahnrad = null;
}
function uebertragen(abMonat) {
const zeile = feld.closest('tr');
const wert = feld.value;
zeile.querySelectorAll('input.betragsfeld').forEach(function (anderes) {
if (Number(anderes.dataset.monat) >= abMonat) {
anderes.value = wert;
}
});
schliessen();
}
[
{ text: 'Ganzes Jahr', ab: function () { return 1; } },
{ text: 'Restliches Jahr', ab: function (monat) { return monat + 1; } }
].forEach(function (eintrag) {
const knopf = document.createElement('button');
knopf.type = 'button';
knopf.className = 'menueeintrag';
knopf.textContent = eintrag.text;
knopf.addEventListener('click', function () {
if (feld) {
uebertragen(eintrag.ab(Number(feld.dataset.monat)));
}
});
menue.appendChild(knopf);
});
planer.addEventListener('click', function (ereignis) {
const knopf = ereignis.target.closest('.zahnrad');
if (!knopf) {
return;
}
const zelle = knopf.closest('td');
const warOffen = menue.parentNode === zelle;
schliessen();
if (warOffen) {
return;
}
feld = zelle.querySelector('input.betragsfeld');
zahnrad = knopf;
knopf.setAttribute('aria-expanded', 'true');
zelle.appendChild(menue);
menue.firstChild.focus();
});
// Klick daneben oder Escape schließt das Menü wieder.
document.addEventListener('click', function (ereignis) {
if (!(ereignis.target instanceof Element)) {
return;
}
if (!ereignis.target.closest('.zahnrad, .zahnradmenue')) {
schliessen();
}
});
document.addEventListener('keydown', function (ereignis) {
if (ereignis.key === 'Escape') {
schliessen();
}
});
}
// Meldungen nach kurzer Zeit ausblenden – Fehler bleiben stehen.
// Über eine Klasse, nicht über element.style: Die Content-Security-Policy
// (style-src 'self') verwirft auch per JavaScript gesetzte Inline-Styles,
// die Meldung bliebe sonst einfach stehen.
document.querySelectorAll('.meldung-erfolg').forEach(function (element) {
window.setTimeout(function () {
element.classList.add('verblasst');
window.setTimeout(function () { element.remove(); }, 400);
}, 5000);
});
})();