diff --git a/frontend/src/components/MatchReportApiDialog.vue b/frontend/src/components/MatchReportApiDialog.vue
index 35b5b98a..519930d4 100755
--- a/frontend/src/components/MatchReportApiDialog.vue
+++ b/frontend/src/components/MatchReportApiDialog.vue
@@ -101,6 +101,33 @@
+
+
+ 🧪 Übermittlung inklusive nuScore-Validierung simulieren
+
+
Keine Netzwerkverbindung: Die Simulation entfernt Doppelpositionen wie eine normalisierte
+ Validierungsantwort und prüft, ob der finale Payload sie wiederherstellt.
+
+
{{ simulationResult.valid ? '✓ Doppel sind im finalen Payload vollständig' : '⚠ Doppel fehlen im finalen Payload' }}
+
{{ simulationResult.summary }}
+
+
+
+ Spiel Heim Gast Prüfung
+
+
+ {{ row.matchNumber }} {{ row.home }} {{ row.guest }}
+ {{ row.valid ? '✓ vollständig' : '✕ unvollständig' }}
+
+
+
+
+
+
+
🧪 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();