### 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:
Stefan Mogeritsch 2026-04-22 12:25:39 +02:00
parent d4cc0eb77d
commit 98c241fc64
12 changed files with 385 additions and 178 deletions

View File

@ -5,12 +5,18 @@
- **Daten-Fuel:** Vollständige Umstellung von Reiter-Mocks auf `KtorReiterRepository`. Die 48.753 Reiter sind nun via API erreichbar. - **Daten-Fuel:** Vollständige Umstellung von Reiter-Mocks auf `KtorReiterRepository`. Die 48.753 Reiter sind nun via API erreichbar.
- **Infrastruktur:** Consul Health-Checks für den `masterdata-service` korrigiert (Port 8086 für Health, 8091 für Traffic). - **Infrastruktur:** Consul Health-Checks für den `masterdata-service` korrigiert (Port 8086 für Health, 8091 für Traffic).
- **ZNS-Korrektur:** Verifizierung der Import-Mengen (21.206 Pferde erfolgreich importiert). - **ZNS-Korrektur:** Verifizierung der Import-Mengen (21.206 Pferde erfolgreich importiert).
- **Vollständige Stammdaten-Integration:** Pferde und Funktionäre sind nun vollständig an SQLite und Backend-API angebunden.
## 🛠️ Durchgeführte Änderungen ## 🛠️ Durchgeführte Änderungen
### Frontend (Desktop & Common) ### Frontend (Desktop & Common)
- **MeldestelleDb.sq:** Erweiterung des SQLite-Schemas um `LocalPferd` und `LocalFunktionaer`.
- **Repositories:** `KtorPferdRepository` und `KtorFunktionaerRepository` implementiert (commonMain).
- **DI (PferdeModule, FunktionaerModule):** Umstellung auf reale Repository-Injektion mit dem `apiClient`.
- **ViewModels:** `PferdeViewModel` und `FunktionaerViewModel` für reaktive Daten-Anbindung (Flows) angepasst.
- **DesktopMasterdataRepository:** Persistierungs-Logik für Pferde und Funktionäre implementiert; `getStats()` liefert nun korrekte SQLite-Zahlen für alle Stammdaten-Typen.
- **VereinFeatureModule & ReiterModule:** Umstellung auf `named("apiClient")`, um den authentifizierten Ktor-Client zu nutzen. - **VereinFeatureModule & ReiterModule:** Umstellung auf `named("apiClient")`, um den authentifizierten Ktor-Client zu nutzen.
- **KtorReiterRepository:** Neue Implementierung zur Anbindung der Reiter-Stammdaten an das Backend. - **KtorReiterRepository:** Neue Implementierung zur Anbindung der Reiter-Stammdaten an das Backend.
- **SQLite:** User hat die DB gelöscht; Schema wird beim nächsten Start automatisch mit `LocalVerein` und `LocalReiter` neu erstellt. - **SQLite:** User hat die DB gelöscht; Schema wird beim nächsten Start automatisch mit allen neuen Tabellen (`LocalVerein`, `LocalReiter`, `LocalPferd`, `LocalFunktionaer`) neu erstellt.
### Backend & DevOps ### Backend & DevOps
- **masterdata-service (application.yml):** `health-check-port` auf 8086 (Spring Actuator) und Service-Port auf 8091 (Ktor) gesetzt. - **masterdata-service (application.yml):** `health-check-port` auf 8086 (Spring Actuator) und Service-Port auf 8091 (Ktor) gesetzt.

View File

