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 orders: [AppOrderSummary] = []
@State var hasLoadedOnce = false
@State var storeRatingByStoreId: [String: Double] = [:]
@State var storeRatingByStoreName: [String: Double] = [:]
var body: some View {
ScrollView(showsIndicators: false) {
@@ -44,6 +46,9 @@ struct OrdersView: View {
orderRow(order)
}
.buttonStyle(.plain)
.padding(14)
.background(AppColors.surface)
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
}
}
}
@@ -55,9 +60,11 @@ struct OrdersView: View {
.navigationBarTitleDisplayMode(.inline)
.task {
await loadOrdersIfNeeded()
await refreshStoreRatings()
}
.refreshable {
await loadOrders(force: true)
await refreshStoreRatings()
}
}
@@ -72,13 +79,28 @@ struct OrdersView: View {
)
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)
.foregroundStyle(AppColors.textPrimary)
Text(humanReadableStatus(for: order))
.font(AppTypography.caption)
.foregroundStyle(AppColors.textMuted)
HStack(spacing: 6) {
Text(humanReadableStatus(for: order))
.font(AppTypography.caption)
.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()
@@ -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 {
@@ -172,6 +191,17 @@ struct OrdersView: View {
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
private func loadOrdersIfNeeded() async {
guard hasLoadedOnce == false else { return }
@@ -332,6 +362,48 @@ struct OrdersView: View {
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 {
@@ -340,6 +412,7 @@ extension AppOrderSummary {
id: tracked.id,
orderId: tracked.realId ?? tracked.id,
realId: tracked.realId,
storeId: nil,
shortId: tracked.shortId,
total: tracked.total,
status: tracked.status,
@@ -350,6 +423,7 @@ extension AppOrderSummary {
paymentMethod: tracked.paymentMethod,
deliveryType: tracked.deliveryType,
storeName: tracked.storeName,
storeLogoURL: tracked.storeLogoURL,
createdAt: tracked.createdAt,
updatedAt: tracked.updatedAt
)
@@ -359,6 +433,7 @@ extension AppOrderSummary {
id: String,
orderId: String?,
realId: String?,
storeId: String?,
shortId: String?,
total: Double?,
status: String?,
@@ -369,12 +444,14 @@ extension AppOrderSummary {
paymentMethod: String?,
deliveryType: String?,
storeName: String?,
storeLogoURL: String?,
createdAt: String?,
updatedAt: String?
) {
self.id = id
self.orderId = orderId
self.realId = realId
self.storeId = storeId
self.shortId = shortId
self.total = total
self.status = status
@@ -385,6 +462,7 @@ extension AppOrderSummary {
self.paymentMethod = paymentMethod
self.deliveryType = deliveryType
self.storeName = storeName
self.storeLogoURL = storeLogoURL
self.createdAt = createdAt
self.updatedAt = updatedAt
}
@@ -453,15 +531,9 @@ struct OrderEntryDestinationView: View {
let order = await fetchOrderForRouting()
guard let order else { return }
guard order.isPaymentConfirmed == false else { return }
guard isOnlinePaymentMethod(order) else { return }
guard shouldOpenPaymentScreen(for: order) else { return }
let normalizedMethod = normalizePaymentMethod(order)
let normalizedStatus = normalize(order.paymentStatus)
if normalizedStatus.contains("PENDING") == false && normalizedStatus.isEmpty == false {
return
}
if normalizedMethod.contains("PIX") {
let pixFromPayment = order.payment?.pix
let pixFromPayload = order.paymentPayload
@@ -489,33 +561,111 @@ struct OrderEntryDestinationView: View {
return
}
if normalizedMethod.contains("CREDIT") || normalizedMethod.contains("CARD") {
if normalizedMethod == "CREDIT_CARD" || normalizedMethod == "DEBIT_CARD" {
cardContext = CardPaymentContext(
orderId: order.id,
shortId: order.shortId ?? initialShortId,
total: order.total ?? fallbackTotal ?? 0
)
return
}
}
@MainActor
func fetchOrderForRouting() async -> PublicOrderResult? {
logger.info("OrderEntry fetch route orderId=\(orderId, privacy: .public)")
do {
let response = try await ApiService().publicOrder(orderId: orderId)
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
}
logger.error("OrderEntry fetch route API error orderId=\(orderId, privacy: .public) message=\((response.message ?? "unknown"), privacy: .public)")
} 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 SessionStateStore.loadTrackedOrder(orderId: orderId)
return nil
}
func isOnlinePaymentMethod(_ order: PublicOrderResult) -> Bool {
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 {

File diff suppressed because it is too large Load Diff