diff --git a/ios/Sources/ListViews.swift b/ios/Sources/ListViews.swift index bbfeb73..fb40b52 100644 --- a/ios/Sources/ListViews.swift +++ b/ios/Sources/ListViews.swift @@ -666,6 +666,7 @@ struct ChargenListView: View { @State private var nurAngebrochen = false @State private var verbrauchen: LotRow? @State private var anbrechen: LotRow? + @State private var ansehen: LotRow? @State private var busy = true @State private var error: String? @State private var hinweis: String? @@ -692,20 +693,32 @@ struct ChargenListView: View { } Section("\(gefiltert.count) Chargen") { ForEach(gefiltert) { zeile in - VStack(alignment: .leading, spacing: 2) { - HStack(spacing: 6) { - Text(zeile.productName) - if zeile.istAngebrochen { - Text("angebrochen") - .font(.caption2) - .padding(.horizontal, 6).padding(.vertical, 2) - .background(Color.accentColor.opacity(0.15)) - .clipShape(Capsule()) + // Antippen oeffnet die Charge. Das Wischen allein genuegte + // nicht: Man sieht es nicht, und man kam gar nicht an die + // Angaben heran. + Button { + ansehen = zeile + } label: { + HStack { + VStack(alignment: .leading, spacing: 2) { + HStack(spacing: 6) { + Text(zeile.productName) + if zeile.istAngebrochen { + Text("angebrochen") + .font(.caption2) + .padding(.horizontal, 6).padding(.vertical, 2) + .background(Color.accentColor.opacity(0.15)) + .clipShape(Capsule()) + } + } + Text(untertitel(zeile)) + .font(.caption).foregroundStyle(.secondary) } + Spacer() + Image(systemName: "chevron.right").foregroundStyle(.tertiary) } - Text(untertitel(zeile)) - .font(.caption).foregroundStyle(.secondary) } + .foregroundStyle(.primary) // Nach links wischen: die beiden Buchungen. Loeschen gibt es // hier bewusst nicht - eine Charge verschwindet durchs // Verbrauchen, und das Korrigieren sitzt beim Artikel. @@ -734,6 +747,14 @@ struct ChargenListView: View { .navigationBarTitleDisplayMode(.inline) .refreshable { await load() } .task { await load() } + .sheet(item: $ansehen) { zeile in + NavigationStack { + ChargeDetailSheet(zeile: zeile, locations: locations) { meldung in + hinweis = meldung + Task { await load() } + } + } + } .sheet(item: $verbrauchen) { zeile in NavigationStack { VerbrauchSheet(zeile: zeile) { meldung in @@ -918,3 +939,124 @@ struct VerbrauchSheet: View { } } } + +/// Eine Charge ansehen – und von hier aus anbrechen oder verbrauchen. +/// +/// Das Wischen in der Liste allein genügt nicht: Man sieht es nicht, und man +/// sieht dort auch nicht, was an der Charge dranhängt. Hier stehen alle Angaben +/// und beide Handlungen als sichtbare Knöpfe. +struct ChargeDetailSheet: View { + let zeile: LotRow + let locations: [StorageLocation] + /// Meldung nach einer Buchung – die Liste zeigt sie an und lädt neu. + var onDone: (String) -> Void + + @EnvironmentObject private var display: DisplaySettings + @Environment(\.dismiss) private var dismiss + + @State private var anbrechenGefragt = false + @State private var busy = false + @State private var error: String? + + private var ortText: String { + guard let id = zeile.locationId, + let ort = locations.first(where: { $0.id == id }) else { return "ohne Lagerort" } + return ort.path(in: locations) + } + + var body: some View { + Form { + if let error { + Section { Text(error).foregroundStyle(.red).font(.callout) } + } + + Section { + Text(zeile.productName).font(.headline) + if let marke = zeile.productBrand, !marke.isEmpty { + Text(marke).font(.caption).foregroundStyle(.secondary) + } + } + + Section("Bestand") { + LabeledContent("Menge", value: zeile.mengeText) + if zeile.istAngebrochen, let start = zeile.openedQuantity { + LabeledContent("Beim Öffnen", value: "\(formatAmount(start)) \(zeile.baseKurz)") + } + LabeledContent("Zustand", + value: zeile.istAngebrochen ? "angebrochen" : "ungeöffnet") + LabeledContent("Lagerort", value: ortText) + } + + Section("Fristen") { + LabeledContent("MHD", value: zeile.bestBefore == nil + ? "ohne MHD" + : display.formatBestBefore(zeile.bestBefore, + precision: zeile.bestBeforePrecision)) + if let geoeffnet = zeile.openedOn { + LabeledContent("Angebrochen am", + value: display.formatBestBefore(geoeffnet, precision: "day")) + } + if let bis = zeile.consumeBy { + LabeledContent("Verbrauchen bis", + value: display.formatBestBefore(bis, precision: "day")) + } + } + + Section { + // Bewusst ein Weiterblaettern und kein zweites Fenster: Ein + // Sheet im Sheet aufzuziehen und beide gleich wieder zu + // schliessen ist in SwiftUI eine zuverlaessige Quelle fuer + // haengende Ansichten. + NavigationLink { + VerbrauchSheet(zeile: zeile) { meldung in + dismiss() + onDone(meldung) + } + } label: { + Label("Verbrauchen", systemImage: "minus.circle") + } + if !zeile.istAngebrochen { + Button { + anbrechenGefragt = true + } label: { + Label("Anbrechen", systemImage: "arrow.up.bin") + } + .disabled(busy) + } + } footer: { + Text(zeile.istAngebrochen + ? "Diese Packung ist offen. Was du verbrauchst, geht von ihr ab." + : "Anbrechen spaltet eine Packung ab und gibt ihr ein Verbrauchsdatum; " + + "der Rest bleibt ungeöffnet mit seinem MHD.") + } + } + .navigationTitle("Charge") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarLeading) { Button("Fertig") { dismiss() } } + } + .confirmationDialog("Eine Packung anbrechen?", isPresented: $anbrechenGefragt, + titleVisibility: .visible) { + Button("Anbrechen") { Task { await brichAn() } } + Button("Abbrechen", role: .cancel) {} + } message: { + Text("Eine Packung wird als eigene, offene Charge geführt und bekommt ein Verbrauchsdatum. Der Rest bleibt ungeöffnet mit seinem MHD.") + } + } + + private func brichAn() async { + busy = true + defer { busy = false } + do { + let offen = try await APIClient.shared.openLot(id: zeile.id) + dismiss() + if let bis = offen.consumeBy { + onDone("Angebrochen – zu verbrauchen bis \(display.formatBestBefore(bis, precision: "day")).") + } else { + onDone("Angebrochen.") + } + } catch { + self.error = error.localizedDescription + } + } +}