import Foundation /// Die Monatsansicht aus `GET /api/budget/{label}` beziehungsweise /// `/budget/current` (`_build_view` in `backend/app/routers/budgets.py`). /// /// Alle Beträge stehen schon in der Hauptwährung. Der Client rechnet nur, /// was es in der API nicht gibt: Prozente und „Rest". struct Budgetmonat: Codable, Sendable, Hashable { /// `"2026-10"`. let label: String let start: Tagesdatum let end: Tagesdatum /// Ob dieser Monat von der Vorlage abweicht. let customized: Bool let baseCurrency: String let prev: String let next: String let groups: [Budgetgruppe] let income: [GeplanteEinnahme] let vertraege: Vertragsreste let totals: Budgetsummen private enum CodingKeys: String, CodingKey { case label, start, end, customized, prev, next, groups, income, vertraege, totals case baseCurrency = "base_currency" } /// Was vom geplanten Ausgabenbudget noch übrig ist. Kann negativ werden. var restCents: Int { totals.plannedExpense - totals.actualExpense } /// Anteil des Verbrauchten, zwischen 0 und 1 gekappt – für einen Balken. /// Ohne Plan gibt es keinen Anteil, nicht etwa einen vollen Balken. var anteilVerbraucht: Double { guard totals.plannedExpense > 0 else { return 0 } return min(1, max(0, Double(totals.actualExpense) / Double(totals.plannedExpense))) } } /// Eine Kategoriegruppe im Budget. struct Budgetgruppe: Codable, Sendable, Identifiable, Hashable { let id: Int let name: String let icon: String let color: String let plannedCents: Int let actualCents: Int /// Überzogen. Sparen ist davon ausgenommen – siehe `good`. let over: Bool /// Alle Kategorien darin sind „Ausgaben, die kein Verlust sind" (Sparen). /// Dann ist Mehr-als-geplant keine Warnung. let good: Bool let ausVertraegenCents: Int let categories: [Budgetkategorie] private enum CodingKeys: String, CodingKey { case id, name, icon, color, over, good, categories case plannedCents = "planned_cents" case actualCents = "actual_cents" case ausVertraegenCents = "aus_vertraegen_cents" } } /// Eine Zeile im Budget. struct Budgetkategorie: Codable, Sendable, Identifiable, Hashable { let categoryId: Int let name: String let icon: String let color: String let plannedCents: Int let actualCents: Int let deltaCents: Int let over: Bool /// `"fest"` oder `"berechnet"`. Bei „berechnet" ergibt sich der Soll-Wert /// aus den Verträgen plus Zuschlag. let modus: String let zuschlagCents: Int /// Was die Verträge dieser Kategorie im Monat kosten. Steht auch bei /// „fest" da – dann als Hinweis, nicht als Summand. let ausVertraegenCents: Int let vertraegeAnzahl: Int let good: Bool var id: Int { categoryId } private enum CodingKeys: String, CodingKey { case name, icon, color, over, modus, good case categoryId = "category_id" case plannedCents = "planned_cents" case actualCents = "actual_cents" case deltaCents = "delta_cents" case zuschlagCents = "zuschlag_cents" case ausVertraegenCents = "aus_vertraegen_cents" case vertraegeAnzahl = "vertraege_anzahl" } var istBerechnet: Bool { modus == "berechnet" } var anteilVerbraucht: Double { guard plannedCents > 0 else { return 0 } return min(1, max(0, Double(actualCents) / Double(plannedCents))) } } /// Eine geplante Einnahme – „Gehalt", „Kindergeld". struct GeplanteEinnahme: Codable, Sendable, Identifiable, Hashable { let id: Int let label: String let categoryId: Int? let targetAccountId: Int? let plannedCents: Int let receivedCents: Int private enum CodingKeys: String, CodingKey { case id, label case categoryId = "category_id" case targetAccountId = "target_account_id" case plannedCents = "planned_cents" case receivedCents = "received_cents" } } /// Verträge, die sich nicht zuordnen liessen. /// /// Ohne diese Angaben fehlt ihr Geld im Budget, ohne dass irgendwo steht, /// dass es fehlt. `ohneKurs` nennt die Währungen, für die kein Wechselkurs /// vorlag – deren Beträge fehlen in der Summe. struct Vertragsreste: Codable, Sendable, Hashable { let ohneKategorieCents: Int let ohneKategorieAnzahl: Int let ohneKurs: [String] private enum CodingKeys: String, CodingKey { case ohneKategorieCents = "ohne_kategorie_cents" case ohneKategorieAnzahl = "ohne_kategorie_anzahl" case ohneKurs = "ohne_kurs" } var istLeer: Bool { ohneKategorieAnzahl == 0 && ohneKurs.isEmpty } } struct Budgetsummen: Codable, Sendable, Hashable { let plannedExpense: Int let actualExpense: Int let plannedIncome: Int let receivedIncome: Int private enum CodingKeys: String, CodingKey { case plannedExpense = "planned_expense" case actualExpense = "actual_expense" case plannedIncome = "planned_income" case receivedIncome = "received_income" } } /// `GET /api/budget/template` – das Standard-Budget hinter den Monaten. struct Budgetvorlage: Codable, Sendable, Hashable { let lines: [Vorlagenzeile] let income: [GeplanteEinnahme] } struct Vorlagenzeile: Codable, Sendable, Identifiable, Hashable { let categoryId: Int let plannedCents: Int let modus: String let zuschlagCents: Int var id: Int { categoryId } private enum CodingKeys: String, CodingKey { case modus case categoryId = "category_id" case plannedCents = "planned_cents" case zuschlagCents = "zuschlag_cents" } }