Add 'Spielplan' links to Navigation component; update index page to include 'Spielplan' section; enhance 'spielplaene' page with filtering, loading states, and error handling for improved user experience.
This commit is contained in:
@@ -57,6 +57,12 @@
|
|||||||
Termine
|
Termine
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
|
||||||
|
<NuxtLink to="/spielplan" @click="currentSubmenu = null"
|
||||||
|
class="px-4 py-2 text-gray-300 hover:text-white font-medium transition-all rounded-lg hover:bg-primary-700/50"
|
||||||
|
active-class="text-white bg-primary-600">
|
||||||
|
Spielplan
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-if="hasGalleryImages"
|
v-if="hasGalleryImages"
|
||||||
to="/galerie"
|
to="/galerie"
|
||||||
@@ -246,6 +252,11 @@
|
|||||||
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors">
|
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors">
|
||||||
Termine
|
Termine
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
<NuxtLink to="/cms/spielplaene"
|
||||||
|
@click="showCmsDropdown = false"
|
||||||
|
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors">
|
||||||
|
Spielpläne
|
||||||
|
</NuxtLink>
|
||||||
<NuxtLink to="/mitgliederbereich/mitglieder"
|
<NuxtLink to="/mitgliederbereich/mitglieder"
|
||||||
@click="showCmsDropdown = false"
|
@click="showCmsDropdown = false"
|
||||||
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors">
|
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors">
|
||||||
@@ -403,6 +414,11 @@
|
|||||||
Termine
|
Termine
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
|
||||||
|
<NuxtLink to="/spielplan" @click="isMobileMenuOpen = false"
|
||||||
|
class="block px-4 py-3 text-gray-300 hover:text-white hover:bg-primary-700/50 rounded-lg font-medium transition-colors">
|
||||||
|
Spielplan
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-if="hasGalleryImages"
|
v-if="hasGalleryImages"
|
||||||
to="/galerie"
|
to="/galerie"
|
||||||
@@ -450,6 +466,10 @@
|
|||||||
class="block px-4 py-2 text-sm text-yellow-300 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors">
|
class="block px-4 py-2 text-sm text-yellow-300 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors">
|
||||||
Termine
|
Termine
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
<NuxtLink to="/cms/spielplaene" @click="isMobileMenuOpen = false"
|
||||||
|
class="block px-4 py-2 text-sm text-yellow-300 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors">
|
||||||
|
Spielpläne
|
||||||
|
</NuxtLink>
|
||||||
<NuxtLink to="/mitgliederbereich/mitglieder" @click="isMobileMenuOpen = false"
|
<NuxtLink to="/mitgliederbereich/mitglieder" @click="isMobileMenuOpen = false"
|
||||||
class="block px-4 py-2 text-sm text-yellow-300 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors">
|
class="block px-4 py-2 text-sm text-yellow-300 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors">
|
||||||
Mitglieder
|
Mitglieder
|
||||||
|
|||||||
218
components/Spielplan.vue
Normal file
218
components/Spielplan.vue
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
<template>
|
||||||
|
<section class="py-16 bg-white">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="text-center mb-12">
|
||||||
|
<h2 class="text-3xl font-bold text-gray-900 mb-4">Nächste Spiele</h2>
|
||||||
|
<p class="text-lg text-gray-600">Aktuelle Termine und Spiele des Vereins</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading State -->
|
||||||
|
<div v-if="isLoading" class="text-center py-8">
|
||||||
|
<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">Spielplan wird geladen...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error State -->
|
||||||
|
<div v-else-if="error" class="text-center py-8">
|
||||||
|
<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>
|
||||||
|
<p class="text-gray-600 mb-4">{{ error }}</p>
|
||||||
|
<NuxtLink to="/spielplan" class="inline-flex items-center px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors">
|
||||||
|
Zum Spielplan
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div v-else-if="!upcomingGames || upcomingGames.length === 0" class="text-center py-8">
|
||||||
|
<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 kommenden Spiele</h3>
|
||||||
|
<p class="text-gray-600 mb-4">Derzeit sind keine Spiele geplant.</p>
|
||||||
|
<NuxtLink to="/spielplan" class="inline-flex items-center px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors">
|
||||||
|
Zum Spielplan
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Games Grid -->
|
||||||
|
<div v-else class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
<div v-for="game in upcomingGames" :key="`${game.Datum}-${game.Mannschaft}`"
|
||||||
|
class="bg-white rounded-xl shadow-lg border border-gray-200 hover:shadow-xl transition-shadow">
|
||||||
|
<div class="p-6">
|
||||||
|
<!-- Date -->
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<svg class="w-5 h-5 text-primary-600 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
<span class="text-sm font-medium text-gray-900">{{ formatDate(game.Datum) }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Teams -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex-1">
|
||||||
|
<p class="text-sm text-gray-600 mb-1">Mannschaft</p>
|
||||||
|
<p class="font-semibold text-gray-900">{{ game.Mannschaft || 'Nicht angegeben' }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-center mx-4">
|
||||||
|
<div class="w-8 h-8 bg-primary-100 rounded-full flex items-center justify-center">
|
||||||
|
<span class="text-primary-600 font-bold text-sm">vs</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 text-right">
|
||||||
|
<p class="text-sm text-gray-600 mb-1">Gegner</p>
|
||||||
|
<p class="font-semibold text-gray-900">{{ game.Gegner || 'Nicht angegeben' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Details -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div v-if="game.Ort" class="flex items-center text-sm text-gray-600">
|
||||||
|
<svg class="w-4 h-4 text-gray-400 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
</svg>
|
||||||
|
{{ game.Ort }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="game.Uhrzeit" class="flex items-center text-sm text-gray-600">
|
||||||
|
<svg class="w-4 h-4 text-gray-400 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
{{ formatTime(game.Uhrzeit) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="game.Status" class="flex items-center text-sm">
|
||||||
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
|
||||||
|
:class="getStatusClass(game.Status)">
|
||||||
|
{{ game.Status }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Result (if available) -->
|
||||||
|
<div v-if="game.Ergebnis" class="mt-4 pt-4 border-t border-gray-200">
|
||||||
|
<div class="text-center">
|
||||||
|
<p class="text-sm text-gray-600 mb-1">Ergebnis</p>
|
||||||
|
<p class="text-lg font-bold text-primary-600">{{ game.Ergebnis }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- View All Button -->
|
||||||
|
<div v-if="upcomingGames && upcomingGames.length > 0" class="text-center mt-8">
|
||||||
|
<NuxtLink to="/spielplan"
|
||||||
|
class="inline-flex items-center px-6 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors">
|
||||||
|
Alle Spiele anzeigen
|
||||||
|
<svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, computed } from 'vue'
|
||||||
|
|
||||||
|
const spielplanData = ref([])
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const error = ref(null)
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
isLoading.value = true
|
||||||
|
error.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/spielplan')
|
||||||
|
const result = await response.json()
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
spielplanData.value = result.data
|
||||||
|
} else {
|
||||||
|
error.value = result.message
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Fehler beim Laden des Spielplans:', err)
|
||||||
|
error.value = 'Fehler beim Laden der Daten'
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const upcomingGames = computed(() => {
|
||||||
|
if (!spielplanData.value || spielplanData.value.length === 0) return []
|
||||||
|
|
||||||
|
const today = new Date()
|
||||||
|
today.setHours(0, 0, 0, 0)
|
||||||
|
|
||||||
|
return spielplanData.value
|
||||||
|
.filter(game => {
|
||||||
|
if (!game.Datum) return false
|
||||||
|
|
||||||
|
const gameDate = new Date(game.Datum)
|
||||||
|
if (isNaN(gameDate.getTime())) return false
|
||||||
|
|
||||||
|
return gameDate >= today
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
const dateA = new Date(a.Datum)
|
||||||
|
const dateB = new Date(b.Datum)
|
||||||
|
return dateA - dateB
|
||||||
|
})
|
||||||
|
.slice(0, 6) // Zeige nur die nächsten 6 Spiele
|
||||||
|
})
|
||||||
|
|
||||||
|
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 '-'
|
||||||
|
|
||||||
|
// Einfache Zeitformatierung (HH:MM)
|
||||||
|
if (timeString.match(/^\d{1,2}:\d{2}$/)) {
|
||||||
|
return timeString
|
||||||
|
}
|
||||||
|
|
||||||
|
return timeString
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusClass = (status) => {
|
||||||
|
const statusMap = {
|
||||||
|
'Geplant': 'bg-blue-100 text-blue-800',
|
||||||
|
'Abgesagt': 'bg-red-100 text-red-800',
|
||||||
|
'Verschoben': 'bg-yellow-100 text-yellow-800',
|
||||||
|
'Beendet': 'bg-green-100 text-green-800',
|
||||||
|
'Läuft': 'bg-purple-100 text-purple-800'
|
||||||
|
}
|
||||||
|
|
||||||
|
return statusMap[status] || 'bg-gray-100 text-gray-800'
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
640
pages/cms/spielplaene.vue
Normal file
640
pages/cms/spielplaene.vue
Normal file
@@ -0,0 +1,640 @@
|
|||||||
|
<template>
|
||||||
|
<div class="min-h-full bg-gray-50">
|
||||||
|
<!-- Fixed Header below navigation -->
|
||||||
|
<div class="fixed top-20 left-0 right-0 z-40 bg-white border-b border-gray-200 shadow-sm">
|
||||||
|
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-3 sm:py-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h1 class="text-xl sm:text-3xl font-display font-bold text-gray-900">Spielpläne bearbeiten</h1>
|
||||||
|
<div class="space-x-3">
|
||||||
|
<button @click="showUploadModal = true" 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">
|
||||||
|
<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"></path>
|
||||||
|
</svg>
|
||||||
|
CSV hochladen
|
||||||
|
</button>
|
||||||
|
<button @click="save" 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">Speichern</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content with top padding -->
|
||||||
|
<div class="pt-20 pb-16">
|
||||||
|
<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 -->
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- Current File Info -->
|
||||||
|
<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"></path>
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-green-800">{{ currentFile.name }}</p>
|
||||||
|
<p class="text-xs text-green-600">{{ currentFile.size }} bytes, {{ currentFile.lastModified ? new Date(currentFile.lastModified).toLocaleDateString('de-DE') : 'Unbekannt' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button @click="removeFile" class="px-3 py-1 text-sm bg-red-100 hover:bg-red-200 text-red-700 rounded-lg transition-colors">
|
||||||
|
Entfernen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Upload Area -->
|
||||||
|
<div
|
||||||
|
@click="triggerFileInput"
|
||||||
|
@dragover.prevent
|
||||||
|
@dragenter.prevent
|
||||||
|
@drop.prevent="handleFileDrop"
|
||||||
|
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 }"
|
||||||
|
>
|
||||||
|
<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"></path>
|
||||||
|
</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>
|
||||||
|
<p class="text-xs text-gray-500">Unterstützte Formate: .csv</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
ref="fileInput"
|
||||||
|
type="file"
|
||||||
|
accept=".csv"
|
||||||
|
@change="handleFileSelect"
|
||||||
|
class="hidden"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Column Selection -->
|
||||||
|
<div v-if="csvData.length > 0 && !columnsSelected" class="bg-white rounded-xl shadow-lg p-6 mb-8">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 mb-4">Spalten auswählen</h2>
|
||||||
|
<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 @click="selectAllColumns" class="px-4 py-2 text-sm bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors">
|
||||||
|
Alle auswählen
|
||||||
|
</button>
|
||||||
|
<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
|
||||||
|
</button>
|
||||||
|
<button @click="confirmColumnSelection"
|
||||||
|
: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">
|
||||||
|
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">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900">Datenvorschau</h2>
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<button @click="exportCSV" class="px-3 py-1 text-sm bg-blue-100 hover:bg-blue-200 text-blue-700 rounded-lg transition-colors">
|
||||||
|
CSV exportieren
|
||||||
|
</button>
|
||||||
|
<button @click="clearData" class="px-3 py-1 text-sm bg-red-100 hover:bg-red-200 text-red-700 rounded-lg transition-colors">
|
||||||
|
Daten löschen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Data Table -->
|
||||||
|
<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-else 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"></path>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</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"
|
||||||
|
@change="handleModalFileSelect"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||||||
|
/>
|
||||||
|
</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>• Spalten: Datum, Mannschaft, Gegner, Ort, Uhrzeit, etc.</p>
|
||||||
|
<p>• Trennzeichen: Komma (,)</p>
|
||||||
|
<p>• Text in Anführungszeichen bei Sonderzeichen</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end space-x-3 pt-4">
|
||||||
|
<button
|
||||||
|
@click="closeUploadModal"
|
||||||
|
class="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="processSelectedFile"
|
||||||
|
:disabled="!selectedFile"
|
||||||
|
class="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors disabled:bg-gray-400"
|
||||||
|
>
|
||||||
|
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"></div>
|
||||||
|
<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'
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: 'auth',
|
||||||
|
})
|
||||||
|
|
||||||
|
useHead({ title: 'CMS: Spielpläne' })
|
||||||
|
|
||||||
|
const fileInput = ref(null)
|
||||||
|
const modalFileInput = ref(null)
|
||||||
|
const gesamtPdfInput = ref(null)
|
||||||
|
const erwachsenePdfInput = ref(null)
|
||||||
|
const nachwuchsPdfInput = 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 uploadedPdfs = ref({
|
||||||
|
gesamt: null,
|
||||||
|
erwachsene: null,
|
||||||
|
nachwuchs: null
|
||||||
|
})
|
||||||
|
|
||||||
|
const triggerFileInput = () => {
|
||||||
|
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 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')
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSV-Parser: Semikolon-getrennt, ohne Anführungszeichen
|
||||||
|
const parseCSVLine = (line) => {
|
||||||
|
return line.split(';').map(value => value.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header-Zeile parsen
|
||||||
|
csvHeaders.value = parseCSVLine(lines[0])
|
||||||
|
|
||||||
|
// Datenzeilen parsen
|
||||||
|
csvData.value = lines.slice(1).map(line => parseCSVLine(line))
|
||||||
|
|
||||||
|
// Spaltenauswahl initialisieren (alle ausgewählt)
|
||||||
|
selectedColumns.value = new Array(csvHeaders.value.length).fill(true)
|
||||||
|
columnsSelected.value = false
|
||||||
|
|
||||||
|
// Datei-Info speichern
|
||||||
|
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 beim Verarbeiten der CSV-Datei:', error)
|
||||||
|
alert('Fehler beim Verarbeiten der CSV-Datei: ' + 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 = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Computed properties for column selection
|
||||||
|
const selectedColumnsCount = computed(() => {
|
||||||
|
return selectedColumns.value.filter(selected => selected).length
|
||||||
|
})
|
||||||
|
|
||||||
|
const getColumnPreview = (index) => {
|
||||||
|
if (csvData.value.length === 0) return 'Keine Daten'
|
||||||
|
const sampleValues = csvData.value.slice(0, 3).map(row => row[index]).filter(val => val && val.trim() !== '')
|
||||||
|
return sampleValues.length > 0 ? `Beispiel: ${sampleValues.join(', ')}` : 'Leere Spalte'
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectAllColumns = () => {
|
||||||
|
selectedColumns.value = selectedColumns.value.map(() => true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deselectAllColumns = () => {
|
||||||
|
selectedColumns.value = selectedColumns.value.map(() => false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmColumnSelection = () => {
|
||||||
|
// Filtere Daten basierend auf ausgewählten Spalten
|
||||||
|
const selectedIndices = selectedColumns.value.map((selected, index) => selected ? index : -1).filter(index => index !== -1)
|
||||||
|
|
||||||
|
filteredCsvHeaders.value = selectedIndices.map(index => csvHeaders.value[index])
|
||||||
|
filteredCsvData.value = csvData.value.map(row => selectedIndices.map(index => row[index]))
|
||||||
|
|
||||||
|
columnsSelected.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearData = () => {
|
||||||
|
if (confirm('Möchten Sie alle Daten wirklich löschen?')) {
|
||||||
|
removeFile()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportCSV = () => {
|
||||||
|
const dataToExport = columnsSelected.value ? filteredCsvData.value : csvData.value
|
||||||
|
const headersToExport = columnsSelected.value ? filteredCsvHeaders.value : csvHeaders.value
|
||||||
|
|
||||||
|
if (dataToExport.length === 0) return
|
||||||
|
|
||||||
|
// CSV generieren (Semikolon-getrennt, ohne Anführungszeichen)
|
||||||
|
const csvContent = [
|
||||||
|
headersToExport.join(';'),
|
||||||
|
...dataToExport.map(row => row.join(';'))
|
||||||
|
].join('\n')
|
||||||
|
|
||||||
|
// Download
|
||||||
|
const blob = new Blob([csvContent], { 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 dataToSave = columnsSelected.value ? filteredCsvData.value : csvData.value
|
||||||
|
const headersToSave = columnsSelected.value ? filteredCsvHeaders.value : csvHeaders.value
|
||||||
|
|
||||||
|
if (dataToSave.length === 0) {
|
||||||
|
alert('Keine Daten zum Speichern vorhanden.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// CSV generieren (Semikolon-getrennt, ohne Anführungszeichen)
|
||||||
|
const csvContent = [
|
||||||
|
headersToSave.join(';'),
|
||||||
|
...dataToSave.map(row => row.join(';'))
|
||||||
|
].join('\n')
|
||||||
|
|
||||||
|
// CSV speichern
|
||||||
|
const response = await fetch('/api/cms/save-csv', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
filename: 'spielplan.csv',
|
||||||
|
content: csvContent
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
alert('Spielplan erfolgreich gespeichert!')
|
||||||
|
} else {
|
||||||
|
alert('Fehler beim Speichern des Spielplans!')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Speichern:', error)
|
||||||
|
alert('Fehler beim Speichern des Spielplans!')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeUploadModal = () => {
|
||||||
|
showUploadModal.value = false
|
||||||
|
selectedFile.value = null
|
||||||
|
if (modalFileInput.value) {
|
||||||
|
modalFileInput.value.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drag and Drop Events
|
||||||
|
const handleDragEnter = () => {
|
||||||
|
isDragOver.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDragLeave = () => {
|
||||||
|
isDragOver.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// Load existing data if available
|
||||||
|
loadExistingData()
|
||||||
|
})
|
||||||
|
|
||||||
|
const loadExistingData = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/data/spielplan.csv')
|
||||||
|
if (response.ok) {
|
||||||
|
const text = await response.text()
|
||||||
|
const lines = text.split('\n').filter(line => line.trim() !== '')
|
||||||
|
|
||||||
|
if (lines.length >= 2) {
|
||||||
|
// Parse existing CSV
|
||||||
|
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 (error) {
|
||||||
|
console.log('Keine bestehende Spielplan-Datei gefunden')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -6,10 +6,13 @@
|
|||||||
<!-- 2. Kommende Termine -->
|
<!-- 2. Kommende Termine -->
|
||||||
<HomeTermine />
|
<HomeTermine />
|
||||||
|
|
||||||
<!-- 3. Aktuelles -->
|
<!-- 3. Nächste Spiele -->
|
||||||
|
<Spielplan />
|
||||||
|
|
||||||
|
<!-- 4. Aktuelles -->
|
||||||
<PublicNews />
|
<PublicNews />
|
||||||
|
|
||||||
<!-- 4. Mitglied werden / Kontakt aufnehmen -->
|
<!-- 5. Mitglied werden / Kontakt aufnehmen -->
|
||||||
<HomeActions />
|
<HomeActions />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -17,6 +20,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import Hero from '~/components/Hero.vue'
|
import Hero from '~/components/Hero.vue'
|
||||||
import HomeTermine from '~/components/HomeTermine.vue'
|
import HomeTermine from '~/components/HomeTermine.vue'
|
||||||
|
import Spielplan from '~/components/Spielplan.vue'
|
||||||
import PublicNews from '~/components/PublicNews.vue'
|
import PublicNews from '~/components/PublicNews.vue'
|
||||||
import HomeActions from '~/components/HomeActions.vue'
|
import HomeActions from '~/components/HomeActions.vue'
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,228 +1,248 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-full py-16 bg-gray-50">
|
<div class="min-h-screen bg-gray-50">
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<!-- Header -->
|
||||||
<div class="text-center mb-12">
|
<div class="bg-white shadow-sm">
|
||||||
<h1 class="text-4xl sm:text-5xl font-display font-bold text-gray-900 mb-4">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||||
Spielpläne
|
|
||||||
</h1>
|
|
||||||
<div class="w-24 h-1 bg-primary-600 mx-auto mb-6" />
|
|
||||||
<p class="text-xl text-gray-600 max-w-3xl mx-auto">
|
|
||||||
Aktuelle Spielpläne der Saison {{ aktuellesSaisonLabel }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Spielpläne -->
|
|
||||||
<div v-if="spielplaene.length > 0" class="space-y-4 max-w-4xl mx-auto">
|
|
||||||
<div
|
|
||||||
v-for="(plan, index) in spielplaene"
|
|
||||||
:key="index"
|
|
||||||
class="bg-white rounded-xl shadow-lg border border-gray-100 p-6 hover:shadow-xl transition-shadow"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="flex items-center space-x-4">
|
|
||||||
<div class="w-12 h-12 bg-primary-100 rounded-lg flex items-center justify-center">
|
|
||||||
<FileText :size="24" class="text-primary-600" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="text-lg font-semibold text-gray-900">{{ plan.titel }}</h3>
|
|
||||||
<p class="text-sm text-gray-500">Saison {{ plan.saison }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a
|
|
||||||
:href="plan.url"
|
|
||||||
download
|
|
||||||
class="inline-flex items-center px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white font-medium rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
<Download :size="18" class="mr-2" />
|
|
||||||
Download
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Keine Spielpläne -->
|
|
||||||
<div v-else class="text-center py-16 bg-white rounded-xl shadow-lg max-w-4xl mx-auto">
|
|
||||||
<FileText :size="48" class="text-gray-400 mx-auto mb-4" />
|
|
||||||
<h3 class="text-xl font-semibold text-gray-900 mb-2">Keine Spielpläne verfügbar</h3>
|
|
||||||
<p class="text-gray-600">
|
|
||||||
Für die aktuelle Saison {{ aktuellesSaisonLabel }} sind noch keine Spielpläne verfügbar.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Online Spielpläne und Tabellen -->
|
|
||||||
<div class="mt-12 max-w-4xl mx-auto">
|
|
||||||
<h2 class="text-2xl font-display font-bold text-gray-900 mb-6 text-center">
|
|
||||||
Online Spielpläne & Tabellen
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div v-if="mannschaftenMitLinks.length > 0" class="space-y-3">
|
|
||||||
<div
|
|
||||||
v-for="(mannschaft, index) in mannschaftenMitLinks"
|
|
||||||
:key="index"
|
|
||||||
class="bg-white rounded-lg shadow border border-gray-100 p-4 hover:shadow-md transition-shadow"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-semibold text-gray-900">{{ mannschaft.mannschaft }}</h3>
|
<h1 class="text-3xl font-bold text-gray-900">Spielpläne</h1>
|
||||||
<p class="text-sm text-gray-500">{{ mannschaft.liga }}</p>
|
<p class="mt-2 text-gray-600">Alle Spielpläne der Mannschaften</p>
|
||||||
</div>
|
|
||||||
<a
|
|
||||||
:href="mannschaft.weitere_informationen_link"
|
|
||||||
target="_blank"
|
|
||||||
class="inline-flex items-center px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors text-sm"
|
|
||||||
>
|
|
||||||
<ExternalLink :size="16" class="mr-2" />
|
|
||||||
Online ansehen
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Info-Box -->
|
<!-- Content -->
|
||||||
<div class="mt-12 max-w-4xl mx-auto bg-primary-50 border border-primary-100 rounded-xl p-6">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
<h3 class="text-lg font-semibold text-primary-900 mb-2">
|
<!-- Filter and Download Section -->
|
||||||
Hinweis
|
<div class="bg-white rounded-xl shadow-lg p-6 mb-8">
|
||||||
</h3>
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
<p class="text-primary-800">
|
<!-- Filter Selection -->
|
||||||
Die Spielpläne werden automatisch für die aktuelle Saison angezeigt.
|
<div class="flex flex-col sm:flex-row sm:items-center gap-4">
|
||||||
Ältere Spielpläne können auf Anfrage bereitgestellt werden.
|
<!-- 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"
|
||||||
|
@change="filterData"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<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"
|
||||||
|
@change="filterData"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<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
|
||||||
|
@click="downloadPDF"
|
||||||
|
: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"
|
||||||
|
>
|
||||||
|
<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 @click="loadData" class="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors">
|
||||||
|
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>
|
</p>
|
||||||
</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 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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import { FileText, Download, ExternalLink } from 'lucide-vue-next'
|
|
||||||
|
|
||||||
const spielplaene = ref([])
|
useHead({
|
||||||
const mannschaftenMitLinks = ref([])
|
title: 'Spielpläne - Mannschaften - Harheimer TC'
|
||||||
|
|
||||||
// Berechne die aktuelle Saison
|
|
||||||
const aktuellesSaison = computed(() => {
|
|
||||||
const jetzt = new Date()
|
|
||||||
const monat = jetzt.getMonth() + 1 // 1-12
|
|
||||||
const jahr = jetzt.getFullYear()
|
|
||||||
|
|
||||||
// Saison wechselt im Juli/August
|
|
||||||
if (monat >= 7) {
|
|
||||||
return { start: jahr, ende: jahr + 1 }
|
|
||||||
} else {
|
|
||||||
return { start: jahr - 1, ende: jahr }
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const aktuellesSaisonLabel = computed(() => {
|
const spielplanData = ref([])
|
||||||
return `${aktuellesSaison.value.start}/${aktuellesSaison.value.ende}`
|
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([])
|
||||||
|
|
||||||
// Funktion zum Extrahieren der Saison aus dem Dateinamen
|
const loadData = async () => {
|
||||||
const extractSaison = (filename) => {
|
isLoading.value = true
|
||||||
console.log('extractSaison für:', filename)
|
error.value = null
|
||||||
|
|
||||||
// Normalisiere alle möglichen Trennzeichen zu einem einzigen Zeichen
|
|
||||||
// Suche nach 4 Ziffern, gefolgt von irgendeinem Nicht-Ziffer-Zeichen, gefolgt von 4 Ziffern
|
|
||||||
let match = filename.match(/(\d{4})[^0-9](\d{4})/)
|
|
||||||
if (match) {
|
|
||||||
const start = parseInt(match[1])
|
|
||||||
const ende = parseInt(match[2])
|
|
||||||
console.log(' Gefunden (4-stellig):', start, ende)
|
|
||||||
return { start, ende, label: `${start}/${ende}` }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Suche nach 2 Ziffern, gefolgt von irgendeinem Nicht-Ziffer-Zeichen, gefolgt von 2 Ziffern
|
|
||||||
match = filename.match(/(\d{2})[^0-9](\d{2})/)
|
|
||||||
if (match) {
|
|
||||||
let start = parseInt(match[1])
|
|
||||||
let ende = parseInt(match[2])
|
|
||||||
|
|
||||||
// Wenn Kurzform (25-26), zu Langform konvertieren
|
|
||||||
if (start < 100) {
|
|
||||||
start = 2000 + start
|
|
||||||
ende = 2000 + ende
|
|
||||||
}
|
|
||||||
console.log(' Gefunden (2-stellig):', start, ende)
|
|
||||||
return { start, ende, label: `${start}/${ende}` }
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(' Keine Saison gefunden')
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prüfe, ob eine Saison zur aktuellen Saison passt
|
|
||||||
const istAktuellesSaison = (saison) => {
|
|
||||||
if (!saison) return false
|
|
||||||
return saison.start === aktuellesSaison.value.start &&
|
|
||||||
saison.ende === aktuellesSaison.value.ende
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lade Spielpläne
|
|
||||||
const loadSpielplaene = async () => {
|
|
||||||
try {
|
try {
|
||||||
console.log('=== SPIELPLÄNE LADEN ===')
|
// Lade Spielplandaten und Mannschaften parallel
|
||||||
console.log('Aktuelle Saison:', aktuellesSaison.value)
|
const [spielplanResponse, mannschaftenResponse] = await Promise.all([
|
||||||
console.log('Saison Label:', aktuellesSaisonLabel.value)
|
fetch('/api/spielplan'),
|
||||||
|
fetch('/data/mannschaften.csv')
|
||||||
|
])
|
||||||
|
|
||||||
// Lade Dateien vom Server
|
// Spielplandaten verarbeiten
|
||||||
const response = await fetch('/api/spielplaene')
|
const spielplanResult = await spielplanResponse.json()
|
||||||
if (!response.ok) {
|
if (spielplanResult.success) {
|
||||||
console.error('Fehler beim Laden der Spielpläne:', response.status)
|
spielplanData.value = spielplanResult.data
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const dateien = await response.json()
|
// Filtere unerwünschte Spalten heraus und benenne um
|
||||||
console.log('Geladene Dateien:', dateien)
|
const originalHeaders = spielplanResult.headers
|
||||||
|
const headersToRemove = ['saison', 'meisterschaft', 'altersklasse', 'liga']
|
||||||
|
|
||||||
const gefiltert = dateien
|
// Filtere unerwünschte Spalten heraus
|
||||||
.map(filename => {
|
const filteredHeaders = originalHeaders.filter(header => {
|
||||||
console.log('Verarbeite Datei:', filename)
|
const headerLower = header.toLowerCase()
|
||||||
const saison = extractSaison(filename)
|
return !headersToRemove.some(toRemove => headerLower.includes(toRemove))
|
||||||
console.log(' Extrahierte Saison:', saison)
|
|
||||||
console.log(' Ist aktuelle Saison?', saison ? istAktuellesSaison(saison) : false)
|
|
||||||
|
|
||||||
if (!saison || !istAktuellesSaison(saison)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extrahiere Titel aus Dateiname
|
|
||||||
const titel = filename
|
|
||||||
.replace(/\.(pdf|PDF|xlsx|XLSX|xls|XLS)$/, '')
|
|
||||||
.replace(/[-_]/g, ' ')
|
|
||||||
.replace(/\d{2,4}[-_\/⁄]\d{2,4}/, '')
|
|
||||||
.trim()
|
|
||||||
|
|
||||||
return {
|
|
||||||
filename,
|
|
||||||
titel: titel || filename,
|
|
||||||
saison: saison.label,
|
|
||||||
url: `/spielplaene/${filename}`
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.filter(item => item !== null)
|
|
||||||
|
|
||||||
spielplaene.value = gefiltert
|
// Benenne "Staffel" in "Gruppe" um (nur für Anzeige)
|
||||||
|
headers.value = filteredHeaders.map(header => {
|
||||||
console.log('Aktuelle Saison:', aktuellesSaisonLabel.value)
|
if (header.toLowerCase().includes('staffel')) {
|
||||||
console.log('Gefundene Spielpläne:', spielplaene.value)
|
return 'Gruppe'
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler beim Laden der Spielpläne:', error)
|
|
||||||
}
|
}
|
||||||
}
|
return header
|
||||||
|
})
|
||||||
|
|
||||||
// Lade Mannschaften aus CSV
|
// Erstelle Mapping für Daten-Zugriff
|
||||||
const loadMannschaften = async () => {
|
const headerMapping = {}
|
||||||
try {
|
headers.value.forEach((displayHeader, index) => {
|
||||||
const response = await fetch('/data/mannschaften.csv')
|
headerMapping[displayHeader] = filteredHeaders[index]
|
||||||
if (!response.ok) return
|
})
|
||||||
|
|
||||||
const csv = await response.text()
|
// Speichere Mapping global für Zugriff in Template
|
||||||
const lines = csv.split('\n').filter(line => line.trim() !== '')
|
window.spielplanHeaderMapping = headerMapping
|
||||||
|
|
||||||
if (lines.length < 2) return
|
lastUpdated.value = new Date().toLocaleString('de-DE')
|
||||||
|
} else {
|
||||||
|
error.value = spielplanResult.message
|
||||||
|
}
|
||||||
|
|
||||||
mannschaftenMitLinks.value = lines.slice(1).map(line => {
|
// Mannschaften aus CMS laden (manuell eingegebene Mannschaften)
|
||||||
|
if (mannschaftenResponse.ok) {
|
||||||
|
const csvText = await mannschaftenResponse.text()
|
||||||
|
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
|
// Besserer CSV-Parser: Respektiert Anführungszeichen
|
||||||
const values = []
|
const values = []
|
||||||
let current = ''
|
let current = ''
|
||||||
@@ -242,27 +262,398 @@ const loadMannschaften = async () => {
|
|||||||
}
|
}
|
||||||
values.push(current.trim())
|
values.push(current.trim())
|
||||||
|
|
||||||
if (values.length < 10) return null
|
return values[0] // Erste Spalte ist der Mannschaftsname
|
||||||
|
}).filter(name => name && name !== '')
|
||||||
return {
|
}
|
||||||
mannschaft: values[0].trim(),
|
|
||||||
liga: values[1].trim(),
|
|
||||||
weitere_informationen_link: values[8].trim()
|
|
||||||
}
|
}
|
||||||
}).filter(mannschaft => mannschaft !== null && mannschaft.weitere_informationen_link !== '')
|
|
||||||
|
|
||||||
console.log('Mannschaften mit Links:', mannschaftenMitLinks.value)
|
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)
|
||||||
|
const currentDate = new Date()
|
||||||
|
const currentYear = currentDate.getFullYear()
|
||||||
|
|
||||||
|
// 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) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Laden der Mannschaften:', 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')
|
||||||
|
|
||||||
|
// Debug: Zeige gefilterte Spiele
|
||||||
|
if (!isMatch && Math.random() < 0.1) { // 10% der gefilterten Spiele loggen
|
||||||
|
console.log('Gefiltert heraus:', row.Termin, 'Runde:', row.Runde)
|
||||||
|
}
|
||||||
|
if (isMatch && Math.random() < 0.05) { // 5% der akzeptierten Spiele loggen
|
||||||
|
console.log('Akzeptiert als Punktrunde:', row.Termin, 'Runde:', row.Runde)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
console.log('selectedWettbewerb.value:', selectedWettbewerb.value)
|
||||||
|
console.log('Nach Wettbewerb-Filter:', wettbewerbFiltered.length, 'von', saisonFiltered.length)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Finale gefilterte Daten:', filteredData.value.length, 'von', spielplanData.value.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (error) {
|
||||||
|
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(() => {
|
onMounted(() => {
|
||||||
loadSpielplaene()
|
loadData()
|
||||||
loadMannschaften()
|
|
||||||
})
|
|
||||||
|
|
||||||
useHead({
|
|
||||||
title: 'Spielpläne - Harheimer TC',
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
195
pages/spielplan.vue
Normal file
195
pages/spielplan.vue
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
<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">Spielplan</h1>
|
||||||
|
<p class="mt-2 text-gray-600">Aktuelle Termine und Spiele des Vereins</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<button @click="refreshData"
|
||||||
|
:disabled="isLoading"
|
||||||
|
class="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors disabled:bg-gray-400">
|
||||||
|
<svg v-if="isLoading" class="w-4 h-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>
|
||||||
|
<svg v-else class="w-4 h-4" 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>
|
||||||
|
Aktualisieren
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
<!-- 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">Spielplan wird 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 @click="loadData" class="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors">
|
||||||
|
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">Kein Spielplan 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">Aktuelle Spiele</h2>
|
||||||
|
<p class="text-sm text-gray-600 mt-1">{{ spielplanData.length }} Einträge</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 spielplanData" :key="index"
|
||||||
|
:class="index % 2 === 0 ? 'bg-white' : 'bg-gray-50'">
|
||||||
|
<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[header]) }}
|
||||||
|
</span>
|
||||||
|
<span v-else-if="header.toLowerCase().includes('uhrzeit')" class="font-mono">
|
||||||
|
{{ formatTime(row[header]) }}
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
{{ row[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, computed } from 'vue'
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
title: 'Spielplan - Harheimer TC'
|
||||||
|
})
|
||||||
|
|
||||||
|
const spielplanData = ref([])
|
||||||
|
const headers = ref([])
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const error = ref(null)
|
||||||
|
const lastUpdated = ref('')
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
isLoading.value = true
|
||||||
|
error.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/spielplan')
|
||||||
|
const result = await response.json()
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
spielplanData.value = result.data
|
||||||
|
headers.value = result.headers
|
||||||
|
lastUpdated.value = new Date().toLocaleString('de-DE')
|
||||||
|
} else {
|
||||||
|
error.value = result.message
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Fehler beim Laden des Spielplans:', err)
|
||||||
|
error.value = 'Fehler beim Laden der Daten'
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshData = () => {
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatHeader = (header) => {
|
||||||
|
const headerMap = {
|
||||||
|
'Datum': 'Datum',
|
||||||
|
'Mannschaft': 'Mannschaft',
|
||||||
|
'Gegner': 'Gegner',
|
||||||
|
'Ort': 'Ort',
|
||||||
|
'Uhrzeit': 'Uhrzeit',
|
||||||
|
'Ergebnis': 'Ergebnis',
|
||||||
|
'Bemerkung': 'Bemerkung',
|
||||||
|
'Status': 'Status'
|
||||||
|
}
|
||||||
|
|
||||||
|
return headerMap[header] || header
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
if (!dateString) return '-'
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Versuche verschiedene Datumsformate zu parsen
|
||||||
|
const date = new Date(dateString)
|
||||||
|
if (isNaN(date.getTime())) {
|
||||||
|
return dateString // Falls nicht parsbar, Original zurückgeben
|
||||||
|
}
|
||||||
|
|
||||||
|
return date.toLocaleDateString('de-DE', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric'
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
return dateString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatTime = (timeString) => {
|
||||||
|
if (!timeString) return '-'
|
||||||
|
|
||||||
|
// Einfache Zeitformatierung (HH:MM)
|
||||||
|
if (timeString.match(/^\d{1,2}:\d{2}$/)) {
|
||||||
|
return timeString
|
||||||
|
}
|
||||||
|
|
||||||
|
return timeString
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
114
public/data/spielplan.csv
Normal file
114
public/data/spielplan.csv
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
Termin;Saison;Meisterschaft;Altersklasse;Liga;Staffel;Runde;HeimMannschaftAltersklasse;HeimMannschaft;GastMannschaftAltersklasse;GastMannschaft
|
||||||
|
01.07.2025 00:00;Pokal 2025/26;Kreis Frankfurt Pokal 2025/26;Jugend 11;Jugend 11 Kreisliga;Kreisliage Ju11;Pokal;Jugend 11;Harheimer TC;Jugend 11;Eintracht Frankfurt II
|
||||||
|
01.09.2025 20:00;Pokal 2025/26;Kreis Frankfurt Pokal 2025/26;Erwachsene;Erwachsene 1. Kreisklasse;1. Kreisklasse;Pokal;Erwachsene;Harheimer TC II;Herren;spielfrei
|
||||||
|
01.09.2025 20:00;Pokal 2025/26;Kreis Frankfurt Pokal 2025/26;Erwachsene;Erwachsene 3. Kreisklasse (3er);3. Kreisklasse;Pokal;Erwachsene;Harheimer TC V;Herren;spielfrei
|
||||||
|
04.09.2025 19:00;Pokal 2025/26;Kreis Frankfurt Pokal 2025/26;Erwachsene;Erwachsene 2. Kreisklasse;2. Kreisklasse;Pokal;Erwachsene;DJK SW Griesheim;Erwachsene;Harheimer TC IV
|
||||||
|
04.09.2025 20:00;Pokal 2025/26;Kreis Frankfurt Pokal 2025/26;Erwachsene;Erwachsene 2. Kreisklasse;2. Kreisklasse;Pokal;Erwachsene;Eintracht Frankfurt X;Erwachsene;Harheimer TC III
|
||||||
|
04.09.2025 20:15;Pokal 2025/26;Kreis Frankfurt Pokal 2025/26;Erwachsene;Erwachsene 1. Kreisklasse;1. Kreisklasse;Pokal;Erwachsene;Harheimer TC;Erwachsene;DJK-SG 1929 Zeilsheim II
|
||||||
|
06.09.2025 10:00;2025/26;Kreis Frankfurt 2025/26;Jugend 13;Jugend 13 1.Kreisklasse;Jugend 13 1. Kreisklasse;VR;Jugend 13;TSG Oberrad II;Jugend 11;Harheimer TC
|
||||||
|
09.09.2025 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 2. Kreisklasse;2. Kreisklasse Gr. 1;VR;Erwachsene;Harheimer TC III;Erwachsene;DJK SW Griesheim
|
||||||
|
11.09.2025 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 1. Kreisklasse;1. Kreisklasse Gr. 1;VR;Erwachsene;Harheimer TC;Erwachsene;TTC 1957 Nieder-Eschbach
|
||||||
|
11.09.2025 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 2. Kreisklasse;2. Kreisklasse Gr. 2;VR;Erwachsene;Harheimer TC IV;Erwachsene;TV Niederrad VI
|
||||||
|
12.09.2025 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 3. Kreisklasse (3er);3. Kreisklasse (3er) Gr.1;VR;Erwachsene;SV Viktoria Preußen V;Erwachsene;Harheimer TC V
|
||||||
|
16.09.2025 19:30;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 2. Kreisklasse;2. Kreisklasse Gr. 1;VR;Erwachsene;TSG Nordwest Frankfurt III;Erwachsene;Harheimer TC III
|
||||||
|
16.09.2025 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 2. Kreisklasse;2. Kreisklasse Gr. 2;VR;Erwachsene;Harheimer TC IV;Erwachsene;Turngemeinde Unterliederbach 1887 VI
|
||||||
|
17.09.2025 20:00;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 1. Kreisklasse;1. Kreisklasse Gr. 2;VR;Erwachsene;TSG Oberrad IX;Erwachsene;Harheimer TC II
|
||||||
|
18.09.2025 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 3. Kreisklasse (3er);3. Kreisklasse (3er) Gr.1;VR;Erwachsene;Harheimer TC V;Erwachsene;TSG Nieder-Erlenbach IV
|
||||||
|
19.09.2025 20:30;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 1. Kreisklasse;1. Kreisklasse Gr. 1;VR;Erwachsene;TS FFM Heddernheim II;Erwachsene;Harheimer TC
|
||||||
|
22.09.2025 19:30;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 3. Kreisklasse (3er);3. Kreisklasse (3er) Gr.1;VR;Erwachsene;TTC 1957 Nieder-Eschbach II;Erwachsene;Harheimer TC V
|
||||||
|
25.09.2025 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 1. Kreisklasse;1. Kreisklasse Gr. 1;VR;Erwachsene;Harheimer TC;Erwachsene;TG Bornheim 1860 IV
|
||||||
|
26.09.2025 19:45;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 2. Kreisklasse;2. Kreisklasse Gr. 1;VR;Erwachsene;TTC Nordend Frankfurt VI;Erwachsene;Harheimer TC III
|
||||||
|
28.09.2025 10:30;2025/26;Kreis Frankfurt 2025/26;Jugend 13;Jugend 13 1.Kreisklasse;Jugend 13 1. Kreisklasse;VR;Jugend 13;TV Niederrad III;Jugend 11;Harheimer TC
|
||||||
|
29.09.2025 19:45;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 2. Kreisklasse;2. Kreisklasse Gr. 2;VR;Erwachsene;TGS Vorwärts Ffm. VI;Erwachsene;Harheimer TC IV
|
||||||
|
30.09.2025 18:00;2025/26;Kreis Frankfurt 2025/26;Jugend 13;Jugend 13 1.Kreisklasse;Jugend 13 1. Kreisklasse;VR;Jugend 11;Harheimer TC;Jugend 13;Eintracht Frankfurt V
|
||||||
|
30.09.2025 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 1. Kreisklasse;1. Kreisklasse Gr. 1;VR;Erwachsene;Harheimer TC;Erwachsene;TV 1874 Bergen-Enkheim VI
|
||||||
|
30.09.2025 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 1. Kreisklasse;1. Kreisklasse Gr. 2;VR;Erwachsene;Harheimer TC II;Erwachsene;TSG Nordwest Frankfurt
|
||||||
|
02.10.2025 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 3. Kreisklasse (3er);3. Kreisklasse (3er) Gr.1;VR;Erwachsene;Harheimer TC V;Erwachsene;Eintracht Frankfurt XII
|
||||||
|
21.10.2025 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 2. Kreisklasse;2. Kreisklasse Gr. 2;VR;Erwachsene;Harheimer TC IV;Erwachsene;TuS Hausen 1860 III
|
||||||
|
23.10.2025 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 1. Kreisklasse;1. Kreisklasse Gr. 1;VR;Erwachsene;TSG Nieder-Erlenbach;Erwachsene;Harheimer TC
|
||||||
|
23.10.2025 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 2. Kreisklasse;2. Kreisklasse Gr. 1;VR;Erwachsene;Harheimer TC III;Erwachsene;TV Preungesheim 1880 II
|
||||||
|
23.10.2025 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 3. Kreisklasse (3er);3. Kreisklasse (3er) Gr.1;VR;Erwachsene;Harheimer TC V;Erwachsene;TV Niederrad VII
|
||||||
|
24.10.2025 19:45;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 1. Kreisklasse;1. Kreisklasse Gr. 2;VR;Erwachsene;TTC Nordend Frankfurt IV;Erwachsene;Harheimer TC II
|
||||||
|
25.10.2025 13:15;2025/26;Kreis Frankfurt 2025/26;Jugend 13;Jugend 13 1.Kreisklasse;Jugend 13 1. Kreisklasse;VR;Jugend 13;Eintracht Frankfurt IV;Jugend 11;Harheimer TC
|
||||||
|
27.10.2025 20:00;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 2. Kreisklasse;2. Kreisklasse Gr. 1;VR;Erwachsene;TSG Nieder-Erlenbach II;Erwachsene;Harheimer TC III
|
||||||
|
30.10.2025 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 1. Kreisklasse;1. Kreisklasse Gr. 1;VR;Erwachsene;Harheimer TC;Erwachsene;TSG Oberrad X
|
||||||
|
31.10.2025 19:30;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 2. Kreisklasse;2. Kreisklasse Gr. 2;VR;Erwachsene;SG 1878 Sossenheim IV;Erwachsene;Harheimer TC IV
|
||||||
|
31.10.2025 20:00;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 1. Kreisklasse;1. Kreisklasse Gr. 2;VR;Erwachsene;TV Eschersheim 1895 VII;Erwachsene;Harheimer TC II
|
||||||
|
04.11.2025 19:30;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 2. Kreisklasse;2. Kreisklasse Gr. 2;VR;Erwachsene;TG Bornheim 1860 V;Erwachsene;Harheimer TC IV
|
||||||
|
04.11.2025 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 1. Kreisklasse;1. Kreisklasse Gr. 2;VR;Erwachsene;Harheimer TC II;Erwachsene;DJK-SG 1929 Zeilsheim II
|
||||||
|
06.11.2025 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 2. Kreisklasse;2. Kreisklasse Gr. 1;VR;Erwachsene;Harheimer TC III;Erwachsene;FTV 1860 Frankfurt II
|
||||||
|
07.11.2025 19:30;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 3. Kreisklasse (3er);3. Kreisklasse (3er) Gr.1;VR;Erwachsene;SG 1878 Sossenheim V;Erwachsene;Harheimer TC V
|
||||||
|
10.11.2025 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 1. Kreisklasse;1. Kreisklasse Gr. 2;VR;Erwachsene;TV 1874 Bergen-Enkheim V;Erwachsene;Harheimer TC II
|
||||||
|
11.11.2025 18:00;2025/26;Kreis Frankfurt 2025/26;Jugend 13;Jugend 13 1.Kreisklasse;Jugend 13 1. Kreisklasse;VR;Jugend 11;Harheimer TC;Jugend 13;TSG Nieder-Erlenbach
|
||||||
|
11.11.2025 20:30;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 1. Kreisklasse;1. Kreisklasse Gr. 1;VR;Erwachsene;TSV 1878 Ginnheim II;Erwachsene;Harheimer TC
|
||||||
|
13.11.2025 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 2. Kreisklasse;2. Kreisklasse Gr. 1;VR;Erwachsene;Harheimer TC III;Erwachsene;TSV Sachsenhausen 1857 II
|
||||||
|
17.11.2025 19:45;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 3. Kreisklasse (3er);3. Kreisklasse (3er) Gr.1;VR;Erwachsene;TGS Vorwärts Ffm. VII;Erwachsene;Harheimer TC V
|
||||||
|
18.11.2025 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 2. Kreisklasse;2. Kreisklasse Gr. 2;VR;Erwachsene;Harheimer TC IV;Erwachsene;Eintracht Frankfurt XI
|
||||||
|
25.11.2025 20:00;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 2. Kreisklasse;2. Kreisklasse Gr. 1;VR;Erwachsene;Turngemeinde Unterliederbach 1887 VII;Erwachsene;Harheimer TC III
|
||||||
|
25.11.2025 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 1. Kreisklasse;1. Kreisklasse Gr. 2;VR;Erwachsene;Harheimer TC II;Erwachsene;TG Bornheim 1860 III
|
||||||
|
27.11.2025 19:30;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 2. Kreisklasse;2. Kreisklasse Gr. 2;VR;Erwachsene;TSG Nordwest Frankfurt II;Erwachsene;Harheimer TC IV
|
||||||
|
27.11.2025 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 1. Kreisklasse;1. Kreisklasse Gr. 1;VR;Erwachsene;Harheimer TC;Erwachsene;TG Sachsenhausen 04 IV
|
||||||
|
27.11.2025 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 3. Kreisklasse (3er);3. Kreisklasse (3er) Gr.1;VR;Erwachsene;Harheimer TC V;Erwachsene;TV Preungesheim 1880 IV
|
||||||
|
28.11.2025 19:45;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 1. Kreisklasse;1. Kreisklasse Gr. 1;VR;Erwachsene;TTC Nordend Frankfurt V;Erwachsene;Harheimer TC
|
||||||
|
29.11.2025 13:15;2025/26;Kreis Frankfurt 2025/26;Jugend 13;Jugend 13 1.Kreisklasse;Jugend 13 1. Kreisklasse;VR;Jugend 11;Eintracht Frankfurt II;Jugend 11;Harheimer TC
|
||||||
|
02.12.2025 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 2. Kreisklasse;2. Kreisklasse Gr. 1;VR;Erwachsene;Harheimer TC III;Erwachsene;TV Eschersheim 1895 VIII
|
||||||
|
09.12.2025 18:00;2025/26;Kreis Frankfurt 2025/26;Jugend 13;Jugend 13 1.Kreisklasse;Jugend 13 1. Kreisklasse;VR;Jugend 11;Harheimer TC;Jugend 13;TV Seckbach 1875
|
||||||
|
09.12.2025 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 1. Kreisklasse;1. Kreisklasse Gr. 2;VR;Erwachsene;Harheimer TC II;Erwachsene;TV Seckbach 1875 III
|
||||||
|
09.12.2025 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 2. Kreisklasse;2. Kreisklasse Gr. 2;VR;Erwachsene;Harheimer TC IV;Erwachsene;TV 1875 Sindlingen III
|
||||||
|
11.12.2025 20:00;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 2. Kreisklasse;2. Kreisklasse Gr. 1;VR;Erwachsene;Eintracht Frankfurt X;Erwachsene;Harheimer TC III
|
||||||
|
11.12.2025 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 3. Kreisklasse (3er);3. Kreisklasse (3er) Gr.1;VR;Erwachsene;Harheimer TC V;Erwachsene;TV 1874 Bergen-Enkheim VII
|
||||||
|
16.12.2025 20:15;Pokal 2025/26;Kreis Frankfurt Pokal 2025/26;Erwachsene;Erwachsene 1. Kreisklasse;1. Kreisklasse;Pokal;Erwachsene;Harheimer TC;Erwachsene;TG Sachsenhausen 04 IV
|
||||||
|
17.12.2025 20:00;Pokal 2025/26;Kreis Frankfurt Pokal 2025/26;Erwachsene;Erwachsene 1. Kreisklasse;1. Kreisklasse;Pokal;Erwachsene;TSG Oberrad IX;Erwachsene;Harheimer TC II
|
||||||
|
18.12.2025 19:00;Pokal 2025/26;Kreis Frankfurt Pokal 2025/26;Erwachsene;Erwachsene 3. Kreisklasse (3er);3. Kreisklasse;Pokal;Erwachsene;DJK SW Griesheim II;Erwachsene;Harheimer TC V
|
||||||
|
20.01.2026 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 2. Kreisklasse;2. Kreisklasse Gr. 2;RR;Erwachsene;Harheimer TC IV;Erwachsene;SG 1878 Sossenheim IV
|
||||||
|
22.01.2026 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 2. Kreisklasse;2. Kreisklasse Gr. 1;RR;Erwachsene;Harheimer TC III;Erwachsene;Turngemeinde Unterliederbach 1887 VII
|
||||||
|
23.01.2026 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 1. Kreisklasse;1. Kreisklasse Gr. 1;RR;Erwachsene;TG Sachsenhausen 04 IV;Erwachsene;Harheimer TC
|
||||||
|
29.01.2026 20:00;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 2. Kreisklasse;2. Kreisklasse Gr. 2;RR;Erwachsene;Eintracht Frankfurt XI;Erwachsene;Harheimer TC IV
|
||||||
|
29.01.2026 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 2. Kreisklasse;2. Kreisklasse Gr. 1;RR;Erwachsene;Harheimer TC III;Erwachsene;TSG Nieder-Erlenbach II
|
||||||
|
04.02.2026 20:00;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 1. Kreisklasse;1. Kreisklasse Gr. 1;RR;Erwachsene;TSG Oberrad X;Erwachsene;Harheimer TC
|
||||||
|
04.02.2026 20:00;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 2. Kreisklasse;2. Kreisklasse Gr. 1;RR;Erwachsene;TV Preungesheim 1880 II;Erwachsene;Harheimer TC III
|
||||||
|
05.02.2026 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 3. Kreisklasse (3er);3. Kreisklasse (3er) Gr.1;RR;Erwachsene;TSG Nieder-Erlenbach IV;Erwachsene;Harheimer TC V
|
||||||
|
06.02.2026 18:00;2025/26;Kreis Frankfurt 2025/26;Jugend 13;Jugend 13 1.Kreisklasse;Jugend 13 1. Kreisklasse;RR;Jugend 13;TV Seckbach 1875;Jugend 11;Harheimer TC
|
||||||
|
06.02.2026 20:30;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 1. Kreisklasse;1. Kreisklasse Gr. 2;RR;Erwachsene;TV Seckbach 1875 III;Erwachsene;Harheimer TC II
|
||||||
|
08.02.2026 13:30;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 2. Kreisklasse;2. Kreisklasse Gr. 2;RR;Erwachsene;TV Niederrad VI;Erwachsene;Harheimer TC IV
|
||||||
|
12.02.2026 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 2. Kreisklasse;2. Kreisklasse Gr. 1;RR;Erwachsene;Harheimer TC III;Erwachsene;TTC Nordend Frankfurt VI
|
||||||
|
12.02.2026 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 3. Kreisklasse (3er);3. Kreisklasse (3er) Gr.1;RR;Erwachsene;Harheimer TC V;Erwachsene;TGS Vorwärts Ffm. VII
|
||||||
|
16.02.2026 18:00;2025/26;Kreis Frankfurt 2025/26;Jugend 13;Jugend 13 1.Kreisklasse;Jugend 13 1. Kreisklasse;RR;Jugend 13;TSG Nieder-Erlenbach;Jugend 11;Harheimer TC
|
||||||
|
16.02.2026 19:45;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 3. Kreisklasse (3er);3. Kreisklasse (3er) Gr.1;RR;Erwachsene;TV Preungesheim 1880 IV;Erwachsene;Harheimer TC V
|
||||||
|
17.02.2026 20:00;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 2. Kreisklasse;2. Kreisklasse Gr. 2;RR;Erwachsene;Turngemeinde Unterliederbach 1887 VI;Erwachsene;Harheimer TC IV
|
||||||
|
17.02.2026 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 1. Kreisklasse;1. Kreisklasse Gr. 2;RR;Erwachsene;Harheimer TC II;Erwachsene;TTC Nordend Frankfurt IV
|
||||||
|
19.02.2026 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 1. Kreisklasse;1. Kreisklasse Gr. 1;RR;Erwachsene;Harheimer TC;Erwachsene;TTC Nordend Frankfurt V
|
||||||
|
20.02.2026 20:00;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 2. Kreisklasse;2. Kreisklasse Gr. 1;RR;Erwachsene;TV Eschersheim 1895 VIII;Erwachsene;Harheimer TC III
|
||||||
|
23.02.2026 19:30;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 1. Kreisklasse;1. Kreisklasse Gr. 1;RR;Erwachsene;TTC 1957 Nieder-Eschbach;Erwachsene;Harheimer TC
|
||||||
|
24.02.2026 18:00;2025/26;Kreis Frankfurt 2025/26;Jugend 13;Jugend 13 1.Kreisklasse;Jugend 13 1. Kreisklasse;RR;Jugend 11;Harheimer TC;Jugend 13;TV Niederrad III
|
||||||
|
24.02.2026 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 2. Kreisklasse;2. Kreisklasse Gr. 2;RR;Erwachsene;Harheimer TC IV;Erwachsene;TGS Vorwärts Ffm. VI
|
||||||
|
26.02.2026 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 2. Kreisklasse;2. Kreisklasse Gr. 1;RR;Erwachsene;Harheimer TC III;Erwachsene;Eintracht Frankfurt X
|
||||||
|
27.02.2026 19:30;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 1. Kreisklasse;1. Kreisklasse Gr. 2;RR;Erwachsene;TSG Nordwest Frankfurt;Erwachsene;Harheimer TC II
|
||||||
|
03.03.2026 19:30;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 2. Kreisklasse;2. Kreisklasse Gr. 2;RR;Erwachsene;TV 1875 Sindlingen III;Erwachsene;Harheimer TC IV
|
||||||
|
05.03.2026 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 1. Kreisklasse;1. Kreisklasse Gr. 1;RR;Erwachsene;Harheimer TC;Erwachsene;TSG Nieder-Erlenbach
|
||||||
|
05.03.2026 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 3. Kreisklasse (3er);3. Kreisklasse (3er) Gr.1;RR;Erwachsene;Harheimer TC V;Erwachsene;SG 1878 Sossenheim V
|
||||||
|
10.03.2026 18:00;2025/26;Kreis Frankfurt 2025/26;Jugend 13;Jugend 13 1.Kreisklasse;Jugend 13 1. Kreisklasse;RR;Jugend 11;Harheimer TC;Jugend 13;TSG Oberrad II
|
||||||
|
11.03.2026 19:30;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 1. Kreisklasse;1. Kreisklasse Gr. 2;RR;Erwachsene;DJK-SG 1929 Zeilsheim II;Erwachsene;Harheimer TC II
|
||||||
|
12.03.2026 19:30;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 1. Kreisklasse;1. Kreisklasse Gr. 1;RR;Erwachsene;TG Bornheim 1860 IV;Erwachsene;Harheimer TC
|
||||||
|
13.03.2026 19:45;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 2. Kreisklasse;2. Kreisklasse Gr. 1;RR;Erwachsene;TSV Sachsenhausen 1857 II;Erwachsene;Harheimer TC III
|
||||||
|
13.03.2026 20:00;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 3. Kreisklasse (3er);3. Kreisklasse (3er) Gr.1;RR;Erwachsene;Eintracht Frankfurt XII;Erwachsene;Harheimer TC V
|
||||||
|
17.03.2026 19:30;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 1. Kreisklasse;1. Kreisklasse Gr. 2;RR;Erwachsene;TG Bornheim 1860 III;Erwachsene;Harheimer TC II
|
||||||
|
17.03.2026 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 2. Kreisklasse;2. Kreisklasse Gr. 2;RR;Erwachsene;Harheimer TC IV;Erwachsene;TG Bornheim 1860 V
|
||||||
|
19.03.2026 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 1. Kreisklasse;1. Kreisklasse Gr. 1;RR;Erwachsene;Harheimer TC;Erwachsene;TS FFM Heddernheim II
|
||||||
|
20.03.2026 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 3. Kreisklasse (3er);3. Kreisklasse (3er) Gr.1;RR;Erwachsene;TV 1874 Bergen-Enkheim VII;Erwachsene;Harheimer TC V
|
||||||
|
24.03.2026 18:00;2025/26;Kreis Frankfurt 2025/26;Jugend 13;Jugend 13 1.Kreisklasse;Jugend 13 1. Kreisklasse;RR;Jugend 11;Harheimer TC;Jugend 13;Eintracht Frankfurt IV
|
||||||
|
24.03.2026 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 1. Kreisklasse;1. Kreisklasse Gr. 2;RR;Erwachsene;Harheimer TC II;Erwachsene;TV 1874 Bergen-Enkheim V
|
||||||
|
26.03.2026 19:30;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 2. Kreisklasse;2. Kreisklasse Gr. 2;RR;Erwachsene;TuS Hausen 1860 III;Erwachsene;Harheimer TC IV
|
||||||
|
26.03.2026 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 2. Kreisklasse;2. Kreisklasse Gr. 1;RR;Erwachsene;Harheimer TC III;Erwachsene;TSG Nordwest Frankfurt III
|
||||||
|
29.03.2026 17:00;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 3. Kreisklasse (3er);3. Kreisklasse (3er) Gr.1;RR;Erwachsene;TV Niederrad VII;Erwachsene;Harheimer TC V
|
||||||
|
14.04.2026 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 1. Kreisklasse;1. Kreisklasse Gr. 2;RR;Erwachsene;Harheimer TC II;Erwachsene;TV Eschersheim 1895 VII
|
||||||
|
14.04.2026 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 2. Kreisklasse;2. Kreisklasse Gr. 1;RR;Erwachsene;FTV 1860 Frankfurt II;Erwachsene;Harheimer TC III
|
||||||
|
16.04.2026 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 1. Kreisklasse;1. Kreisklasse Gr. 1;RR;Erwachsene;Harheimer TC;Erwachsene;TSV 1878 Ginnheim II
|
||||||
|
16.04.2026 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 3. Kreisklasse (3er);3. Kreisklasse (3er) Gr.1;RR;Erwachsene;Harheimer TC V;Erwachsene;TTC 1957 Nieder-Eschbach II
|
||||||
|
18.04.2026 10:00;2025/26;Kreis Frankfurt 2025/26;Jugend 13;Jugend 13 1.Kreisklasse;Jugend 13 1. Kreisklasse;RR;Jugend 13;Eintracht Frankfurt V;Jugend 11;Harheimer TC
|
||||||
|
20.04.2026 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 1. Kreisklasse;1. Kreisklasse Gr. 1;RR;Erwachsene;TV 1874 Bergen-Enkheim VI;Erwachsene;Harheimer TC
|
||||||
|
21.04.2026 18:00;2025/26;Kreis Frankfurt 2025/26;Jugend 13;Jugend 13 1.Kreisklasse;Jugend 13 1. Kreisklasse;RR;Jugend 11;Harheimer TC;Jugend 11;Eintracht Frankfurt II
|
||||||
|
21.04.2026 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 1. Kreisklasse;1. Kreisklasse Gr. 2;RR;Erwachsene;Harheimer TC II;Erwachsene;TSG Oberrad IX
|
||||||
|
23.04.2026 19:00;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 2. Kreisklasse;2. Kreisklasse Gr. 1;RR;Erwachsene;DJK SW Griesheim;Erwachsene;Harheimer TC III
|
||||||
|
23.04.2026 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 2. Kreisklasse;2. Kreisklasse Gr. 2;RR;Erwachsene;Harheimer TC IV;Erwachsene;TSG Nordwest Frankfurt II
|
||||||
|
23.04.2026 20:15;2025/26;Kreis Frankfurt 2025/26;Erwachsene;Erwachsene 3. Kreisklasse (3er);3. Kreisklasse (3er) Gr.1;RR;Erwachsene;Harheimer TC V;Erwachsene;SV Viktoria Preußen V
|
||||||
|
17
public/documents/spielplaene/README.md
Normal file
17
public/documents/spielplaene/README.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Spielplan PDF-Dateien
|
||||||
|
|
||||||
|
Dieses Verzeichnis enthält die PDF-Dateien für die verschiedenen Spielpläne.
|
||||||
|
|
||||||
|
## Erwartete Dateien:
|
||||||
|
|
||||||
|
- `spielplan_gesamt.pdf` - Gesamter Spielplan aller Mannschaften
|
||||||
|
- `spielplan_erwachsene.pdf` - Spielplan der Erwachsenen-Mannschaften
|
||||||
|
- `spielplan_nachwuchs.pdf` - Spielplan der Nachwuchs-Mannschaften
|
||||||
|
|
||||||
|
## Upload über CMS:
|
||||||
|
|
||||||
|
Die PDF-Dateien können über das CMS unter "Spielpläne" hochgeladen werden.
|
||||||
|
|
||||||
|
## Download:
|
||||||
|
|
||||||
|
Die PDF-Dateien werden über die Spielpläne-Seite unter "Mannschaften" -> "Spielpläne" heruntergeladen.
|
||||||
@@ -16,7 +16,8 @@ export default defineEventHandler(async (event) => {
|
|||||||
const allowedFiles = [
|
const allowedFiles = [
|
||||||
'vereinsmeisterschaften.csv',
|
'vereinsmeisterschaften.csv',
|
||||||
'mannschaften.csv',
|
'mannschaften.csv',
|
||||||
'termine.csv'
|
'termine.csv',
|
||||||
|
'spielplan.csv'
|
||||||
]
|
]
|
||||||
|
|
||||||
if (!allowedFiles.includes(filename)) {
|
if (!allowedFiles.includes(filename)) {
|
||||||
|
|||||||
102
server/api/cms/upload-spielplan-pdf.post.js
Normal file
102
server/api/cms/upload-spielplan-pdf.post.js
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import multer from 'multer'
|
||||||
|
import fs from 'fs/promises'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
// Multer-Konfiguration für PDF-Uploads
|
||||||
|
const storage = multer.diskStorage({
|
||||||
|
destination: (req, file, cb) => {
|
||||||
|
const uploadPath = path.join(process.cwd(), 'public', 'documents', 'spielplaene')
|
||||||
|
cb(null, uploadPath)
|
||||||
|
},
|
||||||
|
filename: (req, file, cb) => {
|
||||||
|
const type = req.body.type
|
||||||
|
const filename = `spielplan_${type}.pdf`
|
||||||
|
cb(null, filename)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const upload = multer({
|
||||||
|
storage: storage,
|
||||||
|
fileFilter: (req, file, cb) => {
|
||||||
|
if (file.mimetype === 'application/pdf') {
|
||||||
|
cb(null, true)
|
||||||
|
} else {
|
||||||
|
cb(new Error('Nur PDF-Dateien sind erlaubt'), false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
limits: {
|
||||||
|
fileSize: 10 * 1024 * 1024 // 10MB Limit
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
try {
|
||||||
|
// Prüfe Authentifizierung
|
||||||
|
const authHeader = getHeader(event, 'authorization')
|
||||||
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 401,
|
||||||
|
statusMessage: 'Nicht autorisiert'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multer-Middleware für multipart/form-data
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
upload.single('pdf')(event.node.req, event.node.res, (err) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const file = event.node.req.file
|
||||||
|
const type = event.node.req.body.type
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: 'Keine Datei hochgeladen'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!type || !['gesamt', 'erwachsene', 'nachwuchs'].includes(type)) {
|
||||||
|
// Lösche die hochgeladene Datei
|
||||||
|
await fs.unlink(file.path)
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: 'Ungültiger Typ'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `PDF für ${type} erfolgreich hochgeladen`,
|
||||||
|
filename: file.filename,
|
||||||
|
originalName: file.originalname
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim PDF-Upload:', error)
|
||||||
|
|
||||||
|
if (error.code === 'LIMIT_FILE_SIZE') {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 413,
|
||||||
|
statusMessage: 'Datei zu groß (max. 10MB)'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.message === 'Nur PDF-Dateien sind erlaubt') {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: 'Nur PDF-Dateien sind erlaubt'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
throw createError({
|
||||||
|
statusCode: 500,
|
||||||
|
statusMessage: 'Fehler beim Hochladen der PDF-Datei'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
61
server/api/spielplan.get.js
Normal file
61
server/api/spielplan.get.js
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import fs from 'fs/promises'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
try {
|
||||||
|
const filePath = path.join(process.cwd(), 'public', 'data', 'spielplan.csv')
|
||||||
|
|
||||||
|
// Prüfe ob Datei existiert
|
||||||
|
try {
|
||||||
|
await fs.access(filePath)
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'Spielplan-Datei nicht gefunden',
|
||||||
|
data: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSV-Datei lesen
|
||||||
|
const csvContent = await fs.readFile(filePath, 'utf-8')
|
||||||
|
const lines = csvContent.split('\n').filter(line => line.trim() !== '')
|
||||||
|
|
||||||
|
if (lines.length < 2) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'Spielplan-Datei ist leer oder unvollständig',
|
||||||
|
data: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header-Zeile parsen
|
||||||
|
const headers = lines[0].split(';').map(header => header.trim())
|
||||||
|
|
||||||
|
// Datenzeilen parsen
|
||||||
|
const data = lines.slice(1).map(line => {
|
||||||
|
const values = line.split(';').map(value => value.trim())
|
||||||
|
const row = {}
|
||||||
|
|
||||||
|
headers.forEach((header, index) => {
|
||||||
|
row[header] = values[index] || ''
|
||||||
|
})
|
||||||
|
|
||||||
|
return row
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Spielplan erfolgreich geladen',
|
||||||
|
data: data,
|
||||||
|
headers: headers
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Laden des Spielplans:', error)
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'Fehler beim Laden des Spielplans',
|
||||||
|
data: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
114
server/api/spielplan/download/[filename].get.js
Normal file
114
server/api/spielplan/download/[filename].get.js
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import fs from 'fs/promises'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
try {
|
||||||
|
const filename = getRouterParam(event, 'filename')
|
||||||
|
|
||||||
|
if (!filename) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: 'Dateiname fehlt'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erlaubte Dateinamen für Sicherheit
|
||||||
|
const allowedFiles = [
|
||||||
|
'spielplan_gesamt.pdf',
|
||||||
|
'spielplan_erwachsene.pdf',
|
||||||
|
'spielplan_nachwuchs.pdf',
|
||||||
|
'spielplan_erwachsene_1.pdf',
|
||||||
|
'spielplan_erwachsene_2.pdf',
|
||||||
|
'spielplan_erwachsene_3.pdf',
|
||||||
|
'spielplan_erwachsene_4.pdf',
|
||||||
|
'spielplan_erwachsene_5.pdf',
|
||||||
|
'spielplan_jugendmannschaft.pdf'
|
||||||
|
]
|
||||||
|
|
||||||
|
// Prüfe ob es eine dynamische Mannschafts-PDF ist
|
||||||
|
const isDynamicMannschaft = filename.startsWith('spielplan_') && filename.endsWith('.pdf') &&
|
||||||
|
!allowedFiles.includes(filename)
|
||||||
|
|
||||||
|
if (!allowedFiles.includes(filename) && !isDynamicMannschaft) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 403,
|
||||||
|
statusMessage: 'Datei nicht erlaubt'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let filePath
|
||||||
|
|
||||||
|
if (isDynamicMannschaft) {
|
||||||
|
// Für dynamische Mannschafts-PDFs: Verwende Gesamt-Spielplan als Fallback
|
||||||
|
// Hier könnte später ein PDF-Generator implementiert werden
|
||||||
|
filePath = path.join(process.cwd(), 'public', 'documents', 'spielplaene', 'spielplan_gesamt.pdf')
|
||||||
|
} else {
|
||||||
|
// Für vordefinierte PDFs
|
||||||
|
filePath = path.join(process.cwd(), 'public', 'documents', 'spielplaene', filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfe ob Datei existiert
|
||||||
|
try {
|
||||||
|
await fs.access(filePath)
|
||||||
|
} catch (error) {
|
||||||
|
// Fallback: Erstelle eine informative HTML-Seite
|
||||||
|
const htmlContent = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>PDF nicht verfügbar</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; }
|
||||||
|
.action { margin-top: 20px; padding: 15px; background: #e3f2fd; border-radius: 4px; }
|
||||||
|
.link { color: #1976d2; text-decoration: none; font-weight: bold; }
|
||||||
|
.link:hover { text-decoration: underline; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="error">📄 PDF-Datei nicht verfügbar</div>
|
||||||
|
<div class="message">
|
||||||
|
Die angeforderte PDF-Datei <strong>"${filename}"</strong> wurde noch nicht hochgeladen.
|
||||||
|
</div>
|
||||||
|
<div class="action">
|
||||||
|
<strong>Was können Sie tun?</strong><br>
|
||||||
|
• Laden Sie die PDF-Datei über das <a href="/cms/spielplaene" class="link">CMS</a> hoch<br>
|
||||||
|
• Kontaktieren Sie den Administrator<br>
|
||||||
|
• Versuchen Sie es später erneut
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
|
||||||
|
setHeader(event, 'Content-Type', 'text/html; charset=utf-8')
|
||||||
|
return htmlContent
|
||||||
|
}
|
||||||
|
|
||||||
|
// Datei lesen
|
||||||
|
const fileBuffer = await fs.readFile(filePath)
|
||||||
|
|
||||||
|
// Content-Type setzen
|
||||||
|
setHeader(event, 'Content-Type', 'application/pdf')
|
||||||
|
setHeader(event, 'Content-Disposition', `attachment; filename="${filename}"`)
|
||||||
|
setHeader(event, 'Content-Length', fileBuffer.length.toString())
|
||||||
|
|
||||||
|
return fileBuffer
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Laden der PDF-Datei:', error)
|
||||||
|
|
||||||
|
if (error.statusCode) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
throw createError({
|
||||||
|
statusCode: 500,
|
||||||
|
statusMessage: 'Fehler beim Laden der PDF-Datei'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
352
server/api/spielplan/pdf.get.js
Normal file
352
server/api/spielplan/pdf.get.js
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
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'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = lines[0].split(';')
|
||||||
|
const dataRows = lines.slice(1).map(line => {
|
||||||
|
const values = line.split(';')
|
||||||
|
const row = {}
|
||||||
|
headers.forEach((header, index) => {
|
||||||
|
row[header] = values[index] || ''
|
||||||
|
})
|
||||||
|
return row
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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}}
|
||||||
|
|
||||||
|
\\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}
|
||||||
|
`}
|
||||||
|
|
||||||
|
\\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
|
||||||
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
|
||||||
|
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'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user