migration
This commit is contained in:
254
PediFoods/Views/Main/AddAddressFormView.swift
Normal file
254
PediFoods/Views/Main/AddAddressFormView.swift
Normal file
@@ -0,0 +1,254 @@
|
||||
import SwiftUI
|
||||
|
||||
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 {
|
||||
ZStack {
|
||||
AppColors.backgroundLight.ignoresSafeArea()
|
||||
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(spacing: 0) {
|
||||
screenHeader(
|
||||
title: existingAddress == nil ? "Novo endereço" : "Editar endereço",
|
||||
onBack: { dismiss() }
|
||||
)
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.bottom, 20)
|
||||
|
||||
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 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 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user