From f9ab3d993287934403d64173dc418f951ec1d4ef Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Mon, 18 May 2026 09:39:00 +0200 Subject: [PATCH] feat: Add friendly match management features including API integration and UI updates - 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. --- frontend/src/i18n/locales/de-CH.json | 2 +- frontend/src/i18n/locales/de-extended.json | 2 +- frontend/src/i18n/locales/de.json | 2 +- frontend/src/i18n/locales/en-AU.json | 2 +- frontend/src/i18n/locales/en-GB.json | 2 +- frontend/src/i18n/locales/en-US.json | 2 +- frontend/src/i18n/locales/es.json | 2 +- frontend/src/i18n/locales/fil.json | 2 +- frontend/src/i18n/locales/fr.json | 2 +- frontend/src/i18n/locales/it.json | 2 +- frontend/src/i18n/locales/ja.json | 2 +- frontend/src/i18n/locales/pl.json | 2 +- frontend/src/i18n/locales/th.json | 2 +- frontend/src/i18n/locales/tl.json | 2 +- frontend/src/i18n/locales/zh.json | 2 +- .../ui/InternalTournamentEditorDetailTabs.kt | 153 ++++++++++++++---- .../tt_tagebuch/app/ui/ScheduleScreen.kt | 4 + .../tt_tagebuch/shared/api/MatchesApi.kt | 24 +++ .../tt_tagebuch/shared/api/models/Schedule.kt | 44 +++++ .../tt_tagebuch/shared/i18n/MobileStrings.kt | 30 ++-- .../shared/state/ScheduleManager.kt | 54 +++++++ 21 files changed, 279 insertions(+), 60 deletions(-) diff --git a/frontend/src/i18n/locales/de-CH.json b/frontend/src/i18n/locales/de-CH.json index 35cec3fe..77fb411e 100644 --- a/frontend/src/i18n/locales/de-CH.json +++ b/frontend/src/i18n/locales/de-CH.json @@ -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", diff --git a/frontend/src/i18n/locales/de-extended.json b/frontend/src/i18n/locales/de-extended.json index 2e103eee..7e0957de 100644 --- a/frontend/src/i18n/locales/de-extended.json +++ b/frontend/src/i18n/locales/de-extended.json @@ -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", diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index 720a63e4..a49453ba 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -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", diff --git a/frontend/src/i18n/locales/en-AU.json b/frontend/src/i18n/locales/en-AU.json index 9dad0f8b..1fefde9a 100644 --- a/frontend/src/i18n/locales/en-AU.json +++ b/frontend/src/i18n/locales/en-AU.json @@ -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", diff --git a/frontend/src/i18n/locales/en-GB.json b/frontend/src/i18n/locales/en-GB.json index 45790660..0b67e51c 100644 --- a/frontend/src/i18n/locales/en-GB.json +++ b/frontend/src/i18n/locales/en-GB.json @@ -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", diff --git a/frontend/src/i18n/locales/en-US.json b/frontend/src/i18n/locales/en-US.json index 3f38f061..212b91e0 100644 --- a/frontend/src/i18n/locales/en-US.json +++ b/frontend/src/i18n/locales/en-US.json @@ -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", diff --git a/frontend/src/i18n/locales/es.json b/frontend/src/i18n/locales/es.json index c4578c23..50feb934 100644 --- a/frontend/src/i18n/locales/es.json +++ b/frontend/src/i18n/locales/es.json @@ -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", diff --git a/frontend/src/i18n/locales/fil.json b/frontend/src/i18n/locales/fil.json index fdcda203..1cdf614d 100644 --- a/frontend/src/i18n/locales/fil.json +++ b/frontend/src/i18n/locales/fil.json @@ -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", diff --git a/frontend/src/i18n/locales/fr.json b/frontend/src/i18n/locales/fr.json index 868b935e..f98a1b53 100644 --- a/frontend/src/i18n/locales/fr.json +++ b/frontend/src/i18n/locales/fr.json @@ -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", diff --git a/frontend/src/i18n/locales/it.json b/frontend/src/i18n/locales/it.json index 7491d849..071cfaab 100644 --- a/frontend/src/i18n/locales/it.json +++ b/frontend/src/i18n/locales/it.json @@ -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", diff --git a/frontend/src/i18n/locales/ja.json b/frontend/src/i18n/locales/ja.json index c76a2663..ef97b016 100644 --- a/frontend/src/i18n/locales/ja.json +++ b/frontend/src/i18n/locales/ja.json @@ -1035,7 +1035,7 @@ "noPlayerDataAvailable": "利用可能な選手データがありません", "dataNotRecorded": "未登録", "resultsRanking": "順位", - "newSetPlaceholder": "新しいセット 例: 11:7", + "newSetPlaceholder": "セット", "finishMatch": "終了", "correctMatch": "修正", "markMatchLive": "進行中としてマーク", diff --git a/frontend/src/i18n/locales/pl.json b/frontend/src/i18n/locales/pl.json index 42f196b4..0374828e 100644 --- a/frontend/src/i18n/locales/pl.json +++ b/frontend/src/i18n/locales/pl.json @@ -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", diff --git a/frontend/src/i18n/locales/th.json b/frontend/src/i18n/locales/th.json index f26b08f0..916f09d4 100644 --- a/frontend/src/i18n/locales/th.json +++ b/frontend/src/i18n/locales/th.json @@ -1034,7 +1034,7 @@ "noPlayerDataAvailable": "ไม่มีข้อมูลผู้เล่น", "dataNotRecorded": "ยังไม่ได้บันทึก", "resultsRanking": "อันดับ", - "newSetPlaceholder": "เซตใหม่ เช่น 11:7", + "newSetPlaceholder": "เซต", "finishMatch": "จบการแข่งขัน", "correctMatch": "แก้ไข", "markMatchLive": "ทำเครื่องหมายว่ากำลังแข่ง", diff --git a/frontend/src/i18n/locales/tl.json b/frontend/src/i18n/locales/tl.json index ac2f6c0b..3ab1f4c1 100644 --- a/frontend/src/i18n/locales/tl.json +++ b/frontend/src/i18n/locales/tl.json @@ -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", diff --git a/frontend/src/i18n/locales/zh.json b/frontend/src/i18n/locales/zh.json index f3c678cf..8c71eb60 100644 --- a/frontend/src/i18n/locales/zh.json +++ b/frontend/src/i18n/locales/zh.json @@ -1035,7 +1035,7 @@ "noPlayerDataAvailable": "没有可用的球员数据", "dataNotRecorded": "尚未录入", "resultsRanking": "排名", - "newSetPlaceholder": "新一局,例如 11:7", + "newSetPlaceholder": "局", "finishMatch": "结束比赛", "correctMatch": "更正", "markMatchLive": "标记为进行中", diff --git a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/InternalTournamentEditorDetailTabs.kt b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/InternalTournamentEditorDetailTabs.kt index 75a2b7d2..4e0f3590 100644 --- a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/InternalTournamentEditorDetailTabs.kt +++ b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/InternalTournamentEditorDetailTabs.kt @@ -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?>(null) } + var editingSet by remember { mutableStateOf?>(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>(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) } diff --git a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/ScheduleScreen.kt b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/ScheduleScreen.kt index f2c70167..322b55be 100644 --- a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/ScheduleScreen.kt +++ b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/ScheduleScreen.kt @@ -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 diff --git a/mobile-app/shared/src/commonMain/kotlin/de/tsschulz/tt_tagebuch/shared/api/MatchesApi.kt b/mobile-app/shared/src/commonMain/kotlin/de/tsschulz/tt_tagebuch/shared/api/MatchesApi.kt index 724049a6..c42794ba 100644 --- a/mobile-app/shared/src/commonMain/kotlin/de/tsschulz/tt_tagebuch/shared/api/MatchesApi.kt +++ b/mobile-app/shared/src/commonMain/kotlin/de/tsschulz/tt_tagebuch/shared/api/MatchesApi.kt @@ -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 { + 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") + } } diff --git a/mobile-app/shared/src/commonMain/kotlin/de/tsschulz/tt_tagebuch/shared/api/models/Schedule.kt b/mobile-app/shared/src/commonMain/kotlin/de/tsschulz/tt_tagebuch/shared/api/models/Schedule.kt index 30dd3d8f..36b6cfc3 100644 --- a/mobile-app/shared/src/commonMain/kotlin/de/tsschulz/tt_tagebuch/shared/api/models/Schedule.kt +++ b/mobile-app/shared/src/commonMain/kotlin/de/tsschulz/tt_tagebuch/shared/api/models/Schedule.kt @@ -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 = emptyList(), + val guestParticipants: List = emptyList(), + val resultDetails: List = emptyList(), @Serializable(with = LenientIntListSerializer::class) val playersReady: List = 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 = 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 = emptyList(), + val guestParticipants: List = emptyList(), + val homeMatchPoints: Int = 0, + val guestMatchPoints: Int = 0, + val isCompleted: Boolean = false, + val resultDetails: List = emptyList(), +) + @Serializable data class LeagueTableRowDto( val teamId: Int, @@ -137,4 +180,5 @@ enum class ScheduleViewMode { Team, Overall, Adult, + Friendly, } diff --git a/mobile-app/shared/src/commonMain/kotlin/de/tsschulz/tt_tagebuch/shared/i18n/MobileStrings.kt b/mobile-app/shared/src/commonMain/kotlin/de/tsschulz/tt_tagebuch/shared/i18n/MobileStrings.kt index 82bc972b..85e60743 100644 --- a/mobile-app/shared/src/commonMain/kotlin/de/tsschulz/tt_tagebuch/shared/i18n/MobileStrings.kt +++ b/mobile-app/shared/src/commonMain/kotlin/de/tsschulz/tt_tagebuch/shared/i18n/MobileStrings.kt @@ -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 "还没有可用的课程。添加一个新类。", diff --git a/mobile-app/shared/src/commonMain/kotlin/de/tsschulz/tt_tagebuch/shared/state/ScheduleManager.kt b/mobile-app/shared/src/commonMain/kotlin/de/tsschulz/tt_tagebuch/shared/state/ScheduleManager.kt index 34d395c5..2ac3d6d3 100644 --- a/mobile-app/shared/src/commonMain/kotlin/de/tsschulz/tt_tagebuch/shared/state/ScheduleManager.kt +++ b/mobile-app/shared/src/commonMain/kotlin/de/tsschulz/tt_tagebuch/shared/state/ScheduleManager.kt @@ -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 = emptyList(), val allMatches: List = emptyList(), val overallMatches: List = emptyList(), + val friendlyMatches: List = emptyList(), val leagueTable: List = 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 }) } + } }