diff --git a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/AppRoot.kt b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/AppRoot.kt index 6ce13a11..09c5edc3 100644 --- a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/AppRoot.kt +++ b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/AppRoot.kt @@ -38,7 +38,6 @@ import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.AlertDialog import androidx.compose.material.BottomNavigation import androidx.compose.material.BottomNavigationItem import androidx.compose.material.Button @@ -595,7 +594,7 @@ private fun MobileFeedbackDialog( var sending by remember { mutableStateOf(false) } var error by remember { mutableStateOf(null) } - AlertDialog( + StableAlertDialog( onDismissRequest = { if (!sending) onDismiss() }, title = { Text("Feedback senden") }, text = { @@ -1901,7 +1900,7 @@ private fun DiaryListScreen( } if (showNewDateDialog) { - AlertDialog( + StableAlertDialog( onDismissRequest = { if (!diaryState.isLoading) { showNewDateDialog = false @@ -2566,7 +2565,7 @@ private fun DiaryDetailScreen( ordersForMemberId?.let { memberId -> val member = activeMembers.firstOrNull { it.id == memberId } - AlertDialog( + StableAlertDialog( onDismissRequest = { ordersForMemberId = null ordersList = emptyList() @@ -2610,7 +2609,7 @@ private fun DiaryDetailScreen( statsForMemberId?.let { memberId -> val member = activeMembers.firstOrNull { it.id == memberId } - AlertDialog( + StableAlertDialog( onDismissRequest = { statsForMemberId = null statsList = emptyList() @@ -2792,7 +2791,7 @@ private fun DiaryDetailScreen( !isExcused && ((m.hasImage == true) || (m.imageUrl != null) || (m.primaryImageId != null) || (m.images.isNotEmpty())) } - AlertDialog( + StableAlertDialog( onDismissRequest = { showMembersGalleryDialog = false }, text = { Column(modifier = Modifier.fillMaxWidth()) { @@ -3349,7 +3348,7 @@ private fun DiaryDetailScreen( memberNotesSheetMember?.let { sheetMember -> val notesScroll = rememberScrollState() - AlertDialog( + StableAlertDialog( onDismissRequest = { memberNotesSheetMember = null memberNotesError = null @@ -4480,7 +4479,7 @@ private fun DiaryDetailScreen( val presentParticipantRows = participants .filter { it.isPresentParticipant() } .sortedBy { p -> activeMembers.find { it.id == p.memberId }?.fullName()?.lowercase() ?: "" } - AlertDialog( + StableAlertDialog( onDismissRequest = { if (!assignParticipantsBusy) assigningParticipantsItem = null }, title = { Text(tr("diary.assignParticipants", "Teilnehmer zuordnen")) }, text = { @@ -4584,7 +4583,7 @@ private fun DiaryDetailScreen( assigningPlanItem?.let { assignItem -> var assignGroupMenu by remember { mutableStateOf(false) } - AlertDialog( + StableAlertDialog( onDismissRequest = { if (!planMutating) assigningPlanItem = null }, title = { Text(tr("diary.planAssignGroup", "Zuordnen")) }, text = { @@ -4659,7 +4658,7 @@ private fun DiaryDetailScreen( editingPlanItem?.let { editItem -> var editGroupMenu by remember { mutableStateOf(false) } - AlertDialog( + StableAlertDialog( onDismissRequest = { if (!planMutating) editingPlanItem = null }, title = { Text(tr("diary.editPlanItem", "Eintrag bearbeiten")) }, text = { @@ -4911,7 +4910,7 @@ private fun DiaryDetailScreen( } planImageViewerUrl?.let { u -> val auth = dependencies.diaryAuthHeaders() - AlertDialog( + StableAlertDialog( onDismissRequest = { planImageViewerUrl = null }, confirmButton = { TextButton(onClick = { planImageViewerUrl = null }) { @@ -5111,6 +5110,22 @@ private fun MembersScreen( var membersActionNote by remember { mutableStateOf(null) } var pendingBulkForms by remember { mutableStateOf(false) } var pendingBulkTestOff by remember { mutableStateOf(false) } + var pendingDeactivateMember by remember { mutableStateOf(null) } + var clickTtPendingMemberIds by remember { mutableStateOf>(emptySet()) } + var ordersForMember by remember { mutableStateOf(null) } + var ordersList by remember { mutableStateOf>(emptyList()) } + var ordersLoading by remember { mutableStateOf(false) } + var ordersError by remember { mutableStateOf(null) } + var statsForMember by remember { mutableStateOf(null) } + var statsList by remember { mutableStateOf>(emptyList()) } + var statsLastParticipations by remember { mutableStateOf>(emptyList()) } + var statsLoading by remember { mutableStateOf(false) } + var statsError by remember { mutableStateOf(null) } + var notesForMember by remember { mutableStateOf(null) } + var notesList by remember { mutableStateOf>(emptyList()) } + var notesDraft by rememberSaveable { mutableStateOf("") } + var notesLoading by remember { mutableStateOf(false) } + var notesError by remember { mutableStateOf(null) } var seasonStartYear by rememberSaveable { mutableStateOf(getSeasonStartYearFromDateToday()) } var seasonMenuOpen by remember { mutableStateOf(false) } var selectedAgeGroup by rememberSaveable { mutableStateOf("") } @@ -5299,10 +5314,86 @@ private fun MembersScreen( val filteredEmailsPlain = remember(filteredMembers) { filteredMembers.flatMap { extractEmailAddressesFromMember(it) }.distinct() } + fun loadMemberOrders(member: Member) { + ordersForMember = member + ordersLoading = true + ordersError = null + ordersList = emptyList() + dependencies.applicationScope.launch { + try { + ordersList = dependencies.memberOrdersApi.listForMember(clubId, member.id).orders + } catch (t: Throwable) { + ordersError = t.message + } finally { + ordersLoading = false + } + } + } + fun loadMemberStats(member: Member) { + statsForMember = member + statsLoading = true + statsError = null + statsList = emptyList() + statsLastParticipations = emptyList() + dependencies.applicationScope.launch { + try { + coroutineScope { + val allStats = async { dependencies.membersManager.memberActivityStats(clubId, member.id, "all") } + val recent = async { dependencies.membersManager.memberLastParticipations(clubId, member.id, 8) } + statsList = allStats.await() + statsLastParticipations = recent.await() + } + } catch (t: Throwable) { + statsError = t.message + } finally { + statsLoading = false + } + } + } + fun loadMemberNotes(member: Member) { + notesForMember = member + notesDraft = "" + notesLoading = true + notesError = null + notesList = emptyList() + dependencies.applicationScope.launch { + try { + notesList = dependencies.membersManager.listMemberNotes(clubId, member.id) + } catch (t: Throwable) { + notesError = t.message + } finally { + notesLoading = false + } + } + } + fun quickMarkForm(member: Member) { + if (!canWriteMembers) return + dependencies.applicationScope.launch { + val r = runCatching { dependencies.membersManager.quickUpdateMemberFormHandedOver(clubId, member.id) } + r.onSuccess { membersActionNote = it.message ?: trStr("members.formMarkedAsHandedOver", "Formularstatus aktualisiert.") } + .onFailure { membersActionNote = it.message ?: trStr("members.errorMarkingForm", "Formularstatus konnte nicht geändert werden.") } + dependencies.membersManager.loadMembers(clubId) + } + } + fun requestClickTt(member: Member) { + if (!canWriteMembers || clickTtPendingMemberIds.contains(member.id)) return + clickTtPendingMemberIds = clickTtPendingMemberIds + member.id + dependencies.applicationScope.launch { + try { + val r = dependencies.membersManager.requestClickTtRegistration(clubId, member.id) + membersActionNote = r.message ?: r.error ?: trStr("members.clickTtRequestSuccess", "Click-TT-Antrag angestoßen.") + dependencies.membersManager.loadMembers(clubId) + } catch (t: Throwable) { + membersActionNote = t.message ?: trStr("members.clickTtRequestError", "Click-TT-Antrag konnte nicht gestartet werden.") + } finally { + clickTtPendingMemberIds = clickTtPendingMemberIds - member.id + } + } + } if (pendingBulkForms && canWriteMembers) { val targets = filteredMembers.filter { it.memberFormHandedOver != true } - AlertDialog( + StableAlertDialog( onDismissRequest = { pendingBulkForms = false }, title = { Text(tr("members.bulkFormsTitle", "Formulare übergeben")) }, text = { @@ -5347,7 +5438,7 @@ private fun MembersScreen( } if (pendingBulkTestOff && canWriteMembers) { val targets = filteredMembers.filter { it.testMembership == true } - AlertDialog( + StableAlertDialog( onDismissRequest = { pendingBulkTestOff = false }, title = { Text(tr("members.bulkTestOffTitle", "Testmitgliedschaften beenden")) }, text = { @@ -5391,6 +5482,151 @@ private fun MembersScreen( ) } + pendingDeactivateMember?.let { member -> + StableAlertDialog( + onDismissRequest = { pendingDeactivateMember = null }, + title = { Text(tr("members.deactivateMember", "Mitglied deaktivieren")) }, + text = { Text(trStr("members.deactivateMemberConfirm", "%s deaktivieren?").replace("%s", member.fullName()).replace("{name}", member.fullName())) }, + confirmButton = { + TextButton( + onClick = { + pendingDeactivateMember = null + dependencies.applicationScope.launch { + val r = runCatching { dependencies.membersManager.quickDeactivateMember(clubId, member.id) } + r.onSuccess { membersActionNote = it.message ?: trStr("members.memberDeactivated", "Mitglied deaktiviert.") } + .onFailure { membersActionNote = it.message ?: trStr("members.errorDeactivatingMember", "Mitglied konnte nicht deaktiviert werden.") } + dependencies.membersManager.loadMembers(clubId) + } + }, + ) { Text(tr("mobile.ok", "OK")) } + }, + dismissButton = { TextButton(onClick = { pendingDeactivateMember = null }) { Text(tr("mobile.cancel", "Abbrechen")) } }, + ) + } + ordersForMember?.let { member -> + StableAlertDialog( + onDismissRequest = { ordersForMember = null; ordersList = emptyList(); ordersError = null }, + title = { Text(trStr("orders.memberTitle", "Bestellungen: {name}").replace("{name}", member.fullName())) }, + text = { + Column(modifier = Modifier.fillMaxWidth().heightIn(max = 420.dp).verticalScroll(rememberScrollState())) { + when { + ordersLoading -> CircularProgressIndicator(modifier = Modifier.padding(24.dp).align(Alignment.CenterHorizontally)) + ordersError != null -> Text(ordersError.orEmpty(), color = MaterialTheme.colors.error) + ordersList.isEmpty() -> Text(tr("orders.noOrdersMember", "Für dieses Mitglied gibt es noch keine Bestellungen.")) + else -> ordersList.forEach { order -> + Column(modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp)) { + Text(order.item, fontWeight = FontWeight.SemiBold) + Text(order.status, style = MaterialTheme.typography.caption) + order.cost?.let { Text("${trStr("orders.cost", "Kosten")}: $it", style = MaterialTheme.typography.caption) } + Divider(modifier = Modifier.padding(top = 6.dp)) + } + } + } + } + }, + confirmButton = { TextButton(onClick = { ordersForMember = null; ordersList = emptyList() }) { Text(tr("common.close", "Schließen")) } }, + ) + } + statsForMember?.let { member -> + StableAlertDialog( + onDismissRequest = { statsForMember = null; statsList = emptyList(); statsLastParticipations = emptyList(); statsError = null }, + title = { Text(tr("members.exercises", "Übungen") + ": " + member.fullName()) }, + text = { + Column(modifier = Modifier.fillMaxWidth().heightIn(max = 460.dp).verticalScroll(rememberScrollState())) { + when { + statsLoading -> CircularProgressIndicator(modifier = Modifier.padding(24.dp).align(Alignment.CenterHorizontally)) + statsError != null -> Text(statsError.orEmpty(), color = MaterialTheme.colors.error) + statsList.isEmpty() && statsLastParticipations.isEmpty() -> Text(trStr("memberActivities.noActivities", "Keine Übungen im gewählten Zeitraum gefunden.")) + else -> { + if (statsList.isNotEmpty()) { + Text(tr("members.activityStats", "Aktivität (Statistik)"), fontWeight = FontWeight.SemiBold) + statsList.take(20).forEach { row -> + Text("${row.name ?: row.code ?: "?"} - ${row.count}x", style = MaterialTheme.typography.body2, modifier = Modifier.padding(top = 3.dp)) + } + } + if (statsLastParticipations.isNotEmpty()) { + Spacer(modifier = Modifier.height(10.dp)) + Text(tr("members.lastParticipations", "Letzte Teilnahmen"), fontWeight = FontWeight.SemiBold) + statsLastParticipations.forEach { part -> + Text("${displayActivityDate(part.date)} ${part.activityName ?: part.activityFullName ?: ""}", style = MaterialTheme.typography.body2, modifier = Modifier.padding(top = 3.dp)) + } + } + } + } + } + }, + confirmButton = { TextButton(onClick = { statsForMember = null; statsList = emptyList(); statsLastParticipations = emptyList() }) { Text(tr("common.close", "Schließen")) } }, + ) + } + notesForMember?.let { member -> + StableAlertDialog( + onDismissRequest = { notesForMember = null; notesList = emptyList(); notesDraft = ""; notesError = null }, + title = { Text(tr("members.notes", "Notizen") + ": " + member.fullName()) }, + text = { + Column(modifier = Modifier.fillMaxWidth().heightIn(max = 460.dp).verticalScroll(rememberScrollState())) { + notesError?.let { Text(it, color = MaterialTheme.colors.error, modifier = Modifier.padding(bottom = 6.dp)) } + if (notesLoading) { + CircularProgressIndicator(modifier = Modifier.padding(24.dp).align(Alignment.CenterHorizontally)) + } else if (notesList.isEmpty()) { + Text(trStr("memberNotes.noNotes", "Keine Notizen vorhanden."), style = MaterialTheme.typography.caption) + } else { + notesList.forEach { note -> + Row(modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), verticalAlignment = Alignment.Top) { + Text(note.content.orEmpty(), modifier = Modifier.weight(1f), style = MaterialTheme.typography.body2) + if (canWriteMembers) { + TextButton( + onClick = { + notesLoading = true + dependencies.applicationScope.launch { + try { + notesList = dependencies.membersManager.deleteMemberNote(clubId, note.id) + } catch (t: Throwable) { + notesError = t.message + } finally { + notesLoading = false + } + } + }, + ) { Text(tr("common.delete", "Löschen"), style = MaterialTheme.typography.caption) } + } + } + Divider() + } + } + if (canWriteMembers) { + Spacer(modifier = Modifier.height(8.dp)) + OutlinedTextField( + value = notesDraft, + onValueChange = { notesDraft = it }, + label = { Text(tr("mobile.newNote", "Neue Notiz")) }, + modifier = Modifier.fillMaxWidth(), + enabled = !notesLoading, + ) + TextButton( + onClick = { + val text = notesDraft.trim() + if (text.isEmpty()) return@TextButton + notesLoading = true + dependencies.applicationScope.launch { + try { + notesList = dependencies.membersManager.addMemberNote(clubId, member.id, text) + notesDraft = "" + } catch (t: Throwable) { + notesError = t.message + } finally { + notesLoading = false + } + } + }, + enabled = notesDraft.isNotBlank() && !notesLoading, + ) { Text(tr("memberNotes.add", "Hinzufügen")) } + } + } + }, + confirmButton = { TextButton(onClick = { notesForMember = null; notesList = emptyList(); notesDraft = "" }) { Text(tr("common.close", "Schließen")) } }, + ) + } + val membersBrowseScroll = rememberScrollState() Column( modifier = Modifier @@ -5891,7 +6127,7 @@ private fun MembersScreen( EmptyText(if (query.isBlank()) tr("mobile.noMembers", "Keine Mitglieder gefunden") else tr("mobile.noResults", "Keine Treffer")) } val tableScroll = rememberScrollState() - val tableWidth = if (showTrainingParticipationsColumn) 1030.dp else 930.dp + val tableWidth = if (showTrainingParticipationsColumn) 1180.dp else 1080.dp Row( modifier = Modifier .fillMaxWidth() @@ -5910,7 +6146,7 @@ private fun MembersScreen( if (showTrainingParticipationsColumn) { Text(tr("members.trainingParticipations", "Trainings-Teilnahmen"), modifier = Modifier.width(100.dp), fontWeight = FontWeight.Bold, style = MaterialTheme.typography.caption) } - Text(tr("members.actions", "Aktionen"), modifier = Modifier.width(120.dp), fontWeight = FontWeight.Bold, style = MaterialTheme.typography.caption) + Text(tr("members.actions", "Aktionen"), modifier = Modifier.width(270.dp), fontWeight = FontWeight.Bold, style = MaterialTheme.typography.caption) } Divider() Column(modifier = Modifier.fillMaxWidth()) { @@ -5959,9 +6195,48 @@ private fun MembersScreen( if (showTrainingParticipationsColumn) { Text((member.trainingParticipations ?: 0).toString(), modifier = Modifier.width(100.dp), style = MaterialTheme.typography.caption) } - Row(modifier = Modifier.width(120.dp), horizontalArrangement = Arrangement.spacedBy(4.dp)) { - TextButton(onClick = { stack = MembersStackRoute.Detail(member.id) }, modifier = Modifier.heightIn(min = 28.dp)) { - Text(tr("mobile.detail", "Details"), style = MaterialTheme.typography.caption) + Row(modifier = Modifier.width(270.dp), horizontalArrangement = Arrangement.spacedBy(2.dp), verticalAlignment = Alignment.CenterVertically) { + if (member.memberFormHandedOver != true) { + IconButton( + onClick = { quickMarkForm(member) }, + enabled = canWriteMembers, + modifier = Modifier.size(36.dp).semantics { contentDescription = trStr("members.markFormReceived", "Formular erhalten") }, + ) { Text("✓", color = Color(0xFF2E7D32), fontWeight = FontWeight.Bold) } + } + if (member.active && member.ttr == null && member.qttr == null && member.clickTtApplicationSubmitted != true) { + IconButton( + onClick = { requestClickTt(member) }, + enabled = canWriteMembers && !clickTtPendingMemberIds.contains(member.id), + modifier = Modifier.size(36.dp).semantics { contentDescription = trStr("members.clickTtRequestAction", "Click-TT-Antrag") }, + ) { Text(if (clickTtPendingMemberIds.contains(member.id)) "…" else "🏓", fontSize = 16.sp) } + } + IconButton( + onClick = { loadMemberOrders(member) }, + modifier = Modifier.size(36.dp).semantics { contentDescription = trStr("orders.title", "Bestellungen") }, + ) { Text("📦", fontSize = 16.sp) } + IconButton( + onClick = { stack = MembersStackRoute.Edit(member.id) }, + enabled = canWriteMembers, + modifier = Modifier.size(36.dp).semantics { contentDescription = trStr("members.editMember", "Bearbeiten") }, + ) { Text("✎", fontSize = 16.sp) } + IconButton( + onClick = { loadMemberNotes(member) }, + modifier = Modifier.size(36.dp).semantics { contentDescription = trStr("members.notes", "Notizen") }, + ) { Text("📝", fontSize = 16.sp) } + IconButton( + onClick = { loadMemberStats(member) }, + modifier = Modifier.size(36.dp).semantics { contentDescription = trStr("members.exercises", "Übungen") }, + ) { Text("🏃", fontSize = 16.sp) } + IconButton( + onClick = { stack = MembersStackRoute.Detail(member.id) }, + modifier = Modifier.size(36.dp).semantics { contentDescription = trStr("mobile.detail", "Details") }, + ) { Icon(Icons.Filled.Visibility, contentDescription = null, modifier = Modifier.size(18.dp)) } + if (member.active) { + IconButton( + onClick = { pendingDeactivateMember = member }, + enabled = canWriteMembers, + modifier = Modifier.size(36.dp).semantics { contentDescription = trStr("members.deactivateMember", "Mitglied deaktivieren") }, + ) { Text("⊖", color = MaterialTheme.colors.error, fontWeight = FontWeight.Bold) } } } } @@ -6967,7 +7242,7 @@ private fun DeleteAccountDialog( ) { var password by rememberSaveable { mutableStateOf("") } var error by rememberSaveable { mutableStateOf(null) } - AlertDialog( + StableAlertDialog( onDismissRequest = onDismiss, title = { Text("Eigenes Konto löschen") }, text = { @@ -7671,7 +7946,7 @@ private fun DiaryDeleteConfirmDialogs( onGroupError: (String?) -> Unit, ) { if (showDateConfirm) { - AlertDialog( + StableAlertDialog( onDismissRequest = onDismissDate, title = { Text(tr("diary.confirmDelete", "Trainingstag loeschen")) }, text = { Text(tr("diary.confirmDeleteDateDetails", "Der leere Trainingstag wird unwiderruflich geloescht.")) }, @@ -7691,7 +7966,7 @@ private fun DiaryDeleteConfirmDialogs( ) } groupCandidate?.let { group -> - AlertDialog( + StableAlertDialog( onDismissRequest = onDismissGroup, title = { Text(tr("diary.deleteGroup", "Trainingsgruppe loeschen")) }, text = { Text("${group.name ?: "Gruppe ${group.id}"} wirklich loeschen?") }, @@ -7735,7 +8010,7 @@ private fun DiaryQuickAddMemberDialog( var gender by rememberSaveable { mutableStateOf("unknown") } var busy by remember { mutableStateOf(false) } var error by remember { mutableStateOf(null) } - AlertDialog( + StableAlertDialog( onDismissRequest = { if (!busy) onDismiss() }, title = { Text(tr("diary.quickAdd", "Testmitglied schnell hinzufuegen")) }, text = { @@ -7889,7 +8164,7 @@ private fun DiaryPlanSupplementDialogs( editingNestedItem?.let { nested -> var duration by remember(nested.id) { mutableStateOf(nested.duration?.toString().orEmpty()) } var durationText by remember(nested.id) { mutableStateOf(nested.durationText.orEmpty()) } - AlertDialog( + StableAlertDialog( onDismissRequest = onDismissEditNested, title = { Text(tr("diary.editGroupActivity", "Gruppen-Aktivitaet bearbeiten")) }, text = { diff --git a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/BillingOrdersScreens.kt b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/BillingOrdersScreens.kt index 177f15ee..d882d447 100644 --- a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/BillingOrdersScreens.kt +++ b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/BillingOrdersScreens.kt @@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.AlertDialog import androidx.compose.material.Button import androidx.compose.material.Card import androidx.compose.material.CircularProgressIndicator @@ -967,7 +966,7 @@ private fun BillingClubScreen(dependencies: AppDependencies, onBack: () -> Unit) } deleteRunTarget?.let { target -> - AlertDialog( + StableAlertDialog( onDismissRequest = { deleteRunTarget = null }, title = { Text(tr("billing.deleteBilling", "Löschen")) }, text = { Text(tr("billing.deleteConfirm", "Abrechnungslauf wirklich löschen?")) }, @@ -986,7 +985,7 @@ private fun BillingClubScreen(dependencies: AppDependencies, onBack: () -> Unit) ) } deleteTemplateTarget?.let { target -> - AlertDialog( + StableAlertDialog( onDismissRequest = { deleteTemplateTarget = null }, title = { Text(tr("billing.deleteTemplate", "Vorlage löschen")) }, text = { Text(tr("billing.deleteTemplateConfirm", "Vorlage wirklich löschen?")) }, diff --git a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/ClubAdminScreens.kt b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/ClubAdminScreens.kt index 8df1d330..635beec2 100644 --- a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/ClubAdminScreens.kt +++ b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/ClubAdminScreens.kt @@ -15,7 +15,6 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.AlertDialog import androidx.compose.material.Button import androidx.compose.material.Card import androidx.compose.material.CircularProgressIndicator @@ -346,7 +345,7 @@ private fun ClubAdminPermissionsScreen(dependencies: AppDependencies, onBack: () val dialogMember = customizeFor if (dialogMember != null) { - AlertDialog( + StableAlertDialog( onDismissRequest = { if (!customizeSaving) customizeFor = null }, title = { Text(dialogMember.user?.email ?: "User ${dialogMember.userId}") }, text = { @@ -538,7 +537,7 @@ private fun ClubAdminLogsScreen(dependencies: AppDependencies, onBack: () -> Uni } if (detailId != null) { - AlertDialog( + StableAlertDialog( onDismissRequest = { detailId = null }, title = { Text(tr("mobile.logDetail", "Log-Detail")) }, text = { diff --git a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/ClubSettingsScreens.kt b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/ClubSettingsScreens.kt index 5c843b40..8e635b3f 100644 --- a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/ClubSettingsScreens.kt +++ b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/ClubSettingsScreens.kt @@ -14,7 +14,6 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.AlertDialog import androidx.compose.material.Button import androidx.compose.material.Card import androidx.compose.material.CircularProgressIndicator @@ -723,7 +722,7 @@ private fun ClubTrainingGroupsTabContent( } if (editing != null) { - AlertDialog( + StableAlertDialog( onDismissRequest = { editing = null }, title = { Text(tr("trainingGroupsTab.editGroup", "Gruppe bearbeiten")) }, text = { @@ -758,7 +757,7 @@ private fun ClubTrainingGroupsTabContent( if (deleteTarget != null) { val dg = deleteTarget!! - AlertDialog( + StableAlertDialog( onDismissRequest = { deleteTarget = null }, title = { Text(tr("trainingGroupsTab.delete", "Löschen")) }, text = { @@ -959,7 +958,7 @@ private fun ClubTrainingTimesTabContent( if (editing != null) { val et = editing!! - AlertDialog( + StableAlertDialog( onDismissRequest = { editing = null }, title = { Text(tr("trainingTimesTab.editTime", "Trainingszeit bearbeiten")) }, text = { @@ -1024,7 +1023,7 @@ private fun ClubTrainingTimesTabContent( if (deleteTimeId != null) { val tid = deleteTimeId!! - AlertDialog( + StableAlertDialog( onDismissRequest = { deleteTimeId = null }, title = { Text(tr("trainingTimesTab.delete", "Löschen")) }, text = { Text("Diese Trainingszeit wirklich löschen?") }, diff --git a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/ClubStammdatenScreens.kt b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/ClubStammdatenScreens.kt index 85698a90..61f82793 100644 --- a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/ClubStammdatenScreens.kt +++ b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/ClubStammdatenScreens.kt @@ -15,7 +15,6 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.AlertDialog import androidx.compose.material.Button import androidx.compose.material.Card import androidx.compose.material.CircularProgressIndicator @@ -208,7 +207,7 @@ private fun PredefinedActivityEditorDialog( var err by remember { mutableStateOf(null) } var busy by remember { mutableStateOf(false) } - AlertDialog( + StableAlertDialog( onDismissRequest = onDismiss, title = { Text(if (isNew) resolve("mobile.newPredefined", "Neue Aktivität") else resolve("mobile.editPredefined", "Bearbeiten")) }, text = { diff --git a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/InternalTournamentEditorDetailTabs.kt b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/InternalTournamentEditorDetailTabs.kt index 8f7842e3..7a20f203 100644 --- a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/InternalTournamentEditorDetailTabs.kt +++ b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/InternalTournamentEditorDetailTabs.kt @@ -16,7 +16,6 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll -import androidx.compose.material.AlertDialog import androidx.compose.material.Button import androidx.compose.material.Icon import androidx.compose.material.IconButton @@ -106,7 +105,7 @@ internal fun TournamentEditorClassesTab( var distributedMatches by remember { mutableStateOf>(emptyList()) } var distributedMessage by remember { mutableStateOf(null) } if (showAdd) { - AlertDialog( + StableAlertDialog( onDismissRequest = { showAdd = false }, title = { Text(tr("tournaments.addClass", "Klasse anlegen")) }, text = { @@ -185,7 +184,7 @@ internal fun TournamentEditorClassesTab( } if (showDistributedDialog) { - AlertDialog( + StableAlertDialog( onDismissRequest = { showDistributedDialog = false }, title = { Text(tr("tournaments.distributeTablesResult", "Tischverteilung")) }, text = { @@ -512,7 +511,7 @@ internal fun TournamentEditorParticipantsTab( if (showExternal) { OutlinedButton(onClick = { extDialog = true }) { Text(tr("tournaments.addExternalParticipant", "Extern hinzufügen")) } if (extDialog) { - AlertDialog( + StableAlertDialog( onDismissRequest = { extDialog = false }, title = { Text(tr("tournaments.addExternalParticipant", "Extern hinzufügen")) }, text = { @@ -940,7 +939,7 @@ internal fun TournamentEditorMatchesTab( // Confirmation dialog for deleting a set if (confirmDelete != null) { val (matchIdToDelete, setToDelete) = confirmDelete!! - AlertDialog( + StableAlertDialog( onDismissRequest = { confirmDelete = null }, title = { Text(tr("tournaments.confirmDeleteSetTitle", "Satz löschen")) }, text = { Text(tr("tournaments.confirmDeleteSet", "Soll dieser Satz wirklich gelöscht werden?")) }, @@ -970,7 +969,7 @@ internal fun TournamentEditorMatchesTab( // Edit set dialog if (editingSet != null) { val (matchIdToEdit, setToEdit) = editingSet!! - AlertDialog( + StableAlertDialog( onDismissRequest = { editingSet = null }, title = { Text(tr("tournaments.editSetTitle", "Satz bearbeiten")) }, text = { @@ -1035,7 +1034,7 @@ internal fun TournamentEditorMatchesTab( ) } if (showDistributedDialog) { - AlertDialog( + StableAlertDialog( onDismissRequest = { showDistributedDialog = false }, title = { Text(tr("tournaments.distributeTablesResult", "Tischverteilung")) }, text = { diff --git a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/InternalTournamentEditorScreen.kt b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/InternalTournamentEditorScreen.kt index ca27e19c..24ae14ba 100644 --- a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/InternalTournamentEditorScreen.kt +++ b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/InternalTournamentEditorScreen.kt @@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.AlertDialog import androidx.compose.material.Button import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Divider diff --git a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/InternalTournamentStatsDialog.kt b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/InternalTournamentStatsDialog.kt index 2b9e32b9..ea283ff3 100644 --- a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/InternalTournamentStatsDialog.kt +++ b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/InternalTournamentStatsDialog.kt @@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.AlertDialog import androidx.compose.material.Checkbox import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Divider @@ -148,7 +147,7 @@ fun InternalTournamentStatsDialog( val scroll = rememberScrollState() val s = stats - AlertDialog( + StableAlertDialog( onDismissRequest = onDismiss, title = { Text(tr("tournaments.internalStatsTitle", "Interne Turnier-Statistik (Einzel)")) }, text = { diff --git a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/MemberGroupPortraitCropRoute.kt b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/MemberGroupPortraitCropRoute.kt index 8e62ffc2..4b08fc34 100644 --- a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/MemberGroupPortraitCropRoute.kt +++ b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/MemberGroupPortraitCropRoute.kt @@ -27,7 +27,6 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.AlertDialog import androidx.compose.material.Button import androidx.compose.material.Checkbox import androidx.compose.material.CircularProgressIndicator @@ -491,7 +490,7 @@ fun MemberGroupPortraitCropRoute( } if (memberPickerOpen) { - AlertDialog( + StableAlertDialog( onDismissRequest = { memberPickerOpen = false }, title = { Text(s("members.groupCropPickMember", "Mitglied wählen")) }, text = { diff --git a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/OfficialTournamentsWorkspaceScreen.kt b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/OfficialTournamentsWorkspaceScreen.kt index 94f138fd..a9ffe577 100644 --- a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/OfficialTournamentsWorkspaceScreen.kt +++ b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/OfficialTournamentsWorkspaceScreen.kt @@ -24,7 +24,6 @@ import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material.AlertDialog import androidx.compose.material.Button import androidx.compose.material.Checkbox import androidx.compose.material.CircularProgressIndicator @@ -664,7 +663,7 @@ internal fun OfficialTournamentsWorkspaceScreen( } if (showInfoDialog && infoMessage != null) { - AlertDialog( + StableAlertDialog( onDismissRequest = { showInfoDialog = false }, title = { Text(infoTitle) }, text = { Text(infoMessage!!) }, @@ -681,7 +680,7 @@ internal fun OfficialTournamentsWorkspaceScreen( "reset" -> participantsRows(pBatch, mapSnapshot(), "participated") { memberNameById(it) } else -> emptyList() } - AlertDialog( + StableAlertDialog( onDismissRequest = { batchAction = null }, title = { Text(tr("officialTournaments.batchTitle", "Sammelaktion")) }, text = { Text(tr("officialTournaments.batchConfirm", "{n} Einträge wirklich ausführen?").replace("{n}", rows.size.toString())) }, @@ -736,7 +735,7 @@ internal fun OfficialTournamentsWorkspaceScreen( } deleteTarget?.let { target -> - AlertDialog( + StableAlertDialog( onDismissRequest = { deleteTarget = null }, title = { Text(tr("officialTournaments.deleteTournamentTitle", "Turnier löschen")) }, text = { Text("${target.title ?: ""} (ID ${target.id})") }, diff --git a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/PersonalHubScreens.kt b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/PersonalHubScreens.kt index 6d29eea6..4d49510b 100644 --- a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/PersonalHubScreens.kt +++ b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/PersonalHubScreens.kt @@ -19,7 +19,6 @@ import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.AlertDialog import androidx.compose.material.Button import androidx.compose.material.Card import androidx.compose.material.CircularProgressIndicator @@ -300,7 +299,7 @@ private fun MyTischtennisAccountScreen(dependencies: AppDependencies, onBack: () } if (confirmUnlink) { - AlertDialog( + StableAlertDialog( onDismissRequest = { confirmUnlink = false }, title = { Text(tr("myTischtennisAccount.unlinkAccountTitle", "Account trennen")) }, text = { Text(tr("myTischtennisAccount.unlinkAccountConfirm", "Wirklich trennen?")) }, @@ -359,7 +358,7 @@ private fun MyTischtennisEditorDialog( var err by remember { mutableStateOf(null) } var busy by remember { mutableStateOf(false) } - AlertDialog( + StableAlertDialog( onDismissRequest = onDismiss, title = { Text( @@ -578,7 +577,7 @@ private fun ClickTtAccountScreen(dependencies: AppDependencies, onBack: () -> Un } if (confirmDelete) { - AlertDialog( + StableAlertDialog( onDismissRequest = { confirmDelete = false }, title = { Text(ct("mobile.clickTt.deleteTitle", "Account löschen")) }, text = { Text(ct("mobile.clickTt.deleteConfirm", "Wirklich löschen?")) }, @@ -623,7 +622,7 @@ private fun ClickTtEditorDialog( var err by remember { mutableStateOf(null) } var busy by remember { mutableStateOf(false) } - AlertDialog( + StableAlertDialog( onDismissRequest = onDismiss, title = { Text( diff --git a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/ScheduleScreen.kt b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/ScheduleScreen.kt index 6a1efeb3..e5ce89fe 100644 --- a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/ScheduleScreen.kt +++ b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/ScheduleScreen.kt @@ -21,7 +21,6 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.AlertDialog import androidx.compose.material.Card import androidx.compose.material.Checkbox import androidx.compose.material.CircularProgressIndicator @@ -43,9 +42,11 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog import de.tsschulz.tt_tagebuch.app.AppDependencies import de.tsschulz.tt_tagebuch.app.stats.TrainingStatsDerived import de.tsschulz.tt_tagebuch.shared.api.models.ScheduleMatchDto @@ -348,7 +349,7 @@ internal fun ScheduleScreen(dependencies: AppDependencies, friendlyOnly: Boolean } detailMatch?.let { m -> - AlertDialog( + StableAlertDialog( onDismissRequest = { detailMatch = null }, title = { Text("${m.homeTeam?.name ?: "?"} : ${m.guestTeam?.name ?: "?"}") }, text = { @@ -517,23 +518,42 @@ internal fun ScheduleScreen(dependencies: AppDependencies, friendlyOnly: Boolean LaunchedEffect(m.id, clubId) { loadPlayerDialogMembersFor(m) } - AlertDialog( - onDismissRequest = { if (!playerSaving) playerMatch = null }, - title = { Text(tr("schedule.playerSelectionTitle", "Spieler")) }, - text = { - Column(modifier = Modifier.heightIn(max = 400.dp)) { - playerError?.let { Text(it, color = MaterialTheme.colors.error) } + val dialogHeight = (LocalConfiguration.current.screenHeightDp * 0.82f).dp + Dialog(onDismissRequest = { if (!playerSaving) playerMatch = null }) { + Card( + modifier = Modifier + .fillMaxWidth() + .widthIn(max = 560.dp) + .height(dialogHeight), + elevation = 8.dp, + ) { + Column(modifier = Modifier.fillMaxSize().padding(16.dp)) { + Text( + tr("schedule.playerSelectionTitle", "Spieler"), + style = MaterialTheme.typography.h6, + fontWeight = FontWeight.SemiBold, + ) + Spacer(modifier = Modifier.height(8.dp)) + playerError?.let { + Text(it, color = MaterialTheme.colors.error, modifier = Modifier.padding(bottom = 8.dp)) + } val memberList = playerDialogMembers if (membersState.isLoading && memberList.isEmpty()) { - CircularProgressIndicator(modifier = Modifier.padding(16.dp)) + Box( + modifier = Modifier.fillMaxWidth().weight(1f), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator() + } } else { - val scroll = rememberScrollState() - Column(Modifier.verticalScroll(scroll)) { - memberList.forEach { mem -> + LazyColumn( + modifier = Modifier.fillMaxWidth().weight(1f), + ) { + items(memberList, key = { it.id }) { mem -> val id = mem.id Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp), ) { Column(Modifier.weight(1f)) { Text("${mem.firstName} ${mem.lastName}".trim(), maxLines = 1) @@ -564,39 +584,42 @@ internal fun ScheduleScreen(dependencies: AppDependencies, friendlyOnly: Boolean } } } - } - }, - confirmButton = { - TextButton( - enabled = !playerSaving && !isFriendlyMatchLocked(m), - onClick = { - scope.launch { - playerSaving = true - playerError = null - runCatching { - val visibleIds = playerDialogMembers.map { it.id }.toSet() - fun mergeVisible(existing: List, selected: List): List = - (existing.filter { it !in visibleIds } + selected.filter { it in visibleIds }).distinct() - dependencies.scheduleManager.updateMatchPlayersForMatch( - clubId = clubId, - match = m, - 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") } - playerSaving = false + Spacer(modifier = Modifier.height(12.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + ) { + TextButton(enabled = !playerSaving, onClick = { playerMatch = null }) { + Text(tr("common.cancel", "Abbrechen")) } - }, - ) { Text(tr("common.save", "Speichern")) } - }, - dismissButton = { - TextButton(enabled = !playerSaving, onClick = { playerMatch = null }) { - Text(tr("common.cancel", "Abbrechen")) + TextButton( + enabled = !playerSaving && !isFriendlyMatchLocked(m), + onClick = { + scope.launch { + playerSaving = true + playerError = null + runCatching { + val visibleIds = playerDialogMembers.map { it.id }.toSet() + fun mergeVisible(existing: List, selected: List): List = + (existing.filter { it !in visibleIds } + selected.filter { it in visibleIds }).distinct() + dependencies.scheduleManager.updateMatchPlayersForMatch( + clubId = clubId, + match = m, + 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") } + playerSaving = false + } + }, + ) { Text(if (playerSaving) "…" else tr("common.save", "Speichern")) } + } } - }, - ) + } + } } } @@ -639,7 +662,7 @@ private fun FriendlyMatchEditDialog( return normalized } - AlertDialog( + StableAlertDialog( onDismissRequest = onDismiss, title = { Text(if (match == null) "Freundschaftsspiel anlegen" else "Freundschaftsspiel bearbeiten") }, text = { @@ -812,7 +835,7 @@ private fun FriendlyResultDialog( resultDetails = rows, ) - AlertDialog( + StableAlertDialog( onDismissRequest = onDismiss, title = { Text("Ergebniseingabe") }, text = { diff --git a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/StableDialogs.kt b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/StableDialogs.kt new file mode 100644 index 00000000..2b5c1e46 --- /dev/null +++ b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/StableDialogs.kt @@ -0,0 +1,109 @@ +package de.tsschulz.tt_tagebuch.app.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material.Card +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties + +@Composable +fun StableAlertDialog( + onDismissRequest: () -> Unit, + title: @Composable (() -> Unit)? = null, + text: @Composable (() -> Unit)? = null, + confirmButton: @Composable () -> Unit, + modifier: Modifier = Modifier, + dismissButton: @Composable (() -> Unit)? = null, + properties: DialogProperties = DialogProperties(), +) { + StableAlertDialogFrame( + onDismissRequest = onDismissRequest, + title = title, + text = text, + modifier = modifier, + properties = properties, + buttons = { + dismissButton?.invoke() + confirmButton() + }, + ) +} + +@Composable +fun StableAlertDialog( + onDismissRequest: () -> Unit, + buttons: @Composable () -> Unit, + modifier: Modifier = Modifier, + title: @Composable (() -> Unit)? = null, + text: @Composable (() -> Unit)? = null, + properties: DialogProperties = DialogProperties(), +) { + StableAlertDialogFrame( + onDismissRequest = onDismissRequest, + title = title, + text = text, + modifier = modifier, + properties = properties, + buttons = buttons, + ) +} + +@Composable +private fun StableAlertDialogFrame( + onDismissRequest: () -> Unit, + title: @Composable (() -> Unit)?, + text: @Composable (() -> Unit)?, + modifier: Modifier, + properties: DialogProperties, + buttons: @Composable () -> Unit, +) { + val maxHeight = (LocalConfiguration.current.screenHeightDp * 0.86f).dp + Dialog(onDismissRequest = onDismissRequest, properties = properties) { + Card( + modifier = modifier + .fillMaxWidth() + .widthIn(max = 560.dp) + .heightIn(max = maxHeight), + elevation = 8.dp, + ) { + Column(modifier = Modifier.padding(16.dp)) { + if (title != null) { + androidx.compose.material.ProvideTextStyle( + MaterialTheme.typography.h6.copy(fontWeight = FontWeight.SemiBold), + ) { + title() + } + } + if (text != null) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = if (title != null) 12.dp else 0.dp) + .heightIn(max = maxHeight - 112.dp), + ) { + text() + } + } + Row( + modifier = Modifier.fillMaxWidth().padding(top = 12.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + ) { + buttons() + } + } + } + } +} diff --git a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/TeamEditorScreen.kt b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/TeamEditorScreen.kt index dc0d22ca..61c2b0eb 100644 --- a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/TeamEditorScreen.kt +++ b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/TeamEditorScreen.kt @@ -17,7 +17,6 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.AlertDialog import androidx.compose.material.Button import androidx.compose.material.Card import androidx.compose.material.CircularProgressIndicator @@ -697,7 +696,7 @@ internal fun TeamEditorScreen( } info?.let { msg -> - AlertDialog( + StableAlertDialog( onDismissRequest = { info = null }, title = { Text(t("common.info", "Hinweis")) }, text = { Text(msg) }, diff --git a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/TeamManagementScreen.kt b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/TeamManagementScreen.kt index fc15c39e..fdbddc47 100644 --- a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/TeamManagementScreen.kt +++ b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/TeamManagementScreen.kt @@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material.AlertDialog import androidx.compose.material.Button import androidx.compose.material.Card import androidx.compose.material.CircularProgressIndicator @@ -362,7 +361,7 @@ internal fun TeamManagementScreen( } deleteTarget?.let { target -> - AlertDialog( + StableAlertDialog( onDismissRequest = { deleteTarget = null }, title = { Text(getMobileString("mobile.teamDeleteTitle", "Mannschaft löschen?")) }, text = { Text(target.name.ifBlank { "#${target.id}" }) }, @@ -395,7 +394,7 @@ internal fun TeamManagementScreen( } infoMessage?.let { msg -> - AlertDialog( + StableAlertDialog( onDismissRequest = { infoMessage = null }, title = { Text(getMobileString("common.error", "Fehler")) }, text = { Text(msg) }, diff --git a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/TeamPlanningScreen.kt b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/TeamPlanningScreen.kt index 42613c37..60a06a20 100644 --- a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/TeamPlanningScreen.kt +++ b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/TeamPlanningScreen.kt @@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material.AlertDialog import androidx.compose.material.Button import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Divider @@ -475,7 +474,7 @@ internal fun TeamPlanningScreen( } deleteTeamId?.let { tid -> - AlertDialog( + StableAlertDialog( onDismissRequest = { deleteTeamId = null }, title = { Text(t("mobile.teamDeleteTitle", "Mannschaft löschen?")) }, text = { Text(teams.find { it.id == tid }?.name.orEmpty()) }, @@ -497,7 +496,7 @@ internal fun TeamPlanningScreen( } info?.let { msg -> - AlertDialog( + StableAlertDialog( onDismissRequest = { info = null }, title = { Text(t("common.error", "Fehler")) }, text = { Text(msg) }, diff --git a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/TrainingStatsScreen.kt b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/TrainingStatsScreen.kt index 48e2d177..95499d4b 100644 --- a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/TrainingStatsScreen.kt +++ b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/TrainingStatsScreen.kt @@ -16,7 +16,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.AlertDialog import androidx.compose.material.Card import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Divider @@ -668,7 +667,7 @@ internal fun TrainingStatsScreen(dependencies: AppDependencies) { } detailMember?.let { member -> - AlertDialog( + StableAlertDialog( onDismissRequest = { detailMember = null }, title = { Text( diff --git a/mobile-app/gradle/libs.versions.toml b/mobile-app/gradle/libs.versions.toml index 79e463e9..547d72e1 100644 --- a/mobile-app/gradle/libs.versions.toml +++ b/mobile-app/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] # composeApp (Play Store / „Über die App“-Build) -appVersionCode = "25" -appVersionName = "1.7.5" +appVersionCode = "26" +appVersionName = "1.7.6" agp = "9.2.1" android-compileSdk = "35" android-minSdk = "24" diff --git a/mobile-app/shared/src/commonMain/kotlin/de/tsschulz/tt_tagebuch/shared/api/MembersApi.kt b/mobile-app/shared/src/commonMain/kotlin/de/tsschulz/tt_tagebuch/shared/api/MembersApi.kt index 3b5e59b4..9cd6f02b 100644 --- a/mobile-app/shared/src/commonMain/kotlin/de/tsschulz/tt_tagebuch/shared/api/MembersApi.kt +++ b/mobile-app/shared/src/commonMain/kotlin/de/tsschulz/tt_tagebuch/shared/api/MembersApi.kt @@ -9,6 +9,7 @@ import de.tsschulz.tt_tagebuch.shared.api.models.MemberSetBody import de.tsschulz.tt_tagebuch.shared.api.models.MemberTransferRunBody import io.ktor.client.call.body import io.ktor.client.request.forms.formData +import io.ktor.client.request.delete import io.ktor.client.request.get import io.ktor.client.request.parameter import io.ktor.client.request.post @@ -18,8 +19,16 @@ import io.ktor.http.Headers import io.ktor.http.HttpHeaders import io.ktor.http.contentType import io.ktor.client.request.forms.MultiPartFormDataContent +import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonObject +@Serializable +data class MemberNoteMutationBody( + val clubId: Int, + val memberId: Int? = null, + val content: String? = null, +) + class MembersApi( private val client: AuthedHttpClient, ) { @@ -58,6 +67,32 @@ class MembersApi( return client.http.post("/api/clubmembers/quick-update-member-form/$clubId/$memberId").body() } + suspend fun quickDeactivateMember(clubId: Int, memberId: Int): MemberQuickMutationResponse { + return client.http.post("/api/clubmembers/quick-deactivate/$clubId/$memberId").body() + } + + suspend fun requestClickTtRegistration(clubId: Int, memberId: Int): MemberQuickMutationResponse { + return client.http.post("/api/clubmembers/clicktt-registration/$clubId/$memberId").body() + } + + suspend fun listMemberNotes(clubId: Int, memberId: Int): List { + return client.http.get("/api/member-notes/$memberId") { + parameter("clubId", clubId) + }.body() + } + + suspend fun addMemberNote(clubId: Int, memberId: Int, content: String): List { + return client.http.post("/api/member-notes") { + setBody(MemberNoteMutationBody(clubId = clubId, memberId = memberId, content = content)) + }.body() + } + + suspend fun deleteMemberNote(clubId: Int, noteId: Int): List { + return client.http.delete("/api/member-notes/$noteId") { + setBody(MemberNoteMutationBody(clubId = clubId)) + }.body() + } + suspend fun transferMembers(clubId: Int, body: MemberTransferRunBody): JsonObject { return client.http.post("/api/clubmembers/transfer/$clubId") { setBody(body) diff --git a/mobile-app/shared/src/commonMain/kotlin/de/tsschulz/tt_tagebuch/shared/api/models/Member.kt b/mobile-app/shared/src/commonMain/kotlin/de/tsschulz/tt_tagebuch/shared/api/models/Member.kt index f8a6e23a..985c325f 100644 --- a/mobile-app/shared/src/commonMain/kotlin/de/tsschulz/tt_tagebuch/shared/api/models/Member.kt +++ b/mobile-app/shared/src/commonMain/kotlin/de/tsschulz/tt_tagebuch/shared/api/models/Member.kt @@ -21,6 +21,7 @@ data class Member( val testMembership: Boolean? = null, val picsInInternetAllowed: Boolean? = null, val memberFormHandedOver: Boolean? = null, + val clickTtApplicationSubmitted: Boolean? = null, val adultReleaseApproved: Boolean? = null, val adultReserveApproved: Boolean? = null, val lastTraining: String? = null, diff --git a/mobile-app/shared/src/commonMain/kotlin/de/tsschulz/tt_tagebuch/shared/state/MembersManager.kt b/mobile-app/shared/src/commonMain/kotlin/de/tsschulz/tt_tagebuch/shared/state/MembersManager.kt index 7aa25f3d..d3147da8 100644 --- a/mobile-app/shared/src/commonMain/kotlin/de/tsschulz/tt_tagebuch/shared/state/MembersManager.kt +++ b/mobile-app/shared/src/commonMain/kotlin/de/tsschulz/tt_tagebuch/shared/state/MembersManager.kt @@ -77,6 +77,21 @@ class MembersManager( suspend fun quickUpdateMemberFormHandedOver(clubId: Int, memberId: Int): MemberQuickMutationResponse = membersApi.quickUpdateMemberFormHandedOver(clubId, memberId) + suspend fun quickDeactivateMember(clubId: Int, memberId: Int): MemberQuickMutationResponse = + membersApi.quickDeactivateMember(clubId, memberId) + + suspend fun requestClickTtRegistration(clubId: Int, memberId: Int): MemberQuickMutationResponse = + membersApi.requestClickTtRegistration(clubId, memberId) + + suspend fun listMemberNotes(clubId: Int, memberId: Int): List = + membersApi.listMemberNotes(clubId, memberId) + + suspend fun addMemberNote(clubId: Int, memberId: Int, content: String): List = + membersApi.addMemberNote(clubId, memberId, content) + + suspend fun deleteMemberNote(clubId: Int, noteId: Int): List = + membersApi.deleteMemberNote(clubId, noteId) + suspend fun transferMembers(clubId: Int, body: MemberTransferRunBody): JsonObject = membersApi.transferMembers(clubId, body)