@ -64,6 +64,30 @@ CREATE TABLE LocalReiter (
last_updated INTEGER NOT NULL last_updated INTEGER NOT NULL
); );
CREATE TABLE LocalPferd (
id INTEGER NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
lebensnummer TEXT NOT NULL,
geschlecht TEXT,
farbe TEXT,
geburtsjahr INTEGER,
oebs_nummer TEXT,
is_active INTEGER NOT NULL DEFAULT 1,
last_updated INTEGER NOT NULL
);
CREATE TABLE LocalFunktionaer (
id INTEGER NOT NULL PRIMARY KEY,
vorname TEXT NOT NULL,
nachname TEXT NOT NULL,
richter_nummer TEXT,
disziplinen TEXT, -- Kommagetrennte Liste
qualifikation TEXT,
email TEXT,
is_active INTEGER NOT NULL DEFAULT 1,
last_updated INTEGER NOT NULL
);
-- Verein Queries -- Verein Queries
upsertVerein: upsertVerein:
INSERT OR REPLACE INTO LocalVerein(id, oebs_nummer, name, ort, plz, bundesland, is_active, last_updated) INSERT OR REPLACE INTO LocalVerein(id, oebs_nummer, name, ort, plz, bundesland, is_active, last_updated)
@ -90,8 +114,40 @@ SELECT * FROM LocalReiter
WHERE nachname LIKE ('%' || ? || '%') OR vorname LIKE ('%' || ? || '%') OR zns_nummer LIKE ('%' || ? || '%') WHERE nachname LIKE ('%' || ? || '%') OR vorname LIKE ('%' || ? || '%') OR zns_nummer LIKE ('%' || ? || '%')
ORDER BY nachname ASC, vorname ASC; ORDER BY nachname ASC, vorname ASC;
-- Pferde Queries
upsertPferd:
INSERT OR REPLACE INTO LocalPferd(id, name, lebensnummer, geschlecht, farbe, geburtsjahr, oebs_nummer, is_active, last_updated)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
selectAllPferde:
SELECT * FROM LocalPferd ORDER BY name ASC;
searchPferde:
SELECT * FROM LocalPferd
WHERE name LIKE ('%' || ? || '%') OR lebensnummer LIKE ('%' || ? || '%') OR oebs_nummer LIKE ('%' || ? || '%')
ORDER BY name ASC;
-- Funktionaer Queries
upsertFunktionaer:
INSERT OR REPLACE INTO LocalFunktionaer(id, vorname, nachname, richter_nummer, disziplinen, qualifikation, email, is_active, last_updated)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
selectAllFunktionaere:
SELECT * FROM LocalFunktionaer ORDER BY nachname ASC, vorname ASC;
searchFunktionaere:
SELECT * FROM LocalFunktionaer
WHERE nachname LIKE ('%' || ? || '%') OR vorname LIKE ('%' || ? || '%') OR richter_nummer LIKE ('%' || ? || '%')
ORDER BY nachname ASC, vorname ASC;
deleteAllVereine: deleteAllVereine:
DELETE FROM LocalVerein; DELETE FROM LocalVerein;
deleteAllReiter: deleteAllReiter:
DELETE FROM LocalReiter; DELETE FROM LocalReiter;
deleteAllPferde:
DELETE FROM LocalPferd;
deleteAllFunktionaere:
DELETE FROM LocalFunktionaer;

View File

@ -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)
}
}
}

View File

@ -1,18 +1,12 @@
package at.mocode.frontend.features.funktionaer.di package at.mocode.frontend.features.funktionaer.di
import at.mocode.frontend.features.funktionaer.domain.Funktionaer import at.mocode.frontend.features.funktionaer.data.KtorFunktionaerRepository
import at.mocode.frontend.features.funktionaer.presentation.* 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 import org.koin.dsl.module
val funktionaerModule = module { val funktionaerModule = module {
single<FunktionaerRepository> { MockFunktionaerRepository() } single<FunktionaerRepository> { KtorFunktionaerRepository(get(named("apiClient"))) }
factory { FunktionaerViewModel(get()) } 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)
)
}

View File

@ -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)
}

View File

