feat: add TTR simulator and calculation engine, enhance member model with TTR adjustment constant
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 59s

This commit is contained in:
Torsten Schulz (local)
2026-10-05 15:18:10 +02:00
parent 6fa54163f6
commit 6d982add75
21 changed files with 466 additions and 27 deletions

View File

@@ -5259,20 +5259,36 @@ private fun MembersScreen(
}
.sortedWith(
Comparator { a, b ->
val base = when (selectedSort) {
"ttr" -> (a.ttr ?: Int.MIN_VALUE).compareTo(b.ttr ?: Int.MIN_VALUE)
"qttr" -> (a.qttr ?: Int.MIN_VALUE).compareTo(b.qttr ?: Int.MIN_VALUE)
val compareNames = {
val byLast = a.lastName.lowercase().compareTo(b.lastName.lowercase())
if (byLast != 0) byLast else a.firstName.lowercase().compareTo(b.firstName.lowercase())
}
if (selectedSort == "ttr" || selectedSort == "qttr") {
val ratingA = if (selectedSort == "ttr") a.ttr else a.qttr
val ratingB = if (selectedSort == "ttr") b.ttr else b.qttr
// Wie in der Web-App: nicht bewertete Mitglieder stehen
// unabhängig von der Sortierrichtung immer am Listenende.
when {
ratingA == null && ratingB != null -> 1
ratingA != null && ratingB == null -> -1
ratingA == null && ratingB == null -> compareNames()
else -> {
val direction = if (sortAscending) 1 else -1
val ratingDiff = ratingA!!.compareTo(ratingB!!) * direction
if (ratingDiff != 0) ratingDiff else compareNames()
}
}
} else {
val base = when (selectedSort) {
"lastTraining" -> a.lastTraining.orEmpty().compareTo(b.lastTraining.orEmpty())
"birthDate" -> a.birthDate.orEmpty().compareTo(b.birthDate.orEmpty())
"firstName" -> a.firstName.lowercase().compareTo(b.firstName.lowercase())
"age" -> (memberAgeYears(a) ?: Int.MIN_VALUE).compareTo(memberAgeYears(b) ?: Int.MIN_VALUE)
"openTasks" -> memberOpenTasksCount(a, qualityReq).compareTo(memberOpenTasksCount(b, qualityReq))
else -> {
val byLast = a.lastName.lowercase().compareTo(b.lastName.lowercase())
if (byLast != 0) byLast else a.firstName.lowercase().compareTo(b.firstName.lowercase())
}
else -> compareNames()
}
if (sortAscending) base else -base
}
if (sortAscending) base else -base
},
)
}

View File

@@ -128,6 +128,15 @@ internal object TeamEditorLineupLogic {
return q.toString()
}
/** QTTR-Reihenfolge für Meldungen: bewertete Spieler zuerst, danach Name. */
fun compareForLineup(a: Member, b: Member): Int {
val ratingDiff = lineupRatingValue(b).compareTo(lineupRatingValue(a))
if (ratingDiff != 0) return ratingDiff
val byLast = a.lastName.lowercase().compareTo(b.lastName.lowercase())
if (byLast != 0) return byLast
return a.firstName.lowercase().compareTo(b.firstName.lowercase())
}
fun eligibilityLabel(member: Member, teamAgeGroup: String, seasonLabel: String): String {
val mag = memberAgeGroupCode(member, seasonLabel)
if (teamAgeGroup != "adult" || mag == "adult") return "OK"
@@ -194,11 +203,7 @@ internal object TeamEditorLineupLogic {
byCode.getOrPut(code) { mutableListOf() }.add(row)
}
for (list in byCode.values) {
list.sortWith(
compareByDescending<MemberDisplay> { lineupRatingValue(it.member) }
.thenBy { it.member.lastName.lowercase() }
.thenBy { it.member.firstName.lowercase() },
)
list.sortWith { a, b -> compareForLineup(a.member, b.member) }
}
return groupOrder.mapNotNull { code ->
val rows = byCode[code] ?: return@mapNotNull null

View File

@@ -176,7 +176,7 @@ internal fun TeamPlanningScreen(
if (q.isEmpty()) true
else "${it.firstName} ${it.lastName}".lowercase().contains(q)
}
.sortedWith(compareBy({ it.lastName.lowercase() }, { it.firstName.lowercase() }))
.sortedWith(TeamEditorLineupLogic::compareForLineup)
}
val assignedIds = remember(assignments) { assignments.map { it.memberId }.toSet() }
@@ -189,7 +189,7 @@ internal fun TeamPlanningScreen(
members.filter { m ->
m.active && m.testMembership != true && m.id !in interestedIds &&
(q.isEmpty() || "${m.firstName} ${m.lastName}".lowercase().contains(q))
}.sortedWith(compareBy({ it.lastName.lowercase() }, { it.firstName.lowercase() }))
}.sortedWith(TeamEditorLineupLogic::compareForLineup)
}
fun persistTeam(teamId: Int, nextAssignments: List<PlanningAssignment>, onDone: () -> Unit = {}) {
@@ -281,6 +281,10 @@ internal fun TeamPlanningScreen(
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
Column(Modifier.weight(1f)) {
Text("${m.firstName} ${m.lastName}", fontWeight = FontWeight.Medium)
Text(
"QTTR ${TeamEditorLineupLogic.lineupRatingLabel(m)}",
style = MaterialTheme.typography.caption,
)
}
if (canWrite) {
var addMenu by remember(key1 = m.id) { mutableStateOf(false) }
@@ -344,10 +348,12 @@ internal fun TeamPlanningScreen(
enabled = canWrite && !markingInterest,
modifier = Modifier.fillMaxWidth(),
) {
Text(
addInterestMemberId?.let { id ->
membersById[id]?.let { "${it.firstName} ${it.lastName}" }
} ?: t("teamManagement.selectMember", "Mitglied wählen"),
Text(
addInterestMemberId?.let { id ->
membersById[id]?.let {
"${it.firstName} ${it.lastName} (QTTR ${TeamEditorLineupLogic.lineupRatingLabel(it)})"
}
} ?: t("teamManagement.selectMember", "Mitglied wählen"),
)
}
DropdownMenu(expanded = addInterestMenu, onDismissRequest = { addInterestMenu = false }) {
@@ -357,7 +363,7 @@ internal fun TeamPlanningScreen(
addInterestMemberId = m.id
addInterestMenu = false
},
) { Text("${m.firstName} ${m.lastName}") }
) { Text("${m.firstName} ${m.lastName} (QTTR ${TeamEditorLineupLogic.lineupRatingLabel(m)})") }
}
}
}
@@ -604,7 +610,7 @@ private fun CardTeamPlanningBlock(
Text(t("teamManagement.selectedLineup", "Meldung"), fontWeight = FontWeight.Medium)
ordered.forEachIndexed { index, m ->
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
Text("${index + 1}. ${m.firstName} ${m.lastName}")
Text("${index + 1}. ${m.firstName} ${m.lastName} (${TeamEditorLineupLogic.lineupRatingLabel(m)})")
if (canWrite) {
Row {
TextButton(onClick = { onMoveInTeam(m.id, -1) }, enabled = index > 0) { Text("↑") }
@@ -635,7 +641,7 @@ private fun CardTeamPlanningBlock(
onPersistLineup(next)
}
},
) { Text("${m.firstName} ${m.lastName}") }
) { Text("${m.firstName} ${m.lastName} (${TeamEditorLineupLogic.lineupRatingLabel(m)})") }
}
}
}