Buchungen anlegen, aendern und loeschen; Budget-Zeilen setzen, fuer den Monat oder fuer die Vorlage; Vertraege und Konten anlegen und aendern. Damit kann die App alles, wofuer man bisher an den Rechner musste - ausser Import, Arbeitsliste und Verwaltung. Ein Formular je Sache, nicht zwei: Anlegen und Aendern teilen sich die Maske, weil die Felder dieselben sind und zwei Kopien genau die Stelle waeren, an der sie auseinanderlaufen. Was sich unterscheidet, steht in einem Zweck-Typ. Geteilt in Bausteine/Formular.swift: ein Speichervorgang, der den Knopf sperrt und den Fehler festhaelt (das Formular bleibt offen - zugeklappt waere die Eingabe weg und die Meldung nutzlos), ein Betragsfeld, das an Cent gebunden ist statt an Text, und die Auswahlfelder fuer Kategorie, Konto und Schlagwoerter. Drei Dinge, die stillschweigend falsch waeren: Eine Teilaenderung schickt nur, was gesetzt ist. Ginge ein nicht angefasstes Feld als null hinaus, loeschte jedes Speichern den Empfaenger, die Notiz und die Kategorie gleich mit. Eine leere Tag-Liste geht dagegen sehr wohl mit - sie bedeutet "alle abnehmen" und darf nicht mit "nicht angefasst" zusammenfallen. Bei einem Uebertrag wird keine Kategorie mitgeschickt: Das Geld wechselt nur die Tasche, es wird nicht eingeordnet. Bei Betriebsart "berechnet" bleibt planned_cents unberuehrt - das Backend liest es dann nicht, und es zu ueberschreiben verloere den Wert, auf den ein Zurueckschalten auf "fest" zurueckfallen soll. Die Waehrung eines bestehenden Kontos laesst sich nicht aendern: Jeder gespeicherte Betrag wuerde danach etwas anderes bedeuten. 49 Tests gruen. Texte in Deutsch und Englisch, zeilengleich. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
131 lines
5.4 KiB
Swift
131 lines
5.4 KiB
Swift
import SwiftUI
|
||
|
||
/// Eine Budget-Zeile setzen – für den Monat oder für die Vorlage.
|
||
///
|
||
/// Dieselbe Maske für beides, weil das Backend dieselben drei Angaben
|
||
/// entgegennimmt (`planned_cents`, `modus`, `zuschlag_cents`) und sie über
|
||
/// `uebernimm` durch dieselbe Regel schickt. Was sich unterscheidet, ist nur
|
||
/// der Endpunkt und der Satz darunter.
|
||
struct BudgetZeileBearbeiten: View {
|
||
enum Ziel: Equatable {
|
||
/// Wirkt nur auf diesen Monat; er gilt danach als angepasst.
|
||
case monat(String)
|
||
/// Wirkt auf alle Monate, die nicht eigens angepasst sind.
|
||
case vorlage
|
||
}
|
||
|
||
let ziel: Ziel
|
||
let kategorie: Budgetkategorie
|
||
let waehrung: String
|
||
var fertig: () -> Void
|
||
|
||
@Environment(AppZustand.self) private var zustand
|
||
@Environment(\.dismiss) private var schliesse
|
||
@State private var vorgang = Speichervorgang()
|
||
|
||
@State private var istBerechnet = false
|
||
@State private var geplantCents = 0
|
||
@State private var zuschlagCents = 0
|
||
@State private var vorbelegt = false
|
||
|
||
var body: some View {
|
||
NavigationStack {
|
||
Form {
|
||
Section {
|
||
HStack(spacing: 10) {
|
||
Symbolplakette(symbol: kategorie.icon, farbe: kategorie.color)
|
||
Text(kategorie.name)
|
||
.font(.headline)
|
||
}
|
||
}
|
||
|
||
Section {
|
||
Toggle(String(localized: "budget.ausVertraegenRechnen"),
|
||
isOn: $istBerechnet)
|
||
} footer: {
|
||
Text(istBerechnet ? String(localized: "budget.berechnetErklaerung")
|
||
: String(localized: "budget.festErklaerung"))
|
||
}
|
||
|
||
Section {
|
||
if istBerechnet {
|
||
// Bei „berechnet" ist die hingeschriebene Zahl nicht
|
||
// der Soll-Betrag, sondern nur der Zuschlag obendrauf.
|
||
LabeledContent(String(localized: "budget.ausVertraegen"),
|
||
value: Geldbetrag(cents: kategorie.ausVertraegenCents,
|
||
waehrung: waehrung).formatiert())
|
||
Betragsfeld(titel: String(localized: "budget.zuschlag"),
|
||
waehrung: waehrung, cents: $zuschlagCents)
|
||
LabeledContent(String(localized: "budget.ergibt"),
|
||
value: Geldbetrag(cents: kategorie.ausVertraegenCents + zuschlagCents,
|
||
waehrung: waehrung).formatiert())
|
||
.fontWeight(.medium)
|
||
} else {
|
||
Betragsfeld(titel: String(localized: "budget.geplant"),
|
||
waehrung: waehrung, cents: $geplantCents)
|
||
}
|
||
}
|
||
|
||
if let fehler = vorgang.fehler {
|
||
Section { FehlerZeile(text: fehler) }
|
||
}
|
||
}
|
||
.navigationTitle(zielTitel)
|
||
.navigationBarTitleDisplayMode(.inline)
|
||
.toolbar {
|
||
ToolbarItem(placement: .cancellationAction) {
|
||
Button(String(localized: "allgemein.abbrechen")) { schliesse() }
|
||
}
|
||
ToolbarItem(placement: .confirmationAction) {
|
||
Button(String(localized: "allgemein.sichern")) {
|
||
Task { await sichere() }
|
||
}
|
||
.disabled(vorgang.laeuft)
|
||
}
|
||
}
|
||
.interactiveDismissDisabled(vorgang.laeuft)
|
||
.onAppear {
|
||
guard !vorbelegt else { return }
|
||
vorbelegt = true
|
||
istBerechnet = kategorie.istBerechnet
|
||
geplantCents = kategorie.plannedCents
|
||
zuschlagCents = kategorie.zuschlagCents
|
||
}
|
||
}
|
||
}
|
||
|
||
private var zielTitel: String {
|
||
switch ziel {
|
||
case .monat: return String(localized: "budget.monatSetzen")
|
||
case .vorlage: return String(localized: "budget.vorlageSetzen")
|
||
}
|
||
}
|
||
|
||
private func sichere() async {
|
||
guard let client = zustand.client else { return }
|
||
let modus = istBerechnet ? "berechnet" : "fest"
|
||
// Bei „berechnet" bleibt planned_cents unberührt: Das Backend liest
|
||
// es dann nicht, und es zu überschreiben verlöre den Wert, auf den
|
||
// ein Zurückschalten auf „fest" zurückfallen soll.
|
||
let geplant = istBerechnet ? nil : geplantCents
|
||
let zuschlag = istBerechnet ? zuschlagCents : nil
|
||
|
||
let geglueckt = await vorgang.fuehreAus {
|
||
switch ziel {
|
||
case .monat(let label):
|
||
try await client.setzeBudgetMonat(label, kategorie: kategorie.categoryId,
|
||
geplantCents: geplant, modus: modus,
|
||
zuschlagCents: zuschlag)
|
||
case .vorlage:
|
||
try await client.setzeBudgetVorlage(kategorie: kategorie.categoryId,
|
||
geplantCents: geplant, modus: modus,
|
||
zuschlagCents: zuschlag)
|
||
}
|
||
}
|
||
if geglueckt {
|
||
fertig()
|
||
schliesse()
|
||
}
|
||
}
|
||
}
|