Dodaj nawigacje i odswiezanie szczegolow trasy

This commit is contained in:
admin
2026-06-30 22:16:49 +02:00
parent 621745a93c
commit b034384ab2
4 changed files with 122 additions and 51 deletions
+2 -2
View File
@@ -12,8 +12,8 @@ android {
applicationId = "pl.firmatpp.kierowca" applicationId = "pl.firmatpp.kierowca"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 9 versionCode = 10
versionName = "1.0.8" versionName = "1.0.9"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildConfigField("String", "API_BASE_URL", "\"https://api-intranet.firmatpp.pl/api/\"") buildConfigField("String", "API_BASE_URL", "\"https://api-intranet.firmatpp.pl/api/\"")
@@ -69,15 +69,25 @@ data class DriverRouteDto(
val distanceKm: Double?, val distanceKm: Double?,
val notes: String?, val notes: String?,
val truck: TruckDto?, val truck: TruckDto?,
val originNavigation: NavigationPointDto? = null,
val destinationNavigation: NavigationPointDto? = null,
val photos: List<RoutePhotoDto> = emptyList(), val photos: List<RoutePhotoDto> = emptyList(),
) )
data class TruckDto( data class TruckDto(
val id: String, val id: String,
val registration: String?, val registration: String?,
val trailerRegistration: String? = null,
val name: String?, val name: String?,
) )
data class NavigationPointDto(
val id: String,
val label: String?,
val latitude: Double?,
val longitude: Double?,
)
data class RoutePhotoDto( data class RoutePhotoDto(
val id: String, val id: String,
val routeId: 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.Factory
import androidx.compose.material.icons.outlined.LocationOn import androidx.compose.material.icons.outlined.LocationOn
import androidx.compose.material.icons.outlined.LocalShipping 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.Person
import androidx.compose.material.icons.outlined.Phone import androidx.compose.material.icons.outlined.Phone
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
@@ -114,6 +115,7 @@ import java.util.Locale
import pl.firmatpp.kierowca.R import pl.firmatpp.kierowca.R
import pl.firmatpp.kierowca.data.PhotoUploadMetadata import pl.firmatpp.kierowca.data.PhotoUploadMetadata
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.RoutePhotoDto import pl.firmatpp.kierowca.data.model.RoutePhotoDto
import pl.firmatpp.kierowca.domain.OtpCodeExtractor import pl.firmatpp.kierowca.domain.OtpCodeExtractor
import pl.firmatpp.kierowca.domain.RouteDisplayMapper import pl.firmatpp.kierowca.domain.RouteDisplayMapper
@@ -168,7 +170,7 @@ fun DriverApp(viewModel: DriverViewModel) {
onRoute = viewModel::openRoute, onRoute = viewModel::openRoute,
) )
DriverScreen.Profile -> ProfileScreen(state, viewModel::refreshRoutes, viewModel::openProfile, viewModel::logout) 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) 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 @Composable
private fun DetailScreen( private fun DetailScreen(
state: DriverUiState, state: DriverUiState,
onBack: () -> Unit, onBack: () -> Unit,
onUpload: (Uri, String, PhotoUploadMetadata) -> Unit, onUpload: (Uri, String, PhotoUploadMetadata) -> Unit,
onPhoto: (RoutePhotoDto) -> Unit, onPhoto: (RoutePhotoDto) -> Unit,
onRefresh: () -> Unit,
) { ) {
val context = LocalContext.current val context = LocalContext.current
var cameraUri by remember { mutableStateOf<Uri?>(null) } var cameraUri by remember { mutableStateOf<Uri?>(null) }
val pullRefreshState = rememberPullRefreshState(state.refreshing, onRefresh)
val cameraLauncher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) { ok -> val cameraLauncher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) { ok ->
val capturedUri = cameraUri val capturedUri = cameraUri
if (ok && capturedUri != null) onUpload(capturedUri, "camera", cameraPhotoMetadata(context)) if (ok && capturedUri != null) onUpload(capturedUri, "camera", cameraPhotoMetadata(context))
@@ -598,12 +602,13 @@ private fun DetailScreen(
containerColor = TppColors.Surface, containerColor = TppColors.Surface,
) { padding -> ) { padding ->
if (route == null) return@Scaffold if (route == null) return@Scaffold
Box(Modifier.fillMaxSize().padding(padding).pullRefresh(pullRefreshState)) {
LazyColumn( LazyColumn(
Modifier.fillMaxSize().padding(padding), Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp), contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(24.dp), verticalArrangement = Arrangement.spacedBy(24.dp),
) { ) {
item { ManifestSection(route) } item { ManifestSection(route, onNavigate = { openNavigation(context, it) }) }
item { item {
CargoDocumentationSection( CargoDocumentationSection(
photos = route.photos, photos = route.photos,
@@ -627,6 +632,14 @@ private fun DetailScreen(
} }
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 @Composable
private fun ManifestSection(route: DriverRouteDto) { private fun ManifestSection(route: DriverRouteDto, onNavigate: (NavigationPointDto) -> Unit) {
val stripColor = routeStatusColor(route.status) val stripColor = routeStatusColor(route.status)
Card( Card(
colors = CardDefaults.cardColors(containerColor = Color.White), colors = CardDefaults.cardColors(containerColor = Color.White),
@@ -676,7 +689,6 @@ private fun ManifestSection(route: DriverRouteDto) {
color = TppColors.Ink, color = TppColors.Ink,
) )
} }
RouteStartChip(route.startsAt)
} }
} }
HorizontalDivider(color = TppColors.Outline.copy(alpha = 0.65f)) HorizontalDivider(color = TppColors.Outline.copy(alpha = 0.65f))
@@ -687,6 +699,8 @@ private fun ManifestSection(route: DriverRouteDto) {
icon = Icons.Outlined.Factory, icon = Icons.Outlined.Factory,
iconBackground = TppColors.Panel, iconBackground = TppColors.Panel,
iconTint = TppColors.ContainerGreen, iconTint = TppColors.ContainerGreen,
navigationPoint = route.originNavigation,
onNavigate = onNavigate,
) )
HorizontalDivider(color = TppColors.Outline.copy(alpha = 0.45f)) HorizontalDivider(color = TppColors.Outline.copy(alpha = 0.45f))
RoutePointBlock( RoutePointBlock(
@@ -697,29 +711,14 @@ private fun ManifestSection(route: DriverRouteDto) {
iconBackground = TppColors.Panel.copy(alpha = 0.72f), iconBackground = TppColors.Panel.copy(alpha = 0.72f),
iconTint = TppColors.Forest, iconTint = TppColors.Forest,
framed = true, framed = true,
navigationPoint = route.destinationNavigation,
onNavigate = onNavigate,
) )
RouteFactsFooter(route) 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 @Composable
private fun RoutePointBlock( private fun RoutePointBlock(
label: String, label: String,
@@ -729,10 +728,13 @@ private fun RoutePointBlock(
iconBackground: Color, iconBackground: Color,
iconTint: Color, iconTint: Color,
framed: Boolean = false, framed: Boolean = false,
navigationPoint: NavigationPointDto? = null,
onNavigate: (NavigationPointDto) -> Unit = {},
) { ) {
Row( Row(
Modifier.fillMaxWidth().padding(20.dp), Modifier.fillMaxWidth().padding(20.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
) { ) {
Box( Box(
Modifier.size(48.dp).background(iconBackground, RoundedCornerShape(24.dp)), 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) 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 { private fun DriverRouteDto.truckLabel(): String {
val registration = truck?.registration.orEmpty() val registration = truck?.registration.orEmpty()
val trailerRegistration = truck?.trailerRegistration.orEmpty()
val name = truck?.name.orEmpty() val name = truck?.name.orEmpty()
return when { val vehicle = when {
registration.isNotBlank() && name.isNotBlank() -> "$registration ($name)" registration.isNotBlank() && name.isNotBlank() -> "$registration ($name)"
registration.isNotBlank() -> registration registration.isNotBlank() -> registration
name.isNotBlank() -> name name.isNotBlank() -> name
else -> "-" else -> "-"
} }
return if (trailerRegistration.isNotBlank()) {
"$vehicle\nNaczepa: $trailerRegistration"
} else {
vehicle
}
} }
private fun DriverRouteDto.distanceLabel(): String = private fun DriverRouteDto.distanceLabel(): String =
@@ -945,6 +962,26 @@ private fun createCameraUri(context: Context): Uri {
return FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file) 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 { private fun cameraCapturePermissions(context: Context): Array<String> = buildList {
if (!hasPermission(context, Manifest.permission.CAMERA)) { if (!hasPermission(context, Manifest.permission.CAMERA)) {
add(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) } _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() { fun openProfile() {
_state.update { it.copy(screen = DriverScreen.Profile, error = null) } _state.update { it.copy(screen = DriverScreen.Profile, error = null) }
} }