Merge pull request 'Fix n.n.: „+ Neu“ öffnet das Formular zur Neuanlage als modalen Dialog' (#106) from fds2610-bot/Finanzmanager:main into main

Reviewed-on: https://codeberg.org/fds2610/Finanzmanager/pulls/106
This commit is contained in:
fds2610 2026-10-01 09:53:51 +02:00
commit d5b4c8d350
19 changed files with 505 additions and 345 deletions

View file

@ -1,5 +1,10 @@
# Änderungen
## „+ Neu“ öffnet das Formular als Dialog (01.10.2026)
- Das Formular zur Neuanlage stand in elf Ansichten unter der Liste. Jetzt öffnet der Knopf „+ Neu“ es als modalen Dialog (`app.js`, `.neudialog`); nach dem Speichern erscheint die Liste wieder, mit dem neuen Eintrag. Betroffen: Klassen, Buchungsvorlagen, Regelmäßige Buchungen, Erinnerungen, Verträge, Adressen, Sparziele, Kredite, Abgleichregeln, Budgets, Berichtsgruppen.
- E2E: `neudialog.spec.js`; bestehende Specs öffnen den Dialog zuerst.
## Regelmäßige Buchungen: Klick zum Bearbeiten (01.10.2026)
- In „Alle Reihen“ erscheinen alle Felder zunächst als Text und werden erst beim Anklicken zum Eingabefeld (`reihenliste.js`). Ja/Nein-Felder schalten per Klick um, „Speichern“ erscheint erst nach einer Änderung. Konto und Klasse stehen untereinander, die Spalte Name/Buchungstext nimmt den freien Platz.

View file

@ -1289,6 +1289,19 @@ table.splittabelle input.rechts { text-align: right; }
„Speichern“ und „Abbrechen“, damit es nicht im Vorbeigehen getroffen wird. */
.splitdialog .knopfleiste .knopf.gefahr { margin-left: auto; }
/* „+ Neu“ in den Listenansichten: Das Formular zur Neuanlage liegt in einem
modalen Dialog, den der Knopf öffnet. Der Dialog trägt den Rahmen, das
Formular darin braucht ihn nicht noch einmal. */
.neudialog { max-height: 92vh; overflow-y: auto; }
.neudialog .formular.breit {
max-width: none;
padding: 0;
border: 0;
box-shadow: none;
background: none;
}
/* Die Formatwahl vor Import und Export – als Dialog auf der Verwaltungsseite
und als Kachelinhalt auf der Ausweichseite, deshalb zwei Ebenen: der Dialog
trägt den Rahmen, .formatwahl die Karten. */

View file

@ -314,6 +314,20 @@
});
});
// „+ Neu“ in den Listenansichten: Der Knopf öffnet den Dialog mit dem
// Formular zur Neuanlage. Gespeichert wird per Formularversand; die Ansicht
// lädt danach neu und zeigt die Liste samt neuem Eintrag.
document.querySelectorAll('[data-neudialog]').forEach(function (knopf) {
knopf.addEventListener('click', function () {
const dialog = document.getElementById(knopf.dataset.neudialog);
if (dialog && typeof dialog.showModal === 'function') {
dialog.showModal();
const feld = dialog.querySelector('input:not([type=hidden]), select, textarea');
if (feld) { feld.focus(); }
}
});
});
// Übermittlungsprotokoll im Zahlungsverkehr: Ein Klick auf den Status holt das
// Protokoll der Überweisung in einen modalen Dialog. Ohne <dialog> oder
// fetch bleibt der Status ein Link auf dieselbe Ansicht als eigene Seite.

View file

