import SwiftUI struct OrderTrackingView: View { let orderId: String let initialShortId: String? @State var isLoading = true @State var errorMessage: String? = nil @State var order: PublicOrderResult? = nil @State var tracker = OrderRealtimeTracker() var body: some View { ScrollView(showsIndicators: false) { VStack(spacing: 16) { headerCard if isLoading { ProgressView() .padding(.top, 20) } if let errorMessage, errorMessage.isEmpty == false { Text(errorMessage) .font(AppTypography.body) .foregroundStyle(Color.red) .multilineTextAlignment(.center) .padding(.horizontal, 20) } if let order { timelineCard(order) } footerPlaceholderCard } .padding(.horizontal, 20) .padding(.top, 14) .padding(.bottom, UIDevice.bottomNotch + 24) } .background(AppColors.backgroundLight) .navigationTitle("Pedido \(displayOrderTitle)") .navigationBarTitleDisplayMode(.inline) .task { await loadInitialOrder() tracker.onOrderUpdated = { updated in order = updated isLoading = false errorMessage = nil } tracker.start(orderId: orderId, jwt: DefaultTokenStore().jwt) } .onDisappear { tracker.stop() } } private var displayOrderTitle: String { if let short = order?.shortId, short.isEmpty == false { return "#\(short)" } if let initialShortId, initialShortId.isEmpty == false { return "#\(initialShortId)" } return "#\(orderId.prefix(6))" } private var headerCard: some View { VStack(alignment: .leading, spacing: 6) { Text(statusTitle) .font(AppTypography.heading1) .foregroundStyle(AppColors.textPrimary) Text("Pedido ID #\(order?.shortId ?? initialShortId ?? orderId)") .font(AppTypography.body) .foregroundStyle(AppColors.textMuted) if let order, order.isInDeliveryRoute, let otp = order.displayOtpCode { Text("Seu código do pedido: \(otp) - Informe esse número ao motoboy") .font(AppTypography.body) .foregroundStyle(AppColors.textPrimary) .multilineTextAlignment(.leading) .padding(.top, 2) } HStack { Spacer() Text(statusPill) .font(.system(size: 13, weight: .bold)) .foregroundStyle(AppColors.primary) .padding(.horizontal, 12) .padding(.vertical, 6) .background(AppColors.brandSoft) .clipShape(Capsule()) } } .frame(maxWidth: .infinity, alignment: .leading) .padding(16) .background(AppColors.surface) .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous)) } private func timelineCard(_ order: PublicOrderResult) -> some View { let events: [PublicOrderTimelineEvent] = { if order.timeline.isEmpty == false { return order.timeline } return [ PublicOrderTimelineEvent( status: order.status ?? order.paymentStatus ?? "PENDING", message: nil, time: order.updatedAt ?? order.createdAt ) ] }() return VStack(alignment: .leading, spacing: 12) { Text("Acompanhamento") .font(AppTypography.heading2) .foregroundStyle(AppColors.textPrimary) VStack(alignment: .leading, spacing: 0) { ForEach(Array(events.enumerated()), id: \.offset) { index, event in timelineRow(event: event, isLast: index == events.count - 1) } } } .frame(maxWidth: .infinity, alignment: .leading) .padding(16) .background(AppColors.surface) .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous)) } private func timelineRow(event: PublicOrderTimelineEvent, isLast: Bool) -> some View { HStack(alignment: .top, spacing: 12) { VStack(spacing: 0) { Circle() .fill(AppColors.tertiary) .frame(width: 20, height: 20) .overlay( Image(systemName: "checkmark") .font(.system(size: 11, weight: .bold)) .foregroundStyle(.white) ) if isLast == false { Rectangle() .fill(AppColors.tertiary.opacity(0.45)) .frame(width: 2, height: 30) } } VStack(alignment: .leading, spacing: 2) { Text(humanReadableStatus(event.status ?? event.message ?? "Atualizado")) .font(AppTypography.heading3) .foregroundStyle(AppColors.textPrimary) Text(formatTime(event.time) ?? "") .font(AppTypography.caption) .foregroundStyle(AppColors.textMuted) } Spacer() } } private var footerPlaceholderCard: some View { VStack(spacing: 10) { RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous) .fill(Color.black.opacity(0.9)) .frame(height: 180) .overlay( Image(systemName: "shippingbox.fill") .font(.system(size: 56, weight: .bold)) .foregroundStyle(AppColors.tertiary) ) Text("Acompanhe seu pedido em tempo real") .font(AppTypography.body) .foregroundStyle(AppColors.textMuted) } .frame(maxWidth: .infinity) .padding(16) .background(AppColors.surface) .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous)) } private var statusTitle: String { humanReadableStatus(order?.status ?? "Aguardando") } private var statusPill: String { let value = order?.paymentStatus ?? order?.status ?? "PENDING" return humanReadableStatus(value) } private func humanReadableStatus(_ raw: String) -> String { let normalized = raw.uppercased() if normalized.contains("PAYMENT_PENDING") || normalized == "PENDING" { return "Aguardando pagamento" } if normalized.contains("CONFIRMED") { return "Pagamento confirmado" } if normalized.contains("PREPAR") { return "Em preparo" } if normalized.contains("OUT_FOR_DELIVERY") || normalized.contains("EM_ROTA") || normalized.contains("ROTA") { return "Em rota de entrega" } if normalized.contains("COMPLETED") || normalized.contains("DELIVERED") { return "Pedido entregue" } if normalized.contains("CANCEL") { return "Pedido cancelado" } return raw.capitalized } private func formatTime(_ isoValue: String?) -> String? { guard let isoValue, isoValue.isEmpty == false else { return nil } let iso = ISO8601DateFormatter() iso.formatOptions = [.withInternetDateTime, .withFractionalSeconds] var date: Date? = iso.date(from: isoValue) if date == nil { iso.formatOptions = [.withInternetDateTime] date = iso.date(from: isoValue) } if date == nil { let fallback = DateFormatter() fallback.locale = Locale(identifier: "pt_BR") fallback.dateFormat = "yyyy-MM-dd HH:mm:ss" date = fallback.date(from: isoValue) } guard let date else { return nil } let formatter = DateFormatter() formatter.locale = Locale(identifier: "pt_BR") formatter.dateFormat = "HH:mm" return formatter.string(from: date) } @MainActor private func loadInitialOrder() async { if let cached = SessionStateStore.loadTrackedOrder(orderId: orderId) { order = cached isLoading = false } do { let response = try await ApiService().publicOrder(orderId: orderId) if response.error { errorMessage = response.message ?? "Não foi possível carregar o pedido." } else if let result = response.result { order = result SessionStateStore.saveTrackedOrder(result) errorMessage = nil } } catch { if order == nil { errorMessage = "Não foi possível carregar o pedido." } } isLoading = false } }