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/shells/meldestelle-portal \
|
||||
frontend/shells/meldestelle-desktop \
|
||||
frontend/features/zns-import-feature \
|
||||
docs
|
||||
|
||||
# Copy root build configuration
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ RUN mkdir -p \
|
|||
frontend/shared \
|
||||
frontend/shells/meldestelle-portal \
|
||||
frontend/shells/meldestelle-desktop \
|
||||
frontend/features/zns-import-feature \
|
||||
docs
|
||||
|
||||
# Copy root build configuration
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
package at.mocode.ping.api
|
||||
|
||||
interface PingApi {
|
||||
suspend fun simplePing(): PingResponse
|
||||
suspend fun enhancedPing(simulate: Boolean = false): EnhancedPingResponse
|
||||
suspend fun healthCheck(): HealthResponse
|
||||
suspend fun simplePing(): PingResponse
|
||||
suspend fun enhancedPing(simulate: Boolean = false): EnhancedPingResponse
|
||||
suspend fun healthCheck(): HealthResponse
|
||||
|
||||
// Neue Endpunkte für Security Hardening
|
||||
suspend fun publicPing(): PingResponse
|
||||
suspend fun securePing(): PingResponse
|
||||
// Neue Endpunkte für Security Hardening
|
||||
suspend fun publicPing(): PingResponse
|
||||
suspend fun securePing(): PingResponse
|
||||
|
||||
// Phase 3: Delta-Sync
|
||||
// Changed parameter name to 'since' to match SyncManager convention and backend controller
|
||||
suspend fun syncPings(since: Long): List<PingEvent>
|
||||
// Phase 3: Delta-Sync
|
||||
// Changed parameter name to 'since' to match SyncManager convention and backend controller
|
||||
suspend fun syncPings(since: Long): List<PingEvent>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,19 +8,19 @@ data class PingResponse(val status: String, val timestamp: String, val service:
|
|||
|
||||
@Serializable
|
||||
data class EnhancedPingResponse(
|
||||
val status: String,
|
||||
val timestamp: String,
|
||||
val service: String,
|
||||
val circuitBreakerState: String,
|
||||
val responseTime: Long
|
||||
val status: String,
|
||||
val timestamp: String,
|
||||
val service: String,
|
||||
val circuitBreakerState: String,
|
||||
val responseTime: Long
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class HealthResponse(
|
||||
val status: String,
|
||||
val timestamp: String,
|
||||
val service: String,
|
||||
val healthy: Boolean
|
||||
val status: String,
|
||||
val timestamp: String,
|
||||
val service: String,
|
||||
val healthy: Boolean
|
||||
)
|
||||
|
||||
/**
|
||||
|
|
@ -31,6 +31,6 @@ data class PingEvent(
|
|||
// Using a String for the ID to be compatible with UUIDs from the backend.
|
||||
override val id: 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
|
||||
) : Syncable
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ und über definierte Schnittstellen kommunizieren.
|
|||
→ Detaillierte Planung: `docs/01_Architecture/Roadmap_ZNS_Importer.md`
|
||||
* [x] Backend-Infrastruktur & CP850 Parser (Phase 1 – Parser/Modul)
|
||||
* [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)
|
||||
|
||||
---
|
||||
|
|
|
|||
|
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) ---
|
||||
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 object VeranstaltungNeu : AppScreen("/veranstaltung/neu")
|
||||
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 TURNIER_DETAIL = Regex("/veranstaltung/(\\d+)/turnier/(\\d+)$")
|
||||
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 {
|
||||
return when (route) {
|
||||
|
|
@ -48,6 +57,7 @@ sealed class AppScreen(val route: String) {
|
|||
"/auth/callback" -> AuthCallback
|
||||
"/nennung" -> Nennung
|
||||
"/veranstaltungen" -> Veranstaltungen
|
||||
"/veranstalter/auswahl" -> VeranstalterAuswahl
|
||||
"/veranstaltung/neu" -> VeranstaltungNeu
|
||||
"/reiter" -> Reiter
|
||||
"/pferde" -> Pferde
|
||||
|
|
@ -65,6 +75,12 @@ sealed class AppScreen(val route: String) {
|
|||
VERANSTALTUNG_DETAIL.matchEntire(route)?.destructured?.let { (id) ->
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.User
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
private class FakeNav : NavigationPort {
|
||||
var last: String? = null
|
||||
override val currentScreen: StateFlow<AppScreen> = MutableStateFlow(AppScreen.Landing)
|
||||
override fun navigateTo(route: String) {
|
||||
last = route
|
||||
}
|
||||
override fun navigateToScreen(screen: AppScreen) {
|
||||
last = screen.route
|
||||
}
|
||||
}
|
||||
|
||||
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.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
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.filled.*
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
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.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
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.
|
||||
* 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
|
||||
fun DesktopMainLayout(
|
||||
|
|
@ -31,16 +36,12 @@ fun DesktopMainLayout(
|
|||
onNavigate: (AppScreen) -> Unit,
|
||||
onLogout: () -> Unit,
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxSize()) {
|
||||
DesktopSidebar(
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
DesktopTopBar(
|
||||
currentScreen = currentScreen,
|
||||
onNavigate = onNavigate,
|
||||
onLogout = onLogout,
|
||||
)
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.fillMaxHeight().width(1.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant,
|
||||
)
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
DesktopContentArea(
|
||||
currentScreen = currentScreen,
|
||||
|
|
@ -50,182 +51,246 @@ fun DesktopMainLayout(
|
|||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sidebar
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private data class NavItem(
|
||||
val label: String,
|
||||
val icon: ImageVector,
|
||||
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),
|
||||
)
|
||||
|
||||
/**
|
||||
* TopBar: dunkelblauer Balken mit Breadcrumb-Navigation und Logout-Button.
|
||||
*
|
||||
* Breadcrumb-Logik:
|
||||
* - Root: "🏠 Admin - Verwaltung"
|
||||
* - Veranstaltung: "🏠 Admin - Verwaltung / Veranstaltung #<id>"
|
||||
* - Turnier: "🏠 Admin - Verwaltung / Veranstaltung #<id> / Turnier <tid>"
|
||||
*/
|
||||
@Composable
|
||||
private fun DesktopSidebar(
|
||||
private fun DesktopTopBar(
|
||||
currentScreen: AppScreen,
|
||||
onNavigate: (AppScreen) -> Unit,
|
||||
onLogout: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.width(220.dp)
|
||||
.fillMaxHeight()
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
.padding(vertical = 16.dp),
|
||||
.fillMaxWidth()
|
||||
.height(48.dp)
|
||||
.background(TopBarColor)
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
// 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,
|
||||
)
|
||||
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))
|
||||
}
|
||||
|
||||
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) },
|
||||
// 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 -> {}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
HorizontalDivider(modifier = Modifier.padding(horizontal = 12.dp))
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Logout
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onLogout() }
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
// Logout rechts
|
||||
IconButton(onClick = onLogout) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.Logout,
|
||||
contentDescription = "Logout",
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(
|
||||
text = "Logout",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
contentDescription = "Abmelden",
|
||||
tint = TopBarTextColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SidebarNavItem(
|
||||
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 = item.label,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal,
|
||||
),
|
||||
color = contentColor,
|
||||
)
|
||||
}
|
||||
private fun BreadcrumbSeparator() {
|
||||
Text(
|
||||
text = " / ",
|
||||
color = TopBarTextColor.copy(alpha = 0.6f),
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Content-Bereich: Screen-Routing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Content-Bereich: rendert den passenden Screen je nach aktuellem AppScreen.
|
||||
*/
|
||||
@Composable
|
||||
private fun DesktopContentArea(
|
||||
currentScreen: AppScreen,
|
||||
onNavigate: (AppScreen) -> Unit,
|
||||
) {
|
||||
val nennungViewModel: NennungViewModel = koinViewModel()
|
||||
|
||||
when (currentScreen) {
|
||||
is AppScreen.Veranstaltungen -> VeranstaltungenScreen(
|
||||
onVeranstaltungNeu = { onNavigate(AppScreen.VeranstaltungNeu) },
|
||||
// Root-Screen: Admin-Übersicht
|
||||
is AppScreen.Veranstaltungen -> AdminUebersichtScreen(
|
||||
onVeranstalterAuswahl = { onNavigate(AppScreen.VeranstalterAuswahl) },
|
||||
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(
|
||||
onBack = { onNavigate(AppScreen.Veranstaltungen) },
|
||||
onSave = { onNavigate(AppScreen.Veranstaltungen) },
|
||||
)
|
||||
|
||||
is AppScreen.VeranstaltungDetail -> VeranstaltungDetailScreen(
|
||||
veranstaltungId = currentScreen.id,
|
||||
onBack = { onNavigate(AppScreen.Veranstaltungen) },
|
||||
onTurnierNeu = { onNavigate(AppScreen.TurnierNeu(currentScreen.id)) },
|
||||
onTurnierOeffnen = { turnierId -> onNavigate(AppScreen.TurnierDetail(currentScreen.id, turnierId)) },
|
||||
// Turnier-Screens
|
||||
is AppScreen.TurnierDetail -> TurnierDetailScreen(
|
||||
veranstaltungId = currentScreen.veranstaltungId,
|
||||
turnierId = currentScreen.turnierId,
|
||||
onBack = { onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId)) },
|
||||
)
|
||||
|
||||
is AppScreen.TurnierNeu -> TurnierNeuScreen(
|
||||
veranstaltungId = currentScreen.veranstaltungId,
|
||||
onBack = { onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId)) },
|
||||
onSave = { onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId)) },
|
||||
)
|
||||
|
||||
is AppScreen.TurnierDetail -> TurnierDetailScreen(
|
||||
veranstaltungId = currentScreen.veranstaltungId,
|
||||
turnierId = currentScreen.turnierId,
|
||||
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) },
|
||||
// Fallback → Root
|
||||
else -> AdminUebersichtScreen(
|
||||
onVeranstalterAuswahl = { onNavigate(AppScreen.VeranstalterAuswahl) },
|
||||
onVeranstaltungOeffnen = { id -> onNavigate(AppScreen.VeranstaltungDetail(id)) },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,84 +1,265 @@
|
|||
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.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 at.mocode.nennung.feature.presentation.NennungViewModel
|
||||
import at.mocode.nennung.feature.presentation.NennungsMaske
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
/**
|
||||
* Detailansicht eines bestehenden Turniers (Vision_03: /veranstaltung/{id}/turnier/{tid}).
|
||||
* Tabs: Übersicht | Stammdaten (A-Satz) | Organisation | Bewerbe ⭐ | Preisliste
|
||||
* Der Bewerbe-Tab integriert die NennungsMaske aus dem nennung-feature.
|
||||
* 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,
|
||||
nennungViewModel: NennungViewModel,
|
||||
) {
|
||||
var selectedTab by remember { mutableIntStateOf(3) } // Bewerbe ist Standard-Tab (⭐)
|
||||
val tabs = listOf("Übersicht", "Stammdaten (A-Satz)", "Organisation", "Bewerbe ⭐", "Preisliste")
|
||||
var selectedTab by remember { mutableIntStateOf(0) }
|
||||
|
||||
val tabs = listOf(
|
||||
"STAMMDATEN",
|
||||
"ORGANISATION",
|
||||
"BEWERBE",
|
||||
"ARTIKEL",
|
||||
"ABRECHNUNG",
|
||||
"NENNUNGEN",
|
||||
"STARTLISTEN",
|
||||
"ERGEBNISLISTEN",
|
||||
)
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
// Horizontale Tab-Bar (direkt unter der TopBar)
|
||||
ScrollableTabRow(
|
||||
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 ->
|
||||
Tab(
|
||||
selected = 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()) {
|
||||
when (selectedTab) {
|
||||
0 -> Box(Modifier.padding(24.dp)) {
|
||||
PlaceholderContent("Übersicht", "Turnier-Stammdaten und Status.")
|
||||
}
|
||||
|
||||
1 -> Box(Modifier.padding(24.dp)) {
|
||||
PlaceholderContent("Stammdaten (A-Satz)", "OEPS-Turniernummer, Kategorie, Sparte …")
|
||||
}
|
||||
|
||||
2 -> Box(Modifier.padding(24.dp)) {
|
||||
PlaceholderContent("Organisation", "Richter, Parcourschef, Tierarzt …")
|
||||
}
|
||||
|
||||
3 -> {
|
||||
// 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)) {
|
||||
PlaceholderContent("Preisliste", "Nenngebühren pro Bewerb/Sparte …")
|
||||
}
|
||||
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 …",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
|
|||
2
gradlew
vendored
|
|
@ -57,7 +57,7 @@
|
|||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (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.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
|
|
|
|||