feat(orders): separar pedidos de reviews e listar pendentes

This commit is contained in:
Daniel Arantes Loverde
2026-03-05 10:05:48 -03:00
parent ca83a275ac
commit 56d6ff7223
2 changed files with 1736 additions and 20 deletions

View File

@@ -5,6 +5,8 @@ struct OrdersView: View {
@State var errorMessage: String? = nil @State var errorMessage: String? = nil
@State var orders: [AppOrderSummary] = [] @State var orders: [AppOrderSummary] = []
@State var hasLoadedOnce = false @State var hasLoadedOnce = false
@State var storeRatingByStoreId: [String: Double] = [:]
@State var storeRatingByStoreName: [String: Double] = [:]
var body: some View { var body: some View {
ScrollView(showsIndicators: false) { ScrollView(showsIndicators: false) {
@@ -44,6 +46,9 @@ struct OrdersView: View {
orderRow(order) orderRow(order)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.padding(14)
.background(AppColors.surface)
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
} }
} }
} }
@@ -55,9 +60,11 @@ struct OrdersView: View {
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.task { .task {
await loadOrdersIfNeeded() await loadOrdersIfNeeded()
await refreshStoreRatings()
} }
.refreshable { .refreshable {
await loadOrders(force: true) await loadOrders(force: true)
await refreshStoreRatings()
} }
} }
@@ -72,13 +79,28 @@ struct OrdersView: View {
) )
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
Text("Pedido #\(order.shortId ?? order.id)") Text(order.storeName?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false ? (order.storeName ?? "Pedido") : "Pedido #\(order.shortId ?? order.id)")
.font(AppTypography.heading3) .font(AppTypography.heading3)
.foregroundStyle(AppColors.textPrimary) .foregroundStyle(AppColors.textPrimary)
HStack(spacing: 6) {
Text(humanReadableStatus(for: order)) Text(humanReadableStatus(for: order))
.font(AppTypography.caption) .font(AppTypography.caption)
.foregroundStyle(AppColors.textMuted) .foregroundStyle(AppColors.textMuted)
if let rating = storeRating(for: order) {
Text("")
.font(AppTypography.caption)
.foregroundStyle(AppColors.textMuted)
HStack(spacing: 4) {
Image(systemName: "star.fill")
.font(.system(size: 11, weight: .bold))
.foregroundStyle(Color(hex: "#7CF02A"))
Text(String(format: "%.1f", rating).replacingOccurrences(of: ".", with: ","))
.font(AppTypography.caption)
.foregroundStyle(AppColors.textMuted)
}
}
}
} }
Spacer() Spacer()
@@ -94,9 +116,6 @@ struct OrdersView: View {
} }
} }
} }
.padding(14)
.background(AppColors.surface)
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
} }
private func humanReadableStatus(for order: AppOrderSummary) -> String { private func humanReadableStatus(for order: AppOrderSummary) -> String {
@@ -172,6 +191,17 @@ struct OrdersView: View {
return order.id return order.id
} }
private func normalizedOrderId(_ value: String?) -> String {
(value ?? "").trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
}
private func normalizedStoreName(_ value: String?) -> String {
(value ?? "")
.trimmingCharacters(in: .whitespacesAndNewlines)
.folding(options: [.diacriticInsensitive, .caseInsensitive], locale: .current)
.lowercased()
}
@MainActor @MainActor
private func loadOrdersIfNeeded() async { private func loadOrdersIfNeeded() async {
guard hasLoadedOnce == false else { return } guard hasLoadedOnce == false else { return }
@@ -332,6 +362,48 @@ struct OrdersView: View {
return error.localizedDescription.lowercased().contains("cancel") return error.localizedDescription.lowercased().contains("cancel")
} }
@MainActor
private func refreshStoreRatings() async {
var storeList: [StoreSummary] = AppContentCache.shared.value(
for: AppCacheKey.homeStoresLatestSnapshot,
as: [StoreSummary].self
) ?? []
if storeList.isEmpty {
let response = try? await ApiService().listStores()
storeList = response?.result ?? []
}
var byId: [String: Double] = [:]
var byName: [String: Double] = [:]
for store in storeList {
guard let rating = store.rating, rating > 0 else { continue }
let storeId = normalizedOrderId(store.id)
if storeId.isEmpty == false {
byId[storeId] = rating
}
let nameKey = normalizedStoreName(store.name)
if nameKey.isEmpty == false {
byName[nameKey] = rating
}
}
storeRatingByStoreId = byId
storeRatingByStoreName = byName
}
private func storeRating(for order: AppOrderSummary) -> Double? {
let storeId = normalizedOrderId(order.storeId)
if storeId.isEmpty == false, let fromId = storeRatingByStoreId[storeId] {
return fromId
}
let nameKey = normalizedStoreName(order.storeName)
if nameKey.isEmpty == false, let fromName = storeRatingByStoreName[nameKey] {
return fromName
}
return nil
}
} }
extension AppOrderSummary { extension AppOrderSummary {
@@ -340,6 +412,7 @@ extension AppOrderSummary {
id: tracked.id, id: tracked.id,
orderId: tracked.realId ?? tracked.id, orderId: tracked.realId ?? tracked.id,
realId: tracked.realId, realId: tracked.realId,
storeId: nil,
shortId: tracked.shortId, shortId: tracked.shortId,
total: tracked.total, total: tracked.total,
status: tracked.status, status: tracked.status,
@@ -350,6 +423,7 @@ extension AppOrderSummary {
paymentMethod: tracked.paymentMethod, paymentMethod: tracked.paymentMethod,
deliveryType: tracked.deliveryType, deliveryType: tracked.deliveryType,
storeName: tracked.storeName, storeName: tracked.storeName,
storeLogoURL: tracked.storeLogoURL,
createdAt: tracked.createdAt, createdAt: tracked.createdAt,
updatedAt: tracked.updatedAt updatedAt: tracked.updatedAt
) )
@@ -359,6 +433,7 @@ extension AppOrderSummary {
id: String, id: String,
orderId: String?, orderId: String?,
realId: String?, realId: String?,
storeId: String?,
shortId: String?, shortId: String?,
total: Double?, total: Double?,
status: String?, status: String?,
@@ -369,12 +444,14 @@ extension AppOrderSummary {
paymentMethod: String?, paymentMethod: String?,
deliveryType: String?, deliveryType: String?,
storeName: String?, storeName: String?,
storeLogoURL: String?,
createdAt: String?, createdAt: String?,
updatedAt: String? updatedAt: String?
) { ) {
self.id = id self.id = id
self.orderId = orderId self.orderId = orderId
self.realId = realId self.realId = realId
self.storeId = storeId
self.shortId = shortId self.shortId = shortId
self.total = total self.total = total
self.status = status self.status = status
@@ -385,6 +462,7 @@ extension AppOrderSummary {
self.paymentMethod = paymentMethod self.paymentMethod = paymentMethod
self.deliveryType = deliveryType self.deliveryType = deliveryType
self.storeName = storeName self.storeName = storeName
self.storeLogoURL = storeLogoURL
self.createdAt = createdAt self.createdAt = createdAt
self.updatedAt = updatedAt self.updatedAt = updatedAt
} }
@@ -453,15 +531,9 @@ struct OrderEntryDestinationView: View {
let order = await fetchOrderForRouting() let order = await fetchOrderForRouting()
guard let order else { return } guard let order else { return }
guard order.isPaymentConfirmed == false else { return } guard shouldOpenPaymentScreen(for: order) else { return }
guard isOnlinePaymentMethod(order) else { return }
let normalizedMethod = normalizePaymentMethod(order) let normalizedMethod = normalizePaymentMethod(order)
let normalizedStatus = normalize(order.paymentStatus)
if normalizedStatus.contains("PENDING") == false && normalizedStatus.isEmpty == false {
return
}
if normalizedMethod.contains("PIX") { if normalizedMethod.contains("PIX") {
let pixFromPayment = order.payment?.pix let pixFromPayment = order.payment?.pix
let pixFromPayload = order.paymentPayload let pixFromPayload = order.paymentPayload
@@ -489,33 +561,111 @@ struct OrderEntryDestinationView: View {
return return
} }
if normalizedMethod.contains("CREDIT") || normalizedMethod.contains("CARD") { if normalizedMethod == "CREDIT_CARD" || normalizedMethod == "DEBIT_CARD" {
cardContext = CardPaymentContext( cardContext = CardPaymentContext(
orderId: order.id, orderId: order.id,
shortId: order.shortId ?? initialShortId, shortId: order.shortId ?? initialShortId,
total: order.total ?? fallbackTotal ?? 0 total: order.total ?? fallbackTotal ?? 0
) )
return
} }
} }
@MainActor @MainActor
func fetchOrderForRouting() async -> PublicOrderResult? { func fetchOrderForRouting() async -> PublicOrderResult? {
logger.info("OrderEntry fetch route orderId=\(orderId, privacy: .public)")
do { do {
let response = try await ApiService().publicOrder(orderId: orderId) let response = try await ApiService().publicOrder(orderId: orderId)
if response.error == false, let result = response.result { if response.error == false, let result = response.result {
SessionStateStore.saveTrackedOrder(result) logger.info("OrderEntry fetch route success orderId=\(orderId, privacy: .public) status=\((result.status ?? "nil"), privacy: .public) paymentStatus=\((result.paymentStatus ?? "nil"), privacy: .public)")
return result return result
} }
logger.error("OrderEntry fetch route API error orderId=\(orderId, privacy: .public) message=\((response.message ?? "unknown"), privacy: .public)")
} catch { } catch {
// Fallback to local cache when remote call fails. logger.error("OrderEntry fetch route failure orderId=\(orderId, privacy: .public) error=\(error.localizedDescription, privacy: .public)")
} }
return nil
return SessionStateStore.loadTrackedOrder(orderId: orderId)
} }
func isOnlinePaymentMethod(_ order: PublicOrderResult) -> Bool { func isOnlinePaymentMethod(_ order: PublicOrderResult) -> Bool {
let method = normalizePaymentMethod(order) let method = normalizePaymentMethod(order)
return method.contains("PIX") || method.contains("CREDIT") || method.contains("CARD") return method == "PIX" || method == "CREDIT_CARD" || method == "DEBIT_CARD"
}
func shouldOpenPaymentScreen(for order: PublicOrderResult) -> Bool {
guard order.isPaymentConfirmed == false else { return false }
guard isOnlinePaymentMethod(order) else { return false }
guard isPaymentPending(order) else { return false }
guard isInStorePayment(order) == false else { return false }
let status = normalize(order.status)
if status.contains("PREPAR") ||
status.contains("READY") ||
status.contains("DELIVER") ||
status.contains("ROTA") ||
status.contains("COMPLETED") ||
status.contains("CANCEL") ||
status.contains("REFUND") {
return false
}
let paymentStatus = normalize(order.paymentStatus)
if paymentStatus.contains("CONFIRM") ||
paymentStatus.contains("PAID") ||
paymentStatus.contains("RECEIV") ||
paymentStatus.contains("APPROV") {
return false
}
let method = normalizePaymentMethod(order)
if method == "PIX" {
return hasPixPayload(order)
}
return method == "CREDIT_CARD" || method == "DEBIT_CARD"
}
func hasPixPayload(_ order: PublicOrderResult) -> Bool {
let fromPayment = (order.payment?.pix?.copyPaste ?? "")
.trimmingCharacters(in: .whitespacesAndNewlines)
if fromPayment.isEmpty == false { return true }
let fromPayload = (order.paymentPayload?.copyPaste ?? "")
.trimmingCharacters(in: .whitespacesAndNewlines)
return fromPayload.isEmpty == false
}
func isPaymentPending(_ order: PublicOrderResult) -> Bool {
let status = normalize(order.status)
let paymentStatus = normalize(order.paymentStatus)
let nextAction = normalize(order.nextAction)
if status.contains("PAYMENT_PENDING") {
return true
}
if paymentStatus.contains("PENDING") {
return true
}
if nextAction.contains("PAY") || nextAction.contains("PAYMENT") {
return true
}
return false
}
func isInStorePayment(_ order: PublicOrderResult) -> Bool {
let nextAction = normalize(order.nextAction)
if nextAction.contains("TRACK") || nextAction.contains("DELIVER") {
return true
}
let status = normalize(order.status)
if status.contains("PREPAR") ||
status.contains("READY") ||
status.contains("DELIVER") ||
status.contains("ROTA") ||
status.contains("OUT_FOR_DELIVERY") {
return true
}
return false
} }
func normalizePaymentMethod(_ order: PublicOrderResult) -> String { func normalizePaymentMethod(_ order: PublicOrderResult) -> String {

File diff suppressed because it is too large Load Diff