From 9454761e349f36196bc9370154291a4eead7b746 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Thu, 2 Apr 2026 08:07:38 +0200 Subject: [PATCH] feat(PDFGenerator): add team lineup PDF generation and localization updates - Implemented a new feature to generate a PDF for team lineups, including detailed information such as club name, team name, league, season, gender, and age group. - Enhanced localization files across multiple languages to include new terms related to the PDF generation feature, ensuring a consistent user experience. - Updated the TeamManagementView to include a button for downloading the lineup as a PDF, improving accessibility for users. --- ...eam_gender_team_age_group_to_club_team.sql | 10 ++++ frontend/src/components/PDFGenerator.js | 49 ++++++++++++++++- frontend/src/i18n/locales/de-CH.json | 15 +++++ frontend/src/i18n/locales/de-extended.json | 15 +++++ frontend/src/i18n/locales/de.json | 15 ++++- frontend/src/i18n/locales/en-AU.json | 15 +++++ frontend/src/i18n/locales/en-GB.json | 15 +++++ frontend/src/i18n/locales/en-US.json | 15 +++++ frontend/src/i18n/locales/es.json | 15 +++++ frontend/src/i18n/locales/fil.json | 15 +++++ frontend/src/i18n/locales/fr.json | 15 +++++ frontend/src/i18n/locales/it.json | 15 +++++ frontend/src/i18n/locales/ja.json | 15 +++++ frontend/src/i18n/locales/pl.json | 15 +++++ frontend/src/i18n/locales/th.json | 15 +++++ frontend/src/i18n/locales/tl.json | 15 +++++ frontend/src/i18n/locales/zh.json | 15 +++++ frontend/src/views/TeamManagementView.vue | 55 +++++++++++++++++++ 18 files changed, 337 insertions(+), 2 deletions(-) create mode 100644 backend/migrations/20260402_add_team_gender_team_age_group_to_club_team.sql 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,