From bdd8cb4b11e58bed77db991704792abe34688f46 Mon Sep 17 00:00:00 2001 From: Jens Reinemann Date: Mon, 18 May 2026 10:01:14 +0200 Subject: [PATCH] feat(#106): category tap on dashboard navigates to inventory with filter - Change Screen.ItemList from data object to data class with optional categoryId - DashboardScreen: make CategoryCard clickable with onCategoryClick callback - ItemListViewModel: read initial categoryId from SavedStateHandle - BollwerkNavGraph: wire category click to navigate with categoryId - Add test for initial category filter from navigation args --- .../app/ui/dashboard/DashboardScreen.kt | 10 ++++-- .../bollwerk/app/ui/item/ItemListViewModel.kt | 5 ++- .../app/ui/navigation/BollwerkNavGraph.kt | 6 +++- .../de/bollwerk/app/ui/navigation/Screen.kt | 2 +- .../app/ui/navigation/TopLevelDestination.kt | 2 +- .../app/ui/item/ItemListViewModelTest.kt | 36 +++++++++++++++++++ .../bollwerk/app/ui/navigation/ScreenTest.kt | 25 +++++++++++-- 7 files changed, 78 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/de/bollwerk/app/ui/dashboard/DashboardScreen.kt b/app/src/main/java/de/bollwerk/app/ui/dashboard/DashboardScreen.kt index b0cdaad..545fe97 100644 --- a/app/src/main/java/de/bollwerk/app/ui/dashboard/DashboardScreen.kt +++ b/app/src/main/java/de/bollwerk/app/ui/dashboard/DashboardScreen.kt @@ -34,6 +34,7 @@ import kotlin.math.roundToInt @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun DashboardScreen( + onCategoryClick: (Int) -> Unit = {}, viewModel: DashboardViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -77,7 +78,10 @@ internal fun DashboardScreen( ) } items(uiState.categorySummaries) { summary -> - CategoryCard(summary) + CategoryCard( + summary = summary, + onClick = { onCategoryClick(summary.categoryId) } + ) } } @@ -201,11 +205,13 @@ private fun WarningsSummaryCard(expiryCount: Int) { } } +@OptIn(ExperimentalMaterial3Api::class) @Composable -private fun CategoryCard(summary: CategorySummary) { +private fun CategoryCard(summary: CategorySummary, onClick: () -> Unit) { val currencyFormat = NumberFormat.getCurrencyInstance(Locale.GERMANY) Card( + onClick = onClick, modifier = Modifier.fillMaxWidth() ) { Row( diff --git a/app/src/main/java/de/bollwerk/app/ui/item/ItemListViewModel.kt b/app/src/main/java/de/bollwerk/app/ui/item/ItemListViewModel.kt index 4acecbc..82b1816 100644 --- a/app/src/main/java/de/bollwerk/app/ui/item/ItemListViewModel.kt +++ b/app/src/main/java/de/bollwerk/app/ui/item/ItemListViewModel.kt @@ -1,5 +1,6 @@ package de.bollwerk.app.ui.item +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel @@ -52,12 +53,14 @@ internal data class ItemListUiState( @HiltViewModel internal class ItemListViewModel @Inject constructor( + savedStateHandle: SavedStateHandle, private val itemRepository: ItemRepository, private val categoryRepository: CategoryRepository, private val locationRepository: LocationRepository ) : ViewModel() { - private val _filterState = MutableStateFlow(FilterState()) + private val initialCategoryId: Int? = savedStateHandle.get("categoryId") + private val _filterState = MutableStateFlow(FilterState(categoryId = initialCategoryId)) private val _uiState = MutableStateFlow(ItemListUiState()) val uiState: StateFlow = _uiState.asStateFlow() diff --git a/app/src/main/java/de/bollwerk/app/ui/navigation/BollwerkNavGraph.kt b/app/src/main/java/de/bollwerk/app/ui/navigation/BollwerkNavGraph.kt index ac2ee77..6eaecc7 100644 --- a/app/src/main/java/de/bollwerk/app/ui/navigation/BollwerkNavGraph.kt +++ b/app/src/main/java/de/bollwerk/app/ui/navigation/BollwerkNavGraph.kt @@ -27,7 +27,11 @@ internal fun BollwerkNavGraph( modifier = modifier ) { composable { - DashboardScreen() + DashboardScreen( + onCategoryClick = { categoryId -> + navController.navigate(Screen.ItemList(categoryId = categoryId)) + } + ) } composable { diff --git a/app/src/main/java/de/bollwerk/app/ui/navigation/Screen.kt b/app/src/main/java/de/bollwerk/app/ui/navigation/Screen.kt index a4f5313..c397858 100644 --- a/app/src/main/java/de/bollwerk/app/ui/navigation/Screen.kt +++ b/app/src/main/java/de/bollwerk/app/ui/navigation/Screen.kt @@ -9,7 +9,7 @@ internal sealed interface Screen { data object Dashboard : Screen @Serializable - data object ItemList : Screen + data class ItemList(val categoryId: Int? = null) : Screen @Serializable data class ItemForm(val itemId: String? = null, val prefillJson: String? = null) : Screen diff --git a/app/src/main/java/de/bollwerk/app/ui/navigation/TopLevelDestination.kt b/app/src/main/java/de/bollwerk/app/ui/navigation/TopLevelDestination.kt index d428a3f..677669f 100644 --- a/app/src/main/java/de/bollwerk/app/ui/navigation/TopLevelDestination.kt +++ b/app/src/main/java/de/bollwerk/app/ui/navigation/TopLevelDestination.kt @@ -26,7 +26,7 @@ internal enum class TopLevelDestination( label = "Overview" ), INVENTORY( - route = Screen.ItemList, + route = Screen.ItemList(), selectedIcon = Icons.Filled.Warehouse, unselectedIcon = Icons.Outlined.Warehouse, label = "Storage" diff --git a/app/src/test/java/de/bollwerk/app/ui/item/ItemListViewModelTest.kt b/app/src/test/java/de/bollwerk/app/ui/item/ItemListViewModelTest.kt index b1bcdd8..39a9d18 100644 --- a/app/src/test/java/de/bollwerk/app/ui/item/ItemListViewModelTest.kt +++ b/app/src/test/java/de/bollwerk/app/ui/item/ItemListViewModelTest.kt @@ -1,5 +1,6 @@ package de.bollwerk.app.ui.item +import androidx.lifecycle.SavedStateHandle import de.bollwerk.app.data.db.entity.CategoryEntity import de.bollwerk.app.data.db.entity.ItemEntity import de.bollwerk.app.data.db.entity.LocationEntity @@ -40,6 +41,7 @@ class ItemListViewModelTest { fakeCategoryRepository = FakeCategoryRepository() fakeLocationRepository = FakeLocationRepository() viewModel = ItemListViewModel( + savedStateHandle = SavedStateHandle(), itemRepository = fakeItemRepository, categoryRepository = fakeCategoryRepository, locationRepository = fakeLocationRepository @@ -543,6 +545,40 @@ class ItemListViewModelTest { assertEquals(2, viewModel.uiState.value.groupedItems.values.flatten().size) } + @Test + fun test_init_withCategoryIdFromNavigation_filterIsPreApplied() = runTest(testDispatcher) { + // Given + val vmWithCategory = ItemListViewModel( + savedStateHandle = SavedStateHandle(mapOf("categoryId" to 2)), + itemRepository = fakeItemRepository, + categoryRepository = fakeCategoryRepository, + locationRepository = fakeLocationRepository + ) + fakeCategoryRepository.emit( + listOf( + CategoryEntity(id = 1, name = "Lebensmittel"), + CategoryEntity(id = 2, name = "Hygiene") + ) + ) + fakeLocationRepository.emit(listOf(LocationEntity(id = 1, name = "Keller"))) + fakeItemRepository.emit( + listOf( + buildItemEntity(id = "a", name = "Reis", categoryId = 1), + buildItemEntity(id = "b", name = "Seife", categoryId = 2) + ) + ) + + // When + advanceUntilIdle() + + // Then + val state = vmWithCategory.uiState.value + assertEquals(2, state.selectedCategoryId) + val allItems = state.groupedItems.values.flatten() + assertEquals(1, allItems.size) + assertEquals("Seife", allItems.first().name) + } + // endregion } diff --git a/app/src/test/java/de/bollwerk/app/ui/navigation/ScreenTest.kt b/app/src/test/java/de/bollwerk/app/ui/navigation/ScreenTest.kt index e48d0d7..0da71e5 100644 --- a/app/src/test/java/de/bollwerk/app/ui/navigation/ScreenTest.kt +++ b/app/src/test/java/de/bollwerk/app/ui/navigation/ScreenTest.kt @@ -9,10 +9,10 @@ class ScreenTest { @Test fun test_itemListRoute_isSingleton() { // Given & When - val route = Screen.ItemList + val route = Screen.ItemList() // Then - assertEquals(Screen.ItemList, route) + assertEquals(Screen.ItemList(), route) } @Test @@ -73,4 +73,25 @@ class ScreenTest { // Then assert(route1 != route2) } + + @Test + fun test_itemListRoute_withoutCategoryId_hasNullCategoryId() { + // Given & When + val route = Screen.ItemList() + + // Then + assertNull(route.categoryId) + } + + @Test + fun test_itemListRoute_withCategoryId_hasCorrectCategoryId() { + // Given + val expectedId = 5 + + // When + val route = Screen.ItemList(categoryId = expectedId) + + // Then + assertEquals(expectedId, route.categoryId) + } }