NFC-Tags fuer Einzelstuecke: beschreiben und lesen

Ein aufgeklebtes QR-Etikett uebersteht keine Waesche. Kleidungsstuecke bekommen
deshalb eingenaehte NFC-Tags. Auf das Tag kommt genau das, was auch im QR steckt
(vorrania://i/<UID>) - damit bleibt das Einlesen inhaltlich unveraendert, nur die
Erfassungsart ist neu. Backend und Web brauchten nichts: die UID vergibt der
Server, und GET /items/by-uid/<UID> gibt es laengst.

Gelesen wird nur bei geoeffneter App. iOS startet Apps beim Antippen im
Hintergrund ausschliesslich ueber https-Universal-Links auf einer beanspruchten
Domain und ignoriert eigene Schemes dabei - eine feste Domain ist fuer einen
selbstgehosteten Dienst aber gerade der falsche Weg (siehe f981f41).

Die /i/-Zerlegung lag dreifach kopiert in CheckInView, LookupScanView und
AssignScanView und steckt jetzt in einzelstueckCode(aus:). Die nackte UID wird
nur fuer NFC akzeptiert: eine achtstellige EAN-8 kann zufaellig genau wie eine
UID aussehen, und die Kamera sieht fremde Barcodes - ein Tag haben wir selbst
beschrieben.

Zwei Altlasten in denselben Dateien mit erledigt:
- project.yml deklarierte die applinks-Domain weiter, obwohl f981f41 sie
  entfernt hat. XcodeGen erzeugt die Entitlements aus diesem Block und schrieb
  sie bei jedem "generate" zurueck; die Domain existiert laengst nicht mehr.
- AssignScanView sendete ItemUpdateRequest ohne priceCents/currency. Da encode
  immer alle Felder schreibt (auch null), hat ein Zuordnen per Scan Kaufpreis
  und Waehrung stillschweigend geloescht.

Ebenfalls raus: QRImage und qrLink(forUid:) in ItemViews - ungenutzt, und
qrLink baute noch das alte https-Format.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-12 13:37:24 +02:00
co-authored by Claude Opus 5
parent 9be772557e
commit 09a763a823
11 changed files with 508 additions and 56 deletions
+33 -9
View File
@@ -38,7 +38,19 @@ struct AssignScanView: View {
VStack(spacing: 8) {
Text("Schritt 1: Einzelstück-QR scannen – oder ohne Code:")
.font(.caption).foregroundStyle(.secondary)
// Für Stücke ohne eigenen QR (z.B. Kleidung): aus der Liste wählen.
// Kleidung trägt keinen QR, aber ein eingenähtes NFC-Tag.
NFCLesenButton(titel: "Einzelstück-Tag lesen") { uid in
Task {
let gefunden = await waehle(uid: uid)
if !gefunden {
error = "Kein Einzelstück mit der UID \(uid)."
status = nil
}
}
} onFehler: { meldung in
error = meldung; status = nil
}
// Für Stücke ohne Code jeder Art: aus der Liste wählen.
Button { showPicker = true } label: {
Label("Einzelstück aus Liste wählen", systemImage: "list.bullet")
}
@@ -78,9 +90,14 @@ struct AssignScanView: View {
error = "Erst ein Einzelstück scannen."; status = nil; return
}
do {
// Alle Felder mitsenden: ItemUpdateRequest schreibt auch null
// mit, damit sich Felder leeren lassen. Kaufpreis und Waehrung
// fehlten hier – ein Zuordnen per Scan hat sie stillschweigend
// geloescht.
_ = try await APIClient.shared.updateItem(id: it.id, ItemUpdateRequest(
locationId: locCode, shopId: it.shopId,
acquiredOn: it.acquiredOn, warrantyUntil: it.warrantyUntil, note: it.note))
acquiredOn: it.acquiredOn, warrantyUntil: it.warrantyUntil, note: it.note,
priceCents: it.priceCents, currency: it.currency))
status = "\(it.uid) zugeordnet. Nächstes Stück scannen."
error = nil
pending = nil
@@ -89,16 +106,23 @@ struct AssignScanView: View {
}
}
// Einzelstück-QR (…/i/<UID>)?
if let r = code.range(of: "/i/") {
let uid = String(code[r.upperBound...])
.trimmingCharacters(in: CharacterSet(charactersIn: "/ ")).uppercased()
if !uid.isEmpty, let it = try? await APIClient.shared.itemByUid(uid: uid) {
pending = it; status = nil; error = nil
return
}
if let uid = einzelstueckCode(aus: code), await waehle(uid: uid) {
return
}
error = "Unbekannter Code – bitte einen Einzelstück- oder Lagerort-QR scannen."
}
/// Einzelstück als „wartet auf Lagerort" vormerken – aus Scan, NFC oder Liste.
/// Gibt zurueck, ob es gefunden wurde; der Aufrufer entscheidet ueber die
/// Fehlermeldung, weil ein Scan noch andere Deutungen zulaesst als ein Tag.
@discardableResult
private func waehle(uid: String) async -> Bool {
guard let it = try? await APIClient.shared.itemByUid(uid: uid) else { return false }
pending = it
status = nil
error = nil
return true
}
}
/// Einzelstück aus einer durchsuchbaren Liste wählen – für Stücke ohne eigenen QR.