feat: Zweite Stufe der iOS-App - Uebersicht, Budget, Buchungen, Vertraege
Die App kann jetzt lesen, wofuer man sie oeffnet. Vier neue Reiter: - Uebersicht: Einnahmen/Ausgaben des Monats, Budget-Fortschritt je Gruppe, Kontostaende und die letzten Buchungen. Feste Auswahl statt des verschiebbaren Rasters der Weboberflaeche - auf Telefonbreite waere das Arbeit ohne Gewinn. Vier Endpunkte nebeneinander in einem Abruf, damit der Offline-Stand nicht aus vier verschiedenen Momenten stammt. - Budget: Soll gegen Ist je Gruppe und Kategorie, blaetterbar ueber prev/next aus der Antwort. Rot nur bei echter Ueberschreitung - wer mehr zuruecklegt als geplant, hat nichts ueberzogen. Vertraege ohne Kategorie und fehlende Wechselkurse stehen als Hinweis darunter, statt die Summen still zu verfaelschen. - Buchungen: Suche und Filter laufen im Server (Backend-Ergaenzung B), nicht im Telefon. Gesucht wird beim Absenden, nicht je Tastendruck. - Vertraege: Monats- und Jahressumme, Rhythmus, naechste Faelligkeit. Zwei Dinge, die das Backend bewusst dem Client ueberlaesst: Ein Uebertrag kommt in zwei Gestalten - am Stueck (eine Buchung mit to_account_id) oder als zwei verknuepfte Buchungen, wie die Bank sie liefert. Beide Beine als zwei Zeilen zu zeigen hiesse, dasselbe Geld zweimal zu zeigen. Buchungszeile.zeilen fasst sie zusammen, aber nur wenn beide in der Liste stehen - sonst liesse ein Kontofilter Zeilen verschwinden, die sehr wohl zu diesem Konto gehoeren. Die Richtung kommt aus der Verknuepfung, nicht aus dem Vorzeichen: Manche Banken liefern beide Beine positiv. Kategorien, Schlagwoerter und Konten liegen als Nachschlagewerk im AppZustand, einmal geladen. Je Ansicht neu zu holen waere genau das N+1, das das Backend sich selbst verbietet. Texte in Deutsch und Englisch, zeilengleich. 44 Tests gruen, darunter die Uebertrags-Paarung und jedes Modell gegen eine echte Serverantwort. Nicht geprueft: der Weg gegen ein echtes Backend. Die App laesst nur https zu, ein lokaler Server haette eine ATS-Ausnahme gebraucht. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
import Foundation
|
||||
|
||||
/// `SubscriptionListOut` – die Vertragsliste samt Kopfzahlen.
|
||||
struct Aboliste: Codable, Sendable, Hashable {
|
||||
let subscriptions: [Abo]
|
||||
let summary: Abosummen
|
||||
}
|
||||
|
||||
/// `SubscriptionOut` aus `backend/app/schemas.py`.
|
||||
struct Abo: Codable, Sendable, Identifiable, Hashable {
|
||||
let id: Int
|
||||
let name: String
|
||||
let icon: String
|
||||
let color: String
|
||||
/// Der Betrag je Zahlung, in der Währung des Vertrags.
|
||||
let amountCents: Int
|
||||
let currency: String
|
||||
/// 1 = monatlich, 12 = jährlich, 3 = vierteljährlich …
|
||||
let intervalMonths: Int
|
||||
let accountId: Int?
|
||||
let categoryId: Int?
|
||||
let nextDue: Tagesdatum?
|
||||
/// Woran die Erkennung den Vertrag in einer Buchung wiedererkennt.
|
||||
let payeePattern: String?
|
||||
let savingsAccountId: Int?
|
||||
let isActive: Bool
|
||||
let cancelledAt: Tagesdatum?
|
||||
let notes: String?
|
||||
let position: Int
|
||||
/// Auf einen Monat umgelegt, in der Vertragswährung – vom Server gerechnet.
|
||||
let monatlichCents: Int
|
||||
/// Dasselbe in der Hauptwährung. Fehlt der Kurs, fällt es still auf den
|
||||
/// nativen Wert zurück; welche Währungen betroffen sind, sagt
|
||||
/// `Abosummen.ohneKurs`.
|
||||
let monatlichBaseCents: Int
|
||||
let tageBisFaellig: Int?
|
||||
let hasLogo: Bool
|
||||
let logoVersion: Int?
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id, name, icon, color, currency, notes, position
|
||||
case amountCents = "amount_cents"
|
||||
case intervalMonths = "interval_months"
|
||||
case accountId = "account_id"
|
||||
case categoryId = "category_id"
|
||||
case nextDue = "next_due"
|
||||
case payeePattern = "payee_pattern"
|
||||
case savingsAccountId = "savings_account_id"
|
||||
case isActive = "is_active"
|
||||
case cancelledAt = "cancelled_at"
|
||||
case monatlichCents = "monatlich_cents"
|
||||
case monatlichBaseCents = "monatlich_base_cents"
|
||||
case tageBisFaellig = "tage_bis_faellig"
|
||||
case hasLogo = "has_logo"
|
||||
case logoVersion = "logo_version"
|
||||
}
|
||||
|
||||
var betrag: Geldbetrag { Geldbetrag(cents: amountCents, waehrung: currency) }
|
||||
var monatlich: Geldbetrag { Geldbetrag(cents: monatlichCents, waehrung: currency) }
|
||||
|
||||
/// „monatlich", „jährlich", „alle 3 Monate".
|
||||
var rhythmus: String {
|
||||
switch intervalMonths {
|
||||
case 1: return String(localized: "abo.monatlich")
|
||||
case 3: return String(localized: "abo.vierteljaehrlich")
|
||||
case 6: return String(localized: "abo.halbjaehrlich")
|
||||
case 12: return String(localized: "abo.jaehrlich")
|
||||
default: return String(format: String(localized: "abo.alleNMonate"), intervalMonths)
|
||||
}
|
||||
}
|
||||
|
||||
/// Steht die nächste Zahlung in der nächsten Woche an?
|
||||
var istBaldFaellig: Bool {
|
||||
guard isActive, let tage = tageBisFaellig else { return false }
|
||||
return tage <= 7
|
||||
}
|
||||
}
|
||||
|
||||
/// Was am Kopf der Liste steht – die Zahl, wegen der man sie öffnet.
|
||||
struct Abosummen: Codable, Sendable, Hashable {
|
||||
let currency: String
|
||||
let monatlichCents: Int
|
||||
let jaehrlichCents: Int
|
||||
let anzahlAktiv: Int
|
||||
/// Währungen, die sich mangels Kurs nicht umrechnen liessen. Lieber
|
||||
/// nennen als stillschweigend zu klein summieren.
|
||||
let ohneKurs: [String]
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case currency
|
||||
case monatlichCents = "monatlich_cents"
|
||||
case jaehrlichCents = "jaehrlich_cents"
|
||||
case anzahlAktiv = "anzahl_aktiv"
|
||||
case ohneKurs = "ohne_kurs"
|
||||
}
|
||||
|
||||
var monatlich: Geldbetrag { Geldbetrag(cents: monatlichCents, waehrung: currency) }
|
||||
var jaehrlich: Geldbetrag { Geldbetrag(cents: jaehrlichCents, waehrung: currency) }
|
||||
}
|
||||
Reference in New Issue
Block a user