This commit is contained in:
Daniel Arantes Loverde
2026-07-07 15:11:31 -03:00
parent 8b9ebdcb44
commit e51c99973f
293 changed files with 457 additions and 4919 deletions

View File

@@ -0,0 +1,138 @@
import Foundation
import SwiftUI
extension HomeView {
func buildCategories(from stores: [StoreSummary]) -> [CategoryModel] {
var unique: [CategoryModel] = [
.init(id: "all", title: "Todas", systemIcon: "line.3.horizontal.decrease.circle", emojiIcon: nil)
]
var seen = Set<String>()
for store in stores {
let raw = (store.category ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
if raw.isEmpty { continue }
let dedupe = raw.lowercased()
if seen.contains(dedupe) { continue }
seen.insert(dedupe)
unique.append(.init(id: raw, title: raw, systemIcon: categoryIcon(for: raw), emojiIcon: nil))
}
return unique
}
@MainActor
func loadHomeCategories(withFallbackStores stores: [StoreSummary], forceRefresh: Bool = false) async {
let cacheKey = "public-categories"
if forceRefresh == false,
let cached: [CategoryModel] = AppContentCache.shared.value(for: cacheKey, as: [CategoryModel].self) {
categories = cached
return
}
do {
let response = try await ApiService().listPublicCategories(forceRefresh: forceRefresh)
if response.error == false, let remote = response.result, remote.isEmpty == false {
let mapped = mapPublicCategories(remote)
categories = mapped
AppContentCache.shared.set(mapped, for: cacheKey, ttl: AppCacheTTL.twoHours)
return
}
} catch {
// Fallback handled below.
}
let fallback = buildCategories(from: stores)
categories = fallback
AppContentCache.shared.set(fallback, for: cacheKey, ttl: AppCacheTTL.twoHours)
}
func mapPublicCategories(_ remote: [PublicCategory]) -> [CategoryModel] {
var mapped: [CategoryModel] = []
var seen = Set<String>()
for item in remote {
let id = item.id.trimmingCharacters(in: .whitespacesAndNewlines)
let title = item.name.trimmingCharacters(in: .whitespacesAndNewlines)
if id.isEmpty || title.isEmpty { continue }
if seen.contains(id.lowercased()) { continue }
seen.insert(id.lowercased())
mapped.append(
.init(
id: id,
title: title,
systemIcon: id.lowercased() == "all" ? "line.3.horizontal.decrease.circle" : nil,
emojiIcon: item.icon
)
)
}
if mapped.contains(where: { $0.id.lowercased() == "all" }) == false {
mapped.insert(.init(id: "all", title: "Todas", systemIcon: "line.3.horizontal.decrease.circle", emojiIcon: nil), at: 0)
} else {
mapped.sort { lhs, rhs in
if lhs.id.lowercased() == "all" { return true }
if rhs.id.lowercased() == "all" { return false }
return lhs.title < rhs.title
}
}
return mapped
}
func categoryIcon(for category: String) -> String {
let value = category.folding(options: [.diacriticInsensitive, .caseInsensitive], locale: .current).lowercased()
if value.contains("pizza") { return "takeoutbag.and.cup.and.straw" }
if value.contains("lanche") || value.contains("hamburg") || value.contains("burger") { return "fork.knife" }
if value.contains("cafe") || value.contains("breakfast") { return "sun.max" }
if value.contains("doce") || value.contains("sobremesa") || value.contains("dessert") { return "cup.and.saucer" }
return "storefront"
}
@MainActor
func resolveCoordinates(forceRefresh: Bool) async -> (Double, Double)? {
guard hasConfiguredAddress() else {
return nil
}
// If user selected/saved an address, always trust its coordinates.
// This avoids overriding the chosen city with current device GPS.
if let lat = appState.address.latitude, let lng = appState.address.longitude {
return (lat, lng)
}
if !forceRefresh, let cached = LocationService.shared.cachedLocation() {
appState.address.latitude = cached.0
appState.address.longitude = cached.1
return cached
}
// Fallback to device location only when no address coordinates are available.
let deviceCoordinate = await LocationService.shared.requestLocationAsync(timeoutSeconds: 3)
if let deviceCoordinate {
appState.address.latitude = deviceCoordinate.0
appState.address.longitude = deviceCoordinate.1
}
return deviceCoordinate
}
func hasConfiguredAddress() -> Bool {
if appState.address.selectedId != nil {
return true
}
let normalized = appState.address.display
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
return normalized.isEmpty == false && normalized != "defina seu endereco"
}
func storesUserMessage(_ error: Error) -> String {
if let service = error as? ApiServiceError {
return service.errorDescription ?? "Não foi possível carregar os estabelecimentos."
}
if let network = error as? NetworkError {
return network.errorDescription ?? "Não foi possível carregar os estabelecimentos."
}
return "Não foi possível carregar os estabelecimentos."
}
}