feat(MatchReportApiDialog): implement draft status indicators and local draft persistence
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 42s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 42s
- Added visual indicators for draft status in the MatchReportApiDialog, displaying messages based on syncing state. - Introduced local draft persistence functionality, allowing users to save and restore match report drafts. - Enhanced state management for draft synchronization, including timers and error handling. - Updated methods to ensure local drafts are persisted and cleared appropriately during the match report submission process.
This commit is contained in:
@@ -591,6 +591,13 @@
|
||||
|
||||
<!-- Absenden-Button -->
|
||||
<div class="submit-section">
|
||||
<div
|
||||
v-if="draftStatusText && !isMatchCompleted"
|
||||
class="draft-status-indicator"
|
||||
:class="`state-${draftStatusState}`"
|
||||
>
|
||||
{{ draftStatusText }}
|
||||
</div>
|
||||
<button
|
||||
@click="submitMatchReport"
|
||||
class="btn-primary submit-btn"
|
||||
@@ -752,9 +759,15 @@ export default {
|
||||
// Aktive Zelle der schwebenden Satz-Tastatur: { matchIndex, setIndex } oder null
|
||||
editingSetCell: null,
|
||||
broadcastDraftTimer: null,
|
||||
draftPersistTimer: null,
|
||||
draftSyncTimer: null,
|
||||
meetingDetailsPollInterval: null,
|
||||
isMeetingDetailsPolling: false,
|
||||
lastMeetingDetailsSignature: '',
|
||||
isDraftSyncing: false,
|
||||
lastDraftSyncError: '',
|
||||
draftLastSavedAt: null,
|
||||
draftLastSyncedAt: null,
|
||||
// Abschluss-Felder
|
||||
protestText: '',
|
||||
finalHomePin: '',
|
||||
@@ -909,12 +922,37 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
}
|
||||
|
||||
return !this.areAllMatchResultsValid() || !this.areStartAndEndTimesValid();
|
||||
},
|
||||
draftStatusState() {
|
||||
if (this.isDraftSyncing) {
|
||||
return 'syncing';
|
||||
}
|
||||
if (this.lastDraftSyncError) {
|
||||
return 'fallback';
|
||||
}
|
||||
if (this.draftLastSavedAt) {
|
||||
return 'saved';
|
||||
}
|
||||
return 'idle';
|
||||
},
|
||||
draftStatusText() {
|
||||
if (this.draftStatusState === 'syncing') {
|
||||
return 'Synchronisiere Entwurf...';
|
||||
}
|
||||
if (this.draftStatusState === 'fallback') {
|
||||
return 'Offline-Fallback aktiv (lokal gespeichert)';
|
||||
}
|
||||
if (this.draftStatusState === 'saved') {
|
||||
return 'Zwischengespeichert';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.loadData();
|
||||
this.initializeResults();
|
||||
this.initializeFinalPins();
|
||||
this.restoreLocalDraft();
|
||||
this.startMeetingDetailsPolling();
|
||||
this._matchReportSubmittedHandler = (payload) => {
|
||||
if (!payload?.matchCode || !this.match?.code || String(payload.matchCode) !== String(this.match.code)) {
|
||||
@@ -936,6 +974,12 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
if (this.broadcastDraftTimer) {
|
||||
clearTimeout(this.broadcastDraftTimer);
|
||||
}
|
||||
if (this.draftPersistTimer) {
|
||||
clearTimeout(this.draftPersistTimer);
|
||||
}
|
||||
if (this.draftSyncTimer) {
|
||||
clearTimeout(this.draftSyncTimer);
|
||||
}
|
||||
['home', 'guest'].forEach(team => {
|
||||
if (this.pinFeedbackTimers[team]) {
|
||||
clearTimeout(this.pinFeedbackTimers[team]);
|
||||
@@ -980,6 +1024,39 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
if (newValue === null && this.activeSection === 'completion' && !this.canOpenNextStages) {
|
||||
this.activeSection = 'general';
|
||||
}
|
||||
|
||||
this.persistLocalDraftDebounced();
|
||||
this.broadcastDraftDebounced();
|
||||
},
|
||||
results: {
|
||||
handler() {
|
||||
this.persistLocalDraftDebounced();
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
meetingDetails: {
|
||||
handler() {
|
||||
this.persistLocalDraftDebounced();
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
homePin() {
|
||||
this.persistLocalDraftDebounced();
|
||||
},
|
||||
guestPin() {
|
||||
this.persistLocalDraftDebounced();
|
||||
},
|
||||
finalHomePin() {
|
||||
this.persistLocalDraftDebounced();
|
||||
},
|
||||
finalGuestPin() {
|
||||
this.persistLocalDraftDebounced();
|
||||
},
|
||||
protestText() {
|
||||
this.persistLocalDraftDebounced();
|
||||
},
|
||||
activeSection() {
|
||||
this.persistLocalDraftDebounced();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -1813,6 +1890,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
|
||||
console.log('✅ Spielbericht erfolgreich abgesendet:', result);
|
||||
this.submitSucceeded = true;
|
||||
this.clearLocalDraft();
|
||||
alert('✅ Spielbericht erfolgreich abgesendet!');
|
||||
|
||||
// Dialog schließen
|
||||
@@ -2626,6 +2704,133 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
return state;
|
||||
},
|
||||
|
||||
getDraftStorageKey() {
|
||||
const code = this.match?.code ? String(this.match.code) : '';
|
||||
const uuid = this.meetingData?.nuLigaMeetingUuid ? String(this.meetingData.nuLigaMeetingUuid) : '';
|
||||
if (!code && !uuid) return null;
|
||||
return `matchReportDraft:${code || uuid}:${uuid || 'nouuid'}`;
|
||||
},
|
||||
buildLocalDraftPayload() {
|
||||
return {
|
||||
savedAt: Date.now(),
|
||||
activeSection: this.activeSection,
|
||||
teamNotAppeared: this.teamNotAppeared,
|
||||
homePin: this.homePin,
|
||||
guestPin: this.guestPin,
|
||||
finalHomePin: this.finalHomePin,
|
||||
finalGuestPin: this.finalGuestPin,
|
||||
protestText: this.protestText,
|
||||
matchStartDate: this.match?.startDate || null,
|
||||
matchEndDate: this.match?.endDate || null,
|
||||
isHomeLineupCertified: this.isHomeLineupCertified,
|
||||
isGuestLineupCertified: this.isGuestLineupCertified,
|
||||
results: JSON.parse(JSON.stringify(this.results || [])),
|
||||
meetingDetails: this.meetingDetails ? {
|
||||
signature: this.meetingDetails.signature ? { ...this.meetingDetails.signature } : null,
|
||||
teamLineupHomePlayers: Array.isArray(this.meetingDetails.teamLineupHomePlayers)
|
||||
? JSON.parse(JSON.stringify(this.meetingDetails.teamLineupHomePlayers))
|
||||
: [],
|
||||
teamLineupGuestPlayers: Array.isArray(this.meetingDetails.teamLineupGuestPlayers)
|
||||
? JSON.parse(JSON.stringify(this.meetingDetails.teamLineupGuestPlayers))
|
||||
: []
|
||||
} : null
|
||||
};
|
||||
},
|
||||
persistLocalDraft() {
|
||||
const key = this.getDraftStorageKey();
|
||||
if (!key) return;
|
||||
try {
|
||||
this.draftLastSavedAt = Date.now();
|
||||
window.localStorage.setItem(key, JSON.stringify(this.buildLocalDraftPayload()));
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Draft konnte nicht lokal gespeichert werden:', error);
|
||||
}
|
||||
},
|
||||
persistLocalDraftDebounced() {
|
||||
if (this.draftPersistTimer) {
|
||||
clearTimeout(this.draftPersistTimer);
|
||||
}
|
||||
this.draftPersistTimer = setTimeout(() => {
|
||||
this.draftPersistTimer = null;
|
||||
this.persistLocalDraft();
|
||||
}, 250);
|
||||
},
|
||||
restoreLocalDraft() {
|
||||
const key = this.getDraftStorageKey();
|
||||
if (!key) return;
|
||||
try {
|
||||
const raw = window.localStorage.getItem(key);
|
||||
if (!raw) return;
|
||||
const draft = JSON.parse(raw);
|
||||
if (!draft || typeof draft !== 'object') return;
|
||||
this.draftLastSavedAt = draft.savedAt || Date.now();
|
||||
|
||||
if (draft.matchStartDate) this.match.startDate = draft.matchStartDate;
|
||||
if (draft.matchEndDate) this.match.endDate = draft.matchEndDate;
|
||||
if (draft.activeSection) this.activeSection = draft.activeSection;
|
||||
if (typeof draft.teamNotAppeared !== 'undefined') this.teamNotAppeared = draft.teamNotAppeared;
|
||||
if (typeof draft.homePin === 'string') this.homePin = draft.homePin;
|
||||
if (typeof draft.guestPin === 'string') this.guestPin = draft.guestPin;
|
||||
if (typeof draft.finalHomePin === 'string') this.finalHomePin = draft.finalHomePin;
|
||||
if (typeof draft.finalGuestPin === 'string') this.finalGuestPin = draft.finalGuestPin;
|
||||
if (typeof draft.protestText === 'string') this.protestText = draft.protestText;
|
||||
if (Array.isArray(draft.results) && draft.results.length > 0) {
|
||||
this.results = JSON.parse(JSON.stringify(draft.results));
|
||||
this.syncResultsToMatch();
|
||||
}
|
||||
if (draft.meetingDetails && this.meetingDetails) {
|
||||
if (Array.isArray(draft.meetingDetails.teamLineupHomePlayers)) {
|
||||
this.meetingDetails.teamLineupHomePlayers = JSON.parse(JSON.stringify(draft.meetingDetails.teamLineupHomePlayers));
|
||||
}
|
||||
if (Array.isArray(draft.meetingDetails.teamLineupGuestPlayers)) {
|
||||
this.meetingDetails.teamLineupGuestPlayers = JSON.parse(JSON.stringify(draft.meetingDetails.teamLineupGuestPlayers));
|
||||
}
|
||||
if (draft.meetingDetails.signature && typeof draft.meetingDetails.signature === 'object') {
|
||||
this.meetingDetails.signature = { ...(this.meetingDetails.signature || {}), ...draft.meetingDetails.signature };
|
||||
}
|
||||
}
|
||||
this.isHomeLineupCertified = Boolean(draft.isHomeLineupCertified);
|
||||
this.isGuestLineupCertified = Boolean(draft.isGuestLineupCertified);
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Draft konnte nicht wiederhergestellt werden:', error);
|
||||
}
|
||||
},
|
||||
clearLocalDraft() {
|
||||
const key = this.getDraftStorageKey();
|
||||
if (!key) return;
|
||||
try {
|
||||
window.localStorage.removeItem(key);
|
||||
} catch (error) {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
syncDraftToServerDebounced(delayMs = 600) {
|
||||
if (this.draftSyncTimer) {
|
||||
clearTimeout(this.draftSyncTimer);
|
||||
}
|
||||
this.draftSyncTimer = setTimeout(async () => {
|
||||
this.draftSyncTimer = null;
|
||||
await this.syncDraftToServer();
|
||||
}, delayMs);
|
||||
},
|
||||
async syncDraftToServer() {
|
||||
if (this.isDraftSyncing || !this.meetingData?.nuLigaMeetingUuid) {
|
||||
return;
|
||||
}
|
||||
this.isDraftSyncing = true;
|
||||
this.lastDraftSyncError = '';
|
||||
try {
|
||||
const matchData = JSON.parse(JSON.stringify(this.match));
|
||||
this.updateMatchData(matchData, { finalizeReport: false });
|
||||
await this.validateReport(matchData);
|
||||
this.draftLastSyncedAt = Date.now();
|
||||
} catch (error) {
|
||||
this.lastDraftSyncError = error?.message || 'sync_failed';
|
||||
console.warn('⚠️ Draft-Sync zu nuscore fehlgeschlagen:', error);
|
||||
} finally {
|
||||
this.isDraftSyncing = false;
|
||||
}
|
||||
},
|
||||
/** Entwurf für Broadcast: meetingDetails-Form mit aktuellen Satzergebnissen aus results. */
|
||||
getDraftMatchData() {
|
||||
if (!this.meetingDetails || !Array.isArray(this.meetingDetails.matches)) return null;
|
||||
@@ -2851,6 +3056,8 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
// Synchronisiere zurück ins Match-Objekt
|
||||
this.syncResultsToMatch();
|
||||
this.broadcastDraftDebounced();
|
||||
this.persistLocalDraftDebounced();
|
||||
this.syncDraftToServerDebounced(800);
|
||||
},
|
||||
|
||||
appendToSet(matchIndex, setIndex, char) {
|
||||
@@ -2911,6 +3118,8 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
m.completed = false;
|
||||
m.result = '1:0';
|
||||
this.broadcastDraftDebounced();
|
||||
this.persistLocalDraftDebounced();
|
||||
this.syncDraftToServerDebounced(800);
|
||||
},
|
||||
async loadClubSettings() {
|
||||
try {
|
||||
@@ -3194,6 +3403,8 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
|
||||
// Synchronisiere zurück ins Match-Objekt
|
||||
this.syncResultsToMatch();
|
||||
this.persistLocalDraftDebounced();
|
||||
this.broadcastDraftDebounced();
|
||||
},
|
||||
|
||||
validateSetScore(homeScore, guestScore, originalInput) {
|
||||
@@ -3346,6 +3557,8 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
const now = new Date();
|
||||
const timeString = now.toTimeString().slice(0, 5); // HH:MM format
|
||||
this.match.startDate = this.createDateTimeFromTimeString(timeString);
|
||||
this.persistLocalDraftDebounced();
|
||||
this.syncDraftToServerDebounced(500);
|
||||
},
|
||||
|
||||
setCurrentEndTime() {
|
||||
@@ -3367,10 +3580,14 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
console.log('✅ Endzeit wurde auf nächsten Tag verschoben:', nextDay.toISOString());
|
||||
}
|
||||
}
|
||||
this.persistLocalDraftDebounced();
|
||||
this.syncDraftToServerDebounced(500);
|
||||
},
|
||||
|
||||
setStartTime(timeString) {
|
||||
this.match.startDate = this.createDateTimeFromTimeString(timeString);
|
||||
this.persistLocalDraftDebounced();
|
||||
this.syncDraftToServerDebounced(500);
|
||||
},
|
||||
|
||||
setEndTime(timeString) {
|
||||
@@ -3390,6 +3607,8 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
console.log('✅ Endzeit wurde auf nächsten Tag verschoben:', nextDay.toISOString());
|
||||
}
|
||||
}
|
||||
this.persistLocalDraftDebounced();
|
||||
this.syncDraftToServerDebounced(500);
|
||||
},
|
||||
|
||||
getFormattedTime(dateValue) {
|
||||
@@ -3537,6 +3756,9 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
}
|
||||
|
||||
}
|
||||
this.persistLocalDraftDebounced();
|
||||
this.broadcastDraftDebounced();
|
||||
this.syncDraftToServerDebounced(700);
|
||||
},
|
||||
|
||||
toggleDoublePosition(player, team, doublePosition) {
|
||||
@@ -3568,6 +3790,9 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
}
|
||||
}
|
||||
}
|
||||
this.persistLocalDraftDebounced();
|
||||
this.broadcastDraftDebounced();
|
||||
this.syncDraftToServerDebounced(700);
|
||||
},
|
||||
|
||||
canSelectDoublePosition(player, team, doublePosition) {
|
||||
@@ -3646,6 +3871,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
}
|
||||
this.clearPinFeedback(team);
|
||||
this.clearSigningFeedback(team);
|
||||
this.persistLocalDraftDebounced();
|
||||
},
|
||||
|
||||
pinInputClasses(team) {
|
||||
@@ -3752,6 +3978,9 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
|
||||
// Validiere und speichere die Änderungen
|
||||
await this.validateReport();
|
||||
this.persistLocalDraft();
|
||||
this.broadcastDraftDebounced();
|
||||
await this.syncDraftToServer();
|
||||
} else {
|
||||
this.showSigningError(team);
|
||||
}
|
||||
@@ -5010,6 +5239,38 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.draft-status-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 10px;
|
||||
padding: 0.25rem 0.6rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--surface-muted);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.draft-status-indicator.state-syncing {
|
||||
border-color: rgba(59, 130, 246, 0.35);
|
||||
background: rgba(59, 130, 246, 0.12);
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.draft-status-indicator.state-saved {
|
||||
border-color: rgba(16, 185, 129, 0.3);
|
||||
background: rgba(16, 185, 129, 0.12);
|
||||
color: #047857;
|
||||
}
|
||||
|
||||
.draft-status-indicator.state-fallback {
|
||||
border-color: rgba(245, 158, 11, 0.35);
|
||||
background: rgba(245, 158, 11, 0.15);
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
font-size: 16px;
|
||||
padding: 12px 24px;
|
||||
|
||||
Reference in New Issue
Block a user