feat(match-report): implement test mode simulation for submission validation
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 56s

This commit is contained in:
Torsten Schulz (local)
2026-09-10 12:44:12 +02:00
parent c6dc2e1444
commit d41425eba8

View File

@@ -101,6 +101,33 @@
</ul>
</div>
<div v-if="isTestMode" class="test-simulation">
<button type="button" class="btn-secondary" @click="simulateSubmission">
🧪 Übermittlung inklusive nuScore-Validierung simulieren
</button>
<p>Keine Netzwerkverbindung: Die Simulation entfernt Doppelpositionen wie eine normalisierte
Validierungsantwort und prüft, ob der finale Payload sie wiederherstellt.</p>
<div v-if="simulationResult" class="test-submit-result"
:class="simulationResult.valid ? 'test-success' : 'test-error'">
<strong>{{ simulationResult.valid ? '✓ Doppel sind im finalen Payload vollständig' : '⚠ Doppel fehlen im finalen Payload' }}</strong>
<p>{{ simulationResult.summary }}</p>
<ul v-if="simulationResult.messages.length">
<li v-for="message in simulationResult.messages" :key="message">{{ message }}</li>
</ul>
<div v-if="simulationResult.doubles.length" class="comparison-table-wrap">
<table class="comparison-table">
<thead><tr><th>Spiel</th><th>Heim</th><th>Gast</th><th>Prüfung</th></tr></thead>
<tbody>
<tr v-for="row in simulationResult.doubles" :key="`simulation-double-${row.matchNumber}`">
<td>{{ row.matchNumber }}</td><td>{{ row.home }}</td><td>{{ row.guest }}</td>
<td>{{ row.valid ? '✓ vollständig' : '✕ unvollständig' }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div v-if="isTestMode && (meetingDetails?.isCompleted || meetingData?.isCompleted)" class="test-verification">
<button type="button" class="btn-secondary" @click="verifyCompletedMatchProjection">
🧪 Gespeicherte Paarungen prüfen
@@ -875,6 +902,7 @@ export default {
isTestMode: this.standaloneTest,
testVerification: null,
testSubmissionResult: null,
simulationResult: null,
lastLiveUpdatedAt: null,
originalTestInput: null,
// Unveränderliche Referenz auf den offiziellen Bericht, bevor der
@@ -1213,6 +1241,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
// Ein früherer Prüfhinweis gilt nach einer lokalen Änderung nicht mehr.
this.testVerification = null;
this.testSubmissionResult = null;
this.simulationResult = null;
this.persistLocalDraftDebounced();
},
deep: true
@@ -1223,6 +1252,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
// machen einen bereits angezeigten Abgleich hinfällig.
this.testVerification = null;
this.testSubmissionResult = null;
this.simulationResult = null;
this.persistLocalDraftDebounced();
},
deep: true
@@ -1984,6 +2014,83 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
};
},
// Bildet den kritischen Teil des echten Submit-Ablaufs lokal nach.
// NuScore darf den Payload bei /validate normalisieren; genau dabei
// waren zuvor die Doppelpositionen verloren gegangen.
simulateSubmission() {
if (!this.meetingDetails) {
return;
}
const initialPayload = JSON.parse(JSON.stringify(this.match || {}));
this.updateMatchData(initialPayload, { finalizeReport: false });
// Simulierte Antwort von /validate: Die Doppelinformationen sind
// nicht mehr vorhanden. Der Produktionscode muss sie anschließend
// aus dem lokalen Aufstellungszustand wieder einsetzen.
const normalizedByNuScore = JSON.parse(JSON.stringify(initialPayload));
['teamLineupHomePlayers', 'teamLineupGuestPlayers'].forEach(teamKey => {
(normalizedByNuScore[teamKey] || []).forEach(player => {
player.positionDouble = null;
player.positions = (player.positions || []).filter(position => !/^D\d+$/i.test(String(position)));
});
});
const finalPayload = JSON.parse(JSON.stringify(normalizedByNuScore));
this.applyCurrentLineupStateToPayload(finalPayload);
this.updatePlayerPositions(finalPayload);
const doubles = this.getSimulatedDoubleRows(finalPayload);
const invalidRows = doubles.filter(row => !row.valid);
const messages = invalidRows.map(row =>
`Spiel ${row.matchNumber}: ${row.home} ${row.guest}`
);
if (doubles.length === 0) {
messages.push('Das gewählte Spielsystem enthält keine Doppelbegegnung.');
}
this.simulationResult = {
valid: doubles.length > 0 && invalidRows.length === 0,
summary: invalidRows.length === 0 && doubles.length > 0
? `Die ${doubles.length} Doppelbegegnung(en) bleiben nach der simulierten Validierung vollständig zugeordnet.`
: 'Mindestens eine Doppelbegegnung wäre im finalen Payload nicht vollständig besetzt.',
messages,
doubles,
payload: finalPayload
};
},
getSimulatedDoubleRows(payload) {
const formations = payload?.meetingPlayMode?.matchFormations || this.meetingDetails?.meetingPlayMode?.matchFormations || [];
const nameOf = (player) => [player?.firstname || player?.firstName || '', player?.lastname || player?.lastName || '']
.filter(Boolean).join(' ').trim() || 'nicht anwesend';
const pairFor = (teamKey, position) => {
const players = (payload?.[teamKey] || [])
.filter(player => Number(player?.positionDouble) === position && Number(player?.nuLigaPersonId) > 0)
.map(nameOf);
return {
label: players.length === 2 ? players.join(' / ') : (players.join(' / ') || 'nicht anwesend / nicht anwesend'),
complete: players.length === 2
};
};
return formations.map((formation, index) => {
const homePosition = String(formation?.homeLineupPosition || '');
const guestPosition = String(formation?.guestLineupPosition || '');
if (!/^D\d+$/i.test(homePosition) || !/^D\d+$/i.test(guestPosition)) {
return null;
}
const home = pairFor('teamLineupHomePlayers', Number(homePosition.slice(1)));
const guest = pairFor('teamLineupGuestPlayers', Number(guestPosition.slice(1)));
return {
matchNumber: formation?.matchNr || index + 1,
home: home.label,
guest: guest.label,
valid: home.complete && guest.complete
};
}).filter(Boolean);
},
compareTestResultsToOriginal() {
const originalRows = this.getOriginalReportRows();
if (!originalRows.length) {
@@ -2138,6 +2245,15 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
const submitPayload = validationResult?.object
? JSON.parse(JSON.stringify(validationResult.object))
: matchData;
// nuScore kann den durch /validate zurückgegebenen Entwurf
// normalisieren und dabei Doppelpositionen verwerfen. Für den
// finalen Submit müssen deshalb die gerade bestätigten lokalen
// Aufstellungen erneut in genau diesen Rückgabedatensatz
// übernommen werden. Andernfalls werden die Doppel im
// Berichtsbogen als "nicht anwesend" dargestellt.
this.applyCurrentLineupStateToPayload(submitPayload);
this.updatePlayerPositions(submitPayload);
submitPayload.isCompleted = true;
if (!submitPayload.signature || typeof submitPayload.signature !== 'object') {
submitPayload.signature = {};
@@ -2525,7 +2641,11 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
...player,
isSelected: Boolean(localPlayer.isSelected),
positionSingle: localPlayer.isSelected ? (localPlayer.positionSingle || null) : null,
positionDouble: localPlayer.isSelected ? (localPlayer.positionDouble || null) : null,
// Doppel können auch von einem nur für das Doppel
// vorgesehenen Ersatzspieler belegt werden. Diese
// Position darf daher nicht an isSelected gekoppelt
// sein und muss die Validierungsantwort überleben.
positionDouble: localPlayer.positionDouble || null,
positions: Array.isArray(localPlayer.positions) ? [...localPlayer.positions] : []
};
});
@@ -2734,6 +2854,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
resetStandaloneTest() {
this.initializeStandaloneTestData();
this.simulationResult = null;
},
buildStandaloneTestPayload() {
@@ -3484,6 +3605,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
this.initialCompletionState = false;
this.testVerification = null;
this.testSubmissionResult = null;
this.simulationResult = null;
this.activeSection = 'general';
await this.$nextTick();