Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 46s
This commit introduces a new utility function, fetchCsvText, to streamline the fetching of CSV data across multiple components. The function includes a cache-busting mechanism and retry logic to enhance reliability when retrieving data from the server. This change improves error handling and ensures consistent data retrieval in the Mannschaften overview, detail, and schedule pages, contributing to a more robust application.
777 lines
26 KiB
Vue
777 lines
26 KiB
Vue
<template>
|
|
<div class="min-h-screen bg-gray-50">
|
|
<!-- Header -->
|
|
<div class="bg-white shadow-sm">
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h1 class="text-3xl font-bold text-gray-900">
|
|
Spielpläne
|
|
</h1>
|
|
<p class="mt-2 text-gray-600">
|
|
Alle Spielpläne der Mannschaften
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Content -->
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
<!-- Filter and Download Section -->
|
|
<div class="bg-white rounded-xl shadow-lg p-6 mb-8">
|
|
<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">
|
|
<!-- Wettbewerbs-Filter -->
|
|
<div class="flex items-center space-x-2">
|
|
<label
|
|
for="wettbewerb-select"
|
|
class="text-sm font-medium text-gray-700"
|
|
>
|
|
Wettbewerb:
|
|
</label>
|
|
<select
|
|
id="wettbewerb-select"
|
|
v-model="selectedWettbewerb"
|
|
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="filterData"
|
|
>
|
|
<option value="punktrunde">
|
|
Punktrunde
|
|
</option>
|
|
<option value="pokal">
|
|
Pokal
|
|
</option>
|
|
<option value="alle">
|
|
Alle
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Mannschafts-Filter -->
|
|
<div class="flex items-center space-x-2">
|
|
<label
|
|
for="filter-select"
|
|
class="text-sm font-medium text-gray-700"
|
|
>
|
|
Mannschaft:
|
|
</label>
|
|
<select
|
|
id="filter-select"
|
|
v-model="selectedFilter"
|
|
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="filterData"
|
|
>
|
|
<option value="all">
|
|
Gesamt
|
|
</option>
|
|
<option value="erwachsene">
|
|
Erwachsene
|
|
</option>
|
|
<option value="nachwuchs">
|
|
Nachwuchs
|
|
</option>
|
|
<option
|
|
v-for="mannschaft in mannschaften"
|
|
:key="mannschaft"
|
|
:value="mannschaft"
|
|
>
|
|
{{ mannschaft }}
|
|
</option>
|
|
</select>
|
|
</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"
|
|
>
|
|
<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>
|
|
|
|
<!-- Filter Info -->
|
|
<div class="mt-4 text-sm text-gray-600">
|
|
<span v-if="selectedFilter === 'all'">
|
|
{{ getWettbewerbText() }} - Alle Mannschaften ({{ filteredData.length }} von {{ spielplanData.length }} Einträgen)
|
|
</span>
|
|
<span v-else-if="selectedFilter === 'erwachsene'">
|
|
{{ getWettbewerbText() }} - Erwachsenen-Mannschaften ({{ filteredData.length }} von {{ spielplanData.length }} Einträgen)
|
|
</span>
|
|
<span v-else-if="selectedFilter === 'nachwuchs'">
|
|
{{ getWettbewerbText() }} - Nachwuchs-Mannschaften ({{ filteredData.length }} von {{ spielplanData.length }} Einträgen)
|
|
</span>
|
|
<span v-else>
|
|
{{ getWettbewerbText() }} - {{ selectedFilter }} ({{ filteredData.length }} von {{ spielplanData.length }} Einträgen)
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Loading State -->
|
|
<div
|
|
v-if="isLoading"
|
|
class="text-center py-12"
|
|
>
|
|
<svg
|
|
class="w-8 h-8 text-gray-400 mx-auto mb-4 animate-spin"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
|
/>
|
|
</svg>
|
|
<p class="text-gray-600">
|
|
Spielpläne werden geladen...
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Error State -->
|
|
<div
|
|
v-else-if="error"
|
|
class="bg-red-50 border border-red-200 rounded-lg p-6 text-center"
|
|
>
|
|
<svg
|
|
class="w-12 h-12 text-red-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="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
|
|
/>
|
|
</svg>
|
|
<h3 class="text-lg font-medium text-red-800 mb-2">
|
|
Fehler beim Laden
|
|
</h3>
|
|
<p class="text-red-600 mb-4">
|
|
{{ error }}
|
|
</p>
|
|
<button
|
|
class="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
|
|
@click="loadData"
|
|
>
|
|
Erneut versuchen
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Empty State -->
|
|
<div
|
|
v-else-if="!spielplanData || spielplanData.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 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">
|
|
Keine Spielpläne verfügbar
|
|
</h3>
|
|
<p class="text-gray-600">
|
|
Es wurden noch keine Spielplandaten hochgeladen.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Spielplan Table -->
|
|
<div
|
|
v-else
|
|
class="bg-white rounded-xl shadow-lg overflow-hidden"
|
|
>
|
|
<div class="px-6 py-4 border-b border-gray-200">
|
|
<h2 class="text-xl font-semibold text-gray-900">
|
|
Spielplan
|
|
</h2>
|
|
<p class="text-sm text-gray-600 mt-1">
|
|
{{ getWettbewerbText() }} - {{ filteredData.length }} von {{ spielplanData.length }} Einträgen
|
|
</p>
|
|
</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 in headers"
|
|
:key="header"
|
|
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
|
>
|
|
{{ formatHeader(header) }}
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white divide-y divide-gray-200">
|
|
<tr
|
|
v-for="(row, index) in filteredData"
|
|
:key="index"
|
|
:class="getRowClass(row)"
|
|
>
|
|
<td
|
|
v-for="header in headers"
|
|
:key="header"
|
|
class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"
|
|
>
|
|
<span
|
|
v-if="header.toLowerCase().includes('datum')"
|
|
class="font-mono"
|
|
>
|
|
{{ formatDate(row[getOriginalHeader(header)]) }}
|
|
</span>
|
|
<span
|
|
v-else-if="header.toLowerCase().includes('uhrzeit')"
|
|
class="font-mono"
|
|
>
|
|
{{ formatTime(row[getOriginalHeader(header)]) }}
|
|
</span>
|
|
<span
|
|
v-else-if="header.toLowerCase().includes('mannschaft')"
|
|
class="font-medium"
|
|
>
|
|
{{ row[getOriginalHeader(header)] || '-' }}
|
|
</span>
|
|
<span v-else-if="header.toLowerCase().includes('runde')">
|
|
{{ formatRunde(row[getOriginalHeader(header)]) }}
|
|
</span>
|
|
<span v-else>
|
|
{{ row[getOriginalHeader(header)] || '-' }}
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="px-6 py-4 bg-gray-50 border-t border-gray-200">
|
|
<p class="text-xs text-gray-500">
|
|
Letzte Aktualisierung: {{ lastUpdated }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted } from 'vue'
|
|
|
|
useHead({
|
|
title: 'Spielpläne - Mannschaften - Harheimer TC'
|
|
})
|
|
|
|
const spielplanData = ref([])
|
|
const headers = ref([])
|
|
const isLoading = ref(false)
|
|
const error = ref(null)
|
|
const lastUpdated = ref('')
|
|
const selectedFilter = ref('all')
|
|
const selectedWettbewerb = ref('punktrunde')
|
|
const filteredData = ref([])
|
|
const mannschaften = ref([])
|
|
|
|
async function fetchCsvText(url) {
|
|
const attempt = async () => {
|
|
const withBuster = `${url}${url.includes('?') ? '&' : '?'}_t=${Date.now()}`
|
|
const res = await fetch(withBuster, { cache: 'no-store' })
|
|
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`)
|
|
return await res.text()
|
|
}
|
|
|
|
try {
|
|
return await attempt()
|
|
} catch (_e) {
|
|
// 1 Retry: hilft bei kurzen Restarts/Proxy-Resets (Firefox: NS_ERROR_NET_PARTIAL_TRANSFER)
|
|
await new Promise(resolve => setTimeout(resolve, 150))
|
|
return await attempt()
|
|
}
|
|
}
|
|
|
|
const loadData = async () => {
|
|
isLoading.value = true
|
|
error.value = null
|
|
|
|
try {
|
|
// Lade Spielplandaten und Mannschaften parallel
|
|
const [spielplanResponse, mannschaftenResponse] = await Promise.all([
|
|
fetch('/api/spielplan'),
|
|
fetchCsvText('/data/mannschaften.csv')
|
|
])
|
|
|
|
// Spielplandaten verarbeiten
|
|
const spielplanResult = await spielplanResponse.json()
|
|
if (spielplanResult.success) {
|
|
spielplanData.value = spielplanResult.data
|
|
|
|
// Nur die gewünschten Spalten anzeigen
|
|
const originalHeaders = spielplanResult.headers
|
|
const desiredHeaders = ['Termin', 'HeimMannschaft', 'GastMannschaft', 'Runde', 'Staffel']
|
|
|
|
// Finde die Indizes der gewünschten Spalten
|
|
const headerIndices = desiredHeaders.map(desiredHeader => {
|
|
return originalHeaders.findIndex(header =>
|
|
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)
|
|
headers.value = filteredHeaders.map(header => {
|
|
if (header.toLowerCase().includes('staffel')) {
|
|
return 'Gruppe'
|
|
}
|
|
return header
|
|
})
|
|
|
|
// Erstelle Mapping für Daten-Zugriff
|
|
const headerMapping = {}
|
|
headers.value.forEach((displayHeader, index) => {
|
|
headerMapping[displayHeader] = filteredHeaders[index]
|
|
})
|
|
|
|
// Speichere Mapping global für Zugriff in Template
|
|
window.spielplanHeaderMapping = headerMapping
|
|
|
|
lastUpdated.value = new Date().toLocaleString('de-DE')
|
|
} 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)
|
|
error.value = 'Fehler beim Laden der Daten'
|
|
} finally {
|
|
isLoading.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
|
|
}
|
|
})
|
|
|
|
// 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
|
|
})
|
|
} else if (selectedWettbewerb.value === 'pokal') {
|
|
wettbewerbFiltered = saisonFiltered.filter(row => {
|
|
// Filtere nach Pokal-Spielen
|
|
const runde = (row.Runde || '').toLowerCase()
|
|
return runde === 'pokal' || runde.includes('pokal')
|
|
})
|
|
}
|
|
// "alle" zeigt alle Spiele ohne weitere Filterung
|
|
|
|
// Dann nach Mannschaft filtern
|
|
if (selectedFilter.value === 'all') {
|
|
filteredData.value = wettbewerbFiltered
|
|
} else if (selectedFilter.value === 'erwachsene') {
|
|
filteredData.value = wettbewerbFiltered.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
|
|
}
|
|
|
|
// Filtere nach Erwachsenen-Mannschaften (NICHT Jugend)
|
|
const isErwachsenenHeim = isHarheimerHeim &&
|
|
heimAltersklasse.includes('erwachsene') &&
|
|
!heimAltersklasse.includes('jugend')
|
|
const isErwachsenenGast = isHarheimerGast &&
|
|
gastAltersklasse.includes('erwachsene') &&
|
|
!gastAltersklasse.includes('jugend')
|
|
|
|
return isErwachsenenHeim || isErwachsenenGast
|
|
})
|
|
} else if (selectedFilter.value === 'nachwuchs') {
|
|
filteredData.value = wettbewerbFiltered.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
|
|
}
|
|
|
|
// Filtere nach Jugend-Mannschaften (NUR Jugend)
|
|
const isJugendHeim = isHarheimerHeim &&
|
|
(heimAltersklasse.includes('jugend') || heimMannschaft.includes('jugend'))
|
|
const isJugendGast = isHarheimerGast &&
|
|
(gastAltersklasse.includes('jugend') || gastMannschaft.includes('jugend'))
|
|
|
|
return isJugendHeim || isJugendGast
|
|
})
|
|
} else {
|
|
// Spezifische Mannschaft - Mapping zwischen CMS-Mannschaften und CSV-Daten
|
|
filteredData.value = wettbewerbFiltered.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
|
|
}
|
|
|
|
const cmsMannschaft = selectedFilter.value
|
|
|
|
// Mapping zwischen CMS-Mannschaften und CSV-Daten
|
|
const mannschaftMapping = {
|
|
'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[cmsMannschaft] || []
|
|
|
|
// 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 (cmsMannschaft.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 (cmsMannschaft === 'Jugendmannschaft') {
|
|
// 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
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
const downloadPDF = () => {
|
|
if (!filteredData.value || filteredData.value.length === 0) return
|
|
|
|
// Bestimme den Team-Parameter basierend auf dem Filter
|
|
let teamParam = ''
|
|
|
|
if (selectedFilter.value === 'all') {
|
|
teamParam = 'all'
|
|
} else if (selectedFilter.value === 'erwachsene') {
|
|
teamParam = 'erwachsene'
|
|
} else if (selectedFilter.value === 'nachwuchs') {
|
|
teamParam = 'nachwuchs'
|
|
} else {
|
|
// Für einzelne Mannschaften: Konvertiere Namen
|
|
teamParam = selectedFilter.value.replace(/\s+/g, '_').toLowerCase()
|
|
}
|
|
|
|
// Erstelle Download-URL für dynamische PDF-Generierung
|
|
const params = new URLSearchParams({
|
|
team: teamParam,
|
|
wettbewerb: selectedWettbewerb.value
|
|
})
|
|
const downloadUrl = `/api/spielplan/pdf?${params.toString()}`
|
|
|
|
// Öffne Download in neuem Tab
|
|
window.open(downloadUrl, '_blank')
|
|
}
|
|
|
|
const formatHeader = (header) => {
|
|
const headerMap = {
|
|
'Datum': 'Datum',
|
|
'Mannschaft': 'Mannschaft',
|
|
'Gegner': 'Gegner',
|
|
'Ort': 'Ort',
|
|
'Uhrzeit': 'Uhrzeit',
|
|
'Runde': 'Runde',
|
|
'Gruppe': 'Gruppe',
|
|
'Ergebnis': 'Ergebnis',
|
|
'Bemerkung': 'Bemerkung',
|
|
'Status': 'Status'
|
|
}
|
|
|
|
return headerMap[header] || header
|
|
}
|
|
|
|
const formatDate = (dateString) => {
|
|
if (!dateString) return '-'
|
|
|
|
try {
|
|
const date = new Date(dateString)
|
|
if (isNaN(date.getTime())) {
|
|
return dateString
|
|
}
|
|
|
|
return date.toLocaleDateString('de-DE', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric'
|
|
})
|
|
} catch {
|
|
return dateString
|
|
}
|
|
}
|
|
|
|
const formatTime = (timeString) => {
|
|
if (!timeString) return '-'
|
|
|
|
if (timeString.match(/^\d{1,2}:\d{2}$/)) {
|
|
return timeString
|
|
}
|
|
|
|
return timeString
|
|
}
|
|
|
|
const formatRunde = (rundeString) => {
|
|
if (!rundeString) return '-'
|
|
|
|
const runde = rundeString.toLowerCase()
|
|
|
|
// Ersetze "VR" durch "Vorrunde" und "RR" durch "Rückrunde"
|
|
if (runde === 'vr') {
|
|
return 'Vorrunde'
|
|
} else if (runde === 'rr') {
|
|
return 'Rückrunde'
|
|
} else if (runde === 'pokal') {
|
|
return 'Pokal'
|
|
}
|
|
|
|
return rundeString
|
|
}
|
|
|
|
const getOriginalHeader = (displayHeader) => {
|
|
// Verwende das Mapping um den ursprünglichen Header-Namen zu finden
|
|
if (window.spielplanHeaderMapping && window.spielplanHeaderMapping[displayHeader]) {
|
|
return window.spielplanHeaderMapping[displayHeader]
|
|
}
|
|
return displayHeader
|
|
}
|
|
|
|
const getRowClass = (row) => {
|
|
const termin = row[getOriginalHeader('Termin')] || row['Termin']
|
|
if (!termin) return 'bg-white'
|
|
|
|
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 {
|
|
// Fallback für andere Formate
|
|
spielDatum = new Date(termin)
|
|
}
|
|
|
|
if (isNaN(spielDatum.getTime())) return 'bg-white'
|
|
|
|
const heute = new Date()
|
|
heute.setHours(0, 0, 0, 0)
|
|
|
|
const in7Tagen = new Date(heute)
|
|
in7Tagen.setDate(in7Tagen.getDate() + 7)
|
|
|
|
spielDatum.setHours(0, 0, 0, 0)
|
|
|
|
// Heute: Hellgelb
|
|
if (spielDatum.getTime() === heute.getTime()) {
|
|
return 'bg-yellow-100'
|
|
}
|
|
|
|
// Nächste 7 Tage: Hellblau
|
|
if (spielDatum > heute && spielDatum <= in7Tagen) {
|
|
return 'bg-blue-100'
|
|
}
|
|
|
|
// Standard: Weiß
|
|
return 'bg-white'
|
|
} catch {
|
|
return 'bg-white'
|
|
}
|
|
}
|
|
|
|
const getWettbewerbText = () => {
|
|
switch (selectedWettbewerb.value) {
|
|
case 'punktrunde':
|
|
return 'Punktrunde (Vorrunde + Rückrunde)'
|
|
case 'pokal':
|
|
return 'Pokal'
|
|
case 'alle':
|
|
return 'Alle Wettbewerbe'
|
|
default:
|
|
return 'Punktrunde (Vorrunde + Rückrunde)'
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
loadData()
|
|
})
|
|
</script> |