Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 49s
This commit replaces the text area for entering players with a dynamic list interface, allowing users to add, remove, and reorder players more intuitively. It introduces functionality for moving players between teams and enhances the overall user experience by providing clear feedback and controls. The data structure for players is updated to support these changes, ensuring better data handling during team management.
818 lines
27 KiB
Vue
818 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="p-3 border border-gray-200 rounded-lg bg-white"
|
|
>
|
|
<div class="flex flex-col sm:flex-row sm:items-center gap-2">
|
|
<input
|
|
v-model="spieler.name"
|
|
type="text"
|
|
class="flex-1 px-4 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>
|
|
</div>
|
|
|
|
<!-- Verschieben -->
|
|
<div class="mt-2 flex flex-col sm:flex-row sm:items-center gap-2">
|
|
<input
|
|
v-model="moveTargetBySpielerId[spieler.id]"
|
|
list="mannschaften-targets"
|
|
type="text"
|
|
class="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
|
|
placeholder="In andere Mannschaft verschieben…"
|
|
:disabled="isSaving || !isEditing || otherMannschaften.length === 0"
|
|
>
|
|
<button
|
|
type="button"
|
|
class="inline-flex items-center justify-center px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
:disabled="isSaving || !isEditing || otherMannschaften.length === 0 || !canMoveSpieler(spieler.id)"
|
|
@click="moveSpielerToMannschaft(spieler.id)"
|
|
>
|
|
<ArrowRight
|
|
:size="18"
|
|
class="mr-2"
|
|
/>
|
|
Verschieben
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<datalist id="mannschaften-targets">
|
|
<option
|
|
v-for="t in otherMannschaften"
|
|
:key="t.idx"
|
|
:value="t.name"
|
|
/>
|
|
</datalist>
|
|
|
|
<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 otherMannschaften = computed(() => {
|
|
return mannschaften.value
|
|
.map((m, idx) => ({ idx, name: (m?.mannschaft || '').trim() }))
|
|
.filter(t => t.idx !== editingIndex.value && t.name)
|
|
})
|
|
|
|
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()
|
|
}
|
|
|
|
const addSpieler = () => {
|
|
formData.value.spielerListe.push(newSpielerItem(''))
|
|
}
|
|
|
|
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()
|
|
return Boolean(targetName)
|
|
}
|
|
|
|
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>
|