feat: Erste Stufe der iOS-App - Anmeldung und Kontenliste

Neuer Ordner ios/ mit dem Geruest: Netzwerkschicht, Modelle, Offline-Ablage,
Texte in Deutsch und Englisch, Navigation, der komplette Anmeldeweg (Server
waehlen, anmelden, zweiter Faktor, Pflicht-Passwortwechsel) und als erste
Fachansicht die Kontenliste.

Die Anmeldung laeuft einmal ueber das Sitzungs-Cookie, holt sich dabei eine
App-Marke und legt sie in den Schluesselbund. Danach traegt nur noch der
Header. Kein Passwort auf dem Geraet, kein Ablauf nach 30 Tagen.

Der Code ist nie uebersetzt worden - hier gibt es kein Xcode. Geprueft ist
dafuer jedes Modellfeld gegen echte Antworten des laufenden Backends; dabei
kam heraus, dass created_at ohne Zeitzone und mit Mikrosekunden kommt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-20 16:20:03 +02:00
co-authored by Claude Opus 5
parent 8a7545bf5b
commit 6b86a6bf91
25 changed files with 2908 additions and 0 deletions
+87
View File
@@ -0,0 +1,87 @@
import Foundation
/// `AccountOut` aus `backend/app/schemas.py`.
struct Konto: Codable, Sendable, Identifiable, Hashable {
let id: Int
let name: String
let type: Kontoart
let currency: String
let icon: String
let color: String
let openingBalanceCents: Int
let openingDate: Tagesdatum?
let creditLimitCents: Int?
let isLocked: Bool
let isArchived: Bool
let position: Int
let savingsCategoryId: Int?
let identifiers: [Kontokennung]
/// Saldo in der Währung des Kontos.
let balanceCents: Int
/// Derselbe Saldo in der Hauptwährung – vom Server gerechnet.
let balanceBaseCents: Int
/// Was die Bank selbst meldet. Nur bei angebundenen Konten gesetzt.
let bankBalanceCents: Int?
let bankBalanceAt: Zeitpunkt?
/// Nur ein gebuchter Banksaldo lässt sich mit unserer Rechnung
/// vergleichen; ein verfügbarer enthält Vorgemerktes.
let bankBalanceVergleichbar: Bool
private enum CodingKeys: String, CodingKey {
case id, name, type, currency, icon, color, position, identifiers
case openingBalanceCents = "opening_balance_cents"
case openingDate = "opening_date"
case creditLimitCents = "credit_limit_cents"
case isLocked = "is_locked"
case isArchived = "is_archived"
case savingsCategoryId = "savings_category_id"
case balanceCents = "balance_cents"
case balanceBaseCents = "balance_base_cents"
case bankBalanceCents = "bank_balance_cents"
case bankBalanceAt = "bank_balance_at"
case bankBalanceVergleichbar = "bank_balance_vergleichbar"
}
var saldo: Geldbetrag { Geldbetrag(cents: balanceCents, waehrung: currency) }
/// Ob der gemeldete Banksaldo von unserer Rechnung abweicht – nur dann ist
/// ein Hinweis angebracht.
var weichtVonDerBankAb: Bool {
guard bankBalanceVergleichbar, let bank = bankBalanceCents else { return false }
return bank != balanceCents
}
}
/// `AccountType`
enum Kontoart: String, Codable, Sendable, CaseIterable {
case girokonto, sparkonto, bargeld, kreditkarte, depot, sonstiges
var name: String {
switch self {
case .girokonto: return String(localized: "kontoart.girokonto")
case .sparkonto: return String(localized: "kontoart.sparkonto")
case .bargeld: return String(localized: "kontoart.bargeld")
case .kreditkarte: return String(localized: "kontoart.kreditkarte")
case .depot: return String(localized: "kontoart.depot")
case .sonstiges: return String(localized: "kontoart.sonstiges")
}
}
/// Ein Übertrag auf diese Konten zählt im Budget als Sparen, nicht als
/// neutrale Umbuchung (`SAVINGS_ACCOUNT_TYPES` im Backend).
var giltAlsSparen: Bool { self == .sparkonto || self == .depot }
}
/// `AccountIdentifierOut`
struct Kontokennung: Codable, Sendable, Identifiable, Hashable {
let id: Int
let kind: Art
let value: String
enum Art: String, Codable, Sendable {
case iban
case accountNo = "account_no"
case cardNo = "card_no"
case text
}
}