feat(core+frontend+domain): add ZNS Bewerb parser and integrate start list feature
- **Parser Implementation:** - Introduced `ZnsBewerbParser` to parse n2-XXXXX.dat files and map B-Satz lines to the `ZnsBewerb` domain model. - Added test coverage for parsing B-Satz lines and edge cases in `ZnsParserTest`. - **Frontend Integration:** - Integrated ZNS import functionality into the `BewerbeTabContent` for uploading and previewing Bewerb data before import. - Enhanced `BewerbViewModel` with state and intents for managing ZNS import, preview dialogs, and import confirmation. - Supported start list generation and added modal for previewing generated start lists. - **Domain Services:** - Implemented `StartlistenService` to generate and calculate start times for start lists with respect to participant preferences. - Added extensive test coverage in `StartlistenServiceTest` to validate sorting, preferences, and time calculations. - **UI Enhancements:** - Updated `Bewerbe` tab layout with search, filtering, and action buttons for ZNS import and start list generation. - Introduced dialogs for ZNS import previews and start list previews.
This commit is contained in:
+115
@@ -0,0 +1,115 @@
|
||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.entries.domain.service
|
||||
|
||||
import at.mocode.core.domain.model.StartlistenStatusE
|
||||
import at.mocode.core.domain.model.StartwunschE
|
||||
import at.mocode.entries.domain.model.Abteilung
|
||||
import at.mocode.entries.domain.model.Bewerb
|
||||
import at.mocode.entries.domain.model.DomStartliste
|
||||
import at.mocode.entries.domain.model.Nennung
|
||||
import at.mocode.entries.domain.model.StartlistenEintrag
|
||||
import kotlinx.datetime.LocalTime
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Service zur Generierung und Zeitberechnung von Startlisten.
|
||||
*/
|
||||
class StartlistenService {
|
||||
|
||||
/**
|
||||
* Generiert eine neue Startliste für eine Abteilung basierend auf den Nennungen.
|
||||
*
|
||||
* @param abteilung Die Abteilung, für die die Startliste generiert wird.
|
||||
* @param bewerb Der zugehörige Bewerb (für Zeit-Parameter).
|
||||
* @param nennungen Liste der aktiven Nennungen für diese Abteilung.
|
||||
* @param reiterNamen Map von Reiter-ID zu Name (für Denormalisierung).
|
||||
* @param pferdeNamen Map von Pferde-ID zu Name (für Denormalisierung).
|
||||
* @param zufallssaat Optionaler Seed für die Zufallssortierung.
|
||||
* @return Die generierte [DomStartliste] im Status ENTWURF.
|
||||
*/
|
||||
fun generiereStartliste(
|
||||
abteilung: Abteilung,
|
||||
bewerb: Bewerb,
|
||||
nennungen: List<Nennung>,
|
||||
reiterNamen: Map<Uuid, String>,
|
||||
pferdeNamen: Map<Uuid, String>,
|
||||
zufallssaat: Long? = null
|
||||
): DomStartliste {
|
||||
// 1. Sortierung (Basis: Zufall oder Alphabetisch - hier vereinfacht Zufall)
|
||||
val sortierteNennungen = if (zufallssaat != null) {
|
||||
nennungen.shuffled(kotlin.random.Random(zufallssaat))
|
||||
} else {
|
||||
nennungen.shuffled()
|
||||
}
|
||||
|
||||
// 2. Startwünsche berücksichtigen (VORNE / HINTEN)
|
||||
// Einfache Implementierung: VORNE-Wünsche an den Anfang, HINTEN-Wünsche ans Ende
|
||||
val vorne = sortierteNennungen.filter { it.startwunsch == StartwunschE.VORNE }
|
||||
val neutral = sortierteNennungen.filter { it.startwunsch == StartwunschE.KEIN_WUNSCH }
|
||||
val hinten = sortierteNennungen.filter { it.startwunsch == StartwunschE.HINTEN }
|
||||
|
||||
val finaleNennungen = vorne + neutral + hinten
|
||||
|
||||
// 3. Einträge erstellen
|
||||
val eintraege = finaleNennungen.mapIndexed { index, nennung ->
|
||||
StartlistenEintrag(
|
||||
startnummer = index + 1,
|
||||
nennungId = nennung.nennungId,
|
||||
reiterName = reiterNamen[nennung.reiterId] ?: "Unbekannter Reiter",
|
||||
pferdeName = pferdeNamen[nennung.pferdId] ?: "Unbekanntes Pferd",
|
||||
startwunsch = nennung.startwunsch.name
|
||||
)
|
||||
}
|
||||
|
||||
return DomStartliste(
|
||||
abteilungId = abteilung.abteilungId,
|
||||
bewerbId = bewerb.bewerbId,
|
||||
turnierId = bewerb.turnierId,
|
||||
status = StartlistenStatusE.ENTWURF,
|
||||
eintraege = eintraege
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet die Startzeiten für die Einträge einer Startliste.
|
||||
*
|
||||
* @param startliste Die Startliste, deren Zeiten berechnet werden sollen.
|
||||
* @param bewerb Der zugehörige Bewerb mit den Zeit-Parametern.
|
||||
* @param abteilung Die zugehörige Abteilung (für die Startzeit).
|
||||
* @return Eine Map von Startnummer zu berechneter [LocalTime].
|
||||
*/
|
||||
fun berechneStartzeiten(
|
||||
startliste: DomStartliste,
|
||||
bewerb: Bewerb,
|
||||
abteilung: Abteilung
|
||||
): Map<Int, LocalTime> {
|
||||
val basisZeit = abteilung.startzeit?.let { LocalTime.parse(it) }
|
||||
?: bewerb.beginnZeit
|
||||
?: return emptyMap()
|
||||
|
||||
val reitdauer = bewerb.reitdauerMinuten ?: 0
|
||||
val umbau = bewerb.umbauMinuten ?: 0
|
||||
val besichtigung = bewerb.besichtigungMinuten ?: 0
|
||||
|
||||
val zeiten = mutableMapOf<Int, LocalTime>()
|
||||
var aktuelleZeitInMinuten = basisZeit.hour * 60 + basisZeit.minute
|
||||
|
||||
// Besichtigung vor dem ersten Starter
|
||||
aktuelleZeitInMinuten += besichtigung
|
||||
|
||||
startliste.eintraege.forEach { eintrag ->
|
||||
val stunden = aktuelleZeitInMinuten / 60
|
||||
val minuten = aktuelleZeitInMinuten % 60
|
||||
zeiten[eintrag.startnummer] = LocalTime(stunden % 24, minuten)
|
||||
|
||||
// Zeit für den nächsten Starter berechnen
|
||||
aktuelleZeitInMinuten += reitdauer
|
||||
|
||||
// TODO: Umbauzeiten nach bestimmten Intervallen (z.B. alle 10 Starter)
|
||||
// oder bei Abteilungswechsel berücksichtigen.
|
||||
}
|
||||
|
||||
return zeiten
|
||||
}
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.entries.domain.service
|
||||
|
||||
import at.mocode.core.domain.model.*
|
||||
import at.mocode.entries.domain.model.*
|
||||
import kotlinx.datetime.LocalTime
|
||||
import kotlin.test.*
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
class StartlistenServiceTest {
|
||||
|
||||
private val service = StartlistenService()
|
||||
|
||||
@Test
|
||||
fun `generiereStartliste sollte Eintraege fuer alle Nennungen erstellen`() {
|
||||
val bewerb = createBewerb()
|
||||
val abteilung = createAbteilung(bewerb.bewerbId, 1)
|
||||
val nennungen = listOf(
|
||||
createNennung(abteilung.abteilungId, bewerb.bewerbId),
|
||||
createNennung(abteilung.abteilungId, bewerb.bewerbId)
|
||||
)
|
||||
val reiterNamen = nennungen.associate { it.reiterId to "Reiter ${it.reiterId}" }
|
||||
val pferdeNamen = nennungen.associate { it.pferdId to "Pferd ${it.pferdId}" }
|
||||
|
||||
val startliste = service.generiereStartliste(
|
||||
abteilung, bewerb, nennungen, reiterNamen, pferdeNamen
|
||||
)
|
||||
|
||||
assertEquals(2, startliste.eintraege.size)
|
||||
assertEquals(StartlistenStatusE.ENTWURF, startliste.status)
|
||||
assertEquals(1, startliste.eintraege[0].startnummer)
|
||||
assertEquals(2, startliste.eintraege[1].startnummer)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `generiereStartliste sollte Startwuensche beruecksichtigen`() {
|
||||
val bewerb = createBewerb()
|
||||
val abteilung = createAbteilung(bewerb.bewerbId, 1)
|
||||
val n1 = createNennung(abteilung.abteilungId, bewerb.bewerbId, startwunsch = StartwunschE.HINTEN)
|
||||
val n2 = createNennung(abteilung.abteilungId, bewerb.bewerbId, startwunsch = StartwunschE.VORNE)
|
||||
val n3 = createNennung(abteilung.abteilungId, bewerb.bewerbId, startwunsch = StartwunschE.KEIN_WUNSCH)
|
||||
|
||||
val nennungen = listOf(n1, n2, n3)
|
||||
val reiterNamen = nennungen.associate { it.reiterId to "R" }
|
||||
val pferdeNamen = nennungen.associate { it.pferdId to "P" }
|
||||
|
||||
val startliste = service.generiereStartliste(
|
||||
abteilung, bewerb, nennungen, reiterNamen, pferdeNamen, zufallssaat = 42
|
||||
)
|
||||
|
||||
// VORNE (n2) -> KEIN_WUNSCH (n3) -> HINTEN (n1)
|
||||
assertEquals(n2.nennungId, startliste.eintraege[0].nennungId)
|
||||
assertEquals(n3.nennungId, startliste.eintraege[1].nennungId)
|
||||
assertEquals(n1.nennungId, startliste.eintraege[2].nennungId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `berechneStartzeiten sollte Zeiten korrekt aufsummieren`() {
|
||||
val bewerb = createBewerb(
|
||||
beginnZeit = LocalTime(8, 0),
|
||||
reitdauerMinuten = 5,
|
||||
besichtigungMinuten = 10
|
||||
)
|
||||
val abteilung = createAbteilung(bewerb.bewerbId, 1)
|
||||
val e1 = createStartlistenEintrag(1)
|
||||
val e2 = createStartlistenEintrag(2)
|
||||
val startliste = createStartliste(abteilung.abteilungId, listOf(e1, e2))
|
||||
|
||||
val zeiten = service.berechneStartzeiten(startliste, bewerb, abteilung)
|
||||
|
||||
// 08:00 + 10m Besichtigung = 08:10 (Starter 1)
|
||||
// 08:10 + 5m Reitdauer = 08:15 (Starter 2)
|
||||
assertEquals(LocalTime(8, 10), zeiten[1])
|
||||
assertEquals(LocalTime(8, 15), zeiten[2])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `berechneStartzeiten sollte Abteilungs-Startzeit bevorzugen`() {
|
||||
val bewerb = createBewerb(beginnZeit = LocalTime(8, 0), reitdauerMinuten = 2)
|
||||
val abteilung = createAbteilung(bewerb.bewerbId, 1, startzeit = "09:30")
|
||||
val e1 = createStartlistenEintrag(1)
|
||||
val startliste = createStartliste(abteilung.abteilungId, listOf(e1))
|
||||
|
||||
val zeiten = service.berechneStartzeiten(startliste, bewerb, abteilung)
|
||||
|
||||
assertEquals(LocalTime(9, 30), zeiten[1])
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
private fun createBewerb(
|
||||
beginnZeit: LocalTime? = null,
|
||||
reitdauerMinuten: Int? = null,
|
||||
besichtigungMinuten: Int? = null
|
||||
) = Bewerb(
|
||||
turnierId = Uuid.random(),
|
||||
bewerbNummer = 1,
|
||||
bezeichnung = "Test",
|
||||
sparte = SparteE.SPRINGEN,
|
||||
turnierkategorie = TurnierkategorieE.B,
|
||||
pruefungsTyp = PruefungsTypE.SPRINGEN_UEBRIG,
|
||||
beginnZeit = beginnZeit,
|
||||
reitdauerMinuten = reitdauerMinuten,
|
||||
besichtigungMinuten = besichtigungMinuten
|
||||
)
|
||||
|
||||
private fun createAbteilung(bewerbId: Uuid, nummer: Int, startzeit: String? = null) = Abteilung(
|
||||
bewerbId = bewerbId,
|
||||
abteilungsNummer = nummer,
|
||||
startzeit = startzeit
|
||||
)
|
||||
|
||||
private fun createNennung(
|
||||
abteilungId: Uuid,
|
||||
bewerbId: Uuid,
|
||||
startwunsch: StartwunschE = StartwunschE.KEIN_WUNSCH
|
||||
) = Nennung(
|
||||
abteilungId = abteilungId,
|
||||
bewerbId = bewerbId,
|
||||
turnierId = Uuid.random(),
|
||||
reiterId = Uuid.random(),
|
||||
pferdId = Uuid.random(),
|
||||
startwunsch = startwunsch
|
||||
)
|
||||
|
||||
private fun createStartlistenEintrag(nr: Int) = StartlistenEintrag(
|
||||
startnummer = nr,
|
||||
nennungId = Uuid.random(),
|
||||
reiterName = "R$nr",
|
||||
pferdeName = "P$nr"
|
||||
)
|
||||
|
||||
private fun createStartliste(abtId: Uuid, eintraege: List<StartlistenEintrag>) = DomStartliste(
|
||||
abteilungId = abtId,
|
||||
bewerbId = Uuid.random(),
|
||||
turnierId = Uuid.random(),
|
||||
eintraege = eintraege
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user