Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
062415a75d | ||
|
|
ab68be4c1e | ||
|
|
513c1785ef | ||
|
|
e70a53eca7 | ||
|
|
c3004a3d2e | ||
|
|
fda4b462dc | ||
|
|
6a1a15ade9 | ||
|
|
ea44cf5d2e | ||
|
|
984feb170a | ||
|
|
5691c87e91 |
@@ -22,7 +22,7 @@ val keystoreProperties = Properties().apply {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "pl.firmatpp.kierowca"
|
namespace = "pl.firmatpp.kierowca"
|
||||||
compileSdk = 35
|
compileSdk = 36
|
||||||
|
|
||||||
fun configValue(name: String, default: String): String =
|
fun configValue(name: String, default: String): String =
|
||||||
providers.gradleProperty(name)
|
providers.gradleProperty(name)
|
||||||
@@ -33,9 +33,9 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "pl.firmatpp.kierowca"
|
applicationId = "pl.firmatpp.kierowca"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 36
|
||||||
versionCode = 120
|
versionCode = 125
|
||||||
versionName = "1.0.67"
|
versionName = "1.0.72"
|
||||||
setProperty("archivesBaseName", "pl.firmatpp.kierowca")
|
setProperty("archivesBaseName", "pl.firmatpp.kierowca")
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,10 @@
|
|||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||||
|
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.camera"
|
||||||
|
android:required="false" />
|
||||||
|
|
||||||
<queries>
|
<queries>
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name="android.speech.RecognitionService" />
|
<action android:name="android.speech.RecognitionService" />
|
||||||
|
|||||||
@@ -22,17 +22,6 @@ class DeviceInfoProvider(
|
|||||||
androidVersion = Build.VERSION.RELEASE,
|
androidVersion = Build.VERSION.RELEASE,
|
||||||
sdkInt = Build.VERSION.SDK_INT,
|
sdkInt = Build.VERSION.SDK_INT,
|
||||||
appVersion = BuildConfig.VERSION_NAME,
|
appVersion = BuildConfig.VERSION_NAME,
|
||||||
serialNumber = readSerialNumber(),
|
serialNumber = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun readSerialNumber(): String? =
|
|
||||||
runCatching {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
Build.getSerial()
|
|
||||||
} else {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
Build.SERIAL
|
|
||||||
}
|
|
||||||
}.getOrNull()
|
|
||||||
?.takeUnless { it.isBlank() || it.equals(Build.UNKNOWN, ignoreCase = true) }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import okhttp3.RequestBody.Companion.toRequestBody
|
|||||||
import pl.firmatpp.kierowca.data.api.ApiFactory
|
import pl.firmatpp.kierowca.data.api.ApiFactory
|
||||||
import pl.firmatpp.kierowca.data.api.MobileDriverApi
|
import pl.firmatpp.kierowca.data.api.MobileDriverApi
|
||||||
import pl.firmatpp.kierowca.data.model.BootstrapResponse
|
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.BroadcastAuthBody
|
||||||
import pl.firmatpp.kierowca.data.model.BroadcastAuthResponse
|
import pl.firmatpp.kierowca.data.model.BroadcastAuthResponse
|
||||||
import pl.firmatpp.kierowca.data.model.CancelLeaveRequestBody
|
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.RouteResponse
|
||||||
import pl.firmatpp.kierowca.data.model.StartRouteBody
|
import pl.firmatpp.kierowca.data.model.StartRouteBody
|
||||||
import pl.firmatpp.kierowca.data.model.SyncStateResponse
|
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.model.VerifyOtpBody
|
||||||
import pl.firmatpp.kierowca.data.upload.DispatchSheetUploadEntity
|
import pl.firmatpp.kierowca.data.upload.DispatchSheetUploadEntity
|
||||||
import pl.firmatpp.kierowca.data.upload.PhotoUploadEntity
|
import pl.firmatpp.kierowca.data.upload.PhotoUploadEntity
|
||||||
@@ -56,6 +58,12 @@ class DriverRepository(
|
|||||||
suspend fun bootstrap(date: String? = null): BootstrapResponse =
|
suspend fun bootstrap(date: String? = null): BootstrapResponse =
|
||||||
api.bootstrap(authHeader(requireToken()), date)
|
api.bootstrap(authHeader(requireToken()), date)
|
||||||
|
|
||||||
|
suspend fun acknowledgeTachographReminder(taskIds: List<String>): TachographReminderDto =
|
||||||
|
api.acknowledgeTachographReminder(
|
||||||
|
authHeader(requireToken()),
|
||||||
|
AcknowledgeTachographReminderBody(taskIds),
|
||||||
|
).tachographReminder
|
||||||
|
|
||||||
suspend fun route(routeId: String): RouteResponse =
|
suspend fun route(routeId: String): RouteResponse =
|
||||||
api.route(authHeader(requireToken()), routeId)
|
api.route(authHeader(requireToken()), routeId)
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package pl.firmatpp.kierowca.data.api
|
|||||||
import okhttp3.MultipartBody
|
import okhttp3.MultipartBody
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
import pl.firmatpp.kierowca.data.model.BootstrapResponse
|
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.BroadcastAuthBody
|
||||||
import pl.firmatpp.kierowca.data.model.BroadcastAuthResponse
|
import pl.firmatpp.kierowca.data.model.BroadcastAuthResponse
|
||||||
import pl.firmatpp.kierowca.data.model.CancelLeaveRequestBody
|
import pl.firmatpp.kierowca.data.model.CancelLeaveRequestBody
|
||||||
@@ -67,6 +69,12 @@ interface MobileDriverApi {
|
|||||||
@Query("date") date: String?,
|
@Query("date") date: String?,
|
||||||
): BootstrapResponse
|
): BootstrapResponse
|
||||||
|
|
||||||
|
@POST("mobile/driver/tachograph-reminder/acknowledge")
|
||||||
|
suspend fun acknowledgeTachographReminder(
|
||||||
|
@Header("Authorization") authorization: String,
|
||||||
|
@Body body: AcknowledgeTachographReminderBody,
|
||||||
|
): AcknowledgeTachographReminderResponse
|
||||||
|
|
||||||
@GET("mobile/driver/leave-request-types")
|
@GET("mobile/driver/leave-request-types")
|
||||||
suspend fun leaveRequestTypes(
|
suspend fun leaveRequestTypes(
|
||||||
@Header("Authorization") authorization: String,
|
@Header("Authorization") authorization: String,
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ data class BootstrapResponse(
|
|||||||
val routes: RoutesBucketDto,
|
val routes: RoutesBucketDto,
|
||||||
val driverAppSettings: DriverAppSettingsDto?,
|
val driverAppSettings: DriverAppSettingsDto?,
|
||||||
val dispatchSheetReminder: DispatchSheetReminderDto? = null,
|
val dispatchSheetReminder: DispatchSheetReminderDto? = null,
|
||||||
|
val tachographReminder: TachographReminderDto? = null,
|
||||||
val notificationPreferences: NotificationPreferencesDto? = null,
|
val notificationPreferences: NotificationPreferencesDto? = null,
|
||||||
val realtime: RealtimeConfigDto? = null,
|
val realtime: RealtimeConfigDto? = null,
|
||||||
val syncState: SyncStateResponse? = null,
|
val syncState: SyncStateResponse? = null,
|
||||||
@@ -80,6 +81,7 @@ data class DriverAppSettingsDto(
|
|||||||
val dispatchSheetRemindersEnabled: Boolean? = null,
|
val dispatchSheetRemindersEnabled: Boolean? = null,
|
||||||
val dispatchSheetOnFridays: Boolean? = null,
|
val dispatchSheetOnFridays: Boolean? = null,
|
||||||
val dispatchSheetOnLastWorkingDay: Boolean? = null,
|
val dispatchSheetOnLastWorkingDay: Boolean? = null,
|
||||||
|
val tachographRemindersEnabled: Boolean? = null,
|
||||||
val leaveRequests: LeaveRequestsConfigDto? = null,
|
val leaveRequests: LeaveRequestsConfigDto? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -99,6 +101,30 @@ data class DispatchSheetReminderDto(
|
|||||||
val canUpload: Boolean = false,
|
val canUpload: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class TachographReminderDto(
|
||||||
|
val enabled: Boolean = false,
|
||||||
|
val visible: Boolean = false,
|
||||||
|
val title: String? = null,
|
||||||
|
val message: String? = null,
|
||||||
|
val confirmationLabel: String? = null,
|
||||||
|
val tasks: List<TachographReminderTaskDto>? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class TachographReminderTaskDto(
|
||||||
|
val id: String,
|
||||||
|
val type: String,
|
||||||
|
val label: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AcknowledgeTachographReminderBody(
|
||||||
|
val taskIds: List<String>,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AcknowledgeTachographReminderResponse(
|
||||||
|
val ok: Boolean,
|
||||||
|
val tachographReminder: TachographReminderDto,
|
||||||
|
)
|
||||||
|
|
||||||
data class DispatchSheetPhotoDto(
|
data class DispatchSheetPhotoDto(
|
||||||
val id: String,
|
val id: String,
|
||||||
val driverId: String,
|
val driverId: String,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import com.google.gson.Gson
|
|||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import pl.firmatpp.kierowca.data.DriverRepository
|
import pl.firmatpp.kierowca.data.DriverRepository
|
||||||
|
import pl.firmatpp.kierowca.data.rethrowIfCancellation
|
||||||
import pl.firmatpp.kierowca.data.model.BootstrapResponse
|
import pl.firmatpp.kierowca.data.model.BootstrapResponse
|
||||||
import pl.firmatpp.kierowca.data.model.DriverRouteDto
|
import pl.firmatpp.kierowca.data.model.DriverRouteDto
|
||||||
import pl.firmatpp.kierowca.data.model.RouteResponse
|
import pl.firmatpp.kierowca.data.model.RouteResponse
|
||||||
@@ -38,6 +39,7 @@ class DriverSyncRepository(
|
|||||||
saveSyncStates(response.syncState)
|
saveSyncStates(response.syncState)
|
||||||
CachedValue(response, stale = false, syncedAtEpochMillis = System.currentTimeMillis())
|
CachedValue(response, stale = false, syncedAtEpochMillis = System.currentTimeMillis())
|
||||||
}.getOrElse { throwable ->
|
}.getOrElse { throwable ->
|
||||||
|
throwable.rethrowIfCancellation()
|
||||||
cachedBootstrap(requestedDate, throwable)
|
cachedBootstrap(requestedDate, throwable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,6 +60,7 @@ class DriverSyncRepository(
|
|||||||
saveSyncStates(response.syncState)
|
saveSyncStates(response.syncState)
|
||||||
CachedValue(response, stale = false, syncedAtEpochMillis = System.currentTimeMillis())
|
CachedValue(response, stale = false, syncedAtEpochMillis = System.currentTimeMillis())
|
||||||
}.getOrElse { throwable ->
|
}.getOrElse { throwable ->
|
||||||
|
throwable.rethrowIfCancellation()
|
||||||
cachedRoute(routeId, throwable)
|
cachedRoute(routeId, throwable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -229,18 +229,28 @@ class DiagnosticSnapshotProvider(private val context: Context) {
|
|||||||
val validated = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
|
val validated = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
|
||||||
capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||||
val links = activeNetwork?.let(manager::getLinkProperties)
|
val links = activeNetwork?.let(manager::getLinkProperties)
|
||||||
|
val roaming = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
yesNo(!capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING))
|
||||||
|
} else {
|
||||||
|
"niedostępne"
|
||||||
|
}
|
||||||
|
val mtu = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
links?.mtu?.toString() ?: "brak"
|
||||||
|
} else {
|
||||||
|
"niedostępne"
|
||||||
|
}
|
||||||
return listOf(
|
return listOf(
|
||||||
DiagnosticEntry("Transport", transports.joinToString()),
|
DiagnosticEntry("Transport", transports.joinToString()),
|
||||||
DiagnosticEntry("Internet zweryfikowany", yesNo(validated), if (validated) DiagnosticSeverity.Good else DiagnosticSeverity.Error),
|
DiagnosticEntry("Internet zweryfikowany", yesNo(validated), if (validated) DiagnosticSeverity.Good else DiagnosticSeverity.Error),
|
||||||
DiagnosticEntry("NET_CAPABILITY_INTERNET", yesNo(capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET))),
|
DiagnosticEntry("NET_CAPABILITY_INTERNET", yesNo(capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET))),
|
||||||
DiagnosticEntry("NET_CAPABILITY_VALIDATED", yesNo(capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED))),
|
DiagnosticEntry("NET_CAPABILITY_VALIDATED", yesNo(capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED))),
|
||||||
DiagnosticEntry("Sieć mierzona", yesNo(!capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED))),
|
DiagnosticEntry("Sieć mierzona", yesNo(!capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED))),
|
||||||
DiagnosticEntry("Roaming", yesNo(!capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING))),
|
DiagnosticEntry("Roaming", roaming),
|
||||||
DiagnosticEntry("Pasmo downstream / upstream", "${capabilities.linkDownstreamBandwidthKbps} / ${capabilities.linkUpstreamBandwidthKbps} kb/s"),
|
DiagnosticEntry("Pasmo downstream / upstream", "${capabilities.linkDownstreamBandwidthKbps} / ${capabilities.linkUpstreamBandwidthKbps} kb/s"),
|
||||||
DiagnosticEntry("Interfejs", links?.interfaceName ?: "brak"),
|
DiagnosticEntry("Interfejs", links?.interfaceName ?: "brak"),
|
||||||
DiagnosticEntry("Adresy IP", links?.linkAddresses?.joinToString { it.toString() }?.ifBlank { "brak" } ?: "brak"),
|
DiagnosticEntry("Adresy IP", links?.linkAddresses?.joinToString { it.toString() }?.ifBlank { "brak" } ?: "brak"),
|
||||||
DiagnosticEntry("DNS", links?.dnsServers?.joinToString { it.hostAddress ?: it.toString() }?.ifBlank { "brak" } ?: "brak"),
|
DiagnosticEntry("DNS", links?.dnsServers?.joinToString { it.hostAddress ?: it.toString() }?.ifBlank { "brak" } ?: "brak"),
|
||||||
DiagnosticEntry("MTU", links?.mtu?.toString() ?: "brak"),
|
DiagnosticEntry("MTU", mtu),
|
||||||
DiagnosticEntry("Proxy", links?.httpProxy?.toString() ?: "brak"),
|
DiagnosticEntry("Proxy", links?.httpProxy?.toString() ?: "brak"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,11 @@ class LeaveRequestDecisionNotificationWorker(
|
|||||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
try {
|
||||||
NotificationManagerCompat.from(applicationContext).notify(leaveRequestId.hashCode(), notification)
|
NotificationManagerCompat.from(applicationContext).notify(leaveRequestId.hashCode(), notification)
|
||||||
|
} catch (_: SecurityException) {
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
return Result.success()
|
return Result.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,11 @@ class NewRouteNotificationWorker(
|
|||||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
try {
|
||||||
NotificationManagerCompat.from(applicationContext).notify(route.id.hashCode(), notification)
|
NotificationManagerCompat.from(applicationContext).notify(route.id.hashCode(), notification)
|
||||||
|
} catch (_: SecurityException) {
|
||||||
|
return@runCatching Result.success()
|
||||||
|
}
|
||||||
Result.success()
|
Result.success()
|
||||||
}.getOrElse { throwable ->
|
}.getOrElse { throwable ->
|
||||||
throwable.rethrowIfCancellation()
|
throwable.rethrowIfCancellation()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package pl.firmatpp.kierowca.tracking
|
package pl.firmatpp.kierowca.tracking
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
@@ -160,6 +161,7 @@ class ActiveRouteTrackingService : Service(), LocationListener {
|
|||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
private fun requestLocationUpdates() {
|
private fun requestLocationUpdates() {
|
||||||
if (!hasLocationPermission() || routeId.isNullOrBlank() || driverId.isNullOrBlank()) {
|
if (!hasLocationPermission() || routeId.isNullOrBlank() || driverId.isNullOrBlank()) {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ import androidx.compose.material.icons.outlined.Person
|
|||||||
import androidx.compose.material.icons.outlined.Phone
|
import androidx.compose.material.icons.outlined.Phone
|
||||||
import androidx.compose.material.icons.outlined.Refresh
|
import androidx.compose.material.icons.outlined.Refresh
|
||||||
import androidx.compose.material.icons.outlined.SystemUpdate
|
import androidx.compose.material.icons.outlined.SystemUpdate
|
||||||
|
import androidx.compose.material.icons.outlined.Speed
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||||
import androidx.compose.material.pullrefresh.pullRefresh
|
import androidx.compose.material.pullrefresh.pullRefresh
|
||||||
@@ -83,7 +84,10 @@ import androidx.compose.material3.Button
|
|||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.DatePicker
|
||||||
|
import androidx.compose.material3.DatePickerDialog
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@@ -93,11 +97,13 @@ import androidx.compose.material3.ModalBottomSheet
|
|||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.SelectableDates
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.material3.rememberDatePickerState
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
@@ -145,6 +151,7 @@ import java.time.DayOfWeek
|
|||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.YearMonth
|
import java.time.YearMonth
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
|
import java.time.ZoneOffset
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
@@ -159,6 +166,7 @@ import pl.firmatpp.kierowca.data.model.DriverLeaveRequestDto
|
|||||||
import pl.firmatpp.kierowca.data.model.DriverRouteDto
|
import pl.firmatpp.kierowca.data.model.DriverRouteDto
|
||||||
import pl.firmatpp.kierowca.data.model.NavigationPointDto
|
import pl.firmatpp.kierowca.data.model.NavigationPointDto
|
||||||
import pl.firmatpp.kierowca.data.model.RoutePhotoDto
|
import pl.firmatpp.kierowca.data.model.RoutePhotoDto
|
||||||
|
import pl.firmatpp.kierowca.data.model.TachographReminderDto
|
||||||
import pl.firmatpp.kierowca.data.model.driverLifecycleStatus
|
import pl.firmatpp.kierowca.data.model.driverLifecycleStatus
|
||||||
import pl.firmatpp.kierowca.data.upload.DispatchSheetUploadEntity
|
import pl.firmatpp.kierowca.data.upload.DispatchSheetUploadEntity
|
||||||
import pl.firmatpp.kierowca.data.upload.PhotoUploadEntity
|
import pl.firmatpp.kierowca.data.upload.PhotoUploadEntity
|
||||||
@@ -309,7 +317,7 @@ fun DriverApp(
|
|||||||
BackHandler(enabled = state.screen != DriverScreen.Initializing && state.screen != DriverScreen.Phone && state.screen != DriverScreen.Routes) {
|
BackHandler(enabled = state.screen != DriverScreen.Initializing && state.screen != DriverScreen.Phone && state.screen != DriverScreen.Routes) {
|
||||||
viewModel.back()
|
viewModel.back()
|
||||||
}
|
}
|
||||||
DisposableEffect(lifecycleOwner, state.screen) {
|
DisposableEffect(lifecycleOwner) {
|
||||||
val observer = LifecycleEventObserver { _, event ->
|
val observer = LifecycleEventObserver { _, event ->
|
||||||
when (event) {
|
when (event) {
|
||||||
Lifecycle.Event.ON_START -> {
|
Lifecycle.Event.ON_START -> {
|
||||||
@@ -402,6 +410,7 @@ fun DriverApp(
|
|||||||
state = state,
|
state = state,
|
||||||
onRefresh = viewModel::refreshRoutesSilently,
|
onRefresh = viewModel::refreshRoutesSilently,
|
||||||
onDate = viewModel::selectRouteDate,
|
onDate = viewModel::selectRouteDate,
|
||||||
|
onTachographReminderComplete = viewModel::acknowledgeTachographReminder,
|
||||||
onProfile = viewModel::openProfile,
|
onProfile = viewModel::openProfile,
|
||||||
onPhotoQueue = viewModel::openPhotoQueue,
|
onPhotoQueue = viewModel::openPhotoQueue,
|
||||||
onDispatchSheetUpload = viewModel::uploadDispatchSheetPhoto,
|
onDispatchSheetUpload = viewModel::uploadDispatchSheetPhoto,
|
||||||
@@ -1149,6 +1158,7 @@ private fun RoutesScreen(
|
|||||||
state: DriverUiState,
|
state: DriverUiState,
|
||||||
onRefresh: () -> Unit,
|
onRefresh: () -> Unit,
|
||||||
onDate: (String) -> Unit,
|
onDate: (String) -> Unit,
|
||||||
|
onTachographReminderComplete: (List<String>) -> Unit,
|
||||||
onProfile: () -> Unit,
|
onProfile: () -> Unit,
|
||||||
onPhotoQueue: () -> Unit,
|
onPhotoQueue: () -> Unit,
|
||||||
onDispatchSheetUpload: (Uri, String, PhotoUploadMetadata) -> Unit,
|
onDispatchSheetUpload: (Uri, String, PhotoUploadMetadata) -> Unit,
|
||||||
@@ -1159,6 +1169,8 @@ private fun RoutesScreen(
|
|||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
var cameraUri by remember { mutableStateOf<Uri?>(null) }
|
var cameraUri by remember { mutableStateOf<Uri?>(null) }
|
||||||
var showPreciseLocationPermissionDialog by remember { mutableStateOf(false) }
|
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 cameraLauncher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) { ok ->
|
||||||
val capturedUri = cameraUri
|
val capturedUri = cameraUri
|
||||||
if (
|
if (
|
||||||
@@ -1239,7 +1251,10 @@ private fun RoutesScreen(
|
|||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = TppTheme.colors.ink,
|
color = TppTheme.colors.ink,
|
||||||
)
|
)
|
||||||
DateChip(state.selectedDate)
|
DateChip(
|
||||||
|
date = state.selectedDate,
|
||||||
|
onClick = { showRouteDatePicker = true },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Row(
|
Row(
|
||||||
@@ -1253,7 +1268,10 @@ private fun RoutesScreen(
|
|||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = TppTheme.colors.ink,
|
color = TppTheme.colors.ink,
|
||||||
)
|
)
|
||||||
DateChip(state.selectedDate)
|
DateChip(
|
||||||
|
date = state.selectedDate,
|
||||||
|
onClick = { showRouteDatePicker = true },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1265,6 +1283,15 @@ private fun RoutesScreen(
|
|||||||
onDate = onDate,
|
onDate = onDate,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (shouldShowTachographReminderCard(state.tachographReminder)) {
|
||||||
|
item {
|
||||||
|
TachographReminderCard(
|
||||||
|
reminder = state.tachographReminder,
|
||||||
|
submitting = state.tachographReminderSubmitting,
|
||||||
|
onConfirm = { showTachographConfirmation = true },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
if (shouldShowDispatchSheetReminderCard(state.dispatchSheetReminder)) {
|
if (shouldShowDispatchSheetReminderCard(state.dispatchSheetReminder)) {
|
||||||
item {
|
item {
|
||||||
DispatchSheetReminderCard(
|
DispatchSheetReminderCard(
|
||||||
@@ -1333,6 +1360,31 @@ private fun RoutesScreen(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showRouteDatePicker) {
|
||||||
|
RouteDatePickerDialog(
|
||||||
|
selectedDate = state.selectedDate,
|
||||||
|
minDate = state.minRouteDate,
|
||||||
|
maxDate = state.maxRouteDate,
|
||||||
|
onDismiss = { showRouteDatePicker = false },
|
||||||
|
onDateSelected = { date ->
|
||||||
|
showRouteDatePicker = false
|
||||||
|
onDate(date)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showTachographConfirmation) {
|
||||||
|
TachographReminderConfirmationDialog(
|
||||||
|
reminder = state.tachographReminder,
|
||||||
|
submitting = state.tachographReminderSubmitting,
|
||||||
|
onDismiss = { showTachographConfirmation = false },
|
||||||
|
onConfirm = { taskIds ->
|
||||||
|
showTachographConfirmation = false
|
||||||
|
onTachographReminderComplete(taskIds)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -2226,6 +2278,171 @@ private fun DispatchSheetReminderCard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TachographReminderCard(
|
||||||
|
reminder: TachographReminderDto?,
|
||||||
|
submitting: Boolean,
|
||||||
|
onConfirm: () -> Unit,
|
||||||
|
) {
|
||||||
|
if (!shouldShowTachographReminderCard(reminder)) return
|
||||||
|
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(containerColor = TppTheme.colors.warningContainer),
|
||||||
|
border = BorderStroke(1.dp, TppTheme.colors.warningOutline),
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
Modifier.fillMaxWidth().padding(18.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||||
|
verticalAlignment = Alignment.Top,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(44.dp)
|
||||||
|
.background(Color.White.copy(alpha = 0.72f), RoundedCornerShape(12.dp)),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Outlined.Speed,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = Color(0xFF7A5200),
|
||||||
|
modifier = Modifier.size(27.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(5.dp)) {
|
||||||
|
Text(
|
||||||
|
reminder?.title.orEmpty(),
|
||||||
|
color = TppTheme.colors.ink,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
reminder?.message.orEmpty(),
|
||||||
|
color = Color(0xFF5F4200),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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<String>) -> 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
|
@Composable
|
||||||
private fun OperationalQueueBanner(state: DriverUiState, onPhotoQueue: () -> Unit) {
|
private fun OperationalQueueBanner(state: DriverUiState, onPhotoQueue: () -> Unit) {
|
||||||
val count = state.pendingOperationalItems
|
val count = state.pendingOperationalItems
|
||||||
@@ -2429,19 +2646,120 @@ private fun StitchHeader(height: Dp = 124.dp) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DateChip(date: String) {
|
private fun DateChip(date: String, onClick: () -> Unit) {
|
||||||
val formatter = remember { DateTimeFormatter.ofPattern("d MMM", Locale("pl", "PL")) }
|
val formatter = remember { DateTimeFormatter.ofPattern("d MMM", Locale("pl", "PL")) }
|
||||||
val label = remember(date) { LocalDate.parse(date).format(formatter).replace(".", "").replaceFirstChar { it.uppercase() } }
|
val label = remember(date) { LocalDate.parse(date).format(formatter).replace(".", "").replaceFirstChar { it.uppercase() } }
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.background(TppTheme.colors.panel, RoundedCornerShape(12.dp)).padding(horizontal = 16.dp, vertical = 12.dp),
|
modifier = Modifier
|
||||||
|
.background(TppTheme.colors.panel, RoundedCornerShape(12.dp))
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Outlined.CalendarToday, contentDescription = null, tint = TppTheme.colors.forest)
|
Icon(
|
||||||
|
Icons.Outlined.CalendarToday,
|
||||||
|
contentDescription = "Otwórz kalendarz",
|
||||||
|
tint = TppTheme.colors.forest,
|
||||||
|
)
|
||||||
Text(label, fontFamily = FontFamily.Monospace, color = TppTheme.colors.muted, fontWeight = FontWeight.SemiBold)
|
Text(label, fontFamily = FontFamily.Monospace, color = TppTheme.colors.muted, fontWeight = FontWeight.SemiBold)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun RouteDatePickerDialog(
|
||||||
|
selectedDate: String,
|
||||||
|
minDate: String,
|
||||||
|
maxDate: String,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onDateSelected: (String) -> Unit,
|
||||||
|
) {
|
||||||
|
val bounds = remember(selectedDate, minDate, maxDate) {
|
||||||
|
routeDatePickerBounds(
|
||||||
|
selectedDate = selectedDate,
|
||||||
|
minDate = minDate,
|
||||||
|
maxDate = maxDate,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val selectableDates = remember(bounds) {
|
||||||
|
object : SelectableDates {
|
||||||
|
override fun isSelectableDate(utcTimeMillis: Long): Boolean {
|
||||||
|
val date = Instant.ofEpochMilli(utcTimeMillis).atZone(ZoneOffset.UTC).toLocalDate()
|
||||||
|
return date in bounds.minDate..bounds.maxDate
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isSelectableYear(year: Int): Boolean =
|
||||||
|
year in bounds.minDate.year..bounds.maxDate.year
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val datePickerState = rememberDatePickerState(
|
||||||
|
initialSelectedDateMillis = bounds.selectedDate
|
||||||
|
.atStartOfDay(ZoneOffset.UTC)
|
||||||
|
.toInstant()
|
||||||
|
.toEpochMilli(),
|
||||||
|
yearRange = bounds.minDate.year..bounds.maxDate.year,
|
||||||
|
selectableDates = selectableDates,
|
||||||
|
)
|
||||||
|
|
||||||
|
DatePickerDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
datePickerState.selectedDateMillis
|
||||||
|
?.let { Instant.ofEpochMilli(it).atZone(ZoneOffset.UTC).toLocalDate() }
|
||||||
|
?.takeIf { it in bounds.minDate..bounds.maxDate }
|
||||||
|
?.let { onDateSelected(it.toString()) }
|
||||||
|
},
|
||||||
|
enabled = datePickerState.selectedDateMillis != null,
|
||||||
|
) {
|
||||||
|
Text("Wybierz")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text("Anuluj")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
DatePicker(
|
||||||
|
state = datePickerState,
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
"Wybierz dzień kursów",
|
||||||
|
modifier = Modifier.padding(start = 24.dp, end = 24.dp, top = 16.dp),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
showModeToggle = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal data class RouteDatePickerBounds(
|
||||||
|
val selectedDate: LocalDate,
|
||||||
|
val minDate: LocalDate,
|
||||||
|
val maxDate: LocalDate,
|
||||||
|
)
|
||||||
|
|
||||||
|
internal fun routeDatePickerBounds(
|
||||||
|
selectedDate: String,
|
||||||
|
minDate: String,
|
||||||
|
maxDate: String,
|
||||||
|
): RouteDatePickerBounds {
|
||||||
|
val selected = runCatching { LocalDate.parse(selectedDate) }.getOrDefault(LocalDate.now())
|
||||||
|
val parsedMin = runCatching { LocalDate.parse(minDate) }.getOrDefault(selected)
|
||||||
|
val parsedMax = runCatching { LocalDate.parse(maxDate) }.getOrDefault(selected)
|
||||||
|
val rangeMin = minOf(parsedMin, parsedMax)
|
||||||
|
val rangeMax = maxOf(parsedMin, parsedMax)
|
||||||
|
|
||||||
|
return RouteDatePickerBounds(
|
||||||
|
selectedDate = selected.coerceIn(rangeMin, rangeMax),
|
||||||
|
minDate = rangeMin,
|
||||||
|
maxDate = rangeMax,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RouteDateSelector(selectedDate: String, minDate: String, maxDate: String, onDate: (String) -> Unit) {
|
private fun RouteDateSelector(selectedDate: String, minDate: String, maxDate: String, onDate: (String) -> Unit) {
|
||||||
val dates = remember(minDate, maxDate) {
|
val dates = remember(minDate, maxDate) {
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import kotlin.math.abs
|
|||||||
import pl.firmatpp.kierowca.data.model.DriverRouteDto
|
import pl.firmatpp.kierowca.data.model.DriverRouteDto
|
||||||
import pl.firmatpp.kierowca.data.model.DispatchSheetReminderDto
|
import pl.firmatpp.kierowca.data.model.DispatchSheetReminderDto
|
||||||
import pl.firmatpp.kierowca.data.model.RoutePhotoDto
|
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.model.driverLifecycleStatus
|
||||||
import pl.firmatpp.kierowca.data.upload.PhotoUploadEntity
|
import pl.firmatpp.kierowca.data.upload.PhotoUploadEntity
|
||||||
import pl.firmatpp.kierowca.data.upload.PhotoUploadStatus
|
import pl.firmatpp.kierowca.data.upload.PhotoUploadStatus
|
||||||
@@ -671,6 +673,16 @@ fun canLaunchCameraWithLocationPolicy(requirePreciseLocation: Boolean, hasFineLo
|
|||||||
fun shouldShowDispatchSheetReminderCard(reminder: DispatchSheetReminderDto?): Boolean =
|
fun shouldShowDispatchSheetReminderCard(reminder: DispatchSheetReminderDto?): Boolean =
|
||||||
reminder?.dueToday == true && reminder.status in setOf("missing", "uploaded")
|
reminder?.dueToday == true && reminder.status in setOf("missing", "uploaded")
|
||||||
|
|
||||||
|
fun shouldShowTachographReminderCard(reminder: TachographReminderDto?): Boolean =
|
||||||
|
reminder?.enabled == true &&
|
||||||
|
reminder.visible &&
|
||||||
|
!reminder.title.isNullOrBlank() &&
|
||||||
|
!reminder.message.isNullOrBlank() &&
|
||||||
|
!reminder.tasks.isNullOrEmpty()
|
||||||
|
|
||||||
|
fun defaultTachographConfirmationSelection(tasks: List<TachographReminderTaskDto>): Set<String> =
|
||||||
|
if (tasks.size == 1) setOf(tasks.single().id) else emptySet()
|
||||||
|
|
||||||
fun dispatchSheetPrimaryActionLabel(reminder: DispatchSheetReminderDto?, hasQueuedUpload: Boolean): String =
|
fun dispatchSheetPrimaryActionLabel(reminder: DispatchSheetReminderDto?, hasQueuedUpload: Boolean): String =
|
||||||
when {
|
when {
|
||||||
hasQueuedUpload -> "Wysyłam zdjęcie..."
|
hasQueuedUpload -> "Wysyłam zdjęcie..."
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import pl.firmatpp.kierowca.data.ApiErrorMapper
|
|||||||
import pl.firmatpp.kierowca.data.ApiErrorKind
|
import pl.firmatpp.kierowca.data.ApiErrorKind
|
||||||
import pl.firmatpp.kierowca.data.DriverRepository
|
import pl.firmatpp.kierowca.data.DriverRepository
|
||||||
import pl.firmatpp.kierowca.data.PhotoUploadMetadata
|
import pl.firmatpp.kierowca.data.PhotoUploadMetadata
|
||||||
|
import pl.firmatpp.kierowca.data.rethrowIfCancellation
|
||||||
import pl.firmatpp.kierowca.data.sync.DriverSyncRepository
|
import pl.firmatpp.kierowca.data.sync.DriverSyncRepository
|
||||||
import pl.firmatpp.kierowca.data.sync.NetworkMonitor
|
import pl.firmatpp.kierowca.data.sync.NetworkMonitor
|
||||||
import pl.firmatpp.kierowca.data.sync.StartupOfflineFallbackLoader
|
import pl.firmatpp.kierowca.data.sync.StartupOfflineFallbackLoader
|
||||||
@@ -33,6 +34,7 @@ import pl.firmatpp.kierowca.data.model.DriverLeaveCalendarEntryDto
|
|||||||
import pl.firmatpp.kierowca.data.model.DriverLeaveRequestDto
|
import pl.firmatpp.kierowca.data.model.DriverLeaveRequestDto
|
||||||
import pl.firmatpp.kierowca.data.model.DriverRouteDto
|
import pl.firmatpp.kierowca.data.model.DriverRouteDto
|
||||||
import pl.firmatpp.kierowca.data.model.RoutePhotoDto
|
import pl.firmatpp.kierowca.data.model.RoutePhotoDto
|
||||||
|
import pl.firmatpp.kierowca.data.model.TachographReminderDto
|
||||||
import pl.firmatpp.kierowca.data.model.driverLifecycleStatus
|
import pl.firmatpp.kierowca.data.model.driverLifecycleStatus
|
||||||
import pl.firmatpp.kierowca.data.upload.DispatchSheetUploadEntity
|
import pl.firmatpp.kierowca.data.upload.DispatchSheetUploadEntity
|
||||||
import pl.firmatpp.kierowca.data.upload.DispatchSheetUploadOutbox
|
import pl.firmatpp.kierowca.data.upload.DispatchSheetUploadOutbox
|
||||||
@@ -102,6 +104,8 @@ data class DriverUiState(
|
|||||||
val photoUploads: List<PhotoUploadEntity> = emptyList(),
|
val photoUploads: List<PhotoUploadEntity> = emptyList(),
|
||||||
val queuedPhotoUploads: List<PhotoUploadEntity> = emptyList(),
|
val queuedPhotoUploads: List<PhotoUploadEntity> = emptyList(),
|
||||||
val dispatchSheetReminder: DispatchSheetReminderDto? = null,
|
val dispatchSheetReminder: DispatchSheetReminderDto? = null,
|
||||||
|
val tachographReminder: TachographReminderDto? = null,
|
||||||
|
val tachographReminderSubmitting: Boolean = false,
|
||||||
val appUpdate: AppUpdateDto? = null,
|
val appUpdate: AppUpdateDto? = null,
|
||||||
val dispatchSheetUploads: List<DispatchSheetUploadEntity> = emptyList(),
|
val dispatchSheetUploads: List<DispatchSheetUploadEntity> = emptyList(),
|
||||||
val leaveRequestsConfig: LeaveRequestsConfig? = null,
|
val leaveRequestsConfig: LeaveRequestsConfig? = null,
|
||||||
@@ -199,6 +203,30 @@ internal fun DriverUiState.withLoadedLeaveRequests(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun shouldStartRoutesLoad(
|
||||||
|
screen: DriverScreen,
|
||||||
|
navigateToRoutes: Boolean,
|
||||||
|
startupOfflineOnly: Boolean,
|
||||||
|
startupLoadInProgress: Boolean = false,
|
||||||
|
): Boolean =
|
||||||
|
(!startupLoadInProgress || navigateToRoutes || startupOfflineOnly) &&
|
||||||
|
(screen != DriverScreen.Initializing || navigateToRoutes || startupOfflineOnly)
|
||||||
|
|
||||||
|
internal fun screenAfterBootstrap(
|
||||||
|
currentScreen: DriverScreen,
|
||||||
|
navigateToRoutes: Boolean,
|
||||||
|
leaveRequestsEnabled: Boolean,
|
||||||
|
): DriverScreen = when {
|
||||||
|
currentScreen == DriverScreen.Initializing -> DriverScreen.Routes
|
||||||
|
!leaveRequestsEnabled && currentScreen in setOf(
|
||||||
|
DriverScreen.LeaveRequests,
|
||||||
|
DriverScreen.LeaveRequestDetail,
|
||||||
|
DriverScreen.AddLeaveRequest,
|
||||||
|
) -> DriverScreen.Routes
|
||||||
|
navigateToRoutes -> DriverScreen.Routes
|
||||||
|
else -> currentScreen
|
||||||
|
}
|
||||||
|
|
||||||
class DriverViewModel(application: Application) : AndroidViewModel(application) {
|
class DriverViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
private val appPreferencesStore = AppPreferencesStore(application)
|
private val appPreferencesStore = AppPreferencesStore(application)
|
||||||
private val repository = DriverRepository(application)
|
private val repository = DriverRepository(application)
|
||||||
@@ -233,6 +261,7 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
private var routesRequestGeneration: Long = 0
|
private var routesRequestGeneration: Long = 0
|
||||||
private var routeDetailRequestGeneration: Long = 0
|
private var routeDetailRequestGeneration: Long = 0
|
||||||
private var routesLoadJob: Job? = null
|
private var routesLoadJob: Job? = null
|
||||||
|
private var startupLoadInProgress: Boolean = false
|
||||||
val state: StateFlow<DriverUiState> = _state
|
val state: StateFlow<DriverUiState> = _state
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -470,6 +499,39 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
loadRoutes(date = date, showLoading = true, navigateToRoutes = true)
|
loadRoutes(date = date, showLoading = true, navigateToRoutes = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun acknowledgeTachographReminder(taskIds: List<String>) {
|
||||||
|
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() {
|
fun dismissRouteDayLiveUpdate() {
|
||||||
_state.update { it.copy(routeDayLiveUpdateMessage = null) }
|
_state.update { it.copy(routeDayLiveUpdateMessage = null) }
|
||||||
}
|
}
|
||||||
@@ -480,24 +542,35 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
navigateToRoutes: Boolean,
|
navigateToRoutes: Boolean,
|
||||||
startupOfflineOnly: Boolean = false,
|
startupOfflineOnly: Boolean = false,
|
||||||
) {
|
) {
|
||||||
|
val requestSnapshot = _state.value
|
||||||
|
if (!shouldStartRoutesLoad(
|
||||||
|
screen = requestSnapshot.screen,
|
||||||
|
navigateToRoutes = navigateToRoutes,
|
||||||
|
startupOfflineOnly = startupOfflineOnly,
|
||||||
|
startupLoadInProgress = startupLoadInProgress,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val isStartup = requestSnapshot.screen == DriverScreen.Initializing
|
||||||
|
if (isStartup) startupLoadInProgress = true
|
||||||
val generation = ++routesRequestGeneration
|
val generation = ++routesRequestGeneration
|
||||||
routesLoadJob?.cancel()
|
routesLoadJob?.cancel()
|
||||||
routesLoadJob = viewModelScope.launch {
|
val loadJob = viewModelScope.launch {
|
||||||
val isStartup = _state.value.screen == DriverScreen.Initializing
|
|
||||||
_state.update { it.copy(loading = showLoading, refreshing = !showLoading, feedback = null, error = null) }
|
_state.update { it.copy(loading = showLoading, refreshing = !showLoading, feedback = null, error = null) }
|
||||||
|
|
||||||
runCatching {
|
runCatching {
|
||||||
val cached = when {
|
val cached = when {
|
||||||
startupOfflineOnly -> syncRepository.bootstrapFromCache(date)
|
startupOfflineOnly -> syncRepository.bootstrapFromCache(date)
|
||||||
?: throw IOException("Brak zapisanych danych dla wybranego dnia.")
|
?: throw IOException("Brak zapisanych danych dla wybranego dnia.")
|
||||||
!_state.value.isOnline -> syncRepository.bootstrapFromCache(date)
|
|
||||||
?: throw IOException("Brak zapisanych danych dla wybranego dnia.")
|
|
||||||
isStartup -> {
|
isStartup -> {
|
||||||
startupOfflineFallbackLoader.load(
|
startupOfflineFallbackLoader.load(
|
||||||
onlineLoad = { syncRepository.bootstrap(date) },
|
onlineLoad = { syncRepository.bootstrap(date) },
|
||||||
offlineLoad = { syncRepository.bootstrapFromCache(date) },
|
offlineLoad = { syncRepository.bootstrapFromCache(date) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
!_state.value.isOnline -> syncRepository.bootstrapFromCache(date)
|
||||||
|
?: throw IOException("Brak zapisanych danych dla wybranego dnia.")
|
||||||
else -> syncRepository.bootstrap(date)
|
else -> syncRepository.bootstrap(date)
|
||||||
}
|
}
|
||||||
val response = cached.value
|
val response = cached.value
|
||||||
@@ -509,6 +582,7 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
driver = response.session.driver,
|
driver = response.session.driver,
|
||||||
routes = response.routes.today,
|
routes = response.routes.today,
|
||||||
dispatchSheetReminder = response.dispatchSheetReminder,
|
dispatchSheetReminder = response.dispatchSheetReminder,
|
||||||
|
tachographReminder = response.tachographReminder,
|
||||||
appUpdate = response.appUpdate ?: it.appUpdate,
|
appUpdate = response.appUpdate ?: it.appUpdate,
|
||||||
selectedDate = settings?.selectedDate ?: date ?: it.selectedDate,
|
selectedDate = settings?.selectedDate ?: date ?: it.selectedDate,
|
||||||
minRouteDate = settings?.minDate ?: it.minRouteDate,
|
minRouteDate = settings?.minDate ?: it.minRouteDate,
|
||||||
@@ -526,10 +600,11 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
leaveRequestsConfig = settings?.leaveRequests?.let { config ->
|
leaveRequestsConfig = settings?.leaveRequests?.let { config ->
|
||||||
LeaveRequestsConfig(enabled = config.enabled, types = config.types)
|
LeaveRequestsConfig(enabled = config.enabled, types = config.types)
|
||||||
},
|
},
|
||||||
screen = if (
|
screen = screenAfterBootstrap(
|
||||||
settings?.leaveRequests?.enabled != true &&
|
currentScreen = it.screen,
|
||||||
it.screen in setOf(DriverScreen.LeaveRequests, DriverScreen.LeaveRequestDetail, DriverScreen.AddLeaveRequest)
|
navigateToRoutes = navigateToRoutes,
|
||||||
) DriverScreen.Routes else if (navigateToRoutes) DriverScreen.Routes else it.screen,
|
leaveRequestsEnabled = settings?.leaveRequests?.enabled == true,
|
||||||
|
),
|
||||||
notifyNewRoutes = response.notificationPreferences?.notifyNewRoutes ?: it.notifyNewRoutes,
|
notifyNewRoutes = response.notificationPreferences?.notifyNewRoutes ?: it.notifyNewRoutes,
|
||||||
imageAuthHeader = repository.imageAuthHeader(),
|
imageAuthHeader = repository.imageAuthHeader(),
|
||||||
isStale = cached.stale || !it.isOnline,
|
isStale = cached.stale || !it.isOnline,
|
||||||
@@ -556,6 +631,7 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
reconcileTrackingService(response.routes.today, response.session.driver)
|
reconcileTrackingService(response.routes.today, response.session.driver)
|
||||||
}
|
}
|
||||||
}.onFailure { throwable ->
|
}.onFailure { throwable ->
|
||||||
|
throwable.rethrowIfCancellation()
|
||||||
if (generation != routesRequestGeneration) return@onFailure
|
if (generation != routesRequestGeneration) return@onFailure
|
||||||
reportHandledException("load_routes", throwable, mapOf("date" to date))
|
reportHandledException("load_routes", throwable, mapOf("date" to date))
|
||||||
if (ApiErrorMapper.map(throwable).kind == ApiErrorKind.Auth) {
|
if (ApiErrorMapper.map(throwable).kind == ApiErrorKind.Auth) {
|
||||||
@@ -573,6 +649,14 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
it.copy(loading = false, refreshing = false)
|
it.copy(loading = false, refreshing = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
routesLoadJob = loadJob
|
||||||
|
if (isStartup) {
|
||||||
|
loadJob.invokeOnCompletion {
|
||||||
|
if (generation == routesRequestGeneration) {
|
||||||
|
startupLoadInProgress = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun openRoute(routeId: String) = openRouteWithTarget(routeId, DriverScreen.Detail)
|
fun openRoute(routeId: String) = openRouteWithTarget(routeId, DriverScreen.Detail)
|
||||||
@@ -1773,7 +1857,12 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun checkRemoteSyncState() {
|
private suspend fun checkRemoteSyncState() {
|
||||||
if (!_state.value.isOnline) return
|
val initialSnapshot = _state.value
|
||||||
|
if (
|
||||||
|
!initialSnapshot.isOnline ||
|
||||||
|
initialSnapshot.screen == DriverScreen.Initializing ||
|
||||||
|
initialSnapshot.driver == null
|
||||||
|
) return
|
||||||
syncCheckMutex.withLock {
|
syncCheckMutex.withLock {
|
||||||
syncHintMutex.withLock { highestHintVersions.clear() }
|
syncHintMutex.withLock { highestHintVersions.clear() }
|
||||||
val snapshot = _state.value
|
val snapshot = _state.value
|
||||||
@@ -1820,6 +1909,8 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun handleSyncHint(hint: DriverSyncHint) {
|
private suspend fun handleSyncHint(hint: DriverSyncHint) {
|
||||||
|
val initialSnapshot = _state.value
|
||||||
|
if (initialSnapshot.screen == DriverScreen.Initializing || initialSnapshot.driver == null) return
|
||||||
syncHintMutex.withLock {
|
syncHintMutex.withLock {
|
||||||
val hintKey = listOf(hint.scope, hint.date ?: "-", hint.routeId ?: "-").joinToString(":")
|
val hintKey = listOf(hint.scope, hint.date ?: "-", hint.routeId ?: "-").joinToString(":")
|
||||||
val previousVersion = highestHintVersions[hintKey]
|
val previousVersion = highestHintVersions[hintKey]
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package pl.firmatpp.kierowca.ui
|
package pl.firmatpp.kierowca.ui
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.location.Location
|
import android.location.Location
|
||||||
@@ -39,6 +40,7 @@ internal suspend fun currentPhotoLocation(context: Context): Location? {
|
|||||||
return bestLastKnownLocation(manager)
|
return bestLastKnownLocation(manager)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
private suspend fun LocationManager.awaitSingleLocation(
|
private suspend fun LocationManager.awaitSingleLocation(
|
||||||
context: Context,
|
context: Context,
|
||||||
provider: String,
|
provider: String,
|
||||||
@@ -75,6 +77,7 @@ private fun hasAnyLocationPermission(context: Context): Boolean =
|
|||||||
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED ||
|
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED ||
|
||||||
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
|
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
private fun bestLastKnownLocation(manager: LocationManager): Location? {
|
private fun bestLastKnownLocation(manager: LocationManager): Location? {
|
||||||
val providers = listOf(
|
val providers = listOf(
|
||||||
LocationManager.GPS_PROVIDER,
|
LocationManager.GPS_PROVIDER,
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import org.junit.Test
|
|||||||
import pl.firmatpp.kierowca.data.model.DriverRouteDto
|
import pl.firmatpp.kierowca.data.model.DriverRouteDto
|
||||||
import pl.firmatpp.kierowca.data.model.DispatchSheetReminderDto
|
import pl.firmatpp.kierowca.data.model.DispatchSheetReminderDto
|
||||||
import pl.firmatpp.kierowca.data.model.RoutePhotoDto
|
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.PhotoUploadEntity
|
||||||
import pl.firmatpp.kierowca.data.upload.RouteActionEntity
|
import pl.firmatpp.kierowca.data.upload.RouteActionEntity
|
||||||
import pl.firmatpp.kierowca.data.upload.RouteActionStatus
|
import pl.firmatpp.kierowca.data.upload.RouteActionStatus
|
||||||
@@ -410,6 +412,57 @@ class DriverUiRulesTest {
|
|||||||
assertFalse(shouldShowDispatchSheetReminderCard(null))
|
assertFalse(shouldShowDispatchSheetReminderCard(null))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showsTachographCardOnlyForCompleteReminderEnabledByBackend() {
|
||||||
|
assertTrue(
|
||||||
|
shouldShowTachographReminderCard(
|
||||||
|
TachographReminderDto(
|
||||||
|
enabled = true,
|
||||||
|
visible = true,
|
||||||
|
title = "Odczyt tachografu",
|
||||||
|
message = "Pamiętaj o odczycie.",
|
||||||
|
tasks = listOf(tachographTask()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assertFalse(
|
||||||
|
shouldShowTachographReminderCard(
|
||||||
|
TachographReminderDto(
|
||||||
|
enabled = false,
|
||||||
|
visible = true,
|
||||||
|
title = "Odczyt tachografu",
|
||||||
|
message = "Pamiętaj o odczycie.",
|
||||||
|
tasks = listOf(tachographTask()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assertFalse(
|
||||||
|
shouldShowTachographReminderCard(
|
||||||
|
TachographReminderDto(
|
||||||
|
enabled = true,
|
||||||
|
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<String>(), defaultTachographConfirmationSelection(listOf(driverCard, vehicle)))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun explainsOfflineStateWithoutCachedSync() {
|
fun explainsOfflineStateWithoutCachedSync() {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
@@ -817,4 +870,11 @@ class DriverUiRulesTest {
|
|||||||
photo = null,
|
photo = null,
|
||||||
canUpload = dueToday,
|
canUpload = dueToday,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private fun tachographTask(): TachographReminderTaskDto =
|
||||||
|
TachographReminderTaskDto(
|
||||||
|
id = "driver-card:1:2026-07-31",
|
||||||
|
type = "driver_card",
|
||||||
|
label = "Karta kierowcy",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,55 @@ class DriverUiStateTest {
|
|||||||
assertEquals(ServerConnectionState.Degraded, failed.serverConnectionState)
|
assertEquals(ServerConnectionState.Degraded, failed.serverConnectionState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun silentRefreshCannotReplaceActiveStartupLoad() {
|
||||||
|
assertFalse(
|
||||||
|
shouldStartRoutesLoad(
|
||||||
|
screen = DriverScreen.Initializing,
|
||||||
|
navigateToRoutes = false,
|
||||||
|
startupOfflineOnly = false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assertFalse(
|
||||||
|
shouldStartRoutesLoad(
|
||||||
|
screen = DriverScreen.Routes,
|
||||||
|
navigateToRoutes = false,
|
||||||
|
startupOfflineOnly = false,
|
||||||
|
startupLoadInProgress = true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun startupAndManualOfflineLoadsRemainAvailable() {
|
||||||
|
assertTrue(
|
||||||
|
shouldStartRoutesLoad(
|
||||||
|
screen = DriverScreen.Initializing,
|
||||||
|
navigateToRoutes = true,
|
||||||
|
startupOfflineOnly = false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assertTrue(
|
||||||
|
shouldStartRoutesLoad(
|
||||||
|
screen = DriverScreen.Initializing,
|
||||||
|
navigateToRoutes = false,
|
||||||
|
startupOfflineOnly = true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun successfulBootstrapAlwaysLeavesInitializingScreen() {
|
||||||
|
assertEquals(
|
||||||
|
DriverScreen.Routes,
|
||||||
|
screenAfterBootstrap(
|
||||||
|
currentScreen = DriverScreen.Initializing,
|
||||||
|
navigateToRoutes = false,
|
||||||
|
leaveRequestsEnabled = true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun refreshingLeaveRequestsKeepsDetailScreenOpenAndUpdatesSelectedRequest() {
|
fun refreshingLeaveRequestsKeepsDetailScreenOpenAndUpdatesSelectedRequest() {
|
||||||
val state = DriverUiState(
|
val state = DriverUiState(
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package pl.firmatpp.kierowca.ui
|
||||||
|
|
||||||
|
import java.time.LocalDate
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class RouteDatePickerBoundsTest {
|
||||||
|
@Test
|
||||||
|
fun usesTheSameMinAndMaxDatesAsTheRouteSelector() {
|
||||||
|
val bounds = routeDatePickerBounds(
|
||||||
|
selectedDate = "2026-07-24",
|
||||||
|
minDate = "2026-07-17",
|
||||||
|
maxDate = "2026-07-26",
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(LocalDate.parse("2026-07-24"), bounds.selectedDate)
|
||||||
|
assertEquals(LocalDate.parse("2026-07-17"), bounds.minDate)
|
||||||
|
assertEquals(LocalDate.parse("2026-07-26"), bounds.maxDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun clampsSelectedDateToAllowedRange() {
|
||||||
|
val bounds = routeDatePickerBounds(
|
||||||
|
selectedDate = "2026-07-30",
|
||||||
|
minDate = "2026-07-17",
|
||||||
|
maxDate = "2026-07-26",
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(LocalDate.parse("2026-07-26"), bounds.selectedDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun normalizesAccidentallyReversedServerBounds() {
|
||||||
|
val bounds = routeDatePickerBounds(
|
||||||
|
selectedDate = "2026-07-24",
|
||||||
|
minDate = "2026-07-26",
|
||||||
|
maxDate = "2026-07-17",
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(LocalDate.parse("2026-07-17"), bounds.minDate)
|
||||||
|
assertEquals(LocalDate.parse("2026-07-26"), bounds.maxDate)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "8.7.3"
|
agp = "8.10.1"
|
||||||
kotlin = "2.1.10"
|
kotlin = "2.1.10"
|
||||||
composeBom = "2026.03.00"
|
composeBom = "2026.03.00"
|
||||||
activityCompose = "1.9.3"
|
activityCompose = "1.9.3"
|
||||||
|
|||||||
Reference in New Issue
Block a user