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:
+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