feat(socket): implement match report submission and schedule update events

- Added WebSocket events for match report submission and schedule updates, enhancing real-time communication between clients and the server.
- Updated matchController to emit schedule updates when match players are modified.
- Enhanced nuscoreApiRoutes to emit match report submissions with relevant data for other clients.
- Implemented socket service methods for handling incoming match report submissions and schedule updates in the frontend.
- Updated MatchReportApiDialog and ScheduleView components to handle new WebSocket events, ensuring data synchronization across clients.
This commit is contained in:
Torsten Schulz (local)
2026-02-26 17:07:54 +01:00
parent 0ee9e486b5
commit b3bbca3887
7 changed files with 275 additions and 8 deletions

View File

@@ -696,6 +696,7 @@
<script>
import CryptoJS from 'crypto-js';
import apiClient, { backendBaseUrl } from '../apiClient';
import { onMatchReportSubmitted, offMatchReportSubmitted } from '../services/socketService.js';
export default {
name: 'MatchReportDialog',
@@ -732,6 +733,7 @@ export default {
errors: [],
// Aktive Zelle der schwebenden Satz-Tastatur: { matchIndex, setIndex } oder null
editingSetCell: null,
broadcastDraftTimer: null,
// Abschluss-Felder
protestText: '',
finalHomePin: '',
@@ -863,6 +865,25 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
await this.loadData();
this.initializeResults();
this.initializeFinalPins();
this._matchReportSubmittedHandler = (payload) => {
if (!payload?.matchCode || !this.match?.code || String(payload.matchCode) !== String(this.match.code)) {
return;
}
if (payload.matchData && this.meetingDetails) {
this.applyReceivedMatchData(payload.matchData);
} else {
this.loadData();
}
};
onMatchReportSubmitted(this._matchReportSubmittedHandler);
},
beforeUnmount() {
if (this._matchReportSubmittedHandler) {
offMatchReportSubmitted(this._matchReportSubmittedHandler);
}
if (this.broadcastDraftTimer) {
clearTimeout(this.broadcastDraftTimer);
}
},
watch: {
teamNotAppeared(newValue, oldValue) {
@@ -1461,6 +1482,11 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
this.updateMatchData(matchData);
console.log('✅ Match-Daten aktualisiert');
// Für WebSocket-Broadcast: clubId und gameCode mitsenden
const clubId = this.$store?.getters?.currentClub;
if (clubId) matchData.clubId = String(clubId);
if (this.match?.code) matchData.gameCode = this.match.code;
// Sende die Daten an den Backend-Endpunkt
console.log('📤 Sende Spielbericht an Backend...');
const uuid = this.meetingData.nuLigaMeetingUuid;
@@ -2097,6 +2123,68 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
this.isGuestLineupCertified = true;
}
},
/** Entwurf für Broadcast: meetingDetails-Form mit aktuellen Satzergebnissen aus results. */
getDraftMatchData() {
if (!this.meetingDetails || !Array.isArray(this.meetingDetails.matches)) return null;
const matches = this.meetingDetails.matches.map((m, i) => {
const r = this.results[i];
const out = { ...m };
if (r && Array.isArray(r.sets)) {
for (let s = 0; s < 5; s++) {
const setStr = r.sets[s];
if (setStr && String(setStr).includes(':')) {
const parts = String(setStr).split(':').map(x => parseInt(x, 10) || 0);
out[`set${s + 1}A`] = parts[0];
out[`set${s + 1}B`] = parts[1];
}
}
}
return out;
});
return {
matches,
homePin: this.meetingDetails.homePin,
guestPin: this.meetingDetails.guestPin,
startDate: this.meetingData?.startDate ?? this.match?.startDate,
endDate: this.meetingData?.endDate ?? this.match?.endDate
};
},
async broadcastDraft() {
const clubId = this.$store?.getters?.currentClub;
const gameCode = this.match?.code;
const matchData = this.getDraftMatchData();
if (!clubId || !gameCode || !matchData) return;
try {
await apiClient.post('/nuscore/broadcast-draft', { clubId: String(clubId), gameCode, matchData });
} catch (e) {
// still, no hard error for draft broadcast
}
},
broadcastDraftDebounced() {
if (this.broadcastDraftTimer) clearTimeout(this.broadcastDraftTimer);
this.broadcastDraftTimer = setTimeout(() => {
this.broadcastDraftTimer = null;
this.broadcastDraft();
}, 500);
},
/**
* Übernimmt per WebSocket empfangene Match-Daten (von anderem Gerät) in den Dialog.
* Aktualisiert meetingDetails und meetingData, dann Ergebnisse und Aufstellungs-Bestätigung.
*/
applyReceivedMatchData(matchData) {
if (!matchData || !this.meetingDetails) return;
if (Array.isArray(matchData.matches)) {
this.meetingDetails.matches = matchData.matches;
}
if (matchData.homePin != null) this.meetingDetails.homePin = matchData.homePin;
if (matchData.guestPin != null) this.meetingDetails.guestPin = matchData.guestPin;
if (matchData.startDate != null && this.meetingData) this.meetingData.startDate = matchData.startDate;
if (matchData.endDate != null && this.meetingData) this.meetingData.endDate = matchData.endDate;
this.populateResultsFromMeetingDetails();
this.applyLineupCertificationFromMeetingDetails();
},
resolveSide(label, side) {
// label z.B. "A1 B2" oder "DA1 DB1"
try {
@@ -2244,6 +2332,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
// Synchronisiere zurück ins Match-Objekt
this.syncResultsToMatch();
this.broadcastDraftDebounced();
},
appendToSet(matchIndex, setIndex, char) {
@@ -2274,6 +2363,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
const current = sets[setIndex] || '';
if (current.length >= 6) return;
this.results[matchIndex].sets[setIndex] = current + char;
this.broadcastDraftDebounced();
},
setKeyboardBackspace() {
if (!this.editingSetCell || !this.results[this.editingSetCell.matchIndex]) return;
@@ -2281,6 +2371,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
const sets = this.results[matchIndex].sets || [];
const current = sets[setIndex] || '';
this.results[matchIndex].sets[setIndex] = current.slice(0, -1);
this.broadcastDraftDebounced();
},
setKeyboardClear() {
if (!this.editingSetCell || !this.results[this.editingSetCell.matchIndex]) return;
@@ -2289,6 +2380,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
this.$set(this.results[matchIndex], 'sets', ['', '', '', '', '']);
}
this.results[matchIndex].sets[setIndex] = '';
this.broadcastDraftDebounced();
},
setKeyboardOk() {
if (!this.editingSetCell) return;
@@ -2300,6 +2392,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
const m = this.results[idx];
m.completed = false;
m.result = '1:0';
this.broadcastDraftDebounced();
},
async loadClubSettings() {
try {
@@ -2405,6 +2498,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
// Aktualisiere die Anzeige
this.$forceUpdate();
this.broadcastDraftDebounced();
},
// Berechne gewonnene Sätze für einen Spieler in einem Match

View File

@@ -308,6 +308,18 @@ export const onTournamentChanged = (callback) => {
}
};
export const onScheduleMatchUpdated = (callback) => {
if (socket) {
socket.on('schedule:match:updated', callback);
}
};
export const onMatchReportSubmitted = (callback) => {
if (socket) {
socket.on('schedule:match-report:submitted', callback);
}
};
// Event-Listener entfernen
export const offParticipantAdded = (callback) => {
if (socket) {
@@ -399,3 +411,15 @@ export const offTournamentChanged = (callback) => {
}
};
export const offScheduleMatchUpdated = (callback) => {
if (socket) {
socket.off('schedule:match:updated', callback);
}
};
export const offMatchReportSubmitted = (callback) => {
if (socket) {
socket.off('schedule:match-report:submitted', callback);
}
};

View File

@@ -303,6 +303,14 @@ import InfoDialog from '../components/InfoDialog.vue';
import ConfirmDialog from '../components/ConfirmDialog.vue';
import BaseDialog from '../components/BaseDialog.vue';
import CsvImportDialog from '../components/CsvImportDialog.vue';
import {
connectSocket,
disconnectSocket,
onScheduleMatchUpdated,
offScheduleMatchUpdated,
onMatchReportSubmitted,
offMatchReportSubmitted
} from '../services/socketService.js';
export default {
name: 'ScheduleView',
components: {
@@ -316,6 +324,18 @@ export default {
computed: {
...mapGetters(['isAuthenticated', 'currentClub', 'clubs', 'currentClubName']),
},
watch: {
currentClub(newVal) {
offScheduleMatchUpdated(this.handleScheduleMatchUpdated);
offMatchReportSubmitted(this.handleMatchReportSubmitted);
disconnectSocket();
if (newVal) {
connectSocket(newVal);
onScheduleMatchUpdated(this.handleScheduleMatchUpdated);
onMatchReportSubmitted(this.handleMatchReportSubmitted);
}
},
},
data() {
return {
// Dialog States
@@ -1016,13 +1036,66 @@ export default {
this.fetchingTable = false;
}
},
refreshScheduleData() {
if (!this.selectedLeague) return;
if (this.selectedTeam) {
this.loadMatchesForTeam(this.selectedTeam);
} else if (this.selectedLeague === this.$t('schedule.overallSchedule')) {
this.loadAllMatches();
} else if (this.selectedLeague === this.$t('schedule.adultSchedule')) {
this.loadAdultMatches();
}
},
handleScheduleMatchUpdated(payload) {
if (payload?.match && payload.matchId != null) {
const idx = this.matches.findIndex(m => m.id === payload.matchId);
if (idx !== -1) {
this.matches.splice(idx, 1, payload.match);
return;
}
}
this.refreshScheduleData();
},
handleMatchReportSubmitted(payload) {
if (payload?.matchData && (payload.matchCode != null || payload.matchData?.gameCode || payload.matchData?.code)) {
const code = String(payload.matchCode ?? payload.matchData?.gameCode ?? payload.matchData?.code ?? '');
const idx = this.matches.findIndex(m => m.code === code);
if (idx !== -1) {
const m = { ...this.matches[idx] };
const d = payload.matchData;
if (d.homeMatches != null) m.homeMatchPoints = d.homeMatches;
if (d.guestMatches != null) m.guestMatchPoints = d.guestMatches;
if (typeof d.isCompleted === 'boolean') m.isCompleted = d.isCompleted;
if (d.startDate) m.startDate = d.startDate;
if (d.endDate) m.endDate = d.endDate;
this.matches.splice(idx, 1, m);
return;
}
}
this.refreshScheduleData();
},
},
async created() {
// Teams werden geladen, sobald eine Saison ausgewählt ist
// Die SeasonSelector-Komponente wählt automatisch die aktuelle Saison aus
// und ruft anschließend onSeasonChange auf, was loadTeams() ausführt
this.loadTeams();
}
},
mounted() {
if (this.currentClub) {
connectSocket(this.currentClub);
onScheduleMatchUpdated(this.handleScheduleMatchUpdated);
onMatchReportSubmitted(this.handleMatchReportSubmitted);
}
},
beforeUnmount() {
offScheduleMatchUpdated(this.handleScheduleMatchUpdated);
offMatchReportSubmitted(this.handleMatchReportSubmitted);
disconnectSocket();
},
};
</script>