Refactor code structure for improved readability and maintainability

This commit is contained in:
Torsten Schulz (local)
2026-05-29 10:55:59 +02:00
parent cdbe71eaec
commit 1ea9596006
9 changed files with 502 additions and 13 deletions

View File

@@ -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>

View File

@@ -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.")

View File

@@ -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.")

View File

@@ -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") }
}
}
}

View File

@@ -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 {

View File

@@ -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") }
}
}
}
}
}

View File

@@ -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(

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB