fix: reconcile leave requests in realtime
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -98,21 +98,25 @@ class DriverSyncRepository(
|
|||||||
|
|
||||||
suspend fun saveSyncStates(response: SyncStateResponse?) {
|
suspend fun saveSyncStates(response: SyncStateResponse?) {
|
||||||
response?.scopes.orEmpty().forEach { scope ->
|
response?.scopes.orEmpty().forEach { scope ->
|
||||||
dao.upsertSyncState(
|
saveSyncScope(scope)
|
||||||
DriverSyncStateEntity(
|
|
||||||
key = syncStateKey(scope.scope, scope.date, scope.routeId),
|
|
||||||
scope = scope.scope,
|
|
||||||
date = scope.date,
|
|
||||||
routeId = scope.routeId,
|
|
||||||
checksum = scope.checksum,
|
|
||||||
version = scope.version,
|
|
||||||
computedAt = scope.computedAt,
|
|
||||||
syncedAtEpochMillis = System.currentTimeMillis(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun saveSyncScope(scope: SyncScopeDto) {
|
||||||
|
dao.upsertSyncState(
|
||||||
|
DriverSyncStateEntity(
|
||||||
|
key = syncStateKey(scope.scope, scope.date, scope.routeId),
|
||||||
|
scope = scope.scope,
|
||||||
|
date = scope.date,
|
||||||
|
routeId = scope.routeId,
|
||||||
|
checksum = scope.checksum,
|
||||||
|
version = scope.version,
|
||||||
|
computedAt = scope.computedAt,
|
||||||
|
syncedAtEpochMillis = System.currentTimeMillis(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun clearCache() {
|
suspend fun clearCache() {
|
||||||
dao.clearBootstrap()
|
dao.clearBootstrap()
|
||||||
dao.clearRoutes()
|
dao.clearRoutes()
|
||||||
@@ -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,20 +272,22 @@ 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 {
|
||||||
val auth = gateway.broadcastAuth(socketId, channel).auth
|
channels.forEach { channel ->
|
||||||
if (!isCurrentSocket(socket)) return@launch
|
val auth = gateway.broadcastAuth(socketId, channel).auth
|
||||||
val payload = mapOf(
|
if (!isCurrentSocket(socket)) return@launch
|
||||||
"event" to "pusher:subscribe",
|
val payload = mapOf(
|
||||||
"data" to mapOf(
|
"event" to "pusher:subscribe",
|
||||||
"channel" to channel,
|
"data" to mapOf(
|
||||||
"auth" to auth,
|
"channel" to channel,
|
||||||
),
|
"auth" to auth,
|
||||||
)
|
),
|
||||||
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -121,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,
|
||||||
)
|
)
|
||||||
@@ -151,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 {
|
||||||
@@ -419,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) }
|
val result = runCatching { repository.leaveRequests() }
|
||||||
runCatching { repository.leaveRequests() }
|
result.onSuccess { requests ->
|
||||||
.onSuccess { requests ->
|
_state.update { it.withLoadedLeaveRequests(requests, navigateToList) }
|
||||||
_state.update { it.withLoadedLeaveRequests(requests, navigateToList) }
|
}.onFailure { throwable -> _state.update { it.withApiError(throwable) } }
|
||||||
}
|
_state.update { it.copy(loading = false, refreshing = false) }
|
||||||
.onFailure { throwable -> _state.update { it.withApiError(throwable) } }
|
return result.isSuccess
|
||||||
_state.update { it.copy(loading = false, refreshing = false) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun openLeaveRequest(id: String) {
|
fun openLeaveRequest(id: String) {
|
||||||
@@ -550,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 {
|
||||||
@@ -562,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 }
|
||||||
@@ -575,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) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -583,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,
|
||||||
@@ -612,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(
|
||||||
@@ -632,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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1061,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,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1097,13 +1145,58 @@ 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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun registerPushTokenIfAvailable(driverId: String) {
|
private fun registerPushTokenIfAvailable(driverId: String) {
|
||||||
if (pushTokenRegisteredForDriverId == driverId) return
|
if (pushTokenRegisteredForDriverId == driverId) return
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
@@ -97,13 +114,14 @@ class DriverUiStateTest {
|
|||||||
assertEquals(listOf("above-first", "above-second", "span"), state.displayRoutes.map { it.id })
|
assertEquals(listOf("above-first", "above-second", "span"), state.displayRoutes.map { it.id })
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun leaveRequest(id: String, status: String): DriverLeaveRequestDto =
|
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(
|
private fun route(
|
||||||
|
|||||||
Reference in New Issue
Block a user