@ -19,6 +19,7 @@ include __DIR__ . '/../layout/kopf.php';
</div>
<div class="seitenaktionen">
<button type="button" class="knopf primaer" id="neu-knopf" data-neudialog="neudialog">+ Neu</button>
<a href="/auswertungen" class="knopf">Zur Übersicht</a>
</div>
@ -28,7 +29,7 @@ include __DIR__ . '/../layout/kopf.php';
und rücken unter „Nicht gruppiert“.
</p>
<div class="kachel einzeln">
<dialog id="neudialog" class="splitdialog neudialog">
<h2>Neue Berichtsgruppe</h2>
<form method="post" action="/auswertungen/gruppen/neu" class="zeilenform">
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
@ -39,8 +40,9 @@ include __DIR__ . '/../layout/kopf.php';
<input type="number" name="sortierung" value="0" class="schmal">
</label>
<button type="submit" class="knopf primaer">Anlegen</button>
<button type="submit" class="knopf" formmethod="dialog" formnovalidate>Abbrechen</button>
</form>
</div>
</dialog>
<?php
$echte = array_filter($gruppen, static fn(array $eintrag): bool => $eintrag['gruppe'] !== null);

View file

@ -27,6 +27,7 @@ include __DIR__ . '/../layout/kopf.php';
</p>
<div class="seitenaktionen">
<button type="button" class="knopf primaer" id="neu-knopf" data-neudialog="neudialog">+ Neu</button>
<form method="post" action="/banking/regeln/pruefen" class="inline">
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
<button type="submit" class="knopf"
@ -147,16 +148,19 @@ include __DIR__ . '/../layout/kopf.php';
</table>
<?php endif; ?>
<dialog id="neudialog" class="splitdialog neudialog">
<form method="post" action="/banking/regeln/neu" class="formular breit">
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
<h3>Neue Regel</h3>
<h2>Neue Regel</h2>
<?php $regel = ['aktiv' => 1]; $idPrefix = 'neu_'; ?>
<?php include __DIR__ . '/regelfelder.php'; ?>
<div class="knopfleiste">
<button type="submit" class="knopf primaer">Regel anlegen</button>
<button type="submit" class="knopf" formmethod="dialog" formnovalidate>Abbrechen</button>
</div>
</form>
</dialog>
<?php include __DIR__ . '/../layout/fuss.php'; ?>

View file

@ -11,6 +11,7 @@ include __DIR__ . '/../layout/kopf.php';
<div class="seitenaktionen">
<a href="/planung/budget" class="knopf primaer">Budgetplaner öffnen</a>
<button type="button" class="knopf primaer" id="neu-knopf" data-neudialog="neudialog">+ Neu</button>
<a href="/planung" class="knopf">Zurück zur Planung</a>
</div>
@ -66,9 +67,8 @@ include __DIR__ . '/../layout/kopf.php';
</table>
<?php endif; ?>
<div class="kachelraster">
<div class="kachel">
<header><h2>Neues Budget</h2></header>
<dialog id="neudialog" class="splitdialog neudialog">
<h2>Neues Budget</h2>
<form method="post" action="/planung/budget/neu" class="formular">
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
@ -83,10 +83,12 @@ include __DIR__ . '/../layout/kopf.php';
<div class="knopfleiste">
<button type="submit" class="knopf primaer">Budget anlegen</button>
<button type="submit" class="knopf" formmethod="dialog" formnovalidate>Abbrechen</button>
</div>
</form>
</div>
</dialog>
<div class="kachelraster">
<?php if ($budgets !== []): ?>
<div class="kachel">
<header><h2>Budget kopieren</h2></header>

View file

