chore: implementiere Suche nach Veranstalter via OEPS-Nummer, verbessere UI-Flow im Veranstaltungs-Wizard und erweitere VereinRepository um OEPS-Abfrage
Some checks failed
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled
Some checks failed
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
This commit is contained in:
parent
30b53584f8
commit
7acd9ea4c2
|
|
@ -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] **"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 (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] **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] **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] **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)
|
* [x] **WASM-Transition:** Projektweite Umstellung auf JVM (Desktop) und wasmJs (Web). Eliminierung von `js(IR)`. ✓ (18. April 2026)
|
||||||
|
|
|
||||||
31
docs/99_Journal/2026-04-20_Curator_Session_Summary.md
Normal file
31
docs/99_Journal/2026-04-20_Curator_Session_Summary.md
Normal file
|
|
@ -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.*
|
||||||
14
docs/99_Journal/2026-04-20_Koin_DI_HttpClient_Fix.md
Normal file
14
docs/99_Journal/2026-04-20_Koin_DI_HttpClient_Fix.md
Normal file
|
|
@ -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.*
|
||||||
|
|
@ -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.ZnsRemoteReiter
|
||||||
import at.mocode.frontend.core.domain.zns.ZnsRemoteVerein
|
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 {
|
interface MasterdataRepository {
|
||||||
fun saveVereine(vereine: List<ZnsRemoteVerein>)
|
fun saveVereine(vereine: List<ZnsRemoteVerein>)
|
||||||
fun saveReiter(reiter: List<ZnsRemoteReiter>)
|
fun saveReiter(reiter: List<ZnsRemoteReiter>)
|
||||||
fun savePferde(pferde: List<ZnsRemotePferd>)
|
fun savePferde(pferde: List<ZnsRemotePferd>)
|
||||||
fun saveFunktionaere(funktionaere: List<ZnsRemoteFunktionaer>)
|
fun saveFunktionaere(funktionaere: List<ZnsRemoteFunktionaer>)
|
||||||
|
fun getStats(): MasterdataStats
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,16 @@ class DeviceInitializationViewModel(
|
||||||
val uiState: StateFlow<DeviceInitializationUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<DeviceInitializationUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
init {
|
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 {
|
viewModelScope.launch {
|
||||||
discoveryService.discoveredServices.collect { services ->
|
discoveryService.discoveredServices.collect { services ->
|
||||||
_uiState.update { it.copy(discoveredMasters = services) }
|
_uiState.update { it.copy(discoveredMasters = services) }
|
||||||
|
|
|
||||||
|
|
@ -119,17 +119,15 @@ actual fun DeviceInitializationConfig(
|
||||||
steps = 59,
|
steps = 59,
|
||||||
enabled = !uiState.isLocked
|
enabled = !uiState.isLocked
|
||||||
)
|
)
|
||||||
} else {
|
} else if (!uiState.isLocked) {
|
||||||
// Button zum Abschließen für Clients, da diese keinen Slider/Clients haben
|
// Button zum Abschließen für Clients, da diese keinen Slider/Clients haben
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
if (!uiState.isLocked) {
|
Button(
|
||||||
Button(
|
onClick = { viewModel.completeInitialization() },
|
||||||
onClick = { viewModel.completeInitialization() },
|
modifier = Modifier.fillMaxWidth(),
|
||||||
modifier = Modifier.fillMaxWidth(),
|
enabled = DeviceInitializationValidator.canContinue(settings)
|
||||||
enabled = DeviceInitializationValidator.canContinue(settings)
|
) {
|
||||||
) {
|
Text("Konfiguration abschließen")
|
||||||
Text("Konfiguration abschließen")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -245,7 +243,7 @@ actual fun DeviceInitializationConfig(
|
||||||
Text("Client hinzufügen")
|
Text("Client hinzufügen")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (settings.networkRole != NetworkRole.MASTER) {
|
} else if (settings.networkRole != NetworkRole.MASTER && !uiState.isLocked) {
|
||||||
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
|
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
|
||||||
Text("🔍 Verfügbare Master im Netzwerk", style = MaterialTheme.typography.titleSmall)
|
Text("🔍 Verfügbare Master im Netzwerk", style = MaterialTheme.typography.titleSmall)
|
||||||
|
|
||||||
|
|
@ -277,7 +275,7 @@ actual fun DeviceInitializationConfig(
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
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)
|
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
|
||||||
Text("👥 Erwartete Clients", style = MaterialTheme.typography.titleSmall)
|
Text("👥 Erwartete Clients", style = MaterialTheme.typography.titleSmall)
|
||||||
settings.expectedClients.forEach { client ->
|
settings.expectedClients.forEach { client ->
|
||||||
|
|
|
||||||
|
|
@ -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.domain.NennungRemoteRepository
|
||||||
import at.mocode.frontend.features.nennung.presentation.NennungViewModel
|
import at.mocode.frontend.features.nennung.presentation.NennungViewModel
|
||||||
|
import at.mocode.frontend.features.nennung.presentation.web.OnlineNennungViewModel
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import org.koin.core.module.dsl.viewModel
|
import org.koin.core.module.dsl.viewModel
|
||||||
import org.koin.core.qualifier.named
|
import org.koin.core.qualifier.named
|
||||||
|
|
@ -10,4 +11,5 @@ import org.koin.dsl.module
|
||||||
val nennungFeatureModule = module {
|
val nennungFeatureModule = module {
|
||||||
single<NennungRemoteRepository> { NennungRemoteRepository(get<HttpClient>(named("apiClient"))) }
|
single<NennungRemoteRepository> { NennungRemoteRepository(get<HttpClient>(named("apiClient"))) }
|
||||||
viewModel { NennungViewModel() }
|
viewModel { NennungViewModel() }
|
||||||
|
viewModel { OnlineNennungViewModel(get(named("apiClient"))) }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.data.ProfileApiClient
|
||||||
import at.mocode.frontend.features.profile.presentation.ProfileViewModel
|
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
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val profileModule = module {
|
val profileModule = module {
|
||||||
singleOf(::ProfileApiClient)
|
single { ProfileApiClient(get(named("apiClient")), get()) }
|
||||||
single { ProfileViewModel(get()) }
|
single { ProfileViewModel(get()) }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,17 @@ class DefaultMasterdataRepository(
|
||||||
} else throw Exception("Verein nicht gefunden")
|
} else throw Exception("Verein nicht gefunden")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getStats(): Result<MasterdataStats> = 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
|
// Interne Hilfs-DTOs für das Mapping der Masterdata-API
|
||||||
@Serializable
|
@Serializable
|
||||||
private data class ReiterApiDto(
|
private data class ReiterApiDto(
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,14 @@ data class Verein(
|
||||||
val istVeranstalter: Boolean
|
val istVeranstalter: Boolean
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class MasterdataStats(
|
||||||
|
val lastImport: String?,
|
||||||
|
val vereinCount: Int,
|
||||||
|
val reiterCount: Int,
|
||||||
|
val pferdCount: Int,
|
||||||
|
val funktionaerCount: Int
|
||||||
|
)
|
||||||
|
|
||||||
interface MasterdataRepository {
|
interface MasterdataRepository {
|
||||||
suspend fun searchReiter(query: String): Result<List<Reiter>>
|
suspend fun searchReiter(query: String): Result<List<Reiter>>
|
||||||
suspend fun getReiter(id: String): Result<Reiter>
|
suspend fun getReiter(id: String): Result<Reiter>
|
||||||
|
|
@ -47,4 +55,5 @@ interface MasterdataRepository {
|
||||||
suspend fun searchFunktionaere(query: String): Result<List<Funktionaer>>
|
suspend fun searchFunktionaere(query: String): Result<List<Funktionaer>>
|
||||||
suspend fun listVereine(): Result<List<Verein>>
|
suspend fun listVereine(): Result<List<Verein>>
|
||||||
suspend fun getVereinById(id: String): Result<Verein>
|
suspend fun getVereinById(id: String): Result<Verein>
|
||||||
|
suspend fun getStats(): Result<MasterdataStats>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,10 @@ package at.mocode.veranstaltung.feature.di
|
||||||
|
|
||||||
import at.mocode.veranstaltung.feature.presentation.VeranstaltungManagementViewModel
|
import at.mocode.veranstaltung.feature.presentation.VeranstaltungManagementViewModel
|
||||||
import at.mocode.veranstaltung.feature.presentation.VeranstaltungWizardViewModel
|
import at.mocode.veranstaltung.feature.presentation.VeranstaltungWizardViewModel
|
||||||
|
import org.koin.core.qualifier.named
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val veranstaltungModule = module {
|
val veranstaltungModule = module {
|
||||||
factory { VeranstaltungManagementViewModel(get()) }
|
factory { VeranstaltungManagementViewModel(get()) }
|
||||||
factory { VeranstaltungWizardViewModel(get(), get(), get()) }
|
factory { VeranstaltungWizardViewModel(get(named("apiClient")), get(), get(), get()) }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import at.mocode.frontend.core.designsystem.components.MsFilePicker
|
import at.mocode.frontend.core.designsystem.components.MsFilePicker
|
||||||
|
|
@ -140,44 +140,84 @@ private fun ZnsCheckStep(viewModel: VeranstaltungWizardViewModel) {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
Text("Schritt 1: Stammdaten-Verfügbarkeit prüfen", style = MaterialTheme.typography.titleLarge)
|
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)) {
|
Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer)) {
|
||||||
Row(Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
|
Row(Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||||
Icon(Icons.Default.Warning, null, tint = MaterialTheme.colorScheme.error)
|
Icon(Icons.Default.Warning, null, tint = MaterialTheme.colorScheme.error)
|
||||||
Spacer(Modifier.width(12.dp))
|
Spacer(Modifier.width(12.dp))
|
||||||
Column {
|
Column {
|
||||||
Text("🚨 Stammdaten fehlen!", fontWeight = FontWeight.Bold, style = MaterialTheme.typography.titleMedium)
|
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(
|
Button(
|
||||||
onClick = { /* Navigiere zum ZNS Importer */ },
|
onClick = { viewModel.checkStammdatenStatus() },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
enabled = !state.isCheckingStats,
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error)
|
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))
|
Spacer(Modifier.width(8.dp))
|
||||||
Text("Zum ZNS-Importer")
|
Text("Status prüfen")
|
||||||
}
|
}
|
||||||
|
|
||||||
OutlinedButton(
|
if (!state.isZnsAvailable) {
|
||||||
onClick = { viewModel.checkZnsAvailability() },
|
OutlinedButton(
|
||||||
modifier = Modifier.fillMaxWidth()
|
onClick = { /* Navigiere zum ZNS Importer */ },
|
||||||
) {
|
modifier = Modifier.weight(1f)
|
||||||
Icon(Icons.Default.Refresh, null)
|
) {
|
||||||
Spacer(Modifier.width(8.dp))
|
Icon(Icons.Default.CloudDownload, null)
|
||||||
Text("Status erneut prüfen")
|
Spacer(Modifier.width(8.dp))
|
||||||
}
|
Text("Zum ZNS-Importer")
|
||||||
} 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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Button(onClick = { viewModel.nextStep() }) {
|
}
|
||||||
|
|
||||||
|
if (state.isZnsAvailable) {
|
||||||
|
Button(
|
||||||
|
onClick = { viewModel.nextStep() },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
Text("Weiter zur Veranstalter-Wahl")
|
Text("Weiter zur Veranstalter-Wahl")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -230,19 +270,36 @@ private fun VeranstalterSelectionStep(viewModel: VeranstaltungWizardViewModel) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Information für den User
|
Column(
|
||||||
Text(
|
modifier = Modifier.fillMaxWidth(),
|
||||||
"Geben Sie mindestens 3 Zeichen der OEPS-Nummer ein, um die Stammdaten zu durchsuchen.",
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
|
|
||||||
// Fallback/Demo Button beibehalten für 6-009
|
|
||||||
OutlinedButton(
|
|
||||||
onClick = { viewModel.searchVeranstalterByOepsNr("6-009") },
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
) {
|
||||||
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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import at.mocode.core.domain.serialization.UuidSerializer
|
import at.mocode.core.domain.serialization.UuidSerializer
|
||||||
import at.mocode.frontend.core.auth.data.local.AuthTokenManager
|
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.core.network.NetworkConfig
|
||||||
import at.mocode.frontend.features.verein.domain.VereinRepository
|
import at.mocode.frontend.features.verein.domain.VereinRepository
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
|
|
@ -51,14 +53,17 @@ data class VeranstaltungWizardState(
|
||||||
val isSaving: Boolean = false,
|
val isSaving: Boolean = false,
|
||||||
val error: String? = null,
|
val error: String? = null,
|
||||||
val createdVeranstaltungId: Uuid? = null,
|
val createdVeranstaltungId: Uuid? = null,
|
||||||
val isZnsAvailable: Boolean = false
|
val isZnsAvailable: Boolean = false,
|
||||||
|
val stammdatenStats: MasterdataStats? = null,
|
||||||
|
val isCheckingStats: Boolean = false
|
||||||
)
|
)
|
||||||
|
|
||||||
@OptIn(ExperimentalUuidApi::class)
|
@OptIn(ExperimentalUuidApi::class)
|
||||||
class VeranstaltungWizardViewModel(
|
class VeranstaltungWizardViewModel(
|
||||||
private val httpClient: HttpClient,
|
private val httpClient: HttpClient,
|
||||||
private val authTokenManager: AuthTokenManager,
|
private val authTokenManager: AuthTokenManager,
|
||||||
private val vereinRepository: VereinRepository
|
private val vereinRepository: VereinRepository,
|
||||||
|
private val masterdataRepository: MasterdataRepository
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
var state by mutableStateOf(VeranstaltungWizardState())
|
var state by mutableStateOf(VeranstaltungWizardState())
|
||||||
|
|
@ -66,6 +71,7 @@ class VeranstaltungWizardViewModel(
|
||||||
|
|
||||||
init {
|
init {
|
||||||
checkZnsAvailability()
|
checkZnsAvailability()
|
||||||
|
checkStammdatenStatus()
|
||||||
// Simulation eines Initial-Datums
|
// Simulation eines Initial-Datums
|
||||||
state = state.copy(startDatum = LocalDate(2026, 4, 25), endDatum = LocalDate(2026, 4, 26))
|
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) {
|
fun searchVeranstalterByOepsNr(oepsNr: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val verein = vereinRepository.findByOepsNr(oepsNr)
|
val verein = vereinRepository.findByOepsNr(oepsNr)
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ import org.koin.dsl.module
|
||||||
|
|
||||||
val vereinFeatureModule = module {
|
val vereinFeatureModule = module {
|
||||||
// Desktop-App nutzt im Startup-Mode bevorzugt das Fake-Repository
|
// Desktop-App nutzt im Startup-Mode bevorzugt das Fake-Repository
|
||||||
|
// Kann bei Bedarf auf KtorVereinRepository umgestellt werden:
|
||||||
|
// single<VereinRepository> { KtorVereinRepository(get(named("apiClient"))) }
|
||||||
single<VereinRepository> { FakeVereinRepository() }
|
single<VereinRepository> { FakeVereinRepository() }
|
||||||
viewModelOf(::VereinViewModel)
|
viewModelOf(::VereinViewModel)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ fun DesktopApp() {
|
||||||
&& currentScreen !is AppScreen.VereinVerwaltung
|
&& currentScreen !is AppScreen.VereinVerwaltung
|
||||||
&& currentScreen !is AppScreen.StammdatenImport
|
&& currentScreen !is AppScreen.StammdatenImport
|
||||||
&& currentScreen !is AppScreen.NennungsEingang
|
&& currentScreen !is AppScreen.NennungsEingang
|
||||||
|
&& currentScreen !is AppScreen.VeranstaltungNeu
|
||||||
&& currentScreen !is AppScreen.ConnectivityCheck
|
&& currentScreen !is AppScreen.ConnectivityCheck
|
||||||
) {
|
) {
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,12 @@
|
||||||
package at.mocode.frontend.shell.desktop.repository
|
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.MasterdataRepository
|
||||||
|
import at.mocode.frontend.core.domain.repository.MasterdataStats
|
||||||
import at.mocode.frontend.core.domain.zns.ZnsRemoteFunktionaer
|
import at.mocode.frontend.core.domain.zns.ZnsRemoteFunktionaer
|
||||||
import at.mocode.frontend.core.domain.zns.ZnsRemotePferd
|
import at.mocode.frontend.core.domain.zns.ZnsRemotePferd
|
||||||
import at.mocode.frontend.core.domain.zns.ZnsRemoteReiter
|
import at.mocode.frontend.core.domain.zns.ZnsRemoteReiter
|
||||||
import at.mocode.frontend.core.domain.zns.ZnsRemoteVerein
|
import at.mocode.frontend.core.domain.zns.ZnsRemoteVerein
|
||||||
|
import at.mocode.frontend.shell.desktop.data.*
|
||||||
|
|
||||||
class DesktopMasterdataRepository : MasterdataRepository {
|
class DesktopMasterdataRepository : MasterdataRepository {
|
||||||
|
|
||||||
|
|
@ -46,7 +43,7 @@ class DesktopMasterdataRepository : MasterdataRepository {
|
||||||
satznummer = remote.satznummer,
|
satznummer = remote.satznummer,
|
||||||
oepsNummer = remote.satznummer, // Oft identisch oder Mapping nötig
|
oepsNummer = remote.satznummer, // Oft identisch oder Mapping nötig
|
||||||
lizenzKlasse = remote.lizenzKlasse,
|
lizenzKlasse = remote.lizenzKlasse,
|
||||||
nation = "AUT" // Default für ZNS Import
|
nation = "AUT" // Default für ZNS-Import
|
||||||
)
|
)
|
||||||
if (existingIdx >= 0) {
|
if (existingIdx >= 0) {
|
||||||
Store.reiter[existingIdx] = entry
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ fun DesktopContentArea(
|
||||||
// Haupt-Zentrale: Veranstaltung-Verwaltung
|
// Haupt-Zentrale: Veranstaltung-Verwaltung
|
||||||
is AppScreen.VeranstaltungVerwaltung -> {
|
is AppScreen.VeranstaltungVerwaltung -> {
|
||||||
VeranstaltungenScreen(
|
VeranstaltungenScreen(
|
||||||
onVeranstaltungNeu = { onNavigate(AppScreen.VeranstalterAuswahl) },
|
onVeranstaltungNeu = { onNavigate(AppScreen.VeranstaltungNeu) },
|
||||||
onVeranstaltungOeffnen = { vId: Long, eId: Long -> onNavigate(AppScreen.VeranstaltungProfil(vId, eId)) }
|
onVeranstaltungOeffnen = { vId: Long, eId: Long -> onNavigate(AppScreen.VeranstaltungProfil(vId, eId)) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user