diff --git a/Changes.md b/Changes.md index d8bfd70..91f8f27 100644 --- a/Changes.md +++ b/Changes.md @@ -1,5 +1,31 @@ # Änderungen +## Buchungsmaske nennt das Konto und passt auf einen Bildschirm (17.08.2026) + +*Neue Buchung* und *Buchung bearbeiten* zeigen oben, in welchem Konto die Buchung liegt +bzw. gespeichert wird; bisher stand das Konto nur klein in der Kopfzeile über dem +Formular. Die Maske steht zweispaltig, die Beschriftungen links neben ihren Eingaben und +die Erklärungen in deren Tooltip – damit endet sie samt Splitzeilen und Knopfleiste +oberhalb der Fensterhöhe, wo sie vorher über 1190 px lang war. + +- Kontoangabe als erstes Feld über beide Spalten: Name des Kontos, dahinter die IBAN – + oder, wenn keine hinterlegt ist, Bank und Kontonummer. +- Feldreihenfolge zeilenweise: Art/Datum, Betrag/Empfänger, Kategorie-Gegenkonto/Klasse, + Buchungstext/Nummer, darunter *Abgeglichen*. Namen, Werte und Feld-IDs bleiben + unverändert; der Bereich *Splitbuchung* läuft weiter über die volle Breite. +- Beschriftungen stehen links neben ihrer Eingabe, nicht darüber (neue Variante + `.formular.beschriftung-links`). Das halbiert die Höhe je Feld; Hinweise laufen unter + Beschriftung und Eingabe hinweg, unter 560 px Fensterbreite rutscht die Beschriftung + wieder nach oben. +- Die erklärenden Texte stehen nicht mehr als eigene Zeile zwischen den Feldern, sondern + im `title` ihrer Beschriftung: Konto, Betrag, *Kategorie / Gegenkonto* und + *Splitbuchung*. Eine gepunktete Unterstreichung zeigt, wo ein Tooltip hängt. +- `.formular.zweispaltig` (`public/static/app.css`) stellt jetzt auch Tabellen und + `.feldwert` richtig: Tabellen laufen über beide Spalten, ohne eigenen Rand. +- E2E-Test in `tests/e2e/specs/buchungen.spec.js`: Die Maske nennt das Konto, Datum steht + rechts neben der Art, die Beschriftung links neben ihrer Eingabe, die Erklärungen hängen + in deren Tooltip, und das ganze Formular endet oberhalb der Fensterhöhe. + ## Kontoformular zweispaltig (17.08.2026) *Konto bearbeiten* und *Neues Konto* zeigen ihre Felder in zwei Spalten. Alle Eingaben diff --git a/public/static/app.css b/public/static/app.css index d48e5b6..6b27610 100644 --- a/public/static/app.css +++ b/public/static/app.css @@ -1231,6 +1231,16 @@ table.kostentabelle .gruppenzeile th { padding-top: .7rem; font-weight: 600; } margin-top: .6rem; } +/* Erklärungen zu einem Feld stehen im Tooltip seiner Beschriftung statt als + eigene Zeile darunter – das spart in langen Masken sichtbar Höhe. Die + gepunktete Linie zeigt, dass dort etwas zu holen ist. */ +.formular label[title], +.formular h3 span[title] { + text-decoration: underline dotted; + text-underline-offset: .25em; + cursor: help; +} + /* Zweispaltiges Formular: je Feld ein .feld mit Beschriftung, Eingabe und Hinweis, zwei davon nebeneinander. Lange Formulare – das Kontoformular etwa – bleiben so ohne vertikales Scrollen einsehbar. */ @@ -1253,23 +1263,55 @@ table.kostentabelle .gruppenzeile th { padding-top: .7rem; font-weight: 600; } .formular.zweispaltig .feld .feldhinweis { margin-top: 0; } -/* Über beide Spalten: verstecktes CSRF-Feld, Überschriften, Knopfleiste und - Felder, die für eine halbe Zeile zu breit sind. */ +.formular.zweispaltig .feld .feldwert { margin: 0; } + +.formular.zweispaltig .feld label.ankreuzfeld { margin-top: 0; } + +/* Über beide Spalten: verstecktes CSRF-Feld, Überschriften, Knopfleiste, + Tabellen – die Splitzeilen der Buchungsmaske etwa – und Felder, die für eine + halbe Zeile zu breit sind. */ .formular.zweispaltig > input[type="hidden"], .formular.zweispaltig > h3, .formular.zweispaltig > p, +.formular.zweispaltig > table, .formular.zweispaltig > .knopfleiste, .formular.zweispaltig .feld.ganz { grid-column: 1 / -1; } /* Die Abstände kommen im Grid aus dem gap – eigene Ränder addierten sich dazu. */ .formular.zweispaltig > h3 { margin: 0; } .formular.zweispaltig > p { margin: 0; } +.formular.zweispaltig > table { margin: 0; } .formular.zweispaltig > .knopfleiste { margin-top: .5rem; } @media (max-width: 720px) { .formular.zweispaltig { grid-template-columns: 1fr; } } +/* Beschriftung links neben der Eingabe statt darüber. Das halbiert die Höhe je + Feld – die Buchungsmaske passt damit samt Splitzeilen auf einen Bildschirm. + Hinweise stehen unter der Eingabe, also in derselben Spalte wie diese. */ +.formular.beschriftung-links .feld { + display: grid; + grid-template-columns: minmax(0, 10rem) minmax(0, 1fr); + align-items: center; + gap: .25rem .7rem; +} + +.formular.beschriftung-links .feld > label { grid-column: 1; } +.formular.beschriftung-links .feld > :not(label) { grid-column: 2; } + +/* Das Ankreuzfeld trägt seine Beschriftung selbst und braucht keine Spalte. + Hinweise laufen über beide Spalten: in der schmalen Eingabespalte allein + würden sie zu einem hohen Textblock, der die Zeile auseinanderzieht. */ +.formular.beschriftung-links .feld > label.ankreuzfeld, +.formular.beschriftung-links .feld > .feldhinweis { grid-column: 1 / -1; } + +@media (max-width: 560px) { + .formular.beschriftung-links .feld { grid-template-columns: 1fr; } + .formular.beschriftung-links .feld > label, + .formular.beschriftung-links .feld > :not(label) { grid-column: 1; } +} + .formular label.ankreuzfeld { display: flex; align-items: center; diff --git a/src/Views/buchungen/formular.php b/src/Views/buchungen/formular.php index 4488c9e..5db3270 100644 --- a/src/Views/buchungen/formular.php +++ b/src/Views/buchungen/formular.php @@ -33,6 +33,15 @@ $eigenesKonto = (int)$konto['id']; $mitSplit = false; $mitDepot = true; +// Zusatz hinter dem Kontonamen: was das Konto eindeutig macht, ohne die Zeile +// zu überladen. IBAN sticht, sonst Bank und Kontonummer. +$kontozusatz = trim((string)($konto['iban'] ?? '')) !== '' + ? (string)$konto['iban'] + : trim(implode(' · ', array_filter([ + trim((string)($konto['bankname'] ?? '')), + trim((string)($konto['kontonummer'] ?? '')), + ]))); + // Immer ein paar leere Splitzeilen anbieten; weitere fügt der Knopf hinzu. $splitZeilen = $splits; for ($i = count($splitZeilen); $i < 4; $i++) { @@ -48,75 +57,95 @@ include __DIR__ . '/../layout/kopf.php'; Konto -
+ - - +
+ +

