Files
harheimertc/pages/mannschaften/[slug].vue
Torsten Schulz (local) e19158558d
Some checks failed
Code Analysis and Production Deploy / analyze (push) Failing after 2m39s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Has been skipped
feat(mannschaften): add matches/table tabs on team detail pages
2026-05-20 17:41:43 +02:00

678 lines
24 KiB
Vue

<template>
<div class="min-h-full py-16 bg-gray-50">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<div
v-if="mannschaft"
class="space-y-8"
>
<!-- Header -->
<div class="bg-gradient-to-r from-primary-600 to-primary-700 rounded-xl p-8 text-white">
<h1 class="text-4xl font-display font-bold mb-2">
{{ mannschaft.mannschaft }}
</h1>
<p class="text-primary-100 text-xl">
{{ mannschaft.liga }}
</p>
</div>
<!-- Liga-Info -->
<div class="bg-white rounded-xl shadow-lg p-6">
<h2 class="text-2xl font-semibold text-gray-900 mb-6">
Liga-Informationen
</h2>
<div class="grid md:grid-cols-2 gap-6">
<div class="space-y-4">
<div class="flex items-center space-x-3">
<div class="w-2 h-2 bg-primary-600 rounded-full" />
<span class="text-gray-600">Staffelleiter:</span>
<span class="font-semibold text-gray-900">{{ mannschaft.staffelleiter }}</span>
</div>
<div class="flex items-center space-x-3">
<div class="w-2 h-2 bg-primary-600 rounded-full" />
<span class="text-gray-600">Telefon:</span>
<span class="font-semibold text-gray-900">{{ mannschaft.telefon }}</span>
</div>
</div>
<div class="space-y-4">
<div class="flex items-center space-x-3">
<div class="w-2 h-2 bg-primary-600 rounded-full" />
<span class="text-gray-600">Heimspieltag:</span>
<span class="font-semibold text-gray-900">{{ mannschaft.heimspieltag }}</span>
</div>
<div class="flex items-center space-x-3">
<div class="w-2 h-2 bg-primary-600 rounded-full" />
<span class="text-gray-600">Spielsystem:</span>
<span class="font-semibold text-gray-900">{{ mannschaft.spielsystem }}</span>
</div>
</div>
</div>
</div>
<!-- Mannschaftsaufstellung -->
<div class="bg-white rounded-xl shadow-lg p-6">
<h2 class="text-2xl font-semibold text-gray-900 mb-6">
Mannschaftsaufstellung Saison 2025/26
</h2>
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div
v-for="(spieler, index) in getSpielerListe(mannschaft)"
:key="index"
class="bg-gray-50 rounded-lg p-4 text-center"
:class="spieler === mannschaft.mannschaftsfuehrer ? 'ring-2 ring-primary-500 bg-primary-50' : ''"
>
<div class="font-semibold text-gray-900">
{{ spieler }}
</div>
<div
v-if="spieler === mannschaft.mannschaftsfuehrer"
class="text-xs text-primary-600 font-medium mt-1"
>
Mannschaftsführer
</div>
</div>
</div>
</div>
<!-- Aktueller Spielplan -->
<div class="bg-white rounded-xl shadow-lg overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200">
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 class="text-2xl font-semibold text-gray-900">
Aktueller Spielplan
</h2>
<p
v-if="spielplanSeasonLabel"
class="text-sm text-gray-600 mt-1"
>
Saison {{ spielplanSeasonLabel }}
</p>
</div>
<div class="inline-flex rounded-lg border border-gray-200 p-1 bg-gray-50">
<button
type="button"
class="px-4 py-2 text-sm font-medium rounded-md transition-colors"
:class="activePanelTab === 'matches' ? 'bg-white text-primary-700 shadow-sm' : 'text-gray-600 hover:text-gray-900'"
@click="activePanelTab = 'matches'"
>
Matches
</button>
<button
v-if="hasTableLink"
type="button"
class="px-4 py-2 text-sm font-medium rounded-md transition-colors"
:class="activePanelTab === 'table' ? 'bg-white text-primary-700 shadow-sm' : 'text-gray-600 hover:text-gray-900'"
@click="activePanelTab = 'table'"
>
Tabelle
</button>
</div>
</div>
</div>
<div
v-if="activePanelTab === 'matches' && isSpielplanLoading"
class="p-6 text-sm text-gray-600"
>
Spielplan wird geladen...
</div>
<div
v-else-if="activePanelTab === 'matches' && spielplanError"
class="p-6 text-sm text-red-600"
>
{{ spielplanError }}
</div>
<div
v-else-if="activePanelTab === 'matches' && mannschaftSpielplan.length === 0"
class="p-6 text-sm text-gray-600"
>
Für diese Mannschaft sind im aktuellen Spielplan keine Spiele vorhanden.
</div>
<div
v-else-if="activePanelTab === 'matches'"
class="overflow-x-auto"
>
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Termin
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Heim
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Gast
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Ergebnis
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Runde
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Gruppe
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr
v-for="game in mannschaftSpielplan"
:key="`${game.Termin}-${game.HeimMannschaft}-${game.GastMannschaft}`"
:class="getRowClass(game)"
>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{{ game.Termin || '-' }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 font-medium">
{{ game.HeimMannschaft || '-' }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 font-medium">
{{ game.GastMannschaft || '-' }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{{ formatResult(game) }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{{ formatRunde(game.Runde) }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
<span class="text-xs leading-tight text-gray-700">
<span class="block">
{{ game.Altersklasse || '-' }}
</span>
<span class="block text-gray-500">
{{ formatStaffel(game.Staffel) }}
</span>
</span>
</td>
</tr>
</tbody>
</table>
</div>
<div
v-if="activePanelTab === 'table' && isTableLoading"
class="p-6 text-sm text-gray-600"
>
Tabelle wird geladen...
</div>
<div
v-else-if="activePanelTab === 'table' && tableError"
class="p-6 text-sm text-red-600"
>
{{ tableError }}
</div>
<div
v-else-if="activePanelTab === 'table' && teamTableRows.length === 0"
class="p-6 text-sm text-gray-600"
>
Für diese Mannschaft ist aktuell keine Tabelle hinterlegt.
</div>
<div
v-else-if="activePanelTab === 'table'"
class="overflow-x-auto"
>
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Platz
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Mannschaft
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Spiele
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
S/U/N
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Sätze
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Bälle
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Punkte
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr
v-for="(row, index) in teamTableRows"
:key="`${row.team_id || row.team_name || 'row'}-${index}`"
:class="isCurrentTeamRow(row) ? 'bg-primary-50' : 'bg-white'"
>
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-900">
{{ row.table_rank ?? '-' }}
</td>
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-900 font-medium">
{{ row.team_name || '-' }}
</td>
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-900">
{{ row.meetings_count ?? '-' }}
</td>
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-900">
{{ formatSun(row) }}
</td>
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-900">
{{ row.sets_relation || '-' }}
</td>
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-900">
{{ row.games_relation || '-' }}
</td>
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-900">
{{ formatPunkte(row) }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Links -->
<div class="bg-white rounded-xl shadow-lg p-6">
<h2 class="text-2xl font-semibold text-gray-900 mb-6">
Weitere Informationen
</h2>
<div class="text-center">
<a
v-if="mannschaft.weitere_informationen_link && mannschaft.weitere_informationen_link !== ''"
:href="mannschaft.weitere_informationen_link"
target="_blank"
class="inline-flex items-center px-8 py-4 bg-primary-600 hover:bg-primary-700 text-white font-semibold rounded-lg transition-colors"
>
<BarChart
:size="24"
class="mr-3"
/>
Weitere Informationen
</a>
</div>
</div>
<!-- Letzte Aktualisierung -->
<div class="bg-white rounded-xl shadow-lg p-6">
<p class="text-sm text-gray-500 text-center">
Zuletzt aktualisiert am: {{ formatDate(mannschaft.letzte_aktualisierung) }}
</p>
</div>
<!-- Zurück-Button -->
<div class="text-center">
<NuxtLink
to="/mannschaften"
class="inline-flex items-center px-6 py-3 bg-primary-600 hover:bg-primary-700 text-white font-semibold rounded-lg transition-colors"
>
Zurück zur Übersicht
</NuxtLink>
</div>
</div>
<div
v-else
class="text-center py-16"
>
<h1 class="text-4xl font-display font-bold text-gray-900 mb-4">
Mannschaft nicht gefunden
</h1>
<p class="text-gray-600 mb-8">
Die angeforderte Mannschaft konnte nicht gefunden werden.
</p>
<NuxtLink
to="/mannschaften"
class="inline-flex items-center px-6 py-3 bg-primary-600 hover:bg-primary-700 text-white font-semibold rounded-lg transition-colors"
>
Zur Mannschaftsübersicht
</NuxtLink>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { Users } from 'lucide-vue-next'
const route = useRoute()
const mannschaft = ref(null)
const mannschaftSpielplan = ref([])
const spielplanSeason = ref('')
const isSpielplanLoading = ref(false)
const spielplanError = ref('')
const activePanelTab = ref('matches')
const isTableLoading = ref(false)
const tableError = ref('')
const teamTableRows = ref([])
const hasTableLink = computed(() => {
const link = String(mannschaft.value?.weitere_informationen_link || '').trim()
return link.includes('/tabelle/')
})
const spielplanSeasonLabel = computed(() => {
const match = String(spielplanSeason.value || '').match(/^(\d{2})--(\d{2})$/)
return match ? `20${match[1]}/${match[2]}` : ''
})
async function fetchCsvText(url) {
const attempt = async () => {
const withBuster = `${url}${url.includes('?') ? '&' : '?'}_t=${Date.now()}`
const res = await fetch(withBuster, { cache: 'no-store' })
if (!res.ok) return null
return await res.text()
}
try {
return await attempt()
} catch (_e) {
await new Promise(resolve => setTimeout(resolve, 150))
return await attempt()
}
}
const loadMannschaften = async () => {
try {
const csv = await fetchCsvText('/api/mannschaften')
if (!csv) return
const lines = csv.split('\n').filter(line => line.trim() !== '')
if (lines.length < 2) return
const mannschaften = lines.slice(1).map(line => {
// Besserer CSV-Parser: Respektiert Anführungszeichen
const values = []
let current = ''
let inQuotes = false
for (let i = 0; i < line.length; i++) {
const char = line[i]
if (char === '"') {
inQuotes = !inQuotes
} else if (char === ',' && !inQuotes) {
values.push(current.trim())
current = ''
} else {
current += char
}
}
values.push(current.trim())
if (values.length < 10) return null
return {
mannschaft: values[0].trim(),
liga: values[1].trim(),
staffelleiter: values[2].trim(),
telefon: values[3].trim(),
heimspieltag: values[4].trim(),
spielsystem: values[5].trim(),
mannschaftsfuehrer: values[6].trim(),
spieler: values[7].trim(),
weitere_informationen_link: values[8].trim(),
letzte_aktualisierung: values[9].trim(),
slug: values[0].trim().toLowerCase().replace(/\s+/g, '-')
}
}).filter(mannschaft => mannschaft !== null)
// Finde die Mannschaft basierend auf dem Slug
const currentSlug = route.params.slug
mannschaft.value = mannschaften.find(m => m.slug === currentSlug) || null
if (mannschaft.value) {
useHead({
title: `${mannschaft.value.mannschaft} - Harheimer TC`,
})
await Promise.all([loadSpielplan(), loadTeamTable()])
}
} catch (error) {
console.error('Fehler beim Laden der Mannschaften:', error)
}
}
const loadTeamTable = async () => {
if (!mannschaft.value) return
if (!hasTableLink.value) {
teamTableRows.value = []
tableError.value = ''
return
}
isTableLoading.value = true
tableError.value = ''
try {
const params = new URLSearchParams({ team: mannschaft.value.mannschaft })
if (spielplanSeason.value) {
params.set('season', spielplanSeason.value)
}
const response = await fetch(`/api/spielplan/table?${params.toString()}`)
const result = await response.json()
if (!result.success) {
tableError.value = result.message || 'Tabelle konnte nicht geladen werden.'
teamTableRows.value = []
return
}
teamTableRows.value = Array.isArray(result?.table?.table?.leagueTable)
? result.table.table.leagueTable
: []
} catch (error) {
console.error('Fehler beim Laden der Tabelle:', error)
tableError.value = 'Tabelle konnte nicht geladen werden.'
teamTableRows.value = []
} finally {
isTableLoading.value = false
}
}
const getTeamVariants = (cmsMannschaft) => {
const mannschaftMapping = {
'Erwachsene 1': ['harheimer tc'],
'Erwachsene 2': ['harheimer tc ii'],
'Erwachsene 3': ['harheimer tc iii'],
'Erwachsene 4': ['harheimer tc iv'],
'Erwachsene 5': ['harheimer tc v'],
'Jugendmannschaft': ['harheimer tc']
}
return mannschaftMapping[cmsMannschaft] || []
}
const isExactHarheimTeam = (teamName, variant) => {
if (variant === 'harheimer tc') {
return teamName === 'harheimer tc' ||
(teamName.startsWith('harheimer tc ') && !teamName.match(/harheimer tc\s+[ivx]+/i))
}
return teamName === variant || teamName.startsWith(`${variant} `)
}
const isSpielForMannschaft = (row, cmsMannschaft) => {
const variants = getTeamVariants(cmsMannschaft)
if (!variants.length) return false
const heimMannschaft = (row.HeimMannschaft || '').toLowerCase()
const gastMannschaft = (row.GastMannschaft || '').toLowerCase()
const heimAltersklasse = (row.HeimMannschaftAltersklasse || '').toLowerCase()
const gastAltersklasse = (row.GastMannschaftAltersklasse || '').toLowerCase()
const isHarheimerHeim = heimMannschaft.includes('harheimer tc')
const isHarheimerGast = gastMannschaft.includes('harheimer tc')
if (!isHarheimerHeim && !isHarheimerGast) return false
const mannschaftMatch = variants.some((variant) => {
if (isHarheimerHeim && isExactHarheimTeam(heimMannschaft, variant)) return true
if (isHarheimerGast && isExactHarheimTeam(gastMannschaft, variant)) return true
return false
})
if (!mannschaftMatch) return false
if (cmsMannschaft.startsWith('Erwachsene')) {
const isErwachsenenHeim = isHarheimerHeim &&
heimAltersklasse.includes('erwachsene') &&
!heimAltersklasse.includes('jugend')
const isErwachsenenGast = isHarheimerGast &&
gastAltersklasse.includes('erwachsene') &&
!gastAltersklasse.includes('jugend')
return isErwachsenenHeim || isErwachsenenGast
}
if (cmsMannschaft === 'Jugendmannschaft') {
const isJugendHeim = isHarheimerHeim &&
(heimAltersklasse.includes('jugend') || heimMannschaft.includes('jugend'))
const isJugendGast = isHarheimerGast &&
(gastAltersklasse.includes('jugend') || gastMannschaft.includes('jugend'))
return isJugendHeim || isJugendGast
}
return true
}
const parseTerminTimestamp = (row) => {
const timestamp = Number(row.Timestamp)
if (Number.isFinite(timestamp) && timestamp > 0) return timestamp
const termin = String(row.Termin || '')
const match = termin.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})(?:\s+(\d{1,2}):(\d{2}))?/)
if (!match) return 0
const [, day, month, year, hour = '0', minute = '0'] = match
return new Date(Number(year), Number(month) - 1, Number(day), Number(hour), Number(minute)).getTime() / 1000
}
const loadSpielplan = async () => {
if (!mannschaft.value) return
isSpielplanLoading.value = true
spielplanError.value = ''
try {
const response = await fetch('/api/spielplan')
const result = await response.json()
if (!result.success) {
spielplanError.value = result.message || 'Spielplan konnte nicht geladen werden.'
mannschaftSpielplan.value = []
return
}
spielplanSeason.value = result.season || ''
mannschaftSpielplan.value = result.data
.filter(row => isSpielForMannschaft(row, mannschaft.value.mannschaft))
.sort((a, b) => parseTerminTimestamp(a) - parseTerminTimestamp(b))
} catch (error) {
console.error('Fehler beim Laden des Spielplans:', error)
spielplanError.value = 'Spielplan konnte nicht geladen werden.'
mannschaftSpielplan.value = []
} finally {
isSpielplanLoading.value = false
}
}
const getSpielerListe = (mannschaft) => {
if (!mannschaft.spieler) return []
return mannschaft.spieler.split(';').map(s => s.trim()).filter(s => s !== '')
}
const formatDate = (dateString) => {
if (!dateString) return ''
// Wenn bereits im Format DD.MM.YYYY, direkt zurückgeben
if (/^\d{2}\.\d{2}\.\d{4}$/.test(dateString)) {
return dateString
}
// Versuche, das Datum zu parsen
const date = new Date(dateString)
if (isNaN(date.getTime())) {
return dateString // Falls ungültig, Original zurückgeben
}
return date.toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
})
}
const formatResult = (row) => {
const heim = String(row?.SpieleHeim || '').trim()
const gast = String(row?.SpieleGast || '').trim()
return heim || gast ? `${heim}:${gast}` : '-'
}
const formatRunde = (rundeString) => {
if (!rundeString) return '-'
const runde = rundeString.toLowerCase()
if (runde === 'vr') return 'Vorrunde'
if (runde === 'rr') return 'Rückrunde'
if (runde === 'pokal') return 'Pokal'
return rundeString
}
const formatStaffel = (staffelString) => {
const staffel = String(staffelString || '').trim()
if (!staffel) return '-'
return staffel.replace(/^E(?=\d)/, '')
}
const getRowClass = (row) => {
const timestamp = parseTerminTimestamp(row)
if (!timestamp) return 'bg-white'
const spielDatum = new Date(timestamp * 1000)
spielDatum.setHours(0, 0, 0, 0)
const heute = new Date()
heute.setHours(0, 0, 0, 0)
const in7Tagen = new Date(heute)
in7Tagen.setDate(in7Tagen.getDate() + 7)
if (spielDatum.getTime() === heute.getTime()) return 'bg-yellow-100'
if (spielDatum > heute && spielDatum <= in7Tagen) return 'bg-blue-100'
return 'bg-white'
}
const formatSun = (row) => {
const s = row?.meetings_won
const u = row?.meetings_tie
const n = row?.meetings_lost
if (s == null && u == null && n == null) return '-'
return `${s ?? 0}/${u ?? 0}/${n ?? 0}`
}
const formatPunkte = (row) => {
if (row?.points_won == null && row?.points_lost == null) return '-'
return `${row?.points_won ?? 0}:${row?.points_lost ?? 0}`
}
const isCurrentTeamRow = (row) => {
const teamName = String(row?.team_name || '').toLowerCase()
if (!teamName.includes('harheimer tc')) return false
const variants = getTeamVariants(mannschaft.value?.mannschaft || '')
return variants.some((variant) => isExactHarheimTeam(teamName, variant))
}
onMounted(() => {
loadMannschaften()
})
</script>