feat: Add friendly match management features including API integration and UI updates
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 44s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 44s
- Implemented API methods for listing, creating, updating, and deleting friendly matches. - Enhanced the ScheduleManager to handle friendly matches, including loading and state management. - Updated UI components to support editing and displaying friendly match results. - Modified localization files to reflect changes in terminology for match sets.
This commit is contained in:
@@ -22,6 +22,10 @@ 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.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.Stop
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.DropdownMenu
|
||||
import androidx.compose.material.DropdownMenuItem
|
||||
@@ -543,6 +547,8 @@ internal fun TournamentEditorMatchesTab(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
var confirmDelete by remember { mutableStateOf<Pair<Int, Int>?>(null) }
|
||||
var editingSet by remember { mutableStateOf<Pair<Int, Int>?>(null) }
|
||||
var editInput by remember { mutableStateOf("") }
|
||||
val hasKO = matches.any { (it.round ?: "").lowercase() != "group" && (it.round ?: "").isNotBlank() }
|
||||
var showDistributedDialog by remember { mutableStateOf(false) }
|
||||
var distributedMatches by remember { mutableStateOf<List<TournamentMatchDto>>(emptyList()) }
|
||||
@@ -720,7 +726,7 @@ internal fun TournamentEditorMatchesTab(
|
||||
resultInput = it
|
||||
resultError = null
|
||||
},
|
||||
placeholder = { Text(tr("tournaments.newSetPlaceholder", "11:7")) },
|
||||
placeholder = { Text(tr("tournaments.setShort", "Satz")) },
|
||||
singleLine = true,
|
||||
isError = resultError != null,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
@@ -746,10 +752,29 @@ internal fun TournamentEditorMatchesTab(
|
||||
val results = m.tournamentResults
|
||||
if (!results.isNullOrEmpty()) {
|
||||
Text(formatMatchSets(m))
|
||||
results.sortedBy { it.set }.forEach { r ->
|
||||
results.sortedBy { it.set }.forEach { r ->
|
||||
val a = r.pointsPlayer1?.let { kotlin.math.abs(it).toString() } ?: "-"
|
||||
val b = r.pointsPlayer2?.let { kotlin.math.abs(it).toString() } ?: "-"
|
||||
Text("${r.set}: $a:$b", style = MaterialTheme.typography.caption)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
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),
|
||||
)
|
||||
}
|
||||
IconButton(onClick = {
|
||||
editingSet = Pair(m.id, r.set)
|
||||
editInput = "$a:$b"
|
||||
}, modifier = Modifier.size(28.dp)) {
|
||||
Icon(Icons.Filled.Edit, contentDescription = "Bearbeiten", modifier = Modifier.size(18.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text(m.tournamentResults?.size?.toString() ?: "0")
|
||||
@@ -785,7 +810,7 @@ internal fun TournamentEditorMatchesTab(
|
||||
Column(modifier = Modifier.width(120.dp)) {
|
||||
if (m.isFinished != true) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
TextButton(onClick = {
|
||||
IconButton(onClick = {
|
||||
// finish
|
||||
scope.launch {
|
||||
runCatching {
|
||||
@@ -795,18 +820,31 @@ internal fun TournamentEditorMatchesTab(
|
||||
}.onFailure { onError(it.message) }
|
||||
onReload()
|
||||
}
|
||||
}) { Text(tr("tournaments.finishMatchShort", "Fertig")) }
|
||||
// start/stop match
|
||||
TextButton(onClick = {
|
||||
scope.launch {
|
||||
runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
api.setMatchActive(clubId, tournamentId, m.id, TournamentMatchActiveBody(isActive = true))
|
||||
}
|
||||
}.onFailure { onError(it.message) }
|
||||
onReload()
|
||||
}
|
||||
}) { Text(tr("tournaments.startMatch", "Starten")) }
|
||||
}) { Icon(Icons.Filled.Check, contentDescription = tr("tournaments.finishMatchShort", "Fertig")) }
|
||||
|
||||
if (m.isActive == true) {
|
||||
IconButton(onClick = {
|
||||
scope.launch {
|
||||
runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
api.setMatchActive(clubId, tournamentId, m.id, TournamentMatchActiveBody(isActive = false))
|
||||
}
|
||||
}.onFailure { onError(it.message) }
|
||||
onReload()
|
||||
}
|
||||
}) { Icon(Icons.Filled.Stop, contentDescription = tr("tournaments.stopMatch", "Stoppen")) }
|
||||
} else {
|
||||
IconButton(onClick = {
|
||||
scope.launch {
|
||||
runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
api.setMatchActive(clubId, tournamentId, m.id, TournamentMatchActiveBody(isActive = true))
|
||||
}
|
||||
}.onFailure { onError(it.message) }
|
||||
onReload()
|
||||
}
|
||||
}) { Icon(Icons.Filled.PlayArrow, contentDescription = tr("tournaments.startMatch", "Starten")) }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
TextButton(onClick = {
|
||||
@@ -820,19 +858,7 @@ internal fun TournamentEditorMatchesTab(
|
||||
}
|
||||
}) { Text(tr("tournaments.correct", "Korrigieren")) }
|
||||
}
|
||||
// Stop button shown regardless (to allow stopping active matches)
|
||||
if (m.isActive == true) {
|
||||
TextButton(onClick = {
|
||||
scope.launch {
|
||||
runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
api.setMatchActive(clubId, tournamentId, m.id, TournamentMatchActiveBody(isActive = false))
|
||||
}
|
||||
}.onFailure { onError(it.message) }
|
||||
onReload()
|
||||
}
|
||||
}) { Text(tr("tournaments.stopMatch", "Stoppen")) }
|
||||
}
|
||||
// no duplicate stop button
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
@@ -868,6 +894,73 @@ internal fun TournamentEditorMatchesTab(
|
||||
}
|
||||
)
|
||||
}
|
||||
// Edit set dialog
|
||||
if (editingSet != null) {
|
||||
val (matchIdToEdit, setToEdit) = editingSet!!
|
||||
AlertDialog(
|
||||
onDismissRequest = { editingSet = null },
|
||||
title = { Text(tr("tournaments.editSetTitle", "Satz bearbeiten")) },
|
||||
text = {
|
||||
Column {
|
||||
OutlinedTextField(
|
||||
value = editInput,
|
||||
onValueChange = { editInput = it },
|
||||
placeholder = { Text("11:7") }
|
||||
)
|
||||
Text(tr("tournaments.editSetHint", "Gib ein neues Ergebnis ein, z.B. 11:7"), style = MaterialTheme.typography.caption)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
val normalized = normalizeResult(editInput)
|
||||
if (normalized == null) {
|
||||
onError(tr("tournaments.invalidResultInput", "Ungültiges Ergebnis"))
|
||||
return@TextButton
|
||||
}
|
||||
scope.launch {
|
||||
runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
// Find current match results snapshot
|
||||
val matchObj = matches.firstOrNull { it.id == matchIdToEdit }
|
||||
val results = matchObj?.tournamentResults?.sortedBy { it.set } ?: emptyList()
|
||||
// Tail: sets after the edited one
|
||||
val tail = results.filter { it.set > setToEdit }.map { Pair(it.pointsPlayer1, it.pointsPlayer2) }
|
||||
// Delete tail from highest to lowest
|
||||
for (i in results.size downTo setToEdit + 1) {
|
||||
api.deleteMatchResult(de.tsschulz.tt_tagebuch.shared.api.models.TournamentDeleteMatchResultBody(
|
||||
clubId, tournamentId, matchIdToEdit, i
|
||||
))
|
||||
}
|
||||
// Delete the set to edit
|
||||
api.deleteMatchResult(de.tsschulz.tt_tagebuch.shared.api.models.TournamentDeleteMatchResultBody(
|
||||
clubId, tournamentId, matchIdToEdit, setToEdit
|
||||
))
|
||||
// Add the edited set
|
||||
api.addMatchResult(de.tsschulz.tt_tagebuch.shared.api.models.TournamentAddMatchResultBody(
|
||||
clubId, tournamentId, matchIdToEdit, setToEdit, normalized
|
||||
))
|
||||
// Re-add tail sets in order
|
||||
var cur = setToEdit + 1
|
||||
for (t in tail) {
|
||||
val r = "${kotlin.math.abs(t.first ?: 0)}:${kotlin.math.abs(t.second ?: 0)}"
|
||||
api.addMatchResult(de.tsschulz.tt_tagebuch.shared.api.models.TournamentAddMatchResultBody(
|
||||
clubId, tournamentId, matchIdToEdit, cur, r
|
||||
))
|
||||
cur++
|
||||
}
|
||||
}
|
||||
}.onFailure { onError(it.message) }
|
||||
editingSet = null
|
||||
editInput = ""
|
||||
onReload()
|
||||
}
|
||||
}) { Text(tr("common.save", "Speichern")) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { editingSet = null }) { Text(tr("common.cancel", "Abbrechen")) }
|
||||
}
|
||||
)
|
||||
}
|
||||
if (showDistributedDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showDistributedDialog = false },
|
||||
@@ -948,7 +1041,7 @@ private fun MatchResultRow(
|
||||
value = resultInput,
|
||||
onValueChange = { resultInput = it },
|
||||
modifier = Modifier.weight(1f),
|
||||
placeholder = { Text(tr("tournaments.newSetPlaceholder", "Neuen Satz, z.B. 11:7")) },
|
||||
placeholder = { Text(tr("tournaments.setShort", "Satz")) },
|
||||
singleLine = true,
|
||||
)
|
||||
var tableExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
@@ -29,6 +29,7 @@ import androidx.compose.material.DropdownMenu
|
||||
import androidx.compose.material.DropdownMenuItem
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedButton
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -51,6 +52,9 @@ import de.tsschulz.tt_tagebuch.app.stats.TrainingStatsDerived
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.ScheduleMatchDto
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.ScheduleMatchScope
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.ScheduleViewMode
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.FriendlyMatchSaveBody
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.FriendlyParticipantDto
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.FriendlyResultRowDto
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.canReadSchedule
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.canWriteSchedule
|
||||
import de.tsschulz.tt_tagebuch.shared.i18n.MobileStrings
|
||||
|
||||
Reference in New Issue
Block a user