Erweitert die PDF-Generierung in PDFGenerator.js, um empfohlene und andere Wettbewerbe für Mitglieder zu unterscheiden. Fügt eine neue Struktur für die Anzeige von Empfehlungen und Hinweisen hinzu. Aktualisiert OfficialTournaments.vue, um die Auswahl von Mitgliedern und deren Wettbewerben zu verbessern, einschließlich einer neuen Dialogstruktur und der Verwaltung von Empfehlungen.

This commit is contained in:
Torsten Schulz (local)
2025-08-31 15:55:49 +02:00
parent f49e1896b9
commit e3b8488d2b
2 changed files with 156 additions and 23 deletions

View File

@@ -264,30 +264,116 @@ class PDFGenerator {
});
}
addMemberCompetitions(tournamentTitle, memberName, rows) {
addMemberCompetitions(tournamentTitle, memberName, recommendedRows = [], otherRows = []) {
let y = this.margin;
this.pdf.setFont('helvetica', 'bold');
this.pdf.setFontSize(14);
this.pdf.text(tournamentTitle || 'Offizielles Turnier', this.margin, y);
y += 9;
this.pdf.setFont('helvetica', 'normal');
this.pdf.setFont('helvetica', 'bold');
this.pdf.setFontSize(12);
this.pdf.text(`Mitglied: ${memberName}`, this.margin, y);
y += 8;
this.pdf.setFont('helvetica', 'bold');
this.pdf.text('Wettbewerb', this.margin, y);
this.pdf.text('Datum', this.margin + 110, y);
this.pdf.text('Startzeit', this.margin + 150, y);
y += 7;
this.pdf.setFont('helvetica', 'normal');
for (const r of rows) {
this.pdf.text(r.name || '', this.margin, y);
this.pdf.text(r.date || '', this.margin + 110, y);
this.pdf.text(r.time || '', this.margin + 150, y);
// Empfehlungen (fett)
if (recommendedRows && recommendedRows.length) {
this.pdf.setFont('helvetica', 'bold');
this.pdf.setFontSize(13);
this.pdf.text('Empfehlungen', this.margin, y);
y += 7;
this.pdf.setFont('helvetica', 'bold');
this.pdf.setFontSize(12);
this.pdf.text('Wettbewerb', this.margin, y);
this.pdf.text('Datum', this.margin + 110, y);
this.pdf.text('Startzeit', this.margin + 150, y);
y += 7;
for (const r of recommendedRows) {
this.pdf.text(r.name || '', this.margin, y);
this.pdf.text(r.date || '', this.margin + 110, y);
this.pdf.text(r.time || '', this.margin + 150, y);
y += 7;
if (y > this.pageHeight) {
this.addNewPage();
y = this.margin;
}
}
}
// Weitere spielbare Wettbewerbe (normal)
if (otherRows && otherRows.length) {
y += 5;
if (y > this.pageHeight) { this.addNewPage(); y = this.margin; }
this.pdf.setFont('helvetica', 'bold');
this.pdf.setFontSize(13);
this.pdf.text('Ebenfalls spielbar', this.margin, y);
y += 7;
this.pdf.setFont('helvetica', 'normal');
this.pdf.setFontSize(12);
for (const r of otherRows) {
this.pdf.text(r.name || '', this.margin, y);
this.pdf.text(r.date || '', this.margin + 110, y);
this.pdf.text(r.time || '', this.margin + 150, y);
y += 7;
if (y > this.pageHeight) {
this.addNewPage();
y = this.margin;
}
}
}
// Hinweise-Sektion
const remainingForHints = 60; // Platz für Überschrift + Liste abschätzen
if (y + remainingForHints > this.pageHeight) {
this.addNewPage();
y = this.margin;
} else {
y += 6;
}
this.pdf.setFont('helvetica', 'bold');
this.pdf.setFontSize(13);
this.pdf.text('Hinweise:', this.margin, y);
y += 7;
this.pdf.setFont('helvetica', 'bold');
this.pdf.setFontSize(12);
const maxWidth = 210 - this.margin * 2;
const bullets = [
'Eine Stunde vor Beginn der Konkurrenz in der Halle sein',
'Kein weißes Trikot',
'Sportshorts (oder Sportröckchen), am besten auch nicht weiß',
'Hallenschuhe (dürfen auf Boden nicht abfärben)',
'Eine Flasche Wasser dabei haben',
'Da der Verein die Meldung übernehmen möchte, die Trainer mind. eine Woche vor dem Turnier über die Teilnahme informieren',
];
for (const b of bullets) {
const lines = this.pdf.splitTextToSize(`- ${b}`, maxWidth);
for (const line of lines) {
this.pdf.text(line, this.margin, y);
y += 6;
if (y > this.pageHeight) {
this.addNewPage();
y = this.margin;
this.pdf.setFont('helvetica', 'bold');
this.pdf.setFontSize(12);
}
}
}
// Leerzeile vor dem Abschlusssatz
if (y + 6 > this.pageHeight) {
this.addNewPage();
y = this.margin;
} else {
y += 6;
}
const finalLine = 'Die Trainer probieren bei allen Turnieren anwesend zu sein.';
this.pdf.setFont('helvetica', 'bold');
this.pdf.setFontSize(12);
const finalLines = this.pdf.splitTextToSize(finalLine, maxWidth);
for (const line of finalLines) {
this.pdf.text(line, this.margin, y);
y += 6;
if (y > this.pageHeight) {
this.addNewPage();
y = this.margin;
this.pdf.setFont('helvetica', 'bold');
this.pdf.setFontSize(12);
}
}
this.cursorY = y + 10;

View File

@@ -42,12 +42,12 @@
</div>
<!-- ehemals 'Erkannte Einträge' entfernt -->
</div>
<div class="top-actions">
<div v-if="parsed && parsed.parsedData.competitions && parsed.parsedData.competitions.length">
<div class="top-actions">
<button class="btn-secondary" @click="openMemberDialog" :disabled="!parsed || !activeMembers.length">Mitglieder auswählen</button>
<button class="btn-primary" :disabled="!selectedMemberIds.length" @click="generateMembersPdf">PDF für markierte Mitglieder</button>
</div>
<div v-if="parsed && parsed.parsedData.competitions && parsed.parsedData.competitions.length">
<h3>Konkurrenzen</h3>
<table>
<thead>
@@ -119,11 +119,27 @@
<button class="btn-primary" :disabled="!selectedMemberIds.length" @click="generateMembersPdf; closeMemberDialog()">PDF erzeugen</button>
</div>
<div class="modal-body">
<div class="checkbox-column">
<label v-for="m in activeMembers" :key="m.id" class="check-item">
<input type="checkbox" :value="m.id" v-model="selectedMemberIds" />
<span>{{ m.firstName }} {{ m.lastName }}</span>
</label>
<div class="dialog-layout">
<div class="dialog-col members-col">
<div class="checkbox-column">
<label v-for="m in activeMembers" :key="m.id" class="check-item" @click="selectedMemberIdForDialog = m.id">
<input type="checkbox" :value="m.id" v-model="selectedMemberIds" />
<span :class="{ active: selectedMemberIdForDialog === m.id }">{{ m.firstName }} {{ m.lastName }}</span>
</label>
</div>
</div>
<div class="dialog-col recommendations-col" v-if="selectedMemberInDialog">
<h4>Empfehlungen</h4>
<div v-if="selectedMemberCompetitions.length">
<label v-for="row in selectedMemberCompetitions" :key="row.key" class="check-item">
<input type="checkbox" :checked="isRecommended(selectedMemberInDialog.id, row.key)" @change="toggleRecommendation(selectedMemberInDialog.id, row.key)" />
<span>{{ row.name }} {{ row.date }} {{ row.time }}</span>
</label>
</div>
<div v-else>
<em>Keine passenden Wettbewerbe gefunden.</em>
</div>
</div>
</div>
</div>
</div>
@@ -149,12 +165,24 @@ export default {
expanded: {},
selectedMemberIds: [],
showMemberDialog: false,
memberRecommendations: {},
selectedMemberIdForDialog: null,
};
},
computed: {
...mapGetters(['currentClub']),
activeMembers() {
return (this.members || []).filter(m => m.active === true);
},
selectedMemberInDialog() {
const id = this.selectedMemberIdForDialog;
if (!id) return null;
return (this.members || []).find(m => m.id === id) || null;
},
selectedMemberCompetitions() {
const m = this.selectedMemberInDialog;
if (!m) return [];
return this.competitionsForMember(m);
}
},
methods: {
@@ -211,15 +239,27 @@ export default {
competitionsForMember(member) {
const comps = (this.parsed && this.parsed.parsedData && this.parsed.parsedData.competitions) ? this.parsed.parsedData.competitions : [];
const rows = [];
for (const c of comps) {
for (let idx = 0; idx < comps.length; idx++) {
const c = comps[idx];
if (this.isEligibleForCompetition(member, c)) {
const title = c.ageClassCompetition || c.altersklasseWettbewerb || '';
const st = this.splitDateTime(c.startTime || c.startzeit || '');
rows.push({ name: title, date: st.date, time: st.time });
rows.push({ key: String(idx), name: title, date: st.date, time: st.time, raw: c });
}
}
return rows;
},
isRecommended(memberId, compKey) {
const set = this.memberRecommendations[memberId];
return !!(set && set.has && set.has(compKey));
},
toggleRecommendation(memberId, compKey) {
if (!this.memberRecommendations[memberId]) {
this.$set ? this.$set(this.memberRecommendations, memberId, new Set()) : (this.memberRecommendations[memberId] = new Set());
}
const set = this.memberRecommendations[memberId];
if (set.has(compKey)) set.delete(compKey); else set.add(compKey);
},
async generateMembersPdf() {
if (!this.selectedMemberIds.length) return;
const pdf = new PDFGenerator();
@@ -229,9 +269,12 @@ export default {
const m = (this.members || []).find(x => x.id === mid);
if (!m) continue;
const rows = this.competitionsForMember(m);
const recKeys = this.memberRecommendations[mid] ? Array.from(this.memberRecommendations[mid]) : [];
const recRows = rows.filter(r => recKeys.includes(r.key));
const otherRows = rows.filter(r => !recKeys.includes(r.key));
if (!first) pdf.addNewPage();
first = false;
pdf.addMemberCompetitions(title, `${m.firstName} ${m.lastName}`, rows);
pdf.addMemberCompetitions(title, `${m.firstName} ${m.lastName}`, recRows, otherRows);
}
pdf.save('turnier_mitglieder.pdf');
},
@@ -424,6 +467,10 @@ th, td { border-bottom: 1px solid var(--border-color); padding: 0.5rem; text-ali
.modal-body { padding: .75rem 1rem; overflow: auto; }
.checkbox-column { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: .5rem .75rem; }
.check-item { display: flex; align-items: center; gap: .35rem; }
.dialog-layout { display: grid; grid-template-columns: 1fr 1.2fr; gap: 1rem; align-items: start; }
.dialog-col h4 { margin: 0 0 .5rem 0; }
.members-col .check-item span.active { font-weight: bold; }
.recommendations-col .check-item { padding: .15rem 0; }
</style>