418 lines
18 KiB
Vue
418 lines
18 KiB
Vue
<template>
|
|
<div>
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h2 class="text-xl sm:text-2xl font-display font-bold text-gray-900">
|
|
Spielpläne bearbeiten
|
|
</h2>
|
|
<div class="space-x-3">
|
|
<button
|
|
class="inline-flex items-center px-3 py-1.5 sm:px-4 sm:py-2 rounded-lg bg-green-600 text-white hover:bg-green-700 text-sm sm:text-base"
|
|
@click="showUploadModal = true"
|
|
>
|
|
<svg
|
|
class="w-4 h-4 mr-2"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
><path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
|
|
/></svg>
|
|
CSV hochladen
|
|
</button>
|
|
<button
|
|
class="inline-flex items-center px-3 py-1.5 sm:px-4 sm:py-2 rounded-lg bg-primary-600 text-white hover:bg-primary-700 text-sm sm:text-base"
|
|
@click="save"
|
|
>
|
|
Speichern
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- CSV Upload Section -->
|
|
<div class="mb-8 bg-white rounded-xl shadow-lg p-6">
|
|
<h3 class="text-xl font-semibold text-gray-900 mb-4">
|
|
Vereins-Spielplan (CSV)
|
|
</h3>
|
|
<div
|
|
v-if="currentFile"
|
|
class="mb-4 p-4 bg-green-50 border border-green-200 rounded-lg"
|
|
>
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center">
|
|
<svg
|
|
class="w-5 h-5 text-green-600 mr-2"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
><path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
/></svg>
|
|
<div>
|
|
<p class="text-sm font-medium text-green-800">
|
|
{{ currentFile.name }}
|
|
</p><p class="text-xs text-green-600">
|
|
{{ currentFile.size }} bytes
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
class="px-3 py-1 text-sm bg-red-100 hover:bg-red-200 text-red-700 rounded-lg transition-colors"
|
|
@click="removeFile"
|
|
>
|
|
Entfernen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center hover:border-primary-400 hover:bg-primary-50 transition-colors cursor-pointer"
|
|
:class="{ 'border-primary-400 bg-primary-50': isDragOver }"
|
|
@click="triggerFileInput"
|
|
@dragover.prevent
|
|
@dragenter.prevent
|
|
@drop.prevent="handleFileDrop"
|
|
>
|
|
<svg
|
|
class="w-12 h-12 text-gray-400 mx-auto mb-4"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
><path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
|
|
/></svg>
|
|
<p class="text-lg font-medium text-gray-900 mb-2">
|
|
CSV-Datei hochladen
|
|
</p>
|
|
<p class="text-sm text-gray-600 mb-4">
|
|
Klicken Sie hier oder ziehen Sie eine CSV-Datei hierher
|
|
</p>
|
|
</div>
|
|
<input
|
|
ref="fileInput"
|
|
type="file"
|
|
accept=".csv"
|
|
class="hidden"
|
|
@change="handleFileSelect"
|
|
>
|
|
</div>
|
|
|
|
<!-- Column Selection -->
|
|
<div
|
|
v-if="csvData.length > 0 && !columnsSelected"
|
|
class="bg-white rounded-xl shadow-lg p-6 mb-8"
|
|
>
|
|
<h3 class="text-xl font-semibold text-gray-900 mb-4">
|
|
Spalten auswählen
|
|
</h3>
|
|
<p class="text-sm text-gray-600 mb-6">
|
|
Wählen Sie die Spalten aus, die für den Spielplan gespeichert werden sollen:
|
|
</p>
|
|
<div class="space-y-4">
|
|
<div
|
|
v-for="(header, index) in csvHeaders"
|
|
:key="index"
|
|
class="flex items-center justify-between p-4 border border-gray-200 rounded-lg hover:bg-gray-50"
|
|
>
|
|
<div class="flex items-center">
|
|
<input
|
|
:id="`column-${index}`"
|
|
v-model="selectedColumns[index]"
|
|
type="checkbox"
|
|
class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
|
|
>
|
|
<label
|
|
:for="`column-${index}`"
|
|
class="ml-3 text-sm font-medium text-gray-900"
|
|
>{{ header }}</label>
|
|
</div>
|
|
<div class="text-xs text-gray-500">
|
|
{{ getColumnPreview(index) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="mt-6 flex justify-between items-center">
|
|
<div class="text-sm text-gray-600">
|
|
{{ selectedColumnsCount }} von {{ csvHeaders.length }} Spalten ausgewählt
|
|
</div>
|
|
<div class="space-x-3">
|
|
<button
|
|
class="px-4 py-2 text-sm bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors"
|
|
@click="selectAllColumns"
|
|
>
|
|
Alle auswählen
|
|
</button>
|
|
<button
|
|
class="px-4 py-2 text-sm bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors"
|
|
@click="deselectAllColumns"
|
|
>
|
|
Alle abwählen
|
|
</button>
|
|
<button
|
|
class="px-4 py-2 text-sm bg-blue-100 hover:bg-blue-200 text-blue-700 rounded-lg transition-colors"
|
|
@click="suggestHalleColumns"
|
|
>
|
|
Halle-Spalten vorschlagen
|
|
</button>
|
|
<button
|
|
:disabled="selectedColumnsCount === 0"
|
|
class="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors disabled:bg-gray-400"
|
|
@click="confirmColumnSelection"
|
|
>
|
|
Auswahl bestätigen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Data Preview -->
|
|
<div
|
|
v-if="csvData.length > 0 && columnsSelected"
|
|
class="bg-white rounded-xl shadow-lg p-6"
|
|
>
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h3 class="text-xl font-semibold text-gray-900">
|
|
Datenvorschau
|
|
</h3>
|
|
<div class="flex space-x-2">
|
|
<button
|
|
class="px-3 py-1 text-sm bg-blue-100 hover:bg-blue-200 text-blue-700 rounded-lg transition-colors"
|
|
@click="exportCSV"
|
|
>
|
|
CSV exportieren
|
|
</button>
|
|
<button
|
|
class="px-3 py-1 text-sm bg-red-100 hover:bg-red-200 text-red-700 rounded-lg transition-colors"
|
|
@click="clearData"
|
|
>
|
|
Daten löschen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full divide-y divide-gray-200">
|
|
<thead class="bg-gray-50">
|
|
<tr>
|
|
<th
|
|
v-for="(header, index) in (columnsSelected ? filteredCsvHeaders : csvHeaders)"
|
|
:key="index"
|
|
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
|
>
|
|
{{ header }}
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white divide-y divide-gray-200">
|
|
<tr
|
|
v-for="(row, rowIndex) in (columnsSelected ? filteredCsvData : csvData).slice(0, 10)"
|
|
:key="rowIndex"
|
|
:class="rowIndex % 2 === 0 ? 'bg-white' : 'bg-gray-50'"
|
|
>
|
|
<td
|
|
v-for="(cell, cellIndex) in row"
|
|
:key="cellIndex"
|
|
class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"
|
|
>
|
|
{{ cell }}
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div
|
|
v-if="(columnsSelected ? filteredCsvData : csvData).length > 10"
|
|
class="mt-4 text-center text-sm text-gray-600"
|
|
>
|
|
Zeige erste 10 von {{ (columnsSelected ? filteredCsvData : csvData).length }} Zeilen
|
|
</div>
|
|
<div class="mt-4 text-sm text-gray-600">
|
|
<p><strong>Zeilen:</strong> {{ (columnsSelected ? filteredCsvData : csvData).length }}</p><p><strong>Spalten:</strong> {{ (columnsSelected ? filteredCsvHeaders : csvHeaders).length }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Empty State -->
|
|
<div
|
|
v-if="csvData.length === 0"
|
|
class="text-center py-12 bg-white rounded-xl shadow-lg"
|
|
>
|
|
<svg
|
|
class="w-12 h-12 text-gray-400 mx-auto mb-4"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
><path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
|
/></svg>
|
|
<p class="text-gray-600">
|
|
Keine CSV-Daten geladen.
|
|
</p>
|
|
<p class="text-sm text-gray-500 mt-2">
|
|
Laden Sie eine CSV-Datei hoch, um Spielplandaten zu verwalten.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Upload Modal -->
|
|
<div
|
|
v-if="showUploadModal"
|
|
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
|
|
@click.self="closeUploadModal"
|
|
>
|
|
<div class="bg-white rounded-lg max-w-md w-full p-6">
|
|
<h3 class="text-lg font-semibold text-gray-900 mb-4">
|
|
CSV-Datei hochladen
|
|
</h3>
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">Datei auswählen</label><input
|
|
ref="modalFileInput"
|
|
type="file"
|
|
accept=".csv"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
|
@change="handleModalFileSelect"
|
|
>
|
|
</div>
|
|
<div
|
|
v-if="selectedFile"
|
|
class="p-3 bg-gray-50 rounded-lg"
|
|
>
|
|
<p class="text-sm text-gray-700">
|
|
<strong>Ausgewählte Datei:</strong> {{ selectedFile.name }}
|
|
</p><p class="text-xs text-gray-500">
|
|
{{ selectedFile.size }} bytes
|
|
</p>
|
|
</div>
|
|
<div class="bg-blue-50 p-4 rounded-lg">
|
|
<h4 class="text-sm font-medium text-blue-800 mb-2">
|
|
Erwartetes CSV-Format:
|
|
</h4><div class="text-xs text-blue-700 space-y-1">
|
|
<p>• Erste Zeile: Spaltenüberschriften</p><p>• Trennzeichen: Komma (,)</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex justify-end space-x-3 pt-4">
|
|
<button
|
|
class="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
|
|
@click="closeUploadModal"
|
|
>
|
|
Abbrechen
|
|
</button>
|
|
<button
|
|
:disabled="!selectedFile"
|
|
class="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors disabled:bg-gray-400"
|
|
@click="processSelectedFile"
|
|
>
|
|
Hochladen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Processing Modal -->
|
|
<div
|
|
v-if="isProcessing"
|
|
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
|
|
>
|
|
<div class="bg-white rounded-lg max-w-sm w-full p-6 text-center">
|
|
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600 mx-auto mb-4" />
|
|
<h3 class="text-lg font-semibold text-gray-900 mb-2">
|
|
Verarbeitung läuft...
|
|
</h3>
|
|
<p class="text-sm text-gray-600">
|
|
{{ processingMessage }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, computed } from 'vue'
|
|
|
|
const fileInput = ref(null)
|
|
const modalFileInput = ref(null)
|
|
const showUploadModal = ref(false)
|
|
const isProcessing = ref(false)
|
|
const processingMessage = ref('')
|
|
const isDragOver = ref(false)
|
|
const currentFile = ref(null)
|
|
const selectedFile = ref(null)
|
|
const csvData = ref([])
|
|
const csvHeaders = ref([])
|
|
const selectedColumns = ref([])
|
|
const columnsSelected = ref(false)
|
|
const filteredCsvData = ref([])
|
|
const filteredCsvHeaders = ref([])
|
|
|
|
const triggerFileInput = () => { fileInput.value?.click() }
|
|
const handleFileSelect = (event) => { const file = event.target.files[0]; if (file) processFile(file) }
|
|
const handleModalFileSelect = (event) => { selectedFile.value = event.target.files[0] }
|
|
const handleFileDrop = (event) => { isDragOver.value = false; const file = event.dataTransfer.files[0]; if (file && file.type === 'text/csv') processFile(file) }
|
|
|
|
const processFile = async (file) => {
|
|
isProcessing.value = true; processingMessage.value = 'Datei wird gelesen...'
|
|
try {
|
|
const text = await file.text(); processingMessage.value = 'CSV wird geparst...'
|
|
const lines = text.split('\n').filter(line => line.trim() !== '')
|
|
if (lines.length < 2) throw new Error('CSV-Datei muss mindestens eine Kopfzeile und eine Datenzeile enthalten')
|
|
const parseCSVLine = (line) => { const tabCount = (line.match(/\t/g) || []).length; const semicolonCount = (line.match(/;/g) || []).length; const delimiter = tabCount > semicolonCount ? '\t' : ';'; return line.split(delimiter).map(value => value.trim()) }
|
|
csvHeaders.value = parseCSVLine(lines[0]); csvData.value = lines.slice(1).map(line => parseCSVLine(line))
|
|
selectedColumns.value = new Array(csvHeaders.value.length).fill(true); columnsSelected.value = false
|
|
currentFile.value = { name: file.name, size: file.size, lastModified: file.lastModified }
|
|
processingMessage.value = 'Verarbeitung abgeschlossen!'
|
|
setTimeout(() => { isProcessing.value = false; showUploadModal.value = false }, 1000)
|
|
} catch (error) { console.error('Fehler:', error); alert('Fehler: ' + error.message); isProcessing.value = false }
|
|
}
|
|
|
|
const processSelectedFile = () => { if (selectedFile.value) processFile(selectedFile.value) }
|
|
const removeFile = () => { currentFile.value = null; csvData.value = []; csvHeaders.value = []; selectedColumns.value = []; columnsSelected.value = false; filteredCsvData.value = []; filteredCsvHeaders.value = []; if (fileInput.value) fileInput.value.value = '' }
|
|
const selectedColumnsCount = computed(() => selectedColumns.value.filter(s => s).length)
|
|
const getColumnPreview = (index) => { if (csvData.value.length === 0) return 'Keine Daten'; const sv = csvData.value.slice(0, 3).map(row => row[index]).filter(val => val && val.trim() !== ''); return sv.length > 0 ? `Beispiel: ${sv.join(', ')}` : 'Leere Spalte' }
|
|
const selectAllColumns = () => { selectedColumns.value = selectedColumns.value.map(() => true) }
|
|
const deselectAllColumns = () => { selectedColumns.value = selectedColumns.value.map(() => false) }
|
|
const confirmColumnSelection = () => { const si = selectedColumns.value.map((s, i) => s ? i : -1).filter(i => i !== -1); filteredCsvHeaders.value = si.map(i => csvHeaders.value[i]); filteredCsvData.value = csvData.value.map(row => si.map(i => row[i])); columnsSelected.value = true }
|
|
const suggestHalleColumns = () => { csvHeaders.value.forEach((header, index) => { const h = header.toLowerCase(); if (h.includes('halle') || h.includes('strasse') || h.includes('plz') || h.includes('ort')) selectedColumns.value[index] = true }) }
|
|
const clearData = () => { if (confirm('Möchten Sie alle Daten wirklich löschen?')) removeFile() }
|
|
|
|
const exportCSV = () => {
|
|
const d = columnsSelected.value ? filteredCsvData.value : csvData.value; const h = columnsSelected.value ? filteredCsvHeaders.value : csvHeaders.value
|
|
if (d.length === 0) return
|
|
const csv = [h.join(';'), ...d.map(row => row.join(';'))].join('\n')
|
|
const blob = new Blob([csv], { type: 'text/csv' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `spielplan_export_${new Date().toISOString().split('T')[0]}.csv`; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a)
|
|
}
|
|
|
|
const save = async () => {
|
|
const d = columnsSelected.value ? filteredCsvData.value : csvData.value; const h = columnsSelected.value ? filteredCsvHeaders.value : csvHeaders.value
|
|
if (d.length === 0) { alert('Keine Daten zum Speichern vorhanden.'); return }
|
|
try {
|
|
const csv = [h.join(';'), ...d.map(row => row.join(';'))].join('\n')
|
|
const response = await fetch('/api/cms/save-csv', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ filename: 'spielplan.csv', content: csv }) })
|
|
if (response.ok) alert('Spielplan erfolgreich gespeichert!'); else alert('Fehler beim Speichern!')
|
|
} catch (error) { console.error('Fehler:', error); alert('Fehler beim Speichern!') }
|
|
}
|
|
|
|
const closeUploadModal = () => { showUploadModal.value = false; selectedFile.value = null; if (modalFileInput.value) modalFileInput.value.value = '' }
|
|
|
|
onMounted(() => {
|
|
(async () => {
|
|
try {
|
|
const response = await fetch('/data/spielplan.csv'); if (!response.ok) return; const text = await response.text()
|
|
const lines = text.split('\n').filter(line => line.trim() !== ''); if (lines.length < 2) return
|
|
const parseCSVLine = (line) => { 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()); return values }
|
|
csvHeaders.value = parseCSVLine(lines[0]); csvData.value = lines.slice(1).map(line => parseCSVLine(line))
|
|
currentFile.value = { name: 'spielplan.csv', size: text.length, lastModified: null }
|
|
} catch { /* ignore */ }
|
|
})()
|
|
})
|
|
</script>
|