Files
scan-receipts/app/ios/ScanReceipts/Features/Receipts/ExportSheet.swift

100 lines
3.7 KiB
Swift

import SwiftUI
/// Presented from `ReceiptsListView`'s toolbar. Exports the given receipts as
/// CSV, XLSX, or PDF via `ExportAPI`, writes the result to a temp file (so
/// the receiving app sees the right filename/extension), and hands it to the
/// iOS share sheet.
struct ExportSheet: View {
let receipts: [Receipt]
@Environment(\.dismiss) private var dismiss
@State private var isExporting = false
@State private var errorMessage: String?
@State private var shareFile: ShareFile?
var body: some View {
NavigationStack {
VStack(spacing: ZenithSpacing.sm) {
if receipts.isEmpty {
Text("Keine Belege zum Exportieren.")
.zenithBodyStyle(color: .zenithMuted)
.padding(.top, 40)
} else {
Text("\(receipts.count) Beleg\(receipts.count == 1 ? "" : "e") exportieren")
.zenithHeadlineMdStyle()
.padding(.top, ZenithSpacing.md)
VStack(spacing: ZenithSpacing.xs) {
exportButton(title: "Als CSV exportieren", systemImage: "tablecells", format: .csv)
exportButton(title: "Als Excel exportieren", systemImage: "tablecells.fill", format: .excel)
exportButton(title: "Als PDF exportieren", systemImage: "doc.richtext", format: .pdf)
}
.padding(.horizontal, ZenithSpacing.sm)
}
if isExporting {
ProgressView()
}
if let errorMessage {
Text(errorMessage)
.zenithBodySmStyle(color: .zenithError)
.multilineTextAlignment(.center)
.padding(.horizontal, ZenithSpacing.sm)
}
Spacer()
}
.background(Color.zenithBg.ignoresSafeArea())
.navigationTitle("Exportieren")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Fertig") { dismiss() }
.buttonStyle(.zenithPlain)
}
}
.sheet(item: $shareFile) { file in
ShareSheet(activityItems: [file.url])
}
}
}
private func exportButton(title: String, systemImage: String, format: ExportAPI.Format) -> some View {
Button {
Task { await export(format) }
} label: {
Label(title, systemImage: systemImage)
.frame(maxWidth: .infinity)
}
.buttonStyle(.zenithPrimary)
.disabled(isExporting)
}
private func export(_ format: ExportAPI.Format) async {
isExporting = true
errorMessage = nil
defer { isExporting = false }
do {
let result = try await ExportAPI.export(format, receipts: receipts, locale: "de")
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(result.fileName)
try result.data.write(to: tempURL, options: .atomic)
shareFile = ShareFile(url: tempURL)
} catch let apiError as APIError {
if case .server(let code, _, _) = apiError, code == "pro_required" {
errorMessage = "Export ist Pro-Nutzern vorbehalten."
} else {
errorMessage = apiError.localizedDescription
}
} catch {
errorMessage = error.localizedDescription
}
}
}
/// Wraps a temp-file URL so it can drive `.sheet(item:)`.
private struct ShareFile: Identifiable {
let url: URL
var id: String { url.absoluteString }
}