diff --git a/ios/Sources/DashboardCards.swift b/ios/Sources/DashboardCards.swift index f46cb94..1e0925e 100644 --- a/ios/Sources/DashboardCards.swift +++ b/ios/Sources/DashboardCards.swift @@ -330,7 +330,13 @@ struct MovementsCard: View { } else { VStack(alignment: .leading, spacing: 8) { ForEach(movements.prefix(5)) { movement in - MovementRow(movement: movement) + // Tippen auf eine Bewegung öffnet das jeweilige Produkt. + NavigationLink { + ProductByIdView(productId: movement.productId) + } label: { + MovementRow(movement: movement) + } + .buttonStyle(.plain) } NavigationLink("Ganzer Verlauf") { HistoryView() }.font(.caption) } diff --git a/ios/Sources/HistoryView.swift b/ios/Sources/HistoryView.swift index e21d55a..e48de36 100644 --- a/ios/Sources/HistoryView.swift +++ b/ios/Sources/HistoryView.swift @@ -23,7 +23,17 @@ struct HistoryView: View { ForEach(tage, id: \.key) { tag in Section(display.formatDay(tag.eintraege[0].createdAt)) { ForEach(tag.eintraege) { movement in - MovementRow(movement: movement, showProduct: productId == nil) + // Im Gesamtverlauf führt jede Zeile zum Produkt; im + // Artikel-eigenen Verlauf ist man schon dort (kein Link). + if productId == nil { + NavigationLink { + ProductByIdView(productId: movement.productId) + } label: { + MovementRow(movement: movement, showProduct: true) + } + } else { + MovementRow(movement: movement, showProduct: false) + } } } } diff --git a/ios/Sources/ListViews.swift b/ios/Sources/ListViews.swift index 179903c..675b9b3 100644 --- a/ios/Sources/ListViews.swift +++ b/ios/Sources/ListViews.swift @@ -191,6 +191,10 @@ struct ExpiringView: View { // MARK: - Produktliste struct ProductListView: View { + /// nil = alle, "food" = nur Lebensmittel, "object" = nur Gegenstände. + /// Getrennte Listen wie im Web (dort über `fixedType`/`p.tracking`). + var fixedType: String? = nil + @EnvironmentObject private var session: Session @State private var query = "" @State private var products: [Product] = [] @@ -200,6 +204,20 @@ struct ProductListView: View { @State private var busy = false @State private var showNew = false + /// Auf den festen Typ gefiltert (clientseitig, wie im Web). + private var sichtbar: [Product] { + guard let fixedType else { return products } + return products.filter { ($0.isObject ? "object" : "food") == fixedType } + } + + private var titel: String { + switch fixedType { + case "food": return "Lebensmittel" + case "object": return "Gegenstände" + default: return "Produkte" + } + } + private var filterLabel: String { if categoryId == 0 { return "Ohne Kategorie" } if let id = categoryId, let treffer = categories.first(where: { $0.id == id }) { @@ -228,21 +246,21 @@ struct ProductListView: View { } Section { - ForEach(products) { product in + ForEach(sichtbar) { product in NavigationLink { ProductDetailView(product: product) } label: { ProductRow(product: product) } } - if products.isEmpty && !busy { + if sichtbar.isEmpty && !busy { Text("Keine Treffer").foregroundStyle(.secondary) } } } .searchable(text: $query, prompt: "Artikel suchen") .onChange(of: query) { _ in Task { await load() } } - .navigationTitle("Produkte") + .navigationTitle(titel) .navigationBarTitleDisplayMode(.inline) .refreshable { await load() } .toolbar { @@ -256,9 +274,10 @@ struct ProductListView: View { } .sheet(isPresented: $showNew) { NavigationStack { - // Ohne Barcode: nur Name + Kategorie nötig. Nach dem Anlegen - // schließt sich das Formular und die Liste wird aufgefrischt. - ProductFormView(prefillBarcode: nil, groupId: nil) { _ in + // Ohne Barcode: nur Name + Kategorie nötig. Typ aus der Liste + // vorbelegen. Nach dem Anlegen schließt sich das Formular und die + // Liste wird aufgefrischt. + ProductFormView(prefillBarcode: nil, groupId: nil, initialType: fixedType) { _ in showNew = false Task { await load() } } diff --git a/ios/Sources/ProductDetailView.swift b/ios/Sources/ProductDetailView.swift index fcdc552..b3d0b1b 100644 --- a/ios/Sources/ProductDetailView.swift +++ b/ios/Sources/ProductDetailView.swift @@ -395,6 +395,42 @@ struct ProductDetailView: View { } } +/// Lädt ein Produkt anhand seiner ID nach und zeigt dann die Detailansicht – +/// z.B. beim Antippen einer Bewegung im Verlauf oder auf der Startseite. +struct ProductByIdView: View { + let productId: Int + + @State private var product: Product? + @State private var error: String? + + var body: some View { + Group { + if let product { + ProductDetailView(product: product) + } else if let error { + VStack(spacing: 12) { + Text(error).foregroundStyle(.red).font(.callout) + .multilineTextAlignment(.center) + Button("Erneut versuchen") { Task { await load() } } + } + .padding() + } else { + ProgressView() + } + } + .task { if product == nil { await load() } } + } + + private func load() async { + do { + product = try await APIClient.shared.product(id: productId) + error = nil + } catch { + self.error = error.localizedDescription + } + } +} + /// Menge und MHD einer Charge korrigieren. struct LotEditView: View { let lot: Lot diff --git a/ios/Sources/ProductViews.swift b/ios/Sources/ProductViews.swift index 4ea3e72..cc2aa3c 100644 --- a/ios/Sources/ProductViews.swift +++ b/ios/Sources/ProductViews.swift @@ -56,6 +56,9 @@ struct ProductFormView: View { /// Aus der Open-Food-Facts-Einordnung vorgeschlagene Kategorie. var categoryId: Int? var suggestion: LookupResult.Suggestion? + /// Vorbelegter Typ ("food"/"object"), z.B. aus der Lebensmittel- oder + /// Gegenstände-Liste heraus. Ein Kategorie-Vorschlag hat weiter Vorrang. + var initialType: String? var onCreated: (Product) -> Void @Environment(\.dismiss) private var dismiss @@ -203,6 +206,7 @@ struct ProductFormView: View { } private func prefill() { + if let initialType { modus = initialType } barcode = suggestion?.barcode ?? prefillBarcode ?? "" // Aus dem gescannten Code vorgeschlagene Gruppe und Kategorie // uebernehmen. Beides bleibt aenderbar - gespeichert wird erst mit diff --git a/ios/Sources/RootView.swift b/ios/Sources/RootView.swift index 3c1a458..a5ad680 100644 --- a/ios/Sources/RootView.swift +++ b/ios/Sources/RootView.swift @@ -169,11 +169,19 @@ struct ListsTabView: View { var body: some View { NavigationStack { VStack(spacing: 16) { - NavigationLink { - ProductListView() - } label: { - ActionTile(title: "Produkte", subtitle: "Suchen, ansehen und bearbeiten", - systemImage: "shippingbox") + // Getrennte Produktlisten wie im Web: Lebensmittel und Gegenstände + // nebeneinander als halbbreite Kacheln. + HStack(spacing: 16) { + NavigationLink { + ProductListView(fixedType: "food") + } label: { + CompactTile(title: "Lebensmittel", systemImage: "fork.knife") + } + NavigationLink { + ProductListView(fixedType: "object") + } label: { + CompactTile(title: "Gegenstände", systemImage: "shippingbox") + } } NavigationLink { ItemListView() @@ -207,6 +215,29 @@ struct ListsTabView: View { } } +/// Kompakte, halbbreite Kachel (Symbol oben, Titel darunter) für zwei +/// nebeneinanderstehende Einträge – z.B. Lebensmittel/Gegenstände. +struct CompactTile: View { + let title: String + let systemImage: String + + var body: some View { + VStack(spacing: 8) { + Image(systemName: systemImage) + .font(.title2) + .frame(width: 44, height: 44) + .background(Color.accentColor.opacity(0.15)) + .clipShape(RoundedRectangle(cornerRadius: 10)) + Text(title).font(.headline) + } + .frame(maxWidth: .infinity) + .padding() + .background(Color(.secondarySystemBackground)) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .foregroundStyle(.primary) + } +} + struct ActionTile: View { let title: String let subtitle: String