feat(orders): separar pedidos de reviews e listar pendentes
This commit is contained in:
@@ -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 {
|
||||
|
||||
1566
pedi-foods/Sources/PediFoods/Views/Main/ReviewsView.swift
Normal file
1566
pedi-foods/Sources/PediFoods/Views/Main/ReviewsView.swift
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user