iOS: Kaufpreis + Beleg beim Anlegen; Kaufdatum/Shop-Vorschlag
- ItemAddSheet: Kaufpreis (+Währung) und Beleg (Bild/PDF). PDF wird beim Waehlen analysiert und Kaufdatum/Garantie/Preis/Shop vorbefuellt; erkannter neuer Shop wird beim Anlegen erstellt. Beleg haengt an das erste angelegte Stueck. - ItemEditView-Vorschlag zeigt/uebernimmt jetzt auch Kaufdatum und Shop (bekannten zuweisen, sonst anlegen). - Models/Client: DocumentSuggestions, analyzeItemDocument, ItemCreateRequest um price_cents/currency erweitert. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -105,6 +105,9 @@ struct ItemEditView: View {
|
||||
@State private var suggWarranty: String?
|
||||
@State private var suggPrice: Int?
|
||||
@State private var suggPriceCandidates: [Int] = []
|
||||
@State private var suggAcquired: String?
|
||||
@State private var suggShopId: Int?
|
||||
@State private var suggShopName: String?
|
||||
@State private var busy = false
|
||||
@State private var busyDoc = false
|
||||
@State private var error: String?
|
||||
@@ -161,9 +164,10 @@ struct ItemEditView: View {
|
||||
}
|
||||
|
||||
Section("Belege (Rechnung/Garantieschein)") {
|
||||
if suggWarranty != nil || suggPrice != nil {
|
||||
if hatVorschlag {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Im Beleg erkannt:").font(.caption).foregroundStyle(.secondary)
|
||||
if let a = suggAcquired { Text("Gekauft am \(a)") }
|
||||
if let w = suggWarranty { Text("Garantie bis \(w)") }
|
||||
if suggPriceCandidates.count > 1 {
|
||||
// Mehrere Beträge gefunden – Auswahl anbieten.
|
||||
@@ -179,8 +183,13 @@ struct ItemEditView: View {
|
||||
} else if let p = suggPrice {
|
||||
Text("Kaufpreis \(ItemEditView.formatCents(p)) \(currency)")
|
||||
}
|
||||
if let sid = suggShopId {
|
||||
Text("Shop: \(shops.first(where: { $0.id == sid })?.name ?? "bekannt")")
|
||||
} else if let name = suggShopName {
|
||||
Text("Shop anlegen: \(name)")
|
||||
}
|
||||
HStack {
|
||||
Button("Übernehmen") { applySuggestion() }
|
||||
Button("Übernehmen") { Task { await applySuggestion() } }
|
||||
Spacer()
|
||||
Button("Verwerfen") { verwerfeVorschlag() }
|
||||
.foregroundStyle(.secondary)
|
||||
@@ -301,9 +310,13 @@ struct ItemEditView: View {
|
||||
do {
|
||||
let res = try await APIClient.shared.uploadItemDocument(
|
||||
itemId: item.id, data: data, filename: filename, contentType: contentType)
|
||||
suggWarranty = res.suggestedWarrantyUntil
|
||||
suggPrice = res.suggestedPriceCents
|
||||
suggPriceCandidates = res.suggestedPriceCandidates
|
||||
let s = res.suggestions
|
||||
suggWarranty = s.suggestedWarrantyUntil
|
||||
suggPrice = s.suggestedPriceCents
|
||||
suggPriceCandidates = s.suggestedPriceCandidates
|
||||
suggAcquired = s.suggestedAcquiredOn
|
||||
suggShopId = s.suggestedShopId
|
||||
suggShopName = s.suggestedShopName
|
||||
await reloadDocuments()
|
||||
} catch { self.error = error.localizedDescription }
|
||||
}
|
||||
@@ -331,9 +344,23 @@ struct ItemEditView: View {
|
||||
} catch { self.error = error.localizedDescription }
|
||||
}
|
||||
|
||||
private func applySuggestion() {
|
||||
private var hatVorschlag: Bool {
|
||||
suggWarranty != nil || suggPrice != nil || suggAcquired != nil
|
||||
|| suggShopId != nil || suggShopName != nil
|
||||
}
|
||||
|
||||
private func applySuggestion() async {
|
||||
if let a = suggAcquired, let d = stringToDate(a) { acquired = d; hasAcquired = true }
|
||||
if let w = suggWarranty, let d = stringToDate(w) { warranty = d; hasWarranty = true }
|
||||
if let p = suggPrice { priceText = ItemEditView.formatCents(p) }
|
||||
if let sid = suggShopId {
|
||||
shopId = sid
|
||||
} else if let name = suggShopName,
|
||||
let shop = try? await APIClient.shared.createShop(
|
||||
NewShopRequest(name: name, website: nil)) {
|
||||
if !shops.contains(where: { $0.id == shop.id }) { shops.append(shop) }
|
||||
shopId = shop.id
|
||||
}
|
||||
verwerfeVorschlag()
|
||||
}
|
||||
|
||||
@@ -341,6 +368,9 @@ struct ItemEditView: View {
|
||||
suggWarranty = nil
|
||||
suggPrice = nil
|
||||
suggPriceCandidates = []
|
||||
suggAcquired = nil
|
||||
suggShopId = nil
|
||||
suggShopName = nil
|
||||
}
|
||||
|
||||
/// Eingabe in Hauptwährungseinheit → Rappen/Cent.
|
||||
@@ -383,6 +413,15 @@ struct ItemAddSheet: 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 docData: Data?
|
||||
@State private var docName = ""
|
||||
@State private var docType = ""
|
||||
@State private var newShopName: String?
|
||||
@State private var addInfo: String?
|
||||
@State private var showFileImporter = false
|
||||
@State private var pickerItem: PhotosPickerItem?
|
||||
@State private var busy = false
|
||||
@State private var error: String?
|
||||
|
||||
@@ -406,6 +445,37 @@ struct ItemAddSheet: View {
|
||||
} footer: {
|
||||
Text("Gemeinsame Startwerte – danach je Stück änderbar. Jedes Stück bekommt eine eigene UID + QR.")
|
||||
}
|
||||
|
||||
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 {
|
||||
if docData != nil { Text(docName).font(.callout) }
|
||||
PhotosPicker(selection: $pickerItem, matching: .images) {
|
||||
Label("Bild wählen", systemImage: "photo")
|
||||
}
|
||||
Button { showFileImporter = true } label: {
|
||||
Label("PDF wählen", systemImage: "doc.badge.plus")
|
||||
}
|
||||
if let addInfo { Text(addInfo).font(.caption).foregroundStyle(.secondary) }
|
||||
if let newShopName {
|
||||
Text("Neuer Shop „\(newShopName)“ wird beim Anlegen erstellt.")
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
} header: {
|
||||
Text("Beleg (Rechnung/Garantieschein)")
|
||||
} footer: {
|
||||
Text("Der Beleg wird an das erste angelegte Stück gehängt; aus einem PDF werden Kaufdatum, Garantie, Preis und Shop vorgeschlagen.")
|
||||
}
|
||||
|
||||
if let error { Section { Text(error).foregroundStyle(.red).font(.callout) } }
|
||||
Section {
|
||||
Button(busy ? "Anlegen…" : "Anlegen") { Task { await create() } }.disabled(busy)
|
||||
@@ -414,20 +484,83 @@ struct ItemAddSheet: View {
|
||||
.navigationTitle("Einzelstücke anlegen")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar { ToolbarItem(placement: .topBarLeading) { Button("Abbrechen") { dismiss() } } }
|
||||
.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 analyze(data, name: url.lastPathComponent, type: "application/pdf")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: pickerItem) { neu in
|
||||
guard let neu else { return }
|
||||
Task {
|
||||
if let data = try? await neu.loadTransferable(type: Data.self) {
|
||||
await analyze(data, name: "foto.jpg", type: "image/jpeg")
|
||||
}
|
||||
pickerItem = nil
|
||||
}
|
||||
}
|
||||
.task {
|
||||
locations = (try? await APIClient.shared.locations()) ?? []
|
||||
shops = (try? await APIClient.shared.shops()) ?? []
|
||||
}
|
||||
}
|
||||
|
||||
/// Beleg beim Anlegen analysieren und Felder vorbefüllen (nur bei PDF liefert
|
||||
/// der Server Treffer).
|
||||
private func analyze(_ data: Data, name: String, type: String) async {
|
||||
docData = data
|
||||
docName = name
|
||||
docType = type
|
||||
addInfo = nil
|
||||
newShopName = nil
|
||||
guard let s = try? await APIClient.shared.analyzeItemDocument(
|
||||
data: data, filename: name, contentType: type) else { return }
|
||||
var teile: [String] = []
|
||||
if let a = s.suggestedAcquiredOn, let d = stringToDate(a) {
|
||||
acquired = d; hasAcquired = true; teile.append("Kaufdatum")
|
||||
}
|
||||
if let w = s.suggestedWarrantyUntil, let d = stringToDate(w) {
|
||||
warranty = d; hasWarranty = true; teile.append("Garantie")
|
||||
}
|
||||
if let p = s.suggestedPriceCents {
|
||||
priceText = ItemEditView.formatCents(p); teile.append("Preis")
|
||||
}
|
||||
if let sid = s.suggestedShopId {
|
||||
shopId = sid; teile.append("Shop")
|
||||
} else if let sn = s.suggestedShopName {
|
||||
newShopName = sn
|
||||
}
|
||||
addInfo = teile.isEmpty ? "Beleg erkannt – keine Automatik-Treffer."
|
||||
: "Übernommen: \(teile.joined(separator: ", "))."
|
||||
}
|
||||
|
||||
private func create() async {
|
||||
busy = true; defer { busy = false }; error = nil
|
||||
do {
|
||||
_ = try await APIClient.shared.createItems(productId: productId, ItemCreateRequest(
|
||||
count: count, locationId: locationId, shopId: shopId,
|
||||
var sid = shopId
|
||||
if sid == nil, let name = newShopName,
|
||||
let shop = try? await APIClient.shared.createShop(
|
||||
NewShopRequest(name: name, website: nil)) {
|
||||
sid = shop.id
|
||||
}
|
||||
let cents = ItemEditView.parseCents(priceText)
|
||||
let created = try await APIClient.shared.createItems(productId: productId, ItemCreateRequest(
|
||||
count: count, locationId: locationId, shopId: sid,
|
||||
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))
|
||||
if let data = docData, let first = created.first {
|
||||
_ = try? await APIClient.shared.uploadItemDocument(
|
||||
itemId: first.id, data: data,
|
||||
filename: docName.isEmpty ? "beleg" : docName, contentType: docType)
|
||||
}
|
||||
await onDone()
|
||||
dismiss()
|
||||
} catch { self.error = error.localizedDescription }
|
||||
|
||||
Reference in New Issue
Block a user