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,142 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.clubs.infrastructure.persistence
import at.mocode.clubs.domain.model.DomVerein
import at.mocode.clubs.domain.repository.VereinRepository
import at.mocode.core.domain.model.DatenQuelleE
import org.jetbrains.exposed.v1.core.ResultRow
import org.jetbrains.exposed.v1.core.and
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.core.like
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 VereinRepository.
*/
class ExposedVereinRepository : VereinRepository {
override suspend fun findById(id: Uuid): DomVerein? = transaction {
VereinTable.selectAll().where { VereinTable.id eq id.toJavaUuid() }
.map { rowToVerein(it) }
.singleOrNull()
}
override suspend fun findByVereinsNummer(vereinsNummer: String): DomVerein? = transaction {
VereinTable.selectAll().where { VereinTable.vereinsNummer eq vereinsNummer }
.map { rowToVerein(it) }
.singleOrNull()
}
override suspend fun findByName(searchTerm: String, limit: Int): List<DomVerein> = transaction {
VereinTable.selectAll().where { VereinTable.name like "%$searchTerm%" }
.limit(limit).map { rowToVerein(it) }
}
override suspend fun findByBundesland(bundesland: String, activeOnly: Boolean): List<DomVerein> = transaction {
VereinTable.selectAll().where {
(VereinTable.bundesland eq bundesland).let {
if (activeOnly) it and (VereinTable.istAktiv eq true) else it
}
}.map { rowToVerein(it) }
}
override suspend fun findVeranstalter(activeOnly: Boolean): List<DomVerein> = transaction {
VereinTable.selectAll().where {
(VereinTable.istVeranstalter eq true).let {
if (activeOnly) it and (VereinTable.istAktiv eq true) else it
}
}.map { rowToVerein(it) }
}
override suspend fun findAllActive(limit: Int, offset: Int): List<DomVerein> = transaction {
VereinTable.selectAll().where { VereinTable.istAktiv eq true }
.limit(limit).offset(offset.toLong())
.map { rowToVerein(it) }
}
override suspend fun findAll(limit: Int, offset: Int): List<DomVerein> = transaction {
VereinTable.selectAll()
.limit(limit).offset(offset.toLong())
.map { rowToVerein(it) }
}
override suspend fun save(verein: DomVerein): DomVerein = transaction {
val now = Clock.System.now()
val updated = verein.copy(updatedAt = now)
val javaId = verein.vereinId.toJavaUuid()
val existing = VereinTable.selectAll().where { VereinTable.id eq javaId }.singleOrNull()
if (existing != null) {
VereinTable.update({ VereinTable.id eq javaId }) { vereinToStatement(it, updated) }
} else {
VereinTable.insert {
it[id] = javaId
vereinToStatement(it, updated)
}
}
updated
}
override suspend fun delete(id: Uuid): Boolean = transaction {
VereinTable.deleteWhere { VereinTable.id eq id.toJavaUuid() } > 0
}
override suspend fun countActive(): Long = transaction {
VereinTable.selectAll().where { VereinTable.istAktiv eq true }.count()
}
override suspend fun existsByVereinsNummer(vereinsNummer: String): Boolean = transaction {
VereinTable.selectAll().where { VereinTable.vereinsNummer eq vereinsNummer }.count() > 0
}
private fun rowToVerein(row: ResultRow): DomVerein = DomVerein(
vereinId = (row[VereinTable.id] as UUID).toKotlinUuid(),
vereinsNummer = row[VereinTable.vereinsNummer],
name = row[VereinTable.name],
kurzname = row[VereinTable.kurzname],
bundesland = row[VereinTable.bundesland],
ort = row[VereinTable.ort],
plz = row[VereinTable.plz],
strasse = row[VereinTable.strasse],
email = row[VereinTable.email],
telefon = row[VereinTable.telefon],
website = row[VereinTable.webseite],
oepsRegionNummer = row[VereinTable.oepsRegionsNummer],
istVeranstalter = row[VereinTable.istVeranstalter],
istAktiv = row[VereinTable.istAktiv],
bemerkungen = row[VereinTable.bemerkungen],
datenQuelle = runCatching { DatenQuelleE.valueOf(row[VereinTable.datenQuelle]) }.getOrDefault(DatenQuelleE.IMPORT_ZNS),
createdAt = row[VereinTable.createdAt],
updatedAt = row[VereinTable.updatedAt]
)
private fun vereinToStatement(stmt: UpdateBuilder<*>, v: DomVerein) {
stmt[VereinTable.vereinsNummer] = v.vereinsNummer
stmt[VereinTable.name] = v.name
stmt[VereinTable.kurzname] = v.kurzname
stmt[VereinTable.bundesland] = v.bundesland
stmt[VereinTable.ort] = v.ort
stmt[VereinTable.plz] = v.plz
stmt[VereinTable.strasse] = v.strasse
stmt[VereinTable.email] = v.email
stmt[VereinTable.telefon] = v.telefon
stmt[VereinTable.webseite] = v.website
stmt[VereinTable.oepsRegionsNummer] = v.oepsRegionNummer
stmt[VereinTable.istVeranstalter] = v.istVeranstalter
stmt[VereinTable.istAktiv] = v.istAktiv
stmt[VereinTable.bemerkungen] = v.bemerkungen
stmt[VereinTable.datenQuelle] = v.datenQuelle.name
stmt[VereinTable.createdAt] = v.createdAt
stmt[VereinTable.updatedAt] = v.updatedAt
}
}
@@ -0,0 +1,56 @@
package at.mocode.clubs.infrastructure.persistence
import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.core.java.javaUUID
import org.jetbrains.exposed.v1.datetime.timestamp
/**
* Exposed-Tabellendefinition für Vereine (DomVerein).
*
* Speichert alle Vereins-Daten inkl. Adresse, Kontakt und OEPS-Regionsnummer.
*/
object VereinTable : Table("vereine") {
val id = javaUUID("id").autoGenerate()
override val primaryKey = PrimaryKey(id)
// Identifikation
val vereinsNummer = varchar("vereins_nummer", 20).uniqueIndex()
// Vereinsdaten
val name = varchar("name", 200)
val kurzname = varchar("kurzname", 50).nullable()
// Adresse
val strasse = varchar("strasse", 200).nullable()
val plz = varchar("plz", 10).nullable()
val ort = varchar("ort", 100).nullable()
val bundesland = varchar("bundesland", 50).nullable()
val land = varchar("land", 50).nullable().default("AT")
// Kontakt
val email = varchar("email", 255).nullable()
val telefon = varchar("telefon", 50).nullable()
val webseite = varchar("webseite", 255).nullable()
// OEPS-Daten
val oepsRegionsNummer = varchar("oeps_regions_nummer", 10).nullable()
// Status & Verwaltung
val istAktiv = bool("ist_aktiv").default(true)
val istVeranstalter = bool("ist_veranstalter").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, name)
index(false, bundesland)
index(false, istAktiv)
index(false, istVeranstalter)
index(false, oepsRegionsNummer)
}
}