Erweitert die Benutzeroberfläche in OfficialTournaments.vue um einen neuen Tab für Teilnehmer, einschließlich Filteroptionen zur Anzeige von Anmeldestatus und Teilnahme. Implementiert die Logik zur Gruppierung und Anzeige der Teilnehmerdaten in einer Tabelle.

This commit is contained in:
Torsten Schulz (local)
2025-09-12 13:58:04 +02:00
parent ace15ae1d3
commit cf04e5bfe8

View File

@@ -55,6 +55,7 @@
</div>
<div class="tabs">
<button :class="['tab', activeTab==='competitions' ? 'active' : '']" @click="activeTab='competitions'" title="Konkurrenzen anzeigen">Konkurrenzen</button>
<button :class="['tab', activeTab==='participants' ? 'active' : '']" @click="activeTab='participants'" title="Teilnehmer anzeigen">Teilnehmer</button>
<button :class="['tab', activeTab==='results' ? 'active' : '']" @click="activeTab='results'" title="Ergebnisse anzeigen">Ergebnisse</button>
</div>
<div v-if="activeTab==='competitions'">
@@ -64,7 +65,7 @@
<tr>
<th></th>
<th>Altersklasse/Wettbewerb</th>
<th>Startzeit</th>
<th>Startzeit</th>
<th>Startgeld</th>
</tr>
</thead>
@@ -144,6 +145,46 @@
</tbody>
</table>
</div>
<div v-else-if="activeTab==='participants'">
<h3>Teilnehmer</h3>
<div class="filters">
<label for="participantsFilter">Status:</label>
<select id="participantsFilter" v-model="participantsFilter">
<option value="wants_not_registered">Möchte teilnehmen, nicht angemeldet</option>
<option value="registered">Angemeldet</option>
<option value="participated">Hat gespielt</option>
</select>
</div>
<table>
<thead>
<tr>
<th>Mitglied</th>
<th>Konkurrenz</th>
<th>Startzeit</th>
<th>Angemeldet</th>
<th>Teilgenommen</th>
<th>Platzierung</th>
</tr>
</thead>
<tbody>
<template v-for="group in participantsGroups" :key="group.memberId">
<template v-for="(item, idx) in group.items" :key="item.key">
<tr>
<td v-if="idx === 0" :rowspan="group.items.length" class="member-cell">{{ group.memberName }}</td>
<td class="indented">{{ item.competitionName }}</td>
<td>{{ item.start }}</td>
<td>{{ item.registered ? 'Ja' : 'Nein' }}</td>
<td>{{ item.participated ? 'Ja' : 'Nein' }}</td>
<td>{{ item.placement || '' }}</td>
</tr>
</template>
</template>
<tr v-if="!participantsGroups.length">
<td colspan="6"><em>Keine Einträge für den gewählten Filter.</em></td>
</tr>
</tbody>
</table>
</div>
<div v-else>
<h3>Ergebnisse</h3>
<table>
@@ -269,6 +310,7 @@ export default {
participationMap: {}, // key: `${competitionId}-${memberId}` => { wants, registered, participated, placement }
collator: new Intl.Collator('de', { sensitivity: 'base' }),
activeTab: 'competitions',
participantsFilter: 'wants_not_registered',
topActiveTab: 'events',
loadingClubParticipations: false,
clubParticipationRowsData: [],
@@ -320,6 +362,79 @@ export default {
if (m !== 0) return m;
return this.collator.compare(a.competitionName, b.competitionName);
});
},
participantsRows() {
const comps = (this.parsed?.parsedData?.competitions) || [];
const compById = Object.fromEntries(comps.map(c => [String(c.id), c]));
const rows = [];
// Merge Quelle: parsed.participation + aktueller UI-Status aus participationMap
const seen = new Set();
const merged = [];
if (Array.isArray(this.parsed?.participation)) {
for (const e of this.parsed.participation) {
const competitionId = String(e.competitionId);
const memberId = String(e.memberId);
const key = `${competitionId}-${memberId}`;
seen.add(key);
merged.push({ competitionId, memberId });
}
}
for (const [key, p] of Object.entries(this.participationMap || {})) {
if (seen.has(key)) continue;
const [competitionId, memberId] = key.split('-');
merged.push({ competitionId: String(competitionId), memberId: String(memberId) });
}
for (const e of merged) {
const competitionId = String(e.competitionId);
const memberId = String(e.memberId);
const c = compById[competitionId];
if (!c) continue;
// Hole aktuellen Status (inkl. UI-Änderungen) aus participationMap
const current = this.getParticipation(competitionId, memberId);
const mname = this.memberNameById(memberId);
const start = String(c.startTime || c.startzeit || '');
const base = {
key: `${competitionId}-${memberId}`,
memberName: mname,
competitionName: c.ageClassCompetition || c.altersklasseWettbewerb || '',
start,
registered: !!current.registered,
participated: !!current.participated,
placement: current.placement || null,
wants: !!current.wants,
};
if (this.participantsFilter === 'wants_not_registered') {
if (base.wants && !base.registered && !base.participated) rows.push(base);
} else if (this.participantsFilter === 'registered') {
if (base.registered && !base.participated) rows.push(base);
} else if (this.participantsFilter === 'participated') {
if (base.participated) rows.push(base);
}
}
return rows.sort((a, b) => {
const m = this.collator.compare(a.memberName, b.memberName);
if (m !== 0) return m;
return this.collator.compare(a.competitionName, b.competitionName);
});
},
participantsGroups() {
const groups = [];
const byMember = new Map();
for (const row of this.participantsRows) {
const key = row.memberName;
if (!byMember.has(key)) byMember.set(key, []);
byMember.get(key).push(row);
}
for (const [memberName, items] of byMember.entries()) {
items.sort((a, b) => this.collator.compare(a.competitionName, b.competitionName));
groups.push({
memberName,
memberId: items[0]?.key.split('-')[1] || memberName,
items,
});
}
groups.sort((a, b) => this.collator.compare(a.memberName, b.memberName));
return groups;
}
},
methods: {
@@ -780,6 +895,9 @@ th, td { border-bottom: 1px solid var(--border-color); padding: 0.5rem; text-ali
.eligible-name { background: var(--background, #f1f1f1); border: 1px solid var(--border-color, #ddd); border-radius: 4px; padding: 2px 6px; }
.eligible-table { width: 100%; border-collapse: collapse; margin-top: .25rem; }
.eligible-table th, .eligible-table td { border-bottom: 1px solid var(--border-color); padding: .25rem .4rem; text-align: left; }
.indented { padding-left: 1.25rem; }
.member-cell { font-weight: 600; vertical-align: top; }
.empty-first { border-bottom: 1px solid var(--border-color); }
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.35); display: flex; align-items: center; justify-content: center; z-index: 1000; }
.modal { background: #fff; border-radius: 8px; width: min(800px, 92vw); max-height: 85vh; display: flex; flex-direction: column; box-shadow: 0 10px 30px rgba(0,0,0,.2); }
.modal-header { padding: .75rem 1rem; border-bottom: 1px solid var(--border-color); }