Ausgaben-Kategorien dynamisch vom Backend

ExpenseCategory DTO + GET /api/expenses/categories beim Öffnen der Liste
sowie bei Refresh. Falls der Endpunkt noch nicht ausgerollt ist (oder
fehlschlägt), Fallback auf eine lokale Default-Liste mit den aktuellen
sechs Kategorien.

AddExpenseSheet bekommt die Kategorien als Parameter, statt eigene
Liste zu führen — Source of Truth ist jetzt das Backend.
This commit is contained in:
rene 2026-05-30 12:31:59 +02:00
parent c03f018c0c
commit cf625f3391
2 changed files with 47 additions and 15 deletions

View file

@ -109,6 +109,12 @@ struct ExpenseCreateBody: Encodable {
let notiz: String? let notiz: String?
} }
struct ExpenseCategory: Decodable, Identifiable {
let id: String
let label: String
let color: String?
}
// MARK: - Gassi-Zeiten // MARK: - Gassi-Zeiten
struct GassiZeit: Decodable, Identifiable { struct GassiZeit: Decodable, Identifiable {

View file

@ -2,10 +2,22 @@ import SwiftUI
struct AusgabenView: View { struct AusgabenView: View {
@State private var expenses: [Expense] = [] @State private var expenses: [Expense] = []
@State private var categories: [ExpenseCategory] = []
@State private var isLoading = false @State private var isLoading = false
@State private var errorMessage: String? @State private var errorMessage: String?
@State private var showAdd = false @State private var showAdd = false
/// Lokale Fallback-Liste, falls der Backend-Endpunkt /api/expenses/categories
/// noch nicht ausgerollt ist. Sollte im Normalfall überschrieben werden.
private static let defaultCategories: [ExpenseCategory] = [
ExpenseCategory(id: "futter", label: "Futter", color: nil),
ExpenseCategory(id: "tierarzt", label: "Tierarzt", color: nil),
ExpenseCategory(id: "zubehoer", label: "Zubehör", color: nil),
ExpenseCategory(id: "versicherung", label: "Versicherung", color: nil),
ExpenseCategory(id: "sitter", label: "Sitter", color: nil),
ExpenseCategory(id: "sonstiges", label: "Sonstiges", color: nil)
]
private static let dateFormatter: DateFormatter = { private static let dateFormatter: DateFormatter = {
let f = DateFormatter() let f = DateFormatter()
f.locale = Locale(identifier: "de_DE") f.locale = Locale(identifier: "de_DE")
@ -30,10 +42,27 @@ struct AusgabenView: View {
} }
} }
.sheet(isPresented: $showAdd) { .sheet(isPresented: $showAdd) {
AddExpenseSheet { Task { await load() } } AddExpenseSheet(categories: categories.isEmpty ? Self.defaultCategories : categories) {
Task { await load() }
}
} }
.task { await load() } .task {
.refreshable { await load() } await loadCategories()
await load()
}
.refreshable {
await loadCategories()
await load()
}
}
private func loadCategories() async {
if let fetched: [ExpenseCategory] = try? await APIClient.shared.get("/api/expenses/categories"),
!fetched.isEmpty {
categories = fetched
} else if categories.isEmpty {
categories = Self.defaultCategories
}
} }
@ViewBuilder @ViewBuilder
@ -139,9 +168,10 @@ private struct ExpenseRow: View {
private struct AddExpenseSheet: View { private struct AddExpenseSheet: View {
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
let categories: [ExpenseCategory]
let onSaved: () -> Void let onSaved: () -> Void
@State private var kategorie = "futter" @State private var kategorie: String
@State private var betrag = "" @State private var betrag = ""
@State private var date = Date() @State private var date = Date()
@State private var notiz = "" @State private var notiz = ""
@ -150,23 +180,19 @@ private struct AddExpenseSheet: View {
@State private var isSaving = false @State private var isSaving = false
@State private var errorMessage: String? @State private var errorMessage: String?
/// Backend-Whitelist: tierarzt, futter, zubehoer, versicherung, sitter, sonstiges init(categories: [ExpenseCategory], onSaved: @escaping () -> Void) {
private let kategorien: [(key: String, label: String)] = [ self.categories = categories
("futter", "Futter"), self.onSaved = onSaved
("tierarzt", "Tierarzt"), _kategorie = State(initialValue: categories.first?.id ?? "sonstiges")
("zubehoer", "Zubehör"), }
("versicherung", "Versicherung"),
("sitter", "Sitter"),
("sonstiges", "Sonstiges")
]
var body: some View { var body: some View {
NavigationStack { NavigationStack {
Form { Form {
Section("Kategorie") { Section("Kategorie") {
Picker("Kategorie", selection: $kategorie) { Picker("Kategorie", selection: $kategorie) {
ForEach(kategorien, id: \.key) { entry in ForEach(categories) { entry in
Text(entry.label).tag(entry.key) Text(entry.label).tag(entry.id)
} }
} }
} }