feat(orders-ui): alinhar lista/detalhe ao layout e corrigir build
This commit is contained in:
@@ -81,6 +81,9 @@ struct PublicOrderResult: Codable, Identifiable {
|
|||||||
let nextAction: String?
|
let nextAction: String?
|
||||||
let deliveryType: String?
|
let deliveryType: String?
|
||||||
let deliveryTypeLabel: String?
|
let deliveryTypeLabel: String?
|
||||||
|
let subtotal: Double?
|
||||||
|
let deliveryFee: Double?
|
||||||
|
let discount: Double?
|
||||||
let total: Double?
|
let total: Double?
|
||||||
let storeName: String?
|
let storeName: String?
|
||||||
let storeLogoURL: String?
|
let storeLogoURL: String?
|
||||||
@@ -111,6 +114,15 @@ struct PublicOrderResult: Codable, Identifiable {
|
|||||||
case nextAction
|
case nextAction
|
||||||
case deliveryType
|
case deliveryType
|
||||||
case deliveryTypeLabel
|
case deliveryTypeLabel
|
||||||
|
case subtotal
|
||||||
|
case subTotal
|
||||||
|
case itemsTotal
|
||||||
|
case deliveryFee
|
||||||
|
case delivery_fee
|
||||||
|
case fee
|
||||||
|
case discount
|
||||||
|
case desconto
|
||||||
|
case couponDiscount
|
||||||
case total
|
case total
|
||||||
case storeName
|
case storeName
|
||||||
case storeLogo
|
case storeLogo
|
||||||
@@ -148,6 +160,9 @@ struct PublicOrderResult: Codable, Identifiable {
|
|||||||
nextAction: String? = nil,
|
nextAction: String? = nil,
|
||||||
deliveryType: String? = nil,
|
deliveryType: String? = nil,
|
||||||
deliveryTypeLabel: String? = nil,
|
deliveryTypeLabel: String? = nil,
|
||||||
|
subtotal: Double? = nil,
|
||||||
|
deliveryFee: Double? = nil,
|
||||||
|
discount: Double? = nil,
|
||||||
total: Double? = nil,
|
total: Double? = nil,
|
||||||
storeName: String? = nil,
|
storeName: String? = nil,
|
||||||
storeLogoURL: String? = nil,
|
storeLogoURL: String? = nil,
|
||||||
@@ -176,6 +191,9 @@ struct PublicOrderResult: Codable, Identifiable {
|
|||||||
self.nextAction = nextAction
|
self.nextAction = nextAction
|
||||||
self.deliveryType = deliveryType
|
self.deliveryType = deliveryType
|
||||||
self.deliveryTypeLabel = deliveryTypeLabel
|
self.deliveryTypeLabel = deliveryTypeLabel
|
||||||
|
self.subtotal = subtotal
|
||||||
|
self.deliveryFee = deliveryFee
|
||||||
|
self.discount = discount
|
||||||
self.total = total
|
self.total = total
|
||||||
self.storeName = storeName
|
self.storeName = storeName
|
||||||
self.storeLogoURL = storeLogoURL
|
self.storeLogoURL = storeLogoURL
|
||||||
@@ -217,6 +235,9 @@ struct PublicOrderResult: Codable, Identifiable {
|
|||||||
}
|
}
|
||||||
deliveryType = ApiService.decodeFlexibleString(from: container, keys: [.deliveryType])
|
deliveryType = ApiService.decodeFlexibleString(from: container, keys: [.deliveryType])
|
||||||
deliveryTypeLabel = ApiService.decodeFlexibleString(from: container, keys: [.deliveryTypeLabel])
|
deliveryTypeLabel = ApiService.decodeFlexibleString(from: container, keys: [.deliveryTypeLabel])
|
||||||
|
subtotal = ApiService.decodeFlexibleDouble(from: container, keys: [.subtotal, .subTotal, .itemsTotal])
|
||||||
|
deliveryFee = ApiService.decodeFlexibleDouble(from: container, keys: [.deliveryFee, .delivery_fee, .fee])
|
||||||
|
discount = ApiService.decodeFlexibleDouble(from: container, keys: [.discount, .desconto, .couponDiscount])
|
||||||
total = ApiService.decodeFlexibleDouble(from: container, keys: [.total])
|
total = ApiService.decodeFlexibleDouble(from: container, keys: [.total])
|
||||||
storeName = ApiService.decodeFlexibleString(from: container, keys: [.storeName])
|
storeName = ApiService.decodeFlexibleString(from: container, keys: [.storeName])
|
||||||
storeLogoURL = ApiService.decodeFlexibleString(from: container, keys: [.storeLogo, .store_logo, .logo])
|
storeLogoURL = ApiService.decodeFlexibleString(from: container, keys: [.storeLogo, .store_logo, .logo])
|
||||||
@@ -253,6 +274,9 @@ struct PublicOrderResult: Codable, Identifiable {
|
|||||||
try container.encodeIfPresent(nextAction, forKey: .nextAction)
|
try container.encodeIfPresent(nextAction, forKey: .nextAction)
|
||||||
try container.encodeIfPresent(deliveryType, forKey: .deliveryType)
|
try container.encodeIfPresent(deliveryType, forKey: .deliveryType)
|
||||||
try container.encodeIfPresent(deliveryTypeLabel, forKey: .deliveryTypeLabel)
|
try container.encodeIfPresent(deliveryTypeLabel, forKey: .deliveryTypeLabel)
|
||||||
|
try container.encodeIfPresent(subtotal, forKey: .subtotal)
|
||||||
|
try container.encodeIfPresent(deliveryFee, forKey: .deliveryFee)
|
||||||
|
try container.encodeIfPresent(discount, forKey: .discount)
|
||||||
try container.encodeIfPresent(total, forKey: .total)
|
try container.encodeIfPresent(total, forKey: .total)
|
||||||
try container.encodeIfPresent(storeName, forKey: .storeName)
|
try container.encodeIfPresent(storeName, forKey: .storeName)
|
||||||
try container.encodeIfPresent(createdAt, forKey: .createdAt)
|
try container.encodeIfPresent(createdAt, forKey: .createdAt)
|
||||||
|
|||||||
@@ -15,16 +15,24 @@ struct OrderDetailsView: View {
|
|||||||
if hasAddressInfo {
|
if hasAddressInfo {
|
||||||
addressCard
|
addressCard
|
||||||
}
|
}
|
||||||
reorderButton
|
|
||||||
helpFooter
|
helpFooter
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 20)
|
.padding(.horizontal, 20)
|
||||||
.padding(.top, 14)
|
.padding(.top, 14)
|
||||||
.padding(.bottom, UIDevice.bottomNotch + 24)
|
.padding(.bottom, UIDevice.bottomNotch + 110)
|
||||||
}
|
}
|
||||||
.background(AppColors.backgroundLight)
|
.background(AppColors.backgroundLight)
|
||||||
.navigationTitle("Detalhes do Pedido")
|
.navigationTitle("Detalhes do Pedido")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.safeAreaInset(edge: .bottom) {
|
||||||
|
VStack {
|
||||||
|
reorderButton
|
||||||
|
.padding(.horizontal, 20)
|
||||||
|
.padding(.top, 10)
|
||||||
|
.padding(.bottom, max(10, UIDevice.bottomNotch))
|
||||||
|
}
|
||||||
|
.background(AppColors.backgroundLight.opacity(0.94))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var statusCard: some View {
|
private var statusCard: some View {
|
||||||
@@ -66,7 +74,7 @@ struct OrderDetailsView: View {
|
|||||||
Text(order.storeName?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false ? (order.storeName ?? "Loja") : "Loja")
|
Text(order.storeName?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false ? (order.storeName ?? "Loja") : "Loja")
|
||||||
.font(AppTypography.heading2)
|
.font(AppTypography.heading2)
|
||||||
.foregroundStyle(AppColors.textPrimary)
|
.foregroundStyle(AppColors.textPrimary)
|
||||||
Text("Pedido #\(displayOrderTitle)")
|
Text(storeSubtitle)
|
||||||
.font(AppTypography.body)
|
.font(AppTypography.body)
|
||||||
.foregroundStyle(AppColors.textMuted)
|
.foregroundStyle(AppColors.textMuted)
|
||||||
}
|
}
|
||||||
@@ -130,11 +138,31 @@ struct OrderDetailsView: View {
|
|||||||
.font(AppTypography.body)
|
.font(AppTypography.body)
|
||||||
.foregroundStyle(AppColors.textMuted)
|
.foregroundStyle(AppColors.textMuted)
|
||||||
Spacer()
|
Spacer()
|
||||||
Text(formatCurrency(subtotal))
|
Text(formatCurrency(subtotalValue))
|
||||||
.font(AppTypography.body)
|
.font(AppTypography.body)
|
||||||
.foregroundStyle(AppColors.textMuted)
|
.foregroundStyle(AppColors.textMuted)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Text("Taxa de entrega")
|
||||||
|
.font(AppTypography.body)
|
||||||
|
.foregroundStyle(AppColors.textMuted)
|
||||||
|
Spacer()
|
||||||
|
Text(formatCurrency(deliveryFeeValue))
|
||||||
|
.font(AppTypography.body)
|
||||||
|
.foregroundStyle(AppColors.textMuted)
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Text("Desconto")
|
||||||
|
.font(AppTypography.body)
|
||||||
|
.foregroundStyle(AppColors.textMuted)
|
||||||
|
Spacer()
|
||||||
|
Text("- \(formatCurrency(discountValue))")
|
||||||
|
.font(AppTypography.body)
|
||||||
|
.foregroundStyle(Color(hex: "#18A957"))
|
||||||
|
}
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
@@ -142,7 +170,7 @@ struct OrderDetailsView: View {
|
|||||||
.font(AppTypography.heading2)
|
.font(AppTypography.heading2)
|
||||||
.foregroundStyle(AppColors.textPrimary)
|
.foregroundStyle(AppColors.textPrimary)
|
||||||
Spacer()
|
Spacer()
|
||||||
Text(formatCurrency(order.total ?? subtotal))
|
Text(formatCurrency(totalValue))
|
||||||
.font(AppTypography.heading1)
|
.font(AppTypography.heading1)
|
||||||
.foregroundStyle(AppColors.textPrimary)
|
.foregroundStyle(AppColors.textPrimary)
|
||||||
}
|
}
|
||||||
@@ -217,6 +245,26 @@ struct OrderDetailsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var subtotalValue: Double {
|
||||||
|
order.subtotal ?? subtotal
|
||||||
|
}
|
||||||
|
|
||||||
|
private var deliveryFeeValue: Double {
|
||||||
|
max(0, order.deliveryFee ?? 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var discountValue: Double {
|
||||||
|
max(0, order.discount ?? 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var totalValue: Double {
|
||||||
|
if let total = order.total {
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
let calculated = subtotalValue + deliveryFeeValue - discountValue
|
||||||
|
return max(0, calculated)
|
||||||
|
}
|
||||||
|
|
||||||
private var hasAddressInfo: Bool {
|
private var hasAddressInfo: Bool {
|
||||||
deliveryAddressLine.isEmpty == false || deliveryAddressLine2.isEmpty == false
|
deliveryAddressLine.isEmpty == false || deliveryAddressLine2.isEmpty == false
|
||||||
}
|
}
|
||||||
@@ -241,6 +289,13 @@ struct OrderDetailsView: View {
|
|||||||
.joined(separator: " • ")
|
.joined(separator: " • ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var storeSubtitle: String {
|
||||||
|
if deliveryAddressLine2.isEmpty == false {
|
||||||
|
return deliveryAddressLine2
|
||||||
|
}
|
||||||
|
return "Pedido #\(displayOrderTitle)"
|
||||||
|
}
|
||||||
|
|
||||||
private var statusTitle: String {
|
private var statusTitle: String {
|
||||||
let status = normalized(order.status)
|
let status = normalized(order.status)
|
||||||
if status.contains("CANCEL") { return "Pedido cancelado" }
|
if status.contains("CANCEL") { return "Pedido cancelado" }
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ struct OrdersView: View {
|
|||||||
@State var hasLoadedOnce = false
|
@State var hasLoadedOnce = false
|
||||||
@State var storeRatingByStoreId: [String: Double] = [:]
|
@State var storeRatingByStoreId: [String: Double] = [:]
|
||||||
@State var storeRatingByStoreName: [String: Double] = [:]
|
@State var storeRatingByStoreName: [String: Double] = [:]
|
||||||
|
@State var selectedOrderRoute: OrderRouteContext? = nil
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView(showsIndicators: false) {
|
ScrollView(showsIndicators: false) {
|
||||||
VStack(alignment: .leading, spacing: 14) {
|
VStack(alignment: .leading, spacing: 14) {
|
||||||
Text("Histórico de Pedidos")
|
Text("Meus Pedidos")
|
||||||
.font(AppTypography.heading1)
|
.font(AppTypography.heading1)
|
||||||
.foregroundStyle(AppColors.textPrimary)
|
.foregroundStyle(AppColors.textPrimary)
|
||||||
.padding(.top, 6)
|
.padding(.top, 6)
|
||||||
@@ -34,21 +35,7 @@ struct OrdersView: View {
|
|||||||
.padding(.top, 24)
|
.padding(.top, 24)
|
||||||
} else {
|
} else {
|
||||||
ForEach(orders) { order in
|
ForEach(orders) { order in
|
||||||
let trackingId = trackingOrderId(for: order)
|
orderCard(order)
|
||||||
NavigationLink {
|
|
||||||
OrderEntryDestinationView(
|
|
||||||
orderId: trackingId,
|
|
||||||
initialShortId: order.shortId,
|
|
||||||
fallbackPaymentMethod: order.paymentMethod,
|
|
||||||
fallbackTotal: order.total
|
|
||||||
)
|
|
||||||
} label: {
|
|
||||||
orderRow(order)
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
.padding(14)
|
|
||||||
.background(AppColors.surface)
|
|
||||||
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,103 +53,148 @@ struct OrdersView: View {
|
|||||||
await loadOrders(force: true)
|
await loadOrders(force: true)
|
||||||
await refreshStoreRatings()
|
await refreshStoreRatings()
|
||||||
}
|
}
|
||||||
|
.navigationDestination(item: $selectedOrderRoute) { context in
|
||||||
|
OrderEntryDestinationView(
|
||||||
|
orderId: context.orderId,
|
||||||
|
initialShortId: context.shortId,
|
||||||
|
fallbackPaymentMethod: context.paymentMethod,
|
||||||
|
fallbackTotal: context.total
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func orderRow(_ order: AppOrderSummary) -> some View {
|
private func orderCard(_ order: AppOrderSummary) -> some View {
|
||||||
HStack(spacing: 12) {
|
let status = orderVisualStatus(for: order)
|
||||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
let route = OrderRouteContext(
|
||||||
.fill(AppColors.brandSoft)
|
orderId: trackingOrderId(for: order),
|
||||||
.frame(width: 44, height: 44)
|
shortId: order.shortId,
|
||||||
.overlay(
|
paymentMethod: order.paymentMethod,
|
||||||
Image(systemName: "bag.fill")
|
total: order.total
|
||||||
.foregroundStyle(AppColors.primary)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
return VStack(alignment: .leading, spacing: 14) {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
AsyncStoreImage(imageURL: resolvedMediaURL(order.storeLogoURL))
|
||||||
|
.frame(width: 88, height: 88)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 24, style: .continuous))
|
||||||
|
.background(AppColors.brandSoft, in: RoundedRectangle(cornerRadius: 24, style: .continuous))
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
Text(order.storeName?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false ? (order.storeName ?? "Pedido") : "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.heading2)
|
||||||
.foregroundStyle(AppColors.textPrimary)
|
.foregroundStyle(AppColors.textPrimary)
|
||||||
|
.lineLimit(1)
|
||||||
|
|
||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
Text(humanReadableStatus(for: order))
|
Text(orderMetaText(order))
|
||||||
.font(AppTypography.caption)
|
.font(AppTypography.body)
|
||||||
.foregroundStyle(AppColors.textMuted)
|
.foregroundStyle(AppColors.textMuted)
|
||||||
|
.lineLimit(1)
|
||||||
if let rating = storeRating(for: order) {
|
if let rating = storeRating(for: order) {
|
||||||
Text("•")
|
Text("•")
|
||||||
.font(AppTypography.caption)
|
.font(AppTypography.body)
|
||||||
.foregroundStyle(AppColors.textMuted)
|
.foregroundStyle(AppColors.textMuted)
|
||||||
HStack(spacing: 4) {
|
|
||||||
Image(systemName: "star.fill")
|
Image(systemName: "star.fill")
|
||||||
.font(.system(size: 11, weight: .bold))
|
.font(.system(size: 11, weight: .bold))
|
||||||
.foregroundStyle(Color(hex: "#7CF02A"))
|
.foregroundStyle(Color(hex: "#7CF02A"))
|
||||||
Text(String(format: "%.1f", rating).replacingOccurrences(of: ".", with: ","))
|
Text(String(format: "%.1f", rating).replacingOccurrences(of: ".", with: ","))
|
||||||
.font(AppTypography.caption)
|
.font(AppTypography.body)
|
||||||
.foregroundStyle(AppColors.textMuted)
|
.foregroundStyle(AppColors.textMuted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
Spacer(minLength: 0)
|
||||||
|
|
||||||
VStack(alignment: .trailing, spacing: 4) {
|
Text(status.badgeTitle)
|
||||||
Text(formatCurrency(order.total ?? 0))
|
|
||||||
.font(AppTypography.heading3)
|
|
||||||
.foregroundStyle(AppColors.textPrimary)
|
|
||||||
if let createdAt = formatDate(order.createdAt) {
|
|
||||||
Text(createdAt)
|
|
||||||
.font(AppTypography.caption)
|
.font(AppTypography.caption)
|
||||||
.foregroundStyle(AppColors.textMuted)
|
.foregroundStyle(status.badgeForeground)
|
||||||
}
|
.padding(.horizontal, 10)
|
||||||
}
|
.padding(.vertical, 6)
|
||||||
}
|
.background(status.badgeBackground)
|
||||||
|
.clipShape(Capsule())
|
||||||
}
|
}
|
||||||
|
|
||||||
private func humanReadableStatus(for order: AppOrderSummary) -> String {
|
Divider()
|
||||||
|
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
Button(status.isCanceled ? "Ajuda" : "Ver Detalhes") {
|
||||||
|
selectedOrderRoute = route
|
||||||
|
}
|
||||||
|
.font(AppTypography.heading2)
|
||||||
|
.foregroundStyle(AppColors.textMuted)
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
|
||||||
|
Spacer(minLength: 12)
|
||||||
|
|
||||||
|
Button {
|
||||||
|
if status.isInProgress {
|
||||||
|
selectedOrderRoute = route
|
||||||
|
return
|
||||||
|
}
|
||||||
|
SnackbarCenter.shared.show(
|
||||||
|
title: "Recompra será integrada com o catálogo em breve.",
|
||||||
|
style: .info,
|
||||||
|
icon: "cart.badge.plus",
|
||||||
|
duration: 2.0
|
||||||
|
)
|
||||||
|
} label: {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Image(systemName: status.isInProgress ? "truck.box.fill" : "arrow.clockwise")
|
||||||
|
Text(status.isInProgress ? "Acompanhar" : "Pedir Novamente")
|
||||||
|
.font(AppTypography.heading2)
|
||||||
|
}
|
||||||
|
.foregroundStyle(status.actionForeground)
|
||||||
|
.padding(.horizontal, 24)
|
||||||
|
.padding(.vertical, 12)
|
||||||
|
.background(status.actionBackground)
|
||||||
|
.clipShape(Capsule())
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(18)
|
||||||
|
.background(AppColors.surface)
|
||||||
|
.overlay(alignment: .leading) {
|
||||||
|
if status.isInProgress {
|
||||||
|
RoundedRectangle(cornerRadius: 3, style: .continuous)
|
||||||
|
.fill(Color(hex: "#C8F06E"))
|
||||||
|
.frame(width: 5)
|
||||||
|
.padding(.vertical, 20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 28, style: .continuous))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func orderVisualStatus(for order: AppOrderSummary) -> OrderRowStatusStyle {
|
||||||
let explicitLabel = (order.statusLabel ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
let explicitLabel = (order.statusLabel ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
if explicitLabel.isEmpty == false {
|
if explicitLabel.isEmpty == false {
|
||||||
return explicitLabel
|
let normalizedLabel = explicitLabel.uppercased()
|
||||||
|
if normalizedLabel.contains("CANCEL") { return .canceled }
|
||||||
|
if normalizedLabel.contains("ENTREG") || normalizedLabel.contains("COMPLET") { return .delivered }
|
||||||
}
|
}
|
||||||
|
|
||||||
let detailed = (order.statusDetailed ?? "").uppercased()
|
let normalized = ((order.statusDetailed ?? order.status) ?? "").uppercased()
|
||||||
switch detailed {
|
if normalized.contains("CANCEL") {
|
||||||
case "PENDING_PAYMENT":
|
return .canceled
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
if normalized.contains("COMPLETED") || normalized.contains("DELIVERED") {
|
||||||
let raw = order.status ?? order.paymentStatus ?? "Pendente"
|
return .delivered
|
||||||
let normalized = raw.uppercased()
|
}
|
||||||
if normalized.contains("PAYMENT_PENDING") { return "Aguardando pagamento" }
|
return .inProgress
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func formatCurrency(_ value: Double) -> String {
|
private func formatCurrency(_ value: Double) -> String {
|
||||||
String(format: "R$ %.2f", value).replacingOccurrences(of: ".", with: ",")
|
String(format: "R$ %.2f", value).replacingOccurrences(of: ".", with: ",")
|
||||||
}
|
}
|
||||||
|
|
||||||
private func formatDate(_ isoValue: String?) -> String? {
|
private func orderMetaText(_ order: AppOrderSummary) -> String {
|
||||||
|
let dateText = formatOrderDate(order.createdAt) ?? "Agora"
|
||||||
|
let totalText = formatCurrency(order.total ?? 0)
|
||||||
|
return "\(dateText) • \(totalText)"
|
||||||
|
}
|
||||||
|
|
||||||
|
private func formatOrderDate(_ isoValue: String?) -> String? {
|
||||||
guard let isoValue, isoValue.isEmpty == false else { return nil }
|
guard let isoValue, isoValue.isEmpty == false else { return nil }
|
||||||
let iso = ISO8601DateFormatter()
|
let iso = ISO8601DateFormatter()
|
||||||
iso.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
iso.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||||
@@ -175,7 +207,7 @@ struct OrdersView: View {
|
|||||||
|
|
||||||
let formatter = DateFormatter()
|
let formatter = DateFormatter()
|
||||||
formatter.locale = Locale(identifier: "pt_BR")
|
formatter.locale = Locale(identifier: "pt_BR")
|
||||||
formatter.dateFormat = "dd/MM HH:mm"
|
formatter.dateFormat = "dd MMM, HH:mm"
|
||||||
return formatter.string(from: date)
|
return formatter.string(from: date)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,7 +255,6 @@ struct OrdersView: View {
|
|||||||
trackedMapped = cachedTracked.map {
|
trackedMapped = cachedTracked.map {
|
||||||
AppOrderSummary.fromTracked($0)
|
AppOrderSummary.fromTracked($0)
|
||||||
}
|
}
|
||||||
// Use tracked-only list as bootstrap data only on first load.
|
|
||||||
if hasLoadedOnce == false, previousOrders.isEmpty {
|
if hasLoadedOnce == false, previousOrders.isEmpty {
|
||||||
orders = mergeOrders(apiOrders: [], trackedOrders: trackedMapped)
|
orders = mergeOrders(apiOrders: [], trackedOrders: trackedMapped)
|
||||||
}
|
}
|
||||||
@@ -269,7 +300,6 @@ struct OrdersView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// API order is authoritative for fallback ordering (usually newest first).
|
|
||||||
for (index, item) in apiOrders.enumerated() {
|
for (index, item) in apiOrders.enumerated() {
|
||||||
let key = identityKey(for: item)
|
let key = identityKey(for: item)
|
||||||
map[key] = item
|
map[key] = item
|
||||||
@@ -404,8 +434,66 @@ struct OrdersView: View {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func resolvedMediaURL(_ raw: String?) -> String? {
|
||||||
|
ImageSourceResolver.resolve(raw)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct OrderRouteContext: Identifiable, Hashable {
|
||||||
|
var id: String { orderId }
|
||||||
|
let orderId: String
|
||||||
|
let shortId: String?
|
||||||
|
let paymentMethod: String?
|
||||||
|
let total: Double?
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum OrderRowStatusStyle {
|
||||||
|
case delivered
|
||||||
|
case inProgress
|
||||||
|
case canceled
|
||||||
|
|
||||||
|
var badgeTitle: String {
|
||||||
|
switch self {
|
||||||
|
case .delivered: return "Entregue"
|
||||||
|
case .inProgress: return "Em andamento"
|
||||||
|
case .canceled: return "Cancelado"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var badgeForeground: Color {
|
||||||
|
switch self {
|
||||||
|
case .delivered: return Color(hex: "#16843B")
|
||||||
|
case .inProgress: return Color(hex: "#B06A28")
|
||||||
|
case .canceled: return Color(hex: "#D62828")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var badgeBackground: Color {
|
||||||
|
switch self {
|
||||||
|
case .delivered: return Color(hex: "#E8F7E9")
|
||||||
|
case .inProgress: return Color(hex: "#FFF2E5")
|
||||||
|
case .canceled: return Color(hex: "#FDECEC")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var actionForeground: Color {
|
||||||
|
switch self {
|
||||||
|
case .inProgress: return .white
|
||||||
|
case .delivered, .canceled: return Color(hex: "#0E1A06")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var actionBackground: Color {
|
||||||
|
switch self {
|
||||||
|
case .inProgress: return Color(hex: "#111216")
|
||||||
|
case .delivered, .canceled: return Color(hex: "#C8F06E")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isInProgress: Bool { self == .inProgress }
|
||||||
|
var isCanceled: Bool { self == .canceled }
|
||||||
|
}
|
||||||
extension AppOrderSummary {
|
extension AppOrderSummary {
|
||||||
static func fromTracked(_ tracked: PublicOrderResult) -> AppOrderSummary {
|
static func fromTracked(_ tracked: PublicOrderResult) -> AppOrderSummary {
|
||||||
AppOrderSummary(
|
AppOrderSummary(
|
||||||
|
|||||||
Reference in New Issue
Block a user