feat(orders): separar reviews e criar detalhe de pedido

This commit is contained in:
Daniel Arantes Loverde
2026-03-05 10:10:03 -03:00
parent 56d6ff7223
commit a8e3631177
3 changed files with 586 additions and 0 deletions

View File

@@ -4,6 +4,7 @@ struct AppOrderSummary: Decodable, Identifiable {
let id: String let id: String
let orderId: String? let orderId: String?
let realId: String? let realId: String?
let storeId: String?
let shortId: String? let shortId: String?
let total: Double? let total: Double?
let status: String? let status: String?
@@ -14,6 +15,7 @@ struct AppOrderSummary: Decodable, Identifiable {
let paymentMethod: String? let paymentMethod: String?
let deliveryType: String? let deliveryType: String?
let storeName: String? let storeName: String?
let storeLogoURL: String?
let createdAt: String? let createdAt: String?
let updatedAt: String? let updatedAt: String?
@@ -21,6 +23,8 @@ struct AppOrderSummary: Decodable, Identifiable {
case id case id
case orderId case orderId
case realId case realId
case storeId
case store_id
case shortId case shortId
case total case total
case status case status
@@ -31,6 +35,9 @@ struct AppOrderSummary: Decodable, Identifiable {
case paymentMethod case paymentMethod
case deliveryType case deliveryType
case storeName case storeName
case storeLogo
case store_logo
case logo
case date case date
case createdAt case createdAt
case updatedAt case updatedAt
@@ -41,6 +48,7 @@ struct AppOrderSummary: Decodable, Identifiable {
orderId = ApiService.decodeFlexibleString(from: container, keys: [.orderId]) orderId = ApiService.decodeFlexibleString(from: container, keys: [.orderId])
id = ApiService.decodeFlexibleString(from: container, keys: [.orderId, .realId, .id]) ?? UUID().uuidString id = ApiService.decodeFlexibleString(from: container, keys: [.orderId, .realId, .id]) ?? UUID().uuidString
realId = ApiService.decodeFlexibleString(from: container, keys: [.realId]) realId = ApiService.decodeFlexibleString(from: container, keys: [.realId])
storeId = ApiService.decodeFlexibleString(from: container, keys: [.storeId, .store_id])
shortId = ApiService.decodeFlexibleString(from: container, keys: [.shortId]) shortId = ApiService.decodeFlexibleString(from: container, keys: [.shortId])
total = ApiService.decodeFlexibleDouble(from: container, keys: [.total]) total = ApiService.decodeFlexibleDouble(from: container, keys: [.total])
status = ApiService.decodeFlexibleString(from: container, keys: [.status]) status = ApiService.decodeFlexibleString(from: container, keys: [.status])
@@ -51,6 +59,7 @@ struct AppOrderSummary: Decodable, Identifiable {
paymentMethod = ApiService.decodeFlexibleString(from: container, keys: [.paymentMethod]) paymentMethod = ApiService.decodeFlexibleString(from: container, keys: [.paymentMethod])
deliveryType = ApiService.decodeFlexibleString(from: container, keys: [.deliveryType]) deliveryType = ApiService.decodeFlexibleString(from: container, keys: [.deliveryType])
storeName = ApiService.decodeFlexibleString(from: container, keys: [.storeName]) 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]) let fallbackDate = ApiService.decodeFlexibleString(from: container, keys: [.date])
createdAt = ApiService.decodeFlexibleString(from: container, keys: [.createdAt]) ?? fallbackDate createdAt = ApiService.decodeFlexibleString(from: container, keys: [.createdAt]) ?? fallbackDate
updatedAt = ApiService.decodeFlexibleString(from: container, keys: [.updatedAt]) ?? fallbackDate updatedAt = ApiService.decodeFlexibleString(from: container, keys: [.updatedAt]) ?? fallbackDate
@@ -61,6 +70,7 @@ struct PublicOrderResult: Codable, Identifiable {
let id: String let id: String
let shortId: String? let shortId: String?
let realId: String? let realId: String?
let storeId: String?
let status: String? let status: String?
let paymentStatus: String? let paymentStatus: String?
let paymentConfirmed: Bool? let paymentConfirmed: Bool?
@@ -68,16 +78,20 @@ struct PublicOrderResult: Codable, Identifiable {
let paymentMethodCode: String? let paymentMethodCode: String?
let paymentPayload: CreateOrderPaymentPayload? let paymentPayload: CreateOrderPaymentPayload?
let payment: CreateOrderPaymentInfo? let payment: CreateOrderPaymentInfo?
let nextAction: String?
let deliveryType: String? let deliveryType: String?
let deliveryTypeLabel: String? let deliveryTypeLabel: String?
let total: Double? let total: Double?
let storeName: String? let storeName: String?
let storeLogoURL: String?
let createdAt: String? let createdAt: String?
let updatedAt: String? let updatedAt: String?
let otp: String? let otp: String?
let customerOtp: String? let customerOtp: String?
let confirmOtp: String? let confirmOtp: String?
let cancellationReason: String? let cancellationReason: String?
let deliveryAddress: PublicOrderDeliveryAddress?
let review: PublicOrderReview?
let items: [PublicOrderItem] let items: [PublicOrderItem]
let timeline: [PublicOrderTimelineEvent] let timeline: [PublicOrderTimelineEvent]
@@ -85,6 +99,8 @@ struct PublicOrderResult: Codable, Identifiable {
case id case id
case shortId case shortId
case realId case realId
case storeId
case store_id
case status case status
case paymentStatus case paymentStatus
case paymentConfirmed case paymentConfirmed
@@ -92,16 +108,27 @@ struct PublicOrderResult: Codable, Identifiable {
case paymentMethodCode case paymentMethodCode
case paymentPayload case paymentPayload
case payment case payment
case nextAction
case deliveryType case deliveryType
case deliveryTypeLabel case deliveryTypeLabel
case total case total
case storeName case storeName
case storeLogo
case store_logo
case logo
case createdAt case createdAt
case updatedAt case updatedAt
case otp case otp
case customerOtp case customerOtp
case confirmOtp case confirmOtp
case cancellationReason case cancellationReason
case address
case deliveryAddress
case delivery_address
case customerAddress
case customer_address
case review
case orderReview
case items case items
case timeline case timeline
} }
@@ -110,6 +137,7 @@ struct PublicOrderResult: Codable, Identifiable {
id: String, id: String,
shortId: String? = nil, shortId: String? = nil,
realId: String? = nil, realId: String? = nil,
storeId: String? = nil,
status: String? = nil, status: String? = nil,
paymentStatus: String? = nil, paymentStatus: String? = nil,
paymentConfirmed: Bool? = nil, paymentConfirmed: Bool? = nil,
@@ -117,22 +145,27 @@ struct PublicOrderResult: Codable, Identifiable {
paymentMethodCode: String? = nil, paymentMethodCode: String? = nil,
paymentPayload: CreateOrderPaymentPayload? = nil, paymentPayload: CreateOrderPaymentPayload? = nil,
payment: CreateOrderPaymentInfo? = nil, payment: CreateOrderPaymentInfo? = nil,
nextAction: String? = nil,
deliveryType: String? = nil, deliveryType: String? = nil,
deliveryTypeLabel: String? = nil, deliveryTypeLabel: String? = nil,
total: Double? = nil, total: Double? = nil,
storeName: String? = nil, storeName: String? = nil,
storeLogoURL: String? = nil,
createdAt: String? = nil, createdAt: String? = nil,
updatedAt: String? = nil, updatedAt: String? = nil,
otp: String? = nil, otp: String? = nil,
customerOtp: String? = nil, customerOtp: String? = nil,
confirmOtp: String? = nil, confirmOtp: String? = nil,
cancellationReason: String? = nil, cancellationReason: String? = nil,
deliveryAddress: PublicOrderDeliveryAddress? = nil,
review: PublicOrderReview? = nil,
items: [PublicOrderItem] = [], items: [PublicOrderItem] = [],
timeline: [PublicOrderTimelineEvent] = [] timeline: [PublicOrderTimelineEvent] = []
) { ) {
self.id = id self.id = id
self.shortId = shortId self.shortId = shortId
self.realId = realId self.realId = realId
self.storeId = storeId
self.status = status self.status = status
self.paymentStatus = paymentStatus self.paymentStatus = paymentStatus
self.paymentConfirmed = paymentConfirmed self.paymentConfirmed = paymentConfirmed
@@ -140,16 +173,20 @@ struct PublicOrderResult: Codable, Identifiable {
self.paymentMethodCode = paymentMethodCode self.paymentMethodCode = paymentMethodCode
self.paymentPayload = paymentPayload self.paymentPayload = paymentPayload
self.payment = payment self.payment = payment
self.nextAction = nextAction
self.deliveryType = deliveryType self.deliveryType = deliveryType
self.deliveryTypeLabel = deliveryTypeLabel self.deliveryTypeLabel = deliveryTypeLabel
self.total = total self.total = total
self.storeName = storeName self.storeName = storeName
self.storeLogoURL = storeLogoURL
self.createdAt = createdAt self.createdAt = createdAt
self.updatedAt = updatedAt self.updatedAt = updatedAt
self.otp = otp self.otp = otp
self.customerOtp = customerOtp self.customerOtp = customerOtp
self.confirmOtp = confirmOtp self.confirmOtp = confirmOtp
self.cancellationReason = cancellationReason self.cancellationReason = cancellationReason
self.deliveryAddress = deliveryAddress
self.review = review
self.items = items self.items = items
self.timeline = timeline self.timeline = timeline
} }
@@ -159,11 +196,13 @@ struct PublicOrderResult: Codable, Identifiable {
id = ApiService.decodeFlexibleString(from: container, keys: [.id]) ?? UUID().uuidString id = ApiService.decodeFlexibleString(from: container, keys: [.id]) ?? UUID().uuidString
shortId = ApiService.decodeFlexibleString(from: container, keys: [.shortId]) shortId = ApiService.decodeFlexibleString(from: container, keys: [.shortId])
realId = ApiService.decodeFlexibleString(from: container, keys: [.realId]) realId = ApiService.decodeFlexibleString(from: container, keys: [.realId])
storeId = ApiService.decodeFlexibleString(from: container, keys: [.storeId, .store_id])
status = ApiService.decodeFlexibleString(from: container, keys: [.status]) status = ApiService.decodeFlexibleString(from: container, keys: [.status])
paymentStatus = ApiService.decodeFlexibleString(from: container, keys: [.paymentStatus]) paymentStatus = ApiService.decodeFlexibleString(from: container, keys: [.paymentStatus])
paymentConfirmed = try? container.decode(Bool.self, forKey: .paymentConfirmed) paymentConfirmed = try? container.decode(Bool.self, forKey: .paymentConfirmed)
paymentMethod = ApiService.decodeFlexibleString(from: container, keys: [.paymentMethod]) paymentMethod = ApiService.decodeFlexibleString(from: container, keys: [.paymentMethod])
paymentMethodCode = ApiService.decodeFlexibleString(from: container, keys: [.paymentMethodCode]) paymentMethodCode = ApiService.decodeFlexibleString(from: container, keys: [.paymentMethodCode])
nextAction = ApiService.decodeFlexibleString(from: container, keys: [.nextAction])
payment = try? container.decode(CreateOrderPaymentInfo.self, forKey: .payment) payment = try? container.decode(CreateOrderPaymentInfo.self, forKey: .payment)
if let objectPayload = try? container.decode(CreateOrderPaymentPayload.self, forKey: .paymentPayload) { if let objectPayload = try? container.decode(CreateOrderPaymentPayload.self, forKey: .paymentPayload) {
paymentPayload = objectPayload paymentPayload = objectPayload
@@ -180,16 +219,54 @@ struct PublicOrderResult: Codable, Identifiable {
deliveryTypeLabel = ApiService.decodeFlexibleString(from: container, keys: [.deliveryTypeLabel]) deliveryTypeLabel = ApiService.decodeFlexibleString(from: container, keys: [.deliveryTypeLabel])
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])
createdAt = ApiService.decodeFlexibleString(from: container, keys: [.createdAt]) createdAt = ApiService.decodeFlexibleString(from: container, keys: [.createdAt])
updatedAt = ApiService.decodeFlexibleString(from: container, keys: [.updatedAt]) updatedAt = ApiService.decodeFlexibleString(from: container, keys: [.updatedAt])
otp = ApiService.decodeFlexibleString(from: container, keys: [.otp]) otp = ApiService.decodeFlexibleString(from: container, keys: [.otp])
customerOtp = ApiService.decodeFlexibleString(from: container, keys: [.customerOtp]) customerOtp = ApiService.decodeFlexibleString(from: container, keys: [.customerOtp])
confirmOtp = ApiService.decodeFlexibleString(from: container, keys: [.confirmOtp]) confirmOtp = ApiService.decodeFlexibleString(from: container, keys: [.confirmOtp])
cancellationReason = ApiService.decodeFlexibleString(from: container, keys: [.cancellationReason]) 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)) ?? [] items = (try? container.decode([PublicOrderItem].self, forKey: .items)) ?? []
timeline = (try? container.decode([PublicOrderTimelineEvent].self, forKey: .timeline)) ?? [] 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? { var displayOtpCode: String? {
let values = [customerOtp, otp, confirmOtp] let values = [customerOtp, otp, confirmOtp]
for value in values { 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<CodingKeys>,
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<CodingKeys>,
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 { struct PublicOrderItem: Codable, Identifiable {
let id: String let id: String
let name: String? let name: String?

View File

@@ -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)
}
}

View File

@@ -479,6 +479,7 @@ struct OrderEntryDestinationView: View {
@State var pixContext: PixPaymentContext? = nil @State var pixContext: PixPaymentContext? = nil
@State var cardContext: CardPaymentContext? = nil @State var cardContext: CardPaymentContext? = nil
@State var orderTrackingContext: OrderTrackingContext? = nil @State var orderTrackingContext: OrderTrackingContext? = nil
@State var orderDetails: PublicOrderResult? = nil
var body: some View { var body: some View {
Group { Group {
@@ -511,6 +512,8 @@ struct OrderEntryDestinationView: View {
orderTrackingContext = OrderTrackingContext(orderId: cardContext.orderId, shortId: cardContext.shortId) orderTrackingContext = OrderTrackingContext(orderId: cardContext.orderId, shortId: cardContext.shortId)
} }
) )
} else if let orderDetails {
OrderDetailsView(order: orderDetails, orderId: orderId, initialShortId: initialShortId)
} else { } else {
OrderTrackingView(orderId: orderId, initialShortId: initialShortId) OrderTrackingView(orderId: orderId, initialShortId: initialShortId)
} }
@@ -531,6 +534,10 @@ struct OrderEntryDestinationView: View {
let order = await fetchOrderForRouting() let order = await fetchOrderForRouting()
guard let order else { return } guard let order else { return }
if shouldOpenOrderDetails(for: order) {
orderDetails = order
return
}
guard shouldOpenPaymentScreen(for: order) else { return } guard shouldOpenPaymentScreen(for: order) else { return }
let normalizedMethod = normalizePaymentMethod(order) let normalizedMethod = normalizePaymentMethod(order)
@@ -592,6 +599,13 @@ struct OrderEntryDestinationView: View {
return method == "PIX" || method == "CREDIT_CARD" || method == "DEBIT_CARD" 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 { func shouldOpenPaymentScreen(for order: PublicOrderResult) -> Bool {
guard order.isPaymentConfirmed == false else { return false } guard order.isPaymentConfirmed == false else { return false }
guard isOnlinePaymentMethod(order) else { return false } guard isOnlinePaymentMethod(order) else { return false }