feat: Add friendly match management features including API integration and UI updates
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:
Torsten Schulz (local)
2026-05-18 09:39:00 +02:00
parent 5dfdcb63bc
commit f9ab3d9932
21 changed files with 279 additions and 60 deletions

View File

@@ -337,7 +337,7 @@
"statusFinished": "Fertig",
"dataNotRecorded": "No nid erfasst",
"resultsRanking": "Rangliste",
"newSetPlaceholder": "Neue Satz, z. B. 11:7",
"newSetPlaceholder": "Satz",
"finishMatch": "Abschliessen",
"correctMatch": "Korrigiere",
"markMatchLive": "Als laufend markiere",

View File

@@ -983,7 +983,7 @@
"noPlayerDataAvailable": "Keine Spielerdaten verfügbar",
"dataNotRecorded": "Noch nicht erfasst",
"resultsRanking": "Rangliste",
"newSetPlaceholder": "Neuen Satz, z. B. 11:7",
"newSetPlaceholder": "Satz",
"finishMatch": "Abschließen",
"correctMatch": "Korrigieren",
"markMatchLive": "Als laufend markieren",

View File

@@ -1049,7 +1049,7 @@
"noPlayerDataAvailable": "Keine Spielerdaten verfügbar",
"dataNotRecorded": "Noch nicht erfasst",
"resultsRanking": "Rangliste",
"newSetPlaceholder": "Neuen Satz, z. B. 11:7",
"newSetPlaceholder": "Satz",
"finishMatch": "Abschließen",
"correctMatch": "Korrigieren",
"markMatchLive": "Als laufend markieren",

View File

@@ -1033,7 +1033,7 @@
"noPlayerDataAvailable": "No player data available",
"dataNotRecorded": "Not yet recorded",
"resultsRanking": "Ranking",
"newSetPlaceholder": "New set, e.g. 11:7",
"newSetPlaceholder": "Set",
"finishMatch": "Finish",
"correctMatch": "Correct",
"markMatchLive": "Mark as live",

View File

@@ -1034,7 +1034,7 @@
"noPlayerDataAvailable": "No player data available",
"dataNotRecorded": "Not yet recorded",
"resultsRanking": "Ranking",
"newSetPlaceholder": "New set, e.g. 11:7",
"newSetPlaceholder": "Set",
"finishMatch": "Finish",
"correctMatch": "Correct",
"markMatchLive": "Mark as live",

View File

@@ -1034,7 +1034,7 @@
"noPlayerDataAvailable": "No player data available",
"dataNotRecorded": "Not yet recorded",
"resultsRanking": "Ranking",
"newSetPlaceholder": "New set, e.g. 11:7",
"newSetPlaceholder": "Set",
"finishMatch": "Finish",
"correctMatch": "Correct",
"markMatchLive": "Mark as live",

View File

@@ -1034,7 +1034,7 @@
"noPlayerDataAvailable": "No hay datos de jugadores disponibles",
"dataNotRecorded": "Aún no registrado",
"resultsRanking": "Clasificación",
"newSetPlaceholder": "Nuevo set, p. ej. 11:7",
"newSetPlaceholder": "Set",
"finishMatch": "Finalizar",
"correctMatch": "Corregir",
"markMatchLive": "Marcar como en vivo",

View File

@@ -1035,7 +1035,7 @@
"noPlayerDataAvailable": "Walang available na data ng player",
"dataNotRecorded": "Hindi pa naitala",
"resultsRanking": "Ranggo",
"newSetPlaceholder": "Bagong set, hal. 11:7",
"newSetPlaceholder": "Set",
"finishMatch": "Tapusin",
"correctMatch": "Itama",
"markMatchLive": "Markahan bilang live",

View File

@@ -1033,7 +1033,7 @@
"noPlayerDataAvailable": "Aucune donnée de joueur disponible",
"dataNotRecorded": "Pas encore enregistré",
"resultsRanking": "Classement",
"newSetPlaceholder": "Nouveau set, p. ex. 11:7",
"newSetPlaceholder": "Set",
"finishMatch": "Terminer",
"correctMatch": "Corriger",
"markMatchLive": "Marquer en direct",

View File

