diff --git a/frontend/src/components/PDFGenerator.js b/frontend/src/components/PDFGenerator.js
index c9014c1..9345109 100644
--- a/frontend/src/components/PDFGenerator.js
+++ b/frontend/src/components/PDFGenerator.js
@@ -264,6 +264,77 @@ class PDFGenerator {
});
}
+ addParticipantsSummary(tournamentTitle, tournamentDateText, groups) {
+ // Header
+ const title = tournamentTitle || 'Offizielles Turnier';
+ this.pdf.setFont('helvetica', 'bold');
+ this.pdf.setFontSize(14);
+ this.pdf.text(title, this.margin, this.cursorY);
+ this.cursorY += 8;
+ if (tournamentDateText) {
+ this.pdf.setFont('helvetica', 'normal');
+ this.pdf.setFontSize(12);
+ this.pdf.text(String(tournamentDateText), this.margin, this.cursorY);
+ this.cursorY += 8;
+ }
+
+ // Tabelle mit Gruppierung
+ const head = [['Mitglied', 'Konkurrenz', 'Startzeit', 'Status', 'Platzierung']];
+ const body = [];
+ const rowStyles = [];
+
+ for (const group of groups) {
+ for (let i = 0; i < group.items.length; i++) {
+ const item = group.items[i];
+ const rowData = [
+ i === 0 ? group.memberName : '', // Name nur in erster Zeile
+ item.competitionName,
+ item.start || '–',
+ item.statusText || '',
+ item.placement || ''
+ ];
+ body.push(rowData);
+ rowStyles.push({
+ isFirstRow: i === 0,
+ memberStyle: group.memberStyle,
+ competitionName: item.competitionName,
+ statusStyle: item.statusStyle
+ });
+ }
+ }
+
+ this.pdf.setFontSize(11);
+ autoTable(this.pdf, {
+ startY: this.cursorY,
+ margin: { left: this.margin, right: this.margin },
+ head,
+ body,
+ theme: 'grid',
+ styles: { fontSize: 11 },
+ headStyles: { fillColor: [220, 220, 220], textColor: 0, halign: 'left' },
+ didParseCell: (data) => {
+ if (data.section !== 'body') return;
+ const rowStyle = rowStyles[data.row.index];
+
+ // Formatierung für Mitgliedsname (erste Spalte, erste Zeile der Gruppe)
+ if (data.column.index === 0 && rowStyle.isFirstRow) {
+ if (rowStyle.memberStyle === 'bold') data.cell.styles.fontStyle = 'bold';
+ else if (rowStyle.memberStyle === 'italic') data.cell.styles.fontStyle = 'italic';
+ else data.cell.styles.fontStyle = 'normal';
+ }
+ // Formatierung für Konkurrenzname (zweite Spalte)
+ else if (data.column.index === 1) {
+ if (rowStyle.statusStyle === 'bold') data.cell.styles.fontStyle = 'bold';
+ else if (rowStyle.statusStyle === 'italic') data.cell.styles.fontStyle = 'italic';
+ else data.cell.styles.fontStyle = 'normal';
+ }
+ },
+ didDrawPage: (data) => {
+ this.cursorY = data.cursor.y + 10;
+ }
+ });
+ }
+
addMemberCompetitions(tournamentTitle, memberName, recommendedRows = [], otherRows = [], venues = []) {
let y = this.margin;
this.pdf.setFont('helvetica', 'bold');
diff --git a/frontend/src/views/OfficialTournaments.vue b/frontend/src/views/OfficialTournaments.vue
index 5978564..80a70e1 100644
--- a/frontend/src/views/OfficialTournaments.vue
+++ b/frontend/src/views/OfficialTournaments.vue
@@ -154,6 +154,8 @@
+
+
@@ -448,6 +450,119 @@ export default {
if (byFirst !== 0) return byFirst;
return this.collator.compare(lnA, lnB);
},
+ generateParticipantsPdf() {
+ if (!this.parsed) return;
+ const title = this.parsed?.parsedData?.title || 'Offizielles Turnier';
+ const dateText = this.parsed?.parsedData?.termin || '';
+ // Alle Teilnehmer unabhängig vom Filter
+ const comps = (this.parsed?.parsedData?.competitions) || [];
+ const compById = Object.fromEntries(comps.map(c => [String(c.id), c]));
+ const allRows = [];
+ 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;
+ const current = this.getParticipation(competitionId, memberId);
+ const mname = this.memberNameById(memberId);
+ const start = String(c.startTime || c.startzeit || '–');
+ let statusStyle = 'normal';
+ let statusText = '';
+ if (current.participated) {
+ statusStyle = 'normal';
+ statusText = 'gespielt';
+ } else if (current.registered) {
+ statusStyle = 'italic';
+ statusText = 'angemeldet';
+ } else if (current.wants) {
+ statusStyle = 'bold';
+ statusText = 'möchte teilnehmen (Anmeldung fehlt)';
+ }
+ allRows.push({
+ memberName: mname,
+ competitionName: c.ageClassCompetition || c.altersklasseWettbewerb || '',
+ start,
+ placement: current.placement || '',
+ statusStyle,
+ statusText,
+ wants: current.wants,
+ registered: current.registered,
+ participated: current.participated,
+ });
+ }
+ // Nach Mitglied und Konkurrenz sortieren
+ allRows.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);
+ });
+
+ // Gruppierung nach Mitglied mit korrekter Formatierungslogik
+ const groups = [];
+ const byMember = new Map();
+ for (const row of allRows) {
+ 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));
+
+ // Bestimme Formatierung für den Spielernamen basierend auf allen Konkurrenzen
+ let memberStyle = 'normal';
+ let memberStatusText = '';
+
+ const hasPlayed = items.some(item => item.participated);
+ const wantsToPlay = items.some(item => item.wants);
+ const allRegistered = items.every(item => !item.wants || item.registered);
+ const hasUnregistered = items.some(item => item.wants && !item.registered);
+
+ if (hasPlayed) {
+ memberStyle = 'normal';
+ memberStatusText = 'hat gespielt';
+ } else if (wantsToPlay && hasUnregistered) {
+ memberStyle = 'bold';
+ memberStatusText = 'Anmeldung fehlt';
+ } else if (wantsToPlay && allRegistered) {
+ memberStyle = 'italic';
+ memberStatusText = 'angemeldet';
+ } else {
+ // Fallback für Spieler ohne Teilnahmewünsche
+ memberStyle = 'normal';
+ memberStatusText = '';
+ }
+
+ groups.push({
+ memberName,
+ memberId: items[0]?.memberName || memberName,
+ memberStyle,
+ memberStatusText,
+ items,
+ });
+ }
+ groups.sort((a, b) => this.collator.compare(a.memberName, b.memberName));
+
+ const pdf = new PDFGenerator();
+ pdf.addParticipantsSummary(title, dateText, groups);
+ pdf.save('teilnehmer.pdf');
+ },
clubParticipationRows() {
if (this.clubParticipationRowsData && this.clubParticipationRowsData.length) {
return this.clubParticipationRowsData;