Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32d45f6430 | ||
|
|
4a06e9da8b | ||
|
|
d6b36a1d3a | ||
|
|
e811b4bc43 | ||
|
|
5d3b355163 | ||
|
|
81f3b05653 |
@@ -34,8 +34,8 @@ android {
|
||||
applicationId = "pl.firmatpp.kierowca"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 47
|
||||
versionName = "1.0.44"
|
||||
versionCode = 51
|
||||
versionName = "1.0.45"
|
||||
setProperty("archivesBaseName", "pl.firmatpp.kierowca")
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
|
||||
@@ -14,7 +14,10 @@ class PhotoUploadWorker(
|
||||
appContext: Context,
|
||||
params: WorkerParameters,
|
||||
) : CoroutineWorker(appContext, params) {
|
||||
private val dao = DriverDatabase.get(appContext).photoUploadDao()
|
||||
private val database = DriverDatabase.get(appContext)
|
||||
private val dao = database.photoUploadDao()
|
||||
private val routeActionDao = database.routeActionDao()
|
||||
private val routeActionOutbox = RouteActionOutbox(appContext)
|
||||
private val repository = DriverRepository(appContext)
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
@@ -62,6 +65,7 @@ class PhotoUploadWorker(
|
||||
}
|
||||
|
||||
dao.markConfirmed(clientRequestId, receipt.serverPhotoId)
|
||||
enqueueRouteActionsWaitingForPhoto(clientRequestId)
|
||||
Result.success()
|
||||
}.getOrElse { throwable ->
|
||||
val error = ApiErrorMapper.map(throwable)
|
||||
@@ -90,6 +94,12 @@ class PhotoUploadWorker(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun enqueueRouteActionsWaitingForPhoto(clientRequestId: String) {
|
||||
routeActionDao.waitingForPhotosActions()
|
||||
.filter { routeActionReferencesPhotoClientRequest(it, clientRequestId) }
|
||||
.forEach { routeActionOutbox.enqueueWorker(it.clientActionId) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KEY_CLIENT_REQUEST_ID = "clientRequestId"
|
||||
const val KEY_PROGRESS = "progress"
|
||||
|
||||
@@ -14,11 +14,14 @@ interface RouteActionDao {
|
||||
@Query("SELECT * FROM route_actions WHERE clientActionId = :clientActionId LIMIT 1")
|
||||
suspend fun find(clientActionId: String): RouteActionEntity?
|
||||
|
||||
@Query("SELECT * FROM route_actions WHERE status = 'WAITING_FOR_PHOTOS'")
|
||||
suspend fun waitingForPhotosActions(): List<RouteActionEntity>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM route_actions
|
||||
WHERE routeId = :routeId
|
||||
AND status IN ('PENDING', 'WAITING_FOR_PHOTOS', 'SYNCING', 'FAILED_RETRYABLE', 'FAILED_CONFLICT', 'FAILED_PERMANENT')
|
||||
AND status IN ('PENDING', 'WAITING_FOR_PHOTOS', 'SYNCING', 'CONFIRMED', 'FAILED_RETRYABLE', 'FAILED_CONFLICT', 'FAILED_PERMANENT')
|
||||
ORDER BY createdAtEpochMillis DESC
|
||||
""",
|
||||
)
|
||||
@@ -28,6 +31,14 @@ interface RouteActionDao {
|
||||
"""
|
||||
SELECT * FROM route_actions
|
||||
WHERE status IN ('PENDING', 'WAITING_FOR_PHOTOS', 'SYNCING', 'FAILED_RETRYABLE', 'FAILED_CONFLICT', 'FAILED_PERMANENT')
|
||||
OR clientActionId IN (
|
||||
SELECT confirmed.clientActionId
|
||||
FROM route_actions AS confirmed
|
||||
WHERE confirmed.status = 'CONFIRMED'
|
||||
AND confirmed.routeId = route_actions.routeId
|
||||
ORDER BY confirmed.createdAtEpochMillis DESC
|
||||
LIMIT 2
|
||||
)
|
||||
ORDER BY createdAtEpochMillis DESC
|
||||
""",
|
||||
)
|
||||
|
||||
@@ -53,7 +53,7 @@ class RouteActionOutbox(
|
||||
return entity
|
||||
}
|
||||
|
||||
private fun enqueueWorker(clientActionId: String) {
|
||||
fun enqueueWorker(clientActionId: String) {
|
||||
val request = OneTimeWorkRequestBuilder<RouteActionWorker>()
|
||||
.setInputData(workDataOf(RouteActionWorker.KEY_CLIENT_ACTION_ID to clientActionId))
|
||||
.setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package pl.firmatpp.kierowca.data.upload
|
||||
|
||||
import com.google.gson.Gson
|
||||
|
||||
private val routeActionPhotoDependencyGson = Gson()
|
||||
|
||||
fun routeActionReferencesPhotoClientRequest(action: RouteActionEntity, clientRequestId: String): Boolean {
|
||||
val needle = clientRequestId.trim()
|
||||
if (needle.isBlank()) return false
|
||||
|
||||
return runCatching {
|
||||
routeActionPhotoDependencyGson
|
||||
.fromJson(action.photoClientRequestIdsJson, Array<String>::class.java)
|
||||
?.any { it.trim() == needle }
|
||||
?: false
|
||||
}.getOrDefault(false)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package pl.firmatpp.kierowca.ui
|
||||
|
||||
import pl.firmatpp.kierowca.data.model.DriverRouteDto
|
||||
import pl.firmatpp.kierowca.data.model.RoutePhotoDto
|
||||
import pl.firmatpp.kierowca.data.upload.RouteActionEntity
|
||||
import pl.firmatpp.kierowca.data.upload.RouteActionStatus
|
||||
import pl.firmatpp.kierowca.data.upload.RouteActionType
|
||||
@@ -21,14 +22,18 @@ internal val routeActionVisibleStatuses = setOf(
|
||||
RouteActionStatus.FailedPermanent,
|
||||
)
|
||||
|
||||
private val routeActionProjectionStatuses = routeActionVisibleStatuses + RouteActionStatus.Confirmed
|
||||
|
||||
fun projectDriverRoute(route: DriverRouteDto, actions: List<RouteActionEntity>): ProjectedDriverRoute {
|
||||
val visible = actions
|
||||
.filter { it.routeId == route.id && it.status in routeActionVisibleStatuses }
|
||||
val projectable = actions
|
||||
.filter { it.routeId == route.id && it.status in routeActionProjectionStatuses }
|
||||
var projectedRoute = route
|
||||
var loadingWeightPending = false
|
||||
var unloadingWeightPending = false
|
||||
|
||||
visible
|
||||
projectable
|
||||
.sortedWith(compareBy<RouteActionEntity> { it.createdAtEpochMillis }.thenBy { it.clientActionId })
|
||||
.forEach { action ->
|
||||
when (action.action) {
|
||||
@@ -37,8 +42,9 @@ fun projectDriverRoute(route: DriverRouteDto, actions: List<RouteActionEntity>):
|
||||
status = "W TRAKCIE",
|
||||
loadingWeight = action.weight,
|
||||
trackingStatus = "active",
|
||||
photos = projectedRoute.safePhotos(),
|
||||
)
|
||||
loadingWeightPending = true
|
||||
loadingWeightPending = action.status != RouteActionStatus.Confirmed
|
||||
}
|
||||
RouteActionType.Finish -> {
|
||||
projectedRoute = projectedRoute.copy(
|
||||
@@ -46,8 +52,9 @@ fun projectDriverRoute(route: DriverRouteDto, actions: List<RouteActionEntity>):
|
||||
unloadingWeight = action.weight,
|
||||
trackingStatus = "finished",
|
||||
completedAt = action.occurredAt,
|
||||
photos = projectedRoute.safePhotos(),
|
||||
)
|
||||
unloadingWeightPending = true
|
||||
unloadingWeightPending = action.status != RouteActionStatus.Confirmed
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,3 +74,5 @@ fun projectDriverRoutes(routes: List<DriverRouteDto>, actions: List<RouteActionE
|
||||
|
||||
fun routeActionNeedsAttention(status: String): Boolean =
|
||||
status == RouteActionStatus.FailedConflict || status == RouteActionStatus.FailedPermanent
|
||||
|
||||
internal fun DriverRouteDto.safePhotos(): List<RoutePhotoDto> = photos.orEmpty()
|
||||
|
||||
@@ -114,8 +114,10 @@ fun routeSyncCallout(actions: List<RouteActionEntity>): RouteSyncCalloutUi? {
|
||||
} else {
|
||||
when (unresolved.status) {
|
||||
RouteActionStatus.WaitingForPhotos -> "Czekam na wysłanie zdjęć etapu, potem wyślę zmianę statusu."
|
||||
RouteActionStatus.Pending,
|
||||
RouteActionStatus.Syncing -> "Trwa wysyłanie zmiany statusu kursu."
|
||||
RouteActionStatus.FailedRetryable -> "Nie udało się wysłać. Aplikacja spróbuje ponownie."
|
||||
else -> "Zmiana zapisana w telefonie. Wyślemy ją po odzyskaniu internetu."
|
||||
else -> "Zmiana zapisana w telefonie. Aplikacja wyśle ją automatycznie."
|
||||
}
|
||||
},
|
||||
isConflict = isConflict,
|
||||
@@ -151,9 +153,9 @@ fun routeFlowSteps(route: DriverRouteDto, actions: List<RouteActionEntity>): Lis
|
||||
)
|
||||
}
|
||||
|
||||
fun routePhotosForStage(photos: List<RoutePhotoDto>, stage: String): List<RoutePhotoDto> {
|
||||
fun routePhotosForStage(photos: List<RoutePhotoDto>?, stage: String): List<RoutePhotoDto> {
|
||||
val normalized = normalizedRoutePhotoStage(stage)
|
||||
return photos.filter { normalizedRoutePhotoStage(it.stage) == normalized }
|
||||
return photos.orEmpty().filter { normalizedRoutePhotoStage(it.stage) == normalized }
|
||||
}
|
||||
|
||||
fun normalizedRoutePhotoStage(stage: String?): String =
|
||||
|
||||
@@ -617,6 +617,7 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
|
||||
status = "W TRAKCIE",
|
||||
loadingWeight = weight,
|
||||
trackingStatus = "active",
|
||||
photos = route.safePhotos(),
|
||||
)
|
||||
}
|
||||
"unloading" -> {
|
||||
@@ -626,6 +627,7 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
|
||||
unloadingWeight = weight,
|
||||
trackingStatus = "finished",
|
||||
completedAt = Instant.now().toString(),
|
||||
photos = route.safePhotos(),
|
||||
)
|
||||
}
|
||||
else -> route
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package pl.firmatpp.kierowca.data.upload
|
||||
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class RouteActionPhotoDependenciesTest {
|
||||
@Test
|
||||
fun detectsRouteActionWaitingForConfirmedPhotoClientRequest() {
|
||||
val action = action("""["photo-a","photo-b"]""")
|
||||
|
||||
assertTrue(routeActionReferencesPhotoClientRequest(action, "photo-b"))
|
||||
assertFalse(routeActionReferencesPhotoClientRequest(action, "photo-c"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun malformedPhotoRequestJsonDoesNotMatch() {
|
||||
assertFalse(routeActionReferencesPhotoClientRequest(action("not-json"), "photo-a"))
|
||||
}
|
||||
|
||||
private fun action(photoClientRequestIdsJson: String): RouteActionEntity =
|
||||
RouteActionEntity(
|
||||
clientActionId = "action-1",
|
||||
routeId = "route-1",
|
||||
action = RouteActionType.Start,
|
||||
weight = 12.5,
|
||||
occurredAt = "2026-06-30T07:40:00Z",
|
||||
photoClientRequestIdsJson = photoClientRequestIdsJson,
|
||||
status = RouteActionStatus.WaitingForPhotos,
|
||||
)
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
package pl.firmatpp.kierowca.ui
|
||||
|
||||
import com.google.gson.Gson
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import pl.firmatpp.kierowca.data.model.DriverRouteDto
|
||||
import pl.firmatpp.kierowca.data.model.RoutePhotoDto
|
||||
import pl.firmatpp.kierowca.data.upload.RouteActionEntity
|
||||
import pl.firmatpp.kierowca.data.upload.RouteActionStatus
|
||||
import pl.firmatpp.kierowca.data.upload.RouteActionType
|
||||
@@ -57,6 +59,54 @@ class DriverRouteProjectionTest {
|
||||
assertEquals("finished", projection.route.trackingStatus)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun confirmedFinishStillProjectsPlannedRouteAsFinishedWithoutSyncCallout() {
|
||||
val projection = projectDriverRoute(
|
||||
route = route(status = "ZAPLANOWANA"),
|
||||
actions = listOf(
|
||||
action(RouteActionType.Finish, RouteActionStatus.Confirmed, weight = 11.8, createdAt = 2000),
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals("ZAKOŃCZONA", projection.route.status)
|
||||
assertEquals(11.8, projection.route.unloadingWeight)
|
||||
assertEquals("finished", projection.route.trackingStatus)
|
||||
assertFalse(projection.unloadingWeightPending)
|
||||
assertEquals(emptyList<RouteActionEntity>(), projection.visibleActions)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun routeProjectionNormalizesNullPhotosFromCachedJsonBeforeCopy() {
|
||||
val route = Gson().fromJson(
|
||||
"""
|
||||
{
|
||||
"id": "1",
|
||||
"startsAt": "",
|
||||
"originName": "Baza",
|
||||
"destinationName": "Instalacja",
|
||||
"contractorName": "TPP",
|
||||
"contractName": "Kontrakt",
|
||||
"contractCode": "TPP-1",
|
||||
"relationLabel": "Baza -> Instalacja",
|
||||
"status": "ZAPLANOWANA",
|
||||
"distanceKm": 1.0,
|
||||
"notes": null,
|
||||
"truck": null,
|
||||
"photos": null
|
||||
}
|
||||
""".trimIndent(),
|
||||
DriverRouteDto::class.java,
|
||||
)
|
||||
|
||||
val projection = projectDriverRoute(
|
||||
route = route,
|
||||
actions = listOf(action(RouteActionType.Start, RouteActionStatus.Confirmed, weight = 12.5)),
|
||||
)
|
||||
|
||||
assertEquals("W TRAKCIE", projection.route.status)
|
||||
assertEquals(emptyList<RoutePhotoDto>(), projection.route.photos)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun conflictKeepsLocalRouteValuesAndMarksStageAsAttention() {
|
||||
val projection = projectDriverRoute(
|
||||
|
||||
Reference in New Issue
Block a user