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:
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
129
frontend/src/components/diary/DiaryOverviewPanels.vue
Normal file
129
frontend/src/components/diary/DiaryOverviewPanels.vue
Normal 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> </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>
|
||||
74
frontend/src/components/team/TeamListCard.vue
Normal file
74
frontend/src/components/team/TeamListCard.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -465,6 +465,7 @@
|
||||
"filterAll": "Alle",
|
||||
"filterPresent": "Anwesend",
|
||||
"filterAbsent": "Abwesend",
|
||||
"filterExcused": "Entschuldigt",
|
||||
"filterTest": "Probe",
|
||||
"participantStatusNone": "Kein Status",
|
||||
"participantStatusExcused": "Entschuldigt",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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> </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 */
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user