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

This commit is contained in:
Torsten Schulz (local)
2026-07-17 08:41:11 +02:00
parent f060c9771f
commit bd45beadd5
6 changed files with 1498 additions and 5937 deletions

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

@@ -147,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.")

View File

@@ -619,9 +619,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")

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)

7338
package-lock.json generated Executable file → Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,51 @@
import { getUserFromToken, hasAnyRole } from '../../utils/auth.js'
import { importSpielplan } from '../../utils/spielplan-import.js'
import { importLeagueTables } from '../../utils/spielklassen-tables-import.js'
import { publishImportedSpielplan } from '../../utils/spielplan-publish.js'
import { error as loggerError, info as loggerInfo } from '../../utils/logger.js'
export default defineEventHandler(async (event) => {
let token = getCookie(event, 'auth_token')
if (!token) {
const authorization = getHeader(event, 'authorization')
if (authorization?.startsWith('Bearer ')) token = authorization.substring(7).trim()
}
const user = token ? await getUserFromToken(token) : null
if (!user) {
throw createError({ statusCode: 401, statusMessage: 'Nicht authentifiziert' })
}
if (!hasAnyRole(user, 'admin', 'vorstand')) {
throw createError({ statusCode: 403, statusMessage: 'Keine Berechtigung' })
}
try {
const imported = await importSpielplan()
const published = await publishImportedSpielplan({ inputPath: imported.jsonFile })
let tableMessage = ''
try {
const tables = await importLeagueTables()
tableMessage = ` Tabellen: ${tables.importedCount}/${tables.teamCount}.`
} catch (error) {
loggerError('[cms] Tabellen-Import fehlgeschlagen:', { error })
tableMessage = ' Spielplan wurde aktualisiert; Tabellen konnten nicht aktualisiert werden.'
}
loggerInfo('[cms] Spielplan manuell importiert', {
userId: user.id,
season: published.seasonSlug,
matchCount: imported.matchCount
})
return {
success: true,
message: `Spielplan ${published.seasonSlug} mit ${imported.matchCount} Spielen aktualisiert.${tableMessage}`,
season: published.seasonSlug,
matchCount: imported.matchCount
}
} catch (error) {
loggerError('[cms] Manueller Spielplan-Import fehlgeschlagen:', { error })
throw createError({ statusCode: 502, statusMessage: 'Spielplan konnte nicht von myTischtennis importiert werden' })
}
})