Refactor Veranstalter and Veranstaltung flows: add VeranstalterProfil UI, event creation callback, profile enhancements, and save-enable matrix logic. Extend ZNS import and branding workflows.
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled
This commit is contained in:
+17
-1
@@ -33,6 +33,13 @@ fun TurnierDetailScreen(
|
||||
veranstaltungId: Long,
|
||||
turnierId: Long,
|
||||
onBack: () -> Unit,
|
||||
eventVon: String? = null,
|
||||
eventBis: String? = null,
|
||||
eventOrt: String? = null,
|
||||
veranstalterName: String? = null,
|
||||
veranstalterOrt: String? = null,
|
||||
veranstalterBundesland: String? = null,
|
||||
veranstalterLogoUrl: String? = null,
|
||||
) {
|
||||
var selectedTab by remember { mutableIntStateOf(0) }
|
||||
|
||||
@@ -80,7 +87,16 @@ fun TurnierDetailScreen(
|
||||
// Tab-Inhalte
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
when (selectedTab) {
|
||||
0 -> StammdatenTabContent(turnierId = turnierId)
|
||||
0 -> StammdatenTabContent(
|
||||
turnierId = turnierId,
|
||||
eventVon = eventVon,
|
||||
eventBis = eventBis,
|
||||
eventOrt = eventOrt,
|
||||
veranstalterName = veranstalterName,
|
||||
veranstalterOrt = veranstalterOrt,
|
||||
veranstalterBundesland = veranstalterBundesland,
|
||||
veranstalterLogoUrl = veranstalterLogoUrl,
|
||||
)
|
||||
1 -> OrganisationTabContent()
|
||||
2 -> BewerbeTabContent()
|
||||
3 -> ArtikelTabContent()
|
||||
|
||||
+187
-29
@@ -29,13 +29,26 @@ private val AccentBlue = Color(0xFF3B82F6)
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun StammdatenTabContent(turnierId: Long) {
|
||||
fun StammdatenTabContent(
|
||||
turnierId: Long,
|
||||
eventVon: String? = null,
|
||||
eventBis: String? = null,
|
||||
eventOrt: String? = null,
|
||||
veranstalterName: String? = null,
|
||||
veranstalterOrt: String? = null,
|
||||
veranstalterBundesland: String? = null,
|
||||
veranstalterLogoUrl: String? = null,
|
||||
) {
|
||||
// In einer echten App würden wir diese Daten aus einem ViewModel laden.
|
||||
// Hier simulieren wir den State basierend auf den Anforderungen.
|
||||
|
||||
var turnierNr by remember { mutableStateOf("") }
|
||||
var nrConfirmed by remember { mutableStateOf(false) }
|
||||
var showNrConfirm by remember { mutableStateOf(false) }
|
||||
var znsDataLoaded by remember { mutableStateOf(false) }
|
||||
var znsPayloadVersion by remember { mutableStateOf<String?>(null) }
|
||||
var znsImportedAt by remember { mutableStateOf<String?>(null) }
|
||||
val znsImportHistory = remember { mutableStateListOf<Triple<String, String, Boolean>>() } // (source, payloadVersion, ok)
|
||||
var typ by remember { mutableStateOf("ÖTO (National)") }
|
||||
|
||||
val sparten = remember { mutableStateListOf<String>() }
|
||||
@@ -48,9 +61,11 @@ fun StammdatenTabContent(turnierId: Long) {
|
||||
|
||||
var titel by remember { mutableStateOf("") }
|
||||
var subTitel by remember { mutableStateOf("") }
|
||||
var turnierLogoUrl by remember { mutableStateOf("") }
|
||||
val sponsoren = remember { mutableStateListOf<String>() }
|
||||
|
||||
var showZnsDialog by remember { mutableStateOf(false) }
|
||||
var showZnsLog by remember { mutableStateOf(false) }
|
||||
|
||||
// Hilfs-States für DatePicker
|
||||
var showDatePickerVon by remember { mutableStateOf(false) }
|
||||
@@ -79,7 +94,7 @@ fun StammdatenTabContent(turnierId: Long) {
|
||||
)
|
||||
if (!nrConfirmed) {
|
||||
Button(
|
||||
onClick = { nrConfirmed = true },
|
||||
onClick = { showNrConfirm = true },
|
||||
enabled = turnierNr.length == 5,
|
||||
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue)
|
||||
) {
|
||||
@@ -88,9 +103,9 @@ fun StammdatenTabContent(turnierId: Long) {
|
||||
} else {
|
||||
InputChip(
|
||||
selected = true,
|
||||
onClick = { nrConfirmed = false },
|
||||
onClick = { },
|
||||
label = { Text("Bestätigt") },
|
||||
trailingIcon = { Icon(Icons.Default.Edit, contentDescription = null, modifier = Modifier.size(16.dp)) }
|
||||
trailingIcon = { Icon(Icons.Default.Check, contentDescription = null, modifier = Modifier.size(16.dp)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -108,11 +123,13 @@ fun StammdatenTabContent(turnierId: Long) {
|
||||
FilterChip(
|
||||
selected = typ == "ÖTO (National)",
|
||||
onClick = { typ = "ÖTO (National)" },
|
||||
enabled = nrConfirmed,
|
||||
label = { Text("ÖTO (National)") }
|
||||
)
|
||||
FilterChip(
|
||||
selected = typ == "FEI (International)",
|
||||
onClick = { typ = "FEI (International)" },
|
||||
enabled = nrConfirmed,
|
||||
label = { Text("FEI (International)") }
|
||||
)
|
||||
}
|
||||
@@ -123,16 +140,18 @@ fun StammdatenTabContent(turnierId: Long) {
|
||||
Button(
|
||||
onClick = { showZnsDialog = true },
|
||||
colors = ButtonDefaults.buttonColors(containerColor = AccentBlue)
|
||||
, enabled = nrConfirmed
|
||||
) {
|
||||
Icon(Icons.Default.CloudDownload, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Import via Internet")
|
||||
}
|
||||
OutlinedButton(onClick = { showZnsDialog = true }) {
|
||||
OutlinedButton(onClick = { showZnsDialog = true }, enabled = nrConfirmed) {
|
||||
Icon(Icons.Default.Usb, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Import via USB")
|
||||
}
|
||||
TextButton(onClick = { showZnsLog = true }, enabled = nrConfirmed) { Text("Import-Log anzeigen…") }
|
||||
}
|
||||
|
||||
val znsStatusColor = if (znsDataLoaded) Color(0xFF2E7D32) else MaterialTheme.colorScheme.error
|
||||
@@ -149,6 +168,17 @@ fun StammdatenTabContent(turnierId: Long) {
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 13.sp
|
||||
)
|
||||
if (znsDataLoaded) {
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
listOfNotNull(
|
||||
znsPayloadVersion?.let { "Version: $it" },
|
||||
znsImportedAt?.let { "Zeit: $it" },
|
||||
).joinToString(" • "),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,11 +190,13 @@ fun StammdatenTabContent(turnierId: Long) {
|
||||
FilterChip(
|
||||
selected = sparten.contains("Dressur"),
|
||||
onClick = { if (sparten.contains("Dressur")) sparten.remove("Dressur") else sparten.add("Dressur") },
|
||||
enabled = nrConfirmed,
|
||||
label = { Text("Dressur") }
|
||||
)
|
||||
FilterChip(
|
||||
selected = sparten.contains("Springen"),
|
||||
onClick = { if (sparten.contains("Springen")) sparten.remove("Springen") else sparten.add("Springen") },
|
||||
enabled = nrConfirmed,
|
||||
label = { Text("Springen") }
|
||||
)
|
||||
}
|
||||
@@ -177,6 +209,7 @@ fun StammdatenTabContent(turnierId: Long) {
|
||||
FilterChip(
|
||||
selected = klassen.contains(k),
|
||||
onClick = { if (klassen.contains(k)) klassen.remove(k) else klassen.add(k) },
|
||||
enabled = nrConfirmed,
|
||||
label = { Text(k) }
|
||||
)
|
||||
}
|
||||
@@ -197,64 +230,112 @@ fun StammdatenTabContent(turnierId: Long) {
|
||||
if (suggested.isEmpty()) {
|
||||
Text("Bitte Sparte und Klasse wählen", color = Color.Gray, fontSize = 13.sp)
|
||||
} else {
|
||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
suggested.forEach { c ->
|
||||
InputChip(
|
||||
selected = kat.contains(c),
|
||||
onClick = { if (kat.contains(c)) kat.remove(c) else kat.add(c) },
|
||||
label = { Text(c) }
|
||||
)
|
||||
// Gruppiere nach Sparte (CDN/CSN)
|
||||
val grouped = suggested.groupBy { if (it.startsWith("CDN")) "Dressur" else "Springen" }
|
||||
grouped.forEach { (gruppe, eintraege) ->
|
||||
Text(gruppe, fontWeight = FontWeight.SemiBold, color = PrimaryBlue)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
eintraege.sorted().forEach { c ->
|
||||
InputChip(
|
||||
selected = kat.contains(c),
|
||||
onClick = { if (kat.contains(c)) kat.remove(c) else kat.add(c) },
|
||||
enabled = nrConfirmed,
|
||||
label = { Text(c) }
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FormRow("Zeitraum:") {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
val vonMod = if (nrConfirmed) Modifier.width(160.dp).clickable { showDatePickerVon = true } else Modifier.width(160.dp)
|
||||
OutlinedTextField(
|
||||
value = von,
|
||||
onValueChange = {},
|
||||
label = { Text("Von") },
|
||||
modifier = Modifier.width(160.dp).clickable { showDatePickerVon = true },
|
||||
modifier = vonMod,
|
||||
readOnly = true,
|
||||
enabled = nrConfirmed,
|
||||
trailingIcon = { Icon(Icons.Default.DateRange, null) }
|
||||
)
|
||||
Text("bis")
|
||||
val bisMod = if (nrConfirmed) Modifier.width(160.dp).clickable { showDatePickerBis = true } else Modifier.width(160.dp)
|
||||
OutlinedTextField(
|
||||
value = bis,
|
||||
onValueChange = {},
|
||||
label = { Text("Bis") },
|
||||
modifier = Modifier.width(160.dp).clickable { showDatePickerBis = true },
|
||||
modifier = bisMod,
|
||||
readOnly = true,
|
||||
enabled = nrConfirmed,
|
||||
trailingIcon = { Icon(Icons.Default.DateRange, null) }
|
||||
)
|
||||
}
|
||||
Text("Hinweis: Muss innerhalb des Veranstaltungs-Zeitraums liegen.", fontSize = 11.sp, color = Color.Gray)
|
||||
}
|
||||
|
||||
FormRow("Ort:") {
|
||||
OutlinedTextField(
|
||||
value = ort,
|
||||
onValueChange = { ort = it },
|
||||
label = { Text("Austragungsort") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
supportingText = { Text("Muss mit Veranstaltungsort übereinstimmen.") }
|
||||
)
|
||||
val rangeText = if (eventVon != null && eventBis != null) "Muss zwischen $eventVon – $eventBis liegen." else "Muss innerhalb des Veranstaltungs-Zeitraums liegen."
|
||||
Text(rangeText, fontSize = 11.sp, color = Color.Gray)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Branding (Schritt 3 Logik) ───────────────────────────────────────
|
||||
SectionCard(title = "Turnier-Branding") {
|
||||
// Default-Titel-Vorschlag: [Kategorien] [Verein-Ort] [Bundesland]
|
||||
val defaultTitle = remember(kat.size, veranstalterOrt, veranstalterBundesland) {
|
||||
val cats = if (kat.isEmpty()) "" else kat.sorted().joinToString(" ")
|
||||
listOfNotNull(cats.ifBlank { null },
|
||||
listOfNotNull(veranstalterOrt, veranstalterBundesland).filter { it.isNotBlank() }.joinToString(" ")
|
||||
.takeIf { it.isNotBlank() }
|
||||
).joinToString(" ")
|
||||
}
|
||||
OutlinedTextField(
|
||||
value = titel,
|
||||
onValueChange = { titel = it },
|
||||
label = { Text("Titel") },
|
||||
placeholder = { if (defaultTitle.isNotBlank()) Text(defaultTitle) },
|
||||
enabled = nrConfirmed,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = subTitel,
|
||||
onValueChange = { subTitel = it },
|
||||
label = { Text("Sub-Titel") },
|
||||
enabled = nrConfirmed,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
// Ort im Branding-Bereich platzieren (mit Soft-Warnung bei Abweichung zum Veranstaltungsort)
|
||||
FormRow("Ort:") {
|
||||
OutlinedTextField(
|
||||
value = ort,
|
||||
onValueChange = { ort = it },
|
||||
label = { Text("Austragungsort") },
|
||||
enabled = nrConfirmed,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
supportingText = {
|
||||
if (eventOrt != null && ort.isNotBlank() && ort.trim() != eventOrt.trim()) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(Icons.Default.Warning, contentDescription = null, tint = Color(0xFFF59E0B), modifier = Modifier.size(14.dp))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("Abweichung zum Veranstaltungsort ($eventOrt) – bitte prüfen.", color = Color(0xFFF59E0B))
|
||||
}
|
||||
} else {
|
||||
Text("Muss mit Veranstaltungsort übereinstimmen.")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Turnier-Logo mit Fallback auf Veranstalterlogo
|
||||
OutlinedTextField(
|
||||
value = turnierLogoUrl,
|
||||
onValueChange = { turnierLogoUrl = it },
|
||||
label = { Text("Turnier-Logo (URL/Pfad)") },
|
||||
enabled = nrConfirmed,
|
||||
supportingText = {
|
||||
Text("Wenn leer: verwende Veranstalter-Logo${if (!veranstalterLogoUrl.isNullOrBlank()) " ($veranstalterLogoUrl)" else ""}.")
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
@@ -264,11 +345,12 @@ fun StammdatenTabContent(turnierId: Long) {
|
||||
InputChip(
|
||||
selected = true,
|
||||
onClick = { sponsoren.remove(s) },
|
||||
enabled = nrConfirmed,
|
||||
label = { Text(s) },
|
||||
trailingIcon = { Icon(Icons.Default.Close, null, modifier = Modifier.size(14.dp)) }
|
||||
)
|
||||
}
|
||||
TextButton(onClick = { sponsoren.add("Neuer Sponsor") }) {
|
||||
TextButton(onClick = { sponsoren.add("Neuer Sponsor") }, enabled = nrConfirmed) {
|
||||
Text("+ Hinzufügen")
|
||||
}
|
||||
}
|
||||
@@ -276,10 +358,46 @@ fun StammdatenTabContent(turnierId: Long) {
|
||||
}
|
||||
|
||||
// ── Footer ──────────────────────────────────────────────────────────
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
|
||||
// Save-Enable-Matrix (kleine Checkliste)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
AssistChip(onClick = {}, label = { Text("Nr bestätigt") }, leadingIcon = {
|
||||
Icon(if (nrConfirmed) Icons.Default.Check else Icons.Default.Close, null, tint = if (nrConfirmed) Color(0xFF2E7D32) else MaterialTheme.colorScheme.error)
|
||||
})
|
||||
AssistChip(onClick = {}, label = { Text("ZNS geladen") }, leadingIcon = {
|
||||
Icon(if (znsDataLoaded) Icons.Default.Check else Icons.Default.Close, null, tint = if (znsDataLoaded) Color(0xFF2E7D32) else MaterialTheme.colorScheme.error)
|
||||
})
|
||||
val dateOk = remember(von, bis, eventVon, eventBis) {
|
||||
try {
|
||||
if (eventVon == null || eventBis == null || von.isBlank()) true else {
|
||||
val evV = LocalDate.parse(eventVon)
|
||||
val evB = LocalDate.parse(eventBis)
|
||||
val tV = LocalDate.parse(von)
|
||||
val tB = if (bis.isBlank()) tV else LocalDate.parse(bis)
|
||||
!tV.isBefore(evV) && !tB.isAfter(evB) && !tB.isBefore(tV)
|
||||
}
|
||||
} catch (e: Exception) { false }
|
||||
}
|
||||
AssistChip(onClick = {}, label = { Text("Datum gültig") }, leadingIcon = {
|
||||
Icon(if (dateOk) Icons.Default.Check else Icons.Default.Close, null, tint = if (dateOk) Color(0xFF2E7D32) else MaterialTheme.colorScheme.error)
|
||||
})
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = { /* Speichern */ },
|
||||
enabled = nrConfirmed && znsDataLoaded && kat.isNotEmpty() && von.isNotBlank() && titel.isNotBlank(),
|
||||
enabled = run {
|
||||
val base = nrConfirmed && znsDataLoaded && kat.isNotEmpty() && von.isNotBlank()
|
||||
val dateValid = try {
|
||||
if (eventVon == null || eventBis == null || von.isBlank()) true else {
|
||||
val evV = LocalDate.parse(eventVon)
|
||||
val evB = LocalDate.parse(eventBis)
|
||||
val tV = LocalDate.parse(von)
|
||||
val tB = if (bis.isBlank()) tV else LocalDate.parse(bis)
|
||||
!tV.isBefore(evV) && !tB.isAfter(evB) && !tB.isBefore(tV)
|
||||
}
|
||||
} catch (e: Exception) { false }
|
||||
base && dateValid
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue),
|
||||
modifier = Modifier.padding(bottom = 24.dp)
|
||||
) {
|
||||
@@ -297,7 +415,13 @@ fun StammdatenTabContent(turnierId: Long) {
|
||||
title = { Text("ZNS Import") },
|
||||
text = { Text("Simuliere ZNS-Stammdaten Import für Turnier #$turnierNr...") },
|
||||
confirmButton = {
|
||||
TextButton(onClick = { znsDataLoaded = true; showZnsDialog = false }) { Text("Importieren") }
|
||||
TextButton(onClick = {
|
||||
znsDataLoaded = true
|
||||
znsPayloadVersion = "v2.4"
|
||||
znsImportedAt = java.time.Instant.now().toString()
|
||||
znsImportHistory.add(Triple("Internet/USB", znsPayloadVersion!!, true))
|
||||
showZnsDialog = false
|
||||
}) { Text("Importieren") }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showZnsDialog = false }) { Text("Abbrechen") }
|
||||
@@ -305,6 +429,40 @@ fun StammdatenTabContent(turnierId: Long) {
|
||||
)
|
||||
}
|
||||
|
||||
if (showNrConfirm) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showNrConfirm = false },
|
||||
title = { Text("Turnier-Nummer bestätigen?") },
|
||||
text = { Text("Die Turnier-Nr. ist nach der Bestätigung nicht mehr änderbar.") },
|
||||
confirmButton = {
|
||||
TextButton(onClick = { nrConfirmed = true; showNrConfirm = false }) { Text("Ja, bestätigen") }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showNrConfirm = false }) { Text("Abbrechen") }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (showZnsLog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showZnsLog = false },
|
||||
title = { Text("ZNS Import-Log (letzte 5)") },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
if (znsImportHistory.isEmpty()) {
|
||||
Text("Keine Einträge vorhanden.", color = Color.Gray)
|
||||
} else {
|
||||
znsImportHistory.takeLast(5).asReversed().forEach { (src, ver, ok) ->
|
||||
val c = if (ok) Color(0xFF2E7D32) else MaterialTheme.colorScheme.error
|
||||
Text("• $src – Version $ver – ${if (ok) "OK" else "Fehler"}", color = c, fontSize = 13.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = { TextButton(onClick = { showZnsLog = false }) { Text("Schließen") } }
|
||||
)
|
||||
}
|
||||
|
||||
if (showDatePickerVon) {
|
||||
val state = rememberDatePickerState()
|
||||
DatePickerDialog(
|
||||
|
||||
Reference in New Issue
Block a user