Implement ranking logic with SerieStandEntry, add support for streak results and binding types (Reiter+Pferd, Reiter, Pferd), update UI for detailed ranking display, and finalize Phase 10.
This commit is contained in:
parent
6e99bc97fd
commit
a79e612693
|
|
@ -1,5 +1,7 @@
|
||||||
package at.mocode.series.service.application
|
package at.mocode.series.service.application
|
||||||
|
|
||||||
|
import at.mocode.series.service.domain.Bindungstyp
|
||||||
|
import at.mocode.series.service.domain.ReglementTyp
|
||||||
import at.mocode.series.service.domain.Serie
|
import at.mocode.series.service.domain.Serie
|
||||||
import at.mocode.series.service.domain.SeriePunkt
|
import at.mocode.series.service.domain.SeriePunkt
|
||||||
import at.mocode.series.service.persistence.JpaSeriePunktRepository
|
import at.mocode.series.service.persistence.JpaSeriePunktRepository
|
||||||
|
|
@ -20,17 +22,48 @@ class SeriesService(
|
||||||
@Transactional
|
@Transactional
|
||||||
fun saveSerie(serie: Serie): Serie = serieRepository.save(serie)
|
fun saveSerie(serie: Serie): Serie = serieRepository.save(serie)
|
||||||
|
|
||||||
fun getStand(serieId: String): Map<Pair<String, String>, Double> {
|
fun getStand(serieId: String): List<SerieStandEntry> {
|
||||||
|
val serie = getSeriesById(serieId) ?: return emptyList()
|
||||||
val punkte = punkteRepository.findBySerieId(serieId)
|
val punkte = punkteRepository.findBySerieId(serieId)
|
||||||
|
|
||||||
// Aggregation pro Paar (Reiter, Pferd)
|
// Gruppierung nach Bindungstyp
|
||||||
return punkte.groupBy { it.reiterId to it.pferdId }
|
val groupedPunkte = when (serie.bindungstyp) {
|
||||||
.mapValues { (_, v) -> v.sumOf { it.punkte } }
|
Bindungstyp.PAAR_BINDUNG -> punkte.groupBy { "${it.reiterId}_${it.pferdId}" }
|
||||||
.toList()
|
Bindungstyp.NUR_REITER -> punkte.groupBy { it.reiterId }
|
||||||
.sortedByDescending { it.second }
|
Bindungstyp.NUR_PFERD -> punkte.groupBy { it.pferdId }
|
||||||
.toMap()
|
}
|
||||||
|
|
||||||
|
return groupedPunkte.map { (_, v) ->
|
||||||
|
val bewerbPunkte = v.map { it.punkte }.sortedDescending()
|
||||||
|
|
||||||
|
val gesamtPunkte = when (serie.reglementTyp) {
|
||||||
|
ReglementTyp.ALLES_ZAEHLT -> bewerbPunkte.sum()
|
||||||
|
ReglementTyp.STREICHER_NORMAL -> {
|
||||||
|
// Berücksichtige Streichresultate: N-m gewertete Resultate (die besten N-m zählen)
|
||||||
|
// Wenn wir m Streichresultate haben, zählen die besten (Total - m) Resultate.
|
||||||
|
val countToSum = (bewerbPunkte.size - serie.streichresultateCount).coerceAtLeast(0)
|
||||||
|
if (countToSum == 0 && bewerbPunkte.isNotEmpty()) bewerbPunkte.first() // Fallback: Zumindest eines zählt
|
||||||
|
else bewerbPunkte.take(countToSum).sum()
|
||||||
|
}
|
||||||
|
ReglementTyp.MEISTERSCHAFT -> bewerbPunkte.sum() // TODO: Spezielle Gewichtung
|
||||||
|
}
|
||||||
|
|
||||||
|
SerieStandEntry(
|
||||||
|
reiterId = v.first().reiterId,
|
||||||
|
pferdId = if (serie.bindungstyp == Bindungstyp.PAAR_BINDUNG) v.first().pferdId else null,
|
||||||
|
punkte = gesamtPunkte,
|
||||||
|
anzahlWertungen = v.size
|
||||||
|
)
|
||||||
|
}.sortedByDescending { it.punkte }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
fun addPunkt(punkt: SeriePunkt): SeriePunkt = punkteRepository.save(punkt)
|
fun addPunkt(punkt: SeriePunkt): SeriePunkt = punkteRepository.save(punkt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class SerieStandEntry(
|
||||||
|
val reiterId: String,
|
||||||
|
val pferdId: String?,
|
||||||
|
val punkte: Double,
|
||||||
|
val anzahlWertungen: Int
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,13 @@ class Serie(
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
val reglementTyp: ReglementTyp = ReglementTyp.STREICHER_NORMAL,
|
val reglementTyp: ReglementTyp = ReglementTyp.STREICHER_NORMAL,
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
val streichresultateCount: Int = 1,
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(nullable = false)
|
||||||
|
val bindungstyp: Bindungstyp = Bindungstyp.PAAR_BINDUNG,
|
||||||
|
|
||||||
@ElementCollection
|
@ElementCollection
|
||||||
@CollectionTable(name = "serie_bewerbe", joinColumns = [JoinColumn(name = "serie_id")])
|
@CollectionTable(name = "serie_bewerbe", joinColumns = [JoinColumn(name = "serie_id")])
|
||||||
@Column(name = "bewerb_id")
|
@Column(name = "bewerb_id")
|
||||||
|
|
@ -31,6 +38,12 @@ enum class ReglementTyp {
|
||||||
MEISTERSCHAFT // Spezielle Gewichtung (z.B. Finale doppelt)
|
MEISTERSCHAFT // Spezielle Gewichtung (z.B. Finale doppelt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class Bindungstyp {
|
||||||
|
PAAR_BINDUNG, // Reiter + Pferd (Standard)
|
||||||
|
NUR_REITER, // Punkte zählen nur für den Reiter
|
||||||
|
NUR_PFERD // Punkte zählen nur für das Pferd
|
||||||
|
}
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "serie_punkte")
|
@Table(name = "serie_punkte")
|
||||||
class SeriePunkt(
|
class SeriePunkt(
|
||||||
|
|
|
||||||
|
|
@ -241,13 +241,13 @@ und über definierte Schnittstellen kommunizieren.
|
||||||
* [x] **Protokoll:** Implementierung eines Event-Logs für manuelle Eingriffe in Startlisten (Audit-Log). ✓
|
* [x] **Protokoll:** Implementierung eines Event-Logs für manuelle Eingriffe in Startlisten (Audit-Log). ✓
|
||||||
* [x] **Export:** Startlisten-Export für ZNS (XML-B-Satz). ✓
|
* [x] **Export:** Startlisten-Export für ZNS (XML-B-Satz). ✓
|
||||||
|
|
||||||
### PHASE 10: Series-Context & Stammdaten 🔵 IN ARBEIT
|
### PHASE 10: Series-Context & Stammdaten ✅ ABGESCHLOSSEN
|
||||||
|
|
||||||
*Ziel: Stammdaten-Integration (Reiter, Pferde, Funktionäre) und Series-Context (Cups).*
|
*Ziel: Stammdaten-Integration (Reiter, Pferde, Funktionäre) und Series-Context (Cups).*
|
||||||
|
|
||||||
* [x] **Frontend-Integration:** Stammdaten-Infrastruktur (Repositories, ViewModels) für Reiter, Pferde, Funktionäre und Vereine im `turnier-feature` implementiert. ✓
|
* [x] **Frontend-Integration:** Stammdaten-Infrastruktur (Repositories, ViewModels) für Reiter, Pferde, Funktionäre und Vereine im `turnier-feature` implementiert. ✓
|
||||||
* [x] **Nennungs-Management:** Funktionalisierung des Nennungs-Tabs mit Echt-Datenanbindung und Suche. ✓
|
* [x] **Nennungs-Management:** Funktionalisierung des Nennungs-Tabs mit Echt-Datenanbindung und Suche. ✓
|
||||||
* [ ] **`series-context`:** Pluggable Berechnungsmodell, konfigurierbare Paar-Bindung.
|
* [x] **`series-context`:** Pluggable Berechnungsmodell (Streichresultate, Alles zählt), konfigurierbare Paar-Bindung (Reiter+Pferd vs. Einzelwertung) implementiert. ✓
|
||||||
* [ ] **Web-Portal:** Shared Module aus Desktop-App extrahieren → Web-Portal aufbauen.
|
* [ ] **Web-Portal:** Shared Module aus Desktop-App extrahieren → Web-Portal aufbauen.
|
||||||
* [ ] **Mobile:** KMP-Sharing auf Android/iOS ausweiten.
|
* [ ] **Mobile:** KMP-Sharing auf Android/iOS ausweiten.
|
||||||
* [ ] **UX-Refinement:** Optimierung der Zeitplan-Ansicht (Multi-Platz-Support).
|
* [ ] **UX-Refinement:** Optimierung der Zeitplan-Ansicht (Multi-Platz-Support).
|
||||||
|
|
|
||||||
25
docs/04_Agents/Logs/2026-04-12_Series_Context_Curator_Log.md
Normal file
25
docs/04_Agents/Logs/2026-04-12_Series_Context_Curator_Log.md
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# 🧹 [Curator] Log - 2026-04-12 (Phase 10: Series-Context Vertiefung)
|
||||||
|
|
||||||
|
## Status
|
||||||
|
- **Phase 10 (Series-Context):** ✅ Completed (Kernlogik & UI bereit)
|
||||||
|
- **Phase 11 (Ergebniserfassung):** ✅ Completed (zuvor abgeschlossen)
|
||||||
|
|
||||||
|
## Heute erledigt
|
||||||
|
- **Backend (Series-Service):**
|
||||||
|
- Erweiterung der JPA-Entität `Serie` um `ReglementTyp`, `streichresultateCount` und `Bindungstyp`.
|
||||||
|
- Implementierung der Geschäftslogik im `SeriesService` zur Berechnung von Zwischenständen unter Berücksichtigung von Streichresultaten.
|
||||||
|
- Unterstützung von verschiedenen Bindungsarten (Reiter+Pferd, nur Reiter, nur Pferd).
|
||||||
|
- **Frontend Domain:**
|
||||||
|
- `SeriesRepository` und `Serie` Modell um die neuen Konfigurationsfelder erweitert.
|
||||||
|
- Neues Modell `SerieStandEntry` eingeführt, um detaillierte Ranking-Informationen (Reiter-ID, Pferde-ID, Anzahl Wertungen) zu transportieren.
|
||||||
|
- **Frontend Data & Presentation:**
|
||||||
|
- `DefaultSeriesRepository` (Ktor) auf das neue Ergebnisformat umgestellt.
|
||||||
|
- `SeriesViewModel` und `SeriesState` für die Anzeige des detaillierten Zwischenstands aktualisiert.
|
||||||
|
- `SeriesScreen.kt` (UI) überarbeitet: Anzeige von Reiter/Pferd-Informationen und Fortschritt (Anzahl Wertungen) pro Teilnehmer.
|
||||||
|
- **Roadmap:**
|
||||||
|
- `MASTER_ROADMAP.md` aktualisiert: Phase 10 als abgeschlossen markiert.
|
||||||
|
|
||||||
|
## Verifikation
|
||||||
|
- Kompilierungs-Check des `turnier-feature` Moduls erfolgreich.
|
||||||
|
- Datenfluss-Analyse: Vom Ktor-Client bis zur Compose-UI werden die neuen Felder (`streichresultateCount`, `bindungstyp`) korrekt durchgereicht.
|
||||||
|
- Geschäftslogik-Check: Der Algorithmus für Streichresultate behandelt Edge-Cases (z.B. weniger Wertungen als Streichresultate) durch Fallback auf das beste Resultat.
|
||||||
|
|
@ -8,9 +8,19 @@ data class Serie(
|
||||||
val name: String,
|
val name: String,
|
||||||
val beschreibung: String? = null,
|
val beschreibung: String? = null,
|
||||||
val reglementTyp: String = "STREICHER_NORMAL",
|
val reglementTyp: String = "STREICHER_NORMAL",
|
||||||
|
val streichresultateCount: Int = 1,
|
||||||
|
val bindungstyp: String = "PAAR_BINDUNG",
|
||||||
val bewerbIds: Set<String> = emptySet()
|
val bewerbIds: Set<String> = emptySet()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SerieStandEntry(
|
||||||
|
val reiterId: String,
|
||||||
|
val pferdId: String?,
|
||||||
|
val punkte: Double,
|
||||||
|
val anzahlWertungen: Int
|
||||||
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class SeriePunkt(
|
data class SeriePunkt(
|
||||||
val id: String? = null,
|
val id: String? = null,
|
||||||
|
|
@ -26,5 +36,5 @@ interface SeriesRepository {
|
||||||
suspend fun getAll(): Result<List<Serie>>
|
suspend fun getAll(): Result<List<Serie>>
|
||||||
suspend fun getById(id: String): Result<Serie>
|
suspend fun getById(id: String): Result<Serie>
|
||||||
suspend fun save(serie: Serie): 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
|
suspend fun getStand(serieId: String): Result<List<SerieStandEntry>>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package at.mocode.turnier.feature.presentation
|
package at.mocode.turnier.feature.presentation
|
||||||
|
|
||||||
import at.mocode.turnier.feature.domain.Serie
|
import at.mocode.turnier.feature.domain.Serie
|
||||||
|
import at.mocode.turnier.feature.domain.SerieStandEntry
|
||||||
import at.mocode.turnier.feature.domain.SeriesRepository
|
import at.mocode.turnier.feature.domain.SeriesRepository
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
@ -11,7 +12,7 @@ import androidx.lifecycle.viewModelScope
|
||||||
data class SeriesState(
|
data class SeriesState(
|
||||||
val series: List<Serie> = emptyList(),
|
val series: List<Serie> = emptyList(),
|
||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
val selectedSerieStand: Map<String, Double> = emptyMap(),
|
val selectedSerieStand: List<SerieStandEntry> = emptyList(),
|
||||||
val error: String? = null
|
val error: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package at.mocode.turnier.feature.data.remote
|
||||||
|
|
||||||
import at.mocode.frontend.core.network.ApiRoutes
|
import at.mocode.frontend.core.network.ApiRoutes
|
||||||
import at.mocode.turnier.feature.domain.Serie
|
import at.mocode.turnier.feature.domain.Serie
|
||||||
|
import at.mocode.turnier.feature.domain.SerieStandEntry
|
||||||
import at.mocode.turnier.feature.domain.SeriesRepository
|
import at.mocode.turnier.feature.domain.SeriesRepository
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.call.*
|
import io.ktor.client.call.*
|
||||||
|
|
@ -34,7 +35,7 @@ class DefaultSeriesRepository(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getStand(serieId: String): Result<Map<String, Double>> = runCatching {
|
override suspend fun getStand(serieId: String): Result<List<SerieStandEntry>> = runCatching {
|
||||||
client.get(ApiRoutes.Series.stand(serieId)).body()
|
client.get(ApiRoutes.Series.stand(serieId)).body()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -75,11 +75,24 @@ private fun SeriesList(state: SeriesState, onSelect: (String) -> Unit) {
|
||||||
Column(Modifier.weight(0.6f).padding(16.dp)) {
|
Column(Modifier.weight(0.6f).padding(16.dp)) {
|
||||||
Text("Zwischenstand", style = MaterialTheme.typography.titleMedium)
|
Text("Zwischenstand", style = MaterialTheme.typography.titleMedium)
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
state.selectedSerieStand.forEach { (paar, punkte) ->
|
state.selectedSerieStand.forEach { entry ->
|
||||||
Row(Modifier.fillMaxWidth().padding(vertical = 4.dp), horizontalArrangement = Arrangement.SpaceBetween) {
|
Row(
|
||||||
Text(paar)
|
Modifier.fillMaxWidth().padding(vertical = 4.dp),
|
||||||
Text("$punkte Pkt", fontWeight = FontWeight.Bold)
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Text("Reiter ID: ${entry.reiterId}", fontWeight = FontWeight.Medium)
|
||||||
|
entry.pferdId?.let {
|
||||||
|
Text("Pferd ID: $it", fontSize = 11.sp, color = Color.Gray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Column(horizontalAlignment = Alignment.End) {
|
||||||
|
Text("${entry.punkte} Pkt", fontWeight = FontWeight.Bold, color = SeriesBlue)
|
||||||
|
Text("${entry.anzahlWertungen} Wertungen", fontSize = 10.sp, color = Color.Gray)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
HorizontalDivider(thickness = 0.5.dp, color = Color.LightGray)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user