feat(ScheduleView): enhance player selection dialog with saving state management
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 59s

This commit is contained in:
Torsten Schulz (local)
2026-09-23 09:34:18 +02:00
parent 1fb7afc951
commit f5adf95e23
2 changed files with 96 additions and 22 deletions

View File

@@ -1088,12 +1088,6 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
// Spezielle Behandlung für Braunschweiger System
if (matrixConfig && typeof matrixConfig === 'object' && matrixConfig.matrices) {
const serverMatrix = this.getBraunschweigerServerMatrix();
if (Array.isArray(serverMatrix) && serverMatrix.length > 0) {
console.log('🎯 Braunschweiger System: verwende Server-Matrix', serverMatrix);
return serverMatrix;
}
const homePlayerCount = this.getEffectivePlayerCount('home');
const guestPlayerCount = this.getEffectivePlayerCount('guest');
@@ -1103,8 +1097,22 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
console.log(`🎯 Braunschweiger System: ${homePlayerCount} vs ${guestPlayerCount} → Key: ${playerCountKey}`);
this.logBraunschweigerDebugSnapshot(`Matrix-Auswahl ${playerCountKey}`);
// Verwende spezifische Matrix oder Fallback
return matrixConfig.matrices[playerCountKey] || matrixConfig.default || [];
// Die Matrix muss sich immer nach der aktuell gewählten
// Aufstellung richten. NuScore liefert hier gelegentlich noch
// die 4:4-Formation, obwohl eine Mannschaft nur zu zweit
// antritt. Zwei Spieler werden dabei regelkonform als
// 3er-Konstellation behandelt: 4:2 -> 4:3, 2:4 -> 3:4,
// 3:2/2:3/2:2 -> 3:3. Die dadurch fehlenden Einzel werden
// anschließend automatisch als Nichtantreten gewertet.
const localMatrix = matrixConfig.matrices[playerCountKey];
if (Array.isArray(localMatrix) && localMatrix.length > 0) {
return localMatrix;
}
// Nur für bislang unbekannte Konstellationen auf die
// Serverformation ausweichen.
const serverMatrix = this.getBraunschweigerServerMatrix();
return serverMatrix || matrixConfig.default || [];
}
// Standard-Verhalten für andere Systeme
@@ -2426,6 +2434,11 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
// (etwa 5:3) umstellen.
matchData.playerCountHome = this.getSinglePlayerCount('home');
matchData.playerCountGuest = this.getSinglePlayerCount('guest');
// Die Begegnungsreihenfolge gehört zum Aufstellungsstand.
// Insbesondere bei 4:2 darf kein veralteter 4:4-Entwurf mit
// einem zweiten Doppel an NuScore zurückgehen.
this.applyBraunschweigerMatchFormations(matchData);
// 1.1 Lokalen Aufstellungszustand in den Payload übernehmen
this.applyCurrentLineupStateToPayload(matchData);
@@ -3572,9 +3585,9 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
this.isDraftSyncing = false;
this.lastDraftSyncError = '';
// Den offiziellen Stand nur als Quelle für Teams, Aufstellung und
// Spielsystem laden. Ergebnisse, Abschluss und Signaturen dürfen
// im Testmodus nie übernommen werden.
// Den offiziellen Stand nur als Quelle für Teams, Spielerliste
// und Spielsystem laden. Der Testmodus startet anschließend mit
// einer vollständig leeren lokalen Eingabe.
await this.loadData();
this.resetTestModeState();
},
@@ -3593,11 +3606,31 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
data.signature = {};
});
// Spieler bleiben als auswählbare Liste erhalten; ihre
// Aufstellung (Einzel und Doppel) beginnt im Testmodus dagegen
// immer leer. Damit lässt sich ein Spiel wirklich von vorne
// durchspielen, ohne Daten an NuScore zu verändern.
['teamLineupHomePlayers', 'teamLineupGuestPlayers'].forEach((teamKey) => {
(this.meetingDetails[teamKey] || []).forEach((player) => {
player.isSelected = false;
player.positionSingle = null;
player.positionDouble = null;
player.positions = [];
});
});
// Die vom Server gespeicherten Ergebnisse komplett verwerfen. Die
// leeren Zeilen werden anschließend aus der Spielmatrix erzeugt.
this.meetingDetails.matches = [];
this.results = [];
this.teamNotAppeared = null;
this.homePin = '';
this.guestPin = '';
this.finalHomePin = '';
this.finalGuestPin = '';
this.protestText = '';
this.match.startDate = null;
this.match.endDate = null;
this.isHomeLineupCertified = false;
this.isGuestLineupCertified = false;
this.isGreetingCompleted = false;
@@ -5027,6 +5060,38 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
return trimmedPosition;
},
applyBraunschweigerMatchFormations(target) {
if (!target || !String(this.getPlayMode() || '').toLowerCase().includes('braunschweiger')) {
return;
}
const formations = (this.currentPlayModeMatrix || []).map((label, index) => {
const [homeToken = '', guestToken = ''] = String(label).split('–').map(token => token.trim());
const toNuScorePosition = (token) => token
.replace(/^DA/i, 'D')
.replace(/^DB/i, 'D')
.replace(/^A/i, 'E')
.replace(/^B/i, 'E');
const homeLineupPosition = toNuScorePosition(homeToken);
const guestLineupPosition = toNuScorePosition(guestToken);
return {
matchNr: index + 1,
displayString: `${homeLineupPosition}-${guestLineupPosition}`,
homeLineupPosition,
guestLineupPosition
};
});
if (formations.length === 0) {
return;
}
target.meetingPlayMode = {
...(target.meetingPlayMode && typeof target.meetingPlayMode === 'object' ? target.meetingPlayMode : {}),
matchFormations: formations
};
},
getBraunschweigerServerMatrix() {
const mapFormationsToLabels = (formations) => {
if (!Array.isArray(formations) || formations.length === 0) {

View File

@@ -397,7 +397,7 @@
<td v-if="!playerSelectionDialog.match?.isFriendly" class="checkbox-cell">
<select
:value="playerResponseValue(member)"
:disabled="playerSelectionDialog.readonly"
:disabled="playerSelectionDialog.readonly || playerSelectionDialog.saving"
@change="setPlayerResponse(member, $event.target.value)"
>
<option value="">Keine Auswahl</option>
@@ -409,7 +409,7 @@
<input
type="checkbox"
:checked="member.isReady"
:disabled="playerSelectionDialog.readonly || member.isCancelled"
:disabled="playerSelectionDialog.readonly || playerSelectionDialog.saving || member.isCancelled"
@change="togglePlayerReady(member)"
/>
</td>
@@ -417,7 +417,7 @@
<input
type="checkbox"
:checked="member.isPlanned"
:disabled="playerSelectionDialog.readonly || member.isCancelled || !member.isReady"
:disabled="playerSelectionDialog.readonly || playerSelectionDialog.saving || member.isCancelled || !member.isReady"
@change="togglePlayerPlanned(member)"
/>
</td>
@@ -425,7 +425,7 @@
<input
type="checkbox"
:checked="member.hasPlayed"
:disabled="playerSelectionDialog.readonly || member.isCancelled || !member.isReady || !member.isPlanned"
:disabled="playerSelectionDialog.readonly || playerSelectionDialog.saving || member.isCancelled || !member.isReady || !member.isPlanned"
@change="togglePlayerPlayed(member)"
/>
</td>
@@ -440,8 +440,7 @@
</div>
<div class="dialog-actions">
<button v-if="!playerSelectionDialog.readonly" @click="savePlayerSelection" class="btn-save">{{ $t('schedule.save') }}</button>
<button @click="closePlayerSelectionDialog" class="btn-cancel">{{ $t('schedule.cancel') }}</button>
<button @click="closePlayerSelectionDialog" class="btn-cancel">{{ $t('common.close') }}</button>
</div>
</div>
</BaseDialog>
@@ -941,6 +940,7 @@ export default {
match: null,
members: [],
loading: false,
saving: false,
readonly: false
},
matchObservationDialog: { isOpen: false, member: null, notes: [] },
@@ -1364,6 +1364,7 @@ export default {
this.playerSelectionDialog.readonly = this.isFriendlyMatchReadOnly(match);
this.playerSelectionDialog.isOpen = true;
this.playerSelectionDialog.loading = true;
this.playerSelectionDialog.saving = false;
try {
const normalizePlayersList = (value) => {
@@ -1500,6 +1501,7 @@ export default {
this.playerSelectionDialog.isOpen = false;
this.playerSelectionDialog.match = null;
this.playerSelectionDialog.members = [];
this.playerSelectionDialog.saving = false;
},
async openMatchObservation(member) {
if (!member?.id) return;
@@ -1548,36 +1550,40 @@ export default {
this.locationDialog.match = null;
},
togglePlayerReady(member) {
async togglePlayerReady(member) {
member.isReady = !member.isReady;
await this.savePlayerSelection(false);
},
playerResponseValue(member) {
if (member.isCancelled) return 'cancelled';
return member.isReady ? 'ready' : '';
},
setPlayerResponse(member, value) {
async setPlayerResponse(member, value) {
member.isReady = value === 'ready';
member.isCancelled = value === 'cancelled';
if (!member.isReady) {
member.isPlanned = false;
member.hasPlayed = false;
}
await this.savePlayerSelection(false);
},
togglePlayerPlanned(member) {
async togglePlayerPlanned(member) {
member.isPlanned = !member.isPlanned;
if (!member.isPlanned) {
member.hasPlayed = false;
}
await this.savePlayerSelection(false);
},
togglePlayerPlayed(member) {
async togglePlayerPlayed(member) {
member.hasPlayed = !member.hasPlayed;
await this.savePlayerSelection(false);
},
async savePlayerSelection(closeDialog = true) {
const match = this.playerSelectionDialog.match;
if (!match || this.isFriendlyMatchReadOnly(match)) return;
if (!match || this.isFriendlyMatchReadOnly(match) || this.playerSelectionDialog.saving) return;
const normalizePlayersList = (value) => {
if (Array.isArray(value)) return value.map((id) => Number(id)).filter((id) => Number.isFinite(id));
@@ -1606,6 +1612,7 @@ export default {
console.log('[savePlayerSelection] Saving players:', { playersReady, playersPlanned, playersPlayed, playersCancelled, matchId: match.id });
try {
this.playerSelectionDialog.saving = true;
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,
@@ -1646,6 +1653,8 @@ export default {
} catch (error) {
console.error('Error saving player selection:', error);
await this.showInfo(this.$t('messages.error'), this.$t('schedule.errorSavingPlayerSelection'), '', 'error');
} finally {
this.playerSelectionDialog.saving = false;
}
},