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
android:name=".HarheimerApplication"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:theme="@style/Theme.HarheimerTC"
android:usesCleartextTraffic="${usesCleartextTraffic}">
<activity android:name="de.harheimertc.MainActivity"

View File

@@ -289,13 +289,27 @@ private fun CmsUserListPage(navController: NavController, showBackNavigation: Bo
val viewModel: CmsViewModel = hiltViewModel()
val state by viewModel.state.collectAsState()
var sortAsc by remember { mutableStateOf(true) }
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 }
.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") {
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
if (pending.isNotEmpty()) {
items(pending.size) { index ->
@@ -306,7 +320,6 @@ private fun CmsUserListPage(navController: NavController, showBackNavigation: Bo
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") }
}
}
}

View File

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

View File

@@ -20,6 +20,9 @@ import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
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.graphics.Color
import androidx.compose.ui.text.font.FontWeight
@@ -45,13 +48,68 @@ fun MembersScreen(
) {
val state by viewModel.state.collectAsState()
val query = state.query.trim()
val members = state.members
.filter { member ->
query.isBlank() ||
member.name.contains(query, ignoreCase = true) ||
member.email.orEmpty().contains(query, ignoreCase = true)
var sortAsc by remember { mutableStateOf(true) }
var sortField by remember { mutableStateOf("Nachname") }
val filtered = state.members.filter { member ->
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") {
item {
@@ -63,6 +121,19 @@ fun MembersScreen(
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 {
state.loading -> item { CircularProgressIndicator(color = Primary600) }
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(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))
}
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)
if (!member.email.isNullOrBlank()) Text(member.email, color = Primary600)
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)) {
Badge(if (member.hasLogin) "Login" else member.source.ifBlank { "Mitglied" })
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_600">#dc2626</color>
<color name="accent_500">#71717a</color>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>