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:
Guido
2026-10-21 22:03:28 +02:00
co-authored by Claude Opus 5
parent bab5cc5060
commit d290930759
13 changed files with 1828 additions and 9 deletions
+102
View File
@@ -370,4 +370,106 @@ extension CertainClient {
try await schickeStumm(.post, "transactions/dubletten/behalten",
koerper: ["ids": ids])
}
// MARK: - Kategorien schreiben
func legeKategorieAn(_ eingabe: KategorieAnlegen) async throws -> Kategorie {
try await schicke(.post, "categories", koerper: eingabe)
}
func aendereKategorie(_ id: Int, _ aenderung: KategorieAendern) async throws -> Kategorie {
try await schicke(.patch, "categories/\(id)", koerper: aenderung)
}
/// Geht nur, solange keine Buchung daran hängt – sonst 400. Archivieren
/// ist der Weg für alles, was schon benutzt wurde.
func loescheKategorie(_ id: Int) async throws {
try await schickeStumm(.delete, "categories/\(id)")
}
func legeGruppeAn(_ eingabe: GruppeAnlegen) async throws -> Kategoriegruppe {
try await schicke(.post, "categories/groups", koerper: eingabe)
}
func aendereGruppe(_ id: Int, _ aenderung: GruppeAendern) async throws -> Kategoriegruppe {
try await schicke(.patch, "categories/groups/\(id)", koerper: aenderung)
}
func loescheGruppe(_ id: Int) async throws {
try await schickeStumm(.delete, "categories/groups/\(id)")
}
// MARK: - Schlagwörter (Tags) schreiben
func legeTagAn(_ eingabe: TagAnlegen) async throws -> Schlagwort {
try await schicke(.post, "tags", koerper: eingabe)
}
func aendereTag(_ id: Int, _ aenderung: TagAendern) async throws -> Schlagwort {
try await schicke(.patch, "tags/\(id)", koerper: aenderung)
}
func loescheTag(_ id: Int) async throws {
try await schickeStumm(.delete, "tags/\(id)")
}
// MARK: - Erkennungswörter
func erkennungswoerter(kategorie: Int) async throws -> [Erkennungswort] {
try await hole("categories/\(kategorie)/keywords")
}
func legeErkennungswortAn(kategorie: Int,
_ eingabe: ErkennungswortAnlegen) async throws -> Erkennungswort {
try await schicke(.post, "categories/\(kategorie)/keywords", koerper: eingabe)
}
func loescheErkennungswort(_ id: Int) async throws {
try await schickeStumm(.delete, "categories/keywords/\(id)")
}
/// Wörter, die oft wieder korrigiert werden – zur Prüfung gemeldet.
func markierteErkennungswoerter() async throws -> [MarkiertesWort] {
try await hole("categories/keywords/flagged")
}
/// „Behalten": Das Wort stimmt, die Korrekturen waren Einzelfälle.
func bestaetigeErkennungswort(_ id: Int) async throws {
try await schickeStumm(.post, "categories/keywords/\(id)/confirm")
}
// MARK: - Zweiter Faktor
func zweiterFaktorStand() async throws -> ZweiterFaktorStand {
try await hole("auth/2fa/status")
}
/// Legt ein noch **nicht** aktives Geheimnis an. Gültig wird es erst mit
/// `aktiviereZweitenFaktor`.
func richteZweitenFaktorEin() async throws -> ZweiterFaktorEinrichtung {
try await schicke(.post, "auth/2fa/setup")
}
/// Prüft einen Code aus der Authenticator-App und schaltet scharf.
/// Die Rettungscodes kommen dabei **einmal** – danach nie wieder.
func aktiviereZweitenFaktor(code: String) async throws -> Rettungscodes {
try await schicke(.post, "auth/2fa/activate",
koerper: ZweiterFaktorCode(code: code))
}
// MARK: - Bank
func bankverbindungen() async throws -> [Bankverbindung] {
try await hole("bank/connections")
}
/// Holt die Umsätze dieser Verbindung in den Eingang.
func rufeBankAb(_ verbindung: Int) async throws -> Abrufergebnis {
try await schicke(.post, "bank/connections/\(verbindung)/sync")
}
/// Nur die Salden auffrischen, ohne Umsätze zu holen.
func frischeSaldenAuf(_ verbindung: Int) async throws {
try await schickeStumm(.post, "bank/connections/\(verbindung)/salden")
}
}