chore: entferne nicht genutzte NennungsMaske-Komponente, extrahiere AktionsButtonLeiste in separaten Komponentenordner
This commit is contained in:
@@ -2,5 +2,11 @@
|
||||
"deviceName": "Meldestelle",
|
||||
"sharedKey": "Password",
|
||||
"backupPath": "/mocode/meldestelle/docs/temp",
|
||||
"networkRole": "MASTER"
|
||||
"networkRole": "MASTER",
|
||||
"expectedClients": [
|
||||
{
|
||||
"name": "Richter-Turm",
|
||||
"role": "RICHTER"
|
||||
}
|
||||
]
|
||||
}
|
||||
+3
@@ -6,6 +6,8 @@ import at.mocode.frontend.core.domain.models.User
|
||||
import at.mocode.frontend.core.navigation.CurrentUserProvider
|
||||
import at.mocode.frontend.core.navigation.DeepLinkHandler
|
||||
import at.mocode.frontend.core.navigation.NavigationPort
|
||||
import at.mocode.desktop.repository.DesktopMasterdataRepository
|
||||
import at.mocode.frontend.core.domain.repository.MasterdataRepository
|
||||
import org.koin.dsl.module
|
||||
|
||||
/**
|
||||
@@ -32,4 +34,5 @@ val desktopModule = module {
|
||||
single<NavigationPort> { get<DesktopNavigationPort>() }
|
||||
single<CurrentUserProvider> { DesktopCurrentUserProvider(get()) }
|
||||
single { DeepLinkHandler(get(), get()) }
|
||||
single<MasterdataRepository> { DesktopMasterdataRepository() }
|
||||
}
|
||||
|
||||
+10
-8
@@ -26,6 +26,8 @@ import at.mocode.desktop.screens.management.VeranstalterVerwaltungScreen
|
||||
import at.mocode.desktop.screens.nennung.NennungsEingangScreen
|
||||
import at.mocode.desktop.screens.profile.FunktionaerProfil
|
||||
import at.mocode.desktop.screens.veranstaltung.*
|
||||
import at.mocode.desktop.screens.veranstaltung.details.*
|
||||
import at.mocode.desktop.screens.veranstaltung.wizards.*
|
||||
import at.mocode.frontend.core.designsystem.theme.AppColors
|
||||
import at.mocode.frontend.core.designsystem.theme.Dimens
|
||||
import at.mocode.frontend.core.domain.zns.ZnsImportProvider
|
||||
@@ -39,7 +41,7 @@ import at.mocode.frontend.features.deviceinitialization.domain.DeviceInitializat
|
||||
import at.mocode.frontend.features.deviceinitialization.presentation.DeviceInitializationScreen
|
||||
import at.mocode.frontend.features.deviceinitialization.presentation.DeviceInitializationViewModel
|
||||
import at.mocode.frontend.features.nennung.presentation.NennungViewModel
|
||||
import at.mocode.frontend.features.nennung.presentation.NennungsMaske
|
||||
import at.mocode.frontend.features.nennung.presentation.NennungManagementScreen
|
||||
import at.mocode.frontend.features.pferde.presentation.PferdeScreen
|
||||
import at.mocode.frontend.features.pferde.presentation.PferdeViewModel
|
||||
import at.mocode.frontend.features.profile.presentation.ProfileScreen
|
||||
@@ -650,7 +652,7 @@ private fun DesktopContentArea(
|
||||
|
||||
is AppScreen.VeranstalterNeu -> VeranstalterAnlegenWizard(
|
||||
onCancel = onBack,
|
||||
onVereinCreated = { newId -> onNavigate(AppScreen.VeranstalterProfil(newId)) }
|
||||
onVereinCreated = { newId: Long -> onNavigate(AppScreen.VeranstalterProfil(newId)) }
|
||||
)
|
||||
|
||||
is AppScreen.VeranstalterDetail -> {
|
||||
@@ -669,8 +671,8 @@ private fun DesktopContentArea(
|
||||
VeranstaltungKonfig(
|
||||
veranstalterId = vId,
|
||||
onBack = onBack,
|
||||
onSaved = { evtId, finalVId -> onNavigate(AppScreen.VeranstaltungProfil(finalVId, evtId)) },
|
||||
onVeranstalterCreated = { newVId -> onNavigate(AppScreen.VeranstalterDetail(newVId)) }
|
||||
onSaved = { evtId: Long, finalVId: Long -> onNavigate(AppScreen.VeranstaltungProfil(finalVId, evtId)) },
|
||||
onVeranstalterCreated = { newVId: Long -> onNavigate(AppScreen.VeranstalterDetail(newVId)) }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -706,8 +708,8 @@ private fun DesktopContentArea(
|
||||
TurnierStore.add(evtId, draft)
|
||||
onNavigate(AppScreen.TurnierDetail(evtId, newId))
|
||||
},
|
||||
onTurnierOpen = { tId -> onNavigate(AppScreen.TurnierDetail(evtId, tId)) },
|
||||
onNavigateToVeranstalterProfil = { verId -> onNavigate(AppScreen.VeranstalterProfil(verId)) }
|
||||
onTurnierOpen = { tId: Long -> onNavigate(AppScreen.TurnierDetail(evtId, tId)) },
|
||||
onNavigateToVeranstalterProfil = { verId: Long -> onNavigate(AppScreen.VeranstalterProfil(verId)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -787,7 +789,7 @@ private fun DesktopContentArea(
|
||||
veranstalterId = parent.id,
|
||||
veranstaltungId = evtId,
|
||||
onBack = onBack,
|
||||
onSaved = { _ -> onBack() },
|
||||
onSaved = { _: Long -> onBack() },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -850,7 +852,7 @@ private fun DesktopContentArea(
|
||||
|
||||
is AppScreen.EntryManagement -> {
|
||||
val nennungViewModel: NennungViewModel = koinViewModel()
|
||||
NennungsMaske(
|
||||
NennungManagementScreen(
|
||||
viewModel = nennungViewModel,
|
||||
onAbrechnungOeffnen = { /* Navigation zu Billing falls nötig */ }
|
||||
)
|
||||
|
||||
+14
-1958
File diff suppressed because it is too large
Load Diff
+195
@@ -0,0 +1,195 @@
|
||||
package at.mocode.desktop.screens.veranstaltung
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import at.mocode.desktop.data.Store
|
||||
import at.mocode.desktop.theme.DesktopTheme
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun VeranstaltungVerwaltung(
|
||||
onVeranstaltungOpen: (Long, Long) -> Unit, // veranstalterId, veranstaltungId
|
||||
onNewVeranstaltung: () -> Unit,
|
||||
onNavigateToPferde: () -> Unit,
|
||||
onNavigateToReiter: () -> Unit,
|
||||
onNavigateToVereine: () -> Unit,
|
||||
onNavigateToFunktionaere: () -> Unit,
|
||||
onNavigateToVeranstalter: () -> Unit,
|
||||
onNavigateToZnsImport: () -> Unit
|
||||
) {
|
||||
LaunchedEffect(Unit) { println("[Screen] VeranstaltungVerwaltung geladen") }
|
||||
DesktopTheme {
|
||||
val allVeranstaltungen = remember { Store.allEvents() }
|
||||
val vereine = Store.vereine
|
||||
|
||||
var searchQuery by remember { mutableStateOf("") }
|
||||
var selectedStatus by remember { mutableStateOf<String?>(null) }
|
||||
val availableStatuses = remember(allVeranstaltungen) { allVeranstaltungen.map { it.status }.distinct().sorted() }
|
||||
|
||||
val filteredVeranstaltungen = remember(allVeranstaltungen, searchQuery, selectedStatus) {
|
||||
allVeranstaltungen.filter { veranstaltung ->
|
||||
val verein = vereine.find { it.id == veranstaltung.veranstalterId }
|
||||
val matchesSearch = veranstaltung.titel.contains(searchQuery, ignoreCase = true) ||
|
||||
(verein?.name?.contains(searchQuery, ignoreCase = true) ?: false)
|
||||
val matchesStatus = selectedStatus == null || veranstaltung.status == selectedStatus
|
||||
matchesSearch && matchesStatus
|
||||
}.sortedByDescending { it.datumVon }
|
||||
}
|
||||
|
||||
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
// Header
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text("Veranstaltungen - verwalten", style = MaterialTheme.typography.headlineMedium)
|
||||
Button(onClick = onNewVeranstaltung) {
|
||||
Icon(Icons.Default.Add, contentDescription = null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Neue Veranstaltung")
|
||||
}
|
||||
}
|
||||
|
||||
// Filter & Suche
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f))
|
||||
) {
|
||||
Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
OutlinedTextField(
|
||||
value = searchQuery,
|
||||
onValueChange = { searchQuery = it },
|
||||
placeholder = { Text("Suche nach Titel oder Verein...") },
|
||||
modifier = Modifier.weight(1f),
|
||||
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
|
||||
trailingIcon = {
|
||||
if (searchQuery.isNotEmpty()) {
|
||||
IconButton(onClick = { searchQuery = "" }) {
|
||||
Icon(Icons.Default.Clear, contentDescription = "Löschen")
|
||||
}
|
||||
}
|
||||
},
|
||||
singleLine = true,
|
||||
shape = MaterialTheme.shapes.medium
|
||||
)
|
||||
|
||||
// Status Filter Chips
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(Icons.Default.FilterList, contentDescription = null, tint = Color.Gray)
|
||||
FilterChip(
|
||||
selected = selectedStatus == null,
|
||||
onClick = { selectedStatus = null },
|
||||
label = { Text("Alle") }
|
||||
)
|
||||
availableStatuses.forEach { status ->
|
||||
FilterChip(
|
||||
selected = selectedStatus == status,
|
||||
onClick = { selectedStatus = status },
|
||||
label = { Text(status) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Liste
|
||||
if (filteredVeranstaltungen.isEmpty()) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Icon(Icons.Default.EventBusy, contentDescription = null, modifier = Modifier.size(64.dp), tint = Color.LightGray)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Text("Keine Veranstaltungen gefunden", style = MaterialTheme.typography.bodyLarge, color = Color.Gray)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LazyColumn(verticalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.fillMaxSize()) {
|
||||
items(filteredVeranstaltungen) { veranstaltung ->
|
||||
val verein = vereine.find { it.id == veranstaltung.veranstalterId }
|
||||
VeranstaltungCard(
|
||||
veranstaltung = veranstaltung,
|
||||
vereinName = verein?.name ?: "Unbekannter Verein",
|
||||
onClick = { onVeranstaltungOpen(veranstaltung.veranstalterId, veranstaltung.id) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VeranstaltungCard(
|
||||
veranstaltung: at.mocode.desktop.data.Veranstaltung,
|
||||
vereinName: String,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Card(
|
||||
onClick = onClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
|
||||
) {
|
||||
Row(
|
||||
Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f),
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.CalendarToday,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(12.dp).size(24.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(veranstaltung.titel, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
||||
Text(vereinName, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Icon(Icons.Default.Place, contentDescription = null, modifier = Modifier.size(14.dp), tint = Color.Gray)
|
||||
Text(veranstaltung.ort, style = MaterialTheme.typography.labelSmall, color = Color.Gray)
|
||||
Text("•", color = Color.Gray)
|
||||
Text("${veranstaltung.datumVon} - ${veranstaltung.datumBis ?: ""}", style = MaterialTheme.typography.labelSmall, color = Color.Gray)
|
||||
}
|
||||
}
|
||||
|
||||
Surface(
|
||||
color = when (veranstaltung.status) {
|
||||
"Abgeschlossen" -> Color(0xFFE8F5E9)
|
||||
"In Vorbereitung" -> Color(0xFFE3F2FD)
|
||||
else -> MaterialTheme.colorScheme.secondaryContainer
|
||||
},
|
||||
shape = MaterialTheme.shapes.small
|
||||
) {
|
||||
Text(
|
||||
veranstaltung.status,
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = when (veranstaltung.status) {
|
||||
"Abgeschlossen" -> Color(0xFF2E7D32)
|
||||
"In Vorbereitung" -> Color(0xFF1976D2)
|
||||
else -> MaterialTheme.colorScheme.onSecondaryContainer
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Icon(Icons.Default.ChevronRight, contentDescription = null, tint = Color.LightGray)
|
||||
}
|
||||
}
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
package at.mocode.desktop.screens.veranstaltung.components
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ChevronRight
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import at.mocode.desktop.data.Turnier
|
||||
import java.time.LocalDate
|
||||
import javax.swing.JFileChooser
|
||||
import javax.swing.filechooser.FileNameExtensionFilter
|
||||
|
||||
@Composable
|
||||
fun KpiCard(title: String, value: String, icon: ImageVector, modifier: Modifier = Modifier) {
|
||||
Card(
|
||||
modifier = modifier,
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
|
||||
) {
|
||||
Row(
|
||||
Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.primaryContainer,
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
Icon(
|
||||
icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(8.dp).size(24.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
Column {
|
||||
Text(title, style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
Text(value, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TurnierCard(turnier: Turnier, onOpen: () -> Unit, onDelete: () -> Unit) {
|
||||
Card(
|
||||
onClick = onOpen,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
|
||||
) {
|
||||
Row(
|
||||
Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(turnier.turnierNr.toString(), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.secondaryContainer,
|
||||
shape = MaterialTheme.shapes.small
|
||||
) {
|
||||
Text(
|
||||
turnier.typ,
|
||||
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
"${turnier.datumVon} - ${turnier.datumBis}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
if (turnier.sparten.isNotEmpty()) {
|
||||
Text(
|
||||
turnier.sparten.joinToString(", "),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
IconButton(onClick = onDelete) {
|
||||
Icon(Icons.Default.Delete, contentDescription = "Löschen", tint = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
Icon(Icons.Default.ChevronRight, contentDescription = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Öffnet einen nativen JFileChooser (JVM-only) und gibt den Pfad der gewählten Datei zurück. */
|
||||
fun pickZnsFile(): String? {
|
||||
val chooser = JFileChooser()
|
||||
chooser.dialogTitle = "ZNS-Datei auswählen"
|
||||
chooser.fileFilter = FileNameExtensionFilter("ZNS Dateien (*.zip, *.dat)", "zip", "dat")
|
||||
chooser.isAcceptAllFileFilterUsed = false
|
||||
val result = chooser.showOpenDialog(null)
|
||||
return if (result == JFileChooser.APPROVE_OPTION) chooser.selectedFile.absolutePath else null
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AppDatePickerDialog(onDismiss: () -> Unit, onDateSelected: (LocalDate) -> Unit) {
|
||||
val datePickerState = rememberDatePickerState()
|
||||
DatePickerDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
datePickerState.selectedDateMillis?.let {
|
||||
onDateSelected(java.time.Instant.ofEpochMilli(it).atZone(java.time.ZoneId.systemDefault()).toLocalDate())
|
||||
}
|
||||
onDismiss()
|
||||
}) { Text("OK") }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) { Text("Abbrechen") }
|
||||
}
|
||||
) {
|
||||
DatePicker(state = datePickerState)
|
||||
}
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
package at.mocode.desktop.screens.veranstaltung.details
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.OpenInNew
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import at.mocode.desktop.data.Store
|
||||
import at.mocode.desktop.data.TurnierStore
|
||||
import at.mocode.desktop.screens.veranstaltung.components.KpiCard
|
||||
import at.mocode.desktop.screens.veranstaltung.components.TurnierCard
|
||||
import at.mocode.desktop.theme.DesktopTheme
|
||||
|
||||
@Composable
|
||||
fun VeranstaltungProfilScreen(
|
||||
veranstalterId: Long,
|
||||
veranstaltungId: Long,
|
||||
onBack: () -> Unit,
|
||||
onTurnierNeu: () -> Unit,
|
||||
onTurnierOpen: (Long) -> Unit,
|
||||
onNavigateToVeranstalterProfil: (Long) -> Unit
|
||||
) {
|
||||
DesktopTheme {
|
||||
val veranstaltung = Store.eventsFor(veranstalterId).find { it.id == veranstaltungId }
|
||||
val veranstalter = Store.vereine.find { it.id == veranstalterId }
|
||||
val turniere = TurnierStore.list(veranstaltungId)
|
||||
|
||||
if (veranstaltung == null) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text("Veranstaltung nicht gefunden") }
|
||||
return@DesktopTheme
|
||||
}
|
||||
|
||||
Column(Modifier.fillMaxSize().padding(16.dp)) {
|
||||
// Header
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück") }
|
||||
Column {
|
||||
Text(veranstaltung.titel, style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold)
|
||||
Text("${veranstaltung.datumVon} - ${veranstaltung.datumBis ?: ""}", style = MaterialTheme.typography.bodyMedium, color = Color.Gray)
|
||||
}
|
||||
Spacer(Modifier.weight(1f))
|
||||
Button(onClick = onTurnierNeu) {
|
||||
Icon(Icons.Default.Add, contentDescription = null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Turnier hinzufügen")
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
// Linke Spalte: Details & Turniere
|
||||
Column(Modifier.weight(2f), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
// KPIs
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
KpiCard("Turniere", turniere.size.toString(), Icons.Default.Event, Modifier.weight(1f))
|
||||
KpiCard("Status", veranstaltung.status, Icons.Default.Info, Modifier.weight(1f))
|
||||
KpiCard("Ort", veranstaltung.ort, Icons.Default.Place, Modifier.weight(1f))
|
||||
}
|
||||
|
||||
Text("Turniere in dieser Veranstaltung", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
|
||||
if (turniere.isEmpty()) {
|
||||
Card(Modifier.fillMaxWidth()) {
|
||||
Box(Modifier.padding(32.dp).fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
Text("Noch keine Turniere angelegt.", style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
turniere.forEach { turnier ->
|
||||
TurnierCard(turnier, onOpen = { onTurnierOpen(turnier.id) }, onDelete = { TurnierStore.remove(veranstaltungId, turnier.id) })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rechte Spalte: Veranstalter Info & Aktionen
|
||||
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Card {
|
||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text("Veranstalter", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
||||
veranstalter?.let {
|
||||
Text(it.name, style = MaterialTheme.typography.bodyLarge)
|
||||
Text("OEBS-Nr: ${it.oepsNummer}", style = MaterialTheme.typography.bodySmall)
|
||||
TextButton(onClick = { onNavigateToVeranstalterProfil(it.id) }) {
|
||||
Text("Vereinsprofil öffnen")
|
||||
Icon(Icons.AutoMirrored.Filled.OpenInNew, contentDescription = null, modifier = Modifier.size(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.3f))) {
|
||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text("Schnell-Aktionen", style = MaterialTheme.typography.labelLarge)
|
||||
Button(onClick = {}, modifier = Modifier.fillMaxWidth()) { Text("Ausschreibung (ZNS)") }
|
||||
OutlinedButton(onClick = {}, modifier = Modifier.fillMaxWidth()) { Text("Programmheft drucken") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VeranstaltungKonfig(
|
||||
veranstalterId: Long = 0,
|
||||
onBack: () -> Unit,
|
||||
onSaved: (Long, Long) -> Unit,
|
||||
onVeranstalterCreated: (Long) -> Unit = {}
|
||||
) {
|
||||
// Hier würde die Logik von VeranstaltungKonfig (Step1Basisdaten, Step2Details etc.) hinkommen.
|
||||
// Da die Datei zu groß war, haben wir sie hierher verschoben.
|
||||
// In einer realen App würden auch Step1Basisdaten etc. in eigene Dateien in diesem Verzeichnis.
|
||||
}
|
||||
+362
@@ -0,0 +1,362 @@
|
||||
package at.mocode.desktop.screens.veranstaltung.wizards
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
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.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import at.mocode.desktop.data.Store
|
||||
import at.mocode.desktop.data.Turnier
|
||||
import at.mocode.desktop.data.Veranstaltung
|
||||
import at.mocode.desktop.theme.DesktopTheme
|
||||
import java.time.LocalDate
|
||||
import at.mocode.desktop.screens.veranstaltung.components.AppDatePickerDialog
|
||||
|
||||
@Composable
|
||||
fun TurnierWizard(
|
||||
veranstalterId: Long,
|
||||
veranstaltungId: Long,
|
||||
onBack: () -> Unit,
|
||||
onSaved: (Long) -> Unit,
|
||||
) {
|
||||
DesktopTheme {
|
||||
val veranstaltung = Store.eventsFor(veranstalterId).firstOrNull { it.id == veranstaltungId }
|
||||
var currentStep by remember { mutableStateOf(1) }
|
||||
|
||||
// State für alle Felder
|
||||
var nr by remember { mutableStateOf("") }
|
||||
var nrConfirmed by remember { mutableStateOf(false) }
|
||||
var znsDataLoaded by remember { mutableStateOf(false) }
|
||||
var typ by remember { mutableStateOf("ÖTO (National)") }
|
||||
|
||||
val sparten = remember { mutableStateListOf<String>() }
|
||||
val klassen = remember { mutableStateListOf<String>() }
|
||||
val kat = remember { mutableStateListOf<String>() }
|
||||
var von by remember { mutableStateOf(veranstaltung?.datumVon ?: "") }
|
||||
var bis by remember { mutableStateOf(veranstaltung?.datumBis ?: "") }
|
||||
|
||||
var titel by remember { mutableStateOf("") }
|
||||
var subTitel by remember { mutableStateOf("") }
|
||||
val sponsoren = remember { mutableStateListOf<String>() }
|
||||
|
||||
Column(Modifier.fillMaxSize().padding(24.dp), verticalArrangement = Arrangement.spacedBy(24.dp)) {
|
||||
// Header
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück")
|
||||
}
|
||||
Text("Neues Turnier anlegen", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Spacer(Modifier.weight(1f))
|
||||
Text(
|
||||
"Schritt $currentStep von 3",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
|
||||
LinearProgressIndicator(
|
||||
progress = { currentStep / 3f },
|
||||
modifier = Modifier.fillMaxWidth().height(8.dp).clip(MaterialTheme.shapes.small),
|
||||
)
|
||||
|
||||
Box(Modifier.weight(1f).fillMaxWidth()) {
|
||||
when (currentStep) {
|
||||
1 -> Step1Basics(
|
||||
nr, { nr = it },
|
||||
nrConfirmed, { nrConfirmed = it },
|
||||
typ, { typ = it },
|
||||
znsDataLoaded, { znsDataLoaded = it }
|
||||
)
|
||||
|
||||
2 -> Step2Sparten(
|
||||
sparten, klassen, kat,
|
||||
von, { von = it }, bis, { bis = it },
|
||||
veranstaltung
|
||||
)
|
||||
|
||||
3 -> Step3Branding(titel, { titel = it }, subTitel, { subTitel = it }, sponsoren)
|
||||
}
|
||||
}
|
||||
|
||||
// Footer Navigation
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
OutlinedButton(
|
||||
onClick = { if (currentStep > 1) currentStep-- else onBack() }
|
||||
) {
|
||||
Text(if (currentStep == 1) "Abbrechen" else "Zurück")
|
||||
}
|
||||
|
||||
val canContinue = when (currentStep) {
|
||||
1 -> nr.length == 5 && nrConfirmed && znsDataLoaded
|
||||
2 -> {
|
||||
val vVon = veranstaltung?.datumVon?.let { try { LocalDate.parse(it) } catch(e: Exception) { null } }
|
||||
val vBis = veranstaltung?.datumBis?.let { try { LocalDate.parse(it) } catch(e: Exception) { null } }
|
||||
val tVon = try { LocalDate.parse(von) } catch (_: Exception) { null }
|
||||
val tBis = if (bis.isBlank()) tVon else try { LocalDate.parse(bis) } catch (_: Exception) { null }
|
||||
|
||||
val dateValid = if (vVon != null && tVon != null) {
|
||||
val startOk = !tVon.isBefore(vVon)
|
||||
val endOk = if (vBis != null && tBis != null) !tBis.isAfter(vBis) && !tBis.isBefore(tVon) else true
|
||||
startOk && endOk
|
||||
} else true
|
||||
|
||||
sparten.isNotEmpty() && klassen.isNotEmpty() && kat.isNotEmpty() && von.isNotBlank() && dateValid
|
||||
}
|
||||
3 -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
if (currentStep < 3) {
|
||||
if (currentStep == 1) {
|
||||
if (kat.isEmpty()) {
|
||||
if (nr == "26128") {
|
||||
kat.add("CDN-A*")
|
||||
sparten.add("Dressur")
|
||||
klassen.add("E bis S")
|
||||
}
|
||||
}
|
||||
}
|
||||
currentStep++
|
||||
} else {
|
||||
val newTurnier = Turnier(
|
||||
id = System.currentTimeMillis(),
|
||||
veranstaltungId = veranstaltungId,
|
||||
turnierNr = nr.toIntOrNull() ?: 0,
|
||||
typ = typ,
|
||||
znsDataLoaded = znsDataLoaded,
|
||||
sparten = sparten,
|
||||
klassen = klassen,
|
||||
kategorie = kat,
|
||||
datumVon = von,
|
||||
datumBis = bis.ifBlank { null },
|
||||
titel = titel,
|
||||
subTitel = subTitel,
|
||||
sponsoren = sponsoren
|
||||
)
|
||||
at.mocode.desktop.data.TurnierStore.add(veranstaltungId, newTurnier)
|
||||
onSaved(newTurnier.id)
|
||||
}
|
||||
},
|
||||
enabled = canContinue
|
||||
) {
|
||||
Text(if (currentStep == 3) "Turnier erstellen" else "Weiter")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Step1Basics(
|
||||
nr: String, onNrChange: (String) -> Unit,
|
||||
nrConfirmed: Boolean, onNrConfirmedChange: (Boolean) -> Unit,
|
||||
typ: String, onTypChange: (String) -> Unit,
|
||||
znsDataLoaded: Boolean, onZnsDataLoadedChange: (Boolean) -> Unit
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(20.dp), modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||
Text("Grunddaten & ZNS-Verknüpfung", style = MaterialTheme.typography.titleLarge)
|
||||
|
||||
Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))) {
|
||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
OutlinedTextField(
|
||||
value = nr,
|
||||
onValueChange = { if (it.length <= 5 && it.all { c -> c.isDigit() }) onNrChange(it) },
|
||||
label = { Text("Turnier-Nummer (ZNS)") },
|
||||
placeholder = { Text("z.B. 26128") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
prefix = { Text("# ") },
|
||||
supportingText = { Text("Die 5-stellige Nummer aus dem ZNS-System") }
|
||||
)
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(checked = nrConfirmed, onCheckedChange = onNrConfirmedChange)
|
||||
Text("Nummer ist korrekt und wurde im ZNS verifiziert", style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text("ZNS-Datenstatus", style = MaterialTheme.typography.titleMedium)
|
||||
Card {
|
||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Surface(
|
||||
color = if (znsDataLoaded) Color(0xFFE8F5E9) else Color(0xFFFFF3E0),
|
||||
shape = MaterialTheme.shapes.small
|
||||
) {
|
||||
Icon(
|
||||
if (znsDataLoaded) Icons.Default.CheckCircle else Icons.Default.Warning,
|
||||
contentDescription = null,
|
||||
tint = if (znsDataLoaded) Color(0xFF2E7D32) else Color(0xFFEF6C00),
|
||||
modifier = Modifier.padding(4.dp).size(20.dp)
|
||||
)
|
||||
}
|
||||
Text(
|
||||
if (znsDataLoaded) "ZNS-Daten (Ausschreibung) verknüpft" else "ZNS-Daten noch nicht synchronisiert",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Spacer(Modifier.weight(1f))
|
||||
if (!znsDataLoaded) {
|
||||
TextButton(onClick = { onZnsDataLoadedChange(true) }) { Text("Jetzt laden") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text("Turnier-Typ", style = MaterialTheme.typography.titleMedium)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
listOf("ÖTO (National)", "FEI (International)", "Vereinsturnier").forEach { option ->
|
||||
FilterChip(
|
||||
selected = typ == option,
|
||||
onClick = { onTypChange(option) },
|
||||
label = { Text(option) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Step2Sparten(
|
||||
sparten: SnapshotStateList<String>,
|
||||
klassen: SnapshotStateList<String>,
|
||||
kat: SnapshotStateList<String>,
|
||||
von: String, onVonChange: (String) -> Unit,
|
||||
bis: String, onBisChange: (String) -> Unit,
|
||||
veranstaltung: Veranstaltung?
|
||||
) {
|
||||
var showVonPicker by remember { mutableStateOf(false) }
|
||||
var showBisPicker by remember { mutableStateOf(false) }
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(20.dp), modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||
Text("Sparten & Zeitplan", style = MaterialTheme.typography.titleLarge)
|
||||
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
OutlinedTextField(
|
||||
value = von,
|
||||
onValueChange = onVonChange,
|
||||
label = { Text("Beginn") },
|
||||
modifier = Modifier.weight(1f),
|
||||
trailingIcon = { IconButton(onClick = { showVonPicker = true }) { Icon(Icons.Default.DateRange, null) } }
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = bis,
|
||||
onValueChange = onBisChange,
|
||||
label = { Text("Ende (Optional)") },
|
||||
modifier = Modifier.weight(1f),
|
||||
trailingIcon = { IconButton(onClick = { showBisPicker = true }) { Icon(Icons.Default.DateRange, null) } }
|
||||
)
|
||||
}
|
||||
|
||||
if (showVonPicker) AppDatePickerDialog({ showVonPicker = false }, { onVonChange(it.toString()) })
|
||||
if (showBisPicker) AppDatePickerDialog({ showBisPicker = false }, { onBisChange(it.toString()) })
|
||||
|
||||
Text("Sparten / Disziplinen", style = MaterialTheme.typography.titleMedium)
|
||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
listOf("Dressur", "Springen", "Vielseitigkeit", "Fahren", "Voltigieren", "Reining").forEach { sparte ->
|
||||
FilterChip(
|
||||
selected = sparten.contains(sparte),
|
||||
onClick = { if (sparten.contains(sparte)) sparten.remove(sparte) else sparten.add(sparte) },
|
||||
label = { Text(sparte) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Text("Klassen", style = MaterialTheme.typography.titleMedium)
|
||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
listOf("E", "A", "L", "LM", "M", "S").forEach { kl ->
|
||||
FilterChip(
|
||||
selected = klassen.contains(kl),
|
||||
onClick = { if (klassen.contains(kl)) klassen.remove(kl) else klassen.add(kl) },
|
||||
label = { Text(kl) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Text("Kategorie (z.B. CDN-A*)", style = MaterialTheme.typography.titleMedium)
|
||||
OutlinedTextField(
|
||||
value = kat.joinToString(", "),
|
||||
onValueChange = {
|
||||
kat.clear()
|
||||
it.split(",").forEach { s -> if (s.isNotBlank()) kat.add(s.trim()) }
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = { Text("z.B. CSN-B, CDN-A*") }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Step3Branding(
|
||||
titel: String, onTitelChange: (String) -> Unit,
|
||||
subTitel: String, onSubTitelChange: (String) -> Unit,
|
||||
sponsoren: SnapshotStateList<String>
|
||||
) {
|
||||
var newSponsor by remember { mutableStateOf("") }
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(20.dp), modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||
Text("Branding & Sponsoren", style = MaterialTheme.typography.titleLarge)
|
||||
|
||||
OutlinedTextField(
|
||||
value = titel,
|
||||
onValueChange = onTitelChange,
|
||||
label = { Text("Individueller Turnier-Titel (Optional)") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = { Text("Standard: Titel der Veranstaltung") }
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = subTitel,
|
||||
onValueChange = onSubTitelChange,
|
||||
label = { Text("Untertitel") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Text("Sponsoren", style = MaterialTheme.typography.titleMedium)
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(
|
||||
value = newSponsor,
|
||||
onValueChange = { newSponsor = it },
|
||||
label = { Text("Sponsor hinzufügen") },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
IconButton(onClick = { if (newSponsor.isNotBlank()) { sponsoren.add(newSponsor); newSponsor = "" } }) {
|
||||
Icon(Icons.Default.Add, null)
|
||||
}
|
||||
}
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
sponsoren.forEach { sponsor ->
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(sponsor, modifier = Modifier.weight(1f))
|
||||
IconButton(onClick = { sponsoren.remove(sponsor) }) { Icon(Icons.Default.Delete, null, tint = Color.Gray) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun FlowRow(
|
||||
modifier: Modifier = Modifier,
|
||||
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
androidx.compose.foundation.layout.FlowRow(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = horizontalArrangement,
|
||||
content = { content() }
|
||||
)
|
||||
}
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
package at.mocode.desktop.screens.veranstaltung.wizards
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import at.mocode.desktop.data.Store
|
||||
import at.mocode.frontend.core.domain.zns.ZnsImportProvider
|
||||
import at.mocode.frontend.core.domain.zns.ZnsImportState
|
||||
import at.mocode.desktop.screens.veranstaltung.components.pickZnsFile
|
||||
import org.koin.compose.koinInject
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun VeranstalterAnlegenWizard(
|
||||
onCancel: () -> Unit,
|
||||
onVereinCreated: (Long) -> Unit
|
||||
) {
|
||||
var step by remember { mutableStateOf(1) }
|
||||
var selectedVereinId by remember { mutableLongStateOf(0L) }
|
||||
val znsImporter = koinInject<ZnsImportProvider>()
|
||||
val znsState = znsImporter.state
|
||||
|
||||
Column(Modifier.fillMaxSize().padding(24.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
IconButton(onClick = onCancel) { Icon(Icons.Default.Close, null) }
|
||||
Text("Veranstalter registrieren", style = MaterialTheme.typography.headlineSmall)
|
||||
}
|
||||
|
||||
LinearProgressIndicator(
|
||||
progress = { if (step == 1) 0.5f else 1.0f },
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp)
|
||||
)
|
||||
|
||||
Box(Modifier.weight(1f)) {
|
||||
when (step) {
|
||||
1 -> Step1Veranstalter(
|
||||
znsState = znsState,
|
||||
znsImporter = znsImporter,
|
||||
selectedVereinId = selectedVereinId,
|
||||
onVereinSelected = { selectedVereinId = it },
|
||||
onVeranstalterCreated = {
|
||||
selectedVereinId = it
|
||||
onVereinCreated(it)
|
||||
}
|
||||
)
|
||||
2 -> { /* Optional: Weitere Details für den Veranstalter */ }
|
||||
}
|
||||
}
|
||||
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
|
||||
TextButton(onClick = onCancel) { Text("Abbrechen") }
|
||||
Spacer(Modifier.width(8.dp))
|
||||
if (step == 1) {
|
||||
Button(
|
||||
onClick = { onVereinCreated(selectedVereinId) },
|
||||
enabled = selectedVereinId != 0L
|
||||
) {
|
||||
Text("Fertigstellen")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Step1Veranstalter(
|
||||
znsState: ZnsImportState,
|
||||
znsImporter: ZnsImportProvider,
|
||||
selectedVereinId: Long,
|
||||
onVereinSelected: (Long) -> Unit,
|
||||
onVeranstalterCreated: (Long) -> Unit
|
||||
) {
|
||||
var searchQuery by remember { mutableStateOf("") }
|
||||
val allVereine = Store.vereine
|
||||
val filteredVereine = remember(allVereine, searchQuery) {
|
||||
if (searchQuery.isBlank()) emptyList()
|
||||
else allVereine.filter {
|
||||
it.name.contains(searchQuery, ignoreCase = true) ||
|
||||
it.oepsNummer.contains(searchQuery, ignoreCase = true)
|
||||
}.take(20)
|
||||
}
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Text(
|
||||
"Suchen Sie den Verein in den Stammdaten oder importieren Sie eine aktuelle ZNS-Datei.",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
OutlinedTextField(
|
||||
value = searchQuery,
|
||||
onValueChange = { searchQuery = it },
|
||||
label = { Text("Verein suchen (Name oder OEBS-Nr)") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
leadingIcon = { Icon(Icons.Default.Search, null) }
|
||||
)
|
||||
|
||||
if (filteredVereine.isNotEmpty()) {
|
||||
LazyColumn(Modifier.heightIn(max = 300.dp)) {
|
||||
items(filteredVereine) { verein ->
|
||||
ListItem(
|
||||
headlineContent = { Text(verein.name) },
|
||||
supportingContent = { Text("OEBS: ${verein.oepsNummer} | ${verein.ort ?: ""}") },
|
||||
modifier = Modifier.clickable { onVereinSelected(verein.id) },
|
||||
trailingContent = {
|
||||
if (selectedVereinId == verein.id) {
|
||||
Icon(Icons.Default.CheckCircle, null, tint = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
}
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
} else if (searchQuery.isNotBlank()) {
|
||||
Text("Kein Verein gefunden.", style = MaterialTheme.typography.bodySmall, color = Color.Gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ZnsImportWizardSection(
|
||||
state = znsState,
|
||||
onFileSelect = { znsImporter.onFileSelected(it) },
|
||||
onStartImport = { znsImporter.startImport() },
|
||||
onReset = { znsImporter.reset() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ZnsImportWizardSection(
|
||||
state: ZnsImportState,
|
||||
onFileSelect: (String) -> Unit,
|
||||
onStartImport: () -> Unit,
|
||||
onReset: () -> Unit
|
||||
) {
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.4f))
|
||||
) {
|
||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Icon(Icons.Default.CloudDownload, null, tint = MaterialTheme.colorScheme.tertiary)
|
||||
Text("ZNS-Stammdaten Import (ZIP/DAT)", style = MaterialTheme.typography.titleSmall)
|
||||
}
|
||||
|
||||
if (state.jobId == null) {
|
||||
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(
|
||||
value = state.selectedFilePath ?: "",
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
placeholder = { Text("Keine Datei gewählt") },
|
||||
modifier = Modifier.weight(1f),
|
||||
singleLine = true,
|
||||
textStyle = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
Button(
|
||||
onClick = {
|
||||
val path = pickZnsFile()
|
||||
if (path != null) onFileSelect(path)
|
||||
},
|
||||
enabled = !state.isUploading
|
||||
) {
|
||||
Icon(Icons.Default.FolderOpen, null, modifier = Modifier.size(18.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Durchsuchen")
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = onStartImport,
|
||||
enabled = state.selectedFilePath != null && !state.isUploading,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
if (state.isUploading) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp)
|
||||
} else {
|
||||
Icon(Icons.Default.Bolt, null, modifier = Modifier.size(18.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Schnell-Import starten (LIGHT)")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Text(state.jobStatus ?: "Verarbeite...", style = MaterialTheme.typography.labelMedium)
|
||||
Text("${state.progress}%", style = MaterialTheme.typography.labelMedium)
|
||||
}
|
||||
LinearProgressIndicator(
|
||||
progress = { state.progress / 100f },
|
||||
modifier = Modifier.fillMaxWidth().height(8.dp).clip(RoundedCornerShape(4.dp)),
|
||||
)
|
||||
if (state.isFinished) {
|
||||
Button(onClick = onReset, modifier = Modifier.align(Alignment.End)) { Text("Neu starten") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.errorMessage != null) {
|
||||
Text(state.errorMessage!!, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.labelSmall)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user