migration
This commit is contained in:
196
PediFoods/Views/Main/HomeView+Data.swift
Normal file
196
PediFoods/Views/Main/HomeView+Data.swift
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user