diff --git a/app/src/androidTest/java/com/arygm/quickfix/ui/search/ProfileResultsTest.kt b/app/src/androidTest/java/com/arygm/quickfix/ui/search/ProfileResultsTest.kt index 3e86d4ec..78067b2b 100644 --- a/app/src/androidTest/java/com/arygm/quickfix/ui/search/ProfileResultsTest.kt +++ b/app/src/androidTest/java/com/arygm/quickfix/ui/search/ProfileResultsTest.kt @@ -1,106 +1,45 @@ package com.arygm.quickfix.ui.search import android.graphics.Bitmap -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.unit.dp import com.arygm.quickfix.model.account.Account -import com.arygm.quickfix.model.account.AccountRepositoryFirestore import com.arygm.quickfix.model.account.AccountViewModel -import com.arygm.quickfix.model.category.CategoryRepositoryFirestore -import com.arygm.quickfix.model.category.CategoryViewModel -import com.arygm.quickfix.model.profile.ProfileRepository -import com.arygm.quickfix.model.profile.ProfileViewModel +import com.arygm.quickfix.model.locations.Location import com.arygm.quickfix.model.profile.WorkerProfile -import com.arygm.quickfix.model.profile.WorkerProfileRepositoryFirestore import com.arygm.quickfix.model.search.SearchViewModel -import com.arygm.quickfix.ui.navigation.NavigationActions import com.arygm.quickfix.ui.uiMode.appContentUI.userModeUI.search.ProfileResults import com.google.firebase.Timestamp import io.mockk.every -import io.mockk.invoke import io.mockk.mockk import org.junit.Before import org.junit.Rule import org.junit.Test -import org.mockito.Mockito.mock class ProfileResultsTest { - private lateinit var navigationActions: NavigationActions - private lateinit var workerProfileRepo: WorkerProfileRepositoryFirestore - private lateinit var accountRepositoryFirestore: AccountRepositoryFirestore - private lateinit var categoryRepo: CategoryRepositoryFirestore - private lateinit var searchViewModel: SearchViewModel - private lateinit var accountViewModel: AccountViewModel - private lateinit var categoryViewModel: CategoryViewModel - private lateinit var navigationActionsRoot: NavigationActions - private lateinit var workerRepository: ProfileRepository - private lateinit var workerViewModel: ProfileViewModel - @get:Rule val composeTestRule = createComposeRule() + private lateinit var accountViewModel: AccountViewModel + private lateinit var searchViewModel: SearchViewModel + @Before fun setup() { - navigationActions = mock(NavigationActions::class.java) - navigationActionsRoot = mock(NavigationActions::class.java) - workerProfileRepo = mockk(relaxed = true) - categoryRepo = mockk(relaxed = true) - accountRepositoryFirestore = mock(AccountRepositoryFirestore::class.java) - searchViewModel = SearchViewModel(workerProfileRepo) - categoryViewModel = CategoryViewModel(categoryRepo) accountViewModel = mockk(relaxed = true) + searchViewModel = mockk(relaxed = true) - workerViewModel = mockk(relaxed = true) - - // Mock fetchProfileImageAsBitmap - every { workerViewModel.fetchProfileImageAsBitmap(any(), any(), any()) } answers - { - val onSuccess = arg<(Bitmap) -> Unit>(1) - // Provide a dummy bitmap here (e.g. a solid color bitmap or decode from resources) - val dummyBitmap = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888) - onSuccess(dummyBitmap) // Simulate success callback - } - - // Mock fetchBannerImageAsBitmap - every { workerViewModel.fetchBannerImageAsBitmap(any(), any(), any()) } answers - { - val onSuccess = arg<(Bitmap) -> Unit>(1) - // Provide another dummy bitmap - val dummyBitmap = Bitmap.createBitmap(20, 20, Bitmap.Config.ARGB_8888) - onSuccess(dummyBitmap) // Simulate success callback - } - } + // Mock calculateDistance to return a fixed distance + every { searchViewModel.calculateDistance(any(), any(), any(), any()) } returns 10.0 - @Test - fun profileContent_displaysWorkerProfiles() { - // Set up test data - val testProfiles = - listOf( - WorkerProfile( - uid = "worker0", - fieldOfWork = "Plumbing", - rating = 3.5, - reviews = ArrayDeque(), - location = null, // Simplify for the test - price = 49.0, - ), - WorkerProfile( - uid = "worker1", - fieldOfWork = "Electrical", - rating = 3.0, - reviews = ArrayDeque(), - location = null, - price = 59.0, - )) - - // Mock the AccountViewModel to return sample account data + // Mock AccountViewModel fetchUserAccount every { accountViewModel.fetchUserAccount(any(), captureLambda()) } answers { val uid = firstArg() - val account = + val lambda = secondArg<(Account?) -> Unit>() + lambda( when (uid) { "worker0" -> Account( @@ -108,41 +47,70 @@ class ProfileResultsTest { firstName = "John", lastName = "Doe", email = "", - Timestamp.now()) + birthDate = Timestamp.now()) "worker1" -> Account( uid = "worker1", firstName = "Jane", lastName = "Smith", email = "", - Timestamp.now()) + birthDate = Timestamp.now()) else -> null - } - lambda<(Account?) -> Unit>().invoke(account) + }) } + } + + @Test + fun profileResults_displaysWorkerProfiles_correctly() { + // Test data: profiles, images, and location + val testProfiles = + listOf( + WorkerProfile( + uid = "worker0", + displayName = "John Doe", + fieldOfWork = "Plumbing", + reviews = ArrayDeque(listOf()), + location = Location(40.0, 70.0), + price = 49.0), + WorkerProfile( + uid = "worker1", + displayName = "Jane Smith", + fieldOfWork = "Electrical", + reviews = ArrayDeque(listOf()), + location = Location(41.0, 71.0), + price = 59.0)) + + val dummyBitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888) + val profileImagesMap = mapOf("worker0" to dummyBitmap, "worker1" to dummyBitmap) + val bannerImagesMap = mapOf("worker0" to dummyBitmap, "worker1" to dummyBitmap) + val baseLocation = Location(0.0, 0.0) - // Set the content with ProfileContent composable + // Set the ProfileResults composable composeTestRule.setContent { ProfileResults( profiles = testProfiles, - listState = rememberLazyListState(), searchViewModel = searchViewModel, accountViewModel = accountViewModel, - heightRatio = 1.0f, - workerViewModel = workerViewModel, - onBookClick = { _, _ -> }) + onBookClick = { _, _, _, _ -> }, + profileImagesMap = profileImagesMap, + bannerImagesMap = bannerImagesMap, + baseLocation = baseLocation, + screenHeight = 1000.dp) } // Allow coroutines to complete composeTestRule.waitForIdle() - // Verify that the profile items are displayed correctly - composeTestRule.onNodeWithTag("worker_profile_result_0").assertIsDisplayed() + // Verify first profile + composeTestRule.onNodeWithTag("worker_profile_result0").assertIsDisplayed() composeTestRule.onNodeWithText("John Doe").assertIsDisplayed() composeTestRule.onNodeWithText("Plumbing").assertIsDisplayed() + composeTestRule.onNodeWithText("49.0").assertIsDisplayed() - composeTestRule.onNodeWithTag("worker_profile_result_1").assertIsDisplayed() + // Verify second profile + composeTestRule.onNodeWithTag("worker_profile_result1").assertIsDisplayed() composeTestRule.onNodeWithText("Jane Smith").assertIsDisplayed() composeTestRule.onNodeWithText("Electrical").assertIsDisplayed() + composeTestRule.onNodeWithText("59.0").assertIsDisplayed() } } diff --git a/app/src/androidTest/java/com/arygm/quickfix/ui/search/QuickFixFinderScreenTest.kt b/app/src/androidTest/java/com/arygm/quickfix/ui/search/QuickFixFinderScreenTest.kt index df623f83..b5db2a67 100644 --- a/app/src/androidTest/java/com/arygm/quickfix/ui/search/QuickFixFinderScreenTest.kt +++ b/app/src/androidTest/java/com/arygm/quickfix/ui/search/QuickFixFinderScreenTest.kt @@ -6,16 +6,20 @@ import androidx.datastore.preferences.core.Preferences import com.arygm.quickfix.model.account.AccountViewModel import com.arygm.quickfix.model.category.CategoryRepositoryFirestore import com.arygm.quickfix.model.category.CategoryViewModel +import com.arygm.quickfix.model.locations.Location import com.arygm.quickfix.model.offline.small.PreferencesRepository import com.arygm.quickfix.model.offline.small.PreferencesViewModel +import com.arygm.quickfix.model.profile.Profile import com.arygm.quickfix.model.profile.ProfileRepository import com.arygm.quickfix.model.profile.ProfileViewModel +import com.arygm.quickfix.model.profile.UserProfile import com.arygm.quickfix.model.profile.WorkerProfileRepositoryFirestore import com.arygm.quickfix.model.quickfix.QuickFixViewModel import com.arygm.quickfix.model.search.AnnouncementRepository import com.arygm.quickfix.model.search.AnnouncementViewModel import com.arygm.quickfix.model.search.SearchViewModel import com.arygm.quickfix.ui.navigation.NavigationActions +import com.arygm.quickfix.ui.uiMode.appContentUI.userModeUI.search.QuickFixFinderScreen import io.mockk.mockk import kotlinx.coroutines.flow.MutableStateFlow import org.junit.Before @@ -37,6 +41,7 @@ class QuickFixFinderScreenTest { private lateinit var profileViewModel: ProfileViewModel private lateinit var announcementViewModel: AnnouncementViewModel private lateinit var workerProfileRepo: WorkerProfileRepositoryFirestore + private lateinit var userProfileRepositoryFirestore: ProfileRepository private lateinit var categoryRepo: CategoryRepositoryFirestore private lateinit var searchViewModel: SearchViewModel private lateinit var accountViewModel: AccountViewModel @@ -51,6 +56,8 @@ class QuickFixFinderScreenTest { navigationActions = mock(NavigationActions::class.java) navigationActionsRoot = mock(NavigationActions::class.java) + userProfileRepositoryFirestore = mock(ProfileRepository::class.java) + preferencesRepository = mock(PreferencesRepository::class.java) announcementRepository = mock(AnnouncementRepository::class.java) userProfileRepository = mock(ProfileRepository::class.java) @@ -64,7 +71,26 @@ class QuickFixFinderScreenTest { announcementViewModel = AnnouncementViewModel(announcementRepository, preferencesRepository, userProfileRepository) - profileViewModel = mock(ProfileViewModel::class.java) + org.mockito.kotlin + .doAnswer { invocation -> + val uid = invocation.arguments[0] as String + val onSuccess = invocation.arguments[1] as (Profile?) -> Unit + val onFailure = invocation.arguments[2] as (Exception) -> Unit + + // Return a user profile with a "Home" location + val testUserProfile = + UserProfile( + locations = listOf(Location(latitude = 40.0, longitude = -74.0, name = "Home")), + announcements = emptyList(), + uid = uid) + onSuccess(testUserProfile) + null + } + .`when`(userProfileRepositoryFirestore) + .getProfileById(anyString(), org.mockito.kotlin.any(), org.mockito.kotlin.any()) + + profileViewModel = ProfileViewModel(userProfileRepositoryFirestore) + workerViewModel = mockk(relaxed = true) workerProfileRepo = mockk(relaxed = true) categoryRepo = mockk(relaxed = true) diff --git a/app/src/androidTest/java/com/arygm/quickfix/ui/search/QuickFixSlidingWindowWorkerTest.kt b/app/src/androidTest/java/com/arygm/quickfix/ui/search/QuickFixSlidingWindowWorkerTest.kt index 0a037583..6527e285 100644 --- a/app/src/androidTest/java/com/arygm/quickfix/ui/search/QuickFixSlidingWindowWorkerTest.kt +++ b/app/src/androidTest/java/com/arygm/quickfix/ui/search/QuickFixSlidingWindowWorkerTest.kt @@ -1,11 +1,11 @@ package com.arygm.quickfix.ui.search +import android.graphics.Bitmap import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.unit.dp -import com.arygm.quickfix.R import com.arygm.quickfix.ui.uiMode.appContentUI.userModeUI.search.QuickFixSlidingWindowWorker import org.junit.Rule import org.junit.Test @@ -37,11 +37,11 @@ class QuickFixSlidingWindowWorkerTest { QuickFixSlidingWindowWorker( isVisible = true, onDismiss = { /* No-op */}, - bannerImage = R.drawable.moroccan_flag, - profilePicture = R.drawable.placeholder_worker, + bannerImage = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888), + profilePicture = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888), initialSaved = false, workerCategory = "Painter", - workerAddress = "123 Main Street", + selectedCityName = "123 Main Street", description = "Sample description for the worker.", includedServices = includedServices, addonServices = addonServices, @@ -92,11 +92,11 @@ class QuickFixSlidingWindowWorkerTest { QuickFixSlidingWindowWorker( isVisible = true, onDismiss = { /* No-op */}, - bannerImage = R.drawable.moroccan_flag, - profilePicture = R.drawable.placeholder_worker, + bannerImage = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888), + profilePicture = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888), initialSaved = false, workerCategory = "Painter", - workerAddress = "123 Main Street", + selectedCityName = "123 Main Street", description = "Sample description for the worker.", includedServices = includedServices, addonServices = addOnServices, @@ -155,11 +155,11 @@ class QuickFixSlidingWindowWorkerTest { QuickFixSlidingWindowWorker( isVisible = true, onDismiss = { /* No-op */}, - bannerImage = R.drawable.moroccan_flag, - profilePicture = R.drawable.placeholder_worker, + bannerImage = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888), + profilePicture = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888), initialSaved = false, workerCategory = "Painter", - workerAddress = "123 Main Street", + selectedCityName = "123 Main Street", description = "Sample description for the worker.", includedServices = includedServices, addonServices = addOnServices, diff --git a/app/src/androidTest/java/com/arygm/quickfix/ui/search/SearchOnBoardingTest.kt b/app/src/androidTest/java/com/arygm/quickfix/ui/search/SearchOnBoardingTest.kt index 35438ee6..f0705145 100644 --- a/app/src/androidTest/java/com/arygm/quickfix/ui/search/SearchOnBoardingTest.kt +++ b/app/src/androidTest/java/com/arygm/quickfix/ui/search/SearchOnBoardingTest.kt @@ -5,30 +5,55 @@ import androidx.compose.ui.test.SemanticsMatcher import androidx.compose.ui.test.assert import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertTextEquals +import androidx.compose.ui.test.filter +import androidx.compose.ui.test.hasClickAction +import androidx.compose.ui.test.hasParent +import androidx.compose.ui.test.hasSetTextAction +import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollToIndex import androidx.compose.ui.test.performTextInput +import androidx.compose.ui.test.performTextReplacement import androidx.compose.ui.test.printToLog import androidx.compose.ui.text.AnnotatedString +import androidx.datastore.preferences.core.Preferences import com.arygm.quickfix.model.account.AccountRepositoryFirestore import com.arygm.quickfix.model.account.AccountViewModel import com.arygm.quickfix.model.category.CategoryRepositoryFirestore import com.arygm.quickfix.model.category.CategoryViewModel +import com.arygm.quickfix.model.category.Subcategory +import com.arygm.quickfix.model.locations.Location +import com.arygm.quickfix.model.offline.small.PreferencesRepository +import com.arygm.quickfix.model.offline.small.PreferencesViewModel +import com.arygm.quickfix.model.profile.Profile import com.arygm.quickfix.model.profile.ProfileRepository import com.arygm.quickfix.model.profile.ProfileViewModel +import com.arygm.quickfix.model.profile.UserProfile +import com.arygm.quickfix.model.profile.WorkerProfile import com.arygm.quickfix.model.profile.WorkerProfileRepositoryFirestore import com.arygm.quickfix.model.quickfix.QuickFixViewModel import com.arygm.quickfix.model.search.SearchViewModel import com.arygm.quickfix.ressources.C import com.arygm.quickfix.ui.navigation.NavigationActions +import com.arygm.quickfix.ui.uiMode.appContentUI.userModeUI.navigation.UserTopLevelDestinations import com.arygm.quickfix.ui.uiMode.appContentUI.userModeUI.search.SearchOnBoarding import io.mockk.mockk +import java.time.LocalDate +import java.time.LocalTime +import kotlinx.coroutines.flow.MutableStateFlow import org.junit.Before import org.junit.Rule import org.junit.Test +import org.mockito.ArgumentMatchers.anyString +import org.mockito.Mockito.doAnswer import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import org.mockito.kotlin.any +import org.mockito.kotlin.whenever class SearchOnBoardingTest { @@ -39,7 +64,11 @@ class SearchOnBoardingTest { private lateinit var searchViewModel: SearchViewModel private lateinit var accountViewModel: AccountViewModel private lateinit var categoryViewModel: CategoryViewModel + private lateinit var userViewModel: ProfileViewModel + private lateinit var preferencesViewModel: PreferencesViewModel private lateinit var navigationActionsRoot: NavigationActions + private lateinit var preferencesRepositoryDataStore: PreferencesRepository + private lateinit var userProfileRepositoryFirestore: ProfileRepository private lateinit var quickFixViewModel: QuickFixViewModel private lateinit var workerViewModel: ProfileViewModel private lateinit var workerProfileRepository: ProfileRepository @@ -56,21 +85,48 @@ class SearchOnBoardingTest { categoryRepo = mockk(relaxed = true) accountRepositoryFirestore = mock(AccountRepositoryFirestore::class.java) searchViewModel = SearchViewModel(workerProfileRepo) + userProfileRepositoryFirestore = mock(ProfileRepository::class.java) categoryViewModel = CategoryViewModel(categoryRepo) accountViewModel = mockk(relaxed = true) quickFixViewModel = QuickFixViewModel(mock()) + userViewModel = ProfileViewModel(userProfileRepositoryFirestore) + + preferencesRepositoryDataStore = mock(PreferencesRepository::class.java) + + val mockedPreferenceFlow = MutableStateFlow(null) + whenever(preferencesRepositoryDataStore.getPreferenceByKey(any>())) + .thenReturn(mockedPreferenceFlow) + preferencesViewModel = PreferencesViewModel(preferencesRepositoryDataStore) + doAnswer { invocation -> + val uid = invocation.arguments[0] as String + val onSuccess = invocation.arguments[1] as (Profile?) -> Unit + val onFailure = invocation.arguments[2] as (Exception) -> Unit + + // Return a user profile with a "Home" location + val testUserProfile = + UserProfile( + locations = listOf(Location(latitude = 40.0, longitude = -74.0, name = "Home")), + announcements = emptyList(), + uid = uid) + onSuccess(testUserProfile) + null + } + .`when`(userProfileRepositoryFirestore) + .getProfileById(anyString(), any(), any()) } @Test fun searchOnBoarding_displaysSearchInput() { composeTestRule.setContent { SearchOnBoarding( - navigationActions = navigationActions, + navigationActions, navigationActionsRoot, searchViewModel, accountViewModel, + preferencesViewModel, + userViewModel, categoryViewModel, - quickFixViewModel, + onBookClick = { _, _, _, _ -> }, workerViewModel) } @@ -86,12 +142,14 @@ class SearchOnBoardingTest { fun searchOnBoarding_clearsTextOnTrailingIconClick() { composeTestRule.setContent { SearchOnBoarding( - navigationActions = navigationActions, + navigationActions, navigationActionsRoot, searchViewModel, accountViewModel, + preferencesViewModel, + userViewModel, categoryViewModel, - quickFixViewModel, + onBookClick = { _, _, _, _ -> }, workerViewModel) } @@ -116,13 +174,15 @@ class SearchOnBoardingTest { fun searchOnBoarding_switchesFromCategoriesToProfiles() { composeTestRule.setContent { SearchOnBoarding( - navigationActions = navigationActions, - navigationActionsRoot = navigationActionsRoot, - searchViewModel = searchViewModel, - accountViewModel = accountViewModel, - categoryViewModel = categoryViewModel, - quickFixViewModel = quickFixViewModel, - workerViewModel = workerViewModel) + navigationActions, + navigationActionsRoot, + searchViewModel, + accountViewModel, + preferencesViewModel, + userViewModel, + categoryViewModel, + onBookClick = { _, _, _, _ -> }, + workerViewModel) } // Verify initial state (Categories are displayed) @@ -132,4 +192,279 @@ class SearchOnBoardingTest { // Verify state after query input (Categories disappear, Profiles appear) composeTestRule.onNodeWithText("Categories").assertDoesNotExist() } + + @Test + fun searchOnBoarding_showsFilterButtonsWhenQueryIsNotEmpty() { + composeTestRule.setContent { + SearchOnBoarding( + navigationActions, + navigationActionsRoot, + searchViewModel, + accountViewModel, + preferencesViewModel, + userViewModel, + categoryViewModel, + onBookClick = { _, _, _, _ -> }, + workerViewModel) + } + + // Input text to simulate non-empty search + composeTestRule.onNodeWithTag("searchContent").performTextInput("Painter") + + composeTestRule.onNodeWithTag("tuneButton").performClick() + + // Verify that the filter row becomes visible + composeTestRule.onNodeWithTag("filter_buttons_row").assertIsDisplayed() + } + + @Test + fun testOpenAvailabilityFilterMenu() { + // Set up test worker profiles + val worker1 = + WorkerProfile( + uid = "worker1", + fieldOfWork = "Painter", + rating = 4.5, + workingHours = Pair(LocalTime.of(9, 0), LocalTime.of(17, 0)), + unavailability_list = listOf(LocalDate.now()), + location = Location(40.7128, -74.0060)) + + val worker2 = + WorkerProfile( + uid = "worker2", + fieldOfWork = "Electrician", + rating = 4.0, + workingHours = Pair(LocalTime.of(8, 0), LocalTime.of(16, 0)), + unavailability_list = emptyList(), + location = Location(40.7128, -74.0060)) + + // Update the searchViewModel with test workers + searchViewModel._subCategoryWorkerProfiles.value = listOf(worker1, worker2) + + // Set the composable content + composeTestRule.setContent { + SearchOnBoarding( + navigationActions, + navigationActionsRoot, + searchViewModel, + accountViewModel, + preferencesViewModel, + userViewModel, + categoryViewModel, + onBookClick = { _, _, _, _ -> }, + workerViewModel) + } + + // Wait for the UI to settle + composeTestRule.waitForIdle() + + // Perform a search query to display filter buttons + composeTestRule.onNodeWithTag("searchContent").performTextInput("Painter") + composeTestRule.onNodeWithTag("tuneButton").performClick() + composeTestRule.onNodeWithTag("filter_buttons_row").assertIsDisplayed().performScrollToIndex(3) + + // Scroll to the "Availability" filter button + + // Click the "Availability" filter button + composeTestRule.onNodeWithText("Availability").performClick() + + // Wait for the availability bottom sheet to appear + composeTestRule.waitForIdle() + + // Verify the bottom sheet and its components are displayed + composeTestRule.onNodeWithTag("availabilityBottomSheet").assertIsDisplayed() + composeTestRule.onNodeWithTag("timePickerColumn").assertIsDisplayed() + composeTestRule.onNodeWithTag("timeInput").assertIsDisplayed() + composeTestRule.onNodeWithTag("bottomSheetColumn").assertIsDisplayed() + composeTestRule.onNodeWithText("Enter time").assertIsDisplayed() + + val today = LocalDate.now() + val day = today.dayOfMonth + + val textFields = + composeTestRule.onAllNodes(hasSetTextAction()).filter(hasParent(hasTestTag("timeInput"))) + + // Ensure that we have at least two text fields + assert(textFields.fetchSemanticsNodes().size >= 2) + + // Set the hour to "07" + textFields[0].performTextReplacement("08") + + // Set the minute to "00" + textFields[1].performTextReplacement("00") + + // Find the node representing today's date and perform a click + composeTestRule + .onNode(hasText(day.toString()) and hasClickAction() and !hasSetTextAction()) + .performClick() + + composeTestRule.onNodeWithText("OK").performClick() + + composeTestRule.waitForIdle() + + // Verify that no workers are displayed + } + + @Test + fun testOpenPriceRangeFilterMenu() { + // Set up test worker profiles + val worker1 = + WorkerProfile( + uid = "worker1", + fieldOfWork = "Painter", + rating = 4.5, + workingHours = Pair(LocalTime.of(9, 0), LocalTime.of(17, 0)), + location = Location(40.7128, -74.0060)) + + searchViewModel._subCategoryWorkerProfiles.value = listOf(worker1) + + // Set the composable content + composeTestRule.setContent { + SearchOnBoarding( + navigationActions, + navigationActionsRoot, + searchViewModel, + accountViewModel, + preferencesViewModel, + userViewModel, + categoryViewModel, + onBookClick = { _, _, _, _ -> }, + workerViewModel) + } + // Perform a search query to display filter buttons + composeTestRule.onNodeWithTag("searchContent").performTextInput("Painter") + composeTestRule.onNodeWithTag("tuneButton").performClick() + composeTestRule.onNodeWithTag("filter_buttons_row").assertIsDisplayed().performScrollToIndex(5) + + // Click on "Price Range" filter button + composeTestRule.onNodeWithText("Price Range").performClick() + + // Verify the bottom sheet appears + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("priceRangeModalSheet").assertIsDisplayed() + } + + @Test + fun testOpenLocationFilterMenu() { + // Set up test worker profiles + val worker1 = + WorkerProfile( + uid = "worker1", + fieldOfWork = "Painter", + rating = 4.5, + workingHours = Pair(LocalTime.of(9, 0), LocalTime.of(17, 0)), + location = Location(40.7128, -74.0060)) + + searchViewModel._subCategoryWorkerProfiles.value = listOf(worker1) + + // Set the composable content + composeTestRule.setContent { + SearchOnBoarding( + navigationActions, + navigationActionsRoot, + searchViewModel, + accountViewModel, + preferencesViewModel, + userViewModel, + categoryViewModel, + onBookClick = { _, _, _, _ -> }, + workerViewModel) + } + + // Perform a search query to display filter buttons + composeTestRule.onNodeWithTag("searchContent").performTextInput("Painter") + composeTestRule.onNodeWithTag("tuneButton").performClick() + composeTestRule.onNodeWithTag("filter_buttons_row").assertIsDisplayed() + + // Click on "Location" filter button + composeTestRule.onNodeWithText("Location").performClick() + + // Verify the bottom sheet appears + composeTestRule.onNodeWithTag("locationFilterModalSheet").assertIsDisplayed() + } + + @Test + fun searchOnBoarding_cancelButtonNavigatesHome() { + composeTestRule.setContent { + SearchOnBoarding( + navigationActions, + navigationActionsRoot, + searchViewModel, + accountViewModel, + preferencesViewModel, + userViewModel, + categoryViewModel, + onBookClick = { _, _, _, _ -> }, + workerViewModel) + } + + // Click the Cancel button + composeTestRule.onNodeWithText("Cancel").performClick() + + // Verify navigation to HOME was requested + verify(navigationActionsRoot).navigateTo(UserTopLevelDestinations.HOME) + } + + @Test + fun searchOnBoarding_servicesFilterApplyAndClear() { + // Set up some mock services in searchViewModel + searchViewModel._searchSubcategory.value = + Subcategory(name = "TestSubCategory", tags = listOf("Service1", "Service2")) + + composeTestRule.setContent { + SearchOnBoarding( + navigationActions, + navigationActionsRoot, + searchViewModel, + accountViewModel, + preferencesViewModel, + userViewModel, + categoryViewModel, + onBookClick = { _, _, _, _ -> }, + workerViewModel) + } + + // Perform a search to show filters + composeTestRule.onNodeWithTag("searchContent").performTextInput("Test") + composeTestRule.onNodeWithTag("tuneButton").performClick() + + composeTestRule.onNodeWithTag("filter_buttons_row").performScrollToIndex(1) + // Open services bottom sheet + composeTestRule.onNodeWithText("Service Type").performClick() + + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("chooseServiceTypeModalSheet").assertIsDisplayed() + + // Simulate selecting "Interior Painter" + composeTestRule.onNodeWithText("Apply").performClick() + } + + @Test + fun searchOnBoarding_priceRangeFilterApplyAndClear() { + composeTestRule.setContent { + SearchOnBoarding( + navigationActions, + navigationActionsRoot, + searchViewModel, + accountViewModel, + preferencesViewModel, + userViewModel, + categoryViewModel, + onBookClick = { _, _, _, _ -> }, + workerViewModel) + } + + // Perform a search to show filters + composeTestRule.onNodeWithTag("searchContent").performTextInput("Tester") + composeTestRule.onNodeWithTag("tuneButton").performClick() + composeTestRule.onNodeWithTag("filter_buttons_row").performScrollToIndex(4) + composeTestRule.onNodeWithText("Price Range").performClick() + + // Input a price range and apply + composeTestRule.onNodeWithText("Apply").performClick() + + // Open price range again and clear + composeTestRule.onNodeWithText("Price Range").performClick() + composeTestRule.onNodeWithText("Clear").performClick() + } } diff --git a/app/src/androidTest/java/com/arygm/quickfix/ui/search/SearchWorkerResultScreenTest.kt b/app/src/androidTest/java/com/arygm/quickfix/ui/search/SearchWorkerResultScreenTest.kt index b0125aa8..ed24aa94 100644 --- a/app/src/androidTest/java/com/arygm/quickfix/ui/search/SearchWorkerResultScreenTest.kt +++ b/app/src/androidTest/java/com/arygm/quickfix/ui/search/SearchWorkerResultScreenTest.kt @@ -20,9 +20,9 @@ import androidx.compose.ui.test.hasParent import androidx.compose.ui.test.hasSetTextAction import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.isRoot import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onAllNodesWithTag -import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onChildren import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithTag @@ -30,6 +30,7 @@ import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollToIndex import androidx.compose.ui.test.performTextReplacement +import androidx.compose.ui.test.printToLog import androidx.core.app.ActivityCompat import androidx.datastore.preferences.core.Preferences import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -212,8 +213,8 @@ class SearchWorkerResultScreenTest { searchViewModel, accountViewModel, userViewModel, - preferencesViewModel, quickFixViewModel, + preferencesViewModel, workerViewModel = workerViewModel) } // Verify that Back and Search icons are present in the top bar @@ -221,26 +222,6 @@ class SearchWorkerResultScreenTest { composeTestRule.onNodeWithContentDescription("Search").assertExists().assertIsDisplayed() } - @Test - fun testTitleAndDescriptionAreDisplayed() { - // Set the composable content - composeTestRule.setContent { - SearchWorkerResult( - navigationActions, - searchViewModel, - accountViewModel, - userViewModel, - preferencesViewModel, - quickFixViewModel, - workerViewModel = workerViewModel) - } - // Set the search query and verify that the title and description match the query - searchViewModel.setSearchQuery("Unknown") - - // Check if the description with the query text is displayed - composeTestRule.onAllNodesWithText("Unknown").assertCountEquals(3) - } - @Test fun testFilterButtonsAreDisplayed() { // Set the composable content @@ -250,8 +231,8 @@ class SearchWorkerResultScreenTest { searchViewModel, accountViewModel, userViewModel, - preferencesViewModel, quickFixViewModel, + preferencesViewModel, workerViewModel = workerViewModel) } @@ -260,7 +241,7 @@ class SearchWorkerResultScreenTest { composeTestRule.onNodeWithTag("tuneButton").performClick() // Verify that the LazyRow for filter buttons is visible - val filterButtonsRow = composeTestRule.onNodeWithTag("lazy_filter_row") + val filterButtonsRow = composeTestRule.onNodeWithTag("filter_buttons_row") filterButtonsRow.assertExists().assertIsDisplayed() // Define the expected button texts @@ -290,8 +271,8 @@ class SearchWorkerResultScreenTest { searchViewModel, accountViewModel, userViewModel, - preferencesViewModel, quickFixViewModel, + preferencesViewModel, workerViewModel = workerViewModel) } // Verify that the filter icon button is displayed and has a click action @@ -311,8 +292,8 @@ class SearchWorkerResultScreenTest { searchViewModel, accountViewModel, userViewModel, - preferencesViewModel, quickFixViewModel, + preferencesViewModel, workerViewModel = workerViewModel) } // Scroll through the LazyColumn and verify each profile result is displayed @@ -336,8 +317,8 @@ class SearchWorkerResultScreenTest { searchViewModel, accountViewModel, userViewModel, - preferencesViewModel, quickFixViewModel, + preferencesViewModel, workerViewModel = workerViewModel) } // Perform click on the back button and verify goBack() is called @@ -354,8 +335,8 @@ class SearchWorkerResultScreenTest { searchViewModel, accountViewModel, userViewModel, - preferencesViewModel, quickFixViewModel, + preferencesViewModel, workerViewModel = workerViewModel) } @@ -384,8 +365,8 @@ class SearchWorkerResultScreenTest { searchViewModel, accountViewModel, userViewModel, - preferencesViewModel, quickFixViewModel, + preferencesViewModel, workerViewModel = workerViewModel) } @@ -411,8 +392,8 @@ class SearchWorkerResultScreenTest { searchViewModel, accountViewModel, userViewModel, - preferencesViewModel, quickFixViewModel, + preferencesViewModel, workerViewModel = workerViewModel) } @@ -441,8 +422,8 @@ class SearchWorkerResultScreenTest { searchViewModel, accountViewModel, userViewModel, - preferencesViewModel, quickFixViewModel, + preferencesViewModel, workerViewModel = workerViewModel) } @@ -479,8 +460,8 @@ class SearchWorkerResultScreenTest { searchViewModel, accountViewModel, userViewModel, - preferencesViewModel, quickFixViewModel, + preferencesViewModel, workerViewModel = workerViewModel) } @@ -498,7 +479,6 @@ class SearchWorkerResultScreenTest { .onNodeWithTag("sliding_window_included_services_column") .assertExists() .assertIsDisplayed() - // Check for each included service val includedServices = listOf("Basic Consultation", "Service Inspection") @@ -516,8 +496,8 @@ class SearchWorkerResultScreenTest { searchViewModel, accountViewModel, userViewModel, - preferencesViewModel, quickFixViewModel, + preferencesViewModel, workerViewModel = workerViewModel) } @@ -553,8 +533,8 @@ class SearchWorkerResultScreenTest { searchViewModel, accountViewModel, userViewModel, - preferencesViewModel, quickFixViewModel, + preferencesViewModel, workerViewModel = workerViewModel) } @@ -592,8 +572,8 @@ class SearchWorkerResultScreenTest { searchViewModel, accountViewModel, userViewModel, - preferencesViewModel, quickFixViewModel, + preferencesViewModel, workerViewModel = workerViewModel) } @@ -608,13 +588,69 @@ class SearchWorkerResultScreenTest { // Verify the tags section is displayed composeTestRule.onNodeWithTag("sliding_window_tags_flow_row").assertExists().assertIsDisplayed() - // Check for each tag val tags = listOf("Reliable", "Experienced", "Professional") tags.forEach { tag -> composeTestRule.onNodeWithText(tag).assertExists().assertIsDisplayed() } } + @Test + fun testSaveButtonTogglesBetweenSaveAndSaved() { + // Set up the content + composeTestRule.setContent { + SearchWorkerResult( + navigationActions, + searchViewModel, + accountViewModel, + userViewModel, + quickFixViewModel, + preferencesViewModel, + workerViewModel = workerViewModel) + } + + // Wait until the worker profiles are displayed + composeTestRule.waitForIdle() + + // Click on the "Book" button of the first item + composeTestRule.onAllNodesWithTag("book_button")[0].assertExists().performClick() + + // Wait for the sliding window to appear + composeTestRule.waitForIdle() + + // Verify the "save" button is displayed + composeTestRule + .onNodeWithTag("sliding_window_save_button") + .assertExists() + .assertIsDisplayed() + .assertTextContains("save") + + // Click on the "save" button + composeTestRule.onNodeWithTag("sliding_window_save_button").performClick() + + // Wait for the UI to update + composeTestRule.waitForIdle() + + // Verify the button text changes to "saved" + composeTestRule + .onNodeWithTag("sliding_window_save_button") + .assertExists() + .assertIsDisplayed() + .assertTextContains("saved") + + // Click again to toggle back to "save" + composeTestRule.onNodeWithTag("sliding_window_save_button").performClick() + + // Wait for the UI to update + composeTestRule.waitForIdle() + + // Verify the button text changes back to "save" + composeTestRule + .onNodeWithTag("sliding_window_save_button") + .assertExists() + .assertIsDisplayed() + .assertTextContains("save") + } + @Test fun testWorkerFilteringByAvailabilityDays() { // Set up test worker profiles with specific working hours and unavailability @@ -655,8 +691,8 @@ class SearchWorkerResultScreenTest { searchViewModel, accountViewModel, userViewModel, - preferencesViewModel, quickFixViewModel, + preferencesViewModel, workerViewModel = workerViewModel) } @@ -667,7 +703,7 @@ class SearchWorkerResultScreenTest { composeTestRule.onNodeWithTag("worker_profiles_list").onChildren().assertCountEquals(3) composeTestRule.onNodeWithTag("tuneButton").performClick() - composeTestRule.onNodeWithTag("lazy_filter_row").performScrollToIndex(3) + composeTestRule.onNodeWithTag("filter_buttons_row").performScrollToIndex(3) // Simulate clicking the "Availability" filter button composeTestRule.onNodeWithText("Availability").performClick() @@ -748,8 +784,8 @@ class SearchWorkerResultScreenTest { searchViewModel, accountViewModel, userViewModel, - preferencesViewModel, quickFixViewModel, + preferencesViewModel, workerViewModel = workerViewModel) } @@ -760,7 +796,7 @@ class SearchWorkerResultScreenTest { composeTestRule.onNodeWithTag("worker_profiles_list").onChildren().assertCountEquals(3) composeTestRule.onNodeWithTag("tuneButton").performClick() - composeTestRule.onNodeWithTag("lazy_filter_row").performScrollToIndex(3) + composeTestRule.onNodeWithTag("filter_buttons_row").performScrollToIndex(3) // Simulate clicking the "Availability" filter button composeTestRule.onNodeWithText("Availability").performClick() @@ -841,8 +877,8 @@ class SearchWorkerResultScreenTest { searchViewModel, accountViewModel, userViewModel, - preferencesViewModel, quickFixViewModel, + preferencesViewModel, workerViewModel = workerViewModel) } @@ -853,7 +889,7 @@ class SearchWorkerResultScreenTest { composeTestRule.onNodeWithTag("worker_profiles_list").onChildren().assertCountEquals(3) composeTestRule.onNodeWithTag("tuneButton").performClick() - composeTestRule.onNodeWithTag("lazy_filter_row").performScrollToIndex(3) + composeTestRule.onNodeWithTag("filter_buttons_row").performScrollToIndex(3) // Simulate clicking the "Availability" filter button composeTestRule.onNodeWithText("Availability").performClick() @@ -926,13 +962,13 @@ class SearchWorkerResultScreenTest { searchViewModel, accountViewModel, userViewModel, - preferencesViewModel, quickFixViewModel, + preferencesViewModel, workerViewModel = workerViewModel) } composeTestRule.onNodeWithTag("tuneButton").performClick() - composeTestRule.onNodeWithTag("lazy_filter_row").performScrollToIndex(1) + composeTestRule.onNodeWithTag("filter_buttons_row").performScrollToIndex(1) // Click on the "Service Type" filter button composeTestRule.onNodeWithText("Service Type").performClick() @@ -994,14 +1030,14 @@ class SearchWorkerResultScreenTest { searchViewModel, accountViewModel, userViewModel, - preferencesViewModel, quickFixViewModel, + preferencesViewModel, workerViewModel = workerViewModel) } composeTestRule.onNodeWithTag("tuneButton").performClick() // Scroll to the "Highest Rating" button in the LazyRow - composeTestRule.onNodeWithTag("lazy_filter_row").performScrollToIndex(3) + composeTestRule.onNodeWithTag("filter_buttons_row").performScrollToIndex(3) // Click on the "Highest Rating" filter button composeTestRule.onNodeWithText("Highest Rating").performClick() @@ -1063,13 +1099,13 @@ class SearchWorkerResultScreenTest { searchViewModel, accountViewModel, userViewModel, - preferencesViewModel, quickFixViewModel, + preferencesViewModel, workerViewModel = workerViewModel) } composeTestRule.onNodeWithTag("tuneButton").performClick() - composeTestRule.onNodeWithTag("lazy_filter_row").performScrollToIndex(1) + composeTestRule.onNodeWithTag("filter_buttons_row").performScrollToIndex(1) // Apply Service Type filter composeTestRule.onNodeWithText("Service Type").performClick() composeTestRule.waitForIdle() @@ -1078,7 +1114,7 @@ class SearchWorkerResultScreenTest { composeTestRule.waitForIdle() // Scroll to the "Highest Rating" button in the LazyRow - composeTestRule.onNodeWithTag("lazy_filter_row").performScrollToIndex(3) + composeTestRule.onNodeWithTag("filter_buttons_row").performScrollToIndex(3) // Apply Highest Rating filter composeTestRule.onNodeWithText("Highest Rating").performClick() @@ -1134,13 +1170,13 @@ class SearchWorkerResultScreenTest { searchViewModel, accountViewModel, userViewModel, - preferencesViewModel, quickFixViewModel, + preferencesViewModel, workerViewModel = workerViewModel) } composeTestRule.onNodeWithTag("tuneButton").performClick() - composeTestRule.onNodeWithTag("lazy_filter_row").performScrollToIndex(1) + composeTestRule.onNodeWithTag("filter_buttons_row").performScrollToIndex(1) // Apply Service Type filter for a tag that doesn't exist composeTestRule.onNodeWithText("Service Type").performClick() @@ -1162,13 +1198,13 @@ class SearchWorkerResultScreenTest { searchViewModel, accountViewModel, userViewModel, - preferencesViewModel, quickFixViewModel, + preferencesViewModel, workerViewModel = workerViewModel) } composeTestRule.onNodeWithTag("tuneButton").performClick() - composeTestRule.onNodeWithTag("lazy_filter_row").performScrollToIndex(4) + composeTestRule.onNodeWithTag("filter_buttons_row").performScrollToIndex(4) // Click on the "Price Range" filter button composeTestRule.onNodeWithText("Price Range").performClick() @@ -1212,13 +1248,13 @@ class SearchWorkerResultScreenTest { searchViewModel, accountViewModel, userViewModel, - preferencesViewModel, quickFixViewModel, + preferencesViewModel, workerViewModel = workerViewModel) } composeTestRule.onNodeWithTag("tuneButton").performClick() - composeTestRule.onNodeWithTag("lazy_filter_row").performScrollToIndex(4) + composeTestRule.onNodeWithTag("filter_buttons_row").performScrollToIndex(4) // Click on the "Price Range" filter button composeTestRule.onNodeWithText("Price Range").performClick() @@ -1234,6 +1270,7 @@ class SearchWorkerResultScreenTest { val workerNodes = composeTestRule.onNodeWithTag("worker_profiles_list").onChildren() workerNodes.assertCountEquals(sortedWorkers.size) + workerNodes[0].printToLog("WorkerNode") sortedWorkers.forEachIndexed { index, worker -> workerNodes[index].assert( @@ -1274,13 +1311,13 @@ class SearchWorkerResultScreenTest { searchViewModel, accountViewModel, userViewModel, - preferencesViewModel, quickFixViewModel, + preferencesViewModel, workerViewModel = workerViewModel) } composeTestRule.onNodeWithTag("tuneButton").performClick() - composeTestRule.onNodeWithTag("lazy_filter_row").performScrollToIndex(4) + composeTestRule.onNodeWithTag("filter_buttons_row").performScrollToIndex(4) // Click on the "Price Range" filter button composeTestRule.onNodeWithText("Price Range").performClick() @@ -1329,8 +1366,8 @@ class SearchWorkerResultScreenTest { searchViewModel, accountViewModel, userViewModel, - preferencesViewModel, quickFixViewModel, + preferencesViewModel, workerViewModel = workerViewModel) } @@ -1340,7 +1377,7 @@ class SearchWorkerResultScreenTest { composeTestRule.onNodeWithTag("tuneButton").performClick() // Scroll to the "Location" button in the LazyRow if needed - composeTestRule.onNodeWithTag("lazy_filter_row").performScrollToIndex(1) + composeTestRule.onNodeWithTag("filter_buttons_row").performScrollToIndex(1) // Open the Location filter bottom sheet composeTestRule.onNodeWithText("Location").performClick() @@ -1350,6 +1387,11 @@ class SearchWorkerResultScreenTest { composeTestRule.onNodeWithTag("locationFilterModalSheet").assertIsDisplayed() // Select "Home" location + val roots = composeTestRule.onAllNodes(isRoot()) + roots.fetchSemanticsNodes().forEachIndexed { index, _ -> + println("Root Node $index Hierarchy:") + composeTestRule.onAllNodes(isRoot())[index].printToLog("RootNode$index") + } composeTestRule.onNodeWithText("Home").performClick() // Click Apply @@ -1360,7 +1402,7 @@ class SearchWorkerResultScreenTest { composeTestRule.onNodeWithTag("worker_profiles_list").onChildren().assertCountEquals(1) // Open Location filter again to clear - composeTestRule.onNodeWithTag("lazy_filter_row").performScrollToIndex(1) + composeTestRule.onNodeWithTag("filter_buttons_row").performScrollToIndex(1) composeTestRule.onNodeWithText("Location").performClick() composeTestRule.waitForIdle() @@ -1408,8 +1450,8 @@ class SearchWorkerResultScreenTest { searchViewModel, accountViewModel, userViewModel, - preferencesViewModel, quickFixViewModel, + preferencesViewModel, workerViewModel = workerViewModel) } @@ -1418,7 +1460,7 @@ class SearchWorkerResultScreenTest { composeTestRule.onNodeWithTag("worker_profiles_list").onChildren().assertCountEquals(3) composeTestRule.onNodeWithTag("tuneButton").performClick() - composeTestRule.onNodeWithTag("lazy_filter_row").performScrollToIndex(1) + composeTestRule.onNodeWithTag("filter_buttons_row").performScrollToIndex(1) // Apply Service Type filter = "Interior Painter" composeTestRule.onNodeWithText("Service Type").performClick() composeTestRule.waitForIdle() @@ -1431,7 +1473,7 @@ class SearchWorkerResultScreenTest { // Apply Location filter to get even more specific (Assume "Home") composeTestRule - .onNodeWithTag("lazy_filter_row") + .onNodeWithTag("filter_buttons_row") .performScrollToIndex(1) // scroll to "Location" if needed composeTestRule.onNodeWithText("Location").performClick() composeTestRule.waitForIdle() @@ -1443,7 +1485,7 @@ class SearchWorkerResultScreenTest { composeTestRule.onNodeWithTag("worker_profiles_list").onChildren().assertCountEquals(1) // Now clear the Location filter but keep the Service Type filter - composeTestRule.onNodeWithTag("lazy_filter_row").performScrollToIndex(1) + composeTestRule.onNodeWithTag("filter_buttons_row").performScrollToIndex(1) composeTestRule.onNodeWithText("Location").performClick() composeTestRule.waitForIdle() composeTestRule.onNodeWithTag("resetButton").performClick() @@ -1481,8 +1523,8 @@ class SearchWorkerResultScreenTest { searchViewModel, accountViewModel, userViewModel, - preferencesViewModel, quickFixViewModel, + preferencesViewModel, workerViewModel = workerViewModel) } @@ -1491,7 +1533,7 @@ class SearchWorkerResultScreenTest { composeTestRule.onNodeWithTag("worker_profiles_list").onChildren().assertCountEquals(2) composeTestRule.onNodeWithTag("tuneButton").performClick() - composeTestRule.onNodeWithTag("lazy_filter_row").performScrollToIndex(3) + composeTestRule.onNodeWithTag("filter_buttons_row").performScrollToIndex(3) // Apply Availability filter for today at 10:00 (both should be available) composeTestRule.onNodeWithText("Availability").performClick() composeTestRule.waitForIdle() @@ -1565,8 +1607,8 @@ class SearchWorkerResultScreenTest { searchViewModel, accountViewModel, userViewModel, - preferencesViewModel, quickFixViewModel, + preferencesViewModel, workerViewModel = workerViewModel) } @@ -1575,7 +1617,7 @@ class SearchWorkerResultScreenTest { composeTestRule.onNodeWithTag("tuneButton").performClick() // Apply Highest Rating filter - composeTestRule.onNodeWithTag("lazy_filter_row").performScrollToIndex(3) + composeTestRule.onNodeWithTag("filter_buttons_row").performScrollToIndex(3) composeTestRule.onNodeWithText("Highest Rating").performClick() composeTestRule.waitForIdle() @@ -1614,24 +1656,24 @@ class SearchWorkerResultScreenTest { searchViewModel, accountViewModel, userViewModel, - preferencesViewModel, quickFixViewModel, + preferencesViewModel, workerViewModel = workerViewModel) } composeTestRule.waitForIdle() - // Initially, the lazy_filter_row might not be visible until we click the tune button - composeTestRule.onNodeWithTag("lazy_filter_row").assertDoesNotExist() + // Initially, the filter_buttons_row might not be visible until we click the tune button + composeTestRule.onNodeWithTag("filter_buttons_row").assertDoesNotExist() // Click the tune button to show filter buttons composeTestRule.onNodeWithTag("tuneButton").performClick() - composeTestRule.onNodeWithTag("lazy_filter_row").assertIsDisplayed() + composeTestRule.onNodeWithTag("filter_buttons_row").assertIsDisplayed() // Click the tune button again to hide filter buttons composeTestRule.onNodeWithTag("tuneButton").performClick() composeTestRule.waitForIdle() - composeTestRule.onNodeWithTag("lazy_filter_row").assertDoesNotExist() + composeTestRule.onNodeWithTag("filter_buttons_row").assertDoesNotExist() } @Test @@ -1647,8 +1689,8 @@ class SearchWorkerResultScreenTest { searchViewModel, accountViewModel, userViewModel, - preferencesViewModel, quickFixViewModel, + preferencesViewModel, workerViewModel = workerViewModel) } @@ -1657,7 +1699,7 @@ class SearchWorkerResultScreenTest { composeTestRule.onNodeWithTag("tuneButton").performClick() // Attempt to open the Service Type filter - composeTestRule.onNodeWithTag("lazy_filter_row").performScrollToIndex(1) + composeTestRule.onNodeWithTag("filter_buttons_row").performScrollToIndex(1) composeTestRule.onNodeWithText("Service Type").performClick() composeTestRule.waitForIdle() @@ -1699,13 +1741,13 @@ class SearchWorkerResultScreenTest { searchViewModel, accountViewModel, userViewModel, - preferencesViewModel, quickFixViewModel, + preferencesViewModel, workerViewModel = workerViewModel) } composeTestRule.onNodeWithTag("tuneButton").performClick() - composeTestRule.onNodeWithTag("lazy_filter_row").performScrollToIndex(1) + composeTestRule.onNodeWithTag("filter_buttons_row").performScrollToIndex(1) // Click on the "Service Type" filter button composeTestRule.onNodeWithText("Service Type").performClick() @@ -1790,8 +1832,8 @@ class SearchWorkerResultScreenTest { searchViewModel, accountViewModel, userViewModel, - preferencesViewModel, quickFixViewModel, + preferencesViewModel, workerViewModel = workerViewModel) } @@ -1802,7 +1844,7 @@ class SearchWorkerResultScreenTest { composeTestRule.onNodeWithTag("worker_profiles_list").onChildren().assertCountEquals(3) composeTestRule.onNodeWithTag("tuneButton").performClick() - composeTestRule.onNodeWithTag("lazy_filter_row").performScrollToIndex(3) + composeTestRule.onNodeWithTag("filter_buttons_row").performScrollToIndex(3) // Simulate clicking the "Availability" filter button composeTestRule.onNodeWithText("Availability").performClick() @@ -1898,8 +1940,8 @@ class SearchWorkerResultScreenTest { searchViewModel, accountViewModel, userViewModel, - preferencesViewModel, quickFixViewModel, + preferencesViewModel, workerViewModel = workerViewModel) } @@ -1909,7 +1951,7 @@ class SearchWorkerResultScreenTest { composeTestRule.onNodeWithTag("tuneButton").performClick() // Scroll to the "Location" button in the LazyRow if needed - composeTestRule.onNodeWithTag("lazy_filter_row").performScrollToIndex(1) + composeTestRule.onNodeWithTag("filter_buttons_row").performScrollToIndex(1) // Open the Location filter bottom sheet composeTestRule.onNodeWithText("Location").performClick() @@ -1921,17 +1963,16 @@ class SearchWorkerResultScreenTest { composeTestRule.onNodeWithTag("applyButton").assertIsNotEnabled() // Select "Home" location - composeTestRule.onNodeWithText("Home").performClick() + composeTestRule.onNodeWithTag("locationOptionRow1").performClick() // Click Apply composeTestRule.onNodeWithTag("applyButton").performClick() - composeTestRule.waitForIdle() // Verify that only the worker at "Home" is displayed (worker1) composeTestRule.onNodeWithTag("worker_profiles_list").onChildren().assertCountEquals(1) // Open Location filter again to clear - composeTestRule.onNodeWithTag("lazy_filter_row").performScrollToIndex(1) + composeTestRule.onNodeWithTag("filter_buttons_row").performScrollToIndex(1) composeTestRule.onNodeWithText("Location").performClick() composeTestRule.waitForIdle() @@ -1940,7 +1981,7 @@ class SearchWorkerResultScreenTest { composeTestRule.onNodeWithTag("applyButton").assertIsEnabled() - composeTestRule.onNodeWithText("Home").performClick() + composeTestRule.onNodeWithTag("locationOptionRow0").performClick() // Clear the filter composeTestRule.onNodeWithTag("resetButton").performClick() @@ -2035,16 +2076,15 @@ class SearchWorkerResultScreenTest { searchViewModel, accountViewModel, userViewModel, - preferencesViewModel, quickFixViewModel, - locationHelper = locationHelper, + preferencesViewModel, workerViewModel = workerViewModel) } composeTestRule.waitForIdle() composeTestRule.onNodeWithTag("worker_profiles_list").onChildren().assertCountEquals(3) composeTestRule.onNodeWithTag("tuneButton").performClick() - composeTestRule.onNodeWithTag("lazy_filter_row").performScrollToIndex(5) + composeTestRule.onNodeWithTag("filter_buttons_row").performScrollToIndex(5) // Click on the "Price Range" filter button composeTestRule.onNodeWithText("Emergency").performClick() diff --git a/app/src/main/java/com/arygm/quickfix/ui/elements/defaultBitmap.kt b/app/src/main/java/com/arygm/quickfix/ui/elements/defaultBitmap.kt new file mode 100644 index 00000000..c272af89 --- /dev/null +++ b/app/src/main/java/com/arygm/quickfix/ui/elements/defaultBitmap.kt @@ -0,0 +1,5 @@ +package com.arygm.quickfix.ui.elements + +import android.graphics.Bitmap + +val defaultBitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888) 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 b5a9c23f..3257a4a2 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 @@ -51,7 +51,6 @@ import com.arygm.quickfix.ui.elements.QuickFixDisplayImagesScreen import com.arygm.quickfix.ui.elements.QuickFixOfflineBar import com.arygm.quickfix.ui.navigation.BottomNavigationMenu import com.arygm.quickfix.ui.navigation.NavigationActions -import com.arygm.quickfix.ui.search.QuickFixFinderScreen import com.arygm.quickfix.ui.uiMode.appContentUI.userModeUI.camera.QuickFixDisplayImages import com.arygm.quickfix.ui.uiMode.appContentUI.userModeUI.home.HomeScreen import com.arygm.quickfix.ui.uiMode.appContentUI.userModeUI.home.MessageScreen @@ -65,6 +64,7 @@ import com.arygm.quickfix.ui.uiMode.appContentUI.userModeUI.profile.UserProfileS import com.arygm.quickfix.ui.uiMode.appContentUI.userModeUI.profile.becomeWorker.BusinessScreen 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.userModeUI.search.QuickFixFinderScreen import com.arygm.quickfix.ui.uiMode.appContentUI.userModeUI.search.SearchWorkerResult import com.google.firebase.Firebase import com.google.firebase.firestore.firestore @@ -315,8 +315,8 @@ fun HomeNavHost( searchViewModel, accountViewModel, userViewModel, - preferencesViewModel, quickFixViewModel, + preferencesViewModel, workerViewModel = workerViewModel) } composable(UserScreen.MAP) { @@ -515,8 +515,8 @@ fun SearchNavHost( searchViewModel, accountViewModel, userViewModel, - preferencesViewModel, quickFixViewModel, + preferencesViewModel, workerViewModel = workerViewModel) } composable(UserScreen.SEARCH_LOCATION) { diff --git a/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/map/MapScreen.kt b/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/map/MapScreen.kt index 4001b3ab..cfca2009 100644 --- a/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/map/MapScreen.kt +++ b/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/map/MapScreen.kt @@ -95,8 +95,11 @@ fun MapScreen( var selectedWorker by remember { mutableStateOf(null) } val locationHelper = LocationHelper(context, MainActivity()) - var bannerImage by remember { mutableStateOf(R.drawable.moroccan_flag) } - var profilePicture by remember { mutableStateOf(R.drawable.placeholder_worker) } + val defaultBitmap = + Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888) // Example default Bitmap + + var profilePicture by remember { mutableStateOf(defaultBitmap) } + var bannerPicture by remember { mutableStateOf(defaultBitmap) } var initialSaved by remember { mutableStateOf(false) } var workerAddress by remember { mutableStateOf("") } @@ -252,11 +255,11 @@ fun MapScreen( QuickFixSlidingWindowWorker( isVisible = isWindowVisible, onDismiss = { isWindowVisible = false }, - bannerImage = bannerImage, + bannerImage = bannerPicture, profilePicture = profilePicture, initialSaved = initialSaved, workerCategory = it.fieldOfWork, - workerAddress = workerAddress, + selectedCityName = workerAddress, description = it.description, includedServices = it.includedServices.map { it.name }, addonServices = it.addOnServices.map { it.name }, diff --git a/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/search/Announcement.kt b/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/search/Announcement.kt index 9f8523a5..6cee3062 100644 --- a/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/search/Announcement.kt +++ b/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/search/Announcement.kt @@ -5,8 +5,20 @@ 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.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape @@ -16,10 +28,21 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.PhotoLibrary -import androidx.compose.material3.* +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -127,6 +150,7 @@ fun AnnouncementScreen( fun LocalDateTime.toMillis(): Long = this.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() + fun millisToLocalDateTime(millis: Long): LocalDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(millis), ZoneId.systemDefault()) diff --git a/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/search/ProfileResults.kt b/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/search/ProfileResults.kt index 5cb2777e..5383d9c7 100644 --- a/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/search/ProfileResults.kt +++ b/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/search/ProfileResults.kt @@ -1,11 +1,10 @@ package com.arygm.quickfix.ui.uiMode.appContentUI.userModeUI.search import android.graphics.Bitmap -import android.util.Log -import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -15,27 +14,26 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag -import com.arygm.quickfix.MainActivity +import androidx.compose.ui.unit.Dp import com.arygm.quickfix.model.account.Account import com.arygm.quickfix.model.account.AccountViewModel -import com.arygm.quickfix.model.profile.ProfileViewModel +import com.arygm.quickfix.model.locations.Location import com.arygm.quickfix.model.profile.WorkerProfile import com.arygm.quickfix.model.search.SearchViewModel import com.arygm.quickfix.utils.GeocoderWrapper -import com.arygm.quickfix.utils.LocationHelper -import kotlin.math.roundToInt @Composable fun ProfileResults( modifier: Modifier = Modifier, profiles: List, - listState: LazyListState, searchViewModel: SearchViewModel, accountViewModel: AccountViewModel, - heightRatio: Float, - workerViewModel: ProfileViewModel, + baseLocation: Location, + profileImagesMap: Map, + bannerImagesMap: Map, + screenHeight: Dp, geocoderWrapper: GeocoderWrapper = GeocoderWrapper(LocalContext.current), - onBookClick: (WorkerProfile, String) -> Unit + onBookClick: (WorkerProfile, String, Bitmap, Bitmap) -> Unit, ) { fun getCityNameFromCoordinates(latitude: Double, longitude: Double): String? { val addresses = geocoderWrapper.getFromLocation(latitude, longitude, 1) @@ -44,69 +42,59 @@ fun ProfileResults( ?: addresses?.firstOrNull()?.adminArea } - val context = LocalContext.current - val locationHelper = remember { LocationHelper(context, MainActivity()) } - - LazyColumn(modifier = modifier.fillMaxWidth(), state = listState) { + LazyColumn(modifier = modifier.fillMaxWidth().testTag("worker_profiles_list")) { items(profiles.size) { index -> val profile = profiles[index] - var account by remember { mutableStateOf(null) } var distance by remember { mutableStateOf(null) } - var profileImage by remember { mutableStateOf(null) } var cityName by remember { mutableStateOf(null) } + val profileImage = profileImagesMap[profile.uid] + val bannerImage = bannerImagesMap[profile.uid] + distance = + profile.location + ?.let { workerLocation -> + searchViewModel.calculateDistance( + workerLocation.latitude, + workerLocation.longitude, + baseLocation.latitude, + baseLocation.longitude) + } + ?.toInt() - // Fetch data once using LaunchedEffect, keyed by profile.uid LaunchedEffect(profile.uid) { - // Fetch profile image once - workerViewModel.fetchProfileImageAsBitmap( - profile.uid, - onSuccess = { profileImage = it }, - onFailure = { Log.e("ProfileResults", "Failed to fetch profile image: $it") }) - - // Fetch account data once - accountViewModel.fetchUserAccount(profile.uid) { fetchedAccount -> + accountViewModel.fetchUserAccount(profile.uid) { fetchedAccount: Account? -> account = fetchedAccount } + } - // Get current location once - locationHelper.getCurrentLocation { location -> - location?.let { - distance = - profile.location?.let { workerLocation -> - searchViewModel - .calculateDistance( - workerLocation.latitude, - workerLocation.longitude, - it.latitude, - it.longitude) - .toInt() - } - } + account?.let { acc -> + val locationName = + if (profile.location?.name.isNullOrEmpty()) "Unknown" else profile.location?.name - // Compute city name once + locationName?.let { cityName = - profile.location?.let { loc -> - getCityNameFromCoordinates(loc.latitude, loc.longitude) - } ?: "Unknown" + profile.location?.let { it1 -> + getCityNameFromCoordinates(it1.latitude, profile.location.longitude) + } + cityName?.let { it1 -> + profileImage?.let { it2 -> + SearchWorkerProfileResult( + modifier = Modifier.testTag("worker_profile_result$index"), + profileImage = it2, + name = profile.displayName, + category = profile.fieldOfWork, + rating = profile.reviews.map { review -> review.rating }.average(), + reviewCount = profile.reviews.size, + location = it1, + price = profile.price.toString(), + onBookClick = { onBookClick(profile, it1, it2, bannerImage!!) }, + distance = distance, + ) + } + } } } - - // Only show the result once all required data is available - val displayLoc = cityName ?: "Unknown" - if (account != null && profileImage != null) { - SearchWorkerProfileResult( - modifier = Modifier.fillMaxWidth().testTag("worker_profile_result_$index").clickable {}, - profileImage = profileImage!!, - name = "${account!!.firstName} ${account!!.lastName}", - category = profile.fieldOfWork, - rating = profile.rating, - reviewCount = profile.reviews.size, - location = displayLoc, - price = profile.price.roundToInt().toString(), - distance = distance, - onBookClick = { onBookClick(profile, displayLoc) }) - } + Spacer(modifier = Modifier.height(screenHeight * 0.004f)) } } } diff --git a/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/search/QuickFixFinder.kt b/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/search/QuickFixFinder.kt index 92e87045..fe2d647b 100644 --- a/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/search/QuickFixFinder.kt +++ b/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/search/QuickFixFinder.kt @@ -1,7 +1,8 @@ -package com.arygm.quickfix.ui.search +package com.arygm.quickfix.ui.uiMode.appContentUI.userModeUI.search import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize @@ -11,8 +12,8 @@ import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme.colorScheme +import androidx.compose.material3.MaterialTheme.typography import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Tab @@ -21,25 +22,30 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue 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.platform.LocalContext import androidx.compose.ui.platform.testTag -import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.Dp import androidx.lifecycle.viewmodel.compose.viewModel import com.arygm.quickfix.model.account.AccountViewModel import com.arygm.quickfix.model.category.CategoryViewModel import com.arygm.quickfix.model.offline.small.PreferencesViewModel import com.arygm.quickfix.model.profile.ProfileViewModel +import com.arygm.quickfix.model.profile.WorkerProfile import com.arygm.quickfix.model.quickfix.QuickFixViewModel import com.arygm.quickfix.model.search.AnnouncementViewModel import com.arygm.quickfix.model.search.SearchViewModel +import com.arygm.quickfix.ui.elements.defaultBitmap import com.arygm.quickfix.ui.navigation.NavigationActions -import com.arygm.quickfix.ui.uiMode.appContentUI.userModeUI.search.AnnouncementScreen -import com.arygm.quickfix.ui.uiMode.appContentUI.userModeUI.search.SearchOnBoarding +import com.arygm.quickfix.ui.uiMode.appContentUI.userModeUI.navigation.UserScreen import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -59,63 +65,90 @@ fun QuickFixFinderScreen( preferencesViewModel: PreferencesViewModel, workerViewModel: ProfileViewModel ) { + var isWindowVisible by remember { mutableStateOf(false) } + var selectedCityName by remember { mutableStateOf(null) } + + var selectedWorker by remember { mutableStateOf(WorkerProfile()) } val pagerState = rememberPagerState(pageCount = { 2 }) val colorBackground = if (pagerState.currentPage == 0) colorScheme.background else colorScheme.surface val colorButton = if (pagerState.currentPage == 1) colorScheme.background else colorScheme.surface - Scaffold( - containerColor = colorBackground, - topBar = { - TopAppBar( - title = { - val coroutineScope = rememberCoroutineScope() - Row( - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxSize().padding(end = 20.dp)) { - Surface( - color = colorButton, - shape = RoundedCornerShape(20.dp), - modifier = - Modifier.padding(horizontal = 40.dp).clip(RoundedCornerShape(20.dp))) { - TabRow( - selectedTabIndex = pagerState.currentPage, - containerColor = Color.Transparent, - divider = {}, - indicator = {}, - modifier = - Modifier.padding(horizontal = 1.dp, vertical = 1.dp) - .testTag("quickFixSearchTabRow")) { - QuickFixScreenTab(pagerState, coroutineScope, 0, "Search") - QuickFixScreenTab(pagerState, coroutineScope, 1, "Announce") - } - } - } - }, - colors = TopAppBarDefaults.topAppBarColors(containerColor = colorBackground), - modifier = Modifier.testTag("QuickFixFinderTopBar")) - }, - content = { padding -> - Column( - modifier = Modifier.fillMaxSize().testTag("QuickFixFinderContent").padding(padding), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally) { - HorizontalPager( - state = pagerState, - userScrollEnabled = false, - modifier = Modifier.testTag("quickFixSearchPager")) { page -> - when (page) { - 0 -> + var profilePicture by remember { mutableStateOf(defaultBitmap) } + var bannerPicture by remember { mutableStateOf(defaultBitmap) } + + var initialSaved by remember { mutableStateOf(false) } + BoxWithConstraints(modifier = Modifier.fillMaxSize()) { + val screenHeight = maxHeight + val screenWidth = maxWidth + + Scaffold( + containerColor = colorBackground, + topBar = { + TopAppBar( + title = { + val coroutineScope = rememberCoroutineScope() + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxSize().padding(end = screenWidth * 0.05f)) { + Surface( + color = colorButton, + shape = RoundedCornerShape(screenWidth * 0.05f), + modifier = + Modifier.padding(horizontal = screenWidth * 0.1f) + .clip(RoundedCornerShape(screenWidth * 0.05f))) { + TabRow( + selectedTabIndex = pagerState.currentPage, + containerColor = Color.Transparent, + divider = {}, + indicator = {}, + modifier = + Modifier.padding( + horizontal = screenWidth * 0.01f, + vertical = screenWidth * 0.01f) + .testTag("quickFixSearchTabRow")) { + QuickFixScreenTab( + pagerState, coroutineScope, 0, "Search", screenWidth) + QuickFixScreenTab( + pagerState, coroutineScope, 1, "Announce", screenWidth) + } + } + } + }, + colors = TopAppBarDefaults.topAppBarColors(containerColor = colorBackground), + modifier = Modifier.testTag("QuickFixFinderTopBar")) + }, + content = { padding -> + Column( + modifier = Modifier.fillMaxSize().testTag("QuickFixFinderContent").padding(padding), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally) { + HorizontalPager( + state = pagerState, + userScrollEnabled = false, + modifier = Modifier.testTag("quickFixSearchPager")) { page -> + when (page) { + 0 -> { SearchOnBoarding( navigationActions, navigationActionsRoot, searchViewModel, accountViewModel, + preferencesViewModel, + profileViewModel, categoryViewModel, - quickFixViewModel, - workerViewModel) - 1 -> + onBookClick = { selectedProfile, locName, profile, banner -> + bannerPicture = banner + profilePicture = profile + initialSaved = false + selectedCityName = locName + isWindowVisible = true + selectedWorker = selectedProfile + }, + workerViewModel = workerViewModel) + } + 1 -> { AnnouncementScreen( announcementViewModel, profileViewModel, @@ -124,11 +157,35 @@ fun QuickFixFinderScreen( categoryViewModel, navigationActions = navigationActions, isUser = isUser) - else -> Text("Should never happen !") + } + else -> Text("Should never happen !") + } } - } - } - }) + } + }) + + QuickFixSlidingWindowWorker( + isVisible = isWindowVisible, + onDismiss = { isWindowVisible = false }, + screenHeight = screenHeight, + screenWidth = screenWidth, + onContinueClick = { + quickFixViewModel.setSelectedWorkerProfile(selectedWorker) + navigationActions.navigateTo(UserScreen.QUICKFIX_ONBOARDING) + }, + bannerImage = bannerPicture, + profilePicture = profilePicture, + initialSaved = initialSaved, + workerCategory = selectedWorker.fieldOfWork, + selectedCityName = selectedCityName, + description = selectedWorker.description, + includedServices = selectedWorker.includedServices.map { it.name }, + addonServices = selectedWorker.addOnServices.map { it.name }, + workerRating = selectedWorker.reviews.map { it1 -> it1.rating }.average(), + tags = selectedWorker.tags, + reviews = selectedWorker.reviews.map { it.review }, + ) + } } @Composable @@ -136,14 +193,15 @@ fun QuickFixScreenTab( pagerState: PagerState, coroutineScope: CoroutineScope, currentPage: Int, - title: String + title: String, + screenWidth: Dp ) { Tab( selected = pagerState.currentPage == currentPage, onClick = { coroutineScope.launch { pagerState.scrollToPage(currentPage) } }, modifier = - Modifier.padding(horizontal = 4.dp, vertical = 4.dp) - .clip(RoundedCornerShape(13.dp)) + Modifier.padding(horizontal = screenWidth * 0.01f, vertical = screenWidth * 0.01f) + .clip(RoundedCornerShape(screenWidth * 0.0325f)) .background( if (pagerState.currentPage == currentPage) colorScheme.primary else Color.Transparent) @@ -153,8 +211,9 @@ fun QuickFixScreenTab( color = if (pagerState.currentPage == currentPage) colorScheme.background else colorScheme.tertiaryContainer, - style = MaterialTheme.typography.titleMedium, + style = typography.titleMedium, modifier = - Modifier.padding(horizontal = 16.dp, vertical = 8.dp).testTag("tabText$title")) + Modifier.padding(horizontal = screenWidth * 0.04f, vertical = screenWidth * 0.02f) + .testTag("tabText$title")) } } diff --git a/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/search/QuickFixSlidingWindowWorker.kt b/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/search/QuickFixSlidingWindowWorker.kt index d3efea3e..790317f7 100644 --- a/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/search/QuickFixSlidingWindowWorker.kt +++ b/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/search/QuickFixSlidingWindowWorker.kt @@ -1,6 +1,6 @@ package com.arygm.quickfix.ui.uiMode.appContentUI.userModeUI.search -import android.widget.RatingBar +import android.graphics.Bitmap import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -40,9 +40,10 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.arygm.quickfix.ui.elements.QuickFixButton @@ -54,11 +55,11 @@ import com.arygm.quickfix.ui.elements.RatingBar fun QuickFixSlidingWindowWorker( isVisible: Boolean, onDismiss: () -> Unit, - bannerImage: Int, - profilePicture: Int, + bannerImage: Bitmap?, + profilePicture: Bitmap?, initialSaved: Boolean, workerCategory: String, - workerAddress: String, + selectedCityName: String?, description: String, includedServices: List, addonServices: List, @@ -89,7 +90,7 @@ fun QuickFixSlidingWindowWorker( .testTag("sliding_window_top_bar")) { // Banner Image Image( - painter = painterResource(id = bannerImage), + painter = BitmapPainter(bannerImage!!.asImageBitmap()), contentDescription = "Banner", modifier = Modifier.fillMaxWidth() @@ -114,7 +115,7 @@ fun QuickFixSlidingWindowWorker( // Profile picture overlapping the banner image Image( - painter = painterResource(id = profilePicture), + painter = BitmapPainter(profilePicture!!.asImageBitmap()), contentDescription = "Profile Picture", modifier = Modifier.size(screenHeight * 0.1f) @@ -136,11 +137,13 @@ fun QuickFixSlidingWindowWorker( style = MaterialTheme.typography.headlineLarge, color = colorScheme.onBackground, modifier = Modifier.testTag("sliding_window_worker_category")) - Text( - text = workerAddress, - style = MaterialTheme.typography.headlineSmall, - color = colorScheme.onBackground, - modifier = Modifier.testTag("sliding_window_worker_address")) + selectedCityName?.let { + Text( + text = it, + style = MaterialTheme.typography.headlineSmall, + color = colorScheme.onBackground, + modifier = Modifier.testTag("sliding_window_worker_address")) + } } // Main content should be scrollable diff --git a/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/search/SearchFilters.kt b/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/search/SearchFilters.kt new file mode 100644 index 00000000..ac8550fa --- /dev/null +++ b/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/search/SearchFilters.kt @@ -0,0 +1,277 @@ +package com.arygm.quickfix.ui.uiMode.appContentUI.userModeUI.search + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CalendarMonth +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.Handyman +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.LocationSearching +import androidx.compose.material.icons.filled.MonetizationOn +import androidx.compose.material.icons.filled.Tune +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material.icons.filled.WorkspacePremium +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.MaterialTheme.colorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.arygm.quickfix.model.locations.Location +import com.arygm.quickfix.model.profile.WorkerProfile +import com.arygm.quickfix.model.search.SearchViewModel +import com.arygm.quickfix.ui.elements.QuickFixButton +import com.arygm.quickfix.ui.theme.poppinsTypography +import java.time.LocalDate + +data class SearchFilterButtons( + val onClick: () -> Unit, + val text: String, + val leadingIcon: ImageVector? = null, + val trailingIcon: ImageVector? = null, + val applied: Boolean = false +) + +data class SearchUIState( + val showFilterButtons: Boolean = false, + val showAvailabilityBottomSheet: Boolean = false, + val showServicesBottomSheet: Boolean = false, + val showPriceRangeBottomSheet: Boolean = false, + val showLocationBottomSheet: Boolean = false, +) + +data class SearchFiltersState( + var availabilityFilterApplied: Boolean = false, + var servicesFilterApplied: Boolean = false, + var priceFilterApplied: Boolean = false, + var locationFilterApplied: Boolean = false, + var ratingFilterApplied: Boolean = false, + var emergencyFilterApplied: Boolean = false, + var selectedDays: List = emptyList(), + var selectedHour: Int = 0, + var selectedMinute: Int = 0, + var selectedServices: List = emptyList(), + var selectedPriceStart: Int = 0, + var selectedPriceEnd: Int = 0, + var selectedLocation: Location = Location(), + var maxDistance: Int = 0, + var baseLocation: Location = Location(), + var phoneLocation: Location = Location(0.0, 0.0, "Default"), + var lastAppliedPriceStart: Int = 500, + var lastAppliedPriceEnd: Int = 2500, + var lastAppliedMaxDist: Int = 200, + var selectedLocationIndex: Int? = null, +) + +@Composable +fun rememberSearchFiltersState(): SearchFiltersState { + return remember { SearchFiltersState() } +} + +/** Applies all active filters to [workerProfiles]. */ +fun SearchFiltersState.reapplyFilters( + workerProfiles: List, + searchViewModel: SearchViewModel +): List { + var updatedProfiles = workerProfiles + + if (availabilityFilterApplied) { + updatedProfiles = + searchViewModel.filterWorkersByAvailability( + updatedProfiles, selectedDays, selectedHour, selectedMinute) + } + + if (servicesFilterApplied) { + updatedProfiles = searchViewModel.filterWorkersByServices(updatedProfiles, selectedServices) + } + + if (priceFilterApplied) { + updatedProfiles = + searchViewModel.filterWorkersByPriceRange( + updatedProfiles, selectedPriceStart, selectedPriceEnd) + } + + if (locationFilterApplied) { + updatedProfiles = + searchViewModel.filterWorkersByDistance(updatedProfiles, selectedLocation, maxDistance) + } + + if (ratingFilterApplied) { + updatedProfiles = searchViewModel.sortWorkersByRating(updatedProfiles) + } + if (emergencyFilterApplied) { + updatedProfiles = searchViewModel.emergencyFilter(updatedProfiles, baseLocation) + } + + return updatedProfiles +} + +/** Clears all active filters and returns the original [workerProfiles]. */ +fun SearchFiltersState.clearFilters(workerProfiles: List): List { + availabilityFilterApplied = false + priceFilterApplied = false + locationFilterApplied = false + ratingFilterApplied = false + servicesFilterApplied = false + selectedServices = emptyList() + baseLocation = phoneLocation + return workerProfiles +} + +/** + * Creates the list of filter buttons. + * + * Each button's action updates the filter state and calls [onProfilesUpdated] to update the + * displayed profiles. + */ +fun SearchFiltersState.getFilterButtons( + workerProfiles: List, + filteredProfiles: List, + searchViewModel: SearchViewModel, + onProfilesUpdated: (List) -> Unit, + onShowAvailabilityBottomSheet: () -> Unit, + onShowServicesBottomSheet: () -> Unit, + onShowPriceRangeBottomSheet: () -> Unit, + onShowLocationBottomSheet: () -> Unit +): List { + + return listOf( + SearchFilterButtons( + onClick = { + val cleared = clearFilters(workerProfiles) + onProfilesUpdated(cleared) + }, + text = "Clear", + leadingIcon = Icons.Default.Clear), + SearchFilterButtons( + onClick = { onShowLocationBottomSheet() }, + text = "Location", + leadingIcon = Icons.Default.LocationSearching, + trailingIcon = Icons.Default.KeyboardArrowDown, + applied = locationFilterApplied), + SearchFilterButtons( + onClick = { onShowServicesBottomSheet() }, + text = "Service Type", + leadingIcon = Icons.Default.Handyman, + trailingIcon = Icons.Default.KeyboardArrowDown, + applied = servicesFilterApplied), + SearchFilterButtons( + onClick = { onShowAvailabilityBottomSheet() }, + text = "Availability", + leadingIcon = Icons.Default.CalendarMonth, + trailingIcon = Icons.Default.KeyboardArrowDown, + applied = availabilityFilterApplied), + SearchFilterButtons( + onClick = { + if (ratingFilterApplied) { + ratingFilterApplied = false + onProfilesUpdated(reapplyFilters(workerProfiles, searchViewModel)) + } else { + val rated = searchViewModel.sortWorkersByRating(filteredProfiles) + ratingFilterApplied = true + onProfilesUpdated(rated) + } + }, + text = "Highest Rating", + leadingIcon = Icons.Default.WorkspacePremium, + trailingIcon = if (ratingFilterApplied) Icons.Default.Clear else null, + applied = ratingFilterApplied), + SearchFilterButtons( + onClick = { onShowPriceRangeBottomSheet() }, + text = "Price Range", + leadingIcon = Icons.Default.MonetizationOn, + trailingIcon = Icons.Default.KeyboardArrowDown, + applied = priceFilterApplied), + SearchFilterButtons( + onClick = { + if (emergencyFilterApplied) { + emergencyFilterApplied = false + onProfilesUpdated(reapplyFilters(workerProfiles, searchViewModel)) + } else { + lastAppliedMaxDist = 200 + lastAppliedPriceStart = 500 + lastAppliedPriceEnd = 2500 + selectedLocationIndex = null + selectedServices = emptyList() + availabilityFilterApplied = false + priceFilterApplied = false + locationFilterApplied = false + ratingFilterApplied = false + servicesFilterApplied = false + baseLocation = phoneLocation + + val emergencyFilteredProfiles = + searchViewModel.emergencyFilter(workerProfiles, baseLocation) + emergencyFilterApplied = true + onProfilesUpdated(emergencyFilteredProfiles) + } + }, + text = "Emergency", + leadingIcon = Icons.Default.Warning, + trailingIcon = if (emergencyFilterApplied) Icons.Default.Clear else null, + applied = emergencyFilterApplied)) +} + +@Composable +fun FilterRow( + showFilterButtons: Boolean, + toggleFilterButtons: () -> Unit, + listOfButtons: List, + modifier: Modifier = Modifier, + screenWidth: Dp, + screenHeight: Dp +) { + + IconButton( + onClick = { toggleFilterButtons() }, + modifier = modifier.testTag("tuneButton"), + colors = + IconButtonDefaults.iconButtonColors( + containerColor = + if (showFilterButtons) colorScheme.primary else colorScheme.surface)) { + Icon( + imageVector = Icons.Default.Tune, + contentDescription = "Filter", + tint = if (showFilterButtons) colorScheme.onPrimary else colorScheme.onBackground, + ) + } + + Spacer(modifier = Modifier.width(10.dp)) + + AnimatedVisibility(visible = showFilterButtons) { + LazyRow( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.testTag("filter_buttons_row")) { + items(listOfButtons.size) { index -> + val button = listOfButtons[index] + QuickFixButton( + buttonText = button.text, + onClickAction = button.onClick, + buttonColor = if (button.applied) colorScheme.primary else colorScheme.surface, + textColor = if (button.applied) colorScheme.onPrimary else colorScheme.onBackground, + textStyle = poppinsTypography.labelSmall.copy(fontWeight = FontWeight.Medium), + height = screenHeight * 0.05f, + leadingIcon = button.leadingIcon, + trailingIcon = button.trailingIcon, + leadingIconTint = + if (button.applied) colorScheme.onPrimary else colorScheme.onBackground, + trailingIconTint = + if (button.applied) colorScheme.onPrimary else colorScheme.onBackground, + contentPadding = PaddingValues(vertical = 0.dp, horizontal = screenWidth * 0.02f), + modifier = Modifier.testTag("filter_button_${button.text}")) + Spacer(modifier = Modifier.width(screenHeight * 0.01f)) + } + } + } +} diff --git a/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/search/SearchOnBoarding.kt b/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/search/SearchOnBoarding.kt index 1afd2a4e..075351bd 100644 --- a/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/search/SearchOnBoarding.kt +++ b/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/search/SearchOnBoarding.kt @@ -1,27 +1,35 @@ package com.arygm.quickfix.ui.uiMode.appContentUI.userModeUI.search +import android.graphics.Bitmap import android.util.Log +import android.widget.Toast import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.outlined.Search +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -29,23 +37,30 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Popup -import com.arygm.quickfix.R +import com.arygm.quickfix.MainActivity import com.arygm.quickfix.model.account.AccountViewModel import com.arygm.quickfix.model.category.CategoryViewModel +import com.arygm.quickfix.model.locations.Location +import com.arygm.quickfix.model.offline.small.PreferencesViewModel import com.arygm.quickfix.model.profile.ProfileViewModel +import com.arygm.quickfix.model.profile.UserProfile import com.arygm.quickfix.model.profile.WorkerProfile -import com.arygm.quickfix.model.quickfix.QuickFixViewModel import com.arygm.quickfix.model.search.SearchViewModel +import com.arygm.quickfix.ui.elements.ChooseServiceTypeSheet +import com.arygm.quickfix.ui.elements.QuickFixAvailabilityBottomSheet import com.arygm.quickfix.ui.elements.QuickFixButton +import com.arygm.quickfix.ui.elements.QuickFixLocationFilterBottomSheet +import com.arygm.quickfix.ui.elements.QuickFixPriceRangeBottomSheet 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.appContentUI.userModeUI.navigation.UserTopLevelDestinations +import com.arygm.quickfix.utils.LocationHelper +import com.arygm.quickfix.utils.loadUserId @Composable fun SearchOnBoarding( @@ -53,32 +68,111 @@ fun SearchOnBoarding( navigationActionsRoot: NavigationActions, searchViewModel: SearchViewModel, accountViewModel: AccountViewModel, + preferencesViewModel: PreferencesViewModel, + userProfileViewModel: ProfileViewModel, categoryViewModel: CategoryViewModel, - quickFixViewModel: QuickFixViewModel, - workerViewModel: ProfileViewModel + onBookClick: (WorkerProfile, String, Bitmap, Bitmap) -> Unit, + workerViewModel: ProfileViewModel, + locationHelper: LocationHelper = LocationHelper(LocalContext.current, MainActivity()) ) { + val (uiState, setUiState) = remember { mutableStateOf(SearchUIState()) } + val context = LocalContext.current + val searchSubcategory by searchViewModel.searchSubcategory.collectAsState() + var locationFilterApplied by remember { mutableStateOf(false) } + var userProfile by remember { mutableStateOf(null) } + var lastAppliedMaxDist by remember { mutableIntStateOf(200) } val profiles by workerViewModel.profiles.collectAsState() var searchedWorkers by remember { mutableStateOf(profiles as List) } val focusManager = LocalFocusManager.current + var selectedLocation by remember { mutableStateOf(Location()) } val categories = categoryViewModel.categories.collectAsState().value - Log.d("SearchOnBoarding", "Categories: $categories") val itemCategories = remember { categories } + var uid by remember { mutableStateOf("Loading...") } + val expandedStates = remember { mutableStateListOf(*BooleanArray(itemCategories.size) { false }.toTypedArray()) } val listState = rememberLazyListState() + var selectedLocationIndex by remember { mutableStateOf(null) } var searchQuery by remember { mutableStateOf("") } - var isWindowVisible by remember { mutableStateOf(false) } - var selectedWorker by remember { mutableStateOf(null) } - // Variables for WorkerSlidingWindowContent - // These will be set when a worker profile is selected - var bannerImage by remember { mutableStateOf(R.drawable.moroccan_flag) } - var profilePicture by remember { mutableStateOf(R.drawable.placeholder_worker) } - var initialSaved by remember { mutableStateOf(false) } - var workerAddress by remember { mutableStateOf("") } + // Filtering logic + val filterState = rememberSearchFiltersState() + var baseLocation by remember { mutableStateOf(filterState.phoneLocation) } + var maxDistance by remember { mutableIntStateOf(0) } + + LaunchedEffect(Unit) { + if (locationHelper.checkPermissions()) { + locationHelper.getCurrentLocation { location -> + if (location != null) { + val userLoc = Location(location.latitude, location.longitude, "Phone Location") + filterState.phoneLocation = userLoc + } else { + Toast.makeText(context, "Unable to fetch location", Toast.LENGTH_SHORT).show() + } + } + } else { + Toast.makeText(context, "Enable Location In Settings", Toast.LENGTH_SHORT).show() + } + uid = loadUserId(preferencesViewModel) + userProfileViewModel.fetchUserProfile(uid) { profile -> userProfile = profile as UserProfile } + } + fun updateFilteredProfiles() { + searchedWorkers = filterState.reapplyFilters(profiles as List, searchViewModel) + } + + // Build filter buttons + val listOfButtons = + filterState.getFilterButtons( + workerProfiles = profiles as List, + filteredProfiles = searchedWorkers, + searchViewModel = searchViewModel, + onProfilesUpdated = { updated -> searchedWorkers = updated }, + onShowAvailabilityBottomSheet = { + setUiState(uiState.copy(showAvailabilityBottomSheet = true)) + }, + onShowServicesBottomSheet = { setUiState(uiState.copy(showServicesBottomSheet = true)) }, + onShowPriceRangeBottomSheet = { + setUiState(uiState.copy(showPriceRangeBottomSheet = true)) + }, + onShowLocationBottomSheet = { setUiState(uiState.copy(showLocationBottomSheet = true)) }) + val profileImagesMap by remember { mutableStateOf(mutableMapOf()) } + val bannerImagesMap by remember { mutableStateOf(mutableMapOf()) } + var loading by remember { mutableStateOf(true) } + // Tracks if data is loading + LaunchedEffect(profiles) { + if (profiles.isNotEmpty()) { + searchedWorkers.forEach { profile -> + // Fetch profile images + workerViewModel.fetchProfileImageAsBitmap( + profile.uid, + onSuccess = { bitmap -> + profileImagesMap[profile.uid] = bitmap + checkIfLoadingComplete( + profiles as List, profileImagesMap, bannerImagesMap) { + loading = false + } + }, + onFailure = { Log.e("ProfileResults", "Failed to fetch profile image") }) + + // Fetch banner images + workerViewModel.fetchBannerImageAsBitmap( + profile.uid, + onSuccess = { bitmap -> + bannerImagesMap[profile.uid] = bitmap + checkIfLoadingComplete( + profiles as List, profileImagesMap, bannerImagesMap) { + loading = false + } + }, + onFailure = { Log.e("ProfileResults", "Failed to fetch banner image") }) + } + } else { + loading = false // No profiles to load + } + } BoxWithConstraints { val widthRatio = maxWidth.value / 411f val heightRatio = maxHeight.value / 860f @@ -86,127 +180,216 @@ fun SearchOnBoarding( val screenHeight = maxHeight.value val screenWidth = maxWidth.value - // Use Scaffold for the layout structure Scaffold( containerColor = colorScheme.background, content = { padding -> - Column( - modifier = - Modifier.fillMaxWidth().padding(padding).padding(top = 40.dp * heightRatio), - horizontalAlignment = Alignment.CenterHorizontally) { - Row( - modifier = Modifier.fillMaxWidth().padding(bottom = 10.dp * heightRatio), - horizontalArrangement = Arrangement.Center) { - QuickFixTextFieldCustom( - modifier = Modifier.testTag("searchContent"), - showLeadingIcon = { true }, - leadingIcon = Icons.Outlined.Search, - showTrailingIcon = { searchQuery.isNotEmpty() }, - trailingIcon = { - Icon( - imageVector = Icons.Filled.Clear, - contentDescription = "Clear search query", - tint = colorScheme.onBackground, - ) - }, - placeHolderText = "Find your perfect fix with QuickFix", - value = searchQuery, - onValueChange = { - searchQuery = it - searchedWorkers = - searchViewModel.searchEngine(it, profiles as List) - }, - shape = CircleShape, - textStyle = poppinsTypography.bodyMedium, - textColor = colorScheme.onBackground, - placeHolderColor = colorScheme.onBackground, - leadIconColor = colorScheme.onBackground, - widthField = (screenWidth * 0.8).dp, - heightField = (screenHeight * 0.045).dp, - moveContentHorizontal = 10.dp * widthRatio, - moveContentBottom = 0.dp, - moveContentTop = 0.dp, - sizeIconGroup = 30.dp * sizeRatio, - spaceBetweenLeadIconText = 0.dp, - onClick = true, - ) - Spacer(modifier = Modifier.width(10.dp * widthRatio)) - QuickFixButton( - buttonText = "Cancel", - textColor = colorScheme.onBackground, - buttonColor = colorScheme.background, - buttonOpacity = 1f, - textStyle = poppinsTypography.labelSmall, - onClickAction = { - navigationActionsRoot.navigateTo(UserTopLevelDestinations.HOME) - }, - contentPadding = PaddingValues(0.dp), - ) + if (loading) { + // Display a loader + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator( + color = colorScheme.primary, modifier = Modifier.size(64.dp)) + } + } else { + Column( + modifier = + Modifier.fillMaxWidth().padding(padding).padding(top = 40.dp * heightRatio), + horizontalAlignment = Alignment.CenterHorizontally) { + Row( + modifier = Modifier.fillMaxWidth().padding(bottom = 0.dp * heightRatio), + horizontalArrangement = Arrangement.Center) { + QuickFixTextFieldCustom( + modifier = Modifier.testTag("searchContent"), + showLeadingIcon = { true }, + leadingIcon = Icons.Outlined.Search, + showTrailingIcon = { searchQuery.isNotEmpty() }, + trailingIcon = { + Icon( + imageVector = Icons.Filled.Clear, + contentDescription = "Clear search query", + tint = colorScheme.onBackground, + modifier = Modifier.testTag("clearSearchIcon"), + ) + }, + placeHolderText = "Find your perfect fix with QuickFix", + value = searchQuery, + onValueChange = { + searchQuery = it + searchViewModel.searchEngine(it, profiles as List) + }, + shape = CircleShape, + textStyle = poppinsTypography.bodyMedium, + textColor = colorScheme.onBackground, + placeHolderColor = colorScheme.onBackground, + leadIconColor = colorScheme.onBackground, + widthField = (screenWidth * 0.8).dp, + heightField = (screenHeight * 0.045).dp, + moveContentHorizontal = 10.dp * widthRatio, + moveContentBottom = 0.dp, + moveContentTop = 0.dp, + sizeIconGroup = 30.dp * sizeRatio, + spaceBetweenLeadIconText = 0.dp, + onClick = true, + ) + Spacer(modifier = Modifier.width(10.dp * widthRatio)) + QuickFixButton( + buttonText = "Cancel", + textColor = colorScheme.onBackground, + buttonColor = colorScheme.background, + buttonOpacity = 1f, + textStyle = poppinsTypography.labelSmall, + onClickAction = { + navigationActionsRoot.navigateTo(UserTopLevelDestinations.HOME) + }, + contentPadding = PaddingValues(0.dp), + ) + } + if (searchQuery.isEmpty()) { + // Show Categories + CategoryContent( + navigationActions = navigationActions, + searchViewModel = searchViewModel, + listState = listState, + expandedStates = expandedStates, + itemCategories = itemCategories, + widthRatio = widthRatio, + heightRatio = heightRatio, + ) + } else { + // Show Profiles + // Insert filter buttons here (only when searchQuery is not empty) + Column { + Row( + modifier = + Modifier.fillMaxWidth() + .padding( + top = screenHeight.dp * 0.02f, + bottom = screenHeight.dp * 0.01f) + .padding(horizontal = screenWidth.dp * 0.02f), + verticalAlignment = Alignment.CenterVertically, + ) { + FilterRow( + showFilterButtons = uiState.showFilterButtons, + toggleFilterButtons = { + setUiState( + uiState.copy(showFilterButtons = !uiState.showFilterButtons)) + }, + listOfButtons = listOfButtons, + modifier = Modifier.padding(bottom = screenHeight.dp * 0.01f), + screenWidth = screenWidth.dp, + screenHeight = screenHeight.dp) + } } - if (searchQuery.isEmpty()) { - // Show Categories - CategoryContent( - navigationActions = navigationActions, - searchViewModel = searchViewModel, - listState = listState, - expandedStates = expandedStates, - itemCategories = itemCategories, - widthRatio = widthRatio, - heightRatio = heightRatio, - ) - } else { - // Show Profiles + } ProfileResults( profiles = searchedWorkers, searchViewModel = searchViewModel, accountViewModel = accountViewModel, - listState = listState, - heightRatio = heightRatio, - onBookClick = { selectedProfile, locName -> - selectedWorker = selectedProfile as WorkerProfile - // Set up variables for WorkerSlidingWindowContent - bannerImage = R.drawable.moroccan_flag - profilePicture = R.drawable.placeholder_worker - initialSaved = false - workerAddress = locName - isWindowVisible = true + onBookClick = { selectedProfile, loc, profile, banner -> + onBookClick(selectedProfile, loc, profile, banner) }, - workerViewModel = workerViewModel) - - if (isWindowVisible) { - Popup( - onDismissRequest = { isWindowVisible = false }, - alignment = Alignment.Center) { - selectedWorker?.let { - QuickFixSlidingWindowWorker( - isVisible = isWindowVisible, - onDismiss = { isWindowVisible = false }, - bannerImage = bannerImage, - profilePicture = profilePicture, - initialSaved = initialSaved, - workerCategory = it.fieldOfWork, - workerAddress = workerAddress, - description = it.description, - includedServices = it.includedServices.map { it.name }, - addonServices = it.addOnServices.map { it.name }, - workerRating = it.reviews.map { it1 -> it1.rating }.average(), - tags = it.tags, - reviews = it.reviews.map { it.review }, - screenHeight = screenHeight.dp, - screenWidth = screenWidth.dp, - onContinueClick = { - quickFixViewModel.setSelectedWorkerProfile(it) - navigationActions.navigateTo(UserScreen.QUICKFIX_ONBOARDING) - }) - } - } - } + profileImagesMap = profileImagesMap, + bannerImagesMap = bannerImagesMap, + baseLocation = baseLocation, + screenHeight = screenHeight.dp) } - } + } }, modifier = Modifier.pointerInput(Unit) { detectTapGestures(onTap = { focusManager.clearFocus() }) }) + + QuickFixAvailabilityBottomSheet( + uiState.showAvailabilityBottomSheet, + onDismissRequest = { setUiState(uiState.copy(showAvailabilityBottomSheet = false)) }, + onOkClick = { days, hour, minute -> + filterState.selectedDays = days + filterState.selectedHour = hour + filterState.selectedMinute = minute + filterState.availabilityFilterApplied = true + updateFilteredProfiles() + }, + onClearClick = { + filterState.availabilityFilterApplied = false + filterState.selectedDays = emptyList() + filterState.selectedHour = 0 + filterState.selectedMinute = 0 + updateFilteredProfiles() + }, + clearEnabled = filterState.availabilityFilterApplied) + + searchSubcategory?.let { + ChooseServiceTypeSheet( + uiState.showServicesBottomSheet, + it.tags, + selectedServices = filterState.selectedServices, + onApplyClick = { services -> + filterState.selectedServices = services + filterState.servicesFilterApplied = true + updateFilteredProfiles() + }, + onDismissRequest = { setUiState(uiState.copy(showServicesBottomSheet = false)) }, + onClearClick = { + filterState.selectedServices = emptyList() + filterState.servicesFilterApplied = false + updateFilteredProfiles() + }, + clearEnabled = filterState.servicesFilterApplied) + } + + QuickFixPriceRangeBottomSheet( + uiState.showPriceRangeBottomSheet, + onApplyClick = { start, end -> + filterState.selectedPriceStart = start + filterState.selectedPriceEnd = end + filterState.priceFilterApplied = true + updateFilteredProfiles() + }, + onDismissRequest = { setUiState(uiState.copy(showPriceRangeBottomSheet = false)) }, + onClearClick = { + filterState.selectedPriceStart = 0 + filterState.selectedPriceEnd = 0 + filterState.priceFilterApplied = false + updateFilteredProfiles() + }, + clearEnabled = filterState.priceFilterApplied) + + userProfile?.let { + QuickFixLocationFilterBottomSheet( + uiState.showLocationBottomSheet, + profile = it, + phoneLocation = filterState.phoneLocation, + selectedLocationIndex = selectedLocationIndex, + onApplyClick = { location, max -> + selectedLocation = location + lastAppliedMaxDist = max + baseLocation = location + maxDistance = max + selectedLocationIndex = it.locations.indexOf(location) + 1 + + if (location == Location(0.0, 0.0, "Default")) { + Toast.makeText(context, "Enable Location In Settings", Toast.LENGTH_SHORT).show() + } + if (locationFilterApplied) { + updateFilteredProfiles() + } else { + searchedWorkers = + searchViewModel.filterWorkersByDistance(searchedWorkers, location, max) + } + locationFilterApplied = true + }, + onDismissRequest = { setUiState(uiState.copy(showLocationBottomSheet = false)) }, + onClearClick = { + baseLocation = filterState.phoneLocation + lastAppliedMaxDist = 200 + selectedLocation = Location() + maxDistance = 0 + selectedLocationIndex = null + locationFilterApplied = false + updateFilteredProfiles() + }, + clearEnabled = locationFilterApplied, + end = lastAppliedMaxDist) + } } } diff --git a/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/search/SearchWorkerResult.kt b/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/search/SearchWorkerResult.kt index 82ba28f1..ff08eec8 100644 --- a/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/search/SearchWorkerResult.kt +++ b/app/src/main/java/com/arygm/quickfix/ui/uiMode/appContentUI/userModeUI/search/SearchWorkerResult.kt @@ -1,59 +1,29 @@ package com.arygm.quickfix.ui.uiMode.appContentUI.userModeUI.search -import android.annotation.SuppressLint import android.graphics.Bitmap import android.util.Log import android.widget.Toast -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.itemsIndexed -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.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.Bookmark -import androidx.compose.material.icons.filled.CalendarMonth -import androidx.compose.material.icons.filled.Clear -import androidx.compose.material.icons.filled.Handyman -import androidx.compose.material.icons.filled.KeyboardArrowDown -import androidx.compose.material.icons.filled.LocationSearching -import androidx.compose.material.icons.filled.MonetizationOn import androidx.compose.material.icons.filled.Search -import androidx.compose.material.icons.filled.Tune -import androidx.compose.material.icons.filled.Warning -import androidx.compose.material.icons.filled.WorkspacePremium -import androidx.compose.material.icons.outlined.BookmarkBorder import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.Scaffold @@ -63,27 +33,20 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.graphics.painter.BitmapPainter -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.layout.ContentScale 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 androidx.compose.ui.window.Popup -import androidx.compose.ui.window.PopupProperties import com.arygm.quickfix.MainActivity -import com.arygm.quickfix.model.account.Account import com.arygm.quickfix.model.account.AccountViewModel +import com.arygm.quickfix.model.locations.Location import com.arygm.quickfix.model.offline.small.PreferencesViewModel import com.arygm.quickfix.model.profile.ProfileViewModel import com.arygm.quickfix.model.profile.UserProfile @@ -92,28 +55,15 @@ import com.arygm.quickfix.model.quickfix.QuickFixViewModel import com.arygm.quickfix.model.search.SearchViewModel import com.arygm.quickfix.ui.elements.ChooseServiceTypeSheet import com.arygm.quickfix.ui.elements.QuickFixAvailabilityBottomSheet -import com.arygm.quickfix.ui.elements.QuickFixButton import com.arygm.quickfix.ui.elements.QuickFixLocationFilterBottomSheet import com.arygm.quickfix.ui.elements.QuickFixPriceRangeBottomSheet -import com.arygm.quickfix.ui.elements.QuickFixSlidingWindow -import com.arygm.quickfix.ui.elements.RatingBar +import com.arygm.quickfix.ui.elements.defaultBitmap 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.utils.GeocoderWrapper import com.arygm.quickfix.utils.LocationHelper import com.arygm.quickfix.utils.loadUserId -import java.time.LocalDate -data class SearchFilterButtons( - val onClick: () -> Unit, - val text: String, - val leadingIcon: ImageVector? = null, - val trailingIcon: ImageVector? = null, - val applied: Boolean = false -) - -@SuppressLint("SuspiciousIndentation") @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @Composable fun SearchWorkerResult( @@ -121,25 +71,25 @@ fun SearchWorkerResult( searchViewModel: SearchViewModel, accountViewModel: AccountViewModel, userProfileViewModel: ProfileViewModel, - preferencesViewModel: PreferencesViewModel, quickFixViewModel: QuickFixViewModel, - geocoderWrapper: GeocoderWrapper = GeocoderWrapper(LocalContext.current), + preferencesViewModel: PreferencesViewModel, workerViewModel: ProfileViewModel, - locationHelper: LocationHelper = LocationHelper(LocalContext.current, MainActivity()) ) { - fun getCityNameFromCoordinates(latitude: Double, longitude: Double): String? { - val addresses = geocoderWrapper.getFromLocation(latitude, longitude, 1) - return addresses?.firstOrNull()?.locality - ?: addresses?.firstOrNull()?.subAdminArea - ?: addresses?.firstOrNull()?.adminArea - } - var phoneLocation by remember { - mutableStateOf(com.arygm.quickfix.model.locations.Location(0.0, 0.0, "Default")) - } - var baseLocation by remember { mutableStateOf(phoneLocation) } + val (uiState, setUiState) = remember { mutableStateOf(SearchUIState()) } + var isWindowVisible by remember { mutableStateOf(false) } val context = LocalContext.current + val locationHelper = LocationHelper(context, MainActivity()) + var selectedWorkerProfile by remember { mutableStateOf(WorkerProfile()) } + val filterState = rememberSearchFiltersState() + + var profilePicture by remember { mutableStateOf(defaultBitmap) } + var bannerPicture by remember { mutableStateOf(defaultBitmap) } + var baseLocation by remember { mutableStateOf(filterState.phoneLocation) } var userProfile by remember { mutableStateOf(null) } var uid by remember { mutableStateOf("Loading...") } + val searchSubcategory by searchViewModel.searchSubcategory.collectAsState() + + // Fetch user and set base location var loading by remember { mutableStateOf(true) } // Tracks if data is loading @@ -147,10 +97,9 @@ fun SearchWorkerResult( if (locationHelper.checkPermissions()) { locationHelper.getCurrentLocation { location -> if (location != null) { - phoneLocation = - com.arygm.quickfix.model.locations.Location( - location.latitude, location.longitude, "Phone Location") - baseLocation = phoneLocation + val userLoc = Location(location.latitude, location.longitude, "Phone Location") + filterState.phoneLocation = userLoc + baseLocation = userLoc } else { Toast.makeText(context, "Unable to fetch location", Toast.LENGTH_SHORT).show() } @@ -162,179 +111,40 @@ fun SearchWorkerResult( userProfileViewModel.fetchUserProfile(uid) { profile -> userProfile = profile as UserProfile } } - var selectedWorker by remember { mutableStateOf(WorkerProfile()) } - var selectedCityName by remember { mutableStateOf(null) } - var showFilterButtons by remember { mutableStateOf(false) } - var showAvailabilityBottomSheet by remember { mutableStateOf(false) } - var showServicesBottomSheet by remember { mutableStateOf(false) } - var showPriceRangeBottomSheet by remember { mutableStateOf(false) } - var showLocationBottomSheet by remember { mutableStateOf(false) } val workerProfiles by searchViewModel.subCategoryWorkerProfiles.collectAsState() - Log.d("Chill guy", workerProfiles.size.toString()) var filteredWorkerProfiles by remember { mutableStateOf(workerProfiles) } - val searchSubcategory by searchViewModel.searchSubcategory.collectAsState() val searchCategory by searchViewModel.searchCategory.collectAsState() - var availabilityFilterApplied by remember { mutableStateOf(false) } - var servicesFilterApplied by remember { mutableStateOf(false) } - var priceFilterApplied by remember { mutableStateOf(false) } var locationFilterApplied by remember { mutableStateOf(false) } - var ratingFilterApplied by remember { mutableStateOf(false) } - var emergencyFilterApplied by remember { mutableStateOf(false) } - - var selectedDays by remember { mutableStateOf(emptyList()) } - var selectedHour by remember { mutableStateOf(0) } - var selectedMinute by remember { mutableStateOf(0) } - var selectedServices by remember { mutableStateOf(emptyList()) } - var selectedPriceStart by remember { mutableStateOf(0) } - var selectedPriceEnd by remember { mutableStateOf(0) } - var selectedLocation by remember { mutableStateOf(com.arygm.quickfix.model.locations.Location()) } - var maxDistance by remember { mutableStateOf(0) } + var selectedLocation by remember { mutableStateOf(Location()) } + var maxDistance by remember { mutableIntStateOf(0) } var selectedLocationIndex by remember { mutableStateOf(null) } + var initialSaved by remember { mutableStateOf(false) } + var selectedCityName by remember { mutableStateOf(null) } + var workerAddress by remember { mutableStateOf("") } - var lastAppliedPriceStart by remember { mutableStateOf(500) } - var lastAppliedPriceEnd by remember { mutableStateOf(2500) } - var lastAppliedMaxDist by remember { mutableStateOf(200) } - - fun reapplyFilters() { - Log.d("Chill guy", "entered") - var updatedProfiles = workerProfiles - - if (availabilityFilterApplied) { - updatedProfiles = - searchViewModel.filterWorkersByAvailability( - updatedProfiles, selectedDays, selectedHour, selectedMinute) - } - - if (servicesFilterApplied) { - updatedProfiles = searchViewModel.filterWorkersByServices(updatedProfiles, selectedServices) - } - - if (priceFilterApplied) { - updatedProfiles = - searchViewModel.filterWorkersByPriceRange( - updatedProfiles, selectedPriceStart, selectedPriceEnd) - } - - if (locationFilterApplied) { - updatedProfiles = - searchViewModel.filterWorkersByDistance(updatedProfiles, selectedLocation, maxDistance) - } - - if (ratingFilterApplied) { - updatedProfiles = searchViewModel.sortWorkersByRating(updatedProfiles) - } + var lastAppliedMaxDist by remember { mutableIntStateOf(200) } - if (emergencyFilterApplied) { - updatedProfiles = searchViewModel.emergencyFilter(updatedProfiles, baseLocation) - } - - Log.d("Chill guy", updatedProfiles.size.toString()) - filteredWorkerProfiles = updatedProfiles + val listState = rememberLazyListState() + fun updateFilteredProfiles() { + filteredWorkerProfiles = filterState.reapplyFilters(workerProfiles, searchViewModel) } val listOfButtons = - listOf( - SearchFilterButtons( - onClick = { - filteredWorkerProfiles = workerProfiles - availabilityFilterApplied = false - priceFilterApplied = false - locationFilterApplied = false - ratingFilterApplied = false - servicesFilterApplied = false - emergencyFilterApplied = false - lastAppliedMaxDist = 200 - lastAppliedPriceStart = 500 - lastAppliedPriceEnd = 2500 - selectedLocationIndex = null - selectedServices = emptyList() - baseLocation = phoneLocation - }, - text = "Clear", - leadingIcon = Icons.Default.Clear, - applied = false), - SearchFilterButtons( - onClick = { showLocationBottomSheet = true }, - text = "Location", - leadingIcon = Icons.Default.LocationSearching, - trailingIcon = Icons.Default.KeyboardArrowDown, - applied = locationFilterApplied), - SearchFilterButtons( - onClick = { showServicesBottomSheet = true }, - text = "Service Type", - leadingIcon = Icons.Default.Handyman, - trailingIcon = Icons.Default.KeyboardArrowDown, - applied = servicesFilterApplied), - SearchFilterButtons( - onClick = { showAvailabilityBottomSheet = true }, - text = "Availability", - leadingIcon = Icons.Default.CalendarMonth, - trailingIcon = Icons.Default.KeyboardArrowDown, - applied = availabilityFilterApplied), - SearchFilterButtons( - onClick = { - if (ratingFilterApplied) { - ratingFilterApplied = false - reapplyFilters() - } else { - filteredWorkerProfiles = - searchViewModel.sortWorkersByRating(filteredWorkerProfiles) - ratingFilterApplied = true - } - }, - text = "Highest Rating", - leadingIcon = Icons.Default.WorkspacePremium, - trailingIcon = if (ratingFilterApplied) Icons.Default.Clear else null, - applied = ratingFilterApplied), - SearchFilterButtons( - onClick = { showPriceRangeBottomSheet = true }, - text = "Price Range", - leadingIcon = Icons.Default.MonetizationOn, - trailingIcon = Icons.Default.KeyboardArrowDown, - applied = priceFilterApplied), - SearchFilterButtons( - onClick = { - if (emergencyFilterApplied) { - emergencyFilterApplied = false - reapplyFilters() - } else { - lastAppliedMaxDist = 200 - lastAppliedPriceStart = 500 - lastAppliedPriceEnd = 2500 - selectedLocationIndex = null - selectedServices = emptyList() - availabilityFilterApplied = false - priceFilterApplied = false - locationFilterApplied = false - ratingFilterApplied = false - servicesFilterApplied = false - baseLocation = phoneLocation - filteredWorkerProfiles = workerProfiles - filteredWorkerProfiles = - searchViewModel.emergencyFilter(filteredWorkerProfiles, baseLocation) - emergencyFilterApplied = true - } - }, - text = "Emergency", - leadingIcon = Icons.Default.Warning, - trailingIcon = if (emergencyFilterApplied) Icons.Default.Clear else null, - applied = emergencyFilterApplied)) - - // ==========================================================================// - // ============ TODO: REMOVE NO-DATA WHEN BACKEND IS IMPLEMENTED ============// - // ==========================================================================// - - var bannerPicture by remember { mutableStateOf(null) } - var profilePicture by remember { mutableStateOf(null) } - - // ==========================================================================// - // ==========================================================================// - // ==========================================================================// - - var isWindowVisible by remember { mutableStateOf(false) } - var saved by remember { mutableStateOf(false) } - + filterState.getFilterButtons( + workerProfiles = workerProfiles, + filteredProfiles = filteredWorkerProfiles, + searchViewModel = searchViewModel, + onProfilesUpdated = { updated -> filteredWorkerProfiles = updated }, + onShowAvailabilityBottomSheet = { + setUiState(uiState.copy(showAvailabilityBottomSheet = true)) + }, + onShowServicesBottomSheet = { setUiState(uiState.copy(showServicesBottomSheet = true)) }, + onShowPriceRangeBottomSheet = { + setUiState(uiState.copy(showPriceRangeBottomSheet = true)) + }, + onShowLocationBottomSheet = { setUiState(uiState.copy(showLocationBottomSheet = true)) }) + // Wrap everything in a Box to allow overlay val profileImagesMap by remember { mutableStateOf(mutableMapOf()) } val bannerImagesMap by remember { mutableStateOf(mutableMapOf()) } @@ -368,13 +178,10 @@ fun SearchWorkerResult( loading = false // No profiles to load } } - - // Wrap everything in a Box to allow overlay BoxWithConstraints(modifier = Modifier.fillMaxSize()) { val screenHeight = maxHeight val screenWidth = maxWidth - Log.d("Screen Dimensions", "Height: $screenHeight, Width: $screenWidth") - // Scaffold containing the main UI elements + Scaffold( topBar = { CenterAlignedTopAppBar( @@ -413,7 +220,7 @@ fun SearchWorkerResult( modifier = Modifier.fillMaxWidth().padding(paddingValues), horizontalAlignment = Alignment.CenterHorizontally) { Column( - modifier = Modifier.fillMaxWidth().background(colorScheme.surface), + modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Top) { Text( @@ -431,649 +238,162 @@ fun SearchWorkerResult( color = colorScheme.onSurface, textAlign = TextAlign.Center, ) - Row( - modifier = - Modifier.fillMaxWidth() - .padding( - top = screenHeight * 0.02f, bottom = screenHeight * 0.01f) - .padding(horizontal = screenWidth * 0.02f) - .wrapContentHeight() - .testTag("filter_buttons_row") - .background(colorScheme.surface), - verticalAlignment = Alignment.CenterVertically, - ) { - // Tune Icon - fixed, non-scrollable - IconButton( - onClick = { showFilterButtons = !showFilterButtons }, - modifier = - Modifier.padding(bottom = screenHeight * 0.01f) - .testTag("tuneButton"), - content = { - Icon( - imageVector = Icons.Default.Tune, - contentDescription = "Filter", - tint = - if (showFilterButtons) colorScheme.onPrimary - else colorScheme.onBackground, - ) - }, - colors = - IconButtonDefaults.iconButtonColors( - containerColor = - if (showFilterButtons) colorScheme.primary - else colorScheme.surface), - ) - - Spacer(modifier = Modifier.width(10.dp)) - - AnimatedVisibility(visible = showFilterButtons) { - LazyRow( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.testTag("lazy_filter_row")) { - items(listOfButtons.size) { index -> - QuickFixButton( - buttonText = listOfButtons[index].text, - onClickAction = listOfButtons[index].onClick, - buttonColor = - if (listOfButtons[index].applied) colorScheme.primary - else colorScheme.surface, - textColor = - if (listOfButtons[index].applied) colorScheme.onPrimary - else colorScheme.onBackground, - textStyle = - poppinsTypography.labelSmall.copy( - fontWeight = FontWeight.Medium), - height = screenHeight * 0.05f, - leadingIcon = listOfButtons[index].leadingIcon, - trailingIcon = listOfButtons[index].trailingIcon, - leadingIconTint = - if (listOfButtons[index].applied) colorScheme.onPrimary - else colorScheme.onBackground, - trailingIconTint = - if (listOfButtons[index].applied) colorScheme.onPrimary - else colorScheme.onBackground, - contentPadding = - PaddingValues( - vertical = 0.dp, horizontal = screenWidth * 0.02f), - modifier = - Modifier.testTag( - "filter_button_${listOfButtons[index].text}")) - Spacer(modifier = Modifier.width(screenHeight * 0.01f)) - } - } - } - } - Text( - modifier = - Modifier.align(Alignment.Start) - .padding(start = screenWidth * 0.03f), - text = searchSubcategory?.scale?.longScale ?: "Unknown", - color = colorScheme.error, - style = poppinsTypography.labelSmall, - fontWeight = FontWeight.Medium, - fontSize = 10.sp) } - LazyColumn(modifier = Modifier.fillMaxWidth().testTag("worker_profiles_list")) { - items(filteredWorkerProfiles.size) { index -> - val profile = filteredWorkerProfiles[index] - var account by remember { mutableStateOf(null) } - var distance by remember { mutableStateOf(null) } - var cityName by remember { mutableStateOf(null) } - val profileImage = profileImagesMap[profile.uid] - val bannerImage = bannerImagesMap[profile.uid] - distance = - profile.location - ?.let { workerLocation -> - searchViewModel.calculateDistance( - workerLocation.latitude, - workerLocation.longitude, - baseLocation.latitude, - baseLocation.longitude) - } - ?.toInt() - - LaunchedEffect(profile.uid) { - accountViewModel.fetchUserAccount(profile.uid) { fetchedAccount: Account? -> - account = fetchedAccount - } - } - - account?.let { acc -> - val locationName = - if (profile.location?.name.isNullOrEmpty()) "Unknown" - else profile.location?.name - - locationName?.let { - cityName = - profile.location?.let { it1 -> - getCityNameFromCoordinates(it1.latitude, profile.location.longitude) - } - Log.d("Chill guy", cityName.toString()) - cityName?.let { it1 -> - profileImage?.let { it2 -> - SearchWorkerProfileResult( - modifier = Modifier.testTag("worker_profile_result$index"), - profileImage = it2, - name = profile.displayName, - category = profile.fieldOfWork, - rating = - profile.reviews.map { review -> review.rating }.average(), - reviewCount = profile.reviews.size, - location = it1, - price = profile.price.toString(), - onBookClick = { - selectedWorker = profile - selectedCityName = cityName - isWindowVisible = true - profilePicture = it2 - bannerPicture = bannerImage!! - }, - distance = distance, - ) - } - } - } - } - Spacer(modifier = Modifier.height(screenHeight * 0.004f)) - } + Row( + modifier = + Modifier.fillMaxWidth() + .padding(top = screenHeight * 0.02f, bottom = screenHeight * 0.01f) + .padding(horizontal = screenWidth * 0.02f) + .wrapContentHeight() + .background(colorScheme.surface), + verticalAlignment = Alignment.CenterVertically, + ) { + FilterRow( + showFilterButtons = uiState.showFilterButtons, + toggleFilterButtons = { + setUiState(uiState.copy(showFilterButtons = !uiState.showFilterButtons)) + }, + listOfButtons = listOfButtons, + modifier = Modifier.padding(bottom = screenHeight * 0.01f), + screenWidth = screenWidth, + screenHeight = screenHeight) } + ProfileResults( + profiles = filteredWorkerProfiles, + modifier = Modifier.fillMaxWidth().weight(1f), + searchViewModel = searchViewModel, + accountViewModel = accountViewModel, + onBookClick = { selectedProfile, locName, profile, banner -> + bannerPicture = banner + profilePicture = profile + initialSaved = false + selectedCityName = locName + isWindowVisible = true + selectedWorkerProfile = selectedProfile + }, + profileImagesMap = profileImagesMap, + bannerImagesMap = bannerImagesMap, + baseLocation = baseLocation, + screenHeight = screenHeight, + ) } } } QuickFixAvailabilityBottomSheet( - showAvailabilityBottomSheet, - onDismissRequest = { showAvailabilityBottomSheet = false }, + uiState.showAvailabilityBottomSheet, + onDismissRequest = { setUiState(uiState.copy(showAvailabilityBottomSheet = false)) }, onOkClick = { days, hour, minute -> - selectedDays = days - selectedHour = hour - selectedMinute = minute - if (availabilityFilterApplied) { - reapplyFilters() - } else { - filteredWorkerProfiles = - searchViewModel.filterWorkersByAvailability( - filteredWorkerProfiles, days, hour, minute) - } - availabilityFilterApplied = true + filterState.selectedDays = days + filterState.selectedHour = hour + filterState.selectedMinute = minute + filterState.availabilityFilterApplied = true + updateFilteredProfiles() }, onClearClick = { - availabilityFilterApplied = false - selectedDays = emptyList() - selectedHour = 0 - selectedMinute = 0 - reapplyFilters() + filterState.availabilityFilterApplied = false + filterState.selectedDays = emptyList() + filterState.selectedHour = 0 + filterState.selectedMinute = 0 + updateFilteredProfiles() }, - clearEnabled = availabilityFilterApplied) + clearEnabled = filterState.availabilityFilterApplied) searchSubcategory?.let { ChooseServiceTypeSheet( - showServicesBottomSheet, + uiState.showServicesBottomSheet, it.tags, - selectedServices = selectedServices, + selectedServices = filterState.selectedServices, onApplyClick = { services -> - selectedServices = services - if (servicesFilterApplied) { - reapplyFilters() - } else { - filteredWorkerProfiles = - searchViewModel.filterWorkersByServices(filteredWorkerProfiles, selectedServices) - } - servicesFilterApplied = true + filterState.selectedServices = services + filterState.servicesFilterApplied = true + updateFilteredProfiles() }, - onDismissRequest = { showServicesBottomSheet = false }, + onDismissRequest = { setUiState(uiState.copy(showServicesBottomSheet = false)) }, onClearClick = { - selectedServices = emptyList() - servicesFilterApplied = false - reapplyFilters() + filterState.selectedServices = emptyList() + filterState.servicesFilterApplied = false + updateFilteredProfiles() }, - clearEnabled = servicesFilterApplied) + clearEnabled = filterState.servicesFilterApplied) } QuickFixPriceRangeBottomSheet( - showPriceRangeBottomSheet, + uiState.showPriceRangeBottomSheet, onApplyClick = { start, end -> - selectedPriceStart = start - selectedPriceEnd = end - lastAppliedPriceStart = start - lastAppliedPriceEnd = end - if (priceFilterApplied) { - reapplyFilters() - } else { - filteredWorkerProfiles = - searchViewModel.filterWorkersByPriceRange(filteredWorkerProfiles, start, end) - } - priceFilterApplied = true + filterState.selectedPriceStart = start + filterState.selectedPriceEnd = end + filterState.priceFilterApplied = true + updateFilteredProfiles() }, - onDismissRequest = { showPriceRangeBottomSheet = false }, + onDismissRequest = { setUiState(uiState.copy(showPriceRangeBottomSheet = false)) }, onClearClick = { - selectedPriceStart = 0 - selectedPriceEnd = 0 - lastAppliedPriceStart = 500 - lastAppliedPriceEnd = 2500 - priceFilterApplied = false - reapplyFilters() + filterState.selectedPriceStart = 0 + filterState.selectedPriceEnd = 0 + filterState.priceFilterApplied = false + updateFilteredProfiles() }, - clearEnabled = priceFilterApplied, - start = lastAppliedPriceStart, - end = lastAppliedPriceEnd) + clearEnabled = filterState.priceFilterApplied) userProfile?.let { QuickFixLocationFilterBottomSheet( - showLocationBottomSheet, + uiState.showLocationBottomSheet, profile = it, - phoneLocation = phoneLocation, + phoneLocation = filterState.phoneLocation, selectedLocationIndex = selectedLocationIndex, onApplyClick = { location, max -> selectedLocation = location lastAppliedMaxDist = max baseLocation = location maxDistance = max - selectedLocationIndex = userProfile!!.locations.indexOf(location) + 1 + selectedLocationIndex = it.locations.indexOf(location) + 1 - if (location == com.arygm.quickfix.model.locations.Location(0.0, 0.0, "Default")) { + if (location == Location(0.0, 0.0, "Default")) { Toast.makeText(context, "Enable Location In Settings", Toast.LENGTH_SHORT).show() } if (locationFilterApplied) { - reapplyFilters() + updateFilteredProfiles() } else { filteredWorkerProfiles = searchViewModel.filterWorkersByDistance(filteredWorkerProfiles, location, max) } locationFilterApplied = true }, - onDismissRequest = { showLocationBottomSheet = false }, + onDismissRequest = { setUiState(uiState.copy(showLocationBottomSheet = false)) }, onClearClick = { - baseLocation = phoneLocation + baseLocation = filterState.phoneLocation lastAppliedMaxDist = 200 - selectedLocation = com.arygm.quickfix.model.locations.Location() + selectedLocation = Location() maxDistance = 0 selectedLocationIndex = null locationFilterApplied = false - reapplyFilters() + updateFilteredProfiles() }, clearEnabled = locationFilterApplied, end = lastAppliedMaxDist) } - - if (isWindowVisible) { - Log.d("saved lists", userProfile?.savedList.toString() + selectedWorker.uid) - Popup( - onDismissRequest = { isWindowVisible = false }, - properties = PopupProperties(focusable = true)) { - QuickFixSlidingWindow( - isVisible = isWindowVisible, onDismiss = { isWindowVisible = false }) { - // Content of the sliding window - Column( - modifier = - Modifier.clip(RoundedCornerShape(topStart = 25f, bottomStart = 25f)) - .fillMaxWidth() - .background(colorScheme.background) - .testTag("sliding_window_content")) { - - // Top Bar - Box( - modifier = - Modifier.fillMaxWidth() - .height( - screenHeight * - 0.23f) // Adjusted height to accommodate profile picture - // overlap - .testTag("sliding_window_top_bar")) { - // Banner Image - Image( - painter = BitmapPainter(bannerPicture!!.asImageBitmap()), - contentDescription = "Banner", - modifier = - Modifier.fillMaxWidth() - .height(screenHeight * 0.2f) - .testTag("sliding_window_banner_image"), - contentScale = ContentScale.Crop) - - QuickFixButton( - buttonText = - if (userProfile?.savedList?.contains(selectedWorker.uid) == - true) - "saved" - else "save", - onClickAction = { - val profile = userProfile - if (profile == null) { - Log.e( - "SlidingWindow", - "Cannot update saved list: userProfile is null") - return@QuickFixButton - } - - val isSaved = profile.savedList.contains(selectedWorker.uid) - val updatedList = - if (isSaved) profile.savedList - selectedWorker.uid - else profile.savedList + selectedWorker.uid - val newProfile = profile.copy(savedList = updatedList) - - userProfileViewModel.updateProfile( - newProfile, - onSuccess = { - userProfile = newProfile - val message = - if (isSaved) "Removed from saved list" else "Saved" - Toast.makeText(context, message, Toast.LENGTH_SHORT) - .show() - }, - onFailure = { - Log.e("SlidingWindow", "Failed to update profile") - }) - }, - buttonColor = colorScheme.surface, - textColor = colorScheme.onBackground, - textStyle = MaterialTheme.typography.labelMedium, - contentPadding = PaddingValues(horizontal = screenWidth * 0.01f), - modifier = - Modifier.align(Alignment.BottomEnd) - .width(screenWidth * 0.25f) - .offset(x = -(screenWidth * 0.04f)) - .testTag("sliding_window_save_button"), - leadingIcon = - if (userProfile?.savedList?.contains(selectedWorker.uid) == - true) - Icons.Filled.Bookmark - else Icons.Outlined.BookmarkBorder) - - // Profile picture overlapping the banner image - Image( - painter = BitmapPainter(profilePicture!!.asImageBitmap()), - contentDescription = "Profile Picture", - modifier = - Modifier.size(screenHeight * 0.1f) - .align(Alignment.BottomStart) - .offset(x = screenWidth * 0.04f) - .clip(CircleShape) - .testTag("sliding_window_profile_picture"), - // Negative offset to position correctly - contentScale = ContentScale.Crop) - } - - // Worker Field and Address under the profile picture - Column( - modifier = - Modifier.fillMaxWidth() - .padding(horizontal = screenWidth * 0.04f) - .testTag("sliding_window_worker_additional_info")) { - Text( - text = selectedWorker.displayName, - style = MaterialTheme.typography.headlineLarge, - color = colorScheme.onBackground) - selectedCityName?.let { - Text( - text = it, - style = MaterialTheme.typography.headlineSmall, - color = colorScheme.onBackground, - modifier = Modifier.testTag("sliding_window_worker_address")) - } - } - - // Main content should be scrollable - Column( - modifier = - Modifier.fillMaxWidth() - .verticalScroll(rememberScrollState()) - .background(colorScheme.surface) - .testTag("sliding_window_scrollable_content")) { - Spacer(modifier = Modifier.height(screenHeight * 0.02f)) - - Text( - text = selectedWorker.fieldOfWork, - style = - MaterialTheme.typography.headlineLarge.copy(fontSize = 28.sp), - color = colorScheme.onBackground, - modifier = - Modifier.padding(horizontal = screenWidth * 0.04f) - .testTag("sliding_window_worker_category")) - - Spacer(modifier = Modifier.height(screenHeight * 0.02f)) - // Description with "Show more" functionality - var showFullDescription by remember { mutableStateOf(false) } - val descriptionText = - if (showFullDescription || - selectedWorker.description.length <= 100) { - selectedWorker.description - } else { - selectedWorker.description.take(100) + "..." - } - - Text( - text = descriptionText, - style = MaterialTheme.typography.bodySmall, - color = colorScheme.onSurface, - modifier = - Modifier.padding(horizontal = screenWidth * 0.04f) - .testTag("sliding_window_description")) - - if (selectedWorker.description.length > 100) { - Text( - text = if (showFullDescription) "Show less" else "Show more", - style = - MaterialTheme.typography.bodySmall.copy( - color = colorScheme.primary), - modifier = - Modifier.padding(horizontal = screenWidth * 0.04f) - .clickable { - showFullDescription = !showFullDescription - } - .testTag("sliding_window_description_show_more_button")) - } - - // Delimiter between description and services - Spacer(modifier = Modifier.height(screenHeight * 0.02f)) - - HorizontalDivider( - modifier = - Modifier.padding(horizontal = screenWidth * 0.04f) - .testTag("sliding_window_horizontal_divider_1"), - thickness = 1.dp, - color = colorScheme.onSurface.copy(alpha = 0.2f)) - Spacer(modifier = Modifier.height(screenHeight * 0.02f)) - - // Services Section - Row( - modifier = - Modifier.fillMaxWidth() - .padding(horizontal = screenWidth * 0.04f) - .testTag("sliding_window_services_row")) { - // Included Services - Column( - modifier = - Modifier.weight(1f) - .testTag( - "sliding_window_included_services_column")) { - Text( - text = "Included Services", - style = MaterialTheme.typography.headlineMedium, - color = colorScheme.onBackground) - Spacer(modifier = Modifier.height(screenHeight * 0.01f)) - selectedWorker.includedServices.forEach { service -> - val name = service.name - Text( - text = "• $name", - style = MaterialTheme.typography.bodySmall, - color = colorScheme.onSurface, - modifier = - Modifier.padding( - bottom = screenHeight * 0.005f)) - } - } - - Spacer(modifier = Modifier.width(screenWidth * 0.02f)) - - // Add-On Services - Column( - modifier = - Modifier.weight(1f) - .testTag("sliding_window_addon_services_column")) { - Text( - text = "Add-On Services", - style = MaterialTheme.typography.headlineMedium, - color = colorScheme.primary) - Spacer(modifier = Modifier.height(screenHeight * 0.01f)) - selectedWorker.addOnServices.forEach { service -> - val name = service.name - Text( - text = "• $name", - style = MaterialTheme.typography.bodySmall, - color = colorScheme.primary, - modifier = - Modifier.padding( - bottom = screenHeight * 0.005f)) - } - } - } - - Spacer(modifier = Modifier.height(screenHeight * 0.03f)) - - // Continue Button with Rate/HR - QuickFixButton( - buttonText = "Continue", - onClickAction = { - quickFixViewModel.setSelectedWorkerProfile(selectedWorker) - quickFixViewModel.resetCurrentQuickFix() - navigationActions.navigateTo(UserScreen.QUICKFIX_ONBOARDING) - }, - buttonColor = colorScheme.primary, - textColor = colorScheme.onPrimary, - textStyle = MaterialTheme.typography.labelMedium, - modifier = - Modifier.fillMaxWidth() - .padding(horizontal = screenWidth * 0.04f) - .testTag("sliding_window_continue_button")) - - Spacer(modifier = Modifier.height(screenHeight * 0.02f)) - - HorizontalDivider( - modifier = - Modifier.padding(horizontal = screenWidth * 0.04f) - .testTag("sliding_window_horizontal_divider_2"), - thickness = 1.dp, - color = colorScheme.onSurface.copy(alpha = 0.2f), - ) - Spacer(modifier = Modifier.height(screenHeight * 0.02f)) - - // Tags Section - Text( - text = "Tags", - style = MaterialTheme.typography.headlineMedium, - color = colorScheme.onBackground, - modifier = Modifier.padding(horizontal = screenWidth * 0.04f)) - Spacer(modifier = Modifier.height(screenHeight * 0.01f)) - - // Display tags using FlowRow for wrapping - FlowRow( - horizontalArrangement = Arrangement.spacedBy(screenWidth * 0.02f), - verticalArrangement = Arrangement.spacedBy(screenHeight * 0.01f), - modifier = - Modifier.fillMaxWidth() - .padding(horizontal = screenWidth * 0.04f) - .testTag("sliding_window_tags_flow_row"), - ) { - selectedWorker.tags.forEach { tag -> - Text( - text = tag, - color = colorScheme.primary, - style = MaterialTheme.typography.bodySmall, - modifier = - Modifier.border( - width = 1.dp, - color = colorScheme.primary, - shape = MaterialTheme.shapes.small) - .padding( - horizontal = screenWidth * 0.02f, - vertical = screenHeight * 0.005f)) - } - } - - Spacer(modifier = Modifier.height(screenHeight * 0.02f)) - - HorizontalDivider( - modifier = - Modifier.padding(horizontal = screenWidth * 0.04f) - .testTag("sliding_window_horizontal_divider_3"), - thickness = 1.dp, - color = colorScheme.onSurface.copy(alpha = 0.2f)) - Spacer(modifier = Modifier.height(screenHeight * 0.02f)) - - Text( - text = "Reviews", - style = MaterialTheme.typography.headlineMedium, - color = colorScheme.onBackground, - modifier = Modifier.padding(horizontal = screenWidth * 0.04f)) - Spacer(modifier = Modifier.height(screenHeight * 0.01f)) - - // Star Rating Row - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = - Modifier.padding(horizontal = screenWidth * 0.04f) - .testTag("sliding_window_star_rating_row")) { - RatingBar( - selectedWorker.rating.toFloat(), - modifier = - Modifier.height(screenHeight * 0.03f) - .testTag("starsRow")) - } - Spacer(modifier = Modifier.height(screenHeight * 0.01f)) - Spacer(modifier = Modifier.height(screenHeight * 0.01f)) - LazyRow( - modifier = - Modifier.fillMaxWidth() - .padding(horizontal = screenWidth * 0.04f) - .testTag("sliding_window_reviews_row")) { - itemsIndexed(selectedWorker.reviews) { index, review -> - var isExpanded by remember { mutableStateOf(false) } - val displayText = - if (isExpanded || review.review.length <= 100) { - review.review - } else { - review.review.take(100) + "..." - } - - Box( - modifier = - Modifier.padding(end = screenWidth * 0.02f) - .width(screenWidth * 0.6f) - .clip(RoundedCornerShape(25f)) - .background(colorScheme.background)) { - Column( - modifier = Modifier.padding(screenWidth * 0.02f)) { - Text( - text = displayText, - style = MaterialTheme.typography.bodySmall, - color = colorScheme.onSurface) - if (review.review.length > 100) { - Text( - text = - if (isExpanded) "See less" - else "See more", - style = - MaterialTheme.typography.bodySmall.copy( - color = colorScheme.primary), - modifier = - Modifier.clickable { - isExpanded = !isExpanded - } - .padding( - top = screenHeight * 0.01f)) - } - } - } - } - } - - Spacer(modifier = Modifier.height(screenHeight * 0.02f)) - } - } - } - } - } + QuickFixSlidingWindowWorker( + isVisible = isWindowVisible, + onDismiss = { isWindowVisible = false }, + screenHeight = maxHeight, + screenWidth = maxWidth, + onContinueClick = { + quickFixViewModel.setSelectedWorkerProfile(selectedWorkerProfile) + navigationActions.navigateTo(UserScreen.QUICKFIX_ONBOARDING) + }, + bannerImage = bannerPicture, + profilePicture = profilePicture, + initialSaved = initialSaved, + workerCategory = selectedWorkerProfile.fieldOfWork, + selectedCityName = selectedCityName, + description = selectedWorkerProfile.description, + includedServices = selectedWorkerProfile.includedServices.map { it.name }, + addonServices = selectedWorkerProfile.addOnServices.map { it.name }, + workerRating = selectedWorkerProfile.reviews.map { it1 -> it1.rating }.average(), + tags = selectedWorkerProfile.tags, + reviews = selectedWorkerProfile.reviews.map { it.review }, + ) } } diff --git a/end2end-data/storage_export/metadata/9d6ec3de-086d-4117-b022-0fc31782bb85.json b/end2end-data/storage_export/metadata/9d6ec3de-086d-4117-b022-0fc31782bb85.json deleted file mode 100644 index ac5edee2..00000000 --- a/end2end-data/storage_export/metadata/9d6ec3de-086d-4117-b022-0fc31782bb85.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "profiles/NEdD5q9bLZAO7Fys3q8qJVV6n490/worker/image_1734314288632.jpg", - "bucket": "quickfix-1fd34.appspot.com", - "metageneration": 1, - "generation": 1734314289229, - "contentType": "application/octet-stream", - "storageClass": "STANDARD", - "contentDisposition": "inline", - "downloadTokens": [ - "b077b20f-e6db-41fd-9aa1-220c5439e8b5" - ], - "etag": "At+NRZFnBIPMkMsOBy5jWAJR2TY", - "customMetadata": {}, - "timeCreated": "2024-12-16T01:58:09.229Z", - "updated": "2024-12-16T01:58:09.229Z", - "size": 2455, - "md5Hash": "VXBlJu9vq0cVBuIhZjQNMg==", - "crc32c": "1741704250" -} \ No newline at end of file