feat(schedule): add match report simulator button and functionality
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 54s

This commit is contained in:
Torsten Schulz (local)
2026-09-02 14:34:09 +02:00
parent f753bf43fb
commit 0dc746d1cb
3 changed files with 240 additions and 5 deletions

View File

@@ -20,7 +20,7 @@
</div>
</div>
<label class="test-mode-control" :class="{ active: isTestMode }">
<label v-if="!standaloneTest" class="test-mode-control" :class="{ active: isTestMode }">
<input v-model="isTestMode" type="checkbox" @change="onTestModeChange" />
<span>
<strong>Testmodus</strong>
@@ -28,6 +28,28 @@
</span>
</label>
<div v-else class="standalone-test-banner">
<span>🧪 Eigenständiger Simulator keine Verbindung zu nuScore</span>
<label>
Spielsystem
<select v-model="standaloneTestSystem" @change="initializeStandaloneTestData">
<option v-for="system in standaloneTestSystems" :key="system.value" :value="system.value">{{ system.label }}</option>
</select>
</label>
<button type="button" class="btn-secondary" @click="resetStandaloneTest">Originaleingabe zurücksetzen</button>
</div>
<div v-if="standaloneTest" class="test-data-inspector">
<details>
<summary>Originaleingabe anzeigen</summary>
<pre>{{ formatTestData(originalTestInput) }}</pre>
</details>
<details>
<summary>Aktuellen Zustand anzeigen</summary>
<pre>{{ formatTestData(buildStandaloneTestPayload()) }}</pre>
</details>
</div>
<div v-if="isTestMode && (meetingDetails?.isCompleted || meetingData?.isCompleted)" class="test-verification">
<button type="button" class="btn-secondary" @click="verifyCompletedMatchProjection">
🧪 Gespeicherte Paarungen prüfen
@@ -210,6 +232,18 @@
: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>
</div>
</div>
</div>
@@ -304,6 +338,18 @@
: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>
</div>
</div>
</div>
@@ -732,14 +778,28 @@ export default {
match: {
type: Object,
required: true
},
standaloneTest: {
type: Boolean,
default: false
}
},
data() {
return {
loading: true,
error: null,
isTestMode: false,
isTestMode: this.standaloneTest,
testVerification: null,
originalTestInput: null,
standaloneTestSystem: 'braunschweiger system',
standaloneTestSystems: [
{ value: 'paarkreuz-system', label: 'Paarkreuz-System (6 gegen 6)' },
{ value: 'bundessystem', label: 'Bundessystem (4 gegen 4)' },
{ value: 'werner-scheffler-system', label: 'Werner-Scheffler-System (4 gegen 4)' },
{ value: 'braunschweiger system', label: 'Braunschweiger System (4 gegen 3)' },
{ value: 'modifiziertes swaythling-cup-system', label: 'Modifiziertes Swaythling-Cup-System (3 gegen 3)' },
{ value: 'corbillon-cup-system', label: 'Corbillon-Cup-System (2 gegen 2)' }
],
meetingData: null,
meetingDetails: null,
activeSection: 'general',
@@ -972,6 +1032,11 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
}
},
async mounted() {
if (this.standaloneTest) {
this.initializeStandaloneTestData();
this.initializeResults();
return;
}
await this.loadData();
this.initializeResults();
this.initializeFinalPins();
@@ -1779,6 +1844,11 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
console.log('🚀 Starte Spielbericht-Absendung...');
if (this.isTestMode) {
if (this.standaloneTest) {
this.submitSucceeded = true;
this.meetingData.isCompleted = true;
this.meetingDetails.isCompleted = true;
}
alert('🧪 Testmodus: Der Spielbericht wurde nur lokal geprüft und nicht an nuScore übermittelt.');
return;
}
@@ -2410,6 +2480,102 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
});
},
createStandaloneTestPlayer(id, firstname, lastname, rank, positionSingle = null, positionDouble = null) {
const positions = [];
if (positionSingle) positions.push(`E${positionSingle}`);
if (positionDouble) positions.push(`D${positionDouble}`);
return { nuLigaPersonId: id, firstname, lastname, rank, isSelected: true, positionSingle, positionDouble, positions };
},
initializeStandaloneTestData() {
const configs = {
'paarkreuz-system': { home: 6, guest: 6, doubles: 3 },
bundessystem: { home: 4, guest: 4, doubles: 2 },
'werner-scheffler-system': { home: 4, guest: 4, doubles: 2 },
'braunschweiger system': { home: 4, guest: 3, doubles: 2 },
'modifiziertes swaythling-cup-system': { home: 3, guest: 3, doubles: 1 },
'corbillon-cup-system': { home: 2, guest: 2, doubles: 1 }
};
const config = configs[this.standaloneTestSystem] || configs['braunschweiger system'];
const matrixConfig = this.playModeMatrices[this.standaloneTestSystem];
const rawMatrix = this.standaloneTestSystem === 'braunschweiger system'
? matrixConfig.matrices['4-3']
: matrixConfig;
const formations = rawMatrix.map((label, index) => {
const [homeToken, guestToken] = label.split('').map(token => token.trim());
const toNuScorePosition = token => token.replace(/^DA/, 'D').replace(/^DB/, 'D').replace(/^A/, 'E').replace(/^B/, 'E');
const homeLineupPosition = toNuScorePosition(homeToken);
const guestLineupPosition = toNuScorePosition(guestToken);
return {
matchNr: index + 1,
displayString: `${homeLineupPosition}-${guestLineupPosition}`,
homeLineupPosition,
guestLineupPosition
};
});
const makePlayers = (baseId, side, count) => Array.from({ length: count }, (_, index) => {
const rank = index + 1;
const doublePosition = rank <= config.doubles * 2 ? Math.ceil(rank / 2) : null;
return this.createStandaloneTestPlayer(baseId + rank, side, ['Eins', 'Zwei', 'Drei', 'Vier', 'Fünf', 'Sechs'][index], rank, rank, doublePosition);
});
const homePlayers = makePlayers(0, 'Heim', config.home);
const guestPlayers = makePlayers(100, 'Gast', config.guest);
const matches = formations.map((formation) => ({
matchNr: formation.matchNr,
name: formation.displayString,
matchType: formation.homeLineupPosition.startsWith('D') ? 'DOUBLE' : 'SINGLE',
set1A: 0, set1B: 0, set2A: 0, set2B: 0, set3A: 0, set3B: 0, set4A: 0, set4B: 0, set5A: 0, set5B: 0,
matchesA: 0, matchesB: 0, setsA: 0, setsB: 0, isCompleted: false
}));
this.meetingData = {
scheduleDate: new Date().toISOString(), homeTeamname: 'Test Heim', guestTeamname: 'Test Gast',
homeClub: 'Testverein Heim', guestClub: 'Testverein Gast', championship: 'Testwettbewerb',
league: 'Testliga', group: '', playMode: this.standaloneTestSystem, playThrough: 'ALLGAMES', isCompleted: false
};
this.meetingDetails = {
...this.meetingData,
playerCountHome: config.home, playerCountGuest: config.guest, signature: {},
meetingPlayMode: { matchFormations: formations, singleFormationUpTo: config.home },
optionalPlayModes: [], matches,
teamLineupHomePlayers: homePlayers, teamLineupGuestPlayers: guestPlayers,
teamLineupSinglePositions: Array.from({ length: Math.max(config.home, config.guest) }, (_, index) => index + 1),
teamLineupDoublePositions: Array.from({ length: config.doubles }, (_, index) => index + 1)
};
this.originalTestInput = JSON.parse(JSON.stringify(this.meetingDetails));
this.homePin = 'test';
this.guestPin = 'test';
this.finalHomePin = 'test';
this.finalGuestPin = 'test';
this.isHomeLineupCertified = false;
this.isGuestLineupCertified = false;
this.isGreetingCompleted = false;
this.submitSucceeded = false;
this.initialCompletionState = false;
this.prepareResults();
},
resetStandaloneTest() {
this.initializeStandaloneTestData();
},
buildStandaloneTestPayload() {
if (!this.standaloneTest) return null;
return {
playerCountHome: this.getSinglePlayerCount('home'),
playerCountGuest: this.getSinglePlayerCount('guest'),
teamLineupHomePlayers: this.meetingDetails?.teamLineupHomePlayers || [],
teamLineupGuestPlayers: this.meetingDetails?.teamLineupGuestPlayers || [],
meetingPlayMode: this.meetingDetails?.meetingPlayMode,
signature: this.meetingDetails?.signature,
isGreetingCompleted: this.isGreetingCompleted,
matches: this.results
};
},
formatTestData(data) {
return JSON.stringify(data || {}, null, 2);
},
async loadData() {
this.loading = true;
this.error = null;
@@ -2525,10 +2691,11 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
? this.meetingDetails.matches
: [];
if (serverMatches.length > 0) {
const matrix = this.currentPlayModeMatrix;
this.results = serverMatches.map((match, index) => ({
label: (typeof match?.name === 'string' && match.name.trim()) || `Spiel ${index + 1}`,
homeName: this.formatMeetingMatchSide(match, 'home') || match?.homePlayer || '',
guestName: this.formatMeetingMatchSide(match, 'guest') || match?.guestPlayer || '',
label: (typeof match?.name === 'string' && match.name.trim()) || matrix[index] || `Spiel ${index + 1}`,
homeName: this.formatMeetingMatchSide(match, 'home') || match?.homePlayer || this.resolveSide(matrix[index] || '', 'home'),
guestName: this.formatMeetingMatchSide(match, 'guest') || match?.guestPlayer || this.resolveSide(matrix[index] || '', 'guest'),
sets: ['', '', '', '', ''],
completed: false,
result: '1:0'
@@ -4188,6 +4355,11 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
async validatePin(team, pin) {
if (!pin || !this.meetingDetails) return false;
if (this.standaloneTest) {
this.showPinValidation(team, true, 'Testmodus');
return true;
}
const expectedHash = team === 'home' ? this.meetingDetails.homePin : this.meetingDetails.guestPin;
@@ -4768,6 +4940,49 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
font-size: 12px;
}
.standalone-test-banner {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin: 0 0 12px;
padding: 10px 14px;
border: 1px solid #d78a00;
border-radius: 10px;
background: #fff3cd;
color: #795300;
font-weight: 600;
}
.test-data-inspector {
display: grid;
gap: 8px;
margin: 0 0 16px;
}
.test-data-inspector details {
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 8px 10px;
background: var(--surface-muted);
}
.test-data-inspector summary {
cursor: pointer;
font-weight: 600;
}
.test-data-inspector pre {
max-height: 260px;
overflow: auto;
margin: 10px 0 0;
padding: 10px;
border-radius: 6px;
background: #17202a;
color: #f8f9f9;
font-size: 11px;
}
.test-verification {
margin: -8px 0 16px;
padding: 12px 14px;

View File

@@ -30,6 +30,14 @@
>
{{ $t('schedule.addMatch') }}
</button>
<button
v-if="showScheduleActions"
type="button"
class="btn-secondary schedule-add-match-button"
@click="$emit('open-match-report-simulator')"
>
🧪 Spielbericht simulieren
</button>
<button v-if="showFriendlyActions" @click="$emit('open-friendly-match-modal')" class="btn-secondary">Freundschaftsspiel</button>
<button
v-if="showGalleryButton"

View File

@@ -31,6 +31,7 @@
@season-change="onSeasonChange"
@open-import-modal="openImportModal"
@open-manual-match-modal="openManualMatchDialog"
@open-match-report-simulator="openMatchReportSimulator"
@open-friendly-match-modal="openFriendlyMatchDialog"
@open-gallery-dialog="openGalleryDialog"
@update:team-search-query="teamSearchQuery = $event"
@@ -2863,6 +2864,17 @@ export default {
}
});
},
openMatchReportSimulator() {
this.openDialog({
title: 'Spielbericht simulieren',
component: 'MatchReportApiDialog',
props: {
match: { startDate: new Date(), endDate: null, matches: [] },
standaloneTest: true
}
});
},
async loadLeagueTable(leagueId) {
try {