feat: füge Unterstützung für importierte Spielplan-Teams hinzu und verbessere Teamfilter-Logik
Some checks failed
Code Analysis and Production Deploy / analyze (push) Failing after 6m37s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Has been skipped

This commit is contained in:
Torsten Schulz (local)
2026-07-17 09:29:34 +02:00
parent 30465c7833
commit 1dc84023d0
3 changed files with 100 additions and 39 deletions

View File

@@ -458,6 +458,9 @@ fun CmsSportbetriebScreen(navController: NavController, showBackNavigation: Bool
"mannschaften" to "Mannschaften",
"spielplaene" to "Spielpläne",
)
val spielplanTeamOptions = remember(state.sportSpielplanHeaders, state.sportSpielplanRows) {
importedSpielplanTeams(state.sportSpielplanHeaders, state.sportSpielplanRows)
}
val terminKategorien = listOf("Training", "Punktspiel", "Turnier", "Veranstaltung", "Sonstiges")
LaunchedEffect(Unit) {
@@ -606,6 +609,7 @@ fun CmsSportbetriebScreen(navController: NavController, showBackNavigation: Bool
items(mannschaften.size) { index ->
MannschaftEditorCard(
row = mannschaften[index],
spielplanTeams = spielplanTeamOptions,
onChange = { updated -> mannschaften[index] = updated },
onRemove = { mannschaften.removeAt(index) },
)
@@ -742,12 +746,32 @@ fun CmsSportbetriebScreen(navController: NavController, showBackNavigation: Bool
@Composable
private fun MannschaftEditorCard(
row: CmsMannschaftRow,
spielplanTeams: List<ImportedSpielplanTeam>,
onChange: (CmsMannschaftRow) -> Unit,
onRemove: () -> Unit,
) {
var teamPickerOpen by remember(row.mannschaft, spielplanTeams) { mutableStateOf(false) }
DataCard(row.mannschaft.ifBlank { "Mannschaft" }) {
OutlinedTextField(value = row.mannschaft, onValueChange = { onChange(row.copy(mannschaft = it)) }, label = { Text("Mannschaft") }, modifier = Modifier.fillMaxWidth())
OutlinedTextField(value = row.spielplanMannschaftId, onValueChange = { onChange(row.copy(spielplanMannschaftId = it)) }, label = { Text("Spielplan-Team-ID") }, supportingText = { Text("Eindeutige myTischtennis-Team-ID") }, modifier = Modifier.fillMaxWidth())
if (spielplanTeams.isNotEmpty()) {
Box(modifier = Modifier.fillMaxWidth()) {
OutlinedButton(onClick = { teamPickerOpen = true }, modifier = Modifier.fillMaxWidth()) {
Text("Aus importierten Teams auswählen")
}
DropdownMenu(expanded = teamPickerOpen, onDismissRequest = { teamPickerOpen = false }) {
spielplanTeams.forEach { team ->
DropdownMenuItem(
text = { Text(team.label) },
onClick = {
onChange(row.copy(spielplanMannschaftId = team.id))
teamPickerOpen = false
},
)
}
}
}
}
OutlinedTextField(value = row.liga, onValueChange = { onChange(row.copy(liga = it)) }, label = { Text("Liga") }, modifier = Modifier.fillMaxWidth())
OutlinedTextField(value = row.staffelleiter, onValueChange = { onChange(row.copy(staffelleiter = it)) }, label = { Text("Staffelleiter") }, modifier = Modifier.fillMaxWidth())
OutlinedTextField(value = row.telefon, onValueChange = { onChange(row.copy(telefon = it)) }, label = { Text("Telefon") }, modifier = Modifier.fillMaxWidth())
@@ -763,6 +787,35 @@ private fun MannschaftEditorCard(
}
}
private data class ImportedSpielplanTeam(val id: String, val label: String)
private fun importedSpielplanTeams(headers: List<String>, rows: List<List<String>>): List<ImportedSpielplanTeam> {
fun index(name: String) = headers.indexOf(name)
fun value(row: List<String>, column: String): String {
val columnIndex = index(column)
return row.getOrElse(columnIndex) { "" }.trim()
}
val teams = linkedMapOf<String, ImportedSpielplanTeam>()
listOf("Heim", "Gast").forEach { side ->
rows.forEach { row ->
val isHarheimer = value(row, "${side}VereinNr") == "43030" || value(row, "${side}VereinName") == "Harheimer TC"
val id = value(row, "${side}MannschaftId")
if (!isHarheimer || id.isBlank()) return@forEach
val teamName = value(row, "${side}Mannschaft")
val ageClass = value(row, "${side}MannschaftAltersklasse")
val teamNumber = value(row, "${side}MannschaftNr")
val label = listOf(teamName, ageClass, teamNumber.takeIf { it.isNotBlank() }?.let { "Nr. $it" })
.filterNotNull()
.filter { it.isNotBlank() }
.joinToString(" · ")
teams.putIfAbsent(id, ImportedSpielplanTeam(id, "$label [$id]"))
}
}
return teams.values.sortedBy { it.label }
}
private fun sportSpielplanCsvText(headers: List<String>, rows: List<List<String>>): String {
if (headers.isEmpty()) return ""
return listOf(headers).plus(rows).joinToString("\n") { row -> row.joinToString(";") { it.csvCell(";") } }

View File

@@ -362,6 +362,7 @@
<script setup>
import { ref, computed, onMounted } from 'vue'
import { rowMatchesTeamFilter } from '../../utils/spielplan-filter.js'
const route = useRoute()
@@ -563,45 +564,7 @@ const isSpielForMannschaft = (row, cmsMannschaft) => {
String(row.GastMannschaftId || '').trim() === configuredTeamId
}
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
return rowMatchesTeamFilter(row, cmsMannschaft)
}
const parseTerminTimestamp = (row) => {

View File

@@ -39,6 +39,38 @@ function isErwachsenenTeam(teamKey) {
return teamKey === 'erwachsene' || teamKey.startsWith('erwachsene_')
}
function romanToNumber(value) {
const roman = String(value || '').toUpperCase()
if (!/^[IVX]+$/.test(roman)) return null
const values = { I: 1, V: 5, X: 10 }
let total = 0
for (let index = 0; index < roman.length; index += 1) {
const current = values[roman[index]] || 0
const next = values[roman[index + 1]] || 0
total += current < next ? -current : current
}
return total || null
}
function parseJugendTeamKey(teamKey) {
const match = String(teamKey || '').match(/^jugend_?j?(\d{1,2})(?:_([ivx]+|\d+))?$/i)
if (!match) return null
const teamNumber = match[2]
? (Number(match[2]) || romanToNumber(match[2]))
: null
return { age: match[1], teamNumber: teamNumber ? String(teamNumber) : '' }
}
function sideMatchesJugendTeam({ isHarheimer, ageClass, teamNumber }, team) {
if (!isHarheimer) return false
const agePattern = new RegExp(`(?:jugend\\s*|j)${team.age}\\b`, 'i')
if (!agePattern.test(ageClass)) return false
return !team.teamNumber || String(teamNumber || '').trim() === team.teamNumber
}
export function rowMatchesWettbewerbFilter(row, rawWettbewerb) {
const wettbewerb = normalizeText(rawWettbewerb || 'punktrunde')
const runde = normalizeText(row?.Runde)
@@ -79,6 +111,19 @@ export function rowMatchesTeamFilter(row, rawTeam) {
return isJugendHeim || isJugendGast
}
const jugendTeam = parseJugendTeamKey(teamKey)
if (jugendTeam) {
return sideMatchesJugendTeam({
isHarheimer: isHarheimerHeim,
ageClass: heimAltersklasse,
teamNumber: row?.HeimMannschaftNr
}, jugendTeam) || sideMatchesJugendTeam({
isHarheimer: isHarheimerGast,
ageClass: gastAltersklasse,
teamNumber: row?.GastMannschaftNr
}, jugendTeam)
}
const csvVariants = MANNSCHAFT_MAPPING[teamKey] || []
if (csvVariants.length === 0) {
return false