import SwiftUI struct AddressesView: View { let message: String? @Binding var appState: AppState var selectionMode: Bool = false @Environment(\.dismiss) var dismiss @State var isLoading = false @State var errorMessage: String? = nil @State var addresses: [CustomerAddress] = [] @State var openAddAddressForm = false @State var editingAddress: CustomerAddress? = nil @State var openSwipeRowId: String? = nil @State var deletingRowId: String? = nil @State var settingDefaultRowId: String? = nil let tabBarClearance: CGFloat = 96 var body: some View { ZStack { AppColors.backgroundLight .ignoresSafeArea() ScrollView(showsIndicators: false) { VStack(spacing: 20) { screenHeader(title: "Meus Endereços", onBack: { dismiss() }) if let message { Text(message) .font(AppTypography.body) .foregroundStyle(AppColors.textPrimary) .multilineTextAlignment(.center) .padding(.horizontal, 20) .padding(.vertical, 14) .frame(maxWidth: .infinity, alignment: .center) .background(AppColors.brandSoft) .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous)) } VStack(spacing: 16) { if isLoading { ProgressView() .padding(.top, 24) } else if let errorMessage { Text(errorMessage) .font(AppTypography.body) .foregroundStyle(AppColors.textMuted) .multilineTextAlignment(.center) .padding(.top, 24) } else if addresses.isEmpty { Text("Nenhum endereço cadastrado") .font(AppTypography.body) .foregroundStyle(AppColors.textMuted) .padding(.top, 24) } else { ForEach(Array(addresses.enumerated()), id: \.offset) { index, address in let rowId = addressRowId(for: address, index: index) let isPrimary: Bool = { if address.isDefault == true { return true } if addresses.contains(where: { $0.isDefault == true }) { return false } if let selectedId = appState.address.selectedId { return address.id == selectedId } return index == 0 }() if selectionMode { Button { selectAddress(address) } label: { AddressCard(item: addressToListItem(address, isPrimary: isPrimary)) } .buttonStyle(.plain) } else if addresses.count > 1 { SwipeToDeleteAddressRow( rowId: rowId, openRowId: $openSwipeRowId, isDeleting: deletingRowId == rowId, onDelete: { deleteAddress(address, rowId: rowId) } ) { AddressCard( item: addressToListItem(address, isPrimary: isPrimary), onEdit: { beginEditing(address) }, onSetDefault: { setDefaultAddress(address, rowId: rowId) } ) .appContentShape(Rectangle()) .simultaneousGesture(TapGesture().onEnded { if openSwipeRowId == rowId { openSwipeRowId = nil } }) .opacity(settingDefaultRowId == rowId ? 0.6 : 1.0) } .id(rowId) .opacity(deletingRowId == rowId ? 0.6 : 1.0) .disabled(deletingRowId != nil || settingDefaultRowId != nil) } else { AddressCard( item: addressToListItem(address, isPrimary: isPrimary), onEdit: { beginEditing(address) } ) .overlay(alignment: .bottom) { Text("Ao menos um endereço deve permanecer") .font(AppTypography.caption) .foregroundStyle(AppColors.textMuted.opacity(0.7)) .padding(.bottom, 8) } } } } } } .padding(.horizontal, 20) .padding(.top, 18) } VStack { Spacer() bottomOverlay .padding(.bottom, tabBarClearance) } } .navigationBarBackButtonHidden(true) .appHiddenNavigationBar() .sheet(isPresented: $openAddAddressForm) { NavigationStack { AddAddressFormView(existingAddress: editingAddress) { updatedAddresses, wasEditing in addresses = updatedAddresses applyPreferredAddress(from: updatedAddresses) let title = wasEditing ? "Endereço atualizado com sucesso." : "Endereço adicionado com sucesso." SnackbarCenter.shared.show(title: title, style: .success, icon: "checkmark.seal.fill", duration: 3.0) } } } .onChange(of: openAddAddressForm) { _, isOpen in if isOpen == false { editingAddress = nil } } .onAppear { if isLoading == false, addresses.isEmpty { Task { await loadAddresses() } } } } var bottomOverlay: some View { ZStack(alignment: .bottom) { Rectangle() .fill(AppColors.backgroundLight) .frame(height: 136) Button(action: { editingAddress = nil openAddAddressForm = true }) { HStack(spacing: 12) { Image(systemName: "mappin.circle.fill") .font(.system(size: 24)) Text("Adicionar novo endereço") .font(AppTypography.heading3) } .foregroundStyle(AppColors.textInverse) .frame(maxWidth: .infinity) .padding(.vertical, 18) .background(AppColors.primary) .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusXL, style: .continuous)) } .buttonStyle(.plain) .padding(.horizontal, 20) .padding(.bottom, 14) } } private func screenHeader(title: String, onBack: @escaping () -> Void) -> some View { ZStack { Text(title) .font(AppTypography.heading2) .foregroundStyle(AppColors.textPrimary) HStack { Button(action: onBack) { Image(systemName: "chevron.left") .font(.system(size: 24, weight: .semibold)) .foregroundStyle(AppColors.textPrimary) .frame(width: 52, height: 52) .background(AppColors.surface) .clipShape(Circle()) .shadow(color: .black.opacity(0.06), radius: 8, y: 2) } .buttonStyle(.plain) Spacer() } } } private func selectAddress(_ address: CustomerAddress) { appState.address.selectedId = address.id let label = (address.label ?? "").trimmingCharacters(in: .whitespacesAndNewlines) appState.address.display = label.isEmpty ? "Defina seu endereco" : label if let lat = address.latLong?.first, let lng = address.latLong?.dropFirst().first { appState.address.latitude = lat appState.address.longitude = lng } else { appState.address.latitude = nil appState.address.longitude = nil } SessionStateStore.saveAddress(appState.address) if selectionMode { dismiss() } } private func beginEditing(_ address: CustomerAddress) { openSwipeRowId = nil editingAddress = address openAddAddressForm = true } private func applyPreferredAddress(from updatedAddresses: [CustomerAddress]) { let selected = resolvePreferredAddress(from: updatedAddresses) appState.address.selectedId = selected?.id appState.address.display = selected?.label?.isEmpty == false ? (selected?.label ?? "Defina seu endereco") : "Defina seu endereco" if let lat = selected?.latLong?.first, let lng = selected?.latLong?.dropFirst().first { appState.address.latitude = lat appState.address.longitude = lng } else { appState.address.latitude = nil appState.address.longitude = nil } SessionStateStore.saveAddress(appState.address) } private func addressRowId(for address: CustomerAddress, index: Int) -> String { if let id = address.id, id.isEmpty == false { return "addr:\(id)" } return "idx:\(index):\(address.label ?? ""):\(address.address ?? ""):\(address.number ?? "")" } private func setDefaultAddress(_ address: CustomerAddress, rowId: String) { guard settingDefaultRowId == nil else { return } settingDefaultRowId = rowId openSwipeRowId = nil Task { var resolvedId = address.id?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" if resolvedId.isEmpty { if let book = try? await ApiService().profile(forceRefresh: true).result?.addressBook { await MainActor.run { addresses = book } resolvedId = book.first { $0.address == address.address && $0.number == address.number && $0.zipCode == address.zipCode }?.id?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" } } guard resolvedId.isEmpty == false else { await MainActor.run { settingDefaultRowId = nil SnackbarCenter.shared.show(title: "Não foi possível identificar o endereço.", style: .error, icon: "xmark.octagon.fill", duration: 3.5) } return } do { let response = try await ApiService().setDefaultAddress(addressId: resolvedId) await MainActor.run { settingDefaultRowId = nil if response.error == false { addresses = addresses.map { addr in let isTarget = (addr.id ?? "") == resolvedId return CustomerAddress( id: addr.id, label: addr.label, address: addr.address, number: addr.number, complement: addr.complement, neighborhood: addr.neighborhood, city: addr.city, state: addr.state, zipCode: addr.zipCode, latLong: addr.latLong, isDefault: isTarget ) } selectAddress(address) SnackbarCenter.shared.show(title: "Endereço principal atualizado.", style: .success, icon: "star.fill", duration: 2.5) } else { SnackbarCenter.shared.show(title: response.message ?? "Não foi possível definir endereço principal.", style: .error, icon: "xmark.octagon.fill", duration: 3.0) } } } catch { await MainActor.run { settingDefaultRowId = nil SnackbarCenter.shared.show(title: "Erro ao atualizar endereço principal.", style: .error, icon: "xmark.octagon.fill", duration: 3.0) } } } } private func deleteAddress(_ address: CustomerAddress, rowId: String) { guard addresses.count > 1, deletingRowId == nil else { return } deletingRowId = rowId openSwipeRowId = nil Task { do { let response = try await ApiService().deleteCustomerAddress(address) await MainActor.run { deletingRowId = nil guard response.error == false else { let message = response.message ?? "Não foi possível excluir o endereço." SnackbarCenter.shared.show(title: message, style: .error, icon: "xmark.octagon.fill", duration: 4.0) return } let updatedAddresses = response.result?.addressBook ?? [] addresses = updatedAddresses applyPreferredAddress(from: updatedAddresses) SnackbarCenter.shared.show(title: "Endereço removido com sucesso.", style: .success, icon: "checkmark.seal.fill", duration: 3.0) } } catch { await MainActor.run { deletingRowId = nil let message = error.localizedDescription SnackbarCenter.shared.show(title: message, style: .error, icon: "xmark.octagon.fill", duration: 4.0) } } } } func addressToListItem(_ address: CustomerAddress, isPrimary: Bool) -> AddressListItem { let title = address.label?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false ? (address.label ?? "Endereço") : "Endereço" let line1 = [address.address, address.number] .compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines) } .filter { $0.isEmpty == false } .joined(separator: ", ") let line2 = [address.neighborhood, address.city, address.state] .compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines) } .filter { $0.isEmpty == false } .joined(separator: ", ") let detail = [line1, line2] .filter { $0.isEmpty == false } .joined(separator: " - ") return AddressListItem( title: title, detail: detail.isEmpty ? "Endereço sem detalhes" : detail, icon: iconName(for: title), isPrimary: isPrimary ) } func iconName(for label: String) -> String { let normalized = label.folding(options: [.diacriticInsensitive, .caseInsensitive], locale: .current) if normalized.contains("casa") { return "house.fill" } if normalized.contains("trabalho") { return "briefcase.fill" } return "mappin.and.ellipse" } @MainActor func loadAddresses() async { isLoading = true errorMessage = nil do { let service = ApiService() let response = try await service.profile(forceRefresh: true) guard response.error == false else { errorMessage = response.message ?? "Não foi possível carregar os endereços." isLoading = false return } if let customer = response.result { appState.profile.id = customer.id appState.profile.name = customer.name appState.profile.email = customer.email appState.profile.phone = customer.phoneNumber ?? appState.profile.phone appState.profile.profilePicture = customer.profilePicture ?? "" appState.profile.notificationsEnabled = customer.notificationsEnabled ?? false appState.profile.faceIdEnabled = customer.faceIdEnabled ?? false SessionStateStore.setActiveUserKey( SessionStateStore.makeUserKey(profileId: customer.id, email: customer.email) ) addresses = customer.addressBook ?? [] } else { addresses = [] } if let selected = resolvePreferredAddress(from: addresses) { appState.address.selectedId = selected.id appState.address.display = selected.label ?? "Defina seu endereco" if let lat = selected.latLong?.first, let lng = selected.latLong?.dropFirst().first { appState.address.latitude = lat appState.address.longitude = lng } SessionStateStore.saveAddress(appState.address) } } catch { errorMessage = error.localizedDescription } isLoading = false } private func resolvePreferredAddress(from list: [CustomerAddress]) -> CustomerAddress? { guard list.isEmpty == false else { return nil } let selectedId = appState.address.selectedId?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" if selectedId.isEmpty == false, let byId = list.first(where: { ($0.id ?? "").trimmingCharacters(in: .whitespacesAndNewlines) == selectedId }) { return byId } let normalizedDisplay = appState.address.display .trimmingCharacters(in: .whitespacesAndNewlines) .folding(options: [.diacriticInsensitive, .caseInsensitive], locale: .current) .lowercased() if normalizedDisplay.isEmpty == false && normalizedDisplay != "defina seu endereco", let byLabel = list.first(where: { (($0.label ?? "") .trimmingCharacters(in: .whitespacesAndNewlines) .folding(options: [.diacriticInsensitive, .caseInsensitive], locale: .current) .lowercased()) == normalizedDisplay }) { return byLabel } if let lat = appState.address.latitude, let lng = appState.address.longitude, let byCoordinate = list.first(where: { address in guard let addrLat = address.latLong?.first, let addrLng = address.latLong?.dropFirst().first else { return false } return abs(addrLat - lat) < 0.00001 && abs(addrLng - lng) < 0.00001 }) { return byCoordinate } return list.first } }