diff --git a/mobile-app/composeApp/release/composeApp-release.aab b/mobile-app/composeApp/release/composeApp-release.aab index 012ed02d..c6df54ad 100644 Binary files a/mobile-app/composeApp/release/composeApp-release.aab and b/mobile-app/composeApp/release/composeApp-release.aab differ 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 d8ea0f99..de595a2d 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 @@ -1832,7 +1832,7 @@ private fun DiaryListScreen( selectedNewDateGroupId = null newDateGroupMenuExpanded = false val tmpl = diaryState.dates.firstOrNull() - newDiaryDateStr = kotlin.runCatching { java.time.LocalDate.now().toString() }.getOrElse { "" } + newDiaryDateStr = todayIsoDate() newDiaryStart = diaryTimeForFormField(tmpl?.trainingStart).ifBlank { "17:30" } newDiaryEnd = diaryTimeForFormField(tmpl?.trainingEnd).ifBlank { "19:30" } newDiaryExcludeFromBilling = false @@ -1994,17 +1994,16 @@ private fun DiaryListScreen( ) { Text(tr("diary.applySuggestion", "Vorschlag übernehmen")) } } Divider(modifier = Modifier.padding(vertical = 12.dp)) - OutlinedTextField( + LocalizedDatePickerField( value = newDiaryDateStr, onValueChange = { newDiaryDateStr = it }, - label = { Text(tr("diary.date", "Datum (YYYY-MM-DD)")) }, - singleLine = true, + label = tr("diary.date", "Datum"), enabled = !diaryState.isLoading, modifier = Modifier.fillMaxWidth(), ) TextButton( onClick = { - newDiaryDateStr = kotlin.runCatching { java.time.LocalDate.now().toString() }.getOrElse { newDiaryDateStr } + newDiaryDateStr = todayIsoDate() }, enabled = !diaryState.isLoading, ) { Text(tr("diary.today", "Heute")) } @@ -6465,11 +6464,11 @@ private fun MemberDetailRoute( DetailLine(tr("members.street", "Straße"), member.street ?: "-") DetailLine(tr("members.city", "Ort"), member.city ?: "-") DetailLine(tr("members.postalCode", "PLZ"), member.postalCode ?: "-") - DetailLine(tr("members.birthDate", "Geburtsdatum"), member.birthDate ?: "-") + DetailLine(tr("members.birthDate", "Geburtsdatum"), formatIsoDateForDisplay(member.birthDate)) DetailLine(tr("members.gender", "Geschlecht"), member.gender ?: "-") DetailLine("TTR", member.ttr?.toString() ?: "-") DetailLine("QTTR", member.qttr?.toString() ?: "-") - DetailLine(tr("mobile.lastTraining", "Letztes Training"), member.lastTraining?.take(10) ?: "-") + DetailLine(tr("mobile.lastTraining", "Letztes Training"), formatIsoDateForDisplay(member.lastTraining)) DetailLine(tr("members.trainingParticipations", "Trainings-Teilnahmen"), member.trainingParticipations?.toString() ?: "-") SectionTitle(tr("members.contact", "Kontakt")) @@ -6735,7 +6734,7 @@ private fun MemberEditRoute( OutlinedTextField(street, { street = it }, label = { Text(tr("members.street", "Straße")) }, modifier = Modifier.fillMaxWidth()) OutlinedTextField(postalCode, { postalCode = it }, label = { Text(tr("members.postalCode", "PLZ")) }, modifier = Modifier.fillMaxWidth()) OutlinedTextField(city, { city = it }, label = { Text(tr("members.city", "Ort")) }, modifier = Modifier.fillMaxWidth()) - OutlinedTextField(birthDate, { birthDate = it }, label = { Text(tr("members.birthDate", "Geburtsdatum")) }, modifier = Modifier.fillMaxWidth(), singleLine = true) + LocalizedDatePickerField(birthDate, { birthDate = it }, label = tr("members.birthDate", "Geburtsdatum"), modifier = Modifier.fillMaxWidth()) Box { OutlinedButton(onClick = { genderMenu = true }, modifier = Modifier.fillMaxWidth().padding(top = 8.dp)) { Text("${tr("members.gender", "Geschlecht")}: $gender") @@ -7308,7 +7307,7 @@ private fun DiaryEditForm( Card(modifier = Modifier.fillMaxWidth().padding(vertical = 12.dp), elevation = 2.dp) { Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { - OutlinedTextField(value = date, onValueChange = { date = it }, label = { Text("Datum (YYYY-MM-DD)") }, singleLine = true) + LocalizedDatePickerField(value = date, onValueChange = { date = it }, label = "Datum") Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { OutlinedTextField(value = start, onValueChange = { start = it }, label = { Text("Start") }, modifier = Modifier.weight(1f), singleLine = true) OutlinedTextField(value = end, onValueChange = { end = it }, label = { Text("Ende") }, modifier = Modifier.weight(1f), singleLine = true) @@ -7335,7 +7334,7 @@ private fun DiaryEditForm( ErrorText(error) Button(onClick = { if (!date.matches(Regex("\\d{4}-\\d{2}-\\d{2}"))) { - error = "Datum im Format YYYY-MM-DD eingeben" + error = "Bitte Datum auswählen" return@Button } if (start.isNotBlank() && end.isNotBlank() && start >= end) { @@ -8070,7 +8069,7 @@ private fun DiaryQuickAddMemberDialog( error?.let { Text(it, color = MaterialTheme.colors.error) } OutlinedTextField(firstName, { firstName = it }, label = { Text(tr("members.firstName", "Vorname")) }, singleLine = true) OutlinedTextField(lastName, { lastName = it }, label = { Text(tr("members.lastName", "Nachname")) }, singleLine = true) - OutlinedTextField(birthDate, { birthDate = it }, label = { Text(tr("members.birthDate", "Geburtsdatum")) }, singleLine = true) + LocalizedDatePickerField(birthDate, { birthDate = it }, label = tr("members.birthDate", "Geburtsdatum")) DrawingChoiceRowForDiaryGender(gender) { gender = it } } }, @@ -8694,7 +8693,7 @@ private fun buildMembersCsvExport(rows: List): String { private fun Member.fullName(): String = listOf(firstName, lastName).filter { it.isNotBlank() }.joinToString(" ").ifBlank { "Mitglied $id" } -private fun formatDate(value: String): String = value.take(10) +private fun formatDate(value: String): String = formatIsoDateForDisplay(value, fallback = value.take(10)) private fun formatTimeRange(start: String?, end: String?): String { val parts = listOfNotNull(start?.takeIf { it.isNotBlank() }, end?.takeIf { it.isNotBlank() }) 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 d882d447..5da862e9 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 @@ -845,13 +845,12 @@ private fun BillingClubScreen(dependencies: AppDependencies, onBack: () -> Unit) Switch(checked = omitLocation, onCheckedChange = { omitLocation = it }, enabled = canWrite) Text(tr("billing.omitField", "Feld auslassen"), modifier = Modifier.padding(start = 8.dp)) } - OutlinedTextField( + LocalizedDatePickerField( value = documentDate, onValueChange = { documentDate = it }, - label = { Text(tr("billing.documentDate", "Datum Dokument")) }, + label = tr("billing.documentDate", "Datum Dokument"), modifier = Modifier.fillMaxWidth().padding(top = 4.dp), enabled = canWrite && !omitDocDate, - singleLine = true, ) Row(verticalAlignment = Alignment.CenterVertically) { Switch(checked = omitDocDate, onCheckedChange = { omitDocDate = it }, enabled = canWrite) diff --git a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/CalendarScreen.kt b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/CalendarScreen.kt index fe9435c8..ba6fe004 100644 --- a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/CalendarScreen.kt +++ b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/CalendarScreen.kt @@ -368,19 +368,18 @@ fun CalendarScreen( style = MaterialTheme.typography.caption, color = MaterialTheme.colors.onSurface.copy(alpha = 0.7f), ) - OutlinedTextField( + LocalizedDatePickerField( value = cancelStart, onValueChange = { cancelStart = it }, - label = { Text(tr("mobile.calendarCancellationStart", "Datum (YYYY-MM-DD)")) }, + label = tr("mobile.calendarCancellationStart", "Datum"), modifier = Modifier.fillMaxWidth().padding(top = 8.dp), - singleLine = true, ) - OutlinedTextField( + LocalizedDatePickerField( value = cancelEnd, onValueChange = { cancelEnd = it }, - label = { Text(tr("mobile.calendarCancellationEnd", "Bis optional")) }, + label = tr("mobile.calendarCancellationEnd", "Bis"), modifier = Modifier.fillMaxWidth().padding(top = 4.dp), - singleLine = true, + placeholder = tr("mobile.calendarCancellationEnd", "Bis optional"), ) OutlinedTextField( value = cancelReason, @@ -428,7 +427,7 @@ fun CalendarScreen( verticalAlignment = Alignment.CenterVertically, ) { Column(Modifier.weight(1f)) { - Text("${c.date}–${c.endDate}", style = MaterialTheme.typography.caption) + Text("${formatIsoDateForDisplay(c.dateIso)}–${formatIsoDateForDisplay(c.endDateIso)}", style = MaterialTheme.typography.caption) Text(c.title, style = MaterialTheme.typography.body2, maxLines = 1, overflow = TextOverflow.Ellipsis) } TextButton( diff --git a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/DateFields.kt b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/DateFields.kt new file mode 100644 index 00000000..cdb13449 --- /dev/null +++ b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/DateFields.kt @@ -0,0 +1,67 @@ +package de.tsschulz.tt_tagebuch.app.ui + +import android.app.DatePickerDialog +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.material.OutlinedButton +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import java.time.format.DateTimeParseException +import java.util.Locale + +private val UiIsoDateFormatter: DateTimeFormatter = DateTimeFormatter.ISO_LOCAL_DATE +private val UiGermanDateFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy", Locale.GERMANY) + +internal fun parseIsoDateForUi(value: String?): LocalDate? { + val raw = value?.trim()?.take(10)?.takeIf { it.isNotBlank() } ?: return null + return try { + LocalDate.parse(raw, UiIsoDateFormatter) + } catch (_: DateTimeParseException) { + null + } +} + +internal fun formatIsoDateForDisplay(value: String?, fallback: String = "-"): String { + if (value.isNullOrBlank()) return fallback + return parseIsoDateForUi(value)?.format(UiGermanDateFormatter) ?: value.trim().takeIf { it.isNotBlank() } ?: fallback +} + +internal fun todayIsoDate(): String = LocalDate.now().format(UiIsoDateFormatter) + +@Composable +internal fun LocalizedDatePickerField( + value: String, + onValueChange: (String) -> Unit, + label: String, + modifier: Modifier = Modifier, + enabled: Boolean = true, + placeholder: String = "Datum wählen", +) { + val context = LocalContext.current + val selected = remember(value) { parseIsoDateForUi(value) } + val initial = selected ?: LocalDate.now() + OutlinedButton( + onClick = { + DatePickerDialog( + context, + { _, year, month, dayOfMonth -> + onValueChange(LocalDate.of(year, month + 1, dayOfMonth).format(UiIsoDateFormatter)) + }, + initial.year, + initial.monthValue - 1, + initial.dayOfMonth, + ).show() + }, + enabled = enabled, + modifier = modifier.fillMaxWidth().heightIn(min = 48.dp), + ) { + val display = selected?.format(UiGermanDateFormatter) ?: value.ifBlank { placeholder } + Text("$label: $display", maxLines = 2) + } +} 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 24ae14ba..5590c6aa 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 @@ -367,12 +367,14 @@ private fun TournamentEditorMetaTab( modifier = Modifier.fillMaxWidth().onFocusChanged { fs: FocusState -> if (!fs.isFocused) saveIfChanged(name, date, winningSets, tables, doubles, lastSaved, onSave) }, singleLine = true, ) - OutlinedTextField( + LocalizedDatePickerField( value = date, - onValueChange = { date = it }, - modifier = Modifier.fillMaxWidth().onFocusChanged { fs: FocusState -> if (!fs.isFocused) saveIfChanged(name, date, winningSets, tables, doubles, lastSaved, onSave) }, - label = { Text(tr("tournaments.date", "Datum")) }, - singleLine = true, + onValueChange = { + date = it + saveIfChanged(name, it, winningSets, tables, doubles, lastSaved, onSave) + }, + label = tr("tournaments.date", "Datum"), + modifier = Modifier.fillMaxWidth(), ) OutlinedTextField( value = winningSets, 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 a9ffe577..de3855e6 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 @@ -1026,7 +1026,7 @@ private fun HistoryTable(rows: List, tr: (String, String) -> Str maxLines = 2, overflow = TextOverflow.Ellipsis, ) - Text(r.date ?: "–", Modifier.widthIn(72.dp), style = MaterialTheme.typography.body2) + Text(formatIsoDateForDisplay(r.date), Modifier.widthIn(72.dp), style = MaterialTheme.typography.body2) Text(r.placement ?: "–", Modifier.widthIn(52.dp), style = MaterialTheme.typography.body2) } Divider(color = OfficialPanelBorder.copy(alpha = 0.6f)) @@ -1324,9 +1324,9 @@ private fun CompetitionsTabContent( } if (exp) { Column(Modifier.padding(start = 32.dp, bottom = 8.dp)) { - Text("${tr("officialTournaments.deadlineDate", "Meldeschluss")}: ${c.registrationDeadlineDate ?: c.meldeschlussDatum ?: "–"}", style = MaterialTheme.typography.caption) - Text("${tr("officialTournaments.deadlineOnline", "Online bis")}: ${c.registrationDeadlineOnline ?: c.meldeschlussOnline ?: "–"}", style = MaterialTheme.typography.caption) - Text("${tr("officialTournaments.cutoffDate", "Stichtag")}: ${c.cutoffDate ?: c.stichtag ?: "–"}", style = MaterialTheme.typography.caption) + Text("${tr("officialTournaments.deadlineDate", "Meldeschluss")}: ${formatIsoDateForDisplay(c.registrationDeadlineDate ?: c.meldeschlussDatum)}", style = MaterialTheme.typography.caption) + Text("${tr("officialTournaments.deadlineOnline", "Online bis")}: ${formatIsoDateForDisplay(c.registrationDeadlineOnline ?: c.meldeschlussOnline)}", style = MaterialTheme.typography.caption) + Text("${tr("officialTournaments.cutoffDate", "Stichtag")}: ${formatIsoDateForDisplay(c.cutoffDate ?: c.stichtag)}", style = MaterialTheme.typography.caption) Text("${tr("officialTournaments.openTo", "Offen für")}: ${c.openTo ?: c.offenFuer ?: "–"}", style = MaterialTheme.typography.caption) Text("${tr("officialTournaments.preliminaryRound", "Vorrunde")}: ${c.preliminaryRound ?: c.vorrunde ?: "–"}", style = MaterialTheme.typography.caption) Text("${tr("officialTournaments.finalRound", "Endrunde")}: ${c.finalRound ?: c.endrunde ?: "–"}", style = MaterialTheme.typography.caption) @@ -1571,7 +1571,7 @@ private fun MemberSelectionDialog( ) Column { Text(cr.name, style = MaterialTheme.typography.caption) - Text("${cr.date} ${cr.time} · ${cr.entryFee}", style = MaterialTheme.typography.caption) + Text("${formatIsoDateForDisplay(cr.date)} ${cr.time} · ${cr.entryFee}", style = MaterialTheme.typography.caption) } } } 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 e5ce89fe..91c68435 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 @@ -638,7 +638,7 @@ private fun FriendlyMatchEditDialog( onDelete: (() -> Unit)?, ) { LaunchedEffect(Unit) { onLoadMembers() } - var date by remember(match?.id) { mutableStateOf(match?.date?.take(10) ?: java.time.LocalDate.now().toString()) } + var date by remember(match?.id) { mutableStateOf(match?.date?.take(10) ?: todayIsoDate()) } var time by remember(match?.id) { mutableStateOf(match?.time?.take(5) ?: "") } var homeTeam by remember(match?.id) { mutableStateOf(match?.homeTeam?.name ?: clubName) } var guestTeam by remember(match?.id) { mutableStateOf(match?.guestTeam?.name ?: "") } @@ -668,7 +668,7 @@ private fun FriendlyMatchEditDialog( text = { Column(Modifier.verticalScroll(rememberScrollState())) { error?.let { Text(it, color = MaterialTheme.colors.error) } - OutlinedTextField(date, { date = it }, label = { Text("Datum") }, enabled = !readonly, modifier = Modifier.fillMaxWidth()) + LocalizedDatePickerField(date, { date = it }, label = "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()) diff --git a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/TournamentsScreen.kt b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/TournamentsScreen.kt index f367df3f..71f0a096 100644 --- a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/TournamentsScreen.kt +++ b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/TournamentsScreen.kt @@ -436,7 +436,7 @@ private fun TournamentCreateStandardScreen( onCreated: (Int) -> Unit, ) { var name by remember { mutableStateOf("") } - var date by rememberSaveable { mutableStateOf("") } + var date by rememberSaveable { mutableStateOf(todayIsoDate()) } var ws by remember { mutableStateOf("3") } var error by remember { mutableStateOf(null) } var busy by remember { mutableStateOf(false) } @@ -462,12 +462,11 @@ private fun TournamentCreateStandardScreen( modifier = Modifier.fillMaxWidth(), singleLine = true, ) - OutlinedTextField( + LocalizedDatePickerField( value = date, onValueChange = { date = it }, - label = { Text(tr("tournaments.date", "Datum (YYYY-MM-DD)")) }, + label = tr("tournaments.date", "Datum"), modifier = Modifier.fillMaxWidth(), - singleLine = true, ) OutlinedTextField( value = ws, @@ -520,7 +519,7 @@ private fun TournamentCreateMiniScreen( onCreated: (Int) -> Unit, ) { var ort by remember { mutableStateOf("") } - var date by rememberSaveable { mutableStateOf("") } + var date by rememberSaveable { mutableStateOf(todayIsoDate()) } var year by remember { mutableStateOf(Calendar.getInstance().get(Calendar.YEAR).toString()) } var ws by remember { mutableStateOf("1") } var error by remember { mutableStateOf(null) } @@ -547,12 +546,11 @@ private fun TournamentCreateMiniScreen( modifier = Modifier.fillMaxWidth(), singleLine = true, ) - OutlinedTextField( + LocalizedDatePickerField( value = date, onValueChange = { date = it }, - label = { Text(tr("tournaments.date", "Datum")) }, + label = tr("tournaments.date", "Datum"), modifier = Modifier.fillMaxWidth(), - singleLine = true, ) OutlinedTextField( value = year,