iOS: Kaufpreis + Belege am Einzelstück
ItemEditView bekommt einen Kaufpreis (Betrag + CHF/EUR) und eine Beleg-Sektion: Rechnung/Garantieschein als Bild (Fotoauswahl) oder PDF (Datei-Import) hochladen, ansehen (QuickLook) und loeschen. Nach einem PDF-Upload schlaegt der Server "Garantie bis" und Kaufpreis vor – per Knopf uebernehmbar. Item-Model um priceCents/currency/documents erweitert; Client-Methoden fuer Upload, Download und Loeschen der Belege. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
import SwiftUI
|
||||
import CoreImage
|
||||
import PhotosUI
|
||||
import QuickLook
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
/// QR-Code als Bild (CoreImage). Inhalt ist der Link auf das Einzelstück, damit
|
||||
/// ein Scan – auch mit der Systemkamera – die App/Weboberfläche öffnet.
|
||||
@@ -96,11 +99,20 @@ struct ItemEditView: View {
|
||||
@State private var hasWarranty = false
|
||||
@State private var warranty = Date()
|
||||
@State private var note = ""
|
||||
@State private var priceText = ""
|
||||
@State private var currency = "CHF"
|
||||
@State private var documents: [ItemDocument] = []
|
||||
@State private var suggWarranty: String?
|
||||
@State private var suggPrice: Int?
|
||||
@State private var busy = false
|
||||
@State private var busyDoc = false
|
||||
@State private var error: String?
|
||||
@State private var showRemove = false
|
||||
@State private var reason = "broken"
|
||||
@State private var removeNote = ""
|
||||
@State private var showFileImporter = false
|
||||
@State private var pickerItem: PhotosPickerItem?
|
||||
@State private var previewURL: URL?
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
@@ -136,6 +148,52 @@ struct ItemEditView: View {
|
||||
LabeledField(label: "Notiz", text: $note)
|
||||
}
|
||||
|
||||
Section("Kaufpreis") {
|
||||
HStack {
|
||||
TextField("0.00", text: $priceText).keyboardType(.decimalPad)
|
||||
Picker("", selection: $currency) {
|
||||
Text("CHF").tag("CHF")
|
||||
Text("EUR").tag("EUR")
|
||||
}
|
||||
.pickerStyle(.segmented).frame(width: 130)
|
||||
}
|
||||
}
|
||||
|
||||
Section("Belege (Rechnung/Garantieschein)") {
|
||||
if suggWarranty != nil || suggPrice != nil {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Im Beleg erkannt:").font(.caption).foregroundStyle(.secondary)
|
||||
if let w = suggWarranty { Text("Garantie bis \(w)") }
|
||||
if let p = suggPrice { Text("Kaufpreis \(ItemEditView.formatCents(p)) \(currency)") }
|
||||
HStack {
|
||||
Button("Übernehmen") { applySuggestion() }
|
||||
Spacer()
|
||||
Button("Verwerfen") { suggWarranty = nil; suggPrice = nil }
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.font(.callout)
|
||||
}
|
||||
}
|
||||
ForEach(documents) { d in
|
||||
Button { Task { await openDocument(d) } } label: {
|
||||
Label(d.filename, systemImage: "doc.text")
|
||||
}
|
||||
.swipeActions(edge: .trailing) {
|
||||
Button("Löschen", role: .destructive) { Task { await deleteDoc(d) } }
|
||||
}
|
||||
}
|
||||
if documents.isEmpty {
|
||||
Text("Noch keine Belege.").foregroundStyle(.secondary)
|
||||
}
|
||||
PhotosPicker(selection: $pickerItem, matching: .images) {
|
||||
Label("Bild hochladen", systemImage: "photo")
|
||||
}
|
||||
Button { showFileImporter = true } label: {
|
||||
Label("PDF hochladen", systemImage: "doc.badge.plus")
|
||||
}
|
||||
if busyDoc { ProgressView() }
|
||||
}
|
||||
|
||||
if let error { Section { Text(error).foregroundStyle(.red).font(.callout) } }
|
||||
|
||||
Section {
|
||||
@@ -150,6 +208,30 @@ struct ItemEditView: View {
|
||||
.sheet(isPresented: $showRemove) {
|
||||
NavigationStack { removeSheet }
|
||||
}
|
||||
.fileImporter(isPresented: $showFileImporter, allowedContentTypes: [.pdf]) { result in
|
||||
if case .success(let url) = result {
|
||||
Task {
|
||||
let scoped = url.startAccessingSecurityScopedResource()
|
||||
defer { if scoped { url.stopAccessingSecurityScopedResource() } }
|
||||
if let data = try? Data(contentsOf: url) {
|
||||
await upload(data: data, filename: url.lastPathComponent,
|
||||
contentType: "application/pdf")
|
||||
}
|
||||
}
|
||||
} else if case .failure(let err) = result {
|
||||
error = err.localizedDescription
|
||||
}
|
||||
}
|
||||
.onChange(of: pickerItem) { neu in
|
||||
guard let neu else { return }
|
||||
Task {
|
||||
if let data = try? await neu.loadTransferable(type: Data.self) {
|
||||
await upload(data: data, filename: "foto.jpg", contentType: "image/jpeg")
|
||||
}
|
||||
pickerItem = nil
|
||||
}
|
||||
}
|
||||
.quickLookPreview($previewURL)
|
||||
}
|
||||
|
||||
private var removeSheet: some View {
|
||||
@@ -174,23 +256,84 @@ struct ItemEditView: View {
|
||||
locationId = item.locationId
|
||||
shopId = item.shopId
|
||||
note = item.note ?? ""
|
||||
documents = item.documents
|
||||
currency = item.currency ?? "CHF"
|
||||
if let p = item.priceCents { priceText = ItemEditView.formatCents(p) }
|
||||
if let s = item.acquiredOn, let d = stringToDate(s) { acquired = d; hasAcquired = true }
|
||||
if let s = item.warrantyUntil, let d = stringToDate(s) { warranty = d; hasWarranty = true }
|
||||
}
|
||||
|
||||
private func save() async {
|
||||
busy = true; defer { busy = false }; error = nil
|
||||
let cents = ItemEditView.parseCents(priceText)
|
||||
do {
|
||||
_ = try await APIClient.shared.updateItem(id: item.id, ItemUpdateRequest(
|
||||
locationId: locationId, shopId: shopId,
|
||||
acquiredOn: hasAcquired ? dateToString(acquired) : nil,
|
||||
warrantyUntil: hasWarranty ? dateToString(warranty) : nil,
|
||||
note: note.isEmpty ? nil : note))
|
||||
note: note.isEmpty ? nil : note,
|
||||
priceCents: cents,
|
||||
currency: cents == nil ? nil : currency))
|
||||
onChanged?()
|
||||
dismiss()
|
||||
} catch { self.error = error.localizedDescription }
|
||||
}
|
||||
|
||||
// MARK: - Belege
|
||||
|
||||
private func upload(data: Data, filename: String, contentType: String) async {
|
||||
busyDoc = true
|
||||
defer { busyDoc = false }
|
||||
do {
|
||||
let res = try await APIClient.shared.uploadItemDocument(
|
||||
itemId: item.id, data: data, filename: filename, contentType: contentType)
|
||||
suggWarranty = res.suggestedWarrantyUntil
|
||||
suggPrice = res.suggestedPriceCents
|
||||
await reloadDocuments()
|
||||
} catch { self.error = error.localizedDescription }
|
||||
}
|
||||
|
||||
private func reloadDocuments() async {
|
||||
if let fresh = try? await APIClient.shared.itemByUid(uid: item.uid) {
|
||||
documents = fresh.documents
|
||||
}
|
||||
}
|
||||
|
||||
private func openDocument(_ d: ItemDocument) async {
|
||||
do {
|
||||
let data = try await APIClient.shared.itemDocumentData(itemId: item.id, docId: d.id)
|
||||
let name = d.filename.isEmpty ? "beleg" : d.filename
|
||||
let url = FileManager.default.temporaryDirectory.appendingPathComponent(name)
|
||||
try data.write(to: url)
|
||||
previewURL = url
|
||||
} catch { self.error = error.localizedDescription }
|
||||
}
|
||||
|
||||
private func deleteDoc(_ d: ItemDocument) async {
|
||||
do {
|
||||
try await APIClient.shared.deleteItemDocument(itemId: item.id, docId: d.id)
|
||||
await reloadDocuments()
|
||||
} catch { self.error = error.localizedDescription }
|
||||
}
|
||||
|
||||
private func applySuggestion() {
|
||||
if let w = suggWarranty, let d = stringToDate(w) { warranty = d; hasWarranty = true }
|
||||
if let p = suggPrice { priceText = ItemEditView.formatCents(p) }
|
||||
suggWarranty = nil
|
||||
suggPrice = nil
|
||||
}
|
||||
|
||||
/// Eingabe in Hauptwährungseinheit → Rappen/Cent.
|
||||
static func parseCents(_ s: String) -> Int? {
|
||||
let t = s.trimmingCharacters(in: .whitespaces).replacingOccurrences(of: ",", with: ".")
|
||||
guard !t.isEmpty, let v = Double(t) else { return nil }
|
||||
return Int((v * 100).rounded())
|
||||
}
|
||||
|
||||
static func formatCents(_ c: Int) -> String {
|
||||
String(format: "%.2f", Double(c) / 100)
|
||||
}
|
||||
|
||||
private func remove() async {
|
||||
do {
|
||||
try await APIClient.shared.removeItem(id: item.id, ItemRemoveRequest(
|
||||
|
||||
Reference in New Issue
Block a user