diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/App.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/App.kt index 10b387a..383460b 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/App.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/App.kt @@ -197,6 +197,9 @@ fun App() { onAddResidence = { navController.navigate(AddResidenceRoute) }, + onNavigateToProfile = { + navController.navigate(ProfileRoute) + }, onLogout = { // Clear token and lookups on logout TokenStorage.clearToken() @@ -351,6 +354,24 @@ fun App() { onTaskUpdated = { navController.popBackStack() } ) } + + composable { + com.mycrib.android.ui.screens.ProfileScreen( + onNavigateBack = { + navController.popBackStack() + }, + onLogout = { + // Clear token and lookups on logout + TokenStorage.clearToken() + LookupsRepository.clear() + isLoggedIn = false + isVerified = false + navController.navigate(LoginRoute) { + popUpTo { inclusive = true } + } + } + ) + } } } diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/models/User.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/models/User.kt index 8a88f57..8287847 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/models/User.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/models/User.kt @@ -62,3 +62,10 @@ data class VerifyEmailResponse( val message: String, val verified: Boolean ) + +@Serializable +data class UpdateProfileRequest( + @SerialName("first_name") val firstName: String? = null, + @SerialName("last_name") val lastName: String? = null, + val email: String? = null +) diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/navigation/Routes.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/navigation/Routes.kt index 2baa8ca..1c2bdd9 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/navigation/Routes.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/navigation/Routes.kt @@ -70,3 +70,6 @@ data class EditTaskRoute( @Serializable object TasksRoute + +@Serializable +object ProfileRoute diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/network/AuthApi.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/network/AuthApi.kt index 4a441d4..9ad4f9c 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/network/AuthApi.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/network/AuthApi.kt @@ -97,4 +97,27 @@ class AuthApi(private val client: HttpClient = ApiClient.httpClient) { ApiResult.Error(e.message ?: "Unknown error occurred") } } + + suspend fun updateProfile(token: String, request: UpdateProfileRequest): ApiResult { + return try { + val response = client.put("$baseUrl/auth/update-profile/") { + header("Authorization", "Token $token") + contentType(ContentType.Application.Json) + setBody(request) + } + + if (response.status.isSuccess()) { + ApiResult.Success(response.body()) + } else { + val errorBody = try { + response.body>() + } catch (e: Exception) { + mapOf("error" to "Profile update failed") + } + ApiResult.Error(errorBody["error"] ?: "Profile update failed", response.status.value) + } + } catch (e: Exception) { + ApiResult.Error(e.message ?: "Unknown error occurred") + } + } } diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ProfileScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ProfileScreen.kt new file mode 100644 index 0000000..ae04eeb --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ProfileScreen.kt @@ -0,0 +1,256 @@ +package com.mycrib.android.ui.screens + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.mycrib.android.ui.components.common.ErrorCard +import com.mycrib.android.viewmodel.AuthViewModel +import com.mycrib.shared.network.ApiResult +import com.mycrib.storage.TokenStorage + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ProfileScreen( + onNavigateBack: () -> Unit, + onLogout: () -> Unit, + viewModel: AuthViewModel = viewModel { AuthViewModel() } +) { + var firstName by remember { mutableStateOf("") } + var lastName by remember { mutableStateOf("") } + var email by remember { mutableStateOf("") } + var errorMessage by remember { mutableStateOf("") } + var isLoading by remember { mutableStateOf(false) } + var successMessage by remember { mutableStateOf("") } + var isLoadingUser by remember { mutableStateOf(true) } + + val updateState by viewModel.updateProfileState.collectAsState() + + // Load current user data + LaunchedEffect(Unit) { + val token = TokenStorage.getToken() + if (token != null) { + val authApi = com.mycrib.shared.network.AuthApi() + when (val result = authApi.getCurrentUser(token)) { + is ApiResult.Success -> { + firstName = result.data.firstName ?: "" + lastName = result.data.lastName ?: "" + email = result.data.email + isLoadingUser = false + } + else -> { + errorMessage = "Failed to load user data" + isLoadingUser = false + } + } + } else { + errorMessage = "Not authenticated" + isLoadingUser = false + } + } + + LaunchedEffect(updateState) { + when (updateState) { + is ApiResult.Success -> { + successMessage = "Profile updated successfully" + isLoading = false + errorMessage = "" + viewModel.resetUpdateProfileState() + } + is ApiResult.Error -> { + errorMessage = (updateState as ApiResult.Error).message + isLoading = false + successMessage = "" + } + is ApiResult.Loading -> { + isLoading = true + errorMessage = "" + successMessage = "" + } + else -> {} + } + } + + Scaffold( + topBar = { + TopAppBar( + title = { Text("Profile", fontWeight = FontWeight.SemiBold) }, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon(Icons.Default.ArrowBack, contentDescription = "Back") + } + }, + actions = { + IconButton(onClick = onLogout) { + Icon(Icons.Default.Logout, contentDescription = "Logout") + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface + ) + ) + } + ) { paddingValues -> + if (isLoadingUser) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } else { + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + Spacer(modifier = Modifier.height(8.dp)) + + // Profile Icon + Icon( + Icons.Default.AccountCircle, + contentDescription = null, + modifier = Modifier.size(80.dp), + tint = MaterialTheme.colorScheme.primary + ) + + Text( + "Update Your Profile", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OutlinedTextField( + value = firstName, + onValueChange = { firstName = it }, + label = { Text("First Name") }, + leadingIcon = { + Icon(Icons.Default.Person, contentDescription = null) + }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + shape = RoundedCornerShape(12.dp) + ) + + OutlinedTextField( + value = lastName, + onValueChange = { lastName = it }, + label = { Text("Last Name") }, + leadingIcon = { + Icon(Icons.Default.Person, contentDescription = null) + }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + shape = RoundedCornerShape(12.dp) + ) + + OutlinedTextField( + value = email, + onValueChange = { email = it }, + label = { Text("Email") }, + leadingIcon = { + Icon(Icons.Default.Email, contentDescription = null) + }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + shape = RoundedCornerShape(12.dp) + ) + + if (errorMessage.isNotEmpty()) { + ErrorCard(message = errorMessage) + } + + if (successMessage.isNotEmpty()) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primaryContainer + ), + shape = RoundedCornerShape(12.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + Icons.Default.CheckCircle, + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimaryContainer + ) + Text( + successMessage, + color = MaterialTheme.colorScheme.onPrimaryContainer, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold + ) + } + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + Button( + onClick = { + if (email.isNotEmpty()) { + viewModel.updateProfile( + firstName = firstName.ifBlank { null }, + lastName = lastName.ifBlank { null }, + email = email + ) + } else { + errorMessage = "Email is required" + } + }, + modifier = Modifier + .fillMaxWidth() + .height(56.dp), + enabled = email.isNotEmpty() && !isLoading, + shape = RoundedCornerShape(12.dp) + ) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + color = MaterialTheme.colorScheme.onPrimary, + strokeWidth = 2.dp + ) + } else { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon(Icons.Default.Save, contentDescription = null) + Text( + "Save Changes", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + } + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidencesScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidencesScreen.kt index df4335a..ef208b9 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidencesScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidencesScreen.kt @@ -25,6 +25,7 @@ fun ResidencesScreen( onResidenceClick: (Int) -> Unit, onAddResidence: () -> Unit, onLogout: () -> Unit, + onNavigateToProfile: () -> Unit = {}, viewModel: ResidenceViewModel = viewModel { ResidenceViewModel() } ) { val myResidencesState by viewModel.myResidencesState.collectAsState() @@ -43,6 +44,9 @@ fun ResidencesScreen( ) }, actions = { + IconButton(onClick = onNavigateToProfile) { + Icon(Icons.Default.AccountCircle, contentDescription = "Profile") + } IconButton(onClick = onLogout) { Icon(Icons.Default.ExitToApp, contentDescription = "Logout") } diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/AuthViewModel.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/AuthViewModel.kt index b7c42ec..8baf320 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/AuthViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/AuthViewModel.kt @@ -28,6 +28,9 @@ class AuthViewModel : ViewModel() { private val _verifyEmailState = MutableStateFlow>(ApiResult.Idle) val verifyEmailState: StateFlow> = _verifyEmailState + private val _updateProfileState = MutableStateFlow>(ApiResult.Idle) + val updateProfileState: StateFlow> = _updateProfileState + fun login(username: String, password: String) { viewModelScope.launch { _loginState.value = ApiResult.Loading @@ -94,6 +97,34 @@ class AuthViewModel : ViewModel() { _verifyEmailState.value = ApiResult.Idle } + fun updateProfile(firstName: String?, lastName: String?, email: String?) { + viewModelScope.launch { + _updateProfileState.value = ApiResult.Loading + val token = TokenStorage.getToken() + if (token != null) { + val result = authApi.updateProfile( + token = token, + request = com.mycrib.shared.models.UpdateProfileRequest( + firstName = firstName, + lastName = lastName, + email = email + ) + ) + _updateProfileState.value = when (result) { + is ApiResult.Success -> ApiResult.Success(result.data) + is ApiResult.Error -> result + else -> ApiResult.Error("Unknown error") + } + } else { + _updateProfileState.value = ApiResult.Error("Not authenticated") + } + } + } + + fun resetUpdateProfileState() { + _updateProfileState.value = ApiResult.Idle + } + fun logout() { viewModelScope.launch { val token = TokenStorage.getToken() diff --git a/iosApp/iosApp/Login/LoginView.swift b/iosApp/iosApp/Login/LoginView.swift index de9f632..4cc483c 100644 --- a/iosApp/iosApp/Login/LoginView.swift +++ b/iosApp/iosApp/Login/LoginView.swift @@ -4,7 +4,8 @@ struct LoginView: View { @StateObject private var viewModel = LoginViewModel() @FocusState private var focusedField: Field? @State private var showingRegister = false - @State private var showingVerifyEmail = false + @State private var showMainTab = false + @State private var showVerification = false enum Field { case username, password @@ -98,22 +99,40 @@ struct LoginView: View { } .navigationTitle("Welcome Back") .navigationBarTitleDisplayMode(.large) - .fullScreenCover(isPresented: $viewModel.isAuthenticated) { - if viewModel.isVerified { - MainTabView() - } else { - VerifyEmailView( - onVerifySuccess: { - // After verification, show main tab view - viewModel.isVerified = true - }, - onLogout: { - // Logout and dismiss verification screen - viewModel.logout() - } - ) + .onChange(of: viewModel.isAuthenticated) { _, isAuth in + if isAuth { + print("isAuthenticated changed to true, isVerified = \(viewModel.isVerified)") + if viewModel.isVerified { + showMainTab = true + } else { + showVerification = true + } } } + .onChange(of: viewModel.isVerified) { _, isVerified in + print("isVerified changed to \(isVerified)") + if isVerified && viewModel.isAuthenticated { + showVerification = false + showMainTab = true + } + } + .fullScreenCover(isPresented: $showMainTab) { + MainTabView() + } + .fullScreenCover(isPresented: $showVerification) { + VerifyEmailView( + onVerifySuccess: { + // After verification, show main tab view + viewModel.isVerified = true + }, + onLogout: { + // Logout and dismiss verification screen + viewModel.logout() + showVerification = false + showMainTab = false + } + ) + } .sheet(isPresented: $showingRegister) { RegisterView() } diff --git a/iosApp/iosApp/Login/LoginViewModel.swift b/iosApp/iosApp/Login/LoginViewModel.swift index 757cbf9..7a89b68 100644 --- a/iosApp/iosApp/Login/LoginViewModel.swift +++ b/iosApp/iosApp/Login/LoginViewModel.swift @@ -82,16 +82,21 @@ class LoginViewModel: ObservableObject { // Store user data and verification status self.currentUser = user self.isVerified = user.verified - - // Initialize lookups repository after successful login - LookupsManager.shared.initialize() - - // Update authentication state - self.isAuthenticated = true self.isLoading = false print("Login successful! Token: token") print("User: \(user.username), Verified: \(user.verified)") + print("isVerified set to: \(self.isVerified)") + + // Initialize lookups repository after successful login + LookupsManager.shared.initialize() + + // Update authentication state AFTER setting verified status + // Small delay to ensure state updates are processed + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.isAuthenticated = true + print("isAuthenticated set to true, isVerified is: \(self.isVerified)") + } } } @@ -113,9 +118,13 @@ class LoginViewModel: ObservableObject { // Reset state isAuthenticated = false + isVerified = false + currentUser = nil username = "" password = "" errorMessage = nil + + print("Logged out - all state reset") } func clearError() { diff --git a/iosApp/iosApp/MainTabView.swift b/iosApp/iosApp/MainTabView.swift index e160ff8..42a622d 100644 --- a/iosApp/iosApp/MainTabView.swift +++ b/iosApp/iosApp/MainTabView.swift @@ -20,77 +20,83 @@ struct MainTabView: View { } .tag(1) - ProfileView() - .tabItem { - Label("Profile", systemImage: "person.fill") - } - .tag(2) + NavigationView { + ProfileTabView() + } + .tabItem { + Label("Profile", systemImage: "person.fill") + } + .tag(2) } } } -struct ProfileView: View { +struct ProfileTabView: View { @StateObject private var loginViewModel = LoginViewModel() - @Environment(\.dismiss) var dismiss + @State private var showingProfileEdit = false var body: some View { - NavigationView { - List { - Section { - HStack { - Image(systemName: "person.circle.fill") - .resizable() - .frame(width: 60, height: 60) - .foregroundColor(.blue) + List { + Section { + HStack { + Image(systemName: "person.circle.fill") + .resizable() + .frame(width: 60, height: 60) + .foregroundColor(.blue) - VStack(alignment: .leading, spacing: 4) { - Text("User Profile") - .font(.headline) - - Text("Manage your account") - .font(.caption) - .foregroundColor(.secondary) - } - } - .padding(.vertical, 8) - } - - Section("Settings") { - NavigationLink(destination: Text("Account Settings")) { - Label("Account Settings", systemImage: "gear") - } - - NavigationLink(destination: Text("Notifications")) { - Label("Notifications", systemImage: "bell") - } - - NavigationLink(destination: Text("Privacy")) { - Label("Privacy", systemImage: "lock.shield") - } - } - - Section { - Button(action: { - loginViewModel.logout() - }) { - Label("Log Out", systemImage: "rectangle.portrait.and.arrow.right") - .foregroundColor(.red) - } - } - - Section { VStack(alignment: .leading, spacing: 4) { - Text("MyCrib") - .font(.caption) - .fontWeight(.semibold) + Text("User Profile") + .font(.headline) - Text("Version 1.0.0") - .font(.caption2) + Text("Manage your account") + .font(.caption) .foregroundColor(.secondary) } } + .padding(.vertical, 8) } - .navigationTitle("Profile") + + Section("Account") { + Button(action: { + showingProfileEdit = true + }) { + Label("Edit Profile", systemImage: "person.crop.circle") + .foregroundColor(.primary) + } + + NavigationLink(destination: Text("Notifications")) { + Label("Notifications", systemImage: "bell") + } + + NavigationLink(destination: Text("Privacy")) { + Label("Privacy", systemImage: "lock.shield") + } + } + + Section { + Button(action: { + loginViewModel.logout() + }) { + Label("Log Out", systemImage: "rectangle.portrait.and.arrow.right") + .foregroundColor(.red) + } + } + + Section { + VStack(alignment: .leading, spacing: 4) { + Text("MyCrib") + .font(.caption) + .fontWeight(.semibold) + + Text("Version 1.0.0") + .font(.caption2) + .foregroundColor(.secondary) + } + } + } + .navigationTitle("Profile") + .sheet(isPresented: $showingProfileEdit) { + ProfileView() } } } diff --git a/iosApp/iosApp/Profile/ProfileView.swift b/iosApp/iosApp/Profile/ProfileView.swift new file mode 100644 index 0000000..a843137 --- /dev/null +++ b/iosApp/iosApp/Profile/ProfileView.swift @@ -0,0 +1,142 @@ +import SwiftUI + +struct ProfileView: View { + @StateObject private var viewModel = ProfileViewModel() + @Environment(\.dismiss) var dismiss + @FocusState private var focusedField: Field? + + enum Field { + case firstName, lastName, email + } + + var body: some View { + NavigationView { + if viewModel.isLoadingUser { + VStack { + ProgressView() + Text("Loading profile...") + .font(.subheadline) + .foregroundColor(.secondary) + .padding(.top, 8) + } + } else { + Form { + Section { + VStack(spacing: 16) { + Image(systemName: "person.circle.fill") + .font(.system(size: 60)) + .foregroundStyle(.blue.gradient) + + Text("Profile Settings") + .font(.title2) + .fontWeight(.bold) + } + .frame(maxWidth: .infinity) + .padding(.vertical) + } + .listRowBackground(Color.clear) + + Section { + TextField("First Name", text: $viewModel.firstName) + .textInputAutocapitalization(.words) + .autocorrectionDisabled() + .focused($focusedField, equals: .firstName) + .submitLabel(.next) + .onSubmit { + focusedField = .lastName + } + + TextField("Last Name", text: $viewModel.lastName) + .textInputAutocapitalization(.words) + .autocorrectionDisabled() + .focused($focusedField, equals: .lastName) + .submitLabel(.next) + .onSubmit { + focusedField = .email + } + } header: { + Text("Personal Information") + } + + Section { + TextField("Email", text: $viewModel.email) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .keyboardType(.emailAddress) + .focused($focusedField, equals: .email) + .submitLabel(.done) + .onSubmit { + viewModel.updateProfile() + } + } header: { + Text("Contact") + } footer: { + Text("Email is required and must be unique") + } + + if let errorMessage = viewModel.errorMessage { + Section { + HStack { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundColor(.red) + Text(errorMessage) + .foregroundColor(.red) + .font(.subheadline) + } + } + } + + if let successMessage = viewModel.successMessage { + Section { + HStack { + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.green) + Text(successMessage) + .foregroundColor(.green) + .font(.subheadline) + } + } + } + + Section { + Button(action: viewModel.updateProfile) { + HStack { + Spacer() + if viewModel.isLoading { + ProgressView() + } else { + Text("Save Changes") + .fontWeight(.semibold) + } + Spacer() + } + } + .disabled(viewModel.isLoading || viewModel.email.isEmpty) + } + } + .navigationTitle("Profile") + .navigationBarTitleDisplayMode(.large) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button("Cancel") { + dismiss() + } + } + } + .onChange(of: viewModel.firstName) { _, _ in + viewModel.clearMessages() + } + .onChange(of: viewModel.lastName) { _, _ in + viewModel.clearMessages() + } + .onChange(of: viewModel.email) { _, _ in + viewModel.clearMessages() + } + } + } + } +} + +#Preview { + ProfileView() +} diff --git a/iosApp/iosApp/Profile/ProfileViewModel.swift b/iosApp/iosApp/Profile/ProfileViewModel.swift new file mode 100644 index 0000000..db61b6a --- /dev/null +++ b/iosApp/iosApp/Profile/ProfileViewModel.swift @@ -0,0 +1,121 @@ +import Foundation +import ComposeApp +import Combine + +@MainActor +class ProfileViewModel: ObservableObject { + // MARK: - Published Properties + @Published var firstName: String = "" + @Published var lastName: String = "" + @Published var email: String = "" + @Published var isLoading: Bool = false + @Published var isLoadingUser: Bool = true + @Published var errorMessage: String? + @Published var successMessage: String? + + // MARK: - Private Properties + private let authApi: AuthApi + private let tokenStorage: TokenStorage + + // MARK: - Initialization + init() { + self.authApi = AuthApi(client: ApiClient_iosKt.createHttpClient()) + self.tokenStorage = TokenStorage() + + // Initialize TokenStorage with platform-specific manager + self.tokenStorage.initialize(manager: TokenManager.init()) + + // Load current user data + loadCurrentUser() + } + + // MARK: - Public Methods + func loadCurrentUser() { + guard let token = tokenStorage.getToken() else { + errorMessage = "Not authenticated" + isLoadingUser = false + return + } + + isLoadingUser = true + errorMessage = nil + + authApi.getCurrentUser(token: token) { result, error in + if let successResult = result as? ApiResultSuccess { + self.handleLoadSuccess(user: successResult.data!) + } else if let error = error { + self.errorMessage = error.localizedDescription + self.isLoadingUser = false + } else { + self.errorMessage = "Failed to load user data" + self.isLoadingUser = false + } + } + } + + func updateProfile() { + guard !email.isEmpty else { + errorMessage = "Email is required" + return + } + + guard let token = tokenStorage.getToken() else { + errorMessage = "Not authenticated" + return + } + + isLoading = true + errorMessage = nil + successMessage = nil + + let request = UpdateProfileRequest( + firstName: firstName.isEmpty ? nil : firstName, + lastName: lastName.isEmpty ? nil : lastName, + email: email + ) + + authApi.updateProfile(token: token, request: request) { result, error in + if let successResult = result as? ApiResultSuccess { + self.handleUpdateSuccess(user: successResult.data!) + } else if let error = error { + self.handleError(message: error.localizedDescription) + } else { + self.handleError(message: "Failed to update profile") + } + } + } + + func clearMessages() { + errorMessage = nil + successMessage = nil + } + + // MARK: - Private Methods + @MainActor + private func handleLoadSuccess(user: User) { + firstName = user.firstName ?? "" + lastName = user.lastName ?? "" + email = user.email + isLoadingUser = false + errorMessage = nil + } + + @MainActor + private func handleUpdateSuccess(user: User) { + firstName = user.firstName ?? "" + lastName = user.lastName ?? "" + email = user.email + isLoading = false + errorMessage = nil + successMessage = "Profile updated successfully" + + print("Profile updated: \(user.firstName ?? "") \(user.lastName ?? "")") + } + + @MainActor + private func handleError(message: String) { + isLoading = false + errorMessage = message + successMessage = nil + } +}