import SwiftUI /// The "Belege" tab root (see `App/MainTabView.swift`, which references this /// type by a zero-arg init). Loads the signed-in user's receipts, supports /// client-side search by merchant name, swipe-to-delete, and exporting the /// currently visible receipts via `ExportSheet`. Owns its own /// `NavigationStack`. struct ReceiptsListView: View { @State private var receipts: [Receipt] = [] @State private var searchText = "" @State private var isLoading = false @State private var loadErrorMessage: String? @State private var deleteErrorMessage: String? @State private var showExportSheet = false var body: some View { NavigationStack { Group { if receipts.isEmpty && !isLoading && loadErrorMessage == nil { ContentUnavailableView( "Noch keine Belege gescannt.", systemImage: "list.bullet.rectangle" ) .foregroundStyle(Color.zenithMuted) } else { receiptsList } } .background(Color.zenithBg.ignoresSafeArea()) .navigationTitle("Belege") .searchable(text: $searchText, prompt: "Suchen") .toolbar { ToolbarItem(placement: .topBarTrailing) { Button { showExportSheet = true } label: { Label("Exportieren", systemImage: "square.and.arrow.up") } .disabled(filteredReceipts.isEmpty) } } .task { await load() } .refreshable { await load() } .sheet(isPresented: $showExportSheet) { ExportSheet(receipts: filteredReceipts) } .alert( "Löschen fehlgeschlagen", isPresented: Binding( get: { deleteErrorMessage != nil }, set: { isPresented in if !isPresented { deleteErrorMessage = nil } } ) ) { Button("OK", role: .cancel) { deleteErrorMessage = nil } } message: { Text(deleteErrorMessage ?? "") } } } /// Client-side filter by merchant name — this is also what gets exported /// via the toolbar button, so exporting after a search exports only the /// filtered set. private var filteredReceipts: [Receipt] { let trimmed = searchText.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return receipts } return receipts.filter { $0.merchant.name.localizedCaseInsensitiveContains(trimmed) } } private var receiptsList: some View { List { if let loadErrorMessage { Section { Text(loadErrorMessage) .zenithBodySmStyle(color: .zenithError) } .listRowBackground(Color.zenithSurface) .listRowSeparatorTint(Color.zenithBorder) } ForEach(filteredReceipts) { receipt in NavigationLink { ReceiptDetailView(receipt: receipt) } label: { ReceiptRow(receipt: receipt) } .listRowBackground(Color.zenithSurface) .listRowSeparatorTint(Color.zenithBorder) .swipeActions(edge: .trailing, allowsFullSwipe: true) { Button(role: .destructive) { Task { await delete(receipt) } } label: { Label("Löschen", systemImage: "trash") } } } } .listStyle(.plain) .zenithListBackground() } private func load() async { isLoading = true loadErrorMessage = nil defer { isLoading = false } do { receipts = try await ReceiptsAPI.list() } catch { loadErrorMessage = error.localizedDescription } } /// Optimistic delete: removes the row immediately, then confirms with the /// server. On failure the row is reinserted at its original position and /// an error alert is shown. private func delete(_ receipt: Receipt) async { guard let index = receipts.firstIndex(where: { $0.id == receipt.id }) else { return } let removed = receipts.remove(at: index) do { try await ReceiptsAPI.delete(id: removed.id) } catch { receipts.insert(removed, at: min(index, receipts.count)) deleteErrorMessage = error.localizedDescription } } }