Fix #40: Kategorie im Kontoblatt ohne Neuladen speichern
Das Ändern von Kategorie oder Klasse im Kontoblatt lud bisher die ganze Seite neu, um die Zeile zu aktualisieren – dabei sprang der Bildschirm nach ganz oben. kontoblatt.js schickt die Auswahl jetzt per fetch im Hintergrund; Kategorie, Klasse und das Symbol der Bargeld-Abgleichspalte aktualisieren sich an Ort und Stelle, die Bildlaufposition bleibt stehen. Ohne JavaScript bleibt es bei der bisherigen Weiterleitung.
This commit is contained in:
parent
18c1603dbd
commit
a077e03bee
9 changed files with 276 additions and 30 deletions
19
Changes.md
19
Changes.md
|
|
@ -1,5 +1,24 @@
|
||||||
# Änderungen
|
# Änderungen
|
||||||
|
|
||||||
|
## Kontoblatt: Schnellzuordnung ohne Neuladen (19.08.2026)
|
||||||
|
|
||||||
|
Das Ändern von Kategorie oder Klasse im Kontoblatt lud bisher die ganze Seite neu, um die
|
||||||
|
Zeile zu aktualisieren – dabei sprang der Bildschirm nach ganz oben. Bei einer älteren
|
||||||
|
Buchung weit unten in der Liste musste man nach jeder Änderung wieder dorthin zurückscrollen
|
||||||
|
(Fehler #40).
|
||||||
|
|
||||||
|
kontoblatt.js schickt die Auswahl jetzt im Hintergrund per `fetch`; Kategorie- bzw.
|
||||||
|
Klassenspalte und – falls eingerichtet – das Symbol der Bargeld-Abgleichspalte aktualisieren
|
||||||
|
sich an Ort und Stelle, die Bildlaufposition bleibt stehen. Die Meldung „Zuordnung
|
||||||
|
gespeichert.“ erscheint wie gewohnt, jetzt über `window.FmMeldungen` (app.js) ohne
|
||||||
|
Seitenwechsel.
|
||||||
|
|
||||||
|
Serverseitig unterscheiden `BuchungenController::zuordnung()` und `::klasse()` per
|
||||||
|
`X-Requested-With: XMLHttpRequest`, ob eine JSON-Antwort erwartet wird oder die bisherige
|
||||||
|
Weiterleitung mit Flash-Meldung – Letztere bleibt der einzige Weg ohne JavaScript. Schlägt
|
||||||
|
die Hintergrundanfrage fehl (Netzwerk, abgelaufene Anmeldung), übernimmt wie bisher eine
|
||||||
|
echte Formularübermittlung.
|
||||||
|
|
||||||
## Bargeld-Tool: Adresse mit `/api/v1` (19.08.2026)
|
## Bargeld-Tool: Adresse mit `/api/v1` (19.08.2026)
|
||||||
|
|
||||||
Der Verbindungstest scheiterte mit „Das Bargeld-Tool kennt die Schnittstelle nicht
|
Der Verbindungstest scheiterte mit „Das Bargeld-Tool kennt die Schnittstelle nicht
|
||||||
|
|
|
||||||
|
|
@ -295,12 +295,37 @@
|
||||||
// Erfolgsmeldungen gehen von selbst; Fehler und Warnungen bleiben stehen,
|
// Erfolgsmeldungen gehen von selbst; Fehler und Warnungen bleiben stehen,
|
||||||
// bis sie weggeklickt werden. Sie liegen über der Ansicht – ohne diesen Weg
|
// bis sie weggeklickt werden. Sie liegen über der Ansicht – ohne diesen Weg
|
||||||
// verdeckten sie die Werkzeugleiste bis zum nächsten Seitenwechsel.
|
// verdeckten sie die Werkzeugleiste bis zum nächsten Seitenwechsel.
|
||||||
document.querySelectorAll('.meldung').forEach(function (element) {
|
const einrichten = function (element) {
|
||||||
element.title = 'Zum Ausblenden anklicken';
|
element.title = 'Zum Ausblenden anklicken';
|
||||||
element.addEventListener('click', function () { ausblenden(element); });
|
element.addEventListener('click', function () { ausblenden(element); });
|
||||||
|
|
||||||
if (element.classList.contains('meldung-erfolg')) {
|
if (element.classList.contains('meldung-erfolg')) {
|
||||||
window.setTimeout(function () { ausblenden(element); }, 5000);
|
window.setTimeout(function () { ausblenden(element); }, 5000);
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
|
document.querySelectorAll('.meldung').forEach(einrichten);
|
||||||
|
|
||||||
|
// Für Ansichten, die eine Meldung nachträglich zeigen, ohne die Seite neu
|
||||||
|
// zu laden – etwa das Kontoblatt nach einer Schnellzuordnung
|
||||||
|
// (kontoblatt.js). Den Meldungsbereich selbst gibt es im HTML nur, wenn
|
||||||
|
// der Server ihn beim Laden der Seite mitgab; hier entsteht er bei Bedarf.
|
||||||
|
window.FmMeldungen = {
|
||||||
|
zeigen: function (text, typ) {
|
||||||
|
let bereich = document.querySelector('.meldungen');
|
||||||
|
if (!bereich) {
|
||||||
|
bereich = document.createElement('div');
|
||||||
|
bereich.className = 'meldungen';
|
||||||
|
document.body.appendChild(bereich);
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = document.createElement('div');
|
||||||
|
element.className = 'meldung meldung-' + (typ || 'erfolg');
|
||||||
|
element.setAttribute('role', 'status');
|
||||||
|
element.textContent = text;
|
||||||
|
bereich.appendChild(element);
|
||||||
|
|
||||||
|
einrichten(element);
|
||||||
|
}
|
||||||
|
};
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -167,6 +167,52 @@
|
||||||
window.location.href = '/buchungen/' + feld.dataset.buchung + '/wertpapier?depot=' + depotId;
|
window.location.href = '/buchungen/' + feld.dataset.buchung + '/wertpapier?depot=' + depotId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Die Beschriftung der gewählten Option entspricht Wort für Wort dem, was
|
||||||
|
* die Zeile nach dem Speichern zeigen soll – Kategorie- und Kontonamen
|
||||||
|
* kommen serverseitig aus denselben Listen (zuordnungsliste.php). Der
|
||||||
|
* erste Eintrag „— keine —“ steht für eine leere Zuordnung. */
|
||||||
|
function beschriftungAus(auswahlliste) {
|
||||||
|
if (auswahlliste.value === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return auswahlliste.options[auswahlliste.selectedIndex].textContent.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Speichert eine Schnellzuordnung im Hintergrund, statt das Formular
|
||||||
|
* abzuschicken: Eine echte Formularübermittlung lädt die Seite neu und
|
||||||
|
* reißt den Bildschirm nach oben – bei einer alten Buchung weit unten in
|
||||||
|
* der Liste ein langer Weg zurück (Fehler #40). Das Feld schließt sofort;
|
||||||
|
* seinen Inhalt trägt die Antwort nach.
|
||||||
|
*
|
||||||
|
* Ohne Netz oder JSON-Antwort (etwa eine abgelaufene Anmeldung) übernimmt
|
||||||
|
* eine echte Formularübermittlung wie bisher – die Änderung geht dann
|
||||||
|
* nicht verloren, nur der Bildschirm springt in diesem Ausnahmefall.
|
||||||
|
*/
|
||||||
|
function schnellSpeichern(formular, aktualisieren) {
|
||||||
|
const daten = new FormData(formular);
|
||||||
|
schliessen();
|
||||||
|
|
||||||
|
fetch(formular.action, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
||||||
|
body: daten
|
||||||
|
}).then(function (antwort) {
|
||||||
|
return antwort.json().then(function (ergebnis) {
|
||||||
|
if (ergebnis.typ === 'fehler') {
|
||||||
|
window.FmMeldungen.zeigen(ergebnis.meldung, 'fehler');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
aktualisieren(ergebnis);
|
||||||
|
if (ergebnis.meldung) {
|
||||||
|
window.FmMeldungen.zeigen(ergebnis.meldung, 'erfolg');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch(function () {
|
||||||
|
formular.requestSubmit();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
liste.addEventListener('change', function () {
|
liste.addEventListener('change', function () {
|
||||||
if (!offenesFeld || offenesFormular !== auswahl) {
|
if (!offenesFeld || offenesFormular !== auswahl) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -194,7 +240,21 @@
|
||||||
|
|
||||||
// Eine Rückfrage wegen verlorener Splitzeilen braucht es hier nicht mehr:
|
// Eine Rückfrage wegen verlorener Splitzeilen braucht es hier nicht mehr:
|
||||||
// Für eine aufgeteilte Buchung öffnet sich diese Liste gar nicht erst.
|
// Für eine aufgeteilte Buchung öffnet sich diese Liste gar nicht erst.
|
||||||
auswahl.requestSubmit();
|
const wert = liste.value;
|
||||||
|
const beschriftung = beschriftungAus(liste);
|
||||||
|
|
||||||
|
schnellSpeichern(auswahl, function (ergebnis) {
|
||||||
|
feld.dataset.wert = wert;
|
||||||
|
feld.title = beschriftung !== '' ? beschriftung : 'Kategorie oder Gegenkonto zuordnen';
|
||||||
|
feld.textContent = beschriftung;
|
||||||
|
|
||||||
|
if (ergebnis.bargeldHtml) {
|
||||||
|
const bargeldzelle = feld.closest('tr').querySelector('.bargeldspalte');
|
||||||
|
if (bargeldzelle) {
|
||||||
|
bargeldzelle.innerHTML = ergebnis.bargeldHtml;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Die gewählte Klasse wird sofort gespeichert – der erste Eintrag der Liste
|
/* Die gewählte Klasse wird sofort gespeichert – der erste Eintrag der Liste
|
||||||
|
|
@ -205,7 +265,16 @@
|
||||||
if (!offenesFeld || offenesFormular !== klassenauswahl) {
|
if (!offenesFeld || offenesFormular !== klassenauswahl) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
klassenauswahl.requestSubmit();
|
|
||||||
|
const feld = offenesFeld;
|
||||||
|
const wert = klassenliste.value;
|
||||||
|
const beschriftung = beschriftungAus(klassenliste);
|
||||||
|
|
||||||
|
schnellSpeichern(klassenauswahl, function () {
|
||||||
|
feld.dataset.klasse = wert === '' ? '0' : wert;
|
||||||
|
feld.title = beschriftung !== '' ? beschriftung : 'Klasse zuordnen';
|
||||||
|
feld.textContent = beschriftung;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,4 +77,23 @@ abstract class BaseController
|
||||||
$wert = $_GET[$feld] ?? $standard;
|
$wert = $_GET[$feld] ?? $standard;
|
||||||
return is_string($wert) ? trim($wert) : $standard;
|
return is_string($wert) ? trim($wert) : $standard;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ob die Anfrage per JavaScript im Hintergrund geschickt wurde, statt als
|
||||||
|
* gewöhnliche Formularübermittlung. Nur dann erwartet der Aufrufer JSON
|
||||||
|
* statt einer Weiterleitung – ohne JavaScript bleibt der bisherige Weg
|
||||||
|
* über Location-Header und Flash-Meldung der einzige.
|
||||||
|
*/
|
||||||
|
protected function istAjax(): bool
|
||||||
|
{
|
||||||
|
return ($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '') === 'XMLHttpRequest';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string,mixed> $daten */
|
||||||
|
protected function json(array $daten): void
|
||||||
|
{
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
echo json_encode($daten, JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -231,7 +231,7 @@ class BuchungenController extends BaseController
|
||||||
$zuordnung['gegenkonto_id']
|
$zuordnung['gegenkonto_id']
|
||||||
);
|
);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
View::redirect($ziel, $e->getMessage(), 'fehler');
|
$this->schnellantwort($ziel, $e->getMessage(), 'fehler');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -239,8 +239,21 @@ class BuchungenController extends BaseController
|
||||||
// zu prüfen ist. Das Ergebnis landet an der Buchung und steht danach
|
// zu prüfen ist. Das Ergebnis landet an der Buchung und steht danach
|
||||||
// als Symbol im Kontoblatt.
|
// als Symbol im Kontoblatt.
|
||||||
$abgleich = BargeldAbgleich::pruefe((int)$buchung['id']);
|
$abgleich = BargeldAbgleich::pruefe((int)$buchung['id']);
|
||||||
|
$bargeldHtml = null;
|
||||||
|
if (BargeldAbgleich::eingerichtet()) {
|
||||||
|
$frisch = Buchung::find((int)$buchung['id']);
|
||||||
|
$bargeldHtml = View::bargeldMarke(
|
||||||
|
$frisch['bargeld_status'] ?? null,
|
||||||
|
$frisch['bargeld_hinweis'] ?? null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
View::redirect($ziel, 'Zuordnung gespeichert.' . BargeldAbgleich::rueckmeldung($abgleich));
|
$this->schnellantwort(
|
||||||
|
$ziel,
|
||||||
|
'Zuordnung gespeichert.' . BargeldAbgleich::rueckmeldung($abgleich),
|
||||||
|
'erfolg',
|
||||||
|
['bargeldHtml' => $bargeldHtml]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -265,11 +278,33 @@ class BuchungenController extends BaseController
|
||||||
try {
|
try {
|
||||||
Buchung::klasseSetzen((int)$buchung['id'], $klasseId > 0 ? $klasseId : null);
|
Buchung::klasseSetzen((int)$buchung['id'], $klasseId > 0 ? $klasseId : null);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
View::redirect($ziel, $e->getMessage(), 'fehler');
|
$this->schnellantwort($ziel, $e->getMessage(), 'fehler');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
View::redirect($ziel, $klasseId > 0 ? 'Klasse gespeichert.' : 'Klasse gelöscht.');
|
$this->schnellantwort($ziel, $klasseId > 0 ? 'Klasse gespeichert.' : 'Klasse gelöscht.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Antwort auf eine Schnellzuordnung aus dem Kontoblatt: kontoblatt.js
|
||||||
|
* schickt sie im Hintergrund und braucht nur die Flash-Meldung als JSON,
|
||||||
|
* um die Seite ohne Neuladen an Ort und Stelle zu aktualisieren – Text und
|
||||||
|
* Zuordnung der Zelle kennt es selbst aus der gewählten Option. Ohne
|
||||||
|
* JavaScript bleibt es bei der bisherigen Weiterleitung.
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $daten zusätzliche Felder für die JSON-Antwort
|
||||||
|
*/
|
||||||
|
private function schnellantwort(string $ziel, string $meldung, string $typ = 'erfolg', array $daten = []): void
|
||||||
|
{
|
||||||
|
if ($this->istAjax()) {
|
||||||
|
if ($typ === 'fehler') {
|
||||||
|
http_response_code(422);
|
||||||
|
}
|
||||||
|
$this->json($daten + ['meldung' => $meldung, 'typ' => $typ]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
View::redirect($ziel, $meldung, $typ);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
29
src/View.php
29
src/View.php
|
|
@ -122,6 +122,35 @@ class View
|
||||||
return (float)$wert < 0 ? 'betrag negativ' : 'betrag';
|
return (float)$wert < 0 ? 'betrag negativ' : 'betrag';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Das Symbol der Abgleichspalte im Kontoblatt. Vier Zustände, in dieser
|
||||||
|
* Reihenfolge zu lesen: gefunden, nicht gefunden, Abruf gescheitert – und
|
||||||
|
* der graue Kreis für alles, was mit dem Bargeld-Tool nichts zu tun hat.
|
||||||
|
*
|
||||||
|
* Die Zeichen stehen als Text und nicht als Grafik da: Sie erben damit
|
||||||
|
* Schriftgröße und – beim Ausrufezeichen ausdrücklich gewünscht – die
|
||||||
|
* Farbe des übrigen Textes, in jedem Farbschema.
|
||||||
|
*
|
||||||
|
* Eigenständige Methode statt einer Schleife im Kontoblatt, weil auch die
|
||||||
|
* Schnellzuordnung (kontoblatt.js) dasselbe Symbol braucht, ohne die Seite
|
||||||
|
* neu zu laden.
|
||||||
|
*/
|
||||||
|
public static function bargeldMarke(?string $status, ?string $hinweis): string
|
||||||
|
{
|
||||||
|
$hinweis = trim((string)$hinweis);
|
||||||
|
|
||||||
|
[$zeichen, $klasse, $standardtitel] = match ($status) {
|
||||||
|
'gefunden' => ['✓', 'gefunden', 'Im Bargeld-Tool gefunden'],
|
||||||
|
'fehlt' => ['✗', 'fehlt', 'Im Bargeld-Tool nicht gefunden'],
|
||||||
|
'fehler' => ['!', 'fehler', 'Der Abruf beim Bargeld-Tool ist gescheitert'],
|
||||||
|
default => ['●', 'neutral', 'Wird nicht mit dem Bargeld-Tool abgeglichen'],
|
||||||
|
};
|
||||||
|
|
||||||
|
return '<span class="bargeldmarke bargeld-' . $klasse . '" title="'
|
||||||
|
. self::h($hinweis !== '' ? $hinweis : $standardtitel) . '" role="img" aria-label="'
|
||||||
|
. self::h($standardtitel) . '">' . $zeichen . '</span>';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Farbschema für das data-thema-Attribut: 'hell', 'dunkel' oder '' für die
|
* Farbschema für das data-thema-Attribut: 'hell', 'dunkel' oder '' für die
|
||||||
* Systemvorgabe. Fällt auf die Systemvorgabe zurück, wenn die Einstellung
|
* Systemvorgabe. Fällt auf die Systemvorgabe zurück, wenn die Einstellung
|
||||||
|
|
|
||||||
|
|
@ -106,31 +106,14 @@ $splituebersicht = static function (array $splitzeilen) use ($kategorieNamen, $k
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Das Symbol der Abgleichspalte. Vier Zustände, in dieser Reihenfolge zu lesen:
|
* Das Symbol der Abgleichspalte, siehe View::bargeldMarke() – die Schnell-
|
||||||
* gefunden, nicht gefunden, Abruf gescheitert – und der graue Kreis für alles,
|
* zuordnung (kontoblatt.js) braucht dasselbe Symbol für eine einzelne Zeile,
|
||||||
* was mit dem Bargeld-Tool nichts zu tun hat.
|
* ohne die Seite neu zu laden.
|
||||||
*
|
|
||||||
* Die Zeichen stehen als Text und nicht als Grafik da: Sie erben damit
|
|
||||||
* Schriftgröße und – beim Ausrufezeichen ausdrücklich gewünscht – die Farbe
|
|
||||||
* des übrigen Textes, in jedem Farbschema.
|
|
||||||
*
|
*
|
||||||
* @param array<string,mixed> $zeile
|
* @param array<string,mixed> $zeile
|
||||||
*/
|
*/
|
||||||
$bargeldmarke = static function (array $zeile): string {
|
$bargeldmarke = static fn(array $zeile): string
|
||||||
$status = (string)($zeile['bargeld_status'] ?? '');
|
=> View::bargeldMarke($zeile['bargeld_status'] ?? null, $zeile['bargeld_hinweis'] ?? null);
|
||||||
$hinweis = trim((string)($zeile['bargeld_hinweis'] ?? ''));
|
|
||||||
|
|
||||||
[$zeichen, $klasse, $standardtitel] = match ($status) {
|
|
||||||
'gefunden' => ['✓', 'gefunden', 'Im Bargeld-Tool gefunden'],
|
|
||||||
'fehlt' => ['✗', 'fehlt', 'Im Bargeld-Tool nicht gefunden'],
|
|
||||||
'fehler' => ['!', 'fehler', 'Der Abruf beim Bargeld-Tool ist gescheitert'],
|
|
||||||
default => ['●', 'neutral', 'Wird nicht mit dem Bargeld-Tool abgeglichen'],
|
|
||||||
};
|
|
||||||
|
|
||||||
return '<span class="bargeldmarke bargeld-' . $klasse . '" title="'
|
|
||||||
. View::h($hinweis !== '' ? $hinweis : $standardtitel) . '" role="img" aria-label="'
|
|
||||||
. View::h($standardtitel) . '">' . $zeichen . '</span>';
|
|
||||||
};
|
|
||||||
|
|
||||||
include __DIR__ . '/../layout/kopf.php';
|
include __DIR__ . '/../layout/kopf.php';
|
||||||
?>
|
?>
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ class DarstellungTest extends TestCase
|
||||||
$this->monatsnamen();
|
$this->monatsnamen();
|
||||||
$this->betraege();
|
$this->betraege();
|
||||||
$this->datumsformat();
|
$this->datumsformat();
|
||||||
|
$this->bargeldMarke();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function monatsnamen(): void
|
private function monatsnamen(): void
|
||||||
|
|
@ -56,4 +57,38 @@ class DarstellungTest extends TestCase
|
||||||
$this->assertEquals('18.03.2026', View::datum('2026-03-18 14:22:01'), 'Zeitanteil wird abgeschnitten');
|
$this->assertEquals('18.03.2026', View::datum('2026-03-18 14:22:01'), 'Zeitanteil wird abgeschnitten');
|
||||||
$this->assertEquals('–', View::datum(null), 'Leeres Datum als Gedankenstrich');
|
$this->assertEquals('–', View::datum(null), 'Leeres Datum als Gedankenstrich');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Das Symbol der Bargeld-Abgleichspalte – eigenständig in View, damit die
|
||||||
|
* Schnellzuordnung im Kontoblatt es auch für die Ajax-Antwort bilden kann,
|
||||||
|
* ohne die Seite neu zu laden.
|
||||||
|
*/
|
||||||
|
private function bargeldMarke(): void
|
||||||
|
{
|
||||||
|
$gefunden = View::bargeldMarke('gefunden', null);
|
||||||
|
$this->assert(str_contains($gefunden, 'bargeld-gefunden'), 'Gefunden bekommt seine Klasse');
|
||||||
|
$this->assert(str_contains($gefunden, '✓'), 'Und sein Zeichen');
|
||||||
|
$this->assert(str_contains($gefunden, 'Im Bargeld-Tool gefunden'), 'Und den Standardtitel');
|
||||||
|
|
||||||
|
$fehlt = View::bargeldMarke('fehlt', null);
|
||||||
|
$this->assert(str_contains($fehlt, 'bargeld-fehlt'), 'Fehlt bekommt seine Klasse');
|
||||||
|
$this->assert(str_contains($fehlt, '✗'), 'Und sein Zeichen');
|
||||||
|
|
||||||
|
$fehler = View::bargeldMarke('fehler', 'Zeitüberschreitung');
|
||||||
|
$this->assert(str_contains($fehler, 'bargeld-fehler'), 'Fehler bekommt seine Klasse');
|
||||||
|
$this->assert(str_contains($fehler, '!'), 'Und sein Zeichen');
|
||||||
|
$this->assert(str_contains($fehler, 'Zeitüberschreitung'), 'Ein eigener Hinweis geht vor dem Standardtitel');
|
||||||
|
|
||||||
|
$neutral = View::bargeldMarke(null, null);
|
||||||
|
$this->assert(str_contains($neutral, 'bargeld-neutral'), 'Ohne Status der graue Kreis');
|
||||||
|
$this->assert(str_contains($neutral, '●'), 'Und sein Zeichen');
|
||||||
|
|
||||||
|
$unbekannt = View::bargeldMarke('unfug', null);
|
||||||
|
$this->assertEquals($neutral, $unbekannt, 'Ein unbekannter Status gilt wie kein Status');
|
||||||
|
|
||||||
|
$this->assert(
|
||||||
|
str_contains(View::bargeldMarke('gefunden', '<script>'), '<script>'),
|
||||||
|
'Ein Hinweis wird maskiert'
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -181,6 +181,38 @@ test.describe('Buchungen', () => {
|
||||||
.toContainText(KATEGORIE)
|
.toContainText(KATEGORIE)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Fehler #40: Das Speichern lud bisher die ganze Seite neu und riss den
|
||||||
|
// Bildschirm nach oben – bei einer Buchung weiter unten in der Liste ein
|
||||||
|
// langer Weg zurück. Die Markierung übersteht nur, wenn nichts neu lädt.
|
||||||
|
test('Kategorie ändern lädt die Seite nicht neu und lässt die Bildlaufposition stehen', async ({ page }) => {
|
||||||
|
// Ein niedriges Fenster erzwingt Bildlauf, unabhängig davon, wie viele
|
||||||
|
// Buchungen die vorigen Tests schon angelegt haben.
|
||||||
|
await page.setViewportSize({ width: 1280, height: 400 })
|
||||||
|
|
||||||
|
await page.goto(`/konten/${giroId}/buchung`)
|
||||||
|
await page.fill('#datum', '2026-04-05')
|
||||||
|
await page.fill('#betrag', '5,55')
|
||||||
|
await page.fill('#empfaenger', `Kein Neuladen ${MARKE}`)
|
||||||
|
await page.click('.buchungsmaske button[type="submit"]')
|
||||||
|
await expect(page.locator('.meldung-erfolg')).toBeVisible()
|
||||||
|
|
||||||
|
await page.goto(`/konten/${giroId}`)
|
||||||
|
await page.evaluate(() => { window.scrollTo(0, 300) })
|
||||||
|
await page.evaluate(() => { window.__e2eKeinNeuladen = true })
|
||||||
|
|
||||||
|
const zeile = page.locator('tr', { hasText: `Kein Neuladen ${MARKE}` })
|
||||||
|
await zeile.locator('a.zuordnungsfeld').click()
|
||||||
|
await zeile.locator('#zuordnungsauswahl select').selectOption({ label: KATEGORIE })
|
||||||
|
|
||||||
|
await expect(page.locator('.meldung-erfolg')).toContainText('Zuordnung')
|
||||||
|
await expect(zeile).toContainText(KATEGORIE)
|
||||||
|
|
||||||
|
// Ein echtes Neuladen hätte sowohl die Markierung gelöscht als auch die
|
||||||
|
// Bildlaufposition auf null zurückgesetzt.
|
||||||
|
expect(await page.evaluate(() => window.__e2eKeinNeuladen)).toBe(true)
|
||||||
|
expect(await page.evaluate(() => window.scrollY)).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
|
||||||
test('Klasse im Kontoblatt zuordnen und wieder löschen', async ({ page }) => {
|
test('Klasse im Kontoblatt zuordnen und wieder löschen', async ({ page }) => {
|
||||||
await page.goto(`/konten/${giroId}/buchung`)
|
await page.goto(`/konten/${giroId}/buchung`)
|
||||||
await page.fill('#datum', '2026-04-06')
|
await page.fill('#datum', '2026-04-06')
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue