chore: integriere Logo-Upload und Vorschau in Veranstalter-Wizard, verbessere Navigationslogik und erweitere Datenmodelle
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
This commit is contained in:
@@ -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.
|
* **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).
|
* **Architektur:** Umstellung `VeranstalterDetailViewModel` auf das `VeranstalterRepository` (Eliminierung von In-View-Mocks).
|
||||||
* **Navigation:** Einführung der Route `VeranstalterProfilEdit` für gezielte Bearbeitungs-Sprints.
|
* **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.
|
||||||
|
|||||||
+7
-3
@@ -53,7 +53,7 @@ sealed class AppScreen(val route: String) {
|
|||||||
AppScreen("/veranstalter/$veranstalterId/event/$veranstaltungId")
|
AppScreen("/veranstalter/$veranstalterId/event/$veranstaltungId")
|
||||||
|
|
||||||
data class EventDetail(val id: Long) : AppScreen("/event/$id")
|
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) :
|
data class TurnierDetail(val veranstaltungId: Long, val turnierId: Long) :
|
||||||
AppScreen("/event/$veranstaltungId/turnier/$turnierId")
|
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 FUNKTIONAER_PROFIL = Regex("/funktionaere/profil/(\\d+)$")
|
||||||
private val VERANSTALTER_PROFIL = Regex("/veranstalter/profil/(\\d+)$")
|
private val VERANSTALTER_PROFIL = Regex("/veranstalter/profil/(\\d+)$")
|
||||||
private val VERANSTALTER_PROFIL_EDIT = Regex("/veranstalter/profil/(\\d+)/edit$")
|
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 {
|
fun fromRoute(route: String): AppScreen {
|
||||||
return when (route) {
|
return when (route) {
|
||||||
@@ -107,12 +107,16 @@ sealed class AppScreen(val route: String) {
|
|||||||
"/funktionaere/verwaltung" -> FunktionaerVerwaltung
|
"/funktionaere/verwaltung" -> FunktionaerVerwaltung
|
||||||
"/veranstalter/verwaltung" -> VeranstalterVerwaltung
|
"/veranstalter/verwaltung" -> VeranstalterVerwaltung
|
||||||
"/veranstalter/auswahl" -> VeranstalterAuswahl
|
"/veranstalter/auswahl" -> VeranstalterAuswahl
|
||||||
"/event/neu" -> EventNeu
|
"/event/neu" -> EventNeu()
|
||||||
"/meisterschaften" -> Meisterschaften
|
"/meisterschaften" -> Meisterschaften
|
||||||
"/cups" -> Cups
|
"/cups" -> Cups
|
||||||
"/stammdaten/import" -> StammdatenImport
|
"/stammdaten/import" -> StammdatenImport
|
||||||
"/nennungs-eingang" -> NennungsEingang
|
"/nennungs-eingang" -> NennungsEingang
|
||||||
else -> {
|
else -> {
|
||||||
|
EVENT_NEU.matchEntire(route)?.let { match ->
|
||||||
|
val vId = match.groups[2]?.value?.toLong()
|
||||||
|
return EventNeu(vId)
|
||||||
|
}
|
||||||
BILLING.matchEntire(route)?.destructured?.let { (vId, tId) ->
|
BILLING.matchEntire(route)?.destructured?.let { (vId, tId) ->
|
||||||
return Billing(vId.toLong(), tId.toLong())
|
return Billing(vId.toLong(), tId.toLong())
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -11,6 +11,8 @@ data class Veranstalter(
|
|||||||
val telefon: String = "",
|
val telefon: String = "",
|
||||||
val adresse: String = "",
|
val adresse: String = "",
|
||||||
val mitgliedSeit: String = "",
|
val mitgliedSeit: String = "",
|
||||||
|
val logoUrl: String? = null,
|
||||||
|
val logoBase64: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+5
-1
@@ -1,12 +1,12 @@
|
|||||||
package at.mocode.frontend.features.veranstalter.presentation
|
package at.mocode.frontend.features.veranstalter.presentation
|
||||||
|
|
||||||
|
import at.mocode.frontend.features.veranstalter.domain.VeranstalterRepository
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import at.mocode.frontend.features.veranstalter.domain.VeranstalterRepository
|
|
||||||
import at.mocode.frontend.features.veranstalter.domain.Veranstalter as DomainVeranstalter
|
import at.mocode.frontend.features.veranstalter.domain.Veranstalter as DomainVeranstalter
|
||||||
|
|
||||||
// UDF: State beschreibt die gesamte UI in einem Snapshot
|
// UDF: State beschreibt die gesamte UI in einem Snapshot
|
||||||
@@ -34,6 +34,8 @@ data class VeranstalterListItem(
|
|||||||
val oepsNummer: String,
|
val oepsNummer: String,
|
||||||
val ort: String,
|
val ort: String,
|
||||||
val loginStatus: String,
|
val loginStatus: String,
|
||||||
|
val logoUrl: String? = null,
|
||||||
|
val logoBase64: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
class VeranstalterViewModel(
|
class VeranstalterViewModel(
|
||||||
@@ -101,4 +103,6 @@ private fun DomainVeranstalter.toListItem() = VeranstalterListItem(
|
|||||||
oepsNummer = oepsNummer,
|
oepsNummer = oepsNummer,
|
||||||
ort = ort,
|
ort = ort,
|
||||||
loginStatus = loginStatus,
|
loginStatus = loginStatus,
|
||||||
|
logoUrl = logoUrl,
|
||||||
|
logoBase64 = logoBase64,
|
||||||
)
|
)
|
||||||
|
|||||||
+5
-3
@@ -326,9 +326,11 @@ data class VeranstalterDetailUiModel(
|
|||||||
val ansprechpartner: String,
|
val ansprechpartner: String,
|
||||||
val email: String,
|
val email: String,
|
||||||
val telefon: String,
|
val telefon: String,
|
||||||
val adresse: String,
|
val adresse: String,
|
||||||
val loginStatus: LoginStatus,
|
val loginStatus: LoginStatus,
|
||||||
val mitgliedSeit: String,
|
val mitgliedSeit: String,
|
||||||
|
val logoUrl: String? = null,
|
||||||
|
val logoBase64: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class VeranstaltungListUiModel(
|
data class VeranstaltungListUiModel(
|
||||||
|
|||||||
+2
@@ -11,4 +11,6 @@ data class VeranstalterUiModel(
|
|||||||
val ansprechpartner: String,
|
val ansprechpartner: String,
|
||||||
val email: String,
|
val email: String,
|
||||||
val loginStatus: LoginStatus,
|
val loginStatus: LoginStatus,
|
||||||
|
val logoUrl: String? = null,
|
||||||
|
val logoBase64: String? = null,
|
||||||
)
|
)
|
||||||
|
|||||||
+8
@@ -21,6 +21,8 @@ data class VeranstalterWizardState(
|
|||||||
val email: String = "",
|
val email: String = "",
|
||||||
val telefon: String = "",
|
val telefon: String = "",
|
||||||
val adresse: String = "",
|
val adresse: String = "",
|
||||||
|
val logoUrl: String? = null,
|
||||||
|
val logoBase64: String? = null,
|
||||||
val loginStatus: String = "Aktiv",
|
val loginStatus: String = "Aktiv",
|
||||||
val success: Boolean = false,
|
val success: Boolean = false,
|
||||||
val errorMessage: String? = null
|
val errorMessage: String? = null
|
||||||
@@ -35,6 +37,7 @@ sealed interface VeranstalterWizardIntent {
|
|||||||
data class UpdateEmail(val v: String) : VeranstalterWizardIntent
|
data class UpdateEmail(val v: String) : VeranstalterWizardIntent
|
||||||
data class UpdateTelefon(val v: String) : VeranstalterWizardIntent
|
data class UpdateTelefon(val v: String) : VeranstalterWizardIntent
|
||||||
data class UpdateAdresse(val v: String) : VeranstalterWizardIntent
|
data class UpdateAdresse(val v: String) : VeranstalterWizardIntent
|
||||||
|
data class UpdateLogo(val base64: String?) : VeranstalterWizardIntent
|
||||||
data object Save : VeranstalterWizardIntent
|
data object Save : VeranstalterWizardIntent
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,6 +58,7 @@ class VeranstalterWizardViewModel(
|
|||||||
is VeranstalterWizardIntent.UpdateEmail -> _state.value = _state.value.copy(email = intent.v)
|
is VeranstalterWizardIntent.UpdateEmail -> _state.value = _state.value.copy(email = intent.v)
|
||||||
is VeranstalterWizardIntent.UpdateTelefon -> _state.value = _state.value.copy(telefon = 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.UpdateAdresse -> _state.value = _state.value.copy(adresse = intent.v)
|
||||||
|
is VeranstalterWizardIntent.UpdateLogo -> _state.value = _state.value.copy(logoBase64 = intent.base64)
|
||||||
is VeranstalterWizardIntent.Save -> save()
|
is VeranstalterWizardIntent.Save -> save()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,6 +76,8 @@ class VeranstalterWizardViewModel(
|
|||||||
email = v.email,
|
email = v.email,
|
||||||
telefon = v.telefon,
|
telefon = v.telefon,
|
||||||
adresse = v.adresse,
|
adresse = v.adresse,
|
||||||
|
logoUrl = v.logoUrl,
|
||||||
|
logoBase64 = v.logoBase64,
|
||||||
loginStatus = v.loginStatus
|
loginStatus = v.loginStatus
|
||||||
)
|
)
|
||||||
}.onFailure { t ->
|
}.onFailure { t ->
|
||||||
@@ -93,6 +99,8 @@ class VeranstalterWizardViewModel(
|
|||||||
email = s.email,
|
email = s.email,
|
||||||
telefon = s.telefon,
|
telefon = s.telefon,
|
||||||
adresse = s.adresse,
|
adresse = s.adresse,
|
||||||
|
logoUrl = s.logoUrl,
|
||||||
|
logoBase64 = s.logoBase64,
|
||||||
loginStatus = s.loginStatus
|
loginStatus = s.loginStatus
|
||||||
)
|
)
|
||||||
val result = if (s.editId != null) {
|
val result = if (s.editId != null) {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ kotlin {
|
|||||||
implementation(projects.frontend.features.deviceInitialization)
|
implementation(projects.frontend.features.deviceInitialization)
|
||||||
implementation(projects.frontend.features.znsImportFeature)
|
implementation(projects.frontend.features.znsImportFeature)
|
||||||
implementation(projects.frontend.features.turnierFeature)
|
implementation(projects.frontend.features.turnierFeature)
|
||||||
|
implementation(projects.frontend.features.veranstalterFeature)
|
||||||
|
|
||||||
implementation(compose.foundation)
|
implementation(compose.foundation)
|
||||||
implementation(compose.runtime)
|
implementation(compose.runtime)
|
||||||
|
|||||||
+12
-2
@@ -1,6 +1,5 @@
|
|||||||
package at.mocode.veranstaltung.feature.di
|
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.EventWizardViewModel
|
||||||
import at.mocode.veranstaltung.feature.presentation.VeranstaltungManagementViewModel
|
import at.mocode.veranstaltung.feature.presentation.VeranstaltungManagementViewModel
|
||||||
import org.koin.core.qualifier.named
|
import org.koin.core.qualifier.named
|
||||||
@@ -8,5 +7,16 @@ import org.koin.dsl.module
|
|||||||
|
|
||||||
val veranstaltungModule = module {
|
val veranstaltungModule = module {
|
||||||
factory { VeranstaltungManagementViewModel(get()) }
|
factory { VeranstaltungManagementViewModel(get()) }
|
||||||
factory { EventWizardViewModel(get(named("apiClient")), get(), get(), get(), get<ZnsImportProvider>(), get()) }
|
factory { (veranstalterId: Long?) ->
|
||||||
|
EventWizardViewModel(
|
||||||
|
veranstalterIdParam = veranstalterId,
|
||||||
|
httpClient = get(named("apiClient")),
|
||||||
|
authTokenManager = get(),
|
||||||
|
vereinRepository = get(),
|
||||||
|
veranstalterRepository = get(),
|
||||||
|
masterdataRepository = get(),
|
||||||
|
znsImportProvider = get(),
|
||||||
|
turnierWizardViewModel = get()
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+24
@@ -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.domain.zns.ZnsRemoteVerein
|
||||||
import at.mocode.frontend.core.network.NetworkConfig
|
import at.mocode.frontend.core.network.NetworkConfig
|
||||||
import at.mocode.frontend.features.turnier.presentation.TurnierWizardViewModel
|
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 at.mocode.frontend.features.verein.domain.VereinRepository
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
@@ -64,9 +65,11 @@ data class VeranstaltungWizardState(
|
|||||||
|
|
||||||
@OptIn(ExperimentalUuidApi::class)
|
@OptIn(ExperimentalUuidApi::class)
|
||||||
class EventWizardViewModel(
|
class EventWizardViewModel(
|
||||||
|
private val veranstalterIdParam: Long?,
|
||||||
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 veranstalterRepository: VeranstalterRepository,
|
||||||
private val masterdataRepository: MasterdataRepository,
|
private val masterdataRepository: MasterdataRepository,
|
||||||
private val znsImportProvider: ZnsImportProvider,
|
private val znsImportProvider: ZnsImportProvider,
|
||||||
val turnierWizardViewModel: TurnierWizardViewModel // Injected Child-ViewModel
|
val turnierWizardViewModel: TurnierWizardViewModel // Injected Child-ViewModel
|
||||||
@@ -80,6 +83,27 @@ class EventWizardViewModel(
|
|||||||
checkStammdatenStatus()
|
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))
|
||||||
|
|
||||||
|
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() {
|
fun checkZnsAvailability() {
|
||||||
|
|||||||
+34
-19
@@ -49,25 +49,40 @@ fun DesktopApp() {
|
|||||||
|
|
||||||
// Login-Gate: Nicht-authentifizierte Screens → Login, außer DeviceInitialization ist erlaubt
|
// Login-Gate: Nicht-authentifizierte Screens → Login, außer DeviceInitialization ist erlaubt
|
||||||
// Vision_03 Update: Wir starten mit DeviceInitialization
|
// Vision_03 Update: Wir starten mit DeviceInitialization
|
||||||
if (!authState.isAuthenticated && currentScreen !is AppScreen.Login && currentScreen !is AppScreen.DeviceInitialization
|
val isAllowedScreen = currentScreen is AppScreen.Login ||
|
||||||
&& currentScreen !is AppScreen.EventVerwaltung
|
currentScreen is AppScreen.DeviceInitialization ||
|
||||||
&& currentScreen !is AppScreen.VeranstalterAuswahl && currentScreen !is AppScreen.VeranstalterNeu
|
currentScreen is AppScreen.EventVerwaltung ||
|
||||||
&& currentScreen !is AppScreen.VeranstalterDetail && currentScreen !is AppScreen.EventKonfig
|
currentScreen is AppScreen.VeranstalterAuswahl ||
|
||||||
&& currentScreen !is AppScreen.EventProfil && currentScreen !is AppScreen.TurnierDetail
|
currentScreen is AppScreen.VeranstalterNeu ||
|
||||||
&& currentScreen !is AppScreen.TurnierNeu
|
currentScreen is AppScreen.VeranstalterVerwaltung ||
|
||||||
&& currentScreen !is AppScreen.ReiterVerwaltung && currentScreen !is AppScreen.Reiter
|
currentScreen is AppScreen.VeranstalterDetail ||
|
||||||
&& currentScreen !is AppScreen.PferdVerwaltung && currentScreen !is AppScreen.Pferde
|
currentScreen is AppScreen.VeranstalterProfil ||
|
||||||
&& currentScreen !is AppScreen.VereinVerwaltung && currentScreen !is AppScreen.Vereine
|
currentScreen is AppScreen.VeranstalterProfilEdit ||
|
||||||
&& currentScreen !is AppScreen.FunktionaerVerwaltung && currentScreen !is AppScreen.FunktionaerProfil
|
currentScreen is AppScreen.EventKonfig ||
|
||||||
&& currentScreen !is AppScreen.ReiterProfil
|
currentScreen is AppScreen.EventProfil ||
|
||||||
&& currentScreen !is AppScreen.PferdProfil
|
currentScreen is AppScreen.EventDetail ||
|
||||||
&& currentScreen !is AppScreen.VereinProfil
|
currentScreen is AppScreen.EventNeu ||
|
||||||
&& currentScreen !is AppScreen.StammdatenImport
|
currentScreen is AppScreen.TurnierDetail ||
|
||||||
&& currentScreen !is AppScreen.NennungsEingang
|
currentScreen is AppScreen.TurnierNeu ||
|
||||||
&& currentScreen !is AppScreen.EventNeu
|
currentScreen is AppScreen.ReiterVerwaltung ||
|
||||||
&& currentScreen !is AppScreen.ConnectivityCheck
|
currentScreen is AppScreen.Reiter ||
|
||||||
&& currentScreen !is AppScreen.Dashboard
|
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) {
|
LaunchedEffect(currentScreen) {
|
||||||
if (!DeviceInitializationSettingsManager.isConfigured()) {
|
if (!DeviceInitializationSettingsManager.isConfigured()) {
|
||||||
println("[DesktopApp] Nicht authentifiziert & nicht konfiguriert -> Setup")
|
println("[DesktopApp] Nicht authentifiziert & nicht konfiguriert -> Setup")
|
||||||
|
|||||||
+1
-6
@@ -45,15 +45,10 @@ fun DesktopMainLayout(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Automatische Umleitung zum DeviceInitialization, wenn Setup fehlt (außer wir sind bereits dort)
|
// 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) {
|
if (!onboardingSettings.isConfigured && currentScreen !is AppScreen.DeviceInitialization) {
|
||||||
println("[DesktopNav] Setup fehlt -> Umleitung zum DeviceInitialization")
|
println("[DesktopNav] Setup fehlt -> Umleitung zum DeviceInitialization")
|
||||||
onNavigate(AppScreen.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.")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -81,7 +81,7 @@ fun DesktopContentArea(
|
|||||||
// Haupt-Zentrale: Event-Verwaltung
|
// Haupt-Zentrale: Event-Verwaltung
|
||||||
is AppScreen.EventVerwaltung -> {
|
is AppScreen.EventVerwaltung -> {
|
||||||
VeranstaltungenScreen(
|
VeranstaltungenScreen(
|
||||||
onVeranstaltungNeu = { onNavigate(AppScreen.EventNeu) },
|
onVeranstaltungNeu = { onNavigate(AppScreen.EventNeu()) },
|
||||||
onVeranstaltungOeffnen = { vId: Long, eId: Long -> onNavigate(AppScreen.EventProfil(vId, eId)) }
|
onVeranstaltungOeffnen = { vId: Long, eId: Long -> onNavigate(AppScreen.EventProfil(vId, eId)) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -177,7 +177,7 @@ fun DesktopContentArea(
|
|||||||
veranstalterId = currentScreen.id,
|
veranstalterId = currentScreen.id,
|
||||||
onBack = onBack,
|
onBack = onBack,
|
||||||
onZurVeranstaltung = { evtId: Long -> onNavigate(AppScreen.EventProfil(currentScreen.id, evtId)) },
|
onZurVeranstaltung = { evtId: Long -> onNavigate(AppScreen.EventProfil(currentScreen.id, evtId)) },
|
||||||
onNeuVeranstaltung = { onNavigate(AppScreen.EventNeu) },
|
onNeuVeranstaltung = { onNavigate(AppScreen.EventKonfig(currentScreen.id)) },
|
||||||
onEditVeranstalter = { id ->
|
onEditVeranstalter = { id ->
|
||||||
onNavigate(AppScreen.VeranstalterProfilEdit(id))
|
onNavigate(AppScreen.VeranstalterProfilEdit(id))
|
||||||
}
|
}
|
||||||
@@ -194,7 +194,7 @@ fun DesktopContentArea(
|
|||||||
// Neuer Flow: Veranstalter auswählen → Event-Wizard
|
// Neuer Flow: Veranstalter auswählen → Event-Wizard
|
||||||
is AppScreen.VeranstalterAuswahl -> VeranstalterAuswahl(
|
is AppScreen.VeranstalterAuswahl -> VeranstalterAuswahl(
|
||||||
onBack = onBack,
|
onBack = onBack,
|
||||||
onWeiter = { _ -> onNavigate(AppScreen.EventNeu) },
|
onWeiter = { _ -> onNavigate(AppScreen.EventNeu()) },
|
||||||
onNeu = { onNavigate(AppScreen.VeranstalterNeu) },
|
onNeu = { onNavigate(AppScreen.VeranstalterNeu) },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+192
-21
@@ -1,12 +1,11 @@
|
|||||||
package at.mocode.frontend.shell.desktop.screens.veranstaltung.wizards
|
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.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
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.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.*
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
@@ -15,15 +14,30 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
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 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.ZnsImportProvider
|
||||||
import at.mocode.frontend.core.domain.zns.ZnsImportState
|
import at.mocode.frontend.core.domain.zns.ZnsImportState
|
||||||
import at.mocode.frontend.features.veranstalter.presentation.VeranstalterWizardIntent
|
import at.mocode.frontend.features.veranstalter.presentation.VeranstalterWizardIntent
|
||||||
import at.mocode.frontend.features.veranstalter.presentation.VeranstalterWizardViewModel
|
import at.mocode.frontend.features.veranstalter.presentation.VeranstalterWizardViewModel
|
||||||
import at.mocode.frontend.shell.desktop.data.Store
|
import at.mocode.frontend.shell.desktop.data.Store
|
||||||
import at.mocode.frontend.shell.desktop.screens.veranstaltung.components.pickZnsFile
|
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.koinInject
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@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
|
@Composable
|
||||||
fun Step2VeranstalterDetails(viewModel: VeranstalterWizardViewModel) {
|
fun Step2VeranstalterDetails(viewModel: VeranstalterWizardViewModel) {
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.verticalScroll(rememberScrollState())) {
|
Column(verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||||
Text("Ergänzen Sie die Kontaktdaten für diesen Veranstalter.", style = MaterialTheme.typography.bodyMedium)
|
Text("Ergänzen Sie die Kontaktdaten für diesen Veranstalter.", style = MaterialTheme.typography.bodyMedium)
|
||||||
|
|
||||||
OutlinedTextField(
|
// --- VORSCHAU ---
|
||||||
value = state.name,
|
VeranstalterCardPreview(
|
||||||
onValueChange = { viewModel.send(VeranstalterWizardIntent.UpdateName(it)) },
|
name = state.name,
|
||||||
label = { Text("Name des Veranstalters / Vereins") },
|
ort = state.ort,
|
||||||
modifier = Modifier.fillMaxWidth()
|
oepsNummer = state.oepsNummer,
|
||||||
|
ansprechpartner = state.ansprechpartner,
|
||||||
|
email = state.email,
|
||||||
|
logoBase64 = state.logoBase64,
|
||||||
|
status = state.loginStatus
|
||||||
)
|
)
|
||||||
|
|
||||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
OutlinedTextField(
|
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
value = state.oepsNummer,
|
OutlinedTextField(
|
||||||
onValueChange = { viewModel.send(VeranstalterWizardIntent.UpdateOeps(it)) },
|
value = state.name,
|
||||||
label = { Text("OEBS-Nr") },
|
onValueChange = { viewModel.send(VeranstalterWizardIntent.UpdateName(it)) },
|
||||||
modifier = Modifier.weight(1f)
|
label = { Text("Name des Veranstalters / Vereins") },
|
||||||
)
|
modifier = Modifier.fillMaxWidth()
|
||||||
OutlinedTextField(
|
)
|
||||||
value = state.ort,
|
|
||||||
onValueChange = { viewModel.send(VeranstalterWizardIntent.UpdateOrt(it)) },
|
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
label = { Text("Ort") },
|
OutlinedTextField(
|
||||||
modifier = Modifier.weight(2f)
|
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()
|
HorizontalDivider()
|
||||||
|
|||||||
Reference in New Issue
Block a user