diff --git a/backend/migrations/20260402_add_team_gender_team_age_group_to_club_team.sql b/backend/migrations/20260402_add_team_gender_team_age_group_to_club_team.sql
new file mode 100644
index 00000000..f03ab803
--- /dev/null
+++ b/backend/migrations/20260402_add_team_gender_team_age_group_to_club_team.sql
@@ -0,0 +1,10 @@
+-- club_team: Felder wie backend/models/ClubTeam.js (teamGender, teamAgeGroup)
+-- Fehlen in der DB -> SequelizeDatabaseError ER_BAD_FIELD_ERROR bei getClubTeams.
+
+ALTER TABLE `club_team`
+ ADD COLUMN `team_gender` ENUM('open', 'female') NOT NULL DEFAULT 'open'
+ COMMENT 'Geschlecht Team (offen / nur weiblich)'
+ AFTER `my_tischtennis_team_id`,
+ ADD COLUMN `team_age_group` ENUM('adult', 'J19', 'J17', 'J15', 'J13', 'J11') NOT NULL DEFAULT 'adult'
+ COMMENT 'Altersklasse Mannschaft'
+ AFTER `team_gender`;
diff --git a/frontend/src/components/PDFGenerator.js b/frontend/src/components/PDFGenerator.js
index de017070..78e20737 100644
--- a/frontend/src/components/PDFGenerator.js
+++ b/frontend/src/components/PDFGenerator.js
@@ -68,7 +68,17 @@ class PDFGenerator {
'pdfGenerator.result': 'Ergebnis',
'pdfGenerator.status': 'Status',
'pdfGenerator.placement': 'Platzierung',
- 'pdfGenerator.competitionName': 'Konkurrenz'
+ 'pdfGenerator.competitionName': 'Konkurrenz',
+ 'pdfGenerator.teamLineupTitle': 'Mannschaftsaufstellung',
+ 'pdfGenerator.clubLabel': 'Verein:',
+ 'pdfGenerator.teamNameLabel': 'Mannschaft:',
+ 'pdfGenerator.leagueLabel': 'Spielklasse:',
+ 'pdfGenerator.seasonLabel': 'Saison:',
+ 'pdfGenerator.periodLabel': 'Meldung für:',
+ 'pdfGenerator.teamGenderLabel': 'Team Geschlecht:',
+ 'pdfGenerator.teamAgeGroupLabel': 'Team Altersklasse:',
+ 'pdfGenerator.generatedAt': 'Erstellt:',
+ 'pdfGenerator.lineupQttr': '(Q)TTR'
};
return fallbacks[key] || key;
});
@@ -1510,6 +1520,43 @@ class PDFGenerator {
}
}
+ /**
+ * Mannschaftsaufstellung: Titel, Meta-Zeilen (bereits übersetzt) und Spielertabelle.
+ * @param {object} options
+ * @param {string} [options.title]
+ * @param {string[]} [options.introLines]
+ * @param {string[][]} options.tableHead - Eine Kopfzeile: [ [col1, col2, …] ]
+ * @param {string[][]} options.tableBody
+ */
+ addTeamLineup({ title, introLines, tableHead, tableBody }) {
+ const pdfTitle = title || this.t('pdfGenerator.teamLineupTitle');
+ this.cursorY = this.margin;
+ this.pdf.setFont('helvetica', 'bold');
+ this.pdf.setFontSize(15);
+ this.pdf.text(pdfTitle, this.margin, this.cursorY);
+ this.cursorY += 9;
+ this.pdf.setFont('helvetica', 'normal');
+ this.pdf.setFontSize(10);
+ const maxW = 210 - this.margin * 2;
+ for (const line of introLines || []) {
+ const lines = this.pdf.splitTextToSize(String(line), maxW);
+ lines.forEach((ln) => {
+ this.pdf.text(ln, this.margin, this.cursorY);
+ this.cursorY += 5;
+ });
+ }
+ this.cursorY += 3;
+ autoTable(this.pdf, {
+ startY: this.cursorY,
+ margin: { left: this.margin, right: this.margin },
+ head: tableHead,
+ body: tableBody,
+ theme: 'grid',
+ styles: { fontSize: 10, cellPadding: 2, overflow: 'linebreak' },
+ headStyles: { fillColor: [220, 220, 220], textColor: 0, halign: 'left', fontStyle: 'bold' }
+ });
+ }
+
}
export default PDFGenerator;
diff --git a/frontend/src/i18n/locales/de-CH.json b/frontend/src/i18n/locales/de-CH.json
index 641e8a03..5eb66da2 100644
--- a/frontend/src/i18n/locales/de-CH.json
+++ b/frontend/src/i18n/locales/de-CH.json
@@ -904,11 +904,26 @@
"selectedLineup": "Gemeldete Spieler",
"availableLineupMembers": "Verfügbare Spieler",
"lineupEmpty": "Noch keine Spieler für diese Mannschaft gemeldet.",
+ "exportLineupPdf": "Aufstellung als PDF",
+ "lineupPdfEmpty": "Es sind keine Spieler in der Aufstellung – PDF kann nicht erstellt werden.",
+ "lineupPdfFilePrefix": "Aufstellung",
"lineupSaveError": "Mannschaftsmeldung konnte nicht gespeichert werden.",
"lineupValidationTooLargeGap": "{higher} hat mehr als 30 QTTR Punkte Vorsprung vor {lower}. Diese Reihenfolge bitte korrigieren.",
"firstHalf": "Vorrunde",
"firstHalfFull": "Vorrunde (Juli - Dezember)",
"secondHalf": "Rückrunde",
"secondHalfFull": "Rückrunde (ab 1. Januar)"
+ },
+ "pdfGenerator": {
+ "teamLineupTitle": "Mannschaftsaufstellung",
+ "clubLabel": "Verein:",
+ "teamNameLabel": "Mannschaft:",
+ "leagueLabel": "Spielklasse:",
+ "seasonLabel": "Saison:",
+ "periodLabel": "Meldung für:",
+ "teamGenderLabel": "Team Geschlecht:",
+ "teamAgeGroupLabel": "Team Altersklasse:",
+ "generatedAt": "Erstellt:",
+ "lineupQttr": "(Q)TTR"
}
}
diff --git a/frontend/src/i18n/locales/de-extended.json b/frontend/src/i18n/locales/de-extended.json
index a15f7389..d00827f9 100644
--- a/frontend/src/i18n/locales/de-extended.json
+++ b/frontend/src/i18n/locales/de-extended.json
@@ -588,11 +588,26 @@
"selectedLineup": "Gemeldete Spieler",
"availableLineupMembers": "Verfügbare Spieler",
"lineupEmpty": "Noch keine Spieler für diese Mannschaft gemeldet.",
+ "exportLineupPdf": "Aufstellung als PDF",
+ "lineupPdfEmpty": "Es sind keine Spieler in der Aufstellung – PDF kann nicht erstellt werden.",
+ "lineupPdfFilePrefix": "Aufstellung",
"lineupSaveError": "Mannschaftsmeldung konnte nicht gespeichert werden.",
"lineupValidationTooLargeGap": "{higher} hat mehr als 30 QTTR Punkte Vorsprung vor {lower}. Diese Reihenfolge bitte korrigieren.",
"firstHalf": "Vorrunde",
"firstHalfFull": "Vorrunde (Juli - Dezember)",
"secondHalf": "Rückrunde",
"secondHalfFull": "Rückrunde (ab 1. Januar)"
+ },
+ "pdfGenerator": {
+ "teamLineupTitle": "Mannschaftsaufstellung",
+ "clubLabel": "Verein:",
+ "teamNameLabel": "Mannschaft:",
+ "leagueLabel": "Spielklasse:",
+ "seasonLabel": "Saison:",
+ "periodLabel": "Meldung für:",
+ "teamGenderLabel": "Team Geschlecht:",
+ "teamAgeGroupLabel": "Team Altersklasse:",
+ "generatedAt": "Erstellt:",
+ "lineupQttr": "(Q)TTR"
}
}
diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json
index c420996f..bacd9154 100644
--- a/frontend/src/i18n/locales/de.json
+++ b/frontend/src/i18n/locales/de.json
@@ -1360,6 +1360,9 @@
"selectedLineup": "Gemeldete Spieler",
"availableLineupMembers": "Verfügbare Spieler",
"lineupEmpty": "Noch keine Spieler für diese Mannschaft gemeldet.",
+ "exportLineupPdf": "Aufstellung als PDF",
+ "lineupPdfEmpty": "Es sind keine Spieler in der Aufstellung – PDF kann nicht erstellt werden.",
+ "lineupPdfFilePrefix": "Aufstellung",
"lineupUnsavedChanges": "Ungespeicherte Änderungen in der Mannschaftsmeldung.",
"lineupSaved": "Mannschaftsmeldung gespeichert.",
"lineupSaveError": "Mannschaftsmeldung konnte nicht gespeichert werden.",
@@ -2348,6 +2351,16 @@
"result": "Ergebnis",
"status": "Status",
"placement": "Platzierung",
- "competitionName": "Konkurrenz"
+ "competitionName": "Konkurrenz",
+ "teamLineupTitle": "Mannschaftsaufstellung",
+ "clubLabel": "Verein:",
+ "teamNameLabel": "Mannschaft:",
+ "leagueLabel": "Spielklasse:",
+ "seasonLabel": "Saison:",
+ "periodLabel": "Meldung für:",
+ "teamGenderLabel": "Team Geschlecht:",
+ "teamAgeGroupLabel": "Team Altersklasse:",
+ "generatedAt": "Erstellt:",
+ "lineupQttr": "(Q)TTR"
}
}
diff --git a/frontend/src/i18n/locales/en-AU.json b/frontend/src/i18n/locales/en-AU.json
index 1970ec79..822ec725 100644
--- a/frontend/src/i18n/locales/en-AU.json
+++ b/frontend/src/i18n/locales/en-AU.json
@@ -904,11 +904,26 @@
"selectedLineup": "Selected players",
"availableLineupMembers": "Available players",
"lineupEmpty": "No players have been assigned to this team yet.",
+ "exportLineupPdf": "Export line-up as PDF",
+ "lineupPdfEmpty": "No players in the line-up – cannot create PDF.",
+ "lineupPdfFilePrefix": "Line-up",
"lineupSaveError": "Team line-up could not be saved.",
"lineupValidationTooLargeGap": "{higher} is more than 30 QTTR points ahead of {lower}. Please correct this order.",
"firstHalf": "First half",
"firstHalfFull": "First half (July - December)",
"secondHalf": "Second half",
"secondHalfFull": "Second half (from 1 January)"
+ },
+ "pdfGenerator": {
+ "teamLineupTitle": "Team line-up",
+ "clubLabel": "Club:",
+ "teamNameLabel": "Team:",
+ "leagueLabel": "League:",
+ "seasonLabel": "Season:",
+ "periodLabel": "Registration for:",
+ "teamGenderLabel": "Team gender:",
+ "teamAgeGroupLabel": "Team age group:",
+ "generatedAt": "Generated:",
+ "lineupQttr": "(Q)TTR"
}
}
diff --git a/frontend/src/i18n/locales/en-GB.json b/frontend/src/i18n/locales/en-GB.json
index dafc5f8b..06c620cb 100644
--- a/frontend/src/i18n/locales/en-GB.json
+++ b/frontend/src/i18n/locales/en-GB.json
@@ -1051,6 +1051,9 @@
"selectedLineup": "Selected players",
"availableLineupMembers": "Available players",
"lineupEmpty": "No players have been assigned to this team yet.",
+ "exportLineupPdf": "Export line-up as PDF",
+ "lineupPdfEmpty": "No players in the line-up – cannot create PDF.",
+ "lineupPdfFilePrefix": "Line-up",
"lineupUnsavedChanges": "There are unsaved changes in the team line-up.",
"lineupSaved": "Team line-up saved.",
"lineupSaveError": "Team line-up could not be saved.",
@@ -1163,5 +1166,17 @@
"invalidMimeType": "{label} has an unexpected MIME type: {type}.",
"fileTooLargeTitle": "File too large",
"fileTooLarge": "{label} must not exceed 10 MB."
+ },
+ "pdfGenerator": {
+ "teamLineupTitle": "Team line-up",
+ "clubLabel": "Club:",
+ "teamNameLabel": "Team:",
+ "leagueLabel": "League:",
+ "seasonLabel": "Season:",
+ "periodLabel": "Registration for:",
+ "teamGenderLabel": "Team gender:",
+ "teamAgeGroupLabel": "Team age group:",
+ "generatedAt": "Generated:",
+ "lineupQttr": "(Q)TTR"
}
}
diff --git a/frontend/src/i18n/locales/en-US.json b/frontend/src/i18n/locales/en-US.json
index 17b03502..90710e86 100644
--- a/frontend/src/i18n/locales/en-US.json
+++ b/frontend/src/i18n/locales/en-US.json
@@ -904,11 +904,26 @@
"selectedLineup": "Selected players",
"availableLineupMembers": "Available players",
"lineupEmpty": "No players have been assigned to this team yet.",
+ "exportLineupPdf": "Export line-up as PDF",
+ "lineupPdfEmpty": "No players in the line-up – cannot create PDF.",
+ "lineupPdfFilePrefix": "Lineup",
"lineupSaveError": "Team line-up could not be saved.",
"lineupValidationTooLargeGap": "{higher} is more than 30 QTTR points ahead of {lower}. Please correct this order.",
"firstHalf": "First half",
"firstHalfFull": "First half (July - December)",
"secondHalf": "Second half",
"secondHalfFull": "Second half (from 1 January)"
+ },
+ "pdfGenerator": {
+ "teamLineupTitle": "Team line-up",
+ "clubLabel": "Club:",
+ "teamNameLabel": "Team:",
+ "leagueLabel": "League:",
+ "seasonLabel": "Season:",
+ "periodLabel": "Registration for:",
+ "teamGenderLabel": "Team gender:",
+ "teamAgeGroupLabel": "Team age group:",
+ "generatedAt": "Generated:",
+ "lineupQttr": "(Q)TTR"
}
}
diff --git a/frontend/src/i18n/locales/es.json b/frontend/src/i18n/locales/es.json
index 138d9ed7..827dcca4 100644
--- a/frontend/src/i18n/locales/es.json
+++ b/frontend/src/i18n/locales/es.json
@@ -871,11 +871,26 @@
"selectedLineup": "Jugadores inscritos",
"availableLineupMembers": "Jugadores disponibles",
"lineupEmpty": "Todavía no se ha inscrito ningún jugador para este equipo.",
+ "exportLineupPdf": "Exportar alineación como PDF",
+ "lineupPdfEmpty": "No hay jugadores en la alineación – no se puede crear el PDF.",
+ "lineupPdfFilePrefix": "Alineacion",
"lineupSaveError": "No se pudo guardar la alineación del equipo.",
"lineupValidationTooLargeGap": "{higher} tiene más de 30 puntos QTTR de ventaja sobre {lower}. Corrige este orden.",
"firstHalf": "Primera vuelta",
"firstHalfFull": "Primera vuelta (julio - diciembre)",
"secondHalf": "Segunda vuelta",
"secondHalfFull": "Segunda vuelta (desde el 1 de enero)"
+ },
+ "pdfGenerator": {
+ "teamLineupTitle": "Alineación del equipo",
+ "clubLabel": "Club:",
+ "teamNameLabel": "Equipo:",
+ "leagueLabel": "Liga:",
+ "seasonLabel": "Temporada:",
+ "periodLabel": "Inscripción para:",
+ "teamGenderLabel": "Género del equipo:",
+ "teamAgeGroupLabel": "Categoría de edad del equipo:",
+ "generatedAt": "Generado:",
+ "lineupQttr": "(Q)TTR"
}
}
diff --git a/frontend/src/i18n/locales/fil.json b/frontend/src/i18n/locales/fil.json
index 3e6c3d0e..a7b706e7 100644
--- a/frontend/src/i18n/locales/fil.json
+++ b/frontend/src/i18n/locales/fil.json
@@ -871,11 +871,26 @@
"selectedLineup": "Napiling manlalaro",
"availableLineupMembers": "Magagamit na manlalaro",
"lineupEmpty": "Wala pang manlalarong itinalaga sa koponang ito.",
+ "exportLineupPdf": "I-export ang line-up bilang PDF",
+ "lineupPdfEmpty": "Walang manlalaro sa line-up – hindi makagawa ng PDF.",
+ "lineupPdfFilePrefix": "Line-up",
"lineupSaveError": "Hindi mai-save ang line-up ng koponan.",
"lineupValidationTooLargeGap": "Mahigit 30 QTTR points ang lamang ni {higher} kay {lower}. Pakitama ang ayos na ito.",
"firstHalf": "Unang yugto",
"firstHalfFull": "Unang yugto (Hulyo - Disyembre)",
"secondHalf": "Ikalawang yugto",
"secondHalfFull": "Ikalawang yugto (mula Enero 1)"
+ },
+ "pdfGenerator": {
+ "teamLineupTitle": "Line-up ng koponan",
+ "clubLabel": "Club:",
+ "teamNameLabel": "Koponan:",
+ "leagueLabel": "Liga:",
+ "seasonLabel": "Season:",
+ "periodLabel": "Registration para sa:",
+ "teamGenderLabel": "Kasarian ng koponan:",
+ "teamAgeGroupLabel": "Age group ng koponan:",
+ "generatedAt": "Nilikha:",
+ "lineupQttr": "(Q)TTR"
}
}
diff --git a/frontend/src/i18n/locales/fr.json b/frontend/src/i18n/locales/fr.json
index 4c9d9ea6..8c1ccf72 100644
--- a/frontend/src/i18n/locales/fr.json
+++ b/frontend/src/i18n/locales/fr.json
@@ -871,11 +871,26 @@
"selectedLineup": "Joueurs inscrits",
"availableLineupMembers": "Joueurs disponibles",
"lineupEmpty": "Aucun joueur n’a encore été inscrit pour cette équipe.",
+ "exportLineupPdf": "Exporter la composition en PDF",
+ "lineupPdfEmpty": "Aucun joueur dans la composition – impossible de créer le PDF.",
+ "lineupPdfFilePrefix": "Composition",
"lineupSaveError": "La composition de l’équipe n’a pas pu être enregistrée.",
"lineupValidationTooLargeGap": "{higher} a plus de 30 points QTTR d’avance sur {lower}. Veuillez corriger cet ordre.",
"firstHalf": "Phase aller",
"firstHalfFull": "Phase aller (juillet - décembre)",
"secondHalf": "Phase retour",
"secondHalfFull": "Phase retour (à partir du 1er janvier)"
+ },
+ "pdfGenerator": {
+ "teamLineupTitle": "Composition de l’équipe",
+ "clubLabel": "Club :",
+ "teamNameLabel": "Équipe :",
+ "leagueLabel": "Ligue :",
+ "seasonLabel": "Saison :",
+ "periodLabel": "Inscription pour :",
+ "teamGenderLabel": "Genre d’équipe :",
+ "teamAgeGroupLabel": "Catégorie d’âge d’équipe :",
+ "generatedAt": "Créé le :",
+ "lineupQttr": "(Q)TTR"
}
}
diff --git a/frontend/src/i18n/locales/it.json b/frontend/src/i18n/locales/it.json
index b00a8325..0e22e6a4 100644
--- a/frontend/src/i18n/locales/it.json
+++ b/frontend/src/i18n/locales/it.json
@@ -871,11 +871,26 @@
"selectedLineup": "Giocatori schierati",
"availableLineupMembers": "Giocatori disponibili",
"lineupEmpty": "Nessun giocatore è ancora stato assegnato a questa squadra.",
+ "exportLineupPdf": "Esporta formazione come PDF",
+ "lineupPdfEmpty": "Nessun giocatore in formazione – impossibile creare il PDF.",
+ "lineupPdfFilePrefix": "Formazione",
"lineupSaveError": "Impossibile salvare la formazione della squadra.",
"lineupValidationTooLargeGap": "{higher} ha più di 30 punti QTTR di vantaggio su {lower}. Correggi questo ordine.",
"firstHalf": "Girone d’andata",
"firstHalfFull": "Girone d’andata (luglio - dicembre)",
"secondHalf": "Girone di ritorno",
"secondHalfFull": "Girone di ritorno (dal 1° gennaio)"
+ },
+ "pdfGenerator": {
+ "teamLineupTitle": "Formazione della squadra",
+ "clubLabel": "Società:",
+ "teamNameLabel": "Squadra:",
+ "leagueLabel": "Campionato:",
+ "seasonLabel": "Stagione:",
+ "periodLabel": "Iscrizione per:",
+ "teamGenderLabel": "Genere squadra:",
+ "teamAgeGroupLabel": "Categoria età squadra:",
+ "generatedAt": "Creato:",
+ "lineupQttr": "(Q)TTR"
}
}
diff --git a/frontend/src/i18n/locales/ja.json b/frontend/src/i18n/locales/ja.json
index cf74c1b9..0121ddff 100644
--- a/frontend/src/i18n/locales/ja.json
+++ b/frontend/src/i18n/locales/ja.json
@@ -871,11 +871,26 @@
"selectedLineup": "登録済み選手",
"availableLineupMembers": "利用可能な選手",
"lineupEmpty": "このチームにはまだ選手が登録されていません。",
+ "exportLineupPdf": "メンバー表をPDFで出力",
+ "lineupPdfEmpty": "登録プレイヤーがいないためPDFを作成できません。",
+ "lineupPdfFilePrefix": "メンバー表",
"lineupSaveError": "チーム登録を保存できませんでした。",
"lineupValidationTooLargeGap": "{higher} は {lower} より 30 QTTR ポイント以上高いです。この順序を修正してください。",
"firstHalf": "前期",
"firstHalfFull": "前期(7月 - 12月)",
"secondHalf": "後期",
"secondHalfFull": "後期(1月1日以降)"
+ },
+ "pdfGenerator": {
+ "teamLineupTitle": "チーム登録メンバー",
+ "clubLabel": "クラブ:",
+ "teamNameLabel": "チーム:",
+ "leagueLabel": "リーグ:",
+ "seasonLabel": "シーズン:",
+ "periodLabel": "登録対象:",
+ "teamGenderLabel": "チーム性別:",
+ "teamAgeGroupLabel": "チーム年齢区分:",
+ "generatedAt": "作成日時:",
+ "lineupQttr": "(Q)TTR"
}
}
diff --git a/frontend/src/i18n/locales/pl.json b/frontend/src/i18n/locales/pl.json
index 6df5fc65..a5ff0e63 100644
--- a/frontend/src/i18n/locales/pl.json
+++ b/frontend/src/i18n/locales/pl.json
@@ -871,11 +871,26 @@
"selectedLineup": "Zgłoszeni zawodnicy",
"availableLineupMembers": "Dostępni zawodnicy",
"lineupEmpty": "Do tej drużyny nie zgłoszono jeszcze żadnych zawodników.",
+ "exportLineupPdf": "Eksport składu do PDF",
+ "lineupPdfEmpty": "Brak zawodników w składzie – nie można utworzyć PDF.",
+ "lineupPdfFilePrefix": "Sklad",
"lineupSaveError": "Nie udało się zapisać zgłoszenia drużyny.",
"lineupValidationTooLargeGap": "{higher} ma ponad 30 punktów QTTR przewagi nad {lower}. Popraw tę kolejność.",
"firstHalf": "Pierwsza runda",
"firstHalfFull": "Pierwsza runda (lipiec - grudzień)",
"secondHalf": "Druga runda",
"secondHalfFull": "Druga runda (od 1 stycznia)"
+ },
+ "pdfGenerator": {
+ "teamLineupTitle": "Skład drużyny",
+ "clubLabel": "Klub:",
+ "teamNameLabel": "Drużyna:",
+ "leagueLabel": "Liga:",
+ "seasonLabel": "Sezon:",
+ "periodLabel": "Zgłoszenie na:",
+ "teamGenderLabel": "Płeć drużyny:",
+ "teamAgeGroupLabel": "Kategoria wiekowa drużyny:",
+ "generatedAt": "Utworzono:",
+ "lineupQttr": "(Q)TTR"
}
}
diff --git a/frontend/src/i18n/locales/th.json b/frontend/src/i18n/locales/th.json
index 3e4daf31..0bd44835 100644
--- a/frontend/src/i18n/locales/th.json
+++ b/frontend/src/i18n/locales/th.json
@@ -871,11 +871,26 @@
"selectedLineup": "ผู้เล่นที่ขึ้นทะเบียน",
"availableLineupMembers": "ผู้เล่นที่พร้อมใช้งาน",
"lineupEmpty": "ยังไม่มีการกำหนดผู้เล่นให้ทีมนี้",
+ "exportLineupPdf": "ส่งออกรายชื่อเป็น PDF",
+ "lineupPdfEmpty": "ไม่มีผู้เล่นในรายชื่อ – ไม่สามารถสร้าง PDF ได้",
+ "lineupPdfFilePrefix": "รายชื่อทีม",
"lineupSaveError": "ไม่สามารถบันทึกรายชื่อทีมได้",
"lineupValidationTooLargeGap": "{higher} มีคะแนน QTTR มากกว่า {lower} เกิน 30 คะแนน กรุณาแก้ไขลำดับนี้",
"firstHalf": "ครึ่งแรก",
"firstHalfFull": "ครึ่งแรก (กรกฎาคม - ธันวาคม)",
"secondHalf": "ครึ่งหลัง",
"secondHalfFull": "ครึ่งหลัง (ตั้งแต่ 1 มกราคม)"
+ },
+ "pdfGenerator": {
+ "teamLineupTitle": "รายชื่อทีม",
+ "clubLabel": "สโมสร:",
+ "teamNameLabel": "ทีม:",
+ "leagueLabel": "ลีก:",
+ "seasonLabel": "ฤดูกาล:",
+ "periodLabel": "ลงทะเบียนสำหรับ:",
+ "teamGenderLabel": "เพศทีม:",
+ "teamAgeGroupLabel": "กลุ่มอายุของทีม:",
+ "generatedAt": "สร้างเมื่อ:",
+ "lineupQttr": "(Q)TTR"
}
}
diff --git a/frontend/src/i18n/locales/tl.json b/frontend/src/i18n/locales/tl.json
index 9aac0b67..d3a07216 100644
--- a/frontend/src/i18n/locales/tl.json
+++ b/frontend/src/i18n/locales/tl.json
@@ -871,11 +871,26 @@
"selectedLineup": "Napiling manlalaro",
"availableLineupMembers": "Magagamit na manlalaro",
"lineupEmpty": "Wala pang manlalarong itinalaga sa koponang ito.",
+ "exportLineupPdf": "I-export ang line-up bilang PDF",
+ "lineupPdfEmpty": "Walang manlalaro sa line-up – hindi makagawa ng PDF.",
+ "lineupPdfFilePrefix": "Line-up",
"lineupSaveError": "Hindi mai-save ang line-up ng koponan.",
"lineupValidationTooLargeGap": "Mahigit 30 QTTR points ang lamang ni {higher} kay {lower}. Pakitama ang ayos na ito.",
"firstHalf": "Unang yugto",
"firstHalfFull": "Unang yugto (Hulyo - Disyembre)",
"secondHalf": "Ikalawang yugto",
"secondHalfFull": "Ikalawang yugto (mula Enero 1)"
+ },
+ "pdfGenerator": {
+ "teamLineupTitle": "Line-up ng koponan",
+ "clubLabel": "Club:",
+ "teamNameLabel": "Koponan:",
+ "leagueLabel": "Liga:",
+ "seasonLabel": "Season:",
+ "periodLabel": "Registration para sa:",
+ "teamGenderLabel": "Kasarian ng koponan:",
+ "teamAgeGroupLabel": "Age group ng koponan:",
+ "generatedAt": "Nilikha:",
+ "lineupQttr": "(Q)TTR"
}
}
diff --git a/frontend/src/i18n/locales/zh.json b/frontend/src/i18n/locales/zh.json
index 8c424586..7d47ca4d 100644
--- a/frontend/src/i18n/locales/zh.json
+++ b/frontend/src/i18n/locales/zh.json
@@ -871,11 +871,26 @@
"selectedLineup": "已报名球员",
"availableLineupMembers": "可用球员",
"lineupEmpty": "这支队伍还没有报名任何球员。",
+ "exportLineupPdf": "导出阵容为 PDF",
+ "lineupPdfEmpty": "阵容中没有球员,无法生成 PDF。",
+ "lineupPdfFilePrefix": "阵容",
"lineupSaveError": "无法保存队伍报名。",
"lineupValidationTooLargeGap": "{higher} 比 {lower} 高出超过 30 个 QTTR 积分。请更正这个顺序。",
"firstHalf": "上半程",
"firstHalfFull": "上半程(7月 - 12月)",
"secondHalf": "下半程",
"secondHalfFull": "下半程(1月1日起)"
+ },
+ "pdfGenerator": {
+ "teamLineupTitle": "球队阵容",
+ "clubLabel": "俱乐部:",
+ "teamNameLabel": "球队:",
+ "leagueLabel": "联赛:",
+ "seasonLabel": "赛季:",
+ "periodLabel": "报名阶段:",
+ "teamGenderLabel": "球队性别:",
+ "teamAgeGroupLabel": "球队年龄组:",
+ "generatedAt": "生成时间:",
+ "lineupQttr": "(Q)TTR"
}
}
diff --git a/frontend/src/views/TeamManagementView.vue b/frontend/src/views/TeamManagementView.vue
index 39785156..e4a76616 100644
--- a/frontend/src/views/TeamManagementView.vue
+++ b/frontend/src/views/TeamManagementView.vue
@@ -233,6 +233,9 @@
+
@@ -531,6 +534,7 @@ import ConfirmDialog from '../components/ConfirmDialog.vue';
import TeamListCard from '../components/team/TeamListCard.vue';
import TeamManagementOverview from '../components/team/TeamManagementOverview.vue';
import { buildInfoConfig, buildConfirmConfig } from '../utils/dialogUtils.js';
+import PDFGenerator from '../components/PDFGenerator.js';
export default {
name: 'TeamManagementView',
@@ -1800,6 +1804,56 @@ export default {
}
};
+ const downloadLineupPdf = () => {
+ const members = selectedTeamLineupMembers.value;
+ if (!members.length) {
+ showInfo(t('messages.info'), t('teamManagement.lineupPdfEmpty'), '', 'info');
+ return;
+ }
+ const team = teamToEdit.value;
+ const clubName = store.getters.currentClubName || '';
+ const seasonLabel = team?.season?.season || currentSeason.value?.season || t('teamManagement.seasonUnknown');
+ const leagueName = team?.league?.name || t('teamManagement.noLeague');
+ const halfLabel = lineupHalfOptions.value.find((o) => o.value === selectedLineupHalf.value)?.label || '';
+ const introLines = [
+ `${t('pdfGenerator.clubLabel')} ${clubName}`,
+ `${t('pdfGenerator.teamNameLabel')} ${team?.name || ''}`,
+ `${t('pdfGenerator.leagueLabel')} ${leagueName}`,
+ `${t('pdfGenerator.seasonLabel')} ${seasonLabel}`,
+ `${t('pdfGenerator.periodLabel')} ${halfLabel}`,
+ `${t('pdfGenerator.teamGenderLabel')} ${labelTeamGender(effectiveTeamGender.value)}`,
+ `${t('pdfGenerator.teamAgeGroupLabel')} ${labelAgeGroup(effectiveTeamAgeGroup.value)}`,
+ `${t('pdfGenerator.generatedAt')} ${new Date().toLocaleString()}`
+ ];
+ const tableHead = [[
+ '#',
+ t('teamManagement.player'),
+ t('members.ageGroup'),
+ t('pdfGenerator.lineupQttr'),
+ t('teamManagement.eligibility')
+ ]];
+ const tableBody = members.map((member, index) => [
+ String(index + 1),
+ `${member.firstName || ''} ${member.lastName || ''}`.trim(),
+ String(member.memberAgeGroupLabel || ''),
+ String(member.lineupRatingLabel || ''),
+ String(member.eligibilityLabel || '')
+ ]);
+ const pdfGenerator = new PDFGenerator(20, 10, t);
+ pdfGenerator.addTeamLineup({
+ title: t('pdfGenerator.teamLineupTitle'),
+ introLines,
+ tableHead,
+ tableBody
+ });
+ const safe = (s) => String(s || '')
+ .replace(/[<>:"/\\|?*\u0000-\u001f]/g, '_')
+ .replace(/\s+/g, '_')
+ .slice(0, 80);
+ const filename = `${t('teamManagement.lineupPdfFilePrefix')}_${safe(team?.name)}_${new Date().toISOString().slice(0, 10)}.pdf`;
+ pdfGenerator.save(filename);
+ };
+
const persistTeamLineupAssignments = async (nextAssignments, validationMessage = '') => {
if (!teamToEdit.value?.id || savingTeamLineup.value) {
return false;
@@ -2220,6 +2274,7 @@ export default {
refreshPlayerStats,
loadClubMembers,
loadTeamLineup,
+ downloadLineupPdf,
addMemberToLineup,
removeMemberFromLineup,
moveLineupMember,