feat: add player cancellation handling and member deactivation tracking
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 57s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 57s
This commit is contained in:
@@ -382,7 +382,8 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('schedule.player') }}</th>
|
||||
<th>{{ $t('schedule.ready') }}</th>
|
||||
<th v-if="!playerSelectionDialog.match?.isFriendly">Rückmeldung</th>
|
||||
<th v-else>{{ $t('schedule.ready') }}</th>
|
||||
<th>{{ $t('schedule.planned') }}</th>
|
||||
<th>{{ $t('schedule.played') }}</th>
|
||||
<th>Beobachtung</th>
|
||||
@@ -393,11 +394,22 @@
|
||||
<td class="player-name">
|
||||
{{ member.firstName }} {{ member.lastName }}
|
||||
</td>
|
||||
<td class="checkbox-cell">
|
||||
<td v-if="!playerSelectionDialog.match?.isFriendly" class="checkbox-cell">
|
||||
<select
|
||||
:value="playerResponseValue(member)"
|
||||
:disabled="playerSelectionDialog.readonly"
|
||||
@change="setPlayerResponse(member, $event.target.value)"
|
||||
>
|
||||
<option value="">Keine Auswahl</option>
|
||||
<option value="ready">Will spielen</option>
|
||||
<option value="cancelled">Absage</option>
|
||||
</select>
|
||||
</td>
|
||||
<td v-else class="checkbox-cell">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="member.isReady"
|
||||
:disabled="playerSelectionDialog.readonly"
|
||||
:disabled="playerSelectionDialog.readonly || member.isCancelled"
|
||||
@change="togglePlayerReady(member)"
|
||||
/>
|
||||
</td>
|
||||
@@ -405,7 +417,7 @@
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="member.isPlanned"
|
||||
:disabled="playerSelectionDialog.readonly"
|
||||
:disabled="playerSelectionDialog.readonly || member.isCancelled"
|
||||
@change="togglePlayerPlanned(member)"
|
||||
/>
|
||||
</td>
|
||||
@@ -413,7 +425,7 @@
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="member.hasPlayed"
|
||||
:disabled="playerSelectionDialog.readonly"
|
||||
:disabled="playerSelectionDialog.readonly || member.isCancelled"
|
||||
@change="togglePlayerPlayed(member)"
|
||||
/>
|
||||
</td>
|
||||
@@ -1355,11 +1367,11 @@ export default {
|
||||
|
||||
try {
|
||||
const normalizePlayersList = (value) => {
|
||||
if (Array.isArray(value)) return value;
|
||||
if (Array.isArray(value)) return value.map(Number).filter(Number.isFinite);
|
||||
if (typeof value === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
return Array.isArray(parsed) ? parsed.map(Number).filter(Number.isFinite) : [];
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
@@ -1369,7 +1381,7 @@ export default {
|
||||
const readyIds = normalizePlayersList(match.playersReady);
|
||||
const plannedIds = normalizePlayersList(match.playersPlanned);
|
||||
const playedIds = normalizePlayersList(match.playersPlayed);
|
||||
const preselectedIds = Array.from(new Set([...readyIds, ...plannedIds, ...playedIds]));
|
||||
const cancelledIds = normalizePlayersList(match.playersCancelled);
|
||||
|
||||
let allMembers = [];
|
||||
if (match.isFriendly && match.isSharedFriendly) {
|
||||
@@ -1391,12 +1403,11 @@ export default {
|
||||
.map((id) => Number(id))
|
||||
.filter((id) => Number.isFinite(id))
|
||||
);
|
||||
const visibleMembers = allowedIds.size > 0
|
||||
? activeMembers.filter((member) => allowedIds.has(Number(member.id)))
|
||||
: activeMembers;
|
||||
const visibleMembers = match.isFriendly
|
||||
? activeMembers
|
||||
: activeMembers.filter((member) => allowedIds.has(Number(member.id)));
|
||||
|
||||
// Keep the order defined in the team line-up. Members from lower teams
|
||||
// (which are also eligible) follow afterwards in their own line-up order.
|
||||
// Die Reihenfolge entspricht der Mannschaftsmeldung der ausgewählten Mannschaft.
|
||||
const lineupPositionByMemberId = new Map(
|
||||
eligibleMemberIds.map((id, index) => [Number(id), index])
|
||||
);
|
||||
@@ -1419,12 +1430,16 @@ export default {
|
||||
return la.localeCompare(lb);
|
||||
});
|
||||
|
||||
this.playerSelectionDialog.members = sortedVisibleMembers.map(m => ({
|
||||
...m,
|
||||
isReady: readyIds.includes(m.id) || false,
|
||||
isPlanned: plannedIds.includes(m.id) || false,
|
||||
hasPlayed: playedIds.includes(m.id) || false
|
||||
}));
|
||||
this.playerSelectionDialog.members = sortedVisibleMembers.map(m => {
|
||||
const isCancelled = cancelledIds.includes(m.id);
|
||||
return {
|
||||
...m,
|
||||
isReady: !isCancelled && readyIds.includes(m.id),
|
||||
isPlanned: !isCancelled && plannedIds.includes(m.id),
|
||||
hasPlayed: !isCancelled && playedIds.includes(m.id),
|
||||
isCancelled
|
||||
};
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading members:', error);
|
||||
@@ -1452,8 +1467,9 @@ export default {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Niedrigere Mannschaften = ausgewaehltes Team + alle dahinter in der sortierten Teamliste
|
||||
const candidateTeams = this.teams.slice(selectedIndex);
|
||||
// Die Auswahl enthält ausschließlich die für diese Mannschaft gemeldeten
|
||||
// Spieler – keine Spieler aus anderen Jugendmannschaften.
|
||||
const candidateTeams = [this.teams[selectedIndex]];
|
||||
const lineupResponses = await Promise.all(candidateTeams.map(async (team) => {
|
||||
try {
|
||||
const response = await apiClient.get(`/club-teams/${team.id}/lineup`, {
|
||||
@@ -1535,6 +1551,19 @@ export default {
|
||||
togglePlayerReady(member) {
|
||||
member.isReady = !member.isReady;
|
||||
},
|
||||
playerResponseValue(member) {
|
||||
if (member.isCancelled) return 'cancelled';
|
||||
return member.isReady ? 'ready' : '';
|
||||
},
|
||||
setPlayerResponse(member, value) {
|
||||
member.isReady = value === 'ready';
|
||||
member.isCancelled = value === 'cancelled';
|
||||
if (member.isCancelled) {
|
||||
member.isReady = false;
|
||||
member.isPlanned = false;
|
||||
member.hasPlayed = false;
|
||||
}
|
||||
},
|
||||
|
||||
togglePlayerPlanned(member) {
|
||||
member.isPlanned = !member.isPlanned;
|
||||
@@ -1565,24 +1594,29 @@ export default {
|
||||
...normalizePlayersList(existing).filter((id) => !visibleIds.has(Number(id))),
|
||||
...this.playerSelectionDialog.members.filter(predicate).map((m) => Number(m.id)),
|
||||
].filter((id, index, arr) => Number.isFinite(id) && arr.indexOf(id) === index);
|
||||
const playersReady = mergeVisibleSelection(match.playersReady, (m) => m.isReady);
|
||||
const playersPlanned = mergeVisibleSelection(match.playersPlanned, (m) => m.isPlanned);
|
||||
const playersPlayed = mergeVisibleSelection(match.playersPlayed, (m) => m.hasPlayed);
|
||||
const playersCancelled = mergeVisibleSelection(match.playersCancelled, (m) => m.isCancelled);
|
||||
const cancelledIds = new Set(playersCancelled.map(String));
|
||||
const withoutCancelled = (ids) => ids.filter((id) => !cancelledIds.has(String(id)));
|
||||
const playersReady = withoutCancelled(mergeVisibleSelection(match.playersReady, (m) => m.isReady));
|
||||
const playersPlanned = withoutCancelled(mergeVisibleSelection(match.playersPlanned, (m) => m.isPlanned));
|
||||
const playersPlayed = withoutCancelled(mergeVisibleSelection(match.playersPlayed, (m) => m.hasPlayed));
|
||||
|
||||
console.log('[savePlayerSelection] Saving players:', { playersReady, playersPlanned, playersPlayed, matchId: match.id });
|
||||
console.log('[savePlayerSelection] Saving players:', { playersReady, playersPlanned, playersPlayed, playersCancelled, matchId: match.id });
|
||||
|
||||
try {
|
||||
const response = match.isFriendly
|
||||
? await apiClient.patch(`${match.isSharedFriendly ? `/friendly-matches/shared/${this.currentClub}/${match.id}/players` : `/friendly-matches/${this.currentClub}/${match.id}/players`}`, {
|
||||
playersReady,
|
||||
playersPlanned,
|
||||
playersPlayed
|
||||
playersPlayed,
|
||||
playersCancelled
|
||||
})
|
||||
: await apiClient.patch(`/matches/${match.id}/players`, {
|
||||
clubId: this.currentClub,
|
||||
playersReady,
|
||||
playersPlanned,
|
||||
playersPlayed
|
||||
playersPlayed,
|
||||
playersCancelled
|
||||
});
|
||||
if (response.status >= 400) {
|
||||
throw new Error(response?.data?.error || 'Failed to update match players');
|
||||
@@ -1593,12 +1627,14 @@ export default {
|
||||
match.playersReady = playersReady;
|
||||
match.playersPlanned = playersPlanned;
|
||||
match.playersPlayed = playersPlayed;
|
||||
match.playersCancelled = playersCancelled;
|
||||
|
||||
// Update all members in the list to reflect the current state
|
||||
this.playerSelectionDialog.members.forEach(m => {
|
||||
m.isReady = playersReady.includes(m.id);
|
||||
m.isPlanned = playersPlanned.includes(m.id);
|
||||
m.hasPlayed = playersPlayed.includes(m.id);
|
||||
m.isCancelled = playersCancelled.includes(m.id);
|
||||
});
|
||||
|
||||
if (closeDialog) {
|
||||
@@ -3798,7 +3834,7 @@ li {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 20px 0;
|
||||
min-width: 480px;
|
||||
min-width: 620px;
|
||||
}
|
||||
|
||||
.player-selection-table th,
|
||||
@@ -3811,9 +3847,13 @@ li {
|
||||
}
|
||||
|
||||
.player-selection-table th {
|
||||
background-color: #f8f9fa;
|
||||
background-color: var(--primary-color, #218838);
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.player-selection-table select {
|
||||
min-width: 130px;
|
||||
}
|
||||
|
||||
.player-selection-table tbody tr:hover {
|
||||
|
||||
@@ -439,9 +439,12 @@ export default {
|
||||
filteredOverview() {
|
||||
const totalParticipants = this.filteredTrainingDays.reduce((sum, day) => sum + (day.participantCount || 0), 0);
|
||||
const averageParticipants = this.filteredTrainingDays.length > 0 ? totalParticipants / this.filteredTrainingDays.length : 0;
|
||||
const denominatorMembers = this.filteredMembers.length || 0;
|
||||
const attendanceRate = denominatorMembers > 0 && this.filteredTrainingDays.length > 0
|
||||
? (totalParticipants / (denominatorMembers * this.filteredTrainingDays.length)) * 100
|
||||
const availableMemberSlots = this.filteredTrainingDays.reduce(
|
||||
(sum, day) => sum + Math.max(0, day.nonDeactivatedMemberCount || 0),
|
||||
0
|
||||
);
|
||||
const attendanceRate = availableMemberSlots > 0
|
||||
? (totalParticipants / availableMemberSlots) * 100
|
||||
: 0;
|
||||
const bestTrainingDay = this.filteredTrainingDays.reduce((best, day) => {
|
||||
if (!best || (day.participantCount || 0) > (best.participantCount || 0)) {
|
||||
|
||||
Reference in New Issue
Block a user