Freundschaftsspiele korrigiert
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 52s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 52s
This commit is contained in:
@@ -1,23 +1,24 @@
|
||||
<template>
|
||||
<section class="friendly-participant-column">
|
||||
<h4>{{ title }}</h4>
|
||||
<div class="friendly-add-row">
|
||||
<div v-if="!readonly && allowMembers" 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">
|
||||
<option v-for="member in availableMembers" :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">
|
||||
<p v-else-if="!readonly && memberHint" class="friendly-participant-hint">{{ memberHint }}</p>
|
||||
<div v-if="!readonly && allowManual" 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>
|
||||
<button v-if="!readonly" type="button" @click="$emit('remove', index)">x</button>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
@@ -29,9 +30,21 @@ export default {
|
||||
props: {
|
||||
title: { type: String, required: true },
|
||||
members: { type: Array, required: true },
|
||||
participants: { type: Array, required: true }
|
||||
participants: { type: Array, required: true },
|
||||
allowMembers: { type: Boolean, default: true },
|
||||
allowManual: { type: Boolean, default: true },
|
||||
readonly: { type: Boolean, default: false },
|
||||
memberHint: { type: String, default: '' }
|
||||
},
|
||||
emits: ['add-member', 'add-manual', 'remove'],
|
||||
computed: {
|
||||
availableMembers() {
|
||||
const selected = new Set((this.participants || [])
|
||||
.filter((participant) => participant?.type === 'member')
|
||||
.map((participant) => Number(participant.memberId)));
|
||||
return (this.members || []).filter((member) => !selected.has(Number(member.id)));
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedMemberId: '',
|
||||
@@ -104,4 +117,10 @@ export default {
|
||||
background: var(--background-soft, #f7f7f7);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.friendly-participant-hint {
|
||||
margin: 0;
|
||||
color: var(--text-muted, #666);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,24 +4,30 @@
|
||||
|
||||
<!-- Tab Navigation -->
|
||||
<div class="tab-navigation">
|
||||
<button
|
||||
<button
|
||||
:class="['tab-button', { active: activeTab === 'settings' }]"
|
||||
@click="activeTab = 'settings'"
|
||||
>
|
||||
⚙️ {{ $t('clubSettings.settings') }}
|
||||
</button>
|
||||
<button
|
||||
<button
|
||||
:class="['tab-button', { active: activeTab === 'training-groups' }]"
|
||||
@click="activeTab = 'training-groups'"
|
||||
>
|
||||
👨👩👧👦 {{ $t('clubSettings.trainingGroups') }}
|
||||
</button>
|
||||
<button
|
||||
<button
|
||||
:class="['tab-button', { active: activeTab === 'training-times' }]"
|
||||
@click="activeTab = 'training-times'"
|
||||
>
|
||||
🕐 {{ $t('clubSettings.trainingTimes') }}
|
||||
</button>
|
||||
<button
|
||||
:class="['tab-button', { active: activeTab === 'venues' }]"
|
||||
@click="activeTab = 'venues'"
|
||||
>
|
||||
🏟️ Spiellokale
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Settings Tab -->
|
||||
@@ -138,6 +144,57 @@
|
||||
<TrainingTimesTab />
|
||||
</div>
|
||||
<!-- End Training Times Tab -->
|
||||
|
||||
<!-- Venues Tab -->
|
||||
<div v-if="activeTab === 'venues'" class="venues-tab">
|
||||
<p v-if="!currentClub" class="hint hint-warning">Kein Verein ausgewählt.</p>
|
||||
<p v-else-if="venuesLoading" class="hint">{{ $t('common.loading') }}</p>
|
||||
<p v-else-if="venuesError" class="hint hint-error">{{ venuesError }}</p>
|
||||
|
||||
<section v-if="currentClub" class="card venue-form-card">
|
||||
<h2>{{ venueForm.id ? 'Spiellokal bearbeiten' : 'Spiellokal anlegen' }}</h2>
|
||||
<div class="field-grid">
|
||||
<div class="field-group">
|
||||
<label>Name</label>
|
||||
<input v-model="venueForm.name" class="text-input" placeholder="z. B. Sporthalle Harheim" />
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label>Straße / Adresse</label>
|
||||
<input v-model="venueForm.address" class="text-input" placeholder="Straße und Hausnummer" />
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label>PLZ</label>
|
||||
<input v-model="venueForm.zip" class="text-input" placeholder="60437" />
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label>Ort</label>
|
||||
<input v-model="venueForm.city" class="text-input" placeholder="Frankfurt am Main" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary" @click="saveVenue">{{ venueForm.id ? 'Speichern' : 'Hinzufügen' }}</button>
|
||||
<button v-if="venueForm.id" class="btn btn-secondary" @click="resetVenueForm">Abbrechen</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="currentClub" class="card venues-list-card">
|
||||
<h2>Spiellokale</h2>
|
||||
<p v-if="!venues.length" class="hint">Noch keine Spiellokale angelegt.</p>
|
||||
<div v-else class="venue-list">
|
||||
<div v-for="venue in venues" :key="venue.id" class="venue-row">
|
||||
<div>
|
||||
<strong>{{ venue.name }}</strong>
|
||||
<div class="venue-address">{{ formatVenueAddress(venue) || 'Keine Adresse hinterlegt' }}</div>
|
||||
</div>
|
||||
<div class="venue-actions">
|
||||
<button class="btn btn-secondary" @click="editVenue(venue)">Bearbeiten</button>
|
||||
<button class="btn btn-danger" @click="deleteVenue(venue)">Löschen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<!-- End Venues Tab -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -193,6 +250,10 @@ export default {
|
||||
saved: false,
|
||||
loading: false,
|
||||
loadError: null,
|
||||
venues: [],
|
||||
venuesLoading: false,
|
||||
venuesError: null,
|
||||
venueForm: this.emptyVenueForm(),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -204,7 +265,13 @@ export default {
|
||||
watch: {
|
||||
currentClub: {
|
||||
handler(clubId) {
|
||||
if (clubId) this.loadClubSettings();
|
||||
if (clubId) {
|
||||
this.loadClubSettings();
|
||||
this.loadVenues();
|
||||
} else {
|
||||
this.venues = [];
|
||||
this.resetVenueForm();
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
@@ -267,6 +334,73 @@ export default {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
emptyVenueForm() {
|
||||
return { id: null, name: '', address: '', zip: '', city: '' };
|
||||
},
|
||||
resetVenueForm() {
|
||||
this.venueForm = this.emptyVenueForm();
|
||||
},
|
||||
formatVenueAddress(venue) {
|
||||
return [venue?.address, [venue?.zip, venue?.city].filter(Boolean).join(' ')].filter(Boolean).join(', ');
|
||||
},
|
||||
async loadVenues() {
|
||||
if (!this.currentClub) return;
|
||||
this.venuesLoading = true;
|
||||
this.venuesError = null;
|
||||
try {
|
||||
const response = await apiClient.get(`/club-venues/${this.currentClub}`);
|
||||
this.venues = response.data || [];
|
||||
} catch (e) {
|
||||
this.venuesError = 'Spiellokale konnten nicht geladen werden.';
|
||||
this.venues = [];
|
||||
} finally {
|
||||
this.venuesLoading = false;
|
||||
}
|
||||
},
|
||||
editVenue(venue) {
|
||||
this.venueForm = {
|
||||
id: venue.id,
|
||||
name: venue.name || '',
|
||||
address: venue.address || '',
|
||||
zip: venue.zip || '',
|
||||
city: venue.city || '',
|
||||
};
|
||||
},
|
||||
async saveVenue() {
|
||||
if (!this.currentClub) return;
|
||||
const payload = {
|
||||
name: this.venueForm.name,
|
||||
address: this.venueForm.address,
|
||||
zip: this.venueForm.zip,
|
||||
city: this.venueForm.city,
|
||||
};
|
||||
if (!payload.name.trim()) {
|
||||
alert('Bitte einen Namen für das Spiellokal angeben.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (this.venueForm.id) {
|
||||
await apiClient.put(`/club-venues/${this.currentClub}/${this.venueForm.id}`, payload);
|
||||
} else {
|
||||
await apiClient.post(`/club-venues/${this.currentClub}`, payload);
|
||||
}
|
||||
this.resetVenueForm();
|
||||
await this.loadVenues();
|
||||
} catch (e) {
|
||||
alert('Spiellokal konnte nicht gespeichert werden.');
|
||||
}
|
||||
},
|
||||
async deleteVenue(venue) {
|
||||
if (!this.currentClub || !venue?.id) return;
|
||||
if (!confirm(`Spiellokal "${venue.name}" wirklich löschen?`)) return;
|
||||
try {
|
||||
await apiClient.delete(`/club-venues/${this.currentClub}/${venue.id}`);
|
||||
if (this.venueForm.id === venue.id) this.resetVenueForm();
|
||||
await this.loadVenues();
|
||||
} catch (e) {
|
||||
alert('Spiellokal konnte nicht gelöscht werden.');
|
||||
}
|
||||
},
|
||||
async save() {
|
||||
if (!this.currentClub) {
|
||||
alert(this.$t('clubSettings.noClubSelected'));
|
||||
@@ -326,10 +460,17 @@ export default {
|
||||
.actions { display: flex; align-items: center; gap: 10px; margin-top: 10px; }
|
||||
.btn.btn-primary { background: var(--primary-color); color: #fff; border: none; padding: 8px 12px; border-radius: 6px; cursor: pointer; }
|
||||
.btn.btn-primary:hover { background: var(--primary-hover); }
|
||||
.btn.btn-secondary { background: #f8fafc; color: #1f2937; border: 1px solid #cbd5e1; padding: 8px 12px; border-radius: 6px; cursor: pointer; }
|
||||
.btn.btn-danger { background: #fff5f5; color: #b91c1c; border: 1px solid #fecaca; padding: 8px 12px; border-radius: 6px; cursor: pointer; }
|
||||
.saved-hint { color: #28a745; font-weight: 600; }
|
||||
.hint { color: #666; font-size: 12px; margin-top: 8px; }
|
||||
.hint-warning { color: #856404; background: #fff3cd; padding: 12px; border-radius: 6px; }
|
||||
.hint-error { color: #721c24; background: #f8d7da; padding: 12px; border-radius: 6px; }
|
||||
.venue-form-card, .venues-list-card { margin-bottom: 16px; }
|
||||
.venue-list { display: grid; gap: 10px; }
|
||||
.venue-row { display: flex; justify-content: space-between; gap: 16px; align-items: center; padding: 12px; border: 1px solid #e5e7eb; border-radius: 6px; background: #f9fafb; }
|
||||
.venue-address { margin-top: 4px; color: #666; font-size: 13px; }
|
||||
.venue-actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
|
||||
|
||||
.tab-navigation {
|
||||
display: flex;
|
||||
@@ -363,5 +504,7 @@ export default {
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.field-grid { grid-template-columns: 1fr; }
|
||||
.venue-row { align-items: stretch; flex-direction: column; }
|
||||
.venue-actions { justify-content: flex-start; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -204,7 +204,7 @@
|
||||
<span class="status-label">
|
||||
{{ isOurClubPlayingHome(match) ? $t('schedule.homeLabel') || $t('schedule.homeGame') : $t('schedule.awayLabel') || $t('schedule.away') }}
|
||||
</span>
|
||||
<button type="button" class="btn-small" @click.stop="toggleHomeAway(match)">
|
||||
<button type="button" class="btn-small" :disabled="isFriendlyMatchReadOnly(match)" @click.stop="toggleHomeAway(match)">
|
||||
{{ $t('schedule.swapButton') }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -212,8 +212,8 @@
|
||||
|
||||
<td class="pin-cell">
|
||||
<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>
|
||||
<button type="button" class="btn-secondary" @click.stop="openFriendlyResultDialog(match)">{{ isFriendlyMatchReadOnly(match) ? 'Ansehen' : 'Ergebnis' }}</button>
|
||||
<button type="button" class="btn-secondary" @click.stop="openFriendlyMatchDialog(match)">{{ isFriendlyMatchReadOnly(match) ? 'Details' : 'Bearbeiten' }}</button>
|
||||
</div>
|
||||
<span v-else-if="match.homePin" class="pin-value clickable"
|
||||
@click.stop="copyToClipboard(match.homePin, $t('schedule.homePin'), $event)"
|
||||
@@ -369,6 +369,7 @@
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="member.isReady"
|
||||
:disabled="playerSelectionDialog.readonly"
|
||||
@change="togglePlayerReady(member)"
|
||||
/>
|
||||
</td>
|
||||
@@ -376,6 +377,7 @@
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="member.isPlanned"
|
||||
:disabled="playerSelectionDialog.readonly"
|
||||
@change="togglePlayerPlanned(member)"
|
||||
/>
|
||||
</td>
|
||||
@@ -383,6 +385,7 @@
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="member.hasPlayed"
|
||||
:disabled="playerSelectionDialog.readonly"
|
||||
@change="togglePlayerPlayed(member)"
|
||||
/>
|
||||
</td>
|
||||
@@ -396,7 +399,7 @@
|
||||
</div>
|
||||
|
||||
<div class="dialog-actions">
|
||||
<button @click="savePlayerSelection" class="btn-save">{{ $t('schedule.save') }}</button>
|
||||
<button v-if="!playerSelectionDialog.readonly" @click="savePlayerSelection" class="btn-save">{{ $t('schedule.save') }}</button>
|
||||
<button @click="closePlayerSelectionDialog" class="btn-cancel">{{ $t('schedule.cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -413,7 +416,7 @@
|
||||
<div class="score-summary">
|
||||
<div class="score-display">
|
||||
<span class="score-label">Spielstand:</span>
|
||||
<span class="score-value">{{ friendlyResultScore.home }}:{{ friendlyResultScore.guest }}</span>
|
||||
<span :class="['score-value', friendlyScorePerspectiveClass(friendlyResultScore)]">{{ friendlyResultScore.home }}:{{ friendlyResultScore.guest }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<table class="friendly-result-table">
|
||||
@@ -428,6 +431,8 @@
|
||||
<th>Satz 3</th>
|
||||
<th>Satz 4</th>
|
||||
<th>Satz 5</th>
|
||||
<th>Sätze</th>
|
||||
<th>Punkte</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -435,29 +440,39 @@
|
||||
<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 class="friendly-result-player">{{ row.homeName || '-' }}</td>
|
||||
<td class="friendly-result-player">{{ row.guestName || '-' }}</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)"
|
||||
:disabled="friendlyResultReadonly || isFriendlySetClosed(row, setIndex - 1)"
|
||||
@blur="normalizeFriendlySet(row, setIndex - 1)"
|
||||
/>
|
||||
</td>
|
||||
<td :class="['friendly-result-score-cell', friendlyScorePerspectiveClass(calculateFriendlyRowSets(row))]">{{ friendlyRowSetScore(row) }}</td>
|
||||
<td :class="['friendly-result-score-cell', friendlyScorePerspectiveClass(friendlyRowPointScoreObject(row))]">{{ friendlyRowPointScore(row) }}</td>
|
||||
<td>
|
||||
<button type="button" class="btn-secondary" @click="row.completed = !row.completed">
|
||||
<button type="button" class="btn-secondary" :disabled="friendlyResultReadonly" @click="row.completed = !row.completed">
|
||||
{{ row.completed ? 'Abgeschlossen' : 'Offen' }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="friendly-result-total-row">
|
||||
<td colspan="9">Gesamt</td>
|
||||
<td :class="['friendly-result-score-cell', friendlyScorePerspectiveClass(friendlyResultSetScore)]">{{ friendlyResultSetScore.home }}:{{ friendlyResultSetScore.guest }}</td>
|
||||
<td :class="['friendly-result-score-cell', friendlyScorePerspectiveClass(friendlyResultScore)]">{{ friendlyResultScore.home }}:{{ friendlyResultScore.guest }}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</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 v-if="!friendlyResultReadonly" @click="saveFriendlyResults(false)" class="btn-save">Speichern</button>
|
||||
<button v-if="!friendlyResultReadonly" @click="completeFriendlyResults" class="btn-save">Abschließen</button>
|
||||
<button @click="closeFriendlyResultDialog" class="btn-cancel">{{ $t('schedule.cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -545,44 +560,94 @@
|
||||
>
|
||||
<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>Datum <input v-model="friendlyMatchDialog.form.date" type="date" :disabled="friendlyMatchDialog.readonly" /></label>
|
||||
<label>Uhrzeit <input v-model="friendlyMatchDialog.form.time" type="time" :disabled="friendlyMatchDialog.readonly" /></label>
|
||||
<label>Heimteam <input v-model="friendlyMatchDialog.form.homeTeamName" type="text" :disabled="friendlyMatchDialog.readonly" /></label>
|
||||
<label>Gastteam <input v-model="friendlyMatchDialog.form.guestTeamName" type="text" :disabled="friendlyMatchDialog.readonly" /></label>
|
||||
<label>Spielsystem
|
||||
<select v-model="friendlyMatchDialog.form.matchSystem">
|
||||
<select v-model="friendlyMatchDialog.form.matchSystem" :disabled="friendlyMatchDialog.readonly">
|
||||
<option>Braunschweiger System</option>
|
||||
<option>Bundessystem</option>
|
||||
<option>Werner-Scheffler-System</option>
|
||||
<option>Sechser-Paarkreuz-System</option>
|
||||
<option>Europaliga-System</option>
|
||||
<option>Swaythling-Cup-System</option>
|
||||
<option>Corbillon-Cup-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>
|
||||
<label>Gewinnsätze <input v-model.number="friendlyMatchDialog.form.winningSets" type="number" min="1" :disabled="friendlyMatchDialog.readonly" /></label>
|
||||
<label v-if="canSetFriendlyVenue" class="friendly-venue-field">Spiellokal
|
||||
<select v-model="friendlyMatchDialog.selectedVenueId" :disabled="friendlyMatchDialog.readonly" @change="applyFriendlyVenue">
|
||||
<option value="">Kein Spiellokal ausgewählt</option>
|
||||
<option v-for="venue in friendlyMatchDialog.venues" :key="venue.id" :value="String(venue.id)">{{ venue.name }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<div v-else-if="friendlyVenueSummary" class="friendly-venue-summary">
|
||||
<strong>Spiellokal</strong>
|
||||
<span>{{ friendlyVenueSummary }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="friendly-participants">
|
||||
<FriendlyParticipantsColumn
|
||||
title="Heim-Aufstellung"
|
||||
:members="friendlyMatchDialog.members"
|
||||
:members="friendlyParticipantMemberOptions('homeParticipants')"
|
||||
:participants="friendlyMatchDialog.form.homeParticipants"
|
||||
:allow-members="!friendlyMatchDialog.readonly"
|
||||
:allow-manual="false"
|
||||
:readonly="friendlyMatchDialog.readonly"
|
||||
@add-member="addFriendlyParticipant('homeParticipants', $event)"
|
||||
@add-manual="addManualFriendlyParticipant('homeParticipants', $event)"
|
||||
@remove="removeFriendlyParticipant('homeParticipants', $event)"
|
||||
/>
|
||||
<FriendlyParticipantsColumn
|
||||
title="Gast-Aufstellung"
|
||||
:members="friendlyMatchDialog.members"
|
||||
:members="friendlyParticipantMemberOptions('guestParticipants')"
|
||||
:participants="friendlyMatchDialog.form.guestParticipants"
|
||||
:allow-members="friendlyParticipantMemberOptions('guestParticipants').length > 0"
|
||||
:allow-manual="friendlyParticipantMemberOptions('guestParticipants').length === 0"
|
||||
:readonly="friendlyMatchDialog.readonly"
|
||||
:member-hint="friendlyGuestMemberHint"
|
||||
@add-member="addFriendlyParticipant('guestParticipants', $event)"
|
||||
@add-manual="addManualFriendlyParticipant('guestParticipants', $event)"
|
||||
@remove="removeFriendlyParticipant('guestParticipants', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<section v-if="friendlyEditDoubleRows().length" class="friendly-doubles-section">
|
||||
<h4>Doppel</h4>
|
||||
<div v-for="(row, index) in friendlyEditDoubleRows()" :key="row.id" class="friendly-double-row">
|
||||
<strong>Doppel {{ index + 1 }}</strong>
|
||||
<div class="friendly-double-side">
|
||||
<span>Heim</span>
|
||||
<select :value="friendlyDoublePart(row.homeName, 0)" :disabled="friendlyMatchDialog.readonly" @change="setFriendlyEditDoublePart(row, 'homeName', 0, $event.target.value)">
|
||||
<option value="">Spieler 1</option>
|
||||
<option v-for="name in friendlyEditDoubleOptions('homeParticipants', row, 'homeName', 0)" :key="`ed-h1-${row.id}-${name}`" :value="name">{{ name }}</option>
|
||||
</select>
|
||||
<select :value="friendlyDoublePart(row.homeName, 1)" :disabled="friendlyMatchDialog.readonly" @change="setFriendlyEditDoublePart(row, 'homeName', 1, $event.target.value)">
|
||||
<option value="">Spieler 2</option>
|
||||
<option v-for="name in friendlyEditDoubleOptions('homeParticipants', row, 'homeName', 1)" :key="`ed-h2-${row.id}-${name}`" :value="name">{{ name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="friendly-double-side">
|
||||
<span>Gast</span>
|
||||
<select :value="friendlyDoublePart(row.guestName, 0)" :disabled="friendlyMatchDialog.readonly" @change="setFriendlyEditDoublePart(row, 'guestName', 0, $event.target.value)">
|
||||
<option value="">Spieler 1</option>
|
||||
<option v-for="name in friendlyEditDoubleOptions('guestParticipants', row, 'guestName', 0)" :key="`ed-g1-${row.id}-${name}`" :value="name">{{ name }}</option>
|
||||
</select>
|
||||
<select :value="friendlyDoublePart(row.guestName, 1)" :disabled="friendlyMatchDialog.readonly" @change="setFriendlyEditDoublePart(row, 'guestName', 1, $event.target.value)">
|
||||
<option value="">Spieler 2</option>
|
||||
<option v-for="name in friendlyEditDoubleOptions('guestParticipants', row, 'guestName', 1)" :key="`ed-g2-${row.id}-${name}`" :value="name">{{ name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<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 v-if="!friendlyMatchDialog.readonly" @click="saveFriendlyMatch" class="btn-save">Speichern</button>
|
||||
<button v-if="friendlyMatchDialog.editingId && !friendlyMatchDialog.readonly" @click="deleteFriendlyMatch" class="btn-cancel">Löschen</button>
|
||||
<button @click="closeFriendlyMatchDialog" class="btn-cancel">{{ $t('schedule.cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -659,6 +724,30 @@ export default {
|
||||
friendlyResultScore() {
|
||||
return this.calculateFriendlyResultScore(this.friendlyResultDialog.rows);
|
||||
},
|
||||
friendlyResultSetScore() {
|
||||
return this.calculateFriendlyResultSetScore(this.friendlyResultDialog.rows);
|
||||
},
|
||||
friendlyResultReadonly() {
|
||||
return this.isFriendlyMatchReadOnly(this.friendlyResultDialog.match);
|
||||
},
|
||||
friendlyGuestMemberHint() {
|
||||
const match = this.friendlyMatchDialog.match;
|
||||
if (!match?.isSharedFriendly) return '';
|
||||
if (this.isFriendlyMatchReadOnly(match)) return 'Der Termin ist verstrichen. Die Gastmannschaft ist nur sichtbar.';
|
||||
return 'Gastmitglieder sind erst nach Annahme sichtbar.';
|
||||
},
|
||||
canSetFriendlyVenue() {
|
||||
const match = this.friendlyMatchDialog.match;
|
||||
if (this.friendlyMatchDialog.readonly) return false;
|
||||
if (!match?.isSharedFriendly) return true;
|
||||
return Number(match.homeClubId) === Number(this.currentClub);
|
||||
},
|
||||
friendlyVenueSummary() {
|
||||
const form = this.friendlyMatchDialog.form;
|
||||
return [form.locationName, [form.locationAddress, [form.locationZip, form.locationCity].filter(Boolean).join(' ')].filter(Boolean).join(', ')]
|
||||
.filter(Boolean)
|
||||
.join(' - ');
|
||||
},
|
||||
nextScheduledMatchLabel() {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
@@ -768,7 +857,8 @@ export default {
|
||||
isOpen: false,
|
||||
match: null,
|
||||
members: [],
|
||||
loading: false
|
||||
loading: false,
|
||||
readonly: false
|
||||
},
|
||||
locationDialog: {
|
||||
isOpen: false,
|
||||
@@ -783,7 +873,13 @@ export default {
|
||||
friendlyMatchDialog: {
|
||||
isOpen: false,
|
||||
editingId: null,
|
||||
match: null,
|
||||
members: [],
|
||||
homeMembers: [],
|
||||
guestMembers: [],
|
||||
venues: [],
|
||||
selectedVenueId: '',
|
||||
readonly: false,
|
||||
form: {
|
||||
date: new Date().toISOString().slice(0, 10),
|
||||
time: '',
|
||||
@@ -938,7 +1034,8 @@ export default {
|
||||
guestMatchPoints: 0,
|
||||
isCompleted: false,
|
||||
homeParticipants: [],
|
||||
guestParticipants: []
|
||||
guestParticipants: [],
|
||||
resultDetails: []
|
||||
};
|
||||
},
|
||||
parseFriendlyArray(value) {
|
||||
@@ -953,6 +1050,54 @@ export default {
|
||||
}
|
||||
return [];
|
||||
},
|
||||
friendlyMatchEndsAt(match) {
|
||||
if (!match?.date) return null;
|
||||
const date = String(match.date).slice(0, 10);
|
||||
const value = new Date(`${date}T23:59:59`);
|
||||
return Number.isNaN(value.getTime()) ? null : value;
|
||||
},
|
||||
isFriendlyMatchReadOnly(match) {
|
||||
if (!match?.isFriendly) return false;
|
||||
if (match.isLocked === true) return true;
|
||||
const endsAt = this.friendlyMatchEndsAt(match);
|
||||
return endsAt ? endsAt.getTime() <= Date.now() : false;
|
||||
},
|
||||
sortFriendlyMembers(members) {
|
||||
return (members || []).slice().sort((a, b) => {
|
||||
const fa = (a.firstName || '').toString().toLowerCase();
|
||||
const fb = (b.firstName || '').toString().toLowerCase();
|
||||
if (fa < fb) return -1;
|
||||
if (fa > fb) return 1;
|
||||
return (a.lastName || '').toString().localeCompare((b.lastName || '').toString());
|
||||
});
|
||||
},
|
||||
friendlyMemberIdList(value) {
|
||||
if (Array.isArray(value)) return value.map((id) => Number(id)).filter((id) => Number.isFinite(id));
|
||||
if (typeof value === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return Array.isArray(parsed) ? parsed.map((id) => Number(id)).filter((id) => Number.isFinite(id)) : [];
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
},
|
||||
friendlyEligibleMemberIds(match = this.friendlyMatchDialog.match) {
|
||||
const ready = new Set(this.friendlyMemberIdList(match?.playersReady));
|
||||
const planned = new Set(this.friendlyMemberIdList(match?.playersPlanned));
|
||||
if (!ready.size || !planned.size) return null;
|
||||
return new Set([...ready].filter((id) => planned.has(id)));
|
||||
},
|
||||
friendlyParticipantMemberOptions(field) {
|
||||
const source = field === 'guestParticipants' ? this.friendlyMatchDialog.guestMembers : this.friendlyMatchDialog.homeMembers;
|
||||
const eligible = this.friendlyEligibleMemberIds();
|
||||
if (!eligible) return source;
|
||||
const selected = new Set((this.friendlyMatchDialog.form[field] || [])
|
||||
.filter((participant) => participant?.type === 'member')
|
||||
.map((participant) => Number(participant.memberId)));
|
||||
return source.filter((member) => eligible.has(Number(member.id)) || selected.has(Number(member.id)));
|
||||
},
|
||||
sortMatchesByDateTime(matches) {
|
||||
if (!Array.isArray(matches)) {
|
||||
return [];
|
||||
@@ -1077,6 +1222,7 @@ export default {
|
||||
|
||||
async openPlayerSelectionDialog(match) {
|
||||
this.playerSelectionDialog.match = match;
|
||||
this.playerSelectionDialog.readonly = this.isFriendlyMatchReadOnly(match);
|
||||
this.playerSelectionDialog.isOpen = true;
|
||||
this.playerSelectionDialog.loading = true;
|
||||
|
||||
@@ -1098,11 +1244,17 @@ export default {
|
||||
const playedIds = normalizePlayersList(match.playersPlayed);
|
||||
const preselectedIds = Array.from(new Set([...readyIds, ...plannedIds, ...playedIds]));
|
||||
|
||||
// Fetch members for the current club
|
||||
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;
|
||||
let allMembers = [];
|
||||
if (match.isFriendly && match.isSharedFriendly) {
|
||||
const ownSide = Number(match.homeClubId) === Number(this.currentClub) ? 'home' : 'guest';
|
||||
const response = await apiClient.get(`/friendly-matches/shared/${this.currentClub}/${match.id}/members/${ownSide}`);
|
||||
allMembers = response.data || [];
|
||||
} else {
|
||||
const response = match.isFriendly
|
||||
? await apiClient.get(`/friendly-matches/${this.currentClub}/members/list`)
|
||||
: await apiClient.get(`/clubmembers/get/${this.currentClub}/true`);
|
||||
allMembers = response.data || [];
|
||||
}
|
||||
|
||||
const lineupHalf = this.getLineupHalfForMatch(match);
|
||||
const eligibleMemberIds = match.isFriendly ? [] : await this.getEligibleMemberIdsForSelectedTeam(lineupHalf);
|
||||
@@ -1214,17 +1366,28 @@ export default {
|
||||
|
||||
async savePlayerSelection(closeDialog = true) {
|
||||
const match = this.playerSelectionDialog.match;
|
||||
if (!match) return;
|
||||
if (!match || this.isFriendlyMatchReadOnly(match)) return;
|
||||
|
||||
const playersReady = this.playerSelectionDialog.members
|
||||
.filter(m => m.isReady)
|
||||
.map(m => m.id);
|
||||
const playersPlanned = this.playerSelectionDialog.members
|
||||
.filter(m => m.isPlanned)
|
||||
.map(m => m.id);
|
||||
const playersPlayed = this.playerSelectionDialog.members
|
||||
.filter(m => m.hasPlayed)
|
||||
.map(m => m.id);
|
||||
const normalizePlayersList = (value) => {
|
||||
if (Array.isArray(value)) return value.map((id) => Number(id)).filter((id) => Number.isFinite(id));
|
||||
if (typeof value === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return Array.isArray(parsed) ? parsed.map((id) => Number(id)).filter((id) => Number.isFinite(id)) : [];
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
const visibleIds = new Set(this.playerSelectionDialog.members.map((m) => Number(m.id)));
|
||||
const mergeVisibleSelection = (existing, predicate) => [
|
||||
...normalizePlayersList(existing).filter((id) => !visibleIds.has(Number(id))),
|
||||
...this.playerSelectionDialog.members.filter(predicate).map((m) => Number(m.id)),
|
||||
].filter((id, index, arr) => Number.isFinite(id) && arr.indexOf(id) === index);
|
||||
const playersReady = mergeVisibleSelection(match.playersReady, (m) => m.isReady);
|
||||
const playersPlanned = mergeVisibleSelection(match.playersPlanned, (m) => m.isPlanned);
|
||||
const playersPlayed = mergeVisibleSelection(match.playersPlayed, (m) => m.hasPlayed);
|
||||
|
||||
console.log('[savePlayerSelection] Saving players:', { playersReady, playersPlanned, playersPlayed, matchId: match.id });
|
||||
|
||||
@@ -1386,23 +1549,67 @@ export default {
|
||||
this.selectedFile = file;
|
||||
this.importCSV();
|
||||
},
|
||||
async loadFriendlyMembers() {
|
||||
const response = await apiClient.get(`/friendly-matches/${this.currentClub}/members/list`);
|
||||
const members = response.data || [];
|
||||
// Sort members alphabetically by firstName then lastName
|
||||
this.friendlyMatchDialog.members = members.slice().sort((a, b) => {
|
||||
const fa = (a.firstName || '').toString().toLowerCase();
|
||||
const fb = (b.firstName || '').toString().toLowerCase();
|
||||
if (fa < fb) return -1;
|
||||
if (fa > fb) return 1;
|
||||
const la = (a.lastName || '').toString().toLowerCase();
|
||||
const lb = (b.lastName || '').toString().toLowerCase();
|
||||
return la.localeCompare(lb);
|
||||
});
|
||||
async loadFriendlyVenues(match = null) {
|
||||
this.friendlyMatchDialog.venues = [];
|
||||
this.friendlyMatchDialog.selectedVenueId = '';
|
||||
if (!this.currentClub) return;
|
||||
const canLoad = !match?.isSharedFriendly || Number(match.homeClubId) === Number(this.currentClub);
|
||||
if (!canLoad) return;
|
||||
try {
|
||||
const response = await apiClient.get(`/club-venues/${this.currentClub}`);
|
||||
this.friendlyMatchDialog.venues = response.data || [];
|
||||
} catch (error) {
|
||||
this.friendlyMatchDialog.venues = [];
|
||||
}
|
||||
},
|
||||
findFriendlyVenueForForm() {
|
||||
const form = this.friendlyMatchDialog.form;
|
||||
return this.friendlyMatchDialog.venues.find((venue) => (
|
||||
String(venue.name || '') === String(form.locationName || '')
|
||||
&& String(venue.address || '') === String(form.locationAddress || '')
|
||||
&& String(venue.zip || '') === String(form.locationZip || '')
|
||||
&& String(venue.city || '') === String(form.locationCity || '')
|
||||
));
|
||||
},
|
||||
applyFriendlyVenue() {
|
||||
const venue = this.friendlyMatchDialog.venues.find((item) => String(item.id) === String(this.friendlyMatchDialog.selectedVenueId));
|
||||
if (!venue) {
|
||||
this.friendlyMatchDialog.form.locationName = '';
|
||||
this.friendlyMatchDialog.form.locationAddress = '';
|
||||
this.friendlyMatchDialog.form.locationZip = '';
|
||||
this.friendlyMatchDialog.form.locationCity = '';
|
||||
return;
|
||||
}
|
||||
this.friendlyMatchDialog.form.locationName = venue.name || '';
|
||||
this.friendlyMatchDialog.form.locationAddress = venue.address || '';
|
||||
this.friendlyMatchDialog.form.locationZip = venue.zip || '';
|
||||
this.friendlyMatchDialog.form.locationCity = venue.city || '';
|
||||
},
|
||||
async loadFriendlyMembers(match = null) {
|
||||
const localMembers = async () => {
|
||||
const response = await apiClient.get(`/friendly-matches/${this.currentClub}/members/list`);
|
||||
return this.sortFriendlyMembers(response.data || []);
|
||||
};
|
||||
if (match?.isSharedFriendly) {
|
||||
const [homeResponse, guestResponse] = await Promise.all([
|
||||
apiClient.get(`/friendly-matches/shared/${this.currentClub}/${match.id}/members/home`),
|
||||
apiClient.get(`/friendly-matches/shared/${this.currentClub}/${match.id}/members/guest`),
|
||||
]);
|
||||
this.friendlyMatchDialog.homeMembers = this.sortFriendlyMembers(homeResponse.data || []);
|
||||
this.friendlyMatchDialog.guestMembers = this.sortFriendlyMembers(guestResponse.data || []);
|
||||
this.friendlyMatchDialog.members = [...this.friendlyMatchDialog.homeMembers, ...this.friendlyMatchDialog.guestMembers];
|
||||
return;
|
||||
}
|
||||
const members = await localMembers();
|
||||
this.friendlyMatchDialog.members = members;
|
||||
this.friendlyMatchDialog.homeMembers = members;
|
||||
this.friendlyMatchDialog.guestMembers = [];
|
||||
},
|
||||
async openFriendlyMatchDialog(match = null) {
|
||||
await this.loadFriendlyMembers();
|
||||
await this.loadFriendlyMembers(match);
|
||||
this.friendlyMatchDialog.match = match?.isFriendly ? match : null;
|
||||
this.friendlyMatchDialog.editingId = match?.isFriendly ? match.id : null;
|
||||
this.friendlyMatchDialog.readonly = this.isFriendlyMatchReadOnly(match);
|
||||
this.friendlyMatchDialog.form = match?.isFriendly
|
||||
? {
|
||||
date: match.date ? String(match.date).slice(0, 10) : new Date().toISOString().slice(0, 10),
|
||||
@@ -1421,14 +1628,22 @@ export default {
|
||||
guestMatchPoints: match.guestMatchPoints ?? 0,
|
||||
isCompleted: Boolean(match.isCompleted),
|
||||
homeParticipants: [...this.parseFriendlyArray(match.homeParticipants)],
|
||||
guestParticipants: [...this.parseFriendlyArray(match.guestParticipants)]
|
||||
guestParticipants: [...this.parseFriendlyArray(match.guestParticipants)],
|
||||
resultDetails: [...this.parseFriendlyArray(match.resultDetails)]
|
||||
}
|
||||
: this.emptyFriendlyMatchForm();
|
||||
await this.loadFriendlyVenues(match);
|
||||
const selectedVenue = this.findFriendlyVenueForForm();
|
||||
this.friendlyMatchDialog.selectedVenueId = selectedVenue ? String(selectedVenue.id) : '';
|
||||
this.friendlyMatchDialog.isOpen = true;
|
||||
},
|
||||
closeFriendlyMatchDialog() {
|
||||
this.friendlyMatchDialog.isOpen = false;
|
||||
this.friendlyMatchDialog.editingId = null;
|
||||
this.friendlyMatchDialog.match = null;
|
||||
this.friendlyMatchDialog.readonly = false;
|
||||
this.friendlyMatchDialog.venues = [];
|
||||
this.friendlyMatchDialog.selectedVenueId = '';
|
||||
this.friendlyMatchDialog.form = this.emptyFriendlyMatchForm();
|
||||
},
|
||||
addFriendlyParticipant(field, memberId) {
|
||||
@@ -1448,7 +1663,7 @@ export default {
|
||||
friendlyParticipantLabel(participant, fallback = '') {
|
||||
if (!participant) return fallback;
|
||||
if (participant.type === 'member') {
|
||||
const member = this.friendlyMatchDialog.members.find(m => Number(m.id) === Number(participant.memberId));
|
||||
const member = [...this.friendlyMatchDialog.homeMembers, ...this.friendlyMatchDialog.guestMembers, ...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;
|
||||
@@ -1473,49 +1688,231 @@ export default {
|
||||
const second = labels[secondIndex % labels.length];
|
||||
return first === second ? first : `${first} / ${second}`;
|
||||
},
|
||||
friendlyDoublePart(value, index) {
|
||||
return String(value || '').split('/').map((part) => part.trim())[index] || '';
|
||||
},
|
||||
setFriendlyDoublePart(row, field, index, value) {
|
||||
const parts = [this.friendlyDoublePart(row[field], 0), this.friendlyDoublePart(row[field], 1)];
|
||||
parts[index] = value;
|
||||
row[field] = parts.filter(Boolean).join(' / ');
|
||||
this.autoSaveFriendlyResults();
|
||||
},
|
||||
friendlyResultSideLabels(side) {
|
||||
const participants = side === 'guest'
|
||||
? this.friendlyResultDialog.match?.guestParticipants
|
||||
: this.friendlyResultDialog.match?.homeParticipants;
|
||||
return this.friendlyParticipantLabels(participants);
|
||||
},
|
||||
friendlyResultNameOptions(side, row, field, doubleIndex = null) {
|
||||
const labels = this.friendlyResultSideLabels(side);
|
||||
const used = new Set();
|
||||
for (const candidate of this.friendlyResultDialog.rows || []) {
|
||||
if (candidate === row) continue;
|
||||
if (candidate.type === 'double') {
|
||||
this.friendlyDoublePart(candidate[field], 0) && used.add(this.friendlyDoublePart(candidate[field], 0));
|
||||
this.friendlyDoublePart(candidate[field], 1) && used.add(this.friendlyDoublePart(candidate[field], 1));
|
||||
} else if (candidate[field]) {
|
||||
used.add(candidate[field]);
|
||||
}
|
||||
}
|
||||
if (doubleIndex != null) {
|
||||
const other = this.friendlyDoublePart(row[field], doubleIndex === 0 ? 1 : 0);
|
||||
if (other) used.add(other);
|
||||
}
|
||||
const current = doubleIndex == null ? row[field] : this.friendlyDoublePart(row[field], doubleIndex);
|
||||
return labels.filter((label) => label === current || !used.has(label));
|
||||
},
|
||||
friendlyEditDoubleRows() {
|
||||
const form = this.friendlyMatchDialog.form;
|
||||
const template = this.friendlyResultTemplate({
|
||||
...form,
|
||||
homeParticipants: form.homeParticipants,
|
||||
guestParticipants: form.guestParticipants,
|
||||
});
|
||||
const doubleIds = template.filter((row) => row.type === 'double').map((row) => row.id);
|
||||
if (!Array.isArray(form.resultDetails)) form.resultDetails = [];
|
||||
const existingById = new Map(form.resultDetails
|
||||
.filter((row) => row?.id)
|
||||
.map((row) => [String(row.id), row]));
|
||||
const singles = form.resultDetails.filter((row) => row?.type !== 'double');
|
||||
const doubles = doubleIds.map((id) => {
|
||||
const row = existingById.get(id) || { id, type: 'double', homeName: '', guestName: '', sets: ['', '', '', '', ''], completed: false };
|
||||
row.id = id;
|
||||
row.type = 'double';
|
||||
row.sets = Array.from({ length: 5 }, (_, i) => row.sets?.[i] || '');
|
||||
return row;
|
||||
});
|
||||
form.resultDetails = [...doubles, ...singles];
|
||||
return doubles;
|
||||
},
|
||||
friendlyEditDoubleLabels(field) {
|
||||
return this.friendlyParticipantLabels(this.friendlyMatchDialog.form[field]);
|
||||
},
|
||||
setFriendlyEditDoublePart(row, field, index, value) {
|
||||
const parts = [this.friendlyDoublePart(row[field], 0), this.friendlyDoublePart(row[field], 1)];
|
||||
parts[index] = value;
|
||||
row[field] = parts.filter(Boolean).join(' / ');
|
||||
},
|
||||
friendlyEditDoubleOptions(participantField, row, nameField, doubleIndex) {
|
||||
const labels = this.friendlyEditDoubleLabels(participantField);
|
||||
const rows = this.friendlyEditDoubleRows();
|
||||
const used = new Set();
|
||||
for (const candidate of rows) {
|
||||
if (candidate === row) continue;
|
||||
this.friendlyDoublePart(candidate[nameField], 0) && used.add(this.friendlyDoublePart(candidate[nameField], 0));
|
||||
this.friendlyDoublePart(candidate[nameField], 1) && used.add(this.friendlyDoublePart(candidate[nameField], 1));
|
||||
}
|
||||
const other = this.friendlyDoublePart(row[nameField], doubleIndex === 0 ? 1 : 0);
|
||||
if (other) used.add(other);
|
||||
const current = this.friendlyDoublePart(row[nameField], doubleIndex);
|
||||
return labels.filter((label) => label === current || !used.has(label));
|
||||
},
|
||||
friendlySystemKey(system) {
|
||||
return String(system || '').trim().toLowerCase();
|
||||
},
|
||||
friendlyResultTemplate(match) {
|
||||
const system = this.friendlySystemKey(match.matchSystem);
|
||||
const homeCount = this.friendlyParticipantLabels(match.homeParticipants).length;
|
||||
const guestCount = this.friendlyParticipantLabels(match.guestParticipants).length;
|
||||
const rows = (entries) => entries.map((entry, index) => ({
|
||||
id: entry.id || `${entry.type === 'double' ? 'd' : 's'}-${index + 1}`,
|
||||
type: entry.type,
|
||||
home: entry.home,
|
||||
guest: entry.guest,
|
||||
}));
|
||||
const d = (id, home, guest) => ({ id, type: 'double', home, guest });
|
||||
const s = (id, home, guest) => ({ id, type: 'single', home, guest });
|
||||
if (system.includes('bundessystem')) {
|
||||
return rows([
|
||||
d('d-1', 'D1', 'D1'), d('d-2', 'D2', 'D2'),
|
||||
s('s-1', 'A1', 'B1'), s('s-2', 'A2', 'B2'), s('s-3', 'A3', 'B3'), s('s-4', 'A4', 'B4'),
|
||||
s('s-5', 'A1', 'B2'), s('s-6', 'A2', 'B1'), s('s-7', 'A3', 'B4'), s('s-8', 'A4', 'B3'),
|
||||
]);
|
||||
}
|
||||
if (system.includes('werner')) {
|
||||
return rows([
|
||||
d('d-1', 'D1', 'D1'), d('d-2', 'D2', 'D2'),
|
||||
s('s-1', 'A1', 'B2'), s('s-2', 'A2', 'B1'), s('s-3', 'A3', 'B4'), s('s-4', 'A4', 'B3'),
|
||||
s('s-5', 'A1', 'B1'), s('s-6', 'A2', 'B2'), s('s-7', 'A3', 'B3'), s('s-8', 'A4', 'B4'),
|
||||
]);
|
||||
}
|
||||
if (system.includes('sechser')) {
|
||||
return rows([
|
||||
d('d-1', 'D1', 'D1'), d('d-2', 'D2', 'D2'), d('d-3', 'D3', 'D3'),
|
||||
s('s-1', 'A1', 'B2'), s('s-2', 'A2', 'B1'), s('s-3', 'A1', 'B1'), s('s-4', 'A2', 'B2'),
|
||||
s('s-5', 'A3', 'B4'), s('s-6', 'A4', 'B3'), s('s-7', 'A3', 'B3'), s('s-8', 'A4', 'B4'),
|
||||
s('s-9', 'A5', 'B6'), s('s-10', 'A6', 'B5'), s('s-11', 'A5', 'B5'), s('s-12', 'A6', 'B6'),
|
||||
d('d-4', 'D1', 'D1'),
|
||||
]);
|
||||
}
|
||||
if (system.includes('europaliga')) {
|
||||
return rows([
|
||||
d('d-1', 'D1', 'D1'), d('d-2', 'D2', 'D2'), d('d-3', 'D3', 'D3'),
|
||||
s('s-1', 'A1', 'B1'), s('s-2', 'A2', 'B2'), s('s-3', 'A1', 'B2'), s('s-4', 'A2', 'B1'),
|
||||
s('s-5', 'A3', 'B3'), s('s-6', 'A4', 'B4'), s('s-7', 'A3', 'B4'), s('s-8', 'A4', 'B3'),
|
||||
s('s-9', 'A5', 'B5'), s('s-10', 'A6', 'B6'), s('s-11', 'A5', 'B6'), s('s-12', 'A6', 'B5'),
|
||||
]);
|
||||
}
|
||||
if (system.includes('corbillon')) {
|
||||
return rows([
|
||||
s('s-1', 'A1', 'B1'), s('s-2', 'A2', 'B2'), d('d-1', 'D1', 'D1'), s('s-3', 'A1', 'B2'), s('s-4', 'A2', 'B1'),
|
||||
]);
|
||||
}
|
||||
if (system.includes('modifiziertes swaythling')) {
|
||||
return rows([
|
||||
s('s-1', 'A1', 'B2'), s('s-2', 'A2', 'B1'), s('s-3', 'A3', 'B3'), d('d-1', 'D1', 'D1'), s('s-4', 'A1', 'B1'), s('s-5', 'A3', 'B2'), s('s-6', 'A2', 'B3'),
|
||||
]);
|
||||
}
|
||||
if (system.includes('swaythling')) {
|
||||
return rows([
|
||||
s('s-1', 'A1', 'B1'), s('s-2', 'A2', 'B2'), s('s-3', 'A3', 'B3'), s('s-4', 'A1', 'B2'), s('s-5', 'A2', 'B1'),
|
||||
]);
|
||||
}
|
||||
if (system.includes('braunschweiger')) {
|
||||
if (homeCount >= 4 && guestCount >= 4) {
|
||||
return rows([
|
||||
d('d-1', 'D1', 'D1'), d('d-2', 'D2', 'D2'),
|
||||
s('s-1', 'A1', 'B1'), s('s-2', 'A2', 'B2'), s('s-3', 'A3', 'B3'), s('s-4', 'A4', 'B4'),
|
||||
s('s-5', 'A1', 'B2'), s('s-6', 'A2', 'B1'), s('s-7', 'A3', 'B4'), s('s-8', 'A4', 'B3'),
|
||||
]);
|
||||
}
|
||||
if (homeCount >= 4 && guestCount <= 3) {
|
||||
return rows([
|
||||
d('d-1', 'D1', 'D1'),
|
||||
s('s-1', 'A3', 'B3'), s('s-2', 'A1', 'B2'), s('s-3', 'A2', 'B1'), s('s-4', 'A4', 'B2'),
|
||||
s('s-5', 'A1', 'B1'), s('s-6', 'A4', 'B3'), s('s-7', 'A2', 'B2'), s('s-8', 'A1', 'B3'), s('s-9', 'A3', 'B1'),
|
||||
]);
|
||||
}
|
||||
if (homeCount <= 3 && guestCount >= 4) {
|
||||
return rows([
|
||||
d('d-1', 'D1', 'D1'),
|
||||
s('s-1', 'A3', 'B3'), s('s-2', 'A2', 'B1'), s('s-3', 'A1', 'B2'), s('s-4', 'A2', 'B4'),
|
||||
s('s-5', 'A1', 'B1'), s('s-6', 'A3', 'B4'), s('s-7', 'A2', 'B2'), s('s-8', 'A3', 'B1'), s('s-9', 'A1', 'B3'),
|
||||
]);
|
||||
}
|
||||
return rows([
|
||||
d('d-1', 'D1', 'D1'),
|
||||
s('s-1', 'A1', 'B2'), s('s-2', 'A2', 'B1'), s('s-3', 'A3', 'B2'), s('s-4', 'A2', 'B3'),
|
||||
s('s-5', 'A1', 'B1'), s('s-6', 'A3', 'B3'), s('s-7', 'A2', 'B2'), s('s-8', 'A3', 'B1'), s('s-9', 'A1', 'B3'),
|
||||
]);
|
||||
}
|
||||
const doublesCount = Number.parseInt(match.doublesCount, 10) || 0;
|
||||
const singlesCount = Number.parseInt(match.singlesCount, 10) || 0;
|
||||
return rows([
|
||||
...Array.from({ length: doublesCount }, (_, i) => d(`d-${i + 1}`, `D${i + 1}`, `D${i + 1}`)),
|
||||
...Array.from({ length: singlesCount }, (_, i) => s(`s-${i + 1}`, `A${(i % Math.max(homeCount, 1)) + 1}`, `B${(i % Math.max(guestCount, 1)) + 1}`)),
|
||||
]);
|
||||
},
|
||||
friendlyPlayerForCode(labels, code) {
|
||||
const match = String(code || '').match(/[AB](\d+)/i);
|
||||
if (!match) return '';
|
||||
return labels[Number(match[1]) - 1] || '';
|
||||
},
|
||||
friendlyDoubleForCode(match, labels, side, code) {
|
||||
const number = Number(String(code || '').match(/D[A-Z]?(\d+)/i)?.[1] || 1);
|
||||
const row = this.parseFriendlyArray(match.resultDetails).find((candidate) => String(candidate?.id) === `d-${number}`);
|
||||
const value = row?.[side === 'guest' ? 'guestName' : 'homeName'];
|
||||
return value || this.friendlyDoubleLabel(labels, number - 1);
|
||||
},
|
||||
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;
|
||||
return this.friendlyResultTemplate(match).map((templateRow) => ({
|
||||
id: templateRow.id,
|
||||
type: templateRow.type,
|
||||
homeName: templateRow.type === 'double'
|
||||
? this.friendlyDoubleForCode(match, homeLabels, 'home', templateRow.home)
|
||||
: this.friendlyPlayerForCode(homeLabels, templateRow.home),
|
||||
guestName: templateRow.type === 'double'
|
||||
? this.friendlyDoubleForCode(match, guestLabels, 'guest', templateRow.guest)
|
||||
: this.friendlyPlayerForCode(guestLabels, templateRow.guest),
|
||||
sets: ['', '', '', '', ''],
|
||||
completed: false
|
||||
}));
|
||||
},
|
||||
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;
|
||||
if (!existing.length) return generated;
|
||||
|
||||
const existingById = new Map(existing
|
||||
.filter((row) => row?.id)
|
||||
.map((row) => [String(row.id), row]));
|
||||
|
||||
return generated.map((generatedRow, index) => {
|
||||
const existingRow = existingById.get(String(generatedRow.id)) || existing[index] || null;
|
||||
if (!existingRow) return generatedRow;
|
||||
return {
|
||||
...generatedRow,
|
||||
homeName: generatedRow.homeName || '',
|
||||
guestName: generatedRow.guestName || '',
|
||||
sets: Array.from({ length: 5 }, (_, i) => existingRow.sets?.[i] || generatedRow.sets?.[i] || ''),
|
||||
completed: Boolean(existingRow.completed),
|
||||
};
|
||||
});
|
||||
},
|
||||
async openFriendlyResultDialog(match) {
|
||||
await this.loadFriendlyMembers();
|
||||
await this.loadFriendlyMembers(match);
|
||||
this.friendlyResultDialog.match = match;
|
||||
this.friendlyResultDialog.rows = this.buildFriendlyResultRows(match);
|
||||
this.friendlyResultDialog.error = '';
|
||||
@@ -1555,7 +1952,7 @@ export default {
|
||||
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;
|
||||
if (Math.max(a, b) < 11 || Math.abs(a - b) < 2) return null;
|
||||
return `${a}:${b}`;
|
||||
}
|
||||
const losing = Math.abs(Number(raw));
|
||||
@@ -1587,6 +1984,50 @@ export default {
|
||||
}
|
||||
return { winner: null, decisiveIndex: null };
|
||||
},
|
||||
calculateFriendlyRowSets(row) {
|
||||
const requiredSets = this.getFriendlyWinningSets();
|
||||
let homeSets = 0;
|
||||
let guestSets = 0;
|
||||
for (const set of row.sets || []) {
|
||||
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) break;
|
||||
}
|
||||
return { home: homeSets, guest: guestSets };
|
||||
},
|
||||
friendlyRowSetScore(row) {
|
||||
const sets = this.calculateFriendlyRowSets(row);
|
||||
return `${sets.home}:${sets.guest}`;
|
||||
},
|
||||
friendlyRowPointScore(row) {
|
||||
const score = this.friendlyRowPointScoreObject(row);
|
||||
return `${score.home}:${score.guest}`;
|
||||
},
|
||||
friendlyRowPointScoreObject(row) {
|
||||
const winner = this.calculateFriendlyRowWinner(row);
|
||||
if (winner === 'home') return { home: 1, guest: 0 };
|
||||
if (winner === 'guest') return { home: 0, guest: 1 };
|
||||
return { home: 0, guest: 0 };
|
||||
},
|
||||
friendlyResultViewerSide() {
|
||||
const match = this.friendlyResultDialog.match;
|
||||
if (match?.isSharedFriendly) {
|
||||
if (Number(match.homeClubId) === Number(this.currentClub)) return 'home';
|
||||
if (Number(match.guestClubId) === Number(this.currentClub)) return 'guest';
|
||||
}
|
||||
return this.isOurClubPlayingHome(match) ? 'home' : 'guest';
|
||||
},
|
||||
friendlyScorePerspectiveClass(score) {
|
||||
const home = Number(score?.home || 0);
|
||||
const guest = Number(score?.guest || 0);
|
||||
if (home === guest) return 'score-even';
|
||||
const viewerSide = this.friendlyResultViewerSide();
|
||||
const viewerLeading = viewerSide === 'guest' ? guest > home : home > guest;
|
||||
return viewerLeading ? 'score-leading' : 'score-trailing';
|
||||
},
|
||||
calculateFriendlyRowWinner(row) {
|
||||
return this.calculateFriendlyRowState(row).winner;
|
||||
},
|
||||
@@ -1609,6 +2050,17 @@ export default {
|
||||
return score;
|
||||
}, { home: 0, guest: 0 });
|
||||
},
|
||||
calculateFriendlyResultSetScore(rows) {
|
||||
return (rows || []).reduce((score, row) => {
|
||||
const sets = this.calculateFriendlyRowSets(row);
|
||||
score.home += sets.home;
|
||||
score.guest += sets.guest;
|
||||
return score;
|
||||
}, { home: 0, guest: 0 });
|
||||
},
|
||||
isFriendlyResultComplete(rows = this.friendlyResultDialog.rows) {
|
||||
return Array.isArray(rows) && rows.length > 0 && rows.every((row) => Boolean(this.calculateFriendlyRowWinner(row)));
|
||||
},
|
||||
async autoSaveFriendlyResults() {
|
||||
if (this.friendlyResultDialog.saving) {
|
||||
this.friendlyResultDialog.saveAgain = true;
|
||||
@@ -1622,7 +2074,7 @@ export default {
|
||||
async saveFriendlyResults(isCompleted = false, options = {}) {
|
||||
const { closeDialog = true, reloadMatches = true } = options;
|
||||
const match = this.friendlyResultDialog.match;
|
||||
if (!match) return;
|
||||
if (!match || this.isFriendlyMatchReadOnly(match)) return;
|
||||
for (const row of this.friendlyResultDialog.rows) {
|
||||
const normalizedSets = [];
|
||||
for (const set of row.sets) {
|
||||
@@ -1641,17 +2093,18 @@ export default {
|
||||
this.applyFriendlyRowCompletion(row);
|
||||
}
|
||||
const score = this.calculateFriendlyResultScore(this.friendlyResultDialog.rows);
|
||||
const completed = Boolean(isCompleted || this.isFriendlyResultComplete(this.friendlyResultDialog.rows));
|
||||
try {
|
||||
this.friendlyResultDialog.saving = true;
|
||||
await apiClient.put(`${match.isSharedFriendly ? `/friendly-matches/shared/${this.currentClub}/${match.id}` : `/friendly-matches/${this.currentClub}/${match.id}`}`, {
|
||||
homeMatchPoints: score.home,
|
||||
guestMatchPoints: score.guest,
|
||||
isCompleted,
|
||||
isCompleted: completed,
|
||||
resultDetails: this.friendlyResultDialog.rows
|
||||
});
|
||||
match.homeMatchPoints = score.home;
|
||||
match.guestMatchPoints = score.guest;
|
||||
match.isCompleted = isCompleted;
|
||||
match.isCompleted = completed;
|
||||
match.resultDetails = this.friendlyResultDialog.rows.map((row) => ({ ...row, sets: [...row.sets] }));
|
||||
if (closeDialog) {
|
||||
this.closeFriendlyResultDialog();
|
||||
@@ -1669,6 +2122,8 @@ export default {
|
||||
await this.saveFriendlyResults(true);
|
||||
},
|
||||
async saveFriendlyMatch() {
|
||||
if (this.friendlyMatchDialog.readonly) return;
|
||||
this.friendlyEditDoubleRows();
|
||||
try {
|
||||
const payload = {
|
||||
...this.friendlyMatchDialog.form,
|
||||
@@ -1693,7 +2148,7 @@ export default {
|
||||
}
|
||||
},
|
||||
async toggleHomeAway(match) {
|
||||
if (!match || !match.id) return;
|
||||
if (!match || !match.id || this.isFriendlyMatchReadOnly(match)) return;
|
||||
const originalHome = match.homeTeam ? { ...match.homeTeam } : { name: '' };
|
||||
const originalGuest = match.guestTeam ? { ...match.guestTeam } : { name: '' };
|
||||
// Optimistic UI update: swap locally
|
||||
@@ -1727,7 +2182,7 @@ export default {
|
||||
}
|
||||
},
|
||||
async deleteFriendlyMatch() {
|
||||
if (!this.friendlyMatchDialog.editingId) return;
|
||||
if (!this.friendlyMatchDialog.editingId || this.friendlyMatchDialog.readonly) return;
|
||||
const confirmed = await this.showConfirm('Freundschaftsspiel löschen', 'Soll dieses Freundschaftsspiel gelöscht werden?', '', 'warning');
|
||||
if (!confirmed) return;
|
||||
try {
|
||||
@@ -2474,6 +2929,14 @@ td {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.friendly-venue-summary {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
color: #374151;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.friendly-checkbox {
|
||||
flex-direction: row !important;
|
||||
align-items: center;
|
||||
@@ -2515,6 +2978,34 @@ td {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.friendly-doubles-section {
|
||||
border: 1px solid var(--border-color, #ddd);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.friendly-double-row {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
background: var(--background-soft, #f7f7f7);
|
||||
border-radius: 6px;
|
||||
padding: 0.65rem;
|
||||
}
|
||||
|
||||
.friendly-double-side {
|
||||
display: grid;
|
||||
grid-template-columns: 4rem 1fr 1fr;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.friendly-double-side select {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.friendly-actions-cell {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
@@ -2532,7 +3023,8 @@ td {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.friendly-result-table input {
|
||||
.friendly-result-table input,
|
||||
.friendly-result-table select {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 0.35rem 0.45rem;
|
||||
@@ -2544,11 +3036,37 @@ td {
|
||||
min-width: 12rem;
|
||||
}
|
||||
|
||||
.friendly-double-select {
|
||||
display: grid;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.friendly-result-table .set-input {
|
||||
width: 4.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.friendly-result-score-cell,
|
||||
.score-value {
|
||||
border-radius: 4px;
|
||||
padding: 0.2rem 0.45rem;
|
||||
}
|
||||
|
||||
.score-leading {
|
||||
color: #0f7a3b;
|
||||
background: #e8f7ee;
|
||||
}
|
||||
|
||||
.score-trailing {
|
||||
color: #b42318;
|
||||
background: #fff0ee;
|
||||
}
|
||||
|
||||
.score-even {
|
||||
color: #175cd3;
|
||||
background: #eef4ff;
|
||||
}
|
||||
|
||||
.friendly-result-error {
|
||||
color: #b00020;
|
||||
font-weight: 600;
|
||||
|
||||
Reference in New Issue
Block a user