> */ public static function alle(): array { return Database::fetchAll( 'SELECT * FROM budgets WHERE benutzer_id = ? ORDER BY jahr DESC, name', [Mandant::id()] ); } public static function find(int $id): ?array { $zeile = Database::fetchOne( 'SELECT * FROM budgets WHERE id = ? AND benutzer_id = ?', [$id, Mandant::id()] ); return $zeile === false ? null : $zeile; } public static function findeNachName(string $name): ?array { $zeile = Database::fetchOne( 'SELECT * FROM budgets WHERE name = ? AND benutzer_id = ?', [trim($name), Mandant::id()] ); return $zeile === false ? null : $zeile; } /** Das aktive Budget, ersatzweise das jüngste – oder null, wenn keines existiert. */ public static function aktives(): ?array { $zeile = Database::fetchOne( 'SELECT * FROM budgets WHERE benutzer_id = ? ORDER BY aktiv DESC, jahr DESC, name LIMIT 1', [Mandant::id()] ); return $zeile === false ? null : $zeile; } /** * Das aktive Budget eines bestimmten Jahres, ersatzweise irgendeines aus * diesem Jahr. Der Budgetbericht braucht das: Ein Zeitraum kann über den * Jahreswechsel gehen, und dann gilt je Jahr dessen eigene Planung. */ public static function aktivesFuerJahr(int $jahr): ?array { $zeile = Database::fetchOne( 'SELECT * FROM budgets WHERE benutzer_id = ? AND jahr = ? ORDER BY aktiv DESC, name LIMIT 1', [Mandant::id(), $jahr] ); return $zeile === false ? null : $zeile; } public static function anlegen(string $name, int $jahr, string $beschreibung = '', bool $aktiv = false): int { $name = trim($name); if ($name === '') { throw new \InvalidArgumentException('Das Budget braucht einen Namen.'); } if ($jahr < 1900 || $jahr > 2200) { throw new \InvalidArgumentException('Das Jahr des Budgets ist ungültig.'); } if (self::findeNachName($name) !== null) { throw new \RuntimeException('Das Budget „' . $name . '“ gibt es bereits.'); } Database::execute( 'INSERT INTO budgets (benutzer_id, name, jahr, beschreibung, aktiv) VALUES (?, ?, ?, ?, 0)', [Mandant::id(), $name, $jahr, $beschreibung] ); $id = Database::lastInsertId(); // Das erste Budget ist immer das aktive – sonst zeigt der Planer nichts. if ($aktiv || self::anzahl() === 1) { self::aktivieren($id); } return $id; } public static function speichern(int $id, string $name, int $jahr, string $beschreibung): void { $name = trim($name); if ($name === '') { throw new \InvalidArgumentException('Das Budget braucht einen Namen.'); } $andere = self::findeNachName($name); if ($andere !== null && (int)$andere['id'] !== $id) { throw new \RuntimeException('Das Budget „' . $name . '“ gibt es bereits.'); } Database::execute( 'UPDATE budgets SET name = ?, jahr = ?, beschreibung = ? WHERE id = ? AND benutzer_id = ?', [$name, $jahr, $beschreibung, $id, Mandant::id()] ); } public static function aktivieren(int $id): void { Database::transaktion(static function () use ($id): void { Database::execute('UPDATE budgets SET aktiv = 0 WHERE benutzer_id = ?', [Mandant::id()]); Database::execute( 'UPDATE budgets SET aktiv = 1 WHERE id = ? AND benutzer_id = ?', [$id, Mandant::id()] ); }); } public static function loeschen(int $id): void { $war = self::find($id); Database::execute('DELETE FROM budgets WHERE id = ? AND benutzer_id = ?', [$id, Mandant::id()]); // War es das aktive, rückt das nächste nach – ohne aktives Budget stünde // der Planer leer da, obwohl es noch Budgets gibt. if ($war !== null && (int)$war['aktiv'] === 1) { $naechstes = self::aktives(); if ($naechstes !== null) { self::aktivieren((int)$naechstes['id']); } } } public static function anzahl(): int { return (int)Database::fetchValue( 'SELECT COUNT(*) FROM budgets WHERE benutzer_id = ?', [Mandant::id()], 0 ); } /** * Alle Positionen eines Budgets als [kategorie_id][monat] => Betrag. * * @return array> */ public static function positionen(int $budgetId): array { $zeilen = Database::fetchAll( 'SELECT kategorie_id, monat, betrag FROM budget_positionen WHERE budget_id = ?', [$budgetId] ); $werte = []; foreach ($zeilen as $zeile) { $werte[(int)$zeile['kategorie_id']][(int)$zeile['monat']] = (float)$zeile['betrag']; } return $werte; } /** * Setzt einen Monatswert. Der Betrag kommt ohne Vorzeichen aus dem Formular * und bekommt es hier nach der Kategorieart. Ein leeres Feld (null) löscht * die Position – „nicht geplant“ ist etwas anderes als „0,00 geplant“. */ public static function positionSetzen(int $budgetId, int $kategorieId, int $monat, ?float $betrag): void { if ($monat < 1 || $monat > 12) { throw new \InvalidArgumentException('Monat muss zwischen 1 und 12 liegen.'); } if ($betrag === null) { Database::execute( 'DELETE FROM budget_positionen WHERE budget_id = ? AND kategorie_id = ? AND monat = ?', [$budgetId, $kategorieId, $monat] ); return; } $betrag = self::mitVorzeichen($kategorieId, $betrag); Database::execute( 'INSERT INTO budget_positionen (budget_id, kategorie_id, monat, betrag) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE betrag = VALUES(betrag)', [$budgetId, $kategorieId, $monat, $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. */ public static function kopieren(int $id, string $neuerName, int $neuesJahr): int { $vorlage = self::find($id); if ($vorlage === null) { throw new \RuntimeException('Das zu kopierende Budget gibt es nicht.'); } return (int)Database::transaktion(static function () use ($vorlage, $neuerName, $neuesJahr): int { $neu = self::anlegen($neuerName, $neuesJahr, (string)$vorlage['beschreibung']); Database::execute( 'INSERT INTO budget_positionen (budget_id, kategorie_id, monat, betrag) SELECT ?, kategorie_id, monat, betrag FROM budget_positionen WHERE budget_id = ?', [$neu, (int)$vorlage['id']] ); return $neu; }); } /** Ausgaben negativ, Einnahmen positiv – unabhängig davon, wie eingegeben wurde. */ private static function mitVorzeichen(int $kategorieId, float $betrag): float { $art = Database::fetchValue( 'SELECT art FROM kategorien WHERE id = ? AND benutzer_id = ?', [$kategorieId, Mandant::id()] ); $betrag = abs($betrag); return $art === 'einnahme' ? $betrag : -$betrag; } }