import SwiftUI /// Buchung anlegen oder ändern. /// /// Eine Ansicht für beides: Die Felder sind dieselben, und zwei Formulare /// nebeneinander wären genau die Stelle, an der sie auseinanderlaufen. Was /// sich unterscheidet, steht in `BuchungFormular.Zweck`. struct BuchungFormular: View { enum Zweck: Equatable, Identifiable { case neu case aendern(Buchung) /// Damit `.sheet(item:)` damit umgehen kann. Die ID der Buchung /// genügt: Zwei Formulare für dieselbe Buchung gibt es nicht. var id: Int { switch self { case .neu: return 0 case .aendern(let buchung): return buchung.id } } } let zweck: Zweck /// Wird nach erfolgreichem Speichern oder Löschen gerufen – die Liste /// dahinter muss sich neu laden. var fertig: () -> Void @Environment(AppZustand.self) private var zustand @Environment(\.dismiss) private var schliesse @State private var vorgang = Speichervorgang() @State private var art: Buchungsart = .expense @State private var datum = Date() @State private var kontoId: Int? @State private var zielkontoId: Int? @State private var betragCents = 0 @State private var kategorieId: Int? @State private var empfaenger = "" @State private var zweckText = "" @State private var notiz = "" @State private var etiketten: Set = [] @State private var fragtNachLoeschen = false @State private var vorbelegt = false var body: some View { NavigationStack { Form { Section { Picker(String(localized: "buchungen.art"), selection: $art) { ForEach(Buchungsart.allCases) { eine in Text(eine.name).tag(eine) } } .pickerStyle(.segmented) DatePicker(String(localized: "buchung.datum"), selection: $datum, displayedComponents: .date) Betragsfeld(titel: String(localized: "buchung.betrag"), waehrung: waehrung, cents: $betragCents) } Section { KontoFeld(titel: String(localized: "buchung.konto"), nachschlag: zustand.nachschlag, auswahl: $kontoId) if art == .transfer { KontoFeld(titel: String(localized: "buchung.zielkonto"), nachschlag: zustand.nachschlag, ausser: kontoId, auswahl: $zielkontoId) } else { KategorieFeld(titel: String(localized: "buchung.kategorie"), nachschlag: zustand.nachschlag, art: art == .income ? .income : .expense, auswahl: $kategorieId) } } footer: { if art == .transfer { Text(String(localized: "buchung.uebertragErklaerung")) } } Section { TextField(String(localized: "buchung.empfaenger"), text: $empfaenger) TextField(String(localized: "buchung.verwendungszweck"), text: $zweckText) TextField(String(localized: "buchung.notiz"), text: $notiz, axis: .vertical) .lineLimit(1...4) } Section(String(localized: "buchung.schlagwoerter")) { SchlagwortFeld(nachschlag: zustand.nachschlag, gewaehlt: $etiketten) } if let fehler = vorgang.fehler { Section { FehlerZeile(text: fehler) } } if case .aendern = zweck { Section { Button(String(localized: "buchung.loeschen"), role: .destructive) { fragtNachLoeschen = true } } } } .navigationTitle(titel) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button(String(localized: "allgemein.abbrechen")) { schliesse() } } ToolbarItem(placement: .confirmationAction) { Button(String(localized: "allgemein.sichern")) { Task { await sichere() } } .disabled(!istVollstaendig || vorgang.laeuft) } } .interactiveDismissDisabled(vorgang.laeuft) .confirmationDialog(String(localized: "buchung.loeschenFrage"), isPresented: $fragtNachLoeschen, titleVisibility: .visible) { Button(String(localized: "buchung.loeschen"), role: .destructive) { Task { await loesche() } } Button(String(localized: "allgemein.abbrechen"), role: .cancel) {} } .onAppear(perform: belegeVor) } } private var titel: String { switch zweck { case .neu: return String(localized: "buchung.neu") case .aendern: return String(localized: "buchung.aendern") } } /// Die Währung des gewählten Kontos – Beträge stehen immer in ihr. private var waehrung: String { zustand.nachschlag.konto(kontoId)?.currency ?? zustand.einstellungen.basiswaehrung } /// Das Backend verlangt einen Betrag grösser null und bei einem Übertrag /// ein Zielkonto. Hier zu prüfen erspart dem Nutzer eine Fehlermeldung /// vom Server für etwas, das die App schon weiss. private var istVollstaendig: Bool { guard betragCents > 0, kontoId != nil else { return false } if art == .transfer { return zielkontoId != nil && zielkontoId != kontoId } return true } private func belegeVor() { guard !vorbelegt else { return } vorbelegt = true guard case .aendern(let buchung) = zweck else { kontoId = zustand.nachschlag.konten.first { !$0.isArchived }?.id return } art = buchung.type datum = buchung.date.datum kontoId = buchung.accountId zielkontoId = buchung.toAccountId betragCents = abs(buchung.amountCents) kategorieId = buchung.categoryId empfaenger = buchung.payee ?? "" zweckText = buchung.description ?? "" notiz = buchung.notes ?? "" etiketten = Set(buchung.tagIds) } private func sichere() async { guard let client = zustand.client, let kontoId else { return } let tag = Tagesdatum(datum) // Bei einem Übertrag gibt es keine Kategorie – sie mitzuschicken // hiesse, Geld einzuordnen, das nur die Tasche wechselt. let kategorie = art == .transfer ? nil : kategorieId let ziel = art == .transfer ? zielkontoId : nil let geglueckt = await vorgang.fuehreAus { switch zweck { case .neu: _ = try await client.legeBuchungAn(BuchungAnlegen( type: art, date: tag, accountId: kontoId, toAccountId: ziel, amountCents: betragCents, categoryId: kategorie, payee: leerAlsNil(empfaenger), description: leerAlsNil(zweckText), notes: leerAlsNil(notiz), tagIds: Array(etiketten))) case .aendern(let buchung): _ = try await client.aendereBuchung(buchung.id, BuchungAendern( type: art, date: tag, accountId: kontoId, toAccountId: ziel, amountCents: betragCents, categoryId: kategorie, payee: leerAlsNil(empfaenger), description: leerAlsNil(zweckText), notes: leerAlsNil(notiz), tagIds: Array(etiketten))) } } if geglueckt { fertig() schliesse() } } private func loesche() async { guard let client = zustand.client, case .aendern(let buchung) = zweck else { return } let geglueckt = await vorgang.fuehreAus { try await client.loescheBuchung(buchung.id) } if geglueckt { fertig() schliesse() } } /// Ein leeres Feld ist keine Angabe, sondern das Fehlen einer. Leerzeichen /// als Empfänger zu speichern wäre eine Angabe, die niemand gemacht hat. private func leerAlsNil(_ text: String) -> String? { let sauber = text.trimmingCharacters(in: .whitespacesAndNewlines) return sauber.isEmpty ? nil : sauber } }