feat(veranstaltung): UI-Refactoring und Validierung für Veranstaltungsverwaltung hinzugefügt
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
+135
-23
@@ -1,45 +1,73 @@
|
||||
package at.mocode.veranstaltung.feature.presentation
|
||||
|
||||
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.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import at.mocode.frontend.core.designsystem.models.PlaceholderContent
|
||||
import at.mocode.frontend.core.designsystem.components.MsButton
|
||||
import at.mocode.frontend.core.designsystem.components.MsTextField
|
||||
import at.mocode.frontend.core.designsystem.theme.Dimens
|
||||
|
||||
/**
|
||||
* Formular zum Anlegen einer neuen Veranstaltung (Vision_03: /veranstaltung/neu).
|
||||
* Tabs: Veranstaltung-Übersicht | Stammdaten (A-Satz) | Organisation | Preisliste
|
||||
* TODO: Echte Formular-Felder und Persistenz (Phase 4/5).
|
||||
* Tabs: Stammdaten (A-Satz) | Organisation | Preisliste
|
||||
*/
|
||||
@Composable
|
||||
fun VeranstaltungNeuScreen(
|
||||
onBack: () -> Unit,
|
||||
onSave: () -> Unit,
|
||||
) {
|
||||
var selectedTab by remember { mutableIntStateOf(1) } // Stammdaten ist Standard-Tab
|
||||
val tabs = listOf("Übersicht", "Stammdaten (A-Satz)", "Organisation", "Preisliste")
|
||||
var selectedTab by remember { mutableIntStateOf(0) } // Stammdaten ist Standard-Tab
|
||||
val tabs = listOf("Stammdaten (A-Satz)", "Organisation", "Preisliste")
|
||||
|
||||
// Formular-State für Stammdaten
|
||||
var name by remember { mutableStateOf("") }
|
||||
var ort by remember { mutableStateOf("") }
|
||||
var startDatum by remember { mutableStateOf("") }
|
||||
var endDatum by remember { mutableStateOf("") }
|
||||
var veranstalter by remember { mutableStateOf("") }
|
||||
|
||||
// Validierung
|
||||
val isNameValid = name.isNotBlank()
|
||||
val isOrtValid = ort.isNotBlank()
|
||||
val isStartDatumValid = startDatum.length >= 8 // Einfache Prüfung für DD.MM.YY
|
||||
val isFormValid = isNameValid && isOrtValid && isStartDatumValid
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
// Toolbar
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
// Toolbar (Header)
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shadowElevation = 2.dp,
|
||||
color = MaterialTheme.colorScheme.surface
|
||||
) {
|
||||
Row {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück")
|
||||
Row(
|
||||
modifier = Modifier.padding(Dimens.SpacingM),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
|
||||
) {
|
||||
Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically) {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück")
|
||||
}
|
||||
Spacer(Modifier.width(Dimens.SpacingS))
|
||||
Text(
|
||||
text = "Neue Veranstaltung anlegen",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "Neue Veranstaltung",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
modifier = Modifier.alignByBaseline(),
|
||||
MsButton(
|
||||
text = "Veranstaltung speichern",
|
||||
onClick = onSave,
|
||||
enabled = isFormValid
|
||||
)
|
||||
}
|
||||
Button(onClick = onSave) { Text("Speichern") }
|
||||
}
|
||||
|
||||
PrimaryTabRow(selectedTabIndex = selectedTab) {
|
||||
@@ -52,12 +80,96 @@ fun VeranstaltungNeuScreen(
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize().padding(24.dp)) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(Dimens.SpacingL)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
when (selectedTab) {
|
||||
0 -> PlaceholderContent("Veranstaltung – Übersicht", "Wird nach dem Speichern befüllt.")
|
||||
1 -> PlaceholderContent("Stammdaten (A-Satz)", "Felder: Bezeichnung, Datum, Ort, Veranstalter …")
|
||||
2 -> PlaceholderContent("Organisation", "Felder: Richter, Parcourschef, Tierarzt …")
|
||||
3 -> PlaceholderContent("Preisliste", "Nenngebühren pro Bewerb/Sparte …")
|
||||
0 -> { // Stammdaten
|
||||
Column(
|
||||
modifier = Modifier.widthIn(max = 600.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(Dimens.SpacingM)
|
||||
) {
|
||||
Text(
|
||||
"Allgemeine Informationen (A-Satz)",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
MsTextField(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
label = "Name der Veranstaltung (z.B. Pfingstturnier)",
|
||||
placeholder = "Name eingeben",
|
||||
isError = name.isBlank(),
|
||||
errorMessage = if (name.isBlank()) "Name ist erforderlich" else null
|
||||
)
|
||||
|
||||
MsTextField(
|
||||
value = veranstalter,
|
||||
onValueChange = { veranstalter = it },
|
||||
label = "Veranstalter (Verein)",
|
||||
placeholder = "Verein suchen oder eingeben"
|
||||
)
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingM)) {
|
||||
MsTextField(
|
||||
value = ort,
|
||||
onValueChange = { ort = it },
|
||||
label = "Ort",
|
||||
modifier = Modifier.weight(1f),
|
||||
isError = ort.isBlank()
|
||||
)
|
||||
MsTextField(
|
||||
value = "Österreich",
|
||||
onValueChange = {},
|
||||
label = "Land",
|
||||
modifier = Modifier.weight(0.5f),
|
||||
readOnly = true
|
||||
)
|
||||
}
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingM)) {
|
||||
MsTextField(
|
||||
value = startDatum,
|
||||
onValueChange = { startDatum = it },
|
||||
label = "Startdatum (TT.MM.JJJJ)",
|
||||
placeholder = "24.04.2026",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
MsTextField(
|
||||
value = endDatum,
|
||||
onValueChange = { endDatum = it },
|
||||
label = "Enddatum (TT.MM.JJJJ)",
|
||||
placeholder = "26.04.2026",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
"Hinweis: Diese Daten bilden die Basis für den ZNS-Import und die Abrechnung.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
1 -> { // Organisation
|
||||
Column(verticalArrangement = Arrangement.spacedBy(Dimens.SpacingM)) {
|
||||
Text("Funktionäre & Verantwortliche", style = MaterialTheme.typography.titleLarge)
|
||||
Text("Hier werden Richter, Parcourschefs und Tierärzte zugewiesen.")
|
||||
// Später: MsSearchableSelect für Funktionäre
|
||||
}
|
||||
}
|
||||
|
||||
2 -> { // Preisliste
|
||||
Column(verticalArrangement = Arrangement.spacedBy(Dimens.SpacingM)) {
|
||||
Text("Gebühren & Preisliste", style = MaterialTheme.typography.titleLarge)
|
||||
Text("Definition der Nenngebühren und Pauschalen gemäß ÖTO.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+139
-19
@@ -1,51 +1,171 @@
|
||||
package at.mocode.veranstaltung.feature.presentation
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
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.Add
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material.icons.filled.Event
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import at.mocode.frontend.core.designsystem.models.PlaceholderContent
|
||||
import at.mocode.frontend.core.designsystem.components.MsButton
|
||||
import at.mocode.frontend.core.designsystem.components.MsCard
|
||||
import at.mocode.frontend.core.designsystem.theme.Dimens
|
||||
|
||||
/**
|
||||
* UI-Modell für die Anzeige einer Veranstaltung in der Liste.
|
||||
*/
|
||||
data class VeranstaltungSimpleUiModel(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val untertitel: String?,
|
||||
val ort: String,
|
||||
val datum: String,
|
||||
val logoUrl: String? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Veranstaltungs-Übersicht (Drawer-Einstieg gemäß Vision_03).
|
||||
* Zeigt Liste aller Veranstaltungen + Button "Neue Veranstaltung".
|
||||
* TODO: Echte Daten aus dem event-management-context laden (Phase 4/5).
|
||||
*/
|
||||
@Composable
|
||||
fun VeranstaltungenScreen(
|
||||
onVeranstaltungNeu: () -> Unit,
|
||||
onVeranstaltungOeffnen: (Long) -> Unit,
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxSize().padding(24.dp)) {
|
||||
// Später: Echte Daten aus dem ViewModel laden
|
||||
val veranstaltungen = remember {
|
||||
mutableStateListOf(
|
||||
VeranstaltungSimpleUiModel(
|
||||
id = 1L,
|
||||
name = "Springturnier Neumarkt",
|
||||
untertitel = "CSN-B* | 24. - 26. April 2026",
|
||||
ort = "Neumarkt am Wallersee",
|
||||
datum = "24.04.2026 - 26.04.2026"
|
||||
),
|
||||
VeranstaltungSimpleUiModel(
|
||||
id = 2L,
|
||||
name = "Dressurtage Lamprechtshausen",
|
||||
untertitel = "CDN-A* | 01. - 03. Mai 2026",
|
||||
ort = "Lamprechtshausen",
|
||||
datum = "01.05.2026 - 03.05.2026"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize().padding(Dimens.SpacingL)) {
|
||||
// Header: Titel + Action
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Veranstaltungen",
|
||||
text = "Veranstaltungen - verwalten",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
MsButton(
|
||||
text = "Neue Veranstaltung",
|
||||
onClick = onVeranstaltungNeu
|
||||
// icon = Icons.Default.Add // MsButton unterstützt noch kein Icon im Parameter
|
||||
)
|
||||
Button(onClick = onVeranstaltungNeu) {
|
||||
Icon(Icons.Default.Add, contentDescription = null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Neue Veranstaltung")
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Spacer(Modifier.height(Dimens.SpacingL))
|
||||
|
||||
// Platzhalter – wird durch echte Daten ersetzt
|
||||
PlaceholderContent(
|
||||
title = "Noch keine Veranstaltungen",
|
||||
subtitle = "Lege eine neue Veranstaltung an, um zu beginnen.",
|
||||
)
|
||||
if (veranstaltungen.isEmpty()) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
"Keine Veranstaltungen gefunden. Legen Sie eine neue an.",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(Dimens.SpacingM)
|
||||
) {
|
||||
items(veranstaltungen) { event ->
|
||||
VeranstaltungCard(
|
||||
event = event,
|
||||
onDoubleClick = { onVeranstaltungOeffnen(event.id) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun VeranstaltungCard(
|
||||
event: VeranstaltungSimpleUiModel,
|
||||
onDoubleClick: () -> Unit
|
||||
) {
|
||||
MsCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.combinedClickable(
|
||||
onClick = { /* Einfacher Klick für Selektion, falls gewünscht */ },
|
||||
onDoubleClick = onDoubleClick
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(Dimens.SpacingS),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Platzhalter für Logo
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(64.dp)
|
||||
.clip(MaterialTheme.shapes.small)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Image(
|
||||
imageVector = Icons.Default.Event,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(32.dp),
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.width(Dimens.SpacingM))
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = event.name,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
if (!event.untertitel.isNullOrBlank()) {
|
||||
Text(
|
||||
text = event.untertitel,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "${event.ort} | ${event.datum}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ package at.mocode.desktop.screens.onboarding
|
||||
/**
|
||||
* Validierungslogik für den Onboarding-Wizard.
|
||||
*
|
||||
* Extrahiert aus [OnboardingScreen] für isolierte Unit-Tests (B-2).
|
||||
* Extrahiert aus `OnboardingScreen` für isolierte Unit-Tests (B-2).
|
||||
* Regeln gemäß Onboarding-Spezifikation:
|
||||
* - Gerätename: mindestens 3 Zeichen (nach trim)
|
||||
* - Sicherheitsschlüssel: mindestens 8 Zeichen (nach trim)
|
||||
|
||||
+1
-39
@@ -57,51 +57,13 @@ fun VeranstaltungVerwaltungV2(
|
||||
}
|
||||
|
||||
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
// Navigation Toolbar (Top)
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
AssistChip(
|
||||
onClick = onNavigateToPferde,
|
||||
label = { Text("Pferde") },
|
||||
leadingIcon = { Icon(Icons.Default.Pets, null) })
|
||||
AssistChip(
|
||||
onClick = onNavigateToReiter,
|
||||
label = { Text("Reiter") },
|
||||
leadingIcon = { Icon(Icons.Default.Person, null) })
|
||||
AssistChip(
|
||||
onClick = onNavigateToVereine,
|
||||
label = { Text("Vereine") },
|
||||
leadingIcon = { Icon(Icons.Default.Home, null) })
|
||||
AssistChip(
|
||||
onClick = onNavigateToFunktionaere,
|
||||
label = { Text("Funktionäre") },
|
||||
leadingIcon = { Icon(Icons.Default.Badge, null) })
|
||||
AssistChip(
|
||||
onClick = onNavigateToVeranstalter,
|
||||
label = { Text("Veranstalter") },
|
||||
leadingIcon = { Icon(Icons.Default.Business, null) })
|
||||
VerticalDivider(Modifier.height(32.dp).padding(horizontal = 4.dp))
|
||||
AssistChip(
|
||||
onClick = onNavigateToZnsImport,
|
||||
label = { Text("ZNS Importer") },
|
||||
leadingIcon = { Icon(Icons.Default.CloudDownload, null) },
|
||||
colors = AssistChipDefaults.assistChipColors(
|
||||
labelColor = MaterialTheme.colorScheme.primary,
|
||||
leadingIconContentColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Header
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text("Veranstaltung-Verwaltung", style = MaterialTheme.typography.headlineMedium)
|
||||
Text("Veranstaltungen - verwalten", style = MaterialTheme.typography.headlineMedium)
|
||||
Button(onClick = onNewVeranstaltung) {
|
||||
Icon(Icons.Default.Add, contentDescription = null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
|
||||
Reference in New Issue
Block a user