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 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)
|
||||||
|
|
||||||
Text(humanReadableStatus(for: order))
|
HStack(spacing: 6) {
|
||||||
.font(AppTypography.caption)
|
Text(humanReadableStatus(for: order))
|
||||||
.foregroundStyle(AppColors.textMuted)
|
.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()
|
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 {
|
||||||
|
|||||||
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