Add script for importing match schedule and logging
Some checks failed
Code Analysis and Production Deploy / analyze (push) Has been skipped
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Successful in 2m2s
Code Analysis and Production Deploy / analyze (pull_request) Failing after 33s
Code Analysis and Production Deploy / deploy-production (pull_request) Has been skipped
Code Analysis and Production Deploy / deploy-test (pull_request) Has been skipped
Require Package Version Change / check (pull_request) Failing after 10s

- Created `import-spielplan.js` to fetch and parse the match schedule from the specified URL, saving the output as JSON.
- Added `run-spielplan-import.sh` to automate the execution of the import script and log output.
- Introduced `spielplan.html` file to store the downloaded HTML content for further processing.
This commit is contained in:
Torsten Schulz (local)
2026-05-19 16:23:28 +02:00
parent c78adc0d52
commit 0849c625cb
21 changed files with 11413 additions and 233 deletions

View File

@@ -23,6 +23,33 @@
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<!-- Filter Selection -->
<div class="flex flex-col sm:flex-row sm:items-center gap-4">
<!-- Saison-Filter -->
<div
v-if="seasons.length"
class="flex items-center space-x-2"
>
<label
for="season-select"
class="text-sm font-medium text-gray-700"
>
Saison:
</label>
<select
id="season-select"
v-model="selectedSeason"
class="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500 bg-white text-sm"
@change="onSeasonChange"
>
<option
v-for="season in seasons"
:key="season.slug"
:value="season.slug"
>
{{ season.label }}
</option>
</select>
</div>
<!-- Wettbewerbs-Filter -->
<div class="flex items-center space-x-2">
<label
@@ -83,31 +110,44 @@
</div>
</div>
<!-- Download Button -->
<button
:disabled="isLoading || !filteredData.length"
class="inline-flex items-center px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors disabled:bg-gray-400"
@click="downloadPDF"
>
<svg
class="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
<div class="flex flex-col sm:flex-row sm:items-center gap-3">
<button
:disabled="isLoading"
class="inline-flex items-center justify-center px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors disabled:bg-gray-400"
@click="loadData"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 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>
PDF Download
</button>
Spielplan laden
</button>
<!-- Download Button -->
<button
:disabled="isLoading || !filteredData.length"
class="inline-flex items-center justify-center px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors disabled:bg-gray-400"
@click="downloadPDF"
>
<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="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 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>
PDF Download
</button>
</div>
</div>
<!-- Filter Info -->
<div class="mt-4 text-sm text-gray-600">
<div
v-if="hasLoadedSpielplan"
class="mt-4 text-sm text-gray-600"
>
<span v-if="selectedFilter === 'all'">
{{ getWettbewerbText() }} - Alle Mannschaften ({{ filteredData.length }} von {{ spielplanData.length }} Einträgen)
</span>
@@ -179,6 +219,31 @@
</div>
<!-- Empty State -->
<div
v-else-if="!hasLoadedSpielplan"
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 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
/>
</svg>
<h3 class="text-lg font-medium text-gray-900 mb-2">
Spielplan bereit
</h3>
<p class="text-gray-600">
Laden Sie den aktuellen Spielplan.
</p>
</div>
<div
v-else-if="!spielplanData || spielplanData.length === 0"
class="text-center py-12 bg-white rounded-xl shadow-lg"
@@ -263,6 +328,17 @@
<span v-else-if="header.toLowerCase().includes('runde')">
{{ formatRunde(row[getOriginalHeader(header)]) }}
</span>
<span
v-else-if="header.toLowerCase().includes('gruppe')"
class="text-xs leading-tight text-gray-700"
>
<span class="block">
{{ row.Altersklasse || '-' }}
</span>
<span class="block text-gray-500">
{{ formatStaffel(row[getOriginalHeader(header)]) }}
</span>
</span>
<span v-else>
{{ row[getOriginalHeader(header)] || '-' }}
</span>
@@ -298,6 +374,9 @@ const selectedFilter = ref('all')
const selectedWettbewerb = ref('punktrunde')
const filteredData = ref([])
const mannschaften = ref([])
const seasons = ref([])
const selectedSeason = ref('')
const hasLoadedSpielplan = ref(false)
async function fetchCsvText(url) {
const attempt = async () => {
@@ -321,20 +400,28 @@ const loadData = async () => {
error.value = null
try {
// Lade Spielplandaten und Mannschaften parallel
const params = new URLSearchParams()
if (selectedSeason.value) params.set('season', selectedSeason.value)
const [spielplanResponse, mannschaftenResponse] = await Promise.all([
fetch('/api/spielplan'),
fetch(`/api/spielplan${params.toString() ? `?${params.toString()}` : ''}`),
fetchCsvText('/api/mannschaften')
])
// Spielplandaten verarbeiten
const spielplanResult = await spielplanResponse.json()
await applyMannschaftenResponse(mannschaftenResponse)
if (spielplanResult.success) {
spielplanData.value = spielplanResult.data
spielplanData.value = spielplanResult.data.map(row => ({
...row,
Ergebnis: formatResult(row)
}))
seasons.value = spielplanResult.seasons?.length ? spielplanResult.seasons : seasons.value
selectedSeason.value = spielplanResult.season || selectedSeason.value
// Nur die gewünschten Spalten anzeigen
const originalHeaders = spielplanResult.headers
const desiredHeaders = ['Termin', 'HeimMannschaft', 'GastMannschaft', 'Runde', 'Staffel']
const originalHeaders = [...spielplanResult.headers, 'Ergebnis']
const desiredHeaders = ['Termin', 'HeimMannschaft', 'GastMannschaft', 'Ergebnis', 'Runde', 'Staffel']
// Finde die Indizes der gewünschten Spalten
const headerIndices = desiredHeaders.map(desiredHeader => {
@@ -364,41 +451,11 @@ const loadData = async () => {
window.spielplanHeaderMapping = headerMapping
lastUpdated.value = new Date().toLocaleString('de-DE')
hasLoadedSpielplan.value = true
} else {
error.value = spielplanResult.message
}
// Mannschaften aus CMS laden (manuell eingegebene Mannschaften)
if (mannschaftenResponse) {
const csvText = mannschaftenResponse
const lines = csvText.split('\n').filter(line => line.trim() !== '')
if (lines.length > 1) {
mannschaften.value = lines.slice(1).map(line => {
// Besserer 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())
return values[0] // Erste Spalte ist der Mannschaftsname
}).filter(name => name && name !== '')
}
}
filterData() // Initial filter
} catch (err) {
console.error('Fehler beim Laden der Daten:', err)
@@ -408,68 +465,67 @@ const loadData = async () => {
}
}
const applyMannschaftenResponse = async (csvText) => {
const lines = csvText.split('\n').filter(line => line.trim() !== '')
if (lines.length <= 1) return
mannschaften.value = lines.slice(1).map(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[0]
}).filter(name => name && name !== '')
}
const onSeasonChange = () => {
spielplanData.value = []
filteredData.value = []
headers.value = []
lastUpdated.value = ''
hasLoadedSpielplan.value = false
}
const filterData = () => {
if (!spielplanData.value || spielplanData.value.length === 0) {
filteredData.value = []
return
}
// Zuerst nach aktueller Saison filtern (immer aktiv)
// Da die Spiele bis 2026 gehen, nehmen wir die Saison 2025/26
// Saison läuft vom 01.07. bis 30.06. des Folgejahres
const saisonStartYear = 2025
const saisonEndYear = 2026
const saisonStart = new Date(saisonStartYear, 6, 1) // 01.07.2025
const saisonEnd = new Date(saisonEndYear, 5, 30) // 30.06.2026
let saisonFiltered = spielplanData.value.filter(row => {
const termin = row.Termin
if (!termin) return false
try {
// Parse deutsches Datumsformat: "27.10.2025 20:00"
let spielDatum
if (termin.includes(' ')) {
// Uhrzeit entfernen: "27.10.2025 20:00" -> "27.10.2025"
const datumTeil = termin.split(' ')[0]
// Deutsches Format parsen: "27.10.2025" -> Date
const [tag, monat, jahr] = datumTeil.split('.')
spielDatum = new Date(jahr, monat - 1, tag) // Monat ist 0-basiert
} else {
spielDatum = new Date(termin)
}
if (isNaN(spielDatum.getTime())) return false
// Prüfe ob das Spiel in der aktuellen Saison liegt
const inSaison = spielDatum >= saisonStart && spielDatum <= saisonEnd
return inSaison
} catch (_error) {
console.error('Fehler beim Parsen von Termin:', termin, error)
return false
}
})
let saisonFiltered = spielplanData.value
// Dann nach Wettbewerb filtern
let wettbewerbFiltered = saisonFiltered
if (selectedWettbewerb.value === 'punktrunde') {
wettbewerbFiltered = saisonFiltered.filter(row => {
// Filtere nach Punktrunde-Spielen (VR = Vorrunde, RR = Rückrunde)
const runde = (row.Runde || '').toLowerCase()
const isMatch = runde === 'vr' || runde === 'rr' || runde.includes('vorrunde') || runde.includes('rückrunde')
return isMatch
const staffel = (row.Staffel || '').toLowerCase()
const liga = (row.Liga || '').toLowerCase()
const isPokal = runde.includes('pokal') || staffel.includes('pokal') || liga.includes('pokal')
return !isPokal
})
} else if (selectedWettbewerb.value === 'pokal') {
wettbewerbFiltered = saisonFiltered.filter(row => {
// Filtere nach Pokal-Spielen
const runde = (row.Runde || '').toLowerCase()
return runde === 'pokal' || runde.includes('pokal')
const staffel = (row.Staffel || '').toLowerCase()
const liga = (row.Liga || '').toLowerCase()
return runde.includes('pokal') || staffel.includes('pokal') || liga.includes('pokal')
})
}
// "alle" zeigt alle Spiele ohne weitere Filterung
@@ -634,6 +690,7 @@ const downloadPDF = () => {
team: teamParam,
wettbewerb: selectedWettbewerb.value
})
if (selectedSeason.value) params.set('season', selectedSeason.value)
const downloadUrl = `/api/spielplan/pdf?${params.toString()}`
// Öffne Download in neuem Tab
@@ -703,6 +760,18 @@ const formatRunde = (rundeString) => {
return rundeString
}
const formatStaffel = (staffelString) => {
const staffel = String(staffelString || '').trim()
if (!staffel) return '-'
return staffel.replace(/^E(?=\d)/, '')
}
const formatResult = (row) => {
const heim = String(row?.SpieleHeim || '').trim()
const gast = String(row?.SpieleGast || '').trim()
return heim || gast ? `${heim}:${gast}` : '-'
}
const getOriginalHeader = (displayHeader) => {
// Verwende das Mapping um den ursprünglichen Header-Namen zu finden
if (window.spielplanHeaderMapping && window.spielplanHeaderMapping[displayHeader]) {
@@ -774,4 +843,5 @@ const getWettbewerbText = () => {
onMounted(() => {
loadData()
})
</script>
</script>