payment and list
This commit is contained in:
@@ -4,6 +4,7 @@ struct OrdersView: View {
|
||||
@State var isLoading = false
|
||||
@State var errorMessage: String? = nil
|
||||
@State var orders: [AppOrderSummary] = []
|
||||
@State var hasLoadedOnce = false
|
||||
|
||||
var body: some View {
|
||||
ScrollView(showsIndicators: false) {
|
||||
@@ -31,8 +32,14 @@ struct OrdersView: View {
|
||||
.padding(.top, 24)
|
||||
} else {
|
||||
ForEach(orders) { order in
|
||||
let trackingId = trackingOrderId(for: order)
|
||||
NavigationLink {
|
||||
OrderTrackingView(orderId: order.id, initialShortId: order.shortId)
|
||||
OrderEntryDestinationView(
|
||||
orderId: trackingId,
|
||||
initialShortId: order.shortId,
|
||||
fallbackPaymentMethod: order.paymentMethod,
|
||||
fallbackTotal: order.total
|
||||
)
|
||||
} label: {
|
||||
orderRow(order)
|
||||
}
|
||||
@@ -47,7 +54,10 @@ struct OrdersView: View {
|
||||
.navigationTitle("Meus Pedidos")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.task {
|
||||
await loadOrders()
|
||||
await loadOrdersIfNeeded()
|
||||
}
|
||||
.refreshable {
|
||||
await loadOrders(force: true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +76,7 @@ struct OrdersView: View {
|
||||
.font(AppTypography.heading3)
|
||||
.foregroundStyle(AppColors.textPrimary)
|
||||
|
||||
Text(humanReadableStatus(order.status ?? order.paymentStatus ?? "Pendente"))
|
||||
Text(humanReadableStatus(for: order))
|
||||
.font(AppTypography.caption)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
}
|
||||
@@ -89,11 +99,41 @@ struct OrdersView: View {
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
||||
}
|
||||
|
||||
private func humanReadableStatus(_ raw: String) -> String {
|
||||
private func humanReadableStatus(for order: AppOrderSummary) -> String {
|
||||
let explicitLabel = (order.statusLabel ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if explicitLabel.isEmpty == false {
|
||||
return explicitLabel
|
||||
}
|
||||
|
||||
let detailed = (order.statusDetailed ?? "").uppercased()
|
||||
switch detailed {
|
||||
case "PENDING_PAYMENT":
|
||||
return "Aguardando pagamento"
|
||||
case "PENDING_PREPARATION":
|
||||
return "Pendente de preparo"
|
||||
case "PREPARING":
|
||||
return "Em preparo"
|
||||
case "PENDING_DELIVERY":
|
||||
return "Pendente de entrega"
|
||||
case "READY_FOR_PICKUP":
|
||||
return "Pronto para retirada"
|
||||
case "IN_DELIVERY":
|
||||
return "Em rota"
|
||||
case "COMPLETED":
|
||||
return "Entregue"
|
||||
case "CANCELED":
|
||||
return "Cancelado"
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
let raw = order.status ?? order.paymentStatus ?? "Pendente"
|
||||
let normalized = raw.uppercased()
|
||||
if normalized.contains("PAYMENT_PENDING") || normalized == "PENDING" { return "Aguardando pagamento" }
|
||||
if normalized.contains("CONFIRMED") { return "Confirmado" }
|
||||
if normalized.contains("OUT_FOR_DELIVERY") || normalized.contains("ROTA") { return "Em rota" }
|
||||
if normalized.contains("PAYMENT_PENDING") { return "Aguardando pagamento" }
|
||||
if normalized == "PENDING" || normalized == "ACCEPTED" { return "Pendente de preparo" }
|
||||
if normalized.contains("PREPARING") { return "Em preparo" }
|
||||
if normalized.contains("READY") { return "Pendente de entrega" }
|
||||
if normalized.contains("OUT_FOR_DELIVERY") || normalized.contains("DELIVERING") || normalized.contains("ROTA") { return "Em rota" }
|
||||
if normalized.contains("COMPLETED") || normalized.contains("DELIVERED") { return "Entregue" }
|
||||
if normalized.contains("CANCEL") { return "Cancelado" }
|
||||
return raw.capitalized
|
||||
@@ -120,56 +160,192 @@ struct OrdersView: View {
|
||||
return formatter.string(from: date)
|
||||
}
|
||||
|
||||
private func trackingOrderId(for order: AppOrderSummary) -> String {
|
||||
let orderCandidate = (order.orderId ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if orderCandidate.isEmpty == false {
|
||||
return orderCandidate
|
||||
}
|
||||
let candidate = (order.realId ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if candidate.isEmpty == false {
|
||||
return candidate
|
||||
}
|
||||
return order.id
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func loadOrders() async {
|
||||
private func loadOrdersIfNeeded() async {
|
||||
guard hasLoadedOnce == false else { return }
|
||||
await loadOrders(force: false)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func loadOrders(force: Bool) async {
|
||||
if isLoading { return }
|
||||
if force == false, hasLoadedOnce { return }
|
||||
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
let previousOrders = orders
|
||||
|
||||
var trackedMapped: [AppOrderSummary] = []
|
||||
let cachedTracked = SessionStateStore.loadTrackedOrders()
|
||||
if cachedTracked.isEmpty == false {
|
||||
let mapped = cachedTracked.map {
|
||||
trackedMapped = cachedTracked.map {
|
||||
AppOrderSummary.fromTracked($0)
|
||||
}
|
||||
orders = mergeOrders(apiOrders: orders, trackedOrders: mapped)
|
||||
// Use tracked-only list as bootstrap data only on first load.
|
||||
if hasLoadedOnce == false, previousOrders.isEmpty {
|
||||
orders = mergeOrders(apiOrders: [], trackedOrders: trackedMapped)
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
let response = try await ApiService().listOrders()
|
||||
let response = try await ApiService().listOrders(forceRefresh: force)
|
||||
if response.error {
|
||||
if previousOrders.isEmpty == false {
|
||||
orders = previousOrders
|
||||
}
|
||||
errorMessage = response.message ?? "Não foi possível carregar os pedidos."
|
||||
} else {
|
||||
let remote = response.result ?? []
|
||||
orders = mergeOrders(apiOrders: remote, trackedOrders: orders)
|
||||
orders = mergeOrders(apiOrders: remote, trackedOrders: trackedMapped)
|
||||
}
|
||||
} catch {
|
||||
if isCancelledRequest(error) {
|
||||
if previousOrders.isEmpty == false {
|
||||
orders = previousOrders
|
||||
}
|
||||
isLoading = false
|
||||
return
|
||||
}
|
||||
if orders.isEmpty {
|
||||
errorMessage = "Não foi possível carregar os pedidos."
|
||||
}
|
||||
}
|
||||
|
||||
isLoading = false
|
||||
hasLoadedOnce = true
|
||||
}
|
||||
|
||||
private func mergeOrders(apiOrders: [AppOrderSummary], trackedOrders: [AppOrderSummary]) -> [AppOrderSummary] {
|
||||
var map: [String: AppOrderSummary] = [:]
|
||||
for item in trackedOrders { map[item.id] = item }
|
||||
for item in apiOrders { map[item.id] = item }
|
||||
var sourceRank: [String: Int] = [:]
|
||||
|
||||
for (index, item) in trackedOrders.enumerated() {
|
||||
let key = identityKey(for: item)
|
||||
map[key] = item
|
||||
if sourceRank[key] == nil {
|
||||
sourceRank[key] = 10_000 + index
|
||||
}
|
||||
}
|
||||
|
||||
// API order is authoritative for fallback ordering (usually newest first).
|
||||
for (index, item) in apiOrders.enumerated() {
|
||||
let key = identityKey(for: item)
|
||||
map[key] = item
|
||||
sourceRank[key] = index
|
||||
}
|
||||
|
||||
return map.values.sorted { lhs, rhs in
|
||||
let left = lhs.updatedAt ?? lhs.createdAt ?? ""
|
||||
let right = rhs.updatedAt ?? rhs.createdAt ?? ""
|
||||
return left > right
|
||||
let leftDate = orderDateSortValue(lhs)
|
||||
let rightDate = orderDateSortValue(rhs)
|
||||
if leftDate != rightDate {
|
||||
return leftDate > rightDate
|
||||
}
|
||||
|
||||
let leftRank = sourceRank[identityKey(for: lhs)] ?? Int.max
|
||||
let rightRank = sourceRank[identityKey(for: rhs)] ?? Int.max
|
||||
if leftRank != rightRank {
|
||||
return leftRank < rightRank
|
||||
}
|
||||
|
||||
let leftNumericId = Int(lhs.id)
|
||||
let rightNumericId = Int(rhs.id)
|
||||
if let leftNumericId, let rightNumericId, leftNumericId != rightNumericId {
|
||||
return leftNumericId > rightNumericId
|
||||
}
|
||||
return lhs.id.localizedCompare(rhs.id) == .orderedDescending
|
||||
}
|
||||
}
|
||||
|
||||
private func identityKey(for order: AppOrderSummary) -> String {
|
||||
let raw = trackingOrderId(for: order)
|
||||
return raw.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||
}
|
||||
|
||||
private func orderDateSortValue(_ order: AppOrderSummary) -> Date {
|
||||
parseDateForSort(order.updatedAt)
|
||||
?? parseDateForSort(order.createdAt)
|
||||
?? .distantPast
|
||||
}
|
||||
|
||||
private func parseDateForSort(_ rawValue: String?) -> Date? {
|
||||
guard let rawValue else { return nil }
|
||||
let value = rawValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if value.isEmpty { return nil }
|
||||
|
||||
let iso = ISO8601DateFormatter()
|
||||
iso.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||
if let date = iso.date(from: value) { return date }
|
||||
iso.formatOptions = [.withInternetDateTime]
|
||||
if let date = iso.date(from: value) { return date }
|
||||
|
||||
let formatter = DateFormatter()
|
||||
formatter.locale = Locale(identifier: "pt_BR")
|
||||
|
||||
let formats = [
|
||||
"yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX",
|
||||
"yyyy-MM-dd'T'HH:mm:ssXXXXX",
|
||||
"yyyy-MM-dd HH:mm:ss",
|
||||
"yyyy-MM-dd'T'HH:mm:ss",
|
||||
"yyyy-MM-dd'T'HH:mm:ss.SSSZ",
|
||||
"yyyy-MM-dd HH:mm:ss Z",
|
||||
"dd/MM/yyyy HH:mm:ss",
|
||||
"dd/MM/yyyy HH:mm",
|
||||
"dd/MM/yyyy"
|
||||
]
|
||||
for format in formats {
|
||||
formatter.dateFormat = format
|
||||
if let date = formatter.date(from: value) {
|
||||
return date
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private func isCancelledRequest(_ error: Error) -> Bool {
|
||||
if error is CancellationError {
|
||||
return true
|
||||
}
|
||||
|
||||
if let networkError = error as? NetworkError,
|
||||
case .transportError(let message) = networkError {
|
||||
let normalized = message.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||
return normalized.contains("cancel")
|
||||
}
|
||||
|
||||
if let networkError = error as? NetworkError,
|
||||
case .cancelled = networkError {
|
||||
return true
|
||||
}
|
||||
|
||||
return error.localizedDescription.lowercased().contains("cancel")
|
||||
}
|
||||
}
|
||||
|
||||
extension AppOrderSummary {
|
||||
static func fromTracked(_ tracked: PublicOrderResult) -> AppOrderSummary {
|
||||
AppOrderSummary(
|
||||
id: tracked.id,
|
||||
orderId: tracked.realId ?? tracked.id,
|
||||
realId: tracked.realId,
|
||||
shortId: tracked.shortId,
|
||||
total: tracked.total,
|
||||
status: tracked.status,
|
||||
statusDetailed: nil,
|
||||
statusLabel: nil,
|
||||
nextAction: nil,
|
||||
paymentStatus: tracked.paymentStatus,
|
||||
paymentMethod: tracked.paymentMethod,
|
||||
deliveryType: tracked.deliveryType,
|
||||
@@ -181,9 +357,14 @@ extension AppOrderSummary {
|
||||
|
||||
init(
|
||||
id: String,
|
||||
orderId: String?,
|
||||
realId: String?,
|
||||
shortId: String?,
|
||||
total: Double?,
|
||||
status: String?,
|
||||
statusDetailed: String?,
|
||||
statusLabel: String?,
|
||||
nextAction: String?,
|
||||
paymentStatus: String?,
|
||||
paymentMethod: String?,
|
||||
deliveryType: String?,
|
||||
@@ -192,9 +373,14 @@ extension AppOrderSummary {
|
||||
updatedAt: String?
|
||||
) {
|
||||
self.id = id
|
||||
self.orderId = orderId
|
||||
self.realId = realId
|
||||
self.shortId = shortId
|
||||
self.total = total
|
||||
self.status = status
|
||||
self.statusDetailed = statusDetailed
|
||||
self.statusLabel = statusLabel
|
||||
self.nextAction = nextAction
|
||||
self.paymentStatus = paymentStatus
|
||||
self.paymentMethod = paymentMethod
|
||||
self.deliveryType = deliveryType
|
||||
@@ -203,3 +389,150 @@ extension AppOrderSummary {
|
||||
self.updatedAt = updatedAt
|
||||
}
|
||||
}
|
||||
|
||||
struct OrderEntryDestinationView: View {
|
||||
let orderId: String
|
||||
let initialShortId: String?
|
||||
let fallbackPaymentMethod: String?
|
||||
let fallbackTotal: Double?
|
||||
|
||||
@State var isResolvingRoute = true
|
||||
@State var didResolve = false
|
||||
@State var pixContext: PixPaymentContext? = nil
|
||||
@State var cardContext: CardPaymentContext? = nil
|
||||
@State var orderTrackingContext: OrderTrackingContext? = nil
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if isResolvingRoute {
|
||||
VStack(spacing: 10) {
|
||||
ProgressView()
|
||||
Text("Carregando pedido...")
|
||||
.font(AppTypography.body)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(AppColors.backgroundLight)
|
||||
} else if let pixContext {
|
||||
PaymentPixView(
|
||||
context: pixContext,
|
||||
onPaymentConfirmed: {
|
||||
orderTrackingContext = OrderTrackingContext(orderId: pixContext.orderId, shortId: pixContext.shortId)
|
||||
},
|
||||
onOpenTracking: {
|
||||
orderTrackingContext = OrderTrackingContext(orderId: pixContext.orderId, shortId: pixContext.shortId)
|
||||
}
|
||||
)
|
||||
} else if let cardContext {
|
||||
PaymentCardView(
|
||||
context: cardContext,
|
||||
onPaymentConfirmed: {
|
||||
orderTrackingContext = OrderTrackingContext(orderId: cardContext.orderId, shortId: cardContext.shortId)
|
||||
},
|
||||
onOpenTracking: {
|
||||
orderTrackingContext = OrderTrackingContext(orderId: cardContext.orderId, shortId: cardContext.shortId)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
OrderTrackingView(orderId: orderId, initialShortId: initialShortId)
|
||||
}
|
||||
}
|
||||
.navigationDestination(item: $orderTrackingContext) { context in
|
||||
OrderTrackingView(orderId: context.orderId, initialShortId: context.shortId)
|
||||
}
|
||||
.task {
|
||||
guard didResolve == false else { return }
|
||||
didResolve = true
|
||||
await resolveRoute()
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func resolveRoute() async {
|
||||
defer { isResolvingRoute = false }
|
||||
|
||||
let order = await fetchOrderForRouting()
|
||||
guard let order else { return }
|
||||
guard order.isPaymentConfirmed == false else { return }
|
||||
guard isOnlinePaymentMethod(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
|
||||
let copyPaste = (pixFromPayment?.copyPaste?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false)
|
||||
? pixFromPayment?.copyPaste
|
||||
: pixFromPayload?.copyPaste
|
||||
|
||||
let qrCodeImage = (pixFromPayment?.qrCodeImage?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false)
|
||||
? pixFromPayment?.qrCodeImage
|
||||
: pixFromPayload?.qrCodeImage
|
||||
let expirationDate = (pixFromPayment?.expirationDate?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false)
|
||||
? pixFromPayment?.expirationDate
|
||||
: pixFromPayload?.expirationDate
|
||||
|
||||
pixContext = PixPaymentContext(
|
||||
id: order.id,
|
||||
orderId: order.id,
|
||||
shortId: order.shortId ?? initialShortId,
|
||||
copyPaste: copyPaste?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
|
||||
? (copyPaste ?? "")
|
||||
: "Código PIX indisponível no momento. Aguarde e tente novamente.",
|
||||
qrCodeImageBase64: qrCodeImage,
|
||||
expirationDate: expirationDate
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if normalizedMethod.contains("CREDIT") || normalizedMethod.contains("CARD") {
|
||||
cardContext = CardPaymentContext(
|
||||
orderId: order.id,
|
||||
shortId: order.shortId ?? initialShortId,
|
||||
total: order.total ?? fallbackTotal ?? 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func fetchOrderForRouting() async -> PublicOrderResult? {
|
||||
do {
|
||||
let response = try await ApiService().publicOrder(orderId: orderId)
|
||||
if response.error == false, let result = response.result {
|
||||
SessionStateStore.saveTrackedOrder(result)
|
||||
return result
|
||||
}
|
||||
} catch {
|
||||
// Fallback to local cache when remote call fails.
|
||||
}
|
||||
|
||||
return SessionStateStore.loadTrackedOrder(orderId: orderId)
|
||||
}
|
||||
|
||||
func isOnlinePaymentMethod(_ order: PublicOrderResult) -> Bool {
|
||||
let method = normalizePaymentMethod(order)
|
||||
return method.contains("PIX") || method.contains("CREDIT") || method.contains("CARD")
|
||||
}
|
||||
|
||||
func normalizePaymentMethod(_ order: PublicOrderResult) -> String {
|
||||
let first = normalize(order.paymentMethodCode)
|
||||
if first.isEmpty == false {
|
||||
return first
|
||||
}
|
||||
let second = normalize(order.paymentMethod)
|
||||
if second.isEmpty == false {
|
||||
return second
|
||||
}
|
||||
return normalize(fallbackPaymentMethod)
|
||||
}
|
||||
|
||||
func normalize(_ value: String?) -> String {
|
||||
(value ?? "")
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
.uppercased()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user