diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fd975e5..c9b739a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -33,8 +33,8 @@ android { applicationId = "pl.firmatpp.kierowca" minSdk = 26 targetSdk = 35 - versionCode = 39 - versionName = "1.0.38" + versionCode = 40 + versionName = "1.0.39" setProperty("archivesBaseName", "pl.firmatpp.kierowca") testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/app/src/main/java/pl/firmatpp/kierowca/ui/DriverApp.kt b/app/src/main/java/pl/firmatpp/kierowca/ui/DriverApp.kt index c98cce1..dc2c0b9 100644 --- a/app/src/main/java/pl/firmatpp/kierowca/ui/DriverApp.kt +++ b/app/src/main/java/pl/firmatpp/kierowca/ui/DriverApp.kt @@ -74,6 +74,8 @@ import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.DatePickerDialog +import androidx.compose.material3.DateRangePicker import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon @@ -86,6 +88,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberDateRangePickerState import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect @@ -127,6 +130,7 @@ import java.io.File import java.time.Instant import java.time.LocalDate import java.time.ZoneId +import java.time.ZoneOffset import java.time.format.DateTimeFormatter import kotlinx.coroutines.delay import java.util.Locale @@ -210,7 +214,6 @@ fun DriverApp(viewModel: DriverViewModel, initialRouteId: String? = null, initia onDate = viewModel::selectRouteDate, onProfile = viewModel::openProfile, onPhotoQueue = viewModel::openPhotoQueue, - onLeaveRequests = viewModel::openLeaveRequests, onDispatchSheetUpload = viewModel::uploadDispatchSheetPhoto, onRoute = viewModel::openRoute, onDismissLiveUpdate = viewModel::dismissRouteDayLiveUpdate, @@ -220,6 +223,7 @@ fun DriverApp(viewModel: DriverViewModel, initialRouteId: String? = null, initia onRoutes = viewModel::refreshRoutes, onProfile = viewModel::openProfile, onLogout = viewModel::logout, + onLeaveRequests = viewModel::openLeaveRequests, onNewRouteNotificationsChanged = { enabled -> if (!enabled) { viewModel.updateNewRouteNotifications(false) @@ -574,7 +578,6 @@ private fun RoutesScreen( onDate: (String) -> Unit, onProfile: () -> Unit, onPhotoQueue: () -> Unit, - onLeaveRequests: () -> Unit, onDispatchSheetUpload: (Uri, String, PhotoUploadMetadata) -> Unit, onRoute: (String) -> Unit, onDismissLiveUpdate: () -> Unit, @@ -686,9 +689,6 @@ private fun RoutesScreen( onDate = onDate, ) } - if (DriverLeaveRequestUiRules.isFeatureVisible(state.leaveRequestsConfig)) { - item { LeaveRequestsEntryCard(state.leaveRequests, onLeaveRequests) } - } if (shouldShowDispatchSheetReminderCard(state.dispatchSheetReminder)) { item { DispatchSheetReminderCard( @@ -833,15 +833,6 @@ private fun LeaveRequestsScreen( Scaffold( topBar = { SimpleTopBar("Wnioski urlopowe", onBack) }, containerColor = TppColors.Surface, - floatingActionButton = { - Button( - onClick = onAdd, - colors = ButtonDefaults.buttonColors(containerColor = TppColors.Forest), - shape = RoundedCornerShape(8.dp), - ) { - Text("Nowy wniosek") - } - }, ) { padding -> Box(Modifier.fillMaxSize().padding(padding).pullRefresh(pullRefreshState)) { LazyColumn( @@ -850,6 +841,18 @@ private fun LeaveRequestsScreen( verticalArrangement = Arrangement.spacedBy(12.dp), ) { item { FeedbackAndError(state.feedback, state.error) } + item { + Button( + onClick = onAdd, + modifier = Modifier.fillMaxWidth().height(56.dp), + colors = ButtonDefaults.buttonColors(containerColor = TppColors.Forest), + shape = RoundedCornerShape(8.dp), + ) { + Icon(Icons.Outlined.CalendarToday, contentDescription = null) + Spacer(Modifier.width(8.dp)) + Text("Nowy wniosek", fontWeight = FontWeight.Bold) + } + } if (state.leaveRequests.isEmpty()) { item { EmptyState("Nie masz jeszcze wniosków urlopowych") } } else { @@ -978,6 +981,18 @@ private fun AddLeaveRequestScreen( onDraft: (String?, String?, String?, String?) -> Unit, onSubmit: () -> Unit, ) { + var showDateRangePicker by remember { mutableStateOf(false) } + val selectedStartMillis = remember(state.leaveRequestDateFrom) { + localDateStringToUtcMillis(state.leaveRequestDateFrom) + } + val selectedEndMillis = remember(state.leaveRequestDateTo) { + localDateStringToUtcMillis(state.leaveRequestDateTo) + } + val dateRangePickerState = rememberDateRangePickerState( + initialSelectedStartDateMillis = selectedStartMillis, + initialSelectedEndDateMillis = selectedEndMillis, + ) + Scaffold(topBar = { SimpleTopBar("Nowy wniosek", onBack) }, containerColor = TppColors.Surface) { padding -> LazyColumn( Modifier.fillMaxSize().padding(padding), @@ -1007,18 +1022,33 @@ private fun AddLeaveRequestScreen( } } item { - LeaveTextField( - label = "Data od (RRRR-MM-DD)", - value = state.leaveRequestDateFrom, - onValue = { onDraft(it, null, null, null) }, - ) - } - item { - LeaveTextField( - label = "Data do (RRRR-MM-DD)", - value = state.leaveRequestDateTo, - onValue = { onDraft(null, it, null, null) }, - ) + Card( + colors = CardDefaults.cardColors(containerColor = Color.White), + border = BorderStroke(1.dp, TppColors.Outline), + shape = RoundedCornerShape(8.dp), + modifier = Modifier.fillMaxWidth().clickable { showDateRangePicker = true }, + ) { + Row( + Modifier.fillMaxWidth().padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + Modifier.size(44.dp).background(Color(0xFFEAF7EF), RoundedCornerShape(8.dp)), + contentAlignment = Alignment.Center, + ) { + Icon(Icons.Outlined.CalendarToday, contentDescription = null, tint = TppColors.Forest) + } + Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text("Okres", color = TppColors.Ink, fontWeight = FontWeight.Bold) + Text( + leaveDateRange(state.leaveRequestDateFrom, state.leaveRequestDateTo), + color = TppColors.Muted, + ) + } + Text("Zmień", color = TppColors.Forest, fontWeight = FontWeight.Bold) + } + } } item { LeaveTextField( @@ -1040,6 +1070,55 @@ private fun AddLeaveRequestScreen( } } } + + if (showDateRangePicker) { + DatePickerDialog( + onDismissRequest = { showDateRangePicker = false }, + confirmButton = { + TextButton( + onClick = { + val start = dateRangePickerState.selectedStartDateMillis + val end = dateRangePickerState.selectedEndDateMillis ?: start + if (start != null && end != null) { + onDraft(utcMillisToLocalDateString(start), utcMillisToLocalDateString(end), null, null) + } + showDateRangePicker = false + }, + enabled = dateRangePickerState.selectedStartDateMillis != null, + ) { + Text("Ustaw", color = TppColors.Forest, fontWeight = FontWeight.Bold) + } + }, + dismissButton = { + TextButton(onClick = { showDateRangePicker = false }) { + Text("Anuluj", color = TppColors.Muted) + } + }, + ) { + DateRangePicker( + state = dateRangePickerState, + title = { + Text( + "Wybierz okres urlopu", + modifier = Modifier.padding(start = 24.dp, end = 12.dp, top = 16.dp), + color = TppColors.Ink, + fontWeight = FontWeight.Bold, + ) + }, + headline = { + Text( + leaveDateRange( + dateRangePickerState.selectedStartDateMillis?.let(::utcMillisToLocalDateString), + dateRangePickerState.selectedEndDateMillis?.let(::utcMillisToLocalDateString), + ), + modifier = Modifier.padding(start = 24.dp, end = 12.dp, bottom = 12.dp), + color = TppColors.Muted, + ) + }, + showModeToggle = false, + ) + } + } } @Composable @@ -1111,6 +1190,14 @@ private fun LeaveTextField(label: String, value: String, onValue: (String) -> Un private fun leaveDateRange(from: String?, to: String?): String = if (from == to) from ?: "-" else "${from ?: "?"} - ${to ?: "?"}" +private fun localDateStringToUtcMillis(value: String): Long? = + runCatching { + LocalDate.parse(value).atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli() + }.getOrNull() + +private fun utcMillisToLocalDateString(value: Long): String = + Instant.ofEpochMilli(value).atZone(ZoneOffset.UTC).toLocalDate().toString() + private fun shortDateTime(value: String): String = runCatching { DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm", Locale("pl", "PL")) @@ -1575,6 +1662,7 @@ private fun ProfileScreen( onRoutes: () -> Unit, onProfile: () -> Unit, onLogout: () -> Unit, + onLeaveRequests: () -> Unit, onNewRouteNotificationsChanged: (Boolean) -> Unit, onOpenNotificationSettings: () -> Unit, ) { @@ -1619,6 +1707,9 @@ private fun ProfileScreen( ) } } + if (DriverLeaveRequestUiRules.isFeatureVisible(state.leaveRequestsConfig)) { + LeaveRequestsEntryCard(state.leaveRequests, onLeaveRequests) + } Card( modifier = Modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = Color.White),