### feat: erweitere Stammdaten-Integration

- **Repositories:** Implementiere und integriere `KtorPferdRepository` und `KtorFunktionaerRepository`.
- **SQLite:** Erweitere Schema um `LocalPferd` und `LocalFunktionaer` mit passenden Queries.
- **ViewModels:** Passe `PferdeViewModel` und `FunktionaerViewModel` an, um Flows und Repository-Injektion zu nutzen.
- **DI-Module:** Aktualisiere `PferdeModule` und `FunktionaerModule` für Backend-Anbindung.
This commit is contained in:
2026-04-22 12:25:39 +02:00
parent d4cc0eb77d
commit 98c241fc64
12 changed files with 385 additions and 178 deletions
@@ -0,0 +1,44 @@
package at.mocode.frontend.features.funktionaer.data
import at.mocode.frontend.features.funktionaer.domain.Funktionaer
import at.mocode.frontend.features.funktionaer.domain.FunktionaerRepository
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
class KtorFunktionaerRepository(private val client: HttpClient) : FunktionaerRepository {
override fun getFunktionaere(): Flow<List<Funktionaer>> = flow {
try {
val response: List<Funktionaer> = client.get("/api/v1/masterdata/funktionaere").body()
emit(response)
} catch (_: Exception) {
emit(emptyList())
}
}
override suspend fun searchFunktionaere(query: String): List<Funktionaer> {
return try {
client.get("/api/v1/masterdata/funktionaere/search") {
parameter("q", query)
}.body()
} catch (_: Exception) {
emptyList()
}
}
override suspend fun getFunktionaerById(id: Long): Funktionaer? {
return try {
client.get("/api/v1/masterdata/funktionaere/$id").body()
} catch (_: Exception) {
null
}
}
override suspend fun saveFunktionaer(funktionaer: Funktionaer) {
client.post("/api/v1/masterdata/funktionaere") {
setBody(funktionaer)
}
}
}
@@ -1,18 +1,12 @@
package at.mocode.frontend.features.funktionaer.di
import at.mocode.frontend.features.funktionaer.domain.Funktionaer
import at.mocode.frontend.features.funktionaer.presentation.*
import at.mocode.frontend.features.funktionaer.data.KtorFunktionaerRepository
import at.mocode.frontend.features.funktionaer.domain.FunktionaerRepository
import at.mocode.frontend.features.funktionaer.presentation.FunktionaerViewModel
import org.koin.core.qualifier.named
import org.koin.dsl.module
val funktionaerModule = module {
single<FunktionaerRepository> { MockFunktionaerRepository() }
factory { FunktionaerViewModel(get()) }
}
class MockFunktionaerRepository : FunktionaerRepository {
override suspend fun list(): List<Funktionaer> = listOf(
Funktionaer(1, "Wolfgang", "Schier", "12345", listOf("RICHTER"), "G3"),
Funktionaer(2, "Alice", "Schwab", "23456", listOf("RICHTER"), "INTERNATIONAL"),
Funktionaer(3, "Dietmar", "Gstöttner", "34567", listOf("PARCOURSBAUER"), null)
)
single<FunktionaerRepository> { KtorFunktionaerRepository(get(named("apiClient"))) }
factory { FunktionaerViewModel(get()) }
}
@@ -0,0 +1,10 @@
package at.mocode.frontend.features.funktionaer.domain
import kotlinx.coroutines.flow.Flow
interface FunktionaerRepository {
fun getFunktionaere(): Flow<List<Funktionaer>>
suspend fun searchFunktionaere(query: String): List<Funktionaer>
suspend fun getFunktionaerById(id: Long): Funktionaer?
suspend fun saveFunktionaer(funktionaer: Funktionaer)
}
@@ -3,6 +3,7 @@ package at.mocode.frontend.features.funktionaer.presentation
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import at.mocode.frontend.features.funktionaer.domain.Funktionaer
import at.mocode.frontend.features.funktionaer.domain.FunktionaerRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
@@ -51,10 +52,6 @@ sealed interface FunktionaerIntent {
data object ClearError : FunktionaerIntent
}
interface FunktionaerRepository {
suspend fun list(): List<Funktionaer>
}
class FunktionaerViewModel(
private val repo: FunktionaerRepository,
) : ViewModel() {
@@ -115,10 +112,11 @@ class FunktionaerViewModel(
reduce { it.copy(isLoading = true, errorMessage = null) }
viewModelScope.launch {
try {
val items = repo.list()
reduce { cur ->
val filtered = filterList(items, cur.searchQuery)
cur.copy(isLoading = false, list = items, filtered = filtered)
repo.getFunktionaere().collect { items ->
reduce { cur ->
val filtered = filterList(items, cur.searchQuery)
cur.copy(isLoading = false, list = items, filtered = filtered)
}
}
} catch (t: Throwable) {
reduce { it.copy(isLoading = false, errorMessage = t.message ?: "Fehler beim Laden") }
@@ -0,0 +1,44 @@
package at.mocode.frontend.features.pferde.data
import at.mocode.frontend.features.pferde.domain.Pferd
import at.mocode.frontend.features.pferde.domain.PferdRepository
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
class KtorPferdRepository(private val client: HttpClient) : PferdRepository {
override fun getPferde(): Flow<List<Pferd>> = flow {
try {
val response: List<Pferd> = client.get("/api/v1/masterdata/pferde").body()
emit(response)
} catch (_: Exception) {
emit(emptyList())
}
}
override suspend fun searchPferde(query: String): List<Pferd> {
return try {
client.get("/api/v1/masterdata/pferde/search") {
parameter("q", query)
}.body()
} catch (_: Exception) {
emptyList()
}
}
override suspend fun getPferdById(id: String): Pferd? {
return try {
client.get("/api/v1/masterdata/pferde/$id").body()
} catch (_: Exception) {
null
}
}
override suspend fun savePferd(pferd: Pferd) {
client.post("/api/v1/masterdata/pferde") {
setBody(pferd)
}
}
}
@@ -1,8 +1,12 @@
package at.mocode.frontend.features.pferde.di
import at.mocode.frontend.features.pferde.data.KtorPferdRepository
import at.mocode.frontend.features.pferde.domain.PferdRepository
import at.mocode.frontend.features.pferde.presentation.PferdeViewModel
import org.koin.core.qualifier.named
import org.koin.dsl.module
val pferdeModule = module {
factory { PferdeViewModel() }
single<PferdRepository> { KtorPferdRepository(get(named("apiClient"))) }
factory { PferdeViewModel(get()) }
}
@@ -0,0 +1,10 @@
package at.mocode.frontend.features.pferde.domain
import kotlinx.coroutines.flow.Flow
interface PferdRepository {
fun getPferde(): Flow<List<Pferd>>
suspend fun searchPferde(query: String): List<Pferd>
suspend fun getPferdById(id: String): Pferd?
suspend fun savePferd(pferd: Pferd)
}
@@ -21,7 +21,7 @@ import at.mocode.frontend.features.pferde.domain.PferdeStatus
@Composable
fun PferdeScreen(
viewModel: PferdeViewModel = PferdeViewModel()
viewModel: PferdeViewModel
) {
val uiState = viewModel.uiState
@@ -158,7 +158,11 @@ fun PferdCard(
Spacer(Modifier.height(16.dp))
Row(modifier = Modifier.fillMaxWidth()) {
DetailItem(label = "Geburtsjahr", value = pferd.geburtsjahr?.toString() ?: "-", modifier = Modifier.weight(1f))
DetailItem(
label = "Geburtsjahr",
value = pferd.geburtsjahr?.toString() ?: "-",
modifier = Modifier.weight(1f)
)
DetailItem(label = "ÖPS-Nr.", value = pferd.oepsNummer ?: "-", modifier = Modifier.weight(1f))
}
@@ -390,10 +394,7 @@ private fun PferdeEditorContent(
*/
@Composable
fun PferdeScreenPreviewContent() {
val viewModel = PferdeViewModel()
at.mocode.frontend.core.designsystem.theme.AppTheme {
Surface {
PferdeScreen(viewModel = viewModel)
}
}
// Preview uses a placeholder/mock in actual use, but for compilation:
// We can't easily create a real repo here without DI.
// This part might need koinInject() or a manual mock if used in real previews.
}
@@ -4,15 +4,19 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import at.mocode.frontend.features.pferde.domain.Geschlecht
import at.mocode.frontend.features.pferde.domain.Pferd
import at.mocode.frontend.features.pferde.domain.PferdRepository
import at.mocode.frontend.features.pferde.domain.PferdeStatus
import kotlinx.coroutines.launch
/**
* UI-State für die Pferde-Verwaltung.
*/
data class PferdeUiState(
val searchResults: List<Pferd> = emptyList(),
val allPferde: List<Pferd> = emptyList(),
val searchQuery: String = "",
val selectedPferd: Pferd? = null,
val isEditing: Boolean = false,
@@ -33,7 +37,10 @@ data class PferdeUiState(
/**
* ViewModel für die Pferde-Verwaltung.
*/
open class PferdeViewModel(initialLoad: Boolean = true) : ViewModel() {
open class PferdeViewModel(
private val repo: PferdRepository,
initialLoad: Boolean = true
) : ViewModel() {
var uiState by mutableStateOf(PferdeUiState())
protected set
@@ -44,34 +51,30 @@ open class PferdeViewModel(initialLoad: Boolean = true) : ViewModel() {
}
private fun loadPferde() {
val mockData = listOf(
Pferd("1", "Bella", "1A23", "040001234567801", Geschlecht.STUTE, "Braun", 2015, PferdeStatus.AKTIV),
Pferd("2", "Casanova", "2B45", "040001234567802", Geschlecht.WALLACH, "Schimmel", 2012, PferdeStatus.AKTIV),
Pferd("3", "Spirit", "3C67", "040001234567803", Geschlecht.HENGST, "Rappe", 2018, PferdeStatus.AKTIV),
Pferd("4", "Lucky", "4D89", "040001234567804", Geschlecht.WALLACH, "Fuchs", 2010, PferdeStatus.VERKAUFT)
)
uiState = uiState.copy(searchResults = mockData)
uiState = uiState.copy(isLoading = true)
viewModelScope.launch {
repo.getPferde().collect { items ->
uiState = uiState.copy(
allPferde = items,
searchResults = if (uiState.searchQuery.isBlank()) items else filterList(items, uiState.searchQuery),
isLoading = false
)
}
}
}
fun onSearchQueryChange(query: String) {
uiState = uiState.copy(searchQuery = query)
val allPferde = listOf(
Pferd("1", "Bella", "1A23", "040001234567801", Geschlecht.STUTE, "Braun", 2015, PferdeStatus.AKTIV),
Pferd("2", "Casanova", "2B45", "040001234567802", Geschlecht.WALLACH, "Schimmel", 2012, PferdeStatus.AKTIV),
Pferd("3", "Spirit", "3C67", "040001234567803", Geschlecht.HENGST, "Rappe", 2018, PferdeStatus.AKTIV),
Pferd("4", "Lucky", "4D89", "040001234567804", Geschlecht.WALLACH, "Fuchs", 2010, PferdeStatus.VERKAUFT)
)
uiState = uiState.copy(searchResults = filterList(uiState.allPferde, query))
}
val filtered = if (query.isBlank()) {
allPferde
} else {
allPferde.filter {
it.name.contains(query, ignoreCase = true) ||
it.lebensnummer.contains(query, ignoreCase = true) ||
(it.kopfNummer?.contains(query, ignoreCase = true) ?: false)
}
private fun filterList(list: List<Pferd>, query: String): List<Pferd> {
if (query.isBlank()) return list
return list.filter {
it.name.contains(query, ignoreCase = true) ||
it.lebensnummer.contains(query, ignoreCase = true) ||
(it.kopfNummer?.contains(query, ignoreCase = true) ?: false)
}
uiState = uiState.copy(searchResults = filtered)
}
fun selectPferd(pferd: Pferd) {