feat: unterstütze mehrere Spielplan-Team-IDs und verbessere die Zuordnung importierter Mannschaften
This commit is contained in:
@@ -203,19 +203,21 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">Spielplan-Team-ID</label>
|
<label class="block text-sm font-medium text-gray-700 mb-2">Spielplan-Team-IDs</label>
|
||||||
<select
|
<select
|
||||||
v-model="formData.spielplan_mannschaft_id"
|
:value="selectedSpielplanTeamIds"
|
||||||
|
multiple
|
||||||
|
size="4"
|
||||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||||
:disabled="isSaving"
|
:disabled="isSaving"
|
||||||
|
@change="updateSpielplanTeamIds"
|
||||||
>
|
>
|
||||||
<option value="">Keine ID auswählen</option>
|
|
||||||
<option v-for="team in spielplanTeams" :key="team.id" :value="team.id">
|
<option v-for="team in spielplanTeams" :key="team.id" :value="team.id">
|
||||||
{{ team.label }}
|
{{ team.label }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<p class="mt-1 text-xs text-gray-500">
|
<p class="mt-1 text-xs text-gray-500">
|
||||||
Nach dem myTischtennis-Import das passende Team auswählen.
|
Nach dem myTischtennis-Import alle passenden Einträge auswählen (z. B. Liga und Pokal). Mit Strg bzw. Cmd lassen sich mehrere Einträge markieren.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -444,6 +446,10 @@ function newSpielerItem(name = '') { return { id: `${Date.now()}-${Math.random()
|
|||||||
function parseSpielerString(s) { if (!s) return []; return String(s).split(';').map(x => x.trim()).filter(Boolean).map(name => newSpielerItem(name)) }
|
function parseSpielerString(s) { if (!s) return []; return String(s).split(';').map(x => x.trim()).filter(Boolean).map(name => newSpielerItem(name)) }
|
||||||
function serializeSpielerList(list) { return (list || []).map(s => (s?.name || '').trim()).filter(Boolean).join('; ') }
|
function serializeSpielerList(list) { return (list || []).map(s => (s?.name || '').trim()).filter(Boolean).join('; ') }
|
||||||
function serializeSpielerNames(names) { return (names || []).map(s => String(s || '').trim()).filter(Boolean).join('; ') }
|
function serializeSpielerNames(names) { return (names || []).map(s => String(s || '').trim()).filter(Boolean).join('; ') }
|
||||||
|
const selectedSpielplanTeamIds = computed(() => String(formData.value.spielplan_mannschaft_id || '').split(',').map(id => id.trim()).filter(Boolean))
|
||||||
|
const updateSpielplanTeamIds = (event) => {
|
||||||
|
formData.value.spielplan_mannschaft_id = Array.from(event.target.selectedOptions, option => option.value).join(',')
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchCsvText(url) {
|
async function fetchCsvText(url) {
|
||||||
const attempt = async () => { const r = await fetch(`${url}${url.includes('?') ? '&' : '?'}_t=${Date.now()}`, { cache: 'no-store' }); if (!r.ok) throw new Error(`HTTP ${r.status}`); return await r.text() }
|
const attempt = async () => { const r = await fetch(`${url}${url.includes('?') ? '&' : '?'}_t=${Date.now()}`, { cache: 'no-store' }); if (!r.ok) throw new Error(`HTTP ${r.status}`); return await r.text() }
|
||||||
@@ -531,7 +537,7 @@ const loadSpielplanTeams = async () => {
|
|||||||
const isHarheimer = String(row[`${side}VereinNr`] || '') === '43030' || String(row[`${side}VereinName`] || '') === 'Harheimer TC'
|
const isHarheimer = String(row[`${side}VereinNr`] || '') === '43030' || String(row[`${side}VereinName`] || '') === 'Harheimer TC'
|
||||||
const id = String(row[`${side}MannschaftId`] || '').trim()
|
const id = String(row[`${side}MannschaftId`] || '').trim()
|
||||||
if (!isHarheimer || !id || teams.has(id)) continue
|
if (!isHarheimer || !id || teams.has(id)) continue
|
||||||
const label = [row[`${side}Mannschaft`], row[`${side}MannschaftAltersklasse`], row[`${side}MannschaftNr`] ? `Nr. ${row[`${side}MannschaftNr`]}` : ''].filter(Boolean).join(' · ')
|
const label = [row[`${side}Mannschaft`], row[`${side}MannschaftAltersklasse`], row.Liga || row.Staffel, row[`${side}MannschaftNr`] ? `Nr. ${row[`${side}MannschaftNr`]}` : ''].filter(Boolean).join(' · ')
|
||||||
teams.set(id, { id, label: `${label} [${id}]` })
|
teams.set(id, { id, label: `${label} [${id}]` })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -558,10 +558,13 @@ const isExactHarheimTeam = (teamName, variant) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isSpielForMannschaft = (row, cmsMannschaft) => {
|
const isSpielForMannschaft = (row, cmsMannschaft) => {
|
||||||
const configuredTeamId = String(mannschaft.value?.spielplan_mannschaft_id || '').trim()
|
const configuredTeamIds = String(mannschaft.value?.spielplan_mannschaft_id || '')
|
||||||
if (configuredTeamId) {
|
.split(',')
|
||||||
return String(row.HeimMannschaftId || '').trim() === configuredTeamId ||
|
.map(id => id.trim())
|
||||||
String(row.GastMannschaftId || '').trim() === configuredTeamId
|
.filter(Boolean)
|
||||||
|
if (configuredTeamIds.length) {
|
||||||
|
return configuredTeamIds.includes(String(row.HeimMannschaftId || '').trim()) ||
|
||||||
|
configuredTeamIds.includes(String(row.GastMannschaftId || '').trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
return rowMatchesTeamFilter(row, cmsMannschaft)
|
return rowMatchesTeamFilter(row, cmsMannschaft)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { getUserFromToken, hasAnyRole } from '../../utils/auth.js'
|
|||||||
import { importSpielplan } from '../../utils/spielplan-import.js'
|
import { importSpielplan } from '../../utils/spielplan-import.js'
|
||||||
import { importLeagueTables } from '../../utils/spielklassen-tables-import.js'
|
import { importLeagueTables } from '../../utils/spielklassen-tables-import.js'
|
||||||
import { publishImportedSpielplan } from '../../utils/spielplan-publish.js'
|
import { publishImportedSpielplan } from '../../utils/spielplan-publish.js'
|
||||||
|
import { synchronizeMannschaftenSpielplanIds } from '../../utils/mannschaften-spielplan-ids.js'
|
||||||
import { error as loggerError, info as loggerInfo } from '../../utils/logger.js'
|
import { error as loggerError, info as loggerInfo } from '../../utils/logger.js'
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
@@ -22,6 +23,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
try {
|
try {
|
||||||
const imported = await importSpielplan()
|
const imported = await importSpielplan()
|
||||||
const published = await publishImportedSpielplan({ inputPath: imported.jsonFile })
|
const published = await publishImportedSpielplan({ inputPath: imported.jsonFile })
|
||||||
|
const mapping = await synchronizeMannschaftenSpielplanIds(published.seasonSlug)
|
||||||
|
|
||||||
let tableMessage = ''
|
let tableMessage = ''
|
||||||
try {
|
try {
|
||||||
@@ -40,9 +42,10 @@ export default defineEventHandler(async (event) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: `Spielplan ${published.seasonSlug} mit ${imported.matchCount} Spielen aktualisiert.${tableMessage}`,
|
message: `Spielplan ${published.seasonSlug} mit ${imported.matchCount} Spielen aktualisiert. ${mapping.updatedTeams} Mannschaften automatisch zugeordnet.${tableMessage}`,
|
||||||
season: published.seasonSlug,
|
season: published.seasonSlug,
|
||||||
matchCount: imported.matchCount
|
matchCount: imported.matchCount,
|
||||||
|
mappedTeams: mapping.updatedTeams
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
loggerError('[cms] Manueller Spielplan-Import fehlgeschlagen:', { error })
|
loggerError('[cms] Manueller Spielplan-Import fehlgeschlagen:', { error })
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { importSpielplan } from '../utils/spielplan-import.js'
|
|||||||
import { importLeagueTables } from '../utils/spielklassen-tables-import.js'
|
import { importLeagueTables } from '../utils/spielklassen-tables-import.js'
|
||||||
import { importQttrValues } from '../utils/qttr-import.js'
|
import { importQttrValues } from '../utils/qttr-import.js'
|
||||||
import { publishImportedSpielplan } from '../utils/spielplan-publish.js'
|
import { publishImportedSpielplan } from '../utils/spielplan-publish.js'
|
||||||
|
import { synchronizeMannschaftenSpielplanIds } from '../utils/mannschaften-spielplan-ids.js'
|
||||||
import { info as loggerInfo, error as loggerError } from '../utils/logger.js'
|
import { info as loggerInfo, error as loggerError } from '../utils/logger.js'
|
||||||
import { cleanupPasswordResetLogs } from '../utils/password-reset-log.js'
|
import { cleanupPasswordResetLogs } from '../utils/password-reset-log.js'
|
||||||
|
|
||||||
@@ -115,9 +116,11 @@ function createSpielplanJob(skipSpielplanImport) {
|
|||||||
loggerInfo(`[spielplan-import] ${reason}: ${spielplan.matchCount} Spiele importiert`, { range: `${spielplan.source.season.dateStart} - ${spielplan.source.season.dateEnd}` })
|
loggerInfo(`[spielplan-import] ${reason}: ${spielplan.matchCount} Spiele importiert`, { range: `${spielplan.source.season.dateStart} - ${spielplan.source.season.dateEnd}` })
|
||||||
|
|
||||||
const published = await publishImportedSpielplan({ inputPath: spielplan.jsonFile })
|
const published = await publishImportedSpielplan({ inputPath: spielplan.jsonFile })
|
||||||
|
const mapping = await synchronizeMannschaftenSpielplanIds(published.seasonSlug)
|
||||||
loggerInfo(`[spielplan-import] ${reason}: Spielplan publiziert`, {
|
loggerInfo(`[spielplan-import] ${reason}: Spielplan publiziert`, {
|
||||||
season: published.seasonSlug,
|
season: published.seasonSlug,
|
||||||
internalPath: published.internalSeasonPath
|
internalPath: published.internalSeasonPath,
|
||||||
|
mappedTeams: mapping.updatedTeams
|
||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
138
server/utils/mannschaften-spielplan-ids.js
Normal file
138
server/utils/mannschaften-spielplan-ids.js
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import { promises as fs } from 'fs'
|
||||||
|
import { getServerDataPath } from './paths.js'
|
||||||
|
import { readSpielplanData } from './spielplan-data.js'
|
||||||
|
|
||||||
|
const CLUB_ID = '43030'
|
||||||
|
|
||||||
|
function parseCsvRows(content) {
|
||||||
|
const rows = []
|
||||||
|
let row = []
|
||||||
|
let value = ''
|
||||||
|
let quoted = false
|
||||||
|
|
||||||
|
for (let index = 0; index < content.length; index += 1) {
|
||||||
|
const char = content[index]
|
||||||
|
if (char === '"') {
|
||||||
|
if (quoted && content[index + 1] === '"') {
|
||||||
|
value += '"'
|
||||||
|
index += 1
|
||||||
|
} else {
|
||||||
|
quoted = !quoted
|
||||||
|
}
|
||||||
|
} else if (char === ',' && !quoted) {
|
||||||
|
row.push(value)
|
||||||
|
value = ''
|
||||||
|
} else if ((char === '\n' || char === '\r') && !quoted) {
|
||||||
|
if (char === '\r' && content[index + 1] === '\n') index += 1
|
||||||
|
row.push(value)
|
||||||
|
if (row.some(cell => cell !== '')) rows.push(row)
|
||||||
|
row = []
|
||||||
|
value = ''
|
||||||
|
} else {
|
||||||
|
value += char
|
||||||
|
}
|
||||||
|
}
|
||||||
|
row.push(value)
|
||||||
|
if (row.some(cell => cell !== '')) rows.push(row)
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
function csvCell(value) {
|
||||||
|
const text = String(value ?? '')
|
||||||
|
return /[",\n\r]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text
|
||||||
|
}
|
||||||
|
|
||||||
|
function romanToNumber(value) {
|
||||||
|
const roman = String(value || '').toUpperCase()
|
||||||
|
const values = { I: 1, V: 5, X: 10 }
|
||||||
|
let result = 0
|
||||||
|
for (let index = 0; index < roman.length; index += 1) {
|
||||||
|
const current = values[roman[index]] || 0
|
||||||
|
const next = values[roman[index + 1]] || 0
|
||||||
|
result += current < next ? -current : current
|
||||||
|
}
|
||||||
|
return result || null
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCmsTeamCriteria(name) {
|
||||||
|
const youth = String(name || '').trim().match(/^jugend\s+j(\d{1,2})(?:\s+([ivx]+|\d+))?\b/i)
|
||||||
|
if (youth) {
|
||||||
|
const rawNumber = youth[2]
|
||||||
|
return {
|
||||||
|
ageClass: `Jugend ${youth[1]}`,
|
||||||
|
teamNumber: rawNumber ? (Number(rawNumber) || romanToNumber(rawNumber)) : null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const adult = String(name || '').trim().match(/^erwachsene\s+(\d+)\b/i)
|
||||||
|
if (adult) return { ageClass: 'Erwachsene', teamNumber: Number(adult[1]) }
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectImportedTeams(rows) {
|
||||||
|
const teams = new Map()
|
||||||
|
for (const row of rows) {
|
||||||
|
for (const side of ['Heim', 'Gast']) {
|
||||||
|
if (String(row[`${side}VereinNr`] || '') !== CLUB_ID) continue
|
||||||
|
const id = String(row[`${side}MannschaftId`] || '').trim()
|
||||||
|
if (!id) continue
|
||||||
|
teams.set(id, {
|
||||||
|
id,
|
||||||
|
ageClass: String(row[`${side}MannschaftAltersklasse`] || '').trim(),
|
||||||
|
teamNumber: Number(row[`${side}MannschaftNr`]) || null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...teams.values()]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds every league and cup team ID matching a CMS team to its CSV row.
|
||||||
|
* A manually chosen ID is retained if no automatic match can be derived.
|
||||||
|
*/
|
||||||
|
export async function synchronizeMannschaftenSpielplanIds(seasonSlug) {
|
||||||
|
const filePath = getServerDataPath('public-data', `mannschaften_${seasonSlug}.csv`)
|
||||||
|
let content
|
||||||
|
try {
|
||||||
|
content = await fs.readFile(filePath, 'utf8')
|
||||||
|
} catch (error) {
|
||||||
|
if (error?.code === 'ENOENT') return { updatedTeams: 0, skipped: true }
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = parseCsvRows(content)
|
||||||
|
if (parsed.length < 2) return { updatedTeams: 0 }
|
||||||
|
const header = parsed[0]
|
||||||
|
let idColumn = header.indexOf('Spielplan Mannschaft ID')
|
||||||
|
if (idColumn < 0) {
|
||||||
|
header.push('Spielplan Mannschaft ID')
|
||||||
|
idColumn = header.length - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
const spielplan = await readSpielplanData({ season: seasonSlug })
|
||||||
|
const importedTeams = collectImportedTeams(spielplan.data)
|
||||||
|
let updatedTeams = 0
|
||||||
|
|
||||||
|
for (const row of parsed.slice(1)) {
|
||||||
|
const criteria = getCmsTeamCriteria(row[0])
|
||||||
|
if (!criteria) continue
|
||||||
|
const ids = importedTeams
|
||||||
|
.filter(team => team.ageClass === criteria.ageClass && (!criteria.teamNumber || team.teamNumber === criteria.teamNumber))
|
||||||
|
.map(team => team.id)
|
||||||
|
.sort((a, b) => Number(a) - Number(b))
|
||||||
|
if (!ids.length) continue
|
||||||
|
const nextValue = ids.join(',')
|
||||||
|
if (row[idColumn] === nextValue) continue
|
||||||
|
row[idColumn] = nextValue
|
||||||
|
updatedTeams += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatedTeams) {
|
||||||
|
const output = `${parsed.map(row => header.map((_, index) => csvCell(row[index])).join(',')).join('\n')}\n`
|
||||||
|
const temporaryPath = `${filePath}.tmp-${process.pid}-${Date.now()}`
|
||||||
|
await fs.writeFile(temporaryPath, output, 'utf8')
|
||||||
|
await fs.rename(temporaryPath, filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { updatedTeams, importedTeamCount: importedTeams.length }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user