feat(cms): add season dropdown/create and restore baelle ratio
This commit is contained in:
@@ -1,12 +1,48 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="flex justify-between items-center mb-6">
|
<div class="flex justify-between items-center mb-6 gap-4 flex-wrap">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-2xl sm:text-3xl font-display font-bold text-gray-900 mb-2">
|
<h2 class="text-2xl sm:text-3xl font-display font-bold text-gray-900 mb-2">
|
||||||
Mannschaften verwalten
|
Mannschaften verwalten
|
||||||
</h2>
|
</h2>
|
||||||
<div class="w-24 h-1 bg-primary-600" />
|
<div class="w-24 h-1 bg-primary-600" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 flex-wrap">
|
||||||
|
<label class="text-sm font-medium text-gray-700">Saison</label>
|
||||||
|
<select
|
||||||
|
v-model="selectedSeason"
|
||||||
|
class="px-3 py-2 border border-gray-300 rounded-lg bg-white focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||||
|
:disabled="isLoading || isSaving || isCreatingSeason"
|
||||||
|
@change="onSeasonChange"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="season in seasons"
|
||||||
|
:key="season"
|
||||||
|
:value="season"
|
||||||
|
>
|
||||||
|
{{ formatSeasonLabel(season) }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="flex items-center px-4 py-2 bg-white border border-primary-300 text-primary-700 hover:bg-primary-50 font-semibold rounded-lg transition-colors disabled:opacity-50"
|
||||||
|
:disabled="isLoading || isSaving || isCreatingSeason"
|
||||||
|
@click="createNextSeason"
|
||||||
|
>
|
||||||
|
<Loader2
|
||||||
|
v-if="isCreatingSeason"
|
||||||
|
:size="18"
|
||||||
|
class="animate-spin mr-2"
|
||||||
|
/>
|
||||||
|
<Plus
|
||||||
|
v-else
|
||||||
|
:size="18"
|
||||||
|
class="mr-2"
|
||||||
|
/>
|
||||||
|
Neue Saison anlegen
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="flex items-center px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white font-semibold rounded-lg transition-colors"
|
class="flex items-center px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white font-semibold rounded-lg transition-colors"
|
||||||
@click="openAddModal"
|
@click="openAddModal"
|
||||||
@@ -17,6 +53,7 @@
|
|||||||
/> Mannschaft hinzufügen
|
/> Mannschaft hinzufügen
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="isLoading"
|
v-if="isLoading"
|
||||||
@@ -356,7 +393,10 @@ import { Plus, Trash2, Loader2, AlertCircle, Pencil, Users, ChevronUp, ChevronDo
|
|||||||
|
|
||||||
const isLoading = ref(true)
|
const isLoading = ref(true)
|
||||||
const isSaving = ref(false)
|
const isSaving = ref(false)
|
||||||
|
const isCreatingSeason = ref(false)
|
||||||
const mannschaften = ref([])
|
const mannschaften = ref([])
|
||||||
|
const seasons = ref([])
|
||||||
|
const selectedSeason = ref('')
|
||||||
const showModal = ref(false)
|
const showModal = ref(false)
|
||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
const isEditing = ref(false)
|
const isEditing = ref(false)
|
||||||
@@ -376,6 +416,45 @@ async function fetchCsvText(url) {
|
|||||||
try { return await attempt() } catch { await new Promise(r => setTimeout(r, 150)); return await attempt() }
|
try { return await attempt() } catch { await new Promise(r => setTimeout(r, 150)); return await attempt() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getMannschaftenFilenameForSeason(seasonSlug = '') {
|
||||||
|
const season = String(seasonSlug || '').trim()
|
||||||
|
return season ? `mannschaften_${season}.csv` : 'mannschaften.csv'
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSeasonLabel(seasonSlug) {
|
||||||
|
const match = String(seasonSlug || '').match(/^(\d{2})--(\d{2})$/)
|
||||||
|
return match ? `20${match[1]}/${match[2]}` : String(seasonSlug || '')
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNextSeasonSlug(seasonSlug) {
|
||||||
|
const match = String(seasonSlug || '').match(/^(\d{2})--(\d{2})$/)
|
||||||
|
if (!match) return ''
|
||||||
|
const start = Number(match[1])
|
||||||
|
const nextStart = (start + 1) % 100
|
||||||
|
const nextEnd = (nextStart + 1) % 100
|
||||||
|
return `${String(nextStart).padStart(2, '0')}--${String(nextEnd).padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSeasons() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/mannschaften/seasons')
|
||||||
|
const result = await res.json()
|
||||||
|
if (!result?.success || !Array.isArray(result.seasons) || result.seasons.length === 0) {
|
||||||
|
seasons.value = [result?.currentSeason || '']
|
||||||
|
selectedSeason.value = seasons.value[0] || ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
seasons.value = result.seasons
|
||||||
|
if (!selectedSeason.value || !seasons.value.includes(selectedSeason.value)) {
|
||||||
|
selectedSeason.value = result.defaultSeason || seasons.value[0]
|
||||||
|
}
|
||||||
|
} catch (_error) {
|
||||||
|
if (!seasons.value.length) seasons.value = ['']
|
||||||
|
if (!selectedSeason.value) selectedSeason.value = seasons.value[0] || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const mannschaftenSelectOptions = computed(() => {
|
const mannschaftenSelectOptions = computed(() => {
|
||||||
const current = (formData.value.mannschaft || '').trim()
|
const current = (formData.value.mannschaft || '').trim()
|
||||||
const names = mannschaften.value.map(m => (m?.mannschaft || '').trim()).filter(Boolean)
|
const names = mannschaften.value.map(m => (m?.mannschaft || '').trim()).filter(Boolean)
|
||||||
@@ -392,7 +471,9 @@ function getPendingSpielerNamesForTeamIndex(teamIndex) {
|
|||||||
const loadMannschaften = async () => {
|
const loadMannschaften = async () => {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
try {
|
try {
|
||||||
const csv = await fetchCsvText('/api/mannschaften')
|
const params = new URLSearchParams()
|
||||||
|
if (selectedSeason.value) params.set('season', selectedSeason.value)
|
||||||
|
const csv = await fetchCsvText(`/api/mannschaften${params.toString() ? `?${params.toString()}` : ''}`)
|
||||||
const lines = csv.split('\n').filter(line => line.trim() !== '')
|
const lines = csv.split('\n').filter(line => line.trim() !== '')
|
||||||
if (lines.length < 2) { mannschaften.value = []; return }
|
if (lines.length < 2) { mannschaften.value = []; return }
|
||||||
mannschaften.value = lines.slice(1).map(line => {
|
mannschaften.value = lines.slice(1).map(line => {
|
||||||
@@ -405,6 +486,10 @@ const loadMannschaften = async () => {
|
|||||||
} catch (error) { console.error('Fehler beim Laden:', error); errorMessage.value = 'Fehler beim Laden der Mannschaften'; throw error } finally { isLoading.value = false }
|
} catch (error) { console.error('Fehler beim Laden:', error); errorMessage.value = 'Fehler beim Laden der Mannschaften'; throw error } finally { isLoading.value = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onSeasonChange = async () => {
|
||||||
|
await loadMannschaften().catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
const getSpielerListe = (m) => { if (!m.spieler) return []; return m.spieler.split(';').map(s => s.trim()).filter(s => s !== '') }
|
const getSpielerListe = (m) => { if (!m.spieler) return []; return m.spieler.split(';').map(s => s.trim()).filter(s => s !== '') }
|
||||||
const openAddModal = () => { formData.value = { mannschaft: '', liga: '', staffelleiter: '', telefon: '', heimspieltag: '', spielsystem: '', mannschaftsfuehrer: '', spielerListe: [], weitere_informationen_link: '', letzte_aktualisierung: nowIsoDate() }; showModal.value = true; errorMessage.value = ''; isEditing.value = false; editingIndex.value = -1; resetSpielerDraftState() }
|
const openAddModal = () => { formData.value = { mannschaft: '', liga: '', staffelleiter: '', telefon: '', heimspieltag: '', spielsystem: '', mannschaftsfuehrer: '', spielerListe: [], weitere_informationen_link: '', letzte_aktualisierung: nowIsoDate() }; showModal.value = true; errorMessage.value = ''; isEditing.value = false; editingIndex.value = -1; resetSpielerDraftState() }
|
||||||
const closeModal = () => { showModal.value = false; errorMessage.value = ''; isEditing.value = false; editingIndex.value = -1; resetSpielerDraftState() }
|
const closeModal = () => { showModal.value = false; errorMessage.value = ''; isEditing.value = false; editingIndex.value = -1; resetSpielerDraftState() }
|
||||||
@@ -451,7 +536,59 @@ const saveCSV = async () => {
|
|||||||
const esc = (v) => { if (!v) return ''; const s = String(v); if (s.includes(',') || s.includes('"') || s.includes('\n')) return `"${s.replace(/"/g, '""')}"`; return s }
|
const esc = (v) => { if (!v) return ''; const s = String(v); if (s.includes(',') || s.includes('"') || s.includes('\n')) return `"${s.replace(/"/g, '""')}"`; return s }
|
||||||
return [esc(m.mannschaft), esc(m.liga), esc(m.staffelleiter), esc(m.telefon), esc(m.heimspieltag), esc(m.spielsystem), esc(m.mannschaftsfuehrer), esc(m.spieler), esc(m.weitere_informationen_link), esc(m.letzte_aktualisierung)].join(',')
|
return [esc(m.mannschaft), esc(m.liga), esc(m.staffelleiter), esc(m.telefon), esc(m.heimspieltag), esc(m.spielsystem), esc(m.mannschaftsfuehrer), esc(m.spieler), esc(m.weitere_informationen_link), esc(m.letzte_aktualisierung)].join(',')
|
||||||
})
|
})
|
||||||
await $fetch('/api/cms/save-csv', { method: 'POST', body: { filename: 'mannschaften.csv', content: [header, ...rows].join('\n') } })
|
await $fetch('/api/cms/save-csv', {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
filename: getMannschaftenFilenameForSeason(selectedSeason.value),
|
||||||
|
content: [header, ...rows].join('\n')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const createNextSeason = async () => {
|
||||||
|
const baseSeason = selectedSeason.value || seasons.value[0]
|
||||||
|
const nextSeason = getNextSeasonSlug(baseSeason)
|
||||||
|
if (!nextSeason) {
|
||||||
|
errorMessage.value = 'Naechste Saison konnte nicht ermittelt werden.'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seasons.value.includes(nextSeason)) {
|
||||||
|
selectedSeason.value = nextSeason
|
||||||
|
await loadMannschaften().catch(() => {})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isCreatingSeason.value = true
|
||||||
|
errorMessage.value = ''
|
||||||
|
try {
|
||||||
|
const header = 'Mannschaft,Liga,Staffelleiter,Telefon,Heimspieltag,Spielsystem,Mannschaftsführer,Spieler,Weitere Informationen Link,Letzte Aktualisierung'
|
||||||
|
const rows = mannschaften.value.map(m => {
|
||||||
|
const esc = (v) => { if (!v) return ''; const s = String(v); if (s.includes(',') || s.includes('"') || s.includes('\n')) return `"${s.replace(/"/g, '""')}"`; return s }
|
||||||
|
return [esc(m.mannschaft), esc(m.liga), esc(m.staffelleiter), esc(m.telefon), esc(m.heimspieltag), esc(m.spielsystem), esc(m.mannschaftsfuehrer), esc(m.spieler), esc(m.weitere_informationen_link), esc(m.letzte_aktualisierung || nowIsoDate())].join(',')
|
||||||
|
})
|
||||||
|
|
||||||
|
await $fetch('/api/cms/save-csv', {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
filename: getMannschaftenFilenameForSeason(nextSeason),
|
||||||
|
content: [header, ...rows].join('\n')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await loadSeasons()
|
||||||
|
selectedSeason.value = nextSeason
|
||||||
|
await loadMannschaften().catch(() => {})
|
||||||
|
if (window.showSuccessModal) {
|
||||||
|
window.showSuccessModal('Erfolg', `Neue Saison ${formatSeasonLabel(nextSeason)} wurde angelegt.`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Anlegen der Saison:', error)
|
||||||
|
errorMessage.value = error?.data?.statusMessage || error?.statusMessage || 'Saison konnte nicht angelegt werden.'
|
||||||
|
if (window.showErrorModal) window.showErrorModal('Fehler', errorMessage.value)
|
||||||
|
} finally {
|
||||||
|
isCreatingSeason.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmDelete = (mannschaft, index) => {
|
const confirmDelete = (mannschaft, index) => {
|
||||||
@@ -462,5 +599,8 @@ const confirmDelete = (mannschaft, index) => {
|
|||||||
} else { if (confirm(`Mannschaft "${mannschaft.mannschaft}" wirklich löschen?`)) { mannschaften.value.splice(index, 1); saveCSV(); loadMannschaften() } }
|
} else { if (confirm(`Mannschaft "${mannschaft.mannschaft}" wirklich löschen?`)) { mannschaften.value.splice(index, 1); saveCSV(); loadMannschaften() } }
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => { loadMannschaften().catch(() => {}) })
|
onMounted(async () => {
|
||||||
|
await loadSeasons()
|
||||||
|
await loadMannschaften().catch(() => {})
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -244,6 +244,9 @@
|
|||||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Sätze
|
Sätze
|
||||||
</th>
|
</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">
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Punkte
|
Punkte
|
||||||
</th>
|
</th>
|
||||||
@@ -276,6 +279,9 @@
|
|||||||
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-900">
|
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-900">
|
||||||
{{ formatSaetze(row) }}
|
{{ formatSaetze(row) }}
|
||||||
</td>
|
</td>
|
||||||
|
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-900">
|
||||||
|
{{ formatBaelle(row) }}
|
||||||
|
</td>
|
||||||
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-900">
|
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-900">
|
||||||
{{ formatPunkte(row) }}
|
{{ formatPunkte(row) }}
|
||||||
</td>
|
</td>
|
||||||
@@ -672,6 +678,13 @@ const formatSaetze = (row) => {
|
|||||||
return `${won ?? 0}:${lost ?? 0}`
|
return `${won ?? 0}:${lost ?? 0}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formatBaelle = (row) => {
|
||||||
|
const won = row?.games_won
|
||||||
|
const lost = row?.games_lost
|
||||||
|
if (won == null && lost == null) return row?.games_relation || '-'
|
||||||
|
return `${won ?? 0}:${lost ?? 0}`
|
||||||
|
}
|
||||||
|
|
||||||
const formatPunkte = (row) => {
|
const formatPunkte = (row) => {
|
||||||
if (row?.points_won == null && row?.points_lost == null) return '-'
|
if (row?.points_won == null && row?.points_lost == null) return '-'
|
||||||
return `${row?.points_won ?? 0}:${row?.points_lost ?? 0}`
|
return `${row?.points_won ?? 0}:${row?.points_lost ?? 0}`
|
||||||
|
|||||||
55
server/api/mannschaften/seasons.get.js
Normal file
55
server/api/mannschaften/seasons.get.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { promises as fs } from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import { getCurrentSeasonSlug } from '../../utils/spielplan-data.js'
|
||||||
|
|
||||||
|
const SEASON_FILE_PATTERN = /^mannschaften_(\d{2}--\d{2})\.csv$/
|
||||||
|
|
||||||
|
async function collectSeasonFiles(dirPath) {
|
||||||
|
try {
|
||||||
|
const files = await fs.readdir(dirPath)
|
||||||
|
return files
|
||||||
|
.map((name) => {
|
||||||
|
const match = name.match(SEASON_FILE_PATTERN)
|
||||||
|
return match ? match[1] : null
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
} catch (error) {
|
||||||
|
if (error?.code === 'ENOENT') return []
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineEventHandler(async () => {
|
||||||
|
try {
|
||||||
|
const cwd = process.cwd()
|
||||||
|
const dirs = [
|
||||||
|
path.join(cwd, 'server/data/public-data'),
|
||||||
|
path.join(cwd, '../server/data/public-data'),
|
||||||
|
path.join(cwd, '.output/public/data'),
|
||||||
|
path.join(cwd, 'public/data')
|
||||||
|
]
|
||||||
|
|
||||||
|
const seasonLists = await Promise.all(dirs.map((dir) => collectSeasonFiles(dir)))
|
||||||
|
const allSeasons = [...new Set(seasonLists.flat())].sort().reverse()
|
||||||
|
|
||||||
|
const currentSeason = getCurrentSeasonSlug()
|
||||||
|
const seasons = allSeasons.includes(currentSeason)
|
||||||
|
? allSeasons
|
||||||
|
: [currentSeason, ...allSeasons]
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
seasons,
|
||||||
|
currentSeason,
|
||||||
|
defaultSeason: seasons[0] || currentSeason
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Laden der Mannschafts-Saisons:', error)
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
seasons: [getCurrentSeasonSlug()],
|
||||||
|
currentSeason: getCurrentSeasonSlug(),
|
||||||
|
defaultSeason: getCurrentSeasonSlug()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user