feat(ClubTeam): enhance club team management with lineup features and member eligibility
- Added teamGender and teamAgeGroup fields to ClubTeam model for better categorization. - Updated create and update club team endpoints to handle new fields and default values. - Implemented getClubTeamLineup and updateClubTeamLineup functions for managing team lineups. - Enhanced member management with adultReleaseApproved and adultReserveApproved fields in Member model. - Updated frontend views to support new lineup features and member eligibility flags. - Improved localization for new terms related to team management and member eligibility across multiple languages.
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
"title": "Trainingstagebuch"
|
||||
},
|
||||
"common": {
|
||||
"period": "Zeitraum",
|
||||
"loading": "Lade...",
|
||||
"save": "Speichere",
|
||||
"saved": "Gespeichert",
|
||||
@@ -28,6 +29,7 @@
|
||||
"time": "Zyt",
|
||||
"new": "Neu",
|
||||
"update": "Aktualisiere",
|
||||
"move": "Verschiebe",
|
||||
"refresh": "Neu lade",
|
||||
"create": "Erstelle",
|
||||
"remove": "Entferne",
|
||||
@@ -449,6 +451,8 @@
|
||||
"picsInInternetAllowed": "Pics in Internet erlaubt",
|
||||
"testMembership": "Testmitgliedschaft",
|
||||
"memberFormHandedOver": "Mitgliedsformular ausgehändigt",
|
||||
"adultReleaseApproved": "Freigabe Erwachsene",
|
||||
"adultReserveApproved": "Ersatz bei Erwachsenen",
|
||||
"trainingGroups": "Trainingsgruppen",
|
||||
"noGroupsAssigned": "Keine Gruppen zugeordnet",
|
||||
"noGroupsAvailable": "Keine Gruppen verfügbar",
|
||||
@@ -463,7 +467,7 @@
|
||||
"clearFields": "Felder leeren",
|
||||
"showInactiveMembers": "Inaktive Mitglieder anzeigen",
|
||||
"ageGroup": "Altersklasse",
|
||||
"adults": "Erwachsene (20+)",
|
||||
"adults": "Erwachsene (18+)",
|
||||
"j19": "J19 (19 und jünger)",
|
||||
"j17": "J17 (17 und jünger)",
|
||||
"j15": "J15 (15 und jünger)",
|
||||
@@ -868,5 +872,22 @@
|
||||
"targetMiddleShort": "Mitte kurz",
|
||||
"targetBackhandShort": "Rückhand kurz",
|
||||
"toTarget": "nach"
|
||||
},
|
||||
"teamManagement": {
|
||||
"lineupProposal": "Mannschaftsmeldung nach QTTR",
|
||||
"eligibility": "Einsatz",
|
||||
"eligibilityRegular": "Regulär",
|
||||
"eligibilityAdultRelease": "Freigabe Erwachsene",
|
||||
"eligibilityAdultReserve": "Ersatz Erwachsene",
|
||||
"eligibilityAdultReleaseAndReserve": "Freigabe + Ersatz Erwachsene",
|
||||
"selectedLineup": "Gemeldete Spieler",
|
||||
"availableLineupMembers": "Verfügbare Spieler",
|
||||
"lineupEmpty": "Noch keine Spieler für diese Mannschaft gemeldet.",
|
||||
"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)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,8 @@
|
||||
"days": "Tage",
|
||||
"weeks": "Wochen",
|
||||
"months": "Monate",
|
||||
"years": "Jahre"
|
||||
"years": "Jahre",
|
||||
"period": "Zeitraum"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Startseite",
|
||||
@@ -203,6 +204,8 @@
|
||||
"picsInInternetAllowed": "Pics in Internet erlaubt",
|
||||
"testMembership": "Testmitgliedschaft",
|
||||
"memberFormHandedOver": "Mitgliedsformular ausgehändigt",
|
||||
"adultReleaseApproved": "Freigabe Erwachsene",
|
||||
"adultReserveApproved": "Ersatz bei Erwachsenen",
|
||||
"trainingGroups": "Trainingsgruppen",
|
||||
"subtitle": "Mitglieder suchen, filtern und direkt bearbeiten.",
|
||||
"closeEditor": "Editor schließen",
|
||||
@@ -574,5 +577,22 @@
|
||||
"cookies": "Cookies/Local Storage",
|
||||
"logData": "Logdaten",
|
||||
"recipients": "Empfänger"
|
||||
},
|
||||
"teamManagement": {
|
||||
"lineupProposal": "Mannschaftsmeldung nach QTTR",
|
||||
"eligibility": "Einsatz",
|
||||
"eligibilityRegular": "Regulär",
|
||||
"eligibilityAdultRelease": "Freigabe Erwachsene",
|
||||
"eligibilityAdultReserve": "Ersatz Erwachsene",
|
||||
"eligibilityAdultReleaseAndReserve": "Freigabe + Ersatz Erwachsene",
|
||||
"selectedLineup": "Gemeldete Spieler",
|
||||
"availableLineupMembers": "Verfügbare Spieler",
|
||||
"lineupEmpty": "Noch keine Spieler für diese Mannschaft gemeldet.",
|
||||
"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)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"time": "Zeit",
|
||||
"new": "Neu",
|
||||
"update": "Aktualisieren",
|
||||
"move": "Verschieben",
|
||||
"refresh": "Neu laden",
|
||||
"create": "Erstellen",
|
||||
"remove": "Entfernen",
|
||||
@@ -57,7 +58,8 @@
|
||||
"weeks": "Wochen",
|
||||
"months": "Monate",
|
||||
"years": "Jahre",
|
||||
"ok": "OK"
|
||||
"ok": "OK",
|
||||
"period": "Zeitraum"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Startseite",
|
||||
@@ -224,6 +226,8 @@
|
||||
"picsInInternetAllowed": "Pics in Internet erlaubt",
|
||||
"testMembership": "Testmitgliedschaft",
|
||||
"memberFormHandedOver": "Mitgliedsformular ausgehändigt",
|
||||
"adultReleaseApproved": "Freigabe Erwachsene",
|
||||
"adultReserveApproved": "Ersatz bei Erwachsenen",
|
||||
"trainingGroups": "Trainingsgruppen",
|
||||
"noGroupsAssigned": "Keine Gruppen zugeordnet",
|
||||
"noGroupsAvailable": "Keine Gruppen verfügbar",
|
||||
@@ -238,7 +242,7 @@
|
||||
"clearFields": "Felder leeren",
|
||||
"showInactiveMembers": "Inaktive Mitglieder anzeigen",
|
||||
"ageGroup": "Altersklasse",
|
||||
"adults": "Erwachsene (20+)",
|
||||
"adults": "Erwachsene (18+)",
|
||||
"j19": "J19 (19 und jünger)",
|
||||
"j17": "J17 (17 und jünger)",
|
||||
"j15": "J15 (15 und jünger)",
|
||||
@@ -1323,6 +1327,22 @@
|
||||
"playerStats": "Spieleinsätze",
|
||||
"playerStatsIntro": "Schneller Überblick über Einsätze der Mannschaft in dieser Saison.",
|
||||
"lineupProposal": "Mannschaftsmeldung nach QTTR",
|
||||
"teamGender": "Geschlecht",
|
||||
"teamAgeGroup": "Altersklasse",
|
||||
"teamGenderOpen": "Offen",
|
||||
"teamGenderFemale": "Weiblich",
|
||||
"eligibility": "Einsatz",
|
||||
"eligibilityRegular": "Regulär",
|
||||
"eligibilityAdultRelease": "Freigabe Erwachsene",
|
||||
"eligibilityAdultReserve": "Ersatz Erwachsene",
|
||||
"eligibilityAdultReleaseAndReserve": "Freigabe + Ersatz Erwachsene",
|
||||
"selectedLineup": "Gemeldete Spieler",
|
||||
"availableLineupMembers": "Verfügbare Spieler",
|
||||
"lineupEmpty": "Noch keine Spieler für diese Mannschaft gemeldet.",
|
||||
"lineupUnsavedChanges": "Ungespeicherte Änderungen in der Mannschaftsmeldung.",
|
||||
"lineupSaved": "Mannschaftsmeldung gespeichert.",
|
||||
"lineupSaveError": "Mannschaftsmeldung konnte nicht gespeichert werden.",
|
||||
"lineupValidationTooLargeGap": "{higher} hat mehr als 30 QTTR Punkte Vorsprung vor {lower}. Diese Reihenfolge bitte korrigieren.",
|
||||
"refreshStats": "Aktualisieren",
|
||||
"loadingStats": "Lade Statistiken...",
|
||||
"noPlayerStats": "Keine Spieleinsätze erfasst.",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"title": "Training Diary"
|
||||
},
|
||||
"common": {
|
||||
"period": "Period",
|
||||
"loading": "Loading...",
|
||||
"save": "Save",
|
||||
"saved": "Saved",
|
||||
@@ -29,6 +30,7 @@
|
||||
"time": "Time",
|
||||
"new": "New",
|
||||
"update": "Update",
|
||||
"move": "Move",
|
||||
"refresh": "Reload",
|
||||
"create": "Create",
|
||||
"remove": "Remove",
|
||||
@@ -449,6 +451,8 @@
|
||||
"picsInInternetAllowed": "Pictures allowed online",
|
||||
"testMembership": "Trial membership",
|
||||
"memberFormHandedOver": "Membership form handed over",
|
||||
"adultReleaseApproved": "Approved for adults",
|
||||
"adultReserveApproved": "Adult reserve",
|
||||
"trainingGroups": "Training groups",
|
||||
"noGroupsAssigned": "No groups assigned",
|
||||
"noGroupsAvailable": "No groups available",
|
||||
@@ -463,7 +467,7 @@
|
||||
"clearFields": "Clear fields",
|
||||
"showInactiveMembers": "Show inactive members",
|
||||
"ageGroup": "Age group",
|
||||
"adults": "Adults (20+)",
|
||||
"adults": "Adults (18+)",
|
||||
"j19": "U19 (19 and younger)",
|
||||
"j17": "U17 (17 and younger)",
|
||||
"j15": "U15 (15 and younger)",
|
||||
@@ -868,5 +872,22 @@
|
||||
"targetMiddleShort": "Middle short",
|
||||
"targetBackhandShort": "Backhand short",
|
||||
"toTarget": "to"
|
||||
},
|
||||
"teamManagement": {
|
||||
"lineupProposal": "Line-up by QTTR",
|
||||
"eligibility": "Eligibility",
|
||||
"eligibilityRegular": "Regular",
|
||||
"eligibilityAdultRelease": "Approved for adults",
|
||||
"eligibilityAdultReserve": "Adult reserve",
|
||||
"eligibilityAdultReleaseAndReserve": "Approved + adult reserve",
|
||||
"selectedLineup": "Selected players",
|
||||
"availableLineupMembers": "Available players",
|
||||
"lineupEmpty": "No players have been assigned to this team yet.",
|
||||
"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)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"title": "Training Diary"
|
||||
},
|
||||
"common": {
|
||||
"period": "Period",
|
||||
"loading": "Loading...",
|
||||
"save": "Save",
|
||||
"saved": "Saved",
|
||||
@@ -29,6 +30,7 @@
|
||||
"time": "Time",
|
||||
"new": "New",
|
||||
"update": "Update",
|
||||
"move": "Move",
|
||||
"refresh": "Reload",
|
||||
"create": "Create",
|
||||
"remove": "Remove",
|
||||
@@ -724,6 +726,8 @@
|
||||
"picsInInternetAllowed": "Pictures allowed online",
|
||||
"testMembership": "Trial membership",
|
||||
"memberFormHandedOver": "Membership form handed over",
|
||||
"adultReleaseApproved": "Approved for adults",
|
||||
"adultReserveApproved": "Adult reserve",
|
||||
"trainingGroups": "Training groups",
|
||||
"noGroupsAssigned": "No groups assigned",
|
||||
"noGroupsAvailable": "No groups available",
|
||||
@@ -738,7 +742,7 @@
|
||||
"clearFields": "Clear fields",
|
||||
"showInactiveMembers": "Show inactive members",
|
||||
"ageGroup": "Age group",
|
||||
"adults": "Adults (20+)",
|
||||
"adults": "Adults (18+)",
|
||||
"j19": "U19 (19 and younger)",
|
||||
"j17": "U17 (17 and younger)",
|
||||
"j15": "U15 (15 and younger)",
|
||||
@@ -1014,6 +1018,22 @@
|
||||
"playerStats": "Appearances",
|
||||
"playerStatsIntro": "Quick overview of this team's appearances in the current season.",
|
||||
"lineupProposal": "Line-up by QTTR",
|
||||
"teamGender": "Gender",
|
||||
"teamAgeGroup": "Age group",
|
||||
"teamGenderOpen": "Open",
|
||||
"teamGenderFemale": "Female",
|
||||
"eligibility": "Eligibility",
|
||||
"eligibilityRegular": "Regular",
|
||||
"eligibilityAdultRelease": "Approved for adults",
|
||||
"eligibilityAdultReserve": "Adult reserve",
|
||||
"eligibilityAdultReleaseAndReserve": "Approved + adult reserve",
|
||||
"selectedLineup": "Selected players",
|
||||
"availableLineupMembers": "Available players",
|
||||
"lineupEmpty": "No players have been assigned to this team yet.",
|
||||
"lineupUnsavedChanges": "There are unsaved changes in the team line-up.",
|
||||
"lineupSaved": "Team line-up saved.",
|
||||
"lineupSaveError": "Team line-up could not be saved.",
|
||||
"lineupValidationTooLargeGap": "{higher} is more than 30 QTTR points ahead of {lower}. Please correct this order.",
|
||||
"refreshStats": "Refresh",
|
||||
"loadingStats": "Loading statistics...",
|
||||
"noPlayerStats": "No appearances recorded.",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"title": "Training Diary"
|
||||
},
|
||||
"common": {
|
||||
"period": "Period",
|
||||
"loading": "Loading...",
|
||||
"save": "Save",
|
||||
"saved": "Saved",
|
||||
@@ -29,6 +30,7 @@
|
||||
"time": "Time",
|
||||
"new": "New",
|
||||
"update": "Update",
|
||||
"move": "Move",
|
||||
"refresh": "Reload",
|
||||
"create": "Create",
|
||||
"remove": "Remove",
|
||||
@@ -449,6 +451,8 @@
|
||||
"picsInInternetAllowed": "Pictures allowed online",
|
||||
"testMembership": "Trial membership",
|
||||
"memberFormHandedOver": "Membership form handed over",
|
||||
"adultReleaseApproved": "Approved for adults",
|
||||
"adultReserveApproved": "Adult reserve",
|
||||
"trainingGroups": "Training groups",
|
||||
"noGroupsAssigned": "No groups assigned",
|
||||
"noGroupsAvailable": "No groups available",
|
||||
@@ -463,7 +467,7 @@
|
||||
"clearFields": "Clear fields",
|
||||
"showInactiveMembers": "Show inactive members",
|
||||
"ageGroup": "Age group",
|
||||
"adults": "Adults (20+)",
|
||||
"adults": "Adults (18+)",
|
||||
"j19": "U19 (19 and younger)",
|
||||
"j17": "U17 (17 and younger)",
|
||||
"j15": "U15 (15 and younger)",
|
||||
@@ -868,5 +872,22 @@
|
||||
"targetMiddleShort": "Middle short",
|
||||
"targetBackhandShort": "Backhand short",
|
||||
"toTarget": "to"
|
||||
},
|
||||
"teamManagement": {
|
||||
"lineupProposal": "Line-up by QTTR",
|
||||
"eligibility": "Eligibility",
|
||||
"eligibilityRegular": "Regular",
|
||||
"eligibilityAdultRelease": "Approved for adults",
|
||||
"eligibilityAdultReserve": "Adult reserve",
|
||||
"eligibilityAdultReleaseAndReserve": "Approved + adult reserve",
|
||||
"selectedLineup": "Selected players",
|
||||
"availableLineupMembers": "Available players",
|
||||
"lineupEmpty": "No players have been assigned to this team yet.",
|
||||
"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)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,8 @@
|
||||
"weeks": "semanas",
|
||||
"months": "meses",
|
||||
"years": "años",
|
||||
"ok": "OK"
|
||||
"ok": "OK",
|
||||
"period": "Periodo"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Inicio",
|
||||
@@ -838,5 +839,22 @@
|
||||
"targetMiddleShort": "Centro corto",
|
||||
"targetBackhandShort": "Revés corto",
|
||||
"toTarget": "a"
|
||||
},
|
||||
"teamManagement": {
|
||||
"lineupProposal": "Alineación por QTTR",
|
||||
"eligibility": "Elegibilidad",
|
||||
"eligibilityRegular": "Regular",
|
||||
"eligibilityAdultRelease": "Autorizado para adultos",
|
||||
"eligibilityAdultReserve": "Reserva en adultos",
|
||||
"eligibilityAdultReleaseAndReserve": "Autorizado + reserva en adultos",
|
||||
"selectedLineup": "Jugadores inscritos",
|
||||
"availableLineupMembers": "Jugadores disponibles",
|
||||
"lineupEmpty": "Todavía no se ha inscrito ningún jugador para este equipo.",
|
||||
"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)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,8 @@
|
||||
"weeks": "Linggo",
|
||||
"months": "Buwan",
|
||||
"years": "Taon",
|
||||
"ok": "OK"
|
||||
"ok": "OK",
|
||||
"period": "Panahon"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Home",
|
||||
@@ -838,5 +839,22 @@
|
||||
"targetMiddleShort": "middle short",
|
||||
"targetBackhandShort": "backhand short",
|
||||
"toTarget": "papunta sa"
|
||||
},
|
||||
"teamManagement": {
|
||||
"lineupProposal": "Line-up ayon sa QTTR",
|
||||
"eligibility": "Pagiging karapat-dapat",
|
||||
"eligibilityRegular": "Karaniwan",
|
||||
"eligibilityAdultRelease": "Pinayagan sa adults",
|
||||
"eligibilityAdultReserve": "Reserve sa adults",
|
||||
"eligibilityAdultReleaseAndReserve": "Pinayagan + reserve sa adults",
|
||||
"selectedLineup": "Napiling manlalaro",
|
||||
"availableLineupMembers": "Magagamit na manlalaro",
|
||||
"lineupEmpty": "Wala pang manlalarong itinalaga sa koponang ito.",
|
||||
"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)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,8 @@
|
||||
"weeks": "semaines",
|
||||
"months": "mois",
|
||||
"years": "années",
|
||||
"ok": "OK"
|
||||
"ok": "OK",
|
||||
"period": "Période"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Accueil",
|
||||
@@ -838,5 +839,22 @@
|
||||
"targetMiddleShort": "Milieu court",
|
||||
"targetBackhandShort": "Revers court",
|
||||
"toTarget": "vers"
|
||||
},
|
||||
"teamManagement": {
|
||||
"lineupProposal": "Composition selon le QTTR",
|
||||
"eligibility": "Éligibilité",
|
||||
"eligibilityRegular": "Régulier",
|
||||
"eligibilityAdultRelease": "Autorisé chez les adultes",
|
||||
"eligibilityAdultReserve": "Remplaçant chez les adultes",
|
||||
"eligibilityAdultReleaseAndReserve": "Autorisé + remplaçant chez les adultes",
|
||||
"selectedLineup": "Joueurs inscrits",
|
||||
"availableLineupMembers": "Joueurs disponibles",
|
||||
"lineupEmpty": "Aucun joueur n’a encore été inscrit pour cette équipe.",
|
||||
"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)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,8 @@
|
||||
"weeks": "settimane",
|
||||
"months": "mesi",
|
||||
"years": "anni",
|
||||
"ok": "OK"
|
||||
"ok": "OK",
|
||||
"period": "Periodo"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Home",
|
||||
@@ -838,5 +839,22 @@
|
||||
"targetMiddleShort": "Centro corto",
|
||||
"targetBackhandShort": "Rovescio corto",
|
||||
"toTarget": "verso"
|
||||
},
|
||||
"teamManagement": {
|
||||
"lineupProposal": "Formazione in base al QTTR",
|
||||
"eligibility": "Idoneità",
|
||||
"eligibilityRegular": "Regolare",
|
||||
"eligibilityAdultRelease": "Autorizzato per adulti",
|
||||
"eligibilityAdultReserve": "Riserva negli adulti",
|
||||
"eligibilityAdultReleaseAndReserve": "Autorizzato + riserva negli adulti",
|
||||
"selectedLineup": "Giocatori schierati",
|
||||
"availableLineupMembers": "Giocatori disponibili",
|
||||
"lineupEmpty": "Nessun giocatore è ancora stato assegnato a questa squadra.",
|
||||
"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)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,8 @@
|
||||
"weeks": "週間",
|
||||
"months": "か月",
|
||||
"years": "年",
|
||||
"ok": "OK"
|
||||
"ok": "OK",
|
||||
"period": "期間"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "ホーム",
|
||||
@@ -838,5 +839,22 @@
|
||||
"targetMiddleShort": "ミドル短",
|
||||
"targetBackhandShort": "バック短",
|
||||
"toTarget": "へ"
|
||||
},
|
||||
"teamManagement": {
|
||||
"lineupProposal": "QTTRによるチーム登録",
|
||||
"eligibility": "出場資格",
|
||||
"eligibilityRegular": "通常",
|
||||
"eligibilityAdultRelease": "一般出場許可",
|
||||
"eligibilityAdultReserve": "一般の補欠",
|
||||
"eligibilityAdultReleaseAndReserve": "一般出場許可 + 補欠",
|
||||
"selectedLineup": "登録済み選手",
|
||||
"availableLineupMembers": "利用可能な選手",
|
||||
"lineupEmpty": "このチームにはまだ選手が登録されていません。",
|
||||
"lineupSaveError": "チーム登録を保存できませんでした。",
|
||||
"lineupValidationTooLargeGap": "{higher} は {lower} より 30 QTTR ポイント以上高いです。この順序を修正してください。",
|
||||
"firstHalf": "前期",
|
||||
"firstHalfFull": "前期(7月 - 12月)",
|
||||
"secondHalf": "後期",
|
||||
"secondHalfFull": "後期(1月1日以降)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,8 @@
|
||||
"weeks": "tygodnie",
|
||||
"months": "miesiące",
|
||||
"years": "lata",
|
||||
"ok": "OK"
|
||||
"ok": "OK",
|
||||
"period": "Okres"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Strona główna",
|
||||
@@ -838,5 +839,22 @@
|
||||
"targetMiddleShort": "Środek krótki",
|
||||
"targetBackhandShort": "Backhand krótki",
|
||||
"toTarget": "do"
|
||||
},
|
||||
"teamManagement": {
|
||||
"lineupProposal": "Zgłoszenie według QTTR",
|
||||
"eligibility": "Uprawnienie",
|
||||
"eligibilityRegular": "Standardowe",
|
||||
"eligibilityAdultRelease": "Dopuszczony do dorosłych",
|
||||
"eligibilityAdultReserve": "Rezerwowy u dorosłych",
|
||||
"eligibilityAdultReleaseAndReserve": "Dopuszczony + rezerwowy u dorosłych",
|
||||
"selectedLineup": "Zgłoszeni zawodnicy",
|
||||
"availableLineupMembers": "Dostępni zawodnicy",
|
||||
"lineupEmpty": "Do tej drużyny nie zgłoszono jeszcze żadnych zawodników.",
|
||||
"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)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,8 @@
|
||||
"weeks": "สัปดาห์",
|
||||
"months": "เดือน",
|
||||
"years": "ปี",
|
||||
"ok": "ตกลง"
|
||||
"ok": "ตกลง",
|
||||
"period": "ช่วงเวลา"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "หน้าแรก",
|
||||
@@ -838,5 +839,22 @@
|
||||
"targetMiddleShort": "กลางสั้น",
|
||||
"targetBackhandShort": "แบ็กแฮนด์สั้น",
|
||||
"toTarget": "ไปยัง"
|
||||
},
|
||||
"teamManagement": {
|
||||
"lineupProposal": "จัดทีมตาม QTTR",
|
||||
"eligibility": "สิทธิ์ลงเล่น",
|
||||
"eligibilityRegular": "ปกติ",
|
||||
"eligibilityAdultRelease": "อนุมัติให้เล่นผู้ใหญ่",
|
||||
"eligibilityAdultReserve": "ตัวสำรองผู้ใหญ่",
|
||||
"eligibilityAdultReleaseAndReserve": "อนุมัติ + ตัวสำรองผู้ใหญ่",
|
||||
"selectedLineup": "ผู้เล่นที่ขึ้นทะเบียน",
|
||||
"availableLineupMembers": "ผู้เล่นที่พร้อมใช้งาน",
|
||||
"lineupEmpty": "ยังไม่มีการกำหนดผู้เล่นให้ทีมนี้",
|
||||
"lineupSaveError": "ไม่สามารถบันทึกรายชื่อทีมได้",
|
||||
"lineupValidationTooLargeGap": "{higher} มีคะแนน QTTR มากกว่า {lower} เกิน 30 คะแนน กรุณาแก้ไขลำดับนี้",
|
||||
"firstHalf": "ครึ่งแรก",
|
||||
"firstHalfFull": "ครึ่งแรก (กรกฎาคม - ธันวาคม)",
|
||||
"secondHalf": "ครึ่งหลัง",
|
||||
"secondHalfFull": "ครึ่งหลัง (ตั้งแต่ 1 มกราคม)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,8 @@
|
||||
"weeks": "Linggo",
|
||||
"months": "Buwan",
|
||||
"years": "Taon",
|
||||
"ok": "OK"
|
||||
"ok": "OK",
|
||||
"period": "Panahon"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Home",
|
||||
@@ -838,5 +839,22 @@
|
||||
"targetMiddleShort": "middle short",
|
||||
"targetBackhandShort": "backhand short",
|
||||
"toTarget": "papunta sa"
|
||||
},
|
||||
"teamManagement": {
|
||||
"lineupProposal": "Line-up ayon sa QTTR",
|
||||
"eligibility": "Pagiging karapat-dapat",
|
||||
"eligibilityRegular": "Karaniwan",
|
||||
"eligibilityAdultRelease": "Pinayagan sa adults",
|
||||
"eligibilityAdultReserve": "Reserve sa adults",
|
||||
"eligibilityAdultReleaseAndReserve": "Pinayagan + reserve sa adults",
|
||||
"selectedLineup": "Napiling manlalaro",
|
||||
"availableLineupMembers": "Magagamit na manlalaro",
|
||||
"lineupEmpty": "Wala pang manlalarong itinalaga sa koponang ito.",
|
||||
"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)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,8 @@
|
||||
"weeks": "周",
|
||||
"months": "个月",
|
||||
"years": "年",
|
||||
"ok": "确定"
|
||||
"ok": "确定",
|
||||
"period": "期间"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "首页",
|
||||
@@ -838,5 +839,22 @@
|
||||
"targetMiddleShort": "中路短",
|
||||
"targetBackhandShort": "反手短",
|
||||
"toTarget": "到"
|
||||
},
|
||||
"teamManagement": {
|
||||
"lineupProposal": "按 QTTR 的队伍报名",
|
||||
"eligibility": "参赛资格",
|
||||
"eligibilityRegular": "常规",
|
||||
"eligibilityAdultRelease": "允许参加成人组",
|
||||
"eligibilityAdultReserve": "成人组替补",
|
||||
"eligibilityAdultReleaseAndReserve": "允许参加成人组 + 替补",
|
||||
"selectedLineup": "已报名球员",
|
||||
"availableLineupMembers": "可用球员",
|
||||
"lineupEmpty": "这支队伍还没有报名任何球员。",
|
||||
"lineupSaveError": "无法保存队伍报名。",
|
||||
"lineupValidationTooLargeGap": "{higher} 比 {lower} 高出超过 30 个 QTTR 积分。请更正这个顺序。",
|
||||
"firstHalf": "上半程",
|
||||
"firstHalfFull": "上半程(7月 - 12月)",
|
||||
"secondHalf": "下半程",
|
||||
"secondHalfFull": "下半程(1月1日起)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,6 +257,8 @@
|
||||
<label class="checkbox-item"><span>{{ $t('members.picsInInternetAllowed') }}:</span> <input type="checkbox" v-model="newPicsInInternetAllowed"></label>
|
||||
<label class="checkbox-item"><span>{{ $t('members.testMembership') }}:</span> <input type="checkbox" v-model="testMembership"></label>
|
||||
<label class="checkbox-item"><span>{{ $t('members.memberFormHandedOver') }}:</span> <input type="checkbox" v-model="newMemberFormHandedOver"></label>
|
||||
<label class="checkbox-item"><span>{{ $t('members.adultReleaseApproved') }}:</span> <input type="checkbox" v-model="newAdultReleaseApproved"></label>
|
||||
<label class="checkbox-item"><span>{{ $t('members.adultReserveApproved') }}:</span> <input type="checkbox" v-model="newAdultReserveApproved"></label>
|
||||
|
||||
<!-- Trainingsgruppen -->
|
||||
<div class="contact-section" :class="{ 'member-field-warning-box': editorHasIssue('training-group') }" v-if="memberToEdit">
|
||||
@@ -364,6 +366,18 @@
|
||||
<span v-else-if="member.testMembership && member.trainingParticipations >= 3" class="warning-icon" :title="$t('members.threeOrMoreParticipations')">⚠️</span>
|
||||
<span class="member-meta-text">{{ labelGender(member.gender) }}</span>
|
||||
</div>
|
||||
<div v-if="member.adultReleaseApproved || member.adultReserveApproved" class="member-name-flags">
|
||||
<span
|
||||
v-if="member.adultReleaseApproved"
|
||||
class="member-eligibility-flag"
|
||||
:title="$t('members.adultReleaseApproved')"
|
||||
>FE</span>
|
||||
<span
|
||||
v-if="member.adultReserveApproved"
|
||||
class="member-eligibility-flag"
|
||||
:title="$t('members.adultReserveApproved')"
|
||||
>EE</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
@@ -911,6 +925,8 @@ export default {
|
||||
showInactiveMembers: false,
|
||||
newPicsInInternetAllowed: false,
|
||||
newMemberFormHandedOver: false,
|
||||
newAdultReleaseApproved: false,
|
||||
newAdultReserveApproved: false,
|
||||
isUpdatingRatings: false,
|
||||
showMemberInfo: false,
|
||||
showActivitiesModal: false,
|
||||
@@ -1370,6 +1386,8 @@ export default {
|
||||
this.memberImage = null;
|
||||
this.memberImagePreview = null;
|
||||
this.newMemberFormHandedOver = false;
|
||||
this.newAdultReleaseApproved = false;
|
||||
this.newAdultReserveApproved = false;
|
||||
this.memberContacts = {
|
||||
phones: [{ value: '', isParent: false, parentName: '', isPrimary: false }],
|
||||
emails: [{ value: '', isParent: false, parentName: '', isPrimary: false }]
|
||||
@@ -1589,6 +1607,8 @@ export default {
|
||||
testMembership: this.testMembership,
|
||||
picsInInternetAllowed: this.newPicsInInternetAllowed,
|
||||
memberFormHandedOver: this.newMemberFormHandedOver,
|
||||
adultReleaseApproved: this.newAdultReleaseApproved,
|
||||
adultReserveApproved: this.newAdultReserveApproved,
|
||||
contacts: contacts
|
||||
};
|
||||
|
||||
@@ -1637,6 +1657,8 @@ export default {
|
||||
this.testMembership = member.testMembership;
|
||||
this.newPicsInInternetAllowed = member.picsInInternetAllowed;
|
||||
this.newMemberFormHandedOver = !!member.memberFormHandedOver;
|
||||
this.newAdultReleaseApproved = !!member.adultReleaseApproved;
|
||||
this.newAdultReserveApproved = !!member.adultReserveApproved;
|
||||
|
||||
// Load contacts
|
||||
if (member.contacts && Array.isArray(member.contacts)) {
|
||||
@@ -3574,6 +3596,30 @@ table td {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.member-name-flags {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 0.18rem;
|
||||
}
|
||||
|
||||
.member-eligibility-flag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 2.1rem;
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 999px;
|
||||
background: #eef6ff;
|
||||
color: #12427a;
|
||||
border: 1px solid #bfd7f3;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.member-meta-text {
|
||||
color: #66788a;
|
||||
font-size: 0.88rem;
|
||||
|
||||
@@ -66,6 +66,9 @@
|
||||
<button v-if="teamToEdit && teamToEdit.leagueId" type="button" class="workspace-section-button" :class="{ active: activeEditorSection === 'stats' }" @click="activeEditorSection = 'stats'">
|
||||
{{ t('teamManagement.playerStats') }}
|
||||
</button>
|
||||
<button v-if="teamToEdit" type="button" class="workspace-section-button" :class="{ active: activeEditorSection === 'lineup' }" @click="activeEditorSection = 'lineup'">
|
||||
{{ t('teamManagement.lineupProposal') }}
|
||||
</button>
|
||||
<button v-if="teamToEdit" type="button" class="workspace-section-button" :class="{ active: activeEditorSection === 'documents' }" @click="activeEditorSection = 'documents'">
|
||||
{{ t('teamManagement.documents') }}
|
||||
</button>
|
||||
@@ -97,6 +100,14 @@
|
||||
<span class="settings-summary-label">{{ t('teamManagement.season') }}</span>
|
||||
<strong>{{ teamToEdit.season?.season || t('teamManagement.seasonUnknown') }}</strong>
|
||||
</div>
|
||||
<div class="settings-summary-card">
|
||||
<span class="settings-summary-label">{{ t('teamManagement.teamGender') }}</span>
|
||||
<strong>{{ labelTeamGender(effectiveTeamGender) }}</strong>
|
||||
</div>
|
||||
<div class="settings-summary-card">
|
||||
<span class="settings-summary-label">{{ t('teamManagement.teamAgeGroup') }}</span>
|
||||
<strong>{{ labelAgeGroup(effectiveTeamAgeGroup) }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-form-grid">
|
||||
<label>
|
||||
@@ -113,6 +124,23 @@
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span>{{ t('teamManagement.teamGender') }}:</span>
|
||||
<select v-model="newTeamGender">
|
||||
<option value="open">{{ t('teamManagement.teamGenderOpen') }}</option>
|
||||
<option value="female">{{ t('teamManagement.teamGenderFemale') }}</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span>{{ t('teamManagement.teamAgeGroup') }}:</span>
|
||||
<select v-model="newTeamAgeGroup">
|
||||
<option v-for="option in teamAgeGroupOptions" :key="option.value" :value="option.value">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button @click="addNewTeam" :disabled="!newTeamName.trim()">
|
||||
@@ -184,36 +212,117 @@
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div v-if="lineupProposalGroups.length" class="lineup-proposal-card">
|
||||
<div class="lineup-proposal-header">
|
||||
<strong>{{ t('teamManagement.lineupProposal') }}</strong>
|
||||
<span>{{ lineupProposalMemberCount }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="teamToEdit && activeEditorSection === 'lineup'" class="workspace-section-panel player-stats">
|
||||
<div class="workspace-panel-header">
|
||||
<div>
|
||||
<span class="section-title">📋 {{ t('teamManagement.lineupProposal') }}</span>
|
||||
</div>
|
||||
<div class="lineup-proposal-groups">
|
||||
<section v-for="group in lineupProposalGroups" :key="group.code" class="lineup-proposal-group">
|
||||
<div class="lineup-proposal-group-head">
|
||||
<strong>{{ group.label }}</strong>
|
||||
<span>{{ group.members.length }}</span>
|
||||
</div>
|
||||
<table class="lineup-proposal-table">
|
||||
<div class="lineup-toolbar">
|
||||
<div class="lineup-period-meta">
|
||||
<span class="lineup-period-chip">
|
||||
{{ t('teamManagement.season') }}: {{ teamToEdit?.season?.season || currentSeason?.season || t('teamManagement.seasonUnknown') }}
|
||||
</span>
|
||||
<label class="lineup-period-select">
|
||||
<span>{{ t('common.period') }}</span>
|
||||
<select v-model="selectedLineupHalf" :disabled="savingTeamLineup || loadingTeamLineup">
|
||||
<option v-for="option in lineupHalfOptions" :key="option.value" :value="option.value">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<button @click="loadClubMembers(); loadTeamLineup();" :disabled="loadingLineupMembers || loadingTeamLineup" class="btn-sm">
|
||||
{{ (loadingLineupMembers || loadingTeamLineup) ? '⏳' : '🔄' }} {{ t('teamManagement.refreshStats') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lineup-editor-grid">
|
||||
<div class="lineup-proposal-card">
|
||||
<div class="lineup-proposal-header">
|
||||
<strong>{{ t('teamManagement.selectedLineup') }}</strong>
|
||||
<span>{{ selectedTeamLineupMembers.length }}</span>
|
||||
</div>
|
||||
|
||||
<table v-if="selectedTeamLineupMembers.length" class="lineup-proposal-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>#</th>
|
||||
<th>{{ t('teamManagement.player') }}</th>
|
||||
<th>{{ t('members.ageGroup') }}</th>
|
||||
<th :title="t('teamManagement.qttr')">(Q)TTR</th>
|
||||
<th>{{ t('teamManagement.eligibility') }}</th>
|
||||
<th>{{ t('common.actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody :key="lineupRenderKey" ref="lineupSortableBody">
|
||||
<tr v-for="(member, index) in selectedTeamLineupMembers" :key="member.id" class="team-lineup-row">
|
||||
<td class="lineup-drag-cell">
|
||||
<span class="drag-handle" :title="t('common.move')">⋮⋮</span>
|
||||
</td>
|
||||
<td class="lineup-rank">{{ index + 1 }}</td>
|
||||
<td>{{ member.firstName }} {{ member.lastName }}</td>
|
||||
<td>{{ member.memberAgeGroupLabel }}</td>
|
||||
<td class="lineup-rating">{{ member.lineupRatingLabel }}</td>
|
||||
<td>{{ member.eligibilityLabel }}</td>
|
||||
<td class="lineup-actions-cell">
|
||||
<button type="button" class="btn-secondary btn-upload-sm" @click="moveLineupMember(member.id, 'up')" :disabled="savingTeamLineup || index === 0">↑</button>
|
||||
<button type="button" class="btn-secondary btn-upload-sm" @click="moveLineupMember(member.id, 'down')" :disabled="savingTeamLineup || index === selectedTeamLineupMembers.length - 1">↓</button>
|
||||
<button type="button" class="btn-secondary btn-upload-sm" @click="removeMemberFromLineup(member.id)" :disabled="savingTeamLineup">−</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div v-else class="no-stats">
|
||||
{{ t('teamManagement.lineupEmpty') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="lineupProposalGroups.length" class="lineup-proposal-card">
|
||||
<div class="lineup-proposal-header">
|
||||
<strong>{{ t('teamManagement.availableLineupMembers') }}</strong>
|
||||
<span>{{ availableLineupMembers.length }}</span>
|
||||
</div>
|
||||
<div class="lineup-proposal-groups">
|
||||
<section v-for="group in lineupProposalGroups" :key="group.code" class="lineup-proposal-group">
|
||||
<div class="lineup-proposal-group-head">
|
||||
<strong>{{ group.label }}</strong>
|
||||
<span>{{ group.members.filter((member) => !selectedTeamLineupMembers.some((selected) => selected.id === member.id)).length }}</span>
|
||||
</div>
|
||||
<table class="lineup-proposal-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>{{ t('teamManagement.player') }}</th>
|
||||
<th>{{ t('members.ageGroup') }}</th>
|
||||
<th :title="t('teamManagement.qttr')">(Q)TTR</th>
|
||||
<th>{{ t('teamManagement.eligibility') }}</th>
|
||||
<th>{{ t('common.actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(member, index) in group.members" :key="member.id">
|
||||
<td class="lineup-rank">{{ index + 1 }}</td>
|
||||
<tr v-for="member in group.members.filter((entry) => !selectedTeamLineupMembers.some((selected) => selected.id === entry.id))" :key="member.id">
|
||||
<td>{{ member.firstName }} {{ member.lastName }}</td>
|
||||
<td>{{ member.memberAgeGroupLabel }}</td>
|
||||
<td class="lineup-rating">{{ member.lineupRatingLabel }}</td>
|
||||
<td>{{ member.eligibilityLabel }}</td>
|
||||
<td class="lineup-actions-cell">
|
||||
<button type="button" class="btn-secondary btn-upload-sm" @click="addMemberToLineup(member)" :disabled="savingTeamLineup">+</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!lineupProposalGroups.length" class="no-stats">
|
||||
{{ t('teamManagement.noPlayerStats') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="teamToEdit && activeEditorSection === 'documents'" class="workspace-section-panel advanced-settings">
|
||||
@@ -409,12 +518,13 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted, computed, watch } from 'vue';
|
||||
import { ref, onMounted, computed, watch, nextTick, onBeforeUnmount } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
import SeasonSelector from '../components/SeasonSelector.vue';
|
||||
import apiClient from '../apiClient.js';
|
||||
import { getSafeErrorMessage, getSafeMessage } from '../utils/errorMessages.js';
|
||||
import i18n from '../i18n';
|
||||
import Sortable from 'sortablejs';
|
||||
|
||||
import InfoDialog from '../components/InfoDialog.vue';
|
||||
import ConfirmDialog from '../components/ConfirmDialog.vue';
|
||||
@@ -464,6 +574,8 @@ export default {
|
||||
const teamToEdit = ref(null);
|
||||
const newTeamName = ref('');
|
||||
const newLeagueId = ref('');
|
||||
const newTeamGender = ref('open');
|
||||
const newTeamAgeGroup = ref('adult');
|
||||
const selectedSeasonId = ref(null);
|
||||
const currentSeason = ref(null);
|
||||
const teamDocuments = ref([]);
|
||||
@@ -491,8 +603,16 @@ export default {
|
||||
// Player Stats
|
||||
const playerStats = ref([]);
|
||||
const loadingStats = ref(false);
|
||||
const loadingLineupMembers = ref(false);
|
||||
const loadingTeamLineup = ref(false);
|
||||
const savingTeamLineup = ref(false);
|
||||
const memberById = ref({});
|
||||
const clubMembers = ref([]);
|
||||
const teamLineupAssignments = ref([]);
|
||||
const lineupSortableBody = ref(null);
|
||||
const lineupRenderKey = ref(0);
|
||||
const selectedLineupHalf = ref('first_half');
|
||||
let lineupSortableInstance = null;
|
||||
|
||||
// Scheduler Jobs Info
|
||||
const schedulerJobs = ref({
|
||||
@@ -516,6 +636,14 @@ export default {
|
||||
const teamsWithoutLeagueCount = computed(() => teams.value.filter(team => !team.leagueId).length);
|
||||
const totalSeasonAppearances = computed(() => playerStats.value.reduce((sum, stat) => sum + (Number(stat.totalSeason) || 0), 0));
|
||||
const totalHalfAppearances = computed(() => playerStats.value.reduce((sum, stat) => sum + (Number(isSecondHalf.value ? stat.totalSecondHalf : stat.totalFirstHalf) || 0), 0));
|
||||
const lineupHalfOptions = computed(() => ([
|
||||
{ value: 'first_half', label: t('teamManagement.firstHalfFull') },
|
||||
{ value: 'second_half', label: t('teamManagement.secondHalfFull') }
|
||||
]));
|
||||
const teamAgeGroupOptions = computed(() => ['adult', 'J19', 'J17', 'J15', 'J13', 'J11'].map((value) => ({
|
||||
value,
|
||||
label: value === 'adult' ? t('members.adults') : t(`members.${value.toLowerCase()}`)
|
||||
})));
|
||||
const filteredTeams = computed(() => {
|
||||
const search = teamSearchQuery.value.trim().toLowerCase();
|
||||
return teams.value.filter(team => {
|
||||
@@ -550,22 +678,105 @@ export default {
|
||||
return 'adult';
|
||||
};
|
||||
|
||||
const parseLeagueGenderCode = (leagueName) => {
|
||||
const source = String(leagueName || '').toLowerCase();
|
||||
return /(frauen|damen|mädchen|girls|women)/i.test(source) ? 'female' : 'open';
|
||||
};
|
||||
|
||||
const getConfiguredTeamAgeGroup = (team) => team?.teamAgeGroup || parseLeagueAgeGroupCode(team?.league?.name);
|
||||
const getConfiguredTeamGender = (team) => team?.teamGender || parseLeagueGenderCode(team?.league?.name);
|
||||
|
||||
const effectiveTeamAgeGroup = computed(() => getConfiguredTeamAgeGroup(teamToEdit.value));
|
||||
const effectiveTeamGender = computed(() => getConfiguredTeamGender(teamToEdit.value));
|
||||
|
||||
const getSeasonReferenceYear = () => {
|
||||
const seasonLabel = String(currentSeason.value?.season || teamToEdit.value?.season?.season || '').trim();
|
||||
const fourDigitRange = seasonLabel.match(/(\d{4})\s*\/\s*(\d{4})/);
|
||||
if (fourDigitRange) {
|
||||
return Number(fourDigitRange[2]);
|
||||
}
|
||||
|
||||
const mixedRange = seasonLabel.match(/(\d{4})\s*\/\s*(\d{2})/);
|
||||
if (mixedRange) {
|
||||
const startYear = Number(mixedRange[1]);
|
||||
const endTwoDigits = Number(mixedRange[2]);
|
||||
const century = Math.floor(startYear / 100) * 100;
|
||||
return century + endTwoDigits;
|
||||
}
|
||||
|
||||
const singleYear = seasonLabel.match(/(\d{4})/);
|
||||
if (singleYear) {
|
||||
return Number(singleYear[1]);
|
||||
}
|
||||
|
||||
return new Date().getFullYear();
|
||||
};
|
||||
|
||||
const parseMemberBirthDate = (value) => {
|
||||
if (!value || typeof value !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trimmed = value.trim();
|
||||
const dotted = trimmed.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/);
|
||||
if (dotted) {
|
||||
const year = Number(dotted[3]);
|
||||
const month = Number(dotted[2]) - 1;
|
||||
const day = Number(dotted[1]);
|
||||
const parsed = new Date(year, month, day);
|
||||
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
||||
}
|
||||
|
||||
const isoDate = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||
if (isoDate) {
|
||||
const year = Number(isoDate[1]);
|
||||
const month = Number(isoDate[2]) - 1;
|
||||
const day = Number(isoDate[3]);
|
||||
const parsed = new Date(year, month, day);
|
||||
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
||||
}
|
||||
|
||||
const parsed = new Date(trimmed);
|
||||
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
||||
};
|
||||
|
||||
const getMemberActualAge = (member) => {
|
||||
const birthDate = parseMemberBirthDate(member?.birthDate);
|
||||
if (!birthDate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const today = new Date();
|
||||
let age = today.getFullYear() - birthDate.getFullYear();
|
||||
const hadBirthdayThisYear =
|
||||
today.getMonth() > birthDate.getMonth() ||
|
||||
(today.getMonth() === birthDate.getMonth() && today.getDate() >= birthDate.getDate());
|
||||
if (!hadBirthdayThisYear) {
|
||||
age -= 1;
|
||||
}
|
||||
return age;
|
||||
};
|
||||
|
||||
const getMemberSeasonAge = (member) => {
|
||||
const birthDate = parseMemberBirthDate(member?.birthDate);
|
||||
if (!birthDate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getSeasonReferenceYear() - birthDate.getFullYear();
|
||||
};
|
||||
|
||||
const getMemberAgeGroupCode = (member) => {
|
||||
if (!member?.birthDate) {
|
||||
const seasonAge = getMemberSeasonAge(member);
|
||||
if (seasonAge === null) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
const birthDate = new Date(member.birthDate);
|
||||
if (Number.isNaN(birthDate.getTime())) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
const ageByBirthYear = new Date().getFullYear() - birthDate.getFullYear();
|
||||
if (ageByBirthYear <= 11) return 'J11';
|
||||
if (ageByBirthYear <= 13) return 'J13';
|
||||
if (ageByBirthYear <= 15) return 'J15';
|
||||
if (ageByBirthYear <= 17) return 'J17';
|
||||
if (ageByBirthYear <= 19) return 'J19';
|
||||
if (seasonAge <= 11) return 'J11';
|
||||
if (seasonAge <= 13) return 'J13';
|
||||
if (seasonAge <= 15) return 'J15';
|
||||
if (seasonAge <= 17) return 'J17';
|
||||
if (seasonAge <= 19) return 'J19';
|
||||
return 'adult';
|
||||
};
|
||||
|
||||
@@ -575,6 +786,57 @@ export default {
|
||||
return code;
|
||||
};
|
||||
|
||||
const labelAgeGroup = (code) => getMemberAgeGroupLabel(code);
|
||||
const labelTeamGender = (gender) => gender === 'female' ? t('teamManagement.teamGenderFemale') : t('teamManagement.teamGenderOpen');
|
||||
|
||||
const isFemaleEligible = (member, teamGender) => {
|
||||
if (teamGender !== 'female') return true;
|
||||
return member?.gender === 'female';
|
||||
};
|
||||
|
||||
const isEligibleForTeam = (member, teamAgeGroup, teamGender) => {
|
||||
if (!member?.active || !isFemaleEligible(member, teamGender)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const memberAgeGroup = getMemberAgeGroupCode(member);
|
||||
const memberActualAge = getMemberActualAge(member);
|
||||
if (memberAgeGroup === 'unknown') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (teamAgeGroup === 'adult') {
|
||||
if (memberActualAge !== null && memberActualAge >= 18) {
|
||||
return true;
|
||||
}
|
||||
return !!member?.adultReleaseApproved || !!member?.adultReserveApproved;
|
||||
}
|
||||
|
||||
const order = ['J11', 'J13', 'J15', 'J17', 'J19'];
|
||||
const memberIndex = order.indexOf(memberAgeGroup);
|
||||
const teamIndex = order.indexOf(teamAgeGroup);
|
||||
if (memberIndex < 0 || teamIndex < 0) {
|
||||
return false;
|
||||
}
|
||||
return memberIndex <= teamIndex;
|
||||
};
|
||||
|
||||
const getMemberEligibilityLabel = (member, teamAgeGroup) => {
|
||||
if (teamAgeGroup !== 'adult' || getMemberAgeGroupCode(member) === 'adult') {
|
||||
return t('teamManagement.eligibilityRegular');
|
||||
}
|
||||
if (member?.adultReleaseApproved && member?.adultReserveApproved) {
|
||||
return t('teamManagement.eligibilityAdultReleaseAndReserve');
|
||||
}
|
||||
if (member?.adultReleaseApproved) {
|
||||
return t('teamManagement.eligibilityAdultRelease');
|
||||
}
|
||||
if (member?.adultReserveApproved) {
|
||||
return t('teamManagement.eligibilityAdultReserve');
|
||||
}
|
||||
return t('teamManagement.eligibilityRegular');
|
||||
};
|
||||
|
||||
const getMemberLineupRatingValue = (member) => {
|
||||
const qttr = Number(member?.qttr);
|
||||
if (Number.isFinite(qttr)) return qttr;
|
||||
@@ -589,13 +851,17 @@ export default {
|
||||
};
|
||||
|
||||
const lineupProposalGroups = computed(() => {
|
||||
const members = (clubMembers.value || []).filter((member) => member?.active);
|
||||
const teamAgeGroup = effectiveTeamAgeGroup.value;
|
||||
const teamGender = effectiveTeamGender.value;
|
||||
const members = (clubMembers.value || []).filter((member) => isEligibleForTeam(member, teamAgeGroup, teamGender));
|
||||
if (!members.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const preferredAgeGroup = parseLeagueAgeGroupCode(teamToEdit.value?.league?.name);
|
||||
const defaultOrder = ['adult', 'J19', 'J17', 'J15', 'J13', 'J11', 'unknown'];
|
||||
const preferredAgeGroup = teamAgeGroup;
|
||||
const defaultOrder = teamAgeGroup === 'adult'
|
||||
? ['adult', 'J19', 'J17', 'J15', 'J13', 'J11', 'unknown']
|
||||
: [teamAgeGroup, ...['J19', 'J17', 'J15', 'J13', 'J11'].filter((entry) => entry !== teamAgeGroup), 'unknown'];
|
||||
const groupOrder = defaultOrder.includes(preferredAgeGroup)
|
||||
? [preferredAgeGroup, ...defaultOrder.filter((entry) => entry !== preferredAgeGroup)]
|
||||
: defaultOrder;
|
||||
@@ -612,7 +878,9 @@ export default {
|
||||
}
|
||||
groups.get(code).members.push({
|
||||
...member,
|
||||
lineupRatingLabel: getMemberLineupRatingLabel(member)
|
||||
lineupRatingLabel: getMemberLineupRatingLabel(member),
|
||||
memberAgeGroupLabel: getMemberAgeGroupLabel(code),
|
||||
eligibilityLabel: getMemberEligibilityLabel(member, teamAgeGroup)
|
||||
});
|
||||
});
|
||||
|
||||
@@ -636,6 +904,40 @@ export default {
|
||||
});
|
||||
});
|
||||
const lineupProposalMemberCount = computed(() => lineupProposalGroups.value.reduce((sum, group) => sum + group.members.length, 0));
|
||||
const eligibleLineupMembers = computed(() => lineupProposalGroups.value.flatMap((group) => group.members));
|
||||
const selectedTeamLineupMembers = computed(() => {
|
||||
const memberMap = new Map(eligibleLineupMembers.value.map((member) => [Number(member.id), member]));
|
||||
return [...teamLineupAssignments.value]
|
||||
.sort((a, b) => a.position - b.position)
|
||||
.map((entry) => {
|
||||
const member = memberMap.get(Number(entry.memberId));
|
||||
return member ? { ...member, position: entry.position } : null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
});
|
||||
const availableLineupMembers = computed(() => {
|
||||
const selectedIds = new Set(teamLineupAssignments.value.map((entry) => Number(entry.memberId)));
|
||||
return eligibleLineupMembers.value.filter((member) => !selectedIds.has(Number(member.id)));
|
||||
});
|
||||
const teamLineupValidationMessage = computed(() => {
|
||||
const selected = selectedTeamLineupMembers.value;
|
||||
for (let index = 0; index < selected.length - 1; index += 1) {
|
||||
const currentMember = selected[index];
|
||||
const nextMember = selected[index + 1];
|
||||
const currentRating = getMemberLineupRatingValue(currentMember);
|
||||
const nextRating = getMemberLineupRatingValue(nextMember);
|
||||
if (!Number.isFinite(currentRating) || !Number.isFinite(nextRating)) {
|
||||
continue;
|
||||
}
|
||||
if (nextRating > currentRating + 30) {
|
||||
return t('teamManagement.lineupValidationTooLargeGap', {
|
||||
higher: `${nextMember.firstName} ${nextMember.lastName}`,
|
||||
lower: `${currentMember.firstName} ${currentMember.lastName}`
|
||||
});
|
||||
}
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
// Methods
|
||||
const toggleNewTeam = () => {
|
||||
@@ -654,6 +956,8 @@ export default {
|
||||
const resetNewTeam = () => {
|
||||
newTeamName.value = '';
|
||||
newLeagueId.value = '';
|
||||
newTeamGender.value = 'open';
|
||||
newTeamAgeGroup.value = 'adult';
|
||||
activeEditorSection.value = 'basic';
|
||||
};
|
||||
|
||||
@@ -695,7 +999,9 @@ export default {
|
||||
const teamData = {
|
||||
name: newTeamName.value.trim(),
|
||||
leagueId: newLeagueId.value || null,
|
||||
seasonId: selectedSeasonId.value
|
||||
seasonId: selectedSeasonId.value,
|
||||
teamGender: newTeamGender.value,
|
||||
teamAgeGroup: newTeamAgeGroup.value
|
||||
};
|
||||
|
||||
if (teamToEdit.value) {
|
||||
@@ -727,6 +1033,8 @@ export default {
|
||||
teamToEdit.value = team;
|
||||
newTeamName.value = team.name;
|
||||
newLeagueId.value = team.leagueId || '';
|
||||
newTeamGender.value = getConfiguredTeamGender(team);
|
||||
newTeamAgeGroup.value = getConfiguredTeamAgeGroup(team);
|
||||
teamFormIsOpen.value = true;
|
||||
activeEditorSection.value = 'basic';
|
||||
await loadTeamDocuments();
|
||||
@@ -1362,6 +1670,270 @@ export default {
|
||||
};
|
||||
};
|
||||
|
||||
const loadClubMembers = async () => {
|
||||
if (!teamToEdit.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadingLineupMembers.value = true;
|
||||
try {
|
||||
const membersResp = await apiClient.get(`/clubmembers/get/${selectedClub.value}/true`);
|
||||
const map = {};
|
||||
clubMembers.value = membersResp.data || [];
|
||||
for (const m of clubMembers.value) {
|
||||
map[m.id] = { ttr: m.ttr ?? null, qttr: m.qttr ?? null };
|
||||
}
|
||||
memberById.value = map;
|
||||
} catch (e) {
|
||||
clubMembers.value = [];
|
||||
memberById.value = {};
|
||||
} finally {
|
||||
loadingLineupMembers.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const normalizeTeamLineupAssignments = (assignments) => assignments
|
||||
.filter((entry) => entry && entry.memberId)
|
||||
.map((entry, index) => ({
|
||||
memberId: Number(entry.memberId),
|
||||
position: index + 1
|
||||
}));
|
||||
|
||||
const getLineupValidationMessageForAssignments = (assignments) => {
|
||||
const memberMap = new Map(eligibleLineupMembers.value.map((member) => [Number(member.id), member]));
|
||||
const selected = normalizeTeamLineupAssignments(assignments)
|
||||
.map((entry) => memberMap.get(Number(entry.memberId)))
|
||||
.filter(Boolean);
|
||||
|
||||
for (let index = 0; index < selected.length - 1; index += 1) {
|
||||
const currentMember = selected[index];
|
||||
const nextMember = selected[index + 1];
|
||||
const currentRating = getMemberLineupRatingValue(currentMember);
|
||||
const nextRating = getMemberLineupRatingValue(nextMember);
|
||||
if (!Number.isFinite(currentRating) || !Number.isFinite(nextRating)) {
|
||||
continue;
|
||||
}
|
||||
if (nextRating > currentRating + 30) {
|
||||
return t('teamManagement.lineupValidationTooLargeGap', {
|
||||
higher: `${nextMember.firstName} ${nextMember.lastName}`,
|
||||
lower: `${currentMember.firstName} ${currentMember.lastName}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const getLineupMoveValidationMessage = (currentAssignments, oldIndex, newIndex) => {
|
||||
const normalizedAssignments = normalizeTeamLineupAssignments(currentAssignments);
|
||||
const memberMap = new Map(eligibleLineupMembers.value.map((member) => [Number(member.id), member]));
|
||||
const movedEntry = normalizedAssignments[oldIndex];
|
||||
if (!movedEntry) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const movedMember = memberMap.get(Number(movedEntry.memberId));
|
||||
const movedRating = getMemberLineupRatingValue(movedMember);
|
||||
if (!Number.isFinite(movedRating)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const rangeStart = Math.min(oldIndex, newIndex);
|
||||
const rangeEnd = Math.max(oldIndex, newIndex);
|
||||
const crossedEntries = normalizedAssignments.slice(rangeStart, rangeEnd + 1)
|
||||
.filter((_, index) => (rangeStart + index) !== oldIndex);
|
||||
|
||||
for (const crossedEntry of crossedEntries) {
|
||||
const crossedMember = memberMap.get(Number(crossedEntry.memberId));
|
||||
const crossedRating = getMemberLineupRatingValue(crossedMember);
|
||||
if (!Number.isFinite(crossedRating)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (newIndex < oldIndex) {
|
||||
if (crossedRating > movedRating + 30) {
|
||||
return t('teamManagement.lineupValidationTooLargeGap', {
|
||||
higher: `${crossedMember.firstName} ${crossedMember.lastName}`,
|
||||
lower: `${movedMember.firstName} ${movedMember.lastName}`
|
||||
});
|
||||
}
|
||||
} else if (movedRating > crossedRating + 30) {
|
||||
return t('teamManagement.lineupValidationTooLargeGap', {
|
||||
higher: `${movedMember.firstName} ${movedMember.lastName}`,
|
||||
lower: `${crossedMember.firstName} ${crossedMember.lastName}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const loadTeamLineup = async () => {
|
||||
if (!teamToEdit.value?.id) {
|
||||
teamLineupAssignments.value = [];
|
||||
lineupRenderKey.value += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
loadingTeamLineup.value = true;
|
||||
try {
|
||||
const response = await apiClient.get(`/club-teams/${teamToEdit.value.id}/lineup`, {
|
||||
params: { half: selectedLineupHalf.value }
|
||||
});
|
||||
if (response.status >= 400) {
|
||||
throw new Error('lineuploadfailed');
|
||||
}
|
||||
const assignments = Array.isArray(response.data)
|
||||
? response.data.map((entry) => ({
|
||||
memberId: Number(entry.memberId),
|
||||
position: Number(entry.position)
|
||||
}))
|
||||
: [];
|
||||
teamLineupAssignments.value = normalizeTeamLineupAssignments(assignments);
|
||||
lineupRenderKey.value += 1;
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Mannschaftsmeldung:', error);
|
||||
teamLineupAssignments.value = [];
|
||||
lineupRenderKey.value += 1;
|
||||
} finally {
|
||||
loadingTeamLineup.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const persistTeamLineupAssignments = async (nextAssignments, validationMessage = '') => {
|
||||
if (!teamToEdit.value?.id || savingTeamLineup.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const normalizedAssignments = normalizeTeamLineupAssignments(nextAssignments);
|
||||
if (validationMessage) {
|
||||
lineupRenderKey.value += 1;
|
||||
await nextTick();
|
||||
initializeLineupSortable();
|
||||
await showInfo(t('messages.warning'), validationMessage, '', 'warning');
|
||||
return false;
|
||||
}
|
||||
|
||||
const previousAssignments = [...teamLineupAssignments.value];
|
||||
teamLineupAssignments.value = normalizedAssignments;
|
||||
savingTeamLineup.value = true;
|
||||
try {
|
||||
const response = await apiClient.put(`/club-teams/${teamToEdit.value.id}/lineup`, {
|
||||
assignments: normalizedAssignments,
|
||||
lineupHalf: selectedLineupHalf.value
|
||||
});
|
||||
if (response.status >= 400) {
|
||||
throw new Error('lineupsavefailed');
|
||||
}
|
||||
teamLineupAssignments.value = normalizedAssignments;
|
||||
lineupRenderKey.value += 1;
|
||||
await nextTick();
|
||||
initializeLineupSortable();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Speichern der Mannschaftsmeldung:', error);
|
||||
teamLineupAssignments.value = previousAssignments;
|
||||
lineupRenderKey.value += 1;
|
||||
await nextTick();
|
||||
initializeLineupSortable();
|
||||
await showInfo(t('messages.error'), t('teamManagement.lineupSaveError'), '', 'error');
|
||||
return false;
|
||||
} finally {
|
||||
savingTeamLineup.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const addMemberToLineup = async (member) => {
|
||||
if (!member?.id || savingTeamLineup.value) {
|
||||
return;
|
||||
}
|
||||
if (teamLineupAssignments.value.some((entry) => Number(entry.memberId) === Number(member.id))) {
|
||||
return;
|
||||
}
|
||||
const nextAssignments = [
|
||||
...teamLineupAssignments.value,
|
||||
{ memberId: Number(member.id) }
|
||||
];
|
||||
const validationMessage = getLineupValidationMessageForAssignments(nextAssignments);
|
||||
await persistTeamLineupAssignments(nextAssignments, validationMessage);
|
||||
};
|
||||
|
||||
const removeMemberFromLineup = async (memberId) => {
|
||||
if (savingTeamLineup.value) {
|
||||
return;
|
||||
}
|
||||
await persistTeamLineupAssignments(
|
||||
teamLineupAssignments.value.filter((entry) => Number(entry.memberId) !== Number(memberId))
|
||||
);
|
||||
};
|
||||
|
||||
const moveLineupMember = async (memberId, direction) => {
|
||||
if (savingTeamLineup.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentAssignments = [...teamLineupAssignments.value].sort((a, b) => a.position - b.position);
|
||||
const currentIndex = currentAssignments.findIndex((entry) => Number(entry.memberId) === Number(memberId));
|
||||
if (currentIndex < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1;
|
||||
if (targetIndex < 0 || targetIndex >= currentAssignments.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [entry] = currentAssignments.splice(currentIndex, 1);
|
||||
currentAssignments.splice(targetIndex, 0, entry);
|
||||
const validationMessage = getLineupMoveValidationMessage(
|
||||
[...teamLineupAssignments.value].sort((a, b) => a.position - b.position),
|
||||
currentIndex,
|
||||
targetIndex
|
||||
);
|
||||
await persistTeamLineupAssignments(currentAssignments, validationMessage);
|
||||
};
|
||||
|
||||
const destroyLineupSortable = () => {
|
||||
if (lineupSortableInstance) {
|
||||
lineupSortableInstance.destroy();
|
||||
lineupSortableInstance = null;
|
||||
}
|
||||
};
|
||||
|
||||
const initializeLineupSortable = async () => {
|
||||
await nextTick();
|
||||
destroyLineupSortable();
|
||||
|
||||
if (activeEditorSection.value !== 'lineup' || !lineupSortableBody.value || selectedTeamLineupMembers.value.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
lineupSortableInstance = Sortable.create(lineupSortableBody.value, {
|
||||
draggable: '.team-lineup-row',
|
||||
handle: '.drag-handle',
|
||||
animation: 150,
|
||||
onEnd: async (event) => {
|
||||
if (savingTeamLineup.value || event.oldIndex == null || event.newIndex == null || event.oldIndex === event.newIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentAssignments = [...teamLineupAssignments.value].sort((a, b) => a.position - b.position);
|
||||
const [movedEntry] = currentAssignments.splice(event.oldIndex, 1);
|
||||
currentAssignments.splice(event.newIndex, 0, movedEntry);
|
||||
const validationMessage = getLineupMoveValidationMessage(
|
||||
[...teamLineupAssignments.value].sort((a, b) => a.position - b.position),
|
||||
event.oldIndex,
|
||||
event.newIndex
|
||||
);
|
||||
const success = await persistTeamLineupAssignments(currentAssignments, validationMessage);
|
||||
if (!success) {
|
||||
await nextTick();
|
||||
initializeLineupSortable();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const refreshPlayerStats = async () => {
|
||||
if (!teamToEdit.value || !teamToEdit.value.leagueId) {
|
||||
return;
|
||||
@@ -1370,20 +1942,7 @@ export default {
|
||||
loadingStats.value = true;
|
||||
|
||||
try {
|
||||
// Mitglieder mit (Q)TTR laden, um Werte anzuzeigen
|
||||
try {
|
||||
const membersResp = await apiClient.get(`/clubmembers/get/${selectedClub.value}/true`);
|
||||
const map = {};
|
||||
clubMembers.value = membersResp.data || [];
|
||||
for (const m of clubMembers.value) {
|
||||
map[m.id] = { ttr: m.ttr ?? null, qttr: m.qttr ?? null };
|
||||
}
|
||||
memberById.value = map;
|
||||
} catch (e) {
|
||||
clubMembers.value = [];
|
||||
memberById.value = {};
|
||||
}
|
||||
|
||||
await loadClubMembers();
|
||||
const response = await apiClient.get(
|
||||
`/matches/leagues/${selectedClub.value}/stats/${teamToEdit.value.leagueId}`,
|
||||
{
|
||||
@@ -1495,12 +2054,40 @@ export default {
|
||||
|
||||
// Watch teamToEdit to load stats when a team is selected
|
||||
watch(teamToEdit, (newTeam) => {
|
||||
if (newTeam) {
|
||||
selectedLineupHalf.value = isSecondHalf.value ? 'second_half' : 'first_half';
|
||||
loadClubMembers();
|
||||
loadTeamLineup();
|
||||
} else {
|
||||
clubMembers.value = [];
|
||||
memberById.value = {};
|
||||
teamLineupAssignments.value = [];
|
||||
}
|
||||
|
||||
if (newTeam && newTeam.leagueId) {
|
||||
refreshPlayerStats();
|
||||
} else {
|
||||
playerStats.value = [];
|
||||
}
|
||||
});
|
||||
|
||||
watch(selectedLineupHalf, () => {
|
||||
if (teamToEdit.value?.id) {
|
||||
loadTeamLineup();
|
||||
}
|
||||
});
|
||||
|
||||
watch(activeEditorSection, () => {
|
||||
initializeLineupSortable();
|
||||
});
|
||||
|
||||
watch(selectedTeamLineupMembers, () => {
|
||||
initializeLineupSortable();
|
||||
}, { deep: true });
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
destroyLineupSortable();
|
||||
});
|
||||
|
||||
// Watch selectedSeasonId to load teams when season changes
|
||||
watch(selectedSeasonId, (newSeasonId) => {
|
||||
@@ -1557,6 +2144,8 @@ export default {
|
||||
teamToEdit,
|
||||
newTeamName,
|
||||
newLeagueId,
|
||||
newTeamGender,
|
||||
newTeamAgeGroup,
|
||||
selectedSeasonId,
|
||||
currentSeason,
|
||||
teamDocuments,
|
||||
@@ -1579,6 +2168,12 @@ export default {
|
||||
myTischtennisSuccess,
|
||||
playerStats,
|
||||
loadingStats,
|
||||
loadingLineupMembers,
|
||||
loadingTeamLineup,
|
||||
savingTeamLineup,
|
||||
lineupSortableBody,
|
||||
lineupRenderKey,
|
||||
selectedLineupHalf,
|
||||
filteredLeagues,
|
||||
isSecondHalf,
|
||||
fullyConfiguredTeamsCount,
|
||||
@@ -1586,8 +2181,18 @@ export default {
|
||||
teamsWithoutLeagueCount,
|
||||
totalSeasonAppearances,
|
||||
totalHalfAppearances,
|
||||
lineupHalfOptions,
|
||||
teamAgeGroupOptions,
|
||||
effectiveTeamAgeGroup,
|
||||
effectiveTeamGender,
|
||||
lineupProposalGroups,
|
||||
lineupProposalMemberCount,
|
||||
eligibleLineupMembers,
|
||||
selectedTeamLineupMembers,
|
||||
availableLineupMembers,
|
||||
teamLineupValidationMessage,
|
||||
labelAgeGroup,
|
||||
labelTeamGender,
|
||||
toggleNewTeam,
|
||||
resetToNewTeam,
|
||||
resetNewTeam,
|
||||
@@ -1612,8 +2217,13 @@ export default {
|
||||
clearParsedData,
|
||||
getMyTischtennisStatus,
|
||||
fetchTeamDataManually,
|
||||
refreshPlayerStats
|
||||
,memberById,
|
||||
refreshPlayerStats,
|
||||
loadClubMembers,
|
||||
loadTeamLineup,
|
||||
addMemberToLineup,
|
||||
removeMemberFromLineup,
|
||||
moveLineupMember,
|
||||
memberById,
|
||||
schedulerJobs,
|
||||
formatJobDate,
|
||||
loadSchedulerJobsInfo,
|
||||
@@ -2899,6 +3509,43 @@ export default {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.lineup-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.lineup-period-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.lineup-period-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.4rem 0.7rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(47, 122, 95, 0.08);
|
||||
color: var(--primary-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.lineup-period-select {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: var(--text-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.lineup-period-select select {
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.stats-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
@@ -2994,6 +3641,26 @@ export default {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.lineup-drag-cell {
|
||||
width: 2.4rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: grab;
|
||||
color: #66788a;
|
||||
font-weight: 700;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.team-lineup-row.sortable-chosen .drag-handle,
|
||||
.team-lineup-row.sortable-ghost .drag-handle {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* Legacy styles (can be removed if not used elsewhere) */
|
||||
.mytischtennis-header {
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user