Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
BIN
android-app/app/src/main/res/drawable/ic_launcher_foreground.png
Normal file
BIN
android-app/app/src/main/res/drawable/ic_launcher_foreground.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
@@ -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>
|
||||
BIN
android-app/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
android-app/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
BIN
android-app/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
android-app/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
BIN
android-app/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
android-app/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
BIN
android-app/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
android-app/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
BIN
android-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
android-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
@@ -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>
|
||||
Reference in New Issue
Block a user