Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -403,6 +403,25 @@ data class NewsletterListResponse(
|
||||
val success: Boolean = false,
|
||||
val newsletters: List<NewsletterDto> = emptyList(),
|
||||
)
|
||||
data class NewsletterCreateRequest(
|
||||
val title: String,
|
||||
val content: String,
|
||||
val type: String,
|
||||
val targetGroup: String? = null,
|
||||
val sendToExternal: Boolean? = null,
|
||||
)
|
||||
|
||||
data class NewsletterCreateResponse(
|
||||
val success: Boolean = false,
|
||||
val message: String? = null,
|
||||
val newsletter: NewsletterDto? = null,
|
||||
)
|
||||
|
||||
data class NewsletterSendResponse(
|
||||
val success: Boolean = false,
|
||||
val message: String? = null,
|
||||
val stats: Map<String, Any>? = null,
|
||||
)
|
||||
data class NewsletterGroupDto(
|
||||
val id: String = "",
|
||||
val name: String = "",
|
||||
@@ -556,6 +575,34 @@ interface ApiService {
|
||||
@GET("/api/members")
|
||||
suspend fun members(): Response<MembersResponse>
|
||||
|
||||
data class MemberSaveRequest(
|
||||
val id: String? = null,
|
||||
val firstName: String,
|
||||
val lastName: String,
|
||||
val geburtsdatum: String,
|
||||
val email: String? = null,
|
||||
val phone: String? = null,
|
||||
val address: String? = null,
|
||||
val notes: String? = null,
|
||||
val isMannschaftsspieler: Boolean = false,
|
||||
val hasHallKey: Boolean = false,
|
||||
)
|
||||
|
||||
data class BulkImportRequest(val members: List<Map<String, String>>)
|
||||
data class BulkImportResponse(val success: Boolean = false, val summary: Map<String, Int>? = null)
|
||||
|
||||
@POST("/api/members")
|
||||
suspend fun saveMember(@Body request: MemberSaveRequest): Response<AuthMessageResponse>
|
||||
|
||||
@DELETE("/api/members")
|
||||
suspend fun deleteMember(@Body body: Map<String, String>): Response<AuthMessageResponse>
|
||||
|
||||
@POST("/api/members/bulk")
|
||||
suspend fun bulkImportMembers(@Body request: BulkImportRequest): Response<BulkImportResponse>
|
||||
|
||||
@POST("/api/members/toggle-mannschaftsspieler")
|
||||
suspend fun toggleMannschaftsspieler(@Body body: Map<String, String>): Response<Map<String, Any>>
|
||||
|
||||
@GET("/api/cms/users/list")
|
||||
suspend fun cmsUsers(): Response<CmsUsersResponse>
|
||||
|
||||
@@ -588,6 +635,27 @@ interface ApiService {
|
||||
@GET("/api/newsletter/groups/list")
|
||||
suspend fun newsletterGroups(): Response<NewsletterGroupsResponse>
|
||||
|
||||
@POST("/api/newsletter/groups/create")
|
||||
suspend fun createNewsletterGroup(@Body request: Map<String, @JvmSuppressWildcards Any?>): Response<AuthMessageResponse>
|
||||
|
||||
@PUT("/api/newsletter/groups/{id}")
|
||||
suspend fun updateNewsletterGroup(@Path("id") id: String, @Body request: Map<String, @JvmSuppressWildcards Any?>): Response<AuthMessageResponse>
|
||||
|
||||
@DELETE("/api/newsletter/groups/{id}")
|
||||
suspend fun deleteNewsletterGroup(@Path("id") id: String): Response<AuthMessageResponse>
|
||||
|
||||
@POST("/api/newsletter/create")
|
||||
suspend fun createNewsletter(@Body request: NewsletterCreateRequest): Response<NewsletterCreateResponse>
|
||||
|
||||
@PUT("/api/newsletter/{id}")
|
||||
suspend fun updateNewsletter(@Path("id") id: String, @Body request: Map<String, @JvmSuppressWildcards Any?>): Response<NewsletterCreateResponse>
|
||||
|
||||
@POST("/api/newsletter/{id}/send")
|
||||
suspend fun sendNewsletter(@Path("id") id: String): Response<NewsletterSendResponse>
|
||||
|
||||
@DELETE("/api/newsletter/{id}")
|
||||
suspend fun deleteNewsletter(@Path("id") id: String): Response<AuthMessageResponse>
|
||||
|
||||
@GET("/api/newsletter/groups/public-list")
|
||||
suspend fun publicNewsletterGroups(): Response<NewsletterGroupsResponse>
|
||||
|
||||
|
||||
@@ -143,6 +143,49 @@ class CmsRepository @Inject constructor(
|
||||
response.body() ?: de.harheimertc.data.AuthMessageResponse(success = false, message = "Leere Antwort")
|
||||
}
|
||||
|
||||
suspend fun createNewsletter(request: de.harheimertc.data.NewsletterCreateRequest): Result<de.harheimertc.data.NewsletterCreateResponse> = runCatching {
|
||||
val response = api.createNewsletter(request)
|
||||
if (!response.isSuccessful) error("Newsletter konnte nicht erstellt werden.")
|
||||
response.body() ?: de.harheimertc.data.NewsletterCreateResponse(success = false, message = "Leere Antwort")
|
||||
}
|
||||
|
||||
suspend fun updateNewsletter(id: String, patch: Map<String, Any?>): Result<de.harheimertc.data.NewsletterCreateResponse> = runCatching {
|
||||
val response = api.updateNewsletter(id, patch)
|
||||
if (!response.isSuccessful) error("Newsletter konnte nicht aktualisiert werden.")
|
||||
response.body() ?: de.harheimertc.data.NewsletterCreateResponse(success = false, message = "Leere Antwort")
|
||||
}
|
||||
|
||||
suspend fun sendNewsletter(id: String): Result<de.harheimertc.data.NewsletterSendResponse> = runCatching {
|
||||
val response = api.sendNewsletter(id)
|
||||
if (!response.isSuccessful) error("Newsletter konnte nicht versendet werden.")
|
||||
response.body() ?: de.harheimertc.data.NewsletterSendResponse(success = false, message = "Leere Antwort")
|
||||
}
|
||||
|
||||
suspend fun deleteNewsletter(id: String): Result<de.harheimertc.data.AuthMessageResponse> = runCatching {
|
||||
val response = api.deleteNewsletter(id)
|
||||
if (!response.isSuccessful) error("Newsletter konnte nicht gelöscht werden.")
|
||||
response.body() ?: de.harheimertc.data.AuthMessageResponse(success = false, message = "Leere Antwort")
|
||||
}
|
||||
|
||||
suspend fun createNewsletterGroup(payload: Map<String, @JvmSuppressWildcards Any?>): Result<de.harheimertc.data.AuthMessageResponse> = runCatching {
|
||||
// use generic POST via Retrofit? build request through create endpoint
|
||||
val response = api.createNewsletterGroup(payload)
|
||||
if (!response.isSuccessful) error("Gruppe konnte nicht erstellt werden.")
|
||||
response.body() ?: de.harheimertc.data.AuthMessageResponse(success = false, message = "Leere Antwort")
|
||||
}
|
||||
|
||||
suspend fun updateNewsletterGroup(id: String, patch: Map<String, Any?>): Result<de.harheimertc.data.AuthMessageResponse> = runCatching {
|
||||
val response = api.updateNewsletterGroup(id, patch)
|
||||
if (!response.isSuccessful) error("Gruppe konnte nicht aktualisiert werden.")
|
||||
response.body() ?: de.harheimertc.data.AuthMessageResponse(success = false, message = "Leere Antwort")
|
||||
}
|
||||
|
||||
suspend fun deleteNewsletterGroup(id: String): Result<de.harheimertc.data.AuthMessageResponse> = runCatching {
|
||||
val response = api.deleteNewsletterGroup(id)
|
||||
if (!response.isSuccessful) error("Gruppe konnte nicht gelöscht werden.")
|
||||
response.body() ?: de.harheimertc.data.AuthMessageResponse(success = false, message = "Leere Antwort")
|
||||
}
|
||||
|
||||
suspend fun deleteNews(id: Int): Result<de.harheimertc.data.AuthMessageResponse> = runCatching {
|
||||
val response = api.deleteNews(id)
|
||||
if (!response.isSuccessful) error("News konnten nicht gelöscht werden.")
|
||||
|
||||
@@ -64,6 +64,30 @@ class MemberAreaRepository @Inject constructor(
|
||||
if (!response.isSuccessful) error("News konnten nicht gespeichert werden.")
|
||||
}
|
||||
|
||||
suspend fun saveMember(request: de.harheimertc.data.ApiService.MemberSaveRequest): Result<de.harheimertc.data.AuthMessageResponse> = runCatching {
|
||||
val response = api.saveMember(request)
|
||||
if (!response.isSuccessful) error("Mitglied konnte nicht gespeichert werden.")
|
||||
response.body() ?: de.harheimertc.data.AuthMessageResponse(success = false, message = "Leere Antwort")
|
||||
}
|
||||
|
||||
suspend fun deleteMember(id: String): Result<de.harheimertc.data.AuthMessageResponse> = runCatching {
|
||||
val response = api.deleteMember(mapOf("id" to id))
|
||||
if (!response.isSuccessful) error("Mitglied konnte nicht gelöscht werden.")
|
||||
response.body() ?: de.harheimertc.data.AuthMessageResponse(success = false, message = "Leere Antwort")
|
||||
}
|
||||
|
||||
suspend fun bulkImport(members: List<Map<String, String>>): Result<de.harheimertc.data.ApiService.BulkImportResponse> = runCatching {
|
||||
val response = api.bulkImportMembers(de.harheimertc.data.ApiService.BulkImportRequest(members))
|
||||
if (!response.isSuccessful) error("Bulk-Import fehlgeschlagen")
|
||||
response.body() ?: de.harheimertc.data.ApiService.BulkImportResponse(success = false)
|
||||
}
|
||||
|
||||
suspend fun toggleMannschaftsspieler(memberId: String): Result<Map<String, Any>> = runCatching {
|
||||
val response = api.toggleMannschaftsspieler(mapOf("memberId" to memberId))
|
||||
if (!response.isSuccessful) error("Status konnte nicht umgeschaltet werden.")
|
||||
response.body() ?: emptyMap()
|
||||
}
|
||||
|
||||
suspend fun deleteNews(id: Int): Result<Unit> = runCatching {
|
||||
val response = api.deleteNews(id)
|
||||
if (!response.isSuccessful) error("News konnten nicht gelöscht werden.")
|
||||
|
||||
@@ -35,6 +35,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.navigation.NavController
|
||||
import de.harheimertc.data.CmsUserDto
|
||||
import de.harheimertc.data.ConfigResponse
|
||||
@@ -172,14 +173,173 @@ fun CmsContactRequestsScreen(navController: NavController, showBackNavigation: B
|
||||
@Composable
|
||||
fun CmsNewsletterScreen(navController: NavController, showBackNavigation: Boolean, viewModel: CmsViewModel = hiltViewModel()) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
val loginVm: de.harheimertc.ui.screens.login.LoginViewModel = hiltViewModel()
|
||||
val loginState by loginVm.state.collectAsState()
|
||||
val canWrite = loginState.roles.any { it == "admin" || it == "vorstand" }
|
||||
// dialog state for newsletters
|
||||
var newsletterDialogOpen by remember { mutableStateOf(false) }
|
||||
var editingNewsletter by remember { mutableStateOf<NewsletterDto?>(null) }
|
||||
var nlTitle by remember { mutableStateOf("") }
|
||||
var nlContent by remember { mutableStateOf("") }
|
||||
var nlType by remember { mutableStateOf("subscription") }
|
||||
var nlTargetGroup by remember { mutableStateOf("") }
|
||||
var nlSendToExternal by remember { mutableStateOf(true) }
|
||||
|
||||
// dialog state for groups
|
||||
var groupDialogOpen by remember { mutableStateOf(false) }
|
||||
var editingGroup by remember { mutableStateOf<NewsletterGroupDto?>(null) }
|
||||
var grpName by remember { mutableStateOf("") }
|
||||
var grpDescription by remember { mutableStateOf("") }
|
||||
var grpType by remember { mutableStateOf("subscription") }
|
||||
var grpTargetGroup by remember { mutableStateOf("") }
|
||||
var grpSendToExternal by remember { mutableStateOf(true) }
|
||||
CmsPage(navController, showBackNavigation, "Newsletter", "Newsletter und Gruppen") {
|
||||
if (state.loading) item { CircularProgressIndicator(color = Primary600) }
|
||||
item {
|
||||
if (canWrite) Button(onClick = {
|
||||
editingNewsletter = null
|
||||
nlTitle = ""
|
||||
nlContent = ""
|
||||
nlType = "subscription"
|
||||
nlTargetGroup = ""
|
||||
nlSendToExternal = true
|
||||
newsletterDialogOpen = true
|
||||
}, modifier = Modifier.fillMaxWidth()) { Text("Newsletter erstellen") }
|
||||
}
|
||||
item {
|
||||
if (canWrite) Button(onClick = {
|
||||
editingGroup = null
|
||||
grpName = ""
|
||||
grpDescription = ""
|
||||
grpType = "subscription"
|
||||
grpTargetGroup = ""
|
||||
grpSendToExternal = true
|
||||
groupDialogOpen = true
|
||||
}, modifier = Modifier.fillMaxWidth()) { Text("Gruppe erstellen") }
|
||||
}
|
||||
item { SectionTitle("Newsletter") }
|
||||
if (!state.loading && state.newsletters.isEmpty()) item { EmptyCard("Keine Newsletter gefunden.") }
|
||||
items(state.newsletters.size) { index -> NewsletterCard(state.newsletters[index]) }
|
||||
items(state.newsletters.size) { index ->
|
||||
val item = state.newsletters[index]
|
||||
NewsletterCard(item,
|
||||
onEdit = { nl ->
|
||||
editingNewsletter = nl
|
||||
nlTitle = nl.title
|
||||
nlContent = nl.title ?: ""
|
||||
nlType = "subscription"
|
||||
nlTargetGroup = ""
|
||||
nlSendToExternal = true
|
||||
newsletterDialogOpen = true
|
||||
},
|
||||
onDelete = { id -> viewModel.deleteNewsletter(id) },
|
||||
onSend = { id -> viewModel.sendNewsletter(id) }
|
||||
)
|
||||
}
|
||||
item { SectionTitle("Gruppen") }
|
||||
if (!state.loading && state.newsletterGroups.isEmpty()) item { EmptyCard("Keine Gruppen gefunden.") }
|
||||
items(state.newsletterGroups.size) { index -> NewsletterGroupCard(state.newsletterGroups[index]) }
|
||||
items(state.newsletterGroups.size) { index ->
|
||||
val group = state.newsletterGroups[index]
|
||||
NewsletterGroupCard(group,
|
||||
onEdit = { g ->
|
||||
editingGroup = g
|
||||
grpName = g.name
|
||||
grpDescription = g.description
|
||||
grpType = "subscription"
|
||||
grpTargetGroup = ""
|
||||
grpSendToExternal = true
|
||||
groupDialogOpen = true
|
||||
},
|
||||
onDelete = { id -> viewModel.deleteNewsletterGroup(id) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Newsletter create/edit dialog
|
||||
if (newsletterDialogOpen) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { newsletterDialogOpen = false },
|
||||
title = { Text(if (editingNewsletter == null) "Newsletter erstellen" else "Newsletter bearbeiten") },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text("Titel *")
|
||||
androidx.compose.material3.OutlinedTextField(value = nlTitle, onValueChange = { nlTitle = it })
|
||||
NativeRichTextEditor(nlContent, { nlContent = it }, "Inhalt *")
|
||||
Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically) {
|
||||
Checkbox(checked = nlType == "subscription", onCheckedChange = { if (it) nlType = "subscription" else nlType = "group" })
|
||||
Text("Abonnenten-Newsletter", modifier = Modifier.padding(start = 8.dp))
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Checkbox(checked = nlType == "group", onCheckedChange = { if (it) nlType = "group" else nlType = "subscription" })
|
||||
Text("Gruppen-Newsletter", modifier = Modifier.padding(start = 8.dp))
|
||||
}
|
||||
if (nlType == "group") {
|
||||
OutlinedTextField(value = nlTargetGroup, onValueChange = { nlTargetGroup = it }, label = { Text("Zielgruppe (Gruppen-ID)") })
|
||||
} else {
|
||||
Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically) {
|
||||
Checkbox(checked = nlSendToExternal, onCheckedChange = { nlSendToExternal = it })
|
||||
Text("Auch an externe Abonnenten senden", modifier = Modifier.padding(start = 8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(onClick = {
|
||||
// build request
|
||||
val req = de.harheimertc.data.NewsletterCreateRequest(
|
||||
title = nlTitle,
|
||||
content = nlContent,
|
||||
type = nlType,
|
||||
targetGroup = if (nlType == "group") nlTargetGroup else null,
|
||||
sendToExternal = if (nlType == "subscription") nlSendToExternal else null,
|
||||
)
|
||||
viewModel.saveNewsletter(req)
|
||||
newsletterDialogOpen = false
|
||||
}, enabled = !state.saving) { Text(if (state.saving) "Speichert..." else "Speichern") }
|
||||
},
|
||||
dismissButton = { TextButton(onClick = { newsletterDialogOpen = false }) { Text("Abbrechen") } },
|
||||
)
|
||||
}
|
||||
|
||||
// Group create/edit dialog
|
||||
if (groupDialogOpen) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { groupDialogOpen = false },
|
||||
title = { Text(if (editingGroup == null) "Gruppe erstellen" else "Gruppe bearbeiten") },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(value = grpName, onValueChange = { grpName = it }, label = { Text("Name *") })
|
||||
OutlinedTextField(value = grpDescription, onValueChange = { grpDescription = it }, label = { Text("Beschreibung") })
|
||||
Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically) {
|
||||
Checkbox(checked = grpType == "subscription", onCheckedChange = { if (it) grpType = "subscription" else grpType = "group" })
|
||||
Text("Abonnenten-Gruppe", modifier = Modifier.padding(start = 8.dp))
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Checkbox(checked = grpType == "group", onCheckedChange = { if (it) grpType = "group" else grpType = "subscription" })
|
||||
Text("Manuelle Gruppe", modifier = Modifier.padding(start = 8.dp))
|
||||
}
|
||||
if (grpType == "group") {
|
||||
OutlinedTextField(value = grpTargetGroup, onValueChange = { grpTargetGroup = it }, label = { Text("Zielgruppe (optional)") })
|
||||
} else {
|
||||
Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically) {
|
||||
Checkbox(checked = grpSendToExternal, onCheckedChange = { grpSendToExternal = it })
|
||||
Text("Auch an externe Abonnenten senden", modifier = Modifier.padding(start = 8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(onClick = {
|
||||
val payload = mapOf(
|
||||
"name" to grpName,
|
||||
"description" to grpDescription,
|
||||
"type" to grpType,
|
||||
"targetGroup" to (if (grpType == "group") grpTargetGroup else null),
|
||||
"sendToExternal" to (if (grpType == "subscription") grpSendToExternal else null),
|
||||
)
|
||||
viewModel.createNewsletterGroup(payload)
|
||||
groupDialogOpen = false
|
||||
}, enabled = !state.saving) { Text(if (state.saving) "Speichert..." else "Speichern") }
|
||||
},
|
||||
dismissButton = { TextButton(onClick = { groupDialogOpen = false }) { Text("Abbrechen") } },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,19 +583,31 @@ private fun ContactRequestCard(request: ContactRequestDto) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NewsletterCard(newsletter: NewsletterDto) {
|
||||
private fun NewsletterCard(newsletter: NewsletterDto, onEdit: (NewsletterDto) -> Unit = {}, onDelete: (String) -> Unit = {}, onSend: (String) -> Unit = {}) {
|
||||
val viewModel: CmsViewModel = hiltViewModel()
|
||||
DataCard(newsletter.subject.ifBlank { newsletter.title.ifBlank { newsletter.id } }) {
|
||||
InfoRow("Status", newsletter.status ?: if (newsletter.sentAt != null) "versendet" else "Entwurf")
|
||||
InfoRow("Erstellt", newsletter.createdAt ?: "-")
|
||||
InfoRow("Versendet", newsletter.sentAt ?: "-")
|
||||
Row {
|
||||
TextButton(onClick = { onEdit(newsletter) }) { Text("Bearbeiten") }
|
||||
TextButton(onClick = { newsletter.id.takeIf { it.isNotBlank() }?.let { onDelete(it) } }) { Text("Löschen") }
|
||||
if (newsletter.status != "sent") {
|
||||
TextButton(onClick = { newsletter.id.takeIf { it.isNotBlank() }?.let { onSend(it) } }) { Text("Versenden") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NewsletterGroupCard(group: NewsletterGroupDto) {
|
||||
private fun NewsletterGroupCard(group: NewsletterGroupDto, onEdit: (NewsletterGroupDto) -> Unit = {}, onDelete: (String) -> Unit = {}) {
|
||||
DataCard(group.name.ifBlank { group.id }) {
|
||||
InfoRow("Beschreibung", group.description.ifBlank { "-" })
|
||||
InfoRow("Abonnenten", group.subscribers.size.toString())
|
||||
Row {
|
||||
TextButton(onClick = { onEdit(group) }) { Text("Bearbeiten") }
|
||||
TextButton(onClick = { group.id.takeIf { it.isNotBlank() }?.let { onDelete(it) } }) { Text("Löschen") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -208,6 +208,98 @@ class CmsViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
// --- Newsletter (B4)
|
||||
fun saveNewsletter(request: de.harheimertc.data.NewsletterCreateRequest) {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(saving = true, error = null, message = null)
|
||||
repository.createNewsletter(request)
|
||||
.onSuccess { res ->
|
||||
val newslettersRes = repository.newsletters()
|
||||
_state.value = _state.value.copy(
|
||||
saving = false,
|
||||
newsletters = newslettersRes.getOrNull()?.newsletters.orEmpty(),
|
||||
message = res.message ?: "Newsletter gespeichert",
|
||||
)
|
||||
}
|
||||
.onFailure { err ->
|
||||
_state.value = _state.value.copy(saving = false, error = ErrorMapper.mapError(err) ?: "Newsletter konnte nicht gespeichert werden.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateNewsletter(id: String, patch: Map<String, Any?>) {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(saving = true, error = null, message = null)
|
||||
repository.updateNewsletter(id, patch)
|
||||
.onSuccess { res ->
|
||||
val newslettersRes = repository.newsletters()
|
||||
_state.value = _state.value.copy(saving = false, newsletters = newslettersRes.getOrNull()?.newsletters.orEmpty(), message = res.message ?: "Newsletter aktualisiert")
|
||||
}
|
||||
.onFailure { err -> _state.value = _state.value.copy(saving = false, error = ErrorMapper.mapError(err) ?: "Newsletter konnte nicht aktualisiert werden.") }
|
||||
}
|
||||
}
|
||||
|
||||
fun sendNewsletter(id: String) {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(saving = true, error = null, message = null)
|
||||
repository.sendNewsletter(id)
|
||||
.onSuccess { res ->
|
||||
val newslettersRes = repository.newsletters()
|
||||
_state.value = _state.value.copy(saving = false, newsletters = newslettersRes.getOrNull()?.newsletters.orEmpty(), message = res.message ?: "Newsletter versendet")
|
||||
}
|
||||
.onFailure { err -> _state.value = _state.value.copy(saving = false, error = ErrorMapper.mapError(err) ?: "Newsletter konnte nicht versendet werden.") }
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteNewsletter(id: String) {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(saving = true, error = null, message = null)
|
||||
repository.deleteNewsletter(id)
|
||||
.onSuccess { res ->
|
||||
val newslettersRes = repository.newsletters()
|
||||
_state.value = _state.value.copy(saving = false, newsletters = newslettersRes.getOrNull()?.newsletters.orEmpty(), message = res.message ?: "Newsletter gelöscht")
|
||||
}
|
||||
.onFailure { err -> _state.value = _state.value.copy(saving = false, error = ErrorMapper.mapError(err) ?: "Newsletter konnte nicht gelöscht werden.") }
|
||||
}
|
||||
}
|
||||
|
||||
// --- Newsletter Groups (B4)
|
||||
fun createNewsletterGroup(payload: Map<String, Any?>) {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(saving = true, error = null, message = null)
|
||||
repository.createNewsletterGroup(payload)
|
||||
.onSuccess { res ->
|
||||
val groupsRes = repository.newsletterGroups()
|
||||
_state.value = _state.value.copy(saving = false, newsletterGroups = groupsRes.getOrNull()?.groups.orEmpty(), message = res.message ?: "Gruppe erstellt")
|
||||
}
|
||||
.onFailure { err -> _state.value = _state.value.copy(saving = false, error = ErrorMapper.mapError(err) ?: "Gruppe konnte nicht erstellt werden.") }
|
||||
}
|
||||
}
|
||||
|
||||
fun updateNewsletterGroup(id: String, patch: Map<String, Any?>) {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(saving = true, error = null, message = null)
|
||||
repository.updateNewsletterGroup(id, patch)
|
||||
.onSuccess { res ->
|
||||
val groupsRes = repository.newsletterGroups()
|
||||
_state.value = _state.value.copy(saving = false, newsletterGroups = groupsRes.getOrNull()?.groups.orEmpty(), message = res.message ?: "Gruppe aktualisiert")
|
||||
}
|
||||
.onFailure { err -> _state.value = _state.value.copy(saving = false, error = ErrorMapper.mapError(err) ?: "Gruppe konnte nicht aktualisiert werden.") }
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteNewsletterGroup(id: String) {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(saving = true, error = null, message = null)
|
||||
repository.deleteNewsletterGroup(id)
|
||||
.onSuccess { res ->
|
||||
val groupsRes = repository.newsletterGroups()
|
||||
_state.value = _state.value.copy(saving = false, newsletterGroups = groupsRes.getOrNull()?.groups.orEmpty(), message = res.message ?: "Gruppe gelöscht")
|
||||
}
|
||||
.onFailure { err -> _state.value = _state.value.copy(saving = false, error = ErrorMapper.mapError(err) ?: "Gruppe konnte nicht gelöscht werden.") }
|
||||
}
|
||||
}
|
||||
|
||||
// --- User management actions (B2)
|
||||
fun updateUserRoles(id: String, roles: List<String>) {
|
||||
viewModelScope.launch {
|
||||
|
||||
@@ -11,6 +11,8 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
@@ -25,6 +27,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import android.util.Log
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
@@ -111,6 +114,11 @@ fun MembersScreen(
|
||||
else -> filtered.sortedWith(compareBy<MemberDto> { it.lastName.ifBlank { it.name } }.thenBy { it.firstName })
|
||||
}.let { if (sortAsc) it else it.asReversed() }
|
||||
|
||||
var viewMode by remember { mutableStateOf("cards") }
|
||||
var onlyHallKey by remember { mutableStateOf(false) }
|
||||
val display = remember(members, onlyHallKey) { if (!onlyHallKey) members else members.filter { it.hasHallKey } }
|
||||
Log.i("MembersScreen", "viewMode=$viewMode displayCount=${display.size}")
|
||||
|
||||
MemberAreaPage(navController, showBackNavigation, "Mitgliederliste", "Kontaktdaten der Vereinsmitglieder") {
|
||||
item {
|
||||
OutlinedTextField(
|
||||
@@ -134,11 +142,39 @@ fun MembersScreen(
|
||||
TextButton(onClick = { sortAsc = !sortAsc }) { Text(if (sortAsc) "A→Z" else "Z→A") }
|
||||
}
|
||||
}
|
||||
item {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(onClick = { viewMode = if (viewMode == "cards") "table" else "cards" }, colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFF3F4F6))) {
|
||||
Text(if (viewMode == "cards") "Tabelle" else "Karten", color = Accent900)
|
||||
}
|
||||
androidx.compose.material3.Checkbox(checked = onlyHallKey, onCheckedChange = { onlyHallKey = it })
|
||||
Text("Nur mit Hallenschlüssel", color = Accent700)
|
||||
}
|
||||
}
|
||||
item {
|
||||
if (viewMode == "table") {
|
||||
Text("DEBUG: TABLE", color = Color.Red, modifier = Modifier.fillMaxWidth().padding(8.dp))
|
||||
}
|
||||
}
|
||||
|
||||
when {
|
||||
state.loading -> item { CircularProgressIndicator(color = Primary600) }
|
||||
state.error != null -> item { ErrorCard(state.error.orEmpty(), viewModel::load) }
|
||||
members.isEmpty() -> item { Text("Keine Mitglieder gefunden.", color = Accent700) }
|
||||
else -> items(members.size) { index -> MemberCard(members[index]) }
|
||||
display.isEmpty() -> item { Text("Keine Mitglieder gefunden.", color = Accent700) }
|
||||
else -> if (viewMode == "table") {
|
||||
items(display.size) { index ->
|
||||
val m = display[index]
|
||||
Surface(color = Color.White, shape = RoundedCornerShape(6.dp)) {
|
||||
Row(Modifier.fillMaxWidth().padding(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Column(Modifier.weight(1f)) { Text(m.name, color = Accent900) }
|
||||
Column(Modifier.weight(1f)) { Text(m.email ?: "-", color = Primary600) }
|
||||
Column(Modifier.weight(1f)) { Text(m.phone ?: "-", color = Accent700) }
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
items(display.size) { index -> MemberCard(display[index]) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -220,7 +256,7 @@ private fun MemberAreaPage(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MemberCard(member: MemberDto) {
|
||||
private fun MemberCard(member: MemberDto, onEdit: (MemberDto) -> Unit = {}, onDelete: (MemberDto) -> Unit = {}) {
|
||||
Surface(color = Color.White, shape = RoundedCornerShape(14.dp), shadowElevation = 3.dp) {
|
||||
Column(Modifier.fillMaxWidth().padding(18.dp), verticalArrangement = Arrangement.spacedBy(7.dp)) {
|
||||
Text(member.name.ifBlank { "${member.firstName} ${member.lastName}".trim() }, style = MaterialTheme.typography.titleLarge, color = Accent900)
|
||||
@@ -238,6 +274,12 @@ private fun MemberCard(member: MemberDto) {
|
||||
if (member.email.isNullOrBlank() && member.phone.isNullOrBlank()) {
|
||||
Text("Kontaktdaten sind für dich nicht freigegeben.", color = Accent500)
|
||||
}
|
||||
Row {
|
||||
if (member.editable) {
|
||||
TextButton(onClick = { onEdit(member) }) { Text("Bearbeiten") }
|
||||
TextButton(onClick = { onDelete(member) }) { Text("Löschen") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,38 @@ class MembersViewModel @Inject constructor(
|
||||
.onFailure { _state.value = _state.value.copy(loading = false, error = it.message ?: "Mitglieder konnten nicht geladen werden.") }
|
||||
}
|
||||
}
|
||||
|
||||
fun saveMember(request: de.harheimertc.data.ApiService.MemberSaveRequest) {
|
||||
viewModelScope.launch {
|
||||
repository.saveMember(request)
|
||||
.onSuccess { _ -> load() }
|
||||
.onFailure { /* expose errors if needed */ }
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteMember(id: String) {
|
||||
viewModelScope.launch {
|
||||
repository.deleteMember(id)
|
||||
.onSuccess { _ -> load() }
|
||||
.onFailure { /* handle error */ }
|
||||
}
|
||||
}
|
||||
|
||||
fun bulkImport(members: List<Map<String, String>>) {
|
||||
viewModelScope.launch {
|
||||
repository.bulkImport(members)
|
||||
.onSuccess { _ -> load() }
|
||||
.onFailure { /* handle error */ }
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleMannschaftsspieler(memberId: String) {
|
||||
viewModelScope.launch {
|
||||
repository.toggleMannschaftsspieler(memberId)
|
||||
.onSuccess { _ -> load() }
|
||||
.onFailure { /* handle error */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class MemberNewsUiState(
|
||||
|
||||
BIN
android-app/member_screen.png
Normal file
BIN
android-app/member_screen.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 310 KiB |
Reference in New Issue
Block a user