Refactor Spieler management in Mannschaften component for improved usability
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 49s
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:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user