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:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user