Add training day PDF generation and summary functionality

This commit introduces a new method, `addTrainingDaySummary`, in the `PDFGenerator` class to create detailed summaries for training days, including member activities and their respective times. Additionally, the `DiaryView` component is updated with a new button to generate training day PDFs, enhancing the user experience by allowing easy access to training summaries. Debugging outputs are included for better tracking of data during PDF generation.
This commit is contained in:
Torsten Schulz (local)
2025-11-15 22:58:47 +01:00
parent cd8f40aa9d
commit 5923ef8bba
2 changed files with 375 additions and 1 deletions

View File

@@ -1153,6 +1153,310 @@ class PDFGenerator {
.join(', ');
}
addTrainingDaySummary(clubName, trainingDate, trainingStart, trainingEnd, members, activities, activityMembersMap, groupActivityMembersMap, participantMapByMemberId, groups, trainingPlan = [], memberGroupsMap = {}) {
// Header
const formattedDate = new Date(trainingDate).toLocaleDateString('de-DE');
const formattedStartTime = trainingStart ? trainingStart.slice(0, 5) : '';
const formattedEndTime = trainingEnd ? trainingEnd.slice(0, 5) : '';
this.pdf.setFontSize(14);
this.pdf.setFont('helvetica', 'bold');
this.pdf.text(`${clubName} - Trainingstag`, this.margin, this.cursorY);
this.cursorY += 8;
this.pdf.setFontSize(12);
this.pdf.setFont('helvetica', 'normal');
this.pdf.text(`Datum: ${formattedDate}`, this.margin, this.cursorY);
this.cursorY += 7;
if (formattedStartTime && formattedEndTime) {
this.pdf.text(`Uhrzeit: ${formattedStartTime} - ${formattedEndTime}`, this.margin, this.cursorY);
this.cursorY += 7;
}
this.cursorY += 5;
// Debug-Ausgaben
console.log('[addTrainingDaySummary] members:', members?.length || 0);
console.log('[addTrainingDaySummary] activities:', activities?.length || 0);
console.log('[addTrainingDaySummary] activityMembersMap:', activityMembersMap);
console.log('[addTrainingDaySummary] participantMapByMemberId:', participantMapByMemberId);
// Erstelle Mapping: memberId -> Liste von Aktivitäten
const memberActivitiesMap = new Map();
// Initialisiere für alle Teilnehmer
members.forEach(member => {
if (member && member.id) {
memberActivitiesMap.set(member.id, []);
}
});
// Erstelle umgekehrtes Mapping: participantId -> memberId (einmalig)
const participantToMemberMap = {};
if (participantMapByMemberId) {
Object.keys(participantMapByMemberId).forEach(memberId => {
const participantId = participantMapByMemberId[memberId];
if (participantId) {
participantToMemberMap[participantId] = parseInt(memberId);
}
});
}
// Erstelle Mapping von activityId zu startTime aus trainingPlan
const activityIdToStartTime = new Map();
trainingPlan.forEach(item => {
if (item && item.id && item.startTime) {
activityIdToStartTime.set(item.id, item.startTime);
}
});
// Durchlaufe alle Aktivitäten
let activitiesProcessed = 0;
let activitiesWithMembers = 0;
activities.forEach(activity => {
if (!activity || !activity.id) return;
activitiesProcessed++;
const activityName = activity.predefinedActivity
? activity.predefinedActivity.name
: (activity.description || 'Unbekannte Aktivität');
// Verwende startTime aus trainingPlan, falls vorhanden, sonst leer
const activityStartTime = activityIdToStartTime.get(activity.id) || activity.startTime || '';
const activityDuration = activity.duration ? `${activity.duration} Min` : '';
// Finde alle Teilnehmer dieser Aktivität
const participantIds = activityMembersMap[activity.id];
let hasDirectAssignments = false;
if (participantIds) {
// Handle both Set and Array
const idsArray = participantIds instanceof Set ? Array.from(participantIds) : (Array.isArray(participantIds) ? participantIds : []);
if (idsArray.length > 0) {
hasDirectAssignments = true;
activitiesWithMembers++;
idsArray.forEach(participantId => {
// Finde memberId für diesen participantId
const memberId = participantToMemberMap[participantId];
if (memberId) {
if (!memberActivitiesMap.has(memberId)) {
memberActivitiesMap.set(memberId, []);
}
memberActivitiesMap.get(memberId).push({
name: activityName,
startTime: activityStartTime,
duration: activityDuration
});
} else {
console.log('[addTrainingDaySummary] Kein memberId gefunden für participantId:', participantId);
}
});
}
}
// Wenn keine direkten Zuordnungen vorhanden sind, verwende Gruppen-Zuordnung
if (!hasDirectAssignments) {
const activityGroupId = activity.groupActivity ? activity.groupActivity.id : null;
if (activityGroupId) {
// Aktivität hat eine Gruppe: nehme alle Members dieser Gruppe
members.forEach(member => {
if (member && member.id) {
const memberGroupId = memberGroupsMap[member.id];
// Prüfe ob memberGroupId als String oder Number vorliegt
const memberGroupIdStr = memberGroupId ? String(memberGroupId) : '';
const activityGroupIdStr = String(activityGroupId);
if (memberGroupIdStr === activityGroupIdStr) {
if (!memberActivitiesMap.has(member.id)) {
memberActivitiesMap.set(member.id, []);
}
memberActivitiesMap.get(member.id).push({
name: activityName,
startTime: activityStartTime,
duration: activityDuration
});
}
}
});
} else {
// Aktivität hat keine Gruppe: nehme alle Teilnehmer
members.forEach(member => {
if (member && member.id) {
if (!memberActivitiesMap.has(member.id)) {
memberActivitiesMap.set(member.id, []);
}
memberActivitiesMap.get(member.id).push({
name: activityName,
startTime: activityStartTime,
duration: activityDuration
});
}
});
}
}
});
console.log('[addTrainingDaySummary] Aktivitäten verarbeitet:', activitiesProcessed, 'mit Mitgliedern:', activitiesWithMembers);
// Durchlaufe alle Gruppenaktivitäten
// WICHTIG: Auch aus Zeitblöcken, daher trainingPlan verwenden statt activities
if (groupActivityMembersMap && Object.keys(groupActivityMembersMap).length > 0) {
// Finde Gruppenaktivitäten in trainingPlan (auch in Zeitblöcken)
trainingPlan.forEach(activity => {
if (!activity || !activity.groupActivities) return;
activity.groupActivities.forEach(groupActivity => {
if (!groupActivity || !groupActivity.id) return;
const activityName = groupActivity.groupPredefinedActivity
? groupActivity.groupPredefinedActivity.name
: (groupActivity.description || 'Unbekannte Gruppenaktivität');
// Verwende startTime aus trainingPlan für die übergeordnete Aktivität
const activityStartTime = activityIdToStartTime.get(activity.id) || activity.startTime || '';
const activityDuration = activity.duration ? `${activity.duration} Min` : '';
// Finde alle Teilnehmer dieser Gruppenaktivität
const participantIds = groupActivityMembersMap[groupActivity.id];
let hasDirectGroupActivityAssignments = false;
if (participantIds) {
// Handle both Set and Array
const idsArray = participantIds instanceof Set ? Array.from(participantIds) : (Array.isArray(participantIds) ? participantIds : []);
if (idsArray.length > 0) {
hasDirectGroupActivityAssignments = true;
idsArray.forEach(participantId => {
// Finde memberId für diesen participantId
const memberId = participantToMemberMap[participantId];
if (memberId) {
if (!memberActivitiesMap.has(memberId)) {
memberActivitiesMap.set(memberId, []);
}
memberActivitiesMap.get(memberId).push({
name: activityName,
startTime: activityStartTime,
duration: activityDuration
});
}
});
}
}
// Wenn keine direkten Zuordnungen vorhanden sind, verwende Gruppen-Zuordnung
if (!hasDirectGroupActivityAssignments) {
const groupActivityGroupId = groupActivity.groupsGroupActivity ? groupActivity.groupsGroupActivity.id : null;
if (groupActivityGroupId) {
// Gruppenaktivität hat eine Gruppe: nehme alle Members dieser Gruppe
members.forEach(member => {
if (member && member.id) {
const memberGroupId = memberGroupsMap[member.id];
// Prüfe ob memberGroupId als String oder Number vorliegt
const memberGroupIdStr = memberGroupId ? String(memberGroupId) : '';
const groupActivityGroupIdStr = String(groupActivityGroupId);
if (memberGroupIdStr === groupActivityGroupIdStr) {
if (!memberActivitiesMap.has(member.id)) {
memberActivitiesMap.set(member.id, []);
}
memberActivitiesMap.get(member.id).push({
name: activityName,
startTime: activityStartTime,
duration: activityDuration
});
}
}
});
} else {
// Gruppenaktivität hat keine Gruppe: nehme alle Teilnehmer
members.forEach(member => {
if (member && member.id) {
if (!memberActivitiesMap.has(member.id)) {
memberActivitiesMap.set(member.id, []);
}
memberActivitiesMap.get(member.id).push({
name: activityName,
startTime: activityStartTime,
duration: activityDuration
});
}
});
}
}
});
});
}
// Sortiere Mitglieder alphabetisch
const sortedMembers = members
.filter(m => m && m.id && memberActivitiesMap.has(m.id))
.sort((a, b) => {
const lastNameCompare = (a.lastName || '').localeCompare(b.lastName || '');
if (lastNameCompare !== 0) return lastNameCompare;
return (a.firstName || '').localeCompare(b.firstName || '');
});
console.log('[addTrainingDaySummary] Mitglieder mit Aktivitäten:', sortedMembers.length);
console.log('[addTrainingDaySummary] memberActivitiesMap:', Array.from(memberActivitiesMap.entries()).map(([id, acts]) => ({ id, count: acts.length })));
// Zeige für jeden Teilnehmer seine Aktivitäten
sortedMembers.forEach(member => {
const memberActivities = memberActivitiesMap.get(member.id);
if (!memberActivities || memberActivities.length === 0) return;
// Prüfe ob neue Seite nötig
if (this.cursorY > 250) {
this.addNewPage();
this.cursorY = this.margin;
}
// Mitgliedsname
this.pdf.setFont('helvetica', 'bold');
this.pdf.setFontSize(12);
const memberName = `${member.firstName || ''} ${member.lastName || ''}`.trim();
this.pdf.text(memberName, this.margin, this.cursorY);
this.cursorY += 8;
// Aktivitäten
this.pdf.setFont('helvetica', 'normal');
this.pdf.setFontSize(10);
memberActivities.forEach(activity => {
if (this.cursorY > 280) {
this.addNewPage();
this.cursorY = this.margin;
// Mitgliedsname erneut anzeigen bei Seitenwechsel
this.pdf.setFont('helvetica', 'bold');
this.pdf.setFontSize(12);
this.pdf.text(memberName, this.margin, this.cursorY);
this.cursorY += 8;
this.pdf.setFont('helvetica', 'normal');
this.pdf.setFontSize(10);
}
let activityText = `${activity.name}`;
if (activity.startTime) {
activityText += ` (${activity.startTime}`;
if (activity.duration) {
activityText += `, ${activity.duration}`;
}
activityText += ')';
} else if (activity.duration) {
activityText += ` (${activity.duration})`;
}
this.pdf.text(activityText, this.margin + 5, this.cursorY);
this.cursorY += 6;
});
// Abstand nach Mitglied
this.cursorY += 3;
});
// Falls keine Aktivitäten vorhanden
if (sortedMembers.length === 0) {
this.pdf.setFont('helvetica', 'normal');
this.pdf.setFontSize(11);
this.pdf.text('Keine Aktivitäten für diesen Trainingstag erfasst.', this.margin, this.cursorY);
this.cursorY += 10;
}
}
}
export default PDFGenerator;

