feat(Scheduler, MatchService, PredefinedActivity): enhance scheduling and match fetching features

- Added new scheduler routes to manage scheduling functionalities.
- Updated match fetching logic to include a scope parameter for more flexible data retrieval.
- Introduced a new field `excludeFromStats` in the PredefinedActivity model to manage activity visibility in statistics.
- Enhanced the diary date activity controller to handle predefined activities, improving activity management.
- Refactored various services to support new features and improve overall data handling.
This commit is contained in:
Torsten Schulz (local)
2026-03-17 14:10:35 +01:00
parent f1cfd1147d
commit afe51f399c
53 changed files with 2846 additions and 926 deletions

View File

@@ -14,35 +14,35 @@
<span class="dropdown-arrow"></span>
</button>
<div v-if="userDropdownOpen" class="user-dropdown">
<router-link to="/mytischtennis-account" class="dropdown-item" @click="userDropdownOpen = false">
<button type="button" class="dropdown-item" @click="openUserMenuDialog('MyTischtennisAccount', $t('navigation.myTischtennisAccount'))">
<span class="dropdown-icon">🔗</span>
{{ $t('navigation.myTischtennisAccount') }}
</router-link>
<router-link to="/clicktt-account" class="dropdown-item" @click="userDropdownOpen = false">
</button>
<button type="button" class="dropdown-item" @click="openUserMenuDialog('ClickTtAccount', $t('navigation.clickTtAccount'))">
<span class="dropdown-icon">🏓</span>
HTTV / click-TT Account
</router-link>
<router-link v-if="canManagePermissions" to="/permissions" class="dropdown-item" @click="userDropdownOpen = false">
{{ $t('navigation.clickTtAccount') }}
</button>
<button v-if="canManagePermissions" type="button" class="dropdown-item" @click="openUserMenuDialog('PermissionsView', $t('navigation.permissions'))">
<span class="dropdown-icon">🔐</span>
{{ $t('navigation.permissions') }}
</router-link>
<router-link v-if="hasPermission('members', 'write')" to="/member-transfer-settings" class="dropdown-item" @click="userDropdownOpen = false">
</button>
<button v-if="hasPermission('members', 'write')" type="button" class="dropdown-item" @click="openUserMenuDialog('MemberTransferSettingsView', $t('navigation.memberTransfer'))">
<span class="dropdown-icon">📤</span>
{{ $t('navigation.memberTransfer') }}
</router-link>
<router-link v-if="isAdmin" to="/logs" class="dropdown-item" @click="userDropdownOpen = false">
</button>
<button v-if="isAdmin" type="button" class="dropdown-item" @click="openUserMenuDialog('LogsView', $t('navigation.logs'))">
<span class="dropdown-icon">📋</span>
{{ $t('navigation.logs') }}
</router-link>
<router-link v-if="canManagePermissions" to="/clicktt" class="dropdown-item" @click="userDropdownOpen = false">
</button>
<button v-if="canManagePermissions" type="button" class="dropdown-item" @click="openUserMenuDialog('ClickTtView', $t('navigation.clickTtBrowser'))">
<span class="dropdown-icon">🌐</span>
HTTV / click-TT
</router-link>
{{ $t('navigation.clickTtBrowser') }}
</button>
<div class="dropdown-divider"></div>
<router-link to="/personal-settings" class="dropdown-item" @click="userDropdownOpen = false">
<button type="button" class="dropdown-item" @click="openUserMenuDialog('PersonalSettings', $t('navigation.personalSettings'))">
<span class="dropdown-icon"></span>
{{ $t('navigation.personalSettings') }}
</router-link>
</button>
<div class="dropdown-divider"></div>
<button @click="logout" class="dropdown-item logout-item">
<span class="dropdown-icon">🚪</span>
@@ -303,7 +303,18 @@ export default {
this.confirmDialog.isOpen = false;
},
...mapActions(['setCurrentClub', 'setClubs', 'logout', 'toggleSidebar']),
...mapActions(['setCurrentClub', 'setClubs', 'logout', 'toggleSidebar', 'openDialog']),
openUserMenuDialog(component, title) {
this.userDropdownOpen = false;
this.openDialog({
title,
component,
props: {
embedded: true
}
});
},
async loadUserData() {
try {
@@ -399,9 +410,11 @@ export default {
/* Header */
.app-header {
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: white;
box-shadow: var(--shadow-medium);
background:
linear-gradient(135deg, rgba(24, 70, 54, 0.96), rgba(47, 122, 95, 0.94)),
linear-gradient(90deg, rgba(255, 255, 255, 0.06), transparent);
color: var(--text-on-primary);
box-shadow: 0 8px 24px rgba(24, 70, 54, 0.18);
position: relative;
z-index: 1000;
flex-shrink: 0;
@@ -423,7 +436,7 @@ export default {
.app-header h1 {
margin: 0;
font-weight: 700;
color: white;
color: var(--text-on-primary);
text-align: center;
/* Schriftgröße bleibt wie in der main.scss definiert */
}
@@ -437,17 +450,19 @@ export default {
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background-color: rgba(255, 255, 255, 0.1);
border-radius: 20px;
background-color: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 999px;
font-size: 0.9rem;
color: white;
color: var(--text-on-primary);
border: none;
cursor: pointer;
transition: background-color 0.2s ease;
transition: var(--transition);
}
.user-info:hover {
background-color: rgba(255, 255, 255, 0.2);
background-color: rgba(255, 255, 255, 0.18);
transform: translateY(-1px);
}
.user-icon {
@@ -468,10 +483,11 @@ export default {
position: absolute;
top: calc(100% + 0.5rem);
right: 0;
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
min-width: 200px;
background: var(--surface-color);
border: 1px solid rgba(24, 70, 54, 0.08);
border-radius: 12px;
box-shadow: 0 16px 36px rgba(24, 70, 54, 0.16);
min-width: 240px;
max-height: 70vh;
overflow-y: auto;
overflow-x: hidden;
@@ -495,7 +511,7 @@ export default {
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
color: #333;
color: var(--text-primary);
text-decoration: none;
background: none;
border: none;
@@ -503,11 +519,18 @@ export default {
text-align: left;
cursor: pointer;
font-size: 0.9rem;
transition: background-color 0.15s ease;
border-radius: 0;
box-shadow: none;
margin: 0;
min-height: 0;
justify-content: flex-start;
transition: var(--transition-fast);
}
.dropdown-item:hover {
background-color: #f5f5f5;
background-color: var(--primary-light);
color: var(--primary-strong);
transform: none;
}
.dropdown-icon {
@@ -516,7 +539,7 @@ export default {
.dropdown-divider {
height: 1px;
background-color: #e0e0e0;
background-color: var(--border-color);
margin: 0.25rem 0;
}
@@ -526,7 +549,8 @@ export default {
}
.logout-item:hover {
background-color: #fff5f5;
background-color: #fff1f3;
color: var(--danger-color);
}
.home-link {
@@ -554,9 +578,10 @@ export default {
/* Sidebar */
.sidebar {
width: 280px;
background: white;
border-right: 1px solid var(--border-color);
box-shadow: var(--shadow-small);
background:
linear-gradient(180deg, var(--surface-color), var(--surface-muted));
border-right: 1px solid rgba(24, 70, 54, 0.08);
box-shadow: inset -1px 0 0 rgba(24, 70, 54, 0.04);
overflow-y: auto;
flex-shrink: 0;
display: flex;
@@ -573,15 +598,19 @@ export default {
}
.club-selector {
padding: 0.75rem;
padding: 0.9rem;
margin-bottom: 0.5rem;
flex-shrink: 0;
background: rgba(255, 255, 255, 0.78);
border: 1px solid rgba(24, 70, 54, 0.08);
border-radius: 12px;
box-shadow: 0 8px 20px rgba(24, 70, 54, 0.05);
}
.club-selector .card-title {
font-size: 0.875rem;
margin-bottom: 0.5rem;
color: var(--text-color);
color: var(--primary-strong);
font-weight: 600;
}
@@ -598,7 +627,7 @@ export default {
border-radius: var(--border-radius-small);
font-size: 0.75rem;
background: white;
color: var(--text-color);
color: var(--text-primary);
}
.select-group .btn-primary {
@@ -636,7 +665,7 @@ export default {
.nav-title {
font-size: 0.75rem;
font-weight: 600;
color: var(--text-muted);
color: var(--primary-strong);
text-transform: uppercase;
letter-spacing: 0.025em;
margin-bottom: 0.25rem;
@@ -648,17 +677,26 @@ export default {
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.5rem;
color: var(--text-color);
color: var(--text-primary);
text-decoration: none;
border-radius: var(--border-radius-small);
transition: all var(--transition-fast);
border-radius: 10px;
transition: var(--transition-fast);
font-size: 1rem;
border: 1px solid transparent;
}
.nav-link:hover {
background: var(--primary-light);
color: var(--primary-color);
color: var(--primary-strong);
transform: translateX(0.125rem);
border-color: rgba(47, 122, 95, 0.08);
}
.nav-link.router-link-active {
background: linear-gradient(135deg, var(--primary-soft), rgba(255, 255, 255, 0.92));
color: var(--primary-strong);
border-color: rgba(47, 122, 95, 0.14);
box-shadow: inset 0 0 0 1px rgba(47, 122, 95, 0.03);
}
.nav-icon {
@@ -685,8 +723,8 @@ export default {
/* Auth Navigation */
.auth-nav {
width: 260px;
background: white;
border-right: 1px solid var(--border-color);
background: linear-gradient(180deg, var(--surface-color), var(--surface-muted));
border-right: 1px solid rgba(24, 70, 54, 0.08);
display: flex;
align-items: center;
justify-content: center;
@@ -722,15 +760,16 @@ export default {
.main-content {
flex: 1;
overflow-y: auto;
background: var(--background-light);
background: var(--bg-canvas);
min-height: 0;
padding-bottom: 32px; /* Platz für Statusleiste (24px + 8px padding) */
}
/* Footer */
.app-footer {
background: white;
border-top: 1px solid var(--border-color);
background: rgba(255, 255, 255, 0.86);
border-top: 1px solid rgba(24, 70, 54, 0.08);
backdrop-filter: blur(6px);
padding: 0.75rem 1rem;
}

View File

@@ -34,21 +34,21 @@
}
.alert-success {
background-color: rgba(40, 167, 69, 0.08);
border-color: rgba(40, 167, 69, 0.25);
color: #155724;
background-color: rgba(47, 122, 95, 0.1);
border-color: rgba(47, 122, 95, 0.2);
color: var(--primary-strong);
}
.alert-info {
background-color: rgba(23, 162, 184, 0.08);
border-color: rgba(23, 162, 184, 0.25);
color: #0c5460;
background-color: rgba(51, 111, 168, 0.08);
border-color: rgba(51, 111, 168, 0.18);
color: #204a78;
}
.alert-warning {
background-color: rgba(255, 193, 7, 0.08);
border-color: rgba(255, 193, 7, 0.25);
color: #856404;
background-color: rgba(181, 110, 65, 0.1);
border-color: rgba(181, 110, 65, 0.18);
color: #7a4a2c;
}
.alert-danger {
@@ -61,56 +61,71 @@
.badge {
display: inline-flex;
align-items: center;
padding: 0.125rem 0.625rem;
padding: 0.22rem 0.7rem;
font-size: 0.7rem;
font-weight: 500;
font-weight: 600;
line-height: 1;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: 9999px;
background-color: var(--text-light);
color: white;
border: 1px solid transparent;
background-color: rgba(51, 51, 51, 0.08);
color: var(--text-primary);
}
.badge-primary {
background-color: var(--primary-color);
background-color: var(--primary-soft);
border-color: rgba(47, 122, 95, 0.14);
color: var(--primary-strong);
}
.badge-secondary {
background-color: var(--secondary-color);
background-color: var(--secondary-soft);
border-color: rgba(181, 110, 65, 0.14);
color: #7a4a2c;
}
.badge-success {
background-color: #28a745;
background-color: rgba(47, 122, 95, 0.1);
border-color: rgba(47, 122, 95, 0.16);
color: var(--primary-strong);
}
.badge-danger {
background-color: #dc3545;
background-color: rgba(220, 53, 69, 0.1);
border-color: rgba(220, 53, 69, 0.18);
color: #962d2d;
}
.badge-warning {
background-color: #ffc107;
color: #212529;
background-color: rgba(181, 110, 65, 0.1);
border-color: rgba(181, 110, 65, 0.18);
color: #7a4a2c;
}
.badge-info {
background-color: #17a2b8;
background-color: rgba(51, 111, 168, 0.1);
border-color: rgba(51, 111, 168, 0.18);
color: #204a78;
}
.badge-light {
background-color: #f8f9fa;
color: #212529;
background-color: var(--surface-muted);
border-color: var(--border-color);
color: var(--text-primary);
}
.badge-dark {
background-color: #343a40;
background-color: rgba(24, 70, 54, 0.12);
border-color: rgba(24, 70, 54, 0.2);
color: var(--primary-strong);
}
/* Progress-Bar */
.progress {
height: 0.625rem;
background-color: #e9ecef;
background-color: rgba(24, 70, 54, 0.08);
border-radius: var(--border-radius);
overflow: hidden;
margin: 0.75rem 0;
@@ -123,7 +138,7 @@
display: flex;
align-items: center;
justify-content: center;
color: white;
color: var(--text-on-primary);
font-size: 0.7rem;
font-weight: 500;
}
@@ -137,8 +152,8 @@
.tooltip .tooltip-text {
visibility: hidden;
width: 180px;
background-color: #333;
color: white;
background-color: rgba(24, 70, 54, 0.96);
color: var(--text-on-primary);
text-align: center;
border-radius: var(--border-radius);
padding: 0.375rem;
@@ -161,7 +176,7 @@
margin-left: -4px;
border-width: 4px;
border-style: solid;
border-color: #333 transparent transparent transparent;
border-color: rgba(24, 70, 54, 0.96) transparent transparent transparent;
}
.tooltip:hover .tooltip-text {
@@ -176,7 +191,7 @@
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.4);
background-color: rgba(19, 33, 28, 0.34);
display: flex;
align-items: center;
justify-content: center;
@@ -192,13 +207,13 @@
}
.overlay-content {
background: white;
background: var(--surface-color);
border-radius: var(--border-radius-large);
padding: 1.5rem;
max-width: 90vw;
max-height: 90vh;
overflow-y: auto;
box-shadow: var(--shadow-heavy);
box-shadow: 0 20px 48px rgba(19, 33, 28, 0.2);
transform: scale(0.9);
transition: transform 0.25s ease;
}
@@ -258,6 +273,48 @@
gap: 0.375rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.4rem;
margin-bottom: 1rem;
}
.form-group label {
font-size: 0.82rem;
font-weight: 600;
color: var(--primary-strong);
}
.filter-select,
.filter-input {
background: var(--surface-color);
border: 1px solid rgba(24, 70, 54, 0.12);
border-radius: 10px;
color: var(--text-primary);
min-height: 2.35rem;
}
.filter-select:focus,
.filter-input:focus {
border-color: rgba(47, 122, 95, 0.45);
box-shadow: 0 0 0 3px rgba(47, 122, 95, 0.1);
}
.dialog-content {
background: linear-gradient(180deg, var(--surface-color), var(--surface-muted));
}
.table-container,
.logs-table-container,
.participants-table-container {
background: var(--surface-color);
border: 1px solid rgba(24, 70, 54, 0.08);
border-radius: 14px;
box-shadow: 0 10px 24px rgba(24, 70, 54, 0.05);
overflow: hidden;
}
.tab-item {
margin: 0;
}

View File

@@ -3,26 +3,38 @@
/* Modernes, frisches Design für TrainingsTagebuch */
:root {
/* Bestehende Farben beibehalten */
--primary-color: #4CAF50;
--primary-hover: #45a049;
--secondary-color: #a07040;
--secondary-hover: #804b29;
/* Zentrale Farbpalette */
--primary-color: #2f7a5f;
--primary-hover: #245f49;
--primary-soft: #e8f2ed;
--primary-strong: #184636;
--secondary-color: #b56e41;
--secondary-hover: #915734;
--secondary-soft: #f6ebe2;
--danger-color: #dc3545;
--danger-hover: #c82333;
--nav-bg: #e0f0e8;
--nav-bg: #edf4f0;
--text-primary: #333;
--text-secondary: #666;
--text-light: #999;
--text-muted: #888;
--text-on-primary: #ffffff;
--text-on-secondary: #ffffff;
--bg-light: #f8f9fa;
--bg-canvas: #f4f6f3;
--surface-color: #ffffff;
--surface-muted: #f7faf8;
--border-color: #e9ecef;
--primary-light: #e8f2ed;
--shadow-light: 0 1px 3px rgba(0, 0, 0, 0.08);
--shadow-medium: 0 2px 8px rgba(0, 0, 0, 0.12);
--shadow-heavy: 0 4px 16px rgba(0, 0, 0, 0.15);
--shadow-small: var(--shadow-light);
--border-radius: 6px;
--border-radius-small: 6px;
--border-radius-large: 8px;
--transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
--transition-fast: 0.18s ease;
}
html, body {
@@ -34,7 +46,7 @@ html, body {
font-size: 14px;
line-height: 1.5;
color: var(--text-primary);
background-color: var(--bg-light);
background-color: var(--bg-canvas);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@@ -81,7 +93,7 @@ h3 {
/* Kompaktere Button-Styles */
button {
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: white;
color: var(--text-on-primary);
border: none;
border-radius: var(--border-radius);
padding: 0.5rem 1rem;
@@ -135,7 +147,7 @@ button.cancel-action {
button.cancel-action:hover {
background: var(--primary-color);
color: white;
color: var(--text-on-primary);
transform: translateY(-1px);
box-shadow: var(--shadow-medium);
}
@@ -155,7 +167,7 @@ button.delete-btn:hover,
button[onclick*="delete"]:hover,
button[onclick*="remove"]:hover {
background: var(--danger-color);
color: white;
color: var(--text-on-primary);
transform: translateY(-1px);
box-shadow: var(--shadow-medium);
}
@@ -177,7 +189,7 @@ button.trash-btn {
button.trash-btn:hover {
background: var(--danger-color) !important;
color: white !important;
color: var(--text-on-primary) !important;
transform: translateY(-1px);
box-shadow: var(--shadow-medium);
}
@@ -185,6 +197,7 @@ button.trash-btn:hover {
/* Sekundäre Buttons */
button.secondary {
background: linear-gradient(135deg, var(--secondary-color), var(--secondary-hover));
color: var(--text-on-secondary);
}
button.secondary:hover {
@@ -308,7 +321,7 @@ ul li.checkbox-item label {
input:focus, select:focus, textarea:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.1);
box-shadow: 0 0 0 2px rgba(47, 122, 95, 0.14);
}
input:hover, select:hover, textarea:hover {
@@ -368,7 +381,7 @@ th, td {
th {
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: white;
color: var(--text-on-primary);
font-weight: 600;
text-transform: uppercase;
font-size: 0.75rem;
@@ -376,7 +389,7 @@ th {
}
tr:hover {
background-color: rgba(76, 175, 80, 0.03);
background-color: rgba(47, 122, 95, 0.04);
}
/* Kompaktere Listen */
@@ -526,4 +539,4 @@ a:hover::after {
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
}

View File

@@ -213,10 +213,12 @@ export default {
.form-select,
.form-textarea {
width: 100%;
padding: 0.5rem;
padding: 0.75rem 0.875rem;
border: 1px solid var(--border-color);
border-radius: 4px;
border-radius: 10px;
font-family: inherit;
background: var(--surface-muted);
color: var(--text-color);
}
.form-textarea {
@@ -251,16 +253,17 @@ export default {
.accident-item {
padding: 0.5rem;
margin-bottom: 0.5rem;
background: var(--background-light);
border-radius: 4px;
background: var(--surface-muted);
border: 1px solid var(--border-color);
border-radius: 10px;
font-size: 0.9rem;
}
.btn-primary,
.btn-secondary {
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;
@@ -269,11 +272,12 @@ export default {
.btn-primary {
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: white;
color: var(--text-on-primary);
}
.btn-primary:hover:not(:disabled) {
opacity: 0.9;
opacity: 0.95;
transform: translateY(-1px);
}
.btn-primary:disabled {
@@ -282,12 +286,13 @@ export default {
}
.btn-secondary {
background: #6c757d;
color: white;
background: var(--surface-color);
color: var(--text-color);
border-color: var(--border-color);
}
.btn-secondary:hover {
background: #5a6268;
background: var(--surface-muted);
border-color: var(--primary-soft);
}
</style>

View File

@@ -341,7 +341,7 @@ export default {
}
.close-btn:hover {
background: #dc3545;
background: rgba(200, 74, 56, 0.72);
}
/* Body */
@@ -376,4 +376,3 @@ export default {
}
}
</style>

View File

@@ -131,17 +131,22 @@ export default {
</script>
<style scoped>
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; justify-content: center; align-items: center; z-index: 1000; }
.modal { background: white; border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,0.3); max-width: 600px; width: 90%; max-height: 90vh; overflow-y: auto; display: flex; flex-direction: column; }
.modal-header { padding: 1.5rem; border-bottom: 1px solid #dee2e6; }
.modal-overlay { position: fixed; inset: 0; background: rgba(15, 23, 42, 0.38); display: flex; justify-content: center; align-items: center; z-index: 1000; }
.modal { background: var(--surface-color); border: 1px solid var(--border-color); border-radius: 14px; box-shadow: 0 18px 48px rgba(15, 23, 42, 0.18); max-width: 600px; width: 90%; max-height: 90vh; overflow-y: auto; display: flex; flex-direction: column; }
.modal-header { padding: 1.5rem; border-bottom: 1px solid var(--border-color); background: linear-gradient(180deg, rgba(47, 122, 95, 0.12), rgba(47, 122, 95, 0.04)); }
.modal-header h3 { margin: 0; color: var(--text-color); }
.modal-body { padding: 1.5rem; flex: 1; }
.modal-footer { padding: 1rem 1.5rem; border-top: 1px solid #dee2e6; display: flex; justify-content: flex-end; gap: 1rem; }
.modal-footer { padding: 1rem 1.5rem; border-top: 1px solid var(--border-color); display: flex; justify-content: flex-end; gap: 1rem; background: var(--surface-muted); }
.form-group { margin-bottom: 1.5rem; }
.form-group label { display: block; margin-bottom: 0.5rem; font-weight: 600; color: #495057; }
.form-group input[type="text"], .form-group input[type="password"] { width: 100%; padding: 0.75rem; border: 1px solid #ced4da; border-radius: 4px; font-size: 1rem; box-sizing: border-box; }
.form-group label { display: block; margin-bottom: 0.5rem; font-weight: 600; color: var(--text-color); }
.form-group input[type="text"], .form-group input[type="password"] { width: 100%; padding: 0.75rem 0.875rem; border: 1px solid var(--border-color); border-radius: 10px; font-size: 1rem; box-sizing: border-box; background: var(--surface-muted); color: var(--text-color); }
.form-group input[type="text"]:focus, .form-group input[type="password"]:focus { outline: none; border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(47, 122, 95, 0.16); }
.checkbox-group label { display: flex; align-items: center; gap: 0.5rem; font-weight: normal; }
.error-message { padding: 0.75rem; background-color: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px; color: #721c24; margin-top: 1rem; }
.btn-primary, .btn-secondary { padding: 0.75rem 1.5rem; border: none; border-radius: 4px; font-size: 1rem; cursor: pointer; }
.btn-primary { background-color: #007bff; color: white; }
.btn-secondary { background-color: #6c757d; color: white; }
.error-message { padding: 0.75rem; background: rgba(200, 74, 56, 0.12); border: 1px solid rgba(200, 74, 56, 0.24); border-radius: 10px; color: #7a2e21; margin-top: 1rem; }
.btn-primary, .btn-secondary { padding: 0.75rem 1.5rem; border: 1px solid transparent; border-radius: 10px; font-size: 0.95rem; font-weight: 600; cursor: pointer; transition: background-color 0.2s ease, border-color 0.2s ease, transform 0.2s ease; }
.btn-primary { background: linear-gradient(135deg, var(--primary-color), var(--primary-hover)); color: var(--text-on-primary); }
.btn-primary:hover:not(:disabled) { transform: translateY(-1px); }
.btn-secondary { background: var(--surface-color); border-color: var(--border-color); color: var(--text-color); }
.btn-secondary:hover:not(:disabled) { background: var(--surface-muted); border-color: var(--primary-soft); }
.btn-primary:disabled, .btn-secondary:disabled { opacity: 0.55; cursor: not-allowed; }
</style>

View File

@@ -171,8 +171,8 @@ export default {
.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;
@@ -180,39 +180,42 @@ export default {
}
.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-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-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);
}
</style>

View File

@@ -157,8 +157,8 @@ export default {
.btn-primary,
.btn-secondary {
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;
@@ -167,11 +167,12 @@ export default {
.btn-primary {
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: white;
color: var(--text-on-primary);
}
.btn-primary:hover:not(:disabled) {
opacity: 0.9;
opacity: 0.95;
transform: translateY(-1px);
}
.btn-primary:disabled {
@@ -180,12 +181,13 @@ export default {
}
.btn-secondary {
background: #6c757d;
color: white;
background: var(--surface-color);
color: var(--text-color);
border-color: var(--border-color);
}
.btn-secondary:hover {
background: #5a6268;
background: var(--surface-muted);
border-color: var(--primary-soft);
}
</style>

View File

@@ -689,18 +689,18 @@ canvas {
.btn-animate {
padding: 0.5rem 1rem;
font-size: 0.9rem;
font-weight: 500;
border: 1px solid #007bff;
border-radius: 4px;
background-color: #007bff;
color: white;
font-weight: 600;
border: 1px solid transparent;
border-radius: 10px;
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: var(--text-on-primary);
cursor: pointer;
transition: background-color 0.2s, border-color 0.2s;
transition: opacity 0.2s ease, transform 0.2s ease;
}
.btn-animate:hover:not(:disabled) {
background-color: #0056b3;
border-color: #0056b3;
opacity: 0.95;
transform: translateY(-1px);
}
.btn-animate:disabled {
@@ -711,4 +711,3 @@ canvas {
}
</style>

View File

@@ -1672,14 +1672,13 @@ canvas {
}
.btn-primary {
background-color: #007bff;
color: white;
border-color: #007bff;
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: var(--text-on-primary);
border-color: transparent;
}
.btn-primary:hover {
background-color: #0056b3;
border-color: #0056b3;
opacity: 0.95;
}
.btn-secondary {
@@ -1695,14 +1694,13 @@ canvas {
/* Schlagart-Buttons (VH/RH) - Grün */
.btn-stroke {
background-color: #28a745;
color: white;
border-color: #28a745;
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: var(--text-on-primary);
border-color: transparent;
}
.btn-stroke:hover {
background-color: #218838;
border-color: #218838;
opacity: 0.95;
}
.btn-stroke.btn-secondary {
@@ -1865,7 +1863,7 @@ input[type="range"] {
margin: 0;
border: none;
border-radius: 2px;
background: #28a745; /* grün, auch wenn nichts ausgewählt ist */
background: var(--primary-color);
color: #ffffff;
font-size: 12px;
font-weight: 500;

View File

@@ -92,6 +92,7 @@ export default {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 0.25rem;
}
.form-group {
@@ -106,32 +107,36 @@ export default {
}
.file-input {
padding: 0.5rem;
padding: 0.75rem 0.875rem;
border: 1px solid var(--border-color);
border-radius: 4px;
border-radius: 10px;
cursor: pointer;
background: var(--surface-muted);
color: var(--text-color);
}
.file-input::-webkit-file-upload-button {
padding: 0.375rem 0.75rem;
padding: 0.5rem 0.9rem;
border: 1px solid var(--border-color);
border-radius: 4px;
background: white;
border-radius: 8px;
background: var(--surface-color);
color: var(--text-color);
cursor: pointer;
margin-right: 0.5rem;
}
.file-input::-webkit-file-upload-button:hover {
background: var(--background-light);
background: var(--surface-muted);
}
.file-info {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
background: var(--background-light);
border-radius: 4px;
padding: 0.75rem 0.875rem;
background: var(--surface-muted);
border: 1px solid var(--border-color);
border-radius: 10px;
font-size: 0.875rem;
}
@@ -150,9 +155,9 @@ export default {
.btn-primary,
.btn-secondary {
padding: 0.5rem 1.5rem;
border: none;
border-radius: 4px;
padding: 0.65rem 1.4rem;
border: 1px solid transparent;
border-radius: 10px;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
@@ -161,7 +166,7 @@ export default {
.btn-primary {
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: white;
color: var(--text-on-primary);
}
.btn-primary:hover:not(:disabled) {
@@ -174,12 +179,13 @@ export default {
}
.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);
}
</style>

View File

@@ -56,12 +56,26 @@ import { mapGetters, mapActions } from 'vuex';
import MatchReportDialog from './MatchReportDialog.vue';
import MatchReportApiDialog from './MatchReportApiDialog.vue';
import MatchReportHeaderActions from './MatchReportHeaderActions.vue';
import MyTischtennisAccount from '../views/MyTischtennisAccount.vue';
import ClickTtAccount from '../views/ClickTtAccount.vue';
import PermissionsView from '../views/PermissionsView.vue';
import MemberTransferSettingsView from '../views/MemberTransferSettingsView.vue';
import LogsView from '../views/LogsView.vue';
import ClickTtView from '../views/ClickTtView.vue';
import PersonalSettings from '../views/PersonalSettings.vue';
export default {
components: {
MatchReportDialog,
MatchReportApiDialog,
MatchReportHeaderActions
MatchReportHeaderActions,
MyTischtennisAccount,
ClickTtAccount,
PermissionsView,
MemberTransferSettingsView,
LogsView,
ClickTtView,
PersonalSettings
},
name: 'DialogManager',
computed: {
@@ -74,7 +88,14 @@ export default {
const components = {
'MatchReportDialog': MatchReportDialog,
'MatchReportApiDialog': MatchReportApiDialog,
'MatchReportHeaderActions': MatchReportHeaderActions
'MatchReportHeaderActions': MatchReportHeaderActions,
'MyTischtennisAccount': MyTischtennisAccount,
'ClickTtAccount': ClickTtAccount,
'PermissionsView': PermissionsView,
'MemberTransferSettingsView': MemberTransferSettingsView,
'LogsView': LogsView,
'ClickTtView': ClickTtView,
'PersonalSettings': PersonalSettings
};
const component = components[componentName] || null;
return component;
@@ -193,7 +214,7 @@ export default {
.dialog-header {
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: white;
color: var(--text-on-primary);
padding: 12px 16px;
display: flex;
justify-content: space-between;
@@ -207,6 +228,7 @@ export default {
font-size: 1rem;
font-weight: 600;
flex: 1;
color: var(--text-on-primary);
}
.dialog-header-actions {
@@ -225,7 +247,7 @@ export default {
border: none;
border-radius: 4px;
background: rgba(255, 255, 255, 0.2);
color: white;
color: var(--text-on-primary);
cursor: pointer;
display: flex;
align-items: center;
@@ -240,7 +262,7 @@ export default {
}
.close-btn:hover {
background: #dc3545;
background: rgba(200, 74, 56, 0.72);
}
.dialog-content {
@@ -254,7 +276,7 @@ export default {
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(135deg, rgba(160, 112, 64, 0.95), rgba(128, 75, 41, 0.95));
background: linear-gradient(135deg, rgba(24, 70, 54, 0.96), rgba(47, 122, 95, 0.94));
padding: 4px 16px;
display: flex;
gap: 8px;
@@ -272,7 +294,7 @@ export default {
.minimized-dialog-button {
background: var(--primary-color);
color: white;
color: var(--text-on-primary);
border: none;
padding: 8px 16px;
border-radius: 4px;

View File

@@ -79,9 +79,10 @@ export default {
.activity-empty-state {
padding: 0.7rem 0.75rem;
border-radius: 8px;
background: #f3f7fa;
color: #516978;
border-radius: 10px;
background: var(--surface-muted);
border: 1px solid var(--border-color);
color: var(--text-muted);
margin-bottom: 0.75rem;
}
</style>

View File

@@ -144,10 +144,10 @@ export default {
.participant-gallery-button {
padding: 0.45rem 0.75rem;
border: 1px solid #ccd8e0;
border-radius: 8px;
background: #f3f7fa;
color: #365266;
border: 1px solid var(--border-color);
border-radius: 10px;
background: var(--surface-muted);
color: var(--text-color);
font-weight: 600;
cursor: pointer;
}
@@ -159,10 +159,17 @@ export default {
.participant-search-input {
width: 100%;
padding: 0.55rem 0.7rem;
border: 1px solid #ccd8e0;
border-radius: 8px;
background: #fff;
padding: 0.65rem 0.8rem;
border: 1px solid var(--border-color);
border-radius: 10px;
background: var(--surface-color);
color: var(--text-color);
}
.participant-search-input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(47, 122, 95, 0.14);
}
.participant-filter-chips {
@@ -172,19 +179,20 @@ export default {
}
.participant-filter-chip {
border: 1px solid #ccd8e0;
background: #f3f7fa;
color: #365266;
border: 1px solid var(--border-color);
background: var(--surface-muted);
color: var(--text-color);
border-radius: 999px;
padding: 0.35rem 0.75rem;
font-size: 0.85rem;
cursor: pointer;
font-weight: 600;
}
.participant-filter-chip.active {
background: #dcebf5;
border-color: #8eb7d1;
color: #14374c;
background: rgba(47, 122, 95, 0.12);
border-color: var(--primary-soft);
color: var(--primary-strong);
font-weight: 600;
}
@@ -193,9 +201,9 @@ export default {
font-size: 10px;
width: 5em;
padding: 2px 4px;
border: 1px solid #ccc;
border-radius: 3px;
background-color: white;
color: #333;
border: 1px solid var(--border-color);
border-radius: 6px;
background-color: var(--surface-color);
color: var(--text-color);
}
</style>

View File

@@ -87,7 +87,7 @@ export default {
max-width: 100%;
max-height: 70vh;
object-fit: contain;
border-radius: 4px;
border-radius: 12px;
}
.no-image {
@@ -106,18 +106,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);
}
/* Responsive */
@@ -127,4 +128,3 @@ export default {
}
}
</style>

View File

@@ -201,8 +201,8 @@ export default {
.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;
@@ -212,16 +212,16 @@ 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-success {
background: var(--primary-color);
color: white;
color: var(--text-on-primary);
}
.btn-success:hover {
@@ -229,21 +229,22 @@ export default {
}
.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);
}
</style>

View File

@@ -4650,7 +4650,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
}
.double-btn.active {
background-color: #28a745;
background-color: var(--primary-color);
color: white;
border-color: white;
}
@@ -4818,13 +4818,13 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
/* Zertifizierung-Styles */
.section-btn.certified {
background-color: #d4edda;
border-color: #28a745;
border-color: var(--primary-color);
color: #155724;
}
.certified-badge {
font-size: 0.8em;
background-color: #28a745;
background-color: var(--primary-color);
color: white;
padding: 0 6px;
border-radius: 10px;
@@ -5019,20 +5019,20 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
}
.copy-json-btn {
background: #007bff;
color: white;
border: none;
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: var(--text-on-primary);
border: 1px solid transparent;
padding: 10px 20px;
border-radius: 6px;
border-radius: 10px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
font-weight: 600;
transition: all 0.2s;
align-self: flex-start;
}
.copy-json-btn:hover {
background: #0056b3;
opacity: 0.95;
transform: translateY(-1px);
}
@@ -5179,19 +5179,19 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
}
.time-btn-small {
background-color: #28a745;
color: white;
border: none;
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: var(--text-on-primary);
border: 1px solid transparent;
padding: 4px 8px;
border-radius: 4px;
border-radius: 8px;
font-size: 12px;
font-weight: 500;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.time-btn-small:hover:not(:disabled) {
background-color: #218838;
transform: translateY(-1px);
}
.time-btn-small:disabled {
@@ -5209,11 +5209,11 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
}
.time-btn {
background-color: #28a745;
color: white;
border: none;
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: var(--text-on-primary);
border: 1px solid transparent;
padding: 8px 16px;
border-radius: 6px;
border-radius: 10px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
@@ -5224,7 +5224,6 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
}
.time-btn:hover:not(:disabled) {
background-color: #218838;
transform: translateY(-1px);
}
@@ -5236,11 +5235,11 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
}
.autofill-btn {
background-color: #28a745;
color: white;
border: none;
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: var(--text-on-primary);
border: 1px solid transparent;
padding: 12px 20px;
border-radius: 8px;
border-radius: 10px;
font-weight: 600;
cursor: pointer;
display: flex;
@@ -5248,13 +5247,12 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
gap: 8px;
transition: all 0.3s ease;
font-size: 14px;
box-shadow: 0 4px 12px rgba(40, 167, 69, 0.3);
box-shadow: 0 8px 18px rgba(47, 122, 95, 0.18);
}
.autofill-btn:hover:not(:disabled) {
background-color: #218838;
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(40, 167, 69, 0.4);
box-shadow: 0 12px 24px rgba(47, 122, 95, 0.24);
}
.autofill-btn:disabled {
@@ -5307,10 +5305,10 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
}
.pin-modal-error {
color: #dc3545;
background-color: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 4px;
color: #8b3327;
background-color: rgba(200, 74, 56, 0.12);
border: 1px solid rgba(200, 74, 56, 0.24);
border-radius: 10px;
padding: 10px;
margin-top: 10px;
}
@@ -5324,28 +5322,31 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
}
.btn-secondary {
background-color: #6c757d;
color: white;
border: none;
background: var(--surface-color);
color: var(--text-color);
border: 1px solid var(--border-color);
padding: 10px 20px;
border-radius: 4px;
border-radius: 10px;
cursor: pointer;
font-weight: 600;
}
.btn-secondary:hover {
background-color: #5a6268;
background: var(--surface-muted);
border-color: var(--primary-soft);
}
.btn-primary {
background-color: var(--primary-color);
color: white;
border: none;
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: var(--text-on-primary);
border: 1px solid transparent;
padding: 10px 20px;
border-radius: 4px;
border-radius: 10px;
cursor: pointer;
font-weight: 600;
}
.btn-primary:hover {
background-color: var(--primary-hover);
opacity: 0.95;
}
</style>

View File

@@ -159,10 +159,10 @@ export default {
.section-title {
font-weight: 600;
font-size: 1.1rem;
color: var(--primary-color);
color: var(--primary-strong);
margin: 0;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--background-light);
border-bottom: 1px solid var(--border-color);
}
.participations-list,
@@ -178,11 +178,11 @@ export default {
.participation-date-header {
font-weight: 600;
color: var(--primary-color);
color: var(--primary-strong);
font-size: 0.95rem;
margin-bottom: 0.5rem;
padding-bottom: 0.25rem;
border-bottom: 1px solid var(--background-light);
border-bottom: 1px solid var(--border-color);
}
.participation-activities {
@@ -198,14 +198,15 @@ export default {
justify-content: space-between;
align-items: center;
padding: 0.75rem;
background: var(--background-light);
border-radius: 4px;
background: var(--surface-muted);
border: 1px solid var(--border-color);
border-radius: 10px;
transition: background-color 0.3s ease;
}
.participation-item:hover,
.stat-item:hover {
background: var(--background-hover, rgba(0, 0, 0, 0.05));
background: rgba(47, 122, 95, 0.08);
}
.participation-name,
@@ -222,7 +223,7 @@ export default {
.stat-count {
font-weight: 600;
color: var(--primary-color);
color: var(--primary-strong);
font-size: 1.1rem;
}
@@ -233,4 +234,3 @@ export default {
font-style: italic;
}
</style>

View File

@@ -238,7 +238,7 @@ export default {
display: flex;
align-items: center;
gap: 12px;
background: #f9f9f9;
background: var(--surface-muted);
flex-shrink: 0;
position: relative;
z-index: 10;
@@ -250,11 +250,12 @@ export default {
}
.gallery-controls select {
padding: 6px 12px;
padding: 0.6rem 0.8rem;
border: 1px solid var(--border-color, #ddd);
border-radius: 4px;
border-radius: 10px;
font-size: 14px;
background: white;
background: var(--surface-color);
color: var(--text-color, #333);
cursor: pointer;
}
@@ -324,8 +325,8 @@ export default {
.gallery-member-name {
font-size: 12px;
text-align: center;
color: #ff6b6b;
font-weight: 500;
color: #fff7e8;
font-weight: 600;
margin: 0;
padding: 4px 8px;
position: absolute;
@@ -339,16 +340,16 @@ export default {
}
.gallery-member-item.is-participant .gallery-member-name {
color: #51cf66;
color: #d9f7df;
}
.gallery-member-placeholder {
display: flex;
align-items: center;
justify-content: center;
background-color: #f0f0f0;
background-color: var(--surface-muted);
border-radius: 0;
color: #999;
color: var(--text-muted);
font-size: 12px;
margin: 0;
position: absolute;
@@ -359,4 +360,3 @@ export default {
box-sizing: border-box;
}
</style>

View File

@@ -130,8 +130,9 @@ export default {
.notes-header-info {
padding: 0.5rem;
background: var(--background-light);
border-radius: 4px;
background: var(--surface-muted);
border: 1px solid var(--border-color);
border-radius: 10px;
font-size: 0.9rem;
color: var(--text-muted);
}
@@ -173,11 +174,13 @@ export default {
.note-textarea {
width: 100%;
padding: 0.5rem;
padding: 0.75rem 0.875rem;
border: 1px solid var(--border-color);
border-radius: 4px;
border-radius: 10px;
font-family: inherit;
resize: vertical;
background: var(--surface-muted);
color: var(--text-color);
}
.notes-list h4 {
@@ -197,8 +200,9 @@ export default {
gap: 0.5rem;
padding: 0.5rem;
margin-bottom: 0.5rem;
background: var(--background-light);
border-radius: 4px;
background: var(--surface-muted);
border: 1px solid var(--border-color);
border-radius: 10px;
}
.note-content {
@@ -221,17 +225,18 @@ export default {
.btn-primary {
align-self: flex-start;
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
border: 1px solid transparent;
border-radius: 10px;
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: white;
color: var(--text-on-primary);
font-weight: 600;
cursor: pointer;
transition: opacity 0.2s;
transition: opacity 0.2s ease, transform 0.2s ease;
}
.btn-primary:hover {
opacity: 0.9;
opacity: 0.95;
transform: translateY(-1px);
}
@media (max-width: 768px) {
@@ -246,4 +251,3 @@ export default {
}
}
</style>

View File

@@ -141,6 +141,7 @@ export default {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 0.25rem;
}
.controls-bar {
@@ -167,8 +168,8 @@ export default {
.recommendations-column h4 {
margin: 0 0 0.75rem 0;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--primary-color);
color: var(--primary-color);
border-bottom: 1px solid var(--primary-soft);
color: var(--primary-strong);
}
.checkbox-list {
@@ -176,22 +177,23 @@ export default {
overflow-y: auto;
max-height: 500px;
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 0.5rem;
border-radius: 12px;
padding: 0.625rem;
background: var(--surface-muted);
}
.checkbox-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
padding: 0.625rem 0.7rem;
cursor: pointer;
border-radius: 4px;
border-radius: 10px;
transition: background-color 0.2s;
}
.checkbox-item:hover {
background: var(--background-light);
background: rgba(47, 122, 95, 0.08);
}
.checkbox-item input[type="checkbox"] {
@@ -215,9 +217,9 @@ export default {
.btn-primary,
.btn-secondary {
padding: 0.5rem 1.5rem;
border: none;
border-radius: 4px;
padding: 0.65rem 1.4rem;
border: 1px solid transparent;
border-radius: 10px;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
@@ -226,7 +228,7 @@ export default {
.btn-primary {
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: white;
color: var(--text-on-primary);
}
.btn-primary:hover:not(:disabled) {
@@ -239,12 +241,14 @@ export default {
}
.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);
}
@media (max-width: 768px) {
@@ -257,4 +261,3 @@ export default {
}
}
</style>

View File

@@ -604,16 +604,17 @@ export default {
display: inline-block;
margin-top: 1rem;
padding: 0.75rem 1.5rem;
background-color: #007bff;
color: white;
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: var(--text-on-primary);
text-decoration: none;
border-radius: 4px;
font-weight: 500;
transition: background-color 0.2s;
border-radius: 10px;
font-weight: 600;
transition: transform 0.2s ease, opacity 0.2s ease;
}
.btn-link:hover {
background-color: #0056b3;
opacity: 0.95;
transform: translateY(-1px);
}
.config-summary {
@@ -656,10 +657,10 @@ export default {
.btn-link-small {
display: inline-block;
color: #007bff;
color: var(--primary-color);
text-decoration: none;
font-size: 0.9em;
font-weight: 500;
font-weight: 600;
}
.btn-link-small:hover {
@@ -674,18 +675,19 @@ export default {
}
.btn-primary {
background-color: #007bff;
color: white;
border: none;
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: var(--text-on-primary);
border: 1px solid transparent;
padding: 0.5rem 1.5rem;
border-radius: 4px;
border-radius: 10px;
cursor: pointer;
font-weight: 500;
transition: background-color 0.2s;
font-weight: 600;
transition: transform 0.2s ease, opacity 0.2s ease;
}
.btn-primary:hover:not(:disabled) {
background-color: #0056b3;
opacity: 0.95;
transform: translateY(-1px);
}
.btn-primary:disabled {
@@ -695,18 +697,18 @@ export default {
}
.btn-secondary {
background-color: #6c757d;
color: white;
border: none;
background: var(--surface-color);
color: var(--text-color);
border: 1px solid var(--border-color);
padding: 0.5rem 1.5rem;
border-radius: 4px;
border-radius: 10px;
cursor: pointer;
font-weight: 500;
transition: background-color 0.2s;
font-weight: 600;
transition: background-color 0.2s ease, border-color 0.2s ease;
}
.btn-secondary:hover {
background-color: #5a6268;
background: var(--surface-muted);
border-color: var(--primary-soft);
}
</style>

View File

@@ -235,7 +235,7 @@ export default {
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
background: rgba(15, 23, 42, 0.38);
display: flex;
justify-content: center;
align-items: center;
@@ -243,9 +243,10 @@ export default {
}
.modal {
background: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
background: var(--surface-color);
border: 1px solid var(--border-color);
border-radius: 14px;
box-shadow: 0 18px 48px rgba(15, 23, 42, 0.18);
max-width: 600px;
width: 90%;
max-height: 90vh;
@@ -256,12 +257,13 @@ export default {
.modal-header {
padding: 1.5rem;
border-bottom: 1px solid #dee2e6;
border-bottom: 1px solid var(--border-color);
background: linear-gradient(180deg, rgba(47, 122, 95, 0.12), rgba(47, 122, 95, 0.04));
}
.modal-header h3 {
margin: 0;
color: #495057;
color: var(--text-color);
}
.modal-body {
@@ -271,10 +273,11 @@ export default {
.modal-footer {
padding: 1rem 1.5rem;
border-top: 1px solid #dee2e6;
border-top: 1px solid var(--border-color);
display: flex;
justify-content: flex-end;
gap: 1rem;
background: var(--surface-muted);
}
.form-group {
@@ -285,26 +288,28 @@ export default {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
color: #495057;
color: var(--text-color);
}
.form-group input[type="text"],
.form-group input[type="email"],
.form-group input[type="password"] {
width: 100%;
padding: 0.75rem;
border: 1px solid #ced4da;
border-radius: 4px;
padding: 0.75rem 0.875rem;
border: 1px solid var(--border-color);
border-radius: 10px;
font-size: 1rem;
box-sizing: border-box;
background: var(--surface-muted);
color: var(--text-color);
}
.form-group input[type="text"]:focus,
.form-group input[type="email"]:focus,
.form-group input[type="password"]:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(47, 122, 95, 0.16);
}
.checkbox-group label {
@@ -324,57 +329,58 @@ export default {
.hint {
margin-top: 0.5rem;
font-size: 0.875rem;
color: #6c757d;
font-style: italic;
color: var(--text-muted);
}
.warning {
margin-top: 0.5rem;
font-size: 0.875rem;
color: #dc3545;
color: var(--danger-color);
font-weight: 600;
}
.error-message {
padding: 0.75rem;
background-color: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 4px;
color: #721c24;
background: rgba(200, 74, 56, 0.12);
border: 1px solid rgba(200, 74, 56, 0.24);
border-radius: 10px;
color: #7a2e21;
margin-top: 1rem;
}
.btn-primary, .btn-secondary {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
font-size: 1rem;
border: 1px solid transparent;
border-radius: 10px;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s ease;
transition: background-color 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
}
.btn-primary {
background-color: #007bff;
color: white;
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: var(--text-on-primary);
}
.btn-primary:hover:not(:disabled) {
background-color: #0056b3;
transform: translateY(-1px);
}
.btn-primary:disabled {
background-color: #6c757d;
cursor: not-allowed;
opacity: 0.6;
opacity: 0.55;
}
.btn-secondary {
background-color: #6c757d;
color: white;
background: var(--surface-color);
border-color: var(--border-color);
color: var(--text-color);
}
.btn-secondary:hover:not(:disabled) {
background-color: #545b62;
background: var(--surface-muted);
border-color: var(--primary-soft);
}
.btn-secondary:disabled {
@@ -382,4 +388,3 @@ export default {
opacity: 0.6;
}
</style>

View File

@@ -446,16 +446,16 @@ export default {
}
.download-btn:hover:not(:disabled) {
background: #0056b3;
background: var(--secondary-hover);
}
.create-btn {
background: #28a745;
color: white;
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: var(--text-on-primary);
}
.create-btn:hover:not(:disabled) {
background: #218838;
opacity: 0.95;
}
.analyze-btn:disabled, .download-btn:disabled, .create-btn:disabled {
@@ -525,4 +525,4 @@ export default {
.log-entry {
margin-bottom: 2px;
}
</style>
</style>

View File

@@ -130,6 +130,7 @@ export default {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 0.25rem;
}
.form-row {
@@ -153,11 +154,13 @@ export default {
.form-input,
.form-select {
width: 100%;
padding: 0.5rem;
padding: 0.75rem 0.875rem;
border: 1px solid var(--border-color);
border-radius: 4px;
border-radius: 10px;
font-family: inherit;
font-size: 0.9rem;
background: var(--surface-muted);
color: var(--text-color);
}
.form-input:focus,
@@ -169,9 +172,9 @@ export default {
.btn-primary,
.btn-secondary {
padding: 0.5rem 1.5rem;
border: none;
border-radius: 4px;
padding: 0.65rem 1.4rem;
border: 1px solid transparent;
border-radius: 10px;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
@@ -180,7 +183,7 @@ export default {
.btn-primary {
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: white;
color: var(--text-on-primary);
}
.btn-primary:hover:not(:disabled) {
@@ -193,12 +196,14 @@ export default {
}
.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);
}
@media (max-width: 768px) {
@@ -207,4 +212,3 @@ export default {
}
}
</style>

View File

@@ -453,17 +453,18 @@ export default {
}
.btn-primary {
background: #28a745;
color: #fff;
border: none;
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: var(--text-on-primary);
border: 1px solid transparent;
padding: 8px 16px;
border-radius: 4px;
border-radius: 10px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 600;
}
.btn-primary:hover {
background: #218838;
opacity: 0.95;
}
.btn-secondary {
@@ -480,4 +481,3 @@ export default {
background: #5a6268;
}
</style>

View File

@@ -75,6 +75,8 @@
"logs": "System-Logs",
"memberTransfer": "Mitgliederübertragung",
"myTischtennisAccount": "myTischtennis-Account",
"clickTtAccount": "HTTV / click-TT Account",
"clickTtBrowser": "HTTV / click-TT",
"personalSettings": "Persönliche Einstellungen",
"logout": "Ausloggen",
"login": "Einloggen",
@@ -441,6 +443,9 @@
"addGroup": "Gruppe hinzufügen",
"activityOrTimeblock": "Aktivität / Zeitblock",
"durationMinutes": "Dauer (Min)",
"standardActivities": "Standard-Aktivitäten",
"standardDurationShort": "Min",
"standardActivityAddError": "Standard-Aktivität konnte nicht hinzugefügt werden.",
"addGroupActivity": "Gruppen-Aktivität hinzufügen",
"addGroupButton": "+ Gruppe",
"all": "Alle",
@@ -497,7 +502,7 @@
"errorOccurred": "Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.",
"trainingTimesUpdated": "Trainingszeiten erfolgreich aktualisiert.",
"noActiveTrainingDay": "Kein Trainingstag ausgewählt.",
"statusReady": "Zeiten, Teilnehmer und Trainingsplan sind gepflegt.",
"statusReady": "Zeiten und Trainingsplan sind gepflegt.",
"statusEmpty": "Dieser Trainingstag ist noch leer.",
"statusInProgress": "Dieser Trainingstag ist teilweise vorbereitet.",
"formMarkedAsHandedOver": "Mitgliedsformular als ausgehändigt markiert",
@@ -1090,6 +1095,9 @@
"title": "Vordefinierte Aktivitäten",
"new": "Neu",
"reload": "Neu laden",
"filterAll": "Alle",
"filterStandard": "Standard-Aktivitäten",
"filterCustom": "Selbstdefiniert",
"searchPlaceholder": "Kürzel suchen (z.B. 'as vh us' oder 'vh as us')...",
"deduplicate": "Doppelungen zusammenführen",
"selectSource": "Quelle wählen…",
@@ -1104,6 +1112,7 @@
"duration": "Dauer (Minuten)",
"durationText": "Dauer (Text)",
"durationTextPlaceholder": "z.B. 2x7",
"excludeFromStats": "Nicht in Statistik berücksichtigen",
"description": "Beschreibung",
"addImage": "Bild hinzufügen",
"imageHelp": "Du kannst entweder einen Link zu einem Bild eingeben oder ein Bild hochladen:",
@@ -1130,15 +1139,31 @@
},
"schedule": {
"title": "Spielpläne",
"subtitle": "Teams auswählen, Spieltage prüfen und Ligatabellen im Blick behalten.",
"importSchedule": "Spielplanimport",
"galleryLoading": "Galerie wird geladen…",
"gallery": "Mitglieder-Galerie",
"overallSchedule": "Gesamtspielplan",
"adultSchedule": "Spielplan Erwachsene",
"noTeamsFound": "Keine Teams für diese Saison gefunden",
"noMatchingTeams": "Keine Teams passen zur aktuellen Suche.",
"searchTeams": "Team oder Liga suchen",
"selection": "Auswahl",
"activeSelection": "Aktive Auswahl",
"noSelectionTitle": "Noch keine Auswahl aktiv",
"noSelectionMessage": "Wählen Sie links einen Gesamtspielplan, den Erwachsenenspielplan oder ein Team aus.",
"scheduleTab": "Spielplan",
"tableTab": "Tabelle",
"downloadPDF": "Download PDF",
"refreshTable": "Tabelle aktualisieren",
"tableLoading": "Tabelle wird geladen…",
"fetchTeamData": "Spielplan & Ergebnisse abrufen",
"fetchingTeamData": "Abruf läuft…",
"fetchStartFailed": "Abruf konnte nicht gestartet werden.",
"fetchDataFailed": "Teamdaten konnten nicht abgerufen werden.",
"fetchTimedOut": "Zeitüberschreitung beim Abruf.",
"teamDataFetched": "Teamdaten erfolgreich aktualisiert.",
"teamDataFetchedDetails": "Mannschaft: {team}\nVerarbeitete Datensätze: {count}",
"gamesFor": "Spiele für",
"date": "Datum",
"time": "Uhrzeit",
@@ -1153,7 +1178,19 @@
"leagueTable": "Ligatabelle",
"position": "Platz",
"team": "Team",
"teams": "Teams",
"matches": "Matches",
"completedShort": "Abgeschlossen",
"pendingShort": "Offen",
"nextMatch": "Nächstes Spiel",
"workspaceScheduleDescription": "{matches} Spiele, davon {completed} abgeschlossen und {pending} offen.",
"workspaceTableDescription": "{count} Tabellenzeilen aktuell geladen.",
"matchOverviewTitle": "Spielübersicht",
"matchOverviewDescription": "Steuert, welche Spiele aus der aktuell gewählten Liga im Spielplan angezeigt werden.",
"ownTeamMatches": "Eigene Mannschaft",
"allLeagueMatches": "Alle Spiele",
"otherTeamMatches": "Andere Mannschaft",
"selectOtherTeam": "Andere Mannschaft wählen",
"sets": "Sätze",
"points": "Pkt.",
"balls": "Bälle",
@@ -1196,6 +1233,7 @@
},
"teamManagement": {
"title": "Team-Verwaltung",
"subtitle": "Teams auswählen, Konfiguration prüfen und Liga-Daten an einem Ort pflegen.",
"ratingUpdates": "Rating-Updates",
"lastRun": "Zuletzt",
"updated": "aktualisiert",
@@ -1204,16 +1242,24 @@
"fetched": "abgerufen",
"newTeam": "Neues Team",
"basicSettings": "Grundeinstellungen",
"basicSettingsIntro": "Teamname und Liga in einem ruhigen Formular prüfen und anpassen.",
"editTeam": "Team bearbeiten",
"createNewTeam": "Neues Team anlegen",
"teamName": "Team-Name",
"teamNamePlaceholder": "z.B. Herren 1, Damen 2",
"league": "Spielklasse",
"team": "Team",
"teamId": "Team-ID",
"groupId": "Gruppen-ID",
"association": "Verband",
"groupName": "Gruppenname",
"noLeague": "Keine Spielklasse",
"change": "Ändern",
"createAndEdit": "Anlegen & Bearbeiten",
"clearFields": "Felder leeren",
"playerStats": "Spieleinsätze",
"playerStatsIntro": "Schneller Überblick über Einsätze der Mannschaft in dieser Saison.",
"refreshStats": "Aktualisieren",
"loadingStats": "Lade Statistiken...",
"noPlayerStats": "Keine Spieleinsätze erfasst.",
"player": "Spieler",
@@ -1225,6 +1271,14 @@
"secondHalf": "Rückrunde",
"secondHalfFull": "Rückrunde (ab 1. Januar)",
"documents": "Dokumente",
"documentsIntro": "Code- und PIN-Listen hochladen, prüfen und bei Bedarf erneut parsen.",
"documentAvailable": "Vorhanden",
"documentMissing": "Fehlt",
"latestUpload": "Zuletzt hochgeladen: {date}",
"noDocumentUploadYet": "Noch kein Dokument hochgeladen.",
"uploadNewVersion": "Neue Version hochladen",
"openDocument": "Anzeigen",
"parseDocument": "Neu parsen",
"codeList": "Code-Liste",
"pinList": "Pin-Liste",
"automaticJobs": "Automatische Jobs",
@@ -1233,10 +1287,16 @@
"successful": "Erfolgreich",
"noAutomaticUpdate": "Noch keine automatische Aktualisierung",
"myTischtennis": "MyTischtennis",
"myTischtennisIntro": "URL einfügen, Konfiguration prüfen und bei Bedarf Daten manuell abrufen.",
"configurationStatus": "Konfigurationsstatus",
"manualFetch": "Abrufen",
"noIssues": "Keine offenen Konfigurationsprobleme.",
"parseUrlAction": "URL prüfen",
"myTischtennisUrlPlaceholder": "MyTischtennis URL...",
"teams": "Teams",
"activeTeam": "Aktives Team",
"searchTeams": "Team suchen",
"openInWorkspace": "Zum Bearbeiten öffnen",
"filterAll": "Alle",
"filterConfigured": "Konfiguriert",
"filterNeedsAttention": "Prüfen",

View File

@@ -12,6 +12,7 @@
"edit": "Edit",
"add": "Add",
"close": "Close",
"details": "Details",
"confirm": "Confirm",
"yes": "Yes",
"no": "No",
@@ -41,6 +42,8 @@
"logs": "System Logs",
"memberTransfer": "Member Transfer",
"myTischtennisAccount": "myTischtennis Account",
"clickTtAccount": "HTTV / click-TT Account",
"clickTtBrowser": "HTTV / click-TT",
"personalSettings": "Personal Settings",
"logout": "Logout",
"login": "Login",
@@ -126,6 +129,18 @@
"confirm": "Confirm",
"cancel": "Cancel"
},
"diary": {
"standardActivities": "Standard activities",
"standardDurationShort": "Min",
"standardActivityAddError": "Standard activity could not be added.",
"statusReady": "Times and training plan are set."
},
"predefinedActivities": {
"excludeFromStats": "Exclude from statistics",
"filterAll": "All",
"filterStandard": "Standard activities",
"filterCustom": "Custom"
},
"tournaments": {
"numberOfTables": "Number of tables",
"table": "Table",
@@ -364,6 +379,100 @@
"statusActionGenerate": "Generate",
"statusActionStart": "Start"
},
"schedule": {
"title": "Schedules",
"subtitle": "Select teams, review fixtures and keep league tables in view.",
"importSchedule": "Import schedule",
"galleryLoading": "Loading gallery…",
"gallery": "Member gallery",
"overallSchedule": "Overall schedule",
"adultSchedule": "Adult schedule",
"noTeamsFound": "No teams found for this season",
"noMatchingTeams": "No teams match the current search.",
"searchTeams": "Search team or league",
"selection": "Selection",
"activeSelection": "Active selection",
"noSelectionTitle": "No active selection yet",
"noSelectionMessage": "Choose the overall schedule, the adult schedule or a team from the left.",
"scheduleTab": "Schedule",
"tableTab": "Table",
"downloadPDF": "Download PDF",
"refreshTable": "Refresh table",
"tableLoading": "Loading table…",
"fetchTeamData": "Fetch schedule & results",
"fetchingTeamData": "Fetching…",
"fetchStartFailed": "The fetch could not be started.",
"fetchDataFailed": "Team data could not be fetched.",
"fetchTimedOut": "Timed out while fetching data.",
"teamDataFetched": "Team data updated successfully.",
"teamDataFetchedDetails": "Team: {team}\nProcessed records: {count}",
"gamesFor": "Matches for",
"date": "Date",
"time": "Time",
"homeTeam": "Home team",
"guestTeam": "Away team",
"result": "Result",
"ageClass": "Age group",
"code": "Code",
"homePin": "Home PIN",
"guestPin": "Away PIN",
"noGames": "No matches available",
"leagueTable": "League table",
"position": "Position",
"team": "Team",
"teams": "Teams",
"matches": "Matches",
"completedShort": "Completed",
"pendingShort": "Open",
"nextMatch": "Next match",
"workspaceScheduleDescription": "{matches} matches, {completed} completed and {pending} open.",
"workspaceTableDescription": "{count} table rows currently loaded.",
"matchOverviewTitle": "Match overview",
"matchOverviewDescription": "Controls which matches from the currently selected league are shown in the schedule.",
"ownTeamMatches": "Own team",
"allLeagueMatches": "All matches",
"otherTeamMatches": "Other team",
"selectOtherTeam": "Select other team",
"sets": "Sets",
"points": "Pts",
"balls": "Balls",
"noTableData": "No table data available",
"galleryTitle": "Member gallery - click an image to mark as ready",
"imageSize": "Image size",
"noMembersWithImages": "No members with images found.",
"playerSelection": "Player selection",
"vs": "vs",
"loadingMembers": "Loading members...",
"player": "Player",
"ready": "Ready",
"planned": "Planned",
"played": "Played",
"noActiveMembers": "No active members found",
"save": "Save",
"cancel": "Cancel",
"openMatchReport": "Open match report",
"copyCode": "Copy code",
"copyHomePin": "Copy home PIN",
"copyGuestPin": "Copy away PIN",
"errorLoadingMembers": "Failed to load the member list.",
"playerSelectionSaved": "Player selection saved",
"errorSavingPlayerSelection": "Failed to save the player selection",
"pleaseSelectGame": "Please select a match first",
"errorLoadingGallery": "The gallery could not be loaded.",
"errorLoadingTeams": "Failed to load teams",
"noLeagueForTeam": "No league is assigned to this team. Please assign a league first.",
"errorLoadingMatches": "Failed to load matches",
"errorLoadingOverallSchedule": "Failed to load the overall schedule",
"errorLoadingAdultSchedule": "Failed to load the adult schedule",
"noMatchesForPDF": "No matches found to generate a PDF.",
"errorCopying": "Error while copying",
"schedulePDF": "Schedules.pdf",
"selectSpecificLeague": "Please select a specific league to load the table.",
"tableDataLoaded": "League table data loaded successfully from myTischtennis.",
"errorLoadingTable": "Failed to load league table data from myTischtennis",
"scheduleImportSuccess": "Schedule imported successfully.",
"errorImportingCSV": "Failed to import the CSV file"
},
"members": {
"subtitle": "Search, filter and edit members directly.",
"closeEditor": "Close editor",
@@ -466,5 +575,142 @@
"composeEmail": "Compose email",
"copyContactSummary": "Copy contact summary",
"copyContactSummarySuccess": "Contact summary copied to clipboard."
},
"teamManagement": {
"title": "Team Management",
"subtitle": "Select teams, review configuration and maintain league data in one place.",
"ratingUpdates": "Rating updates",
"lastRun": "Last run",
"updated": "updated",
"error": "Error",
"matchResults": "Match results",
"fetched": "fetched",
"newTeam": "New team",
"basicSettings": "Basic settings",
"basicSettingsIntro": "Review and adjust team name and league in a calmer form.",
"editTeam": "Edit team",
"createNewTeam": "Create new team",
"teamName": "Team name",
"teamNamePlaceholder": "e.g. Men 1, Women 2",
"league": "League",
"team": "Team",
"teamId": "Team ID",
"groupId": "Group ID",
"association": "Association",
"groupName": "Group name",
"noLeague": "No league",
"change": "Change",
"createAndEdit": "Create & edit",
"clearFields": "Clear fields",
"playerStats": "Appearances",
"playerStatsIntro": "Quick overview of this team's appearances in the current season.",
"refreshStats": "Refresh",
"loadingStats": "Loading statistics...",
"noPlayerStats": "No appearances recorded.",
"player": "Player",
"qttr": "(Q)TTR rating",
"season": "Season",
"seasonFull": "Full season (from 1 July)",
"firstHalf": "First half",
"firstHalfFull": "First half (July - December)",
"secondHalf": "Second half",
"secondHalfFull": "Second half (from 1 January)",
"documents": "Documents",
"documentsIntro": "Upload, review and re-parse code lists and PIN lists when needed.",
"documentAvailable": "Available",
"documentMissing": "Missing",
"latestUpload": "Last uploaded: {date}",
"noDocumentUploadYet": "No document uploaded yet.",
"uploadNewVersion": "Upload new version",
"openDocument": "Open",
"parseDocument": "Parse again",
"codeList": "Code list",
"pinList": "PIN list",
"automaticJobs": "Automatic jobs",
"lastUpdated": "Last updated",
"status": "Status",
"successful": "Successful",
"noAutomaticUpdate": "No automatic update yet",
"myTischtennis": "myTischtennis",
"myTischtennisIntro": "Paste a URL, review the configuration and fetch data manually when needed.",
"configurationStatus": "Configuration status",
"manualFetch": "Fetch",
"noIssues": "No open configuration issues.",
"parseUrlAction": "Check URL",
"myTischtennisUrlPlaceholder": "myTischtennis URL...",
"teams": "Teams",
"activeTeam": "Active team",
"searchTeams": "Search team",
"openInWorkspace": "Open to edit",
"filterAll": "All",
"filterConfigured": "Configured",
"filterNeedsAttention": "Needs review",
"filterNoLeague": "Without league",
"seasonUnknown": "unknown",
"noTeamsYet": "No teams yet. Create your first team.",
"noMatchingTeams": "No teams match the current search or filter.",
"edit": "Edit",
"delete": "Delete",
"noAssignment": "No assignment",
"created": "Created",
"unknown": "Unknown",
"fullyConfigured": "Fully configured",
"partiallyConfigured": "Partially configured",
"notConfigured": "Not configured",
"never": "Never",
"showCodeList": "Show code list",
"showPinList": "Show PIN list",
"deleteTeamTitle": "Delete club team",
"deleteTeamConfirm": "Do you really want to delete the club team \"{name}\"?",
"errorDeletingTeam": "Failed to delete the club team.",
"teamHasNoLeague": "This team is not assigned to a league.",
"assignLeagueBeforeDocuments": "Please assign a league to the team first so documents can be processed.",
"assignLeagueBeforeParsing": "Please assign a league to the team first to parse PDF files.",
"documentParsedSummary": "{label} uploaded and parsed successfully.\n\nMatches found: {matchesFound}\nNew matches created: {created}\nMatches updated: {updated}",
"errorsCount": "Errors: {count}",
"moreErrors": "... and {count} more",
"noMatchesFoundTitle": "No matches found",
"noMatchesFoundDetails": "Note: No matches detected.\nLines in document: {lines}",
"documentUploaded": "{label} \"{fileName}\" was uploaded successfully.",
"errorUploadingDocument": "Failed to upload and parse the file.",
"matchesSummary": "Matches found: {matchesFound}\nNew matches created: {created}\nMatches updated: {updated}",
"errorParsingPdf": "Failed to parse the PDF file",
"documentNotFound": "The selected document could not be found.",
"missingLeagueForTeam": "No league was provided for the selected team.",
"pdfFileNotFound": "The PDF file could not be found.",
"reuploadFile": "Please upload the file again and try once more.",
"errorLoadingPdf": "Failed to load the PDF.",
"errorParsingUrl": "The URL could not be parsed. Please check the format.",
"configureLeagueTitle": "Configure league?",
"tableUrlDetected": "Table URL detected",
"configureLeagueDetails": "Association: {association}\nSeason: {season}\nLeague: {league}\nGroup ID: {groupId}\n\nDo you want to configure this league in the database? This enables automatic table data retrieval.",
"selectTeamTitle": "Select team",
"selectTeamFirst": "Please select a team first",
"selectTeamForConfiguration": "To activate the myTischtennis configuration, you must first select a team from the list.",
"teamConfiguredSuccess": "Team configured successfully. Automatic data retrieval is now active.",
"teamConfiguredDetails": "League: {league}\nSeason: {season}\nAutomatic data retrieval is now active.",
"errorConfiguringTeam": "The team could not be configured.",
"leagueConfiguredSuccess": "League configured successfully. Table data can now be retrieved automatically.",
"leagueConfiguredDetails": "League: {league}\nSeason: {season}\nAssociation: {association}\nGroup ID: {groupId}\n\nTable data can now be retrieved automatically.",
"errorConfiguringLeague": "The league could not be configured.",
"notCreated": "Not created",
"autoFetchEnabled": "Automatic data retrieval is enabled",
"missingItems": "Missing: {items}",
"enterUrlForAutoConfig": "Enter a myTischtennis URL for automatic configuration",
"errorLoadingStats": "Statistics could not be loaded.",
"asyncJobStartFailed": "The async job could not be started.",
"dataFetchFailed": "Data could not be fetched.",
"fetchTimedOut": "Timed out while fetching data (async job took too long).",
"teamDataFetched": "Team data fetched successfully.",
"unknownTeam": "Unknown team",
"teamDataFetchedDetails": "Team: {team}\nFetched records: {count}",
"tableUpdateLabel": "Table update:",
"mytischtennisLoginRequired": "Login to myTischtennis required",
"fetchTimeoutShort": "Timed out while fetching data.",
"invalidFileTypeTitle": "Invalid file type",
"invalidFileExtension": "{label} must have one of the following extensions: {extensions}.",
"invalidMimeType": "{label} has an unexpected MIME type: {type}.",
"fileTooLargeTitle": "File too large",
"fileTooLarge": "{label} must not exceed 10 MB."
}
}

View File

@@ -1,7 +1,7 @@
<template>
<div class="mytt-account-page">
<div class="mytt-account-page" :class="{ 'embedded-dialog-view': embedded }">
<div class="page-container">
<h1>HTTV / click-TT Account</h1>
<h1 v-if="!embedded">{{ $t('navigation.clickTtAccount') }}</h1>
<div class="account-container">
<div v-if="loading" class="loading">Lade Account</div>
@@ -86,6 +86,12 @@ import ConfirmDialog from '../components/ConfirmDialog.vue';
export default {
name: 'ClickTtAccount',
components: { ClickTtAccountDialog, InfoDialog, ConfirmDialog },
props: {
embedded: {
type: Boolean,
default: false
}
},
data() {
return {
loading: true,
@@ -196,8 +202,37 @@ export default {
.info-row { display: flex; gap: 1rem; margin-bottom: 1rem; }
.info-row label { min-width: 220px; font-weight: 600; }
.button-group { display: flex; gap: 1rem; margin-top: 1.5rem; }
.btn-primary, .btn-secondary, .btn-danger { padding: 0.75rem 1rem; border: none; border-radius: 6px; cursor: pointer; }
.btn-primary { background: #007bff; color: white; }
.btn-secondary { background: #6c757d; color: white; }
.btn-danger { background: #dc3545; color: white; }
.btn-primary, .btn-secondary, .btn-danger {
padding: 0.75rem 1rem;
border: 1px solid transparent;
border-radius: 10px;
cursor: pointer;
font-weight: 600;
transition: background-color 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: var(--text-on-primary);
}
.btn-primary:hover {
transform: translateY(-1px);
}
.btn-secondary {
background: var(--surface-color);
border-color: var(--border-color);
color: var(--text-color);
}
.btn-secondary:hover {
background: var(--surface-muted);
border-color: var(--primary-soft);
}
.btn-danger {
background: rgba(200, 74, 56, 0.12);
border-color: rgba(200, 74, 56, 0.24);
color: #8b3327;
}
.btn-danger:hover {
background: rgba(200, 74, 56, 0.18);
}
.embedded-dialog-view .page-container { max-width: none; padding: 0; }
</style>

View File

@@ -1,7 +1,7 @@
<template>
<div class="clicktt-view">
<div class="header">
<h1>HTTV / click-TT Seiten</h1>
<div class="clicktt-view" :class="{ 'embedded-dialog-view': embedded }">
<div v-if="!embedded" class="header">
<h1>{{ $t('navigation.clickTtBrowser') }}</h1>
<p class="subtitle">
Lade und bediene click-TT-Seiten (Ligenübersicht, Vereinsinfo, Spielplan) im iframe.
Alle Aufrufe werden geloggt.
@@ -98,6 +98,12 @@ const presetUrls = {
export default {
name: 'ClickTtView',
props: {
embedded: {
type: Boolean,
default: false
}
},
setup() {
const pageType = ref('leaguePage');
const association = ref('HeTTV');
@@ -175,6 +181,10 @@ export default {
max-width: 1400px;
margin: 0 auto;
}
.clicktt-view.embedded-dialog-view {
padding: 0;
max-width: none;
}
.header {
margin-bottom: 1.5rem;

View File

@@ -256,10 +256,6 @@
<span>{{ $t('diary.trainingWindow') }}</span>
<strong>{{ Boolean(trainingStart || trainingEnd) ? $t('diary.statusReadyShort') : $t('diary.statusOpenShort') }}</strong>
</div>
<div class="diary-readiness-item" :class="{ 'ready': participants.length > 0 }">
<span>{{ $t('diary.participants') }}</span>
<strong>{{ participants.length > 0 ? $t('diary.statusReadyShort') : $t('diary.statusOpenShort') }}</strong>
</div>
<div class="diary-readiness-item" :class="{ 'ready': trainingPlan.length > 0 }">
<span>{{ $t('diary.trainingPlan') }}</span>
<strong>{{ trainingPlan.length > 0 ? $t('diary.statusReadyShort') : $t('diary.statusOpenShort') }}</strong>
@@ -281,6 +277,23 @@
</strong>
<button type="button" class="btn-secondary" @click="cancelAddItem">{{ $t('common.cancel') }}</button>
</div>
<div v-if="addNewItem && standardDiaryActivities.length" class="plan-quick-actions">
<span class="plan-quick-actions-label">{{ $t('diary.standardActivities') }}</span>
<div class="plan-quick-actions-list">
<button
v-for="activity in standardDiaryActivities"
:key="activity.name"
type="button"
class="plan-quick-action"
@click="addStandardPlanActivity(activity)"
>
{{ activity.name }}
<span v-if="activity.duration" class="plan-quick-action-meta">
· {{ activity.duration }} {{ $t('diary.standardDurationShort') }}
</span>
</button>
</div>
</div>
<div class="plan-composer-grid">
<div v-if="addNewItem || addNewGroupActivity" class="plan-composer-field">
<label>{{ $t('diary.activityOrTimeblock') }}</label>
@@ -489,7 +502,7 @@
: (item.predefinedActivity ? item.predefinedActivity.name :
item.activity) }}
</span>
<span class="plan-status-badge" :class="`plan-status-badge-${getPlanItemStatus(item).tone}`" :title="getPlanItemStatus(item).reason || ''">
<span v-if="!isStructuralPlanItem(item)" class="plan-status-badge" :class="`plan-status-badge-${getPlanItemStatus(item).tone}`" :title="getPlanItemStatus(item).reason || ''">
{{ getPlanItemStatus(item).label }}
</span>
</div>
@@ -510,7 +523,7 @@
<td>
<div class="plan-row-actions">
<button v-if="!item.isTimeblock" @click="startActivityEdit(item)" class="plan-row-action-button">{{ $t('common.edit') }}</button>
<button v-if="!item.isTimeblock" @click="toggleActivityMembers(item)" :title="$t('diary.assignParticipants')"
<button v-if="!item.isTimeblock && !isStructuralPlanItem(item)" @click="toggleActivityMembers(item)" :title="$t('diary.assignParticipants')"
class="plan-row-action-button">{{ $t('diary.assignShort') }}</button>
<button v-if="item.isTimeblock" @click="addGroupActivityToTimeblock(item.id)" :title="$t('diary.addGroupActivity')"
class="plan-row-action-button plan-row-action-button-primary">{{ $t('diary.addGroupButton') }}</button>
@@ -570,7 +583,7 @@
? groupItem.groupPredefinedActivity.code
: groupItem.groupPredefinedActivity.name }}
</span>
<span class="plan-status-badge" :class="`plan-status-badge-${getPlanItemStatus(groupItem).tone}`" :title="getPlanItemStatus(groupItem).reason || ''">
<span v-if="!isStructuralPlanItem(groupItem)" class="plan-status-badge" :class="`plan-status-badge-${getPlanItemStatus(groupItem).tone}`" :title="getPlanItemStatus(groupItem).reason || ''">
{{ getPlanItemStatus(groupItem).label }}
</span>
</div>
@@ -580,7 +593,7 @@
<td>
<div class="plan-row-actions">
<button @click="startGroupActivityEdit(groupItem)" class="plan-row-action-button">{{ $t('common.edit') }}</button>
<button @click="toggleGroupActivityMembers(groupItem)" :title="$t('diary.assignParticipants')"
<button v-if="!isStructuralPlanItem(groupItem)" @click="toggleGroupActivityMembers(groupItem)" :title="$t('diary.assignParticipants')"
class="plan-row-action-button">{{ $t('diary.assignShort') }}</button>
<button @click="removeGroupActivity(groupItem.id)" class="plan-row-action-button plan-row-action-button-danger" :title="$t('diary.delete')">{{ $t('common.delete') }}</button>
</div>
@@ -807,6 +820,20 @@ import {
offGroupChanged
} from '../services/socketService.js';
const STANDARD_DIARY_ACTIVITY_NAMES = [
'Begrüßung',
'Aktivierung',
'Aufbauen',
'Turnier',
'Abbauen',
'Abschlussgespräch',
];
const normalizeStandardActivityFlag = (activity) => ({
...(activity || {}),
excludeFromStats: Boolean(activity?.excludeFromStats ?? activity?.exclude_from_stats),
});
export default {
name: 'DiaryView',
components: {
@@ -881,7 +908,9 @@ export default {
predefinedActivityId: null,
isTimeBlock: false,
},
standardDiaryActivityDefinitions: STANDARD_DIARY_ACTIVITY_NAMES.map((name) => ({ name })),
predefinedActivities: [],
standardPredefinedActivities: [],
showDropdown: false,
showImage: false,
imageUrl: '',
@@ -1041,17 +1070,41 @@ export default {
openPlanItems() {
const result = [];
for (const item of this.trainingPlan || []) {
if (!item?.isTimeblock && this.getPlanItemStatus(item).key === 'open') {
if (!item?.isTimeblock && !this.isStructuralPlanItem(item) && this.getPlanItemStatus(item).key === 'open') {
result.push(item);
}
for (const groupItem of item.groupActivities || []) {
if (this.getPlanItemStatus(groupItem).key === 'open') {
if (!this.isStructuralPlanItem(groupItem) && this.getPlanItemStatus(groupItem).key === 'open') {
result.push(groupItem);
}
}
}
return result;
},
standardDiaryActivities() {
if (this.standardPredefinedActivities.length > 0) {
return [...this.standardPredefinedActivities].sort((a, b) => {
const aIndex = STANDARD_DIARY_ACTIVITY_NAMES.indexOf(a.name);
const bIndex = STANDARD_DIARY_ACTIVITY_NAMES.indexOf(b.name);
if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;
if (aIndex !== -1) return -1;
if (bIndex !== -1) return 1;
return a.name.localeCompare(b.name, 'de-DE');
});
}
return this.standardDiaryActivityDefinitions.map((definition) => {
const existing = this.predefinedActivities.find((activity) => activity.name === definition.name);
return existing || definition;
}).sort((a, b) => {
const aIndex = STANDARD_DIARY_ACTIVITY_NAMES.indexOf(a.name);
const bIndex = STANDARD_DIARY_ACTIVITY_NAMES.indexOf(b.name);
if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;
if (aIndex !== -1) return -1;
if (bIndex !== -1) return 1;
return a.name.localeCompare(b.name, 'de-DE');
});
},
filteredDiaryMembers() {
const search = (this.participantSearchQuery || '').trim().toLowerCase();
@@ -1120,15 +1173,14 @@ export default {
}
const hasTimes = Boolean(this.trainingStart || this.trainingEnd);
const hasParticipants = this.participants.length > 0;
const hasPlan = this.trainingPlan.length > 0;
const hasActivities = this.activities.length > 0;
if (hasTimes && hasParticipants && hasPlan && this.openPlanItems.length === 0) {
if (hasTimes && hasPlan && this.openPlanItems.length === 0) {
return this.$t('diary.statusReady');
}
if (!hasParticipants && !hasPlan && !hasActivities && !hasTimes) {
if (!hasPlan && !hasActivities && !hasTimes) {
return this.$t('diary.statusEmpty');
}
@@ -1138,9 +1190,8 @@ export default {
methods: {
isDiaryDayConfigured() {
const hasTimes = Boolean(this.trainingStart || this.trainingEnd);
const hasParticipants = Array.isArray(this.participants) && this.participants.length > 0;
const hasPlan = Array.isArray(this.trainingPlan) && this.trainingPlan.length > 0;
return hasTimes && hasParticipants && hasPlan;
return hasTimes && hasPlan;
},
toggleOverviewPanel(panel) {
this.activeOverviewPanel = this.activeOverviewPanel === panel ? null : panel;
@@ -1619,12 +1670,23 @@ export default {
async loadPredefinedActivities() {
try {
const response = await apiClient.get('/predefined-activities');
this.predefinedActivities = response.data;
this.predefinedActivities = (response.data || []).map(normalizeStandardActivityFlag);
} catch (error) {
this.showInfo(this.$t('messages.error'), this.$t('diary.errorLoadingPredefinedActivities'), '', 'error');
}
},
async loadStandardPredefinedActivities() {
try {
const response = await apiClient.get('/predefined-activities', {
params: { scope: 'standard' }
});
this.standardPredefinedActivities = (response.data || []).map(normalizeStandardActivityFlag);
} catch (error) {
this.standardPredefinedActivities = [];
}
},
async loadGroups() {
try {
const response = await apiClient.get(`/group/${this.currentClub}/${this.date.id}`);
@@ -1908,9 +1970,60 @@ export default {
this.newPlanItem.activity = activity.name;
this.newPlanItem.durationText = activity.durationText;
this.newPlanItem.duration = activity.duration || '';
this.newPlanItem.predefinedActivityId = activity.id || null;
this.showDropdown = false;
},
async ensureStandardDiaryActivity(definition) {
const existing = this.predefinedActivities.find((activity) => activity.name === definition.name);
if (existing) {
if (existing.excludeFromStats) {
return existing;
}
await apiClient.put(`/predefined-activities/${existing.id}`, {
name: existing.name,
code: existing.code,
description: existing.description,
durationText: existing.durationText,
duration: existing.duration,
imageLink: existing.imageLink,
drawingData: existing.drawingData,
excludeFromStats: true,
});
await this.loadPredefinedActivities();
await this.loadStandardPredefinedActivities();
return this.predefinedActivities.find((activity) => activity.name === definition.name) || existing;
}
const created = await apiClient.post('/predefined-activities', {
name: definition.name,
code: definition.code || null,
description: definition.description || '',
durationText: definition.durationText || '',
duration: definition.duration || null,
excludeFromStats: true,
}).then((response) => response.data);
await this.loadPredefinedActivities();
await this.loadStandardPredefinedActivities();
return created;
},
async addStandardPlanActivity(activity) {
try {
const standardActivity = await this.ensureStandardDiaryActivity(activity);
this.newPlanItem.activity = standardActivity.name;
this.newPlanItem.durationText = standardActivity.durationText || (standardActivity.duration ? String(standardActivity.duration) : '');
this.newPlanItem.duration = standardActivity.duration || '';
this.newPlanItem.predefinedActivityId = standardActivity.id || null;
await this.addPlanItem();
} catch (error) {
const msg = getSafeErrorMessage(error, this.$t('diary.standardActivityAddError'));
this.showInfo(this.$t('messages.error'), msg, '', 'error');
}
},
async addPlanItem() {
try {
if (this.addNewItem || this.addNewTimeblock) {
@@ -1924,6 +2037,7 @@ export default {
await apiClient.post(`/diary-date-activities/${this.currentClub}`, {
diaryDateId: this.date.id,
activity: this.addNewTimeblock ? '' : this.newPlanItem.activity,
predefinedActivityId: this.newPlanItem.predefinedActivityId,
isTimeblock: this.addNewTimeblock,
duration: this.newPlanItem.duration,
durationText: this.newPlanItem.durationText,
@@ -3013,11 +3127,19 @@ export default {
const map = this.participantMapByMemberId || {};
return map[memberId];
},
isStructuralPlanItem(item) {
const activity = item?.groupPredefinedActivity || item?.predefinedActivity || null;
return Boolean(activity?.excludeFromStats);
},
getPlanItemStatus(item) {
if (!item) {
return { key: 'open', label: this.$t('diary.statusOpenShort'), tone: 'open' };
}
if (this.isStructuralPlanItem(item)) {
return { key: 'ready', label: '', tone: 'ready' };
}
if (item.isTimeblock) {
const groupActivities = item.groupActivities || [];
if (groupActivities.length === 0) {
@@ -3584,6 +3706,7 @@ export default {
},
async mounted() {
await this.init();
await this.loadStandardPredefinedActivities();
// Socket.IO verbinden
if (this.currentClub) {
@@ -4271,9 +4394,10 @@ img {
}
.modal {
background: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
background: var(--surface-color);
border: 1px solid var(--border-color);
border-radius: 14px;
box-shadow: 0 18px 48px rgba(15, 23, 42, 0.18);
max-width: 500px;
width: 90%;
max-height: 90vh;
@@ -4284,12 +4408,13 @@ img {
.modal-header {
padding: 1rem 1.5rem;
border-bottom: 1px solid #dee2e6;
border-bottom: 1px solid var(--border-color);
background: linear-gradient(180deg, rgba(47, 122, 95, 0.12), rgba(47, 122, 95, 0.04));
}
.modal-header h3 {
margin: 0;
color: #495057;
color: var(--text-color);
}
.modal-body {
@@ -4318,7 +4443,7 @@ img {
gap: 0.5rem;
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid #dee2e6;
border-top: 1px solid var(--border-color);
flex-wrap: nowrap;
}
@@ -4330,54 +4455,57 @@ img {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #495057;
color: var(--text-color);
}
.form-group input,
.form-group select {
width: 100%;
padding: 0.5rem;
border: 1px solid #ced4da;
border-radius: 4px;
padding: 0.75rem 0.875rem;
border: 1px solid var(--border-color);
border-radius: 10px;
font-size: 1rem;
background: var(--surface-muted);
color: var(--text-color);
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: #80bdff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(47, 122, 95, 0.14);
}
.modal .btn-secondary {
background-color: #6c757d !important;
color: white !important;
border: none !important;
background: var(--surface-color) !important;
color: var(--text-color) !important;
border: 1px solid var(--border-color) !important;
padding: 0.5rem 1rem !important;
border-radius: 4px !important;
border-radius: 10px !important;
cursor: pointer !important;
font-size: 0.9rem !important;
}
.modal .btn-secondary:hover {
background-color: #5a6268 !important;
background: var(--surface-muted) !important;
border-color: var(--primary-soft) !important;
}
.modal .btn-primary {
background-color: #007bff !important;
color: white !important;
border: none !important;
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover)) !important;
color: var(--text-on-primary) !important;
border: 1px solid transparent !important;
padding: 0.5rem 1rem !important;
border-radius: 4px !important;
border-radius: 10px !important;
cursor: pointer !important;
font-size: 0.9rem !important;
}
.btn-palette {
background: linear-gradient(135deg, #4CAF50, #45a049);
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;
padding: 0.25rem;
font-size: 1rem;
cursor: pointer;
@@ -4392,12 +4520,12 @@ img {
}
.btn-palette:hover {
background: linear-gradient(135deg, #45a049, #3d8b40);
opacity: 0.95;
transform: translateY(-1px);
}
.modal .btn-primary:hover {
background-color: #0056b3 !important;
opacity: 0.95 !important;
}
.modal .btn-primary:disabled {
@@ -4725,6 +4853,37 @@ img {
margin-bottom: 0.85rem;
}
.plan-quick-actions {
margin-bottom: 0.9rem;
padding: 0.85rem;
border: 1px solid var(--border-color, #d9e4ec);
border-radius: 10px;
background: var(--surface-subtle, #f3f7fa);
}
.plan-quick-actions-label {
display: block;
margin-bottom: 0.65rem;
font-size: 0.82rem;
font-weight: 700;
color: var(--text-muted, #4d6979);
}
.plan-quick-actions-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 0.65rem;
}
.plan-quick-action {
justify-content: flex-start;
}
.plan-quick-action-meta {
color: var(--text-muted, #4d6979);
font-weight: 500;
}
.plan-composer-grid {
display: grid;
grid-template-columns: 1.8fr 1fr 1.2fr;
@@ -5276,12 +5435,12 @@ img {
}
.dialog-actions .btn-primary {
background: #28a745;
color: #fff;
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: var(--text-on-primary);
}
.dialog-actions .btn-primary:hover:not(:disabled) {
background: #218838;
opacity: 0.95;
}
.dialog-actions .btn-primary:disabled {

View File

@@ -651,78 +651,94 @@ export default {
}
.log-type-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
border-radius: 4px;
display: inline-flex;
align-items: center;
padding: 0.25rem 0.55rem;
border-radius: 999px;
font-size: 0.8em;
font-weight: 500;
font-weight: 600;
border: 1px solid transparent;
}
.log-type-api_request {
background: #e0f2fe;
color: #0369a1;
background: rgba(47, 122, 95, 0.1);
color: var(--primary-strong);
border-color: rgba(47, 122, 95, 0.18);
}
.log-type-scheduler {
background: #fef3c7;
color: #92400e;
background: rgba(181, 110, 65, 0.14);
color: #8a4f28;
border-color: rgba(181, 110, 65, 0.24);
}
.log-type-cron_job {
background: #fce7f3;
color: #9f1239;
background: rgba(87, 92, 144, 0.12);
color: #454b79;
border-color: rgba(87, 92, 144, 0.22);
}
.method-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
border-radius: 4px;
display: inline-flex;
align-items: center;
padding: 0.25rem 0.55rem;
border-radius: 999px;
font-size: 0.8em;
font-weight: 600;
font-family: monospace;
border: 1px solid transparent;
}
.method-GET {
background: #dbeafe;
color: #1e40af;
background: rgba(67, 97, 168, 0.12);
color: #28457e;
border-color: rgba(67, 97, 168, 0.24);
}
.method-POST {
background: #dcfce7;
color: #166534;
background: rgba(47, 122, 95, 0.12);
color: #1f5f49;
border-color: rgba(47, 122, 95, 0.24);
}
.method-PUT {
background: #fef3c7;
color: #92400e;
background: rgba(181, 110, 65, 0.14);
color: #8a4f28;
border-color: rgba(181, 110, 65, 0.24);
}
.method-DELETE {
background: #fee2e2;
color: #991b1b;
background: rgba(200, 74, 56, 0.12);
color: #8b3327;
border-color: rgba(200, 74, 56, 0.24);
}
.status-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
border-radius: 4px;
display: inline-flex;
align-items: center;
padding: 0.25rem 0.55rem;
border-radius: 999px;
font-size: 0.8em;
font-weight: 600;
border: 1px solid transparent;
}
.status-success {
background: #dcfce7;
color: #166534;
background: rgba(47, 122, 95, 0.12);
color: #1f5f49;
border-color: rgba(47, 122, 95, 0.24);
}
.status-client-error {
background: #fef3c7;
color: #92400e;
background: rgba(181, 110, 65, 0.14);
color: #8a4f28;
border-color: rgba(181, 110, 65, 0.24);
}
.status-server-error {
background: #fee2e2;
color: #991b1b;
background: rgba(200, 74, 56, 0.12);
color: #8b3327;
border-color: rgba(200, 74, 56, 0.24);
}
.path-cell {
@@ -782,4 +798,3 @@ export default {
font-weight: 500;
}
</style>

View File

@@ -1321,18 +1321,19 @@ address={{address}}`;
.btn-import {
align-self: flex-start;
padding: 0.6rem 1.2rem;
background-color: #28a745;
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: 10px;
cursor: pointer;
font-weight: 500;
transition: background-color 0.2s;
font-weight: 600;
transition: transform 0.2s ease, opacity 0.2s ease;
font-size: 0.95em;
}
.btn-import:hover:not(:disabled) {
background-color: #218838;
opacity: 0.95;
transform: translateY(-1px);
}
.btn-import:disabled {
@@ -1350,39 +1351,38 @@ address={{address}}`;
}
.btn-primary {
background-color: #007bff;
color: white;
border: none;
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: var(--text-on-primary);
border: 1px solid transparent;
padding: 0.75rem 1.5rem;
border-radius: 4px;
border-radius: 10px;
cursor: pointer;
font-weight: 500;
transition: background-color 0.2s;
font-weight: 600;
transition: background-color 0.2s ease, transform 0.2s ease;
}
.btn-primary:hover:not(:disabled) {
background-color: #0056b3;
transform: translateY(-1px);
}
.btn-primary:disabled {
background-color: #6c757d;
cursor: not-allowed;
opacity: 0.6;
}
.btn-danger {
background-color: #dc3545;
color: white;
border: none;
background: rgba(200, 74, 56, 0.12);
color: #8b3327;
border: 1px solid rgba(200, 74, 56, 0.24);
padding: 0.75rem 1.5rem;
border-radius: 4px;
border-radius: 10px;
cursor: pointer;
font-weight: 500;
transition: background-color 0.2s;
font-weight: 600;
transition: background-color 0.2s ease;
}
.btn-danger:hover:not(:disabled) {
background-color: #c82333;
background: rgba(200, 74, 56, 0.18);
}
.btn-danger:disabled {
@@ -1391,4 +1391,3 @@ address={{address}}`;
opacity: 0.6;
}
</style>

View File

@@ -2528,18 +2528,20 @@ table td {
}
.btn-update-ratings {
background-color: #28a745;
color: white;
border: none;
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: var(--text-on-primary);
border: 1px solid transparent;
padding: 0.5rem 1rem;
border-radius: 4px;
border-radius: 10px;
cursor: pointer;
font-size: 0.9em;
transition: background-color 0.2s ease;
font-weight: 600;
transition: transform 0.2s ease, opacity 0.2s ease;
}
.btn-update-ratings:hover:not(:disabled) {
background-color: #218838;
transform: translateY(-1px);
opacity: 0.95;
}
.btn-update-ratings:disabled {
@@ -2579,22 +2581,21 @@ table td {
}
.rotate-btn {
background-color: #007bff;
color: white;
border: none;
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: var(--text-on-primary);
border: 1px solid transparent;
padding: 0.75rem 1rem;
border-radius: 6px;
border-radius: 10px;
cursor: pointer;
font-size: 0.9em;
font-weight: 500;
font-weight: 600;
transition: all 0.2s ease;
min-width: 120px;
}
.rotate-btn:hover {
background-color: #0056b3;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 123, 255, 0.3);
box-shadow: 0 8px 18px rgba(47, 122, 95, 0.18);
}
.rotate-btn:active {
@@ -2608,17 +2609,18 @@ table td {
.btn-primary {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
border: 1px solid transparent;
border-radius: 10px;
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: white;
color: var(--text-on-primary);
font-weight: 600;
cursor: pointer;
transition: opacity 0.2s;
transition: opacity 0.2s, transform 0.2s ease;
}
.btn-primary:hover {
opacity: 0.9;
opacity: 0.95;
transform: translateY(-1px);
}
/* Dropdown Styles */
@@ -2628,28 +2630,30 @@ table td {
}
.btn-dropdown {
background-color: #007bff;
color: white;
border: none;
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: var(--text-on-primary);
border: 1px solid transparent;
padding: 0.5rem 1rem;
border-radius: 4px;
border-radius: 10px;
cursor: pointer;
font-size: 0.9em;
transition: background-color 0.2s ease;
font-weight: 600;
transition: transform 0.2s ease, opacity 0.2s ease;
}
.btn-dropdown:hover {
background-color: #0056b3;
opacity: 0.95;
transform: translateY(-1px);
}
.dropdown-content {
position: absolute;
top: 100%;
left: 0;
background: white;
border: 1px solid #ddd;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
background: var(--surface-color);
border: 1px solid var(--border-color);
border-radius: 10px;
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.14);
z-index: 1000;
min-width: 200px;
margin-top: 4px;
@@ -2688,18 +2692,20 @@ table td {
/* Button Styles */
.btn-activities {
margin-left: 0.5rem;
background-color: #28a745;
color: white;
border: none;
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: var(--text-on-primary);
border: 1px solid transparent;
padding: 0.5rem 1rem;
border-radius: 4px;
border-radius: 10px;
cursor: pointer;
font-size: 0.9em;
transition: background-color 0.2s ease;
font-weight: 600;
transition: transform 0.2s ease, opacity 0.2s ease;
}
.btn-activities:hover {
background-color: #218838;
transform: translateY(-1px);
opacity: 0.95;
}
/* Filter Styles */
@@ -3241,30 +3247,31 @@ table td {
.btn-add-contact {
margin-top: 0.5rem;
padding: 0.4rem 0.8rem;
background-color: #28a745;
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: 10px;
cursor: pointer;
font-size: 0.9em;
font-weight: 600;
}
.btn-add-contact:hover {
background-color: #218838;
opacity: 0.95;
}
.btn-remove-contact {
padding: 0.2rem 0.5rem;
background-color: #dc3545;
color: white;
border: none;
border-radius: 4px;
background: rgba(200, 74, 56, 0.12);
color: #8b3327;
border: 1px solid rgba(200, 74, 56, 0.24);
border-radius: 8px;
cursor: pointer;
font-size: 0.9em;
}
.btn-remove-contact:hover {
background-color: #c82333;
background: rgba(200, 74, 56, 0.18);
}
.member-groups-list {

View File

@@ -1,7 +1,7 @@
<template>
<div class="mytt-account-page">
<div class="mytt-account-page" :class="{ 'embedded-dialog-view': embedded }">
<div class="page-container">
<h1>{{ $t('myTischtennisAccount.title') }}</h1>
<h1 v-if="!embedded">{{ $t('myTischtennisAccount.title') }}</h1>
<div class="account-container">
<div v-if="loading" class="loading">{{ $t('myTischtennisAccount.loading') }}</div>
@@ -107,6 +107,12 @@ import InfoDialog from '../components/InfoDialog.vue';
import ConfirmDialog from '../components/ConfirmDialog.vue';
export default {
name: 'MyTischtennisAccount',
props: {
embedded: {
type: Boolean,
default: false
}
},
components: {
MyTischtennisDialog,
InfoDialog,
@@ -428,29 +434,32 @@ h1 {
.btn-primary, .btn-secondary, .btn-danger {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
border: 1px solid transparent;
border-radius: 10px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.2s ease;
font-weight: 600;
transition: background-color 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
}
.btn-primary {
background-color: #007bff;
color: white;
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: var(--text-on-primary);
}
.btn-primary:hover {
background-color: #0056b3;
transform: translateY(-1px);
}
.btn-secondary {
background-color: #6c757d;
color: white;
background: var(--surface-color);
border-color: var(--border-color);
color: var(--text-color);
}
.btn-secondary:hover {
background-color: #545b62;
background: var(--surface-muted);
border-color: var(--primary-soft);
}
.btn-secondary:disabled {
@@ -555,21 +564,22 @@ h1 {
}
.btn-danger {
background-color: #dc3545;
color: white;
background: rgba(200, 74, 56, 0.12);
color: #8b3327;
border: 1px solid rgba(200, 74, 56, 0.24);
}
.btn-danger:hover {
background-color: #c82333;
background: rgba(200, 74, 56, 0.18);
}
.btn-info {
background-color: #17a2b8;
color: white;
background: rgba(47, 122, 95, 0.12);
color: var(--primary-strong);
border: 1px solid rgba(47, 122, 95, 0.24);
}
.btn-info:hover {
background-color: #138496;
background: rgba(47, 122, 95, 0.18);
}
</style>

View File

@@ -2063,21 +2063,21 @@ th, td { border-bottom: 1px solid var(--border-color); padding: 0.5rem; text-ali
}
.btn-register {
background-color: #007bff;
color: white;
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: var(--text-on-primary);
}
.btn-register:hover {
background-color: #0056b3;
opacity: 0.95;
}
.btn-participate {
background-color: #28a745;
color: white;
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: var(--text-on-primary);
}
.btn-participate:hover {
background-color: #1e7e34;
opacity: 0.95;
}
.btn-reset {

View File

@@ -1,6 +1,6 @@
<template>
<div class="page-container">
<h1>{{ $t('settings.personalSettings') }}</h1>
<div class="page-container" :class="{ 'embedded-dialog-view': embedded }">
<h1 v-if="!embedded">{{ $t('settings.personalSettings') }}</h1>
<div class="settings-container">
<div class="settings-section card">
@@ -34,6 +34,12 @@ import { mapGetters, mapActions } from 'vuex';
export default {
name: 'PersonalSettings',
props: {
embedded: {
type: Boolean,
default: false
}
},
data() {
return {
selectedLanguage: this.$i18n.locale,
@@ -77,6 +83,10 @@ export default {
margin: 0 auto;
padding: 2rem;
}
.page-container.embedded-dialog-view {
max-width: none;
padding: 0;
}
h1 {
color: var(--text-color, #333);
@@ -157,4 +167,3 @@ h1 {
}
}
</style>

View File

@@ -17,6 +17,32 @@
/>
<button v-if="searchQuery" @click="clearSearch" class="btn-clear-search"></button>
</div>
<div class="scope-filters">
<button
type="button"
class="scope-filter"
:class="{ active: selectedScope === 'all' }"
@click="selectedScope = 'all'"
>
{{ $t('predefinedActivities.filterAll') }}
</button>
<button
type="button"
class="scope-filter"
:class="{ active: selectedScope === 'standard' }"
@click="selectedScope = 'standard'"
>
{{ $t('predefinedActivities.filterStandard') }}
</button>
<button
type="button"
class="scope-filter"
:class="{ active: selectedScope === 'custom' }"
@click="selectedScope = 'custom'"
>
{{ $t('predefinedActivities.filterCustom') }}
</button>
</div>
<div>
<button @click="deduplicate" class="btn-secondary">{{ $t('predefinedActivities.deduplicate') }}</button>
</div>
@@ -40,6 +66,7 @@
<div class="meta">
<span v-if="a.duration">{{ a.duration }} {{ $t('predefinedActivities.min') }}</span>
<span v-if="a.durationText"> ({{ a.durationText }})</span>
<span v-if="a.excludeFromStats" class="meta-chip">{{ $t('predefinedActivities.excludeFromStats') }}</span>
</div>
</li>
</ul>
@@ -65,6 +92,10 @@
<label>{{ $t('predefinedActivities.durationText') }}
<input type="text" v-model="editModel.durationText" :placeholder="$t('predefinedActivities.durationTextPlaceholder')" />
</label>
<label class="checkbox-field">
<input type="checkbox" v-model="editModel.excludeFromStats" />
<span>{{ $t('predefinedActivities.excludeFromStats') }}</span>
</label>
<label>{{ $t('predefinedActivities.description') }}
<textarea v-model="editModel.description" rows="4" />
</label>
@@ -146,6 +177,21 @@ import ConfirmDialog from '../components/ConfirmDialog.vue';
import { mapGetters } from 'vuex';
import { debounce } from '../utils/debounce.js';
import { buildInfoConfig, buildConfirmConfig, safeErrorMessage } from '../utils/dialogUtils.js';
const STANDARD_ACTIVITY_NAMES = new Set([
'Begrüßung',
'Aktivierung',
'Aufbauen',
'Turnier',
'Abbauen',
'Abschlussgespräch',
]);
const normalizeStandardActivityFlag = (activity) => ({
...(activity || {}),
excludeFromStats: Boolean(activity?.excludeFromStats ?? activity?.exclude_from_stats) || STANDARD_ACTIVITY_NAMES.has(activity?.name),
});
export default {
name: 'PredefinedActivities',
components: {
@@ -182,14 +228,25 @@ export default {
searchResults: [],
isSearching: false,
showDrawingDialog: false,
selectedScope: 'all',
};
},
computed: {
isStandardActivity() {
return (activity) => Boolean(activity?.excludeFromStats) || STANDARD_ACTIVITY_NAMES.has(activity?.name);
},
filteredActivities() {
const activitiesToFilter = this.searchQuery.trim() ? this.searchResults : this.activities;
if (this.selectedScope === 'standard') {
return (activitiesToFilter || []).filter((activity) => this.isStandardActivity(activity));
}
if (this.selectedScope === 'custom') {
return (activitiesToFilter || []).filter((activity) => !this.isStandardActivity(activity));
}
return activitiesToFilter || [];
},
sortedActivities() {
// Wenn gesucht wird, zeige Suchergebnisse, sonst alle Aktivitäten
const activitiesToSort = this.searchQuery.trim() ? this.searchResults : this.activities;
return [...(activitiesToSort || [])].sort((a, b) => {
return [...this.filteredActivities].sort((a, b) => {
const ac = (a.code || '').toLocaleLowerCase('de-DE');
const bc = (b.code || '').toLocaleLowerCase('de-DE');
const aEmpty = ac === '';
@@ -247,7 +304,7 @@ export default {
const response = await apiClient.get('/predefined-activities/search/query', {
params: { q: query, limit: 50 }
});
this.searchResults = response.data || [];
this.searchResults = (response.data || []).map((activity) => normalizeStandardActivityFlag(activity));
} catch (error) {
console.error('Error searching activities:', error);
this.searchResults = [];
@@ -268,11 +325,11 @@ export default {
},
normalizeActivity(activity, images) {
const drawingData = this.parseDrawingData(activity && activity.drawingData);
return { ...(activity || {}), drawingData };
return { ...normalizeStandardActivityFlag(activity), drawingData };
},
async reload() {
const r = await apiClient.get('/predefined-activities');
this.activities = r.data || [];
this.activities = (r.data || []).map((activity) => normalizeStandardActivityFlag(activity));
},
async select(a) {
this.selectedActivity = a;
@@ -324,6 +381,7 @@ export default {
description: '',
duration: null,
durationText: '',
excludeFromStats: false,
imageLink: '',
drawingData: '',
};
@@ -545,6 +603,17 @@ select { max-width: 220px; }
.items li:hover { background: var(--primary-light);
}
.items li.active { background: var(--primary-light); color: var(--primary-color); }
.meta-chip {
display: inline-flex;
align-items: center;
margin-left: 0.4rem;
padding: 0.1rem 0.4rem;
border-radius: 999px;
background: rgba(41, 110, 78, 0.12);
color: var(--primary-strong, #296e4e);
font-size: 0.72rem;
font-weight: 700;
}
.detail {
background: white;
border: 1px solid var(--border-color);
@@ -557,6 +626,14 @@ select { max-width: 220px; }
overflow: auto;
}
label { display: block; margin-bottom: 0.5rem; }
.checkbox-field {
display: flex;
align-items: center;
gap: 0.5rem;
}
.checkbox-field input[type="checkbox"] {
width: auto;
}
input[type="text"], input[type="number"], textarea { width: 100%; }
.actions { margin-top: 0.75rem; display: flex; gap: 0.5rem; }
.image-section {
@@ -624,14 +701,14 @@ input[type="text"], input[type="number"], textarea { width: 100%; }
}
.btn-danger {
background: #dc3545;
color: white;
border: none;
background: rgba(200, 74, 56, 0.12);
color: #8b3327;
border: 1px solid rgba(200, 74, 56, 0.24);
border-radius: var(--border-radius-small);
}
.btn-danger:hover {
background: #c82333;
background: rgba(200, 74, 56, 0.18);
}
.drawing-button-section {
@@ -644,6 +721,28 @@ input[type="text"], input[type="number"], textarea { width: 100%; }
margin: 1rem 0;
}
.scope-filters {
display: flex;
flex-wrap: wrap;
gap: 0.45rem;
margin: 0 0 1rem;
}
.scope-filter {
border: 1px solid var(--border-color);
background: var(--surface-subtle, #f3f7fa);
color: var(--text-color);
border-radius: 999px;
padding: 0.35rem 0.75rem;
}
.scope-filter.active {
background: rgba(41, 110, 78, 0.12);
border-color: rgba(41, 110, 78, 0.28);
color: var(--primary-strong, #296e4e);
font-weight: 700;
}
.search-input {
width: 100%;
padding: 0.75rem 2.5rem 0.75rem 1rem;
@@ -685,4 +784,3 @@ input[type="text"], input[type="number"], textarea { width: 100%; }
color: #333;
}
</style>

View File

@@ -1,49 +1,137 @@
<template>
<div>
<h2>{{ $t('schedule.title') }}</h2>
<SeasonSelector v-model="selectedSeasonId" @season-change="onSeasonChange" :show-current-season="true" />
<button @click="openImportModal">{{ $t('schedule.importSchedule') }}</button>
<button
v-if="playerSelectionDialog.match"
@click="openGalleryDialog"
class="btn-secondary"
:disabled="galleryLoading"
>
{{ galleryLoading ? $t('schedule.galleryLoading') : $t('schedule.gallery') }}
</button>
<div class="schedule-view">
<div class="schedule-page-header">
<div class="schedule-page-title">
<h2>{{ $t('schedule.title') }}</h2>
<p>{{ $t('schedule.subtitle') }}</p>
</div>
<div class="schedule-page-actions">
<SeasonSelector v-model="selectedSeasonId" @season-change="onSeasonChange" :show-current-season="true" />
<button @click="openImportModal">{{ $t('schedule.importSchedule') }}</button>
<button
v-if="playerSelectionDialog.match"
@click="openGalleryDialog"
class="btn-secondary"
:disabled="galleryLoading"
>
{{ galleryLoading ? $t('schedule.galleryLoading') : $t('schedule.gallery') }}
</button>
</div>
</div>
<div v-if="hoveredMatch && hoveredMatch.location" class="hover-info">
<p><strong>{{ hoveredMatch.location.name || 'N/A' }}</strong></p>
<p>{{ hoveredMatch.location.address || 'N/A' }}</p>
<p>{{ hoveredMatch.location.zip || '' }} {{ hoveredMatch.location.city || 'N/A' }}</p>
</div>
<div class="output">
<ul>
<li class="special-link" @click="loadAllMatches">{{ $t('schedule.overallSchedule') }}</li>
<li class="special-link" @click="loadAdultMatches">{{ $t('schedule.adultSchedule') }}</li>
<li class="divider"></li>
<li v-for="team in teams" :key="team.id" @click="loadMatchesForTeam(team)"
:class="{ active: selectedTeam && selectedTeam.id === team.id }">
{{ team.name }}<span class="team-league" v-if="team.league && team.league.name"> ({{ team.league.name }})</span>
</li>
<li v-if="teams.length === 0" class="no-leagues">{{ $t('schedule.noTeamsFound') }}</li>
</ul>
<div class="schedule-summary-bar">
<div class="schedule-summary-item">
<span class="schedule-summary-label">{{ $t('schedule.teams') }}</span>
<strong>{{ filteredScheduleTeams.length }}/{{ teams.length }}</strong>
</div>
<div class="schedule-summary-item">
<span class="schedule-summary-label">{{ $t('schedule.matches') }}</span>
<strong>{{ matches.length }}</strong>
</div>
<div class="schedule-summary-item">
<span class="schedule-summary-label">{{ $t('schedule.completedShort') }}</span>
<strong>{{ completedMatchesCount }}</strong>
</div>
<div class="schedule-summary-item">
<span class="schedule-summary-label">{{ $t('schedule.pendingShort') }}</span>
<strong>{{ pendingMatchesCount }}</strong>
</div>
<div class="schedule-summary-item" v-if="nextScheduledMatchLabel">
<span class="schedule-summary-label">{{ $t('schedule.nextMatch') }}</span>
<strong>{{ nextScheduledMatchLabel }}</strong>
</div>
</div>
<div class="output schedule-layout">
<aside class="schedule-sidebar">
<div class="schedule-sidebar-card">
<div class="schedule-sidebar-header">
<h3>{{ $t('schedule.selection') }}</h3>
<span class="schedule-selection-count">{{ filteredScheduleTeams.length }}/{{ teams.length }}</span>
</div>
<input
v-model.trim="teamSearchQuery"
type="search"
class="schedule-team-search"
:placeholder="$t('schedule.searchTeams')"
/>
<div class="schedule-quick-links">
<button
type="button"
class="schedule-quick-link"
:class="{ active: selectedLeague === $t('schedule.overallSchedule') }"
@click="loadAllMatches"
>
{{ $t('schedule.overallSchedule') }}
</button>
<button
type="button"
class="schedule-quick-link"
:class="{ active: selectedLeague === $t('schedule.adultSchedule') }"
@click="loadAdultMatches"
>
{{ $t('schedule.adultSchedule') }}
</button>
</div>
<ul class="schedule-team-list">
<li v-for="team in filteredScheduleTeams" :key="team.id" @click="loadMatchesForTeam(team)"
:class="{ active: selectedTeam && selectedTeam.id === team.id }">
<span class="schedule-team-name">{{ team.name }}</span>
<span class="team-league" v-if="team.league && team.league.name">{{ team.league.name }}</span>
</li>
<li v-if="teams.length === 0" class="no-leagues">{{ $t('schedule.noTeamsFound') }}</li>
<li v-else-if="filteredScheduleTeams.length === 0" class="no-leagues">{{ $t('schedule.noMatchingTeams') }}</li>
</ul>
</div>
</aside>
<div class="flex-item" ref="scheduleContainer">
<div class="schedule-workspace-header">
<div>
<span class="workspace-eyebrow">{{ $t('schedule.activeSelection') }}</span>
<h3 v-if="selectedLeague">{{ selectedLeague }}</h3>
<h3 v-else>{{ $t('schedule.noSelectionTitle') }}</h3>
<p v-if="selectedLeague">{{ workspaceDescription }}</p>
<p v-else>{{ $t('schedule.noSelectionMessage') }}</p>
</div>
<div v-if="selectedLeague" class="schedule-workspace-actions">
<button
v-if="activeTab === 'schedule' && selectedTeam"
type="button"
class="btn-secondary"
@click="fetchTeamDataManually"
:disabled="fetchingTeamData"
>
{{ fetchingTeamData ? $t('schedule.fetchingTeamData') : $t('schedule.fetchTeamData') }}
</button>
<button v-if="activeTab === 'schedule'" @click="generatePDF">{{ $t('schedule.downloadPDF') }}</button>
<button
v-if="activeTab === 'table' && selectedTeam"
type="button"
class="btn-secondary"
@click="fetchTableFromMyTischtennis"
:disabled="fetchingTable"
>
{{ fetchingTable ? $t('schedule.tableLoading') : $t('schedule.refreshTable') }}
</button>
</div>
</div>
<!-- Tab Navigation - nur anzeigen wenn Liga ausgewählt -->
<div v-if="selectedLeague && selectedLeague !== ''" class="tab-navigation">
<button
:class="['tab-button', { active: activeTab === 'schedule' }]"
@click="activeTab = 'schedule'"
>
📅 {{ $t('schedule.scheduleTab') }}
📅 {{ $t('schedule.scheduleTab') }} <span class="tab-count">{{ matches.length }}</span>
</button>
<button
:class="['tab-button', { active: activeTab === 'table' }]"
@click="activeTab = 'table'"
>
📊 {{ $t('schedule.tableTab') }}
📊 {{ $t('schedule.tableTab') }} <span class="tab-count">{{ leagueTable.length }}</span>
</button>
</div>
@@ -51,9 +139,50 @@
<div v-if="selectedLeague && selectedLeague !== ''" class="tab-content">
<!-- Spielplan Tab -->
<div v-show="activeTab === 'schedule'" class="tab-panel">
<button @click="generatePDF">{{ $t('schedule.downloadPDF') }}</button>
<div v-if="selectedTeam" class="league-match-scope-card">
<div class="league-match-scope-header">
<strong>{{ $t('schedule.matchOverviewTitle') }}</strong>
<span>{{ $t('schedule.matchOverviewDescription') }}</span>
</div>
<div class="league-match-scope-controls">
<button
type="button"
class="team-filter-chip"
:class="{ active: leagueMatchScope === 'own' }"
@click="setLeagueMatchScope('own')"
>
{{ $t('schedule.ownTeamMatches') }}
</button>
<button
type="button"
class="team-filter-chip"
:class="{ active: leagueMatchScope === 'all' }"
@click="setLeagueMatchScope('all')"
>
{{ $t('schedule.allLeagueMatches') }}
</button>
<button
type="button"
class="team-filter-chip"
:class="{ active: leagueMatchScope === 'other' }"
@click="setLeagueMatchScope('other')"
>
{{ $t('schedule.otherTeamMatches') }}
</button>
<select
v-if="leagueMatchScope === 'other'"
v-model="selectedComparisonTeamName"
class="league-match-scope-select"
@change="applyLeagueMatchScope"
>
<option value="">{{ $t('schedule.selectOtherTeam') }}</option>
<option v-for="teamName in leagueTeamOptions" :key="teamName" :value="teamName">
{{ teamName }}
</option>
</select>
</div>
</div>
<div v-if="matches.length > 0">
<h3>{{ $t('schedule.gamesFor') }} {{ selectedLeague }}</h3>
<table id="schedule-table">
<thead>
<tr>
@@ -164,6 +293,10 @@
</div>
</div>
</div>
<div v-else class="schedule-empty-state">
<h3>{{ $t('schedule.noSelectionTitle') }}</h3>
<p>{{ $t('schedule.noSelectionMessage') }}</p>
</div>
</div>
</div>
@@ -323,6 +456,52 @@ export default {
},
computed: {
...mapGetters(['isAuthenticated', 'currentClub', 'clubs', 'currentClubName']),
filteredScheduleTeams() {
const query = this.teamSearchQuery.trim().toLowerCase();
if (!query) {
return this.teams;
}
return this.teams.filter((team) => {
const teamName = team.name?.toLowerCase() || '';
const leagueName = team.league?.name?.toLowerCase() || '';
return teamName.includes(query) || leagueName.includes(query);
});
},
completedMatchesCount() {
return this.matches.filter(match => match.isCompleted).length;
},
pendingMatchesCount() {
return this.matches.length - this.completedMatchesCount;
},
nextScheduledMatchLabel() {
const today = new Date();
today.setHours(0, 0, 0, 0);
const nextMatch = this.matches
.filter(match => match.date)
.map(match => ({ ...match, _date: new Date(match.date) }))
.filter(match => !isNaN(match._date.getTime()) && match._date >= today)
.sort((a, b) => a._date - b._date)[0];
return nextMatch ? this.formatDate(nextMatch.date) : '';
},
workspaceDescription() {
if (!this.selectedLeague) return '';
return this.activeTab === 'table'
? this.$t('schedule.workspaceTableDescription', { count: this.leagueTable.length })
: this.$t('schedule.workspaceScheduleDescription', {
matches: this.matches.length,
completed: this.completedMatchesCount,
pending: this.pendingMatchesCount
});
},
leagueTeamOptions() {
const ownTeamName = this.selectedTeam?.name;
return Array.from(new Set(this.allLeagueMatches.flatMap(match => [
match.homeTeam?.name,
match.guestTeam?.name
].filter(Boolean))))
.filter(teamName => teamName !== ownTeamName)
.sort((a, b) => a.localeCompare(b));
},
},
watch: {
currentClub: {
@@ -369,6 +548,12 @@ export default {
activeTab: 'schedule',
leagueTable: [],
fetchingTable: false,
fetchingTeamData: false,
teamSearchQuery: '',
ownLeagueMatches: [],
allLeagueMatches: [],
leagueMatchScope: 'own',
selectedComparisonTeamName: '',
// Player Selection Dialog
playerSelectionDialog: {
@@ -790,6 +975,10 @@ export default {
if (idxA !== idxB) return idxA - idxB;
return a.name.localeCompare(b.name);
});
if (!this.selectedLeague && this.teams.length > 0) {
await this.loadMatchesForSpecificTeam(this.teams[0], { resetScope: true });
}
} catch (error) {
console.error('ScheduleView: Error loading teams:', error);
this.showInfo(this.$t('messages.error'), this.$t('schedule.errorLoadingTeams'), '', 'error');
@@ -804,6 +993,9 @@ export default {
this.selectedTeam = null;
},
async loadMatchesForTeam(team) {
await this.loadMatchesForSpecificTeam(team, { resetScope: true });
},
async loadMatchesForSpecificTeam(team, { resetScope = false } = {}) {
if (!team || !team.league) {
this.showInfo(this.$t('messages.warning'), this.$t('schedule.noLeagueForTeam'), '', 'warning');
return;
@@ -811,20 +1003,69 @@ export default {
this.selectedTeam = team;
this.selectedLeague = `${team.name}${team.league?.name ? ` (${team.league.name})` : ''}`;
this.activeTab = 'schedule';
if (resetScope) {
this.leagueMatchScope = 'own';
this.selectedComparisonTeamName = '';
}
try {
const response = await apiClient.get(`/matches/leagues/${this.currentClub}/matches/${team.league.id}`);
this.matches = response.data;
const ownResponse = await apiClient.get(`/matches/leagues/${this.currentClub}/matches/${team.league.id}`);
this.ownLeagueMatches = ownResponse.data;
await this.loadLeagueMatches(team.league.id);
this.applyLeagueMatchScope();
// Lade auch die Tabellendaten für diese Liga
await this.loadLeagueTable(team.league.id);
} catch (error) {
this.showInfo(this.$t('messages.error'), this.$t('schedule.errorLoadingMatches'), '', 'error');
this.matches = [];
this.ownLeagueMatches = [];
this.allLeagueMatches = [];
}
},
async loadLeagueMatches(leagueId) {
const response = await apiClient.get(`/matches/leagues/${this.currentClub}/matches/${leagueId}?scope=all`);
this.allLeagueMatches = response.data;
},
applyLeagueMatchScope() {
if (!this.selectedTeam) {
return;
}
const ownTeamName = this.selectedTeam.name;
if (this.leagueMatchScope === 'all') {
this.matches = this.allLeagueMatches;
return;
}
if (this.leagueMatchScope === 'other') {
if (!this.selectedComparisonTeamName && this.leagueTeamOptions.length > 0) {
this.selectedComparisonTeamName = this.leagueTeamOptions[0];
}
this.matches = this.selectedComparisonTeamName
? this.allLeagueMatches.filter(match =>
match.homeTeam?.name === this.selectedComparisonTeamName ||
match.guestTeam?.name === this.selectedComparisonTeamName
)
: [];
return;
}
this.matches = this.ownLeagueMatches.length > 0
? this.ownLeagueMatches
: this.allLeagueMatches.filter(match =>
match.homeTeam?.name === ownTeamName || match.guestTeam?.name === ownTeamName
);
},
setLeagueMatchScope(scope) {
this.leagueMatchScope = scope;
if (scope !== 'other') {
this.selectedComparisonTeamName = '';
}
this.applyLeagueMatchScope();
},
async loadAllMatches() {
this.selectedLeague = this.$t('schedule.overallSchedule');
this.selectedTeam = null;
this.ownLeagueMatches = [];
this.allLeagueMatches = [];
this.leagueMatchScope = 'own';
this.selectedComparisonTeamName = '';
try {
const seasonParam = this.selectedSeasonId ? `?seasonid=${this.selectedSeasonId}` : '';
const response = await apiClient.get(`/matches/leagues/${this.currentClub}/matches${seasonParam}`);
@@ -837,6 +1078,10 @@ export default {
async loadAdultMatches() {
this.selectedLeague = this.$t('schedule.adultSchedule');
this.selectedTeam = null;
this.ownLeagueMatches = [];
this.allLeagueMatches = [];
this.leagueMatchScope = 'own';
this.selectedComparisonTeamName = '';
try {
const seasonParam = this.selectedSeasonId ? `?seasonid=${this.selectedSeasonId}` : '';
const response = await apiClient.get(`/matches/leagues/${this.currentClub}/matches${seasonParam}`);
@@ -1039,11 +1284,58 @@ export default {
this.fetchingTable = false;
}
},
async fetchTeamDataManually() {
if (!this.selectedTeam?.id) {
return;
}
this.fetchingTeamData = true;
try {
const response = await apiClient.post('/scheduler/match_results', {
clubTeamId: this.selectedTeam.id
});
const result = response?.data;
if (!result?.success) {
throw new Error(result?.error || result?.message || this.$t('schedule.fetchDataFailed'));
}
await this.loadMatchesForSpecificTeam(this.selectedTeam);
if (this.selectedTeam?.league?.id) {
await this.loadLeagueTable(this.selectedTeam.league.id);
}
const fetchedCount =
result?.result?.totalFetched ??
result?.result?.fetchedCount ??
result?.result?.data?.fetchedCount ??
0;
await this.showInfo(
this.$t('messages.success'),
this.$t('schedule.teamDataFetched'),
this.$t('schedule.teamDataFetchedDetails', {
team: this.selectedTeam.name,
count: fetchedCount
}),
'success'
);
} catch (error) {
await this.showInfo(
this.$t('messages.error'),
getSafeErrorMessage(error, this.$t('schedule.fetchDataFailed')),
'',
'error'
);
} finally {
this.fetchingTeamData = false;
}
},
refreshScheduleData() {
if (!this.selectedLeague) return;
if (this.selectedTeam) {
this.loadMatchesForTeam(this.selectedTeam);
this.loadMatchesForSpecificTeam(this.selectedTeam);
} else if (this.selectedLeague === this.$t('schedule.overallSchedule')) {
this.loadAllMatches();
} else if (this.selectedLeague === this.$t('schedule.adultSchedule')) {
@@ -1443,18 +1735,20 @@ li {
}
.fetch-table-btn {
background-color: #28a745;
color: white;
border: none;
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: var(--text-on-primary);
border: 1px solid transparent;
padding: 8px 16px;
border-radius: 4px;
border-radius: 10px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s ease;
font-weight: 600;
transition: transform 0.2s ease, opacity 0.2s ease;
}
.fetch-table-btn:hover:not(:disabled) {
background-color: #218838;
transform: translateY(-1px);
opacity: 0.95;
}
.fetch-table-btn:disabled {
@@ -1673,23 +1967,284 @@ li {
}
.btn-save {
background-color: #28a745;
color: white;
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: var(--text-on-primary);
}
.btn-save:hover {
background-color: #218838;
transform: translateY(-1px);
}
.btn-cancel {
background-color: #6c757d;
color: white;
background: var(--surface-color);
color: var(--text-color);
border-color: var(--border-color);
}
.btn-cancel:hover {
background-color: #5a6268;
}
.schedule-view {
display: flex;
flex-direction: column;
gap: 16px;
}
.schedule-page-header {
display: flex;
justify-content: space-between;
gap: 16px;
align-items: flex-start;
flex-wrap: wrap;
}
.schedule-page-title h2 {
margin: 0 0 4px;
}
.schedule-page-title p {
margin: 0;
color: var(--text-muted, #6c757d);
}
.schedule-page-actions {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.schedule-summary-bar {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.schedule-summary-item {
min-width: 120px;
padding: 10px 12px;
border: 1px solid var(--border-color, #dee2e6);
border-radius: 10px;
background: #fff;
}
.schedule-summary-label {
display: block;
font-size: 12px;
color: var(--text-muted, #6c757d);
margin-bottom: 4px;
}
.schedule-layout {
display: grid;
grid-template-columns: 280px minmax(0, 1fr);
gap: 16px;
align-items: start;
}
.schedule-sidebar {
min-width: 0;
}
.schedule-sidebar-card,
.schedule-workspace-header,
.schedule-empty-state {
border: 1px solid var(--border-color, #dee2e6);
border-radius: 12px;
background: #fff;
}
.schedule-sidebar-card {
padding: 14px;
}
.schedule-sidebar-header,
.schedule-workspace-header {
display: flex;
justify-content: space-between;
gap: 16px;
align-items: flex-start;
}
.schedule-sidebar-header h3,
.schedule-workspace-header h3 {
margin: 0;
}
.schedule-selection-count,
.workspace-eyebrow {
font-size: 12px;
color: var(--text-muted, #6c757d);
}
.workspace-eyebrow {
display: block;
margin-bottom: 4px;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.schedule-workspace-header {
padding: 14px 16px;
margin-bottom: 14px;
}
.schedule-workspace-header p {
margin: 6px 0 0;
color: var(--text-muted, #6c757d);
}
.schedule-workspace-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.schedule-team-search {
width: 100%;
margin-top: 12px;
margin-bottom: 12px;
padding: 10px 12px;
border: 1px solid var(--border-color, #ced4da);
border-radius: 8px;
}
.schedule-quick-links {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 12px;
}
.schedule-quick-link {
width: 100%;
text-align: left;
padding: 10px 12px;
border: 1px solid var(--border-color, #dee2e6);
border-radius: 8px;
background: #fff;
cursor: pointer;
}
.schedule-quick-link.active,
.schedule-team-list li.active {
border-color: var(--primary-color, #2b7cff);
color: var(--primary-color, #2b7cff);
background: rgba(43, 124, 255, 0.08);
font-weight: 600;
}
.schedule-team-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.schedule-team-list li {
display: flex;
flex-direction: column;
gap: 4px;
padding: 10px 12px;
border: 1px solid var(--border-color, #dee2e6);
border-radius: 8px;
cursor: pointer;
}
.schedule-team-name {
font-weight: 600;
}
.league-match-scope-card {
border: 1px solid var(--border-color, #dee2e6);
border-radius: 14px;
background: linear-gradient(180deg, rgba(47, 122, 95, 0.08), rgba(47, 122, 95, 0.03));
padding: 14px;
margin-bottom: 12px;
}
.league-match-scope-header {
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 10px;
}
.league-match-scope-header span {
color: var(--text-muted, #6c757d);
font-size: 14px;
}
.league-match-scope-controls {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.team-filter-chip {
border: 1px solid var(--border-color, #dee2e6);
border-radius: 999px;
background: var(--surface-color, #fff);
color: var(--text-color, #333);
padding: 8px 12px;
cursor: pointer;
font-weight: 600;
}
.team-filter-chip.active {
border-color: var(--primary-soft, #5f9b82);
color: var(--primary-strong, #1f5f49);
background: rgba(47, 122, 95, 0.12);
}
.league-match-scope-select {
min-width: 220px;
padding: 8px 10px;
border: 1px solid var(--border-color, #dee2e6);
border-radius: 10px;
background: var(--surface-color, #fff);
color: var(--text-color, #333);
}
.league-match-scope-select:focus {
outline: none;
border-color: var(--primary-color, #2f7a5f);
box-shadow: 0 0 0 3px rgba(47, 122, 95, 0.14);
}
.tab-count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 22px;
height: 22px;
padding: 0 6px;
margin-left: 6px;
border-radius: 999px;
background: rgba(47, 122, 95, 0.12);
color: var(--primary-strong, #1f5f49);
font-size: 12px;
}
.schedule-empty-state {
padding: 28px;
text-align: center;
}
.schedule-empty-state h3 {
margin-top: 0;
margin-bottom: 8px;
}
.schedule-empty-state p {
margin: 0;
color: var(--text-muted, #6c757d);
}
.output ul li.active {
font-weight: 600;
color: var(--primary-color, #2b7cff);
@@ -1698,4 +2253,10 @@ li {
.team-league {
color: var(--text-muted, #6c757d);
}
@media (max-width: 960px) {
.schedule-layout {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -1,24 +1,55 @@
<template>
<div>
<h2>{{ t('teamManagement.title') }}</h2>
<SeasonSelector
v-model="selectedSeasonId"
@season-change="onSeasonChange"
:show-current-season="true"
/>
<div v-if="schedulerJobs.rating_updates || schedulerJobs.match_results" class="scheduler-jobs-summary">
<div class="scheduler-jobs-summary-copy">
<strong>{{ t('teamManagement.automaticJobs') }}</strong>
<span v-if="schedulerJobs.rating_updates?.lastRun || schedulerJobs.match_results?.lastRun" class="scheduler-jobs-summary-text">
{{ t('teamManagement.lastRun') }}:
{{ formatJobDate(schedulerJobs.match_results?.lastRun || schedulerJobs.rating_updates?.lastRun) }}
</span>
<div class="team-management-view">
<div class="team-management-hero">
<div class="team-management-hero-header">
<div class="team-management-hero-copy">
<h2>{{ t('teamManagement.title') }}</h2>
<p>{{ t('teamManagement.subtitle') }}</p>
</div>
<div class="team-management-hero-actions">
<div class="team-management-season-selector">
<span class="team-management-hero-label">{{ t('teamManagement.season') }}</span>
<SeasonSelector
v-model="selectedSeasonId"
@season-change="onSeasonChange"
:show-current-season="true"
/>
</div>
</div>
</div>
<div v-if="schedulerJobs.rating_updates || schedulerJobs.match_results" class="scheduler-jobs-summary">
<div class="scheduler-jobs-summary-copy">
<strong>{{ t('teamManagement.automaticJobs') }}</strong>
<span v-if="schedulerJobs.rating_updates?.lastRun || schedulerJobs.match_results?.lastRun" class="scheduler-jobs-summary-text">
{{ t('teamManagement.lastRun') }}:
{{ formatJobDate(schedulerJobs.match_results?.lastRun || schedulerJobs.rating_updates?.lastRun) }}
</span>
</div>
<button type="button" class="scheduler-jobs-toggle" @click="showGlobalJobDetails = !showGlobalJobDetails">
<span class="scheduler-jobs-toggle-icon">{{ showGlobalJobDetails ? '▾' : '▸' }}</span>
<span class="scheduler-jobs-toggle-text">{{ showGlobalJobDetails ? t('common.close') : t('common.details') }}</span>
</button>
</div>
<div class="team-management-summary">
<div class="team-summary-card">
<span class="team-summary-label">{{ t('teamManagement.teams') }}</span>
<strong class="team-summary-value">{{ teams.length }}</strong>
</div>
<div class="team-summary-card">
<span class="team-summary-label">{{ t('teamManagement.fullyConfigured') }}</span>
<strong class="team-summary-value">{{ fullyConfiguredTeamsCount }}</strong>
</div>
<div class="team-summary-card">
<span class="team-summary-label">{{ t('teamManagement.partiallyConfigured') }}</span>
<strong class="team-summary-value">{{ partiallyConfiguredTeamsCount }}</strong>
</div>
<div class="team-summary-card">
<span class="team-summary-label">{{ t('teamManagement.noLeague') }}</span>
<strong class="team-summary-value">{{ teamsWithoutLeagueCount }}</strong>
</div>
</div>
<button type="button" class="scheduler-jobs-toggle" @click="showGlobalJobDetails = !showGlobalJobDetails">
{{ showGlobalJobDetails ? t('common.close') : t('common.details') }}
</button>
</div>
<div v-if="showGlobalJobDetails && (schedulerJobs.rating_updates || schedulerJobs.match_results)" class="scheduler-jobs-info">
@@ -53,25 +84,6 @@
</div>
</div>
<div class="team-management-summary">
<div class="team-summary-card">
<span class="team-summary-label">{{ t('teamManagement.teams') }}</span>
<strong class="team-summary-value">{{ teams.length }}</strong>
</div>
<div class="team-summary-card">
<span class="team-summary-label">{{ t('teamManagement.fullyConfigured') }}</span>
<strong class="team-summary-value">{{ fullyConfiguredTeamsCount }}</strong>
</div>
<div class="team-summary-card">
<span class="team-summary-label">{{ t('teamManagement.partiallyConfigured') }}</span>
<strong class="team-summary-value">{{ partiallyConfiguredTeamsCount }}</strong>
</div>
<div class="team-summary-card">
<span class="team-summary-label">{{ t('teamManagement.noLeague') }}</span>
<strong class="team-summary-value">{{ teamsWithoutLeagueCount }}</strong>
</div>
</div>
<div class="teams-list">
<div class="teams-list-header">
<h3>{{ t('teamManagement.teams') }} ({{ filteredTeams.length }}) - {{ t('teamManagement.season') }} {{ currentSeason?.season || t('teamManagement.seasonUnknown') }}</h3>
@@ -115,11 +127,9 @@
<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="editTeam(team)" class="btn-edit" :title="t('teamManagement.edit')">
</button>
<button @click.stop="deleteTeam(team)" class="btn-delete" :title="t('teamManagement.delete')">
🗑
</button>
@@ -225,21 +235,42 @@
</div>
<div v-if="activeEditorSection === 'basic'" class="workspace-section-panel basic-settings">
<label>
<span>{{ t('teamManagement.teamName') }}:</span>
<input type="text" v-model="newTeamName" :placeholder="t('teamManagement.teamNamePlaceholder')">
</label>
<label>
<span>{{ t('teamManagement.league') }}:</span>
<select v-model="newLeagueId">
<option value="">{{ t('teamManagement.noLeague') }}</option>
<option v-for="league in filteredLeagues" :key="league.id" :value="league.id">
{{ league.name }}
</option>
</select>
</label>
<div class="workspace-panel-header">
<div>
<span class="section-title"> {{ t('teamManagement.basicSettings') }}</span>
<p class="workspace-panel-copy">{{ t('teamManagement.basicSettingsIntro') }}</p>
</div>
</div>
<div v-if="teamToEdit" class="settings-summary-grid">
<div class="settings-summary-card">
<span class="settings-summary-label">{{ t('teamManagement.teamName') }}</span>
<strong>{{ teamToEdit.name }}</strong>
</div>
<div class="settings-summary-card">
<span class="settings-summary-label">{{ t('teamManagement.league') }}</span>
<strong>{{ teamToEdit.league ? teamToEdit.league.name : t('teamManagement.noLeague') }}</strong>
</div>
<div class="settings-summary-card">
<span class="settings-summary-label">{{ t('teamManagement.season') }}</span>
<strong>{{ teamToEdit.season?.season || t('teamManagement.seasonUnknown') }}</strong>
</div>
</div>
<div class="settings-form-grid">
<label>
<span>{{ t('teamManagement.teamName') }}:</span>
<input type="text" v-model="newTeamName" :placeholder="t('teamManagement.teamNamePlaceholder')">
</label>
<label>
<span>{{ t('teamManagement.league') }}:</span>
<select v-model="newLeagueId">
<option value="">{{ t('teamManagement.noLeague') }}</option>
<option v-for="league in filteredLeagues" :key="league.id" :value="league.id">
{{ league.name }}
</option>
</select>
</label>
</div>
<div class="form-actions">
<button @click="addNewTeam" :disabled="!newTeamName.trim()">
{{ teamToEdit ? t('teamManagement.change') : t('teamManagement.createAndEdit') }}
@@ -254,12 +285,29 @@
</div>
<div v-if="teamToEdit && teamToEdit.leagueId && activeEditorSection === 'stats'" class="workspace-section-panel player-stats">
<div class="stats-header">
<span class="section-title">📊 {{ t('teamManagement.playerStats') }}</span>
<div class="workspace-panel-header">
<div>
<span class="section-title">📊 {{ t('teamManagement.playerStats') }}</span>
<p class="workspace-panel-copy">{{ t('teamManagement.playerStatsIntro') }}</p>
</div>
<button @click="refreshPlayerStats" :disabled="loadingStats" class="btn-sm">
{{ loadingStats ? '⏳' : '🔄' }}
{{ loadingStats ? '⏳' : '🔄' }} {{ t('teamManagement.refreshStats') }}
</button>
</div>
<div v-if="playerStats.length > 0" class="stats-summary-grid">
<div class="settings-summary-card">
<span class="settings-summary-label">{{ t('teamManagement.player') }}</span>
<strong>{{ playerStats.length }}</strong>
</div>
<div class="settings-summary-card">
<span class="settings-summary-label">{{ t('teamManagement.season') }}</span>
<strong>{{ totalSeasonAppearances }}</strong>
</div>
<div class="settings-summary-card">
<span class="settings-summary-label">{{ isSecondHalf ? t('teamManagement.secondHalf') : t('teamManagement.firstHalf') }}</span>
<strong>{{ totalHalfAppearances }}</strong>
</div>
</div>
<div v-if="loadingStats" class="loading-stats">{{ t('teamManagement.loadingStats') }}</div>
@@ -295,15 +343,78 @@
</div>
<div v-if="teamToEdit && activeEditorSection === 'documents'" class="workspace-section-panel advanced-settings">
<div class="upload-actions compact">
<div class="workspace-panel-header">
<div>
<span class="section-title">📋 {{ t('teamManagement.documents') }}</span>
<div class="upload-buttons-compact">
<button @click="uploadCodeList" class="btn-upload-sm">
📋 {{ t('teamManagement.codeList') }}
</button>
<button @click="uploadPinList" class="btn-upload-sm">
🔐 {{ t('teamManagement.pinList') }}
</button>
<p class="workspace-panel-copy">{{ t('teamManagement.documentsIntro') }}</p>
</div>
</div>
<div class="document-status-grid">
<div class="document-status-card">
<div class="document-status-head">
<strong>{{ t('teamManagement.codeList') }}</strong>
<span class="document-status-badge" :class="getTeamDocuments(teamToEdit.id, 'code_list').length ? 'complete' : 'missing'">
{{ getTeamDocuments(teamToEdit.id, 'code_list').length ? t('teamManagement.documentAvailable') : t('teamManagement.documentMissing') }}
</span>
</div>
<p class="document-status-meta">
{{ getLatestTeamDocument(teamToEdit.id, 'code_list') ? t('teamManagement.latestUpload', { date: formatDate(getLatestTeamDocument(teamToEdit.id, 'code_list').createdAt) }) : t('teamManagement.noDocumentUploadYet') }}
</p>
<div class="document-status-actions">
<button @click="uploadCodeList" class="btn-upload-sm">
📋 {{ getTeamDocuments(teamToEdit.id, 'code_list').length ? t('teamManagement.uploadNewVersion') : t('teamManagement.codeList') }}
</button>
<button
v-if="getLatestTeamDocument(teamToEdit.id, 'code_list')"
type="button"
class="btn-secondary btn-upload-sm"
@click="showPDFDialog(teamToEdit.id, 'code_list')"
>
{{ t('teamManagement.openDocument') }}
</button>
<button
v-if="getLatestTeamDocument(teamToEdit.id, 'code_list')"
type="button"
class="btn-secondary btn-upload-sm"
@click="parsePDF(getLatestTeamDocument(teamToEdit.id, 'code_list'))"
:disabled="!!parsingDocuments[getLatestTeamDocument(teamToEdit.id, 'code_list').id]"
>
{{ t('teamManagement.parseDocument') }}
</button>
</div>
</div>
<div class="document-status-card">
<div class="document-status-head">
<strong>{{ t('teamManagement.pinList') }}</strong>
<span class="document-status-badge" :class="getTeamDocuments(teamToEdit.id, 'pin_list').length ? 'complete' : 'missing'">
{{ getTeamDocuments(teamToEdit.id, 'pin_list').length ? t('teamManagement.documentAvailable') : t('teamManagement.documentMissing') }}
</span>
</div>
<p class="document-status-meta">
{{ getLatestTeamDocument(teamToEdit.id, 'pin_list') ? t('teamManagement.latestUpload', { date: formatDate(getLatestTeamDocument(teamToEdit.id, 'pin_list').createdAt) }) : t('teamManagement.noDocumentUploadYet') }}
</p>
<div class="document-status-actions">
<button @click="uploadPinList" class="btn-upload-sm">
🔐 {{ getTeamDocuments(teamToEdit.id, 'pin_list').length ? t('teamManagement.uploadNewVersion') : t('teamManagement.pinList') }}
</button>
<button
v-if="getLatestTeamDocument(teamToEdit.id, 'pin_list')"
type="button"
class="btn-secondary btn-upload-sm"
@click="showPDFDialog(teamToEdit.id, 'pin_list')"
>
{{ t('teamManagement.openDocument') }}
</button>
<button
v-if="getLatestTeamDocument(teamToEdit.id, 'pin_list')"
type="button"
class="btn-secondary btn-upload-sm"
@click="parsePDF(getLatestTeamDocument(teamToEdit.id, 'pin_list'))"
:disabled="!!parsingDocuments[getLatestTeamDocument(teamToEdit.id, 'pin_list').id]"
>
{{ t('teamManagement.parseDocument') }}
</button>
</div>
</div>
</div>
</div>
@@ -330,12 +441,10 @@
<div v-if="teamToEdit && activeEditorSection === 'myTischtennis'" class="workspace-section-panel advanced-settings">
<div class="mytischtennis-config compact">
<div class="mytischtennis-header-compact">
<span class="section-title">🏓 {{ t('teamManagement.myTischtennis') }}</span>
<div class="status-inline">
<span v-if="getMyTischtennisStatus(teamToEdit).complete" class="badge-sm complete"></span>
<span v-else-if="getMyTischtennisStatus(teamToEdit).partial" class="badge-sm partial"></span>
<span v-else class="badge-sm missing"></span>
<div class="workspace-panel-header">
<div>
<span class="section-title">🏓 {{ t('teamManagement.myTischtennis') }}</span>
<p class="workspace-panel-copy">{{ t('teamManagement.myTischtennisIntro') }}</p>
</div>
<button
v-if="getMyTischtennisStatus(teamToEdit).complete"
@@ -343,9 +452,23 @@
:disabled="fetchingTeamData"
class="btn-sm"
>
{{ fetchingTeamData ? '⏳' : '🔄' }}
{{ fetchingTeamData ? '⏳' : '🔄' }} {{ t('teamManagement.manualFetch') }}
</button>
</div>
<div class="mytt-status-card">
<div class="mytt-status-line">
<span class="mytt-status-label">{{ t('teamManagement.configurationStatus') }}</span>
<span v-if="getMyTischtennisStatus(teamToEdit).complete" class="status-badge complete"> {{ t('teamManagement.fullyConfigured') }}</span>
<span v-else-if="getMyTischtennisStatus(teamToEdit).partial" class="status-badge partial"> {{ t('teamManagement.partiallyConfigured') }}</span>
<span v-else class="status-badge missing"> {{ t('teamManagement.notConfigured') }}</span>
</div>
<p v-if="getMyTischtennisStatus(teamToEdit).missing" class="mytt-status-copy">
{{ getMyTischtennisStatus(teamToEdit).missing }}
</p>
<p v-else class="mytt-status-copy">
{{ t('teamManagement.noIssues') }}
</p>
</div>
<div class="compact-input-row">
<input
@@ -356,7 +479,9 @@
class="compact-url-input"
:disabled="parsingUrl"
>
<span v-if="parsingUrl" class="inline-status"></span>
<button type="button" class="btn-secondary btn-upload-sm" @click="parseMyTischtennisUrl" :disabled="parsingUrl || !myTischtennisUrl.trim()">
{{ parsingUrl ? '⏳' : t('teamManagement.parseUrlAction') }}
</button>
</div>
<div v-if="myTischtennisError" class="compact-message error"> {{ myTischtennisError }}</div>
@@ -510,6 +635,8 @@ export default {
const fullyConfiguredTeamsCount = computed(() => teams.value.filter(team => getMyTischtennisStatus(team).complete).length);
const partiallyConfiguredTeamsCount = computed(() => teams.value.filter(team => getMyTischtennisStatus(team).partial).length);
const teamsWithoutLeagueCount = computed(() => teams.value.filter(team => !team.leagueId).length);
const totalSeasonAppearances = computed(() => playerStats.value.reduce((sum, stat) => sum + (Number(stat.totalSeason) || 0), 0));
const totalHalfAppearances = computed(() => playerStats.value.reduce((sum, stat) => sum + (Number(isSecondHalf.value ? stat.totalSecondHalf : stat.totalFirstHalf) || 0), 0));
const filteredTeams = computed(() => {
const search = teamSearchQuery.value.trim().toLowerCase();
return teams.value.filter(team => {
@@ -980,6 +1107,12 @@ export default {
doc.clubTeamId === teamId && doc.documentType === documentType
);
};
const getLatestTeamDocument = (teamId, documentType) => {
return getTeamDocuments(teamId, documentType)
.slice()
.sort((a, b) => new Date(b.createdAt || 0) - new Date(a.createdAt || 0))[0] || null;
};
const showPDFDialog = async (teamId, documentType) => {
const documents = getTeamDocuments(teamId, documentType);
@@ -1226,7 +1359,7 @@ export default {
if (!hasLeague) missingItems.push(t('teamManagement.league'));
if (hasLeague && !team.league.myTischtennisGroupId) missingItems.push(t('teamManagement.groupId'));
if (hasLeague && !team.league.association) missingItems.push(t('teamManagement.association'));
if (hasLeague && !team.league.groupName) missingItems.push(t('teamManagement.groupName'));
if (hasLeague && !team.league.groupname) missingItems.push(t('teamManagement.groupName'));
const complete = hasTeamId && hasLeagueConfig;
const partial = (hasTeamId || hasLeagueConfig) && !complete;
@@ -1468,6 +1601,8 @@ export default {
fullyConfiguredTeamsCount,
partiallyConfiguredTeamsCount,
teamsWithoutLeagueCount,
totalSeasonAppearances,
totalHalfAppearances,
toggleNewTeam,
resetToNewTeam,
resetNewTeam,
@@ -1480,6 +1615,7 @@ export default {
loadAllTeamDocuments,
parsePDF,
getTeamDocuments,
getLatestTeamDocument,
showPDFDialog,
closePDFDialog,
formatDate,
@@ -1504,6 +1640,57 @@ export default {
</script>
<style scoped>
.team-management-view {
display: flex;
flex-direction: column;
gap: 1rem;
}
.team-management-hero {
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
background: var(--background-light);
padding: 0.9rem;
}
.team-management-hero-header {
display: flex;
justify-content: space-between;
gap: 1rem;
align-items: flex-start;
flex-wrap: wrap;
margin-bottom: 0.85rem;
}
.team-management-hero-copy h2 {
margin: 0 0 0.25rem;
}
.team-management-hero-copy p {
margin: 0;
color: var(--text-muted);
}
.team-management-hero-actions {
display: flex;
gap: 0.75rem;
align-items: flex-start;
}
.team-management-season-selector {
display: flex;
flex-direction: column;
gap: 0.3rem;
}
.team-management-hero-label {
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.03em;
text-transform: uppercase;
color: var(--text-muted);
}
.scheduler-jobs-summary {
display: flex;
justify-content: space-between;
@@ -1528,12 +1715,27 @@ export default {
}
.scheduler-jobs-toggle {
display: inline-flex;
align-items: center;
gap: 0.35rem;
min-width: 96px;
padding: 0.4rem 0.75rem;
border: 1px solid var(--border-color);
border-radius: 999px;
background: white;
cursor: pointer;
font-weight: 600;
color: var(--text-color);
line-height: 1;
white-space: nowrap;
}
.scheduler-jobs-toggle-icon {
color: var(--text-muted);
}
.scheduler-jobs-toggle-text {
color: inherit;
}
.team-management-summary {
@@ -1808,10 +2010,17 @@ export default {
.team-search-input {
width: 100%;
padding: 0.55rem 0.7rem;
padding: 0.65rem 0.8rem;
border: 1px solid var(--border-color);
border-radius: 8px;
background: white;
border-radius: 10px;
background: var(--surface-color);
color: var(--text-color);
}
.team-search-input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(47, 122, 95, 0.14);
}
.team-filter-chips {
@@ -1822,18 +2031,19 @@ export default {
.team-filter-chip {
border: 1px solid var(--border-color);
background: var(--background-light);
background: var(--surface-muted);
color: var(--text-color);
border-radius: 999px;
padding: 0.35rem 0.75rem;
font-size: 0.85rem;
cursor: pointer;
font-weight: 600;
}
.team-filter-chip.active {
background: var(--primary-light);
border-color: var(--primary-color);
color: var(--primary-dark);
background: rgba(47, 122, 95, 0.12);
border-color: var(--primary-soft);
color: var(--primary-strong);
font-weight: 600;
}
@@ -1896,6 +2106,11 @@ export default {
color: var(--text-muted);
}
.team-open-hint {
font-size: 0.78rem;
color: var(--primary-color);
}
.team-actions {
display: flex;
gap: 0.5rem;
@@ -1910,10 +2125,6 @@ export default {
transition: var(--transition);
}
.btn-edit:hover {
background: var(--primary-light);
}
.btn-delete:hover {
background: #fee;
}
@@ -1938,7 +2149,114 @@ export default {
line-height: 1.35;
}
.workspace-panel-header {
display: flex;
justify-content: space-between;
gap: 1rem;
align-items: flex-start;
margin-bottom: 0.9rem;
}
.workspace-panel-copy {
margin: 0.25rem 0 0;
color: var(--text-muted);
font-size: 0.92rem;
}
.document-status-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 0.9rem;
}
.document-status-card,
.mytt-status-card {
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
background: var(--background-light);
padding: 0.9rem;
}
.document-status-head,
.mytt-status-line {
display: flex;
justify-content: space-between;
gap: 0.75rem;
align-items: flex-start;
}
.document-status-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
font-size: 0.8rem;
font-weight: 600;
padding: 0.22rem 0.55rem;
border-radius: 999px;
}
.document-status-badge.complete {
background: #d9f5e3;
color: #1f7a3f;
}
.document-status-badge.missing {
background: #fdeaea;
color: #b23b3b;
}
.document-status-meta,
.mytt-status-copy {
margin: 0.55rem 0 0;
color: var(--text-muted);
line-height: 1.4;
}
.document-status-actions {
display: flex;
flex-wrap: wrap;
gap: 0.55rem;
margin-top: 0.8rem;
}
.settings-summary-grid,
.stats-summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 0.75rem;
margin-bottom: 0.9rem;
}
.settings-summary-card {
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
background: var(--background-light);
padding: 0.8rem 0.9rem;
}
.settings-summary-label {
display: block;
font-size: 0.8rem;
color: var(--text-muted);
margin-bottom: 0.25rem;
}
.settings-form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 0.9rem;
}
.mytt-status-label {
font-weight: 600;
color: var(--text-color);
}
@media (max-width: 768px) {
.team-management-hero-header {
flex-direction: column;
}
.scheduler-jobs-summary {
flex-direction: column;
align-items: flex-start;
@@ -1993,17 +2311,17 @@ export default {
.btn-upload-sm {
padding: 4px 10px;
font-size: 0.85rem;
border: none;
border-radius: 3px;
font-weight: 500;
border: 1px solid transparent;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: var(--transition);
background-color: #007bff;
color: white;
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: var(--text-on-primary);
}
.btn-upload-sm:hover {
background-color: #0056b3;
transform: translateY(-1px);
}
/* Legacy styles */
@@ -2029,8 +2347,8 @@ export default {
.upload-btn {
padding: 0.4rem 1rem;
border: none;
border-radius: var(--border-radius-small);
border: 1px solid transparent;
border-radius: 10px;
font-weight: 600;
cursor: pointer;
transition: var(--transition);
@@ -2041,21 +2359,22 @@ export default {
}
.code-list-btn {
background: #4CAF50;
color: white;
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: var(--text-on-primary);
}
.code-list-btn:hover {
background: #45a049;
transform: translateY(-1px);
}
.pin-list-btn {
background: #FF9800;
color: white;
background: rgba(181, 110, 65, 0.14);
border-color: rgba(181, 110, 65, 0.24);
color: #8a4f28;
}
.pin-list-btn:hover {
background: #e68900;
background: rgba(181, 110, 65, 0.2);
}
@media (max-width: 768px) {
@@ -2072,13 +2391,13 @@ export default {
.upload-confirmation {
margin-top: 0.75rem;
padding: 0.75rem;
background: #f8f9fa;
background: var(--surface-muted);
border-radius: var(--border-radius);
border: 1px solid #dee2e6;
border: 1px solid var(--border-color);
}
.selected-file-info {
background: #e9ecef;
background: rgba(15, 23, 42, 0.05);
padding: 0.5rem;
border-radius: var(--border-radius-small);
margin-bottom: 0.5rem;
@@ -2094,11 +2413,11 @@ export default {
.confirm-parse-btn {
background: #4caf50;
color: white;
border: none;
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: var(--text-on-primary);
border: 1px solid transparent;
padding: 0.4rem 1rem;
border-radius: var(--border-radius-small);
border-radius: 10px;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
@@ -2106,20 +2425,20 @@ export default {
}
.confirm-parse-btn:hover:not(:disabled) {
background: #45a049;
transform: translateY(-1px);
}
.confirm-parse-btn:disabled {
background: #cccccc;
cursor: not-allowed;
opacity: 0.6;
}
.cancel-parse-btn {
background: #6c757d;
color: white;
border: none;
background: var(--surface-color);
color: var(--text-color);
border: 1px solid var(--border-color);
padding: 0.4rem 1rem;
border-radius: var(--border-radius-small);
border-radius: 10px;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
@@ -2127,7 +2446,8 @@ export default {
}
.cancel-parse-btn:hover {
background: #5a6268;
background: var(--surface-muted);
border-color: var(--primary-soft);
}
.selected-file {
@@ -2683,30 +3003,33 @@ export default {
}
.status-badge {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: var(--border-radius-small);
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding: 0.28rem 0.78rem;
border-radius: 999px;
font-size: 0.85rem;
font-weight: 600;
white-space: nowrap;
border: 1px solid transparent;
}
.status-badge.complete {
background: #e8f5e9;
color: #2e7d32;
border: 1px solid #4caf50;
background: rgba(47, 122, 95, 0.12);
color: #1f5f49;
border-color: rgba(47, 122, 95, 0.24);
}
.status-badge.partial {
background: #fff3e0;
color: #e65100;
border: 1px solid #ff9800;
background: rgba(181, 110, 65, 0.14);
color: #8a4f28;
border-color: rgba(181, 110, 65, 0.28);
}
.status-badge.missing {
background: #ffebee;
color: #c62828;
border: 1px solid #ef5350;
background: rgba(200, 74, 56, 0.12);
color: #8b3327;
border-color: rgba(200, 74, 56, 0.24);
}
.mytischtennis-status {

View File

@@ -4700,7 +4700,7 @@ button {
/* Beendete Spiele */
.match-finished-win {
background-color: #28a745; /* Dunkelgrün */
background-color: var(--primary-color);
color: white;
font-weight: bold;
}
@@ -4952,42 +4952,49 @@ button {
min-width: 200px;
max-width: 300px;
padding: 0.4rem;
border: 1px solid #ccc;
border-radius: 4px;
border: 1px solid var(--border-color);
border-radius: 8px;
background: var(--surface-color);
color: var(--text-color);
}
.btn-add {
padding: 0.4rem 0.8rem;
background-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: 10px;
cursor: pointer;
font-size: 0.9em;
white-space: nowrap;
font-weight: 600;
transition: transform 0.2s ease, opacity 0.2s ease;
}
.btn-add:hover {
background-color: #0056b3;
transform: translateY(-1px);
opacity: 0.95;
}
.training-btn {
background-color: #28a745;
color: white;
border: none;
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: var(--text-on-primary);
border: 1px solid transparent;
padding: 0.5rem 1rem;
border-radius: 4px;
border-radius: 10px;
cursor: pointer;
font-size: 0.9em;
transition: background-color 0.2s ease;
font-weight: 600;
transition: transform 0.2s ease, opacity 0.2s ease;
}
.training-btn:hover {
background-color: #218838;
transform: translateY(-1px);
opacity: 0.95;
}
.training-btn:active {
background-color: #1e7e34;
transform: translateY(0);
}
.tournament-classes {
@@ -5430,14 +5437,14 @@ tbody tr:hover:not(.active-match) {
}
.btn-live.active {
background-color: #28a745;
border-color: #28a745;
background-color: var(--primary-color);
border-color: var(--primary-color);
color: white;
}
.btn-live.active:hover {
background-color: #218838;
border-color: #1e7e34;
background-color: var(--primary-hover);
border-color: var(--primary-hover);
}
/* Button für "Korrigieren" - weißer Hintergrund mit grüner Schrift */
@@ -5446,8 +5453,8 @@ tbody tr:hover:not(.active-match) {
background: white !important;
background-color: white !important;
background-image: none !important;
color: #4CAF50 !important;
border: 1px solid #4CAF50 !important;
color: var(--primary-color) !important;
border: 1px solid var(--primary-color) !important;
padding: 0.25rem 0.5rem;
font-size: 0.9em;
border-radius: 3px;
@@ -5467,8 +5474,8 @@ tbody tr:hover:not(.active-match) {
background: #f8f9fa !important;
background-color: #f8f9fa !important;
background-image: none !important;
border-color: #45a049 !important;
color: #45a049 !important;
border-color: var(--primary-hover) !important;
color: var(--primary-hover) !important;
transform: none !important;
}
.pairings-section {
@@ -5560,7 +5567,7 @@ tbody tr:hover:not(.active-match) {
}
.pairings-table thead {
background-color: #28a745;
background-color: var(--primary-color);
color: white;
}
@@ -5583,17 +5590,17 @@ tbody tr:hover:not(.active-match) {
.btn-random-pairings {
padding: 0.5rem 1rem;
background-color: #28a745;
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: 10px;
cursor: pointer;
font-size: 0.95em;
font-weight: 500;
font-weight: 600;
}
.btn-random-pairings:hover {
background-color: #218838;
opacity: 0.95;
}
.btn-random-pairings:disabled {
@@ -5609,17 +5616,17 @@ tbody tr:hover:not(.active-match) {
.btn-random-pairings {
padding: 0.5rem 1rem;
background-color: #28a745;
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: 10px;
cursor: pointer;
font-size: 0.95em;
font-weight: 500;
font-weight: 600;
}
.btn-random-pairings:hover {
background-color: #218838;
opacity: 0.95;
}
.btn-random-pairings:disabled {