chore: remove obsolete screens from meldestelle-desktop module
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Failing after 2m56s
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Failing after 3m3s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Failing after 2m49s
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Successful in 2m13s

- Deleted unused screens including `AdminUebersichtScreen`, `AktorScreens`, `StammdatenImportScreen`, `TurnierDetailScreen`, and supporting components such as `PlaceholderContent`.
- Cleaned up references and placeholders to streamline module structure.

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
2026-03-26 15:08:38 +01:00
parent 1d393fdefe
commit c2b3b5889f
50 changed files with 5067 additions and 1016 deletions
@@ -10,6 +10,7 @@ plugins {
alias(libs.plugins.composeCompiler)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.kotlinSerialization)
id("org.jetbrains.compose.hot-reload")
}
kotlin {
@@ -30,6 +31,9 @@ kotlin {
implementation(projects.frontend.features.nennungFeature)
implementation(projects.frontend.features.pingFeature)
implementation(projects.frontend.features.znsImportFeature)
implementation(projects.frontend.features.veranstalterFeature)
implementation(projects.frontend.features.veranstaltungFeature)
implementation(projects.frontend.features.turnierFeature)
// Compose Desktop
implementation(compose.desktop.currentOs)
@@ -39,6 +43,7 @@ kotlin {
implementation(compose.ui)
implementation(compose.materialIconsExtended)
implementation(compose.uiTooling)
implementation(libs.composeHotReloadApi)
// DI (Koin)
implementation(libs.koin.core)
@@ -59,6 +64,7 @@ kotlin {
}
}
compose.desktop {
application {
mainClass = "at.mocode.desktop.MainKt"
@@ -9,7 +9,7 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import at.mocode.desktop.navigation.DesktopNavigationPort
import at.mocode.desktop.screens.DesktopMainLayout
import at.mocode.desktop.screens.layout.DesktopMainLayout
import at.mocode.frontend.core.auth.data.AuthTokenManager
import at.mocode.frontend.core.auth.presentation.LoginScreen
import at.mocode.frontend.core.auth.presentation.LoginViewModel
@@ -0,0 +1,50 @@
package at.mocode.desktop
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.window.singleWindowApplication
import at.mocode.turnier.feature.presentation.TurnierDetailScreen
import at.mocode.veranstalter.feature.presentation.VeranstalterAuswahlScreen
import at.mocode.veranstalter.feature.presentation.VeranstalterDetailScreen
import at.mocode.veranstalter.feature.presentation.VeranstalterNeuScreen
import at.mocode.veranstaltung.feature.presentation.AdminUebersichtScreen
import at.mocode.veranstaltung.feature.presentation.VeranstaltungUebersichtScreen
/**
* Hot-Reload Preview Entry Point
*
* Starten mit:
* ./gradlew :frontend:shells:meldestelle-desktop:hotRunJvm
*
* Einfach den gewünschten Screen in `PreviewContent()` eintragen,
* Gradle-Task starten Änderungen werden live ohne Neustart übernommen.
* Bei singleWindowApplication ist kein DevelopmentEntryPoint-Wrapper nötig.
*/
fun main() = singleWindowApplication(title = "🔥 Hot-Reload Preview") {
PreviewContent()
}
@Preview
@Composable
private fun PreviewContent() {
MaterialTheme {
Surface {
// ── Hier den gewünschten Screen eintragen ──────────────────────
// VeranstalterAuswahlScreen(onVeranstalterSelected = {}, onNeuerVeranstalter = {})
// VeranstalterNeuScreen(onBack = {}, onSave = {})
// VeranstalterDetailScreen(veranstalterId = 1L, onBack = {}, onVeranstaltungSelected = {})
// VeranstaltungUebersichtScreen(veranstalterId = 1L, onBack = {}, onTurnierSelected = {})
// TurnierDetailScreen(turnierId = 1L, onBack = {})
// ──────────────────────────────────────────────────────────────
// Standard: AdminUebersichtScreen (Startseite nach Login)
AdminUebersichtScreen(
onVeranstalterAuswahl = {},
onVeranstaltungOeffnen = {},
onPingService = {}
)
}
}
}
@@ -1,348 +0,0 @@
package at.mocode.desktop.screens
import androidx.compose.foundation.BorderStroke
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.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.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
// Status-Farben gemäß Vision_03
private val StatusVorbereitung = Color(0xFFEA580C) // Orange
private val StatusLive = Color(0xFF16A34A) // Grün
private val StatusAbgeschlossen = Color(0xFF6B7280) // Grau
/**
* Root-Screen der Desktop-App gemäß Vision_03 (Screenshot 23/24).
*
* Layout:
* - KPI-Kacheln oben (Live/Aktiv, In Vorbereitung, Gesamt, Archiv)
* - Toolbar: "+ Neue Veranstaltung" + Suche + Status-Filter
* - Veranstaltungs-Cards (expandiert mit Turnier-Liste)
*
* TODO: Echte Daten aus event-management-context laden (Phase 4/5).
*/
@Composable
fun AdminUebersichtScreen(
onVeranstalterAuswahl: () -> Unit,
onVeranstaltungOeffnen: (Long) -> Unit,
) {
// Placeholder-Daten für die UI-Struktur
val veranstaltungen = listOf<VeranstaltungUiModel>() // leer bis Backend angebunden
Column(modifier = Modifier.fillMaxSize()) {
// KPI-Kacheln
KpiKachelRow(
liveAktiv = 0,
inVorbereitung = 0,
gesamt = 0,
archiv = 0,
)
// Toolbar
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Button(
onClick = onVeranstalterAuswahl,
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1E3A8A)),
) {
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(16.dp))
Spacer(Modifier.width(4.dp))
Text("Neue Veranstaltung")
}
OutlinedTextField(
value = "",
onValueChange = {},
placeholder = { Text("Suche nach Name, Ort oder Turnier-Nr.", fontSize = 13.sp) },
modifier = Modifier.weight(1f).height(48.dp),
singleLine = true,
)
// Status-Filter Chips
StatusFilterChip("Alle", selected = true)
StatusFilterChip("Vorbereitung", selected = false)
StatusFilterChip("Live", selected = false)
StatusFilterChip("Abgeschlossen", selected = false)
}
HorizontalDivider()
// Veranstaltungs-Liste
if (veranstaltungen.isEmpty()) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = "Noch keine Veranstaltungen",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(Modifier.height(8.dp))
Text(
text = "Lege eine neue Veranstaltung an, um zu beginnen.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(Modifier.height(16.dp))
Button(
onClick = onVeranstalterAuswahl,
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1E3A8A)),
) {
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(16.dp))
Spacer(Modifier.width(4.dp))
Text("Neue Veranstaltung anlegen")
}
}
}
} else {
LazyColumn(
modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(vertical = 8.dp),
) {
items(veranstaltungen) { veranstaltung ->
VeranstaltungCard(
veranstaltung = veranstaltung,
onOeffnen = { onVeranstaltungOeffnen(veranstaltung.id) },
onLoeschen = { /* TODO */ },
)
}
}
}
}
}
@Composable
private fun KpiKachelRow(
liveAktiv: Int,
inVorbereitung: Int,
gesamt: Int,
archiv: Int,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
KpiKachel(
label = "LIVE / AKTIV",
wert = liveAktiv.toString(),
akzentFarbe = StatusLive,
modifier = Modifier.weight(1f),
)
KpiKachel(
label = "IN VORBEREITUNG",
wert = inVorbereitung.toString(),
akzentFarbe = Color(0xFF3B82F6),
modifier = Modifier.weight(1f),
)
KpiKachel(
label = "GESAMT",
wert = gesamt.toString(),
akzentFarbe = Color(0xFF6B7280),
modifier = Modifier.weight(1f),
)
KpiKachel(
label = "ARCHIV",
wert = archiv.toString(),
akzentFarbe = Color(0xFF9CA3AF),
modifier = Modifier.weight(1f),
)
}
}
@Composable
private fun KpiKachel(
label: String,
wert: String,
akzentFarbe: Color,
modifier: Modifier = Modifier,
) {
Card(
modifier = modifier,
border = BorderStroke(2.dp, akzentFarbe),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
) {
Column(modifier = Modifier.padding(12.dp)) {
Text(
text = label,
fontSize = 10.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontWeight = FontWeight.Medium,
)
Text(
text = wert,
fontSize = 28.sp,
fontWeight = FontWeight.Bold,
color = akzentFarbe,
)
}
}
}
@Composable
private fun StatusFilterChip(label: String, selected: Boolean) {
FilterChip(
selected = selected,
onClick = {},
label = { Text(label, fontSize = 12.sp) },
)
}
@Composable
private fun VeranstaltungCard(
veranstaltung: VeranstaltungUiModel,
onOeffnen: () -> Unit,
onLoeschen: () -> Unit,
) {
Card(
modifier = Modifier.fillMaxWidth(),
border = if (veranstaltung.status == VeranstaltungStatus.VORBEREITUNG)
BorderStroke(1.dp, Color(0xFF3B82F6)) else null,
) {
Column(modifier = Modifier.padding(16.dp)) {
// Header
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Top,
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = veranstaltung.name,
fontWeight = FontWeight.SemiBold,
fontSize = 15.sp,
)
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier.padding(top = 2.dp),
) {
Text("📍 ${veranstaltung.ort}", fontSize = 12.sp, color = Color(0xFF6B7280))
Text("📅 ${veranstaltung.datum}", fontSize = 12.sp, color = Color(0xFF6B7280))
Text("🏆 ${veranstaltung.turnierAnzahl} Turniere", fontSize = 12.sp, color = Color(0xFF6B7280))
}
}
StatusBadge(veranstaltung.status)
}
// Turnier-Liste
if (veranstaltung.turniere.isNotEmpty()) {
Spacer(Modifier.height(8.dp))
Text("Turniere (${veranstaltung.turniere.size}):", fontSize = 12.sp, fontWeight = FontWeight.Medium)
veranstaltung.turniere.forEach { turnier ->
Row(
modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Surface(
shape = MaterialTheme.shapes.small,
color = Color(0xFF1E3A8A),
) {
Text(
text = turnier.nummer.toString(),
color = Color.White,
fontSize = 11.sp,
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
)
}
Text("${turnier.name} (${turnier.bewerbAnzahl} Bewerbe)", fontSize = 12.sp)
}
OutlinedButton(onClick = onOeffnen, modifier = Modifier.height(28.dp)) {
Text("Öffnen", fontSize = 11.sp)
}
}
}
}
// Footer
Spacer(Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Text("Nennungen: ${veranstaltung.nennungen}", fontSize = 12.sp, color = Color(0xFF6B7280))
Text("Letzte Aktivität: ${veranstaltung.letzteAktivitaet}", fontSize = 12.sp, color = Color(0xFF6B7280))
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(
onClick = onOeffnen,
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1E3A8A)),
modifier = Modifier.height(32.dp),
) {
Text("Öffnen", fontSize = 12.sp)
}
IconButton(onClick = onLoeschen, modifier = Modifier.size(32.dp)) {
Icon(Icons.Default.Delete, contentDescription = "Löschen", tint = Color(0xFFDC2626))
}
}
}
}
}
}
@Composable
private fun StatusBadge(status: VeranstaltungStatus) {
val (text, color) = when (status) {
VeranstaltungStatus.VORBEREITUNG -> "Vorbereitung" to StatusVorbereitung
VeranstaltungStatus.LIVE -> "Live" to StatusLive
VeranstaltungStatus.ABGESCHLOSSEN -> "Abgeschlossen" to StatusAbgeschlossen
}
Surface(
shape = MaterialTheme.shapes.small,
color = color.copy(alpha = 0.15f),
border = BorderStroke(1.dp, color),
) {
Text(
text = text,
color = color,
fontSize = 11.sp,
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp),
)
}
}
// --- UI-Modelle (Placeholder bis echte Domain-Modelle angebunden sind) ---
data class VeranstaltungUiModel(
val id: Long,
val name: String,
val ort: String,
val datum: String,
val turnierAnzahl: Int,
val nennungen: Int,
val letzteAktivitaet: String,
val status: VeranstaltungStatus,
val turniere: List<TurnierUiModel> = emptyList(),
)
data class TurnierUiModel(
val id: Long,
val nummer: Long,
val name: String,
val bewerbAnzahl: Int,
)
enum class VeranstaltungStatus { VORBEREITUNG, LIVE, ABGESCHLOSSEN }
@@ -1,72 +0,0 @@
package at.mocode.desktop.screens
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
/**
* Placeholder-Screens für Akteur-Verwaltung (actor-context).
* Werden in Phase 4/5 mit echten Daten aus dem actor-context befüllt.
*/
@Composable
fun ReiterScreen() {
Column(modifier = Modifier.fillMaxSize().padding(24.dp)) {
Text("Reiter", style = MaterialTheme.typography.headlineMedium)
Spacer(Modifier.height(24.dp))
PlaceholderContent(
title = "Reiter-Verwaltung",
subtitle = "Satznummer, Lizenzklasse, Sparten-Lizenz actor-context (Phase 4).",
)
}
}
@Composable
fun PferdeScreen() {
Column(modifier = Modifier.fillMaxSize().padding(24.dp)) {
Text("Pferde", style = MaterialTheme.typography.headlineMedium)
Spacer(Modifier.height(24.dp))
PlaceholderContent(
title = "Pferde-Verwaltung",
subtitle = "Lebensnummer, ZNS-Daten, Passbesitzer actor-context (Phase 4).",
)
}
}
@Composable
fun FunktionaereScreen() {
Column(modifier = Modifier.fillMaxSize().padding(24.dp)) {
Text("Funktionäre", style = MaterialTheme.typography.headlineMedium)
Spacer(Modifier.height(24.dp))
PlaceholderContent(
title = "Funktionäre-Verwaltung",
subtitle = "Richter, Parcourschef, Tierarzt actor-context (Phase 4).",
)
}
}
@Composable
fun MeisterschaftenScreen() {
Column(modifier = Modifier.fillMaxSize().padding(24.dp)) {
Text("Meisterschaften", style = MaterialTheme.typography.headlineMedium)
Spacer(Modifier.height(24.dp))
PlaceholderContent(
title = "Meisterschaften",
subtitle = "Konfigurierbare Reglements, Punktesysteme series-context (Phase 2+).",
)
}
}
@Composable
fun CupsScreen() {
Column(modifier = Modifier.fillMaxSize().padding(24.dp)) {
Text("Cups", style = MaterialTheme.typography.headlineMedium)
Spacer(Modifier.height(24.dp))
PlaceholderContent(
title = "Cups & Serien",
subtitle = "Pluggable Berechnungsmodell, Paar-Bindung series-context (Phase 2+).",
)
}
}
@@ -1,47 +0,0 @@
package at.mocode.desktop.screens
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Construction
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
/**
* Wiederverwendbarer Platzhalter für Screens, die noch nicht implementiert sind.
*/
@Composable
fun PlaceholderContent(
title: String,
subtitle: String = "Wird in einer späteren Phase implementiert.",
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(
imageVector = Icons.Default.Construction,
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = MaterialTheme.colorScheme.outlineVariant,
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = subtitle,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.outline,
)
}
}
}
@@ -1,257 +0,0 @@
package at.mocode.desktop.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
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.text.font.FontFamily
import androidx.compose.ui.unit.dp
import at.mocode.zns.feature.ZnsImportViewModel
import org.koin.compose.viewmodel.koinViewModel
import javax.swing.JFileChooser
import javax.swing.filechooser.FileNameExtensionFilter
@Composable
fun StammdatenImportScreen(
viewModel: ZnsImportViewModel = koinViewModel(),
) {
val state = viewModel.state
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
// Titel
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(Icons.Default.CloudUpload, contentDescription = null, tint = MaterialTheme.colorScheme.primary)
Text("Stammdaten-Import (ZNS)", style = MaterialTheme.typography.headlineSmall)
}
HorizontalDivider()
// Datei-Auswahl
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Text("Datei auswählen", style = MaterialTheme.typography.titleMedium)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth(),
) {
OutlinedTextField(
value = state.selectedFilePath ?: "",
onValueChange = {},
readOnly = true,
placeholder = { Text("Keine Datei ausgewählt…") },
modifier = Modifier.weight(1f),
singleLine = true,
)
Button(
onClick = {
val path = pickZipFile()
if (path != null) viewModel.onFileSelected(path)
},
enabled = !state.isUploading && !(!state.isFinished && state.jobId != null),
) {
Icon(Icons.Default.FolderOpen, contentDescription = null)
Spacer(Modifier.width(4.dp))
Text("Durchsuchen")
}
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(
onClick = { viewModel.startImport() },
enabled = state.selectedFilePath != null && !state.isUploading && !(state.jobId != null && !state.isFinished),
) {
if (state.isUploading) {
CircularProgressIndicator(
modifier = Modifier.size(16.dp),
strokeWidth = 2.dp,
color = MaterialTheme.colorScheme.onPrimary
)
Spacer(Modifier.width(8.dp))
} else {
Icon(Icons.Default.Upload, contentDescription = null)
Spacer(Modifier.width(4.dp))
}
Text("Import starten")
}
if (state.isFinished || state.errorMessage != null) {
OutlinedButton(onClick = { viewModel.reset() }) {
Icon(Icons.Default.Refresh, contentDescription = null)
Spacer(Modifier.width(4.dp))
Text("Zurücksetzen")
}
}
}
}
}
// Fehler-Banner
if (state.errorMessage != null) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer),
) {
Row(
modifier = Modifier.padding(12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Icon(Icons.Default.Error, contentDescription = null, tint = MaterialTheme.colorScheme.error)
Text(state.errorMessage!!, color = MaterialTheme.colorScheme.onErrorContainer)
}
}
}
// Fortschritt
if (state.jobId != null) {
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text("Status", style = MaterialTheme.typography.titleMedium)
StatusChip(state.jobStatus)
}
LinearProgressIndicator(
progress = { state.progress / 100f },
modifier = Modifier.fillMaxWidth().height(8.dp),
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
state.progressDetail.ifBlank { "Warte auf Server…" },
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Text(
"${state.progress}%",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
if (state.isFinished && state.jobStatus == "ABGESCHLOSSEN") {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Icon(
Icons.Default.CheckCircle,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(16.dp)
)
Text(
"Import erfolgreich abgeschlossen.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary
)
}
}
}
}
}
// Fehler-Liste
if (state.errors.isNotEmpty()) {
Card(
modifier = Modifier.fillMaxWidth().weight(1f),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp),
) {
Icon(
Icons.Default.Warning,
contentDescription = null,
tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(18.dp)
)
Text(
"Import-Fehler (${state.errors.size})",
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.error,
)
}
HorizontalDivider()
LazyColumn(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
items(state.errors) { error ->
Row(
horizontalArrangement = Arrangement.spacedBy(6.dp),
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surface, RoundedCornerShape(4.dp))
.padding(horizontal = 8.dp, vertical = 4.dp),
) {
Text("", color = MaterialTheme.colorScheme.error)
Text(
error,
style = MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace),
)
}
}
}
}
}
}
}
}
@Composable
private fun StatusChip(status: String?) {
val (label, color) = when (status) {
"AUSSTEHEND" -> "Ausstehend" to MaterialTheme.colorScheme.outline
"ENTPACKEN" -> "Entpacken…" to MaterialTheme.colorScheme.tertiary
"LADE_VEREINE" -> "Lade Vereine…" to MaterialTheme.colorScheme.secondary
"LADE_REITER" -> "Lade Reiter…" to MaterialTheme.colorScheme.secondary
"LADE_PFERDE" -> "Lade Pferde…" to MaterialTheme.colorScheme.secondary
"LADE_RICHTER" -> "Lade Richter…" to MaterialTheme.colorScheme.secondary
"ABGESCHLOSSEN" -> "Abgeschlossen ✓" to MaterialTheme.colorScheme.primary
"FEHLER" -> "Fehler ✗" to MaterialTheme.colorScheme.error
else -> (status ?: "") to MaterialTheme.colorScheme.outline
}
Surface(
shape = RoundedCornerShape(12.dp),
color = color.copy(alpha = 0.15f),
) {
Text(
label,
modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp),
style = MaterialTheme.typography.labelSmall,
color = color,
)
}
}
/** Öffnet einen nativen JFileChooser (JVM-only) und gibt den Pfad der gewählten ZIP zurück. */
private fun pickZipFile(): String? {
val chooser = JFileChooser()
chooser.dialogTitle = "ZNS.zip auswählen"
chooser.fileFilter = FileNameExtensionFilter("ZIP-Archiv (*.zip)", "zip")
chooser.isAcceptAllFileFilterUsed = false
val result = chooser.showOpenDialog(null)
return if (result == JFileChooser.APPROVE_OPTION) chooser.selectedFile.absolutePath else null
}
@@ -1,265 +0,0 @@
package at.mocode.desktop.screens
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
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 androidx.compose.ui.unit.sp
/**
* Detailansicht eines Turniers gemäß Vision_03.
*
* Layout: Horizontale Tab-Bar mit 8 Tabs (kein eigener Toolbar-Zurück-Button
* Navigation erfolgt über den Breadcrumb in der TopBar).
*
* Tabs:
* 1. STAMMDATEN Turnier-Konfiguration, ZNS-Import, Sparten, Datum
* 2. ORGANISATION Funktionäre, Richterkollegium, Austragungsplätze
* 3. BEWERBE 3-spaltiges Layout (Aktionen | Tabelle | Detail-Panel)
* 4. ARTIKEL Gebühren, Stallungen & Boxen, Zusatzgebühren
* 5. ABRECHNUNG Buchungen, Offene Posten, Rechnung
* 6. NENNUNGEN Pferd+Reiter-Suche, Verkauf/Buchungen, Bewerbsübersicht
* 7. STARTLISTEN Bewerbs-Tabs, Sortierung, Zeit/Dauer
* 8. ERGEBNISLISTEN Bewerbs-Tabs, Platzierung & Geldpreise
*
* TODO: Echte Inhalte pro Tab implementieren (Phase 4/5).
*/
@Composable
fun TurnierDetailScreen(
veranstaltungId: Long,
turnierId: Long,
onBack: () -> Unit,
) {
var selectedTab by remember { mutableIntStateOf(0) }
val tabs = listOf(
"STAMMDATEN",
"ORGANISATION",
"BEWERBE",
"ARTIKEL",
"ABRECHNUNG",
"NENNUNGEN",
"STARTLISTEN",
"ERGEBNISLISTEN",
)
Column(modifier = Modifier.fillMaxSize()) {
// Horizontale Tab-Bar (direkt unter der TopBar)
ScrollableTabRow(
selectedTabIndex = selectedTab,
containerColor = MaterialTheme.colorScheme.surface,
contentColor = Color(0xFF1E3A8A),
edgePadding = 0.dp,
) {
tabs.forEachIndexed { index, title ->
Tab(
selected = selectedTab == index,
onClick = { selectedTab = index },
text = {
Text(
text = title,
fontSize = 13.sp,
fontWeight = if (selectedTab == index) FontWeight.Bold else FontWeight.Normal,
)
},
)
}
}
HorizontalDivider()
// Tab-Inhalte
Box(modifier = Modifier.fillMaxSize()) {
when (selectedTab) {
0 -> StammdatenTabContent(turnierId = turnierId)
1 -> OrganisationTabContent()
2 -> BewerbeTabContent()
3 -> ArtikelTabContent()
4 -> AbrechnungTabContent()
5 -> NennungenTabContent()
6 -> StartlistenTabContent()
7 -> ErgebnislistenTabContent()
}
}
}
}
// ---------------------------------------------------------------------------
// Tab-Inhalte (Placeholder werden in späteren Phasen befüllt)
// ---------------------------------------------------------------------------
@Composable
private fun StammdatenTabContent(turnierId: Long) {
PlaceholderContent(
title = "Stammdaten Turnier $turnierId",
subtitle = "Turnier-Konfiguration, ZNS-Import, Sparten, Klassen, Datum …",
)
}
@Composable
private fun OrganisationTabContent() {
PlaceholderContent(
title = "Organisation",
subtitle = "Funktionäre & Offizielle (C-Satz), Richterkollegium, Austragungsplätze …",
)
}
@Composable
private fun BewerbeTabContent() {
// Typ C: 3-spaltiges Layout (Aktionen | Tabelle | Detail-Panel)
Row(modifier = Modifier.fillMaxSize()) {
// Linke Aktions-Spalte
BewerbeAktionsSpalte(modifier = Modifier.width(140.dp).fillMaxHeight())
VerticalDivider()
// Mittlere Tabelle
Box(modifier = Modifier.weight(1f).fillMaxHeight()) {
PlaceholderContent(
title = "Bewerbe",
subtitle = "Liste aller Bewerbe dieses Turniers …",
)
}
VerticalDivider()
// Rechtes Detail-Panel
BewerbeDetailPanel(modifier = Modifier.width(320.dp).fillMaxHeight())
}
}
@Composable
private fun BewerbeAktionsSpalte(modifier: Modifier = Modifier) {
Column(
modifier = modifier.padding(8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
AktionsButton("Änderungen\nSpeichern")
AktionsButton("Änderungen\nRückgängig")
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
AktionsButton("Bewerb\nEinfügen")
AktionsButton("Bewerb\nLöschen")
AktionsButton("Bewerb Teilen")
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
AktionsButton("Bewerb nach\noben verschieben")
AktionsButton("Bewerb nach\nunten verschieben")
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
AktionsButton("Startliste\nBearbeiten")
AktionsButton("Startliste\nDrucken")
AktionsButton("Ergebnisliste\nBearbeiten")
AktionsButton("Ergebnisliste\nDrucken")
}
}
@Composable
private fun AktionsButton(label: String) {
OutlinedButton(
onClick = {},
modifier = Modifier.fillMaxWidth().height(48.dp),
contentPadding = PaddingValues(horizontal = 4.dp, vertical = 2.dp),
) {
Text(label, fontSize = 11.sp, lineHeight = 13.sp)
}
}
@Composable
private fun BewerbeDetailPanel(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(12.dp)) {
// Sub-Tabs: Bewerb | Bewertung | Geldpreise | Ort/Zeit
var subTab by remember { mutableIntStateOf(0) }
val subTabs = listOf("Bewerb", "Bewertung", "Geldpreise", "Ort/Zeit")
TabRow(
selectedTabIndex = subTab,
containerColor = MaterialTheme.colorScheme.surface,
contentColor = Color(0xFF1E3A8A),
) {
subTabs.forEachIndexed { i, title ->
Tab(
selected = subTab == i,
onClick = { subTab = i },
text = { Text(title, fontSize = 12.sp) },
)
}
}
Spacer(Modifier.height(8.dp))
PlaceholderContent(
title = subTabs[subTab],
subtitle = "Bewerb-Details …",
)
}
}
@Composable
private fun ArtikelTabContent() {
PlaceholderContent(
title = "Artikel Nennungen & Gebühren",
subtitle = "Nenngebühr, Startgebühr, Sporteuro, Stallungen & Boxen, Zusatzgebühren …",
)
}
@Composable
private fun AbrechnungTabContent() {
PlaceholderContent(
title = "Abrechnung",
subtitle = "Buchungen, Offene Posten, Rechnung …",
)
}
@Composable
private fun NennungenTabContent() {
// Typ B: 2-spaltig (Pferd+Reiter-Suche | Verkauf/Buchungen)
Row(modifier = Modifier.fillMaxSize()) {
Box(modifier = Modifier.weight(1f).fillMaxHeight()) {
PlaceholderContent(
title = "Nennungen",
subtitle = "Pferd- und Reiter-Suche, Nennungs-Tabelle …",
)
}
VerticalDivider()
Box(modifier = Modifier.width(340.dp).fillMaxHeight()) {
PlaceholderContent(
title = "Verkauf / Buchungen",
subtitle = "Artikel-Buchungen, Bewerbsübersicht …",
)
}
}
}
@Composable
private fun StartlistenTabContent() {
// Typ B: Tabelle + rechtes Sortier/Zeit-Panel
Row(modifier = Modifier.fillMaxSize()) {
Box(modifier = Modifier.weight(1f).fillMaxHeight()) {
PlaceholderContent(
title = "Startlisten",
subtitle = "Bewerbs-Tabs, Starter-Liste …",
)
}
VerticalDivider()
Box(modifier = Modifier.width(280.dp).fillMaxHeight()) {
PlaceholderContent(
title = "Sortierung & Zeit",
subtitle = "Aufsteigend/Absteigend, Auslosung, Beginnzeit, Reitdauer …",
)
}
}
}
@Composable
private fun ErgebnislistenTabContent() {
// Typ B: Tabelle + rechtes Platzierungs-Panel
Row(modifier = Modifier.fillMaxSize()) {
Box(modifier = Modifier.weight(1f).fillMaxHeight()) {
PlaceholderContent(
title = "Ergebnislisten",
subtitle = "Bewerbs-Tabs, Ergebnis-Eingabe (Fehler, Zeit) …",
)
}
VerticalDivider()
Box(modifier = Modifier.width(280.dp).fillMaxHeight()) {
PlaceholderContent(
title = "Platzierung & Geldpreis",
subtitle = "Anzahl Platzierte, Geldpreis, Import/Export/Drucken …",
)
}
}
}
@@ -1,64 +0,0 @@
package at.mocode.desktop.screens
import androidx.compose.foundation.layout.*
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.unit.dp
/**
* Formular zum Anlegen eines neuen Turniers (Vision_03: /veranstaltung/{id}/turnier/neu).
* Tabs: Übersicht | Stammdaten (A-Satz) | Organisation | Bewerbe ⭐ | Preisliste
* TODO: Echte Formular-Felder und Persistenz (Phase 4/5).
*/
@Composable
fun TurnierNeuScreen(
veranstaltungId: Long,
onBack: () -> Unit,
onSave: () -> Unit,
) {
var selectedTab by remember { mutableIntStateOf(3) } // Bewerbe ist Standard-Tab (⭐)
val tabs = listOf("Übersicht", "Stammdaten (A-Satz)", "Organisation", "Bewerbe ⭐", "Preisliste")
Column(modifier = Modifier.fillMaxSize()) {
Row(
modifier = Modifier.fillMaxWidth().padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Row {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück")
}
Spacer(Modifier.width(8.dp))
Text(
text = "Neues Turnier (Veranstaltung #$veranstaltungId)",
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.alignByBaseline(),
)
}
Button(onClick = onSave) { Text("Speichern") }
}
PrimaryTabRow(selectedTabIndex = selectedTab) {
tabs.forEachIndexed { index, title ->
Tab(
selected = selectedTab == index,
onClick = { selectedTab = index },
text = { Text(title) },
)
}
}
Box(modifier = Modifier.fillMaxSize().padding(24.dp)) {
when (selectedTab) {
0 -> PlaceholderContent("Übersicht", "Wird nach dem Speichern befüllt.")
1 -> PlaceholderContent("Stammdaten (A-Satz)", "OEPS-Turniernummer, Kategorie, Sparte …")
2 -> PlaceholderContent("Organisation", "Richter, Parcourschef, Tierarzt …")
3 -> PlaceholderContent("Bewerbe", "Bewerbe anlegen und Abteilungen konfigurieren …")
4 -> PlaceholderContent("Preisliste", "Nenngebühren pro Bewerb/Sparte …")
}
}
}
}
@@ -1,172 +0,0 @@
package at.mocode.desktop.screens
import androidx.compose.foundation.background
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.Search
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 androidx.compose.ui.unit.sp
private val PrimaryBlue = Color(0xFF1E3A8A)
private val AccentBlue = Color(0xFF3B82F6)
/**
* Screen: "Admin - Verwaltung / Veranstalter auswählen"
*
* Gemäß Figma Vision_03 (figma-entwurf_22 / figma-entwurf_20):
* - Tabelle aller registrierten Veranstalter/Kunden
* - Klick auf Zeile → Veranstalter markiert (selektiert)
* - "Weiter zum Veranstalter"-Button wird aktiv sobald ein Veranstalter ausgewählt ist
*
* TODO: Echte Daten aus customer-context laden (Phase 4/5).
*/
@Composable
fun VeranstalterAuswahlScreen(
onZurueck: () -> Unit,
onWeiter: (Long) -> Unit,
) {
var selectedId by remember { mutableStateOf<Long?>(null) }
var suchtext by remember { mutableStateOf("") }
// Placeholder-Daten
val veranstalter = remember {
listOf(
VeranstalterUiModel(1L, "Reit- und Fahrverein Wels", "Wels", "", 12),
VeranstalterUiModel(2L, "Pferdesportverein Linz", "Linz", "", 8),
VeranstalterUiModel(3L, "Reiterverein Salzburg", "Salzburg", "S", 5),
VeranstalterUiModel(4L, "Reitclub Wien Nord", "Wien", "W", 3),
VeranstalterUiModel(5L, "Fahrverein Graz", "Graz", "ST", 7),
)
}
val gefiltert = veranstalter.filter {
suchtext.isBlank() || it.name.contains(suchtext, ignoreCase = true) ||
it.ort.contains(suchtext, ignoreCase = true)
}
Column(modifier = Modifier.fillMaxSize()) {
// Seiten-Header
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Column {
Text(
text = "Veranstalter auswählen",
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
)
Text(
text = "Wähle einen registrierten Veranstalter aus, um eine neue Veranstaltung anzulegen.",
fontSize = 13.sp,
color = Color(0xFF6B7280),
)
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedButton(onClick = onZurueck) {
Text("Abbrechen")
}
Button(
onClick = { selectedId?.let { onWeiter(it) } },
enabled = selectedId != null,
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue),
) {
Text("Weiter zum Veranstalter")
}
}
}
HorizontalDivider()
// Suchfeld
OutlinedTextField(
value = suchtext,
onValueChange = { suchtext = it },
placeholder = { Text("Suche nach Name oder Ort...", fontSize = 13.sp) },
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp)
.height(48.dp),
singleLine = true,
)
// Tabellen-Header
Row(
modifier = Modifier
.fillMaxWidth()
.background(Color(0xFFF3F4F6))
.padding(horizontal = 16.dp, vertical = 6.dp),
) {
Text("Name", fontWeight = FontWeight.SemiBold, fontSize = 12.sp, modifier = Modifier.weight(3f))
Text("Ort", fontWeight = FontWeight.SemiBold, fontSize = 12.sp, modifier = Modifier.weight(1.5f))
Text("Bundesland", fontWeight = FontWeight.SemiBold, fontSize = 12.sp, modifier = Modifier.weight(1f))
Text("Veranstaltungen", fontWeight = FontWeight.SemiBold, fontSize = 12.sp, modifier = Modifier.weight(1f))
}
HorizontalDivider()
// Tabellen-Inhalt
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(gefiltert) { v ->
val isSelected = v.id == selectedId
Row(
modifier = Modifier
.fillMaxWidth()
.background(
if (isSelected) AccentBlue.copy(alpha = 0.1f)
else Color.Transparent
)
.clickable { selectedId = v.id }
.padding(horizontal = 16.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically,
) {
// Auswahl-Indikator
RadioButton(
selected = isSelected,
onClick = { selectedId = v.id },
colors = RadioButtonDefaults.colors(selectedColor = AccentBlue),
modifier = Modifier.size(20.dp),
)
Spacer(Modifier.width(8.dp))
Text(
text = v.name,
fontSize = 13.sp,
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal,
modifier = Modifier.weight(3f),
)
Text(v.ort, fontSize = 13.sp, modifier = Modifier.weight(1.5f))
Text(v.bundesland, fontSize = 13.sp, modifier = Modifier.weight(1f))
Text(
text = "${v.veranstaltungsAnzahl}",
fontSize = 13.sp,
modifier = Modifier.weight(1f),
)
}
HorizontalDivider(color = Color(0xFFE5E7EB))
}
}
}
}
// --- UI-Modell ---
data class VeranstalterUiModel(
val id: Long,
val name: String,
val ort: String,
val bundesland: String,
val veranstaltungsAnzahl: Int,
)
@@ -1,213 +0,0 @@
package at.mocode.desktop.screens
import androidx.compose.foundation.BorderStroke
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.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
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 androidx.compose.ui.unit.sp
private val PrimaryBlue = Color(0xFF1E3A8A)
private val StatusVorbereitungColor = Color(0xFFEA580C)
private val StatusLiveColor = Color(0xFF16A34A)
private val StatusAbgeschlossenColor = Color(0xFF6B7280)
/**
* Screen: "Admin - Verwaltung / Veranstalter auswählen / <Vereinsname>"
*
* Gemäß Figma Vision_03 (figma-entwurf_19):
* - Veranstalter-Profil (Name, Ort, Kontakt)
* - Liste aller Veranstaltungen dieses Veranstalters
* - Klick auf Veranstaltung → VeranstaltungUebersicht
*
* TODO: Echte Daten aus customer-context / event-management-context laden (Phase 4/5).
*/
@Composable
fun VeranstalterDetailScreen(
veranstalterId: Long,
onZurueck: () -> Unit,
onVeranstaltungOeffnen: (Long) -> Unit,
onVeranstaltungNeu: () -> Unit,
) {
// Placeholder-Daten
val veranstalter = remember(veranstalterId) {
VeranstalterUiModel(
id = veranstalterId,
name = "Reit- und Fahrverein Wels",
ort = "Wels",
bundesland = "",
veranstaltungsAnzahl = 12,
)
}
val veranstaltungen = remember(veranstalterId) {
listOf(
VeranstaltungUiModel(
id = 1L, name = "Frühjahrsturnier Wels 2026", ort = "Wels", datum = "15.04.2026",
turnierAnzahl = 3, nennungen = 47, letzteAktivitaet = "heute",
status = VeranstaltungStatus.VORBEREITUNG,
),
VeranstaltungUiModel(
id = 2L, name = "Sommerturnier Wels 2025", ort = "Wels", datum = "20.07.2025",
turnierAnzahl = 5, nennungen = 112, letzteAktivitaet = "vor 8 Monaten",
status = VeranstaltungStatus.ABGESCHLOSSEN,
),
VeranstaltungUiModel(
id = 3L, name = "Herbstturnier Wels 2025", ort = "Wels", datum = "12.10.2025",
turnierAnzahl = 4, nennungen = 89, letzteAktivitaet = "vor 5 Monaten",
status = VeranstaltungStatus.ABGESCHLOSSEN,
),
)
}
Column(modifier = Modifier.fillMaxSize()) {
// Veranstalter-Profil-Header
Surface(
modifier = Modifier.fillMaxWidth(),
color = Color(0xFFF8FAFC),
border = BorderStroke(1.dp, Color(0xFFE2E8F0)),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Column {
Text(
text = veranstalter.name,
fontSize = 22.sp,
fontWeight = FontWeight.Bold,
)
Spacer(Modifier.height(4.dp))
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
Text("📍 ${veranstalter.ort}, ${veranstalter.bundesland}", fontSize = 13.sp, color = Color(0xFF6B7280))
Text("🏆 ${veranstalter.veranstaltungsAnzahl} Veranstaltungen gesamt", fontSize = 13.sp, color = Color(0xFF6B7280))
}
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedButton(onClick = onZurueck) {
Text("← Zurück zur Auswahl")
}
Button(
onClick = onVeranstaltungNeu,
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue),
) {
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(16.dp))
Spacer(Modifier.width(4.dp))
Text("Neue Veranstaltung")
}
}
}
}
HorizontalDivider()
// Veranstaltungs-Liste
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = "Veranstaltungen (${veranstaltungen.size})",
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
)
}
LazyColumn(
modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(bottom = 16.dp),
) {
items(veranstaltungen) { veranstaltung ->
VeranstaltungListCard(
veranstaltung = veranstaltung,
onOeffnen = { onVeranstaltungOeffnen(veranstaltung.id) },
)
}
}
}
}
@Composable
private fun VeranstaltungListCard(
veranstaltung: VeranstaltungUiModel,
onOeffnen: () -> Unit,
) {
val statusColor = when (veranstaltung.status) {
VeranstaltungStatus.VORBEREITUNG -> StatusVorbereitungColor
VeranstaltungStatus.LIVE -> StatusLiveColor
VeranstaltungStatus.ABGESCHLOSSEN -> StatusAbgeschlossenColor
}
val statusText = when (veranstaltung.status) {
VeranstaltungStatus.VORBEREITUNG -> "Vorbereitung"
VeranstaltungStatus.LIVE -> "Live"
VeranstaltungStatus.ABGESCHLOSSEN -> "Abgeschlossen"
}
Card(
modifier = Modifier.fillMaxWidth(),
border = if (veranstaltung.status == VeranstaltungStatus.VORBEREITUNG)
BorderStroke(1.dp, Color(0xFF3B82F6)) else null,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Column(modifier = Modifier.weight(1f)) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
text = veranstaltung.name,
fontWeight = FontWeight.SemiBold,
fontSize = 15.sp,
)
Surface(
shape = MaterialTheme.shapes.small,
color = statusColor.copy(alpha = 0.15f),
border = BorderStroke(1.dp, statusColor),
) {
Text(
text = statusText,
color = statusColor,
fontSize = 11.sp,
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp),
)
}
}
Spacer(Modifier.height(4.dp))
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
Text("📍 ${veranstaltung.ort}", fontSize = 12.sp, color = Color(0xFF6B7280))
Text("📅 ${veranstaltung.datum}", fontSize = 12.sp, color = Color(0xFF6B7280))
Text("🏆 ${veranstaltung.turnierAnzahl} Turniere", fontSize = 12.sp, color = Color(0xFF6B7280))
Text("📋 ${veranstaltung.nennungen} Nennungen", fontSize = 12.sp, color = Color(0xFF6B7280))
}
}
Button(
onClick = onOeffnen,
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue),
) {
Text("Veranstaltung öffnen →")
}
}
}
}
@@ -1,66 +0,0 @@
package at.mocode.desktop.screens
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.Add
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
/**
* Detailansicht einer bestehenden Veranstaltung (Vision_03: /veranstaltung/{id}).
* Zeigt Übersicht-Tab mit Turniere-Section.
* TODO: Echte Daten laden (Phase 4/5).
*/
@Composable
fun VeranstaltungDetailScreen(
veranstaltungId: Long,
onBack: () -> Unit,
onTurnierNeu: () -> Unit,
onTurnierOeffnen: (Long) -> Unit,
) {
Column(modifier = Modifier.fillMaxSize()) {
// Toolbar
Row(
modifier = Modifier.fillMaxWidth().padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück")
}
Spacer(Modifier.width(8.dp))
Text(
text = "Veranstaltung #$veranstaltungId",
style = MaterialTheme.typography.headlineSmall,
)
}
PrimaryTabRow(selectedTabIndex = 0) {
Tab(selected = true, onClick = {}, text = { Text("Veranstaltung Übersicht") })
}
Column(modifier = Modifier.fillMaxSize().padding(24.dp)) {
// Turniere-Section
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text("Turniere", style = MaterialTheme.typography.titleMedium)
OutlinedButton(onClick = onTurnierNeu) {
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(16.dp))
Spacer(Modifier.width(4.dp))
Text("Neues Turnier")
}
}
Spacer(Modifier.height(16.dp))
PlaceholderContent(
title = "Noch keine Turniere",
subtitle = "Lege ein neues Turnier für diese Veranstaltung an.",
)
}
}
}
@@ -1,63 +0,0 @@
package at.mocode.desktop.screens
import androidx.compose.foundation.layout.*
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.unit.dp
/**
* 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).
*/
@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")
Column(modifier = Modifier.fillMaxSize()) {
// Toolbar
Row(
modifier = Modifier.fillMaxWidth().padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Row {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück")
}
Spacer(Modifier.width(8.dp))
Text(
text = "Neue Veranstaltung",
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.alignByBaseline(),
)
}
Button(onClick = onSave) { Text("Speichern") }
}
PrimaryTabRow(selectedTabIndex = selectedTab) {
tabs.forEachIndexed { index, title ->
Tab(
selected = selectedTab == index,
onClick = { selectedTab = index },
text = { Text(title) },
)
}
}
Box(modifier = Modifier.fillMaxSize().padding(24.dp)) {
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 …")
}
}
}
}
@@ -1,349 +0,0 @@
package at.mocode.desktop.screens
import androidx.compose.foundation.BorderStroke
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.material.icons.filled.FileDownload
import androidx.compose.material.icons.filled.FileUpload
import androidx.compose.material.icons.filled.FolderOpen
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
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 androidx.compose.ui.unit.sp
private val PrimaryBlue = Color(0xFF1E3A8A)
private val ZnsGreen = Color(0xFF16A34A)
private val ImportOrange = Color(0xFFEA580C)
private val ExportBlue = Color(0xFF2563EB)
/**
* Screen: "Veranstaltung - Übersicht"
*
* Gemäß Figma Vision_03 (figma-entwurf_17):
* - Veranstaltungs-Header (Name, Datum, Ort, Status)
* - Liste aller Turniere dieser Veranstaltung als Cards
* - Jede Turnier-Card hat Buttons:
* - "Öffnen" → Meldestelle des Turniers öffnen (TurnierDetail)
* - "Import" → Datenbank-Sicherung importieren
* - "Export" → Datenbank-Sicherung exportieren
* - "ZNS" → ZNS-Import für dieses Turnier starten
*
* Warum ZNS hier? Jedes Turnier hat seine eigene Datenbank/Kassa.
* Der ZNS-Import muss daher turnierspezifisch sein.
*
* TODO: Echte Daten aus event-management-context laden (Phase 4/5).
*/
@Composable
fun VeranstaltungUebersichtScreen(
veranstalterId: Long,
veranstaltungId: Long,
onZurueck: () -> Unit,
onTurnierOeffnen: (turnierId: Long) -> Unit,
onTurnierNeu: () -> Unit,
onZnsImport: (turnierId: Long) -> Unit,
onDbImport: (turnierId: Long) -> Unit,
onDbExport: (turnierId: Long) -> Unit,
) {
// Placeholder-Daten
val veranstaltung = remember(veranstaltungId) {
VeranstaltungUiModel(
id = veranstaltungId,
name = "Frühjahrsturnier Wels 2026",
ort = "Wels",
datum = "15.04.2026",
turnierAnzahl = 3,
nennungen = 47,
letzteAktivitaet = "heute",
status = VeranstaltungStatus.VORBEREITUNG,
)
}
val turniere = remember(veranstaltungId) {
listOf(
TurnierKarteUiModel(
id = 1L,
nummer = 1L,
name = "Dressurturnier",
sparte = "Dressur",
bewerbAnzahl = 8,
nennungen = 24,
status = TurnierKarteStatus.VORBEREITUNG,
datum = "15.04.2026",
),
TurnierKarteUiModel(
id = 2L,
nummer = 2L,
name = "Springturnier",
sparte = "Springen",
bewerbAnzahl = 6,
nennungen = 18,
status = TurnierKarteStatus.VORBEREITUNG,
datum = "15.04.2026",
),
TurnierKarteUiModel(
id = 3L,
nummer = 3L,
name = "Vielseitigkeitsturnier",
sparte = "Vielseitigkeit",
bewerbAnzahl = 4,
nennungen = 5,
status = TurnierKarteStatus.VORBEREITUNG,
datum = "16.04.2026",
),
)
}
Column(modifier = Modifier.fillMaxSize()) {
// Veranstaltungs-Header
Surface(
modifier = Modifier.fillMaxWidth(),
color = Color(0xFFF8FAFC),
border = BorderStroke(1.dp, Color(0xFFE2E8F0)),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Column {
Text(
text = veranstaltung.name,
fontSize = 22.sp,
fontWeight = FontWeight.Bold,
)
Spacer(Modifier.height(4.dp))
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
Text("📍 ${veranstaltung.ort}", fontSize = 13.sp, color = Color(0xFF6B7280))
Text("📅 ${veranstaltung.datum}", fontSize = 13.sp, color = Color(0xFF6B7280))
Text("🏆 ${veranstaltung.turnierAnzahl} Turniere", fontSize = 13.sp, color = Color(0xFF6B7280))
Text("📋 ${veranstaltung.nennungen} Nennungen gesamt", fontSize = 13.sp, color = Color(0xFF6B7280))
}
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedButton(onClick = onZurueck) {
Text("← Zurück")
}
Button(
onClick = onTurnierNeu,
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue),
) {
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(16.dp))
Spacer(Modifier.width(4.dp))
Text("Neues Turnier")
}
}
}
}
HorizontalDivider()
// Turnier-Liste
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = "Turniere (${turniere.size})",
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
)
Spacer(Modifier.width(8.dp))
Text(
text = "Jedes Turnier hat eine eigene Datenbank und Kassa.",
fontSize = 12.sp,
color = Color(0xFF6B7280),
)
}
LazyColumn(
modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
contentPadding = PaddingValues(bottom = 16.dp),
) {
items(turniere) { turnier ->
TurnierKarte(
turnier = turnier,
onOeffnen = { onTurnierOeffnen(turnier.id) },
onZns = { onZnsImport(turnier.id) },
onImport = { onDbImport(turnier.id) },
onExport = { onDbExport(turnier.id) },
)
}
}
}
}
@Composable
private fun TurnierKarte(
turnier: TurnierKarteUiModel,
onOeffnen: () -> Unit,
onZns: () -> Unit,
onImport: () -> Unit,
onExport: () -> Unit,
) {
val statusColor = when (turnier.status) {
TurnierKarteStatus.VORBEREITUNG -> Color(0xFFEA580C)
TurnierKarteStatus.LIVE -> Color(0xFF16A34A)
TurnierKarteStatus.ABGESCHLOSSEN -> Color(0xFF6B7280)
}
val statusText = when (turnier.status) {
TurnierKarteStatus.VORBEREITUNG -> "Vorbereitung"
TurnierKarteStatus.LIVE -> "Live"
TurnierKarteStatus.ABGESCHLOSSEN -> "Abgeschlossen"
}
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
) {
Column(modifier = Modifier.padding(16.dp)) {
// Turnier-Header
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp),
) {
// Turnier-Nummer Badge
Surface(
shape = MaterialTheme.shapes.small,
color = PrimaryBlue,
) {
Text(
text = "T${turnier.nummer}",
color = Color.White,
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
)
}
Column {
Text(
text = turnier.name,
fontWeight = FontWeight.SemiBold,
fontSize = 16.sp,
)
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Text("🏇 ${turnier.sparte}", fontSize = 12.sp, color = Color(0xFF6B7280))
Text("📅 ${turnier.datum}", fontSize = 12.sp, color = Color(0xFF6B7280))
Text("${turnier.bewerbAnzahl} Bewerbe", fontSize = 12.sp, color = Color(0xFF6B7280))
Text("${turnier.nennungen} Nennungen", fontSize = 12.sp, color = Color(0xFF6B7280))
}
}
}
// Status-Badge
Surface(
shape = MaterialTheme.shapes.small,
color = statusColor.copy(alpha = 0.15f),
border = BorderStroke(1.dp, statusColor),
) {
Text(
text = statusText,
color = statusColor,
fontSize = 11.sp,
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp),
)
}
}
Spacer(Modifier.height(12.dp))
HorizontalDivider(color = Color(0xFFE5E7EB))
Spacer(Modifier.height(12.dp))
// Aktions-Buttons
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
// Öffnen Hauptaktion
Button(
onClick = onOeffnen,
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue),
modifier = Modifier.height(36.dp),
) {
Icon(
Icons.Default.FolderOpen,
contentDescription = null,
modifier = Modifier.size(16.dp),
)
Spacer(Modifier.width(4.dp))
Text("Öffnen", fontSize = 13.sp)
}
// ZNS-Import turnierspezifisch (eigene DB!)
OutlinedButton(
onClick = onZns,
border = BorderStroke(1.dp, ZnsGreen),
modifier = Modifier.height(36.dp),
) {
Text("ZNS", fontSize = 13.sp, color = ZnsGreen, fontWeight = FontWeight.SemiBold)
}
Spacer(Modifier.weight(1f))
// Import / Export DB-Sicherung
OutlinedButton(
onClick = onImport,
border = BorderStroke(1.dp, ImportOrange),
modifier = Modifier.height(36.dp),
) {
Icon(
Icons.Default.FileUpload,
contentDescription = null,
tint = ImportOrange,
modifier = Modifier.size(15.dp),
)
Spacer(Modifier.width(4.dp))
Text("Import", fontSize = 13.sp, color = ImportOrange)
}
OutlinedButton(
onClick = onExport,
border = BorderStroke(1.dp, ExportBlue),
modifier = Modifier.height(36.dp),
) {
Icon(
Icons.Default.FileDownload,
contentDescription = null,
tint = ExportBlue,
modifier = Modifier.size(15.dp),
)
Spacer(Modifier.width(4.dp))
Text("Export", fontSize = 13.sp, color = ExportBlue)
}
}
}
}
}
// --- UI-Modelle ---
data class TurnierKarteUiModel(
val id: Long,
val nummer: Long,
val name: String,
val sparte: String,
val bewerbAnzahl: Int,
val nennungen: Int,
val status: TurnierKarteStatus,
val datum: String,
)
enum class TurnierKarteStatus { VORBEREITUNG, LIVE, ABGESCHLOSSEN }
@@ -1,50 +0,0 @@
package at.mocode.desktop.screens
import androidx.compose.foundation.layout.*
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.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
/**
* 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)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = "Veranstaltungen",
style = MaterialTheme.typography.headlineMedium,
)
Button(onClick = onVeranstaltungNeu) {
Icon(Icons.Default.Add, contentDescription = null)
Spacer(Modifier.width(8.dp))
Text("Neue Veranstaltung")
}
}
Spacer(modifier = Modifier.height(24.dp))
// Platzhalter wird durch echte Daten ersetzt
PlaceholderContent(
title = "Noch keine Veranstaltungen",
subtitle = "Lege eine neue Veranstaltung an, um zu beginnen.",
)
}
}
@@ -1,4 +1,4 @@
package at.mocode.desktop.screens
package at.mocode.desktop.screens.layout
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -15,6 +15,19 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import at.mocode.frontend.core.navigation.AppScreen
import at.mocode.veranstaltung.feature.presentation.AdminUebersichtScreen
import at.mocode.veranstalter.feature.presentation.VeranstalterAuswahlScreen
import at.mocode.veranstalter.feature.presentation.VeranstalterNeuScreen
import at.mocode.veranstalter.feature.presentation.VeranstalterDetailScreen
import at.mocode.veranstaltung.feature.presentation.VeranstaltungUebersichtScreen
import at.mocode.veranstaltung.feature.presentation.VeranstaltungDetailScreen
import at.mocode.veranstaltung.feature.presentation.VeranstaltungNeuScreen
import at.mocode.turnier.feature.presentation.TurnierDetailScreen
import at.mocode.turnier.feature.presentation.TurnierNeuScreen
import at.mocode.zns.feature.presentation.StammdatenImportScreen
import at.mocode.ping.feature.presentation.PingScreen
import org.koin.compose.koinInject
import at.mocode.ping.feature.presentation.PingViewModel
// Primärfarbe der TopBar (kann später ins Theme ausgelagert werden)
private val TopBarColor = Color(0xFF1E3A8A)
@@ -55,9 +68,9 @@ fun DesktopMainLayout(
* TopBar: dunkelblauer Balken mit Breadcrumb-Navigation und Logout-Button.
*
* Breadcrumb-Logik:
* - Root: "🏠 Admin - Verwaltung"
* - Root: "🏠 Admin - Verwaltung"
* - Veranstaltung: "🏠 Admin - Verwaltung / Veranstaltung #<id>"
* - Turnier: "🏠 Admin - Verwaltung / Veranstaltung #<id> / Turnier <tid>"
* - Turnier: "🏠 Admin - Verwaltung / Veranstaltung #<id> / Turnier <tid>"
*/
@Composable
private fun DesktopTopBar(
@@ -107,6 +120,23 @@ private fun DesktopTopBar(
fontSize = 14.sp,
)
}
is AppScreen.VeranstalterNeu -> {
BreadcrumbSeparator()
Text(
text = "Veranstalter auswählen",
color = TopBarTextColor.copy(alpha = 0.75f),
fontSize = 14.sp,
modifier = Modifier.clickable { onNavigate(AppScreen.VeranstalterAuswahl) },
)
BreadcrumbSeparator()
Text(
text = "Neuer Veranstalter",
color = TopBarTextColor,
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
)
}
is AppScreen.VeranstalterDetail -> {
BreadcrumbSeparator()
Text(
@@ -199,6 +229,15 @@ private fun DesktopTopBar(
fontSize = 14.sp,
)
}
is AppScreen.Ping -> {
BreadcrumbSeparator()
Text(
text = "Ping Service",
color = TopBarTextColor,
fontSize = 14.sp,
)
}
else -> {}
}
}
@@ -236,12 +275,19 @@ private fun DesktopContentArea(
is AppScreen.Veranstaltungen -> AdminUebersichtScreen(
onVeranstalterAuswahl = { onNavigate(AppScreen.VeranstalterAuswahl) },
onVeranstaltungOeffnen = { id -> onNavigate(AppScreen.VeranstaltungDetail(id)) },
onPingService = { onNavigate(AppScreen.Ping) },
)
// Neuer Flow: Veranstalter auswählen → Detail → Veranstaltung-Übersicht
is AppScreen.VeranstalterAuswahl -> VeranstalterAuswahlScreen(
onZurueck = { onNavigate(AppScreen.Veranstaltungen) },
onWeiter = { veranstalterId -> onNavigate(AppScreen.VeranstalterDetail(veranstalterId)) },
onNeuerVeranstalter = { onNavigate(AppScreen.VeranstalterNeu) },
)
is AppScreen.VeranstalterNeu -> VeranstalterNeuScreen(
onAbbrechen = { onNavigate(AppScreen.VeranstalterAuswahl) },
onSpeichern = { _, _, _ -> onNavigate(AppScreen.VeranstalterAuswahl) },
)
is AppScreen.VeranstalterDetail -> VeranstalterDetailScreen(
veranstalterId = currentScreen.veranstalterId,
@@ -288,6 +334,15 @@ private fun DesktopContentArea(
onSave = { onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId)) },
)
// Ping-Screen
is AppScreen.Ping -> {
val pingViewModel: PingViewModel = koinInject()
PingScreen(
viewModel = pingViewModel,
onBack = { onNavigate(AppScreen.Veranstaltungen) },
)
}
// Fallback → Root
else -> AdminUebersichtScreen(
onVeranstalterAuswahl = { onNavigate(AppScreen.VeranstalterAuswahl) },
@@ -0,0 +1,175 @@
package at.mocode.desktop.screens.preview
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import at.mocode.veranstalter.feature.presentation.VeranstalterAuswahlScreen
import at.mocode.veranstalter.feature.presentation.VeranstalterNeuScreen
import at.mocode.veranstalter.feature.presentation.VeranstalterDetailScreen
import at.mocode.veranstaltung.feature.presentation.VeranstaltungUebersichtScreen
import at.mocode.turnier.feature.presentation.TurnierDetailScreen
import at.mocode.turnier.feature.presentation.StammdatenTabContent
import at.mocode.turnier.feature.presentation.OrganisationTabContent
import at.mocode.turnier.feature.presentation.BewerbeTabContent
import at.mocode.turnier.feature.presentation.ArtikelTabContent
import at.mocode.turnier.feature.presentation.AbrechnungTabContent
import at.mocode.turnier.feature.presentation.NennungenTabContent
import at.mocode.turnier.feature.presentation.StartlistenTabContent
import at.mocode.turnier.feature.presentation.ErgebnislistenTabContent
// ─────────────────────────────────────────────────────────────────────────────
// Compose Desktop Previews alle wichtigen Screens auf einen Blick
//
// Verwendung: In IntelliJ IDEA / Android Studio die @Preview-Funktion öffnen
// und auf das Preview-Icon in der Gutter klicken.
// ─────────────────────────────────────────────────────────────────────────────
// ── Veranstalter-Auswahl ─────────────────────────────────────────────────────
@Preview
@Composable
fun PreviewVeranstalterAuswahlScreen() {
MaterialTheme {
VeranstalterAuswahlScreen(
onZurueck = {},
onWeiter = {},
onNeuerVeranstalter = {},
)
}
}
// ── Neuer Veranstalter ───────────────────────────────────────────────────────
@Preview
@Composable
fun PreviewVeranstalterNeuScreen() {
MaterialTheme {
VeranstalterNeuScreen(
onAbbrechen = {},
onSpeichern = { _, _, _ -> },
)
}
}
// ── Veranstalter-Detail ──────────────────────────────────────────────────────
@Preview
@Composable
fun PreviewVeranstalterDetailScreen() {
MaterialTheme {
VeranstalterDetailScreen(
veranstalterId = 1L,
onZurueck = {},
onVeranstaltungOeffnen = {},
onVeranstaltungNeu = {},
)
}
}
// ── Veranstaltung-Übersicht ──────────────────────────────────────────────────
@Preview
@Composable
fun PreviewVeranstaltungUebersichtScreen() {
MaterialTheme {
VeranstaltungUebersichtScreen(
veranstalterId = 1L,
veranstaltungId = 1L,
onZurueck = {},
onTurnierOeffnen = {},
onTurnierNeu = {},
onZnsImport = {},
onDbImport = {},
onDbExport = {},
)
}
}
// ── Turnier-Detail (alle Tabs) ───────────────────────────────────────────────
@Preview
@Composable
fun PreviewTurnierDetailScreen() {
MaterialTheme {
TurnierDetailScreen(
veranstaltungId = 1L,
turnierId = 1L,
onBack = {},
)
}
}
// ── Turnier-Tabs einzeln ─────────────────────────────────────────────────────
@Preview
@Composable
fun PreviewTurnierStammdatenTab() {
MaterialTheme {
StammdatenTabContent(turnierId = 1L)
}
}
@Preview
@Composable
fun PreviewTurnierOrganisationTab() {
MaterialTheme {
OrganisationTabContent()
}
}
@Preview
@Composable
fun PreviewTurnierBewerbeTab() {
MaterialTheme {
BewerbeTabContent()
}
}
@Preview
@Composable
fun PreviewTurnierArtikelTab() {
MaterialTheme {
ArtikelTabContent()
}
}
@Preview
@Composable
fun PreviewTurnierAbrechnungTab() {
MaterialTheme {
AbrechnungTabContent()
}
}
@Preview
@Composable
fun PreviewTurnierNennungenTab() {
MaterialTheme {
NennungenTabContent()
}
}
@Preview
@Composable
fun PreviewTurnierStartlistenTab() {
MaterialTheme {
StartlistenTabContent()
}
}
@Preview
@Composable
fun PreviewTurnierErgebnislistenTab() {
MaterialTheme {
ErgebnislistenTabContent()
}
}
// ── Stammdaten-Import ────────────────────────────────────────────────────────
// StammdatenImportScreen benötigt einen ZnsImportViewModel (Koin) Preview nur als Hinweis.
// @Preview
// @Composable
// fun PreviewStammdatenImportScreen() {
// MaterialTheme { StammdatenImportScreen() }
// }