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,121 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.clubs.domain.model
import at.mocode.core.domain.model.DatenQuelleE
import at.mocode.core.domain.serialization.KotlinInstantSerializer
import at.mocode.core.domain.serialization.UuidSerializer
import kotlinx.serialization.Serializable
import kotlin.time.Clock
import kotlin.time.Instant
import kotlin.uuid.Uuid
/**
* Domain-Modell für einen Verein im actor-context.
*
* Repräsentiert einen OEPS-Mitgliedsverein, der als Veranstalter von Turnieren
* und als Heimverein von Reitern und Funktionären fungiert.
* Daten werden primär aus dem ZNS (VEREIN01.dat) importiert.
*
* Aggregate Root des `clubs`-Bounded Context.
*
* @property vereinId Eindeutige interne ID (UUID).
* @property vereinsNummer ÖPS-Vereinsnummer aus ZNS (VEREIN01.dat), 4-stellig. Primärschlüssel für ZNS-Datenaustausch.
* @property name Offizieller Vereinsname.
* @property kurzname Kurzbezeichnung des Vereins (optional).
* @property bundesland Bundesland, in dem der Verein ansässig ist.
* @property ort Ort / Stadt des Vereinssitzes.
* @property plz Postleitzahl.
* @property strasse Straße und Hausnummer.
* @property email Offizielle E-Mail-Adresse des Vereins.
* @property telefon Telefonnummer des Vereins.
* @property website Website-URL des Vereins.
* @property oepsRegionNummer Regionsnummer beim OEPS (Landesverband).
* @property istVeranstalter Ob der Verein als Veranstalter von Turnieren zugelassen ist.
* @property istAktiv Ob der Verein aktuell aktiv ist.
* @property bemerkungen Interne Notizen.
* @property datenQuelle Herkunft des Datensatzes (ZNS-Import oder manuell).
* @property createdAt Erstellungszeitpunkt.
* @property updatedAt Letzter Änderungszeitpunkt.
*/
@Serializable
data class DomVerein(
@Serializable(with = UuidSerializer::class)
val vereinId: Uuid = Uuid.random(),
// Identifikation
val vereinsNummer: String,
// Stammdaten
var name: String,
var kurzname: String? = null,
// Adresse
var bundesland: String? = null,
var ort: String? = null,
var plz: String? = null,
var strasse: String? = null,
// Kontakt
var email: String? = null,
var telefon: String? = null,
var website: String? = null,
// OEPS-Verwaltung
var oepsRegionNummer: String? = null,
var istVeranstalter: Boolean = false,
// 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 Anzeigenamen zurück Kurzname bevorzugt, sonst vollständiger Name.
*/
fun getDisplayName(): String = kurzname ?: name
/**
* Gibt den vollständigen Anzeigenamen mit Vereinsnummer zurück.
*/
fun getDisplayNameWithNummer(): String = "${getDisplayName()} ($vereinsNummer)"
/**
* Prüft, ob vollständige Adressdaten vorhanden sind.
*/
fun hasCompleteAddress(): Boolean =
!ort.isNullOrBlank() && !plz.isNullOrBlank() && !strasse.isNullOrBlank()
/**
* Validiert den Verein für den Einsatz als Veranstalter.
* Gibt Warnungen zurück (kein harter Fehler Override-Event möglich).
*/
fun validateFuerVeranstaltung(): List<String> {
val warnings = mutableListOf<String>()
if (!istAktiv) {
warnings.add("Verein ${getDisplayName()} ist nicht aktiv.")
}
if (!istVeranstalter) {
warnings.add("Verein ${getDisplayName()} ist nicht als Veranstalter zugelassen.")
}
if (!hasCompleteAddress()) {
warnings.add("Verein ${getDisplayName()} hat keine vollständige Adresse hinterlegt.")
}
return warnings
}
/**
* Erstellt eine Kopie mit aktualisiertem Zeitstempel.
*/
fun withUpdatedTimestamp(): DomVerein = this.copy(updatedAt = Clock.System.now())
}
@@ -0,0 +1,72 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.clubs.domain.repository
import at.mocode.clubs.domain.model.DomVerein
import kotlin.uuid.Uuid
/**
* Repository-Interface für DomVerein (Verein) Domain-Operationen.
*
* Definiert den Vertrag für Datenzugriffs-Operationen ohne Abhängigkeit
* von konkreten Implementierungsdetails (Datenbank, etc.).
*/
interface VereinRepository {
/**
* Sucht einen Verein anhand seiner eindeutigen ID.
*/
suspend fun findById(id: Uuid): DomVerein?
/**
* Sucht einen Verein anhand seiner OEPS-Vereinsnummer.
*/
suspend fun findByVereinsNummer(vereinsNummer: String): DomVerein?
/**
* Sucht Vereine anhand des Namens (Teilübereinstimmung).
*/
suspend fun findByName(searchTerm: String, limit: Int = 50): List<DomVerein>
/**
* Sucht alle Vereine eines Bundeslandes.
*/
suspend fun findByBundesland(bundesland: String, activeOnly: Boolean = true): List<DomVerein>
/**
* Sucht alle Vereine, die als Veranstalter markiert sind.
*/
suspend fun findVeranstalter(activeOnly: Boolean = true): List<DomVerein>
/**
* Gibt alle aktiven Vereine zurück (paginiert).
*/
suspend fun findAllActive(limit: Int = 100, offset: Int = 0): List<DomVerein>
/**
* Gibt alle Vereine zurück (paginiert).
*/
suspend fun findAll(limit: Int = 100, offset: Int = 0): List<DomVerein>
/**
* Speichert einen Verein (Insert oder Update).
*/
suspend fun save(verein: DomVerein): DomVerein
/**
* Löscht einen Verein anhand seiner ID.
*
* @return true wenn gelöscht, false wenn nicht gefunden
*/
suspend fun delete(id: Uuid): Boolean
/**
* Zählt alle aktiven Vereine.
*/
suspend fun countActive(): Long
/**
* Prüft ob ein Verein mit der gegebenen Vereinsnummer bereits existiert.
*/
suspend fun existsByVereinsNummer(vereinsNummer: String): Boolean
}