Tournament search and filters
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 17m19s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 17m19s
This commit is contained in:
@@ -20,7 +20,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label v-if="!standaloneTest" class="test-mode-control" :class="{ active: isTestMode }">
|
||||
<label v-if="!standaloneTest && !viewOnly" class="test-mode-control" :class="{ active: isTestMode }">
|
||||
<input v-model="isTestMode" type="checkbox" @change="onTestModeChange" />
|
||||
<span>
|
||||
<strong>Testmodus</strong>
|
||||
@@ -28,7 +28,13 @@
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div v-else class="standalone-test-banner">
|
||||
<div v-if="viewOnly" class="view-only-banner">
|
||||
<span>👁️ Auswärtsspiel – nur Ansicht. Der Spielbericht wird live aktualisiert.</span>
|
||||
<span v-if="lastLiveUpdatedAt">Zuletzt aktualisiert: {{ formatLiveUpdatedAt() }}</span>
|
||||
<button type="button" class="btn-secondary" :disabled="loading" @click="refreshViewOnlyReport">↻ Aktualisieren</button>
|
||||
</div>
|
||||
|
||||
<div v-if="standaloneTest" class="standalone-test-banner">
|
||||
<span>🧪 Eigenständiger Simulator – keine Verbindung zu nuScore</span>
|
||||
<label>
|
||||
Spielsystem
|
||||
@@ -110,7 +116,7 @@
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nuscore-sections">
|
||||
<div class="nuscore-sections">
|
||||
<div class="section-grid">
|
||||
<button class="section-btn" :class="{ active: activeSection === 'general' }" @click="setActiveSection('general')">
|
||||
<div class="section-icon">ℹ️</div>
|
||||
@@ -178,8 +184,9 @@
|
||||
:value="getFormattedTime(match.startDate)"
|
||||
@change="setStartTime($event.target.value)"
|
||||
class="time-input"
|
||||
:disabled="viewOnly"
|
||||
/>
|
||||
<button @click="setCurrentStartTime" class="time-btn" :title="$t('matchReportApi.setCurrentTime')">🕐</button>
|
||||
<button @click="setCurrentStartTime" class="time-btn" :title="$t('matchReportApi.setCurrentTime')" :disabled="viewOnly">🕐</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -191,8 +198,9 @@
|
||||
:value="getFormattedTime(match.endDate)"
|
||||
@change="setEndTime($event.target.value)"
|
||||
class="time-input"
|
||||
:disabled="viewOnly"
|
||||
/>
|
||||
<button @click="setCurrentEndTime" class="time-btn" :title="$t('matchReportApi.setCurrentTime')">🕐</button>
|
||||
<button @click="setCurrentEndTime" class="time-btn" :title="$t('matchReportApi.setCurrentTime')" :disabled="viewOnly">🕐</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -209,6 +217,7 @@
|
||||
v-model="teamNotAppeared"
|
||||
class="not-appeared-select"
|
||||
:class="{ 'not-appeared': teamNotAppeared !== null }"
|
||||
:disabled="viewOnly"
|
||||
>
|
||||
<option :value="null">{{ $t('matchReportApi.bothTeamsAppeared') }}</option>
|
||||
<option value="home">{{ $t('matchReportApi.homeTeamNotAppeared', { team: meetingData.homeTeamname || meetingData.homeClub }) }}</option>
|
||||
@@ -462,7 +471,7 @@
|
||||
<div v-else-if="activeSection === 'greeting'" class="greeting-content">
|
||||
<div class="greeting-header">
|
||||
<h3>Begrüßung</h3>
|
||||
<button
|
||||
<button v-if="!viewOnly"
|
||||
@click="toggleGreetingCompleted()"
|
||||
class="completion-toggle-btn"
|
||||
:class="{ 'completed': isGreetingCompleted }"
|
||||
@@ -479,7 +488,7 @@
|
||||
<div class="time-row">
|
||||
<strong>Startzeit:</strong>
|
||||
<span>{{ formatTime(match.startDate) }}</span>
|
||||
<button @click="setCurrentStartTime()" class="time-btn-small" :disabled="isMatchCompleted">🕐 jetzt setzen</button>
|
||||
<button v-if="!viewOnly" @click="setCurrentStartTime()" class="time-btn-small" :disabled="isMatchCompleted">🕐 jetzt setzen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -537,8 +546,8 @@
|
||||
</td>
|
||||
<td class="state-cell">
|
||||
<div class="state-content">
|
||||
<button v-if="!m.completed" class="state-btn" @click="completeMatch(idx)" title="Match abschließen">✔</button>
|
||||
<button v-else class="state-btn" @click="reopenMatch(idx)" title="Match wieder öffnen">✎</button>
|
||||
<button v-if="!viewOnly && !m.completed" class="state-btn" @click="completeMatch(idx)" title="Match abschließen">✔</button>
|
||||
<button v-else-if="!viewOnly" class="state-btn" @click="reopenMatch(idx)" title="Match wieder öffnen">✎</button>
|
||||
<div class="set-ratio">{{ getSetRatio(m) }}</div>
|
||||
</div>
|
||||
</td>
|
||||
@@ -557,7 +566,7 @@
|
||||
</table>
|
||||
|
||||
<!-- Auto-Fill Button -->
|
||||
<div class="autofill-section">
|
||||
<div v-if="!viewOnly" class="autofill-section">
|
||||
<button @click="autoFillEmptyMatches()" class="autofill-btn" :disabled="isMatchCompleted">
|
||||
📝 Auto-ausfüllen: Leere Positionen mit 3:0 automatisch ausfüllen
|
||||
</button>
|
||||
@@ -577,7 +586,7 @@
|
||||
<div class="time-row">
|
||||
<strong>Endzeit:</strong>
|
||||
<span>{{ formatTime(match.endDate) }}</span>
|
||||
<button @click="setEndTimeAndNavigate()" class="time-btn-small" :disabled="isMatchCompleted">🕐 jetzt setzen</button>
|
||||
<button v-if="!viewOnly" @click="setEndTimeAndNavigate()" class="time-btn-small" :disabled="isMatchCompleted">🕐 jetzt setzen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -853,6 +862,10 @@ export default {
|
||||
standaloneTest: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
viewOnly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -862,6 +875,7 @@ export default {
|
||||
isTestMode: this.standaloneTest,
|
||||
testVerification: null,
|
||||
testSubmissionResult: null,
|
||||
lastLiveUpdatedAt: null,
|
||||
originalTestInput: null,
|
||||
// Unveränderliche Referenz auf den offiziellen Bericht, bevor der
|
||||
// Testmodus seinen vollständig leeren lokalen Zustand erzeugt.
|
||||
@@ -1007,6 +1021,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
return sets[this.editingSetCell.setIndex] || '';
|
||||
},
|
||||
canOpenNextStages() {
|
||||
if (this.viewOnly) return true;
|
||||
// Wenn das Match bereits abgeschlossen ist, dürfen keine Änderungen mehr gemacht werden
|
||||
if (this.isMatchCompleted) {
|
||||
return false;
|
||||
@@ -1020,12 +1035,14 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
return this.isHomeLineupCertified && this.isGuestLineupCertified;
|
||||
},
|
||||
canOpenResultStage() {
|
||||
if (this.viewOnly) return true;
|
||||
if (this.teamNotAppeared !== null) {
|
||||
return false;
|
||||
}
|
||||
return this.areBothLineupsCertified();
|
||||
},
|
||||
canOpenCompletionStage() {
|
||||
if (this.viewOnly) return true;
|
||||
if (this.isMatchCompleted) {
|
||||
return false;
|
||||
}
|
||||
@@ -1242,6 +1259,16 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
this.meetingDetailsPollInterval = null;
|
||||
}
|
||||
},
|
||||
async refreshViewOnlyReport() {
|
||||
if (!this.viewOnly || this.loading) return;
|
||||
await this.loadData();
|
||||
},
|
||||
formatLiveUpdatedAt() {
|
||||
if (!this.lastLiveUpdatedAt) return '';
|
||||
return new Intl.DateTimeFormat('de-DE', {
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit'
|
||||
}).format(this.lastLiveUpdatedAt);
|
||||
},
|
||||
buildMeetingDetailsSignature(details) {
|
||||
if (!details || typeof details !== 'object') {
|
||||
return '';
|
||||
@@ -1319,6 +1346,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
|
||||
this.lastMeetingDetailsSignature = latestSignature;
|
||||
this.meetingDetails = latestMeetingDetails;
|
||||
this.lastLiveUpdatedAt = new Date();
|
||||
|
||||
if (latestMeetingDetails.startDate != null && this.meetingData) {
|
||||
this.meetingData.startDate = latestMeetingDetails.startDate;
|
||||
@@ -1459,6 +1487,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
|
||||
// Toggle Begrüßung abgeschlossen/nicht abgeschlossen
|
||||
toggleGreetingCompleted() {
|
||||
if (this.viewOnly) return;
|
||||
this.isGreetingCompleted = !this.isGreetingCompleted;
|
||||
|
||||
if (this.isGreetingCompleted) {
|
||||
@@ -1482,6 +1511,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
|
||||
// Setze aktuelle Zeit für Startzeit
|
||||
setCurrentStartTime() {
|
||||
if (this.viewOnly) return;
|
||||
const now = new Date();
|
||||
const timeString = now.toTimeString().substring(0, 8); // HH:MM:SS Format
|
||||
this.match.startDate = now;
|
||||
@@ -1501,11 +1531,13 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
|
||||
// Setze aktuelle Zeit für Startzeit (Legacy für Kompatibilität)
|
||||
setCurrentEndTime() {
|
||||
if (this.viewOnly) return;
|
||||
this.setEndTimeAndNavigate();
|
||||
},
|
||||
|
||||
// Auto-Fill für leere Matches
|
||||
autoFillEmptyMatches() {
|
||||
if (this.viewOnly) return;
|
||||
console.log('🔧 Starte Auto-Fill für leere Matches...');
|
||||
|
||||
this.results.forEach((match, index) => {
|
||||
@@ -1990,6 +2022,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
|
||||
async submitMatchReport() {
|
||||
try {
|
||||
if (this.viewOnly) return;
|
||||
console.log('🚀 Starte Spielbericht-Absendung...');
|
||||
|
||||
if (this.isTestMode) {
|
||||
@@ -2852,6 +2885,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
if (detailsResponse.ok) {
|
||||
this.meetingDetails = await detailsResponse.json();
|
||||
this.lastMeetingDetailsSignature = this.buildMeetingDetailsSignature(this.meetingDetails);
|
||||
this.lastLiveUpdatedAt = new Date();
|
||||
console.log('[nuscore] /meetingdetails response:', this.meetingDetails);
|
||||
this.logBraunschweigerDebugSnapshot('Server /meetingdetails');
|
||||
}
|
||||
@@ -3611,6 +3645,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
}
|
||||
},
|
||||
completeMatch(idx) {
|
||||
if (this.viewOnly) return;
|
||||
const m = this.results[idx];
|
||||
|
||||
// Validiere ob Match korrekt abgeschlossen werden kann
|
||||
@@ -3687,6 +3722,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
this.closeSetKeyboard();
|
||||
},
|
||||
reopenMatch(idx) {
|
||||
if (this.viewOnly) return;
|
||||
const m = this.results[idx];
|
||||
m.completed = false;
|
||||
m.result = '1:0';
|
||||
@@ -3822,6 +3858,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
|
||||
// Prüfe ob ein Satz-Input-Feld deaktiviert werden sollte
|
||||
isSetInputDisabled(matchIndex, setIndex) {
|
||||
if (this.viewOnly) return true;
|
||||
const match = this.results[matchIndex];
|
||||
|
||||
// Wenn Match bereits abgeschlossen, alle Felder deaktiviert
|
||||
@@ -4312,6 +4349,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
},
|
||||
|
||||
togglePlayerSelection(player, team) {
|
||||
if (this.viewOnly) return;
|
||||
if (this.isLineupLocked(team)) return;
|
||||
if (!this.meetingDetails) return;
|
||||
|
||||
@@ -4352,6 +4390,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
},
|
||||
|
||||
toggleDoublePosition(player, team, doublePosition) {
|
||||
if (this.viewOnly) return;
|
||||
if (this.isLineupLocked(team)) return;
|
||||
if (!this.meetingDetails) return;
|
||||
|
||||
@@ -4386,6 +4425,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
},
|
||||
|
||||
canSelectDoublePosition(player, team, doublePosition) {
|
||||
if (this.viewOnly) return false;
|
||||
if (!this.meetingDetails) return false;
|
||||
|
||||
const teamKey = team === 'home' ? 'teamLineupHomePlayers' : 'teamLineupGuestPlayers';
|
||||
@@ -4538,6 +4578,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
|
||||
// Aufstellung signieren
|
||||
async signLineup(team) {
|
||||
if (this.viewOnly) return;
|
||||
if (this.isLineupLocked(team)) {
|
||||
return;
|
||||
}
|
||||
@@ -5204,6 +5245,21 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.view-only-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
margin: 0 0 16px;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid #7b95a8;
|
||||
border-radius: 10px;
|
||||
background: #eef5fa;
|
||||
color: #254d68;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.test-mode-control.active {
|
||||
border-color: #d78a00;
|
||||
background: #fff3cd;
|
||||
|
||||
@@ -13,12 +13,28 @@
|
||||
|
||||
<section v-if="workspaceTab === 'suggestions'" class="workspace-panel tournament-suggestions-panel" aria-label="Turniervorschläge">
|
||||
<div class="panel-header"><h3>Turniervorschläge</h3><p>Turniere aus dem myTischtennis-Kalender prüfen und danach manuell einpflegen.</p></div>
|
||||
<div class="panel-toolbar"><button class="btn-primary" :disabled="fetchingSuggestions" @click="fetchSuggestions">{{ fetchingSuggestions ? 'Abruf läuft …' : 'Jetzt abrufen' }}</button><span class="toolbar-meta">{{ newSuggestions.length }} neu</span></div>
|
||||
<div class="panel-toolbar suggestion-toolbar">
|
||||
<button class="btn-primary" :disabled="fetchingSuggestions" @click="fetchSuggestions">{{ fetchingSuggestions ? 'Abruf läuft …' : 'Jetzt abrufen' }}</button>
|
||||
<input
|
||||
v-model.trim="suggestionSearch"
|
||||
type="search"
|
||||
class="title-input"
|
||||
placeholder="Turnier suchen"
|
||||
aria-label="Turniervorschläge suchen"
|
||||
/>
|
||||
<select v-model="suggestionStatusFilter" aria-label="Status der Turniervorschläge filtern">
|
||||
<option value="new">Neue</option>
|
||||
<option value="reviewed">Geprüfte</option>
|
||||
<option value="dismissed">Ausgeblendete</option>
|
||||
<option value="all">Alle</option>
|
||||
</select>
|
||||
<span class="toolbar-meta">{{ filteredSuggestions.length }} angezeigt · {{ newSuggestions.length }} neu</span>
|
||||
</div>
|
||||
<p v-if="suggestionError" class="suggestion-error">{{ suggestionError }}</p>
|
||||
<ul v-if="newSuggestions.length" class="event-list suggestion-list">
|
||||
<li v-for="suggestion in newSuggestions" :key="suggestion.id" class="event-item"><div class="suggestion-main"><strong>{{ suggestion.title }}</strong><span>{{ suggestion.eventDate || 'Termin offen' }}<template v-if="suggestion.location"> · {{ suggestion.location }}</template></span></div><a class="btn-secondary" :href="suggestion.sourceUrl" target="_blank" rel="noopener">Öffnen</a><button class="btn-secondary" @click="updateSuggestionStatus(suggestion, 'reviewed')">Geprüft</button><button class="btn-secondary" @click="updateSuggestionStatus(suggestion, 'dismissed')">Ausblenden</button></li>
|
||||
<ul v-if="filteredSuggestions.length" class="event-list suggestion-list">
|
||||
<li v-for="suggestion in filteredSuggestions" :key="suggestion.id" class="event-item"><div class="suggestion-main"><strong>{{ suggestion.title }}</strong><span>{{ suggestion.eventDate || 'Termin offen' }}<template v-if="suggestion.location"> · {{ suggestion.location }}</template></span></div><a class="btn-secondary" :href="suggestion.sourceUrl" target="_blank" rel="noopener">Öffnen</a><template v-if="suggestion.status === 'dismissed'"><button class="btn-secondary" @click="updateSuggestionStatus(suggestion, 'new')">Einblenden</button></template><template v-else><button class="btn-secondary" @click="updateSuggestionStatus(suggestion, suggestion.status === 'reviewed' ? 'new' : 'reviewed')">{{ suggestion.status === 'reviewed' ? 'Als neu markieren' : 'Geprüft' }}</button><button class="btn-secondary" @click="updateSuggestionStatus(suggestion, 'dismissed')">Ausblenden</button></template></li>
|
||||
</ul>
|
||||
<p v-else class="empty-state compact">Keine neuen Vorschläge. „Jetzt abrufen“ eignet sich zum Testen.</p>
|
||||
<p v-else class="empty-state compact">{{ suggestionSearch ? 'Keine passenden Turniervorschläge gefunden.' : 'Keine Turniervorschläge in dieser Ansicht. „Jetzt abrufen“ aktualisiert den Kalender.' }}</p>
|
||||
</section>
|
||||
<section v-else-if="workspaceTab === 'import'" class="workspace-panel" aria-label="Turnier importieren">
|
||||
<div class="panel-header">
|
||||
@@ -652,6 +668,7 @@ export default {
|
||||
editingTitle: '',
|
||||
autoRegistering: false,
|
||||
suggestions: [], fetchingSuggestions: false, suggestionError: '',
|
||||
suggestionSearch: '', suggestionStatusFilter: 'new',
|
||||
tacticPlans: [], tacticPlan: null, tacticPlansSnapshot: '', tacticsLoading: false, tacticSaving: false, tacticsError: '', tacticTournamentId: null, tacticLoadToken: 0, reloadToken: 0,
|
||||
tournamentObservationDialog: { isOpen: false, member: null, notes: [] },
|
||||
};
|
||||
@@ -731,6 +748,18 @@ export default {
|
||||
});
|
||||
},
|
||||
newSuggestions() { return (this.suggestions || []).filter((suggestion) => suggestion.status === 'new'); },
|
||||
filteredSuggestions() {
|
||||
const search = String(this.suggestionSearch || '').trim().toLocaleLowerCase('de');
|
||||
return (this.suggestions || []).filter((suggestion) => {
|
||||
if (this.suggestionStatusFilter !== 'all' && suggestion.status !== this.suggestionStatusFilter) return false;
|
||||
if (!search) return true;
|
||||
return [suggestion.title, suggestion.eventDate, suggestion.location, suggestion.organizer, suggestion.federation, suggestion.sourceUrl]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.toLocaleLowerCase('de')
|
||||
.includes(search);
|
||||
});
|
||||
},
|
||||
historySummaryText() {
|
||||
const rows = this.clubParticipationRows();
|
||||
if (this.loadingClubParticipations) return 'Turnierbeteiligungen werden geladen';
|
||||
|
||||
@@ -2860,7 +2860,8 @@ export default {
|
||||
title,
|
||||
component: 'MatchReportApiDialog',
|
||||
props: {
|
||||
match
|
||||
match,
|
||||
viewOnly: !this.isOurClubPlayingHome(match)
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user