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
This commit is contained in:
parent
7ccd2dc1fd
commit
bdd8cb4b11
7 changed files with 78 additions and 8 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<Int>("categoryId")
|
||||
private val _filterState = MutableStateFlow(FilterState(categoryId = initialCategoryId))
|
||||
private val _uiState = MutableStateFlow(ItemListUiState())
|
||||
val uiState: StateFlow<ItemListUiState> = _uiState.asStateFlow()
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,11 @@ internal fun BollwerkNavGraph(
|
|||
modifier = modifier
|
||||
) {
|
||||
composable<Screen.Dashboard> {
|
||||
DashboardScreen()
|
||||
DashboardScreen(
|
||||
onCategoryClick = { categoryId ->
|
||||
navController.navigate(Screen.ItemList(categoryId = categoryId))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable<Screen.ItemList> {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue