feat(entries+time-scheduling): add support for automatic breaks and inspection type configurations
- **Domain Enhancements:** - Introduced `PausenKonfiguration` and `BesichtigungsBlock` entities to handle automatic breaks and inspection scheduling. - Added `BesichtigungsTypE` enum for inspection types (`ZU_FUSS`, `ZU_PFERD`). - Updated `Bewerb` and `Abteilung` models to include pause and inspection type fields. - **Service Updates:** - Enhanced `StartlistenService` to calculate start times, accounting for breaks and inspection buffers. - Extended `BewerbService` to support patchable time scheduling via new `updateZeitplan` API. - **Persistence Changes:** - Updated tables (`BewerbTable`, `AbteilungTable`) to persist break configurations and inspection types. - Implemented repository mappings to include these new fields. - **Testing:** - Introduced `BewerbeZeitplanIntegrationTest` to validate new scheduling behaviors, including automatic pauses and inspection handling. - **Documentation:** - Added rulebook and conceptual documents for inspection and scheduling logic in `docs/01_Architecture/`.
This commit is contained in:
+4
@@ -3,6 +3,7 @@
|
|||||||
package at.mocode.entries.domain.model
|
package at.mocode.entries.domain.model
|
||||||
|
|
||||||
import at.mocode.core.domain.model.AbteilungsTeilungsTypE
|
import at.mocode.core.domain.model.AbteilungsTeilungsTypE
|
||||||
|
import at.mocode.core.domain.model.BesichtigungsTypE
|
||||||
import at.mocode.core.domain.serialization.InstantSerializer
|
import at.mocode.core.domain.serialization.InstantSerializer
|
||||||
import at.mocode.core.domain.serialization.UuidSerializer
|
import at.mocode.core.domain.serialization.UuidSerializer
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
@@ -55,6 +56,9 @@ data class Abteilung(
|
|||||||
// Zeitplanung
|
// Zeitplanung
|
||||||
var startzeit: String? = null,
|
var startzeit: String? = null,
|
||||||
|
|
||||||
|
/** Besichtigungstyp für diese Abteilung (optional, wenn abweichend von Standard). */
|
||||||
|
var besichtigungsTyp: BesichtigungsTypE? = null,
|
||||||
|
|
||||||
// Verwaltung
|
// Verwaltung
|
||||||
var bemerkungen: String? = null,
|
var bemerkungen: String? = null,
|
||||||
|
|
||||||
|
|||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||||
|
|
||||||
|
package at.mocode.entries.domain.model
|
||||||
|
|
||||||
|
import at.mocode.core.domain.model.BesichtigungsTypE
|
||||||
|
import at.mocode.core.domain.serialization.UuidSerializer
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repräsentiert einen Zeitblock für die Parcoursbesichtigung.
|
||||||
|
* Kann mit mehreren Abteilungen oder Bewerben verknüpft sein.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class BesichtigungsBlock(
|
||||||
|
@Serializable(with = UuidSerializer::class)
|
||||||
|
val besichtigungsBlockId: Uuid = Uuid.random(),
|
||||||
|
|
||||||
|
/** Typ der Besichtigung (zu Fuß / zu Pferd). */
|
||||||
|
val typ: BesichtigungsTypE = BesichtigungsTypE.ZU_FUSS,
|
||||||
|
|
||||||
|
/** Geplante Dauer in Minuten. */
|
||||||
|
val dauerMinuten: Int = 15,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liste der verknüpften Abteilungs-IDs.
|
||||||
|
* Eine Besichtigung kann für mehrere Abteilungen gleichzeitig stattfinden.
|
||||||
|
*/
|
||||||
|
val abteilungIds: List<@Serializable(with = UuidSerializer::class) Uuid> = emptyList(),
|
||||||
|
|
||||||
|
/** Optionaler Puffer nach der Besichtigung bis zum ersten Start (Standard: 5 Min gemäß ÖTO). */
|
||||||
|
val pufferMinuten: Int = 5
|
||||||
|
)
|
||||||
+19
@@ -97,6 +97,10 @@ data class Bewerb(
|
|||||||
var reitdauerMinuten: Int? = null,
|
var reitdauerMinuten: Int? = null,
|
||||||
var umbauMinuten: Int? = null,
|
var umbauMinuten: Int? = null,
|
||||||
var besichtigungMinuten: Int? = null,
|
var besichtigungMinuten: Int? = null,
|
||||||
|
|
||||||
|
/** Konfiguration für Pausen während der Prüfung. */
|
||||||
|
var pausenKonfiguration: PausenKonfiguration? = null,
|
||||||
|
|
||||||
var stechenGeplant: Boolean = false,
|
var stechenGeplant: Boolean = false,
|
||||||
|
|
||||||
// Finanzen
|
// Finanzen
|
||||||
@@ -297,3 +301,18 @@ data class Bewerb(
|
|||||||
*/
|
*/
|
||||||
fun withUpdatedTimestamp(): Bewerb = this.copy(updatedAt = Clock.System.now())
|
fun withUpdatedTimestamp(): Bewerb = this.copy(updatedAt = Clock.System.now())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Konfiguration für automatische Pausen nach einer bestimmten Anzahl von Startern.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class PausenKonfiguration(
|
||||||
|
/** Pause alle X Starter (0 = keine automatischen Pausen). */
|
||||||
|
val starterIntervall: Int = 0,
|
||||||
|
|
||||||
|
/** Dauer der Pause in Minuten. */
|
||||||
|
val dauerMinuten: Int = 10,
|
||||||
|
|
||||||
|
/** Optionale Bezeichnung (z.B. "Platzpflege"). */
|
||||||
|
val bezeichnung: String? = null
|
||||||
|
)
|
||||||
|
|||||||
+12
-4
@@ -98,16 +98,24 @@ class StartlistenService {
|
|||||||
// Besichtigung vor dem ersten Starter
|
// Besichtigung vor dem ersten Starter
|
||||||
aktuelleZeitInMinuten += besichtigung
|
aktuelleZeitInMinuten += besichtigung
|
||||||
|
|
||||||
startliste.eintraege.forEach { eintrag ->
|
// Puffer nach Besichtigung (ÖTO)
|
||||||
|
if (besichtigung > 0) {
|
||||||
|
aktuelleZeitInMinuten += 5
|
||||||
|
}
|
||||||
|
|
||||||
|
startliste.eintraege.forEachIndexed { index, eintrag ->
|
||||||
|
// Pause nach Intervall berücksichtigen
|
||||||
|
val pausenKonf = bewerb.pausenKonfiguration
|
||||||
|
if (pausenKonf != null && pausenKonf.starterIntervall > 0 && index > 0 && index % pausenKonf.starterIntervall == 0) {
|
||||||
|
aktuelleZeitInMinuten += pausenKonf.dauerMinuten
|
||||||
|
}
|
||||||
|
|
||||||
val stunden = aktuelleZeitInMinuten / 60
|
val stunden = aktuelleZeitInMinuten / 60
|
||||||
val minuten = aktuelleZeitInMinuten % 60
|
val minuten = aktuelleZeitInMinuten % 60
|
||||||
zeiten[eintrag.startnummer] = LocalTime(stunden % 24, minuten)
|
zeiten[eintrag.startnummer] = LocalTime(stunden % 24, minuten)
|
||||||
|
|
||||||
// Zeit für den nächsten Starter berechnen
|
// Zeit für den nächsten Starter berechnen
|
||||||
aktuelleZeitInMinuten += reitdauer
|
aktuelleZeitInMinuten += reitdauer
|
||||||
|
|
||||||
// TODO: Umbauzeiten nach bestimmten Intervallen (z.B. alle 10 Starter)
|
|
||||||
// oder bei Abteilungswechsel berücksichtigen.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return zeiten
|
return zeiten
|
||||||
|
|||||||
+1
@@ -10,6 +10,7 @@ data class Abteilung(
|
|||||||
val nr: Int,
|
val nr: Int,
|
||||||
val bezeichnung: String,
|
val bezeichnung: String,
|
||||||
val typ: String,
|
val typ: String,
|
||||||
|
val besichtigungsTyp: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
interface AbteilungRepository {
|
interface AbteilungRepository {
|
||||||
|
|||||||
+4
-1
@@ -22,7 +22,8 @@ class AbteilungRepositoryImpl : AbteilungRepository {
|
|||||||
bewerbId = row[AbteilungTable.bewerbId].toKotlinUuid(),
|
bewerbId = row[AbteilungTable.bewerbId].toKotlinUuid(),
|
||||||
nr = row[AbteilungTable.nr],
|
nr = row[AbteilungTable.nr],
|
||||||
bezeichnung = row[AbteilungTable.bezeichnung],
|
bezeichnung = row[AbteilungTable.bezeichnung],
|
||||||
typ = row[AbteilungTable.typ]
|
typ = row[AbteilungTable.typ],
|
||||||
|
besichtigungsTyp = row[AbteilungTable.besichtigungsTyp]
|
||||||
)
|
)
|
||||||
|
|
||||||
override suspend fun create(a: Abteilung): Abteilung = tenantTransaction {
|
override suspend fun create(a: Abteilung): Abteilung = tenantTransaction {
|
||||||
@@ -33,6 +34,7 @@ class AbteilungRepositoryImpl : AbteilungRepository {
|
|||||||
s[AbteilungTable.nr] = a.nr
|
s[AbteilungTable.nr] = a.nr
|
||||||
s[AbteilungTable.bezeichnung] = a.bezeichnung
|
s[AbteilungTable.bezeichnung] = a.bezeichnung
|
||||||
s[AbteilungTable.typ] = a.typ
|
s[AbteilungTable.typ] = a.typ
|
||||||
|
s[AbteilungTable.besichtigungsTyp] = a.besichtigungsTyp
|
||||||
s[AbteilungTable.createdAt] = now
|
s[AbteilungTable.createdAt] = now
|
||||||
s[AbteilungTable.updatedAt] = now
|
s[AbteilungTable.updatedAt] = now
|
||||||
}
|
}
|
||||||
@@ -53,6 +55,7 @@ class AbteilungRepositoryImpl : AbteilungRepository {
|
|||||||
s[AbteilungTable.nr] = a.nr
|
s[AbteilungTable.nr] = a.nr
|
||||||
s[AbteilungTable.bezeichnung] = a.bezeichnung
|
s[AbteilungTable.bezeichnung] = a.bezeichnung
|
||||||
s[AbteilungTable.typ] = a.typ
|
s[AbteilungTable.typ] = a.typ
|
||||||
|
s[AbteilungTable.besichtigungsTyp] = a.besichtigungsTyp
|
||||||
s[AbteilungTable.updatedAt] = now
|
s[AbteilungTable.updatedAt] = now
|
||||||
}
|
}
|
||||||
AbteilungTable.selectAll().where { AbteilungTable.id eq a.id.toJavaUuid() }.map(::rowToAbteilung).single()
|
AbteilungTable.selectAll().where { AbteilungTable.id eq a.id.toJavaUuid() }.map(::rowToAbteilung).single()
|
||||||
|
|||||||
+4
@@ -33,6 +33,10 @@ data class Bewerb(
|
|||||||
val umbauMinuten: Int? = null,
|
val umbauMinuten: Int? = null,
|
||||||
val besichtigungMinuten: Int? = null,
|
val besichtigungMinuten: Int? = null,
|
||||||
val stechenGeplant: Boolean = false,
|
val stechenGeplant: Boolean = false,
|
||||||
|
// Pausen
|
||||||
|
val pausenStarterIntervall: Int? = null,
|
||||||
|
val pausenDauerMinuten: Int? = null,
|
||||||
|
val pausenBezeichnung: String? = null,
|
||||||
// Finanzen
|
// Finanzen
|
||||||
val startgeldCent: Long? = null,
|
val startgeldCent: Long? = null,
|
||||||
val nenngeldCent: Long? = null,
|
val nenngeldCent: Long? = null,
|
||||||
|
|||||||
+12
@@ -70,6 +70,10 @@ class BewerbRepositoryImpl : BewerbRepository {
|
|||||||
umbauMinuten = row[BewerbTable.umbauMinuten],
|
umbauMinuten = row[BewerbTable.umbauMinuten],
|
||||||
besichtigungMinuten = row[BewerbTable.besichtigungMinuten],
|
besichtigungMinuten = row[BewerbTable.besichtigungMinuten],
|
||||||
stechenGeplant = row[BewerbTable.stechenGeplant],
|
stechenGeplant = row[BewerbTable.stechenGeplant],
|
||||||
|
// Pausen
|
||||||
|
pausenStarterIntervall = row[BewerbTable.pausenStarterIntervall],
|
||||||
|
pausenDauerMinuten = row[BewerbTable.pausenDauerMinuten],
|
||||||
|
pausenBezeichnung = row[BewerbTable.pausenBezeichnung],
|
||||||
// Finanzen
|
// Finanzen
|
||||||
startgeldCent = row[BewerbTable.startgeldCent],
|
startgeldCent = row[BewerbTable.startgeldCent],
|
||||||
nenngeldCent = row[BewerbTable.nenngeldCent],
|
nenngeldCent = row[BewerbTable.nenngeldCent],
|
||||||
@@ -106,6 +110,10 @@ class BewerbRepositoryImpl : BewerbRepository {
|
|||||||
s[BewerbTable.umbauMinuten] = b.umbauMinuten
|
s[BewerbTable.umbauMinuten] = b.umbauMinuten
|
||||||
s[BewerbTable.besichtigungMinuten] = b.besichtigungMinuten
|
s[BewerbTable.besichtigungMinuten] = b.besichtigungMinuten
|
||||||
s[BewerbTable.stechenGeplant] = b.stechenGeplant
|
s[BewerbTable.stechenGeplant] = b.stechenGeplant
|
||||||
|
// Pausen
|
||||||
|
s[BewerbTable.pausenStarterIntervall] = b.pausenStarterIntervall
|
||||||
|
s[BewerbTable.pausenDauerMinuten] = b.pausenDauerMinuten
|
||||||
|
s[BewerbTable.pausenBezeichnung] = b.pausenBezeichnung
|
||||||
// Finanzen
|
// Finanzen
|
||||||
s[BewerbTable.startgeldCent] = b.startgeldCent
|
s[BewerbTable.startgeldCent] = b.startgeldCent
|
||||||
s[BewerbTable.nenngeldCent] = b.nenngeldCent
|
s[BewerbTable.nenngeldCent] = b.nenngeldCent
|
||||||
@@ -155,6 +163,10 @@ class BewerbRepositoryImpl : BewerbRepository {
|
|||||||
s[BewerbTable.umbauMinuten] = b.umbauMinuten
|
s[BewerbTable.umbauMinuten] = b.umbauMinuten
|
||||||
s[BewerbTable.besichtigungMinuten] = b.besichtigungMinuten
|
s[BewerbTable.besichtigungMinuten] = b.besichtigungMinuten
|
||||||
s[BewerbTable.stechenGeplant] = b.stechenGeplant
|
s[BewerbTable.stechenGeplant] = b.stechenGeplant
|
||||||
|
// Pausen
|
||||||
|
s[BewerbTable.pausenStarterIntervall] = b.pausenStarterIntervall
|
||||||
|
s[BewerbTable.pausenDauerMinuten] = b.pausenDauerMinuten
|
||||||
|
s[BewerbTable.pausenBezeichnung] = b.pausenBezeichnung
|
||||||
// Finanzen
|
// Finanzen
|
||||||
s[BewerbTable.startgeldCent] = b.startgeldCent
|
s[BewerbTable.startgeldCent] = b.startgeldCent
|
||||||
s[BewerbTable.nenngeldCent] = b.nenngeldCent
|
s[BewerbTable.nenngeldCent] = b.nenngeldCent
|
||||||
|
|||||||
+27
@@ -6,6 +6,7 @@ import at.mocode.entries.domain.repository.NennungRepository
|
|||||||
import at.mocode.entries.service.errors.LockedException
|
import at.mocode.entries.service.errors.LockedException
|
||||||
import at.mocode.entries.service.persistence.TurnierTable
|
import at.mocode.entries.service.persistence.TurnierTable
|
||||||
import at.mocode.entries.service.tenant.tenantTransaction
|
import at.mocode.entries.service.tenant.tenantTransaction
|
||||||
|
import at.mocode.entries.domain.model.PausenKonfiguration
|
||||||
import at.mocode.entries.domain.model.RichterEinsatz
|
import at.mocode.entries.domain.model.RichterEinsatz
|
||||||
import at.mocode.entries.domain.service.CompetitionWarningService
|
import at.mocode.entries.domain.service.CompetitionWarningService
|
||||||
import org.jetbrains.exposed.v1.core.eq
|
import org.jetbrains.exposed.v1.core.eq
|
||||||
@@ -13,6 +14,8 @@ import org.jetbrains.exposed.v1.jdbc.selectAll
|
|||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
import kotlin.uuid.toJavaUuid
|
import kotlin.uuid.toJavaUuid
|
||||||
|
|
||||||
|
typealias DomBewerb = at.mocode.entries.domain.model.Bewerb
|
||||||
|
|
||||||
class BewerbService(
|
class BewerbService(
|
||||||
private val repo: BewerbRepository,
|
private val repo: BewerbRepository,
|
||||||
private val nennungen: NennungRepository,
|
private val nennungen: NennungRepository,
|
||||||
@@ -53,6 +56,10 @@ class BewerbService(
|
|||||||
umbauMinuten = req.umbauMinuten,
|
umbauMinuten = req.umbauMinuten,
|
||||||
besichtigungMinuten = req.besichtigungMinuten,
|
besichtigungMinuten = req.besichtigungMinuten,
|
||||||
stechenGeplant = req.stechenGeplant,
|
stechenGeplant = req.stechenGeplant,
|
||||||
|
// Pausen
|
||||||
|
pausenStarterIntervall = req.pausenStarterIntervall,
|
||||||
|
pausenDauerMinuten = req.pausenDauerMinuten,
|
||||||
|
pausenBezeichnung = req.pausenBezeichnung,
|
||||||
// Finanzen
|
// Finanzen
|
||||||
startgeldCent = req.startgeldCent,
|
startgeldCent = req.startgeldCent,
|
||||||
geldpreisAusbezahlt = req.geldpreisAusbezahlt
|
geldpreisAusbezahlt = req.geldpreisAusbezahlt
|
||||||
@@ -93,6 +100,11 @@ class BewerbService(
|
|||||||
umbauMinuten = req.umbauMinuten,
|
umbauMinuten = req.umbauMinuten,
|
||||||
besichtigungMinuten = req.besichtigungMinuten,
|
besichtigungMinuten = req.besichtigungMinuten,
|
||||||
stechenGeplant = req.stechenGeplant,
|
stechenGeplant = req.stechenGeplant,
|
||||||
|
// Pausen
|
||||||
|
pausenStarterIntervall = req.pausenStarterIntervall,
|
||||||
|
pausenDauerMinuten = req.pausenDauerMinuten,
|
||||||
|
pausenBezeichnung = req.pausenBezeichnung,
|
||||||
|
// Finanzen
|
||||||
startgeldCent = req.startgeldCent,
|
startgeldCent = req.startgeldCent,
|
||||||
geldpreisAusbezahlt = req.geldpreisAusbezahlt,
|
geldpreisAusbezahlt = req.geldpreisAusbezahlt,
|
||||||
znsNummer = req.znsNummer,
|
znsNummer = req.znsNummer,
|
||||||
@@ -133,6 +145,10 @@ class BewerbService(
|
|||||||
umbauMinuten = req.umbauMinuten,
|
umbauMinuten = req.umbauMinuten,
|
||||||
besichtigungMinuten = req.besichtigungMinuten,
|
besichtigungMinuten = req.besichtigungMinuten,
|
||||||
stechenGeplant = req.stechenGeplant,
|
stechenGeplant = req.stechenGeplant,
|
||||||
|
// Pausen
|
||||||
|
pausenStarterIntervall = req.pausenStarterIntervall,
|
||||||
|
pausenDauerMinuten = req.pausenDauerMinuten,
|
||||||
|
pausenBezeichnung = req.pausenBezeichnung,
|
||||||
// Finanzen
|
// Finanzen
|
||||||
startgeldCent = req.startgeldCent,
|
startgeldCent = req.startgeldCent,
|
||||||
geldpreisAusbezahlt = req.geldpreisAusbezahlt,
|
geldpreisAusbezahlt = req.geldpreisAusbezahlt,
|
||||||
@@ -140,6 +156,17 @@ class BewerbService(
|
|||||||
return repo.update(updated)
|
return repo.update(updated)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun updateZeitplan(id: Uuid, req: UpdateZeitplanRequest): Bewerb {
|
||||||
|
val current = get(id)
|
||||||
|
// Hier erlauben wir Änderungen auch wenn PUBLISHED (da Drag & Drop im Live-Betrieb nötig)
|
||||||
|
val updated = current.copy(
|
||||||
|
geplantesDatum = req.geplantesDatum,
|
||||||
|
beginnZeit = req.beginnZeit,
|
||||||
|
austragungsplatzId = req.austragungsplatzId?.let { Uuid.parse(it) }
|
||||||
|
)
|
||||||
|
return repo.update(updated)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun delete(id: Uuid) {
|
suspend fun delete(id: Uuid) {
|
||||||
val current = get(id)
|
val current = get(id)
|
||||||
if (isTurnierPublished(current.turnierId)) throw LockedException("Turnier ist PUBLISHED – Bewerbe können nicht gelöscht werden")
|
if (isTurnierPublished(current.turnierId)) throw LockedException("Turnier ist PUBLISHED – Bewerbe können nicht gelöscht werden")
|
||||||
|
|||||||
+32
@@ -47,6 +47,11 @@ data class CreateBewerbRequest(
|
|||||||
val besichtigungMinuten: Int? = null,
|
val besichtigungMinuten: Int? = null,
|
||||||
val stechenGeplant: Boolean = false,
|
val stechenGeplant: Boolean = false,
|
||||||
|
|
||||||
|
// Pausen
|
||||||
|
val pausenStarterIntervall: Int? = null,
|
||||||
|
val pausenDauerMinuten: Int? = null,
|
||||||
|
val pausenBezeichnung: String? = null,
|
||||||
|
|
||||||
// Finanzen
|
// Finanzen
|
||||||
val startgeldCent: Long? = null,
|
val startgeldCent: Long? = null,
|
||||||
val geldpreisAusbezahlt: Boolean = false,
|
val geldpreisAusbezahlt: Boolean = false,
|
||||||
@@ -84,6 +89,11 @@ data class UpdateBewerbRequest(
|
|||||||
val besichtigungMinuten: Int? = null,
|
val besichtigungMinuten: Int? = null,
|
||||||
val stechenGeplant: Boolean = false,
|
val stechenGeplant: Boolean = false,
|
||||||
|
|
||||||
|
// Pausen
|
||||||
|
val pausenStarterIntervall: Int? = null,
|
||||||
|
val pausenDauerMinuten: Int? = null,
|
||||||
|
val pausenBezeichnung: String? = null,
|
||||||
|
|
||||||
// Finanzen
|
// Finanzen
|
||||||
val startgeldCent: Long? = null,
|
val startgeldCent: Long? = null,
|
||||||
val geldpreisAusbezahlt: Boolean = false,
|
val geldpreisAusbezahlt: Boolean = false,
|
||||||
@@ -122,6 +132,11 @@ data class BewerbResponse(
|
|||||||
val besichtigungMinuten: Int?,
|
val besichtigungMinuten: Int?,
|
||||||
val stechenGeplant: Boolean,
|
val stechenGeplant: Boolean,
|
||||||
|
|
||||||
|
// Pausen
|
||||||
|
val pausenStarterIntervall: Int?,
|
||||||
|
val pausenDauerMinuten: Int?,
|
||||||
|
val pausenBezeichnung: String?,
|
||||||
|
|
||||||
// Finanzen
|
// Finanzen
|
||||||
val startgeldCent: Long?,
|
val startgeldCent: Long?,
|
||||||
val geldpreisAusbezahlt: Boolean,
|
val geldpreisAusbezahlt: Boolean,
|
||||||
@@ -129,9 +144,17 @@ data class BewerbResponse(
|
|||||||
// ZNS-Integration
|
// ZNS-Integration
|
||||||
val znsNummer: Int?,
|
val znsNummer: Int?,
|
||||||
val znsAbteilung: Int?,
|
val znsAbteilung: Int?,
|
||||||
|
|
||||||
val warnungen: List<AbteilungsWarnungDto> = emptyList(),
|
val warnungen: List<AbteilungsWarnungDto> = emptyList(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** Request für schnelles Zeitplan-Update (Drag & Drop). */
|
||||||
|
data class UpdateZeitplanRequest(
|
||||||
|
val geplantesDatum: LocalDate?,
|
||||||
|
val beginnZeit: LocalTime?,
|
||||||
|
val austragungsplatzId: String?
|
||||||
|
)
|
||||||
|
|
||||||
data class AbteilungsWarnungDto(
|
data class AbteilungsWarnungDto(
|
||||||
val code: AbteilungsWarnungCodeE,
|
val code: AbteilungsWarnungCodeE,
|
||||||
val nachricht: String,
|
val nachricht: String,
|
||||||
@@ -168,6 +191,9 @@ private fun domainToDto(b: Bewerb, warnungen: List<AbteilungsWarnung> = emptyLis
|
|||||||
geldpreisAusbezahlt = b.geldpreisAusbezahlt,
|
geldpreisAusbezahlt = b.geldpreisAusbezahlt,
|
||||||
znsNummer = b.znsNummer,
|
znsNummer = b.znsNummer,
|
||||||
znsAbteilung = b.znsAbteilung,
|
znsAbteilung = b.znsAbteilung,
|
||||||
|
pausenStarterIntervall = b.pausenStarterIntervall,
|
||||||
|
pausenDauerMinuten = b.pausenDauerMinuten,
|
||||||
|
pausenBezeichnung = b.pausenBezeichnung,
|
||||||
warnungen = warnungen.map { AbteilungsWarnungDto(it.code, it.nachricht, it.oetoParagraph) }
|
warnungen = warnungen.map { AbteilungsWarnungDto(it.code, it.nachricht, it.oetoParagraph) }
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -232,4 +258,10 @@ class BewerbeController(
|
|||||||
suspend fun delete(@PathVariable id: String) {
|
suspend fun delete(@PathVariable id: String) {
|
||||||
service.delete(Uuid.parse(id))
|
service.delete(Uuid.parse(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PatchMapping("/bewerbe/{id}/zeitplan")
|
||||||
|
suspend fun updateZeitplan(@PathVariable id: String, @RequestBody body: UpdateZeitplanRequest): BewerbResponse {
|
||||||
|
val b = service.updateZeitplan(Uuid.parse(id), body)
|
||||||
|
return domainToDto(b)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -10,6 +10,7 @@ object AbteilungTable : Table("abteilungen") {
|
|||||||
val nr = integer("nr")
|
val nr = integer("nr")
|
||||||
val bezeichnung = text("bezeichnung")
|
val bezeichnung = text("bezeichnung")
|
||||||
val typ = varchar("typ", 32)
|
val typ = varchar("typ", 32)
|
||||||
|
val besichtigungsTyp = varchar("besichtigungs_typ", 20).nullable()
|
||||||
val createdAt = timestamp("created_at")
|
val createdAt = timestamp("created_at")
|
||||||
val updatedAt = timestamp("updated_at")
|
val updatedAt = timestamp("updated_at")
|
||||||
|
|
||||||
|
|||||||
+5
@@ -34,6 +34,11 @@ object BewerbTable : Table("bewerbe") {
|
|||||||
val besichtigungMinuten = integer("besichtigung_minuten").nullable()
|
val besichtigungMinuten = integer("besichtigung_minuten").nullable()
|
||||||
val stechenGeplant = bool("stechen_geplant").default(false)
|
val stechenGeplant = bool("stechen_geplant").default(false)
|
||||||
|
|
||||||
|
// Pausen
|
||||||
|
val pausenStarterIntervall = integer("pausen_starter_intervall").nullable()
|
||||||
|
val pausenDauerMinuten = integer("pausen_dauer_minuten").nullable()
|
||||||
|
val pausenBezeichnung = varchar("pausen_bezeichnung", 100).nullable()
|
||||||
|
|
||||||
// Finanzen
|
// Finanzen
|
||||||
val startgeldCent = long("startgeld_cent").nullable()
|
val startgeldCent = long("startgeld_cent").nullable()
|
||||||
val nenngeldCent = long("nenngeld_cent").nullable()
|
val nenngeldCent = long("nenngeld_cent").nullable()
|
||||||
|
|||||||
+112
@@ -0,0 +1,112 @@
|
|||||||
|
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||||
|
|
||||||
|
package at.mocode.entries.service.bewerbe
|
||||||
|
|
||||||
|
import at.mocode.entries.service.persistence.*
|
||||||
|
import at.mocode.entries.service.tenant.Tenant
|
||||||
|
import at.mocode.entries.service.tenant.TenantContextHolder
|
||||||
|
import at.mocode.entries.service.tenant.tenantTransaction
|
||||||
|
import kotlin.time.Clock
|
||||||
|
import kotlinx.datetime.LocalTime
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.SchemaUtils
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.deleteAll
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.insert
|
||||||
|
import org.junit.jupiter.api.AfterEach
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest
|
||||||
|
import org.springframework.test.context.ActiveProfiles
|
||||||
|
import kotlin.uuid.Uuid
|
||||||
|
import kotlin.uuid.toJavaUuid
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
@ActiveProfiles("test")
|
||||||
|
class BewerbeZeitplanIntegrationTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private lateinit var bewerbService: BewerbService
|
||||||
|
|
||||||
|
private val turnierId = Uuid.random()
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setup() {
|
||||||
|
TenantContextHolder.set(Tenant(turnierId.toString(), "PUBLIC", "jdbc:h2:mem:entries-test"))
|
||||||
|
kotlinx.coroutines.runBlocking {
|
||||||
|
tenantTransaction {
|
||||||
|
SchemaUtils.create(
|
||||||
|
TurnierTable,
|
||||||
|
BewerbTable,
|
||||||
|
AbteilungTable,
|
||||||
|
BewerbRichterEinsatzTable
|
||||||
|
)
|
||||||
|
BewerbRichterEinsatzTable.deleteAll()
|
||||||
|
BewerbTable.deleteAll()
|
||||||
|
AbteilungTable.deleteAll()
|
||||||
|
TurnierTable.deleteAll()
|
||||||
|
|
||||||
|
TurnierTable.insert {
|
||||||
|
it[id] = turnierId.toJavaUuid()
|
||||||
|
it[veranstaltungId] = Uuid.random().toJavaUuid()
|
||||||
|
it[oepsTurniernummer] = "26001"
|
||||||
|
it[turnierNummer] = "1"
|
||||||
|
it[einschraenkungen] = "{}"
|
||||||
|
it[status] = "DRAFT"
|
||||||
|
it[createdAt] = Clock.System.now()
|
||||||
|
it[updatedAt] = Clock.System.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
fun teardown() {
|
||||||
|
TenantContextHolder.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `bewerb mit pausen erstellen und abrufen`() = kotlinx.coroutines.runBlocking {
|
||||||
|
// GIVEN
|
||||||
|
val request = CreateBewerbRequest(
|
||||||
|
klasse = "A",
|
||||||
|
bezeichnung = "Springpferdeprüfung",
|
||||||
|
pausenStarterIntervall = 20,
|
||||||
|
pausenDauerMinuten = 15,
|
||||||
|
pausenBezeichnung = "Platzpflege",
|
||||||
|
besichtigungMinuten = 20
|
||||||
|
)
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
val created = bewerbService.create(turnierId, request)
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
val fetched = bewerbService.get(created.id)
|
||||||
|
assertEquals(20, fetched.pausenStarterIntervall)
|
||||||
|
assertEquals(15, fetched.pausenDauerMinuten)
|
||||||
|
assertEquals("Platzpflege", fetched.pausenBezeichnung)
|
||||||
|
assertEquals(20, fetched.besichtigungMinuten)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `zeitplan update via patch`() = kotlinx.coroutines.runBlocking {
|
||||||
|
// GIVEN
|
||||||
|
val bewerb = bewerbService.create(turnierId, CreateBewerbRequest(
|
||||||
|
klasse = "L",
|
||||||
|
bezeichnung = "Standardspringprüfung"
|
||||||
|
))
|
||||||
|
val patchRequest = UpdateZeitplanRequest(
|
||||||
|
geplantesDatum = null,
|
||||||
|
beginnZeit = LocalTime(14, 30),
|
||||||
|
austragungsplatzId = null
|
||||||
|
)
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
val updated = bewerbService.updateZeitplan(bewerb.id, patchRequest)
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assertEquals(LocalTime(14, 30), updated.beginnZeit)
|
||||||
|
val fetched = bewerbService.get(bewerb.id)
|
||||||
|
assertEquals(LocalTime(14, 30), fetched.beginnZeit)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -536,3 +536,15 @@ enum class ReglementE {
|
|||||||
/** Internationales Reglement (FEI) */
|
/** Internationales Reglement (FEI) */
|
||||||
FEI
|
FEI
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Typ der Parcoursbesichtigung.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
enum class BesichtigungsTypE {
|
||||||
|
/** Klassische Besichtigung zu Fuß. */
|
||||||
|
ZU_FUSS,
|
||||||
|
|
||||||
|
/** Besichtigung zu Pferd (z.B. Springpferdeprüfungen). */
|
||||||
|
ZU_PFERD
|
||||||
|
}
|
||||||
|
|||||||
@@ -231,6 +231,7 @@ und über definierte Schnittstellen kommunizieren.
|
|||||||
* [x] **Billing-Service:** Initialisierung, Teilnehmer-Konten & Buchungs-Logik (v1). ✓
|
* [x] **Billing-Service:** Initialisierung, Teilnehmer-Konten & Buchungs-Logik (v1). ✓
|
||||||
* [x] **Entries-Integration:** Automatische Buchung von Nenngeldern bei Nennungs-Abgabe. ✓
|
* [x] **Entries-Integration:** Automatische Buchung von Nenngeldern bei Nennungs-Abgabe. ✓
|
||||||
* [x] **ZNS-Importer:** Hardening & Integrationstests für Funktionärs-Updates. ✓
|
* [x] **ZNS-Importer:** Hardening & Integrationstests für Funktionärs-Updates. ✓
|
||||||
|
* [x] **Konzept:** Fachliches Konzept für Zeitplan-Optimierung (Drag & Drop) erstellt. ✓
|
||||||
* [ ] **Zeitplan:** Dynamische Verschiebung von Bewerben (Drag & Drop im Kalender).
|
* [ ] **Zeitplan:** Dynamische Verschiebung von Bewerben (Drag & Drop im Kalender).
|
||||||
* [ ] **Protokoll:** Implementierung eines Event-Logs für manuelle Eingriffe in Startlisten.
|
* [ ] **Protokoll:** Implementierung eines Event-Logs für manuelle Eingriffe in Startlisten.
|
||||||
* [ ] **Export:** Startlisten-Export für ZNS (XML-B-Satz).
|
* [ ] **Export:** Startlisten-Export für ZNS (XML-B-Satz).
|
||||||
@@ -287,3 +288,5 @@ und über definierte Schnittstellen kommunizieren.
|
|||||||
| Masterdata Roadmap | `backend/services/masterdata/docs/ROADMAP.md` |
|
| Masterdata Roadmap | `backend/services/masterdata/docs/ROADMAP.md` |
|
||||||
| Masterdata Changelog | `backend/services/masterdata/docs/CHANGELOG.md` |
|
| Masterdata Changelog | `backend/services/masterdata/docs/CHANGELOG.md` |
|
||||||
| Masterdata Operations | `backend/services/masterdata/docs/runbooks/masterdata-ops.md` |
|
| Masterdata Operations | `backend/services/masterdata/docs/runbooks/masterdata-ops.md` |
|
||||||
|
| Zeitplan-Optimierung | `docs/01_Architecture/konzept-zeitplan-optimierung-de.md` |
|
||||||
|
| Parcoursbesichtigung-Rulebook | `docs/01_Architecture/rulebook-check-parcoursbesichtigung-de.md` |
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# Konzept: Zeitplan-Optimierung (Drag & Drop Logik)
|
||||||
|
|
||||||
|
> **Status:** ENTWURF | **Datum:** 11. April 2026
|
||||||
|
> **Autor:** 🏗️ Lead Architect
|
||||||
|
> **Kontext:** Phase 9 — Zeitplan & Protokollierung
|
||||||
|
|
||||||
|
## 1. Vision & Zielsetzung
|
||||||
|
Die Zeitplan-Optimierung ist das zentrale Werkzeug für die Meldestelle, um den Turnierablauf dynamisch an die Gegebenheiten vor Ort anzupassen. Ziel ist eine intuitive, visuelle Oberfläche (Kalender-Ansicht), in der Bewerbe und Abteilungen per Drag & Drop verschoben werden können, wobei das System automatisch Auswirkungen auf Folge-Bewerbe berechnet und vor Konflikten warnt.
|
||||||
|
|
||||||
|
## 2. Fachliche Anforderungen (Use Cases)
|
||||||
|
|
||||||
|
### UC-1: Verschieben eines Bewerbs/einer Abteilung
|
||||||
|
- Ein Benutzer zieht einen Bewerb oder eine Abteilung auf eine neue Startzeit oder einen anderen Austragungsplatz.
|
||||||
|
- **System-Reaktion:**
|
||||||
|
- Neuberechnung der Endzeit basierend auf `reitdauerMinuten * starterAnzahl + besichtigungMinuten + umbauMinuten`.
|
||||||
|
- Prüfung auf Überschneidungen am selben Austragungsplatz.
|
||||||
|
- Warnung bei Konflikten (z.B. Richter-Doppelbelegung).
|
||||||
|
|
||||||
|
### UC-2: Dynamische Zeitplan-Anpassung („Anschließend“)
|
||||||
|
- Bewerbe können als `ANSCHLIESSEND` markiert werden (`beginnZeitTyp`).
|
||||||
|
- **System-Reaktion:** Wenn der vorangehende Bewerb verschoben wird oder länger dauert, verschiebt sich der anschließende Bewerb automatisch mit.
|
||||||
|
|
||||||
|
### UC-3: Drag & Drop in der Startliste
|
||||||
|
- Innerhalb einer Startliste können Teilnehmer per Drag & Drop umsortiert werden.
|
||||||
|
- **System-Reaktion:** Automatische Aktualisierung der Kopfnummern (Startnummern) und Neuberechnung der individuellen Startzeiten.
|
||||||
|
|
||||||
|
### UC-4: Protokollierung (Audit Log)
|
||||||
|
- Jede manuelle Änderung am Zeitplan oder der Startlisten-Reihenfolge muss protokolliert werden.
|
||||||
|
- **Grund:** Nachvollziehbarkeit bei Einsprüchen und Synchronisation zwischen verschiedenen Arbeitsplätzen (Meldestelle ↔ Richterturm).
|
||||||
|
|
||||||
|
## 3. Datenmodell-Erweiterungen & Logik
|
||||||
|
|
||||||
|
### 3.1 Erweiterung `Bewerb` & `Abteilung`
|
||||||
|
Das bestehende Modell in `entries-domain` deckt bereits viele Felder ab. Für die Optimierung präzisieren wir:
|
||||||
|
- **`Umbauzeit`:** Zeit zwischen zwei Abteilungen oder Bewerben.
|
||||||
|
- **`Pausen`:** Geplante Unterbrechungen (z.B. Mittagspause, Platzpflege) werden als spezielle „Blocker-Events“ im Zeitplan geführt.
|
||||||
|
- **`BesichtigungsBlock` (NEU):** Eigenständiges Objekt für Parcoursbesichtigungen.
|
||||||
|
- Kann mit mehreren Bewerben/Abteilungen verknüpft werden (Cross-Competition Inspection).
|
||||||
|
- Unterstützt Typen: `ZU_FUSS` (Standard) und `ZU_PFERD` (Spezialfall für Springpferde bis 110cm gemäß ÖTO).
|
||||||
|
- Validierung: Mindestens 5 Min. Puffer vor dem ersten Start (ÖTO §43).
|
||||||
|
|
||||||
|
### 3.2 Zeitberechnungs-Algorithmus (Präzisierung)
|
||||||
|
Die Logik in `StartlistenService.berechneStartzeiten` wird erweitert:
|
||||||
|
1. **Basis:** `Startzeit` der Abteilung.
|
||||||
|
2. **Vorlauf:** `besichtigungMinuten`.
|
||||||
|
3. **Starter-Loop:**
|
||||||
|
- `individuelleStartzeit = aktuelleZeit`.
|
||||||
|
- `aktuelleZeit += bewerb.reitdauerMinuten`.
|
||||||
|
- *Neu:* Berücksichtigung von festen Pausen nach X Startern (z.B. 10 Min. Pause alle 20 Starter).
|
||||||
|
4. **Nachlauf:** `umbauMinuten` am Ende der Abteilung/des Bewerbs.
|
||||||
|
|
||||||
|
### 3.3 Drag & Drop Logik (Frontend & Backend)
|
||||||
|
- **Frontend (Compose Desktop):**
|
||||||
|
- Implementierung eines `DraggableBewerbItem`.
|
||||||
|
- Visuelle Darstellung von Konflikten (Rote Markierung bei Überlappung).
|
||||||
|
- **Backend (API):**
|
||||||
|
- Neuer Endpunkt `PATCH /bewerbe/{id}/zeitplan` für schnelle Updates.
|
||||||
|
- Validierung der neuen Zeiten gegen den `austragungsplatzId` und `richterEinsaetze`.
|
||||||
|
|
||||||
|
## 4. Konflikt-Management
|
||||||
|
Das System arbeitet nach dem Prinzip **„Warnen statt Blockieren“** (ADR-0016):
|
||||||
|
- **Harte Fehler:** Nur bei technischer Unmöglichkeit (z.B. Datum in der Vergangenheit bei Live-Betrieb).
|
||||||
|
- **Warnungen:**
|
||||||
|
- Überlappung auf dem Platz.
|
||||||
|
- Richter hat gleichzeitig Einsatz in anderem Bewerb.
|
||||||
|
- Zeitunterschreitung für Reiter (Reiter startet in zwei kurz aufeinanderfolgenden Bewerben).
|
||||||
|
|
||||||
|
## 5. Synchronisation (Offline-First)
|
||||||
|
Änderungen am Zeitplan erzeugen `SyncEvents` (gemäß ADR-0022).
|
||||||
|
- **Lamport-Uhren:** Stellen sicher, dass bei gleichzeitigen Änderungen an zwei Laptops die zeitlich spätere Änderung (oder die mit höherer Priorität) gewinnt.
|
||||||
|
- **Echtzeit-Update:** Über WebSockets werden Zeitplan-Änderungen sofort an alle verbundenen Clients (z.B. Anzeige-Monitor, Richter-Tablet) gepusht.
|
||||||
|
|
||||||
|
## 6. Nächste Schritte
|
||||||
|
1. **Backend:** ✅ Implementiert (BewerbService, API, Zeitberechnungs-Pausen).
|
||||||
|
2. **Frontend:** Prototyping der Kalender-Ansicht mit Compose Desktop.
|
||||||
|
3. **QA:** Test-Szenarien für komplexe Verschiebungen (Kettenreaktionen bei „Anschließend“).
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Regelwerks-Check: Parcoursbesichtigung & Bewerbszusammenlegung
|
||||||
|
|
||||||
|
> **Status:** FINAL | **Datum:** 11. April 2026
|
||||||
|
> **Autor:** 📜 ÖTO/FEI Rulebook Expert
|
||||||
|
> **Kontext:** Zeitplan-Optimierung & Praxis-Szenarien
|
||||||
|
|
||||||
|
## 1. Parcoursbesichtigung zu Pferd
|
||||||
|
Die Aussage des Users wurde geprüft und bestätigt.
|
||||||
|
|
||||||
|
### 1.1 Regelwerks-Referenz (ÖTO 2026)
|
||||||
|
Gemäß **ÖTO Teil B, § 43 (Abweichungen)** gilt:
|
||||||
|
- **Normalfall:** Parcoursbesichtigung erfolgt zu Fuß.
|
||||||
|
- **Springpferde- & Jungpferdeprüfungen (bis 110 cm):** Eine Besichtigung **zu Pferd im Schritt** kann von der Richtergruppe erlaubt werden.
|
||||||
|
- **Zweck:** Junge, unerfahrene Pferde sollen stressfrei an optische Reize (Fangständer, Farben, Unterbauten) herangeführt werden.
|
||||||
|
|
||||||
|
### 1.2 Organisatorische Implikationen
|
||||||
|
- **Dauer:** Die Besichtigung zu Pferd benötigt oft etwas mehr Zeit für die Koordination am Einlass (15-20 Min. sind praxisnah).
|
||||||
|
- **Sicherheit:** Da Pferde im Parcours sind, während andere Reiter eventuell noch draußen warten, muss der Zeitplan einen Puffer von mindestens **5 Minuten** zwischen Ende Besichtigung und erstem Start vorsehen (ÖTO Vorschrift).
|
||||||
|
|
||||||
|
## 2. Zusammengelegte Besichtigungen (Cross-Competition Inspection)
|
||||||
|
In der Praxis üblich, wenn der Parcours für aufeinanderfolgende Bewerbe identisch bleibt.
|
||||||
|
|
||||||
|
### 2.1 Szenario
|
||||||
|
- **Bewerb A:** Springpferdeprüfung 105cm (Besichtigung zu Pferd erlaubt).
|
||||||
|
- **Bewerb B:** Standardspringprüfung 105cm (Besichtigung zu Fuß).
|
||||||
|
- **Lösung:** Eine gemeinsame Besichtigungszeit vor Bewerb A.
|
||||||
|
|
||||||
|
### 2.2 Regelwerks-Konformität
|
||||||
|
- Es gibt kein Verbot für gemeinsame Besichtigungen, solange die Teilnehmer beider Bewerbe die Möglichkeit haben, den Parcours regelkonform zu besichtigen.
|
||||||
|
- **Wichtig:** Wenn Bewerb B erst 2 Stunden später startet, muss für Teilnehmer von Bewerb B theoretisch eine zweite Besichtigungsmöglichkeit (oder ein "Refresh") angeboten werden, falls der Parcours zwischenzeitlich verändert wurde. Wenn er identisch bleibt, reicht die einmalige Bekanntgabe.
|
||||||
|
|
||||||
|
## 3. Anforderungen für die Software (Zeitplan-Modul)
|
||||||
|
|
||||||
|
### 3.1 Datenmodell
|
||||||
|
- `Abteilung` oder `Bewerb` benötigt ein Flag `besichtigungZuPferdErlaubt` (Boolean).
|
||||||
|
- `BesichtigungsBlock` als eigenständiges Entitätsobjekt im Zeitplan, das mit **mehreren** Bewerben/Abteilungen verknüpft werden kann.
|
||||||
|
|
||||||
|
### 3.2 Validierung & Warnungen
|
||||||
|
- **Warnung:** Wenn ein Besichtigungsblock mit "zu Pferd" markiert ist, aber ein verbundener Bewerb (z.B. L-Springen) dies laut ÖTO normalerweise nicht vorsieht (Diskretionsspielraum der Richter).
|
||||||
|
- **Puffer-Check:** Automatische Prüfung, ob zwischen `Ende Besichtigung` und `Start Erster Reiter` mindestens 5 Minuten liegen.
|
||||||
|
|
||||||
|
## 4. Fazit für Architect & Entwickler
|
||||||
|
Die "Besichtigung zu Pferd" ist ein valider Spezialfall für Springpferdeprüfungen. Die Software muss erlauben, Besichtigungszeiten flexibel vor einen oder mehrere Bewerbe zu lagern und den Typ (Fuß/Pferd) zu kennzeichnen.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Geprüft durch den 📜 ÖTO/FEI Rulebook Expert am 11.04.2026*
|
||||||
@@ -36,20 +36,26 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🟠 Sprint C — Priorität 2 (nächste Woche)
|
## 🟠 Sprint C — In Arbeit
|
||||||
|
|
||||||
- [ ] **C-1** | Synchronisations-Protokoll-Konzeption
|
- [x] **C-1** | Zeitplan-Optimierung Konzept
|
||||||
- [x] Offline-First-Konzept für Desktop ↔ Backend ausarbeiten
|
- [x] Fachliche Anforderungen (Use Cases) definiert
|
||||||
- [x] Conflict-Resolution-Strategie definieren (gleichzeitige Änderungen)
|
- [x] Zeitberechnungs-Algorithmus spezifiziert
|
||||||
- [x] Konzept-Dokument in `docs/01_Architecture/` ablegen → `docs/01_Architecture/konzept-offline-first-desktop-backend-de.md`
|
- [x] Drag & Drop Logik für Kalender-Ansicht entworfen
|
||||||
- Verweis/Bezug: Baut auf ADR-0021 (Tenant) und ADR-0022 (LAN-Sync Lamport) auf; einheitliches `SyncEvent`-Modell Desktop↔Backend.
|
- [x] Konzept-Dokument in `docs/01_Architecture/` abgelegt → `docs/01_Architecture/konzept-zeitplan-optimierung-de.md`
|
||||||
|
|
||||||
- [ ] **C-2** | MASTER_ROADMAP aktualisieren
|
- [ ] **C-2** | MASTER_ROADMAP aktualisieren
|
||||||
- [x] Desktop-App-Fokus eintragen
|
- [x] Phase 9 Fortschritt reflektieren
|
||||||
- [x] Tenant-Isolation-Meilensteine (Sprint A Ergebnisse) als erledigt markieren
|
- [x] Link zum Zeitplan-Konzept ergänzt
|
||||||
- [x] Offline-Sync-Meilensteine eintragen
|
- [ ] Weitere Sprints (D, E) grob skizzieren
|
||||||
- [x] Phase 8 Fortschritt reflektieren
|
|
||||||
- Update: Siehe `docs/01_Architecture/MASTER_ROADMAP.md` (Stand 2026-04-03) — Produktfokus ergänzt, ADR‑0021/0022 in ADR‑Tabelle eingetragen, Phase‑8‑Status („Konzept/ADR erledigt“) markiert, To‑do „Offline‑First Desktop↔Backend“ verlinkt.
|
---
|
||||||
|
|
||||||
|
## 🔵 Sprint D — Geplant
|
||||||
|
|
||||||
|
- [ ] **D-1** | USB-Stick Fallback (Sync)
|
||||||
|
- [ ] Technische Machbarkeit (File-Storage vs. SQLite-Export) prüfen
|
||||||
|
- [ ] ADR für Offline-Transfer erstellen
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,10 @@
|
|||||||
|
|
||||||
| Agent | Sprint A | Sprint B | Sprint C | Nächste Aktion |
|
| Agent | Sprint A | Sprint B | Sprint C | Nächste Aktion |
|
||||||
|---------------|------------------|------------------------------------------|-------------------|-------------------------------------------------------|
|
|---------------|------------------|------------------------------------------|-------------------|-------------------------------------------------------|
|
||||||
| 🏗️ Architect | ✅ Abgeschlossen | ✅ Abgeschlossen | ⬜ Nicht gestartet | Zeitplan-Optimierung Konzept |
|
| 🏗️ Architect | ✅ Abgeschlossen | ✅ Abgeschlossen | 🟡 In Arbeit | Zeitplan-Optimierung (ADR/Konzept) |
|
||||||
| 👷 Backend | ✅ Abgeschlossen | ✅ B-1/B-2 fertig | ⬜ Nicht gestartet | C-1 Nennungs-Service Erweiterung |
|
| 👷 Backend | ✅ Abgeschlossen | ✅ B-1/B-2 fertig | ⬜ Nicht gestartet | C-1 Nennungs-Service Erweiterung |
|
||||||
| 🎨 Frontend | ✅ Abgeschlossen | 🟡 B-2/B-3 teilweise / B-4 offen | ⬜ Nicht gestartet | B-4 Kassa-Screen & StoreV2-Ablösung |
|
| 🎨 Frontend | ✅ Abgeschlossen | 🟡 B-2/B-3 teilweise / B-4 offen | ⬜ Nicht gestartet | B-4 Kassa-Screen & StoreV2-Ablösung |
|
||||||
| 📜 Rulebook | ✅ Abgeschlossen | ✅ B-2 abgeschlossen | ⬜ Nicht gestartet | C-1 AltersklasseRechner |
|
| 📜 Rulebook | ✅ Abgeschlossen | ✅ B-2 abgeschlossen | 🟡 In Arbeit | Parcoursbesichtigung-Rulebook Check |
|
||||||
| 🐧 DevOps | ✅ Abgeschlossen | ✅ Abgeschlossen | ✅ C-1/C-2 fertig | C-3 Produktions-Deployment |
|
| 🐧 DevOps | ✅ Abgeschlossen | ✅ Abgeschlossen | ✅ C-1/C-2 fertig | C-3 Produktions-Deployment |
|
||||||
| 🧐 QA | ✅ Abgeschlossen | ✅ B-1/B-3 fertig | ⬜ Nicht gestartet | B-2 Onboarding-Tests |
|
| 🧐 QA | ✅ Abgeschlossen | ✅ B-1/B-3 fertig | ⬜ Nicht gestartet | B-2 Onboarding-Tests |
|
||||||
| 🖌️ UI/UX | ✅ Abgeschlossen | 🔴 B-1/B-4 offen | ⬜ Nicht gestartet | B-4 Wireframes für Kassa-Screen |
|
| 🖌️ UI/UX | ✅ Abgeschlossen | 🔴 B-1/B-4 offen | ⬜ Nicht gestartet | B-4 Wireframes für Kassa-Screen |
|
||||||
@@ -28,7 +28,6 @@ Diese Aufgaben blockieren andere Agenten und müssen zuerst erledigt werden:
|
|||||||
| Priorität | Agent | Aufgabe | Blockiert |
|
| Priorität | Agent | Aufgabe | Blockiert |
|
||||||
|-----------|---------------|-----------------------------------------------|---------------------------------------------------|
|
|-----------|---------------|-----------------------------------------------|---------------------------------------------------|
|
||||||
| 🔴 P1 | 🖌️ UI/UX | B-4: Wireframes für Kassa-Screen | 🎨 Frontend: B-4 Kassa-Screen |
|
| 🔴 P1 | 🖌️ UI/UX | B-4: Wireframes für Kassa-Screen | 🎨 Frontend: B-4 Kassa-Screen |
|
||||||
| 🔴 P1 | 🏗️ Architect | C-1: Zeitplan-Optimierung Konzept | 👷 Backend: C-2; 🎨 Frontend: C-2 |
|
|
||||||
| 🔴 P1 | 🎨 Frontend | B-2: StoreV2-Ablösung | 🧐 QA: B-4 ViewModel-Tests |
|
| 🔴 P1 | 🎨 Frontend | B-2: StoreV2-Ablösung | 🧐 QA: B-4 ViewModel-Tests |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -38,7 +37,7 @@ Diese Aufgaben blockieren andere Agenten und müssen zuerst erledigt werden:
|
|||||||
### 🏗️ Architect
|
### 🏗️ Architect
|
||||||
|
|
||||||
1. ✅ **B-1** ADR-0022 LAN-Sync-Protokoll (Event-Sourcing vs. CRDT vs. Timestamp)
|
1. ✅ **B-1** ADR-0022 LAN-Sync-Protokoll (Event-Sourcing vs. CRDT vs. Timestamp)
|
||||||
2. 🔴 **C-1** Konzept für Zeitplan-Optimierung (Drag & Drop Logik)
|
2. ✅ **C-1** Konzept für Zeitplan-Optimierung (Drag & Drop Logik)
|
||||||
|
|
||||||
### 👷 Backend Developer
|
### 👷 Backend Developer
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user