From a66351a6f4626d3f2b2e103f3016f1fb15160acf Mon Sep 17 00:00:00 2001 From: admin Date: Tue, 30 Jun 2026 22:44:14 +0200 Subject: [PATCH] =?UTF-8?q?Ogranicz=20zdj=C4=99cia=20do=20dzisiejszych=20k?= =?UTF-8?q?urs=C3=B3w?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 4 +- .../kierowca/data/DriverRepository.kt | 4 ++ .../kierowca/data/api/MobileDriverApi.kt | 7 ++ .../kierowca/data/model/DriverModels.kt | 2 + .../java/pl/firmatpp/kierowca/ui/DriverApp.kt | 66 +++++++++++++++---- .../pl/firmatpp/kierowca/ui/DriverUiRules.kt | 6 ++ .../firmatpp/kierowca/ui/DriverViewModel.kt | 6 ++ .../firmatpp/kierowca/ui/DriverUiRulesTest.kt | 23 +++++++ 8 files changed, 104 insertions(+), 14 deletions(-) create mode 100644 app/src/main/java/pl/firmatpp/kierowca/ui/DriverUiRules.kt create mode 100644 app/src/test/java/pl/firmatpp/kierowca/ui/DriverUiRulesTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9884656..d40afea 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,8 +12,8 @@ android { applicationId = "pl.firmatpp.kierowca" minSdk = 26 targetSdk = 35 - versionCode = 10 - versionName = "1.0.9" + versionCode = 11 + versionName = "1.0.10" 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/DriverRepository.kt b/app/src/main/java/pl/firmatpp/kierowca/data/DriverRepository.kt index f10ab79..081adae 100644 --- a/app/src/main/java/pl/firmatpp/kierowca/data/DriverRepository.kt +++ b/app/src/main/java/pl/firmatpp/kierowca/data/DriverRepository.kt @@ -62,6 +62,10 @@ class DriverRepository( ) } + suspend fun deletePhoto(photoId: String) { + api.deletePhoto(authHeader(requireToken()), photoId) + } + suspend fun logout() { val token = tokenStore.read() if (token != null) { diff --git a/app/src/main/java/pl/firmatpp/kierowca/data/api/MobileDriverApi.kt b/app/src/main/java/pl/firmatpp/kierowca/data/api/MobileDriverApi.kt index b6dd78c..969285e 100644 --- a/app/src/main/java/pl/firmatpp/kierowca/data/api/MobileDriverApi.kt +++ b/app/src/main/java/pl/firmatpp/kierowca/data/api/MobileDriverApi.kt @@ -10,6 +10,7 @@ import pl.firmatpp.kierowca.data.model.RouteResponse import pl.firmatpp.kierowca.data.model.VerifyOtpBody import pl.firmatpp.kierowca.data.model.VerifyOtpResponse import retrofit2.http.Body +import retrofit2.http.DELETE import retrofit2.http.GET import retrofit2.http.Header import retrofit2.http.Multipart @@ -52,4 +53,10 @@ interface MobileDriverApi { @Part("longitude") longitude: RequestBody?, @Part("locationAccuracyMeters") locationAccuracyMeters: RequestBody?, ): PhotoUploadResponse + + @DELETE("mobile/driver/photos/{photoId}") + suspend fun deletePhoto( + @Header("Authorization") authorization: String, + @Path("photoId") photoId: String, + ): Map } diff --git a/app/src/main/java/pl/firmatpp/kierowca/data/model/DriverModels.kt b/app/src/main/java/pl/firmatpp/kierowca/data/model/DriverModels.kt index 21006e9..9b85631 100644 --- a/app/src/main/java/pl/firmatpp/kierowca/data/model/DriverModels.kt +++ b/app/src/main/java/pl/firmatpp/kierowca/data/model/DriverModels.kt @@ -100,6 +100,8 @@ data class RoutePhotoDto( val longitude: Double?, val locationAccuracyMeters: Double?, val createdAt: String?, + val canDelete: Boolean = false, + val deleteUntil: String? = null, ) data class PhotoUploadResponse( 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 57f4df5..4027913 100644 --- a/app/src/main/java/pl/firmatpp/kierowca/ui/DriverApp.kt +++ b/app/src/main/java/pl/firmatpp/kierowca/ui/DriverApp.kt @@ -44,6 +44,7 @@ import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.AddPhotoAlternate @@ -51,6 +52,7 @@ import androidx.compose.material.icons.outlined.ArrowBack import androidx.compose.material.icons.outlined.CameraAlt import androidx.compose.material.icons.outlined.CalendarToday import androidx.compose.material.icons.outlined.Factory +import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.LocationOn import androidx.compose.material.icons.outlined.LocalShipping import androidx.compose.material.icons.outlined.Navigation @@ -170,7 +172,7 @@ 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::refreshSelectedRoute) + DriverScreen.Detail -> DetailScreen(state, viewModel::back, viewModel::uploadPhoto, viewModel::openPhoto, viewModel::deletePhoto, viewModel::refreshSelectedRoute) DriverScreen.Photo -> PhotoScreen(state, viewModel::back) } @@ -351,8 +353,16 @@ private fun RouteDateSelector(selectedDate: String, minDate: String, maxDate: St }.toList() }.getOrElse { listOf(LocalDate.parse(selectedDate)) } } + val listState = rememberLazyListState() - LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + LaunchedEffect(dates, selectedDate) { + val selectedIndex = dates.indexOfFirst { it.toString() == selectedDate } + if (selectedIndex >= 0) { + listState.scrollToItem(selectedIndex) + } + } + + LazyRow(state = listState, horizontalArrangement = Arrangement.spacedBy(8.dp)) { items(dates, key = { it.toString() }) { date -> DateSelectorChip( date = date, @@ -575,6 +585,7 @@ private fun DetailScreen( onBack: () -> Unit, onUpload: (Uri, String, PhotoUploadMetadata) -> Unit, onPhoto: (RoutePhotoDto) -> Unit, + onDeletePhoto: (RoutePhotoDto) -> Unit, onRefresh: () -> Unit, ) { val context = LocalContext.current @@ -594,6 +605,7 @@ private fun DetailScreen( if (uri != null) onUpload(uri, "gallery", PhotoUploadMetadata()) } val route = state.selectedRoute + val canManagePhotos = canManageRoutePhotos(state.selectedDate) Scaffold( topBar = { @@ -614,7 +626,9 @@ private fun DetailScreen( photos = route.photos, imageAuthHeader = state.imageAuthHeader, allowGalleryUploads = state.allowGalleryUploads, + canManagePhotos = canManagePhotos, onPhoto = onPhoto, + onDeletePhoto = onDeletePhoto, onCamera = { val newUri = createCameraUri(context) cameraUri = newUri @@ -800,7 +814,9 @@ private fun CargoDocumentationSection( photos: List, imageAuthHeader: String?, allowGalleryUploads: Boolean, + canManagePhotos: Boolean, onPhoto: (RoutePhotoDto) -> Unit, + onDeletePhoto: (RoutePhotoDto) -> Unit, onCamera: () -> Unit, onGallery: () -> Unit, ) { @@ -811,12 +827,18 @@ private fun CargoDocumentationSection( Box(Modifier.width(116.dp).height(3.dp).background(TppColors.ContainerGreen)) } Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - if (allowGalleryUploads) { - CargoActionButton("Dodaj zdjęcie z galerii", Icons.Outlined.AddPhotoAlternate, TppColors.ContainerGreen, onGallery) + if (canManagePhotos) { + if (allowGalleryUploads) { + CargoActionButton("Dodaj zdjęcie z galerii", Icons.Outlined.AddPhotoAlternate, TppColors.ContainerGreen, onGallery) + } + CargoActionButton("Zrób nowe zdjęcie", Icons.Outlined.CameraAlt, TppColors.Forest, onCamera) + } else { + Box(Modifier.fillMaxWidth().background(TppColors.Panel, RoundedCornerShape(4.dp)).padding(14.dp)) { + Text("Zdjęcia można dodawać tylko dla dzisiejszych kursów.", color = TppColors.Muted, fontFamily = FontFamily.Monospace) + } } - CargoActionButton("Zrób nowe zdjęcie", Icons.Outlined.CameraAlt, TppColors.Forest, onCamera) } - PhotoGrid(photos, imageAuthHeader, onPhoto) + PhotoGrid(photos, imageAuthHeader, onPhoto, onDeletePhoto) } } @@ -839,6 +861,7 @@ private fun PhotoGrid( photos: List, imageAuthHeader: String?, onPhoto: (RoutePhotoDto) -> Unit, + onDeletePhoto: (RoutePhotoDto) -> Unit, ) { val rows = ceil((photos.size.coerceAtLeast(1) / 2f).toDouble()).toInt() val gridHeight = (rows * 172).coerceIn(172, 520).dp @@ -865,12 +888,31 @@ private fun PhotoGrid( horizontalArrangement = Arrangement.spacedBy(12.dp), ) { items(photos, key = { it.id }) { photo -> - AsyncImage( - model = imageRequest(photo.url, imageAuthHeader), - contentDescription = "Zdjecie ladunku", - contentScale = ContentScale.Crop, - modifier = Modifier.aspectRatio(1f).clickable { onPhoto(photo) }, - ) + Box(Modifier.aspectRatio(1f)) { + AsyncImage( + model = imageRequest(photo.url, imageAuthHeader), + contentDescription = "Zdjecie ladunku", + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize().clickable { onPhoto(photo) }, + ) + if (photo.canDelete) { + IconButton( + onClick = { onDeletePhoto(photo) }, + modifier = Modifier + .align(Alignment.TopEnd) + .padding(6.dp) + .size(34.dp) + .background(TppColors.Error, RoundedCornerShape(17.dp)), + ) { + Icon( + Icons.Outlined.Delete, + contentDescription = "Usuń zdjęcie", + tint = Color.White, + modifier = Modifier.size(18.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 new file mode 100644 index 0000000..5edbad8 --- /dev/null +++ b/app/src/main/java/pl/firmatpp/kierowca/ui/DriverUiRules.kt @@ -0,0 +1,6 @@ +package pl.firmatpp.kierowca.ui + +import java.time.LocalDate + +fun canManageRoutePhotos(selectedDate: String, today: LocalDate = LocalDate.now()): Boolean = + runCatching { LocalDate.parse(selectedDate).isEqual(today) }.getOrDefault(false) 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 96f1c1a..db3d72b 100644 --- a/app/src/main/java/pl/firmatpp/kierowca/ui/DriverViewModel.kt +++ b/app/src/main/java/pl/firmatpp/kierowca/ui/DriverViewModel.kt @@ -147,6 +147,12 @@ class DriverViewModel(application: Application) : AndroidViewModel(application) openRoute(route.id) } + fun deletePhoto(photo: RoutePhotoDto) = runLoading { + val route = _state.value.selectedRoute ?: return@runLoading + repository.deletePhoto(photo.id) + openRoute(route.id) + } + 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 new file mode 100644 index 0000000..e4bfead --- /dev/null +++ b/app/src/test/java/pl/firmatpp/kierowca/ui/DriverUiRulesTest.kt @@ -0,0 +1,23 @@ +package pl.firmatpp.kierowca.ui + +import java.time.LocalDate +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class DriverUiRulesTest { + private val today = LocalDate.parse("2026-06-30") + + @Test + fun allowsPhotoActionsOnlyForTodayRouteDate() { + assertTrue(canManageRoutePhotos("2026-06-30", today)) + assertFalse(canManageRoutePhotos("2026-06-29", today)) + assertFalse(canManageRoutePhotos("2026-07-01", today)) + } + + @Test + fun blocksPhotoActionsWhenDateIsInvalid() { + assertFalse(canManageRoutePhotos("", today)) + assertFalse(canManageRoutePhotos("not-a-date", today)) + } +}