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,251 @@
import SwiftUI
struct AddAddressFormView: View {
@Environment(\.dismiss) var dismiss
@Environment(\.colorScheme) var colorScheme
@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
}
@ViewBuilder private var logoImage: some View {
#if os(Android)
SwiftUI.Image("pedifoods")
.resizable()
#else
SwiftUI.Image("pedifoods")
.resizable()
#endif
}
var body: some View {
ZStack {
(colorScheme == .dark ? Color.black : AppColors.backgroundLight).ignoresSafeArea()
ScrollView {
VStack(spacing: 0) {
logoImage
.scaledToFit()
.frame(width: 120, height: 120)
Text(existingAddress == nil ? "Novo endereço" : "Editar endereço")
.font(AppTypography.heading1)
.foregroundStyle(colorScheme == .dark ? Color.white : AppColors.textPrimary)
Text(existingAddress == nil ? "Preencha os dados abaixo para adicionar um endereço." : "Atualize os dados do endereço abaixo.")
.font(AppTypography.body)
.foregroundStyle(colorScheme == .dark ? Color.white : AppColors.textPrimary)
.multilineTextAlignment(.center)
.padding(.top, 8)
.padding(.bottom, 20)
VStack(alignment: .leading, spacing: 16) {
LoginField(icon: "tag", placeholder: "Ex: Casa, Trabalho", text: $label)
LoginField(icon: "mail", placeholder: "CEP", 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", text: $address)
LoginField(icon: "number", placeholder: "Número", text: $number)
LoginField(icon: "plus.app", placeholder: "Complemento (opcional)", text: $complement)
LoginField(icon: "square.grid.2x2", placeholder: "Bairro", text: $neighborhood)
LoginField(icon: "building.2", placeholder: "Cidade", text: $city)
LoginField(icon: "map", placeholder: "Estado (UF)", 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, 0)
}
.navigationBarBackButtonHidden(true)
.toolbar(.hidden, for: .navigationBar)
.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
)
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
}
}