Erweitert die Backend- und Frontend-Funktionalität zur Unterstützung von Teams und Saisons. Fügt neue Routen für Team- und Club-Team-Management hinzu, aktualisiert die Match- und Team-Modelle zur Berücksichtigung von Saisons, und implementiert die Saison-Auswahl in der Benutzeroberfläche. Optimiert die Logik zur Abfrage von Ligen und Spielen basierend auf der ausgewählten Saison.

This commit is contained in:
Torsten Schulz (local)
2025-10-01 22:47:13 +02:00
parent f8f4d23c4e
commit a6493990d3
23 changed files with 2309 additions and 105 deletions

View File

@@ -65,6 +65,10 @@
<span class="nav-icon"></span>
Vordefinierte Aktivitäten
</a>
<a href="/team-management" class="nav-link">
<span class="nav-icon">👥</span>
Team-Verwaltung
</a>
</div>
</nav>
@@ -170,7 +174,7 @@ export default {
},
loadClub() {
this.setCurrentClub(this.currentClub);
this.setCurrentClub(this.selectedClub);
this.$router.push('/training-stats');
},

View File

@@ -0,0 +1,301 @@
<template>
<div class="season-selector">
<label>
<span>Saison:</span>
<div class="season-input-group">
<select v-model="selectedSeasonId" @change="onSeasonChange" class="season-select" :disabled="loading">
<option value="">{{ loading ? 'Lade...' : 'Saison wählen...' }}</option>
<option v-for="season in seasons" :key="season.id" :value="season.id">
{{ season.season }}
</option>
</select>
<button @click="showNewSeasonForm = !showNewSeasonForm" class="btn-add-season" title="Neue Saison hinzufügen">
{{ showNewSeasonForm ? '✕' : '+' }}
</button>
</div>
</label>
<div v-if="error" class="error-message">
{{ error }}
</div>
<div v-if="showNewSeasonForm" class="new-season-form">
<label>
<span>Neue Saison:</span>
<input
type="text"
v-model="newSeasonString"
placeholder="z.B. 2023/2024"
@keyup.enter="createSeason"
class="season-input"
>
</label>
<div class="form-actions">
<button @click="createSeason" :disabled="!isValidSeasonFormat" class="btn-create">
Erstellen
</button>
<button @click="cancelNewSeason" class="btn-cancel">
Abbrechen
</button>
</div>
</div>
</div>
</template>
<script>
import { ref, computed, onMounted, watch } from 'vue';
import { useStore } from 'vuex';
import apiClient from '../apiClient.js';
export default {
name: 'SeasonSelector',
props: {
modelValue: {
type: [String, Number],
default: null
},
showCurrentSeason: {
type: Boolean,
default: true
}
},
emits: ['update:modelValue', 'season-change'],
setup(props, { emit }) {
const store = useStore();
// Reactive data
const seasons = ref([]);
const selectedSeasonId = ref(props.modelValue);
const showNewSeasonForm = ref(false);
const newSeasonString = ref('');
const loading = ref(false);
const error = ref(null);
// Computed
const isValidSeasonFormat = computed(() => {
const seasonRegex = /^\d{4}\/\d{4}$/;
return seasonRegex.test(newSeasonString.value);
});
// Methods
const loadSeasons = async () => {
loading.value = true;
error.value = null;
try {
console.log('SeasonSelector: Loading seasons');
const response = await apiClient.get('/seasons');
console.log('SeasonSelector: Loaded seasons:', response.data);
seasons.value = response.data;
// Wenn showCurrentSeason true ist und keine Saison ausgewählt, wähle die aktuelle
if (props.showCurrentSeason && !selectedSeasonId.value && seasons.value.length > 0) {
// Die erste Saison ist die neueste (sortiert nach DESC)
selectedSeasonId.value = seasons.value[0].id;
emit('update:modelValue', selectedSeasonId.value);
emit('season-change', seasons.value[0]);
}
} catch (err) {
console.error('Fehler beim Laden der Saisons:', err);
error.value = 'Fehler beim Laden der Saisons';
} finally {
loading.value = false;
}
};
const onSeasonChange = () => {
const selectedSeason = seasons.value.find(s => s.id == selectedSeasonId.value);
emit('update:modelValue', selectedSeasonId.value);
emit('season-change', selectedSeason);
};
const createSeason = async () => {
if (!isValidSeasonFormat.value) return;
try {
const response = await apiClient.post('/seasons', {
season: newSeasonString.value
});
const newSeason = response.data;
seasons.value.unshift(newSeason); // Am Anfang einfügen (neueste zuerst)
selectedSeasonId.value = newSeason.id;
emit('update:modelValue', selectedSeasonId.value);
emit('season-change', newSeason);
// Formular zurücksetzen
newSeasonString.value = '';
showNewSeasonForm.value = false;
} catch (error) {
console.error('Fehler beim Erstellen der Saison:', error);
if (error.response?.data?.error === 'alreadyexists') {
alert('Diese Saison existiert bereits!');
} else {
alert('Fehler beim Erstellen der Saison');
}
}
};
const cancelNewSeason = () => {
newSeasonString.value = '';
showNewSeasonForm.value = false;
};
// Watch for prop changes
watch(() => props.modelValue, (newValue) => {
selectedSeasonId.value = newValue;
});
// Lifecycle
onMounted(() => {
loadSeasons();
});
return {
seasons,
selectedSeasonId,
showNewSeasonForm,
newSeasonString,
loading,
error,
isValidSeasonFormat,
onSeasonChange,
createSeason,
cancelNewSeason
};
}
};
</script>
<style scoped>
.season-selector {
margin-bottom: 1rem;
}
.season-selector label {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.season-selector label span {
font-weight: 600;
color: var(--text-color);
}
.season-input-group {
display: flex;
gap: 0.5rem;
align-items: center;
}
.season-select {
flex: 1;
padding: 0.75rem;
border: 1px solid var(--border-color);
border-radius: var(--border-radius-small);
font-size: 1rem;
background: white;
}
.season-select:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px var(--primary-light);
}
.btn-add-season {
background: var(--primary-color);
color: white;
border: none;
border-radius: var(--border-radius-small);
width: 2.5rem;
height: 2.5rem;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-weight: bold;
transition: var(--transition);
}
.btn-add-season:hover {
background: var(--primary-dark);
}
.new-season-form {
background: var(--background-light);
padding: 1rem;
border-radius: var(--border-radius);
border: 1px solid var(--border-color);
margin-top: 0.5rem;
}
.new-season-form label {
margin-bottom: 1rem;
}
.season-input {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--border-color);
border-radius: var(--border-radius-small);
font-size: 1rem;
}
.season-input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px var(--primary-light);
}
.form-actions {
display: flex;
gap: 0.5rem;
}
.btn-create {
background: var(--primary-color);
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: var(--border-radius-small);
cursor: pointer;
font-weight: 600;
transition: var(--transition);
}
.btn-create:hover:not(:disabled) {
background: var(--primary-dark);
}
.btn-create:disabled {
background: var(--text-muted);
cursor: not-allowed;
}
.btn-cancel {
background: var(--background-light);
color: var(--text-color);
border: 1px solid var(--border-color);
padding: 0.5rem 1rem;
border-radius: var(--border-radius-small);
cursor: pointer;
font-weight: 600;
transition: var(--transition);
}
.btn-cancel:hover {
background: var(--border-color);
}
.error-message {
color: #dc3545;
font-size: 0.875rem;
margin-top: 0.5rem;
padding: 0.5rem;
background: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: var(--border-radius-small);
}
</style>

