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:
2026-03-24 18:22:15 +01:00
parent c624df8744
commit 354bd49de6
75 changed files with 7616 additions and 48 deletions
@@ -0,0 +1,136 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.persons.domain.model
import at.mocode.core.domain.model.DatenQuelleE
import at.mocode.core.domain.model.LizenzKlasseE
import at.mocode.core.domain.model.SparteE
import at.mocode.core.domain.serialization.KotlinInstantSerializer
import at.mocode.core.domain.serialization.KotlinLocalDateSerializer
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
/**
* Domain model representing a rider (Reiter) in the actor-context.
*
* A rider is a specialization of a person with additional equestrian-specific
* attributes such as license, start card, and competition eligibility.
* Data is primarily sourced from the OEPS ZNS (LIZENZ01.DAT).
*
* Key rules (ÖTO):
* - A rider requires an active Startkarte (annual fee paid) to compete nationally.
* - LizenzKlasse determines which competition classes the rider may enter.
* - Satznummer (6-digit) is the primary key for ZNS data exchange.
* - Kopfnummer is NOT a unique identifier it can change.
*
* @property reiterId Unique internal identifier (UUID).
* @property personId Reference to the base DomPerson record (UUID).
* @property satznummer 6-digit ZNS primary key for data exchange. Primary key for ZNS.
* @property lizenzNummer OEPS license number (from ZNS LIZENZ01.DAT).
* @property lizenzKlasse License class determining competition eligibility (e.g. R1, RD2).
* @property lizenzSparten Disciplines for which the license is valid.
* @property startkartAktiv Whether the annual start card fee has been paid.
* @property startkartSaison Season year for which the start card is valid (e.g. 2026).
* @property feiId FEI international rider ID (optional).
* @property nation Nation code (e.g. AUT).
* @property geburtsdatum Date of birth (for age class validation).
* @property vereinsNummer Club number (OEPS).
* @property vereinsName Club name.
* @property istGastreiter Whether the rider is a guest rider (foreign nationality, not in Austrian club).
* @property istAktiv Whether the rider is currently active in the system.
* @property datenQuelle Source of the data.
* @property createdAt Timestamp when this record was created.
* @property updatedAt Timestamp when this record was last updated.
*/
@Serializable
data class DomReiter(
@Serializable(with = UuidSerializer::class)
val reiterId: Uuid = Uuid.random(),
// Reference to base person
@Serializable(with = UuidSerializer::class)
val personId: Uuid,
// ZNS Identification
val satznummer: String,
val lizenzNummer: String? = null,
// License & Eligibility
val lizenzKlasse: LizenzKlasseE = LizenzKlasseE.LIZENZFREI,
val lizenzSparten: List<SparteE> = emptyList(),
// Start Card (Startkarte) annual fee proof
val startkartAktiv: Boolean = false,
val startkartSaison: Int? = null,
// International
val feiId: String? = null,
val nation: String? = null,
// Personal Data (denormalized from DomPerson for performance)
val nachname: String,
val vorname: String,
@Serializable(with = KotlinLocalDateSerializer::class)
val geburtsdatum: LocalDate? = null,
// Club Affiliation
val vereinsNummer: String? = null,
val vereinsName: String? = null,
// Status
val istGastreiter: Boolean = false,
val istAktiv: Boolean = true,
val datenQuelle: DatenQuelleE = DatenQuelleE.IMPORT_ZNS,
// Audit Fields
@Serializable(with = KotlinInstantSerializer::class)
val createdAt: Instant = Clock.System.now(),
@Serializable(with = KotlinInstantSerializer::class)
var updatedAt: Instant = Clock.System.now()
) {
/**
* Returns the display name of the rider.
*/
fun getDisplayName(): String = "$vorname $nachname"
/**
* Checks if the rider is eligible to compete nationally.
* Requires an active start card (Startkarte).
*/
fun isStartberechtigt(): Boolean = istAktiv && startkartAktiv
/**
* Checks if the rider holds a license for the given discipline.
*/
fun hasLizenzForSparte(sparte: SparteE): Boolean =
lizenzKlasse == LizenzKlasseE.LIZENZFREI || lizenzSparten.contains(sparte)
/**
* Validates the rider for competition entry.
* Returns a list of warning messages (never hard errors TBA has final say).
*/
fun validateForNennung(sparte: SparteE): List<String> {
val warnings = mutableListOf<String>()
if (!istAktiv) {
warnings.add("Reiter ${getDisplayName()} ist nicht aktiv")
}
if (!startkartAktiv) {
warnings.add("Reiter ${getDisplayName()} hat keine aktive Startkarte für Saison $startkartSaison")
}
if (!hasLizenzForSparte(sparte)) {
warnings.add("Reiter ${getDisplayName()} hat keine Lizenz für Sparte $sparte (Lizenzklasse: $lizenzKlasse)")
}
return warnings
}
/**
* Creates a copy of this rider with an updated timestamp.
*/
fun withUpdatedTimestamp(): DomReiter = this.copy(updatedAt = Clock.System.now())
}
@@ -0,0 +1,89 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.persons.domain.repository
import at.mocode.core.domain.model.LizenzKlasseE
import at.mocode.core.domain.model.SparteE
import at.mocode.persons.domain.model.DomReiter
import kotlin.uuid.Uuid
/**
* Repository-Interface für DomReiter (Reiter) Domain-Operationen.
*
* Definiert den Vertrag für Datenzugriffs-Operationen ohne Abhängigkeit
* von konkreten Implementierungsdetails (Datenbank, etc.).
*/
interface ReiterRepository {
/**
* Sucht einen Reiter anhand seiner eindeutigen ID.
*/
suspend fun findById(id: Uuid): DomReiter?
/**
* Sucht einen Reiter anhand seiner Satznummer (OEPS-Mitgliedsnummer).
*/
suspend fun findBySatznummer(satznummer: String): DomReiter?
/**
* Sucht einen Reiter anhand seiner FEI-ID.
*/
suspend fun findByFeiId(feiId: String): DomReiter?
/**
* Sucht Reiter anhand von Vor- und/oder Nachname (Teilübereinstimmung).
*/
suspend fun findByName(searchTerm: String, limit: Int = 50): List<DomReiter>
/**
* Sucht alle Reiter eines bestimmten Vereins.
*/
suspend fun findByVereinsNummer(vereinsNummer: String, activeOnly: Boolean = true): List<DomReiter>
/**
* Sucht alle Reiter mit einer bestimmten Lizenzklasse.
*/
suspend fun findByLizenzKlasse(lizenzKlasse: LizenzKlasseE, activeOnly: Boolean = true): List<DomReiter>
/**
* Sucht alle Reiter, die für eine bestimmte Sparte lizenziert sind.
*/
suspend fun findBySparte(sparte: SparteE, activeOnly: Boolean = true): List<DomReiter>
/**
* Sucht alle Gastreiter.
*/
suspend fun findGastreiter(activeOnly: Boolean = true): List<DomReiter>
/**
* Gibt alle aktiven Reiter zurück (paginiert).
*/
suspend fun findAllActive(limit: Int = 100, offset: Int = 0): List<DomReiter>
/**
* Gibt alle Reiter zurück (paginiert).
*/
suspend fun findAll(limit: Int = 100, offset: Int = 0): List<DomReiter>
/**
* Speichert einen Reiter (Insert oder Update).
*/
suspend fun save(reiter: DomReiter): DomReiter
/**
* Löscht einen Reiter anhand seiner ID.
*
* @return true wenn gelöscht, false wenn nicht gefunden
*/
suspend fun delete(id: Uuid): Boolean
/**
* Zählt alle aktiven Reiter.
*/
suspend fun countActive(): Long
/**
* Prüft ob ein Reiter mit der gegebenen Satznummer bereits existiert.
*/
suspend fun existsBySatznummer(satznummer: String): Boolean
}