@ -3,6 +3,7 @@ package at.mocode.frontend.features.funktionaer.presentation
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import at.mocode.frontend.features.funktionaer.domain.Funktionaer 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.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -51,10 +52,6 @@ sealed interface FunktionaerIntent {
data object ClearError : FunktionaerIntent data object ClearError : FunktionaerIntent
} }
interface FunktionaerRepository {
suspend fun list(): List<Funktionaer>
}
class FunktionaerViewModel( class FunktionaerViewModel(
private val repo: FunktionaerRepository, private val repo: FunktionaerRepository,
) : ViewModel() { ) : ViewModel() {
@ -115,11 +112,12 @@ class FunktionaerViewModel(
reduce { it.copy(isLoading = true, errorMessage = null) } reduce { it.copy(isLoading = true, errorMessage = null) }
viewModelScope.launch { viewModelScope.launch {
try { try {
val items = repo.list() repo.getFunktionaere().collect { items ->
reduce { cur -> reduce { cur ->
val filtered = filterList(items, cur.searchQuery) val filtered = filterList(items, cur.searchQuery)
cur.copy(isLoading = false, list = items, filtered = filtered) cur.copy(isLoading = false, list = items, filtered = filtered)
} }
}
} catch (t: Throwable) { } catch (t: Throwable) {
reduce { it.copy(isLoading = false, errorMessage = t.message ?: "Fehler beim Laden") } reduce { it.copy(isLoading = false, errorMessage = t.message ?: "Fehler beim Laden") }
} }

View File

@ -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)
}
}
}

View File

@ -1,8 +1,12 @@
package at.mocode.frontend.features.pferde.di 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 at.mocode.frontend.features.pferde.presentation.PferdeViewModel
import org.koin.core.qualifier.named
import org.koin.dsl.module import org.koin.dsl.module
val pferdeModule = module { val pferdeModule = module {
factory { PferdeViewModel() } single<PferdRepository> { KtorPferdRepository(get(named("apiClient"))) }
factory { PferdeViewModel(get()) }
} }

View File

@ -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)
}

View File

