Compare commits

...
1 Commits
Author SHA1 Message Date
admin b034384ab2 Dodaj nawigacje i odswiezanie szczegolow trasy 2026-06-30 22:16:49 +02:00
4 changed files with 122 additions and 51 deletions
+2 -2
View File
@@ -12,8 +12,8 @@ android {
applicationId = "pl.firmatpp.kierowca"
minSdk = 26
targetSdk = 35
versionCode = 9
versionName = "1.0.8"
versionCode = 10
versionName = "1.0.9"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildConfigField("String", "API_BASE_URL", "\"https://api-intranet.firmatpp.pl/api/\"")
@@ -69,15 +69,25 @@ data class DriverRouteDto(
val distanceKm: Double?,
val notes: String?,
val truck: TruckDto?,
val originNavigation: NavigationPointDto? = null,
val destinationNavigation: NavigationPointDto? = null,
val photos: List<RoutePhotoDto> = emptyList(),
)
data class TruckDto(
val id: String,
val registration: String?,
val trailerRegistration: String? = null,
val name: String?,
)
data class NavigationPointDto(
val id: String,
val label: String?,
val latitude: Double?,
val longitude: Double?,
)
data class RoutePhotoDto(
val id: String,
val routeId: String,
@@ -53,6 +53,7 @@ import androidx.compose.material.icons.outlined.CalendarToday
import androidx.compose.material.icons.outlined.Factory
import androidx.compose.material.icons.outlined.LocationOn
import androidx.compose.material.icons.outlined.LocalShipping
import androidx.compose.material.icons.outlined.Navigation
import androidx.compose.material.icons.outlined.Person
import androidx.compose.material.icons.outlined.Phone
import androidx.compose.material.ExperimentalMaterialApi
@@ -114,6 +115,7 @@ import java.util.Locale
import pl.firmatpp.kierowca.R
import pl.firmatpp.kierowca.data.PhotoUploadMetadata
import pl.firmatpp.kierowca.data.model.DriverRouteDto
import pl.firmatpp.kierowca.data.model.NavigationPointDto
import pl.firmatpp.kierowca.data.model.RoutePhotoDto
import pl.firmatpp.kierowca.domain.OtpCodeExtractor
import pl.firmatpp.kierowca.domain.RouteDisplayMapper
@@ -168,7 +170,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)
DriverScreen.Detail -> DetailScreen(state, viewModel::back, viewModel::uploadPhoto, viewModel::openPhoto, viewModel::refreshSelectedRoute)
DriverScreen.Photo -> PhotoScreen(state, viewModel::back)
}
@@ -566,16 +568,18 @@ private fun routeStatusColor(status: String): Color {
}
}
@OptIn(ExperimentalMaterial3Api::class)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@Composable
private fun DetailScreen(
state: DriverUiState,
onBack: () -> Unit,
onUpload: (Uri, String, PhotoUploadMetadata) -> Unit,
onPhoto: (RoutePhotoDto) -> Unit,
onRefresh: () -> Unit,
) {
val context = LocalContext.current
var cameraUri by remember { mutableStateOf<Uri?>(null) }
val pullRefreshState = rememberPullRefreshState(state.refreshing, onRefresh)
val cameraLauncher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) { ok ->
val capturedUri = cameraUri
if (ok && capturedUri != null) onUpload(capturedUri, "camera", cameraPhotoMetadata(context))
@@ -598,34 +602,43 @@ private fun DetailScreen(
containerColor = TppColors.Surface,
) { padding ->
if (route == null) return@Scaffold
LazyColumn(
Modifier.fillMaxSize().padding(padding),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(24.dp),
) {
item { ManifestSection(route) }
item {
CargoDocumentationSection(
photos = route.photos,
imageAuthHeader = state.imageAuthHeader,
allowGalleryUploads = state.allowGalleryUploads,
onPhoto = onPhoto,
onCamera = {
val newUri = createCameraUri(context)
cameraUri = newUri
val missingPermissions = cameraCapturePermissions(context)
if (missingPermissions.isEmpty()) {
cameraLauncher.launch(newUri)
} else {
cameraPermissionLauncher.launch(missingPermissions)
}
},
onGallery = {
pickerLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
},
)
Box(Modifier.fillMaxSize().padding(padding).pullRefresh(pullRefreshState)) {
LazyColumn(
Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(24.dp),
) {
item { ManifestSection(route, onNavigate = { openNavigation(context, it) }) }
item {
CargoDocumentationSection(
photos = route.photos,
imageAuthHeader = state.imageAuthHeader,
allowGalleryUploads = state.allowGalleryUploads,
onPhoto = onPhoto,
onCamera = {
val newUri = createCameraUri(context)
cameraUri = newUri
val missingPermissions = cameraCapturePermissions(context)
if (missingPermissions.isEmpty()) {
cameraLauncher.launch(newUri)
} else {
cameraPermissionLauncher.launch(missingPermissions)
}
},
onGallery = {
pickerLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
},
)
}
item { ErrorText(state.error) }
}
item { ErrorText(state.error) }
PullRefreshIndicator(
refreshing = state.refreshing,
state = pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter),
backgroundColor = Color.White,
contentColor = TppColors.Forest,
)
}
}
}
@@ -653,7 +666,7 @@ private fun DetailHeader(onBack: () -> Unit) {
}
@Composable
private fun ManifestSection(route: DriverRouteDto) {
private fun ManifestSection(route: DriverRouteDto, onNavigate: (NavigationPointDto) -> Unit) {
val stripColor = routeStatusColor(route.status)
Card(
colors = CardDefaults.cardColors(containerColor = Color.White),
@@ -676,7 +689,6 @@ private fun ManifestSection(route: DriverRouteDto) {
color = TppColors.Ink,
)
}
RouteStartChip(route.startsAt)
}
}
HorizontalDivider(color = TppColors.Outline.copy(alpha = 0.65f))
@@ -687,6 +699,8 @@ private fun ManifestSection(route: DriverRouteDto) {
icon = Icons.Outlined.Factory,
iconBackground = TppColors.Panel,
iconTint = TppColors.ContainerGreen,
navigationPoint = route.originNavigation,
onNavigate = onNavigate,
)
HorizontalDivider(color = TppColors.Outline.copy(alpha = 0.45f))
RoutePointBlock(
@@ -697,29 +711,14 @@ private fun ManifestSection(route: DriverRouteDto) {
iconBackground = TppColors.Panel.copy(alpha = 0.72f),
iconTint = TppColors.Forest,
framed = true,
navigationPoint = route.destinationNavigation,
onNavigate = onNavigate,
)
RouteFactsFooter(route)
}
}
}
@Composable
private fun RouteStartChip(startsAt: String) {
Row(
modifier = Modifier.background(TppColors.Panel, RoundedCornerShape(8.dp)).padding(horizontal = 10.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp),
) {
Icon(Icons.Outlined.CalendarToday, contentDescription = null, tint = TppColors.Muted, modifier = Modifier.size(18.dp))
Text(
startsAt.ifBlank { "Dziś" },
color = TppColors.Muted,
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.SemiBold,
)
}
}
@Composable
private fun RoutePointBlock(
label: String,
@@ -729,10 +728,13 @@ private fun RoutePointBlock(
iconBackground: Color,
iconTint: Color,
framed: Boolean = false,
navigationPoint: NavigationPointDto? = null,
onNavigate: (NavigationPointDto) -> Unit = {},
) {
Row(
Modifier.fillMaxWidth().padding(20.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Box(
Modifier.size(48.dp).background(iconBackground, RoundedCornerShape(24.dp)),
@@ -760,6 +762,14 @@ private fun RoutePointBlock(
Text(subtitle, color = TppColors.Muted, style = MaterialTheme.typography.bodyLarge)
}
}
if (navigationPoint?.hasCoordinates() == true) {
IconButton(
onClick = { onNavigate(navigationPoint) },
modifier = Modifier.size(48.dp).background(TppColors.Forest, RoundedCornerShape(24.dp)),
) {
Icon(Icons.Outlined.Navigation, contentDescription = "Nawiguj", tint = Color.White)
}
}
}
}
@@ -880,14 +890,21 @@ private fun EmptyPhotoState() {
private fun DriverRouteDto.truckLabel(): String {
val registration = truck?.registration.orEmpty()
val trailerRegistration = truck?.trailerRegistration.orEmpty()
val name = truck?.name.orEmpty()
return when {
val vehicle = when {
registration.isNotBlank() && name.isNotBlank() -> "$registration ($name)"
registration.isNotBlank() -> registration
name.isNotBlank() -> name
else -> "-"
}
return if (trailerRegistration.isNotBlank()) {
"$vehicle\nNaczepa: $trailerRegistration"
} else {
vehicle
}
}
private fun DriverRouteDto.distanceLabel(): String =
@@ -945,6 +962,26 @@ private fun createCameraUri(context: Context): Uri {
return FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file)
}
private fun NavigationPointDto.hasCoordinates(): Boolean = latitude != null && longitude != null
private fun openNavigation(context: Context, point: NavigationPointDto) {
val latitude = point.latitude ?: return
val longitude = point.longitude ?: return
val googleMapsIntent = Intent(Intent.ACTION_VIEW, Uri.parse("google.navigation:q=$latitude,$longitude"))
.setPackage("com.google.android.apps.maps")
if (googleMapsIntent.resolveActivity(context.packageManager) != null) {
context.startActivity(googleMapsIntent)
return
}
val label = Uri.encode(point.label?.takeIf { it.isNotBlank() } ?: "Cel")
val geoIntent = Intent(Intent.ACTION_VIEW, Uri.parse("geo:0,0?q=$latitude,$longitude($label)"))
if (geoIntent.resolveActivity(context.packageManager) != null) {
context.startActivity(geoIntent)
}
}
private fun cameraCapturePermissions(context: Context): Array<String> = buildList {
if (!hasPermission(context, Manifest.permission.CAMERA)) {
add(Manifest.permission.CAMERA)
@@ -113,6 +113,30 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
_state.update { it.copy(screen = DriverScreen.Detail, selectedRoute = response.route, error = null) }
}
fun refreshSelectedRoute() {
val routeId = _state.value.selectedRoute?.id ?: return
viewModelScope.launch {
_state.update { it.copy(refreshing = true, error = null) }
runCatching { repository.route(routeId) }
.onSuccess { response ->
_state.update {
it.copy(
selectedRoute = response.route,
routes = it.routes.map { route -> if (route.id == routeId) response.route else route },
error = null,
)
}
}
.onFailure { throwable ->
_state.update { it.copy(error = throwable.message ?: "Wystapil blad.") }
}
_state.update { it.copy(refreshing = false) }
}
}
fun openProfile() {
_state.update { it.copy(screen = DriverScreen.Profile, error = null) }
}