Fix login
This commit is contained in:
263
pedi-foods/Sources/PediFoods/Views/Main/OrderTrackingView.swift
Normal file
263
pedi-foods/Sources/PediFoods/Views/Main/OrderTrackingView.swift
Normal file
@@ -0,0 +1,263 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user