dev #42
@@ -21,6 +21,7 @@ import okhttp3.RequestBody
|
|||||||
data class ContactRequest(val name: String, val email: String, val message: String)
|
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 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 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(
|
data class TerminDto(
|
||||||
val datum: String = "",
|
val datum: String = "",
|
||||||
val uhrzeit: String? = null,
|
val uhrzeit: String? = null,
|
||||||
@@ -590,6 +591,21 @@ interface ApiService {
|
|||||||
@GET("/api/termine")
|
@GET("/api/termine")
|
||||||
suspend fun termine(): Response<TermineResponse>
|
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")
|
@GET("/api/spielplan")
|
||||||
suspend fun spielplan(@Query("season") season: String? = null): Response<SpielplanResponse>
|
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.SaveCsvRequest
|
||||||
import de.harheimertc.data.SaveCsvResponse
|
import de.harheimertc.data.SaveCsvResponse
|
||||||
import de.harheimertc.data.SecureOfflineCache
|
import de.harheimertc.data.SecureOfflineCache
|
||||||
|
import de.harheimertc.data.SpielplanResponse
|
||||||
|
import de.harheimertc.data.TerminDto
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class CmsRepository @Inject constructor(
|
class CmsRepository @Inject constructor(
|
||||||
@@ -83,6 +85,86 @@ class CmsRepository @Inject constructor(
|
|||||||
response.body() ?: SaveCsvResponse(success = false, message = "Leere Antwort")
|
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> =
|
suspend fun users(): Result<CmsUsersResponse> =
|
||||||
fetchEncryptedFallback(
|
fetchEncryptedFallback(
|
||||||
load = {
|
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>> =
|
private fun parseCsv(csv: String): List<List<String>> =
|
||||||
csv.lineSequence().filter(String::isNotBlank).map(::parseCsvLine).toList()
|
csv.lineSequence().filter(String::isNotBlank).map(::parseCsvLine).toList()
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package de.harheimertc.ui.screens.cms
|
package de.harheimertc.ui.screens.cms
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.app.DatePickerDialog
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -36,6 +38,8 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
@@ -48,10 +52,12 @@ import de.harheimertc.data.NewsletterGroupDto
|
|||||||
import de.harheimertc.data.PasswordResetMatchingUserDto
|
import de.harheimertc.data.PasswordResetMatchingUserDto
|
||||||
import de.harheimertc.data.PasswordResetAttemptDto
|
import de.harheimertc.data.PasswordResetAttemptDto
|
||||||
import de.harheimertc.data.PasswordResetStepDto
|
import de.harheimertc.data.PasswordResetStepDto
|
||||||
|
import de.harheimertc.data.TerminDto
|
||||||
import de.harheimertc.ui.components.FormMessages
|
import de.harheimertc.ui.components.FormMessages
|
||||||
import de.harheimertc.ui.components.LoadingState
|
import de.harheimertc.ui.components.LoadingState
|
||||||
import de.harheimertc.ui.components.NativeRichTextEditor
|
import de.harheimertc.ui.components.NativeRichTextEditor
|
||||||
import de.harheimertc.ui.navigation.Destinations
|
import de.harheimertc.ui.navigation.Destinations
|
||||||
|
import de.harheimertc.repositories.CmsMannschaftRow
|
||||||
import de.harheimertc.repositories.MeisterschaftResult
|
import de.harheimertc.repositories.MeisterschaftResult
|
||||||
import de.harheimertc.ui.theme.Accent100
|
import de.harheimertc.ui.theme.Accent100
|
||||||
import de.harheimertc.ui.theme.Accent500
|
import de.harheimertc.ui.theme.Accent500
|
||||||
@@ -61,6 +67,7 @@ import de.harheimertc.ui.theme.Primary100
|
|||||||
import de.harheimertc.ui.theme.Primary600
|
import de.harheimertc.ui.theme.Primary600
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.util.Calendar
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -432,14 +439,71 @@ fun CmsVereinsmeisterschaftenScreen(navController: NavController, showBackNaviga
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CmsSportbetriebScreen(navController: NavController, showBackNavigation: Boolean, viewModel: CmsViewModel = hiltViewModel()) {
|
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") }
|
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(
|
val tabs = listOf(
|
||||||
"termine" to "Termine",
|
"termine" to "Termine",
|
||||||
"mannschaften" to "Mannschaften",
|
"mannschaften" to "Mannschaften",
|
||||||
"spielplaene" to "Spielpläne",
|
"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 {
|
item {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -455,33 +519,283 @@ fun CmsSportbetriebScreen(navController: NavController, showBackNavigation: Bool
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!state.sportLoading) {
|
||||||
when (activeTab) {
|
when (activeTab) {
|
||||||
"termine" -> item {
|
"termine" -> {
|
||||||
DataCard("Termine verwalten") {
|
item {
|
||||||
Text("Termine werden im CMS gepflegt und erscheinen anschließend auf der Terminseite.", color = Accent700)
|
Button(
|
||||||
Button(onClick = { navController.navigate(Destinations.Termine.route) }, modifier = Modifier.fillMaxWidth()) {
|
onClick = { openTerminDialog(null) },
|
||||||
Text("Termine anzeigen")
|
enabled = !state.sportSaving,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Text("Termin hinzufügen")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (state.sportTermine.isEmpty()) {
|
||||||
|
item { EmptyCard("Keine Termine gefunden.") }
|
||||||
}
|
}
|
||||||
"mannschaften" -> item {
|
items(state.sportTermine.size) { index ->
|
||||||
DataCard("Mannschaften verwalten") {
|
val termin = state.sportTermine[index]
|
||||||
Text("Mannschaftsdaten und Saisons entsprechen dem CMS-Bereich der Web-Oberfläche.", color = Accent700)
|
DataCard(termin.titel.ifBlank { "Termin" }) {
|
||||||
Button(onClick = { navController.navigate(Destinations.Mannschaften.route) }, modifier = Modifier.fillMaxWidth()) {
|
InfoRow("Datum", listOf(termin.datum, termin.uhrzeit.orEmpty()).filter(String::isNotBlank).joinToString(" "))
|
||||||
Text("Mannschaften öffnen")
|
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(
|
||||||
"spielplaene" -> item {
|
onClick = { viewModel.deleteSportTermin(termin) },
|
||||||
DataCard("Spielpläne") {
|
enabled = !state.sportSaving,
|
||||||
Text("Spielpläne und Ergebnisse werden aus den importierten Spielplandaten angezeigt.", color = Accent700)
|
modifier = Modifier.weight(1f),
|
||||||
Button(onClick = { navController.navigate(Destinations.Spielplan.route) }, modifier = Modifier.fillMaxWidth()) {
|
) {
|
||||||
Text("Spielpläne öffnen")
|
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
|
@Composable
|
||||||
@@ -562,7 +876,7 @@ fun CmsNewsletterScreen(
|
|||||||
onEdit = { nl ->
|
onEdit = { nl ->
|
||||||
editingNewsletter = nl
|
editingNewsletter = nl
|
||||||
nlTitle = nl.title
|
nlTitle = nl.title
|
||||||
nlContent = nl.title ?: ""
|
nlContent = nl.title
|
||||||
nlType = "subscription"
|
nlType = "subscription"
|
||||||
nlTargetGroup = ""
|
nlTargetGroup = ""
|
||||||
nlSendToExternal = true
|
nlSendToExternal = true
|
||||||
@@ -692,6 +1006,12 @@ fun CmsEinstellungenScreen(navController: NavController, showBackNavigation: Boo
|
|||||||
var websiteVorname by remember { mutableStateOf("") }
|
var websiteVorname by remember { mutableStateOf("") }
|
||||||
var websiteNachname by remember { mutableStateOf("") }
|
var websiteNachname by remember { mutableStateOf("") }
|
||||||
var websiteEmail 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) {
|
LaunchedEffect(config) {
|
||||||
config?.let {
|
config?.let {
|
||||||
@@ -703,6 +1023,14 @@ fun CmsEinstellungenScreen(navController: NavController, showBackNavigation: Boo
|
|||||||
websiteVorname = it.website.verantwortlicher.vorname
|
websiteVorname = it.website.verantwortlicher.vorname
|
||||||
websiteNachname = it.website.verantwortlicher.nachname
|
websiteNachname = it.website.verantwortlicher.nachname
|
||||||
websiteEmail = it.website.verantwortlicher.email
|
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,
|
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())
|
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 {
|
item {
|
||||||
DataCard("Systemstatus") {
|
DataCard("Systemstatus") {
|
||||||
InfoRow("Mitgliedschaftstarife", config.mitgliedschaft.size.toString())
|
InfoRow("Mitgliedschaftstarife", config.mitgliedschaft.size.toString())
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ import de.harheimertc.data.PasswordResetMatchingUserDto
|
|||||||
import de.harheimertc.data.PasswordResetAttemptDto
|
import de.harheimertc.data.PasswordResetAttemptDto
|
||||||
import de.harheimertc.data.NewsDto
|
import de.harheimertc.data.NewsDto
|
||||||
import de.harheimertc.data.NewsSaveRequest
|
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.CmsRepository
|
||||||
import de.harheimertc.repositories.MeisterschaftResult
|
import de.harheimertc.repositories.MeisterschaftResult
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
@@ -39,6 +43,16 @@ data class CmsUiState(
|
|||||||
val passwordResetFailedOnly: Boolean = true,
|
val passwordResetFailedOnly: Boolean = true,
|
||||||
val news: List<NewsDto> = emptyList(),
|
val news: List<NewsDto> = emptyList(),
|
||||||
val meisterschaften: List<MeisterschaftResult> = 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
|
@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) {
|
fun saveConfig(config: ConfigResponse) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_state.value = _state.value.copy(saving = true, error = null, message = null)
|
_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.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||||
import kotlinx.coroutines.test.resetMain
|
import kotlinx.coroutines.test.resetMain
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import kotlinx.coroutines.test.setMain
|
import kotlinx.coroutines.test.setMain
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
@@ -27,6 +29,12 @@ class CmsViewModelTest {
|
|||||||
Dispatchers.resetMain()
|
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
|
@Test
|
||||||
fun load_populatesState() = runTest {
|
fun load_populatesState() = runTest {
|
||||||
val repo = mockk<de.harheimertc.repositories.CmsRepository>()
|
val repo = mockk<de.harheimertc.repositories.CmsRepository>()
|
||||||
@@ -37,11 +45,11 @@ class CmsViewModelTest {
|
|||||||
coEvery { repo.contactRequests() } returns Result.success(emptyList())
|
coEvery { repo.contactRequests() } returns Result.success(emptyList())
|
||||||
coEvery { repo.newsletters() } returns Result.success(de.harheimertc.data.NewsletterListResponse())
|
coEvery { repo.newsletters() } returns Result.success(de.harheimertc.data.NewsletterListResponse())
|
||||||
coEvery { repo.newsletterGroups() } returns Result.success(de.harheimertc.data.NewsletterGroupsResponse())
|
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.passwordResetDiagnostics(any(), any()) } returns Result.success(de.harheimertc.data.PasswordResetDiagnosticsResponse())
|
||||||
coEvery { repo.vereinsmeisterschaften() } returns Result.success(emptyList())
|
coEvery { repo.vereinsmeisterschaften() } returns Result.success(emptyList())
|
||||||
|
|
||||||
val vm = CmsViewModel(repo)
|
val vm = viewModel(repo)
|
||||||
// advance init launched coroutine
|
// advance init launched coroutine
|
||||||
dispatcher.scheduler.advanceUntilIdle()
|
dispatcher.scheduler.advanceUntilIdle()
|
||||||
|
|
||||||
@@ -66,7 +74,7 @@ class CmsViewModelTest {
|
|||||||
coEvery { repo.vereinsmeisterschaften() } returns Result.success(emptyList())
|
coEvery { repo.vereinsmeisterschaften() } returns Result.success(emptyList())
|
||||||
coEvery { repo.saveConfig(any()) } returns Result.success(cfg)
|
coEvery { repo.saveConfig(any()) } returns Result.success(cfg)
|
||||||
coEvery { repo.saveNews(any()) } returns Result.success(de.harheimertc.data.AuthMessageResponse(success = true, message = "ok"))
|
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
|
// wait for init/load to finish before saving to avoid race
|
||||||
dispatcher.scheduler.advanceUntilIdle()
|
dispatcher.scheduler.advanceUntilIdle()
|
||||||
@@ -95,7 +103,7 @@ class CmsViewModelTest {
|
|||||||
coEvery { repo.vereinsmeisterschaften() } returns Result.success(emptyList())
|
coEvery { repo.vereinsmeisterschaften() } returns Result.success(emptyList())
|
||||||
coEvery { repo.saveNews(any()) } returns Result.success(de.harheimertc.data.AuthMessageResponse(success = true, message = "saved"))
|
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()
|
dispatcher.scheduler.advanceUntilIdle()
|
||||||
|
|
||||||
vm.saveNews(de.harheimertc.data.NewsSaveRequest(id = null, title = "t", content = "c"))
|
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.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")))))
|
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()
|
dispatcher.scheduler.advanceUntilIdle()
|
||||||
|
|
||||||
vm.updateUserRoles("1", listOf("admin", "vorstand"))
|
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.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))))
|
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()
|
dispatcher.scheduler.advanceUntilIdle()
|
||||||
|
|
||||||
vm.setUserActive("2", false)
|
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"))
|
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()
|
dispatcher.scheduler.advanceUntilIdle()
|
||||||
|
|
||||||
vm.resendInvite("10")
|
vm.resendInvite("10")
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
<p class="text-sm font-medium text-green-800">
|
<p class="text-sm font-medium text-green-800">
|
||||||
{{ currentFile.name }}
|
{{ currentFile.name }}
|
||||||
</p><p class="text-xs text-green-600">
|
</p><p class="text-xs text-green-600">
|
||||||
{{ currentFile.size }} bytes
|
{{ currentFileLabel }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</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()) }
|
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))
|
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
|
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!'
|
processingMessage.value = 'Verarbeitung abgeschlossen!'
|
||||||
setTimeout(() => { isProcessing.value = false; showUploadModal.value = false }, 1000)
|
setTimeout(() => { isProcessing.value = false; showUploadModal.value = false }, 1000)
|
||||||
} catch (error) { console.error('Fehler:', error); alert('Fehler: ' + error.message); isProcessing.value = false }
|
} 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 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 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 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 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 selectAllColumns = () => { selectedColumns.value = selectedColumns.value.map(() => true) }
|
||||||
const deselectAllColumns = () => { selectedColumns.value = selectedColumns.value.map(() => false) }
|
const deselectAllColumns = () => { selectedColumns.value = selectedColumns.value.map(() => false) }
|
||||||
@@ -415,7 +420,7 @@ onMounted(() => {
|
|||||||
csvHeaders.value = result.headers
|
csvHeaders.value = result.headers
|
||||||
csvData.value = result.data.map(row => csvHeaders.value.map(header => row[header] || ''))
|
csvData.value = result.data.map(row => csvHeaders.value.map(header => row[header] || ''))
|
||||||
selectedColumns.value = new Array(csvHeaders.value.length).fill(true)
|
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 */ }
|
} catch { /* ignore */ }
|
||||||
})()
|
})()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { deleteTermin } from '../utils/termine.js'
|
|||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
try {
|
try {
|
||||||
const token = getCookie(event, 'auth_token')
|
const token = getCookie(event, 'auth_token') || getHeader(event, 'authorization')?.replace(/^Bearer\s+/i, '')
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
throw createError({
|
throw createError({
|
||||||
@@ -58,4 +58,3 @@ export default defineEventHandler(async (event) => {
|
|||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { readTermine } from '../utils/termine.js'
|
|||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
try {
|
try {
|
||||||
const token = getCookie(event, 'auth_token')
|
const token = getCookie(event, 'auth_token') || getHeader(event, 'authorization')?.replace(/^Bearer\s+/i, '')
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
throw createError({
|
throw createError({
|
||||||
@@ -42,4 +42,3 @@ export default defineEventHandler(async (event) => {
|
|||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { sendNewEventPush } from '../utils/push-notifications.js'
|
|||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
try {
|
try {
|
||||||
const token = getCookie(event, 'auth_token')
|
const token = getCookie(event, 'auth_token') || getHeader(event, 'authorization')?.replace(/^Bearer\s+/i, '')
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
throw createError({
|
throw createError({
|
||||||
@@ -63,4 +63,3 @@ export default defineEventHandler(async (event) => {
|
|||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user