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:
+186
@@ -0,0 +1,186 @@
|
||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.officials.infrastructure.persistence
|
||||
|
||||
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.officials.domain.model.DomFunktionaer
|
||||
import at.mocode.officials.domain.repository.FunktionaerRepository
|
||||
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 FunktionaerRepository.
|
||||
*/
|
||||
class ExposedFunktionaerRepository : FunktionaerRepository {
|
||||
|
||||
override suspend fun findById(id: Uuid): DomFunktionaer? = transaction {
|
||||
FunktionaerTable.selectAll().where { FunktionaerTable.id eq id.toJavaUuid() }
|
||||
.map { rowToFunktionaer(it) }
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByRichterNummer(richterNummer: String): DomFunktionaer? = transaction {
|
||||
FunktionaerTable.selectAll().where { FunktionaerTable.richterNummer eq richterNummer }
|
||||
.map { rowToFunktionaer(it) }
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByName(searchTerm: String, limit: Int): List<DomFunktionaer> = transaction {
|
||||
val pattern = "%$searchTerm%"
|
||||
FunktionaerTable.selectAll().where {
|
||||
(FunktionaerTable.nachname like pattern) or (FunktionaerTable.vorname like pattern)
|
||||
}.limit(limit).map { rowToFunktionaer(it) }
|
||||
}
|
||||
|
||||
override suspend fun findByRolle(rolle: FunktionaerRolleE, activeOnly: Boolean): List<DomFunktionaer> = transaction {
|
||||
FunktionaerTable.selectAll().where {
|
||||
(FunktionaerTable.rollen like "%${rolle.name}%").let {
|
||||
if (activeOnly) it and (FunktionaerTable.istAktiv eq true) else it
|
||||
}
|
||||
}.map { rowToFunktionaer(it) }
|
||||
}
|
||||
|
||||
override suspend fun findByRichterQualifikation(
|
||||
qualifikation: RichterQualifikationE,
|
||||
activeOnly: Boolean
|
||||
): List<DomFunktionaer> = transaction {
|
||||
FunktionaerTable.selectAll().where {
|
||||
(FunktionaerTable.richterQualifikation eq qualifikation.name).let {
|
||||
if (activeOnly) it and (FunktionaerTable.istAktiv eq true) else it
|
||||
}
|
||||
}.map { rowToFunktionaer(it) }
|
||||
}
|
||||
|
||||
override suspend fun findBySparte(sparte: SparteE, activeOnly: Boolean): List<DomFunktionaer> = transaction {
|
||||
FunktionaerTable.selectAll().where {
|
||||
(FunktionaerTable.qualifiziertFuerSparten like "%${sparte.name}%").let {
|
||||
if (activeOnly) it and (FunktionaerTable.istAktiv eq true) else it
|
||||
}
|
||||
}.map { rowToFunktionaer(it) }
|
||||
}
|
||||
|
||||
override suspend fun findByVereinsNummer(vereinsNummer: String, activeOnly: Boolean): List<DomFunktionaer> =
|
||||
transaction {
|
||||
FunktionaerTable.selectAll().where {
|
||||
(FunktionaerTable.vereinsNummer eq vereinsNummer).let {
|
||||
if (activeOnly) it and (FunktionaerTable.istAktiv eq true) else it
|
||||
}
|
||||
}.map { rowToFunktionaer(it) }
|
||||
}
|
||||
|
||||
override suspend fun findAllActive(limit: Int, offset: Int): List<DomFunktionaer> = transaction {
|
||||
FunktionaerTable.selectAll().where { FunktionaerTable.istAktiv eq true }
|
||||
.limit(limit).offset(offset.toLong())
|
||||
.map { rowToFunktionaer(it) }
|
||||
}
|
||||
|
||||
override suspend fun findAll(limit: Int, offset: Int): List<DomFunktionaer> = transaction {
|
||||
FunktionaerTable.selectAll()
|
||||
.limit(limit).offset(offset.toLong())
|
||||
.map { rowToFunktionaer(it) }
|
||||
}
|
||||
|
||||
override suspend fun save(funktionaer: DomFunktionaer): DomFunktionaer = transaction {
|
||||
val now = Clock.System.now()
|
||||
val updated = funktionaer.copy(updatedAt = now)
|
||||
val javaId = funktionaer.funktionaerId.toJavaUuid()
|
||||
val existing = FunktionaerTable.selectAll().where { FunktionaerTable.id eq javaId }.singleOrNull()
|
||||
if (existing != null) {
|
||||
FunktionaerTable.update({ FunktionaerTable.id eq javaId }) { funktionaerToStatement(it, updated) }
|
||||
} else {
|
||||
FunktionaerTable.insert {
|
||||
it[id] = javaId
|
||||
funktionaerToStatement(it, updated)
|
||||
}
|
||||
}
|
||||
updated
|
||||
}
|
||||
|
||||
override suspend fun delete(id: Uuid): Boolean = transaction {
|
||||
FunktionaerTable.deleteWhere { FunktionaerTable.id eq id.toJavaUuid() } > 0
|
||||
}
|
||||
|
||||
override suspend fun countActive(): Long = transaction {
|
||||
FunktionaerTable.selectAll().where { FunktionaerTable.istAktiv eq true }.count()
|
||||
}
|
||||
|
||||
override suspend fun countByRichterQualifikation(qualifikation: RichterQualifikationE, activeOnly: Boolean): Long =
|
||||
transaction {
|
||||
FunktionaerTable.selectAll().where {
|
||||
(FunktionaerTable.richterQualifikation eq qualifikation.name).let {
|
||||
if (activeOnly) it and (FunktionaerTable.istAktiv eq true) else it
|
||||
}
|
||||
}.count()
|
||||
}
|
||||
|
||||
override suspend fun existsByRichterNummer(richterNummer: String): Boolean = transaction {
|
||||
FunktionaerTable.selectAll().where { FunktionaerTable.richterNummer eq richterNummer }.count() > 0
|
||||
}
|
||||
|
||||
private fun rowToFunktionaer(row: ResultRow): DomFunktionaer {
|
||||
val rollen = try {
|
||||
Json.decodeFromString<List<FunktionaerRolleE>>(row[FunktionaerTable.rollen]).toSet()
|
||||
} catch (_: Exception) {
|
||||
emptySet()
|
||||
}
|
||||
|
||||
val sparten = try {
|
||||
Json.decodeFromString<List<SparteE>>(row[FunktionaerTable.qualifiziertFuerSparten]).toSet()
|
||||
} catch (_: Exception) {
|
||||
emptySet()
|
||||
}
|
||||
|
||||
return DomFunktionaer(
|
||||
funktionaerId = (row[FunktionaerTable.id] as UUID).toKotlinUuid(),
|
||||
richterNummer = row[FunktionaerTable.richterNummer],
|
||||
vorname = row[FunktionaerTable.vorname],
|
||||
nachname = row[FunktionaerTable.nachname],
|
||||
geburtsdatum = row[FunktionaerTable.geburtsdatum],
|
||||
rollen = rollen,
|
||||
richterQualifikation = row[FunktionaerTable.richterQualifikation]?.let {
|
||||
runCatching { RichterQualifikationE.valueOf(it) }.getOrNull()
|
||||
},
|
||||
qualifiziertFuerSparten = sparten,
|
||||
email = row[FunktionaerTable.email],
|
||||
telefon = row[FunktionaerTable.telefon],
|
||||
vereinsNummer = row[FunktionaerTable.vereinsNummer],
|
||||
istAktiv = row[FunktionaerTable.istAktiv],
|
||||
bemerkungen = row[FunktionaerTable.bemerkungen],
|
||||
datenQuelle = runCatching { DatenQuelleE.valueOf(row[FunktionaerTable.datenQuelle]) }.getOrDefault(DatenQuelleE.IMPORT_ZNS),
|
||||
createdAt = row[FunktionaerTable.createdAt],
|
||||
updatedAt = row[FunktionaerTable.updatedAt]
|
||||
)
|
||||
}
|
||||
|
||||
private fun funktionaerToStatement(stmt: UpdateBuilder<*>, f: DomFunktionaer) {
|
||||
stmt[FunktionaerTable.richterNummer] = f.richterNummer
|
||||
stmt[FunktionaerTable.vorname] = f.vorname
|
||||
stmt[FunktionaerTable.nachname] = f.nachname
|
||||
stmt[FunktionaerTable.geburtsdatum] = f.geburtsdatum
|
||||
stmt[FunktionaerTable.rollen] = Json.encodeToString(f.rollen.toList())
|
||||
stmt[FunktionaerTable.richterQualifikation] = f.richterQualifikation?.name
|
||||
stmt[FunktionaerTable.qualifiziertFuerSparten] = Json.encodeToString(f.qualifiziertFuerSparten.toList())
|
||||
stmt[FunktionaerTable.email] = f.email
|
||||
stmt[FunktionaerTable.telefon] = f.telefon
|
||||
stmt[FunktionaerTable.vereinsNummer] = f.vereinsNummer
|
||||
stmt[FunktionaerTable.istAktiv] = f.istAktiv
|
||||
stmt[FunktionaerTable.bemerkungen] = f.bemerkungen
|
||||
stmt[FunktionaerTable.datenQuelle] = f.datenQuelle.name
|
||||
stmt[FunktionaerTable.createdAt] = f.createdAt
|
||||
stmt[FunktionaerTable.updatedAt] = f.updatedAt
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package at.mocode.officials.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 Funktionäre (DomFunktionaer).
|
||||
*
|
||||
* Speichert alle Funktionärs-Daten inkl. Rollen (JSON), Richterqualifikation
|
||||
* und Sparten-Qualifikationen (JSON) gemäß ÖTO-Anforderungen.
|
||||
*/
|
||||
object FunktionaerTable : Table("funktionaere") {
|
||||
|
||||
val id = javaUUID("id").autoGenerate()
|
||||
override val primaryKey = PrimaryKey(id)
|
||||
|
||||
// Identifikation
|
||||
val richterNummer = varchar("richter_nummer", 50).nullable()
|
||||
|
||||
// Persönliche Daten
|
||||
val vorname = varchar("vorname", 100)
|
||||
val nachname = varchar("nachname", 100)
|
||||
val geburtsdatum = date("geburtsdatum").nullable()
|
||||
|
||||
// Rollen & Qualifikationen (als JSON-Arrays gespeichert)
|
||||
val rollen = text("rollen") // JSON array of FunktionaerRolleE
|
||||
val richterQualifikation = varchar("richter_qualifikation", 50).nullable()
|
||||
val qualifiziertFuerSparten = text("qualifiziert_fuer_sparten") // JSON array of SparteE
|
||||
|
||||
// Kontaktdaten
|
||||
val email = varchar("email", 255).nullable()
|
||||
val telefon = varchar("telefon", 50).nullable()
|
||||
val vereinsNummer = varchar("vereins_nummer", 20).nullable()
|
||||
|
||||
// Status & Verwaltung
|
||||
val istAktiv = bool("ist_aktiv").default(true)
|
||||
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(false, vereinsNummer)
|
||||
index(false, istAktiv)
|
||||
index(false, richterQualifikation)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user