Freundschaftsspiele korrigiert
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 52s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 52s
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import java.util.Properties
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
val backendBaseUrlForRelease = providers.gradleProperty("backendBaseUrl")
|
||||
@@ -12,6 +13,22 @@ val socketBaseUrl = providers.gradleProperty("socketBaseUrl")
|
||||
.orElse("wss://tt-tagebuch.de:3051")
|
||||
.get()
|
||||
|
||||
val signingPropertiesFile = rootProject.file("signing.properties")
|
||||
val signingProperties = Properties().apply {
|
||||
if (signingPropertiesFile.isFile) {
|
||||
signingPropertiesFile.inputStream().use(::load)
|
||||
}
|
||||
}
|
||||
|
||||
fun signingValue(name: String, envName: String): String? =
|
||||
signingProperties.getProperty(name)?.takeIf { it.isNotBlank() }
|
||||
?: System.getenv(envName)?.takeIf { it.isNotBlank() }
|
||||
|
||||
val releaseStoreFile = signingValue("storeFile", "TTT_RELEASE_STORE_FILE")
|
||||
val releaseStorePassword = signingValue("storePassword", "TTT_RELEASE_STORE_PASSWORD")
|
||||
val releaseKeyAlias = signingValue("keyAlias", "TTT_RELEASE_KEY_ALIAS")
|
||||
val releaseKeyPassword = signingValue("keyPassword", "TTT_RELEASE_KEY_PASSWORD")
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.androidApplication)
|
||||
@@ -70,6 +87,14 @@ android {
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
signingConfigs {
|
||||
create("releaseSigning") {
|
||||
if (releaseStoreFile != null) storeFile = file(releaseStoreFile)
|
||||
if (releaseStorePassword != null) storePassword = releaseStorePassword
|
||||
if (releaseKeyAlias != null) keyAlias = releaseKeyAlias
|
||||
if (releaseKeyPassword != null) keyPassword = releaseKeyPassword
|
||||
}
|
||||
}
|
||||
packaging {
|
||||
resources {
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
@@ -77,6 +102,9 @@ android {
|
||||
}
|
||||
buildTypes {
|
||||
getByName("release") {
|
||||
if (releaseStoreFile != null && releaseStorePassword != null && releaseKeyAlias != null && releaseKeyPassword != null) {
|
||||
signingConfig = signingConfigs.getByName("releaseSigning")
|
||||
}
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
|
||||
Binary file not shown.
@@ -1596,6 +1596,8 @@ private fun DiaryListScreen(
|
||||
var newDateGroupMenuExpanded by remember { mutableStateOf(false) }
|
||||
var selectedNewDateGroupId by remember { mutableStateOf<Int?>(null) }
|
||||
var quickCreateBusy by remember { mutableStateOf(false) }
|
||||
var autoSelectedEntryForClubId by rememberSaveable(clubId) { mutableStateOf<Int?>(null) }
|
||||
var suppressAutoSelect by rememberSaveable(clubId) { mutableStateOf(false) }
|
||||
|
||||
val diaryDatesNormKey = remember(diaryState.dates) {
|
||||
diaryState.dates.map { it.date.take(10).trim() }.sorted().joinToString("|")
|
||||
@@ -1644,35 +1646,35 @@ private fun DiaryListScreen(
|
||||
|
||||
LaunchedEffect(diaryState.dates, selectedEntryId) {
|
||||
try {
|
||||
Log.d("DiaryListDebug", "LaunchedEffect: selectedEntryId=$selectedEntryId dates=${diaryState.dates.map { it.id }}")
|
||||
if (selectedEntryId != null) {
|
||||
val found = diaryState.dates.any { it.id == selectedEntryId }
|
||||
Log.d("DiaryListDebug", "selectedEntryId present in dates? $found")
|
||||
if (found) {
|
||||
// Force parent/state sync in case order of events produced a stale state.
|
||||
onSelectedEntryId(selectedEntryId)
|
||||
}
|
||||
} else {
|
||||
} else if (!suppressAutoSelect && autoSelectedEntryForClubId == null) {
|
||||
// No selection set — if we have dates, default to the first one so details show immediately.
|
||||
if (diaryState.dates.isNotEmpty()) {
|
||||
val firstId = diaryState.dates.first().id
|
||||
Log.d("DiaryListDebug", "No selectedEntryId - defaulting to first date id=$firstId")
|
||||
autoSelectedEntryForClubId = firstId
|
||||
onSelectedEntryId(firstId)
|
||||
}
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
Log.d("DiaryListDebug", "error in debug effect: ${t.message}")
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
val selectedEntry = diaryState.dates.firstOrNull { it.id == selectedEntryId }
|
||||
if (selectedEntry != null) {
|
||||
Log.d("DiaryListDebug", "selectedEntry found -> id=${selectedEntry.id} date=${selectedEntry.date}")
|
||||
DiaryDetailScreen(
|
||||
clubId = clubId,
|
||||
entry = selectedEntry,
|
||||
dependencies = dependencies,
|
||||
onBack = { onSelectedEntryId(null) },
|
||||
onBack = {
|
||||
suppressAutoSelect = true
|
||||
onSelectedEntryId(null)
|
||||
},
|
||||
onOpenMemberPortraitCrop = onOpenMemberPortraitCrop,
|
||||
onOpenMembersGallery = onOpenMembersGallery,
|
||||
)
|
||||
|
||||
@@ -77,7 +77,6 @@ internal fun ScheduleScreen(dependencies: AppDependencies, friendlyOnly: Boolean
|
||||
val cm = context.getSystemService(android.content.Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
|
||||
cm.setPrimaryClip(ClipData.newPlainText("tt_tagebuch", value))
|
||||
}
|
||||
|
||||
var teamMenu by remember { mutableStateOf(false) }
|
||||
var otherTeamMenu by remember { mutableStateOf(false) }
|
||||
var detailMatch by remember { mutableStateOf<ScheduleMatchDto?>(null) }
|
||||
@@ -85,8 +84,38 @@ internal fun ScheduleScreen(dependencies: AppDependencies, friendlyOnly: Boolean
|
||||
var friendlyEditMatch by remember { mutableStateOf<ScheduleMatchDto?>(null) }
|
||||
var showFriendlyCreate by remember { mutableStateOf(false) }
|
||||
var friendlyResultMatch by remember { mutableStateOf<ScheduleMatchDto?>(null) }
|
||||
var friendlyHomeMembers by remember { mutableStateOf(emptyList<FriendlyMemberOption>()) }
|
||||
var friendlyGuestMembers by remember { mutableStateOf(emptyList<FriendlyMemberOption>()) }
|
||||
var playerError by remember { mutableStateOf<String?>(null) }
|
||||
var playerSaving by remember { mutableStateOf(false) }
|
||||
var playerDialogMembers by remember { mutableStateOf(emptyList<de.tsschulz.tt_tagebuch.shared.api.models.Member>()) }
|
||||
|
||||
suspend fun loadFriendlyMembersFor(match: ScheduleMatchDto?) {
|
||||
fun de.tsschulz.tt_tagebuch.shared.api.models.Member.toOption() = FriendlyMemberOption(id, "$firstName $lastName".trim())
|
||||
if (match?.isSharedFriendly == true) {
|
||||
friendlyHomeMembers = dependencies.matchesApi.listSharedFriendlyMembers(clubId, match.id, "home").filter { it.active }.map { it.toOption() }.sortedBy { it.name.lowercase() }
|
||||
friendlyGuestMembers = dependencies.matchesApi.listSharedFriendlyMembers(clubId, match.id, "guest").filter { it.active }.map { it.toOption() }.sortedBy { it.name.lowercase() }
|
||||
} else {
|
||||
dependencies.membersManager.loadMembers(clubId)
|
||||
val options = dependencies.matchesApi.listFriendlyMembers(clubId).filter { it.active }.map { it.toOption() }.sortedBy { it.name.lowercase() }
|
||||
friendlyHomeMembers = options
|
||||
friendlyGuestMembers = emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loadPlayerDialogMembersFor(match: ScheduleMatchDto) {
|
||||
playerDialogMembers = if (match.isFriendly && match.isSharedFriendly) {
|
||||
val ownSide = if (match.homeClubId == clubId) "home" else "guest"
|
||||
dependencies.matchesApi.listSharedFriendlyMembers(clubId, match.id, ownSide)
|
||||
.filter { it.active }
|
||||
.sortedBy { "${it.firstName} ${it.lastName}".lowercase() }
|
||||
} else if (match.isFriendly) {
|
||||
dependencies.matchesApi.listFriendlyMembers(clubId).filter { it.active }.sortedBy { "${it.firstName} ${it.lastName}".lowercase() }
|
||||
} else {
|
||||
dependencies.membersManager.loadMembers(clubId)
|
||||
dependencies.membersManager.state.value.members.filter { it.active }.sortedBy { "${it.firstName} ${it.lastName}".lowercase() }
|
||||
}
|
||||
}
|
||||
|
||||
var readyIds by remember { mutableStateOf(emptyList<Int>()) }
|
||||
var plannedIds by remember { mutableStateOf(emptyList<Int>()) }
|
||||
@@ -393,13 +422,13 @@ internal fun ScheduleScreen(dependencies: AppDependencies, friendlyOnly: Boolean
|
||||
friendlyResultMatch = m
|
||||
detailMatch = null
|
||||
},
|
||||
) { Text("Ergebnis") }
|
||||
) { Text(if (isFriendlyMatchLocked(m)) "Ansehen" else "Ergebnis") }
|
||||
TextButton(
|
||||
onClick = {
|
||||
friendlyEditMatch = m
|
||||
detailMatch = null
|
||||
},
|
||||
) { Text("Bearbeiten") }
|
||||
) { Text(if (isFriendlyMatchLocked(m)) "Details" else "Bearbeiten") }
|
||||
} else {
|
||||
TextButton(
|
||||
onClick = {
|
||||
@@ -421,11 +450,16 @@ internal fun ScheduleScreen(dependencies: AppDependencies, friendlyOnly: Boolean
|
||||
}
|
||||
|
||||
if (showFriendlyCreate || friendlyEditMatch != null) {
|
||||
LaunchedEffect(friendlyEditMatch?.id, showFriendlyCreate) {
|
||||
loadFriendlyMembersFor(friendlyEditMatch)
|
||||
}
|
||||
FriendlyMatchEditDialog(
|
||||
match = friendlyEditMatch,
|
||||
clubName = clubState.clubs.find { it.id == clubId }?.name.orEmpty(),
|
||||
memberOptions = membersState.members.filter { it.active }.map { FriendlyMemberOption(it.id, "${it.firstName} ${it.lastName}".trim()) },
|
||||
onLoadMembers = { scope.launch { dependencies.membersManager.loadMembers(clubId) } },
|
||||
homeMemberOptions = friendlyFilteredMembers(friendlyEditMatch, friendlyHomeMembers),
|
||||
guestMemberOptions = friendlyFilteredMembers(friendlyEditMatch, friendlyGuestMembers),
|
||||
readonly = isFriendlyMatchLocked(friendlyEditMatch),
|
||||
onLoadMembers = { scope.launch { loadFriendlyMembersFor(friendlyEditMatch) } },
|
||||
onDismiss = {
|
||||
showFriendlyCreate = false
|
||||
friendlyEditMatch = null
|
||||
@@ -433,6 +467,7 @@ internal fun ScheduleScreen(dependencies: AppDependencies, friendlyOnly: Boolean
|
||||
onSave = { body ->
|
||||
scope.launch {
|
||||
if (friendlyEditMatch != null) {
|
||||
if (isFriendlyMatchLocked(friendlyEditMatch)) return@launch
|
||||
dependencies.scheduleManager.updateFriendlyMatch(clubId, friendlyEditMatch!!.id, body)
|
||||
} else {
|
||||
dependencies.scheduleManager.createFriendlyMatch(clubId, body)
|
||||
@@ -444,6 +479,7 @@ internal fun ScheduleScreen(dependencies: AppDependencies, friendlyOnly: Boolean
|
||||
onDelete = if (friendlyEditMatch != null) {
|
||||
{
|
||||
scope.launch {
|
||||
if (isFriendlyMatchLocked(friendlyEditMatch)) return@launch
|
||||
dependencies.scheduleManager.deleteFriendlyMatch(clubId, friendlyEditMatch!!.id)
|
||||
friendlyEditMatch = null
|
||||
}
|
||||
@@ -453,12 +489,15 @@ internal fun ScheduleScreen(dependencies: AppDependencies, friendlyOnly: Boolean
|
||||
}
|
||||
|
||||
friendlyResultMatch?.let { match ->
|
||||
LaunchedEffect(match.id) { loadFriendlyMembersFor(match) }
|
||||
FriendlyResultDialog(
|
||||
match = match,
|
||||
memberOptions = membersState.members.filter { it.active }.map { FriendlyMemberOption(it.id, "${it.firstName} ${it.lastName}".trim()) },
|
||||
memberOptions = friendlyHomeMembers + friendlyGuestMembers,
|
||||
readonly = isFriendlyMatchLocked(match),
|
||||
onDismiss = { friendlyResultMatch = null },
|
||||
onSave = { body ->
|
||||
scope.launch {
|
||||
if (isFriendlyMatchLocked(match)) return@launch
|
||||
dependencies.scheduleManager.updateFriendlyMatch(clubId, match.id, body)
|
||||
val updated = body.toMatchLike(match)
|
||||
friendlyResultMatch = updated
|
||||
@@ -466,6 +505,7 @@ internal fun ScheduleScreen(dependencies: AppDependencies, friendlyOnly: Boolean
|
||||
},
|
||||
onComplete = { body ->
|
||||
scope.launch {
|
||||
if (isFriendlyMatchLocked(match)) return@launch
|
||||
dependencies.scheduleManager.updateFriendlyMatch(clubId, match.id, body.copy(isCompleted = true))
|
||||
friendlyResultMatch = null
|
||||
}
|
||||
@@ -475,7 +515,7 @@ internal fun ScheduleScreen(dependencies: AppDependencies, friendlyOnly: Boolean
|
||||
|
||||
playerMatch?.let { m ->
|
||||
LaunchedEffect(m.id, clubId) {
|
||||
dependencies.membersManager.loadMembers(clubId)
|
||||
loadPlayerDialogMembersFor(m)
|
||||
}
|
||||
AlertDialog(
|
||||
onDismissRequest = { if (!playerSaving) playerMatch = null },
|
||||
@@ -483,8 +523,8 @@ internal fun ScheduleScreen(dependencies: AppDependencies, friendlyOnly: Boolean
|
||||
text = {
|
||||
Column(modifier = Modifier.heightIn(max = 400.dp)) {
|
||||
playerError?.let { Text(it, color = MaterialTheme.colors.error) }
|
||||
val memberList = membersState.members.filter { it.active }
|
||||
if (membersState.isLoading) {
|
||||
val memberList = playerDialogMembers
|
||||
if (membersState.isLoading && memberList.isEmpty()) {
|
||||
CircularProgressIndicator(modifier = Modifier.padding(16.dp))
|
||||
} else {
|
||||
val scroll = rememberScrollState()
|
||||
@@ -528,18 +568,21 @@ internal fun ScheduleScreen(dependencies: AppDependencies, friendlyOnly: Boolean
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
enabled = !playerSaving,
|
||||
enabled = !playerSaving && !isFriendlyMatchLocked(m),
|
||||
onClick = {
|
||||
scope.launch {
|
||||
playerSaving = true
|
||||
playerError = null
|
||||
runCatching {
|
||||
val visibleIds = playerDialogMembers.map { it.id }.toSet()
|
||||
fun mergeVisible(existing: List<Int>, selected: List<Int>): List<Int> =
|
||||
(existing.filter { it !in visibleIds } + selected.filter { it in visibleIds }).distinct()
|
||||
dependencies.scheduleManager.updateMatchPlayersForMatch(
|
||||
clubId = clubId,
|
||||
match = m,
|
||||
ready = readyIds,
|
||||
planned = plannedIds,
|
||||
played = playedIds,
|
||||
ready = mergeVisible(m.playersReady, readyIds),
|
||||
planned = mergeVisible(m.playersPlanned, plannedIds),
|
||||
played = mergeVisible(m.playersPlayed, playedIds),
|
||||
)
|
||||
playerMatch = null
|
||||
}.onFailure { playerError = it.message ?: tr("schedule.errorSavingPlayerSelection", "Speichern fehlgeschlagen") }
|
||||
@@ -563,7 +606,9 @@ private data class FriendlyMemberOption(val id: Int, val name: String)
|
||||
private fun FriendlyMatchEditDialog(
|
||||
match: ScheduleMatchDto?,
|
||||
clubName: String,
|
||||
memberOptions: List<FriendlyMemberOption>,
|
||||
homeMemberOptions: List<FriendlyMemberOption>,
|
||||
guestMemberOptions: List<FriendlyMemberOption>,
|
||||
readonly: Boolean,
|
||||
onLoadMembers: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
onSave: (FriendlyMatchSaveBody) -> Unit,
|
||||
@@ -578,28 +623,72 @@ private fun FriendlyMatchEditDialog(
|
||||
var winningSetsText by remember(match?.id) { mutableStateOf((match?.winningSets ?: 3).toString()) }
|
||||
var homeParticipants by remember(match?.id) { mutableStateOf(match?.homeParticipants ?: emptyList()) }
|
||||
var guestParticipants by remember(match?.id) { mutableStateOf(match?.guestParticipants ?: emptyList()) }
|
||||
var resultRows by remember(match?.id) { mutableStateOf(match?.resultDetails ?: emptyList()) }
|
||||
var error by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
fun doubleRows(): List<FriendlyResultRowDto> {
|
||||
val template = friendlyResultTemplate(matchSystem, homeParticipants.size, guestParticipants.size, match?.doublesCount ?: 4, match?.singlesCount ?: 12)
|
||||
val doubleIds = template.filter { it.type == "double" }.map { it.id }
|
||||
val existingById = resultRows.filter { it.id.isNotBlank() }.associateBy { it.id }
|
||||
val singles = resultRows.filter { it.type != "double" }
|
||||
val normalized = doubleIds.map { id ->
|
||||
val row = existingById[id] ?: FriendlyResultRowDto(id = id, type = "double", sets = List(5) { "" })
|
||||
row.copy(id = id, type = "double", sets = List(5) { row.sets.getOrNull(it).orEmpty() })
|
||||
}
|
||||
resultRows = normalized + singles
|
||||
return normalized
|
||||
}
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(if (match == null) "Freundschaftsspiel anlegen" else "Freundschaftsspiel bearbeiten") },
|
||||
text = {
|
||||
Column(Modifier.verticalScroll(rememberScrollState())) {
|
||||
error?.let { Text(it, color = MaterialTheme.colors.error) }
|
||||
OutlinedTextField(date, { date = it }, label = { Text("Datum") }, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(time, { time = it }, label = { Text("Uhrzeit") }, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(homeTeam, { homeTeam = it }, label = { Text("Heimteam") }, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(guestTeam, { guestTeam = it }, label = { Text("Gastteam") }, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(matchSystem, { matchSystem = it }, label = { Text("Spielsystem") }, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(winningSetsText, { winningSetsText = it.filter(Char::isDigit) }, label = { Text("Gewinnsätze") }, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(date, { date = it }, label = { Text("Datum") }, enabled = !readonly, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(time, { time = it }, label = { Text("Uhrzeit") }, enabled = !readonly, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(homeTeam, { homeTeam = it }, label = { Text("Heimteam") }, enabled = !readonly, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(guestTeam, { guestTeam = it }, label = { Text("Gastteam") }, enabled = !readonly, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(matchSystem, { matchSystem = it }, label = { Text("Spielsystem") }, enabled = !readonly, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(winningSetsText, { winningSetsText = it.filter(Char::isDigit) }, label = { Text("Gewinnsätze") }, enabled = !readonly, modifier = Modifier.fillMaxWidth())
|
||||
Spacer(Modifier.height(8.dp))
|
||||
FriendlyParticipantEditor("Heim-Aufstellung", memberOptions, homeParticipants) { homeParticipants = it }
|
||||
FriendlyParticipantEditor("Heim-Aufstellung", homeMemberOptions, homeParticipants, allowMembers = true, allowManual = false, readonly = readonly) { homeParticipants = it }
|
||||
Spacer(Modifier.height(8.dp))
|
||||
FriendlyParticipantEditor("Gast-Aufstellung", memberOptions, guestParticipants) { guestParticipants = it }
|
||||
FriendlyParticipantEditor("Gast-Aufstellung", guestMemberOptions, guestParticipants, allowMembers = guestMemberOptions.isNotEmpty(), allowManual = guestMemberOptions.isEmpty(), readonly = readonly) { guestParticipants = it }
|
||||
val doubles = doubleRows()
|
||||
if (doubles.isNotEmpty()) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text("Doppel", fontWeight = FontWeight.SemiBold)
|
||||
doubles.forEachIndexed { index, row ->
|
||||
Card(Modifier.fillMaxWidth().padding(vertical = 4.dp), elevation = 1.dp) {
|
||||
Column(Modifier.padding(8.dp)) {
|
||||
Text("Doppel ${index + 1}", fontWeight = FontWeight.SemiBold)
|
||||
FriendlyResultPlayerSelector(
|
||||
label = "Heim",
|
||||
row = row,
|
||||
fieldValue = row.homeName,
|
||||
sideLabels = friendlyResultSideLabels(homeParticipants, homeMemberOptions),
|
||||
rows = doubles,
|
||||
readonly = readonly,
|
||||
onChange = { value -> resultRows = resultRows.map { if (it.id == row.id) it.copy(homeName = value) else it } },
|
||||
)
|
||||
FriendlyResultPlayerSelector(
|
||||
label = "Gast",
|
||||
row = row,
|
||||
fieldValue = row.guestName,
|
||||
sideLabels = friendlyResultSideLabels(guestParticipants, guestMemberOptions),
|
||||
rows = doubles,
|
||||
readonly = readonly,
|
||||
onChange = { value -> resultRows = resultRows.map { if (it.id == row.id) it.copy(guestName = value) else it } },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
if (!readonly) TextButton(
|
||||
onClick = {
|
||||
val winningSets = winningSetsText.toIntOrNull()?.takeIf { it > 0 } ?: 3
|
||||
if (date.isBlank() || homeTeam.isBlank() || guestTeam.isBlank()) {
|
||||
@@ -619,7 +708,7 @@ private fun FriendlyMatchEditDialog(
|
||||
homeMatchPoints = match?.homeMatchPoints ?: 0,
|
||||
guestMatchPoints = match?.guestMatchPoints ?: 0,
|
||||
isCompleted = match?.isCompleted ?: false,
|
||||
resultDetails = match?.resultDetails ?: emptyList(),
|
||||
resultDetails = resultRows,
|
||||
),
|
||||
)
|
||||
},
|
||||
@@ -627,7 +716,7 @@ private fun FriendlyMatchEditDialog(
|
||||
},
|
||||
dismissButton = {
|
||||
Row {
|
||||
onDelete?.let {
|
||||
if (!readonly) onDelete?.let {
|
||||
TextButton(onClick = it) { Text("Löschen") }
|
||||
}
|
||||
TextButton(onClick = onDismiss) { Text("Abbrechen") }
|
||||
@@ -641,18 +730,21 @@ private fun FriendlyParticipantEditor(
|
||||
title: String,
|
||||
members: List<FriendlyMemberOption>,
|
||||
participants: List<FriendlyParticipantDto>,
|
||||
allowMembers: Boolean = true,
|
||||
allowManual: Boolean = true,
|
||||
readonly: Boolean = false,
|
||||
onChange: (List<FriendlyParticipantDto>) -> Unit,
|
||||
) {
|
||||
var menuOpen by remember { mutableStateOf(false) }
|
||||
var manualName by remember { mutableStateOf("") }
|
||||
Column {
|
||||
Text(title, fontWeight = FontWeight.SemiBold)
|
||||
Box(Modifier.fillMaxWidth()) {
|
||||
if (!readonly && allowMembers) Box(Modifier.fillMaxWidth()) {
|
||||
OutlinedButton(onClick = { menuOpen = true }, modifier = Modifier.fillMaxWidth()) {
|
||||
Text("Mitglied hinzufügen")
|
||||
}
|
||||
DropdownMenu(expanded = menuOpen, onDismissRequest = { menuOpen = false }) {
|
||||
members.forEach { member ->
|
||||
members.filter { member -> participants.none { it.type == "member" && it.memberId == member.id } }.forEach { member ->
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
@@ -664,7 +756,7 @@ private fun FriendlyParticipantEditor(
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) {
|
||||
if (!readonly && allowManual) Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) {
|
||||
OutlinedTextField(
|
||||
manualName,
|
||||
{ manualName = it },
|
||||
@@ -685,7 +777,7 @@ private fun FriendlyParticipantEditor(
|
||||
participants.forEachIndexed { index, participant ->
|
||||
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(participantLabel(participant, members), modifier = Modifier.weight(1f))
|
||||
TextButton(onClick = { onChange(participants.filterIndexed { i, _ -> i != index }) }) { Text("x") }
|
||||
if (!readonly) TextButton(onClick = { onChange(participants.filterIndexed { i, _ -> i != index }) }) { Text("x") }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -695,6 +787,7 @@ private fun FriendlyParticipantEditor(
|
||||
private fun FriendlyResultDialog(
|
||||
match: ScheduleMatchDto,
|
||||
memberOptions: List<FriendlyMemberOption>,
|
||||
readonly: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
onSave: (FriendlyMatchSaveBody) -> Unit,
|
||||
onComplete: (FriendlyMatchSaveBody) -> Unit,
|
||||
@@ -731,7 +824,8 @@ private fun FriendlyResultDialog(
|
||||
Card(Modifier.fillMaxWidth().padding(vertical = 4.dp), elevation = 1.dp) {
|
||||
Column(Modifier.padding(8.dp)) {
|
||||
Text("${index + 1}. ${if (row.type == "double") "Doppel" else "Einzel"}", fontWeight = FontWeight.SemiBold)
|
||||
Text("${row.homeName} : ${row.guestName}", style = MaterialTheme.typography.caption)
|
||||
Text("Heim: ${row.homeName.ifBlank { "-" }}", style = MaterialTheme.typography.caption)
|
||||
Text("Gast: ${row.guestName.ifBlank { "-" }}", style = MaterialTheme.typography.caption)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
val state = friendlyRowState(row, match.winningSets)
|
||||
(0 until 5).forEach { setIndex ->
|
||||
@@ -748,13 +842,13 @@ private fun FriendlyResultDialog(
|
||||
}
|
||||
},
|
||||
label = { Text("${setIndex + 1}") },
|
||||
enabled = !disabled,
|
||||
enabled = !readonly && !disabled,
|
||||
modifier = Modifier.weight(1f),
|
||||
singleLine = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
OutlinedButton(
|
||||
if (!readonly) OutlinedButton(
|
||||
onClick = {
|
||||
val normalized = normalizeFriendlyRow(row, match.winningSets)
|
||||
if (normalized == null) {
|
||||
@@ -773,11 +867,134 @@ private fun FriendlyResultDialog(
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = { TextButton(onClick = { onComplete(body(true)) }) { Text("Abschließen") } },
|
||||
confirmButton = { if (!readonly) TextButton(onClick = { onComplete(body(true)) }) { Text("Abschließen") } },
|
||||
dismissButton = { TextButton(onClick = onDismiss) { Text("Schließen") } },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FriendlyResultPlayerSelector(
|
||||
label: String,
|
||||
row: FriendlyResultRowDto,
|
||||
fieldValue: String,
|
||||
sideLabels: List<String>,
|
||||
rows: List<FriendlyResultRowDto>,
|
||||
readonly: Boolean,
|
||||
onChange: (String) -> Unit,
|
||||
) {
|
||||
if (row.type == "double") {
|
||||
Column {
|
||||
FriendlyResultNameMenu(
|
||||
label = "$label 1",
|
||||
value = friendlyDoublePart(fieldValue, 0),
|
||||
options = friendlyResultNameOptions(sideLabels, rows, row, label, fieldValue, 0),
|
||||
readonly = readonly,
|
||||
onChange = { value -> onChange(friendlySetDoublePart(fieldValue, 0, value)) },
|
||||
)
|
||||
FriendlyResultNameMenu(
|
||||
label = "$label 2",
|
||||
value = friendlyDoublePart(fieldValue, 1),
|
||||
options = friendlyResultNameOptions(sideLabels, rows, row, label, fieldValue, 1),
|
||||
readonly = readonly,
|
||||
onChange = { value -> onChange(friendlySetDoublePart(fieldValue, 1, value)) },
|
||||
)
|
||||
}
|
||||
} else {
|
||||
FriendlyResultNameMenu(
|
||||
label = label,
|
||||
value = fieldValue,
|
||||
options = friendlyResultNameOptions(sideLabels, rows, row, label, fieldValue, null),
|
||||
readonly = readonly,
|
||||
onChange = onChange,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FriendlyResultNameMenu(
|
||||
label: String,
|
||||
value: String,
|
||||
options: List<String>,
|
||||
readonly: Boolean,
|
||||
onChange: (String) -> Unit,
|
||||
) {
|
||||
var open by remember { mutableStateOf(false) }
|
||||
Box(Modifier.fillMaxWidth()) {
|
||||
OutlinedButton(
|
||||
enabled = !readonly,
|
||||
onClick = { open = true },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) { Text(value.ifBlank { label }) }
|
||||
DropdownMenu(expanded = open, onDismissRequest = { open = false }) {
|
||||
DropdownMenuItem(onClick = { open = false; onChange("") }) { Text("-") }
|
||||
options.forEach { option ->
|
||||
DropdownMenuItem(onClick = { open = false; onChange(option) }) { Text(option) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun friendlyResultSideLabels(participants: List<FriendlyParticipantDto>, members: List<FriendlyMemberOption>): List<String> =
|
||||
participants.map { participantLabel(it, members) }.filter { it.isNotBlank() }
|
||||
|
||||
private fun friendlyDoublePart(value: String, index: Int): String =
|
||||
value.split("/").map { it.trim() }.getOrNull(index).orEmpty()
|
||||
|
||||
private fun friendlySetDoublePart(value: String, index: Int, part: String): String {
|
||||
val parts = mutableListOf(friendlyDoublePart(value, 0), friendlyDoublePart(value, 1))
|
||||
parts[index] = part
|
||||
return parts.filter { it.isNotBlank() }.joinToString(" / ")
|
||||
}
|
||||
|
||||
private fun friendlyResultNameOptions(
|
||||
labels: List<String>,
|
||||
rows: List<FriendlyResultRowDto>,
|
||||
currentRow: FriendlyResultRowDto,
|
||||
label: String,
|
||||
currentValue: String,
|
||||
doubleIndex: Int?,
|
||||
): List<String> {
|
||||
val fieldSelector: (FriendlyResultRowDto) -> String = if (label.startsWith("Heim")) { row -> row.homeName } else { row -> row.guestName }
|
||||
val used = mutableSetOf<String>()
|
||||
rows.filter { it != currentRow }.forEach { row ->
|
||||
val value = fieldSelector(row)
|
||||
if (row.type == "double") {
|
||||
friendlyDoublePart(value, 0).takeIf { it.isNotBlank() }?.let { used.add(it) }
|
||||
friendlyDoublePart(value, 1).takeIf { it.isNotBlank() }?.let { used.add(it) }
|
||||
} else if (value.isNotBlank()) {
|
||||
used.add(value)
|
||||
}
|
||||
}
|
||||
val current = if (doubleIndex == null) currentValue else friendlyDoublePart(currentValue, doubleIndex)
|
||||
if (doubleIndex != null) {
|
||||
friendlyDoublePart(currentValue, if (doubleIndex == 0) 1 else 0).takeIf { it.isNotBlank() }?.let { used.add(it) }
|
||||
}
|
||||
return labels.filter { it == current || it !in used }
|
||||
}
|
||||
|
||||
private fun isFriendlyMatchLocked(match: ScheduleMatchDto?): Boolean {
|
||||
if (match?.isFriendly != true) return false
|
||||
if (match.isLocked) return true
|
||||
val date = match.date?.take(10) ?: return false
|
||||
return runCatching {
|
||||
val endsAt = java.time.LocalDate.parse(date).atTime(23, 59, 59)
|
||||
!endsAt.isAfter(java.time.LocalDateTime.now())
|
||||
}.getOrDefault(false)
|
||||
}
|
||||
|
||||
private fun friendlyEligibleMemberIds(match: ScheduleMatchDto?): Set<Int>? {
|
||||
fun ids(value: List<Int>) = value.toSet()
|
||||
val ready = ids(match?.playersReady ?: emptyList())
|
||||
val planned = ids(match?.playersPlanned ?: emptyList())
|
||||
if (ready.isEmpty() || planned.isEmpty()) return null
|
||||
return ready.intersect(planned)
|
||||
}
|
||||
|
||||
private fun friendlyFilteredMembers(match: ScheduleMatchDto?, members: List<FriendlyMemberOption>): List<FriendlyMemberOption> {
|
||||
val eligible = friendlyEligibleMemberIds(match) ?: return members
|
||||
return members.filter { it.id in eligible }
|
||||
}
|
||||
|
||||
private fun FriendlyMatchSaveBody.toMatchLike(match: ScheduleMatchDto): ScheduleMatchDto =
|
||||
match.copy(
|
||||
homeMatchPoints = homeMatchPoints,
|
||||
@@ -793,26 +1010,113 @@ private fun participantLabel(participant: FriendlyParticipantDto, members: List<
|
||||
return listOf(participant.firstName, participant.lastName).filter { it.isNotBlank() }.joinToString(" ")
|
||||
}
|
||||
|
||||
private data class FriendlyResultTemplateRow(
|
||||
val id: String,
|
||||
val type: String,
|
||||
val home: String,
|
||||
val guest: String,
|
||||
)
|
||||
|
||||
private fun friendlyResultTemplate(
|
||||
matchSystem: String?,
|
||||
homeCount: Int,
|
||||
guestCount: Int,
|
||||
fallbackDoublesCount: Int,
|
||||
fallbackSinglesCount: Int,
|
||||
): List<FriendlyResultTemplateRow> {
|
||||
val system = matchSystem.orEmpty().trim().lowercase()
|
||||
fun d(id: String, home: String, guest: String) = FriendlyResultTemplateRow(id, "double", home, guest)
|
||||
fun s(id: String, home: String, guest: String) = FriendlyResultTemplateRow(id, "single", home, guest)
|
||||
if ("bundessystem" in system) return listOf(
|
||||
d("d-1", "D1", "D1"), d("d-2", "D2", "D2"),
|
||||
s("s-1", "A1", "B1"), s("s-2", "A2", "B2"), s("s-3", "A3", "B3"), s("s-4", "A4", "B4"),
|
||||
s("s-5", "A1", "B2"), s("s-6", "A2", "B1"), s("s-7", "A3", "B4"), s("s-8", "A4", "B3"),
|
||||
)
|
||||
if ("werner" in system) return listOf(
|
||||
d("d-1", "D1", "D1"), d("d-2", "D2", "D2"),
|
||||
s("s-1", "A1", "B2"), s("s-2", "A2", "B1"), s("s-3", "A3", "B4"), s("s-4", "A4", "B3"),
|
||||
s("s-5", "A1", "B1"), s("s-6", "A2", "B2"), s("s-7", "A3", "B3"), s("s-8", "A4", "B4"),
|
||||
)
|
||||
if ("sechser" in system) return listOf(
|
||||
d("d-1", "D1", "D1"), d("d-2", "D2", "D2"), d("d-3", "D3", "D3"),
|
||||
s("s-1", "A1", "B2"), s("s-2", "A2", "B1"), s("s-3", "A1", "B1"), s("s-4", "A2", "B2"),
|
||||
s("s-5", "A3", "B4"), s("s-6", "A4", "B3"), s("s-7", "A3", "B3"), s("s-8", "A4", "B4"),
|
||||
s("s-9", "A5", "B6"), s("s-10", "A6", "B5"), s("s-11", "A5", "B5"), s("s-12", "A6", "B6"),
|
||||
d("d-4", "D1", "D1"),
|
||||
)
|
||||
if ("europaliga" in system) return listOf(
|
||||
d("d-1", "D1", "D1"), d("d-2", "D2", "D2"), d("d-3", "D3", "D3"),
|
||||
s("s-1", "A1", "B1"), s("s-2", "A2", "B2"), s("s-3", "A1", "B2"), s("s-4", "A2", "B1"),
|
||||
s("s-5", "A3", "B3"), s("s-6", "A4", "B4"), s("s-7", "A3", "B4"), s("s-8", "A4", "B3"),
|
||||
s("s-9", "A5", "B5"), s("s-10", "A6", "B6"), s("s-11", "A5", "B6"), s("s-12", "A6", "B5"),
|
||||
)
|
||||
if ("corbillon" in system) return listOf(
|
||||
s("s-1", "A1", "B1"), s("s-2", "A2", "B2"), d("d-1", "D1", "D1"), s("s-3", "A1", "B2"), s("s-4", "A2", "B1"),
|
||||
)
|
||||
if ("modifiziertes swaythling" in system) return listOf(
|
||||
s("s-1", "A1", "B2"), s("s-2", "A2", "B1"), s("s-3", "A3", "B3"), d("d-1", "D1", "D1"),
|
||||
s("s-4", "A1", "B1"), s("s-5", "A3", "B2"), s("s-6", "A2", "B3"),
|
||||
)
|
||||
if ("swaythling" in system) return listOf(
|
||||
s("s-1", "A1", "B1"), s("s-2", "A2", "B2"), s("s-3", "A3", "B3"), s("s-4", "A1", "B2"), s("s-5", "A2", "B1"),
|
||||
)
|
||||
if ("braunschweiger" in system) {
|
||||
if (homeCount >= 4 && guestCount >= 4) return listOf(
|
||||
d("d-1", "D1", "D1"), d("d-2", "D2", "D2"),
|
||||
s("s-1", "A1", "B1"), s("s-2", "A2", "B2"), s("s-3", "A3", "B3"), s("s-4", "A4", "B4"),
|
||||
s("s-5", "A1", "B2"), s("s-6", "A2", "B1"), s("s-7", "A3", "B4"), s("s-8", "A4", "B3"),
|
||||
)
|
||||
if (homeCount >= 4 && guestCount <= 3) return listOf(
|
||||
d("d-1", "D1", "D1"),
|
||||
s("s-1", "A3", "B3"), s("s-2", "A1", "B2"), s("s-3", "A2", "B1"), s("s-4", "A4", "B2"),
|
||||
s("s-5", "A1", "B1"), s("s-6", "A4", "B3"), s("s-7", "A2", "B2"), s("s-8", "A1", "B3"), s("s-9", "A3", "B1"),
|
||||
)
|
||||
if (homeCount <= 3 && guestCount >= 4) return listOf(
|
||||
d("d-1", "D1", "D1"),
|
||||
s("s-1", "A3", "B3"), s("s-2", "A2", "B1"), s("s-3", "A1", "B2"), s("s-4", "A2", "B4"),
|
||||
s("s-5", "A1", "B1"), s("s-6", "A3", "B4"), s("s-7", "A2", "B2"), s("s-8", "A3", "B1"), s("s-9", "A1", "B3"),
|
||||
)
|
||||
return listOf(
|
||||
d("d-1", "D1", "D1"),
|
||||
s("s-1", "A1", "B2"), s("s-2", "A2", "B1"), s("s-3", "A3", "B2"), s("s-4", "A2", "B3"),
|
||||
s("s-5", "A1", "B1"), s("s-6", "A3", "B3"), s("s-7", "A2", "B2"), s("s-8", "A3", "B1"), s("s-9", "A1", "B3"),
|
||||
)
|
||||
}
|
||||
val homeSlots = homeCount.coerceAtLeast(1)
|
||||
val guestSlots = guestCount.coerceAtLeast(1)
|
||||
return buildList {
|
||||
repeat(fallbackDoublesCount.coerceAtLeast(0)) { add(d("d-${it + 1}", "D${it + 1}", "D${it + 1}")) }
|
||||
repeat(fallbackSinglesCount.coerceAtLeast(0)) { add(s("s-${it + 1}", "A${(it % homeSlots) + 1}", "B${(it % guestSlots) + 1}")) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildFriendlyResultRows(match: ScheduleMatchDto, members: List<FriendlyMemberOption>): List<FriendlyResultRowDto> {
|
||||
val existing = match.resultDetails
|
||||
val generated = generateFriendlyResultRows(match, members)
|
||||
if (existing.isNotEmpty()) {
|
||||
return existing.mapIndexed { index, row ->
|
||||
row.copy(
|
||||
homeName = row.homeName.ifBlank { generated.getOrNull(index)?.homeName.orEmpty() },
|
||||
guestName = row.guestName.ifBlank { generated.getOrNull(index)?.guestName.orEmpty() },
|
||||
sets = List(5) { row.sets.getOrNull(it).orEmpty() },
|
||||
if (existing.isEmpty()) return generated
|
||||
|
||||
val existingById = existing.filter { it.id.isNotBlank() }.associateBy { it.id }
|
||||
return generated.mapIndexed { index, generatedRow ->
|
||||
val existingRow = existingById[generatedRow.id] ?: existing.getOrNull(index)
|
||||
if (existingRow == null) {
|
||||
generatedRow
|
||||
} else {
|
||||
generatedRow.copy(
|
||||
sets = List(5) { existingRow.sets.getOrNull(it) ?: generatedRow.sets.getOrNull(it).orEmpty() },
|
||||
completed = existingRow.completed,
|
||||
)
|
||||
}
|
||||
}
|
||||
return generated
|
||||
}
|
||||
|
||||
private fun generateFriendlyResultRows(match: ScheduleMatchDto, members: List<FriendlyMemberOption>): List<FriendlyResultRowDto> {
|
||||
val home = match.homeParticipants.map { participantLabel(it, members) }.filter { it.isNotBlank() }
|
||||
val guest = match.guestParticipants.map { participantLabel(it, members) }.filter { it.isNotBlank() }
|
||||
fun single(list: List<String>, index: Int): String = list.getOrNull(index % kotlin.math.max(list.size, 1)).orEmpty()
|
||||
fun double(list: List<String>, index: Int): String {
|
||||
fun player(list: List<String>, code: String): String {
|
||||
val number = Regex("[AB](\\d+)", RegexOption.IGNORE_CASE).find(code)?.groupValues?.getOrNull(1)?.toIntOrNull() ?: return ""
|
||||
return list.getOrNull(number - 1).orEmpty()
|
||||
}
|
||||
fun fallbackDouble(list: List<String>, index: Int): String {
|
||||
if (list.isEmpty()) return ""
|
||||
if (list.size == 1) return list.first()
|
||||
val pairs = listOf(0 to 1, 2 to 3, 0 to 2, 1 to 3)
|
||||
@@ -821,13 +1125,20 @@ private fun generateFriendlyResultRows(match: ScheduleMatchDto, members: List<Fr
|
||||
val b = list[pair.second % list.size]
|
||||
return if (a == b) a else "$a / $b"
|
||||
}
|
||||
return buildList {
|
||||
repeat(match.doublesCount.coerceAtLeast(0)) { i ->
|
||||
add(FriendlyResultRowDto(id = "d-${i + 1}", type = "double", homeName = double(home, i), guestName = double(guest, i), sets = List(5) { "" }))
|
||||
}
|
||||
repeat(match.singlesCount.coerceAtLeast(0)) { i ->
|
||||
add(FriendlyResultRowDto(id = "s-${i + 1}", type = "single", homeName = single(home, i), guestName = single(guest, i), sets = List(5) { "" }))
|
||||
}
|
||||
fun doubleName(list: List<String>, side: String, code: String): String {
|
||||
val number = Regex("D[A-Z]?(\\d+)", RegexOption.IGNORE_CASE).find(code)?.groupValues?.getOrNull(1)?.toIntOrNull() ?: 1
|
||||
val existing = match.resultDetails.find { it.id == "d-$number" }
|
||||
val value = if (side == "guest") existing?.guestName else existing?.homeName
|
||||
return value?.takeIf { it.isNotBlank() } ?: fallbackDouble(list, number - 1)
|
||||
}
|
||||
return friendlyResultTemplate(match.matchSystem, home.size, guest.size, match.doublesCount, match.singlesCount).map { row ->
|
||||
FriendlyResultRowDto(
|
||||
id = row.id,
|
||||
type = row.type,
|
||||
homeName = if (row.type == "double") doubleName(home, "home", row.home) else player(home, row.home),
|
||||
guestName = if (row.type == "double") doubleName(guest, "guest", row.guest) else player(guest, row.guest),
|
||||
sets = List(5) { "" },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -839,7 +1150,7 @@ private fun normalizeFriendlySet(value: String): String? {
|
||||
if (parts.size != 2) return null
|
||||
val a = parts[0].toIntOrNull() ?: return null
|
||||
val b = parts[1].toIntOrNull() ?: return null
|
||||
if (a < 0 || b < 0 || (a < 11 && b < 11) || kotlin.math.abs(a - b) < 2) return null
|
||||
if (a < 0 || b < 0 || kotlin.math.max(a, b) < 11 || kotlin.math.abs(a - b) < 2) return null
|
||||
return "$a:$b"
|
||||
}
|
||||
val losing = raw.removePrefix("-").toIntOrNull() ?: return null
|
||||
|
||||
Reference in New Issue
Block a user