This commit is contained in:
Daniel Arantes Loverde
2026-03-05 10:10:27 -03:00
parent a8e3631177
commit 1407f0b9de
27 changed files with 1493 additions and 84 deletions

View File

@@ -18,6 +18,8 @@ struct OrderTrackingView: View {
@State var order: PublicOrderResult? = nil
@State var tracker = OrderRealtimeTracker()
@State var showCancellationReason = false
@State var reviewDraft: ReviewDraft? = nil
@State var didSaveReviewForCurrentOrder = false
var body: some View {
ScrollView(showsIndicators: false) {
@@ -27,7 +29,11 @@ struct OrderTrackingView: View {
statusBanner
timelineSection
placeholderCard
contactButton
if shouldShowReviewButton {
reviewButton
} else {
contactButton
}
}
.padding(.horizontal, 20)
.padding(.top, 14)
@@ -53,6 +59,16 @@ struct OrderTrackingView: View {
.onDisappear {
tracker.stop()
}
.navigationDestination(item: $reviewDraft) { draft in
MyReviewsView(initialOrder: draft)
}
.onReceive(NotificationCenter.default.publisher(for: .orderReviewDidSave)) { payload in
guard let orderIdFromEvent = payload.userInfo?["orderId"] as? String else { return }
let currentOrderId = (order?.id ?? orderId).trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
if orderIdFromEvent.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == currentOrderId {
didSaveReviewForCurrentOrder = true
}
}
}
private var topHeader: some View {
@@ -217,6 +233,19 @@ struct OrderTrackingView: View {
.buttonStyle(.plain)
}
private var reviewButton: some View {
Button("AVALIAR PEDIDO") {
guard let reviewTargetDraft else { return }
reviewDraft = reviewTargetDraft
}
.font(AppTypography.heading2)
.foregroundStyle(Color(hex: "#0E1A06"))
.frame(maxWidth: .infinity, minHeight: 56)
.background(Color(hex: "#7CF02A"))
.clipShape(Capsule())
.buttonStyle(.plain)
}
private func statusBadge(title: String, fg: Color, bg: Color, icon: String) -> some View {
HStack(spacing: 8) {
Image(systemName: icon)
@@ -444,12 +473,43 @@ struct OrderTrackingView: View {
return customerOtpSubtitle
}
let label = (eventLabel ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
if label.isEmpty == false {
if shouldUseTimelineEventLabel(label, fallback: fallback) {
return label
}
return fallback
}
private func shouldUseTimelineEventLabel(_ label: String, fallback: String) -> Bool {
guard label.isEmpty == false else { return false }
let foldedLabel = label
.folding(options: [.diacriticInsensitive, .caseInsensitive], locale: .current)
.lowercased()
let foldedFallback = fallback
.folding(options: [.diacriticInsensitive, .caseInsensitive], locale: .current)
.lowercased()
if foldedLabel == foldedFallback { return false }
let englishHints = [
"order",
"confirmed",
"in progress",
"progress",
"delivery",
"delivered",
"ready",
"sent",
"out for",
"began"
]
if englishHints.contains(where: { foldedLabel.contains($0) }) {
return false
}
return true
}
private var customerOtpSubtitle: String {
if let otp = customerOtpCode {
return "Código para o entregador: \(otp)"
@@ -548,6 +608,57 @@ struct OrderTrackingView: View {
return isCanceled == false && isWaitingPayment == false && stepIndex >= terminalIndex
}
private var shouldShowReviewButton: Bool {
guard isCompletedOrder else { return false }
guard isCanceled == false else { return false }
guard let reviewTargetDraft else { return false }
if didSaveReviewForCurrentOrder { return false }
if hasPersistedReviewForCurrentOrder { return false }
return order?.review == nil
}
private var hasPersistedReviewForCurrentOrder: Bool {
reviewIdCandidates.contains { candidate in
SessionStateStore.hasOrderReview(orderId: candidate)
}
}
private var reviewIdCandidates: [String] {
let values = [
orderId,
order?.id,
order?.realId,
order?.shortId
]
var unique: [String] = []
var seen = Set<String>()
for raw in values {
let normalized = (raw ?? "")
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
guard normalized.isEmpty == false, seen.contains(normalized) == false else { continue }
seen.insert(normalized)
unique.append(normalized)
}
return unique
}
private var reviewTargetDraft: ReviewDraft? {
let idFromOrder = (order?.id ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
let id = idFromOrder.isEmpty ? orderId : idFromOrder
guard id.isEmpty == false else { return nil }
return ReviewDraft(
orderId: id,
storeId: order?.storeId,
shortId: order?.shortId ?? initialShortId,
storeName: order?.storeName,
storeLogoURL: order?.storeLogoURL,
createdAt: order?.createdAt,
total: order?.total
)
}
private var cancellationReasonText: String {
let value = (order?.cancellationReason ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
return value.isEmpty ? "Sem detalhe informado." : value
@@ -590,7 +701,7 @@ struct OrderTrackingView: View {
case 1:
return "tracking-preparing"
case 2:
return isPickup ? "tracking-ready" : "tracking-preparing"
return "tracking-ready"
case 3:
return "tracking-delivering"
default:
@@ -636,24 +747,21 @@ struct OrderTrackingView: View {
@MainActor
private func loadInitialOrder() async {
if let cached = SessionStateStore.loadTrackedOrder(orderId: orderId) {
order = cached
isLoading = false
}
logger.info("OrderTracking initial fetch orderId=\(orderId, privacy: .public)")
do {
let response = try await ApiService().publicOrder(orderId: orderId)
if response.error {
errorMessage = response.message ?? "Não foi possível carregar o pedido."
logger.error("OrderTracking initial fetch API error orderId=\(orderId, privacy: .public) message=\((response.message ?? "unknown"), privacy: .public)")
} else if let result = response.result {
order = result
SessionStateStore.saveTrackedOrder(result)
errorMessage = nil
logger.info("OrderTracking initial fetch success orderId=\(orderId, privacy: .public) status=\((result.status ?? "nil"), privacy: .public) paymentStatus=\((result.paymentStatus ?? "nil"), privacy: .public)")
}
} catch {
if order == nil {
errorMessage = "Não foi possível carregar o pedido."
}
errorMessage = "Não foi possível carregar o pedido."
logger.error("OrderTracking initial fetch failure orderId=\(orderId, privacy: .public) error=\(error.localizedDescription, privacy: .public)")
}
isLoading = false