From 8f051158c1f18e85db1937e3ae787243d18dd069 Mon Sep 17 00:00:00 2001 From: DH Android Finalizer Date: Fri, 11 Sep 2026 15:21:04 +0000 Subject: [PATCH] android-finalizer: apply final field flow and Spanish UX --- .../korexlabs/dhinspeccion/MainViewModel.kt | 12 ++++-- .../dhinspeccion/ui/FieldFindingScreen.kt | 2 +- .../korexlabs/dhinspeccion/ui/LoginGate.kt | 21 +++++++++- .../dhinspeccion/ui/ModernMobileActsScreen.kt | 38 ++++++++----------- .../dhinspeccion/ui/ModernVisitRoot.kt | 25 +++++++----- 5 files changed, 59 insertions(+), 39 deletions(-) diff --git a/android-app/app/src/main/java/com/korexlabs/dhinspeccion/MainViewModel.kt b/android-app/app/src/main/java/com/korexlabs/dhinspeccion/MainViewModel.kt index 006ea1d..ae689c1 100644 --- a/android-app/app/src/main/java/com/korexlabs/dhinspeccion/MainViewModel.kt +++ b/android-app/app/src/main/java/com/korexlabs/dhinspeccion/MainViewModel.kt @@ -241,6 +241,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { latitude: Double, longitude: Double, accuracyM: Double?, + description: String? = null, ) { val visitId = visit?.id ?: return if (visit?.status != "IN_PROGRESS") { @@ -258,6 +259,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { familyId = familyId, name = name.trim(), commonName = commonName?.trim()?.takeIf { it.isNotBlank() }, + description = description?.trim()?.takeIf { it.isNotBlank() }, attributes = attributes, deviceLatitude = latitude, deviceLongitude = longitude, @@ -305,6 +307,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { latitude: Double, longitude: Double, accuracyM: Double?, + description: String? = null, ) { val visitId = visit?.id ?: return val asset = selectedFieldAsset?.asset ?: return @@ -480,12 +483,12 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { launchBusy { actClosure = actsRepository.lock(actId) refreshSelectedActInternal(actId) - notice = "Acta bloqueada. Su contenido quedó inmutable; ahora deben resolverse las firmas y manifestaciones." + notice = "Acta cerrada y pendiente de firma. Su contenido quedó inmutable; las firmas y manifestaciones pueden completarse a continuación." } } fun reopenSelectedAct() { - error = "Un Acta bloqueada es inmutable y no puede volver a borrador." + error = "Un Acta pendiente de firma es inmutable y no puede volver a edición." } fun signSelectedActAsInspector( @@ -493,6 +496,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { latitude: Double?, longitude: Double?, accuracyM: Double?, + description: String? = null, ) { val actId = selectedAct?.id ?: return launchBusy { @@ -546,7 +550,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { refreshSelectedActInternal(actId) loadActsInternal(currentVisit.id, selectDraft = false) clearFindingState() - notice = "${selectedAct?.code ?: "Acta"} SELLADA e inmutable. Podés crear otra Acta o continuar hacia el cierre de la Inspección." + notice = "${selectedAct?.code ?: "Acta"} firmada y cerrada. Podés crear otra Acta o continuar hacia el cierre de la Inspección." } } @@ -555,7 +559,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { launchBusy { visit = actsRepository.closeVisit(visitId) loadVisitsInternal() - notice = "Inspección cerrada. Todas sus Actas quedaron SELLADAS y disponibles para el circuito de oficina." + notice = "Inspección cerrada. Todas sus Actas quedaron firmadas y disponibles para el circuito de oficina." } } diff --git a/android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/FieldFindingScreen.kt b/android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/FieldFindingScreen.kt index 9b4b882..ccd6e85 100644 --- a/android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/FieldFindingScreen.kt +++ b/android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/FieldFindingScreen.kt @@ -210,7 +210,7 @@ fun FieldFindingScreen(model: MainViewModel) { Card(Modifier.fillMaxWidth()) { Column(Modifier.padding(10.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) { Text("${finding.code} · ${finding.title}", fontWeight = FontWeight.SemiBold) - Text("Gravedad: ${finding.severity ?: "s/d"} · ${finding.status}", style = MaterialTheme.typography.bodySmall) + Text("Gravedad: ${finding.severity ?: "s/d"} · ${findingStatusLabelEs(finding.status)}", style = MaterialTheme.typography.bodySmall) Text( "Evidencias: ${evidence.size} · Fotos: ${evidence.count { it.kind == "PHOTO" }}", style = MaterialTheme.typography.bodySmall, diff --git a/android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/LoginGate.kt b/android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/LoginGate.kt index b6fefa6..92c1bc6 100644 --- a/android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/LoginGate.kt +++ b/android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/LoginGate.kt @@ -3,11 +3,13 @@ package com.korexlabs.dhinspeccion.ui import android.content.Context import androidx.biometric.BiometricManager import androidx.biometric.BiometricPrompt +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Visibility @@ -29,7 +31,9 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation @@ -37,6 +41,7 @@ import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat import androidx.fragment.app.FragmentActivity import com.korexlabs.dhinspeccion.MainViewModel +import com.korexlabs.dhinspeccion.R private const val BIOMETRIC_PREFS = "dh_v2_biometric" private const val BIOMETRIC_ENABLED = "enabled" @@ -105,7 +110,13 @@ private fun EnhancedLoginScreen(model: MainViewModel, onPasswordLogin: () -> Uni var passwordVisible by rememberSaveable { mutableStateOf(false) } Box(Modifier.fillMaxSize().padding(24.dp), contentAlignment = Alignment.Center) { - Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(14.dp)) { + Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(14.dp), horizontalAlignment = Alignment.CenterHorizontally) { + Image( + painter = painterResource(R.drawable.mendoza_logo), + contentDescription = "Gobierno de Mendoza", + modifier = Modifier.fillMaxWidth().height(170.dp), + contentScale = ContentScale.Fit, + ) Text("DH Inspección", style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold) Text("Aplicación de campo · Dirección de Hidrocarburos") model.error?.let { Text(it, color = MaterialTheme.colorScheme.error) } @@ -113,7 +124,7 @@ private fun EnhancedLoginScreen(model: MainViewModel, onPasswordLogin: () -> Uni OutlinedTextField( value = identifier, onValueChange = { identifier = it }, - label = { Text("Usuario o email") }, + label = { Text("Usuario o correo electrónico") }, modifier = Modifier.fillMaxWidth(), singleLine = true, ) @@ -198,6 +209,12 @@ private fun BiometricUnlockScreen( verticalArrangement = Arrangement.spacedBy(14.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { + Image( + painter = painterResource(R.drawable.mendoza_logo), + contentDescription = "Gobierno de Mendoza", + modifier = Modifier.fillMaxWidth().height(140.dp), + contentScale = ContentScale.Fit, + ) Text("DH Inspección", style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold) Text("Ingresá con tu huella") error?.let { Text(it, color = MaterialTheme.colorScheme.error) } diff --git a/android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/ModernMobileActsScreen.kt b/android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/ModernMobileActsScreen.kt index 50020ce..2a43b61 100644 --- a/android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/ModernMobileActsScreen.kt +++ b/android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/ModernMobileActsScreen.kt @@ -245,7 +245,7 @@ fun ModernMobileActsScreen( ) { Text( if (newActUrgency == "URGENT") { - "El plazo urgente se computará desde el Acta conforme a la política institucional vigente al bloquearla." + "El plazo urgente se computará desde el Acta conforme a la política institucional vigente al cerrar su contenido." } else { "El plazo no urgente comienza con la oficialización GEDO, no al crear el Acta." }, @@ -319,12 +319,12 @@ fun ModernMobileActsScreen( OutlinedTextField(fullName, { fullName = it }, label = { Text("Nombre y apellido *") }, modifier = Modifier.fillMaxWidth(), singleLine = true) Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(6.dp)) { listOf("DNI", "CUIL", "PASSPORT", "OTHER").forEach { kind -> - FilterChip(selected = documentType == kind, onClick = { documentType = kind }, label = { Text(kind) }) + FilterChip(selected = documentType == kind, onClick = { documentType = kind }, label = { Text(documentTypeLabelEs(kind)) }) } } OutlinedTextField(documentNumber, { documentNumber = it }, label = { Text("Documento *") }, modifier = Modifier.fillMaxWidth(), singleLine = true) OutlinedTextField(position, { position = it }, label = { Text("Cargo *") }, modifier = Modifier.fillMaxWidth(), singleLine = true) - OutlinedTextField(email, { email = it }, label = { Text("Email") }, modifier = Modifier.fillMaxWidth(), singleLine = true) + OutlinedTextField(email, { email = it }, label = { Text("Correo electrónico") }, modifier = Modifier.fillMaxWidth(), singleLine = true) OutlinedTextField(phone, { phone = it }, label = { Text("Teléfono") }, modifier = Modifier.fillMaxWidth(), singleLine = true) Button( onClick = { model.setCompanyResponsiblePresent(fullName, documentType, documentNumber, position, email, phone) }, @@ -353,17 +353,17 @@ fun ModernMobileActsScreen( Row(verticalAlignment = Alignment.CenterVertically) { Icon(Icons.Filled.Lock, null, tint = MaterialTheme.colorScheme.error) Spacer(Modifier.width(8.dp)) - Text("Finalizar contenido", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) + Text("Cerrar Acta", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) } Text( - "Al bloquear el Acta, su contenido y sus Hallazgos quedan inmutables.", + "Al cerrar el contenido, el Acta queda inmutable y pasa a Pendiente de firma. Podés firmarla ahora o continuar con otra Acta.", color = MaterialTheme.colorScheme.onSurfaceVariant, ) Button( onClick = { model.prepareSelectedAct() }, enabled = !model.busy && closure?.responsible != null, modifier = Modifier.fillMaxWidth(), - ) { Text("Finalizar y BLOQUEAR Acta") } + ) { Text("Cerrar Acta y dejar pendiente de firma") } } } } @@ -379,10 +379,10 @@ fun ModernMobileActsScreen( Row(verticalAlignment = Alignment.CenterVertically) { Icon(Icons.Filled.Lock, null, tint = MaterialTheme.colorScheme.secondary) Spacer(Modifier.width(8.dp)) - Text("Acta BLOQUEADA", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) + Text("Acta pendiente de firma", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) } - Text("El contenido ya es inmutable. Restan firmas y manifestaciones para poder sellarla.") - closure?.closure?.preparedSha256?.let { Text("Hash · $it", style = MaterialTheme.typography.bodySmall) } + Text("El contenido ya está cerrado e inmutable. Completá la firma del inspector y la manifestación de la empresa cuando corresponda.") + closure?.closure?.preparedSha256?.let { Text("Huella de integridad · $it", style = MaterialTheme.typography.bodySmall) } HorizontalDivider() Text("Firma del inspector", fontWeight = FontWeight.Bold) @@ -403,7 +403,7 @@ fun ModernMobileActsScreen( val detail = when (companyOutcome.status) { "SIGNED" -> if (companyOutcome.companyManifestation == "DISSENT") "Firma en disidencia" else "Firma en conformidad" "REFUSED" -> "Negativa a firmar" - else -> companyOutcome.status + else -> humanBackendLabelEs(companyOutcome.status) } SuccessLine(detail) companyOutcome.reason?.let { Text(it, style = MaterialTheme.typography.bodySmall) } @@ -462,7 +462,7 @@ fun ModernMobileActsScreen( ) { Icon(Icons.Filled.CheckCircle, null) Spacer(Modifier.width(6.dp)) - Text("SELLAR Acta definitivamente") + Text("Firmar y cerrar Acta") } } else { Text( @@ -480,8 +480,8 @@ fun ModernMobileActsScreen( Row(Modifier.padding(18.dp), horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.Top) { Icon(Icons.Filled.CheckCircle, null, tint = MaterialTheme.colorScheme.primary) Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(5.dp)) { - Text("Acta SELLADA e inmutable", fontWeight = FontWeight.Bold) - Text("Desde este sellado se genera el PDF del Acta y el INF Word editable para el Inspector.") + Text("Acta firmada y cerrada", fontWeight = FontWeight.Bold) + Text("Desde este cierre se genera el PDF del Acta y el informe INF editable (DOCX) para el Inspector.") closure?.closure?.finalSha256?.let { Text("SHA-256 · $it", style = MaterialTheme.typography.bodySmall) } } } @@ -503,10 +503,10 @@ fun ModernMobileActsScreen( Column(Modifier.padding(18.dp), verticalArrangement = Arrangement.spacedBy(9.dp)) { Text("Finalizar Inspección", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) if (pendingActs.isEmpty()) { - SuccessLine("Todas las Actas están SELLADAS") + SuccessLine("Todas las Actas están firmadas y cerradas") } else { Text( - "Falta SELLAR: ${pendingActs.joinToString { it.code }}", + "Falta firmar y cerrar: ${pendingActs.joinToString { it.code }}", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.error, ) @@ -531,13 +531,7 @@ private fun SuccessLine(text: String) { } } -private fun modernActStatusLabel(status: String): String = when (status) { - "DRAFT" -> "Borrador" - "LOCKED" -> "Bloqueada" - "SEALED" -> "Sellada" - "CANCELLED" -> "Cancelada" - else -> status -} +private fun modernActStatusLabel(status: String): String = actStatusLabelEs(status) private fun hasModernActLocation(context: Context): Boolean = ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED || diff --git a/android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/ModernVisitRoot.kt b/android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/ModernVisitRoot.kt index c624720..e0d2fac 100644 --- a/android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/ModernVisitRoot.kt +++ b/android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/ModernVisitRoot.kt @@ -296,6 +296,7 @@ private fun ModernFieldInventoryScreen(model: MainViewModel, onBack: () -> Unit) var parentLabel by rememberSaveable(visit.id) { mutableStateOf("") } var name by rememberSaveable(visit.id) { mutableStateOf("") } var commonName by rememberSaveable(visit.id) { mutableStateOf("") } + var description by rememberSaveable(visit.id) { mutableStateOf("") } var selectedTypeId by rememberSaveable(visit.id) { mutableStateOf(null) } var selectedFamilyId by rememberSaveable(visit.id) { mutableStateOf(null) } var familySearch by rememberSaveable(visit.id) { mutableStateOf("") } @@ -311,6 +312,7 @@ private fun ModernFieldInventoryScreen(model: MainViewModel, onBack: () -> Unit) fun resetForm() { name = "" commonName = "" + description = "" selectedTypeId = null selectedFamilyId = null familySearch = "" @@ -444,6 +446,7 @@ private fun ModernFieldInventoryScreen(model: MainViewModel, onBack: () -> Unit) familyId = selectedFamilyId, name = name, commonName = commonName, + description = description, attributes = buildModernAttributes(type, attributeValues), latitude = geo.latitude, longitude = geo.longitude, @@ -800,18 +803,26 @@ private fun ModernFieldInventoryScreen(model: MainViewModel, onBack: () -> Unit) OutlinedTextField( value = name, onValueChange = { name = it }, - label = { Text("Nombre o código visible *") }, - supportingText = { Text("Usá lo que figura en placa o identifica el elemento en campo.") }, + label = { Text("Nombre técnico *") }, + supportingText = { Text("Usá la denominación técnica del elemento. El código DH se genera automáticamente.") }, modifier = Modifier.fillMaxWidth(), singleLine = true, ) OutlinedTextField( value = commonName, onValueChange = { commonName = it }, - label = { Text("Nombre habitual") }, + label = { Text("Nombre habitual (opcional)") }, modifier = Modifier.fillMaxWidth(), singleLine = true, ) + OutlinedTextField( + value = description, + onValueChange = { description = it }, + label = { Text("Descripción (opcional)") }, + supportingText = { Text("Dato equivalente al alta desde el Dashboard.") }, + modifier = Modifier.fillMaxWidth(), + minLines = 2, + ) selectedType?.attributes?.forEach { definition -> OutlinedTextField( @@ -1073,13 +1084,7 @@ private fun modernNormalize(value: String): String = value.trim().lowercase() .replace('ó', 'o').replace('í', 'i').replace('á', 'a').replace('é', 'e').replace('ú', 'u') .replace("_", "").replace("-", "").replace(" ", "") -private fun modernStatusLabel(status: String): String = when (status) { - "PLANNED" -> "Planificada" - "IN_PROGRESS" -> "En curso" - "CLOSED" -> "Cerrada" - "CANCELLED" -> "Cancelada" - else -> status -} +private fun modernStatusLabel(status: String): String = visitStatusLabelEs(status) private fun buildModernAttributes(type: FieldType, values: Map): Map = type.attributes.mapNotNull { definition ->