@@ -1035,7 +1035,7 @@
"noPlayerDataAvailable": "Nessun dato del giocatore disponibile",
"dataNotRecorded": "Non ancora registrato",
"resultsRanking": "Classifica",
"newSetPlaceholder": "Nuovo set, es. 11:7",
"newSetPlaceholder": "Set",
"finishMatch": "Concludi",
"correctMatch": "Correggi",
"markMatchLive": "Segna come live",

View File

@@ -1035,7 +1035,7 @@
"noPlayerDataAvailable": "利用可能な選手データがありません",
"dataNotRecorded": "未登録",
"resultsRanking": "順位",
"newSetPlaceholder": "新しいセット 例: 11:7",
"newSetPlaceholder": "セット",
"finishMatch": "終了",
"correctMatch": "修正",
"markMatchLive": "進行中としてマーク",

View File

@@ -1031,7 +1031,7 @@
"noPlayerDataAvailable": "Brak danych gracza",
"dataNotRecorded": "Jeszcze nie wprowadzono",
"resultsRanking": "Zaszeregowanie",
"newSetPlaceholder": "Nowy set, np. 11:7",
"newSetPlaceholder": "Set",
"finishMatch": "Zakończ",
"correctMatch": "Popraw",
"markMatchLive": "Oznacz jako na żywo",

View File

@@ -1034,7 +1034,7 @@
"noPlayerDataAvailable": "ไม่มีข้อมูลผู้เล่น",
"dataNotRecorded": "ยังไม่ได้บันทึก",
"resultsRanking": "อันดับ",
"newSetPlaceholder": "เซตใหม่ เช่น 11:7",
"newSetPlaceholder": "เซต",
"finishMatch": "จบการแข่งขัน",
"correctMatch": "แก้ไข",
"markMatchLive": "ทำเครื่องหมายว่ากำลังแข่ง",

View File

@@ -1035,7 +1035,7 @@
"noPlayerDataAvailable": "Walang available na data ng player",
"dataNotRecorded": "Hindi pa naitala",
"resultsRanking": "Ranggo",
"newSetPlaceholder": "Bagong set, hal. 11:7",
"newSetPlaceholder": "Set",
"finishMatch": "Tapusin",
"correctMatch": "Itama",
"markMatchLive": "Markahan bilang live",

View File

@@ -1035,7 +1035,7 @@
"noPlayerDataAvailable": "没有可用的球员数据",
"dataNotRecorded": "尚未录入",
"resultsRanking": "排名",
"newSetPlaceholder": "新一局,例如 11:7",
"newSetPlaceholder": "",
"finishMatch": "结束比赛",
"correctMatch": "更正",
"markMatchLive": "标记为进行中",

View File

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

View File

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

View File

@@ -3,12 +3,16 @@ package de.tsschulz.tt_tagebuch.shared.api
import de.tsschulz.tt_tagebuch.shared.api.http.AuthedHttpClient
import de.tsschulz.tt_tagebuch.shared.api.models.LeaguePlayerStatDto
import de.tsschulz.tt_tagebuch.shared.api.models.LeagueTableRowDto
import de.tsschulz.tt_tagebuch.shared.api.models.FriendlyMatchSaveBody
import de.tsschulz.tt_tagebuch.shared.api.models.ScheduleMatchDto
import de.tsschulz.tt_tagebuch.shared.api.models.UpdateMatchPlayersBody
import io.ktor.client.call.body
import io.ktor.client.request.delete
import io.ktor.client.request.get
import io.ktor.client.request.parameter
import io.ktor.client.request.patch
import io.ktor.client.request.post
import io.ktor.client.request.put
import io.ktor.client.request.setBody
class MatchesApi(
@@ -41,4 +45,24 @@ class MatchesApi(
setBody(body)
}
}
suspend fun listFriendlyMatches(clubId: Int): List<ScheduleMatchDto> {
return client.http.get("/api/friendly-matches/$clubId").body()
}
suspend fun createFriendlyMatch(clubId: Int, body: FriendlyMatchSaveBody): ScheduleMatchDto {
return client.http.post("/api/friendly-matches/$clubId") {
setBody(body)
}.body()
}
suspend fun updateFriendlyMatch(clubId: Int, matchId: Int, body: FriendlyMatchSaveBody): ScheduleMatchDto {
return client.http.put("/api/friendly-matches/$clubId/$matchId") {
setBody(body)
}.body()
}
suspend fun deleteFriendlyMatch(clubId: Int, matchId: Int) {
client.http.delete("/api/friendly-matches/$clubId/$matchId")
}
}

