App: die selbst draufgesetzten Posten fehlten in der Einkaufsliste

Die Einkaufsliste besteht aus zwei Haelften: gerechnete Bedarfe (Artikel
und Gruppen unter ihrem Mindestbestand, Bedarfe je Lagerort) und das,
was jemand selbst draufgesetzt hat - eigene Eintraege und die aus einem
Rezept uebernommenen Zutaten.

Die App kannte nur die erste Haelfte. Sie fragte /shopping-list,
/shopping-list/groups und /shopping-list/by-location ab - drei Routen,
die ausschliesslich rechnen. Die gespeicherten Eintraege liegen in
/shopping-list/entries und kommen gebuendelt ueber /shopping-list/all;
danach hat die App nie gefragt. Ergebnis: Man setzt im Web etwas auf die
Liste, nimmt das Telefon mit in den Laden, und es ist nicht da.

Jetzt holt die App die gebuendelte Route - ein Aufruf statt drei - und
zeigt die eigenen Posten in einem eigenen Abschnitt "Selbst
draufgesetzt", mit Herkunft ("fuer Pfannkuchen") und Menge. Antippen
hakt ab: Der Posten verschwindet sofort, geht das Speichern schief,
kommt er zurueck - lieber ein Ruecksprung als ein falsches Haekchen.

Dieselbe Blindstelle hatte die Dashboard-Karte der App; sie meldete
"alle Mindestbestaende sind gedeckt", waehrend Posten auf der Liste
standen. Auch sie nimmt jetzt die gebuendelte Route.

Dazu ein Hinweis im Docstring von GET /shopping-list: Diese Route ist
ein Drittel der Liste, wer "die Liste" will, nimmt /all. Die Aufteilung
hat in zwei Tagen zweimal jemanden glauben lassen, es sei nichts da -
beim zweiten Mal war es die App selbst.

Gegen den laufenden Server geprueft: Mit einem Eintrag "Backpapier"
liefert /shopping-list weiterhin [] und /shopping-list/all den Eintrag
samt art, name, menge_text, source, recipe_name und done - genau die
Felder, die das neue Swift-Modell liest.

Nicht kompiliert: kein Xcode auf dieser Maschine. Geprueft sind
Klammer- und Anfuehrungsbalance, dass die drei alten Routen niemand mehr
aufruft, und dass die benutzten Hilfsfunktionen (makeRequest, jsonBody,
send) wirklich so heissen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-03 07:24:58 +02:00
co-authored by Claude Opus 5
parent 892c52e49a
commit f04d80a330
5 changed files with 148 additions and 7 deletions
+49 -4
View File
@@ -51,14 +51,39 @@ struct ShoppingListView: View {
@State private var items: [ShoppingItem] = []
@State private var groups: [GroupShoppingItem] = []
@State private var byLocation: [LocationNeeds] = []
/// Selbst draufgesetzte Posten - von Hand oder aus einem Rezept uebernommen.
@State private var entries: [ShoppingEntry] = []
@State private var busy = true
@State private var error: String?
/// Abgehakte verschwinden: Die Liste ist zum Einkaufen da, nicht zum
/// Nachlesen. Das Aufraeumen ("Erledigte entfernen") macht die
/// Weboberflaeche.
private var offeneEintraege: [ShoppingEntry] { entries.filter { !$0.done } }
var body: some View {
List {
if let error {
Section { Text(error).foregroundStyle(.red).font(.callout) }
}
if !offeneEintraege.isEmpty {
Section("Selbst draufgesetzt") {
ForEach(offeneEintraege) { eintrag in
Button {
Task { await hake(eintrag) }
} label: {
ListRow(
systemImage: eintrag.istNotiz ? "square.and.pencil" : "shippingbox",
title: eintrag.name,
subtitle: eintrag.untertitel
) {
if eintrag.istGruppe { GroupBadge() }
}
}
.buttonStyle(.plain)
}
}
}
if !items.isEmpty {
Section("Artikel unter Mindestbestand") {
ForEach(items) { item in
@@ -103,7 +128,8 @@ struct ShoppingListView: View {
}
}
}
if items.isEmpty && groups.isEmpty && byLocation.isEmpty && !busy {
if items.isEmpty && groups.isEmpty && byLocation.isEmpty
&& offeneEintraege.isEmpty && !busy {
Text("Nichts einzukaufen – alle Mindestbestände sind gedeckt.")
.foregroundStyle(.secondary)
}
@@ -114,13 +140,32 @@ struct ShoppingListView: View {
.task { await load() }
}
/// Abhaken. Der Posten verschwindet sofort; geht das Speichern schief,
/// kommt er zurueck - lieber ein Ruecksprung als ein falsches Haekchen.
private func hake(_ eintrag: ShoppingEntry) async {
let vorher = entries
entries.removeAll { $0.id == eintrag.id }
do {
try await APIClient.shared.setShoppingEntryDone(eintrag.id, done: true)
} catch {
entries = vorher
self.error = error.localizedDescription
}
}
private func load() async {
busy = true
defer { busy = false }
do {
items = try await APIClient.shared.shoppingList()
groups = try await APIClient.shared.shoppingGroups()
byLocation = try await APIClient.shared.shoppingByLocation()
// EIN Aufruf statt drei - und der einzige, der die selbst
// draufgesetzten Posten mitbringt. Die drei Einzelrouten liefern
// nur die gerechneten Bedarfe; wer nur sie abfragt, sieht die
// eigenen Eintraege ueberhaupt nicht.
let alles = try await APIClient.shared.shoppingAll()
items = alles.products
groups = alles.groups
byLocation = alles.byLocation
entries = alles.entries
error = nil
} catch {
self.error = error.localizedDescription