diff --git a/app/src/androidTest/java/com/arygm/quickfix/kaspresso/MainActivityTest.kt b/app/src/androidTest/java/com/arygm/quickfix/kaspresso/MainActivityTest.kt index bb62f12d..5d3a20ed 100644 --- a/app/src/androidTest/java/com/arygm/quickfix/kaspresso/MainActivityTest.kt +++ b/app/src/androidTest/java/com/arygm/quickfix/kaspresso/MainActivityTest.kt @@ -291,6 +291,7 @@ class MainActivityTest : TestCase() { composeTestRule.onNodeWithTag("TimeDropdownMenuFieldEndTime").performClick() composeTestRule.onNodeWithTag("TimeDropdownMenuItem_00:45").performClick() composeTestRule.onNodeWithTag(professionalInfoScreenCategoryField).performClick() + Log.e("TestLog", "debut test") // Select the first category composeTestRule @@ -389,8 +390,9 @@ class MainActivityTest : TestCase() { .perform(click()) onView(withText("Announcement")) // Match the TextView that has the text "Hello World" .perform(click()) - onView(withText("Messages")) // Match the TextView that has the text "Hello World" + onView(withText("Chats")) // Match the TextView that has the text "Hello World" .perform(click()) + onView(withText("Profile")) // Match the TextView that has the text "Hello World" .perform(click()) composeTestRule.onNodeWithText("- Withdraw funds").assertIsDisplayed() @@ -403,19 +405,25 @@ class MainActivityTest : TestCase() { composeTestRule.onNodeWithText("Support").assertIsDisplayed() composeTestRule.onNodeWithText("Legal").assertIsDisplayed() composeTestRule.onNodeWithText("Log out").assertIsDisplayed() + Log.e("TestLog", "avant probbleme") + composeTestRule.waitUntil("find the AccountConfigurationOption", timeoutMillis = 20000) { composeTestRule .onAllNodesWithTag("AccountConfigurationOption") .fetchSemanticsNodes() .isNotEmpty() } + updateAccountConfigurationAndVerify( composeTestRule, "Rame", "Hatime", "28/10/2004", "Rame Hatime", 2) composeTestRule.waitUntil("find the switch", timeoutMillis = 20000) { composeTestRule.onAllNodesWithTag(C.Tag.buttonSwitch).fetchSemanticsNodes().isNotEmpty() } + Log.e("TestLog", "apres probbleme") + composeTestRule.onNodeWithTag(C.Tag.buttonSwitch, useUnmergedTree = true).performClick() } + Log.e("TestLog", "fin test") } @Test diff --git a/app/src/androidTest/java/com/arygm/quickfix/ui/chat/ChatsScreenTest.kt b/app/src/androidTest/java/com/arygm/quickfix/ui/chat/ChatsScreenTest.kt new file mode 100644 index 00000000..db03d0f5 --- /dev/null +++ b/app/src/androidTest/java/com/arygm/quickfix/ui/chat/ChatsScreenTest.kt @@ -0,0 +1,193 @@ +package com.arygm.quickfix.ui.chat + +import androidx.compose.ui.test.* +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.arygm.quickfix.model.account.Account +import com.arygm.quickfix.model.account.AccountRepository +import com.arygm.quickfix.model.account.AccountViewModel +import com.arygm.quickfix.model.messaging.Chat +import com.arygm.quickfix.model.messaging.ChatRepository +import com.arygm.quickfix.model.messaging.ChatViewModel +import com.arygm.quickfix.model.messaging.Message +import com.arygm.quickfix.model.offline.small.PreferencesRepository +import com.arygm.quickfix.model.offline.small.PreferencesViewModel +import com.arygm.quickfix.ui.navigation.NavigationActions +import com.arygm.quickfix.ui.uiMode.appContentUI.workerMode.messages.ChatsScreen +import com.arygm.quickfix.utils.* +import com.google.firebase.Timestamp +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.* +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@RunWith(AndroidJUnit4::class) +@OptIn(ExperimentalCoroutinesApi::class) +class ChatsScreenTest { + + @get:Rule val composeTestRule = createComposeRule() + + @Mock private lateinit var accountRepository: AccountRepository + private lateinit var accountViewModel: AccountViewModel + + @Mock private lateinit var chatRepository: ChatRepository + private lateinit var chatViewModel: ChatViewModel + + @Mock private lateinit var preferencesRepository: PreferencesRepository + private lateinit var preferencesViewModel: PreferencesViewModel + + @Mock private lateinit var navigationActions: NavigationActions + + private val testUserId = "testUserId" + + private val testChats = + listOf( + Chat( + chatId = "1", + workeruid = "testUserId", + useruid = "user1", + quickFixUid = "quickfix1", + messages = + listOf( + Message("msg1", "user1", "Hello there!", Timestamp.now()), + Message("msg2", "worker1", "Hi!", Timestamp.now()))), + Chat( + chatId = "2", + workeruid = "testUserId", + useruid = "user2", + quickFixUid = "quickfix2", + messages = + listOf( + Message("msg1", "user2", "Another message", Timestamp.now()), + Message("msg2", "worker2", "Reply to message", Timestamp.now())))) + + @Before + fun setup() { + MockitoAnnotations.openMocks(this) + + accountViewModel = AccountViewModel(accountRepository) + chatViewModel = ChatViewModel(chatRepository) + preferencesViewModel = PreferencesViewModel(preferencesRepository) + + // Utiliser flowOf(...) pour chaque préférence afin d'émettre une seule valeur et terminer + whenever(preferencesRepository.getPreferenceByKey(eq(UID_KEY))).thenReturn(flowOf(testUserId)) + whenever(preferencesRepository.getPreferenceByKey(eq(APP_MODE_KEY))).thenReturn(flowOf("USER")) + whenever(preferencesRepository.getPreferenceByKey(eq(FIRST_NAME_KEY))) + .thenReturn(flowOf("Tester")) + whenever(preferencesRepository.getPreferenceByKey(eq(LAST_NAME_KEY))).thenReturn(flowOf("User")) + whenever(preferencesRepository.getPreferenceByKey(eq(EMAIL_KEY))) + .thenReturn(flowOf("test@example.com")) + whenever(preferencesRepository.getPreferenceByKey(eq(BIRTH_DATE_KEY))) + .thenReturn(flowOf("01/01/2000")) + whenever(preferencesRepository.getPreferenceByKey(eq(IS_WORKER_KEY))).thenReturn(flowOf(false)) + + val mainAccount = + Account( + uid = testUserId, + firstName = "Tester", + lastName = "User", + email = "test@example.com", + birthDate = Timestamp.now(), + isWorker = false, + activeChats = listOf("1", "2")) + + whenever(accountRepository.getAccountById(eq(testUserId), any(), any())).thenAnswer { + val onSuccess = it.arguments[1] as (Account?) -> Unit + onSuccess(mainAccount) + } + + // user1 + whenever(accountRepository.getAccountById(eq("user1"), any(), any())).thenAnswer { + val onSuccess = it.arguments[1] as (Account?) -> Unit + onSuccess( + Account( + uid = "user1", + firstName = "John", + lastName = "Doe", + email = "john@example.com", + birthDate = Timestamp.now(), + isWorker = false, + activeChats = emptyList())) + } + + // user2 + whenever(accountRepository.getAccountById(eq("user2"), any(), any())).thenAnswer { + val onSuccess = it.arguments[1] as (Account?) -> Unit + onSuccess( + Account( + uid = "user2", + firstName = "Jane", + lastName = "Smith", + email = "jane@example.com", + birthDate = Timestamp.now(), + isWorker = false, + activeChats = emptyList())) + } + + // Mock des chats, appelés en interne par le ChatViewModel + + } + + @Test + fun chatsScreen_displaysChatsAndNavigatesOnClick() = runTest { + whenever(chatRepository.getChatByChatUid(eq("1"), any(), any())).thenAnswer { + val onSuccess = it.arguments[1] as (Chat?) -> Unit + onSuccess(testChats[0]) + } + whenever(chatRepository.getChatByChatUid(eq("2"), any(), any())).thenAnswer { + val onSuccess = it.arguments[1] as (Chat?) -> Unit + onSuccess(testChats[1]) + } + // runTest pour pouvoir appeler du suspend si nécessaire + composeTestRule.setContent { + ChatsScreen( + navigationActions = navigationActions, + accountViewModel = accountViewModel, + chatViewModel = chatViewModel, + preferencesViewModel = preferencesViewModel) + } + + // Vérifie que "John" et "Jane" s'affichent + composeTestRule.onNodeWithText("John").assertExists() + composeTestRule.onNodeWithText("Jane").assertExists() + + // Clique sur "John" + composeTestRule.onNodeWithText("John").performClick() + + // Vérifie la navigation + verify(navigationActions).navigateTo(any()) + } + + @Test + fun chatsScreen_filtersChatsBasedOnSearchQuery() = runTest { + whenever(chatRepository.getChatByChatUid(eq("1"), any(), any())).thenAnswer { + val onSuccess = it.arguments[1] as (Chat?) -> Unit + onSuccess(testChats[0]) + } + whenever(chatRepository.getChatByChatUid(eq("2"), any(), any())).thenAnswer { + val onSuccess = it.arguments[1] as (Chat?) -> Unit + onSuccess(testChats[1]) + } + composeTestRule.setContent { + ChatsScreen( + navigationActions = navigationActions, + accountViewModel = accountViewModel, + chatViewModel = chatViewModel, + preferencesViewModel = preferencesViewModel) + } + + // Entrer "Jane" + composeTestRule.onNodeWithTag("customSearchField").performTextInput("Jane") + + // Vérifie que seul "Jane" est visible + composeTestRule.onAllNodesWithText("Jane").filter(hasTestTag("ChatItem")).assertCountEquals(1) + composeTestRule.onNodeWithText("John").assertDoesNotExist() + } +} diff --git a/app/src/androidTest/java/com/arygm/quickfix/ui/home/MessageUserNoModeScreenTest.kt b/app/src/androidTest/java/com/arygm/quickfix/ui/home/MessageUserNoModeScreenTest.kt index 286ea9bf..73271c1e 100644 --- a/app/src/androidTest/java/com/arygm/quickfix/ui/home/MessageUserNoModeScreenTest.kt +++ b/app/src/androidTest/java/com/arygm/quickfix/ui/home/MessageUserNoModeScreenTest.kt @@ -12,11 +12,7 @@ import com.arygm.quickfix.model.account.AccountViewModel import com.arygm.quickfix.model.bill.BillField import com.arygm.quickfix.model.bill.Units import com.arygm.quickfix.model.locations.Location -import com.arygm.quickfix.model.messaging.Chat -import com.arygm.quickfix.model.messaging.ChatRepository -import com.arygm.quickfix.model.messaging.ChatStatus -import com.arygm.quickfix.model.messaging.ChatViewModel -import com.arygm.quickfix.model.messaging.Message +import com.arygm.quickfix.model.messaging.* import com.arygm.quickfix.model.offline.small.PreferencesRepositoryDataStore import com.arygm.quickfix.model.offline.small.PreferencesViewModel import com.arygm.quickfix.model.profile.Profile @@ -59,13 +55,13 @@ class MessageUserNoModeScreenTest { private lateinit var preferencesRepositoryDataStore: PreferencesRepositoryDataStore private lateinit var accountRepository: AccountRepository private lateinit var accountViewModel: AccountViewModel + private lateinit var workerProfileRepositoryFirestore: WorkerProfileRepositoryFirestore private lateinit var workerViewModel: ProfileViewModel // Implémentation de test pour Service data class TestService(override val name: String) : Service - // Class-level flows private val appModeFlow = MutableStateFlow("USER") private val userIdFlow = MutableStateFlow("testUserId") @@ -123,10 +119,11 @@ class MessageUserNoModeScreenTest { preferencesViewModel = PreferencesViewModel(preferencesRepositoryDataStore) accountRepository = mock(AccountRepository::class.java) accountViewModel = AccountViewModel(accountRepository) + workerProfileRepositoryFirestore = mock(WorkerProfileRepositoryFirestore::class.java) workerViewModel = ProfileViewModel(workerProfileRepositoryFirestore) - // Mock repository methods + // Mock repository init whenever(quickFixRepository.init(any())).thenAnswer { val onSuccess = it.getArgument<() -> Unit>(0) onSuccess() @@ -150,7 +147,7 @@ class MessageUserNoModeScreenTest { .whenever(chatRepository) .getChats(any(), any()) - // Mock getQuickFixByUid to return fakeQuickFix when called with fakeQuickFixUid + // Mock getQuickFixByUid to return fakeQuickFix doAnswer { invocation -> val uid = invocation.getArgument(0) val onResult = invocation.getArgument<(QuickFix?) -> Unit>(1) @@ -165,13 +162,13 @@ class MessageUserNoModeScreenTest { .getQuickFixById(any(), any(), any()) } - // Initialize ViewModels with mocked repositories + // Initialize ViewModels chatViewModel = ChatViewModel(chatRepository) quickFixViewModel = QuickFixViewModel(quickFixRepository) runBlocking { - chatViewModel.getChats() // Load chats - quickFixViewModel.getQuickFixes() // Load QuickFixes if necessary + chatViewModel.getChats() + quickFixViewModel.getQuickFixes() } doAnswer { invocation -> @@ -186,7 +183,6 @@ class MessageUserNoModeScreenTest { @Test fun testQuickFixDetailsAreDisplayed() { - // Ensure the QuickFixViewModel has the QuickFix data composeTestRule.setContent { QuickFixTheme { MessageScreen( @@ -199,12 +195,12 @@ class MessageUserNoModeScreenTest { } } - // Verify the display of quickFixDetails composeTestRule.onNodeWithTag("quickFixDetails").assertIsDisplayed() } @Test fun testMessagesAreDisplayed() { + runBlocking { doAnswer { invocation -> val onSuccess = invocation.getArgument<(List) -> Unit>(0) @@ -240,7 +236,6 @@ class MessageUserNoModeScreenTest { @Test fun testAcceptedStatusShowsActiveConversationText() { - // Le statut du chat est ACCEPTED par défaut composeTestRule.setContent { QuickFixTheme { MessageScreen( @@ -259,11 +254,9 @@ class MessageUserNoModeScreenTest { @Test fun testGettingSuggestionsShowsSuggestions() { runBlocking { - // Arrange: On modifie le chat pour qu'il ait le statut GETTING_SUGGESTIONS val gettingSuggestionsChat = fakeChat.copy(chatStatus = ChatStatus.GETTING_SUGGESTIONS) doAnswer { invocation -> val onSuccess = invocation.getArgument<(List) -> Unit>(0) - // Retourne ce chat modifié onSuccess(listOf(gettingSuggestionsChat)) null } @@ -276,7 +269,6 @@ class MessageUserNoModeScreenTest { quickFixViewModel.getQuickFixes() } - // Act composeTestRule.setContent { QuickFixTheme { MessageScreen( @@ -289,21 +281,17 @@ class MessageUserNoModeScreenTest { } } - // Assert : Vérifier que les suggestions sont affichées composeTestRule.onNodeWithText("How is it going?").assertIsDisplayed() composeTestRule.onNodeWithText("Is the time and day okay for you?").assertIsDisplayed() - composeTestRule.onNodeWithText("I can’t wait to work with you!").assertIsDisplayed() } } @Test fun testWorkerRefusedStatusShowsRefusalMessage() { runBlocking { - // Arrange: On modifie le chat pour qu'il ait le statut WORKER_REFUSED val refusedChat = fakeChat.copy(chatStatus = ChatStatus.WORKER_REFUSED) doAnswer { invocation -> val onSuccess = invocation.getArgument<(List) -> Unit>(0) - // Retourne ce chat refusé onSuccess(listOf(refusedChat)) null } @@ -316,7 +304,6 @@ class MessageUserNoModeScreenTest { quickFixViewModel.getQuickFixes() } - // Act composeTestRule.setContent { QuickFixTheme { MessageScreen( @@ -329,7 +316,6 @@ class MessageUserNoModeScreenTest { } } - // Assert : Vérifier que le message de refus est affiché composeTestRule .onNodeWithText( "John the Worker has rejected the QuickFix. No big deal! Contact another worker from the search screen! 😊") @@ -340,21 +326,14 @@ class MessageUserNoModeScreenTest { @Test fun testSendingMessageWhenAcceptedWorks() { runBlocking { - // Arrange: Le statut est ACCEPTED par défaut dans fakeChat - // On veut vérifier que l'envoi de message fonctionne. On va moquer sendMessage pour vérifier - // qu'il est appelé. doAnswer { invocation -> - val chat = invocation.getArgument(0) - val message = invocation.getArgument(1) val onSuccess = invocation.getArgument<() -> Unit>(2) - // Simule le succès onSuccess() null } .whenever(chatRepository) .sendMessage(any(), any(), any(), any()) - // Act composeTestRule.setContent { QuickFixTheme { MessageScreen( @@ -367,13 +346,9 @@ class MessageUserNoModeScreenTest { } } - // On entre un texte dans le champ de message composeTestRule.onNodeWithTag("messageTextField").performTextInput("Hello from test!") - // On clique sur le bouton d'envoi composeTestRule.onNodeWithTag("sendButton").performClick() - // Assert - // Vérifier que sendMessage a été appelé avec le message "Hello from test!" verify(chatRepository) .sendMessage(eq(fakeChat), argThat { content == "Hello from test!" }, any(), any()) } @@ -382,7 +357,6 @@ class MessageUserNoModeScreenTest { @Test fun testWaitingForResponseStatusAsUserShowsAwaitingConfirmation() { runBlocking { - // Arrange: On modifie le chat pour qu'il ait le statut WAITING_FOR_RESPONSE val waitingChat = fakeChat.copy(chatStatus = ChatStatus.WAITING_FOR_RESPONSE) doAnswer { invocation -> val onSuccess = invocation.getArgument<(List) -> Unit>(0) @@ -398,7 +372,6 @@ class MessageUserNoModeScreenTest { quickFixViewModel.getQuickFixes() } - // Act composeTestRule.setContent { QuickFixTheme { MessageScreen( @@ -411,7 +384,6 @@ class MessageUserNoModeScreenTest { } } - // Assert : Vérifier le texte d'attente composeTestRule .onNodeWithText("Awaiting confirmation from John the Worker...") .assertIsDisplayed() @@ -421,7 +393,6 @@ class MessageUserNoModeScreenTest { @Test fun testWaitingForResponseStatusAsWorkerShowsAcceptRejectButtons() { runBlocking { - // Arrange: On modifie le chat pour qu'il ait le statut WAITING_FOR_RESPONSE val waitingChat = fakeChat.copy(chatStatus = ChatStatus.WAITING_FOR_RESPONSE) doAnswer { invocation -> val onSuccess = invocation.getArgument<(List) -> Unit>(0) @@ -440,7 +411,6 @@ class MessageUserNoModeScreenTest { quickFixViewModel.getQuickFixes() } - // Act composeTestRule.setContent { QuickFixTheme { MessageScreen( @@ -453,7 +423,6 @@ class MessageUserNoModeScreenTest { } } - // Assert : Vérifier les boutons accept/reject composeTestRule .onNodeWithText("Would you like to accept this QuickFix request?") .assertIsDisplayed() @@ -465,10 +434,7 @@ class MessageUserNoModeScreenTest { @Test fun testClickingOnSuggestionSendsMessageAndUpdatesChat() { runBlocking { - // Arrange: On modifie le chat pour qu'il ait le statut GETTING_SUGGESTIONS val gettingSuggestionsChat = fakeChat.copy(chatStatus = ChatStatus.GETTING_SUGGESTIONS) - - // On force le repository à retourner ce chat doAnswer { invocation -> val onSuccess = invocation.getArgument<(List) -> Unit>(0) onSuccess(listOf(gettingSuggestionsChat)) @@ -483,16 +449,8 @@ class MessageUserNoModeScreenTest { quickFixViewModel.getQuickFixes() } - // Comme isUser = true, on a les suggestions côté user : "How is it going?", etc. - val suggestions = - listOf( - "How is it going?", - "Is the time and day okay for you?", - "I can’t wait to work with you!") + val suggestions = listOf("How is it going?", "Is the time and day okay for you?") - // On veut vérifier que lorsque l'utilisateur clique sur une suggestion, updateChat et - // sendMessage sont appelés. - // On va mocker sendMessage et updateChat pour capturer leurs arguments. doAnswer { invocation -> val onSuccess = invocation.getArgument<() -> Unit>(1) onSuccess() @@ -509,7 +467,6 @@ class MessageUserNoModeScreenTest { .whenever(chatRepository) .sendMessage(any(), any(), any(), any()) - // Act composeTestRule.setContent { QuickFixTheme { MessageScreen( @@ -522,19 +479,15 @@ class MessageUserNoModeScreenTest { } } - // Assert: Les suggestions doivent être affichées suggestions.forEach { suggestion -> composeTestRule.onNodeWithText(suggestion).assertIsDisplayed() } - // On clique sur la première suggestion par exemple val chosenSuggestion = suggestions.first() composeTestRule.onNodeWithText(chosenSuggestion).performClick() - // Vérifier que updateChat a été appelé pour passer le statut en ACCEPTED verify(chatRepository).updateChat(argThat { chatStatus == ChatStatus.ACCEPTED }, any(), any()) - // Vérifier que sendMessage a été appelé avec le message "How is it going?" verify(chatRepository) .sendMessage( argThat { chatId == gettingSuggestionsChat.chatId }, diff --git a/app/src/androidTest/java/com/arygm/quickfix/ui/profile/ProfileConfigurationUserNoModeScreenTest.kt b/app/src/androidTest/java/com/arygm/quickfix/ui/profile/ProfileConfigurationUserNoModeScreenTest.kt index ef7cf8d3..453ac8dd 100644 --- a/app/src/androidTest/java/com/arygm/quickfix/ui/profile/ProfileConfigurationUserNoModeScreenTest.kt +++ b/app/src/androidTest/java/com/arygm/quickfix/ui/profile/ProfileConfigurationUserNoModeScreenTest.kt @@ -1,9 +1,7 @@ package com.arygm.quickfix.ui.profile -import android.graphics.Bitmap import androidx.compose.ui.test.* import androidx.compose.ui.test.junit4.createComposeRule -import androidx.datastore.preferences.core.Preferences import com.arygm.quickfix.model.account.Account import com.arygm.quickfix.model.account.AccountRepository import com.arygm.quickfix.model.account.AccountViewModel @@ -12,19 +10,14 @@ import com.arygm.quickfix.model.offline.small.PreferencesViewModel import com.arygm.quickfix.ui.navigation.NavigationActions import com.arygm.quickfix.ui.theme.QuickFixTheme import com.arygm.quickfix.ui.uiMode.appContentUI.userModeUI.profile.AccountConfigurationScreen -import com.arygm.quickfix.utils.IS_WORKER_KEY +import com.arygm.quickfix.utils.* import com.google.firebase.Timestamp import kotlinx.coroutines.flow.flowOf import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Rule import org.junit.Test -import org.mockito.Mockito.* -import org.mockito.kotlin.any -import org.mockito.kotlin.argumentCaptor -import org.mockito.kotlin.mock -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever +import org.mockito.kotlin.* class ProfileConfigurationUserNoModeScreenTest { @@ -43,6 +36,7 @@ class ProfileConfigurationUserNoModeScreenTest { lastName = "Doe", birthDate = Timestamp.now(), email = "john.doe@example.com", + isWorker = true, profilePicture = "https://example.com/profile.jpg") @Before @@ -53,27 +47,40 @@ class ProfileConfigurationUserNoModeScreenTest { preferencesRepository = mock() preferencesViewModel = PreferencesViewModel(preferencesRepository) - // Mock preferences repository to provide test data - whenever(preferencesRepository.getPreferenceByKey(any>())) - .thenReturn(flowOf("testValue")) - whenever(preferencesRepository.getPreferenceByKey(com.arygm.quickfix.utils.UID_KEY)) - .thenReturn(flowOf("testUid")) - whenever(preferencesRepository.getPreferenceByKey(com.arygm.quickfix.utils.FIRST_NAME_KEY)) - .thenReturn(flowOf("John")) - whenever(preferencesRepository.getPreferenceByKey(com.arygm.quickfix.utils.LAST_NAME_KEY)) - .thenReturn(flowOf("Doe")) - whenever(preferencesRepository.getPreferenceByKey(com.arygm.quickfix.utils.EMAIL_KEY)) + // Mock des préférences + whenever(preferencesRepository.getPreferenceByKey(UID_KEY)).thenReturn(flowOf("testUid")) + whenever(preferencesRepository.getPreferenceByKey(FIRST_NAME_KEY)).thenReturn(flowOf("John")) + whenever(preferencesRepository.getPreferenceByKey(LAST_NAME_KEY)).thenReturn(flowOf("Doe")) + whenever(preferencesRepository.getPreferenceByKey(EMAIL_KEY)) .thenReturn(flowOf("john.doe@example.com")) - whenever(preferencesRepository.getPreferenceByKey(com.arygm.quickfix.utils.BIRTH_DATE_KEY)) + whenever(preferencesRepository.getPreferenceByKey(BIRTH_DATE_KEY)) .thenReturn(flowOf("01/01/1990")) - whenever(preferencesRepository.getPreferenceByKey(com.arygm.quickfix.utils.PROFILE_PICTURE_KEY)) + whenever(preferencesRepository.getPreferenceByKey(PROFILE_PICTURE_KEY)) .thenReturn(flowOf("https://example.com/profile.jpg")) whenever(preferencesRepository.getPreferenceByKey(IS_WORKER_KEY)).thenReturn(flowOf(true)) + + // Mock fetchUserAccount pour retourner testUserProfile + doAnswer { invocation -> + val onResult = invocation.getArgument<(Account?) -> Unit>(1) + onResult(testUserProfile) + null + } + .whenever(accountRepository) + .getAccountById(eq("testUid"), any(), any()) + + // IMPORTANT : On force l'échec du chargement de l'image pour que profileBitmap reste null + doAnswer { invocation -> + val onFailure = invocation.getArgument<(Exception) -> Unit>(2) + onFailure(Exception("Failed to load image")) + null + } + .whenever(accountRepository) + .fetchAccountProfileImageAsBitmap(eq("testUid"), any(), any()) } @Test fun testUpdateFirstNameAndLastName() { - // Mock account update + // Mock updateAccount doAnswer { invocation -> val onSuccess = invocation.getArgument<() -> Unit>(1) onSuccess() @@ -91,30 +98,29 @@ class ProfileConfigurationUserNoModeScreenTest { } } - // Update first name and last name + // Attendre que l'UI soit stable + composeTestRule.waitForIdle() + + // Modifier les champs de nom/prénom pour déclencher isModified composeTestRule.onNodeWithTag("firstNameInput").performTextReplacement("Jane") composeTestRule.onNodeWithTag("lastNameInput").performTextReplacement("Smith") - // Click Save button + // Le bouton doit s'activer + composeTestRule.onNodeWithTag("SaveButton").assertIsEnabled() + + // Cliquer sur Save composeTestRule.onNodeWithTag("SaveButton").performClick() - // Verify account update - val profileCaptor = argumentCaptor() - verify(accountRepository).updateAccount(profileCaptor.capture(), any(), any()) - assertEquals("Jane", profileCaptor.firstValue.firstName) - assertEquals("Smith", profileCaptor.firstValue.lastName) + // Vérifier l'appel + val captor = argumentCaptor() + verify(accountRepository).updateAccount(captor.capture(), any(), any()) + assertEquals("Jane", captor.firstValue.firstName) + assertEquals("Smith", captor.firstValue.lastName) } @Test fun testUpdateEmailWithValidEmail() { - // Mock account exists check and update - doAnswer { invocation -> - val onSuccess = invocation.getArgument<(Pair) -> Unit>(1) - onSuccess(Pair(false, null)) - null - } - .whenever(accountRepository) - .accountExists(any(), any(), any()) + // Mock updateAccount doAnswer { invocation -> val onSuccess = invocation.getArgument<() -> Unit>(1) onSuccess() @@ -132,44 +138,43 @@ class ProfileConfigurationUserNoModeScreenTest { } } - // Update email + composeTestRule.waitForIdle() + + // Changer l'email vers un email valide différent composeTestRule.onNodeWithTag("emailInput").performTextReplacement("jane.smith@example.com") - // Click Save button + // Le bouton doit s'activer + composeTestRule.onNodeWithTag("SaveButton").assertIsEnabled() + + // Cliquer sur Save composeTestRule.onNodeWithTag("SaveButton").performClick() - // Verify account update - val profileCaptor = argumentCaptor() - verify(accountRepository).updateAccount(profileCaptor.capture(), any(), any()) - assertEquals("jane.smith@example.com", profileCaptor.firstValue.email) + // Vérifier l'appel + val captor = argumentCaptor() + verify(accountRepository).updateAccount(captor.capture(), any(), any()) + assertEquals("jane.smith@example.com", captor.firstValue.email) } @Test fun testUpdateProfilePicture() { - // Mock image upload + // Mock uploadAccountImages doAnswer { invocation -> - val onSuccess = - invocation.getArgument<(List) -> Unit>( - 2) // Third argument is the success callback - onSuccess( - listOf("https://example.com/new-profile.jpg")) // Simulate success with a new profile - // picture URL + val onSuccess = invocation.getArgument<(List) -> Unit>(2) + onSuccess(listOf("https://example.com/new-profile.jpg")) null } .whenever(accountRepository) .uploadAccountImages(any(), any(), any(), any()) - // Mock account update + // Mock updateAccount doAnswer { invocation -> - val onSuccess = - invocation.getArgument<() -> Unit>(1) // Second argument is the success callback - onSuccess() // Simulate success + val onSuccess = invocation.getArgument<() -> Unit>(1) + onSuccess() null } .whenever(accountRepository) .updateAccount(any(), any(), any()) - // Set up the UI composeTestRule.setContent { QuickFixTheme { AccountConfigurationScreen( @@ -179,25 +184,40 @@ class ProfileConfigurationUserNoModeScreenTest { } } - // Simulate clicking the profile image to trigger selection + composeTestRule.waitForIdle() + + // Simuler un changement sur le prénom pour activer isModified + composeTestRule.onNodeWithTag("firstNameInput").performTextReplacement("Mary") + + // Maintenant on simule le clic sur l'image : dans le code réel, cela ouvre la feuille de + // sélection d'image. On va directement appeler uploadAccountImages via l'accountViewModel + // pour simuler l'upload. Pour cela, on a besoin que l'imageChanged soit vrai. + // Ici, on ne dispose pas du code de sélection d'image dans le test, mais on va simuler + // l'action en forçant l'état. + + // On clique sur l'image pour montrer qu'on a choisi une nouvelle image (dans la vraie app, + // cela ouvrirait une bottom sheet). composeTestRule.onNodeWithTag("ProfileImage").performClick() - // Simulate selecting a new profile image (mock bitmap) - val testBitmap: Bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888) - composeTestRule.runOnUiThread { - accountViewModel.uploadAccountImages( - "testUid", listOf(testBitmap), onSuccess = {}, onFailure = {}) - } + // Simuler l'action interne qui met imageChanged à true. On va juste mettre un nouveau + // bitmap et revalider le bouton Save. + // Normalement, cette action se fait quand l'utilisateur sélectionne une image. + // On va juste retaper dans le champ email pour redéclencher isModified + composeTestRule.onNodeWithTag("emailInput").performTextReplacement("john.new@example.com") - // Simulate changing an input field to ensure `isModified` is true - composeTestRule.onNodeWithTag("firstNameInput").performTextReplacement("Jane") + // Le bouton Save devrait être activé + composeTestRule.onNodeWithTag("SaveButton").assertIsEnabled() - // Click Save button to trigger account update + // Cliquer sur Save pour déclencher updateAccount avec la nouvelle photo composeTestRule.onNodeWithTag("SaveButton").performClick() - // Verify that the account update includes the new profile picture URL - val profileCaptor = argumentCaptor() - verify(accountRepository).updateAccount(profileCaptor.capture(), any(), any()) + val captor = argumentCaptor() + verify(accountRepository).updateAccount(captor.capture(), any(), any()) + // On ne peut pas vérifier l'URL directement parce que l'upload se fait avant l'updateAccount, + // mais on peut au moins vérifier que l'updateAccount a été appelé. + // Si besoin, on peut vérifier si l'URL du profilePicture correspond à celle renvoyée + // par uploadAccountImages + assertEquals("john.new@example.com", captor.firstValue.email) } @Test @@ -211,11 +231,13 @@ class ProfileConfigurationUserNoModeScreenTest { } } - // Enter invalid email + composeTestRule.waitForIdle() + + // Email invalide composeTestRule.onNodeWithTag("emailInput").performTextReplacement("invalid-email") composeTestRule.onNodeWithTag("SaveButton").assertIsNotEnabled() - // Enter invalid birth date + // Date invalide composeTestRule.onNodeWithTag("birthDateInput").performTextReplacement("invalid-date") composeTestRule.onNodeWithTag("SaveButton").assertIsNotEnabled() } diff --git a/app/src/main/java/com/arygm/quickfix/model/account/Account.kt b/app/src/main/java/com/arygm/quickfix/model/account/Account.kt index aa97b907..768d8c4a 100644 --- a/app/src/main/java/com/arygm/quickfix/model/account/Account.kt +++ b/app/src/main/java/com/arygm/quickfix/model/account/Account.kt @@ -10,6 +10,5 @@ data class Account( val birthDate: Timestamp, val isWorker: Boolean = false, val activeChats: List = emptyList(), - val profilePicture: String = - "https://example.com/default-profile-pic.jpg" // Default profile picture URL + val profilePicture: String = "" // Default profile picture URL ) diff --git a/app/src/main/java/com/arygm/quickfix/model/account/AccountRepository.kt b/app/src/main/java/com/arygm/quickfix/model/account/AccountRepository.kt index 06a98544..a9db5473 100644 --- a/app/src/main/java/com/arygm/quickfix/model/account/AccountRepository.kt +++ b/app/src/main/java/com/arygm/quickfix/model/account/AccountRepository.kt @@ -28,4 +28,10 @@ interface AccountRepository { onSuccess: (List) -> Unit, onFailure: (Exception) -> Unit ) + + fun fetchAccountProfileImageAsBitmap( + profilePictureUrl: String, + onSuccess: (Bitmap) -> Unit, + onFailure: (Exception) -> Unit + ) } diff --git a/app/src/main/java/com/arygm/quickfix/model/account/AccountRepositoryFirestore.kt b/app/src/main/java/com/arygm/quickfix/model/account/AccountRepositoryFirestore.kt index 24dad56e..8b8cc616 100644 --- a/app/src/main/java/com/arygm/quickfix/model/account/AccountRepositoryFirestore.kt +++ b/app/src/main/java/com/arygm/quickfix/model/account/AccountRepositoryFirestore.kt @@ -1,6 +1,7 @@ package com.arygm.quickfix.model.account import android.graphics.Bitmap +import android.graphics.BitmapFactory import android.util.Log import com.arygm.quickfix.utils.performFirestoreOperation import com.google.firebase.Firebase @@ -181,4 +182,57 @@ open class AccountRepositoryFirestore( .addOnFailureListener { exception -> onFailure(exception) } } } + + override fun fetchAccountProfileImageAsBitmap( + profilePictureUrl: String, + onSuccess: (Bitmap) -> Unit, + onFailure: (Exception) -> Unit + ) { + fetchProfileImageUrl( + accountId = profilePictureUrl, + onSuccess = { url -> + Log.e("AccountRepositoryFirestore", "url: $url") + if (url.isEmpty() || url == "https://example.com/default-profile-pic.jpg") { + val defaultProfileBitmap = + createSolidColorBitmap(width = 200, height = 200, color = 0xFF66001A.toInt()) + onSuccess(defaultProfileBitmap) + } else { + val imageRef = FirebaseStorage.getInstance().getReferenceFromUrl(url) + imageRef + .getBytes(Long.MAX_VALUE) + .addOnSuccessListener { bytes -> + val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) + onSuccess(bitmap) + } + .addOnFailureListener { exception -> onFailure(exception) } + } + }, + onFailure = onFailure, + documentId = "profilePicture" // Le champ Firestore où est stockée l’URL + ) + } + + private fun fetchProfileImageUrl( + accountId: String, + onSuccess: (String) -> Unit, + onFailure: (Exception) -> Unit, + documentId: String + ) { + val firestore = db + val collection = firestore.collection(collectionPath) + collection + .document(accountId) + .get() + .addOnSuccessListener { document -> + val imageUrl = document[documentId] as? String ?: "" + onSuccess(imageUrl) + } + .addOnFailureListener { onFailure(it) } + } + + fun createSolidColorBitmap(width: Int, height: Int, color: Int): Bitmap { + val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + bitmap.eraseColor(color) + return bitmap + } } diff --git a/app/src/main/java/com/arygm/quickfix/model/account/AccountViewModel.kt b/app/src/main/java/com/arygm/quickfix/model/account/AccountViewModel.kt index e22955e1..179c5c5c 100644 --- a/app/src/main/java/com/arygm/quickfix/model/account/AccountViewModel.kt +++ b/app/src/main/java/com/arygm/quickfix/model/account/AccountViewModel.kt @@ -117,4 +117,12 @@ open class AccountViewModel(private val repository: AccountRepository) : ViewMod repository.uploadAccountImages( accountId = accountId, images = images, onSuccess = onSuccess, onFailure = onFailure) } + + fun fetchAccountProfileImageAsBitmap( + accountId: String, + onSuccess: (Bitmap) -> Unit, + onFailure: (Exception) -> Unit + ) { + repository.fetchAccountProfileImageAsBitmap(accountId, onSuccess, onFailure) + } } diff --git a/app/src/main/java/com/arygm/quickfix/ui/dashboard/ChatWidget.kt b/app/src/main/java/com/arygm/quickfix/ui/dashboard/ChatWidget.kt index cad7e64d..d82b4d95 100644 --- a/app/src/main/java/com/arygm/quickfix/ui/dashboard/ChatWidget.kt +++ b/app/src/main/java/com/arygm/quickfix/ui/dashboard/ChatWidget.kt @@ -1,6 +1,7 @@ package com.arygm.quickfix.ui.dashboard import android.annotation.SuppressLint +import android.graphics.Bitmap import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -32,15 +33,14 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.arygm.quickfix.R import com.arygm.quickfix.model.account.AccountViewModel import com.arygm.quickfix.model.category.CategoryViewModel import com.arygm.quickfix.model.category.getCategoryIcon @@ -162,6 +162,19 @@ fun ChatItem( image = it?.let { it1 -> getCategoryIcon(it1) } } } + var otherUserId = chat.useruid + if (mode == AppMode.USER) { + otherUserId = chat.workeruid + } + + var otherProfileBitmap by remember { mutableStateOf(null) } + + LaunchedEffect(otherUserId) { + accountViewModel.fetchAccountProfileImageAsBitmap( + accountId = otherUserId, + onSuccess = { bitmap -> otherProfileBitmap = bitmap }, + onFailure = {}) + } Row( modifier = Modifier.fillMaxWidth() @@ -170,17 +183,20 @@ fun ChatItem( .testTag("MessageItem_${chat.chatId}"), // Added testTag verticalAlignment = Alignment.CenterVertically) { Column(modifier = Modifier.weight(0.15f)) { + val imageModifier = + Modifier.size(40.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)) + if (otherProfileBitmap != null) { + Image( + bitmap = otherProfileBitmap!!.asImageBitmap(), + contentDescription = "Profile Picture", + contentScale = ContentScale.Crop, + modifier = imageModifier) + } else { + Box(modifier = imageModifier) + } // Profile image placeholder - Image( - painter = - painterResource( - id = R.drawable.placeholder_worker), // Replace with an actual drawable - contentDescription = "Profile Picture", - contentScale = ContentScale.Crop, - modifier = - Modifier.size(40.dp) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f))) } // Text information @@ -236,7 +252,9 @@ fun ChatItem( (chat.chatStatus == ChatStatus.WAITING_FOR_RESPONSE || chat.chatStatus == ChatStatus.GETTING_SUGGESTIONS)) { "Await confirmation from ${workerProfile.displayName}" - } else chat.messages.last().content, // Removed leading comma for clarity + } else { + if (!chat.messages.isEmpty()) chat.messages.last().content else "" + }, // Removed leading comma for clarity modifier = Modifier.testTag( if (chat.messages.isEmpty()) "No messages" diff --git a/app/src/main/java/com/arygm/quickfix/ui/elements/QuickFixProfileElement.kt b/app/src/main/java/com/arygm/quickfix/ui/elements/QuickFixProfileElement.kt index 576e2d25..9042fc03 100644 --- a/app/src/main/java/com/arygm/quickfix/ui/elements/QuickFixProfileElement.kt +++ b/app/src/main/java/com/arygm/quickfix/ui/elements/QuickFixProfileElement.kt @@ -1,5 +1,6 @@ package com.arygm.quickfix.ui.elements +import android.graphics.Bitmap import android.util.Log import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image @@ -18,10 +19,12 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.ArrowForwardIos +import androidx.compose.material.icons.outlined.CameraAlt import androidx.compose.material.icons.outlined.WorkOutline import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults @@ -51,15 +54,18 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.painterResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.arygm.quickfix.R +import coil.compose.SubcomposeAsyncImage +import com.arygm.quickfix.model.account.AccountViewModel import com.arygm.quickfix.model.offline.small.PreferencesViewModel import com.arygm.quickfix.model.offline.small.PreferencesViewModelUserProfile import com.arygm.quickfix.model.offline.small.PreferencesViewModelWorkerProfile @@ -75,6 +81,7 @@ import com.arygm.quickfix.utils.clearPreferences import com.arygm.quickfix.utils.clearUserProfilePreferences import com.arygm.quickfix.utils.clearWorkerProfilePreferences import com.arygm.quickfix.utils.loadAppMode +import com.arygm.quickfix.utils.loadUserId import com.arygm.quickfix.utils.loadWallet import com.arygm.quickfix.utils.setAppMode import com.google.firebase.auth.ktx.auth @@ -94,6 +101,7 @@ fun QuickFixProfileScreenElement( initialState: Boolean, switchMode: AppMode, sections: List<@Composable (Modifier) -> Unit>, // Dynamic sections + accountViewModel: AccountViewModel // Added AccountViewModel for fetching profile picture ) { val firstName by preferencesViewModel.firstName.collectAsState(initial = "") val lastName by preferencesViewModel.lastName.collectAsState(initial = "") @@ -109,7 +117,22 @@ fun QuickFixProfileScreenElement( // Compute display name using the collected first and last names val displayName = capitalizeName(firstName, lastName) var isChecked by remember { mutableStateOf(initialState) } // State to track the switch state + val context = LocalContext.current + // State for profile image bitmap + var profileBitmap by remember { mutableStateOf(null) } + var userId by remember { mutableStateOf("") } + LaunchedEffect(Unit) { + // Load the user ID + userId = loadUserId(preferencesViewModel) + // Fetch the profile image from the backend + accountViewModel.fetchAccountProfileImageAsBitmap( + accountId = userId, + onSuccess = { bitmap -> profileBitmap = bitmap }, + onFailure = { + // Leave profileBitmap as null if fetching fails + }) + } BoxWithConstraints(modifier = Modifier.fillMaxSize()) { val screenWidth = maxWidth val screenHeight = maxHeight @@ -148,11 +171,36 @@ fun QuickFixProfileScreenElement( Modifier.padding(end = screenWidth * 0.05f, top = screenHeight * 0.02f) .size(screenWidth * 0.2f) .testTag("ProfilePicture")) { - Image( - painter = painterResource(id = R.drawable.placeholder_worker), - contentDescription = "Profile Picture", - modifier = Modifier.fillMaxSize().clip(RoundedCornerShape(50)), - contentScale = ContentScale.Crop) + // Display the fetched profile image or placeholder + if (profileBitmap != null) { + profileBitmap?.let { + Image( + bitmap = it.asImageBitmap(), + contentDescription = "Profile Image", + modifier = Modifier.fillMaxSize().clip(CircleShape), + contentScale = ContentScale.Crop) + } + ?: SubcomposeAsyncImage( + model = "https://example.com/default-profile-pic.jpg", + contentDescription = "Profile Image", + modifier = Modifier.fillMaxSize().clip(CircleShape), + contentScale = ContentScale.Crop, + error = { + Icon( + Icons.Outlined.CameraAlt, + contentDescription = "Placeholder", + tint = Color.Gray) + }, + ) + } else { + Box( + modifier = + Modifier.padding( + end = screenWidth * 0.05f, top = screenHeight * 0.02f) + .size(screenWidth * 0.2f) + .background(Color.Gray, CircleShape) + .testTag("ProfilePicture")) + } } }, colors = TopAppBarDefaults.topAppBarColors(containerColor = colorScheme.background)) diff --git a/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/AppContentNavGraph.kt b/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/AppContentNavGraph.kt index 65fa30fa..70f47896 100644 --- a/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/AppContentNavGraph.kt +++ b/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/AppContentNavGraph.kt @@ -118,7 +118,8 @@ fun AppContentNavGraph( workerViewModel, quickFixViewModel, chatViewModel, - categoryViewModel) + categoryViewModel, + userPreferencesViewModel) } } } diff --git a/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/UserModeNavGraph.kt b/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/UserModeNavGraph.kt index 4660d473..853be77f 100644 --- a/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/UserModeNavGraph.kt +++ b/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/UserModeNavGraph.kt @@ -174,6 +174,7 @@ fun UserModeNavHost( announcementViewModel, isUser) } + composable(UserRoute.SEARCH) { SearchNavHost( isUser, @@ -385,7 +386,8 @@ fun ProfileNavHost( preferencesViewModel, userPreferencesViewModel, appContentNavigationActions, - modeViewModel) + modeViewModel, + accountViewModel) } composable(UserScreen.ACCOUNT_CONFIGURATION) { AccountConfigurationScreen(profileNavigationActions, accountViewModel, preferencesViewModel) diff --git a/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/home/MessagesScreen.kt b/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/home/MessagesScreen.kt index 4dc5f543..9240f2b7 100644 --- a/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/home/MessagesScreen.kt +++ b/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/home/MessagesScreen.kt @@ -1,6 +1,8 @@ package com.arygm.quickfix.ui.uiMode.appContentUI.userModeUI.home +import android.graphics.Bitmap import android.util.Log +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn @@ -20,13 +22,18 @@ import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.arygm.quickfix.R import com.arygm.quickfix.model.account.AccountViewModel import com.arygm.quickfix.model.messaging.ChatStatus import com.arygm.quickfix.model.messaging.ChatViewModel @@ -58,12 +65,7 @@ fun MessageScreen( workerViewModel: ProfileViewModel, accountViewModel: AccountViewModel ) { - var userId by remember { mutableStateOf("") } - var mode by remember { mutableStateOf("") } - LaunchedEffect(Unit) { - userId = loadUserId(preferencesViewModel) - mode = loadAppMode(preferencesViewModel) - } + // Collecting the selected chat from the ViewModel as state val activeChat = chatViewModel.selectedChat.collectAsState().value @@ -74,9 +76,17 @@ fun MessageScreen( } // Assigning the non-null value of activeChat val chat = activeChat + var userId by remember { mutableStateOf("") } + var mode by remember { mutableStateOf("") } + var otherUserId by remember { mutableStateOf("") } var chatStatus by remember { mutableStateOf(chat.chatStatus) } var messages by remember { mutableStateOf(chat.messages) } // État local des messages + LaunchedEffect(Unit) { + userId = loadUserId(preferencesViewModel) + mode = loadAppMode(preferencesViewModel) + otherUserId = if (userId == chat.workeruid) chat.useruid else chat.workeruid + } var quickFix by remember { mutableStateOf(null) } // Finding the associated QuickFix for the selected chat quickFixViewModel.fetchQuickFix(chat.quickFixUid, onResult = { quickFix = it }) @@ -87,11 +97,20 @@ fun MessageScreen( } val chatId = chat.chatId + var otherProfileBitmap by remember { mutableStateOf(null) } + // Mutable states to manage input text and sliding window visibility var messageText by remember { mutableStateOf("") } var isSlidingWindowVisible by remember { mutableStateOf(false) } val coroutineScope = rememberCoroutineScope() - + LaunchedEffect(otherUserId) { + accountViewModel.fetchAccountProfileImageAsBitmap( + accountId = otherUserId, + onSuccess = { bitmap -> otherProfileBitmap = bitmap }, + onFailure = { + // En cas d'erreur, on laisse otherProfileBitmap = null (placeholder) + }) + } // Retrieve chat status and prepare suggestions based on user role (User or Worker) val suggestions = if (mode == AppMode.USER.name) { @@ -191,8 +210,9 @@ fun MessageScreen( } // Header section with a back button Header( - navigationActions, + navigationActions = navigationActions, modifier = Modifier.testTag("backButton"), + otherProfileBitmap = otherProfileBitmap, displayName = displayNameHeader) }, bottomBar = { @@ -480,6 +500,8 @@ fun MessageScreen( fun Header( navigationActions: NavigationActions, modifier: Modifier = Modifier, + otherProfileBitmap: Bitmap?, + widthRatio: Float = 1.0f, displayName: String ) { Box(modifier = Modifier.fillMaxWidth().background(colorScheme.surface).padding(vertical = 8.dp)) { @@ -497,11 +519,25 @@ fun Header( Column( modifier = Modifier.align(Alignment.Center), horizontalAlignment = Alignment.CenterHorizontally) { - Box( - modifier = - Modifier.size(40.dp) - .background(Color.Black, CircleShape) - .testTag("profilePicture")) + val imageModifier = + Modifier.size(40.dp * widthRatio).clip(CircleShape).background(Color.Gray) + if (otherProfileBitmap != null) { + Log.e("hhaha", "kkhaawi ${otherProfileBitmap}") + Image( + bitmap = otherProfileBitmap.asImageBitmap(), + contentDescription = "Profile Picture", + contentScale = ContentScale.Crop, + modifier = imageModifier) + } else { + Log.e("hhaha", "kkhaawi") + + Text("allo") + Image( + painter = painterResource(id = R.drawable.placeholder_worker), + contentDescription = "Profile Picture", + contentScale = ContentScale.Crop, + modifier = imageModifier) + } Text( text = displayName, fontWeight = FontWeight.Bold, diff --git a/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/profile/AccountConfiguration.kt b/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/profile/AccountConfiguration.kt index 8ce96eba..6c9fb572 100644 --- a/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/profile/AccountConfiguration.kt +++ b/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/profile/AccountConfiguration.kt @@ -50,6 +50,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil.compose.SubcomposeAsyncImage import com.arygm.quickfix.model.account.Account @@ -73,7 +74,7 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.firebase.Timestamp import java.util.GregorianCalendar -@SuppressLint("StateFlowValueCalledInComposition") +@SuppressLint("StateFlowValueCalledInComposition", "UnusedBoxWithConstraintsScope") @OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class) @Composable fun AccountConfigurationScreen( @@ -92,6 +93,7 @@ fun AccountConfigurationScreen( mutableStateOf("https://example.com/default-profile-pic.jpg") } var imageChanged by remember { mutableStateOf(false) } + var userAccount by remember { mutableStateOf(null) } // State for input fields var inputFirstName by remember { mutableStateOf("Loading...") } @@ -107,6 +109,10 @@ fun AccountConfigurationScreen( var isUploading by remember { mutableStateOf(false) } + val context = LocalContext.current + var emailError by remember { mutableStateOf(false) } + var birthDateError by remember { mutableStateOf(false) } + LaunchedEffect(Unit) { // Load saved data uid = loadUserId(preferencesViewModel) @@ -116,6 +122,7 @@ fun AccountConfigurationScreen( savedBirthDate = loadBirthDate(preferencesViewModel) savedProfilePicture = loadProfilePicture(preferencesViewModel) isWorker = loadIsWorker(preferencesViewModel) + accountViewModel.fetchUserAccount(uid) { userAccount = it } // Initialize input fields with saved data inputFirstName = savedFirstName @@ -123,17 +130,21 @@ fun AccountConfigurationScreen( inputEmail = savedEmail inputBirthDate = savedBirthDate inputProfilePicture = savedProfilePicture - } - var emailError by remember { mutableStateOf(false) } - var birthDateError by remember { mutableStateOf(false) } + // Fetch le bitmap depuis le backend en utilisant accountViewModel + accountViewModel.fetchAccountProfileImageAsBitmap( + accountId = uid, + onSuccess = { bitmap -> profileBitmap = bitmap }, + onFailure = { + // En cas d'erreur, laissez profileBitmap à null - le placeholder sera affiché + }) + } - val context = LocalContext.current + val screenWidth = with(LocalContext.current.resources.displayMetrics) { widthPixels.dp / density } + val screenHeight = + with(LocalContext.current.resources.displayMetrics) { heightPixels.dp / density } BoxWithConstraints(modifier = Modifier.fillMaxSize()) { - val screenWidth = maxWidth - val screenHeight = maxHeight - Column( modifier = Modifier.fillMaxSize() @@ -171,14 +182,13 @@ fun AccountConfigurationScreen( bitmap = it.asImageBitmap(), contentDescription = "Profile Image", modifier = Modifier.fillMaxSize().clip(CircleShape), - contentScale = ContentScale.Crop // Fit image inside the circle - ) + contentScale = ContentScale.Crop) } ?: SubcomposeAsyncImage( model = savedProfilePicture, contentDescription = "Profile Image", modifier = Modifier.fillMaxSize().clip(CircleShape), - contentScale = ContentScale.Crop, // Fit image inside the circle + contentScale = ContentScale.Crop, error = { Icon( Icons.Outlined.CameraAlt, @@ -313,9 +323,9 @@ fun AccountConfigurationScreen( images = listOf(profileBitmap!!), onSuccess = { imageUrls -> val newProfilePicture = imageUrls.first() - val updatedAccount = - Account( - uid = uid, + val newAccount = + userAccount?.copy( + profilePicture = newProfilePicture, firstName = inputFirstName, lastName = inputLastName, email = inputEmail, @@ -326,26 +336,29 @@ fun AccountConfigurationScreen( inputBirthDate.split("/")[1].toInt() - 1, inputBirthDate.split("/")[0].toInt()) .time), - isWorker = isWorker, - profilePicture = newProfilePicture) + isWorker = isWorker) - accountViewModel.updateAccount( - updatedAccount, - onSuccess = { - setAccountPreferences(preferencesViewModel, updatedAccount) - isUploading = false - Toast.makeText(context, "Profile updated!", Toast.LENGTH_SHORT).show() - savedFirstName = inputFirstName - savedLastName = inputLastName - savedEmail = inputEmail - savedBirthDate = inputBirthDate - savedProfilePicture = newProfilePicture - imageChanged = false - }, - onFailure = { - isUploading = false - Toast.makeText(context, "Update failed!", Toast.LENGTH_SHORT).show() - }) + if (newAccount != null) { + accountViewModel.updateAccount( + newAccount, + onSuccess = { + setAccountPreferences(preferencesViewModel, newAccount) + isUploading = false + Toast.makeText(context, "Profile updated!", Toast.LENGTH_SHORT) + .show() + savedFirstName = inputFirstName + savedLastName = inputLastName + savedEmail = inputEmail + savedBirthDate = inputBirthDate + savedProfilePicture = newProfilePicture + imageChanged = false + navigationActions.goBack() + }, + onFailure = { + isUploading = false + Toast.makeText(context, "Update failed!", Toast.LENGTH_SHORT).show() + }) + } }, onFailure = { isUploading = false @@ -353,9 +366,8 @@ fun AccountConfigurationScreen( }) } else { // Update without changing the image - val updatedAccount = - Account( - uid = uid, + val newAccount = + userAccount?.copy( firstName = inputFirstName, lastName = inputLastName, email = inputEmail, @@ -366,24 +378,25 @@ fun AccountConfigurationScreen( inputBirthDate.split("/")[1].toInt() - 1, inputBirthDate.split("/")[0].toInt()) .time), - isWorker = isWorker, - profilePicture = savedProfilePicture) - accountViewModel.updateAccount( - updatedAccount, - onSuccess = { - setAccountPreferences(preferencesViewModel, updatedAccount) - Toast.makeText(context, "Profile updated!", Toast.LENGTH_SHORT).show() - savedFirstName = inputFirstName - savedLastName = inputLastName - savedEmail = inputEmail - savedBirthDate = inputBirthDate - imageChanged = false - }, - onFailure = { - Toast.makeText(context, "Update failed!", Toast.LENGTH_SHORT).show() - }) + isWorker = isWorker) + if (newAccount != null) { + accountViewModel.updateAccount( + newAccount, + onSuccess = { + setAccountPreferences(preferencesViewModel, newAccount) + Toast.makeText(context, "Profile updated!", Toast.LENGTH_SHORT).show() + savedFirstName = inputFirstName + savedLastName = inputLastName + savedEmail = inputEmail + savedBirthDate = inputBirthDate + imageChanged = false + navigationActions.goBack() + }, + onFailure = { + Toast.makeText(context, "Update failed!", Toast.LENGTH_SHORT).show() + }) + } } - navigationActions.goBack() }, enabled = isModified && !emailError && !birthDateError, colors = diff --git a/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/profile/UserProfileScreen.kt b/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/profile/UserProfileScreen.kt index 87c2c6e5..23e5c85f 100644 --- a/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/profile/UserProfileScreen.kt +++ b/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/profile/UserProfileScreen.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.unit.dp +import com.arygm.quickfix.model.account.AccountViewModel import com.arygm.quickfix.model.offline.small.PreferencesViewModel import com.arygm.quickfix.model.offline.small.PreferencesViewModelUserProfile import com.arygm.quickfix.model.switchModes.AppMode @@ -30,7 +31,8 @@ fun UserProfileScreen( preferencesViewModel: PreferencesViewModel, userPreferencesViewModel: PreferencesViewModelUserProfile, appContentNavigationActions: NavigationActions, - modeViewModel: ModeViewModel + modeViewModel: ModeViewModel, + accountViewModel: AccountViewModel ) { val isWorker by preferencesViewModel.isWorkerFlow.collectAsState(initial = false) @@ -76,6 +78,7 @@ fun UserProfileScreen( action = { navigationActions.navigateTo(UserScreen.TO_WORKER) }) // Pass sections as lambdas to `QuickFixProfileScreenElement` + BoxWithConstraints { val screenWidth = maxWidth val screenHeight = maxHeight @@ -106,6 +109,7 @@ fun UserProfileScreen( cardCornerRadius = 16.dp, showConditionalItem = !isWorker, conditionalItem = conditionalWorkerSection) - })) + }), + accountViewModel = accountViewModel) } } diff --git a/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/workerMode/WorkerModeNavGraph.kt b/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/workerMode/WorkerModeNavGraph.kt index 74173d20..084203f3 100644 --- a/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/workerMode/WorkerModeNavGraph.kt +++ b/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/workerMode/WorkerModeNavGraph.kt @@ -32,6 +32,7 @@ import com.arygm.quickfix.model.locations.LocationViewModel import com.arygm.quickfix.model.messaging.ChatViewModel import com.arygm.quickfix.model.offline.small.PreferencesRepositoryDataStore import com.arygm.quickfix.model.offline.small.PreferencesViewModel +import com.arygm.quickfix.model.offline.small.PreferencesViewModelUserProfile import com.arygm.quickfix.model.offline.small.PreferencesViewModelWorkerProfile import com.arygm.quickfix.model.profile.ProfileViewModel import com.arygm.quickfix.model.profile.WorkerProfileRepositoryFirestore @@ -49,7 +50,7 @@ import com.arygm.quickfix.ui.uiMode.appContentUI.userModeUI.profile.AccountConfi import com.arygm.quickfix.ui.uiMode.appContentUI.userModeUI.quickfix.QuickFixOnBoarding import com.arygm.quickfix.ui.uiMode.appContentUI.userModeUI.search.AnnouncementDetailScreen import com.arygm.quickfix.ui.uiMode.appContentUI.workerMode.announcements.AnnouncementsScreen -import com.arygm.quickfix.ui.uiMode.appContentUI.workerMode.messages.MessagesScreen +import com.arygm.quickfix.ui.uiMode.appContentUI.workerMode.messages.ChatsScreen import com.arygm.quickfix.ui.uiMode.appContentUI.workerMode.profile.WorkerProfileScreen import com.arygm.quickfix.ui.uiMode.appContentUI.workerMode.quickfix.QuickFixBilling import com.arygm.quickfix.ui.uiMode.workerMode.navigation.WORKER_TOP_LEVEL_DESTINATIONS @@ -75,7 +76,8 @@ fun WorkerModeNavGraph( workerViewModel: ProfileViewModel, quickFixViewModel: QuickFixViewModel, chatViewModel: ChatViewModel, - categoryViewModel: CategoryViewModel + categoryViewModel: CategoryViewModel, + userPreferencesViewModel: PreferencesViewModelUserProfile ) { val context = LocalContext.current val workerNavController = rememberNavController() @@ -118,6 +120,7 @@ fun WorkerModeNavGraph( showBottomBar = false } } + Box(modifier = Modifier.fillMaxSize()) { Scaffold( topBar = { QuickFixOfflineBar(isVisible = isOffline) }, @@ -146,8 +149,15 @@ fun WorkerModeNavGraph( announcementViewModel, categoryViewModel) } - composable(WorkerRoute.MESSAGES) { - MessagesNavHost(onScreenChange = { currentScreen = it }) + composable(WorkerRoute.CHATS) { + MessagesNavHost( + onScreenChange = { currentScreen = it }, + preferencesViewModel, + workerNavigationActions, + chatViewModel, + quickFixViewModel, + accountViewModel, + workerViewModel) } composable(WorkerRoute.ANNOUNCEMENT) { AnnouncementsNavHost( @@ -194,7 +204,15 @@ fun WorkerModeNavGraph( } @Composable -fun MessagesNavHost(onScreenChange: (String) -> Unit) { +fun MessagesNavHost( + onScreenChange: (String) -> Unit, + pre: PreferencesViewModel, + workerNavigationActions: NavigationActions, + chatViewModel: ChatViewModel, + quickFixViewModel: QuickFixViewModel, + accountViewModel: AccountViewModel, + workerViewModel: ProfileViewModel +) { val dashboardNavController = rememberNavController() val navigationActions = remember { NavigationActions(dashboardNavController) } @@ -202,8 +220,19 @@ fun MessagesNavHost(onScreenChange: (String) -> Unit) { LaunchedEffect(navigationActions.currentScreen) { onScreenChange(navigationActions.currentScreen) } - NavHost(navController = dashboardNavController, startDestination = WorkerScreen.MESSAGES) { - composable(WorkerScreen.MESSAGES) { MessagesScreen() } + NavHost(navController = dashboardNavController, startDestination = WorkerScreen.CHATS) { + composable(WorkerScreen.CHATS) { + ChatsScreen(navigationActions, accountViewModel, chatViewModel, pre) + } + composable(WorkerScreen.MESSAGES) { + MessageScreen( + chatViewModel, + navigationActions, + quickFixViewModel, + pre, + workerViewModel, + accountViewModel) + } } } @@ -343,7 +372,8 @@ fun ProfileNavHost( preferencesViewModel, workerPreferenceViewModel, appContentNavigationActions, - modeViewModel) + modeViewModel, + accountViewModel) } composable(WorkerScreen.ACCOUNT_CONFIGURATION) { AccountConfigurationScreen(profileNavigationActions, accountViewModel, preferencesViewModel) diff --git a/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/workerMode/messages/ChatsScreen.kt b/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/workerMode/messages/ChatsScreen.kt new file mode 100644 index 00000000..8d325932 --- /dev/null +++ b/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/workerMode/messages/ChatsScreen.kt @@ -0,0 +1,248 @@ +package com.arygm.quickfix.ui.uiMode.appContentUI.workerMode.messages + +import android.graphics.Bitmap +import android.util.Log +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Search +import androidx.compose.material3.* +import androidx.compose.material3.MaterialTheme.colorScheme +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.arygm.quickfix.model.account.AccountViewModel +import com.arygm.quickfix.model.messaging.Chat +import com.arygm.quickfix.model.messaging.ChatViewModel +import com.arygm.quickfix.model.offline.small.PreferencesViewModel +import com.arygm.quickfix.ui.elements.QuickFixTextFieldCustom +import com.arygm.quickfix.ui.navigation.NavigationActions +import com.arygm.quickfix.ui.theme.poppinsTypography +import com.arygm.quickfix.ui.uiMode.appContentUI.userModeUI.navigation.UserScreen +import com.arygm.quickfix.ui.uiMode.workerMode.navigation.WorkerScreen +import com.arygm.quickfix.utils.loadAppMode +import com.arygm.quickfix.utils.loadUserId +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Locale +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ChatsScreen( + navigationActions: NavigationActions, + accountViewModel: AccountViewModel, + chatViewModel: ChatViewModel, + preferencesViewModel: PreferencesViewModel +) { + var mode by remember { mutableStateOf("") } + var uid by remember { mutableStateOf("") } + var chats by remember { mutableStateOf(emptyList()) } + var searchQuery by remember { mutableStateOf("") } + val coroutineScope = rememberCoroutineScope() + + // Map pour stocker useruid -> firstName + val userFirstNameMap = remember { mutableStateMapOf() } + + LaunchedEffect(Unit) { + mode = loadAppMode(preferencesViewModel) + uid = loadUserId(preferencesViewModel) + + accountViewModel.fetchUserAccount(uid) { account -> + account?.activeChats?.forEach { chatUid -> + coroutineScope.launch { + chatViewModel.getChatByChatUid( + chatUid, + onSuccess = { chat -> + if (chat != null) { + chats = chats + chat + // Récupérer le firstName de useruid pour chaque chat + accountViewModel.fetchUserAccount(chat.useruid) { userAccount -> + userFirstNameMap[chat.useruid] = userAccount?.firstName ?: "Unknown" + } + } + }, + onFailure = { e -> Log.e("ChatScreen", "Failed: ${e.message}") }) + } + } + } + } + + BoxWithConstraints { + val widthRatio = maxWidth.value / 411f + val heightRatio = maxHeight.value / 860f + + Scaffold( + containerColor = colorScheme.background, + topBar = { + Column( + modifier = + Modifier.fillMaxWidth() + .padding(horizontal = 8.dp * widthRatio, vertical = 8.dp * heightRatio)) { + Text( + text = "Messages", + style = poppinsTypography.headlineLarge.copy(fontWeight = FontWeight.Bold), + color = colorScheme.onBackground, + modifier = Modifier.padding(bottom = 8.dp * heightRatio)) + + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally) { + QuickFixTextFieldCustom( + value = searchQuery, + onValueChange = { searchQuery = it }, + showLeadingIcon = { true }, + leadingIcon = Icons.Outlined.Search, + placeHolderText = "Search", + modifier = + Modifier.fillMaxWidth() + .height(40.dp * heightRatio) + .testTag("customSearchField"), + widthField = 380.dp * widthRatio) + } + } + }, + content = { padding -> + LazyColumn(modifier = Modifier.fillMaxSize().padding(padding)) { + // Filtrer les chats en comparant avec userFirstName + val filteredChats = + chats.filter { chat -> + val userFirstName = userFirstNameMap[chat.useruid] ?: "" + userFirstName.contains(searchQuery, ignoreCase = true) && chat.workeruid == uid + } + + itemsIndexed(filteredChats) { index, chat -> + // Chat Item + + ChatItem( + chat = chat, + accountViewModel = accountViewModel, + userFirstName = userFirstNameMap[chat.useruid] ?: "Loading...", + onClick = { + chatViewModel.selectChat(chat) + if (mode == "USER") navigationActions.navigateTo(UserScreen.MESSAGES) + else navigationActions.navigateTo(WorkerScreen.MESSAGES) + }, + widthRatio = widthRatio, + heightRatio = heightRatio) + + // Ajouter un Divider sauf pour le dernier élément + if (index < filteredChats.size - 1) { + Column(modifier = Modifier.padding(start = 32.dp * widthRatio)) { + Divider( + color = colorScheme.onSurface.copy(alpha = 0.2f), // Couleur de la ligne + thickness = 1.dp, // Épaisseur de la ligne + modifier = + Modifier.padding( + vertical = 4.dp * heightRatio) // Espacement autour du Divider + .testTag("Divider") // Ajout du testTag ici + ) + } + } + } + } + }) + } +} + +@Composable +fun ChatItem( + chat: Chat, + userFirstName: String, + accountViewModel: AccountViewModel, + onClick: () -> Unit, + widthRatio: Float, + heightRatio: Float +) { + val otherUserId = chat.useruid + + var otherProfileBitmap by remember { mutableStateOf(null) } + + LaunchedEffect(otherUserId) { + accountViewModel.fetchAccountProfileImageAsBitmap( + accountId = otherUserId, + onSuccess = { bitmap -> otherProfileBitmap = bitmap }, + onFailure = { + // Laisser null si échec + }) + } + Row( + modifier = + Modifier.fillMaxWidth() + .testTag("ChatItem") + .clickable { onClick() } + .padding(vertical = 8.dp * heightRatio, horizontal = 12.dp * widthRatio), + verticalAlignment = Alignment.CenterVertically) { + // Profile Picture Placeholder + val imageModifier = + Modifier.size(48.dp * widthRatio).clip(CircleShape).background(Color.Gray) + if (otherProfileBitmap != null) { + Image( + bitmap = otherProfileBitmap!!.asImageBitmap(), + contentDescription = "Profile Picture", + contentScale = ContentScale.Crop, + modifier = imageModifier) + } else { + Box(modifier = imageModifier) + } + + Spacer(modifier = Modifier.width(8.dp * widthRatio)) + + // Chat Info + Column(modifier = Modifier.weight(1f)) { + Text( + text = userFirstName, + style = poppinsTypography.bodyMedium.copy(fontWeight = FontWeight.Bold), + color = colorScheme.onBackground) + Text( + text = chat.messages.lastOrNull()?.content ?: "No messages yet", + style = poppinsTypography.bodySmall, + color = colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis) + } + + Spacer(modifier = Modifier.width(8.dp * widthRatio)) + + // Timestamp + val formattedDate = formatMessageTimestamp(chat.messages.lastOrNull()?.timestamp) + Text( + text = formattedDate, + style = MaterialTheme.typography.labelSmall, + color = colorScheme.onSurfaceVariant) + } +} + +// Helper function to format timestamp +fun formatMessageTimestamp(timestamp: com.google.firebase.Timestamp?): String { + if (timestamp == null) return "--:--" + + val now = Calendar.getInstance() + val messageTime = Calendar.getInstance().apply { time = timestamp.toDate() } + + return if (now.get(Calendar.YEAR) == messageTime.get(Calendar.YEAR) && + now.get(Calendar.DAY_OF_YEAR) == messageTime.get(Calendar.DAY_OF_YEAR)) { + // If the message is from today, show hours and minutes + SimpleDateFormat("HH:mm", Locale.getDefault()).format(messageTime.time) + } else { + // If the message is from a different day, show day and month + SimpleDateFormat("dd MMM", Locale.getDefault()).format(messageTime.time) + } +} diff --git a/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/workerMode/messages/MessagesScreen.kt b/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/workerMode/messages/MessagesScreen.kt deleted file mode 100644 index 0ca5dc80..00000000 --- a/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/workerMode/messages/MessagesScreen.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.arygm.quickfix.ui.uiMode.appContentUI.workerMode.messages - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import com.arygm.quickfix.ui.theme.poppinsTypography - -@Composable -fun MessagesScreen() { - Column( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally) { - Text("Messages Screen", style = poppinsTypography.headlineLarge) - } -} diff --git a/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/workerMode/navigation/WorkerNavigation.kt b/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/workerMode/navigation/WorkerNavigation.kt index 65aca138..6e253fcf 100644 --- a/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/workerMode/navigation/WorkerNavigation.kt +++ b/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/workerMode/navigation/WorkerNavigation.kt @@ -12,6 +12,7 @@ object WorkerRoute { const val HOME = "Home" const val ANNOUNCEMENT = "Announcement" const val MESSAGES = "Messages" + const val CHATS = "Chats" const val PROFILE = "Profile" } @@ -19,6 +20,7 @@ object WorkerScreen { const val HOME = "Home Screen" const val ANNOUNCEMENT = "Announcement Screen" const val MESSAGES = "Messages Screen" + const val CHATS = "Chats Screen" const val PROFILE = "Profile Screen" const val ACCOUNT_CONFIGURATION = "Account configuration Screen" const val QUIKFIX_ONBOARDING = "QuickFix Onboarding Screen" @@ -33,9 +35,9 @@ object WorkerTopLevelDestinations { val ANNOUNCEMENT = TopLevelDestination( route = WorkerRoute.ANNOUNCEMENT, icon = Icons.Outlined.Campaign, textId = "Announcement") - val MESSAGES = + val CHATS = TopLevelDestination( - route = WorkerRoute.MESSAGES, icon = Icons.Filled.MailOutline, textId = "Messages") + route = WorkerRoute.CHATS, icon = Icons.Filled.MailOutline, textId = "Messages") val PROFILE = TopLevelDestination( route = WorkerRoute.PROFILE, icon = Icons.Filled.PersonOutline, textId = "Profile") @@ -45,14 +47,14 @@ val WORKER_TOP_LEVEL_DESTINATIONS = listOf( WorkerTopLevelDestinations.HOME, WorkerTopLevelDestinations.ANNOUNCEMENT, - WorkerTopLevelDestinations.MESSAGES, + WorkerTopLevelDestinations.CHATS, WorkerTopLevelDestinations.PROFILE) val getBottomBarIdWorker: (String) -> Int = { route -> when (route) { WorkerRoute.HOME -> 1 WorkerRoute.ANNOUNCEMENT -> 2 - WorkerRoute.MESSAGES -> 3 + WorkerRoute.CHATS -> 3 WorkerRoute.PROFILE -> 4 else -> -1 // Should not happen } diff --git a/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/workerMode/profile/WorkerProfileScreen.kt b/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/workerMode/profile/WorkerProfileScreen.kt index 204fd321..941b169c 100644 --- a/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/workerMode/profile/WorkerProfileScreen.kt +++ b/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/workerMode/profile/WorkerProfileScreen.kt @@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.unit.dp +import com.arygm.quickfix.model.account.AccountViewModel import com.arygm.quickfix.model.offline.small.PreferencesViewModel import com.arygm.quickfix.model.offline.small.PreferencesViewModelWorkerProfile import com.arygm.quickfix.model.switchModes.AppMode @@ -29,7 +30,8 @@ fun WorkerProfileScreen( preferencesViewModel: PreferencesViewModel, workerPreferencesViewModel: PreferencesViewModelWorkerProfile, appContentNavigationActions: NavigationActions, - modeViewModel: ModeViewModel + modeViewModel: ModeViewModel, + accountViewModel: AccountViewModel ) { val isWorker by preferencesViewModel.isWorkerFlow.collectAsState(initial = false) @@ -96,6 +98,7 @@ fun WorkerProfileScreen( screenWidth = screenWidth, cardCornerRadius = 16.dp, ) - })) + }), + accountViewModel = accountViewModel) } } diff --git a/app/src/test/java/com/arygm/quickfix/model/account/AccountRepositoryFirestoreTest.kt b/app/src/test/java/com/arygm/quickfix/model/account/AccountRepositoryFirestoreTest.kt index fa60540f..c107d96a 100644 --- a/app/src/test/java/com/arygm/quickfix/model/account/AccountRepositoryFirestoreTest.kt +++ b/app/src/test/java/com/arygm/quickfix/model/account/AccountRepositoryFirestoreTest.kt @@ -780,4 +780,63 @@ class AccountRepositoryFirestoreTest { assertFalse(callbackCalled) } + + @Test + fun fetchAccountProfileImageAsBitmap_emptyUrl_returnsDefaultBitmap() { + val accountId = "someAccountId" + val documentId = "profilePicture" + + // Mock Firestore document get + val tcs = TaskCompletionSource() + `when`(mockCollectionReference.document(accountId)).thenReturn(mockDocumentReference) + `when`(mockDocumentReference.get()).thenReturn(tcs.task) + `when`(mockDocumentSnapshot.exists()).thenReturn(true) + `when`(mockDocumentSnapshot.get(documentId)).thenReturn("") + tcs.setResult(mockDocumentSnapshot) + + var onSuccessCalled = false + var returnedBitmap: Bitmap? = null + + accountRepositoryFirestore.fetchAccountProfileImageAsBitmap( + profilePictureUrl = accountId, + onSuccess = { + onSuccessCalled = true + returnedBitmap = it + }, + onFailure = { fail("Should not fail with empty URL") }) + + shadowOf(Looper.getMainLooper()).idle() + + assertTrue(onSuccessCalled) + assertNotNull(returnedBitmap) + // On ne vérifie pas exactement le bitmap, mais on s'assure qu'il n'est pas null + } + + @Test + fun fetchAccountProfileImageAsBitmap_firestoreFails_callsOnFailure() { + val accountId = "someAccountId" + val documentId = "profilePicture" + val firestoreException = Exception("Firestore error") + + val tcsDoc = TaskCompletionSource() + `when`(mockCollectionReference.document(accountId)).thenReturn(mockDocumentReference) + `when`(mockDocumentReference.get()).thenReturn(tcsDoc.task) + + var onFailureCalled = false + + accountRepositoryFirestore.fetchAccountProfileImageAsBitmap( + profilePictureUrl = accountId, + onSuccess = { fail("Should not succeed") }, + onFailure = { + onFailureCalled = true + assertEquals(firestoreException.message, it.message) + }) + + // Simuler une erreur Firestore + tcsDoc.setException(firestoreException) + + shadowOf(Looper.getMainLooper()).idle() + + assertTrue(onFailureCalled) + } } diff --git a/app/src/test/java/com/arygm/quickfix/model/messaging/ChatViewModelTest.kt b/app/src/test/java/com/arygm/quickfix/model/messaging/ChatViewModelTest.kt index 08c5cf36..6ddf4505 100644 --- a/app/src/test/java/com/arygm/quickfix/model/messaging/ChatViewModelTest.kt +++ b/app/src/test/java/com/arygm/quickfix/model/messaging/ChatViewModelTest.kt @@ -162,6 +162,7 @@ class ChatViewModelTest { @Test fun deleteChat_whenSuccess_updatesChats() = runTest { + println("9bl") doAnswer { invocation -> val onSuccess = invocation.getArgument<() -> Unit>(1) onSuccess()