@ -22,6 +22,10 @@ include __DIR__ . '/../layout/kopf.php';
kündigen“, „Steuerbescheid prüfen“. Offene Erinnerungen erscheinen im Finanzkalender.
</p>
<div class="seitenaktionen">
<button type="button" class="knopf primaer" id="neu-knopf" data-neudialog="neudialog">+ Neu</button>
</div>
<?php if ($erinnerungen === []): ?>
<p class="platzhalter">Keine Erinnerungen.</p>
<?php else: ?>
@ -65,9 +69,10 @@ include __DIR__ . '/../layout/kopf.php';
</table>
<?php endif; ?>
<dialog id="neudialog" class="splitdialog neudialog">
<form method="post" action="/termine/erinnerungen/neu" class="formular breit">
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
<h3>Neue Erinnerung</h3>
<h2>Neue Erinnerung</h2>
<label for="text">Erinnerung</label>
<input type="text" id="text" name="text" required>
@ -80,7 +85,9 @@ include __DIR__ . '/../layout/kopf.php';
<div class="knopfleiste">
<button type="submit" class="knopf primaer">Erinnerung anlegen</button>
<button type="submit" class="knopf" formmethod="dialog" formnovalidate>Abbrechen</button>
</div>
</form>
</dialog>
<?php include __DIR__ . '/../layout/fuss.php'; ?>

View file

@ -23,6 +23,10 @@ include __DIR__ . '/../layout/kopf.php';
<a href="/termine/erinnerungen" class="tab">Erinnerungen</a>
</div>
<div class="seitenaktionen">
<button type="button" class="knopf primaer" id="neu-knopf" data-neudialog="neudialog">+ Neu</button>
</div>
<?php if ($vorschau['faellig'] > 0): ?>
<div class="seitenaktionen">
<span class="hinweis">
@ -217,9 +221,10 @@ include __DIR__ . '/../layout/kopf.php';
</table>
<?php endif; ?>
<dialog id="neudialog" class="splitdialog neudialog">
<form method="post" action="/termine/neu" class="formular breit">
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
<h3>Neue regelmäßige Buchung</h3>
<h2>Neue regelmäßige Buchung</h2>
<label for="name">Name</label>
<input type="text" id="name" name="name" required placeholder="z. B. Miete">
@ -287,8 +292,10 @@ include __DIR__ . '/../layout/kopf.php';
<div class="knopfleiste">
<button type="submit" class="knopf primaer">Reihe anlegen</button>
<button type="submit" class="knopf" formmethod="dialog" formnovalidate>Abbrechen</button>
</div>
</form>
</dialog>
<?php include __DIR__ . '/../layout/fuss.php'; ?>

View file

@ -25,6 +25,10 @@ include __DIR__ . '/../layout/kopf.php';
hinausgeht.
</p>
<div class="seitenaktionen">
<button type="button" class="knopf primaer" id="neu-knopf" data-neudialog="neudialog">+ Neu</button>
</div>
<form method="get" action="/vertraege/adressen" class="filterleiste">
<label for="suche">Suchen</label>
<input type="search" id="suche" name="suche" value="<?= View::h($suche) ?>"
@ -126,9 +130,10 @@ include __DIR__ . '/../layout/kopf.php';
</table>
<?php endif; ?>
<dialog id="neudialog" class="splitdialog neudialog">
<form method="post" action="/vertraege/adressen/neu" class="formular breit">
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
<h3>Neue Adresse</h3>
<h2>Neue Adresse</h2>
<label for="name">Name</label>
<input type="text" id="name" name="name" required>
@ -176,7 +181,9 @@ include __DIR__ . '/../layout/kopf.php';
<div class="knopfleiste">
<button type="submit" class="knopf primaer">Adresse anlegen</button>
<button type="submit" class="knopf" formmethod="dialog" formnovalidate>Abbrechen</button>
</div>
</form>
</dialog>
<?php include __DIR__ . '/../layout/fuss.php'; ?>

View file

