/ BalanceKit / AddFoodView.swift
AddFoodView.swift
  1  import SwiftUI
  2  import Combine
  3  import UIKit
  4  
  5  struct AddFoodView: View {
  6      @Environment(\.dismiss) private var dismiss
  7      @ObservedObject var dataManager: FoodDataManager
  8      
  9      // MARK: - State Properties
 10      
 11      // Input fields for the base 100g values
 12      @State private var foodName = ""
 13      @State private var baseCalories = ""
 14      @State private var baseProtein = ""
 15      @State private var baseCarbs = ""
 16      @State private var baseFat = ""
 17      
 18      // Quantity for scaling
 19      @State private var quantity: Double = 100
 20      
 21      // General properties
 22      @State private var selectedDate = Date()
 23      @State private var selectedMealType: MealType = .lunch
 24      
 25      // Barcode Scanner related
 26      @State private var isShowingScanner = false
 27      @State private var scannedBarcode: String? = nil
 28      private let foodFactsService = OpenFoodFactsService()
 29      
 30      // Alerts and fetching state
 31      @State private var isFetching = false
 32      @State private var showingAlert = false
 33      @State private var alertTitle = ""
 34      @State private var alertMessage = ""
 35      
 36      // MARK: - Computed Properties for Scaled Values
 37      
 38      private var scaledCalories: Double {
 39          (Double(baseCalories) ?? 0) * (quantity / 100.0)
 40      }
 41      private var scaledProtein: Double {
 42          (Double(baseProtein) ?? 0) * (quantity / 100.0)
 43      }
 44      private var scaledCarbs: Double {
 45          (Double(baseCarbs) ?? 0) * (quantity / 100.0)
 46      }
 47      private var scaledFat: Double {
 48          (Double(baseFat) ?? 0) * (quantity / 100.0)
 49      }
 50      
 51      private var isSaveDisabled: Bool {
 52          foodName.isEmpty || baseCalories.isEmpty
 53      }
 54      
 55      // MARK: - Body
 56      
 57      var body: some View {
 58          NavigationStack {
 59              Form {
 60                  // Section for Base Values (per 100g)
 61                  Section(header: Text("Basiswerte (pro 100g)")) {
 62                      HStack {
 63                          TextField("Name des Lebensmittels", text: $foodName)
 64                          if isFetching {
 65                              ProgressView().padding(.leading, 5)
 66                          }
 67                      }
 68                      Button(action: { isShowingScanner = true }) {
 69                          Label("Mit Barcode-Scanner ausfüllen", systemImage: "barcode.viewfinder")
 70                      }
 71                      
 72                      TextField("Kalorien (kcal)", text: $baseCalories).keyboardType(.decimalPad)
 73                      TextField("Protein (g)", text: $baseProtein).keyboardType(.decimalPad)
 74                      TextField("Kohlenhydrate (g)", text: $baseCarbs).keyboardType(.decimalPad)
 75                      TextField("Fett (g)", text: $baseFat).keyboardType(.decimalPad)
 76                  }
 77                  
 78                  // Section for Quantity
 79                  Section(header: Text("Menge")) {
 80                      Stepper(value: $quantity, in: 0...5000, step: 50) {
 81                          Text("\(quantity, specifier: "%.0f") g")
 82                      }
 83                  }
 84                  
 85                  // Section for Calculated Totals
 86                  Section(header: Text("Gesamtwerte für diese Mahlzeit")) {
 87                      HStack {
 88                          Text("Kalorien")
 89                          Spacer()
 90                          Text("\(scaledCalories, specifier: "%.0f") kcal")
 91                              .foregroundColor(.secondary)
 92                              .animation(.easeInOut(duration: 0.3), value: scaledCalories)
 93                      }
 94                      HStack {
 95                          Text("Protein")
 96                          Spacer()
 97                          Text("\(scaledProtein, specifier: "%.1f") g")
 98                              .foregroundColor(.secondary)
 99                              .animation(.easeInOut(duration: 0.3), value: scaledProtein)
100                      }
101                      HStack {
102                          Text("Kohlenhydrate")
103                          Spacer()
104                          Text("\(scaledCarbs, specifier: "%.1f") g")
105                              .foregroundColor(.secondary)
106                              .animation(.easeInOut(duration: 0.3), value: scaledCarbs)
107                      }
108                      HStack {
109                          Text("Fett")
110                          Spacer()
111                          Text("\(scaledFat, specifier: "%.1f") g")
112                              .foregroundColor(.secondary)
113                              .animation(.easeInOut(duration: 0.3), value: scaledFat)
114                      }
115                  }
116                  
117                  // Section for Date and Meal Type
118                  Section {
119                      DatePicker("Datum", selection: $selectedDate, displayedComponents: .date)
120                      Picker("Mahlzeit", selection: $selectedMealType) {
121                          ForEach(MealType.allCases, id: \.self) { type in
122                              Label(type.rawValue, systemImage: type.icon)
123                          }
124                      }
125                  }
126                  
127                  // Save Button
128                  Section {
129                      Button("Speichern") {
130                          saveFood()
131                      }
132                      .disabled(isSaveDisabled)
133                  }
134              }
135              .navigationTitle("Nahrungsmittel hinzufügen")
136              .navigationBarTitleDisplayMode(.large)
137              .toolbar {
138                  ToolbarItem(placement: .cancellationAction) {
139                      Button("Abbrechen") { dismiss() }
140                  }
141              }
142              .sheet(isPresented: $isShowingScanner) {
143                  BarcodeScannerView(scannedCode: $scannedBarcode)
144              }
145              .onChange(of: scannedBarcode) { oldValue, newBarcode in
146                  if let barcode = newBarcode {
147                      fetchProductDetails(for: barcode)
148                  }
149              }
150              .alert(isPresented: $showingAlert) {
151                  Alert(
152                      title: Text(alertTitle), 
153                      message: Text(alertMessage), 
154                      dismissButton: .default(Text("OK")) {
155                          // Haptic feedback bei Fehlern
156                          let feedback = UINotificationFeedbackGenerator()
157                          feedback.notificationOccurred(.error)
158                      }
159                  )
160              }
161          }
162      }
163      
164      // MARK: - Logic
165      
166      private func fetchProductDetails(for barcode: String) {
167          isFetching = true
168          Task {
169              do {
170                  let product = try await foodFactsService.fetchProduct(for: barcode)
171                  
172                  await MainActor.run {
173                      self.foodName = product.productName ?? "Unbekanntes Produkt"
174                      if let nutriments = product.nutriments {
175                          self.baseCalories = String(format: "%.0f", nutriments.energyKcal100G ?? 0)
176                          self.baseProtein = String(format: "%.1f", nutriments.proteins100G ?? 0)
177                          self.baseCarbs = String(format: "%.1f", nutriments.carbohydrates100G ?? 0)
178                          self.baseFat = String(format: "%.1f", nutriments.fat100G ?? 0)
179                          self.quantity = 100 // Reset to default
180                      } else {
181                          showAlert(title: "Keine Nährwerte", message: "Für dieses Produkt wurden keine Nährwertinformationen gefunden.")
182                      }
183                      isFetching = false
184                  }
185              } catch {
186                  await MainActor.run {
187                      isFetching = false
188                      handleFetchError(error, barcode: barcode)
189                  }
190              }
191          }
192      }
193      
194      private func saveFood() {
195          // Haptic feedback beim Speichern
196          let impact = UIImpactFeedbackGenerator(style: .light)
197          impact.impactOccurred()
198          
199          dataManager.addFoodItem(
200              name: foodName,
201              calories: Int(scaledCalories.rounded()),
202              protein: scaledProtein,
203              carbs: scaledCarbs,
204              fat: scaledFat,
205              date: selectedDate,
206              mealType: selectedMealType
207          )
208          dismiss()
209      }
210      
211      private func handleFetchError(_ error: Error, barcode: String) {
212          switch error {
213          case OpenFoodFactsService.ServiceError.productNotFound:
214              showAlert(title: "Produkt nicht gefunden", message: "Der Barcode \(barcode) wurde in der Datenbank nicht gefunden. Bitte versuche es erneut oder gib die Werte manuell ein.")
215          case OpenFoodFactsService.ServiceError.noNutriments:
216              showAlert(title: "Keine Nährwerte", message: "Für dieses Produkt wurden keine Nährwertinformationen gefunden. Du kannst die Werte manuell eingeben.")
217          default:
218              showAlert(title: "Fehler", message: "Ein unerwarteter Fehler ist aufgetreten. Bitte versuche es erneut oder gib die Werte manuell ein.")
219          }
220      }
221      
222      private func showAlert(title: String, message: String) {
223          self.alertTitle = title
224          self.alertMessage = message
225          self.showingAlert = true
226      }
227  }