feat: Implement filtering for regular matches and update related functionalities
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 48s

This commit is contained in:
Torsten Schulz (local)
2026-05-31 18:51:00 +02:00
parent 0ff67dae80
commit 5727404f88
9 changed files with 139 additions and 15 deletions

View File

@@ -324,6 +324,7 @@ internal fun TournamentEditorParticipantsTab(
classes: List<TournamentClassDto>,
participants: List<TournamentParticipantRowDto>,
externalParticipants: List<TournamentExternalParticipantRowDto>,
tournamentDate: String?,
dependencies: AppDependencies,
tr: (String, String) -> String,
onReload: () -> Unit,
@@ -337,6 +338,8 @@ internal fun TournamentEditorParticipantsTab(
var extDialog by remember { mutableStateOf(false) }
var extFirst by remember { mutableStateOf("") }
var extLast by remember { mutableStateOf("") }
var importingFromTraining by remember { mutableStateOf(false) }
var trainingImportMessage by remember { mutableStateOf<String?>(null) }
LaunchedEffect(clubId) {
dependencies.membersManager.loadMembers(clubId)
@@ -355,8 +358,73 @@ internal fun TournamentEditorParticipantsTab(
TextButton(onClick = { showInternal = !showInternal }) { Text(if (showInternal) tr("mobile.hide", "Verbergen") else tr("mobile.show", "Anzeigen")) }
}
if (showInternal) {
Row {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedButton(onClick = { addMenu = true }) { Text(tr("tournaments.addParticipant", "Teilnehmer hinzufügen")) }
Button(
enabled = !importingFromTraining,
onClick = {
scope.launch {
importingFromTraining = true
trainingImportMessage = null
runCatching {
withContext(Dispatchers.IO) {
val dateKey = normalizeTournamentDate(tournamentDate)
if (dateKey.isBlank()) {
return@withContext tr("tournaments.noTournamentDate", "Kein Turnierdatum vorhanden.")
}
val trainingDay = dependencies.diaryManager
.listDates(clubId)
.firstOrNull { normalizeTournamentDate(it.date) == dateKey }
?: return@withContext tr("tournaments.noTrainingDayForDate", "Kein Trainingstag für dieses Datum gefunden.")
val allTrainingParticipants = dependencies.diaryManager.listTrainingParticipants(trainingDay.id)
val presentTrainingParticipants = allTrainingParticipants.filter { it.attendanceStatus == "present" }
if (presentTrainingParticipants.isEmpty()) {
return@withContext if (allTrainingParticipants.isEmpty()) {
tr("tournaments.noTrainingParticipantsForDate", "Keine Teilnehmer im Trainingstag für dieses Datum gefunden.")
} else {
tr("tournaments.noPresentTrainingParticipantsForDate", "Keine anwesenden Teilnehmer im Trainingstag für dieses Datum gefunden.")
}
}
val existing = participants.mapNotNull { it.clubMemberId }.toSet()
val toAdd = presentTrainingParticipants
.map { it.memberId }
.filter { it !in existing }
.distinct()
if (toAdd.isEmpty()) {
return@withContext tr("tournaments.trainingParticipantsAlreadyAdded", "Alle anwesenden Trainingsteilnehmer sind bereits im Turnier.")
}
toAdd.forEach { memberId ->
api.addInternalParticipant(
TournamentAddInternalParticipantBody(
clubId = clubId,
classId = null,
participant = memberId,
tournamentId = tournamentId,
),
)
}
tr("tournaments.trainingParticipantsLoaded", "Anwesende Trainingsteilnehmer übernommen: {count}")
.replace("{count}", toAdd.size.toString())
}
}.fold(
onSuccess = {
trainingImportMessage = it
onReload()
},
onFailure = { onError(it.message) },
)
importingFromTraining = false
}
},
) {
Text(
if (importingFromTraining) {
tr("mobile.loading", "Laden...")
} else {
tr("tournaments.loadParticipantsFromTraining", "Aus Trainingstag laden")
},
)
}
DropdownMenu(expanded = addMenu, onDismissRequest = { addMenu = false }) {
val existing = participants.mapNotNull { it.clubMemberId }.toSet()
membersState.members
@@ -388,6 +456,7 @@ internal fun TournamentEditorParticipantsTab(
}
}
}
trainingImportMessage?.let { Text(it, style = MaterialTheme.typography.caption) }
participants.forEach { p ->
val label = p.member?.let { "${it.firstName.orEmpty()} ${it.lastName.orEmpty()}".trim() }.orEmpty().ifBlank { "#${p.id}" }
Row(
@@ -508,6 +577,10 @@ internal fun TournamentEditorParticipantsTab(
}
}
private fun normalizeTournamentDate(date: String?): String {
return date.orEmpty().trim().substringBefore("T")
}
@Composable
private fun ClassPickerChipRow(
classes: List<TournamentClassDto>,

View File

@@ -230,6 +230,7 @@ internal fun InternalTournamentEditorScreen(
classes = classes,
participants = participants,
externalParticipants = externalParticipants,
tournamentDate = d.date,
dependencies = dependencies,
tr = ::tr,
onReload = { reloadAll() },

View File

@@ -379,6 +379,15 @@ internal fun ScheduleScreen(dependencies: AppDependencies, friendlyOnly: Boolean
}
if (permissions?.canWriteSchedule() == true) {
if (m.isFriendly) {
TextButton(
onClick = {
playerError = null
playerMatch = m
detailMatch = null
},
) {
Text(tr("schedule.players", "Aufstellung / Spieler"))
}
TextButton(
onClick = {
friendlyResultMatch = m
@@ -525,9 +534,9 @@ internal fun ScheduleScreen(dependencies: AppDependencies, friendlyOnly: Boolean
playerSaving = true
playerError = null
runCatching {
dependencies.scheduleManager.updateMatchPlayers(
dependencies.scheduleManager.updateMatchPlayersForMatch(
clubId = clubId,
matchId = m.id,
match = m,
ready = readyIds,
planned = plannedIds,
played = playedIds,

View File

@@ -1,7 +1,7 @@
[versions]
# composeApp (Play Store / „Über die App“-Build)
appVersionCode = "20"
appVersionName = "1.7.0"
appVersionCode = "21"
appVersionName = "1.7.1"
agp = "9.2.1"
android-compileSdk = "35"
android-minSdk = "24"

View File

@@ -80,6 +80,12 @@ class MatchesApi(
}
}
suspend fun updateFriendlyMatchPlayers(clubId: Int, matchId: Int, body: UpdateMatchPlayersBody) {
client.http.patch("/api/friendly-matches/$clubId/$matchId/players") {
setBody(body)
}
}
suspend fun deleteFriendlyMatch(clubId: Int, matchId: Int) {
client.http.delete("/api/friendly-matches/$clubId/$matchId")
}

View File

@@ -59,6 +59,8 @@ data class ScheduleState(
get() = ScheduleLogic.leagueTeamNames(ScheduleLogic.mergeUniqueMatches(allMatches, ownMatches))
}
private fun List<ScheduleMatchDto>.withoutFriendly(): List<ScheduleMatchDto> = filterNot { it.isFriendly }
class ScheduleManager(
private val clubTeamsApi: ClubTeamsApi,
private val matchesApi: MatchesApi,
@@ -155,8 +157,8 @@ class ScheduleManager(
val leagueId = team.league?.id ?: return
_state.update { it.copy(isLoading = true, error = null, viewMode = ScheduleViewMode.Team) }
try {
val own = matchesApi.listMatchesForLeague(clubId, leagueId, "own")
val all = matchesApi.listMatchesForLeague(clubId, leagueId, "all")
val own = matchesApi.listMatchesForLeague(clubId, leagueId, "own").withoutFriendly()
val all = matchesApi.listMatchesForLeague(clubId, leagueId, "all").withoutFriendly()
val table = runCatching { matchesApi.leagueTable(clubId, leagueId) }.getOrElse { emptyList() }
_state.update {
it.copy(
@@ -198,7 +200,7 @@ class ScheduleManager(
)
}
try {
val matches = matchesApi.listMatchesForLeagues(clubId, _state.value.seasonId)
val matches = matchesApi.listMatchesForLeagues(clubId, _state.value.seasonId).withoutFriendly()
_state.update { it.copy(overallMatches = matches, isLoading = false, error = null) }
} catch (t: Throwable) {
_state.update {
@@ -226,7 +228,7 @@ class ScheduleManager(
)
}
try {
val matches = matchesApi.listMatchesForLeagues(clubId, _state.value.seasonId)
val matches = matchesApi.listMatchesForLeagues(clubId, _state.value.seasonId).withoutFriendly()
_state.update { it.copy(overallMatches = matches, isLoading = false, error = null) }
} catch (t: Throwable) {
_state.update {
@@ -299,6 +301,27 @@ class ScheduleManager(
refresh(clubId)
}
suspend fun updateMatchPlayersForMatch(
clubId: Int,
match: ScheduleMatchDto,
ready: List<Int>,
planned: List<Int>,
played: List<Int>,
) {
val body = UpdateMatchPlayersBody(
clubId = clubId,
playersReady = ready,
playersPlanned = planned,
playersPlayed = played,
)
when {
match.isFriendly && match.isSharedFriendly -> matchesApi.updateSharedFriendlyMatchPlayers(clubId, match.id, body)
match.isFriendly -> matchesApi.updateFriendlyMatchPlayers(clubId, match.id, body)
else -> matchesApi.updateMatchPlayers(match.id, body)
}
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)) }