feat: separate backend base URL configurations for release and debug builds
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 46s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 46s
This commit is contained in:
@@ -1,9 +1,13 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
val backendBaseUrl = providers.gradleProperty("backendBaseUrl")
|
||||
val backendBaseUrlForRelease = providers.gradleProperty("backendBaseUrl")
|
||||
.orElse("https://tt-tagebuch.de")
|
||||
.get()
|
||||
|
||||
val backendBaseUrlForDebug = providers.gradleProperty("backendBaseUrl")
|
||||
.orElse("http://10.0.2.2:3005")
|
||||
.get()
|
||||
|
||||
val socketBaseUrl = providers.gradleProperty("socketBaseUrl")
|
||||
.orElse("wss://tt-tagebuch.de:3051")
|
||||
.get()
|
||||
@@ -60,7 +64,7 @@ android {
|
||||
targetSdk = libs.versions.android.targetSdk.get().toInt()
|
||||
versionCode = libs.versions.appVersionCode.get().toInt()
|
||||
versionName = libs.versions.appVersionName.get()
|
||||
buildConfigField("String", "BACKEND_BASE_URL", "\"$backendBaseUrl\"")
|
||||
// BUILD CONFIGS are provided per-buildType below. SOCKET_BASE_URL remains in defaultConfig
|
||||
buildConfigField("String", "SOCKET_BASE_URL", "\"$socketBaseUrl\"")
|
||||
}
|
||||
buildFeatures {
|
||||
@@ -78,6 +82,12 @@ android {
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro",
|
||||
)
|
||||
// Release: default to production, can be overridden with -PbackendBaseUrl
|
||||
buildConfigField("String", "BACKEND_BASE_URL", "\"$backendBaseUrlForRelease\"")
|
||||
}
|
||||
getByName("debug") {
|
||||
// Debug: default to emulator localhost via 10.0.2.2, can be overridden with -PbackendBaseUrl
|
||||
buildConfigField("String", "BACKEND_BASE_URL", "\"$backendBaseUrlForDebug\"")
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
|
||||
@@ -6,12 +6,22 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.AlertDialog
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.DropdownMenu
|
||||
import androidx.compose.material.DropdownMenuItem
|
||||
@@ -29,6 +39,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.tsschulz.tt_tagebuch.app.AppDependencies
|
||||
@@ -41,6 +52,13 @@ import de.tsschulz.tt_tagebuch.shared.api.models.TournamentClassDto
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.TournamentDeleteKnockoutBody
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.TournamentExternalParticipantRowDto
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.TournamentFinishMatchBody
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.TournamentMatchTableBody
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.TournamentReopenMatchBody
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.focus.FocusState
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.TournamentStartKnockoutBody
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.TournamentMatchDto
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.TournamentParticipantRowDto
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.TournamentRemoveInternalParticipantBody
|
||||
@@ -49,6 +67,7 @@ import de.tsschulz.tt_tagebuch.shared.api.models.UpdateTournamentClassBody
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.AddExternalTournamentParticipantBody
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.RemoveExternalTournamentParticipantBody
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.InternalTournamentDetailDto
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -115,12 +134,12 @@ internal fun TournamentEditorClassesTab(
|
||||
},
|
||||
)
|
||||
}
|
||||
Column(Modifier.fillMaxSize().padding(16.dp)) {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
Button(onClick = { showAdd = true }, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(tr("tournaments.addClass", "Klasse anlegen"))
|
||||
}
|
||||
LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(top = 12.dp)) {
|
||||
items(classes, key = { it.id }) { c ->
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(top = 12.dp)) {
|
||||
classes.forEach { c ->
|
||||
ClassRowEditor(
|
||||
clubId = clubId,
|
||||
tournamentId = tournamentId,
|
||||
@@ -204,7 +223,7 @@ internal fun TournamentEditorParticipantsTab(
|
||||
val api = dependencies.tournamentsApi
|
||||
val scope = dependencies.applicationScope
|
||||
val membersState by dependencies.membersManager.state.collectAsState()
|
||||
val scroll = rememberScrollState()
|
||||
|
||||
var addMenu by remember { mutableStateOf(false) }
|
||||
var extDialog by remember { mutableStateOf(false) }
|
||||
var extFirst by remember { mutableStateOf("") }
|
||||
@@ -214,157 +233,168 @@ internal fun TournamentEditorParticipantsTab(
|
||||
dependencies.membersManager.loadMembers(clubId)
|
||||
}
|
||||
|
||||
var showInternal by remember { mutableStateOf(true) }
|
||||
var showExternal by remember { mutableStateOf(false) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scroll)
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(tr("tournaments.internalParticipants", "Vereinsmitglieder"), fontWeight = FontWeight.SemiBold)
|
||||
Row {
|
||||
OutlinedButton(onClick = { addMenu = true }) { Text(tr("tournaments.addParticipant", "Teilnehmer hinzufügen")) }
|
||||
DropdownMenu(expanded = addMenu, onDismissRequest = { addMenu = false }) {
|
||||
val existing = participants.mapNotNull { it.clubMemberId }.toSet()
|
||||
membersState.members
|
||||
.filter { it.id !in existing }
|
||||
.take(200)
|
||||
.forEach { m ->
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
addMenu = false
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(tr("tournaments.internalParticipants", "Vereinsmitglieder"), style = MaterialTheme.typography.subtitle1, fontWeight = FontWeight.SemiBold)
|
||||
TextButton(onClick = { showInternal = !showInternal }) { Text(if (showInternal) tr("mobile.hide", "Verbergen") else tr("mobile.show", "Anzeigen")) }
|
||||
}
|
||||
if (showInternal) {
|
||||
Row {
|
||||
OutlinedButton(onClick = { addMenu = true }) { Text(tr("tournaments.addParticipant", "Teilnehmer hinzufügen")) }
|
||||
DropdownMenu(expanded = addMenu, onDismissRequest = { addMenu = false }) {
|
||||
val existing = participants.mapNotNull { it.clubMemberId }.toSet()
|
||||
membersState.members
|
||||
.filter { it.id !in existing }
|
||||
.take(200)
|
||||
.forEach { m ->
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
addMenu = false
|
||||
scope.launch {
|
||||
runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
api.addInternalParticipant(
|
||||
TournamentAddInternalParticipantBody(
|
||||
clubId = clubId,
|
||||
classId = null,
|
||||
participant = m.id,
|
||||
tournamentId = tournamentId,
|
||||
),
|
||||
)
|
||||
}
|
||||
}.onFailure { onError(it.message) }
|
||||
onReload()
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text("${m.firstName} ${m.lastName}".trim())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
participants.forEach { p ->
|
||||
val label = p.member?.let { "${it.firstName.orEmpty()} ${it.lastName.orEmpty()}".trim() }.orEmpty().ifBlank { "#${p.id}" }
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(label, fontWeight = FontWeight.Medium)
|
||||
ClassPickerChipRow(
|
||||
classes = classes,
|
||||
selectedClassId = p.classId,
|
||||
tr = tr,
|
||||
onPick = { classId ->
|
||||
scope.launch {
|
||||
runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
api.addInternalParticipant(
|
||||
TournamentAddInternalParticipantBody(
|
||||
clubId = clubId,
|
||||
classId = null,
|
||||
participant = m.id,
|
||||
tournamentId = tournamentId,
|
||||
),
|
||||
api.updateParticipantClass(
|
||||
clubId,
|
||||
tournamentId,
|
||||
p.id,
|
||||
UpdateParticipantClassBody(classId = classId, isExternal = false),
|
||||
)
|
||||
}
|
||||
}.onFailure { onError(it.message) }
|
||||
onReload()
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text("${m.firstName} ${m.lastName}".trim())
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
participants.forEach { p ->
|
||||
val label = p.member?.let { "${it.firstName.orEmpty()} ${it.lastName.orEmpty()}".trim() }.orEmpty().ifBlank { "#${p.id}" }
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(label, fontWeight = FontWeight.Medium)
|
||||
ClassPickerChipRow(
|
||||
classes = classes,
|
||||
selectedClassId = p.classId,
|
||||
tr = tr,
|
||||
onPick = { classId ->
|
||||
scope.launch {
|
||||
runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
api.updateParticipantClass(
|
||||
clubId,
|
||||
tournamentId,
|
||||
p.id,
|
||||
UpdateParticipantClassBody(classId = classId, isExternal = false),
|
||||
)
|
||||
}
|
||||
}.onFailure { onError(it.message) }
|
||||
onReload()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
TextButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
api.removeInternalParticipant(
|
||||
TournamentRemoveInternalParticipantBody(clubId, tournamentId, p.id),
|
||||
)
|
||||
}
|
||||
}.onFailure { onError(it.message) }
|
||||
onReload()
|
||||
}
|
||||
},
|
||||
) { Text(tr("mobile.remove", "Entfernen")) }
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
|
||||
Text(tr("tournaments.externalParticipants", "Externe Teilnehmer"), fontWeight = FontWeight.SemiBold, modifier = Modifier.padding(top = 12.dp))
|
||||
OutlinedButton(onClick = { extDialog = true }) { Text(tr("tournaments.addExternalParticipant", "Extern hinzufügen")) }
|
||||
if (extDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { extDialog = false },
|
||||
title = { Text(tr("tournaments.addExternalParticipant", "Extern hinzufügen")) },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(extFirst, { extFirst = it }, label = { Text(tr("members.firstName", "Vorname")) })
|
||||
OutlinedTextField(extLast, { extLast = it }, label = { Text(tr("members.lastName", "Nachname")) })
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
if (extFirst.isBlank() || extLast.isBlank()) return@TextButton
|
||||
scope.launch {
|
||||
runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
api.addExternalParticipant(
|
||||
AddExternalTournamentParticipantBody(
|
||||
clubId = clubId,
|
||||
tournamentId = tournamentId,
|
||||
classId = null,
|
||||
firstName = extFirst.trim(),
|
||||
lastName = extLast.trim(),
|
||||
),
|
||||
api.removeInternalParticipant(
|
||||
TournamentRemoveInternalParticipantBody(clubId, tournamentId, p.id),
|
||||
)
|
||||
}
|
||||
}.onFailure { onError(it.message) }
|
||||
extDialog = false
|
||||
extFirst = ""
|
||||
extLast = ""
|
||||
onReload()
|
||||
}
|
||||
},
|
||||
) { Text(tr("mobile.add", "Hinzufügen")) }
|
||||
},
|
||||
dismissButton = { TextButton(onClick = { extDialog = false }) { Text(tr("common.cancel", "Abbrechen")) } },
|
||||
)
|
||||
) { Text(tr("mobile.remove", "Entfernen")) }
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
externalParticipants.forEach { e ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text("${e.firstName.orEmpty()} ${e.lastName.orEmpty()} (${tr("tournaments.externalShort", "ext.")})".trim())
|
||||
TextButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
api.removeExternalParticipant(
|
||||
RemoveExternalTournamentParticipantBody(clubId, tournamentId, e.id),
|
||||
)
|
||||
}
|
||||
}.onFailure { onError(it.message) }
|
||||
onReload()
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth().padding(top = 8.dp)) {
|
||||
Text(tr("tournaments.externalParticipants", "Externe Teilnehmer"), style = MaterialTheme.typography.subtitle1, fontWeight = FontWeight.SemiBold)
|
||||
TextButton(onClick = { showExternal = !showExternal }) { Text(if (showExternal) tr("mobile.hide", "Verbergen") else tr("mobile.show", "Anzeigen")) }
|
||||
}
|
||||
if (showExternal) {
|
||||
OutlinedButton(onClick = { extDialog = true }) { Text(tr("tournaments.addExternalParticipant", "Extern hinzufügen")) }
|
||||
if (extDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { extDialog = false },
|
||||
title = { Text(tr("tournaments.addExternalParticipant", "Extern hinzufügen")) },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(extFirst, { extFirst = it }, label = { Text(tr("members.firstName", "Vorname")) })
|
||||
OutlinedTextField(extLast, { extLast = it }, label = { Text(tr("members.lastName", "Nachname")) })
|
||||
}
|
||||
},
|
||||
) { Text(tr("mobile.remove", "Entfernen")) }
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
if (extFirst.isBlank() || extLast.isBlank()) return@TextButton
|
||||
scope.launch {
|
||||
runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
api.addExternalParticipant(
|
||||
AddExternalTournamentParticipantBody(
|
||||
clubId = clubId,
|
||||
tournamentId = tournamentId,
|
||||
classId = null,
|
||||
firstName = extFirst.trim(),
|
||||
lastName = extLast.trim(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}.onFailure { onError(it.message) }
|
||||
extDialog = false
|
||||
extFirst = ""
|
||||
extLast = ""
|
||||
onReload()
|
||||
}
|
||||
},
|
||||
) { Text(tr("mobile.add", "Hinzufügen")) }
|
||||
},
|
||||
dismissButton = { TextButton(onClick = { extDialog = false }) { Text(tr("common.cancel", "Abbrechen")) } },
|
||||
)
|
||||
}
|
||||
externalParticipants.forEach { e ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text("${e.firstName.orEmpty()} ${e.lastName.orEmpty()} (${tr("tournaments.externalShort", "ext.")})".trim())
|
||||
TextButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
api.removeExternalParticipant(
|
||||
RemoveExternalTournamentParticipantBody(clubId, tournamentId, e.id),
|
||||
)
|
||||
}
|
||||
}.onFailure { onError(it.message) }
|
||||
onReload()
|
||||
}
|
||||
},
|
||||
) { Text(tr("mobile.remove", "Entfernen")) }
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -393,20 +423,21 @@ internal fun TournamentEditorMatchesTab(
|
||||
tournamentId: Int,
|
||||
matches: List<TournamentMatchDto>,
|
||||
winningSets: Int,
|
||||
detail: InternalTournamentDetailDto,
|
||||
tr: (String, String) -> String,
|
||||
api: TournamentsApi,
|
||||
scope: CoroutineScope,
|
||||
onReload: () -> Unit,
|
||||
onError: (String?) -> Unit,
|
||||
) {
|
||||
val scroll = rememberScrollState()
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scroll)
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
var confirmDelete by remember { mutableStateOf<Pair<Int, Int>?>(null) }
|
||||
val hasKO = matches.any { (it.round ?: "").lowercase() != "group" && (it.round ?: "").isNotBlank() }
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
@@ -420,24 +451,214 @@ internal fun TournamentEditorMatchesTab(
|
||||
}
|
||||
},
|
||||
) { Text(tr("tournaments.resetKnockoutShort", "KO löschen")) }
|
||||
}
|
||||
val grouped = matches.groupBy { it.round }
|
||||
grouped.entries.sortedBy { it.key }.forEach { (round, list) ->
|
||||
Text("$round (${list.size})", fontWeight = FontWeight.SemiBold, style = MaterialTheme.typography.subtitle1)
|
||||
list.forEach { m ->
|
||||
MatchResultRow(
|
||||
clubId = clubId,
|
||||
tournamentId = tournamentId,
|
||||
m = m,
|
||||
winningSets = winningSets,
|
||||
tr = tr,
|
||||
api = api,
|
||||
scope = scope,
|
||||
onReload = onReload,
|
||||
onError = onError,
|
||||
)
|
||||
if (!hasKO) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
api.startKnockout(TournamentStartKnockoutBody(clubId, tournamentId))
|
||||
}
|
||||
}.onFailure { onError(it.message) }
|
||||
onReload()
|
||||
}
|
||||
},
|
||||
) { Text(tr("tournaments.startKnockout", "K.-o. starten")) }
|
||||
}
|
||||
}
|
||||
// Table-like view: show header + flattened rows so Android resembles web UI
|
||||
val displayList = matches.sortedWith(compareBy<TournamentMatchDto> {
|
||||
it.isFinished == true
|
||||
}.thenBy {
|
||||
// primary: if groupRound available use it, else try parse round as int, else large
|
||||
it.groupRound ?: it.round.toIntOrNull() ?: Int.MAX_VALUE
|
||||
}.thenBy { it.groupId ?: Int.MAX_VALUE }.thenBy { it.groupRound ?: 0 })
|
||||
|
||||
// header
|
||||
Row(modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text("Runde", fontWeight = FontWeight.SemiBold, modifier = Modifier.width(56.dp))
|
||||
Text(tr("tournaments.group", "Gruppe"), fontWeight = FontWeight.SemiBold, modifier = Modifier.width(96.dp))
|
||||
Text(tr("tournaments.match", "Begegnung"), fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
|
||||
Text(tr("tournaments.result", "Ergebnis"), fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(0.7f))
|
||||
Text("Sätze", fontWeight = FontWeight.SemiBold, modifier = Modifier.width(72.dp))
|
||||
Text("Aktion", fontWeight = FontWeight.SemiBold, modifier = Modifier.width(96.dp))
|
||||
}
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
displayList.forEach { m ->
|
||||
var resultInput by remember(m.id, m.tournamentResults) { mutableStateOf("") }
|
||||
var resultError by remember(m.id, m.tournamentResults) { mutableStateOf<String?>(null) }
|
||||
var wasResultFocused by remember(m.id) { mutableStateOf(false) }
|
||||
var isSavingResult by remember(m.id) { mutableStateOf(false) }
|
||||
fun saveResultInput() {
|
||||
if (isSavingResult) return
|
||||
if (resultInput.isBlank()) {
|
||||
resultError = null
|
||||
return
|
||||
}
|
||||
val normalized = normalizeResult(resultInput)
|
||||
if (normalized == null) {
|
||||
resultError = tr("tournaments.invalidResultInput", "Ungültiges Ergebnis")
|
||||
onError(tr("tournaments.invalidResultInputHint", "Bitte ein gültiges Ergebnis eingeben, z.B. 11:7, 7 oder -7."))
|
||||
return
|
||||
}
|
||||
val nextSet = (m.tournamentResults?.maxOfOrNull { it.set } ?: 0) + 1
|
||||
isSavingResult = true
|
||||
scope.launch {
|
||||
runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
api.addMatchResult(
|
||||
TournamentAddMatchResultBody(
|
||||
clubId = clubId,
|
||||
tournamentId = tournamentId,
|
||||
matchId = m.id,
|
||||
set = nextSet,
|
||||
result = normalized,
|
||||
),
|
||||
)
|
||||
}
|
||||
}.fold(
|
||||
onSuccess = {
|
||||
resultInput = ""
|
||||
resultError = null
|
||||
isSavingResult = false
|
||||
onReload()
|
||||
},
|
||||
onFailure = {
|
||||
resultError = it.message ?: tr("messages.error", "Fehler")
|
||||
isSavingResult = false
|
||||
onError(resultError)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
val roundLabel = m.groupRound?.toString() ?: m.round.takeIf { it.isNotBlank() } ?: "-"
|
||||
val groupLabel = m.groupId?.let { "${tr("tournaments.group", "Gruppe")} $it" } ?: "-"
|
||||
Row(modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(roundLabel, modifier = Modifier.width(56.dp))
|
||||
Text(groupLabel, modifier = Modifier.width(96.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text("${displayNameFromPlayerJson(m.player1)} – ${displayNameFromPlayerJson(m.player2)}", style = MaterialTheme.typography.body2)
|
||||
val results = m.tournamentResults
|
||||
if (!results.isNullOrEmpty()) {
|
||||
results.sortedBy { it.set }.forEach { r ->
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
val a = r.pointsPlayer1?.toString() ?: "-"
|
||||
val b = r.pointsPlayer2?.toString() ?: "-"
|
||||
Text("${r.set}: $a:$b", style = MaterialTheme.typography.caption)
|
||||
if (m.isFinished != true) {
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
IconButton(onClick = { confirmDelete = Pair(m.id, r.set) }, modifier = Modifier.size(28.dp)) {
|
||||
Icon(
|
||||
Icons.Filled.Close,
|
||||
contentDescription = "Löschen",
|
||||
tint = MaterialTheme.colors.error,
|
||||
modifier = Modifier.size(18.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Column(modifier = Modifier.weight(0.7f)) {
|
||||
if (m.isFinished == true) {
|
||||
Text(formatMatchSets(m))
|
||||
} else if (m.result == "BYE") {
|
||||
Text("BYE")
|
||||
} else {
|
||||
OutlinedTextField(
|
||||
value = resultInput,
|
||||
onValueChange = {
|
||||
resultInput = it
|
||||
resultError = null
|
||||
},
|
||||
placeholder = { Text(tr("tournaments.newSetPlaceholder", "11:7")) },
|
||||
singleLine = true,
|
||||
isError = resultError != null,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Text,
|
||||
imeAction = ImeAction.Done,
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { saveResultInput() }),
|
||||
modifier = Modifier
|
||||
.width(86.dp)
|
||||
.onFocusChanged { focusState ->
|
||||
if (wasResultFocused && !focusState.isFocused) {
|
||||
saveResultInput()
|
||||
}
|
||||
wasResultFocused = focusState.isFocused
|
||||
},
|
||||
)
|
||||
resultError?.let {
|
||||
Text(it, color = MaterialTheme.colors.error, style = MaterialTheme.typography.caption)
|
||||
}
|
||||
}
|
||||
}
|
||||
Text(m.tournamentResults?.size?.toString() ?: "0", modifier = Modifier.width(72.dp))
|
||||
// actions column
|
||||
Column(modifier = Modifier.width(96.dp)) {
|
||||
if (m.isFinished != true) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
TextButton(onClick = {
|
||||
// finish
|
||||
scope.launch {
|
||||
runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
api.finishMatch(TournamentFinishMatchBody(clubId, tournamentId, m.id))
|
||||
}
|
||||
}.onFailure { onError(it.message) }
|
||||
onReload()
|
||||
}
|
||||
}) { Text(tr("tournaments.finishMatchShort", "Fertig")) }
|
||||
}
|
||||
} else {
|
||||
TextButton(onClick = {
|
||||
scope.launch {
|
||||
runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
api.reopenMatch(TournamentReopenMatchBody(clubId, tournamentId, m.id))
|
||||
}
|
||||
}.onFailure { onError(it.message) }
|
||||
onReload()
|
||||
}
|
||||
}) { Text(tr("tournaments.correct", "Korrigieren")) }
|
||||
}
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
// Confirmation dialog for deleting a set
|
||||
if (confirmDelete != null) {
|
||||
val (matchIdToDelete, setToDelete) = confirmDelete!!
|
||||
AlertDialog(
|
||||
onDismissRequest = { confirmDelete = null },
|
||||
title = { Text(tr("tournaments.confirmDeleteSetTitle", "Satz löschen")) },
|
||||
text = { Text(tr("tournaments.confirmDeleteSet", "Soll dieser Satz wirklich gelöscht werden?")) },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
scope.launch {
|
||||
runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
api.deleteMatchResult(de.tsschulz.tt_tagebuch.shared.api.models.TournamentDeleteMatchResultBody(
|
||||
clubId = clubId,
|
||||
tournamentId = tournamentId,
|
||||
matchId = matchIdToDelete,
|
||||
set = setToDelete,
|
||||
))
|
||||
}
|
||||
}.onFailure { onError(it.message) }
|
||||
confirmDelete = null
|
||||
onReload()
|
||||
}
|
||||
}) { Text(tr("common.delete", "Löschen")) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { confirmDelete = null }) { Text(tr("common.cancel", "Abbrechen")) }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -447,6 +668,7 @@ private fun MatchResultRow(
|
||||
tournamentId: Int,
|
||||
m: TournamentMatchDto,
|
||||
@Suppress("UNUSED_PARAMETER") winningSets: Int,
|
||||
maxTables: Int?,
|
||||
tr: (String, String) -> String,
|
||||
api: TournamentsApi,
|
||||
scope: CoroutineScope,
|
||||
@@ -458,19 +680,50 @@ private fun MatchResultRow(
|
||||
val p2 = displayNameFromPlayerJson(m.player2)
|
||||
Column(modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp)) {
|
||||
Text("$p1 vs $p2", style = MaterialTheme.typography.body2)
|
||||
val setsText = m.tournamentResults?.joinToString(", ") { r ->
|
||||
val a = r.pointsPlayer1?.toString() ?: "-"
|
||||
val b = r.pointsPlayer2?.toString() ?: "-"
|
||||
"$a:$b"
|
||||
} ?: ""
|
||||
if (setsText.isNotBlank()) {
|
||||
Text(setsText, style = MaterialTheme.typography.body2)
|
||||
}
|
||||
Text(
|
||||
"${tr("tournaments.result", "Ergebnis")}: ${m.result ?: "—"} ${if (m.isFinished == true) "✓" else ""}",
|
||||
style = MaterialTheme.typography.caption,
|
||||
)
|
||||
if (m.isFinished != true) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(top = 4.dp)) {
|
||||
Row(modifier = Modifier.padding(top = 4.dp).fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(
|
||||
value = resultInput,
|
||||
onValueChange = { resultInput = it },
|
||||
modifier = Modifier.weight(1f),
|
||||
placeholder = { Text("11:9") },
|
||||
placeholder = { Text(tr("tournaments.newSetPlaceholder", "Neuen Satz, z.B. 11:7")) },
|
||||
singleLine = true,
|
||||
)
|
||||
var tableExpanded by remember { mutableStateOf(false) }
|
||||
val max = maxTables ?: 12
|
||||
Box {
|
||||
OutlinedButton(onClick = { tableExpanded = true }, modifier = Modifier.width(84.dp)) {
|
||||
Text(m.tableNumber?.toString() ?: "-")
|
||||
}
|
||||
DropdownMenu(expanded = tableExpanded, onDismissRequest = { tableExpanded = false }) {
|
||||
for (i in 1..max) {
|
||||
DropdownMenuItem(onClick = {
|
||||
tableExpanded = false
|
||||
scope.launch {
|
||||
runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
api.setMatchTable(clubId, tournamentId, m.id, TournamentMatchTableBody(i))
|
||||
}
|
||||
}.onFailure { onError(it.message) }
|
||||
onReload()
|
||||
}
|
||||
}) { Text(i.toString()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.weight(0.1f))
|
||||
TextButton(
|
||||
onClick = {
|
||||
val r = normalizeResult(resultInput) ?: return@TextButton
|
||||
@@ -509,6 +762,22 @@ private fun MatchResultRow(
|
||||
},
|
||||
) { Text(tr("tournaments.finishMatchShort", "Fertig")) }
|
||||
}
|
||||
} else {
|
||||
// finished -> allow reopening for correction
|
||||
Row(modifier = Modifier.padding(top = 6.dp)) {
|
||||
Text(tr("tournaments.table", "Tisch") + ": ${m.tableNumber ?: "-"}")
|
||||
Spacer(Modifier.weight(1f))
|
||||
TextButton(onClick = {
|
||||
scope.launch {
|
||||
runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
api.reopenMatch(TournamentReopenMatchBody(clubId, tournamentId, m.id))
|
||||
}
|
||||
}.onFailure { onError(it.message) }
|
||||
onReload()
|
||||
}
|
||||
}) { Text(tr("tournaments.correct", "Korrigieren")) }
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
@@ -516,13 +785,36 @@ private fun MatchResultRow(
|
||||
|
||||
private fun normalizeResult(s: String): String? {
|
||||
val t = s.trim()
|
||||
if (!t.contains(':')) return null
|
||||
val parts = t.split(':')
|
||||
if (parts.size != 2) return null
|
||||
val a = parts[0].trim().toIntOrNull() ?: return null
|
||||
val b = parts[1].trim().toIntOrNull() ?: return null
|
||||
if (a < 0 || b < 0) return null
|
||||
return "$a:$b"
|
||||
if (t.contains(':')) {
|
||||
val parts = t.split(':')
|
||||
if (parts.size != 2) return null
|
||||
val a = parts[0].trim().toIntOrNull() ?: return null
|
||||
val b = parts[1].trim().toIntOrNull() ?: return null
|
||||
if (a < 0 || b < 0) return null
|
||||
if ((a < 11 && b < 11) || kotlin.math.abs(a - b) < 2) return null
|
||||
return "$a:$b"
|
||||
}
|
||||
|
||||
val value = t.toIntOrNull() ?: return null
|
||||
val losing = kotlin.math.abs(value)
|
||||
val winning = if (losing < 10) 11 else losing + 2
|
||||
return if (t.startsWith("-")) {
|
||||
"$losing:$winning"
|
||||
} else {
|
||||
"$winning:$losing"
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatMatchSets(match: TournamentMatchDto): String {
|
||||
val results = match.tournamentResults.orEmpty()
|
||||
if (results.isEmpty()) return match.result ?: "—"
|
||||
return results
|
||||
.sortedBy { it.set }
|
||||
.joinToString(", ") { result ->
|
||||
val p1 = result.pointsPlayer1?.let { kotlin.math.abs(it).toString() } ?: "-"
|
||||
val p2 = result.pointsPlayer2?.let { kotlin.math.abs(it).toString() } ?: "-"
|
||||
"$p1:$p2"
|
||||
}
|
||||
}
|
||||
|
||||
private fun displayNameFromPlayerJson(el: JsonElement?): String {
|
||||
@@ -572,7 +864,7 @@ internal fun TournamentEditorPairingsTab(
|
||||
}.getOrElse { null }
|
||||
}
|
||||
|
||||
Column(Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(16.dp)) {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
if (doublesClasses.isEmpty()) {
|
||||
Text(tr("tournaments.noDoublesClasses", "Keine Doppel-Klassen."))
|
||||
return@Column
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.AlertDialog
|
||||
@@ -56,6 +57,10 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import android.util.Log
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.focus.FocusState
|
||||
@@ -89,11 +94,12 @@ internal fun InternalTournamentEditorScreen(
|
||||
val showPairingsTab = detail?.isDoublesTournament == true
|
||||
val tabTitles = remember(showPairingsTab, languageCode) {
|
||||
buildList {
|
||||
add(tr("mobile.tournamentEditorTabMeta", "Stammdaten"))
|
||||
add(tr("mobile.tournamentEditorTabFlow", "Ablauf & Gruppen"))
|
||||
add(tr("mobile.tournamentEditorTabClasses", "Klassen"))
|
||||
add(tr("mobile.tournamentEditorTabParticipants", "Teilnehmer"))
|
||||
add(tr("mobile.tournamentEditorTabMatches", "Spiele"))
|
||||
// Match web ordering: Config, Participants, Groups, Results, Placements, (Pairings)
|
||||
add(tr("tournaments.tabConfig", "Konfiguration"))
|
||||
add(tr("tournaments.tabParticipants", "Teilnehmer"))
|
||||
add(tr("tournaments.tabGroups", "Gruppen"))
|
||||
add(tr("tournaments.tabResults", "Ergebnisse"))
|
||||
add(tr("tournaments.tabPlacements", "Platzierungen"))
|
||||
if (showPairingsTab) add(tr("tournaments.pairings", "Doppel-Paarungen"))
|
||||
}
|
||||
}
|
||||
@@ -118,7 +124,12 @@ internal fun InternalTournamentEditorScreen(
|
||||
}
|
||||
}.fold(
|
||||
onSuccess = { r ->
|
||||
detail = r.detail
|
||||
detail = r.detail
|
||||
try {
|
||||
Log.d("InternalTournamentEditor", "Loaded detail.numberOfTables=${'$'}{r.detail.numberOfTables}")
|
||||
} catch (e: Exception) {
|
||||
// ignore
|
||||
}
|
||||
classes = r.classes
|
||||
participants = r.participants
|
||||
externalParticipants = r.external
|
||||
@@ -186,9 +197,15 @@ internal fun InternalTournamentEditorScreen(
|
||||
Divider()
|
||||
|
||||
when (tabIndex) {
|
||||
// 0: Config (Meta with integrated Classes management)
|
||||
0 -> TournamentEditorMetaTab(
|
||||
clubId = clubId,
|
||||
tournamentId = tournamentId,
|
||||
detail = d,
|
||||
classes = classes,
|
||||
tr = ::tr,
|
||||
api = api,
|
||||
scope = scope,
|
||||
onSave = { body ->
|
||||
scope.launch {
|
||||
runCatching {
|
||||
@@ -202,8 +219,25 @@ internal fun InternalTournamentEditorScreen(
|
||||
reloadAll()
|
||||
}
|
||||
},
|
||||
onReload = { reloadAll() },
|
||||
onError = { error = it },
|
||||
)
|
||||
1 -> TournamentEditorFlowTab(
|
||||
// 1: Participants (web's second tab)
|
||||
1 -> Column(Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(16.dp)) {
|
||||
TournamentEditorParticipantsTab(
|
||||
clubId = clubId,
|
||||
tournamentId = tournamentId,
|
||||
classes = classes,
|
||||
participants = participants,
|
||||
externalParticipants = externalParticipants,
|
||||
dependencies = dependencies,
|
||||
tr = ::tr,
|
||||
onReload = { reloadAll() },
|
||||
onError = { error = it },
|
||||
)
|
||||
}
|
||||
// 2: Groups / Flow
|
||||
2 -> TournamentEditorFlowTab(
|
||||
clubId = clubId,
|
||||
tournamentId = tournamentId,
|
||||
detail = d,
|
||||
@@ -214,51 +248,41 @@ internal fun InternalTournamentEditorScreen(
|
||||
onReload = { reloadAll() },
|
||||
onError = { error = it },
|
||||
)
|
||||
2 -> TournamentEditorClassesTab(
|
||||
clubId = clubId,
|
||||
tournamentId = tournamentId,
|
||||
classes = classes,
|
||||
tr = ::tr,
|
||||
api = api,
|
||||
scope = scope,
|
||||
onReload = { reloadAll() },
|
||||
onError = { error = it },
|
||||
)
|
||||
3 -> TournamentEditorParticipantsTab(
|
||||
clubId = clubId,
|
||||
tournamentId = tournamentId,
|
||||
classes = classes,
|
||||
participants = participants,
|
||||
externalParticipants = externalParticipants,
|
||||
dependencies = dependencies,
|
||||
tr = ::tr,
|
||||
onReload = { reloadAll() },
|
||||
onError = { error = it },
|
||||
)
|
||||
4 -> TournamentEditorMatchesTab(
|
||||
clubId = clubId,
|
||||
tournamentId = tournamentId,
|
||||
matches = matches,
|
||||
winningSets = d.winningSets ?: 3,
|
||||
tr = ::tr,
|
||||
api = api,
|
||||
scope = scope,
|
||||
onReload = { reloadAll() },
|
||||
onError = { error = it },
|
||||
)
|
||||
5 -> if (showPairingsTab) {
|
||||
TournamentEditorPairingsTab(
|
||||
// 3: Results / Matches
|
||||
3 -> Column(Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(16.dp)) {
|
||||
TournamentEditorMatchesTab(
|
||||
clubId = clubId,
|
||||
tournamentId = tournamentId,
|
||||
classes = classes,
|
||||
participants = participants,
|
||||
externalParticipants = externalParticipants,
|
||||
matches = matches,
|
||||
winningSets = d.winningSets ?: 3,
|
||||
detail = d,
|
||||
tr = ::tr,
|
||||
api = api,
|
||||
scope = scope,
|
||||
onReload = { reloadAll() },
|
||||
onError = { error = it },
|
||||
)
|
||||
}
|
||||
// 4: Placements (placeholder for now)
|
||||
4 -> TournamentEditorPlacementsTab(
|
||||
tr = ::tr,
|
||||
)
|
||||
// 5: Pairings (optional)
|
||||
5 -> if (showPairingsTab) {
|
||||
Column(Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(16.dp)) {
|
||||
TournamentEditorPairingsTab(
|
||||
clubId = clubId,
|
||||
tournamentId = tournamentId,
|
||||
classes = classes,
|
||||
participants = participants,
|
||||
externalParticipants = externalParticipants,
|
||||
tr = ::tr,
|
||||
api = api,
|
||||
scope = scope,
|
||||
onReload = { reloadAll() },
|
||||
onError = { error = it },
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Spacer(Modifier.height(16.dp))
|
||||
}
|
||||
@@ -267,6 +291,23 @@ internal fun InternalTournamentEditorScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TournamentEditorPlacementsTab(
|
||||
tr: (String, String) -> String,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(tr("tournaments.placementsPendingTitle", "Platzierungen noch nicht verfügbar"), fontWeight = FontWeight.SemiBold)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(tr("tournaments.noPlacementsYet", "Noch keine Platzierungen vorhanden."))
|
||||
}
|
||||
}
|
||||
|
||||
private data class TournamentEditorLoadResult(
|
||||
val detail: InternalTournamentDetailDto,
|
||||
val classes: List<TournamentClassDto>,
|
||||
@@ -278,9 +319,16 @@ private data class TournamentEditorLoadResult(
|
||||
|
||||
@Composable
|
||||
private fun TournamentEditorMetaTab(
|
||||
clubId: Int,
|
||||
tournamentId: Int,
|
||||
detail: InternalTournamentDetailDto,
|
||||
classes: List<TournamentClassDto>,
|
||||
tr: (String, String) -> String,
|
||||
api: de.tsschulz.tt_tagebuch.shared.api.TournamentsApi,
|
||||
scope: kotlinx.coroutines.CoroutineScope,
|
||||
onSave: (UpdateTournamentMetaBody) -> Unit,
|
||||
onReload: () -> Unit,
|
||||
onError: (String?) -> Unit,
|
||||
) {
|
||||
var name by remember(detail.id) { mutableStateOf(detail.name.orEmpty()) }
|
||||
var date by remember(detail.id) { mutableStateOf(detail.date.orEmpty()) }
|
||||
@@ -299,56 +347,81 @@ private fun TournamentEditorMetaTab(
|
||||
),
|
||||
)
|
||||
}
|
||||
val scroll = rememberScrollState()
|
||||
var showClasses by remember { mutableStateOf(false) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scroll)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Text(tr("tournaments.tournamentName", "Turniername"), fontWeight = FontWeight.SemiBold)
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
modifier = Modifier.fillMaxWidth().onFocusChanged { fs: FocusState -> if (!fs.isFocused) saveIfChanged(name, date, winningSets, tables, doubles, lastSaved, onSave) },
|
||||
singleLine = true,
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = date,
|
||||
onValueChange = { date = it },
|
||||
modifier = Modifier.fillMaxWidth().onFocusChanged { fs: FocusState -> if (!fs.isFocused) saveIfChanged(name, date, winningSets, tables, doubles, lastSaved, onSave) },
|
||||
label = { Text(tr("tournaments.date", "Datum")) },
|
||||
singleLine = true,
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = winningSets,
|
||||
onValueChange = { winningSets = it.filter { ch -> ch.isDigit() }.take(2) },
|
||||
modifier = Modifier.fillMaxWidth().onFocusChanged { fs: FocusState -> if (!fs.isFocused) saveIfChanged(name, date, winningSets, tables, doubles, lastSaved, onSave) },
|
||||
label = { Text(tr("tournaments.winningSets", "Gewinnsätze")) },
|
||||
singleLine = true,
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = tables,
|
||||
onValueChange = { tables = it.filter { ch -> ch.isDigit() } },
|
||||
modifier = Modifier.fillMaxWidth().onFocusChanged { fs: FocusState -> if (!fs.isFocused) saveIfChanged(name, date, winningSets, tables, doubles, lastSaved, onSave) },
|
||||
label = { Text(tr("mobile.tables", "Tische")) },
|
||||
singleLine = true,
|
||||
)
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(tr("mobile.doublesTournament", "Doppel-Turnier"))
|
||||
Switch(checked = doubles, onCheckedChange = {
|
||||
doubles = it
|
||||
saveIfChanged(name, date, winningSets, tables, doubles, lastSaved, onSave)
|
||||
})
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(tr("tournaments.metaSectionTitle", "Stammdaten"), style = MaterialTheme.typography.h6, fontWeight = FontWeight.SemiBold)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(tr("tournaments.tournamentName", "Turniername"), fontWeight = FontWeight.SemiBold)
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
modifier = Modifier.fillMaxWidth().onFocusChanged { fs: FocusState -> if (!fs.isFocused) saveIfChanged(name, date, winningSets, tables, doubles, lastSaved, onSave) },
|
||||
singleLine = true,
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = date,
|
||||
onValueChange = { date = it },
|
||||
modifier = Modifier.fillMaxWidth().onFocusChanged { fs: FocusState -> if (!fs.isFocused) saveIfChanged(name, date, winningSets, tables, doubles, lastSaved, onSave) },
|
||||
label = { Text(tr("tournaments.date", "Datum")) },
|
||||
singleLine = true,
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = winningSets,
|
||||
onValueChange = { winningSets = it.filter { ch -> ch.isDigit() }.take(2) },
|
||||
modifier = Modifier.fillMaxWidth().onFocusChanged { fs: FocusState -> if (!fs.isFocused) saveIfChanged(name, date, winningSets, tables, doubles, lastSaved, onSave) },
|
||||
label = { Text(tr("tournaments.winningSets", "Gewinnsätze")) },
|
||||
singleLine = true,
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = tables,
|
||||
onValueChange = { tables = it.filter { ch -> ch.isDigit() } },
|
||||
modifier = Modifier.fillMaxWidth().onFocusChanged { fs: FocusState -> if (!fs.isFocused) saveIfChanged(name, date, winningSets, tables, doubles, lastSaved, onSave) },
|
||||
label = { Text(tr("tournaments.numberOfTables", "Tische")) },
|
||||
singleLine = true,
|
||||
)
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(tr("mobile.doublesTournament", "Doppel-Turnier"))
|
||||
Switch(checked = doubles, onCheckedChange = {
|
||||
doubles = it
|
||||
saveIfChanged(name, date, winningSets, tables, doubles, lastSaved, onSave)
|
||||
})
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
saveIfChanged(name, date, winningSets, tables, doubles, lastSaved, onSave, force = true)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(tr("mobile.save", "Speichern"))
|
||||
}
|
||||
Divider(modifier = Modifier.padding(vertical = 12.dp))
|
||||
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(tr("tournaments.classesSection", "Klassen"), style = MaterialTheme.typography.subtitle1, fontWeight = FontWeight.SemiBold)
|
||||
TextButton(onClick = { showClasses = !showClasses }) {
|
||||
Text(if (showClasses) tr("mobile.hide", "Verbergen") else tr("mobile.show", "Anzeigen"))
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
saveIfChanged(name, date, winningSets, tables, doubles, lastSaved, onSave, force = true)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(tr("mobile.save", "Speichern"))
|
||||
if (showClasses) {
|
||||
TournamentEditorClassesTab(
|
||||
clubId = clubId,
|
||||
tournamentId = tournamentId,
|
||||
classes = classes,
|
||||
tr = tr,
|
||||
api = api,
|
||||
scope = scope,
|
||||
onReload = onReload,
|
||||
onError = onError,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -534,34 +607,61 @@ private fun TournamentEditorFlowTab(
|
||||
},
|
||||
) { Text(tr("tournaments.resetMatches", "Spiele zurücksetzen")) }
|
||||
}
|
||||
Divider(modifier = Modifier.padding(vertical = 8.dp))
|
||||
Text(tr("mobile.tournamentKnockout", "K.-o.-Runde"), fontWeight = FontWeight.SemiBold)
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
runCatching {
|
||||
kotlinx.coroutines.withContext(Dispatchers.IO) {
|
||||
api.startKnockout(
|
||||
de.tsschulz.tt_tagebuch.shared.api.models.TournamentStartKnockoutBody(clubId, tournamentId),
|
||||
)
|
||||
}
|
||||
}.onFailure { onError(it.message) }
|
||||
onReload()
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(tr("tournaments.startKnockout", "K.-o. starten"))
|
||||
}
|
||||
// K.-o.-Runde starten: verschoben (nicht in Gruppen-Tab)
|
||||
Text(
|
||||
tr("mobile.tournamentGroupsPreview", "Gruppen-Daten (Überblick)") + ": " +
|
||||
when (groupsJson) {
|
||||
is kotlinx.serialization.json.JsonArray -> "${groupsJson.size} " + tr("mobile.entries", "Einträge")
|
||||
null -> "—"
|
||||
else -> tr("mobile.dataLoaded", "geladen")
|
||||
},
|
||||
tr("mobile.tournamentGroupsPreview", "Gruppen-Daten (Überblick)"),
|
||||
style = MaterialTheme.typography.caption,
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.75f),
|
||||
)
|
||||
|
||||
when (groupsJson) {
|
||||
is JsonArray -> {
|
||||
val arr = groupsJson as JsonArray
|
||||
if (arr.isEmpty()) {
|
||||
Text(tr("mobile.noGroups", "Keine Gruppen vorhanden"))
|
||||
} else {
|
||||
arr.forEach { gElem ->
|
||||
val obj = gElem.jsonObject
|
||||
val classIdVal = obj["classId"]?.jsonPrimitive?.contentOrNull?.toIntOrNull()
|
||||
val groupNumber = obj["groupNumber"]?.jsonPrimitive?.contentOrNull?.toIntOrNull() ?: 0
|
||||
Text(
|
||||
(classIdVal?.toString() ?: "—") + " — " + tr("tournaments.group", "Gruppe") + " ${groupNumber}",
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
val participantsElem = obj["participants"]
|
||||
if (participantsElem is JsonArray) {
|
||||
// Table header
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
Text("#", fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(0.08f))
|
||||
Text(tr("tournaments.participant", "Teilnehmer"), fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(0.62f))
|
||||
Text(tr("tournaments.sets", "Sätze"), fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(0.15f))
|
||||
Text(tr("tournaments.points", "Punkte"), fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(0.15f))
|
||||
}
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
participantsElem.forEach { p ->
|
||||
val pObj = p.jsonObject
|
||||
val pname = pObj["name"]?.jsonPrimitive?.contentOrNull ?: "?"
|
||||
val pos = pObj["position"]?.jsonPrimitive?.contentOrNull ?: "-"
|
||||
val setsWon = pObj["setsWon"]?.jsonPrimitive?.contentOrNull ?: "-"
|
||||
val setsLost = pObj["setsLost"]?.jsonPrimitive?.contentOrNull ?: "-"
|
||||
val pointsWon = pObj["pointsWon"]?.jsonPrimitive?.contentOrNull ?: "-"
|
||||
val pointsLost = pObj["pointsLost"]?.jsonPrimitive?.contentOrNull ?: "-"
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
Text(pos, modifier = Modifier.weight(0.08f))
|
||||
Text(pname, modifier = Modifier.weight(0.62f))
|
||||
Text("${setsWon}:${setsLost}", modifier = Modifier.weight(0.15f))
|
||||
Text("${pointsWon}:${pointsLost}", modifier = Modifier.weight(0.15f))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text(tr("mobile.noGroupParticipants", "Keine Teilnehmer"))
|
||||
}
|
||||
Divider(modifier = Modifier.padding(vertical = 8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
null -> Text("—")
|
||||
else -> Text(tr("mobile.dataLoaded", "geladen"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,13 @@ class TournamentsApi(
|
||||
}
|
||||
|
||||
suspend fun getTournament(clubId: Int, tournamentId: Int): InternalTournamentDetailDto {
|
||||
return client.http.get("/api/tournament/$clubId/$tournamentId").body()
|
||||
val result: InternalTournamentDetailDto = client.http.get("/api/tournament/$clubId/$tournamentId").body()
|
||||
try {
|
||||
println("[TournamentsApi] getTournament -> numberOfTables: ${'$'}{result.numberOfTables}")
|
||||
} catch (t: Throwable) {
|
||||
// ignore on platforms where println may not be available
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
suspend fun getInternalTournamentStats(
|
||||
|
||||
Reference in New Issue
Block a user