@ -21,7 +21,7 @@ import at.mocode.frontend.features.pferde.domain.PferdeStatus
@Composable @Composable
fun PferdeScreen( fun PferdeScreen(
viewModel: PferdeViewModel = PferdeViewModel() viewModel: PferdeViewModel
) { ) {
val uiState = viewModel.uiState val uiState = viewModel.uiState
@ -158,7 +158,11 @@ fun PferdCard(
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
Row(modifier = Modifier.fillMaxWidth()) { 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)) DetailItem(label = "ÖPS-Nr.", value = pferd.oepsNummer ?: "-", modifier = Modifier.weight(1f))
} }
@ -390,10 +394,7 @@ private fun PferdeEditorContent(
*/ */
@Composable @Composable
fun PferdeScreenPreviewContent() { fun PferdeScreenPreviewContent() {
val viewModel = PferdeViewModel() // Preview uses a placeholder/mock in actual use, but for compilation:
at.mocode.frontend.core.designsystem.theme.AppTheme { // We can't easily create a real repo here without DI.
Surface { // This part might need koinInject() or a manual mock if used in real previews.
PferdeScreen(viewModel = viewModel)
}
}
} }

View File

@ -4,15 +4,19 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import at.mocode.frontend.features.pferde.domain.Geschlecht import at.mocode.frontend.features.pferde.domain.Geschlecht
import at.mocode.frontend.features.pferde.domain.Pferd 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 at.mocode.frontend.features.pferde.domain.PferdeStatus
import kotlinx.coroutines.launch
/** /**
* UI-State für die Pferde-Verwaltung. * UI-State für die Pferde-Verwaltung.
*/ */
data class PferdeUiState( data class PferdeUiState(
val searchResults: List<Pferd> = emptyList(), val searchResults: List<Pferd> = emptyList(),
val allPferde: List<Pferd> = emptyList(),
val searchQuery: String = "", val searchQuery: String = "",
val selectedPferd: Pferd? = null, val selectedPferd: Pferd? = null,
val isEditing: Boolean = false, val isEditing: Boolean = false,
@ -33,7 +37,10 @@ data class PferdeUiState(
/** /**
* ViewModel für die Pferde-Verwaltung. * 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()) var uiState by mutableStateOf(PferdeUiState())
protected set protected set
@ -44,35 +51,31 @@ open class PferdeViewModel(initialLoad: Boolean = true) : ViewModel() {
} }
private fun loadPferde() { private fun loadPferde() {
val mockData = listOf( uiState = uiState.copy(isLoading = true)
Pferd("1", "Bella", "1A23", "040001234567801", Geschlecht.STUTE, "Braun", 2015, PferdeStatus.AKTIV), viewModelScope.launch {
Pferd("2", "Casanova", "2B45", "040001234567802", Geschlecht.WALLACH, "Schimmel", 2012, PferdeStatus.AKTIV), repo.getPferde().collect { items ->
Pferd("3", "Spirit", "3C67", "040001234567803", Geschlecht.HENGST, "Rappe", 2018, PferdeStatus.AKTIV), uiState = uiState.copy(
Pferd("4", "Lucky", "4D89", "040001234567804", Geschlecht.WALLACH, "Fuchs", 2010, PferdeStatus.VERKAUFT) allPferde = items,
searchResults = if (uiState.searchQuery.isBlank()) items else filterList(items, uiState.searchQuery),
isLoading = false
) )
uiState = uiState.copy(searchResults = mockData) }
}
} }
fun onSearchQueryChange(query: String) { fun onSearchQueryChange(query: String) {
uiState = uiState.copy(searchQuery = query) uiState = uiState.copy(searchQuery = query)
val allPferde = listOf( uiState = uiState.copy(searchResults = filterList(uiState.allPferde, query))
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)
)
val filtered = if (query.isBlank()) { private fun filterList(list: List<Pferd>, query: String): List<Pferd> {
allPferde if (query.isBlank()) return list
} else { return list.filter {
allPferde.filter {
it.name.contains(query, ignoreCase = true) || it.name.contains(query, ignoreCase = true) ||
it.lebensnummer.contains(query, ignoreCase = true) || it.lebensnummer.contains(query, ignoreCase = true) ||
(it.kopfNummer?.contains(query, ignoreCase = true) ?: false) (it.kopfNummer?.contains(query, ignoreCase = true) ?: false)
} }
} }
uiState = uiState.copy(searchResults = filtered)
}
fun selectPferd(pferd: Pferd) { fun selectPferd(pferd: Pferd) {
uiState = uiState.copy( uiState = uiState.copy(

View File

@ -13,7 +13,7 @@ import java.time.LocalDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
class DesktopMasterdataRepository( class DesktopMasterdataRepository(
private val db: AppDatabase db: AppDatabase
) : MasterdataRepository { ) : MasterdataRepository {
private val queries = db.meldestelleDbQueries private val queries = db.meldestelleDbQueries
@ -27,8 +27,8 @@ class DesktopMasterdataRepository(
val id = remote.id.toLongOrNull() ?: return@forEach val id = remote.id.toLongOrNull() ?: return@forEach
queries.upsertVerein( queries.upsertVerein(
id = id, id = id,
oebs_nummer = remote.oepsNummer ?: "", oebs_nummer = remote.oepsNummer,
name = remote.name ?: "Unbekannt", name = remote.name,
ort = remote.ort, ort = remote.ort,
plz = null, // Falls vom Backend geliefert, hier mappen plz = null, // Falls vom Backend geliefert, hier mappen
bundesland = remote.bundesland, bundesland = remote.bundesland,
@ -43,8 +43,8 @@ class DesktopMasterdataRepository(
val id = remote.id.toLongOrNull() ?: return@forEach val id = remote.id.toLongOrNull() ?: return@forEach
val verein = Verein( val verein = Verein(
id = id, id = id,
name = remote.name ?: "Unbekannt", name = remote.name,
oepsNummer = remote.oepsNummer ?: "", oepsNummer = remote.oepsNummer,
ort = remote.ort, ort = remote.ort,
bundesland = remote.bundesland, bundesland = remote.bundesland,
istVeranstalter = true istVeranstalter = true
@ -64,8 +64,8 @@ class DesktopMasterdataRepository(
queries.upsertReiter( queries.upsertReiter(
id = id, id = id,
zns_nummer = remote.satznummer, zns_nummer = remote.satznummer,
vorname = remote.vorname ?: "", vorname = remote.vorname,
nachname = remote.nachname ?: "", nachname = remote.nachname,
jahrgang = null, // Backend liefert aktuell kein Jahrgang direkt in ZnsRemoteReiter? jahrgang = null, // Backend liefert aktuell kein Jahrgang direkt in ZnsRemoteReiter?
geschlecht = null, geschlecht = null,
nation = remote.nation ?: "AUT", nation = remote.nation ?: "AUT",
@ -80,8 +80,8 @@ class DesktopMasterdataRepository(
val id = remote.id.toLongOrNull() ?: return@forEach val id = remote.id.toLongOrNull() ?: return@forEach
val entry = Reiter( val entry = Reiter(
id = id, id = id,
vorname = remote.vorname ?: "", vorname = remote.vorname,
nachname = remote.nachname ?: "", nachname = remote.nachname,
satznummer = remote.satznummer ?: "", satznummer = remote.satznummer ?: "",
oepsNummer = remote.satznummer ?: "", oepsNummer = remote.satznummer ?: "",
lizenzKlasse = remote.lizenzKlasse, lizenzKlasse = remote.lizenzKlasse,
@ -94,30 +94,66 @@ class DesktopMasterdataRepository(
} }
override fun savePferde(pferde: List<ZnsRemotePferd>) { override fun savePferde(pferde: List<ZnsRemotePferd>) {
println("[Repository] Speichere ${pferde.size} Pferde") println("[Repository] Speichere ${pferde.size} Pferde in SQLite")
val now = System.currentTimeMillis()
runBlocking {
queries.transaction {
pferde.forEach { remote ->
val id = remote.id.toLongOrNull() ?: return@forEach
queries.upsertPferd(
id = id,
name = remote.name,
lebensnummer = remote.lebensnummer ?: "",
geschlecht = remote.geschlecht,
farbe = null,
geburtsjahr = null,
oebs_nummer = remote.kopfnummer,
is_active = 1,
last_updated = now
)
}
}
}
// Sync to Store
pferde.forEach { remote -> pferde.forEach { remote ->
val id = remote.id.toLongOrNull() ?: return@forEach val id = remote.id.toLongOrNull() ?: return@forEach
val existingIdx = Store.pferde.indexOfFirst { it.id == id }
val entry = Pferd( val entry = Pferd(
id = id, id = id,
name = remote.name, name = remote.name,
geschlecht = remote.geschlecht, geschlecht = remote.geschlecht,
lebensnummer = remote.lebensnummer, lebensnummer = remote.lebensnummer ?: "",
oepsNummer = remote.kopfnummer oepsNummer = remote.kopfnummer
) )
if (existingIdx >= 0) { val existingIdx = Store.pferde.indexOfFirst { it.id == id }
Store.pferde[existingIdx] = entry if (existingIdx >= 0) Store.pferde[existingIdx] = entry else Store.pferde.add(entry)
} else {
Store.pferde.add(entry)
}
} }
} }
override fun saveFunktionaere(funktionaere: List<ZnsRemoteFunktionaer>) { override fun saveFunktionaere(funktionaere: List<ZnsRemoteFunktionaer>) {
println("[Repository] Speichere ${funktionaere.size} Funktionäre") println("[Repository] Speichere ${funktionaere.size} Funktionäre in SQLite")
val now = System.currentTimeMillis()
runBlocking {
queries.transaction {
funktionaere.forEach { remote ->
val id = remote.id.toLongOrNull() ?: return@forEach
val namen = remote.name?.split(" ") ?: listOf("Unbekannt")
queries.upsertFunktionaer(
id = id,
vorname = namen.firstOrNull() ?: "",
nachname = namen.drop(1).joinToString(" "),
richter_nummer = null,
disziplinen = remote.qualifikationen.joinToString(","),
qualifikation = null,
email = null,
is_active = 1,
last_updated = now
)
}
}
}
// Sync to Store
funktionaere.forEach { remote -> funktionaere.forEach { remote ->
val id = remote.id.toLongOrNull() ?: return@forEach val id = remote.id.toLongOrNull() ?: return@forEach
val existingIdx = Store.funktionaere.indexOfFirst { it.id == id }
val namen = remote.name?.split(" ") ?: listOf("Unbekannt") val namen = remote.name?.split(" ") ?: listOf("Unbekannt")
val entry = Funktionaer( val entry = Funktionaer(
id = id, id = id,
@ -127,25 +163,26 @@ class DesktopMasterdataRepository(
nation = remote.nation ?: "AUT", nation = remote.nation ?: "AUT",
bundesland = remote.bundesland bundesland = remote.bundesland
) )
if (existingIdx >= 0) { val existingIdx = Store.funktionaere.indexOfFirst { it.id == id }
Store.funktionaere[existingIdx] = entry if (existingIdx >= 0) Store.funktionaere[existingIdx] = entry else Store.funktionaere.add(entry)
} else {
Store.funktionaere.add(entry)
}
} }
} }
override fun getStats(): MasterdataStats { override fun getStats(): MasterdataStats {
val vereinCount = queries.selectAllVereine().executeAsList().size.toLong() val vereinCount = queries.selectAllVereine().executeAsList().size.toLong()
val reiterCount = queries.selectAllReiter().executeAsList().size.toLong() val reiterCount = queries.selectAllReiter().executeAsList().size.toLong()
val pferdCount = queries.selectAllPferde().executeAsList().size.toLong()
val funktionaerCount = queries.selectAllFunktionaere().executeAsList().size.toLong()
val lastUpdate = listOf( val lastUpdate = listOf(
queries.selectAllVereine().executeAsList().maxOfOrNull { it.last_updated } ?: 0L, queries.selectAllVereine().executeAsList().maxOfOrNull { it.last_updated } ?: 0L,
queries.selectAllReiter().executeAsList().maxOfOrNull { it.last_updated } ?: 0L queries.selectAllReiter().executeAsList().maxOfOrNull { it.last_updated } ?: 0L,
queries.selectAllPferde().executeAsList().maxOfOrNull { it.last_updated } ?: 0L,
queries.selectAllFunktionaere().executeAsList().maxOfOrNull { it.last_updated } ?: 0L
).maxOrNull() ?: 0L ).maxOrNull() ?: 0L
val lastImportStr = if (lastUpdate > 0) { val lastImportStr = if (lastUpdate > 0) {
val dt = LocalDateTime.now() // Vereinfacht, idealerweise aus lastUpdate Zeitstempel val dt = LocalDateTime.now() // Vereinfacht, idealerweise aus lastUpdate-Zeitstempeln
dt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) dt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
} else "Nie" } else "Nie"
@ -153,8 +190,8 @@ class DesktopMasterdataRepository(
lastImport = lastImportStr, lastImport = lastImportStr,
vereinCount = vereinCount.toInt(), vereinCount = vereinCount.toInt(),
reiterCount = reiterCount.toInt(), reiterCount = reiterCount.toInt(),
pferdCount = Store.pferde.size, pferdCount = pferdCount.toInt(),
funktionaerCount = Store.funktionaere.size funktionaerCount = funktionaerCount.toInt()
) )
} }
} }