Refactor Spieler management in Mannschaften component for improved usability
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.
This commit is contained in:
Torsten Schulz (local)
2026-01-18 23:10:44 +01:00
parent a0dd4f6134
commit 7ada3b62c4

View File

@@ -242,19 +242,114 @@
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-2"> <label class="block text-sm font-medium text-gray-700 mb-2">
Spieler (durch Semikolon getrennt) Spieler
</label> </label>
<textarea <div class="space-y-2">
v-model="formData.spieler" <div
rows="4" v-if="formData.spielerListe.length === 0"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500" class="text-sm text-gray-500"
placeholder="Spieler 1; Spieler 2; Spieler 3" >
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" :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"
/> />
<p class="mt-1 text-xs text-gray-500"> Verschieben
Mehrere Spieler durch Semikolon (;) trennen </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> </p>
</div> </div>
</div>
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-2"> <label class="block text-sm font-medium text-gray-700 mb-2">
@@ -309,8 +404,8 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted, computed } from 'vue'
import { Plus, Trash2, Loader2, AlertCircle, Pencil, Users } from 'lucide-vue-next' import { Plus, Trash2, Loader2, AlertCircle, Pencil, Users, ChevronUp, ChevronDown, ArrowRight } from 'lucide-vue-next'
const isLoading = ref(true) const isLoading = ref(true)
const isSaving = ref(false) const isSaving = ref(false)
@@ -328,11 +423,71 @@ const formData = ref({
heimspieltag: '', heimspieltag: '',
spielsystem: '', spielsystem: '',
mannschaftsfuehrer: '', mannschaftsfuehrer: '',
spieler: '', spielerListe: [],
weitere_informationen_link: '', weitere_informationen_link: '',
letzte_aktualisierung: '' 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 () => { const loadMannschaften = async () => {
isLoading.value = true isLoading.value = true
try { try {
@@ -408,14 +563,15 @@ const openAddModal = () => {
heimspieltag: '', heimspieltag: '',
spielsystem: '', spielsystem: '',
mannschaftsfuehrer: '', mannschaftsfuehrer: '',
spieler: '', spielerListe: [],
weitere_informationen_link: '', weitere_informationen_link: '',
letzte_aktualisierung: new Date().toISOString().split('T')[0] letzte_aktualisierung: nowIsoDate()
} }
showModal.value = true showModal.value = true
errorMessage.value = '' errorMessage.value = ''
isEditing.value = false isEditing.value = false
editingIndex.value = -1 editingIndex.value = -1
resetSpielerDraftState()
} }
const closeModal = () => { const closeModal = () => {
@@ -423,6 +579,7 @@ const closeModal = () => {
errorMessage.value = '' errorMessage.value = ''
isEditing.value = false isEditing.value = false
editingIndex.value = -1 editingIndex.value = -1
resetSpielerDraftState()
} }
const openEditModal = (mannschaft, index) => { const openEditModal = (mannschaft, index) => {
@@ -434,14 +591,85 @@ const openEditModal = (mannschaft, index) => {
heimspieltag: mannschaft.heimspieltag || '', heimspieltag: mannschaft.heimspieltag || '',
spielsystem: mannschaft.spielsystem || '', spielsystem: mannschaft.spielsystem || '',
mannschaftsfuehrer: mannschaft.mannschaftsfuehrer || '', mannschaftsfuehrer: mannschaft.mannschaftsfuehrer || '',
spieler: mannschaft.spieler || '', spielerListe: parseSpielerString(mannschaft.spieler || ''),
weitere_informationen_link: mannschaft.weitere_informationen_link || '', weitere_informationen_link: mannschaft.weitere_informationen_link || '',
letzte_aktualisierung: mannschaft.letzte_aktualisierung || new Date().toISOString().split('T')[0] letzte_aktualisierung: mannschaft.letzte_aktualisierung || nowIsoDate()
} }
isEditing.value = true isEditing.value = true
editingIndex.value = index editingIndex.value = index
showModal.value = true showModal.value = true
errorMessage.value = '' 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 () => { const saveMannschaft = async () => {
@@ -449,12 +677,44 @@ const saveMannschaft = async () => {
errorMessage.value = '' errorMessage.value = ''
try { 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) { if (isEditing.value && editingIndex.value >= 0) {
// Aktualisiere bestehende Mannschaft // Aktualisiere bestehende Mannschaft
mannschaften.value[editingIndex.value] = { ...formData.value } mannschaften.value[editingIndex.value] = { ...updated }
} else { } else {
// Füge neue Mannschaft hinzu // Füge neue Mannschaft hinzu
mannschaften.value.push({ ...formData.value }) 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 // Speichere als CSV