feat(tournament): add PDF generation for missing participant data
- Implemented a new feature to generate a PDF report for participants with missing data in mini championships. - Added a button in the TournamentPlacementsTab component to trigger the PDF generation, which is disabled while loading. - Enhanced internationalization by adding translation keys for the new PDF feature across multiple languages. - Updated the TournamentTab component to pass the `isMiniChampionship` prop and handle the new `show-info` event.
This commit is contained in:
@@ -89,6 +89,12 @@
|
||||
<div v-if="Object.keys(finalPlacementsByClassFiltered).length === 0 && groupPlacements.length === 0" class="no-placements">
|
||||
<p>{{ $t('tournaments.noPlacementsYet') }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="isMiniChampionship && (participants.length > 0 || externalParticipants.length > 0)" class="missing-data-pdf" style="margin-top: 1.5rem;">
|
||||
<button @click="generateMissingDataPDF" class="btn-primary" :disabled="pdfLoading">
|
||||
{{ pdfLoading ? $t('messages.loading') + '...' : $t('tournaments.missingDataPDF') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Player Details Dialog -->
|
||||
<PlayerDetailsDialog
|
||||
@@ -105,6 +111,9 @@
|
||||
<script>
|
||||
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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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é"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +107,11 @@
|
||||
"noFreeTables": "空き卓がありません。",
|
||||
"noAssignableMatches": "両選手が空いている試合がありません。",
|
||||
"tablesDistributed": "卓が割り当てられました。",
|
||||
"missingDataPDF": "不足データをPDFで",
|
||||
"missingDataPDFTitle": "参加者の不足データ – ミニ選手権",
|
||||
"missingDataPDFSubtitle": "不足しているデータ(____で表示)を参加者から収集し、ここに記入してください。",
|
||||
"allDataComplete": "すべての参加者データが揃っています。",
|
||||
"address": "住所",
|
||||
"dataNotRecorded": "未登録"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +107,11 @@
|
||||
"noFreeTables": "ไม่มีโต๊ะว่าง",
|
||||
"noAssignableMatches": "ไม่มีแมตช์ที่ผู้เล่นทั้งสองว่าง",
|
||||
"tablesDistributed": "จัดสรรโต๊ะเรียบร้อยแล้ว",
|
||||
"missingDataPDF": "ข้อมูลที่ขาดหายเป็น PDF",
|
||||
"missingDataPDFTitle": "ข้อมูลผู้เข้าร่วมที่ขาดหาย – มินิแชมเปี้ยนชิพ",
|
||||
"missingDataPDFSubtitle": "กรุณาเก็บข้อมูลที่ขาดหาย (ทำเครื่องหมาย ____) จากผู้เข้าร่วมและจดบันทึกที่นี่",
|
||||
"allDataComplete": "ข้อมูลผู้เข้าร่วมทั้งหมดครบถ้วน",
|
||||
"address": "ที่อยู่",
|
||||
"dataNotRecorded": "ยังไม่ได้บันทึก"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +107,11 @@
|
||||
"noFreeTables": "没有可用的空闲球台。",
|
||||
"noAssignableMatches": "没有两位选手都空闲的比赛。",
|
||||
"tablesDistributed": "球台已分配。",
|
||||
"missingDataPDF": "缺失数据导出为PDF",
|
||||
"missingDataPDFTitle": "参赛者缺失数据 – 迷你锦标赛",
|
||||
"missingDataPDFSubtitle": "请向参赛者收集缺失数据(标记为____)并在此记录。",
|
||||
"allDataComplete": "所有参赛者数据已完整。",
|
||||
"address": "地址",
|
||||
"dataNotRecorded": "尚未录入"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,7 +257,9 @@
|
||||
:external-participants="externalParticipants"
|
||||
:pairings="pairings"
|
||||
:club-id="currentClub"
|
||||
:is-mini-championship="isMiniChampionship"
|
||||
@update:selectedViewClass="selectedViewClass = $event"
|
||||
@show-info="showInfo"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user