Refactor code structure for improved readability and maintainability

This commit is contained in:
Torsten Schulz (local)
2026-05-29 08:52:03 +02:00
parent 125a00819d
commit cdbe71eaec
12 changed files with 150 additions and 11 deletions

View File

@@ -6,6 +6,7 @@
<application <application
android:name=".HarheimerApplication" android:name=".HarheimerApplication"
android:label="@string/app_name" android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:theme="@style/Theme.HarheimerTC" android:theme="@style/Theme.HarheimerTC"
android:usesCleartextTraffic="${usesCleartextTraffic}"> android:usesCleartextTraffic="${usesCleartextTraffic}">
<activity android:name="de.harheimertc.MainActivity" <activity android:name="de.harheimertc.MainActivity"

View File

@@ -289,13 +289,27 @@ private fun CmsUserListPage(navController: NavController, showBackNavigation: Bo
val viewModel: CmsViewModel = hiltViewModel() val viewModel: CmsViewModel = hiltViewModel()
val state by viewModel.state.collectAsState() val state by viewModel.state.collectAsState()
var sortAsc by remember { mutableStateOf(true) }
val pending = state.users.filter { it.active == false } val pending = state.users.filter { it.active == false }
.sortedWith(compareBy({ (it.name.ifBlank { it.email ?: "" }).lowercase() }))
.let { if (sortAsc) it else it.asReversed() }
val active = state.users.filter { it.active == true } val active = state.users.filter { it.active == true }
.sortedWith(compareBy({ (it.name ?: "").lowercase() })) .sortedWith(compareBy({ (it.name.ifBlank { it.email ?: "" }).lowercase() }))
.let { if (sortAsc) it else it.asReversed() }
CmsPage(navController, showBackNavigation, title, "Registrierte Zugänge und Rollen") { CmsPage(navController, showBackNavigation, title, "Registrierte Zugänge und Rollen") {
if (state.users.isEmpty()) item { EmptyCard("Keine Benutzer gefunden oder keine Berechtigung.") } if (state.users.isEmpty()) item { EmptyCard("Keine Benutzer gefunden oder keine Berechtigung.") }
// Sort controls
item {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Sortierung", color = Accent700)
TextButton(onClick = { sortAsc = !sortAsc }) { Text(if (sortAsc) "A→Z" else "Z→A") }
}
}
// Pending (inactive) users first with highlighted background // Pending (inactive) users first with highlighted background
if (pending.isNotEmpty()) { if (pending.isNotEmpty()) {
items(pending.size) { index -> items(pending.size) { index ->
@@ -306,7 +320,6 @@ private fun CmsUserListPage(navController: NavController, showBackNavigation: Bo
Text(user.email ?: "-", color = Accent700) Text(user.email ?: "-", color = Accent700)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(top = 6.dp)) { 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.setUserActive(user.id, true) }, enabled = !state.saving) { Text("Freischalten") }
Button(onClick = { viewModel.resendInvite(user.id) }, enabled = !state.saving) { Text("Invite erneut") }
} }
} }
} }

View File

