This commit is contained in:
Daniel Arantes Loverde
2026-02-15 08:18:06 -03:00
parent d7b8b1864a
commit 0f5ad4c268
35 changed files with 4090 additions and 309 deletions

View File

@@ -56,11 +56,54 @@ extension StoreDetailView {
return "R$ --"
}
var isStoreOpen: Bool {
info?.isOpen ?? true
}
var summaryCardHeight: CGFloat {
summaryCardBaseHeight + (isStoreOpen ? 0 : closedBannerHeight)
}
var closedStoreBannerText: String {
let label = info?.statusLabel?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if label.isEmpty {
return "Loja fechada • Consulte o horário de abertura"
}
let normalized = label.lowercased()
if normalized.hasPrefix("fechado") {
let cleaned = label.replacingOccurrences(of: "Fechado", with: "")
.replacingOccurrences(of: "fechado", with: "")
.trimmingCharacters(in: CharacterSet(charactersIn: " -:•"))
if cleaned.isEmpty == false {
return "Loja fechada • \(cleaned)"
}
}
return "Loja fechada • \(label)"
}
@MainActor
func loadStoreData() async {
func loadStoreData(forceRefresh: Bool = false) async {
isLoading = true
errorMessage = nil
let infoCacheKey = "store-info:\(storeId)"
let catalogCacheKey = "store-catalog:\(storeId)"
if forceRefresh == false,
let cachedInfo: StoreInfoResult = AppContentCache.shared.value(for: infoCacheKey, as: StoreInfoResult.self),
let cachedCatalog: [StoreCatalogCategory] = AppContentCache.shared.value(for: catalogCacheKey, as: [StoreCatalogCategory].self) {
info = cachedInfo
categories = cachedCatalog
selectedCategoryId = cachedCatalog.first?.id
isLoading = false
return
}
if forceRefresh {
AppContentCache.shared.invalidate(prefix: "store-info:\(storeId)")
AppContentCache.shared.invalidate(prefix: "store-catalog:\(storeId)")
}
do {
async let infoRequest = ApiService().storeInfo(storeId: storeId)
async let catalogRequest = ApiService().storeCatalog(storeId: storeId)
@@ -80,6 +123,10 @@ extension StoreDetailView {
info = infoResponse.result
categories = catalogResponse.result ?? []
selectedCategoryId = categories.first?.id
if let info = infoResponse.result {
AppContentCache.shared.set(info, for: infoCacheKey, ttl: 300)
}
AppContentCache.shared.set(categories, for: catalogCacheKey, ttl: 300)
isLoading = false
} catch {
isLoading = false
@@ -96,15 +143,16 @@ extension StoreDetailView {
}
func resolvedURL(_ raw: String?) -> String? {
guard let raw = raw?.trimmingCharacters(in: .whitespacesAndNewlines),
raw.isEmpty == false else { return nil }
guard var normalized = raw?.trimmingCharacters(in: .whitespacesAndNewlines),
normalized.isEmpty == false else { return nil }
let lower = raw.lowercased()
if lower.hasPrefix("http://") || lower.hasPrefix("https://") {
return raw
normalized = normalized.replacingOccurrences(of: "\\/", with: "/")
let lower = normalized.lowercased()
if lower.hasPrefix("http://") || lower.hasPrefix("https://") || lower.hasPrefix("data:image") {
return normalized
}
let base = ApiConfig.baseURL.absoluteString.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
let path = raw.hasPrefix("/") ? raw : "/\(raw)"
let path = normalized.hasPrefix("/") ? normalized : "/\(normalized)"
return "\(base)\(path)"
}
@@ -113,6 +161,23 @@ extension StoreDetailView {
return String(format: "R$ %.2f", value).replacingOccurrences(of: ".", with: ",")
}
func listPriceLabel(for product: StoreCatalogProduct, in category: StoreCatalogCategory) -> String {
guard product.type?.lowercased() == "pizza", product.pizzaPrices.isEmpty == false else {
return formatCurrency(product.price)
}
if let firstSizeId = category.pizzaConfig?.sizes.first?.id,
let firstSizePrice = product.pizzaPrices[firstSizeId] {
return "A partir de \(formatCurrency(firstSizePrice))"
}
if let fallback = product.pizzaPrices.sorted(by: { $0.key < $1.key }).first?.value {
return "A partir de \(formatCurrency(fallback))"
}
return formatCurrency(product.price)
}
var topSectionHeight: CGFloat {
cardTopInset + summaryCardHeight
}
@@ -146,7 +211,53 @@ extension StoreDetailView {
.reduce(0) { $0 + $1.quantity }
}
func quantityInCart(for item: StoreCatalogListItem) -> Int {
if item.isPizzaSummary {
let ids = Set(item.pizzaProductIds)
return appState.cart.items
.filter { $0.storeId == storeId && ids.contains($0.productId) }
.reduce(0) { $0 + $1.quantity }
}
return quantityInCart(for: item.product.id)
}
func listItems(for category: StoreCatalogCategory) -> [StoreCatalogListItem] {
if category.isPizzaCategory {
guard let first = category.products.first else { return [] }
let representativeImage = category.products
.compactMap(\.image)
.first { $0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false }
return [
StoreCatalogListItem(
id: "\(category.id)::pizza-summary",
product: first,
title: "Pizza de varios sabores",
description: "Escolha o tamanho da sua fome",
imageURL: representativeImage ?? first.image,
isPizzaSummary: true,
pizzaCategoryId: category.id,
pizzaProductIds: category.products.map(\.id)
)
]
}
return category.products.map { product in
StoreCatalogListItem(
id: product.id,
product: product,
title: product.name,
description: product.description,
imageURL: product.image
)
}
}
func requestAddToCart(_ item: CartItemState) {
guard isStoreOpen else {
SnackbarCenter.shared.show(title: "Loja fechada no momento.", style: .warning, icon: "moon.zzz.fill", duration: 2.0)
return
}
pendingCartAction = .add
if shouldAskForStoreSwitch(for: storeId) {
pendingCartItem = item
@@ -158,6 +269,10 @@ extension StoreDetailView {
}
func requestSetCartItem(_ item: CartItemState) {
guard isStoreOpen || item.quantity <= 0 else {
SnackbarCenter.shared.show(title: "Loja fechada no momento.", style: .warning, icon: "moon.zzz.fill", duration: 2.0)
return
}
pendingCartAction = .set
if shouldAskForStoreSwitch(for: storeId) && item.quantity > 0 {
pendingCartItem = item
@@ -169,6 +284,10 @@ extension StoreDetailView {
}
func requestOpenProductSheet(_ product: StoreCatalogProduct) {
guard isStoreOpen else {
SnackbarCenter.shared.show(title: "Loja fechada no momento.", style: .warning, icon: "moon.zzz.fill", duration: 2.0)
return
}
pendingCartAction = .openProductSheet
if shouldAskForStoreSwitch(for: storeId) {
pendingCartItem = nil
@@ -179,6 +298,22 @@ extension StoreDetailView {
selectedProduct = product
}
func requestOpenPizzaSheet(categoryId: String) {
guard isStoreOpen else {
SnackbarCenter.shared.show(title: "Loja fechada no momento.", style: .warning, icon: "moon.zzz.fill", duration: 2.0)
return
}
pendingCartAction = .openPizzaSheet
if shouldAskForStoreSwitch(for: storeId) {
pendingCartItem = nil
pendingProductSheet = nil
pendingPizzaCategoryId = categoryId
showSwitchStoreAlert = true
return
}
selectedPizzaCategoryId = categoryId
}
func applyAddToCart(_ item: CartItemState) {
if appState.cart.storeId == nil {
appState.cart.storeId = storeId
@@ -224,4 +359,5 @@ enum CartAction {
case add
case set
case openProductSheet
case openPizzaSheet
}