iOS: getrennte Lebensmittel/Gegenstaende-Listen + anklickbarer Verlauf
Listen-Tab: zwei halbe Kacheln "Lebensmittel" und "Gegenstaende" (ProductListView mit fixedType, clientseitig ueber isObject gefiltert wie im Web); Neuanlage uebernimmt den Typ. Einzelstuecke bleiben unveraendert (reine Gegenstands-Welt). Verlauf und "Letzte Bewegungen" auf der Startseite: Tippen auf eine Bewegung oeffnet ueber den neuen Lader ProductByIdView das jeweilige Produkt. Der artikel-eigene Verlauf bleibt ohne Link. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -330,8 +330,14 @@ struct MovementsCard: View {
|
||||
} else {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
ForEach(movements.prefix(5)) { movement in
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -169,11 +169,19 @@ struct ListsTabView: View {
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
VStack(spacing: 16) {
|
||||
// Getrennte Produktlisten wie im Web: Lebensmittel und Gegenstände
|
||||
// nebeneinander als halbbreite Kacheln.
|
||||
HStack(spacing: 16) {
|
||||
NavigationLink {
|
||||
ProductListView()
|
||||
ProductListView(fixedType: "food")
|
||||
} label: {
|
||||
ActionTile(title: "Produkte", subtitle: "Suchen, ansehen und bearbeiten",
|
||||
systemImage: "shippingbox")
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user