Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0115e85fc | ||
|
|
0e15d5f9a4 | ||
|
|
5555676395 | ||
|
|
ca512f5993 |
@@ -33,8 +33,8 @@ android {
|
|||||||
applicationId = "pl.firmatpp.kierowca"
|
applicationId = "pl.firmatpp.kierowca"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 33
|
versionCode = 37
|
||||||
versionName = "1.0.32"
|
versionName = "1.0.36"
|
||||||
setProperty("archivesBaseName", "pl.firmatpp.kierowca")
|
setProperty("archivesBaseName", "pl.firmatpp.kierowca")
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ data class BootstrapResponse(
|
|||||||
val routes: RoutesBucketDto,
|
val routes: RoutesBucketDto,
|
||||||
val driverAppSettings: DriverAppSettingsDto?,
|
val driverAppSettings: DriverAppSettingsDto?,
|
||||||
val notificationPreferences: NotificationPreferencesDto? = null,
|
val notificationPreferences: NotificationPreferencesDto? = null,
|
||||||
|
val realtime: RealtimeConfigDto? = null,
|
||||||
val syncState: SyncStateResponse? = null,
|
val syncState: SyncStateResponse? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -71,6 +72,12 @@ data class NotificationPreferencesDto(
|
|||||||
val notifyNewRoutes: Boolean = false,
|
val notifyNewRoutes: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class RealtimeConfigDto(
|
||||||
|
val reverbEnabled: Boolean = false,
|
||||||
|
val reverbAppKey: String? = null,
|
||||||
|
val reverbWsBaseUrl: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
data class RoutesBucketDto(
|
data class RoutesBucketDto(
|
||||||
val today: List<DriverRouteDto> = emptyList(),
|
val today: List<DriverRouteDto> = emptyList(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ import okhttp3.OkHttpClient
|
|||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.WebSocket
|
import okhttp3.WebSocket
|
||||||
import okhttp3.WebSocketListener
|
import okhttp3.WebSocketListener
|
||||||
import pl.firmatpp.kierowca.BuildConfig
|
|
||||||
import pl.firmatpp.kierowca.data.DriverRepository
|
import pl.firmatpp.kierowca.data.DriverRepository
|
||||||
|
import pl.firmatpp.kierowca.data.model.RealtimeConfigDto
|
||||||
|
|
||||||
class DriverLiveSyncClient(
|
class DriverLiveSyncClient(
|
||||||
private val repository: DriverRepository,
|
private val repository: DriverRepository,
|
||||||
@@ -25,20 +25,29 @@ class DriverLiveSyncClient(
|
|||||||
private val client: OkHttpClient = OkHttpClient(),
|
private val client: OkHttpClient = OkHttpClient(),
|
||||||
private val gson: Gson = Gson(),
|
private val gson: Gson = Gson(),
|
||||||
) {
|
) {
|
||||||
|
private companion object {
|
||||||
|
const val HEARTBEAT_INTERVAL_MS = 10_000L
|
||||||
|
}
|
||||||
|
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
private val started = AtomicBoolean(false)
|
private val started = AtomicBoolean(false)
|
||||||
private var webSocket: WebSocket? = null
|
private var webSocket: WebSocket? = null
|
||||||
private var driverId: String? = null
|
private var driverId: String? = null
|
||||||
|
private var realtimeConfig: RealtimeConfigDto? = null
|
||||||
private var socketId: String? = null
|
private var socketId: String? = null
|
||||||
private var heartbeatJob: Job? = null
|
private var heartbeatJob: Job? = null
|
||||||
|
|
||||||
fun start(driverId: String) {
|
fun start(driverId: String, config: RealtimeConfigDto?) {
|
||||||
if (BuildConfig.REVERB_APP_KEY.isBlank()) return
|
val appKey = config?.reverbAppKey?.takeIf { it.isNotBlank() } ?: return
|
||||||
|
val wsBaseUrl = config.reverbWsBaseUrl?.takeIf { it.isNotBlank() } ?: return
|
||||||
|
if (!config.reverbEnabled) return
|
||||||
|
|
||||||
|
realtimeConfig = config
|
||||||
this.driverId = driverId
|
this.driverId = driverId
|
||||||
if (!started.compareAndSet(false, true)) return
|
if (!started.compareAndSet(false, true)) return
|
||||||
|
|
||||||
val wsUrl = BuildConfig.REVERB_WS_BASE_URL.trimEnd('/') +
|
val wsUrl = wsBaseUrl.trimEnd('/') +
|
||||||
"/" + BuildConfig.REVERB_APP_KEY +
|
"/" + appKey +
|
||||||
"?protocol=7&client=android&version=1.0&flash=false"
|
"?protocol=7&client=android&version=1.0&flash=false"
|
||||||
|
|
||||||
webSocket = client.newWebSocket(
|
webSocket = client.newWebSocket(
|
||||||
@@ -72,6 +81,7 @@ class DriverLiveSyncClient(
|
|||||||
webSocket?.close(1000, "logout")
|
webSocket?.close(1000, "logout")
|
||||||
webSocket = null
|
webSocket = null
|
||||||
driverId = null
|
driverId = null
|
||||||
|
realtimeConfig = null
|
||||||
socketId = null
|
socketId = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +156,7 @@ class DriverLiveSyncClient(
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
delay(5_000)
|
delay(5_000)
|
||||||
if (!started.get() && driverId == id) {
|
if (!started.get() && driverId == id) {
|
||||||
start(id)
|
start(id, realtimeConfig)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,7 +166,7 @@ class DriverLiveSyncClient(
|
|||||||
heartbeatJob = scope.launch {
|
heartbeatJob = scope.launch {
|
||||||
while (started.get()) {
|
while (started.get()) {
|
||||||
reportRealtimeStatus("connected")
|
reportRealtimeStatus("connected")
|
||||||
delay(30_000)
|
delay(HEARTBEAT_INTERVAL_MS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -203,6 +203,7 @@ fun DriverApp(viewModel: DriverViewModel, initialRouteId: String? = null) {
|
|||||||
onProfile = viewModel::openProfile,
|
onProfile = viewModel::openProfile,
|
||||||
onPhotoQueue = viewModel::openPhotoQueue,
|
onPhotoQueue = viewModel::openPhotoQueue,
|
||||||
onRoute = viewModel::openRoute,
|
onRoute = viewModel::openRoute,
|
||||||
|
onDismissLiveUpdate = viewModel::dismissRouteDayLiveUpdate,
|
||||||
)
|
)
|
||||||
DriverScreen.Profile -> ProfileScreen(
|
DriverScreen.Profile -> ProfileScreen(
|
||||||
state = state,
|
state = state,
|
||||||
@@ -546,7 +547,15 @@ private fun RoutesScreen(
|
|||||||
onProfile: () -> Unit,
|
onProfile: () -> Unit,
|
||||||
onPhotoQueue: () -> Unit,
|
onPhotoQueue: () -> Unit,
|
||||||
onRoute: (String) -> Unit,
|
onRoute: (String) -> Unit,
|
||||||
|
onDismissLiveUpdate: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
LaunchedEffect(state.routeDayLiveUpdateMessage) {
|
||||||
|
if (!state.routeDayLiveUpdateMessage.isNullOrBlank()) {
|
||||||
|
delay(6_000)
|
||||||
|
onDismissLiveUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
BoxWithConstraints(Modifier.fillMaxSize()) {
|
BoxWithConstraints(Modifier.fillMaxSize()) {
|
||||||
val screenWidthDp = maxWidth.value.toInt()
|
val screenWidthDp = maxWidth.value.toInt()
|
||||||
val screenHeightDp = maxHeight.value.toInt()
|
val screenHeightDp = maxHeight.value.toInt()
|
||||||
@@ -554,7 +563,17 @@ private fun RoutesScreen(
|
|||||||
val compactWidth = screenWidthDp < 360
|
val compactWidth = screenWidthDp < 360
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = { StitchHeader(height = appBrandHeaderHeightDp(screenHeightDp).dp) },
|
topBar = {
|
||||||
|
Box(Modifier.fillMaxWidth()) {
|
||||||
|
StitchHeader(height = appBrandHeaderHeightDp(screenHeightDp).dp)
|
||||||
|
RouteDayLiveUpdateBanner(
|
||||||
|
message = state.routeDayLiveUpdateMessage,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.padding(horizontal = horizontalPadding, vertical = 10.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
StitchBottomBar(
|
StitchBottomBar(
|
||||||
activeScreen = DriverScreen.Routes,
|
activeScreen = DriverScreen.Routes,
|
||||||
@@ -628,6 +647,37 @@ private fun RoutesScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RouteDayLiveUpdateBanner(message: String?, modifier: Modifier = Modifier) {
|
||||||
|
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
|
@Composable
|
||||||
private fun PhotoQueueBanner(uploads: List<PhotoUploadEntity>, onPhotoQueue: () -> Unit) {
|
private fun PhotoQueueBanner(uploads: List<PhotoUploadEntity>, onPhotoQueue: () -> Unit) {
|
||||||
val count = queuedPhotoUploadCount(uploads.map { it.status })
|
val count = queuedPhotoUploadCount(uploads.map { it.status })
|
||||||
@@ -1238,7 +1288,7 @@ private fun DetailScreen(
|
|||||||
contentPadding = PaddingValues(horizontal = horizontalPadding, vertical = 16.dp),
|
contentPadding = PaddingValues(horizontal = horizontalPadding, vertical = 16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(24.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 { OfflineStaleBanner(state) }
|
||||||
item {
|
item {
|
||||||
RouteCompletionSection(
|
RouteCompletionSection(
|
||||||
@@ -1456,8 +1506,9 @@ private fun photoCountLabel(count: Int): String =
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@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 stripColor = routeStatusColor(route.status)
|
||||||
|
val routeDate = route.routeDate ?: selectedDate
|
||||||
Card(
|
Card(
|
||||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||||
shape = RoundedCornerShape(8.dp),
|
shape = RoundedCornerShape(8.dp),
|
||||||
@@ -1465,20 +1516,23 @@ private fun ManifestSection(route: DriverRouteDto, onNavigate: (NavigationPointD
|
|||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
Column(Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(14.dp)) {
|
Column(Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(14.dp)) {
|
||||||
Row(
|
Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
Modifier.fillMaxWidth(),
|
Row(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
Modifier.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.Top,
|
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
) {
|
verticalAlignment = Alignment.Top,
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.weight(1f)) {
|
) {
|
||||||
StatusChip(route.status, stripColor)
|
StatusChip(route.status, stripColor)
|
||||||
Text(
|
Box(Modifier.weight(1f), contentAlignment = Alignment.CenterEnd) {
|
||||||
"Zlecenie #${route.contractCode ?: route.id}",
|
RouteDateStatusChip(routeDate)
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
}
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
color = TppColors.Ink,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
Text(
|
||||||
|
"Zlecenie #${route.contractCode ?: route.id}",
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = TppColors.Ink,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HorizontalDivider(color = TppColors.Outline.copy(alpha = 0.65f))
|
HorizontalDivider(color = TppColors.Outline.copy(alpha = 0.65f))
|
||||||
@@ -1509,6 +1563,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
|
@Composable
|
||||||
private fun RoutePointBlock(
|
private fun RoutePointBlock(
|
||||||
label: String,
|
label: String,
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
package pl.firmatpp.kierowca.ui
|
package pl.firmatpp.kierowca.ui
|
||||||
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.time.DayOfWeek
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.OffsetDateTime
|
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.DriverRouteDto
|
||||||
import pl.firmatpp.kierowca.data.model.RoutePhotoDto
|
import pl.firmatpp.kierowca.data.model.RoutePhotoDto
|
||||||
import pl.firmatpp.kierowca.data.upload.PhotoUploadEntity
|
import pl.firmatpp.kierowca.data.upload.PhotoUploadEntity
|
||||||
|
|
||||||
|
private val shortDateFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("dd.MM")
|
||||||
|
|
||||||
fun canManageRoutePhotos(selectedDate: String, today: LocalDate = LocalDate.now()): Boolean =
|
fun canManageRoutePhotos(selectedDate: String, today: LocalDate = LocalDate.now()): Boolean =
|
||||||
runCatching { LocalDate.parse(selectedDate).isEqual(today) }.getOrDefault(false)
|
runCatching { LocalDate.parse(selectedDate).isEqual(today) }.getOrDefault(false)
|
||||||
|
|
||||||
@@ -18,6 +23,38 @@ fun canCompleteRouteFromDriverApp(
|
|||||||
canManageRoutePhotos(selectedDate, today)
|
canManageRoutePhotos(selectedDate, today)
|
||||||
&& route.status in setOf("ZAPLANOWANA", "W TRAKCIE")
|
&& 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 =
|
fun inlinePhotoGridRows(photoCount: Int): Int =
|
||||||
if (photoCount <= 0) 0 else (photoCount + 1) / 2
|
if (photoCount <= 0) 0 else (photoCount + 1) / 2
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ data class DriverUiState(
|
|||||||
val isOnline: Boolean = true,
|
val isOnline: Boolean = true,
|
||||||
val isStale: Boolean = false,
|
val isStale: Boolean = false,
|
||||||
val lastSuccessfulSyncAtEpochMillis: Long? = null,
|
val lastSuccessfulSyncAtEpochMillis: Long? = null,
|
||||||
|
val routeDayLiveUpdateMessage: String? = null,
|
||||||
val feedback: String? = null,
|
val feedback: String? = null,
|
||||||
val error: 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 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) {
|
private fun loadRoutes(date: String?, showLoading: Boolean, navigateToRoutes: Boolean) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
@@ -168,7 +176,7 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
response.session.driver.id.let { driverId ->
|
response.session.driver.id.let { driverId ->
|
||||||
liveSyncClient.start(driverId)
|
liveSyncClient.start(driverId, response.realtime)
|
||||||
registerPushTokenIfAvailable(driverId)
|
registerPushTokenIfAvailable(driverId)
|
||||||
}
|
}
|
||||||
}.onFailure { throwable ->
|
}.onFailure { throwable ->
|
||||||
@@ -457,8 +465,13 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
val snapshot = _state.value
|
val snapshot = _state.value
|
||||||
when (hint.scope) {
|
when (hint.scope) {
|
||||||
DriverSyncRepository.SCOPE_ROUTES -> {
|
DriverSyncRepository.SCOPE_ROUTES -> {
|
||||||
if (hint.date == null || hint.date == snapshot.selectedDate) {
|
if (shouldShowRouteDayLiveUpdate(snapshot.selectedDate, hint.date)) {
|
||||||
refreshRoutesSilently()
|
refreshRoutesSilently()
|
||||||
|
if (snapshot.screen == DriverScreen.Routes) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(routeDayLiveUpdateMessage = "Spedytor zaktualizował zlecenia dla tego dnia.")
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
DriverSyncWorker.enqueue(getApplication(), hint.date, null)
|
DriverSyncWorker.enqueue(getApplication(), hint.date, null)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,23 @@ class DriverUiRulesTest {
|
|||||||
assertFalse(canCompleteRouteFromDriverApp(route(status = "ZAPLANOWANA"), "2026-06-29", today))
|
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
|
@Test
|
||||||
fun calculatesPhotoGridRowsForTwoColumnInlineGallery() {
|
fun calculatesPhotoGridRowsForTwoColumnInlineGallery() {
|
||||||
assertEquals(0, inlinePhotoGridRows(0))
|
assertEquals(0, inlinePhotoGridRows(0))
|
||||||
|
|||||||
Reference in New Issue
Block a user