feat: integrate new desktop shell and extend backend & ADRs
- Added `meldestelle-desktop` module using JVM/Compose Desktop, registered in `settings.gradle.kts`. - Integrated new screens and desktop navigation into core: `Veranstaltungen`, `TurnierDetail`, etc. - Expanded backend with `ExposedFunktionaerRepository` in `officials-infrastructure`. - Completed ADRs for bounded context mapping (`ADR-0014`) and context map (`ADR-0015`). - Updated and extended project documentation with session logs and architecture decisions. Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
+154
@@ -0,0 +1,154 @@
|
||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.events.domain.model
|
||||
|
||||
import at.mocode.core.domain.model.AusschreibungsStatusE
|
||||
import at.mocode.core.domain.model.SparteE
|
||||
import at.mocode.core.domain.serialization.KotlinInstantSerializer
|
||||
import at.mocode.core.domain.serialization.UuidSerializer
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Aggregate Root für die offizielle Ausschreibung einer Veranstaltung gemäß ÖTO.
|
||||
*
|
||||
* Die Ausschreibung ist das offizielle Dokument, das alle relevanten Informationen
|
||||
* für Nennende enthält (Bewerbe, Gebühren, Nennschluss, Richter, etc.).
|
||||
* Sie muss vom Verband genehmigt werden, bevor Nennungen möglich sind.
|
||||
*
|
||||
* @property ausschreibungsId Eindeutige interne ID (UUID).
|
||||
* @property veranstaltungId Referenz auf die zugehörige Veranstaltung.
|
||||
* @property titel Offizieller Titel der Ausschreibung.
|
||||
* @property sparten Enthaltene Sparten.
|
||||
* @property nennschluss Nennschluss-Datum (Pflichtfeld gemäß ÖTO).
|
||||
* @property nachnennung Ob Nachnennungen möglich sind und bis wann.
|
||||
* @property nachnennungBis Datum bis zu dem Nachnennungen möglich sind (optional).
|
||||
* @property nenngebuehrBasisCent Basis-Nenngebühr in Cent (zur Vermeidung von Floating-Point-Fehlern).
|
||||
* @property nachnenngebuehrCent Nachnenngebühr in Cent (Aufschlag auf Basis-Nenngebühr).
|
||||
* @property sportfoerderbeitragCent Sportförderbeitrag in Cent (ÖTO-Pflichtabgabe).
|
||||
* @property tierwohleuroAktiv Ob der Tierwohl-Euro erhoben wird.
|
||||
* @property veranstaltungsortBeschreibung Detaillierte Beschreibung des Veranstaltungsorts.
|
||||
* @property anfahrtsBeschreibung Anfahrtsbeschreibung (optional).
|
||||
* @property stallplatzeVerfuegbar Anzahl verfügbarer Stallplätze (optional).
|
||||
* @property stallplatzgebuehrCent Stallplatzgebühr pro Nacht in Cent (optional).
|
||||
* @property kontaktEmail Kontakt-E-Mail für Rückfragen.
|
||||
* @property kontaktTelefon Kontakt-Telefonnummer (optional).
|
||||
* @property zusatzinformationen Freitext für weitere Informationen.
|
||||
* @property status Aktueller Status im Genehmigungsworkflow.
|
||||
* @property eingereichtAm Datum der Einreichung beim Verband (optional).
|
||||
* @property genehmigungsNummer Offizielle Genehmigungsnummer (nach Genehmigung).
|
||||
* @property createdAt Erstellungszeitpunkt.
|
||||
* @property updatedAt Letzter Änderungszeitpunkt.
|
||||
*/
|
||||
@Serializable
|
||||
data class DomAusschreibung(
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
val ausschreibungsId: Uuid = Uuid.random(),
|
||||
|
||||
// Zugehörigkeit
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
var veranstaltungId: Uuid,
|
||||
|
||||
// Basis-Informationen
|
||||
var titel: String,
|
||||
var sparten: List<SparteE> = emptyList(),
|
||||
|
||||
// Termine
|
||||
var nennschluss: LocalDate,
|
||||
var nachnennung: Boolean = false,
|
||||
var nachnennungBis: LocalDate? = null,
|
||||
|
||||
// Gebühren (in Cent, Integer zur Vermeidung von Floating-Point-Fehlern)
|
||||
var nenngebuehrBasisCent: Int = 0,
|
||||
var nachnenngebuehrCent: Int = 0,
|
||||
var sportfoerderbeitragCent: Int = 0,
|
||||
var tierwohleuroAktiv: Boolean = true,
|
||||
|
||||
// Veranstaltungsort
|
||||
var veranstaltungsortBeschreibung: String? = null,
|
||||
var anfahrtsBeschreibung: String? = null,
|
||||
|
||||
// Stallplätze
|
||||
var stallplatzeVerfuegbar: Int? = null,
|
||||
var stallplatzgebuehrCent: Int? = null,
|
||||
|
||||
// Kontakt
|
||||
var kontaktEmail: String? = null,
|
||||
var kontaktTelefon: String? = null,
|
||||
|
||||
// Freitext
|
||||
var zusatzinformationen: String? = null,
|
||||
|
||||
// Workflow-Status
|
||||
var status: AusschreibungsStatusE = AusschreibungsStatusE.ENTWURF,
|
||||
var eingereichtAm: LocalDate? = null,
|
||||
var genehmigungsNummer: String? = null,
|
||||
|
||||
// Audit
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
val createdAt: Instant = Clock.System.now(),
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
var updatedAt: Instant = Clock.System.now()
|
||||
) {
|
||||
/**
|
||||
* Validiert die Pflichtfelder für die Einreichung beim Verband.
|
||||
* Gibt Warnungen zurück – keine harten Fehler (Warn-Logik statt Exception, ADR-0007).
|
||||
*/
|
||||
fun validateFuerEinreichung(): List<String> {
|
||||
val warnings = mutableListOf<String>()
|
||||
if (titel.isBlank()) {
|
||||
warnings.add("Ausschreibungstitel ist erforderlich.")
|
||||
}
|
||||
if (sparten.isEmpty()) {
|
||||
warnings.add("Mindestens eine Sparte muss angegeben werden.")
|
||||
}
|
||||
if (kontaktEmail.isNullOrBlank()) {
|
||||
warnings.add("Kontakt-E-Mail ist für die Einreichung erforderlich.")
|
||||
}
|
||||
if (nenngebuehrBasisCent < 0) {
|
||||
warnings.add("Nenngebühr darf nicht negativ sein.")
|
||||
}
|
||||
if (nachnenngebuehrCent < 0) {
|
||||
warnings.add("Nachnenngebühr darf nicht negativ sein.")
|
||||
}
|
||||
if (nachnennung && nachnennungBis == null) {
|
||||
warnings.add("Nachnennungs-Datum muss angegeben werden, wenn Nachnennungen erlaubt sind.")
|
||||
}
|
||||
nachnennungBis?.let { nb ->
|
||||
if (nb <= nennschluss) {
|
||||
warnings.add("Nachnennungs-Datum muss nach dem Nennschluss liegen.")
|
||||
}
|
||||
}
|
||||
stallplatzgebuehrCent?.let { sg ->
|
||||
if (sg < 0) {
|
||||
warnings.add("Stallplatzgebühr darf nicht negativ sein.")
|
||||
}
|
||||
}
|
||||
return warnings
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Basis-Nenngebühr als formatierten Euro-String zurück (z.B. "12,50 €").
|
||||
*/
|
||||
fun getNenngebuehrAlsEuroString(): String {
|
||||
val euro = nenngebuehrBasisCent / 100
|
||||
val cent = nenngebuehrBasisCent % 100
|
||||
return "$euro,${cent.toString().padStart(2, '0')} €"
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Gesamtgebühr (Nenngebühr + Sportförderbeitrag + ggf. Tierwohl-Euro) in Cent zurück.
|
||||
*/
|
||||
fun getGesamtgebuehrCent(): Int {
|
||||
val tierwohl = if (tierwohleuroAktiv) 100 else 0
|
||||
return nenngebuehrBasisCent + sportfoerderbeitragCent + tierwohl
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine Kopie mit aktualisiertem Zeitstempel.
|
||||
*/
|
||||
fun withUpdatedTimestamp(): DomAusschreibung = this.copy(updatedAt = Clock.System.now())
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.events.domain.model
|
||||
|
||||
import at.mocode.core.domain.model.SparteE
|
||||
import at.mocode.core.domain.model.TurnierkategorieE
|
||||
import at.mocode.core.domain.model.TurnierStatusE
|
||||
import at.mocode.core.domain.serialization.KotlinInstantSerializer
|
||||
import at.mocode.core.domain.serialization.UuidSerializer
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Aggregate Root für ein einzelnes Turnier innerhalb einer Veranstaltung gemäß ÖTO § 2 Abs. 1.
|
||||
*
|
||||
* Ein Turnier ist die konkrete Durchführungseinheit (z.B. "Springturnier CSN-C Samstag").
|
||||
* Eine Veranstaltung kann mehrere Turniere enthalten (z.B. Dressur-Turnier + Spring-Turnier).
|
||||
* Jedes Turnier hat eine eigene Kategorie (CSN-C, CDN, etc.) und Sparte.
|
||||
*
|
||||
* @property turnierId Eindeutige interne ID (UUID).
|
||||
* @property veranstaltungId Referenz auf die übergeordnete Veranstaltung.
|
||||
* @property name Offizieller Name des Turniers.
|
||||
* @property sparte Sparte des Turniers (Springen, Dressur, Vielseitigkeit, etc.).
|
||||
* @property kategorie Turnierkategorie gemäß ÖTO (CSN-C, CDN, CAN, etc.).
|
||||
* @property datum Datum des Turniers (kann innerhalb der Veranstaltungsdauer liegen).
|
||||
* @property richterObmannId ID des Richter-Obmanns (Referenz auf officials-context).
|
||||
* @property parcoursbauerId ID des Parcoursbauers (Referenz auf officials-context, nur Springen).
|
||||
* @property status Aktueller Status des Turniers.
|
||||
* @property maxBewerbe Maximale Anzahl an Bewerben (optional, aus Ausschreibung).
|
||||
* @property istMeisterschaft Ob dieses Turnier Meisterschafts-Charakter hat.
|
||||
* @property bemerkungen Interne Bemerkungen.
|
||||
* @property createdAt Erstellungszeitpunkt.
|
||||
* @property updatedAt Letzter Änderungszeitpunkt.
|
||||
*/
|
||||
@Serializable
|
||||
data class DomTurnier(
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
val turnierId: Uuid = Uuid.random(),
|
||||
|
||||
// Zugehörigkeit
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
var veranstaltungId: Uuid,
|
||||
|
||||
// Basis-Informationen
|
||||
var name: String,
|
||||
var sparte: SparteE,
|
||||
var kategorie: TurnierkategorieE,
|
||||
var datum: LocalDate,
|
||||
|
||||
// Funktionäre
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
var richterObmannId: Uuid? = null,
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
var parcoursbauerId: Uuid? = null,
|
||||
|
||||
// Workflow-Status
|
||||
var status: TurnierStatusE = TurnierStatusE.GEPLANT,
|
||||
|
||||
// Konfiguration
|
||||
var maxBewerbe: Int? = null,
|
||||
var istMeisterschaft: Boolean = false,
|
||||
|
||||
// Administrativ
|
||||
var bemerkungen: String? = null,
|
||||
|
||||
// Audit
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
val createdAt: Instant = Clock.System.now(),
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
var updatedAt: Instant = Clock.System.now()
|
||||
) {
|
||||
/**
|
||||
* Prüft ob das Turnier Pflicht-Funktionäre zugewiesen hat.
|
||||
* Gibt Warnungen zurück – keine harten Fehler (Warn-Logik statt Exception, ADR-0007).
|
||||
*/
|
||||
fun validateFunktionaerBesetzung(): List<String> {
|
||||
val warnings = mutableListOf<String>()
|
||||
if (richterObmannId == null) {
|
||||
warnings.add("Kein Richter-Obmann zugewiesen. Pflichtfeld für Turnierdurchführung (ÖTO).")
|
||||
}
|
||||
if (sparte == SparteE.SPRINGEN && parcoursbauerId == null) {
|
||||
warnings.add("Kein Parcoursbauer zugewiesen. Pflichtfeld für Springturniere (ÖTO).")
|
||||
}
|
||||
return warnings
|
||||
}
|
||||
|
||||
/**
|
||||
* Validiert die Pflichtfelder für die Turnier-Planung.
|
||||
* Gibt Warnungen zurück – keine harten Fehler (Warn-Logik statt Exception, ADR-0007).
|
||||
*/
|
||||
fun validateFuerPlanung(): List<String> {
|
||||
val warnings = mutableListOf<String>()
|
||||
if (name.isBlank()) {
|
||||
warnings.add("Turniername ist erforderlich.")
|
||||
}
|
||||
maxBewerbe?.let { max ->
|
||||
if (max <= 0) {
|
||||
warnings.add("Maximale Bewerb-Anzahl muss positiv sein.")
|
||||
}
|
||||
}
|
||||
return warnings
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine Kopie mit aktualisiertem Zeitstempel.
|
||||
*/
|
||||
fun withUpdatedTimestamp(): DomTurnier = this.copy(updatedAt = Clock.System.now())
|
||||
}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.events.domain.model
|
||||
|
||||
import at.mocode.core.domain.model.SparteE
|
||||
import at.mocode.core.domain.model.VeranstaltungsStatusE
|
||||
import at.mocode.core.domain.model.VeranstaltungsTypE
|
||||
import at.mocode.core.domain.serialization.KotlinInstantSerializer
|
||||
import at.mocode.core.domain.serialization.UuidSerializer
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Aggregate Root für eine pferdesportliche Veranstaltung gemäß ÖTO § 2 Abs. 1.
|
||||
*
|
||||
* Eine Veranstaltung ist der organisatorische Rahmen (z.B. "Frühjahrsturnier Wiener Neustadt").
|
||||
* Sie kann ein oder mehrere Turniere enthalten. Die Unterscheidung Veranstaltung ≠ Turnier
|
||||
* ist eine zentrale ADR-Entscheidung (ADR-0003).
|
||||
*
|
||||
* @property veranstaltungId Eindeutige interne ID (UUID).
|
||||
* @property name Offizieller Name der Veranstaltung.
|
||||
* @property veranstaltungsTyp Typ gemäß ÖTO (z.B. NATIONAL, INTERNATIONAL).
|
||||
* @property sparten Liste der enthaltenen Sparten (Springen, Dressur, etc.).
|
||||
* @property veranstalterVereinId ID des veranstaltenden Vereins (Referenz auf clubs-context).
|
||||
* @property verantwortlicheFunktionaerId ID des verantwortlichen Funktionärs (Referenz auf officials-context).
|
||||
* @property startDatum Erster Veranstaltungstag.
|
||||
* @property endDatum Letzter Veranstaltungstag.
|
||||
* @property ort Veranstaltungsort (Adresse / Reitanlage).
|
||||
* @property nennschluss Nennschluss-Datum gemäß Ausschreibung.
|
||||
* @property status Aktueller Status im Planungs-Workflow.
|
||||
* @property ausschreibungsId Referenz auf die zugehörige Ausschreibung (optional bis Genehmigung).
|
||||
* @property oepsGenehmigungsNummer Offizielle Genehmigungsnummer des Verbands (nach Genehmigung).
|
||||
* @property bemerkungen Interne Bemerkungen.
|
||||
* @property createdAt Erstellungszeitpunkt.
|
||||
* @property updatedAt Letzter Änderungszeitpunkt.
|
||||
*/
|
||||
@Serializable
|
||||
data class DomVeranstaltung(
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
val veranstaltungId: Uuid = Uuid.random(),
|
||||
|
||||
// Basis-Informationen
|
||||
var name: String,
|
||||
var veranstaltungsTyp: VeranstaltungsTypE,
|
||||
var sparten: List<SparteE> = emptyList(),
|
||||
|
||||
// Organisation
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
var veranstalterVereinId: Uuid,
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
var verantwortlicheFunktionaerId: Uuid? = null,
|
||||
|
||||
// Termine
|
||||
var startDatum: LocalDate,
|
||||
var endDatum: LocalDate,
|
||||
var ort: String,
|
||||
var nennschluss: LocalDate? = null,
|
||||
|
||||
// Workflow-Status
|
||||
var status: VeranstaltungsStatusE = VeranstaltungsStatusE.IN_PLANUNG,
|
||||
|
||||
// Verknüpfungen
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
var ausschreibungsId: Uuid? = null,
|
||||
var oepsGenehmigungsNummer: String? = null,
|
||||
|
||||
// Administrativ
|
||||
var bemerkungen: String? = null,
|
||||
|
||||
// Audit
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
val createdAt: Instant = Clock.System.now(),
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
var updatedAt: Instant = Clock.System.now()
|
||||
) {
|
||||
/**
|
||||
* Gibt die Dauer der Veranstaltung in Tagen zurück.
|
||||
*/
|
||||
fun getDauerInTagen(): Int =
|
||||
(endDatum.toEpochDays() - startDatum.toEpochDays()).toInt() + 1
|
||||
|
||||
/**
|
||||
* Prüft ob die Veranstaltung mehrtägig ist.
|
||||
*/
|
||||
fun istMehrtaegig(): Boolean = startDatum != endDatum
|
||||
|
||||
/**
|
||||
* Prüft ob Nennungen aktuell möglich sind (Status GENEHMIGT und Nennschluss nicht abgelaufen).
|
||||
* Gibt Warnungen zurück – keine harten Fehler (Warn-Logik statt Exception, ADR-0007).
|
||||
*/
|
||||
fun validateNennungsmoeglichkeit(): List<String> {
|
||||
val warnings = mutableListOf<String>()
|
||||
if (status != VeranstaltungsStatusE.GENEHMIGT) {
|
||||
warnings.add(
|
||||
"Veranstaltung ist nicht im Status GENEHMIGT (aktuell: $status). " +
|
||||
"Nennungen sind erst nach Genehmigung möglich."
|
||||
)
|
||||
}
|
||||
if (nennschluss == null) {
|
||||
warnings.add("Kein Nennschluss definiert. Bitte Ausschreibung vervollständigen.")
|
||||
}
|
||||
if (veranstalterVereinId == null) {
|
||||
warnings.add("Kein Veranstalter-Verein zugewiesen.")
|
||||
}
|
||||
return warnings
|
||||
}
|
||||
|
||||
/**
|
||||
* Validiert die Pflichtfelder für die Einreichung beim Verband.
|
||||
* Gibt Warnungen zurück – keine harten Fehler (Warn-Logik statt Exception, ADR-0007).
|
||||
*/
|
||||
fun validateFuerEinreichung(): List<String> {
|
||||
val warnings = mutableListOf<String>()
|
||||
if (name.isBlank()) {
|
||||
warnings.add("Veranstaltungsname ist erforderlich.")
|
||||
}
|
||||
if (ort.isBlank()) {
|
||||
warnings.add("Veranstaltungsort ist erforderlich.")
|
||||
}
|
||||
if (endDatum < startDatum) {
|
||||
warnings.add("Enddatum darf nicht vor dem Startdatum liegen.")
|
||||
}
|
||||
if (sparten.isEmpty()) {
|
||||
warnings.add("Mindestens eine Sparte muss angegeben werden.")
|
||||
}
|
||||
if (nennschluss == null) {
|
||||
warnings.add("Nennschluss ist für die Einreichung erforderlich.")
|
||||
}
|
||||
nennschluss?.let { nl ->
|
||||
if (nl >= startDatum) {
|
||||
warnings.add("Nennschluss muss vor dem Veranstaltungsbeginn liegen (§ 2 ÖTO).")
|
||||
}
|
||||
}
|
||||
if (ausschreibungsId == null) {
|
||||
warnings.add("Keine Ausschreibung verknüpft. Einreichung ohne Ausschreibung nicht möglich.")
|
||||
}
|
||||
return warnings
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine Kopie mit aktualisiertem Zeitstempel.
|
||||
*/
|
||||
fun withUpdatedTimestamp(): DomVeranstaltung = this.copy(updatedAt = Clock.System.now())
|
||||
}
|
||||
Reference in New Issue
Block a user