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

This commit is contained in:
2026-04-01 02:49:22 +02:00
parent f44b2c8126
commit 09debdef86
14 changed files with 1163 additions and 43 deletions
@@ -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()
@@ -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(
@@ -5,10 +5,17 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Chat
import androidx.compose.material.icons.filled.Devices
import androidx.compose.material.icons.filled.Wifi
import androidx.compose.material.icons.filled.WifiOff
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -57,11 +64,14 @@ fun DesktopMainLayout(
onNavigate = onNavigate,
onLogout = onLogout,
)
Box(modifier = Modifier.fillMaxSize()) {
DesktopContentArea(
currentScreen = currentScreen,
onNavigate = onNavigate,
)
Column(modifier = Modifier.fillMaxSize()) {
Box(modifier = Modifier.weight(1f).fillMaxWidth()) {
DesktopContentArea(
currentScreen = currentScreen,
onNavigate = onNavigate,
)
}
DesktopFooterBar()
}
}
}
@@ -259,6 +269,20 @@ private fun DesktopTopBar(
}
}
// Hilfsfunktion: OEPS-Bundeslandcode → Abkürzung
private fun mapOepsToBundesland(code: String): String = when (code.uppercase()) {
"OOE" -> ""
"NOE" -> ""
"ST" -> "Stmk."
"W" -> "Wien"
"BGLD", "B" -> "Bgld."
"K" -> "Ktn."
"S" -> "Sbg."
"T" -> "Tirol"
"V" -> "Vbg."
else -> code
}
@Composable
private fun BreadcrumbSeparator() {
Text(
@@ -344,7 +368,8 @@ private fun DesktopContentArea(
if (vId == 0L) onNavigate(AppScreen.Veranstaltungen)
else onNavigate(AppScreen.VeranstalterDetail(vId))
},
onSaved = { evtId, finalVId -> onNavigate(AppScreen.VeranstaltungUebersicht(finalVId, evtId)) }
onSaved = { evtId, finalVId -> onNavigate(AppScreen.VeranstaltungUebersicht(finalVId, evtId)) },
onVeranstalterCreated = { newVId -> onNavigate(AppScreen.VeranstalterDetail(newVId)) }
)
}
is AppScreen.VeranstaltungUebersicht -> {
@@ -365,7 +390,20 @@ private fun DesktopContentArea(
veranstalterId = vId,
veranstaltungId = evtId,
onBack = { onNavigate(AppScreen.VeranstalterDetail(vId)) },
onTurnierNeu = { onNavigate(AppScreen.TurnierNeu(evtId)) },
onTurnierNeu = {
val veranstaltung = at.mocode.desktop.v2.StoreV2.eventsFor(vId).firstOrNull { it.id == evtId }
val list = at.mocode.desktop.v2.TurnierStoreV2.list(evtId)
val newId = (list.maxOfOrNull { it.id } ?: 0L) + 1L
val draft = at.mocode.desktop.v2.TurnierV2(
id = newId,
veranstaltungId = evtId,
turnierNr = 0,
datumVon = veranstaltung?.datumVon ?: "",
datumBis = veranstaltung?.datumBis,
)
at.mocode.desktop.v2.TurnierStoreV2.add(evtId, draft)
onNavigate(AppScreen.TurnierDetail(evtId, newId))
},
onTurnierOpen = { tId -> onNavigate(AppScreen.TurnierDetail(evtId, tId)) },
)
}
@@ -375,7 +413,23 @@ private fun DesktopContentArea(
is AppScreen.VeranstaltungDetail -> VeranstaltungDetailScreen(
veranstaltungId = currentScreen.id,
onBack = { onNavigate(AppScreen.Veranstaltungen) },
onTurnierNeu = { onNavigate(AppScreen.TurnierNeu(currentScreen.id)) },
onTurnierNeu = {
val v = at.mocode.desktop.v2.StoreV2.vereine.firstOrNull { vv ->
at.mocode.desktop.v2.StoreV2.eventsFor(vv.id).any { it.id == currentScreen.id }
}
val veranstaltung = v?.let { at.mocode.desktop.v2.StoreV2.eventsFor(it.id).firstOrNull { e -> e.id == currentScreen.id } }
val list = at.mocode.desktop.v2.TurnierStoreV2.list(currentScreen.id)
val newId = (list.maxOfOrNull { it.id } ?: 0L) + 1L
val draft = at.mocode.desktop.v2.TurnierV2(
id = newId,
veranstaltungId = currentScreen.id,
turnierNr = 0,
datumVon = veranstaltung?.datumVon ?: "",
datumBis = veranstaltung?.datumBis,
)
at.mocode.desktop.v2.TurnierStoreV2.add(currentScreen.id, draft)
onNavigate(AppScreen.TurnierDetail(currentScreen.id, newId))
},
onTurnierOeffnen = { tid -> onNavigate(AppScreen.TurnierDetail(currentScreen.id, tid)) },
)
is AppScreen.VeranstaltungNeu -> VeranstaltungNeuScreen(
@@ -395,10 +449,20 @@ private fun DesktopContentArea(
onBack = { onNavigate(AppScreen.Veranstaltungen) }
)
} else {
val veranstaltung = at.mocode.desktop.v2.StoreV2.eventsFor(parent.id).firstOrNull { it.id == evtId }
val blCode = parent.oepsNummer.split("-").getOrNull(1) ?: ""
val bundesland = mapOepsToBundesland(blCode)
TurnierDetailScreen(
veranstaltungId = evtId,
turnierId = currentScreen.turnierId,
onBack = { onNavigate(AppScreen.VeranstaltungUebersicht(parent.id, evtId)) },
eventVon = veranstaltung?.datumVon,
eventBis = veranstaltung?.datumBis,
eventOrt = veranstaltung?.ort,
veranstalterName = parent.name,
veranstalterOrt = parent.ort,
veranstalterBundesland = bundesland,
veranstalterLogoUrl = veranstaltung?.logoUrl,
)
}
}
@@ -455,3 +519,48 @@ private fun DesktopContentArea(
)
}
}
@Composable
private fun DesktopFooterBar() {
// Stub-Status für MVP
val online = remember { mutableStateOf(true) }
val deviceConnected = remember { mutableStateOf(true) }
val deviceName = "Richter-Turm"
Row(
modifier = Modifier
.fillMaxWidth()
.height(36.dp)
.background(Color(0xFFF3F4F6))
.padding(horizontal = 12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = if (online.value) Icons.Filled.Wifi else Icons.Filled.WifiOff,
contentDescription = null,
tint = if (online.value) Color(0xFF059669) else Color(0xFFDC2626)
)
Spacer(Modifier.width(6.dp))
Text(if (online.value) "Online" else "Offline", color = Color(0xFF374151), fontSize = 12.sp)
Spacer(Modifier.width(16.dp))
Icon(Icons.Filled.Devices, contentDescription = null, tint = if (deviceConnected.value) Color(0xFF2563EB) else Color(0xFF9CA3AF))
Spacer(Modifier.width(6.dp))
Text(
if (deviceConnected.value) "Verbunden: $deviceName" else "Kein Gerät verbunden",
color = Color(0xFF374151),
fontSize = 12.sp
)
}
Row(verticalAlignment = Alignment.CenterVertically) {
if (deviceConnected.value) {
OutlinedButton(onClick = { /* öffne Chat-Panel */ }, contentPadding = PaddingValues(horizontal = 10.dp, vertical = 4.dp)) {
Icon(Icons.Filled.Chat, contentDescription = null, tint = Color(0xFF2563EB))
Spacer(Modifier.width(6.dp))
Text("Chat", color = Color(0xFF2563EB), fontSize = 12.sp)
}
}
}
}
}
@@ -125,6 +125,57 @@ fun VeranstalterDetailV2(
Button(onClick = onNeuVeranstaltung) { Text("+ Neue Veranstaltung") }
}
// Profil-Bereich (Logo URL, Ansprechpartner, Kontakt, Adresse)
val verein = remember(veranstalterId) { StoreV2.vereine.firstOrNull { it.id == veranstalterId } }
if (verein != null) {
Card {
Column(Modifier.fillMaxWidth().padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("VeranstalterProfil", style = MaterialTheme.typography.titleMedium)
OutlinedTextField(
value = verein.logoUrl ?: "",
onValueChange = { verein.logoUrl = it.ifBlank { null } },
label = { Text("LogoURL (optional)") },
modifier = Modifier.fillMaxWidth()
)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(
value = verein.ansprechpartner ?: "",
onValueChange = { verein.ansprechpartner = it.ifBlank { null } },
label = { Text("Ansprechpartner (optional)") },
modifier = Modifier.weight(1f)
)
OutlinedTextField(
value = verein.telefon ?: "",
onValueChange = { verein.telefon = it.ifBlank { null } },
label = { Text("Telefon (optional)") },
modifier = Modifier.weight(1f)
)
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(
value = verein.email ?: "",
onValueChange = { verein.email = it.ifBlank { null } },
label = { Text("EMail (optional)") },
modifier = Modifier.weight(1f)
)
OutlinedTextField(
value = verein.oepsNummer,
onValueChange = { verein.oepsNummer = it },
label = { Text("OEPSNummer") },
modifier = Modifier.weight(1f)
)
}
OutlinedTextField(
value = verein.adresse ?: "",
onValueChange = { verein.adresse = it.ifBlank { null } },
label = { Text("Adresse (optional)") },
modifier = Modifier.fillMaxWidth(),
minLines = 2
)
}
}
}
val events = StoreV2.eventsFor(veranstalterId)
if (events.isEmpty()) Text("Noch keine Veranstaltungen angelegt.", color = Color(0xFF6B7280))
@@ -5,9 +5,15 @@ import androidx.compose.runtime.snapshots.SnapshotStateList
data class Verein(
val id: Long,
val name: String,
val oepsNummer: String,
val ort: String,
var name: String,
var oepsNummer: String,
var ort: String,
// Profil-Felder (minimal laut Abstimmung)
var logoUrl: String? = null,
var ansprechpartner: String? = null,
var email: String? = null,
var telefon: String? = null,
var adresse: String? = null,
)
data class VeranstaltungV2(
@@ -297,6 +297,7 @@ fun VeranstaltungKonfigV2(
veranstalterId: Long = 0,
onBack: () -> Unit,
onSaved: (Long, Long) -> Unit, // eventId, veranstalterId
onVeranstalterCreated: (Long) -> Unit = {}, // Neuer Flow: nach Vereinsanlage ins Profil
) {
DesktopThemeV2 {
var currentStep by remember { mutableStateOf(if (veranstalterId == 0L) 1 else 2) }
@@ -466,9 +467,9 @@ fun VeranstaltungKonfigV2(
VeranstalterAnlegenWizard(
onCancel = { showVereinNeu = false },
onVereinCreated = { newId ->
selectedVereinId = newId
// Neuer gewünschter Flow: nach Schritt 2 ins VeranstalterProfil wechseln
showVereinNeu = false
currentStep = 2
onVeranstalterCreated(newId)
}
)
}