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,126 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.officials.domain.model
import at.mocode.core.domain.model.DatenQuelleE
import at.mocode.core.domain.model.FunktionaerRolleE
import at.mocode.core.domain.model.RichterQualifikationE
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
/**
* Domain-Modell für einen Funktionär im actor-context.
*
* Repräsentiert eine Person mit einer definierten Rolle bei Turnieren (Richter, TBA,
* Parcoursbauer, etc.). Die Qualifikation wird gegen `RICHT01.DAT` aus dem ZNS geprüft.
*
* Aggregate Root des `officials`-Bounded Context.
*
* @property funktionaerId Eindeutige interne ID (UUID).
* @property richterNummer ÖPS-Funktionärsnummer aus ZNS (RICHT01.dat), 6-stellig.
* @property vorname Vorname der Person.
* @property nachname Nachname der Person.
* @property geburtsdatum Geburtsdatum (optional, für Altersklassen-Prüfung).
* @property rollen Menge der Rollen, die diese Person ausüben darf (TBA, Richter, ...).
* @property richterQualifikation Qualifikationsstufe als Richter (GA, G1G3, International).
* @property qualifiziertFuerSparten Sparten, für die eine Richter-Qualifikation vorliegt.
* @property email E-Mail-Adresse für Kommunikation.
* @property telefon Telefonnummer.
* @property vereinsNummer Vereinsnummer des Heimvereins (Referenz auf DomVerein).
* @property istAktiv Ob der Funktionär aktuell aktiv/einsatzbereit ist.
* @property bemerkungen Interne Notizen.
* @property datenQuelle Herkunft des Datensatzes (ZNS-Import oder manuell).
* @property createdAt Erstellungszeitpunkt.
* @property updatedAt Letzter Änderungszeitpunkt.
*/
@Serializable
data class DomFunktionaer(
@Serializable(with = UuidSerializer::class)
val funktionaerId: Uuid = Uuid.random(),
// Identifikation
val richterNummer: String? = null,
// Persönliche Daten
var vorname: String,
var nachname: String,
var geburtsdatum: LocalDate? = null,
// Qualifikation & Rollen
var rollen: Set<FunktionaerRolleE> = emptySet(),
var richterQualifikation: RichterQualifikationE? = null,
var qualifiziertFuerSparten: Set<SparteE> = emptySet(),
// Kontakt
var email: String? = null,
var telefon: String? = null,
// Vereinszugehörigkeit
var vereinsNummer: String? = null,
// Status & Verwaltung
var istAktiv: Boolean = true,
var bemerkungen: String? = null,
var datenQuelle: DatenQuelleE = DatenQuelleE.IMPORT_ZNS,
// Audit
@Serializable(with = KotlinInstantSerializer::class)
val createdAt: Instant = Clock.System.now(),
@Serializable(with = KotlinInstantSerializer::class)
var updatedAt: Instant = Clock.System.now()
) {
/**
* Gibt den vollständigen Anzeigenamen zurück.
*/
fun getDisplayName(): String = "$vorname $nachname"
/**
* Gibt den Anzeigenamen mit Funktionärsnummer zurück (falls vorhanden).
*/
fun getDisplayNameWithNummer(): String =
richterNummer?.let { "${getDisplayName()} ($it)" } ?: getDisplayName()
/**
* Prüft, ob der Funktionär als Richter für eine bestimmte Sparte qualifiziert ist.
*/
fun istRichterFuerSparte(sparte: SparteE): Boolean =
rollen.contains(FunktionaerRolleE.RICHTER) && qualifiziertFuerSparten.contains(sparte)
/**
* Prüft, ob der Funktionär die Rolle TBA ausüben darf.
*/
fun istTba(): Boolean = rollen.contains(FunktionaerRolleE.TBA)
/**
* Validiert die Pflichtfelder für den Turniereinsatz.
* Gibt eine Liste von Warnungen zurück (kein harter Fehler Override-Event möglich).
*/
fun validateFuerTurniereinsatz(rolle: FunktionaerRolleE, sparte: SparteE? = null): List<String> {
val warnings = mutableListOf<String>()
if (!istAktiv) {
warnings.add("Funktionär ${getDisplayName()} ist nicht aktiv.")
}
if (!rollen.contains(rolle)) {
warnings.add("Funktionär ${getDisplayName()} hat keine Qualifikation für Rolle $rolle.")
}
if (rolle == FunktionaerRolleE.RICHTER && sparte != null && !istRichterFuerSparte(sparte)) {
warnings.add("Funktionär ${getDisplayName()} ist nicht als Richter für Sparte $sparte qualifiziert.")
}
return warnings
}
/**
* Erstellt eine Kopie mit aktualisiertem Zeitstempel.
*/
fun withUpdatedTimestamp(): DomFunktionaer = this.copy(updatedAt = Clock.System.now())
}
@@ -0,0 +1,93 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.officials.domain.repository
import at.mocode.core.domain.model.FunktionaerRolleE
import at.mocode.core.domain.model.RichterQualifikationE
import at.mocode.core.domain.model.SparteE
import at.mocode.officials.domain.model.DomFunktionaer
import kotlin.uuid.Uuid
/**
* Repository-Interface für DomFunktionaer (Funktionär) Domain-Operationen.
*
* Definiert den Vertrag für Datenzugriffs-Operationen ohne Abhängigkeit
* von konkreten Implementierungsdetails (Datenbank, etc.).
*/
interface FunktionaerRepository {
/**
* Sucht einen Funktionär anhand seiner eindeutigen ID.
*/
suspend fun findById(id: Uuid): DomFunktionaer?
/**
* Sucht einen Funktionär anhand seiner Richternummer.
*/
suspend fun findByRichterNummer(richterNummer: String): DomFunktionaer?
/**
* Sucht Funktionäre anhand von Vor- und/oder Nachname (Teilübereinstimmung).
*/
suspend fun findByName(searchTerm: String, limit: Int = 50): List<DomFunktionaer>
/**
* Sucht alle Funktionäre mit einer bestimmten Rolle.
*/
suspend fun findByRolle(rolle: FunktionaerRolleE, activeOnly: Boolean = true): List<DomFunktionaer>
/**
* Sucht alle Richter mit einer bestimmten Qualifikation.
*/
suspend fun findByRichterQualifikation(
qualifikation: RichterQualifikationE,
activeOnly: Boolean = true
): List<DomFunktionaer>
/**
* Sucht alle Funktionäre, die für eine bestimmte Sparte qualifiziert sind.
*/
suspend fun findBySparte(sparte: SparteE, activeOnly: Boolean = true): List<DomFunktionaer>
/**
* Sucht alle Funktionäre eines bestimmten Vereins.
*/
suspend fun findByVereinsNummer(vereinsNummer: String, activeOnly: Boolean = true): List<DomFunktionaer>
/**
* Gibt alle aktiven Funktionäre zurück (paginiert).
*/
suspend fun findAllActive(limit: Int = 100, offset: Int = 0): List<DomFunktionaer>
/**
* Gibt alle Funktionäre zurück (paginiert).
*/
suspend fun findAll(limit: Int = 100, offset: Int = 0): List<DomFunktionaer>
/**
* Speichert einen Funktionär (Insert oder Update).
*/
suspend fun save(funktionaer: DomFunktionaer): DomFunktionaer
/**
* Löscht einen Funktionär anhand seiner ID.
*
* @return true wenn gelöscht, false wenn nicht gefunden
*/
suspend fun delete(id: Uuid): Boolean
/**
* Zählt alle aktiven Funktionäre.
*/
suspend fun countActive(): Long
/**
* Zählt alle Richter (Rolle = RICHTER) mit einer bestimmten Qualifikation.
*/
suspend fun countByRichterQualifikation(qualifikation: RichterQualifikationE, activeOnly: Boolean = true): Long
/**
* Prüft ob ein Funktionär mit der gegebenen Richternummer bereits existiert.
*/
suspend fun existsByRichterNummer(richterNummer: String): Boolean
}