Fügt Unterstützung für parallele Entwicklungsumgebungen hinzu und aktualisiert die Benutzeroberfläche. Neue Routen und Komponenten für Trainingsstatistiken implementiert. Fehlerbehebungen und Verbesserungen in der Benutzeroberfläche vorgenommen.

This commit is contained in:
Torsten Schulz (local)
2025-08-22 15:47:16 +02:00
parent e827964688
commit 8bd05e4e38
40 changed files with 4670 additions and 346 deletions

View File

@@ -1,41 +1,80 @@
<template>
<div class="main">
<button class="menu-toggle" @click="toggleMenu">
{{ isMenuOpen ? 'Menü schließen' : 'Menü öffnen' }}
</button>
<header class="app-header">
<h1>Trainingstagebuch</h1>
</header>
<div v-if="isAuthenticated" class="navigation" :class="{ 'menu-open': isMenuOpen }">
<div class="club-selector">
<div>
Verein:
<select v-model="selectedClub">
<option value="">---</option>
<option value="new">Neuer Verein</option>
<option v-for="club in clubs" :key="club.id" :value="club.id">{{ club.name }}</option>
</select>
<button @click="loadClub">-&gt;</button>
<div class="app-container">
<aside v-if="isAuthenticated" class="sidebar">
<div class="sidebar-content">
<div class="club-selector card">
<h3 class="card-title">Verein auswählen</h3>
<div class="select-group">
<select v-model="selectedClub" class="club-select">
<option value="">Verein wählen...</option>
<option value="new">Neuer Verein</option>
<option v-for="club in clubs" :key="club.id" :value="club.id">{{ club.name }}</option>
</select>
<button @click="loadClub" class="btn-primary" :disabled="!selectedClub">
<span>Laden</span>
</button>
</div>
</div>
<nav v-if="selectedClub" class="nav-menu">
<div class="nav-section">
<h4 class="nav-title">Verwaltung</h4>
<a href="/members" class="nav-link">
<span class="nav-icon">👥</span>
Mitglieder
</a>
<a href="/diary" class="nav-link">
<span class="nav-icon">📝</span>
Tagebuch
</a>
<a href="/pending-approvals" class="nav-link">
<span class="nav-icon"></span>
Freigaben
</a>
<a href="/training-stats" class="nav-link">
<span class="nav-icon">📊</span>
Trainings-Statistik
</a>
</div>
<div class="nav-section">
<h4 class="nav-title">Organisation</h4>
<a href="/schedule" class="nav-link">
<span class="nav-icon">📅</span>
Spielpläne
</a>
<a href="/tournaments" class="nav-link">
<span class="nav-icon">🏆</span>
Turniere
</a>
</div>
</nav>
<div class="sidebar-footer">
<button @click="logout()" class="btn-secondary logout-btn">
<span class="nav-icon">🚪</span>
Ausloggen
</button>
</div>
</div>
</aside>
<div v-else class="auth-nav">
<div class="auth-links">
<a href="/login" class="btn-primary">Einloggen</a>
<a href="/register" class="btn-secondary">Registrieren</a>
</div>
</div>
<div v-if="selectedClub" class="nav-links">
<a href="/members">Mitglieder</a>
<a href="/diary">Tagebuch</a>
<a href="/pending-approvals">Freigaben</a>
<a href="/schedule">Spielpläne</a>
<a href="/tournaments">Turniere</a>
</div>
<div class="logout-btn">
<button @click="logout()">Ausloggen</button>
</div>
<main class="main-content">
<router-view class="content fade-in"></router-view>
</main>
</div>
<div v-else class="navigation">
<a href="/login">Einloggen</a>
<a href="/register">Registrieren</a>
</div>
<router-view class="content"></router-view>
</div>
</template>
@@ -48,7 +87,6 @@ export default {
data() {
return {
selectedClub: null,
isMenuOpen: false,
sessionInterval: null,
};
},
@@ -75,10 +113,6 @@ export default {
this.$router.push(`/showclub/${this.currentClub}`);
},
toggleMenu() {
this.isMenuOpen = !this.isMenuOpen;
},
async checkSession() {
try {
const response = await apiClient.get('/session/status');
@@ -86,8 +120,9 @@ export default {
this.handleLogout();
}
} catch (error) {
console.error('Session check failed:', error);
this.handleLogout();
this.isAuthenticated = false;
this.username = '';
this.currentClub = '';
}
},
@@ -119,159 +154,343 @@ export default {
</script>
<style scoped>
/* Main Container */
.main {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
height: 100vh;
overflow: hidden;
}
.navigation {
background-color: #e0f0e8;
display: flex;
flex-direction: column;
padding: 0.5rem;
}
.club-selector,
.logout-btn {
margin-bottom: 1rem;
}
.nav-links {
display: flex;
flex-direction: column;
margin-bottom: 1rem;
}
.navigation>a {
text-decoration: none;
margin: 0.3em 0;
color: #a07040;
}
.content {
flex: 1;
padding: 0.5em;
overflow: auto;
}
button {
padding: 0.3em 0.6em;
background-color: #a07040;
/* Header */
.app-header {
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: white;
border: none;
cursor: pointer;
box-shadow: var(--shadow-medium);
position: relative;
z-index: 1000;
flex-shrink: 0;
}
button:hover {
background-color: #804b29;
.header-content {
display: flex;
justify-content: center;
align-items: center;
padding: 0 0.75rem;
height: 3rem;
}
select {
padding: 0.3em;
border: 1px solid #ccc;
/* Styling für das erste h1 (aus main.scss) - Design vom zweiten, aber ursprüngliche Schriftgröße */
.app-header h1 {
margin: 0;
font-weight: 700;
color: white;
text-align: center;
/* Schriftgröße bleibt wie in der main.scss definiert */
}
/* Menü-Toggle-Button nur auf kleinen Bildschirmen anzeigen */
.menu-toggle {
/* App Container */
.app-container {
display: flex;
flex: 1;
overflow: hidden;
min-height: 0;
}
/* Sidebar */
.sidebar {
width: 280px;
background: white;
border-right: 1px solid var(--border-color);
box-shadow: var(--shadow-small);
overflow-y: auto;
flex-shrink: 0;
display: flex;
flex-direction: column;
}
.sidebar-content {
padding: 0.75rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
flex: 1;
min-height: 0;
}
.club-selector {
padding: 0.75rem;
margin-bottom: 0.5rem;
flex-shrink: 0;
}
.club-selector .card-title {
font-size: 0.875rem;
margin-bottom: 0.5rem;
color: var(--text-color);
font-weight: 600;
}
.select-group {
display: flex;
gap: 0.375rem;
align-items: center;
}
.club-select {
flex: 1;
padding: 0.375rem 0.5rem;
border: 1px solid var(--border-color);
border-radius: var(--border-radius-small);
font-size: 0.75rem;
background: white;
color: var(--text-color);
}
.select-group .btn-primary {
padding: 0.375rem 0.5rem;
font-size: 0.75rem;
white-space: nowrap;
}
/* Navigation Menu */
.nav-menu {
display: flex;
flex-direction: column;
gap: 0.75rem;
flex: 1;
min-height: 0;
overflow-y: auto;
}
.nav-section {
display: flex;
flex-direction: column;
gap: 0.25rem;
flex-shrink: 0;
}
.nav-title {
font-size: 0.75rem;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.025em;
margin-bottom: 0.25rem;
padding: 0 0.25rem;
}
.nav-link {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.5rem;
color: var(--text-color);
text-decoration: none;
border-radius: var(--border-radius-small);
transition: all var(--transition-fast);
font-size: 0.75rem;
}
.nav-link:hover {
background: var(--primary-light);
color: var(--primary-color);
transform: translateX(0.125rem);
}
.nav-icon {
font-size: 0.875rem;
width: 1rem;
text-align: center;
}
/* Sidebar Footer */
.sidebar-footer {
margin-top: auto;
padding-top: 0.75rem;
border-top: 1px solid var(--border-color);
flex-shrink: 0;
}
.logout-btn {
width: 100%;
padding: 0.5rem;
font-size: 0.75rem;
justify-content: center;
}
/* Auth Navigation */
.auth-nav {
width: 260px;
background: white;
border-right: 1px solid var(--border-color);
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
}
.auth-links {
display: flex;
flex-direction: column;
gap: 0.75rem;
width: 100%;
}
.auth-links a {
text-align: center;
padding: 0.75rem;
border-radius: var(--border-radius);
text-decoration: none;
font-weight: 500;
transition: var(--transition);
}
.auth-links a:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-medium);
}
.auth-links a::after {
display: none;
}
/* Main Content */
.main-content {
flex: 1;
overflow-y: auto;
background: var(--background-light);
min-height: 0;
}
.content {
padding: 1.5rem;
min-height: 100%;
}
/* Responsive Design */
@media (min-width: 768px) {
.main {
flex-direction: row;
@media (max-width: 1024px) {
.sidebar {
width: 240px;
}
.navigation {
width: 13em;
padding: 1rem;
}
.content {
padding: 1rem;
height: calc(100% - 2.5rem);
}
.nav-links {
flex-direction: column;
}
.nav-links>a {
margin: 0 1em;
}
select {
width: 100%;
padding: 1.25rem;
}
}
@media (max-width: 767px) {
.main {
flex-direction: column;
@media (max-width: 768px) {
.sidebar {
width: 220px;
}
.navigation {
display: none;
flex-direction: column;
width: 100%;
}
/* Das Menü anzeigen, wenn es geöffnet ist */
.menu-open {
display: flex;
}
.content {
padding: 0.75rem;
}
.header-content {
padding: 0 0.5rem;
}
.app-header h1 {
font-size: 1.125rem;
}
.sidebar-content {
padding: 0.5rem;
}
.nav-links {
display: block;
}
.nav-links>a {
display: block;
margin: 0.5em 0;
}
/* Menü-Toggle-Button nur auf kleinen Bildschirmen */
.menu-toggle {
display: block;
background-color: #a07040;
color: white;
border: none;
padding: 0.5em;
margin: 0.5em;
cursor: pointer;
}
.menu-toggle:hover {
background-color: #804b29;
.main-content {
overflow-y: auto;
}
}
@media (max-width: 480px) {
.navigation {
font-size: 1.5em;
}
select {
.sidebar {
width: 100%;
position: fixed;
top: 3rem;
left: 0;
height: calc(100vh - 3rem);
z-index: 999;
}
button {
width: 100%;
margin-top: 0.5em;
.content {
padding: 0.625rem;
}
.nav-links>a {
font-size: 0.85em;
padding: 0.5em;
border-bottom: 1px solid #ddd;
.header-content {
padding: 0 0.5rem;
}
.app-header h1 {
font-size: 1rem;
}
.sidebar-content {
padding: 0.5rem;
}
.main-content {
margin-left: 0;
overflow-y: auto;
}
}
/* Button-Varianten */
.btn-primary {
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: white;
border: none;
border-radius: var(--border-radius);
padding: 0.5rem 1rem;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
box-shadow: var(--shadow-light);
transition: var(--transition);
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 2.25rem;
}
.btn-primary:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-medium);
color: white;
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
box-shadow: var(--shadow-light);
}
.btn-secondary {
background: white;
color: var(--secondary-color);
border: 1.5px solid var(--secondary-color);
border-radius: var(--border-radius);
padding: 0.5rem 1rem;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
box-shadow: var(--shadow-light);
transition: var(--transition);
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 2.25rem;
}
.btn-secondary:hover {
background: var(--secondary-color);
color: white;
transform: translateY(-1px);
box-shadow: var(--shadow-medium);
}
</style>

View File

@@ -0,0 +1,500 @@
/* Moderne UI-Komponenten für TrainingsTagebuch */
/* Alert-Komponenten */
.alert {
padding: 0.75rem 1.25rem;
margin: 0.75rem 0;
border: 1px solid transparent;
border-radius: var(--border-radius);
position: relative;
display: flex;
align-items: flex-start;
gap: 0.625rem;
}
.alert-icon {
font-size: 1.125rem;
flex-shrink: 0;
margin-top: 0.125rem;
}
.alert-content {
flex: 1;
}
.alert-title {
font-weight: 600;
margin: 0 0 0.125rem 0;
font-size: 0.9rem;
}
.alert-message {
margin: 0;
line-height: 1.4;
}
.alert-success {
background-color: rgba(40, 167, 69, 0.08);
border-color: rgba(40, 167, 69, 0.25);
color: #155724;
}
.alert-info {
background-color: rgba(23, 162, 184, 0.08);
border-color: rgba(23, 162, 184, 0.25);
color: #0c5460;
}
.alert-warning {
background-color: rgba(255, 193, 7, 0.08);
border-color: rgba(255, 193, 7, 0.25);
color: #856404;
}
.alert-danger {
background-color: rgba(220, 53, 69, 0.08);
border-color: rgba(220, 53, 69, 0.25);
color: #721c24;
}
/* Badge-Komponenten */
.badge {
display: inline-flex;
align-items: center;
padding: 0.125rem 0.625rem;
font-size: 0.7rem;
font-weight: 500;
line-height: 1;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: 9999px;
background-color: var(--text-light);
color: white;
}
.badge-primary {
background-color: var(--primary-color);
}
.badge-secondary {
background-color: var(--secondary-color);
}
.badge-success {
background-color: #28a745;
}
.badge-danger {
background-color: #dc3545;
}
.badge-warning {
background-color: #ffc107;
color: #212529;
}
.badge-info {
background-color: #17a2b8;
}
.badge-light {
background-color: #f8f9fa;
color: #212529;
}
.badge-dark {
background-color: #343a40;
}
/* Progress-Bar */
.progress {
height: 0.625rem;
background-color: #e9ecef;
border-radius: var(--border-radius);
overflow: hidden;
margin: 0.75rem 0;
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, var(--primary-color), var(--primary-hover));
transition: width 0.5s ease;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 0.7rem;
font-weight: 500;
}
/* Tooltip */
.tooltip {
position: relative;
display: inline-block;
}
.tooltip .tooltip-text {
visibility: hidden;
width: 180px;
background-color: #333;
color: white;
text-align: center;
border-radius: var(--border-radius);
padding: 0.375rem;
position: absolute;
z-index: 1;
bottom: 125%;
left: 50%;
margin-left: -90px;
opacity: 0;
transition: opacity 0.25s;
font-size: 0.8rem;
line-height: 1.3;
}
.tooltip .tooltip-text::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
margin-left: -4px;
border-width: 4px;
border-style: solid;
border-color: #333 transparent transparent transparent;
}
.tooltip:hover .tooltip-text {
visibility: visible;
opacity: 1;
}
/* Modal-ähnliche Overlays */
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.4);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
opacity: 0;
visibility: hidden;
transition: all 0.25s ease;
}
.overlay.active {
opacity: 1;
visibility: visible;
}
.overlay-content {
background: white;
border-radius: var(--border-radius-large);
padding: 1.5rem;
max-width: 90vw;
max-height: 90vh;
overflow-y: auto;
box-shadow: var(--shadow-heavy);
transform: scale(0.9);
transition: transform 0.25s ease;
}
.overlay.active .overlay-content {
transform: scale(1);
}
/* Skeleton Loading */
.skeleton {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: loading 1.5s infinite;
border-radius: var(--border-radius);
}
.skeleton-text {
height: 0.875rem;
margin-bottom: 0.375rem;
}
.skeleton-text:last-child {
width: 60%;
}
.skeleton-avatar {
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
}
.skeleton-button {
height: 2.25rem;
width: 7rem;
}
@keyframes loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
/* Tabs */
.tabs {
border-bottom: 1px solid var(--border-color);
margin-bottom: 1.5rem;
}
.tab-list {
display: flex;
list-style: none;
margin: 0;
padding: 0;
gap: 0.375rem;
}
.tab-item {
margin: 0;
}
.tab-button {
background: none;
border: none;
padding: 0.625rem 1.25rem;
color: var(--text-secondary);
cursor: pointer;
border-bottom: 2px solid transparent;
transition: var(--transition);
font-weight: 500;
min-height: auto;
margin: 0;
box-shadow: none;
}
.tab-button:hover {
color: var(--primary-color);
transform: none;
box-shadow: none;
}
.tab-button.active {
color: var(--primary-color);
border-bottom-color: var(--primary-color);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* Accordion */
.accordion {
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
overflow: hidden;
}
.accordion-item {
border-bottom: 1px solid var(--border-color);
}
.accordion-item:last-child {
border-bottom: none;
}
.accordion-header {
background: none;
border: none;
width: 100%;
padding: 0.875rem 1.25rem;
text-align: left;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
font-weight: 500;
color: var(--text-primary);
transition: var(--transition);
min-height: auto;
margin: 0;
box-shadow: none;
}
.accordion-header:hover {
background-color: rgba(76, 175, 80, 0.04);
transform: none;
box-shadow: none;
}
.accordion-icon {
transition: transform 0.25s ease;
font-size: 1.125rem;
color: var(--text-light);
}
.accordion-header.active .accordion-icon {
transform: rotate(180deg);
}
.accordion-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.25s ease;
}
.accordion-content.active {
max-height: 400px;
}
.accordion-body {
padding: 0 1.25rem 1.25rem;
color: var(--text-secondary);
line-height: 1.5;
}
/* Breadcrumb */
.breadcrumb {
display: flex;
align-items: center;
gap: 0.375rem;
margin: 0.75rem 0;
font-size: 0.8rem;
color: var(--text-light);
}
.breadcrumb-item {
display: flex;
align-items: center;
}
.breadcrumb-separator {
margin: 0 0.375rem;
color: var(--text-light);
}
.breadcrumb-link {
color: var(--primary-color);
text-decoration: none;
transition: var(--transition);
}
.breadcrumb-link:hover {
color: var(--primary-hover);
text-decoration: underline;
}
.breadcrumb-current {
color: var(--text-secondary);
font-weight: 500;
}
/* Pagination */
.pagination {
display: flex;
align-items: center;
justify-content: center;
gap: 0.125rem;
margin: 1.5rem 0;
}
.page-item {
list-style: none;
margin: 0;
}
.page-link {
display: flex;
align-items: center;
justify-content: center;
width: 2.25rem;
height: 2.25rem;
border: 1px solid var(--border-color);
background: white;
color: var(--text-primary);
text-decoration: none;
border-radius: var(--border-radius);
transition: var(--transition);
font-weight: 500;
}
.page-link:hover {
background-color: var(--primary-color);
color: white;
border-color: var(--primary-color);
transform: translateY(-1px);
box-shadow: var(--shadow-light);
}
.page-link.active {
background-color: var(--primary-color);
color: white;
border-color: var(--primary-color);
}
.page-link.disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
/* Responsive Design für Komponenten */
@media (max-width: 768px) {
.overlay-content {
margin: 0.75rem;
padding: 1.25rem;
}
.tab-list {
flex-wrap: wrap;
}
.tab-button {
padding: 0.5rem 0.875rem;
font-size: 0.8rem;
}
.accordion-header {
padding: 0.75rem 1rem;
}
.accordion-body {
padding: 0 1rem 1rem;
}
.pagination {
gap: 0.125rem;
}
.page-link {
width: 2rem;
height: 2rem;
font-size: 0.8rem;
}
}
@media (max-width: 480px) {
.alert {
padding: 0.625rem 0.875rem;
flex-direction: column;
align-items: flex-start;
gap: 0.375rem;
}
.tooltip .tooltip-text {
width: 140px;
margin-left: -70px;
}
.overlay-content {
margin: 0.375rem;
padding: 0.875rem;
}
}

View File

@@ -1,69 +1,529 @@
/* Import der Komponenten */
@import './components.scss';
/* Modernes, frisches Design für TrainingsTagebuch */
:root {
/* Bestehende Farben beibehalten */
--primary-color: #4CAF50;
--primary-hover: #45a049;
--secondary-color: #a07040;
--secondary-hover: #804b29;
--danger-color: #dc3545;
--danger-hover: #c82333;
--nav-bg: #e0f0e8;
--text-primary: #333;
--text-secondary: #666;
--text-light: #999;
--text-muted: #888;
--bg-light: #f8f9fa;
--border-color: #e9ecef;
--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);
--border-radius: 6px;
--border-radius-large: 8px;
--transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
html, body {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
width: 100%;
height: 100%;
padding: 0;
margin: 0;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
line-height: 1.5;
color: var(--text-primary);
background-color: var(--bg-light);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
display: flex;
flex-direction: column;
}
h1 {
margin: 0;
height: 3rem;
padding: 0 0.5rem;
text-align: center;
background-color: #f0f0f0;
color: #4CAF50;
text-shadow: 2px 2px 3px #a0a0a0;
display: flex;
flex-direction: column;
}
h2 {
margin: 0;
margin: 0 0 0.75rem 0;
font-size: 1.5rem;
font-weight: 600;
color: var(--text-primary);
position: relative;
padding-bottom: 0.375rem;
}
h2::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 2.5rem;
height: 2px;
background: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
border-radius: 1px;
}
h3 {
font-size: 1.125rem;
font-weight: 600;
color: var(--text-primary);
margin: 1.25rem 0 0.5rem 0;
}
#app {
flex: 1;
width: 100%;
height: 100%;
overflow: hidden;
flex: 1;
width: 100%;
height: 100%;
overflow: hidden;
}
/* Kompaktere Button-Styles */
button {
background-color: #4CAF50;
color: white;
border: 1px solid #4CAF50;
border-radius: 0;
padding: 2px 5px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
cursor: pointer;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2);
transition: background-color 0.3s ease;
margin: 2px;
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: white;
border: none;
border-radius: var(--border-radius);
padding: 0.5rem 1rem;
text-align: center;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
box-shadow: var(--shadow-light);
transition: var(--transition);
margin: 0.125rem;
position: relative;
overflow: hidden;
min-height: 2.25rem;
}
button::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.15), transparent);
transition: left 0.4s;
}
button:hover::before {
left: 100%;
}
button:hover {
background-color: #45a049;
transform: translateY(-1px);
box-shadow: var(--shadow-medium);
}
button:active {
transform: translateY(0);
box-shadow: var(--shadow-light);
}
button.cancel-action {
background-color: white;
color: #4CAF50;
border: 1px solid #4CAF50;
border-radius: 0;
padding: 2px 5px; text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
cursor: pointer;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2);
transition: background-color 0.3s ease, color 0.3s ease;
background: white;
color: var(--primary-color);
border: 1.5px solid var(--primary-color);
box-shadow: var(--shadow-light);
}
button.cancel-action:hover {
background-color: #f2f2f2;
color: #45a049;
background: var(--primary-color);
color: white;
transform: translateY(-1px);
box-shadow: var(--shadow-medium);
}
/* Mülleimer-Buttons (Delete-Buttons) */
button.delete-btn,
button[onclick*="delete"],
button[onclick*="remove"] {
background: white;
color: var(--danger-color);
border: 1.5px solid var(--danger-color);
box-shadow: var(--shadow-light);
transition: var(--transition);
}
button.delete-btn:hover,
button[onclick*="delete"]:hover,
button[onclick*="remove"]:hover {
background: var(--danger-color);
color: white;
transform: translateY(-1px);
box-shadow: var(--shadow-medium);
}
/* Spezielle Styles für Mülleimer-Symbol */
button.trash-btn {
background: white !important;
color: var(--danger-color) !important;
border: 1.5px solid var(--danger-color) !important;
box-shadow: var(--shadow-light);
transition: var(--transition);
min-width: 2rem;
min-height: 2rem;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 1rem;
}
button.trash-btn:hover {
background: var(--danger-color) !important;
color: white !important;
transform: translateY(-1px);
box-shadow: var(--shadow-medium);
}
/* Sekundäre Buttons */
button.secondary {
background: linear-gradient(135deg, var(--secondary-color), var(--secondary-hover));
}
button.secondary:hover {
box-shadow: var(--shadow-medium);
}
/* Kleine Buttons */
button.small {
padding: 0.375rem 0.75rem;
font-size: 0.8rem;
min-height: 1.875rem;
}
/* Große Buttons */
button.large {
padding: 0.75rem 1.5rem;
font-size: 0.9rem;
min-height: 2.75rem;
}
/* Icon-Buttons */
button.icon {
width: 2.25rem;
height: 2.25rem;
padding: 0;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
}
/* Kompaktere Form-Elemente */
input, select, textarea {
width: 100%;
padding: 0.5rem 0.75rem;
border: 1.5px solid var(--border-color);
border-radius: var(--border-radius);
font-size: 0.85rem;
transition: var(--transition);
background: white;
color: var(--text-primary);
box-sizing: border-box;
}
/* Spezielle Styles für Checkboxen */
input[type="checkbox"] {
width: auto;
min-width: 1rem;
height: 1rem;
margin: 0 0.5rem 0 0;
padding: 0;
border: 1.5px solid var(--border-color);
border-radius: 3px;
background: white;
cursor: pointer;
flex-shrink: 0;
vertical-align: middle;
}
input[type="checkbox"]:checked {
background: var(--primary-color);
border-color: var(--primary-color);
position: relative;
}
input[type="checkbox"]:checked::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 0.75rem;
font-weight: bold;
}
/* Label-Styles für Checkboxen */
label {
display: inline-flex;
align-items: center;
cursor: pointer;
margin: 0.25rem 0;
font-size: 0.85rem;
color: var(--text-primary);
line-height: 1.4;
vertical-align: middle;
}
label span {
margin-right: 0.5rem;
}
/* Checkbox-Container für bessere Ausrichtung */
.checkbox-group {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
align-items: center;
}
.checkbox-item {
display: inline-flex;
align-items: center;
margin: 0.25rem 0;
vertical-align: middle;
}
/* Spezielle Anpassungen für Listen mit Checkboxen */
ul li.checkbox-item {
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0.25rem 0;
}
ul li.checkbox-item label {
margin: 0;
flex: 1;
}
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);
}
input:hover, select:hover, textarea:hover {
border-color: var(--primary-color);
}
/* Kompaktere Karten */
.card {
background: white;
border-radius: var(--border-radius-large);
padding: 1rem;
margin: 0.75rem 0;
box-shadow: var(--shadow-light);
border: 1px solid var(--border-color);
transition: var(--transition);
}
.card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-medium);
}
.card-header {
border-bottom: 1px solid var(--border-color);
padding-bottom: 0.75rem;
margin-bottom: 0.75rem;
}
.card-title {
font-size: 1.125rem;
font-weight: 600;
color: var(--text-primary);
margin: 0;
}
.card-body {
color: var(--text-secondary);
line-height: 1.5;
}
/* Kompaktere Tabellen */
table {
width: 100%;
border-collapse: collapse;
background: white;
border-radius: var(--border-radius);
overflow: hidden;
box-shadow: var(--shadow-light);
margin: 0.75rem 0;
}
th, td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
th {
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: white;
font-weight: 600;
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 0.4px;
}
tr:hover {
background-color: rgba(76, 175, 80, 0.03);
}
/* Kompaktere Listen */
ul, ol {
padding-left: 1.25rem;
}
li {
margin: 0.375rem 0;
color: var(--text-secondary);
}
/* Links */
a {
color: var(--primary-color);
text-decoration: none;
transition: var(--transition);
position: relative;
}
a:hover {
color: var(--primary-hover);
}
a::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
width: 0;
height: 1px;
background: var(--primary-color);
transition: width 0.25s ease;
}
a:hover::after {
width: 100%;
}
/* Utility-Klassen */
.pointer {
cursor: pointer;
cursor: pointer;
}
.text-center {
text-align: center;
}
.text-left {
text-align: left;
}
.text-right {
text-align: right;
}
.mb-1 { margin-bottom: 0.375rem; }
.mb-2 { margin-bottom: 0.75rem; }
.mb-3 { margin-bottom: 1.125rem; }
.mb-4 { margin-bottom: 1.5rem; }
.mt-1 { margin-top: 0.375rem; }
.mt-2 { margin-top: 0.75rem; }
.mt-3 { margin-top: 1.125rem; }
.mt-4 { margin-top: 1.5rem; }
.p-1 { padding: 0.375rem; }
.p-2 { padding: 0.75rem; }
.p-3 { padding: 1.125rem; }
.p-4 { padding: 1.5rem; }
/* Responsive Design */
@media (max-width: 768px) {
html {
font-size: 13px;
}
button {
padding: 0.4rem 0.8rem;
font-size: 0.8rem;
}
.card {
padding: 0.75rem;
margin: 0.5rem 0;
}
h1 {
height: 2.75rem;
font-size: 1.125rem;
}
h2 {
font-size: 1.375rem;
}
}
/* Animationen */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(15px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in {
animation: fadeIn 0.5s ease-out;
}
@keyframes slideIn {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0);
}
}
.slide-in {
animation: slideIn 0.3s ease-out;
}
/* Loading-States */
.loading {
opacity: 0.6;
pointer-events: none;
}
.loading::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 16px;
height: 16px;
margin: -8px 0 0 -8px;
border: 2px solid var(--primary-color);
border-top: 2px solid transparent;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