+ + + + +

+
- - +
+ + +
- - -

Immer positiv eingeben – die Richtung bestimmt die Art oben.

+
+ + +
- - +
+ + +
- - -

- Unter den Kategorien stehen die eigenen Konten in eckigen Klammern. Eines davon - macht die Buchung zur Umbuchung: Sie wird einmal gespeichert und erscheint auf - beiden Kontoblättern – auf dem Gegenkonto mit umgekehrtem Vorzeichen. -

+
+ + +
- - +
+ + +
- - +
+ + +
- - +
+ + +
- +
+ + +
-

Splitbuchung (optional)

-

- Sobald hier Beträge stehen, gilt die Zuordnung aus den Zeilen; Kategorie und - Gegenkonto oben bleiben dann außen vor. Die Summe muss dem Buchungsbetrag - entsprechen. -

+
+ +
+ +

Splitbuchung + (optional)

diff --git a/tests/e2e/specs/buchungen.spec.js b/tests/e2e/specs/buchungen.spec.js index 234fea1..f23be17 100644 --- a/tests/e2e/specs/buchungen.spec.js +++ b/tests/e2e/specs/buchungen.spec.js @@ -70,6 +70,41 @@ test.describe('Buchungen', () => { await expect(page.locator('.saldoanzeige').first()).toContainText('1.000,00') }) + // Die Maske nennt oben das Konto, in dem die Buchung liegt, und stellt ihre + // Felder zweispaltig – beides, damit man beim Erfassen weder rätselt noch + // vertikal scrollt. + test('Buchungsmaske nennt das Konto und passt auf einen Bildschirm', async ({ page }) => { + await page.setViewportSize({ width: 1280, height: 900 }) + await page.goto(`/konten/${giroId}/buchung`) + + // Das Konto steht in der Maske selbst, nicht nur in der Kopfzeile. + await expect(page.locator('.buchungsmaske .feldwert')).toContainText(GIRO) + + // Gleiche Zeile, zweite Spalte. + const art = await page.locator('#art').boundingBox() + const datum = await page.locator('#datum').boundingBox() + expect(Math.abs(art.y - datum.y)).toBeLessThan(5) + expect(datum.x).toBeGreaterThan(art.x + art.width) + + // Und die Beschriftung steht links neben der Eingabe, nicht darüber. + const beschriftung = await page.locator('label[for="betrag"]').boundingBox() + const betrag = await page.locator('#betrag').boundingBox() + expect(beschriftung.x + beschriftung.width).toBeLessThanOrEqual(betrag.x) + expect(Math.abs( + (beschriftung.y + beschriftung.height / 2) - (betrag.y + betrag.height / 2) + )).toBeLessThan(5) + + // Die Erklärungen stehen im Tooltip der Beschriftung, nicht als eigene + // Zeile darunter – sonst passte die Maske nicht auf einen Bildschirm. + await expect(page.locator('label[for="zuordnung"]')).toHaveAttribute('title', /Umbuchung/) + await expect(page.locator('label[for="betrag"]')).toHaveAttribute('title', /positiv/) + + // Und genau das ist der Zweck: Die ganze Maske samt Splitzeilen und + // Knopfleiste endet oberhalb der Fensterhöhe. + const maske = await page.locator('.buchungsmaske').boundingBox() + expect(maske.y + maske.height).toBeLessThan(900) + }) + test('Ausgabe buchen und Saldo fortschreiben', async ({ page }) => { await page.goto(`/konten/${giroId}/buchung`)