feat(tournament): add group match creation and enhance match handling
- Implemented createGroupMatches function to generate matches for existing groups without altering group assignments. - Updated resetMatches function to support optional class filtering when resetting group matches. - Enhanced frontend components to filter and display group matches based on selected class, improving user experience. - Adjusted tournament results display to reflect accurate match statistics, including wins and losses.
This commit is contained in:
@@ -90,13 +90,16 @@
|
||||
<td><strong>G{{ String.fromCharCode(96 + group.groupNumber) }}{{ idx + 1 }}</strong></td>
|
||||
<td>{{ pl.position }}.</td>
|
||||
<td><span v-if="pl.seeded" class="seeded-star">★</span>{{ pl.name }}</td>
|
||||
<td>{{ pl.points }}</td>
|
||||
<td>{{ (pl.matchesWon || 0) * 2 }}:{{ (pl.matchesLost || 0) * 2 }}</td>
|
||||
<td>{{ pl.setsWon }}:{{ pl.setsLost }}</td>
|
||||
<td>
|
||||
{{ pl.setDiff >= 0 ? '+' + pl.setDiff : pl.setDiff }}
|
||||
</td>
|
||||
<td>
|
||||
{{ pl.pointsWon }}:{{ pl.pointsLost }} ({{ (pl.pointsWon - pl.pointsLost) >= 0 ? '+' + (pl.pointsWon - pl.pointsLost) : (pl.pointsWon - pl.pointsLost) }})
|
||||
{{ Math.abs(pl.pointsWon || 0) }}:{{ Math.abs(pl.pointsLost || 0) }}
|
||||
<span v-if="(Math.abs(pl.pointsWon || 0) - Math.abs(pl.pointsLost || 0)) !== 0" class="points-diff">
|
||||
({{ (Math.abs(pl.pointsWon || 0) - Math.abs(pl.pointsLost || 0)) >= 0 ? '+' : '' }}{{ Math.abs(pl.pointsWon || 0) - Math.abs(pl.pointsLost || 0) }})
|
||||
</span>
|
||||
</td>
|
||||
<td v-for="(opponent, oppIdx) in groupRankings[group.groupId]"
|
||||
:key="`match-${pl.id}-${opponent.id}`"
|
||||
@@ -116,12 +119,12 @@
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<div v-if="!matches.some(m => m.round === 'group')" class="reset-controls" style="margin-top:1rem">
|
||||
<div v-if="filteredGroupMatches.length === 0" class="reset-controls" style="margin-top:1rem">
|
||||
<button @click="$emit('create-matches')" class="btn-primary">
|
||||
▶️ Gruppenspiele berechnen
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="matches.some(m => m.round === 'group')" class="reset-controls" style="margin-top:1rem">
|
||||
<div v-if="filteredGroupMatches.length > 0" class="reset-controls" style="margin-top:1rem">
|
||||
<button @click="$emit('reset-matches')" class="trash-btn">
|
||||
🗑️ {{ $t('tournaments.resetGroupMatches') }}
|
||||
</button>
|
||||
@@ -207,7 +210,34 @@ export default {
|
||||
'create-matches',
|
||||
'highlight-match'
|
||||
],
|
||||
computed: {
|
||||
filteredGroupMatches() {
|
||||
return this.filterMatchesByClass(this.matches.filter(m => m.round === 'group'));
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
filterMatchesByClass(matches) {
|
||||
// Wenn keine Klasse ausgewählt ist (null), zeige alle
|
||||
if (this.selectedViewClass === null || this.selectedViewClass === undefined) {
|
||||
return matches;
|
||||
}
|
||||
// Wenn "Ohne Klasse" ausgewählt ist
|
||||
if (this.selectedViewClass === '__none__' || this.selectedViewClass === 'null') {
|
||||
return matches.filter(m => m.classId === null || m.classId === undefined);
|
||||
}
|
||||
// Filtere nach der ausgewählten Klasse
|
||||
const selectedId = Number(this.selectedViewClass);
|
||||
if (Number.isNaN(selectedId)) {
|
||||
return matches;
|
||||
}
|
||||
return matches.filter(m => {
|
||||
const matchClassId = m.classId;
|
||||
if (matchClassId === null || matchClassId === undefined) {
|
||||
return false;
|
||||
}
|
||||
return Number(matchClassId) === selectedId;
|
||||
});
|
||||
},
|
||||
shouldShowClass(classId) {
|
||||
// Wenn keine Klasse ausgewählt ist (null), zeige alle
|
||||
if (this.selectedViewClass === null || this.selectedViewClass === undefined) {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
:selected-date="selectedDate"
|
||||
@update:modelValue="$emit('update:selectedViewClass', $event)"
|
||||
/>
|
||||
<section v-if="groupMatches.length" class="group-matches">
|
||||
<section v-if="filteredGroupMatches.length" class="group-matches">
|
||||
<h4>{{ $t('tournaments.groupMatches') }}</h4>
|
||||
<table>
|
||||
<thead>
|
||||
@@ -21,7 +21,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="m in groupMatches" :key="m.id" :data-match-id="m.id" :class="{ 'active-match': activeMatchId === m.id, 'match-finished': m.isFinished, 'match-live': m.isActive }" @click="$emit('update:activeMatchId', m.id)">
|
||||
<tr v-for="m in filteredGroupMatches" :key="m.id" :data-match-id="m.id" :class="{ 'active-match': activeMatchId === m.id, 'match-finished': m.isFinished, 'match-live': m.isActive }" @click="$emit('update:activeMatchId', m.id)">
|
||||
<td>{{ m.groupRound }}</td>
|
||||
<td>
|
||||
<template v-if="getGroupClassName(m.groupId)">
|
||||
@@ -95,22 +95,22 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<div v-if="participants.length > 1 && !groupMatches.length && !knockoutMatches.length" class="start-matches" style="margin-top:1.5rem">
|
||||
<div v-if="participants.length > 1 && !filteredGroupMatches.length && !filteredKnockoutMatches.length" class="start-matches" style="margin-top:1.5rem">
|
||||
<button @click="$emit('start-matches')">
|
||||
{{ $t('tournaments.createMatches') }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="canStartKnockout && !showKnockout && getTotalNumberOfGroups > 1" class="ko-start">
|
||||
<div v-if="canStartKnockout && !showKnockout && numberOfGroupsForSelectedClass > 1" class="ko-start">
|
||||
<button @click="$emit('start-knockout')">
|
||||
{{ $t('tournaments.startKORound') }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="showKnockout && canResetKnockout && getTotalNumberOfGroups > 1" class="ko-reset" style="margin-top:1rem">
|
||||
<div v-if="showKnockout && canResetKnockout && numberOfGroupsForSelectedClass > 1" class="ko-reset" style="margin-top:1rem">
|
||||
<button @click="$emit('reset-knockout')" class="trash-btn">
|
||||
🗑️ {{ $t('tournaments.deleteKORound') }}
|
||||
</button>
|
||||
</div>
|
||||
<section v-if="showKnockout && getTotalNumberOfGroups > 1" class="ko-round">
|
||||
<section v-if="showKnockout && numberOfGroupsForSelectedClass > 1 && filteredKnockoutMatches.length" class="ko-round">
|
||||
<h4>{{ $t('tournaments.koRound') }}</h4>
|
||||
<table>
|
||||
<thead>
|
||||
@@ -124,7 +124,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="m in knockoutMatches" :key="m.id" :class="{ 'active-match': activeMatchId === m.id, 'match-finished': m.isFinished, 'match-live': m.isActive }" @click="$emit('update:activeMatchId', m.id)">
|
||||
<tr v-for="m in filteredKnockoutMatches" :key="m.id" :class="{ 'active-match': activeMatchId === m.id, 'match-finished': m.isFinished, 'match-live': m.isActive }" @click="$emit('update:activeMatchId', m.id)">
|
||||
<td>{{ getKnockoutMatchClassName(m) }}</td>
|
||||
<td>{{ m.round }}</td>
|
||||
<td>
|
||||
@@ -188,9 +188,9 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<section v-if="Object.keys(groupedRankingList).length > 0" class="ranking">
|
||||
<section v-if="Object.keys(filteredGroupedRankingList).length > 0" class="ranking">
|
||||
<h4>Rangliste</h4>
|
||||
<template v-for="(classKey, idx) in Object.keys(groupedRankingList).sort((a, b) => {
|
||||
<template v-for="(classKey, idx) in Object.keys(filteredGroupedRankingList).sort((a, b) => {
|
||||
const aNum = a === 'null' ? 999999 : parseInt(a);
|
||||
const bNum = b === 'null' ? 999999 : parseInt(b);
|
||||
return aNum - bNum;
|
||||
@@ -205,7 +205,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(entry, entryIdx) in groupedRankingList[classKey].filter(e => Number(e.position) <= 3)" :key="`${entry.member.id}-${entryIdx}`">
|
||||
<tr v-for="(entry, entryIdx) in filteredGroupedRankingList[classKey].filter(e => Number(e.position) <= 3)" :key="`${entry.member.id}-${entryIdx}`">
|
||||
<td>{{ entry.position }}.</td>
|
||||
<td>
|
||||
{{ entry.member.firstName }}
|
||||
@@ -289,6 +289,71 @@ export default {
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredGroupMatches() {
|
||||
return this.filterMatchesByClass(this.groupMatches);
|
||||
},
|
||||
filteredKnockoutMatches() {
|
||||
return this.filterMatchesByClass(this.knockoutMatches);
|
||||
},
|
||||
filteredGroupedRankingList() {
|
||||
// Wenn keine Klasse ausgewählt ist (null), zeige alle
|
||||
if (this.selectedViewClass === null || this.selectedViewClass === undefined) {
|
||||
return this.groupedRankingList;
|
||||
}
|
||||
// Wenn "Ohne Klasse" ausgewählt ist
|
||||
if (this.selectedViewClass === '__none__' || this.selectedViewClass === 'null') {
|
||||
const result = {};
|
||||
if (this.groupedRankingList['null']) {
|
||||
result['null'] = this.groupedRankingList['null'];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// Filtere nach der ausgewählten Klasse
|
||||
const selectedId = Number(this.selectedViewClass);
|
||||
if (Number.isNaN(selectedId)) {
|
||||
return this.groupedRankingList;
|
||||
}
|
||||
const result = {};
|
||||
const classKey = String(selectedId);
|
||||
if (this.groupedRankingList[classKey]) {
|
||||
result[classKey] = this.groupedRankingList[classKey];
|
||||
}
|
||||
return result;
|
||||
},
|
||||
numberOfGroupsForSelectedClass() {
|
||||
// Zähle direkt die Gruppen für die ausgewählte Klasse
|
||||
// Nur Stage 1 Gruppen (stageId null/undefined) zählen
|
||||
// Und nur Gruppen mit mindestens einem Teilnehmer
|
||||
let groupsToCount = this.groups.filter(g =>
|
||||
(!g.stageId || g.stageId === null || g.stageId === undefined) &&
|
||||
g.participants && Array.isArray(g.participants) && g.participants.length > 0
|
||||
);
|
||||
|
||||
// Wenn keine Klasse ausgewählt ist, zähle alle Stage 1 Gruppen
|
||||
if (this.selectedViewClass === null || this.selectedViewClass === undefined) {
|
||||
return groupsToCount.length;
|
||||
}
|
||||
|
||||
// Wenn "Ohne Klasse" ausgewählt ist
|
||||
if (this.selectedViewClass === '__none__' || this.selectedViewClass === 'null') {
|
||||
return groupsToCount.filter(g => g.classId === null || g.classId === undefined).length;
|
||||
}
|
||||
|
||||
// Filtere nach der ausgewählten Klasse
|
||||
const selectedId = Number(this.selectedViewClass);
|
||||
if (Number.isNaN(selectedId)) {
|
||||
return groupsToCount.length;
|
||||
}
|
||||
|
||||
return groupsToCount.filter(g => {
|
||||
if (g.classId === null || g.classId === undefined) {
|
||||
return false;
|
||||
}
|
||||
return Number(g.classId) === selectedId;
|
||||
}).length;
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
'update:selectedViewClass',
|
||||
'update:activeMatchId',
|
||||
@@ -305,6 +370,28 @@ export default {
|
||||
'reset-knockout'
|
||||
],
|
||||
methods: {
|
||||
filterMatchesByClass(matches) {
|
||||
// Wenn keine Klasse ausgewählt ist (null), zeige alle
|
||||
if (this.selectedViewClass === null || this.selectedViewClass === undefined) {
|
||||
return matches;
|
||||
}
|
||||
// Wenn "Ohne Klasse" ausgewählt ist
|
||||
if (this.selectedViewClass === '__none__' || this.selectedViewClass === 'null') {
|
||||
return matches.filter(m => m.classId === null || m.classId === undefined);
|
||||
}
|
||||
// Filtere nach der ausgewählten Klasse
|
||||
const selectedId = Number(this.selectedViewClass);
|
||||
if (Number.isNaN(selectedId)) {
|
||||
return matches;
|
||||
}
|
||||
return matches.filter(m => {
|
||||
const matchClassId = m.classId;
|
||||
if (matchClassId === null || matchClassId === undefined) {
|
||||
return false;
|
||||
}
|
||||
return Number(matchClassId) === selectedId;
|
||||
});
|
||||
},
|
||||
getGroupClassName(groupId) {
|
||||
if (!groupId) return '';
|
||||
const group = this.groups.find(g => g.groupId === groupId);
|
||||
|
||||
@@ -175,7 +175,7 @@
|
||||
@randomize-groups="randomizeGroups()"
|
||||
@reset-groups="resetGroups()"
|
||||
@reset-matches="resetMatches()"
|
||||
@create-matches="startMatches()"
|
||||
@create-matches="createMatches()"
|
||||
@highlight-match="highlightMatch"
|
||||
/>
|
||||
|
||||
@@ -487,9 +487,11 @@ export default {
|
||||
setsWon: p.setsWon || 0,
|
||||
setsLost: p.setsLost || 0,
|
||||
setDiff: p.setDiff || 0,
|
||||
pointsWon: p.pointsWon || 0,
|
||||
pointsLost: p.pointsLost || 0,
|
||||
pointRatio: p.pointRatio || 0
|
||||
pointsWon: Math.abs(p.pointsWon || 0),
|
||||
pointsLost: Math.abs(p.pointsLost || 0),
|
||||
pointRatio: p.pointRatio || 0,
|
||||
matchesWon: p.matchesWon || 0,
|
||||
matchesLost: p.matchesLost || 0
|
||||
}));
|
||||
});
|
||||
return rankings;
|
||||
@@ -1314,6 +1316,41 @@ export default {
|
||||
|
||||
this.showKnockout = this.matches.some(m => m.round !== 'group');
|
||||
},
|
||||
|
||||
async loadMatches() {
|
||||
// Lade nur die Matches, ohne die Teilnehmer-Daten zu überschreiben
|
||||
const mRes = await apiClient.get(
|
||||
`/tournament/matches/${this.currentClub}/${this.selectedDate}`
|
||||
);
|
||||
const grpMap = this.groups.reduce((m, g) => {
|
||||
m[g.groupId] = g.groupNumber;
|
||||
return m;
|
||||
}, {});
|
||||
|
||||
this.matches = mRes.data.map(m => {
|
||||
// Verwende groupId aus dem Backend, falls vorhanden, sonst aus den Spielern
|
||||
const matchGroupId = m.groupId || m.player1?.groupId || m.player2?.groupId;
|
||||
|
||||
// Stelle sicher, dass groupRound vorhanden ist (kann als group_round vom Backend kommen)
|
||||
const groupRound = m.groupRound || m.group_round || 0;
|
||||
|
||||
const groupNumber = grpMap[matchGroupId] || 0;
|
||||
|
||||
return {
|
||||
...m,
|
||||
groupId: matchGroupId,
|
||||
groupNumber: groupNumber,
|
||||
groupRound: groupRound,
|
||||
resultInput: '',
|
||||
isActive: m.isActive || false
|
||||
};
|
||||
});
|
||||
|
||||
// Setze Kollaps-Status: ausgeklappt wenn keine Spiele, eingeklappt wenn Spiele vorhanden
|
||||
this.showParticipants = this.matches.length === 0;
|
||||
|
||||
this.showKnockout = this.matches.some(m => m.round !== 'group');
|
||||
},
|
||||
|
||||
async handleTournamentChanged(data) {
|
||||
if (!data || !data.tournamentId) {
|
||||
@@ -1855,6 +1892,25 @@ export default {
|
||||
await this.loadTournamentData();
|
||||
},
|
||||
|
||||
async createMatches() {
|
||||
if (!this.isGroupTournament) {
|
||||
return;
|
||||
}
|
||||
if (!this.groups.length) {
|
||||
await this.createGroups();
|
||||
}
|
||||
// Übergebe classId, wenn eine Klasse ausgewählt ist (nicht '__none__' oder null)
|
||||
const classId = (this.selectedViewClass && this.selectedViewClass !== '__none__' && this.selectedViewClass !== null)
|
||||
? this.selectedViewClass
|
||||
: null;
|
||||
await apiClient.post('/tournament/matches/create', {
|
||||
clubId: this.currentClub,
|
||||
tournamentId: this.selectedDate,
|
||||
classId: classId
|
||||
});
|
||||
await this.loadTournamentData();
|
||||
},
|
||||
|
||||
async onModusChange() {
|
||||
const type = this.isGroupTournament ? 'groups' : 'knockout';
|
||||
const desired = Math.max(1, parseInt(String(this.numberOfGroups), 10) || 1);
|
||||
@@ -1877,9 +1933,14 @@ export default {
|
||||
},
|
||||
|
||||
async resetMatches() {
|
||||
// Übergebe classId, wenn eine Klasse ausgewählt ist (nicht '__none__' oder null)
|
||||
const classId = (this.selectedViewClass && this.selectedViewClass !== '__none__' && this.selectedViewClass !== null)
|
||||
? this.selectedViewClass
|
||||
: null;
|
||||
await apiClient.post('/tournament/matches/reset', {
|
||||
clubId: this.currentClub,
|
||||
tournamentId: this.selectedDate
|
||||
tournamentId: this.selectedDate,
|
||||
classId: classId
|
||||
});
|
||||
await this.loadTournamentData();
|
||||
},
|
||||
@@ -2201,8 +2262,17 @@ export default {
|
||||
|
||||
console.log('[updateParticipantGroup] Updating participant:', participant.id, 'to groupNumber:', groupNumber, 'isExternal:', participant.isExternal);
|
||||
|
||||
// Aktualisiere lokal
|
||||
participant.groupNumber = groupNumber;
|
||||
// Speichere die alte groupNumber für den Fall eines Fehlers
|
||||
const oldGroupNumber = participant.groupNumber;
|
||||
|
||||
// Aktualisiere lokal sofort für responsive UI (mit Vue-Reaktivität)
|
||||
if (this.$set) {
|
||||
// Vue 2
|
||||
this.$set(participant, 'groupNumber', groupNumber);
|
||||
} else {
|
||||
// Vue 3
|
||||
participant.groupNumber = groupNumber;
|
||||
}
|
||||
|
||||
// Sende nur diesen einen Teilnehmer an Backend
|
||||
try {
|
||||
@@ -2221,17 +2291,86 @@ export default {
|
||||
this.groups = [...response.data];
|
||||
console.log('[updateParticipantGroup] Updated groups:', this.groups);
|
||||
} else {
|
||||
// Fallback: Lade Daten neu
|
||||
await this.loadTournamentData();
|
||||
// Fallback: Lade Gruppen neu
|
||||
const gRes = await apiClient.get('/tournament/groups', {
|
||||
params: {
|
||||
clubId: this.currentClub,
|
||||
tournamentId: this.selectedDate
|
||||
}
|
||||
});
|
||||
const groupsData = Array.isArray(gRes.data)
|
||||
? gRes.data
|
||||
: (Array.isArray(gRes.data?.groups) ? gRes.data.groups : []);
|
||||
this.groups = [...groupsData];
|
||||
}
|
||||
// Force Vue update, um sicherzustellen, dass die Gruppenübersicht aktualisiert wird
|
||||
this.$forceUpdate();
|
||||
|
||||
// Aktualisiere auch die groupId des Teilnehmers basierend auf der neuen groupNumber
|
||||
if (groupNumber !== null) {
|
||||
const newGroup = this.groups.find(g => g.groupNumber === groupNumber);
|
||||
if (newGroup) {
|
||||
if (this.$set) {
|
||||
this.$set(participant, 'groupId', newGroup.groupId);
|
||||
} else {
|
||||
participant.groupId = newGroup.groupId;
|
||||
}
|
||||
} else {
|
||||
// Gruppe nicht gefunden, setze groupId auf null
|
||||
if (this.$set) {
|
||||
this.$set(participant, 'groupId', null);
|
||||
} else {
|
||||
participant.groupId = null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// groupNumber ist null, also auch groupId auf null
|
||||
if (this.$set) {
|
||||
this.$set(participant, 'groupId', null);
|
||||
} else {
|
||||
participant.groupId = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Lade Matches neu, da sich die Gruppenzuordnung geändert hat
|
||||
await this.loadMatches();
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Aktualisieren der Gruppe:', error);
|
||||
// Bei Fehler: Lade Daten neu und setze groupNumber zurück
|
||||
participant.groupNumber = participant.groupId ? this.groups.find(g => g.groupId === participant.groupId)?.groupNumber || null : null;
|
||||
await this.loadTournamentData();
|
||||
this.$forceUpdate();
|
||||
// Bei Fehler: Setze groupNumber zurück auf den alten Wert
|
||||
if (this.$set) {
|
||||
this.$set(participant, 'groupNumber', oldGroupNumber);
|
||||
} else {
|
||||
participant.groupNumber = oldGroupNumber;
|
||||
}
|
||||
// Lade Gruppen neu, um sicherzustellen, dass groupId korrekt ist
|
||||
try {
|
||||
const gRes = await apiClient.get('/tournament/groups', {
|
||||
params: {
|
||||
clubId: this.currentClub,
|
||||
tournamentId: this.selectedDate
|
||||
}
|
||||
});
|
||||
const groupsData = Array.isArray(gRes.data)
|
||||
? gRes.data
|
||||
: (Array.isArray(gRes.data?.groups) ? gRes.data.groups : []);
|
||||
this.groups = [...groupsData];
|
||||
// Setze groupId basierend auf dem alten groupNumber
|
||||
if (oldGroupNumber !== null) {
|
||||
const oldGroup = this.groups.find(g => g.groupNumber === oldGroupNumber);
|
||||
const oldGroupId = oldGroup ? oldGroup.groupId : null;
|
||||
if (this.$set) {
|
||||
this.$set(participant, 'groupId', oldGroupId);
|
||||
} else {
|
||||
participant.groupId = oldGroupId;
|
||||
}
|
||||
} else {
|
||||
if (this.$set) {
|
||||
this.$set(participant, 'groupId', null);
|
||||
} else {
|
||||
participant.groupId = null;
|
||||
}
|
||||
}
|
||||
} catch (loadError) {
|
||||
console.error('Fehler beim Neuladen der Gruppen:', loadError);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user