Integrate series-service microservice with API gateway routing, implement Series domain and point aggregation logic, and update frontend with SeriesViewModel, SeriesScreen, and dynamic state handling.
This commit is contained in:
+5
@@ -36,4 +36,9 @@ object ApiRoutes {
|
||||
const val ROOT = "/api/v1/results"
|
||||
fun bewerb(bewerbId: String) = "$ROOT/bewerb/$bewerbId"
|
||||
}
|
||||
|
||||
object Series {
|
||||
const val ROOT = "/api/v1/series"
|
||||
fun stand(serieId: String) = "$ROOT/$serieId/stand"
|
||||
}
|
||||
}
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package at.mocode.turnier.feature.domain
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Serie(
|
||||
val id: String? = null,
|
||||
val name: String,
|
||||
val beschreibung: String? = null,
|
||||
val reglementTyp: String = "STREICHER_NORMAL",
|
||||
val bewerbIds: Set<String> = emptySet()
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SeriePunkt(
|
||||
val id: String? = null,
|
||||
val serieId: String,
|
||||
val reiterId: String,
|
||||
val pferdId: String,
|
||||
val bewerbId: String,
|
||||
val punkte: Double,
|
||||
val platzierung: Int
|
||||
)
|
||||
|
||||
interface SeriesRepository {
|
||||
suspend fun getAll(): Result<List<Serie>>
|
||||
suspend fun getById(id: String): Result<Serie>
|
||||
suspend fun save(serie: Serie): Result<Serie>
|
||||
suspend fun getStand(serieId: String): Result<Map<String, Double>> // Einfache Map Reiter+Pferd ID zu Punkten
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package at.mocode.turnier.feature.presentation
|
||||
|
||||
import at.mocode.turnier.feature.domain.Serie
|
||||
import at.mocode.turnier.feature.domain.SeriesRepository
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
|
||||
data class SeriesState(
|
||||
val series: List<Serie> = emptyList(),
|
||||
val isLoading: Boolean = false,
|
||||
val selectedSerieStand: Map<String, Double> = emptyMap(),
|
||||
val error: String? = null
|
||||
)
|
||||
|
||||
class SeriesViewModel(
|
||||
private val repository: SeriesRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val _state = MutableStateFlow(SeriesState())
|
||||
val state = _state.asStateFlow()
|
||||
|
||||
init {
|
||||
loadSeries()
|
||||
}
|
||||
|
||||
fun loadSeries() {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(isLoading = true)
|
||||
repository.getAll()
|
||||
.onSuccess { series ->
|
||||
_state.value = _state.value.copy(series = series, isLoading = false)
|
||||
}
|
||||
.onFailure {
|
||||
_state.value = _state.value.copy(error = it.message, isLoading = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun selectSerie(id: String) {
|
||||
viewModelScope.launch {
|
||||
repository.getStand(id)
|
||||
.onSuccess { stand ->
|
||||
_state.value = _state.value.copy(selectedSerieStand = stand)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createSerie(name: String) {
|
||||
viewModelScope.launch {
|
||||
repository.save(Serie(name = name))
|
||||
.onSuccess {
|
||||
loadSeries()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package at.mocode.turnier.feature.data.remote
|
||||
|
||||
import at.mocode.frontend.core.network.ApiRoutes
|
||||
import at.mocode.turnier.feature.domain.Serie
|
||||
import at.mocode.turnier.feature.domain.SeriesRepository
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.http.*
|
||||
|
||||
class DefaultSeriesRepository(
|
||||
private val client: HttpClient
|
||||
) : SeriesRepository {
|
||||
|
||||
override suspend fun getAll(): Result<List<Serie>> = runCatching {
|
||||
client.get(ApiRoutes.Series.ROOT).body()
|
||||
}
|
||||
|
||||
override suspend fun getById(id: String): Result<Serie> = runCatching {
|
||||
client.get("${ApiRoutes.Series.ROOT}/$id").body()
|
||||
}
|
||||
|
||||
override suspend fun save(serie: Serie): Result<Serie> = runCatching {
|
||||
if (serie.id == null) {
|
||||
client.post(ApiRoutes.Series.ROOT) {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(serie)
|
||||
}.body()
|
||||
} else {
|
||||
client.put("${ApiRoutes.Series.ROOT}/${serie.id}") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(serie)
|
||||
}.body()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getStand(serieId: String): Result<Map<String, Double>> = runCatching {
|
||||
client.get(ApiRoutes.Series.stand(serieId)).body()
|
||||
}
|
||||
}
|
||||
+2
@@ -19,6 +19,7 @@ val turnierFeatureModule = module {
|
||||
single<at.mocode.turnier.feature.domain.NennungRepository> { DefaultNennungRepository(client = get(qualifier = named("apiClient"))) }
|
||||
single<at.mocode.turnier.feature.domain.MasterdataRepository> { DefaultMasterdataRepository(client = get(qualifier = named("apiClient"))) }
|
||||
single<at.mocode.turnier.feature.domain.ErgebnisRepository> { DefaultErgebnisRepository(client = get(qualifier = named("apiClient"))) }
|
||||
single<at.mocode.turnier.feature.domain.SeriesRepository> { DefaultSeriesRepository(client = get(qualifier = named("apiClient"))) }
|
||||
|
||||
// ViewModels
|
||||
factory { TurnierViewModel(repo = get()) }
|
||||
@@ -46,4 +47,5 @@ val turnierFeatureModule = module {
|
||||
turnierId = turnierId
|
||||
)
|
||||
}
|
||||
factory { SeriesViewModel(repository = get()) }
|
||||
}
|
||||
|
||||
+62
-20
@@ -1,27 +1,31 @@
|
||||
package at.mocode.turnier.feature.presentation
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
|
||||
private val SeriesBlue = Color(0xFF1E3A8A)
|
||||
|
||||
/**
|
||||
* SERIES-Screen gemäß Vision_03 & Phase 10.
|
||||
*
|
||||
* Zeigt Cups, Serien und Meisterschaften mit konfigurierbaren Reglements.
|
||||
*/
|
||||
@Composable
|
||||
fun SeriesScreen(
|
||||
title: String,
|
||||
onBack: () -> Unit
|
||||
onBack: () -> Unit,
|
||||
viewModel: SeriesViewModel = koinViewModel()
|
||||
) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
// Toolbar
|
||||
Row(
|
||||
@@ -34,7 +38,7 @@ fun SeriesScreen(
|
||||
Text("Konfiguration & Auswertung (Phase 10)", fontSize = 13.sp, color = Color.Gray)
|
||||
}
|
||||
Button(
|
||||
onClick = { /* Neu anlegen Dialog */ },
|
||||
onClick = { viewModel.createSerie("Neuer Cup") },
|
||||
colors = ButtonDefaults.buttonColors(containerColor = SeriesBlue)
|
||||
) {
|
||||
Text("Neue Serie anlegen")
|
||||
@@ -43,23 +47,61 @@ fun SeriesScreen(
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// Leere Liste (Placeholder)
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text("Keine $title konfiguriert", fontSize = 16.sp, fontWeight = FontWeight.Medium)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(
|
||||
"Verknüpfe Bewerbe zu einer Serie, um Punktestände automatisch zu berechnen.",
|
||||
fontSize = 13.sp,
|
||||
color = Color.Gray,
|
||||
modifier = Modifier.padding(horizontal = 32.dp),
|
||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
||||
)
|
||||
Spacer(Modifier.height(24.dp))
|
||||
OutlinedButton(onClick = onBack) {
|
||||
Text("Zurück zur Verwaltung")
|
||||
if (state.series.isEmpty()) {
|
||||
EmptyState(title, onBack)
|
||||
} else {
|
||||
SeriesList(state, onSelect = { viewModel.selectSerie(it) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SeriesList(state: SeriesState, onSelect: (String) -> Unit) {
|
||||
Row(Modifier.fillMaxSize()) {
|
||||
LazyColumn(Modifier.weight(0.4f).padding(16.dp)) {
|
||||
items(state.series) { serie ->
|
||||
Card(
|
||||
onClick = { serie.id?.let { onSelect(it) } },
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)
|
||||
) {
|
||||
Column(Modifier.padding(12.dp)) {
|
||||
Text(serie.name, fontWeight = FontWeight.Bold)
|
||||
Text(serie.reglementTyp, fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
VerticalDivider()
|
||||
Column(Modifier.weight(0.6f).padding(16.dp)) {
|
||||
Text("Zwischenstand", style = MaterialTheme.typography.titleMedium)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
state.selectedSerieStand.forEach { (paar, punkte) ->
|
||||
Row(Modifier.fillMaxWidth().padding(vertical = 4.dp), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Text(paar)
|
||||
Text("$punkte Pkt", fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmptyState(title: String, onBack: () -> Unit) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text("Keine $title konfiguriert", fontSize = 16.sp, fontWeight = FontWeight.Medium)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(
|
||||
"Verknüpfe Bewerbe zu einer Serie, um Punktestände automatisch zu berechnen.",
|
||||
fontSize = 13.sp,
|
||||
color = Color.Gray,
|
||||
modifier = Modifier.padding(horizontal = 32.dp),
|
||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
||||
)
|
||||
Spacer(Modifier.height(24.dp))
|
||||
OutlinedButton(onClick = onBack) {
|
||||
Text("Zurück zur Verwaltung")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-1
@@ -12,7 +12,6 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import at.mocode.turnier.feature.domain.Bewerb
|
||||
import at.mocode.turnier.feature.domain.Ergebnis
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
|
||||
Reference in New Issue
Block a user