@@ -79,7 +79,7 @@ class CmsViewModel @Inject constructor(
val fetchedUsers = usersResult.getOrNull()?.users.orEmpty() val fetchedUsers = usersResult.getOrNull()?.users.orEmpty()
val pendingUsers = fetchedUsers.filter { it.active == false } val pendingUsers = fetchedUsers.filter { it.active == false }
val activeUsers = fetchedUsers.filter { it.active == true } val activeUsers = fetchedUsers.filter { it.active == true }
.sortedBy { (it.name ?: "").lowercase() } .sortedBy { it.name.lowercase() }
val orderedUsers = pendingUsers + activeUsers val orderedUsers = pendingUsers + activeUsers
_state.value = CmsUiState( _state.value = CmsUiState(

View File

@@ -20,6 +20,9 @@ import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@@ -45,13 +48,68 @@ fun MembersScreen(
) { ) {
val state by viewModel.state.collectAsState() val state by viewModel.state.collectAsState()
val query = state.query.trim() val query = state.query.trim()
val members = state.members var sortAsc by remember { mutableStateOf(true) }
.filter { member -> var sortField by remember { mutableStateOf("Nachname") }
query.isBlank() ||
member.name.contains(query, ignoreCase = true) || val filtered = state.members.filter { member ->
member.email.orEmpty().contains(query, ignoreCase = true) query.isBlank() ||
member.name.contains(query, ignoreCase = true) ||
member.email.orEmpty().contains(query, ignoreCase = true)
}
// helpers
fun dayMonthKey(m: MemberDto): Int {
val src = m.geburtsdatum ?: m.birthday ?: return Int.MAX_VALUE
val s = src.trim()
try {
if (s.matches(Regex("^\\d{4}[-./].*"))) {
val ld = java.time.LocalDate.parse(s)
return ld.monthValue * 100 + ld.dayOfMonth
}
} catch (_: Exception) { }
// fallback: handle ISO without year (MM-DD or M-D), or German (DD.MM(.YYYY))
val isoNoYear = Regex("^(\\d{1,2})[-/](\\d{1,2})$")
val german = Regex("^(\\d{1,2})\\.(\\d{1,2})(?:\\.(\\d{2,4}))?$")
isoNoYear.find(s)?.let {
val (mo, d) = it.destructured
return try { mo.toInt() * 100 + d.toInt() } catch (_: Exception) { Int.MAX_VALUE }
} }
.sortedWith(compareBy<MemberDto> { it.lastName.ifBlank { it.name } }.thenBy { it.firstName }) german.find(s)?.let {
val (d, mo, _) = it.destructured
return try { mo.toInt() * 100 + d.toInt() } catch (_: Exception) { Int.MAX_VALUE }
}
val r = Regex("(\\d{1,2})\\D+(\\d{1,2})")
val match = r.find(s) ?: return Int.MAX_VALUE
val (a, b) = match.destructured
return try {
if (a.toInt() > 12) b.toInt() * 100 + a.toInt() else a.toInt() * 100 + b.toInt()
} catch (_: Exception) { Int.MAX_VALUE }
}
fun ageKey(m: MemberDto): Int {
val src = m.geburtsdatum ?: m.birthday ?: return Int.MAX_VALUE
val s = src.trim()
try {
if (s.matches(Regex("^\\d{4}[-./].*"))) {
val ld = java.time.LocalDate.parse(s)
val current = java.time.LocalDate.now().year
return current - ld.year
}
} catch (_: Exception) { }
val germanYear = Regex("^(\\d{1,2})\\.(\\d{1,2})\\.(\\d{2,4})$")
germanYear.find(s)?.let {
val yearStr = it.groupValues[3]
return try { java.time.LocalDate.now().year - yearStr.toInt() } catch (_: Exception) { Int.MAX_VALUE }
}
return Int.MAX_VALUE
}
val members = when (sortField) {
"Vorname" -> filtered.sortedWith(compareBy({ it.firstName.ifBlank { it.name } }, { it.lastName }))
"Geburtstag" -> filtered.sortedWith(compareBy({ dayMonthKey(it) }, { it.lastName }))
"Alter" -> filtered.sortedWith(compareBy({ ageKey(it) }, { it.lastName }))
else -> filtered.sortedWith(compareBy<MemberDto> { it.lastName.ifBlank { it.name } }.thenBy { it.firstName })
}.let { if (sortAsc) it else it.asReversed() }
MemberAreaPage(navController, showBackNavigation, "Mitgliederliste", "Kontaktdaten der Vereinsmitglieder") { MemberAreaPage(navController, showBackNavigation, "Mitgliederliste", "Kontaktdaten der Vereinsmitglieder") {
item { item {
@@ -63,6 +121,19 @@ fun MembersScreen(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
} }
item {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Sortieren nach", color = Accent700)
var expanded by remember { mutableStateOf(false) }
TextButton(onClick = { expanded = true }) { Text(sortField) }
androidx.compose.material3.DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
listOf("Nachname", "Vorname", "Geburtstag", "Alter").forEach { opt ->
androidx.compose.material3.DropdownMenuItem(text = { Text(opt) }, onClick = { sortField = opt; expanded = false })
}
}
TextButton(onClick = { sortAsc = !sortAsc }) { Text(if (sortAsc) "A→Z" else "Z→A") }
}
}
when { when {
state.loading -> item { CircularProgressIndicator(color = Primary600) } state.loading -> item { CircularProgressIndicator(color = Primary600) }
state.error != null -> item { ErrorCard(state.error.orEmpty(), viewModel::load) } state.error != null -> item { ErrorCard(state.error.orEmpty(), viewModel::load) }
@@ -141,7 +212,7 @@ private fun MemberAreaPage(
Text("< Intern", color = Primary600, fontWeight = FontWeight.SemiBold) Text("< Intern", color = Primary600, fontWeight = FontWeight.SemiBold)
} }
} }
Text(title, style = MaterialTheme.typography.displayLarge, color = Accent900) Text(title, style = MaterialTheme.typography.headlineMedium, color = Accent900)
Text(subtitle, color = Accent500, modifier = Modifier.padding(top = 8.dp)) Text(subtitle, color = Accent500, modifier = Modifier.padding(top = 8.dp))
} }
content() content()
@@ -155,7 +226,10 @@ private fun MemberCard(member: MemberDto) {
Text(member.name.ifBlank { "${member.firstName} ${member.lastName}".trim() }, style = MaterialTheme.typography.titleLarge, color = Accent900) Text(member.name.ifBlank { "${member.firstName} ${member.lastName}".trim() }, style = MaterialTheme.typography.titleLarge, color = Accent900)
if (!member.email.isNullOrBlank()) Text(member.email, color = Primary600) if (!member.email.isNullOrBlank()) Text(member.email, color = Primary600)
if (!member.phone.isNullOrBlank()) Text(member.phone, color = Accent700) if (!member.phone.isNullOrBlank()) Text(member.phone, color = Accent700)
if (!member.birthday.isNullOrBlank()) Text("Geburtstag: ${member.birthday}", color = Accent500) if (!member.birthday.isNullOrBlank()) {
val display = formatDayMonth(member.birthday) ?: member.birthday
Text("Geburtstag: $display", color = Accent500)
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Badge(if (member.hasLogin) "Login" else member.source.ifBlank { "Mitglied" }) Badge(if (member.hasLogin) "Login" else member.source.ifBlank { "Mitglied" })
if (member.isMannschaftsspieler) Badge("Mannschaft") if (member.isMannschaftsspieler) Badge("Mannschaft")
@@ -201,3 +275,48 @@ private fun ErrorCard(message: String, onRetry: () -> Unit) {
} }
} }
} }
private fun normalizeToIso(srcRaw: String?): String? {
val src = srcRaw?.trim() ?: return null
try {
if (src.matches(Regex("^\\d{4}[-./].*"))) {
val ld = java.time.LocalDate.parse(src)
return String.format("%04d-%02d-%02d", ld.year, ld.monthValue, ld.dayOfMonth)
}
} catch (_: Exception) { }
val isoNoYear = Regex("^(\\d{1,2})[-/](\\d{1,2})$")
isoNoYear.find(src)?.let {
val (mo, d) = it.destructured
return String.format("%02d-%02d", mo.toInt(), d.toInt())
}
val german = Regex("^(\\d{1,2})\\.(\\d{1,2})(?:\\.(\\d{2,4}))?$")
german.find(src)?.let {
val (d, mo, y) = it.destructured
return if (y.isNullOrBlank()) String.format("%02d-%02d", mo.toInt(), d.toInt()) else String.format("%04d-%02d-%02d", y.toInt(), mo.toInt(), d.toInt())
}
return null
}
private fun formatDayMonth(srcRaw: String?): String? {
val src = srcRaw?.trim() ?: return null
try {
if (src.matches(Regex("^\\d{4}[-./].*"))) {
val ld = java.time.LocalDate.parse(src)
return String.format("%02d.%02d.", ld.dayOfMonth, ld.monthValue)
}
} catch (_: Exception) { }
val isoNoYear = Regex("^(\\d{1,2})[-/](\\d{1,2})$")
isoNoYear.find(src)?.let {
val (mo, d) = it.destructured
return try { String.format("%02d.%02d.", d.toInt(), mo.toInt()) } catch (_: Exception) { null }
}
val german = Regex("^(\\d{1,2})\\.(\\d{1,2})(?:\\.(\\d{2,4}))?$")
german.find(src)?.let {
val (d, mo, _) = it.destructured
return try { String.format("%02d.%02d.", d.toInt(), mo.toInt()) } catch (_: Exception) { null }
}
val r = Regex("(\\d{1,2})\\D+(\\d{1,2})")
val match = r.find(src) ?: return null
val (a, b) = match.destructured
return try { String.format("%02d.%02d.", a.toInt(), b.toInt()) } catch (_: Exception) { null }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

View File

@@ -3,4 +3,5 @@
<color name="primary_500">#ef4444</color> <color name="primary_500">#ef4444</color>
<color name="primary_600">#dc2626</color> <color name="primary_600">#dc2626</color>
<color name="accent_500">#71717a</color> <color name="accent_500">#71717a</color>
<color name="ic_launcher_background">#FFFFFF</color>
</resources> </resources>