Compare commits

..
1 Commits
Author SHA1 Message Date
admin 0e15d5f9a4 Show live day updates and route date chip 2026-07-02 16:24:06 +02:00
5 changed files with 156 additions and 18 deletions
+2 -2
View File
@@ -33,8 +33,8 @@ android {
applicationId = "pl.firmatpp.kierowca"
minSdk = 26
targetSdk = 35
versionCode = 35
versionName = "1.0.34"
versionCode = 36
versionName = "1.0.35"
setProperty("archivesBaseName", "pl.firmatpp.kierowca")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@@ -203,6 +203,7 @@ fun DriverApp(viewModel: DriverViewModel, initialRouteId: String? = null) {
onProfile = viewModel::openProfile,
onPhotoQueue = viewModel::openPhotoQueue,
onRoute = viewModel::openRoute,
onDismissLiveUpdate = viewModel::dismissRouteDayLiveUpdate,
)
DriverScreen.Profile -> ProfileScreen(
state = state,
@@ -546,7 +547,15 @@ private fun RoutesScreen(
onProfile: () -> Unit,
onPhotoQueue: () -> Unit,
onRoute: (String) -> Unit,
onDismissLiveUpdate: () -> Unit,
) {
LaunchedEffect(state.routeDayLiveUpdateMessage) {
if (!state.routeDayLiveUpdateMessage.isNullOrBlank()) {
delay(6_000)
onDismissLiveUpdate()
}
}
BoxWithConstraints(Modifier.fillMaxSize()) {
val screenWidthDp = maxWidth.value.toInt()
val screenHeightDp = maxHeight.value.toInt()
@@ -599,6 +608,7 @@ private fun RoutesScreen(
}
}
}
item { RouteDayLiveUpdateBanner(state.routeDayLiveUpdateMessage) }
item {
RouteDateSelector(
selectedDate = state.selectedDate,
@@ -628,6 +638,37 @@ private fun RoutesScreen(
}
}
@Composable
private fun RouteDayLiveUpdateBanner(message: String?) {
if (message.isNullOrBlank()) return
Card(
colors = CardDefaults.cardColors(containerColor = Color(0xFFEAF7EF)),
border = BorderStroke(1.dp, Color(0xFF9BD1AD)),
shape = RoundedCornerShape(8.dp),
modifier = Modifier.fillMaxWidth(),
) {
Row(
Modifier.fillMaxWidth().padding(14.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
Box(
Modifier.size(40.dp).background(Color.White, RoundedCornerShape(6.dp)),
contentAlignment = Alignment.Center,
) {
Icon(Icons.Outlined.Refresh, contentDescription = null, tint = TppColors.Forest)
}
Text(
message,
color = TppColors.Forest,
fontWeight = FontWeight.Bold,
modifier = Modifier.weight(1f),
)
}
}
}
@Composable
private fun PhotoQueueBanner(uploads: List<PhotoUploadEntity>, onPhotoQueue: () -> Unit) {
val count = queuedPhotoUploadCount(uploads.map { it.status })
@@ -1238,7 +1279,7 @@ private fun DetailScreen(
contentPadding = PaddingValues(horizontal = horizontalPadding, vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(24.dp),
) {
item { ManifestSection(route, onNavigate = { openNavigation(context, it) }) }
item { ManifestSection(route, selectedDate = state.selectedDate, onNavigate = { openNavigation(context, it) }) }
item { OfflineStaleBanner(state) }
item {
RouteCompletionSection(
@@ -1456,8 +1497,9 @@ private fun photoCountLabel(count: Int): String =
}
@Composable
private fun ManifestSection(route: DriverRouteDto, onNavigate: (NavigationPointDto) -> Unit) {
private fun ManifestSection(route: DriverRouteDto, selectedDate: String, onNavigate: (NavigationPointDto) -> Unit) {
val stripColor = routeStatusColor(route.status)
val routeDate = route.routeDate ?: selectedDate
Card(
colors = CardDefaults.cardColors(containerColor = Color.White),
shape = RoundedCornerShape(8.dp),
@@ -1465,13 +1507,17 @@ private fun ManifestSection(route: DriverRouteDto, onNavigate: (NavigationPointD
) {
Column {
Column(Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(14.dp)) {
Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.Top,
) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.weight(1f)) {
StatusChip(route.status, stripColor)
Box(Modifier.weight(1f), contentAlignment = Alignment.CenterEnd) {
RouteDateStatusChip(routeDate)
}
}
Text(
"Zlecenie #${route.contractCode ?: route.id}",
style = MaterialTheme.typography.headlineSmall,
@@ -1480,7 +1526,6 @@ private fun ManifestSection(route: DriverRouteDto, onNavigate: (NavigationPointD
)
}
}
}
HorizontalDivider(color = TppColors.Outline.copy(alpha = 0.65f))
RoutePointBlock(
label = "Załadunek",
@@ -1509,6 +1554,32 @@ private fun ManifestSection(route: DriverRouteDto, onNavigate: (NavigationPointD
}
}
@Composable
private fun RouteDateStatusChip(routeDate: String, modifier: Modifier = Modifier) {
Row(
modifier = modifier
.background(Color(0xFFE9F2FF), RoundedCornerShape(3.dp))
.padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Icon(
Icons.Outlined.CalendarToday,
contentDescription = null,
tint = Color(0xFF1D4E89),
modifier = Modifier.size(16.dp),
)
Text(
routeDateChipLabel(routeDate),
color = Color(0xFF1D4E89),
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
}
}
@Composable
private fun RoutePointBlock(
label: String,
@@ -1,12 +1,17 @@
package pl.firmatpp.kierowca.ui
import java.io.File
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.OffsetDateTime
import java.time.format.DateTimeFormatter
import java.time.temporal.ChronoUnit
import pl.firmatpp.kierowca.data.model.DriverRouteDto
import pl.firmatpp.kierowca.data.model.RoutePhotoDto
import pl.firmatpp.kierowca.data.upload.PhotoUploadEntity
private val shortDateFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("dd.MM")
fun canManageRoutePhotos(selectedDate: String, today: LocalDate = LocalDate.now()): Boolean =
runCatching { LocalDate.parse(selectedDate).isEqual(today) }.getOrDefault(false)
@@ -18,6 +23,38 @@ fun canCompleteRouteFromDriverApp(
canManageRoutePhotos(selectedDate, today)
&& route.status in setOf("ZAPLANOWANA", "W TRAKCIE")
fun shouldShowRouteDayLiveUpdate(viewedDate: String, hintDate: String?): Boolean =
viewedDate.isNotBlank() && (hintDate == null || hintDate == viewedDate)
fun routeDateChipLabel(routeDate: String?, today: LocalDate = LocalDate.now()): String {
val date = routeDate
?.takeIf { it.isNotBlank() }
?.let { runCatching { LocalDate.parse(it) }.getOrNull() }
?: return ""
val fallback = date.format(shortDateFormatter)
val days = ChronoUnit.DAYS.between(today, date)
val human = when {
days == 0L -> "dzisiaj"
days == 1L -> "jutro"
days in 2L..7L -> polishWeekdayPhrase(date.dayOfWeek)
else -> null
}
return human?.let { "$it · $fallback" } ?: fallback
}
private fun polishWeekdayPhrase(day: DayOfWeek): String =
when (day) {
DayOfWeek.MONDAY -> "w poniedziałek"
DayOfWeek.TUESDAY -> "we wtorek"
DayOfWeek.WEDNESDAY -> "w środę"
DayOfWeek.THURSDAY -> "w czwartek"
DayOfWeek.FRIDAY -> "w piątek"
DayOfWeek.SATURDAY -> "w sobotę"
DayOfWeek.SUNDAY -> "w niedzielę"
}
fun inlinePhotoGridRows(photoCount: Int): Int =
if (photoCount <= 0) 0 else (photoCount + 1) / 2
@@ -56,6 +56,7 @@ data class DriverUiState(
val isOnline: Boolean = true,
val isStale: Boolean = false,
val lastSuccessfulSyncAtEpochMillis: Long? = null,
val routeDayLiveUpdateMessage: String? = null,
val feedback: String? = null,
val error: String? = null,
)
@@ -137,7 +138,14 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
fun refreshRoutesSilently() = loadRoutes(date = _state.value.selectedDate, showLoading = false, navigateToRoutes = false)
fun selectRouteDate(date: String) = loadRoutes(date = date, showLoading = true, navigateToRoutes = true)
fun selectRouteDate(date: String) {
_state.update { it.copy(routeDayLiveUpdateMessage = null) }
loadRoutes(date = date, showLoading = true, navigateToRoutes = true)
}
fun dismissRouteDayLiveUpdate() {
_state.update { it.copy(routeDayLiveUpdateMessage = null) }
}
private fun loadRoutes(date: String?, showLoading: Boolean, navigateToRoutes: Boolean) {
viewModelScope.launch {
@@ -457,8 +465,13 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
val snapshot = _state.value
when (hint.scope) {
DriverSyncRepository.SCOPE_ROUTES -> {
if (hint.date == null || hint.date == snapshot.selectedDate) {
if (shouldShowRouteDayLiveUpdate(snapshot.selectedDate, hint.date)) {
refreshRoutesSilently()
if (snapshot.screen == DriverScreen.Routes) {
_state.update {
it.copy(routeDayLiveUpdateMessage = "Spedytor zaktualizował zlecenia dla tego dnia.")
}
}
} else {
DriverSyncWorker.enqueue(getApplication(), hint.date, null)
}
@@ -34,6 +34,23 @@ class DriverUiRulesTest {
assertFalse(canCompleteRouteFromDriverApp(route(status = "ZAPLANOWANA"), "2026-06-29", today))
}
@Test
fun showsLiveRouteDayUpdateOnlyForViewedDate() {
assertTrue(shouldShowRouteDayLiveUpdate(viewedDate = "2026-03-12", hintDate = "2026-03-12"))
assertTrue(shouldShowRouteDayLiveUpdate(viewedDate = "2026-03-12", hintDate = null))
assertFalse(shouldShowRouteDayLiveUpdate(viewedDate = "2026-03-12", hintDate = "2026-03-13"))
assertFalse(shouldShowRouteDayLiveUpdate(viewedDate = "", hintDate = "2026-03-12"))
}
@Test
fun formatsRouteDateChipHumanFirstWithShortFallback() {
assertEquals("dzisiaj · 30.06", routeDateChipLabel("2026-06-30", today))
assertEquals("jutro · 01.07", routeDateChipLabel("2026-07-01", today))
assertEquals("w poniedziałek · 06.07", routeDateChipLabel("2026-07-06", today))
assertEquals("02.03", routeDateChipLabel("2027-03-02", today))
assertEquals("", routeDateChipLabel("", today))
}
@Test
fun calculatesPhotoGridRowsForTwoColumnInlineGallery() {
assertEquals(0, inlinePhotoGridRows(0))