import SwiftUI extension StoreDetailView { func heroIconButton(icon: String, action: @escaping () -> Void) -> some View { Button(action: action) { Image(systemName: icon) .font(.system(size: 14, weight: .semibold)) .foregroundStyle(Color.white) .frame(width: 32, height: 32) .background(Color.white.opacity(0.24)) .clipShape(Circle()) } .buttonStyle(.plain) } func statItem(title: String, value: String) -> some View { VStack(spacing: 4) { Text(title) .font(AppTypography.caption) .foregroundStyle(AppColors.textMuted) Text(value) .font(AppTypography.heading3) .foregroundStyle(AppColors.textPrimary) } .frame(maxWidth: .infinity) } var ratingChip: some View { HStack(spacing: 6) { Image(systemName: "star.fill") .font(.caption) .foregroundStyle(Color(hex: "#F5B335")) Text(String(format: "%.1f", storeRating ?? 0)) .font(AppTypography.caption) .foregroundStyle(AppColors.textPrimary) } .padding(.horizontal, 10) .padding(.vertical, 6) .background(AppColors.brandSoft) .clipShape(Capsule()) } var storeSubtitle: String { let category = (storeCategory ?? "").trimmingCharacters(in: .whitespacesAndNewlines) if category.isEmpty { return "Restaurant" } return category } var deliveryValueLabel: String { if let minOrder = info?.minOrder { return formatCurrency(minOrder) } if let storeDistance, storeDistance.isEmpty == false { return storeDistance } return "R$ --" } @MainActor func loadStoreData() async { isLoading = true errorMessage = nil do { async let infoRequest = ApiService().storeInfo(storeId: storeId) async let catalogRequest = ApiService().storeCatalog(storeId: storeId) let (infoResponse, catalogResponse) = try await (infoRequest, catalogRequest) if infoResponse.error { errorMessage = infoResponse.message ?? "Não foi possível carregar a loja." isLoading = false return } if catalogResponse.error { errorMessage = catalogResponse.message ?? "Não foi possível carregar o catálogo." isLoading = false return } info = infoResponse.result categories = catalogResponse.result ?? [] selectedCategoryId = categories.first?.id isLoading = false } catch { isLoading = false if let network = error as? NetworkError { errorMessage = network.errorDescription ?? "Erro ao carregar loja." return } if let service = error as? ApiServiceError { errorMessage = service.errorDescription ?? "Erro ao carregar loja." return } errorMessage = "Erro ao carregar loja." } } func resolvedURL(_ raw: String?) -> String? { guard let raw = raw?.trimmingCharacters(in: .whitespacesAndNewlines), raw.isEmpty == false else { return nil } let lower = raw.lowercased() if lower.hasPrefix("http://") || lower.hasPrefix("https://") { return raw } let base = ApiConfig.baseURL.absoluteString.trimmingCharacters(in: CharacterSet(charactersIn: "/")) let path = raw.hasPrefix("/") ? raw : "/\(raw)" return "\(base)\(path)" } func formatCurrency(_ value: Double?) -> String { guard let value else { return "R$ --" } return String(format: "R$ %.2f", value).replacingOccurrences(of: ".", with: ",") } var topSectionHeight: CGFloat { cardTopInset + summaryCardHeight } func sectionAnchorId(for categoryId: String) -> String { "category-section-\(categoryId)" } func syncCategoryWithScroll() { guard isLoading == false else { return } guard isProgrammaticCategoryScroll == false else { return } guard categoryHeaderOffsets.isEmpty == false else { return } // Section whose header is nearest to the top content area wins. let topThreshold: CGFloat = 180 let sorted = categoryHeaderOffsets.sorted { $0.value < $1.value } if let current = sorted.last(where: { $0.value <= topThreshold })?.key { selectedCategoryId = current return } if let firstVisible = sorted.first?.key { selectedCategoryId = firstVisible } } func quantityInCart(for productId: String) -> Int { appState.cart.items .filter { $0.storeId == storeId && $0.productId == productId } .reduce(0) { $0 + $1.quantity } } func requestAddToCart(_ item: CartItemState) { pendingCartAction = .add if shouldAskForStoreSwitch(for: storeId) { pendingCartItem = item pendingProductSheet = nil showSwitchStoreAlert = true return } applyAddToCart(item) } func requestSetCartItem(_ item: CartItemState) { pendingCartAction = .set if shouldAskForStoreSwitch(for: storeId) && item.quantity > 0 { pendingCartItem = item pendingProductSheet = nil showSwitchStoreAlert = true return } applySetCartItem(item) } func requestOpenProductSheet(_ product: StoreCatalogProduct) { pendingCartAction = .openProductSheet if shouldAskForStoreSwitch(for: storeId) { pendingCartItem = nil pendingProductSheet = product showSwitchStoreAlert = true return } selectedProduct = product } func applyAddToCart(_ item: CartItemState) { if appState.cart.storeId == nil { appState.cart.storeId = storeId appState.cart.storeName = storeName } appState.cart.add(item: item) SnackbarCenter.shared.show(title: "Produto adicionado ao carrinho.", style: .success, icon: "checkmark.seal.fill", duration: 2.0) } func applySetCartItem(_ item: CartItemState) { if item.quantity > 0, appState.cart.storeId == nil { appState.cart.storeId = storeId appState.cart.storeName = storeName } appState.cart.set(item: item) if item.quantity > 0 { SnackbarCenter.shared.show(title: "Carrinho atualizado.", style: .success, icon: "checkmark.seal.fill", duration: 1.8) } else { SnackbarCenter.shared.show(title: "Produto removido do carrinho.", style: .success, icon: "checkmark.seal.fill", duration: 1.8) } } func currentQuantity(forCartItemId itemId: String) -> Int { appState.cart.items.first(where: { $0.id == itemId })?.quantity ?? 0 } func shouldAskForStoreSwitch(for targetStoreId: String) -> Bool { guard appState.cart.items.isEmpty == false else { return false } guard let currentStoreId = currentCartStoreId(), currentStoreId.isEmpty == false else { return false } return currentStoreId != targetStoreId } func currentCartStoreId() -> String? { if let storeId = appState.cart.storeId, storeId.isEmpty == false { return storeId } return appState.cart.items.first?.storeId } } enum CartAction { case add case set case openProductSheet }