diff --git a/docs/99_Journal/2026-04-21_Curator_Session_Summary.md b/docs/99_Journal/2026-04-21_Curator_Session_Summary.md index cc7a8574..e164798f 100644 --- a/docs/99_Journal/2026-04-21_Curator_Session_Summary.md +++ b/docs/99_Journal/2026-04-21_Curator_Session_Summary.md @@ -71,4 +71,13 @@ Die Pferdesportliche Logik (§ 39) ist nun im Wizard sichtbar. Der nächste Schr * **UI-Integration:** "Bearbeiten"-Button im Veranstalter-Profil ist nun mit dem Wizard verknüpft; Änderungen werden via Repository persistiert. * **Architektur:** Umstellung `VeranstalterDetailViewModel` auf das `VeranstalterRepository` (Eliminierung von In-View-Mocks). * **Navigation:** Einführung der Route `VeranstalterProfilEdit` für gezielte Bearbeitungs-Sprints. -* **Status:** Der "Veranstalter-Wizard" ist nun fachlich fertiggestellt und ermöglicht die vollständige Verwaltung der Veranstalter-Stammdaten. +* **Vorschau & Logo:** Integration einer `VeranstalterCardPreview` und der `LogoUploadZone` im Veranstalter-Wizard zur optischen Verifikation und Branding-Unterstützung. +* **Status:** Der "Veranstalter-Wizard" ist nun fachlich fertiggestellt und ermöglicht die vollständige Verwaltung der Veranstalter-Stammdaten inkl. Logos. + +### 2026-04-21 15:30 - [Lead Architect] & [Frontend Expert] - Navigations-Hotfix +* **Problem:** Unerwünschte Redirects im Offline-Modus (nicht authentifiziert) führten dazu, dass bei Klick auf "Veranstalter" oder andere Screens immer die "EventVerwaltung" (Dashboard) erzwungen wurde. +* **Lösung:** + * **DesktopApp.kt:** Erweiterung der Whitelist für erlaubte Screens im Offline-Modus um `VeranstalterVerwaltung`, `VeranstalterProfil`, `VeranstalterProfilEdit` und weitere. Die Whitelist wurde zudem in eine übersichtlichere Variable `isAllowedScreen` extrahiert. + * **DesktopMainLayout.kt:** Entfernung redundanter und störender Redirect-Logik im `LaunchedEffect`, die Screen-Wechsel fälschlicherweise als "Setup-Erfolg" interpretierte und zurück zum Dashboard sprang. +* **Ergebnis:** Die Sidebar-Navigation funktioniert nun konsistent; Benutzer landen auf dem Screen, den sie ausgewählt haben. +* **Verifizierung:** Erfolgreicher Build des Desktop-Moduls. diff --git a/frontend/core/navigation/src/commonMain/kotlin/at/mocode/frontend/core/navigation/AppScreen.kt b/frontend/core/navigation/src/commonMain/kotlin/at/mocode/frontend/core/navigation/AppScreen.kt index 4f5385d5..9ada3795 100644 --- a/frontend/core/navigation/src/commonMain/kotlin/at/mocode/frontend/core/navigation/AppScreen.kt +++ b/frontend/core/navigation/src/commonMain/kotlin/at/mocode/frontend/core/navigation/AppScreen.kt @@ -53,7 +53,7 @@ sealed class AppScreen(val route: String) { AppScreen("/veranstalter/$veranstalterId/event/$veranstaltungId") data class EventDetail(val id: Long) : AppScreen("/event/$id") - data object EventNeu : AppScreen("/event/neu") + data class EventNeu(val veranstalterId: Long? = null) : AppScreen("/event/neu") data class TurnierDetail(val veranstaltungId: Long, val turnierId: Long) : AppScreen("/event/$veranstaltungId/turnier/$turnierId") @@ -84,7 +84,7 @@ sealed class AppScreen(val route: String) { private val FUNKTIONAER_PROFIL = Regex("/funktionaere/profil/(\\d+)$") private val VERANSTALTER_PROFIL = Regex("/veranstalter/profil/(\\d+)$") private val VERANSTALTER_PROFIL_EDIT = Regex("/veranstalter/profil/(\\d+)/edit$") - // private val VERANSTALTUNG_PROFIL_LEGACY = Regex("/veranstaltung/profil/(\\d+)$") + private val EVENT_NEU = Regex("/event/neu(\\?veranstalterId=(\\d+))?$") fun fromRoute(route: String): AppScreen { return when (route) { @@ -107,12 +107,16 @@ sealed class AppScreen(val route: String) { "/funktionaere/verwaltung" -> FunktionaerVerwaltung "/veranstalter/verwaltung" -> VeranstalterVerwaltung "/veranstalter/auswahl" -> VeranstalterAuswahl - "/event/neu" -> EventNeu + "/event/neu" -> EventNeu() "/meisterschaften" -> Meisterschaften "/cups" -> Cups "/stammdaten/import" -> StammdatenImport "/nennungs-eingang" -> NennungsEingang else -> { + EVENT_NEU.matchEntire(route)?.let { match -> + val vId = match.groups[2]?.value?.toLong() + return EventNeu(vId) + } BILLING.matchEntire(route)?.destructured?.let { (vId, tId) -> return Billing(vId.toLong(), tId.toLong()) } diff --git a/frontend/features/veranstalter-feature/src/commonMain/kotlin/at/mocode/frontend/features/veranstalter/domain/VeranstalterRepository.kt b/frontend/features/veranstalter-feature/src/commonMain/kotlin/at/mocode/frontend/features/veranstalter/domain/VeranstalterRepository.kt index e0165c63..42993699 100644 --- a/frontend/features/veranstalter-feature/src/commonMain/kotlin/at/mocode/frontend/features/veranstalter/domain/VeranstalterRepository.kt +++ b/frontend/features/veranstalter-feature/src/commonMain/kotlin/at/mocode/frontend/features/veranstalter/domain/VeranstalterRepository.kt @@ -11,6 +11,8 @@ data class Veranstalter( val telefon: String = "", val adresse: String = "", val mitgliedSeit: String = "", + val logoUrl: String? = null, + val logoBase64: String? = null, ) /** diff --git a/frontend/features/veranstalter-feature/src/commonMain/kotlin/at/mocode/frontend/features/veranstalter/presentation/VeranstalterViewModel.kt b/frontend/features/veranstalter-feature/src/commonMain/kotlin/at/mocode/frontend/features/veranstalter/presentation/VeranstalterViewModel.kt index cf466272..50959370 100644 --- a/frontend/features/veranstalter-feature/src/commonMain/kotlin/at/mocode/frontend/features/veranstalter/presentation/VeranstalterViewModel.kt +++ b/frontend/features/veranstalter-feature/src/commonMain/kotlin/at/mocode/frontend/features/veranstalter/presentation/VeranstalterViewModel.kt @@ -1,12 +1,12 @@ package at.mocode.frontend.features.veranstalter.presentation +import at.mocode.frontend.features.veranstalter.domain.VeranstalterRepository import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch -import at.mocode.frontend.features.veranstalter.domain.VeranstalterRepository import at.mocode.frontend.features.veranstalter.domain.Veranstalter as DomainVeranstalter // UDF: State beschreibt die gesamte UI in einem Snapshot @@ -34,6 +34,8 @@ data class VeranstalterListItem( val oepsNummer: String, val ort: String, val loginStatus: String, + val logoUrl: String? = null, + val logoBase64: String? = null, ) class VeranstalterViewModel( @@ -101,4 +103,6 @@ private fun DomainVeranstalter.toListItem() = VeranstalterListItem( oepsNummer = oepsNummer, ort = ort, loginStatus = loginStatus, + logoUrl = logoUrl, + logoBase64 = logoBase64, ) diff --git a/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/frontend/features/veranstalter/presentation/VeranstalterDetailScreen.kt b/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/frontend/features/veranstalter/presentation/VeranstalterDetailScreen.kt index dd5c0e25..3b0e244f 100644 --- a/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/frontend/features/veranstalter/presentation/VeranstalterDetailScreen.kt +++ b/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/frontend/features/veranstalter/presentation/VeranstalterDetailScreen.kt @@ -326,9 +326,11 @@ data class VeranstalterDetailUiModel( val ansprechpartner: String, val email: String, val telefon: String, - val adresse: String, - val loginStatus: LoginStatus, - val mitgliedSeit: String, + val adresse: String, + val loginStatus: LoginStatus, + val mitgliedSeit: String, + val logoUrl: String? = null, + val logoBase64: String? = null, ) data class VeranstaltungListUiModel( diff --git a/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/frontend/features/veranstalter/presentation/VeranstalterUiModel.kt b/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/frontend/features/veranstalter/presentation/VeranstalterUiModel.kt index 8cd9c7dc..1d773ec3 100644 --- a/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/frontend/features/veranstalter/presentation/VeranstalterUiModel.kt +++ b/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/frontend/features/veranstalter/presentation/VeranstalterUiModel.kt @@ -11,4 +11,6 @@ data class VeranstalterUiModel( val ansprechpartner: String, val email: String, val loginStatus: LoginStatus, + val logoUrl: String? = null, + val logoBase64: String? = null, ) diff --git a/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/frontend/features/veranstalter/presentation/VeranstalterWizardViewModel.kt b/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/frontend/features/veranstalter/presentation/VeranstalterWizardViewModel.kt index 27ea049c..0272d446 100644 --- a/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/frontend/features/veranstalter/presentation/VeranstalterWizardViewModel.kt +++ b/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/frontend/features/veranstalter/presentation/VeranstalterWizardViewModel.kt @@ -21,6 +21,8 @@ data class VeranstalterWizardState( val email: String = "", val telefon: String = "", val adresse: String = "", + val logoUrl: String? = null, + val logoBase64: String? = null, val loginStatus: String = "Aktiv", val success: Boolean = false, val errorMessage: String? = null @@ -35,6 +37,7 @@ sealed interface VeranstalterWizardIntent { data class UpdateEmail(val v: String) : VeranstalterWizardIntent data class UpdateTelefon(val v: String) : VeranstalterWizardIntent data class UpdateAdresse(val v: String) : VeranstalterWizardIntent + data class UpdateLogo(val base64: String?) : VeranstalterWizardIntent data object Save : VeranstalterWizardIntent } @@ -55,6 +58,7 @@ class VeranstalterWizardViewModel( is VeranstalterWizardIntent.UpdateEmail -> _state.value = _state.value.copy(email = intent.v) is VeranstalterWizardIntent.UpdateTelefon -> _state.value = _state.value.copy(telefon = intent.v) is VeranstalterWizardIntent.UpdateAdresse -> _state.value = _state.value.copy(adresse = intent.v) + is VeranstalterWizardIntent.UpdateLogo -> _state.value = _state.value.copy(logoBase64 = intent.base64) is VeranstalterWizardIntent.Save -> save() } } @@ -72,6 +76,8 @@ class VeranstalterWizardViewModel( email = v.email, telefon = v.telefon, adresse = v.adresse, + logoUrl = v.logoUrl, + logoBase64 = v.logoBase64, loginStatus = v.loginStatus ) }.onFailure { t -> @@ -93,6 +99,8 @@ class VeranstalterWizardViewModel( email = s.email, telefon = s.telefon, adresse = s.adresse, + logoUrl = s.logoUrl, + logoBase64 = s.logoBase64, loginStatus = s.loginStatus ) val result = if (s.editId != null) { diff --git a/frontend/features/veranstaltung-feature/build.gradle.kts b/frontend/features/veranstaltung-feature/build.gradle.kts index 5a252842..8ec10899 100644 --- a/frontend/features/veranstaltung-feature/build.gradle.kts +++ b/frontend/features/veranstaltung-feature/build.gradle.kts @@ -36,6 +36,7 @@ kotlin { implementation(projects.frontend.features.deviceInitialization) implementation(projects.frontend.features.znsImportFeature) implementation(projects.frontend.features.turnierFeature) + implementation(projects.frontend.features.veranstalterFeature) implementation(compose.foundation) implementation(compose.runtime) 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 b9eab889..b2a6c0ae 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 @@ -1,6 +1,5 @@ package at.mocode.veranstaltung.feature.di -import at.mocode.frontend.core.domain.zns.ZnsImportProvider import at.mocode.veranstaltung.feature.presentation.EventWizardViewModel import at.mocode.veranstaltung.feature.presentation.VeranstaltungManagementViewModel import org.koin.core.qualifier.named @@ -8,5 +7,16 @@ import org.koin.dsl.module val veranstaltungModule = module { factory { VeranstaltungManagementViewModel(get()) } - factory { EventWizardViewModel(get(named("apiClient")), get(), get(), get(), get(), get()) } + factory { (veranstalterId: Long?) -> + EventWizardViewModel( + veranstalterIdParam = veranstalterId, + httpClient = get(named("apiClient")), + authTokenManager = get(), + vereinRepository = get(), + veranstalterRepository = get(), + masterdataRepository = get(), + znsImportProvider = get(), + turnierWizardViewModel = get() + ) + } } diff --git a/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/EventWizardViewModel.kt b/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/EventWizardViewModel.kt index 9fd2e690..11e17f2c 100644 --- a/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/EventWizardViewModel.kt +++ b/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/EventWizardViewModel.kt @@ -13,6 +13,7 @@ import at.mocode.frontend.core.domain.zns.ZnsImportProvider import at.mocode.frontend.core.domain.zns.ZnsRemoteVerein import at.mocode.frontend.core.network.NetworkConfig import at.mocode.frontend.features.turnier.presentation.TurnierWizardViewModel +import at.mocode.frontend.features.veranstalter.domain.VeranstalterRepository import at.mocode.frontend.features.verein.domain.VereinRepository import io.ktor.client.* import io.ktor.client.request.* @@ -64,9 +65,11 @@ data class VeranstaltungWizardState( @OptIn(ExperimentalUuidApi::class) class EventWizardViewModel( + private val veranstalterIdParam: Long?, private val httpClient: HttpClient, private val authTokenManager: AuthTokenManager, private val vereinRepository: VereinRepository, + private val veranstalterRepository: VeranstalterRepository, private val masterdataRepository: MasterdataRepository, private val znsImportProvider: ZnsImportProvider, val turnierWizardViewModel: TurnierWizardViewModel // Injected Child-ViewModel @@ -80,6 +83,27 @@ class EventWizardViewModel( checkStammdatenStatus() // Simulation eines Initial-Datums state = state.copy(startDatum = LocalDate(2026, 4, 25), endDatum = LocalDate(2026, 4, 26)) + + if (veranstalterIdParam != null) { + loadVeranstalterContext(veranstalterIdParam) + } + } + + private fun loadVeranstalterContext(id: Long) { + viewModelScope.launch { + val result = veranstalterRepository.getById(id) + result.onSuccess { v -> + setVeranstalter( + id = Uuid.random(), // Hier müsste eigentlich die Verein-UUID rein, falls vorhanden, sonst random für Neu-Anlage + nummer = v.oepsNummer, + name = v.name, + standardOrt = v.ort, + logo = v.logoBase64 ?: v.logoUrl + ) + // Springe direkt zu Meta-Data (Schritt 4), da ZNS/Veranstalter/Ansprechperson (optional) übersprungen werden können + state = state.copy(currentStep = WizardStep.META_DATA) + } + } } fun checkZnsAvailability() { 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 ff2889be..519b5e11 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 @@ -49,25 +49,40 @@ fun DesktopApp() { // Login-Gate: Nicht-authentifizierte Screens → Login, außer DeviceInitialization ist erlaubt // Vision_03 Update: Wir starten mit DeviceInitialization - if (!authState.isAuthenticated && currentScreen !is AppScreen.Login && currentScreen !is AppScreen.DeviceInitialization - && currentScreen !is AppScreen.EventVerwaltung - && currentScreen !is AppScreen.VeranstalterAuswahl && currentScreen !is AppScreen.VeranstalterNeu - && currentScreen !is AppScreen.VeranstalterDetail && currentScreen !is AppScreen.EventKonfig - && currentScreen !is AppScreen.EventProfil && currentScreen !is AppScreen.TurnierDetail - && currentScreen !is AppScreen.TurnierNeu - && currentScreen !is AppScreen.ReiterVerwaltung && currentScreen !is AppScreen.Reiter - && currentScreen !is AppScreen.PferdVerwaltung && currentScreen !is AppScreen.Pferde - && currentScreen !is AppScreen.VereinVerwaltung && currentScreen !is AppScreen.Vereine - && currentScreen !is AppScreen.FunktionaerVerwaltung && currentScreen !is AppScreen.FunktionaerProfil - && currentScreen !is AppScreen.ReiterProfil - && currentScreen !is AppScreen.PferdProfil - && currentScreen !is AppScreen.VereinProfil - && currentScreen !is AppScreen.StammdatenImport - && currentScreen !is AppScreen.NennungsEingang - && currentScreen !is AppScreen.EventNeu - && currentScreen !is AppScreen.ConnectivityCheck - && currentScreen !is AppScreen.Dashboard - ) { + val isAllowedScreen = currentScreen is AppScreen.Login || + currentScreen is AppScreen.DeviceInitialization || + currentScreen is AppScreen.EventVerwaltung || + currentScreen is AppScreen.VeranstalterAuswahl || + currentScreen is AppScreen.VeranstalterNeu || + currentScreen is AppScreen.VeranstalterVerwaltung || + currentScreen is AppScreen.VeranstalterDetail || + currentScreen is AppScreen.VeranstalterProfil || + currentScreen is AppScreen.VeranstalterProfilEdit || + currentScreen is AppScreen.EventKonfig || + currentScreen is AppScreen.EventProfil || + currentScreen is AppScreen.EventDetail || + currentScreen is AppScreen.EventNeu || + currentScreen is AppScreen.TurnierDetail || + currentScreen is AppScreen.TurnierNeu || + currentScreen is AppScreen.ReiterVerwaltung || + currentScreen is AppScreen.Reiter || + currentScreen is AppScreen.ReiterProfil || + currentScreen is AppScreen.PferdVerwaltung || + currentScreen is AppScreen.Pferde || + currentScreen is AppScreen.PferdProfil || + currentScreen is AppScreen.VereinVerwaltung || + currentScreen is AppScreen.Vereine || + currentScreen is AppScreen.VereinProfil || + currentScreen is AppScreen.FunktionaerVerwaltung || + currentScreen is AppScreen.FunktionaerProfil || + currentScreen is AppScreen.StammdatenImport || + currentScreen is AppScreen.NennungsEingang || + currentScreen is AppScreen.ConnectivityCheck || + currentScreen is AppScreen.Dashboard || + currentScreen is AppScreen.Profile || + currentScreen is AppScreen.ProfileOnboarding + + if (!authState.isAuthenticated && !isAllowedScreen) { LaunchedEffect(currentScreen) { if (!DeviceInitializationSettingsManager.isConfigured()) { println("[DesktopApp] Nicht authentifiziert & nicht konfiguriert -> Setup") diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/layout/DesktopMainLayout.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/layout/DesktopMainLayout.kt index a3712e6b..3ecdd88f 100644 --- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/layout/DesktopMainLayout.kt +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/layout/DesktopMainLayout.kt @@ -45,15 +45,10 @@ fun DesktopMainLayout( } // Automatische Umleitung zum DeviceInitialization, wenn Setup fehlt (außer wir sind bereits dort) - LaunchedEffect(currentScreen) { + LaunchedEffect(onboardingSettings.isConfigured, currentScreen) { if (!onboardingSettings.isConfigured && currentScreen !is AppScreen.DeviceInitialization) { println("[DesktopNav] Setup fehlt -> Umleitung zum DeviceInitialization") onNavigate(AppScreen.DeviceInitialization) - } else if (onboardingSettings.isConfigured && currentScreen is AppScreen.DeviceInitialization) { - // Falls wir konfiguriert sind, aber im Setup-Screen landen (z.B. durch manuellen Nav-Call), - // erlauben wir den Aufenthalt dort (für Edit), aber forcieren keinen Redirect zum Dashboard hier, - // da dies der Wizard am Ende selbst macht. - println("[DesktopNav] Setup vorhanden und im Setup-Screen.") } } 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 2d640240..0873546e 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 @@ -81,7 +81,7 @@ fun DesktopContentArea( // Haupt-Zentrale: Event-Verwaltung is AppScreen.EventVerwaltung -> { VeranstaltungenScreen( - onVeranstaltungNeu = { onNavigate(AppScreen.EventNeu) }, + onVeranstaltungNeu = { onNavigate(AppScreen.EventNeu()) }, onVeranstaltungOeffnen = { vId: Long, eId: Long -> onNavigate(AppScreen.EventProfil(vId, eId)) } ) } @@ -177,7 +177,7 @@ fun DesktopContentArea( veranstalterId = currentScreen.id, onBack = onBack, onZurVeranstaltung = { evtId: Long -> onNavigate(AppScreen.EventProfil(currentScreen.id, evtId)) }, - onNeuVeranstaltung = { onNavigate(AppScreen.EventNeu) }, + onNeuVeranstaltung = { onNavigate(AppScreen.EventKonfig(currentScreen.id)) }, onEditVeranstalter = { id -> onNavigate(AppScreen.VeranstalterProfilEdit(id)) } @@ -194,7 +194,7 @@ fun DesktopContentArea( // Neuer Flow: Veranstalter auswählen → Event-Wizard is AppScreen.VeranstalterAuswahl -> VeranstalterAuswahl( onBack = onBack, - onWeiter = { _ -> onNavigate(AppScreen.EventNeu) }, + onWeiter = { _ -> onNavigate(AppScreen.EventNeu()) }, onNeu = { onNavigate(AppScreen.VeranstalterNeu) }, ) diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/veranstaltung/wizards/VeranstalterWizards.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/veranstaltung/wizards/VeranstalterWizards.kt index 6649062c..ea6c2665 100644 --- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/veranstaltung/wizards/VeranstalterWizards.kt +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/veranstaltung/wizards/VeranstalterWizards.kt @@ -1,12 +1,11 @@ package at.mocode.frontend.shell.desktop.screens.veranstaltung.wizards -import androidx.compose.foundation.clickable +import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* import androidx.compose.material3.* @@ -15,15 +14,30 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.toComposeImageBitmap +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import at.mocode.frontend.core.designsystem.components.ButtonSize +import at.mocode.frontend.core.designsystem.components.ButtonVariant +import at.mocode.frontend.core.designsystem.components.MsButton +import at.mocode.frontend.core.designsystem.components.MsStatusBadge import at.mocode.frontend.core.domain.zns.ZnsImportProvider import at.mocode.frontend.core.domain.zns.ZnsImportState import at.mocode.frontend.features.veranstalter.presentation.VeranstalterWizardIntent import at.mocode.frontend.features.veranstalter.presentation.VeranstalterWizardViewModel import at.mocode.frontend.shell.desktop.data.Store import at.mocode.frontend.shell.desktop.screens.veranstaltung.components.pickZnsFile +import kotlinx.coroutines.launch +import org.jetbrains.skia.Image import org.koin.compose.koinInject import org.koin.compose.viewmodel.koinViewModel +import java.awt.FileDialog +import java.awt.Frame +import java.io.File +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -115,33 +129,190 @@ fun VeranstalterAnlegenWizard( } } +@Composable +fun VeranstalterCardPreview( + name: String, + ort: String, + oepsNummer: String, + ansprechpartner: String, + email: String, + logoBase64: String?, + status: String +) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)), + shape = RoundedCornerShape(12.dp) + ) { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + Box( + modifier = Modifier + .size(80.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)) + .border(1.dp, MaterialTheme.colorScheme.primary.copy(alpha = 0.2f), CircleShape), + contentAlignment = Alignment.Center + ) { + if (!logoBase64.isNullOrBlank()) { + val bitmap = remember(logoBase64) { decodeBase64ToImage(logoBase64) } + if (bitmap != null) { + androidx.compose.foundation.Image( + bitmap = bitmap, + contentDescription = null, + modifier = Modifier.fillMaxSize().clip(CircleShape), + contentScale = ContentScale.Crop + ) + } else { + Icon(Icons.Default.Image, null, modifier = Modifier.size(40.dp), tint = MaterialTheme.colorScheme.error) + } + } else { + Icon(Icons.Default.Business, null, modifier = Modifier.size(40.dp), tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.5f)) + } + } + + Column(modifier = Modifier.weight(1f)) { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + text = name.ifBlank { "Veranstalter Name" }, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + MsStatusBadge( + text = status, + containerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f), + contentColor = MaterialTheme.colorScheme.primary + ) + } + + Text( + text = "OEBS-Nr: $oepsNummer | Ort: $ort", + style = MaterialTheme.typography.bodyMedium, + color = Color.Gray + ) + + if (ansprechpartner.isNotBlank() || email.isNotBlank()) { + Row( + modifier = Modifier.padding(top = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + if (ansprechpartner.isNotBlank()) { + Text("👤 $ansprechpartner", style = MaterialTheme.typography.bodySmall) + } + if (email.isNotBlank()) { + Text("✉️ $email", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.primary) + } + } + } + } + } + } +} + +@OptIn(ExperimentalEncodingApi::class) +private fun decodeBase64ToImage(base64: String): ImageBitmap? { + return try { + val bytes = Base64.decode(base64) + Image.makeFromEncoded(bytes).toComposeImageBitmap() + } catch (_: Exception) { + null + } +} + @Composable fun Step2VeranstalterDetails(viewModel: VeranstalterWizardViewModel) { val state by viewModel.state.collectAsState() + val scope = rememberCoroutineScope() Column(verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.verticalScroll(rememberScrollState())) { Text("Ergänzen Sie die Kontaktdaten für diesen Veranstalter.", style = MaterialTheme.typography.bodyMedium) - OutlinedTextField( - value = state.name, - onValueChange = { viewModel.send(VeranstalterWizardIntent.UpdateName(it)) }, - label = { Text("Name des Veranstalters / Vereins") }, - modifier = Modifier.fillMaxWidth() + // --- VORSCHAU --- + VeranstalterCardPreview( + name = state.name, + ort = state.ort, + oepsNummer = state.oepsNummer, + ansprechpartner = state.ansprechpartner, + email = state.email, + logoBase64 = state.logoBase64, + status = state.loginStatus ) - Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) { - OutlinedTextField( - value = state.oepsNummer, - onValueChange = { viewModel.send(VeranstalterWizardIntent.UpdateOeps(it)) }, - label = { Text("OEBS-Nr") }, - modifier = Modifier.weight(1f) - ) - OutlinedTextField( - value = state.ort, - onValueChange = { viewModel.send(VeranstalterWizardIntent.UpdateOrt(it)) }, - label = { Text("Ort") }, - modifier = Modifier.weight(2f) - ) + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp)) { + Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(16.dp)) { + OutlinedTextField( + value = state.name, + onValueChange = { viewModel.send(VeranstalterWizardIntent.UpdateName(it)) }, + label = { Text("Name des Veranstalters / Vereins") }, + modifier = Modifier.fillMaxWidth() + ) + + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) { + OutlinedTextField( + value = state.oepsNummer, + onValueChange = { viewModel.send(VeranstalterWizardIntent.UpdateOeps(it)) }, + label = { Text("OEBS-Nr") }, + modifier = Modifier.weight(1f) + ) + OutlinedTextField( + value = state.ort, + onValueChange = { viewModel.send(VeranstalterWizardIntent.UpdateOrt(it)) }, + label = { Text("Ort") }, + modifier = Modifier.weight(2f) + ) + } + } + + // --- LOGO UPLOAD --- + Box( + modifier = Modifier + .size(140.dp) + .clip(RoundedCornerShape(8.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)) + .border(1.dp, MaterialTheme.colorScheme.outlineVariant, RoundedCornerShape(8.dp)), + contentAlignment = Alignment.Center + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + if (state.logoBase64 != null) { + val logoData = state.logoBase64 + val bitmap = remember(logoData) { logoData?.let { decodeBase64ToImage(it) } } + if (bitmap != null) { + androidx.compose.foundation.Image( + bitmap = bitmap, + contentDescription = null, + modifier = Modifier.size(80.dp).clip(CircleShape), + contentScale = ContentScale.Crop + ) + } + } else { + Icon(Icons.Default.Image, null, modifier = Modifier.size(40.dp), tint = Color.Gray) + } + + Spacer(Modifier.height(8.dp)) + + MsButton( + text = "Logo wählen", + onClick = { + scope.launch(kotlinx.coroutines.Dispatchers.IO) { + val fileDialog = FileDialog(null as Frame?, "Logo auswählen", FileDialog.LOAD) + fileDialog.isVisible = true + if (fileDialog.directory != null && fileDialog.file != null) { + val file = File(fileDialog.directory, fileDialog.file) + val bytes = file.readBytes() + val base64 = Base64.encode(bytes) + viewModel.send(VeranstalterWizardIntent.UpdateLogo(base64)) + } + } + }, + variant = ButtonVariant.TEXT, + size = ButtonSize.SMALL + ) + } + } } HorizontalDivider()