Upgrade dependencies and refactor: Update Gradle to 9.4.0, adjust TopBar and TurnierDetailScreen UI, and add ZNS import feature to Docker build context
|
|
@ -65,6 +65,7 @@ RUN mkdir -p \
|
||||||
frontend/shared \
|
frontend/shared \
|
||||||
frontend/shells/meldestelle-portal \
|
frontend/shells/meldestelle-portal \
|
||||||
frontend/shells/meldestelle-desktop \
|
frontend/shells/meldestelle-desktop \
|
||||||
|
frontend/features/zns-import-feature \
|
||||||
docs
|
docs
|
||||||
|
|
||||||
# Copy root build configuration
|
# Copy root build configuration
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ RUN mkdir -p \
|
||||||
frontend/shared \
|
frontend/shared \
|
||||||
frontend/shells/meldestelle-portal \
|
frontend/shells/meldestelle-portal \
|
||||||
frontend/shells/meldestelle-desktop \
|
frontend/shells/meldestelle-desktop \
|
||||||
|
frontend/features/zns-import-feature \
|
||||||
docs
|
docs
|
||||||
|
|
||||||
# Copy root build configuration
|
# Copy root build configuration
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,6 @@ data class PingEvent(
|
||||||
// Using a String for the ID to be compatible with UUIDs from the backend.
|
// Using a String for the ID to be compatible with UUIDs from the backend.
|
||||||
override val id: String,
|
override val id: String,
|
||||||
val message: String,
|
val message: String,
|
||||||
// Using a Long for the timestamp, which can be derived from a UUIDv7.
|
// Using along with the timestamp, which can be derived from a UUIDv7.
|
||||||
override val lastModified: Long
|
override val lastModified: Long
|
||||||
) : Syncable
|
) : Syncable
|
||||||
|
|
|
||||||
|
|
@ -146,7 +146,7 @@ und über definierte Schnittstellen kommunizieren.
|
||||||
→ Detaillierte Planung: `docs/01_Architecture/Roadmap_ZNS_Importer.md`
|
→ Detaillierte Planung: `docs/01_Architecture/Roadmap_ZNS_Importer.md`
|
||||||
* [x] Backend-Infrastruktur & CP850 Parser (Phase 1 – Parser/Modul)
|
* [x] Backend-Infrastruktur & CP850 Parser (Phase 1 – Parser/Modul)
|
||||||
* [x] Domain-Mapping & Upsert in DB (Phase 2)
|
* [x] Domain-Mapping & Upsert in DB (Phase 2)
|
||||||
* [ ] REST-API & Job-Management (Phase 1 – Controller/Job-Registry)
|
* [x] REST-API & Job-Management (Phase 1 – Controller/Job-Registry)
|
||||||
* [ ] Frontend-Integration mit File-Picker & Status-Polling (Phase 3)
|
* [ ] Frontend-Integration mit File-Picker & Status-Polling (Phase 3)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 143 KiB |
|
After Width: | Height: | Size: 149 KiB |
|
After Width: | Height: | Size: 149 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
|
@ -17,6 +17,13 @@ sealed class AppScreen(val route: String) {
|
||||||
|
|
||||||
// --- Desktop-Navigation (Vision_03) ---
|
// --- Desktop-Navigation (Vision_03) ---
|
||||||
data object Veranstaltungen : AppScreen("/veranstaltungen")
|
data object Veranstaltungen : AppScreen("/veranstaltungen")
|
||||||
|
|
||||||
|
// Neuer Flow: + Neue Veranstaltung → Veranstalter auswählen → Veranstalter-Detail → Veranstaltung-Übersicht
|
||||||
|
data object VeranstalterAuswahl : AppScreen("/veranstalter/auswahl")
|
||||||
|
data class VeranstalterDetail(val veranstalterId: Long) : AppScreen("/veranstalter/$veranstalterId")
|
||||||
|
data class VeranstaltungUebersicht(val veranstalterId: Long, val veranstaltungId: Long) :
|
||||||
|
AppScreen("/veranstalter/$veranstalterId/veranstaltung/$veranstaltungId")
|
||||||
|
|
||||||
data class VeranstaltungDetail(val id: Long) : AppScreen("/veranstaltung/$id")
|
data class VeranstaltungDetail(val id: Long) : AppScreen("/veranstaltung/$id")
|
||||||
data object VeranstaltungNeu : AppScreen("/veranstaltung/neu")
|
data object VeranstaltungNeu : AppScreen("/veranstaltung/neu")
|
||||||
data class TurnierDetail(val veranstaltungId: Long, val turnierId: Long) :
|
data class TurnierDetail(val veranstaltungId: Long, val turnierId: Long) :
|
||||||
|
|
@ -34,6 +41,8 @@ sealed class AppScreen(val route: String) {
|
||||||
private val VERANSTALTUNG_DETAIL = Regex("/veranstaltung/(\\d+)$")
|
private val VERANSTALTUNG_DETAIL = Regex("/veranstaltung/(\\d+)$")
|
||||||
private val TURNIER_DETAIL = Regex("/veranstaltung/(\\d+)/turnier/(\\d+)$")
|
private val TURNIER_DETAIL = Regex("/veranstaltung/(\\d+)/turnier/(\\d+)$")
|
||||||
private val TURNIER_NEU = Regex("/veranstaltung/(\\d+)/turnier/neu$")
|
private val TURNIER_NEU = Regex("/veranstaltung/(\\d+)/turnier/neu$")
|
||||||
|
private val VERANSTALTER_DETAIL = Regex("/veranstalter/(\\d+)$")
|
||||||
|
private val VERANSTALTUNG_UEBERSICHT = Regex("/veranstalter/(\\d+)/veranstaltung/(\\d+)$")
|
||||||
|
|
||||||
fun fromRoute(route: String): AppScreen {
|
fun fromRoute(route: String): AppScreen {
|
||||||
return when (route) {
|
return when (route) {
|
||||||
|
|
@ -48,6 +57,7 @@ sealed class AppScreen(val route: String) {
|
||||||
"/auth/callback" -> AuthCallback
|
"/auth/callback" -> AuthCallback
|
||||||
"/nennung" -> Nennung
|
"/nennung" -> Nennung
|
||||||
"/veranstaltungen" -> Veranstaltungen
|
"/veranstaltungen" -> Veranstaltungen
|
||||||
|
"/veranstalter/auswahl" -> VeranstalterAuswahl
|
||||||
"/veranstaltung/neu" -> VeranstaltungNeu
|
"/veranstaltung/neu" -> VeranstaltungNeu
|
||||||
"/reiter" -> Reiter
|
"/reiter" -> Reiter
|
||||||
"/pferde" -> Pferde
|
"/pferde" -> Pferde
|
||||||
|
|
@ -65,6 +75,12 @@ sealed class AppScreen(val route: String) {
|
||||||
VERANSTALTUNG_DETAIL.matchEntire(route)?.destructured?.let { (id) ->
|
VERANSTALTUNG_DETAIL.matchEntire(route)?.destructured?.let { (id) ->
|
||||||
return VeranstaltungDetail(id.toLong())
|
return VeranstaltungDetail(id.toLong())
|
||||||
}
|
}
|
||||||
|
VERANSTALTER_DETAIL.matchEntire(route)?.destructured?.let { (vId) ->
|
||||||
|
return VeranstalterDetail(vId.toLong())
|
||||||
|
}
|
||||||
|
VERANSTALTUNG_UEBERSICHT.matchEntire(route)?.destructured?.let { (verId, vId) ->
|
||||||
|
return VeranstaltungUebersicht(verId.toLong(), vId.toLong())
|
||||||
|
}
|
||||||
Landing // Default fallback
|
Landing // Default fallback
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,21 @@ package at.mocode.frontend.core.navigation
|
||||||
|
|
||||||
import at.mocode.frontend.core.domain.models.AppRoles
|
import at.mocode.frontend.core.domain.models.AppRoles
|
||||||
import at.mocode.frontend.core.domain.models.User
|
import at.mocode.frontend.core.domain.models.User
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
private class FakeNav : NavigationPort {
|
private class FakeNav : NavigationPort {
|
||||||
var last: String? = null
|
var last: String? = null
|
||||||
|
override val currentScreen: StateFlow<AppScreen> = MutableStateFlow(AppScreen.Landing)
|
||||||
override fun navigateTo(route: String) {
|
override fun navigateTo(route: String) {
|
||||||
last = route
|
last = route
|
||||||
}
|
}
|
||||||
|
override fun navigateToScreen(screen: AppScreen) {
|
||||||
|
last = screen.route
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class FakeUserProvider(private val user: User?) : CurrentUserProvider {
|
private class FakeUserProvider(private val user: User?) : CurrentUserProvider {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,348 @@
|
||||||
|
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 }
|
||||||
|
|
@ -3,27 +3,32 @@ package at.mocode.desktop.screens
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.automirrored.filled.Logout
|
import androidx.compose.material.icons.automirrored.filled.Logout
|
||||||
import androidx.compose.material.icons.filled.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import at.mocode.frontend.core.navigation.AppScreen
|
import at.mocode.frontend.core.navigation.AppScreen
|
||||||
import at.mocode.nennung.feature.presentation.NennungViewModel
|
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
// Primärfarbe der TopBar (kann später ins Theme ausgelagert werden)
|
||||||
|
private val TopBarColor = Color(0xFF1E3A8A)
|
||||||
|
private val TopBarTextColor = Color.White
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Haupt-Layout der Desktop-App gemäß Vision_03.
|
* Haupt-Layout der Desktop-App gemäß Vision_03.
|
||||||
* Sidebar (links) + Content-Bereich (rechts).
|
*
|
||||||
|
* Struktur:
|
||||||
|
* - TopBar (dunkelblau): App-Titel + Breadcrumb + Logout
|
||||||
|
* - Content: kontextabhängiger Screen
|
||||||
|
*
|
||||||
|
* Kein Nav-Rail, keine Sidebar – Navigation erfolgt über
|
||||||
|
* Breadcrumb-Klicks und horizontale Tabs innerhalb der Screens.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun DesktopMainLayout(
|
fun DesktopMainLayout(
|
||||||
|
|
@ -31,16 +36,12 @@ fun DesktopMainLayout(
|
||||||
onNavigate: (AppScreen) -> Unit,
|
onNavigate: (AppScreen) -> Unit,
|
||||||
onLogout: () -> Unit,
|
onLogout: () -> Unit,
|
||||||
) {
|
) {
|
||||||
Row(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
DesktopSidebar(
|
DesktopTopBar(
|
||||||
currentScreen = currentScreen,
|
currentScreen = currentScreen,
|
||||||
onNavigate = onNavigate,
|
onNavigate = onNavigate,
|
||||||
onLogout = onLogout,
|
onLogout = onLogout,
|
||||||
)
|
)
|
||||||
HorizontalDivider(
|
|
||||||
modifier = Modifier.fillMaxHeight().width(1.dp),
|
|
||||||
color = MaterialTheme.colorScheme.outlineVariant,
|
|
||||||
)
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
DesktopContentArea(
|
DesktopContentArea(
|
||||||
currentScreen = currentScreen,
|
currentScreen = currentScreen,
|
||||||
|
|
@ -50,182 +51,246 @@ fun DesktopMainLayout(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
/**
|
||||||
// Sidebar
|
* TopBar: dunkelblauer Balken mit Breadcrumb-Navigation und Logout-Button.
|
||||||
// ---------------------------------------------------------------------------
|
*
|
||||||
|
* Breadcrumb-Logik:
|
||||||
private data class NavItem(
|
* - Root: "🏠 Admin - Verwaltung"
|
||||||
val label: String,
|
* - Veranstaltung: "🏠 Admin - Verwaltung / Veranstaltung #<id>"
|
||||||
val icon: ImageVector,
|
* - Turnier: "🏠 Admin - Verwaltung / Veranstaltung #<id> / Turnier <tid>"
|
||||||
val screen: AppScreen,
|
*/
|
||||||
)
|
|
||||||
|
|
||||||
private val navItems = listOf(
|
|
||||||
NavItem("Veranstaltungen", Icons.Default.Event, AppScreen.Veranstaltungen),
|
|
||||||
NavItem("Reiter", Icons.Default.Person, AppScreen.Reiter),
|
|
||||||
NavItem("Pferde", Icons.Default.Star, AppScreen.Pferde),
|
|
||||||
NavItem("Funktionäre", Icons.Default.Badge, AppScreen.Funktionaere),
|
|
||||||
NavItem("Meisterschaften", Icons.Default.EmojiEvents, AppScreen.Meisterschaften),
|
|
||||||
NavItem("Cups", Icons.Default.WorkspacePremium, AppScreen.Cups),
|
|
||||||
NavItem("Stammdaten-Import", Icons.Default.CloudUpload, AppScreen.StammdatenImport),
|
|
||||||
)
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DesktopSidebar(
|
private fun DesktopTopBar(
|
||||||
currentScreen: AppScreen,
|
currentScreen: AppScreen,
|
||||||
onNavigate: (AppScreen) -> Unit,
|
onNavigate: (AppScreen) -> Unit,
|
||||||
onLogout: () -> Unit,
|
onLogout: () -> Unit,
|
||||||
) {
|
) {
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.width(220.dp)
|
|
||||||
.fillMaxHeight()
|
|
||||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
|
||||||
.padding(vertical = 16.dp),
|
|
||||||
) {
|
|
||||||
// App-Titel
|
|
||||||
Text(
|
|
||||||
text = "Meldestelle",
|
|
||||||
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold),
|
|
||||||
modifier = Modifier.padding(horizontal = 20.dp, vertical = 8.dp),
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
HorizontalDivider(modifier = Modifier.padding(horizontal = 12.dp))
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
// Navigations-Einträge
|
|
||||||
navItems.forEach { item ->
|
|
||||||
val isSelected = currentScreen::class == item.screen::class
|
|
||||||
SidebarNavItem(
|
|
||||||
item = item,
|
|
||||||
isSelected = isSelected,
|
|
||||||
onClick = { onNavigate(item.screen) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
HorizontalDivider(modifier = Modifier.padding(horizontal = 12.dp))
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
// Logout
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable { onLogout() }
|
.height(48.dp)
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
.background(TopBarColor)
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
) {
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
// Zurück-Pfeil (nur wenn nicht Root)
|
||||||
|
if (currentScreen !is AppScreen.Veranstaltungen) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = "Zurück",
|
||||||
|
tint = TopBarTextColor,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(20.dp)
|
||||||
|
.clickable { onNavigate(AppScreen.Veranstaltungen) },
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Root-Link
|
||||||
|
Text(
|
||||||
|
text = "🏠 Admin - Verwaltung",
|
||||||
|
color = TopBarTextColor,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
modifier = Modifier.clickable { onNavigate(AppScreen.Veranstaltungen) },
|
||||||
|
)
|
||||||
|
|
||||||
|
// Breadcrumb-Segmente je nach Screen
|
||||||
|
when (currentScreen) {
|
||||||
|
is AppScreen.VeranstalterAuswahl -> {
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Veranstalter auswählen",
|
||||||
|
color = TopBarTextColor,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is AppScreen.VeranstalterDetail -> {
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Veranstalter auswählen",
|
||||||
|
color = TopBarTextColor.copy(alpha = 0.75f),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
modifier = Modifier.clickable { onNavigate(AppScreen.VeranstalterAuswahl) },
|
||||||
|
)
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Veranstalter #${currentScreen.veranstalterId}",
|
||||||
|
color = TopBarTextColor,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is AppScreen.VeranstaltungUebersicht -> {
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Veranstalter auswählen",
|
||||||
|
color = TopBarTextColor.copy(alpha = 0.75f),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
modifier = Modifier.clickable { onNavigate(AppScreen.VeranstalterAuswahl) },
|
||||||
|
)
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Veranstalter #${currentScreen.veranstalterId}",
|
||||||
|
color = TopBarTextColor.copy(alpha = 0.75f),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
onNavigate(AppScreen.VeranstalterDetail(currentScreen.veranstalterId))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Veranstaltung #${currentScreen.veranstaltungId}",
|
||||||
|
color = TopBarTextColor,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is AppScreen.VeranstaltungDetail -> {
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Veranstaltung #${currentScreen.id}",
|
||||||
|
color = TopBarTextColor,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is AppScreen.VeranstaltungNeu -> {
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Neue Veranstaltung",
|
||||||
|
color = TopBarTextColor,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is AppScreen.TurnierDetail -> {
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Veranstaltung #${currentScreen.veranstaltungId}",
|
||||||
|
color = TopBarTextColor.copy(alpha = 0.75f),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Turnier ${currentScreen.turnierId}",
|
||||||
|
color = TopBarTextColor,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is AppScreen.TurnierNeu -> {
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Veranstaltung #${currentScreen.veranstaltungId}",
|
||||||
|
color = TopBarTextColor.copy(alpha = 0.75f),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Neues Turnier",
|
||||||
|
color = TopBarTextColor,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logout rechts
|
||||||
|
IconButton(onClick = onLogout) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.AutoMirrored.Filled.Logout,
|
imageVector = Icons.AutoMirrored.Filled.Logout,
|
||||||
contentDescription = "Logout",
|
contentDescription = "Abmelden",
|
||||||
tint = MaterialTheme.colorScheme.error,
|
tint = TopBarTextColor,
|
||||||
modifier = Modifier.size(20.dp),
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
|
||||||
Text(
|
|
||||||
text = "Logout",
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.error,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SidebarNavItem(
|
private fun BreadcrumbSeparator() {
|
||||||
item: NavItem,
|
|
||||||
isSelected: Boolean,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
) {
|
|
||||||
val bgColor = if (isSelected)
|
|
||||||
MaterialTheme.colorScheme.primaryContainer
|
|
||||||
else
|
|
||||||
MaterialTheme.colorScheme.surfaceVariant
|
|
||||||
|
|
||||||
val contentColor = if (isSelected)
|
|
||||||
MaterialTheme.colorScheme.onPrimaryContainer
|
|
||||||
else
|
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 8.dp, vertical = 2.dp)
|
|
||||||
.background(bgColor, RoundedCornerShape(8.dp))
|
|
||||||
.clickable { onClick() }
|
|
||||||
.padding(horizontal = 12.dp, vertical = 10.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = item.icon,
|
|
||||||
contentDescription = item.label,
|
|
||||||
tint = contentColor,
|
|
||||||
modifier = Modifier.size(20.dp),
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
|
||||||
Text(
|
Text(
|
||||||
text = item.label,
|
text = " / ",
|
||||||
style = MaterialTheme.typography.bodyMedium.copy(
|
color = TopBarTextColor.copy(alpha = 0.6f),
|
||||||
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal,
|
fontSize = 14.sp,
|
||||||
),
|
|
||||||
color = contentColor,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Content-Bereich: Screen-Routing
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Content-Bereich: rendert den passenden Screen je nach aktuellem AppScreen.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
private fun DesktopContentArea(
|
private fun DesktopContentArea(
|
||||||
currentScreen: AppScreen,
|
currentScreen: AppScreen,
|
||||||
onNavigate: (AppScreen) -> Unit,
|
onNavigate: (AppScreen) -> Unit,
|
||||||
) {
|
) {
|
||||||
val nennungViewModel: NennungViewModel = koinViewModel()
|
|
||||||
|
|
||||||
when (currentScreen) {
|
when (currentScreen) {
|
||||||
is AppScreen.Veranstaltungen -> VeranstaltungenScreen(
|
// Root-Screen: Admin-Übersicht
|
||||||
onVeranstaltungNeu = { onNavigate(AppScreen.VeranstaltungNeu) },
|
is AppScreen.Veranstaltungen -> AdminUebersichtScreen(
|
||||||
|
onVeranstalterAuswahl = { onNavigate(AppScreen.VeranstalterAuswahl) },
|
||||||
onVeranstaltungOeffnen = { id -> onNavigate(AppScreen.VeranstaltungDetail(id)) },
|
onVeranstaltungOeffnen = { id -> onNavigate(AppScreen.VeranstaltungDetail(id)) },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Neuer Flow: Veranstalter auswählen → Detail → Veranstaltung-Übersicht
|
||||||
|
is AppScreen.VeranstalterAuswahl -> VeranstalterAuswahlScreen(
|
||||||
|
onZurueck = { onNavigate(AppScreen.Veranstaltungen) },
|
||||||
|
onWeiter = { veranstalterId -> onNavigate(AppScreen.VeranstalterDetail(veranstalterId)) },
|
||||||
|
)
|
||||||
|
is AppScreen.VeranstalterDetail -> VeranstalterDetailScreen(
|
||||||
|
veranstalterId = currentScreen.veranstalterId,
|
||||||
|
onZurueck = { onNavigate(AppScreen.VeranstalterAuswahl) },
|
||||||
|
onVeranstaltungOeffnen = { vId ->
|
||||||
|
onNavigate(AppScreen.VeranstaltungUebersicht(currentScreen.veranstalterId, vId))
|
||||||
|
},
|
||||||
|
onVeranstaltungNeu = { onNavigate(AppScreen.VeranstalterDetail(currentScreen.veranstalterId)) },
|
||||||
|
)
|
||||||
|
is AppScreen.VeranstaltungUebersicht -> VeranstaltungUebersichtScreen(
|
||||||
|
veranstalterId = currentScreen.veranstalterId,
|
||||||
|
veranstaltungId = currentScreen.veranstaltungId,
|
||||||
|
onZurueck = { onNavigate(AppScreen.VeranstalterDetail(currentScreen.veranstalterId)) },
|
||||||
|
onTurnierOeffnen = { tId ->
|
||||||
|
onNavigate(AppScreen.TurnierDetail(currentScreen.veranstaltungId, tId))
|
||||||
|
},
|
||||||
|
onTurnierNeu = { onNavigate(AppScreen.TurnierNeu(currentScreen.veranstaltungId)) },
|
||||||
|
onZnsImport = { /* TODO: ZNS-Import Dialog für Turnier */ },
|
||||||
|
onDbImport = { /* TODO: DB-Import Dialog */ },
|
||||||
|
onDbExport = { /* TODO: DB-Export Dialog */ },
|
||||||
|
)
|
||||||
|
|
||||||
|
// Veranstaltungs-Screens
|
||||||
|
is AppScreen.VeranstaltungDetail -> VeranstaltungDetailScreen(
|
||||||
|
veranstaltungId = currentScreen.id,
|
||||||
|
onBack = { onNavigate(AppScreen.Veranstaltungen) },
|
||||||
|
onTurnierNeu = { onNavigate(AppScreen.TurnierNeu(currentScreen.id)) },
|
||||||
|
onTurnierOeffnen = { tid -> onNavigate(AppScreen.TurnierDetail(currentScreen.id, tid)) },
|
||||||
|
)
|
||||||
is AppScreen.VeranstaltungNeu -> VeranstaltungNeuScreen(
|
is AppScreen.VeranstaltungNeu -> VeranstaltungNeuScreen(
|
||||||
onBack = { onNavigate(AppScreen.Veranstaltungen) },
|
onBack = { onNavigate(AppScreen.Veranstaltungen) },
|
||||||
onSave = { onNavigate(AppScreen.Veranstaltungen) },
|
onSave = { onNavigate(AppScreen.Veranstaltungen) },
|
||||||
)
|
)
|
||||||
|
|
||||||
is AppScreen.VeranstaltungDetail -> VeranstaltungDetailScreen(
|
// Turnier-Screens
|
||||||
veranstaltungId = currentScreen.id,
|
is AppScreen.TurnierDetail -> TurnierDetailScreen(
|
||||||
onBack = { onNavigate(AppScreen.Veranstaltungen) },
|
veranstaltungId = currentScreen.veranstaltungId,
|
||||||
onTurnierNeu = { onNavigate(AppScreen.TurnierNeu(currentScreen.id)) },
|
turnierId = currentScreen.turnierId,
|
||||||
onTurnierOeffnen = { turnierId -> onNavigate(AppScreen.TurnierDetail(currentScreen.id, turnierId)) },
|
onBack = { onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId)) },
|
||||||
)
|
)
|
||||||
|
|
||||||
is AppScreen.TurnierNeu -> TurnierNeuScreen(
|
is AppScreen.TurnierNeu -> TurnierNeuScreen(
|
||||||
veranstaltungId = currentScreen.veranstaltungId,
|
veranstaltungId = currentScreen.veranstaltungId,
|
||||||
onBack = { onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId)) },
|
onBack = { onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId)) },
|
||||||
onSave = { onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId)) },
|
onSave = { onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId)) },
|
||||||
)
|
)
|
||||||
|
|
||||||
is AppScreen.TurnierDetail -> TurnierDetailScreen(
|
// Fallback → Root
|
||||||
veranstaltungId = currentScreen.veranstaltungId,
|
else -> AdminUebersichtScreen(
|
||||||
turnierId = currentScreen.turnierId,
|
onVeranstalterAuswahl = { onNavigate(AppScreen.VeranstalterAuswahl) },
|
||||||
onBack = { onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId)) },
|
|
||||||
nennungViewModel = nennungViewModel,
|
|
||||||
)
|
|
||||||
|
|
||||||
is AppScreen.Reiter -> ReiterScreen()
|
|
||||||
is AppScreen.Pferde -> PferdeScreen()
|
|
||||||
is AppScreen.Funktionaere -> FunktionaereScreen()
|
|
||||||
is AppScreen.Meisterschaften -> MeisterschaftenScreen()
|
|
||||||
is AppScreen.Cups -> CupsScreen()
|
|
||||||
is AppScreen.StammdatenImport -> StammdatenImportScreen()
|
|
||||||
// Fallback für alle anderen Screens (Dashboard, Ping etc.)
|
|
||||||
else -> VeranstaltungenScreen(
|
|
||||||
onVeranstaltungNeu = { onNavigate(AppScreen.VeranstaltungNeu) },
|
|
||||||
onVeranstaltungOeffnen = { id -> onNavigate(AppScreen.VeranstaltungDetail(id)) },
|
onVeranstaltungOeffnen = { id -> onNavigate(AppScreen.VeranstaltungDetail(id)) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,84 +1,265 @@
|
||||||
package at.mocode.desktop.screens
|
package at.mocode.desktop.screens
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
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.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
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.dp
|
||||||
import at.mocode.nennung.feature.presentation.NennungViewModel
|
import androidx.compose.ui.unit.sp
|
||||||
import at.mocode.nennung.feature.presentation.NennungsMaske
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detailansicht eines bestehenden Turniers (Vision_03: /veranstaltung/{id}/turnier/{tid}).
|
* Detailansicht eines Turniers gemäß Vision_03.
|
||||||
* Tabs: Übersicht | Stammdaten (A-Satz) | Organisation | Bewerbe ⭐ | Preisliste
|
*
|
||||||
* Der Bewerbe-Tab integriert die NennungsMaske aus dem nennung-feature.
|
* 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
|
@Composable
|
||||||
fun TurnierDetailScreen(
|
fun TurnierDetailScreen(
|
||||||
veranstaltungId: Long,
|
veranstaltungId: Long,
|
||||||
turnierId: Long,
|
turnierId: Long,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
nennungViewModel: NennungViewModel,
|
|
||||||
) {
|
) {
|
||||||
var selectedTab by remember { mutableIntStateOf(3) } // Bewerbe ist Standard-Tab (⭐)
|
var selectedTab by remember { mutableIntStateOf(0) }
|
||||||
val tabs = listOf("Übersicht", "Stammdaten (A-Satz)", "Organisation", "Bewerbe ⭐", "Preisliste")
|
|
||||||
|
val tabs = listOf(
|
||||||
|
"STAMMDATEN",
|
||||||
|
"ORGANISATION",
|
||||||
|
"BEWERBE",
|
||||||
|
"ARTIKEL",
|
||||||
|
"ABRECHNUNG",
|
||||||
|
"NENNUNGEN",
|
||||||
|
"STARTLISTEN",
|
||||||
|
"ERGEBNISLISTEN",
|
||||||
|
)
|
||||||
|
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
Row(
|
// Horizontale Tab-Bar (direkt unter der TopBar)
|
||||||
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
ScrollableTabRow(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
selectedTabIndex = selectedTab,
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
contentColor = Color(0xFF1E3A8A),
|
||||||
|
edgePadding = 0.dp,
|
||||||
) {
|
) {
|
||||||
IconButton(onClick = onBack) {
|
|
||||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück")
|
|
||||||
}
|
|
||||||
Spacer(Modifier.width(8.dp))
|
|
||||||
Text(
|
|
||||||
text = "Turnier #$turnierId",
|
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
PrimaryTabRow(selectedTabIndex = selectedTab) {
|
|
||||||
tabs.forEachIndexed { index, title ->
|
tabs.forEachIndexed { index, title ->
|
||||||
Tab(
|
Tab(
|
||||||
selected = selectedTab == index,
|
selected = selectedTab == index,
|
||||||
onClick = { selectedTab = index },
|
onClick = { selectedTab = index },
|
||||||
text = { Text(title) },
|
text = {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
fontWeight = if (selectedTab == index) FontWeight.Bold else FontWeight.Normal,
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HorizontalDivider()
|
||||||
|
|
||||||
|
// Tab-Inhalte
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
when (selectedTab) {
|
when (selectedTab) {
|
||||||
0 -> Box(Modifier.padding(24.dp)) {
|
0 -> StammdatenTabContent(turnierId = turnierId)
|
||||||
PlaceholderContent("Übersicht", "Turnier-Stammdaten und Status.")
|
1 -> OrganisationTabContent()
|
||||||
|
2 -> BewerbeTabContent()
|
||||||
|
3 -> ArtikelTabContent()
|
||||||
|
4 -> AbrechnungTabContent()
|
||||||
|
5 -> NennungenTabContent()
|
||||||
|
6 -> StartlistenTabContent()
|
||||||
|
7 -> ErgebnislistenTabContent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
1 -> Box(Modifier.padding(24.dp)) {
|
// ---------------------------------------------------------------------------
|
||||||
PlaceholderContent("Stammdaten (A-Satz)", "OEPS-Turniernummer, Kategorie, Sparte …")
|
// Tab-Inhalte (Placeholder – werden in späteren Phasen befüllt)
|
||||||
}
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
2 -> Box(Modifier.padding(24.dp)) {
|
@Composable
|
||||||
PlaceholderContent("Organisation", "Richter, Parcourschef, Tierarzt …")
|
private fun StammdatenTabContent(turnierId: Long) {
|
||||||
}
|
PlaceholderContent(
|
||||||
|
title = "Stammdaten – Turnier $turnierId",
|
||||||
3 -> {
|
subtitle = "Turnier-Konfiguration, ZNS-Import, Sparten, Klassen, Datum …",
|
||||||
// Nennungs-Workflow: NennungsMaske aus nennung-feature
|
|
||||||
NennungsMaske(
|
|
||||||
viewModel = nennungViewModel,
|
|
||||||
onStartlisteOeffnen = { /* TODO: Navigation zu Startliste */ },
|
|
||||||
onErgebnisseOeffnen = { /* TODO: Navigation zu Ergebnisse */ },
|
|
||||||
onAbrechnungOeffnen = { /* TODO: Navigation zu Abrechnung */ },
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
4 -> Box(Modifier.padding(24.dp)) {
|
@Composable
|
||||||
PlaceholderContent("Preisliste", "Nenngebühren pro Bewerb/Sparte …")
|
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 …",
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,172 @@
|
||||||
|
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", "OÖ", 12),
|
||||||
|
VeranstalterUiModel(2L, "Pferdesportverein Linz", "Linz", "OÖ", 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,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,213 @@
|
||||||
|
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 = "OÖ",
|
||||||
|
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 →")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,349 @@
|
||||||
|
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 }
|
||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -1,6 +1,6 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
|
||||||
2
gradlew
vendored
|
|
@ -57,7 +57,7 @@
|
||||||
# Darwin, MinGW, and NonStop.
|
# Darwin, MinGW, and NonStop.
|
||||||
#
|
#
|
||||||
# (3) This script is generated from the Groovy template
|
# (3) This script is generated from the Groovy template
|
||||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
# https://github.com/gradle/gradle/blob/b631911858264c0b6e4d6603d677ff5218766cee/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
# within the Gradle project.
|
# within the Gradle project.
|
||||||
#
|
#
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
|
|
||||||