feat(match-report): implement original report comparison and enhance test mode functionality
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 55s

This commit is contained in:
Torsten Schulz (local)
2026-09-02 15:18:39 +02:00
parent 812cfa66b8
commit 8e6d178a6f

View File

@@ -39,12 +39,48 @@
<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>
<div v-if="isTestMode" class="test-data-inspector">
<details v-if="originalTestReport" class="original-report-comparison">
<summary>Originalbericht zum Vergleich</summary>
<div class="comparison-summary">
<span><strong>Status:</strong> {{ getOriginalReportStatus() }}</span>
<span><strong>Spielstand:</strong> {{ getOriginalReportScore() }}</span>
</div>
<div class="comparison-table-wrap">
<table class="comparison-table">
<thead>
<tr>
<th>Spiel</th>
<th>Heim</th>
<th>Gast</th>
<th>Sätze</th>
<th>Ergebnis</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="row in getOriginalReportRows()" :key="`original-match-${row.matchNumber}`">
<td>{{ row.matchNumber }}</td>
<td>{{ row.homeName || '—' }}</td>
<td>{{ row.guestName || '—' }}</td>
<td class="comparison-sets">{{ row.setsText || '—' }}</td>
<td>{{ row.result }}</td>
<td>
<button type="button" class="copy-original-btn" :disabled="!row.hasSets"
@click="copyOriginalPairingResults(row)">
Übernehmen
</button>
</td>
</tr>
</tbody>
</table>
</div>
</details>
<details>
<summary>Originaldaten anzeigen (technisch)</summary>
<pre>{{ formatTestData(originalTestReport || originalTestInput) }}</pre>
</details>
<details v-if="standaloneTest">
<summary>Aktuellen Zustand anzeigen</summary>
<pre>{{ formatTestData(buildStandaloneTestPayload()) }}</pre>
</details>
@@ -805,6 +841,9 @@ export default {
isTestMode: this.standaloneTest,
testVerification: null,
originalTestInput: null,
// Unveränderliche Referenz auf den offiziellen Bericht, bevor der
// Testmodus seinen vollständig leeren lokalen Zustand erzeugt.
originalTestReport: null,
standaloneTestSystem: 'braunschweiger system',
standaloneTestSystems: [
{ value: 'paarkreuz-system', label: 'Paarkreuz-System (6 gegen 6)' },
@@ -1231,7 +1270,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
});
},
async pollLatestMeetingDetails() {
if (this.loading || this.isMeetingDetailsPolling) {
if (this.isTestMode || this.loading || this.isMeetingDetailsPolling) {
return;
}
@@ -2571,6 +2610,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
this.submitSucceeded = false;
this.initialCompletionState = false;
this.prepareResults();
this.captureOriginalTestReport();
},
resetStandaloneTest() {
@@ -2595,6 +2635,93 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
return JSON.stringify(data || {}, null, 2);
},
captureOriginalTestReport() {
// JSON-Kopie schützt die Vergleichsansicht vor allen lokalen
// Änderungen, die der Testmodus anschließend am Bericht vornimmt.
this.originalTestReport = JSON.parse(JSON.stringify({
meetingData: this.meetingData || {},
meetingDetails: this.meetingDetails || {},
results: this.results || []
}));
},
getOriginalReportRows() {
const details = this.originalTestReport?.meetingDetails || {};
const matches = Array.isArray(details.matches) ? details.matches : [];
return matches.map((match, index) => {
const sets = Array.from({ length: 5 }, (_, setIndex) => {
const home = Number(match?.[`set${setIndex + 1}A`] || 0);
const guest = Number(match?.[`set${setIndex + 1}B`] || 0);
return home > 0 || guest > 0 ? `${home}:${guest}` : '';
});
const homeWins = sets.filter(set => {
const [home, guest] = set.split(':').map(Number);
return home > guest;
}).length;
const guestWins = sets.filter(set => {
const [home, guest] = set.split(':').map(Number);
return guest > home;
}).length;
const homeName = this.formatMeetingMatchSide(match, 'home') || match?.homePlayer || '';
const guestName = this.formatMeetingMatchSide(match, 'guest') || match?.guestPlayer || '';
return {
matchNumber: match?.matchNr || index + 1,
index,
homeName,
guestName,
sets,
setsText: sets.filter(Boolean).join(', '),
hasSets: sets.some(Boolean),
result: homeWins || guestWins ? `${homeWins}:${guestWins}` : '—',
completed: match?.isCompleted === true || match?.matchesA === 1 || match?.matchesB === 1 || homeWins >= 3 || guestWins >= 3
};
});
},
getOriginalReportStatus() {
return this.originalTestReport?.meetingDetails?.isCompleted || this.originalTestReport?.meetingData?.isCompleted
? 'Abgeschlossen'
: 'Offen';
},
getOriginalReportScore() {
const details = this.originalTestReport?.meetingDetails || {};
const home = details.homeMatches ?? this.originalTestReport?.meetingData?.homeMatches;
const guest = details.guestMatches ?? this.originalTestReport?.meetingData?.guestMatches;
if (Number.isFinite(Number(home)) && Number.isFinite(Number(guest))) {
return `${home}:${guest}`;
}
const calculatedScore = this.getOriginalReportRows().reduce((score, row) => {
if (!row.completed) return score;
const [home, guest] = row.result.split(':').map(Number);
if (home > guest) score.home += 1;
if (guest > home) score.guest += 1;
return score;
}, { home: 0, guest: 0 });
return `${calculatedScore.home}:${calculatedScore.guest}`;
},
copyOriginalPairingResults(row) {
if (!row?.hasSets) return;
const targetIndex = this.results.findIndex((_, index) => {
const formation = this.meetingDetails?.meetingPlayMode?.matchFormations?.[index];
return Number(formation?.matchNr || index + 1) === Number(row.matchNumber);
});
const localMatch = this.results[targetIndex >= 0 ? targetIndex : row.index];
if (!localMatch) return;
localMatch.sets = [...row.sets];
localMatch.completed = row.completed;
localMatch.result = row.result === '—' ? '' : (row.result === '3:0' || row.result === '3:1' || row.result === '3:2' ? '1:0' : '0:1');
this.calculateMatchResult(targetIndex >= 0 ? targetIndex : row.index);
this.errors[targetIndex >= 0 ? targetIndex : row.index] = null;
this.$forceUpdate();
},
async loadData() {
this.loading = true;
this.error = null;
@@ -3204,12 +3331,42 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
this.isDraftSyncing = false;
this.lastDraftSyncError = '';
// Ein Test beginnt mit dem aktuellen offiziellen Stand, damit ein
// alter lokaler Entwurf nicht versehentlich getestet wird.
// Den offiziellen Stand nur als Quelle für Teams, Aufstellung und
// Spielsystem laden. Ergebnisse, Abschluss und Signaturen dürfen
// im Testmodus nie übernommen werden.
await this.loadData();
this.prepareResults();
this.populateResultsFromMeetingDetails();
this.resetTestModeState();
},
async resetTestModeState() {
if (!this.meetingDetails || !this.meetingData) {
return;
}
this.captureOriginalTestReport();
[this.meetingDetails, this.meetingData].forEach((data) => {
data.isCompleted = false;
data.wo = null;
data.endDate = null;
data.signature = {};
});
// 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.isHomeLineupCertified = false;
this.isGuestLineupCertified = false;
this.isGreetingCompleted = false;
this.submitSucceeded = false;
this.initialCompletionState = false;
this.testVerification = null;
this.activeSection = 'general';
await this.$nextTick();
this.prepareResults();
},
/**
@@ -5024,6 +5181,84 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
font-size: 11px;
}
.original-report-comparison {
border-color: rgba(33, 110, 57, 0.38) !important;
background: #f7fbf8 !important;
}
.comparison-summary {
display: flex;
flex-wrap: wrap;
gap: 8px 18px;
margin: 10px 0;
color: #355044;
font-size: 13px;
}
.comparison-table-wrap {
overflow-x: auto;
border: 1px solid #dce8df;
border-radius: 6px;
background: #fff;
}
.comparison-table {
width: 100%;
min-width: 620px;
border-collapse: collapse;
font-size: 12px;
}
.comparison-table th,
.comparison-table td {
padding: 7px 8px;
border-bottom: 1px solid #edf2ee;
text-align: left;
vertical-align: middle;
}
.comparison-table th {
color: #496052;
background: #edf6ef;
font-size: 11px;
letter-spacing: 0.02em;
text-transform: uppercase;
}
.comparison-table tbody tr:last-child td {
border-bottom: 0;
}
.comparison-sets {
color: #355044;
font-family: monospace;
white-space: nowrap;
}
.copy-original-btn {
padding: 5px 8px;
border: 1px solid #4f8d66;
border-radius: 5px;
background: #fff;
color: #216e39;
cursor: pointer;
font: inherit;
font-weight: 600;
}
.copy-original-btn:hover:not(:disabled),
.copy-original-btn:focus-visible:not(:disabled) {
border-color: #216e39;
background: #e8f4eb;
outline: none;
}
.copy-original-btn:disabled {
border-color: #d5ddd7;
color: #8a958d;
cursor: not-allowed;
}
.test-verification {
margin: -8px 0 16px;
padding: 12px 14px;