View File

@@ -14,6 +14,7 @@ import TrainingStatsView from './views/TrainingStatsView.vue';
import PredefinedActivities from './views/PredefinedActivities.vue';
import OfficialTournaments from './views/OfficialTournaments.vue';
import MyTischtennisAccount from './views/MyTischtennisAccount.vue';
import TeamManagementView from './views/TeamManagementView.vue';
import Impressum from './views/Impressum.vue';
import Datenschutz from './views/Datenschutz.vue';
@@ -33,6 +34,7 @@ const routes = [
{ path: '/predefined-activities', component: PredefinedActivities },
{ path: '/official-tournaments', component: OfficialTournaments },
{ path: '/mytischtennis-account', component: MyTischtennisAccount },
{ path: '/team-management', component: TeamManagementView },
{ path: '/impressum', component: Impressum },
{ path: '/datenschutz', component: Datenschutz },
];

View File

@@ -1,6 +1,13 @@
<template>
<div>
<h2>Spielpläne</h2>
<SeasonSelector
v-model="selectedSeasonId"
@season-change="onSeasonChange"
:show-current-season="true"
/>
<button @click="openImportModal">Spielplanimport</button>
<div v-if="hoveredMatch && hoveredMatch.location" class="hover-info">
<p><strong>{{ hoveredMatch.location.name || 'N/A' }}</strong></p>
@@ -12,8 +19,9 @@
<li class="special-link" @click="loadAllMatches">Gesamtspielplan</li>
<li class="special-link" @click="loadAdultMatches">Spielplan Erwachsene</li>
<li class="divider"></li>
<li v-for="league in leagues" :key="league" @click="loadMatchesForLeague(league.id, league.name)">{{
<li v-for="league in leagues" :key="league.id" @click="loadMatchesForLeague(league.id, league.name)">{{
league.name }}</li>
<li v-if="leagues.length === 0" class="no-leagues">Keine Ligen für diese Saison gefunden</li>
</ul>
<div class="flex-item">
<button @click="generatePDF">Download PDF</button>
@@ -65,9 +73,13 @@
import { mapGetters } from 'vuex';
import apiClient from '../apiClient.js';
import PDFGenerator from '../components/PDFGenerator.js';
import SeasonSelector from '../components/SeasonSelector.vue';
export default {
name: 'ScheduleView',
components: {
SeasonSelector
},
computed: {
...mapGetters(['isAuthenticated', 'currentClub', 'clubs', 'currentClubName']),
},
@@ -79,6 +91,8 @@ export default {
matches: [],
selectedLeague: '',
hoveredMatch: null,
selectedSeasonId: null,
currentSeason: null,
};
},
methods: {
@@ -174,12 +188,25 @@ export default {
async loadLeagues() {
try {
const clubId = this.currentClub;
const response = await apiClient.get(`/matches/leagues/current/${clubId}`);
const seasonParam = this.selectedSeasonId ? `?seasonid=${this.selectedSeasonId}` : '';
console.log('ScheduleView: Loading leagues for club:', clubId, 'season:', this.selectedSeasonId);
const response = await apiClient.get(`/matches/leagues/current/${clubId}${seasonParam}`);
console.log('ScheduleView: Loaded leagues:', response.data);
this.leagues = this.sortLeagues(response.data);
console.log('ScheduleView: Sorted leagues:', this.leagues);
} catch (error) {
console.error('ScheduleView: Error loading leagues:', error);
alert('Fehler beim Laden der Ligen');
}
},
onSeasonChange(season) {
console.log('ScheduleView: Season changed to:', season);
this.currentSeason = season;
this.loadLeagues();
// Leere die aktuellen Matches, da sie für eine andere Saison sind
this.matches = [];
this.selectedLeague = '';
},
async loadMatchesForLeague(leagueId, leagueName) {
this.selectedLeague = leagueName;
try {
@@ -193,7 +220,8 @@ export default {
async loadAllMatches() {
this.selectedLeague = 'Gesamtspielplan';
try {
const response = await apiClient.get(`/matches/leagues/${this.currentClub}/matches`);
const seasonParam = this.selectedSeasonId ? `?seasonid=${this.selectedSeasonId}` : '';
const response = await apiClient.get(`/matches/leagues/${this.currentClub}/matches${seasonParam}`);
this.matches = response.data;
} catch (error) {
alert('Fehler beim Laden des Gesamtspielplans');
@@ -203,7 +231,8 @@ export default {
async loadAdultMatches() {
this.selectedLeague = 'Spielplan Erwachsene';
try {
const response = await apiClient.get(`/matches/leagues/${this.currentClub}/matches`);
const seasonParam = this.selectedSeasonId ? `?seasonid=${this.selectedSeasonId}` : '';
const response = await apiClient.get(`/matches/leagues/${this.currentClub}/matches${seasonParam}`);
// Filtere nur Erwachsenenligen (keine Jugendligen)
const allMatches = response.data;
this.matches = allMatches.filter(match => {
@@ -303,7 +332,9 @@ export default {
},
},
async created() {
await this.loadLeagues();
// Ligen werden geladen, sobald eine Saison ausgewählt ist
// Die SeasonSelector-Komponente wird automatisch die aktuelle Saison auswählen
// und dann onSeasonChange aufrufen, was loadLeagues() triggert
}
};
</script>
@@ -428,6 +459,12 @@ li {
transition: all 0.3s ease;
}
.no-leagues {
color: #666;
font-style: italic;
padding: 0.5rem;
}
.divider {
height: 1px;
background-color: #ddd;

View File

@@ -0,0 +1,612 @@
<template>
<div>
<h2>Team-Verwaltung</h2>
<SeasonSelector
v-model="selectedSeasonId"
@season-change="onSeasonChange"
:show-current-season="true"
/>
<div class="newteam">
<div class="toggle-new-team">
<span @click="toggleNewTeam">
<span class="add">{{ teamFormIsOpen ? '-' : '+' }}</span>
{{ teamToEdit === null ? "Neues Team" : "Team bearbeiten" }}
</span>
<button v-if="teamToEdit !== null" @click="resetToNewTeam">Neues Team anlegen</button>
</div>
<div v-if="teamFormIsOpen" class="new-team-form">
<label>
<span>Team-Name:</span>
<input type="text" v-model="newTeamName" placeholder="z.B. Herren 1, Damen 2">
</label>
<label>
<span>Spielklasse:</span>
<select v-model="newLeagueId">
<option value="">Keine Spielklasse</option>
<option v-for="league in filteredLeagues" :key="league.id" :value="league.id">
{{ league.name }}
</option>
</select>
</label>
<div class="form-actions">
<button @click="addNewTeam" :disabled="!newTeamName.trim()">
{{ teamToEdit ? 'Ändern' : 'Anlegen & Bearbeiten' }}
</button>
<button @click="resetNewTeam" v-if="teamToEdit === null" class="cancel-action">
Felder leeren
</button>
<button @click="resetToNewTeam" v-if="teamToEdit !== null" class="cancel-action">
Neues Team anlegen
</button>
</div>
<!-- Upload-Buttons nur beim Bearbeiten eines bestehenden Teams -->
<div v-if="teamToEdit" class="upload-actions">
<h4>Team-Dokumente hochladen</h4>
<div class="upload-buttons">
<button @click="uploadCodeList" class="upload-btn code-list-btn">
📋 Code-Liste hochladen
</button>
<button @click="uploadPinList" class="upload-btn pin-list-btn">
🔐 Pin-Liste hochladen
</button>
</div>
</div>
</div>
</div>
<div class="teams-list">
<h3>Teams ({{ teams.length }}) - Saison {{ currentSeason?.season || 'unbekannt' }}</h3>
<div v-if="teams.length === 0" class="no-teams">
<p>Noch keine Teams vorhanden. Erstellen Sie Ihr erstes Team!</p>
</div>
<div v-else class="teams-grid">
<div
v-for="team in teams"
:key="team.id"
class="team-card"
@click="editTeam(team)"
>
<div class="team-header">
<h4>{{ team.name }}</h4>
<div class="team-actions">
<button @click.stop="editTeam(team)" class="btn-edit" title="Bearbeiten">
</button>
<button @click.stop="deleteTeam(team)" class="btn-delete" title="Löschen">
🗑
</button>
</div>
</div>
<div class="team-info">
<div class="info-row">
<span class="label">Spielklasse:</span>
<span class="value">
{{ team.league ? team.league.name : 'Keine Zuordnung' }}
</span>
</div>
<div class="info-row">
<span class="label">Saison:</span>
<span class="value">{{ team.season?.season || 'Unbekannt' }}</span>
</div>
<div class="info-row">
<span class="label">Erstellt:</span>
<span class="value">{{ formatDate(team.createdAt) }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { ref, onMounted, computed } from 'vue';
import { useStore } from 'vuex';
import SeasonSelector from '../components/SeasonSelector.vue';
import apiClient from '../apiClient.js';
export default {
name: 'TeamManagementView',
components: {
SeasonSelector
},
setup() {
const store = useStore();
// Reactive data
const teams = ref([]);
const leagues = ref([]);
const teamFormIsOpen = ref(false);
const teamToEdit = ref(null);
const newTeamName = ref('');
const newLeagueId = ref('');
const selectedSeasonId = ref(null);
const currentSeason = ref(null);
// Computed
const selectedClub = computed(() => store.state.currentClub);
const authToken = computed(() => store.state.token);
const filteredLeagues = computed(() => {
if (!selectedSeasonId.value) return [];
return leagues.value.filter(league => league.seasonId == selectedSeasonId.value);
});
// Methods
const toggleNewTeam = () => {
teamFormIsOpen.value = !teamFormIsOpen.value;
if (!teamFormIsOpen.value) {
resetNewTeam();
}
};
const resetToNewTeam = () => {
teamToEdit.value = null;
resetNewTeam();
};
const resetNewTeam = () => {
newTeamName.value = '';
newLeagueId.value = '';
};
const loadTeams = async () => {
if (!selectedClub.value || !selectedSeasonId.value) {
console.log('TeamManagementView: Skipping loadTeams - club:', selectedClub.value, 'season:', selectedSeasonId.value);
return;
}
try {
console.log('TeamManagementView: Loading club teams for club:', selectedClub.value, 'season:', selectedSeasonId.value);
const response = await apiClient.get(`/club-teams/club/${selectedClub.value}?seasonid=${selectedSeasonId.value}`);
console.log('TeamManagementView: Loaded club teams:', response.data);
teams.value = response.data;
} catch (error) {
console.error('Fehler beim Laden der Club-Teams:', error);
}
};
const loadLeagues = async () => {
if (!selectedClub.value) return;
try {
console.log('TeamManagementView: Loading leagues for club:', selectedClub.value, 'season:', selectedSeasonId.value);
const seasonParam = selectedSeasonId.value ? `?seasonid=${selectedSeasonId.value}` : '';
const response = await apiClient.get(`/club-teams/leagues/${selectedClub.value}${seasonParam}`);
console.log('TeamManagementView: Loaded leagues:', response.data);
leagues.value = response.data;
} catch (error) {
console.error('Fehler beim Laden der Spielklassen:', error);
}
};
const addNewTeam = async () => {
if (!newTeamName.value.trim() || !selectedClub.value || !selectedSeasonId.value) return;
try {
const teamData = {
name: newTeamName.value.trim(),
leagueId: newLeagueId.value || null,
seasonId: selectedSeasonId.value
};
if (teamToEdit.value) {
// Bearbeitung eines bestehenden Teams
await apiClient.put(`/club-teams/${teamToEdit.value.id}`, teamData);
await loadTeams();
resetNewTeam();
teamFormIsOpen.value = false;
teamToEdit.value = null;
} else {
// Erstellen eines neuen Teams
const response = await apiClient.post(`/club-teams/club/${selectedClub.value}`, teamData);
const newTeam = response.data;
await loadTeams();
// Neues Team automatisch auswählen und bearbeiten
teamToEdit.value = newTeam;
newTeamName.value = newTeam.name;
newLeagueId.value = newTeam.leagueId || '';
teamFormIsOpen.value = true; // Formular bleibt offen für weitere Bearbeitung
}
} catch (error) {
console.error('Fehler beim Speichern des Club-Teams:', error);
}
};
const editTeam = (team) => {
teamToEdit.value = team;
newTeamName.value = team.name;
newLeagueId.value = team.leagueId || '';
teamFormIsOpen.value = true;
};
const deleteTeam = async (team) => {
if (!confirm(`Möchten Sie das Club-Team "${team.name}" wirklich löschen?`)) {
return;
}
try {
await apiClient.delete(`/club-teams/${team.id}`);
await loadTeams();
} catch (error) {
console.error('Fehler beim Löschen des Club-Teams:', error);
}
};
const uploadCodeList = () => {
if (!teamToEdit.value) return;
// Erstelle ein verstecktes File-Input-Element
const input = document.createElement('input');
input.type = 'file';
input.accept = '.pdf,.doc,.docx,.txt,.csv';
input.onchange = async (event) => {
const file = event.target.files[0];
if (!file) return;
try {
console.log('Code-Liste hochladen für Team:', teamToEdit.value.name, 'Datei:', file.name);
// TODO: Implementiere Upload-Logik für Code-Liste
alert(`Code-Liste "${file.name}" würde für Team "${teamToEdit.value.name}" hochgeladen werden.`);
} catch (error) {
console.error('Fehler beim Hochladen der Code-Liste:', error);
alert('Fehler beim Hochladen der Code-Liste');
}
};
input.click();
};
const uploadPinList = () => {
if (!teamToEdit.value) return;
// Erstelle ein verstecktes File-Input-Element
const input = document.createElement('input');
input.type = 'file';
input.accept = '.pdf,.doc,.docx,.txt,.csv';
input.onchange = async (event) => {
const file = event.target.files[0];
if (!file) return;
try {
console.log('Pin-Liste hochladen für Team:', teamToEdit.value.name, 'Datei:', file.name);
// TODO: Implementiere Upload-Logik für Pin-Liste
alert(`Pin-Liste "${file.name}" würde für Team "${teamToEdit.value.name}" hochgeladen werden.`);
} catch (error) {
console.error('Fehler beim Hochladen der Pin-Liste:', error);
alert('Fehler beim Hochladen der Pin-Liste');
}
};
input.click();
};
const formatDate = (dateString) => {
return new Date(dateString).toLocaleDateString('de-DE');
};
const onSeasonChange = (season) => {
currentSeason.value = season;
loadTeams();
loadLeagues();
};
// Lifecycle
onMounted(() => {
console.log('TeamManagementView: onMounted - Store state:', {
currentClub: store.state.currentClub,
token: store.state.token,
username: store.state.username
});
// Lade Ligen beim ersten Laden der Seite (ohne Saison-Filter)
loadLeagues();
});
return {
teams,
leagues,
teamFormIsOpen,
teamToEdit,
newTeamName,
newLeagueId,
selectedSeasonId,
currentSeason,
filteredLeagues,
toggleNewTeam,
resetToNewTeam,
resetNewTeam,
addNewTeam,
editTeam,
deleteTeam,
uploadCodeList,
uploadPinList,
formatDate,
onSeasonChange
};
}
};
</script>
<style scoped>
.newteam {
margin-bottom: 2rem;
}
.toggle-new-team {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
}
.toggle-new-team span {
cursor: pointer;
font-weight: 600;
color: var(--primary-color);
display: flex;
align-items: center;
gap: 0.5rem;
}
.add {
background: var(--primary-color);
color: white;
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
}
.new-team-form {
background: var(--background-light);
padding: 1.5rem;
border-radius: var(--border-radius);
border: 1px solid var(--border-color);
margin-bottom: 1rem;
}
.new-team-form label {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 1rem;
}
.new-team-form label span {
font-weight: 600;
color: var(--text-color);
}
.new-team-form input,
.new-team-form select {
padding: 0.75rem;
border: 1px solid var(--border-color);
border-radius: var(--border-radius-small);
font-size: 1rem;
}
.new-team-form input:focus,
.new-team-form select:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px var(--primary-light);
}
.form-actions {
display: flex;
gap: 1rem;
margin-top: 1rem;
}
.form-actions button {
padding: 0.75rem 1.5rem;
border: none;
border-radius: var(--border-radius-small);
font-weight: 600;
cursor: pointer;
transition: var(--transition);
}
.form-actions button:not(.cancel-action) {
background: var(--primary-color);
color: white;
}
.form-actions button:not(.cancel-action):hover {
background: var(--primary-dark);
}
.form-actions button:not(.cancel-action):disabled {
background: var(--text-muted);
cursor: not-allowed;
}
.cancel-action {
background: var(--background-light);
color: var(--text-color);
border: 1px solid var(--border-color);
}
.cancel-action:hover {
background: var(--border-color);
}
.teams-list h3 {
margin-bottom: 1rem;
color: var(--text-color);
}
.no-teams {
text-align: center;
padding: 2rem;
color: var(--text-muted);
background: var(--background-light);
border-radius: var(--border-radius);
border: 1px solid var(--border-color);
}
.teams-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1rem;
}
.team-card {
background: white;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
padding: 1.5rem;
cursor: pointer;
transition: var(--transition);
}
.team-card:hover {
border-color: var(--primary-color);
box-shadow: var(--shadow-small);
transform: translateY(-2px);
}
.team-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
}
.team-header h4 {
margin: 0;
color: var(--text-color);
font-size: 1.25rem;
}
.team-actions {
display: flex;
gap: 0.5rem;
}
.team-actions button {
background: none;
border: none;
cursor: pointer;
padding: 0.25rem;
border-radius: var(--border-radius-small);
transition: var(--transition);
}
.btn-edit:hover {
background: var(--primary-light);
}
.btn-delete:hover {
background: #fee;
}
.team-info {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.info-row .label {
font-weight: 600;
color: var(--text-muted);
}
.info-row .value {
color: var(--text-color);
}
@media (max-width: 768px) {
.teams-grid {
grid-template-columns: 1fr;
}
.team-header {
flex-direction: column;
gap: 1rem;
}
.team-actions {
align-self: flex-end;
}
}
/* Upload-Buttons Styles */
.upload-actions {
margin-top: 2rem;
padding: 1.5rem;
background: var(--background-light);
border-radius: var(--border-radius-medium);
border: 1px solid var(--border-color);
}
.upload-actions h4 {
margin: 0 0 1rem 0;
color: var(--text-color);
font-size: 1.1rem;
}
.upload-buttons {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.upload-btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: var(--border-radius-small);
font-weight: 600;
cursor: pointer;
transition: var(--transition);
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.95rem;
}
.code-list-btn {
background: #4CAF50;
color: white;
}
.code-list-btn:hover {
background: #45a049;
}
.pin-list-btn {
background: #FF9800;
color: white;
}
.pin-list-btn:hover {
background: #e68900;
}
@media (max-width: 768px) {
.upload-buttons {
flex-direction: column;
}
.upload-btn {
justify-content: center;
}
}
</style>