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:
parent
48ffadaaa2
commit
2b3e2d8c1b
|
|
@ -51,14 +51,14 @@
|
|||
|
||||
## 🟠 Sprint B — Kurzfristig (nächste Woche)
|
||||
|
||||
- [ ] **B-1** | ViewModels für alle V3-Screens umsetzen
|
||||
- [ ] `TurnierViewModel`
|
||||
- [ ] `BewerbViewModel` (inkl. Abteilungs-Logik)
|
||||
- [ ] `PferdProfilViewModel`
|
||||
- [ ] `ReiterProfilViewModel`
|
||||
- [ ] `VereinsViewModel`
|
||||
- [ ] `FunktionaerViewModel`
|
||||
- [ ] `AbteilungViewModel` (Startliste, Ergebnisse)
|
||||
- [x] **B-1** | ViewModels für alle V3-Screens umsetzen
|
||||
- [x] `TurnierViewModel`
|
||||
- [x] `BewerbViewModel` (inkl. Abteilungs-Logik via Dialog-VM)
|
||||
- [x] `PferdProfilViewModel`
|
||||
- [x] `ReiterProfilViewModel`
|
||||
- [x] `VereinsViewModel`
|
||||
- [x] `FunktionaerViewModel`
|
||||
- [x] `AbteilungViewModel` (Startliste, Ergebnisse)
|
||||
|
||||
- [ ] **B-2** | Ktor-Clients und Repositories für Backend-Anbindung vorbereiten
|
||||
- [ ] Ktor-HTTP-Client konfigurieren (BaseURL, Auth-Header, Timeout)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user