diff --git a/PayfritWorks/Views/ProfileScreen.swift b/PayfritWorks/Views/ProfileScreen.swift index 7e550ac..6d8f1fc 100644 --- a/PayfritWorks/Views/ProfileScreen.swift +++ b/PayfritWorks/Views/ProfileScreen.swift @@ -6,8 +6,23 @@ struct ProfileScreen: View { @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 { - appState.userName ?? "Worker" + if let p = profile { + return [p.firstName, p.lastName].filter { !$0.isEmpty }.joined(separator: " ") + } + return appState.userName ?? "Worker" } private var avatarURL: URL? { @@ -57,34 +72,20 @@ struct ProfileScreen: View { .scaleEffect(appearAnimation ? 1 : 0.9) .opacity(appearAnimation ? 1 : 0) - // Info section - VStack(alignment: .leading, spacing: 12) { - Text("Account Information") - .font(.subheadline) - .fontWeight(.semibold) - .foregroundColor(.payfritGreen) - .padding(.horizontal, 16) - - VStack(spacing: 0) { - infoRow(icon: "person.fill", label: "Name", value: displayName) - 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) + // 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) } - .opacity(appearAnimation ? 1 : 0) - .offset(y: appearAnimation ? 0 : 15) - - // Note - Text("Profile information is managed through your employer. Contact your manager if you need to update your details.") - .font(.caption) - .foregroundColor(.secondary) - .multilineTextAlignment(.center) - .padding(.horizontal, 24) - .padding(.top, 8) - .opacity(appearAnimation ? 1 : 0) Spacer() .frame(height: 20) @@ -93,16 +94,17 @@ struct ProfileScreen: View { .background(Color(.systemGroupedBackground).ignoresSafeArea()) .navigationTitle("Profile") .navigationBarTitleDisplayMode(.inline) - .task { - do { - if let urlString = try await APIService.shared.getAvatarUrl(), - let url = URL(string: urlString) { - avatarURLLoaded = url + .toolbar { + if !isEditing && !isLoadingProfile && profileError == nil { + ToolbarItem(placement: .navigationBarTrailing) { + Button("Edit") { startEditing() } + .foregroundColor(.payfritGreen) } - } catch { - // Avatar load failed } } + .task { + await loadAvatarAndProfile() + } .onAppear { withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) { appearAnimation = true @@ -110,6 +112,89 @@ struct ProfileScreen: View { } } + // 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() @@ -143,6 +228,96 @@ struct ProfileScreen: View { .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 {