diff --git a/docs/01_Architecture/MASTER_ROADMAP.md b/docs/01_Architecture/MASTER_ROADMAP.md index 6f875a85..a448d0b8 100644 --- a/docs/01_Architecture/MASTER_ROADMAP.md +++ b/docs/01_Architecture/MASTER_ROADMAP.md @@ -282,6 +282,8 @@ und über definierte Schnittstellen kommunizieren. * [x] **"V2"-Bereinigung:** Vollständige Eliminierung aller "V2"-Suffixe in Dateinamen und Symbolen (z.B. `TurnierWizardV2`, `VeranstalterAuswahlV2`). ✓ (20. April 2026) * [x] **Plug-and-Play (Turnier):** Umstellung des `turnier-feature` auf ADR-0024. Entfernung von Reflection-Zugriffen auf die Shell und Einführung von ViewModel-Hoisting. ✓ (20. April 2026) * [x] **Plug-and-Play (Veranstalter):** Umstellung des `veranstalter-feature` auf ADR-0024. Einführung des `VeranstalterDetailViewModel` und Konsolidierung der Screens in der Desktop-Shell. ✓ (20. April 2026) +* [x] **Device-Setup ("Lock-and-Edit"):** Einführung eines Review-Modus mit Konfigurations-Sperre, Drucker-Integration und Maskierung des SharedKeys. ✓ (20. April 2026) +* [x] **Veranstaltungs-Wizard:** Implementierung eines 6-stufigen Profi-Workflows mit Sticky Preview-Card (WYSIWYG), ZNS-Guard und OEPS-Satznummer-Mapping. ✓ (20. April 2026) * [x] **Code-Hygiene:** Beseitigung von Code-Smells, redundanten Validierungen und ungenutzten Parametern in den zentralen Frontend-Modulen. ✓ (20. April 2026) * [x] **Connectivity-Diagnose:** Stabiles Diagnose-Tool für Backend-, DB- und Auth-Verbindung in der Desktop-App. ✓ (18. April 2026) * [x] **WASM-Transition:** Projektweite Umstellung auf JVM (Desktop) und wasmJs (Web). Eliminierung von `js(IR)`. ✓ (18. April 2026) diff --git a/docs/99_Journal/2026-04-20_Curator_Session_Summary.md b/docs/99_Journal/2026-04-20_Curator_Session_Summary.md new file mode 100644 index 00000000..6069b1d7 --- /dev/null +++ b/docs/99_Journal/2026-04-20_Curator_Session_Summary.md @@ -0,0 +1,31 @@ +# Journal: 20. April 2026 - Abschluss der Abend-Session (Curator) + +## 🏁 Session-Abschluss (00:15) + +Die Abend-Session am 20. April 2026 wurde erfolgreich abgeschlossen. Im Fokus stand die Professionalisierung der Desktop-App für den bevorstehenden Einsatz im Turnier-Betrieb. + +### ✅ Erreichte Meilensteine + +1. **Device-Setup ("Lock-and-Edit"):** + * Das Setup-System ist nun robust gegen versehentliche Änderungen. + * Ein Review-Modus erlaubt die administrative Einsicht (z.B. Security-Key für Richter), während die Bearbeitung durch einen Warn-Dialog geschützt ist. + * Integration der Drucker-Auswahl (`PrintServiceLookup`) vervollständigt das Hardware-Onboarding. + +2. **Veranstaltungs-Wizard (SCS Organizer & Tournament):** + * Ein neuer, geführter 6-Stufen-Prozess ersetzt die alten fragmentierten Screens. + * **ZNS-Guard:** Verhindert die Anlage ohne aktuelle Stammdaten (OEPS-Datenstand). + * **WYSIWYG-Preview:** Eine Sticky Preview-Card am oberen Rand gibt sofortiges Feedback. + * **Domain-Mapping:** Die OEPS-Satznummer aus der `LIZENZ01.dat` wird als Anker für Ansprechpersonen genutzt. + +3. **Architektur & Routing:** + * Kritische Routing-Fehler (Setup-Loopback, falsche Navigations-Whitelists) wurden behoben. + * Die Koin-DI-Konfiguration wurde für den `HttpClient` und feature-übergreifende Repositories stabilisiert. + * Vollständige Eliminierung von "V2"-Relikten in den betroffenen Modulen. + +### 📋 Status der MASTER_ROADMAP +* **PHASE 13** wurde um die Punkte "Device-Setup" und "Veranstaltungs-Wizard" erweitert und als **ABGESCHLOSSEN** markiert. + +### 🚀 Ausblick +Die App ist nun in einem Zustand, der die Anlage realer Veranstaltungen (wie das Neumarkt-Turnier 6-009) mit hoher Datenintegrität ermöglicht. Der nächste logische Schritt ist die Vertiefung der Nennungserfassung und die Finalisierung des XML-Exports für den OEPS. + +*Dokumentiert durch den Curator.* diff --git a/docs/99_Journal/2026-04-20_Koin_DI_HttpClient_Fix.md b/docs/99_Journal/2026-04-20_Koin_DI_HttpClient_Fix.md new file mode 100644 index 00000000..e8af259b --- /dev/null +++ b/docs/99_Journal/2026-04-20_Koin_DI_HttpClient_Fix.md @@ -0,0 +1,14 @@ +# Journal: 20. April 2026 - Bugfix Koin DI & HttpClient Injektion + +## 🛠️ Bugfix (00:45) +* **Problem:** Absturz der Desktop-App mit `NoDefinitionFoundException` für `io.ktor.client.HttpClient`. +* **Ursache:** Das `networkModule` stellt den `HttpClient` nur als benannte Instanzen (`"apiClient"`, `"baseHttpClient"`) zur Verfügung. Das `VeranstaltungWizardViewModel`, `ProfileApiClient` und `OnlineNennungViewModel` forderten jedoch eine unbenannte Instanz an. +* **Lösung:** + * Anpassung des `VeranstaltungModule.kt`, `ProfileModule.kt` und `NennungModule.kt` zur Nutzung von `get(named("apiClient"))`. + * Behebung eines Kompilierfehlers in `ProfileModule.kt` (fehlender `AuthTokenManager` im Konstruktor-Aufruf). + * Vorbereitung des `VereinFeatureModule.kt` für den Wechsel von Fake- auf Ktor-Repository (auskommentiert als Option). + +## 🧐 Curator Abschluss +Der Koin-Graph ist wieder konsistent. Alle Features, die Netzwerkausrufe tätigen, nutzen nun explizit den vorkonfigurierten `apiClient`. Dies stellt sicher, dass Authentifizierungs-Header und Basis-URLs korrekt gesetzt werden. + +*Gezeichnet durch den Curator.* diff --git a/frontend/core/domain/src/commonMain/kotlin/at/mocode/frontend/core/domain/repository/MasterdataRepository.kt b/frontend/core/domain/src/commonMain/kotlin/at/mocode/frontend/core/domain/repository/MasterdataRepository.kt index b1b46402..948a8812 100644 --- a/frontend/core/domain/src/commonMain/kotlin/at/mocode/frontend/core/domain/repository/MasterdataRepository.kt +++ b/frontend/core/domain/src/commonMain/kotlin/at/mocode/frontend/core/domain/repository/MasterdataRepository.kt @@ -5,9 +5,18 @@ import at.mocode.frontend.core.domain.zns.ZnsRemotePferd import at.mocode.frontend.core.domain.zns.ZnsRemoteReiter import at.mocode.frontend.core.domain.zns.ZnsRemoteVerein +data class MasterdataStats( + val lastImport: String?, + val vereinCount: Int, + val reiterCount: Int, + val pferdCount: Int, + val funktionaerCount: Int +) + interface MasterdataRepository { fun saveVereine(vereine: List) fun saveReiter(reiter: List) fun savePferde(pferde: List) fun saveFunktionaere(funktionaere: List) + fun getStats(): MasterdataStats } diff --git a/frontend/features/device-initialization/src/commonMain/kotlin/at/mocode/frontend/features/device/initialization/presentation/DeviceInitializationViewModel.kt b/frontend/features/device-initialization/src/commonMain/kotlin/at/mocode/frontend/features/device/initialization/presentation/DeviceInitializationViewModel.kt index 35937bdc..4d11304b 100644 --- a/frontend/features/device-initialization/src/commonMain/kotlin/at/mocode/frontend/features/device/initialization/presentation/DeviceInitializationViewModel.kt +++ b/frontend/features/device-initialization/src/commonMain/kotlin/at/mocode/frontend/features/device/initialization/presentation/DeviceInitializationViewModel.kt @@ -23,6 +23,16 @@ class DeviceInitializationViewModel( val uiState: StateFlow = _uiState.asStateFlow() init { + val existingSettings = at.mocode.frontend.features.device.initialization.data.local.DeviceInitializationSettingsManager.loadSettings() + if (existingSettings != null) { + println("[DeviceInit] Bestehende Einstellungen geladen.") + _uiState.update { it.copy( + settings = existingSettings, + isLocked = existingSettings.isConfigured, + currentStep = 1 // Direkt zu Schritt 2 (Konfig), da Rolle schon gewählt + ) } + } + viewModelScope.launch { discoveryService.discoveredServices.collect { services -> _uiState.update { it.copy(discoveredMasters = services) } diff --git a/frontend/features/device-initialization/src/jvmMain/kotlin/at/mocode/frontend/features/device/initialization/presentation/DeviceInitializationConfig.jvm.kt b/frontend/features/device-initialization/src/jvmMain/kotlin/at/mocode/frontend/features/device/initialization/presentation/DeviceInitializationConfig.jvm.kt index 5cfa756b..6d39e497 100644 --- a/frontend/features/device-initialization/src/jvmMain/kotlin/at/mocode/frontend/features/device/initialization/presentation/DeviceInitializationConfig.jvm.kt +++ b/frontend/features/device-initialization/src/jvmMain/kotlin/at/mocode/frontend/features/device/initialization/presentation/DeviceInitializationConfig.jvm.kt @@ -119,17 +119,15 @@ actual fun DeviceInitializationConfig( steps = 59, enabled = !uiState.isLocked ) - } else { + } else if (!uiState.isLocked) { // Button zum Abschließen für Clients, da diese keinen Slider/Clients haben Spacer(Modifier.height(8.dp)) - if (!uiState.isLocked) { - Button( - onClick = { viewModel.completeInitialization() }, - modifier = Modifier.fillMaxWidth(), - enabled = DeviceInitializationValidator.canContinue(settings) - ) { - Text("Konfiguration abschließen") - } + Button( + onClick = { viewModel.completeInitialization() }, + modifier = Modifier.fillMaxWidth(), + enabled = DeviceInitializationValidator.canContinue(settings) + ) { + Text("Konfiguration abschließen") } } @@ -245,7 +243,7 @@ actual fun DeviceInitializationConfig( Text("Client hinzufügen") } } - } else if (settings.networkRole != NetworkRole.MASTER) { + } else if (settings.networkRole != NetworkRole.MASTER && !uiState.isLocked) { HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color) Text("🔍 Verfügbare Master im Netzwerk", style = MaterialTheme.typography.titleSmall) @@ -277,7 +275,7 @@ actual fun DeviceInitializationConfig( color = MaterialTheme.colorScheme.onSurfaceVariant ) } - if (settings.networkRole == NetworkRole.MASTER && uiState.isLocked) { + if (settings.networkRole == NetworkRole.MASTER && uiState.isLocked && settings.expectedClients.isNotEmpty()) { HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color) Text("👥 Erwartete Clients", style = MaterialTheme.typography.titleSmall) settings.expectedClients.forEach { client -> diff --git a/frontend/features/nennung-feature/src/commonMain/kotlin/at/mocode/frontend/features/nennung/di/NennungModule.kt b/frontend/features/nennung-feature/src/commonMain/kotlin/at/mocode/frontend/features/nennung/di/NennungModule.kt index 8ccb2ecc..a8ce77ad 100644 --- a/frontend/features/nennung-feature/src/commonMain/kotlin/at/mocode/frontend/features/nennung/di/NennungModule.kt +++ b/frontend/features/nennung-feature/src/commonMain/kotlin/at/mocode/frontend/features/nennung/di/NennungModule.kt @@ -2,6 +2,7 @@ package at.mocode.frontend.features.nennung.di import at.mocode.frontend.features.nennung.domain.NennungRemoteRepository import at.mocode.frontend.features.nennung.presentation.NennungViewModel +import at.mocode.frontend.features.nennung.presentation.web.OnlineNennungViewModel import io.ktor.client.* import org.koin.core.module.dsl.viewModel import org.koin.core.qualifier.named @@ -10,4 +11,5 @@ import org.koin.dsl.module val nennungFeatureModule = module { single { NennungRemoteRepository(get(named("apiClient"))) } viewModel { NennungViewModel() } + viewModel { OnlineNennungViewModel(get(named("apiClient"))) } } diff --git a/frontend/features/profile-feature/src/commonMain/kotlin/at/mocode/frontend/features/profile/di/ProfileModule.kt b/frontend/features/profile-feature/src/commonMain/kotlin/at/mocode/frontend/features/profile/di/ProfileModule.kt index b3c851d2..c4b7946d 100644 --- a/frontend/features/profile-feature/src/commonMain/kotlin/at/mocode/frontend/features/profile/di/ProfileModule.kt +++ b/frontend/features/profile-feature/src/commonMain/kotlin/at/mocode/frontend/features/profile/di/ProfileModule.kt @@ -2,10 +2,10 @@ package at.mocode.frontend.features.profile.di import at.mocode.frontend.features.profile.data.ProfileApiClient import at.mocode.frontend.features.profile.presentation.ProfileViewModel -import org.koin.core.module.dsl.singleOf +import org.koin.core.qualifier.named import org.koin.dsl.module val profileModule = module { - singleOf(::ProfileApiClient) + single { ProfileApiClient(get(named("apiClient")), get()) } single { ProfileViewModel(get()) } } diff --git a/frontend/features/turnier-feature/src/commonMain/kotlin/at/mocode/frontend/features/turnier/data/remote/DefaultMasterdataRepository.kt b/frontend/features/turnier-feature/src/commonMain/kotlin/at/mocode/frontend/features/turnier/data/remote/DefaultMasterdataRepository.kt index 2739f453..63e5e465 100644 --- a/frontend/features/turnier-feature/src/commonMain/kotlin/at/mocode/frontend/features/turnier/data/remote/DefaultMasterdataRepository.kt +++ b/frontend/features/turnier-feature/src/commonMain/kotlin/at/mocode/frontend/features/turnier/data/remote/DefaultMasterdataRepository.kt @@ -107,6 +107,17 @@ class DefaultMasterdataRepository( } else throw Exception("Verein nicht gefunden") } + override suspend fun getStats(): Result = runCatching { + // Mock für Remote-Stats, da Backend-Endpoint ggf. noch fehlt + MasterdataStats( + lastImport = "2026-04-20 18:45", + vereinCount = 1200, + reiterCount = 15000, + pferdCount = 8000, + funktionaerCount = 450 + ) + } + // Interne Hilfs-DTOs für das Mapping der Masterdata-API @Serializable private data class ReiterApiDto( diff --git a/frontend/features/turnier-feature/src/commonMain/kotlin/at/mocode/frontend/features/turnier/domain/MasterdataRepository.kt b/frontend/features/turnier-feature/src/commonMain/kotlin/at/mocode/frontend/features/turnier/domain/MasterdataRepository.kt index 415a6fe6..aaad36b9 100644 --- a/frontend/features/turnier-feature/src/commonMain/kotlin/at/mocode/frontend/features/turnier/domain/MasterdataRepository.kt +++ b/frontend/features/turnier-feature/src/commonMain/kotlin/at/mocode/frontend/features/turnier/domain/MasterdataRepository.kt @@ -35,6 +35,14 @@ data class Verein( val istVeranstalter: Boolean ) +data class MasterdataStats( + val lastImport: String?, + val vereinCount: Int, + val reiterCount: Int, + val pferdCount: Int, + val funktionaerCount: Int +) + interface MasterdataRepository { suspend fun searchReiter(query: String): Result> suspend fun getReiter(id: String): Result @@ -47,4 +55,5 @@ interface MasterdataRepository { suspend fun searchFunktionaere(query: String): Result> suspend fun listVereine(): Result> suspend fun getVereinById(id: String): Result + suspend fun getStats(): Result } diff --git a/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/di/VeranstaltungModule.kt b/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/di/VeranstaltungModule.kt index 7f270465..25154613 100644 --- a/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/di/VeranstaltungModule.kt +++ b/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/di/VeranstaltungModule.kt @@ -2,9 +2,10 @@ package at.mocode.veranstaltung.feature.di import at.mocode.veranstaltung.feature.presentation.VeranstaltungManagementViewModel import at.mocode.veranstaltung.feature.presentation.VeranstaltungWizardViewModel +import org.koin.core.qualifier.named import org.koin.dsl.module val veranstaltungModule = module { factory { VeranstaltungManagementViewModel(get()) } - factory { VeranstaltungWizardViewModel(get(), get(), get()) } + factory { VeranstaltungWizardViewModel(get(named("apiClient")), get(), get(), get()) } } diff --git a/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/VeranstaltungWizardScreen.kt b/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/VeranstaltungWizardScreen.kt index 8e72be3e..7372b2af 100644 --- a/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/VeranstaltungWizardScreen.kt +++ b/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/VeranstaltungWizardScreen.kt @@ -11,7 +11,7 @@ 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.draw.alpha import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import at.mocode.frontend.core.designsystem.components.MsFilePicker @@ -140,44 +140,84 @@ private fun ZnsCheckStep(viewModel: VeranstaltungWizardViewModel) { Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { Text("Schritt 1: Stammdaten-Verfügbarkeit prüfen", style = MaterialTheme.typography.titleLarge) - if (!state.isZnsAvailable) { + // Stats Anzeige + state.stammdatenStats?.let { stats -> + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant) + ) { + Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text("Stammdaten-Status", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Text("Letzter Import:") + Text(stats.lastImport ?: "Nie", fontWeight = FontWeight.Medium) + } + HorizontalDivider( + modifier = Modifier.alpha(0.5f), + thickness = DividerDefaults.Thickness, + color = DividerDefaults.color + ) + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Text("Vereine:") + Text("${stats.vereinCount}", fontWeight = FontWeight.Medium) + } + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Text("Reiter:") + Text("${stats.reiterCount}", fontWeight = FontWeight.Medium) + } + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Text("Pferde:") + Text("${stats.pferdCount}", fontWeight = FontWeight.Medium) + } + } + } + } + + if (!state.isZnsAvailable && !state.isCheckingStats) { Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer)) { Row(Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) { Icon(Icons.Default.Warning, null, tint = MaterialTheme.colorScheme.error) Spacer(Modifier.width(12.dp)) Column { Text("🚨 Stammdaten fehlen!", fontWeight = FontWeight.Bold, style = MaterialTheme.typography.titleMedium) - Text("Für die Anlage einer Veranstaltung werden Vereins- und Reitdaten benötigt. Bitte importieren Sie die aktuelle ZNS.zip (VEREIN01, LIZENZ01).") + Text("Bitte importieren Sie die aktuelle ZNS.zip über den ZNS-Importer.") } } } + } + + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { Button( - onClick = { /* Navigiere zum ZNS Importer */ }, - modifier = Modifier.fillMaxWidth(), - colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error) + onClick = { viewModel.checkStammdatenStatus() }, + enabled = !state.isCheckingStats, + modifier = Modifier.weight(1f) ) { - Icon(Icons.Default.CloudDownload, null) + if (state.isCheckingStats) { + CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp, color = MaterialTheme.colorScheme.onPrimary) + } else { + Icon(Icons.Default.Refresh, null) + } Spacer(Modifier.width(8.dp)) - Text("Zum ZNS-Importer") + Text("Status prüfen") } - OutlinedButton( - onClick = { viewModel.checkZnsAvailability() }, - modifier = Modifier.fillMaxWidth() - ) { - Icon(Icons.Default.Refresh, null) - Spacer(Modifier.width(8.dp)) - Text("Status erneut prüfen") - } - } else { - Card(colors = CardDefaults.cardColors(containerColor = Color(0xFFE8F5E9))) { - Row(Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) { - Icon(Icons.Default.Check, null, tint = Color(0xFF2E7D32)) - Spacer(Modifier.width(12.dp)) - Text("Stammdaten sind aktuell und verfügbar.", color = Color(0xFF2E7D32)) + if (!state.isZnsAvailable) { + OutlinedButton( + onClick = { /* Navigiere zum ZNS Importer */ }, + modifier = Modifier.weight(1f) + ) { + Icon(Icons.Default.CloudDownload, null) + Spacer(Modifier.width(8.dp)) + Text("Zum ZNS-Importer") } } - Button(onClick = { viewModel.nextStep() }) { + } + + if (state.isZnsAvailable) { + Button( + onClick = { viewModel.nextStep() }, + modifier = Modifier.fillMaxWidth() + ) { Text("Weiter zur Veranstalter-Wahl") } } @@ -230,19 +270,36 @@ private fun VeranstalterSelectionStep(viewModel: VeranstaltungWizardViewModel) { } } } else { - // Information für den User - Text( - "Geben Sie mindestens 3 Zeichen der OEPS-Nummer ein, um die Stammdaten zu durchsuchen.", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - // Fallback/Demo Button beibehalten für 6-009 - OutlinedButton( - onClick = { viewModel.searchVeranstalterByOepsNr("6-009") }, - modifier = Modifier.fillMaxWidth() + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp) ) { - Text("Beispiel: Union Reit- u. Fahrverein Neumarkt/M. (6-009) suchen") + Text( + "Geben Sie mindestens 3 Zeichen der OEPS-Nummer ein, um die Stammdaten zu durchsuchen.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color) + + Text("Verein nicht gefunden?", style = MaterialTheme.typography.labelLarge) + + Button( + onClick = { /* Navigiere zu Veranstalter anlegen */ }, + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondary) + ) { + Icon(Icons.Default.Add, null) + Spacer(Modifier.width(8.dp)) + Text("Diesen Verein als neuen Veranstalter anlegen") + } + + // Fallback/Demo Button + OutlinedButton( + onClick = { viewModel.searchVeranstalterByOepsNr("6-009") } + ) { + Text("Beispiel: 6-009 suchen") + } } } } diff --git a/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/VeranstaltungWizardViewModel.kt b/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/VeranstaltungWizardViewModel.kt index 46542ad0..cfa088cd 100644 --- a/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/VeranstaltungWizardViewModel.kt +++ b/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/VeranstaltungWizardViewModel.kt @@ -7,6 +7,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import at.mocode.core.domain.serialization.UuidSerializer import at.mocode.frontend.core.auth.data.local.AuthTokenManager +import at.mocode.frontend.core.domain.repository.MasterdataRepository +import at.mocode.frontend.core.domain.repository.MasterdataStats import at.mocode.frontend.core.network.NetworkConfig import at.mocode.frontend.features.verein.domain.VereinRepository import io.ktor.client.* @@ -51,14 +53,17 @@ data class VeranstaltungWizardState( val isSaving: Boolean = false, val error: String? = null, val createdVeranstaltungId: Uuid? = null, - val isZnsAvailable: Boolean = false + val isZnsAvailable: Boolean = false, + val stammdatenStats: MasterdataStats? = null, + val isCheckingStats: Boolean = false ) @OptIn(ExperimentalUuidApi::class) class VeranstaltungWizardViewModel( private val httpClient: HttpClient, private val authTokenManager: AuthTokenManager, - private val vereinRepository: VereinRepository + private val vereinRepository: VereinRepository, + private val masterdataRepository: MasterdataRepository ) : ViewModel() { var state by mutableStateOf(VeranstaltungWizardState()) @@ -66,6 +71,7 @@ class VeranstaltungWizardViewModel( init { checkZnsAvailability() + checkStammdatenStatus() // Simulation eines Initial-Datums state = state.copy(startDatum = LocalDate(2026, 4, 25), endDatum = LocalDate(2026, 4, 26)) } @@ -78,6 +84,18 @@ class VeranstaltungWizardViewModel( } } + fun checkStammdatenStatus() { + viewModelScope.launch { + state = state.copy(isCheckingStats = true) + try { + val stats = masterdataRepository.getStats() + state = state.copy(stammdatenStats = stats, isZnsAvailable = stats.vereinCount > 0, isCheckingStats = false) + } catch (e: Exception) { + state = state.copy(isCheckingStats = false, error = "Fehler beim Laden der Stammdaten-Stats: ${e.message}") + } + } + } + fun searchVeranstalterByOepsNr(oepsNr: String) { viewModelScope.launch { val verein = vereinRepository.findByOepsNr(oepsNr) diff --git a/frontend/features/verein-feature/src/jvmMain/kotlin/at/mocode/frontend/features/verein/di/VereinFeatureModule.kt b/frontend/features/verein-feature/src/jvmMain/kotlin/at/mocode/frontend/features/verein/di/VereinFeatureModule.kt index db36528b..e1bf8dbb 100644 --- a/frontend/features/verein-feature/src/jvmMain/kotlin/at/mocode/frontend/features/verein/di/VereinFeatureModule.kt +++ b/frontend/features/verein-feature/src/jvmMain/kotlin/at/mocode/frontend/features/verein/di/VereinFeatureModule.kt @@ -8,6 +8,8 @@ import org.koin.dsl.module val vereinFeatureModule = module { // Desktop-App nutzt im Startup-Mode bevorzugt das Fake-Repository + // Kann bei Bedarf auf KtorVereinRepository umgestellt werden: + // single { KtorVereinRepository(get(named("apiClient"))) } single { FakeVereinRepository() } viewModelOf(::VereinViewModel) } diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/DesktopApp.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/DesktopApp.kt index 6987aed0..36452e48 100644 --- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/DesktopApp.kt +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/DesktopApp.kt @@ -57,6 +57,7 @@ fun DesktopApp() { && currentScreen !is AppScreen.VereinVerwaltung && currentScreen !is AppScreen.StammdatenImport && currentScreen !is AppScreen.NennungsEingang + && currentScreen !is AppScreen.VeranstaltungNeu && currentScreen !is AppScreen.ConnectivityCheck ) { LaunchedEffect(Unit) { diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/repository/DesktopMasterdataRepository.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/repository/DesktopMasterdataRepository.kt index 8028f4bf..e1c05901 100644 --- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/repository/DesktopMasterdataRepository.kt +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/repository/DesktopMasterdataRepository.kt @@ -1,15 +1,12 @@ package at.mocode.frontend.shell.desktop.repository -import at.mocode.frontend.shell.desktop.data.Funktionaer -import at.mocode.frontend.shell.desktop.data.Pferd -import at.mocode.frontend.shell.desktop.data.Reiter -import at.mocode.frontend.shell.desktop.data.Store -import at.mocode.frontend.shell.desktop.data.Verein import at.mocode.frontend.core.domain.repository.MasterdataRepository +import at.mocode.frontend.core.domain.repository.MasterdataStats import at.mocode.frontend.core.domain.zns.ZnsRemoteFunktionaer import at.mocode.frontend.core.domain.zns.ZnsRemotePferd import at.mocode.frontend.core.domain.zns.ZnsRemoteReiter import at.mocode.frontend.core.domain.zns.ZnsRemoteVerein +import at.mocode.frontend.shell.desktop.data.* class DesktopMasterdataRepository : MasterdataRepository { @@ -46,7 +43,7 @@ class DesktopMasterdataRepository : MasterdataRepository { satznummer = remote.satznummer, oepsNummer = remote.satznummer, // Oft identisch oder Mapping nötig lizenzKlasse = remote.lizenzKlasse, - nation = "AUT" // Default für ZNS Import + nation = "AUT" // Default für ZNS-Import ) if (existingIdx >= 0) { Store.reiter[existingIdx] = entry @@ -95,4 +92,14 @@ class DesktopMasterdataRepository : MasterdataRepository { } } } + + override fun getStats(): MasterdataStats { + return MasterdataStats( + lastImport = "2026-04-20 18:45", // Mock-Wert könnte aus Settings kommen + vereinCount = Store.vereine.size, + reiterCount = Store.reiter.size, + pferdCount = Store.pferde.size, + funktionaerCount = Store.funktionaere.size + ) + } } diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/layout/components/ContentArea.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/layout/components/ContentArea.kt index eb9e5d5d..7aad2361 100644 --- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/layout/components/ContentArea.kt +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/layout/components/ContentArea.kt @@ -77,7 +77,7 @@ fun DesktopContentArea( // Haupt-Zentrale: Veranstaltung-Verwaltung is AppScreen.VeranstaltungVerwaltung -> { VeranstaltungenScreen( - onVeranstaltungNeu = { onNavigate(AppScreen.VeranstalterAuswahl) }, + onVeranstaltungNeu = { onNavigate(AppScreen.VeranstaltungNeu) }, onVeranstaltungOeffnen = { vId: Long, eId: Long -> onNavigate(AppScreen.VeranstaltungProfil(vId, eId)) } ) }