import SwiftUI /// Die Bankanbindungen – abrufen, Salden auffrischen. /// /// **Herstellen geht hier nicht.** Die Bank leitet fest auf /// `{base_url}/bank/rueckkehr` zurück, registriert bei Enable Banking; dieser /// Weg führt zwangsläufig über den Browser am Rechner. Abrufen dagegen ist /// genau das, was man unterwegs will. struct BankAnsicht: View { @Environment(AppZustand.self) private var zustand @State private var lader: Lader<[Bankverbindung]>? @State private var vorgang = Speichervorgang() @State private var meldung: String? var body: some View { Group { if let lader { Inhalt(zustand: lader.zustand, nochmal: { Task { await lader.lade() } }) { liste in inhalt(liste) } } else { ProgressView() } } .navigationTitle(String(localized: "nebenraeume.bank")) .task { guard lader == nil, let client = zustand.client else { return } let neuer = Lader<[Bankverbindung]>(schluessel: "bank/connections") { try await client.bankverbindungen() } lader = neuer await neuer.lade() } .alert(String(localized: "bank.abrufFertig"), isPresented: .constant(meldung != nil)) { Button(String(localized: "allgemein.ok")) { meldung = nil } } message: { Text(meldung ?? "") } .alert(String(localized: "fehler.titel"), isPresented: .constant(vorgang.fehler != nil)) { Button(String(localized: "allgemein.ok")) { vorgang.fehler = nil } } message: { Text(vorgang.fehler ?? "") } } @ViewBuilder private func inhalt(_ liste: [Bankverbindung]) -> some View { if liste.isEmpty { LeerAnsicht(titel: String(localized: "bank.leerTitel"), erklaerung: String(localized: "bank.leerErklaerung"), symbol: "building.columns") } else { List { ForEach(liste) { verbindung in Section { VerbindungsZeile(verbindung: verbindung) Button { Task { await rufeAb(verbindung) } } label: { Label(String(localized: "bank.abrufen"), systemImage: "arrow.down.circle") } Button { Task { await frischeSalden(verbindung) } } label: { Label(String(localized: "bank.saldenAuffrischen"), systemImage: "arrow.clockwise") } } header: { Text(verbindung.aspspName) } footer: { if let fehler = verbindung.lastError { // Der letzte Fehler steht am Server. Ihn zu // verschweigen hiesse, den Nutzer vergeblich // abrufen zu lassen. Label(fehler, systemImage: "exclamationmark.triangle") .foregroundStyle(.orange) } } } } .listStyle(.insetGrouped) .refreshable { await lader?.aktualisiere() } .disabled(vorgang.laeuft) } } private func rufeAb(_ verbindung: Bankverbindung) async { guard let client = zustand.client else { return } var ergebnis: Abrufergebnis? let geglueckt = await vorgang.fuehreAus { ergebnis = try await client.rufeBankAb(verbindung.id) } if geglueckt { meldung = ergebnis?.meldung await lader?.aktualisiere() } } private func frischeSalden(_ verbindung: Bankverbindung) async { guard let client = zustand.client else { return } if await vorgang.fuehreAus({ try await client.frischeSaldenAuf(verbindung.id) }) { meldung = String(localized: "bank.saldenAufgefrischt") await lader?.aktualisiere() } } } private struct VerbindungsZeile: View { let verbindung: Bankverbindung var body: some View { VStack(alignment: .leading, spacing: 4) { LabeledContent(String(localized: "bank.land"), value: verbindung.aspspCountry) if let zuletzt = verbindung.lastSyncAt { LabeledContent(String(localized: "bank.zuletzt")) { Text(zuletzt.datum.formatted(.relative(presentation: .named))) } } else { LabeledContent(String(localized: "bank.zuletzt"), value: String(localized: "bank.nie")) } if let gueltig = verbindung.validUntil { LabeledContent(String(localized: "bank.gueltigBis")) { Text(gueltig.datum.formatted(.dateTime.day().month().year())) // Eine auslaufende Anbindung bricht sonst ohne // Vorwarnung ab, und niemand weiss warum. .foregroundStyle(verbindung.expiresSoon ? .orange : .secondary) } } if verbindung.pollMinutes > 0 { LabeledContent(String(localized: "bank.takt")) { Text(String(format: String(localized: "bank.alleNMinuten"), verbindung.pollMinutes)) } } } .font(.callout) } }