/ BalanceKit / DailyView.swift
DailyView.swift
  1  //
  2  //  DailyView.swift
  3  //  BalanceKit
  4  //
  5  //  Created by Alexander Kunau on 30.10.24.
  6  //
  7  
  8  import SwiftUI
  9  
 10  // Helper View for a single metric in the dashboard
 11  struct DashboardMetricView: View {
 12      let label: String
 13      let value: String
 14      let unit: String
 15      let color: Color
 16      var progress: Double? = nil // Optional progress for circular view
 17  
 18      var body: some View {
 19          VStack(spacing: 4) {
 20              if let progress = progress {
 21                  ZStack {
 22                      Circle()
 23                          .stroke(lineWidth: 10)
 24                          .opacity(0.2)
 25                          .foregroundColor(color)
 26  
 27                      Circle()
 28                          .trim(from: 0.0, to: CGFloat(min(progress, 1.0)))
 29                          .stroke(style: StrokeStyle(lineWidth: 10, lineCap: .round, lineJoin: .round))
 30                          .foregroundColor(color)
 31                          .rotationEffect(Angle(degrees: -90)) // Start from top
 32                          .animation(.easeOut(duration: 0.5), value: progress)
 33  
 34                      VStack {
 35                          Text(value)
 36                              .font(.title)
 37                              .fontWeight(.bold)
 38                          Text(unit)
 39                              .font(.caption)
 40                              .foregroundColor(.secondary)
 41                      }
 42                  }
 43                  .frame(height: 100) // Angepasste Höhe für den Kreis
 44                  
 45                  Text(label)
 46                      .font(.headline)
 47                      .fontWeight(.medium)
 48                      .lineLimit(1)
 49                      .minimumScaleFactor(0.8)
 50                      .padding(.top, 8)
 51  
 52              } else {
 53                  Text(label)
 54                      .font(.headline)
 55                      .fontWeight(.medium)
 56                      .lineLimit(1)
 57                      .minimumScaleFactor(0.8)
 58                      .frame(height: 40) // Fixed height for labels to ensure alignment
 59                  Spacer()
 60                  Text(value)
 61                      .font(.largeTitle)
 62                      .fontWeight(.bold)
 63                      .foregroundColor(color)
 64                  Text(unit)
 65                      .font(.caption)
 66                      .foregroundColor(.secondary)
 67                  Spacer()
 68              }
 69          }
 70          .frame(maxWidth: .infinity, minHeight: 140) // Mindesthöhe für alle Kacheln
 71          .padding()
 72          .background(Color(.secondarySystemGroupedBackground))
 73          .cornerRadius(16)
 74      }
 75  }
 76  
 77  
 78  struct DailyView: View {
 79      @ObservedObject var dataManager: FoodDataManager
 80      @ObservedObject var workoutManager: WorkoutManager
 81      @ObservedObject var userProfile: UserProfile
 82      @State private var selectedDate = Date()
 83      @State private var selectedMealType: MealType? = nil
 84      @State private var showingPresetManager = false
 85      @State private var showingReportView = false
 86      @State private var showingWaterSheet = false
 87      @State private var itemToEdit: FoodItem? = nil
 88      
 89      private let columns = [GridItem(.flexible()), GridItem(.flexible())]
 90      private let dashboardColumns = [GridItem(.flexible()), GridItem(.flexible())]
 91      private let macroColumns = [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())]
 92  
 93      var body: some View {
 94          ScrollView {
 95              VStack(spacing: 0) {
 96                  // Header Section with DatePicker
 97                  DatePicker("Datum", selection: $selectedDate, displayedComponents: .date)
 98                      .datePickerStyle(.compact)
 99                      .padding([.horizontal, .top])
100                      .padding(.bottom, 8) // Added space after the date picker
101                      .background(Color(.systemGroupedBackground))
102                      .onChange(of: selectedDate) {
103                          // Aktualisiere HealthKit-Daten, wenn sich das Datum ändert
104                          if let healthManager = dataManager.healthManager {
105                              // Wenn es das heutige Datum ist, normale Aktualisierung
106                              if Calendar.current.isDateInToday(selectedDate) {
107                                  healthManager.fetchTodaysData()
108                              }
109                              // Ansonsten werden die Daten bei Bedarf durch WorkoutManager abgerufen
110                          }
111                      }
112                  
113                  // HealthKit Aktivitäten anzeigen, wenn verfügbar
114                  if let healthManager = dataManager.healthManager {
115                      HealthActivityView(healthManager: healthManager, date: selectedDate)
116                          .padding(.horizontal)
117                          .padding(.bottom)
118                  }
119  
120                  // Dashboard Grid
121                  LazyVGrid(columns: dashboardColumns, spacing: 8) {
122                      // Main Net Calorie Tile with Progress
123                      DashboardMetricView(
124                          label: "Netto-Kalorien",
125                          value: "\(nettoCaloriesTodayWithHealthKit)",
126                          unit: "Ziel: \(dataManager.dailyCalorieGoal)",
127                          color: calorieProgressWithHealthKit <= 1.0 ? .green : .red,
128                          progress: calorieProgressWithHealthKit,
129                      )
130                      .gridCellColumns(2) // Span across two columns
131                  
132                      // Consumed and Burned Calories
133                      DashboardMetricView(
134                          label: "Aufgenommen",
135                          value: "\(dataManager.totalCaloriesForDay(selectedDate))",
136                          unit: "kcal",
137                          color: .blue
138                      )
139  
140                      DashboardMetricView(
141                          label: "Verbrannt",
142                          value: "\(workoutManager.totalCaloriesBurnedWithHealthKit(date: selectedDate, healthManager: dataManager.healthManager))",
143                          unit: "kcal",
144                          color: .orange
145                      )
146                      
147                      // Wasserkonsum mit + Button zum schnellen Hinzufügen
148                      if let healthManager = dataManager.healthManager {
149                          if Calendar.current.isDateInToday(selectedDate) {
150                              // Für heute direkt den aktuellen Wert verwenden
151                              WaterDashboardView(
152                                  healthManager: healthManager
153                              )
154                          } else {
155                              // Für andere Tage den Wasserwert dynamisch laden
156                              WaterDashboardViewHistorical(
157                                  date: selectedDate,
158                                  healthManager: healthManager
159                              )
160                          }
161                      } else {
162                          DashboardMetricView(
163                              label: "Wasser",
164                              value: "0",
165                              unit: "ml",
166                              color: .blue
167                          )
168                      }
169                  }
170                  .padding(.horizontal)
171                  .padding(.bottom)
172                  .background(Color(.systemGroupedBackground))
173  
174                  // Macro Nutrients Section
175                  VStack(spacing: 0) {
176                      Text("Makronährstoffe")
177                          .font(.title2)
178                          .fontWeight(.bold)
179                          .frame(maxWidth: .infinity, alignment: .leading)
180                          .padding(.vertical)
181  
182                      LazyVGrid(columns: macroColumns, spacing: 8) {
183                          DashboardMetricView(
184                              label: "Protein",
185                              value: String(format: "%.0f", dataManager.totalProteinForDay(selectedDate)),
186                              unit: "von \(String(format: "%.0f", dataManager.dailyProteinGoal))g",
187                              color: macroColor(current: dataManager.totalProteinForDay(selectedDate), goal: dataManager.dailyProteinGoal)
188                          )
189                          .equalsFrame(in: macroColumns)
190                          
191                          DashboardMetricView(
192                              label: "Kohlenh.",
193                              value: String(format: "%.0f", dataManager.totalCarbsForDay(selectedDate)),
194                              unit: "von \(String(format: "%.0f", dataManager.dailyCarbsGoal))g",
195                              color: macroColor(current: dataManager.totalCarbsForDay(selectedDate), goal: dataManager.dailyCarbsGoal)
196                          )
197                          .equalsFrame(in: macroColumns)
198                          
199                          DashboardMetricView(
200                              label: "Fett",
201                              value: String(format: "%.0f", dataManager.totalFatForDay(selectedDate)),
202                              unit: "von \(String(format: "%.0f", dataManager.dailyFatGoal))g",
203                              color: macroColor(current: dataManager.totalFatForDay(selectedDate), goal: dataManager.dailyFatGoal)
204                          )
205                          .equalsFrame(in: macroColumns)
206                      }
207                  }
208                  .padding()
209                  .background(Color(.systemGroupedBackground))
210  
211                  // Food items list section
212                  foodList
213              }
214          }
215          .navigationTitle("Tagesübersicht")
216          .navigationBarTitleDisplayMode(.large)
217          .refreshable {
218              // Einfache Pull-to-Refresh Funktionalität
219              dataManager.objectWillChange.send()
220          }
221          .background(Color(.systemGroupedBackground).edgesIgnoringSafeArea(.all))
222          .sheet(isPresented: $showingPresetManager) {
223              MealPresetView(dataManager: dataManager)
224          }
225          .sheet(isPresented: $showingReportView) {
226              NavigationStack {
227                  ReportView(dataManager: dataManager)
228              }
229          }
230          .sheet(item: $itemToEdit) { item in
231              EditFoodView(dataManager: dataManager, foodItem: item)
232          }
233          .sheet(isPresented: $showingWaterSheet) {
234              if let healthManager = dataManager.healthManager {
235                  AddWaterView(healthManager: healthManager, isPresented: $showingWaterSheet)
236              }
237          }
238      }
239      
240      // Funktion zum Anzeigen des Wasser-Eingabe-Sheets
241      private func showWaterIntakeSheet() {
242          showingWaterSheet = true
243      }
244  
245      // The list of food items, extracted for clarity
246      private var foodList: some View {
247          VStack(alignment: .leading, spacing: 16) {
248              HStack {
249                  Text("Heutige Mahlzeiten")
250                      .font(.title2)
251                      .fontWeight(.bold)
252                  Spacer()
253                  Button("Vorlagen") {
254                      showingPresetManager = true
255                  }
256                  .buttonStyle(.bordered)
257              }
258              .padding(.horizontal)
259  
260              Picker("Mahlzeit filtern", selection: $selectedMealType) {
261                  Text("Alle").tag(nil as MealType?)
262                  ForEach(MealType.allCases, id: \.self) { type in
263                      Text(type.rawValue).tag(type as MealType?)
264                  }
265              }
266              .pickerStyle(.segmented)
267              .padding(.horizontal)
268              
269              if dataManager.foodItemsForDay(selectedDate, mealType: selectedMealType).isEmpty {
270                  emptyStateView
271              } else {
272                  // Mahlzeiten nach Typ gruppieren
273                  if selectedMealType == nil {
274                      ForEach(MealType.allCases, id: \.self) { mealType in
275                          let mealItems = dataManager.foodItemsForDay(selectedDate, mealType: mealType)
276                          if !mealItems.isEmpty {
277                              mealSection(title: mealType.rawValue, icon: mealType.icon, items: mealItems, mealType: mealType)
278                          }
279                      }
280                  } else {
281                      // Zeige nur die ausgewählte Mahlzeitenart
282                      let filteredItems = dataManager.foodItemsForDay(selectedDate, mealType: selectedMealType)
283                      mealSection(title: selectedMealType!.rawValue, icon: selectedMealType!.icon, items: filteredItems, mealType: selectedMealType)
284                  }
285              }
286          }
287          .padding(.top)
288          .padding(.bottom, 8)
289      }
290      
291      // A view for an empty list of food items
292      private var emptyStateView: some View {
293          VStack(spacing: 12) {
294              Image(systemName: "fork.knife.circle")
295                  .font(.system(size: 50))
296                  .foregroundColor(.secondary)
297              Text("Keine Einträge")
298                  .font(.headline)
299              Text("Füge deine erste Mahlzeit für \(formattedDate) hinzu.")
300                  .foregroundColor(.secondary)
301                  .multilineTextAlignment(.center)
302          }
303          .frame(maxWidth: .infinity)
304          .padding()
305          .background(Color(.secondarySystemGroupedBackground))
306          .cornerRadius(16)
307          .padding()
308      }
309  
310      // A reusable view for a meal section
311      private func mealSection(title: String, icon: String, items: [FoodItem], mealType: MealType?) -> some View {
312          VStack(alignment: .leading, spacing: 0) {
313              Label(title, systemImage: icon)
314                  .font(.headline)
315                  .padding([.horizontal, .top])
316                  .padding(.bottom, 8)
317              
318              // Using List instead of VStack for better swipe gesture support
319              List {
320                  ForEach(items) { item in
321                      foodItemRow(item)
322                          .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
323                          .listRowBackground(Color.clear)
324                          .swipeActions(edge: .trailing) {
325                              Button(role: .destructive) {
326                                  if let index = dataManager.foodItems.firstIndex(where: { $0.id == item.id }) {
327                                      dataManager.foodItems.remove(at: index)
328                                  }
329                              } label: {
330                                  Label("Löschen", systemImage: "trash")
331                              }
332                          }
333                  }
334                  .listRowSeparator(.hidden)
335              }
336              .listStyle(PlainListStyle())
337              .environment(\.defaultMinListRowHeight, 0)
338              .background(Color(.secondarySystemGroupedBackground))
339              .cornerRadius(12)
340              .scrollDisabled(true)
341              .frame(height: CGFloat(items.count) * 72)
342              .padding(0)
343          }
344          .padding(.horizontal)
345      }
346      
347      private func foodItemRow(_ item: FoodItem) -> some View {
348          Button(action: {
349              itemToEdit = item
350          }) {
351              VStack(alignment: .leading, spacing: 6) {
352                  HStack {
353                      VStack(alignment: .leading, spacing: 2) {
354                          Text(item.name)
355                              .font(.subheadline)
356                              .fontWeight(.medium)
357                              .foregroundColor(.primary)
358                          Text("\(formattedTime(for: item.date))")
359                              .font(.caption2)
360                              .foregroundColor(.secondary)
361                      }
362                      
363                      Spacer()
364                      
365                      HStack(spacing: 2) {
366                          Text("\(item.calories)")
367                              .font(.headline)
368                              .fontWeight(.semibold)
369                              .foregroundColor(.primary)
370                          Text("kcal")
371                              .font(.caption)
372                              .foregroundColor(.secondary)
373                      }
374                  }
375                  
376                  HStack(spacing: 10) {
377                      Text("P: \(String(format: "%.0f", item.protein))g")
378                          .font(.caption)
379                          .foregroundColor(Color(red: 0xfe/255, green: 0x86/255, blue: 0x61/255))
380                      
381                      Text("K: \(String(format: "%.0f", item.carbs))g")
382                          .font(.caption)
383                          .foregroundColor(Color(red: 0x5D/255, green: 0xB9/255, blue: 0x85/255))
384                      
385                      Text("F: \(String(format: "%.0f", item.fat))g")
386                          .font(.caption)
387                          .foregroundColor(Color(red: 0xFF/255, green: 0xD2/255, blue: 0x5F/255))
388                  }
389              }
390              .padding(.top, 12)
391              .padding(.bottom, 12)
392              .padding(.horizontal, 16)
393              .background(Color(.secondarySystemGroupedBackground))
394          }
395          .buttonStyle(PlainButtonStyle())
396          .contextMenu {
397              Button(action: {
398                  itemToEdit = item
399              }) {
400                  Label("Bearbeiten", systemImage: "pencil")
401              }
402              
403              Button(role: .destructive, action: {
404                  if let index = dataManager.foodItems.firstIndex(where: { $0.id == item.id }) {
405                      dataManager.foodItems.remove(at: index)
406                  }
407              }) {
408                  Label("Löschen", systemImage: "trash")
409              }
410          }
411      }
412      
413      private var foodItems: [FoodItem] {
414          dataManager.foodItemsForDay(selectedDate)
415      }
416      
417      private var formattedDate: String {
418          let formatter = DateFormatter()
419          formatter.dateStyle = .medium
420          return formatter.string(from: selectedDate)
421      }
422      
423      private func formattedTime(for date: Date) -> String {
424          let formatter = DateFormatter()
425          formatter.timeStyle = .short
426          return formatter.string(from: date)
427      }
428      
429      private func deleteFoodItems(at offsets: IndexSet, for mealType: MealType?) {
430          // Wir müssen die Indizes aus der gefilterten Liste auf die Hauptliste mappen
431          let dayItems = dataManager.foodItemsForDay(selectedDate, mealType: mealType)
432          let itemsToDelete = offsets.map { dayItems[$0] }
433          
434          for item in itemsToDelete {
435              if let index = dataManager.foodItems.firstIndex(where: { $0.id == item.id }) {
436                  dataManager.foodItems.remove(at: index)
437              }
438          }
439      }
440      
441      // Berechnung der Netto-Kalorien (Aufnahme - Verbrauch) nur mit manuellen Workouts
442      private var nettoCaloriesToday: Int {
443          let consumed = dataManager.totalCaloriesForDay(selectedDate)
444          let burned = workoutManager.totalCaloriesBurnedForDay(selectedDate)
445          return consumed - burned
446      }
447      
448      // Berechnung der Netto-Kalorien (Aufnahme - Verbrauch) mit HealthKit-Daten
449      private var nettoCaloriesTodayWithHealthKit: Int {
450          let consumed = dataManager.totalCaloriesForDay(selectedDate)
451          let burned = workoutManager.totalCaloriesBurnedWithHealthKit(date: selectedDate, healthManager: dataManager.healthManager)
452          let netto = consumed - burned
453          print("DEBUG DailyView: consumed=\(consumed), burned=\(burned), netto=\(netto)")
454          return netto  // Subtrahiere verbrannte Kalorien (0 gegessen - 300 verbrannt = -300)
455      }
456      
457      // Berechnung des Fortschritts für die Netto-Kalorien ohne HealthKit
458      private var calorieProgress: Double {
459          if dataManager.dailyCalorieGoal <= 0 {
460              return 0.0
461          }
462          return Double(nettoCaloriesToday) / Double(dataManager.dailyCalorieGoal)
463      }
464      
465      // Berechnung des Fortschritts für die Netto-Kalorien mit HealthKit
466      private var calorieProgressWithHealthKit: Double {
467          if dataManager.dailyCalorieGoal <= 0 {
468              return 0.0
469          }
470          return Double(nettoCaloriesTodayWithHealthKit) / Double(dataManager.dailyCalorieGoal)
471      }
472      
473      // Berechnet die Farbe für Makronährstoffe basierend auf Zielerreichung
474      private func macroColor(current: Double, goal: Double) -> Color {
475          guard goal > 0 else { return .gray }
476          
477          let percentage = (current / goal) * 100
478          let difference = abs(percentage - 100)
479          
480          // Grün: ±10% vom Ziel (90-110%)
481          if difference <= 10 {
482              return Color(red: 0x5D/255, green: 0xB9/255, blue: 0x85/255)
483          }
484          // Gelb: ±25% vom Ziel (75-125%)
485          else if difference <= 25 {
486              return Color(red: 0xFF/255, green: 0xD2/255, blue: 0x5F/255)
487          }
488          // Orange/Rot: >25% Abweichung
489          else {
490              return Color(red: 0xfe/255, green: 0x86/255, blue: 0x61/255)
491          }
492      }
493  }
494  
495  #Preview {
496      NavigationStack {
497          DailyView(
498              dataManager: FoodDataManager(),
499              workoutManager: WorkoutManager(),
500              userProfile: UserProfile()
501          )
502      }
503  }
504  
505  // ViewModifier zum Erzwingen gleicher Kachelgrößen in einem Grid
506  extension View {
507      func equalsFrame(in columns: [GridItem]) -> some View {
508          self.frame(maxWidth: .infinity, minHeight: 140)
509      }
510  }