import PhotosUI import SwiftUI import VisionKit /// Root view of the "Scannen" tab (see `App/MainTabView.swift`). Offers two /// entry points into the scan flow — the system document camera and a photo /// library fallback — uploads whichever image the user provides for AI /// extraction via `ScanViewModel`, then pushes `ReceiptReviewView` with the /// result. struct ScanTabView: View { @StateObject private var viewModel = ScanViewModel() @State private var isShowingCamera = false @State private var cameraUnsupportedAlertPresented = false @State private var photoPickerItem: PhotosPickerItem? @State private var extractedReceipt: Receipt? @State private var isShowingReview = false var body: some View { NavigationStack { ZStack { Color.zenithBg.ignoresSafeArea() VStack(spacing: ZenithSpacing.md) { Spacer() Image(systemName: "camera.viewfinder") .font(.system(size: 64)) .foregroundStyle(.zenithSubtle) Text("Beleg scannen") .zenithHeadlineLgStyle() Text("Fotografiere einen Beleg oder wähle ein vorhandenes Foto aus deiner Fotomediathek.") .zenithBodySmStyle() .multilineTextAlignment(.center) .padding(.horizontal, ZenithSpacing.md) VStack(spacing: ZenithSpacing.xs) { Button { startCamera() } label: { Label("Kamera", systemImage: "camera") .frame(maxWidth: .infinity) } .buttonStyle(.zenithPrimary) .disabled(viewModel.isUploading) PhotosPicker(selection: $photoPickerItem, matching: .images) { Label("Aus Fotos wählen", systemImage: "photo.on.rectangle") .frame(maxWidth: .infinity) } .buttonStyle(.zenithSecondary) .disabled(viewModel.isUploading) } .padding(.horizontal, ZenithSpacing.md) .padding(.top, ZenithSpacing.xs) if viewModel.isUploading { ProgressView("Beleg wird analysiert …") .tint(.zenithBlack) .zenithBodySmStyle() .padding(.top, ZenithSpacing.xs) } if let errorMessage = viewModel.errorMessage { Text(errorMessage) .zenithBodySmStyle(color: .zenithError) .multilineTextAlignment(.center) .padding(.horizontal, ZenithSpacing.md) .padding(.top, ZenithSpacing.unit) } Spacer() Spacer() } } .navigationTitle("Scannen") .fullScreenCover(isPresented: $isShowingCamera) { DocumentCameraView { image in isShowingCamera = false guard let image else { return } Task { await processImage(image) } } .ignoresSafeArea() } .onChange(of: photoPickerItem) { _, newItem in guard let newItem else { return } Task { await handlePickedPhoto(newItem) photoPickerItem = nil } } .alert("Kamera nicht verfügbar", isPresented: $cameraUnsupportedAlertPresented) { Button("OK", role: .cancel) {} } message: { Text("Die Dokumentenkamera wird auf diesem Gerät nicht unterstützt. Bitte wähle stattdessen ein Foto aus deiner Fotomediathek.") } .navigationDestination(isPresented: $isShowingReview) { if let extractedReceipt { ReceiptReviewView(receipt: extractedReceipt) } } } } private func startCamera() { if VNDocumentCameraViewController.isSupported { isShowingCamera = true } else { cameraUnsupportedAlertPresented = true } } private func handlePickedPhoto(_ item: PhotosPickerItem) async { do { guard let data = try await item.loadTransferable(type: Data.self), let image = UIImage(data: data) else { viewModel.errorMessage = "Foto konnte nicht geladen werden." return } await processImage(image) } catch { viewModel.errorMessage = "Foto konnte nicht geladen werden." } } private func processImage(_ image: UIImage) async { if let receipt = await viewModel.uploadAndExtract(image: image) { extractedReceipt = receipt isShowingReview = true } } }