App: eine Charge laesst sich jetzt oeffnen und ansehen

Die neue Chargenliste reagierte nur aufs Wischen. Das sieht man nicht,
und man kam an die Angaben der Charge ueberhaupt nicht heran - Menge,
Fristen und Lagerort standen zwar gekuerzt in der Zeile, aber mehr gab
es nicht.

Antippen oeffnet jetzt die Charge: Artikel und Marke, Menge (bei einer
offenen Packung auch die Menge beim Oeffnen), Zustand, Lagerort, MHD,
Anbruchtag und Verbrauchsdatum. Darunter beide Handlungen als sichtbare
Knoepfe - damit haengt "Anbrechen" nicht mehr allein an einer Geste, die
niemand findet. Das Wischen bleibt als Abkuerzung.

Das Verbrauchen-Formular wird aus der Detailansicht BLAETTERND geoeffnet,
nicht als zweites Fenster. Ein Sheet im Sheet aufzuziehen und beide
gleich wieder zu schliessen ist in SwiftUI eine zuverlaessige Quelle
fuer haengende Ansichten; so bleibt es eine Ebene.

Nicht kompiliert (kein Xcode hier): geprueft sind Klammer- und
Anfuehrungsbalance, dass keine Code-Zeile ein unpaariges
Anfuehrungszeichen traegt, und dass vom entfernten Zustand nichts
stehengeblieben ist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-03 08:57:59 +02:00
co-authored by Claude Opus 5
parent c860c82912
commit ce05e5f4f6
+153 -11
View File
@@ -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
}
}
}