@ -23,6 +23,10 @@ include __DIR__ . '/../layout/kopf.php';
<a href="/vertraege/kredite" class="tab">Kredite</a>
</div>
<div class="seitenaktionen">
<button type="button" class="knopf primaer" id="neu-knopf" data-neudialog="neudialog">+ Neu</button>
</div>
<?php if ($fristen !== []): ?>
<div class="kachel einzeln">
<header><h2>Kündigungsfristen laufen ab</h2></header>
@ -131,9 +135,10 @@ include __DIR__ . '/../layout/kopf.php';
</table>
<?php endif; ?>
<dialog id="neudialog" class="splitdialog neudialog">
<form method="post" action="/vertraege/neu" class="formular breit">
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
<h3>Neuer Vertrag</h3>
<h2>Neuer Vertrag</h2>
<label for="name">Bezeichnung</label>
<input type="text" id="name" name="name" required placeholder="z. B. Hausratversicherung">
@ -204,7 +209,9 @@ include __DIR__ . '/../layout/kopf.php';
<div class="knopfleiste">
<button type="submit" class="knopf primaer">Vertrag anlegen</button>
<button type="submit" class="knopf" formmethod="dialog" formnovalidate>Abbrechen</button>
</div>
</form>
</dialog>
<?php include __DIR__ . '/../layout/fuss.php'; ?>

View file

@ -21,6 +21,10 @@ include __DIR__ . '/../layout/kopf.php';
<span class="tab aktiv">Kredite</span>
</div>
<div class="seitenaktionen">
<button type="button" class="knopf primaer" id="neu-knopf" data-neudialog="neudialog">+ Neu</button>
</div>
<?php if ($kredite !== []): ?>
<table class="liste zahlen">
<thead>
@ -201,9 +205,10 @@ include __DIR__ . '/../layout/kopf.php';
</form>
<?php endif; ?>
<dialog id="neudialog" class="splitdialog neudialog">
<form method="post" action="/vertraege/kredite/neu" class="formular breit">
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
<h3>Neuer Kredit</h3>
<h2>Neuer Kredit</h2>
<p class="feldhinweis">
Gerechnet wird als Annuitätendarlehen: gleichbleibende Rate, sinkender Zinsanteil.
Der Tilgungsplan entsteht aus diesen Angaben und wird nicht gespeichert.
@ -252,7 +257,9 @@ include __DIR__ . '/../layout/kopf.php';
<div class="knopfleiste">
<button type="submit" class="knopf primaer">Kredit anlegen</button>
<button type="submit" class="knopf" formmethod="dialog" formnovalidate>Abbrechen</button>
</div>
</form>
</dialog>
<?php include __DIR__ . '/../layout/fuss.php'; ?>

View file

@ -24,6 +24,10 @@ include __DIR__ . '/../layout/kopf.php';
auf dem Konto lag.
</p>
<div class="seitenaktionen">
<button type="button" class="knopf primaer" id="neu-knopf" data-neudialog="neudialog">+ Neu</button>
</div>
<?php if ($ziele === []): ?>
<p class="platzhalter">Noch kein Sparziel angelegt.</p>
<?php else: ?>
@ -93,9 +97,10 @@ include __DIR__ . '/../layout/kopf.php';
</div>
<?php endif; ?>
<dialog id="neudialog" class="splitdialog neudialog">
<form method="post" action="/vertraege/sparziele/neu" class="formular breit">
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
<h3>Neues Sparziel</h3>
<h2>Neues Sparziel</h2>
<label for="name">Name</label>
<input type="text" id="name" name="name" required placeholder="z. B. Neues Auto">
@ -132,7 +137,9 @@ include __DIR__ . '/../layout/kopf.php';
<div class="knopfleiste">
<button type="submit" class="knopf primaer">Sparziel anlegen</button>
<button type="submit" class="knopf" formmethod="dialog" formnovalidate>Abbrechen</button>
</div>
</form>
</dialog>
<?php include __DIR__ . '/../layout/fuss.php'; ?>

View file