View File

@@ -243,7 +243,6 @@ class PDFGenerator {
addTable(tableId, highlightName = '') {
this.pdf.setFontSize(11);
console.log(highlightName);
autoTable(this.pdf, {
html: `#${tableId}`,
startY: this.cursorY,

View File

@@ -10,6 +10,7 @@ import DiaryView from './views/DiaryView.vue';
import PendingApprovalsView from './views/PendingApprovalsView.vue';
import ScheduleView from './views/ScheduleView.vue';
import TournamentsView from './views/TournamentsView.vue';
import TrainingStatsView from './views/TrainingStatsView.vue';
const routes = [
{ path: '/register', component: Register },
@@ -23,6 +24,7 @@ const routes = [
{ path: '/pending-approvals', component: PendingApprovalsView},
{ path: '/schedule', component: ScheduleView},
{ path: '/tournaments', component: TournamentsView },
{ path: '/training-stats', component: TrainingStatsView },
];
const router = createRouter({

View File

@@ -11,8 +11,7 @@ const store = createStore({
try {
return JSON.parse(localStorage.getItem('clubs')) || [];
} catch (e) {
console.error('Error parsing clubs from localStorage:', e);
return [];
this.clubs = [];
}
})(),
},

View File

@@ -1,79 +1,232 @@
/* Globale Styles für TrainingsTagebuch */
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
color-scheme: light;
color: #333;
background-color: #f8f9fa;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
* {
box-sizing: border-box;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
padding: 0;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.card {
padding: 2em;
background-color: inherit;
color: inherit;
line-height: inherit;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
width: 100%;
height: 100vh;
margin: 0;
padding: 0;
text-align: left;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
/* Utility-Klassen für das neue Design */
.text-center { text-align: center; }
.text-left { text-align: left; }
.text-right { text-align: right; }
.mb-0 { margin-bottom: 0; }
.mb-1 { margin-bottom: 0.25rem; }
.mb-2 { margin-bottom: 0.5rem; }
.mb-3 { margin-bottom: 1rem; }
.mb-4 { margin-bottom: 1.5rem; }
.mb-5 { margin-bottom: 3rem; }
.mt-0 { margin-top: 0; }
.mt-1 { margin-top: 0.25rem; }
.mt-2 { margin-top: 0.5rem; }
.mt-3 { margin-top: 1rem; }
.mt-4 { margin-top: 1.5rem; }
.mt-5 { margin-top: 3rem; }
.p-0 { padding: 0; }
.p-1 { padding: 0.25rem; }
.p-2 { padding: 0.5rem; }
.p-3 { padding: 1rem; }
.p-4 { padding: 1.5rem; }
.p-5 { padding: 3rem; }
/* Responsive Container */
.container {
width: 100%;
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
}
.container-fluid {
width: 100%;
padding: 0 1rem;
}
/* Grid System */
.row {
display: flex;
flex-wrap: wrap;
margin: 0 -0.5rem;
}
.col {
flex: 1;
padding: 0 0.5rem;
}
.col-12 { flex: 0 0 100%; }
.col-6 { flex: 0 0 50%; }
.col-4 { flex: 0 0 33.333333%; }
.col-3 { flex: 0 0 25%; }
@media (max-width: 768px) {
.col-md-12 { flex: 0 0 100%; }
.col-md-6 { flex: 0 0 50%; }
.col-md-4 { flex: 0 0 33.333333%; }
.col-md-3 { flex: 0 0 25%; }
}
@media (max-width: 480px) {
.col-sm-12 { flex: 0 0 100%; }
.col-sm-6 { flex: 0 0 50%; }
.col-sm-4 { flex: 0 0 33.333333%; }
.col-sm-3 { flex: 0 0 25%; }
}
/* Flexbox Utilities */
.d-flex { display: flex; }
.d-inline-flex { display: inline-flex; }
.d-block { display: block; }
.d-inline-block { display: inline-block; }
.d-none { display: none; }
.flex-row { flex-direction: row; }
.flex-column { flex-direction: column; }
.flex-wrap { flex-wrap: wrap; }
.flex-nowrap { flex-wrap: nowrap; }
.justify-content-start { justify-content: flex-start; }
.justify-content-center { justify-content: center; }
.justify-content-end { justify-content: flex-end; }
.justify-content-between { justify-content: space-between; }
.justify-content-around { justify-content: space-around; }
.align-items-start { align-items: flex-start; }
.align-items-center { align-items: center; }
.align-items-end { align-items: flex-end; }
.align-items-stretch { align-items: stretch; }
.flex-1 { flex: 1; }
.flex-auto { flex: auto; }
.flex-none { flex: none; }
/* Spacing Utilities */
.gap-0 { gap: 0; }
.gap-1 { gap: 0.25rem; }
.gap-2 { gap: 0.5rem; }
.gap-3 { gap: 1rem; }
.gap-4 { gap: 1.5rem; }
.gap-5 { gap: 3rem; }
/* Border Utilities */
.border { border: 1px solid #e9ecef; }
.border-top { border-top: 1px solid #e9ecef; }
.border-right { border-right: 1px solid #e9ecef; }
.border-bottom { border-bottom: 1px solid #e9ecef; }
.border-left { border-left: 1px solid #e9ecef; }
.border-0 { border: 0; }
.rounded { border-radius: 0.375rem; }
.rounded-sm { border-radius: 0.25rem; }
.rounded-lg { border-radius: 0.5rem; }
.rounded-xl { border-radius: 0.75rem; }
/* Shadow Utilities */
.shadow-sm { box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); }
.shadow { box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); }
.shadow-md { box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); }
.shadow-lg { box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); }
.shadow-xl { box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); }
/* Position Utilities */
.position-relative { position: relative; }
.position-absolute { position: absolute; }
.position-fixed { position: fixed; }
.position-sticky { position: sticky; }
/* Overflow Utilities */
.overflow-hidden { overflow: hidden; }
.overflow-auto { overflow: auto; }
.overflow-scroll { overflow: scroll; }
/* Cursor Utilities */
.cursor-pointer { cursor: pointer; }
.cursor-default { cursor: default; }
.cursor-not-allowed { cursor: not-allowed; }
/* Text Utilities */
.text-primary { color: #4CAF50; }
.text-secondary { color: #a07040; }
.text-success { color: #28a745; }
.text-danger { color: #dc3545; }
.text-warning { color: #ffc107; }
.text-info { color: #17a2b8; }
.text-light { color: #6c757d; }
.text-dark { color: #343a40; }
.text-muted { color: #6c757d; }
.text-white { color: #ffffff; }
.text-black { color: #000000; }
.font-weight-light { font-weight: 300; }
.font-weight-normal { font-weight: 400; }
.font-weight-medium { font-weight: 500; }
.font-weight-semibold { font-weight: 600; }
.font-weight-bold { font-weight: 700; }
.font-size-sm { font-size: 0.875rem; }
.font-size-base { font-size: 1rem; }
.font-size-lg { font-size: 1.125rem; }
.font-size-xl { font-size: 1.25rem; }
/* Background Utilities */
.bg-primary { background-color: #4CAF50; }
.bg-secondary { background-color: #a07040; }
.bg-success { background-color: #28a745; }
.bg-danger { background-color: #dc3545; }
.bg-warning { background-color: #ffc107; }
.bg-info { background-color: #17a2b8; }
.bg-light { background-color: #f8f9fa; }
.bg-dark { background-color: #343a40; }
.bg-white { background-color: #ffffff; }
.bg-transparent { background-color: transparent; }
/* Responsive Utilities */
@media (max-width: 768px) {
.d-md-none { display: none; }
.d-md-block { display: block; }
.d-md-flex { display: flex; }
}
@media (max-width: 480px) {
.d-sm-none { display: none; }
.d-sm-block { display: block; }
.d-sm-flex { display: flex; }
}
/* Print Utilities */
@media print {
.d-print-none { display: none; }
.d-print-block { display: block; }
.d-print-flex { display: flex; }
}

View File

@@ -17,8 +17,7 @@
alert('Account activated! You can now log in.');
this.$router.push('/login');
} catch (error) {
console.error(error);
alert('Activation failed.');
alert('Aktivierung fehlgeschlagen');
}
},
},

View File

@@ -48,8 +48,7 @@ export default {
this.club = response.data;
this.accessAllowed = true;
} catch (error) {
console.error("Zugriff auf den Verein nicht gestattet", error);
this.accessAllowed = false;
alert('Zugriff auf den Verein nicht gestattet');
}
},
async loadOpenRequests() {
@@ -57,7 +56,7 @@ export default {
const response = await apiClient.get(`/clubmembers/notapproved/${this.currentClub}`);
this.openRequests = response.data;
} catch (error) {
console.error("Fehler beim Laden der offenen Anfragen", error);
alert('Fehler beim Laden der offenen Anfragen');
}
},
async requestAccess() {

View File

@@ -112,7 +112,7 @@
v-if="item.durationText && item.durationText.trim() !== ''"> ({{
item.durationText }})</span>
</td>
<td><button @click="removePlanItem(item.id)">Entfernen</button></td>
<td><button @click="removePlanItem(item.id)" class="trash-btn">🗑</button></td>
</tr>
<template v-for="groupItem in item.groupActivities">
<tr>
@@ -185,14 +185,17 @@
@remove="removeActivityTag" :allow-empty="false" @keydown.enter.prevent="addNewTagFromInput" />
<h3>Teilnehmer</h3>
<ul>
<li v-for="member in sortedMembers()" :key="member.id">
<input type="checkbox" :value="member.id" @change="toggleParticipant(member.id)"
:checked="isParticipant(member.id)">
<span class="clickable" @click="selectMember(member)"
:class="{ highlighted: selectedMember && selectedMember.id === member.id }">{{
member ? member.firstName : ''
}} {{
member ? member.lastName : '' }}</span>
<li v-for="member in sortedMembers()" :key="member.id" class="checkbox-item">
<label class="checkbox-item">
<input type="checkbox" :value="member.id" @change="toggleParticipant(member.id)"
:checked="isParticipant(member.id)">
<span class="clickable" @click="selectMember(member)"
:class="{ highlighted: selectedMember && selectedMember.id === member.id }">{{
member ? member.firstName : ''
}} {{
member ? member.lastName : ''
}}</span>
</label>
<span v-if="false" @click="openNotesModal(member)" class="clickable">📝</span>
<span @click="showPic(member)" class="img-icon" v-if="member.hasImage">&#x1F5BC;</span>
<span class="pointer" @click="openTagInfos(member)"></span>
@@ -243,7 +246,7 @@
</div>
<ul>
<li v-for="note in notes" :key="note.id">
<button @click="deleteNote(note.id)" class="cancel-action">Löschen</button>
<button @click="deleteNote(note.id)" class="trash-btn">🗑</button>
{{ note.content }}
</li>
</ul>
@@ -462,7 +465,6 @@ export default {
});
alert('Trainingszeiten erfolgreich aktualisiert.');
} catch (error) {
console.error('Fehler beim Aktualisieren der Trainingszeiten:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
@@ -492,7 +494,7 @@ export default {
const response = await apiClient.get('/predefined-activities');
this.predefinedActivities = response.data;
} catch (error) {
console.error('Fehler beim Laden der vordefinierten Aktivitäten:', error);
alert('Fehler beim Laden der vordefinierten Aktivitäten');
}
},
@@ -566,7 +568,6 @@ export default {
label: tag.tag.label
}));
} catch (error) {
console.error('Error loading member notes and tags:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
this.doMemberTagUpdates = true;
@@ -612,7 +613,6 @@ export default {
this.availableTags.push(newTag);
this.selectedActivityTags.push(newTag);
} catch (error) {
console.error('Fehler beim Hinzufügen eines neuen Tags:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
@@ -632,14 +632,12 @@ export default {
this.selectedMemberTags.push(newTag);
await this.linkTagToMemberAndDate(newTag);
} catch (error) {
console.error('Fehler beim Hinzufügen eines neuen Tags für das Mitglied:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
async linkTagToDiaryDate(tag) {
if (!tag || !tag.id) {
console.warn("Ungültiges Tag-Objekt:", tag);
return;
}
try {
@@ -649,7 +647,6 @@ export default {
tagId: tagId
});
} catch (error) {
console.error('Fehler beim Verknüpfen des Tags mit dem Trainingstag:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
@@ -663,7 +660,6 @@ export default {
tagId: tagId
});
} catch (error) {
console.error('Fehler beim Verknüpfen des Tags mit dem Mitglied und Datum:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
@@ -681,7 +677,6 @@ export default {
}
this.previousActivityTags = [...selectedTags];
} catch (error) {
console.error('Fehler beim Verknüpfen der Tags mit dem Trainingstag:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
@@ -698,7 +693,6 @@ export default {
}
this.previousMemberTags = [...this.selectedMemberTags];
} catch (error) {
console.error('Fehler beim Verknüpfen der Tags mit dem Mitglied und Datum:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
@@ -712,7 +706,6 @@ export default {
});
this.selectedMemberTags = this.selectedMemberTags.filter(tag => tag.id !== tagId);
} catch (error) {
console.error('Fehler beim Entfernen des Tags:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
@@ -726,7 +719,6 @@ export default {
});
this.notes = this.notes.filter(note => note.content !== noteContent);
} catch (error) {
console.error('Fehler beim Entfernen der Notiz:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
@@ -739,7 +731,6 @@ export default {
});
this.selectedActivityTags = this.selectedActivityTags.filter(t => t.id !== tagId);
} catch (error) {
console.error('Fehler beim Entfernen des Tags:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
@@ -785,7 +776,6 @@ export default {
this.trainingPlan = await apiClient.get(`/diary-date-activities/${this.currentClub}/${this.date.id}`).then(response => response.data);
this.calculateIntermediateTimes();
} catch (error) {
console.error('Fehler beim Hinzufügen des Planungsitems:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
@@ -797,7 +787,7 @@ export default {
});
this.calculateIntermediateTimes();
} catch (error) {
console.error('Fehler beim Aktualisieren der Planungs-Item-Gruppe:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
@@ -807,7 +797,7 @@ export default {
this.trainingPlan = this.trainingPlan.filter(item => item.id !== planItemId);
this.calculateIntermediateTimes();
} catch (error) {
console.error('Fehler beim Entfernen des Planungsitems:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
@@ -889,7 +879,6 @@ export default {
await apiClient.delete(`/diary-date-activities/${this.currentClub}/${planItemId}`);
this.trainingPlan = this.trainingPlan.filter(item => item.id !== planItemId);
} catch (error) {
console.error('Fehler beim Entfernen des Planungsitems:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
@@ -901,7 +890,6 @@ export default {
});
this.recalculateTimes();
} catch (error) {
console.error('Fehler beim Aktualisieren der Reihenfolge:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
@@ -1012,7 +1000,6 @@ export default {
});
this.editingGroupId = null;
} catch (error) {
console.error('Fehler beim Speichern der Gruppendaten:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
@@ -1021,7 +1008,6 @@ export default {
},
async openTagInfos(member) {
if (!member) {
console.warn("Member is undefined or null");
return;
}
this.showTagHistoryModal = true;

View File

@@ -1,8 +1,80 @@
<template>
<div>
<h2>Home</h2>
<p v-if="!isAuthenticated">Du bist nicht eingeloggt.<router-link to="/login">Einloggen</router-link> oder <router-link to="/register">Registrieren</router-link></p>
<p v-else>Herzlich Willkommen <button @click="logout">Ausloggen</button></p>
<div class="home-container">
<div class="welcome-section">
<div class="welcome-card card">
<div class="card-header">
<h2 class="card-title">Willkommen im TrainingsTagebuch</h2>
</div>
<div class="card-body">
<div v-if="!isAuthenticated" class="auth-message">
<p class="message-text">
Melde dich an, um deine Vereine und Trainingsaktivitäten zu verwalten.
</p>
<div class="auth-actions">
<router-link to="/login" class="btn-primary">
<span class="btn-icon">🔐</span>
Einloggen
</router-link>
<router-link to="/register" class="btn-secondary">
<span class="btn-icon">📝</span>
Registrieren
</router-link>
</div>
</div>
<div v-else class="user-welcome">
<div class="user-avatar">
<span class="avatar-icon">👋</span>
</div>
<p class="welcome-text">
Herzlich Willkommen zurück! Du bist erfolgreich eingeloggt.
</p>
<div class="user-actions">
<button @click="logout" class="btn-secondary">
<span class="btn-icon">🚪</span>
Ausloggen
</button>
</div>
</div>
</div>
</div>
</div>
<div v-if="isAuthenticated" class="features-section">
<h3 class="section-title">Was kannst du hier machen?</h3>
<div class="features-grid">
<div class="feature-card card">
<div class="feature-icon">👥</div>
<h4 class="feature-title">Mitglieder verwalten</h4>
<p class="feature-description">
Verwalte deine Vereinsmitglieder, erstelle Gruppen und behalte den Überblick über alle Teilnehmer.
</p>
</div>
<div class="feature-card card">
<div class="feature-icon">📝</div>
<h4 class="feature-title">Tagebuch führen</h4>
<p class="feature-description">
Dokumentiere deine Trainingsaktivitäten, Notizen und wichtige Ereignisse im Verein.
</p>
</div>
<div class="feature-card card">
<div class="feature-icon">📅</div>
<h4 class="feature-title">Spielpläne organisieren</h4>
<p class="feature-description">
Plane und organisiere Spiele, Turniere und andere Veranstaltungen für deinen Verein.
</p>
</div>
<div class="feature-card card">
<div class="feature-icon">🏆</div>
<h4 class="feature-title">Turniere verwalten</h4>
<p class="feature-description">
Erstelle und verwalte Turniere, Gruppen und Ergebnisse für deine Vereinsaktivitäten.
</p>
</div>
</div>
</div>
</div>
</template>
@@ -10,6 +82,7 @@
import { mapGetters, mapActions } from 'vuex';
export default {
name: 'Home',
computed: {
...mapGetters(['isAuthenticated']),
},
@@ -18,3 +91,196 @@ export default {
},
};
</script>
<style scoped>
.home-container {
max-width: 1200px;
margin: 0 auto;
}
.welcome-section {
margin-bottom: 2rem;
}
.welcome-card {
text-align: center;
background: linear-gradient(135deg, rgba(76, 175, 80, 0.05), rgba(160, 112, 64, 0.05));
border: 1px solid rgba(76, 175, 80, 0.2);
}
.card-title {
color: var(--primary-color);
font-size: 1.75rem;
margin-bottom: 0.75rem;
}
.auth-message, .user-welcome {
display: flex;
flex-direction: column;
align-items: center;
gap: 1.25rem;
}
.message-text, .welcome-text {
font-size: 1rem;
color: var(--text-secondary);
line-height: 1.5;
max-width: 600px;
margin: 0;
}
.auth-actions, .user-actions {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
justify-content: center;
}
.user-avatar {
width: 64px;
height: 64px;
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 0.75rem;
}
.avatar-icon {
font-size: 2rem;
}
.btn-icon {
margin-right: 0.375rem;
font-size: 1rem;
}
/* Features Section */
.features-section {
margin-top: 3rem;
}
.section-title {
text-align: center;
margin-bottom: 1.5rem;
color: var(--text-primary);
font-size: 1.5rem;
font-weight: 600;
}
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 1.5rem;
margin-top: 1.5rem;
}
.feature-card {
text-align: center;
padding: 1.5rem 1.25rem;
transition: var(--transition);
border: 1px solid var(--border-color);
position: relative;
overflow: hidden;
}
.feature-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
transform: scaleX(0);
transition: transform 0.25s ease;
}
.feature-card:hover::before {
transform: scaleX(1);
}
.feature-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-heavy);
}
.feature-icon {
font-size: 2.5rem;
margin-bottom: 0.75rem;
display: block;
}
.feature-title {
font-size: 1.125rem;
font-weight: 600;
color: var(--text-primary);
margin: 0 0 0.75rem 0;
}
.feature-description {
color: var(--text-secondary);
line-height: 1.5;
margin: 0;
font-size: 0.9rem;
}
/* Responsive Design */
@media (max-width: 768px) {
.home-container {
padding: 0 0.75rem;
}
.welcome-card {
margin: 0 0.375rem;
}
.card-title {
font-size: 1.5rem;
}
.features-grid {
grid-template-columns: 1fr;
gap: 1.25rem;
}
.feature-card {
padding: 1.25rem 1rem;
}
.auth-actions, .user-actions {
flex-direction: column;
width: 100%;
max-width: 280px;
}
.btn-primary, .btn-secondary {
width: 100%;
justify-content: center;
}
}
@media (max-width: 480px) {
.card-title {
font-size: 1.375rem;
}
.section-title {
font-size: 1.375rem;
}
.feature-icon {
font-size: 2.25rem;
}
.user-avatar {
width: 56px;
height: 56px;
}
.avatar-icon {
font-size: 1.75rem;
}
}
</style>

View File

@@ -30,8 +30,7 @@ export default {
await this.login({ token: response.data.token, username: this.email });
this.$router.push('/');
} catch (error) {
console.error(error);
alert('Login failed.');
alert('Login fehlgeschlagen');
}
},
},

View File

@@ -20,9 +20,9 @@
<label><span>Geburtsdatum:</span> <input type="date" v-model="newBirthdate"></label>
<label><span>Telefon-Nr.:</span> <input type="text" v-model="newPhone"></label>
<label><span>Email-Adresse:</span> <input type="email" v-model="newEmail"></label>
<label><span>Aktiv:</span> <input type="checkbox" v-model="newActive"></label>
<label><span>Pics in Internet erlaubt:</span> <input type="checkbox" v-model="newPicsInInternetAllowed"></label>
<label><span>Testmitgliedschaft:</span> <input type="checkbox" v-model="testMembership" </label>
<label class="checkbox-item"><span>Aktiv:</span> <input type="checkbox" v-model="newActive"></label>
<label class="checkbox-item"><span>Pics in Internet erlaubt:</span> <input type="checkbox" v-model="newPicsInInternetAllowed"></label>
<label class="checkbox-item"><span>Testmitgliedschaft:</span> <input type="checkbox" v-model="testMembership"></label>
<label><span>Bild:</span> <input type="file" @change="onFileSelected"></label>
<div v-if="memberImagePreview">
<img :src="memberImagePreview" alt="Vorschau des Mitgliedsbildes"
@@ -35,8 +35,11 @@
</div>
</div>
</div>
<div>
<input type="checkbox" v-model="showInactiveMembers"> Inaktive Mitglieder anzeigen
<div class="checkbox-group">
<label class="checkbox-item">
<input type="checkbox" v-model="showInactiveMembers">
<span>Inaktive Mitglieder anzeigen</span>
</label>
</div>
<div>
<table>
@@ -93,7 +96,7 @@
</div>
<ul>
<li v-for="note in notes" :key="note.id">
<button @click="deleteNote(note.id)" class="cancel-action">Löschen</button>
<button @click="deleteNote(note.id)" class="trash-btn">🗑</button>
{{ note.content }}
</li>
</ul>
@@ -211,7 +214,7 @@ export default {
response = await apiClient.post(`/clubmembers/set/${this.currentClub}`, memberData);
this.loadMembers();
} catch (error) {
console.error("Fehler beim Speichern des Mitglieds:", error);
alert('Fehler beim Speichern des Mitglieds');
return;
}
@@ -226,7 +229,7 @@ export default {
}
});
} catch (error) {
console.error("Fehler beim Hochladen des Bildes:", error);
// Kein Alert - es ist normal, dass nicht alle Mitglieder Bilder haben
}
}
@@ -234,7 +237,7 @@ export default {
this.memberFormIsOpen = false;
},
async editMember(member) {
console.log(member.birthDate);
const birthDate = member.birthDate;
this.memberToEdit = member;
this.memberFormIsOpen = true;
this.newFirstname = member.firstName;
@@ -244,7 +247,6 @@ export default {
this.newPhone = member.phone;
this.newEmail = member.email;
this.newActive = member.active;
const date = new Date(member.birthDate);
this.newBirthdate = date.toISOString().split('T')[0];
this.testMembership = member.testMembership;
this.newPicsInInternetAllowed = member.picsInInternetAllowed;
@@ -254,7 +256,7 @@ export default {
});
this.memberImagePreview = URL.createObjectURL(response.data);
} catch (error) {
console.error("Fehler beim Laden des Bildes:", error);
// Kein Alert - es ist normal, dass nicht alle Mitglieder Bilder haben
this.memberImagePreview = null;
}
},
@@ -308,7 +310,7 @@ export default {
const imageUrl = URL.createObjectURL(response.data);
member.imageUrl = imageUrl;
} catch (error) {
console.error("Failed to load member image:", error);
// Kein Alert - es ist normal, dass nicht alle Mitglieder Bilder haben
member.imageUrl = null;
}
},

View File

@@ -53,7 +53,7 @@ export default {
const response = await apiClient.get(`/clubs/pending/${this.currentClub}`);
this.pendingUsers = response.data.map(entry => entry.user);
} catch (error) {
console.error('Fehler beim Laden der ausstehenden Anfragen:', error);
alert('Fehler beim Laden der ausstehenden Anfragen');
}
},
async approveUser(userId) {
@@ -64,7 +64,7 @@ export default {
});
this.pendingUsers = this.pendingUsers.filter(user => user.id !== userId);
} catch (error) {
console.error('Fehler beim Genehmigen des Benutzers:', error);
alert('Fehler beim Genehmigen des Benutzers');
}
},
async rejectUser(userId) {
@@ -75,7 +75,7 @@ export default {
});
this.pendingUsers = this.pendingUsers.filter(user => user.id !== userId);
} catch (error) {
console.error('Fehler beim Ablehnen des Benutzers:', error);
alert('Fehler beim Ablehnen des Benutzers');
}
},
},

View File

@@ -26,8 +26,7 @@
await axios.post('/api/auth/register', { email: this.email, password: this.password });
alert('Registration successful! Please check your email to activate your account.');
} catch (error) {
console.error(error);
alert('Registration failed.');
alert('Registrierung fehlgeschlagen');
}
},
},

View File

@@ -104,8 +104,7 @@ export default {
this.closeImportModal();
this.loadLeagues();
} catch (error) {
console.error('Fehler beim Importieren der CSV:', error);
alert('Fehler beim Importieren der CSV.');
alert('Fehler beim Importieren der CSV-Datei');
}
},
async loadLeagues() {
@@ -114,7 +113,7 @@ export default {
const response = await apiClient.get(`/matches/leagues/current/${clubId}`);
this.leagues = response.data;
} catch (error) {
console.error('Failed to load leagues:', error);
alert('Fehler beim Laden der Ligen');
}
},
async loadMatchesForLeague(leagueId, leagueName) {
@@ -123,7 +122,7 @@ export default {
const response = await apiClient.get(`/matches/leagues/${this.currentClub}/matches/${leagueId}`);
this.matches = response.data;
} catch (error) {
console.error('Failed to load matches:', error);
alert('Fehler beim Laden der Matches');
this.matches = [];
}
},
@@ -158,14 +157,13 @@ export default {
const uniqueLocations = this.getUniqueLocations();
uniqueLocations.forEach((addressLines, clubName) => {
if (!clubName.includes(highlightName)) {
console.log(clubName, highlightName);
pdfGen.addAddress(clubName, addressLines);
}
});
pdfGen.save('Spielpläne.pdf');
} else {
console.error('No matches found to generate PDF.');
alert('Keine Matches gefunden, um PDF zu generieren.');
}
},
getUniqueLocations() {

View File

@@ -19,9 +19,9 @@
</div>
</div>
<div v-if="selectedDate !== 'new'" class="tournament-setup">
<label>
<label class="checkbox-item">
<input type="checkbox" v-model="isGroupTournament" @change="onModusChange" />
Spielen in Gruppen
<span>Spielen in Gruppen</span>
</label>
<section class="participants">
<h4>Teilnehmer</h4>
@@ -40,8 +40,8 @@
</select>
</label>
</template>
<button @click="removeParticipant(participant)" style="margin-left:0.5rem">
Entfernen
<button @click="removeParticipant(participant)" style="margin-left:0.5rem" class="trash-btn">
🗑
</button>
</li>
</ul>
@@ -102,8 +102,8 @@
<button @click="resetGroups">
Gruppen zurücksetzen
</button>
<button @click="resetMatches" style="margin-left:0.5rem">
Gruppenspiele löschen
<button @click="resetMatches" style="margin-left:0.5rem" class="trash-btn">
🗑 Gruppenspiele
</button>
</div>
</section>
@@ -190,8 +190,8 @@
</div>
<div v-if="showKnockout && canResetKnockout" class="ko-reset" style="margin-top:1rem">
<button @click="resetKnockout">
K.o.-Runde löschen
<button @click="resetKnockout" class="trash-btn">
🗑️ K.o.-Runde
</button>
</div>
<section v-if="showKnockout" class="ko-round">
@@ -448,13 +448,11 @@ export default {
) {
return `${x}:${y}`;
}
console.warn('Ungültiges Satz-Ergebnis:', s);
return null;
}
const num = Number(s);
if (isNaN(num)) {
console.warn('Ungültiges Ergebnisformat:', raw);
return null;
}
@@ -594,7 +592,7 @@ export default {
);
const updated = allRes.data.find(m2 => m2.id === match.id);
if (!updated) {
console.error('Konnte aktualisiertes Match nicht finden');
alert('Fehler beim Aktualisieren des Matches');
return;
}
match.tournamentResults = updated.tournamentResults || [];
@@ -785,8 +783,7 @@ export default {
});
await this.loadTournamentData();
} catch (err) {
console.error('Reset KO failed:', err);
alert(err.response?.data?.error || err.message);
alert('Fehler beim Zurücksetzen der K.o.-Runde');
}
}
}

View File

@@ -0,0 +1,427 @@
<template>
<div class="training-stats">
<h2>Trainings-Statistik</h2>
<div class="stats-overview">
<div class="stats-summary">
<div class="stat-card">
<h3>Aktive Mitglieder</h3>
<div class="stat-number">{{ activeMembers.length }}</div>
</div>
<div class="stat-card">
<h3>Durchschnittliche Teilnahme (12 Monate)</h3>
<div class="stat-number">{{ averageParticipation12Months.toFixed(1) }}</div>
</div>
<div class="stat-card">
<h3>Durchschnittliche Teilnahme (3 Monate)</h3>
<div class="stat-number">{{ averageParticipation3Months.toFixed(1) }}</div>
</div>
</div>
</div>
<div class="members-table-container">
<table class="members-table">
<thead>
<tr>
<th>Name</th>
<th>Geburtsdatum</th>
<th>Teilnahmen (12 Monate)</th>
<th>Teilnahmen (3 Monate)</th>
<th>Teilnahmen (Gesamt)</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
<tr v-for="member in activeMembers" :key="member.id" class="member-row">
<td>{{ member.firstName }} {{ member.lastName }}</td>
<td>{{ formatBirthdate(member.birthDate) }}</td>
<td>{{ member.participation12Months }}</td>
<td>{{ member.participation3Months }}</td>
<td>{{ member.participationTotal }}</td>
<td>
<button @click="showMemberDetails(member)" class="btn-primary btn-small">
Details anzeigen
</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Modal für Mitgliedsdetails -->
<div v-if="showDetailsModal" class="modal">
<div class="modal-content">
<span class="close" @click="closeDetailsModal">&times;</span>
<h3>Trainings-Details: {{ selectedMember.firstName }} {{ selectedMember.lastName }}</h3>
<div class="member-info">
<p><strong>Geburtsdatum:</strong> {{ formatBirthdate(selectedMember.birthDate) }}</p>
<p><strong>Geburtsjahr:</strong> {{ getBirthYear(selectedMember.birthDate) }}</p>
</div>
<div class="participation-summary">
<div class="summary-item">
<span class="label">Letzte 12 Monate:</span>
<span class="value">{{ selectedMember.participation12Months }}</span>
</div>
<div class="summary-item">
<span class="label">Letzte 3 Monate:</span>
<span class="value">{{ selectedMember.participation3Months }}</span>
</div>
<div class="summary-item">
<span class="label">Gesamt:</span>
<span class="value">{{ selectedMember.participationTotal }}</span>
</div>
</div>
<div class="training-details">
<h4>Trainingsteilnahmen (absteigend sortiert)</h4>
<div class="training-list">
<div v-for="training in selectedMember.trainingDetails" :key="training.id" class="training-item">
<div class="training-date">{{ formatDate(training.date) }}</div>
<div class="training-activity">{{ training.activityName }}</div>
<div class="training-time">{{ training.startTime }} - {{ training.endTime }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import apiClient from '../apiClient';
export default {
name: 'TrainingStatsView',
computed: {
...mapGetters(['isAuthenticated', 'currentClub']),
averageParticipation12Months() {
if (this.activeMembers.length === 0) return 0;
const total = this.activeMembers.reduce((sum, member) => sum + member.participation12Months, 0);
return total / this.activeMembers.length;
},
averageParticipation3Months() {
if (this.activeMembers.length === 0) return 0;
const total = this.activeMembers.reduce((sum, member) => sum + member.participation3Months, 0);
return total / this.activeMembers.length;
}
},
data() {
return {
activeMembers: [],
showDetailsModal: false,
selectedMember: {},
loading: false
};
},
async mounted() {
if (this.currentClub) {
await this.loadTrainingStats();
}
},
watch: {
currentClub: {
handler(newClub) {
if (newClub) {
this.loadTrainingStats();
}
},
immediate: true
}
},
methods: {
async loadTrainingStats() {
if (!this.currentClub) return;
this.loading = true;
try {
const response = await apiClient.get(`/training-stats/${this.currentClub}`);
this.activeMembers = response.data;
} catch (error) {
// Kein Alert - es ist normal, dass nicht alle Daten verfügbar sind
} finally {
this.loading = false;
}
},
showMemberDetails(member) {
this.selectedMember = member;
this.showDetailsModal = true;
},
closeDetailsModal() {
this.showDetailsModal = false;
this.selectedMember = {};
},
formatBirthdate(birthDate) {
if (!birthDate) return '-';
const date = new Date(birthDate);
return `${String(date.getDate()).padStart(2, '0')}.${String(date.getMonth() + 1).padStart(2, '0')}.${date.getFullYear()}`;
},
getBirthYear(birthDate) {
if (!birthDate) return '-';
const date = new Date(birthDate);
return date.getFullYear();
},
formatDate(dateString) {
const date = new Date(dateString);
return `${String(date.getDate()).padStart(2, '0')}.${String(date.getMonth() + 1).padStart(2, '0')}.${date.getFullYear()}`;
}
}
};
</script>
<style scoped>
.training-stats {
padding: 1.5rem;
max-width: 1200px;
margin: 0 auto;
}
.stats-overview {
margin-bottom: 2rem;
}
.stats-summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.stat-card {
background: white;
padding: 1.5rem;
border-radius: var(--border-radius-large);
box-shadow: var(--shadow-light);
text-align: center;
border: 1px solid var(--border-color);
}
.stat-card h3 {
margin: 0 0 1rem 0;
font-size: 0.875rem;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.025em;
}
.stat-number {
font-size: 2rem;
font-weight: 700;
color: var(--primary-color);
}
.members-table-container {
background: white;
border-radius: var(--border-radius-large);
box-shadow: var(--shadow-light);
overflow: hidden;
border: 1px solid var(--border-color);
}
.members-table {
width: 100%;
border-collapse: collapse;
}
.members-table th {
background: var(--bg-light);
padding: 1rem;
text-align: left;
font-weight: 600;
color: var(--text-primary);
border-bottom: 1px solid var(--border-color);
}
.members-table td {
padding: 1rem;
border-bottom: 1px solid var(--border-color);
vertical-align: middle;
}
.members-table tr:hover {
background: var(--bg-light);
}
.member-row {
cursor: pointer;
}
.member-row:hover {
background: var(--bg-light);
}
.btn-small {
padding: 0.375rem 0.75rem;
font-size: 0.75rem;
min-height: 1.875rem;
}
/* Modal Styles */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: var(--border-radius-large);
padding: 2rem;
max-width: 800px;
max-height: 90vh;
overflow-y: auto;
position: relative;
box-shadow: var(--shadow-heavy);
}
.close {
position: absolute;
top: 1rem;
right: 1.5rem;
font-size: 1.5rem;
cursor: pointer;
color: var(--text-secondary);
font-weight: bold;
}
.close:hover {
color: var(--text-primary);
}
.member-info {
background: var(--bg-light);
padding: 1rem;
border-radius: var(--border-radius);
margin-bottom: 1.5rem;
}
.member-info p {
margin: 0.5rem 0;
}
.participation-summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.summary-item {
background: white;
padding: 1rem;
border-radius: var(--border-radius);
border: 1px solid var(--border-color);
text-align: center;
}
.summary-item .label {
display: block;
font-size: 0.875rem;
color: var(--text-secondary);
margin-bottom: 0.5rem;
}
.summary-item .value {
display: block;
font-size: 1.5rem;
font-weight: 700;
color: var(--primary-color);
}
.training-details h4 {
margin: 0 0 1rem 0;
color: var(--text-primary);
}
.training-list {
max-height: 400px;
overflow-y: auto;
}
.training-item {
display: grid;
grid-template-columns: 120px 1fr 150px;
gap: 1rem;
padding: 0.75rem;
border-bottom: 1px solid var(--border-color);
align-items: center;
}
.training-item:last-child {
border-bottom: none;
}
.training-item:hover {
background: var(--bg-light);
}
.training-date {
font-weight: 600;
color: var(--text-primary);
}
.training-activity {
color: var(--text-secondary);
}
.training-time {
font-size: 0.875rem;
color: var(--text-muted);
text-align: right;
}
/* Responsive Design */
@media (max-width: 768px) {
.training-stats {
padding: 1rem;
}
.stats-summary {
grid-template-columns: 1fr;
}
.members-table {
font-size: 0.875rem;
}
.members-table th,
.members-table td {
padding: 0.75rem 0.5rem;
}
.modal-content {
margin: 1rem;
padding: 1.5rem;
}
.training-item {
grid-template-columns: 1fr;
gap: 0.5rem;
text-align: center;
}
.training-time {
text-align: center;
}
}
</style>