import SwiftUI struct ProfileView: View { @Binding var root: RootFlow @Binding var selectedTab: MainTab let tokenStore: TokenStore @Binding var appState: AppState @State var openAddressesOnboarding = false @State var onboardingMessage: String? = nil var body: some View { VStack(spacing: 20) { header VStack(spacing: 12) { NavigationLink("Pedidos") { OrdersView() } NavigationLink("Enderecos") { AddressesView(message: nil, appState: $appState) } NavigationLink("Ajuda") { Text("Ajuda") } } .font(AppTypography.body) .foregroundStyle(AppColors.textPrimary) NavigationLink(isActive: $openAddressesOnboarding) { AddressesView(message: onboardingMessage, appState: $appState) } label: { EmptyView() } .hidden() Spacer() SecondaryButton(title: "Sair") { tokenStore.clear() appState.session.isAuthenticated = false appState.session.jwt = nil root = .auth } .padding(.horizontal, 20) } .padding(.top, 24) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(AppColors.backgroundLight) .onAppear { guard let message = appState.address.onboardingMessage else { return } onboardingMessage = message appState.address.onboardingMessage = nil openAddressesOnboarding = true } } private var header: some View { VStack(spacing: 8) { Circle() .fill(AppColors.primary) .frame(width: 72, height: 72) .overlay( Text("DL") .font(.headline) .foregroundStyle(AppColors.textInverse) ) Text(appState.profile.name.isEmpty ? "Daniel Loverde" : appState.profile.name) .font(AppTypography.heading2) .foregroundStyle(AppColors.textPrimary) Text(appState.profile.email.isEmpty ? "daniel@pedifoods.com.br" : appState.profile.email) .font(.caption) .foregroundStyle(AppColors.secondary) } } } struct AddressesView: View { let message: String? @Binding var appState: AppState @Environment(\.dismiss) var dismiss @State var isLoading = false @State var errorMessage: String? = nil @State var addresses: [CustomerAddress] = [] 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 AddressCard(item: addressToListItem(address, isPrimary: index == 0)) } } } } .padding(.horizontal, 20) .padding(.top, 18) } VStack { Spacer() bottomOverlay .padding(.bottom, tabBarClearance) } } .navigationBarBackButtonHidden(true) .toolbar(.hidden, for: .navigationBar) .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: {}) { 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) } } 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 } addresses = response.result?.addressBook ?? [] if let first = addresses.first { appState.address.selectedId = first.id appState.address.display = first.label ?? "Defina seu endereco" if let lat = first.latLong?.first, let lng = first.latLong?.dropFirst().first { appState.address.latitude = lat appState.address.longitude = lng } } } catch { errorMessage = error.localizedDescription } isLoading = false } } struct AddressCard: View { let item: AddressListItem var body: some View { HStack(spacing: 14) { icon VStack(alignment: .leading, spacing: 8) { HStack(spacing: 10) { Text(item.title) .font(AppTypography.heading2) .foregroundStyle(AppColors.textPrimary) if item.isPrimary { Text("PRINCIPAL") .font(AppTypography.overline) .foregroundStyle(AppColors.textPrimary) .padding(.horizontal, 10) .padding(.vertical, 6) .background(AppColors.tertiary) .clipShape(Capsule()) .lineLimit(1) .fixedSize(horizontal: true, vertical: false) } } Text(item.detail) .font(AppTypography.body) .foregroundStyle(AppColors.textMuted) .lineLimit(2) } Spacer(minLength: 8) Rectangle() .fill(AppColors.backgroundLight) .frame(width: 1, height: 96) VStack(spacing: 24) { Button(action: {}) { Image(systemName: "pencil") .font(.system(size: 22)) .foregroundStyle(AppColors.textMuted) } Button(action: {}) { Image(systemName: "trash") .font(.system(size: 22)) .foregroundStyle(AppColors.textMuted) } } .frame(width: 40) } .padding(.horizontal, 16) .padding(.vertical, 20) .background(AppColors.surface) .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusXL, style: .continuous)) } var icon: some View { Image(systemName: item.icon) .font(.system(size: 28)) .foregroundStyle(item.isPrimary ? AppColors.primary : AppColors.textPrimary) .frame(width: 84, height: 84) .background(item.isPrimary ? AppColors.brandSoft : AppColors.backgroundLight) .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous)) } } struct AddressListItem: Identifiable { let id = UUID() let title: String let detail: String let icon: String let isPrimary: Bool } struct OrdersView: View { var body: some View { VStack(spacing: 16) { Text("Pedidos") .font(AppTypography.heading1) Text("Historico vazio") .foregroundStyle(AppColors.secondary) } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(AppColors.backgroundLight) } }