View File

@@ -82,6 +82,8 @@ data class ScheduleLeagueDetailsDto(
@Serializable
data class ScheduleMatchDto(
val id: Int,
val friendlyMatchId: Int? = null,
val isFriendly: Boolean = false,
val date: String? = null,
val time: String? = null,
val homeTeamId: Int? = null,
@@ -95,6 +97,13 @@ data class ScheduleMatchDto(
val guestMatchPoints: Int = 0,
val isCompleted: Boolean = false,
val pdfUrl: String? = null,
val matchSystem: String? = null,
val singlesCount: Int = 12,
val doublesCount: Int = 4,
val winningSets: Int = 3,
val homeParticipants: List<FriendlyParticipantDto> = emptyList(),
val guestParticipants: List<FriendlyParticipantDto> = emptyList(),
val resultDetails: List<FriendlyResultRowDto> = emptyList(),
@Serializable(with = LenientIntListSerializer::class)
val playersReady: List<Int> = emptyList(),
@Serializable(with = LenientIntListSerializer::class)
@@ -107,6 +116,40 @@ data class ScheduleMatchDto(
val leagueDetails: ScheduleLeagueDetailsDto? = null,
)
@Serializable
data class FriendlyParticipantDto(
val type: String = "manual",
val memberId: Int? = null,
val firstName: String = "",
val lastName: String = "",
)
@Serializable
data class FriendlyResultRowDto(
val id: String = "",
val type: String = "single",
val homeName: String = "",
val guestName: String = "",
val sets: List<String> = emptyList(),
val completed: Boolean = false,
)
@Serializable
data class FriendlyMatchSaveBody(
val date: String,
val time: String? = null,
val homeTeamName: String,
val guestTeamName: String,
val matchSystem: String = "Braunschweiger System",
val winningSets: Int = 3,
val homeParticipants: List<FriendlyParticipantDto> = emptyList(),
val guestParticipants: List<FriendlyParticipantDto> = emptyList(),
val homeMatchPoints: Int = 0,
val guestMatchPoints: Int = 0,
val isCompleted: Boolean = false,
val resultDetails: List<FriendlyResultRowDto> = emptyList(),
)
@Serializable
data class LeagueTableRowDto(
val teamId: Int,
@@ -137,4 +180,5 @@ enum class ScheduleViewMode {
Team,
Overall,
Adult,
Friendly,
}

View File

@@ -2209,7 +2209,7 @@ object MobileStrings {
"tournaments.missingDataPDFTitleTop3" to "Fehlendi Date Top 3 Minimeisterschaft",
"tournaments.name" to "Name",
"tournaments.newMiniChampionship" to "Neue Minimeisterschaft",
"tournaments.newSetPlaceholder" to "Neue Satz, z. B. 11:7",
"tournaments.setShort" to "Satz",
"tournaments.newTournament" to "Neues Turnier",
"tournaments.noAssignableMatches" to "Kei Spiel verfüegbar, wo beidi Spieler frei sind.",
"tournaments.noClassesYet" to "Noch keine Klassen vorhanden. Fügen Sie eine neue Klasse hinzu.",
@@ -4725,7 +4725,7 @@ object MobileStrings {
"tournaments.missingDataPDFTitleTop3" to "Fehlende Daten Top 3 Minimeisterschaft",
"tournaments.name" to "Name",
"tournaments.newMiniChampionship" to "Neue Minimeisterschaft",
"tournaments.newSetPlaceholder" to "Neuen Satz, z. B. 11:7",
"tournaments.setShort" to "Satz",
"tournaments.newTournament" to "Neues Turnier",
"tournaments.noAssignableMatches" to "Keine Spiele verfügbar, bei denen beide Spieler frei sind.",
"tournaments.noClassesYet" to "Noch keine Klassen vorhanden. Fügen Sie eine neue Klasse hinzu.",
@@ -7236,7 +7236,7 @@ object MobileStrings {
"tournaments.missingDataPDFTitleTop3" to "Fehlende Daten Top 3 Minimeisterschaft",
"tournaments.name" to "Name",
"tournaments.newMiniChampionship" to "Neue Minimeisterschaft",
"tournaments.newSetPlaceholder" to "Neuen Satz, z. B. 11:7",
"tournaments.setShort" to "Satz",
"tournaments.newTournament" to "Neues Turnier",
"tournaments.noAssignableMatches" to "Keine Spiele verfügbar, bei denen beide Spieler frei sind.",
"tournaments.noClassesYet" to "Noch keine Klassen vorhanden. Fügen Sie eine neue Klasse hinzu.",
@@ -9743,7 +9743,7 @@ object MobileStrings {
"tournaments.missingDataPDFTitleTop3" to "Missing data Top 3 Mini championship",
"tournaments.name" to "name",
"tournaments.newMiniChampionship" to "New Mini Championship",
"tournaments.newSetPlaceholder" to "New set, e.g. 11:7",
"tournaments.setShort" to "Set",
"tournaments.newTournament" to "Add New Tournament",
"tournaments.noAssignableMatches" to "No matches available where both players are free.",
"tournaments.noClassesYet" to "No classes available yet. Add a new class.",
@@ -12250,7 +12250,7 @@ object MobileStrings {
"tournaments.missingDataPDFTitleTop3" to "Missing data Top 3 Mini championship",
"tournaments.name" to "name",
"tournaments.newMiniChampionship" to "New Mini Championship",
"tournaments.newSetPlaceholder" to "New set, e.g. 11:7",
"tournaments.setShort" to "Set",
"tournaments.newTournament" to "Add New Tournament",
"tournaments.noAssignableMatches" to "No matches available where both players are free.",
"tournaments.noClassesYet" to "No classes available yet. Add a new class.",
@@ -14757,7 +14757,7 @@ object MobileStrings {
"tournaments.missingDataPDFTitleTop3" to "Missing data Top 3 Mini championship",
"tournaments.name" to "name",
"tournaments.newMiniChampionship" to "New Mini Championship",
"tournaments.newSetPlaceholder" to "New set, e.g. 11:7",
"tournaments.setShort" to "Set",
"tournaments.newTournament" to "Add New Tournament",
"tournaments.noAssignableMatches" to "No matches available where both players are free.",
"tournaments.noClassesYet" to "No classes available yet. Add a new class.",
@@ -17264,7 +17264,7 @@ object MobileStrings {
"tournaments.missingDataPDFTitleTop3" to "Datos faltantes Top 3 Mini campeonato",
"tournaments.name" to "nombre",
"tournaments.newMiniChampionship" to "Nuevo Mini Campeonato",
"tournaments.newSetPlaceholder" to "Nuevo set, p. ej. 11:7",
"tournaments.setShort" to "Set",
"tournaments.newTournament" to "Agregar nuevo torneo",
"tournaments.noAssignableMatches" to "No hay partidos disponibles en los que ambos jugadores estén libres.",
"tournaments.noClassesYet" to "Aún no hay clases disponibles. Añade una nueva clase.",
@@ -19771,7 +19771,7 @@ object MobileStrings {
"tournaments.missingDataPDFTitleTop3" to "Nawawalang datos Top 3 Mini championship",
"tournaments.name" to "pangalan",
"tournaments.newMiniChampionship" to "Bagong Mini Championship",
"tournaments.newSetPlaceholder" to "Bagong set, hal. 11:7",
"tournaments.setShort" to "Set",
"tournaments.newTournament" to "Magdagdag ng Bagong Tournament",
"tournaments.noAssignableMatches" to "Walang laban kung saan pareho ang mga manlalaro ay bakante.",
"tournaments.noClassesYet" to "Wala pang klase. Magdagdag ng bagong klase.",
@@ -22278,7 +22278,7 @@ object MobileStrings {
"tournaments.missingDataPDFTitleTop3" to "Données manquantes Top 3 Mini-championnat",
"tournaments.name" to "nom",
"tournaments.newMiniChampionship" to "Nouveau mini-championnat",
"tournaments.newSetPlaceholder" to "Nouveau set, p. ex. 11:7",
"tournaments.setShort" to "Set",
"tournaments.newTournament" to "Ajouter un nouveau tournoi",
"tournaments.noAssignableMatches" to "Aucun match disponible où les deux joueurs sont libres.",
"tournaments.noClassesYet" to "Aucun cours disponible pour l'instant. Ajoutez une nouvelle classe.",
@@ -24785,7 +24785,7 @@ object MobileStrings {
"tournaments.missingDataPDFTitleTop3" to "Dati mancanti Top 3 Mini campionato",
"tournaments.name" to "nome",
"tournaments.newMiniChampionship" to "Nuovo Mini Campionato",
"tournaments.newSetPlaceholder" to "Nuovo set, es. 11:7",
"tournaments.setShort" to "Set",
"tournaments.newTournament" to "Aggiungi nuovo torneo",
"tournaments.noAssignableMatches" to "Nessuna partita disponibile in cui entrambi i giocatori sono liberi.",
"tournaments.noClassesYet" to "Nessuna lezione ancora disponibile. Aggiungi una nuova classe.",
@@ -27292,7 +27292,7 @@ object MobileStrings {
"tournaments.missingDataPDFTitleTop3" to "不足データ トップ3 ミニ選手権",
"tournaments.name" to "名前",
"tournaments.newMiniChampionship" to "新しいミニチャンピオンシップ",
"tournaments.newSetPlaceholder" to "新しいセット 例: 11:7",
"tournaments.setShort" to "セット",
"tournaments.newTournament" to "新しいトーナメントを追加",
"tournaments.noAssignableMatches" to "両選手が空いている試合がありません。",
"tournaments.noClassesYet" to "まだ利用可能なクラスはありません。新しいクラスを追加します。",
@@ -29799,7 +29799,7 @@ object MobileStrings {
"tournaments.missingDataPDFTitleTop3" to "Brakujące dane Top 3 Mini mistrzostwa",
"tournaments.name" to "nazwa",
"tournaments.newMiniChampionship" to "Nowe Mini Mistrzostwa",
"tournaments.newSetPlaceholder" to "Nowy set, np. 11:7",
"tournaments.setShort" to "Set",
"tournaments.newTournament" to "Dodaj nowy turniej",
"tournaments.noAssignableMatches" to "Brak meczów, w których obaj gracze są wolni.",
"tournaments.noClassesYet" to "Nie ma jeszcze dostępnych zajęć. Dodaj nową klasę.",
@@ -32306,7 +32306,7 @@ object MobileStrings {
"tournaments.missingDataPDFTitleTop3" to "ข้อมูลที่ขาดหาย 3 อันดับแรก มินิแชมเปี้ยนชิพ",
"tournaments.name" to "ชื่อ",
"tournaments.newMiniChampionship" to "ใหม่ มินิ แชมเปี้ยนชิพ",
"tournaments.newSetPlaceholder" to "เซตใหม่ เช่น 11:7",
"tournaments.setShort" to "เซต",
"tournaments.newTournament" to "เพิ่มทัวร์นาเมนต์ใหม่",
"tournaments.noAssignableMatches" to "ไม่มีแมตช์ที่ผู้เล่นทั้งสองว่าง",
"tournaments.noClassesYet" to "ยังไม่มีชั้นเรียนว่าง เพิ่มชั้นเรียนใหม่",
@@ -34813,7 +34813,7 @@ object MobileStrings {
"tournaments.missingDataPDFTitleTop3" to "Nawawalang datos Top 3 Mini championship",
"tournaments.name" to "pangalan",
"tournaments.newMiniChampionship" to "Bagong Mini Championship",
"tournaments.newSetPlaceholder" to "Bagong set, hal. 11:7",
"tournaments.setShort" to "Set",
"tournaments.newTournament" to "Magdagdag ng Bagong Tournament",
"tournaments.noAssignableMatches" to "Walang laban kung saan pareho ang mga manlalaro ay bakante.",
"tournaments.noClassesYet" to "Wala pang klase. Magdagdag ng bagong klase.",
@@ -37320,7 +37320,7 @@ object MobileStrings {
"tournaments.missingDataPDFTitleTop3" to "缺失数据 前3名迷你锦标赛",
"tournaments.name" to "姓名",
"tournaments.newMiniChampionship" to "新迷你锦标赛",
"tournaments.newSetPlaceholder" to "新一局,例如 11:7",
"tournaments.setShort" to "",
"tournaments.newTournament" to "添加新锦标赛",
"tournaments.noAssignableMatches" to "没有两位选手都空闲的比赛。",
"tournaments.noClassesYet" to "还没有可用的课程。添加一个新类。",

View File

@@ -4,6 +4,7 @@ import de.tsschulz.tt_tagebuch.shared.api.ClubTeamsApi
import de.tsschulz.tt_tagebuch.shared.api.MatchesApi
import de.tsschulz.tt_tagebuch.shared.api.ScheduleLogic
import de.tsschulz.tt_tagebuch.shared.api.models.ClubTeamDto
import de.tsschulz.tt_tagebuch.shared.api.models.FriendlyMatchSaveBody
import de.tsschulz.tt_tagebuch.shared.api.models.LeagueTableRowDto
import de.tsschulz.tt_tagebuch.shared.api.models.ScheduleMatchDto
import de.tsschulz.tt_tagebuch.shared.api.models.ScheduleMatchScope
@@ -21,6 +22,7 @@ data class ScheduleState(
val ownMatches: List<ScheduleMatchDto> = emptyList(),
val allMatches: List<ScheduleMatchDto> = emptyList(),
val overallMatches: List<ScheduleMatchDto> = emptyList(),
val friendlyMatches: List<ScheduleMatchDto> = emptyList(),
val leagueTable: List<LeagueTableRowDto> = emptyList(),
val matchScope: ScheduleMatchScope = ScheduleMatchScope.Own,
val otherTeamName: String = "",
@@ -36,6 +38,7 @@ data class ScheduleState(
ScheduleViewMode.Overall -> ScheduleLogic.sortMatches(overallMatches)
ScheduleViewMode.Adult ->
ScheduleLogic.sortMatches(ScheduleLogic.filterAdultLeagues(overallMatches))
ScheduleViewMode.Friendly -> ScheduleLogic.sortMatches(friendlyMatches)
ScheduleViewMode.Team -> {
val t = selectedTeam ?: return emptyList()
ScheduleLogic.applyTeamMatchScope(
@@ -75,6 +78,7 @@ class ScheduleManager(
}
ScheduleViewMode.Overall -> loadOverallSchedule(clubId)
ScheduleViewMode.Adult -> loadAdultSchedule(clubId)
ScheduleViewMode.Friendly -> loadFriendlyMatches(clubId)
}
}
@@ -228,6 +232,35 @@ class ScheduleManager(
}
}
suspend fun loadFriendlyMatches(clubId: Int) {
_state.update {
it.copy(
isLoading = true,
error = null,
viewMode = ScheduleViewMode.Friendly,
matchScope = ScheduleMatchScope.Own,
otherTeamName = "",
selectedTeamId = null,
ownMatches = emptyList(),
allMatches = emptyList(),
overallMatches = emptyList(),
leagueTable = emptyList(),
)
}
try {
val matches = matchesApi.listFriendlyMatches(clubId)
_state.update { it.copy(friendlyMatches = matches, isLoading = false, error = null) }
} catch (t: Throwable) {
_state.update {
it.copy(
isLoading = false,
friendlyMatches = emptyList(),
error = t.toUserMessage("Freundschaftsspiele konnten nicht geladen werden"),
)
}
}
}
fun setMatchScope(scope: ScheduleMatchScope) {
_state.update { it.copy(matchScope = scope) }
if (scope == ScheduleMatchScope.Other) {
@@ -255,4 +288,25 @@ class ScheduleManager(
)
refresh(clubId)
}
suspend fun createFriendlyMatch(clubId: Int, body: FriendlyMatchSaveBody) {
val saved = matchesApi.createFriendlyMatch(clubId, body)
_state.update { it.copy(friendlyMatches = ScheduleLogic.sortMatches(it.friendlyMatches + saved)) }
}
suspend fun updateFriendlyMatch(clubId: Int, matchId: Int, body: FriendlyMatchSaveBody) {
val saved = matchesApi.updateFriendlyMatch(clubId, matchId, body)
_state.update {
it.copy(
friendlyMatches = ScheduleLogic.sortMatches(
it.friendlyMatches.filterNot { match -> match.id == matchId } + saved,
),
)
}
}
suspend fun deleteFriendlyMatch(clubId: Int, matchId: Int) {
matchesApi.deleteFriendlyMatch(clubId, matchId)
_state.update { it.copy(friendlyMatches = it.friendlyMatches.filterNot { match -> match.id == matchId }) }
}
}