import Foundation import SwiftUI extension PizzaProductDetailSheet { // MARK: - Steps var stepSizes: some View { accordionSection( step: 0, label: "Tamanho", summary: selectedSize.map { "\($0.name ?? "") • Até \(max(1, $0.maxFlavors ?? 1)) sabor(es)" } ) { ForEach(sizes) { size in radioRow( title: size.name ?? "Tamanho", subtitle: "Até \(max(1, size.maxFlavors ?? 1)) sabor(es)", isSelected: selectedSizeId == size.id ) { selectedSizeId = size.id applyAutoSelections() withAnimation(.easeInOut(duration: 0.2)) { expandedStep = nextStep(after: 0) } } } } } var stepDoughs: some View { accordionSection( step: 1, label: "Massa", summary: doughs.count <= 1 ? (doughs.first?.name ?? "Tradicional") : doughs.first(where: { $0.id == selectedDoughId })?.name ) { if doughs.count <= 1 { Text(doughs.first?.name ?? "Massa tradicional") .font(AppTypography.body) .foregroundStyle(AppColors.textPrimary) } else { ForEach(doughs) { dough in radioRow( title: dough.name ?? "Massa", subtitle: nil, isSelected: selectedDoughId == dough.id ) { selectedDoughId = dough.id applyAutoSelections() withAnimation(.easeInOut(duration: 0.2)) { expandedStep = nextStep(after: 1) } } } } } } var stepCrusts: some View { accordionSection( step: 2, label: "Borda", summary: crusts.count <= 1 ? crustDescription(crusts.first) : crusts.first(where: { $0.id == selectedCrustId }).map { crustDescription($0) } ) { if crusts.count <= 1 { Text(crustDescription(crusts.first)) .font(AppTypography.body) .foregroundStyle(AppColors.textPrimary) } else { ForEach(crusts) { crust in radioRow( title: crust.name ?? "Borda", subtitle: (crust.priceModifier ?? 0) > 0 ? "+ \(formatCurrency(crust.priceModifier ?? 0))" : nil, isSelected: selectedCrustId == crust.id ) { selectedCrustId = crust.id withAnimation(.easeInOut(duration: 0.2)) { expandedStep = 3 } } } } } } var stepFlavors: some View { accordionSection( step: 3, label: "Sabores (\(selectedFlavorIds.count)/\(maxFlavorsAllowed))", summary: selectedFlavorIds.isEmpty ? nil : selectedFlavorProducts.map(\.name).joined(separator: ", ") ) { Text("Toque no sabor para escolher adicionais.") .font(.caption) .foregroundStyle(AppColors.textMuted) ForEach(flavors) { flavor in let isSelected = selectedFlavorIds.contains(flavor.id) let price = selectedSizeId.flatMap { flavor.pizzaPrices[$0] } ?? flavor.price let maxReached = selectedFlavorIds.count >= maxFlavorsAllowed HStack(spacing: 10) { AsyncStoreImage(imageURL: resolveImageURL(flavor.image)) .frame(width: 52, height: 52) .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous)) VStack(alignment: .leading, spacing: 4) { Text(flavor.name) .font(AppTypography.body) .foregroundStyle(AppColors.textPrimary) if let price { Text(formatCurrency(price)) .font(.caption) .foregroundStyle(AppColors.textMuted) } } Spacer() Toggle("", isOn: Binding( get: { isSelected }, set: { value in if value { addFlavor(flavor.id) } else { removeFlavor(flavor.id) } } )) .labelsHidden() .disabled(!isSelected && maxReached) } .padding(.vertical, 2) .appContentShape(Rectangle()) .onTapGesture { guard isSelected else { return } guard flavor.addonGroups.contains(where: { $0.items.isEmpty == false }) else { return } selectedFlavorForAddons = flavor } } } } // MARK: - Accordion container func accordionSection( step: Int, label: String, summary: String?, @ViewBuilder content: () -> some View ) -> some View { let isExpanded = expandedStep == step let isDone = summary != nil return VStack(spacing: 0) { Button { withAnimation(.easeInOut(duration: 0.2)) { expandedStep = isExpanded ? -1 : step } } label: { HStack(spacing: 10) { ZStack { Circle() .fill(isDone || isExpanded ? AppColors.primary : AppColors.textMuted.opacity(0.25)) .frame(width: 26, height: 26) if isDone && !isExpanded { Image(systemName: "checkmark") .font(.system(size: 11, weight: .bold)) .foregroundStyle(.white) } else { Text("\(step + 1)") .font(.system(size: 12, weight: .bold)) .foregroundStyle(.white) } } Text(label) .font(AppTypography.heading3) .foregroundStyle(AppColors.textPrimary) Spacer() if let summary, !isExpanded { Text(summary) .font(AppTypography.caption) .foregroundStyle(AppColors.textMuted) .lineLimit(1) .truncationMode(.tail) .frame(maxWidth: 140, alignment: .trailing) } Image(systemName: isExpanded ? "chevron.up" : "chevron.down") .font(.system(size: 12, weight: .semibold)) .foregroundStyle(AppColors.textMuted) } .padding(14) } .buttonStyle(.plain) if isExpanded { Divider().padding(.horizontal, 14) VStack(alignment: .leading, spacing: 10) { content() } .padding(14) .transition(.opacity.combined(with: .move(edge: .top))) } } .background(AppColors.surface) .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous)) } // MARK: - Helpers func nextStep(after step: Int) -> Int { if step == 0 { if doughs.count > 1 { return 1 } if crusts.count > 1 { return 2 } return 3 } if step == 1 { if crusts.count > 1 { return 2 } return 3 } return 3 } func radioRow(title: String, subtitle: String?, isSelected: Bool, action: @escaping () -> Void) -> some View { Button(action: action) { HStack(spacing: 10) { ZStack { Circle() .stroke(isSelected ? AppColors.primary : AppColors.textMuted.opacity(0.4), lineWidth: 2) .frame(width: 20, height: 20) if isSelected { Circle() .fill(AppColors.primary) .frame(width: 10, height: 10) } } VStack(alignment: .leading, spacing: 2) { Text(title) .font(AppTypography.body) .foregroundStyle(AppColors.textPrimary) if let subtitle, subtitle.isEmpty == false { Text(subtitle) .font(.caption) .foregroundStyle(AppColors.textMuted) } } Spacer() } .appContentShape(Rectangle()) } .buttonStyle(.plain) } func crustDescription(_ crust: StorePizzaCrust?) -> String { guard let crust else { return "Sem borda especial" } let name = crust.name ?? "Borda" let modifier = crust.priceModifier ?? 0 return modifier > 0 ? "\(name) (+ \(formatCurrency(modifier)))" : name } func applyAutoSelections() { if selectedSizeId != nil { if doughs.count == 1 { selectedDoughId = doughs.first?.id } else if doughs.isEmpty { selectedDoughId = "__none__" } } if isDoughReady { if crusts.count == 1 { selectedCrustId = crusts.first?.id } else if crusts.isEmpty { selectedCrustId = "__none__" } } } func trimFlavorSelectionByLimit() { let limit = maxFlavorsAllowed guard selectedFlavorIds.count > limit else { return } selectedFlavorIds = Set(selectedFlavorIds.sorted().prefix(limit)) } func addFlavor(_ flavorId: String) { guard !selectedFlavorIds.contains(flavorId), selectedFlavorIds.count < maxFlavorsAllowed else { return } selectedFlavorIds.insert(flavorId) } func removeFlavor(_ flavorId: String) { selectedFlavorIds.remove(flavorId) flavorAddonQuantities.removeValue(forKey: flavorId) } func formatCurrency(_ value: Double) -> String { String(format: "R$ %.2f", value).replacingOccurrences(of: ".", with: ",") } }