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:
2026-04-11 12:21:37 +02:00
parent 97ed8ad20a
commit 0aa1a1b9b7
19 changed files with 423 additions and 20 deletions
@@ -3,6 +3,7 @@
package at.mocode.entries.domain.model
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.UuidSerializer
import kotlinx.serialization.Serializable
@@ -55,6 +56,9 @@ data class Abteilung(
// Zeitplanung
var startzeit: String? = null,
/** Besichtigungstyp für diese Abteilung (optional, wenn abweichend von Standard). */
var besichtigungsTyp: BesichtigungsTypE? = null,
// Verwaltung
var bemerkungen: String? = null,
@@ -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
)
@@ -97,6 +97,10 @@ data class Bewerb(
var reitdauerMinuten: Int? = null,
var umbauMinuten: Int? = null,
var besichtigungMinuten: Int? = null,
/** Konfiguration für Pausen während der Prüfung. */
var pausenKonfiguration: PausenKonfiguration? = null,
var stechenGeplant: Boolean = false,
// Finanzen
@@ -297,3 +301,18 @@ data class Bewerb(
*/
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
)
@@ -98,16 +98,24 @@ class StartlistenService {
// Besichtigung vor dem ersten Starter
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 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