From e70a53eca71be317101fb5beebad4fe889895883 Mon Sep 17 00:00:00 2001 From: admin Date: Fri, 24 Jul 2026 13:33:27 +0200 Subject: [PATCH] =?UTF-8?q?Dodaj=20potwierdzanie=20odczyt=C3=B3w=20tachogr?= =?UTF-8?q?afu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kierowca/data/DriverRepository.kt | 8 + .../kierowca/data/api/MobileDriverApi.kt | 8 + .../kierowca/data/model/DriverModels.kt | 17 +++ .../java/pl/firmatpp/kierowca/ui/DriverApp.kt | 144 +++++++++++++++++- .../pl/firmatpp/kierowca/ui/DriverUiRules.kt | 7 +- .../firmatpp/kierowca/ui/DriverViewModel.kt | 34 +++++ .../firmatpp/kierowca/ui/DriverUiRulesTest.kt | 24 +++ 7 files changed, 239 insertions(+), 3 deletions(-) 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 5bdc777..6a66dbb 100644 --- a/app/src/main/java/pl/firmatpp/kierowca/data/DriverRepository.kt +++ b/app/src/main/java/pl/firmatpp/kierowca/data/DriverRepository.kt @@ -12,6 +12,7 @@ import okhttp3.RequestBody.Companion.toRequestBody import pl.firmatpp.kierowca.data.api.ApiFactory import pl.firmatpp.kierowca.data.api.MobileDriverApi import pl.firmatpp.kierowca.data.model.BootstrapResponse +import pl.firmatpp.kierowca.data.model.AcknowledgeTachographReminderBody import pl.firmatpp.kierowca.data.model.BroadcastAuthBody import pl.firmatpp.kierowca.data.model.BroadcastAuthResponse import pl.firmatpp.kierowca.data.model.CancelLeaveRequestBody @@ -33,6 +34,7 @@ import pl.firmatpp.kierowca.data.model.RoutePointsResponse import pl.firmatpp.kierowca.data.model.RouteResponse import pl.firmatpp.kierowca.data.model.StartRouteBody import pl.firmatpp.kierowca.data.model.SyncStateResponse +import pl.firmatpp.kierowca.data.model.TachographReminderDto import pl.firmatpp.kierowca.data.model.VerifyOtpBody import pl.firmatpp.kierowca.data.upload.DispatchSheetUploadEntity import pl.firmatpp.kierowca.data.upload.PhotoUploadEntity @@ -56,6 +58,12 @@ class DriverRepository( suspend fun bootstrap(date: String? = null): BootstrapResponse = api.bootstrap(authHeader(requireToken()), date) + suspend fun acknowledgeTachographReminder(taskIds: List): TachographReminderDto = + api.acknowledgeTachographReminder( + authHeader(requireToken()), + AcknowledgeTachographReminderBody(taskIds), + ).tachographReminder + suspend fun route(routeId: String): RouteResponse = api.route(authHeader(requireToken()), routeId) 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 c2dfccd..c618cad 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 @@ -3,6 +3,8 @@ package pl.firmatpp.kierowca.data.api import okhttp3.MultipartBody import okhttp3.RequestBody import pl.firmatpp.kierowca.data.model.BootstrapResponse +import pl.firmatpp.kierowca.data.model.AcknowledgeTachographReminderBody +import pl.firmatpp.kierowca.data.model.AcknowledgeTachographReminderResponse import pl.firmatpp.kierowca.data.model.BroadcastAuthBody import pl.firmatpp.kierowca.data.model.BroadcastAuthResponse import pl.firmatpp.kierowca.data.model.CancelLeaveRequestBody @@ -67,6 +69,12 @@ interface MobileDriverApi { @Query("date") date: String?, ): BootstrapResponse + @POST("mobile/driver/tachograph-reminder/acknowledge") + suspend fun acknowledgeTachographReminder( + @Header("Authorization") authorization: String, + @Body body: AcknowledgeTachographReminderBody, + ): AcknowledgeTachographReminderResponse + @GET("mobile/driver/leave-request-types") suspend fun leaveRequestTypes( @Header("Authorization") authorization: String, 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 99a1c5d..5d57121 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 @@ -106,6 +106,23 @@ data class TachographReminderDto( val visible: Boolean = false, val title: String? = null, val message: String? = null, + val confirmationLabel: String? = null, + val tasks: List? = null, +) + +data class TachographReminderTaskDto( + val id: String, + val type: String, + val label: String, +) + +data class AcknowledgeTachographReminderBody( + val taskIds: List, +) + +data class AcknowledgeTachographReminderResponse( + val ok: Boolean, + val tachographReminder: TachographReminderDto, ) data class DispatchSheetPhotoDto( 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 2eb886c..28f6325 100644 --- a/app/src/main/java/pl/firmatpp/kierowca/ui/DriverApp.kt +++ b/app/src/main/java/pl/firmatpp/kierowca/ui/DriverApp.kt @@ -84,6 +84,7 @@ import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Checkbox import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DatePicker import androidx.compose.material3.DatePickerDialog @@ -409,6 +410,7 @@ fun DriverApp( state = state, onRefresh = viewModel::refreshRoutesSilently, onDate = viewModel::selectRouteDate, + onTachographReminderComplete = viewModel::acknowledgeTachographReminder, onProfile = viewModel::openProfile, onPhotoQueue = viewModel::openPhotoQueue, onDispatchSheetUpload = viewModel::uploadDispatchSheetPhoto, @@ -1156,6 +1158,7 @@ private fun RoutesScreen( state: DriverUiState, onRefresh: () -> Unit, onDate: (String) -> Unit, + onTachographReminderComplete: (List) -> Unit, onProfile: () -> Unit, onPhotoQueue: () -> Unit, onDispatchSheetUpload: (Uri, String, PhotoUploadMetadata) -> Unit, @@ -1167,6 +1170,7 @@ private fun RoutesScreen( var cameraUri by remember { mutableStateOf(null) } var showPreciseLocationPermissionDialog by remember { mutableStateOf(false) } var showRouteDatePicker by remember { mutableStateOf(false) } + var showTachographConfirmation by remember { mutableStateOf(false) } val cameraLauncher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) { ok -> val capturedUri = cameraUri if ( @@ -1281,7 +1285,11 @@ private fun RoutesScreen( } if (shouldShowTachographReminderCard(state.tachographReminder)) { item { - TachographReminderCard(state.tachographReminder) + TachographReminderCard( + reminder = state.tachographReminder, + submitting = state.tachographReminderSubmitting, + onConfirm = { showTachographConfirmation = true }, + ) } } if (shouldShowDispatchSheetReminderCard(state.dispatchSheetReminder)) { @@ -1365,6 +1373,18 @@ private fun RoutesScreen( }, ) } + + if (showTachographConfirmation) { + TachographReminderConfirmationDialog( + reminder = state.tachographReminder, + submitting = state.tachographReminderSubmitting, + onDismiss = { showTachographConfirmation = false }, + onConfirm = { taskIds -> + showTachographConfirmation = false + onTachographReminderComplete(taskIds) + }, + ) + } } @Composable @@ -2259,7 +2279,11 @@ private fun DispatchSheetReminderCard( } @Composable -private fun TachographReminderCard(reminder: TachographReminderDto?) { +private fun TachographReminderCard( + reminder: TachographReminderDto?, + submitting: Boolean, + onConfirm: () -> Unit, +) { if (!shouldShowTachographReminderCard(reminder)) return Card( @@ -2300,9 +2324,125 @@ private fun TachographReminderCard(reminder: TachographReminderDto?) { ) } } + Button( + onClick = onConfirm, + enabled = !submitting, + modifier = Modifier + .fillMaxWidth() + .padding(start = 18.dp, end = 18.dp, bottom = 18.dp) + .height(52.dp), + colors = ButtonDefaults.buttonColors(containerColor = TppTheme.colors.forest), + shape = MaterialTheme.shapes.medium, + ) { + if (submitting) { + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + color = Color.White, + strokeWidth = 2.dp, + ) + } else { + Icon( + Icons.Outlined.CheckCircle, + contentDescription = null, + tint = Color.White, + ) + } + Spacer(Modifier.width(10.dp)) + Text( + reminder?.confirmationLabel?.takeIf { it.isNotBlank() } ?: "Wykonano", + color = Color.White, + fontWeight = FontWeight.Bold, + ) + } } } +@Composable +private fun TachographReminderConfirmationDialog( + reminder: TachographReminderDto?, + submitting: Boolean, + onDismiss: () -> Unit, + onConfirm: (List) -> Unit, +) { + val tasks = reminder?.tasks.orEmpty() + val taskIds = tasks.map { it.id } + var selectedIds by remember(taskIds) { + mutableStateOf(defaultTachographConfirmationSelection(tasks)) + } + + if (tasks.isEmpty()) { + LaunchedEffect(Unit) { onDismiss() } + return + } + + AlertDialog( + onDismissRequest = { if (!submitting) onDismiss() }, + title = { + Text( + "Potwierdź wykonanie", + color = TppTheme.colors.ink, + fontWeight = FontWeight.Bold, + ) + }, + text = { + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + Text( + if (tasks.size == 1) { + "Potwierdź wykonanie poniższej czynności." + } else { + "Zaznacz, które czynności zostały wykonane." + }, + color = TppTheme.colors.muted, + ) + tasks.forEach { task -> + val checked = task.id in selectedIds + Row( + modifier = Modifier + .fillMaxWidth() + .background(TppTheme.colors.panel, RoundedCornerShape(10.dp)) + .clickable(enabled = !submitting) { + selectedIds = if (checked) selectedIds - task.id else selectedIds + task.id + } + .padding(horizontal = 10.dp, vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Checkbox( + checked = checked, + onCheckedChange = { isChecked -> + selectedIds = if (isChecked) selectedIds + task.id else selectedIds - task.id + }, + enabled = !submitting, + ) + Text( + task.label, + modifier = Modifier.weight(1f), + color = TppTheme.colors.ink, + fontWeight = FontWeight.SemiBold, + ) + } + } + } + }, + confirmButton = { + TextButton( + onClick = { onConfirm(taskIds.filter { it in selectedIds }) }, + enabled = selectedIds.isNotEmpty() && !submitting, + ) { + Text("Potwierdź") + } + }, + dismissButton = { + TextButton( + onClick = onDismiss, + enabled = !submitting, + ) { + Text("Anuluj") + } + }, + ) +} + @Composable private fun OperationalQueueBanner(state: DriverUiState, onPhotoQueue: () -> Unit) { val count = state.pendingOperationalItems 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 772037f..c7765b8 100644 --- a/app/src/main/java/pl/firmatpp/kierowca/ui/DriverUiRules.kt +++ b/app/src/main/java/pl/firmatpp/kierowca/ui/DriverUiRules.kt @@ -15,6 +15,7 @@ import pl.firmatpp.kierowca.data.model.DriverRouteDto import pl.firmatpp.kierowca.data.model.DispatchSheetReminderDto import pl.firmatpp.kierowca.data.model.RoutePhotoDto import pl.firmatpp.kierowca.data.model.TachographReminderDto +import pl.firmatpp.kierowca.data.model.TachographReminderTaskDto import pl.firmatpp.kierowca.data.model.driverLifecycleStatus import pl.firmatpp.kierowca.data.upload.PhotoUploadEntity import pl.firmatpp.kierowca.data.upload.PhotoUploadStatus @@ -676,7 +677,11 @@ fun shouldShowTachographReminderCard(reminder: TachographReminderDto?): Boolean reminder?.enabled == true && reminder.visible && !reminder.title.isNullOrBlank() && - !reminder.message.isNullOrBlank() + !reminder.message.isNullOrBlank() && + !reminder.tasks.isNullOrEmpty() + +fun defaultTachographConfirmationSelection(tasks: List): Set = + if (tasks.size == 1) setOf(tasks.single().id) else emptySet() fun dispatchSheetPrimaryActionLabel(reminder: DispatchSheetReminderDto?, hasQueuedUpload: Boolean): String = when { 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 2b3fdd0..53fbbef 100644 --- a/app/src/main/java/pl/firmatpp/kierowca/ui/DriverViewModel.kt +++ b/app/src/main/java/pl/firmatpp/kierowca/ui/DriverViewModel.kt @@ -104,6 +104,7 @@ data class DriverUiState( val queuedPhotoUploads: List = emptyList(), val dispatchSheetReminder: DispatchSheetReminderDto? = null, val tachographReminder: TachographReminderDto? = null, + val tachographReminderSubmitting: Boolean = false, val appUpdate: AppUpdateDto? = null, val dispatchSheetUploads: List = emptyList(), val leaveRequestsConfig: LeaveRequestsConfig? = null, @@ -472,6 +473,39 @@ class DriverViewModel(application: Application) : AndroidViewModel(application) loadRoutes(date = date, showLoading = true, navigateToRoutes = true) } + fun acknowledgeTachographReminder(taskIds: List) { + val snapshot = _state.value + if (taskIds.isEmpty() || snapshot.tachographReminderSubmitting) return + if (!snapshot.isOnline) { + _state.update { it.copy(error = "Potwierdzenie odczytu wymaga połączenia z internetem.") } + return + } + + viewModelScope.launch { + _state.update { + it.copy( + tachographReminderSubmitting = true, + error = null, + feedback = null, + ) + } + runCatching { repository.acknowledgeTachographReminder(taskIds) } + .onSuccess { reminder -> + _state.update { + it.copy( + tachographReminder = reminder, + feedback = "Zapisano potwierdzenie odczytu.", + error = null, + ) + } + } + .onFailure { throwable -> + _state.update { it.withApiError(throwable) } + } + _state.update { it.copy(tachographReminderSubmitting = false) } + } + } + fun dismissRouteDayLiveUpdate() { _state.update { it.copy(routeDayLiveUpdateMessage = null) } } 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 ca2ffc1..2c6596b 100644 --- a/app/src/test/java/pl/firmatpp/kierowca/ui/DriverUiRulesTest.kt +++ b/app/src/test/java/pl/firmatpp/kierowca/ui/DriverUiRulesTest.kt @@ -9,6 +9,7 @@ import pl.firmatpp.kierowca.data.model.DriverRouteDto import pl.firmatpp.kierowca.data.model.DispatchSheetReminderDto import pl.firmatpp.kierowca.data.model.RoutePhotoDto import pl.firmatpp.kierowca.data.model.TachographReminderDto +import pl.firmatpp.kierowca.data.model.TachographReminderTaskDto import pl.firmatpp.kierowca.data.upload.PhotoUploadEntity import pl.firmatpp.kierowca.data.upload.RouteActionEntity import pl.firmatpp.kierowca.data.upload.RouteActionStatus @@ -420,6 +421,7 @@ class DriverUiRulesTest { visible = true, title = "Odczyt tachografu", message = "Pamiętaj o odczycie.", + tasks = listOf(tachographTask()), ), ), ) @@ -430,6 +432,7 @@ class DriverUiRulesTest { visible = true, title = "Odczyt tachografu", message = "Pamiętaj o odczycie.", + tasks = listOf(tachographTask()), ), ), ) @@ -440,12 +443,26 @@ class DriverUiRulesTest { visible = false, title = "Odczyt tachografu", message = "Pamiętaj o odczycie.", + tasks = listOf(tachographTask()), ), ), ) assertFalse(shouldShowTachographReminderCard(null)) } + @Test + fun preselectsOnlySingleTachographConfirmationTask() { + val driverCard = tachographTask() + val vehicle = TachographReminderTaskDto( + id = "vehicle:12:2026-07-31", + type = "vehicle_tachograph", + label = "Tachograf pojazdu", + ) + + assertEquals(setOf(driverCard.id), defaultTachographConfirmationSelection(listOf(driverCard))) + assertEquals(emptySet(), defaultTachographConfirmationSelection(listOf(driverCard, vehicle))) + } + @Test fun explainsOfflineStateWithoutCachedSync() { assertEquals( @@ -853,4 +870,11 @@ class DriverUiRulesTest { photo = null, canUpload = dueToday, ) + + private fun tachographTask(): TachographReminderTaskDto = + TachographReminderTaskDto( + id = "driver-card:1:2026-07-31", + type = "driver_card", + label = "Karta kierowcy", + ) }