Fix login
This commit is contained in:
@@ -187,8 +187,8 @@ struct OtpView: View {
|
||||
SnackbarCenter.shared.show(title: "Login realizado com sucesso.", style: .success, icon: "checkmark.seal.fill", duration: 2.5)
|
||||
appState.session.isAuthenticated = true
|
||||
appState.session.jwt = response.result?.token
|
||||
hydrateUserState(from: response.result?.customer)
|
||||
routeAfterLogin()
|
||||
let hasServerAddress = hydrateUserState(from: response.result?.customer)
|
||||
routeAfterLogin(hasServerAddress: hasServerAddress)
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
@@ -201,16 +201,8 @@ struct OtpView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func routeAfterLogin() {
|
||||
if let cached = LocationService.shared.cachedLocation() {
|
||||
appState.address.latitude = cached.0
|
||||
appState.address.longitude = cached.1
|
||||
selectedTab = .home
|
||||
root = .main
|
||||
return
|
||||
}
|
||||
|
||||
if hasConfiguredAddress() {
|
||||
private func routeAfterLogin(hasServerAddress: Bool) {
|
||||
if hasServerAddress || hasConfiguredAddress() {
|
||||
selectedTab = .home
|
||||
root = .main
|
||||
return
|
||||
@@ -221,7 +213,7 @@ struct OtpView: View {
|
||||
root = .main
|
||||
}
|
||||
|
||||
private func hydrateUserState(from customer: CustomerProfile?) {
|
||||
private func hydrateUserState(from customer: CustomerProfile?) -> Bool {
|
||||
if let customer {
|
||||
appState.profile.id = customer.id
|
||||
appState.profile.name = customer.name
|
||||
@@ -236,21 +228,24 @@ struct OtpView: View {
|
||||
appState.cart = CartState()
|
||||
}
|
||||
|
||||
if let preferred = customer.addressBook?.first {
|
||||
let addresses = customer.addressBook ?? []
|
||||
if let preferred = addresses.first {
|
||||
appState.address.selectedId = preferred.id
|
||||
let label = (preferred.label ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
appState.address.display = label.isEmpty ? "Defina seu endereco" : label
|
||||
if let lat = preferred.latLong?.first, let lng = preferred.latLong?.dropFirst().first {
|
||||
appState.address.latitude = lat
|
||||
appState.address.longitude = lng
|
||||
} else {
|
||||
appState.address.latitude = nil
|
||||
appState.address.longitude = nil
|
||||
}
|
||||
SessionStateStore.saveAddress(appState.address)
|
||||
} else if let cached = SessionStateStore.loadAddress() {
|
||||
appState.address = cached
|
||||
} else {
|
||||
appState.address = AddressState()
|
||||
SessionStateStore.clearAddress()
|
||||
}
|
||||
return
|
||||
return addresses.isEmpty == false
|
||||
}
|
||||
|
||||
appState.profile.email = email
|
||||
@@ -262,11 +257,8 @@ struct OtpView: View {
|
||||
} else {
|
||||
appState.cart = CartState()
|
||||
}
|
||||
if let cached = SessionStateStore.loadAddress() {
|
||||
appState.address = cached
|
||||
} else {
|
||||
appState.address = AddressState()
|
||||
}
|
||||
appState.address = AddressState()
|
||||
return false
|
||||
}
|
||||
|
||||
private func hasConfiguredAddress() -> Bool {
|
||||
@@ -274,10 +266,6 @@ struct OtpView: View {
|
||||
return true
|
||||
}
|
||||
|
||||
if appState.address.latitude != nil, appState.address.longitude != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
let normalized = appState.address.display
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
.lowercased()
|
||||
|
||||
@@ -201,16 +201,3 @@ struct AddressListItem: Identifiable {
|
||||
let icon: String
|
||||
let isPrimary: Bool
|
||||
}
|
||||
|
||||
struct OrdersView: View {
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
Text("Pedidos")
|
||||
.font(AppTypography.heading1)
|
||||
Text("Historico vazio")
|
||||
.foregroundStyle(AppColors.secondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(AppColors.backgroundLight)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,21 +211,6 @@ extension CheckoutView {
|
||||
func handleConfirmPaymentTap() async {
|
||||
guard canConfirmPayment else { return }
|
||||
|
||||
if useInAppPayment == false {
|
||||
SnackbarCenter.shared.show(title: "Pagamento presencial selecionado.", style: .info, icon: "creditcard.fill", duration: 2.0)
|
||||
return
|
||||
}
|
||||
|
||||
if paymentMethod == .creditCard {
|
||||
openCardPayment = true
|
||||
return
|
||||
}
|
||||
|
||||
guard paymentMethod == .pix else {
|
||||
SnackbarCenter.shared.show(title: "Método de pagamento não suportado no app.", style: .warning, icon: "exclamationmark.triangle.fill", duration: 2.0)
|
||||
return
|
||||
}
|
||||
|
||||
guard let storeId = appState.cart.storeId, storeId.isEmpty == false else {
|
||||
SnackbarCenter.shared.show(title: "Loja do pedido não encontrada.", style: .error, icon: "xmark.octagon.fill", duration: 2.0)
|
||||
return
|
||||
@@ -233,7 +218,13 @@ extension CheckoutView {
|
||||
|
||||
await refreshSelectedCustomerAddress()
|
||||
|
||||
let payloadBuildResult = buildCreateOrderPayload(paymentMethod: .pix)
|
||||
let effectivePaymentMethod = paymentMethod
|
||||
if useInAppPayment && availableInAppPaymentMethods.contains(effectivePaymentMethod) == false {
|
||||
SnackbarCenter.shared.show(title: "Método de pagamento não suportado no app.", style: .warning, icon: "exclamationmark.triangle.fill", duration: 2.0)
|
||||
return
|
||||
}
|
||||
|
||||
let payloadBuildResult = buildCreateOrderPayload(paymentMethod: effectivePaymentMethod)
|
||||
guard case .success(let payload) = payloadBuildResult else {
|
||||
let message: String
|
||||
if case .failure(let reason) = payloadBuildResult {
|
||||
@@ -252,7 +243,7 @@ extension CheckoutView {
|
||||
let response = try await ApiService().createOrder(storeId: storeId, payload: payload)
|
||||
if response.error {
|
||||
SnackbarCenter.shared.show(
|
||||
title: response.message ?? "Não foi possível gerar o pagamento PIX.",
|
||||
title: response.message ?? "Não foi possível criar o pedido.",
|
||||
style: .error,
|
||||
icon: "xmark.octagon.fill",
|
||||
duration: 3.0
|
||||
@@ -265,23 +256,52 @@ extension CheckoutView {
|
||||
return
|
||||
}
|
||||
|
||||
let pixPayload = result.payment?.pix ?? result.paymentPayload
|
||||
guard let copyPaste = pixPayload?.copyPaste, copyPaste.isEmpty == false else {
|
||||
let orderSnapshot = result.asPublicOrderResult()
|
||||
SessionStateStore.saveTrackedOrder(orderSnapshot)
|
||||
|
||||
let orderId = result.id ?? UUID().uuidString
|
||||
if useInAppPayment == false {
|
||||
orderTrackingContext = OrderTrackingContext(orderId: orderId, shortId: result.shortId)
|
||||
return
|
||||
}
|
||||
|
||||
if effectivePaymentMethod == .creditCard {
|
||||
cardPaymentContext = CardPaymentContext(
|
||||
orderId: orderId,
|
||||
shortId: result.shortId,
|
||||
total: totalValue
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
let pixFromPayment = result.payment?.pix
|
||||
let pixFromPayload = result.paymentPayload
|
||||
|
||||
let copyPaste = (pixFromPayment?.copyPaste?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false)
|
||||
? pixFromPayment?.copyPaste
|
||||
: pixFromPayload?.copyPaste
|
||||
let qrCodeImage = (pixFromPayment?.qrCodeImage?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false)
|
||||
? pixFromPayment?.qrCodeImage
|
||||
: pixFromPayload?.qrCodeImage
|
||||
let expirationDate = (pixFromPayment?.expirationDate?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false)
|
||||
? pixFromPayment?.expirationDate
|
||||
: pixFromPayload?.expirationDate
|
||||
|
||||
guard let copyPaste, copyPaste.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false else {
|
||||
SnackbarCenter.shared.show(title: "Código PIX não retornado pela API.", style: .error, icon: "xmark.octagon.fill", duration: 3.0)
|
||||
return
|
||||
}
|
||||
|
||||
let orderId = result.id ?? UUID().uuidString
|
||||
pixPaymentContext = PixPaymentContext(
|
||||
id: orderId,
|
||||
orderId: orderId,
|
||||
shortId: result.shortId,
|
||||
copyPaste: copyPaste,
|
||||
qrCodeImageBase64: pixPayload?.qrCodeImage,
|
||||
expirationDate: pixPayload?.expirationDate
|
||||
qrCodeImageBase64: qrCodeImage,
|
||||
expirationDate: expirationDate
|
||||
)
|
||||
} catch {
|
||||
SnackbarCenter.shared.show(title: "Não foi possível gerar o pagamento PIX.", style: .error, icon: "xmark.octagon.fill", duration: 3.0)
|
||||
SnackbarCenter.shared.show(title: "Não foi possível criar o pedido.", style: .error, icon: "xmark.octagon.fill", duration: 3.0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,8 @@ struct CheckoutView: View {
|
||||
@State var lastAcceptedAddressState: AddressState? = nil
|
||||
@State var isSubmittingOrder = false
|
||||
@State var pixPaymentContext: PixPaymentContext? = nil
|
||||
@State var openCardPayment = false
|
||||
@State var cardPaymentContext: CardPaymentContext? = nil
|
||||
@State var orderTrackingContext: OrderTrackingContext? = nil
|
||||
|
||||
var paymentConfig: StorePaymentMethodsInfo? { storeInfo?.paymentMethods }
|
||||
|
||||
@@ -151,10 +152,17 @@ struct CheckoutView: View {
|
||||
}
|
||||
}
|
||||
.navigationDestination(item: $pixPaymentContext) { context in
|
||||
PaymentPixView(context: context)
|
||||
PaymentPixView(context: context) {
|
||||
orderTrackingContext = OrderTrackingContext(orderId: context.orderId, shortId: context.shortId)
|
||||
}
|
||||
}
|
||||
.navigationDestination(isPresented: $openCardPayment) {
|
||||
PaymentCardView(total: totalValue)
|
||||
.navigationDestination(item: $cardPaymentContext) { context in
|
||||
PaymentCardView(context: context) {
|
||||
orderTrackingContext = OrderTrackingContext(orderId: context.orderId, shortId: context.shortId)
|
||||
}
|
||||
}
|
||||
.navigationDestination(item: $orderTrackingContext) { context in
|
||||
OrderTrackingView(orderId: context.orderId, initialShortId: context.shortId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -529,9 +537,26 @@ struct PixPaymentContext: Identifiable, Hashable {
|
||||
let expirationDate: String?
|
||||
}
|
||||
|
||||
struct OrderTrackingContext: Identifiable, Hashable {
|
||||
var id: String { orderId }
|
||||
let orderId: String
|
||||
let shortId: String?
|
||||
}
|
||||
|
||||
struct CardPaymentContext: Identifiable, Hashable {
|
||||
var id: String { orderId }
|
||||
let orderId: String
|
||||
let shortId: String?
|
||||
let total: Double
|
||||
}
|
||||
|
||||
struct PaymentPixView: View {
|
||||
let context: PixPaymentContext
|
||||
var onOpenTracking: (() -> Void)? = nil
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@State var tracker = OrderRealtimeTracker()
|
||||
@State var latestOrder: PublicOrderResult? = nil
|
||||
@State var hasOpenedTracking = false
|
||||
|
||||
private var qrImageSource: String? {
|
||||
guard let raw = context.qrCodeImageBase64?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
@@ -601,7 +626,11 @@ struct PaymentPixView: View {
|
||||
}
|
||||
|
||||
Button("Já realizei o pagamento") {
|
||||
dismiss()
|
||||
if latestOrder?.isPaymentConfirmed == true {
|
||||
openTrackingOnce()
|
||||
} else {
|
||||
SnackbarCenter.shared.show(title: "Pagamento ainda não confirmado.", style: .info, icon: "clock.fill", duration: 2.0)
|
||||
}
|
||||
}
|
||||
.font(AppTypography.heading3)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
@@ -612,6 +641,25 @@ struct PaymentPixView: View {
|
||||
.background(AppColors.backgroundLight)
|
||||
.navigationTitle("Pagamento via PIX")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.task {
|
||||
tracker.onOrderUpdated = { updated in
|
||||
latestOrder = updated
|
||||
if updated.isPaymentConfirmed {
|
||||
openTrackingOnce()
|
||||
}
|
||||
}
|
||||
tracker.start(orderId: context.orderId, jwt: DefaultTokenStore().jwt)
|
||||
}
|
||||
.onDisappear {
|
||||
tracker.stop()
|
||||
}
|
||||
}
|
||||
|
||||
private func openTrackingOnce() {
|
||||
guard hasOpenedTracking == false else { return }
|
||||
hasOpenedTracking = true
|
||||
onOpenTracking?()
|
||||
dismiss()
|
||||
}
|
||||
|
||||
private func copyToClipboard(_ value: String) {
|
||||
@@ -625,12 +673,16 @@ struct PaymentPixView: View {
|
||||
}
|
||||
|
||||
struct PaymentCardView: View {
|
||||
let total: Double
|
||||
let context: CardPaymentContext
|
||||
var onOpenTracking: (() -> Void)? = nil
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@State var cardHolderName = ""
|
||||
@State var cardNumber = ""
|
||||
@State var expiry = ""
|
||||
@State var cvv = ""
|
||||
@State var tracker = OrderRealtimeTracker()
|
||||
@State var latestOrder: PublicOrderResult? = nil
|
||||
@State var hasOpenedTracking = false
|
||||
|
||||
var body: some View {
|
||||
ScrollView(showsIndicators: false) {
|
||||
@@ -643,7 +695,7 @@ struct PaymentCardView: View {
|
||||
Text("Total do Pedido")
|
||||
.font(AppTypography.caption)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
Text(formatCurrency(total))
|
||||
Text(formatCurrency(context.total))
|
||||
.font(AppTypography.heading2)
|
||||
.foregroundStyle(AppColors.textPrimary)
|
||||
}
|
||||
@@ -670,11 +722,19 @@ struct PaymentCardView: View {
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
||||
|
||||
PrimaryButton(title: "Salvar e Pagar") {
|
||||
SnackbarCenter.shared.show(title: "Fluxo de cartão em construção.", style: .info, icon: "creditcard.fill", duration: 2.0)
|
||||
if latestOrder?.isPaymentConfirmed == true {
|
||||
openTrackingOnce()
|
||||
} else {
|
||||
SnackbarCenter.shared.show(title: "Pagamento ainda não confirmado.", style: .info, icon: "clock.fill", duration: 2.0)
|
||||
}
|
||||
}
|
||||
|
||||
Button("Apenas Pagar") {
|
||||
dismiss()
|
||||
if latestOrder?.isPaymentConfirmed == true {
|
||||
openTrackingOnce()
|
||||
} else {
|
||||
SnackbarCenter.shared.show(title: "Aguardando confirmação do pagamento.", style: .info, icon: "clock.fill", duration: 2.0)
|
||||
}
|
||||
}
|
||||
.font(AppTypography.heading3)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
@@ -688,6 +748,25 @@ struct PaymentCardView: View {
|
||||
.background(AppColors.backgroundLight)
|
||||
.navigationTitle("Pagamento")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.task {
|
||||
tracker.onOrderUpdated = { updated in
|
||||
latestOrder = updated
|
||||
if updated.isPaymentConfirmed {
|
||||
openTrackingOnce()
|
||||
}
|
||||
}
|
||||
tracker.start(orderId: context.orderId, jwt: DefaultTokenStore().jwt)
|
||||
}
|
||||
.onDisappear {
|
||||
tracker.stop()
|
||||
}
|
||||
}
|
||||
|
||||
private func openTrackingOnce() {
|
||||
guard hasOpenedTracking == false else { return }
|
||||
hasOpenedTracking = true
|
||||
onOpenTracking?()
|
||||
dismiss()
|
||||
}
|
||||
|
||||
private func labeledField(_ label: String, placeholder: String, text: Binding<String>) -> some View {
|
||||
|
||||
@@ -78,6 +78,10 @@ extension HomeView {
|
||||
|
||||
@MainActor
|
||||
func resolveCoordinates(forceRefresh: Bool) async -> (Double, Double)? {
|
||||
guard hasConfiguredAddress() else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !forceRefresh {
|
||||
if let lat = appState.address.latitude, let lng = appState.address.longitude {
|
||||
return (lat, lng)
|
||||
@@ -87,9 +91,6 @@ extension HomeView {
|
||||
appState.address.longitude = cached.1
|
||||
return cached
|
||||
}
|
||||
if hasConfiguredAddress() == false {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
let coordinate = await LocationService.shared.requestLocationAsync(timeoutSeconds: 3)
|
||||
if let coordinate {
|
||||
@@ -103,9 +104,6 @@ extension HomeView {
|
||||
if appState.address.selectedId != nil {
|
||||
return true
|
||||
}
|
||||
if appState.address.latitude != nil, appState.address.longitude != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
let normalized = appState.address.display
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
205
pedi-foods/Sources/PediFoods/Views/Main/OrdersView.swift
Normal file
205
pedi-foods/Sources/PediFoods/Views/Main/OrdersView.swift
Normal file
@@ -0,0 +1,205 @@
|
||||
import SwiftUI
|
||||
|
||||
struct OrdersView: View {
|
||||
@State var isLoading = false
|
||||
@State var errorMessage: String? = nil
|
||||
@State var orders: [AppOrderSummary] = []
|
||||
|
||||
var body: some View {
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
Text("Histórico de Pedidos")
|
||||
.font(AppTypography.heading1)
|
||||
.foregroundStyle(AppColors.textPrimary)
|
||||
.padding(.top, 6)
|
||||
|
||||
if isLoading {
|
||||
ProgressView()
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.top, 24)
|
||||
} else if let errorMessage, errorMessage.isEmpty == false {
|
||||
Text(errorMessage)
|
||||
.font(AppTypography.body)
|
||||
.foregroundStyle(Color.red)
|
||||
.multilineTextAlignment(.center)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.top, 24)
|
||||
} else if orders.isEmpty {
|
||||
Text("Nenhum pedido encontrado.")
|
||||
.font(AppTypography.body)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
.padding(.top, 24)
|
||||
} else {
|
||||
ForEach(orders) { order in
|
||||
NavigationLink {
|
||||
OrderTrackingView(orderId: order.id, initialShortId: order.shortId)
|
||||
} label: {
|
||||
orderRow(order)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.bottom, UIDevice.bottomNotch + 18)
|
||||
}
|
||||
.background(AppColors.backgroundLight)
|
||||
.navigationTitle("Meus Pedidos")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.task {
|
||||
await loadOrders()
|
||||
}
|
||||
}
|
||||
|
||||
private func orderRow(_ order: AppOrderSummary) -> some View {
|
||||
HStack(spacing: 12) {
|
||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||
.fill(AppColors.brandSoft)
|
||||
.frame(width: 44, height: 44)
|
||||
.overlay(
|
||||
Image(systemName: "bag.fill")
|
||||
.foregroundStyle(AppColors.primary)
|
||||
)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Pedido #\(order.shortId ?? order.id)")
|
||||
.font(AppTypography.heading3)
|
||||
.foregroundStyle(AppColors.textPrimary)
|
||||
|
||||
Text(humanReadableStatus(order.status ?? order.paymentStatus ?? "Pendente"))
|
||||
.font(AppTypography.caption)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(alignment: .trailing, spacing: 4) {
|
||||
Text(formatCurrency(order.total ?? 0))
|
||||
.font(AppTypography.heading3)
|
||||
.foregroundStyle(AppColors.textPrimary)
|
||||
if let createdAt = formatDate(order.createdAt) {
|
||||
Text(createdAt)
|
||||
.font(AppTypography.caption)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(14)
|
||||
.background(AppColors.surface)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
||||
}
|
||||
|
||||
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 "Confirmado" }
|
||||
if normalized.contains("OUT_FOR_DELIVERY") || normalized.contains("ROTA") { return "Em rota" }
|
||||
if normalized.contains("COMPLETED") || normalized.contains("DELIVERED") { return "Entregue" }
|
||||
if normalized.contains("CANCEL") { return "Cancelado" }
|
||||
return raw.capitalized
|
||||
}
|
||||
|
||||
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/MM HH:mm"
|
||||
return formatter.string(from: date)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func loadOrders() async {
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
|
||||
let cachedTracked = SessionStateStore.loadTrackedOrders()
|
||||
if cachedTracked.isEmpty == false {
|
||||
let mapped = cachedTracked.map {
|
||||
AppOrderSummary.fromTracked($0)
|
||||
}
|
||||
orders = mergeOrders(apiOrders: orders, trackedOrders: mapped)
|
||||
}
|
||||
|
||||
do {
|
||||
let response = try await ApiService().listOrders()
|
||||
if response.error {
|
||||
errorMessage = response.message ?? "Não foi possível carregar os pedidos."
|
||||
} else {
|
||||
let remote = response.result ?? []
|
||||
orders = mergeOrders(apiOrders: remote, trackedOrders: orders)
|
||||
}
|
||||
} catch {
|
||||
if orders.isEmpty {
|
||||
errorMessage = "Não foi possível carregar os pedidos."
|
||||
}
|
||||
}
|
||||
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
private func mergeOrders(apiOrders: [AppOrderSummary], trackedOrders: [AppOrderSummary]) -> [AppOrderSummary] {
|
||||
var map: [String: AppOrderSummary] = [:]
|
||||
for item in trackedOrders { map[item.id] = item }
|
||||
for item in apiOrders { map[item.id] = item }
|
||||
|
||||
return map.values.sorted { lhs, rhs in
|
||||
let left = lhs.updatedAt ?? lhs.createdAt ?? ""
|
||||
let right = rhs.updatedAt ?? rhs.createdAt ?? ""
|
||||
return left > right
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AppOrderSummary {
|
||||
static func fromTracked(_ tracked: PublicOrderResult) -> AppOrderSummary {
|
||||
AppOrderSummary(
|
||||
id: tracked.id,
|
||||
shortId: tracked.shortId,
|
||||
total: tracked.total,
|
||||
status: tracked.status,
|
||||
paymentStatus: tracked.paymentStatus,
|
||||
paymentMethod: tracked.paymentMethod,
|
||||
deliveryType: tracked.deliveryType,
|
||||
storeName: tracked.storeName,
|
||||
createdAt: tracked.createdAt,
|
||||
updatedAt: tracked.updatedAt
|
||||
)
|
||||
}
|
||||
|
||||
init(
|
||||
id: String,
|
||||
shortId: String?,
|
||||
total: Double?,
|
||||
status: String?,
|
||||
paymentStatus: String?,
|
||||
paymentMethod: String?,
|
||||
deliveryType: String?,
|
||||
storeName: String?,
|
||||
createdAt: String?,
|
||||
updatedAt: String?
|
||||
) {
|
||||
self.id = id
|
||||
self.shortId = shortId
|
||||
self.total = total
|
||||
self.status = status
|
||||
self.paymentStatus = paymentStatus
|
||||
self.paymentMethod = paymentMethod
|
||||
self.deliveryType = deliveryType
|
||||
self.storeName = storeName
|
||||
self.createdAt = createdAt
|
||||
self.updatedAt = updatedAt
|
||||
}
|
||||
}
|
||||
@@ -192,6 +192,7 @@ struct ProfileView: View {
|
||||
private func logout() {
|
||||
tokenStore.clear()
|
||||
SessionStateStore.clearActiveUser()
|
||||
SessionStateStore.clearTrackedOrders()
|
||||
AppContentCache.shared.invalidate()
|
||||
AppImageCache.shared.invalidateAll()
|
||||
appState = AppState()
|
||||
|
||||
Reference in New Issue
Block a user