Compare commits
29 Commits
d563d81584
...
redesign
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3314aa06b4 | ||
|
|
12c909d6a4 | ||
|
|
c2594fd330 | ||
|
|
00b08e2f92 | ||
|
|
93da9fbcbe | ||
|
|
82dcb1da40 | ||
|
|
e82c1d9b0a | ||
|
|
c1c648de74 | ||
|
|
7523023be8 | ||
|
|
2103e9c1e8 | ||
|
|
7aadb5e215 | ||
|
|
76d7000735 | ||
|
|
51e59e85ef | ||
|
|
0475e3084d | ||
|
|
9889430109 | ||
|
|
cb89fdd911 | ||
|
|
281c25b05d | ||
|
|
1beb5c2eee | ||
|
|
461f2de8dc | ||
|
|
75a7c409cd | ||
|
|
751c302036 | ||
|
|
f0e1ad39b0 | ||
|
|
1dc84023d0 | ||
|
|
30465c7833 | ||
|
|
d260f00756 | ||
|
|
f0ac4b7e56 | ||
|
|
bd45beadd5 | ||
|
|
f060c9771f | ||
|
|
0ab71166aa |
@@ -13,7 +13,16 @@ jobs:
|
|||||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||||
with:
|
with:
|
||||||
clean: true
|
clean: true
|
||||||
fetch-depth: 0
|
# Der Analyse-Workflow benötigt keine Historie. Ein flacher Checkout
|
||||||
|
# verhindert, dass der Runner das große Repository-Pack vollständig lädt.
|
||||||
|
fetch-depth: 1
|
||||||
|
# Der Web-Workflow braucht keine Android-Artefakte. Insbesondere liegt
|
||||||
|
# dort ein großer Heap-Dump im aktuellen Commit, der auch bei einem
|
||||||
|
# flachen Checkout übertragen würde und den Runner-Checkout abbricht.
|
||||||
|
sparse-checkout: |
|
||||||
|
/*
|
||||||
|
!/android-app/
|
||||||
|
sparse-checkout-cone-mode: false
|
||||||
|
|
||||||
- name: Ensure clean workspace
|
- name: Ensure clean workspace
|
||||||
run: |
|
run: |
|
||||||
@@ -116,7 +125,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
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -94,6 +94,8 @@ dist
|
|||||||
/android-app/**/build/
|
/android-app/**/build/
|
||||||
/android-app/local.properties
|
/android-app/local.properties
|
||||||
/android-app/gradle-local.properties
|
/android-app/gradle-local.properties
|
||||||
|
# JVM Heap-Dumps sind lokale Diagnoseartefakte und dürfen nie ins Repository.
|
||||||
|
*.hprof
|
||||||
|
|
||||||
# Build output (but keep production data!)
|
# Build output (but keep production data!)
|
||||||
.output
|
.output
|
||||||
|
|||||||
4
.osv-scanner.toml
Normal file
4
.osv-scanner.toml
Normal 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."
|
||||||
@@ -78,12 +78,12 @@ val ensureProductionApiBaseUrl = tasks.register("ensureProductionApiBaseUrl") {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "de.harheimertc"
|
namespace = "de.harheimertc"
|
||||||
compileSdk = 35
|
compileSdk = 36
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "de.harheimertc"
|
applicationId = "de.harheimertc"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 35
|
targetSdk = 36
|
||||||
versionCode = androidVersionCode
|
versionCode = androidVersionCode
|
||||||
versionName = androidVersionName
|
versionName = androidVersionName
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
android-app/app/production/release/app-production-release.aab
Executable file → Normal file
BIN
android-app/app/production/release/app-production-release.aab
Executable file → Normal file
Binary file not shown.
@@ -166,6 +166,7 @@ data class MembershipResponse(
|
|||||||
val success: Boolean = false,
|
val success: Boolean = false,
|
||||||
val message: String? = null,
|
val message: String? = null,
|
||||||
val downloadUrl: String? = null,
|
val downloadUrl: String? = null,
|
||||||
|
val downloadToken: String? = null,
|
||||||
)
|
)
|
||||||
data class LoginRequest(
|
data class LoginRequest(
|
||||||
val email: String,
|
val email: String,
|
||||||
@@ -652,12 +653,18 @@ 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>
|
||||||
|
|
||||||
@Streaming
|
@Streaming
|
||||||
@GET
|
@GET
|
||||||
suspend fun downloadMembershipPdf(@Url downloadUrl: String): Response<ResponseBody>
|
suspend fun downloadMembershipPdf(
|
||||||
|
@Url downloadUrl: String,
|
||||||
|
@retrofit2.http.Header("X-Membership-Download-Token") downloadToken: String? = null,
|
||||||
|
): Response<ResponseBody>
|
||||||
|
|
||||||
@POST("/api/auth/login")
|
@POST("/api/auth/login")
|
||||||
suspend fun login(@Body request: LoginRequest): Response<LoginResponse>
|
suspend fun login(@Body request: LoginRequest): Response<LoginResponse>
|
||||||
|
|||||||
@@ -16,14 +16,16 @@ import de.harheimertc.R
|
|||||||
import de.harheimertc.ui.navigation.Destinations
|
import de.harheimertc.ui.navigation.Destinations
|
||||||
|
|
||||||
object HarheimerNotifications {
|
object HarheimerNotifications {
|
||||||
const val DEFAULT_CHANNEL_ID = "harheimer_tc_updates"
|
// A new ID is intentional: Android does not allow an app update to raise
|
||||||
|
// the importance of an already-created notification channel.
|
||||||
|
const val DEFAULT_CHANNEL_ID = "harheimer_tc_updates_v2"
|
||||||
|
|
||||||
fun createChannels(context: Context) {
|
fun createChannels(context: Context) {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||||
val channel = NotificationChannel(
|
val channel = NotificationChannel(
|
||||||
DEFAULT_CHANNEL_ID,
|
DEFAULT_CHANNEL_ID,
|
||||||
"Harheimer TC",
|
"Harheimer TC",
|
||||||
NotificationManager.IMPORTANCE_DEFAULT,
|
NotificationManager.IMPORTANCE_HIGH,
|
||||||
).apply {
|
).apply {
|
||||||
description = "Benachrichtigungen des Harheimer TC"
|
description = "Benachrichtigungen des Harheimer TC"
|
||||||
}
|
}
|
||||||
@@ -47,7 +49,7 @@ object HarheimerNotifications {
|
|||||||
.setContentTitle(title)
|
.setContentTitle(title)
|
||||||
.setContentText(message)
|
.setContentText(message)
|
||||||
.setStyle(NotificationCompat.BigTextStyle().bigText(message))
|
.setStyle(NotificationCompat.BigTextStyle().bigText(message))
|
||||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
.setContentIntent(createContentIntent(context, notificationId, data))
|
.setContentIntent(createContentIntent(context, notificationId, data))
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
.build()
|
.build()
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import java.io.File
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
data class MembershipDocument(val message: String, val uri: String)
|
data class MembershipDocument(val message: String, val uri: String? = null)
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class MembershipRepository @Inject constructor(
|
class MembershipRepository @Inject constructor(
|
||||||
@@ -18,21 +18,33 @@ class MembershipRepository @Inject constructor(
|
|||||||
) {
|
) {
|
||||||
suspend fun submit(request: MembershipRequest): Result<MembershipDocument> = runCatching {
|
suspend fun submit(request: MembershipRequest): Result<MembershipDocument> = runCatching {
|
||||||
val response = api.generateMembershipPdf(request)
|
val response = api.generateMembershipPdf(request)
|
||||||
if (!response.isSuccessful) error("HTTP ${response.code()}")
|
if (!response.isSuccessful) {
|
||||||
|
val serverMessage = response.errorBody()?.string()
|
||||||
|
?.let { Regex("\\\"(?:statusMessage|message)\\\"\\s*:\\s*\\\"([^\\\"]+)\\\"").find(it)?.groupValues?.getOrNull(1) ?: it }
|
||||||
|
?.takeIf { it.isNotBlank() }
|
||||||
|
error(serverMessage ?: "Der Antrag konnte nicht übermittelt werden (HTTP ${response.code()}).")
|
||||||
|
}
|
||||||
val body = response.body() ?: error("Leere Antwort")
|
val body = response.body() ?: error("Leere Antwort")
|
||||||
if (!body.success) error(body.message ?: "Antrag konnte nicht erstellt werden.")
|
if (!body.success) error(body.message ?: "Antrag konnte nicht erstellt werden.")
|
||||||
val downloadUrl = body.downloadUrl ?: error("PDF-Download fehlt.")
|
val uri = body.downloadUrl?.let { downloadUrl ->
|
||||||
val documentResponse = api.downloadMembershipPdf(downloadUrl)
|
runCatching {
|
||||||
if (!documentResponse.isSuccessful) error("PDF konnte nicht heruntergeladen werden.")
|
val documentResponse = api.downloadMembershipPdf(downloadUrl, body.downloadToken)
|
||||||
val directory = File(context.cacheDir, "membership").apply { mkdirs() }
|
if (!documentResponse.isSuccessful) error("PDF konnte nicht heruntergeladen werden.")
|
||||||
val file = File(directory, "beitrittserklaerung.pdf")
|
val directory = File(context.cacheDir, "membership").apply { mkdirs() }
|
||||||
documentResponse.body()?.byteStream()?.use { input ->
|
val file = File(directory, "beitrittserklaerung.pdf")
|
||||||
file.outputStream().use { output -> input.copyTo(output) }
|
documentResponse.body()?.byteStream()?.use { input ->
|
||||||
} ?: error("Leere PDF-Antwort")
|
file.outputStream().use { output -> input.copyTo(output) }
|
||||||
val uri = FileProvider.getUriForFile(context, "${context.packageName}.files", file)
|
} ?: error("Leere PDF-Antwort")
|
||||||
|
FileProvider.getUriForFile(context, "${context.packageName}.files", file).toString()
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
MembershipDocument(
|
MembershipDocument(
|
||||||
message = body.message ?: "Beitrittsformular erfolgreich erstellt.",
|
message = if (uri == null) {
|
||||||
uri = uri.toString(),
|
"${body.message ?: "Mitgliedschaftsantrag erfolgreich übermittelt."} Das PDF konnte nicht auf diesem Gerät gespeichert werden."
|
||||||
|
} else {
|
||||||
|
body.message ?: "Beitrittsformular erfolgreich erstellt."
|
||||||
|
},
|
||||||
|
uri = uri,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(";") } }
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user