feat(warnings): implement dedicated Warnings screen with ViewModel
ui/warnings/WarningsScreen.kt: full implementation replacing placeholder. Shows expiry warnings (colored by urgency: URGENT=error, WARNING=orange) and min-stock warnings as individual cards in a LazyColumn. Displays empty state when no warnings exist. ui/warnings/WarningsViewModel.kt: HiltViewModel observing ItemRepository flow, delegates to GetExpiryWarningsUseCase and GetMinStockWarningsUseCase. Exposes WarningsUiState via StateFlow. ui/warnings/WarningsUiState.kt: data class with expiryWarnings, minStockWarnings, isLoading, and derived properties (totalWarningCount, hasWarnings). ui/dashboard/DashboardScreen.kt: replaced ExpiryWarningsCard and MinStockWarningsCard with compact WarningsSummaryCard showing only warning counts. Removed unused domain model imports. tests: 7 WarningsViewModel unit tests covering empty state, expiry warnings, min-stock warnings, combined counts, reactive updates. Closes #34
This commit is contained in:
parent
a4c0dc63b4
commit
34bd1f603f
5 changed files with 472 additions and 85 deletions
|
|
@ -27,9 +27,6 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import de.krisenvorrat.app.domain.model.CategorySummary
|
||||
import de.krisenvorrat.app.domain.model.ExpiryUrgency
|
||||
import de.krisenvorrat.app.domain.model.ExpiryWarning
|
||||
import de.krisenvorrat.app.domain.model.MinStockWarning
|
||||
import java.text.NumberFormat
|
||||
import java.util.Locale
|
||||
import kotlin.math.roundToInt
|
||||
|
|
@ -63,12 +60,13 @@ internal fun DashboardScreen(
|
|||
|
||||
item { SupplyRangeCard(supplyRangeDays = uiState.supplyRangeDays) }
|
||||
|
||||
if (uiState.hasExpiryWarnings) {
|
||||
item { ExpiryWarningsCard(warnings = uiState.expiryWarnings) }
|
||||
}
|
||||
|
||||
if (uiState.hasMinStockWarnings) {
|
||||
item { MinStockWarningsCard(warnings = uiState.minStockWarnings) }
|
||||
if (uiState.hasExpiryWarnings || uiState.hasMinStockWarnings) {
|
||||
item {
|
||||
WarningsSummaryCard(
|
||||
expiryCount = uiState.expiryWarnings.size,
|
||||
minStockCount = uiState.minStockWarnings.size
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (uiState.categorySummaries.isNotEmpty()) {
|
||||
|
|
@ -178,7 +176,8 @@ private fun SupplyRangeCard(supplyRangeDays: Double) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun ExpiryWarningsCard(warnings: List<ExpiryWarning>) {
|
||||
private fun WarningsSummaryCard(expiryCount: Int, minStockCount: Int) {
|
||||
val totalCount = expiryCount + minStockCount
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
|
|
@ -187,78 +186,24 @@ private fun ExpiryWarningsCard(warnings: List<ExpiryWarning>) {
|
|||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
text = "Ablaufdaten-Warnungen (${warnings.size})",
|
||||
text = "⚠️ $totalCount Warnungen",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onErrorContainer
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
warnings.forEach { warning ->
|
||||
val color = when (warning.urgency) {
|
||||
ExpiryUrgency.URGENT -> MaterialTheme.colorScheme.error
|
||||
ExpiryUrgency.WARNING -> Color(0xFFE65100)
|
||||
}
|
||||
val daysText = when {
|
||||
warning.daysUntilExpiry < 0 -> "abgelaufen"
|
||||
warning.daysUntilExpiry == 0L -> "läuft heute ab"
|
||||
else -> "noch ${warning.daysUntilExpiry} Tage"
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 2.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = warning.item.name,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = color
|
||||
)
|
||||
Text(
|
||||
text = daysText,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = color
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
if (expiryCount > 0) {
|
||||
Text(
|
||||
text = "$expiryCount Ablaufdaten-Warnungen",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onErrorContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MinStockWarningsCard(warnings: List<MinStockWarning>) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer
|
||||
)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
text = "Mindestbestand unterschritten (${warnings.size})",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onErrorContainer
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
warnings.forEach { warning ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 2.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = warning.item.name,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Text(
|
||||
text = "fehlen: ${String.format(Locale.GERMANY, "%.1f", warning.deficit)} ${warning.item.unit}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
if (minStockCount > 0) {
|
||||
Text(
|
||||
text = "$minStockCount Mindestbestand-Warnungen",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onErrorContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,33 +1,175 @@
|
|||
package de.krisenvorrat.app.ui.warnings
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import de.krisenvorrat.app.domain.model.ExpiryUrgency
|
||||
import de.krisenvorrat.app.domain.model.ExpiryWarning
|
||||
import de.krisenvorrat.app.domain.model.MinStockWarning
|
||||
import java.util.Locale
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
internal fun WarningsScreen() {
|
||||
internal fun WarningsScreen(
|
||||
viewModel: WarningsViewModel = hiltViewModel()
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
TopAppBar(title = { Text("Warnungen") })
|
||||
Box(
|
||||
|
||||
if (uiState.isLoading) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
} else if (!uiState.hasWarnings) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "Keine Warnungen vorhanden",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
item { Spacer(modifier = Modifier.height(4.dp)) }
|
||||
|
||||
if (uiState.hasExpiryWarnings) {
|
||||
item {
|
||||
Text(
|
||||
text = "Ablaufdaten (${uiState.expiryWarnings.size})",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.padding(top = 4.dp)
|
||||
)
|
||||
}
|
||||
items(uiState.expiryWarnings) { warning ->
|
||||
ExpiryWarningItem(warning)
|
||||
}
|
||||
}
|
||||
|
||||
if (uiState.hasMinStockWarnings) {
|
||||
item {
|
||||
Text(
|
||||
text = "Mindestbestand unterschritten (${uiState.minStockWarnings.size})",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.padding(top = 4.dp)
|
||||
)
|
||||
}
|
||||
items(uiState.minStockWarnings) { warning ->
|
||||
MinStockWarningItem(warning)
|
||||
}
|
||||
}
|
||||
|
||||
item { Spacer(modifier = Modifier.height(8.dp)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExpiryWarningItem(warning: ExpiryWarning) {
|
||||
val containerColor = when (warning.urgency) {
|
||||
ExpiryUrgency.URGENT -> MaterialTheme.colorScheme.errorContainer
|
||||
ExpiryUrgency.WARNING -> Color(0xFFFFF3E0)
|
||||
}
|
||||
val contentColor = when (warning.urgency) {
|
||||
ExpiryUrgency.URGENT -> MaterialTheme.colorScheme.onErrorContainer
|
||||
ExpiryUrgency.WARNING -> Color(0xFFE65100)
|
||||
}
|
||||
val daysText = when {
|
||||
warning.daysUntilExpiry < 0 -> "abgelaufen"
|
||||
warning.daysUntilExpiry == 0L -> "läuft heute ab"
|
||||
else -> "noch ${warning.daysUntilExpiry} Tage"
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = containerColor)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Warnungen werden in einem späteren Schritt implementiert.",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
text = warning.item.name,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = contentColor
|
||||
)
|
||||
Text(
|
||||
text = daysText,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = contentColor
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MinStockWarningItem(warning: MinStockWarning) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = warning.item.name,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onErrorContainer
|
||||
)
|
||||
Text(
|
||||
text = "fehlen: ${String.format(Locale.GERMANY, "%.1f", warning.deficit)} ${warning.item.unit}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
package de.krisenvorrat.app.ui.warnings
|
||||
|
||||
import de.krisenvorrat.app.domain.model.ExpiryWarning
|
||||
import de.krisenvorrat.app.domain.model.MinStockWarning
|
||||
|
||||
internal data class WarningsUiState(
|
||||
val expiryWarnings: List<ExpiryWarning> = emptyList(),
|
||||
val minStockWarnings: List<MinStockWarning> = emptyList(),
|
||||
val isLoading: Boolean = true
|
||||
) {
|
||||
val hasExpiryWarnings: Boolean
|
||||
get() = expiryWarnings.isNotEmpty()
|
||||
|
||||
val hasMinStockWarnings: Boolean
|
||||
get() = minStockWarnings.isNotEmpty()
|
||||
|
||||
val totalWarningCount: Int
|
||||
get() = expiryWarnings.size + minStockWarnings.size
|
||||
|
||||
val hasWarnings: Boolean
|
||||
get() = hasExpiryWarnings || hasMinStockWarnings
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package de.krisenvorrat.app.ui.warnings
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import de.krisenvorrat.app.domain.repository.ItemRepository
|
||||
import de.krisenvorrat.app.domain.usecase.GetExpiryWarningsUseCase
|
||||
import de.krisenvorrat.app.domain.usecase.GetMinStockWarningsUseCase
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
internal class WarningsViewModel @Inject constructor(
|
||||
private val itemRepository: ItemRepository,
|
||||
private val getExpiryWarnings: GetExpiryWarningsUseCase,
|
||||
private val getMinStockWarnings: GetMinStockWarningsUseCase
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(WarningsUiState())
|
||||
val uiState: StateFlow<WarningsUiState> = _uiState.asStateFlow()
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
itemRepository.getAll().collect { items ->
|
||||
_uiState.update {
|
||||
WarningsUiState(
|
||||
expiryWarnings = getExpiryWarnings(items),
|
||||
minStockWarnings = getMinStockWarnings(items),
|
||||
isLoading = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,239 @@
|
|||
package de.krisenvorrat.app.ui.warnings
|
||||
|
||||
import de.krisenvorrat.app.data.db.entity.ItemEntity
|
||||
import de.krisenvorrat.app.domain.repository.ItemRepository
|
||||
import de.krisenvorrat.app.domain.usecase.GetExpiryWarningsUseCase
|
||||
import de.krisenvorrat.app.domain.usecase.GetMinStockWarningsUseCase
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.time.LocalDate
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class WarningsViewModelTest {
|
||||
|
||||
private val testDispatcher = StandardTestDispatcher()
|
||||
private lateinit var fakeItemRepository: FakeItemRepository
|
||||
private lateinit var viewModel: WarningsViewModel
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
Dispatchers.setMain(testDispatcher)
|
||||
fakeItemRepository = FakeItemRepository()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
|
||||
private fun createViewModel() = WarningsViewModel(
|
||||
itemRepository = fakeItemRepository,
|
||||
getExpiryWarnings = GetExpiryWarningsUseCase(),
|
||||
getMinStockWarnings = GetMinStockWarningsUseCase()
|
||||
)
|
||||
|
||||
@Test
|
||||
fun test_init_withNoData_stateHasEmptyLists() = runTest(testDispatcher) {
|
||||
// Given – empty repository
|
||||
viewModel = createViewModel()
|
||||
|
||||
// When
|
||||
advanceUntilIdle()
|
||||
|
||||
// Then
|
||||
val state = viewModel.uiState.value
|
||||
assertFalse(state.isLoading)
|
||||
assertFalse(state.hasExpiryWarnings)
|
||||
assertFalse(state.hasMinStockWarnings)
|
||||
assertFalse(state.hasWarnings)
|
||||
assertEquals(0, state.totalWarningCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_init_withExpiringItems_expiryWarningsArePresent() = runTest(testDispatcher) {
|
||||
// Given – item expiring in 3 months (within URGENT_MONTHS=6)
|
||||
val expiryDate = LocalDate.now().plusMonths(3)
|
||||
fakeItemRepository.emit(
|
||||
listOf(buildTestItem(id = "a", expiryDate = expiryDate))
|
||||
)
|
||||
viewModel = createViewModel()
|
||||
|
||||
// When
|
||||
advanceUntilIdle()
|
||||
|
||||
// Then
|
||||
val state = viewModel.uiState.value
|
||||
assertTrue(state.hasExpiryWarnings)
|
||||
assertEquals(1, state.expiryWarnings.size)
|
||||
assertTrue(state.hasWarnings)
|
||||
assertEquals(1, state.totalWarningCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_init_withMinStockViolation_minStockWarningsArePresent() = runTest(testDispatcher) {
|
||||
// Given – item with quantity 1 but minStock 5
|
||||
fakeItemRepository.emit(
|
||||
listOf(buildTestItem(id = "a", quantity = 1.0, minStock = 5.0))
|
||||
)
|
||||
viewModel = createViewModel()
|
||||
|
||||
// When
|
||||
advanceUntilIdle()
|
||||
|
||||
// Then
|
||||
val state = viewModel.uiState.value
|
||||
assertTrue(state.hasMinStockWarnings)
|
||||
assertEquals(1, state.minStockWarnings.size)
|
||||
assertEquals(4.0, state.minStockWarnings.first().deficit, 0.001)
|
||||
assertTrue(state.hasWarnings)
|
||||
assertEquals(1, state.totalWarningCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_init_withBothWarnings_totalCountIsCombined() = runTest(testDispatcher) {
|
||||
// Given – one expiring item and one below min stock
|
||||
val expiryDate = LocalDate.now().plusMonths(3)
|
||||
fakeItemRepository.emit(
|
||||
listOf(
|
||||
buildTestItem(id = "a", expiryDate = expiryDate),
|
||||
buildTestItem(id = "b", quantity = 1.0, minStock = 5.0)
|
||||
)
|
||||
)
|
||||
viewModel = createViewModel()
|
||||
|
||||
// When
|
||||
advanceUntilIdle()
|
||||
|
||||
// Then
|
||||
val state = viewModel.uiState.value
|
||||
assertTrue(state.hasExpiryWarnings)
|
||||
assertTrue(state.hasMinStockWarnings)
|
||||
assertEquals(2, state.totalWarningCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_init_withFarExpiryDate_noExpiryWarnings() = runTest(testDispatcher) {
|
||||
// Given – item expiring in 2 years (beyond WARNING_MONTHS=12)
|
||||
val expiryDate = LocalDate.now().plusYears(2)
|
||||
fakeItemRepository.emit(
|
||||
listOf(buildTestItem(id = "a", expiryDate = expiryDate))
|
||||
)
|
||||
viewModel = createViewModel()
|
||||
|
||||
// When
|
||||
advanceUntilIdle()
|
||||
|
||||
// Then
|
||||
assertFalse(viewModel.uiState.value.hasExpiryWarnings)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_init_withSufficientStock_noMinStockWarnings() = runTest(testDispatcher) {
|
||||
// Given – item with quantity above minStock
|
||||
fakeItemRepository.emit(
|
||||
listOf(buildTestItem(id = "a", quantity = 10.0, minStock = 5.0))
|
||||
)
|
||||
viewModel = createViewModel()
|
||||
|
||||
// When
|
||||
advanceUntilIdle()
|
||||
|
||||
// Then
|
||||
assertFalse(viewModel.uiState.value.hasMinStockWarnings)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_reactiveUpdate_whenItemsChange_stateUpdates() = runTest(testDispatcher) {
|
||||
// Given – start with no warnings
|
||||
viewModel = createViewModel()
|
||||
advanceUntilIdle()
|
||||
assertFalse(viewModel.uiState.value.hasWarnings)
|
||||
|
||||
// When – an item below min stock appears
|
||||
fakeItemRepository.emit(
|
||||
listOf(buildTestItem(id = "a", quantity = 1.0, minStock = 5.0))
|
||||
)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Then
|
||||
assertTrue(viewModel.uiState.value.hasMinStockWarnings)
|
||||
assertEquals(1, viewModel.uiState.value.totalWarningCount)
|
||||
}
|
||||
}
|
||||
|
||||
// region Test Helpers
|
||||
|
||||
private fun buildTestItem(
|
||||
id: String = "id1",
|
||||
name: String = "Konserve",
|
||||
categoryId: Int = 1,
|
||||
quantity: Double = 1.0,
|
||||
unit: String = "Stk",
|
||||
unitPrice: Double = 0.0,
|
||||
kcalPer100g: Int? = null,
|
||||
expiryDate: LocalDate? = null,
|
||||
locationId: Int = 1,
|
||||
minStock: Double = 0.0
|
||||
) = ItemEntity(
|
||||
id = id,
|
||||
name = name,
|
||||
categoryId = categoryId,
|
||||
quantity = quantity,
|
||||
unit = unit,
|
||||
unitPrice = unitPrice,
|
||||
kcalPer100g = kcalPer100g,
|
||||
expiryDate = expiryDate,
|
||||
locationId = locationId,
|
||||
minStock = minStock,
|
||||
notes = "",
|
||||
lastUpdated = 0L
|
||||
)
|
||||
|
||||
private class FakeItemRepository : ItemRepository {
|
||||
private val flow = MutableStateFlow<List<ItemEntity>>(emptyList())
|
||||
|
||||
fun emit(items: List<ItemEntity>) {
|
||||
flow.value = items
|
||||
}
|
||||
|
||||
override fun getAll(): Flow<List<ItemEntity>> = flow
|
||||
|
||||
override suspend fun getById(id: String): ItemEntity? =
|
||||
flow.value.find { it.id == id }
|
||||
|
||||
override suspend fun insert(item: ItemEntity) {
|
||||
flow.value = flow.value + item
|
||||
}
|
||||
|
||||
override suspend fun update(item: ItemEntity) {
|
||||
flow.value = flow.value.map { if (it.id == item.id) item else it }
|
||||
}
|
||||
|
||||
override suspend fun delete(item: ItemEntity) {
|
||||
flow.value = flow.value.filter { it.id != item.id }
|
||||
}
|
||||
|
||||
override fun getByCategory(categoryId: Int): Flow<List<ItemEntity>> =
|
||||
MutableStateFlow(flow.value.filter { it.categoryId == categoryId })
|
||||
|
||||
override fun getByLocation(locationId: Int): Flow<List<ItemEntity>> =
|
||||
MutableStateFlow(flow.value.filter { it.locationId == locationId })
|
||||
|
||||
override fun getExpiringSoon(daysUntil: Int): Flow<List<ItemEntity>> =
|
||||
MutableStateFlow(emptyList())
|
||||
}
|
||||
|
||||
// endregion
|
||||
Loading…
Reference in a new issue