import SwiftUI struct ProfileScreen: View { @EnvironmentObject var appState: AppState @Environment(\.dismiss) private var dismiss @State private var appearAnimation = false @State private var avatarURLLoaded: URL? // Profile data from API @State private var profile: APIService.UserProfile? @State private var isLoadingProfile = true @State private var profileError: String? // Edit mode @State private var isEditing = false @State private var editFirstName = "" @State private var editLastName = "" @State private var isSaving = false @State private var saveError: String? private var displayName: String { if let p = profile { return [p.firstName, p.lastName].filter { !$0.isEmpty }.joined(separator: " ") } return appState.userName ?? "Worker" } private var avatarURL: URL? { if let loaded = avatarURLLoaded { return loaded } guard let urlString = appState.userPhotoUrl, !urlString.isEmpty else { return nil } return URL(string: urlString) } var body: some View { ScrollView { VStack(spacing: 20) { // Avatar section VStack(spacing: 8) { if let url = avatarURL { AsyncImage(url: url) { phase in switch phase { case .success(let image): image .resizable() .scaledToFill() case .failure: placeholderAvatar default: ProgressView() .frame(width: 80, height: 80) } } .frame(width: 80, height: 80) .clipShape(Circle()) .overlay(Circle().stroke(Color.payfritGreen.opacity(0.3), lineWidth: 2)) .shadow(color: .black.opacity(0.1), radius: 6, y: 3) } else { placeholderAvatar } Text(displayName) .font(.title3) .fontWeight(.bold) .foregroundColor(.primary) Text(appState.roleName) .font(.caption) .foregroundColor(.secondary) } .frame(maxWidth: .infinity) .padding(.top, 16) .scaleEffect(appearAnimation ? 1 : 0.9) .opacity(appearAnimation ? 1 : 0) // Content if isLoadingProfile { LoadingView(message: "Loading profile...") .frame(height: 120) } else if let error = profileError { ErrorView(message: error) { loadProfile() } } else if isEditing { editSection .transition(.opacity.combined(with: .move(edge: .bottom))) } else { infoSection .opacity(appearAnimation ? 1 : 0) .offset(y: appearAnimation ? 0 : 15) } Spacer() .frame(height: 20) } } .background(Color(.systemGroupedBackground).ignoresSafeArea()) .navigationTitle("Profile") .navigationBarTitleDisplayMode(.inline) .toolbar { if !isEditing && !isLoadingProfile && profileError == nil { ToolbarItem(placement: .navigationBarTrailing) { Button("Edit") { startEditing() } .foregroundColor(.payfritGreen) } } } .task { await loadAvatarAndProfile() } .onAppear { withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) { appearAnimation = true } } } // MARK: - Info Section (Read-Only) private var infoSection: some View { VStack(alignment: .leading, spacing: 12) { Text("Account Information") .font(.subheadline) .fontWeight(.semibold) .foregroundColor(.payfritGreen) .padding(.horizontal, 16) VStack(spacing: 0) { if let p = profile { infoRow(icon: "person.fill", label: "First Name", value: p.firstName.isEmpty ? "-" : p.firstName) Divider().padding(.leading, 54) infoRow(icon: "person.fill", label: "Last Name", value: p.lastName.isEmpty ? "-" : p.lastName) Divider().padding(.leading, 54) } infoRow(icon: "briefcase.fill", label: "Role", value: appState.roleName) } .background(Color(.secondarySystemGroupedBackground)) .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) .padding(.horizontal, 16) } } // MARK: - Edit Section private var editSection: some View { VStack(alignment: .leading, spacing: 12) { Text("Edit Profile") .font(.subheadline) .fontWeight(.semibold) .foregroundColor(.payfritGreen) .padding(.horizontal, 16) VStack(spacing: 0) { editRow(icon: "person.fill", label: "First Name", text: $editFirstName) Divider().padding(.leading, 54) editRow(icon: "person.fill", label: "Last Name", text: $editLastName) } .background(Color(.secondarySystemGroupedBackground)) .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) .padding(.horizontal, 16) if let error = saveError { Text(error) .font(.caption) .foregroundColor(.red) .padding(.horizontal, 16) } HStack(spacing: 12) { Button { withAnimation { isEditing = false } saveError = nil } label: { Text("Cancel") .frame(maxWidth: .infinity) } .buttonStyle(.bordered) .disabled(isSaving) Button { saveProfile() } label: { if isSaving { ProgressView() .progressViewStyle(CircularProgressViewStyle(tint: .white)) .frame(maxWidth: .infinity, minHeight: 20) } else { Text("Save") .frame(maxWidth: .infinity) } } .buttonStyle(.borderedProminent) .tint(.payfritGreen) .disabled(isSaving) } .padding(.horizontal, 16) .padding(.top, 4) } } // MARK: - Subviews private var placeholderAvatar: some View { Image(systemName: "person.circle.fill") .resizable() .foregroundStyle(.linearGradient( colors: [Color.payfritGreen.opacity(0.6), Color.payfritGreen.opacity(0.3)], startPoint: .top, endPoint: .bottom )) .frame(width: 80, height: 80) .shadow(color: .black.opacity(0.1), radius: 6, y: 3) } private func infoRow(icon: String, label: String, value: String) -> some View { HStack(spacing: 14) { Image(systemName: icon) .font(.subheadline) .foregroundColor(.payfritGreen) .frame(width: 24) VStack(alignment: .leading, spacing: 2) { Text(label) .font(.caption) .foregroundColor(.secondary) Text(value) .font(.subheadline) .foregroundColor(.primary) } Spacer() } .padding(.horizontal, 16) .padding(.vertical, 12) } private func editRow(icon: String, label: String, text: Binding) -> some View { HStack(spacing: 14) { Image(systemName: icon) .font(.subheadline) .foregroundColor(.payfritGreen) .frame(width: 24) VStack(alignment: .leading, spacing: 2) { Text(label) .font(.caption) .foregroundColor(.secondary) TextField(label, text: text) .font(.subheadline) .textContentType(label == "First Name" ? .givenName : .familyName) .autocorrectionDisabled() } Spacer() } .padding(.horizontal, 16) .padding(.vertical, 12) } // MARK: - Actions private func loadAvatarAndProfile() async { // Load avatar do { if let urlString = try await APIService.shared.getAvatarUrl(), let url = URL(string: urlString) { avatarURLLoaded = url } } catch { // Avatar load failed, continue } // Load profile loadProfile() } private func loadProfile() { isLoadingProfile = true profileError = nil Task { do { let p = try await APIService.shared.getProfile() profile = p isLoadingProfile = false } catch { profileError = error.localizedDescription isLoadingProfile = false } } } private func startEditing() { editFirstName = profile?.firstName ?? "" editLastName = profile?.lastName ?? "" saveError = nil withAnimation { isEditing = true } } private func saveProfile() { isSaving = true saveError = nil Task { do { try await APIService.shared.updateProfile( firstName: editFirstName, lastName: editLastName ) // Refresh profile data let p = try await APIService.shared.getProfile() profile = p // Update appState display name let newName = [p.firstName, p.lastName] .filter { !$0.isEmpty } .joined(separator: " ") appState.userName = newName.isEmpty ? nil : newName isSaving = false withAnimation { isEditing = false } } catch { saveError = error.localizedDescription isSaving = false } } } } #Preview { NavigationStack { ProfileScreen() .environmentObject(AppState()) } }