import SwiftUI #if canImport(LCEssentials) import LCEssentials #endif struct AddAddressFormView: View { @Environment(\.dismiss) var dismiss @State var label = "" @State var zipCode = "" @State var address = "" @State var number = "" @State var complement = "" @State var neighborhood = "" @State var city = "" @State var state = "" @State var isLoading = false @State var isLookingUpZipCode = false @State var zipLookupMessage: String? = nil @State var lastLookedUpZipCode = "" @State var lookedUpLatitude: Double? = nil @State var lookedUpLongitude: Double? = nil let existingAddress: CustomerAddress? let onSave: ([CustomerAddress], Bool) -> Void private var isFormValid: Bool { !label.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && normalizeZipCodeForAPI(zipCode).count == 8 && !address.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !number.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !neighborhood.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !city.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !state.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } var body: some View { LCENavigationView { content } .setLeftButton(image: AnyView(AppBackButtonIcon())) { dismiss() } .setTitle(text: Text(existingAddress == nil ? "Novo endereço" : "Editar endereço").font(AppTypography.heading2).foregroundStyle(AppColors.textPrimary)) } private var content: some View { ZStack { AppColors.backgroundLight.ignoresSafeArea() ScrollView(showsIndicators: false) { VStack(spacing: 0) { VStack(alignment: .leading, spacing: 16) { LoginField(icon: "tag", placeholder: "Ex: Casa, Trabalho", keyboardType: .default, text: $label) LoginField(icon: "mail", placeholder: "CEP", keyboardType: .numberPad, text: $zipCode) .onChange(of: zipCode) { _, newValue in let masked = formatZipCodeBR(newValue) if masked != newValue { zipCode = masked } let normalized = normalizeZipCodeForAPI(masked) if normalized.count == 8, normalized != lastLookedUpZipCode, !isLookingUpZipCode { Task { await lookupAddressByZipCode(normalized) } } } if isLookingUpZipCode { HStack(spacing: 8) { ProgressView() Text("Buscando endereço pelo CEP...") .font(.caption) .foregroundStyle(AppColors.textMuted) } .padding(.horizontal, 6) } else if let zipLookupMessage { Text(zipLookupMessage) .font(.caption) .foregroundStyle(AppColors.textMuted) .padding(.horizontal, 6) } LoginField(icon: "mappin.and.ellipse", placeholder: "Rua / Avenida", keyboardType: .default, text: $address) LoginField(icon: "number", placeholder: "Número", keyboardType: .default, text: $number) LoginField(icon: "plus.app", placeholder: "Complemento (opcional)", keyboardType: .default, text: $complement) LoginField(icon: "square.grid.2x2", placeholder: "Bairro", keyboardType: .default, text: $neighborhood) LoginField(icon: "building.2", placeholder: "Cidade", keyboardType: .default, text: $city) LoginField(icon: "map", placeholder: "Estado (UF)", keyboardType: .default, text: $state) .onChange(of: state) { _, newValue in let normalized = String(newValue.uppercased().prefix(2)) if normalized != newValue { state = normalized } } } .padding(.horizontal, 24) .padding(.bottom, 20) PrimaryButton(title: existingAddress == nil ? "Salvar endereço" : "Atualizar endereço", image: Image(systemName: "checkmark")) { saveAddress() } .padding(.horizontal, 24) .disabled(!isFormValid || isLoading) .opacity((!isFormValid || isLoading) ? 0.5 : 1.0) .tint(AppColors.tertiary) SecondaryButton(title: "Cancelar") { dismiss() } .padding(.horizontal, 24) .padding(.top, 12) Spacer().frame(height: 120) } } .padding(.top, 18) } .navigationBarBackButtonHidden(true) .appHiddenNavigationBar() .onAppear { populateFromExistingAddressIfNeeded() } } private func saveAddress() { guard !isLoading else { return } let latLong: [Double]? = { if let lat = lookedUpLatitude, let lng = lookedUpLongitude { return [lat, lng] } return nil }() let newAddress = CustomerAddress( id: existingAddress?.id ?? UUID().uuidString, label: clean(label), address: clean(address), number: clean(number), complement: optional(clean(complement)), neighborhood: clean(neighborhood), city: clean(city), state: clean(state), zipCode: optional(normalizeZipCodeForAPI(zipCode)), latLong: latLong, isDefault: existingAddress?.isDefault ) isLoading = true zipLookupMessage = nil Task { do { let response = try await ApiService().saveCustomerAddress(newAddress, replacingAddressId: existingAddress?.id) await MainActor.run { isLoading = false if response.error { zipLookupMessage = response.message ?? "Não foi possível salvar o endereço." SnackbarCenter.shared.show(title: zipLookupMessage ?? "Não foi possível salvar o endereço.", style: .error, icon: "xmark.octagon.fill", duration: 4.0) return } let updatedAddresses = response.result?.addressBook ?? [newAddress] onSave(updatedAddresses, existingAddress != nil) dismiss() } } catch { await MainActor.run { isLoading = false let message = error.localizedDescription zipLookupMessage = message SnackbarCenter.shared.show(title: message, style: .error, icon: "xmark.octagon.fill", duration: 4.0) } } } } private func populateFromExistingAddressIfNeeded() { guard let existingAddress else { return } label = existingAddress.label ?? "" zipCode = formatZipCodeBR(existingAddress.zipCode ?? "") lastLookedUpZipCode = normalizeZipCodeForAPI(zipCode) address = existingAddress.address ?? "" number = existingAddress.number ?? "" complement = existingAddress.complement ?? "" neighborhood = existingAddress.neighborhood ?? "" city = existingAddress.city ?? "" state = String((existingAddress.state ?? "").uppercased().prefix(2)) lookedUpLatitude = existingAddress.latLong?.first lookedUpLongitude = existingAddress.latLong?.dropFirst().first } private func clean(_ value: String) -> String { value.trimmingCharacters(in: .whitespacesAndNewlines) } private func optional(_ value: String) -> String? { value.isEmpty ? nil : value } @MainActor private func lookupAddressByZipCode(_ zip: String) async { isLookingUpZipCode = true zipLookupMessage = nil defer { isLookingUpZipCode = false } do { let response = try await ApiService().lookupZipCode(zip) lastLookedUpZipCode = zip guard response.error == false, let result = response.result else { zipLookupMessage = response.message ?? "Não foi possível consultar este CEP." return } fillAddressFields(with: result) zipLookupMessage = "Endereço preenchido automaticamente." } catch { zipLookupMessage = "Não foi possível consultar o CEP agora." } } private func fillAddressFields(with result: CepLookupResult) { if address.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { address = result.street?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" } if neighborhood.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { neighborhood = result.neighborhood?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" } if city.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { city = result.city?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" } if state.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { state = String((result.state ?? "").uppercased().prefix(2)) } if complement.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { complement = result.complement?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" } lookedUpLatitude = result.latitude lookedUpLongitude = result.longitude } }