@ -9,6 +9,7 @@ include __DIR__ . '/../layout/kopf.php';
?>
<div class="seitenaktionen">
<button type="button" class="knopf primaer" id="neu-knopf" data-neudialog="neudialog">+ Neu</button>
<a href="/verwaltung" class="knopf">Zurück zur Verwaltung</a>
<a href="/verwaltung/import" class="knopf">QIF-Import</a>
</div>
@ -69,9 +70,10 @@ include __DIR__ . '/../layout/kopf.php';
</table>
<?php endif; ?>
<dialog id="neudialog" class="splitdialog neudialog">
<form method="post" action="/verwaltung/klassen/neu" class="formular breit">
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
<h3>Neue Klasse</h3>
<h2>Neue Klasse</h2>
<label for="name">Name</label>
<input type="text" id="name" name="name" required>
@ -81,7 +83,9 @@ include __DIR__ . '/../layout/kopf.php';
<div class="knopfleiste">
<button type="submit" class="knopf primaer">Klasse anlegen</button>
<button type="submit" class="knopf" formmethod="dialog" formnovalidate>Abbrechen</button>
</div>
</form>
</dialog>
<?php include __DIR__ . '/../layout/fuss.php'; ?>

View file

@ -27,6 +27,10 @@ include __DIR__ . '/../layout/kopf.php';
die Splitzeilen im gleichen Verhältnis mitgezogen.
</p>
<div class="seitenaktionen">
<button type="button" class="knopf primaer" id="neu-knopf" data-neudialog="neudialog">+ Neu</button>
</div>
<form method="get" action="/vorlagen" class="filterleiste">
<label for="suche">Suchen</label>
<input type="search" id="suche" name="suche" value="<?= View::h($suche) ?>"
@ -106,9 +110,10 @@ include __DIR__ . '/../layout/kopf.php';
</table>
<?php endif; ?>
<dialog id="neudialog" class="splitdialog neudialog">
<form method="post" action="/vorlagen/neu" class="formular breit">
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
<h3>Neue Vorlage</h3>
<h2>Neue Vorlage</h2>
<label for="name">Name</label>
<input type="text" id="name" name="name" required>
@ -148,7 +153,9 @@ include __DIR__ . '/../layout/kopf.php';
<div class="knopfleiste">
<button type="submit" class="knopf primaer">Vorlage anlegen</button>
<button type="submit" class="knopf" formmethod="dialog" formnovalidate>Abbrechen</button>
</div>
</form>
</dialog>
<?php include __DIR__ . '/../layout/fuss.php'; ?>

View file

@ -54,8 +54,9 @@ test.describe('Buchungen', () => {
await page.click('.formular button[type="submit"]')
await page.goto('/verwaltung/klassen')
await page.click('#neu-knopf')
await page.fill('form[action="/verwaltung/klassen/neu"] #name', KLASSE)
await page.click('form[action="/verwaltung/klassen/neu"] button[type="submit"]')
await page.click('form[action="/verwaltung/klassen/neu"] button.primaer')
await page.close()
})

View file

