import SwiftUI #if canImport(LCEssentials) import LCEssentials #endif struct OrderDetailsView: View { let order: PublicOrderResult let orderId: String let initialShortId: String? @Binding var appState: AppState @Environment(\.dismiss) var dismiss @Environment(\.openURL) var openURL @State private var storeContactPhone: String? = nil @State private var resolvedStoreLogoURL: String? = nil @State private var showCallAlert = false @State private var navigateToStore = false @State private var showClearCartAlert = false var body: some View { LCENavigationView { content } .setLeftButton(image: AnyView(AppBackButtonIcon())) { dismiss() } .setTitle(text: Text("Detalhes do Pedido").font(AppTypography.heading2).foregroundStyle(AppColors.textPrimary)) } private var content: some View { ScrollView(showsIndicators: false) { VStack(spacing: 14) { statusCard storeCard itemsCard totalsCard if hasAddressInfo { addressCard } helpFooter } .padding(.horizontal, 20) .padding(.top, 14) .padding(.bottom, UIDevice.bottomNotch) } .background(AppColors.backgroundLight) .appHiddenNavigationBar() .navigationBarBackButtonHidden(true) .navigationDestination(isPresented: $navigateToStore) { if let storeId = order.storeId, storeId.isEmpty == false { StoreDetailView( storeId: storeId, storeName: order.storeName ?? "Loja", storeCoverURL: nil, storeLogoURL: order.storeLogoURL, storeCategory: nil, storeRating: nil, storeDistance: nil, storeDeliveryFee: nil, appState: $appState ) } } .alert("Ligar para a loja?", isPresented: $showCallAlert) { Button("Ligar para \(order.storeName ?? "a loja")") { if let phone = storeContactPhone { openTel(phone) } } Button("Cancelar", role: .cancel) {} } message: { Text("WhatsApp não encontrado. Deseja ligar para \(order.storeName ?? "a loja")?") } .task { await loadStoreContactPhone() } .alert("Substituir carrinho?", isPresented: $showClearCartAlert) { Button("Limpar e adicionar", role: .destructive) { applyReorder(clearFirst: true) } Button("Cancelar", role: .cancel) {} } message: { Text("Seu carrinho tem itens de \(appState.cart.storeName ?? appState.cart.storeId ?? "outra loja"). Deseja limpar e adicionar itens de \(order.storeName ?? "esta loja")?") } .appBottomSafeAreaInset { VStack { reorderButton .padding(.horizontal, 20) .padding(.top, 10) .padding(.bottom, UIDevice.bottomNotch + 60) } .background(AppColors.backgroundLight.opacity(0.94)) } } 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) if let reason = cancellationReasonText { Text(reason) .font(AppTypography.caption) .foregroundStyle(AppColors.textMuted) .padding(.top, 2) } if let addr = deliveryAddressSummary { HStack(spacing: 4) { Image(systemName: "mappin.circle.fill") .font(.system(size: 11)) .foregroundStyle(AppColors.textMuted) Text(addr) .font(AppTypography.caption) .foregroundStyle(AppColors.textMuted) .lineLimit(2) } .padding(.top, 2) } } Spacer(minLength: 0) } .padding(16) .background(AppColors.surface) .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous)) } private var storeCard: some View { Button { if order.storeId?.isEmpty == false { navigateToStore = true } } label: { HStack(spacing: 12) { AsyncStoreImage(imageURL: resolvedMediaURL(resolvedStoreLogoURL ?? 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(storeSubtitle) .font(AppTypography.body) .foregroundStyle(AppColors.textMuted) } Spacer(minLength: 0) if order.storeId?.isEmpty == false { Text("Ver loja") .font(AppTypography.body) .foregroundStyle(Color(hex: "#A5D645")) } } .padding(16) .background(AppColors.surface) .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous)) } .buttonStyle(.plain) } 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(subtotalValue)) .font(AppTypography.body) .foregroundStyle(AppColors.textMuted) } HStack { Text("Taxa de entrega") .font(AppTypography.body) .foregroundStyle(AppColors.textMuted) Spacer() Text(formatCurrency(deliveryFeeValue)) .font(AppTypography.body) .foregroundStyle(AppColors.textMuted) } HStack { Text("Desconto") .font(AppTypography.body) .foregroundStyle(AppColors.textMuted) Spacer() Text("- \(formatCurrency(discountValue))") .font(AppTypography.body) .foregroundStyle(Color(hex: "#18A957")) } Divider() HStack { Text("Total") .font(AppTypography.heading2) .foregroundStyle(AppColors.textPrimary) Spacer() Text(formatCurrency(totalValue)) .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") { reorder() } .font(AppTypography.heading2) .foregroundStyle(Color(hex: "#0E1A06")) .frame(maxWidth: .infinity, minHeight: 56) .background(order.items.isEmpty ? Color(hex: "#C8F06E").opacity(0.45) : Color(hex: "#C8F06E")) .clipShape(Capsule()) .buttonStyle(.plain) .disabled(order.items.isEmpty) } private var helpFooter: some View { Button { handleHelpTap() } label: { Text("Precisa de ajuda com esse pedido?") .font(AppTypography.body) .foregroundStyle(Color(hex: "#A5D645")) .frame(maxWidth: .infinity, alignment: .center) .padding(.vertical, 8) } .buttonStyle(.plain) } private var subtotal: Double { order.items.reduce(0) { partial, item in partial + (Double(max(1, item.qty ?? 1)) * (item.price ?? 0)) } } private var subtotalValue: Double { order.subtotal ?? subtotal } private var deliveryFeeValue: Double { max(0, order.deliveryFee ?? 0) } private var discountValue: Double { max(0, order.discount ?? 0) } private var totalValue: Double { if let total = order.total { return total } let calculated = subtotalValue + deliveryFeeValue - discountValue return max(0, calculated) } 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 storeSubtitle: String { if deliveryAddressLine2.isEmpty == false { return deliveryAddressLine2 } return "Pedido #\(displayOrderTitle)" } private var deliveryAddressSummary: String? { if let full = order.fullAddress, full.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false { return full.trimmingCharacters(in: .whitespacesAndNewlines) } let line1 = deliveryAddressLine let line2 = deliveryAddressLine2 let combined = [line1, line2].filter { $0.isEmpty == false }.joined(separator: ", ") return combined.isEmpty ? nil : combined } private var cancellationReasonText: String? { guard statusTitle.contains("cancelado"), let reason = order.cancellationReason, reason.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false else { return nil } return "Motivo: \(reason.trimmingCharacters(in: .whitespacesAndNewlines))" } 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 event = order.timeline.first, let date = event.date, date.isEmpty == false { let time = event.time.flatMap { $0.isEmpty ? nil : $0 } let combined = time.map { "\(date) às \($0)" } ?? date return "\(statusDatePrefix) \(combined)" } 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 reorder() { guard order.items.isEmpty == false else { return } let cartStoreId = appState.cart.storeId ?? appState.cart.items.first?.storeId ?? "" let orderStoreId = order.storeId ?? "" let cartHasDifferentStore = cartStoreId.isEmpty == false && orderStoreId.isEmpty == false && cartStoreId != orderStoreId && appState.cart.items.isEmpty == false if cartHasDifferentStore { showClearCartAlert = true } else { applyReorder(clearFirst: false) } } private func applyReorder(clearFirst: Bool) { if clearFirst { appState.cart.clear() } let storeId = order.storeId ?? "" if appState.cart.storeId == nil || appState.cart.storeId?.isEmpty == true { appState.cart.storeId = storeId appState.cart.storeName = order.storeName } var addedCount = 0 for item in order.items { let name = item.name?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" guard name.isEmpty == false else { continue } let qty = max(1, item.qty ?? 1) let price = item.price ?? 0 let cartItem = CartItemState( id: UUID().uuidString, productId: item.productId ?? item.id, storeId: storeId, name: name, imageURL: nil, details: nil, addons: [], quantity: qty, unitPrice: price ) appState.cart.add(item: cartItem) addedCount += qty } let label = addedCount == 1 ? "1 item adicionado ao carrinho." : "\(addedCount) itens adicionados ao carrinho." SnackbarCenter.shared.show(title: label, style: .success, icon: "cart.badge.plus", duration: 2.5) } private func formatDate(_ isoValue: String?) -> String? { guard let isoValue, isoValue.isEmpty == false else { return nil } let iso = ISO8601DateFormatter() let optionSets: [ISO8601DateFormatter.Options] = [ [.withInternetDateTime, .withFractionalSeconds], [.withInternetDateTime], [.withFullDate, .withTime, .withColonSeparatorInTime], [.withFullDate, .withTime, .withColonSeparatorInTime, .withTimeZone], [.withFullDate] ] var date: Date? = nil for options in optionSets { iso.formatOptions = options if let d = iso.date(from: isoValue) { date = d break } } if date == nil { let fallback = DateFormatter() fallback.locale = Locale(identifier: "en_US_POSIX") for fmt in ["yyyy-MM-dd'T'HH:mm:ss.SSSZ", "yyyy-MM-dd'T'HH:mm:ssZ", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd"] { fallback.dateFormat = fmt if let d = fallback.date(from: isoValue) { date = d; break } } } 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) } @MainActor private func loadStoreContactPhone() async { if let inline = order.storePhone, inline.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false { storeContactPhone = inline } guard let storeId = order.storeId?.trimmingCharacters(in: .whitespacesAndNewlines), storeId.isEmpty == false else { return } do { let response = try await ApiService().storeInfo(storeId: storeId) if response.error == false, let result = response.result { let phone = (result.whatsapp ?? result.phone ?? "").trimmingCharacters(in: .whitespacesAndNewlines) if phone.isEmpty == false { storeContactPhone = phone } if let logo = result.logo, logo.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false { resolvedStoreLogoURL = logo } } } catch {} } private func handleHelpTap() { guard let phoneRaw = storeContactPhone, phoneRaw.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false else { SnackbarCenter.shared.show( title: "Telefone da loja indisponível.", style: .warning, icon: "exclamationmark.triangle.fill", duration: 2.8 ) return } if let waURL = makeWhatsAppURL(from: phoneRaw) { openURL(waURL) } else { showCallAlert = true } } private func openTel(_ phoneRaw: String) { let digits = phoneRaw.filter(\.isNumber) if digits.isEmpty { return } if let url = URL(string: "tel://\(digits)") { openURL(url) } } private func makeWhatsAppURL(from phoneRaw: String) -> URL? { var digits = phoneRaw.filter(\.isNumber) if digits.isEmpty { return nil } if digits.hasPrefix("0") { digits = String(digits.drop(while: { $0 == "0" })) } if digits.hasPrefix("55") == false && (digits.count == 10 || digits.count == 11) { digits = "55" + digits } guard digits.count >= 12 else { return nil } return URL(string: "https://wa.me/\(digits)") } 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) } }