Updated stuff
All checks were successful
Code Analysis and Production Deploy / analyze (push) Successful in 6m4s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Successful in 2m10s

This commit is contained in:
Torsten Schulz (local)
2026-06-12 16:49:00 +02:00
parent 44d441811c
commit e537839e28
10 changed files with 197 additions and 157 deletions

View File

@@ -72,8 +72,8 @@ object HarheimerNotifications {
}
private fun destinationRoute(data: Map<String, String>): String = when (data["type"]) {
"news" -> Destinations.MemberNews.route
"event", "events_today", "events_tomorrow", "news_expiring" -> Destinations.Termine.route
"news", "news_expiring" -> Destinations.MemberNews.route
"event", "events_today", "events_tomorrow" -> Destinations.Termine.route
"team_matches" -> Destinations.Spielplan.route
"birthdays" -> Destinations.MemberArea.route
"contact_request" -> Destinations.CmsContactRequests.route

View File

@@ -422,6 +422,7 @@ private fun menuSection(route: String?): MenuSection? = when (route) {
Destinations.CmsStartseite.route,
Destinations.CmsInhalte.route,
Destinations.CmsVereinsmeisterschaften.route,
Destinations.CmsNews.route,
Destinations.CmsSportbetrieb.route,
Destinations.CmsMitgliederverwaltung.route,
Destinations.CmsNewsletter.route,
@@ -484,7 +485,7 @@ private fun submenu(section: MenuSection?, state: NavigationUiState): List<MenuT
add(MenuTarget("Startseite", Destinations.CmsStartseite.route))
add(MenuTarget("Inhalte", Destinations.CmsInhalte.route))
add(MenuTarget("Vereinsmeisterschaften", Destinations.CmsVereinsmeisterschaften.route))
add(MenuTarget("News", Destinations.MemberNews.route))
add(MenuTarget("News", Destinations.CmsNews.route))
add(MenuTarget("Sportbetrieb", Destinations.CmsSportbetrieb.route))
add(MenuTarget("Mitgliederverwaltung", Destinations.CmsMitgliederverwaltung.route))
add(MenuTarget("Einstellungen", Destinations.CmsEinstellungen.route))

View File

@@ -48,6 +48,7 @@ sealed class Destinations(val route: String) {
object CmsStartseite : Destinations("cms/startseite")
object CmsInhalte : Destinations("cms/inhalte")
object CmsVereinsmeisterschaften : Destinations("cms/vereinsmeisterschaften")
object CmsNews : Destinations("cms/news")
object CmsSportbetrieb : Destinations("cms/sportbetrieb")
object CmsMitgliederverwaltung : Destinations("cms/mitgliederverwaltung")
object CmsNewsletter : Destinations("cms/newsletter")

View File

@@ -335,6 +335,9 @@ fun NavGraph(
composable(Destinations.CmsVereinsmeisterschaften.route) {
de.harheimertc.ui.screens.cms.CmsVereinsmeisterschaftenScreen(navController, !persistentNavigation)
}
composable(Destinations.CmsNews.route) {
de.harheimertc.ui.screens.cms.CmsNewsScreen(navController, !persistentNavigation)
}
composable(Destinations.CmsSportbetrieb.route) {
de.harheimertc.ui.screens.cms.CmsSportbetriebScreen(navController, !persistentNavigation)
}

View File

@@ -432,114 +432,53 @@ fun CmsVereinsmeisterschaftenScreen(navController: NavController, showBackNaviga
@Composable
fun CmsSportbetriebScreen(navController: NavController, showBackNavigation: Boolean, viewModel: CmsViewModel = hiltViewModel()) {
val state by viewModel.state.collectAsState()
val config = state.config
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>() }
var activeTab by remember { mutableStateOf("termine") }
val tabs = listOf(
"termine" to "Termine",
"mannschaften" to "Mannschaften",
"spielplaene" to "Spielpläne",
)
LaunchedEffect(config) {
config?.let {
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)
CmsPage(navController, showBackNavigation, "Sportbetrieb", "Termine, Mannschaften und Spielpläne pflegen") {
item {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
tabs.forEach { (id, label) ->
if (activeTab == id) {
Button(onClick = { activeTab = id }, modifier = Modifier.weight(1f)) { Text(label) }
} else {
OutlinedButton(onClick = { activeTab = id }, modifier = Modifier.weight(1f)) { Text(label) }
}
}
}
}
}
CmsPage(navController, showBackNavigation, "Sportbetrieb", "Trainingszeiten, Trainingsort und Trainer pflegen") {
when {
state.loading || config == null -> item { LoadingState("Sportbetriebsdaten werden geladen...") }
else -> {
item {
Button(
onClick = {
viewModel.saveConfig(
config.copy(
training = config.training.copy(
ort = config.training.ort.copy(
name = ortName,
strasse = ortStrasse,
plz = ortPlz,
ort = ortOrt,
),
zeiten = trainingTimes.toList(),
),
trainer = trainers.toList(),
),
)
},
enabled = !state.saving,
modifier = Modifier.fillMaxWidth(),
) {
Text(if (state.saving) "Speichert..." else "Speichern")
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")
}
}
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))
}
}
"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")
}
}
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")
}
}
"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")
}
}
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 { FormMessages(state.error, state.message) }
}
}
}
@@ -950,6 +889,7 @@ private fun CmsSummaryGrid(navController: NavController, state: CmsUiState) {
val cards = listOf(
Triple("Startseite", "Öffentliche Startseite", Destinations.CmsStartseite.route),
Triple("Inhalte", "Vereinsseiten", Destinations.CmsInhalte.route),
Triple("News", "Interne und öffentliche News", Destinations.CmsNews.route),
Triple("Sportbetrieb", "Training und Sportdaten", Destinations.CmsSportbetrieb.route),
Triple("Mitgliederverwaltung", "${state.users.size} Benutzer", Destinations.CmsMitgliederverwaltung.route),
Triple("Kontaktanfragen", "${state.contactRequests.size} Anfragen", Destinations.CmsContactRequests.route),

View File

@@ -142,7 +142,7 @@ fun NotificationSettingsScreen(
ToggleRow("Punktspiele der eigenen Mannschaft", state.settings.ownTeamMatches) {
viewModel.update(state.settings.copy(ownTeamMatches = it))
}
Text("Die eigene Mannschaft wird aus dem Namen und der Mannschaftszusammensetzung ermittelt.", color = Accent700)
OwnTeamInfo(state.ownTeams, state.currentUserName)
ToggleRow("Punktspiele aller Mannschaften", state.settings.allTeamMatches) {
viewModel.update(state.settings.copy(allTeamMatches = it))
}
@@ -214,6 +214,16 @@ private fun ToggleRow(label: String, checked: Boolean, onCheckedChange: (Boolean
}
}
@Composable
private fun OwnTeamInfo(ownTeams: List<Mannschaft>, currentUserName: String) {
val text = when {
ownTeams.isNotEmpty() -> "Erkannte eigene Mannschaft: " + ownTeams.joinToString { it.mannschaft }
currentUserName.isBlank() -> "Eigene Mannschaft kann erst nach geladenem Profil ermittelt werden."
else -> "Keine eigene Mannschaft für " + currentUserName + " erkannt."
}
Text(text, color = Accent700)
}
@Composable
private fun TimeSelection(selectedTime: String, onSelectTime: (String) -> Unit) {
Row(

View File

@@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import de.harheimertc.repositories.Mannschaft
import de.harheimertc.repositories.LoginRepository
import de.harheimertc.repositories.MannschaftenRepository
import de.harheimertc.repositories.NotificationPreferences
import de.harheimertc.repositories.NotificationPreferencesRepository
@@ -16,6 +17,8 @@ data class NotificationSettingsUiState(
val loading: Boolean = true,
val settings: NotificationPreferences = NotificationPreferences(),
val teams: List<Mannschaft> = emptyList(),
val ownTeams: List<Mannschaft> = emptyList(),
val currentUserName: String = "",
val seasons: List<String> = emptyList(),
val error: String? = null,
val saveError: String? = null,
@@ -25,6 +28,7 @@ data class NotificationSettingsUiState(
class NotificationSettingsViewModel @Inject constructor(
private val preferencesRepository: NotificationPreferencesRepository,
private val mannschaftenRepository: MannschaftenRepository,
private val loginRepository: LoginRepository,
) : ViewModel() {
private val _state = MutableStateFlow(NotificationSettingsUiState())
val state: StateFlow<NotificationSettingsUiState> = _state
@@ -37,12 +41,14 @@ class NotificationSettingsViewModel @Inject constructor(
viewModelScope.launch {
_state.value = _state.value.copy(loading = true, error = null, saveError = null)
val storedSettings = preferencesRepository.loadRemote().getOrElse { preferencesRepository.loadLocal() }
val authStatus = loginRepository.status().getOrNull()
val currentUserName = authStatus?.user?.name.orEmpty()
val seasonsResponse = mannschaftenRepository.fetchSeasons().getOrNull()
val seasons = seasonsResponse?.seasons.orEmpty()
val selectedSeason = storedSettings.selectedTeamSeason
?: seasonsResponse?.defaultSeason?.takeIf { it.isNotBlank() }
?: seasons.firstOrNull()
loadTeams(storedSettings.copy(selectedTeamSeason = selectedSeason), seasons)
loadTeams(storedSettings.copy(selectedTeamSeason = selectedSeason), seasons, currentUserName)
}
}
@@ -50,7 +56,7 @@ class NotificationSettingsViewModel @Inject constructor(
val current = _state.value.settings
viewModelScope.launch {
_state.value = _state.value.copy(loading = true, error = null, saveError = null)
loadTeams(current.copy(selectedTeamSeason = season), _state.value.seasons, syncRemote = true)
loadTeams(current.copy(selectedTeamSeason = season), _state.value.seasons, _state.value.currentUserName, syncRemote = true)
}
}
@@ -76,7 +82,7 @@ class NotificationSettingsViewModel @Inject constructor(
update(current.copy(selectedTeamSlugs = nextTeams))
}
private suspend fun loadTeams(settings: NotificationPreferences, seasons: List<String>, syncRemote: Boolean = false) {
private suspend fun loadTeams(settings: NotificationPreferences, seasons: List<String>, currentUserName: String, syncRemote: Boolean = false) {
mannschaftenRepository.fetchMannschaften(settings.selectedTeamSeason)
.onSuccess { teams ->
val knownSlugs = teams.map { it.slug }.toSet()
@@ -89,6 +95,8 @@ class NotificationSettingsViewModel @Inject constructor(
loading = false,
settings = nextSettings,
teams = teams,
ownTeams = ownTeamsForUser(currentUserName, teams),
currentUserName = currentUserName,
seasons = seasons,
saveError = saveError,
)
@@ -101,6 +109,7 @@ class NotificationSettingsViewModel @Inject constructor(
_state.value = NotificationSettingsUiState(
loading = false,
settings = settings,
currentUserName = currentUserName,
seasons = seasons,
error = error.message ?: "Mannschaften konnten nicht geladen werden.",
saveError = saveError,
@@ -108,3 +117,27 @@ class NotificationSettingsViewModel @Inject constructor(
}
}
}
private fun ownTeamsForUser(userName: String, teams: List<Mannschaft>): List<Mannschaft> {
if (normalizePersonName(userName).isBlank()) return emptyList()
return teams.filter { team ->
team.spieler.any { player -> personNameMatches(player, userName) } ||
personNameMatches(team.mannschaftsfuehrer, userName)
}
}
private fun personNameMatches(candidate: String, userName: String): Boolean {
val normalizedCandidate = normalizePersonName(candidate)
val normalizedUserName = normalizePersonName(userName)
if (normalizedCandidate.isBlank() || normalizedUserName.isBlank()) return false
if (normalizedCandidate == normalizedUserName) return true
val candidateParts = normalizedCandidate.split(" " ).filter { it.isNotBlank() }.toSet()
val userParts = normalizedUserName.split(" " ).filter { it.isNotBlank() }
return userParts.size >= 2 && userParts.all { it in candidateParts }
}
private fun normalizePersonName(value: String): String = value
.lowercase()
.replace(Regex("[^a-z0-9äöüß]+"), " ")
.trim()