segregate files

This commit is contained in:
Daniel Arantes Loverde
2026-02-13 11:01:54 -03:00
parent 4a40936293
commit d7b8b1864a
20 changed files with 2653 additions and 1923 deletions

View File

@@ -0,0 +1,339 @@
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
}
}