Files
harheimertc/pages/cms/mannschaften.vue
Torsten Schulz (local) a0dd4f6134
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 53s
Update package dependencies and enhance CSV handling in CMS
This commit updates the version of several packages in package.json and package-lock.json, including downgrading "quill" to 2.0.2 and upgrading "devalue", "diff", "h3", "node-mock-http", "tar", and "undici" to their latest versions. Additionally, it improves the CSV file handling in the CMS by implementing a cache-busting mechanism for fetching data and enhancing error handling during file saving, ensuring more robust data management.
2026-01-18 22:25:04 +01:00

558 lines
18 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 (durch Semikolon getrennt)
</label>
<textarea
v-model="formData.spieler"
rows="4"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
placeholder="Spieler 1; Spieler 2; Spieler 3"
:disabled="isSaving"
/>
<p class="mt-1 text-xs text-gray-500">
Mehrere Spieler durch Semikolon (;) trennen
</p>
</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 } from 'vue'
import { Plus, Trash2, Loader2, AlertCircle, Pencil, Users } 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: '',
spieler: '',
weitere_informationen_link: '',
letzte_aktualisierung: ''
})
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: '',
spieler: '',
weitere_informationen_link: '',
letzte_aktualisierung: new Date().toISOString().split('T')[0]
}
showModal.value = true
errorMessage.value = ''
isEditing.value = false
editingIndex.value = -1
}
const closeModal = () => {
showModal.value = false
errorMessage.value = ''
isEditing.value = false
editingIndex.value = -1
}
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 || '',
spieler: mannschaft.spieler || '',
weitere_informationen_link: mannschaft.weitere_informationen_link || '',
letzte_aktualisierung: mannschaft.letzte_aktualisierung || new Date().toISOString().split('T')[0]
}
isEditing.value = true
editingIndex.value = index
showModal.value = true
errorMessage.value = ''
}
const saveMannschaft = async () => {
isSaving.value = true
errorMessage.value = ''
try {
if (isEditing.value && editingIndex.value >= 0) {
// Aktualisiere bestehende Mannschaft
mannschaften.value[editingIndex.value] = { ...formData.value }
} else {
// Füge neue Mannschaft hinzu
mannschaften.value.push({ ...formData.value })
}
// 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>