Android implementation of sportbetrieb, 401-fix
This commit is contained in:
@@ -21,6 +21,7 @@ import okhttp3.RequestBody
|
||||
data class ContactRequest(val name: String, val email: String, val message: String)
|
||||
data class ContactResponse(val ok: Boolean, val id: String? = null, val message: String? = null)
|
||||
data class TermineResponse(val success: Boolean = true, val termine: List<TerminDto> = emptyList())
|
||||
data class TermineManageResponse(val success: Boolean = true, val termine: List<TerminDto> = emptyList())
|
||||
data class TerminDto(
|
||||
val datum: String = "",
|
||||
val uhrzeit: String? = null,
|
||||
@@ -590,6 +591,21 @@ interface ApiService {
|
||||
@GET("/api/termine")
|
||||
suspend fun termine(): Response<TermineResponse>
|
||||
|
||||
@GET("/api/termine-manage")
|
||||
suspend fun termineManage(): Response<TermineManageResponse>
|
||||
|
||||
@POST("/api/termine-manage")
|
||||
suspend fun saveTermin(@Body request: TerminDto): Response<AuthMessageResponse>
|
||||
|
||||
@DELETE("/api/termine-manage")
|
||||
suspend fun deleteTermin(
|
||||
@Query("datum") datum: String,
|
||||
@Query("uhrzeit") uhrzeit: String = "",
|
||||
@Query("titel") titel: String,
|
||||
@Query("beschreibung") beschreibung: String = "",
|
||||
@Query("kategorie") kategorie: String = "Sonstiges",
|
||||
): Response<AuthMessageResponse>
|
||||
|
||||
@GET("/api/spielplan")
|
||||
suspend fun spielplan(@Query("season") season: String? = null): Response<SpielplanResponse>
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ import de.harheimertc.data.PasswordResetDiagnosticsResponse
|
||||
import de.harheimertc.data.SaveCsvRequest
|
||||
import de.harheimertc.data.SaveCsvResponse
|
||||
import de.harheimertc.data.SecureOfflineCache
|
||||
import de.harheimertc.data.SpielplanResponse
|
||||
import de.harheimertc.data.TerminDto
|
||||
import javax.inject.Inject
|
||||
|
||||
class CmsRepository @Inject constructor(
|
||||
@@ -83,6 +85,86 @@ class CmsRepository @Inject constructor(
|
||||
response.body() ?: SaveCsvResponse(success = false, message = "Leere Antwort")
|
||||
}
|
||||
|
||||
suspend fun managedTermine(): Result<List<TerminDto>> = runCatching {
|
||||
val response = api.termineManage()
|
||||
if (!response.isSuccessful) error("Termine konnten nicht geladen werden.")
|
||||
response.body()?.termine.orEmpty()
|
||||
}
|
||||
|
||||
suspend fun saveTermin(request: TerminDto): Result<de.harheimertc.data.AuthMessageResponse> = runCatching {
|
||||
val response = api.saveTermin(request)
|
||||
if (!response.isSuccessful) error("Termin konnte nicht gespeichert werden.")
|
||||
response.body() ?: de.harheimertc.data.AuthMessageResponse(success = false, message = "Leere Antwort")
|
||||
}
|
||||
|
||||
suspend fun deleteTermin(request: TerminDto): Result<de.harheimertc.data.AuthMessageResponse> = runCatching {
|
||||
val response = api.deleteTermin(
|
||||
datum = request.datum,
|
||||
uhrzeit = request.uhrzeit.orEmpty(),
|
||||
titel = request.titel,
|
||||
beschreibung = request.beschreibung.orEmpty(),
|
||||
kategorie = request.kategorie ?: "Sonstiges",
|
||||
)
|
||||
if (!response.isSuccessful) error("Termin konnte nicht gelöscht werden.")
|
||||
response.body() ?: de.harheimertc.data.AuthMessageResponse(success = false, message = "Leere Antwort")
|
||||
}
|
||||
|
||||
suspend fun mannschaften(season: String? = null): Result<List<CmsMannschaftRow>> = runCatching {
|
||||
val response = api.mannschaften(season)
|
||||
if (!response.isSuccessful) error("Mannschaften konnten nicht geladen werden.")
|
||||
parseCsv(response.body()?.string().orEmpty()).drop(1).mapNotNull { values ->
|
||||
if (values.size < 10 || values[0].isBlank()) return@mapNotNull null
|
||||
CmsMannschaftRow(
|
||||
mannschaft = values[0],
|
||||
liga = values[1],
|
||||
staffelleiter = values[2],
|
||||
telefon = values[3],
|
||||
heimspieltag = values[4],
|
||||
spielsystem = values[5],
|
||||
mannschaftsfuehrer = values[6],
|
||||
spieler = values[7],
|
||||
informationenLink = values[8],
|
||||
letzteAktualisierung = values[9],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun mannschaftenSeasons(): Result<de.harheimertc.data.MannschaftenSeasonsResponse> = runCatching {
|
||||
val response = api.mannschaftenSeasons()
|
||||
if (!response.isSuccessful) error("Saisons konnten nicht geladen werden.")
|
||||
response.body() ?: de.harheimertc.data.MannschaftenSeasonsResponse()
|
||||
}
|
||||
|
||||
suspend fun saveMannschaften(season: String?, rows: List<CmsMannschaftRow>): Result<SaveCsvResponse> = runCatching {
|
||||
val response = api.saveCsv(
|
||||
SaveCsvRequest(
|
||||
filename = season?.takeIf { it.isNotBlank() }?.let { "mannschaften_$it.csv" } ?: "mannschaften.csv",
|
||||
content = rows.toMannschaftenCsv(),
|
||||
),
|
||||
)
|
||||
if (!response.isSuccessful) error("Mannschaften konnten nicht gespeichert werden.")
|
||||
response.body() ?: SaveCsvResponse(success = false, message = "Leere Antwort")
|
||||
}
|
||||
|
||||
suspend fun spielplan(season: String? = null): Result<SpielplanResponse> = runCatching {
|
||||
val response = api.spielplan(season)
|
||||
if (!response.isSuccessful) error("Spielplan konnte nicht geladen werden.")
|
||||
val body = response.body() ?: error("Leere Antwort")
|
||||
if (!body.success) error(body.message ?: "Spielplan konnte nicht geladen werden.")
|
||||
body
|
||||
}
|
||||
|
||||
suspend fun saveSpielplan(headers: List<String>, rows: List<List<String>>): Result<SaveCsvResponse> = runCatching {
|
||||
val response = api.saveCsv(
|
||||
SaveCsvRequest(
|
||||
filename = "spielplan.csv",
|
||||
content = listOf(headers).plus(rows).joinToString("\n") { row -> row.toCsvRow(";") },
|
||||
),
|
||||
)
|
||||
if (!response.isSuccessful) error("Spielplan konnte nicht gespeichert werden.")
|
||||
response.body() ?: SaveCsvResponse(success = false, message = "Leere Antwort")
|
||||
}
|
||||
|
||||
suspend fun users(): Result<CmsUsersResponse> =
|
||||
fetchEncryptedFallback(
|
||||
load = {
|
||||
@@ -288,6 +370,58 @@ class CmsRepository @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
data class CmsMannschaftRow(
|
||||
val mannschaft: String = "",
|
||||
val liga: String = "",
|
||||
val staffelleiter: String = "",
|
||||
val telefon: String = "",
|
||||
val heimspieltag: String = "",
|
||||
val spielsystem: String = "",
|
||||
val mannschaftsfuehrer: String = "",
|
||||
val spieler: String = "",
|
||||
val informationenLink: String = "",
|
||||
val letzteAktualisierung: String = "",
|
||||
)
|
||||
|
||||
private fun List<CmsMannschaftRow>.toMannschaftenCsv(): String {
|
||||
val header = listOf(
|
||||
"Mannschaft",
|
||||
"Liga",
|
||||
"Staffelleiter",
|
||||
"Telefon",
|
||||
"Heimspieltag",
|
||||
"Spielsystem",
|
||||
"Mannschaftsführer",
|
||||
"Spieler",
|
||||
"Weitere Informationen Link",
|
||||
"Letzte Aktualisierung",
|
||||
).toCsvRow()
|
||||
val rows = map { row ->
|
||||
listOf(
|
||||
row.mannschaft,
|
||||
row.liga,
|
||||
row.staffelleiter,
|
||||
row.telefon,
|
||||
row.heimspieltag,
|
||||
row.spielsystem,
|
||||
row.mannschaftsfuehrer,
|
||||
row.spieler,
|
||||
row.informationenLink,
|
||||
row.letzteAktualisierung,
|
||||
).toCsvRow()
|
||||
}
|
||||
return listOf(header).plus(rows).joinToString("\n")
|
||||
}
|
||||
|
||||
private fun List<String>.toCsvRow(delimiter: String = ","): String =
|
||||
joinToString(delimiter) { value -> value.csvEscape(delimiter) }
|
||||
|
||||
private fun String.csvEscape(delimiter: String): String {
|
||||
val needsQuotes = contains(delimiter) || contains('"') || contains('\n') || contains('\r')
|
||||
val escaped = replace("\"", "\"\"")
|
||||
return if (needsQuotes) "\"$escaped\"" else escaped
|
||||
}
|
||||
|
||||
private fun parseCsv(csv: String): List<List<String>> =
|
||||
csv.lineSequence().filter(String::isNotBlank).map(::parseCsvLine).toList()
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package de.harheimertc.ui.screens.cms
|
||||
|
||||
import android.content.Intent
|
||||
import android.app.DatePickerDialog
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -36,6 +38,8 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.navigation.NavController
|
||||
@@ -48,10 +52,12 @@ import de.harheimertc.data.NewsletterGroupDto
|
||||
import de.harheimertc.data.PasswordResetMatchingUserDto
|
||||
import de.harheimertc.data.PasswordResetAttemptDto
|
||||
import de.harheimertc.data.PasswordResetStepDto
|
||||
import de.harheimertc.data.TerminDto
|
||||
import de.harheimertc.ui.components.FormMessages
|
||||
import de.harheimertc.ui.components.LoadingState
|
||||
import de.harheimertc.ui.components.NativeRichTextEditor
|
||||
import de.harheimertc.ui.navigation.Destinations
|
||||
import de.harheimertc.repositories.CmsMannschaftRow
|
||||
import de.harheimertc.repositories.MeisterschaftResult
|
||||
import de.harheimertc.ui.theme.Accent100
|
||||
import de.harheimertc.ui.theme.Accent500
|
||||
@@ -61,6 +67,7 @@ import de.harheimertc.ui.theme.Primary100
|
||||
import de.harheimertc.ui.theme.Primary600
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
|
||||
@Composable
|
||||
@@ -432,14 +439,71 @@ fun CmsVereinsmeisterschaftenScreen(navController: NavController, showBackNaviga
|
||||
|
||||
@Composable
|
||||
fun CmsSportbetriebScreen(navController: NavController, showBackNavigation: Boolean, viewModel: CmsViewModel = hiltViewModel()) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
val context = LocalContext.current
|
||||
var activeTab by remember { mutableStateOf("termine") }
|
||||
val mannschaften = remember { mutableStateListOf<CmsMannschaftRow>() }
|
||||
var spielplanCsv by remember { mutableStateOf("") }
|
||||
var spielplanEditorOpen by remember { mutableStateOf(false) }
|
||||
var terminDialogOpen by remember { mutableStateOf(false) }
|
||||
var editingTermin by remember { mutableStateOf<TerminDto?>(null) }
|
||||
var terminDatum by remember { mutableStateOf("") }
|
||||
var terminUhrzeit by remember { mutableStateOf("") }
|
||||
var terminTitel by remember { mutableStateOf("") }
|
||||
var terminBeschreibung by remember { mutableStateOf("") }
|
||||
var terminKategorie by remember { mutableStateOf("Sonstiges") }
|
||||
var terminKategorieOpen by remember { mutableStateOf(false) }
|
||||
val tabs = listOf(
|
||||
"termine" to "Termine",
|
||||
"mannschaften" to "Mannschaften",
|
||||
"spielplaene" to "Spielpläne",
|
||||
)
|
||||
val terminKategorien = listOf("Training", "Punktspiel", "Turnier", "Veranstaltung", "Sonstiges")
|
||||
|
||||
CmsPage(navController, showBackNavigation, "Sportbetrieb", "Termine, Mannschaften und Spielpläne pflegen") {
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.loadSportbetrieb()
|
||||
}
|
||||
|
||||
LaunchedEffect(state.sportMannschaften) {
|
||||
mannschaften.clear()
|
||||
mannschaften.addAll(state.sportMannschaften)
|
||||
}
|
||||
|
||||
LaunchedEffect(state.sportSpielplanHeaders, state.sportSpielplanRows) {
|
||||
spielplanCsv = sportSpielplanCsvText(state.sportSpielplanHeaders, state.sportSpielplanRows)
|
||||
}
|
||||
|
||||
fun openTerminDialog(termin: TerminDto?) {
|
||||
editingTermin = termin
|
||||
terminDatum = termin?.datum.orEmpty()
|
||||
terminUhrzeit = termin?.uhrzeit.orEmpty()
|
||||
terminTitel = termin?.titel.orEmpty()
|
||||
terminBeschreibung = termin?.beschreibung.orEmpty()
|
||||
terminKategorie = termin?.kategorie ?: "Sonstiges"
|
||||
terminDialogOpen = true
|
||||
}
|
||||
|
||||
fun openDatePicker() {
|
||||
val calendar = Calendar.getInstance()
|
||||
runCatching {
|
||||
val parts = terminDatum.split("-")
|
||||
if (parts.size == 3) {
|
||||
calendar.set(parts[0].toInt(), parts[1].toInt() - 1, parts[2].toInt())
|
||||
}
|
||||
}
|
||||
DatePickerDialog(
|
||||
context,
|
||||
{ _, year, month, day ->
|
||||
terminDatum = "%04d-%02d-%02d".format(Locale.ROOT, year, month + 1, day)
|
||||
},
|
||||
calendar.get(Calendar.YEAR),
|
||||
calendar.get(Calendar.MONTH),
|
||||
calendar.get(Calendar.DAY_OF_MONTH),
|
||||
).show()
|
||||
}
|
||||
|
||||
CmsPage(navController, showBackNavigation, "Sportbetrieb", "Termine, Mannschaften und Spielpläne") {
|
||||
if (state.sportLoading) item { LoadingState("Sportbetriebsdaten werden geladen...") }
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -455,33 +519,283 @@ fun CmsSportbetriebScreen(navController: NavController, showBackNavigation: Bool
|
||||
}
|
||||
}
|
||||
|
||||
when (activeTab) {
|
||||
"termine" -> item {
|
||||
DataCard("Termine verwalten") {
|
||||
Text("Termine werden im CMS gepflegt und erscheinen anschließend auf der Terminseite.", color = Accent700)
|
||||
Button(onClick = { navController.navigate(Destinations.Termine.route) }, modifier = Modifier.fillMaxWidth()) {
|
||||
Text("Termine anzeigen")
|
||||
}
|
||||
}
|
||||
}
|
||||
"mannschaften" -> item {
|
||||
DataCard("Mannschaften verwalten") {
|
||||
Text("Mannschaftsdaten und Saisons entsprechen dem CMS-Bereich der Web-Oberfläche.", color = Accent700)
|
||||
Button(onClick = { navController.navigate(Destinations.Mannschaften.route) }, modifier = Modifier.fillMaxWidth()) {
|
||||
Text("Mannschaften öffnen")
|
||||
}
|
||||
}
|
||||
}
|
||||
"spielplaene" -> item {
|
||||
DataCard("Spielpläne") {
|
||||
Text("Spielpläne und Ergebnisse werden aus den importierten Spielplandaten angezeigt.", color = Accent700)
|
||||
Button(onClick = { navController.navigate(Destinations.Spielplan.route) }, modifier = Modifier.fillMaxWidth()) {
|
||||
Text("Spielpläne öffnen")
|
||||
if (!state.sportLoading) {
|
||||
when (activeTab) {
|
||||
"termine" -> {
|
||||
item {
|
||||
Button(
|
||||
onClick = { openTerminDialog(null) },
|
||||
enabled = !state.sportSaving,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text("Termin hinzufügen")
|
||||
}
|
||||
}
|
||||
if (state.sportTermine.isEmpty()) {
|
||||
item { EmptyCard("Keine Termine gefunden.") }
|
||||
}
|
||||
items(state.sportTermine.size) { index ->
|
||||
val termin = state.sportTermine[index]
|
||||
DataCard(termin.titel.ifBlank { "Termin" }) {
|
||||
InfoRow("Datum", listOf(termin.datum, termin.uhrzeit.orEmpty()).filter(String::isNotBlank).joinToString(" "))
|
||||
InfoRow("Kategorie", termin.kategorie ?: "Sonstiges")
|
||||
if (!termin.beschreibung.isNullOrBlank()) {
|
||||
Text(termin.beschreibung, color = Accent700)
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) {
|
||||
OutlinedButton(onClick = { openTerminDialog(termin) }, modifier = Modifier.weight(1f)) {
|
||||
Text("Bearbeiten")
|
||||
}
|
||||
TextButton(
|
||||
onClick = { viewModel.deleteSportTermin(termin) },
|
||||
enabled = !state.sportSaving,
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
Text("Löschen")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"mannschaften" -> {
|
||||
item {
|
||||
if (state.sportMannschaftenSeasons.isNotEmpty()) {
|
||||
DataCard("Saison") {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
state.sportMannschaftenSeasons.forEach { season ->
|
||||
if (season == state.sportMannschaftenSeason) {
|
||||
Button(onClick = { }, modifier = Modifier.weight(1f)) { Text(season) }
|
||||
} else {
|
||||
OutlinedButton(
|
||||
onClick = { viewModel.loadSportMannschaftenSeason(season) },
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
Text(season)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) {
|
||||
Button(
|
||||
onClick = {
|
||||
mannschaften.add(CmsMannschaftRow(letzteAktualisierung = OffsetDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE)))
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
Text("Hinzufügen")
|
||||
}
|
||||
Button(
|
||||
onClick = { viewModel.saveSportMannschaften(state.sportMannschaftenSeason, mannschaften.toList()) },
|
||||
enabled = !state.sportSaving,
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
Text(if (state.sportSaving) "Speichert..." else "Speichern")
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mannschaften.isEmpty()) {
|
||||
item { EmptyCard("Keine Mannschaften gefunden.") }
|
||||
}
|
||||
items(mannschaften.size) { index ->
|
||||
MannschaftEditorCard(
|
||||
row = mannschaften[index],
|
||||
onChange = { updated -> mannschaften[index] = updated },
|
||||
onRemove = { mannschaften.removeAt(index) },
|
||||
)
|
||||
}
|
||||
}
|
||||
"spielplaene" -> {
|
||||
item {
|
||||
DataCard("Vereins-Spielplan (CSV)") {
|
||||
val seasonLabel = state.sportSpielplanSeason.ifBlank { "aktuelle Saison" }
|
||||
val fileName = state.sportSpielplanSeason.takeIf { it.isNotBlank() }?.let { "spielplan-$it.json" } ?: "spielplan.csv"
|
||||
InfoRow("Datei", fileName)
|
||||
InfoRow("Saison", seasonLabel)
|
||||
InfoRow("Einträge", state.sportSpielplanRows.size.toString())
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) {
|
||||
OutlinedButton(
|
||||
onClick = { viewModel.loadSportbetrieb() },
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
Text("Neu laden")
|
||||
}
|
||||
Button(
|
||||
onClick = { spielplanEditorOpen = true },
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
Text("CSV bearbeiten")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
item { FormMessages(state.error, state.message) }
|
||||
}
|
||||
}
|
||||
|
||||
if (terminDialogOpen) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { terminDialogOpen = false },
|
||||
title = { Text(if (editingTermin == null) "Termin hinzufügen" else "Termin bearbeiten") },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedButton(onClick = { openDatePicker() }, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(terminDatum.ifBlank { "Datum auswählen" })
|
||||
}
|
||||
OutlinedTextField(value = terminUhrzeit, onValueChange = { terminUhrzeit = it }, label = { Text("Uhrzeit") }, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(value = terminTitel, onValueChange = { terminTitel = it }, label = { Text("Titel") }, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(value = terminBeschreibung, onValueChange = { terminBeschreibung = it }, label = { Text("Beschreibung") }, modifier = Modifier.fillMaxWidth(), minLines = 3)
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
OutlinedButton(onClick = { terminKategorieOpen = true }, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(terminKategorie.ifBlank { "Kategorie auswählen" })
|
||||
}
|
||||
DropdownMenu(expanded = terminKategorieOpen, onDismissRequest = { terminKategorieOpen = false }) {
|
||||
terminKategorien.forEach { kategorie ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(kategorie) },
|
||||
onClick = {
|
||||
terminKategorie = kategorie
|
||||
terminKategorieOpen = false
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
viewModel.saveSportTermin(
|
||||
editingTermin,
|
||||
TerminDto(
|
||||
datum = terminDatum,
|
||||
uhrzeit = terminUhrzeit.takeIf { it.isNotBlank() },
|
||||
titel = terminTitel,
|
||||
beschreibung = terminBeschreibung.takeIf { it.isNotBlank() },
|
||||
kategorie = terminKategorie.ifBlank { "Sonstiges" },
|
||||
),
|
||||
)
|
||||
terminDialogOpen = false
|
||||
},
|
||||
enabled = !state.sportSaving && terminDatum.isNotBlank() && terminTitel.isNotBlank(),
|
||||
) {
|
||||
Text(if (state.sportSaving) "Speichert..." else "Speichern")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { terminDialogOpen = false }) { Text("Abbrechen") }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (spielplanEditorOpen) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { spielplanEditorOpen = false },
|
||||
title = { Text("Spielplan CSV bearbeiten") },
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
value = spielplanCsv,
|
||||
onValueChange = { spielplanCsv = it },
|
||||
label = { Text("CSV mit Semikolon") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
minLines = 12,
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
val (headers, rows) = parseSportCsvText(spielplanCsv)
|
||||
viewModel.saveSportSpielplan(headers, rows)
|
||||
spielplanEditorOpen = false
|
||||
},
|
||||
enabled = !state.sportSaving && spielplanCsv.isNotBlank(),
|
||||
) {
|
||||
Text(if (state.sportSaving) "Speichert..." else "Speichern")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { spielplanEditorOpen = false }) { Text("Abbrechen") }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MannschaftEditorCard(
|
||||
row: CmsMannschaftRow,
|
||||
onChange: (CmsMannschaftRow) -> Unit,
|
||||
onRemove: () -> Unit,
|
||||
) {
|
||||
DataCard(row.mannschaft.ifBlank { "Mannschaft" }) {
|
||||
OutlinedTextField(value = row.mannschaft, onValueChange = { onChange(row.copy(mannschaft = it)) }, label = { Text("Mannschaft") }, 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.telefon, onValueChange = { onChange(row.copy(telefon = it)) }, label = { Text("Telefon") }, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(value = row.heimspieltag, onValueChange = { onChange(row.copy(heimspieltag = it)) }, label = { Text("Heimspieltag") }, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(value = row.spielsystem, onValueChange = { onChange(row.copy(spielsystem = it)) }, label = { Text("Spielsystem") }, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(value = row.mannschaftsfuehrer, onValueChange = { onChange(row.copy(mannschaftsfuehrer = it)) }, label = { Text("Mannschaftsführer") }, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(value = row.spieler, onValueChange = { onChange(row.copy(spieler = it)) }, label = { Text("Spieler") }, modifier = Modifier.fillMaxWidth(), minLines = 2)
|
||||
OutlinedTextField(value = row.informationenLink, onValueChange = { onChange(row.copy(informationenLink = it)) }, label = { Text("Weitere Informationen Link") }, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(value = row.letzteAktualisierung, onValueChange = { onChange(row.copy(letzteAktualisierung = it)) }, label = { Text("Letzte Aktualisierung") }, modifier = Modifier.fillMaxWidth())
|
||||
TextButton(onClick = onRemove, modifier = Modifier.fillMaxWidth()) {
|
||||
Text("Entfernen")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(";") } }
|
||||
}
|
||||
|
||||
private fun parseSportCsvText(text: String): Pair<List<String>, List<List<String>>> {
|
||||
val lines = text.lineSequence().filter { it.isNotBlank() }.toList()
|
||||
if (lines.isEmpty()) return emptyList<String>() to emptyList()
|
||||
val headers = parseDelimitedLine(lines.first(), ';')
|
||||
val rows = lines.drop(1).map { parseDelimitedLine(it, ';') }
|
||||
return headers to rows
|
||||
}
|
||||
|
||||
private fun parseDelimitedLine(line: String, delimiter: Char): List<String> {
|
||||
val values = mutableListOf<String>()
|
||||
val value = StringBuilder()
|
||||
var quoted = false
|
||||
var index = 0
|
||||
while (index < line.length) {
|
||||
when (val char = line[index]) {
|
||||
'"' -> {
|
||||
if (quoted && index + 1 < line.length && line[index + 1] == '"') {
|
||||
value.append('"')
|
||||
index++
|
||||
} else {
|
||||
quoted = !quoted
|
||||
}
|
||||
}
|
||||
delimiter -> if (quoted) value.append(char) else {
|
||||
values += value.toString()
|
||||
value.clear()
|
||||
}
|
||||
else -> value.append(char)
|
||||
}
|
||||
index++
|
||||
}
|
||||
values += value.toString()
|
||||
return values
|
||||
}
|
||||
|
||||
private fun String.csvCell(delimiter: String): String {
|
||||
val needsQuotes = contains(delimiter) || contains('"') || contains('\n') || contains('\r')
|
||||
val escaped = replace("\"", "\"\"")
|
||||
return if (needsQuotes) "\"$escaped\"" else escaped
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -562,7 +876,7 @@ fun CmsNewsletterScreen(
|
||||
onEdit = { nl ->
|
||||
editingNewsletter = nl
|
||||
nlTitle = nl.title
|
||||
nlContent = nl.title ?: ""
|
||||
nlContent = nl.title
|
||||
nlType = "subscription"
|
||||
nlTargetGroup = ""
|
||||
nlSendToExternal = true
|
||||
@@ -692,6 +1006,12 @@ fun CmsEinstellungenScreen(navController: NavController, showBackNavigation: Boo
|
||||
var websiteVorname by remember { mutableStateOf("") }
|
||||
var websiteNachname by remember { mutableStateOf("") }
|
||||
var websiteEmail by remember { mutableStateOf("") }
|
||||
var ortName by remember { mutableStateOf("") }
|
||||
var ortStrasse by remember { mutableStateOf("") }
|
||||
var ortPlz by remember { mutableStateOf("") }
|
||||
var ortOrt by remember { mutableStateOf("") }
|
||||
val trainingTimes = remember { mutableStateListOf<de.harheimertc.data.TrainingTimeDto>() }
|
||||
val trainers = remember { mutableStateListOf<de.harheimertc.data.TrainerDto>() }
|
||||
|
||||
LaunchedEffect(config) {
|
||||
config?.let {
|
||||
@@ -703,6 +1023,14 @@ fun CmsEinstellungenScreen(navController: NavController, showBackNavigation: Boo
|
||||
websiteVorname = it.website.verantwortlicher.vorname
|
||||
websiteNachname = it.website.verantwortlicher.nachname
|
||||
websiteEmail = it.website.verantwortlicher.email
|
||||
ortName = it.training.ort.name
|
||||
ortStrasse = it.training.ort.strasse
|
||||
ortPlz = it.training.ort.plz
|
||||
ortOrt = it.training.ort.ort
|
||||
trainingTimes.clear()
|
||||
trainingTimes.addAll(it.training.zeiten)
|
||||
trainers.clear()
|
||||
trainers.addAll(it.trainer)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -729,6 +1057,16 @@ fun CmsEinstellungenScreen(navController: NavController, showBackNavigation: Boo
|
||||
email = websiteEmail,
|
||||
),
|
||||
),
|
||||
training = config.training.copy(
|
||||
ort = config.training.ort.copy(
|
||||
name = ortName,
|
||||
strasse = ortStrasse,
|
||||
plz = ortPlz,
|
||||
ort = ortOrt,
|
||||
),
|
||||
zeiten = trainingTimes.toList(),
|
||||
),
|
||||
trainer = trainers.toList(),
|
||||
),
|
||||
)
|
||||
},
|
||||
@@ -761,6 +1099,63 @@ fun CmsEinstellungenScreen(navController: NavController, showBackNavigation: Boo
|
||||
OutlinedTextField(value = websiteEmail, onValueChange = { websiteEmail = it }, label = { Text("E-Mail") }, modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
item {
|
||||
DataCard("Trainingsort") {
|
||||
OutlinedTextField(value = ortName, onValueChange = { ortName = it }, label = { Text("Name") }, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(value = ortStrasse, onValueChange = { ortStrasse = it }, label = { Text("Straße") }, modifier = Modifier.fillMaxWidth())
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.fillMaxWidth()) {
|
||||
OutlinedTextField(value = ortPlz, onValueChange = { ortPlz = it }, label = { Text("PLZ") }, modifier = Modifier.weight(1f))
|
||||
OutlinedTextField(value = ortOrt, onValueChange = { ortOrt = it }, label = { Text("Ort") }, modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
DataCard("Trainingszeiten") {
|
||||
trainingTimes.forEachIndexed { index, zeit ->
|
||||
TrainingTimeEditorCard(
|
||||
zeit = zeit,
|
||||
onChange = { updated -> trainingTimes[index] = updated },
|
||||
onRemove = { trainingTimes.removeAt(index) },
|
||||
)
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
trainingTimes.add(
|
||||
de.harheimertc.data.TrainingTimeDto(
|
||||
id = "training-${System.currentTimeMillis()}",
|
||||
tag = "Montag",
|
||||
),
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text("Trainingszeit hinzufügen")
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
DataCard("Trainer") {
|
||||
trainers.forEachIndexed { index, trainer ->
|
||||
TrainerEditorCard(
|
||||
trainer = trainer,
|
||||
onChange = { updated -> trainers[index] = updated },
|
||||
onRemove = { trainers.removeAt(index) },
|
||||
)
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
trainers.add(
|
||||
de.harheimertc.data.TrainerDto(
|
||||
id = "trainer-${System.currentTimeMillis()}",
|
||||
),
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text("Trainer hinzufügen")
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
DataCard("Systemstatus") {
|
||||
InfoRow("Mitgliedschaftstarife", config.mitgliedschaft.size.toString())
|
||||
|
||||
@@ -13,6 +13,10 @@ import de.harheimertc.data.PasswordResetMatchingUserDto
|
||||
import de.harheimertc.data.PasswordResetAttemptDto
|
||||
import de.harheimertc.data.NewsDto
|
||||
import de.harheimertc.data.NewsSaveRequest
|
||||
import de.harheimertc.data.SeasonDto
|
||||
import de.harheimertc.data.SpielDto
|
||||
import de.harheimertc.data.TerminDto
|
||||
import de.harheimertc.repositories.CmsMannschaftRow
|
||||
import de.harheimertc.repositories.CmsRepository
|
||||
import de.harheimertc.repositories.MeisterschaftResult
|
||||
import kotlinx.coroutines.async
|
||||
@@ -39,6 +43,16 @@ data class CmsUiState(
|
||||
val passwordResetFailedOnly: Boolean = true,
|
||||
val news: List<NewsDto> = emptyList(),
|
||||
val meisterschaften: List<MeisterschaftResult> = emptyList(),
|
||||
val sportLoading: Boolean = false,
|
||||
val sportSaving: Boolean = false,
|
||||
val sportTermine: List<TerminDto> = emptyList(),
|
||||
val sportMannschaften: List<CmsMannschaftRow> = emptyList(),
|
||||
val sportMannschaftenSeasons: List<String> = emptyList(),
|
||||
val sportMannschaftenSeason: String = "",
|
||||
val sportSpielplanHeaders: List<String> = emptyList(),
|
||||
val sportSpielplanRows: List<List<String>> = emptyList(),
|
||||
val sportSpielplanSeason: String = "",
|
||||
val sportSpielplanSeasons: List<SeasonDto> = emptyList(),
|
||||
)
|
||||
|
||||
@HiltViewModel
|
||||
@@ -179,6 +193,156 @@ class CmsViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun loadSportbetrieb() {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(sportLoading = true, error = null, message = null)
|
||||
|
||||
val termineRes = async { repository.managedTermine() }
|
||||
val seasonsRes = async { repository.mannschaftenSeasons() }
|
||||
val spielplanRes = async { repository.spielplan() }
|
||||
|
||||
val termineResult = termineRes.await()
|
||||
val seasonsResult = seasonsRes.await()
|
||||
val seasonInfo = seasonsResult.getOrNull()
|
||||
val selectedSeason = _state.value.sportMannschaftenSeason.takeIf { it.isNotBlank() }
|
||||
?: seasonInfo?.defaultSeason?.takeIf { it.isNotBlank() }
|
||||
?: seasonInfo?.currentSeason?.takeIf { it.isNotBlank() }
|
||||
?: seasonInfo?.seasons?.firstOrNull().orEmpty()
|
||||
val mannschaftenResult = repository.mannschaften(selectedSeason.takeIf { it.isNotBlank() })
|
||||
val spielplanResult = spielplanRes.await()
|
||||
val errors = listOfNotNull(
|
||||
ErrorMapper.mapError(termineResult.exceptionOrNull()),
|
||||
ErrorMapper.mapError(seasonsResult.exceptionOrNull()),
|
||||
ErrorMapper.mapError(mannschaftenResult.exceptionOrNull()),
|
||||
ErrorMapper.mapError(spielplanResult.exceptionOrNull()),
|
||||
)
|
||||
val spielplan = spielplanResult.getOrNull()
|
||||
val headers = spielplan?.headers.orEmpty()
|
||||
|
||||
_state.value = _state.value.copy(
|
||||
sportLoading = false,
|
||||
sportTermine = termineResult.getOrNull().orEmpty(),
|
||||
sportMannschaften = mannschaftenResult.getOrNull().orEmpty(),
|
||||
sportMannschaftenSeasons = seasonInfo?.seasons.orEmpty(),
|
||||
sportMannschaftenSeason = selectedSeason,
|
||||
sportSpielplanHeaders = headers,
|
||||
sportSpielplanRows = spielplan?.data.orEmpty().map { row -> headers.map { header -> row.valueForHeader(header) } },
|
||||
sportSpielplanSeason = spielplan?.season.orEmpty(),
|
||||
sportSpielplanSeasons = spielplan?.seasons.orEmpty(),
|
||||
error = errors.takeIf { it.isNotEmpty() }?.joinToString("; "),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadSportMannschaftenSeason(season: String) {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(sportLoading = true, error = null, message = null, sportMannschaftenSeason = season)
|
||||
repository.mannschaften(season)
|
||||
.onSuccess { rows ->
|
||||
_state.value = _state.value.copy(sportLoading = false, sportMannschaften = rows)
|
||||
}
|
||||
.onFailure { err ->
|
||||
_state.value = _state.value.copy(
|
||||
sportLoading = false,
|
||||
error = ErrorMapper.mapError(err) ?: "Mannschaften konnten nicht geladen werden.",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveSportTermin(original: TerminDto?, termin: TerminDto) {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(sportSaving = true, error = null, message = null)
|
||||
if (original != null) {
|
||||
repository.deleteTermin(original)
|
||||
.onFailure { err ->
|
||||
_state.value = _state.value.copy(
|
||||
sportSaving = false,
|
||||
error = ErrorMapper.mapError(err) ?: "Alter Termin konnte nicht ersetzt werden.",
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
val saveResult = repository.saveTermin(termin)
|
||||
saveResult
|
||||
.onSuccess { response ->
|
||||
val termine = repository.managedTermine().getOrDefault(_state.value.sportTermine)
|
||||
_state.value = _state.value.copy(
|
||||
sportSaving = false,
|
||||
sportTermine = termine,
|
||||
message = response.message ?: "Termin gespeichert.",
|
||||
)
|
||||
}
|
||||
.onFailure { err ->
|
||||
_state.value = _state.value.copy(
|
||||
sportSaving = false,
|
||||
error = ErrorMapper.mapError(err) ?: "Termin konnte nicht gespeichert werden.",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteSportTermin(termin: TerminDto) {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(sportSaving = true, error = null, message = null)
|
||||
repository.deleteTermin(termin)
|
||||
.onSuccess { response ->
|
||||
_state.value = _state.value.copy(
|
||||
sportSaving = false,
|
||||
sportTermine = _state.value.sportTermine.filterNot { it == termin },
|
||||
message = response.message ?: "Termin gelöscht.",
|
||||
)
|
||||
}
|
||||
.onFailure { err ->
|
||||
_state.value = _state.value.copy(
|
||||
sportSaving = false,
|
||||
error = ErrorMapper.mapError(err) ?: "Termin konnte nicht gelöscht werden.",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveSportMannschaften(season: String, rows: List<CmsMannschaftRow>) {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(sportSaving = true, error = null, message = null)
|
||||
repository.saveMannschaften(season.takeIf { it.isNotBlank() }, rows)
|
||||
.onSuccess { response ->
|
||||
_state.value = _state.value.copy(
|
||||
sportSaving = false,
|
||||
sportMannschaften = rows,
|
||||
message = response.message ?: "Mannschaften gespeichert.",
|
||||
)
|
||||
}
|
||||
.onFailure { err ->
|
||||
_state.value = _state.value.copy(
|
||||
sportSaving = false,
|
||||
error = ErrorMapper.mapError(err) ?: "Mannschaften konnten nicht gespeichert werden.",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveSportSpielplan(headers: List<String>, rows: List<List<String>>) {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(sportSaving = true, error = null, message = null)
|
||||
repository.saveSpielplan(headers, rows)
|
||||
.onSuccess { response ->
|
||||
_state.value = _state.value.copy(
|
||||
sportSaving = false,
|
||||
sportSpielplanHeaders = headers,
|
||||
sportSpielplanRows = rows,
|
||||
message = response.message ?: "Spielplan gespeichert.",
|
||||
)
|
||||
}
|
||||
.onFailure { err ->
|
||||
_state.value = _state.value.copy(
|
||||
sportSaving = false,
|
||||
error = ErrorMapper.mapError(err) ?: "Spielplan konnte nicht gespeichert werden.",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveConfig(config: ConfigResponse) {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(saving = true, error = null, message = null)
|
||||
@@ -462,3 +626,18 @@ class CmsViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun SpielDto.valueForHeader(header: String): String = when (header) {
|
||||
"Termin" -> termin
|
||||
"HeimMannschaft" -> heimMannschaft
|
||||
"GastMannschaft" -> gastMannschaft
|
||||
"HeimMannschaftAltersklasse" -> heimAltersklasse
|
||||
"GastMannschaftAltersklasse" -> gastAltersklasse
|
||||
"Altersklasse" -> altersklasse
|
||||
"Liga" -> liga
|
||||
"Staffel" -> staffel
|
||||
"Runde" -> runde.orEmpty()
|
||||
"SpieleHeim" -> spieleHeim
|
||||
"SpieleGast" -> spieleGast
|
||||
else -> ""
|
||||
}
|
||||
|
||||
@@ -2,11 +2,13 @@ package de.harheimertc.ui.screens.cms
|
||||
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
@@ -27,6 +29,12 @@ class CmsViewModelTest {
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
|
||||
private fun viewModel(repo: de.harheimertc.repositories.CmsRepository): CmsViewModel {
|
||||
val connectivity = mockk<de.harheimertc.data.ConnectivityMonitor>()
|
||||
every { connectivity.online } returns MutableStateFlow(true)
|
||||
return CmsViewModel(repo, connectivity)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun load_populatesState() = runTest {
|
||||
val repo = mockk<de.harheimertc.repositories.CmsRepository>()
|
||||
@@ -37,11 +45,11 @@ class CmsViewModelTest {
|
||||
coEvery { repo.contactRequests() } returns Result.success(emptyList())
|
||||
coEvery { repo.newsletters() } returns Result.success(de.harheimertc.data.NewsletterListResponse())
|
||||
coEvery { repo.newsletterGroups() } returns Result.success(de.harheimertc.data.NewsletterGroupsResponse())
|
||||
coEvery { repo.news() } returns Result.success(de.harheimertc.data.NewsResponse(success = true, news = listOf(de.harheimertc.data.NewsDto(id = 5, title = "T", content = "C"))))
|
||||
coEvery { repo.news() } returns Result.success(de.harheimertc.data.NewsResponse(success = true, news = listOf(de.harheimertc.data.NewsDto(id = "5", title = "T", content = "C"))))
|
||||
coEvery { repo.passwordResetDiagnostics(any(), any()) } returns Result.success(de.harheimertc.data.PasswordResetDiagnosticsResponse())
|
||||
coEvery { repo.vereinsmeisterschaften() } returns Result.success(emptyList())
|
||||
|
||||
val vm = CmsViewModel(repo)
|
||||
val vm = viewModel(repo)
|
||||
// advance init launched coroutine
|
||||
dispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
@@ -66,7 +74,7 @@ class CmsViewModelTest {
|
||||
coEvery { repo.vereinsmeisterschaften() } returns Result.success(emptyList())
|
||||
coEvery { repo.saveConfig(any()) } returns Result.success(cfg)
|
||||
coEvery { repo.saveNews(any()) } returns Result.success(de.harheimertc.data.AuthMessageResponse(success = true, message = "ok"))
|
||||
val vm = CmsViewModel(repo)
|
||||
val vm = viewModel(repo)
|
||||
|
||||
// wait for init/load to finish before saving to avoid race
|
||||
dispatcher.scheduler.advanceUntilIdle()
|
||||
@@ -95,7 +103,7 @@ class CmsViewModelTest {
|
||||
coEvery { repo.vereinsmeisterschaften() } returns Result.success(emptyList())
|
||||
coEvery { repo.saveNews(any()) } returns Result.success(de.harheimertc.data.AuthMessageResponse(success = true, message = "saved"))
|
||||
|
||||
val vm = CmsViewModel(repo)
|
||||
val vm = viewModel(repo)
|
||||
dispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
vm.saveNews(de.harheimertc.data.NewsSaveRequest(id = null, title = "t", content = "c"))
|
||||
@@ -122,7 +130,7 @@ class CmsViewModelTest {
|
||||
coEvery { repo.updateUserRoles(any(), any()) } returns Result.success(de.harheimertc.data.AuthMessageResponse(success = true, message = "roles updated"))
|
||||
coEvery { repo.users() } returns Result.success(de.harheimertc.data.CmsUsersResponse(listOf(de.harheimertc.data.CmsUserDto(id = "1", email = "u@e", name = "U", roles = listOf("admin", "vorstand")))))
|
||||
|
||||
val vm = CmsViewModel(repo)
|
||||
val vm = viewModel(repo)
|
||||
dispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
vm.updateUserRoles("1", listOf("admin", "vorstand"))
|
||||
@@ -150,7 +158,7 @@ class CmsViewModelTest {
|
||||
coEvery { repo.updateUserActive(any(), any()) } returns Result.success(de.harheimertc.data.AuthMessageResponse(success = true, message = "user updated"))
|
||||
coEvery { repo.users() } returns Result.success(de.harheimertc.data.CmsUsersResponse(listOf(de.harheimertc.data.CmsUserDto(id = "2", email = "v@e", name = "V", active = false))))
|
||||
|
||||
val vm = CmsViewModel(repo)
|
||||
val vm = viewModel(repo)
|
||||
dispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
vm.setUserActive("2", false)
|
||||
@@ -177,7 +185,7 @@ class CmsViewModelTest {
|
||||
|
||||
coEvery { repo.resendInvite(any()) } returns Result.success(de.harheimertc.data.AuthMessageResponse(success = true, message = "invite sent"))
|
||||
|
||||
val vm = CmsViewModel(repo)
|
||||
val vm = viewModel(repo)
|
||||
dispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
vm.resendInvite("10")
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
<p class="text-sm font-medium text-green-800">
|
||||
{{ currentFile.name }}
|
||||
</p><p class="text-xs text-green-600">
|
||||
{{ currentFile.size }} bytes
|
||||
{{ currentFileLabel }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -368,7 +368,7 @@ const processFile = async (file) => {
|
||||
const parseCSVLine = (line) => { const tabCount = (line.match(/\t/g) || []).length; const semicolonCount = (line.match(/;/g) || []).length; const delimiter = tabCount > semicolonCount ? '\t' : ';'; return line.split(delimiter).map(value => value.trim()) }
|
||||
csvHeaders.value = parseCSVLine(lines[0]); csvData.value = lines.slice(1).map(line => parseCSVLine(line))
|
||||
selectedColumns.value = new Array(csvHeaders.value.length).fill(true); columnsSelected.value = false
|
||||
currentFile.value = { name: file.name, size: file.size, lastModified: file.lastModified }
|
||||
currentFile.value = { name: file.name, size: file.size, entries: csvData.value.length, lastModified: file.lastModified }
|
||||
processingMessage.value = 'Verarbeitung abgeschlossen!'
|
||||
setTimeout(() => { isProcessing.value = false; showUploadModal.value = false }, 1000)
|
||||
} catch (error) { console.error('Fehler:', error); alert('Fehler: ' + error.message); isProcessing.value = false }
|
||||
@@ -377,6 +377,11 @@ const processFile = async (file) => {
|
||||
const processSelectedFile = () => { if (selectedFile.value) processFile(selectedFile.value) }
|
||||
const removeFile = () => { currentFile.value = null; csvData.value = []; csvHeaders.value = []; selectedColumns.value = []; columnsSelected.value = false; filteredCsvData.value = []; filteredCsvHeaders.value = []; if (fileInput.value) fileInput.value.value = '' }
|
||||
const selectedColumnsCount = computed(() => selectedColumns.value.filter(s => s).length)
|
||||
const currentFileLabel = computed(() => {
|
||||
if (!currentFile.value) return ''
|
||||
if (typeof currentFile.value.entries === 'number') return `${currentFile.value.entries} Einträge`
|
||||
return `${currentFile.value.size} bytes`
|
||||
})
|
||||
const getColumnPreview = (index) => { if (csvData.value.length === 0) return 'Keine Daten'; const sv = csvData.value.slice(0, 3).map(row => row[index]).filter(val => val && val.trim() !== ''); return sv.length > 0 ? `Beispiel: ${sv.join(', ')}` : 'Leere Spalte' }
|
||||
const selectAllColumns = () => { selectedColumns.value = selectedColumns.value.map(() => true) }
|
||||
const deselectAllColumns = () => { selectedColumns.value = selectedColumns.value.map(() => false) }
|
||||
@@ -415,7 +420,7 @@ onMounted(() => {
|
||||
csvHeaders.value = result.headers
|
||||
csvData.value = result.data.map(row => csvHeaders.value.map(header => row[header] || ''))
|
||||
selectedColumns.value = new Array(csvHeaders.value.length).fill(true)
|
||||
currentFile.value = { name: result.season ? `spielplan-${result.season}.json` : 'spielplan.csv', size: csvData.value.length, lastModified: null }
|
||||
currentFile.value = { name: result.season ? `spielplan-${result.season}.json` : 'spielplan.csv', entries: csvData.value.length, lastModified: null }
|
||||
} catch { /* ignore */ }
|
||||
})()
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ import { deleteTermin } from '../utils/termine.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const token = getCookie(event, 'auth_token')
|
||||
const token = getCookie(event, 'auth_token') || getHeader(event, 'authorization')?.replace(/^Bearer\s+/i, '')
|
||||
|
||||
if (!token) {
|
||||
throw createError({
|
||||
@@ -58,4 +58,3 @@ export default defineEventHandler(async (event) => {
|
||||
throw error
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { readTermine } from '../utils/termine.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const token = getCookie(event, 'auth_token')
|
||||
const token = getCookie(event, 'auth_token') || getHeader(event, 'authorization')?.replace(/^Bearer\s+/i, '')
|
||||
|
||||
if (!token) {
|
||||
throw createError({
|
||||
@@ -42,4 +42,3 @@ export default defineEventHandler(async (event) => {
|
||||
throw error
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { sendNewEventPush } from '../utils/push-notifications.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const token = getCookie(event, 'auth_token')
|
||||
const token = getCookie(event, 'auth_token') || getHeader(event, 'authorization')?.replace(/^Bearer\s+/i, '')
|
||||
|
||||
if (!token) {
|
||||
throw createError({
|
||||
@@ -63,4 +63,3 @@ export default defineEventHandler(async (event) => {
|
||||
throw error
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user