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"
/>