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

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