feat(TrainingStats): enhance training statistics view and participant details
- Updated TrainingStatsService to include member details (first name, last name) in participant data. - Modified TrainingDetailsDialog to remove unnecessary time display for training sessions. - Added new filters for training days in TrainingStatsView, allowing users to select specific training days and view attending members. - Enhanced localization files to support new training day filter and participant-related strings across multiple languages.
This commit is contained in:
@@ -205,7 +205,12 @@ class TrainingStatsService {
|
||||
include: [{
|
||||
model: Participant,
|
||||
as: 'participantList',
|
||||
attributes: ['id', 'attendanceStatus']
|
||||
attributes: ['id', 'memberId', 'attendanceStatus'],
|
||||
include: [{
|
||||
model: Member,
|
||||
as: 'member',
|
||||
attributes: ['id', 'firstName', 'lastName']
|
||||
}]
|
||||
}],
|
||||
order: [['date', 'DESC']]
|
||||
});
|
||||
@@ -215,7 +220,22 @@ class TrainingStatsService {
|
||||
date: day.date,
|
||||
participantCount: day.participantList
|
||||
? day.participantList.filter((participant) => !participant.attendanceStatus || participant.attendanceStatus === 'present').length
|
||||
: 0
|
||||
: 0,
|
||||
participants: day.participantList
|
||||
? day.participantList
|
||||
.filter((participant) => !participant.attendanceStatus || participant.attendanceStatus === 'present')
|
||||
.map((participant) => ({
|
||||
id: participant.member?.id || participant.memberId,
|
||||
firstName: participant.member?.firstName || '',
|
||||
lastName: participant.member?.lastName || '',
|
||||
}))
|
||||
.filter((participant) => Number.isFinite(Number(participant.id)))
|
||||
.sort((a, b) => {
|
||||
const lastCompare = String(a.lastName || '').localeCompare(String(b.lastName || ''), 'de', { sensitivity: 'base' });
|
||||
if (lastCompare !== 0) return lastCompare;
|
||||
return String(a.firstName || '').localeCompare(String(b.firstName || ''), 'de', { sensitivity: 'base' });
|
||||
})
|
||||
: []
|
||||
}));
|
||||
|
||||
const totalParticipants12Months = formattedTrainingDays.reduce((sum, day) => sum + (day.participantCount || 0), 0);
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
>
|
||||
<div class="training-date">{{ formatDate(training.date) }}</div>
|
||||
<div class="training-activity">{{ training.activityName }}</div>
|
||||
<div class="training-time">{{ training.startTime }} - {{ training.endTime }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!member.trainingDetails || member.trainingDetails.length === 0" class="no-trainings">
|
||||
@@ -188,11 +187,6 @@ export default {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.training-time {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.no-trainings {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
@@ -219,4 +213,3 @@ export default {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -795,10 +795,14 @@
|
||||
"averageParticipationHalfYear": "Durchschnittliche Teilnahme (Halbjahr)",
|
||||
"averageParticipationYear": "Durchschnittliche Teilnahme (Jahr)",
|
||||
"trainingDays": "Trainingstage (letzte 12 Monate)",
|
||||
"trainingDayFilter": "Trainingstag",
|
||||
"allTrainingDays": "Alle Trainingstage",
|
||||
"memberParticipations": "Mitglieder-Teilnahmen",
|
||||
"date": "Datum",
|
||||
"weekday": "Wochentag",
|
||||
"participants": "Teilnehmer",
|
||||
"attendingMembers": "Anwesende Mitglieder",
|
||||
"noParticipants": "Keine Teilnehmer",
|
||||
"name": "Name",
|
||||
"ttr": "TTR",
|
||||
"qttr": "QTTR",
|
||||
|
||||
@@ -1098,10 +1098,14 @@
|
||||
"averageParticipationHalfYear": "Durchschnittliche Teilnahme (Halbjahr)",
|
||||
"averageParticipationYear": "Durchschnittliche Teilnahme (Jahr)",
|
||||
"trainingDays": "Trainingstage (letzte 12 Monate)",
|
||||
"trainingDayFilter": "Trainingstag",
|
||||
"allTrainingDays": "Alle Trainingstage",
|
||||
"memberParticipations": "Mitglieder-Teilnahmen",
|
||||
"date": "Datum",
|
||||
"weekday": "Wochentag",
|
||||
"participants": "Teilnehmer",
|
||||
"attendingMembers": "Anwesende Mitglieder",
|
||||
"noParticipants": "Keine Teilnehmer",
|
||||
"name": "Name",
|
||||
"ttr": "TTR",
|
||||
"qttr": "QTTR",
|
||||
@@ -1318,6 +1322,7 @@
|
||||
"clearFields": "Felder leeren",
|
||||
"playerStats": "Spieleinsätze",
|
||||
"playerStatsIntro": "Schneller Überblick über Einsätze der Mannschaft in dieser Saison.",
|
||||
"lineupProposal": "Mannschaftsmeldung nach QTTR",
|
||||
"refreshStats": "Aktualisieren",
|
||||
"loadingStats": "Lade Statistiken...",
|
||||
"noPlayerStats": "Keine Spieleinsätze erfasst.",
|
||||
|
||||
@@ -795,10 +795,14 @@
|
||||
"averageParticipationHalfYear": "Average participation (half-year)",
|
||||
"averageParticipationYear": "Average participation (year)",
|
||||
"trainingDays": "Training days (last 12 months)",
|
||||
"trainingDayFilter": "Training day",
|
||||
"allTrainingDays": "All training days",
|
||||
"memberParticipations": "Member participations",
|
||||
"date": "Date",
|
||||
"weekday": "Weekday",
|
||||
"participants": "Participants",
|
||||
"attendingMembers": "Attending members",
|
||||
"noParticipants": "No participants",
|
||||
"name": "Name",
|
||||
"ttr": "TTR",
|
||||
"qttr": "QTTR",
|
||||
|
||||
@@ -911,10 +911,14 @@
|
||||
"averageParticipationHalfYear": "Average participation (half-year)",
|
||||
"averageParticipationYear": "Average participation (year)",
|
||||
"trainingDays": "Training days (last 12 months)",
|
||||
"trainingDayFilter": "Training day",
|
||||
"allTrainingDays": "All training days",
|
||||
"memberParticipations": "Member participations",
|
||||
"date": "Date",
|
||||
"weekday": "Weekday",
|
||||
"participants": "Participants",
|
||||
"attendingMembers": "Attending members",
|
||||
"noParticipants": "No participants",
|
||||
"name": "Name",
|
||||
"ttr": "TTR",
|
||||
"qttr": "QTTR",
|
||||
@@ -1009,6 +1013,7 @@
|
||||
"clearFields": "Clear fields",
|
||||
"playerStats": "Appearances",
|
||||
"playerStatsIntro": "Quick overview of this team's appearances in the current season.",
|
||||
"lineupProposal": "Line-up by QTTR",
|
||||
"refreshStats": "Refresh",
|
||||
"loadingStats": "Loading statistics...",
|
||||
"noPlayerStats": "No appearances recorded.",
|
||||
|
||||
@@ -795,10 +795,14 @@
|
||||
"averageParticipationHalfYear": "Average participation (half-year)",
|
||||
"averageParticipationYear": "Average participation (year)",
|
||||
"trainingDays": "Training days (last 12 months)",
|
||||
"trainingDayFilter": "Training day",
|
||||
"allTrainingDays": "All training days",
|
||||
"memberParticipations": "Member participations",
|
||||
"date": "Date",
|
||||
"weekday": "Weekday",
|
||||
"participants": "Participants",
|
||||
"attendingMembers": "Attending members",
|
||||
"noParticipants": "No participants",
|
||||
"name": "Name",
|
||||
"ttr": "TTR",
|
||||
"qttr": "QTTR",
|
||||
|
||||
@@ -765,10 +765,14 @@
|
||||
"averageParticipationHalfYear": "Participación media (semestre)",
|
||||
"averageParticipationYear": "Participación media (año)",
|
||||
"trainingDays": "Días de entrenamiento (últimos 12 meses)",
|
||||
"trainingDayFilter": "Día de entrenamiento",
|
||||
"allTrainingDays": "Todos los días de entrenamiento",
|
||||
"memberParticipations": "Participaciones de los miembros",
|
||||
"date": "Fecha",
|
||||
"weekday": "Día de la semana",
|
||||
"participants": "Participantes",
|
||||
"attendingMembers": "Miembros presentes",
|
||||
"noParticipants": "Sin participantes",
|
||||
"name": "Nombre",
|
||||
"ttr": "TTR",
|
||||
"qttr": "QTTR",
|
||||
|
||||
@@ -765,10 +765,14 @@
|
||||
"averageParticipationHalfYear": "Karaniwang paglahok (kalahating taon)",
|
||||
"averageParticipationYear": "Karaniwang paglahok (taon)",
|
||||
"trainingDays": "Mga araw ng pagsasanay (huling 12 buwan)",
|
||||
"trainingDayFilter": "Araw ng pagsasanay",
|
||||
"allTrainingDays": "Lahat ng araw ng pagsasanay",
|
||||
"memberParticipations": "Paglahok ng miyembro",
|
||||
"date": "Petsa",
|
||||
"weekday": "Araw ng linggo",
|
||||
"participants": "Mga kalahok",
|
||||
"attendingMembers": "Mga dumalong miyembro",
|
||||
"noParticipants": "Walang kalahok",
|
||||
"name": "Pangalan",
|
||||
"ttr": "TTR",
|
||||
"qttr": "QTTR",
|
||||
|
||||
@@ -765,10 +765,14 @@
|
||||
"averageParticipationHalfYear": "Participation moyenne (semestre)",
|
||||
"averageParticipationYear": "Participation moyenne (année)",
|
||||
"trainingDays": "Jours d'entraînement (12 derniers mois)",
|
||||
"trainingDayFilter": "Jour d'entraînement",
|
||||
"allTrainingDays": "Tous les jours d'entraînement",
|
||||
"memberParticipations": "Participations des membres",
|
||||
"date": "Date",
|
||||
"weekday": "Jour de semaine",
|
||||
"participants": "Participants",
|
||||
"attendingMembers": "Membres présents",
|
||||
"noParticipants": "Aucun participant",
|
||||
"name": "Nom",
|
||||
"ttr": "TTR",
|
||||
"qttr": "QTTR",
|
||||
|
||||
@@ -765,10 +765,14 @@
|
||||
"averageParticipationHalfYear": "Partecipazione media (semestre)",
|
||||
"averageParticipationYear": "Partecipazione media (anno)",
|
||||
"trainingDays": "Giorni di allenamento (ultimi 12 mesi)",
|
||||
"trainingDayFilter": "Giorno di allenamento",
|
||||
"allTrainingDays": "Tutti i giorni di allenamento",
|
||||
"memberParticipations": "Partecipazioni dei membri",
|
||||
"date": "Data",
|
||||
"weekday": "Giorno della settimana",
|
||||
"participants": "Partecipanti",
|
||||
"attendingMembers": "Membri presenti",
|
||||
"noParticipants": "Nessun partecipante",
|
||||
"name": "Nome",
|
||||
"ttr": "TTR",
|
||||
"qttr": "QTTR",
|
||||
|
||||
@@ -765,10 +765,14 @@
|
||||
"averageParticipationHalfYear": "平均参加数(半年)",
|
||||
"averageParticipationYear": "平均参加数(年間)",
|
||||
"trainingDays": "練習日(過去 12 か月)",
|
||||
"trainingDayFilter": "練習日",
|
||||
"allTrainingDays": "すべての練習日",
|
||||
"memberParticipations": "メンバー参加数",
|
||||
"date": "日付",
|
||||
"weekday": "曜日",
|
||||
"participants": "参加者",
|
||||
"attendingMembers": "参加した会員",
|
||||
"noParticipants": "参加者なし",
|
||||
"name": "名前",
|
||||
"ttr": "TTR",
|
||||
"qttr": "QTTR",
|
||||
|
||||
@@ -765,10 +765,14 @@
|
||||
"averageParticipationHalfYear": "Średnia frekwencja (półrocze)",
|
||||
"averageParticipationYear": "Średnia frekwencja (rok)",
|
||||
"trainingDays": "Dni treningowe (ostatnie 12 miesięcy)",
|
||||
"trainingDayFilter": "Dzień treningowy",
|
||||
"allTrainingDays": "Wszystkie dni treningowe",
|
||||
"memberParticipations": "Udziały członków",
|
||||
"date": "Data",
|
||||
"weekday": "Dzień tygodnia",
|
||||
"participants": "Uczestnicy",
|
||||
"attendingMembers": "Obecni członkowie",
|
||||
"noParticipants": "Brak uczestników",
|
||||
"name": "Nazwa",
|
||||
"ttr": "TTR",
|
||||
"qttr": "QTTR",
|
||||
|
||||
@@ -765,10 +765,14 @@
|
||||
"averageParticipationHalfYear": "การเข้าร่วมเฉลี่ย (ครึ่งปี)",
|
||||
"averageParticipationYear": "การเข้าร่วมเฉลี่ย (ปี)",
|
||||
"trainingDays": "วันฝึกซ้อม (12 เดือนล่าสุด)",
|
||||
"trainingDayFilter": "วันฝึกซ้อม",
|
||||
"allTrainingDays": "วันฝึกซ้อมทั้งหมด",
|
||||
"memberParticipations": "การเข้าร่วมของสมาชิก",
|
||||
"date": "วันที่",
|
||||
"weekday": "วันในสัปดาห์",
|
||||
"participants": "ผู้เข้าร่วม",
|
||||
"attendingMembers": "สมาชิกที่เข้าร่วม",
|
||||
"noParticipants": "ไม่มีผู้เข้าร่วม",
|
||||
"name": "ชื่อ",
|
||||
"ttr": "TTR",
|
||||
"qttr": "QTTR",
|
||||
|
||||
@@ -765,10 +765,14 @@
|
||||
"averageParticipationHalfYear": "Karaniwang paglahok (kalahating taon)",
|
||||
"averageParticipationYear": "Karaniwang paglahok (taon)",
|
||||
"trainingDays": "Mga araw ng pagsasanay (huling 12 buwan)",
|
||||
"trainingDayFilter": "Araw ng pagsasanay",
|
||||
"allTrainingDays": "Lahat ng araw ng pagsasanay",
|
||||
"memberParticipations": "Paglahok ng miyembro",
|
||||
"date": "Petsa",
|
||||
"weekday": "Araw ng linggo",
|
||||
"participants": "Mga kalahok",
|
||||
"attendingMembers": "Mga dumalong miyembro",
|
||||
"noParticipants": "Walang kalahok",
|
||||
"name": "Pangalan",
|
||||
"ttr": "TTR",
|
||||
"qttr": "QTTR",
|
||||
|
||||
@@ -765,10 +765,14 @@
|
||||
"averageParticipationHalfYear": "平均参与人数(半年)",
|
||||
"averageParticipationYear": "平均参与人数(全年)",
|
||||
"trainingDays": "训练日(近 12 个月)",
|
||||
"trainingDayFilter": "训练日",
|
||||
"allTrainingDays": "所有训练日",
|
||||
"memberParticipations": "成员参与次数",
|
||||
"date": "日期",
|
||||
"weekday": "星期",
|
||||
"participants": "参与者",
|
||||
"attendingMembers": "出席成员",
|
||||
"noParticipants": "无参与者",
|
||||
"name": "姓名",
|
||||
"ttr": "TTR",
|
||||
"qttr": "QTTR",
|
||||
|
||||
@@ -646,8 +646,6 @@ export default {
|
||||
|
||||
const allMembers = response.data;
|
||||
|
||||
// Filter members by age class if league has age class info
|
||||
// For now, show all active members
|
||||
const activeMembers = allMembers.filter(m => m.active);
|
||||
|
||||
this.playerSelectionDialog.members = activeMembers.map(m => ({
|
||||
|
||||
@@ -183,6 +183,37 @@
|
||||
</tr>
|
||||
</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 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">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>{{ t('teamManagement.player') }}</th>
|
||||
<th :title="t('teamManagement.qttr')">(Q)TTR</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(member, index) in group.members" :key="member.id">
|
||||
<td class="lineup-rank">{{ index + 1 }}</td>
|
||||
<td>{{ member.firstName }} {{ member.lastName }}</td>
|
||||
<td class="lineup-rating">{{ member.lineupRatingLabel }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="teamToEdit && activeEditorSection === 'documents'" class="workspace-section-panel advanced-settings">
|
||||
@@ -461,6 +492,7 @@ export default {
|
||||
const playerStats = ref([]);
|
||||
const loadingStats = ref(false);
|
||||
const memberById = ref({});
|
||||
const clubMembers = ref([]);
|
||||
|
||||
// Scheduler Jobs Info
|
||||
const schedulerJobs = ref({
|
||||
@@ -502,6 +534,108 @@ export default {
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
const parseLeagueAgeGroupCode = (leagueName) => {
|
||||
const source = String(leagueName || '');
|
||||
const compactMatch = source.match(/([JM])(\d{1,2})/i);
|
||||
if (compactMatch) {
|
||||
return `J${compactMatch[2]}`;
|
||||
}
|
||||
|
||||
const youthMatch = source.match(/(?:jugend|mädchen)\s*(\d{1,2})/i);
|
||||
if (youthMatch) {
|
||||
return `J${youthMatch[1]}`;
|
||||
}
|
||||
|
||||
return 'adult';
|
||||
};
|
||||
|
||||
const getMemberAgeGroupCode = (member) => {
|
||||
if (!member?.birthDate) {
|
||||
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';
|
||||
return 'adult';
|
||||
};
|
||||
|
||||
const getMemberAgeGroupLabel = (code) => {
|
||||
if (code === 'adult') return t('members.adults');
|
||||
if (code === 'unknown') return t('unknown');
|
||||
return code;
|
||||
};
|
||||
|
||||
const getMemberLineupRatingValue = (member) => {
|
||||
const qttr = Number(member?.qttr);
|
||||
if (Number.isFinite(qttr)) return qttr;
|
||||
const ttr = Number(member?.ttr);
|
||||
if (Number.isFinite(ttr)) return ttr;
|
||||
return Number.NEGATIVE_INFINITY;
|
||||
};
|
||||
|
||||
const getMemberLineupRatingLabel = (member) => {
|
||||
const rating = getMemberLineupRatingValue(member);
|
||||
return Number.isFinite(rating) ? String(rating) : '–';
|
||||
};
|
||||
|
||||
const lineupProposalGroups = computed(() => {
|
||||
const members = (clubMembers.value || []).filter((member) => member?.active);
|
||||
if (!members.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const preferredAgeGroup = parseLeagueAgeGroupCode(teamToEdit.value?.league?.name);
|
||||
const defaultOrder = ['adult', 'J19', 'J17', 'J15', 'J13', 'J11', 'unknown'];
|
||||
const groupOrder = defaultOrder.includes(preferredAgeGroup)
|
||||
? [preferredAgeGroup, ...defaultOrder.filter((entry) => entry !== preferredAgeGroup)]
|
||||
: defaultOrder;
|
||||
|
||||
const groups = new Map();
|
||||
members.forEach((member) => {
|
||||
const code = getMemberAgeGroupCode(member);
|
||||
if (!groups.has(code)) {
|
||||
groups.set(code, {
|
||||
code,
|
||||
label: getMemberAgeGroupLabel(code),
|
||||
members: []
|
||||
});
|
||||
}
|
||||
groups.get(code).members.push({
|
||||
...member,
|
||||
lineupRatingLabel: getMemberLineupRatingLabel(member)
|
||||
});
|
||||
});
|
||||
|
||||
return Array.from(groups.values())
|
||||
.map((group) => ({
|
||||
...group,
|
||||
members: [...group.members].sort((a, b) => {
|
||||
const ratingDiff = getMemberLineupRatingValue(b) - getMemberLineupRatingValue(a);
|
||||
if (ratingDiff !== 0) return ratingDiff;
|
||||
const lastNameDiff = String(a.lastName || '').localeCompare(String(b.lastName || ''), 'de', { sensitivity: 'base' });
|
||||
if (lastNameDiff !== 0) return lastNameDiff;
|
||||
return String(a.firstName || '').localeCompare(String(b.firstName || ''), 'de', { sensitivity: 'base' });
|
||||
})
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
const indexA = groupOrder.indexOf(a.code);
|
||||
const indexB = groupOrder.indexOf(b.code);
|
||||
const safeA = indexA >= 0 ? indexA : groupOrder.length;
|
||||
const safeB = indexB >= 0 ? indexB : groupOrder.length;
|
||||
return safeA - safeB;
|
||||
});
|
||||
});
|
||||
const lineupProposalMemberCount = computed(() => lineupProposalGroups.value.reduce((sum, group) => sum + group.members.length, 0));
|
||||
|
||||
// Methods
|
||||
const toggleNewTeam = () => {
|
||||
@@ -1240,11 +1374,13 @@ export default {
|
||||
try {
|
||||
const membersResp = await apiClient.get(`/clubmembers/get/${selectedClub.value}/true`);
|
||||
const map = {};
|
||||
for (const m of membersResp.data || []) {
|
||||
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 = {};
|
||||
}
|
||||
|
||||
@@ -1450,6 +1586,8 @@ export default {
|
||||
teamsWithoutLeagueCount,
|
||||
totalSeasonAppearances,
|
||||
totalHalfAppearances,
|
||||
lineupProposalGroups,
|
||||
lineupProposalMemberCount,
|
||||
toggleNewTeam,
|
||||
resetToNewTeam,
|
||||
resetNewTeam,
|
||||
@@ -2798,6 +2936,64 @@ export default {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.lineup-proposal-card {
|
||||
margin-top: 1rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.lineup-proposal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.85rem 1rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.lineup-proposal-groups {
|
||||
display: grid;
|
||||
gap: 0.9rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.lineup-proposal-group {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-small);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.lineup-proposal-group-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.7rem 0.9rem;
|
||||
background: rgba(47, 122, 95, 0.08);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.lineup-proposal-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.lineup-proposal-table th,
|
||||
.lineup-proposal-table td {
|
||||
padding: 0.55rem 0.75rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.lineup-proposal-table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.lineup-rank,
|
||||
.lineup-rating {
|
||||
width: 90px;
|
||||
text-align: center;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* Legacy styles (can be removed if not used elsewhere) */
|
||||
.mytischtennis-header {
|
||||
display: flex;
|
||||
|
||||
@@ -10,6 +10,13 @@
|
||||
<option v-for="option in weekdayOptions" :key="option.value" :value="option.value">{{ option.label }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="stats-filter">
|
||||
<span>{{ $t('trainingStats.trainingDayFilter') }}</span>
|
||||
<select v-model="selectedTrainingDay">
|
||||
<option value="all">{{ $t('trainingStats.allTrainingDays') }}</option>
|
||||
<option v-for="option in trainingDayOptions" :key="option.value" :value="option.value">{{ option.label }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="stats-filter">
|
||||
<span>Trainingsgruppe</span>
|
||||
<select v-model="selectedTrainingGroup">
|
||||
@@ -173,6 +180,7 @@
|
||||
<th>{{ $t('trainingStats.date') }}</th>
|
||||
<th>{{ $t('trainingStats.weekday') }}</th>
|
||||
<th>{{ $t('trainingStats.participants') }}</th>
|
||||
<th>{{ $t('trainingStats.attendingMembers') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -180,6 +188,18 @@
|
||||
<td>{{ formatDate(day.date) }}</td>
|
||||
<td>{{ getWeekday(day.date) }}</td>
|
||||
<td>{{ day.participantCount }}</td>
|
||||
<td>
|
||||
<div v-if="day.participants && day.participants.length" class="training-day-members">
|
||||
<span
|
||||
v-for="participant in day.participants"
|
||||
:key="participant.id"
|
||||
class="training-day-member-chip"
|
||||
>
|
||||
{{ participant.firstName }} {{ participant.lastName }}
|
||||
</span>
|
||||
</div>
|
||||
<span v-else class="training-day-empty">{{ $t('trainingStats.noParticipants') }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -323,17 +343,7 @@ export default {
|
||||
return Array.from(groups.values()).sort((a, b) => a.label.localeCompare(b.label, 'de'));
|
||||
},
|
||||
|
||||
filteredMembers() {
|
||||
if (this.selectedTrainingGroup === 'all') {
|
||||
return this.activeMembers;
|
||||
}
|
||||
|
||||
return this.activeMembers.filter((member) =>
|
||||
(member.trainingGroups || []).some((group) => String(group.id) === this.selectedTrainingGroup)
|
||||
);
|
||||
},
|
||||
|
||||
filteredTrainingDays() {
|
||||
trainingDaysByWeekday() {
|
||||
if (this.selectedWeekday === 'all') {
|
||||
return this.trainingDays;
|
||||
}
|
||||
@@ -341,6 +351,50 @@ export default {
|
||||
return this.trainingDays.filter((day) => String(new Date(day.date).getDay()) === this.selectedWeekday);
|
||||
},
|
||||
|
||||
trainingDayOptions() {
|
||||
return this.trainingDaysByWeekday.map((day) => ({
|
||||
value: String(day.id),
|
||||
label: `${this.formatDate(day.date)} (${this.getWeekday(day.date)})`,
|
||||
}));
|
||||
},
|
||||
|
||||
selectedTrainingDayParticipantIds() {
|
||||
if (this.selectedTrainingDay === 'all') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selectedDay = this.trainingDays.find((day) => String(day.id) === String(this.selectedTrainingDay));
|
||||
if (!selectedDay || !Array.isArray(selectedDay.participants)) {
|
||||
return new Set();
|
||||
}
|
||||
|
||||
return new Set(selectedDay.participants.map((participant) => Number(participant.id)).filter((id) => Number.isFinite(id)));
|
||||
},
|
||||
|
||||
filteredMembers() {
|
||||
let members = this.activeMembers;
|
||||
|
||||
if (this.selectedTrainingGroup !== 'all') {
|
||||
members = members.filter((member) =>
|
||||
(member.trainingGroups || []).some((group) => String(group.id) === this.selectedTrainingGroup)
|
||||
);
|
||||
}
|
||||
|
||||
if (this.selectedTrainingDayParticipantIds) {
|
||||
members = members.filter((member) => this.selectedTrainingDayParticipantIds.has(Number(member.id)));
|
||||
}
|
||||
|
||||
return members;
|
||||
},
|
||||
|
||||
filteredTrainingDays() {
|
||||
if (this.selectedTrainingDay === 'all') {
|
||||
return this.trainingDaysByWeekday;
|
||||
}
|
||||
|
||||
return this.trainingDaysByWeekday.filter((day) => String(day.id) === String(this.selectedTrainingDay));
|
||||
},
|
||||
|
||||
filteredOverview() {
|
||||
const totalParticipants = this.filteredTrainingDays.reduce((sum, day) => sum + (day.participantCount || 0), 0);
|
||||
const averageParticipants = this.filteredTrainingDays.length > 0 ? totalParticipants / this.filteredTrainingDays.length : 0;
|
||||
@@ -515,6 +569,7 @@ export default {
|
||||
inactive: 0
|
||||
},
|
||||
selectedWeekday: 'all',
|
||||
selectedTrainingDay: 'all',
|
||||
selectedTrainingGroup: 'all',
|
||||
showDetailsModal: false,
|
||||
selectedMember: {},
|
||||
@@ -540,6 +595,13 @@ export default {
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
selectedWeekday() {
|
||||
if (this.selectedTrainingDay === 'all') return;
|
||||
const hasSelectedDay = this.trainingDaysByWeekday.some((day) => String(day.id) === String(this.selectedTrainingDay));
|
||||
if (!hasSelectedDay) {
|
||||
this.selectedTrainingDay = 'all';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -939,6 +1001,28 @@ export default {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.training-day-members {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.training-day-member-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.2rem 0.55rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(26, 130, 172, 0.12);
|
||||
color: var(--text-color);
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.training-day-empty {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.members-table th {
|
||||
background: var(--bg-light);
|
||||
padding: 1rem;
|
||||
|
||||
Reference in New Issue
Block a user