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?
}
struct ExpenseCategory: Decodable, Identifiable {
let id: String
let label: String
let color: String?
}
// MARK: - Gassi-Zeiten
struct GassiZeit: Decodable, Identifiable {

View file

@ -2,10 +2,22 @@ import SwiftUI
struct AusgabenView: View {
@State private var expenses: [Expense] = []
@State private var categories: [ExpenseCategory] = []
@State private var isLoading = false
@State private var errorMessage: String?
@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 = {
let f = DateFormatter()
f.locale = Locale(identifier: "de_DE")
@ -30,10 +42,27 @@ struct AusgabenView: View {
}
}
.sheet(isPresented: $showAdd) {
AddExpenseSheet { Task { await load() } }
AddExpenseSheet(categories: categories.isEmpty ? Self.defaultCategories : categories) {
Task { await load() }
}
}
.task { await load() }
.refreshable { await load() }
.task {
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
@ -139,9 +168,10 @@ private struct ExpenseRow: View {
private struct AddExpenseSheet: View {
@Environment(\.dismiss) private var dismiss
let categories: [ExpenseCategory]
let onSaved: () -> Void
@State private var kategorie = "futter"
@State private var kategorie: String
@State private var betrag = ""
@State private var date = Date()
@State private var notiz = ""
@ -150,23 +180,19 @@ private struct AddExpenseSheet: View {
@State private var isSaving = false
@State private var errorMessage: String?
/// Backend-Whitelist: tierarzt, futter, zubehoer, versicherung, sitter, sonstiges
private let kategorien: [(key: String, label: String)] = [
("futter", "Futter"),
("tierarzt", "Tierarzt"),
("zubehoer", "Zubehör"),
("versicherung", "Versicherung"),
("sitter", "Sitter"),
("sonstiges", "Sonstiges")
]
init(categories: [ExpenseCategory], onSaved: @escaping () -> Void) {
self.categories = categories
self.onSaved = onSaved
_kategorie = State(initialValue: categories.first?.id ?? "sonstiges")
}
var body: some View {
NavigationStack {
Form {
Section("Kategorie") {
Picker("Kategorie", selection: $kategorie) {
ForEach(kategorien, id: \.key) { entry in
Text(entry.label).tag(entry.key)
ForEach(categories) { entry in
Text(entry.label).tag(entry.id)
}
}
}