feat: Dritte Stufe der iOS-App - alles laesst sich jetzt bearbeiten

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>
This commit is contained in:
Guido
2026-10-21 21:04:46 +02:00
co-authored by Claude Opus 5
parent 4c61a02c53
commit 82db09e6de
15 changed files with 1653 additions and 4 deletions
+98
View File
@@ -190,4 +190,102 @@ extension CertainClient {
try await hole("dashboard/recent",
abfrage: [URLQueryItem(name: "limit", value: String(grenze))])
}
// MARK: - Schreiben: Buchungen
func legeBuchungAn(_ eingabe: BuchungAnlegen) async throws -> Buchung {
try await schicke(.post, "transactions", koerper: eingabe)
}
func aendereBuchung(_ id: Int, _ aenderung: BuchungAendern) async throws -> Buchung {
try await schicke(.patch, "transactions/\(id)", koerper: aenderung)
}
func loescheBuchung(_ id: Int) async throws {
try await schickeStumm(.delete, "transactions/\(id)")
}
// MARK: - Schreiben: Budget
/// Setzt eine Zeile der Monatsansicht. Der Monat wird dadurch als
/// „angepasst" geführt und folgt der Vorlage nicht mehr.
///
/// Query statt Körper – so will es das Backend (`routers/budgets.py`).
@discardableResult
func setzeBudgetMonat(_ label: String, kategorie: Int, geplantCents: Int?,
modus: String? = nil,
zuschlagCents: Int? = nil) async throws -> Budgetmonat {
try await schicke(.put, "budget/\(label)/category/\(kategorie)",
abfrage: budgetAbfrage(geplantCents, modus, zuschlagCents))
}
/// Setzt eine Zeile der Standard-Vorlage. Wirkt auf alle Monate, die
/// nicht eigens angepasst sind.
@discardableResult
func setzeBudgetVorlage(kategorie: Int, geplantCents: Int?, modus: String? = nil,
zuschlagCents: Int? = nil) async throws -> Vorlagenzeile {
try await schicke(.put, "budget/template/category/\(kategorie)",
abfrage: budgetAbfrage(geplantCents, modus, zuschlagCents))
}
private func budgetAbfrage(_ geplantCents: Int?, _ modus: String?,
_ zuschlagCents: Int?) -> [URLQueryItem] {
var abfrage: [URLQueryItem] = []
if let geplantCents {
abfrage.append(URLQueryItem(name: "planned_cents", value: String(geplantCents)))
}
if let modus {
abfrage.append(URLQueryItem(name: "modus", value: modus))
}
if let zuschlagCents {
abfrage.append(URLQueryItem(name: "zuschlag_cents", value: String(zuschlagCents)))
}
return abfrage
}
/// Nimmt die Anpassungen dieses Monats zurück – er folgt wieder der Vorlage.
@discardableResult
func setzeBudgetMonatZurueck(_ label: String) async throws -> Budgetmonat {
try await schicke(.delete, "budget/\(label)")
}
/// Überschreibt den Monat mit der Vorlage.
@discardableResult
func holeVorlageInMonat(_ label: String) async throws -> Budgetmonat {
try await schicke(.post, "budget/\(label)/from-template")
}
/// Macht diesen Monat zur neuen Vorlage.
@discardableResult
func macheMonatZurVorlage(_ label: String) async throws -> Budgetvorlage {
try await schicke(.post, "budget/template/from-month/\(label)")
}
// MARK: - Schreiben: Verträge
func legeAboAn(_ eingabe: AboAnlegen) async throws -> Abo {
try await schicke(.post, "subscriptions", koerper: eingabe)
}
func aendereAbo(_ id: Int, _ aenderung: AboAendern) async throws -> Abo {
try await schicke(.patch, "subscriptions/\(id)", koerper: aenderung)
}
func loescheAbo(_ id: Int) async throws {
try await schickeStumm(.delete, "subscriptions/\(id)")
}
// MARK: - Schreiben: Konten
func legeKontoAn(_ eingabe: KontoAnlegen) async throws -> Konto {
try await schicke(.post, "accounts", koerper: eingabe)
}
func aendereKonto(_ id: Int, _ aenderung: KontoAendern) async throws -> Konto {
try await schicke(.patch, "accounts/\(id)", koerper: aenderung)
}
func loescheKonto(_ id: Int) async throws {
try await schickeStumm(.delete, "accounts/\(id)")
}
}