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 let tabBarClearance: CGFloat = 96 var body: some View { ZStack { AppColors.backgroundLight .ignoresSafeArea() ScrollView(showsIndicators: false) { VStack(spacing: 20) { header 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 isSelected: Bool = { if let selectedId = appState.address.selectedId { return address.id == selectedId } return index == 0 }() if selectionMode { Button { selectAddress(address) } label: { AddressCard(item: addressToListItem(address, isPrimary: isSelected)) } .buttonStyle(.plain) } else { SwipeToDeleteAddressRow( rowId: rowId, openRowId: $openSwipeRowId, isDeleting: deletingRowId == rowId, onDelete: { deleteAddress(address, rowId: rowId) } ) { AddressCard( item: addressToListItem(address, isPrimary: isSelected), onEdit: { beginEditing(address) } ) .contentShape(Rectangle()) .onTapGesture { if openSwipeRowId == rowId { openSwipeRowId = nil } } } .id(rowId) .opacity(deletingRowId == rowId ? 0.6 : 1.0) .disabled(deletingRowId != nil) } } } } } .padding(.horizontal, 20) .padding(.top, 18) } VStack { Spacer() bottomOverlay .padding(.bottom, tabBarClearance) } } .navigationBarBackButtonHidden(true) .toolbar(.hidden, for: .navigationBar) .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 header: some View { ZStack { Text("Meus Endereços") .font(AppTypography.heading2) .foregroundStyle(AppColors.textPrimary) HStack { Button(action: { dismiss() }) { 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) } Spacer() } } } 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 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 = updatedAddresses.first(where: { $0.id == appState.address.selectedId }) ?? updatedAddresses.first 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 deleteAddress(_ address: CustomerAddress, rowId: String) { guard 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() 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 SessionStateStore.setActiveUserKey( SessionStateStore.makeUserKey(profileId: customer.id, email: customer.email) ) addresses = customer.addressBook ?? [] } else { addresses = [] } if let selected = addresses.first(where: { $0.id == appState.address.selectedId }) ?? addresses.first { 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 } }