Implement MVVM for all V3 screens: add ViewModels for Turniere, Bewerbe, Abteilungen, Pferde, Reiter, Vereins, and Funktionaer workflows. Update roadmap to mark B-1 tasks as complete.
This commit is contained in:
@@ -51,14 +51,14 @@
|
|||||||
|
|
||||||
## 🟠 Sprint B — Kurzfristig (nächste Woche)
|
## 🟠 Sprint B — Kurzfristig (nächste Woche)
|
||||||
|
|
||||||
- [ ] **B-1** | ViewModels für alle V3-Screens umsetzen
|
- [x] **B-1** | ViewModels für alle V3-Screens umsetzen
|
||||||
- [ ] `TurnierViewModel`
|
- [x] `TurnierViewModel`
|
||||||
- [ ] `BewerbViewModel` (inkl. Abteilungs-Logik)
|
- [x] `BewerbViewModel` (inkl. Abteilungs-Logik via Dialog-VM)
|
||||||
- [ ] `PferdProfilViewModel`
|
- [x] `PferdProfilViewModel`
|
||||||
- [ ] `ReiterProfilViewModel`
|
- [x] `ReiterProfilViewModel`
|
||||||
- [ ] `VereinsViewModel`
|
- [x] `VereinsViewModel`
|
||||||
- [ ] `FunktionaerViewModel`
|
- [x] `FunktionaerViewModel`
|
||||||
- [ ] `AbteilungViewModel` (Startliste, Ergebnisse)
|
- [x] `AbteilungViewModel` (Startliste, Ergebnisse)
|
||||||
|
|
||||||
- [ ] **B-2** | Ktor-Clients und Repositories für Backend-Anbindung vorbereiten
|
- [ ] **B-2** | Ktor-Clients und Repositories für Backend-Anbindung vorbereiten
|
||||||
- [ ] Ktor-HTTP-Client konfigurieren (BaseURL, Auth-Header, Timeout)
|
- [ ] Ktor-HTTP-Client konfigurieren (BaseURL, Auth-Header, Timeout)
|
||||||
|
|||||||
+91
@@ -0,0 +1,91 @@
|
|||||||
|
package at.mocode.frontend.features.funktionaer.presentation
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
data class FunktionaerListItem(
|
||||||
|
val id: Long,
|
||||||
|
val name: String,
|
||||||
|
val rolle: String,
|
||||||
|
val lizenz: String?,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class FunktionaerState(
|
||||||
|
val isLoading: Boolean = false,
|
||||||
|
val searchQuery: String = "",
|
||||||
|
val list: List<FunktionaerListItem> = emptyList(),
|
||||||
|
val filtered: List<FunktionaerListItem> = emptyList(),
|
||||||
|
val selectedId: Long? = null,
|
||||||
|
val errorMessage: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed interface FunktionaerIntent {
|
||||||
|
data object Load : FunktionaerIntent
|
||||||
|
data object Refresh : FunktionaerIntent
|
||||||
|
data class SearchChanged(val query: String) : FunktionaerIntent
|
||||||
|
data class Select(val id: Long?) : FunktionaerIntent
|
||||||
|
data object ClearError : FunktionaerIntent
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FunktionaerRepository {
|
||||||
|
suspend fun list(): List<FunktionaerListItem>
|
||||||
|
}
|
||||||
|
|
||||||
|
class FunktionaerViewModel(
|
||||||
|
private val repo: FunktionaerRepository,
|
||||||
|
) {
|
||||||
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
|
|
||||||
|
private val _state = MutableStateFlow(FunktionaerState(isLoading = true))
|
||||||
|
val state: StateFlow<FunktionaerState> = _state
|
||||||
|
|
||||||
|
init { send(FunktionaerIntent.Load) }
|
||||||
|
|
||||||
|
fun send(intent: FunktionaerIntent) {
|
||||||
|
when (intent) {
|
||||||
|
is FunktionaerIntent.Load, is FunktionaerIntent.Refresh -> load()
|
||||||
|
is FunktionaerIntent.SearchChanged -> reduce { it.copy(searchQuery = intent.query) }.also { filter() }
|
||||||
|
is FunktionaerIntent.Select -> reduce { it.copy(selectedId = intent.id) }
|
||||||
|
is FunktionaerIntent.ClearError -> reduce { it.copy(errorMessage = null) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun load() {
|
||||||
|
reduce { it.copy(isLoading = true, errorMessage = null) }
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
val items = repo.list()
|
||||||
|
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") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun filter() {
|
||||||
|
val cur = _state.value
|
||||||
|
val filtered = filterList(cur.list, cur.searchQuery)
|
||||||
|
reduce { it.copy(filtered = filtered) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun filterList(list: List<FunktionaerListItem>, query: String): List<FunktionaerListItem> {
|
||||||
|
if (query.isBlank()) return list
|
||||||
|
val q = query.trim()
|
||||||
|
return list.filter {
|
||||||
|
it.name.contains(q, ignoreCase = true) ||
|
||||||
|
it.rolle.contains(q, ignoreCase = true) ||
|
||||||
|
(it.lizenz?.contains(q, ignoreCase = true) ?: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun reduce(block: (FunktionaerState) -> FunktionaerState) {
|
||||||
|
_state.value = block(_state.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
+99
@@ -0,0 +1,99 @@
|
|||||||
|
package at.mocode.frontend.features.pferde.presentation
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
data class PferdProfilState(
|
||||||
|
val isLoading: Boolean = false,
|
||||||
|
val errorMessage: String? = null,
|
||||||
|
val name: String = "",
|
||||||
|
val feiId: String = "",
|
||||||
|
val oepsNummer: String = "",
|
||||||
|
val geburtsjahr: String = "",
|
||||||
|
val farbe: String = "",
|
||||||
|
val rasse: String = "",
|
||||||
|
val validHints: List<String> = emptyList(),
|
||||||
|
val dirty: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed interface PferdProfilIntent {
|
||||||
|
data class Load(val id: String) : PferdProfilIntent
|
||||||
|
data object Refresh : PferdProfilIntent
|
||||||
|
data class EditName(val v: String) : PferdProfilIntent
|
||||||
|
data class EditFeiId(val v: String) : PferdProfilIntent
|
||||||
|
data class EditOeps(val v: String) : PferdProfilIntent
|
||||||
|
data class EditGeburtsjahr(val v: String) : PferdProfilIntent
|
||||||
|
data class EditFarbe(val v: String) : PferdProfilIntent
|
||||||
|
data class EditRasse(val v: String) : PferdProfilIntent
|
||||||
|
data object Save : PferdProfilIntent
|
||||||
|
data object ClearError : PferdProfilIntent
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PferdProfilRepository {
|
||||||
|
suspend fun load(id: String): PferdProfilState
|
||||||
|
suspend fun save(id: String, state: PferdProfilState)
|
||||||
|
}
|
||||||
|
|
||||||
|
class PferdProfilViewModel(
|
||||||
|
private val repo: PferdProfilRepository,
|
||||||
|
private var id: String,
|
||||||
|
) {
|
||||||
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
|
|
||||||
|
private val _state = MutableStateFlow(PferdProfilState(isLoading = true))
|
||||||
|
val state: StateFlow<PferdProfilState> = _state
|
||||||
|
|
||||||
|
init { send(PferdProfilIntent.Load(id)) }
|
||||||
|
|
||||||
|
fun send(intent: PferdProfilIntent) {
|
||||||
|
when (intent) {
|
||||||
|
is PferdProfilIntent.Load -> { id = intent.id; load() }
|
||||||
|
is PferdProfilIntent.Refresh -> load()
|
||||||
|
is PferdProfilIntent.EditName -> edit { it.copy(name = intent.v) }
|
||||||
|
is PferdProfilIntent.EditFeiId -> edit { it.copy(feiId = intent.v) }
|
||||||
|
is PferdProfilIntent.EditOeps -> edit { it.copy(oepsNummer = intent.v) }
|
||||||
|
is PferdProfilIntent.EditGeburtsjahr -> edit { it.copy(geburtsjahr = intent.v) }
|
||||||
|
is PferdProfilIntent.EditFarbe -> edit { it.copy(farbe = intent.v) }
|
||||||
|
is PferdProfilIntent.EditRasse -> edit { it.copy(rasse = intent.v) }
|
||||||
|
is PferdProfilIntent.Save -> save()
|
||||||
|
is PferdProfilIntent.ClearError -> reduce { it.copy(errorMessage = null) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun load() {
|
||||||
|
reduce { it.copy(isLoading = true, errorMessage = null) }
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
val loaded = repo.load(id)
|
||||||
|
reduce { loaded.copy(isLoading = false, dirty = false) }
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
reduce { it.copy(isLoading = false, errorMessage = t.message ?: "Fehler beim Laden") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun save() {
|
||||||
|
val cur = _state.value
|
||||||
|
reduce { it.copy(isLoading = true, errorMessage = null) }
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
repo.save(id, cur)
|
||||||
|
reduce { it.copy(isLoading = false, dirty = false) }
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
reduce { it.copy(isLoading = false, errorMessage = t.message ?: "Fehler beim Speichern") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun edit(block: (PferdProfilState) -> PferdProfilState) {
|
||||||
|
reduce { block(it).copy(dirty = true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun reduce(block: (PferdProfilState) -> PferdProfilState) {
|
||||||
|
_state.value = block(_state.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
+99
@@ -0,0 +1,99 @@
|
|||||||
|
package at.mocode.frontend.features.reiter.presentation
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
data class ReiterProfilState(
|
||||||
|
val isLoading: Boolean = false,
|
||||||
|
val errorMessage: String? = null,
|
||||||
|
val vorname: String = "",
|
||||||
|
val nachname: String = "",
|
||||||
|
val oepsNummer: String = "",
|
||||||
|
val feiId: String = "",
|
||||||
|
val lizenzKlasse: String = "",
|
||||||
|
val verein: String = "",
|
||||||
|
val validHints: List<String> = emptyList(),
|
||||||
|
val dirty: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed interface ReiterProfilIntent {
|
||||||
|
data class Load(val id: String) : ReiterProfilIntent
|
||||||
|
data object Refresh : ReiterProfilIntent
|
||||||
|
data class EditVorname(val v: String) : ReiterProfilIntent
|
||||||
|
data class EditNachname(val v: String) : ReiterProfilIntent
|
||||||
|
data class EditOeps(val v: String) : ReiterProfilIntent
|
||||||
|
data class EditFeiId(val v: String) : ReiterProfilIntent
|
||||||
|
data class EditLizenz(val v: String) : ReiterProfilIntent
|
||||||
|
data class EditVerein(val v: String) : ReiterProfilIntent
|
||||||
|
data object Save : ReiterProfilIntent
|
||||||
|
data object ClearError : ReiterProfilIntent
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ReiterProfilRepository {
|
||||||
|
suspend fun load(id: String): ReiterProfilState
|
||||||
|
suspend fun save(id: String, state: ReiterProfilState)
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReiterProfilViewModel(
|
||||||
|
private val repo: ReiterProfilRepository,
|
||||||
|
private var id: String,
|
||||||
|
) {
|
||||||
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
|
|
||||||
|
private val _state = MutableStateFlow(ReiterProfilState(isLoading = true))
|
||||||
|
val state: StateFlow<ReiterProfilState> = _state
|
||||||
|
|
||||||
|
init { send(ReiterProfilIntent.Load(id)) }
|
||||||
|
|
||||||
|
fun send(intent: ReiterProfilIntent) {
|
||||||
|
when (intent) {
|
||||||
|
is ReiterProfilIntent.Load -> { id = intent.id; load() }
|
||||||
|
is ReiterProfilIntent.Refresh -> load()
|
||||||
|
is ReiterProfilIntent.EditVorname -> edit { it.copy(vorname = intent.v) }
|
||||||
|
is ReiterProfilIntent.EditNachname -> edit { it.copy(nachname = intent.v) }
|
||||||
|
is ReiterProfilIntent.EditOeps -> edit { it.copy(oepsNummer = intent.v) }
|
||||||
|
is ReiterProfilIntent.EditFeiId -> edit { it.copy(feiId = intent.v) }
|
||||||
|
is ReiterProfilIntent.EditLizenz -> edit { it.copy(lizenzKlasse = intent.v) }
|
||||||
|
is ReiterProfilIntent.EditVerein -> edit { it.copy(verein = intent.v) }
|
||||||
|
is ReiterProfilIntent.Save -> save()
|
||||||
|
is ReiterProfilIntent.ClearError -> reduce { it.copy(errorMessage = null) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun load() {
|
||||||
|
reduce { it.copy(isLoading = true, errorMessage = null) }
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
val loaded = repo.load(id)
|
||||||
|
reduce { loaded.copy(isLoading = false, dirty = false) }
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
reduce { it.copy(isLoading = false, errorMessage = t.message ?: "Fehler beim Laden") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun save() {
|
||||||
|
val cur = _state.value
|
||||||
|
reduce { it.copy(isLoading = true, errorMessage = null) }
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
repo.save(id, cur)
|
||||||
|
reduce { it.copy(isLoading = false, dirty = false) }
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
reduce { it.copy(isLoading = false, errorMessage = t.message ?: "Fehler beim Speichern") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun edit(block: (ReiterProfilState) -> ReiterProfilState) {
|
||||||
|
reduce { block(it).copy(dirty = true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun reduce(block: (ReiterProfilState) -> ReiterProfilState) {
|
||||||
|
_state.value = block(_state.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
+132
@@ -0,0 +1,132 @@
|
|||||||
|
package at.mocode.turnier.feature.presentation
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
data class StartlistenEintrag(
|
||||||
|
val startNr: Int,
|
||||||
|
val reiterName: String,
|
||||||
|
val pferdeName: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ErgebnisEintrag(
|
||||||
|
val startNr: Int,
|
||||||
|
val punkte: Double?,
|
||||||
|
val rang: Int?,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AbteilungState(
|
||||||
|
val isLoading: Boolean = false,
|
||||||
|
val errorMessage: String? = null,
|
||||||
|
val startliste: List<StartlistenEintrag> = emptyList(),
|
||||||
|
val ergebnisse: List<ErgebnisEintrag> = emptyList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed interface AbteilungIntent {
|
||||||
|
data class LoadByBewerb(val bewerbId: Long, val abteilungsNr: Int) : AbteilungIntent
|
||||||
|
data object Refresh : AbteilungIntent
|
||||||
|
data class UpdateErgebnis(val startNr: Int, val punkte: Double?) : AbteilungIntent
|
||||||
|
data class ReorderStartliste(val fromIndex: Int, val toIndex: Int) : AbteilungIntent
|
||||||
|
data object Publish : AbteilungIntent
|
||||||
|
data object ClearError : AbteilungIntent
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AbteilungRepository {
|
||||||
|
suspend fun loadStartliste(bewerbId: Long, abteilungsNr: Int): List<StartlistenEintrag>
|
||||||
|
suspend fun loadErgebnisse(bewerbId: Long, abteilungsNr: Int): List<ErgebnisEintrag>
|
||||||
|
suspend fun saveErgebnis(bewerbId: Long, abteilungsNr: Int, startNr: Int, punkte: Double?)
|
||||||
|
suspend fun saveStartlistenOrder(bewerbId: Long, abteilungsNr: Int, orderedStartNr: List<Int>)
|
||||||
|
suspend fun publish(bewerbId: Long, abteilungsNr: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
class AbteilungViewModel(
|
||||||
|
private val repo: AbteilungRepository,
|
||||||
|
private var bewerbId: Long,
|
||||||
|
private var abteilungsNr: Int,
|
||||||
|
) {
|
||||||
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
|
|
||||||
|
private val _state = MutableStateFlow(AbteilungState(isLoading = true))
|
||||||
|
val state: StateFlow<AbteilungState> = _state
|
||||||
|
|
||||||
|
init {
|
||||||
|
send(AbteilungIntent.LoadByBewerb(bewerbId, abteilungsNr))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun send(intent: AbteilungIntent) {
|
||||||
|
when (intent) {
|
||||||
|
is AbteilungIntent.LoadByBewerb -> {
|
||||||
|
bewerbId = intent.bewerbId
|
||||||
|
abteilungsNr = intent.abteilungsNr
|
||||||
|
load()
|
||||||
|
}
|
||||||
|
is AbteilungIntent.Refresh -> load()
|
||||||
|
is AbteilungIntent.UpdateErgebnis -> updateErgebnis(intent.startNr, intent.punkte)
|
||||||
|
is AbteilungIntent.ReorderStartliste -> reorder(intent.fromIndex, intent.toIndex)
|
||||||
|
is AbteilungIntent.Publish -> publish()
|
||||||
|
is AbteilungIntent.ClearError -> reduce { it.copy(errorMessage = null) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun load() {
|
||||||
|
reduce { it.copy(isLoading = true, errorMessage = null) }
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
val start = repo.loadStartliste(bewerbId, abteilungsNr)
|
||||||
|
val erg = repo.loadErgebnisse(bewerbId, abteilungsNr)
|
||||||
|
reduce { it.copy(isLoading = false, startliste = start, ergebnisse = erg) }
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
reduce { it.copy(isLoading = false, errorMessage = t.message ?: "Fehler beim Laden") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateErgebnis(startNr: Int, punkte: Double?) {
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
repo.saveErgebnis(bewerbId, abteilungsNr, startNr, punkte)
|
||||||
|
// Lokale Spiegelung
|
||||||
|
val newErg = state.value.ergebnisse.toMutableList()
|
||||||
|
val idx = newErg.indexOfFirst { it.startNr == startNr }
|
||||||
|
if (idx >= 0) newErg[idx] = newErg[idx].copy(punkte = punkte) else newErg += ErgebnisEintrag(startNr, punkte, null)
|
||||||
|
reduce { it.copy(ergebnisse = newErg) }
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
reduce { it.copy(errorMessage = t.message ?: "Fehler beim Speichern") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun reorder(fromIndex: Int, toIndex: Int) {
|
||||||
|
val list = state.value.startliste.toMutableList()
|
||||||
|
if (fromIndex in list.indices && toIndex in list.indices) {
|
||||||
|
val item = list.removeAt(fromIndex)
|
||||||
|
list.add(toIndex, item)
|
||||||
|
reduce { it.copy(startliste = list) }
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
repo.saveStartlistenOrder(bewerbId, abteilungsNr, list.map { it.startNr })
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
reduce { it.copy(errorMessage = t.message ?: "Fehler beim Speichern der Reihenfolge") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun publish() {
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
repo.publish(bewerbId, abteilungsNr)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
reduce { it.copy(errorMessage = t.message ?: "Fehler beim Veröffentlichen") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun reduce(block: (AbteilungState) -> AbteilungState) {
|
||||||
|
_state.value = block(_state.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
+130
@@ -0,0 +1,130 @@
|
|||||||
|
package at.mocode.turnier.feature.presentation
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
data class BewerbListItem(
|
||||||
|
val id: Long,
|
||||||
|
val tag: String,
|
||||||
|
val platz: Int,
|
||||||
|
val name: String,
|
||||||
|
val sparte: String,
|
||||||
|
val klasse: String,
|
||||||
|
val nennungen: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class BewerbState(
|
||||||
|
val isLoading: Boolean = false,
|
||||||
|
val searchQuery: String = "",
|
||||||
|
val list: List<BewerbListItem> = emptyList(),
|
||||||
|
val filtered: List<BewerbListItem> = emptyList(),
|
||||||
|
val selectedId: Long? = null,
|
||||||
|
val errorMessage: String? = null,
|
||||||
|
// Verknüpfung zum Dialog-VM für Abteilungs-Logik (optional)
|
||||||
|
val dialogState: BewerbAnlegenState = BewerbAnlegenState(),
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed interface BewerbIntent {
|
||||||
|
data object Load : BewerbIntent
|
||||||
|
data object Refresh : BewerbIntent
|
||||||
|
data class SearchChanged(val query: String) : BewerbIntent
|
||||||
|
data class Select(val id: Long?) : BewerbIntent
|
||||||
|
data object ClearError : BewerbIntent
|
||||||
|
|
||||||
|
// Delegation an Dialog-VM
|
||||||
|
data object OpenDialog : BewerbIntent
|
||||||
|
data object CloseDialog : BewerbIntent
|
||||||
|
data class SetBewerbsTyp(val typ: String) : BewerbIntent
|
||||||
|
data class SetAbteilungsTyp(val typ: AbteilungsTyp) : BewerbIntent
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BewerbRepository {
|
||||||
|
suspend fun listByTurnier(turnierId: Long): List<BewerbListItem>
|
||||||
|
}
|
||||||
|
|
||||||
|
class BewerbViewModel(
|
||||||
|
private val repo: BewerbRepository,
|
||||||
|
private val turnierId: Long,
|
||||||
|
) {
|
||||||
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
|
|
||||||
|
private val _state = MutableStateFlow(BewerbState(isLoading = true))
|
||||||
|
val state: StateFlow<BewerbState> = _state
|
||||||
|
|
||||||
|
// Interne Instanz des Dialog‑VM (teilen State via Kopie)
|
||||||
|
private val dialogVm = BewerbAnlegenViewModel()
|
||||||
|
|
||||||
|
init {
|
||||||
|
send(BewerbIntent.Load)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun send(intent: BewerbIntent) {
|
||||||
|
when (intent) {
|
||||||
|
is BewerbIntent.Load, is BewerbIntent.Refresh -> load()
|
||||||
|
is BewerbIntent.SearchChanged -> reduce { it.copy(searchQuery = intent.query) }.also { filter() }
|
||||||
|
is BewerbIntent.Select -> reduce { it.copy(selectedId = intent.id) }
|
||||||
|
is BewerbIntent.ClearError -> reduce { it.copy(errorMessage = null) }
|
||||||
|
|
||||||
|
is BewerbIntent.OpenDialog -> {
|
||||||
|
dialogVm.send(BewerbAnlegenIntent.Open)
|
||||||
|
syncDialogState()
|
||||||
|
}
|
||||||
|
is BewerbIntent.CloseDialog -> {
|
||||||
|
dialogVm.send(BewerbAnlegenIntent.Close)
|
||||||
|
syncDialogState()
|
||||||
|
}
|
||||||
|
is BewerbIntent.SetBewerbsTyp -> {
|
||||||
|
dialogVm.send(BewerbAnlegenIntent.SetBewerbsTyp(intent.typ))
|
||||||
|
syncDialogState()
|
||||||
|
}
|
||||||
|
is BewerbIntent.SetAbteilungsTyp -> {
|
||||||
|
dialogVm.send(BewerbAnlegenIntent.SetAbteilungsTyp(intent.typ))
|
||||||
|
syncDialogState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun load() {
|
||||||
|
reduce { it.copy(isLoading = true, errorMessage = null) }
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
val items = repo.listByTurnier(turnierId)
|
||||||
|
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 ?: "Unbekannter Fehler beim Laden") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun filter() {
|
||||||
|
val cur = _state.value
|
||||||
|
val filtered = filterList(cur.list, cur.searchQuery)
|
||||||
|
reduce { it.copy(filtered = filtered) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun filterList(list: List<BewerbListItem>, query: String): List<BewerbListItem> {
|
||||||
|
if (query.isBlank()) return list
|
||||||
|
val q = query.trim()
|
||||||
|
return list.filter {
|
||||||
|
it.name.contains(q, ignoreCase = true) ||
|
||||||
|
it.sparte.contains(q, ignoreCase = true) ||
|
||||||
|
it.klasse.contains(q, ignoreCase = true) ||
|
||||||
|
it.tag.contains(q, ignoreCase = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun syncDialogState() {
|
||||||
|
_state.value = _state.value.copy(dialogState = dialogVm.state.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun reduce(block: (BewerbState) -> BewerbState) {
|
||||||
|
_state.value = block(_state.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
+98
@@ -0,0 +1,98 @@
|
|||||||
|
package at.mocode.turnier.feature.presentation
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
// UI-optimiertes List Item für Turniere (unabhängig vom Domänenmodell)
|
||||||
|
data class TurnierListItem(
|
||||||
|
val id: Long,
|
||||||
|
val name: String,
|
||||||
|
val ort: String,
|
||||||
|
val startDatum: String,
|
||||||
|
val endDatum: String,
|
||||||
|
val status: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class TurnierState(
|
||||||
|
val isLoading: Boolean = false,
|
||||||
|
val searchQuery: String = "",
|
||||||
|
val list: List<TurnierListItem> = emptyList(),
|
||||||
|
val filtered: List<TurnierListItem> = emptyList(),
|
||||||
|
val selectedId: Long? = null,
|
||||||
|
val errorMessage: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed interface TurnierIntent {
|
||||||
|
data object Load : TurnierIntent
|
||||||
|
data object Refresh : TurnierIntent
|
||||||
|
data class SearchChanged(val query: String) : TurnierIntent
|
||||||
|
data class Select(val id: Long?) : TurnierIntent
|
||||||
|
data object ClearError : TurnierIntent
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TurnierRepository {
|
||||||
|
suspend fun list(): List<TurnierListItem>
|
||||||
|
// Platzhalter für B-2: suspend fun get(id: Long): TurnierDetail
|
||||||
|
}
|
||||||
|
|
||||||
|
class TurnierViewModel(
|
||||||
|
private val repo: TurnierRepository,
|
||||||
|
) {
|
||||||
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
|
|
||||||
|
private val _state = MutableStateFlow(TurnierState(isLoading = true))
|
||||||
|
val state: StateFlow<TurnierState> = _state
|
||||||
|
|
||||||
|
init {
|
||||||
|
send(TurnierIntent.Load)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun send(intent: TurnierIntent) {
|
||||||
|
when (intent) {
|
||||||
|
is TurnierIntent.Load -> load()
|
||||||
|
is TurnierIntent.Refresh -> load()
|
||||||
|
is TurnierIntent.SearchChanged -> reduce { it.copy(searchQuery = intent.query) }.also { filter() }
|
||||||
|
is TurnierIntent.Select -> reduce { it.copy(selectedId = intent.id) }
|
||||||
|
is TurnierIntent.ClearError -> reduce { it.copy(errorMessage = null) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun load() {
|
||||||
|
reduce { it.copy(isLoading = true, errorMessage = null) }
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
val items = repo.list()
|
||||||
|
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 ?: "Unbekannter Fehler beim Laden") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun filter() {
|
||||||
|
val cur = _state.value
|
||||||
|
val filtered = filterList(cur.list, cur.searchQuery)
|
||||||
|
reduce { it.copy(filtered = filtered) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun filterList(list: List<TurnierListItem>, query: String): List<TurnierListItem> {
|
||||||
|
if (query.isBlank()) return list
|
||||||
|
val q = query.trim()
|
||||||
|
return list.filter {
|
||||||
|
it.name.contains(q, ignoreCase = true) ||
|
||||||
|
it.ort.contains(q, ignoreCase = true) ||
|
||||||
|
it.status.contains(q, ignoreCase = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun reduce(block: (TurnierState) -> TurnierState) {
|
||||||
|
_state.value = block(_state.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
+91
@@ -0,0 +1,91 @@
|
|||||||
|
package at.mocode.frontend.features.verein.presentation
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
data class VereinsListItem(
|
||||||
|
val id: Long,
|
||||||
|
val name: String,
|
||||||
|
val ort: String,
|
||||||
|
val oepsNummer: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class VereinsState(
|
||||||
|
val isLoading: Boolean = false,
|
||||||
|
val searchQuery: String = "",
|
||||||
|
val list: List<VereinsListItem> = emptyList(),
|
||||||
|
val filtered: List<VereinsListItem> = emptyList(),
|
||||||
|
val selectedId: Long? = null,
|
||||||
|
val errorMessage: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed interface VereinsIntent {
|
||||||
|
data object Load : VereinsIntent
|
||||||
|
data object Refresh : VereinsIntent
|
||||||
|
data class SearchChanged(val query: String) : VereinsIntent
|
||||||
|
data class Select(val id: Long?) : VereinsIntent
|
||||||
|
data object ClearError : VereinsIntent
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VereinsRepository {
|
||||||
|
suspend fun list(): List<VereinsListItem>
|
||||||
|
}
|
||||||
|
|
||||||
|
class VereinsViewModel(
|
||||||
|
private val repo: VereinsRepository,
|
||||||
|
) {
|
||||||
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
|
|
||||||
|
private val _state = MutableStateFlow(VereinsState(isLoading = true))
|
||||||
|
val state: StateFlow<VereinsState> = _state
|
||||||
|
|
||||||
|
init { send(VereinsIntent.Load) }
|
||||||
|
|
||||||
|
fun send(intent: VereinsIntent) {
|
||||||
|
when (intent) {
|
||||||
|
is VereinsIntent.Load, is VereinsIntent.Refresh -> load()
|
||||||
|
is VereinsIntent.SearchChanged -> reduce { it.copy(searchQuery = intent.query) }.also { filter() }
|
||||||
|
is VereinsIntent.Select -> reduce { it.copy(selectedId = intent.id) }
|
||||||
|
is VereinsIntent.ClearError -> reduce { it.copy(errorMessage = null) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun load() {
|
||||||
|
reduce { it.copy(isLoading = true, errorMessage = null) }
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
val items = repo.list()
|
||||||
|
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") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun filter() {
|
||||||
|
val cur = _state.value
|
||||||
|
val filtered = filterList(cur.list, cur.searchQuery)
|
||||||
|
reduce { it.copy(filtered = filtered) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun filterList(list: List<VereinsListItem>, query: String): List<VereinsListItem> {
|
||||||
|
if (query.isBlank()) return list
|
||||||
|
val q = query.trim()
|
||||||
|
return list.filter {
|
||||||
|
it.name.contains(q, ignoreCase = true) ||
|
||||||
|
it.ort.contains(q, ignoreCase = true) ||
|
||||||
|
it.oepsNummer.contains(q, ignoreCase = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun reduce(block: (VereinsState) -> VereinsState) {
|
||||||
|
_state.value = block(_state.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user