feat: Fuenfte Stufe der iOS-App - Kategorien, Schlagwoerter, Bank, zweiter Faktor
Die Nebenraeume haengen unter Einstellungen, nicht als eigene Reiter: Man braucht sie selten, und eine Leiste mit zehn Reitern findet niemand mehr. - Kategorien: anlegen, aendern, archivieren, loeschen - samt Gruppen. Eine Kategorie oeffnet ihre Erkennungswoerter, die sich dort hinzufuegen und entfernen lassen. Darunter steht, was sonst niemand merkt: Sie wirken nur auf kuenftige Importe. - Woerter, die oft korrigiert werden, stehen als Pruefmeldung in den Einstellungen - behalten oder entfernen. - Schlagwoerter: anlegen, umbenennen, umfaerben, loeschen. Neue bekommen reihum eine Farbe, damit nicht alle grau sind. - Einstellungen: Erscheinungsbild, Hauptwaehrung und Budget-Starttag lassen sich jetzt aendern. Ohne Sichern-Knopf - es ist je Zeile genau ein Wert, und das Backend nimmt ihn einzeln entgegen. Dazu der Hinweis, dass sie dem Konto gehoeren und nicht dem Geraet. - Bank: abrufen und Salden auffrischen. Verbinden bleibt am Rechner, die Bank leitet fest dorthin zurueck. Der letzte Fehler der Verbindung steht dabei, sonst ruft man vergeblich ab. - Zweiter Faktor: einrichten in drei Schritten. Auf dem Telefon fuehrt der Weg ueber die otpauth-Adresse direkt in die Authenticator-App statt ueber einen QR-Code, den man sich selbst zeigen muesste. Die Rettungscodes erscheinen einmal; das Blatt laesst sich nicht wegwischen und erst schliessen, wenn man bestaetigt hat, sie gesichert zu haben. Abschalten des zweiten Faktors bleibt bewusst draussen: Es verlangt das Kontopasswort, und die App speichert keines. Ein Vollzugang auf diesem Geraet soll den Schutz des Kontos nicht aufheben koennen. 59 Tests gruen. Texte in Deutsch und Englisch, zeilengleich. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
import Foundation
|
||||
|
||||
// MARK: - Schlagwörter
|
||||
|
||||
/// `KeywordOut` – ein Wort, an dem die Erkennung eine Kategorie festmacht.
|
||||
///
|
||||
/// Nicht zu verwechseln mit `Schlagwort` (einem Tag): Ein Tag klebt an einer
|
||||
/// Buchung, ein Schlagwort hängt an einer **Kategorie** und wirkt nur auf
|
||||
/// künftige Importe.
|
||||
struct Erkennungswort: Codable, Sendable, Identifiable, Hashable {
|
||||
let id: Int
|
||||
let categoryId: Int
|
||||
let word: String
|
||||
/// Nur wenn der Betrag mitentscheiden soll – „Migros" über 200 ist
|
||||
/// Haushalt, darunter Lebensmittel.
|
||||
let amountCents: Int?
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id, word
|
||||
case categoryId = "category_id"
|
||||
case amountCents = "amount_cents"
|
||||
}
|
||||
}
|
||||
|
||||
/// `FlaggedKeywordOut` – ein Wort, das oft wieder korrigiert wird.
|
||||
///
|
||||
/// Ab fünf Anwendungen und mindestens einem Drittel Korrekturen meldet das
|
||||
/// Backend es zur Prüfung. Behalten oder entfernen entscheidet der Mensch.
|
||||
struct MarkiertesWort: Codable, Sendable, Identifiable, Hashable {
|
||||
let id: Int
|
||||
let categoryId: Int?
|
||||
let categoryName: String
|
||||
let word: String
|
||||
let appliedCount: Int
|
||||
let overriddenCount: Int
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id, word
|
||||
case categoryId = "category_id"
|
||||
case categoryName = "category_name"
|
||||
case appliedCount = "applied_count"
|
||||
case overriddenCount = "overridden_count"
|
||||
}
|
||||
}
|
||||
|
||||
/// `KeywordCreate`
|
||||
struct ErkennungswortAnlegen: Encodable, Sendable {
|
||||
let word: String
|
||||
var amountCents: Int?
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case word
|
||||
case amountCents = "amount_cents"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Kategorien schreiben
|
||||
|
||||
/// `CategoryCreate`
|
||||
struct KategorieAnlegen: Encodable, Sendable {
|
||||
let groupId: Int
|
||||
let name: String
|
||||
var icon = "more-horizontal"
|
||||
var color = "#6b7480"
|
||||
/// Zählt in keiner Auswertung mit – etwa durchlaufende Posten.
|
||||
var excludedFromStats = false
|
||||
/// Eine Ausgabe, die kein Verlust ist: Sparen.
|
||||
var spendingIsGood = false
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case name, icon, color
|
||||
case groupId = "group_id"
|
||||
case excludedFromStats = "excluded_from_stats"
|
||||
case spendingIsGood = "spending_is_good"
|
||||
}
|
||||
}
|
||||
|
||||
/// `CategoryUpdate` – nur die gesetzten Felder gehen mit.
|
||||
struct KategorieAendern: Encodable, Sendable {
|
||||
var name: String?
|
||||
var groupId: Int?
|
||||
var icon: String?
|
||||
var color: String?
|
||||
var isArchived: Bool?
|
||||
var excludedFromStats: Bool?
|
||||
var spendingIsGood: Bool?
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case name, icon, color
|
||||
case groupId = "group_id"
|
||||
case isArchived = "is_archived"
|
||||
case excludedFromStats = "excluded_from_stats"
|
||||
case spendingIsGood = "spending_is_good"
|
||||
}
|
||||
}
|
||||
|
||||
/// `CategoryGroupCreate`
|
||||
struct GruppeAnlegen: Encodable, Sendable {
|
||||
let name: String
|
||||
var kind: Kategorieart = .expense
|
||||
var icon = "list"
|
||||
var color = "#6b7480"
|
||||
}
|
||||
|
||||
/// `CategoryGroupUpdate`
|
||||
struct GruppeAendern: Encodable, Sendable {
|
||||
var name: String?
|
||||
var icon: String?
|
||||
var color: String?
|
||||
}
|
||||
|
||||
// MARK: - Tags schreiben
|
||||
|
||||
/// `TagCreate`
|
||||
struct TagAnlegen: Encodable, Sendable {
|
||||
let name: String
|
||||
var color = "#6b7480"
|
||||
}
|
||||
|
||||
/// `TagUpdate`
|
||||
struct TagAendern: Encodable, Sendable {
|
||||
var name: String?
|
||||
var color: String?
|
||||
}
|
||||
|
||||
// MARK: - Zweiter Faktor
|
||||
|
||||
/// `TwoFactorStatusOut`
|
||||
struct ZweiterFaktorStand: Codable, Sendable, Hashable {
|
||||
let enabled: Bool
|
||||
let recoveryCodesLeft: Int
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case enabled
|
||||
case recoveryCodesLeft = "recovery_codes_left"
|
||||
}
|
||||
}
|
||||
|
||||
/// `TwoFactorSetupOut` – das noch **nicht** aktive Geheimnis.
|
||||
///
|
||||
/// Gültig wird es erst, wenn ein Code daraus geprüft wurde. Wer die
|
||||
/// Einrichtung abbricht, lässt einfach ein unbenutztes Geheimnis stehen.
|
||||
struct ZweiterFaktorEinrichtung: Codable, Sendable, Hashable {
|
||||
let secret: String
|
||||
let otpauthUri: String
|
||||
/// Der QR-Code als `data:`-URI. Auf dem Telefon selten gebraucht – dort
|
||||
/// führt der Weg über `otpauthUri` direkt in die Authenticator-App.
|
||||
let qrPng: String
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case secret
|
||||
case otpauthUri = "otpauth_uri"
|
||||
case qrPng = "qr_png"
|
||||
}
|
||||
}
|
||||
|
||||
/// `RecoveryCodesOut` – einmal zu sehen, danach nie wieder.
|
||||
struct Rettungscodes: Codable, Sendable, Hashable {
|
||||
let codes: [String]
|
||||
}
|
||||
|
||||
struct ZweiterFaktorCode: Encodable, Sendable {
|
||||
let code: String
|
||||
}
|
||||
|
||||
// MARK: - Bank
|
||||
|
||||
/// `BankConnectionOut` – eine bestehende Anbindung.
|
||||
///
|
||||
/// Hergestellt wird sie am Rechner: Die Bank leitet fest auf
|
||||
/// `{base_url}/bank/rueckkehr` zurück. Abrufen geht von hier.
|
||||
struct Bankverbindung: Codable, Sendable, Identifiable, Hashable {
|
||||
let id: Int
|
||||
let aspspName: String
|
||||
let aspspCountry: String
|
||||
let validUntil: Zeitpunkt?
|
||||
let lastSyncAt: Zeitpunkt?
|
||||
/// Takt des automatischen Abrufs in Minuten, 0 = aus.
|
||||
let pollMinutes: Int
|
||||
let lastError: String?
|
||||
let expiresSoon: Bool
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case aspspName = "aspsp_name"
|
||||
case aspspCountry = "aspsp_country"
|
||||
case validUntil = "valid_until"
|
||||
case lastSyncAt = "last_sync_at"
|
||||
case pollMinutes = "poll_minutes"
|
||||
case lastError = "last_error"
|
||||
case expiresSoon = "expires_soon"
|
||||
}
|
||||
}
|
||||
|
||||
/// Was ein Abruf zurückmeldet.
|
||||
struct Abrufergebnis: Codable, Sendable, Hashable {
|
||||
let geholt: Int
|
||||
let neu: Int
|
||||
let gebucht: Int?
|
||||
let vorgemerkt: Int?
|
||||
let doppelt: Int?
|
||||
let uebertraege: Int?
|
||||
/// Die Verbindung hat kein zugeordnetes Konto – dann wurde nichts geholt.
|
||||
let ohneZuordnung: Bool?
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case geholt, neu, gebucht, vorgemerkt, doppelt, uebertraege
|
||||
case ohneZuordnung = "ohne_zuordnung"
|
||||
}
|
||||
|
||||
/// Ein Satz für die Rückmeldung.
|
||||
var meldung: String {
|
||||
if ohneZuordnung == true {
|
||||
return String(localized: "bank.ohneZuordnung")
|
||||
}
|
||||
return String(format: String(localized: "bank.abrufErgebnis"), geholt, neu)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user