From 06557e77b8e31bf7862cb40d11bef5009705ca94 Mon Sep 17 00:00:00 2001 From: admin Date: Wed, 1 Jul 2026 02:43:52 +0200 Subject: [PATCH] =?UTF-8?q?Dodaj=20usuwanie=20=C5=9Bwie=C5=BCo=20zapisanyc?= =?UTF-8?q?h=20zdj=C4=99=C4=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 4 +- .../kierowca/data/upload/PhotoUploadOutbox.kt | 7 ++ .../java/pl/firmatpp/kierowca/ui/DriverApp.kt | 71 ++++++++++++++++--- .../pl/firmatpp/kierowca/ui/DriverUiRules.kt | 3 + .../firmatpp/kierowca/ui/DriverViewModel.kt | 25 +++++++ .../firmatpp/kierowca/ui/DriverUiRulesTest.kt | 8 +++ 6 files changed, 107 insertions(+), 11 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cddd2ec..00c523d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -13,8 +13,8 @@ android { applicationId = "pl.firmatpp.kierowca" minSdk = 26 targetSdk = 35 - versionCode = 16 - versionName = "1.0.15" + versionCode = 17 + versionName = "1.0.16" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" buildConfigField("String", "API_BASE_URL", "\"https://api-intranet.firmatpp.pl/api/\"") diff --git a/app/src/main/java/pl/firmatpp/kierowca/data/upload/PhotoUploadOutbox.kt b/app/src/main/java/pl/firmatpp/kierowca/data/upload/PhotoUploadOutbox.kt index 30a8aaa..7bc8129 100644 --- a/app/src/main/java/pl/firmatpp/kierowca/data/upload/PhotoUploadOutbox.kt +++ b/app/src/main/java/pl/firmatpp/kierowca/data/upload/PhotoUploadOutbox.kt @@ -69,6 +69,13 @@ class PhotoUploadOutbox( enqueueWorker(clientRequestId) } + suspend fun discard(clientRequestId: String) { + dao.find(clientRequestId)?.let { upload -> + File(upload.localPath).delete() + dao.delete(upload.clientRequestId) + } + } + suspend fun discardConfirmedServerPhotos(route: DriverRouteDto) { val confirmedRequestIds = route.photos.mapNotNull { it.clientRequestId }.distinct() if (confirmedRequestIds.isEmpty()) return diff --git a/app/src/main/java/pl/firmatpp/kierowca/ui/DriverApp.kt b/app/src/main/java/pl/firmatpp/kierowca/ui/DriverApp.kt index 3824e9d..661ea96 100644 --- a/app/src/main/java/pl/firmatpp/kierowca/ui/DriverApp.kt +++ b/app/src/main/java/pl/firmatpp/kierowca/ui/DriverApp.kt @@ -179,7 +179,16 @@ fun DriverApp(viewModel: DriverViewModel) { onRoute = viewModel::openRoute, ) DriverScreen.Profile -> ProfileScreen(state, viewModel::refreshRoutes, viewModel::openProfile, viewModel::logout) - DriverScreen.Detail -> DetailScreen(state, viewModel::back, viewModel::uploadPhoto, viewModel::openPhoto, viewModel::deletePhoto, viewModel::retryPhotoUpload, viewModel::refreshSelectedRoute) + DriverScreen.Detail -> DetailScreen( + state, + viewModel::back, + viewModel::uploadPhoto, + viewModel::openPhoto, + viewModel::deletePhoto, + viewModel::deleteConfirmedUpload, + viewModel::retryPhotoUpload, + viewModel::refreshSelectedRoute, + ) DriverScreen.Photo -> PhotoScreen(state, viewModel::back) } @@ -738,6 +747,7 @@ private fun DetailScreen( onUpload: (Uri, String, PhotoUploadMetadata) -> Unit, onPhoto: (RoutePhotoDto) -> Unit, onDeletePhoto: (RoutePhotoDto) -> Unit, + onDeleteUpload: (PhotoUploadEntity) -> Unit, onRetryUpload: (PhotoUploadEntity) -> Unit, onRefresh: () -> Unit, ) { @@ -784,6 +794,7 @@ private fun DetailScreen( canManagePhotos = canManagePhotos, onPhoto = onPhoto, onDeletePhoto = onDeletePhoto, + onDeleteUpload = onDeleteUpload, onRetryUpload = onRetryUpload, onCamera = { val newUri = createCameraUri(context) @@ -975,6 +986,7 @@ private fun CargoDocumentationSection( canManagePhotos: Boolean, onPhoto: (RoutePhotoDto) -> Unit, onDeletePhoto: (RoutePhotoDto) -> Unit, + onDeleteUpload: (PhotoUploadEntity) -> Unit, onRetryUpload: (PhotoUploadEntity) -> Unit, onCamera: () -> Unit, onGallery: () -> Unit, @@ -997,7 +1009,7 @@ private fun CargoDocumentationSection( } } } - PhotoGrid(photos, uploads, deletingPhotoIds, imageAuthHeader, onPhoto, onDeletePhoto, onRetryUpload) + PhotoGrid(photos, uploads, deletingPhotoIds, imageAuthHeader, onPhoto, onDeletePhoto, onDeleteUpload, onRetryUpload) } } @@ -1023,9 +1035,10 @@ private fun PhotoGrid( imageAuthHeader: String?, onPhoto: (RoutePhotoDto) -> Unit, onDeletePhoto: (RoutePhotoDto) -> Unit, + onDeleteUpload: (PhotoUploadEntity) -> Unit, onRetryUpload: (PhotoUploadEntity) -> Unit, ) { - var photoPendingDelete by remember { mutableStateOf(null) } + var pendingDelete by remember { mutableStateOf(null) } val items = (uploads.map { PhotoGridItem.Upload(it) } + photos.map { PhotoGridItem.Server(it) }) .sortedByDescending { it.sortEpochMillis } val visibleAttachmentCount = visiblePhotoAttachmentCount(photos.size, uploads.size) @@ -1057,11 +1070,13 @@ private fun PhotoGrid( imageAuthHeader = imageAuthHeader, isDeleting = item.photo.id in deletingPhotoIds, onPhoto = onPhoto, - onDeletePhoto = { photoPendingDelete = it }, + onDeletePhoto = { pendingDelete = PhotoDeleteTarget.Server(it) }, modifier = Modifier.weight(1f), ) is PhotoGridItem.Upload -> PendingPhotoTile( upload = item.upload, + isDeleting = item.upload.serverPhotoId?.let { it in deletingPhotoIds } == true, + onDelete = { pendingDelete = PhotoDeleteTarget.Upload(it) }, onRetry = onRetryUpload, modifier = Modifier.weight(1f), ) @@ -1077,23 +1092,26 @@ private fun PhotoGrid( } } - photoPendingDelete?.let { photo -> + pendingDelete?.let { target -> AlertDialog( - onDismissRequest = { photoPendingDelete = null }, + onDismissRequest = { pendingDelete = null }, title = { Text("Usunąć zdjęcie?", color = TppColors.Ink, fontWeight = FontWeight.Bold) }, text = { Text("Czy na pewno chcesz usunąć to zdjęcie?", color = TppColors.Muted) }, confirmButton = { TextButton( onClick = { - photoPendingDelete = null - onDeletePhoto(photo) + pendingDelete = null + when (target) { + is PhotoDeleteTarget.Server -> onDeletePhoto(target.photo) + is PhotoDeleteTarget.Upload -> onDeleteUpload(target.upload) + } }, ) { Text("Usuń", color = TppColors.Error, fontWeight = FontWeight.Bold) } }, dismissButton = { - TextButton(onClick = { photoPendingDelete = null }) { + TextButton(onClick = { pendingDelete = null }) { Text("Anuluj", color = TppColors.Muted) } }, @@ -1109,6 +1127,11 @@ private sealed class PhotoGridItem(open val sortEpochMillis: Long) { data class Upload(val upload: PhotoUploadEntity) : PhotoGridItem(upload.createdAtEpochMillis) } +private sealed class PhotoDeleteTarget { + data class Server(val photo: RoutePhotoDto) : PhotoDeleteTarget() + data class Upload(val upload: PhotoUploadEntity) : PhotoDeleteTarget() +} + @Composable private fun PhotoTile( photo: RoutePhotoDto, @@ -1158,10 +1181,13 @@ private fun PhotoTile( @Composable private fun PendingPhotoTile( upload: PhotoUploadEntity, + isDeleting: Boolean, + onDelete: (PhotoUploadEntity) -> Unit, onRetry: (PhotoUploadEntity) -> Unit, modifier: Modifier = Modifier, ) { val status = upload.statusType + val canDeleteServerPhoto = confirmedUploadServerPhotoId(upload.status, upload.serverPhotoId) != null Box(modifier.aspectRatio(1f).background(TppColors.Panel, RoundedCornerShape(4.dp))) { AsyncImage( model = File(upload.localPath), @@ -1170,6 +1196,33 @@ private fun PendingPhotoTile( modifier = Modifier.fillMaxSize(), ) Box(Modifier.fillMaxSize().background(Color.Black.copy(alpha = 0.34f))) + if (isDeleting) { + Box( + Modifier.fillMaxSize().background(Color.White.copy(alpha = 0.72f)), + contentAlignment = Alignment.Center, + ) { + Text("Usuwam", color = TppColors.Muted, fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold) + } + } + if (canDeleteServerPhoto) { + IconButton( + onClick = { onDelete(upload) }, + enabled = !isDeleting, + modifier = Modifier + .align(Alignment.TopEnd) + .padding(8.dp) + .size(40.dp) + .background(TppColors.Surface.copy(alpha = 0.96f), RoundedCornerShape(20.dp)) + .border(1.dp, TppColors.Error.copy(alpha = 0.35f), RoundedCornerShape(20.dp)), + ) { + Icon( + Icons.Outlined.Delete, + contentDescription = "Usuń zdjęcie", + tint = TppColors.Error, + modifier = Modifier.size(19.dp), + ) + } + } Column( Modifier.align(Alignment.BottomStart).fillMaxWidth().background(Color.White.copy(alpha = 0.94f)).padding(10.dp), verticalArrangement = Arrangement.spacedBy(8.dp), diff --git a/app/src/main/java/pl/firmatpp/kierowca/ui/DriverUiRules.kt b/app/src/main/java/pl/firmatpp/kierowca/ui/DriverUiRules.kt index 2eed540..d8a22d2 100644 --- a/app/src/main/java/pl/firmatpp/kierowca/ui/DriverUiRules.kt +++ b/app/src/main/java/pl/firmatpp/kierowca/ui/DriverUiRules.kt @@ -17,6 +17,9 @@ fun routePhotoSortEpochMillis(createdAt: String?, takenAt: String?, fallback: Lo ?: parseIsoOffsetEpochMillis(takenAt) ?: fallback +fun confirmedUploadServerPhotoId(status: String, serverPhotoId: String?): String? = + serverPhotoId?.takeIf { status == "CONFIRMED" && it.isNotBlank() } + private fun parseIsoOffsetEpochMillis(value: String?): Long? = value?.takeIf { it.isNotBlank() }?.let { runCatching { OffsetDateTime.parse(it).toInstant().toEpochMilli() }.getOrNull() diff --git a/app/src/main/java/pl/firmatpp/kierowca/ui/DriverViewModel.kt b/app/src/main/java/pl/firmatpp/kierowca/ui/DriverViewModel.kt index 197f2f6..f5b94d8 100644 --- a/app/src/main/java/pl/firmatpp/kierowca/ui/DriverViewModel.kt +++ b/app/src/main/java/pl/firmatpp/kierowca/ui/DriverViewModel.kt @@ -210,6 +210,31 @@ class DriverViewModel(application: Application) : AndroidViewModel(application) } } + fun deleteConfirmedUpload(upload: PhotoUploadEntity) { + val route = _state.value.selectedRoute ?: return + val serverPhotoId = confirmedUploadServerPhotoId(upload.status, upload.serverPhotoId) ?: return + + viewModelScope.launch { + _state.update { it.copy(deletingPhotoIds = it.deletingPhotoIds + serverPhotoId, error = null) } + runCatching { + repository.deletePhoto(serverPhotoId) + photoUploadOutbox.discard(upload.clientRequestId) + val response = repository.route(route.id) + photoUploadOutbox.discardConfirmedServerPhotos(response.route) + _state.update { + it.copy( + selectedRoute = response.route, + routes = it.routes.map { item -> if (item.id == route.id) response.route else item }, + error = null, + ) + } + }.onFailure { throwable -> + _state.update { it.copy(error = ApiErrorMapper.map(throwable).message) } + } + _state.update { it.copy(deletingPhotoIds = it.deletingPhotoIds - serverPhotoId) } + } + } + fun openPhoto(photo: RoutePhotoDto) { _state.update { it.copy(screen = DriverScreen.Photo, selectedPhoto = photo) } } diff --git a/app/src/test/java/pl/firmatpp/kierowca/ui/DriverUiRulesTest.kt b/app/src/test/java/pl/firmatpp/kierowca/ui/DriverUiRulesTest.kt index bd08348..5eec9f6 100644 --- a/app/src/test/java/pl/firmatpp/kierowca/ui/DriverUiRulesTest.kt +++ b/app/src/test/java/pl/firmatpp/kierowca/ui/DriverUiRulesTest.kt @@ -60,4 +60,12 @@ class DriverUiRulesTest { ) assertEquals(fallback, routePhotoSortEpochMillis(createdAt = null, takenAt = null, fallback = fallback)) } + + @Test + fun allowsDeleteIconForConfirmedLocalUploadWithServerPhotoId() { + assertEquals("42", confirmedUploadServerPhotoId("CONFIRMED", "42")) + assertEquals(null, confirmedUploadServerPhotoId("CONFIRMED", "")) + assertEquals(null, confirmedUploadServerPhotoId("UPLOADING", "42")) + assertEquals(null, confirmedUploadServerPhotoId("FAILED_RETRYABLE", "42")) + } }