migration

This commit is contained in:
Daniel Arantes Loverde
2026-08-11 13:22:02 -03:00
parent c4d6998595
commit 7702836fe7
231 changed files with 4329 additions and 1958 deletions

View File

@@ -0,0 +1,196 @@
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."
}
/// Anonymous store loading: no account, no coordinates just the
/// manually-picked state/city from the public locator. The BFF endpoint
/// has no category filter, so any category chip selection is applied
/// client-side via `filteredStores` (HomeView+Filtering.swift), same as
/// the multi-select filters already do.
@MainActor
func loadGuestStores(hadExistingStores: Bool, category: String?, refreshCategories: Bool) async {
guard let state = GuestLocationStore.shared.selectedState,
let city = GuestLocationStore.shared.selectedCity else {
isLoadingStores = false
stores = []
storesError = "Escolha um estado e cidade para visualizar os estabelecimentos."
appState.activeModal = .addressPicker
return
}
do {
let items = try await PublicLocationService.shared.fetchStores(state: state, city: city)
isLoadingStores = false
let mapped = items.map(StoreSummary.init(publicItem:))
stores = mapped
if refreshCategories || (category == nil && categories.count <= 1) {
await loadHomeCategories(withFallbackStores: mapped, forceRefresh: refreshCategories)
if categories.contains(where: { $0.id == selectedCategory }) == false {
selectedCategory = "all"
}
}
storesError = nil
} catch {
if isCancelledRequest(error) {
isLoadingStores = false
return
}
isLoadingStores = false
reportStoresLoadFailure(storesUserMessage(error), hadExistingStores: hadExistingStores)
}
}
}
extension StoreSummary {
/// Maps the public-locator DTO onto the same model HomeView already
/// renders distance/positiveReviews don't exist in that response.
init(publicItem: PublicStoreListItem) {
self.id = publicItem.id
self.name = publicItem.name ?? "Loja"
self.logo = publicItem.logo
self.cover = publicItem.cover
self.category = publicItem.category
self.rating = publicItem.rating
self.reviewsCount = publicItem.totalReviews
self.positiveReviews = nil
self.deliveryTime = publicItem.deliveryTime
self.deliveryFee = publicItem.deliveryFee
self.distance = nil
self.isOpen = publicItem.isOpen
self.statusLabel = publicItem.statusLabel
}
}