App: Lagerort bei Chargen anzeigen und aendern
In der Chargenliste eines Artikels standen nur MHD und Menge. Zwei Chargen
mit gleichem MHD, die sich nur im Lagerort unterscheiden, sahen dadurch
identisch aus. Die Zeile nennt jetzt zusaetzlich den Lagerort-Pfad (bzw.
"ohne Ort").
Im Sheet "Charge aendern" kommt ein Lagerort-Picker samt QR-Scan dazu -
wie in der Weboberflaeche. Das Backend konnte das ueber PATCH /lots/{id}
schon; LotUpdateRequest sendet location_id jetzt immer mit (auch null),
damit sich ein gesetzter Ort wieder entfernen laesst.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -605,15 +605,28 @@ struct ProductUpdateRequest: Encodable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LotUpdateRequest: Codable {
|
struct LotUpdateRequest: Encodable {
|
||||||
var quantity: Double?
|
var quantity: Double?
|
||||||
var bestBefore: String?
|
var bestBefore: String?
|
||||||
var bestBeforePrecision: String?
|
var bestBeforePrecision: String?
|
||||||
|
var locationId: String?
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case quantity
|
case quantity
|
||||||
case bestBefore = "best_before"
|
case bestBefore = "best_before"
|
||||||
case bestBeforePrecision = "best_before_precision"
|
case bestBeforePrecision = "best_before_precision"
|
||||||
|
case locationId = "location_id"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Den Lagerort immer senden (auch null), damit „ohne Ort" den gesetzten Ort
|
||||||
|
// wieder entfernt. Die übrigen Felder bleiben weg, wenn sie nicht gesetzt
|
||||||
|
// sind – das Backend fasst sie dann als „unverändert" auf.
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
var c = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try c.encodeIfPresent(quantity, forKey: .quantity)
|
||||||
|
try c.encodeIfPresent(bestBefore, forKey: .bestBefore)
|
||||||
|
try c.encodeIfPresent(bestBeforePrecision, forKey: .bestBeforePrecision)
|
||||||
|
try c.encode(locationId, forKey: .locationId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -241,7 +241,10 @@ struct ProductDetailView: View {
|
|||||||
HStack {
|
HStack {
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text("MHD \(display.formatBestBefore(lot.bestBefore, precision: lot.bestBeforePrecision))")
|
Text("MHD \(display.formatBestBefore(lot.bestBefore, precision: lot.bestBeforePrecision))")
|
||||||
Text("\(formatAmount(lot.quantity / current.articleUnitFactor)) \(current.articleUnitLabel)")
|
// Ohne den Lagerort sehen zwei Chargen mit
|
||||||
|
// gleichem MHD identisch aus – er ist hier
|
||||||
|
// oft das einzige Unterscheidungsmerkmal.
|
||||||
|
Text("\(formatAmount(lot.quantity / current.articleUnitFactor)) \(current.articleUnitLabel) · \(lotLocationLabel(lot))")
|
||||||
.font(.caption).foregroundStyle(.secondary)
|
.font(.caption).foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
@@ -286,7 +289,7 @@ struct ProductDetailView: View {
|
|||||||
}
|
}
|
||||||
.sheet(item: $editLot) { lot in
|
.sheet(item: $editLot) { lot in
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
LotEditView(lot: lot, product: current) {
|
LotEditView(lot: lot, product: current, locations: locations) {
|
||||||
Task { await reload() }
|
Task { await reload() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -364,6 +367,14 @@ struct ProductDetailView: View {
|
|||||||
return Binding(get: { fieldValues[key] ?? "" }, set: { fieldValues[key] = $0 })
|
return Binding(get: { fieldValues[key] ?? "" }, set: { fieldValues[key] = $0 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Lagerort einer Charge als Pfad („Keller → Regal 2"). Chargen ohne Ort –
|
||||||
|
/// und solche, deren Ort (noch) nicht geladen ist – bleiben „ohne Ort".
|
||||||
|
private func lotLocationLabel(_ lot: Lot) -> String {
|
||||||
|
guard let id = lot.locationId,
|
||||||
|
let ort = locations.first(where: { $0.id == id }) else { return "ohne Ort" }
|
||||||
|
return ort.path(in: locations)
|
||||||
|
}
|
||||||
|
|
||||||
private func reload() async {
|
private func reload() async {
|
||||||
groups = (try? await APIClient.shared.groups()) ?? []
|
groups = (try? await APIClient.shared.groups()) ?? []
|
||||||
categories = (try? await APIClient.shared.categories()) ?? []
|
categories = (try? await APIClient.shared.categories()) ?? []
|
||||||
@@ -508,10 +519,11 @@ struct ProductByIdView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Menge und MHD einer Charge korrigieren.
|
/// Menge, MHD und Lagerort einer Charge korrigieren.
|
||||||
struct LotEditView: View {
|
struct LotEditView: View {
|
||||||
let lot: Lot
|
let lot: Lot
|
||||||
let product: Product
|
let product: Product
|
||||||
|
let locations: [StorageLocation]
|
||||||
var onSaved: () -> Void
|
var onSaved: () -> Void
|
||||||
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
@@ -520,6 +532,8 @@ struct LotEditView: View {
|
|||||||
@State private var hasDate = false
|
@State private var hasDate = false
|
||||||
@State private var bestBefore = Date()
|
@State private var bestBefore = Date()
|
||||||
@State private var precision = "day"
|
@State private var precision = "day"
|
||||||
|
@State private var locationId: String?
|
||||||
|
@State private var locScanShown = false
|
||||||
@State private var busy = false
|
@State private var busy = false
|
||||||
@State private var error: String?
|
@State private var error: String?
|
||||||
|
|
||||||
@@ -545,6 +559,19 @@ struct LotEditView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !locations.isEmpty {
|
||||||
|
Section("Lagerort") {
|
||||||
|
Picker("Lagerort", selection: $locationId) {
|
||||||
|
Text("– ohne –").tag(String?.none)
|
||||||
|
ForEach(locations) { l in Text(l.path(in: locations)).tag(String?.some(l.id)) }
|
||||||
|
}
|
||||||
|
// Ort per QR am Regal/Fach setzen, statt in der Liste zu suchen.
|
||||||
|
Button { locScanShown = true } label: {
|
||||||
|
Label("Lagerort scannen", systemImage: "qrcode.viewfinder")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let error {
|
if let error {
|
||||||
Section { Text(error).foregroundStyle(.red).font(.callout) }
|
Section { Text(error).foregroundStyle(.red).font(.callout) }
|
||||||
}
|
}
|
||||||
@@ -559,11 +586,16 @@ struct LotEditView: View {
|
|||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .topBarLeading) { Button("Abbrechen") { dismiss() } }
|
ToolbarItem(placement: .topBarLeading) { Button("Abbrechen") { dismiss() } }
|
||||||
}
|
}
|
||||||
|
.fullScreenCover(isPresented: $locScanShown) {
|
||||||
|
LocationScannerView(locations: locations) { ort in locationId = ort.id }
|
||||||
|
.ignoresSafeArea()
|
||||||
|
}
|
||||||
.onAppear(perform: fill)
|
.onAppear(perform: fill)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func fill() {
|
private func fill() {
|
||||||
quantity = formatAmount(lot.quantity / product.articleUnitFactor)
|
quantity = formatAmount(lot.quantity / product.articleUnitFactor)
|
||||||
|
locationId = lot.locationId
|
||||||
precision = lot.bestBeforePrecision == "month" ? "month" : "day"
|
precision = lot.bestBeforePrecision == "month" ? "month" : "day"
|
||||||
if let raw = lot.bestBefore {
|
if let raw = lot.bestBefore {
|
||||||
let formatter = DateFormatter()
|
let formatter = DateFormatter()
|
||||||
@@ -595,7 +627,8 @@ struct LotEditView: View {
|
|||||||
LotUpdateRequest(
|
LotUpdateRequest(
|
||||||
quantity: menge * product.articleUnitFactor,
|
quantity: menge * product.articleUnitFactor,
|
||||||
bestBefore: hasDate ? formatter.string(from: datum) : nil,
|
bestBefore: hasDate ? formatter.string(from: datum) : nil,
|
||||||
bestBeforePrecision: precision
|
bestBeforePrecision: precision,
|
||||||
|
locationId: locationId
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
onSaved()
|
onSaved()
|
||||||
|
|||||||
Reference in New Issue
Block a user