diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d3b6ad8..cddd2ec 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 = 15 - versionName = "1.0.14" + versionCode = 16 + versionName = "1.0.15" 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/ui/DriverApp.kt b/app/src/main/java/pl/firmatpp/kierowca/ui/DriverApp.kt index b9e1e00..3824e9d 100644 --- a/app/src/main/java/pl/firmatpp/kierowca/ui/DriverApp.kt +++ b/app/src/main/java/pl/firmatpp/kierowca/ui/DriverApp.kt @@ -62,6 +62,7 @@ import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card @@ -75,6 +76,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable @@ -1023,7 +1025,10 @@ private fun PhotoGrid( onDeletePhoto: (RoutePhotoDto) -> Unit, onRetryUpload: (PhotoUploadEntity) -> Unit, ) { - val items = uploads.map { PhotoGridItem.Upload(it) } + photos.map { PhotoGridItem.Server(it) } + var photoPendingDelete 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) Card( colors = CardDefaults.cardColors(containerColor = Color.White), @@ -1032,7 +1037,7 @@ private fun PhotoGrid( ) { Column(Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { Text( - "Załączone zdjęcia (${photos.size})", + "Załączone zdjęcia ($visibleAttachmentCount)", color = TppColors.Muted, fontFamily = FontFamily.Monospace, fontWeight = FontWeight.SemiBold, @@ -1052,7 +1057,7 @@ private fun PhotoGrid( imageAuthHeader = imageAuthHeader, isDeleting = item.photo.id in deletingPhotoIds, onPhoto = onPhoto, - onDeletePhoto = onDeletePhoto, + onDeletePhoto = { photoPendingDelete = it }, modifier = Modifier.weight(1f), ) is PhotoGridItem.Upload -> PendingPhotoTile( @@ -1071,11 +1076,37 @@ private fun PhotoGrid( } } } + + photoPendingDelete?.let { photo -> + AlertDialog( + onDismissRequest = { photoPendingDelete = 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) + }, + ) { + Text("Usuń", color = TppColors.Error, fontWeight = FontWeight.Bold) + } + }, + dismissButton = { + TextButton(onClick = { photoPendingDelete = null }) { + Text("Anuluj", color = TppColors.Muted) + } + }, + ) + } } -private sealed class PhotoGridItem { - data class Server(val photo: RoutePhotoDto) : PhotoGridItem() - data class Upload(val upload: PhotoUploadEntity) : PhotoGridItem() +private sealed class PhotoGridItem(open val sortEpochMillis: Long) { + data class Server(val photo: RoutePhotoDto) : PhotoGridItem( + routePhotoSortEpochMillis(photo.createdAt, photo.takenAt, fallback = 0L), + ) + + data class Upload(val upload: PhotoUploadEntity) : PhotoGridItem(upload.createdAtEpochMillis) } @Composable 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 cf2b6f3..2eed540 100644 --- a/app/src/main/java/pl/firmatpp/kierowca/ui/DriverUiRules.kt +++ b/app/src/main/java/pl/firmatpp/kierowca/ui/DriverUiRules.kt @@ -1,9 +1,23 @@ package pl.firmatpp.kierowca.ui import java.time.LocalDate +import java.time.OffsetDateTime fun canManageRoutePhotos(selectedDate: String, today: LocalDate = LocalDate.now()): Boolean = runCatching { LocalDate.parse(selectedDate).isEqual(today) }.getOrDefault(false) fun inlinePhotoGridRows(photoCount: Int): Int = if (photoCount <= 0) 0 else (photoCount + 1) / 2 + +fun visiblePhotoAttachmentCount(serverPhotoCount: Int, localUploadCount: Int): Int = + serverPhotoCount.coerceAtLeast(0) + localUploadCount.coerceAtLeast(0) + +fun routePhotoSortEpochMillis(createdAt: String?, takenAt: String?, fallback: Long): Long = + parseIsoOffsetEpochMillis(createdAt) + ?: parseIsoOffsetEpochMillis(takenAt) + ?: fallback + +private fun parseIsoOffsetEpochMillis(value: String?): Long? = + value?.takeIf { it.isNotBlank() }?.let { + runCatching { OffsetDateTime.parse(it).toInstant().toEpochMilli() }.getOrNull() + } 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 3cf8981..bd08348 100644 --- a/app/src/test/java/pl/firmatpp/kierowca/ui/DriverUiRulesTest.kt +++ b/app/src/test/java/pl/firmatpp/kierowca/ui/DriverUiRulesTest.kt @@ -30,4 +30,34 @@ class DriverUiRulesTest { assertEquals(2, inlinePhotoGridRows(3)) assertEquals(3, inlinePhotoGridRows(5)) } + + @Test + fun countsServerPhotosAndLocalUploadsAsVisibleAttachments() { + assertEquals(0, visiblePhotoAttachmentCount(serverPhotoCount = 0, localUploadCount = 0)) + assertEquals(1, visiblePhotoAttachmentCount(serverPhotoCount = 0, localUploadCount = 1)) + assertEquals(3, visiblePhotoAttachmentCount(serverPhotoCount = 1, localUploadCount = 2)) + } + + @Test + fun usesCreatedAtBeforeTakenAtForNewestFirstPhotoSorting() { + val fallback = 100L + + assertEquals( + 1_782_892_800_000L, + routePhotoSortEpochMillis( + createdAt = "2026-07-01T10:00:00+02:00", + takenAt = "2026-07-01T08:00:00+02:00", + fallback = fallback, + ), + ) + assertEquals( + 1_782_885_600_000L, + routePhotoSortEpochMillis( + createdAt = null, + takenAt = "2026-07-01T08:00:00+02:00", + fallback = fallback, + ), + ) + assertEquals(fallback, routePhotoSortEpochMillis(createdAt = null, takenAt = null, fallback = fallback)) + } }