From a8e3631177af2983caa18bdd379e41fe3f2f1a58 Mon Sep 17 00:00:00 2001 From: Daniel Arantes Loverde Date: Thu, 5 Mar 2026 10:10:03 -0300 Subject: [PATCH] feat(orders): separar reviews e criar detalhe de pedido --- .../Services/ApiOrderTrackingModels.swift | 251 ++++++++++++++ .../Views/Main/OrderDetailsView.swift | 321 ++++++++++++++++++ .../PediFoods/Views/Main/OrdersView.swift | 14 + 3 files changed, 586 insertions(+) create mode 100644 pedi-foods/Sources/PediFoods/Views/Main/OrderDetailsView.swift diff --git a/pedi-foods/Sources/PediFoods/Services/ApiOrderTrackingModels.swift b/pedi-foods/Sources/PediFoods/Services/ApiOrderTrackingModels.swift index 2ba9699..ce04e0f 100644 --- a/pedi-foods/Sources/PediFoods/Services/ApiOrderTrackingModels.swift +++ b/pedi-foods/Sources/PediFoods/Services/ApiOrderTrackingModels.swift @@ -4,6 +4,7 @@ struct AppOrderSummary: Decodable, Identifiable { let id: String let orderId: String? let realId: String? + let storeId: String? let shortId: String? let total: Double? let status: String? @@ -14,6 +15,7 @@ struct AppOrderSummary: Decodable, Identifiable { let paymentMethod: String? let deliveryType: String? let storeName: String? + let storeLogoURL: String? let createdAt: String? let updatedAt: String? @@ -21,6 +23,8 @@ struct AppOrderSummary: Decodable, Identifiable { case id case orderId case realId + case storeId + case store_id case shortId case total case status @@ -31,6 +35,9 @@ struct AppOrderSummary: Decodable, Identifiable { case paymentMethod case deliveryType case storeName + case storeLogo + case store_logo + case logo case date case createdAt case updatedAt @@ -41,6 +48,7 @@ struct AppOrderSummary: Decodable, Identifiable { orderId = ApiService.decodeFlexibleString(from: container, keys: [.orderId]) id = ApiService.decodeFlexibleString(from: container, keys: [.orderId, .realId, .id]) ?? UUID().uuidString realId = ApiService.decodeFlexibleString(from: container, keys: [.realId]) + storeId = ApiService.decodeFlexibleString(from: container, keys: [.storeId, .store_id]) shortId = ApiService.decodeFlexibleString(from: container, keys: [.shortId]) total = ApiService.decodeFlexibleDouble(from: container, keys: [.total]) status = ApiService.decodeFlexibleString(from: container, keys: [.status]) @@ -51,6 +59,7 @@ struct AppOrderSummary: Decodable, Identifiable { paymentMethod = ApiService.decodeFlexibleString(from: container, keys: [.paymentMethod]) deliveryType = ApiService.decodeFlexibleString(from: container, keys: [.deliveryType]) storeName = ApiService.decodeFlexibleString(from: container, keys: [.storeName]) + storeLogoURL = ApiService.decodeFlexibleString(from: container, keys: [.storeLogo, .store_logo, .logo]) let fallbackDate = ApiService.decodeFlexibleString(from: container, keys: [.date]) createdAt = ApiService.decodeFlexibleString(from: container, keys: [.createdAt]) ?? fallbackDate updatedAt = ApiService.decodeFlexibleString(from: container, keys: [.updatedAt]) ?? fallbackDate @@ -61,6 +70,7 @@ struct PublicOrderResult: Codable, Identifiable { let id: String let shortId: String? let realId: String? + let storeId: String? let status: String? let paymentStatus: String? let paymentConfirmed: Bool? @@ -68,16 +78,20 @@ struct PublicOrderResult: Codable, Identifiable { let paymentMethodCode: String? let paymentPayload: CreateOrderPaymentPayload? let payment: CreateOrderPaymentInfo? + let nextAction: String? let deliveryType: String? let deliveryTypeLabel: String? let total: Double? let storeName: String? + let storeLogoURL: String? let createdAt: String? let updatedAt: String? let otp: String? let customerOtp: String? let confirmOtp: String? let cancellationReason: String? + let deliveryAddress: PublicOrderDeliveryAddress? + let review: PublicOrderReview? let items: [PublicOrderItem] let timeline: [PublicOrderTimelineEvent] @@ -85,6 +99,8 @@ struct PublicOrderResult: Codable, Identifiable { case id case shortId case realId + case storeId + case store_id case status case paymentStatus case paymentConfirmed @@ -92,16 +108,27 @@ struct PublicOrderResult: Codable, Identifiable { case paymentMethodCode case paymentPayload case payment + case nextAction case deliveryType case deliveryTypeLabel case total case storeName + case storeLogo + case store_logo + case logo case createdAt case updatedAt case otp case customerOtp case confirmOtp case cancellationReason + case address + case deliveryAddress + case delivery_address + case customerAddress + case customer_address + case review + case orderReview case items case timeline } @@ -110,6 +137,7 @@ struct PublicOrderResult: Codable, Identifiable { id: String, shortId: String? = nil, realId: String? = nil, + storeId: String? = nil, status: String? = nil, paymentStatus: String? = nil, paymentConfirmed: Bool? = nil, @@ -117,22 +145,27 @@ struct PublicOrderResult: Codable, Identifiable { paymentMethodCode: String? = nil, paymentPayload: CreateOrderPaymentPayload? = nil, payment: CreateOrderPaymentInfo? = nil, + nextAction: String? = nil, deliveryType: String? = nil, deliveryTypeLabel: String? = nil, total: Double? = nil, storeName: String? = nil, + storeLogoURL: String? = nil, createdAt: String? = nil, updatedAt: String? = nil, otp: String? = nil, customerOtp: String? = nil, confirmOtp: String? = nil, cancellationReason: String? = nil, + deliveryAddress: PublicOrderDeliveryAddress? = nil, + review: PublicOrderReview? = nil, items: [PublicOrderItem] = [], timeline: [PublicOrderTimelineEvent] = [] ) { self.id = id self.shortId = shortId self.realId = realId + self.storeId = storeId self.status = status self.paymentStatus = paymentStatus self.paymentConfirmed = paymentConfirmed @@ -140,16 +173,20 @@ struct PublicOrderResult: Codable, Identifiable { self.paymentMethodCode = paymentMethodCode self.paymentPayload = paymentPayload self.payment = payment + self.nextAction = nextAction self.deliveryType = deliveryType self.deliveryTypeLabel = deliveryTypeLabel self.total = total self.storeName = storeName + self.storeLogoURL = storeLogoURL self.createdAt = createdAt self.updatedAt = updatedAt self.otp = otp self.customerOtp = customerOtp self.confirmOtp = confirmOtp self.cancellationReason = cancellationReason + self.deliveryAddress = deliveryAddress + self.review = review self.items = items self.timeline = timeline } @@ -159,11 +196,13 @@ struct PublicOrderResult: Codable, Identifiable { id = ApiService.decodeFlexibleString(from: container, keys: [.id]) ?? UUID().uuidString shortId = ApiService.decodeFlexibleString(from: container, keys: [.shortId]) realId = ApiService.decodeFlexibleString(from: container, keys: [.realId]) + storeId = ApiService.decodeFlexibleString(from: container, keys: [.storeId, .store_id]) status = ApiService.decodeFlexibleString(from: container, keys: [.status]) paymentStatus = ApiService.decodeFlexibleString(from: container, keys: [.paymentStatus]) paymentConfirmed = try? container.decode(Bool.self, forKey: .paymentConfirmed) paymentMethod = ApiService.decodeFlexibleString(from: container, keys: [.paymentMethod]) paymentMethodCode = ApiService.decodeFlexibleString(from: container, keys: [.paymentMethodCode]) + nextAction = ApiService.decodeFlexibleString(from: container, keys: [.nextAction]) payment = try? container.decode(CreateOrderPaymentInfo.self, forKey: .payment) if let objectPayload = try? container.decode(CreateOrderPaymentPayload.self, forKey: .paymentPayload) { paymentPayload = objectPayload @@ -180,16 +219,54 @@ struct PublicOrderResult: Codable, Identifiable { deliveryTypeLabel = ApiService.decodeFlexibleString(from: container, keys: [.deliveryTypeLabel]) total = ApiService.decodeFlexibleDouble(from: container, keys: [.total]) storeName = ApiService.decodeFlexibleString(from: container, keys: [.storeName]) + storeLogoURL = ApiService.decodeFlexibleString(from: container, keys: [.storeLogo, .store_logo, .logo]) createdAt = ApiService.decodeFlexibleString(from: container, keys: [.createdAt]) updatedAt = ApiService.decodeFlexibleString(from: container, keys: [.updatedAt]) otp = ApiService.decodeFlexibleString(from: container, keys: [.otp]) customerOtp = ApiService.decodeFlexibleString(from: container, keys: [.customerOtp]) confirmOtp = ApiService.decodeFlexibleString(from: container, keys: [.confirmOtp]) cancellationReason = ApiService.decodeFlexibleString(from: container, keys: [.cancellationReason]) + deliveryAddress = (try? container.decode(PublicOrderDeliveryAddress.self, forKey: .address)) + ?? (try? container.decode(PublicOrderDeliveryAddress.self, forKey: .deliveryAddress)) + ?? (try? container.decode(PublicOrderDeliveryAddress.self, forKey: .delivery_address)) + ?? (try? container.decode(PublicOrderDeliveryAddress.self, forKey: .customerAddress)) + ?? (try? container.decode(PublicOrderDeliveryAddress.self, forKey: .customer_address)) + review = (try? container.decode(PublicOrderReview.self, forKey: .review)) + ?? (try? container.decode(PublicOrderReview.self, forKey: .orderReview)) items = (try? container.decode([PublicOrderItem].self, forKey: .items)) ?? [] timeline = (try? container.decode([PublicOrderTimelineEvent].self, forKey: .timeline)) ?? [] } + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encodeIfPresent(shortId, forKey: .shortId) + try container.encodeIfPresent(realId, forKey: .realId) + try container.encodeIfPresent(storeId, forKey: .storeId) + try container.encodeIfPresent(status, forKey: .status) + try container.encodeIfPresent(paymentStatus, forKey: .paymentStatus) + try container.encodeIfPresent(paymentConfirmed, forKey: .paymentConfirmed) + try container.encodeIfPresent(paymentMethod, forKey: .paymentMethod) + try container.encodeIfPresent(paymentMethodCode, forKey: .paymentMethodCode) + try container.encodeIfPresent(paymentPayload, forKey: .paymentPayload) + try container.encodeIfPresent(payment, forKey: .payment) + try container.encodeIfPresent(nextAction, forKey: .nextAction) + try container.encodeIfPresent(deliveryType, forKey: .deliveryType) + try container.encodeIfPresent(deliveryTypeLabel, forKey: .deliveryTypeLabel) + try container.encodeIfPresent(total, forKey: .total) + try container.encodeIfPresent(storeName, forKey: .storeName) + try container.encodeIfPresent(createdAt, forKey: .createdAt) + try container.encodeIfPresent(updatedAt, forKey: .updatedAt) + try container.encodeIfPresent(otp, forKey: .otp) + try container.encodeIfPresent(customerOtp, forKey: .customerOtp) + try container.encodeIfPresent(confirmOtp, forKey: .confirmOtp) + try container.encodeIfPresent(cancellationReason, forKey: .cancellationReason) + try container.encodeIfPresent(deliveryAddress, forKey: .address) + try container.encodeIfPresent(review, forKey: .review) + try container.encode(items, forKey: .items) + try container.encode(timeline, forKey: .timeline) + } + var displayOtpCode: String? { let values = [customerOtp, otp, confirmOtp] for value in values { @@ -251,6 +328,180 @@ struct PublicOrderResult: Codable, Identifiable { } } +struct PublicOrderDeliveryAddress: Codable { + let label: String? + let street: String? + let number: String? + let neighborhood: String? + let city: String? + let state: String? + let zip: String? + let complement: String? + + enum CodingKeys: String, CodingKey { + case label + case street + case address + case number + case neighborhood + case district + case city + case state + case zip + case zipCode + case zipcode + case complement + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + label = ApiService.decodeFlexibleString(from: container, keys: [.label]) + street = ApiService.decodeFlexibleString(from: container, keys: [.street, .address]) + number = ApiService.decodeFlexibleString(from: container, keys: [.number]) + neighborhood = ApiService.decodeFlexibleString(from: container, keys: [.neighborhood, .district]) + city = ApiService.decodeFlexibleString(from: container, keys: [.city]) + state = ApiService.decodeFlexibleString(from: container, keys: [.state]) + zip = ApiService.decodeFlexibleString(from: container, keys: [.zip, .zipCode, .zipcode]) + complement = ApiService.decodeFlexibleString(from: container, keys: [.complement]) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(label, forKey: .label) + try container.encodeIfPresent(street, forKey: .street) + try container.encodeIfPresent(number, forKey: .number) + try container.encodeIfPresent(neighborhood, forKey: .neighborhood) + try container.encodeIfPresent(city, forKey: .city) + try container.encodeIfPresent(state, forKey: .state) + try container.encodeIfPresent(zip, forKey: .zip) + try container.encodeIfPresent(complement, forKey: .complement) + } +} + +struct PublicOrderReview: Codable { + let id: String? + let orderId: String? + let rate: Int? + let message: String? + let orderRate: Int? + let orderComment: String? + let orderPositiveTags: [String]? + let orderImprovementTags: [String]? + let deliverySentiment: String? + let deliveryPositiveTags: [String]? + let deliveryNegativeTags: [String]? + let appNps: Int? + let platform: String? + let date: String? + + enum CodingKeys: String, CodingKey { + case id + case orderId + case rate + case message + case orderRate + case orderComment + case orderPositiveTags + case orderImprovementTags + case itemFeedback + case improvementFeedback + case deliverySentiment + case deliveryFeedback + case deliveryPositiveTags + case deliveryNegativeTags + case appNps + case app_nps + case platform + case date + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = ApiService.decodeFlexibleString(from: container, keys: [.id]) + orderId = ApiService.decodeFlexibleString(from: container, keys: [.orderId]) + rate = ApiService.decodeFlexibleInt(from: container, keys: [.rate]) + message = ApiService.decodeFlexibleString(from: container, keys: [.message]) + orderRate = ApiService.decodeFlexibleInt(from: container, keys: [.orderRate, .rate]) + orderComment = ApiService.decodeFlexibleString(from: container, keys: [.orderComment, .message]) + orderPositiveTags = Self.decodeStringList( + from: container, + keys: [.orderPositiveTags, .itemFeedback] + ) + orderImprovementTags = Self.decodeStringList( + from: container, + keys: [.orderImprovementTags, .improvementFeedback] + ) + deliverySentiment = ApiService.decodeFlexibleString(from: container, keys: [.deliverySentiment, .deliveryFeedback]) + deliveryPositiveTags = Self.decodeStringList(from: container, keys: [.deliveryPositiveTags]) + deliveryNegativeTags = Self.decodeStringList(from: container, keys: [.deliveryNegativeTags]) + appNps = Self.decodeNps(from: container, keys: [.appNps, .app_nps]) + platform = ApiService.decodeFlexibleString(from: container, keys: [.platform]) + date = ApiService.decodeFlexibleString(from: container, keys: [.date]) + } + + private static func decodeStringList( + from container: KeyedDecodingContainer, + keys: [CodingKeys] + ) -> [String]? { + for key in keys { + if let list = try? container.decode([String].self, forKey: key) { + return list + } + if let single = try? container.decode(String.self, forKey: key) { + let normalized = single.trimmingCharacters(in: .whitespacesAndNewlines) + if normalized.isEmpty == false { + return [normalized] + } + } + } + return nil + } + + private static func decodeNps( + from container: KeyedDecodingContainer, + keys: [CodingKeys] + ) -> Int? { + for key in keys { + if let value = try? container.decode(Int.self, forKey: key) { + return value + } + if let value = try? container.decode(Double.self, forKey: key) { + return Int(value.rounded()) + } + if let raw = try? container.decode(String.self, forKey: key) { + let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) + if trimmed.isEmpty { continue } + if let asInt = Int(trimmed) { + return asInt + } + let normalized = trimmed.replacingOccurrences(of: ",", with: ".") + if let asDouble = Double(normalized) { + return Int(asDouble.rounded()) + } + } + } + return nil + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(id, forKey: .id) + try container.encodeIfPresent(orderId, forKey: .orderId) + try container.encodeIfPresent(rate, forKey: .rate) + try container.encodeIfPresent(message, forKey: .message) + try container.encodeIfPresent(orderRate, forKey: .orderRate) + try container.encodeIfPresent(orderComment, forKey: .orderComment) + try container.encodeIfPresent(orderPositiveTags, forKey: .orderPositiveTags) + try container.encodeIfPresent(orderImprovementTags, forKey: .orderImprovementTags) + try container.encodeIfPresent(deliverySentiment, forKey: .deliverySentiment) + try container.encodeIfPresent(deliveryPositiveTags, forKey: .deliveryPositiveTags) + try container.encodeIfPresent(deliveryNegativeTags, forKey: .deliveryNegativeTags) + try container.encodeIfPresent(appNps, forKey: .appNps) + try container.encodeIfPresent(platform, forKey: .platform) + try container.encodeIfPresent(date, forKey: .date) + } +} + struct PublicOrderItem: Codable, Identifiable { let id: String let name: String? diff --git a/pedi-foods/Sources/PediFoods/Views/Main/OrderDetailsView.swift b/pedi-foods/Sources/PediFoods/Views/Main/OrderDetailsView.swift new file mode 100644 index 0000000..4568e9b --- /dev/null +++ b/pedi-foods/Sources/PediFoods/Views/Main/OrderDetailsView.swift @@ -0,0 +1,321 @@ +import SwiftUI + +struct OrderDetailsView: View { + let order: PublicOrderResult + let orderId: String + let initialShortId: String? + + var body: some View { + ScrollView(showsIndicators: false) { + VStack(spacing: 14) { + statusCard + storeCard + itemsCard + totalsCard + if hasAddressInfo { + addressCard + } + reorderButton + helpFooter + } + .padding(.horizontal, 20) + .padding(.top, 14) + .padding(.bottom, UIDevice.bottomNotch + 24) + } + .background(AppColors.backgroundLight) + .navigationTitle("Detalhes do Pedido") + .navigationBarTitleDisplayMode(.inline) + } + + private var statusCard: some View { + HStack(spacing: 14) { + Circle() + .fill(AppColors.brandSoft) + .frame(width: 54, height: 54) + .overlay( + Image(systemName: statusIcon) + .font(.system(size: 22, weight: .bold)) + .foregroundStyle(statusColor) + ) + + VStack(alignment: .leading, spacing: 2) { + Text(statusTitle) + .font(AppTypography.heading3) + .foregroundStyle(AppColors.textPrimary) + + Text(statusDateText) + .font(AppTypography.body) + .foregroundStyle(AppColors.textMuted) + } + + Spacer(minLength: 0) + } + .padding(16) + .background(AppColors.surface) + .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous)) + } + + private var storeCard: some View { + HStack(spacing: 12) { + AsyncStoreImage(imageURL: resolvedMediaURL(order.storeLogoURL)) + .frame(width: 54, height: 54) + .clipShape(Circle()) + .background(AppColors.brandSoft, in: Circle()) + + VStack(alignment: .leading, spacing: 4) { + Text(order.storeName?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false ? (order.storeName ?? "Loja") : "Loja") + .font(AppTypography.heading2) + .foregroundStyle(AppColors.textPrimary) + Text("Pedido #\(displayOrderTitle)") + .font(AppTypography.body) + .foregroundStyle(AppColors.textMuted) + } + + Spacer(minLength: 0) + + Text("Ver loja") + .font(AppTypography.body) + .foregroundStyle(Color(hex: "#A5D645")) + } + .padding(16) + .background(AppColors.surface) + .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous)) + } + + private var itemsCard: some View { + VStack(alignment: .leading, spacing: 14) { + Text("Itens do Pedido") + .font(AppTypography.heading2) + .foregroundStyle(AppColors.textPrimary) + + ForEach(order.items) { item in + HStack(alignment: .top, spacing: 12) { + Text("\(max(1, item.qty ?? 1))") + .font(AppTypography.heading3) + .foregroundStyle(AppColors.textPrimary) + .frame(width: 30, height: 30) + .background(AppColors.backgroundLight) + .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) + + VStack(alignment: .leading, spacing: 2) { + Text(item.name?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false ? (item.name ?? "Item") : "Item") + .font(AppTypography.heading3) + .foregroundStyle(AppColors.textPrimary) + } + + Spacer(minLength: 0) + + if let price = item.price { + Text(formatCurrency(price)) + .font(AppTypography.heading3) + .foregroundStyle(AppColors.textPrimary) + } + } + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(16) + .background(AppColors.surface) + .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous)) + } + + private var totalsCard: some View { + VStack(alignment: .leading, spacing: 10) { + Text("Resumo de Valores") + .font(AppTypography.heading2) + .foregroundStyle(AppColors.textPrimary) + + HStack { + Text("Subtotal") + .font(AppTypography.body) + .foregroundStyle(AppColors.textMuted) + Spacer() + Text(formatCurrency(subtotal)) + .font(AppTypography.body) + .foregroundStyle(AppColors.textMuted) + } + + Divider() + + HStack { + Text("Total") + .font(AppTypography.heading2) + .foregroundStyle(AppColors.textPrimary) + Spacer() + Text(formatCurrency(order.total ?? subtotal)) + .font(AppTypography.heading1) + .foregroundStyle(AppColors.textPrimary) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(16) + .background(AppColors.surface) + .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous)) + } + + private var addressCard: some View { + VStack(alignment: .leading, spacing: 12) { + HStack(spacing: 10) { + Circle() + .fill(AppColors.backgroundLight) + .frame(width: 34, height: 34) + .overlay( + Image(systemName: "mappin.circle.fill") + .font(.system(size: 18, weight: .semibold)) + .foregroundStyle(AppColors.textMuted) + ) + Text("ENDEREÇO DE ENTREGA") + .font(AppTypography.caption) + .foregroundStyle(AppColors.textMuted) + Spacer(minLength: 0) + } + + Text(deliveryAddressLine) + .font(AppTypography.body) + .foregroundStyle(AppColors.textPrimary) + + if deliveryAddressLine2.isEmpty == false { + Text(deliveryAddressLine2) + .font(AppTypography.body) + .foregroundStyle(AppColors.textMuted) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(16) + .background(AppColors.surface) + .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous)) + } + + private var reorderButton: some View { + Button("Pedir Novamente") { + SnackbarCenter.shared.show( + title: "Recompra será integrada com o catálogo em breve.", + style: .info, + icon: "cart.badge.plus", + duration: 2.0 + ) + } + .font(AppTypography.heading2) + .foregroundStyle(Color(hex: "#0E1A06")) + .frame(maxWidth: .infinity, minHeight: 56) + .background(Color(hex: "#C8F06E")) + .clipShape(Capsule()) + .buttonStyle(.plain) + } + + private var helpFooter: some View { + Text("Precisa de ajuda com esse pedido?") + .font(AppTypography.body) + .foregroundStyle(Color(hex: "#A5D645")) + .frame(maxWidth: .infinity, alignment: .center) + .padding(.vertical, 8) + } + + private var subtotal: Double { + order.items.reduce(0) { partial, item in + partial + (Double(max(1, item.qty ?? 1)) * (item.price ?? 0)) + } + } + + private var hasAddressInfo: Bool { + deliveryAddressLine.isEmpty == false || deliveryAddressLine2.isEmpty == false + } + + private var deliveryAddressLine: String { + guard let address = order.deliveryAddress else { return "" } + let street = normalizedText(address.street) + let number = normalizedText(address.number) + let base = [street, number].filter { $0.isEmpty == false }.joined(separator: ", ") + if base.isEmpty == false { return base } + return normalizedText(address.label) + } + + private var deliveryAddressLine2: String { + guard let address = order.deliveryAddress else { return "" } + let neighborhood = normalizedText(address.neighborhood) + let city = normalizedText(address.city) + let state = normalizedText(address.state) + let zip = normalizedText(address.zip) + return [neighborhood, city, state, zip] + .filter { $0.isEmpty == false } + .joined(separator: " • ") + } + + private var statusTitle: String { + let status = normalized(order.status) + if status.contains("CANCEL") { return "Pedido cancelado" } + if status.contains("COMPLETED") || status.contains("DELIVERED") { + return normalized(order.deliveryType ?? order.deliveryTypeLabel).contains("PICKUP") ? "Pedido retirado" : "Pedido concluído" + } + if status.contains("DELIVER") || status.contains("ROTA") { return "Pedido em rota" } + if status.contains("READY") { return "Pedido pronto" } + if status.contains("PREPAR") { return "Pedido em produção" } + return "Pedido confirmado" + } + + private var statusDateText: String { + if let formatted = formatDate(order.updatedAt ?? order.createdAt) { + return "\(statusDatePrefix) \(formatted)" + } + return statusDatePrefix + } + + private var statusDatePrefix: String { + if statusTitle.contains("cancelado") { return "Cancelado em" } + if statusTitle.contains("retirado") { return "Retirado em" } + if statusTitle.contains("concluído") { return "Entregue em" } + return "Atualizado em" + } + + private var statusIcon: String { + statusTitle.contains("cancelado") ? "xmark" : "checkmark" + } + + private var statusColor: Color { + statusTitle.contains("cancelado") ? Color.red : AppColors.primary + } + + private var displayOrderTitle: String { + if let short = order.shortId, short.isEmpty == false { return short } + let orderIdValue = order.id.trimmingCharacters(in: .whitespacesAndNewlines) + if orderIdValue.isEmpty == false { return orderIdValue } + if let initialShortId, initialShortId.isEmpty == false { return initialShortId } + return String(orderId.prefix(6)) + } + + private func formatCurrency(_ value: Double) -> String { + String(format: "R$ %.2f", value).replacingOccurrences(of: ".", with: ",") + } + + private func formatDate(_ isoValue: String?) -> String? { + guard let isoValue, isoValue.isEmpty == false else { return nil } + let iso = ISO8601DateFormatter() + iso.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + var date = iso.date(from: isoValue) + if date == nil { + iso.formatOptions = [.withInternetDateTime] + date = iso.date(from: isoValue) + } + guard let date else { return nil } + + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "pt_BR") + formatter.dateFormat = "dd MMM, HH:mm" + return formatter.string(from: date) + } + + private func normalized(_ value: String?) -> String { + (value ?? "") + .folding(options: [.diacriticInsensitive, .caseInsensitive], locale: .current) + .uppercased() + .trimmingCharacters(in: .whitespacesAndNewlines) + } + + private func resolvedMediaURL(_ raw: String?) -> String? { + ImageSourceResolver.resolve(raw) + } + + private func normalizedText(_ value: String?) -> String { + (value ?? "").trimmingCharacters(in: .whitespacesAndNewlines) + } +} diff --git a/pedi-foods/Sources/PediFoods/Views/Main/OrdersView.swift b/pedi-foods/Sources/PediFoods/Views/Main/OrdersView.swift index f78c60d..86b666f 100644 --- a/pedi-foods/Sources/PediFoods/Views/Main/OrdersView.swift +++ b/pedi-foods/Sources/PediFoods/Views/Main/OrdersView.swift @@ -479,6 +479,7 @@ struct OrderEntryDestinationView: View { @State var pixContext: PixPaymentContext? = nil @State var cardContext: CardPaymentContext? = nil @State var orderTrackingContext: OrderTrackingContext? = nil + @State var orderDetails: PublicOrderResult? = nil var body: some View { Group { @@ -511,6 +512,8 @@ struct OrderEntryDestinationView: View { orderTrackingContext = OrderTrackingContext(orderId: cardContext.orderId, shortId: cardContext.shortId) } ) + } else if let orderDetails { + OrderDetailsView(order: orderDetails, orderId: orderId, initialShortId: initialShortId) } else { OrderTrackingView(orderId: orderId, initialShortId: initialShortId) } @@ -531,6 +534,10 @@ struct OrderEntryDestinationView: View { let order = await fetchOrderForRouting() guard let order else { return } + if shouldOpenOrderDetails(for: order) { + orderDetails = order + return + } guard shouldOpenPaymentScreen(for: order) else { return } let normalizedMethod = normalizePaymentMethod(order) @@ -592,6 +599,13 @@ struct OrderEntryDestinationView: View { return method == "PIX" || method == "CREDIT_CARD" || method == "DEBIT_CARD" } + func shouldOpenOrderDetails(for order: PublicOrderResult) -> Bool { + let status = normalize(order.status) + if status.contains("CANCEL") { return true } + if status.contains("COMPLETED") || status.contains("DELIVERED") { return true } + return false + } + func shouldOpenPaymentScreen(for order: PublicOrderResult) -> Bool { guard order.isPaymentConfirmed == false else { return false } guard isOnlinePaymentMethod(order) else { return false }