feat: implement table distribution logic and update UI for match assignments
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 43s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 43s
This commit is contained in:
@@ -2068,6 +2068,8 @@ class TournamentService {
|
||||
match.result = `${win}:${lose}`;
|
||||
// Wenn ein Match abgeschlossen wird, darf es nicht mehr als aktiv gelten
|
||||
match.isActive = false;
|
||||
// Freigebe den Tisch, damit er für neue Zuweisungen verfügbar ist
|
||||
match.tableNumber = null;
|
||||
await match.save();
|
||||
console.log(`[finishMatch] match ${matchId} finished, result=${match.result}, isActive set to false`);
|
||||
|
||||
@@ -2921,13 +2923,17 @@ class TournamentService {
|
||||
|
||||
// Ermittle aktuell laufende Matches, damit wir nur Spiele verteilen, bei denen beide Spieler frei sind
|
||||
const activeMatches = await TournamentMatch.findAll({ where: { tournamentId, isActive: true, isFinished: false } });
|
||||
const activePlayerIds = new Set();
|
||||
|
||||
// Nur Spieler aus aktiv laufenden Matches mit einem zugewiesenen Tisch blockieren.
|
||||
const occupiedPlayerIds = new Set();
|
||||
activeMatches.forEach(am => {
|
||||
if (am.player1Id) activePlayerIds.add(Number(am.player1Id));
|
||||
if (am.player2Id) activePlayerIds.add(Number(am.player2Id));
|
||||
if (am.tableNumber != null) {
|
||||
if (am.player1Id) occupiedPlayerIds.add(Number(am.player1Id));
|
||||
if (am.player2Id) occupiedPlayerIds.add(Number(am.player2Id));
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`[distributeTables] activeMatches=${activeMatches.length} activePlayers=${[...activePlayerIds].slice(0,10).join(',')}`);
|
||||
console.log(`[distributeTables] activeMatches=${activeMatches.length} occupiedPlayers=${[...occupiedPlayerIds].slice(0,10).join(',')}`);
|
||||
|
||||
// Lade nur noch die nicht abgeschlossenen, noch nicht zugewiesenen Matches
|
||||
const matches = await TournamentMatch.findAll({
|
||||
@@ -2937,33 +2943,59 @@ class TournamentService {
|
||||
|
||||
console.log(`[distributeTables] candidateMatches=${matches.length}`);
|
||||
|
||||
let idx = 0;
|
||||
// Ermittle aktuell belegte Tischnummern basierend auf laufenden Matches
|
||||
const occupiedTables = new Set();
|
||||
activeMatches.forEach(am => {
|
||||
if (am.tableNumber != null) occupiedTables.add(Number(am.tableNumber));
|
||||
});
|
||||
|
||||
console.log(`[distributeTables] occupiedTables=${[...occupiedTables].join(',')}`);
|
||||
|
||||
// Erzeuge Liste verfügbarer Tische (1..numTables) minus belegte Tische
|
||||
const availableTables = [];
|
||||
for (let t = 1; t <= numTables; t++) {
|
||||
if (!occupiedTables.has(t)) availableTables.push(t);
|
||||
}
|
||||
|
||||
if (availableTables.length === 0) {
|
||||
console.log('[distributeTables] no available tables (all occupied by active matches)');
|
||||
return [];
|
||||
}
|
||||
|
||||
const updated = [];
|
||||
// Verwende `busyPlayers` (initial aus occupiedPlayerIds) und erweitere es bei Zuweisungen,
|
||||
// damit ein Spieler nicht mehrere Tische in einer Verteilung bekommt.
|
||||
const busyPlayers = new Set(occupiedPlayerIds);
|
||||
|
||||
for (const m of matches) {
|
||||
// Spiele mit BYE (fehlendem Spieler) überspringen
|
||||
if (!m.player1Id || !m.player2Id) {
|
||||
console.log(`[distributeTables] skipping match ${m.id} (BYE or missing player) p1=${m.player1Id} p2=${m.player2Id}`);
|
||||
continue;
|
||||
}
|
||||
// Nur vergeben, wenn beide Spieler aktuell nicht in einem aktiven Match sind
|
||||
if (activePlayerIds.has(Number(m.player1Id)) || activePlayerIds.has(Number(m.player2Id))) {
|
||||
console.log(`[distributeTables] skipping match ${m.id} because player active p1=${m.player1Id} p2=${m.player2Id}`);
|
||||
// Nur vergeben, wenn beide Spieler aktuell keinen Tisch belegen (busyPlayers)
|
||||
if (busyPlayers.has(Number(m.player1Id)) || busyPlayers.has(Number(m.player2Id))) {
|
||||
console.log(`[distributeTables] skipping match ${m.id} because player occupies table p1=${m.player1Id} p2=${m.player2Id}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const assign = (idx % numTables) + 1;
|
||||
// Wenn keine freien Tische mehr vorhanden, abbrechen
|
||||
if (availableTables.length === 0) {
|
||||
console.log('[distributeTables] no more available tables to assign');
|
||||
break;
|
||||
}
|
||||
|
||||
// Nimm den nächsten freien Tisch (FIFO) und markiere ihn als belegt
|
||||
const assign = availableTables.shift();
|
||||
m.tableNumber = assign;
|
||||
// Markiere das Spiel als gestartet (läuft)
|
||||
m.isActive = true;
|
||||
m.isActive = true; // Markiere Spiel als gestartet
|
||||
await m.save();
|
||||
updated.push(m.toJSON ? m.toJSON() : m);
|
||||
|
||||
console.log(`[distributeTables] assigned match ${m.id} -> table ${assign}`);
|
||||
|
||||
// Spieler gelten nun als aktiv - damit kein weiteres Spiel für sie zugewiesen wird
|
||||
activePlayerIds.add(Number(m.player1Id));
|
||||
activePlayerIds.add(Number(m.player2Id));
|
||||
idx++;
|
||||
// Markiere Spieler als busy, damit sie in dieser Verteilung keinen weiteren Tisch bekommen
|
||||
busyPlayers.add(Number(m.player1Id));
|
||||
busyPlayers.add(Number(m.player2Id));
|
||||
}
|
||||
|
||||
console.log(`[distributeTables] finished: assignedCount=${updated.length}`);
|
||||
|
||||
@@ -94,6 +94,8 @@ internal fun TournamentEditorClassesTab(
|
||||
var showAdd by remember { mutableStateOf(false) }
|
||||
var newName by remember { mutableStateOf("") }
|
||||
var newDoubles by remember { mutableStateOf(false) }
|
||||
var showDistributedDialog by remember { mutableStateOf(false) }
|
||||
var distributedMatches by remember { mutableStateOf<List<TournamentMatchDto>>(emptyList()) }
|
||||
if (showAdd) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showAdd = false },
|
||||
@@ -142,15 +144,56 @@ internal fun TournamentEditorClassesTab(
|
||||
OutlinedButton(onClick = {
|
||||
scope.launch {
|
||||
runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
val resp = withContext(Dispatchers.IO) {
|
||||
api.distributeTables(de.tsschulz.tt_tagebuch.shared.api.models.TournamentClubTournamentBody(clubId, tournamentId))
|
||||
}
|
||||
// If server returned updated matches, fetch full match details to show names
|
||||
if (resp.updated.isNotEmpty()) {
|
||||
val allMatches = withContext(Dispatchers.IO) { api.listMatches(clubId, tournamentId) }
|
||||
val ids = resp.updated.map { it.id }.toSet()
|
||||
distributedMatches = allMatches.filter { it.id in ids }
|
||||
showDistributedDialog = true
|
||||
}
|
||||
}.onFailure { onError(it.message) }
|
||||
onReload()
|
||||
}
|
||||
}, modifier = Modifier.weight(1f)) {
|
||||
Text(tr("tournaments.distributeTables", "Freie Tische verteilen"))
|
||||
}
|
||||
|
||||
if (showDistributedDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showDistributedDialog = false },
|
||||
title = { Text(tr("tournaments.distributeTablesResult", "Tischverteilung")) },
|
||||
text = {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
// Header
|
||||
Row(Modifier.fillMaxWidth().padding(bottom = 8.dp)) {
|
||||
Text(tr("tournaments.playerA", "Spieler A"), modifier = Modifier.weight(1f), fontWeight = FontWeight.SemiBold)
|
||||
Text(tr("tournaments.playerB", "Spieler B"), modifier = Modifier.weight(1f), fontWeight = FontWeight.SemiBold)
|
||||
Text(tr("tournaments.tableShort", "Tisch"), modifier = Modifier.width(48.dp), fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
Divider()
|
||||
LazyColumn {
|
||||
items(distributedMatches) { m ->
|
||||
val name1 = extractPlayerName(m.player1)
|
||||
val name2 = extractPlayerName(m.player2)
|
||||
Row(Modifier.fillMaxWidth().padding(vertical = 6.dp)) {
|
||||
Text(name1, modifier = Modifier.weight(1f))
|
||||
Text("–", modifier = Modifier.width(16.dp), textAlign = androidx.compose.ui.text.style.TextAlign.Center)
|
||||
Text(name2, modifier = Modifier.weight(1f))
|
||||
Text((m.tableNumber ?: "-").toString(), modifier = Modifier.width(48.dp))
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { showDistributedDialog = false }) { Text(tr("common.ok", "OK")) }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(top = 12.dp)) {
|
||||
classes.forEach { c ->
|
||||
@@ -222,6 +265,30 @@ private fun ClassRowEditor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractPlayerName(playerElem: JsonElement?): String {
|
||||
try {
|
||||
if (playerElem == null || playerElem is JsonNull) return "-"
|
||||
val obj = playerElem.jsonObject
|
||||
// Try member
|
||||
val member = obj["member"]?.jsonObject
|
||||
if (member != null) {
|
||||
val first = member["firstName"]?.jsonPrimitive?.contentOrNull ?: ""
|
||||
val last = member["lastName"]?.jsonPrimitive?.contentOrNull ?: ""
|
||||
val name = "$first $last".trim()
|
||||
if (name.isNotBlank()) return name
|
||||
}
|
||||
// Try direct fields
|
||||
val first = obj["firstName"]?.jsonPrimitive?.contentOrNull
|
||||
val last = obj["lastName"]?.jsonPrimitive?.contentOrNull
|
||||
if (first != null || last != null) return listOfNotNull(first, last).joinToString(" ")
|
||||
// Fallback to id
|
||||
val id = obj["id"]?.jsonPrimitive?.intOrNull
|
||||
return if (id != null) "#${id}" else "-"
|
||||
} catch (e: Exception) {
|
||||
return "-"
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun TournamentEditorParticipantsTab(
|
||||
clubId: Int,
|
||||
|
||||
@@ -156,10 +156,10 @@ class TournamentsApi(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun distributeTables(body: TournamentClubTournamentBody) {
|
||||
client.http.post("/api/tournament/matches/distribute") {
|
||||
suspend fun distributeTables(body: TournamentClubTournamentBody): de.tsschulz.tt_tagebuch.shared.api.models.DistributeTablesResponseDto {
|
||||
return client.http.post("/api/tournament/matches/distribute") {
|
||||
setBody(body)
|
||||
}
|
||||
}.body()
|
||||
}
|
||||
|
||||
suspend fun getGroups(clubId: Int, tournamentId: Int): JsonElement {
|
||||
|
||||
@@ -284,6 +284,12 @@ data class TournamentMatchDto(
|
||||
val tournamentResults: List<TournamentMatchSetResultDto>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DistributeTablesResponseDto(
|
||||
val updated: List<TournamentMatchDto> = emptyList(),
|
||||
val message: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TournamentAddMatchResultBody(
|
||||
val clubId: Int,
|
||||
|
||||
Reference in New Issue
Block a user