### feat: implementiere SQLite-Integration und Repository-Refactoring
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Failing after 58s
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Successful in 6m0s
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Successful in 6m10s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Failing after 2m0s
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Successful in 1m55s

- Erstelle Persistenz-Layer mit SQLite-Tabellen für `Verein` und `Reiter` inkl. Queries.
- Entferne Mock-Daten in `ReiterViewModel` und nutze Repository-Injektion.
- Integriere neue Tabellen und Queries im `DesktopMasterdataRepository`.
- Erweitere `VeranstalterWizardViewModel` um lokale Suche mit SQLite-Queries.
- Harmonisiere Feldnamen (`remoteReiterResults`) über alle Module hinweg.
- Aktualisiere DI-Module (`VeranstalterModule`, `ReiterModule`, `DesktopModule`) mit SQLite-Injektionen.
- Refaktor UI-Komponenten und Screens (`ReiterScreen`, `StammdatenImportScreen`) mit neuer Logik.
This commit is contained in:
2026-04-22 02:20:51 +02:00
parent f18b002f4e
commit e0b1ce8836
22 changed files with 301 additions and 73 deletions
@@ -49,7 +49,7 @@ class ProfileOnboardingViewModel(
znsImportProvider.searchRemote(state.searchQuery)
state = state.copy(
isLoading = false,
searchResults = znsImportProvider.state.remoteReiter
searchResults = znsImportProvider.state.remoteReiterResults
)
} catch (e: Exception) {
state = state.copy(isLoading = false, error = "Fehler bei der ZNS-Suche: ${e.message}")
@@ -0,0 +1,22 @@
package at.mocode.frontend.features.reiter.data
import at.mocode.frontend.features.reiter.domain.Reiter
import at.mocode.frontend.features.reiter.domain.ReiterRepository
class FakeReiterRepository : ReiterRepository {
private val mockData = mutableListOf(
Reiter("1", "Stefan", "Möbius", "123456"),
Reiter("2", "Julia", "Reiterin", "654321"),
Reiter("3", "Max", "Mustermann", "112233")
)
override suspend fun getReiter(): Result<List<Reiter>> = Result.success(mockData)
override suspend fun searchReiter(query: String): Result<List<Reiter>> = Result.success(
mockData.filter { it.vorname.contains(query, true) || it.nachname.contains(query, true) }
)
override suspend fun findByZnsNr(znsNr: String): Reiter? = mockData.find { it.satznummer == znsNr }
override suspend fun saveReiter(reiter: Reiter): Result<Reiter> = Result.success(reiter)
}
@@ -1,8 +1,11 @@
package at.mocode.frontend.features.reiter.di
import at.mocode.frontend.features.reiter.data.FakeReiterRepository
import at.mocode.frontend.features.reiter.domain.ReiterRepository
import at.mocode.frontend.features.reiter.presentation.ReiterViewModel
import org.koin.dsl.module
val reiterModule = module {
factory { ReiterViewModel() }
single<ReiterRepository> { FakeReiterRepository() }
factory { ReiterViewModel(get<ReiterRepository>()) }
}
@@ -0,0 +1,8 @@
package at.mocode.frontend.features.reiter.domain
interface ReiterRepository {
suspend fun getReiter(): Result<List<Reiter>>
suspend fun searchReiter(query: String): Result<List<Reiter>>
suspend fun findByZnsNr(znsNr: String): Reiter?
suspend fun saveReiter(reiter: Reiter): Result<Reiter>
}
@@ -14,6 +14,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import at.mocode.frontend.core.designsystem.components.*
import at.mocode.frontend.core.designsystem.models.PlaceholderContent
import at.mocode.frontend.features.reiter.data.FakeReiterRepository
import at.mocode.frontend.features.reiter.domain.LizenzKlasse
import at.mocode.frontend.features.reiter.domain.Reiter
import at.mocode.frontend.features.reiter.domain.Sparte
@@ -58,7 +59,7 @@ fun ReiterScreen(
Spacer(Modifier.height(16.dp))
ReiterCard(
reiter = uiState.selectedReiter,
onEdit = { viewModel.selectReiter(uiState.selectedReiter) }
onEdit = { viewModel.selectReiter(uiState.selectedReiter!!) }
)
}
} else {
@@ -392,7 +393,7 @@ private fun ReiterEditorContent(
@Composable
fun ReiterScreenPreviewContent() {
val viewModel = ReiterViewModel().apply {
val viewModel = ReiterViewModel(FakeReiterRepository()).apply {
// Optional: Hier könnten Mock-Daten direkt gesetzt werden,
// falls das ViewModel dies unterstützt.
}
@@ -4,10 +4,9 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import at.mocode.frontend.features.reiter.domain.LizenzKlasse
import at.mocode.frontend.features.reiter.domain.Reiter
import at.mocode.frontend.features.reiter.domain.ReiterStatus
import at.mocode.frontend.features.reiter.domain.Sparte
import androidx.lifecycle.viewModelScope
import at.mocode.frontend.features.reiter.domain.*
import kotlinx.coroutines.launch
/**
* UI-State für die Reiter-Verwaltung.
@@ -36,32 +35,42 @@ data class ReiterUiState(
/**
* ViewModel für die Reiter-Verwaltung.
* In einem echten Szenario würden wir hier ein Repository injizieren.
*/
open class ReiterViewModel(initialLoad: Boolean = true) : ViewModel() {
open class ReiterViewModel(
private val repository: ReiterRepository,
initialLoad: Boolean = true
) : ViewModel() {
var uiState by mutableStateOf(ReiterUiState())
protected set
init {
if (initialLoad) {
// Initialer Load (Mock-Daten)
loadReiter()
}
}
private fun loadReiter() {
val mockData = listOf(
Reiter("1", "Stefan", "Möbius", "123456", LizenzKlasse.R2D2, Sparte.DRESSUR, ReiterStatus.AKTIV),
Reiter("2", "Julia", "Reiterin", "654321", LizenzKlasse.R1, Sparte.SPRINGEN, ReiterStatus.AKTIV),
Reiter("3", "Max", "Mustermann", "112233", LizenzKlasse.KEINE, Sparte.KEINE, ReiterStatus.GESPERRT),
Reiter("4", "Lisa", "Springen", "445566", LizenzKlasse.R3, Sparte.SPRINGEN, ReiterStatus.AKTIV)
)
uiState = uiState.copy(searchResults = mockData)
uiState = uiState.copy(isLoading = true)
viewModelScope.launch {
repository.getReiter().onSuccess { data ->
uiState = uiState.copy(searchResults = data, isLoading = false)
}.onFailure {
uiState = uiState.copy(isLoading = false)
}
}
}
fun onSearchQueryChange(query: String) {
uiState = uiState.copy(searchQuery = query)
// Hier würde die Filter-Logik greifen
if (query.length >= 3) {
viewModelScope.launch {
repository.searchReiter(query).onSuccess { data ->
uiState = uiState.copy(searchResults = data)
}
}
} else if (query.isEmpty()) {
loadReiter()
}
}
fun selectReiter(reiter: Reiter) {
@@ -3,6 +3,7 @@ package at.mocode.frontend.features.reiter.presentation
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import at.mocode.frontend.core.designsystem.preview.ComponentPreview
import at.mocode.frontend.features.reiter.data.FakeReiterRepository
import at.mocode.frontend.features.reiter.domain.LizenzKlasse
import at.mocode.frontend.features.reiter.domain.Reiter
import at.mocode.frontend.features.reiter.domain.ReiterStatus
@@ -11,7 +12,7 @@ import at.mocode.frontend.features.reiter.domain.Sparte
/**
* Hilf's-ViewModel für die Vorschau, um den Status direkt setzen zu können.
*/
private class PreviewReiterViewModel(initialState: ReiterUiState) : ReiterViewModel(initialLoad = false) {
private class PreviewReiterViewModel(initialState: ReiterUiState) : ReiterViewModel(FakeReiterRepository(), initialLoad = false) {
init {
uiState = initialState
}
@@ -20,7 +21,7 @@ private class PreviewReiterViewModel(initialState: ReiterUiState) : ReiterViewMo
@ComponentPreview
@Composable
fun PreviewReiterScreen_List() {
val viewModel = ReiterViewModel() // Nutzt die Mock-Daten aus dem init-Block
val viewModel = ReiterViewModel(FakeReiterRepository()) // Nutzt die Mock-Daten aus dem init-Block
MaterialTheme {
ReiterScreen(viewModel = viewModel)
}
@@ -28,6 +28,7 @@ kotlin {
sourceSets {
commonMain.dependencies {
implementation(projects.frontend.features.vereinFeature)
implementation(projects.frontend.core.localDb)
implementation(projects.frontend.core.designSystem)
implementation(projects.frontend.core.network)
implementation(projects.frontend.core.domain)
@@ -1,5 +1,8 @@
package at.mocode.frontend.features.veranstalter.di
import at.mocode.frontend.core.domain.repository.MasterdataRepository
import at.mocode.frontend.core.domain.zns.ZnsImportProvider
import at.mocode.frontend.core.localdb.AppDatabase
import at.mocode.frontend.features.veranstalter.data.remote.FakeVeranstalterRepository
import at.mocode.frontend.features.veranstalter.domain.VeranstalterRepository
import at.mocode.frontend.features.veranstalter.presentation.VeranstalterDetailViewModel
@@ -11,5 +14,12 @@ val veranstalterModule = module {
single<VeranstalterRepository> { FakeVeranstalterRepository() }
factory { VeranstalterViewModel(get()) }
factory { VeranstalterDetailViewModel(get()) }
factory { VeranstalterWizardViewModel(get(), get(), get()) }
factory {
VeranstalterWizardViewModel(
repo = get<VeranstalterRepository>(),
masterdataRepository = get<MasterdataRepository>(),
znsImportProvider = get<ZnsImportProvider>(),
db = get<AppDatabase>()
)
}
}
@@ -5,14 +5,12 @@ import at.mocode.frontend.core.domain.repository.MasterdataRepository
import at.mocode.frontend.core.domain.zns.ZnsImportProvider
import at.mocode.frontend.core.domain.zns.ZnsRemoteReiter
import at.mocode.frontend.core.domain.zns.ZnsRemoteVerein
import at.mocode.frontend.core.localdb.AppDatabase
import at.mocode.frontend.features.veranstalter.domain.Veranstalter
import at.mocode.frontend.features.veranstalter.domain.VeranstalterRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
data class VeranstalterWizardState(
val isLoading: Boolean = false,
@@ -63,8 +61,10 @@ sealed interface VeranstalterWizardIntent {
class VeranstalterWizardViewModel(
private val repo: VeranstalterRepository,
private val masterdataRepository: MasterdataRepository,
private val znsImportProvider: ZnsImportProvider
private val znsImportProvider: ZnsImportProvider,
private val db: AppDatabase
) : ViewModel() {
private val queries = db.meldestelleDbQueries
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val _state = MutableStateFlow(VeranstalterWizardState())
val state: StateFlow<VeranstalterWizardState> = _state
@@ -96,11 +96,22 @@ class VeranstalterWizardViewModel(
}
_state.value = _state.value.copy(isSearchingVerein = true)
scope.launch {
znsImportProvider.searchRemote(query)
_state.value = _state.value.copy(
isSearchingVerein = false,
vereinSearchResults = znsImportProvider.state.remoteResults
)
val localResults = queries.searchVereine(query, query).executeAsList().map { v ->
ZnsRemoteVerein(v.id.toString(), v.name, v.oebs_nummer, v.ort, v.bundesland)
}
if (localResults.isNotEmpty()) {
_state.value = _state.value.copy(
isSearchingVerein = false,
vereinSearchResults = localResults
)
} else {
znsImportProvider.searchRemote(query)
_state.value = _state.value.copy(
isSearchingVerein = false,
vereinSearchResults = znsImportProvider.state.remoteResults
)
}
}
}
@@ -112,11 +123,22 @@ class VeranstalterWizardViewModel(
}
_state.value = _state.value.copy(isSearchingReiter = true)
scope.launch {
znsImportProvider.searchRemote(query)
_state.value = _state.value.copy(
isSearchingReiter = false,
reiterSearchResults = znsImportProvider.state.remoteReiter
)
val localResults = queries.searchReiter(query, query, query).executeAsList().map { r ->
ZnsRemoteReiter(r.id.toString(), r.zns_nummer, r.nachname, r.vorname, null, "", r.nation, null)
}
if (localResults.isNotEmpty()) {
_state.value = _state.value.copy(
isSearchingReiter = false,
reiterSearchResults = localResults
)
} else {
znsImportProvider.searchRemote(query)
_state.value = _state.value.copy(
isSearchingReiter = false,
reiterSearchResults = znsImportProvider.state.remoteReiterResults
)
}
}
}
@@ -211,4 +233,9 @@ class VeranstalterWizardViewModel(
}
}
}
override fun onCleared() {
scope.cancel()
super.onCleared()
}
}
@@ -1,15 +1,13 @@
package at.mocode.frontend.features.verein.di
import at.mocode.frontend.features.verein.data.FakeVereinRepository
import at.mocode.frontend.features.verein.data.KtorVereinRepository
import at.mocode.frontend.features.verein.domain.VereinRepository
import at.mocode.frontend.features.verein.presentation.VereinViewModel
import org.koin.core.module.dsl.viewModelOf
import org.koin.dsl.module
val vereinFeatureModule = module {
// Desktop-App nutzt im Startup-Mode bevorzugt das Fake-Repository
// Kann bei Bedarf auf KtorVereinRepository umgestellt werden:
// single<VereinRepository> { KtorVereinRepository(get(named("apiClient"))) }
single<VereinRepository> { FakeVereinRepository() }
// Desktop-App nutzt nun das KtorVereinRepository (API) oder wir könnten ein SQLite Repository bauen
single<VereinRepository> { KtorVereinRepository(get()) }
viewModelOf(::VereinViewModel)
}
@@ -152,7 +152,7 @@ class ZnsImportViewModel(
val results = json.decodeFromString<List<ReiterRemoteDto>>(responseText)
state = state.copy(
isSearching = false,
remoteReiter = results.map {
remoteReiterResults = results.map {
ZnsRemoteReiter(it.reiterId, it.satznummer, it.nachname, it.vorname, it.reiterLizenz, it.lizenzKlasse)
}
)
@@ -198,7 +198,12 @@ class ZnsImportViewModel(
}
} else emptyList()
// 3. Pferde
state = state.copy(
remoteResults = vResults,
remoteReiterResults = rResults,
isSyncing = false,
isFinished = true
)
val pResponse: HttpResponse = httpClient.get("${NetworkConfig.baseUrl}/api/v1/masterdata/horse") {
parameter("limit", 1000)
if (token != null) header(HttpHeaders.Authorization, "Bearer $token")