Compare commits

...
7 changed files with 544 additions and 95 deletions
+2 -2
View File
@@ -34,8 +34,8 @@ android {
applicationId = "pl.firmatpp.kierowca"
minSdk = 26
targetSdk = 36
versionCode = 127
versionName = "1.0.74"
versionCode = 129
versionName = "1.0.76"
setProperty("archivesBaseName", "pl.firmatpp.kierowca")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@@ -88,6 +88,13 @@ data class DriverAppSettingsDto(
data class LeaveRequestsConfigDto(
val enabled: Boolean = false,
val types: List<String> = listOf("URLOP"),
val usagePoolCodes: List<String> = listOf(
"ANNUAL",
"ANNUAL_DISABILITY",
"FORCE_MAJEURE",
"CHILD_CARE_14",
"CARE_LEAVE",
),
)
data class DispatchSheetReminderDto(
@@ -252,6 +259,13 @@ data class DriverLeaveSummaryDto(
val dailyNormMinutes: Int = 480,
val balanceConfigured: Boolean = false,
val overdueQuantity: Int = 0,
val visibleUsagePoolCodes: List<String> = listOf(
"ANNUAL",
"ANNUAL_DISABILITY",
"FORCE_MAJEURE",
"CHILD_CARE_14",
"CARE_LEAVE",
),
val availableTypes: List<LeaveRequestTypeDto> = emptyList(),
val reminders: List<DriverLeaveReminderDto> = emptyList(),
)
@@ -62,6 +62,7 @@ import androidx.compose.material.icons.outlined.CalendarToday
import androidx.compose.material.icons.outlined.CheckCircle
import androidx.compose.material.icons.outlined.CloudUpload
import androidx.compose.material.icons.outlined.BugReport
import androidx.compose.material.icons.outlined.Collections
import androidx.compose.material.icons.outlined.ContentCopy
import androidx.compose.material.icons.outlined.Factory
import androidx.compose.material.icons.outlined.Delete
@@ -190,6 +191,11 @@ private enum class RouteLocationPermissionPurpose {
RestoreRequiredTracking,
}
private enum class RouteStageCameraMode {
Single,
Multiple,
}
@Composable
fun DriverApp(
viewModel: DriverViewModel,
@@ -1547,6 +1553,12 @@ private fun LeaveBalanceOverview(state: DriverUiState) {
val summary = state.leaveSummary
val totals = DriverLeaveRequestUiRules.annualTotals(summary)
val norm = summary?.dailyNormMinutes ?: 480
val showAnnualUsage = DriverLeaveRequestUiRules.isUsageVisible(summary, "ANNUAL")
val otherPools = DriverLeaveRequestUiRules.otherEntitlementPools(summary)
if (summary != null && !showAnnualUsage && otherPools.isEmpty()) {
return
}
Card(
colors = CardDefaults.cardColors(containerColor = TppTheme.colors.card),
@@ -1555,99 +1567,107 @@ private fun LeaveBalanceOverview(state: DriverUiState) {
modifier = Modifier.fillMaxWidth(),
) {
Column(Modifier.padding(18.dp), verticalArrangement = Arrangement.spacedBy(14.dp)) {
Text("Twój urlop wypoczynkowy", color = TppTheme.colors.ink, fontWeight = FontWeight.Bold, fontSize = 20.sp)
Text(
if (showAnnualUsage) "Twój urlop wypoczynkowy" else "Twoje limity nieobecności",
color = TppTheme.colors.ink,
fontWeight = FontWeight.Bold,
fontSize = 20.sp,
)
when {
summary == null -> {
Text("Pobieramy aktualne saldo…", color = TppTheme.colors.muted)
}
!summary.balanceConfigured -> {
Text(
"Kadry przygotowują Twoje saldo",
color = Color(0xFF7A5A00),
fontWeight = FontWeight.Bold,
)
Text(
"Możesz zapisać wniosek, ale jego godziny zostaną odjęte dopiero po potwierdzeniu puli urlopu.",
color = TppTheme.colors.muted,
)
}
else -> {
Text("Pozostało do wykorzystania", color = TppTheme.colors.muted, fontWeight = FontWeight.SemiBold)
Text(
DriverLeaveRequestUiRules.formatQuantity(totals.availableMinutes, "minutes", norm),
color = TppTheme.colors.forest,
fontWeight = FontWeight.Bold,
fontSize = 26.sp,
)
val denominator = (totals.grantedMinutes).coerceAtLeast(1)
LinearProgressIndicator(
progress = { (totals.usedMinutes.toFloat() / denominator).coerceIn(0f, 1f) },
modifier = Modifier.fillMaxWidth().height(7.dp),
color = TppTheme.colors.forest,
trackColor = TppTheme.colors.successContainer,
)
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) {
LeaveBalanceStat(
title = "Wykorzystano",
value = DriverLeaveRequestUiRules.formatQuantity(totals.usedMinutes, "minutes", norm),
modifier = Modifier.weight(1f),
)
LeaveBalanceStat(
title = "W przyszłych urlopach",
value = DriverLeaveRequestUiRules.formatQuantity(totals.reservedMinutes, "minutes", norm),
modifier = Modifier.weight(1f),
)
if (showAnnualUsage) {
when {
summary == null -> {
Text("Pobieramy aktualne saldo…", color = TppTheme.colors.muted)
}
}
}
if (totals.overdueMinutes > 0) {
Row(
Modifier
.fillMaxWidth()
.background(TppTheme.colors.warningContainer, RoundedCornerShape(6.dp))
.border(1.dp, TppTheme.colors.warningOutline, RoundedCornerShape(6.dp))
.padding(12.dp),
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.Top,
) {
Icon(Icons.Outlined.Info, contentDescription = null, tint = Color(0xFF7A5A00))
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(3.dp)) {
Text("Masz zaległy urlop", color = Color(0xFF7A5A00), fontWeight = FontWeight.Bold)
!summary.balanceConfigured -> {
Text(
"${DriverLeaveRequestUiRules.formatQuantity(totals.overdueMinutes, "minutes", norm)} zaplanuj możliwie szybko, najpóźniej do 30 września. Saldo nie zniknie automatycznie.",
"Kadry przygotowują Twoje saldo",
color = Color(0xFF7A5A00),
fontSize = 13.sp,
fontWeight = FontWeight.Bold,
)
Text(
"Możesz zapisać wniosek, ale jego godziny zostaną odjęte dopiero po potwierdzeniu puli urlopu.",
color = TppTheme.colors.muted,
)
}
else -> {
Text("Pozostało do wykorzystania", color = TppTheme.colors.muted, fontWeight = FontWeight.SemiBold)
Text(
DriverLeaveRequestUiRules.formatQuantity(totals.availableMinutes, "minutes", norm),
color = TppTheme.colors.forest,
fontWeight = FontWeight.Bold,
fontSize = 26.sp,
)
val denominator = (totals.grantedMinutes).coerceAtLeast(1)
LinearProgressIndicator(
progress = { (totals.usedMinutes.toFloat() / denominator).coerceIn(0f, 1f) },
modifier = Modifier.fillMaxWidth().height(7.dp),
color = TppTheme.colors.forest,
trackColor = TppTheme.colors.successContainer,
)
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) {
LeaveBalanceStat(
title = "Wykorzystano",
value = DriverLeaveRequestUiRules.formatQuantity(totals.usedMinutes, "minutes", norm),
modifier = Modifier.weight(1f),
)
LeaveBalanceStat(
title = "W przyszłych urlopach",
value = DriverLeaveRequestUiRules.formatQuantity(totals.reservedMinutes, "minutes", norm),
modifier = Modifier.weight(1f),
)
}
}
}
if (totals.overdueMinutes > 0) {
Row(
Modifier
.fillMaxWidth()
.background(TppTheme.colors.warningContainer, RoundedCornerShape(6.dp))
.border(1.dp, TppTheme.colors.warningOutline, RoundedCornerShape(6.dp))
.padding(12.dp),
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.Top,
) {
Icon(Icons.Outlined.Info, contentDescription = null, tint = Color(0xFF7A5A00))
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(3.dp)) {
Text("Masz zaległy urlop", color = Color(0xFF7A5A00), fontWeight = FontWeight.Bold)
Text(
"${DriverLeaveRequestUiRules.formatQuantity(totals.overdueMinutes, "minutes", norm)} zaplanuj możliwie szybko, najpóźniej do 30 września. Saldo nie zniknie automatycznie.",
color = Color(0xFF7A5A00),
fontSize = 13.sp,
)
}
}
}
DriverLeaveRequestUiRules.annualPools(summary).forEach { pool ->
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text(
if (pool.year < LocalDate.now().year) "Urlop zaległy z ${pool.year} r." else "Pula na ${pool.year} r.",
color = TppTheme.colors.muted,
)
Text(
DriverLeaveRequestUiRules.formatQuantity(pool.availableQuantity, pool.unit, norm),
color = TppTheme.colors.ink,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.End,
)
}
}
}
DriverLeaveRequestUiRules.annualPools(summary).forEach { pool ->
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
if (otherPools.isNotEmpty()) {
if (showAnnualUsage) {
HorizontalDivider(color = TppTheme.colors.outline)
Text(
if (pool.year < LocalDate.now().year) "Urlop zaległy z ${pool.year} r." else "Pula na ${pool.year} r.",
color = TppTheme.colors.muted,
)
Text(
DriverLeaveRequestUiRules.formatQuantity(pool.availableQuantity, pool.unit, norm),
"Pozostałe dostępne limity",
color = TppTheme.colors.ink,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.End,
fontSize = 17.sp,
)
}
}
val otherPools = DriverLeaveRequestUiRules.otherEntitlementPools(summary)
if (otherPools.isNotEmpty()) {
HorizontalDivider(color = TppTheme.colors.outline)
Text(
"Pozostałe dostępne limity",
color = TppTheme.colors.ink,
fontWeight = FontWeight.Bold,
fontSize = 17.sp,
)
Text(
"Te limity są rozliczane osobno i nie zmniejszają zwykłego urlopu wypoczynkowego.",
color = TppTheme.colors.muted,
@@ -4411,6 +4431,8 @@ private fun RouteStageScreen(
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
var cameraUri by remember { mutableStateOf<Uri?>(null) }
var pendingCameraMode by remember { mutableStateOf<RouteStageCameraMode?>(null) }
var showMultiplePhotoCamera by remember(stage) { mutableStateOf(false) }
var showPreciseLocationPermissionDialog by remember { mutableStateOf(false) }
var voiceDictationActive by remember(stage) { mutableStateOf(false) }
val route = state.displaySelectedRoute
@@ -4465,6 +4487,7 @@ private fun RouteStageScreen(
cameraUri = null
}
val cameraPermissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { grants ->
val requestedMode = pendingCameraMode
val pendingUri = cameraUri
val cameraGranted = grants[Manifest.permission.CAMERA] == true || hasPermission(context, Manifest.permission.CAMERA)
val fineLocationGranted = grants[Manifest.permission.ACCESS_FINE_LOCATION] == true ||
@@ -4472,14 +4495,43 @@ private fun RouteStageScreen(
if (!canLaunchCameraWithLocationPolicy(state.requirePreciseLocationForPhotos, fineLocationGranted)) {
showPreciseLocationPermissionDialog = true
cameraUri = null
pendingCameraMode = null
return@rememberLauncherForActivityResult
}
if (cameraGranted && pendingUri != null) cameraLauncher.launch(pendingUri)
if (cameraGranted) {
when (requestedMode) {
RouteStageCameraMode.Single -> {
if (pendingUri != null) cameraLauncher.launch(pendingUri)
}
RouteStageCameraMode.Multiple -> {
showMultiplePhotoCamera = true
}
null -> Unit
}
} else {
cameraUri = null
}
pendingCameraMode = null
}
val pickerLauncher = rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri ->
if (uri != null) onUpload(uri, "gallery", PhotoUploadMetadata())
}
if (showMultiplePhotoCamera) {
MultiplePhotoCameraScreen(
stage = stage,
onPhotoCaptured = { uri ->
coroutineScope.launch {
onUpload(uri, "camera", cameraPhotoMetadata(context))
}
},
onClose = {
showMultiplePhotoCamera = false
},
)
return
}
BoxWithConstraints(Modifier.fillMaxSize()) {
val horizontalPadding = screenHorizontalPaddingDp(maxWidth.value.toInt()).dp
Scaffold(
@@ -4559,19 +4611,37 @@ private fun RouteStageScreen(
if (showPhotoActions) {
item {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
CargoActionButton("Zrób nowe zdjęcie", Icons.Outlined.CameraAlt, TppTheme.colors.forest) {
val newUri = createCameraUri(context)
cameraUri = newUri
val missingPermissions = cameraCapturePermissions(
context = context,
requirePreciseLocation = state.requirePreciseLocationForPhotos,
)
if (missingPermissions.isEmpty()) {
cameraLauncher.launch(newUri)
} else {
cameraPermissionLauncher.launch(missingPermissions)
}
}
RouteStageCameraActions(
onSinglePhoto = {
val newUri = createCameraUri(context)
cameraUri = newUri
pendingCameraMode = RouteStageCameraMode.Single
val missingPermissions = cameraCapturePermissions(
context = context,
requirePreciseLocation = state.requirePreciseLocationForPhotos,
)
if (missingPermissions.isEmpty()) {
pendingCameraMode = null
cameraLauncher.launch(newUri)
} else {
cameraPermissionLauncher.launch(missingPermissions)
}
},
onMultiplePhotos = {
cameraUri = null
pendingCameraMode = RouteStageCameraMode.Multiple
val missingPermissions = cameraCapturePermissions(
context = context,
requirePreciseLocation = state.requirePreciseLocationForPhotos,
)
if (missingPermissions.isEmpty()) {
pendingCameraMode = null
showMultiplePhotoCamera = true
} else {
cameraPermissionLauncher.launch(missingPermissions)
}
},
)
if (state.allowGalleryUploads) {
CargoActionButton("Dodaj zdjęcie z galerii", Icons.Outlined.AddPhotoAlternate, TppTheme.colors.containerGreen) {
pickerLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
@@ -5478,6 +5548,107 @@ private fun RouteStageDocumentationBlock(
private fun formatRouteWeight(weight: Double): String =
if (weight % 1.0 == 0.0) "${weight.toInt()} t" else String.format(Locale("pl", "PL"), "%.3f t", weight)
@Composable
private fun RouteStageCameraActions(
onSinglePhoto: () -> Unit,
onMultiplePhotos: () -> Unit,
) {
BoxWithConstraints(Modifier.fillMaxWidth()) {
if (maxWidth >= 300.dp) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
RouteStageCameraActionButton(
label = "Jedno zdjęcie",
icon = Icons.Outlined.CameraAlt,
color = TppTheme.colors.forest,
compact = true,
modifier = Modifier.weight(1f),
onClick = onSinglePhoto,
)
RouteStageCameraActionButton(
label = "Wiele zdjęć",
icon = Icons.Outlined.Collections,
color = TppTheme.colors.navy,
compact = true,
modifier = Modifier.weight(1f),
onClick = onMultiplePhotos,
)
}
} else {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
RouteStageCameraActionButton(
label = "Jedno zdjęcie",
icon = Icons.Outlined.CameraAlt,
color = TppTheme.colors.forest,
compact = false,
modifier = Modifier.fillMaxWidth(),
onClick = onSinglePhoto,
)
RouteStageCameraActionButton(
label = "Wiele zdjęć",
icon = Icons.Outlined.Collections,
color = TppTheme.colors.navy,
compact = false,
modifier = Modifier.fillMaxWidth(),
onClick = onMultiplePhotos,
)
}
}
}
}
@Composable
private fun RouteStageCameraActionButton(
label: String,
icon: ImageVector,
color: Color,
compact: Boolean,
modifier: Modifier = Modifier,
onClick: () -> Unit,
) {
Button(
onClick = onClick,
modifier = modifier.height(if (compact) 76.dp else 64.dp),
colors = ButtonDefaults.buttonColors(containerColor = color),
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp),
shape = MaterialTheme.shapes.medium,
) {
if (compact) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Icon(
icon,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(22.dp),
)
Text(
text = label,
color = Color.White,
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Center,
)
}
} else {
Icon(icon, contentDescription = null, tint = Color.White)
Spacer(Modifier.width(10.dp))
Text(
text = label,
color = Color.White,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
)
}
}
}
@Composable
private fun CargoActionButton(label: String, icon: ImageVector, color: Color, onClick: () -> Unit) {
Button(
@@ -106,6 +106,9 @@ object DriverLeaveRequestUiRules {
.filter { it.poolCode == "ANNUAL" && it.status == "active" }
.sortedByDescending { it.year }
fun isUsageVisible(summary: DriverLeaveSummaryDto?, poolCode: String): Boolean =
summary?.visibleUsagePoolCodes?.contains(poolCode) ?: true
fun otherEntitlementPools(summary: DriverLeaveSummaryDto?): List<DriverLeavePoolDto> =
summary?.pools.orEmpty()
.filter { it.poolCode != "ANNUAL" && it.status == "active" }
@@ -0,0 +1,252 @@
package pl.firmatpp.kierowca.ui
import android.content.Context
import android.net.Uri
import androidx.activity.compose.BackHandler
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.view.CameraController
import androidx.camera.view.LifecycleCameraController
import androidx.camera.view.PreviewView
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
import androidx.compose.material.icons.outlined.CameraAlt
import androidx.compose.material.icons.outlined.CheckCircle
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.lifecycle.compose.LocalLifecycleOwner
import java.io.File
import pl.firmatpp.kierowca.ui.theme.TppTheme
@Composable
internal fun MultiplePhotoCameraScreen(
stage: String,
onPhotoCaptured: (Uri) -> Unit,
onClose: () -> Unit,
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val currentOnPhotoCaptured by rememberUpdatedState(onPhotoCaptured)
val cameraController = remember(context) {
LifecycleCameraController(context).apply {
setEnabledUseCases(CameraController.IMAGE_CAPTURE)
imageCaptureMode = ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY
}
}
var capturedCount by remember { mutableIntStateOf(0) }
var isCapturing by remember { mutableStateOf(false) }
var cameraUnavailable by remember { mutableStateOf(false) }
var cameraError by remember { mutableStateOf<String?>(null) }
DisposableEffect(cameraController, lifecycleOwner) {
runCatching {
cameraController.bindToLifecycle(lifecycleOwner)
}.onFailure {
cameraUnavailable = true
cameraError = "Nie udało się uruchomić aparatu."
}
onDispose {
cameraController.unbind()
}
}
BackHandler {
if (!isCapturing) onClose()
}
Box(
Modifier
.fillMaxSize()
.background(Color.Black),
) {
AndroidView(
factory = { previewContext ->
PreviewView(previewContext).apply {
controller = cameraController
implementationMode = PreviewView.ImplementationMode.COMPATIBLE
scaleType = PreviewView.ScaleType.FILL_CENTER
}
},
modifier = Modifier.fillMaxSize(),
)
Row(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.TopCenter)
.background(Color.Black.copy(alpha = 0.62f))
.statusBarsPadding()
.padding(horizontal = 8.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically,
) {
IconButton(
onClick = onClose,
enabled = !isCapturing,
) {
Icon(
Icons.AutoMirrored.Outlined.ArrowBack,
contentDescription = "Wróć",
tint = Color.White,
)
}
Column {
Text(
text = if (stage == "loading") "Zdjęcia załadunku" else "Zdjęcia rozładunku",
color = Color.White,
style = MaterialTheme.typography.titleMedium,
)
Text(
text = "Wykonano: $capturedCount",
color = Color.White.copy(alpha = 0.78f),
style = MaterialTheme.typography.bodyMedium,
)
}
}
Column(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter)
.background(Color.Black.copy(alpha = 0.68f))
.navigationBarsPadding()
.padding(horizontal = 20.dp, vertical = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
cameraError?.let { message ->
Text(
text = message,
color = Color.White,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier
.fillMaxWidth()
.background(Color(0xFF8C1D18), RoundedCornerShape(6.dp))
.padding(10.dp),
)
}
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Spacer(Modifier.size(112.dp, 52.dp))
Button(
onClick = {
if (isCapturing || cameraUnavailable) return@Button
val photoFile = createMultipleCameraFile(context)
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
isCapturing = true
cameraController.takePicture(
outputOptions,
ContextCompat.getMainExecutor(context),
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
isCapturing = false
cameraError = null
capturedCount += 1
currentOnPhotoCaptured(cameraFileUri(context, photoFile))
}
override fun onError(exception: ImageCaptureException) {
isCapturing = false
photoFile.delete()
cameraError = "Nie udało się zapisać zdjęcia. Spróbuj ponownie."
}
},
)
},
enabled = !isCapturing && !cameraUnavailable,
modifier = Modifier.size(78.dp),
shape = CircleShape,
colors = ButtonDefaults.buttonColors(
containerColor = Color.White,
disabledContainerColor = Color.White.copy(alpha = 0.55f),
),
contentPadding = ButtonDefaults.ContentPadding,
) {
if (isCapturing) {
CircularProgressIndicator(
modifier = Modifier.size(28.dp),
color = TppTheme.colors.forest,
strokeWidth = 3.dp,
)
} else {
Icon(
Icons.Outlined.CameraAlt,
contentDescription = "Zrób zdjęcie",
tint = TppTheme.colors.forest,
modifier = Modifier.size(32.dp),
)
}
}
Button(
onClick = onClose,
enabled = !isCapturing,
modifier = Modifier.size(width = 112.dp, height = 52.dp),
colors = ButtonDefaults.buttonColors(
containerColor = TppTheme.colors.forest,
),
shape = RoundedCornerShape(6.dp),
) {
Icon(
Icons.Outlined.CheckCircle,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(20.dp),
)
Text(
text = "Gotowe",
color = Color.White,
modifier = Modifier.padding(start = 6.dp),
)
}
}
}
}
}
private fun createMultipleCameraFile(context: Context): File {
val directory = File(context.cacheDir, "camera").apply { mkdirs() }
return File.createTempFile("ladunek-", ".jpg", directory)
}
private fun cameraFileUri(context: Context, file: File): Uri =
FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file)
@@ -19,6 +19,7 @@ class DriverLeaveApiContractTest {
"dailyNormMinutes": 480,
"balanceConfigured": true,
"overdueQuantity": 480,
"visibleUsagePoolCodes": ["ANNUAL", "CHILD_CARE_14"],
"pools": [{
"id": 7,
"poolCode": "ANNUAL",
@@ -49,6 +50,7 @@ class DriverLeaveApiContractTest {
)
assertEquals(480, response.data.dailyNormMinutes)
assertEquals(listOf("ANNUAL", "CHILD_CARE_14"), response.data.visibleUsagePoolCodes)
assertEquals(7680, response.data.pools.single().availableQuantity)
assertEquals("URLOP", response.data.availableTypes.single().value)
assertEquals("ANNUAL", response.data.availableTypes.single().code)
@@ -124,6 +124,13 @@ class DriverLeaveRequestUiRulesTest {
assertEquals(listOf("CHILD_CARE_14", "CARE_LEAVE"), pools.map { it.poolCode })
assertEquals("Urlop opiekuńczy", DriverLeaveRequestUiRules.poolLabel("CARE_LEAVE"))
assertEquals("Rodzina i opieka", DriverLeaveRequestUiRules.categoryLabel("family"))
assertTrue(DriverLeaveRequestUiRules.isUsageVisible(summary, "ANNUAL"))
assertFalse(
DriverLeaveRequestUiRules.isUsageVisible(
summary.copy(visibleUsagePoolCodes = emptyList()),
"ANNUAL",
),
)
}
@Test