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>
This commit is contained in:
parent
d6c2c8f06e
commit
20b2406219
12 changed files with 397 additions and 260 deletions
17
Changes.md
17
Changes.md
|
|
@ -1,5 +1,22 @@
|
|||
# Änderungen
|
||||
|
||||
## Budgetplaner: drei Ansichten und Verteilen über das Zahnrad (11.08.2026)
|
||||
|
||||
- Abgelaufene Monate werden **nicht mehr zusammengefasst**. Alle zwölf Monate stehen
|
||||
einzeln da; die Sammelspalte „Januar–Vormonat“ entfällt samt
|
||||
`Budgetplaner::abgelaufenBis()` und `Budgetplaner::summe()`.
|
||||
- Statt *Verlauf* und *alle Monate* gibt es drei Ansichten: **Alles** (Budget, Ist und
|
||||
Saldo je Monat), **Nur Budget** und **Nur IST** mit je einer Spalte. Alte URLs mit
|
||||
`ansicht=verlauf|alle` fallen auf *Alles* zurück.
|
||||
- Die Budgetspalte ist überall dort, wo sie steht, editierbar – auch in vergangenen
|
||||
Monaten.
|
||||
- Vor jedem Eingabefeld sitzt ein **⚙**: *Ganzes Jahr* oder *Restliches Jahr* schreibt den
|
||||
Wert des Feldes in die übrigen Monate der Zeile und überschreibt, was dort stand.
|
||||
Gearbeitet wird auf den Eingabefeldern, sichtbar vor dem Speichern; der Knopf **↦** und
|
||||
`Budget::aufFolgemonateUebertragen()` entfallen dafür.
|
||||
- `app.js` war doppelt eingebunden (Kopf **und** Fuß): Jeder Handler lief zweimal, jede
|
||||
Rückfrage kam zweimal. Jetzt nur noch im Kopf.
|
||||
|
||||
## Phase 3 – Budgetplaner und Monatsabschluss (11.08.2026)
|
||||
|
||||
**Datenmodell**
|
||||
|
|
|
|||
|
|
@ -53,18 +53,36 @@ zählten Unterkategorien mehrfach.
|
|||
|
||||
### Ansichten
|
||||
|
||||
| Ansicht | Spalten |
|
||||
Alle zwölf Monate stehen immer einzeln da – auch die abgelaufenen. Zusammengefasst wird
|
||||
nichts: Ein vergangener Monat ließe sich sonst weder nachlesen noch nachplanen. Die Ansicht
|
||||
entscheidet nur, welche der drei Wertspalten je Monat erscheinen:
|
||||
|
||||
| Ansicht | Spalten je Monat (und für das Jahr) |
|
||||
|---|---|
|
||||
| **Verlauf** (Standard) | Jahressumme, abgelaufene Monate zusammengefasst, laufender Monat mit Budget/Ist/Saldo, danach je Folgemonat eine Budgetspalte |
|
||||
| **Alle Monate** | Jahressumme und zwölf Budgetspalten – zum Planen des nächsten Jahres oder Nachbessern eines vergangenen |
|
||||
| **Alles** (Standard) | Budget, Ist, Saldo |
|
||||
| **Nur Budget** | Budget |
|
||||
| **Nur IST** | Ist |
|
||||
|
||||
Was als abgelaufen gilt, entscheidet `Budgetplaner::abgelaufenBis()`: im laufenden Jahr der
|
||||
Vormonat, in vergangenen Jahren der Dezember, in künftigen nichts.
|
||||
Wo die Budgetspalte steht, ist sie **immer editierbar**, im Januar wie im Dezember.
|
||||
`BudgetController::ansicht()` nimmt nur `alles`, `budget` und `ist` an; alles andere fällt
|
||||
auf `alles` zurück, damit ein altes Lesezeichen nicht ins Leere zeigt. Den laufenden Monat
|
||||
hebt `Budgetplaner::laufenderMonat()` farblich hervor.
|
||||
|
||||
Die ganze Tabelle steht in **einem** Formular und wird mit einem Knopf gespeichert – ohne
|
||||
JavaScript, damit mehrere geänderte Zeilen zusammen ankommen. Der Knopf **↦** in jeder
|
||||
Zeile überträgt den Wert des ersten Eingabemonats auf alle Folgemonate
|
||||
(`Budget::aufFolgemonateUebertragen()`).
|
||||
Die ganze Tabelle steht in **einem** Formular und wird mit einem Knopf gespeichert – so
|
||||
kommen mehrere geänderte Zeilen zusammen an.
|
||||
|
||||
### Werte über die Zeile verteilen
|
||||
|
||||
Vor jedem Eingabefeld sitzt ein **⚙**. Es öffnet zwei Einträge:
|
||||
|
||||
* **Ganzes Jahr** – der Wert des Feldes geht in alle zwölf Monate der Zeile
|
||||
* **Restliches Jahr** – nur in die Monate danach
|
||||
|
||||
Beides überschreibt vorhandene Werte und arbeitet in `app.js` auf den Eingabefeldern, nicht
|
||||
auf der Datenbank: Das Ergebnis ist vor dem Speichern zu sehen und geht mit demselben Knopf
|
||||
weg wie jede andere Änderung. Das Menü gibt es nur einmal im Dokument und wandert in die
|
||||
angeklickte Zelle – ein frei positioniertes bräuchte ein `style`-Attribut, das die
|
||||
Content-Security-Policy verwirft.
|
||||
|
||||
`Budget::kopieren()` legt ein Budget samt aller Monatswerte im neuen Jahr an – der Weg zum
|
||||
Jahresbudget, ohne 105 Kategorien noch einmal zu tippen.
|
||||
|
|
@ -104,8 +122,9 @@ rückwärts und endet am ersten bereits abgeschlossenen Monat.
|
|||
## Tests
|
||||
|
||||
* `tests/11_BudgetTest.php` – Vorzeichen, „nicht geplant“, Ist-Werte samt Splits,
|
||||
Baumsummen, Saldo, Folgemonate, Kopie
|
||||
Baumsummen, Saldo, Kopie, Ansichtswahl
|
||||
* `tests/12_MonatsabschlussTest.php` – Zeiträume, Prüflauf, Snapshot, Sperre,
|
||||
Wiedereröffnung, Vorschlag
|
||||
* `tests/e2e/specs/planung.spec.js` – Budget anlegen, Wert eintragen und wiederfinden,
|
||||
Übertragen auf Folgemonate, Abschluss mit Snapshot und Wiedereröffnung
|
||||
Spaltenbild der drei Ansichten, Verteilen über das Zahnrad, Abschluss mit Snapshot und
|
||||
Wiedereröffnung
|
||||
|
|
|
|||
|
|
@ -451,6 +451,58 @@ table.budgetplaner td.rechts {
|
|||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* Zahnrad vor jedem Eingabefeld: verteilt den Wert über die Zeile. Das Menü
|
||||
dazu gibt es nur einmal; app.js hängt es in die angeklickte Zelle, die es
|
||||
deshalb verankern muss. Positioniert wird über CSS, nicht über ein per
|
||||
JavaScript gesetztes style-Attribut – das verwürfe die
|
||||
Content-Security-Policy (style-src 'self'). */
|
||||
table.budgetplaner td.budgetzelle { position: relative; }
|
||||
|
||||
.zahnrad {
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--text-gedaempft);
|
||||
cursor: pointer;
|
||||
font-size: .85rem;
|
||||
line-height: 1;
|
||||
padding: 0 .25rem 0 0;
|
||||
}
|
||||
|
||||
.zahnrad:hover,
|
||||
.zahnrad[aria-expanded="true"] { color: var(--akzent); }
|
||||
|
||||
.zahnradmenue {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: .3rem;
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 10rem;
|
||||
padding: .2rem;
|
||||
border: 1px solid var(--rand-kraeftig);
|
||||
border-radius: var(--radius);
|
||||
background: var(--flaeche);
|
||||
box-shadow: 0 4px 12px rgb(0 0 0 / 18%);
|
||||
}
|
||||
|
||||
/* Fließtext, nicht Zahlensatz: Die Zelle vererbt sonst ihre Ziffernschrift. */
|
||||
.menueeintrag {
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-family: var(--schrift);
|
||||
font-size: .88rem;
|
||||
text-align: left;
|
||||
padding: .35rem .5rem;
|
||||
border-radius: var(--radius);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.menueeintrag:hover { background: var(--flaeche-gedaempft); }
|
||||
|
||||
/* Aufklappen des Kategoriebaums; ohne JavaScript bleibt alles sichtbar. */
|
||||
.klappknopf {
|
||||
border: none;
|
||||
|
|
@ -711,6 +763,14 @@ table.splittabelle input.rechts { text-align: right; }
|
|||
font-size: .85rem;
|
||||
}
|
||||
|
||||
/* Ein Knopf, der die gerade gewählte Ansicht markiert – etwa die drei
|
||||
Spaltenbilder des Budgetplaners. */
|
||||
.knopf.aktiv {
|
||||
background: var(--akzent-schwach);
|
||||
border-color: var(--akzent);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.knopf.gefahr {
|
||||
border-color: var(--negativ);
|
||||
color: var(--negativ);
|
||||
|
|
|
|||
|
|
@ -52,6 +52,102 @@
|
|||
});
|
||||
});
|
||||
|
||||
// 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,
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ use Finanzmanager\View;
|
|||
* Budgetverwaltung (anlegen, kopieren, aktivieren).
|
||||
*
|
||||
* Die Monatswerte werden nicht einzeln gespeichert, sondern die ganze Tabelle
|
||||
* auf einmal – ein Formular um die Tabelle herum, ein POST. Das kommt ohne
|
||||
* JavaScript aus und hält die Eingabe zusammen, wenn mehrere Zeilen geändert
|
||||
* wurden.
|
||||
* auf einmal – ein Formular um die Tabelle herum, ein POST. Das hält die
|
||||
* Eingabe zusammen, wenn mehrere Zeilen geändert wurden, und lässt das
|
||||
* Zahnrad einer Zelle ihren Wert vor dem Speichern über die Zeile verteilen.
|
||||
*/
|
||||
class BudgetController extends BaseController
|
||||
{
|
||||
|
|
@ -29,16 +29,12 @@ class BudgetController extends BaseController
|
|||
return;
|
||||
}
|
||||
|
||||
$ansicht = $this->query('ansicht') === 'alle' ? 'alle' : 'verlauf';
|
||||
$tabelle = Budgetplaner::tabelle($budget);
|
||||
|
||||
$this->render('planung/budgetplaner.php', [
|
||||
'titel' => 'Budgetplaner',
|
||||
'budget' => $budget,
|
||||
'budgets' => Budget::alle(),
|
||||
'tabelle' => $tabelle,
|
||||
'ansicht' => $ansicht,
|
||||
'abgelaufenBis' => Budgetplaner::abgelaufenBis((int)$budget['jahr']),
|
||||
'tabelle' => Budgetplaner::tabelle($budget),
|
||||
'ansicht' => self::ansicht($this->query('ansicht')),
|
||||
'laufenderMonat' => Budgetplaner::laufenderMonat((int)$budget['jahr']),
|
||||
'nurGeplante' => $this->query('nur_geplante') === '1',
|
||||
]);
|
||||
|
|
@ -61,7 +57,7 @@ class BudgetController extends BaseController
|
|||
return;
|
||||
}
|
||||
|
||||
$werte = $_POST['betrag'] ?? [];
|
||||
$werte = $_POST['betrag'] ?? [];
|
||||
$geaendert = 0;
|
||||
|
||||
if (is_array($werte)) {
|
||||
|
|
@ -77,16 +73,10 @@ class BudgetController extends BaseController
|
|||
}
|
||||
}
|
||||
|
||||
// „Auf Folgemonate übertragen“ kommt als Knopfwert „kategorie:monat“ und
|
||||
// greift auf den soeben gespeicherten Stand.
|
||||
$meldung = $geaendert > 0 ? 'Budget gespeichert.' : 'Nichts zu speichern.';
|
||||
$uebertragen = $this->post('uebertragen');
|
||||
if ($uebertragen !== '' && preg_match('/^(\d+):(\d{1,2})$/', $uebertragen, $treffer) === 1) {
|
||||
Budget::aufFolgemonateUebertragen($id, (int)$treffer[1], (int)$treffer[2]);
|
||||
$meldung = 'Wert auf die Folgemonate übertragen.';
|
||||
}
|
||||
|
||||
View::redirect($this->zurueckZumPlaner($id), $meldung);
|
||||
View::redirect(
|
||||
$this->zurueckZumPlaner($id),
|
||||
$geaendert > 0 ? 'Budget gespeichert.' : 'Nichts zu speichern.'
|
||||
);
|
||||
}
|
||||
|
||||
public function verwaltung(): void
|
||||
|
|
@ -200,6 +190,16 @@ class BudgetController extends BaseController
|
|||
return trim($eingabe) === '' ? null : KontenController::zahl($eingabe);
|
||||
}
|
||||
|
||||
/**
|
||||
* Die drei Ansichten des Planers: alle drei Wertspalten je Monat, nur die
|
||||
* Budgetspalte oder nur die Ist-Spalte. Unbekanntes fällt auf „alles“
|
||||
* zurück, damit eine alte Lesezeichen-URL nicht ins Leere zeigt.
|
||||
*/
|
||||
public static function ansicht(string $gewaehlt): string
|
||||
{
|
||||
return in_array($gewaehlt, ['budget', 'ist'], true) ? $gewaehlt : 'alles';
|
||||
}
|
||||
|
||||
/** @return array<string,mixed>|null */
|
||||
private function gewaehltesBudget(): ?array
|
||||
{
|
||||
|
|
@ -217,10 +217,7 @@ class BudgetController extends BaseController
|
|||
/** Nach dem Speichern zurück in dieselbe Ansicht, nicht auf das aktive Budget. */
|
||||
private function zurueckZumPlaner(int $id): string
|
||||
{
|
||||
$pfad = '/planung/budget?budget=' . $id;
|
||||
if ($this->post('ansicht') === 'alle') {
|
||||
$pfad .= '&ansicht=alle';
|
||||
}
|
||||
$pfad = '/planung/budget?budget=' . $id . '&ansicht=' . self::ansicht($this->post('ansicht'));
|
||||
if ($this->post('nur_geplante') === '1') {
|
||||
$pfad .= '&nur_geplante=1';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -185,37 +185,6 @@ class Budget
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trägt den Wert eines Monats in alle folgenden Monate des Jahres ein –
|
||||
* der „auf Folgemonate übertragen“-Knopf des FinanzManagers.
|
||||
*/
|
||||
public static function aufFolgemonateUebertragen(int $budgetId, int $kategorieId, int $abMonat): void
|
||||
{
|
||||
$betrag = Database::fetchValue(
|
||||
'SELECT betrag FROM budget_positionen WHERE budget_id = ? AND kategorie_id = ? AND monat = ?',
|
||||
[$budgetId, $kategorieId, $abMonat]
|
||||
);
|
||||
|
||||
Database::transaktion(static function () use ($budgetId, $kategorieId, $abMonat, $betrag): void {
|
||||
for ($monat = $abMonat + 1; $monat <= 12; $monat++) {
|
||||
if ($betrag === null) {
|
||||
Database::execute(
|
||||
'DELETE FROM budget_positionen WHERE budget_id = ? AND kategorie_id = ? AND monat = ?',
|
||||
[$budgetId, $kategorieId, $monat]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
Database::execute(
|
||||
'INSERT INTO budget_positionen (budget_id, kategorie_id, monat, betrag)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE betrag = VALUES(betrag)',
|
||||
[$budgetId, $kategorieId, $monat, (float)$betrag]
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Legt eine Kopie samt aller Positionen an – so entsteht das Budget für das
|
||||
* neue Jahr aus dem laufenden, statt 105 Kategorien noch einmal zu tippen.
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@ use Finanzmanager\Models\Kategorie;
|
|||
|
||||
/**
|
||||
* Stellt die Tabelle des Budgetplaners zusammen: Zeilen sind der Kategoriebaum,
|
||||
* Spalten die zwölf Monate mit Budget, Ist und Saldo.
|
||||
* Spalten die zwölf Monate mit Budget, Ist und Saldo. Zusammengefasst wird
|
||||
* nichts – auch abgelaufene Monate stehen einzeln da und bleiben beplanbar;
|
||||
* welche der drei Wertspalten je Monat erscheinen, entscheidet die Ansicht.
|
||||
*
|
||||
* Vorzeichen durchgehend wie in der Buchhaltung (Ausgaben negativ). Daraus
|
||||
* folgt eine Definition, die für Einnahmen wie Ausgaben gleich funktioniert:
|
||||
|
|
@ -151,40 +153,6 @@ class Budgetplaner
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Summiert einen Monatsbereich einer Zeile – für die zusammengefassten
|
||||
* Spalten „Januar bis Vormonat“ und den Jahresüberblick.
|
||||
*
|
||||
* @param array<int, float> $reihe
|
||||
*/
|
||||
public static function summe(array $reihe, int $vonMonat, int $bisMonat): float
|
||||
{
|
||||
$summe = 0.0;
|
||||
for ($monat = max(1, $vonMonat); $monat <= min(12, $bisMonat); $monat++) {
|
||||
$summe += $reihe[$monat] ?? 0.0;
|
||||
}
|
||||
return $summe;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bis zu welchem Monat das Jahr abgelaufen ist: im laufenden Jahr der
|
||||
* Vormonat, in vergangenen Jahren der Dezember, in künftigen gar nichts.
|
||||
*/
|
||||
public static function abgelaufenBis(int $jahr, ?string $heute = null): int
|
||||
{
|
||||
$heute = $heute ?? date('Y-m-d');
|
||||
$jetztJahr = (int)substr($heute, 0, 4);
|
||||
$jetztMonat = (int)substr($heute, 5, 2);
|
||||
|
||||
if ($jahr < $jetztJahr) {
|
||||
return 12;
|
||||
}
|
||||
if ($jahr > $jetztJahr) {
|
||||
return 0;
|
||||
}
|
||||
return $jetztMonat - 1;
|
||||
}
|
||||
|
||||
/** Der Monat, der im laufenden Jahr gerade läuft – sonst 0. */
|
||||
public static function laufenderMonat(int $jahr, ?string $heute = null): int
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/app.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ $istAktiv = static function (string $pfad) use ($aktuellerPfad): bool {
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title><?= View::h($titel ?? 'Finanzmanager') ?> – Finanzmanager</title>
|
||||
<link rel="stylesheet" href="/static/app.css">
|
||||
<!-- Nur hier eingebunden: Ein zweites Mal im Fuß ließe jeden Handler doppelt
|
||||
laufen – Rückfragen kämen zweimal, Menüs entstünden zweimal. -->
|
||||
<script src="/static/app.js" defer></script>
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><text y='13' font-size='13'>€</text></svg>">
|
||||
</head>
|
||||
|
|
|
|||
|
|
@ -7,19 +7,23 @@ use Finanzmanager\View;
|
|||
/** @var list<array<string,mixed>> $budgets */
|
||||
/** @var array{zeilen: list<array<string,mixed>>, gesamt: array<string,mixed>, jahr: int} $tabelle */
|
||||
/** @var string $ansicht */
|
||||
/** @var int $abgelaufenBis */
|
||||
/** @var int $laufenderMonat */
|
||||
/** @var bool $nurGeplante */
|
||||
/** @var string $csrfToken */
|
||||
|
||||
$jahr = (int)$budget['jahr'];
|
||||
|
||||
// Welche Monate bekommen eine Eingabespalte? In der Verlaufsansicht sind das
|
||||
// der laufende Monat und alles danach – abgelaufene Monate fasst eine einzige
|
||||
// Spalte zusammen, wie im FinanzManager. „Alle Monate“ zeigt alle zwölf.
|
||||
$alleMonate = $ansicht === 'alle';
|
||||
$erster = $alleMonate ? 1 : max(1, $laufenderMonat === 0 ? 1 : $laufenderMonat);
|
||||
$eingabe = range($erster, 12);
|
||||
// Alle zwölf Monate stehen einzeln da – auch die abgelaufenen. Zusammengefasst
|
||||
// wird nichts mehr: Ein vergangener Monat lässt sich sonst weder nachlesen noch
|
||||
// nachplanen. Die Ansicht entscheidet nur, welche Wertspalten je Monat kommen.
|
||||
$ansichten = ['alles' => 'Alles', 'budget' => 'Nur Budget', 'ist' => 'Nur IST'];
|
||||
|
||||
$zeigtBudget = $ansicht !== 'ist';
|
||||
$zeigtIst = $ansicht !== 'budget';
|
||||
$zeigtSaldo = $ansicht === 'alles';
|
||||
$spalten = ($zeigtBudget ? 1 : 0) + ($zeigtIst ? 1 : 0) + ($zeigtSaldo ? 1 : 0);
|
||||
|
||||
$monate = range(1, 12);
|
||||
|
||||
$zelle = static function (float $wert): string {
|
||||
return View::betrag($wert, false);
|
||||
|
|
@ -58,9 +62,14 @@ include __DIR__ . '/../layout/kopf.php';
|
|||
<button type="submit" class="knopf klein">Öffnen</button>
|
||||
</form>
|
||||
|
||||
<a href="<?= View::h($link(['ansicht' => $alleMonate ? 'verlauf' : 'alle'])) ?>" class="knopf">
|
||||
<?= $alleMonate ? 'Ansicht: Verlauf' : 'Ansicht: alle Monate' ?>
|
||||
</a>
|
||||
<?php foreach ($ansichten as $wert => $beschriftung): ?>
|
||||
<a href="<?= View::h($link(['ansicht' => $wert])) ?>"
|
||||
class="knopf<?= $ansicht === $wert ? ' aktiv' : '' ?>"
|
||||
<?= $ansicht === $wert ? 'aria-current="page"' : '' ?>>
|
||||
<?= View::h($beschriftung) ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<a href="<?= View::h($link(['nur_geplante' => $nurGeplante ? null : '1'])) ?>" class="knopf">
|
||||
<?= $nurGeplante ? 'Alle Kategorien' : 'Nur geplante Kategorien' ?>
|
||||
</a>
|
||||
|
|
@ -71,11 +80,17 @@ include __DIR__ . '/../layout/kopf.php';
|
|||
|
||||
<p class="hinweis">
|
||||
Budget <strong><?= View::h($budget['name']) ?></strong> für <?= $jahr ?>.
|
||||
Ausgaben werden ohne Vorzeichen eingetragen – die Kategorieart bestimmt es.
|
||||
Ein leeres Feld heißt „nicht geplant“, eine 0 heißt „mit 0,00 geplant“.
|
||||
<strong>Saldo</strong> ist Ist minus Budget: negativ bedeutet über Plan.
|
||||
<?php if ($zeigtSaldo): ?>
|
||||
<strong>Saldo</strong> ist Ist minus Budget: negativ bedeutet über Plan.
|
||||
<?php endif; ?>
|
||||
Übergeordnete Kategorien summieren ihre Unterkategorien und werden deshalb
|
||||
nicht selbst beplant.
|
||||
<?php if ($zeigtBudget): ?>
|
||||
Ausgaben werden ohne Vorzeichen eingetragen – die Kategorieart bestimmt es.
|
||||
Ein leeres Feld heißt „nicht geplant“, eine 0 heißt „mit 0,00 geplant“.
|
||||
Das <span aria-hidden="true">⚙</span> vor einem Feld überträgt dessen Wert
|
||||
auf das ganze oder das restliche Jahr.
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
|
||||
<form method="post" action="/planung/budget/<?= (int)$budget['id'] ?>/positionen">
|
||||
|
|
@ -85,48 +100,35 @@ include __DIR__ . '/../layout/kopf.php';
|
|||
<input type="hidden" name="nur_geplante" value="1">
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="knopfleiste">
|
||||
<button type="submit" class="knopf primaer">Budget speichern</button>
|
||||
</div>
|
||||
<?php if ($zeigtBudget): ?>
|
||||
<div class="knopfleiste">
|
||||
<button type="submit" class="knopf primaer">Budget speichern</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="breittabelle">
|
||||
<table class="liste budgetplaner">
|
||||
<thead>
|
||||
<tr>
|
||||
<th rowspan="2" class="spalte-kategorie">Kategorie</th>
|
||||
<th colspan="3" class="gruppenkopf">Jahr <?= $jahr ?></th>
|
||||
<?php if (!$alleMonate && $abgelaufenBis > 0): ?>
|
||||
<th colspan="3" class="gruppenkopf">
|
||||
Januar–<?= View::h(Budgetplaner::MONATSKUERZEL[$abgelaufenBis]) ?>
|
||||
</th>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($eingabe as $monat): ?>
|
||||
<th class="gruppenkopf<?= $monat === $laufenderMonat ? ' laufend' : '' ?>"
|
||||
colspan="<?= !$alleMonate && $monat === $laufenderMonat ? 3 : 1 ?>">
|
||||
<th <?= $spalten > 1 ? 'rowspan="2" ' : '' ?>class="spalte-kategorie">Kategorie</th>
|
||||
<th colspan="<?= $spalten ?>" class="gruppenkopf">Jahr <?= $jahr ?></th>
|
||||
<?php foreach ($monate as $monat): ?>
|
||||
<th colspan="<?= $spalten ?>"
|
||||
class="gruppenkopf<?= $monat === $laufenderMonat ? ' laufend' : '' ?>">
|
||||
<?= View::h(Budgetplaner::MONATSKUERZEL[$monat]) ?>
|
||||
</th>
|
||||
<?php endforeach; ?>
|
||||
<th rowspan="2" class="rechts">↦</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="rechts">Budget</th>
|
||||
<th class="rechts">Ist</th>
|
||||
<th class="rechts">Saldo</th>
|
||||
<?php if (!$alleMonate && $abgelaufenBis > 0): ?>
|
||||
<th class="rechts">Budget</th>
|
||||
<th class="rechts">Ist</th>
|
||||
<th class="rechts">Saldo</th>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($eingabe as $monat): ?>
|
||||
<?php if (!$alleMonate && $monat === $laufenderMonat): ?>
|
||||
<th class="rechts laufend">Budget</th>
|
||||
<th class="rechts laufend">Ist</th>
|
||||
<th class="rechts laufend">Saldo</th>
|
||||
<?php else: ?>
|
||||
<th class="rechts">Budget</th>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
<?php if ($spalten > 1): ?>
|
||||
<tr>
|
||||
<?php foreach (array_merge([0], $monate) as $block): ?>
|
||||
<?php $laufend = $block > 0 && $block === $laufenderMonat ? ' laufend' : ''; ?>
|
||||
<?php if ($zeigtBudget): ?><th class="rechts<?= $laufend ?>">Budget</th><?php endif; ?>
|
||||
<?php if ($zeigtIst): ?><th class="rechts<?= $laufend ?>">Ist</th><?php endif; ?>
|
||||
<?php if ($zeigtSaldo): ?><th class="rechts<?= $laufend ?>">Saldo</th><?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($tabelle['zeilen'] as $zeile): ?>
|
||||
|
|
@ -134,9 +136,9 @@ include __DIR__ . '/../layout/kopf.php';
|
|||
if ($nurGeplante && !$zeile['geplant']) {
|
||||
continue;
|
||||
}
|
||||
$id = (int)$zeile['id'];
|
||||
$blatt = !$zeile['hat_kinder'];
|
||||
$klassen = $blatt ? '' : ' summenzeile-kategorie';
|
||||
$id = (int)$zeile['id'];
|
||||
$blatt = !$zeile['hat_kinder'];
|
||||
$klassen = $blatt ? '' : ' summenzeile-kategorie';
|
||||
?>
|
||||
<tr class="kategoriezeile<?= $klassen ?>" data-kategorie="<?= $id ?>"
|
||||
data-eltern="<?= $zeile['eltern_id'] === null ? '' : (int)$zeile['eltern_id'] ?>">
|
||||
|
|
@ -150,93 +152,87 @@ include __DIR__ . '/../layout/kopf.php';
|
|||
</span>
|
||||
</td>
|
||||
|
||||
<td class="rechts"><?= $zelle($zeile['jahrBudget']) ?></td>
|
||||
<td class="rechts"><?= $zelle($zeile['jahrIst']) ?></td>
|
||||
<td class="rechts <?= View::betragKlasse($zeile['jahrSaldo']) ?>">
|
||||
<?= $zelle($zeile['jahrSaldo']) ?>
|
||||
</td>
|
||||
|
||||
<?php if (!$alleMonate && $abgelaufenBis > 0): ?>
|
||||
<?php
|
||||
$budgetBis = Budgetplaner::summe($zeile['budget'], 1, $abgelaufenBis);
|
||||
$istBis = Budgetplaner::summe($zeile['ist'], 1, $abgelaufenBis);
|
||||
?>
|
||||
<td class="rechts nebentext"><?= $zelle($budgetBis) ?></td>
|
||||
<td class="rechts nebentext"><?= $zelle($istBis) ?></td>
|
||||
<td class="rechts <?= View::betragKlasse($istBis - $budgetBis) ?>">
|
||||
<?= $zelle($istBis - $budgetBis) ?>
|
||||
<?php if ($zeigtBudget): ?>
|
||||
<td class="rechts"><?= $zelle($zeile['jahrBudget']) ?></td>
|
||||
<?php endif; ?>
|
||||
<?php if ($zeigtIst): ?>
|
||||
<td class="rechts"><?= $zelle($zeile['jahrIst']) ?></td>
|
||||
<?php endif; ?>
|
||||
<?php if ($zeigtSaldo): ?>
|
||||
<td class="rechts <?= View::betragKlasse($zeile['jahrSaldo']) ?>">
|
||||
<?= $zelle($zeile['jahrSaldo']) ?>
|
||||
</td>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php foreach ($eingabe as $monat): ?>
|
||||
<td class="rechts<?= $monat === $laufenderMonat ? ' laufend' : '' ?>">
|
||||
<?php if ($blatt): ?>
|
||||
<input type="text" inputmode="decimal" class="betragsfeld"
|
||||
name="betrag[<?= $id ?>][<?= $monat ?>]"
|
||||
value="<?= isset($zeile['eigenBudget'][$monat])
|
||||
? View::h(View::betrag(abs($zeile['eigenBudget'][$monat]), false)) : '' ?>"
|
||||
aria-label="<?= View::h($zeile['name'] . ' ' . Budgetplaner::MONATSNAMEN[$monat]) ?>">
|
||||
<?php else: ?>
|
||||
<?= $zelle($zeile['budget'][$monat]) ?>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<?php if (!$alleMonate && $monat === $laufenderMonat): ?>
|
||||
<td class="rechts laufend"><?= $zelle($zeile['ist'][$monat]) ?></td>
|
||||
<td class="rechts laufend <?= View::betragKlasse($zeile['saldo'][$monat]) ?>">
|
||||
<?php foreach ($monate as $monat): ?>
|
||||
<?php $laufend = $monat === $laufenderMonat ? ' laufend' : ''; ?>
|
||||
<?php if ($zeigtBudget): ?>
|
||||
<td class="rechts budgetzelle<?= $laufend ?>">
|
||||
<?php if ($blatt): ?>
|
||||
<button type="button" class="zahnrad" aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
aria-label="<?= View::h($zeile['name'] . ' ' . Budgetplaner::MONATSNAMEN[$monat]) ?> übertragen">⚙</button>
|
||||
<input type="text" inputmode="decimal" class="betragsfeld"
|
||||
data-monat="<?= $monat ?>"
|
||||
name="betrag[<?= $id ?>][<?= $monat ?>]"
|
||||
value="<?= isset($zeile['eigenBudget'][$monat])
|
||||
? View::h(View::betrag(abs($zeile['eigenBudget'][$monat]), false)) : '' ?>"
|
||||
aria-label="<?= View::h($zeile['name'] . ' ' . Budgetplaner::MONATSNAMEN[$monat]) ?>">
|
||||
<?php else: ?>
|
||||
<?= $zelle($zeile['budget'][$monat]) ?>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<?php endif; ?>
|
||||
<?php if ($zeigtIst): ?>
|
||||
<td class="rechts<?= $laufend ?>"><?= $zelle($zeile['ist'][$monat]) ?></td>
|
||||
<?php endif; ?>
|
||||
<?php if ($zeigtSaldo): ?>
|
||||
<td class="rechts<?= $laufend ?> <?= View::betragKlasse($zeile['saldo'][$monat]) ?>">
|
||||
<?= $zelle($zeile['saldo'][$monat]) ?>
|
||||
</td>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<td class="rechts">
|
||||
<?php if ($blatt): ?>
|
||||
<button type="submit" name="uebertragen" class="knopf klein"
|
||||
value="<?= $id ?>:<?= $erster ?>"
|
||||
title="Wert aus <?= View::h(Budgetplaner::MONATSNAMEN[$erster]) ?> auf alle Folgemonate übertragen">↦</button>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="summenzeile">
|
||||
<td class="spalte-kategorie">Gesamt</td>
|
||||
<td class="rechts"><?= $zelle($tabelle['gesamt']['jahrBudget']) ?></td>
|
||||
<td class="rechts"><?= $zelle($tabelle['gesamt']['jahrIst']) ?></td>
|
||||
<td class="rechts <?= View::betragKlasse($tabelle['gesamt']['jahrSaldo']) ?>">
|
||||
<?= $zelle($tabelle['gesamt']['jahrSaldo']) ?>
|
||||
</td>
|
||||
<?php if (!$alleMonate && $abgelaufenBis > 0): ?>
|
||||
<?php
|
||||
$budgetBis = Budgetplaner::summe($tabelle['gesamt']['budget'], 1, $abgelaufenBis);
|
||||
$istBis = Budgetplaner::summe($tabelle['gesamt']['ist'], 1, $abgelaufenBis);
|
||||
?>
|
||||
<td class="rechts"><?= $zelle($budgetBis) ?></td>
|
||||
<td class="rechts"><?= $zelle($istBis) ?></td>
|
||||
<td class="rechts <?= View::betragKlasse($istBis - $budgetBis) ?>">
|
||||
<?= $zelle($istBis - $budgetBis) ?>
|
||||
<?php if ($zeigtBudget): ?>
|
||||
<td class="rechts"><?= $zelle($tabelle['gesamt']['jahrBudget']) ?></td>
|
||||
<?php endif; ?>
|
||||
<?php if ($zeigtIst): ?>
|
||||
<td class="rechts"><?= $zelle($tabelle['gesamt']['jahrIst']) ?></td>
|
||||
<?php endif; ?>
|
||||
<?php if ($zeigtSaldo): ?>
|
||||
<td class="rechts <?= View::betragKlasse($tabelle['gesamt']['jahrSaldo']) ?>">
|
||||
<?= $zelle($tabelle['gesamt']['jahrSaldo']) ?>
|
||||
</td>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($eingabe as $monat): ?>
|
||||
<td class="rechts<?= $monat === $laufenderMonat ? ' laufend' : '' ?>">
|
||||
<?= $zelle($tabelle['gesamt']['budget'][$monat]) ?>
|
||||
</td>
|
||||
<?php if (!$alleMonate && $monat === $laufenderMonat): ?>
|
||||
<td class="rechts laufend"><?= $zelle($tabelle['gesamt']['ist'][$monat]) ?></td>
|
||||
<td class="rechts laufend <?= View::betragKlasse($tabelle['gesamt']['saldo'][$monat]) ?>">
|
||||
<?php foreach ($monate as $monat): ?>
|
||||
<?php $laufend = $monat === $laufenderMonat ? ' laufend' : ''; ?>
|
||||
<?php if ($zeigtBudget): ?>
|
||||
<td class="rechts<?= $laufend ?>"><?= $zelle($tabelle['gesamt']['budget'][$monat]) ?></td>
|
||||
<?php endif; ?>
|
||||
<?php if ($zeigtIst): ?>
|
||||
<td class="rechts<?= $laufend ?>"><?= $zelle($tabelle['gesamt']['ist'][$monat]) ?></td>
|
||||
<?php endif; ?>
|
||||
<?php if ($zeigtSaldo): ?>
|
||||
<td class="rechts<?= $laufend ?> <?= View::betragKlasse($tabelle['gesamt']['saldo'][$monat]) ?>">
|
||||
<?= $zelle($tabelle['gesamt']['saldo'][$monat]) ?>
|
||||
</td>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="knopfleiste">
|
||||
<button type="submit" class="knopf primaer">Budget speichern</button>
|
||||
</div>
|
||||
<?php if ($zeigtBudget): ?>
|
||||
<div class="knopfleiste">
|
||||
<button type="submit" class="knopf primaer">Budget speichern</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
|
||||
<?php include __DIR__ . '/../layout/fuss.php'; ?>
|
||||
|
|
|
|||
|
|
@ -31,9 +31,8 @@ class BudgetTest extends TestCase
|
|||
$this->istWerteAusBuchungenUndSplits();
|
||||
$this->tabelleSummiertDenBaum();
|
||||
$this->saldoZeigtUeberschreitung();
|
||||
$this->folgemonateUebertragen();
|
||||
$this->kopieFuerDasNaechsteJahr();
|
||||
$this->zeitraeumeDerAnsicht();
|
||||
$this->ansichtDesPlaners();
|
||||
}
|
||||
|
||||
private function stammdaten(): void
|
||||
|
|
@ -235,22 +234,6 @@ class BudgetTest extends TestCase
|
|||
);
|
||||
}
|
||||
|
||||
private function folgemonateUebertragen(): void
|
||||
{
|
||||
$budget = $this->budgetAnlegen();
|
||||
Budget::positionSetzen($budget, $this->lebensmittel, 3, 300.0);
|
||||
Budget::positionSetzen($budget, $this->lebensmittel, 12, 999.0);
|
||||
|
||||
Budget::aufFolgemonateUebertragen($budget, $this->lebensmittel, 3);
|
||||
|
||||
$positionen = Budget::positionen($budget)[$this->lebensmittel];
|
||||
$this->assert(!isset($positionen[2]), 'Der Vormonat bleibt unangetastet');
|
||||
$this->assertEquals(-300.0, $positionen[3], 'Der Ausgangsmonat bleibt stehen');
|
||||
$this->assertEquals(-300.0, $positionen[4], 'April übernimmt den Wert');
|
||||
$this->assertEquals(-300.0, $positionen[12], 'Auch ein vorhandener Dezemberwert wird überschrieben');
|
||||
$this->assertEquals(10, count($positionen), 'März bis Dezember sind gefüllt');
|
||||
}
|
||||
|
||||
private function kopieFuerDasNaechsteJahr(): void
|
||||
{
|
||||
$budget = $this->budgetAnlegen('Vorlage');
|
||||
|
|
@ -283,23 +266,19 @@ class BudgetTest extends TestCase
|
|||
$this->assert($doppelt, 'Zwei Budgets mit demselben Namen gehen nicht');
|
||||
}
|
||||
|
||||
/** Welche Monate gelten als abgelaufen, welcher läuft gerade? */
|
||||
private function zeitraeumeDerAnsicht(): void
|
||||
/**
|
||||
* Der laufende Monat wird im Planer hervorgehoben; die Ansicht entscheidet,
|
||||
* welche Wertspalten je Monat erscheinen.
|
||||
*/
|
||||
private function ansichtDesPlaners(): void
|
||||
{
|
||||
$this->assertEquals(7, Budgetplaner::abgelaufenBis(2026, '2026-08-11'), 'Im August sind sieben Monate durch');
|
||||
$this->assertEquals(0, Budgetplaner::abgelaufenBis(2026, '2026-01-15'), 'Im Januar ist noch nichts abgelaufen');
|
||||
$this->assertEquals(12, Budgetplaner::abgelaufenBis(2025, '2026-08-11'), 'Ein vergangenes Jahr ist ganz durch');
|
||||
$this->assertEquals(0, Budgetplaner::abgelaufenBis(2027, '2026-08-11'), 'Ein künftiges Jahr ist offen');
|
||||
|
||||
$this->assertEquals(8, Budgetplaner::laufenderMonat(2026, '2026-08-11'), 'Der laufende Monat ist der August');
|
||||
$this->assertEquals(0, Budgetplaner::laufenderMonat(2027, '2026-08-11'), 'Andere Jahre haben keinen laufenden Monat');
|
||||
|
||||
$reihe = [];
|
||||
for ($monat = 1; $monat <= 12; $monat++) {
|
||||
$reihe[$monat] = 10.0;
|
||||
}
|
||||
$this->assertEquals(70.0, Budgetplaner::summe($reihe, 1, 7), 'Summe über die abgelaufenen Monate');
|
||||
$this->assertEquals(120.0, Budgetplaner::summe($reihe, 1, 99), 'Über den Dezember hinaus wird nicht gezählt');
|
||||
$this->assertEquals(0.0, Budgetplaner::summe($reihe, 5, 4), 'Ein leerer Bereich ergibt null');
|
||||
$this->assertEquals('alles', BudgetController::ansicht('alles'), 'Alle drei Wertspalten');
|
||||
$this->assertEquals('budget', BudgetController::ansicht('budget'), 'Nur die Budgetspalte');
|
||||
$this->assertEquals('ist', BudgetController::ansicht('ist'), 'Nur die Ist-Spalte');
|
||||
$this->assertEquals('alles', BudgetController::ansicht(''), 'Ohne Angabe stehen alle Spalten da');
|
||||
$this->assertEquals('alles', BudgetController::ansicht('verlauf'), 'Eine alte URL fällt auf „alles“ zurück');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ const PASSWORT = process.env.E2E_PASSWORD || 'e2e-testpasswort'
|
|||
// und dürfen sich nicht an Resten eines früheren Laufs stoßen.
|
||||
const MARKE = Date.now().toString().slice(-6)
|
||||
|
||||
// Ein Jahr weit in der Zukunft: dort sind alle zwölf Monate Folgemonate, die
|
||||
// Spaltenaufteilung des Planers bleibt damit über die Jahre stabil.
|
||||
// Ein Jahr weit in der Zukunft: dort gibt es keine Buchungen, die Ist-Werte
|
||||
// bleiben leer und die geprüften Summen damit über die Jahre stabil.
|
||||
const JAHR = new Date().getFullYear() + 5
|
||||
|
||||
async function anmelden(page) {
|
||||
|
|
@ -65,7 +65,7 @@ test.describe('Planung', () => {
|
|||
test('Budget anlegen, Monatswert eintragen und wiederfinden', async ({ page }) => {
|
||||
const id = await budgetAnlegen(page, `E2E-Budget ${MARKE}`)
|
||||
|
||||
await page.goto(`/planung/budget?budget=${id}&ansicht=alle`)
|
||||
await page.goto(`/planung/budget?budget=${id}`)
|
||||
|
||||
const zeile = ersteBeplanbareZeile(page)
|
||||
const kategorie = (await zeile.locator('.baumname').innerText()).trim()
|
||||
|
|
@ -80,24 +80,59 @@ test.describe('Planung', () => {
|
|||
await expect(wieder).toContainText('250,00')
|
||||
})
|
||||
|
||||
test('Wert auf die Folgemonate übertragen', async ({ page }) => {
|
||||
const id = await budgetAnlegen(page, `E2E-Folgemonate ${MARKE}`)
|
||||
test('Die drei Ansichten zeigen Budget, Ist und Saldo je Monat', async ({ page }) => {
|
||||
const id = await budgetAnlegen(page, `E2E-Ansichten ${MARKE}`)
|
||||
|
||||
await page.goto(`/planung/budget?budget=${id}&ansicht=alle`)
|
||||
// Alles: zwölf Monate à drei Wertspalten, dazu die Jahresspalten.
|
||||
await page.goto(`/planung/budget?budget=${id}`)
|
||||
const zeile = ersteBeplanbareZeile(page)
|
||||
await expect(zeile.locator('td')).toHaveCount(1 + 3 + 12 * 3)
|
||||
await expect(zeile.locator('input.betragsfeld')).toHaveCount(12)
|
||||
// Kein Monat ist zusammengefasst: Januar steht als eigene Spalte da.
|
||||
await expect(page.locator('th.gruppenkopf', { hasText: 'Jan' })).toHaveCount(1)
|
||||
|
||||
await page.click('a:has-text("Nur Budget")')
|
||||
await expect(ersteBeplanbareZeile(page).locator('td')).toHaveCount(1 + 1 + 12)
|
||||
await expect(ersteBeplanbareZeile(page).locator('input.betragsfeld')).toHaveCount(12)
|
||||
|
||||
// Nur IST ist eine reine Leseansicht – ohne Eingabefelder und ohne Speichern.
|
||||
await page.click('a:has-text("Nur IST")')
|
||||
await expect(ersteBeplanbareZeile(page).locator('td')).toHaveCount(1 + 1 + 12)
|
||||
await expect(page.locator('input.betragsfeld')).toHaveCount(0)
|
||||
await expect(page.locator('button:has-text("Budget speichern")')).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('Zahnrad überträgt den Wert auf das ganze und das restliche Jahr', async ({ page }) => {
|
||||
const id = await budgetAnlegen(page, `E2E-Zahnrad ${MARKE}`)
|
||||
|
||||
await page.goto(`/planung/budget?budget=${id}&ansicht=budget`)
|
||||
|
||||
const zeile = ersteBeplanbareZeile(page)
|
||||
const kategorie = (await zeile.locator('.baumname').innerText()).trim()
|
||||
const felder = zeile.locator('input.betragsfeld')
|
||||
|
||||
await zeile.locator('input.betragsfeld').first().fill('100,00')
|
||||
await zeile.locator('button[name="uebertragen"]').click()
|
||||
await expect(page.locator('.meldung-erfolg')).toContainText('Folgemonate')
|
||||
|
||||
const wieder = page.locator('tr.kategoriezeile', { hasText: kategorie }).first()
|
||||
const felder = wieder.locator('input.betragsfeld')
|
||||
// Ganzes Jahr: der Wert aus dem März steht danach auch im Januar.
|
||||
await felder.nth(2).fill('100,00')
|
||||
await zeile.locator('.zahnrad').nth(2).click()
|
||||
await page.click('.zahnradmenue button:has-text("Ganzes Jahr")')
|
||||
await expect(felder.nth(0)).toHaveValue('100,00')
|
||||
await expect(felder.nth(11)).toHaveValue('100,00')
|
||||
// Zwölf Monate à 100,00 stehen als Jahressumme in der Zeile.
|
||||
await expect(wieder).toContainText('1.200,00')
|
||||
|
||||
// Restliches Jahr: die Monate davor bleiben stehen.
|
||||
await felder.nth(5).fill('40,00')
|
||||
await zeile.locator('.zahnrad').nth(5).click()
|
||||
await page.click('.zahnradmenue button:has-text("Restliches Jahr")')
|
||||
await expect(felder.nth(4)).toHaveValue('100,00')
|
||||
await expect(felder.nth(6)).toHaveValue('40,00')
|
||||
await expect(felder.nth(11)).toHaveValue('40,00')
|
||||
|
||||
// Erst das Speichern schreibt die verteilten Werte weg.
|
||||
await page.click('button:has-text("Budget speichern")')
|
||||
await expect(page.locator('.meldung-erfolg')).toContainText('Budget gespeichert')
|
||||
|
||||
const wieder = page.locator('tr.kategoriezeile', { hasText: kategorie }).first()
|
||||
// Fünf Monate à 100,00 und sieben à 40,00 ergeben 780,00 im Jahr.
|
||||
await expect(wieder).toContainText('780,00')
|
||||
})
|
||||
|
||||
test('Monat abschließen, Snapshot ansehen und wiedereröffnen', async ({ page }) => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue