Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 44s
This commit updates the Spieler management interface by replacing the text input for moving players with a dropdown select, streamlining the process of transferring players between teams. It also adjusts the layout for better responsiveness and user experience, ensuring that player names are pre-selected based on their current team. These changes enhance the overall functionality and accessibility of the Mannschaften component.
824 lines
27 KiB
Vue
824 lines
27 KiB
Vue
<template>
|
|
<div class="min-h-full py-16 bg-gray-50">
|
|
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div class="flex justify-between items-center mb-6">
|
|
<div>
|
|
<h1 class="text-4xl sm:text-5xl font-display font-bold text-gray-900 mb-2">
|
|
Mannschaften verwalten
|
|
</h1>
|
|
<div class="w-24 h-1 bg-primary-600 mb-4" />
|
|
</div>
|
|
<button
|
|
class="flex items-center px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white font-semibold rounded-lg transition-colors"
|
|
@click="openAddModal"
|
|
>
|
|
<Plus
|
|
:size="20"
|
|
class="mr-2"
|
|
/>
|
|
Mannschaft hinzufügen
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Loading State -->
|
|
<div
|
|
v-if="isLoading"
|
|
class="flex items-center justify-center py-12"
|
|
>
|
|
<Loader2
|
|
:size="40"
|
|
class="animate-spin text-primary-600"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Mannschaften Table -->
|
|
<div
|
|
v-else
|
|
class="bg-white rounded-xl shadow-lg overflow-hidden"
|
|
>
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full divide-y divide-gray-200">
|
|
<thead class="bg-gray-50">
|
|
<tr>
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Mannschaft
|
|
</th>
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Liga
|
|
</th>
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Staffelleiter
|
|
</th>
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Mannschaftsführer
|
|
</th>
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Spieler
|
|
</th>
|
|
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Aktionen
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white divide-y divide-gray-200">
|
|
<tr
|
|
v-for="(mannschaft, index) in mannschaften"
|
|
:key="index"
|
|
class="hover:bg-gray-50"
|
|
>
|
|
<td class="px-4 py-3 text-sm font-medium text-gray-900">
|
|
{{ mannschaft.mannschaft }}
|
|
</td>
|
|
<td class="px-4 py-3 text-sm text-gray-600">
|
|
{{ mannschaft.liga }}
|
|
</td>
|
|
<td class="px-4 py-3 text-sm text-gray-600">
|
|
{{ mannschaft.staffelleiter }}
|
|
</td>
|
|
<td class="px-4 py-3 text-sm text-gray-600">
|
|
{{ mannschaft.mannschaftsfuehrer }}
|
|
</td>
|
|
<td class="px-4 py-3 text-sm text-gray-600">
|
|
<div class="max-w-xs truncate">
|
|
{{ getSpielerListe(mannschaft).join(', ') || '-' }}
|
|
</div>
|
|
</td>
|
|
<td class="px-4 py-3 whitespace-nowrap text-right text-sm font-medium space-x-3">
|
|
<button
|
|
class="text-gray-600 hover:text-gray-900"
|
|
title="Bearbeiten"
|
|
@click="openEditModal(mannschaft, index)"
|
|
>
|
|
<Pencil
|
|
:size="18"
|
|
/>
|
|
</button>
|
|
<button
|
|
class="text-red-600 hover:text-red-900"
|
|
title="Löschen"
|
|
@click="confirmDelete(mannschaft, index)"
|
|
>
|
|
<Trash2
|
|
:size="18"
|
|
/>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Empty State -->
|
|
<div
|
|
v-if="!isLoading && mannschaften.length === 0"
|
|
class="bg-white rounded-xl shadow-lg p-12 text-center"
|
|
>
|
|
<Users
|
|
:size="48"
|
|
class="text-gray-400 mx-auto mb-4"
|
|
/>
|
|
<h3 class="text-lg font-medium text-gray-900 mb-2">
|
|
Keine Mannschaften vorhanden
|
|
</h3>
|
|
<p class="text-gray-600 mb-6">
|
|
Fügen Sie die erste Mannschaft hinzu.
|
|
</p>
|
|
<button
|
|
class="px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white font-semibold rounded-lg transition-colors"
|
|
@click="openAddModal"
|
|
>
|
|
Mannschaft hinzufügen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add/Edit Modal -->
|
|
<div
|
|
v-if="showModal"
|
|
class="fixed inset-0 z-50 bg-black/50 flex items-center justify-center p-4"
|
|
@click.self="closeModal"
|
|
>
|
|
<div class="bg-white rounded-xl shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
|
<div class="p-6 border-b border-gray-200">
|
|
<h2 class="text-2xl font-display font-bold text-gray-900">
|
|
{{ isEditing ? 'Mannschaft bearbeiten' : 'Neue Mannschaft' }}
|
|
</h2>
|
|
</div>
|
|
|
|
<form
|
|
class="p-6 space-y-4"
|
|
@submit.prevent="saveMannschaft"
|
|
>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
Mannschaft *
|
|
</label>
|
|
<input
|
|
v-model="formData.mannschaft"
|
|
type="text"
|
|
required
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
|
|
:disabled="isSaving"
|
|
>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
Liga *
|
|
</label>
|
|
<input
|
|
v-model="formData.liga"
|
|
type="text"
|
|
required
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
|
|
:disabled="isSaving"
|
|
>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
Staffelleiter
|
|
</label>
|
|
<input
|
|
v-model="formData.staffelleiter"
|
|
type="text"
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
|
|
:disabled="isSaving"
|
|
>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
Telefon
|
|
</label>
|
|
<input
|
|
v-model="formData.telefon"
|
|
type="tel"
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
|
|
:disabled="isSaving"
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
Heimspieltag
|
|
</label>
|
|
<input
|
|
v-model="formData.heimspieltag"
|
|
type="text"
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
|
|
:disabled="isSaving"
|
|
>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
Spielsystem
|
|
</label>
|
|
<input
|
|
v-model="formData.spielsystem"
|
|
type="text"
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
|
|
:disabled="isSaving"
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
Mannschaftsführer
|
|
</label>
|
|
<input
|
|
v-model="formData.mannschaftsfuehrer"
|
|
type="text"
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
|
|
:disabled="isSaving"
|
|
>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
Spieler
|
|
</label>
|
|
<div class="space-y-2">
|
|
<div
|
|
v-if="formData.spielerListe.length === 0"
|
|
class="text-sm text-gray-500"
|
|
>
|
|
Noch keine Spieler eingetragen.
|
|
</div>
|
|
|
|
<div
|
|
v-for="(spieler, index) in formData.spielerListe"
|
|
:key="spieler.id"
|
|
class="px-3 py-2 border border-gray-200 rounded-lg bg-white"
|
|
>
|
|
<div class="flex flex-col lg:flex-row lg:items-center gap-2">
|
|
<input
|
|
v-model="spieler.name"
|
|
type="text"
|
|
class="flex-1 min-w-[14rem] px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
|
|
placeholder="Spielername"
|
|
:disabled="isSaving"
|
|
>
|
|
|
|
<!-- Reihenfolge -->
|
|
<div class="flex items-center gap-1">
|
|
<button
|
|
type="button"
|
|
class="p-2 border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
title="Nach oben"
|
|
:disabled="isSaving || index === 0"
|
|
@click="moveSpielerUp(index)"
|
|
>
|
|
<ChevronUp :size="18" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="p-2 border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
title="Nach unten"
|
|
:disabled="isSaving || index === formData.spielerListe.length - 1"
|
|
@click="moveSpielerDown(index)"
|
|
>
|
|
<ChevronDown :size="18" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="p-2 border border-red-300 text-red-700 rounded-lg hover:bg-red-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
title="Spieler entfernen"
|
|
:disabled="isSaving"
|
|
@click="removeSpieler(spieler.id)"
|
|
>
|
|
<Trash2 :size="18" />
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Verschieben (kompakt in gleicher Zeile) -->
|
|
<div class="flex items-center gap-2">
|
|
<select
|
|
v-model="moveTargetBySpielerId[spieler.id]"
|
|
class="min-w-[14rem] px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
:disabled="isSaving || !isEditing || mannschaftenSelectOptions.length <= 1"
|
|
title="Mannschaft auswählen"
|
|
>
|
|
<option
|
|
v-for="t in mannschaftenSelectOptions"
|
|
:key="t"
|
|
:value="t"
|
|
>
|
|
{{ t }}
|
|
</option>
|
|
</select>
|
|
|
|
<button
|
|
type="button"
|
|
class="inline-flex items-center justify-center px-3 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
:disabled="isSaving || !isEditing || mannschaftenSelectOptions.length <= 1 || !canMoveSpieler(spieler.id)"
|
|
@click="moveSpielerToMannschaft(spieler.id)"
|
|
title="In ausgewählte Mannschaft verschieben"
|
|
>
|
|
<ArrowRight :size="18" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-3 flex items-center justify-between">
|
|
<button
|
|
type="button"
|
|
class="inline-flex items-center px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-800 font-semibold rounded-lg transition-colors"
|
|
:disabled="isSaving"
|
|
@click="addSpieler()"
|
|
>
|
|
<Plus
|
|
:size="18"
|
|
class="mr-2"
|
|
/>
|
|
Spieler hinzufügen
|
|
</button>
|
|
<p class="text-xs text-gray-500">
|
|
Reihenfolge per ↑/↓ ändern. Verschieben nur bei bestehenden Mannschaften.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
Weitere Informationen (Link)
|
|
</label>
|
|
<input
|
|
v-model="formData.weitere_informationen_link"
|
|
type="url"
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
|
|
placeholder="https://..."
|
|
:disabled="isSaving"
|
|
>
|
|
</div>
|
|
|
|
<div
|
|
v-if="errorMessage"
|
|
class="flex items-center p-3 rounded-md bg-red-50 text-red-700 text-sm"
|
|
>
|
|
<AlertCircle
|
|
:size="20"
|
|
class="mr-2"
|
|
/>
|
|
{{ errorMessage }}
|
|
</div>
|
|
|
|
<div class="flex justify-end space-x-4 pt-4">
|
|
<button
|
|
type="button"
|
|
class="px-6 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
|
:disabled="isSaving"
|
|
@click="closeModal"
|
|
>
|
|
Abbrechen
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
class="px-6 py-2 bg-primary-600 hover:bg-primary-700 text-white font-semibold rounded-lg transition-colors flex items-center"
|
|
:disabled="isSaving"
|
|
>
|
|
<Loader2
|
|
v-if="isSaving"
|
|
:size="20"
|
|
class="animate-spin mr-2"
|
|
/>
|
|
<span>{{ isSaving ? 'Speichert...' : 'Speichern' }}</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, computed } from 'vue'
|
|
import { Plus, Trash2, Loader2, AlertCircle, Pencil, Users, ChevronUp, ChevronDown, ArrowRight } from 'lucide-vue-next'
|
|
|
|
const isLoading = ref(true)
|
|
const isSaving = ref(false)
|
|
const mannschaften = ref([])
|
|
const showModal = ref(false)
|
|
const errorMessage = ref('')
|
|
const isEditing = ref(false)
|
|
const editingIndex = ref(-1)
|
|
|
|
const formData = ref({
|
|
mannschaft: '',
|
|
liga: '',
|
|
staffelleiter: '',
|
|
telefon: '',
|
|
heimspieltag: '',
|
|
spielsystem: '',
|
|
mannschaftsfuehrer: '',
|
|
spielerListe: [],
|
|
weitere_informationen_link: '',
|
|
letzte_aktualisierung: ''
|
|
})
|
|
|
|
// Für Verschieben-UI (Combobox pro Spieler)
|
|
const moveTargetBySpielerId = ref({})
|
|
// Pending-Änderungen für andere Teams (wird erst beim Speichern angewendet)
|
|
const pendingSpielerNamesByTeamIndex = ref({}) // { [index: number]: string[] }
|
|
|
|
function nowIsoDate() {
|
|
return new Date().toISOString().split('T')[0]
|
|
}
|
|
|
|
function newSpielerItem(name = '') {
|
|
return {
|
|
id: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
name
|
|
}
|
|
}
|
|
|
|
function parseSpielerString(spielerString) {
|
|
if (!spielerString) return []
|
|
return String(spielerString)
|
|
.split(';')
|
|
.map(s => s.trim())
|
|
.filter(Boolean)
|
|
.map(name => newSpielerItem(name))
|
|
}
|
|
|
|
function serializeSpielerList(spielerListe) {
|
|
return (spielerListe || [])
|
|
.map(s => (s?.name || '').trim())
|
|
.filter(Boolean)
|
|
.join('; ')
|
|
}
|
|
|
|
function serializeSpielerNames(spielerNames) {
|
|
return (spielerNames || [])
|
|
.map(s => String(s || '').trim())
|
|
.filter(Boolean)
|
|
.join('; ')
|
|
}
|
|
|
|
const mannschaftenSelectOptions = computed(() => {
|
|
const current = (formData.value.mannschaft || '').trim()
|
|
const names = mannschaften.value
|
|
.map(m => (m?.mannschaft || '').trim())
|
|
.filter(Boolean)
|
|
return [...new Set([current, ...names])].filter(Boolean)
|
|
})
|
|
|
|
function resetSpielerDraftState() {
|
|
moveTargetBySpielerId.value = {}
|
|
pendingSpielerNamesByTeamIndex.value = {}
|
|
}
|
|
|
|
function getPendingSpielerNamesForTeamIndex(teamIndex) {
|
|
if (pendingSpielerNamesByTeamIndex.value[teamIndex]) {
|
|
return pendingSpielerNamesByTeamIndex.value[teamIndex]
|
|
}
|
|
const existing = mannschaften.value[teamIndex]
|
|
const list = existing ? getSpielerListe(existing) : []
|
|
pendingSpielerNamesByTeamIndex.value[teamIndex] = [...list]
|
|
return pendingSpielerNamesByTeamIndex.value[teamIndex]
|
|
}
|
|
|
|
const loadMannschaften = async () => {
|
|
isLoading.value = true
|
|
try {
|
|
// Cache-Buster: Browser/CDN könnten CSV sonst aggressiv cachen
|
|
const url = `/data/mannschaften.csv?_t=${Date.now()}`
|
|
const response = await fetch(url, { cache: 'no-store' })
|
|
if (!response.ok) {
|
|
throw new Error('Fehler beim Laden der Mannschaften')
|
|
}
|
|
|
|
const csv = await response.text()
|
|
const lines = csv.split('\n').filter(line => line.trim() !== '')
|
|
|
|
if (lines.length < 2) {
|
|
mannschaften.value = []
|
|
return
|
|
}
|
|
|
|
mannschaften.value = lines.slice(1).map(line => {
|
|
// CSV-Parser: Respektiert Anführungszeichen
|
|
const values = []
|
|
let current = ''
|
|
let inQuotes = false
|
|
|
|
for (let i = 0; i < line.length; i++) {
|
|
const char = line[i]
|
|
|
|
if (char === '"') {
|
|
inQuotes = !inQuotes
|
|
} else if (char === ',' && !inQuotes) {
|
|
values.push(current.trim())
|
|
current = ''
|
|
} else {
|
|
current += char
|
|
}
|
|
}
|
|
values.push(current.trim())
|
|
|
|
if (values.length < 10) return null
|
|
|
|
return {
|
|
mannschaft: values[0]?.trim() || '',
|
|
liga: values[1]?.trim() || '',
|
|
staffelleiter: values[2]?.trim() || '',
|
|
telefon: values[3]?.trim() || '',
|
|
heimspieltag: values[4]?.trim() || '',
|
|
spielsystem: values[5]?.trim() || '',
|
|
mannschaftsfuehrer: values[6]?.trim() || '',
|
|
spieler: values[7]?.trim() || '',
|
|
weitere_informationen_link: values[8]?.trim() || '',
|
|
letzte_aktualisierung: values[9]?.trim() || ''
|
|
}
|
|
}).filter(mannschaft => mannschaft !== null && mannschaft.mannschaft !== '')
|
|
} catch (error) {
|
|
console.error('Fehler beim Laden der Mannschaften:', error)
|
|
errorMessage.value = 'Fehler beim Laden der Mannschaften'
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
const getSpielerListe = (mannschaft) => {
|
|
if (!mannschaft.spieler) return []
|
|
return mannschaft.spieler.split(';').map(s => s.trim()).filter(s => s !== '')
|
|
}
|
|
|
|
const openAddModal = () => {
|
|
formData.value = {
|
|
mannschaft: '',
|
|
liga: '',
|
|
staffelleiter: '',
|
|
telefon: '',
|
|
heimspieltag: '',
|
|
spielsystem: '',
|
|
mannschaftsfuehrer: '',
|
|
spielerListe: [],
|
|
weitere_informationen_link: '',
|
|
letzte_aktualisierung: nowIsoDate()
|
|
}
|
|
showModal.value = true
|
|
errorMessage.value = ''
|
|
isEditing.value = false
|
|
editingIndex.value = -1
|
|
resetSpielerDraftState()
|
|
}
|
|
|
|
const closeModal = () => {
|
|
showModal.value = false
|
|
errorMessage.value = ''
|
|
isEditing.value = false
|
|
editingIndex.value = -1
|
|
resetSpielerDraftState()
|
|
}
|
|
|
|
const openEditModal = (mannschaft, index) => {
|
|
formData.value = {
|
|
mannschaft: mannschaft.mannschaft || '',
|
|
liga: mannschaft.liga || '',
|
|
staffelleiter: mannschaft.staffelleiter || '',
|
|
telefon: mannschaft.telefon || '',
|
|
heimspieltag: mannschaft.heimspieltag || '',
|
|
spielsystem: mannschaft.spielsystem || '',
|
|
mannschaftsfuehrer: mannschaft.mannschaftsfuehrer || '',
|
|
spielerListe: parseSpielerString(mannschaft.spieler || ''),
|
|
weitere_informationen_link: mannschaft.weitere_informationen_link || '',
|
|
letzte_aktualisierung: mannschaft.letzte_aktualisierung || nowIsoDate()
|
|
}
|
|
isEditing.value = true
|
|
editingIndex.value = index
|
|
showModal.value = true
|
|
errorMessage.value = ''
|
|
resetSpielerDraftState()
|
|
// Pro Spieler: aktuelle Mannschaft vorauswählen
|
|
const currentTeam = (formData.value.mannschaft || '').trim()
|
|
for (const s of formData.value.spielerListe) {
|
|
moveTargetBySpielerId.value[s.id] = currentTeam
|
|
}
|
|
}
|
|
|
|
const addSpieler = () => {
|
|
const item = newSpielerItem('')
|
|
formData.value.spielerListe.push(item)
|
|
moveTargetBySpielerId.value[item.id] = (formData.value.mannschaft || '').trim()
|
|
}
|
|
|
|
const removeSpieler = (spielerId) => {
|
|
const idx = formData.value.spielerListe.findIndex(s => s.id === spielerId)
|
|
if (idx === -1) return
|
|
formData.value.spielerListe.splice(idx, 1)
|
|
if (moveTargetBySpielerId.value[spielerId]) {
|
|
delete moveTargetBySpielerId.value[spielerId]
|
|
}
|
|
}
|
|
|
|
const moveSpielerUp = (index) => {
|
|
if (index <= 0) return
|
|
const arr = formData.value.spielerListe
|
|
const item = arr[index]
|
|
arr.splice(index, 1)
|
|
arr.splice(index - 1, 0, item)
|
|
}
|
|
|
|
const moveSpielerDown = (index) => {
|
|
const arr = formData.value.spielerListe
|
|
if (index < 0 || index >= arr.length - 1) return
|
|
const item = arr[index]
|
|
arr.splice(index, 1)
|
|
arr.splice(index + 1, 0, item)
|
|
}
|
|
|
|
const canMoveSpieler = (spielerId) => {
|
|
const targetName = (moveTargetBySpielerId.value[spielerId] || '').trim()
|
|
const currentTeam = (formData.value.mannschaft || '').trim()
|
|
return Boolean(targetName) && Boolean(currentTeam) && targetName !== currentTeam
|
|
}
|
|
|
|
const moveSpielerToMannschaft = (spielerId) => {
|
|
if (!isEditing.value || editingIndex.value < 0) return
|
|
|
|
const targetName = (moveTargetBySpielerId.value[spielerId] || '').trim()
|
|
if (!targetName) return
|
|
|
|
const targetIndex = mannschaften.value.findIndex((m, idx) => {
|
|
if (idx === editingIndex.value) return false
|
|
return (m?.mannschaft || '').trim() === targetName
|
|
})
|
|
|
|
if (targetIndex === -1) {
|
|
errorMessage.value = 'Ziel-Mannschaft nicht gefunden. Bitte aus der Liste auswählen.'
|
|
return
|
|
}
|
|
|
|
const idx = formData.value.spielerListe.findIndex(s => s.id === spielerId)
|
|
if (idx === -1) return
|
|
|
|
const spielerName = (formData.value.spielerListe[idx]?.name || '').trim()
|
|
if (!spielerName) {
|
|
errorMessage.value = 'Bitte zuerst einen Spielernamen eintragen.'
|
|
return
|
|
}
|
|
|
|
// Entfernen aus aktueller Mannschaft
|
|
formData.value.spielerListe.splice(idx, 1)
|
|
|
|
// Hinzufügen zur Ziel-Mannschaft (pending; wird erst beim Speichern geschrieben)
|
|
const pendingList = getPendingSpielerNamesForTeamIndex(targetIndex)
|
|
pendingList.push(spielerName)
|
|
|
|
// UI zurücksetzen
|
|
delete moveTargetBySpielerId.value[spielerId]
|
|
}
|
|
|
|
const saveMannschaft = async () => {
|
|
isSaving.value = true
|
|
errorMessage.value = ''
|
|
|
|
try {
|
|
const spielerString = serializeSpielerList(formData.value.spielerListe)
|
|
const updated = {
|
|
mannschaft: formData.value.mannschaft || '',
|
|
liga: formData.value.liga || '',
|
|
staffelleiter: formData.value.staffelleiter || '',
|
|
telefon: formData.value.telefon || '',
|
|
heimspieltag: formData.value.heimspieltag || '',
|
|
spielsystem: formData.value.spielsystem || '',
|
|
mannschaftsfuehrer: formData.value.mannschaftsfuehrer || '',
|
|
spieler: spielerString,
|
|
weitere_informationen_link: formData.value.weitere_informationen_link || '',
|
|
letzte_aktualisierung: formData.value.letzte_aktualisierung || nowIsoDate()
|
|
}
|
|
|
|
if (isEditing.value && editingIndex.value >= 0) {
|
|
// Aktualisiere bestehende Mannschaft
|
|
mannschaften.value[editingIndex.value] = { ...updated }
|
|
} else {
|
|
// Füge neue Mannschaft hinzu
|
|
mannschaften.value.push({ ...updated })
|
|
}
|
|
|
|
// Pending-Verschiebungen anwenden (andere Mannschaften)
|
|
const touchedTeamIndexes = Object.keys(pendingSpielerNamesByTeamIndex.value)
|
|
if (touchedTeamIndexes.length > 0) {
|
|
const ts = nowIsoDate()
|
|
for (const idxStr of touchedTeamIndexes) {
|
|
const idx = Number(idxStr)
|
|
if (!Number.isFinite(idx)) continue
|
|
const existing = mannschaften.value[idx]
|
|
if (!existing) continue
|
|
const pendingNames = pendingSpielerNamesByTeamIndex.value[idx]
|
|
mannschaften.value[idx] = {
|
|
...existing,
|
|
spieler: serializeSpielerNames(pendingNames),
|
|
letzte_aktualisierung: ts
|
|
}
|
|
}
|
|
}
|
|
|
|
// Speichere als CSV
|
|
await saveCSV()
|
|
|
|
closeModal()
|
|
await loadMannschaften()
|
|
|
|
if (window.showSuccessModal) {
|
|
window.showSuccessModal('Erfolg', 'Mannschaft wurde erfolgreich gespeichert')
|
|
}
|
|
} catch (error) {
|
|
console.error('Fehler beim Speichern:', error)
|
|
errorMessage.value = error?.data?.statusMessage || error?.statusMessage || error?.data?.message || 'Fehler beim Speichern der Mannschaft.'
|
|
if (window.showErrorModal) {
|
|
window.showErrorModal('Fehler', errorMessage.value)
|
|
}
|
|
} finally {
|
|
isSaving.value = false
|
|
}
|
|
}
|
|
|
|
const saveCSV = async () => {
|
|
// CSV-Header
|
|
const header = 'Mannschaft,Liga,Staffelleiter,Telefon,Heimspieltag,Spielsystem,Mannschaftsführer,Spieler,Weitere Informationen Link,Letzte Aktualisierung'
|
|
|
|
// CSV-Zeilen generieren
|
|
const rows = mannschaften.value.map(m => {
|
|
// Escape-Werte, die Kommas oder Anführungszeichen enthalten
|
|
const escapeCSV = (value) => {
|
|
if (!value) return ''
|
|
const str = String(value)
|
|
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
|
|
return `"${str.replace(/"/g, '""')}"`
|
|
}
|
|
return str
|
|
}
|
|
|
|
return [
|
|
escapeCSV(m.mannschaft),
|
|
escapeCSV(m.liga),
|
|
escapeCSV(m.staffelleiter),
|
|
escapeCSV(m.telefon),
|
|
escapeCSV(m.heimspieltag),
|
|
escapeCSV(m.spielsystem),
|
|
escapeCSV(m.mannschaftsfuehrer),
|
|
escapeCSV(m.spieler),
|
|
escapeCSV(m.weitere_informationen_link),
|
|
escapeCSV(m.letzte_aktualisierung)
|
|
].join(',')
|
|
})
|
|
|
|
const csvContent = [header, ...rows].join('\n')
|
|
|
|
// Speichere über API
|
|
await $fetch('/api/cms/save-csv', {
|
|
method: 'POST',
|
|
body: {
|
|
filename: 'mannschaften.csv',
|
|
content: csvContent
|
|
}
|
|
})
|
|
}
|
|
|
|
const confirmDelete = (mannschaft, index) => {
|
|
if (window.showConfirmModal) {
|
|
window.showConfirmModal('Mannschaft löschen', `Möchten Sie die Mannschaft "${mannschaft.mannschaft}" wirklich löschen?`, async () => {
|
|
try {
|
|
mannschaften.value.splice(index, 1)
|
|
await saveCSV()
|
|
await loadMannschaften()
|
|
window.showSuccessModal('Erfolg', 'Mannschaft wurde erfolgreich gelöscht')
|
|
} catch (error) {
|
|
console.error('Fehler beim Löschen:', error)
|
|
window.showErrorModal('Fehler', 'Fehler beim Löschen der Mannschaft')
|
|
}
|
|
})
|
|
} else {
|
|
// Fallback ohne Modal
|
|
if (confirm(`Möchten Sie die Mannschaft "${mannschaft.mannschaft}" wirklich löschen?`)) {
|
|
mannschaften.value.splice(index, 1)
|
|
saveCSV()
|
|
loadMannschaften()
|
|
}
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
loadMannschaften()
|
|
})
|
|
|
|
definePageMeta({
|
|
middleware: 'auth',
|
|
layout: 'default'
|
|
})
|
|
|
|
useHead({
|
|
title: 'Mannschaften verwalten - Harheimer TC',
|
|
})
|
|
</script>
|