Compare commits

..
Author SHA1 Message Date
admin 0564101e9e chore: release android driver 1.0.51 2026-07-10 22:35:04 +02:00
admin 2baa544b32 fix: reconcile leave requests in realtime 2026-07-10 22:32:13 +02:00
admin f9c3a2e305 Sortuj kursy kierowcy według dnia 2026-07-10 21:31:17 +02:00
10 changed files with 222 additions and 51 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 = 103 versionCode = 104
versionName = "1.0.50" versionName = "1.0.51"
setProperty("archivesBaseName", "pl.firmatpp.kierowca") setProperty("archivesBaseName", "pl.firmatpp.kierowca")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@@ -71,8 +71,8 @@ class DriverRepository(
suspend fun createLeaveRequest(dateFrom: String, dateTo: String, type: String, note: String?): DriverLeaveRequestDto = suspend fun createLeaveRequest(dateFrom: String, dateTo: String, type: String, note: String?): DriverLeaveRequestDto =
api.createLeaveRequest(authHeader(requireToken()), CreateLeaveRequestBody(dateFrom, dateTo, type, note)).data api.createLeaveRequest(authHeader(requireToken()), CreateLeaveRequestBody(dateFrom, dateTo, type, note)).data
suspend fun cancelLeaveRequest(id: String, comment: String? = null): DriverLeaveRequestDto = suspend fun cancelLeaveRequest(id: String, expectedVersion: Long, comment: String? = null): DriverLeaveRequestDto =
api.cancelLeaveRequest(authHeader(requireToken()), id, CancelLeaveRequestBody(comment)).data api.cancelLeaveRequest(authHeader(requireToken()), id, CancelLeaveRequestBody(comment, expectedVersion)).data
suspend fun completeRoute(routeId: String) = suspend fun completeRoute(routeId: String) =
api.completeRoute(authHeader(requireToken()), routeId) api.completeRoute(authHeader(requireToken()), routeId)
@@ -193,6 +193,7 @@ data class CreateLeaveRequestBody(
data class CancelLeaveRequestBody( data class CancelLeaveRequestBody(
val comment: String? = null, val comment: String? = null,
val expectedVersion: Long? = null,
) )
data class DriverLeaveRequestDto( data class DriverLeaveRequestDto(
@@ -204,6 +205,10 @@ data class DriverLeaveRequestDto(
val typeLabel: String? = null, val typeLabel: String? = null,
val note: String? = null, val note: String? = null,
val status: String, val status: String,
val version: Long = 1,
val updatedAt: String? = null,
val allowedDecisions: List<String> = emptyList(),
val canCancel: Boolean = false,
val submittedAt: String? = null, val submittedAt: String? = null,
val decidedAt: String? = null, val decidedAt: String? = null,
val decisionComment: String? = null, val decisionComment: String? = null,
@@ -245,6 +250,8 @@ data class DriverRouteDto(
val id: String, val id: String,
val routeDate: String? = null, val routeDate: String? = null,
val unloadingDate: String? = null, val unloadingDate: String? = null,
val ordering: Int? = null,
val dayOrderings: Map<String, Int>? = emptyMap(),
val startsAt: String, val startsAt: String,
val originName: String, val originName: String,
val destinationName: String, val destinationName: String,
@@ -98,6 +98,11 @@ class DriverSyncRepository(
suspend fun saveSyncStates(response: SyncStateResponse?) { suspend fun saveSyncStates(response: SyncStateResponse?) {
response?.scopes.orEmpty().forEach { scope -> response?.scopes.orEmpty().forEach { scope ->
saveSyncScope(scope)
}
}
suspend fun saveSyncScope(scope: SyncScopeDto) {
dao.upsertSyncState( dao.upsertSyncState(
DriverSyncStateEntity( DriverSyncStateEntity(
key = syncStateKey(scope.scope, scope.date, scope.routeId), key = syncStateKey(scope.scope, scope.date, scope.routeId),
@@ -111,7 +116,6 @@ class DriverSyncRepository(
), ),
) )
} }
}
suspend fun clearCache() { suspend fun clearCache() {
dao.clearBootstrap() dao.clearBootstrap()
@@ -166,5 +170,6 @@ class DriverSyncRepository(
const val SCOPE_SETTINGS = "settings" const val SCOPE_SETTINGS = "settings"
const val SCOPE_DISPATCH_SHEET = "dispatch_sheet" const val SCOPE_DISPATCH_SHEET = "dispatch_sheet"
const val SCOPE_LEAVE_REQUESTS = "leave_requests" const val SCOPE_LEAVE_REQUESTS = "leave_requests"
const val SCOPE_LEAVE_CALENDAR = "leave_calendar"
} }
} }
@@ -272,10 +272,11 @@ class DriverLiveSyncClient(
private fun subscribe(socket: WebSocket, socketId: String) { private fun subscribe(socket: WebSocket, socketId: String) {
val id = synchronized(lock) { driverId } ?: return val id = synchronized(lock) { driverId } ?: return
val channel = "private-driver-mobile.$id" val channels = listOf("private-driver-mobile.$id", "private-driver-mobile.0")
scope.launch { scope.launch {
runCatching { runCatching {
channels.forEach { channel ->
val auth = gateway.broadcastAuth(socketId, channel).auth val auth = gateway.broadcastAuth(socketId, channel).auth
if (!isCurrentSocket(socket)) return@launch if (!isCurrentSocket(socket)) return@launch
val payload = mapOf( val payload = mapOf(
@@ -286,6 +287,7 @@ class DriverLiveSyncClient(
), ),
) )
check(socket.send(gson.toJson(payload))) { "WebSocket send returned false" } check(socket.send(gson.toJson(payload))) { "WebSocket send returned false" }
}
}.onFailure { throwable -> }.onFailure { throwable ->
AppDiagnostics.log("realtime_subscription_error: ${throwable.message ?: throwable::class.java.simpleName}") AppDiagnostics.log("realtime_subscription_error: ${throwable.message ?: throwable::class.java.simpleName}")
socket.close(1000, "subscription_failed") socket.close(1000, "subscription_failed")
@@ -52,6 +52,10 @@ class DriverSyncWorker(
syncRepository.saveSyncStates(response) syncRepository.saveSyncStates(response)
refreshed = true refreshed = true
} }
DriverSyncRepository.SCOPE_LEAVE_CALENDAR -> {
syncRepository.saveSyncStates(response)
refreshed = true
}
} }
} }
} }
@@ -66,6 +66,9 @@ fun routeIsActiveToday(
fun routeUnloadingDate(route: DriverRouteDto): String? = route.unloadingDate ?: route.routeDate fun routeUnloadingDate(route: DriverRouteDto): String? = route.unloadingDate ?: route.routeDate
fun routeDayOrdering(route: DriverRouteDto, date: String): Int =
route.dayOrderings?.get(date) ?: route.ordering ?: Int.MAX_VALUE
fun routeScheduleDateLabel(date: String?): String = fun routeScheduleDateLabel(date: String?): String =
date date
?.let { runCatching { LocalDate.parse(it).format(shortDateFormatter) }.getOrNull() } ?.let { runCatching { LocalDate.parse(it).format(shortDateFormatter) }.getOrNull() }
@@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import java.time.LocalDate import java.time.LocalDate
import java.time.Instant import java.time.Instant
import pl.firmatpp.kierowca.data.AppPreferencesStore import pl.firmatpp.kierowca.data.AppPreferencesStore
@@ -96,7 +97,10 @@ data class DriverUiState(
val displayRoutes: List<DriverRouteDto> val displayRoutes: List<DriverRouteDto>
get() = projectDriverRoutes(routes, projectionActions) get() = projectDriverRoutes(routes, projectionActions)
.sortedBy { route -> if (route.status == "ZAKOŃCZONA") 1 else 0 } .sortedWith(
compareBy<DriverRouteDto> { route -> if (route.status == "ZAKOŃCZONA") 1 else 0 }
.thenBy { route -> routeDayOrdering(route, selectedDate) },
)
val selectedRouteProjection: ProjectedDriverRoute? val selectedRouteProjection: ProjectedDriverRoute?
get() = selectedRoute?.let { projectDriverRoute(it, projectionActions) } get() = selectedRoute?.let { projectDriverRoute(it, projectionActions) }
@@ -118,12 +122,17 @@ internal fun DriverUiState.withLoadedLeaveRequests(
requests: List<DriverLeaveRequestDto>, requests: List<DriverLeaveRequestDto>,
navigateToList: Boolean, navigateToList: Boolean,
): DriverUiState { ): DriverUiState {
val currentById = leaveRequests.associateBy { it.id }
val merged = requests.map { incoming ->
currentById[incoming.id]?.takeIf { current -> current.version > incoming.version } ?: incoming
}.toMutableList()
leaveRequests.filter { current -> requests.none { it.id == current.id } }.forEach(merged::add)
val refreshedSelectedRequest = selectedLeaveRequest?.let { selected -> val refreshedSelectedRequest = selectedLeaveRequest?.let { selected ->
requests.firstOrNull { request -> request.id == selected.id } ?: selected merged.firstOrNull { request -> request.id == selected.id }?.takeIf { it.version >= selected.version } ?: selected
} }
return copy( return copy(
screen = if (navigateToList) DriverScreen.LeaveRequests else screen, screen = if (navigateToList) DriverScreen.LeaveRequests else screen,
leaveRequests = requests, leaveRequests = merged,
selectedLeaveRequest = refreshedSelectedRequest, selectedLeaveRequest = refreshedSelectedRequest,
error = null, error = null,
) )
@@ -148,6 +157,9 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
private var routeActionsJob: Job? = null private var routeActionsJob: Job? = null
private var dispatchSheetUploadsJob: Job? = null private var dispatchSheetUploadsJob: Job? = null
private var pushTokenRegisteredForDriverId: String? = null private var pushTokenRegisteredForDriverId: String? = null
private val leaveMutationMutex = Mutex()
private var pendingLeaveHint: DriverSyncHint? = null
private var leaveCalendarRequestGeneration: Long = 0
val state: StateFlow<DriverUiState> = _state val state: StateFlow<DriverUiState> = _state
init { init {
@@ -416,24 +428,22 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
fun openLeaveRequests() { fun openLeaveRequests() {
if (!DriverLeaveRequestUiRules.isFeatureVisible(_state.value.leaveRequestsConfig)) return if (!DriverLeaveRequestUiRules.isFeatureVisible(_state.value.leaveRequestsConfig)) return
loadLeaveRequests(showLoading = true, navigateToList = true) viewModelScope.launch { loadLeaveRequests(showLoading = true, navigateToList = true) }
} }
fun refreshLeaveRequests() { fun refreshLeaveRequests() {
if (!DriverLeaveRequestUiRules.isFeatureVisible(_state.value.leaveRequestsConfig)) return if (!DriverLeaveRequestUiRules.isFeatureVisible(_state.value.leaveRequestsConfig)) return
loadLeaveRequests(showLoading = false, navigateToList = false) viewModelScope.launch { loadLeaveRequests(showLoading = false, navigateToList = false) }
} }
private fun loadLeaveRequests(showLoading: Boolean, navigateToList: Boolean) { private suspend fun loadLeaveRequests(showLoading: Boolean, navigateToList: Boolean): Boolean {
viewModelScope.launch {
_state.update { it.copy(loading = showLoading, refreshing = !showLoading, error = null, feedback = null) } _state.update { it.copy(loading = showLoading, refreshing = !showLoading, error = null, feedback = null) }
runCatching { repository.leaveRequests() } val result = runCatching { repository.leaveRequests() }
.onSuccess { requests -> result.onSuccess { requests ->
_state.update { it.withLoadedLeaveRequests(requests, navigateToList) } _state.update { it.withLoadedLeaveRequests(requests, navigateToList) }
} }.onFailure { throwable -> _state.update { it.withApiError(throwable) } }
.onFailure { throwable -> _state.update { it.withApiError(throwable) } }
_state.update { it.copy(loading = false, refreshing = false) } _state.update { it.copy(loading = false, refreshing = false) }
} return result.isSuccess
} }
fun openLeaveRequest(id: String) { fun openLeaveRequest(id: String) {
@@ -547,6 +557,7 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
runCatching { LocalDate.parse(snapshot.leaveCalendarLoadedUntil).plusDays(1) }.getOrDefault(LocalDate.now()) runCatching { LocalDate.parse(snapshot.leaveCalendarLoadedUntil).plusDays(1) }.getOrDefault(LocalDate.now())
} }
val to = from.plusMonths(6).minusDays(1) val to = from.plusMonths(6).minusDays(1)
val generation = ++leaveCalendarRequestGeneration
viewModelScope.launch { viewModelScope.launch {
_state.update { _state.update {
@@ -559,6 +570,7 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
} }
runCatching { repository.leaveCalendar(from.toString(), to.toString()) } runCatching { repository.leaveCalendar(from.toString(), to.toString()) }
.onSuccess { entries -> .onSuccess { entries ->
if (generation != leaveCalendarRequestGeneration) return@onSuccess
_state.update { _state.update {
val merged = (it.leaveCalendarEntries + entries) val merged = (it.leaveCalendarEntries + entries)
.distinctBy { entry -> entry.id } .distinctBy { entry -> entry.id }
@@ -572,6 +584,7 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
} }
} }
.onFailure { throwable -> .onFailure { throwable ->
if (generation != leaveCalendarRequestGeneration) return@onFailure
_state.update { it.withApiError(throwable).copy(leaveCalendarLoading = false) } _state.update { it.withApiError(throwable).copy(leaveCalendarLoading = false) }
} }
} }
@@ -580,13 +593,16 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
fun submitLeaveRequest() { fun submitLeaveRequest() {
val snapshot = _state.value val snapshot = _state.value
if (!DriverLeaveRequestUiRules.isFeatureVisible(snapshot.leaveRequestsConfig)) return if (!DriverLeaveRequestUiRules.isFeatureVisible(snapshot.leaveRequestsConfig)) return
if (!leaveMutationMutex.tryLock()) return
if (snapshot.leaveRequestDateFrom < LocalDate.now().toString()) { if (snapshot.leaveRequestDateFrom < LocalDate.now().toString()) {
_state.update { it.copy(error = "Data od nie może być z przeszłości.") } _state.update { it.copy(error = "Data od nie może być z przeszłości.") }
leaveMutationMutex.unlock()
return return
} }
viewModelScope.launch { viewModelScope.launch {
_state.update { it.copy(loading = true, error = null, feedback = null) } _state.update { it.copy(loading = true, error = null, feedback = null) }
try {
runCatching { runCatching {
repository.createLeaveRequest( repository.createLeaveRequest(
dateFrom = snapshot.leaveRequestDateFrom, dateFrom = snapshot.leaveRequestDateFrom,
@@ -609,16 +625,22 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
_state.update { it.withApiError(throwable) } _state.update { it.withApiError(throwable) }
} }
_state.update { it.copy(loading = false) } _state.update { it.copy(loading = false) }
} finally {
leaveMutationMutex.unlock()
consumePendingLeaveHint()
}
} }
} }
fun cancelSelectedLeaveRequest() { fun cancelSelectedLeaveRequest() {
val request = _state.value.selectedLeaveRequest ?: return val request = _state.value.selectedLeaveRequest ?: return
if (!DriverLeaveRequestUiRules.canCancel(request.status, request.dateFrom.orEmpty())) return if (!DriverLeaveRequestUiRules.canCancel(request.status, request.dateFrom.orEmpty())) return
if (!leaveMutationMutex.tryLock()) return
viewModelScope.launch { viewModelScope.launch {
_state.update { it.copy(loading = true, error = null, feedback = null) } _state.update { it.copy(loading = true, error = null, feedback = null) }
runCatching { repository.cancelLeaveRequest(request.id) } try {
runCatching { repository.cancelLeaveRequest(request.id, request.version) }
.onSuccess { updated -> .onSuccess { updated ->
_state.update { _state.update {
it.copy( it.copy(
@@ -629,8 +651,26 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
) )
} }
} }
.onFailure { throwable -> _state.update { it.withApiError(throwable) } } .onFailure { throwable ->
val apiError = ApiErrorMapper.map(throwable)
if (apiError.kind == ApiErrorKind.Conflict && apiError.code == "leave_request_stale") {
runCatching { repository.leaveRequest(request.id) }
.onSuccess { current ->
_state.update {
it.withLoadedLeaveRequests(listOf(current), navigateToList = false)
.copy(error = "Wniosek został wcześniej zmieniony. Pokazujemy aktualny stan.")
}
}
.onFailure { refreshError -> _state.update { it.withApiError(refreshError) } }
} else {
_state.update { it.withApiError(throwable) }
}
}
_state.update { it.copy(loading = false) } _state.update { it.copy(loading = false) }
} finally {
leaveMutationMutex.unlock()
consumePendingLeaveHint()
}
} }
} }
@@ -1058,12 +1098,23 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
val snapshot = _state.value val snapshot = _state.value
val routeId = snapshot.selectedRoute?.id val routeId = snapshot.selectedRoute?.id
val remote = runCatching { syncRepository.fetchSyncState(snapshot.selectedDate, routeId) }.getOrNull() ?: return val remote = runCatching { syncRepository.fetchSyncState(snapshot.selectedDate, routeId) }.getOrNull() ?: return
val needsRefresh = remote.scopes.any { syncRepository.shouldRefresh(it) } val staleScopes = remote.scopes.filter { syncRepository.shouldRefresh(it) }
if (needsRefresh) { if (staleScopes.isEmpty()) {
refreshCurrentScopeFromSyncState()
} else {
syncRepository.saveSyncStates(remote) syncRepository.saveSyncStates(remote)
return
}
staleScopes.forEach { scope ->
handleSyncHint(
DriverSyncHint(
scope = scope.scope,
date = scope.date,
routeId = scope.routeId,
version = scope.version,
checksum = scope.checksum,
),
)
} }
} }
@@ -1094,10 +1145,55 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
DriverSyncRepository.SCOPE_SETTINGS -> refreshRoutesSilently() DriverSyncRepository.SCOPE_SETTINGS -> refreshRoutesSilently()
DriverSyncRepository.SCOPE_DISPATCH_SHEET -> refreshRoutesSilently() DriverSyncRepository.SCOPE_DISPATCH_SHEET -> refreshRoutesSilently()
DriverSyncRepository.SCOPE_LEAVE_REQUESTS -> { DriverSyncRepository.SCOPE_LEAVE_REQUESTS -> {
if (leaveMutationMutex.isLocked) {
val pending = pendingLeaveHint
if (pending == null || hint.version > pending.version) pendingLeaveHint = hint
return
}
if (snapshot.screen == DriverScreen.LeaveRequests || snapshot.screen == DriverScreen.LeaveRequestDetail) { if (snapshot.screen == DriverScreen.LeaveRequests || snapshot.screen == DriverScreen.LeaveRequestDetail) {
refreshLeaveRequests() if (loadLeaveRequests(showLoading = false, navigateToList = false)) {
syncRepository.saveSyncScope(hint.asScope())
}
} else {
syncRepository.saveSyncScope(hint.asScope())
} }
} }
DriverSyncRepository.SCOPE_LEAVE_CALENDAR -> {
if (snapshot.screen == DriverScreen.LeaveCalendar) {
refreshLoadedLeaveCalendar(hint)
} else {
syncRepository.saveSyncScope(hint.asScope())
}
}
}
}
private suspend fun consumePendingLeaveHint() {
val hint = pendingLeaveHint ?: return
pendingLeaveHint = null
if (loadLeaveRequests(showLoading = false, navigateToList = false)) {
syncRepository.saveSyncScope(hint.asScope())
}
}
private suspend fun refreshLoadedLeaveCalendar(hint: DriverSyncHint) {
val generation = ++leaveCalendarRequestGeneration
val snapshot = _state.value
val from = LocalDate.now()
val loadedUntil = snapshot.leaveCalendarLoadedUntil
?.let { runCatching { LocalDate.parse(it) }.getOrNull() }
?.takeUnless { it.isBefore(from) }
?: from.plusMonths(6).minusDays(1)
runCatching { repository.leaveCalendar(from.toString(), loadedUntil.toString()) }
.onSuccess { entries ->
if (generation != leaveCalendarRequestGeneration) return@onSuccess
_state.update { it.copy(leaveCalendarEntries = entries, leaveCalendarLoadedUntil = loadedUntil.toString(), leaveCalendarLoading = false) }
syncRepository.saveSyncScope(hint.asScope())
}
.onFailure { throwable ->
if (generation != leaveCalendarRequestGeneration) return@onFailure
_state.update { it.withApiError(throwable).copy(leaveCalendarLoading = false) }
} }
} }
@@ -107,6 +107,21 @@ class DriverLiveSyncClientTest {
assertTrue(factory.sockets.single().sent.any { it.contains("pusher:pong") }) assertTrue(factory.sockets.single().sent.any { it.contains("pusher:pong") })
} }
@Test
fun subscribesToOwnUpdatesAndSharedLeaveCalendarUpdates() = runTest {
val factory = FakeWebSocketFactory()
val client = liveClient(factory = factory, scope = this)
client.start("driver-1", config)
factory.sockets.single().message(connectionEstablished("socket-1"))
runCurrent()
val sent = factory.sockets.single().sent
assertTrue(sent.any { it.contains("private-driver-mobile.driver-1") })
assertTrue(sent.any { it.contains("private-driver-mobile.0") })
client.stop()
}
@Test @Test
fun closesAndReconnectsWhenConnectedSocketStopsReceivingMessages() = runTest { fun closesAndReconnectsWhenConnectedSocketStopsReceivingMessages() = runTest {
val factory = FakeWebSocketFactory() val factory = FakeWebSocketFactory()
@@ -55,6 +55,23 @@ class DriverUiStateTest {
assertEquals(1, refreshed.leaveRequests.size) assertEquals(1, refreshed.leaveRequests.size)
} }
@Test
fun staleLeaveRequestRefreshDoesNotOverwriteNewerMutationResult() {
val state = DriverUiState(
screen = DriverScreen.LeaveRequestDetail,
leaveRequests = listOf(leaveRequest(id = "leave-1", status = "cancel_requested", version = 3)),
selectedLeaveRequest = leaveRequest(id = "leave-1", status = "cancel_requested", version = 3),
)
val refreshed = state.withLoadedLeaveRequests(
requests = listOf(leaveRequest(id = "leave-1", status = "approved", version = 2)),
navigateToList = false,
)
assertEquals("cancel_requested", refreshed.leaveRequests.single().status)
assertEquals(3L, refreshed.selectedLeaveRequest?.version)
}
@Test @Test
fun displayRoutesAndSelectedRouteUseLocalRouteActionsProjection() { fun displayRoutesAndSelectedRouteUseLocalRouteActionsProjection() {
val route = route(status = "ZAPLANOWANA") val route = route(status = "ZAPLANOWANA")
@@ -83,19 +100,41 @@ class DriverUiStateTest {
assertEquals(listOf("planned", "active", "completed"), state.displayRoutes.map { it.id }) assertEquals(listOf("planned", "active", "completed"), state.displayRoutes.map { it.id })
} }
private fun leaveRequest(id: String, status: String): DriverLeaveRequestDto = @Test
fun displayRoutesUsesTheSelectedDayOrderingForMultiDayRoutes() {
val state = DriverUiState(
selectedDate = "2026-07-02",
routes = listOf(
route(id = "span", status = "ZAPLANOWANA", ordering = 0, dayOrderings = mapOf("2026-07-02" to 2)),
route(id = "above-first", status = "ZAPLANOWANA", ordering = 0, dayOrderings = mapOf("2026-07-02" to 0)),
route(id = "above-second", status = "ZAPLANOWANA", ordering = 1, dayOrderings = mapOf("2026-07-02" to 1)),
),
)
assertEquals(listOf("above-first", "above-second", "span"), state.displayRoutes.map { it.id })
}
private fun leaveRequest(id: String, status: String, version: Long = 1): DriverLeaveRequestDto =
DriverLeaveRequestDto( DriverLeaveRequestDto(
id = id, id = id,
dateFrom = "2026-07-10", dateFrom = "2026-07-10",
dateTo = "2026-07-12", dateTo = "2026-07-12",
type = "URLOP", type = "URLOP",
status = status, status = status,
version = version,
) )
private fun route(id: String = "1", status: String): DriverRouteDto = private fun route(
id: String = "1",
status: String,
ordering: Int? = null,
dayOrderings: Map<String, Int> = emptyMap(),
): DriverRouteDto =
DriverRouteDto( DriverRouteDto(
id = id, id = id,
routeDate = "2026-06-30", routeDate = "2026-06-30",
ordering = ordering,
dayOrderings = dayOrderings,
startsAt = "", startsAt = "",
originName = "Baza", originName = "Baza",
destinationName = "Instalacja", destinationName = "Instalacja",