### 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.
- **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).
- **Vollständige Stammdaten-Integration:** Pferde und Funktionäre sind nun vollständig an SQLite und Backend-API angebunden.
## 🛠️ Durchgeführte Änderungen
### 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.
- **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
- **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
);
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
upsertVerein:
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 ('%' || ? || '%')
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:
DELETE FROM LocalVerein;
deleteAllReiter:
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
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()) }
}

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.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") }

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

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
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.
}

View File

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

View File

@ -13,148 +13,185 @@ import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
class DesktopMasterdataRepository(
private val db: AppDatabase
db: AppDatabase
) : MasterdataRepository {
private val queries = db.meldestelleDbQueries
private val queries = db.meldestelleDbQueries
override fun saveVereine(vereine: List<ZnsRemoteVerein>) {
println("[Repository] Speichere ${vereine.size} Vereine in SQLite")
val now = System.currentTimeMillis()
runBlocking {
queries.transaction {
vereine.forEach { remote ->
val id = remote.id.toLongOrNull() ?: return@forEach
queries.upsertVerein(
id = id,
oebs_nummer = remote.oepsNummer ?: "",
name = remote.name ?: "Unbekannt",
ort = remote.ort,
plz = null, // Falls vom Backend geliefert, hier mappen
bundesland = remote.bundesland,
is_active = 1,
last_updated = now
)
}
}
}
// Update Mock Store for backward compatibility during transition
override fun saveVereine(vereine: List<ZnsRemoteVerein>) {
println("[Repository] Speichere ${vereine.size} Vereine in SQLite")
val now = System.currentTimeMillis()
runBlocking {
queries.transaction {
vereine.forEach { remote ->
val id = remote.id.toLongOrNull() ?: return@forEach
val verein = Verein(
id = id,
name = remote.name ?: "Unbekannt",
oepsNummer = remote.oepsNummer ?: "",
ort = remote.ort,
bundesland = remote.bundesland,
istVeranstalter = true
)
val existingIdx = Store.vereine.indexOfFirst { it.id == id }
if (existingIdx >= 0) Store.vereine[existingIdx] = verein else Store.vereine.add(verein)
val id = remote.id.toLongOrNull() ?: return@forEach
queries.upsertVerein(
id = id,
oebs_nummer = remote.oepsNummer,
name = remote.name,
ort = remote.ort,
plz = null, // Falls vom Backend geliefert, hier mappen
bundesland = remote.bundesland,
is_active = 1,
last_updated = now
)
}
}
}
// Update Mock Store for backward compatibility during transition
vereine.forEach { remote ->
val id = remote.id.toLongOrNull() ?: return@forEach
val verein = Verein(
id = id,
name = remote.name,
oepsNummer = remote.oepsNummer,
ort = remote.ort,
bundesland = remote.bundesland,
istVeranstalter = true
)
val existingIdx = Store.vereine.indexOfFirst { it.id == id }
if (existingIdx >= 0) Store.vereine[existingIdx] = verein else Store.vereine.add(verein)
}
}
override fun saveReiter(reiter: List<ZnsRemoteReiter>) {
println("[Repository] Speichere ${reiter.size} Reiter in SQLite")
val now = System.currentTimeMillis()
runBlocking {
queries.transaction {
reiter.forEach { remote ->
val id = remote.id.toLongOrNull() ?: return@forEach
queries.upsertReiter(
id = id,
zns_nummer = remote.satznummer,
vorname = remote.vorname ?: "",
nachname = remote.nachname ?: "",
jahrgang = null, // Backend liefert aktuell kein Jahrgang direkt in ZnsRemoteReiter?
geschlecht = null,
nation = remote.nation ?: "AUT",
is_active = 1,
last_updated = now
)
}
}
}
// Sync to Store
override fun saveReiter(reiter: List<ZnsRemoteReiter>) {
println("[Repository] Speichere ${reiter.size} Reiter in SQLite")
val now = System.currentTimeMillis()
runBlocking {
queries.transaction {
reiter.forEach { remote ->
val id = remote.id.toLongOrNull() ?: return@forEach
val entry = Reiter(
id = id,
vorname = remote.vorname ?: "",
nachname = remote.nachname ?: "",
satznummer = remote.satznummer ?: "",
oepsNummer = remote.satznummer ?: "",
lizenzKlasse = remote.lizenzKlasse,
nation = remote.nation ?: "AUT",
bundesland = remote.bundesland
)
val existingIdx = Store.reiter.indexOfFirst { it.id == id }
if (existingIdx >= 0) Store.reiter[existingIdx] = entry else Store.reiter.add(entry)
val id = remote.id.toLongOrNull() ?: return@forEach
queries.upsertReiter(
id = id,
zns_nummer = remote.satznummer,
vorname = remote.vorname,
nachname = remote.nachname,
jahrgang = null, // Backend liefert aktuell kein Jahrgang direkt in ZnsRemoteReiter?
geschlecht = null,
nation = remote.nation ?: "AUT",
is_active = 1,
last_updated = now
)
}
}
}
// Sync to Store
reiter.forEach { remote ->
val id = remote.id.toLongOrNull() ?: return@forEach
val entry = Reiter(
id = id,
vorname = remote.vorname,
nachname = remote.nachname,
satznummer = remote.satznummer ?: "",
oepsNummer = remote.satznummer ?: "",
lizenzKlasse = remote.lizenzKlasse,
nation = remote.nation ?: "AUT",
bundesland = remote.bundesland
)
val existingIdx = Store.reiter.indexOfFirst { it.id == id }
if (existingIdx >= 0) Store.reiter[existingIdx] = entry else Store.reiter.add(entry)
}
}
override fun savePferde(pferde: List<ZnsRemotePferd>) {
println("[Repository] Speichere ${pferde.size} Pferde")
override fun savePferde(pferde: List<ZnsRemotePferd>) {
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
val existingIdx = Store.pferde.indexOfFirst { it.id == id }
val entry = Pferd(
id = id,
name = remote.name,
geschlecht = remote.geschlecht,
lebensnummer = remote.lebensnummer,
oepsNummer = remote.kopfnummer
)
if (existingIdx >= 0) {
Store.pferde[existingIdx] = entry
} else {
Store.pferde.add(entry)
}
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 ->
val id = remote.id.toLongOrNull() ?: return@forEach
val entry = Pferd(
id = id,
name = remote.name,
geschlecht = remote.geschlecht,
lebensnummer = remote.lebensnummer ?: "",
oepsNummer = remote.kopfnummer
)
val existingIdx = Store.pferde.indexOfFirst { it.id == id }
if (existingIdx >= 0) Store.pferde[existingIdx] = entry else Store.pferde.add(entry)
}
}
override fun saveFunktionaere(funktionaere: List<ZnsRemoteFunktionaer>) {
println("[Repository] Speichere ${funktionaere.size} Funktionäre")
override fun saveFunktionaere(funktionaere: List<ZnsRemoteFunktionaer>) {
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 existingIdx = Store.funktionaere.indexOfFirst { it.id == id }
val namen = remote.name?.split(" ") ?: listOf("Unbekannt")
val entry = Funktionaer(
id = id,
vorname = namen.firstOrNull() ?: "",
nachname = namen.drop(1).joinToString(" "),
rollen = remote.qualifikationen,
nation = remote.nation ?: "AUT",
bundesland = remote.bundesland
)
if (existingIdx >= 0) {
Store.funktionaere[existingIdx] = entry
} else {
Store.funktionaere.add(entry)
}
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
)
}
}
}
override fun getStats(): MasterdataStats {
val vereinCount = queries.selectAllVereine().executeAsList().size.toLong()
val reiterCount = queries.selectAllReiter().executeAsList().size.toLong()
val lastUpdate = listOf(
queries.selectAllVereine().executeAsList().maxOfOrNull { it.last_updated } ?: 0L,
queries.selectAllReiter().executeAsList().maxOfOrNull { it.last_updated } ?: 0L
).maxOrNull() ?: 0L
val lastImportStr = if (lastUpdate > 0) {
val dt = LocalDateTime.now() // Vereinfacht, idealerweise aus lastUpdate Zeitstempel
dt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
} else "Nie"
return MasterdataStats(
lastImport = lastImportStr,
vereinCount = vereinCount.toInt(),
reiterCount = reiterCount.toInt(),
pferdCount = Store.pferde.size,
funktionaerCount = Store.funktionaere.size
)
// Sync to Store
funktionaere.forEach { remote ->
val id = remote.id.toLongOrNull() ?: return@forEach
val namen = remote.name?.split(" ") ?: listOf("Unbekannt")
val entry = Funktionaer(
id = id,
vorname = namen.firstOrNull() ?: "",
nachname = namen.drop(1).joinToString(" "),
rollen = remote.qualifikationen,
nation = remote.nation ?: "AUT",
bundesland = remote.bundesland
)
val existingIdx = Store.funktionaere.indexOfFirst { it.id == id }
if (existingIdx >= 0) Store.funktionaere[existingIdx] = entry else Store.funktionaere.add(entry)
}
}
override fun getStats(): MasterdataStats {
val vereinCount = queries.selectAllVereine().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(
queries.selectAllVereine().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
val lastImportStr = if (lastUpdate > 0) {
val dt = LocalDateTime.now() // Vereinfacht, idealerweise aus lastUpdate-Zeitstempeln
dt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
} else "Nie"
return MasterdataStats(
lastImport = lastImportStr,
vereinCount = vereinCount.toInt(),
reiterCount = reiterCount.toInt(),
pferdCount = pferdCount.toInt(),
funktionaerCount = funktionaerCount.toInt()
)
}
}