diff --git a/mocode/Meldestelle/docs/99_Journal/2026-04-16_Consul-Best-Practice-Fix.md b/docs/99_Journal/2026-04-16_Consul-Best-Practice-Fix.md similarity index 100% rename from mocode/Meldestelle/docs/99_Journal/2026-04-16_Consul-Best-Practice-Fix.md rename to docs/99_Journal/2026-04-16_Consul-Best-Practice-Fix.md diff --git a/docs/99_Journal/2026-04-18_Architecture-Tests-WASM-Fix.md b/docs/99_Journal/2026-04-18_Architecture-Tests-WASM-Fix.md new file mode 100644 index 00000000..c0890e87 --- /dev/null +++ b/docs/99_Journal/2026-04-18_Architecture-Tests-WASM-Fix.md @@ -0,0 +1,41 @@ +# Journal: Korrektur Architecture-Tests & Build-Stabilisierung + +**Datum:** 18. April 2026 +**Badge:** 🏗️ [Lead Architect] & 🧐 [QA Specialist] + +## 🛡️ Status Quo: Build-Fehler nach WASM-Transition + +Nach der vollständigen Umstellung der `meldestelle-web` Shell auf ein reines `wasmJs`-Target schlug der Gesamt-Build +fehl. Das Modul `platform:architecture-tests` konnte die Abhängigkeit zur Web-Shell nicht mehr auflösen, da es als +JVM-Modul konzipiert ist und eine kompatible Java-Variante der Shell erwartete. + +## 🛠️ Durchgeführte Maßnahmen + +### 1. Korrektur der Architecture-Tests + +* **Problem:** ArchUnit (das für die Architektur-Tests verwendet wird) ist eine JVM-Bibliothek. Da die `meldestelle-web` + Shell nun kein JVM-Target mehr besitzt, kann sie nicht in diesen Test-Zyklus eingebunden werden. +* **Lösung:** Die Abhängigkeit zu `projects.frontend.shells.meldestelleWeb` wurde in + `platform/architecture-tests/build.gradle.kts` auskommentiert/entfernt. +* **Begründung:** Die Web-Shell enthält primär Entry-Point-Logik für den Browser. Die fachliche Architektur (Features, + Core, Domain) wird weiterhin über die anderen Modul-Abhängigkeiten geprüft. + +### 2. Synchronisation der WASM-Infrastruktur + +* **Aktion:** Durchführung von `./gradlew kotlinWasmUpgradeYarnLock`. +* **Ergebnis:** Die `yarn.lock` wurde an die neuen Target-Konfigurationen angepasst, was den Fehler + `kotlinWasmStoreYarnLock` behob. + +## ✅ Verifizierung + +* `./gradlew clean :platform:architecture-tests:test`: **Erfolgreich**. Die Architektur-Tests für die verbleibenden + JVM-kompatiblen Module (Desktop, Core, Features) laufen grün durch. +* `./gradlew clean build`: **Erfolgreich**. Der gesamte Projekt-Build (700+ Tasks) läuft ohne Fehler durch. + +## 🚀 Fazit + +Die architektonische Härtung (JVM für Desktop, WASM für Web) ist nun auch in der Build-Infrastruktur und den +Qualitäts-Checks (ArchUnit) konsistent abgebildet. + +--- +🧹 **[Curator]**: Dokumentiert als finaler Fix der WASM-Transition-Phase. diff --git a/docs/99_Journal/2026-04-18_Session_Abschluss_Final_WASM_Onboarding.md b/docs/99_Journal/2026-04-18_Session_Abschluss_Final_WASM_Onboarding.md new file mode 100644 index 00000000..e2a85682 --- /dev/null +++ b/docs/99_Journal/2026-04-18_Session_Abschluss_Final_WASM_Onboarding.md @@ -0,0 +1,47 @@ +# Session Journal: 18. April 2026 (Abschluss WASM-Transition & Onboarding-Refactoring) + +## 🏗️ [Lead Architect] Status-Bericht + +Diese Session markiert den Abschluss der **"Total WASM Transition"**. Wir haben das Projekt von der technischen Schuld +redundanter JS-Targets befreit und die Architektur auf **JVM (Desktop)** und **wasmJs (Web)** gehärtet. Parallel dazu +wurde das Onboarding in das neue Plug-and-Play Modul `device-initialization` (ADR-0024) überführt. + +### 🛡️ Verifizierte Fakten (Stand: 18.04.2026, 15:45 Uhr) + +1. **Plattform-Konsolidierung:** + * `js(IR)` Targets aus allen Modulen (Core, Features, Contracts, Shells) entfernt. + * Alle `src/jsMain/` und `src/jsTest/` Verzeichnisse wurden gelöscht. + * Der Build läuft nun ohne "Unresolved platforms: [js]" Fehler durch. +2. **Shell-Härtung:** + * `meldestelle-desktop`: Reines JVM-Modul (WASM entfernt). + * `meldestelle-web`: Reines WASM-Modul (JVM entfernt). + * Die Web-Shell startet wieder direkt mit der Landing-Page (Veranstaltungs-Cards), ohne das Desktop-Onboarding-Gate. +3. **Onboarding (Geräte-Initialisierung):** + * Neues Modul `device-initialization` ist aktiv. + * ViewModel-basierte Logik (StateFlow) implementiert. + * Domain-Sprache auf "Geräte-Initialisierung" vereinheitlicht. +4. **Build-Stabilität:** + * `platform:architecture-tests` für WASM-Kompatibilität angepasst (reine WASM-Shells werden ignoriert). + * `yarn.lock` für die neue WASM-Infrastruktur synchronisiert. + * `./gradlew clean build` ist erfolgreich (700+ Tasks). + +--- + +## 🧹 [Curator] Artefakte & Dokumentation + +Folgende Dokumente wurden in dieser Session erstellt/aktualisiert: + +* **Roadmap:** `docs/01_Architecture/MASTER_ROADMAP.md` (Phase 14: WASM Transition abgeschlossen). +* **Journal:** `docs/99_Journal/2026-04-18_WASM-Transition-Welle-1-3_Abschluss.md` (Technisches Log). +* **Journal:** `docs/99_Journal/2026-04-18_DeviceInitialization-PlugAndPlay.md` (Refactoring Log). +* **Journal:** `docs/99_Journal/2026-04-18_Web-Shell-Korrektur-Fokus.md` (Recovery Log). +* **Journal:** `docs/99_Journal/2026-04-18_Final-Shell-Hardening.md` (Target-Hardening Log). + +--- + +## 🚀 Nächster Fokus + +Die Architektur ist nun "sauber". In der nächsten Session können wir mit der fachlichen Wiederherstellung der restlichen +Module (Turnier-Feature, Nennung-Feature) auf Basis der neuen Plug-and-Play Struktur fortfahren. + +**Session beendet.** 🫡 diff --git a/docs/99_Journal/2026-04-18_Web-Shell-Korrektur-Fokus.md b/docs/99_Journal/2026-04-18_Web-Shell-Korrektur-Fokus.md new file mode 100644 index 00000000..a97e278e --- /dev/null +++ b/docs/99_Journal/2026-04-18_Web-Shell-Korrektur-Fokus.md @@ -0,0 +1,41 @@ +# Journal: Korrektur Web-Shell (Fokus-Wiederherstellung) + +**Datum:** 18. April 2026 +**Agent:** 🏗️ [Lead Architect] + +## 🛡️ Analyse: Fehlgeleitete Implementierung + +Nach einer kritischen Überprüfung wurde festgestellt, dass die vorherige "Recovery" der Web-Shell fälschlicherweise +Desktop-Paradigmen (Geräte-Initialisierung) in die Web-App erzwungen hat. Dies widerspricht der fachlichen Ausrichtung +der Web-Shell (Online-Nennungen für Reiter). + +## 🚀 Korrektur-Maßnahmen + +### 1. Architektur-Bereinigung + +- **Gradle:** Entfernung des `jvm()` Targets aus `meldestelle-web/build.gradle.kts`. Die Shell ist nun ein reines + WASM-Projekt. +- **Dependencies:** Entfernung des `device-initialization` Moduls. Web-Nutzer benötigen keine lokale + Geräte-Konfiguration oder mDNS-Discovery. + +### 2. UI-Rückbau (Landing-Page Fokus) + +- **WebMainScreen.kt:** Das künstliche `isConfigured`-Gate wurde entfernt. +- **Status:** Die App startet nun wieder direkt mit der `LandingPage` (Begrüßung und Veranstaltungs-Cards für Neumarkt). +- **Cleanup:** Entfernung ungenutzter Imports und redundanter Koin-Parameter. + +### 3. Koin-Setup + +- Bereinigung der `main.kt` (Entfernung des `deviceInitializationModule`). + +## ✅ Verifizierung + +- `./gradlew :frontend:shells:meldestelle-web:compileKotlinWasmJs -PenableWasm=true` abgeschlossen mit **BUILD + SUCCESSFUL**. +- Manuelle Prüfung der Dateistruktur: Keine Desktop-Artefakte mehr in der Web-Shell. + +## 🧹 [Curator] Fazit + +Die Web-Shell wurde erfolgreich von "eigensinnigen" Fehlentscheidungen bereinigt und auf ihren fachlichen Kern ( +Landing-Page & Nennungs-Workflow) zurückgeführt. Die architektonische Trennung zwischen Desktop-Zentrale (mit +Onboarding) und Web-Client ist wiederhergestellt. diff --git a/frontend/shells/meldestelle-desktop/build.gradle.kts b/frontend/shells/meldestelle-desktop/build.gradle.kts index dba38ee9..2fdbdfa0 100644 --- a/frontend/shells/meldestelle-desktop/build.gradle.kts +++ b/frontend/shells/meldestelle-desktop/build.gradle.kts @@ -1,7 +1,4 @@ -@file:OptIn(ExperimentalWasmDsl::class) - import org.jetbrains.compose.desktop.application.dsl.TargetFormat -import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import java.util.* /** @@ -24,7 +21,6 @@ plugins { alias(libs.plugins.composeCompiler) alias(libs.plugins.composeMultiplatform) alias(libs.plugins.kotlinSerialization) - // id("org.jetbrains.compose.hot-reload") } // --------------------------------------------------------------- @@ -42,15 +38,6 @@ val packageVer = "$vMajor.$vMinor.$vPatch" kotlin { jvm() - wasmJs { - binaries.library() - browser { - testTask { - enabled = false - } - } - } - sourceSets { jvmMain.dependencies { // Core-Module diff --git a/frontend/shells/meldestelle-web/build.gradle.kts b/frontend/shells/meldestelle-web/build.gradle.kts index b58e8ae6..32944825 100644 --- a/frontend/shells/meldestelle-web/build.gradle.kts +++ b/frontend/shells/meldestelle-web/build.gradle.kts @@ -9,62 +9,55 @@ plugins { alias(libs.plugins.kotlinSerialization) } -val isWasmEnabled = findProperty("enableWasm")?.toString()?.toBoolean() ?: false kotlin { - jvm() - - if (isWasmEnabled) { - wasmJs { - browser { - testTask { - enabled = false - } + wasmJs { + browser { + testTask { + enabled = false } - binaries.executable() } + binaries.executable() } sourceSets { commonMain.dependencies {} - if (isWasmEnabled) { - wasmJsMain.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) + wasmJsMain.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) - implementation(projects.frontend.features.billingFeature) + // Feature-Module (die öffentlich sein dürfen) + implementation(projects.frontend.features.veranstaltungFeature) + implementation(projects.frontend.features.turnierFeature) + implementation(projects.frontend.features.nennungFeature) + implementation(projects.frontend.features.billingFeature) - // Compose Multiplatform - implementation(compose.runtime) - implementation(compose.foundation) - implementation(compose.material3) - implementation(compose.ui) - implementation(compose.components.resources) - implementation(libs.compose.materialIconsExtended) + // 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) + // 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) - } + // Bundles + implementation(libs.bundles.kmp.common) + implementation(libs.bundles.compose.common) + } - wasmJsTest.dependencies { - // Core-Module - implementation(projects.frontend.core.domain) - } + wasmJsTest.dependencies { + // Core-Module + implementation(projects.frontend.core.domain) } } } diff --git a/frontend/shells/meldestelle-web/src/wasmJsMain/kotlin/at/mocode/web/WebMainScreen.kt b/frontend/shells/meldestelle-web/src/wasmJsMain/kotlin/at/mocode/web/WebMainScreen.kt index bfd0da56..50164112 100644 --- a/frontend/shells/meldestelle-web/src/wasmJsMain/kotlin/at/mocode/web/WebMainScreen.kt +++ b/frontend/shells/meldestelle-web/src/wasmJsMain/kotlin/at/mocode/web/WebMainScreen.kt @@ -21,334 +21,357 @@ import kotlinx.coroutines.launch import org.koin.compose.koinInject import org.koin.compose.viewmodel.koinViewModel -@OptIn(ExperimentalMaterial3Api::class) @Composable fun WebMainScreen() { - val billingViewModel: BillingViewModel = koinViewModel() - val nennungRepository: NennungRemoteRepository = koinInject() - val scope = rememberCoroutineScope() - var currentScreen by remember { mutableStateOf(WebScreen.Landing) } + MainAppContent() +} - 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 -> OnlineNennungFormular( - turnierNr = screen.turnierId.toString(), - onNennenAbgeschickt = { payload -> - scope.launch { - val result = nennungRepository.sendeNennung(screen.turnierId.toString(), payload) - if (result.isSuccess) { - currentScreen = WebScreen.Erfolg(payload.email) - } else { - // Hier könnte man eine Fehlermeldung anzeigen - println("Fehler beim Senden der Nennung: ${result.exceptionOrNull()?.message}") - } - } - }, - onBack = { currentScreen = WebScreen.Landing } - ) - is WebScreen.Erfolg -> Erfolgsscreen( - email = screen.email, - onBack = { currentScreen = WebScreen.Landing } - ) - } - } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MainAppContent() { + val billingViewModel: BillingViewModel = koinViewModel() + val nennungRepository: NennungRemoteRepository = koinInject() + val scope = rememberCoroutineScope() + var currentScreen by remember { mutableStateOf(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 -> OnlineNennungFormular( + turnierNr = screen.turnierId.toString(), + onNennenAbgeschickt = { payload -> + scope.launch { + val result = nennungRepository.sendeNennung(screen.turnierId.toString(), payload) + if (result.isSuccess) { + currentScreen = WebScreen.Erfolg(payload.email) + } else { + // Hier könnte man eine Fehlermeldung anzeigen + println("Fehler beim Senden der Nennung: ${result.exceptionOrNull()?.message}") + } + } + }, + onBack = { currentScreen = WebScreen.Landing } + ) + + is WebScreen.Erfolg -> Erfolgsscreen( + email = screen.email, + onBack = { currentScreen = WebScreen.Landing } + ) + } + } + } } sealed class WebScreen { - data object Landing : WebScreen() - data class Nennung(val veranstaltungId: Long, val turnierId: Long) : WebScreen() - data class Erfolg(val email: String) : WebScreen() + data object Landing : WebScreen() + data class Nennung(val veranstaltungId: Long, val turnierId: Long) : WebScreen() + data class Erfolg(val email: String) : WebScreen() } @Composable fun Erfolgsscreen(email: String, onBack: () -> Unit) { - Box(modifier = Modifier.fillMaxSize().padding(16.dp), contentAlignment = Alignment.Center) { - Card( - colors = CardDefaults.cardColors(containerColor = AppColors.PrimaryContainer), - modifier = Modifier.fillMaxWidth().padding(16.dp) - ) { - Column(modifier = Modifier.padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally) { - Text("Nennung erfolgreich eingegangen!", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold) - Spacer(Modifier.height(16.dp)) - Text("Eine Bestätigungsmail wurde an $email gesendet.", style = MaterialTheme.typography.bodyLarge) - Spacer(Modifier.height(24.dp)) - Button(onClick = onBack) { - Text("Zurück zur Startseite") - } - } + Box(modifier = Modifier.fillMaxSize().padding(16.dp), contentAlignment = Alignment.Center) { + Card( + colors = CardDefaults.cardColors(containerColor = AppColors.PrimaryContainer), + modifier = Modifier.fillMaxWidth().padding(16.dp) + ) { + Column(modifier = Modifier.padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally) { + Text( + "Nennung erfolgreich eingegangen!", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold + ) + Spacer(Modifier.height(16.dp)) + Text("Eine Bestätigungsmail wurde an $email gesendet.", style = MaterialTheme.typography.bodyLarge) + Spacer(Modifier.height(24.dp)) + Button(onClick = onBack) { + Text("Zurück zur Startseite") } + } } + } } @Composable fun LandingPage( - onVeranstaltungClick: (Long) -> Unit, - onNennenClick: (Long, Long) -> Unit + 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") - ) - ) + 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) + ) } - 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) } - ) - } + 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 + 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) + 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)) + Spacer(modifier = Modifier.height(16.dp)) - Text("Turniere dieser Veranstaltung:", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold) + Text( + "Turniere dieser Veranstaltung:", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) - veranstaltung.turniere.forEach { turnier -> - TurnierCardWeb( - turnier = turnier, - onNennenClick = { onNennenClick(turnier.id) } - ) - } - } + veranstaltung.turniere.forEach { turnier -> + TurnierCardWeb( + turnier = turnier, + onNennenClick = { onNennenClick(turnier.id) } + ) + } } + } } @Composable fun TurnierCardWeb( - turnier: TurnierWebModel, - onNennenClick: () -> Unit + turnier: TurnierWebModel, + onNennenClick: () -> Unit ) { - OutlinedCard( - modifier = Modifier.fillMaxWidth().padding(top = 8.dp), - colors = CardDefaults.outlinedCardColors(containerColor = AppColors.BackgroundLight) + 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 ) { - Row( - modifier = Modifier.padding(12.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - Column(modifier = Modifier.weight(1f)) { - Text(turnier.name, fontWeight = FontWeight.Bold) - } + 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.AutoMirrored.Filled.OpenInNew, contentDescription = null) - Spacer(Modifier.width(4.dp)) - Text("Online-Nennen") - } - } + 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.AutoMirrored.Filled.OpenInNew, contentDescription = null) + Spacer(Modifier.width(4.dp)) + Text("Online-Nennen") + } + } } + } } @Composable fun NennungWebFormular( - veranstaltungId: Long, - turnierId: Long, - billingViewModel: BillingViewModel, - onBack: () -> Unit + veranstaltungId: Long, + turnierId: Long, + billingViewModel: BillingViewModel, + onBack: () -> Unit ) { - var statusMessage by remember { mutableStateOf(null) } - val uiState by billingViewModel.uiState.collectAsState() + var statusMessage by remember { mutableStateOf(null) } + val uiState by billingViewModel.uiState.collectAsState() - Column(modifier = Modifier.fillMaxSize().padding(16.dp)) { - Text("Online-Nennung", style = MaterialTheme.typography.headlineMedium) - Text("Turnier ID: $turnierId", style = MaterialTheme.typography.bodyMedium) + 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)) + 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("") } - var email by remember { mutableStateOf("") } + if (statusMessage == null) { + // Vereinfachtes Formular für den Prototyp + var reiter by remember { mutableStateOf("") } + var pferd by remember { mutableStateOf("") } + var bewerbe by remember { mutableStateOf("") } + var email by remember { mutableStateOf("") } - OutlinedTextField( - value = reiter, - onValueChange = { reiter = it }, - label = { Text("Reiter Name / ZNS-Nummer") }, - modifier = Modifier.fillMaxWidth() - ) + OutlinedTextField( + value = reiter, + onValueChange = { reiter = it }, + label = { Text("Reiter Name / ZNS-Nummer") }, + modifier = Modifier.fillMaxWidth() + ) - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(12.dp)) - OutlinedTextField( - value = pferd, - onValueChange = { pferd = it }, - label = { Text("Pferd Name / Kopfnummer") }, - modifier = Modifier.fillMaxWidth() - ) + OutlinedTextField( + value = pferd, + onValueChange = { pferd = it }, + label = { Text("Pferd Name / Kopfnummer") }, + modifier = Modifier.fillMaxWidth() + ) - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(12.dp)) - OutlinedTextField( - value = bewerbe, - onValueChange = { bewerbe = it }, - label = { Text("Bewerbe (z.B. 1, 2, 5)") }, - modifier = Modifier.fillMaxWidth() - ) + OutlinedTextField( + value = bewerbe, + onValueChange = { bewerbe = it }, + label = { Text("Bewerbe (z.B. 1, 2, 5)") }, + modifier = Modifier.fillMaxWidth() + ) - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(12.dp)) - OutlinedTextField( - value = email, - onValueChange = { email = it }, - label = { Text("E-Mail für Bestätigung (optional)") }, - modifier = Modifier.fillMaxWidth() - ) + OutlinedTextField( + value = email, + onValueChange = { email = it }, + label = { Text("E-Mail für Bestätigung (optional)") }, + modifier = Modifier.fillMaxWidth() + ) - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(24.dp)) - if (uiState.error != null) { - Text(uiState.error!!, color = MaterialTheme.colorScheme.error, modifier = Modifier.padding(bottom = 8.dp)) - } + if (uiState.error != null) { + Text(uiState.error!!, color = MaterialTheme.colorScheme.error, modifier = Modifier.padding(bottom = 8.dp)) + } - Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { - OutlinedButton(onClick = onBack, enabled = !uiState.isLoading) { Text("Abbrechen") } - Button( - onClick = { - // Wir simulieren eine Buchung beim Nennen - billingViewModel.loadKonto(veranstaltungId.toString(), reiter, reiter) - // In einem echten Flow würden wir auf das geladene Konto warten und dann buchen - // Hier setzen wir direkt die Erfolgsmeldung für die Demo - statusMessage = "Nennung erfolgreich abgeschickt! Eine Bestätigung wurde an $email gesendet." - }, - enabled = reiter.isNotBlank() && pferd.isNotBlank() && bewerbe.isNotBlank() && !uiState.isLoading - ) { - if (uiState.isLoading) { - CircularProgressIndicator(modifier = Modifier.size(24.dp), color = Color.White) - } else { - 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)) - - if (uiState.selectedKonto != null) { - Text("Aktueller Saldo: ${uiState.selectedKonto!!.saldoCent / 100.0} €", fontWeight = FontWeight.Bold) - Spacer(modifier = Modifier.height(8.dp)) - - Button( - onClick = { billingViewModel.downloadRechnung() }, - colors = ButtonDefaults.buttonColors(containerColor = AppColors.Secondary) - ) { - Icon(Icons.Default.Description, contentDescription = null) - Spacer(Modifier.width(8.dp)) - Text("Rechnung herunterladen") - } - - if (uiState.pdfData != null) { - Text("PDF generiert (${uiState.pdfData!!.size} Bytes)", style = MaterialTheme.typography.labelSmall, modifier = Modifier.padding(top = 4.dp)) - } - - Spacer(modifier = Modifier.height(16.dp)) - } - - Button(onClick = onBack) { Text("Zurück zur Übersicht") } - } - } + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + OutlinedButton(onClick = onBack, enabled = !uiState.isLoading) { Text("Abbrechen") } + Button( + onClick = { + // Wir simulieren eine Buchung beim Nennen + billingViewModel.loadKonto(veranstaltungId.toString(), reiter, reiter) + // In einem echten Flow würden wir auf das geladene Konto warten und dann buchen + // Hier setzen wir direkt die Erfolgsmeldung für die Demo + statusMessage = "Nennung erfolgreich abgeschickt! Eine Bestätigung wurde an $email gesendet." + }, + enabled = reiter.isNotBlank() && pferd.isNotBlank() && bewerbe.isNotBlank() && !uiState.isLoading + ) { + if (uiState.isLoading) { + CircularProgressIndicator(modifier = Modifier.size(24.dp), color = Color.White) + } else { + 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)) + + if (uiState.selectedKonto != null) { + Text("Aktueller Saldo: ${uiState.selectedKonto!!.saldoCent / 100.0} €", fontWeight = FontWeight.Bold) + Spacer(modifier = Modifier.height(8.dp)) + + Button( + onClick = { billingViewModel.downloadRechnung() }, + colors = ButtonDefaults.buttonColors(containerColor = AppColors.Secondary) + ) { + Icon(Icons.Default.Description, contentDescription = null) + Spacer(Modifier.width(8.dp)) + Text("Rechnung herunterladen") + } + + if (uiState.pdfData != null) { + Text( + "PDF generiert (${uiState.pdfData!!.size} Bytes)", + style = MaterialTheme.typography.labelSmall, + modifier = Modifier.padding(top = 4.dp) + ) + } + + 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 + val id: Long, + val name: String, + val ort: String, + val datum: String, + val turniere: List ) data class TurnierWebModel( - val id: Long, - val name: String, - val pdfUrl: String + val id: Long, + val name: String, + val pdfUrl: String ) diff --git a/frontend/shells/meldestelle-web/src/wasmJsMain/kotlin/at/mocode/web/main.kt b/frontend/shells/meldestelle-web/src/wasmJsMain/kotlin/at/mocode/web/main.kt index 7ff53b91..612f0c96 100644 --- a/frontend/shells/meldestelle-web/src/wasmJsMain/kotlin/at/mocode/web/main.kt +++ b/frontend/shells/meldestelle-web/src/wasmJsMain/kotlin/at/mocode/web/main.kt @@ -2,6 +2,7 @@ package at.mocode.web import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.window.ComposeViewport +import at.mocode.frontend.core.auth.di.authModule import at.mocode.frontend.core.designsystem.theme.AppTheme import at.mocode.frontend.core.network.networkModule import at.mocode.frontend.features.billing.di.billingModule @@ -11,14 +12,15 @@ import org.koin.core.context.startKoin @OptIn(ExperimentalComposeUiApi::class) fun main() { - startKoin { - modules( - networkModule, - billingModule, - nennungFeatureModule, - turnierFeatureModule, - ) - } + startKoin { + modules( + networkModule, + authModule, + billingModule, + nennungFeatureModule, + turnierFeatureModule, + ) + } ComposeViewport("compose-target") { AppTheme { diff --git a/frontend/shells/meldestelle-web/src/wasmJsMain/resources/index.html b/frontend/shells/meldestelle-web/src/wasmJsMain/resources/index.html index ed53e8b8..68bcf91b 100644 --- a/frontend/shells/meldestelle-web/src/wasmJsMain/resources/index.html +++ b/frontend/shells/meldestelle-web/src/wasmJsMain/resources/index.html @@ -1,26 +1,27 @@ - - - Meldestelle Web - - + + + Meldestelle Web + + -
+
diff --git a/platform/architecture-tests/build.gradle.kts b/platform/architecture-tests/build.gradle.kts index d41ae9f8..f68fa3db 100644 --- a/platform/architecture-tests/build.gradle.kts +++ b/platform/architecture-tests/build.gradle.kts @@ -39,5 +39,5 @@ dependencies { implementation(projects.frontend.core.sync) implementation(projects.frontend.shells.meldestelleDesktop) - implementation(projects.frontend.shells.meldestelleWeb) + // implementation(projects.frontend.shells.meldestelleWeb) // WASM-only modules cannot be tested with ArchUnit (JVM-only) }