feat(DialogExamples, DiaryParticipantsPanel, ImageViewerDialog, MatchReportApiDialog, MemberGalleryDialog, LogsView, TeamManagementView, TournamentTab, i18n): enhance UI components and localization

- Updated various UI components to improve styling and user experience, including DialogExamples, DiaryParticipantsPanel, ImageViewerDialog, and MatchReportApiDialog.
- Introduced new participant status filter in DiaryParticipantsPanel, allowing for 'excused' status.
- Enhanced image upload section in ImageViewerDialog with improved layout and styling.
- Refactored MatchReportApiDialog to streamline pin input handling and feedback mechanisms.
- Added functionality to filter members in MemberGalleryDialog based on a new `shouldShowMember` prop.
- Improved styling in LogsView for better readability and user interaction.
- Refactored TeamManagementView to utilize a new TeamListCard component for better code organization and maintainability.
- Updated TournamentTab to enhance the tournament workspace header with improved data display and interaction.
- Expanded localization files to include new keys for participant status and other UI elements, enhancing accessibility for users in both English and German.
This commit is contained in:
Torsten Schulz (local)
2026-03-17 16:00:30 +01:00
parent 483d5d2bc7
commit 6320c5ca72
18 changed files with 875 additions and 506 deletions

View File

@@ -64,7 +64,7 @@
>
<p>{{ $t('dialogExamples.largeModalText') }}</p>
<p>{{ $t('dialogExamples.largeModalText2') }}</p>
<div style="height: 400px; background: #f5f5f5; margin-top: 1rem; padding: 1rem;">
<div class="example-scroll-area">
{{ $t('dialogExamples.scrollArea') }}
</div>
</BaseDialog>
@@ -368,9 +368,10 @@ export default {
.example-section {
margin-bottom: 2rem;
padding: 1rem;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
background: var(--surface-color);
border: 1px solid var(--border-color);
border-radius: 12px;
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
}
.example-section h3 {
@@ -385,13 +386,23 @@ export default {
flex-wrap: wrap;
}
.example-scroll-area {
height: 400px;
margin-top: 1rem;
padding: 1rem;
border: 1px dashed var(--border-color);
border-radius: 12px;
background: var(--surface-muted);
color: var(--text-muted);
}
.btn-primary,
.btn-secondary,
.btn-warning,
.btn-danger {
padding: 0.5rem 1.5rem;
border: none;
border-radius: 4px;
border: 1px solid transparent;
border-radius: 10px;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
@@ -400,48 +411,53 @@ export default {
.btn-primary {
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: white;
color: var(--text-on-primary);
}
.btn-primary:hover {
opacity: 0.9;
transform: translateY(-1px);
}
.btn-secondary {
background: #6c757d;
color: white;
background: var(--surface-color);
border-color: var(--border-color);
color: var(--text-color);
}
.btn-secondary:hover {
background: #5a6268;
background: var(--surface-muted);
border-color: var(--primary-soft);
}
.btn-warning {
background: #ffc107;
color: #212529;
background: rgba(181, 110, 65, 0.14);
border-color: rgba(181, 110, 65, 0.24);
color: #8a4f28;
}
.btn-warning:hover {
background: #e0a800;
background: rgba(181, 110, 65, 0.2);
}
.btn-danger {
background: #dc3545;
color: white;
background: rgba(200, 74, 56, 0.12);
border-color: rgba(200, 74, 56, 0.24);
color: #8b3327;
}
.btn-danger:hover {
background: #c82333;
background: rgba(200, 74, 56, 0.18);
}
.minimized-section {
position: fixed;
bottom: 2rem;
right: 2rem;
background: white;
background: var(--surface-color);
border: 1px solid var(--border-color);
padding: 1rem;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
border-radius: 12px;
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.12);
}
.minimized-section h4 {
@@ -454,16 +470,15 @@ export default {
display: block;
margin-bottom: 0.5rem;
padding: 0.5rem 1rem;
background: var(--primary-color);
color: white;
border: none;
border-radius: 4px;
background: var(--surface-color);
color: var(--primary-strong);
border: 1px solid var(--primary-soft);
border-radius: 10px;
cursor: pointer;
font-size: 0.875rem;
}
.minimized-btn:hover {
background: var(--primary-hover);
background: rgba(47, 122, 95, 0.1);
}
</style>

View File

@@ -23,6 +23,7 @@
<button type="button" class="participant-filter-chip" :class="{ active: participantFilter === 'all' }" @click="$emit('update:participantFilter', 'all')">{{ $t('diary.filterAll') }}</button>
<button type="button" class="participant-filter-chip" :class="{ active: participantFilter === 'present' }" @click="$emit('update:participantFilter', 'present')">{{ $t('diary.filterPresent') }}</button>
<button type="button" class="participant-filter-chip" :class="{ active: participantFilter === 'absent' }" @click="$emit('update:participantFilter', 'absent')">{{ $t('diary.filterAbsent') }}</button>
<button type="button" class="participant-filter-chip" :class="{ active: participantFilter === 'excused' }" @click="$emit('update:participantFilter', 'excused')">{{ $t('diary.filterExcused') }}</button>
<button type="button" class="participant-filter-chip" :class="{ active: participantFilter === 'test' }" @click="$emit('update:participantFilter', 'test')">{{ $t('diary.filterTest') }}</button>
</div>
</div>

View File

@@ -79,13 +79,13 @@
</div>
<div v-if="allowUpload" class="upload-section">
<div style="display: flex; gap: 10px; align-items: center;">
<div class="upload-actions">
<input type="file" multiple accept="image/*" @change="handleFileSelect" ref="fileInput" style="display: none;" id="image-upload-file">
<label for="image-upload-file" class="upload-label" style="cursor: pointer; padding: 8px 15px; border: 1px solid #ccc; border-radius: 4px; background: #f5f5f5; display: inline-block;">
<label for="image-upload-file" class="upload-label">
📁 {{ $t('imageViewer.selectFiles') }}
</label>
<input type="file" multiple accept="image/*" capture="environment" @change="handleFileSelect" ref="cameraInput" style="display: none;" id="image-upload-camera">
<label for="image-upload-camera" class="upload-label" style="cursor: pointer; padding: 8px 15px; border: 1px solid #ccc; border-radius: 4px; background: #f5f5f5; display: inline-block;">
<label for="image-upload-camera" class="upload-label">
📷 {{ $t('imageViewer.camera') }}
</label>
</div>
@@ -333,8 +333,9 @@ export default {
justify-content: center;
align-items: center;
min-height: 220px;
background: #f5f5f5;
border-radius: 6px;
background: var(--surface-muted);
border: 1px solid var(--border-color);
border-radius: 12px;
overflow: hidden;
position: relative;
}
@@ -354,9 +355,9 @@ export default {
}
.nav-button {
border: none;
background: rgba(0, 0, 0, 0.4);
color: #fff;
border: 1px solid var(--border-color);
background: rgba(255, 255, 255, 0.88);
color: var(--text-color);
width: 36px;
height: 36px;
border-radius: 50%;
@@ -365,12 +366,13 @@ export default {
justify-content: center;
font-size: 1.5rem;
cursor: pointer;
transition: background 0.2s ease;
transition: background 0.2s ease, border-color 0.2s ease;
margin: 0 0.5rem;
}
.nav-button:hover {
background: rgba(0, 0, 0, 0.6);
background: var(--surface-color);
border-color: var(--primary-soft);
}
.image-actions {
@@ -383,8 +385,8 @@ export default {
.action-btn {
padding: 0.5rem 1.25rem;
border: 1px solid var(--border-color);
border-radius: 4px;
background: white;
border-radius: 10px;
background: var(--surface-color);
color: var(--text-color);
font-size: 0.9rem;
cursor: pointer;
@@ -392,18 +394,18 @@ export default {
}
.action-btn:hover {
background: var(--primary-light);
border-color: var(--primary-color);
color: var(--primary-color);
background: rgba(47, 122, 95, 0.1);
border-color: var(--primary-soft);
color: var(--primary-strong);
}
.action-btn--danger {
border-color: #dc3545;
color: #dc3545;
border-color: rgba(200, 74, 56, 0.24);
color: #8b3327;
}
.action-btn--danger:hover {
background: #dc354514;
background: rgba(200, 74, 56, 0.12);
}
.upload-section {
@@ -411,20 +413,28 @@ export default {
justify-content: center;
}
.upload-actions {
display: flex;
gap: 10px;
align-items: center;
}
.upload-label {
position: relative;
padding: 0.6rem 1.4rem;
border: 1px dashed var(--border-color);
border-radius: 6px;
border-radius: 10px;
cursor: pointer;
color: var(--text-color);
background: var(--surface-muted);
display: inline-block;
font-size: 0.95rem;
transition: border 0.2s ease, background 0.2s ease;
}
.upload-label:hover {
border-color: var(--primary-color);
background: var(--primary-light);
border-color: var(--primary-soft);
background: rgba(47, 122, 95, 0.1);
}
.upload-label input {
@@ -445,7 +455,7 @@ export default {
.thumbnail {
width: 80px;
height: 80px;
border-radius: 4px;
border-radius: 10px;
overflow: hidden;
cursor: pointer;
position: relative;
@@ -472,8 +482,8 @@ export default {
position: absolute;
bottom: 4px;
right: 4px;
background: rgba(40, 167, 69, 0.85);
color: #fff;
background: rgba(47, 122, 95, 0.9);
color: var(--text-on-primary);
padding: 2px 6px;
font-size: 0.65rem;
border-radius: 12px;
@@ -487,18 +497,19 @@ export default {
.btn-secondary {
padding: 0.5rem 1.5rem;
border: none;
border-radius: 4px;
border: 1px solid var(--border-color);
border-radius: 10px;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
background: #6c757d;
color: white;
background: var(--surface-color);
color: var(--text-color);
}
.btn-secondary:hover {
background: #5a6268;
background: var(--surface-muted);
border-color: var(--primary-soft);
}
@media (max-width: 768px) {
@@ -515,4 +526,3 @@ export default {
}
}
</style>

View File

@@ -142,11 +142,11 @@
<div class="pin-input-group">
<label for="homePin">{{ $t('matchReportApi.homePin') }}:</label>
<div class="pin-input-wrapper">
<input id="homePin" v-model="homePin" type="password" class="pin-input"
<input id="homePin" v-model="homePin" type="password" :class="pinInputClasses('home')"
@input="onPinChange('home', $event)" />
<button @click="signLineup('home')" class="sign-btn"
<button @click="signLineup('home')" :class="signButtonClasses('home')"
:disabled="!canSignLineup('home')">
{{ $t('matchReportApi.signLineup') }}
{{ signButtonLabel('home') }}
</button>
</div>
<!-- Fehlermeldung für Mindestspielerzahlen -->
@@ -234,14 +234,14 @@
<div class="pin-input-group">
<label for="guestPin">{{ $t('matchReportApi.guestPin') }}:</label>
<div class="pin-input-wrapper">
<input id="guestPin" v-model="guestPin" type="password" class="pin-input"
<input id="guestPin" v-model="guestPin" type="password" :class="pinInputClasses('guest')"
autocomplete="new-password" autocapitalize="off" spellcheck="false"
:readonly="!(match && match.guestPin)"
@focus="(match && match.guestPin) ? null : (guestPin='', $event.target.removeAttribute('readonly'))"
@input="onPinChange('guest', $event)" />
<button @click="signLineup('guest')" class="sign-btn"
<button @click="signLineup('guest')" :class="signButtonClasses('guest')"
:disabled="!canSignLineup('guest')">
{{ $t('matchReportApi.signLineup') }}
{{ signButtonLabel('guest') }}
</button>
</div>
<!-- Fehlermeldung für Mindestspielerzahlen -->
@@ -715,6 +715,22 @@ export default {
activeSection: 'general',
homePin: '',
guestPin: '',
pinFeedback: {
home: '',
guest: ''
},
signingFeedback: {
home: '',
guest: ''
},
pinFeedbackTimers: {
home: null,
guest: null
},
signingFeedbackTimers: {
home: null,
guest: null
},
isHomeLineupCertified: false,
isGuestLineupCertified: false,
isGreetingCompleted: false,
@@ -884,6 +900,14 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
if (this.broadcastDraftTimer) {
clearTimeout(this.broadcastDraftTimer);
}
['home', 'guest'].forEach(team => {
if (this.pinFeedbackTimers[team]) {
clearTimeout(this.pinFeedbackTimers[team]);
}
if (this.signingFeedbackTimers[team]) {
clearTimeout(this.signingFeedbackTimers[team]);
}
});
},
watch: {
teamNotAppeared(newValue, oldValue) {
@@ -1571,69 +1595,6 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
}
},
showMatchDataDialog(matchData) {
// Erstelle einen neuen Dialog für die Datenanzeige
const dialog = document.createElement('div');
dialog.className = 'match-data-dialog-overlay';
const dialogContent = document.createElement('div');
dialogContent.className = 'match-data-dialog';
const header = document.createElement('div');
header.className = 'match-data-dialog-header';
header.innerHTML = `
<h3>📋 Vollständiges Match-Objekt</h3>
<button class="close-dialog-btn" onclick="this.closest('.match-data-dialog-overlay').remove()">✕</button>
`;
const content = document.createElement('div');
content.className = 'match-data-dialog-content';
// Pretty-print das JSON
const jsonString = JSON.stringify(matchData, null, 2);
const pre = document.createElement('pre');
pre.textContent = jsonString;
pre.className = 'json-display';
const copyButton = document.createElement('button');
copyButton.className = 'copy-json-btn';
copyButton.textContent = '📋 JSON kopieren';
copyButton.onclick = () => {
navigator.clipboard.writeText(jsonString).then(() => {
copyButton.textContent = '✅ Kopiert!';
setTimeout(() => {
copyButton.textContent = '📋 JSON kopieren';
}, 2000);
});
};
content.appendChild(pre);
content.appendChild(copyButton);
dialogContent.appendChild(header);
dialogContent.appendChild(content);
dialog.appendChild(dialogContent);
// Dialog zum Body hinzufügen
document.body.appendChild(dialog);
// Dialog schließen bei Klick außerhalb
dialog.onclick = (e) => {
if (e.target === dialog) {
dialog.remove();
}
};
// ESC-Taste zum Schließen
const handleEsc = (e) => {
if (e.key === 'Escape') {
dialog.remove();
document.removeEventListener('keydown', handleEsc);
}
};
document.addEventListener('keydown', handleEsc);
},
updateMatchData(matchData) {
try {
console.log('🔄 updateMatchData: Verwende kompletten Original-Meeting-Daten...');
@@ -2999,27 +2960,6 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
}
},
async copyCode(event) {
try {
await navigator.clipboard.writeText(this.meetingData.gameCode);
// Visuelles Feedback
const button = event.target;
const originalText = button.textContent;
button.textContent = '✅ Kopiert!';
button.style.backgroundColor = '#28a745';
setTimeout(() => {
button.textContent = originalText;
button.style.backgroundColor = '';
}, 2000);
} catch (error) {
console.error('❌ Fehler beim Kopieren:', error);
}
},
togglePlayerSelection(player, team) {
if (!this.meetingDetails) return;
@@ -3140,6 +3080,80 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
} else {
this.guestPin = pin;
}
this.clearPinFeedback(team);
this.clearSigningFeedback(team);
},
pinInputClasses(team) {
return [
'pin-input',
{
'pin-input-success': this.pinFeedback[team] === 'success' || this.signingFeedback[team] === 'success',
'pin-input-error': this.pinFeedback[team] === 'error' || this.signingFeedback[team] === 'error'
}
];
},
signButtonClasses(team) {
return [
'sign-btn',
{
'sign-btn-success': this.signingFeedback[team] === 'success',
'sign-btn-error': this.signingFeedback[team] === 'error'
}
];
},
signButtonLabel(team) {
if (this.signingFeedback[team] === 'success') {
return '✅ Signiert!';
}
if (this.signingFeedback[team] === 'error') {
return `✍️ ${this.$t('matchReportApi.signLineup')}`;
}
return `✍️ ${this.$t('matchReportApi.signLineup')}`;
},
scheduleFeedbackReset(storeName, timerStoreName, team, delay = 3000) {
if (this[timerStoreName][team]) {
clearTimeout(this[timerStoreName][team]);
}
this[timerStoreName][team] = setTimeout(() => {
this[storeName][team] = '';
this[timerStoreName][team] = null;
}, delay);
},
setPinFeedback(team, state) {
this.pinFeedback[team] = state;
if (!state) {
if (this.pinFeedbackTimers[team]) {
clearTimeout(this.pinFeedbackTimers[team]);
this.pinFeedbackTimers[team] = null;
}
return;
}
this.scheduleFeedbackReset('pinFeedback', 'pinFeedbackTimers', team);
},
clearPinFeedback(team) {
this.setPinFeedback(team, '');
},
setSigningFeedback(team, state) {
this.signingFeedback[team] = state;
if (!state) {
if (this.signingFeedbackTimers[team]) {
clearTimeout(this.signingFeedbackTimers[team]);
this.signingFeedbackTimers[team] = null;
}
return;
}
this.scheduleFeedbackReset('signingFeedback', 'signingFeedbackTimers', team);
},
clearSigningFeedback(team) {
this.setSigningFeedback(team, '');
},
// Aufstellung signieren
@@ -3316,59 +3330,18 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
},
showPinValidation(team, isValid, algorithm = null) {
// Visuelles Feedback für PIN-Validierung
const input = document.getElementById(team === 'home' ? 'homePin' : 'guestPin');
if (input) {
if (isValid) {
input.style.borderColor = '#28a745';
input.style.backgroundColor = '#d4edda';
} else {
input.style.borderColor = '#dc3545';
input.style.backgroundColor = '#f8d7da';
}
// Reset nach 3 Sekunden
setTimeout(() => {
input.style.borderColor = '';
input.style.backgroundColor = '';
}, 3000);
}
void algorithm;
this.setPinFeedback(team, isValid ? 'success' : 'error');
},
showSigningSuccess(team) {
const input = document.getElementById(team === 'home' ? 'homePin' : 'guestPin');
if (input) {
input.style.borderColor = '#28a745';
input.style.backgroundColor = '#d4edda';
// Button temporär deaktivieren
const button = input.parentElement.querySelector('.sign-btn');
if (button) {
const originalText = button.textContent;
button.textContent = '✅ Signiert!';
button.disabled = true;
setTimeout(() => {
button.textContent = originalText;
button.disabled = false;
input.style.borderColor = '';
input.style.backgroundColor = '';
}, 3000);
}
}
this.setPinFeedback(team, 'success');
this.setSigningFeedback(team, 'success');
},
showSigningError(team) {
const input = document.getElementById(team === 'home' ? 'homePin' : 'guestPin');
if (input) {
input.style.borderColor = '#dc3545';
input.style.backgroundColor = '#f8d7da';
setTimeout(() => {
input.style.borderColor = '';
input.style.backgroundColor = '';
}, 3000);
}
this.setPinFeedback(team, 'error');
this.setSigningFeedback(team, 'error');
},
canSignLineup(team) {
@@ -4714,7 +4687,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
border: 2px solid #e0e0e0;
border-radius: 6px;
font-size: 1em;
transition: border-color 0.3s ease;
transition: border-color 0.3s ease, background-color 0.3s ease, box-shadow 0.3s ease;
}
.pin-input:focus {
@@ -4723,6 +4696,16 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
box-shadow: 0 0 0 3px rgba(74, 140, 190, 0.1);
}
.pin-input-success {
border-color: var(--success-color);
background-color: rgba(46, 125, 50, 0.12);
}
.pin-input-error {
border-color: var(--danger-color);
background-color: rgba(180, 66, 66, 0.12);
}
.load-pin-btn {
padding: 10px 16px;
background-color: var(--secondary-color);
@@ -4765,6 +4748,14 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
white-space: nowrap;
}
.sign-btn-success {
background-color: var(--success-color);
}
.sign-btn-error {
background-color: var(--danger-color);
}
.sign-btn:hover:not(:disabled) {
background-color: var(--primary-hover);
transform: translateY(-1px);

View File

@@ -70,6 +70,10 @@ export default {
isParticipant: {
type: Function,
default: () => false
},
shouldShowMember: {
type: Function,
default: () => true
}
},
emits: ['update:modelValue', 'member-click'],
@@ -114,7 +118,7 @@ export default {
this.revokeGalleryImage();
try {
const response = await apiClient.get(`/clubmembers/gallery/${this.currentClub}?format=json&size=${this.gallerySize}`);
const members = response.data.members || [];
const members = (response.data.members || []).filter(member => this.shouldShowMember(member));
// Setze Größe basierend auf Anzahl der Mitglieder nur beim ersten Laden
if (this.isInitialLoad) {

View File

@@ -0,0 +1,129 @@
<template>
<div class="diary-overview-panels">
<div class="diary-overview-switcher">
<button type="button" class="diary-overview-switch" :class="{ active: activeOverviewPanel === 'trainingDay' }" @click="$emit('toggle-panel', 'trainingDay')">
{{ $t('diary.trainingDaySection') }}
</button>
<button type="button" class="diary-overview-switch" :class="{ active: activeOverviewPanel === 'details' }" @click="$emit('toggle-panel', 'details')">
{{ $t('common.details') }}
</button>
<button type="button" class="diary-overview-switch" :class="{ active: activeOverviewPanel === 'groups' }" @click="$emit('toggle-panel', 'groups')">
{{ $t('diary.groupsSection') }}
</button>
</div>
<section v-if="activeOverviewPanel === 'trainingDay'" class="diary-toggle-card">
<div class="diary-toggle-body diary-toggle-body-open">
<div class="diary-workspace-header">
<div class="diary-workspace-copy">
<div class="diary-workspace-label">{{ $t('diary.activeTrainingDay') }}</div>
<h3>{{ formattedDate }}</h3>
<p class="diary-workspace-status">{{ diaryStatusText }}</p>
</div>
<div class="diary-workspace-stats">
<div class="diary-stat-card">
<span class="diary-stat-label">{{ $t('diary.trainingWindow') }}</span>
<strong class="diary-stat-value">{{ diaryTimeRangeLabel }}</strong>
</div>
<div class="diary-stat-card">
<span class="diary-stat-label">{{ $t('diary.participants') }}</span>
<strong class="diary-stat-value">{{ participantCount }}</strong>
</div>
<div class="diary-stat-card">
<span class="diary-stat-label">{{ $t('diary.trainingPlan') }}</span>
<strong class="diary-stat-value">{{ trainingPlanCount }}</strong>
</div>
<div class="diary-stat-card">
<span class="diary-stat-label">{{ $t('diary.freeActivities') }}</span>
<strong class="diary-stat-value">{{ activitiesCount }}</strong>
</div>
</div>
</div>
</div>
</section>
<section v-if="activeOverviewPanel === 'details'" class="diary-toggle-card">
<div class="diary-toggle-body diary-toggle-body-open">
<form @submit.prevent="$emit('update-training-times')" class="diary-general-form">
<div>
<label for="editTrainingStart">{{ $t('diary.trainingStart') }}:</label>
<input id="editTrainingStart" :value="trainingStart" type="time" step="300" @input="$emit('update:training-start', $event.target.value)" />
</div>
<div>
<label for="editTrainingEnd">{{ $t('diary.trainingEnd') }}:</label>
<input id="editTrainingEnd" :value="trainingEnd" type="time" step="300" @input="$emit('update:training-end', $event.target.value)" />
</div>
<button type="submit">{{ $t('diary.updateTimes') }}</button>
</form>
</div>
</section>
<section v-if="activeOverviewPanel === 'groups'" class="diary-toggle-card">
<div class="diary-toggle-body diary-toggle-body-open">
<div class="diary-groups-grid">
<div>
<h4>{{ $t('diary.existingGroups') }}</h4>
<ul>
<li v-for="group in groups" :key="group.id">
<span v-if="editingGroupId !== group.id" @click="$emit('edit-group', group.id)">{{ group.name }}</span>
<input
v-else
type="text"
:value="group.name"
style="display: inline; width: 10em"
@input="$emit('update-group-field', { groupId: group.id, field: 'name', value: $event.target.value })"
@blur="$emit('save-group', group)"
@keyup.enter="$emit('save-group', group)"
@keyup.esc="$emit('cancel-edit-group')"
/>
<span v-if="editingGroupId !== group.id" @click="$emit('edit-group', group.id)"> ({{ $t('diary.leader') }}: {{ group.lead }}) </span>
<input
v-else
type="text"
:value="group.lead"
style="display: inline; width: 10em"
@input="$emit('update-group-field', { groupId: group.id, field: 'lead', value: $event.target.value })"
@blur="$emit('save-group', group)"
@keyup.enter="$emit('save-group', group)"
@keyup.esc="$emit('cancel-edit-group')"
/>
<button v-if="editingGroupId !== group.id" class="trash-btn" style="margin-left: 10px" :title="$t('diary.deleteGroup')" @click="$emit('delete-group', group.id)">🗑</button>
</li>
</ul>
</div>
<div>
<h4>{{ $t('diary.createGroups') }}</h4>
<div class="groups">
<div>
<label for="groupCount">{{ $t('diary.numberOfGroups') }}:</label>
<input id="groupCount" :value="newGroupCount" type="number" :min="groups.length > 0 ? 1 : 2" max="10" required @input="$emit('update:new-group-count', Number($event.target.value))" />
</div>
<div>
<label>&nbsp;</label>
<button type="button" @click="$emit('create-groups')">{{ groups.length > 0 ? $t('diary.addGroup') : $t('diary.createGroups') }}</button>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
</template>
<script>
export default {
name: 'DiaryOverviewPanels',
props: {
activeOverviewPanel: { type: String, default: null },
formattedDate: { type: String, default: '' },
diaryStatusText: { type: String, default: '' },
diaryTimeRangeLabel: { type: String, default: '' },
participantCount: { type: Number, default: 0 },
trainingPlanCount: { type: Number, default: 0 },
activitiesCount: { type: Number, default: 0 },
trainingStart: { type: String, default: '' },
trainingEnd: { type: String, default: '' },
groups: { type: Array, required: true },
editingGroupId: { type: [Number, String, null], default: null },
newGroupCount: { type: Number, default: 2 }
},
emits: ['toggle-panel', 'update-training-times', 'update:training-start', 'update:training-end', 'edit-group', 'update-group-field', 'save-group', 'cancel-edit-group', 'delete-group', 'update:new-group-count', 'create-groups']
};
</script>

View File

@@ -0,0 +1,74 @@
<template>
<div class="team-card" :class="{ active }" @click="$emit('edit')">
<div class="team-header">
<div class="team-header-copy">
<h4>{{ team.name }}</h4>
<span class="team-league-inline">{{ leagueName }}</span>
<span class="team-open-hint">{{ $t('teamManagement.openInWorkspace') }}</span>
</div>
<div class="team-actions">
<button @click.stop="$emit('delete')" class="btn-delete" :title="$t('teamManagement.delete')">
🗑
</button>
</div>
</div>
<div class="team-card-badges">
<span v-if="status.complete" class="status-badge complete" :title="status.tooltip">
{{ $t('teamManagement.fullyConfigured') }}
</span>
<span v-else-if="status.partial" class="status-badge partial" :title="status.tooltip">
{{ $t('teamManagement.partiallyConfigured') }}
</span>
<span v-else class="status-badge missing" :title="status.tooltip">
{{ $t('teamManagement.notConfigured') }}
</span>
<span class="status-badge neutral">{{ seasonLabel }}</span>
</div>
<div class="team-card-meta">
<span class="team-card-meta-item">{{ $t('teamManagement.created') }}: {{ createdText }}</span>
<span class="team-card-meta-item">{{ $t('teamManagement.lastUpdated') }}: {{ lastUpdatedText }}</span>
</div>
<div class="team-documents">
<div class="documents-label">{{ $t('teamManagement.documents') }}</div>
<div class="document-icons">
<button
v-if="hasCodeList"
@click.stop="$emit('show-code-list')"
class="document-icon code-list-icon"
:title="$t('teamManagement.showCodeList')"
>
📋
</button>
<button
v-if="hasPinList"
@click.stop="$emit('show-pin-list')"
class="document-icon pin-list-icon"
:title="$t('teamManagement.showPinList')"
>
🔐
</button>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'TeamListCard',
props: {
team: { type: Object, required: true },
active: { type: Boolean, default: false },
status: { type: Object, required: true },
leagueName: { type: String, required: true },
seasonLabel: { type: String, required: true },
createdText: { type: String, required: true },
lastUpdatedText: { type: String, required: true },
hasCodeList: { type: Boolean, default: false },
hasPinList: { type: Boolean, default: false }
},
emits: ['edit', 'delete', 'show-code-list', 'show-pin-list']
};
</script>

View File

@@ -0,0 +1,80 @@
<template>
<div>
<div class="selected-tournament-strip">
<div class="selected-tournament-main">
<span class="selected-tournament-label">{{ $t('tournaments.selectedTournament') }}</span>
<strong>{{ tournamentName || selectionDisplay || '' }}</strong>
</div>
<div class="selected-tournament-meta">
<span>{{ tournamentDateLabel }}</span>
<span>{{ modeLabel }}</span>
<span>{{ classCount }} {{ $t('tournaments.classes') }}</span>
<span>{{ participantCount }} {{ $t('tournaments.participants') }}</span>
</div>
</div>
<div class="tournament-status-strip">
<div v-for="status in workspaceStatusChips" :key="status.key" :class="['tournament-status-chip', `status-${status.tone}`]">
<button v-if="status.action" type="button" class="tournament-status-main" @click="$emit('navigate-status', status.action)">{{ status.label }}</button>
<span v-else class="tournament-status-main tournament-status-main-static">{{ status.label }}</span>
<button v-if="status.quickAction" type="button" class="tournament-status-action" @click="$emit('quick-action', status.quickAction)">{{ status.quickAction.label }}</button>
</div>
</div>
<div v-if="workspaceProblems.length > 0" class="workspace-problems">
<div class="workspace-problems-header">
<strong>{{ $t('tournaments.workspaceProblemsTitle', { count: workspaceProblems.length }) }}</strong>
</div>
<div class="workspace-problems-list">
<div v-for="problem in workspaceProblems" :key="problem.key" class="workspace-problem-item">
<div class="workspace-problem-copy">
<strong class="workspace-problem-title">{{ problem.title }}</strong>
<span class="workspace-problem-description">{{ problem.description }}</span>
</div>
<button type="button" class="workspace-problem-action" @click="problem.quickAction ? $emit('quick-action', problem.quickAction) : $emit('navigate-status', problem.action)">
{{ problem.actionLabel }}
</button>
</div>
</div>
</div>
<div class="tournament-tabs">
<button @click="$emit('set-active-tab', 'config')" :class="['tab-button', { active: activeTab === 'config' }]">{{ $t('tournaments.tabConfig') }}</button>
<button @click="$emit('set-active-tab', 'participants')" :class="['tab-button', { active: activeTab === 'participants' }]">
{{ $t('tournaments.tabParticipants') }}
<span class="tab-badge">{{ participantCount }}</span>
</button>
<button v-if="isGroupTournament" @click="$emit('set-active-tab', 'groups')" :class="['tab-button', { active: activeTab === 'groups' }]">
{{ $t('tournaments.tabGroups') }}
<span class="tab-badge">{{ groupCount }}</span>
</button>
<button @click="$emit('set-active-tab', 'results')" :class="['tab-button', { active: activeTab === 'results' }]">
{{ $t('tournaments.tabResults') }} / {{ $t('tournaments.tabPlacements') }}
<span class="tab-badge">{{ matchCount }}</span>
</button>
</div>
<div v-if="activeTab === 'results'" class="results-subnav">
<button type="button" :class="['subnav-button', { active: resultsSubTab === 'matches' }]" @click="$emit('set-results-sub-tab', 'matches')">{{ $t('tournaments.tabResults') }}</button>
<button type="button" :class="['subnav-button', { active: resultsSubTab === 'placements' }]" @click="$emit('set-results-sub-tab', 'placements')">{{ $t('tournaments.tabPlacements') }}</button>
</div>
</div>
</template>
<script>
export default {
name: 'TournamentWorkspaceHeader',
props: {
tournamentName: { type: String, default: '' },
selectionDisplay: { type: String, default: '' },
tournamentDateLabel: { type: String, default: '' },
modeLabel: { type: String, default: '' },
classCount: { type: Number, default: 0 },
participantCount: { type: Number, default: 0 },
groupCount: { type: Number, default: 0 },
matchCount: { type: Number, default: 0 },
workspaceStatusChips: { type: Array, required: true },
workspaceProblems: { type: Array, required: true },
activeTab: { type: String, required: true },
resultsSubTab: { type: String, required: true },
isGroupTournament: { type: Boolean, default: false }
},
emits: ['navigate-status', 'quick-action', 'set-active-tab', 'set-results-sub-tab']
};
</script>

View File

@@ -465,6 +465,7 @@
"filterAll": "Alle",
"filterPresent": "Anwesend",
"filterAbsent": "Abwesend",
"filterExcused": "Entschuldigt",
"filterTest": "Probe",
"participantStatusNone": "Kein Status",
"participantStatusExcused": "Entschuldigt",

View File

@@ -134,6 +134,7 @@
"standardDurationShort": "Min",
"standardActivityAddError": "Standard activity could not be added.",
"statusReady": "Times and training plan are set.",
"filterExcused": "Excused",
"participantStatusNone": "No status",
"participantStatusExcused": "Excused",
"participantStatusCancelled": "Cancelled"

View File

@@ -70,116 +70,31 @@
</div>
<div v-if="date !== null && !showForm" class="diary-content">
<div class="diary-overview-panels">
<div class="diary-overview-switcher">
<button
type="button"
class="diary-overview-switch"
:class="{ active: activeOverviewPanel === 'trainingDay' }"
@click="toggleOverviewPanel('trainingDay')"
>
{{ $t('diary.trainingDaySection') }}
</button>
<button
type="button"
class="diary-overview-switch"
:class="{ active: activeOverviewPanel === 'details' }"
@click="toggleOverviewPanel('details')"
>
{{ $t('common.details') }}
</button>
<button
type="button"
class="diary-overview-switch"
:class="{ active: activeOverviewPanel === 'groups' }"
@click="toggleOverviewPanel('groups')"
>
{{ $t('diary.groupsSection') }}
</button>
</div>
<section v-if="activeOverviewPanel === 'trainingDay'" class="diary-toggle-card">
<div class="diary-toggle-body diary-toggle-body-open">
<div class="diary-workspace-header">
<div class="diary-workspace-copy">
<div class="diary-workspace-label">{{ $t('diary.activeTrainingDay') }}</div>
<h3>{{ getFormattedDate(date.date) }}</h3>
<p class="diary-workspace-status">{{ diaryStatusText }}</p>
</div>
<div class="diary-workspace-stats">
<div class="diary-stat-card">
<span class="diary-stat-label">{{ $t('diary.trainingWindow') }}</span>
<strong class="diary-stat-value">{{ diaryTimeRangeLabel }}</strong>
</div>
<div class="diary-stat-card">
<span class="diary-stat-label">{{ $t('diary.participants') }}</span>
<strong class="diary-stat-value">{{ participants.length }}</strong>
</div>
<div class="diary-stat-card">
<span class="diary-stat-label">{{ $t('diary.trainingPlan') }}</span>
<strong class="diary-stat-value">{{ trainingPlan.length }}</strong>
</div>
<div class="diary-stat-card">
<span class="diary-stat-label">{{ $t('diary.freeActivities') }}</span>
<strong class="diary-stat-value">{{ activities.length }}</strong>
</div>
</div>
</div>
</div>
</section>
<section v-if="activeOverviewPanel === 'details'" class="diary-toggle-card">
<div class="diary-toggle-body diary-toggle-body-open">
<form @submit.prevent="updateTrainingTimes" class="diary-general-form">
<div>
<label for="editTrainingStart">{{ $t('diary.trainingStart') }}:</label>
<input type="time" step="300" id="editTrainingStart" v-model="trainingStart" />
</div>
<div>
<label for="editTrainingEnd">{{ $t('diary.trainingEnd') }}:</label>
<input type="time" step="300" id="editTrainingEnd" v-model="trainingEnd" />
</div>
<button type="submit">{{ $t('diary.updateTimes') }}</button>
</form>
</div>
</section>
<section v-if="activeOverviewPanel === 'groups'" class="diary-toggle-card">
<div class="diary-toggle-body diary-toggle-body-open">
<div class="diary-groups-grid">
<div>
<h4>{{ $t('diary.existingGroups') }}</h4>
<ul>
<li v-for="group in groups" :key="group.id">
<span v-if="editingGroupId !== group.id" @click="editGroup(group.id)">{{ group.name }}</span>
<input v-else type="text" v-model="group.name" @blur="saveGroup(group)"
@keyup.enter="saveGroup(group)" @keyup.esc="cancelEditGroup"
style="display: inline;width:10em" />
<span v-if="editingGroupId !== group.id" @click="editGroup(group.id)"> ({{ $t('diary.leader') }}: {{ group.lead }}) </span>
<input v-else type="text" v-model="group.lead" @blur="saveGroup(group)"
@keyup.enter="saveGroup(group)" @keyup.esc="cancelEditGroup"
style="display: inline;width:10em" />
<button v-if="editingGroupId !== group.id" @click="deleteGroup(group.id)"
class="trash-btn"
style="margin-left: 10px;"
:title="$t('diary.deleteGroup')">🗑</button>
</li>
</ul>
</div>
<div>
<h4>{{ $t('diary.createGroups') }}</h4>
<div class="groups">
<div>
<label for="groupCount">{{ $t('diary.numberOfGroups') }}:</label>
<input type="number" id="groupCount" v-model="newGroupCount" :min="groups.length > 0 ? 1 : 2" max="10" required />
</div>
<div>
<label>&nbsp;</label>
<button type="submit" @click="createGroups">{{ groups.length > 0 ? $t('diary.addGroup') : $t('diary.createGroups') }}</button>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
<DiaryOverviewPanels
:active-overview-panel="activeOverviewPanel"
:formatted-date="date?.date ? getFormattedDate(date.date) : ''"
:diary-status-text="diaryStatusText"
:diary-time-range-label="diaryTimeRangeLabel"
:participant-count="participants.length"
:training-plan-count="trainingPlan.length"
:activities-count="activities.length"
:training-start="trainingStart"
:training-end="trainingEnd"
:groups="groups"
:editing-group-id="editingGroupId"
:new-group-count="newGroupCount"
@toggle-panel="toggleOverviewPanel"
@update-training-times="updateTrainingTimes"
@update:training-start="trainingStart = $event"
@update:training-end="trainingEnd = $event"
@edit-group="editGroup"
@update-group-field="updateGroupField"
@save-group="saveGroup"
@cancel-edit-group="cancelEditGroup"
@delete-group="deleteGroup"
@update:new-group-count="newGroupCount = $event"
@create-groups="createGroups"
/>
<!-- Tab-Navigation für Mobile -->
<div class="mobile-tabs">
@@ -753,6 +668,7 @@
:current-club="currentClub"
:date="date"
:is-participant="isParticipant"
:should-show-member="shouldShowGalleryMember"
@member-click="handleGalleryMemberClick"
/>
</div>
@@ -798,6 +714,7 @@ import QuickAddMemberDialog from '../components/QuickAddMemberDialog.vue';
import MemberGalleryDialog from '../components/MemberGalleryDialog.vue';
import DiaryParticipantsPanel from '../components/DiaryParticipantsPanel.vue';
import DiaryActivitiesPanel from '../components/DiaryActivitiesPanel.vue';
import DiaryOverviewPanels from '../components/diary/DiaryOverviewPanels.vue';
import {
connectSocket,
disconnectSocket,
@@ -860,7 +777,8 @@ export default {
QuickAddMemberDialog,
MemberGalleryDialog,
DiaryParticipantsPanel,
DiaryActivitiesPanel
DiaryActivitiesPanel,
DiaryOverviewPanels
},
data() {
return {
@@ -1121,9 +1039,11 @@ export default {
return this.sortedMembers().filter(member => {
const isPresent = this.isParticipant(member.id);
const isExcused = this.getParticipantStatus(member.id) === 'excused';
if (this.participantFilter === 'present' && !isPresent) return false;
if (this.participantFilter === 'absent' && isPresent) return false;
if (this.participantFilter === 'excused' && !isExcused) return false;
if (this.participantFilter === 'test' && !member.testMembership) return false;
if (!search) return true;
@@ -1726,6 +1646,10 @@ export default {
return this.participantStatusMap[memberId] || '';
},
shouldShowGalleryMember(member) {
return this.getParticipantStatus(member.memberId) !== 'excused';
},
async toggleParticipant(memberId) {
const isParticipant = this.isParticipant(memberId);
const dateId = this.date.id;
@@ -2608,6 +2532,13 @@ export default {
editGroup(groupId) {
this.editingGroupId = groupId;
},
updateGroupField({ groupId, field, value }) {
this.groups = this.groups.map(group =>
Number(group.id) === Number(groupId)
? { ...group, [field]: value }
: group
);
},
async saveGroup(group) {
try {
await apiClient.put(`/group/${group.id}`, {
@@ -5266,9 +5197,9 @@ img {
.mobile-tabs {
display: none;
width: 100%;
border-bottom: 2px solid #ddd;
border-bottom: 1px solid var(--border-color);
margin-bottom: 1rem;
background: #f5f5f5;
background: var(--surface-muted);
position: sticky;
top: 0;
z-index: 100;
@@ -5283,7 +5214,7 @@ img {
font-size: 1rem;
border-bottom: 3px solid transparent;
transition: all 0.3s ease;
color: #666;
color: var(--text-muted);
}
.mobile-tab-count {
@@ -5294,20 +5225,20 @@ img {
margin-left: 0.35rem;
padding: 0.05rem 0.35rem;
border-radius: 999px;
background: rgba(255, 255, 255, 0.2);
background: rgba(47, 122, 95, 0.1);
font-size: 0.78rem;
}
.tab-button:hover {
background: #e9e9e9;
color: #333;
background: var(--surface-color);
color: var(--text-color);
}
.tab-button.active {
border-bottom-color: #007bff;
color: #007bff;
border-bottom-color: var(--primary-color);
color: var(--primary-strong);
font-weight: 600;
background: white;
background: var(--surface-color);
}
/* Mobile Tab Content */

View File

@@ -598,14 +598,15 @@ export default {
.stat-value {
font-weight: 600;
color: var(--primary-color, #007bff);
color: var(--primary-strong);
}
.logs-table-container {
overflow-x: auto;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
background: var(--surface-color);
border: 1px solid var(--border-color);
border-radius: 12px;
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
}
.logs-table {
@@ -614,23 +615,23 @@ export default {
}
.logs-table thead {
background: var(--background-light, #f8f9fa);
background: var(--surface-muted);
}
.logs-table th {
padding: 1rem;
text-align: left;
font-weight: 600;
border-bottom: 2px solid #ddd;
border-bottom: 1px solid var(--border-color);
}
.logs-table td {
padding: 0.75rem 1rem;
border-bottom: 1px solid #eee;
border-bottom: 1px solid var(--border-color);
}
.logs-table tbody tr:hover {
background: var(--background-light, #f8f9fa);
background: var(--surface-muted);
}
.log-error {
@@ -757,16 +758,17 @@ export default {
.btn-view {
padding: 0.25rem 0.75rem;
background: var(--primary-color, #007bff);
color: white;
border: none;
border-radius: 4px;
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: var(--text-on-primary);
border: 1px solid transparent;
border-radius: 8px;
cursor: pointer;
font-size: 0.85em;
font-weight: 600;
}
.btn-view:hover {
opacity: 0.9;
transform: translateY(-1px);
}
.pagination {

View File

@@ -114,76 +114,23 @@
</div>
<div v-else class="teams-grid">
<div
<TeamListCard
v-for="team in filteredTeams"
:key="team.id"
class="team-card"
:class="{ active: teamToEdit && teamToEdit.id === team.id }"
@click="editTeam(team)"
>
<div class="team-header">
<div class="team-header-copy">
<h4>{{ team.name }}</h4>
<span class="team-league-inline">
{{ team.league ? team.league.name : t('teamManagement.noLeague') }}
</span>
<span class="team-open-hint">{{ t('teamManagement.openInWorkspace') }}</span>
</div>
<div class="team-actions">
<button @click.stop="deleteTeam(team)" class="btn-delete" :title="t('teamManagement.delete')">
🗑
</button>
</div>
</div>
<div class="team-card-badges">
<span v-if="getMyTischtennisStatus(team).complete" class="status-badge complete" :title="getMyTischtennisStatus(team).tooltip">
{{ t('teamManagement.fullyConfigured') }}
</span>
<span v-else-if="getMyTischtennisStatus(team).partial" class="status-badge partial" :title="getMyTischtennisStatus(team).tooltip">
{{ t('teamManagement.partiallyConfigured') }}
</span>
<span v-else class="status-badge missing" :title="getMyTischtennisStatus(team).tooltip">
{{ t('teamManagement.notConfigured') }}
</span>
<span class="status-badge neutral">
{{ team.season?.season || t('teamManagement.unknown') }}
</span>
</div>
<div class="team-card-meta">
<span class="team-card-meta-item">
{{ t('teamManagement.created') }}: {{ formatDate(team.createdAt) }}
</span>
<span class="team-card-meta-item">
{{ t('teamManagement.lastUpdated') }}:
{{ getTeamJobInfo(team) && getTeamJobInfo(team).lastRun ? formatJobDate(getTeamJobInfo(team).lastRun) : t('teamManagement.never') }}
</span>
</div>
<!-- PDF-Dokumente Icons -->
<div class="team-documents">
<div class="documents-label">{{ t('teamManagement.documents') }}</div>
<div class="document-icons">
<button
v-if="getTeamDocuments(team.id, 'code_list').length > 0"
@click.stop="showPDFDialog(team.id, 'code_list')"
class="document-icon code-list-icon"
:title="t('teamManagement.showCodeList')"
>
📋
</button>
<button
v-if="getTeamDocuments(team.id, 'pin_list').length > 0"
@click.stop="showPDFDialog(team.id, 'pin_list')"
class="document-icon pin-list-icon"
:title="t('teamManagement.showPinList')"
>
🔐
</button>
</div>
</div>
</div>
:team="team"
:active="teamToEdit && teamToEdit.id === team.id"
:status="getMyTischtennisStatus(team)"
:league-name="team.league ? team.league.name : t('teamManagement.noLeague')"
:season-label="team.season?.season || t('teamManagement.unknown')"
:created-text="formatDate(team.createdAt)"
:last-updated-text="getTeamJobInfo(team) && getTeamJobInfo(team).lastRun ? formatJobDate(getTeamJobInfo(team).lastRun) : t('teamManagement.never')"
:has-code-list="getTeamDocuments(team.id, 'code_list').length > 0"
:has-pin-list="getTeamDocuments(team.id, 'pin_list').length > 0"
@edit="editTeam(team)"
@delete="deleteTeam(team)"
@show-code-list="showPDFDialog(team.id, 'code_list')"
@show-pin-list="showPDFDialog(team.id, 'pin_list')"
/>
</div>
</div>
@@ -544,15 +491,17 @@ import i18n from '../i18n';
import InfoDialog from '../components/InfoDialog.vue';
import ConfirmDialog from '../components/ConfirmDialog.vue';
import TeamListCard from '../components/team/TeamListCard.vue';
import { buildInfoConfig, buildConfirmConfig } from '../utils/dialogUtils.js';
export default {
name: 'TeamManagementView',
components: {
SeasonSelector
,
InfoDialog,
ConfirmDialog},
SeasonSelector,
InfoDialog,
ConfirmDialog,
TeamListCard
},
setup() {
const store = useStore();
const t = (key, params) => i18n.global.t(key, params);

View File

@@ -77,111 +77,25 @@
</div>
</div>
<div v-if="selectedDate !== 'new'" class="workspace-detail tournament-setup">
<div class="selected-tournament-strip">
<div class="selected-tournament-main">
<span class="selected-tournament-label">{{ $t('tournaments.selectedTournament') }}</span>
<strong>{{ currentTournamentName || currentSelectionDisplay || '' }}</strong>
</div>
<div class="selected-tournament-meta">
<span>{{ currentTournamentDate ? formatDisplayDate(currentTournamentDate) : $t('tournaments.unknownDate') }}</span>
<span>{{ currentModeLabel }}</span>
<span>{{ tournamentClasses.length }} {{ $t('tournaments.classes') }}</span>
<span>{{ totalParticipantCount }} {{ $t('tournaments.participants') }}</span>
</div>
</div>
<div class="tournament-status-strip">
<div
v-for="status in workspaceStatusChips"
:key="status.key"
:class="['tournament-status-chip', `status-${status.tone}`]"
>
<button
v-if="status.action"
type="button"
class="tournament-status-main"
@click="navigateFromStatus(status.action)"
>
{{ status.label }}
</button>
<span v-else class="tournament-status-main tournament-status-main-static">{{ status.label }}</span>
<button
v-if="status.quickAction"
type="button"
class="tournament-status-action"
@click="executeStatusQuickAction(status.quickAction)"
>
{{ status.quickAction.label }}
</button>
</div>
</div>
<div v-if="workspaceProblems.length > 0" class="workspace-problems">
<div class="workspace-problems-header">
<strong>{{ $t('tournaments.workspaceProblemsTitle', { count: workspaceProblems.length }) }}</strong>
</div>
<div class="workspace-problems-list">
<div
v-for="problem in workspaceProblems"
:key="problem.key"
class="workspace-problem-item"
>
<div class="workspace-problem-copy">
<strong class="workspace-problem-title">{{ problem.title }}</strong>
<span class="workspace-problem-description">{{ problem.description }}</span>
</div>
<button
type="button"
class="workspace-problem-action"
@click="problem.quickAction ? executeStatusQuickAction(problem.quickAction) : navigateFromStatus(problem.action)"
>
{{ problem.actionLabel }}
</button>
</div>
</div>
</div>
<!-- Tab-Navigation -->
<div class="tournament-tabs">
<button
@click="setActiveTab('config')"
:class="['tab-button', { 'active': activeTab === 'config' }]">
{{ $t('tournaments.tabConfig') }}
</button>
<button
@click="setActiveTab('participants')"
:class="['tab-button', { 'active': activeTab === 'participants' }]">
{{ $t('tournaments.tabParticipants') }}
<span class="tab-badge">{{ totalParticipantCount }}</span>
</button>
<button
v-if="isGroupTournament"
@click="setActiveTab('groups')"
:class="['tab-button', { 'active': activeTab === 'groups' }]">
{{ $t('tournaments.tabGroups') }}
<span class="tab-badge">{{ groups.length }}</span>
</button>
<button
@click="setActiveTab('results')"
:class="['tab-button', { 'active': activeTab === 'results' }]">
{{ $t('tournaments.tabResults') }} / {{ $t('tournaments.tabPlacements') }}
<span class="tab-badge">{{ matches.length }}</span>
</button>
</div>
<div v-if="activeTab === 'results'" class="results-subnav">
<button
type="button"
:class="['subnav-button', { active: resultsSubTab === 'matches' }]"
@click="setResultsSubTab('matches')"
>
{{ $t('tournaments.tabResults') }}
</button>
<button
type="button"
:class="['subnav-button', { active: resultsSubTab === 'placements' }]"
@click="setResultsSubTab('placements')"
>
{{ $t('tournaments.tabPlacements') }}
</button>
</div>
<TournamentWorkspaceHeader
:tournament-name="currentTournamentName"
:selection-display="currentSelectionDisplay"
:tournament-date-label="currentTournamentDate ? formatDisplayDate(currentTournamentDate) : $t('tournaments.unknownDate')"
:mode-label="currentModeLabel"
:class-count="tournamentClasses.length"
:participant-count="totalParticipantCount"
:group-count="groups.length"
:match-count="matches.length"
:workspace-status-chips="workspaceStatusChips"
:workspace-problems="workspaceProblems"
:active-tab="activeTab"
:results-sub-tab="resultsSubTab"
:is-group-tournament="isGroupTournament"
@navigate-status="navigateFromStatus"
@quick-action="executeStatusQuickAction"
@set-active-tab="setActiveTab"
@set-results-sub-tab="setResultsSubTab"
/>
<!-- Tab: Konfiguration -->
<TournamentConfigTab
@@ -406,6 +320,7 @@ import TournamentGroupsTab from '../components/tournament/TournamentGroupsTab.vu
import TournamentParticipantsTab from '../components/tournament/TournamentParticipantsTab.vue';
import TournamentResultsTab from '../components/tournament/TournamentResultsTab.vue';
import TournamentPlacementsTab from '../components/tournament/TournamentPlacementsTab.vue';
import TournamentWorkspaceHeader from '../components/tournament/TournamentWorkspaceHeader.vue';
export default {
name: 'TournamentTab',
components: {
@@ -415,7 +330,8 @@ export default {
TournamentGroupsTab,
TournamentParticipantsTab,
TournamentResultsTab,
TournamentPlacementsTab
TournamentPlacementsTab,
TournamentWorkspaceHeader
},
props: {
allowsExternal: {