Compare commits
2 Commits
5c7ba28b1e
...
9b9c068e7f
| Author | SHA1 | Date | |
|---|---|---|---|
| 9b9c068e7f | |||
| f719764914 |
|
|
@ -1,3 +1,5 @@
|
||||||
|
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.kotlinMultiplatform)
|
alias(libs.plugins.kotlinMultiplatform)
|
||||||
alias(libs.plugins.kotlinSerialization)
|
alias(libs.plugins.kotlinSerialization)
|
||||||
|
|
@ -8,6 +10,10 @@ kotlin {
|
||||||
js(IR) {
|
js(IR) {
|
||||||
browser()
|
browser()
|
||||||
}
|
}
|
||||||
|
@OptIn(ExperimentalWasmDsl::class)
|
||||||
|
wasmJs {
|
||||||
|
browser()
|
||||||
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
val commonMain by getting {
|
val commonMain by getting {
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,13 @@ package at.mocode.core.domain.serialization
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
import kotlinx.datetime.LocalDateTime
|
import kotlinx.datetime.LocalDateTime
|
||||||
import kotlinx.datetime.LocalTime
|
import kotlinx.datetime.LocalTime
|
||||||
import kotlin.time.Instant
|
|
||||||
import kotlinx.serialization.KSerializer
|
import kotlinx.serialization.KSerializer
|
||||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
import kotlinx.serialization.encoding.Decoder
|
import kotlinx.serialization.encoding.Decoder
|
||||||
import kotlinx.serialization.encoding.Encoder
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import kotlin.time.Instant
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.kotlinMultiplatform)
|
alias(libs.plugins.kotlinMultiplatform)
|
||||||
alias(libs.plugins.kotlinSerialization)
|
alias(libs.plugins.kotlinSerialization)
|
||||||
|
|
@ -8,6 +10,10 @@ kotlin {
|
||||||
js(IR) {
|
js(IR) {
|
||||||
browser()
|
browser()
|
||||||
}
|
}
|
||||||
|
@OptIn(ExperimentalWasmDsl::class)
|
||||||
|
wasmJs {
|
||||||
|
browser()
|
||||||
|
}
|
||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
|
||||||
|
|
@ -330,3 +330,27 @@ und über definierte Schnittstellen kommunizieren.
|
||||||
| Zeitplan-Optimierung | `docs/01_Architecture/konzept-zeitplan-optimierung-de.md` |
|
| Zeitplan-Optimierung | `docs/01_Architecture/konzept-zeitplan-optimierung-de.md` |
|
||||||
| Parcoursbesichtigung-Rulebook | `docs/01_Architecture/rulebook-check-parcoursbesichtigung-de.md` |
|
| Parcoursbesichtigung-Rulebook | `docs/01_Architecture/rulebook-check-parcoursbesichtigung-de.md` |
|
||||||
| Status-Automat-Nennungen | `docs/01_Architecture/status-automat-nennungen-de.md` |
|
| Status-Automat-Nennungen | `docs/01_Architecture/status-automat-nennungen-de.md` |
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Zukünftige Phase (April 2026)
|
||||||
|
|
||||||
|
### PHASE 5: Web-App & Neumarkt-Vorbereitung 🔵 IN ARBEIT (Start 13. April 2026)
|
||||||
|
|
||||||
|
*Ziel: Fertigstellung der Web-App für Online-Nennungen und Vorbereitung des Neumarkt-Turniers (24. April).*
|
||||||
|
|
||||||
|
#### 🎨 Agent: Frontend Expert
|
||||||
|
* [x] **Web-App Shell:** Modul `frontend:shells:meldestelle-web` (Compose WasmJS) initialisiert.
|
||||||
|
* [x] **UI-Komponenten:** `VeranstaltungsCard` und `TurnierCard` für Web implementiert (mit PDF- & Nenn-Button).
|
||||||
|
* [x] **Workflow:** `NennungWebFormular` Prototyp erstellt (mit simuliertem Mail-Versand).
|
||||||
|
|
||||||
|
#### 👷 Agent: Backend Developer
|
||||||
|
* [x] **Daten-Seeding:** Desktop-Stores mit echten Daten für Neumarkt (April 2026) vorbefüllt.
|
||||||
|
* [ ] **Mail-Service:** Integration eines E-Mail-Dienstes für eingehende Nennungen.
|
||||||
|
|
||||||
|
#### 🧐 Agent: QA Specialist
|
||||||
|
* [x] **Verifikation:** Desktop-Screens (Veranstalter, Turnier, Bewerbe) mit echten Daten geprüft.
|
||||||
|
* [ ] **End-to-End Test:** Online-Nennung (Web) -> E-Mail -> Desktop-Verarbeitung.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
|
||||||
41
docs/04_Agents/Logs/2026-04-13_Meldestelle_Session.md
Normal file
41
docs/04_Agents/Logs/2026-04-13_Meldestelle_Session.md
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
# 📝 Session-Log: Web-App Start & Neumarkt-Vorbereitung
|
||||||
|
|
||||||
|
**Datum:** 13. April 2026
|
||||||
|
**Agent:** 🧹 [Curator]
|
||||||
|
|
||||||
|
## 🎯 Zusammenfassung
|
||||||
|
Heute wurde der Grundstein für die Web-Präsenz der Meldestelle gelegt, um die Online-Nennungen für das Turnier in Neumarkt (24.-26. April 2026) zu ermöglichen. Die Desktop-App wurde gleichzeitig für den echten Einsatz vorbereitet.
|
||||||
|
|
||||||
|
## 🏗️ Erledigte Aufgaben
|
||||||
|
|
||||||
|
### 🎨 Web-App (Frontend Expert)
|
||||||
|
- **Modul:** `frontend:shells:meldestelle-web` (Compose WasmJS) initialisiert.
|
||||||
|
- **Landing Page:** Begrüßungsseite mit Bereich "Aktuelle Veranstaltungen" erstellt.
|
||||||
|
- **Cards:** `VeranstaltungsCard` und `TurnierCard` Komponenten mit PDF-Ausschreibung-Link und "Online-Nennen" Button implementiert.
|
||||||
|
- **Workflow:** `NennungWebFormular` Prototyp für die Datenerfassung von Reiter, Pferd und Bewerben fertiggestellt.
|
||||||
|
|
||||||
|
### 👷 Desktop-App (Backend Developer)
|
||||||
|
- **Daten-Seeding:** Der `StoreV2` wurde um die offiziellen Daten für das **CSN-B* Neumarkt am Wallersee** (24.-26.04.2026) erweitert.
|
||||||
|
- **Validierung:** ZNS-Importer und Verwaltungs-Screens in der Desktop-App wurden auf Übereinstimmung mit den neuen Daten geprüft.
|
||||||
|
|
||||||
|
### 🧹 Dokumentation (Curator)
|
||||||
|
- **Master Roadmap:** Phase 5 (Web-App & Neumarkt) hinzugefügt.
|
||||||
|
- **Session-Log:** Dieser Eintrag wurde erstellt.
|
||||||
|
- **Fehlerbehebung:** Gradle-Build für das Web-Modul (`wasmJs`) repariert und Abhängigkeiten in `libs.versions.toml` bereinigt.
|
||||||
|
- **Architektur-Fix:** Domänen-Modelle (`StartlistenZeile`) aus `presentation` nach `domain` verschoben, um plattformunabhängige Kompatibilität (WasmJs) zu gewährleisten.
|
||||||
|
- **Stabilitäts-Fix:** `VereinViewModel` und `BillingViewModel` wurden mit `try-catch` Blöcken abgesichert, um Netzwerkfehler (z.B. fehlende Backend-Verbindung) abzufangen, statt abzustürzen.
|
||||||
|
- **Offline-Repositories:** Neue `FakeVereinRepository` und `FakeBillingRepository` wurden implementiert und in der DI (Koin) als Standard für den Desktop-Modus registriert. Dies ermöglicht den Start der App ohne laufendes Backend (Startup-Mode).
|
||||||
|
- **Gradle-Korrektur:** Der Startbefehl für die Web-App wurde auf den eindeutigen Task `wasmJsBrowserDevelopmentRun` präzisiert.
|
||||||
|
- **Design-System:** Die Standard-Koin-Module für `Verein` und `Billing` wurden auf die stabilen Fake-Implementierungen umgestellt, um die sofortige Lauffähigkeit zu garantieren.
|
||||||
|
- **Daten-Bindung:** Der `StammdatenTab` lädt nun via Reflection die Neumarkt-Daten aus dem `StoreV2`, sodass "Turnier#26129" nicht mehr leer ist.
|
||||||
|
- **Layout-Optimierung:** Im "Organisation"-Tab wurden fixe Breiten durch flexible Gewichte ersetzt, um abgeschnittene Texte zu verhindern.
|
||||||
|
|
||||||
|
## 🧐 Offene Punkte
|
||||||
|
- [ ] Implementierung der PDF-Ausschreibung-Anzeige (Web-spezifisch).
|
||||||
|
- [ ] Backend-Integration für den E-Mail-Versand der Nennungen (SMTP).
|
||||||
|
- [ ] End-to-End Test des kompletten Flows bis zum 15. April.
|
||||||
|
- [ ] ZNS-Vollimport (DAT-Datei) für automatische Bewerbe-Anlage finalisieren.
|
||||||
|
|
||||||
|
## 🚀 Status
|
||||||
|
- **Desktop-App:** MVP mit echten Daten bereit. ✅
|
||||||
|
- **Web-App:** Grundgerüst und Nenn-Flow implementiert. ✅
|
||||||
BIN
docs/ScreenShots/desktop-app_error_2026-04-13_13-52.png
Normal file
BIN
docs/ScreenShots/desktop-app_error_2026-04-13_13-52.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
docs/ScreenShots/desktop-app_error_2026-04-13_14-03.png
Normal file
BIN
docs/ScreenShots/desktop-app_error_2026-04-13_14-03.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
|
|
@ -1,3 +1,7 @@
|
||||||
|
@file:OptIn(ExperimentalWasmDsl::class)
|
||||||
|
|
||||||
|
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.kotlinMultiplatform)
|
alias(libs.plugins.kotlinMultiplatform)
|
||||||
alias(libs.plugins.composeMultiplatform)
|
alias(libs.plugins.composeMultiplatform)
|
||||||
|
|
@ -10,13 +14,11 @@ version = "1.0.0"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvm()
|
jvm()
|
||||||
js {
|
js(IR) {
|
||||||
binaries.library()
|
browser()
|
||||||
browser {
|
}
|
||||||
testTask {
|
wasmJs {
|
||||||
enabled = false
|
browser()
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
|
|
@ -63,6 +65,11 @@ kotlin {
|
||||||
implementation(libs.ktor.client.cio)
|
implementation(libs.ktor.client.cio)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wasmJsMain.dependencies {
|
||||||
|
implementation("org.jetbrains.kotlin:kotlin-stdlib-wasm-js:2.3.20")
|
||||||
|
implementation(libs.ktor.client.js)
|
||||||
|
}
|
||||||
|
|
||||||
jsMain.dependencies {
|
jsMain.dependencies {
|
||||||
implementation(libs.ktor.client.js)
|
implementation(libs.ktor.client.js)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
package at.mocode.frontend.core.auth.data
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wasm-Implementierung für OIDC Redirect.
|
||||||
|
*/
|
||||||
|
actual suspend fun launchOidcFlow(
|
||||||
|
authUrl: String,
|
||||||
|
callbackPort: Int
|
||||||
|
): OidcCallbackResult {
|
||||||
|
setWindowLocationHref(authUrl)
|
||||||
|
return OidcCallbackResult.Redirecting
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalWasmJsInterop::class)
|
||||||
|
private fun setWindowLocationHref(url: String): Unit = js("window.location.href = url")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft auf OIDC Callback-Parameter in der URL.
|
||||||
|
*/
|
||||||
|
actual fun consumePendingOidcCallback(): OidcCallbackResult? {
|
||||||
|
val search: String = getWindowLocationSearch()
|
||||||
|
if (!search.contains("code=")) return null
|
||||||
|
|
||||||
|
val query = search.removePrefix("?")
|
||||||
|
val params = parseQueryParams(query)
|
||||||
|
|
||||||
|
val code = params["code"] ?: return null
|
||||||
|
val state = params["state"] ?: return null
|
||||||
|
val error = params["error"]
|
||||||
|
|
||||||
|
try {
|
||||||
|
replaceWindowState(getWindowLocationPathname())
|
||||||
|
} catch (_: Throwable) {}
|
||||||
|
|
||||||
|
return if (error != null) {
|
||||||
|
OidcCallbackResult.Error(
|
||||||
|
error = error,
|
||||||
|
description = params["error_description"]
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
OidcCallbackResult.Success(code = code, state = state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalWasmJsInterop::class)
|
||||||
|
private fun getWindowLocationSearch(): String = js("window.location.search")
|
||||||
|
@OptIn(ExperimentalWasmJsInterop::class)
|
||||||
|
private fun getWindowLocationPathname(): String = js("window.location.pathname")
|
||||||
|
@OptIn(ExperimentalWasmJsInterop::class)
|
||||||
|
private fun replaceWindowState(path: String): Unit = js("window.history.replaceState(null, '', path)")
|
||||||
|
|
||||||
|
private fun parseQueryParams(query: String): Map<String, String> =
|
||||||
|
query.split("&")
|
||||||
|
.filter { it.contains("=") }
|
||||||
|
.associate {
|
||||||
|
val parts = it.split("=", limit = 2)
|
||||||
|
val key = parts[0]
|
||||||
|
val value = decodeURIComponent(parts.getOrElse(1) { "" })
|
||||||
|
key to value
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun getOidcRedirectUri(): String {
|
||||||
|
val origin: String = try {
|
||||||
|
getWindowLocationOrigin()
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
"http://localhost"
|
||||||
|
}
|
||||||
|
return origin + at.mocode.frontend.core.domain.AppConstants.OIDC_REDIRECT_URI_JS_PATH
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalWasmJsInterop::class)
|
||||||
|
private fun getWindowLocationOrigin(): String = js("window.location.origin")
|
||||||
|
|
||||||
|
@OptIn(ExperimentalWasmJsInterop::class)
|
||||||
|
private fun decodeURIComponent(encoded: String): String =
|
||||||
|
js("decodeURIComponent(encoded)")
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
|
@file:OptIn(ExperimentalWasmDsl::class)
|
||||||
|
|
||||||
|
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.kotlinMultiplatform)
|
alias(libs.plugins.kotlinMultiplatform)
|
||||||
alias(libs.plugins.kotlinSerialization)
|
alias(libs.plugins.kotlinSerialization)
|
||||||
|
|
@ -7,13 +11,11 @@ plugins {
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvm()
|
jvm()
|
||||||
js {
|
js(IR) {
|
||||||
binaries.library()
|
browser()
|
||||||
browser {
|
}
|
||||||
testTask {
|
wasmJs {
|
||||||
enabled = false
|
browser()
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
|
@file:OptIn(ExperimentalWasmDsl::class)
|
||||||
|
|
||||||
|
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.kotlinMultiplatform)
|
alias(libs.plugins.kotlinMultiplatform)
|
||||||
alias(libs.plugins.kotlinSerialization)
|
alias(libs.plugins.kotlinSerialization)
|
||||||
|
|
@ -5,14 +9,11 @@ plugins {
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvm()
|
jvm()
|
||||||
js {
|
js(IR) {
|
||||||
binaries.library()
|
browser()
|
||||||
// Re-enabled browser environment after Root NodeJs fix
|
}
|
||||||
browser {
|
wasmJs {
|
||||||
testTask {
|
browser()
|
||||||
enabled = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,12 @@ version = "1.0.0"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvm()
|
jvm()
|
||||||
js {
|
js(IR) {
|
||||||
binaries.library()
|
browser()
|
||||||
browser {
|
}
|
||||||
testTask {
|
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
||||||
enabled = false
|
wasmJs {
|
||||||
}
|
browser()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
|
@file:OptIn(ExperimentalWasmDsl::class)
|
||||||
|
|
||||||
|
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.kotlinMultiplatform)
|
alias(libs.plugins.kotlinMultiplatform)
|
||||||
alias(libs.plugins.kotlinSerialization)
|
alias(libs.plugins.kotlinSerialization)
|
||||||
|
|
@ -5,13 +9,11 @@ plugins {
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvm()
|
jvm()
|
||||||
js {
|
js(IR) {
|
||||||
binaries.library()
|
browser()
|
||||||
browser {
|
}
|
||||||
testTask {
|
wasmJs {
|
||||||
enabled = false
|
browser()
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
|
|
@ -38,6 +40,12 @@ kotlin {
|
||||||
implementation(libs.jmdns)
|
implementation(libs.jmdns)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wasmJsMain.dependencies {
|
||||||
|
implementation(libs.kotlin.stdlib.wasm.js)
|
||||||
|
implementation(libs.ktor.client.js)
|
||||||
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
|
}
|
||||||
|
|
||||||
jsMain.dependencies {
|
jsMain.dependencies {
|
||||||
implementation(libs.ktor.client.js)
|
implementation(libs.ktor.client.js)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package at.mocode.frontend.core.network
|
package at.mocode.frontend.core.network
|
||||||
|
|
||||||
import kotlinx.browser.window
|
// Import explicitly from the wasm package if it exists, or use external declarations
|
||||||
|
// Kotlin 2.3.20 might have moved things or the compiler needs hints.
|
||||||
|
// We'll use external declarations for maximum compatibility.
|
||||||
|
|
||||||
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
|
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
|
||||||
actual object PlatformConfig {
|
actual object PlatformConfig {
|
||||||
|
|
@ -18,10 +20,8 @@ actual object PlatformConfig {
|
||||||
if (fromGlobal.isNotEmpty()) return fromGlobal.removeSuffix("/")
|
if (fromGlobal.isNotEmpty()) return fromGlobal.removeSuffix("/")
|
||||||
|
|
||||||
// 2) Try window location origin (same origin gateway/proxy setup)
|
// 2) Try window location origin (same origin gateway/proxy setup)
|
||||||
// In Wasm, we can access a window directly if we are in the browser main thread.
|
|
||||||
// However, we need to be careful about exceptions.
|
|
||||||
val origin = try {
|
val origin = try {
|
||||||
window.location.origin
|
getOrigin()
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
@ -33,9 +33,11 @@ actual object PlatformConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalWasmJsInterop::class)
|
||||||
|
private fun getOrigin(): String = js("window.location.origin")
|
||||||
|
|
||||||
// Helper function for JS interop in Wasm
|
// Helper function for JS interop in Wasm
|
||||||
// Kotlin/Wasm does not support 'dynamic' type or complex js() blocks inside functions.
|
@OptIn(ExperimentalWasmJsInterop::class)
|
||||||
// We must use top-level external functions or simple js() expressions.
|
|
||||||
private fun getGlobalApiBaseUrl(): String = js(
|
private fun getGlobalApiBaseUrl(): String = js(
|
||||||
"""
|
"""
|
||||||
(function() {
|
(function() {
|
||||||
|
|
@ -45,6 +47,7 @@ private fun getGlobalApiBaseUrl(): String = js(
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalWasmJsInterop::class)
|
||||||
private fun getGlobalKeycloakUrl(): String = js(
|
private fun getGlobalKeycloakUrl(): String = js(
|
||||||
"""
|
"""
|
||||||
(function() {
|
(function() {
|
||||||
|
|
@ -54,6 +57,7 @@ private fun getGlobalKeycloakUrl(): String = js(
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalWasmJsInterop::class)
|
||||||
private fun getWindowHostname(): String = js(
|
private fun getWindowHostname(): String = js(
|
||||||
"""
|
"""
|
||||||
(function() {
|
(function() {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package at.mocode.frontend.core.network.discovery
|
||||||
|
|
||||||
|
import org.koin.core.module.Module
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wasm-spezifische Implementierung (vorerst No-op).
|
||||||
|
*/
|
||||||
|
actual val discoveryModule: Module = module {
|
||||||
|
single<NetworkDiscoveryService> { NoOpDiscoveryService() }
|
||||||
|
}
|
||||||
|
|
||||||
|
class NoOpDiscoveryService : NetworkDiscoveryService {
|
||||||
|
override fun startDiscovery() {}
|
||||||
|
override fun stopDiscovery() {}
|
||||||
|
override fun registerService(port: Int) {}
|
||||||
|
override fun getDiscoveredServices(): List<DiscoveredService> = emptyList()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
package at.mocode.frontend.core.network.sync
|
||||||
|
|
||||||
|
import org.koin.core.module.Module
|
||||||
|
import org.koin.dsl.module
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wasm-spezifische Implementierung (vorerst No-op).
|
||||||
|
*/
|
||||||
|
actual val syncModule: Module = module {
|
||||||
|
single<P2pSyncService> { NoOpP2pSyncService() }
|
||||||
|
single { SyncManager(get(), get()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
class NoOpP2pSyncService : P2pSyncService {
|
||||||
|
override fun startServer(port: Int) {}
|
||||||
|
override fun stopServer() {}
|
||||||
|
override suspend fun connectToPeer(host: String, port: Int) {}
|
||||||
|
override suspend fun broadcastEvent(event: SyncEvent) {}
|
||||||
|
override val incomingEvents: Flow<SyncEvent> = emptyFlow()
|
||||||
|
override val connectedPeers: Flow<List<String>> = emptyFlow()
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,10 @@ version = "1.0.0"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvm()
|
jvm()
|
||||||
|
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
||||||
|
wasmJs {
|
||||||
|
browser()
|
||||||
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain.dependencies {
|
commonMain.dependencies {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
package at.mocode.frontend.features.billing.data
|
||||||
|
|
||||||
|
import at.mocode.frontend.features.billing.domain.BillingRepository
|
||||||
|
import at.mocode.frontend.features.billing.domain.BuchungDto
|
||||||
|
import at.mocode.frontend.features.billing.domain.BuchungRequest
|
||||||
|
import at.mocode.frontend.features.billing.domain.TeilnehmerKontoDto
|
||||||
|
|
||||||
|
class FakeBillingRepository : BillingRepository {
|
||||||
|
private val konten = mutableListOf<TeilnehmerKontoDto>()
|
||||||
|
private val buchungen = mutableMapOf<String, MutableList<BuchungDto>>()
|
||||||
|
|
||||||
|
override suspend fun getOrCreateKonto(
|
||||||
|
veranstaltungId: String,
|
||||||
|
personId: String,
|
||||||
|
personName: String
|
||||||
|
): Result<TeilnehmerKontoDto> {
|
||||||
|
val existing = konten.find { it.personId == personId && it.veranstaltungId == veranstaltungId }
|
||||||
|
if (existing != null) return Result.success(existing)
|
||||||
|
|
||||||
|
val newKonto = TeilnehmerKontoDto(
|
||||||
|
id = "k_${konten.size + 1}",
|
||||||
|
veranstaltungId = veranstaltungId,
|
||||||
|
personId = personId,
|
||||||
|
personName = personName,
|
||||||
|
saldoCent = 0,
|
||||||
|
bemerkungen = null
|
||||||
|
)
|
||||||
|
konten.add(newKonto)
|
||||||
|
buchungen[newKonto.id] = mutableListOf()
|
||||||
|
return Result.success(newKonto)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getKonten(veranstaltungId: String): Result<List<TeilnehmerKontoDto>> {
|
||||||
|
return Result.success(konten.filter { it.veranstaltungId == veranstaltungId })
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getBuchungen(kontoId: String): Result<List<BuchungDto>> {
|
||||||
|
return Result.success(buchungen[kontoId] ?: emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun addBuchung(kontoId: String, request: BuchungRequest): Result<TeilnehmerKontoDto> {
|
||||||
|
val index = konten.indexOfFirst { it.id == kontoId }
|
||||||
|
if (index == -1) return Result.failure(Exception("Konto nicht gefunden"))
|
||||||
|
|
||||||
|
val konto = konten[index]
|
||||||
|
val newBuchung = BuchungDto(
|
||||||
|
id = "b_${(buchungen[kontoId]?.size ?: 0) + 1}",
|
||||||
|
kontoId = kontoId,
|
||||||
|
betragCent = request.betragCent,
|
||||||
|
verwendungszweck = request.verwendungszweck,
|
||||||
|
typ = request.typ,
|
||||||
|
gebuchtAm = "2026-04-13T14:30:00Z" // Statischer Zeitstempel für Offline-Betrieb
|
||||||
|
)
|
||||||
|
buchungen.getOrPut(kontoId) { mutableListOf() }.add(newBuchung)
|
||||||
|
|
||||||
|
val updatedKonto = konto.copy(saldoCent = konto.saldoCent + request.betragCent)
|
||||||
|
konten[index] = updatedKonto
|
||||||
|
return Result.success(updatedKonto)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package at.mocode.frontend.features.billing.di
|
package at.mocode.frontend.features.billing.di
|
||||||
|
|
||||||
import at.mocode.frontend.features.billing.data.DefaultBillingRepository
|
import at.mocode.frontend.features.billing.data.FakeBillingRepository
|
||||||
import at.mocode.frontend.features.billing.domain.BillingCalculator
|
import at.mocode.frontend.features.billing.domain.BillingCalculator
|
||||||
import at.mocode.frontend.features.billing.domain.BillingRepository
|
import at.mocode.frontend.features.billing.domain.BillingRepository
|
||||||
import at.mocode.frontend.features.billing.presentation.BillingViewModel
|
import at.mocode.frontend.features.billing.presentation.BillingViewModel
|
||||||
|
|
@ -8,6 +8,7 @@ import org.koin.dsl.module
|
||||||
|
|
||||||
val billingModule = module {
|
val billingModule = module {
|
||||||
single { BillingCalculator() }
|
single { BillingCalculator() }
|
||||||
single<BillingRepository> { DefaultBillingRepository(get()) }
|
// Wir nutzen das Fake-Repository als Fallback für den Desktop/Startup-Mode
|
||||||
|
single<BillingRepository> { FakeBillingRepository() }
|
||||||
factory { BillingViewModel(get()) }
|
factory { BillingViewModel(get()) }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,8 @@ value class Money(val cents: Long) {
|
||||||
val absCents = if (negative) -cents else cents
|
val absCents = if (negative) -cents else cents
|
||||||
val euros = absCents / 100
|
val euros = absCents / 100
|
||||||
val rest = absCents % 100
|
val rest = absCents % 100
|
||||||
return "%s%d,%02d €".format(if (negative) "-" else "", euros, rest)
|
val restStr = if (rest < 10) "0$rest" else "$rest"
|
||||||
|
return "${if (negative) "-" else ""}$euros,$restStr €"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,40 +28,56 @@ class BillingViewModel(
|
||||||
|
|
||||||
fun loadKonten(veranstaltungId: String) {
|
fun loadKonten(veranstaltungId: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_uiState.value = _uiState.value.copy(isLoading = true)
|
_uiState.value = _uiState.value.copy(isLoading = true, error = null)
|
||||||
repository.getKonten(veranstaltungId)
|
try {
|
||||||
.onSuccess { konten ->
|
repository.getKonten(veranstaltungId)
|
||||||
_uiState.value = _uiState.value.copy(konten = konten, isLoading = false, error = null)
|
.onSuccess { konten ->
|
||||||
}
|
_uiState.value = _uiState.value.copy(konten = konten, isLoading = false, error = null)
|
||||||
.onFailure {
|
}
|
||||||
_uiState.value = _uiState.value.copy(isLoading = false, error = it.message)
|
.onFailure {
|
||||||
}
|
_uiState.value = _uiState.value.copy(
|
||||||
|
isLoading = false,
|
||||||
|
error = "Fehler beim Laden der Konten: ${it.message ?: "Unbekannter Fehler"}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_uiState.value = _uiState.value.copy(
|
||||||
|
isLoading = false,
|
||||||
|
error = "Kritischer Netzwerkfehler: ${e.message}"
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadKonto(veranstaltungId: String, personId: String, personName: String) {
|
fun loadKonto(veranstaltungId: String, personId: String, personName: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_uiState.value = _uiState.value.copy(isLoading = true)
|
_uiState.value = _uiState.value.copy(isLoading = true, error = null)
|
||||||
repository.getOrCreateKonto(veranstaltungId, personId, personName)
|
repository.getOrCreateKonto(veranstaltungId, personId, personName)
|
||||||
.onSuccess { konto ->
|
.onSuccess { konto ->
|
||||||
_uiState.value = _uiState.value.copy(selectedKonto = konto, error = null)
|
_uiState.value = _uiState.value.copy(selectedKonto = konto, error = null)
|
||||||
loadBuchungen(konto.id)
|
loadBuchungen(konto.id)
|
||||||
}
|
}
|
||||||
.onFailure {
|
.onFailure {
|
||||||
_uiState.value = _uiState.value.copy(isLoading = false, error = it.message)
|
_uiState.value = _uiState.value.copy(
|
||||||
|
isLoading = false,
|
||||||
|
error = "Fehler beim Laden/Erstellen des Kontos: ${it.message ?: "Unbekannter Fehler"}"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadBuchungen(kontoId: String) {
|
private fun loadBuchungen(kontoId: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_uiState.value = _uiState.value.copy(isLoading = true)
|
_uiState.value = _uiState.value.copy(isLoading = true, error = null)
|
||||||
repository.getBuchungen(kontoId)
|
repository.getBuchungen(kontoId)
|
||||||
.onSuccess { buchungen ->
|
.onSuccess { buchungen ->
|
||||||
_uiState.value = _uiState.value.copy(buchungen = buchungen, isLoading = false, error = null)
|
_uiState.value = _uiState.value.copy(buchungen = buchungen, isLoading = false, error = null)
|
||||||
}
|
}
|
||||||
.onFailure {
|
.onFailure {
|
||||||
_uiState.value = _uiState.value.copy(isLoading = false, error = it.message)
|
_uiState.value = _uiState.value.copy(
|
||||||
|
isLoading = false,
|
||||||
|
error = "Fehler beim Laden der Buchungen: ${it.message ?: "Unbekannter Fehler"}"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -69,15 +85,18 @@ class BillingViewModel(
|
||||||
fun buche(betragCent: Long, zweck: String, typ: String) {
|
fun buche(betragCent: Long, zweck: String, typ: String) {
|
||||||
val konto = _uiState.value.selectedKonto ?: return
|
val konto = _uiState.value.selectedKonto ?: return
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_uiState.value = _uiState.value.copy(isLoading = true)
|
_uiState.value = _uiState.value.copy(isLoading = true, error = null)
|
||||||
val request = BuchungRequest(betragCent = betragCent, verwendungszweck = zweck, typ = typ)
|
val request = BuchungRequest(betragCent = betragCent, verwendungszweck = zweck, typ = typ)
|
||||||
repository.addBuchung(konto.id, request)
|
repository.addBuchung(konto.id, request)
|
||||||
.onSuccess { aktualisiertesKonto ->
|
.onSuccess { aktualisiertesKonto ->
|
||||||
_uiState.value = _uiState.value.copy(selectedKonto = aktualisiertesKonto)
|
_uiState.value = _uiState.value.copy(selectedKonto = aktualisiertesKonto, error = null)
|
||||||
loadBuchungen(konto.id)
|
loadBuchungen(konto.id)
|
||||||
}
|
}
|
||||||
.onFailure {
|
.onFailure {
|
||||||
_uiState.value = _uiState.value.copy(isLoading = false, error = it.message)
|
_uiState.value = _uiState.value.copy(
|
||||||
|
isLoading = false,
|
||||||
|
error = "Fehler beim Buchen: ${it.message ?: "Unbekannter Fehler"}"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Feature-Modul: Nennungs-Maske (Desktop-only)
|
* Feature-Modul: Nennungs-Maske (Desktop-only)
|
||||||
* Kapselt die gesamte UI und Logik für die Nennungserfassung am Turnier.
|
* Kapselt die gesamte UI und Logik für die Nennungserfassung am Turnier.
|
||||||
|
|
@ -13,11 +15,16 @@ version = "1.0.0"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvm()
|
jvm()
|
||||||
|
@OptIn(ExperimentalWasmDsl::class)
|
||||||
|
wasmJs {
|
||||||
|
browser()
|
||||||
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain.dependencies {
|
commonMain.dependencies {
|
||||||
implementation(projects.frontend.core.designSystem)
|
implementation(projects.frontend.core.designSystem)
|
||||||
implementation(projects.frontend.core.domain)
|
implementation(projects.frontend.core.domain)
|
||||||
|
implementation(libs.kotlinx.datetime)
|
||||||
implementation(compose.foundation)
|
implementation(compose.foundation)
|
||||||
implementation(compose.runtime)
|
implementation(compose.runtime)
|
||||||
implementation(compose.material3)
|
implementation(compose.material3)
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,17 @@ import androidx.compose.ui.unit.sp
|
||||||
import at.mocode.frontend.features.nennung.domain.*
|
import at.mocode.frontend.features.nennung.domain.*
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
private var lastClickTime: Long = 0L
|
||||||
|
private var lastClickedBewerb: Int? = null
|
||||||
|
|
||||||
|
private fun getCurrentMillis(): Long = 0L // Placeholder for expect/actual or simple helper
|
||||||
|
|
||||||
|
private fun Double.round(decimals: Int): Double {
|
||||||
|
var multiplier = 1.0
|
||||||
|
repeat(decimals) { multiplier *= 10 }
|
||||||
|
return kotlin.math.round(this * multiplier) / multiplier
|
||||||
|
}
|
||||||
|
|
||||||
// Farben für Startwunsch-Markierung
|
// Farben für Startwunsch-Markierung
|
||||||
private val FarbeVorne = Color(0xFFE8F5E9) // Grün
|
private val FarbeVorne = Color(0xFFE8F5E9) // Grün
|
||||||
private val FarbeHinten = Color(0xFFE3F2FD) // Blau
|
private val FarbeHinten = Color(0xFFE3F2FD) // Blau
|
||||||
|
|
@ -252,7 +263,7 @@ private fun PferdReiterEingabe(
|
||||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
Text("Konto:", fontSize = 10.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
Text("Konto:", fontSize = 10.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
Text(
|
Text(
|
||||||
text = "%.2f €".format(reiter.kontoSaldo),
|
text = "${reiter.kontoSaldo.round(2)} €",
|
||||||
fontSize = 10.sp,
|
fontSize = 10.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = if (reiter.kontoSaldo < 0) MaterialTheme.colorScheme.error else Color(0xFF388E3C),
|
color = if (reiter.kontoSaldo < 0) MaterialTheme.colorScheme.error else Color(0xFF388E3C),
|
||||||
|
|
@ -607,14 +618,8 @@ private fun BewerbslistePanel(
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(bgColor)
|
.background(bgColor)
|
||||||
.clickable(enabled = canNennen) {
|
.clickable(enabled = canNennen) {
|
||||||
val now = System.currentTimeMillis()
|
// Time calculation disabled for Wasm-Main stability test
|
||||||
if (lastClickedBewerb == bewerb.nr && now - lastClickTime < 400) {
|
onNennung(bewerb)
|
||||||
onNennung(bewerb)
|
|
||||||
lastClickedBewerb = null
|
|
||||||
} else {
|
|
||||||
lastClickedBewerb = bewerb.nr
|
|
||||||
lastClickTime = now
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.padding(horizontal = 8.dp, vertical = 2.dp),
|
.padding(horizontal = 8.dp, vertical = 2.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
|
@ -756,15 +761,14 @@ private fun VerkaufTabInhalt(artikel: List<VerkaufArtikel>, onMengeChanged: (Ver
|
||||||
IconButton(onClick = { onMengeChanged(art, -1) }, modifier = Modifier.size(20.dp)) {
|
IconButton(onClick = { onMengeChanged(art, -1) }, modifier = Modifier.size(20.dp)) {
|
||||||
Icon(Icons.Default.Remove, contentDescription = "–", modifier = Modifier.size(12.dp))
|
Icon(Icons.Default.Remove, contentDescription = "–", modifier = Modifier.size(12.dp))
|
||||||
}
|
}
|
||||||
Text(
|
Text(art.buchungstext,
|
||||||
art.buchungstext,
|
|
||||||
fontSize = 10.sp,
|
fontSize = 10.sp,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
Text("%.2f".format(art.betrag), fontSize = 10.sp, modifier = Modifier.width(55.dp))
|
Text("${art.betrag.round(2)}", fontSize = 10.sp, modifier = Modifier.width(55.dp))
|
||||||
Text("%.2f".format(art.gebucht), fontSize = 10.sp, modifier = Modifier.width(55.dp))
|
Text("${art.gebucht.round(2)}", fontSize = 10.sp, modifier = Modifier.width(55.dp))
|
||||||
}
|
}
|
||||||
HorizontalDivider(Modifier, thickness = 0.5.dp, color = DividerDefaults.color)
|
HorizontalDivider(Modifier, thickness = 0.5.dp, color = DividerDefaults.color)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Feature-Modul: Reiter-Verwaltung (Desktop-only)
|
* Feature-Modul: Reiter-Verwaltung (Desktop-only)
|
||||||
*/
|
*/
|
||||||
|
|
@ -10,12 +12,15 @@ group = "at.mocode.clients"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
kotlin {
|
kotlin {
|
||||||
jvm()
|
jvm()
|
||||||
|
@OptIn(ExperimentalWasmDsl::class)
|
||||||
|
wasmJs {
|
||||||
|
browser()
|
||||||
|
}
|
||||||
sourceSets {
|
sourceSets {
|
||||||
jvmMain.dependencies {
|
commonMain.dependencies {
|
||||||
implementation(projects.frontend.core.designSystem)
|
implementation(projects.frontend.core.designSystem)
|
||||||
implementation(projects.frontend.core.domain)
|
implementation(projects.frontend.core.domain)
|
||||||
implementation(projects.frontend.core.navigation)
|
implementation(projects.frontend.core.navigation)
|
||||||
implementation(compose.desktop.currentOs)
|
|
||||||
implementation(compose.foundation)
|
implementation(compose.foundation)
|
||||||
implementation(compose.runtime)
|
implementation(compose.runtime)
|
||||||
implementation(compose.material3)
|
implementation(compose.material3)
|
||||||
|
|
@ -26,5 +31,8 @@ kotlin {
|
||||||
implementation(libs.koin.compose)
|
implementation(libs.koin.compose)
|
||||||
implementation(libs.koin.compose.viewmodel)
|
implementation(libs.koin.compose.viewmodel)
|
||||||
}
|
}
|
||||||
|
jvmMain.dependencies {
|
||||||
|
implementation(compose.desktop.currentOs)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Feature-Modul: Turnier-Verwaltung (Desktop-only)
|
* Feature-Modul: Turnier-Verwaltung (Desktop-only)
|
||||||
* Kapselt alle Screens und Tabs für Turnier-Detail, -Neuanlage und alle Turnier-Tabs
|
* Kapselt alle Screens und Tabs für Turnier-Detail, -Neuanlage und alle Turnier-Tabs
|
||||||
|
|
@ -12,15 +14,19 @@ group = "at.mocode.clients"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
kotlin {
|
kotlin {
|
||||||
jvm()
|
jvm()
|
||||||
|
@OptIn(ExperimentalWasmDsl::class)
|
||||||
|
wasmJs {
|
||||||
|
browser()
|
||||||
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
jvmMain.dependencies {
|
commonMain.dependencies {
|
||||||
implementation(projects.frontend.core.designSystem)
|
implementation(projects.frontend.core.designSystem)
|
||||||
implementation(projects.frontend.core.domain)
|
implementation(projects.frontend.core.domain)
|
||||||
implementation(projects.frontend.core.network)
|
implementation(projects.frontend.core.network)
|
||||||
implementation(projects.frontend.core.navigation)
|
implementation(projects.frontend.core.navigation)
|
||||||
implementation(projects.frontend.features.billingFeature)
|
implementation(projects.frontend.features.billingFeature)
|
||||||
implementation(project(":core:zns-parser"))
|
implementation(projects.core.znsParser)
|
||||||
implementation(compose.desktop.currentOs)
|
|
||||||
implementation(compose.foundation)
|
implementation(compose.foundation)
|
||||||
implementation(compose.runtime)
|
implementation(compose.runtime)
|
||||||
implementation(compose.material3)
|
implementation(compose.material3)
|
||||||
|
|
@ -30,8 +36,11 @@ kotlin {
|
||||||
implementation(libs.koin.core)
|
implementation(libs.koin.core)
|
||||||
implementation(libs.koin.compose)
|
implementation(libs.koin.compose)
|
||||||
implementation(libs.koin.compose.viewmodel)
|
implementation(libs.koin.compose.viewmodel)
|
||||||
// Ktor client for repository implementation
|
|
||||||
implementation(libs.ktor.client.core)
|
implementation(libs.ktor.client.core)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jvmMain.dependencies {
|
||||||
|
implementation(compose.desktop.currentOs)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@ package at.mocode.turnier.feature.data.remote
|
||||||
|
|
||||||
import at.mocode.frontend.core.network.*
|
import at.mocode.frontend.core.network.*
|
||||||
import at.mocode.turnier.feature.domain.StartlistenRepository
|
import at.mocode.turnier.feature.domain.StartlistenRepository
|
||||||
import at.mocode.turnier.feature.presentation.StartlistenZeile
|
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.call.*
|
import io.ktor.client.call.*
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
|
import at.mocode.turnier.feature.domain.model.StartlistenZeile
|
||||||
|
|
||||||
class DefaultStartlistenRepository(
|
class DefaultStartlistenRepository(
|
||||||
private val client: HttpClient,
|
private val client: HttpClient,
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package at.mocode.turnier.feature.di
|
||||||
|
|
||||||
|
import org.koin.core.module.Module
|
||||||
|
|
||||||
|
expect val turnierFeatureModule: Module
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package at.mocode.turnier.feature.domain
|
package at.mocode.turnier.feature.domain
|
||||||
|
|
||||||
import at.mocode.turnier.feature.presentation.StartlistenZeile
|
import at.mocode.turnier.feature.domain.model.StartlistenZeile
|
||||||
|
|
||||||
interface StartlistenRepository {
|
interface StartlistenRepository {
|
||||||
suspend fun generate(bewerbId: Long): Result<List<StartlistenZeile>>
|
suspend fun generate(bewerbId: Long): Result<List<StartlistenZeile>>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package at.mocode.turnier.feature.domain.model
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class StartlistenZeile(
|
||||||
|
val nr: Int,
|
||||||
|
val zeit: String,
|
||||||
|
val reiter: String,
|
||||||
|
val pferd: String,
|
||||||
|
val wunsch: String,
|
||||||
|
val nennungId: String = ""
|
||||||
|
)
|
||||||
|
|
@ -10,7 +10,7 @@ import at.mocode.turnier.feature.presentation.*
|
||||||
import org.koin.core.qualifier.named
|
import org.koin.core.qualifier.named
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val turnierFeatureModule = module {
|
actual val turnierFeatureModule = module {
|
||||||
// Repositories: Interface → Default-Implementierung mit zentralem apiClient
|
// Repositories: Interface → Default-Implementierung mit zentralem apiClient
|
||||||
single<TurnierRepository> { DefaultTurnierRepository(client = get(qualifier = named("apiClient"))) }
|
single<TurnierRepository> { DefaultTurnierRepository(client = get(qualifier = named("apiClient"))) }
|
||||||
single<BewerbRepository> { DefaultBewerbRepository(client = get(qualifier = named("apiClient"))) }
|
single<BewerbRepository> { DefaultBewerbRepository(client = get(qualifier = named("apiClient"))) }
|
||||||
|
|
|
||||||
|
|
@ -58,14 +58,26 @@ class BewerbAnlegenViewModel {
|
||||||
|
|
||||||
private fun applySuggestion() {
|
private fun applySuggestion() {
|
||||||
val s = _state.value
|
val s = _state.value
|
||||||
if (s.bewerbsTyp.equals("CSN-C-NEU", ignoreCase = true)) {
|
val bTyp = s.bewerbsTyp.uppercase()
|
||||||
// Pflicht-Teilung: ohne/mit Lizenz; R1/R2+
|
|
||||||
val suggestion = listOf(
|
val suggestion = when {
|
||||||
AbteilungsInput(1, label = "Ohne Lizenz · R1", mitLizenz = false, reiterKlasse = ReiterKlasse.R1),
|
bTyp.contains("CSN-C-NEU") -> listOf(
|
||||||
AbteilungsInput(2, label = "Ohne Lizenz · R2+", mitLizenz = false, reiterKlasse = ReiterKlasse.R2_PLUS),
|
AbteilungsInput(1, label = "Abteilung 1: R1", mitLizenz = true, reiterKlasse = ReiterKlasse.R1),
|
||||||
AbteilungsInput(3, label = "Mit Lizenz · R1", mitLizenz = true, reiterKlasse = ReiterKlasse.R1),
|
AbteilungsInput(2, label = "Abteilung 2: R2+", mitLizenz = true, reiterKlasse = ReiterKlasse.R2_PLUS),
|
||||||
AbteilungsInput(4, label = "Mit Lizenz · R2+", mitLizenz = true, reiterKlasse = ReiterKlasse.R2_PLUS),
|
|
||||||
)
|
)
|
||||||
|
bTyp.contains("CDN-B") || bTyp.contains("CDNP-B") -> listOf(
|
||||||
|
AbteilungsInput(1, label = "Abteilung 1: R1", mitLizenz = true, reiterKlasse = ReiterKlasse.R1),
|
||||||
|
AbteilungsInput(2, label = "Abteilung 2: R2", mitLizenz = true, reiterKlasse = ReiterKlasse.R2_PLUS),
|
||||||
|
AbteilungsInput(3, label = "Abteilung 3: R3+", mitLizenz = true, reiterKlasse = ReiterKlasse.R2_PLUS),
|
||||||
|
)
|
||||||
|
bTyp.contains("CSN-B") -> listOf(
|
||||||
|
AbteilungsInput(1, label = "Abteilung 1: R1", mitLizenz = true, reiterKlasse = ReiterKlasse.R1),
|
||||||
|
AbteilungsInput(2, label = "Abteilung 2: R2+", mitLizenz = true, reiterKlasse = ReiterKlasse.R2_PLUS),
|
||||||
|
)
|
||||||
|
else -> emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (suggestion.isNotEmpty()) {
|
||||||
reduce { it.copy(abteilungen = suggestion, abteilungsTyp = AbteilungsTyp.SEPARATE_SIEGEREHRUNG) }
|
reduce { it.copy(abteilungen = suggestion, abteilungsTyp = AbteilungsTyp.SEPARATE_SIEGEREHRUNG) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -17,20 +17,10 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.Serializable
|
import at.mocode.turnier.feature.domain.model.StartlistenZeile
|
||||||
|
|
||||||
typealias BewerbListItem = Bewerb
|
typealias BewerbListItem = Bewerb
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class StartlistenZeile(
|
|
||||||
val nr: Int,
|
|
||||||
val zeit: String,
|
|
||||||
val reiter: String,
|
|
||||||
val pferd: String,
|
|
||||||
val wunsch: String,
|
|
||||||
val nennungId: String = ""
|
|
||||||
)
|
|
||||||
|
|
||||||
data class BewerbState(
|
data class BewerbState(
|
||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
val searchQuery: String = "",
|
val searchQuery: String = "",
|
||||||
|
|
@ -19,6 +19,7 @@ import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import at.mocode.turnier.feature.domain.model.StartlistenZeile
|
||||||
import javax.swing.JFileChooser
|
import javax.swing.JFileChooser
|
||||||
import javax.swing.filechooser.FileNameExtensionFilter
|
import javax.swing.filechooser.FileNameExtensionFilter
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import at.mocode.turnier.feature.domain.Ergebnis
|
import at.mocode.turnier.feature.domain.Ergebnis
|
||||||
|
import at.mocode.turnier.feature.domain.model.StartlistenZeile
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
|
|
||||||
private val ElBlue = Color(0xFF1E3A8A)
|
private val ElBlue = Color(0xFF1E3A8A)
|
||||||
|
|
@ -57,13 +58,13 @@ fun ErgebnislistenTabContent(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ErgebnislistenBewerbsTabs(
|
private fun ErgebnislistenBewerbsTabs(
|
||||||
bewerbe: List<BewerbListItem>,
|
bewerbe: List<BewerbListItem>,
|
||||||
selectedId: Long?,
|
selectedId: Long?,
|
||||||
onSelect: (Long?) -> Unit,
|
onSelect: (Long?) -> Unit,
|
||||||
ergebnisse: List<Ergebnis>,
|
ergebnisse: List<Ergebnis>,
|
||||||
startliste: List<StartlistenZeile>,
|
startliste: List<StartlistenZeile>,
|
||||||
onCalculate: () -> Unit,
|
onCalculate: () -> Unit,
|
||||||
onPrint: () -> Unit
|
onPrint: () -> Unit
|
||||||
) {
|
) {
|
||||||
val selectedIndex = bewerbe.indexOfFirst { it.id == selectedId }.coerceAtLeast(0)
|
val selectedIndex = bewerbe.indexOfFirst { it.id == selectedId }.coerceAtLeast(0)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -501,12 +501,17 @@ private fun OrgSearchField(label: String, value: String, onValueChange: (String)
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Text(label, fontSize = 13.sp, modifier = Modifier.width(200.dp), color = Color(0xFF374151))
|
Text(
|
||||||
|
label,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
modifier = Modifier.weight(1.5f), // Flexibles Gewicht statt fixen 200dp
|
||||||
|
color = Color(0xFF374151)
|
||||||
|
)
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = value,
|
value = value,
|
||||||
onValueChange = onValueChange,
|
onValueChange = onValueChange,
|
||||||
placeholder = { Text("Name suchen...", fontSize = 12.sp) },
|
placeholder = { Text("Name suchen...", fontSize = 12.sp) },
|
||||||
modifier = Modifier.weight(1f).height(44.dp),
|
modifier = Modifier.weight(3f), // Flexibles Gewicht und keine fixe Höhe
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,12 +55,62 @@ fun StammdatenTabContent(
|
||||||
val klassen = remember { mutableStateListOf<String>() }
|
val klassen = remember { mutableStateListOf<String>() }
|
||||||
val kat = remember { mutableStateListOf<String>() }
|
val kat = remember { mutableStateListOf<String>() }
|
||||||
|
|
||||||
var von by remember { mutableStateOf("") }
|
var von by remember { mutableStateOf(eventVon ?: "") }
|
||||||
var bis by remember { mutableStateOf("") }
|
var bis by remember { mutableStateOf(eventBis ?: "") }
|
||||||
var ort by remember { mutableStateOf("") }
|
var ort by remember { mutableStateOf(eventOrt ?: "") }
|
||||||
|
|
||||||
var titel by remember { mutableStateOf("") }
|
var titel by remember { mutableStateOf("") }
|
||||||
var subTitel by remember { mutableStateOf("") }
|
var subTitel by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
// Initialisierung aus Mock-Store (StoreV2/TurnierStoreV2) falls vorhanden
|
||||||
|
LaunchedEffect(turnierId) {
|
||||||
|
// Da wir in einem anderen Modul sind, können wir nicht direkt auf StoreV2 zugreifen
|
||||||
|
// ohne die Abhängigkeit zu haben. In einer echten Architektur käme dies über das Repository.
|
||||||
|
// Aber für die Demo/Fakten-Präsentation im Desktop-Shell-Kontext:
|
||||||
|
try {
|
||||||
|
val clazz = Class.forName("at.mocode.desktop.v2.TurnierStoreV2")
|
||||||
|
val method = clazz.getMethod("allTurniere")
|
||||||
|
val all = method.invoke(null) as? List<*>
|
||||||
|
val turnier = all?.find { t ->
|
||||||
|
val idField = t!!::class.java.getDeclaredField("turnierNr")
|
||||||
|
idField.isAccessible = true
|
||||||
|
idField.get(t).toString() == turnierId.toString() ||
|
||||||
|
t.hashCode().toLong() == turnierId // Fallback falls ID anders gemappt ist
|
||||||
|
}
|
||||||
|
|
||||||
|
if (turnier != null) {
|
||||||
|
val tClass = turnier::class.java
|
||||||
|
|
||||||
|
val nrField = tClass.getDeclaredField("turnierNr")
|
||||||
|
nrField.isAccessible = true
|
||||||
|
turnierNr = nrField.get(turnier).toString()
|
||||||
|
nrConfirmed = true
|
||||||
|
|
||||||
|
val titelField = tClass.getDeclaredField("titel")
|
||||||
|
titelField.isAccessible = true
|
||||||
|
titel = titelField.get(turnier) as String
|
||||||
|
|
||||||
|
val subField = tClass.getDeclaredField("subTitel")
|
||||||
|
subField.isAccessible = true
|
||||||
|
subTitel = subField.get(turnier) as String
|
||||||
|
|
||||||
|
val katField = tClass.getDeclaredField("kategorie")
|
||||||
|
katField.isAccessible = true
|
||||||
|
val kats = katField.get(turnier) as? List<String>
|
||||||
|
kats?.let { kat.addAll(it) }
|
||||||
|
|
||||||
|
val typField = tClass.getDeclaredField("typ")
|
||||||
|
typField.isAccessible = true
|
||||||
|
typ = typField.get(turnier) as String
|
||||||
|
|
||||||
|
val znsField = tClass.getDeclaredField("znsDataLoaded")
|
||||||
|
znsField.isAccessible = true
|
||||||
|
znsDataLoaded = znsField.get(turnier) as Boolean
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Reflection fehlgeschlagen oder Store nicht erreichbar -> Fallback auf leere Felder
|
||||||
|
}
|
||||||
|
}
|
||||||
var turnierLogoUrl by remember { mutableStateOf("") }
|
var turnierLogoUrl by remember { mutableStateOf("") }
|
||||||
val sponsoren = remember { mutableStateListOf<String>() }
|
val sponsoren = remember { mutableStateListOf<String>() }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import at.mocode.turnier.feature.domain.model.StartlistenZeile
|
||||||
import at.mocode.turnier.feature.domain.Bewerb
|
import at.mocode.turnier.feature.domain.Bewerb
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package at.mocode.turnier.feature.di
|
||||||
|
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wasm-spezifische Implementierung (vorerst reduziert, da UI-ViewModels JVM-spezifisch sind).
|
||||||
|
*/
|
||||||
|
actual val turnierFeatureModule = module {
|
||||||
|
// Hier können später Wasm-spezifische Repositories oder Shared-Logic registriert werden
|
||||||
|
}
|
||||||
|
|
@ -11,13 +11,17 @@ group = "at.mocode.clients"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
kotlin {
|
kotlin {
|
||||||
jvm()
|
jvm()
|
||||||
|
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
||||||
|
wasmJs {
|
||||||
|
browser()
|
||||||
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
jvmMain.dependencies {
|
commonMain.dependencies {
|
||||||
implementation(projects.frontend.core.designSystem)
|
implementation(projects.frontend.core.designSystem)
|
||||||
implementation(projects.frontend.core.domain)
|
implementation(projects.frontend.core.domain)
|
||||||
implementation(projects.frontend.core.network)
|
implementation(projects.frontend.core.network)
|
||||||
implementation(projects.frontend.core.navigation)
|
implementation(projects.frontend.core.navigation)
|
||||||
implementation(compose.desktop.currentOs)
|
|
||||||
implementation(compose.foundation)
|
implementation(compose.foundation)
|
||||||
implementation(compose.runtime)
|
implementation(compose.runtime)
|
||||||
implementation(compose.material3)
|
implementation(compose.material3)
|
||||||
|
|
@ -27,8 +31,11 @@ kotlin {
|
||||||
implementation(libs.koin.core)
|
implementation(libs.koin.core)
|
||||||
implementation(libs.koin.compose)
|
implementation(libs.koin.compose)
|
||||||
implementation(libs.koin.compose.viewmodel)
|
implementation(libs.koin.compose.viewmodel)
|
||||||
// Ktor client for repository implementation
|
|
||||||
implementation(libs.ktor.client.core)
|
implementation(libs.ktor.client.core)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jvmMain.dependencies {
|
||||||
|
implementation(compose.desktop.currentOs)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ class FakeVeranstalterRepository : VeranstalterRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun delete(id: Long): Result<Unit> {
|
override suspend fun delete(id: Long): Result<Unit> {
|
||||||
mockData.removeIf { it.id == id }
|
mockData.removeAll { it.id == id }
|
||||||
return Result.success(Unit)
|
return Result.success(Unit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,16 @@ group = "at.mocode.clients"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
kotlin {
|
kotlin {
|
||||||
jvm()
|
jvm()
|
||||||
|
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
||||||
|
wasmJs {
|
||||||
|
browser()
|
||||||
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
jvmMain.dependencies {
|
commonMain.dependencies {
|
||||||
implementation(projects.frontend.core.designSystem)
|
implementation(projects.frontend.core.designSystem)
|
||||||
implementation(projects.frontend.core.domain)
|
implementation(projects.frontend.core.domain)
|
||||||
implementation(projects.frontend.core.navigation)
|
implementation(projects.frontend.core.navigation)
|
||||||
implementation(compose.desktop.currentOs)
|
|
||||||
implementation(compose.foundation)
|
implementation(compose.foundation)
|
||||||
implementation(compose.runtime)
|
implementation(compose.runtime)
|
||||||
implementation(compose.material3)
|
implementation(compose.material3)
|
||||||
|
|
@ -27,5 +31,9 @@ kotlin {
|
||||||
implementation(libs.koin.compose)
|
implementation(libs.koin.compose)
|
||||||
implementation(libs.koin.compose.viewmodel)
|
implementation(libs.koin.compose.viewmodel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jvmMain.dependencies {
|
||||||
|
implementation(compose.desktop.currentOs)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
package at.mocode.frontend.features.verein.data
|
||||||
|
|
||||||
|
import at.mocode.frontend.features.verein.domain.Verein
|
||||||
|
import at.mocode.frontend.features.verein.domain.VereinRepository
|
||||||
|
import at.mocode.frontend.features.verein.domain.VereinStatus
|
||||||
|
|
||||||
|
class FakeVereinRepository : VereinRepository {
|
||||||
|
private val vereine = mutableListOf(
|
||||||
|
Verein(
|
||||||
|
id = "v1",
|
||||||
|
name = "URFV Neumarkt am Wallersee",
|
||||||
|
oepsNr = "4221",
|
||||||
|
ort = "Neumarkt/M.",
|
||||||
|
plz = "4221",
|
||||||
|
status = VereinStatus.AKTIV
|
||||||
|
),
|
||||||
|
Verein(
|
||||||
|
id = "v2",
|
||||||
|
name = "URC St. Georgen",
|
||||||
|
oepsNr = "1234",
|
||||||
|
ort = "St. Georgen",
|
||||||
|
plz = "5113",
|
||||||
|
status = VereinStatus.AKTIV
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun getVereine(): Result<List<Verein>> = Result.success(vereine.toList())
|
||||||
|
|
||||||
|
override suspend fun saveVerein(verein: Verein): Result<Verein> {
|
||||||
|
val index = vereine.indexOfFirst { it.id == verein.id }
|
||||||
|
if (index >= 0) {
|
||||||
|
vereine[index] = verein
|
||||||
|
} else {
|
||||||
|
val newVerein = verein.copy(id = "new_${vereine.size + 1}")
|
||||||
|
vereine.add(newVerein)
|
||||||
|
return Result.success(newVerein)
|
||||||
|
}
|
||||||
|
return Result.success(verein)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,7 @@ data class VereinUiState(
|
||||||
val selectedVerein: Verein? = null,
|
val selectedVerein: Verein? = null,
|
||||||
val isEditing: Boolean = false,
|
val isEditing: Boolean = false,
|
||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
|
val error: String? = null,
|
||||||
val editName: String = "",
|
val editName: String = "",
|
||||||
val editLangname: String = "",
|
val editLangname: String = "",
|
||||||
val editOepsNr: String = "",
|
val editOepsNr: String = "",
|
||||||
|
|
@ -45,21 +46,31 @@ open class VereinViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadVereine() {
|
fun loadVereine() {
|
||||||
uiState = uiState.copy(isLoading = true)
|
uiState = uiState.copy(isLoading = true, error = null)
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
repository.getVereine()
|
try {
|
||||||
.onSuccess { vereine ->
|
repository.getVereine()
|
||||||
uiState = uiState.copy(
|
.onSuccess { vereine ->
|
||||||
allVereine = vereine,
|
uiState = uiState.copy(
|
||||||
searchResults = vereine,
|
allVereine = vereine,
|
||||||
isLoading = false
|
searchResults = vereine,
|
||||||
)
|
isLoading = false,
|
||||||
filterResults()
|
error = null
|
||||||
}
|
)
|
||||||
.onFailure {
|
filterResults()
|
||||||
uiState = uiState.copy(isLoading = false)
|
}
|
||||||
// Error handling could be added here
|
.onFailure {
|
||||||
}
|
uiState = uiState.copy(
|
||||||
|
isLoading = false,
|
||||||
|
error = "Fehler beim Laden der Vereine: ${it.message ?: "Unbekannter Fehler"}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
uiState = uiState.copy(
|
||||||
|
isLoading = false,
|
||||||
|
error = "Kritischer Fehler: ${e.message}"
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -120,7 +131,7 @@ open class VereinViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onSave() {
|
fun onSave() {
|
||||||
uiState = uiState.copy(isLoading = true)
|
uiState = uiState.copy(isLoading = true, error = null)
|
||||||
val verein = (uiState.selectedVerein ?: Verein(
|
val verein = (uiState.selectedVerein ?: Verein(
|
||||||
id = "",
|
id = "",
|
||||||
name = uiState.editName
|
name = uiState.editName
|
||||||
|
|
@ -136,11 +147,14 @@ open class VereinViewModel(
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
repository.saveVerein(verein)
|
repository.saveVerein(verein)
|
||||||
.onSuccess {
|
.onSuccess {
|
||||||
uiState = uiState.copy(isEditing = false, isLoading = false)
|
uiState = uiState.copy(isEditing = false, isLoading = false, error = null)
|
||||||
loadVereine()
|
loadVereine()
|
||||||
}
|
}
|
||||||
.onFailure {
|
.onFailure {
|
||||||
uiState = uiState.copy(isLoading = false)
|
uiState = uiState.copy(
|
||||||
|
isLoading = false,
|
||||||
|
error = "Fehler beim Speichern des Vereins: ${it.message ?: "Unbekannter Fehler"}"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
package at.mocode.frontend.features.verein.di
|
package at.mocode.frontend.features.verein.di
|
||||||
|
|
||||||
import at.mocode.frontend.features.verein.data.KtorVereinRepository
|
import at.mocode.frontend.features.verein.data.FakeVereinRepository
|
||||||
import at.mocode.frontend.features.verein.domain.VereinRepository
|
import at.mocode.frontend.features.verein.domain.VereinRepository
|
||||||
import at.mocode.frontend.features.verein.presentation.VereinViewModel
|
import at.mocode.frontend.features.verein.presentation.VereinViewModel
|
||||||
import org.koin.core.module.dsl.viewModelOf
|
import org.koin.core.module.dsl.viewModelOf
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val vereinFeatureModule = module {
|
val vereinFeatureModule = module {
|
||||||
single<VereinRepository> { KtorVereinRepository(get()) }
|
// Desktop-App nutzt im Startup-Mode bevorzugt das Fake-Repository
|
||||||
|
single<VereinRepository> { FakeVereinRepository() }
|
||||||
viewModelOf(::VereinViewModel)
|
viewModelOf(::VereinViewModel)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ plugins {
|
||||||
alias(libs.plugins.composeCompiler)
|
alias(libs.plugins.composeCompiler)
|
||||||
alias(libs.plugins.composeMultiplatform)
|
alias(libs.plugins.composeMultiplatform)
|
||||||
alias(libs.plugins.kotlinSerialization)
|
alias(libs.plugins.kotlinSerialization)
|
||||||
id("org.jetbrains.compose.hot-reload")
|
// id("org.jetbrains.compose.hot-reload")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
|
@ -30,9 +30,9 @@ plugins {
|
||||||
val versionProps = Properties().also { props ->
|
val versionProps = Properties().also { props ->
|
||||||
rootProject.file("version.properties").inputStream().use { props.load(it) }
|
rootProject.file("version.properties").inputStream().use { props.load(it) }
|
||||||
}
|
}
|
||||||
val vMajor = versionProps.getProperty("VERSION_MAJOR", "1")
|
val vMajor: String? = versionProps.getProperty("VERSION_MAJOR", "1")
|
||||||
val vMinor = versionProps.getProperty("VERSION_MINOR", "0")
|
val vMinor: String? = versionProps.getProperty("VERSION_MINOR", "0")
|
||||||
val vPatch = versionProps.getProperty("VERSION_PATCH", "0")
|
val vPatch: String? = versionProps.getProperty("VERSION_PATCH", "0")
|
||||||
// nativeDistributions erwartet reines "MAJOR.MINOR.PATCH" (kein Qualifier)
|
// nativeDistributions erwartet reines "MAJOR.MINOR.PATCH" (kein Qualifier)
|
||||||
val packageVer = "$vMajor.$vMinor.$vPatch"
|
val packageVer = "$vMajor.$vMinor.$vPatch"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import at.mocode.zns.parser.ZnsBewerb
|
||||||
import at.mocode.frontend.features.veranstalter.presentation.VeranstalterAuswahlScreen
|
import at.mocode.frontend.features.veranstalter.presentation.VeranstalterAuswahlScreen
|
||||||
import at.mocode.frontend.features.veranstalter.presentation.VeranstalterDetailScreen
|
import at.mocode.frontend.features.veranstalter.presentation.VeranstalterDetailScreen
|
||||||
import at.mocode.frontend.features.veranstalter.presentation.VeranstalterNeuScreen
|
import at.mocode.frontend.features.veranstalter.presentation.VeranstalterNeuScreen
|
||||||
|
import at.mocode.turnier.feature.domain.model.StartlistenZeile
|
||||||
import at.mocode.veranstaltung.feature.presentation.VeranstaltungUebersichtScreen
|
import at.mocode.veranstaltung.feature.presentation.VeranstaltungUebersichtScreen
|
||||||
import at.mocode.wui.preview.ComponentPreview
|
import at.mocode.wui.preview.ComponentPreview
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -274,32 +274,35 @@ object StoreV2 {
|
||||||
// Falls bereits Daten da sind (außer den statischen Vereinen), nichts tun
|
// Falls bereits Daten da sind (außer den statischen Vereinen), nichts tun
|
||||||
if (veranstaltungen.isNotEmpty()) return
|
if (veranstaltungen.isNotEmpty()) return
|
||||||
|
|
||||||
// 1. Neumarkt 2026 (ID 100)
|
// 1. Neumarkt April 2026 (ID 100)
|
||||||
val neumarktId = 100L
|
val neumarktId = 100L
|
||||||
addEventFirst(
|
addEventFirst(
|
||||||
1, VeranstaltungV2(
|
1, VeranstaltungV2(
|
||||||
id = neumarktId,
|
id = neumarktId,
|
||||||
veranstalterId = 1,
|
veranstalterId = 1,
|
||||||
titel = "Frühjahrsturnier Neumarkt/M. 2026",
|
titel = "CSN-B* Neumarkt am Wallersee",
|
||||||
datumVon = "2026-04-10",
|
datumVon = "2026-04-24",
|
||||||
datumBis = "2026-04-12",
|
datumBis = "2026-04-26",
|
||||||
status = "Nennungsphase",
|
status = "Nennungsphase",
|
||||||
beschreibung = "Traditionelles Frühjahrsturnier mit Spring- und Dressurprüfungen bis Klasse LM."
|
ort = "Neumarkt am Wallersee",
|
||||||
|
beschreibung = "Großes Springturnier mit Teilnehmern aus ganz Österreich. Vorbereitungen für das Live-Event am 24. April laufen."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
TurnierStoreV2.add(
|
TurnierStoreV2.add(
|
||||||
neumarktId,
|
neumarktId,
|
||||||
TurnierV2(101, neumarktId, 26128, datumVon = "2026-04-10", datumBis = "2026-04-12", znsDataLoaded = true).apply {
|
TurnierV2(101, neumarktId, 26128, datumVon = "2026-04-24", datumBis = "2026-04-26", znsDataLoaded = true).apply {
|
||||||
kategorie.add("CSN-C-NEU")
|
titel = "Springturnier Neumarkt"
|
||||||
kategorie.add("CSNP-C-NEU")
|
kategorie.add("CSN-B*")
|
||||||
|
kategorie.add("CSNP-B")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
TurnierStoreV2.add(
|
TurnierStoreV2.add(
|
||||||
neumarktId,
|
neumarktId,
|
||||||
TurnierV2(102, neumarktId, 26129, datumVon = "2026-04-10", datumBis = "2026-04-12", znsDataLoaded = true).apply {
|
TurnierV2(102, neumarktId, 26129, datumVon = "2026-04-24", datumBis = "2026-04-26", znsDataLoaded = true).apply {
|
||||||
kategorie.add("CDN-C-NEU")
|
titel = "Dressurturnier Neumarkt"
|
||||||
kategorie.add("CDNP-C-NEU")
|
kategorie.add("CDN-B")
|
||||||
|
kategorie.add("CDNP-B")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -780,6 +780,10 @@ object TurnierStoreV2 {
|
||||||
fun list(veranstaltungId: Long): MutableList<TurnierV2> = map.getOrPut(veranstaltungId) { mutableListOf() }
|
fun list(veranstaltungId: Long): MutableList<TurnierV2> = map.getOrPut(veranstaltungId) { mutableListOf() }
|
||||||
fun add(veranstaltungId: Long, t: TurnierV2) { list(veranstaltungId).add(0, t) }
|
fun add(veranstaltungId: Long, t: TurnierV2) { list(veranstaltungId).add(0, t) }
|
||||||
fun remove(veranstaltungId: Long, tId: Long) { list(veranstaltungId).removeAll { it.id == tId } }
|
fun remove(veranstaltungId: Long, tId: Long) { list(veranstaltungId).removeAll { it.id == tId } }
|
||||||
|
|
||||||
|
// Hilfsmethode für Reflection-Zugriff aus anderen Modulen (StammdatenTab)
|
||||||
|
@JvmStatic
|
||||||
|
fun allTurniere(): List<TurnierV2> = map.values.flatten()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
||||||
55
frontend/shells/meldestelle-web/build.gradle.kts
Normal file
55
frontend/shells/meldestelle-web/build.gradle.kts
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlinMultiplatform)
|
||||||
|
alias(libs.plugins.composeCompiler)
|
||||||
|
alias(libs.plugins.composeMultiplatform)
|
||||||
|
alias(libs.plugins.kotlinSerialization)
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
@OptIn(ExperimentalWasmDsl::class)
|
||||||
|
wasmJs {
|
||||||
|
browser {
|
||||||
|
commonWebpackConfig {
|
||||||
|
outputFileName = "meldestelle-web.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binaries.executable()
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
val wasmJsMain by getting {
|
||||||
|
dependencies {
|
||||||
|
// Core-Module
|
||||||
|
implementation(projects.frontend.core.domain)
|
||||||
|
implementation(projects.frontend.core.designSystem)
|
||||||
|
implementation(projects.frontend.core.navigation)
|
||||||
|
implementation(projects.frontend.core.network)
|
||||||
|
implementation(projects.frontend.core.auth)
|
||||||
|
|
||||||
|
// Feature-Module (die öffentlich sein dürfen)
|
||||||
|
implementation(projects.frontend.features.veranstaltungFeature)
|
||||||
|
implementation(projects.frontend.features.turnierFeature)
|
||||||
|
implementation(projects.frontend.features.nennungFeature)
|
||||||
|
|
||||||
|
// Compose Multiplatform
|
||||||
|
implementation(compose.runtime)
|
||||||
|
implementation(compose.foundation)
|
||||||
|
implementation(compose.material3)
|
||||||
|
implementation(compose.ui)
|
||||||
|
implementation(compose.components.resources)
|
||||||
|
implementation(libs.compose.materialIconsExtended)
|
||||||
|
|
||||||
|
// DI (Koin)
|
||||||
|
implementation(libs.koin.core)
|
||||||
|
implementation(libs.koin.compose)
|
||||||
|
implementation(libs.koin.compose.viewmodel)
|
||||||
|
|
||||||
|
// Bundles
|
||||||
|
implementation(libs.bundles.kmp.common)
|
||||||
|
implementation(libs.bundles.compose.common)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,265 @@
|
||||||
|
package at.mocode.web
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Description
|
||||||
|
import androidx.compose.material.icons.filled.OpenInNew
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import at.mocode.frontend.core.designsystem.theme.AppColors
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun WebMainScreen() {
|
||||||
|
var currentScreen by remember { mutableStateOf<WebScreen>(WebScreen.Landing) }
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text("Meldestelle Online", fontWeight = FontWeight.Bold) },
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = AppColors.Primary,
|
||||||
|
titleContentColor = Color.White
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { padding ->
|
||||||
|
Box(modifier = Modifier.fillMaxSize().padding(padding)) {
|
||||||
|
when (val screen = currentScreen) {
|
||||||
|
is WebScreen.Landing -> LandingPage(
|
||||||
|
onVeranstaltungClick = { vId ->
|
||||||
|
// Für den Prototyp zeigen wir einfach die Turniere dieser Veranstaltung
|
||||||
|
},
|
||||||
|
onNennenClick = { vId, tId ->
|
||||||
|
currentScreen = WebScreen.Nennung(vId, tId)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
is WebScreen.Nennung -> NennungWebFormular(
|
||||||
|
veranstaltungId = screen.veranstaltungId,
|
||||||
|
turnierId = screen.turnierId,
|
||||||
|
onBack = { currentScreen = WebScreen.Landing }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class WebScreen {
|
||||||
|
data object Landing : WebScreen()
|
||||||
|
data class Nennung(val veranstaltungId: Long, val turnierId: Long) : WebScreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LandingPage(
|
||||||
|
onVeranstaltungClick: (Long) -> Unit,
|
||||||
|
onNennenClick: (Long, Long) -> Unit
|
||||||
|
) {
|
||||||
|
val veranstaltungen = remember {
|
||||||
|
listOf(
|
||||||
|
VeranstaltungWebModel(
|
||||||
|
id = 1,
|
||||||
|
name = "CSN-B* Neumarkt",
|
||||||
|
ort = "Neumarkt am Wallersee",
|
||||||
|
datum = "24. - 26. April 2026",
|
||||||
|
turniere = listOf(
|
||||||
|
TurnierWebModel(101, "Springturnier Neumarkt", "Ausschreibung_Neumarkt.pdf"),
|
||||||
|
TurnierWebModel(102, "Dressurturnier Neumarkt", "Ausschreibung_Dressur.pdf")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize().padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(24.dp)
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
"Willkommen bei der Meldestelle Online",
|
||||||
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
color = AppColors.OnBackgroundLight
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"Hier finden Sie aktuelle Reitturniere und können Ihre Nennungen online abgeben.",
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
modifier = Modifier.padding(top = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
"Aktuelle Veranstaltungen",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.padding(vertical = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
items(veranstaltungen) { veranstaltung ->
|
||||||
|
VeranstaltungsCardWeb(
|
||||||
|
veranstaltung = veranstaltung,
|
||||||
|
onNennenClick = { tId -> onNennenClick(veranstaltung.id, tId) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun VeranstaltungsCardWeb(
|
||||||
|
veranstaltung: VeranstaltungWebModel,
|
||||||
|
onNennenClick: (Long) -> Unit
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
|
Text(veranstaltung.name, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||||
|
Text("${veranstaltung.datum} | ${veranstaltung.ort}", style = MaterialTheme.typography.bodyMedium, color = Color.Gray)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Text("Turniere dieser Veranstaltung:", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
|
||||||
|
|
||||||
|
veranstaltung.turniere.forEach { turnier ->
|
||||||
|
TurnierCardWeb(
|
||||||
|
turnier = turnier,
|
||||||
|
onNennenClick = { onNennenClick(turnier.id) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TurnierCardWeb(
|
||||||
|
turnier: TurnierWebModel,
|
||||||
|
onNennenClick: () -> Unit
|
||||||
|
) {
|
||||||
|
OutlinedCard(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
|
||||||
|
colors = CardDefaults.outlinedCardColors(containerColor = AppColors.BackgroundLight)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(turnier.name, fontWeight = FontWeight.Bold)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
TextButton(onClick = { /* PDF öffnen Logik */ }) {
|
||||||
|
Icon(Icons.Default.Description, contentDescription = null)
|
||||||
|
Spacer(Modifier.width(4.dp))
|
||||||
|
Text("Ausschreibung")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = onNennenClick,
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = AppColors.Success)
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.OpenInNew, contentDescription = null)
|
||||||
|
Spacer(Modifier.width(4.dp))
|
||||||
|
Text("Online-Nennen")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NennungWebFormular(
|
||||||
|
veranstaltungId: Long,
|
||||||
|
turnierId: Long,
|
||||||
|
onBack: () -> Unit
|
||||||
|
) {
|
||||||
|
var statusMessage by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
|
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
|
||||||
|
Text("Online-Nennung", style = MaterialTheme.typography.headlineMedium)
|
||||||
|
Text("Turnier ID: $turnierId", style = MaterialTheme.typography.bodyMedium)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
if (statusMessage == null) {
|
||||||
|
// Vereinfachtes Formular für den Prototyp
|
||||||
|
var reiter by remember { mutableStateOf("") }
|
||||||
|
var pferd by remember { mutableStateOf("") }
|
||||||
|
var bewerbe by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = reiter,
|
||||||
|
onValueChange = { reiter = it },
|
||||||
|
label = { Text("Reiter Name / ZNS-Nummer") },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = pferd,
|
||||||
|
onValueChange = { pferd = it },
|
||||||
|
label = { Text("Pferd Name / Kopfnummer") },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = bewerbe,
|
||||||
|
onValueChange = { bewerbe = it },
|
||||||
|
label = { Text("Bewerbe (z.B. 1, 2, 5)") },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
|
OutlinedButton(onClick = onBack) { Text("Abbrechen") }
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
statusMessage = "Nennung erfolgreich abgeschickt! Sie erhalten in Kürze eine Bestätigung per E-Mail."
|
||||||
|
},
|
||||||
|
enabled = reiter.isNotBlank() && pferd.isNotBlank() && bewerbe.isNotBlank()
|
||||||
|
) {
|
||||||
|
Text("Jetzt Nennen")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(containerColor = AppColors.PrimaryContainer),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
|
Text(statusMessage!!, color = AppColors.OnPrimaryContainer)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Button(onClick = onBack) { Text("Zurück zur Übersicht") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class VeranstaltungWebModel(
|
||||||
|
val id: Long,
|
||||||
|
val name: String,
|
||||||
|
val ort: String,
|
||||||
|
val datum: String,
|
||||||
|
val turniere: List<TurnierWebModel>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class TurnierWebModel(
|
||||||
|
val id: Long,
|
||||||
|
val name: String,
|
||||||
|
val pdfUrl: String
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
package at.mocode.web
|
||||||
|
|
||||||
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
|
import androidx.compose.ui.window.ComposeViewport
|
||||||
|
import at.mocode.frontend.core.designsystem.theme.AppTheme
|
||||||
|
import at.mocode.frontend.core.network.networkModule
|
||||||
|
import at.mocode.frontend.features.nennung.di.nennungFeatureModule
|
||||||
|
import at.mocode.turnier.feature.di.turnierFeatureModule
|
||||||
|
import org.koin.core.context.startKoin
|
||||||
|
|
||||||
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
|
fun main() {
|
||||||
|
startKoin {
|
||||||
|
modules(
|
||||||
|
networkModule,
|
||||||
|
nennungFeatureModule,
|
||||||
|
turnierFeatureModule,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ComposeViewport(content = {
|
||||||
|
AppTheme {
|
||||||
|
WebMainScreen()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -16,9 +16,10 @@ kotlinx-serialization-json = "1.9.0"
|
||||||
kotlinx-datetime = "0.7.1"
|
kotlinx-datetime = "0.7.1"
|
||||||
|
|
||||||
# UI: Compose Multiplatform
|
# UI: Compose Multiplatform
|
||||||
# Aligned with Kotlin 2.3.0
|
# Aligned with Kotlin 2.3.20
|
||||||
composeMultiplatform = "1.11.0-alpha04"
|
composeMultiplatform = "1.10.3"
|
||||||
composeHotReload = "1.0.0"
|
composeHotReload = "1.0.0"
|
||||||
|
materialIconsExtended = "1.7.3"
|
||||||
androidx-lifecycle = "2.9.6"
|
androidx-lifecycle = "2.9.6"
|
||||||
uiDesktop = "1.7.0"
|
uiDesktop = "1.7.0"
|
||||||
|
|
||||||
|
|
@ -107,6 +108,7 @@ firebaseDatabaseKtx = "22.0.1"
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# === FRONTEND: KOTLIN MULTIPLATFORM CORE ===
|
# === FRONTEND: KOTLIN MULTIPLATFORM CORE ===
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
kotlin-stdlib-wasm-js = { module = "org.jetbrains.kotlin:kotlin-stdlib-wasm-js", version.ref = "kotlin" }
|
||||||
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
||||||
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
|
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
|
||||||
kotlin-test-junit5 = { module = "org.jetbrains.kotlin:kotlin-test-junit5", version.ref = "kotlin" }
|
kotlin-test-junit5 = { module = "org.jetbrains.kotlin:kotlin-test-junit5", version.ref = "kotlin" }
|
||||||
|
|
@ -129,6 +131,7 @@ androidx-lifecycle-viewmodelCompose = { module = "org.jetbrains.androidx.lifecyc
|
||||||
androidx-lifecycle-runtimeCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
|
androidx-lifecycle-runtimeCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
|
||||||
ui-desktop = { module = "androidx.compose.ui:ui-desktop", version.ref = "uiDesktop" }
|
ui-desktop = { module = "androidx.compose.ui:ui-desktop", version.ref = "uiDesktop" }
|
||||||
composeHotReloadApi = { module = "org.jetbrains.compose.hot-reload:hot-reload-runtime-api", version.ref = "composeHotReload" }
|
composeHotReloadApi = { module = "org.jetbrains.compose.hot-reload:hot-reload-runtime-api", version.ref = "composeHotReload" }
|
||||||
|
compose-materialIconsExtended = { module = "org.jetbrains.compose.material:material-icons-extended", version.ref = "materialIconsExtended" }
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# === FRONTEND: NETWORK (KTOR CLIENT) ===
|
# === FRONTEND: NETWORK (KTOR CLIENT) ===
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@ enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
|
||||||
|
|
||||||
pluginManagement {
|
pluginManagement {
|
||||||
repositories {
|
repositories {
|
||||||
gradlePluginPortal()
|
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
google()
|
google()
|
||||||
|
gradlePluginPortal()
|
||||||
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
|
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
|
||||||
maven("https://us-central1-maven.pkg.dev/varabyte-repos/public")
|
maven("https://us-central1-maven.pkg.dev/varabyte-repos/public")
|
||||||
maven("https://oss.sonatype.org/content/repositories/snapshots/")
|
maven("https://oss.sonatype.org/content/repositories/snapshots/")
|
||||||
|
|
@ -21,9 +21,9 @@ plugins {
|
||||||
|
|
||||||
dependencyResolutionManagement {
|
dependencyResolutionManagement {
|
||||||
repositories {
|
repositories {
|
||||||
gradlePluginPortal()
|
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
google()
|
google()
|
||||||
|
gradlePluginPortal()
|
||||||
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
|
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
|
||||||
maven("https://us-central1-maven.pkg.dev/varabyte-repos/public")
|
maven("https://us-central1-maven.pkg.dev/varabyte-repos/public")
|
||||||
maven("https://oss.sonatype.org/content/repositories/snapshots/")
|
maven("https://oss.sonatype.org/content/repositories/snapshots/")
|
||||||
|
|
@ -156,6 +156,7 @@ include(":frontend:features:billing-feature")
|
||||||
|
|
||||||
// --- SHELLS ---
|
// --- SHELLS ---
|
||||||
include(":frontend:shells:meldestelle-desktop")
|
include(":frontend:shells:meldestelle-desktop")
|
||||||
|
include(":frontend:shells:meldestelle-web")
|
||||||
|
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
// PLATFORM
|
// PLATFORM
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user