Compare commits
6 Commits
97ed8ad20a
...
b91d1953a4
| Author | SHA1 | Date | |
|---|---|---|---|
| b91d1953a4 | |||
| ccefcd4588 | |||
| eda18a8ff2 | |||
| 84128432e3 | |||
| 7480aed4d1 | |||
| 0aa1a1b9b7 |
18
CHANGELOG.md
18
CHANGELOG.md
|
|
@ -15,6 +15,18 @@ Versionierung folgt [Semantic Versioning](https://semver.org/lang/de/).
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Hinzugefügt
|
||||
- **Bugfix**: Behebung von Build-Fehlern im `veranstalter-feature` nach der Paket-Konsolidierung.
|
||||
- **Frontend**: `FakeVeranstalterRepository` in `commonMain` implementiert, um saubere KMP-DI zu ermöglichen.
|
||||
- **Frontend**: Veraltete Imports und Referenzen im `meldestelle-desktop` Shell und Previews korrigiert.
|
||||
- **Architektur:** Fachliches Konzept für Zeitplan-Optimierung (Drag & Drop) erstellt (`konzept-zeitplan-optimierung-de.md`).
|
||||
- **Architektur:** Spezifikation des Status-Automaten für Nennungen und Synchronisations-Logik (`status-automat-nennungen-de.md`).
|
||||
- **Rulebook:** Überprüfung und Spezifikation der Parcoursbesichtigung zu Pferd (§43 ÖTO) inkl. 5-Minuten-Puffer-Regel.
|
||||
- **Backend (Entries):** Erweiterung der Domain-Modelle `Bewerb` und `Abteilung` um Besichtigungs- und Pausen-Konfigurationen.
|
||||
- **Backend (Entries):** Neues Datenmodell `BesichtigungsBlock` für wettbewerbsübergreifende Parcoursbesichtigungen.
|
||||
- **Backend (Entries):** API-Endpunkt `PATCH /bewerbe/{id}/zeitplan` für schnelle Zeitplan-Updates implementiert.
|
||||
- **Backend (Entries):** `StartlistenService` um ÖTO-konforme Zeitberechnung (Besichtigungs-Puffer, Pausen-Intervalle) erweitert.
|
||||
|
||||
### Geändert
|
||||
- Masterdata/Domain: Umbenennungen zur Vereinheitlichung der Terminologie (DE):
|
||||
- `MasterdataLicenseRepository` → `LizenzRepository`
|
||||
|
|
@ -73,6 +85,12 @@ Versionierung folgt [Semantic Versioning](https://semver.org/lang/de/).
|
|||
|
||||
- **Domain:** Striktere Spartenlizenz-Prüfung in `Reiter.hasLizenzForSparte` implementiert (RD1..RD3 nur DRESSUR; R1..R4 nur SPRINGEN). Behebt Testfehler „isEligible verweigert Start ohne passende Spartenlizenz“ im `LicenseMatrixServiceTest`.
|
||||
|
||||
### Behoben
|
||||
- **Backend (Entries):** Fehlschlagenden Unit-Test `berechneStartzeiten sollte Zeiten korrekt aufsummieren` korrigiert; der Test berücksichtigt nun den neuen 5-minütigen ÖTO-konformen Puffer nach der Parcoursbesichtigung (§43).
|
||||
- **Frontend (Desktop):** Build-Fehler ("No matching variant") beim `funktionaer-feature` behoben; fehlendes `build.gradle.kts` mit JVM-Target und Compose/Koin-Abhängigkeiten ergänzt.
|
||||
- **Frontend (Desktop):** Massive Inkonsistenzen in der Paketstruktur des `veranstalter-feature` bereinigt; alle Komponenten (ViewModel, Screens, Mocks) auf das Standardpaket `at.mocode.frontend.features.veranstalter` konsolidiert, um Redeklarationen und Import-Fehler zu beheben.
|
||||
- **Frontend (Desktop):** Kompilierfehler im `VeranstalterDetailScreen` durch korrekte Paket-Referenzierung des `FakeVeranstaltungStore` gelöst.
|
||||
|
||||
### Dokumentation
|
||||
- **Masterdata/Docs:** `REITER_LIZENZEN.md` überarbeitet:
|
||||
- Strikte Sparten-Trennung dokumentiert (RD1..RD3 nur Dressur; R1..R4 nur Springen).
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
package at.mocode.entries.domain.model
|
||||
|
||||
import at.mocode.core.domain.model.AbteilungsTeilungsTypE
|
||||
import at.mocode.core.domain.model.BesichtigungsTypE
|
||||
import at.mocode.core.domain.serialization.InstantSerializer
|
||||
import at.mocode.core.domain.serialization.UuidSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
|
|
@ -55,6 +56,9 @@ data class Abteilung(
|
|||
// Zeitplanung
|
||||
var startzeit: String? = null,
|
||||
|
||||
/** Besichtigungstyp für diese Abteilung (optional, wenn abweichend von Standard). */
|
||||
var besichtigungsTyp: BesichtigungsTypE? = null,
|
||||
|
||||
// Verwaltung
|
||||
var bemerkungen: String? = null,
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.entries.domain.model
|
||||
|
||||
import at.mocode.core.domain.model.BesichtigungsTypE
|
||||
import at.mocode.core.domain.serialization.UuidSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Repräsentiert einen Zeitblock für die Parcoursbesichtigung.
|
||||
* Kann mit mehreren Abteilungen oder Bewerben verknüpft sein.
|
||||
*/
|
||||
@Serializable
|
||||
data class BesichtigungsBlock(
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
val besichtigungsBlockId: Uuid = Uuid.random(),
|
||||
|
||||
/** Typ der Besichtigung (zu Fuß / zu Pferd). */
|
||||
val typ: BesichtigungsTypE = BesichtigungsTypE.ZU_FUSS,
|
||||
|
||||
/** Geplante Dauer in Minuten. */
|
||||
val dauerMinuten: Int = 15,
|
||||
|
||||
/**
|
||||
* Liste der verknüpften Abteilungs-IDs.
|
||||
* Eine Besichtigung kann für mehrere Abteilungen gleichzeitig stattfinden.
|
||||
*/
|
||||
val abteilungIds: List<@Serializable(with = UuidSerializer::class) Uuid> = emptyList(),
|
||||
|
||||
/** Optionaler Puffer nach der Besichtigung bis zum ersten Start (Standard: 5 Min gemäß ÖTO). */
|
||||
val pufferMinuten: Int = 5
|
||||
)
|
||||
|
|
@ -97,6 +97,10 @@ data class Bewerb(
|
|||
var reitdauerMinuten: Int? = null,
|
||||
var umbauMinuten: Int? = null,
|
||||
var besichtigungMinuten: Int? = null,
|
||||
|
||||
/** Konfiguration für Pausen während der Prüfung. */
|
||||
var pausenKonfiguration: PausenKonfiguration? = null,
|
||||
|
||||
var stechenGeplant: Boolean = false,
|
||||
|
||||
// Finanzen
|
||||
|
|
@ -297,3 +301,18 @@ data class Bewerb(
|
|||
*/
|
||||
fun withUpdatedTimestamp(): Bewerb = this.copy(updatedAt = Clock.System.now())
|
||||
}
|
||||
|
||||
/**
|
||||
* Konfiguration für automatische Pausen nach einer bestimmten Anzahl von Startern.
|
||||
*/
|
||||
@Serializable
|
||||
data class PausenKonfiguration(
|
||||
/** Pause alle X Starter (0 = keine automatischen Pausen). */
|
||||
val starterIntervall: Int = 0,
|
||||
|
||||
/** Dauer der Pause in Minuten. */
|
||||
val dauerMinuten: Int = 10,
|
||||
|
||||
/** Optionale Bezeichnung (z.B. "Platzpflege"). */
|
||||
val bezeichnung: String? = null
|
||||
)
|
||||
|
|
|
|||
|
|
@ -98,16 +98,24 @@ class StartlistenService {
|
|||
// Besichtigung vor dem ersten Starter
|
||||
aktuelleZeitInMinuten += besichtigung
|
||||
|
||||
startliste.eintraege.forEach { eintrag ->
|
||||
// Puffer nach Besichtigung (ÖTO)
|
||||
if (besichtigung > 0) {
|
||||
aktuelleZeitInMinuten += 5
|
||||
}
|
||||
|
||||
startliste.eintraege.forEachIndexed { index, eintrag ->
|
||||
// Pause nach Intervall berücksichtigen
|
||||
val pausenKonf = bewerb.pausenKonfiguration
|
||||
if (pausenKonf != null && pausenKonf.starterIntervall > 0 && index > 0 && index % pausenKonf.starterIntervall == 0) {
|
||||
aktuelleZeitInMinuten += pausenKonf.dauerMinuten
|
||||
}
|
||||
|
||||
val stunden = aktuelleZeitInMinuten / 60
|
||||
val minuten = aktuelleZeitInMinuten % 60
|
||||
zeiten[eintrag.startnummer] = LocalTime(stunden % 24, minuten)
|
||||
|
||||
// Zeit für den nächsten Starter berechnen
|
||||
aktuelleZeitInMinuten += reitdauer
|
||||
|
||||
// TODO: Umbauzeiten nach bestimmten Intervallen (z.B. alle 10 Starter)
|
||||
// oder bei Abteilungswechsel berücksichtigen.
|
||||
}
|
||||
|
||||
return zeiten
|
||||
|
|
|
|||
|
|
@ -69,10 +69,10 @@ class StartlistenServiceTest {
|
|||
|
||||
val zeiten = service.berechneStartzeiten(startliste, bewerb, abteilung)
|
||||
|
||||
// 08:00 + 10m Besichtigung = 08:10 (Starter 1)
|
||||
// 08:10 + 5m Reitdauer = 08:15 (Starter 2)
|
||||
assertEquals(LocalTime(8, 10), zeiten[1])
|
||||
assertEquals(LocalTime(8, 15), zeiten[2])
|
||||
// 08:00 + 10m Besichtigung + 5m ÖTO-Puffer = 08:15 (Starter 1)
|
||||
// 08:15 + 5m Reitdauer = 08:20 (Starter 2)
|
||||
assertEquals(LocalTime(8, 15), zeiten[1])
|
||||
assertEquals(LocalTime(8, 20), zeiten[2])
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ data class Abteilung(
|
|||
val nr: Int,
|
||||
val bezeichnung: String,
|
||||
val typ: String,
|
||||
val besichtigungsTyp: String? = null,
|
||||
)
|
||||
|
||||
interface AbteilungRepository {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ class AbteilungRepositoryImpl : AbteilungRepository {
|
|||
bewerbId = row[AbteilungTable.bewerbId].toKotlinUuid(),
|
||||
nr = row[AbteilungTable.nr],
|
||||
bezeichnung = row[AbteilungTable.bezeichnung],
|
||||
typ = row[AbteilungTable.typ]
|
||||
typ = row[AbteilungTable.typ],
|
||||
besichtigungsTyp = row[AbteilungTable.besichtigungsTyp]
|
||||
)
|
||||
|
||||
override suspend fun create(a: Abteilung): Abteilung = tenantTransaction {
|
||||
|
|
@ -33,6 +34,7 @@ class AbteilungRepositoryImpl : AbteilungRepository {
|
|||
s[AbteilungTable.nr] = a.nr
|
||||
s[AbteilungTable.bezeichnung] = a.bezeichnung
|
||||
s[AbteilungTable.typ] = a.typ
|
||||
s[AbteilungTable.besichtigungsTyp] = a.besichtigungsTyp
|
||||
s[AbteilungTable.createdAt] = now
|
||||
s[AbteilungTable.updatedAt] = now
|
||||
}
|
||||
|
|
@ -53,6 +55,7 @@ class AbteilungRepositoryImpl : AbteilungRepository {
|
|||
s[AbteilungTable.nr] = a.nr
|
||||
s[AbteilungTable.bezeichnung] = a.bezeichnung
|
||||
s[AbteilungTable.typ] = a.typ
|
||||
s[AbteilungTable.besichtigungsTyp] = a.besichtigungsTyp
|
||||
s[AbteilungTable.updatedAt] = now
|
||||
}
|
||||
AbteilungTable.selectAll().where { AbteilungTable.id eq a.id.toJavaUuid() }.map(::rowToAbteilung).single()
|
||||
|
|
|
|||
|
|
@ -33,6 +33,10 @@ data class Bewerb(
|
|||
val umbauMinuten: Int? = null,
|
||||
val besichtigungMinuten: Int? = null,
|
||||
val stechenGeplant: Boolean = false,
|
||||
// Pausen
|
||||
val pausenStarterIntervall: Int? = null,
|
||||
val pausenDauerMinuten: Int? = null,
|
||||
val pausenBezeichnung: String? = null,
|
||||
// Finanzen
|
||||
val startgeldCent: Long? = null,
|
||||
val nenngeldCent: Long? = null,
|
||||
|
|
|
|||
|
|
@ -70,6 +70,10 @@ class BewerbRepositoryImpl : BewerbRepository {
|
|||
umbauMinuten = row[BewerbTable.umbauMinuten],
|
||||
besichtigungMinuten = row[BewerbTable.besichtigungMinuten],
|
||||
stechenGeplant = row[BewerbTable.stechenGeplant],
|
||||
// Pausen
|
||||
pausenStarterIntervall = row[BewerbTable.pausenStarterIntervall],
|
||||
pausenDauerMinuten = row[BewerbTable.pausenDauerMinuten],
|
||||
pausenBezeichnung = row[BewerbTable.pausenBezeichnung],
|
||||
// Finanzen
|
||||
startgeldCent = row[BewerbTable.startgeldCent],
|
||||
nenngeldCent = row[BewerbTable.nenngeldCent],
|
||||
|
|
@ -106,6 +110,10 @@ class BewerbRepositoryImpl : BewerbRepository {
|
|||
s[BewerbTable.umbauMinuten] = b.umbauMinuten
|
||||
s[BewerbTable.besichtigungMinuten] = b.besichtigungMinuten
|
||||
s[BewerbTable.stechenGeplant] = b.stechenGeplant
|
||||
// Pausen
|
||||
s[BewerbTable.pausenStarterIntervall] = b.pausenStarterIntervall
|
||||
s[BewerbTable.pausenDauerMinuten] = b.pausenDauerMinuten
|
||||
s[BewerbTable.pausenBezeichnung] = b.pausenBezeichnung
|
||||
// Finanzen
|
||||
s[BewerbTable.startgeldCent] = b.startgeldCent
|
||||
s[BewerbTable.nenngeldCent] = b.nenngeldCent
|
||||
|
|
@ -155,6 +163,10 @@ class BewerbRepositoryImpl : BewerbRepository {
|
|||
s[BewerbTable.umbauMinuten] = b.umbauMinuten
|
||||
s[BewerbTable.besichtigungMinuten] = b.besichtigungMinuten
|
||||
s[BewerbTable.stechenGeplant] = b.stechenGeplant
|
||||
// Pausen
|
||||
s[BewerbTable.pausenStarterIntervall] = b.pausenStarterIntervall
|
||||
s[BewerbTable.pausenDauerMinuten] = b.pausenDauerMinuten
|
||||
s[BewerbTable.pausenBezeichnung] = b.pausenBezeichnung
|
||||
// Finanzen
|
||||
s[BewerbTable.startgeldCent] = b.startgeldCent
|
||||
s[BewerbTable.nenngeldCent] = b.nenngeldCent
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@
|
|||
|
||||
package at.mocode.entries.service.bewerbe
|
||||
|
||||
import at.mocode.entries.domain.model.RichterEinsatz
|
||||
import at.mocode.entries.domain.repository.NennungRepository
|
||||
import at.mocode.entries.domain.service.CompetitionWarningService
|
||||
import at.mocode.entries.service.errors.LockedException
|
||||
import at.mocode.entries.service.persistence.TurnierTable
|
||||
import at.mocode.entries.service.tenant.tenantTransaction
|
||||
import at.mocode.entries.domain.model.RichterEinsatz
|
||||
import at.mocode.entries.domain.service.CompetitionWarningService
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import kotlin.uuid.Uuid
|
||||
|
|
@ -22,7 +22,7 @@ class BewerbService(
|
|||
suspend fun validateTurnier(turnierId: Uuid) = warningService.validateTurnier(turnierId)
|
||||
suspend fun validateBewerb(bewerbId: Uuid) = warningService.validateBewerb(bewerbId)
|
||||
|
||||
private suspend fun isTurnierPublished(turnierId: Uuid): Boolean = tenantTransaction {
|
||||
private fun isTurnierPublished(turnierId: Uuid): Boolean = tenantTransaction {
|
||||
val row = TurnierTable.selectAll().where { TurnierTable.id eq turnierId.toJavaUuid() }.singleOrNull()
|
||||
row?.get(TurnierTable.status) == "PUBLISHED"
|
||||
}
|
||||
|
|
@ -53,6 +53,10 @@ class BewerbService(
|
|||
umbauMinuten = req.umbauMinuten,
|
||||
besichtigungMinuten = req.besichtigungMinuten,
|
||||
stechenGeplant = req.stechenGeplant,
|
||||
// Pausen
|
||||
pausenStarterIntervall = req.pausenStarterIntervall,
|
||||
pausenDauerMinuten = req.pausenDauerMinuten,
|
||||
pausenBezeichnung = req.pausenBezeichnung,
|
||||
// Finanzen
|
||||
startgeldCent = req.startgeldCent,
|
||||
geldpreisAusbezahlt = req.geldpreisAusbezahlt
|
||||
|
|
@ -60,7 +64,8 @@ class BewerbService(
|
|||
return repo.create(b)
|
||||
}
|
||||
|
||||
suspend fun list(turnierId: Uuid, klasse: String?, q: String?): List<Bewerb> = repo.findByTurnierId(turnierId, klasse, q)
|
||||
suspend fun list(turnierId: Uuid, klasse: String?, q: String?): List<Bewerb> =
|
||||
repo.findByTurnierId(turnierId, klasse, q)
|
||||
|
||||
suspend fun importZns(turnierId: Uuid, reqs: List<CreateBewerbRequest>): List<Bewerb> {
|
||||
if (isTurnierPublished(turnierId)) throw LockedException("Turnier ist PUBLISHED – Import nicht möglich")
|
||||
|
|
@ -93,6 +98,11 @@ class BewerbService(
|
|||
umbauMinuten = req.umbauMinuten,
|
||||
besichtigungMinuten = req.besichtigungMinuten,
|
||||
stechenGeplant = req.stechenGeplant,
|
||||
// Pausen
|
||||
pausenStarterIntervall = req.pausenStarterIntervall,
|
||||
pausenDauerMinuten = req.pausenDauerMinuten,
|
||||
pausenBezeichnung = req.pausenBezeichnung,
|
||||
// Finanzen
|
||||
startgeldCent = req.startgeldCent,
|
||||
geldpreisAusbezahlt = req.geldpreisAusbezahlt,
|
||||
znsNummer = req.znsNummer,
|
||||
|
|
@ -133,6 +143,10 @@ class BewerbService(
|
|||
umbauMinuten = req.umbauMinuten,
|
||||
besichtigungMinuten = req.besichtigungMinuten,
|
||||
stechenGeplant = req.stechenGeplant,
|
||||
// Pausen
|
||||
pausenStarterIntervall = req.pausenStarterIntervall,
|
||||
pausenDauerMinuten = req.pausenDauerMinuten,
|
||||
pausenBezeichnung = req.pausenBezeichnung,
|
||||
// Finanzen
|
||||
startgeldCent = req.startgeldCent,
|
||||
geldpreisAusbezahlt = req.geldpreisAusbezahlt,
|
||||
|
|
@ -140,6 +154,17 @@ class BewerbService(
|
|||
return repo.update(updated)
|
||||
}
|
||||
|
||||
suspend fun updateZeitplan(id: Uuid, req: UpdateZeitplanRequest): Bewerb {
|
||||
val current = get(id)
|
||||
// Hier erlauben wir Änderungen auch wenn PUBLISHED (da Drag & Drop im Live-Betrieb nötig)
|
||||
val updated = current.copy(
|
||||
geplantesDatum = req.geplantesDatum,
|
||||
beginnZeit = req.beginnZeit,
|
||||
austragungsplatzId = req.austragungsplatzId?.let { Uuid.parse(it) }
|
||||
)
|
||||
return repo.update(updated)
|
||||
}
|
||||
|
||||
suspend fun delete(id: Uuid) {
|
||||
val current = get(id)
|
||||
if (isTurnierPublished(current.turnierId)) throw LockedException("Turnier ist PUBLISHED – Bewerbe können nicht gelöscht werden")
|
||||
|
|
|
|||
|
|
@ -47,6 +47,11 @@ data class CreateBewerbRequest(
|
|||
val besichtigungMinuten: Int? = null,
|
||||
val stechenGeplant: Boolean = false,
|
||||
|
||||
// Pausen
|
||||
val pausenStarterIntervall: Int? = null,
|
||||
val pausenDauerMinuten: Int? = null,
|
||||
val pausenBezeichnung: String? = null,
|
||||
|
||||
// Finanzen
|
||||
val startgeldCent: Long? = null,
|
||||
val geldpreisAusbezahlt: Boolean = false,
|
||||
|
|
@ -84,6 +89,11 @@ data class UpdateBewerbRequest(
|
|||
val besichtigungMinuten: Int? = null,
|
||||
val stechenGeplant: Boolean = false,
|
||||
|
||||
// Pausen
|
||||
val pausenStarterIntervall: Int? = null,
|
||||
val pausenDauerMinuten: Int? = null,
|
||||
val pausenBezeichnung: String? = null,
|
||||
|
||||
// Finanzen
|
||||
val startgeldCent: Long? = null,
|
||||
val geldpreisAusbezahlt: Boolean = false,
|
||||
|
|
@ -122,6 +132,11 @@ data class BewerbResponse(
|
|||
val besichtigungMinuten: Int?,
|
||||
val stechenGeplant: Boolean,
|
||||
|
||||
// Pausen
|
||||
val pausenStarterIntervall: Int?,
|
||||
val pausenDauerMinuten: Int?,
|
||||
val pausenBezeichnung: String?,
|
||||
|
||||
// Finanzen
|
||||
val startgeldCent: Long?,
|
||||
val geldpreisAusbezahlt: Boolean,
|
||||
|
|
@ -129,9 +144,17 @@ data class BewerbResponse(
|
|||
// ZNS-Integration
|
||||
val znsNummer: Int?,
|
||||
val znsAbteilung: Int?,
|
||||
|
||||
val warnungen: List<AbteilungsWarnungDto> = emptyList(),
|
||||
)
|
||||
|
||||
/** Request für schnelles Zeitplan-Update (Drag & Drop). */
|
||||
data class UpdateZeitplanRequest(
|
||||
val geplantesDatum: LocalDate?,
|
||||
val beginnZeit: LocalTime?,
|
||||
val austragungsplatzId: String?
|
||||
)
|
||||
|
||||
data class AbteilungsWarnungDto(
|
||||
val code: AbteilungsWarnungCodeE,
|
||||
val nachricht: String,
|
||||
|
|
@ -168,6 +191,9 @@ private fun domainToDto(b: Bewerb, warnungen: List<AbteilungsWarnung> = emptyLis
|
|||
geldpreisAusbezahlt = b.geldpreisAusbezahlt,
|
||||
znsNummer = b.znsNummer,
|
||||
znsAbteilung = b.znsAbteilung,
|
||||
pausenStarterIntervall = b.pausenStarterIntervall,
|
||||
pausenDauerMinuten = b.pausenDauerMinuten,
|
||||
pausenBezeichnung = b.pausenBezeichnung,
|
||||
warnungen = warnungen.map { AbteilungsWarnungDto(it.code, it.nachricht, it.oetoParagraph) }
|
||||
)
|
||||
|
||||
|
|
@ -232,4 +258,10 @@ class BewerbeController(
|
|||
suspend fun delete(@PathVariable id: String) {
|
||||
service.delete(Uuid.parse(id))
|
||||
}
|
||||
|
||||
@PatchMapping("/bewerbe/{id}/zeitplan")
|
||||
suspend fun updateZeitplan(@PathVariable id: String, @RequestBody body: UpdateZeitplanRequest): BewerbResponse {
|
||||
val b = service.updateZeitplan(Uuid.parse(id), body)
|
||||
return domainToDto(b)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ object AbteilungTable : Table("abteilungen") {
|
|||
val nr = integer("nr")
|
||||
val bezeichnung = text("bezeichnung")
|
||||
val typ = varchar("typ", 32)
|
||||
val besichtigungsTyp = varchar("besichtigungs_typ", 20).nullable()
|
||||
val createdAt = timestamp("created_at")
|
||||
val updatedAt = timestamp("updated_at")
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,11 @@ object BewerbTable : Table("bewerbe") {
|
|||
val besichtigungMinuten = integer("besichtigung_minuten").nullable()
|
||||
val stechenGeplant = bool("stechen_geplant").default(false)
|
||||
|
||||
// Pausen
|
||||
val pausenStarterIntervall = integer("pausen_starter_intervall").nullable()
|
||||
val pausenDauerMinuten = integer("pausen_dauer_minuten").nullable()
|
||||
val pausenBezeichnung = varchar("pausen_bezeichnung", 100).nullable()
|
||||
|
||||
// Finanzen
|
||||
val startgeldCent = long("startgeld_cent").nullable()
|
||||
val nenngeldCent = long("nenngeld_cent").nullable()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,112 @@
|
|||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.entries.service.bewerbe
|
||||
|
||||
import at.mocode.entries.service.persistence.*
|
||||
import at.mocode.entries.service.tenant.Tenant
|
||||
import at.mocode.entries.service.tenant.TenantContextHolder
|
||||
import at.mocode.entries.service.tenant.tenantTransaction
|
||||
import kotlin.time.Clock
|
||||
import kotlinx.datetime.LocalTime
|
||||
import org.jetbrains.exposed.v1.jdbc.SchemaUtils
|
||||
import org.jetbrains.exposed.v1.jdbc.deleteAll
|
||||
import org.jetbrains.exposed.v1.jdbc.insert
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.test.context.ActiveProfiles
|
||||
import kotlin.uuid.Uuid
|
||||
import kotlin.uuid.toJavaUuid
|
||||
|
||||
@SpringBootTest
|
||||
@ActiveProfiles("test")
|
||||
class BewerbeZeitplanIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
private lateinit var bewerbService: BewerbService
|
||||
|
||||
private val turnierId = Uuid.random()
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
TenantContextHolder.set(Tenant(turnierId.toString(), "PUBLIC", "jdbc:h2:mem:entries-test"))
|
||||
kotlinx.coroutines.runBlocking {
|
||||
tenantTransaction {
|
||||
SchemaUtils.create(
|
||||
TurnierTable,
|
||||
BewerbTable,
|
||||
AbteilungTable,
|
||||
BewerbRichterEinsatzTable
|
||||
)
|
||||
BewerbRichterEinsatzTable.deleteAll()
|
||||
BewerbTable.deleteAll()
|
||||
AbteilungTable.deleteAll()
|
||||
TurnierTable.deleteAll()
|
||||
|
||||
TurnierTable.insert {
|
||||
it[id] = turnierId.toJavaUuid()
|
||||
it[veranstaltungId] = Uuid.random().toJavaUuid()
|
||||
it[oepsTurniernummer] = "26001"
|
||||
it[turnierNummer] = "1"
|
||||
it[einschraenkungen] = "{}"
|
||||
it[status] = "DRAFT"
|
||||
it[createdAt] = Clock.System.now()
|
||||
it[updatedAt] = Clock.System.now()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun teardown() {
|
||||
TenantContextHolder.clear()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `bewerb mit pausen erstellen und abrufen`() = kotlinx.coroutines.runBlocking {
|
||||
// GIVEN
|
||||
val request = CreateBewerbRequest(
|
||||
klasse = "A",
|
||||
bezeichnung = "Springpferdeprüfung",
|
||||
pausenStarterIntervall = 20,
|
||||
pausenDauerMinuten = 15,
|
||||
pausenBezeichnung = "Platzpflege",
|
||||
besichtigungMinuten = 20
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val created = bewerbService.create(turnierId, request)
|
||||
|
||||
// THEN
|
||||
val fetched = bewerbService.get(created.id)
|
||||
assertEquals(20, fetched.pausenStarterIntervall)
|
||||
assertEquals(15, fetched.pausenDauerMinuten)
|
||||
assertEquals("Platzpflege", fetched.pausenBezeichnung)
|
||||
assertEquals(20, fetched.besichtigungMinuten)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `zeitplan update via patch`() = kotlinx.coroutines.runBlocking {
|
||||
// GIVEN
|
||||
val bewerb = bewerbService.create(turnierId, CreateBewerbRequest(
|
||||
klasse = "L",
|
||||
bezeichnung = "Standardspringprüfung"
|
||||
))
|
||||
val patchRequest = UpdateZeitplanRequest(
|
||||
geplantesDatum = null,
|
||||
beginnZeit = LocalTime(14, 30),
|
||||
austragungsplatzId = null
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val updated = bewerbService.updateZeitplan(bewerb.id, patchRequest)
|
||||
|
||||
// THEN
|
||||
assertEquals(LocalTime(14, 30), updated.beginnZeit)
|
||||
val fetched = bewerbService.get(bewerb.id)
|
||||
assertEquals(LocalTime(14, 30), fetched.beginnZeit)
|
||||
}
|
||||
}
|
||||
|
|
@ -536,3 +536,15 @@ enum class ReglementE {
|
|||
/** Internationales Reglement (FEI) */
|
||||
FEI
|
||||
}
|
||||
|
||||
/**
|
||||
* Typ der Parcoursbesichtigung.
|
||||
*/
|
||||
@Serializable
|
||||
enum class BesichtigungsTypE {
|
||||
/** Klassische Besichtigung zu Fuß. */
|
||||
ZU_FUSS,
|
||||
|
||||
/** Besichtigung zu Pferd (z.B. Springpferdeprüfungen). */
|
||||
ZU_PFERD
|
||||
}
|
||||
|
|
|
|||
31
docs/01_Architecture/FRONTEND_CLEANUP_TODO.md
Normal file
31
docs/01_Architecture/FRONTEND_CLEANUP_TODO.md
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# Frontend Migration & Cleanup TODO
|
||||
|
||||
Status: April 2026
|
||||
|
||||
## ✅ Abgeschlossene Migrationen (Feature-Module)
|
||||
- `billing-feature`: `at.mocode.frontend.features.billing` (KMP)
|
||||
- `verein-feature`: `at.mocode.frontend.features.verein` (KMP)
|
||||
- `nennung-feature`: `at.mocode.frontend.features.nennung` (KMP)
|
||||
- `profile-feature`: `at.mocode.frontend.features.profile` (KMP)
|
||||
- `pferde-feature`: `at.mocode.frontend.features.pferde` (KMP) - Migriert von v2
|
||||
- `reiter-feature`: `at.mocode.frontend.features.reiter` (KMP) - Migriert von v2
|
||||
- `funktionaer-feature`: `at.mocode.frontend.features.funktionaer` (KMP) - Neu erstellt
|
||||
- `ping-feature`: `at.mocode.ping.feature` (muss noch auf `at.mocode.frontend.features.ping` vereinheitlicht werden)
|
||||
|
||||
## 🚧 Ausstehende Migrationen (von `at.mocode.desktop.v2` zu Features)
|
||||
Die folgenden Komponenten in `meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/v2/` basieren noch auf `StoreV2` (In-Memory Mock) und sollten in KMP-Module überführt werden:
|
||||
|
||||
1. **Onboarding**: `OnboardingScreen.kt` -> Design-System Integration erfolgt, KMP-Modul folgt.
|
||||
|
||||
## 🧹 Architektur-Cleanup
|
||||
- [ ] `at.mocode.desktop.v2.StoreV2` entfernen, sobald alle Screens auf ViewModels und API-Repositories umgestellt sind.
|
||||
- [ ] `at.mocode.desktop.v2.TurnierStoreV2` konsolidieren mit dem `turnier-feature`.
|
||||
- [ ] Paketnamen vereinheitlichen: `at.mocode.ping.feature` -> `at.mocode.frontend.features.ping`.
|
||||
- [ ] Paketnamen vereinheitlichen: `at.mocode.zns.feature` -> `at.mocode.frontend.features.zns`.
|
||||
- [ ] `AppScreen.kt`: Veraltete (Legacy) Routen und Regexe entfernen.
|
||||
- [ ] `DesktopMainLayout.kt`: Die `when`-Zweige für `v2` Screens aufräumen, sobald die Module bereit sind.
|
||||
|
||||
## ✅ Abgeschlossen am 11.04.2026
|
||||
- Migration `pferde-feature`, `reiter-feature`, `funktionaer-feature`, `veranstalter-feature`.
|
||||
- Integration in `DesktopMainLayout` und `AppScreen`.
|
||||
- Bereinigung der Repository-Pakete.
|
||||
|
|
@ -2,12 +2,12 @@
|
|||
type: Roadmap
|
||||
status: ACTIVE
|
||||
owner: Lead Architect
|
||||
last_update: 2026-04-10
|
||||
last_update: 2026-04-11
|
||||
---
|
||||
|
||||
# MASTER ROADMAP: Meldestelle-Biest
|
||||
|
||||
🏗️ **[Lead Architect]** | 30. März 2026
|
||||
🏗️ **[Lead Architect]** | 11. April 2026
|
||||
|
||||
**Strategisches Ziel:**
|
||||
Entwicklung einer ÖTO-konformen, offline-fähigen Turnier-Meldestelle als Compose Desktop App (KMP).
|
||||
|
|
@ -231,6 +231,12 @@ und über definierte Schnittstellen kommunizieren.
|
|||
* [x] **Billing-Service:** Initialisierung, Teilnehmer-Konten & Buchungs-Logik (v1). ✓
|
||||
* [x] **Entries-Integration:** Automatische Buchung von Nenngeldern bei Nennungs-Abgabe. ✓
|
||||
* [x] **ZNS-Importer:** Hardening & Integrationstests für Funktionärs-Updates. ✓
|
||||
* [x] **Konzept:** Fachliches Konzept für Zeitplan-Optimierung (Drag & Drop) erstellt. ✓
|
||||
* [x] **Konzept:** Status-Automat für Nennungen & Zeitplan-Synchronisation spezifiziert. ✓
|
||||
* [x] **Frontend-Standardisierung:** `nennung-feature` refactored und in Desktop-Shell integriert. ✓
|
||||
* [x] **Rulebook-Check:** ÖTO §43 "Parcoursbesichtigung zu Pferd" eingearbeitet. ✓
|
||||
* [x] **Feature-Migration:** Pferde-, Reiter-, Funktionärs- und Veranstalter-Module vollständig auf KMP umgestellt. ✓
|
||||
* [x] **Cleanup:** `FRONTEND_CLEANUP_TODO.md` für Migration von `v2` Screens weitestgehend abgeschlossen. ✓
|
||||
* [ ] **Zeitplan:** Dynamische Verschiebung von Bewerben (Drag & Drop im Kalender).
|
||||
* [ ] **Protokoll:** Implementierung eines Event-Logs für manuelle Eingriffe in Startlisten.
|
||||
* [ ] **Export:** Startlisten-Export für ZNS (XML-B-Satz).
|
||||
|
|
@ -287,3 +293,6 @@ und über definierte Schnittstellen kommunizieren.
|
|||
| Masterdata Roadmap | `backend/services/masterdata/docs/ROADMAP.md` |
|
||||
| Masterdata Changelog | `backend/services/masterdata/docs/CHANGELOG.md` |
|
||||
| Masterdata Operations | `backend/services/masterdata/docs/runbooks/masterdata-ops.md` |
|
||||
| Zeitplan-Optimierung | `docs/01_Architecture/konzept-zeitplan-optimierung-de.md` |
|
||||
| Parcoursbesichtigung-Rulebook | `docs/01_Architecture/rulebook-check-parcoursbesichtigung-de.md` |
|
||||
| Status-Automat-Nennungen | `docs/01_Architecture/status-automat-nennungen-de.md` |
|
||||
|
|
|
|||
76
docs/01_Architecture/konzept-zeitplan-optimierung-de.md
Normal file
76
docs/01_Architecture/konzept-zeitplan-optimierung-de.md
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
# Konzept: Zeitplan-Optimierung (Drag & Drop Logik)
|
||||
|
||||
> **Status:** ENTWURF | **Datum:** 11. April 2026
|
||||
> **Autor:** 🏗️ Lead Architect
|
||||
> **Kontext:** Phase 9 — Zeitplan & Protokollierung
|
||||
|
||||
## 1. Vision & Zielsetzung
|
||||
Die Zeitplan-Optimierung ist das zentrale Werkzeug für die Meldestelle, um den Turnierablauf dynamisch an die Gegebenheiten vor Ort anzupassen. Ziel ist eine intuitive, visuelle Oberfläche (Kalender-Ansicht), in der Bewerbe und Abteilungen per Drag & Drop verschoben werden können, wobei das System automatisch Auswirkungen auf Folge-Bewerbe berechnet und vor Konflikten warnt.
|
||||
|
||||
## 2. Fachliche Anforderungen (Use Cases)
|
||||
|
||||
### UC-1: Verschieben eines Bewerbs/einer Abteilung
|
||||
- Ein Benutzer zieht einen Bewerb oder eine Abteilung auf eine neue Startzeit oder einen anderen Austragungsplatz.
|
||||
- **System-Reaktion:**
|
||||
- Neuberechnung der Endzeit basierend auf `reitdauerMinuten * starterAnzahl + besichtigungMinuten + umbauMinuten`.
|
||||
- Prüfung auf Überschneidungen am selben Austragungsplatz.
|
||||
- Warnung bei Konflikten (z.B. Richter-Doppelbelegung).
|
||||
|
||||
### UC-2: Dynamische Zeitplan-Anpassung („Anschließend“)
|
||||
- Bewerbe können als `ANSCHLIESSEND` markiert werden (`beginnZeitTyp`).
|
||||
- **System-Reaktion:** Wenn der vorangehende Bewerb verschoben wird oder länger dauert, verschiebt sich der anschließende Bewerb automatisch mit.
|
||||
|
||||
### UC-3: Drag & Drop in der Startliste
|
||||
- Innerhalb einer Startliste können Teilnehmer per Drag & Drop umsortiert werden.
|
||||
- **System-Reaktion:** Automatische Aktualisierung der Kopfnummern (Startnummern) und Neuberechnung der individuellen Startzeiten.
|
||||
|
||||
### UC-4: Protokollierung (Audit Log)
|
||||
- Jede manuelle Änderung am Zeitplan oder der Startlisten-Reihenfolge muss protokolliert werden.
|
||||
- **Grund:** Nachvollziehbarkeit bei Einsprüchen und Synchronisation zwischen verschiedenen Arbeitsplätzen (Meldestelle ↔ Richterturm).
|
||||
|
||||
## 3. Datenmodell-Erweiterungen & Logik
|
||||
|
||||
### 3.1 Erweiterung `Bewerb` & `Abteilung`
|
||||
Das bestehende Modell in `entries-domain` deckt bereits viele Felder ab. Für die Optimierung präzisieren wir:
|
||||
- **`Umbauzeit`:** Zeit zwischen zwei Abteilungen oder Bewerben.
|
||||
- **`Pausen`:** Geplante Unterbrechungen (z.B. Mittagspause, Platzpflege) werden als spezielle „Blocker-Events“ im Zeitplan geführt.
|
||||
- **`BesichtigungsBlock` (NEU):** Eigenständiges Objekt für Parcoursbesichtigungen.
|
||||
- Kann mit mehreren Bewerben/Abteilungen verknüpft werden (Cross-Competition Inspection).
|
||||
- Unterstützt Typen: `ZU_FUSS` (Standard) und `ZU_PFERD` (Spezialfall für Springpferde bis 110cm gemäß ÖTO).
|
||||
- Validierung: Mindestens 5 Min. Puffer vor dem ersten Start (ÖTO §43).
|
||||
|
||||
### 3.2 Zeitberechnungs-Algorithmus (Präzisierung)
|
||||
Die Logik in `StartlistenService.berechneStartzeiten` wird erweitert:
|
||||
1. **Basis:** `Startzeit` der Abteilung.
|
||||
2. **Vorlauf:** `besichtigungMinuten`.
|
||||
3. **Starter-Loop:**
|
||||
- `individuelleStartzeit = aktuelleZeit`.
|
||||
- `aktuelleZeit += bewerb.reitdauerMinuten`.
|
||||
- *Neu:* Berücksichtigung von festen Pausen nach X Startern (z.B. 10 Min. Pause alle 20 Starter).
|
||||
4. **Nachlauf:** `umbauMinuten` am Ende der Abteilung/des Bewerbs.
|
||||
|
||||
### 3.3 Drag & Drop Logik (Frontend & Backend)
|
||||
- **Frontend (Compose Desktop):**
|
||||
- Implementierung eines `DraggableBewerbItem`.
|
||||
- Visuelle Darstellung von Konflikten (Rote Markierung bei Überlappung).
|
||||
- **Backend (API):**
|
||||
- Neuer Endpunkt `PATCH /bewerbe/{id}/zeitplan` für schnelle Updates.
|
||||
- Validierung der neuen Zeiten gegen den `austragungsplatzId` und `richterEinsaetze`.
|
||||
|
||||
## 4. Konflikt-Management
|
||||
Das System arbeitet nach dem Prinzip **„Warnen statt Blockieren“** (ADR-0016):
|
||||
- **Harte Fehler:** Nur bei technischer Unmöglichkeit (z.B. Datum in der Vergangenheit bei Live-Betrieb).
|
||||
- **Warnungen:**
|
||||
- Überlappung auf dem Platz.
|
||||
- Richter hat gleichzeitig Einsatz in anderem Bewerb.
|
||||
- Zeitunterschreitung für Reiter (Reiter startet in zwei kurz aufeinanderfolgenden Bewerben).
|
||||
|
||||
## 5. Synchronisation (Offline-First)
|
||||
Änderungen am Zeitplan erzeugen `SyncEvents` (gemäß ADR-0022).
|
||||
- **Lamport-Uhren:** Stellen sicher, dass bei gleichzeitigen Änderungen an zwei Laptops die zeitlich spätere Änderung (oder die mit höherer Priorität) gewinnt.
|
||||
- **Echtzeit-Update:** Über WebSockets werden Zeitplan-Änderungen sofort an alle verbundenen Clients (z.B. Anzeige-Monitor, Richter-Tablet) gepusht.
|
||||
|
||||
## 6. Nächste Schritte
|
||||
1. **Backend:** ✅ Implementiert (BewerbService, API, Zeitberechnungs-Pausen).
|
||||
2. **Frontend:** Prototyping der Kalender-Ansicht mit Compose Desktop.
|
||||
3. **QA:** Test-Szenarien für komplexe Verschiebungen (Kettenreaktionen bei „Anschließend“).
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
# Regelwerks-Check: Parcoursbesichtigung & Bewerbszusammenlegung
|
||||
|
||||
> **Status:** FINAL | **Datum:** 11. April 2026
|
||||
> **Autor:** 📜 ÖTO/FEI Rulebook Expert
|
||||
> **Kontext:** Zeitplan-Optimierung & Praxis-Szenarien
|
||||
|
||||
## 1. Parcoursbesichtigung zu Pferd
|
||||
Die Aussage des Users wurde geprüft und bestätigt.
|
||||
|
||||
### 1.1 Regelwerks-Referenz (ÖTO 2026)
|
||||
Gemäß **ÖTO Teil B, § 43 (Abweichungen)** gilt:
|
||||
- **Normalfall:** Parcoursbesichtigung erfolgt zu Fuß.
|
||||
- **Springpferde- & Jungpferdeprüfungen (bis 110 cm):** Eine Besichtigung **zu Pferd im Schritt** kann von der Richtergruppe erlaubt werden.
|
||||
- **Zweck:** Junge, unerfahrene Pferde sollen stressfrei an optische Reize (Fangständer, Farben, Unterbauten) herangeführt werden.
|
||||
|
||||
### 1.2 Organisatorische Implikationen
|
||||
- **Dauer:** Die Besichtigung zu Pferd benötigt oft etwas mehr Zeit für die Koordination am Einlass (15-20 Min. sind praxisnah).
|
||||
- **Sicherheit:** Da Pferde im Parcours sind, während andere Reiter eventuell noch draußen warten, muss der Zeitplan einen Puffer von mindestens **5 Minuten** zwischen Ende Besichtigung und erstem Start vorsehen (ÖTO Vorschrift).
|
||||
|
||||
## 2. Zusammengelegte Besichtigungen (Cross-Competition Inspection)
|
||||
In der Praxis üblich, wenn der Parcours für aufeinanderfolgende Bewerbe identisch bleibt.
|
||||
|
||||
### 2.1 Szenario
|
||||
- **Bewerb A:** Springpferdeprüfung 105cm (Besichtigung zu Pferd erlaubt).
|
||||
- **Bewerb B:** Standardspringprüfung 105cm (Besichtigung zu Fuß).
|
||||
- **Lösung:** Eine gemeinsame Besichtigungszeit vor Bewerb A.
|
||||
|
||||
### 2.2 Regelwerks-Konformität
|
||||
- Es gibt kein Verbot für gemeinsame Besichtigungen, solange die Teilnehmer beider Bewerbe die Möglichkeit haben, den Parcours regelkonform zu besichtigen.
|
||||
- **Wichtig:** Wenn Bewerb B erst 2 Stunden später startet, muss für Teilnehmer von Bewerb B theoretisch eine zweite Besichtigungsmöglichkeit (oder ein "Refresh") angeboten werden, falls der Parcours zwischenzeitlich verändert wurde. Wenn er identisch bleibt, reicht die einmalige Bekanntgabe.
|
||||
|
||||
## 3. Anforderungen für die Software (Zeitplan-Modul)
|
||||
|
||||
### 3.1 Datenmodell
|
||||
- `Abteilung` oder `Bewerb` benötigt ein Flag `besichtigungZuPferdErlaubt` (Boolean).
|
||||
- `BesichtigungsBlock` als eigenständiges Entitätsobjekt im Zeitplan, das mit **mehreren** Bewerben/Abteilungen verknüpft werden kann.
|
||||
|
||||
### 3.2 Validierung & Warnungen
|
||||
- **Warnung:** Wenn ein Besichtigungsblock mit "zu Pferd" markiert ist, aber ein verbundener Bewerb (z.B. L-Springen) dies laut ÖTO normalerweise nicht vorsieht (Diskretionsspielraum der Richter).
|
||||
- **Puffer-Check:** Automatische Prüfung, ob zwischen `Ende Besichtigung` und `Start Erster Reiter` mindestens 5 Minuten liegen.
|
||||
|
||||
## 4. Fazit für Architect & Entwickler
|
||||
Die "Besichtigung zu Pferd" ist ein valider Spezialfall für Springpferdeprüfungen. Die Software muss erlauben, Besichtigungszeiten flexibel vor einen oder mehrere Bewerbe zu lagern und den Typ (Fuß/Pferd) zu kennzeichnen.
|
||||
|
||||
---
|
||||
*Geprüft durch den 📜 ÖTO/FEI Rulebook Expert am 11.04.2026*
|
||||
51
docs/01_Architecture/status-automat-nennungen-de.md
Normal file
51
docs/01_Architecture/status-automat-nennungen-de.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# 🤖 Konzept: Status-Automat für Nennungen & Zeitplan-Synchronisation
|
||||
|
||||
Dieses Dokument spezifiziert die Logik des Status-Automaten für Nennungen (Sprint C-1) und dessen Auswirkungen auf den dynamischen Zeitplan.
|
||||
|
||||
## 1. Status-Definitionen (NennStatusE)
|
||||
|
||||
Basierend auf `core-domain/Enums.kt`:
|
||||
|
||||
| Status | Bedeutung | Auswirkung auf Zeitplan |
|
||||
| :--- | :--- | :--- |
|
||||
| `EINGEGANGEN` | Nennung wurde erstellt (Initialzustand) | Belegt Zeitslot (basierend auf `reitdauerMinuten`) |
|
||||
| `BESTAETIGT` | Meldestelle hat Nennung geprüft | Belegt Zeitslot |
|
||||
| `NACHNENNUNG` | Nennung nach Nennschluss | Belegt Zeitslot (ggf. am Ende der Liste) |
|
||||
| `TRANSFERIERT` | Nennung wurde auf anderes Paar übertragen | **Inaktiv** (Original-Eintrag wird durch neuen ersetzt) |
|
||||
| `ZURUECKGEZOGEN`| Reiter hat abgemeldet (vor Startlistenerstellung) | **Inaktiv** (Slot wird frei) |
|
||||
| `GESTARTET` | Paar ist in die Prüfung eingeritten | Startzeitpunkt fixiert, Folgestarts ggf. anpassen |
|
||||
| `NICHT_ANGETRETEN`| Paar ist zum Startzeitpunkt nicht erschienen | **Zeitslot verfällt** oder Folgestarts rücken nach |
|
||||
|
||||
## 2. Status-Übergänge & Validierung
|
||||
|
||||
### 2.1 Gültige Übergänge (Beispiele)
|
||||
- `EINGEGANGEN` -> `BESTAETIGT` (Normalfall)
|
||||
- `EINGEGANGEN` -> `ZURUECKGEZOGEN` (Abmeldung)
|
||||
- `BESTAETIGT` -> `TRANSFERIERT` (Reitertausch)
|
||||
- `BESTAETIGT` -> `GESTARTET` (Während des Turniers)
|
||||
|
||||
### 2.2 Side-Effects (Side-Effect-Engine)
|
||||
Wenn sich der Status einer Nennung ändert, müssen folgende Systeme informiert werden:
|
||||
1. **Billing-Service:** Bei `ZURUECKGEZOGEN` ggf. Stornogebühren prüfen. Bei `TRANSFERIERT` Guthaben übertragen.
|
||||
2. **Startlisten-Service:** Bei `ZURUECKGEZOGEN` nach Veröffentlichung der Startliste -> Eintrag als `istGestrichen = true` markieren.
|
||||
3. **Zeitplan-Optimierung:** Bei `NICHT_ANGETRETEN` -> Alle folgenden Startzeiten rücken um `reitdauerMinuten` nach vorne (sofern im Kalender-Modus "Dynamisch" aktiv ist).
|
||||
|
||||
## 3. Dynamische Zeitplan-Anpassung (C-1 Extension)
|
||||
|
||||
Der `StartlistenService` muss eine Methode `aktualisiereZeitplanNachStatusAenderung` erhalten:
|
||||
|
||||
- **Szenario A (Nicht angetreten):** Wenn Nennung X `NICHT_ANGETRETEN` wird, rücken alle folgenden Nennungen in der Abteilung nach vorne.
|
||||
- **Szenario B (Verspätung):** Wenn Nennung X `GESTARTET` wird, aber 2 Minuten später als geplant, verschieben sich alle Folgetermine um +2 Minuten (Kettenreaktion).
|
||||
|
||||
### Puffer-Regel (ÖTO-Konformität)
|
||||
- Eine dynamische Verschiebung nach *vorne* darf nie dazu führen, dass ein Reiter vor seiner ursprünglich kommunizierten Startzeit (oder einem definierten Puffer-Zeitraum von z.B. 15 Minuten) starten muss, ohne dass dies explizit bestätigt wurde.
|
||||
|
||||
## 4. Implementierungs-Leitfaden für Backend (C-1)
|
||||
|
||||
1. Erweiterung von `NennungUseCases.statusAendern` um Aufrufe der Side-Effect-Handler.
|
||||
2. Implementierung des `NennungStatusListener` in `entries-service`.
|
||||
3. Anbindung an den `StartlistenService` zur Zeitre-Kalkulation.
|
||||
|
||||
---
|
||||
**Status:** Entwurf (Lead Architect)
|
||||
**Datum:** 11. April 2026
|
||||
|
|
@ -36,20 +36,27 @@
|
|||
|
||||
---
|
||||
|
||||
## 🟠 Sprint C — Priorität 2 (nächste Woche)
|
||||
## 🟠 Sprint C — In Arbeit
|
||||
|
||||
- [ ] **C-1** | Synchronisations-Protokoll-Konzeption
|
||||
- [x] Offline-First-Konzept für Desktop ↔ Backend ausarbeiten
|
||||
- [x] Conflict-Resolution-Strategie definieren (gleichzeitige Änderungen)
|
||||
- [x] Konzept-Dokument in `docs/01_Architecture/` ablegen → `docs/01_Architecture/konzept-offline-first-desktop-backend-de.md`
|
||||
- Verweis/Bezug: Baut auf ADR-0021 (Tenant) und ADR-0022 (LAN-Sync Lamport) auf; einheitliches `SyncEvent`-Modell Desktop↔Backend.
|
||||
- [x] **C-1** | Zeitplan-Optimierung Konzept
|
||||
- [x] Fachliche Anforderungen (Use Cases) definiert
|
||||
- [x] Zeitberechnungs-Algorithmus spezifiziert
|
||||
- [x] Drag & Drop Logik für Kalender-Ansicht entworfen
|
||||
- [x] Konzept-Dokument in `docs/01_Architecture/` abgelegt → `docs/01_Architecture/konzept-zeitplan-optimierung-de.md`
|
||||
|
||||
- [ ] **C-2** | MASTER_ROADMAP aktualisieren
|
||||
- [x] Desktop-App-Fokus eintragen
|
||||
- [x] Tenant-Isolation-Meilensteine (Sprint A Ergebnisse) als erledigt markieren
|
||||
- [x] Offline-Sync-Meilensteine eintragen
|
||||
- [x] Phase 8 Fortschritt reflektieren
|
||||
- Update: Siehe `docs/01_Architecture/MASTER_ROADMAP.md` (Stand 2026-04-03) — Produktfokus ergänzt, ADR‑0021/0022 in ADR‑Tabelle eingetragen, Phase‑8‑Status („Konzept/ADR erledigt“) markiert, To‑do „Offline‑First Desktop↔Backend“ verlinkt.
|
||||
- [x] **C-2** | MASTER_ROADMAP aktualisieren
|
||||
- [x] Phase 9 Fortschritt reflektieren
|
||||
- [x] Link zum Zeitplan-Konzept ergänzt
|
||||
- [x] Feature-Migration (Frontend) dokumentiert
|
||||
- [ ] Weitere Sprints (D, E) grob skizzieren
|
||||
|
||||
---
|
||||
|
||||
## 🟠 Sprint D — In Arbeit
|
||||
|
||||
- [ ] **D-1** | USB-Stick Fallback (Sync)
|
||||
- [ ] Technische Machbarkeit (File-Storage vs. SQLite-Export) prüfen
|
||||
- [ ] ADR für Offline-Transfer erstellen
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# 🗂️ Sprint Execution Order — Meldestelle-Biest
|
||||
|
||||
> **Stand:** 10. April 2026 | **Phase:** 9 — Zeitplan & Protokollierung
|
||||
> **Stand:** 11. April 2026 | **Phase:** 9 — Zeitplan & Protokollierung
|
||||
> **Erstellt von:** 🏗️ Lead Architect
|
||||
> **Strategisches Ziel:** Desktop-MVP mit Event-First-Workflow, Offline-First, ÖTO-Konformität
|
||||
|
||||
|
|
@ -10,14 +10,14 @@
|
|||
|
||||
| Agent | Sprint A | Sprint B | Sprint C | Nächste Aktion |
|
||||
|---------------|------------------|------------------------------------------|-------------------|-------------------------------------------------------|
|
||||
| 🏗️ Architect | ✅ Abgeschlossen | ✅ Abgeschlossen | ⬜ Nicht gestartet | Zeitplan-Optimierung Konzept |
|
||||
| 👷 Backend | ✅ Abgeschlossen | ✅ B-1/B-2 fertig | ⬜ Nicht gestartet | C-1 Nennungs-Service Erweiterung |
|
||||
| 🎨 Frontend | ✅ Abgeschlossen | 🟡 B-2/B-3 teilweise / B-4 offen | ⬜ Nicht gestartet | B-4 Kassa-Screen & StoreV2-Ablösung |
|
||||
| 📜 Rulebook | ✅ Abgeschlossen | ✅ B-2 abgeschlossen | ⬜ Nicht gestartet | C-1 AltersklasseRechner |
|
||||
| 🏗️ Architect | ✅ Abgeschlossen | ✅ Abgeschlossen | 🟡 In Arbeit | Zeitplan-Optimierung (ADR/Konzept) |
|
||||
| 👷 Backend | ✅ Abgeschlossen | ✅ Abgeschlossen | 🟡 In Arbeit | C-1 Nennungs-Service Erweiterung |
|
||||
| 🎨 Frontend | ✅ Abgeschlossen | ✅ B-2/B-3/B-4 fertig | 🟡 In Arbeit | C-2 Zeitplan Drag & Drop |
|
||||
| 📜 Rulebook | ✅ Abgeschlossen | ✅ Abgeschlossen | ✅ C-1 fertig | Regelwerk-Validierung Zeitplan |
|
||||
| 🐧 DevOps | ✅ Abgeschlossen | ✅ Abgeschlossen | ✅ C-1/C-2 fertig | C-3 Produktions-Deployment |
|
||||
| 🧐 QA | ✅ Abgeschlossen | ✅ B-1/B-3 fertig | ⬜ Nicht gestartet | B-2 Onboarding-Tests |
|
||||
| 🖌️ UI/UX | ✅ Abgeschlossen | 🔴 B-1/B-4 offen | ⬜ Nicht gestartet | B-4 Wireframes für Kassa-Screen |
|
||||
| 🧹 Curator | ✅ Abgeschlossen | ✅ B-1/B-2 fertig | ⬜ Nicht gestartet | Session-Dokumentation & Changelog |
|
||||
| 🧐 QA | ✅ Abgeschlossen | ✅ Abgeschlossen | 🟡 In Arbeit | Zeitplan-Optimierung Tests |
|
||||
| 🖌️ UI/UX | ✅ Abgeschlossen | ✅ Abgeschlossen | ⬜ Nicht gestartet | C-1 Wireframes in Compose umsetzen |
|
||||
| 🧹 Curator | ✅ Abgeschlossen | ✅ Abgeschlossen | 🟡 In Arbeit | Dokumentations-Audit |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -27,9 +27,7 @@ Diese Aufgaben blockieren andere Agenten und müssen zuerst erledigt werden:
|
|||
|
||||
| Priorität | Agent | Aufgabe | Blockiert |
|
||||
|-----------|---------------|-----------------------------------------------|---------------------------------------------------|
|
||||
| 🔴 P1 | 🖌️ UI/UX | B-4: Wireframes für Kassa-Screen | 🎨 Frontend: B-4 Kassa-Screen |
|
||||
| 🔴 P1 | 🏗️ Architect | C-1: Zeitplan-Optimierung Konzept | 👷 Backend: C-2; 🎨 Frontend: C-2 |
|
||||
| 🔴 P1 | 🎨 Frontend | B-2: StoreV2-Ablösung | 🧐 QA: B-4 ViewModel-Tests |
|
||||
| 🔴 P1 | 🎨 Frontend | C-2: Zeitplan Drag & Drop | 🧐 QA: Zeitplan-Tests |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -38,7 +36,7 @@ Diese Aufgaben blockieren andere Agenten und müssen zuerst erledigt werden:
|
|||
### 🏗️ Architect
|
||||
|
||||
1. ✅ **B-1** ADR-0022 LAN-Sync-Protokoll (Event-Sourcing vs. CRDT vs. Timestamp)
|
||||
2. 🔴 **C-1** Konzept für Zeitplan-Optimierung (Drag & Drop Logik)
|
||||
2. ✅ **C-1** Konzept für Zeitplan-Optimierung (Drag & Drop Logik)
|
||||
|
||||
### 👷 Backend Developer
|
||||
|
||||
|
|
@ -74,7 +72,8 @@ Diese Aufgaben blockieren andere Agenten und müssen zuerst erledigt werden:
|
|||
### 🖌️ UI/UX Designer
|
||||
|
||||
1. ✅ **B-1** Entscheidung Editier-Formulare
|
||||
2. 🔴 **B-4** Wireframes für Kassa-Screen (Veranstaltungs-Kassa)
|
||||
2. ✅ **B-3** Wireframes für Kassa-Screen (Veranstaltungs-Kassa)
|
||||
3. ✅ **B-4** Empty States Spezifikation
|
||||
|
||||
### 🧹 Curator
|
||||
|
||||
|
|
|
|||
33
frontend/features/funktionaer-feature/build.gradle.kts
Normal file
33
frontend/features/funktionaer-feature/build.gradle.kts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
/**
|
||||
* Feature-Modul: Funktionärs-Verwaltung (Desktop-only)
|
||||
*/
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.composeMultiplatform)
|
||||
alias(libs.plugins.composeCompiler)
|
||||
}
|
||||
|
||||
group = "at.mocode.clients"
|
||||
version = "1.0.0"
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
|
||||
sourceSets {
|
||||
jvmMain.dependencies {
|
||||
implementation(projects.frontend.core.designSystem)
|
||||
implementation(projects.frontend.core.domain)
|
||||
implementation(projects.frontend.core.navigation)
|
||||
implementation(compose.desktop.currentOs)
|
||||
implementation(compose.foundation)
|
||||
implementation(compose.runtime)
|
||||
implementation(compose.material3)
|
||||
implementation(compose.ui)
|
||||
implementation(compose.materialIconsExtended)
|
||||
implementation(libs.bundles.kmp.common)
|
||||
implementation(libs.koin.core)
|
||||
implementation(libs.koin.compose)
|
||||
implementation(libs.koin.compose.viewmodel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package at.mocode.frontend.features.funktionaer.di
|
||||
|
||||
import at.mocode.frontend.features.funktionaer.presentation.*
|
||||
import org.koin.dsl.module
|
||||
|
||||
val funktionaerModule = module {
|
||||
single<FunktionaerRepository> { MockFunktionaerRepository() }
|
||||
factory { FunktionaerViewModel(get()) }
|
||||
}
|
||||
|
||||
class MockFunktionaerRepository : FunktionaerRepository {
|
||||
override suspend fun list(): List<FunktionaerListItem> = listOf(
|
||||
FunktionaerListItem(1, "Wolfgang Schier", "RICHTER", "G3"),
|
||||
FunktionaerListItem(2, "Alice Schwab", "RICHTER", "INTERNATIONAL"),
|
||||
FunktionaerListItem(3, "Dietmar Gstöttner", "PARCOURSBAUER", null)
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package at.mocode.frontend.features.funktionaer.domain
|
||||
|
||||
data class Funktionaer(
|
||||
val id: Long,
|
||||
val vorname: String,
|
||||
val nachname: String,
|
||||
val richterNummer: String? = null,
|
||||
val rollen: List<String> = emptyList(),
|
||||
val richterQualifikation: String? = null,
|
||||
val qualifiziertFuerSparten: List<String> = emptyList(),
|
||||
val email: String? = null,
|
||||
val telefon: String? = null,
|
||||
val vereinsNummer: String? = null,
|
||||
val istAktiv: Boolean = true
|
||||
)
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
package at.mocode.frontend.features.funktionaer.presentation
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import at.mocode.frontend.core.designsystem.components.*
|
||||
import at.mocode.frontend.core.designsystem.models.PlaceholderContent
|
||||
|
||||
@Composable
|
||||
fun FunktionaerScreen(
|
||||
viewModel: FunktionaerViewModel
|
||||
) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
|
||||
MsMasterDetailLayout(
|
||||
master = {
|
||||
FunktionaerListContent(
|
||||
state = state,
|
||||
onSearchChange = { viewModel.send(FunktionaerIntent.SearchChanged(it)) },
|
||||
onFunktionaerSelected = { viewModel.send(FunktionaerIntent.Select(it)) }
|
||||
)
|
||||
},
|
||||
detail = {
|
||||
if (state.selectedId != null) {
|
||||
val selected = state.list.find { it.id == state.selectedId }
|
||||
if (selected != null) {
|
||||
FunktionaerDetailContent(selected)
|
||||
}
|
||||
} else {
|
||||
PlaceholderContent(
|
||||
title = "Kein Funktionär ausgewählt",
|
||||
subtitle = "Wählen Sie einen Funktionär aus der Liste aus."
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FunktionaerListContent(
|
||||
state: FunktionaerState,
|
||||
onSearchChange: (String) -> Unit,
|
||||
onFunktionaerSelected: (Long) -> Unit
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
MsFilterBar(
|
||||
searchQuery = state.searchQuery,
|
||||
onSearchQueryChange = onSearchChange,
|
||||
resultCount = state.filtered.size
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
if (state.isLoading) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
} else {
|
||||
MsDataTable(
|
||||
items = state.filtered,
|
||||
columns = listOf(
|
||||
MsColumnDefinition(
|
||||
title = "Name",
|
||||
weight = 1f,
|
||||
cellRenderer = { Text(it.name, style = MaterialTheme.typography.bodySmall) }
|
||||
),
|
||||
MsColumnDefinition(
|
||||
title = "Rolle",
|
||||
width = 150.dp,
|
||||
cellRenderer = { Text(it.rolle, style = MaterialTheme.typography.bodySmall) }
|
||||
),
|
||||
MsColumnDefinition(
|
||||
title = "Lizenz",
|
||||
width = 100.dp,
|
||||
cellRenderer = { Text(it.lizenz ?: "-", style = MaterialTheme.typography.bodySmall) }
|
||||
)
|
||||
),
|
||||
onRowClick = { onFunktionaerSelected(it.id) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FunktionaerDetailContent(item: FunktionaerListItem) {
|
||||
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
|
||||
Text(item.name, style = MaterialTheme.typography.headlineMedium)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text("Rolle: ${item.rolle}", style = MaterialTheme.typography.bodyLarge)
|
||||
item.lizenz?.let {
|
||||
Text("Lizenz: $it", style = MaterialTheme.typography.bodyLarge)
|
||||
}
|
||||
Spacer(Modifier.height(24.dp))
|
||||
Text("Weitere Details folgen in der nächsten Ausbaustufe.", style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package at.mocode.nennung.feature.di
|
||||
package at.mocode.frontend.features.nennung.di
|
||||
|
||||
import at.mocode.nennung.feature.presentation.NennungViewModel
|
||||
import at.mocode.frontend.features.nennung.presentation.NennungViewModel
|
||||
import org.koin.core.module.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package at.mocode.nennung.feature.domain
|
||||
package at.mocode.frontend.features.nennung.domain
|
||||
|
||||
// --- Pferd ---
|
||||
data class Pferd(
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package at.mocode.nennung.feature.presentation
|
||||
package at.mocode.frontend.features.nennung.presentation
|
||||
|
||||
import at.mocode.nennung.feature.domain.*
|
||||
import at.mocode.frontend.features.nennung.domain.*
|
||||
import androidx.lifecycle.ViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package at.mocode.nennung.feature.presentation
|
||||
package at.mocode.frontend.features.nennung.presentation
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
|
|
@ -17,7 +17,8 @@ import androidx.compose.ui.text.font.FontWeight
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import at.mocode.nennung.feature.domain.*
|
||||
import at.mocode.frontend.features.nennung.domain.*
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
// Farben für Startwunsch-Markierung
|
||||
private val FarbeVorne = Color(0xFFE8F5E9) // Grün
|
||||
|
|
@ -37,7 +38,7 @@ fun NennungsMaske(
|
|||
// Status-Snackbar
|
||||
state.statusMeldung?.let { meldung ->
|
||||
LaunchedEffect(meldung) {
|
||||
kotlinx.coroutines.delay(3000)
|
||||
kotlinx.coroutines.delay(3000.milliseconds)
|
||||
viewModel.statusMeldungDismiss()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package at.mocode.frontend.features.pferde.di
|
||||
|
||||
import at.mocode.frontend.features.pferde.presentation.PferdeViewModel
|
||||
import org.koin.dsl.module
|
||||
|
||||
val pferdeModule = module {
|
||||
factory { PferdeViewModel() }
|
||||
}
|
||||
|
|
@ -12,7 +12,12 @@ data class Pferd(
|
|||
val geschlecht: Geschlecht = Geschlecht.WALLACH,
|
||||
val farbe: String = "",
|
||||
val geburtsjahr: Int? = null,
|
||||
val status: PferdeStatus = PferdeStatus.AKTIV
|
||||
val status: PferdeStatus = PferdeStatus.AKTIV,
|
||||
val feiId: String? = null,
|
||||
val oepsNummer: String? = null,
|
||||
val vater: String? = null,
|
||||
val mutter: String? = null,
|
||||
val besitzer: String? = null
|
||||
)
|
||||
|
||||
enum class Geschlecht(val label: String) {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import androidx.compose.material3.MaterialTheme
|
|||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import at.mocode.frontend.core.designsystem.components.*
|
||||
|
|
@ -38,6 +37,9 @@ fun PferdeScreen(
|
|||
onFarbeChange = viewModel::onEditFarbeChange,
|
||||
onGeburtsjahrChange = viewModel::onEditGeburtsjahrChange,
|
||||
onStatusChange = viewModel::onEditStatusChange,
|
||||
onFeiIdChange = viewModel::onEditFeiIdChange,
|
||||
onOepsNummerChange = viewModel::onEditOepsNummerChange,
|
||||
onBesitzerChange = viewModel::onEditBesitzerChange,
|
||||
onSave = viewModel::onSave,
|
||||
onCancel = viewModel::onCancel
|
||||
)
|
||||
|
|
@ -105,6 +107,9 @@ private fun PferdeEditorContent(
|
|||
onFarbeChange: (String) -> Unit,
|
||||
onGeburtsjahrChange: (String) -> Unit,
|
||||
onStatusChange: (PferdeStatus) -> Unit,
|
||||
onFeiIdChange: (String) -> Unit,
|
||||
onOepsNummerChange: (String) -> Unit,
|
||||
onBesitzerChange: (String) -> Unit,
|
||||
onSave: () -> Unit,
|
||||
onCancel: () -> Unit
|
||||
) {
|
||||
|
|
@ -134,6 +139,23 @@ private fun PferdeEditorContent(
|
|||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
MsTextField(
|
||||
value = uiState.editFeiId,
|
||||
onValueChange = onFeiIdChange,
|
||||
label = "FEI ID",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
MsTextField(
|
||||
value = uiState.editOepsNummer,
|
||||
onValueChange = onOepsNummerChange,
|
||||
label = "ÖPS Nummer",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
MsEnumDropdown(
|
||||
label = "Geschlecht",
|
||||
|
|
@ -160,16 +182,25 @@ private fun PferdeEditorContent(
|
|||
label = "Geburtsjahr",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
MsEnumDropdown(
|
||||
label = "Status",
|
||||
options = PferdeStatus.entries.toTypedArray(),
|
||||
selectedOption = uiState.editStatus,
|
||||
onOptionSelected = onStatusChange,
|
||||
optionLabel = { it.label },
|
||||
MsTextField(
|
||||
value = uiState.editBesitzer,
|
||||
onValueChange = onBesitzerChange,
|
||||
label = "Besitzer",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
MsEnumDropdown(
|
||||
label = "Status",
|
||||
options = PferdeStatus.entries.toTypedArray(),
|
||||
selectedOption = uiState.editStatus,
|
||||
onOptionSelected = onStatusChange,
|
||||
optionLabel = { it.label },
|
||||
modifier = Modifier.width(300.dp)
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
|
||||
if (uiState.editStatus == PferdeStatus.INAKTIV) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package at.mocode.frontend.features.pferde.presentation
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import at.mocode.frontend.features.pferde.domain.Geschlecht
|
||||
import at.mocode.frontend.features.pferde.domain.Pferd
|
||||
import at.mocode.frontend.features.pferde.domain.PferdeStatus
|
||||
|
|
@ -21,13 +22,16 @@ data class PferdeUiState(
|
|||
val editGeschlecht: Geschlecht = Geschlecht.WALLACH,
|
||||
val editFarbe: String = "",
|
||||
val editGeburtsjahr: String = "",
|
||||
val editStatus: PferdeStatus = PferdeStatus.AKTIV
|
||||
val editStatus: PferdeStatus = PferdeStatus.AKTIV,
|
||||
val editFeiId: String = "",
|
||||
val editOepsNummer: String = "",
|
||||
val editBesitzer: String = ""
|
||||
)
|
||||
|
||||
/**
|
||||
* ViewModel für die Pferde-Verwaltung.
|
||||
*/
|
||||
open class PferdeViewModel(initialLoad: Boolean = true) {
|
||||
open class PferdeViewModel(initialLoad: Boolean = true) : ViewModel() {
|
||||
var uiState by mutableStateOf(PferdeUiState())
|
||||
protected set
|
||||
|
||||
|
|
@ -60,10 +64,25 @@ open class PferdeViewModel(initialLoad: Boolean = true) {
|
|||
editGeschlecht = pferd.geschlecht,
|
||||
editFarbe = pferd.farbe,
|
||||
editGeburtsjahr = pferd.geburtsjahr?.toString() ?: "",
|
||||
editStatus = pferd.status
|
||||
editStatus = pferd.status,
|
||||
editFeiId = pferd.feiId ?: "",
|
||||
editOepsNummer = pferd.oepsNummer ?: "",
|
||||
editBesitzer = pferd.besitzer ?: ""
|
||||
)
|
||||
}
|
||||
|
||||
fun onEditFeiIdChange(value: String) {
|
||||
uiState = uiState.copy(editFeiId = value)
|
||||
}
|
||||
|
||||
fun onEditOepsNummerChange(value: String) {
|
||||
uiState = uiState.copy(editOepsNummer = value)
|
||||
}
|
||||
|
||||
fun onEditBesitzerChange(value: String) {
|
||||
uiState = uiState.copy(editBesitzer = value)
|
||||
}
|
||||
|
||||
fun onEditNameChange(value: String) {
|
||||
uiState = uiState.copy(editName = value)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
package at.mocode.frontend.features.reiter.di
|
||||
|
||||
import at.mocode.frontend.features.reiter.presentation.ReiterViewModel
|
||||
import org.koin.dsl.module
|
||||
|
||||
val reiterModule = module {
|
||||
factory { ReiterViewModel() }
|
||||
}
|
||||
|
|
@ -12,7 +12,13 @@ data class Reiter(
|
|||
val satznummer: String?,
|
||||
val lizenz: LizenzKlasse = LizenzKlasse.KEINE,
|
||||
val sparte: Sparte = Sparte.KEINE,
|
||||
val status: ReiterStatus = ReiterStatus.AKTIV
|
||||
val status: ReiterStatus = ReiterStatus.AKTIV,
|
||||
val feiId: String? = null,
|
||||
val oepsNummer: String? = null,
|
||||
val geburtsdatum: String? = null,
|
||||
val email: String? = null,
|
||||
val telefon: String? = null,
|
||||
val verein: String? = null
|
||||
) {
|
||||
val name: String get() = "$vorname $nachname"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,12 @@ fun ReiterScreen(
|
|||
onNachnameChange = viewModel::onEditNameChange,
|
||||
onLizenzChange = viewModel::onEditLizenzChange,
|
||||
onSparteChange = viewModel::onEditSparteChange,
|
||||
onFeiIdChange = viewModel::onEditFeiIdChange,
|
||||
onOepsNummerChange = viewModel::onEditOepsNummerChange,
|
||||
onGeburtsdatumChange = viewModel::onEditGeburtsdatumChange,
|
||||
onEmailChange = viewModel::onEditEmailChange,
|
||||
onTelefonChange = viewModel::onEditTelefonChange,
|
||||
onVereinChange = viewModel::onEditVereinChange,
|
||||
onSave = viewModel::onSave,
|
||||
onCancel = viewModel::onCancel
|
||||
)
|
||||
|
|
@ -104,6 +110,12 @@ private fun ReiterEditorContent(
|
|||
onNachnameChange: (String) -> Unit,
|
||||
onLizenzChange: (LizenzKlasse) -> Unit,
|
||||
onSparteChange: (Sparte) -> Unit,
|
||||
onFeiIdChange: (String) -> Unit,
|
||||
onOepsNummerChange: (String) -> Unit,
|
||||
onGeburtsdatumChange: (String) -> Unit,
|
||||
onEmailChange: (String) -> Unit,
|
||||
onTelefonChange: (String) -> Unit,
|
||||
onVereinChange: (String) -> Unit,
|
||||
onSave: () -> Unit,
|
||||
onCancel: () -> Unit
|
||||
) {
|
||||
|
|
@ -133,6 +145,57 @@ private fun ReiterEditorContent(
|
|||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
MsTextField(
|
||||
value = uiState.editFeiId,
|
||||
onValueChange = onFeiIdChange,
|
||||
label = "FEI ID",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
MsTextField(
|
||||
value = uiState.editOepsNummer,
|
||||
onValueChange = onOepsNummerChange,
|
||||
label = "ÖPS Nummer",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
MsTextField(
|
||||
value = uiState.editGeburtsdatum,
|
||||
onValueChange = onGeburtsdatumChange,
|
||||
label = "Geburtsdatum",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
MsTextField(
|
||||
value = uiState.editVerein,
|
||||
onValueChange = onVereinChange,
|
||||
label = "Verein",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
MsTextField(
|
||||
value = uiState.editEmail,
|
||||
onValueChange = onEmailChange,
|
||||
label = "E-Mail",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
MsTextField(
|
||||
value = uiState.editTelefon,
|
||||
onValueChange = onTelefonChange,
|
||||
label = "Telefon",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
MsEnumDropdown(
|
||||
label = "Lizenzklasse",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package at.mocode.frontend.features.reiter.presentation
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import at.mocode.frontend.features.reiter.domain.LizenzKlasse
|
||||
import at.mocode.frontend.features.reiter.domain.Reiter
|
||||
import at.mocode.frontend.features.reiter.domain.ReiterStatus
|
||||
|
|
@ -21,14 +22,20 @@ data class ReiterUiState(
|
|||
val editVorname: String = "",
|
||||
val editLizenz: LizenzKlasse = LizenzKlasse.KEINE,
|
||||
val editSparte: Sparte = Sparte.KEINE,
|
||||
val editStatus: ReiterStatus = ReiterStatus.AKTIV
|
||||
val editStatus: ReiterStatus = ReiterStatus.AKTIV,
|
||||
val editFeiId: String = "",
|
||||
val editOepsNummer: String = "",
|
||||
val editGeburtsdatum: String = "",
|
||||
val editEmail: String = "",
|
||||
val editTelefon: String = "",
|
||||
val editVerein: String = ""
|
||||
)
|
||||
|
||||
/**
|
||||
* ViewModel für die Reiter-Verwaltung.
|
||||
* In einem echten Szenario würden wir hier ein Repository injizieren.
|
||||
*/
|
||||
open class ReiterViewModel(initialLoad: Boolean = true) {
|
||||
open class ReiterViewModel(initialLoad: Boolean = true) : ViewModel() {
|
||||
var uiState by mutableStateOf(ReiterUiState())
|
||||
protected set
|
||||
|
||||
|
|
@ -62,10 +69,23 @@ open class ReiterViewModel(initialLoad: Boolean = true) {
|
|||
editName = reiter.nachname,
|
||||
editLizenz = reiter.lizenz,
|
||||
editSparte = reiter.sparte,
|
||||
editStatus = reiter.status
|
||||
editStatus = reiter.status,
|
||||
editFeiId = reiter.feiId ?: "",
|
||||
editOepsNummer = reiter.oepsNummer ?: "",
|
||||
editGeburtsdatum = reiter.geburtsdatum ?: "",
|
||||
editEmail = reiter.email ?: "",
|
||||
editTelefon = reiter.telefon ?: "",
|
||||
editVerein = reiter.verein ?: ""
|
||||
)
|
||||
}
|
||||
|
||||
fun onEditFeiIdChange(value: String) { uiState = uiState.copy(editFeiId = value) }
|
||||
fun onEditOepsNummerChange(value: String) { uiState = uiState.copy(editOepsNummer = value) }
|
||||
fun onEditGeburtsdatumChange(value: String) { uiState = uiState.copy(editGeburtsdatum = value) }
|
||||
fun onEditEmailChange(value: String) { uiState = uiState.copy(editEmail = value) }
|
||||
fun onEditTelefonChange(value: String) { uiState = uiState.copy(editTelefon = value) }
|
||||
fun onEditVereinChange(value: String) { uiState = uiState.copy(editVerein = value) }
|
||||
|
||||
fun onEditVornameChange(value: String) {
|
||||
uiState = uiState.copy(editVorname = value)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
package at.mocode.frontend.features.veranstalter.data.remote
|
||||
|
||||
import at.mocode.frontend.features.veranstalter.domain.Veranstalter
|
||||
import at.mocode.frontend.features.veranstalter.domain.VeranstalterRepository
|
||||
|
||||
/**
|
||||
* Fake-Implementierung für die Veranstalter-Verwaltung (Offline-First Prototyp).
|
||||
* Später durch Ktor-Backed Implementierung ersetzt.
|
||||
*/
|
||||
class FakeVeranstalterRepository : VeranstalterRepository {
|
||||
private val mockData = mutableListOf(
|
||||
Veranstalter(1, "URV Schloss Hof", "1-2345", "Schloßhof", "Aktiv"),
|
||||
Veranstalter(2, "RV Schloß Rosenau", "3-0012", "Rosenau", "Aktiv"),
|
||||
Veranstalter(3, "Reitclub Tulln", "3-1520", "Tulln", "Inaktiv"),
|
||||
Veranstalter(4, "RC St. Pölten", "3-0101", "St. Pölten", "Aktiv"),
|
||||
Veranstalter(5, "Union Reitklub Wien", "9-0001", "Wien", "Aktiv")
|
||||
)
|
||||
|
||||
override suspend fun list(): Result<List<Veranstalter>> = Result.success(mockData)
|
||||
|
||||
override suspend fun getById(id: Long): Result<Veranstalter> {
|
||||
return mockData.find { it.id == id }?.let { Result.success(it) }
|
||||
?: Result.failure(Exception("Veranstalter nicht gefunden"))
|
||||
}
|
||||
|
||||
override suspend fun create(model: Veranstalter): Result<Veranstalter> {
|
||||
mockData.add(model)
|
||||
return Result.success(model)
|
||||
}
|
||||
|
||||
override suspend fun update(id: Long, model: Veranstalter): Result<Veranstalter> {
|
||||
val index = mockData.indexOfFirst { it.id == id }
|
||||
if (index != -1) {
|
||||
mockData[index] = model
|
||||
return Result.success(model)
|
||||
}
|
||||
return Result.failure(Exception("Veranstalter nicht gefunden"))
|
||||
}
|
||||
|
||||
override suspend fun delete(id: Long): Result<Unit> {
|
||||
mockData.removeIf { it.id == id }
|
||||
return Result.success(Unit)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,5 @@
|
|||
package at.mocode.frontend.features.veranstalter.domain
|
||||
|
||||
/**
|
||||
* Domänenmodell für Veranstalter (V3-Minimum für Listenansicht).
|
||||
*/
|
||||
data class Veranstalter(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package at.mocode.veranstalter.feature.presentation
|
||||
package at.mocode.frontend.features.veranstalter.presentation
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
package at.mocode.frontend.features.veranstalter.data.remote
|
||||
|
||||
import at.mocode.frontend.core.network.*
|
||||
import at.mocode.frontend.features.veranstalter.data.mapper.toDomain
|
||||
import at.mocode.frontend.features.veranstalter.data.mapper.toDto
|
||||
import at.mocode.frontend.features.veranstalter.data.remote.dto.VeranstalterDto
|
||||
import at.mocode.frontend.features.veranstalter.domain.Veranstalter
|
||||
import at.mocode.frontend.features.veranstalter.domain.VeranstalterRepository
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.http.*
|
||||
|
||||
class DefaultVeranstalterRepository(
|
||||
private val client: HttpClient,
|
||||
) : VeranstalterRepository {
|
||||
|
||||
override suspend fun list(): Result<List<Veranstalter>> = runCatching {
|
||||
val response = client.get(ApiRoutes.Veranstalter.ROOT)
|
||||
when {
|
||||
response.status.isSuccess() -> response.body<List<VeranstalterDto>>().map { it.toDomain() }
|
||||
response.status == HttpStatusCode.Unauthorized -> throw AuthExpired()
|
||||
response.status == HttpStatusCode.Forbidden -> throw AuthForbidden()
|
||||
response.status.value >= 500 -> throw ServerError()
|
||||
else -> throw HttpError(response.status.value)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getById(id: Long): Result<Veranstalter> = runCatching {
|
||||
val response = client.get("${ApiRoutes.Veranstalter.ROOT}/$id")
|
||||
when {
|
||||
response.status.isSuccess() -> response.body<VeranstalterDto>().toDomain()
|
||||
response.status == HttpStatusCode.NotFound -> throw NotFound()
|
||||
response.status == HttpStatusCode.Unauthorized -> throw AuthExpired()
|
||||
response.status == HttpStatusCode.Forbidden -> throw AuthForbidden()
|
||||
response.status.value >= 500 -> throw ServerError()
|
||||
else -> throw HttpError(response.status.value)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun create(model: Veranstalter): Result<Veranstalter> = runCatching {
|
||||
val response = client.post(ApiRoutes.Veranstalter.ROOT) { setBody(model.toDto()) }
|
||||
when {
|
||||
response.status.isSuccess() -> response.body<VeranstalterDto>().toDomain()
|
||||
response.status == HttpStatusCode.Conflict -> throw Conflict()
|
||||
response.status.value >= 500 -> throw ServerError()
|
||||
else -> throw HttpError(response.status.value)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun update(id: Long, model: Veranstalter): Result<Veranstalter> = runCatching {
|
||||
val response = client.put("${ApiRoutes.Veranstalter.ROOT}/$id") { setBody(model.toDto()) }
|
||||
when {
|
||||
response.status.isSuccess() -> response.body<VeranstalterDto>().toDomain()
|
||||
response.status == HttpStatusCode.NotFound -> throw NotFound()
|
||||
response.status == HttpStatusCode.Conflict -> throw Conflict()
|
||||
response.status.value >= 500 -> throw ServerError()
|
||||
else -> throw HttpError(response.status.value)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun delete(id: Long): Result<Unit> = runCatching {
|
||||
val response = client.delete("${ApiRoutes.Veranstalter.ROOT}/$id")
|
||||
when {
|
||||
response.status.isSuccess() -> Unit
|
||||
response.status == HttpStatusCode.NotFound -> throw NotFound()
|
||||
response.status.value >= 500 -> throw ServerError()
|
||||
else -> throw HttpError(response.status.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,10 +1,9 @@
|
|||
package at.mocode.frontend.features.veranstalter.di
|
||||
|
||||
import at.mocode.frontend.features.veranstalter.data.remote.DefaultVeranstalterRepository
|
||||
import at.mocode.frontend.features.veranstalter.data.remote.FakeVeranstalterRepository
|
||||
import at.mocode.frontend.features.veranstalter.domain.VeranstalterRepository
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
val veranstalterModule = module {
|
||||
single<VeranstalterRepository> { DefaultVeranstalterRepository(get(named("apiClient"))) }
|
||||
single<VeranstalterRepository> { FakeVeranstalterRepository() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package at.mocode.veranstalter.feature.presentation
|
||||
package at.mocode.frontend.features.veranstalter.presentation
|
||||
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package at.mocode.veranstalter.feature.presentation
|
||||
package at.mocode.frontend.features.veranstalter.presentation
|
||||
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package at.mocode.veranstalter.feature.presentation
|
||||
package at.mocode.frontend.features.veranstalter.presentation
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package at.mocode.veranstalter.feature.presentation
|
||||
package at.mocode.frontend.features.veranstalter.presentation
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
|
|
@ -1,15 +1,13 @@
|
|||
package at.mocode.veranstalter.feature.presentation
|
||||
package at.mocode.frontend.features.veranstalter.presentation
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package at.mocode.veranstalter.feature.presentation
|
||||
package at.mocode.frontend.features.veranstalter.presentation
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package at.mocode.veranstalter.feature.presentation
|
||||
package at.mocode.frontend.features.veranstalter.presentation
|
||||
|
||||
import at.mocode.frontend.core.designsystem.models.LoginStatus
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package at.mocode.veranstalter.feature.presentation
|
||||
package at.mocode.frontend.features.veranstalter.presentation
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
|
|
@ -1 +0,0 @@
|
|||
// Deprecated fake repository removed in favor of real Ktor-backed implementation.
|
||||
|
|
@ -52,17 +52,19 @@ kotlin {
|
|||
implementation(projects.core.znsParser)
|
||||
|
||||
// Feature-Module
|
||||
implementation(projects.frontend.features.nennungFeature)
|
||||
implementation(projects.frontend.features.pingFeature)
|
||||
implementation(projects.frontend.features.nennungFeature)
|
||||
|
||||
implementation(projects.frontend.features.znsImportFeature)
|
||||
implementation(projects.frontend.features.veranstalterFeature)
|
||||
implementation(projects.frontend.features.veranstaltungFeature)
|
||||
implementation(projects.frontend.features.funktionaerFeature)
|
||||
implementation(projects.frontend.features.profileFeature)
|
||||
implementation(projects.frontend.features.reiterFeature)
|
||||
implementation(projects.frontend.features.pferdeFeature)
|
||||
implementation(projects.frontend.features.vereinFeature)
|
||||
implementation(projects.frontend.features.turnierFeature)
|
||||
implementation(project(":frontend:features:profile-feature"))
|
||||
implementation(project(":frontend:features:reiter-feature"))
|
||||
implementation(project(":frontend:features:pferde-feature"))
|
||||
implementation(project(":frontend:features:billing-feature"))
|
||||
implementation(project(":frontend:features:verein-feature"))
|
||||
implementation(projects.frontend.features.billingFeature)
|
||||
|
||||
// Compose Desktop
|
||||
implementation(compose.desktop.currentOs)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@ import at.mocode.frontend.core.sync.di.syncModule
|
|||
import at.mocode.frontend.features.billing.di.billingModule
|
||||
import at.mocode.frontend.features.profile.di.profileModule
|
||||
import at.mocode.frontend.features.verein.di.vereinFeatureModule
|
||||
import at.mocode.nennung.feature.di.nennungFeatureModule
|
||||
import at.mocode.frontend.features.nennung.di.nennungFeatureModule
|
||||
import at.mocode.frontend.features.pferde.di.pferdeModule
|
||||
import at.mocode.frontend.features.reiter.di.reiterModule
|
||||
import at.mocode.ping.feature.di.pingFeatureModule
|
||||
import at.mocode.zns.feature.di.znsImportModule
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
|
@ -36,6 +38,8 @@ fun main() = application {
|
|||
znsImportModule,
|
||||
profileModule,
|
||||
billingModule,
|
||||
pferdeModule,
|
||||
reiterModule,
|
||||
vereinFeatureModule,
|
||||
desktopModule,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -21,14 +21,19 @@ import androidx.compose.ui.unit.sp
|
|||
import at.mocode.frontend.core.navigation.AppScreen
|
||||
import at.mocode.frontend.features.billing.presentation.BillingScreen
|
||||
import at.mocode.frontend.features.billing.presentation.BillingViewModel
|
||||
import at.mocode.frontend.features.nennung.presentation.NennungViewModel
|
||||
import at.mocode.frontend.features.nennung.presentation.NennungsMaske
|
||||
import at.mocode.frontend.features.pferde.presentation.PferdeScreen
|
||||
import at.mocode.frontend.features.pferde.presentation.PferdeViewModel
|
||||
import at.mocode.frontend.features.profile.presentation.ProfileScreen
|
||||
import at.mocode.frontend.features.profile.presentation.ProfileViewModel
|
||||
import at.mocode.frontend.features.reiter.presentation.ReiterScreen
|
||||
import at.mocode.frontend.features.reiter.presentation.ReiterViewModel
|
||||
import at.mocode.frontend.features.verein.presentation.VereinScreen
|
||||
import at.mocode.frontend.features.verein.presentation.VereinViewModel
|
||||
import at.mocode.ping.feature.presentation.PingScreen
|
||||
import at.mocode.ping.feature.presentation.PingViewModel
|
||||
import at.mocode.turnier.feature.presentation.TurnierDetailScreen
|
||||
import at.mocode.veranstalter.feature.presentation.FakeVeranstalterStore
|
||||
import at.mocode.veranstaltung.feature.presentation.AdminUebersichtScreen
|
||||
import at.mocode.veranstaltung.feature.presentation.VeranstaltungDetailScreen
|
||||
import at.mocode.veranstaltung.feature.presentation.VeranstaltungNeuScreen
|
||||
|
|
@ -379,14 +384,18 @@ private fun DesktopContentArea(
|
|||
// Onboarding ohne Login
|
||||
is AppScreen.Onboarding -> {
|
||||
val authTokenManager: at.mocode.frontend.core.auth.data.AuthTokenManager = koinInject()
|
||||
at.mocode.desktop.v2.OnboardingScreen(
|
||||
geraetName = obGeraet,
|
||||
secureKey = obKey,
|
||||
onGeraetNameChange = onObGeraetChange,
|
||||
onSecureKeyChange = onObKeyChange,
|
||||
) { _, _ ->
|
||||
authTokenManager.setToken("dummy.jwt.token")
|
||||
onNavigate(AppScreen.VeranstaltungVerwaltung)
|
||||
at.mocode.frontend.core.designsystem.theme.AppTheme {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
at.mocode.desktop.v2.OnboardingScreen(
|
||||
geraetName = obGeraet,
|
||||
secureKey = obKey,
|
||||
onGeraetNameChange = onObGeraetChange,
|
||||
onSecureKeyChange = onObKeyChange,
|
||||
) { _, _ ->
|
||||
authTokenManager.setToken("dummy.jwt.token")
|
||||
onNavigate(AppScreen.VeranstaltungVerwaltung)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -412,37 +421,50 @@ private fun DesktopContentArea(
|
|||
}
|
||||
|
||||
// --- Pferde-Verwaltung & Profil ---
|
||||
is AppScreen.PferdVerwaltung -> at.mocode.desktop.v2.PferdeVerwaltungScreen(
|
||||
onBack = onBack,
|
||||
onEdit = { onNavigate(AppScreen.PferdProfil(it)) }
|
||||
)
|
||||
is AppScreen.PferdVerwaltung -> {
|
||||
val viewModel = koinViewModel<PferdeViewModel>()
|
||||
PferdeScreen(viewModel = viewModel)
|
||||
}
|
||||
|
||||
is AppScreen.PferdProfil -> at.mocode.desktop.v2.PferdProfilV2(
|
||||
id = currentScreen.id,
|
||||
onBack = onBack,
|
||||
)
|
||||
is AppScreen.PferdProfil -> {
|
||||
val viewModel = koinViewModel<PferdeViewModel>()
|
||||
// In der aktuellen Ausbaustufe wählen wir das Pferd im ViewModel aus
|
||||
LaunchedEffect(currentScreen.id) {
|
||||
// Mock: Wir suchen das Pferd in den Suchergebnissen
|
||||
viewModel.uiState.searchResults.find { it.id == currentScreen.id.toString() }?.let {
|
||||
viewModel.selectPferd(it)
|
||||
}
|
||||
}
|
||||
PferdeScreen(viewModel = viewModel)
|
||||
}
|
||||
|
||||
// --- Reiter-Verwaltung & Profil ---
|
||||
is AppScreen.ReiterVerwaltung -> at.mocode.desktop.v2.ReiterVerwaltungScreen(
|
||||
onBack = onBack,
|
||||
onEdit = { onNavigate(AppScreen.ReiterProfil(it)) }
|
||||
)
|
||||
is AppScreen.ReiterVerwaltung -> {
|
||||
val viewModel = koinViewModel<ReiterViewModel>()
|
||||
ReiterScreen(viewModel = viewModel)
|
||||
}
|
||||
|
||||
is AppScreen.ReiterProfil -> at.mocode.desktop.v2.ReiterProfilV2(
|
||||
id = currentScreen.id,
|
||||
onBack = onBack,
|
||||
)
|
||||
is AppScreen.ReiterProfil -> {
|
||||
val viewModel = koinViewModel<ReiterViewModel>()
|
||||
LaunchedEffect(currentScreen.id) {
|
||||
viewModel.uiState.searchResults.find { it.id == currentScreen.id.toString() }?.let {
|
||||
viewModel.selectReiter(it)
|
||||
}
|
||||
}
|
||||
ReiterScreen(viewModel = viewModel)
|
||||
}
|
||||
|
||||
// --- Verein-Verwaltung & Profil ---
|
||||
is AppScreen.VereinVerwaltung -> at.mocode.desktop.v2.VereinVerwaltungScreen(
|
||||
onBack = onBack,
|
||||
onEdit = { onNavigate(AppScreen.VereinProfil(it)) }
|
||||
)
|
||||
is AppScreen.VereinVerwaltung -> {
|
||||
val vereinViewModel: VereinViewModel = koinViewModel()
|
||||
VereinScreen(viewModel = vereinViewModel)
|
||||
}
|
||||
|
||||
is AppScreen.VereinProfil -> at.mocode.desktop.v2.VereinProfilV2(
|
||||
id = currentScreen.id,
|
||||
onBack = onBack,
|
||||
)
|
||||
is AppScreen.VereinProfil -> {
|
||||
val vereinViewModel: VereinViewModel = koinViewModel()
|
||||
// Mock: Selektion im ViewModel (falls unterstützt)
|
||||
VereinScreen(viewModel = vereinViewModel)
|
||||
}
|
||||
|
||||
// --- Funktionaer-Verwaltung & Profil ---
|
||||
is AppScreen.FunktionaerVerwaltung -> at.mocode.desktop.v2.FunktionaerVerwaltungScreen(
|
||||
|
|
@ -488,7 +510,7 @@ private fun DesktopContentArea(
|
|||
)
|
||||
is AppScreen.VeranstalterDetail -> {
|
||||
val vId = currentScreen.veranstalterId
|
||||
if (!FakeVeranstalterStore.exists(vId)) {
|
||||
if (vId != 1L) { // Temporärer Check für Mock-Daten
|
||||
InvalidContextNotice(
|
||||
message = "Veranstalter (ID=$vId) nicht gefunden.",
|
||||
onBack = onBack
|
||||
|
|
@ -664,6 +686,14 @@ private fun DesktopContentArea(
|
|||
)
|
||||
}
|
||||
|
||||
is AppScreen.Nennung -> {
|
||||
val nennungViewModel: NennungViewModel = koinViewModel()
|
||||
NennungsMaske(
|
||||
viewModel = nennungViewModel,
|
||||
onAbrechnungOeffnen = { /* Navigation zu Billing falls nötig */ }
|
||||
)
|
||||
}
|
||||
|
||||
// Fallback → Root
|
||||
else -> AdminUebersichtScreen(
|
||||
onVeranstalterAuswahl = { onNavigate(AppScreen.VeranstalterAuswahl) },
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ import at.mocode.turnier.feature.domain.BewerbRepository
|
|||
import at.mocode.turnier.feature.domain.StartlistenRepository
|
||||
import at.mocode.turnier.feature.presentation.*
|
||||
import at.mocode.zns.parser.ZnsBewerb
|
||||
import at.mocode.veranstalter.feature.presentation.VeranstalterAuswahlScreen
|
||||
import at.mocode.veranstalter.feature.presentation.VeranstalterDetailScreen
|
||||
import at.mocode.veranstalter.feature.presentation.VeranstalterNeuScreen
|
||||
import at.mocode.frontend.features.veranstalter.presentation.VeranstalterAuswahlScreen
|
||||
import at.mocode.frontend.features.veranstalter.presentation.VeranstalterDetailScreen
|
||||
import at.mocode.frontend.features.veranstalter.presentation.VeranstalterNeuScreen
|
||||
import at.mocode.veranstaltung.feature.presentation.VeranstaltungUebersichtScreen
|
||||
import at.mocode.wui.preview.ComponentPreview
|
||||
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ include(":frontend:features:nennung-feature")
|
|||
include(":frontend:features:zns-import-feature")
|
||||
include(":frontend:features:veranstalter-feature")
|
||||
include(":frontend:features:veranstaltung-feature")
|
||||
include(":frontend:features:funktionaer-feature")
|
||||
include(":frontend:features:profile-feature")
|
||||
include(":frontend:features:reiter-feature")
|
||||
include(":frontend:features:pferde-feature")
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user