Compare commits

..
Author SHA1 Message Date
admin 3267fee439 Usprawnij wybór urlopu przy konfliktach 2026-07-09 00:31:06 +02:00
4 changed files with 91 additions and 17 deletions
+2 -2
View File
@@ -34,8 +34,8 @@ android {
applicationId = "pl.firmatpp.kierowca" applicationId = "pl.firmatpp.kierowca"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 101 versionCode = 102
versionName = "1.0.48" versionName = "1.0.49"
setProperty("archivesBaseName", "pl.firmatpp.kierowca") setProperty("archivesBaseName", "pl.firmatpp.kierowca")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@@ -1053,10 +1053,23 @@ private fun LeaveCalendarScreen(
val context = LocalContext.current val context = LocalContext.current
val listState = rememberLazyListState() val listState = rememberLazyListState()
val months = remember(state.leaveCalendarLoadedUntil) { leaveCalendarMonths(state.leaveCalendarLoadedUntil) } val months = remember(state.leaveCalendarLoadedUntil) { leaveCalendarMonths(state.leaveCalendarLoadedUntil) }
var detailsDate by remember { mutableStateOf<LocalDate?>(null) } var showConflictDetails by remember { mutableStateOf(false) }
val detailsEntries = detailsDate val selectedConflictEntries = remember(
?.let { DriverLeaveRequestUiRules.calendarEntriesForDate(state.leaveCalendarEntries, it) } state.leaveCalendarEntries,
.orEmpty() state.leaveCalendarHasSelection,
state.leaveRequestDateFrom,
state.leaveRequestDateTo,
) {
if (state.leaveCalendarHasSelection) {
DriverLeaveRequestUiRules.calendarEntriesForRange(
entries = state.leaveCalendarEntries,
dateFrom = state.leaveRequestDateFrom,
dateTo = state.leaveRequestDateTo,
)
} else {
emptyList()
}
}
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
LaunchedEffect(months.size, state.leaveCalendarLoading) { LaunchedEffect(months.size, state.leaveCalendarLoading) {
@@ -1074,6 +1087,8 @@ private fun LeaveCalendarScreen(
bottomBar = { bottomBar = {
LeaveCalendarContinueBar( LeaveCalendarContinueBar(
state = state, state = state,
conflictCount = selectedConflictEntries.size,
onShowConflicts = { showConflictDetails = true },
onContinue = onContinue, onContinue = onContinue,
) )
}, },
@@ -1093,11 +1108,8 @@ private fun LeaveCalendarScreen(
selectedFrom = state.leaveRequestDateFrom, selectedFrom = state.leaveRequestDateFrom,
selectedTo = state.leaveRequestDateTo, selectedTo = state.leaveRequestDateTo,
hasSelection = state.leaveCalendarHasSelection, hasSelection = state.leaveCalendarHasSelection,
onDayClick = { date, entries -> onDayClick = { date, _ ->
onDateSelected(date.toString()) onDateSelected(date.toString())
if (entries.isNotEmpty()) {
detailsDate = date
}
}, },
) )
} }
@@ -1121,15 +1133,15 @@ private fun LeaveCalendarScreen(
} }
} }
if (detailsDate != null && detailsEntries.isNotEmpty()) { if (showConflictDetails && selectedConflictEntries.isNotEmpty()) {
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = { detailsDate = null }, onDismissRequest = { showConflictDetails = false },
sheetState = sheetState, sheetState = sheetState,
containerColor = TppTheme.colors.card, containerColor = TppTheme.colors.card,
) { ) {
LeaveCalendarDetailsSheet( LeaveCalendarDetailsSheet(
date = detailsDate, title = "Urlopy w wybranym terminie",
entries = detailsEntries, entries = selectedConflictEntries,
onCall = { dialUri -> onCall = { dialUri ->
context.startActivity(Intent(Intent.ACTION_DIAL, Uri.parse(dialUri))) context.startActivity(Intent(Intent.ACTION_DIAL, Uri.parse(dialUri)))
}, },
@@ -1139,7 +1151,12 @@ private fun LeaveCalendarScreen(
} }
@Composable @Composable
private fun LeaveCalendarContinueBar(state: DriverUiState, onContinue: () -> Unit) { private fun LeaveCalendarContinueBar(
state: DriverUiState,
conflictCount: Int,
onShowConflicts: () -> Unit,
onContinue: () -> Unit,
) {
val selectionText = when { val selectionText = when {
!state.leaveCalendarHasSelection -> "Wybierz pierwszy dzień urlopu" !state.leaveCalendarHasSelection -> "Wybierz pierwszy dzień urlopu"
else -> leaveDateRange(state.leaveRequestDateFrom, state.leaveRequestDateTo) else -> leaveDateRange(state.leaveRequestDateFrom, state.leaveRequestDateTo)
@@ -1178,6 +1195,28 @@ private fun LeaveCalendarContinueBar(state: DriverUiState, onContinue: () -> Uni
Text("Dalej", fontWeight = FontWeight.Bold) Text("Dalej", fontWeight = FontWeight.Bold)
} }
} }
if (state.leaveCalendarHasSelection && conflictCount > 0) {
Row(
Modifier
.fillMaxWidth()
.background(TppTheme.colors.warningContainer, RoundedCornerShape(4.dp))
.border(1.dp, TppTheme.colors.warningOutline, RoundedCornerShape(4.dp))
.padding(horizontal = 12.dp, vertical = 10.dp),
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(Icons.Outlined.Info, contentDescription = null, tint = Color(0xFF7A5A00), modifier = Modifier.size(20.dp))
Text(
"Nakłada się z ${leaveConflictCountLabel(conflictCount)}",
color = Color(0xFF7A5A00),
fontWeight = FontWeight.Bold,
modifier = Modifier.weight(1f),
)
TextButton(onClick = onShowConflicts) {
Text("Zobacz", color = TppTheme.colors.forest, fontWeight = FontWeight.Bold)
}
}
}
} }
} }
@@ -1307,7 +1346,7 @@ private fun LeaveCalendarDayCell(
@Composable @Composable
private fun LeaveCalendarDetailsSheet( private fun LeaveCalendarDetailsSheet(
date: LocalDate?, title: String,
entries: List<DriverLeaveCalendarEntryDto>, entries: List<DriverLeaveCalendarEntryDto>,
onCall: (String) -> Unit, onCall: (String) -> Unit,
) { ) {
@@ -1316,7 +1355,7 @@ private fun LeaveCalendarDetailsSheet(
verticalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp),
) { ) {
Text( Text(
date?.let { DateTimeFormatter.ofPattern("d MMMM yyyy", Locale("pl", "PL")).format(it) } ?: "Urlopy w tym dniu", title,
color = TppTheme.colors.ink, color = TppTheme.colors.ink,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
fontSize = 22.sp, fontSize = 22.sp,
@@ -2926,6 +2965,9 @@ private fun photoCountLabel(count: Int): String =
else -> "zdjęć" else -> "zdjęć"
} }
private fun leaveConflictCountLabel(count: Int): String =
if (count == 1) "1 urlopem" else "$count urlopami"
@Composable @Composable
private fun ManifestSection(route: DriverRouteDto, selectedDate: String, onNavigate: (NavigationPointDto) -> Unit) { private fun ManifestSection(route: DriverRouteDto, selectedDate: String, onNavigate: (NavigationPointDto) -> Unit) {
val stripColor = routeStatusColor(route.status) val stripColor = routeStatusColor(route.status)
@@ -61,6 +61,19 @@ object DriverLeaveRequestUiRules {
from != null && to != null && !date.isBefore(from) && !date.isAfter(to) from != null && to != null && !date.isBefore(from) && !date.isAfter(to)
} }
fun calendarEntriesForRange(entries: List<DriverLeaveCalendarEntryDto>, dateFrom: String, dateTo: String): List<DriverLeaveCalendarEntryDto> {
val selectedFrom = runCatching { LocalDate.parse(dateFrom) }.getOrNull() ?: return emptyList()
val selectedTo = runCatching { LocalDate.parse(dateTo) }.getOrNull() ?: return emptyList()
val from = minOf(selectedFrom, selectedTo)
val to = maxOf(selectedFrom, selectedTo)
return entries.filter { entry ->
val entryFrom = entry.dateFrom?.let { runCatching { LocalDate.parse(it) }.getOrNull() }
val entryTo = entry.dateTo?.let { runCatching { LocalDate.parse(it) }.getOrNull() }
entryFrom != null && entryTo != null && !entryTo.isBefore(from) && !entryFrom.isAfter(to)
}
}
fun phoneDialUri(phoneNumber: String?): String? { fun phoneDialUri(phoneNumber: String?): String? {
val raw = phoneNumber?.trim().orEmpty() val raw = phoneNumber?.trim().orEmpty()
if (raw.isBlank()) return null if (raw.isBlank()) return null
@@ -64,6 +64,25 @@ class DriverLeaveRequestUiRulesTest {
assertEquals(listOf("week", "single-day"), matching.map { it.id }) assertEquals(listOf("week", "single-day"), matching.map { it.id })
} }
@Test
fun findsCalendarEntriesThatOverlapSelectedRange() {
val entries = listOf(
calendarEntry(id = "starts-before", dateFrom = "2026-08-01", dateTo = "2026-08-04"),
calendarEntry(id = "inside", dateFrom = "2026-08-06", dateTo = "2026-08-07"),
calendarEntry(id = "ends-after", dateFrom = "2026-08-09", dateTo = "2026-08-12"),
calendarEntry(id = "before", dateFrom = "2026-07-20", dateTo = "2026-07-22"),
calendarEntry(id = "after", dateFrom = "2026-08-13", dateTo = "2026-08-14"),
)
val matching = DriverLeaveRequestUiRules.calendarEntriesForRange(
entries = entries,
dateFrom = "2026-08-03",
dateTo = "2026-08-10",
)
assertEquals(listOf("starts-before", "inside", "ends-after"), matching.map { it.id })
}
@Test @Test
fun normalizesPhoneNumberIntoDialerUri() { fun normalizesPhoneNumberIntoDialerUri() {
assertEquals("tel:+48600700800", DriverLeaveRequestUiRules.phoneDialUri("+48 600 700 800")) assertEquals("tel:+48600700800", DriverLeaveRequestUiRules.phoneDialUri("+48 600 700 800"))