Refactor PDF upload and CSV parsing logic in 'spielplaene' and 'mannschaften' components; implement automatic delimiter detection for CSV files and enhance hall information extraction for improved data handling. Update UI to remove PDF upload section and streamline CSV upload process.
This commit is contained in:
@@ -22,71 +22,6 @@
|
|||||||
<div class="pt-20 pb-16">
|
<div class="pt-20 pb-16">
|
||||||
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
|
||||||
<!-- PDF Upload Section -->
|
|
||||||
<div class="mb-8 bg-white rounded-xl shadow-lg p-6">
|
|
||||||
<h2 class="text-xl font-semibold text-gray-900 mb-4">Spielplan PDF-Dateien</h2>
|
|
||||||
<p class="text-sm text-gray-600 mb-6">Laden Sie die PDF-Dateien für die verschiedenen Spielpläne hoch:</p>
|
|
||||||
|
|
||||||
<div class="grid gap-6 md:grid-cols-3">
|
|
||||||
<!-- Gesamt PDF -->
|
|
||||||
<div class="border border-gray-200 rounded-lg p-4">
|
|
||||||
<h3 class="font-medium text-gray-900 mb-2">Gesamt</h3>
|
|
||||||
<p class="text-sm text-gray-600 mb-3">Alle Mannschaften</p>
|
|
||||||
<input
|
|
||||||
ref="gesamtPdfInput"
|
|
||||||
type="file"
|
|
||||||
accept=".pdf"
|
|
||||||
@change="handlePdfUpload('gesamt', $event)"
|
|
||||||
class="hidden"
|
|
||||||
/>
|
|
||||||
<button @click="triggerPdfInput('gesamt')" class="w-full px-3 py-2 text-sm bg-blue-100 hover:bg-blue-200 text-blue-700 rounded-lg transition-colors">
|
|
||||||
PDF hochladen
|
|
||||||
</button>
|
|
||||||
<div v-if="uploadedPdfs.gesamt" class="mt-2 text-xs text-green-600">
|
|
||||||
✓ {{ uploadedPdfs.gesamt }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Erwachsene PDF -->
|
|
||||||
<div class="border border-gray-200 rounded-lg p-4">
|
|
||||||
<h3 class="font-medium text-gray-900 mb-2">Erwachsene</h3>
|
|
||||||
<p class="text-sm text-gray-600 mb-3">Erwachsenen-Mannschaften</p>
|
|
||||||
<input
|
|
||||||
ref="erwachsenePdfInput"
|
|
||||||
type="file"
|
|
||||||
accept=".pdf"
|
|
||||||
@change="handlePdfUpload('erwachsene', $event)"
|
|
||||||
class="hidden"
|
|
||||||
/>
|
|
||||||
<button @click="triggerPdfInput('erwachsene')" class="w-full px-3 py-2 text-sm bg-blue-100 hover:bg-blue-200 text-blue-700 rounded-lg transition-colors">
|
|
||||||
PDF hochladen
|
|
||||||
</button>
|
|
||||||
<div v-if="uploadedPdfs.erwachsene" class="mt-2 text-xs text-green-600">
|
|
||||||
✓ {{ uploadedPdfs.erwachsene }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Nachwuchs PDF -->
|
|
||||||
<div class="border border-gray-200 rounded-lg p-4">
|
|
||||||
<h3 class="font-medium text-gray-900 mb-2">Nachwuchs</h3>
|
|
||||||
<p class="text-sm text-gray-600 mb-3">Nachwuchs-Mannschaften</p>
|
|
||||||
<input
|
|
||||||
ref="nachwuchsPdfInput"
|
|
||||||
type="file"
|
|
||||||
accept=".pdf"
|
|
||||||
@change="handlePdfUpload('nachwuchs', $event)"
|
|
||||||
class="hidden"
|
|
||||||
/>
|
|
||||||
<button @click="triggerPdfInput('nachwuchs')" class="w-full px-3 py-2 text-sm bg-blue-100 hover:bg-blue-200 text-blue-700 rounded-lg transition-colors">
|
|
||||||
PDF hochladen
|
|
||||||
</button>
|
|
||||||
<div v-if="uploadedPdfs.nachwuchs" class="mt-2 text-xs text-green-600">
|
|
||||||
✓ {{ uploadedPdfs.nachwuchs }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- CSV Upload Section -->
|
<!-- CSV Upload Section -->
|
||||||
<div class="mb-8 bg-white rounded-xl shadow-lg p-6">
|
<div class="mb-8 bg-white rounded-xl shadow-lg p-6">
|
||||||
<h2 class="text-xl font-semibold text-gray-900 mb-4">Vereins-Spielplan (CSV)</h2>
|
<h2 class="text-xl font-semibold text-gray-900 mb-4">Vereins-Spielplan (CSV)</h2>
|
||||||
@@ -171,6 +106,9 @@
|
|||||||
<button @click="deselectAllColumns" class="px-4 py-2 text-sm bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors">
|
<button @click="deselectAllColumns" class="px-4 py-2 text-sm bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors">
|
||||||
Alle abwählen
|
Alle abwählen
|
||||||
</button>
|
</button>
|
||||||
|
<button @click="suggestHalleColumns" class="px-4 py-2 text-sm bg-blue-100 hover:bg-blue-200 text-blue-700 rounded-lg transition-colors">
|
||||||
|
Halle-Spalten vorschlagen
|
||||||
|
</button>
|
||||||
<button @click="confirmColumnSelection"
|
<button @click="confirmColumnSelection"
|
||||||
:disabled="selectedColumnsCount === 0"
|
: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">
|
class="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors disabled:bg-gray-400">
|
||||||
@@ -318,9 +256,6 @@ useHead({ title: 'CMS: Spielpläne' })
|
|||||||
|
|
||||||
const fileInput = ref(null)
|
const fileInput = ref(null)
|
||||||
const modalFileInput = ref(null)
|
const modalFileInput = ref(null)
|
||||||
const gesamtPdfInput = ref(null)
|
|
||||||
const erwachsenePdfInput = ref(null)
|
|
||||||
const nachwuchsPdfInput = ref(null)
|
|
||||||
const showUploadModal = ref(false)
|
const showUploadModal = ref(false)
|
||||||
const isProcessing = ref(false)
|
const isProcessing = ref(false)
|
||||||
const processingMessage = ref('')
|
const processingMessage = ref('')
|
||||||
@@ -334,57 +269,10 @@ const selectedColumns = ref([])
|
|||||||
const columnsSelected = ref(false)
|
const columnsSelected = ref(false)
|
||||||
const filteredCsvData = ref([])
|
const filteredCsvData = ref([])
|
||||||
const filteredCsvHeaders = ref([])
|
const filteredCsvHeaders = ref([])
|
||||||
const uploadedPdfs = ref({
|
|
||||||
gesamt: null,
|
|
||||||
erwachsene: null,
|
|
||||||
nachwuchs: null
|
|
||||||
})
|
|
||||||
|
|
||||||
const triggerFileInput = () => {
|
const triggerFileInput = () => {
|
||||||
fileInput.value?.click()
|
fileInput.value?.click()
|
||||||
}
|
}
|
||||||
|
|
||||||
const triggerPdfInput = (type) => {
|
|
||||||
if (type === 'gesamt') {
|
|
||||||
gesamtPdfInput.value?.click()
|
|
||||||
} else if (type === 'erwachsene') {
|
|
||||||
erwachsenePdfInput.value?.click()
|
|
||||||
} else if (type === 'nachwuchs') {
|
|
||||||
nachwuchsPdfInput.value?.click()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePdfUpload = async (type, event) => {
|
|
||||||
const file = event.target.files[0]
|
|
||||||
if (!file) return
|
|
||||||
|
|
||||||
if (file.type !== 'application/pdf') {
|
|
||||||
alert('Bitte wählen Sie eine PDF-Datei aus.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const formData = new FormData()
|
|
||||||
formData.append('pdf', file)
|
|
||||||
formData.append('type', type)
|
|
||||||
|
|
||||||
const response = await fetch('/api/cms/upload-spielplan-pdf', {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData
|
|
||||||
})
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
uploadedPdfs.value[type] = file.name
|
|
||||||
alert(`PDF für ${type} erfolgreich hochgeladen!`)
|
|
||||||
} else {
|
|
||||||
alert('Fehler beim Hochladen der PDF-Datei!')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler beim PDF-Upload:', error)
|
|
||||||
alert('Fehler beim Hochladen der PDF-Datei!')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleFileSelect = (event) => {
|
const handleFileSelect = (event) => {
|
||||||
const file = event.target.files[0]
|
const file = event.target.files[0]
|
||||||
if (file) {
|
if (file) {
|
||||||
@@ -417,9 +305,14 @@ const processFile = async (file) => {
|
|||||||
throw new Error('CSV-Datei muss mindestens eine Kopfzeile und eine Datenzeile enthalten')
|
throw new Error('CSV-Datei muss mindestens eine Kopfzeile und eine Datenzeile enthalten')
|
||||||
}
|
}
|
||||||
|
|
||||||
// CSV-Parser: Semikolon-getrennt, ohne Anführungszeichen
|
// CSV-Parser: Automatische Erkennung von Trennzeichen (Tab oder Semikolon)
|
||||||
const parseCSVLine = (line) => {
|
const parseCSVLine = (line) => {
|
||||||
return line.split(';').map(value => value.trim())
|
// Prüfe ob Tab oder Semikolon häufiger vorkommt
|
||||||
|
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())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Header-Zeile parsen
|
// Header-Zeile parsen
|
||||||
@@ -432,6 +325,16 @@ const processFile = async (file) => {
|
|||||||
selectedColumns.value = new Array(csvHeaders.value.length).fill(true)
|
selectedColumns.value = new Array(csvHeaders.value.length).fill(true)
|
||||||
columnsSelected.value = false
|
columnsSelected.value = false
|
||||||
|
|
||||||
|
// Debug: Zeige verfügbare Spalten
|
||||||
|
console.log('Verfügbare Spalten:', csvHeaders.value)
|
||||||
|
const halleSpalten = csvHeaders.value.filter(header =>
|
||||||
|
header.toLowerCase().includes('halle') ||
|
||||||
|
header.toLowerCase().includes('strasse') ||
|
||||||
|
header.toLowerCase().includes('plz') ||
|
||||||
|
header.toLowerCase().includes('ort')
|
||||||
|
)
|
||||||
|
console.log('Halle-Spalten gefunden:', halleSpalten)
|
||||||
|
|
||||||
// Datei-Info speichern
|
// Datei-Info speichern
|
||||||
currentFile.value = {
|
currentFile.value = {
|
||||||
name: file.name,
|
name: file.name,
|
||||||
@@ -501,6 +404,19 @@ const confirmColumnSelection = () => {
|
|||||||
columnsSelected.value = true
|
columnsSelected.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const suggestHalleColumns = () => {
|
||||||
|
// Automatisch Halle-Spalten vorschlagen
|
||||||
|
csvHeaders.value.forEach((header, index) => {
|
||||||
|
const headerLower = header.toLowerCase()
|
||||||
|
if (headerLower.includes('halle') ||
|
||||||
|
headerLower.includes('strasse') ||
|
||||||
|
headerLower.includes('plz') ||
|
||||||
|
headerLower.includes('ort')) {
|
||||||
|
selectedColumns.value[index] = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const clearData = () => {
|
const clearData = () => {
|
||||||
if (confirm('Möchten Sie alle Daten wirklich löschen?')) {
|
if (confirm('Möchten Sie alle Daten wirklich löschen?')) {
|
||||||
removeFile()
|
removeFile()
|
||||||
|
|||||||
@@ -204,15 +204,19 @@ const loadData = async () => {
|
|||||||
if (spielplanResult.success) {
|
if (spielplanResult.success) {
|
||||||
spielplanData.value = spielplanResult.data
|
spielplanData.value = spielplanResult.data
|
||||||
|
|
||||||
// Filtere unerwünschte Spalten heraus und benenne um
|
// Nur die gewünschten Spalten anzeigen
|
||||||
const originalHeaders = spielplanResult.headers
|
const originalHeaders = spielplanResult.headers
|
||||||
const headersToRemove = ['saison', 'meisterschaft', 'altersklasse', 'liga']
|
const desiredHeaders = ['Termin', 'HeimMannschaft', 'GastMannschaft', 'Runde', 'Staffel']
|
||||||
|
|
||||||
// Filtere unerwünschte Spalten heraus
|
// Finde die Indizes der gewünschten Spalten
|
||||||
const filteredHeaders = originalHeaders.filter(header => {
|
const headerIndices = desiredHeaders.map(desiredHeader => {
|
||||||
const headerLower = header.toLowerCase()
|
return originalHeaders.findIndex(header =>
|
||||||
return !headersToRemove.some(toRemove => headerLower.includes(toRemove))
|
header.toLowerCase() === desiredHeader.toLowerCase()
|
||||||
})
|
)
|
||||||
|
}).filter(index => index !== -1)
|
||||||
|
|
||||||
|
// Filtere nur die gewünschten Spalten
|
||||||
|
const filteredHeaders = headerIndices.map(index => originalHeaders[index])
|
||||||
|
|
||||||
// Benenne "Staffel" in "Gruppe" um (nur für Anzeige)
|
// Benenne "Staffel" in "Gruppe" um (nur für Anzeige)
|
||||||
headers.value = filteredHeaders.map(header => {
|
headers.value = filteredHeaders.map(header => {
|
||||||
|
|||||||
@@ -35,9 +35,19 @@ export default defineEventHandler(async (event) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers = lines[0].split(';')
|
// Automatische Erkennung des Trennzeichens
|
||||||
|
const firstLine = lines[0]
|
||||||
|
const tabCount = (firstLine.match(/\t/g) || []).length
|
||||||
|
const semicolonCount = (firstLine.match(/;/g) || []).length
|
||||||
|
const delimiter = tabCount > semicolonCount ? '\t' : ';'
|
||||||
|
|
||||||
|
console.log(`Verwendetes Trennzeichen: ${delimiter === '\t' ? 'Tab' : 'Semikolon'}`)
|
||||||
|
|
||||||
|
const headers = firstLine.split(delimiter)
|
||||||
|
console.log('CSV-Header:', headers)
|
||||||
|
|
||||||
const dataRows = lines.slice(1).map(line => {
|
const dataRows = lines.slice(1).map(line => {
|
||||||
const values = line.split(';')
|
const values = line.split(delimiter)
|
||||||
const row = {}
|
const row = {}
|
||||||
headers.forEach((header, index) => {
|
headers.forEach((header, index) => {
|
||||||
row[header] = values[index] || ''
|
row[header] = values[index] || ''
|
||||||
@@ -45,6 +55,9 @@ export default defineEventHandler(async (event) => {
|
|||||||
return row
|
return row
|
||||||
})
|
})
|
||||||
|
|
||||||
|
console.log('Anzahl Datenzeilen:', dataRows.length)
|
||||||
|
console.log('Erste Datenzeile:', dataRows[0])
|
||||||
|
|
||||||
// Filtere Daten basierend auf Team
|
// Filtere Daten basierend auf Team
|
||||||
let filteredData = dataRows
|
let filteredData = dataRows
|
||||||
|
|
||||||
@@ -183,6 +196,45 @@ export default defineEventHandler(async (event) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Sammle Halle-Informationen für die jeweilige Mannschaft
|
||||||
|
const hallenMap = new Map()
|
||||||
|
|
||||||
|
// Debug: Zeige verfügbare Spalten
|
||||||
|
console.log('Verfügbare Spalten in gefilterten Daten:', Object.keys(filteredData[0] || {}))
|
||||||
|
|
||||||
|
filteredData.forEach((row, index) => {
|
||||||
|
// Suche Halle-Spalten mit verschiedenen möglichen Namen
|
||||||
|
const halleName = row.HalleName || row.halleName || row.Halle || row.halle || ''
|
||||||
|
const halleStrasse = row.HalleStrasse || row.halleStrasse || row.HalleStrasse || row.halleStrasse || ''
|
||||||
|
const hallePLZ = row.HallePLZ || row.hallePLZ || row.HallePLZ || row.hallePLZ || ''
|
||||||
|
const halleOrt = row.HalleOrt || row.halleOrt || row.HalleOrt || row.halleOrt || ''
|
||||||
|
const heimMannschaft = row.HeimMannschaft || ''
|
||||||
|
|
||||||
|
// Debug: Zeige Halle-Daten für erste paar Zeilen
|
||||||
|
if (index < 3) {
|
||||||
|
console.log(`Zeile ${index}: HalleName="${halleName}", HalleStrasse="${halleStrasse}", HallePLZ="${hallePLZ}", HalleOrt="${halleOrt}", HeimMannschaft="${heimMannschaft}"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (halleName && halleStrasse && hallePLZ && halleOrt) {
|
||||||
|
const halleKey = `${halleName}|${halleStrasse}|${hallePLZ}|${halleOrt}`
|
||||||
|
if (!hallenMap.has(halleKey)) {
|
||||||
|
hallenMap.set(halleKey, {
|
||||||
|
name: halleName,
|
||||||
|
strasse: halleStrasse,
|
||||||
|
plz: hallePLZ,
|
||||||
|
ort: halleOrt,
|
||||||
|
mannschaften: new Set()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Füge Heimmannschaft hinzu (unabhängig von Harheimer TC)
|
||||||
|
hallenMap.get(halleKey).mannschaften.add(heimMannschaft)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const hallenListe = Array.from(hallenMap.values())
|
||||||
|
console.log('Gefundene Hallen:', hallenListe.length, hallenListe)
|
||||||
|
|
||||||
// Generiere LaTeX-Code für PDF
|
// Generiere LaTeX-Code für PDF
|
||||||
const teamName = team.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())
|
const teamName = team.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())
|
||||||
const currentDate = new Date().toLocaleDateString('de-DE')
|
const currentDate = new Date().toLocaleDateString('de-DE')
|
||||||
@@ -256,6 +308,27 @@ ${filteredData.map(row => {
|
|||||||
\\end{center}
|
\\end{center}
|
||||||
`}
|
`}
|
||||||
|
|
||||||
|
${hallenListe.length > 0 ? `
|
||||||
|
\\newpage
|
||||||
|
\\section*{Spielstätten}
|
||||||
|
\\vspace{0.5cm}
|
||||||
|
|
||||||
|
\\begin{longtable}{|p{5cm}|p{4cm}|p{8cm}|}
|
||||||
|
\\hline
|
||||||
|
\\textbf{Heimmannschaft} & \\textbf{Halle} & \\textbf{Adresse} \\\\
|
||||||
|
\\hline
|
||||||
|
\\endhead
|
||||||
|
|
||||||
|
${hallenListe.map(halle => {
|
||||||
|
const halleName = halle.name.replace(/&/g, '\\&')
|
||||||
|
const adresse = `${halle.strasse.replace(/&/g, '\\&')}, ${halle.plz} ${halle.ort.replace(/&/g, '\\&')}`
|
||||||
|
const mannschaften = Array.from(halle.mannschaften).map(m => m.replace(/&/g, '\\&')).join(', ')
|
||||||
|
return `${mannschaften} & ${halleName} & ${adresse} \\\\ \\hline`
|
||||||
|
}).join('\n')}
|
||||||
|
|
||||||
|
\\end{longtable}
|
||||||
|
` : ''}
|
||||||
|
|
||||||
\\vfill
|
\\vfill
|
||||||
\\begin{center}
|
\\begin{center}
|
||||||
\\small Generiert am ${currentDate} | Harheimer TC
|
\\small Generiert am ${currentDate} | Harheimer TC
|
||||||
|
|||||||
Reference in New Issue
Block a user