feat(tournament): implement multi-stage tournament support with intermediate and final stages
- Added backend controller for tournament stages with endpoints to get, upsert, and advance stages. - Created database migration for new tables: tournament_stage and tournament_stage_advancement. - Updated models for TournamentStage and TournamentStageAdvancement. - Enhanced frontend components to manage tournament stages, including configuration for intermediate and final rounds. - Implemented logic for saving and advancing tournament stages, including handling of pool rules and third place matches. - Added error handling and loading states in the frontend for better user experience.
This commit is contained in:
@@ -47,11 +47,152 @@
|
||||
@update:newClassGender="$emit('update:newClassGender', $event)"
|
||||
@update:newClassMinBirthYear="$emit('update:newClassMinBirthYear', $event)"
|
||||
/>
|
||||
|
||||
<div class="stage-config" style="margin-top: 1.5rem;">
|
||||
<h3>Zwischenrunde & Endrunde</h3>
|
||||
<div v-if="stageConfig.loading" style="opacity: 0.8;">
|
||||
Lade Zwischenrunden …
|
||||
</div>
|
||||
<div v-else>
|
||||
<p style="margin: 0.25rem 0 1rem; opacity: 0.8;">
|
||||
Zwischenrunde ist optional. Wenn du sie aktivierst, gibt es danach immer eine Endrunde.
|
||||
KO-Endrunde wird als <strong>ein einziges Feld</strong> erzeugt.
|
||||
</p>
|
||||
|
||||
<div style="display: grid; gap: 0.75rem; max-width: 720px;">
|
||||
<label class="checkbox-item" style="margin: 0.25rem 0;">
|
||||
<input type="checkbox" v-model="stageConfig.useIntermediateStage" />
|
||||
<span>Zwischenrunde verwenden</span>
|
||||
</label>
|
||||
|
||||
<div v-if="stageConfig.useIntermediateStage" style="border: 1px solid #eee; border-radius: 6px; padding: 0.75rem;">
|
||||
<strong>Zwischenrunde (Runde 2)</strong>
|
||||
<label>
|
||||
Runde 2 Modus:
|
||||
<select v-model="stageConfig.stage2Type">
|
||||
<option value="groups">Gruppen</option>
|
||||
<option value="knockout">KO</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label v-if="stageConfig.stage2Type === 'groups'">
|
||||
Anzahl Gruppen in Runde 2 (pro Pool):
|
||||
<input type="number" min="1" v-model.number="stageConfig.stage2GroupCount" />
|
||||
</label>
|
||||
|
||||
<div class="pool-config" style="border: 1px solid #ddd; border-radius: 6px; padding: 0.75rem;">
|
||||
<div style="display:flex; align-items:center; justify-content: space-between; gap: 1rem;">
|
||||
<strong>Weiterkommen: Vorrunde → Zwischenrunde (1→2)</strong>
|
||||
<button class="btn-secondary" @click="addPoolRule('12')" type="button">Pool-Regel hinzufügen</button>
|
||||
</div>
|
||||
<div v-if="stageConfig.pools12.length === 0" style="margin-top: 0.5rem; opacity: 0.8;">
|
||||
Noch keine Regeln. Beispiel: Plätze 1 & 2 -> obere Runde-2-Gruppen.
|
||||
</div>
|
||||
<div v-for="(rule, idx) in stageConfig.pools12" :key="`12-${idx}`" style="display:grid; gap:0.5rem; margin-top:0.75rem; padding-top:0.75rem; border-top: 1px solid #eee;">
|
||||
<label>
|
||||
Plätze aus jeder Gruppe (z.B. 1,2):
|
||||
<input type="text" v-model="rule.fromPlacesText" />
|
||||
</label>
|
||||
|
||||
<div style="display:flex; gap: 0.75rem; flex-wrap: wrap; align-items: end;">
|
||||
<label>
|
||||
Ziel:
|
||||
<select v-model="rule.targetType">
|
||||
<option value="groups">Gruppen</option>
|
||||
<option value="knockout">KO</option>
|
||||
</select>
|
||||
</label>
|
||||
<label v-if="rule.targetType === 'groups'">
|
||||
Ziel-Gruppenanzahl:
|
||||
<input type="number" min="1" v-model.number="rule.targetGroupCount" />
|
||||
</label>
|
||||
<button class="btn-danger" type="button" @click="removePoolRule(idx)">Entfernen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border: 1px solid #eee; border-radius: 6px; padding: 0.75rem;">
|
||||
<strong>Endrunde (Runde {{ stageConfig.useIntermediateStage ? 3 : 2 }})</strong>
|
||||
<label style="margin-top: 0.5rem;">
|
||||
Endrunde Modus:
|
||||
<select v-model="stageConfig.finalStageType">
|
||||
<option value="groups">Gruppen</option>
|
||||
<option value="knockout">KO</option>
|
||||
</select>
|
||||
</label>
|
||||
<label v-if="stageConfig.finalStageType === 'knockout'" style="margin-top: 0.5rem; display:block;">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="stageConfig.finalStageThirdPlace"
|
||||
@change="onThirdPlaceToggle($event.target.checked)"
|
||||
@click.stop
|
||||
/>
|
||||
Platz 3 ausspielen
|
||||
</label>
|
||||
<label v-if="stageConfig.finalStageType === 'groups'">
|
||||
Anzahl Gruppen in Endrunde:
|
||||
<input type="number" min="1" v-model.number="stageConfig.finalStageGroupCount" />
|
||||
</label>
|
||||
|
||||
<div class="pool-config" style="border: 1px solid #ddd; border-radius: 6px; padding: 0.75rem; margin-top: 0.75rem;">
|
||||
<div style="display:flex; align-items:center; justify-content: space-between; gap: 1rem;">
|
||||
<strong>Weiterkommen: {{ stageConfig.useIntermediateStage ? 'Zwischenrunde → Endrunde (2→3)' : 'Vorrunde → Endrunde (1→3)' }}</strong>
|
||||
<button class="btn-secondary" @click="addPoolRule('final')" type="button">Pool-Regel hinzufügen</button>
|
||||
</div>
|
||||
<div v-if="finalPools.length === 0" style="margin-top: 0.5rem; opacity: 0.8;">
|
||||
Beispiel: Plätze 1 & 2 -> Endrunde.
|
||||
</div>
|
||||
<div v-for="(rule, idx) in finalPools" :key="`final-${idx}`" style="display:grid; gap:0.5rem; margin-top:0.75rem; padding-top:0.75rem; border-top: 1px solid #eee;">
|
||||
<label>
|
||||
Plätze aus jeder Gruppe (z.B. 1,2):
|
||||
<input type="text" v-model="rule.fromPlacesText" />
|
||||
</label>
|
||||
<div style="display:flex; gap: 0.75rem; flex-wrap: wrap; align-items: end;">
|
||||
<label>
|
||||
Ziel:
|
||||
<select v-model="rule.targetType">
|
||||
<option value="groups">Gruppen</option>
|
||||
<option value="knockout">KO</option>
|
||||
</select>
|
||||
</label>
|
||||
<label v-if="rule.targetType === 'groups'">
|
||||
Ziel-Gruppenanzahl:
|
||||
<input type="number" min="1" v-model.number="rule.targetGroupCount" />
|
||||
</label>
|
||||
<button class="btn-danger" type="button" @click="removePoolRule(idx, 'final')">Entfernen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; gap: 0.75rem; flex-wrap: wrap;">
|
||||
<button class="btn-primary" type="button" @click="onSaveClick">Runden speichern</button>
|
||||
<button v-if="!stageConfig.useIntermediateStage" class="btn-secondary" type="button" @click="advanceStage(1, 3)">
|
||||
Endrunde aus Vorrunde erstellen
|
||||
</button>
|
||||
<button v-else class="btn-secondary" type="button" @click="advanceStage(1, 2)">
|
||||
Zwischenrunde aus Vorrunde erstellen
|
||||
</button>
|
||||
<button v-if="stageConfig.useIntermediateStage" class="btn-secondary" type="button" @click="advanceStage(2, 3)">
|
||||
Endrunde aus Zwischenrunde erstellen
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="stageConfig.error" style="color: #b00020; white-space: pre-wrap;">
|
||||
{{ stageConfig.error }}
|
||||
</div>
|
||||
<div v-if="stageConfig.success" style="color: #1b5e20;">
|
||||
{{ stageConfig.success }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TournamentClassList from './TournamentClassList.vue';
|
||||
import apiClient from '../../apiClient';
|
||||
|
||||
export default {
|
||||
name: 'TournamentConfigTab',
|
||||
@@ -59,6 +200,14 @@ export default {
|
||||
TournamentClassList
|
||||
},
|
||||
props: {
|
||||
clubId: {
|
||||
type: [Number, String],
|
||||
required: true
|
||||
},
|
||||
tournamentId: {
|
||||
type: [Number, String],
|
||||
required: true
|
||||
},
|
||||
tournamentName: {
|
||||
type: String,
|
||||
required: true
|
||||
@@ -120,6 +269,36 @@ export default {
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
stageConfig: {
|
||||
loading: false,
|
||||
useIntermediateStage: true,
|
||||
stage2Type: 'groups',
|
||||
stage2GroupCount: 2,
|
||||
pools12: [],
|
||||
poolsFinal: [],
|
||||
finalStageType: 'knockout',
|
||||
finalStageThirdPlace: false,
|
||||
finalStageGroupCount: 1,
|
||||
error: null,
|
||||
success: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
finalPools() {
|
||||
return this.stageConfig.poolsFinal;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
tournamentId: {
|
||||
immediate: true,
|
||||
handler() {
|
||||
this.loadStageConfig();
|
||||
}
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
'update:tournamentName',
|
||||
'update:tournamentDate',
|
||||
@@ -142,6 +321,336 @@ export default {
|
||||
'update:newClassGender',
|
||||
'update:newClassMinBirthYear'
|
||||
]
|
||||
,
|
||||
methods: {
|
||||
onSaveClick() {
|
||||
this.saveStageConfig();
|
||||
},
|
||||
|
||||
async onThirdPlaceToggle(checked) {
|
||||
// UI sofort aktualisieren
|
||||
this.stageConfig.finalStageThirdPlace = checked === true;
|
||||
this.stageConfig.error = null;
|
||||
this.stageConfig.success = null;
|
||||
|
||||
if (!this.clubId || !this.tournamentId) {
|
||||
this.stageConfig.error = 'Kann nicht speichern: clubId oder tournamentId fehlt.';
|
||||
return;
|
||||
}
|
||||
|
||||
// Unabhängig von Pool-Regeln speichern:
|
||||
// wir laden die aktuelle Stage-Konfiguration und patchen nur target.thirdPlace.
|
||||
try {
|
||||
const getRes = await apiClient.get('/tournament/stages', {
|
||||
params: {
|
||||
clubId: Number(this.clubId),
|
||||
tournamentId: Number(this.tournamentId),
|
||||
}
|
||||
});
|
||||
if (getRes.status >= 400) throw new Error(getRes.data?.error || 'Fehler beim Laden');
|
||||
|
||||
if (!Array.isArray(getRes.data?.stages) || !Array.isArray(getRes.data?.advancements)) {
|
||||
throw new Error('Fehlerhafte Antwort vom Server (stages/advancements fehlen).');
|
||||
}
|
||||
|
||||
let stages = Array.isArray(getRes.data?.stages) ? getRes.data.stages : [];
|
||||
let advancements = Array.isArray(getRes.data?.advancements) ? getRes.data.advancements : [];
|
||||
|
||||
// Wenn noch keine Konfiguration existiert, können wir das Flag nicht "isoliert" speichern.
|
||||
// Dann erzeugen wir eine minimale Stage+Advancement-Konfiguration (mit Default-Poolregel),
|
||||
// damit thirdPlace überhaupt persistiert werden kann.
|
||||
if (stages.length === 0 && advancements.length === 0) {
|
||||
const built = this.buildPayload();
|
||||
stages = built.stages;
|
||||
advancements = built.advancements;
|
||||
|
||||
// Falls der Nutzer noch keine Pool-Regeln angelegt hat, legen wir eine Default-Regel an,
|
||||
// damit der Backend-Validator nicht abbricht.
|
||||
for (const adv of advancements) {
|
||||
const pools = adv?.config?.pools;
|
||||
if (!Array.isArray(pools) || pools.length === 0) {
|
||||
adv.config = adv.config || {};
|
||||
adv.config.pools = [{
|
||||
fromPlaces: [1, 2],
|
||||
target: { type: 'knockout', singleField: true, thirdPlace: checked === true },
|
||||
}];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const patchedAdvancements = advancements.map(a => {
|
||||
// Wenn wir gerade initial erzeugen (adv hat fromStageIndex/toStageIndex), patchen wir direkt.
|
||||
if (a && a.fromStageIndex != null && a.toStageIndex != null) {
|
||||
const isFinal = (Number(a.fromStageIndex) === 1 && Number(a.toStageIndex) === 3)
|
||||
|| (Number(a.fromStageIndex) === 2 && Number(a.toStageIndex) === 3);
|
||||
if (!isFinal) return a;
|
||||
const cfg = a?.config && typeof a.config === 'object' ? a.config : {};
|
||||
const pools = Array.isArray(cfg.pools) ? cfg.pools : [];
|
||||
return {
|
||||
...a,
|
||||
config: {
|
||||
...cfg,
|
||||
pools: pools.map(p => {
|
||||
const target = p?.target && typeof p.target === 'object' ? p.target : {};
|
||||
if (target.type === 'knockout') {
|
||||
return { ...p, target: { ...target, thirdPlace: checked === true } };
|
||||
}
|
||||
return p;
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Existing DB-shape: patch nur Final-Übergang (1->3 / 2->3) via stageId mapping
|
||||
// Nur Final-Übergang patchen: (1->3) oder (2->3)
|
||||
const stageById = new Map(stages.map(s => [Number(s.id), s]));
|
||||
const fromIdx = Number(stageById.get(Number(a?.fromStageId))?.index);
|
||||
const toIdx = Number(stageById.get(Number(a?.toStageId))?.index);
|
||||
const isFinal = (fromIdx === 1 && toIdx === 3) || (fromIdx === 2 && toIdx === 3);
|
||||
if (!isFinal) return a;
|
||||
|
||||
const cfg = a?.config && typeof a.config === 'object' ? a.config : {};
|
||||
const pools = Array.isArray(cfg.pools) ? cfg.pools : [];
|
||||
return {
|
||||
...a,
|
||||
config: {
|
||||
...cfg,
|
||||
pools: pools.map(p => {
|
||||
const target = p?.target && typeof p.target === 'object' ? p.target : {};
|
||||
if (target.type === 'knockout') {
|
||||
return { ...p, target: { ...target, thirdPlace: checked === true } };
|
||||
}
|
||||
return p;
|
||||
})
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const putRes = await apiClient.put('/tournament/stages', {
|
||||
clubId: Number(this.clubId),
|
||||
tournamentId: Number(this.tournamentId),
|
||||
stages,
|
||||
advancements: patchedAdvancements,
|
||||
});
|
||||
if (putRes.status >= 400) throw new Error(putRes.data?.error || 'Fehler beim Speichern');
|
||||
|
||||
await this.loadStageConfig();
|
||||
this.stageConfig.success = 'Gespeichert.';
|
||||
} catch (e) {
|
||||
this.stageConfig.error = e?.message || String(e);
|
||||
}
|
||||
},
|
||||
async loadStageConfig() {
|
||||
if (!this.clubId || !this.tournamentId) return;
|
||||
this.stageConfig.loading = true;
|
||||
this.stageConfig.error = null;
|
||||
this.stageConfig.success = null;
|
||||
|
||||
try {
|
||||
const res = await apiClient.get('/tournament/stages', {
|
||||
params: {
|
||||
clubId: Number(this.clubId),
|
||||
tournamentId: Number(this.tournamentId),
|
||||
}
|
||||
});
|
||||
if (res.status >= 400) throw new Error(res.data?.error || 'Fehler beim Laden der Zwischenrunden');
|
||||
|
||||
const stages = Array.isArray(res.data?.stages) ? res.data.stages : [];
|
||||
const advancements = Array.isArray(res.data?.advancements) ? res.data.advancements : [];
|
||||
|
||||
// Zwischenrunde optional: wenn Stage 2 fehlt, gehen wir von direkter Endrunde aus.
|
||||
const stage2 = stages.find(s => Number(s.index) === 2);
|
||||
this.stageConfig.useIntermediateStage = !!stage2;
|
||||
|
||||
if (stage2) {
|
||||
this.stageConfig.stage2Type = stage2.type || 'groups';
|
||||
this.stageConfig.stage2GroupCount = stage2.numberOfGroups || 2;
|
||||
}
|
||||
|
||||
const stage3 = stages.find(s => Number(s.index) === 3);
|
||||
if (stage3) {
|
||||
this.stageConfig.finalStageType = stage3.type || 'knockout';
|
||||
this.stageConfig.finalStageGroupCount = stage3.numberOfGroups || 1;
|
||||
} else {
|
||||
// Fallback, wenn bisher nur 1->2 existierte
|
||||
this.stageConfig.finalStageType = 'knockout';
|
||||
this.stageConfig.finalStageGroupCount = 1;
|
||||
}
|
||||
|
||||
const adv12 = advancements.find(a => Number(a?.fromStageId) && Number(a?.toStageId) && Number(stages.find(s => s.id === a.fromStageId)?.index) === 1 && Number(stages.find(s => s.id === a.toStageId)?.index) === 2) || null;
|
||||
const advFinal = advancements.find(a => {
|
||||
const fromIdx = Number(stages.find(s => s.id === a.fromStageId)?.index);
|
||||
const toIdx = Number(stages.find(s => s.id === a.toStageId)?.index);
|
||||
return (fromIdx === 1 && toIdx === 3) || (fromIdx === 2 && toIdx === 3);
|
||||
}) || null;
|
||||
|
||||
const pools12 = Array.isArray(adv12?.config?.pools) ? adv12.config.pools : [];
|
||||
this.stageConfig.pools12 = pools12.map(p => ({
|
||||
fromPlacesText: Array.isArray(p.fromPlaces) ? p.fromPlaces.join(',') : '',
|
||||
targetType: p?.target?.type || 'groups',
|
||||
targetGroupCount: p?.target?.groupCount || this.stageConfig.stage2GroupCount || 2,
|
||||
}));
|
||||
|
||||
const poolsFinal = Array.isArray(advFinal?.config?.pools) ? advFinal.config.pools : [];
|
||||
this.stageConfig.poolsFinal = poolsFinal.map(p => ({
|
||||
fromPlacesText: Array.isArray(p.fromPlaces) ? p.fromPlaces.join(',') : '',
|
||||
targetType: p?.target?.type || this.stageConfig.finalStageType || 'knockout',
|
||||
targetGroupCount: p?.target?.groupCount || this.stageConfig.finalStageGroupCount || 1,
|
||||
}));
|
||||
|
||||
// KO-Flag gilt für die gesamte Endrunde: true, sobald irgendeine Final-KO-Regel thirdPlace=true hat.
|
||||
this.stageConfig.finalStageThirdPlace = poolsFinal.some(p => p?.target?.type === 'knockout' && p?.target?.thirdPlace === true);
|
||||
} catch (e) {
|
||||
this.stageConfig.error = e?.message || String(e);
|
||||
} finally {
|
||||
this.stageConfig.loading = false;
|
||||
}
|
||||
},
|
||||
addPoolRule(which = '12') {
|
||||
const isFinal = which === 'final';
|
||||
const targetArray = isFinal ? this.stageConfig.poolsFinal : this.stageConfig.pools12;
|
||||
targetArray.push({
|
||||
fromPlacesText: '1,2',
|
||||
targetType: 'groups',
|
||||
targetGroupCount: isFinal
|
||||
? (this.stageConfig.finalStageGroupCount || 1)
|
||||
: (this.stageConfig.stage2GroupCount || 2),
|
||||
});
|
||||
},
|
||||
removePoolRule(idx, which = '12') {
|
||||
const targetArray = (which === 'final') ? this.stageConfig.poolsFinal : this.stageConfig.pools12;
|
||||
targetArray.splice(idx, 1);
|
||||
},
|
||||
buildPoolsPayload(rules, defaultGroupCount, knockoutSingleField = false, knockoutThirdPlace = false) {
|
||||
return (rules || [])
|
||||
.map(r => {
|
||||
const fromPlaces = String(r.fromPlacesText || '')
|
||||
.split(',')
|
||||
.map(x => Number(String(x).trim()))
|
||||
.filter(n => Number.isFinite(n) && n > 0);
|
||||
return {
|
||||
fromPlaces,
|
||||
target: r.targetType === 'knockout'
|
||||
? { type: 'knockout', singleField: knockoutSingleField, thirdPlace: knockoutThirdPlace }
|
||||
: { type: 'groups', groupCount: Math.max(1, Number(r.targetGroupCount || defaultGroupCount || 1)) }
|
||||
};
|
||||
})
|
||||
.filter(p => p.fromPlaces.length > 0);
|
||||
},
|
||||
buildPayload() {
|
||||
const pools12 = this.stageConfig.useIntermediateStage
|
||||
? this.buildPoolsPayload(this.stageConfig.pools12, this.stageConfig.stage2GroupCount || 2, false)
|
||||
: [];
|
||||
const poolsFinal = this.buildPoolsPayload(
|
||||
this.stageConfig.poolsFinal,
|
||||
this.stageConfig.finalStageGroupCount || 1,
|
||||
true,
|
||||
this.stageConfig.finalStageThirdPlace === true
|
||||
);
|
||||
|
||||
const stages = [
|
||||
{ index: 1, type: 'groups', name: 'Vorrunde' },
|
||||
];
|
||||
const advancements = [];
|
||||
|
||||
if (this.stageConfig.useIntermediateStage) {
|
||||
stages.push({
|
||||
index: 2,
|
||||
type: this.stageConfig.stage2Type,
|
||||
name: 'Zwischenrunde',
|
||||
numberOfGroups: this.stageConfig.stage2Type === 'groups'
|
||||
? Math.max(1, Number(this.stageConfig.stage2GroupCount || 1))
|
||||
: null,
|
||||
});
|
||||
|
||||
advancements.push({
|
||||
fromStageIndex: 1,
|
||||
toStageIndex: 2,
|
||||
mode: 'pools',
|
||||
config: { pools: pools12 },
|
||||
});
|
||||
|
||||
stages.push({
|
||||
index: 3,
|
||||
type: this.stageConfig.finalStageType,
|
||||
name: 'Endrunde',
|
||||
numberOfGroups: this.stageConfig.finalStageType === 'groups'
|
||||
? Math.max(1, Number(this.stageConfig.finalStageGroupCount || 1))
|
||||
: null,
|
||||
});
|
||||
|
||||
advancements.push({
|
||||
fromStageIndex: 2,
|
||||
toStageIndex: 3,
|
||||
mode: 'pools',
|
||||
config: { pools: poolsFinal },
|
||||
});
|
||||
} else {
|
||||
stages.push({
|
||||
index: 3,
|
||||
type: this.stageConfig.finalStageType,
|
||||
name: 'Endrunde',
|
||||
numberOfGroups: this.stageConfig.finalStageType === 'groups'
|
||||
? Math.max(1, Number(this.stageConfig.finalStageGroupCount || 1))
|
||||
: null,
|
||||
});
|
||||
|
||||
advancements.push({
|
||||
fromStageIndex: 1,
|
||||
toStageIndex: 3,
|
||||
mode: 'pools',
|
||||
config: { pools: poolsFinal },
|
||||
});
|
||||
}
|
||||
|
||||
return { stages, advancements };
|
||||
},
|
||||
async saveStageConfig() {
|
||||
this.stageConfig.error = null;
|
||||
this.stageConfig.success = null;
|
||||
try {
|
||||
const { stages, advancements } = this.buildPayload();
|
||||
|
||||
// Validierung: Für jeden Übergang müssen Pools vorhanden sein
|
||||
for (const adv of advancements) {
|
||||
const hasPools = Array.isArray(adv?.config?.pools) && adv.config.pools.length > 0;
|
||||
if (!hasPools) {
|
||||
const label = `${adv.fromStageIndex}→${adv.toStageIndex}`;
|
||||
throw new Error(`Bitte mindestens eine Pool-Regel für ${label} anlegen (z.B. Plätze 1,2).`);
|
||||
}
|
||||
}
|
||||
|
||||
const res = await apiClient.put('/tournament/stages', {
|
||||
clubId: Number(this.clubId),
|
||||
tournamentId: Number(this.tournamentId),
|
||||
stages,
|
||||
advancements,
|
||||
});
|
||||
if (res.status >= 400) throw new Error(res.data?.error || 'Fehler beim Speichern');
|
||||
|
||||
await this.loadStageConfig();
|
||||
this.stageConfig.success = 'Gespeichert.';
|
||||
} catch (e) {
|
||||
this.stageConfig.error = e?.message || String(e);
|
||||
}
|
||||
},
|
||||
async advanceStage(fromStageIndex, toStageIndex) {
|
||||
this.stageConfig.error = null;
|
||||
this.stageConfig.success = null;
|
||||
try {
|
||||
const res = await apiClient.post('/tournament/stages/advance', {
|
||||
clubId: Number(this.clubId),
|
||||
tournamentId: Number(this.tournamentId),
|
||||
fromStageIndex: Number(fromStageIndex),
|
||||
toStageIndex: Number(toStageIndex),
|
||||
});
|
||||
if (res.status >= 400) throw new Error(res.data?.error || 'Fehler beim Erstellen der Runde');
|
||||
this.stageConfig.success = `Runde ${toStageIndex} wurde erstellt.`;
|
||||
} catch (e) {
|
||||
this.stageConfig.error = e?.message || String(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
<button @click="$emit('randomize-groups')">{{ $t('tournaments.randomizeGroups') }}</button>
|
||||
<button @click="$emit('reset-groups')">{{ $t('tournaments.resetGroups') }}</button>
|
||||
</section>
|
||||
<section v-if="groups.length" class="groups-overview">
|
||||
<section v-if="groups.length" class="groups-overview">
|
||||
<h3>{{ $t('tournaments.groupsOverview') }}</h3>
|
||||
<template v-for="(classGroups, classId) in groupsByClass" :key="classId">
|
||||
<template v-if="shouldShowClass(classId === 'null' ? null : parseInt(classId))">
|
||||
@@ -116,7 +116,12 @@
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<div class="reset-controls" style="margin-top:1rem">
|
||||
<div v-if="!matches.some(m => m.round === 'group')" 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">
|
||||
<button @click="$emit('reset-matches')" class="trash-btn">
|
||||
🗑️ {{ $t('tournaments.resetGroupMatches') }}
|
||||
</button>
|
||||
@@ -199,6 +204,7 @@ export default {
|
||||
'randomize-groups',
|
||||
'reset-groups',
|
||||
'reset-matches',
|
||||
'create-matches',
|
||||
'highlight-match'
|
||||
],
|
||||
methods: {
|
||||
|
||||
@@ -356,6 +356,7 @@ export default {
|
||||
};
|
||||
},
|
||||
getPlayerName(p) {
|
||||
if (!p) return 'TBD';
|
||||
if (p.member) {
|
||||
return p.member.firstName + ' ' + p.member.lastName;
|
||||
} else {
|
||||
|
||||
@@ -71,6 +71,8 @@
|
||||
<!-- Tab: Konfiguration -->
|
||||
<TournamentConfigTab
|
||||
v-if="activeTab === 'config'"
|
||||
:club-id="currentClub"
|
||||
:tournament-id="selectedDate"
|
||||
:tournament-name="currentTournamentName"
|
||||
:tournament-date="currentTournamentDate"
|
||||
:winning-sets="currentWinningSets"
|
||||
@@ -173,6 +175,7 @@
|
||||
@randomize-groups="randomizeGroups()"
|
||||
@reset-groups="resetGroups()"
|
||||
@reset-matches="resetMatches()"
|
||||
@create-matches="startMatches()"
|
||||
@highlight-match="highlightMatch"
|
||||
/>
|
||||
|
||||
@@ -371,6 +374,7 @@ export default {
|
||||
if (roundName.includes('Achtelfinale')) return 0;
|
||||
if (roundName.includes('Viertelfinale')) return 1;
|
||||
if (roundName.includes('Halbfinale')) return 2;
|
||||
if (roundName.includes('Spiel um Platz 3')) return 2.5;
|
||||
if (roundName.includes('Finale')) return 3;
|
||||
// Für Runden wie "6-Runde", "8-Runde" etc. - extrahiere die Zahl
|
||||
const numberMatch = roundName.match(/(\d+)-Runde/);
|
||||
|
||||
Reference in New Issue
Block a user