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:
+136
@@ -0,0 +1,136 @@
|
||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.persons.domain.model
|
||||
|
||||
import at.mocode.core.domain.model.DatenQuelleE
|
||||
import at.mocode.core.domain.model.LizenzKlasseE
|
||||
import at.mocode.core.domain.model.SparteE
|
||||
import at.mocode.core.domain.serialization.KotlinInstantSerializer
|
||||
import at.mocode.core.domain.serialization.KotlinLocalDateSerializer
|
||||
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 model representing a rider (Reiter) in the actor-context.
|
||||
*
|
||||
* A rider is a specialization of a person with additional equestrian-specific
|
||||
* attributes such as license, start card, and competition eligibility.
|
||||
* Data is primarily sourced from the OEPS ZNS (LIZENZ01.DAT).
|
||||
*
|
||||
* Key rules (ÖTO):
|
||||
* - A rider requires an active Startkarte (annual fee paid) to compete nationally.
|
||||
* - LizenzKlasse determines which competition classes the rider may enter.
|
||||
* - Satznummer (6-digit) is the primary key for ZNS data exchange.
|
||||
* - Kopfnummer is NOT a unique identifier – it can change.
|
||||
*
|
||||
* @property reiterId Unique internal identifier (UUID).
|
||||
* @property personId Reference to the base DomPerson record (UUID).
|
||||
* @property satznummer 6-digit ZNS primary key for data exchange. Primary key for ZNS.
|
||||
* @property lizenzNummer OEPS license number (from ZNS LIZENZ01.DAT).
|
||||
* @property lizenzKlasse License class determining competition eligibility (e.g. R1, RD2).
|
||||
* @property lizenzSparten Disciplines for which the license is valid.
|
||||
* @property startkartAktiv Whether the annual start card fee has been paid.
|
||||
* @property startkartSaison Season year for which the start card is valid (e.g. 2026).
|
||||
* @property feiId FEI international rider ID (optional).
|
||||
* @property nation Nation code (e.g. AUT).
|
||||
* @property geburtsdatum Date of birth (for age class validation).
|
||||
* @property vereinsNummer Club number (OEPS).
|
||||
* @property vereinsName Club name.
|
||||
* @property istGastreiter Whether the rider is a guest rider (foreign nationality, not in Austrian club).
|
||||
* @property istAktiv Whether the rider is currently active in the system.
|
||||
* @property datenQuelle Source of the data.
|
||||
* @property createdAt Timestamp when this record was created.
|
||||
* @property updatedAt Timestamp when this record was last updated.
|
||||
*/
|
||||
@Serializable
|
||||
data class DomReiter(
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
val reiterId: Uuid = Uuid.random(),
|
||||
|
||||
// Reference to base person
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
val personId: Uuid,
|
||||
|
||||
// ZNS Identification
|
||||
val satznummer: String,
|
||||
val lizenzNummer: String? = null,
|
||||
|
||||
// License & Eligibility
|
||||
val lizenzKlasse: LizenzKlasseE = LizenzKlasseE.LIZENZFREI,
|
||||
val lizenzSparten: List<SparteE> = emptyList(),
|
||||
|
||||
// Start Card (Startkarte) – annual fee proof
|
||||
val startkartAktiv: Boolean = false,
|
||||
val startkartSaison: Int? = null,
|
||||
|
||||
// International
|
||||
val feiId: String? = null,
|
||||
val nation: String? = null,
|
||||
|
||||
// Personal Data (denormalized from DomPerson for performance)
|
||||
val nachname: String,
|
||||
val vorname: String,
|
||||
@Serializable(with = KotlinLocalDateSerializer::class)
|
||||
val geburtsdatum: LocalDate? = null,
|
||||
|
||||
// Club Affiliation
|
||||
val vereinsNummer: String? = null,
|
||||
val vereinsName: String? = null,
|
||||
|
||||
// Status
|
||||
val istGastreiter: Boolean = false,
|
||||
val istAktiv: Boolean = true,
|
||||
val datenQuelle: DatenQuelleE = DatenQuelleE.IMPORT_ZNS,
|
||||
|
||||
// Audit Fields
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
val createdAt: Instant = Clock.System.now(),
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
var updatedAt: Instant = Clock.System.now()
|
||||
) {
|
||||
/**
|
||||
* Returns the display name of the rider.
|
||||
*/
|
||||
fun getDisplayName(): String = "$vorname $nachname"
|
||||
|
||||
/**
|
||||
* Checks if the rider is eligible to compete nationally.
|
||||
* Requires an active start card (Startkarte).
|
||||
*/
|
||||
fun isStartberechtigt(): Boolean = istAktiv && startkartAktiv
|
||||
|
||||
/**
|
||||
* Checks if the rider holds a license for the given discipline.
|
||||
*/
|
||||
fun hasLizenzForSparte(sparte: SparteE): Boolean =
|
||||
lizenzKlasse == LizenzKlasseE.LIZENZFREI || lizenzSparten.contains(sparte)
|
||||
|
||||
/**
|
||||
* Validates the rider for competition entry.
|
||||
* Returns a list of warning messages (never hard errors – TBA has final say).
|
||||
*/
|
||||
fun validateForNennung(sparte: SparteE): List<String> {
|
||||
val warnings = mutableListOf<String>()
|
||||
|
||||
if (!istAktiv) {
|
||||
warnings.add("Reiter ${getDisplayName()} ist nicht aktiv")
|
||||
}
|
||||
if (!startkartAktiv) {
|
||||
warnings.add("Reiter ${getDisplayName()} hat keine aktive Startkarte für Saison $startkartSaison")
|
||||
}
|
||||
if (!hasLizenzForSparte(sparte)) {
|
||||
warnings.add("Reiter ${getDisplayName()} hat keine Lizenz für Sparte $sparte (Lizenzklasse: $lizenzKlasse)")
|
||||
}
|
||||
|
||||
return warnings
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a copy of this rider with an updated timestamp.
|
||||
*/
|
||||
fun withUpdatedTimestamp(): DomReiter = this.copy(updatedAt = Clock.System.now())
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.persons.domain.repository
|
||||
|
||||
import at.mocode.core.domain.model.LizenzKlasseE
|
||||
import at.mocode.core.domain.model.SparteE
|
||||
import at.mocode.persons.domain.model.DomReiter
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Repository-Interface für DomReiter (Reiter) Domain-Operationen.
|
||||
*
|
||||
* Definiert den Vertrag für Datenzugriffs-Operationen ohne Abhängigkeit
|
||||
* von konkreten Implementierungsdetails (Datenbank, etc.).
|
||||
*/
|
||||
interface ReiterRepository {
|
||||
|
||||
/**
|
||||
* Sucht einen Reiter anhand seiner eindeutigen ID.
|
||||
*/
|
||||
suspend fun findById(id: Uuid): DomReiter?
|
||||
|
||||
/**
|
||||
* Sucht einen Reiter anhand seiner Satznummer (OEPS-Mitgliedsnummer).
|
||||
*/
|
||||
suspend fun findBySatznummer(satznummer: String): DomReiter?
|
||||
|
||||
/**
|
||||
* Sucht einen Reiter anhand seiner FEI-ID.
|
||||
*/
|
||||
suspend fun findByFeiId(feiId: String): DomReiter?
|
||||
|
||||
/**
|
||||
* Sucht Reiter anhand von Vor- und/oder Nachname (Teilübereinstimmung).
|
||||
*/
|
||||
suspend fun findByName(searchTerm: String, limit: Int = 50): List<DomReiter>
|
||||
|
||||
/**
|
||||
* Sucht alle Reiter eines bestimmten Vereins.
|
||||
*/
|
||||
suspend fun findByVereinsNummer(vereinsNummer: String, activeOnly: Boolean = true): List<DomReiter>
|
||||
|
||||
/**
|
||||
* Sucht alle Reiter mit einer bestimmten Lizenzklasse.
|
||||
*/
|
||||
suspend fun findByLizenzKlasse(lizenzKlasse: LizenzKlasseE, activeOnly: Boolean = true): List<DomReiter>
|
||||
|
||||
/**
|
||||
* Sucht alle Reiter, die für eine bestimmte Sparte lizenziert sind.
|
||||
*/
|
||||
suspend fun findBySparte(sparte: SparteE, activeOnly: Boolean = true): List<DomReiter>
|
||||
|
||||
/**
|
||||
* Sucht alle Gastreiter.
|
||||
*/
|
||||
suspend fun findGastreiter(activeOnly: Boolean = true): List<DomReiter>
|
||||
|
||||
/**
|
||||
* Gibt alle aktiven Reiter zurück (paginiert).
|
||||
*/
|
||||
suspend fun findAllActive(limit: Int = 100, offset: Int = 0): List<DomReiter>
|
||||
|
||||
/**
|
||||
* Gibt alle Reiter zurück (paginiert).
|
||||
*/
|
||||
suspend fun findAll(limit: Int = 100, offset: Int = 0): List<DomReiter>
|
||||
|
||||
/**
|
||||
* Speichert einen Reiter (Insert oder Update).
|
||||
*/
|
||||
suspend fun save(reiter: DomReiter): DomReiter
|
||||
|
||||
/**
|
||||
* Löscht einen Reiter anhand seiner ID.
|
||||
*
|
||||
* @return true wenn gelöscht, false wenn nicht gefunden
|
||||
*/
|
||||
suspend fun delete(id: Uuid): Boolean
|
||||
|
||||
/**
|
||||
* Zählt alle aktiven Reiter.
|
||||
*/
|
||||
suspend fun countActive(): Long
|
||||
|
||||
/**
|
||||
* Prüft ob ein Reiter mit der gegebenen Satznummer bereits existiert.
|
||||
*/
|
||||
suspend fun existsBySatznummer(satznummer: String): Boolean
|
||||
}
|
||||
+174
@@ -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
|
||||
}
|
||||
}
|
||||
+55
@@ -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)
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
-- Migration V001: Create Reiter (Riders) table
|
||||
-- Speichert alle Reiter-Daten inkl. Lizenz, Sparten (JSON) und ZNS-Identifikation.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS reiter
|
||||
(
|
||||
id
|
||||
UUID
|
||||
PRIMARY
|
||||
KEY
|
||||
DEFAULT
|
||||
gen_random_uuid
|
||||
(
|
||||
),
|
||||
satznummer VARCHAR
|
||||
(
|
||||
20
|
||||
),
|
||||
fei_id VARCHAR
|
||||
(
|
||||
20
|
||||
),
|
||||
vorname VARCHAR
|
||||
(
|
||||
100
|
||||
) NOT NULL,
|
||||
nachname VARCHAR
|
||||
(
|
||||
100
|
||||
) NOT NULL,
|
||||
geburtsdatum DATE,
|
||||
nation VARCHAR
|
||||
(
|
||||
3
|
||||
) DEFAULT 'AUT',
|
||||
vereins_nummer VARCHAR
|
||||
(
|
||||
20
|
||||
),
|
||||
vereins_name VARCHAR
|
||||
(
|
||||
200
|
||||
),
|
||||
lizenz_klasse VARCHAR
|
||||
(
|
||||
50
|
||||
),
|
||||
lizenziert_fuer_sparten TEXT NOT NULL DEFAULT '[]',
|
||||
ist_aktiv BOOLEAN NOT NULL DEFAULT true,
|
||||
ist_gastreiter BOOLEAN NOT NULL DEFAULT false,
|
||||
bemerkungen TEXT,
|
||||
daten_quelle VARCHAR
|
||||
(
|
||||
50
|
||||
) NOT NULL DEFAULT 'IMPORT_ZNS',
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Unique Indizes für ZNS-Identifikation
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uk_reiter_satznummer ON reiter(satznummer) WHERE satznummer IS NOT NULL;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uk_reiter_fei_id ON reiter(fei_id) WHERE fei_id IS NOT NULL;
|
||||
|
||||
-- Performance-Indizes
|
||||
CREATE INDEX IF NOT EXISTS idx_reiter_nachname ON reiter(nachname);
|
||||
CREATE INDEX IF NOT EXISTS idx_reiter_vorname ON reiter(vorname);
|
||||
CREATE INDEX IF NOT EXISTS idx_reiter_vereins_nummer ON reiter(vereins_nummer);
|
||||
CREATE INDEX IF NOT EXISTS idx_reiter_ist_aktiv ON reiter(ist_aktiv);
|
||||
CREATE INDEX IF NOT EXISTS idx_reiter_lizenz_klasse ON reiter(lizenz_klasse);
|
||||
CREATE INDEX IF NOT EXISTS idx_reiter_ist_gastreiter ON reiter(ist_gastreiter);
|
||||
|
||||
-- Dokumentation
|
||||
COMMENT
|
||||
ON TABLE reiter IS 'Reiter/Teilnehmer gemäß OEPS-Mitgliederregister (ZNS)';
|
||||
COMMENT
|
||||
ON COLUMN reiter.id IS 'Eindeutige interne ID (UUID)';
|
||||
COMMENT
|
||||
ON COLUMN reiter.satznummer IS 'OEPS-Satznummer (Mitgliedsnummer, eindeutig)';
|
||||
COMMENT
|
||||
ON COLUMN reiter.fei_id IS 'FEI-ID für internationale Starts (eindeutig)';
|
||||
COMMENT
|
||||
ON COLUMN reiter.lizenz_klasse IS 'Lizenzklasse (LizenzKlasseE): LIZENZFREI, AMATEUR, PROFI, etc.';
|
||||
COMMENT
|
||||
ON COLUMN reiter.lizenziert_fuer_sparten IS 'JSON-Array der lizenzierten Sparten (SparteE)';
|
||||
COMMENT
|
||||
ON COLUMN reiter.ist_gastreiter IS 'Gastreiter ohne OEPS-Mitgliedschaft (z.B. ausländische Starter)';
|
||||
COMMENT
|
||||
ON COLUMN reiter.daten_quelle IS 'Datenherkunft: MANUELL, IMPORT_ZNS, IMPORT_FEI';
|
||||
Reference in New Issue
Block a user