19 Commits

Author SHA1 Message Date
42398dad21 Merge pull request 'dev' (#46) from dev into main
All checks were successful
Code Analysis and Production Deploy / analyze (push) Has been skipped
Code Analysis and Production Deploy / deploy-production (push) Successful in 2m47s
Code Analysis and Production Deploy / deploy-test (push) Has been skipped
Reviewed-on: #46
2026-07-17 14:00:12 +02:00
Torsten Schulz (local)
281c25b05d feat: aktualisiere die Version auf 1.8.4 und verbessere die Erkennung von Jugendmannschaften in den Filterfunktionen
All checks were successful
Code Analysis and Production Deploy / analyze (push) Successful in 8m54s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Successful in 2m41s
2026-07-17 13:46:25 +02:00
Torsten Schulz (local)
1beb5c2eee feat: verbessere die Auswahl von Spielplan-Team-IDs durch Checkboxen und aktualisiere die Logik zur Verarbeitung von Team-Kriterien
All checks were successful
Code Analysis and Production Deploy / analyze (push) Successful in 7m10s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Successful in 2m43s
2026-07-17 12:05:22 +02:00
Torsten Schulz (local)
461f2de8dc feat: unterstütze mehrere Spielplan-Team-IDs und verbessere die Zuordnung importierter Mannschaften
All checks were successful
Code Analysis and Production Deploy / analyze (push) Successful in 7m17s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Successful in 2m46s
2026-07-17 11:49:55 +02:00
Torsten Schulz (local)
75a7c409cd feat: füge Auswahl für Spielplan-Team-ID hinzu und lade Teams von der API
All checks were successful
Code Analysis and Production Deploy / analyze (push) Successful in 6m55s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Successful in 2m40s
2026-07-17 11:20:15 +02:00
Torsten Schulz (local)
751c302036 feat: füge Importfunktion für Spielpläne von myTischtennis hinzu und verbessere den Ladevorgang
All checks were successful
Code Analysis and Production Deploy / analyze (push) Successful in 6m55s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Successful in 2m43s
2026-07-17 11:03:46 +02:00
Torsten Schulz (local)
f0e1ad39b0 Verbessere die Altersklassenerkennung in der Funktion sideMatchesJugendTeam durch Verwendung eines regulären Ausdrucks zur genauen Übereinstimmung
All checks were successful
Code Analysis and Production Deploy / analyze (push) Successful in 7m4s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Successful in 2m55s
2026-07-17 10:33:46 +02:00
Torsten Schulz (local)
1dc84023d0 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
2026-07-17 09:29:34 +02:00
Torsten Schulz (local)
30465c7833 Verbessere Codequalität und Sicherheit durch Refactoring, Hinzufügen von ESLint-Regeln und Aktualisierung der OSV-Scanner-Konfiguration
All checks were successful
Code Analysis and Production Deploy / analyze (push) Successful in 6m45s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Successful in 2m57s
2026-07-17 09:08:42 +02:00
Torsten Schulz (local)
d260f00756 Verbessere Fehlerbehandlung in der Verschlüsselung und PDF-Generierung durch Hinzufügen von Ursacheninformationen
Some checks failed
Code Analysis and Production Deploy / analyze (push) Failing after 6m34s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Has been skipped
2026-07-17 08:52:16 +02:00
Torsten Schulz (local)
f0ac4b7e56 Verbessere Fehlerbehandlung beim Laden von PDF-Vorlagen und füge Ursacheninformationen hinzu
Some checks failed
Code Analysis and Production Deploy / deploy-production (push) Has been cancelled
Code Analysis and Production Deploy / deploy-test (push) Has been cancelled
Code Analysis and Production Deploy / analyze (push) Has been cancelled
2026-07-17 08:50:47 +02:00
Torsten Schulz (local)
bd45beadd5 feat(cms): add endpoint for manual spielplan import with league tables handling
Some checks failed
Code Analysis and Production Deploy / analyze (push) Failing after 3m47s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Has been skipped
2026-07-17 08:41:11 +02:00
Torsten Schulz (local)
f060c9771f Füge Unterstützung für Spielplan-Team-ID in mehreren Komponenten hinzu und aktualisiere CSV-Export
All checks were successful
Code Analysis and Production Deploy / analyze (push) Successful in 7m2s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Successful in 2m15s
2026-07-17 08:29:34 +02:00
d563d81584 Merge pull request 'Füge Funktion zur Umwandlung von Saison-Slugs in Labels hinzu und aktualisiere PDF-Daten mit saisonalen Informationen' (#45) from dev into main
All checks were successful
Code Analysis and Production Deploy / analyze (push) Has been skipped
Code Analysis and Production Deploy / deploy-production (push) Successful in 1m59s
Code Analysis and Production Deploy / deploy-test (push) Has been skipped
Reviewed-on: #45
2026-07-15 11:38:20 +02:00
Torsten Schulz (local)
0ab71166aa Refactor Spielplan-Filterlogik in separate Utility-Funktionen und verbessere die Filterung im PDF-Generierungsprozess 2026-07-15 11:30:06 +02:00
Torsten Schulz (local)
d9c5d242ec Füge Funktion zur Umwandlung von Saison-Slugs in Labels hinzu und aktualisiere PDF-Daten mit saisonalen Informationen
All checks were successful
Code Analysis and Production Deploy / analyze (push) Successful in 7m51s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Successful in 1m56s
2026-07-15 11:27:34 +02:00
380d0a8332 Merge pull request 'dev' (#44) from dev into main
All checks were successful
Code Analysis and Production Deploy / analyze (push) Has been skipped
Code Analysis and Production Deploy / deploy-production (push) Successful in 2m41s
Code Analysis and Production Deploy / deploy-test (push) Has been skipped
Reviewed-on: #44
2026-07-15 11:12:16 +02:00
Torsten Schulz (local)
cb1e7a2fed Aktualisiere Actions-Checkout- und Setup-Versionen in CI-Workflows
All checks were successful
Code Analysis and Production Deploy / analyze (push) Successful in 10m18s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Successful in 3m6s
2026-07-15 10:42:20 +02:00
Torsten Schulz (local)
3586704cce Update file permissions for multiple server and test files to executable mode
Some checks failed
Code Analysis and Production Deploy / analyze (push) Failing after 10m29s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Has been skipped
2026-07-15 08:45:12 +02:00
579 changed files with 2179 additions and 6411 deletions

0
.codex Normal file → Executable file
View File

6
.gitea/workflows/code-analysis.yml Normal file → Executable file
View File

@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with: with:
clean: true clean: true
fetch-depth: 0 fetch-depth: 0
@@ -29,7 +29,7 @@ jobs:
echo "eslint entries in package.json:" && rg '"eslint"' package.json || true echo "eslint entries in package.json:" && rg '"eslint"' package.json || true
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with: with:
node-version: 22 node-version: 22
cache: npm cache: npm
@@ -116,7 +116,7 @@ jobs:
chmod +x osv-scanner chmod +x osv-scanner
./osv-scanner --version ./osv-scanner --version
test -f ./package-lock.json test -f ./package-lock.json
./osv-scanner --lockfile ./package-lock.json ./osv-scanner scan -L ./package-lock.json --config ./.osv-scanner.toml
deploy-production: deploy-production:
runs-on: ubuntu-latest runs-on: ubuntu-latest

0
.gitea/workflows/version-gate.yml Normal file → Executable file
View File

6
.github/workflows/android-ci.yml vendored Normal file → Executable file
View File

@@ -10,14 +10,14 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@v4 uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: '17' java-version: '17'
- name: Cache Gradle - name: Cache Gradle
uses: actions/cache@v4 uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
with: with:
path: | path: |
~/.gradle/caches ~/.gradle/caches

0
.gitignore vendored Normal file → Executable file
View File

0
.gitleaks.toml Normal file → Executable file
View File

0
.gitleaksignore Normal file → Executable file
View File

0
.nvmrc Normal file → Executable file
View File

4
.osv-scanner.toml Normal file
View File

@@ -0,0 +1,4 @@
[[IgnoredVulns]]
id = "GHSA-v3m3-f69x-jf25"
ignoreUntil = 2026-12-31
reason = "Temporary exception: Quill 2.0.3 is required by the current RichTextEditor implementation, and OSV currently reports no fixed version. Track upstream fix and remove this ignore once a patched release is available."

0
.semgrepignore Normal file → Executable file
View File

0
ANDROID_ARCHITECTURE.md Normal file → Executable file
View File

0
ANDROID_KOTLIN_PLAN.md Normal file → Executable file
View File

0
ANDROID_PORT_TODO.md Normal file → Executable file
View File

0
ANDROID_REPO_ENDPOINTS.md Normal file → Executable file
View File

0
AUTH_README.md Normal file → Executable file
View File

0
DATENSCHUTZ.md Normal file → Executable file
View File

0
DATENSCHUTZ_UEBERSICHT.md Normal file → Executable file
View File

0
DEPLOYMENT.md Normal file → Executable file
View File

0
android-app/PLAYSTORE_ASSETS.md Normal file → Executable file
View File

0
android-app/app/build.gradle.kts Normal file → Executable file
View File

View File

0
android-app/app/local/release/app-local-release.aab Normal file → Executable file
View File

Binary file not shown.

0
android-app/app/proguard-rules.pro vendored Normal file → Executable file
View File

0
android-app/app/src/androidTest/AndroidManifest.xml Normal file → Executable file
View File

View File

0
android-app/app/src/debug/AndroidManifest.xml Normal file → Executable file
View File

0
android-app/app/src/main/AndroidManifest.xml Normal file → Executable file
View File

View File

View File

View File

@@ -652,6 +652,9 @@ interface ApiService {
@POST("/api/cms/save-csv") @POST("/api/cms/save-csv")
suspend fun saveCsv(@Body request: SaveCsvRequest): Response<SaveCsvResponse> suspend fun saveCsv(@Body request: SaveCsvRequest): Response<SaveCsvResponse>
@POST("/api/cms/import-spielplan")
suspend fun importSpielplan(): Response<AuthMessageResponse>
@POST("/api/membership/generate-pdf") @POST("/api/membership/generate-pdf")
suspend fun generateMembershipPdf(@Body request: MembershipRequest): Response<MembershipResponse> suspend fun generateMembershipPdf(@Body request: MembershipRequest): Response<MembershipResponse>

View File

View File

View File

View File

View File

View File

@@ -125,6 +125,7 @@ class CmsRepository @Inject constructor(
spieler = values[7], spieler = values[7],
informationenLink = values[8], informationenLink = values[8],
letzteAktualisierung = values[9], letzteAktualisierung = values[9],
spielplanMannschaftId = values.getOrElse(10) { "" },
) )
} }
} }
@@ -146,6 +147,12 @@ class CmsRepository @Inject constructor(
response.body() ?: SaveCsvResponse(success = false, message = "Leere Antwort") response.body() ?: SaveCsvResponse(success = false, message = "Leere Antwort")
} }
suspend fun importSpielplan(): Result<de.harheimertc.data.AuthMessageResponse> = runCatching {
val response = api.importSpielplan()
if (!response.isSuccessful) error("Spielplan konnte nicht importiert werden.")
response.body() ?: de.harheimertc.data.AuthMessageResponse(success = false, message = "Leere Antwort")
}
suspend fun spielplan(season: String? = null): Result<SpielplanResponse> = runCatching { suspend fun spielplan(season: String? = null): Result<SpielplanResponse> = runCatching {
val response = api.spielplan(season) val response = api.spielplan(season)
if (!response.isSuccessful) error("Spielplan konnte nicht geladen werden.") if (!response.isSuccessful) error("Spielplan konnte nicht geladen werden.")
@@ -381,6 +388,7 @@ data class CmsMannschaftRow(
val spieler: String = "", val spieler: String = "",
val informationenLink: String = "", val informationenLink: String = "",
val letzteAktualisierung: String = "", val letzteAktualisierung: String = "",
val spielplanMannschaftId: String = "",
) )
private fun List<CmsMannschaftRow>.toMannschaftenCsv(): String { private fun List<CmsMannschaftRow>.toMannschaftenCsv(): String {
@@ -395,6 +403,7 @@ private fun List<CmsMannschaftRow>.toMannschaftenCsv(): String {
"Spieler", "Spieler",
"Weitere Informationen Link", "Weitere Informationen Link",
"Letzte Aktualisierung", "Letzte Aktualisierung",
"Spielplan Mannschaft ID",
).toCsvRow() ).toCsvRow()
val rows = map { row -> val rows = map { row ->
listOf( listOf(
@@ -408,6 +417,7 @@ private fun List<CmsMannschaftRow>.toMannschaftenCsv(): String {
row.spieler, row.spieler,
row.informationenLink, row.informationenLink,
row.letzteAktualisierung, row.letzteAktualisierung,
row.spielplanMannschaftId,
).toCsvRow() ).toCsvRow()
} }
return listOf(header).plus(rows).joinToString("\n") return listOf(header).plus(rows).joinToString("\n")

View File

View File

View File

@@ -458,6 +458,9 @@ fun CmsSportbetriebScreen(navController: NavController, showBackNavigation: Bool
"mannschaften" to "Mannschaften", "mannschaften" to "Mannschaften",
"spielplaene" to "Spielpläne", "spielplaene" to "Spielpläne",
) )
val spielplanTeamOptions = remember(state.sportSpielplanHeaders, state.sportSpielplanRows) {
importedSpielplanTeams(state.sportSpielplanHeaders, state.sportSpielplanRows)
}
val terminKategorien = listOf("Training", "Punktspiel", "Turnier", "Veranstaltung", "Sonstiges") val terminKategorien = listOf("Training", "Punktspiel", "Turnier", "Veranstaltung", "Sonstiges")
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@@ -606,6 +609,7 @@ fun CmsSportbetriebScreen(navController: NavController, showBackNavigation: Bool
items(mannschaften.size) { index -> items(mannschaften.size) { index ->
MannschaftEditorCard( MannschaftEditorCard(
row = mannschaften[index], row = mannschaften[index],
spielplanTeams = spielplanTeamOptions,
onChange = { updated -> mannschaften[index] = updated }, onChange = { updated -> mannschaften[index] = updated },
onRemove = { mannschaften.removeAt(index) }, onRemove = { mannschaften.removeAt(index) },
) )
@@ -619,9 +623,19 @@ fun CmsSportbetriebScreen(navController: NavController, showBackNavigation: Bool
InfoRow("Datei", fileName) InfoRow("Datei", fileName)
InfoRow("Saison", seasonLabel) InfoRow("Saison", seasonLabel)
InfoRow("Einträge", state.sportSpielplanRows.size.toString()) InfoRow("Einträge", state.sportSpielplanRows.size.toString())
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) {
Button(
onClick = { viewModel.importSportSpielplan() },
enabled = !state.sportSaving,
modifier = Modifier.weight(1f),
) {
Text(if (state.sportSaving) "Importiert..." else "Von myTischtennis importieren")
}
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) { Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) {
OutlinedButton( OutlinedButton(
onClick = { viewModel.loadSportbetrieb() }, onClick = { viewModel.loadSportbetrieb() },
enabled = !state.sportSaving,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
) { ) {
Text("Neu laden") Text("Neu laden")
@@ -732,11 +746,32 @@ fun CmsSportbetriebScreen(navController: NavController, showBackNavigation: Bool
@Composable @Composable
private fun MannschaftEditorCard( private fun MannschaftEditorCard(
row: CmsMannschaftRow, row: CmsMannschaftRow,
spielplanTeams: List<ImportedSpielplanTeam>,
onChange: (CmsMannschaftRow) -> Unit, onChange: (CmsMannschaftRow) -> Unit,
onRemove: () -> Unit, onRemove: () -> Unit,
) { ) {
var teamPickerOpen by remember(row.mannschaft, spielplanTeams) { mutableStateOf(false) }
DataCard(row.mannschaft.ifBlank { "Mannschaft" }) { DataCard(row.mannschaft.ifBlank { "Mannschaft" }) {
OutlinedTextField(value = row.mannschaft, onValueChange = { onChange(row.copy(mannschaft = it)) }, label = { Text("Mannschaft") }, modifier = Modifier.fillMaxWidth()) 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.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.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()) OutlinedTextField(value = row.telefon, onValueChange = { onChange(row.copy(telefon = it)) }, label = { Text("Telefon") }, modifier = Modifier.fillMaxWidth())
@@ -752,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 { private fun sportSpielplanCsvText(headers: List<String>, rows: List<List<String>>): String {
if (headers.isEmpty()) return "" if (headers.isEmpty()) return ""
return listOf(headers).plus(rows).joinToString("\n") { row -> row.joinToString(";") { it.csvCell(";") } } return listOf(headers).plus(rows).joinToString("\n") { row -> row.joinToString(";") { it.csvCell(";") } }

View File

@@ -322,6 +322,33 @@ class CmsViewModel @Inject constructor(
} }
} }
fun importSportSpielplan() {
viewModelScope.launch {
_state.value = _state.value.copy(sportSaving = true, error = null, message = null)
repository.importSpielplan()
.onSuccess { response ->
if (!response.success) {
_state.value = _state.value.copy(
sportSaving = false,
error = response.message ?: "Spielplan konnte nicht importiert werden.",
)
return@onSuccess
}
_state.value = _state.value.copy(
sportSaving = false,
message = response.message ?: "Spielplan aktualisiert.",
)
loadSportbetrieb()
}
.onFailure { err ->
_state.value = _state.value.copy(
sportSaving = false,
error = ErrorMapper.mapError(err) ?: "Spielplan konnte nicht importiert werden.",
)
}
}
}
fun saveSportSpielplan(headers: List<String>, rows: List<List<String>>) { fun saveSportSpielplan(headers: List<String>, rows: List<List<String>>) {
viewModelScope.launch { viewModelScope.launch {
_state.value = _state.value.copy(sportSaving = true, error = null, message = null) _state.value = _state.value.copy(sportSaving = true, error = null, message = null)

Some files were not shown because too many files have changed in this diff Show More