diff --git a/frontend/src/components/tournament/TournamentPlacementsTab.vue b/frontend/src/components/tournament/TournamentPlacementsTab.vue index d4a734d8..7dbbef12 100644 --- a/frontend/src/components/tournament/TournamentPlacementsTab.vue +++ b/frontend/src/components/tournament/TournamentPlacementsTab.vue @@ -89,6 +89,12 @@

{{ $t('tournaments.noPlacementsYet') }}

+ +
+ +
import TournamentClassSelector from './TournamentClassSelector.vue'; import PlayerDetailsDialog from './PlayerDetailsDialog.vue'; +import jsPDF from 'jspdf'; +import autoTable from 'jspdf-autotable'; +import apiClient from '../../apiClient.js'; export default { name: 'TournamentPlacementsTab', @@ -122,15 +131,17 @@ export default { groups: { type: Array, required: true }, groupRankings: { type: Object, required: true }, knockoutMatches: { type: Array, required: true }, - clubId: { type: [Number, String], required: true } + clubId: { type: [Number, String], required: true }, + isMiniChampionship: { type: Boolean, default: false } }, - emits: ['update:selectedViewClass'], + emits: ['update:selectedViewClass', 'show-info'], data() { return { showPlayerDialog: false, selectedPlayerId: null, selectedPlayerIsExternal: false, - selectedPlayerName: '' + selectedPlayerName: '', + pdfLoading: false }; }, watch: { @@ -527,6 +538,199 @@ export default { const c = (this.tournamentClasses || []).find(x => x.id === cid); return Boolean(c && c.isDoubles); }, + async generateMissingDataPDF() { + this.pdfLoading = true; + try { + // 1. Alle Teilnehmer-Daten laden + const allPlayerData = []; + + // Interne Mitglieder laden + let members = []; + try { + const res = await apiClient.get(`/clubmembers/get/${Number(this.clubId)}/true`); + members = res.data || []; + } catch (e) { + console.error('Fehler beim Laden der Mitglieder:', e); + } + + // Externe Teilnehmer laden + let externals = []; + try { + const res = await apiClient.post('/tournament/external-participants', { + clubId: Number(this.clubId), + tournamentId: this.selectedDate, + classId: null + }); + externals = res.data || []; + } catch (e) { + console.error('Fehler beim Laden der externen Teilnehmer:', e); + } + + // Interne Teilnehmer verarbeiten + for (const p of this.participants) { + const memberId = p.member?.id || p.clubMemberId; + const member = members.find(m => m.id === memberId); + if (!member) continue; + + let address = ''; + const parts = []; + if (member.street) parts.push(member.street); + if (member.postalCode) parts.push(member.postalCode); + if (member.city) parts.push(member.city); + address = parts.join(', '); + + let email = ''; + if (member.contacts && Array.isArray(member.contacts)) { + email = member.contacts.filter(c => c.type === 'email').map(c => c.value).join(', '); + } else if (member.email) { + email = member.email; + } + + const name = `${member.firstName || ''} ${member.lastName || ''}`.trim(); + const gender = member.gender && member.gender !== 'unknown' ? member.gender : null; + + allPlayerData.push({ + name, + birthDate: member.birthDate || null, + gender, + address: address || null, + email: email || null + }); + } + + // Externe Teilnehmer verarbeiten + for (const ext of this.externalParticipants) { + const found = externals.find(e => e.id === ext.id); + const src = found || ext; + const name = `${src.firstName || ''} ${src.lastName || ''}`.trim(); + const gender = src.gender && src.gender !== 'unknown' ? src.gender : null; + + allPlayerData.push({ + name, + birthDate: src.birthDate || null, + gender, + address: src.address || null, + email: src.email || null + }); + } + + // 2. Nur Teilnehmer mit fehlenden Daten filtern + const fields = ['birthDate', 'gender', 'address', 'email']; + const playersWithMissing = allPlayerData.filter(p => + fields.some(f => !p[f]) + ); + + if (playersWithMissing.length === 0) { + this.$emit('show-info', this.$t('messages.info'), this.$t('tournaments.allDataComplete')); + return; + } + + // Sortieren nach Name + playersWithMissing.sort((a, b) => a.name.localeCompare(b.name)); + + // 3. PDF erzeugen + const pdf = new jsPDF('p', 'mm', 'a4'); + const margin = 15; + const t = this.$t; + + // Titel + pdf.setFontSize(16); + pdf.setFont('helvetica', 'bold'); + pdf.text(t('tournaments.missingDataPDFTitle'), margin, 20); + + pdf.setFontSize(10); + pdf.setFont('helvetica', 'normal'); + pdf.text(t('tournaments.missingDataPDFSubtitle'), margin, 27); + + // Formatierung der Felder + const formatGender = (g) => { + if (!g) return ''; + const map = { male: t('members.genderMale'), female: t('members.genderFemale'), diverse: t('members.genderDiverse') }; + return map[g] || g; + }; + const formatDate = (d) => { + if (!d) return ''; + try { + const date = new Date(d); + if (isNaN(date.getTime())) return d; + return `${String(date.getDate()).padStart(2, '0')}.${String(date.getMonth() + 1).padStart(2, '0')}.${date.getFullYear()}`; + } catch (e) { return d; } + }; + + const missing = t('tournaments.dataNotRecorded'); + const linePlaceholder = '____________________'; + + // Tabelle + const head = [[ + t('members.firstName') + ' / ' + t('members.lastName'), + t('members.birthdate'), + t('members.gender'), + t('tournaments.address'), + t('members.emailAddress') + ]]; + + const body = playersWithMissing.map(p => [ + p.name, + p.birthDate ? formatDate(p.birthDate) : linePlaceholder, + p.gender ? formatGender(p.gender) : linePlaceholder, + p.address || linePlaceholder, + p.email || linePlaceholder + ]); + + autoTable(pdf, { + startY: 32, + margin: { left: margin, right: margin }, + head, + body, + styles: { + fontSize: 9, + cellPadding: 3, + lineColor: [200, 200, 200], + lineWidth: 0.25, + valign: 'middle' + }, + headStyles: { + fillColor: [76, 175, 80], + textColor: 255, + fontStyle: 'bold', + fontSize: 9 + }, + columnStyles: { + 0: { cellWidth: 35 }, + 1: { cellWidth: 28 }, + 2: { cellWidth: 25 }, + 3: { cellWidth: 45 }, + 4: { cellWidth: 45 } + }, + didParseCell: (data) => { + // Platzhalter-Zeilen grau und kursiv markieren + if (data.section === 'body' && data.cell.raw === linePlaceholder) { + data.cell.styles.textColor = [180, 180, 180]; + data.cell.styles.fontStyle = 'italic'; + } + } + }); + + // Fußzeile + const pageCount = pdf.internal.getNumberOfPages(); + for (let i = 1; i <= pageCount; i++) { + pdf.setPage(i); + pdf.setFontSize(8); + pdf.setTextColor(150); + pdf.text( + `${t('tournaments.missingDataPDFTitle')} – ${new Date().toLocaleDateString('de-DE')} – Seite ${i}/${pageCount}`, + margin, + 290 + ); + } + + pdf.save(`Fehlende_Daten_Minimeisterschaft_${new Date().toISOString().slice(0, 10)}.pdf`); + } catch (error) { + console.error('Fehler beim Erstellen des PDFs:', error); + } finally { + this.pdfLoading = false; + } + }, openPlayerDialog(entry) { console.log('[openPlayerDialog] entry:', entry); // Für Doppel-Paarungen können wir keine Details anzeigen diff --git a/frontend/src/i18n/locales/de-CH.json b/frontend/src/i18n/locales/de-CH.json index 49e7b879..41338628 100644 --- a/frontend/src/i18n/locales/de-CH.json +++ b/frontend/src/i18n/locales/de-CH.json @@ -107,6 +107,11 @@ "noFreeTables": "Kei freie Tisch verfüegbar.", "noAssignableMatches": "Kei Spiel verfüegbar, wo beidi Spieler frei sind.", "tablesDistributed": "Tisch sind verteilt worde.", + "missingDataPDF": "Fehlendi Date als PDF", + "missingDataPDFTitle": "Fehlendi Teilnehmerdaten – Minimeisterschaft", + "missingDataPDFSubtitle": "Bitte d'fehlende Date (markiert mit ____) bi de Teilnehmer nochfroge und do notiere.", + "allDataComplete": "Alli Teilnehmerdaten sind vollständig.", + "address": "Adrässe", "dataNotRecorded": "No nid erfasst" } } diff --git a/frontend/src/i18n/locales/de-extended.json b/frontend/src/i18n/locales/de-extended.json index 7d9a8721..7cb1169e 100644 --- a/frontend/src/i18n/locales/de-extended.json +++ b/frontend/src/i18n/locales/de-extended.json @@ -275,6 +275,11 @@ "noFreeTables": "Keine freien Tische verfügbar.", "noAssignableMatches": "Keine Spiele verfügbar, bei denen beide Spieler frei sind.", "tablesDistributed": "Tische wurden verteilt.", + "missingDataPDF": "Fehlende Daten als PDF", + "missingDataPDFTitle": "Fehlende Teilnehmerdaten – Minimeisterschaft", + "missingDataPDFSubtitle": "Bitte die fehlenden Daten (markiert mit ____) bei den Teilnehmern erfragen und hier notieren.", + "allDataComplete": "Alle Teilnehmerdaten sind vollständig.", + "address": "Adresse", "dataNotRecorded": "Noch nicht erfasst" }, "permissions": { diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index d0cd49df..ac1e6b7e 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -581,6 +581,11 @@ "noFreeTables": "Keine freien Tische verfügbar.", "noAssignableMatches": "Keine Spiele verfügbar, bei denen beide Spieler frei sind.", "tablesDistributed": "Tische wurden verteilt.", + "missingDataPDF": "Fehlende Daten als PDF", + "missingDataPDFTitle": "Fehlende Teilnehmerdaten – Minimeisterschaft", + "missingDataPDFSubtitle": "Bitte die fehlenden Daten (markiert mit ____) bei den Teilnehmern erfragen und hier notieren.", + "allDataComplete": "Alle Teilnehmerdaten sind vollständig.", + "address": "Adresse", "create": "Erstellen", "exportPDF": "PDF exportieren", "playInGroups": "Spielen in Gruppen", diff --git a/frontend/src/i18n/locales/en-AU.json b/frontend/src/i18n/locales/en-AU.json index e9ea4f71..c786c2b5 100644 --- a/frontend/src/i18n/locales/en-AU.json +++ b/frontend/src/i18n/locales/en-AU.json @@ -107,6 +107,11 @@ "noFreeTables": "No free tables available.", "noAssignableMatches": "No matches available where both players are free.", "tablesDistributed": "Tables have been distributed.", + "missingDataPDF": "Missing data as PDF", + "missingDataPDFTitle": "Missing participant data – Mini championship", + "missingDataPDFSubtitle": "Please collect the missing data (marked with ____) from participants and note them here.", + "allDataComplete": "All participant data is complete.", + "address": "Address", "dataNotRecorded": "Not yet recorded" } } diff --git a/frontend/src/i18n/locales/en-GB.json b/frontend/src/i18n/locales/en-GB.json index 8462eb02..0b0c6ebf 100644 --- a/frontend/src/i18n/locales/en-GB.json +++ b/frontend/src/i18n/locales/en-GB.json @@ -107,6 +107,11 @@ "noFreeTables": "No free tables available.", "noAssignableMatches": "No matches available where both players are free.", "tablesDistributed": "Tables have been distributed.", + "missingDataPDF": "Missing data as PDF", + "missingDataPDFTitle": "Missing participant data – Mini championship", + "missingDataPDFSubtitle": "Please collect the missing data (marked with ____) from participants and note them here.", + "allDataComplete": "All participant data is complete.", + "address": "Address", "dataNotRecorded": "Not yet recorded" } } diff --git a/frontend/src/i18n/locales/en-US.json b/frontend/src/i18n/locales/en-US.json index c016cf26..6137ae66 100644 --- a/frontend/src/i18n/locales/en-US.json +++ b/frontend/src/i18n/locales/en-US.json @@ -107,6 +107,11 @@ "noFreeTables": "No free tables available.", "noAssignableMatches": "No matches available where both players are free.", "tablesDistributed": "Tables have been distributed.", + "missingDataPDF": "Missing data as PDF", + "missingDataPDFTitle": "Missing participant data – Mini championship", + "missingDataPDFSubtitle": "Please collect the missing data (marked with ____) from participants and note them here.", + "allDataComplete": "All participant data is complete.", + "address": "Address", "dataNotRecorded": "Not yet recorded" } } diff --git a/frontend/src/i18n/locales/es.json b/frontend/src/i18n/locales/es.json index 0c785d4b..05fd03c9 100644 --- a/frontend/src/i18n/locales/es.json +++ b/frontend/src/i18n/locales/es.json @@ -107,6 +107,11 @@ "noFreeTables": "No hay mesas libres disponibles.", "noAssignableMatches": "No hay partidos disponibles en los que ambos jugadores estén libres.", "tablesDistributed": "Las mesas han sido distribuidas.", + "missingDataPDF": "Datos faltantes como PDF", + "missingDataPDFTitle": "Datos de participantes faltantes – Mini campeonato", + "missingDataPDFSubtitle": "Por favor, recopile los datos faltantes (marcados con ____) de los participantes y anótelos aquí.", + "allDataComplete": "Todos los datos de los participantes están completos.", + "address": "Dirección", "dataNotRecorded": "Aún no registrado" } } diff --git a/frontend/src/i18n/locales/fil.json b/frontend/src/i18n/locales/fil.json index c7c43444..d0722cb0 100644 --- a/frontend/src/i18n/locales/fil.json +++ b/frontend/src/i18n/locales/fil.json @@ -107,6 +107,11 @@ "noFreeTables": "Walang bakanteng mesa.", "noAssignableMatches": "Walang laban kung saan pareho ang mga manlalaro ay bakante.", "tablesDistributed": "Ang mga mesa ay naipamahagi na.", + "missingDataPDF": "Nawawalang datos bilang PDF", + "missingDataPDFTitle": "Nawawalang datos ng mga kalahok – Mini championship", + "missingDataPDFSubtitle": "Pakikolekta ang nawawalang datos (may markang ____) mula sa mga kalahok at itala dito.", + "allDataComplete": "Kumpleto na ang lahat ng datos ng mga kalahok.", + "address": "Address", "dataNotRecorded": "Hindi pa naitala" } } diff --git a/frontend/src/i18n/locales/fr.json b/frontend/src/i18n/locales/fr.json index 372df45b..1fa5bf84 100644 --- a/frontend/src/i18n/locales/fr.json +++ b/frontend/src/i18n/locales/fr.json @@ -107,6 +107,11 @@ "noFreeTables": "Aucune table libre disponible.", "noAssignableMatches": "Aucun match disponible où les deux joueurs sont libres.", "tablesDistributed": "Les tables ont été distribuées.", + "missingDataPDF": "Données manquantes en PDF", + "missingDataPDFTitle": "Données manquantes des participants – Mini-championnat", + "missingDataPDFSubtitle": "Veuillez collecter les données manquantes (marquées ____) auprès des participants et les noter ici.", + "allDataComplete": "Toutes les données des participants sont complètes.", + "address": "Adresse", "dataNotRecorded": "Pas encore enregistré" } } diff --git a/frontend/src/i18n/locales/it.json b/frontend/src/i18n/locales/it.json index dbacd1ed..68ffab3b 100644 --- a/frontend/src/i18n/locales/it.json +++ b/frontend/src/i18n/locales/it.json @@ -107,6 +107,11 @@ "noFreeTables": "Nessun tavolo libero disponibile.", "noAssignableMatches": "Nessuna partita disponibile in cui entrambi i giocatori sono liberi.", "tablesDistributed": "I tavoli sono stati distribuiti.", + "missingDataPDF": "Dati mancanti come PDF", + "missingDataPDFTitle": "Dati mancanti dei partecipanti – Mini campionato", + "missingDataPDFSubtitle": "Si prega di raccogliere i dati mancanti (contrassegnati con ____) dai partecipanti e annotarli qui.", + "allDataComplete": "Tutti i dati dei partecipanti sono completi.", + "address": "Indirizzo", "dataNotRecorded": "Non ancora registrato" } } diff --git a/frontend/src/i18n/locales/ja.json b/frontend/src/i18n/locales/ja.json index 745e2183..e513b368 100644 --- a/frontend/src/i18n/locales/ja.json +++ b/frontend/src/i18n/locales/ja.json @@ -107,6 +107,11 @@ "noFreeTables": "空き卓がありません。", "noAssignableMatches": "両選手が空いている試合がありません。", "tablesDistributed": "卓が割り当てられました。", + "missingDataPDF": "不足データをPDFで", + "missingDataPDFTitle": "参加者の不足データ – ミニ選手権", + "missingDataPDFSubtitle": "不足しているデータ(____で表示)を参加者から収集し、ここに記入してください。", + "allDataComplete": "すべての参加者データが揃っています。", + "address": "住所", "dataNotRecorded": "未登録" } } diff --git a/frontend/src/i18n/locales/pl.json b/frontend/src/i18n/locales/pl.json index 704194ab..e20ec641 100644 --- a/frontend/src/i18n/locales/pl.json +++ b/frontend/src/i18n/locales/pl.json @@ -107,6 +107,11 @@ "noFreeTables": "Brak wolnych stołów.", "noAssignableMatches": "Brak meczów, w których obaj gracze są wolni.", "tablesDistributed": "Stoły zostały rozdzielone.", + "missingDataPDF": "Brakujące dane jako PDF", + "missingDataPDFTitle": "Brakujące dane uczestników – Mini mistrzostwa", + "missingDataPDFSubtitle": "Proszę zebrać brakujące dane (oznaczone ____) od uczestników i zanotować je tutaj.", + "allDataComplete": "Wszystkie dane uczestników są kompletne.", + "address": "Adres", "dataNotRecorded": "Jeszcze nie wprowadzono" } } diff --git a/frontend/src/i18n/locales/th.json b/frontend/src/i18n/locales/th.json index 26d4cc31..71bbaa72 100644 --- a/frontend/src/i18n/locales/th.json +++ b/frontend/src/i18n/locales/th.json @@ -107,6 +107,11 @@ "noFreeTables": "ไม่มีโต๊ะว่าง", "noAssignableMatches": "ไม่มีแมตช์ที่ผู้เล่นทั้งสองว่าง", "tablesDistributed": "จัดสรรโต๊ะเรียบร้อยแล้ว", + "missingDataPDF": "ข้อมูลที่ขาดหายเป็น PDF", + "missingDataPDFTitle": "ข้อมูลผู้เข้าร่วมที่ขาดหาย – มินิแชมเปี้ยนชิพ", + "missingDataPDFSubtitle": "กรุณาเก็บข้อมูลที่ขาดหาย (ทำเครื่องหมาย ____) จากผู้เข้าร่วมและจดบันทึกที่นี่", + "allDataComplete": "ข้อมูลผู้เข้าร่วมทั้งหมดครบถ้วน", + "address": "ที่อยู่", "dataNotRecorded": "ยังไม่ได้บันทึก" } } diff --git a/frontend/src/i18n/locales/tl.json b/frontend/src/i18n/locales/tl.json index c7c43444..d0722cb0 100644 --- a/frontend/src/i18n/locales/tl.json +++ b/frontend/src/i18n/locales/tl.json @@ -107,6 +107,11 @@ "noFreeTables": "Walang bakanteng mesa.", "noAssignableMatches": "Walang laban kung saan pareho ang mga manlalaro ay bakante.", "tablesDistributed": "Ang mga mesa ay naipamahagi na.", + "missingDataPDF": "Nawawalang datos bilang PDF", + "missingDataPDFTitle": "Nawawalang datos ng mga kalahok – Mini championship", + "missingDataPDFSubtitle": "Pakikolekta ang nawawalang datos (may markang ____) mula sa mga kalahok at itala dito.", + "allDataComplete": "Kumpleto na ang lahat ng datos ng mga kalahok.", + "address": "Address", "dataNotRecorded": "Hindi pa naitala" } } diff --git a/frontend/src/i18n/locales/zh.json b/frontend/src/i18n/locales/zh.json index b322578e..60fd22b9 100644 --- a/frontend/src/i18n/locales/zh.json +++ b/frontend/src/i18n/locales/zh.json @@ -107,6 +107,11 @@ "noFreeTables": "没有可用的空闲球台。", "noAssignableMatches": "没有两位选手都空闲的比赛。", "tablesDistributed": "球台已分配。", + "missingDataPDF": "缺失数据导出为PDF", + "missingDataPDFTitle": "参赛者缺失数据 – 迷你锦标赛", + "missingDataPDFSubtitle": "请向参赛者收集缺失数据(标记为____)并在此记录。", + "allDataComplete": "所有参赛者数据已完整。", + "address": "地址", "dataNotRecorded": "尚未录入" } } diff --git a/frontend/src/views/TournamentTab.vue b/frontend/src/views/TournamentTab.vue index 5b1711bd..3d1a84fd 100644 --- a/frontend/src/views/TournamentTab.vue +++ b/frontend/src/views/TournamentTab.vue @@ -257,7 +257,9 @@ :external-participants="externalParticipants" :pairings="pairings" :club-id="currentClub" + :is-mini-championship="isMiniChampionship" @update:selectedViewClass="selectedViewClass = $event" + @show-info="showInfo" />