import SwiftUI import SwiftData struct FirstDishesStepView: View { @Environment(\.modelContext) private var context @ObservedObject var viewModel: OnboardingViewModel let tags: [Tag] var onFinish: () -> Void @State private var showTagSelector = false @Query(sort: \Tag.sortOrder) private var storedTags: [Tag] @Query private var allSettings: [AppSettings] private var language: AppLanguage { (allSettings.first?.languageEnum ?? .system).resolved() } private var availableTags: [Tag] { var seen = Set() let merged = (tags + storedTags).filter { tag in seen.insert(tag.id).inserted } return merged.sorted(by: { $0.sortOrder < $1.sortOrder }) } private var defaultTagId: UUID? { availableTags.first(where: { $0.name.caseInsensitiveCompare("Verduras") == .orderedSame })?.id ?? availableTags.first(where: { $0.nameEN.caseInsensitiveCompare("Vegetables") == .orderedSame })?.id ?? availableTags.sorted(by: { $0.sortOrder < $1.sortOrder }).first?.id } private let maxVisibleSuggestions = 3 private var addedDishesReversed: [(index: Int, dish: (name: String, tagIds: [UUID]))] { Array(viewModel.addedDishes.enumerated()).reversed().map { ($0.offset, $0.element) } } var body: some View { ScrollView { VStack(spacing: 0) { VStack(spacing: 24) { Text("first_dishes_title") .font(.mealMoodH1) .foregroundColor(.mealMoodTextPrimary) .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, 24) .padding(.top, 12) Text("first_dishes_subtitle") .font(.mealMoodSmall) .foregroundColor(.mealMoodTextSecondary) .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, 24) // New dish form VStack(spacing: 12) { TextField("first_dishes_name_placeholder", text: $viewModel.newDishName) .font(.mealMoodBody) .padding(14) .background(Color.mealMoodSurface) .cornerRadius(12) .overlay( RoundedRectangle(cornerRadius: 12) .stroke(Color(hex: "#F0F0F0"), lineWidth: 1) ) // Selected tags if !viewModel.newDishTags.isEmpty { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 6) { ForEach(availableTags.filter { viewModel.newDishTags.contains($0.id) }) { tag in HStack(spacing: 4) { TagPill(name: tag.localizedName(language: language), color: tag.color) Button { viewModel.newDishTags.remove(tag.id) } label: { Image(systemName: "xmark.circle.fill") .font(.system(size: 14)) .foregroundColor(.mealMoodTextSecondary) } } } } } } Button { ensureDefaultTagsIfNeeded() showTagSelector = true } label: { HStack { Image(systemName: "plus.circle") Text("first_dishes_add_tag") } .font(.mealMoodSmall) .foregroundColor(.mealMoodCoral) } .frame(maxWidth: .infinity, alignment: .leading) if availableTags.isEmpty { Text("first_dishes_no_tags") .font(.mealMoodCaption) .foregroundColor(.mealMoodError) .frame(maxWidth: .infinity, alignment: .leading) } Button { ensureDefaultTagsIfNeeded() viewModel.addDish(defaultTagId: defaultTagId) HapticManager.shared.notification(type: .success) } label: { HStack { Image(systemName: "plus") Text("first_dishes_add_dish") } .font(.mealMoodBodyBold) .foregroundColor(.white) .padding(.vertical, 12) .frame(maxWidth: .infinity) .background(viewModel.canAddDish ? Color.mealMoodCoral : Color.gray.opacity(0.3)) .cornerRadius(12) } .disabled(!viewModel.canAddDish) } .padding(.horizontal, 24) // Added dishes list if !viewModel.addedDishes.isEmpty { VStack(alignment: .leading, spacing: 8) { Text("first_dishes_added") .font(.mealMoodSmall) .foregroundColor(.mealMoodTextSecondary) .padding(.horizontal, 24) ScrollView { LazyVStack(spacing: 8) { ForEach(addedDishesReversed, id: \.index) { item in let index = item.index let dish = item.dish HStack { Image(systemName: "checkmark.circle.fill") .foregroundColor(.mealMoodSuccess) Text(dish.name) .font(.mealMoodBody) .foregroundColor(.mealMoodTextPrimary) // Show tag pills HStack(spacing: 4) { ForEach(availableTags.filter { dish.tagIds.contains($0.id) }.prefix(2)) { tag in TagPill(name: tag.localizedName(language: language), color: tag.color) } } Spacer() Button { viewModel.removeDish(at: index) } label: { Image(systemName: "trash") .font(.system(size: 14)) .foregroundColor(.mealMoodError) } } .padding(12) .background(Color.mealMoodSurface) .cornerRadius(12) } } } .frame(maxHeight: 190) .padding(.horizontal, 24) } } VStack(alignment: .leading, spacing: 10) { Text("first_dishes_suggestions") .font(.mealMoodSmall) .foregroundColor(.mealMoodTextSecondary) .padding(.horizontal, 24) Text("Added \(viewModel.addedDishes.count)/\(viewModel.maxOnboardingDishes)") .font(.mealMoodCaption) .foregroundColor(.mealMoodTextSecondary) .padding(.horizontal, 24) ForEach(visibleSuggestions(), id: \.name) { suggestion in Button { viewModel.addSuggestedDish( name: suggestion.name, preferredTagNames: suggestion.tagHints, availableTags: availableTags ) } label: { HStack { VStack(alignment: .leading, spacing: 2) { Text(suggestion.name) .font(.mealMoodBodyBold) .foregroundColor(.mealMoodTextPrimary) Text("first_dishes_suggestion_add") .font(.mealMoodCaption) .foregroundColor(.mealMoodTextSecondary) } Spacer() Image(systemName: "plus.circle.fill") .foregroundColor(.mealMoodCoral) } .padding(12) .background(Color.mealMoodSurface) .cornerRadius(12) } .disabled(!viewModel.canAddMoreDishes) .padding(.horizontal, 24) } if !viewModel.canAddMoreDishes { Text("first_dishes_limit_reached") .font(.mealMoodCaption) .foregroundColor(.mealMoodTextSecondary) .padding(.horizontal, 24) } } Spacer(minLength: 20) } .frame(maxWidth: .infinity, alignment: .top) PrimaryButton( title: String(localized: "onboarding_finish"), icon: "🎉", action: { UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) HapticManager.shared.notification(type: .success) onFinish() }, isEnabled: viewModel.addedDishes.count >= 2 ) .padding(.horizontal, 24) .padding(.top, 12) .padding(.bottom, 24) } } .sheet(isPresented: $showTagSelector) { TagSelectorSheet( tags: availableTags, selectedTagIds: $viewModel.newDishTags ) } .onAppear { ensureDefaultTagsIfNeeded() } .onChange(of: storedTags.count) { _, newCount in if newCount == 0 { ensureDefaultTagsIfNeeded() } } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) } private func suggestedDishes() -> [(name: String, tagHints: [String])] { if language == .english { return [ (name: "Roast chicken", tagHints: ["Meat"]), (name: "Lentil stew", tagHints: ["Legumes"]), (name: "Veggie pasta", tagHints: ["Pasta/Rice", "Vegetables"]), (name: "Chicken curry", tagHints: ["Meat"]), (name: "Salmon with rice", tagHints: ["Fish", "Pasta/Rice"]), (name: "Turkey meatballs", tagHints: ["Meat"]), (name: "Chickpea salad", tagHints: ["Legumes", "Vegetables"]), (name: "Beef tacos", tagHints: ["Meat"]), (name: "Vegetable soup", tagHints: ["Vegetables"]), (name: "Baked cod", tagHints: ["Fish"]) ] } return [ (name: "Pollo asado", tagHints: ["Carne"]), (name: "Lentejas estofadas", tagHints: ["Legumbres"]), (name: "Pasta con verduras", tagHints: ["Pasta/Arroz", "Verduras"]), (name: "Pollo al curry", tagHints: ["Carne"]), (name: "Salmón con arroz", tagHints: ["Pescado", "Pasta/Arroz"]), (name: "Albóndigas de pavo", tagHints: ["Carne"]), (name: "Ensalada de garbanzos", tagHints: ["Legumbres", "Verduras"]), (name: "Tacos de ternera", tagHints: ["Carne"]), (name: "Crema de verduras", tagHints: ["Verduras"]), (name: "Merluza al horno", tagHints: ["Pescado"]) ] } private func visibleSuggestions() -> [(name: String, tagHints: [String])] { let addedNames = Set( viewModel.addedDishes.map { $0.name.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() } ) let remaining = suggestedDishes().filter { !addedNames.contains($0.name.lowercased()) } return Array(remaining.prefix(maxVisibleSuggestions)) } private func ensureDefaultTagsIfNeeded() { let descriptor = FetchDescriptor() if (try? context.fetch(descriptor))?.isEmpty ?? true { DefaultDataService.createDefaultTags(context: context) } } }