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:
+7
-3
@@ -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())
|
||||
}
|
||||
|
||||
+2
@@ -11,6 +11,8 @@ data class Veranstalter(
|
||||
val telefon: String = "",
|
||||
val adresse: 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
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
+5
-3
@@ -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(
|
||||
|
||||
+2
@@ -11,4 +11,6 @@ data class VeranstalterUiModel(
|
||||
val ansprechpartner: String,
|
||||
val email: String,
|
||||
val loginStatus: LoginStatus,
|
||||
val logoUrl: String? = null,
|
||||
val logoBase64: String? = null,
|
||||
)
|
||||
|
||||
+8
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
+12
-2
@@ -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<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.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() {
|
||||
|
||||
+34
-19
@@ -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")
|
||||
|
||||
+1
-6
@@ -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.")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -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) },
|
||||
)
|
||||
|
||||
|
||||
+192
-21
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user