feat: Implement friendly match management features
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 44s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 44s
- Added backend support for managing friendly matches including listing, creating, updating, and deleting matches. - Introduced a new database table `friendly_match` with relevant fields for match details. - Created a service layer to handle business logic related to friendly matches. - Developed API routes for friendly match operations with appropriate authentication and authorization. - Added a Vue component for managing participants in friendly matches, allowing selection of members and manual entry of names. - Updated existing tournament editor screens to integrate friendly match functionalities.
This commit is contained in:
@@ -113,6 +113,10 @@
|
||||
<span class="nav-icon">📅</span>
|
||||
{{ $t('navigation.schedule') }}
|
||||
</router-link>
|
||||
<router-link v-if="hasPermission('schedule', 'read')" to="/friendly-matches" class="nav-link" title="Freundschaftsspiele">
|
||||
<span class="nav-icon">🤝</span>
|
||||
Freundschaftsspiele
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
@@ -292,7 +296,7 @@ export default {
|
||||
return this.isClubOwner || this.userRole === 'admin' || this.hasPermission('permissions', 'read');
|
||||
},
|
||||
isFullHeightRoute() {
|
||||
return this.$route?.name === 'schedule';
|
||||
return this.$route?.name === 'schedule' || this.$route?.name === 'friendly-matches';
|
||||
},
|
||||
viewReloadKey() {
|
||||
return `${this.$route.fullPath}|${this.currentClub || 'no-club'}`;
|
||||
|
||||
107
frontend/src/components/schedule/FriendlyParticipantsColumn.vue
Normal file
107
frontend/src/components/schedule/FriendlyParticipantsColumn.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<section class="friendly-participant-column">
|
||||
<h4>{{ title }}</h4>
|
||||
<div class="friendly-add-row">
|
||||
<select v-model="selectedMemberId">
|
||||
<option value="">Mitglied auswählen</option>
|
||||
<option v-for="member in members" :key="member.id" :value="member.id">
|
||||
{{ member.firstName }} {{ member.lastName }}
|
||||
</option>
|
||||
</select>
|
||||
<button type="button" @click="addSelectedMember">Hinzufügen</button>
|
||||
</div>
|
||||
<div class="friendly-add-row">
|
||||
<input v-model="manualName" type="text" placeholder="Manueller Name" @keyup.enter="addManual" />
|
||||
<button type="button" @click="addManual">Hinzufügen</button>
|
||||
</div>
|
||||
<ul class="friendly-participant-list">
|
||||
<li v-for="(participant, index) in participants" :key="index">
|
||||
<span>{{ participantLabel(participant) }}</span>
|
||||
<button type="button" @click="$emit('remove', index)">x</button>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'FriendlyParticipantsColumn',
|
||||
props: {
|
||||
title: { type: String, required: true },
|
||||
members: { type: Array, required: true },
|
||||
participants: { type: Array, required: true }
|
||||
},
|
||||
emits: ['add-member', 'add-manual', 'remove'],
|
||||
data() {
|
||||
return {
|
||||
selectedMemberId: '',
|
||||
manualName: ''
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
memberLabel(id) {
|
||||
const member = this.members.find(m => Number(m.id) === Number(id));
|
||||
return member ? `${member.firstName} ${member.lastName}`.trim() : `Mitglied #${id}`;
|
||||
},
|
||||
participantLabel(participant) {
|
||||
if (participant.type === 'member') return this.memberLabel(participant.memberId);
|
||||
return `${participant.firstName || ''} ${participant.lastName || ''}`.trim();
|
||||
},
|
||||
addSelectedMember() {
|
||||
if (!this.selectedMemberId) return;
|
||||
this.$emit('add-member', Number(this.selectedMemberId));
|
||||
this.selectedMemberId = '';
|
||||
},
|
||||
addManual() {
|
||||
const value = this.manualName.trim();
|
||||
if (!value) return;
|
||||
this.$emit('add-manual', value);
|
||||
this.manualName = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.friendly-participant-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
border: 1px solid var(--border-color, #ddd);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.friendly-add-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.friendly-add-row input,
|
||||
.friendly-add-row select {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 0.45rem 0.55rem;
|
||||
border: 1px solid var(--border-color, #ddd);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.friendly-participant-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0.5rem 0 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.friendly-participant-list li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
padding: 0.35rem 0.5rem;
|
||||
background: var(--background-soft, #f7f7f7);
|
||||
border-radius: 6px;
|
||||
}
|
||||
</style>
|
||||
@@ -3,17 +3,19 @@
|
||||
<div class="schedule-static-chrome">
|
||||
<div class="schedule-page-header">
|
||||
<div class="schedule-page-title">
|
||||
<h2>{{ $t('schedule.title') }}</h2>
|
||||
<p>{{ $t('schedule.subtitle') }}</p>
|
||||
<h2>{{ resolvedTitle }}</h2>
|
||||
<p>{{ resolvedSubtitle }}</p>
|
||||
</div>
|
||||
<div class="schedule-page-actions">
|
||||
<SeasonSelector
|
||||
v-if="showScheduleActions"
|
||||
:model-value="selectedSeasonId"
|
||||
:show-current-season="true"
|
||||
@update:model-value="$emit('update:selected-season-id', $event)"
|
||||
@season-change="$emit('season-change', $event)"
|
||||
/>
|
||||
<button @click="$emit('open-import-modal')">{{ $t('schedule.importSchedule') }}</button>
|
||||
<button v-if="showScheduleActions" @click="$emit('open-import-modal')">{{ $t('schedule.importSchedule') }}</button>
|
||||
<button v-if="showFriendlyActions" @click="$emit('open-friendly-match-modal')" class="btn-secondary">Freundschaftsspiel</button>
|
||||
<button
|
||||
v-if="showGalleryButton"
|
||||
@click="$emit('open-gallery-dialog')"
|
||||
@@ -50,7 +52,7 @@
|
||||
</div>
|
||||
|
||||
<div class="output schedule-layout">
|
||||
<aside class="schedule-sidebar">
|
||||
<aside v-if="showSidebar" class="schedule-sidebar">
|
||||
<div class="schedule-sidebar-card">
|
||||
<div class="schedule-sidebar-header">
|
||||
<h3>{{ $t('schedule.selection') }}</h3>
|
||||
@@ -63,7 +65,7 @@
|
||||
:placeholder="$t('schedule.searchTeams')"
|
||||
@input="$emit('update:team-search-query', $event.target.value.trim())"
|
||||
/>
|
||||
<div class="schedule-quick-links">
|
||||
<div v-if="showScheduleActions" class="schedule-quick-links">
|
||||
<button
|
||||
type="button"
|
||||
class="schedule-quick-link"
|
||||
@@ -131,7 +133,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedLeague && selectedLeague !== ''" class="tab-navigation">
|
||||
<div v-if="selectedLeague && selectedLeague !== '' && showTableTab" class="tab-navigation">
|
||||
<button :class="['tab-button', { active: activeTab === 'schedule' }]" @click="$emit('update:active-tab', 'schedule')">
|
||||
📅 {{ $t('schedule.scheduleTab') }} <span class="tab-count">{{ matchesCount }}</span>
|
||||
</button>
|
||||
@@ -169,6 +171,8 @@ export default {
|
||||
components: { SeasonSelector },
|
||||
props: {
|
||||
selectedSeasonId: { type: [Number, String, null], default: null },
|
||||
title: { type: String, default: '' },
|
||||
subtitle: { type: String, default: '' },
|
||||
showGalleryButton: { type: Boolean, required: true },
|
||||
galleryLoading: { type: Boolean, required: true },
|
||||
filteredScheduleTeamsCount: { type: Number, required: true },
|
||||
@@ -187,12 +191,17 @@ export default {
|
||||
activeTab: { type: String, required: true },
|
||||
tableCount: { type: Number, required: true },
|
||||
fetchingTeamData: { type: Boolean, required: true },
|
||||
fetchingTable: { type: Boolean, required: true }
|
||||
fetchingTable: { type: Boolean, required: true },
|
||||
showFriendlyActions: { type: Boolean, default: false },
|
||||
showScheduleActions: { type: Boolean, default: true },
|
||||
showSidebar: { type: Boolean, default: true },
|
||||
showTableTab: { type: Boolean, default: true }
|
||||
},
|
||||
emits: [
|
||||
'update:selected-season-id',
|
||||
'season-change',
|
||||
'open-import-modal',
|
||||
'open-friendly-match-modal',
|
||||
'open-gallery-dialog',
|
||||
'update:team-search-query',
|
||||
'load-all-matches',
|
||||
@@ -202,7 +211,15 @@ export default {
|
||||
'generate-pdf',
|
||||
'fetch-table',
|
||||
'update:active-tab'
|
||||
]
|
||||
],
|
||||
computed: {
|
||||
resolvedTitle() {
|
||||
return this.title || this.$t('schedule.title');
|
||||
},
|
||||
resolvedSubtitle() {
|
||||
return this.subtitle || this.$t('schedule.subtitle');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ const routes = [
|
||||
{ path: '/calendar', name: 'calendar', component: CalendarView },
|
||||
{ path: '/pending-approvals', name: 'pending-approvals', component: PendingApprovalsView},
|
||||
{ path: '/schedule', name: 'schedule', component: ScheduleView},
|
||||
{ path: '/friendly-matches', name: 'friendly-matches', component: ScheduleView, props: { friendlyOnly: true } },
|
||||
{ path: '/tournaments', name: 'tournaments', component: TournamentsView },
|
||||
{ path: '/tournament-participations', name: 'tournament-participations', component: OfficialTournaments },
|
||||
{ path: '/training-stats', name: 'training-stats', component: TrainingStatsView },
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
<ScheduleLayoutShell
|
||||
ref="scheduleShell"
|
||||
:selected-season-id="selectedSeasonId"
|
||||
:title="friendlyOnly ? 'Freundschaftsspiele' : $t('schedule.title')"
|
||||
:subtitle="friendlyOnly ? 'Interne Spiele ohne Click-TT-Verbindung verwalten.' : $t('schedule.subtitle')"
|
||||
:show-gallery-button="Boolean(playerSelectionDialog.match)"
|
||||
:gallery-loading="galleryLoading"
|
||||
:filtered-schedule-teams-count="filteredScheduleTeams.length"
|
||||
@@ -22,9 +24,14 @@
|
||||
:table-count="leagueTable.length"
|
||||
:fetching-team-data="fetchingTeamData"
|
||||
:fetching-table="fetchingTable"
|
||||
:show-friendly-actions="friendlyOnly"
|
||||
:show-schedule-actions="!friendlyOnly"
|
||||
:show-sidebar="!friendlyOnly"
|
||||
:show-table-tab="!friendlyOnly"
|
||||
@update:selected-season-id="selectedSeasonId = $event"
|
||||
@season-change="onSeasonChange"
|
||||
@open-import-modal="openImportModal"
|
||||
@open-friendly-match-modal="openFriendlyMatchDialog"
|
||||
@open-gallery-dialog="openGalleryDialog"
|
||||
@update:team-search-query="teamSearchQuery = $event"
|
||||
@load-all-matches="loadAllMatches"
|
||||
@@ -89,13 +96,14 @@
|
||||
<th>{{ $t('schedule.time') }}</th>
|
||||
<th>{{ $t('schedule.homeTeam') }}</th>
|
||||
<th>{{ $t('schedule.guestTeam') }}</th>
|
||||
<th>{{ $t('schedule.result') }}</th>
|
||||
<th v-if="!friendlyOnly">{{ $t('schedule.result') }}</th>
|
||||
<th
|
||||
v-if="selectedLeague === $t('schedule.overallSchedule') || selectedLeague === $t('schedule.adultSchedule')">
|
||||
{{ $t('schedule.ageClass') }}</th>
|
||||
<th>{{ $t('schedule.code') }}</th>
|
||||
<th>{{ $t('schedule.homePin') }}</th>
|
||||
<th>{{ $t('schedule.guestPin') }}</th>
|
||||
<th v-if="!friendlyOnly">{{ $t('schedule.code') }}</th>
|
||||
<th>{{ friendlyOnly ? 'Aktionen' : $t('schedule.homePin') }}</th>
|
||||
<th v-if="!friendlyOnly">{{ $t('schedule.guestPin') }}</th>
|
||||
<th v-if="friendlyOnly"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -123,7 +131,7 @@
|
||||
<td :class="{ 'highlighted-club': isClubHighlighted(match.guestTeam?.name) }">
|
||||
{{ match.guestTeam?.name || 'N/A' }}
|
||||
</td>
|
||||
<td class="result-cell" :class="getResultClass(match)">
|
||||
<td v-if="!friendlyOnly" class="result-cell" :class="getResultClass(match)">
|
||||
<span v-if="match.isCompleted" class="result-score">
|
||||
{{ match.homeMatchPoints }}:{{ match.guestMatchPoints }}
|
||||
</span>
|
||||
@@ -132,7 +140,7 @@
|
||||
<td
|
||||
v-if="selectedLeague === $t('schedule.overallSchedule') || selectedLeague === $t('schedule.adultSchedule')">
|
||||
{{ match.leagueDetails?.name || 'N/A' }}</td>
|
||||
<td class="code-cell match-report-cell" @click.stop="openMatchReport(match)">
|
||||
<td v-if="!friendlyOnly" class="code-cell match-report-cell" @click.stop="openMatchReport(match)">
|
||||
<span v-if="match.code && selectedLeague && selectedLeague !== ''">
|
||||
<button @click.stop="openMatchReport(match)" class="nuscore-link"
|
||||
:title="$t('schedule.openMatchReport')">📊</button>
|
||||
@@ -142,20 +150,30 @@
|
||||
<span v-else-if="match.code" class="code-value clickable"
|
||||
@click.stop="copyToClipboard(match.code, $t('schedule.code'), $event)"
|
||||
:title="$t('schedule.copyCode') + ': ' + match.code">{{ match.code }}</span>
|
||||
<span v-else class="no-data">-</span>
|
||||
<span v-else-if="!match.isFriendly" class="no-data">-</span>
|
||||
</td>
|
||||
<td class="pin-cell">
|
||||
<span v-if="match.homePin" class="pin-value clickable"
|
||||
<div v-if="match.isFriendly" class="friendly-actions-cell">
|
||||
<button type="button" class="btn-secondary" @click.stop="openFriendlyResultDialog(match)">Ergebnis</button>
|
||||
<button type="button" class="btn-secondary" @click.stop="openFriendlyMatchDialog(match)">Bearbeiten</button>
|
||||
</div>
|
||||
<span v-else-if="match.homePin" class="pin-value clickable"
|
||||
@click.stop="copyToClipboard(match.homePin, $t('schedule.homePin'), $event)"
|
||||
:title="$t('schedule.copyHomePin') + ': ' + match.homePin">{{ match.homePin }}</span>
|
||||
<span v-else class="no-data">-</span>
|
||||
</td>
|
||||
<td class="pin-cell">
|
||||
<td v-if="!friendlyOnly" class="pin-cell">
|
||||
<span v-if="match.guestPin" class="pin-value clickable"
|
||||
@click.stop="copyToClipboard(match.guestPin, $t('schedule.guestPin'), $event)"
|
||||
:title="$t('schedule.copyGuestPin') + ': ' + match.guestPin">{{ match.guestPin }}</span>
|
||||
<span v-else class="no-data">-</span>
|
||||
</td>
|
||||
<td v-if="friendlyOnly" class="result-cell" :class="getResultClass(match)">
|
||||
<span v-if="match.isCompleted" class="result-score">
|
||||
{{ match.homeMatchPoints }}:{{ match.guestMatchPoints }}
|
||||
</span>
|
||||
<span v-else class="result-pending">—</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -326,6 +344,67 @@
|
||||
</div>
|
||||
</BaseDialog>
|
||||
|
||||
<BaseDialog
|
||||
v-model="friendlyResultDialog.isOpen"
|
||||
:title="`Ergebniseingabe - ${friendlyResultDialog.match?.homeTeam?.name || ''} vs ${friendlyResultDialog.match?.guestTeam?.name || ''}`"
|
||||
width="96vw"
|
||||
max-width="1500px"
|
||||
@close="closeFriendlyResultDialog"
|
||||
>
|
||||
<div class="friendly-result-dialog" v-if="friendlyResultDialog.match">
|
||||
<div class="score-summary">
|
||||
<div class="score-display">
|
||||
<span class="score-label">Spielstand:</span>
|
||||
<span class="score-value">{{ friendlyResultScore.home }}:{{ friendlyResultScore.guest }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<table class="friendly-result-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Typ</th>
|
||||
<th>Heim</th>
|
||||
<th>Gast</th>
|
||||
<th>Satz 1</th>
|
||||
<th>Satz 2</th>
|
||||
<th>Satz 3</th>
|
||||
<th>Satz 4</th>
|
||||
<th>Satz 5</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, index) in friendlyResultDialog.rows" :key="row.id">
|
||||
<td>{{ index + 1 }}</td>
|
||||
<td>{{ row.type === 'double' ? 'Doppel' : 'Einzel' }}</td>
|
||||
<td><input v-model="row.homeName" class="player-input" type="text" /></td>
|
||||
<td><input v-model="row.guestName" class="player-input" type="text" /></td>
|
||||
<td v-for="setIndex in 5" :key="setIndex">
|
||||
<input
|
||||
v-model="row.sets[setIndex - 1]"
|
||||
class="set-input"
|
||||
placeholder="11:7"
|
||||
:disabled="isFriendlySetClosed(row, setIndex - 1)"
|
||||
@blur="normalizeFriendlySet(row, setIndex - 1)"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn-secondary" @click="row.completed = !row.completed">
|
||||
{{ row.completed ? 'Abgeschlossen' : 'Offen' }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-if="friendlyResultDialog.error" class="friendly-result-error">{{ friendlyResultDialog.error }}</div>
|
||||
<div class="dialog-actions">
|
||||
<button @click="saveFriendlyResults(false)" class="btn-save">Speichern</button>
|
||||
<button @click="completeFriendlyResults" class="btn-save">Abschließen</button>
|
||||
<button @click="closeFriendlyResultDialog" class="btn-cancel">{{ $t('schedule.cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
|
||||
<BaseDialog
|
||||
v-model="locationDialog.isOpen"
|
||||
:title="$t('schedule.locationDialogTitle')"
|
||||
@@ -359,6 +438,57 @@
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
|
||||
<BaseDialog
|
||||
v-model="friendlyMatchDialog.isOpen"
|
||||
:title="friendlyMatchDialog.editingId ? 'Freundschaftsspiel bearbeiten' : 'Freundschaftsspiel anlegen'"
|
||||
:max-width="900"
|
||||
@close="closeFriendlyMatchDialog"
|
||||
>
|
||||
<div class="friendly-form">
|
||||
<div class="friendly-form-grid">
|
||||
<label>Datum <input v-model="friendlyMatchDialog.form.date" type="date" /></label>
|
||||
<label>Uhrzeit <input v-model="friendlyMatchDialog.form.time" type="time" /></label>
|
||||
<label>Heimteam <input v-model="friendlyMatchDialog.form.homeTeamName" type="text" /></label>
|
||||
<label>Gastteam <input v-model="friendlyMatchDialog.form.guestTeamName" type="text" /></label>
|
||||
<label>Spielsystem
|
||||
<select v-model="friendlyMatchDialog.form.matchSystem">
|
||||
<option>Braunschweiger System</option>
|
||||
<option>Bundessystem</option>
|
||||
<option>Werner-Scheffler-System</option>
|
||||
<option>Modifiziertes Swaythling-Cup-System</option>
|
||||
<option>Freies System</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>Gewinnsätze <input v-model.number="friendlyMatchDialog.form.winningSets" type="number" min="1" /></label>
|
||||
</div>
|
||||
|
||||
<div class="friendly-participants">
|
||||
<FriendlyParticipantsColumn
|
||||
title="Heim-Aufstellung"
|
||||
:members="friendlyMatchDialog.members"
|
||||
:participants="friendlyMatchDialog.form.homeParticipants"
|
||||
@add-member="addFriendlyParticipant('homeParticipants', $event)"
|
||||
@add-manual="addManualFriendlyParticipant('homeParticipants', $event)"
|
||||
@remove="removeFriendlyParticipant('homeParticipants', $event)"
|
||||
/>
|
||||
<FriendlyParticipantsColumn
|
||||
title="Gast-Aufstellung"
|
||||
:members="friendlyMatchDialog.members"
|
||||
:participants="friendlyMatchDialog.form.guestParticipants"
|
||||
@add-member="addFriendlyParticipant('guestParticipants', $event)"
|
||||
@add-manual="addManualFriendlyParticipant('guestParticipants', $event)"
|
||||
@remove="removeFriendlyParticipant('guestParticipants', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="dialog-actions">
|
||||
<button @click="saveFriendlyMatch" class="btn-save">Speichern</button>
|
||||
<button v-if="friendlyMatchDialog.editingId" @click="deleteFriendlyMatch" class="btn-cancel">Löschen</button>
|
||||
<button @click="closeFriendlyMatchDialog" class="btn-cancel">{{ $t('schedule.cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -373,6 +503,7 @@ import ConfirmDialog from '../components/ConfirmDialog.vue';
|
||||
import BaseDialog from '../components/BaseDialog.vue';
|
||||
import CsvImportDialog from '../components/CsvImportDialog.vue';
|
||||
import ScheduleLayoutShell from '../components/schedule/ScheduleLayoutShell.vue';
|
||||
import FriendlyParticipantsColumn from '../components/schedule/FriendlyParticipantsColumn.vue';
|
||||
import {
|
||||
connectSocket,
|
||||
disconnectSocket,
|
||||
@@ -383,13 +514,20 @@ import {
|
||||
} from '../services/socketService.js';
|
||||
export default {
|
||||
name: 'ScheduleView',
|
||||
props: {
|
||||
friendlyOnly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
SeasonSelector,
|
||||
InfoDialog,
|
||||
ConfirmDialog,
|
||||
BaseDialog,
|
||||
CsvImportDialog,
|
||||
ScheduleLayoutShell
|
||||
ScheduleLayoutShell,
|
||||
FriendlyParticipantsColumn
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['isAuthenticated', 'currentClub', 'clubs', 'currentClubName']),
|
||||
@@ -410,6 +548,9 @@ export default {
|
||||
pendingMatchesCount() {
|
||||
return this.matches.length - this.completedMatchesCount;
|
||||
},
|
||||
friendlyResultScore() {
|
||||
return this.calculateFriendlyResultScore(this.friendlyResultDialog.rows);
|
||||
},
|
||||
nextScheduledMatchLabel() {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
@@ -424,6 +565,8 @@ export default {
|
||||
if (!this.selectedLeague) return '';
|
||||
return this.activeTab === 'table'
|
||||
? this.$t('schedule.workspaceTableDescription', { count: this.leagueTable.length })
|
||||
: this.friendlyOnly
|
||||
? `Interne Freundschaftsspiele: ${this.matches.length}`
|
||||
: this.$t('schedule.workspaceScheduleDescription', {
|
||||
matches: this.matches.length,
|
||||
completed: this.completedMatchesCount,
|
||||
@@ -439,6 +582,9 @@ export default {
|
||||
.filter(teamName => teamName !== ownTeamName)
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
},
|
||||
friendlyMatchesLabel() {
|
||||
return 'Freundschaftsspiele';
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentClub: {
|
||||
@@ -451,6 +597,9 @@ export default {
|
||||
connectSocket(newVal);
|
||||
onScheduleMatchUpdated(this.handleScheduleMatchUpdated);
|
||||
onMatchReportSubmitted(this.handleMatchReportSubmitted);
|
||||
if (this.friendlyOnly) {
|
||||
this.loadFriendlyMatches();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -490,6 +639,7 @@ export default {
|
||||
allLeagueMatches: [],
|
||||
leagueMatchScope: 'own',
|
||||
selectedComparisonTeamName: '',
|
||||
friendlyMatches: [],
|
||||
|
||||
// Player Selection Dialog
|
||||
playerSelectionDialog: {
|
||||
@@ -508,9 +658,75 @@ export default {
|
||||
galleryMembers: [],
|
||||
galleryError: '',
|
||||
gallerySize: 200,
|
||||
friendlyMatchDialog: {
|
||||
isOpen: false,
|
||||
editingId: null,
|
||||
members: [],
|
||||
form: {
|
||||
date: new Date().toISOString().slice(0, 10),
|
||||
time: '',
|
||||
homeTeamName: '',
|
||||
guestTeamName: '',
|
||||
locationName: '',
|
||||
locationAddress: '',
|
||||
locationZip: '',
|
||||
locationCity: '',
|
||||
matchSystem: 'Braunschweiger System',
|
||||
singlesCount: 12,
|
||||
doublesCount: 4,
|
||||
winningSets: 3,
|
||||
homeMatchPoints: 0,
|
||||
guestMatchPoints: 0,
|
||||
isCompleted: false,
|
||||
homeParticipants: [],
|
||||
guestParticipants: []
|
||||
}
|
||||
},
|
||||
friendlyResultDialog: {
|
||||
isOpen: false,
|
||||
match: null,
|
||||
rows: [],
|
||||
error: '',
|
||||
saving: false,
|
||||
saveAgain: false
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
emptyFriendlyMatchForm() {
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
return {
|
||||
date: today,
|
||||
time: '',
|
||||
homeTeamName: this.currentClubName || '',
|
||||
guestTeamName: '',
|
||||
locationName: '',
|
||||
locationAddress: '',
|
||||
locationZip: '',
|
||||
locationCity: '',
|
||||
matchSystem: 'Braunschweiger System',
|
||||
singlesCount: 12,
|
||||
doublesCount: 4,
|
||||
winningSets: 3,
|
||||
homeMatchPoints: 0,
|
||||
guestMatchPoints: 0,
|
||||
isCompleted: false,
|
||||
homeParticipants: [],
|
||||
guestParticipants: []
|
||||
};
|
||||
},
|
||||
parseFriendlyArray(value) {
|
||||
if (Array.isArray(value)) return value;
|
||||
if (typeof value === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
},
|
||||
sortMatchesByDateTime(matches) {
|
||||
if (!Array.isArray(matches)) {
|
||||
return [];
|
||||
@@ -643,12 +859,14 @@ export default {
|
||||
const preselectedIds = Array.from(new Set([...readyIds, ...plannedIds, ...playedIds]));
|
||||
|
||||
// Fetch members for the current club
|
||||
const response = await apiClient.get(`/clubmembers/get/${this.currentClub}/true`);
|
||||
const response = match.isFriendly
|
||||
? await apiClient.get(`/friendly-matches/${this.currentClub}/members/list`)
|
||||
: await apiClient.get(`/clubmembers/get/${this.currentClub}/true`);
|
||||
const allMembers = response.data;
|
||||
|
||||
const lineupHalf = this.getLineupHalfForMatch(match);
|
||||
const eligibleMemberIds = await this.getEligibleMemberIdsForSelectedTeam(lineupHalf);
|
||||
const activeMembers = allMembers.filter(member => member.active);
|
||||
const eligibleMemberIds = match.isFriendly ? [] : await this.getEligibleMemberIdsForSelectedTeam(lineupHalf);
|
||||
const activeMembers = match.isFriendly ? allMembers : allMembers.filter(member => member.active);
|
||||
const allowedIds = new Set(
|
||||
[...eligibleMemberIds, ...preselectedIds]
|
||||
.map((id) => Number(id))
|
||||
@@ -760,12 +978,18 @@ export default {
|
||||
console.log('[savePlayerSelection] Saving players:', { playersReady, playersPlanned, playersPlayed, matchId: match.id });
|
||||
|
||||
try {
|
||||
const response = await apiClient.patch(`/matches/${match.id}/players`, {
|
||||
clubId: this.currentClub,
|
||||
playersReady,
|
||||
playersPlanned,
|
||||
playersPlayed
|
||||
});
|
||||
const response = match.isFriendly
|
||||
? await apiClient.patch(`/friendly-matches/${this.currentClub}/${match.id}/players`, {
|
||||
playersReady,
|
||||
playersPlanned,
|
||||
playersPlayed
|
||||
})
|
||||
: await apiClient.patch(`/matches/${match.id}/players`, {
|
||||
clubId: this.currentClub,
|
||||
playersReady,
|
||||
playersPlanned,
|
||||
playersPlayed
|
||||
});
|
||||
if (response.status >= 400) {
|
||||
throw new Error(response?.data?.error || 'Failed to update match players');
|
||||
}
|
||||
@@ -911,6 +1135,309 @@ export default {
|
||||
this.selectedFile = file;
|
||||
this.importCSV();
|
||||
},
|
||||
async loadFriendlyMembers() {
|
||||
const response = await apiClient.get(`/friendly-matches/${this.currentClub}/members/list`);
|
||||
this.friendlyMatchDialog.members = response.data || [];
|
||||
},
|
||||
async openFriendlyMatchDialog(match = null) {
|
||||
await this.loadFriendlyMembers();
|
||||
this.friendlyMatchDialog.editingId = match?.isFriendly ? match.id : null;
|
||||
this.friendlyMatchDialog.form = match?.isFriendly
|
||||
? {
|
||||
date: match.date ? String(match.date).slice(0, 10) : new Date().toISOString().slice(0, 10),
|
||||
time: match.time ? String(match.time).slice(0, 5) : '',
|
||||
homeTeamName: match.homeTeam?.name || '',
|
||||
guestTeamName: match.guestTeam?.name || '',
|
||||
locationName: match.location?.name === 'N/A' ? '' : (match.location?.name || ''),
|
||||
locationAddress: match.location?.address || '',
|
||||
locationZip: match.location?.zip || '',
|
||||
locationCity: match.location?.city || '',
|
||||
matchSystem: match.matchSystem || 'Braunschweiger System',
|
||||
singlesCount: match.singlesCount ?? 12,
|
||||
doublesCount: match.doublesCount ?? 4,
|
||||
winningSets: match.winningSets ?? 3,
|
||||
homeMatchPoints: match.homeMatchPoints ?? 0,
|
||||
guestMatchPoints: match.guestMatchPoints ?? 0,
|
||||
isCompleted: Boolean(match.isCompleted),
|
||||
homeParticipants: [...this.parseFriendlyArray(match.homeParticipants)],
|
||||
guestParticipants: [...this.parseFriendlyArray(match.guestParticipants)]
|
||||
}
|
||||
: this.emptyFriendlyMatchForm();
|
||||
this.friendlyMatchDialog.isOpen = true;
|
||||
},
|
||||
closeFriendlyMatchDialog() {
|
||||
this.friendlyMatchDialog.isOpen = false;
|
||||
this.friendlyMatchDialog.editingId = null;
|
||||
this.friendlyMatchDialog.form = this.emptyFriendlyMatchForm();
|
||||
},
|
||||
addFriendlyParticipant(field, memberId) {
|
||||
const list = this.friendlyMatchDialog.form[field];
|
||||
if (list.some(p => p.type === 'member' && Number(p.memberId) === Number(memberId))) return;
|
||||
list.push({ type: 'member', memberId });
|
||||
},
|
||||
addManualFriendlyParticipant(field, fullName) {
|
||||
const parts = String(fullName).trim().split(/\s+/);
|
||||
const firstName = parts.shift() || '';
|
||||
const lastName = parts.join(' ');
|
||||
this.friendlyMatchDialog.form[field].push({ type: 'manual', firstName, lastName });
|
||||
},
|
||||
removeFriendlyParticipant(field, index) {
|
||||
this.friendlyMatchDialog.form[field].splice(index, 1);
|
||||
},
|
||||
friendlyParticipantLabel(participant, fallback = '') {
|
||||
if (!participant) return fallback;
|
||||
if (participant.type === 'member') {
|
||||
const member = this.friendlyMatchDialog.members.find(m => Number(m.id) === Number(participant.memberId));
|
||||
return member ? `${member.firstName} ${member.lastName}`.trim() : fallback;
|
||||
}
|
||||
return `${participant.firstName || ''} ${participant.lastName || ''}`.trim() || fallback;
|
||||
},
|
||||
friendlyParticipantLabels(participants) {
|
||||
return (Array.isArray(participants) ? participants : [])
|
||||
.map((participant) => this.friendlyParticipantLabel(participant, ''))
|
||||
.filter(Boolean);
|
||||
},
|
||||
friendlyDoubleLabel(labels, index) {
|
||||
if (!labels.length) return '';
|
||||
if (labels.length === 1) return labels[0];
|
||||
const pairings = [
|
||||
[0, 1],
|
||||
[2, 3],
|
||||
[0, 2],
|
||||
[1, 3]
|
||||
];
|
||||
const fallbackStart = (index * 2) % labels.length;
|
||||
const [firstIndex, secondIndex] = pairings[index] || [fallbackStart, fallbackStart + 1];
|
||||
const first = labels[firstIndex % labels.length];
|
||||
const second = labels[secondIndex % labels.length];
|
||||
return first === second ? first : `${first} / ${second}`;
|
||||
},
|
||||
buildGeneratedFriendlyResultRows(match) {
|
||||
const homeLabels = this.friendlyParticipantLabels(match.homeParticipants);
|
||||
const guestLabels = this.friendlyParticipantLabels(match.guestParticipants);
|
||||
const rows = [];
|
||||
for (let i = 0; i < 4; i += 1) {
|
||||
rows.push({
|
||||
id: `d-${i + 1}`,
|
||||
type: 'double',
|
||||
homeName: this.friendlyDoubleLabel(homeLabels, i),
|
||||
guestName: this.friendlyDoubleLabel(guestLabels, i),
|
||||
sets: ['', '', '', '', ''],
|
||||
completed: false
|
||||
});
|
||||
}
|
||||
for (let i = 0; i < 12; i += 1) {
|
||||
rows.push({
|
||||
id: `s-${i + 1}`,
|
||||
type: 'single',
|
||||
homeName: homeLabels[i % Math.max(homeLabels.length, 1)] || '',
|
||||
guestName: guestLabels[i % Math.max(guestLabels.length, 1)] || '',
|
||||
sets: ['', '', '', '', ''],
|
||||
completed: false
|
||||
});
|
||||
}
|
||||
return rows;
|
||||
},
|
||||
buildFriendlyResultRows(match) {
|
||||
const existing = this.parseFriendlyArray(match.resultDetails);
|
||||
const generated = this.buildGeneratedFriendlyResultRows(match);
|
||||
if (existing.length) {
|
||||
return existing.map((row, index) => ({
|
||||
id: row.id || `m-${index}`,
|
||||
type: row.type === 'double' ? 'double' : 'single',
|
||||
homeName: row.homeName || generated[index]?.homeName || '',
|
||||
guestName: row.guestName || generated[index]?.guestName || '',
|
||||
sets: Array.from({ length: 5 }, (_, i) => row.sets?.[i] || ''),
|
||||
completed: Boolean(row.completed)
|
||||
}));
|
||||
}
|
||||
return generated;
|
||||
},
|
||||
async openFriendlyResultDialog(match) {
|
||||
await this.loadFriendlyMembers();
|
||||
this.friendlyResultDialog.match = match;
|
||||
this.friendlyResultDialog.rows = this.buildFriendlyResultRows(match);
|
||||
this.friendlyResultDialog.error = '';
|
||||
this.friendlyResultDialog.isOpen = true;
|
||||
},
|
||||
closeFriendlyResultDialog() {
|
||||
this.friendlyResultDialog.isOpen = false;
|
||||
this.friendlyResultDialog.match = null;
|
||||
this.friendlyResultDialog.rows = [];
|
||||
this.friendlyResultDialog.error = '';
|
||||
this.friendlyResultDialog.saving = false;
|
||||
this.friendlyResultDialog.saveAgain = false;
|
||||
},
|
||||
async normalizeFriendlySet(row, index) {
|
||||
const value = String(row.sets[index] || '').trim();
|
||||
if (!value) {
|
||||
this.applyFriendlyRowCompletion(row);
|
||||
await this.autoSaveFriendlyResults();
|
||||
return;
|
||||
}
|
||||
const normalized = this.normalizeFriendlySetValue(value);
|
||||
if (!normalized) {
|
||||
this.friendlyResultDialog.error = 'Bitte gültige Sätze eingeben, z.B. 11:7, 7 oder -7.';
|
||||
return;
|
||||
}
|
||||
row.sets[index] = normalized;
|
||||
this.applyFriendlyRowCompletion(row);
|
||||
this.friendlyResultDialog.error = '';
|
||||
await this.autoSaveFriendlyResults();
|
||||
},
|
||||
normalizeFriendlySetValue(value) {
|
||||
const raw = String(value || '').trim();
|
||||
if (!raw) return '';
|
||||
if (raw.includes(':')) {
|
||||
const parts = raw.split(':');
|
||||
if (parts.length !== 2) return null;
|
||||
const a = Number(parts[0]);
|
||||
const b = Number(parts[1]);
|
||||
if (!Number.isInteger(a) || !Number.isInteger(b) || a < 0 || b < 0) return null;
|
||||
if ((a < 11 && b < 11) || Math.abs(a - b) < 2) return null;
|
||||
return `${a}:${b}`;
|
||||
}
|
||||
const losing = Math.abs(Number(raw));
|
||||
if (!Number.isInteger(losing)) return null;
|
||||
const winning = losing < 10 ? 11 : losing + 2;
|
||||
return raw.startsWith('-') ? `${losing}:${winning}` : `${winning}:${losing}`;
|
||||
},
|
||||
getFriendlyWinningSets() {
|
||||
const value = Number.parseInt(this.friendlyResultDialog.match?.winningSets, 10);
|
||||
return Number.isInteger(value) && value > 0 ? value : 3;
|
||||
},
|
||||
calculateFriendlyRowState(row) {
|
||||
const requiredSets = this.getFriendlyWinningSets();
|
||||
let homeSets = 0;
|
||||
let guestSets = 0;
|
||||
for (let index = 0; index < (row.sets || []).length; index += 1) {
|
||||
const set = row.sets[index];
|
||||
const normalized = this.normalizeFriendlySetValue(set);
|
||||
if (!normalized) continue;
|
||||
const [home, guest] = normalized.split(':').map(Number);
|
||||
if (home > guest) homeSets += 1;
|
||||
if (guest > home) guestSets += 1;
|
||||
if (homeSets >= requiredSets || guestSets >= requiredSets) {
|
||||
return {
|
||||
winner: homeSets > guestSets ? 'home' : 'guest',
|
||||
decisiveIndex: index
|
||||
};
|
||||
}
|
||||
}
|
||||
return { winner: null, decisiveIndex: null };
|
||||
},
|
||||
calculateFriendlyRowWinner(row) {
|
||||
return this.calculateFriendlyRowState(row).winner;
|
||||
},
|
||||
applyFriendlyRowCompletion(row) {
|
||||
const state = this.calculateFriendlyRowState(row);
|
||||
row.completed = Boolean(state.winner);
|
||||
if (state.winner && Number.isInteger(state.decisiveIndex)) {
|
||||
row.sets = row.sets.map((set, index) => index > state.decisiveIndex ? '' : set);
|
||||
}
|
||||
},
|
||||
isFriendlySetClosed(row, index) {
|
||||
const state = this.calculateFriendlyRowState(row);
|
||||
return Boolean(state.winner && Number.isInteger(state.decisiveIndex) && index > state.decisiveIndex);
|
||||
},
|
||||
calculateFriendlyResultScore(rows) {
|
||||
return (rows || []).reduce((score, row) => {
|
||||
const winner = this.calculateFriendlyRowWinner(row);
|
||||
if (winner === 'home') score.home += 1;
|
||||
if (winner === 'guest') score.guest += 1;
|
||||
return score;
|
||||
}, { home: 0, guest: 0 });
|
||||
},
|
||||
async autoSaveFriendlyResults() {
|
||||
if (this.friendlyResultDialog.saving) {
|
||||
this.friendlyResultDialog.saveAgain = true;
|
||||
return;
|
||||
}
|
||||
do {
|
||||
this.friendlyResultDialog.saveAgain = false;
|
||||
await this.saveFriendlyResults(false, { closeDialog: false, reloadMatches: false });
|
||||
} while (this.friendlyResultDialog.saveAgain);
|
||||
},
|
||||
async saveFriendlyResults(isCompleted = false, options = {}) {
|
||||
const { closeDialog = true, reloadMatches = true } = options;
|
||||
const match = this.friendlyResultDialog.match;
|
||||
if (!match) return;
|
||||
for (const row of this.friendlyResultDialog.rows) {
|
||||
const normalizedSets = [];
|
||||
for (const set of row.sets) {
|
||||
if (!String(set || '').trim()) {
|
||||
normalizedSets.push('');
|
||||
continue;
|
||||
}
|
||||
const normalized = this.normalizeFriendlySetValue(set);
|
||||
if (!normalized) {
|
||||
this.friendlyResultDialog.error = 'Bitte ungültige Sätze korrigieren.';
|
||||
return;
|
||||
}
|
||||
normalizedSets.push(normalized);
|
||||
}
|
||||
row.sets = normalizedSets;
|
||||
this.applyFriendlyRowCompletion(row);
|
||||
}
|
||||
const score = this.calculateFriendlyResultScore(this.friendlyResultDialog.rows);
|
||||
try {
|
||||
this.friendlyResultDialog.saving = true;
|
||||
await apiClient.put(`/friendly-matches/${this.currentClub}/${match.id}`, {
|
||||
homeMatchPoints: score.home,
|
||||
guestMatchPoints: score.guest,
|
||||
isCompleted,
|
||||
resultDetails: this.friendlyResultDialog.rows
|
||||
});
|
||||
match.homeMatchPoints = score.home;
|
||||
match.guestMatchPoints = score.guest;
|
||||
match.isCompleted = isCompleted;
|
||||
match.resultDetails = this.friendlyResultDialog.rows.map((row) => ({ ...row, sets: [...row.sets] }));
|
||||
if (closeDialog) {
|
||||
this.closeFriendlyResultDialog();
|
||||
}
|
||||
if (reloadMatches) {
|
||||
await this.loadFriendlyMatches();
|
||||
}
|
||||
} catch (error) {
|
||||
this.friendlyResultDialog.error = getSafeErrorMessage(error, 'Ergebnisse konnten nicht gespeichert werden.');
|
||||
} finally {
|
||||
this.friendlyResultDialog.saving = false;
|
||||
}
|
||||
},
|
||||
async completeFriendlyResults() {
|
||||
await this.saveFriendlyResults(true);
|
||||
},
|
||||
async saveFriendlyMatch() {
|
||||
try {
|
||||
const payload = {
|
||||
...this.friendlyMatchDialog.form,
|
||||
homeParticipants: this.parseFriendlyArray(this.friendlyMatchDialog.form.homeParticipants).map((participant) => ({ ...participant })),
|
||||
guestParticipants: this.parseFriendlyArray(this.friendlyMatchDialog.form.guestParticipants).map((participant) => ({ ...participant }))
|
||||
};
|
||||
const id = this.friendlyMatchDialog.editingId;
|
||||
if (id) {
|
||||
await apiClient.put(`/friendly-matches/${this.currentClub}/${id}`, payload);
|
||||
} else {
|
||||
await apiClient.post(`/friendly-matches/${this.currentClub}`, payload);
|
||||
}
|
||||
this.closeFriendlyMatchDialog();
|
||||
await this.loadFriendlyMatches();
|
||||
} catch (error) {
|
||||
await this.showInfo('Fehler', getSafeErrorMessage(error, 'Freundschaftsspiel konnte nicht gespeichert werden.'), '', 'error');
|
||||
}
|
||||
},
|
||||
async deleteFriendlyMatch() {
|
||||
if (!this.friendlyMatchDialog.editingId) return;
|
||||
const confirmed = await this.showConfirm('Freundschaftsspiel löschen', 'Soll dieses Freundschaftsspiel gelöscht werden?', '', 'warning');
|
||||
if (!confirmed) return;
|
||||
try {
|
||||
await apiClient.delete(`/friendly-matches/${this.currentClub}/${this.friendlyMatchDialog.editingId}`);
|
||||
this.closeFriendlyMatchDialog();
|
||||
await this.loadFriendlyMatches();
|
||||
} catch (error) {
|
||||
await this.showInfo('Fehler', getSafeErrorMessage(error, 'Freundschaftsspiel konnte nicht gelöscht werden.'), '', 'error');
|
||||
}
|
||||
},
|
||||
async importCSV() {
|
||||
if (!this.selectedFile) return;
|
||||
|
||||
@@ -1159,6 +1686,25 @@ export default {
|
||||
this.matches = [];
|
||||
}
|
||||
},
|
||||
async loadFriendlyMatches() {
|
||||
this.selectedLeague = this.friendlyMatchesLabel;
|
||||
this.selectedTeam = null;
|
||||
this.ownLeagueMatches = [];
|
||||
this.allLeagueMatches = [];
|
||||
this.leagueMatchScope = 'own';
|
||||
this.selectedComparisonTeamName = '';
|
||||
this.activeTab = 'schedule';
|
||||
this.leagueTable = [];
|
||||
try {
|
||||
const response = await apiClient.get(`/friendly-matches/${this.currentClub}`);
|
||||
this.friendlyMatches = response.data || [];
|
||||
this.matches = this.sortMatchesByDateTime(this.friendlyMatches);
|
||||
} catch (error) {
|
||||
this.showInfo(this.$t('messages.error'), getSafeErrorMessage(error, 'Freundschaftsspiele konnten nicht geladen werden.'), '', 'error');
|
||||
this.friendlyMatches = [];
|
||||
this.matches = [];
|
||||
}
|
||||
},
|
||||
formatDate(date) {
|
||||
if (!date) return 'N/A';
|
||||
const d = new Date(date);
|
||||
@@ -1401,10 +1947,35 @@ export default {
|
||||
this.loadAllMatches();
|
||||
} else if (this.selectedLeague === this.$t('schedule.adultSchedule')) {
|
||||
this.loadAdultMatches();
|
||||
} else if (this.selectedLeague === this.friendlyMatchesLabel) {
|
||||
this.loadFriendlyMatches();
|
||||
}
|
||||
},
|
||||
|
||||
handleScheduleMatchUpdated(payload) {
|
||||
if (this.friendlyOnly) {
|
||||
if (payload?.match?.isFriendly) {
|
||||
const idx = this.matches.findIndex(m => m.id === payload.match.id);
|
||||
if (idx !== -1) {
|
||||
this.matches.splice(idx, 1, payload.match);
|
||||
} else {
|
||||
this.matches.push(payload.match);
|
||||
}
|
||||
this.friendlyMatches = [...this.matches];
|
||||
this.matches = this.sortMatchesByDateTime(this.matches);
|
||||
return;
|
||||
}
|
||||
if (payload?.matchId != null) {
|
||||
const idx = this.matches.findIndex(m => m.id === payload.matchId);
|
||||
if (idx !== -1) {
|
||||
this.matches.splice(idx, 1);
|
||||
this.friendlyMatches = [...this.matches];
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.refreshScheduleData();
|
||||
return;
|
||||
}
|
||||
if (payload?.match && payload.matchId != null) {
|
||||
const idx = this.matches.findIndex(m => m.id === payload.matchId);
|
||||
if (idx !== -1) {
|
||||
@@ -1437,9 +2008,10 @@ export default {
|
||||
},
|
||||
},
|
||||
async created() {
|
||||
// Teams werden geladen, sobald eine Saison ausgewählt ist
|
||||
// Die SeasonSelector-Komponente wählt automatisch die aktuelle Saison aus
|
||||
// und ruft anschließend onSeasonChange auf, was loadTeams() ausführt
|
||||
if (this.friendlyOnly) {
|
||||
await this.loadFriendlyMatches();
|
||||
return;
|
||||
}
|
||||
this.loadTeams();
|
||||
},
|
||||
beforeUnmount() {
|
||||
@@ -1535,6 +2107,116 @@ td {
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.friendly-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.friendly-form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.friendly-form-grid label,
|
||||
.friendly-participant-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.friendly-form-grid input,
|
||||
.friendly-form-grid select,
|
||||
.friendly-add-row input,
|
||||
.friendly-add-row select {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 0.45rem 0.55rem;
|
||||
border: 1px solid var(--border-color, #ddd);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.friendly-checkbox {
|
||||
flex-direction: row !important;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.friendly-participants {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.friendly-participant-column {
|
||||
border: 1px solid var(--border-color, #ddd);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.friendly-add-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.friendly-participant-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0.5rem 0 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.friendly-participant-list li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
padding: 0.35rem 0.5rem;
|
||||
background: var(--background-soft, #f7f7f7);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.friendly-actions-cell {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.friendly-result-dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.friendly-result-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.friendly-result-table input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 0.35rem 0.45rem;
|
||||
border: 1px solid var(--border-color, #ddd);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.friendly-result-table .player-input {
|
||||
min-width: 12rem;
|
||||
}
|
||||
|
||||
.friendly-result-table .set-input {
|
||||
width: 4.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.friendly-result-error {
|
||||
color: #b00020;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.modal {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
Reference in New Issue
Block a user