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:
+126
@@ -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, G1–G3, 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())
|
||||
}
|
||||
+93
@@ -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
|
||||
}
|
||||
+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)
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
-- Migration V001: Create Funktionaere (Officials) table
|
||||
-- Speichert alle Funktionärs-Daten inkl. Rollen (JSON), Richterqualifikation
|
||||
-- und Sparten-Qualifikationen (JSON) gemäß ÖTO-Anforderungen.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS funktionaere
|
||||
(
|
||||
id
|
||||
UUID
|
||||
PRIMARY
|
||||
KEY
|
||||
DEFAULT
|
||||
gen_random_uuid
|
||||
(
|
||||
),
|
||||
richter_nummer VARCHAR
|
||||
(
|
||||
50
|
||||
),
|
||||
vorname VARCHAR
|
||||
(
|
||||
100
|
||||
) NOT NULL,
|
||||
nachname VARCHAR
|
||||
(
|
||||
100
|
||||
) NOT NULL,
|
||||
geburtsdatum DATE,
|
||||
rollen TEXT NOT NULL DEFAULT '[]',
|
||||
richter_qualifikation VARCHAR
|
||||
(
|
||||
50
|
||||
),
|
||||
qualifiziert_fuer_sparten TEXT NOT NULL DEFAULT '[]',
|
||||
email VARCHAR
|
||||
(
|
||||
255
|
||||
),
|
||||
telefon VARCHAR
|
||||
(
|
||||
50
|
||||
),
|
||||
vereins_nummer VARCHAR
|
||||
(
|
||||
20
|
||||
),
|
||||
ist_aktiv BOOLEAN NOT NULL DEFAULT true,
|
||||
bemerkungen TEXT,
|
||||
daten_quelle VARCHAR
|
||||
(
|
||||
50
|
||||
) NOT NULL DEFAULT 'MANUELL',
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Unique index für Richternummer (wenn gesetzt)
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uk_funktionaere_richter_nummer
|
||||
ON funktionaere(richter_nummer)
|
||||
WHERE richter_nummer IS NOT NULL;
|
||||
|
||||
-- Performance-Indizes
|
||||
CREATE INDEX IF NOT EXISTS idx_funktionaere_nachname ON funktionaere(nachname);
|
||||
CREATE INDEX IF NOT EXISTS idx_funktionaere_vorname ON funktionaere(vorname);
|
||||
CREATE INDEX IF NOT EXISTS idx_funktionaere_vereins_nummer ON funktionaere(vereins_nummer);
|
||||
CREATE INDEX IF NOT EXISTS idx_funktionaere_ist_aktiv ON funktionaere(ist_aktiv);
|
||||
CREATE INDEX IF NOT EXISTS idx_funktionaere_richter_qual ON funktionaere(richter_qualifikation);
|
||||
|
||||
-- Dokumentation
|
||||
COMMENT
|
||||
ON TABLE funktionaere IS 'Funktionäre (Richter, Parcoursbauer, TBA, etc.) gemäß ÖTO-Regelwerk';
|
||||
COMMENT
|
||||
ON COLUMN funktionaere.id IS 'Eindeutige interne ID (UUID)';
|
||||
COMMENT
|
||||
ON COLUMN funktionaere.richter_nummer IS 'Offizielle OEPS-Richternummer (eindeutig, optional)';
|
||||
COMMENT
|
||||
ON COLUMN funktionaere.rollen IS 'JSON-Array der Funktionärs-Rollen (FunktionaerRolleE)';
|
||||
COMMENT
|
||||
ON COLUMN funktionaere.richter_qualifikation IS 'Richter-Qualifikationsstufe (RichterQualifikationE): GA, G1, G2, G3, INTERNATIONAL';
|
||||
COMMENT
|
||||
ON COLUMN funktionaere.qualifiziert_fuer_sparten IS 'JSON-Array der Sparten-Qualifikationen (SparteE)';
|
||||
COMMENT
|
||||
ON COLUMN funktionaere.daten_quelle IS 'Datenherkunft: MANUELL, IMPORT_ZNS, IMPORT_FEI';
|
||||
Reference in New Issue
Block a user