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>
124 lines
3.9 KiB
Swift
124 lines
3.9 KiB
Swift
import Foundation
|
||
|
||
// Die Karten des Dashboards. Anders als im Web ist die Auswahl hier fest –
|
||
// ein verschiebbares Raster auf Telefonbreite wäre Arbeit ohne Gewinn. Die
|
||
// vier Endpunkte sind dieselben, die das Web benutzt.
|
||
|
||
/// `GET /api/dashboard/balances`
|
||
struct Kontostaende: Codable, Sendable, Hashable {
|
||
let accounts: [Kontozeile]
|
||
/// Verfügbar, ohne gebundene und archivierte Konten.
|
||
let total: Int
|
||
/// Was auf gebundenen Konten liegt – Geld, an das man nicht herankommt.
|
||
let gebunden: Int
|
||
let currency: String
|
||
|
||
var summe: Geldbetrag { Geldbetrag(cents: total, waehrung: currency) }
|
||
var gebundenBetrag: Geldbetrag { Geldbetrag(cents: gebunden, waehrung: currency) }
|
||
|
||
struct Kontozeile: Codable, Sendable, Identifiable, Hashable {
|
||
let id: Int
|
||
let name: String
|
||
let icon: String
|
||
let color: String
|
||
let currency: String
|
||
let balanceCents: Int
|
||
let balanceBaseCents: Int
|
||
let archived: Bool
|
||
let locked: Bool
|
||
|
||
private enum CodingKeys: String, CodingKey {
|
||
case id, name, icon, color, currency, archived, locked
|
||
case balanceCents = "balance_cents"
|
||
case balanceBaseCents = "balance_base_cents"
|
||
}
|
||
|
||
var saldo: Geldbetrag { Geldbetrag(cents: balanceCents, waehrung: currency) }
|
||
}
|
||
}
|
||
|
||
/// `GET /api/dashboard/budget-progress`
|
||
struct Budgetfortschritt: Codable, Sendable, Hashable {
|
||
let rows: [Zeile]
|
||
let currency: String
|
||
let label: String
|
||
let groupBy: String
|
||
|
||
private enum CodingKeys: String, CodingKey {
|
||
case rows, currency, label
|
||
case groupBy = "group_by"
|
||
}
|
||
|
||
struct Zeile: Codable, Sendable, Identifiable, Hashable {
|
||
let name: String
|
||
let color: String
|
||
let planned: Int
|
||
let actual: Int
|
||
let over: Bool
|
||
/// Sparen: mehr als geplant ist hier keine Warnung.
|
||
let good: Bool
|
||
|
||
var id: String { name }
|
||
|
||
var anteil: Double {
|
||
guard planned > 0 else { return 0 }
|
||
return min(1, max(0, Double(actual) / Double(planned)))
|
||
}
|
||
}
|
||
}
|
||
|
||
/// `GET /api/dashboard/income-expense`
|
||
struct EinnahmenAusgaben: Codable, Sendable, Hashable {
|
||
let income: Int
|
||
let expense: Int
|
||
let net: Int
|
||
let currency: String
|
||
let label: String
|
||
|
||
var einnahmen: Geldbetrag { Geldbetrag(cents: income, waehrung: currency) }
|
||
var ausgaben: Geldbetrag { Geldbetrag(cents: expense, waehrung: currency) }
|
||
var uebrig: Geldbetrag { Geldbetrag(cents: net, waehrung: currency) }
|
||
}
|
||
|
||
/// `GET /api/dashboard/recent`
|
||
struct LetzteBuchungen: Codable, Sendable, Hashable {
|
||
let transactions: [Zeile]
|
||
let currency: String
|
||
|
||
struct Zeile: Codable, Sendable, Identifiable, Hashable {
|
||
let id: Int
|
||
let date: Tagesdatum
|
||
let type: Buchungsart
|
||
/// Schon fertig formuliert: die Kategorie, sonst der Empfänger, bei
|
||
/// einem Übertrag „Konto A → Konto B".
|
||
let title: String
|
||
let icon: String
|
||
let color: String
|
||
/// `"pos"`, `"neg"` oder nichts – bei einem Übertrag gibt es keine
|
||
/// Richtung, weil kein Geld den Haushalt verlässt.
|
||
let sign: String?
|
||
let amountCents: Int
|
||
let currency: String
|
||
let amountBaseCents: Int
|
||
|
||
private enum CodingKeys: String, CodingKey {
|
||
case id, date, type, title, icon, color, sign, currency
|
||
case amountCents = "amount_cents"
|
||
case amountBaseCents = "amount_base_cents"
|
||
}
|
||
|
||
var betrag: Geldbetrag { Geldbetrag(cents: amountCents, waehrung: currency) }
|
||
|
||
/// Grün bei Einnahme, rot bei Ausgabe, neutral beim Übertrag.
|
||
var vorzeichen: Vorzeichen {
|
||
switch sign {
|
||
case "pos": return .plus
|
||
case "neg": return .minus
|
||
default: return .keines
|
||
}
|
||
}
|
||
|
||
enum Vorzeichen: Sendable { case plus, minus, keines }
|
||
}
|
||
}
|