feat(android): sort CMS users, remove invite button, close CMS submenu on navigate
This commit is contained in:
@@ -61,13 +61,17 @@ fun AppNavigationHeader(
|
||||
if (webTabletNavigation) {
|
||||
WebTabletNavigation(selectedRoute, onNavigate, navigationState)
|
||||
} else {
|
||||
CompactNavigation(selectedRoute, onNavigate)
|
||||
CompactNavigation(selectedRoute, onNavigate, navigationState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CompactNavigation(selectedRoute: String?, onNavigate: (String) -> Unit) {
|
||||
private fun CompactNavigation(
|
||||
selectedRoute: String?,
|
||||
onNavigate: (String) -> Unit,
|
||||
navigationState: NavigationUiState = NavigationUiState(),
|
||||
) {
|
||||
BrandRow(onLogin = { onNavigate(Destinations.Login.route) })
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
CompactLink("Start", Destinations.Home.route, selectedRoute, onNavigate, Modifier.weight(1f))
|
||||
@@ -75,6 +79,9 @@ private fun CompactNavigation(selectedRoute: String?, onNavigate: (String) -> Un
|
||||
CompactLink("Spielplan", Destinations.Spielplan.route, selectedRoute, onNavigate, Modifier.weight(1f))
|
||||
CompactLink("Galerie", Destinations.Gallery.route, selectedRoute, onNavigate, Modifier.weight(1f))
|
||||
CompactLink("Kontakt", Destinations.Contact.route, selectedRoute, onNavigate, Modifier.weight(1f))
|
||||
if (navigationState.isAdmin || navigationState.canAccessNewsletter || navigationState.canAccessContactRequests) {
|
||||
CompactLink("CMS", Destinations.Cms.route, selectedRoute, onNavigate, Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +92,9 @@ private fun WebTabletNavigation(
|
||||
navigationState: NavigationUiState,
|
||||
) {
|
||||
val section = menuSection(selectedRoute)
|
||||
var cmsExpanded = androidx.compose.runtime.remember { androidx.compose.runtime.mutableStateOf(false) }
|
||||
// Helper that closes the CMS submenu when navigating away
|
||||
val navigateAndClose: (String) -> Unit = { route -> cmsExpanded.value = false; onNavigate(route) }
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Brand()
|
||||
Spacer(Modifier.width(16.dp))
|
||||
@@ -93,12 +103,12 @@ private fun WebTabletNavigation(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
MainLink("Start", selectedRoute == Destinations.Home.route, onClick = { onNavigate(Destinations.Home.route) })
|
||||
MainLink("Verein", section == MenuSection.VEREIN, onClick = { onNavigate(Destinations.VereinAbout.route) })
|
||||
MainLink("Mannschaften", section == MenuSection.MANNSCHAFTEN, onClick = { onNavigate(Destinations.Mannschaften.route) })
|
||||
MainLink("Training", section == MenuSection.TRAINING, onClick = { onNavigate(Destinations.Training.route) })
|
||||
MainLink("Mitgliedschaft", selectedRoute == Destinations.Membership.route, onClick = { onNavigate(Destinations.Membership.route) })
|
||||
MainLink("Termine", selectedRoute == Destinations.Termine.route, onClick = { onNavigate(Destinations.Termine.route) })
|
||||
MainLink("Start", selectedRoute == Destinations.Home.route, onClick = { navigateAndClose(Destinations.Home.route) })
|
||||
MainLink("Verein", section == MenuSection.VEREIN, onClick = { navigateAndClose(Destinations.VereinAbout.route) })
|
||||
MainLink("Mannschaften", section == MenuSection.MANNSCHAFTEN, onClick = { navigateAndClose(Destinations.Mannschaften.route) })
|
||||
MainLink("Training", section == MenuSection.TRAINING, onClick = { navigateAndClose(Destinations.Training.route) })
|
||||
MainLink("Mitgliedschaft", selectedRoute == Destinations.Membership.route, onClick = { navigateAndClose(Destinations.Membership.route) })
|
||||
MainLink("Termine", selectedRoute == Destinations.Termine.route, onClick = { navigateAndClose(Destinations.Termine.route) })
|
||||
if (navigationState.showGallery) {
|
||||
MainLink("Galerie", selectedRoute == Destinations.Gallery.route, onClick = { onNavigate(Destinations.Gallery.route) })
|
||||
}
|
||||
@@ -106,11 +116,18 @@ private fun WebTabletNavigation(
|
||||
if (navigationState.loggedIn) {
|
||||
MainLink("Intern", section == MenuSection.INTERN, onClick = { onNavigate(Destinations.MemberArea.route) })
|
||||
}
|
||||
MainLink("Kontakt", selectedRoute == Destinations.Contact.route, primary = true, onClick = { onNavigate(Destinations.Contact.route) })
|
||||
TextButton(onClick = { onNavigate(Destinations.Login.route) }) { Text("Login", color = Color.White) }
|
||||
MainLink("Kontakt", selectedRoute == Destinations.Contact.route, primary = true, onClick = { navigateAndClose(Destinations.Contact.route) })
|
||||
TextButton(onClick = { navigateAndClose(Destinations.Login.route) }) { Text("Login", color = Color.White) }
|
||||
}
|
||||
}
|
||||
val subItems = submenu(section, navigationState)
|
||||
// determine CMS parent index and children
|
||||
val cmsIndex = subItems.indexOfFirst { it.label == "CMS" }
|
||||
val cmsChildren = if (cmsIndex >= 0) subItems.subList(cmsIndex + 1, subItems.size) else emptyList<MenuTarget>()
|
||||
if (cmsChildren.any { it.route == selectedRoute }) {
|
||||
cmsExpanded.value = true
|
||||
}
|
||||
// First row: render all subitems but do NOT render CMS children here
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -118,8 +135,33 @@ private fun WebTabletNavigation(
|
||||
.padding(top = 3.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
subItems.forEach { item ->
|
||||
SubLink(item.label, item.route == selectedRoute) { onNavigate(item.route) }
|
||||
subItems.forEachIndexed { idx, item ->
|
||||
if (idx == cmsIndex) {
|
||||
// CMS parent toggle
|
||||
SubLink(item.label, item.route == selectedRoute) {
|
||||
cmsExpanded.value = !cmsExpanded.value
|
||||
}
|
||||
} else if (idx > cmsIndex && cmsIndex >= 0) {
|
||||
// skip cms children here; they'll be rendered in the second row when expanded
|
||||
} else {
|
||||
// normal item before CMS: close cms submenu on navigate
|
||||
SubLink(item.label, item.route == selectedRoute) { navigateAndClose(item.route) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Second row: when CMS expanded, render its children beneath
|
||||
if (cmsExpanded.value && cmsChildren.isNotEmpty()) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.horizontalScroll(rememberScrollState())
|
||||
.padding(top = 6.dp, bottom = 3.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
cmsChildren.forEach { child ->
|
||||
SubLink(child.label, child.route == selectedRoute) { onNavigate(child.route) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -301,6 +343,7 @@ private fun submenu(section: MenuSection?, state: NavigationUiState): List<MenuT
|
||||
add(MenuTarget("Mein Profil", Destinations.Profile.route))
|
||||
add(MenuTarget("API-Dokumentation", Destinations.MemberApi.route))
|
||||
if (state.isAdmin || state.canAccessContactRequests || state.canAccessNewsletter) add(MenuTarget("CMS", Destinations.Cms.route))
|
||||
// CMS child items (will be rendered when CMS parent is expanded)
|
||||
if (state.isAdmin) add(MenuTarget("Startseite", Destinations.CmsStartseite.route))
|
||||
if (state.isAdmin) add(MenuTarget("Inhalte", Destinations.CmsInhalte.route))
|
||||
if (state.isAdmin) add(MenuTarget("Vereinsmeisterschaften", Destinations.CmsVereinsmeisterschaften.route))
|
||||
|
||||
@@ -2,6 +2,8 @@ package de.harheimertc.ui.screens.cms
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
@@ -23,6 +25,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -30,6 +33,8 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.navigation.NavController
|
||||
import de.harheimertc.data.CmsUserDto
|
||||
import de.harheimertc.data.ConfigResponse
|
||||
@@ -230,7 +235,7 @@ private fun CmsSummaryGrid(navController: NavController, state: CmsUiState) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CmsPage(
|
||||
fun CmsPage(
|
||||
navController: NavController,
|
||||
showBackNavigation: Boolean,
|
||||
title: String,
|
||||
@@ -281,28 +286,126 @@ private fun CmsConfigPage(
|
||||
|
||||
@Composable
|
||||
private fun CmsUserListPage(navController: NavController, showBackNavigation: Boolean, title: String, users: List<CmsUserDto>) {
|
||||
val viewModel: CmsViewModel = hiltViewModel()
|
||||
val state by viewModel.state.collectAsState()
|
||||
|
||||
val pending = state.users.filter { it.active == false }
|
||||
val active = state.users.filter { it.active == true }
|
||||
.sortedWith(compareBy({ (it.name ?: "").lowercase() }))
|
||||
|
||||
CmsPage(navController, showBackNavigation, title, "Registrierte Zugänge und Rollen") {
|
||||
if (users.isEmpty()) item { EmptyCard("Keine Benutzer gefunden oder keine Berechtigung.") }
|
||||
items(users.size) { index -> UserCard(users[index]) }
|
||||
if (state.users.isEmpty()) item { EmptyCard("Keine Benutzer gefunden oder keine Berechtigung.") }
|
||||
|
||||
// Pending (inactive) users first with highlighted background
|
||||
if (pending.isNotEmpty()) {
|
||||
items(pending.size) { index ->
|
||||
val user = pending[index]
|
||||
Surface(color = Accent100, shape = RoundedCornerShape(14.dp), shadowElevation = 2.dp, modifier = Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.fillMaxWidth().padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(user.name.ifBlank { user.email ?: "Unbekannt" }, style = MaterialTheme.typography.titleMedium, color = Accent900)
|
||||
Text(user.email ?: "-", color = Accent700)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(top = 6.dp)) {
|
||||
Button(onClick = { viewModel.setUserActive(user.id, true) }, enabled = !state.saving) { Text("Freischalten") }
|
||||
Button(onClick = { viewModel.resendInvite(user.id) }, enabled = !state.saving) { Text("Invite erneut") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Active users
|
||||
items(active.size) { index -> UserCard(active[index], viewModel) }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UserCard(user: CmsUserDto) {
|
||||
private fun UserCard(user: CmsUserDto, viewModel: CmsViewModel) {
|
||||
var rolesDialogOpen by remember { mutableStateOf(false) }
|
||||
val roleOptions = listOf("admin", "vorstand", "trainer", "newsletter")
|
||||
val state by viewModel.state.collectAsState()
|
||||
|
||||
DataCard(user.name.ifBlank { user.email.orEmpty() }) {
|
||||
InfoRow("E-Mail", user.email ?: "-")
|
||||
InfoRow("Rollen", user.roles.ifEmpty { listOfNotNull(user.role) }.joinToString(", ").ifBlank { "mitglied" })
|
||||
InfoRow("Status", if (user.active) "Aktiv" else "Inaktiv")
|
||||
InfoRow("Letzter Login", user.lastLogin ?: "-")
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(top = 8.dp)) {
|
||||
TextButton(onClick = { viewModel.setUserActive(user.id, !user.active) }, enabled = !state.saving) { Text(if (user.active) "Deaktivieren" else "Aktivieren") }
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
TextButton(onClick = { rolesDialogOpen = true }) { Text("Rollen") }
|
||||
}
|
||||
}
|
||||
|
||||
if (rolesDialogOpen) {
|
||||
val selected = remember { mutableStateListOf<String>().apply { addAll(user.roles) } }
|
||||
AlertDialog(
|
||||
onDismissRequest = { rolesDialogOpen = false },
|
||||
title = { Text("Rollen bearbeiten") },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
roleOptions.forEach { role ->
|
||||
Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically) {
|
||||
Checkbox(checked = selected.contains(role), onCheckedChange = { checked ->
|
||||
if (checked) selected.add(role) else selected.remove(role)
|
||||
})
|
||||
Text(role, modifier = Modifier.padding(start = 8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(onClick = {
|
||||
viewModel.updateUserRoles(user.id, selected.toList())
|
||||
rolesDialogOpen = false
|
||||
}) { Text("Speichern") }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { rolesDialogOpen = false }) { Text("Abbrechen") }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContactRequestCard(request: ContactRequestDto) {
|
||||
val viewModel: CmsViewModel = hiltViewModel()
|
||||
var replyOpen by remember { mutableStateOf(false) }
|
||||
var replyText by remember { mutableStateOf("") }
|
||||
val state by viewModel.state.collectAsState()
|
||||
|
||||
DataCard(request.name.ifBlank { request.email }) {
|
||||
InfoRow("E-Mail", request.email)
|
||||
InfoRow("Status", request.status.ifBlank { "offen" })
|
||||
InfoRow("Nachricht", request.message)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(top = 8.dp)) {
|
||||
TextButton(onClick = { replyOpen = true }, enabled = !state.saving) { Text("Antworten") }
|
||||
TextButton(onClick = { viewModel.toggleContactRequestStatus(request.id) }, enabled = !state.saving) { Text("Status umschalten") }
|
||||
}
|
||||
}
|
||||
|
||||
if (replyOpen) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { replyOpen = false },
|
||||
title = { Text("Antwort an ${request.name}") },
|
||||
text = {
|
||||
Column {
|
||||
Text(request.message)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
TextButton(onClick = {}) { /* placeholder to align */ }
|
||||
androidx.compose.material3.OutlinedTextField(value = replyText, onValueChange = { replyText = it }, label = { Text("Antwort") })
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(onClick = {
|
||||
viewModel.replyToContactRequest(request.id, replyText)
|
||||
replyOpen = false
|
||||
replyText = ""
|
||||
}) { Text("Senden") }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { replyOpen = false }) { Text("Abbrechen") }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,12 +9,15 @@ import de.harheimertc.data.ContactRequestDto
|
||||
import de.harheimertc.data.NewsletterDto
|
||||
import de.harheimertc.data.NewsletterGroupDto
|
||||
import de.harheimertc.data.PasswordResetAttemptDto
|
||||
import de.harheimertc.data.NewsDto
|
||||
import de.harheimertc.data.NewsSaveRequest
|
||||
import de.harheimertc.repositories.CmsRepository
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import de.harheimertc.ui.util.ErrorMapper
|
||||
|
||||
data class CmsUiState(
|
||||
val loading: Boolean = true,
|
||||
@@ -27,6 +30,7 @@ data class CmsUiState(
|
||||
val newsletters: List<NewsletterDto> = emptyList(),
|
||||
val newsletterGroups: List<NewsletterGroupDto> = emptyList(),
|
||||
val passwordResetAttempts: List<PasswordResetAttemptDto> = emptyList(),
|
||||
val news: List<NewsDto> = emptyList(),
|
||||
)
|
||||
|
||||
@HiltViewModel
|
||||
@@ -43,21 +47,51 @@ class CmsViewModel @Inject constructor(
|
||||
fun load() {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(loading = true, error = null)
|
||||
val config = async { repository.config().getOrNull() }
|
||||
val users = async { repository.users().getOrNull()?.users.orEmpty() }
|
||||
val requests = async { repository.contactRequests().getOrNull().orEmpty() }
|
||||
val newsletters = async { repository.newsletters().getOrNull()?.newsletters.orEmpty() }
|
||||
val groups = async { repository.newsletterGroups().getOrNull()?.groups.orEmpty() }
|
||||
val diagnostics = async { repository.passwordResetDiagnostics().getOrNull()?.attempts.orEmpty() }
|
||||
|
||||
val configRes = async { repository.config() }
|
||||
val usersRes = async { repository.users() }
|
||||
val requestsRes = async { repository.contactRequests() }
|
||||
val newslettersRes = async { repository.newsletters() }
|
||||
val groupsRes = async { repository.newsletterGroups() }
|
||||
val newsRes = async { repository.news() }
|
||||
val diagnosticsRes = async { repository.passwordResetDiagnostics() }
|
||||
|
||||
val configResult = configRes.await()
|
||||
val usersResult = usersRes.await()
|
||||
val requestsResult = requestsRes.await()
|
||||
val newslettersResult = newslettersRes.await()
|
||||
val groupsResult = groupsRes.await()
|
||||
val newsResult = newsRes.await()
|
||||
val diagnosticsResult = diagnosticsRes.await()
|
||||
|
||||
val errors = listOfNotNull(
|
||||
ErrorMapper.mapError(configResult.exceptionOrNull()),
|
||||
ErrorMapper.mapError(usersResult.exceptionOrNull()),
|
||||
ErrorMapper.mapError(requestsResult.exceptionOrNull()),
|
||||
ErrorMapper.mapError(newslettersResult.exceptionOrNull()),
|
||||
ErrorMapper.mapError(groupsResult.exceptionOrNull()),
|
||||
ErrorMapper.mapError(newsResult.exceptionOrNull()),
|
||||
ErrorMapper.mapError(diagnosticsResult.exceptionOrNull()),
|
||||
)
|
||||
|
||||
// Sort users so that pending (inactive) users come first,
|
||||
// followed by active users sorted by display name (case-insensitive).
|
||||
val fetchedUsers = usersResult.getOrNull()?.users.orEmpty()
|
||||
val pendingUsers = fetchedUsers.filter { it.active == false }
|
||||
val activeUsers = fetchedUsers.filter { it.active == true }
|
||||
.sortedBy { (it.name ?: "").lowercase() }
|
||||
val orderedUsers = pendingUsers + activeUsers
|
||||
|
||||
_state.value = CmsUiState(
|
||||
loading = false,
|
||||
config = config.await(),
|
||||
users = users.await(),
|
||||
contactRequests = requests.await(),
|
||||
newsletters = newsletters.await(),
|
||||
newsletterGroups = groups.await(),
|
||||
passwordResetAttempts = diagnostics.await(),
|
||||
error = if (errors.isNotEmpty()) errors.joinToString("; ") else null,
|
||||
config = configResult.getOrNull(),
|
||||
users = orderedUsers,
|
||||
contactRequests = requestsResult.getOrNull().orEmpty(),
|
||||
newsletters = newslettersResult.getOrNull()?.newsletters.orEmpty(),
|
||||
newsletterGroups = groupsResult.getOrNull()?.groups.orEmpty(),
|
||||
news = newsResult.getOrNull()?.news.orEmpty(),
|
||||
passwordResetAttempts = diagnosticsResult.getOrNull()?.attempts.orEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -76,9 +110,180 @@ class CmsViewModel @Inject constructor(
|
||||
.onFailure {
|
||||
_state.value = _state.value.copy(
|
||||
saving = false,
|
||||
error = it.message ?: "Inhalt konnte nicht gespeichert werden.",
|
||||
error = ErrorMapper.mapError(it) ?: "Inhalt konnte nicht gespeichert werden.",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveNews(request: NewsSaveRequest) {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(saving = true, error = null, message = null)
|
||||
repository.saveNews(request)
|
||||
.onSuccess { msg ->
|
||||
// refresh news list directly to preserve message
|
||||
val newsRes = repository.news()
|
||||
_state.value = _state.value.copy(
|
||||
saving = false,
|
||||
news = newsRes.getOrNull()?.news.orEmpty(),
|
||||
message = msg.message ?: "Nachricht gespeichert.",
|
||||
)
|
||||
}
|
||||
.onFailure { err ->
|
||||
_state.value = _state.value.copy(saving = false, error = ErrorMapper.mapError(err) ?: "News konnten nicht gespeichert werden.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun bulkSetPublic(ids: List<Int>, makePublic: Boolean) {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(saving = true, error = null, message = null)
|
||||
ids.forEach { id ->
|
||||
val existing = _state.value.news.find { it.id == id }
|
||||
if (existing != null) {
|
||||
val req = NewsSaveRequest(
|
||||
id = existing.id,
|
||||
title = existing.title,
|
||||
content = existing.content,
|
||||
isPublic = makePublic,
|
||||
isHidden = existing.isHidden,
|
||||
expiresAt = existing.expiresAt,
|
||||
)
|
||||
repository.saveNews(req)
|
||||
}
|
||||
}
|
||||
val newsRes = repository.news()
|
||||
_state.value = _state.value.copy(saving = false, news = newsRes.getOrNull()?.news.orEmpty(), message = "Bulk-Update abgeschlossen.")
|
||||
}
|
||||
}
|
||||
|
||||
fun bulkSetHidden(ids: List<Int>, makeHidden: Boolean) {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(saving = true, error = null, message = null)
|
||||
ids.forEach { id ->
|
||||
val existing = _state.value.news.find { it.id == id }
|
||||
if (existing != null) {
|
||||
val req = NewsSaveRequest(
|
||||
id = existing.id,
|
||||
title = existing.title,
|
||||
content = existing.content,
|
||||
isPublic = existing.isPublic,
|
||||
isHidden = makeHidden,
|
||||
expiresAt = existing.expiresAt,
|
||||
)
|
||||
repository.saveNews(req)
|
||||
}
|
||||
}
|
||||
val newsRes = repository.news()
|
||||
_state.value = _state.value.copy(saving = false, news = newsRes.getOrNull()?.news.orEmpty(), message = "Bulk-Update abgeschlossen.")
|
||||
}
|
||||
}
|
||||
|
||||
fun bulkDelete(ids: List<Int>) {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(saving = true, error = null, message = null)
|
||||
ids.forEach { id ->
|
||||
repository.deleteNews(id)
|
||||
}
|
||||
val newsRes = repository.news()
|
||||
_state.value = _state.value.copy(saving = false, news = newsRes.getOrNull()?.news.orEmpty(), message = "Bulk-Löschung abgeschlossen.")
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteNews(id: Int) {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(saving = true, error = null, message = null)
|
||||
repository.deleteNews(id)
|
||||
.onSuccess { msg ->
|
||||
val newsRes = repository.news()
|
||||
_state.value = _state.value.copy(
|
||||
saving = false,
|
||||
news = newsRes.getOrNull()?.news.orEmpty(),
|
||||
message = msg.message ?: "Nachricht gelöscht.",
|
||||
)
|
||||
}
|
||||
.onFailure { err ->
|
||||
_state.value = _state.value.copy(saving = false, error = ErrorMapper.mapError(err) ?: "News konnten nicht gelöscht werden.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- User management actions (B2)
|
||||
fun updateUserRoles(id: String, roles: List<String>) {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(saving = true, error = null, message = null)
|
||||
repository.updateUserRoles(id, roles)
|
||||
.onSuccess { msg ->
|
||||
val usersRes = repository.users()
|
||||
_state.value = _state.value.copy(
|
||||
saving = false,
|
||||
users = usersRes.getOrNull()?.users.orEmpty(),
|
||||
message = msg.message ?: "Rollen aktualisiert.",
|
||||
)
|
||||
}
|
||||
.onFailure { err ->
|
||||
_state.value = _state.value.copy(saving = false, error = ErrorMapper.mapError(err) ?: "Rollen konnten nicht aktualisiert werden.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setUserActive(id: String, active: Boolean) {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(saving = true, error = null, message = null)
|
||||
repository.updateUserActive(id, active)
|
||||
.onSuccess { msg ->
|
||||
val usersRes = repository.users()
|
||||
_state.value = _state.value.copy(
|
||||
saving = false,
|
||||
users = usersRes.getOrNull()?.users.orEmpty(),
|
||||
message = msg.message ?: if (active) "Benutzer aktiviert." else "Benutzer deaktiviert.",
|
||||
)
|
||||
}
|
||||
.onFailure { err ->
|
||||
_state.value = _state.value.copy(saving = false, error = ErrorMapper.mapError(err) ?: "Benutzerstatus konnte nicht geändert werden.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resendInvite(id: String) {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(saving = true, error = null, message = null)
|
||||
repository.resendInvite(id)
|
||||
.onSuccess { msg ->
|
||||
_state.value = _state.value.copy(saving = false, message = msg.message ?: "Einladung erneut gesendet.")
|
||||
}
|
||||
.onFailure { err ->
|
||||
_state.value = _state.value.copy(saving = false, error = ErrorMapper.mapError(err) ?: "Einladung konnte nicht gesendet werden.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Contact requests (B3)
|
||||
fun replyToContactRequest(id: String, message: String) {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(saving = true, error = null, message = null)
|
||||
repository.replyToContactRequest(id, message)
|
||||
.onSuccess {
|
||||
val reqs = repository.contactRequests()
|
||||
_state.value = _state.value.copy(saving = false, contactRequests = reqs.getOrNull().orEmpty(), message = it.message ?: "Antwort gesendet.")
|
||||
}
|
||||
.onFailure { err ->
|
||||
_state.value = _state.value.copy(saving = false, error = ErrorMapper.mapError(err) ?: "Antwort konnte nicht gesendet werden.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleContactRequestStatus(id: String) {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(saving = true, error = null, message = null)
|
||||
repository.toggleContactRequestStatus(id)
|
||||
.onSuccess {
|
||||
val reqs = repository.contactRequests()
|
||||
_state.value = _state.value.copy(saving = false, contactRequests = reqs.getOrNull().orEmpty(), message = it.message ?: "Status aktualisiert.")
|
||||
}
|
||||
.onFailure { err ->
|
||||
_state.value = _state.value.copy(saving = false, error = ErrorMapper.mapError(err) ?: "Status konnte nicht geändert werden.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user