/ BalanceKit / ReportView.swift
ReportView.swift
1 // 2 // ReportView.swift 3 // BalanceKit 4 // 5 // Created by Alexander Kunau on 12.07.25. 6 // 7 8 import SwiftUI 9 import Charts 10 11 struct ReportView: View { 12 @ObservedObject var dataManager: FoodDataManager 13 @State private var selectedPeriod: TimePeriod = .week 14 15 enum TimePeriod: String, CaseIterable { 16 case week = "Woche" 17 case month = "Monat" 18 case quarter = "90 Tage" 19 } 20 21 var body: some View { 22 ScrollView { 23 VStack(spacing: 20) { 24 // Übersicht Metriken 25 overviewSection 26 27 // Kalorien-Verlauf 28 caloriesChartSection 29 30 // Makronährstoffe-Verlauf 31 macrosChartSection 32 33 // Durchschnittswerte 34 averagesSection 35 } 36 .padding() 37 } 38 .navigationTitle("Berichte") 39 .navigationBarTitleDisplayMode(.inline) 40 } 41 42 // MARK: - Übersicht Section 43 private var overviewSection: some View { 44 VStack(spacing: 16) { 45 Text("Übersicht") 46 .font(.headline) 47 .frame(maxWidth: .infinity, alignment: .leading) 48 49 HStack(spacing: 16) { 50 // Durchschnitt Kalorien 51 VStack(spacing: 8) { 52 Text("\(Int(averageCalories))") 53 .font(.system(size: 32, weight: .bold)) 54 .foregroundColor(.green) 55 Text("Ø Kalorien/Tag") 56 .font(.caption) 57 .foregroundColor(.secondary) 58 .multilineTextAlignment(.center) 59 } 60 .frame(maxWidth: .infinity) 61 .padding() 62 .background(Color(.secondarySystemGroupedBackground)) 63 .cornerRadius(12) 64 65 // Gesamt Kalorien 66 VStack(spacing: 8) { 67 Text("\(totalCalories)") 68 .font(.system(size: 32, weight: .bold)) 69 .foregroundColor(.blue) 70 Text("Gesamt") 71 .font(.caption) 72 .foregroundColor(.secondary) 73 } 74 .frame(maxWidth: .infinity) 75 .padding() 76 .background(Color(.secondarySystemGroupedBackground)) 77 .cornerRadius(12) 78 } 79 } 80 } 81 82 // MARK: - Kalorien Chart Section 83 private var caloriesChartSection: some View { 84 VStack(spacing: 12) { 85 HStack { 86 Text("Kalorien-Verlauf") 87 .font(.headline) 88 Spacer() 89 Picker("Zeitraum", selection: $selectedPeriod) { 90 ForEach(TimePeriod.allCases, id: \.self) { period in 91 Text(period.rawValue).tag(period) 92 } 93 } 94 .pickerStyle(.menu) 95 } 96 97 Chart { 98 ForEach(caloriesData, id: \.date) { data in 99 BarMark( 100 x: .value("Datum", data.date), 101 y: .value("Kalorien", data.calories) 102 ) 103 .foregroundStyle(.green) 104 } 105 } 106 .frame(height: 200) 107 .chartXAxis { 108 AxisMarks(values: .automatic(desiredCount: selectedPeriod == .week ? 7 : selectedPeriod == .month ? 6 : 9)) 109 } 110 .padding() 111 .background(Color(.secondarySystemGroupedBackground)) 112 .cornerRadius(16) 113 } 114 } 115 116 // MARK: - Makros Chart Section 117 private var macrosChartSection: some View { 118 VStack(spacing: 12) { 119 Text("Makronährstoffe") 120 .font(.headline) 121 .frame(maxWidth: .infinity, alignment: .leading) 122 123 Chart { 124 ForEach(macrosData, id: \.date) { data in 125 LineMark( 126 x: .value("Datum", data.date), 127 y: .value("Protein", data.protein) 128 ) 129 .foregroundStyle(Color(red: 0xfe/255, green: 0x86/255, blue: 0x61/255)) 130 .interpolationMethod(.catmullRom) 131 132 LineMark( 133 x: .value("Datum", data.date), 134 y: .value("Kohlenhydrate", data.carbs) 135 ) 136 .foregroundStyle(Color(red: 0x5D/255, green: 0xB9/255, blue: 0x85/255)) 137 .interpolationMethod(.catmullRom) 138 139 LineMark( 140 x: .value("Datum", data.date), 141 y: .value("Fette", data.fat) 142 ) 143 .foregroundStyle(Color(red: 0xFF/255, green: 0xD2/255, blue: 0x5F/255)) 144 .interpolationMethod(.catmullRom) 145 } 146 } 147 .frame(height: 200) 148 .chartXAxis { 149 AxisMarks(values: .automatic(desiredCount: selectedPeriod == .week ? 7 : selectedPeriod == .month ? 6 : 9)) 150 } 151 .chartForegroundStyleScale([ 152 "Protein": Color(red: 0xfe/255, green: 0x86/255, blue: 0x61/255), 153 "Kohlenhydrate": Color(red: 0x5D/255, green: 0xB9/255, blue: 0x85/255), 154 "Fette": Color(red: 0xFF/255, green: 0xD2/255, blue: 0x5F/255) 155 ]) 156 .padding() 157 .background(Color(.secondarySystemGroupedBackground)) 158 .cornerRadius(16) 159 } 160 } 161 162 // MARK: - Durchschnittswerte Section 163 private var averagesSection: some View { 164 VStack(spacing: 12) { 165 Text("Durchschnittswerte") 166 .font(.headline) 167 .frame(maxWidth: .infinity, alignment: .leading) 168 169 VStack(spacing: 8) { 170 HStack { 171 HStack(spacing: 4) { 172 Circle() 173 .fill(Color(red: 0xfe/255, green: 0x86/255, blue: 0x61/255)) 174 .frame(width: 8, height: 8) 175 Text("Protein") 176 .font(.subheadline) 177 } 178 Spacer() 179 Text("\(String(format: "%.1f", averageProtein))g/Tag") 180 .font(.subheadline) 181 .fontWeight(.medium) 182 } 183 184 HStack { 185 HStack(spacing: 4) { 186 Circle() 187 .fill(Color(red: 0x5D/255, green: 0xB9/255, blue: 0x85/255)) 188 .frame(width: 8, height: 8) 189 Text("Kohlenhydrate") 190 .font(.subheadline) 191 } 192 Spacer() 193 Text("\(String(format: "%.1f", averageCarbs))g/Tag") 194 .font(.subheadline) 195 .fontWeight(.medium) 196 } 197 198 HStack { 199 HStack(spacing: 4) { 200 Circle() 201 .fill(Color(red: 0xFF/255, green: 0xD2/255, blue: 0x5F/255)) 202 .frame(width: 8, height: 8) 203 Text("Fette") 204 .font(.subheadline) 205 } 206 Spacer() 207 Text("\(String(format: "%.1f", averageFat))g/Tag") 208 .font(.subheadline) 209 .fontWeight(.medium) 210 } 211 } 212 .padding() 213 .background(Color(.secondarySystemGroupedBackground)) 214 .cornerRadius(16) 215 } 216 } 217 218 // MARK: - Berechnungen 219 220 private var periods: Int { 221 switch selectedPeriod { 222 case .week: 223 return 7 224 case .month: 225 return 30 226 case .quarter: 227 return 90 228 } 229 } 230 231 private var caloriesData: [(date: Date, calories: Int)] { 232 let calendar = Calendar.current 233 let now = Date() 234 var data: [(date: Date, calories: Int)] = [] 235 236 for i in (0..<periods).reversed() { 237 guard let date = calendar.date(byAdding: .day, value: -i, to: now) else { continue } 238 let calories = dataManager.totalCaloriesForDay(date) 239 data.append((date: date, calories: calories)) 240 } 241 242 return data 243 } 244 245 private var macrosData: [(date: Date, protein: Double, carbs: Double, fat: Double)] { 246 let calendar = Calendar.current 247 let now = Date() 248 var data: [(date: Date, protein: Double, carbs: Double, fat: Double)] = [] 249 250 for i in (0..<periods).reversed() { 251 guard let date = calendar.date(byAdding: .day, value: -i, to: now) else { continue } 252 let protein = dataManager.totalProteinForDay(date) 253 let carbs = dataManager.totalCarbsForDay(date) 254 let fat = dataManager.totalFatForDay(date) 255 data.append((date: date, protein: protein, carbs: carbs, fat: fat)) 256 } 257 258 return data 259 } 260 261 private var totalCalories: Int { 262 caloriesData.reduce(0) { $0 + $1.calories } 263 } 264 265 private var averageCalories: Double { 266 let total = Double(totalCalories) 267 return total / Double(periods) 268 } 269 270 private var averageProtein: Double { 271 let total = macrosData.reduce(0.0) { $0 + $1.protein } 272 return total / Double(periods) 273 } 274 275 private var averageCarbs: Double { 276 let total = macrosData.reduce(0.0) { $0 + $1.carbs } 277 return total / Double(periods) 278 } 279 280 private var averageFat: Double { 281 let total = macrosData.reduce(0.0) { $0 + $1.fat } 282 return total / Double(periods) 283 } 284 } 285 286 #Preview { 287 NavigationStack { 288 ReportView(dataManager: FoodDataManager()) 289 } 290 }