@ -201,6 +201,7 @@ test.describe('Module der Phasen 4 bis 7', () => {
test('Eine falsche IBAN wird beim Anlegen einer Adresse abgewiesen', async ({ page }) => {
await page.goto('/vertraege/adressen')
await page.click('#neu-knopf')
await page.fill('#name', 'E2E-Falschtest')
await page.fill('#iban', 'DE02120300000000202052')
@ -211,6 +212,7 @@ test.describe('Module der Phasen 4 bis 7', () => {
test('Ein Kredit zeigt seinen gerechneten Tilgungsplan', async ({ page }) => {
await page.goto('/vertraege/kredite')
await page.click('#neu-knopf')
await page.fill('#name', 'E2E-Darlehen')
await page.fill('#darlehensbetrag', '100000')
@ -314,10 +316,11 @@ test.describe('Module der Phasen 4 bis 7', () => {
test('Regelmäßige Buchungen werden per Klick bearbeitbar', async ({ page }) => {
await page.goto('/termine')
await page.click('#neu-knopf')
await page.fill('#name', 'E2E-Reihe')
await page.fill('#neu_betrag', '-12,50')
await page.fill('#neu_buchungstext', 'E2E-Text')
await page.click('form[action="/termine/neu"] button[type="submit"]')
await page.click('form[action="/termine/neu"] button.primaer')
const zeile = page.locator('table.reihen tr', { hasText: 'E2E-Reihe' })
await expect(zeile.locator('input[name="betrag"]')).toBeHidden()
@ -398,6 +401,7 @@ test.describe('Module der Phasen 4 bis 7', () => {
// Erst eine Adresse anlegen, damit die Auswahl etwas zu bieten hat.
await page.goto('/vertraege/adressen')
await page.click('#neu-knopf')
await page.fill('#name', empfaenger)
await page.fill('#iban', 'DE02120300000000202051')
await page.click('button:has-text("Adresse anlegen")')

View file

@ -0,0 +1,60 @@
// @ts-check
const { test, expect } = require('@playwright/test')
const BENUTZER = process.env.E2E_USER || 'e2e'
const PASSWORT = process.env.E2E_PASSWORD || 'e2e-testpasswort'
const MARKE = Date.now().toString().slice(-6)
// Alle Listenansichten, in denen „+ Neu“ ein modales Formular zur Neuanlage öffnet.
const ANSICHTEN = [
'/verwaltung/klassen',
'/vorlagen',
'/termine',
'/termine/erinnerungen',
'/vertraege',
'/vertraege/adressen',
'/vertraege/sparziele',
'/vertraege/kredite',
'/banking/regeln',
'/planung/budget/verwaltung',
'/auswertungen/gruppen',
]
async function anmelden(page) {
await page.goto('/login')
await page.fill('#benutzername', BENUTZER)
await page.fill('#passwort', PASSWORT)
await page.click('button[type="submit"]')
await expect(page).toHaveURL(/\/$/)
}
test.describe('„+ Neu“ öffnet den Dialog zur Neuanlage', () => {
test.beforeEach(async ({ page }) => {
await anmelden(page)
})
for (const pfad of ANSICHTEN) {
test(`${pfad}: Dialog erst nach Klick, Abbrechen schließt ihn`, async ({ page }) => {
await page.goto(pfad)
await expect(page.locator('#neudialog')).toBeHidden()
await page.click('#neu-knopf')
await expect(page.locator('#neudialog')).toBeVisible()
await page.locator('#neudialog button', { hasText: 'Abbrechen' }).click()
await expect(page.locator('#neudialog')).toBeHidden()
})
}
test('Klasse anlegen: Dialog verschwindet, Eintrag steht in der Liste', async ({ page }) => {
const name = `E2E Dialogklasse ${MARKE}`
await page.goto('/verwaltung/klassen')
await page.click('#neu-knopf')
await page.fill('#neudialog #name', name)
await page.locator('#neudialog button[type="submit"]', { hasText: 'Klasse anlegen' }).click()
await expect(page.locator('#neudialog')).toBeHidden()
await expect(page.locator(`input[value="${name}"]`).first()).toBeVisible()
})
})

View file

@ -35,6 +35,7 @@ async function anmelden(page) {
*/
async function budgetAnlegen(page, name) {
await page.goto('/planung/budget/verwaltung')
await page.click('#neu-knopf')
await page.fill('#name', name)
await page.fill('#jahr', String(JAHR))
await page.click('.formular button:has-text("Budget anlegen")')

View file

@ -163,9 +163,10 @@ test.describe('Stammdaten', () => {
const name = `E2E Projekt ${MARKE}`
await page.goto('/verwaltung/klassen')
await page.click('#neu-knopf')
await page.fill('.formular #name', name)
await page.fill('.formular #beschreibung', 'aus dem E2E-Test')
await page.click('.formular button[type="submit"]')
await page.click('.formular button:has-text("Klasse anlegen")')
await expect(page.locator('.meldung-erfolg')).toBeVisible()