feat(MatchReportApiDialog): refactor double position selection for players and enhance result table layout
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 58s

This commit is contained in:
Torsten Schulz (local)
2026-10-02 22:47:00 +02:00
parent f07415b877
commit 6fa54163f6

View File

@@ -307,29 +307,12 @@
<div class="player-action">👆 {{ $t('matchReportApi.clickToRemove') }}</div>
</button>
<div class="double-selection">
<button class="double-btn d1-btn"
:class="{ active: player.positionDouble === 1 }"
@click="toggleDoublePosition(player, 'home', 1)"
:disabled="!canSelectDoublePosition(player, 'home', 1)">
D1
</button>
<button class="double-btn d2-btn"
:class="{ active: player.positionDouble === 2 }"
@click="toggleDoublePosition(player, 'home', 2)"
:disabled="!canSelectDoublePosition(player, 'home', 2)">
D2
</button>
<button v-if="meetingDetails.teamLineupDoublePositions.includes(3)" class="double-btn"
:class="{ active: player.positionDouble === 3 }"
@click="toggleDoublePosition(player, 'home', 3)"
:disabled="!canSelectDoublePosition(player, 'home', 3)">
D3
</button>
<button v-if="meetingDetails.teamLineupDoublePositions.includes(3)" class="double-btn"
:class="{ active: player.positionDouble === 3 }"
@click="toggleDoublePosition(player, 'home', 3)"
:disabled="!canSelectDoublePosition(player, 'home', 3)">
D3
<button v-for="doublePosition in getDoublePositions()" :key="`home-${player.nuLigaPersonId}-${doublePosition}`"
class="double-btn"
:class="{ active: player.positionDouble === doublePosition }"
@click="toggleDoublePosition(player, 'home', doublePosition)"
:disabled="!canSelectDoublePosition(player, 'home', doublePosition)">
D{{ doublePosition }}
</button>
</div>
</div>
@@ -355,17 +338,12 @@
<div class="player-action">👆 Klicken zum Hinzufügen</div>
</button>
<div class="double-selection">
<button class="double-btn d1-btn"
:class="{ active: player.positionDouble === 1 }"
@click="toggleDoublePosition(player, 'home', 1)"
:disabled="!canSelectDoublePosition(player, 'home', 1)">
D1
</button>
<button class="double-btn d2-btn"
:class="{ active: player.positionDouble === 2 }"
@click="toggleDoublePosition(player, 'home', 2)"
:disabled="!canSelectDoublePosition(player, 'home', 2)">
D2
<button v-for="doublePosition in getDoublePositions()" :key="`home-${player.nuLigaPersonId}-${doublePosition}`"
class="double-btn"
:class="{ active: player.positionDouble === doublePosition }"
@click="toggleDoublePosition(player, 'home', doublePosition)"
:disabled="!canSelectDoublePosition(player, 'home', doublePosition)">
D{{ doublePosition }}
</button>
</div>
@@ -426,29 +404,12 @@
<div class="player-action">👆 {{ $t('matchReportApi.clickToRemove') }}</div>
</button>
<div class="double-selection">
<button class="double-btn d1-btn"
:class="{ active: player.positionDouble === 1 }"
@click="toggleDoublePosition(player, 'guest', 1)"
:disabled="!canSelectDoublePosition(player, 'guest', 1)">
D1
</button>
<button class="double-btn d2-btn"
:class="{ active: player.positionDouble === 2 }"
@click="toggleDoublePosition(player, 'guest', 2)"
:disabled="!canSelectDoublePosition(player, 'guest', 2)">
D2
</button>
<button v-if="meetingDetails.teamLineupDoublePositions.includes(3)" class="double-btn"
:class="{ active: player.positionDouble === 3 }"
@click="toggleDoublePosition(player, 'guest', 3)"
:disabled="!canSelectDoublePosition(player, 'guest', 3)">
D3
</button>
<button v-if="meetingDetails.teamLineupDoublePositions.includes(3)" class="double-btn"
:class="{ active: player.positionDouble === 3 }"
@click="toggleDoublePosition(player, 'guest', 3)"
:disabled="!canSelectDoublePosition(player, 'guest', 3)">
D3
<button v-for="doublePosition in getDoublePositions()" :key="`guest-${player.nuLigaPersonId}-${doublePosition}`"
class="double-btn"
:class="{ active: player.positionDouble === doublePosition }"
@click="toggleDoublePosition(player, 'guest', doublePosition)"
:disabled="!canSelectDoublePosition(player, 'guest', doublePosition)">
D{{ doublePosition }}
</button>
</div>
</div>
@@ -474,17 +435,12 @@
<div class="player-action">👆 Klicken zum Hinzufügen</div>
</button>
<div class="double-selection">
<button class="double-btn d1-btn"
:class="{ active: player.positionDouble === 1 }"
@click="toggleDoublePosition(player, 'guest', 1)"
:disabled="!canSelectDoublePosition(player, 'guest', 1)">
D1
</button>
<button class="double-btn d2-btn"
:class="{ active: player.positionDouble === 2 }"
@click="toggleDoublePosition(player, 'guest', 2)"
:disabled="!canSelectDoublePosition(player, 'guest', 2)">
D2
<button v-for="doublePosition in getDoublePositions()" :key="`guest-${player.nuLigaPersonId}-${doublePosition}`"
class="double-btn"
:class="{ active: player.positionDouble === doublePosition }"
@click="toggleDoublePosition(player, 'guest', doublePosition)"
:disabled="!canSelectDoublePosition(player, 'guest', doublePosition)">
D{{ doublePosition }}
</button>
</div>
</div>
@@ -537,64 +493,66 @@
</div>
</div>
<table class="result-table">
<thead>
<tr>
<th>#</th>
<th>Heim</th>
<th>Gast</th>
<th>Satz 1</th>
<th>Satz 2</th>
<th>Satz 3</th>
<th>Satz 4</th>
<th>Satz 5</th>
<th>Status</th>
<th>Ergebnis</th>
</tr>
</thead>
<tbody>
<template v-for="(m, idx) in results" :key="idx">
<div class="result-table-wrap" tabindex="0" aria-label="Ergebnistabelle horizontal scrollen">
<table class="result-table">
<thead>
<tr>
<td>{{ idx + 1 }}</td>
<td>{{ displayMatchSide(m.homeName) }}</td>
<td>{{ displayMatchSide(m.guestName) }}</td>
<td v-for="(s, sIdx) in m.sets" :key="sIdx" class="set-cell">
<div class="set-input-wrapper">
<input
class="set-input"
:class="{ 'gap-warning': shouldHighlightSetInput(idx, sIdx), 'keyboard-open': isSetKeyboardOpen(idx, sIdx) }"
v-model="m.sets[sIdx]"
placeholder=":"
:readonly="true"
:disabled="isSetInputDisabled(idx, sIdx)"
@click="openSetKeyboard(idx, sIdx)"
/>
</div>
</td>
<td class="state-cell">
<div class="state-content">
<button v-if="!viewOnly && !m.completed && canAutoFillAbsentMatch(m)"
class="state-btn absent-player-btn"
@click="autoFillAbsentMatch(idx)"
title="Nicht anwesend: Begegnung automatisch werten">⚡</button>
<button v-if="!viewOnly && !m.completed" class="state-btn" @click="completeMatch(idx)" title="Match abschließen">✔</button>
<button v-else-if="!viewOnly" class="state-btn" @click="reopenMatch(idx)" title="Match wieder öffnen">✎</button>
<div class="set-ratio">{{ getSetRatio(m) }}</div>
</div>
</td>
<td class="result-cell">
<div class="match-result">{{ getMatchResult(m) }}</div>
</td>
<th>#</th>
<th>Heim</th>
<th>Gast</th>
<th>Satz 1</th>
<th>Satz 2</th>
<th>Satz 3</th>
<th>Satz 4</th>
<th>Satz 5</th>
<th>Status</th>
<th>Ergebnis</th>
</tr>
<!-- Fehlermeldung für diese Match-Zeile -->
<tr v-if="getMatchError(idx)" class="error-row">
<td colspan="10" class="error-cell">
<div class="error-message">{{ getMatchError(idx) }}</div>
</td>
</tr>
</template>
</tbody>
</table>
</thead>
<tbody>
<template v-for="(m, idx) in results" :key="idx">
<tr>
<td>{{ idx + 1 }}</td>
<td>{{ displayMatchSide(m.homeName) }}</td>
<td>{{ displayMatchSide(m.guestName) }}</td>
<td v-for="(s, sIdx) in m.sets" :key="sIdx" class="set-cell">
<div class="set-input-wrapper">
<input
class="set-input"
:class="{ 'gap-warning': shouldHighlightSetInput(idx, sIdx), 'keyboard-open': isSetKeyboardOpen(idx, sIdx) }"
v-model="m.sets[sIdx]"
placeholder=":"
:readonly="true"
:disabled="isSetInputDisabled(idx, sIdx)"
@click="openSetKeyboard(idx, sIdx)"
/>
</div>
</td>
<td class="state-cell">
<div class="state-content">
<button v-if="!viewOnly && !m.completed && canAutoFillAbsentMatch(m)"
class="state-btn absent-player-btn"
@click="autoFillAbsentMatch(idx)"
title="Nicht anwesend: Begegnung automatisch werten">⚡</button>
<button v-if="!viewOnly && !m.completed" class="state-btn" @click="completeMatch(idx)" title="Match abschließen">✔</button>
<button v-else-if="!viewOnly" class="state-btn" @click="reopenMatch(idx)" title="Match wieder öffnen">✎</button>
<div class="set-ratio">{{ getSetRatio(m) }}</div>
</div>
</td>
<td class="result-cell">
<div class="match-result">{{ getMatchResult(m) }}</div>
</td>
</tr>
<!-- Fehlermeldung für diese Match-Zeile -->
<tr v-if="getMatchError(idx)" class="error-row">
<td colspan="10" class="error-cell">
<div class="error-message">{{ getMatchError(idx) }}</div>
</td>
</tr>
</template>
</tbody>
</table>
</div>
<!-- Auto-Fill Button -->
<div v-if="!viewOnly" class="autofill-section">
@@ -2859,6 +2817,8 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
return Boolean(normalized) && ![
'nicht angegeben',
'nicht angegeben / nicht angegeben',
'nicht anwesend',
'nicht anwesend / nicht anwesend',
'not specified',
'unknown',
'unbekannt',
@@ -4670,8 +4630,13 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
const playerIndex = this.meetingDetails[teamKey].findIndex(p => p.nuLigaPersonId === player.nuLigaPersonId);
if (playerIndex !== -1) {
// Toggle selection status
this.meetingDetails[teamKey][playerIndex].isSelected = !this.meetingDetails[teamKey][playerIndex].isSelected;
const selectedPlayer = this.meetingDetails[teamKey][playerIndex];
selectedPlayer.isSelected = !selectedPlayer.isSelected;
// Eine Doppelposition ohne aufgestellten Spieler ist ungültig
// und darf weder Plätze blockieren noch in Paarungen auftauchen.
if (!selectedPlayer.isSelected) {
selectedPlayer.positionDouble = null;
}
// Recalculate all positions for selected players
this.recalculatePositions(teamKey);
@@ -4702,6 +4667,14 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
);
},
getDoublePositions() {
const positions = this.meetingDetails?.teamLineupDoublePositions;
if (Array.isArray(positions) && positions.length > 0) {
return [...new Set(positions.map(Number).filter(Number.isInteger))].sort((a, b) => a - b);
}
return [1, 2];
},
toggleDoublePosition(player, team, doublePosition) {
if (this.viewOnly) return;
if (this.isLineupLocked(team)) return;
@@ -4711,11 +4684,13 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
const playerIndex = this.meetingDetails[teamKey].findIndex(p => p.nuLigaPersonId === player.nuLigaPersonId);
if (playerIndex !== -1) {
const currentDoublePosition = this.meetingDetails[teamKey][playerIndex].positionDouble;
const selectedPlayer = this.meetingDetails[teamKey][playerIndex];
if (!selectedPlayer.isSelected) return;
const currentDoublePosition = selectedPlayer.positionDouble;
if (currentDoublePosition === doublePosition) {
// Deselect if already selected
this.meetingDetails[teamKey][playerIndex].positionDouble = null;
selectedPlayer.positionDouble = null;
} else {
// Check if we can select this position
if (!this.canSelectDoublePosition(player, team, doublePosition)) {
@@ -4723,13 +4698,13 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
}
// Select new position (automatically removes from previous position)
this.meetingDetails[teamKey][playerIndex].positionDouble = doublePosition;
selectedPlayer.positionDouble = doublePosition;
}
// Ergebnisse neu berechnen, wenn nicht beide Aufstellungen zertifiziert sind
if (!this.isHomeLineupCertified || !this.isGuestLineupCertified) {
this.prepareResults();
}
// Sowohl Setzen als auch Entfernen muss die Doppelzeilen
// unmittelbar aus den aktuellen Paaren neu auflösen.
if (!this.isHomeLineupCertified || !this.isGuestLineupCertified) {
this.prepareResults();
}
}
this.persistLocalDraftDebounced();
@@ -4743,15 +4718,20 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
const teamKey = team === 'home' ? 'teamLineupHomePlayers' : 'teamLineupGuestPlayers';
// Doppelpartner müssen Teil der Aufstellung sein. Damit können
// Karten in „Verfügbare Spieler“ keine unsichtbaren Doppel belegen.
if (!player.isSelected) {
return false;
}
// Check if player is already selected for a different double position
if (player.positionDouble && player.positionDouble !== doublePosition) {
return false; // Player is already assigned to a different double position
}
// Count how many players are already selected for this double position
// Include ALL players (both selected and available) who have this double position
// Pro Doppelposition sind genau zwei aufgestellte Spieler erlaubt.
const playersInDoublePosition = this.meetingDetails[teamKey].filter(p =>
p.positionDouble === doublePosition && p.nuLigaPersonId !== player.nuLigaPersonId && p.nuLigaPersonId > 0
p.isSelected && p.positionDouble === doublePosition && p.nuLigaPersonId !== player.nuLigaPersonId && p.nuLigaPersonId > 0
);
// Allow selection if less than 2 players are already selected for this position
@@ -6061,7 +6041,16 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
}
.result-content { margin-top: 2px; }
.result-table { width: 100%; border-collapse: collapse; background: #fff; border: 1px solid #e0e0e0; border-radius: 8px; overflow: hidden; }
.result-table-wrap {
width: 100%;
overflow-x: auto;
overscroll-behavior-inline: contain;
border: 1px solid #e0e0e0;
border-radius: 8px;
background: #fff;
}
.result-table-wrap:focus { outline: 2px solid rgba(var(--primary-rgb), 0.35); outline-offset: 2px; }
.result-table { width: 100%; min-width: 960px; border-collapse: collapse; background: #fff; }
.result-table th, .result-table td { border-bottom: 1px solid #eee; padding: 8px; text-align: left; }
.result-table thead th { background: #f8f9fa; }
.set-input-wrapper {
@@ -6766,8 +6755,8 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
/* Doppel-Auswahl - Layout bereits oben definiert */
.player-main {
flex: 1;
width: 100%;
flex: 1 1 auto;
min-width: 0;
border: 0;
background: transparent;
color: inherit;
@@ -6780,6 +6769,12 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
-webkit-tap-highlight-color: transparent;
}
.player-name,
.player-details,
.player-action {
overflow-wrap: anywhere;
}
.player-main:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);