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,174 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.persons.infrastructure.persistence
import at.mocode.core.domain.model.DatenQuelleE
import at.mocode.core.domain.model.LizenzKlasseE
import at.mocode.core.domain.model.SparteE
import at.mocode.persons.domain.model.DomReiter
import at.mocode.persons.domain.repository.ReiterRepository
import kotlinx.serialization.json.Json
import org.jetbrains.exposed.v1.core.*
import org.jetbrains.exposed.v1.core.statements.UpdateBuilder
import org.jetbrains.exposed.v1.jdbc.deleteWhere
import org.jetbrains.exposed.v1.jdbc.insert
import org.jetbrains.exposed.v1.jdbc.selectAll
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import org.jetbrains.exposed.v1.jdbc.update
import java.util.*
import kotlin.time.Clock
import kotlin.uuid.Uuid
import kotlin.uuid.toJavaUuid
import kotlin.uuid.toKotlinUuid
/**
* Exposed-basierte Implementierung des ReiterRepository.
*/
class ExposedReiterRepository : ReiterRepository {
override suspend fun findById(id: Uuid): DomReiter? = transaction {
ReiterTable.selectAll().where { ReiterTable.id eq id.toJavaUuid() }
.map { rowToReiter(it) }
.singleOrNull()
}
override suspend fun findBySatznummer(satznummer: String): DomReiter? = transaction {
ReiterTable.selectAll().where { ReiterTable.satznummer eq satznummer }
.map { rowToReiter(it) }
.singleOrNull()
}
override suspend fun findByFeiId(feiId: String): DomReiter? = transaction {
ReiterTable.selectAll().where { ReiterTable.feiId eq feiId }
.map { rowToReiter(it) }
.singleOrNull()
}
override suspend fun findByName(searchTerm: String, limit: Int): List<DomReiter> = transaction {
val pattern = "%$searchTerm%"
ReiterTable.selectAll().where {
(ReiterTable.nachname like pattern) or (ReiterTable.vorname like pattern)
}.limit(limit).map { rowToReiter(it) }
}
override suspend fun findByVereinsNummer(vereinsNummer: String, activeOnly: Boolean): List<DomReiter> = transaction {
ReiterTable.selectAll().where {
(ReiterTable.vereinsNummer eq vereinsNummer).let {
if (activeOnly) it and (ReiterTable.istAktiv eq true) else it
}
}.map { rowToReiter(it) }
}
override suspend fun findByLizenzKlasse(lizenzKlasse: LizenzKlasseE, activeOnly: Boolean): List<DomReiter> =
transaction {
ReiterTable.selectAll().where {
(ReiterTable.lizenzKlasse eq lizenzKlasse.name).let {
if (activeOnly) it and (ReiterTable.istAktiv eq true) else it
}
}.map { rowToReiter(it) }
}
override suspend fun findBySparte(sparte: SparteE, activeOnly: Boolean): List<DomReiter> = transaction {
ReiterTable.selectAll().where {
(ReiterTable.lizenziertFuerSparten like "%${sparte.name}%").let {
if (activeOnly) it and (ReiterTable.istAktiv eq true) else it
}
}.map { rowToReiter(it) }
}
override suspend fun findGastreiter(activeOnly: Boolean): List<DomReiter> = transaction {
ReiterTable.selectAll().where {
(ReiterTable.istGastreiter eq true).let {
if (activeOnly) it and (ReiterTable.istAktiv eq true) else it
}
}.map { rowToReiter(it) }
}
override suspend fun findAllActive(limit: Int, offset: Int): List<DomReiter> = transaction {
ReiterTable.selectAll().where { ReiterTable.istAktiv eq true }
.limit(limit).offset(offset.toLong())
.map { rowToReiter(it) }
}
override suspend fun findAll(limit: Int, offset: Int): List<DomReiter> = transaction {
ReiterTable.selectAll()
.limit(limit).offset(offset.toLong())
.map { rowToReiter(it) }
}
override suspend fun save(reiter: DomReiter): DomReiter = transaction {
val now = Clock.System.now()
val updated = reiter.copy(updatedAt = now)
val javaId = reiter.reiterId.toJavaUuid()
val existing = ReiterTable.selectAll().where { ReiterTable.id eq javaId }.singleOrNull()
if (existing != null) {
ReiterTable.update({ ReiterTable.id eq javaId }) { reiterToStatement(it, updated) }
} else {
ReiterTable.insert {
it[id] = javaId
reiterToStatement(it, updated)
}
}
updated
}
override suspend fun delete(id: Uuid): Boolean = transaction {
ReiterTable.deleteWhere { ReiterTable.id eq id.toJavaUuid() } > 0
}
override suspend fun countActive(): Long = transaction {
ReiterTable.selectAll().where { ReiterTable.istAktiv eq true }.count()
}
override suspend fun existsBySatznummer(satznummer: String): Boolean = transaction {
ReiterTable.selectAll().where { ReiterTable.satznummer eq satznummer }.count() > 0
}
private fun rowToReiter(row: ResultRow): DomReiter {
val sparten = try {
Json.decodeFromString<List<SparteE>>(row[ReiterTable.lizenziertFuerSparten])
} catch (_: Exception) {
emptyList()
}
return DomReiter(
reiterId = (row[ReiterTable.id] as UUID).toKotlinUuid(),
personId = (row[ReiterTable.id] as UUID).toKotlinUuid(), // same as reiterId for now
satznummer = row[ReiterTable.satznummer] ?: "",
feiId = row[ReiterTable.feiId],
nation = row[ReiterTable.nation],
vorname = row[ReiterTable.vorname],
nachname = row[ReiterTable.nachname],
geburtsdatum = row[ReiterTable.geburtsdatum],
vereinsNummer = row[ReiterTable.vereinsNummer],
vereinsName = row[ReiterTable.vereinsName],
lizenzKlasse = runCatching { LizenzKlasseE.valueOf(row[ReiterTable.lizenzKlasse] ?: "") }.getOrDefault(
LizenzKlasseE.LIZENZFREI
),
lizenzSparten = sparten,
istGastreiter = row[ReiterTable.istGastreiter],
istAktiv = row[ReiterTable.istAktiv],
datenQuelle = runCatching { DatenQuelleE.valueOf(row[ReiterTable.datenQuelle]) }.getOrDefault(DatenQuelleE.IMPORT_ZNS),
createdAt = row[ReiterTable.createdAt],
updatedAt = row[ReiterTable.updatedAt]
)
}
private fun reiterToStatement(stmt: UpdateBuilder<*>, r: DomReiter) {
stmt[ReiterTable.satznummer] = r.satznummer
stmt[ReiterTable.feiId] = r.feiId
stmt[ReiterTable.vorname] = r.vorname
stmt[ReiterTable.nachname] = r.nachname
stmt[ReiterTable.geburtsdatum] = r.geburtsdatum
stmt[ReiterTable.nation] = r.nation
stmt[ReiterTable.vereinsNummer] = r.vereinsNummer
stmt[ReiterTable.vereinsName] = r.vereinsName
stmt[ReiterTable.lizenzKlasse] = r.lizenzKlasse.name
stmt[ReiterTable.lizenziertFuerSparten] = Json.encodeToString(r.lizenzSparten)
stmt[ReiterTable.istGastreiter] = r.istGastreiter
stmt[ReiterTable.istAktiv] = r.istAktiv
stmt[ReiterTable.datenQuelle] = r.datenQuelle.name
stmt[ReiterTable.createdAt] = r.createdAt
stmt[ReiterTable.updatedAt] = r.updatedAt
}
}
@@ -0,0 +1,55 @@
package at.mocode.persons.infrastructure.persistence
import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.core.java.javaUUID
import org.jetbrains.exposed.v1.datetime.date
import org.jetbrains.exposed.v1.datetime.timestamp
/**
* Exposed-Tabellendefinition für Reiter (DomReiter).
*
* Speichert alle Reiter-Daten inkl. Lizenz, Sparten (JSON) und ZNS-Identifikation.
*/
object ReiterTable : Table("reiter") {
val id = javaUUID("id").autoGenerate()
override val primaryKey = PrimaryKey(id)
// Identifikation
val satznummer = varchar("satznummer", 20).nullable()
val feiId = varchar("fei_id", 20).nullable()
// Persönliche Daten
val vorname = varchar("vorname", 100)
val nachname = varchar("nachname", 100)
val geburtsdatum = date("geburtsdatum").nullable()
val nation = varchar("nation", 3).nullable().default("AUT")
// Vereinsdaten
val vereinsNummer = varchar("vereins_nummer", 20).nullable()
val vereinsName = varchar("vereins_name", 200).nullable()
// Lizenz & Qualifikation
val lizenzKlasse = varchar("lizenz_klasse", 50).nullable()
val lizenziertFuerSparten = text("lizenziert_fuer_sparten") // JSON array of SparteE
// Status & Verwaltung
val istAktiv = bool("ist_aktiv").default(true)
val istGastreiter = bool("ist_gastreiter").default(false)
val bemerkungen = text("bemerkungen").nullable()
val datenQuelle = varchar("daten_quelle", 50)
// Audit-Felder
val createdAt = timestamp("created_at")
val updatedAt = timestamp("updated_at")
init {
index(false, nachname)
index(false, vorname)
index(true, satznummer)
index(true, feiId)
index(false, vereinsNummer)
index(false, istAktiv)
index(false, lizenzKlasse)
}
}