feat(cms): add season dropdown/create and restore baelle ratio
This commit is contained in:
@@ -1,21 +1,58 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div class="flex justify-between items-center mb-6 gap-4 flex-wrap">
|
||||
<div>
|
||||
<h2 class="text-2xl sm:text-3xl font-display font-bold text-gray-900 mb-2">
|
||||
Mannschaften verwalten
|
||||
</h2>
|
||||
<div class="w-24 h-1 bg-primary-600" />
|
||||
</div>
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<Plus
|
||||
:size="20"
|
||||
class="mr-2"
|
||||
/> Mannschaft hinzufügen
|
||||
</button>
|
||||
|
||||
<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
|
||||
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"
|
||||
>
|
||||
<Plus
|
||||
:size="20"
|
||||
class="mr-2"
|
||||
/> Mannschaft hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -356,7 +393,10 @@ import { Plus, Trash2, Loader2, AlertCircle, Pencil, Users, ChevronUp, ChevronDo
|
||||
|
||||
const isLoading = ref(true)
|
||||
const isSaving = ref(false)
|
||||
const isCreatingSeason = ref(false)
|
||||
const mannschaften = ref([])
|
||||
const seasons = ref([])
|
||||
const selectedSeason = ref('')
|
||||
const showModal = ref(false)
|
||||
const errorMessage = ref('')
|
||||
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() }
|
||||
}
|
||||
|
||||
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 current = (formData.value.mannschaft || '').trim()
|
||||
const names = mannschaften.value.map(m => (m?.mannschaft || '').trim()).filter(Boolean)
|
||||
@@ -392,7 +471,9 @@ function getPendingSpielerNamesForTeamIndex(teamIndex) {
|
||||
const loadMannschaften = async () => {
|
||||
isLoading.value = true
|
||||
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() !== '')
|
||||
if (lines.length < 2) { mannschaften.value = []; return }
|
||||
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 }
|
||||
}
|
||||
|
||||
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 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() }
|
||||
@@ -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 }
|
||||
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) => {
|
||||
@@ -462,5 +599,8 @@ const confirmDelete = (mannschaft, index) => {
|
||||
} 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>
|
||||
|
||||
@@ -244,6 +244,9 @@
|
||||
<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>
|
||||
@@ -276,6 +279,9 @@
|
||||
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-900">
|
||||
{{ formatSaetze(row) }}
|
||||
</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">
|
||||
{{ formatPunkte(row) }}
|
||||
</td>
|
||||
@@ -672,6 +678,13 @@ const formatSaetze = (row) => {
|
||||
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) => {
|
||||
if (row?.points_won == null && row?.points_lost == null) return '-'
|
||||
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