diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 886bc65..938ef19 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,8 +12,8 @@ android { applicationId = "pl.firmatpp.kierowca" minSdk = 26 targetSdk = 35 - versionCode = 7 - versionName = "1.0.6" + versionCode = 8 + versionName = "1.0.7" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" buildConfigField("String", "API_BASE_URL", "\"https://api-intranet.firmatpp.pl/api/\"") diff --git a/app/src/main/java/pl/firmatpp/kierowca/ui/DriverApp.kt b/app/src/main/java/pl/firmatpp/kierowca/ui/DriverApp.kt index 6b7cab5..5da28fe 100644 --- a/app/src/main/java/pl/firmatpp/kierowca/ui/DriverApp.kt +++ b/app/src/main/java/pl/firmatpp/kierowca/ui/DriverApp.kt @@ -48,6 +48,8 @@ import androidx.compose.material.icons.outlined.AddPhotoAlternate import androidx.compose.material.icons.outlined.ArrowBack import androidx.compose.material.icons.outlined.CameraAlt import androidx.compose.material.icons.outlined.CalendarToday +import androidx.compose.material.icons.outlined.Factory +import androidx.compose.material.icons.outlined.LocationOn import androidx.compose.material.icons.outlined.LocalShipping import androidx.compose.material.icons.outlined.Person import androidx.compose.material.icons.outlined.Phone @@ -103,6 +105,7 @@ import com.google.android.gms.common.api.Status import java.io.File import java.time.LocalDate import java.time.format.DateTimeFormatter +import kotlin.math.ceil import kotlinx.coroutines.delay import java.util.Locale import pl.firmatpp.kierowca.R @@ -584,12 +587,7 @@ private fun DetailScreen( Scaffold( topBar = { - TopAppBar( - modifier = Modifier.statusBarsPadding(), - title = { Text("Szczegoly kursu") }, - navigationIcon = { IconButton(onClick = onBack) { Icon(Icons.Outlined.ArrowBack, contentDescription = "Wstecz") } }, - colors = TopAppBarDefaults.topAppBarColors(containerColor = TppColors.Surface), - ) + DetailHeader(onBack) }, containerColor = TppColors.Surface, ) { padding -> @@ -597,44 +595,191 @@ private fun DetailScreen( LazyColumn( Modifier.fillMaxSize().padding(padding), contentPadding = PaddingValues(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(24.dp), ) { item { ManifestSection(route) } item { - Text("Zdjecia ladunku", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) - Spacer(Modifier.height(12.dp)) - PhotoGrid(route.photos, state.imageAuthHeader, state.allowGalleryUploads, onPhoto, onCamera = { - val newUri = createCameraUri(context) - cameraUri = newUri - if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { - cameraLauncher.launch(newUri) - } else { - cameraPermissionLauncher.launch(Manifest.permission.CAMERA) - } - }, onGallery = { - pickerLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) - }) + CargoDocumentationSection( + photos = route.photos, + imageAuthHeader = state.imageAuthHeader, + allowGalleryUploads = state.allowGalleryUploads, + onPhoto = onPhoto, + onCamera = { + val newUri = createCameraUri(context) + cameraUri = newUri + if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { + cameraLauncher.launch(newUri) + } else { + cameraPermissionLauncher.launch(Manifest.permission.CAMERA) + } + }, + onGallery = { + pickerLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) + }, + ) } item { ErrorText(state.error) } } } } +@Composable +private fun DetailHeader(onBack: () -> Unit) { + Column(Modifier.fillMaxWidth().background(TppColors.Surface).statusBarsPadding()) { + Row( + Modifier.fillMaxWidth().height(64.dp).padding(start = 8.dp, end = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + IconButton(onClick = onBack, modifier = Modifier.size(48.dp)) { + Icon(Icons.Outlined.ArrowBack, contentDescription = "Wstecz", tint = TppColors.Ink) + } + Text( + "Szczegóły Trasy", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold, + color = TppColors.Ink, + modifier = Modifier.padding(start = 4.dp), + ) + } + HorizontalDivider(color = TppColors.Outline, thickness = 1.dp) + } +} + @Composable private fun ManifestSection(route: DriverRouteDto) { - Card(colors = CardDefaults.cardColors(containerColor = Color.White), shape = RoundedCornerShape(8.dp)) { - Column(Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { - Icon(Icons.Outlined.LocalShipping, contentDescription = null, tint = TppColors.Forest) - Text(route.relationLabel, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold) - Text("Kontrakt: ${route.contractCode ?: route.contractName ?: route.contractorName}", fontFamily = FontFamily.Monospace) - Text("Status: ${route.status}", color = TppColors.Muted) - if (!route.notes.isNullOrBlank()) Text(route.notes, color = TppColors.Muted) + val stripColor = routeStatusColor(route.status) + Card( + colors = CardDefaults.cardColors(containerColor = Color.White), + shape = RoundedCornerShape(8.dp), + border = BorderStroke(1.dp, TppColors.Outline), + ) { + Column { + Column(Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(14.dp)) { + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + ) { + Column(verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.weight(1f)) { + StatusChip(route.status, stripColor) + Text( + "Zlecenie #${route.contractCode ?: route.id}", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + color = TppColors.Ink, + ) + } + RouteStartChip(route.startsAt) + } + } + HorizontalDivider(color = TppColors.Outline.copy(alpha = 0.65f)) + RoutePointBlock( + label = "Załadunek", + title = route.originName.ifBlank { "Nie podano miejsca załadunku" }, + subtitle = route.contractorName, + icon = Icons.Outlined.Factory, + iconBackground = TppColors.Panel, + iconTint = TppColors.ContainerGreen, + ) + HorizontalDivider(color = TppColors.Outline.copy(alpha = 0.45f)) + RoutePointBlock( + label = "Rozładunek", + title = route.destinationName.ifBlank { "Nie podano miejsca rozładunku" }, + subtitle = route.notes?.takeIf { it.isNotBlank() } ?: "Uwagi do zlecenia pojawią się tutaj.", + icon = Icons.Outlined.LocationOn, + iconBackground = TppColors.Panel.copy(alpha = 0.72f), + iconTint = TppColors.Forest, + framed = true, + ) + RouteFactsFooter(route) } } } @Composable -private fun PhotoGrid( +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, + title: String, + subtitle: String, + icon: ImageVector, + iconBackground: Color, + iconTint: Color, + framed: Boolean = false, +) { + Row( + Modifier.fillMaxWidth().padding(20.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + Box( + Modifier.size(48.dp).background(iconBackground, RoundedCornerShape(24.dp)), + contentAlignment = Alignment.Center, + ) { + Icon(icon, contentDescription = null, tint = iconTint, modifier = Modifier.size(26.dp)) + } + Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(6.dp)) { + Text( + label.uppercase(Locale("pl", "PL")), + color = TppColors.Muted, + fontFamily = FontFamily.Monospace, + fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.labelMedium, + ) + Text(title, color = TppColors.Ink, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) + if (framed) { + Box(Modifier.fillMaxWidth().background(TppColors.Panel, RoundedCornerShape(4.dp)).padding(12.dp)) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text("UWAGI DO ZLECENIA", color = TppColors.Muted, fontFamily = FontFamily.Monospace, style = MaterialTheme.typography.labelSmall) + Text(subtitle, color = TppColors.Ink, fontWeight = FontWeight.SemiBold) + } + } + } else { + Text(subtitle, color = TppColors.Muted, style = MaterialTheme.typography.bodyLarge) + } + } + } +} + +@Composable +private fun RouteFactsFooter(route: DriverRouteDto) { + Column(Modifier.fillMaxWidth().background(TppColors.Panel).padding(20.dp), verticalArrangement = Arrangement.spacedBy(14.dp)) { + Row(horizontalArrangement = Arrangement.spacedBy(14.dp), modifier = Modifier.fillMaxWidth()) { + RouteFact("Typ ładunku", route.contractName ?: route.contractorName, Modifier.weight(1f)) + RouteFact("Pojazd", route.truckLabel(), Modifier.weight(1f)) + } + Row(horizontalArrangement = Arrangement.spacedBy(14.dp), modifier = Modifier.fillMaxWidth()) { + RouteFact("Nr kontraktu", route.contractCode ?: "#${route.id}", Modifier.weight(1f)) + RouteFact("Dystans", route.distanceLabel(), Modifier.weight(1f)) + } + } +} + +@Composable +private fun RouteFact(label: String, value: String, modifier: Modifier = Modifier) { + Column(modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text(label, color = TppColors.Muted, fontFamily = FontFamily.Monospace, style = MaterialTheme.typography.labelSmall) + Text(value.ifBlank { "-" }, color = TppColors.Ink, fontWeight = FontWeight.Bold) + } +} + +@Composable +private fun CargoDocumentationSection( photos: List, imageAuthHeader: String?, allowGalleryUploads: Boolean, @@ -642,41 +787,105 @@ private fun PhotoGrid( onCamera: () -> Unit, onGallery: () -> Unit, ) { - LazyVerticalGrid( - columns = GridCells.Fixed(2), - modifier = Modifier.height(360.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - item { ActionTile("Aparat", Icons.Outlined.CameraAlt, onCamera) } - if (allowGalleryUploads) { - item { ActionTile("Galeria", Icons.Outlined.AddPhotoAlternate, onGallery) } + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + Column { + Text("Dokumentacja ładunku", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, color = TppColors.Ink) + Spacer(Modifier.height(8.dp)) + Box(Modifier.width(116.dp).height(3.dp).background(TppColors.ContainerGreen)) } - items(photos, key = { it.id }) { photo -> - AsyncImage( - model = imageRequest(photo.url, imageAuthHeader), - contentDescription = "Zdjecie ladunku", - contentScale = ContentScale.Crop, - modifier = Modifier.aspectRatio(1f).clickable { onPhoto(photo) }, + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + if (allowGalleryUploads) { + CargoActionButton("Dodaj zdjęcie z galerii", Icons.Outlined.AddPhotoAlternate, TppColors.ContainerGreen, onGallery) + } + CargoActionButton("Zrób nowe zdjęcie", Icons.Outlined.CameraAlt, TppColors.Forest, onCamera) + } + PhotoGrid(photos, imageAuthHeader, onPhoto) + } +} + +@Composable +private fun CargoActionButton(label: String, icon: ImageVector, color: Color, onClick: () -> Unit) { + Button( + onClick = onClick, + modifier = Modifier.fillMaxWidth().height(64.dp), + colors = ButtonDefaults.buttonColors(containerColor = color), + shape = RoundedCornerShape(8.dp), + ) { + Icon(icon, contentDescription = null, tint = Color.White) + Spacer(Modifier.width(10.dp)) + Text(label, color = Color.White, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) + } +} + +@Composable +private fun PhotoGrid( + photos: List, + imageAuthHeader: String?, + onPhoto: (RoutePhotoDto) -> Unit, +) { + val rows = ceil((photos.size.coerceAtLeast(1) / 2f).toDouble()).toInt() + val gridHeight = (rows * 172).coerceIn(172, 520).dp + + Card( + colors = CardDefaults.cardColors(containerColor = Color.White), + shape = RoundedCornerShape(8.dp), + border = BorderStroke(1.dp, TppColors.Outline), + ) { + Column(Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { + Text( + "Załączone zdjęcia (${photos.size})", + color = TppColors.Muted, + fontFamily = FontFamily.Monospace, + fontWeight = FontWeight.SemiBold, ) + if (photos.isEmpty()) { + EmptyPhotoState() + } else { + LazyVerticalGrid( + columns = GridCells.Fixed(2), + modifier = Modifier.height(gridHeight), + verticalArrangement = Arrangement.spacedBy(12.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + items(photos, key = { it.id }) { photo -> + AsyncImage( + model = imageRequest(photo.url, imageAuthHeader), + contentDescription = "Zdjecie ladunku", + contentScale = ContentScale.Crop, + modifier = Modifier.aspectRatio(1f).clickable { onPhoto(photo) }, + ) + } + } + } } } } @Composable -private fun ActionTile(label: String, icon: androidx.compose.ui.graphics.vector.ImageVector, onClick: () -> Unit) { - Card( - modifier = Modifier.aspectRatio(1f).clickable(onClick = onClick), - colors = CardDefaults.cardColors(containerColor = TppColors.Panel), - shape = RoundedCornerShape(12.dp), +private fun EmptyPhotoState() { + Box( + Modifier.fillMaxWidth().height(132.dp).background(TppColors.Panel, RoundedCornerShape(4.dp)), + contentAlignment = Alignment.Center, ) { - Column(Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) { - Icon(icon, contentDescription = label, tint = TppColors.Forest) - Text(label, color = TppColors.Muted) - } + Text("Brak załączonych zdjęć", color = TppColors.Muted, fontFamily = FontFamily.Monospace) } } +private fun DriverRouteDto.truckLabel(): String { + val registration = truck?.registration.orEmpty() + val name = truck?.name.orEmpty() + + return when { + registration.isNotBlank() && name.isNotBlank() -> "$registration ($name)" + registration.isNotBlank() -> registration + name.isNotBlank() -> name + else -> "-" + } +} + +private fun DriverRouteDto.distanceLabel(): String = + distanceKm?.takeIf { it > 0 }?.let { "%.1f km".format(Locale.US, it) } ?: "-" + @OptIn(ExperimentalMaterial3Api::class) @Composable private fun PhotoScreen(state: DriverUiState, onBack: () -> Unit) {