452 lines
16 KiB
JavaScript
452 lines
16 KiB
JavaScript
import fs from 'fs/promises'
|
|
import path from 'path'
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
try {
|
|
const query = getQuery(event)
|
|
const team = query.team
|
|
|
|
if (!team) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
statusMessage: 'Team-Parameter fehlt'
|
|
})
|
|
}
|
|
|
|
// Lade Spielplandaten
|
|
const csvPath = path.join(process.cwd(), 'public/data/spielplan.csv')
|
|
let csvContent
|
|
|
|
try {
|
|
csvContent = await fs.readFile(csvPath, 'utf-8')
|
|
} catch (_error) {
|
|
throw createError({
|
|
statusCode: 404,
|
|
statusMessage: 'Spielplandaten nicht gefunden'
|
|
})
|
|
}
|
|
|
|
// Parse CSV
|
|
const lines = csvContent.split('\n').filter(line => line.trim())
|
|
if (lines.length < 2) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
statusMessage: 'Keine Spielplandaten verfügbar'
|
|
})
|
|
}
|
|
|
|
// 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' : ';'
|
|
|
|
// nosemgrep: javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring
|
|
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 values = line.split(delimiter)
|
|
const row = {}
|
|
headers.forEach((header, index) => {
|
|
row[header] = values[index] || ''
|
|
})
|
|
return row
|
|
})
|
|
|
|
console.log('Anzahl Datenzeilen:', dataRows.length)
|
|
console.log('Erste Datenzeile:', dataRows[0])
|
|
|
|
// Filtere Daten basierend auf Team
|
|
let filteredData = dataRows
|
|
|
|
if (team !== 'all') {
|
|
filteredData = dataRows.filter(row => {
|
|
const heimMannschaft = (row.HeimMannschaft || '').toLowerCase()
|
|
const gastMannschaft = (row.GastMannschaft || '').toLowerCase()
|
|
const heimAltersklasse = (row.HeimMannschaftAltersklasse || '').toLowerCase()
|
|
const gastAltersklasse = (row.GastMannschaftAltersklasse || '').toLowerCase()
|
|
|
|
// Prüfe ob eine der Mannschaften Harheimer TC ist
|
|
const isHarheimerHeim = heimMannschaft.includes('harheimer tc')
|
|
const isHarheimerGast = gastMannschaft.includes('harheimer tc')
|
|
|
|
if (!isHarheimerHeim && !isHarheimerGast) {
|
|
return false // Kein Harheimer TC Spiel
|
|
}
|
|
|
|
// Mapping zwischen Team-Namen und CSV-Daten
|
|
const mannschaftMapping = {
|
|
'erwachsene': ['harheimer tc'], // Alle Erwachsenen-Mannschaften
|
|
'nachwuchs': ['harheimer tc'], // Alle Jugend-Mannschaften
|
|
'erwachsene_1': ['harheimer tc'], // Nur ohne römische Zahl
|
|
'erwachsene_2': ['harheimer tc ii'],
|
|
'erwachsene_3': ['harheimer tc iii'],
|
|
'erwachsene_4': ['harheimer tc iv'],
|
|
'erwachsene_5': ['harheimer tc v'],
|
|
'jugendmannschaft': ['harheimer tc'] // Jugend hat keine römische Zahl
|
|
}
|
|
|
|
const csvVariants = mannschaftMapping[team] || []
|
|
|
|
if (csvVariants.length === 0) {
|
|
return false
|
|
}
|
|
|
|
// Prüfe Mannschafts-Zuordnung UND Altersklasse
|
|
const mannschaftMatch = csvVariants.some(variant => {
|
|
// Strikte Übereinstimmung: Prüfe exakte Mannschaftsnamen
|
|
if (isHarheimerHeim) {
|
|
// Für "harheimer tc" (Erwachsene 1): Nur wenn KEINE römische Zahl folgt
|
|
if (variant === 'harheimer tc') {
|
|
return heimMannschaft === 'harheimer tc' ||
|
|
heimMannschaft.startsWith('harheimer tc ') &&
|
|
!heimMannschaft.match(/harheimer tc\s+[ivx]+/i)
|
|
}
|
|
// Für andere Mannschaften: Exakte Übereinstimmung
|
|
return heimMannschaft === variant || heimMannschaft.startsWith(variant + ' ')
|
|
}
|
|
if (isHarheimerGast) {
|
|
// Für "harheimer tc" (Erwachsene 1): Nur wenn KEINE römische Zahl folgt
|
|
if (variant === 'harheimer tc') {
|
|
return gastMannschaft === 'harheimer tc' ||
|
|
gastMannschaft.startsWith('harheimer tc ') &&
|
|
!gastMannschaft.match(/harheimer tc\s+[ivx]+/i)
|
|
}
|
|
// Für andere Mannschaften: Exakte Übereinstimmung
|
|
return gastMannschaft === variant || gastMannschaft.startsWith(variant + ' ')
|
|
}
|
|
return false
|
|
})
|
|
|
|
if (!mannschaftMatch) {
|
|
return false
|
|
}
|
|
|
|
// Zusätzliche Altersklassen-Prüfung für spezifische Mannschaften
|
|
if (team.startsWith('erwachsene')) {
|
|
// Erwachsenen-Mannschaften: MUSS Erwachsene sein, DARF NICHT Jugend sein
|
|
const isErwachsenenHeim = isHarheimerHeim &&
|
|
heimAltersklasse.includes('erwachsene') &&
|
|
!heimAltersklasse.includes('jugend')
|
|
const isErwachsenenGast = isHarheimerGast &&
|
|
gastAltersklasse.includes('erwachsene') &&
|
|
!gastAltersklasse.includes('jugend')
|
|
|
|
return isErwachsenenHeim || isErwachsenenGast
|
|
} else if (team === 'jugendmannschaft' || team === 'nachwuchs') {
|
|
// Jugend-Mannschaft: MUSS Jugend sein
|
|
const isJugendHeim = isHarheimerHeim &&
|
|
(heimAltersklasse.includes('jugend') || heimMannschaft.includes('jugend'))
|
|
const isJugendGast = isHarheimerGast &&
|
|
(gastAltersklasse.includes('jugend') || gastMannschaft.includes('jugend'))
|
|
|
|
return isJugendHeim || isJugendGast
|
|
}
|
|
|
|
return true // Fallback für unbekannte Mannschaften
|
|
})
|
|
}
|
|
|
|
// Filtere nach Wettbewerb (Standard: Punktrunde)
|
|
const wettbewerb = query.wettbewerb || 'punktrunde'
|
|
if (wettbewerb !== 'alle') {
|
|
filteredData = filteredData.filter(row => {
|
|
const runde = (row.Runde || '').toLowerCase()
|
|
|
|
if (wettbewerb === 'punktrunde') {
|
|
return runde === 'vr' ||
|
|
runde.includes('vorrunde') ||
|
|
runde === 'rr' ||
|
|
runde.includes('rückrunde') ||
|
|
runde.includes('verbandsrunde')
|
|
} else if (wettbewerb === 'pokal') {
|
|
return runde === 'pokal' || runde.includes('pokal')
|
|
}
|
|
|
|
return true
|
|
})
|
|
}
|
|
|
|
// Filtere nach aktueller Saison (2025/26)
|
|
const currentSaisonStart = new Date(2025, 6, 1) // 01.07.2025
|
|
const currentSaisonEnd = new Date(2026, 5, 30) // 30.06.2026
|
|
|
|
filteredData = filteredData.filter(row => {
|
|
const termin = row.Termin
|
|
if (!termin) return false
|
|
|
|
try {
|
|
let spielDatum
|
|
|
|
if (termin.includes(' ')) {
|
|
const datumTeil = termin.split(' ')[0]
|
|
const [tag, monat, jahr] = datumTeil.split('.')
|
|
spielDatum = new Date(jahr, monat - 1, tag)
|
|
} else {
|
|
spielDatum = new Date(termin)
|
|
}
|
|
|
|
if (isNaN(spielDatum.getTime())) return false
|
|
|
|
return spielDatum >= currentSaisonStart && spielDatum <= currentSaisonEnd
|
|
} catch (_error) {
|
|
return false
|
|
}
|
|
})
|
|
|
|
// 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) {
|
|
// nosemgrep: javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring
|
|
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
|
|
const teamName = team.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())
|
|
const currentDate = new Date().toLocaleDateString('de-DE')
|
|
|
|
let latexContent = `
|
|
\\documentclass[9pt,a4paper]{article}
|
|
\\usepackage[utf8]{inputenc}
|
|
\\usepackage[ngerman]{babel}
|
|
\\usepackage{geometry}
|
|
\\usepackage{array}
|
|
\\usepackage{longtable}
|
|
\\usepackage{helvet}
|
|
\\renewcommand{\\familydefault}{\\sfdefault}
|
|
|
|
\\geometry{margin=2cm}
|
|
|
|
\\title{Spielplan ${teamName}}
|
|
\\author{Harheimer TC}
|
|
\\date{Saison 2025/26 - Stand: ${currentDate}}
|
|
|
|
% PDF-Metadaten für Sicherheit
|
|
\\usepackage[pdftex]{hyperref}
|
|
\\hypersetup{
|
|
pdfauthor={Harheimer TC},
|
|
pdftitle={Spielplan ${teamName} - Saison 2025/26},
|
|
pdfsubject={Vereinsspielplan},
|
|
pdfkeywords={Tischtennis, Spielplan, Harheimer TC, ${teamName}},
|
|
pdfcreator={Harheimer TC CMS},
|
|
pdfproducer={LaTeX mit pdflatex},
|
|
pdfcreationdate={${new Date().toISOString()}},
|
|
pdfmoddate={${new Date().toISOString()}},
|
|
colorlinks=false,
|
|
pdfborder={0 0 0}
|
|
}
|
|
|
|
\\begin{document}
|
|
\\maketitle
|
|
|
|
\\section*{Spielplan ${teamName}}
|
|
\\vspace{0.5cm}
|
|
|
|
${filteredData.length > 0 ? `
|
|
\\renewcommand{\\arraystretch}{1.3}
|
|
\\begin{longtable}{|p{1.8cm}|p{1.1cm}|p{6.5cm}|p{6.5cm}|}
|
|
\\hline
|
|
\\textbf{Datum} & \\textbf{Uhrzeit} & \\textbf{Heim} & \\textbf{Gast} \\\\
|
|
\\hline
|
|
\\endhead
|
|
|
|
${filteredData.map(row => {
|
|
const termin = row.Termin || ''
|
|
const datum = termin.includes(' ') ? termin.split(' ')[0] : termin
|
|
const uhrzeit = termin.includes(' ') ? termin.split(' ')[1] : ''
|
|
const heim = (row.HeimMannschaft || '').replace(/&/g, '\\&')
|
|
const gast = (row.GastMannschaft || '').replace(/&/g, '\\&')
|
|
const heimAltersklasse = (row.HeimMannschaftAltersklasse || '').toLowerCase()
|
|
const gastAltersklasse = (row.GastMannschaftAltersklasse || '').toLowerCase()
|
|
|
|
// Prüfe ob es Jugend-Mannschaften sind
|
|
const isHeimJugend = heimAltersklasse.includes('jugend') || heim.toLowerCase().includes('jugend')
|
|
const isGastJugend = gastAltersklasse.includes('jugend') || gast.toLowerCase().includes('jugend')
|
|
|
|
// Füge "(J) " vor Jugendmannschaften hinzu (nur bei Gesamtspielplan)
|
|
let heimFormatted = heim
|
|
let gastFormatted = gast
|
|
|
|
if (team === 'all') {
|
|
if (isHeimJugend) {
|
|
heimFormatted = `(J) ${heim}`
|
|
}
|
|
if (isGastJugend) {
|
|
gastFormatted = `(J) ${gast}`
|
|
}
|
|
}
|
|
|
|
const heimBold = heimFormatted.toLowerCase().includes('harheimer tc') ? `\\textbf{${heimFormatted}}` : heimFormatted
|
|
const gastBold = gastFormatted.toLowerCase().includes('harheimer tc') ? `\\textbf{${gastFormatted}}` : gastFormatted
|
|
|
|
return `${datum} & ${uhrzeit} & ${heimBold} & ${gastBold} \\\\ \\hline`
|
|
}).join('\n')}
|
|
|
|
\\end{longtable}
|
|
` : `
|
|
\\begin{center}
|
|
\\textit{Keine Spiele für ${teamName} gefunden.}
|
|
\\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
|
|
\\begin{center}
|
|
\\small Generiert am ${currentDate} | Harheimer TC
|
|
\\end{center}
|
|
|
|
\\end{document}`
|
|
|
|
// Schreibe LaTeX-Datei temporär
|
|
const tempDir = path.join(process.cwd(), 'temp')
|
|
try {
|
|
await fs.mkdir(tempDir, { recursive: true })
|
|
} catch (_error) {
|
|
// Verzeichnis existiert bereits
|
|
}
|
|
|
|
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
|
const tempTexFile = path.join(tempDir, `spielplan_${team}_${Date.now()}.tex`)
|
|
await fs.writeFile(tempTexFile, latexContent, 'utf-8')
|
|
|
|
// Kompiliere LaTeX zu PDF
|
|
const { exec } = await import('child_process')
|
|
const { promisify } = await import('util')
|
|
const execAsync = promisify(exec)
|
|
|
|
try {
|
|
const { stdout, stderr } = await execAsync(`pdflatex -interaction=nonstopmode -output-directory=${tempDir} "${tempTexFile}"`)
|
|
|
|
// PDF-Datei lesen
|
|
const pdfFile = tempTexFile.replace('.tex', '.pdf')
|
|
const pdfBuffer = await fs.readFile(pdfFile)
|
|
|
|
// Cleanup: Lösche temporäre Dateien
|
|
setTimeout(async () => {
|
|
try {
|
|
await fs.unlink(tempTexFile)
|
|
await fs.unlink(pdfFile)
|
|
await fs.unlink(tempTexFile.replace('.tex', '.log'))
|
|
await fs.unlink(tempTexFile.replace('.tex', '.aux'))
|
|
} catch (_error) {
|
|
console.error('Fehler beim Löschen temporärer Dateien:', error)
|
|
}
|
|
}, 5000)
|
|
|
|
// Setze PDF-Headers
|
|
setHeader(event, 'Content-Type', 'application/pdf')
|
|
setHeader(event, 'Content-Disposition', `attachment; filename="spielplan_${team}.pdf"`)
|
|
setHeader(event, 'Content-Length', pdfBuffer.length.toString())
|
|
|
|
// Füge Sicherheits-Header hinzu
|
|
setHeader(event, 'X-Content-Type-Options', 'nosniff')
|
|
setHeader(event, 'X-Frame-Options', 'DENY')
|
|
setHeader(event, 'X-XSS-Protection', '1; mode=block')
|
|
setHeader(event, 'Cache-Control', 'no-cache, no-store, must-revalidate')
|
|
setHeader(event, 'Pragma', 'no-cache')
|
|
setHeader(event, 'Expires', '0')
|
|
|
|
return pdfBuffer
|
|
|
|
} catch (compileError) {
|
|
console.error('LaTeX-Kompilierung fehlgeschlagen:', compileError)
|
|
|
|
// Fallback: HTML-Response mit Fehlermeldung
|
|
const errorHtml = `
|
|
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>PDF-Generierung fehlgeschlagen</title>
|
|
<style>
|
|
body { font-family: Arial, sans-serif; margin: 40px; background-color: #f5f5f5; }
|
|
.container { background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
|
.error { color: #d32f2f; font-size: 24px; margin-bottom: 20px; }
|
|
.message { color: #333; font-size: 16px; line-height: 1.6; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="error">❌ PDF-Generierung fehlgeschlagen</div>
|
|
<div class="message">
|
|
Die PDF-Generierung für <strong>"${teamName}"</strong> ist fehlgeschlagen.<br>
|
|
Bitte kontaktieren Sie den Administrator.
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>`
|
|
|
|
setHeader(event, 'Content-Type', 'text/html; charset=utf-8')
|
|
return errorHtml
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Fehler beim Generieren des Spielplans:', error)
|
|
|
|
if (error.statusCode) {
|
|
throw error
|
|
}
|
|
|
|
throw createError({
|
|
statusCode: 500,
|
|
statusMessage: 'Fehler beim Generieren des Spielplans'
|
|
})
|
|
}
|
|
})
|