import SwiftUI struct StatsView: View { let weekPlans: [WeekPlan] let allDishes: [Dish] let allTags: [Tag] let language: AppLanguage @Environment(\.dismiss) private var dismiss var body: some View { NavigationStack { ScrollView { VStack(spacing: 24) { summaryGrid topDishesSection tagBreakdownSection } .padding(.horizontal, 16) .padding(.vertical, 20) } .background(Color.mealMoodBackground.ignoresSafeArea()) .onAppear { AnalyticsService.logStatsViewed() } .navigationTitle("stats_title") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button("tag_selector_done") { dismiss() } } } } } // MARK: - Summary grid private var summaryGrid: some View { VStack(spacing: 12) { HStack(spacing: 12) { StatCard( icon: "flame.fill", iconColor: .mealMoodCoral, value: "\(streak)", label: "stats_streak_label" ) StatCard( icon: "calendar", iconColor: .mealMoodMint, value: "\(weeksWithAnyDish)", label: "stats_weeks_planned_label" ) } HStack(spacing: 12) { StatCard( icon: "checkmark.circle.fill", iconColor: .mealMoodSuccess, value: "\(completeWeeks)", label: "stats_complete_weeks_label" ) StatCard( icon: "chart.bar.fill", iconColor: .mealMoodWarning, value: completionRateText, label: "stats_completion_rate_label" ) } } } // MARK: - Top dishes private var topDishesSection: some View { VStack(alignment: .leading, spacing: 12) { Text("stats_top_dishes_title") .font(.mealMoodH3) .foregroundColor(.mealMoodTextPrimary) if topDishes.isEmpty { Text("stats_no_data") .font(.mealMoodBody) .foregroundColor(.mealMoodTextSecondary) .frame(maxWidth: .infinity, alignment: .center) .padding(.vertical, 16) } else { VStack(spacing: 0) { ForEach(Array(topDishes.enumerated()), id: \.element.dish.id) { index, entry in TopDishRow( rank: index + 1, dishName: entry.dish.name, count: entry.count, maxCount: topDishes.first?.count ?? 1 ) if index < topDishes.count - 1 { Divider().padding(.leading, 44) } } } .background(Color.mealMoodSurface) .clipShape(RoundedRectangle(cornerRadius: 12)) } } } // MARK: - Tag breakdown private var tagBreakdownSection: some View { VStack(alignment: .leading, spacing: 12) { Text("stats_tags_title") .font(.mealMoodH3) .foregroundColor(.mealMoodTextPrimary) if tagUsage.isEmpty { Text("stats_no_data") .font(.mealMoodBody) .foregroundColor(.mealMoodTextSecondary) .frame(maxWidth: .infinity, alignment: .center) .padding(.vertical, 16) } else { VStack(spacing: 10) { ForEach(tagUsage, id: \.tag.id) { entry in TagBarRow(tagName: entry.tag.localizedName(language: language), tagColor: entry.tag.color, count: entry.count, maxCount: tagUsage.first?.count ?? 1) } } .padding(14) .background(Color.mealMoodSurface) .clipShape(RoundedRectangle(cornerRadius: 12)) } } } // MARK: - Computed stats private var plannedWeeks: [WeekPlan] { weekPlans.filter { plan in plan.slotList.contains { $0.dishId != nil } } } private var weeksWithAnyDish: Int { plannedWeeks.count } private var completeWeeks: Int { weekPlans.filter { plan in !plan.slotList.isEmpty && plan.slotList.allSatisfy { $0.dishId != nil || $0.isEatingOut || $0.isSkipped } }.count } private var completionRateText: String { guard weeksWithAnyDish > 0 else { return "—" } let rate = Int(Double(completeWeeks) / Double(weeksWithAnyDish) * 100) return "\(rate)%" } private var streak: Int { let sorted = plannedWeeks .map { $0.weekStartDate.startOfWeek() } .sorted(by: >) guard !sorted.isEmpty else { return 0 } let calendar = Calendar.current var count = 1 var previous = sorted[0] for date in sorted.dropFirst() { let diff = calendar.dateComponents([.day], from: date, to: previous).day ?? 0 if diff <= 7 { count += 1 previous = date } else { break } } return count } private var dishUsage: [UUID: Int] { var counts: [UUID: Int] = [:] for plan in weekPlans { for slot in plan.slotList { if let id = slot.dishId { counts[id, default: 0] += 1 } if let id = slot.secondaryDishId { counts[id, default: 0] += 1 } } } return counts } private struct DishEntry { let dish: Dish; let count: Int } private var topDishes: [DishEntry] { let usage = dishUsage let dishMap = Dictionary(allDishes.map { ($0.id, $0) }, uniquingKeysWith: { first, _ in first }) return usage .compactMap { id, count in dishMap[id].map { DishEntry(dish: $0, count: count) } } .sorted { $0.count > $1.count } .prefix(8) .map { $0 } } private struct TagEntry { let tag: Tag; let count: Int } private var tagUsage: [TagEntry] { let dishMap = Dictionary(allDishes.map { ($0.id, $0) }, uniquingKeysWith: { first, _ in first }) let tagMap = Dictionary(allTags.map { ($0.id, $0) }, uniquingKeysWith: { first, _ in first }) var counts: [UUID: Int] = [:] for plan in weekPlans { for slot in plan.slotList { for dishId in [slot.dishId, slot.secondaryDishId].compactMap({ $0 }) { guard let dish = dishMap[dishId] else { continue } for tagId in dish.tagIds { counts[tagId, default: 0] += 1 } } } } return counts .compactMap { id, count in tagMap[id].map { TagEntry(tag: $0, count: count) } } .sorted { $0.count > $1.count } .prefix(6) .map { $0 } } } // MARK: - Sub-views private struct StatCard: View { let icon: String let iconColor: Color let value: String let label: LocalizedStringKey var body: some View { VStack(spacing: 8) { Image(systemName: icon) .font(.system(size: 24)) .foregroundColor(iconColor) Text(value) .font(.mealMoodH2) .foregroundColor(.mealMoodTextPrimary) Text(label) .font(.mealMoodCaption) .foregroundColor(.mealMoodTextSecondary) .multilineTextAlignment(.center) } .frame(maxWidth: .infinity) .padding(.vertical, 18) .background(Color.mealMoodSurface) .clipShape(RoundedRectangle(cornerRadius: 14)) } } private struct TopDishRow: View { let rank: Int let dishName: String let count: Int let maxCount: Int var body: some View { HStack(spacing: 12) { Text("\(rank)") .font(.mealMoodCaption) .foregroundColor(.mealMoodTextSecondary) .frame(width: 20, alignment: .center) VStack(alignment: .leading, spacing: 4) { Text(dishName) .font(.mealMoodBody) .foregroundColor(.mealMoodTextPrimary) .lineLimit(1) GeometryReader { geo in RoundedRectangle(cornerRadius: 3) .fill(Color.mealMoodCoral.opacity(0.25)) .frame(width: geo.size.width, height: 4) .overlay(alignment: .leading) { RoundedRectangle(cornerRadius: 3) .fill(Color.mealMoodCoral) .frame(width: geo.size.width * CGFloat(count) / CGFloat(maxCount), height: 4) } } .frame(height: 4) } Text("\(count)×") .font(.mealMoodCaption) .foregroundColor(.mealMoodTextSecondary) .frame(width: 28, alignment: .trailing) } .padding(.horizontal, 14) .padding(.vertical, 12) } } private struct TagBarRow: View { let tagName: String let tagColor: String let count: Int let maxCount: Int var body: some View { HStack(spacing: 10) { Circle() .fill(Color(hex: tagColor)) .frame(width: 10, height: 10) Text(tagName) .font(.mealMoodSmall) .foregroundColor(.mealMoodTextPrimary) .frame(width: 90, alignment: .leading) .lineLimit(1) GeometryReader { geo in RoundedRectangle(cornerRadius: 3) .fill(Color(hex: tagColor).opacity(0.2)) .frame(width: geo.size.width, height: 8) .overlay(alignment: .leading) { RoundedRectangle(cornerRadius: 3) .fill(Color(hex: tagColor)) .frame(width: geo.size.width * CGFloat(count) / CGFloat(maxCount), height: 8) } } .frame(height: 8) Text("\(count)") .font(.mealMoodCaption) .foregroundColor(.mealMoodTextSecondary) .frame(width: 28, alignment: .trailing) } } }