Upgrade dependencies and refactor: Update Gradle to 9.4.0, adjust TopBar and TurnierDetailScreen UI, and add ZNS import feature to Docker build context

This commit is contained in:
Stefan Mogeritsch 2026-03-25 23:46:06 +01:00
parent b8e5065d6a
commit 49e97915e8
41 changed files with 1576 additions and 2668 deletions

View File

@ -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

View File

@ -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

View File

@ -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>
}

View File

@ -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

View File

@ -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)
---

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

View File

@ -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
}
}

View File

@ -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 {

View File

@ -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 }

View File

@ -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)) },
)
}

View File

@ -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 …",
)
}
}
}

View File

@ -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", "", 12),
VeranstalterUiModel(2L, "Pferdesportverein Linz", "Linz", "", 8),
VeranstalterUiModel(3L, "Reiterverein Salzburg", "Salzburg", "S", 5),
VeranstalterUiModel(4L, "Reitclub Wien Nord", "Wien", "W", 3),
VeranstalterUiModel(5L, "Fahrverein Graz", "Graz", "ST", 7),
)
}
val gefiltert = veranstalter.filter {
suchtext.isBlank() || it.name.contains(suchtext, ignoreCase = true) ||
it.ort.contains(suchtext, ignoreCase = true)
}
Column(modifier = Modifier.fillMaxSize()) {
// Seiten-Header
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Column {
Text(
text = "Veranstalter auswählen",
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
)
Text(
text = "Wähle einen registrierten Veranstalter aus, um eine neue Veranstaltung anzulegen.",
fontSize = 13.sp,
color = Color(0xFF6B7280),
)
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedButton(onClick = onZurueck) {
Text("Abbrechen")
}
Button(
onClick = { selectedId?.let { onWeiter(it) } },
enabled = selectedId != null,
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue),
) {
Text("Weiter zum Veranstalter")
}
}
}
HorizontalDivider()
// Suchfeld
OutlinedTextField(
value = suchtext,
onValueChange = { suchtext = it },
placeholder = { Text("Suche nach Name oder Ort...", fontSize = 13.sp) },
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp)
.height(48.dp),
singleLine = true,
)
// Tabellen-Header
Row(
modifier = Modifier
.fillMaxWidth()
.background(Color(0xFFF3F4F6))
.padding(horizontal = 16.dp, vertical = 6.dp),
) {
Text("Name", fontWeight = FontWeight.SemiBold, fontSize = 12.sp, modifier = Modifier.weight(3f))
Text("Ort", fontWeight = FontWeight.SemiBold, fontSize = 12.sp, modifier = Modifier.weight(1.5f))
Text("Bundesland", fontWeight = FontWeight.SemiBold, fontSize = 12.sp, modifier = Modifier.weight(1f))
Text("Veranstaltungen", fontWeight = FontWeight.SemiBold, fontSize = 12.sp, modifier = Modifier.weight(1f))
}
HorizontalDivider()
// Tabellen-Inhalt
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(gefiltert) { v ->
val isSelected = v.id == selectedId
Row(
modifier = Modifier
.fillMaxWidth()
.background(
if (isSelected) AccentBlue.copy(alpha = 0.1f)
else Color.Transparent
)
.clickable { selectedId = v.id }
.padding(horizontal = 16.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically,
) {
// Auswahl-Indikator
RadioButton(
selected = isSelected,
onClick = { selectedId = v.id },
colors = RadioButtonDefaults.colors(selectedColor = AccentBlue),
modifier = Modifier.size(20.dp),
)
Spacer(Modifier.width(8.dp))
Text(
text = v.name,
fontSize = 13.sp,
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal,
modifier = Modifier.weight(3f),
)
Text(v.ort, fontSize = 13.sp, modifier = Modifier.weight(1.5f))
Text(v.bundesland, fontSize = 13.sp, modifier = Modifier.weight(1f))
Text(
text = "${v.veranstaltungsAnzahl}",
fontSize = 13.sp,
modifier = Modifier.weight(1f),
)
}
HorizontalDivider(color = Color(0xFFE5E7EB))
}
}
}
}
// --- UI-Modell ---
data class VeranstalterUiModel(
val id: Long,
val name: String,
val ort: String,
val bundesland: String,
val veranstaltungsAnzahl: Int,
)

View File

@ -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 = "",
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 →")
}
}
}
}

View File

@ -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 }

Binary file not shown.

View File

@ -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
View File

@ -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/.

File diff suppressed because it is too large Load Diff