feat(tournament): add number of tables feature and update related logic
- Introduced a new field `numberOfTables` in the Tournament model to track the number of tables for tournaments. - Updated the tournament update logic to include `numberOfTables` when modifying tournament details. - Added a new endpoint to set the table number for matches, enhancing match management. - Updated frontend components to support the new `numberOfTables` feature, including input fields and table distribution logic. - Enhanced internationalization with new translation keys for table-related features.
This commit is contained in:
@@ -271,9 +271,9 @@ export const getTournament = async (req, res) => {
|
||||
export const updateTournament = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId } = req.params;
|
||||
const { name, date, winningSets } = req.body;
|
||||
const { name, date, winningSets, numberOfTables } = req.body;
|
||||
try {
|
||||
const tournament = await tournamentService.updateTournament(token, clubId, tournamentId, name, date, winningSets);
|
||||
const tournament = await tournamentService.updateTournament(token, clubId, tournamentId, name, date, winningSets, numberOfTables);
|
||||
// Emit Socket-Event
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.status(200).json(tournament);
|
||||
@@ -542,6 +542,21 @@ export const setMatchActive = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const setMatchTableNumber = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, matchId } = req.params;
|
||||
const { tableNumber } = req.body;
|
||||
try {
|
||||
await tournamentService.setMatchTableNumber(token, clubId, tournamentId, matchId, tableNumber);
|
||||
// Emit Socket-Event
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.status(200).json({ message: 'Tischnummer aktualisiert' });
|
||||
} catch (err) {
|
||||
console.error('[setMatchTableNumber] Error:', err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
};
|
||||
|
||||
// Externe Teilnehmer hinzufügen
|
||||
export const addExternalParticipant = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
-- Anzahl der Tische im Turnier
|
||||
ALTER TABLE tournament
|
||||
ADD COLUMN number_of_tables INT NULL DEFAULT NULL
|
||||
COMMENT 'Anzahl der Tische, auf denen gespielt wird';
|
||||
|
||||
-- Tischnummer pro Match
|
||||
ALTER TABLE tournament_match
|
||||
ADD COLUMN table_number INT NULL DEFAULT NULL
|
||||
COMMENT 'Tischnummer, an der das Match stattfindet';
|
||||
@@ -45,6 +45,12 @@ const Tournament = sequelize.define('Tournament', {
|
||||
field: 'mini_championship_year',
|
||||
comment: 'Jahr der Minimeisterschaft; nur gesetzt bei Minimeisterschaften'
|
||||
},
|
||||
numberOfTables: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
comment: 'Anzahl der Tische, auf denen gespielt wird'
|
||||
},
|
||||
}, {
|
||||
underscored: true,
|
||||
tableName: 'tournament',
|
||||
|
||||
@@ -63,6 +63,12 @@ const TournamentMatch = sequelize.define('TournamentMatch', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
tableNumber: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
comment: 'Tischnummer, an der das Match stattfindet'
|
||||
},
|
||||
}, {
|
||||
underscored: true,
|
||||
tableName: 'tournament_match',
|
||||
|
||||
3147
backend/package-lock.json
generated
3147
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,7 @@
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"axios": "^1.12.2",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bcrypt": "^6.0.0",
|
||||
"cors": "^2.8.5",
|
||||
"crypto": "^1.0.1",
|
||||
"csv-parser": "^3.0.0",
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
reopenMatch,
|
||||
deleteKnockoutMatches,
|
||||
setMatchActive,
|
||||
setMatchTableNumber,
|
||||
addExternalParticipant,
|
||||
getExternalParticipants,
|
||||
removeExternalParticipant,
|
||||
@@ -76,6 +77,7 @@ router.delete('/match/result', authenticate, deleteMatchResult);
|
||||
router.post("/match/reopen", authenticate, reopenMatch);
|
||||
router.post('/match/finish', authenticate, finishMatch);
|
||||
router.put('/match/:clubId/:tournamentId/:matchId/active', authenticate, setMatchActive);
|
||||
router.put('/match/:clubId/:tournamentId/:matchId/table', authenticate, setMatchTableNumber);
|
||||
router.get('/matches/:clubId/:tournamentId', authenticate, getTournamentMatches);
|
||||
router.post('/knockout', authenticate, startKnockout);
|
||||
router.delete("/matches/knockout", authenticate, deleteKnockoutMatches);
|
||||
|
||||
@@ -2138,8 +2138,8 @@ Ve // 2. Neues Turnier anlegen
|
||||
return JSON.parse(JSON.stringify(t));
|
||||
}
|
||||
|
||||
// Update Turnier (Name, Datum und Gewinnsätze)
|
||||
async updateTournament(userToken, clubId, tournamentId, name, date, winningSets) {
|
||||
// Update Turnier (Name, Datum, Gewinnsätze und Tischanzahl)
|
||||
async updateTournament(userToken, clubId, tournamentId, name, date, winningSets, numberOfTables) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const tournament = await Tournament.findOne({ where: { id: tournamentId, clubId } });
|
||||
if (!tournament) {
|
||||
@@ -2162,6 +2162,12 @@ Ve // 2. Neues Turnier anlegen
|
||||
}
|
||||
tournament.winningSets = winningSets;
|
||||
}
|
||||
if (numberOfTables !== undefined) {
|
||||
if (numberOfTables !== null && numberOfTables < 1) {
|
||||
throw new Error('Anzahl der Tische muss mindestens 1 sein');
|
||||
}
|
||||
tournament.numberOfTables = numberOfTables;
|
||||
}
|
||||
|
||||
await tournament.save();
|
||||
return JSON.parse(JSON.stringify(tournament));
|
||||
@@ -3355,6 +3361,20 @@ Ve // 2. Neues Turnier anlegen
|
||||
await match.save();
|
||||
}
|
||||
|
||||
async setMatchTableNumber(userToken, clubId, tournamentId, matchId, tableNumber) {
|
||||
await checkAccess(userToken, clubId);
|
||||
|
||||
const match = await TournamentMatch.findOne({
|
||||
where: { id: matchId, tournamentId }
|
||||
});
|
||||
if (!match) {
|
||||
throw new Error("Match nicht gefunden");
|
||||
}
|
||||
|
||||
match.tableNumber = tableNumber != null && tableNumber !== '' ? Number(tableNumber) : null;
|
||||
await match.save();
|
||||
}
|
||||
|
||||
async resetKnockout(userToken, clubId, tournamentId, classId = null) {
|
||||
await checkAccess(userToken, clubId);
|
||||
// lösche alle Matches außer Gruppenphase
|
||||
|
||||
20
frontend/package-lock.json
generated
20
frontend/package-lock.json
generated
@@ -1902,9 +1902,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.0.tgz",
|
||||
"integrity": "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==",
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz",
|
||||
"integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optional": true,
|
||||
"optionalDependencies": {
|
||||
@@ -2727,9 +2727,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jspdf": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.0.0.tgz",
|
||||
"integrity": "sha512-w12U97Z6edKd2tXDn3LzTLg7C7QLJlx0BPfM3ecjK2BckUl9/81vZ+r5gK4/3KQdhAcEZhENUxRhtgYBj75MqQ==",
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.1.0.tgz",
|
||||
"integrity": "sha512-xd1d/XRkwqnsq6FP3zH1Q+Ejqn2ULIJeDZ+FTKpaabVpZREjsJKRJwuokTNgdqOU+fl55KgbvgZ1pRTSWCP2kQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4",
|
||||
@@ -2739,7 +2739,7 @@
|
||||
"optionalDependencies": {
|
||||
"canvg": "^3.0.11",
|
||||
"core-js": "^3.6.0",
|
||||
"dompurify": "^3.2.4",
|
||||
"dompurify": "^3.3.1",
|
||||
"html2canvas": "^1.0.0-rc.5"
|
||||
}
|
||||
},
|
||||
@@ -2793,9 +2793,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"version": "4.17.23",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
||||
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
<div class="info-message">
|
||||
{{ message }}
|
||||
</div>
|
||||
<div v-if="details" class="info-details">
|
||||
<div v-if="detailsHtml" class="info-details" v-html="detailsHtml"></div>
|
||||
<div v-else-if="details" class="info-details">
|
||||
{{ details }}
|
||||
</div>
|
||||
<div v-if="$slots.default" class="info-extra">
|
||||
@@ -62,6 +63,10 @@ export default {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
detailsHtml: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'info',
|
||||
@@ -175,6 +180,15 @@ export default {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.info-details :deep(table) {
|
||||
color: #000;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.info-details :deep(tr:nth-child(even)) {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.info-extra {
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
|
||||
@@ -13,6 +13,10 @@
|
||||
{{ $t('tournaments.winningSets') }}:
|
||||
<input type="number" :value="winningSets" @input="$emit('update:winningSets', parseInt($event.target.value))" min="1" />
|
||||
</label>
|
||||
<label>
|
||||
{{ $t('tournaments.numberOfTables') }}:
|
||||
<input type="number" :value="numberOfTables" @input="$emit('update:numberOfTables', $event.target.value === '' ? null : parseInt($event.target.value))" min="1" placeholder="–" />
|
||||
</label>
|
||||
<button @click="$emit('generate-pdf')" class="btn-primary" style="margin-top: 1rem;">{{ $t('tournaments.exportPDF') }}</button>
|
||||
</div>
|
||||
<label class="checkbox-item">
|
||||
@@ -220,6 +224,10 @@ export default {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
numberOfTables: {
|
||||
type: [Number, null],
|
||||
default: null
|
||||
},
|
||||
isGroupTournament: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
@@ -307,6 +315,7 @@ export default {
|
||||
'update:tournamentName',
|
||||
'update:tournamentDate',
|
||||
'update:winningSets',
|
||||
'update:numberOfTables',
|
||||
'update:isGroupTournament',
|
||||
'generate-pdf',
|
||||
'edit-class',
|
||||
|
||||
@@ -7,6 +7,11 @@
|
||||
:selected-date="selectedDate"
|
||||
@update:modelValue="$emit('update:selectedViewClass', $event)"
|
||||
/>
|
||||
<div v-if="numberOfTables && (filteredGroupMatches.length || filteredKnockoutMatches.length)" class="distribute-tables-bar">
|
||||
<button @click="$emit('distribute-tables')" class="btn-primary">
|
||||
{{ $t('tournaments.distributeTables') }}
|
||||
</button>
|
||||
</div>
|
||||
<section v-if="filteredGroupMatches.length" class="group-matches">
|
||||
<h4>{{ $t('tournaments.groupMatches') }}</h4>
|
||||
<table>
|
||||
@@ -14,6 +19,7 @@
|
||||
<tr>
|
||||
<th>{{ $t('tournaments.round') }}</th>
|
||||
<th>{{ $t('tournaments.group') }}</th>
|
||||
<th v-if="numberOfTables">{{ $t('tournaments.table') }}</th>
|
||||
<th>{{ $t('tournaments.encounter') }}</th>
|
||||
<th>{{ $t('tournaments.result') }}</th>
|
||||
<th>{{ $t('tournaments.sets') }}</th>
|
||||
@@ -31,6 +37,22 @@
|
||||
{{ $t('tournaments.groupNumber') }} {{ m.groupNumber }}
|
||||
</template>
|
||||
</td>
|
||||
<td v-if="numberOfTables">
|
||||
<select
|
||||
:value="m.tableNumber || ''"
|
||||
@change="$emit('set-match-table', m, $event.target.value === '' ? null : parseInt($event.target.value))"
|
||||
class="table-select"
|
||||
:disabled="m.isFinished"
|
||||
>
|
||||
<option value="">–</option>
|
||||
<option
|
||||
v-for="t in numberOfTables"
|
||||
:key="t"
|
||||
:value="t"
|
||||
:disabled="occupiedTables.has(t) && m.tableNumber !== t"
|
||||
>{{ t }}{{ occupiedTables.has(t) && m.tableNumber !== t ? ' ●' : '' }}</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<template v-if="m.isFinished">
|
||||
<span v-if="winnerIsPlayer1(m)">
|
||||
@@ -126,6 +148,7 @@
|
||||
<tr>
|
||||
<th>{{ $t('tournaments.class') }}</th>
|
||||
<th>{{ $t('tournaments.round') }}</th>
|
||||
<th v-if="numberOfTables">{{ $t('tournaments.table') }}</th>
|
||||
<th>{{ $t('tournaments.encounter') }}</th>
|
||||
<th>{{ $t('tournaments.result') }}</th>
|
||||
<th>{{ $t('tournaments.sets') }}</th>
|
||||
@@ -136,6 +159,22 @@
|
||||
<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 v-if="numberOfTables">
|
||||
<select
|
||||
:value="m.tableNumber || ''"
|
||||
@change="$emit('set-match-table', m, $event.target.value === '' ? null : parseInt($event.target.value))"
|
||||
class="table-select"
|
||||
:disabled="m.isFinished"
|
||||
>
|
||||
<option value="">–</option>
|
||||
<option
|
||||
v-for="t in numberOfTables"
|
||||
:key="t"
|
||||
:value="t"
|
||||
:disabled="occupiedTables.has(t) && m.tableNumber !== t"
|
||||
>{{ t }}{{ occupiedTables.has(t) && m.tableNumber !== t ? ' ●' : '' }}</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<template v-if="m.isFinished">
|
||||
<span v-if="winnerIsPlayer1(m)">
|
||||
@@ -296,6 +335,10 @@ export default {
|
||||
pairings: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
numberOfTables: {
|
||||
type: [Number, null],
|
||||
default: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -330,6 +373,17 @@ export default {
|
||||
}
|
||||
return result;
|
||||
},
|
||||
occupiedTables() {
|
||||
// Tische, die von laufenden (nicht abgeschlossenen) Matches belegt sind
|
||||
const allMatches = [...this.groupMatches, ...this.knockoutMatches];
|
||||
const occupied = new Set();
|
||||
for (const m of allMatches) {
|
||||
if (m.tableNumber && !m.isFinished) {
|
||||
occupied.add(m.tableNumber);
|
||||
}
|
||||
}
|
||||
return occupied;
|
||||
},
|
||||
numberOfGroupsForSelectedClass() {
|
||||
// Zähle direkt die Gruppen für die ausgewählte Klasse
|
||||
// Nur Stage 1 Gruppen (stageId null/undefined) zählen
|
||||
@@ -374,6 +428,8 @@ export default {
|
||||
'finish-match',
|
||||
'reopen-match',
|
||||
'set-match-active',
|
||||
'set-match-table',
|
||||
'distribute-tables',
|
||||
'start-matches',
|
||||
'start-knockout',
|
||||
'reset-knockout'
|
||||
@@ -583,4 +639,17 @@ export default {
|
||||
.active-match:hover {
|
||||
background-color: #ffe69c !important;
|
||||
}
|
||||
|
||||
.distribute-tables-bar {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.table-select {
|
||||
width: 3.5rem;
|
||||
padding: 0.15rem 0.25rem;
|
||||
font-size: 0.85rem;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -574,6 +574,13 @@
|
||||
"name": "Name",
|
||||
"tournamentName": "Turniername",
|
||||
"winningSets": "Gewinnsätze",
|
||||
"numberOfTables": "Anzahl Tische",
|
||||
"table": "Tisch",
|
||||
"distributeTables": "Freie Tische verteilen",
|
||||
"distributeTablesResult": "Tischverteilung",
|
||||
"noFreeTables": "Keine freien Tische verfügbar.",
|
||||
"noAssignableMatches": "Keine Spiele verfügbar, bei denen beide Spieler frei sind.",
|
||||
"tablesDistributed": "Tische wurden verteilt.",
|
||||
"create": "Erstellen",
|
||||
"exportPDF": "PDF exportieren",
|
||||
"playInGroups": "Spielen in Gruppen",
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
:is-mini-championship="isMiniChampionship"
|
||||
:tournament-date="currentTournamentDate"
|
||||
:winning-sets="currentWinningSets"
|
||||
:number-of-tables="currentNumberOfTables"
|
||||
:is-group-tournament="isGroupTournament"
|
||||
:tournament-classes="tournamentClasses"
|
||||
:show-classes="showClasses"
|
||||
@@ -113,6 +114,7 @@
|
||||
@update:tournamentName="currentTournamentName = $event; updateTournament()"
|
||||
@update:tournamentDate="currentTournamentDate = $event; updateTournament()"
|
||||
@update:winningSets="currentWinningSets = $event; updateTournament()"
|
||||
@update:numberOfTables="currentNumberOfTables = $event; updateTournament()"
|
||||
@update:isGroupTournament="isGroupTournament = $event; onModusChange()"
|
||||
@generate-pdf="generatePDF"
|
||||
@edit-class="editClass"
|
||||
@@ -223,6 +225,7 @@
|
||||
:participants="participants"
|
||||
:groups="groups"
|
||||
:pairings="pairings"
|
||||
:number-of-tables="currentNumberOfTables"
|
||||
@update:selectedViewClass="selectedViewClass = $event"
|
||||
@update:activeMatchId="activeMatchId = $event"
|
||||
@update:editingResult="editingResult = $event"
|
||||
@@ -233,6 +236,8 @@
|
||||
@finish-match="finishMatch"
|
||||
@reopen-match="reopenMatch"
|
||||
@set-match-active="setMatchActive"
|
||||
@set-match-table="setMatchTableNumber"
|
||||
@distribute-tables="distributeTables"
|
||||
@start-matches="startMatches"
|
||||
@start-knockout="startKnockout"
|
||||
@reset-knockout="resetKnockout"
|
||||
@@ -263,6 +268,7 @@
|
||||
:title="infoDialog.title"
|
||||
:message="infoDialog.message"
|
||||
:details="infoDialog.details"
|
||||
:details-html="infoDialog.detailsHtml"
|
||||
:type="infoDialog.type"
|
||||
/>
|
||||
|
||||
@@ -320,6 +326,7 @@ export default {
|
||||
title: '',
|
||||
message: '',
|
||||
details: '',
|
||||
detailsHtml: '',
|
||||
type: 'info'
|
||||
},
|
||||
confirmDialog: {
|
||||
@@ -340,6 +347,7 @@ export default {
|
||||
currentTournamentName: '',
|
||||
currentTournamentDate: '',
|
||||
currentWinningSets: 3,
|
||||
currentNumberOfTables: null,
|
||||
dates: [],
|
||||
participants: [],
|
||||
selectedMember: null,
|
||||
@@ -1153,8 +1161,8 @@ export default {
|
||||
},
|
||||
|
||||
// Dialog Helper Methods
|
||||
async showInfo(title, message, details = '', type = 'info') {
|
||||
this.infoDialog = buildInfoConfig({ title, message, details, type });
|
||||
async showInfo(title, message, details = '', type = 'info', detailsHtml = '') {
|
||||
this.infoDialog = { ...buildInfoConfig({ title, message, details, type }), detailsHtml };
|
||||
},
|
||||
|
||||
async showConfirm(title, message, details = '', type = 'info', options = {}) {
|
||||
@@ -1220,6 +1228,7 @@ export default {
|
||||
this.currentWinningSets = (tournament.winningSets != null && tournament.winningSets >= 1)
|
||||
? tournament.winningSets
|
||||
: defaultSets;
|
||||
this.currentNumberOfTables = tournament.numberOfTables != null ? tournament.numberOfTables : null;
|
||||
this.isGroupTournament = tournament.type === 'groups';
|
||||
// Defensive: Backend/DB kann (historisch/UI-default) 0/null liefern.
|
||||
// Für gruppenbasierte Turniere ohne Klassen brauchen wir hier aber eine sinnvolle Zahl,
|
||||
@@ -1585,7 +1594,8 @@ export default {
|
||||
await apiClient.put(`/tournament/${this.currentClub}/${this.selectedDate}`, {
|
||||
name: this.currentTournamentName || this.currentTournamentDate,
|
||||
date: this.currentTournamentDate,
|
||||
winningSets: this.currentWinningSets
|
||||
winningSets: this.currentWinningSets,
|
||||
numberOfTables: this.currentNumberOfTables
|
||||
});
|
||||
// Prüfe, ob es einen Trainingstag für das neue Datum gibt
|
||||
await this.checkTrainingForDate(this.currentTournamentDate);
|
||||
@@ -1842,6 +1852,145 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
async setMatchTableNumber(match, tableNumber) {
|
||||
try {
|
||||
await apiClient.put(`/tournament/match/${this.currentClub}/${this.selectedDate}/${match.id}/table`, {
|
||||
tableNumber: tableNumber
|
||||
});
|
||||
await this.loadTournamentData();
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Setzen der Tischnummer:', error);
|
||||
await this.loadTournamentData();
|
||||
}
|
||||
},
|
||||
|
||||
async distributeTables() {
|
||||
const numberOfTables = this.currentNumberOfTables;
|
||||
if (!numberOfTables) return;
|
||||
|
||||
const allMatches = [...this.groupMatches, ...this.knockoutMatches];
|
||||
|
||||
// 1. Belegte Tische ermitteln (nur laufende, nicht abgeschlossene Matches)
|
||||
const occupiedTables = new Set();
|
||||
for (const m of allMatches) {
|
||||
if (m.tableNumber && !m.isFinished) {
|
||||
occupiedTables.add(m.tableNumber);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Freie Tische ermitteln
|
||||
const freeTables = [];
|
||||
for (let t = 1; t <= numberOfTables; t++) {
|
||||
if (!occupiedTables.has(t)) {
|
||||
freeTables.push(t);
|
||||
}
|
||||
}
|
||||
|
||||
if (freeTables.length === 0) {
|
||||
await this.showInfo(
|
||||
this.$t('tournaments.distributeTablesResult'),
|
||||
this.$t('tournaments.noFreeTables'),
|
||||
'', 'info'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Spieler ermitteln, die gerade in einem laufenden Match sind
|
||||
const busyPlayers = new Set();
|
||||
for (const m of allMatches) {
|
||||
if (!m.isFinished && m.isActive) {
|
||||
if (m.player1Id) busyPlayers.add(m.player1Id);
|
||||
if (m.player2Id) busyPlayers.add(m.player2Id);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Verfügbare Matches: nicht abgeschlossen, nicht aktiv, beide Spieler vorhanden und frei, kein Tisch zugewiesen
|
||||
const assignableMatches = allMatches.filter(m =>
|
||||
!m.isFinished &&
|
||||
!m.isActive &&
|
||||
m.player1Id && m.player2Id &&
|
||||
!busyPlayers.has(m.player1Id) &&
|
||||
!busyPlayers.has(m.player2Id) &&
|
||||
!m.tableNumber
|
||||
);
|
||||
|
||||
if (assignableMatches.length === 0) {
|
||||
await this.showInfo(
|
||||
this.$t('tournaments.distributeTablesResult'),
|
||||
this.$t('tournaments.noAssignableMatches'),
|
||||
'', 'info'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. Tische an Matches vergeben
|
||||
const assignments = [];
|
||||
const newBusyPlayers = new Set(busyPlayers);
|
||||
|
||||
for (const table of freeTables) {
|
||||
// Nächstes Match finden, bei dem beide Spieler noch frei sind
|
||||
const matchIdx = assignableMatches.findIndex(m =>
|
||||
!newBusyPlayers.has(m.player1Id) &&
|
||||
!newBusyPlayers.has(m.player2Id)
|
||||
);
|
||||
if (matchIdx === -1) break;
|
||||
|
||||
const match = assignableMatches.splice(matchIdx, 1)[0];
|
||||
newBusyPlayers.add(match.player1Id);
|
||||
newBusyPlayers.add(match.player2Id);
|
||||
assignments.push({ match, table });
|
||||
}
|
||||
|
||||
if (assignments.length === 0) {
|
||||
await this.showInfo(
|
||||
this.$t('tournaments.distributeTablesResult'),
|
||||
this.$t('tournaments.noAssignableMatches'),
|
||||
'', 'info'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 6. API-Aufrufe: Tisch setzen + Match als aktiv markieren
|
||||
try {
|
||||
for (const { match, table } of assignments) {
|
||||
await apiClient.put(`/tournament/match/${this.currentClub}/${this.selectedDate}/${match.id}/table`, {
|
||||
tableNumber: table
|
||||
});
|
||||
await apiClient.put(`/tournament/match/${this.currentClub}/${this.selectedDate}/${match.id}/active`, {
|
||||
isActive: true
|
||||
});
|
||||
}
|
||||
|
||||
// 7. Daten neu laden
|
||||
await this.loadTournamentData();
|
||||
|
||||
// 8. Dialog mit Ergebnis anzeigen
|
||||
const rows = assignments.map(({ match, table }) => {
|
||||
const name1 = this.getPlayerName(match.player1);
|
||||
const name2 = this.getPlayerName(match.player2);
|
||||
return `<tr><td style="font-weight:bold; padding:0.35rem 0.75rem;">${table}</td><td style="padding:0.35rem 0.75rem;">${name1}</td><td style="padding:0.35rem 0.75rem;">${name2}</td></tr>`;
|
||||
});
|
||||
const html = `<table style="margin:0.75rem auto; border-collapse:collapse; color:#000;"><thead><tr><th style="padding:0.35rem 0.75rem; border-bottom:2px solid #ccc; text-align:left;">Tisch</th><th style="padding:0.35rem 0.75rem; border-bottom:2px solid #ccc; text-align:left;">Spieler 1</th><th style="padding:0.35rem 0.75rem; border-bottom:2px solid #ccc; text-align:left;">Spieler 2</th></tr></thead><tbody>${rows.join('')}</tbody></table>`;
|
||||
|
||||
await this.showInfo(
|
||||
this.$t('tournaments.distributeTablesResult'),
|
||||
this.$t('tournaments.tablesDistributed'),
|
||||
'',
|
||||
'success',
|
||||
html
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Verteilen der Tische:', error);
|
||||
await this.loadTournamentData();
|
||||
await this.showInfo(
|
||||
this.$t('messages.error'),
|
||||
'Fehler beim Verteilen der Tische.',
|
||||
error.message || '',
|
||||
'error'
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
async generatePDF() {
|
||||
if (!this.selectedDate || this.selectedDate === 'new') {
|
||||
await this.showInfo('Hinweis', 'Bitte wählen Sie zuerst ein Turnier aus.', '', 'info');
|
||||
|
||||
Reference in New Issue
Block a user