View File

@@ -432,7 +432,11 @@
</table>
<div style="margin-top: 1rem; margin-bottom: 2rem; padding-bottom: 1rem;">
<button v-if="trainingPlan && trainingPlan.length && trainingPlan.length > 0"
@click="generatePDF">Als PDF
@click="generatePDF">Trainingsplan als PDF
herunterladen</button>
<button v-if="date && participants && participants.length > 0"
@click="generateTrainingDayPDF"
style="margin-left: 1rem;">Trainingstag als PDF
herunterladen</button>
</div>
</div>
@@ -1919,6 +1923,72 @@ export default {
pdf.addTrainingPlan(this.currentClubName, this.date.date, this.trainingStart, this.trainingEnd, this.trainingPlan);
pdf.save('trainingsplan.pdf');
},
async generateTrainingDayPDF() {
if (!this.date || !this.participants || this.participants.length === 0) {
this.showInfo('Fehler', 'Keine Teilnehmer für diesen Trainingstag vorhanden.', '', 'error');
return;
}
try {
// Lade alle Aktivitäts-Mitglieder-Zuordnungen
await this.loadAllActivityMembers();
// Filtere Mitglieder: nur die, die auch Teilnehmer sind
const participantMembers = this.members.filter(m =>
m && m.id && this.participants.includes(m.id)
);
// Verwende trainingPlan statt activities, da die Aktivitäten dort sind
// Filtere Zeitblöcke komplett aus, da sie immer mehrere Gruppen umfassen
const activitiesForPDF = this.trainingPlan.filter(item => !item.isTimeblock);
// Debug-Ausgaben
console.log('[generateTrainingDayPDF] participantMembers:', participantMembers.length);
console.log('[generateTrainingDayPDF] trainingPlan items:', this.trainingPlan.length);
console.log('[generateTrainingDayPDF] activitiesForPDF:', activitiesForPDF.length);
console.log('[generateTrainingDayPDF] activityMembersMap keys:', Object.keys(this.activityMembersMap || {}));
console.log('[generateTrainingDayPDF] groupActivityMembersMap keys:', Object.keys(this.groupActivityMembersMap || {}));
console.log('[generateTrainingDayPDF] participantMapByMemberId:', this.participantMapByMemberId);
const pdf = new PDFGenerator();
pdf.addTrainingDaySummary(
this.currentClubName,
this.date.date,
this.trainingStart,
this.trainingEnd,
participantMembers,
activitiesForPDF,
this.activityMembersMap,
this.groupActivityMembersMap,
this.participantMapByMemberId,
this.groups,
this.trainingPlan,
this.memberGroupsMap
);
const dateStr = new Date(this.date.date).toLocaleDateString('de-DE').replace(/\./g, '-');
pdf.save(`trainingstag-${dateStr}.pdf`);
} catch (error) {
console.error('Fehler beim Generieren des Trainingstag-PDFs:', error);
this.showInfo('Fehler', 'Fehler beim Generieren des PDFs.', error.message || '', 'error');
}
},
async loadAllActivityMembers() {
// Lade für alle Aktivitäten im trainingPlan die Mitglieder-Zuordnungen
for (const activity of this.trainingPlan) {
if (activity && activity.id) {
await this.ensureActivityMembersLoaded(activity.id);
}
// Lade auch für Gruppenaktivitäten
if (activity && activity.groupActivities) {
for (const groupActivity of activity.groupActivities) {
if (groupActivity && groupActivity.id) {
await this.ensureGroupActivityMembersLoaded(groupActivity.id);
}
}
}
}
},
sortedMembers() {
// Erstelle eine Kopie des Arrays, um Mutation zu vermeiden
return [...this.members].sort((a, b) => {