refactor: kcal/100g -> kcal/kg umbenennen und Mindestbestand entfernen
- ItemEntity, ItemDto: kcalPer100g -> kcalPerKg (kcal_per_kg), minStock-Spalte komplett entfernt - CalculateSupplyRangeUseCase: Formel angepasst (/ 1000.0 * kcalPerKg) - GetMinStockWarningsUseCase + MinStockWarning: gelöscht - UI (ItemFormScreen, WarningsScreen, DashboardScreen): Mindestbestand- Felder und Warnungsabschnitte entfernt - ViewModels, UiState, Repository: alle Referenzen bereinigt - Server (Tables, InventoryRepository): Schema angepasst - Room: fallbackToDestructiveMigration() hinzugefügt (keine Produktivdaten) - Alle 434 Tests gruen
This commit is contained in:
parent
395939a4ec
commit
8280a9daf9
37 changed files with 290 additions and 473 deletions
125
Anforderungen/design/server-tech/candidates.md
Normal file
125
Anforderungen/design/server-tech/candidates.md
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
# Technology Candidates – REST-Server für Geräte-Synchronisierung (Phase 2)
|
||||
|
||||
Date: 2026-05-14
|
||||
Requirements file: requirements.md
|
||||
|
||||
---
|
||||
|
||||
## Candidate Table
|
||||
|
||||
| Name | Description | Use case | Age | Adoption | Last release | Update cadence | Req. coverage | Score |
|
||||
| ----------------- | --------------------------------------------------- | -------------------------------------------- | ---------------- | --------------------------- | ------------- | -------------- | ------------------------ | ----- |
|
||||
| Ktor | Kotlin-nativer async Web-Framework von JetBrains | Microservices, APIs, leichtgewichtige Server | ~8 Jahre (2018) | Moderate (JetBrains-backed) | 2025 (3.x) | Monatlich | Alle Must + alle Should | **9** |
|
||||
| Spring Boot | Enterprise-Java/Kotlin-Framework, Industriestandard | Enterprise-Apps, REST-APIs, Microservices | ~12 Jahre (2014) | Widespread | 2025 (3.x) | Monatlich | Alle Must, teilw. Should | **6** |
|
||||
| Node.js + Express | JavaScript-Runtime + minimalistisches Web-Framework | APIs, Prototyping, Full-Stack JS | ~15 Jahre (2010) | Widespread | 2025 (5.x) | Regelmäßig | Alle Must, wenig Should | **5** |
|
||||
| Python + FastAPI | Modernes Python-Web-Framework mit Type Hints | Schnelle API-Entwicklung, ML-Backends | ~7 Jahre (2018) | Widespread | 2025 (0.115+) | Monatlich | Alle Must, wenig Should | **5** |
|
||||
|
||||
---
|
||||
|
||||
## Candidate Details
|
||||
|
||||
### 1. Ktor (JetBrains)
|
||||
|
||||
**Homepage:** https://ktor.io
|
||||
**Repository:** https://github.com/ktorio/ktor
|
||||
|
||||
**Requirement coverage:**
|
||||
|
||||
- ✅ Must: REST-API (JSON), einfaches Deployment (Fat JAR / Docker), CRUD-Unterstützung, JSON via kotlinx.serialization nativ, aktiv gewartet (JetBrains), sehr geringer Ressourcenverbrauch
|
||||
- ✅ Should: **Kotlin-nativ** – gleiche Sprache wie Client, Datenmodelle direkt teilbar, kotlinx.serialization als First-Class-Citizen, Coroutines-basiert, kein Application-Server nötig (eingebetteter Netty/CIO), exzellente Dokumentation (ktor.io), einfache DB-Anbindung (Exposed ORM), einfaches Testing
|
||||
- ✅ Nice-to-Have: WebSocket-Support eingebaut, Authentifizierungs-Plugins, Docker-ready (Fat JAR), OpenAPI-Plugin verfügbar, Hot-Reload via Auto-Reload-Feature, Type-safe Routing
|
||||
|
||||
**Stärken:**
|
||||
|
||||
- **Gleiche Sprache wie der Client** – Datenmodelle (`@Serializable`-Klassen) können in einem Shared-Modul zwischen Server und Client geteilt werden
|
||||
- Koroutinen-basiert – konsistent mit dem Android-Client
|
||||
- Sehr leichtgewichtig – Start in < 1 Sekunde, wenig RAM
|
||||
- JetBrains-maintained – langfristige Pflege gesichert
|
||||
- Modulares Plugin-System – nur einbinden was man braucht
|
||||
|
||||
**Risiken / Schwächen:**
|
||||
|
||||
- Kleinere Community als Spring Boot – weniger StackOverflow-Antworten
|
||||
- Weniger Enterprise-Features out of the box (kein Spring-Ökosystem)
|
||||
- Bei komplexen Anforderungen muss man mehr selbst bauen
|
||||
|
||||
---
|
||||
|
||||
### 2. Spring Boot
|
||||
|
||||
**Homepage:** https://spring.io/projects/spring-boot
|
||||
**Repository:** https://github.com/spring-projects/spring-boot
|
||||
|
||||
**Requirement coverage:**
|
||||
|
||||
- ✅ Must: REST-API, Deployment, CRUD, JSON, aktiv gewartet, stabil
|
||||
- ⚠️ Should: Kotlin-Support vorhanden aber nicht nativ (Java-First), kotlinx.serialization nicht Standard (Jackson wird bevorzugt), schwergewichtig (Tomcat eingebettet, hoher RAM-Verbrauch), Coroutines-Support nur via WebFlux
|
||||
- ✅ Nice-to-Have: WebSocket, Spring Security, Docker, Swagger/OpenAPI, DevTools Hot-Reload
|
||||
|
||||
**Stärken:**
|
||||
|
||||
- Industriestandard mit riesiger Community und Ökosystem
|
||||
- Extrem viele Tutorials, Bücher, StackOverflow-Antworten
|
||||
- Battle-tested in Produktion bei Millionen von Projekten
|
||||
- Spring Security für Authentication/Authorization
|
||||
|
||||
**Risiken / Schwächen:**
|
||||
|
||||
- **Overhead für ein kleines Projekt:** Hoher RAM-Verbrauch (200–500 MB), langsame Startzeit, viel Boilerplate
|
||||
- **Java-First:** Kotlin ist Second-Class-Citizen – Spring-Idiome (Annotations, Bean-Wiring) fühlen sich in Kotlin weniger natürlich an
|
||||
- **Keine geteilten Datenmodelle:** kotlinx.serialization wird nicht nativ unterstützt, Jackson-Annotationen nötig – das Client-Datenmodell kann nicht 1:1 wiederverwendet werden
|
||||
- Für 2–10 Nutzer massiv überdimensioniert
|
||||
|
||||
---
|
||||
|
||||
### 3. Node.js + Express
|
||||
|
||||
**Homepage:** https://expressjs.com
|
||||
**Repository:** https://github.com/expressjs/express
|
||||
|
||||
**Requirement coverage:**
|
||||
|
||||
- ✅ Must: REST-API, einfaches Deployment, CRUD, JSON nativ, aktiv gewartet, geringer Verbrauch
|
||||
- ❌ Should: **Andere Sprache (JavaScript/TypeScript)** – keine Wiederverwendung von Kotlin-Datenmodellen, keine Coroutines, kein kotlinx.serialization, andere Toolchain (npm vs. Gradle)
|
||||
- ⚠️ Nice-to-Have: WebSocket (via ws/socket.io), Passport.js für Auth, Docker einfach, OpenAPI-Generierung möglich
|
||||
|
||||
**Stärken:**
|
||||
|
||||
- Extrem leichtgewichtig und schnell aufzusetzen
|
||||
- Riesige Community, npm-Ökosystem
|
||||
- Sehr gute Performance für I/O-lastige Workloads
|
||||
- Viele fertige Middleware-Pakete
|
||||
|
||||
**Risiken / Schwächen:**
|
||||
|
||||
- **Sprachwechsel:** Kotlin (Client) ↔ JavaScript (Server) – kein Code-Sharing, doppelte Modell-Definition
|
||||
- **Typsicherheit:** JavaScript bietet weniger Typsicherheit als Kotlin (TypeScript hilft, ist aber ein separates Ökosystem)
|
||||
- **Dependency-Overhead:** npm-Ökosystem hat bekannte Supply-Chain-Risiken
|
||||
- Separates Tooling und Build-System nötig
|
||||
|
||||
---
|
||||
|
||||
### 4. Python + FastAPI
|
||||
|
||||
**Homepage:** https://fastapi.tiangolo.com
|
||||
**Repository:** https://github.com/tiangolo/fastapi
|
||||
|
||||
**Requirement coverage:**
|
||||
|
||||
- ✅ Must: REST-API, einfaches Deployment, CRUD, JSON, aktiv gewartet, geringer Verbrauch
|
||||
- ❌ Should: **Andere Sprache (Python)** – keine Wiederverwendung von Kotlin-Datenmodellen, kein kotlinx.serialization, andere Toolchain (pip vs. Gradle), kein Coroutines-Bezug
|
||||
- ✅ Nice-to-Have: WebSocket-Support, OAuth2-Support, Docker einfach, **automatische OpenAPI/Swagger-Generierung** (Highlight!), Hot-Reload via uvicorn
|
||||
|
||||
**Stärken:**
|
||||
|
||||
- Extrem schnelle Entwicklung dank Auto-Dokumentation und Type Hints
|
||||
- Automatische OpenAPI-Doku out of the box
|
||||
- Async I/O via asyncio
|
||||
- Gut für Prototyping
|
||||
|
||||
**Risiken / Schwächen:**
|
||||
|
||||
- **Sprachwechsel:** Kotlin ↔ Python – komplett andere Toolchain, kein Code-Sharing
|
||||
- **GIL:** Python Global Interpreter Lock kann bei CPU-intensiven Operationen limitieren (hier aber irrelevant)
|
||||
- **Deployment:** Python-Umgebungen können fragiler sein als JVM-Fat-JARs (venv, pip-Abhängigkeiten)
|
||||
- Weniger natürliche Integration mit dem Kotlin/Gradle-basierten Projekt
|
||||
60
Anforderungen/design/server-tech/requirements.md
Normal file
60
Anforderungen/design/server-tech/requirements.md
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
# Technology Requirements – REST-Server für Geräte-Synchronisierung (Phase 2)
|
||||
|
||||
Date: 2026-05-14
|
||||
Issue: #10
|
||||
Author: Tech-Decision Workflow
|
||||
|
||||
---
|
||||
|
||||
## Kontext
|
||||
|
||||
Die Krisenvorrat-App (Android/Kotlin) speichert in v1.0 alle Daten lokal (Room/SQLite). In Phase 2 soll ein REST-Server hinzukommen, der die Synchronisierung und das Sharing des Inventars zwischen mehreren Geräten ermöglicht.
|
||||
|
||||
**Einsatzszenario:**
|
||||
|
||||
- Privater Gebrauch, kein kommerzieller Betrieb
|
||||
- Wenige Geräte (2–10), keine hohe Last
|
||||
- Während Entwicklung: Server lokal auf Laptop, Testgerät im LAN
|
||||
- Später: kleiner Server im Internet (VPS / Homeserver)
|
||||
- Datenformat: JSON (kotlinx.serialization, bereits im Client definiert)
|
||||
|
||||
**Vom Issue genannte Optionen:** Ktor, Spring Boot, Node.js/Express, Python/FastAPI
|
||||
|
||||
---
|
||||
|
||||
## Must-Have (Eliminatoren)
|
||||
|
||||
- REST-API bereitstellen (JSON Request/Response)
|
||||
- Einfaches Deployment auf einem kleinen Linux-VPS oder Homeserver
|
||||
- Unterstützung für grundlegende CRUD-Operationen (Inventar-Items, Kategorien, Lagerorte)
|
||||
- JSON als primäres Datenformat (kompatibel mit dem bestehenden Client-Datenmodell)
|
||||
- Aktiv gewartet, stabile Release-Versionen verfügbar
|
||||
- Geringe Betriebskosten (wenig RAM/CPU-Verbrauch für wenige Nutzer)
|
||||
|
||||
## Should-Have (gewichtete Pluspunkte)
|
||||
|
||||
- **Kotlin-Kompatibilität:** Gleiche Sprache wie der Android-Client (Wiederverwendung von Datenmodellen, kotlinx.serialization)
|
||||
- Einfache Einrichtung und geringer Boilerplate
|
||||
- Integrierte Unterstützung für Coroutines / async I/O
|
||||
- Leichtgewichtig (kein Application-Server wie Tomcat nötig)
|
||||
- Gute Dokumentation und Lernressourcen
|
||||
- Einfache Datenbankanbindung (SQLite oder PostgreSQL)
|
||||
- Einfaches Testen (Unit- und Integrationstests)
|
||||
|
||||
## Nice-to-Have (Bonus)
|
||||
|
||||
- WebSocket-Unterstützung (für Echtzeit-Sync in einer späteren Phase)
|
||||
- Eingebaute Authentifizierung / Sicherheitsmechanismen
|
||||
- Docker-Image oder einfache Containerisierung
|
||||
- OpenAPI / Swagger-Generierung
|
||||
- Hot-Reload / schnelle Entwicklungszyklen
|
||||
- Type-safe Routing
|
||||
|
||||
## Constraints
|
||||
|
||||
- **Zielplattform Server:** Linux (VPS / Homeserver), optional auch Windows/macOS für lokale Entwicklung
|
||||
- **Client-Sprache:** Kotlin (Android) – geteilte Modelle sind ein großer Vorteil
|
||||
- **Serialisierung:** kotlinx.serialization (bereits im Client im Einsatz)
|
||||
- **Lizenz:** Open Source (MIT, Apache 2.0 o.ä.)
|
||||
- **Skalierung:** Nicht relevant – max. 10 gleichzeitige Nutzer
|
||||
- **Budget:** Kein Budget für kommerzielle Lizenzen oder teure Hosting-Lösungen
|
||||
|
|
@ -32,10 +32,9 @@ internal data class ItemEntity(
|
|||
@ColumnInfo(name = "quantity") val quantity: Double,
|
||||
@ColumnInfo(name = "unit") val unit: String,
|
||||
@ColumnInfo(name = "unit_price") val unitPrice: Double,
|
||||
@ColumnInfo(name = "kcal_per_100g") val kcalPer100g: Int?,
|
||||
@ColumnInfo(name = "kcal_per_kg") val kcalPerKg: Int?,
|
||||
@ColumnInfo(name = "expiry_date") val expiryDate: LocalDate?,
|
||||
@ColumnInfo(name = "location_id") val locationId: Int,
|
||||
@ColumnInfo(name = "min_stock") val minStock: Double,
|
||||
@ColumnInfo(name = "notes") val notes: String,
|
||||
@ColumnInfo(name = "last_updated") val lastUpdated: Long
|
||||
)
|
||||
|
|
|
|||
|
|
@ -64,10 +64,9 @@ internal class ImportExportRepositoryImpl @Inject constructor(
|
|||
quantity = item.quantity,
|
||||
unit = item.unit,
|
||||
unitPrice = item.unitPrice,
|
||||
kcalPer100g = item.kcalPer100g,
|
||||
kcalPerKg = item.kcalPerKg,
|
||||
expiryDate = item.expiryDate?.toString(),
|
||||
locationId = item.locationId,
|
||||
minStock = item.minStock,
|
||||
notes = item.notes,
|
||||
lastUpdated = item.lastUpdated
|
||||
)
|
||||
|
|
@ -102,10 +101,9 @@ internal class ImportExportRepositoryImpl @Inject constructor(
|
|||
quantity = item.quantity,
|
||||
unit = item.unit,
|
||||
unitPrice = item.unitPrice,
|
||||
kcalPer100g = item.kcalPer100g,
|
||||
kcalPerKg = item.kcalPerKg,
|
||||
expiryDate = item.expiryDate?.let { LocalDate.parse(it) },
|
||||
locationId = item.locationId,
|
||||
minStock = item.minStock,
|
||||
notes = item.notes,
|
||||
lastUpdated = item.lastUpdated
|
||||
)
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ internal object DatabaseModule {
|
|||
fun provideDatabase(@ApplicationContext context: Context): KrisenvorratDatabase =
|
||||
Room.databaseBuilder(context, KrisenvorratDatabase::class.java, "krisenvorrat.db")
|
||||
.addCallback(DefaultDataCallback)
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
||||
|
||||
private object DefaultDataCallback : RoomDatabase.Callback() {
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
package de.krisenvorrat.app.domain.model
|
||||
|
||||
import de.krisenvorrat.app.data.db.entity.ItemEntity
|
||||
|
||||
internal data class MinStockWarning(
|
||||
val item: ItemEntity,
|
||||
val deficit: Double
|
||||
)
|
||||
|
|
@ -19,9 +19,9 @@ internal class CalculateSupplyRangeUseCase @Inject constructor() {
|
|||
if (dailyNeed <= 0) return 0.0
|
||||
|
||||
val totalKcal = items.sumOf { item ->
|
||||
val kcalPer100g = item.kcalPer100g ?: return@sumOf 0.0
|
||||
val kcalPerKg = item.kcalPerKg ?: return@sumOf 0.0
|
||||
val grams = convertToGrams(item.quantity, item.unit) ?: return@sumOf 0.0
|
||||
(grams / 100.0) * kcalPer100g
|
||||
(grams / 1000.0) * kcalPerKg
|
||||
}
|
||||
|
||||
return totalKcal / dailyNeed
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
package de.krisenvorrat.app.domain.usecase
|
||||
|
||||
import de.krisenvorrat.app.data.db.entity.ItemEntity
|
||||
import de.krisenvorrat.app.domain.model.MinStockWarning
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class GetMinStockWarningsUseCase @Inject constructor() {
|
||||
|
||||
operator fun invoke(items: List<ItemEntity>): List<MinStockWarning> {
|
||||
return items
|
||||
.filter { it.quantity < it.minStock }
|
||||
.map { item ->
|
||||
MinStockWarning(
|
||||
item = item,
|
||||
deficit = item.minStock - item.quantity
|
||||
)
|
||||
}
|
||||
.sortedByDescending { it.deficit }
|
||||
}
|
||||
}
|
||||
|
|
@ -60,11 +60,10 @@ internal fun DashboardScreen(
|
|||
|
||||
item { SupplyRangeCard(supplyRangeDays = uiState.supplyRangeDays) }
|
||||
|
||||
if (uiState.hasExpiryWarnings || uiState.hasMinStockWarnings) {
|
||||
if (uiState.hasExpiryWarnings) {
|
||||
item {
|
||||
WarningsSummaryCard(
|
||||
expiryCount = uiState.expiryWarnings.size,
|
||||
minStockCount = uiState.minStockWarnings.size
|
||||
expiryCount = uiState.expiryWarnings.size
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -176,8 +175,8 @@ private fun SupplyRangeCard(supplyRangeDays: Double) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun WarningsSummaryCard(expiryCount: Int, minStockCount: Int) {
|
||||
val totalCount = expiryCount + minStockCount
|
||||
private fun WarningsSummaryCard(expiryCount: Int) {
|
||||
val totalCount = expiryCount
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
|
|
@ -198,13 +197,6 @@ private fun WarningsSummaryCard(expiryCount: Int, minStockCount: Int) {
|
|||
color = MaterialTheme.colorScheme.onErrorContainer
|
||||
)
|
||||
}
|
||||
if (minStockCount > 0) {
|
||||
Text(
|
||||
text = "$minStockCount Mindestbestand-Warnungen",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onErrorContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,14 +2,12 @@ package de.krisenvorrat.app.ui.dashboard
|
|||
|
||||
import de.krisenvorrat.app.domain.model.CategorySummary
|
||||
import de.krisenvorrat.app.domain.model.ExpiryWarning
|
||||
import de.krisenvorrat.app.domain.model.MinStockWarning
|
||||
|
||||
internal data class DashboardUiState(
|
||||
val categorySummaries: List<CategorySummary> = emptyList(),
|
||||
val totalValue: Double = 0.0,
|
||||
val supplyRangeDays: Double = 0.0,
|
||||
val expiryWarnings: List<ExpiryWarning> = emptyList(),
|
||||
val minStockWarnings: List<MinStockWarning> = emptyList(),
|
||||
val isLoading: Boolean = true
|
||||
) {
|
||||
val totalItemCount: Int
|
||||
|
|
@ -17,7 +15,4 @@ internal data class DashboardUiState(
|
|||
|
||||
val hasExpiryWarnings: Boolean
|
||||
get() = expiryWarnings.isNotEmpty()
|
||||
|
||||
val hasMinStockWarnings: Boolean
|
||||
get() = minStockWarnings.isNotEmpty()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import de.krisenvorrat.app.domain.usecase.CalculateCategorySummaryUseCase
|
|||
import de.krisenvorrat.app.domain.usecase.CalculateSupplyRangeUseCase
|
||||
import de.krisenvorrat.app.domain.usecase.CalculateTotalValueUseCase
|
||||
import de.krisenvorrat.app.domain.usecase.GetExpiryWarningsUseCase
|
||||
import de.krisenvorrat.app.domain.usecase.GetMinStockWarningsUseCase
|
||||
import de.krisenvorrat.app.domain.repository.CategoryRepository
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
|
@ -30,8 +29,7 @@ internal class DashboardViewModel @Inject constructor(
|
|||
private val calculateCategorySummary: CalculateCategorySummaryUseCase,
|
||||
private val calculateTotalValue: CalculateTotalValueUseCase,
|
||||
private val calculateSupplyRange: CalculateSupplyRangeUseCase,
|
||||
private val getExpiryWarnings: GetExpiryWarningsUseCase,
|
||||
private val getMinStockWarnings: GetMinStockWarningsUseCase
|
||||
private val getExpiryWarnings: GetExpiryWarningsUseCase
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(DashboardUiState())
|
||||
|
|
@ -54,7 +52,6 @@ internal class DashboardViewModel @Inject constructor(
|
|||
totalValue = calculateTotalValue(items),
|
||||
supplyRangeDays = calculateSupplyRange(items, totalDailyKcal),
|
||||
expiryWarnings = getExpiryWarnings(items),
|
||||
minStockWarnings = getMinStockWarnings(items),
|
||||
isLoading = false
|
||||
)
|
||||
}.collect { state ->
|
||||
|
|
|
|||
|
|
@ -177,11 +177,11 @@ internal fun ItemFormScreen(
|
|||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// kcal/100g
|
||||
// kcal/kg
|
||||
OutlinedTextField(
|
||||
value = uiState.kcalPer100g,
|
||||
onValueChange = viewModel::updateKcalPer100g,
|
||||
label = { Text("kcal / 100g") },
|
||||
value = uiState.kcalPerKg,
|
||||
onValueChange = viewModel::updateKcalPerKg,
|
||||
label = { Text("kcal / kg") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
|
|
@ -189,18 +189,6 @@ internal fun ItemFormScreen(
|
|||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Mindestbestand
|
||||
OutlinedTextField(
|
||||
value = uiState.minStock,
|
||||
onValueChange = viewModel::updateMinStock,
|
||||
label = { Text("Mindestbestand") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Notizen
|
||||
OutlinedTextField(
|
||||
value = uiState.notes,
|
||||
|
|
|
|||
|
|
@ -27,10 +27,9 @@ internal data class ItemFormUiState(
|
|||
val quantity: String = "",
|
||||
val unit: String = "",
|
||||
val unitPrice: String = "",
|
||||
val kcalPer100g: String = "",
|
||||
val kcalPerKg: String = "",
|
||||
val expiryDate: LocalDate? = null,
|
||||
val locationId: Int? = null,
|
||||
val minStock: String = "",
|
||||
val notes: String = "",
|
||||
val categories: List<CategoryEntity> = emptyList(),
|
||||
val locations: List<LocationEntity> = emptyList(),
|
||||
|
|
@ -109,10 +108,9 @@ internal class ItemFormViewModel @Inject constructor(
|
|||
quantity = item.quantity.toBigDecimal().stripTrailingZeros().toPlainString(),
|
||||
unit = item.unit,
|
||||
unitPrice = item.unitPrice.toBigDecimal().stripTrailingZeros().toPlainString(),
|
||||
kcalPer100g = item.kcalPer100g?.toString() ?: "",
|
||||
kcalPerKg = item.kcalPerKg?.toString() ?: "",
|
||||
expiryDate = item.expiryDate,
|
||||
locationId = item.locationId,
|
||||
minStock = item.minStock.toBigDecimal().stripTrailingZeros().toPlainString(),
|
||||
notes = item.notes,
|
||||
isLoading = false
|
||||
)
|
||||
|
|
@ -146,8 +144,8 @@ internal class ItemFormViewModel @Inject constructor(
|
|||
_uiState.update { it.copy(unitPrice = value) }
|
||||
}
|
||||
|
||||
fun updateKcalPer100g(value: String) {
|
||||
_uiState.update { it.copy(kcalPer100g = value) }
|
||||
fun updateKcalPerKg(value: String) {
|
||||
_uiState.update { it.copy(kcalPerKg = value) }
|
||||
}
|
||||
|
||||
fun updateExpiryDate(value: LocalDate?) {
|
||||
|
|
@ -158,10 +156,6 @@ internal class ItemFormViewModel @Inject constructor(
|
|||
_uiState.update { it.copy(locationId = value, validationErrors = it.validationErrors - "locationId") }
|
||||
}
|
||||
|
||||
fun updateMinStock(value: String) {
|
||||
_uiState.update { it.copy(minStock = value) }
|
||||
}
|
||||
|
||||
fun updateNotes(value: String) {
|
||||
_uiState.update { it.copy(notes = value) }
|
||||
}
|
||||
|
|
@ -183,10 +177,9 @@ internal class ItemFormViewModel @Inject constructor(
|
|||
quantity = state.quantity.toDouble(),
|
||||
unit = state.unit.trim(),
|
||||
unitPrice = state.unitPrice.toDoubleOrNull() ?: 0.0,
|
||||
kcalPer100g = state.kcalPer100g.toIntOrNull(),
|
||||
kcalPerKg = state.kcalPerKg.toIntOrNull(),
|
||||
expiryDate = state.expiryDate,
|
||||
locationId = state.locationId!!,
|
||||
minStock = state.minStock.toDoubleOrNull() ?: 0.0,
|
||||
notes = state.notes.trim(),
|
||||
lastUpdated = System.currentTimeMillis()
|
||||
)
|
||||
|
|
|
|||
|
|
@ -82,10 +82,9 @@ internal class ItemListViewModel @Inject constructor(
|
|||
quantity = item.quantity,
|
||||
unit = item.unit,
|
||||
unitPrice = 0.0,
|
||||
kcalPer100g = null,
|
||||
kcalPerKg = null,
|
||||
expiryDate = item.expiryDate,
|
||||
locationId = 0,
|
||||
minStock = 0.0,
|
||||
notes = "",
|
||||
lastUpdated = 0L
|
||||
)
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ 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)
|
||||
|
|
@ -84,19 +83,6 @@ internal fun WarningsScreen(
|
|||
}
|
||||
}
|
||||
|
||||
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)) }
|
||||
}
|
||||
}
|
||||
|
|
@ -145,32 +131,4 @@ private fun ExpiryWarningItem(warning: ExpiryWarning) {
|
|||
}
|
||||
}
|
||||
|
||||
@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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,22 +1,17 @@
|
|||
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
|
||||
get() = expiryWarnings.size
|
||||
|
||||
val hasWarnings: Boolean
|
||||
get() = hasExpiryWarnings || hasMinStockWarnings
|
||||
get() = hasExpiryWarnings
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ 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
|
||||
|
|
@ -16,8 +15,7 @@ import javax.inject.Inject
|
|||
@HiltViewModel
|
||||
internal class WarningsViewModel @Inject constructor(
|
||||
private val itemRepository: ItemRepository,
|
||||
private val getExpiryWarnings: GetExpiryWarningsUseCase,
|
||||
private val getMinStockWarnings: GetMinStockWarningsUseCase
|
||||
private val getExpiryWarnings: GetExpiryWarningsUseCase
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(WarningsUiState())
|
||||
|
|
@ -29,7 +27,6 @@ internal class WarningsViewModel @Inject constructor(
|
|||
_uiState.update {
|
||||
WarningsUiState(
|
||||
expiryWarnings = getExpiryWarnings(items),
|
||||
minStockWarnings = getMinStockWarnings(items),
|
||||
isLoading = false
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ class ImportExportRepositoryImplTest {
|
|||
@Test
|
||||
fun test_importFromJson_withValidJson_upsertAllEntities() = runBlocking {
|
||||
// Given
|
||||
val validJson = """{"version":1,"categories":[{"id":1,"name":"Lebensmittel"}],"locations":[{"id":1,"name":"Keller"}],"items":[{"id":"item1","name":"Konserve","categoryId":1,"quantity":2.0,"unit":"Stk","unitPrice":1.5,"kcalPer100g":null,"expiryDate":null,"locationId":1,"minStock":1.0,"notes":"","lastUpdated":0}],"settings":[{"key":"theme","value":"dark"}]}"""
|
||||
val validJson = """{"version":1,"categories":[{"id":1,"name":"Lebensmittel"}],"locations":[{"id":1,"name":"Keller"}],"items":[{"id":"item1","name":"Konserve","categoryId":1,"quantity":2.0,"unit":"Stk","unitPrice":1.5,"kcalPerKg":null,"expiryDate":null,"locationId":1,"notes":"","lastUpdated":0}],"settings":[{"key":"theme","value":"dark"}]}"""
|
||||
val categoryDao = FakeCategoryDao()
|
||||
val locationDao = FakeLocationDao()
|
||||
val itemDao = FakeItemDao()
|
||||
|
|
|
|||
|
|
@ -44,10 +44,9 @@ class JsonRoundtripTest {
|
|||
quantity = 10.0,
|
||||
unit = "Stk",
|
||||
unitPrice = 2.49,
|
||||
kcalPer100g = 180,
|
||||
kcalPerKg = 180,
|
||||
expiryDate = LocalDate.of(2027, 6, 15),
|
||||
locationId = 1,
|
||||
minStock = 5.0,
|
||||
notes = "Ravioli",
|
||||
lastUpdated = 1700000000L
|
||||
),
|
||||
|
|
@ -58,10 +57,9 @@ class JsonRoundtripTest {
|
|||
quantity = 3.0,
|
||||
unit = "Stk",
|
||||
unitPrice = 0.99,
|
||||
kcalPer100g = null,
|
||||
kcalPerKg = null,
|
||||
expiryDate = null,
|
||||
locationId = 2,
|
||||
minStock = 1.0,
|
||||
notes = "",
|
||||
lastUpdated = 1700000001L
|
||||
)
|
||||
|
|
@ -121,10 +119,9 @@ class JsonRoundtripTest {
|
|||
assertEquals(original.quantity, imported?.quantity)
|
||||
assertEquals(original.unit, imported?.unit)
|
||||
assertEquals(original.unitPrice, imported?.unitPrice)
|
||||
assertEquals(original.kcalPer100g, imported?.kcalPer100g)
|
||||
assertEquals(original.kcalPerKg, imported?.kcalPerKg)
|
||||
assertEquals(original.expiryDate, imported?.expiryDate)
|
||||
assertEquals(original.locationId, imported?.locationId)
|
||||
assertEquals(original.minStock, imported?.minStock)
|
||||
assertEquals(original.notes, imported?.notes)
|
||||
assertEquals(original.lastUpdated, imported?.lastUpdated)
|
||||
}
|
||||
|
|
@ -154,10 +151,9 @@ class JsonRoundtripTest {
|
|||
quantity = 1.0,
|
||||
unit = "Stk",
|
||||
unitPrice = 0.0,
|
||||
kcalPer100g = null,
|
||||
kcalPerKg = null,
|
||||
expiryDate = null,
|
||||
locationId = 1,
|
||||
minStock = 0.0,
|
||||
notes = "",
|
||||
lastUpdated = 0L
|
||||
)
|
||||
|
|
@ -176,7 +172,7 @@ class JsonRoundtripTest {
|
|||
// Then
|
||||
assertTrue(result.isSuccess)
|
||||
val imported = importItemDao.getItems().first()
|
||||
assertEquals(null, imported.kcalPer100g)
|
||||
assertEquals(null, imported.kcalPerKg)
|
||||
assertEquals(null, imported.expiryDate)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -121,10 +121,9 @@ internal fun buildItemEntity(id: String = "item1") = ItemEntity(
|
|||
quantity = 2.0,
|
||||
unit = "Stk",
|
||||
unitPrice = 1.5,
|
||||
kcalPer100g = null,
|
||||
kcalPerKg = null,
|
||||
expiryDate = null,
|
||||
locationId = 1,
|
||||
minStock = 1.0,
|
||||
notes = "",
|
||||
lastUpdated = 0L
|
||||
)
|
||||
|
|
|
|||
|
|
@ -92,10 +92,9 @@ private fun buildItem(
|
|||
quantity = 2.0,
|
||||
unit = "Stk",
|
||||
unitPrice = 1.5,
|
||||
kcalPer100g = null,
|
||||
kcalPerKg = null,
|
||||
expiryDate = null,
|
||||
locationId = locationId,
|
||||
minStock = 1.0,
|
||||
notes = "",
|
||||
lastUpdated = 0L
|
||||
)
|
||||
|
|
|
|||
|
|
@ -47,10 +47,9 @@ class SyncServiceImplTest {
|
|||
quantity = 5.0,
|
||||
unit = "Dose",
|
||||
unitPrice = 1.29,
|
||||
kcalPer100g = 100,
|
||||
kcalPerKg = 100,
|
||||
expiryDate = "2027-06-01",
|
||||
locationId = 1,
|
||||
minStock = 2.0,
|
||||
notes = "",
|
||||
lastUpdated = System.currentTimeMillis()
|
||||
)
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@ class CalculateSupplyRangeUseCaseTest {
|
|||
|
||||
@Test
|
||||
fun test_invoke_withKgItems_returnsCorrectDays() {
|
||||
// Given – 2 kg Reis à 350 kcal/100g = 7000 kcal
|
||||
// Given – 2 kg Reis à 3500 kcal/kg = 7000 kcal
|
||||
// 4000 kcal/Tag (2 × 2000) → 1.75 Tage
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", quantity = 2.0, unit = "kg", kcalPer100g = 350)
|
||||
buildTestItem(id = "1", quantity = 2.0, unit = "kg", kcalPerKg = 3500)
|
||||
)
|
||||
|
||||
// When
|
||||
|
|
@ -24,10 +24,10 @@ class CalculateSupplyRangeUseCaseTest {
|
|||
|
||||
@Test
|
||||
fun test_invoke_withGramItems_returnsCorrectDays() {
|
||||
// Given – 500 g Nudeln à 360 kcal/100g = 1800 kcal
|
||||
// Given – 500 g Nudeln à 3600 kcal/kg = 1800 kcal
|
||||
// 2000 kcal/Tag → 0.9 Tage
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", quantity = 500.0, unit = "g", kcalPer100g = 360)
|
||||
buildTestItem(id = "1", quantity = 500.0, unit = "g", kcalPerKg = 3600)
|
||||
)
|
||||
|
||||
// When
|
||||
|
|
@ -39,11 +39,11 @@ class CalculateSupplyRangeUseCaseTest {
|
|||
|
||||
@Test
|
||||
fun test_invoke_withMultipleItems_sumsTotalKcal() {
|
||||
// Given – 1 kg Reis (350 kcal/100g = 3500 kcal) + 500 g Nudeln (360 kcal/100g = 1800 kcal) = 5300 kcal
|
||||
// Given – 1 kg Reis (3500 kcal/kg = 3500 kcal) + 500 g Nudeln (3600 kcal/kg = 1800 kcal) = 5300 kcal
|
||||
// 4000 kcal/Tag → 1.325 Tage
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", quantity = 1.0, unit = "kg", kcalPer100g = 350),
|
||||
buildTestItem(id = "2", quantity = 500.0, unit = "g", kcalPer100g = 360)
|
||||
buildTestItem(id = "1", quantity = 1.0, unit = "kg", kcalPerKg = 3500),
|
||||
buildTestItem(id = "2", quantity = 500.0, unit = "g", kcalPerKg = 3600)
|
||||
)
|
||||
|
||||
// When
|
||||
|
|
@ -64,13 +64,13 @@ class CalculateSupplyRangeUseCaseTest {
|
|||
|
||||
@Test
|
||||
fun test_invoke_withNullKcal_skipsItem() {
|
||||
// Given – Item ohne kcalPer100g wird ignoriert
|
||||
// Given – Item ohne kcalPerKg wird ignoriert
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", quantity = 1.0, unit = "kg", kcalPer100g = null),
|
||||
buildTestItem(id = "2", quantity = 1.0, unit = "kg", kcalPer100g = 200)
|
||||
buildTestItem(id = "1", quantity = 1.0, unit = "kg", kcalPerKg = null),
|
||||
buildTestItem(id = "2", quantity = 1.0, unit = "kg", kcalPerKg = 2000)
|
||||
)
|
||||
|
||||
// When – Nur 1 kg à 200 kcal/100g = 2000 kcal, 2000 kcal/Tag = 1.0 Tage
|
||||
// When – Nur 1 kg à 2000 kcal/kg = 2000 kcal, 2000 kcal/Tag = 1.0 Tage
|
||||
val result = useCase(items, totalDailyKcal = 2000)
|
||||
|
||||
// Then
|
||||
|
|
@ -81,7 +81,7 @@ class CalculateSupplyRangeUseCaseTest {
|
|||
fun test_invoke_withNonWeightUnit_skipsItem() {
|
||||
// Given – "Stk" ist keine Gewichtseinheit → wird ignoriert
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", quantity = 5.0, unit = "Stk", kcalPer100g = 200)
|
||||
buildTestItem(id = "1", quantity = 5.0, unit = "Stk", kcalPerKg = 2000)
|
||||
)
|
||||
|
||||
// When
|
||||
|
|
@ -95,7 +95,7 @@ class CalculateSupplyRangeUseCaseTest {
|
|||
fun test_invoke_withZeroTotalDailyKcal_returnsZero() {
|
||||
// Given
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", quantity = 1.0, unit = "kg", kcalPer100g = 350)
|
||||
buildTestItem(id = "1", quantity = 1.0, unit = "kg", kcalPerKg = 3500)
|
||||
)
|
||||
|
||||
// When
|
||||
|
|
@ -107,10 +107,10 @@ class CalculateSupplyRangeUseCaseTest {
|
|||
|
||||
@Test
|
||||
fun test_invoke_withDefaultParameters_uses4000KcalPerDay() {
|
||||
// Given – 4 kg Reis à 350 kcal/100g = 14000 kcal
|
||||
// Given – 4 kg Reis à 3500 kcal/kg = 14000 kcal
|
||||
// Default: 4000 kcal/Tag → 3.5 Tage
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", quantity = 4.0, unit = "kg", kcalPer100g = 350)
|
||||
buildTestItem(id = "1", quantity = 4.0, unit = "kg", kcalPerKg = 3500)
|
||||
)
|
||||
|
||||
// When
|
||||
|
|
@ -122,10 +122,10 @@ class CalculateSupplyRangeUseCaseTest {
|
|||
|
||||
@Test
|
||||
fun test_invoke_withMgUnit_convertsCorrectly() {
|
||||
// Given – 500000 mg = 500 g à 200 kcal/100g = 1000 kcal
|
||||
// Given – 500000 mg = 500 g à 2000 kcal/kg = 1000 kcal
|
||||
// 1000 kcal/Tag → 1.0 Tag
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", quantity = 500000.0, unit = "mg", kcalPer100g = 200)
|
||||
buildTestItem(id = "1", quantity = 500000.0, unit = "mg", kcalPerKg = 2000)
|
||||
)
|
||||
|
||||
// When
|
||||
|
|
@ -137,12 +137,12 @@ class CalculateSupplyRangeUseCaseTest {
|
|||
|
||||
@Test
|
||||
fun test_invoke_withMixedUnits_onlyCountsWeightBased() {
|
||||
// Given – 1 kg (350 kcal/100g) + 5 Stk (ignored) + 2 L (ignored)
|
||||
// Given – 1 kg (3500 kcal/kg) + 5 Stk (ignored) + 2 L (ignored)
|
||||
// Total: 3500 kcal, 2000 kcal/Tag → 1.75 Tage
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", quantity = 1.0, unit = "kg", kcalPer100g = 350),
|
||||
buildTestItem(id = "2", quantity = 5.0, unit = "Stk", kcalPer100g = 100),
|
||||
buildTestItem(id = "3", quantity = 2.0, unit = "L", kcalPer100g = 45)
|
||||
buildTestItem(id = "1", quantity = 1.0, unit = "kg", kcalPerKg = 3500),
|
||||
buildTestItem(id = "2", quantity = 5.0, unit = "Stk", kcalPerKg = 1000),
|
||||
buildTestItem(id = "3", quantity = 2.0, unit = "L", kcalPerKg = 450)
|
||||
)
|
||||
|
||||
// When
|
||||
|
|
|
|||
|
|
@ -1,128 +0,0 @@
|
|||
package de.krisenvorrat.app.domain.usecase
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class GetMinStockWarningsUseCaseTest {
|
||||
|
||||
private val useCase = GetMinStockWarningsUseCase()
|
||||
|
||||
@Test
|
||||
fun test_invoke_withItemBelowMinStock_returnsWarning() {
|
||||
// Given
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", quantity = 2.0, minStock = 5.0)
|
||||
)
|
||||
|
||||
// When
|
||||
val result = useCase(items)
|
||||
|
||||
// Then
|
||||
assertEquals(1, result.size)
|
||||
assertEquals(3.0, result[0].deficit, 0.001)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_withItemAtMinStock_returnsNoWarning() {
|
||||
// Given
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", quantity = 5.0, minStock = 5.0)
|
||||
)
|
||||
|
||||
// When
|
||||
val result = useCase(items)
|
||||
|
||||
// Then
|
||||
assertTrue(result.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_withItemAboveMinStock_returnsNoWarning() {
|
||||
// Given
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", quantity = 10.0, minStock = 5.0)
|
||||
)
|
||||
|
||||
// When
|
||||
val result = useCase(items)
|
||||
|
||||
// Then
|
||||
assertTrue(result.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_withEmptyList_returnsEmptyList() {
|
||||
// Given / When
|
||||
val result = useCase(emptyList())
|
||||
|
||||
// Then
|
||||
assertTrue(result.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_withZeroMinStock_returnsNoWarning() {
|
||||
// Given
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", quantity = 0.0, minStock = 0.0)
|
||||
)
|
||||
|
||||
// When
|
||||
val result = useCase(items)
|
||||
|
||||
// Then
|
||||
assertTrue(result.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_withZeroQuantity_returnsWarning() {
|
||||
// Given
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", quantity = 0.0, minStock = 3.0)
|
||||
)
|
||||
|
||||
// When
|
||||
val result = useCase(items)
|
||||
|
||||
// Then
|
||||
assertEquals(1, result.size)
|
||||
assertEquals(3.0, result[0].deficit, 0.001)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_resultIsSortedByDeficitDescending() {
|
||||
// Given
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", quantity = 3.0, minStock = 5.0), // deficit 2
|
||||
buildTestItem(id = "2", quantity = 1.0, minStock = 10.0), // deficit 9
|
||||
buildTestItem(id = "3", quantity = 4.0, minStock = 7.0) // deficit 3
|
||||
)
|
||||
|
||||
// When
|
||||
val result = useCase(items)
|
||||
|
||||
// Then
|
||||
assertEquals(3, result.size)
|
||||
assertEquals("2", result[0].item.id) // deficit 9
|
||||
assertEquals("3", result[1].item.id) // deficit 3
|
||||
assertEquals("1", result[2].item.id) // deficit 2
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_withMixedItems_onlyReturnsBelowMinStock() {
|
||||
// Given
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", quantity = 2.0, minStock = 5.0), // below
|
||||
buildTestItem(id = "2", quantity = 10.0, minStock = 3.0), // above
|
||||
buildTestItem(id = "3", quantity = 5.0, minStock = 5.0), // equal
|
||||
buildTestItem(id = "4", quantity = 0.0, minStock = 1.0) // below
|
||||
)
|
||||
|
||||
// When
|
||||
val result = useCase(items)
|
||||
|
||||
// Then
|
||||
assertEquals(2, result.size)
|
||||
assertTrue(result.all { it.item.id in listOf("1", "4") })
|
||||
}
|
||||
}
|
||||
|
|
@ -10,10 +10,9 @@ internal fun buildTestItem(
|
|||
quantity: Double = 1.0,
|
||||
unit: String = "Stk",
|
||||
unitPrice: Double = 0.0,
|
||||
kcalPer100g: Int? = null,
|
||||
kcalPerKg: Int? = null,
|
||||
expiryDate: LocalDate? = null,
|
||||
locationId: Int = 1,
|
||||
minStock: Double = 0.0
|
||||
locationId: Int = 1
|
||||
) = ItemEntity(
|
||||
id = id,
|
||||
name = name,
|
||||
|
|
@ -21,10 +20,9 @@ internal fun buildTestItem(
|
|||
quantity = quantity,
|
||||
unit = unit,
|
||||
unitPrice = unitPrice,
|
||||
kcalPer100g = kcalPer100g,
|
||||
kcalPerKg = kcalPerKg,
|
||||
expiryDate = expiryDate,
|
||||
locationId = locationId,
|
||||
minStock = minStock,
|
||||
notes = "",
|
||||
lastUpdated = 0L
|
||||
)
|
||||
|
|
|
|||
|
|
@ -293,8 +293,8 @@ class CategoryListViewModelTest {
|
|||
fakeItemRepository.addItem(
|
||||
ItemEntity(
|
||||
id = "i1", name = "Reis", categoryId = 1, quantity = 1.0,
|
||||
unit = "kg", unitPrice = 0.0, kcalPer100g = null, expiryDate = null,
|
||||
locationId = 1, minStock = 0.0, notes = "", lastUpdated = 0L
|
||||
unit = "kg", unitPrice = 0.0, kcalPerKg = null, expiryDate = null,
|
||||
locationId = 1, notes = "", lastUpdated = 0L
|
||||
)
|
||||
)
|
||||
val category = CategoryEntity(id = 1, name = "Lebensmittel")
|
||||
|
|
@ -318,8 +318,8 @@ class CategoryListViewModelTest {
|
|||
fakeItemRepository.addItem(
|
||||
ItemEntity(
|
||||
id = "i1", name = "Reis", categoryId = 1, quantity = 1.0,
|
||||
unit = "kg", unitPrice = 0.0, kcalPer100g = null, expiryDate = null,
|
||||
locationId = 1, minStock = 0.0, notes = "", lastUpdated = 0L
|
||||
unit = "kg", unitPrice = 0.0, kcalPerKg = null, expiryDate = null,
|
||||
locationId = 1, notes = "", lastUpdated = 0L
|
||||
)
|
||||
)
|
||||
val category = CategoryEntity(id = 1, name = "Lebensmittel")
|
||||
|
|
@ -345,8 +345,8 @@ class CategoryListViewModelTest {
|
|||
fakeItemRepository.addItem(
|
||||
ItemEntity(
|
||||
id = "i1", name = "Reis", categoryId = 1, quantity = 1.0,
|
||||
unit = "kg", unitPrice = 0.0, kcalPer100g = null, expiryDate = null,
|
||||
locationId = 1, minStock = 0.0, notes = "", lastUpdated = 0L
|
||||
unit = "kg", unitPrice = 0.0, kcalPerKg = null, expiryDate = null,
|
||||
locationId = 1, notes = "", lastUpdated = 0L
|
||||
)
|
||||
)
|
||||
viewModel.showDeleteDialog(CategoryEntity(id = 1, name = "Lebensmittel"))
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import de.krisenvorrat.app.domain.usecase.CalculateCategorySummaryUseCase
|
|||
import de.krisenvorrat.app.domain.usecase.CalculateSupplyRangeUseCase
|
||||
import de.krisenvorrat.app.domain.usecase.CalculateTotalValueUseCase
|
||||
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
|
||||
|
|
@ -61,8 +60,7 @@ class DashboardViewModelTest {
|
|||
calculateCategorySummary = CalculateCategorySummaryUseCase(),
|
||||
calculateTotalValue = CalculateTotalValueUseCase(),
|
||||
calculateSupplyRange = CalculateSupplyRangeUseCase(),
|
||||
getExpiryWarnings = GetExpiryWarningsUseCase(),
|
||||
getMinStockWarnings = GetMinStockWarningsUseCase()
|
||||
getExpiryWarnings = GetExpiryWarningsUseCase()
|
||||
)
|
||||
|
||||
@Test
|
||||
|
|
@ -80,7 +78,6 @@ class DashboardViewModelTest {
|
|||
assertEquals(0.0, state.totalValue, 0.001)
|
||||
assertEquals(0.0, state.supplyRangeDays, 0.001)
|
||||
assertFalse(state.hasExpiryWarnings)
|
||||
assertFalse(state.hasMinStockWarnings)
|
||||
assertEquals(0, state.totalItemCount)
|
||||
}
|
||||
|
||||
|
|
@ -151,7 +148,7 @@ class DashboardViewModelTest {
|
|||
)
|
||||
fakeItemRepository.emit(
|
||||
listOf(
|
||||
buildTestItem(id = "a", quantity = 1000.0, unit = "g", kcalPer100g = 200)
|
||||
buildTestItem(id = "a", quantity = 1000.0, unit = "g", kcalPerKg = 2000)
|
||||
)
|
||||
)
|
||||
viewModel = createViewModel()
|
||||
|
|
@ -174,7 +171,7 @@ class DashboardViewModelTest {
|
|||
allZeroAgeGroups.toJson()
|
||||
)
|
||||
fakeItemRepository.emit(
|
||||
listOf(buildTestItem(id = "a", quantity = 1000.0, unit = "g", kcalPer100g = 400))
|
||||
listOf(buildTestItem(id = "a", quantity = 1000.0, unit = "g", kcalPerKg = 4000))
|
||||
)
|
||||
viewModel = createViewModel()
|
||||
|
||||
|
|
@ -203,39 +200,6 @@ class DashboardViewModelTest {
|
|||
assertEquals(1, state.expiryWarnings.size)
|
||||
}
|
||||
|
||||
@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)
|
||||
}
|
||||
|
||||
@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_init_withFarExpiryDate_noExpiryWarnings() = runTest(testDispatcher) {
|
||||
// Given – item expiring in 2 years (beyond WARNING_MONTHS=12)
|
||||
|
|
@ -282,10 +246,9 @@ private fun buildTestItem(
|
|||
quantity: Double = 1.0,
|
||||
unit: String = "Stk",
|
||||
unitPrice: Double = 0.0,
|
||||
kcalPer100g: Int? = null,
|
||||
kcalPerKg: Int? = null,
|
||||
expiryDate: LocalDate? = null,
|
||||
locationId: Int = 1,
|
||||
minStock: Double = 0.0
|
||||
locationId: Int = 1
|
||||
) = ItemEntity(
|
||||
id = id,
|
||||
name = name,
|
||||
|
|
@ -293,10 +256,9 @@ private fun buildTestItem(
|
|||
quantity = quantity,
|
||||
unit = unit,
|
||||
unitPrice = unitPrice,
|
||||
kcalPer100g = kcalPer100g,
|
||||
kcalPerKg = kcalPerKg,
|
||||
expiryDate = expiryDate,
|
||||
locationId = locationId,
|
||||
minStock = minStock,
|
||||
notes = "",
|
||||
lastUpdated = 0L
|
||||
)
|
||||
|
|
|
|||
|
|
@ -149,8 +149,8 @@ class ItemFormViewModelTest {
|
|||
fakeItemRepository.addItem(
|
||||
ItemEntity(
|
||||
id = "prev-1", name = "Alt", categoryId = 1, quantity = 1.0,
|
||||
unit = "kg", unitPrice = 0.0, kcalPer100g = null, expiryDate = null,
|
||||
locationId = 2, minStock = 0.0, notes = "", lastUpdated = 1000L
|
||||
unit = "kg", unitPrice = 0.0, kcalPerKg = null, expiryDate = null,
|
||||
locationId = 2, notes = "", lastUpdated = 1000L
|
||||
)
|
||||
)
|
||||
fakeLocationRepository.emit(
|
||||
|
|
@ -174,8 +174,8 @@ class ItemFormViewModelTest {
|
|||
fakeItemRepository.addItem(
|
||||
ItemEntity(
|
||||
id = "prev-1", name = "Alt", categoryId = 1, quantity = 1.0,
|
||||
unit = "kg", unitPrice = 0.0, kcalPer100g = null, expiryDate = null,
|
||||
locationId = 99, minStock = 0.0, notes = "", lastUpdated = 1000L
|
||||
unit = "kg", unitPrice = 0.0, kcalPerKg = null, expiryDate = null,
|
||||
locationId = 99, notes = "", lastUpdated = 1000L
|
||||
)
|
||||
)
|
||||
fakeLocationRepository.emit(
|
||||
|
|
@ -205,10 +205,9 @@ class ItemFormViewModelTest {
|
|||
quantity = 5.0,
|
||||
unit = "Stk",
|
||||
unitPrice = 2.5,
|
||||
kcalPer100g = 120,
|
||||
kcalPerKg = 120,
|
||||
expiryDate = LocalDate.of(2026, 12, 31),
|
||||
locationId = 2,
|
||||
minStock = 2.0,
|
||||
notes = "Bohnen",
|
||||
lastUpdated = 1000L
|
||||
)
|
||||
|
|
@ -227,10 +226,9 @@ class ItemFormViewModelTest {
|
|||
assertEquals("5", state.quantity)
|
||||
assertEquals("Stk", state.unit)
|
||||
assertEquals("2.5", state.unitPrice)
|
||||
assertEquals("120", state.kcalPer100g)
|
||||
assertEquals("120", state.kcalPerKg)
|
||||
assertEquals(LocalDate.of(2026, 12, 31), state.expiryDate)
|
||||
assertEquals(2, state.locationId)
|
||||
assertEquals("2", state.minStock)
|
||||
assertEquals("Bohnen", state.notes)
|
||||
}
|
||||
|
||||
|
|
@ -463,10 +461,9 @@ class ItemFormViewModelTest {
|
|||
quantity = 2.0,
|
||||
unit = "Stk",
|
||||
unitPrice = 0.0,
|
||||
kcalPer100g = null,
|
||||
kcalPerKg = null,
|
||||
expiryDate = null,
|
||||
locationId = 1,
|
||||
minStock = 0.0,
|
||||
notes = "",
|
||||
lastUpdated = 0L
|
||||
)
|
||||
|
|
|
|||
|
|
@ -230,10 +230,9 @@ private fun buildItemEntity(
|
|||
quantity = 2.0,
|
||||
unit = "Stk",
|
||||
unitPrice = 1.5,
|
||||
kcalPer100g = null,
|
||||
kcalPerKg = null,
|
||||
expiryDate = null,
|
||||
locationId = locationId,
|
||||
minStock = 1.0,
|
||||
notes = "",
|
||||
lastUpdated = 0L
|
||||
)
|
||||
|
|
|
|||
|
|
@ -292,8 +292,8 @@ class LocationListViewModelTest {
|
|||
fakeItemRepository.addItem(
|
||||
ItemEntity(
|
||||
id = "i1", name = "Reis", categoryId = 1, quantity = 1.0,
|
||||
unit = "kg", unitPrice = 0.0, kcalPer100g = null, expiryDate = null,
|
||||
locationId = 1, minStock = 0.0, notes = "", lastUpdated = 0L
|
||||
unit = "kg", unitPrice = 0.0, kcalPerKg = null, expiryDate = null,
|
||||
locationId = 1, notes = "", lastUpdated = 0L
|
||||
)
|
||||
)
|
||||
val location = LocationEntity(id = 1, name = "Keller")
|
||||
|
|
@ -317,8 +317,8 @@ class LocationListViewModelTest {
|
|||
fakeItemRepository.addItem(
|
||||
ItemEntity(
|
||||
id = "i1", name = "Reis", categoryId = 1, quantity = 1.0,
|
||||
unit = "kg", unitPrice = 0.0, kcalPer100g = null, expiryDate = null,
|
||||
locationId = 1, minStock = 0.0, notes = "", lastUpdated = 0L
|
||||
unit = "kg", unitPrice = 0.0, kcalPerKg = null, expiryDate = null,
|
||||
locationId = 1, notes = "", lastUpdated = 0L
|
||||
)
|
||||
)
|
||||
val location = LocationEntity(id = 1, name = "Keller")
|
||||
|
|
@ -344,8 +344,8 @@ class LocationListViewModelTest {
|
|||
fakeItemRepository.addItem(
|
||||
ItemEntity(
|
||||
id = "i1", name = "Wasser", categoryId = 1, quantity = 1.0,
|
||||
unit = "Flasche", unitPrice = 0.0, kcalPer100g = null, expiryDate = null,
|
||||
locationId = 1, minStock = 0.0, notes = "", lastUpdated = 0L
|
||||
unit = "Flasche", unitPrice = 0.0, kcalPerKg = null, expiryDate = null,
|
||||
locationId = 1, notes = "", lastUpdated = 0L
|
||||
)
|
||||
)
|
||||
viewModel.showDeleteDialog(LocationEntity(id = 1, name = "Keller"))
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ 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
|
||||
|
|
@ -41,8 +40,7 @@ class WarningsViewModelTest {
|
|||
|
||||
private fun createViewModel() = WarningsViewModel(
|
||||
itemRepository = fakeItemRepository,
|
||||
getExpiryWarnings = GetExpiryWarningsUseCase(),
|
||||
getMinStockWarnings = GetMinStockWarningsUseCase()
|
||||
getExpiryWarnings = GetExpiryWarningsUseCase()
|
||||
)
|
||||
|
||||
@Test
|
||||
|
|
@ -57,7 +55,6 @@ class WarningsViewModelTest {
|
|||
val state = viewModel.uiState.value
|
||||
assertFalse(state.isLoading)
|
||||
assertFalse(state.hasExpiryWarnings)
|
||||
assertFalse(state.hasMinStockWarnings)
|
||||
assertFalse(state.hasWarnings)
|
||||
assertEquals(0, state.totalWarningCount)
|
||||
}
|
||||
|
|
@ -82,48 +79,6 @@ class WarningsViewModelTest {
|
|||
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)
|
||||
|
|
@ -140,21 +95,6 @@ class WarningsViewModelTest {
|
|||
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
|
||||
|
|
@ -162,14 +102,15 @@ class WarningsViewModelTest {
|
|||
advanceUntilIdle()
|
||||
assertFalse(viewModel.uiState.value.hasWarnings)
|
||||
|
||||
// When – an item below min stock appears
|
||||
// When – an expiring item appears
|
||||
val expiryDate = LocalDate.now().plusMonths(3)
|
||||
fakeItemRepository.emit(
|
||||
listOf(buildTestItem(id = "a", quantity = 1.0, minStock = 5.0))
|
||||
listOf(buildTestItem(id = "a", expiryDate = expiryDate))
|
||||
)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Then
|
||||
assertTrue(viewModel.uiState.value.hasMinStockWarnings)
|
||||
assertTrue(viewModel.uiState.value.hasExpiryWarnings)
|
||||
assertEquals(1, viewModel.uiState.value.totalWarningCount)
|
||||
}
|
||||
}
|
||||
|
|
@ -183,10 +124,9 @@ private fun buildTestItem(
|
|||
quantity: Double = 1.0,
|
||||
unit: String = "Stk",
|
||||
unitPrice: Double = 0.0,
|
||||
kcalPer100g: Int? = null,
|
||||
kcalPerKg: Int? = null,
|
||||
expiryDate: LocalDate? = null,
|
||||
locationId: Int = 1,
|
||||
minStock: Double = 0.0
|
||||
locationId: Int = 1
|
||||
) = ItemEntity(
|
||||
id = id,
|
||||
name = name,
|
||||
|
|
@ -194,10 +134,9 @@ private fun buildTestItem(
|
|||
quantity = quantity,
|
||||
unit = unit,
|
||||
unitPrice = unitPrice,
|
||||
kcalPer100g = kcalPer100g,
|
||||
kcalPerKg = kcalPerKg,
|
||||
expiryDate = expiryDate,
|
||||
locationId = locationId,
|
||||
minStock = minStock,
|
||||
notes = "",
|
||||
lastUpdated = 0L
|
||||
)
|
||||
|
|
|
|||
|
|
@ -23,10 +23,9 @@ internal object Items : Table("items") {
|
|||
val quantity = double("quantity")
|
||||
val unit = varchar("unit", 50)
|
||||
val unitPrice = double("unit_price")
|
||||
val kcalPer100g = integer("kcal_per_100g").nullable()
|
||||
val kcalPerKg = integer("kcal_per_kg").nullable()
|
||||
val expiryDate = varchar("expiry_date", 10).nullable()
|
||||
val locationId = integer("location_id").references(Locations.id)
|
||||
val minStock = double("min_stock")
|
||||
val notes = text("notes")
|
||||
val lastUpdated = long("last_updated")
|
||||
|
||||
|
|
|
|||
|
|
@ -45,10 +45,9 @@ internal class InventoryRepository {
|
|||
it[quantity] = item.quantity
|
||||
it[unit] = item.unit
|
||||
it[unitPrice] = item.unitPrice
|
||||
it[kcalPer100g] = item.kcalPer100g
|
||||
it[kcalPerKg] = item.kcalPerKg
|
||||
it[expiryDate] = item.expiryDate
|
||||
it[locationId] = item.locationId
|
||||
it[minStock] = item.minStock
|
||||
it[notes] = item.notes
|
||||
it[lastUpdated] = item.lastUpdated
|
||||
}
|
||||
|
|
@ -87,10 +86,9 @@ internal class InventoryRepository {
|
|||
quantity = it[Items.quantity],
|
||||
unit = it[Items.unit],
|
||||
unitPrice = it[Items.unitPrice],
|
||||
kcalPer100g = it[Items.kcalPer100g],
|
||||
kcalPerKg = it[Items.kcalPerKg],
|
||||
expiryDate = it[Items.expiryDate],
|
||||
locationId = it[Items.locationId],
|
||||
minStock = it[Items.minStock],
|
||||
notes = it[Items.notes],
|
||||
lastUpdated = it[Items.lastUpdated]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -197,10 +197,9 @@ class ApplicationTest {
|
|||
quantity = 5.0,
|
||||
unit = "Stück",
|
||||
unitPrice = 3.99,
|
||||
kcalPer100g = 250,
|
||||
kcalPerKg = 250,
|
||||
expiryDate = "2027-06-15",
|
||||
locationId = 1,
|
||||
minStock = 2.0,
|
||||
notes = "Vollkornbrot in der Dose",
|
||||
lastUpdated = 1715000000L
|
||||
)
|
||||
|
|
|
|||
|
|
@ -102,10 +102,9 @@ class EndToEndSyncTest {
|
|||
quantity = 3.0,
|
||||
unit = "Stück",
|
||||
unitPrice = 1.50,
|
||||
kcalPer100g = null,
|
||||
kcalPerKg = null,
|
||||
expiryDate = null,
|
||||
locationId = 10,
|
||||
minStock = 1.0,
|
||||
notes = "",
|
||||
lastUpdated = 1715100000L
|
||||
)
|
||||
|
|
@ -174,10 +173,9 @@ class EndToEndSyncTest {
|
|||
quantity = 10.0,
|
||||
unit = "Stück",
|
||||
unitPrice = 3.99,
|
||||
kcalPer100g = 250,
|
||||
kcalPerKg = 250,
|
||||
expiryDate = "2027-06-15",
|
||||
locationId = 1,
|
||||
minStock = 2.0,
|
||||
notes = "Menge erhöht",
|
||||
lastUpdated = 1715200000L
|
||||
)
|
||||
|
|
@ -245,10 +243,9 @@ class EndToEndSyncTest {
|
|||
quantity = 5.0,
|
||||
unit = "Stück",
|
||||
unitPrice = 3.99,
|
||||
kcalPer100g = 250,
|
||||
kcalPerKg = 250,
|
||||
expiryDate = "2027-06-15",
|
||||
locationId = 1,
|
||||
minStock = 2.0,
|
||||
notes = "Vollkornbrot in der Dose",
|
||||
lastUpdated = 1715000000L
|
||||
),
|
||||
|
|
@ -259,10 +256,9 @@ class EndToEndSyncTest {
|
|||
quantity = 24.0,
|
||||
unit = "Liter",
|
||||
unitPrice = 0.49,
|
||||
kcalPer100g = 0,
|
||||
kcalPerKg = 0,
|
||||
expiryDate = "2028-01-01",
|
||||
locationId = 2,
|
||||
minStock = 12.0,
|
||||
notes = "Stilles Wasser",
|
||||
lastUpdated = 1715000000L
|
||||
)
|
||||
|
|
|
|||
|
|
@ -66,10 +66,9 @@ class InventoryRepositoryTest {
|
|||
assertEquals(5.0, item.quantity, 0.001)
|
||||
assertEquals("Stück", item.unit)
|
||||
assertEquals(3.99, item.unitPrice, 0.001)
|
||||
assertEquals(250, item.kcalPer100g)
|
||||
assertEquals(250, item.kcalPerKg)
|
||||
assertEquals("2027-06-15", item.expiryDate)
|
||||
assertEquals(1, item.locationId)
|
||||
assertEquals(2.0, item.minStock, 0.001)
|
||||
assertEquals("Vollkornbrot in der Dose", item.notes)
|
||||
assertEquals(1715000000L, item.lastUpdated)
|
||||
|
||||
|
|
@ -118,10 +117,9 @@ class InventoryRepositoryTest {
|
|||
quantity = 10.0,
|
||||
unit = "Stück",
|
||||
unitPrice = 1.50,
|
||||
kcalPer100g = null,
|
||||
kcalPerKg = null,
|
||||
expiryDate = null,
|
||||
locationId = 1,
|
||||
minStock = 5.0,
|
||||
notes = "",
|
||||
lastUpdated = 1715000000L
|
||||
)
|
||||
|
|
@ -135,7 +133,7 @@ class InventoryRepositoryTest {
|
|||
|
||||
// Then
|
||||
val item = result.items[0]
|
||||
assertNull(item.kcalPer100g)
|
||||
assertNull(item.kcalPerKg)
|
||||
assertNull(item.expiryDate)
|
||||
}
|
||||
|
||||
|
|
@ -156,10 +154,9 @@ class InventoryRepositoryTest {
|
|||
quantity = 5.0,
|
||||
unit = "Stück",
|
||||
unitPrice = 3.99,
|
||||
kcalPer100g = 250,
|
||||
kcalPerKg = 250,
|
||||
expiryDate = "2027-06-15",
|
||||
locationId = 1,
|
||||
minStock = 2.0,
|
||||
notes = "Vollkornbrot in der Dose",
|
||||
lastUpdated = 1715000000L
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,10 +10,9 @@ data class ItemDto(
|
|||
val quantity: Double,
|
||||
val unit: String,
|
||||
val unitPrice: Double,
|
||||
val kcalPer100g: Int?,
|
||||
val kcalPerKg: Int?,
|
||||
val expiryDate: String?,
|
||||
val locationId: Int,
|
||||
val minStock: Double,
|
||||
val notes: String,
|
||||
val lastUpdated: Long
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue