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;