From f5adf95e23a01e2eecb379cee842efae33976c99 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Wed, 23 Sep 2026 09:34:18 +0200 Subject: [PATCH] feat(ScheduleView): enhance player selection dialog with saving state management --- .../src/components/MatchReportApiDialog.vue | 87 ++++++++++++++++--- frontend/src/views/ScheduleView.vue | 31 ++++--- 2 files changed, 96 insertions(+), 22 deletions(-) diff --git a/frontend/src/components/MatchReportApiDialog.vue b/frontend/src/components/MatchReportApiDialog.vue index c8e47261..92f79768 100755 --- a/frontend/src/components/MatchReportApiDialog.vue +++ b/frontend/src/components/MatchReportApiDialog.vue @@ -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) { diff --git a/frontend/src/views/ScheduleView.vue b/frontend/src/views/ScheduleView.vue index 4ae0c1d5..a6e153bd 100755 --- a/frontend/src/views/ScheduleView.vue +++ b/frontend/src/views/ScheduleView.vue @@ -397,7 +397,7 @@ @@ -417,7 +417,7 @@ @@ -425,7 +425,7 @@ @@ -440,8 +440,7 @@
- - +
@@ -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; } },