Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2455a89c3e | ||
|
|
2a99183d31 | ||
|
|
675d01bea7 | ||
|
|
26d79f1e4d | ||
|
|
496bb785c1 | ||
|
|
f513817388 | ||
|
|
92a21d8f15 | ||
|
|
753fa9915f | ||
|
|
ec0ef732f7 | ||
|
|
87adf71114 | ||
|
|
0a856a1857 | ||
|
|
247c652d40 | ||
|
|
b5ee17ce95 | ||
|
|
cca2d38b98 | ||
|
|
ebce2bb957 | ||
|
|
963485618b | ||
|
|
8f051158c1 | ||
|
|
2d8425dec5 | ||
|
|
cfae2bb9ac | ||
|
|
5d8949d7d2 | ||
|
|
304adcd9b0 | ||
|
|
20dd110928 | ||
|
|
80ffb35065 | ||
|
|
6f79abbcff | ||
|
|
d18375b85b | ||
|
|
85a84e0f63 | ||
|
|
03f42209b9 |
@@ -0,0 +1,15 @@
|
|||||||
|
# Checklist final Android 0.19.0
|
||||||
|
|
||||||
|
La candidata sólo se promueve si el mismo SHA cumple:
|
||||||
|
|
||||||
|
- iniciar Inspección;
|
||||||
|
- crear/abrir Acta;
|
||||||
|
- seleccionar o crear Instalación/Subinstalación;
|
||||||
|
- exigir GPS + fotografía para Inventario nacido en campo antes de Hallazgos;
|
||||||
|
- registrar Hallazgos sobre el elemento seleccionado;
|
||||||
|
- cerrar contenido del Acta y mostrar **Pendiente de firma**;
|
||||||
|
- completar firma del Inspector y manifestación/firma o negativa de la empresa;
|
||||||
|
- mostrar el Acta como **Firmada y cerrada**;
|
||||||
|
- permitir cerrar la Inspección sólo con todas las Actas activas firmadas y cerradas;
|
||||||
|
- no exponer estados internos en inglés en la interfaz de campo;
|
||||||
|
- Android CI / RC, DH V2 CI y auditoría de dependencias en verde.
|
||||||
+57
-26
@@ -1,40 +1,71 @@
|
|||||||
# DH Inspección Android · release Campo Moderno 0.18.0
|
# DH Inspección Android · release final de campo 0.19.0
|
||||||
|
|
||||||
## Candidata vigente
|
## Candidata vigente
|
||||||
|
|
||||||
- Fase funcional: **Campo Moderno / UX dinámica de Inventario y Actas**.
|
- Fase funcional: **Flujo final de campo · Inspección → Acta → Hallazgos → Firma**.
|
||||||
- `versionName`: **0.18.0**.
|
- `versionName`: **0.19.0**.
|
||||||
- `versionCode`: **27**.
|
- `versionCode`: **28**.
|
||||||
- Application ID release: `com.korexlabs.dhinspeccion`.
|
- Application ID release: `com.korexlabs.dhinspeccion`.
|
||||||
- Application ID debug/QA: `com.korexlabs.dhinspeccion.debug`.
|
- Application ID debug/QA: `com.korexlabs.dhinspeccion.debug`.
|
||||||
- API: `https://dhv2.korexlabs.com/api/v3/`.
|
- API: `https://dhv2.korexlabs.com/api/v3/`.
|
||||||
|
|
||||||
La variante debug es independiente de la app productiva y puede instalarse para QA/presentación sin sobrescribir una instalación release histórica.
|
La variante debug es independiente de la app productiva y puede instalarse para QA/presentación sin sobrescribir una instalación release histórica.
|
||||||
|
|
||||||
## Novedades de campo 0.18.0
|
## Procedimiento operativo validado
|
||||||
|
|
||||||
La APK adopta una capa visual Material 3 propia de DH: superficies más limpias, jerarquía visual más clara, botones y estados más legibles, tarjetas con menor ruido y una navegación consistente entre Inspección, Actas e Inventario.
|
La APK 0.19.0 fija como recorrido principal de campo:
|
||||||
|
|
||||||
La carga de una Subinstalación mantiene la integridad del modelo físico pero reduce pasos:
|
1. **Iniciar Inspección**. Al iniciarla se habilitan Actas, Hallazgos e Inventario de campo.
|
||||||
|
2. **Crear o abrir un Acta**. Puede haber varias Actas dentro de una misma Inspección, pero sólo una puede permanecer en elaboración al mismo tiempo.
|
||||||
|
3. **Agregar Hallazgos** sobre una Instalación o Subinstalación seleccionada.
|
||||||
|
4. Si el elemento ya existe, se lo selecciona desde el Inventario del Área/Yacimiento de la Inspección.
|
||||||
|
5. Si no existe, se da de alta desde campo sin abandonar el Acta.
|
||||||
|
6. Un elemento nuevo debe completar **GPS + fotografía** antes de poder recibir Hallazgos.
|
||||||
|
7. Al terminar el contenido del Acta se usa **Cerrar Acta y dejar pendiente de firma**. Desde ese momento el contenido queda inmutable y el estado visible es **Pendiente de firma**.
|
||||||
|
8. La firma del Inspector y la manifestación/firma o negativa de la empresa completan el Acta, que pasa a **Firmada y cerrada**.
|
||||||
|
9. La Inspección sólo puede cerrarse cuando todas sus Actas no canceladas están firmadas y cerradas.
|
||||||
|
|
||||||
1. el buscador de **Instalaciones padre** ahora consulta mientras el inspector escribe;
|
La nomenclatura técnica interna de API (`DRAFT`, `LOCKED`, `SEALED`, etc.) no se muestra al inspector: la interfaz usa textos operativos en castellano.
|
||||||
2. cada coincidencia aparece como tarjeta seleccionable y tocarla avanza directamente a la identificación de la Subinstalación;
|
|
||||||
3. la clasificación técnica continúa filtrada por compatibilidad con la Instalación elegida;
|
|
||||||
4. **Guardar y tomar foto** captura GPS y abre la cámara automáticamente.
|
|
||||||
|
|
||||||
Se conserva la regla de que un elemento nacido en campo no puede recibir Hallazgos hasta contar con GPS y fotografía.
|
## Alta de Instalaciones y Subinstalaciones
|
||||||
|
|
||||||
## Actas en Android
|
El alta móvil queda alineada con el Dashboard y con las validaciones del servidor.
|
||||||
|
|
||||||
El espacio de Actas fue rediseñado para mostrar con claridad:
|
### Instalación
|
||||||
|
|
||||||
- contexto de la Inspección y Yacimiento;
|
- contexto territorial: Yacimiento/Área de la Inspección;
|
||||||
- estado de cada Acta y cantidad de Hallazgos;
|
- clasificación técnica compatible;
|
||||||
- urgencia mediante selección explícita;
|
- **Nombre técnico** obligatorio;
|
||||||
- responsable de empresa, bloqueo, firmas, manifestación y sellado;
|
- Nombre habitual opcional;
|
||||||
- condición necesaria para cerrar la Inspección.
|
- Descripción opcional;
|
||||||
|
- atributos técnicos definidos por el tipo; sólo los marcados como obligatorios bloquean el guardado;
|
||||||
|
- código DH generado por el sistema cuando corresponde;
|
||||||
|
- GPS de la tablet;
|
||||||
|
- fotografía obligatoria antes de crear Hallazgos.
|
||||||
|
|
||||||
El listado que consume Android usa un read-model móvil liviano (`/inspection-visits/:visitId/acts/mobile`). Esta lectura no depende del módulo documental de oficina ni de `inspection_reports`, de modo que entrar a una Inspección en campo no queda acoplado a la proyección de Informes/GEDO.
|
### Subinstalación
|
||||||
|
|
||||||
|
Además de lo anterior requiere elegir primero la **Instalación padre**. El buscador consulta mientras se escribe y cada resultado es seleccionable tocando la tarjeta.
|
||||||
|
|
||||||
|
La clasificación y los atributos se obtienen dinámicamente desde el mismo catálogo administrado por el Dashboard; la APK no mantiene listas técnicas paralelas.
|
||||||
|
|
||||||
|
## Actas y firma
|
||||||
|
|
||||||
|
La terminología visible se simplifica:
|
||||||
|
|
||||||
|
- `DRAFT` → **En elaboración**;
|
||||||
|
- `LOCKED` → **Pendiente de firma**;
|
||||||
|
- `SEALED` → **Firmada y cerrada**.
|
||||||
|
|
||||||
|
Cerrar el contenido no cierra automáticamente la Inspección y tampoco obliga a terminar la firma en ese instante: puede continuarse con otra Acta. Una vez cerrado el contenido, el Acta es inmutable.
|
||||||
|
|
||||||
|
Después de la firma/cierre definitivo se mantiene el circuito documental existente: PDF del Acta e informe INF editable (DOCX) para oficina.
|
||||||
|
|
||||||
|
## Castellano e identidad visual
|
||||||
|
|
||||||
|
Se revisaron las pantallas activas de campo para evitar exponer estados y opciones internas en inglés. Entre otros cambios: Pasaporte/Otro en documentos, estados de Inspección/Acta/Hallazgo en castellano, Correo electrónico, Huella de integridad y terminología de cierre orientada al usuario.
|
||||||
|
|
||||||
|
La pantalla de ingreso y el desbloqueo biométrico utilizan la identidad visual de Mendoza ya incorporada al proyecto.
|
||||||
|
|
||||||
## Barrera obligatoria
|
## Barrera obligatoria
|
||||||
|
|
||||||
@@ -42,16 +73,16 @@ Todo cambio Android o de API que pueda afectar al cliente móvil debe pasar `And
|
|||||||
|
|
||||||
1. validación de identidad, HTTPS y políticas básicas del manifest;
|
1. validación de identidad, HTTPS y políticas básicas del manifest;
|
||||||
2. Android lint;
|
2. Android lint;
|
||||||
3. unit tests Android reales, con verificación de que exista al menos una prueba ejecutada;
|
3. pruebas unitarias Android reales, con verificación de que exista al menos una prueba ejecutada;
|
||||||
4. `assembleDebug`;
|
4. `assembleDebug`;
|
||||||
5. `assembleRelease` para comprobar que la variante productiva compile;
|
5. `assembleRelease` para comprobar que la variante productiva compile;
|
||||||
6. empaquetado del APK debug con SHA-256 y metadata de commit/versionado.
|
6. empaquetado del APK debug con SHA-256 y metadata de commit/versionado.
|
||||||
|
|
||||||
Además, `DH V2 CI` debe mantener verdes API, WEB y contrato Docker/migraciones antes de promover el cambio.
|
Además, `DH V2 CI` debe mantener verdes API, WEB y contrato Docker/migraciones antes de promover cambios que afecten al servidor.
|
||||||
|
|
||||||
## Firma release
|
## Firma release
|
||||||
|
|
||||||
La clave histórica de firma **no se versiona ni se reemplaza**. La CI compila la variante release para detectar roturas, pero la APK productiva final debe firmarse con la clave histórica antes de instalarse como actualización de `com.korexlabs.dhinspeccion`.
|
La clave histórica de firma **no se versiona ni se reemplaza**. La CI compila la variante release para detectar roturas, pero una APK productiva que deba actualizar `com.korexlabs.dhinspeccion` tiene que firmarse con la clave histórica.
|
||||||
|
|
||||||
No se debe crear una clave nueva para resolver una falta de acceso: eso rompería la continuidad de actualización de tablets que ya tengan una versión firmada con la clave anterior.
|
No se debe crear una clave nueva para resolver una falta de acceso: eso rompería la continuidad de actualización de tablets que ya tengan una versión firmada con la clave anterior.
|
||||||
|
|
||||||
@@ -60,7 +91,7 @@ No se debe crear una clave nueva para resolver una falta de acceso: eso romperí
|
|||||||
Antes de distribuir una APK productiva:
|
Antes de distribuir una APK productiva:
|
||||||
|
|
||||||
- todas las barreras de CI del SHA exacto deben estar verdes;
|
- todas las barreras de CI del SHA exacto deben estar verdes;
|
||||||
- comprobar certificado/huella de firma contra la versión histórica;
|
- comprobar certificado/huella de firma contra la versión histórica si se distribuirá como actualización productiva;
|
||||||
- realizar actualización sobre al menos una tablet con la versión productiva anterior;
|
- realizar actualización sobre al menos una tablet con la versión productiva anterior cuando corresponda;
|
||||||
- ejecutar smoke funcional contra el entorno objetivo: login, abrir Inspección, abrir Actas sin error, crear Acta, búsqueda dinámica de Instalación, selección de padre, alta de Subinstalación, GPS/foto, Hallazgo y cierre;
|
- ejecutar smoke funcional: ingreso, iniciar Inspección, crear Acta, seleccionar/crear Instalación o Subinstalación, GPS/foto, Hallazgo, cerrar Acta → Pendiente de firma, firma/manifestación, cierre de Acta y cierre de Inspección;
|
||||||
- registrar el SHA Git y SHA-256 de la APK distribuida.
|
- registrar el SHA Git y SHA-256 de la APK distribuida.
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ android {
|
|||||||
applicationId = "com.korexlabs.dhinspeccion"
|
applicationId = "com.korexlabs.dhinspeccion"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 27
|
versionCode = 28
|
||||||
versionName = "0.18.0"
|
versionName = "0.19.0"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (acts.any { it.status == "DRAFT" }) {
|
if (acts.any { it.status == "DRAFT" }) {
|
||||||
error = "Ya existe un Acta en borrador. Bloqueala o cancelala antes de crear la siguiente."
|
error = "Ya existe un Acta en elaboración. Cerrala o cancelala antes de crear la siguiente."
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
launchBusy {
|
launchBusy {
|
||||||
@@ -241,6 +241,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
latitude: Double,
|
latitude: Double,
|
||||||
longitude: Double,
|
longitude: Double,
|
||||||
accuracyM: Double?,
|
accuracyM: Double?,
|
||||||
|
description: String? = null,
|
||||||
) {
|
) {
|
||||||
val visitId = visit?.id ?: return
|
val visitId = visit?.id ?: return
|
||||||
if (visit?.status != "IN_PROGRESS") {
|
if (visit?.status != "IN_PROGRESS") {
|
||||||
@@ -258,6 +259,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
familyId = familyId,
|
familyId = familyId,
|
||||||
name = name.trim(),
|
name = name.trim(),
|
||||||
commonName = commonName?.trim()?.takeIf { it.isNotBlank() },
|
commonName = commonName?.trim()?.takeIf { it.isNotBlank() },
|
||||||
|
description = description?.trim()?.takeIf { it.isNotBlank() },
|
||||||
attributes = attributes,
|
attributes = attributes,
|
||||||
deviceLatitude = latitude,
|
deviceLatitude = latitude,
|
||||||
deviceLongitude = longitude,
|
deviceLongitude = longitude,
|
||||||
@@ -340,7 +342,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
val assetId = selectedFieldAsset?.asset?.id ?: return
|
val assetId = selectedFieldAsset?.asset?.id ?: return
|
||||||
val draft = selectedDraftAct()
|
val draft = selectedDraftAct()
|
||||||
if (draft == null) {
|
if (draft == null) {
|
||||||
error = "Creá o seleccioná el Acta en borrador antes de registrar Hallazgos."
|
error = "Creá o seleccioná el Acta en elaboración antes de registrar Hallazgos."
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
launchBusy {
|
launchBusy {
|
||||||
@@ -362,7 +364,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
val assetId = selectedFieldAsset?.asset?.id ?: return
|
val assetId = selectedFieldAsset?.asset?.id ?: return
|
||||||
val actId = selectedDraftAct()?.id
|
val actId = selectedDraftAct()?.id
|
||||||
if (actId == null) {
|
if (actId == null) {
|
||||||
error = "No hay un Acta en borrador seleccionada."
|
error = "No hay un Acta en elaboración seleccionada."
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (description.isBlank()) {
|
if (description.isBlank()) {
|
||||||
@@ -471,7 +473,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
absenceReason = reason.trim(),
|
absenceReason = reason.trim(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
notice = "Ausencia del responsable registrada. La manifestación de empresa quedará pendiente y deberá resolverse antes de sellar el Acta."
|
notice = "Ausencia del responsable registrada. La manifestación de empresa quedará pendiente y deberá resolverse antes de firmar y cerrar el Acta."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -480,12 +482,12 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
launchBusy {
|
launchBusy {
|
||||||
actClosure = actsRepository.lock(actId)
|
actClosure = actsRepository.lock(actId)
|
||||||
refreshSelectedActInternal(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() {
|
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(
|
fun signSelectedActAsInspector(
|
||||||
@@ -546,7 +548,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
refreshSelectedActInternal(actId)
|
refreshSelectedActInternal(actId)
|
||||||
loadActsInternal(currentVisit.id, selectDraft = false)
|
loadActsInternal(currentVisit.id, selectDraft = false)
|
||||||
clearFindingState()
|
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 +557,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
launchBusy {
|
launchBusy {
|
||||||
visit = actsRepository.closeVisit(visitId)
|
visit = actsRepository.closeVisit(visitId)
|
||||||
loadVisitsInternal()
|
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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ fun FieldFindingScreen(model: MainViewModel) {
|
|||||||
Text(asset.name, fontWeight = FontWeight.Bold)
|
Text(asset.name, fontWeight = FontWeight.Bold)
|
||||||
Text("${asset.code} · Acta ${options.act.code}", style = MaterialTheme.typography.bodySmall)
|
Text("${asset.code} · Acta ${options.act.code}", style = MaterialTheme.typography.bodySmall)
|
||||||
Text(
|
Text(
|
||||||
"GPS + foto del Inventario: ${if (options.capture.readyForFinding) "OK" else "pendiente"}",
|
"GPS + foto del Inventario: ${if (options.capture.readyForFinding) "completo" else "pendiente"}",
|
||||||
color = if (options.capture.readyForFinding) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error,
|
color = if (options.capture.readyForFinding) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
)
|
)
|
||||||
@@ -210,7 +210,7 @@ fun FieldFindingScreen(model: MainViewModel) {
|
|||||||
Card(Modifier.fillMaxWidth()) {
|
Card(Modifier.fillMaxWidth()) {
|
||||||
Column(Modifier.padding(10.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
Column(Modifier.padding(10.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
Text("${finding.code} · ${finding.title}", fontWeight = FontWeight.SemiBold)
|
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(
|
Text(
|
||||||
"Evidencias: ${evidence.size} · Fotos: ${evidence.count { it.kind == "PHOTO" }}",
|
"Evidencias: ${evidence.size} · Fotos: ${evidence.count { it.kind == "PHOTO" }}",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ package com.korexlabs.dhinspeccion.ui
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.biometric.BiometricManager
|
import androidx.biometric.BiometricManager
|
||||||
import androidx.biometric.BiometricPrompt
|
import androidx.biometric.BiometricPrompt
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Visibility
|
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.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
@@ -37,6 +41,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.korexlabs.dhinspeccion.MainViewModel
|
import com.korexlabs.dhinspeccion.MainViewModel
|
||||||
|
import com.korexlabs.dhinspeccion.R
|
||||||
|
|
||||||
private const val BIOMETRIC_PREFS = "dh_v2_biometric"
|
private const val BIOMETRIC_PREFS = "dh_v2_biometric"
|
||||||
private const val BIOMETRIC_ENABLED = "enabled"
|
private const val BIOMETRIC_ENABLED = "enabled"
|
||||||
@@ -105,7 +110,13 @@ private fun EnhancedLoginScreen(model: MainViewModel, onPasswordLogin: () -> Uni
|
|||||||
var passwordVisible by rememberSaveable { mutableStateOf(false) }
|
var passwordVisible by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
Box(Modifier.fillMaxSize().padding(24.dp), contentAlignment = Alignment.Center) {
|
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.ic_mendoza_launcher_exact),
|
||||||
|
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("DH Inspección", style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold)
|
||||||
Text("Aplicación de campo · Dirección de Hidrocarburos")
|
Text("Aplicación de campo · Dirección de Hidrocarburos")
|
||||||
model.error?.let { Text(it, color = MaterialTheme.colorScheme.error) }
|
model.error?.let { Text(it, color = MaterialTheme.colorScheme.error) }
|
||||||
@@ -113,7 +124,7 @@ private fun EnhancedLoginScreen(model: MainViewModel, onPasswordLogin: () -> Uni
|
|||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = identifier,
|
value = identifier,
|
||||||
onValueChange = { identifier = it },
|
onValueChange = { identifier = it },
|
||||||
label = { Text("Usuario o email") },
|
label = { Text("Usuario o correo electrónico") },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
)
|
)
|
||||||
@@ -198,6 +209,12 @@ private fun BiometricUnlockScreen(
|
|||||||
verticalArrangement = Arrangement.spacedBy(14.dp),
|
verticalArrangement = Arrangement.spacedBy(14.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.ic_mendoza_launcher_exact),
|
||||||
|
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("DH Inspección", style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold)
|
||||||
Text("Ingresá con tu huella")
|
Text("Ingresá con tu huella")
|
||||||
error?.let { Text(it, color = MaterialTheme.colorScheme.error) }
|
error?.let { Text(it, color = MaterialTheme.colorScheme.error) }
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ private fun MobileVisitCard(visit: VisitSummary, onOpen: () -> Unit) {
|
|||||||
Column(Modifier.padding(14.dp), verticalArrangement = Arrangement.spacedBy(5.dp)) {
|
Column(Modifier.padding(14.dp), verticalArrangement = Arrangement.spacedBy(5.dp)) {
|
||||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||||
Text(visit.code, fontWeight = FontWeight.Bold)
|
Text(visit.code, fontWeight = FontWeight.Bold)
|
||||||
Text(visit.status)
|
Text(visitStatusLabelEs(visit.status))
|
||||||
}
|
}
|
||||||
Text(visit.operatorCompany?.name ?: "Operadora sin definir")
|
Text(visit.operatorCompany?.name ?: "Operadora sin definir")
|
||||||
Text(visit.operationalArea?.name ?: "Área sin definir", style = MaterialTheme.typography.bodySmall)
|
Text(visit.operationalArea?.name ?: "Área sin definir", style = MaterialTheme.typography.bodySmall)
|
||||||
|
|||||||
+17
-23
@@ -245,7 +245,7 @@ fun ModernMobileActsScreen(
|
|||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
if (newActUrgency == "URGENT") {
|
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 {
|
} else {
|
||||||
"El plazo no urgente comienza con la oficialización GEDO, no al crear el Acta."
|
"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)
|
OutlinedTextField(fullName, { fullName = it }, label = { Text("Nombre y apellido *") }, modifier = Modifier.fillMaxWidth(), singleLine = true)
|
||||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
listOf("DNI", "CUIL", "PASSPORT", "OTHER").forEach { kind ->
|
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(documentNumber, { documentNumber = it }, label = { Text("Documento *") }, modifier = Modifier.fillMaxWidth(), singleLine = true)
|
||||||
OutlinedTextField(position, { position = it }, label = { Text("Cargo *") }, 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)
|
OutlinedTextField(phone, { phone = it }, label = { Text("Teléfono") }, modifier = Modifier.fillMaxWidth(), singleLine = true)
|
||||||
Button(
|
Button(
|
||||||
onClick = { model.setCompanyResponsiblePresent(fullName, documentType, documentNumber, position, email, phone) },
|
onClick = { model.setCompanyResponsiblePresent(fullName, documentType, documentNumber, position, email, phone) },
|
||||||
@@ -353,17 +353,17 @@ fun ModernMobileActsScreen(
|
|||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Icon(Icons.Filled.Lock, null, tint = MaterialTheme.colorScheme.error)
|
Icon(Icons.Filled.Lock, null, tint = MaterialTheme.colorScheme.error)
|
||||||
Spacer(Modifier.width(8.dp))
|
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(
|
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,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
Button(
|
Button(
|
||||||
onClick = { model.prepareSelectedAct() },
|
onClick = { model.prepareSelectedAct() },
|
||||||
enabled = !model.busy && closure?.responsible != null,
|
enabled = !model.busy && closure?.responsible != null,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
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) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Icon(Icons.Filled.Lock, null, tint = MaterialTheme.colorScheme.secondary)
|
Icon(Icons.Filled.Lock, null, tint = MaterialTheme.colorScheme.secondary)
|
||||||
Spacer(Modifier.width(8.dp))
|
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.")
|
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("Hash · $it", style = MaterialTheme.typography.bodySmall) }
|
closure?.closure?.preparedSha256?.let { Text("Huella de integridad · $it", style = MaterialTheme.typography.bodySmall) }
|
||||||
|
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
Text("Firma del inspector", fontWeight = FontWeight.Bold)
|
Text("Firma del inspector", fontWeight = FontWeight.Bold)
|
||||||
@@ -403,7 +403,7 @@ fun ModernMobileActsScreen(
|
|||||||
val detail = when (companyOutcome.status) {
|
val detail = when (companyOutcome.status) {
|
||||||
"SIGNED" -> if (companyOutcome.companyManifestation == "DISSENT") "Firma en disidencia" else "Firma en conformidad"
|
"SIGNED" -> if (companyOutcome.companyManifestation == "DISSENT") "Firma en disidencia" else "Firma en conformidad"
|
||||||
"REFUSED" -> "Negativa a firmar"
|
"REFUSED" -> "Negativa a firmar"
|
||||||
else -> companyOutcome.status
|
else -> humanBackendLabelEs(companyOutcome.status)
|
||||||
}
|
}
|
||||||
SuccessLine(detail)
|
SuccessLine(detail)
|
||||||
companyOutcome.reason?.let { Text(it, style = MaterialTheme.typography.bodySmall) }
|
companyOutcome.reason?.let { Text(it, style = MaterialTheme.typography.bodySmall) }
|
||||||
@@ -413,7 +413,7 @@ fun ModernMobileActsScreen(
|
|||||||
Row(Modifier.padding(12.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
Row(Modifier.padding(12.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
Icon(Icons.Filled.ErrorOutline, null, tint = MaterialTheme.colorScheme.error)
|
Icon(Icons.Filled.ErrorOutline, null, tint = MaterialTheme.colorScheme.error)
|
||||||
Text(
|
Text(
|
||||||
"La ausencia no resuelve la manifestación. Debe registrarse firma o negativa antes de sellar.",
|
"La ausencia no resuelve la manifestación. Debe registrarse firma o negativa antes de firmar y cerrar el Acta.",
|
||||||
Modifier.weight(1f),
|
Modifier.weight(1f),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
)
|
)
|
||||||
@@ -462,7 +462,7 @@ fun ModernMobileActsScreen(
|
|||||||
) {
|
) {
|
||||||
Icon(Icons.Filled.CheckCircle, null)
|
Icon(Icons.Filled.CheckCircle, null)
|
||||||
Spacer(Modifier.width(6.dp))
|
Spacer(Modifier.width(6.dp))
|
||||||
Text("SELLAR Acta definitivamente")
|
Text("Firmar y cerrar Acta")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Text(
|
Text(
|
||||||
@@ -480,8 +480,8 @@ fun ModernMobileActsScreen(
|
|||||||
Row(Modifier.padding(18.dp), horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.Top) {
|
Row(Modifier.padding(18.dp), horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.Top) {
|
||||||
Icon(Icons.Filled.CheckCircle, null, tint = MaterialTheme.colorScheme.primary)
|
Icon(Icons.Filled.CheckCircle, null, tint = MaterialTheme.colorScheme.primary)
|
||||||
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(5.dp)) {
|
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(5.dp)) {
|
||||||
Text("Acta SELLADA e inmutable", fontWeight = FontWeight.Bold)
|
Text("Acta firmada y cerrada", fontWeight = FontWeight.Bold)
|
||||||
Text("Desde este sellado se genera el PDF del Acta y el INF Word editable para el Inspector.")
|
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) }
|
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)) {
|
Column(Modifier.padding(18.dp), verticalArrangement = Arrangement.spacedBy(9.dp)) {
|
||||||
Text("Finalizar Inspección", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
Text("Finalizar Inspección", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
||||||
if (pendingActs.isEmpty()) {
|
if (pendingActs.isEmpty()) {
|
||||||
SuccessLine("Todas las Actas están SELLADAS")
|
SuccessLine("Todas las Actas están firmadas y cerradas")
|
||||||
} else {
|
} else {
|
||||||
Text(
|
Text(
|
||||||
"Falta SELLAR: ${pendingActs.joinToString { it.code }}",
|
"Falta firmar y cerrar: ${pendingActs.joinToString { it.code }}",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.error,
|
color = MaterialTheme.colorScheme.error,
|
||||||
)
|
)
|
||||||
@@ -531,13 +531,7 @@ private fun SuccessLine(text: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun modernActStatusLabel(status: String): String = when (status) {
|
private fun modernActStatusLabel(status: String): String = actStatusLabelEs(status)
|
||||||
"DRAFT" -> "Borrador"
|
|
||||||
"LOCKED" -> "Bloqueada"
|
|
||||||
"SEALED" -> "Sellada"
|
|
||||||
"CANCELLED" -> "Cancelada"
|
|
||||||
else -> status
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hasModernActLocation(context: Context): Boolean =
|
private fun hasModernActLocation(context: Context): Boolean =
|
||||||
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED ||
|
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED ||
|
||||||
|
|||||||
@@ -274,7 +274,7 @@ private fun ModernChecklistCard(visit: VisitDetail) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (checklist.stale) {
|
if (checklist.stale) {
|
||||||
Text("El checklist requiere actualización.", color = MaterialTheme.colorScheme.error)
|
Text("La lista de control requiere actualización.", color = MaterialTheme.colorScheme.error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -296,6 +296,7 @@ private fun ModernFieldInventoryScreen(model: MainViewModel, onBack: () -> Unit)
|
|||||||
var parentLabel by rememberSaveable(visit.id) { mutableStateOf("") }
|
var parentLabel by rememberSaveable(visit.id) { mutableStateOf("") }
|
||||||
var name by rememberSaveable(visit.id) { mutableStateOf("") }
|
var name by rememberSaveable(visit.id) { mutableStateOf("") }
|
||||||
var commonName 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<String?>(null) }
|
var selectedTypeId by rememberSaveable(visit.id) { mutableStateOf<String?>(null) }
|
||||||
var selectedFamilyId by rememberSaveable(visit.id) { mutableStateOf<String?>(null) }
|
var selectedFamilyId by rememberSaveable(visit.id) { mutableStateOf<String?>(null) }
|
||||||
var familySearch by rememberSaveable(visit.id) { mutableStateOf("") }
|
var familySearch by rememberSaveable(visit.id) { mutableStateOf("") }
|
||||||
@@ -311,6 +312,7 @@ private fun ModernFieldInventoryScreen(model: MainViewModel, onBack: () -> Unit)
|
|||||||
fun resetForm() {
|
fun resetForm() {
|
||||||
name = ""
|
name = ""
|
||||||
commonName = ""
|
commonName = ""
|
||||||
|
description = ""
|
||||||
selectedTypeId = null
|
selectedTypeId = null
|
||||||
selectedFamilyId = null
|
selectedFamilyId = null
|
||||||
familySearch = ""
|
familySearch = ""
|
||||||
@@ -444,6 +446,7 @@ private fun ModernFieldInventoryScreen(model: MainViewModel, onBack: () -> Unit)
|
|||||||
familyId = selectedFamilyId,
|
familyId = selectedFamilyId,
|
||||||
name = name,
|
name = name,
|
||||||
commonName = commonName,
|
commonName = commonName,
|
||||||
|
description = description,
|
||||||
attributes = buildModernAttributes(type, attributeValues),
|
attributes = buildModernAttributes(type, attributeValues),
|
||||||
latitude = geo.latitude,
|
latitude = geo.latitude,
|
||||||
longitude = geo.longitude,
|
longitude = geo.longitude,
|
||||||
@@ -800,18 +803,26 @@ private fun ModernFieldInventoryScreen(model: MainViewModel, onBack: () -> Unit)
|
|||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = name,
|
value = name,
|
||||||
onValueChange = { name = it },
|
onValueChange = { name = it },
|
||||||
label = { Text("Nombre o código visible *") },
|
label = { Text("Nombre técnico *") },
|
||||||
supportingText = { Text("Usá lo que figura en placa o identifica el elemento en campo.") },
|
supportingText = { Text("Usá la denominación técnica del elemento. El código DH se genera automáticamente.") },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
)
|
)
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = commonName,
|
value = commonName,
|
||||||
onValueChange = { commonName = it },
|
onValueChange = { commonName = it },
|
||||||
label = { Text("Nombre habitual") },
|
label = { Text("Nombre habitual (opcional)") },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
)
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = description,
|
||||||
|
onValueChange = { description = it },
|
||||||
|
label = { Text("Descripción (opcional)") },
|
||||||
|
supportingText = { Text("Dato equivalente al alta desde el panel web.") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
minLines = 2,
|
||||||
|
)
|
||||||
|
|
||||||
selectedType?.attributes?.forEach { definition ->
|
selectedType?.attributes?.forEach { definition ->
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
@@ -1066,20 +1077,14 @@ private fun ModernStepHeader(step: Int, title: String) {
|
|||||||
|
|
||||||
private fun modernItemTypeCode(item: FieldInventoryItem): String {
|
private fun modernItemTypeCode(item: FieldInventoryItem): String {
|
||||||
val type = item.type ?: return ""
|
val type = item.type ?: return ""
|
||||||
return modernNormalize(type.typeName ?: type.name)
|
return modernNormalize(type.code.ifBlank { type.typeName ?: type.name })
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun modernNormalize(value: String): String = value.trim().lowercase()
|
private fun modernNormalize(value: String): String = value.trim().lowercase()
|
||||||
.replace('ó', 'o').replace('í', 'i').replace('á', 'a').replace('é', 'e').replace('ú', 'u')
|
.replace('ó', 'o').replace('í', 'i').replace('á', 'a').replace('é', 'e').replace('ú', 'u')
|
||||||
.replace("_", "").replace("-", "").replace(" ", "")
|
.replace("_", "").replace("-", "").replace(" ", "")
|
||||||
|
|
||||||
private fun modernStatusLabel(status: String): String = when (status) {
|
private fun modernStatusLabel(status: String): String = visitStatusLabelEs(status)
|
||||||
"PLANNED" -> "Planificada"
|
|
||||||
"IN_PROGRESS" -> "En curso"
|
|
||||||
"CLOSED" -> "Cerrada"
|
|
||||||
"CANCELLED" -> "Cancelada"
|
|
||||||
else -> status
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildModernAttributes(type: FieldType, values: Map<String, String>): Map<String, Any?> =
|
private fun buildModernAttributes(type: FieldType, values: Map<String, String>): Map<String, Any?> =
|
||||||
type.attributes.mapNotNull { definition ->
|
type.attributes.mapNotNull { definition ->
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ fun SignaturePad(
|
|||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
Text(label, fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold)
|
Text(label, fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold)
|
||||||
Text(
|
Text(
|
||||||
"Firmá dentro del recuadro. La imagen se guarda como PNG y se incorpora al hash del Acta.",
|
"Firmá dentro del recuadro. La imagen se guarda como PNG y se incorpora a la huella de integridad del Acta.",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
)
|
)
|
||||||
Canvas(
|
Canvas(
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package com.korexlabs.dhinspeccion.ui
|
||||||
|
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
internal fun visitStatusLabelEs(status: String?): String = when (status?.uppercase(Locale.ROOT)) {
|
||||||
|
"PLANNED" -> "Planificada"
|
||||||
|
"IN_PROGRESS" -> "En curso"
|
||||||
|
"CLOSED" -> "Cerrada"
|
||||||
|
"CANCELLED" -> "Cancelada"
|
||||||
|
null, "" -> "Sin estado"
|
||||||
|
else -> humanBackendLabelEs(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun actStatusLabelEs(status: String?): String = when (status?.uppercase(Locale.ROOT)) {
|
||||||
|
"DRAFT" -> "En elaboración"
|
||||||
|
"LOCKED" -> "Pendiente de firma"
|
||||||
|
"SEALED" -> "Firmada y cerrada"
|
||||||
|
"CLOSED" -> "Cerrada"
|
||||||
|
"RECTIFIED" -> "Rectificada"
|
||||||
|
"CANCELLED" -> "Cancelada"
|
||||||
|
null, "" -> "Sin estado"
|
||||||
|
else -> humanBackendLabelEs(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun findingStatusLabelEs(status: String?): String = when (status?.uppercase(Locale.ROOT)) {
|
||||||
|
"OPEN" -> "Abierto"
|
||||||
|
"PENDING" -> "Pendiente"
|
||||||
|
"COMPANY_RESPONSE_PENDING" -> "Pendiente de respuesta de la empresa"
|
||||||
|
"COMPANY_RESPONDED" -> "Respondido por la empresa"
|
||||||
|
"VERIFICATION_PENDING" -> "Pendiente de verificación"
|
||||||
|
"VERIFICATION_SCHEDULED" -> "Verificación programada"
|
||||||
|
"READY_TO_CLOSE" -> "Listo para cierre"
|
||||||
|
"CLOSED" -> "Cerrado"
|
||||||
|
"CANCELLED" -> "Cancelado"
|
||||||
|
null, "" -> "Sin estado"
|
||||||
|
else -> humanBackendLabelEs(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun documentTypeLabelEs(type: String): String = when (type.uppercase(Locale.ROOT)) {
|
||||||
|
"DNI" -> "DNI"
|
||||||
|
"CUIL" -> "CUIL"
|
||||||
|
"PASSPORT" -> "Pasaporte"
|
||||||
|
"OTHER" -> "Otro"
|
||||||
|
else -> humanBackendLabelEs(type)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun humanBackendLabelEs(value: String): String {
|
||||||
|
val clean = value.trim().replace('_', ' ').replace('-', ' ').lowercase(Locale("es", "AR"))
|
||||||
|
return clean.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale("es", "AR")) else it.toString() }
|
||||||
|
}
|
||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
package com.korexlabs.dhinspeccion
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class FinalFieldFlowContractTest {
|
||||||
|
private val visit = File("src/main/java/com/korexlabs/dhinspeccion/ui/ModernVisitRoot.kt").readText()
|
||||||
|
private val acts = File("src/main/java/com/korexlabs/dhinspeccion/ui/ModernMobileActsScreen.kt").readText()
|
||||||
|
private val findings = File("src/main/java/com/korexlabs/dhinspeccion/ui/FieldFindingScreen.kt").readText()
|
||||||
|
private val home = File("src/main/java/com/korexlabs/dhinspeccion/ui/MobileHomeScreen.kt").readText()
|
||||||
|
private val labels = File("src/main/java/com/korexlabs/dhinspeccion/ui/UiSpanishLabels.kt").readText()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun inspectionActFindingFlowMatchesFieldProcedure() {
|
||||||
|
assertTrue(visit.contains("Iniciar inspección"))
|
||||||
|
assertTrue(acts.contains("Crear nueva Acta"))
|
||||||
|
assertTrue(acts.contains("Agregar Hallazgo"))
|
||||||
|
assertTrue(visit.contains("Instalación"))
|
||||||
|
assertTrue(visit.contains("Subinstalación"))
|
||||||
|
assertTrue(visit.contains("Guardar y tomar foto"))
|
||||||
|
assertTrue(acts.contains("Cerrar Acta y dejar pendiente de firma"))
|
||||||
|
assertTrue(labels.contains("\"LOCKED\" -> \"Pendiente de firma\""))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun fieldCreateKeepsDashboardEquivalentMinimumAndCaptureBarrier() {
|
||||||
|
assertTrue(visit.contains("Nombre técnico *"))
|
||||||
|
assertTrue(visit.contains("Nombre habitual (opcional)"))
|
||||||
|
assertTrue(visit.contains("Descripción (opcional)"))
|
||||||
|
assertTrue(visit.contains("familyReady"))
|
||||||
|
assertTrue(visit.contains("attributesReady"))
|
||||||
|
assertTrue(visit.contains("GPS + foto"))
|
||||||
|
assertTrue(visit.contains("La foto es obligatoria antes de registrar Hallazgos"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun structuralSelectionUsesTheServerTypeCode() {
|
||||||
|
assertTrue(visit.contains("type.code.ifBlank { type.typeName ?: type.name }"))
|
||||||
|
assertTrue(visit.contains("modernItemTypeCode(it) in setOf(\"instalacion\", \"subinstalacion\")"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun currentFieldUiDoesNotExposeBackendEnglishStates() {
|
||||||
|
assertTrue(home.contains("visitStatusLabelEs(visit.status)"))
|
||||||
|
assertTrue(findings.contains("findingStatusLabelEs(finding.status)"))
|
||||||
|
assertTrue(acts.contains("documentTypeLabelEs(kind)"))
|
||||||
|
assertTrue(labels.contains("\"PASSPORT\" -> \"Pasaporte\""))
|
||||||
|
assertTrue(labels.contains("\"OTHER\" -> \"Otro\""))
|
||||||
|
assertFalse(acts.contains("Finalizar y BLOQUEAR Acta"))
|
||||||
|
assertFalse(acts.contains("Acta BLOQUEADA"))
|
||||||
|
assertFalse(acts.contains("SELLAR Acta"))
|
||||||
|
assertFalse(acts.contains("Acta SELLADA"))
|
||||||
|
assertFalse(acts.contains("Text(\"Email\")"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,8 +8,8 @@ class ReleaseMetadataTest {
|
|||||||
@Test
|
@Test
|
||||||
fun debugBuildKeepsSeparateApplicationIdentity() {
|
fun debugBuildKeepsSeparateApplicationIdentity() {
|
||||||
assertEquals("com.korexlabs.dhinspeccion.debug", BuildConfig.APPLICATION_ID)
|
assertEquals("com.korexlabs.dhinspeccion.debug", BuildConfig.APPLICATION_ID)
|
||||||
assertEquals(27, BuildConfig.VERSION_CODE)
|
assertEquals(28, BuildConfig.VERSION_CODE)
|
||||||
assertEquals("0.18.0-debug", BuildConfig.VERSION_NAME)
|
assertEquals("0.19.0-debug", BuildConfig.VERSION_NAME)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ function mountedRepoFile(path: string): string {
|
|||||||
test('F6.3 Android test cut targets production API and has a distinct installable debug version', () => {
|
test('F6.3 Android test cut targets production API and has a distinct installable debug version', () => {
|
||||||
const gradle = mountedRepoFile('android-app/app/build.gradle.kts');
|
const gradle = mountedRepoFile('android-app/app/build.gradle.kts');
|
||||||
|
|
||||||
assert.match(gradle, /versionCode = 27/);
|
assert.match(gradle, /versionCode = 28/);
|
||||||
assert.match(gradle, /versionName = "0\.18\.0"/);
|
assert.match(gradle, /versionName = "0\.19\.0"/);
|
||||||
assert.match(gradle, /https:\/\/dhv2\.korexlabs\.com\/api\/v3\//);
|
assert.match(gradle, /https:\/\/dhv2\.korexlabs\.com\/api\/v3\//);
|
||||||
assert.match(gradle, /applicationIdSuffix = "\.debug"/);
|
assert.match(gradle, /applicationIdSuffix = "\.debug"/);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import type { AuthUser } from '../lib/api';
|
||||||
|
|
||||||
|
function normalizeRole(value: string) {
|
||||||
|
return value
|
||||||
|
.normalize('NFD')
|
||||||
|
.replace(/[\u0300-\u036f]/g, '')
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
const ADMIN_ROLES = new Set([
|
||||||
|
'admin',
|
||||||
|
'administrator',
|
||||||
|
'administrador',
|
||||||
|
'superadmin',
|
||||||
|
'super admin',
|
||||||
|
'superadministrator',
|
||||||
|
'super administrator',
|
||||||
|
'superadministrador',
|
||||||
|
'super administrador',
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function hasAdministratorRole(user: AuthUser | null) {
|
||||||
|
return Boolean(user?.roles.some((role) => ADMIN_ROLES.has(normalizeRole(role))));
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useParams, useSearchParams } from 'react-router';
|
import { useParams, useSearchParams } from 'react-router';
|
||||||
|
import { useAuth } from '../auth/AuthContext';
|
||||||
|
import { hasAdministratorRole } from '../auth/adminAccess';
|
||||||
import { Alert, LoadingBlock, errorMessage } from '../components/Feedback';
|
import { Alert, LoadingBlock, errorMessage } from '../components/Feedback';
|
||||||
import { getAsset } from '../lib/api';
|
import { getAsset } from '../lib/api';
|
||||||
import type { AssetDetail } from '../lib/api';
|
import type { AssetDetail } from '../lib/api';
|
||||||
@@ -10,6 +12,7 @@ import { TerritorialInventoryPage, isTerritorialInventoryAsset } from './Territo
|
|||||||
export function InventoryDetailPage() {
|
export function InventoryDetailPage() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const [params] = useSearchParams();
|
const [params] = useSearchParams();
|
||||||
|
const { user } = useAuth();
|
||||||
const [asset, setAsset] = useState<AssetDetail | null>(null);
|
const [asset, setAsset] = useState<AssetDetail | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
@@ -31,8 +34,11 @@ export function InventoryDetailPage() {
|
|||||||
if (loading) return <LoadingBlock label="Cargando registro…" />;
|
if (loading) return <LoadingBlock label="Cargando registro…" />;
|
||||||
if (!asset) return <Alert>{error || 'No se pudo cargar el registro.'}</Alert>;
|
if (!asset) return <Alert>{error || 'No se pudo cargar el registro.'}</Alert>;
|
||||||
|
|
||||||
const advanced = params.get('advanced') === '1';
|
const advancedRequested = params.get('advanced') === '1';
|
||||||
|
const advancedAllowed = hasAdministratorRole(user);
|
||||||
|
const advanced = advancedRequested && advancedAllowed;
|
||||||
const isCompany = asset.type.code.toLowerCase() === 'empresa';
|
const isCompany = asset.type.code.toLowerCase() === 'empresa';
|
||||||
|
|
||||||
if (isCompany && !advanced) {
|
if (isCompany && !advanced) {
|
||||||
return <CompanyInventoryPage initialAsset={asset} />;
|
return <CompanyInventoryPage initialAsset={asset} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,17 +2,19 @@ import { useEffect, useMemo, useState } from 'react';
|
|||||||
import type { FormEvent } from 'react';
|
import type { FormEvent } from 'react';
|
||||||
import { Link, useSearchParams } from 'react-router';
|
import { Link, useSearchParams } from 'react-router';
|
||||||
import { useAuth } from '../auth/AuthContext';
|
import { useAuth } from '../auth/AuthContext';
|
||||||
|
import { hasAdministratorRole } from '../auth/adminAccess';
|
||||||
import { Alert, LoadingBlock, errorMessage } from '../components/Feedback';
|
import { Alert, LoadingBlock, errorMessage } from '../components/Feedback';
|
||||||
import { Icon } from '../components/Icon';
|
import { Icon } from '../components/Icon';
|
||||||
import { AssetHistoryPanel } from '../features/assets/AssetHistoryPanel';
|
import { AssetHistoryPanel } from '../features/assets/AssetHistoryPanel';
|
||||||
import { assetStatusClass, assetStatusLabel } from '../features/assets/assetPresentation';
|
import { assetStatusClass, assetStatusLabel } from '../features/assets/assetPresentation';
|
||||||
import {
|
import {
|
||||||
|
getAssetLineage,
|
||||||
listAreaCompanyRelations,
|
listAreaCompanyRelations,
|
||||||
listAssetTreeChildren,
|
listAssetTreeChildren,
|
||||||
listInspectionVisits,
|
listInspectionVisits,
|
||||||
updateAsset,
|
updateAsset,
|
||||||
} from '../lib/api';
|
} from '../lib/api';
|
||||||
import type { AreaCompanyRelation, AssetDetail, AssetListItem } from '../lib/api';
|
import type { AreaCompanyRelation, AssetDetail, AssetLineageItem, AssetListItem } from '../lib/api';
|
||||||
import {
|
import {
|
||||||
listInspectionActsGlobalF4,
|
listInspectionActsGlobalF4,
|
||||||
type InspectionActStatusF4,
|
type InspectionActStatusF4,
|
||||||
@@ -31,8 +33,12 @@ function normalized(value: string) {
|
|||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function tokensForType(type: { code: string; name: string }) {
|
||||||
|
return new Set(`${normalized(type.code)} ${normalized(type.name)}`.split(' ').filter(Boolean));
|
||||||
|
}
|
||||||
|
|
||||||
function typeTokens(asset: AssetDetail) {
|
function typeTokens(asset: AssetDetail) {
|
||||||
return new Set(`${normalized(asset.type.code)} ${normalized(asset.type.name)}`.split(' ').filter(Boolean));
|
return tokensForType(asset.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isTerritorialInventoryAsset(asset: AssetDetail) {
|
export function isTerritorialInventoryAsset(asset: AssetDetail) {
|
||||||
@@ -44,6 +50,14 @@ function isOperationalArea(asset: AssetDetail) {
|
|||||||
return typeTokens(asset).has('area');
|
return typeTokens(asset).has('area');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isYacimiento(asset: AssetDetail) {
|
||||||
|
return typeTokens(asset).has('yacimiento');
|
||||||
|
}
|
||||||
|
|
||||||
|
function lineageArea(lineage: AssetLineageItem[]) {
|
||||||
|
return [...lineage].reverse().find((item) => tokensForType(item.type).has('area')) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
function relationRoleLabel(role: AreaCompanyRelation['relationRole']) {
|
function relationRoleLabel(role: AreaCompanyRelation['relationRole']) {
|
||||||
if (role === 'OPERATOR') return 'Operadora';
|
if (role === 'OPERATOR') return 'Operadora';
|
||||||
if (role === 'TECHNICAL_OPERATOR') return 'Operadora técnica';
|
if (role === 'TECHNICAL_OPERATOR') return 'Operadora técnica';
|
||||||
@@ -57,7 +71,7 @@ export function TerritorialInventoryPage({ initialAsset }: { initialAsset: Asset
|
|||||||
const [params, setParams] = useSearchParams();
|
const [params, setParams] = useSearchParams();
|
||||||
const requestedTab = params.get('tab') as TerritoryTab | null;
|
const requestedTab = params.get('tab') as TerritoryTab | null;
|
||||||
const tab: TerritoryTab = requestedTab === 'history' ? 'history' : 'summary';
|
const tab: TerritoryTab = requestedTab === 'history' ? 'history' : 'summary';
|
||||||
const { hasPermission } = useAuth();
|
const { user, hasPermission } = useAuth();
|
||||||
const canEdit = hasPermission('assets.update');
|
const canEdit = hasPermission('assets.update');
|
||||||
const canCreate = hasPermission('assets.create');
|
const canCreate = hasPermission('assets.create');
|
||||||
const canReadHistory = hasPermission('assets.read_history');
|
const canReadHistory = hasPermission('assets.read_history');
|
||||||
@@ -65,10 +79,12 @@ export function TerritorialInventoryPage({ initialAsset }: { initialAsset: Asset
|
|||||||
const canReadActs = hasPermission('inspection_acts.read');
|
const canReadActs = hasPermission('inspection_acts.read');
|
||||||
const canReadReports = hasPermission('inspection_reports.read');
|
const canReadReports = hasPermission('inspection_reports.read');
|
||||||
const canReadInspections = hasPermission('inspections.read');
|
const canReadInspections = hasPermission('inspections.read');
|
||||||
|
const canAdvanced = hasAdministratorRole(user);
|
||||||
|
|
||||||
const [asset, setAsset] = useState(initialAsset);
|
const [asset, setAsset] = useState(initialAsset);
|
||||||
const [children, setChildren] = useState<AssetListItem[]>([]);
|
const [children, setChildren] = useState<AssetListItem[]>([]);
|
||||||
const [childrenHasMore, setChildrenHasMore] = useState(false);
|
const [childrenHasMore, setChildrenHasMore] = useState(false);
|
||||||
|
const [lineage, setLineage] = useState<AssetLineageItem[]>([]);
|
||||||
const [relations, setRelations] = useState<AreaCompanyRelation[]>([]);
|
const [relations, setRelations] = useState<AreaCompanyRelation[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
@@ -80,18 +96,31 @@ export function TerritorialInventoryPage({ initialAsset }: { initialAsset: Asset
|
|||||||
const [metrics, setMetrics] = useState({ activeInspections: 0, acts: 0, reports: 0 });
|
const [metrics, setMetrics] = useState({ activeInspections: 0, acts: 0, reports: 0 });
|
||||||
|
|
||||||
const area = isOperationalArea(asset);
|
const area = isOperationalArea(asset);
|
||||||
|
const yacimiento = isYacimiento(asset);
|
||||||
const activeRelations = useMemo(() => relations.filter((item) => item.active), [relations]);
|
const activeRelations = useMemo(() => relations.filter((item) => item.active), [relations]);
|
||||||
|
const relationAreaContext = useMemo(() => {
|
||||||
|
if (area) return { id: asset.id, code: asset.code, name: asset.name };
|
||||||
|
if (asset.operationalArea) return asset.operationalArea;
|
||||||
|
const ancestor = lineageArea(lineage);
|
||||||
|
return ancestor ? { id: ancestor.id, code: ancestor.code, name: ancestor.name } : null;
|
||||||
|
}, [area, asset.id, asset.code, asset.name, asset.operationalArea, lineage]);
|
||||||
|
|
||||||
const loadSummary = async () => {
|
const loadSummary = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError('');
|
setError('');
|
||||||
try {
|
try {
|
||||||
|
const loadedLineage = await getAssetLineage(asset.id);
|
||||||
|
const ancestorArea = lineageArea(loadedLineage);
|
||||||
|
const relationAreaId = area
|
||||||
|
? asset.id
|
||||||
|
: asset.operationalArea?.id ?? ancestorArea?.id ?? null;
|
||||||
const [childPage, loadedRelations] = await Promise.all([
|
const [childPage, loadedRelations] = await Promise.all([
|
||||||
listAssetTreeChildren({ parentId: asset.id, limit: 100 }),
|
listAssetTreeChildren({ parentId: asset.id, limit: 100 }),
|
||||||
area && canReadRelations
|
relationAreaId && canReadRelations
|
||||||
? listAreaCompanyRelations({ areaId: asset.id, includeHistory: true })
|
? listAreaCompanyRelations({ areaId: relationAreaId, includeHistory: true })
|
||||||
: Promise.resolve([]),
|
: Promise.resolve([]),
|
||||||
]);
|
]);
|
||||||
|
setLineage(loadedLineage);
|
||||||
setChildren(childPage.data);
|
setChildren(childPage.data);
|
||||||
setChildrenHasMore(childPage.meta.hasMore);
|
setChildrenHasMore(childPage.meta.hasMore);
|
||||||
setRelations(loadedRelations);
|
setRelations(loadedRelations);
|
||||||
@@ -223,10 +252,10 @@ export function TerritorialInventoryPage({ initialAsset }: { initialAsset: Asset
|
|||||||
{childrenHasMore && <div className="territory-card-actions"><Link className="button secondary" to={`/inventarios?section=territory&parentId=${asset.id}`}>Ver contenido completo</Link></div>}
|
{childrenHasMore && <div className="territory-card-actions"><Link className="button secondary" to={`/inventarios?section=territory&parentId=${asset.id}`}>Ver contenido completo</Link></div>}
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
{area && canReadRelations && <article className="panel territory-card">
|
{relationAreaContext && canReadRelations && <article className="panel territory-card">
|
||||||
<div className="territory-card-heading"><div><span className="eyebrow">OPERACIÓN</span><h2>Empresas vinculadas</h2><p>Quién opera o participa actualmente en esta área.</p></div><span className="count-pill">{activeRelations.length}</span></div>
|
<div className="territory-card-heading"><div><span className="eyebrow">OPERACIÓN</span><h2>Empresas vinculadas</h2><p>{area ? 'Quién opera o participa actualmente en esta área.' : yacimiento ? `Empresas que operan o participan en el Área ${relationAreaContext.name}, contexto operativo de este yacimiento.` : `Empresas vinculadas al Área ${relationAreaContext.name}.`}</p></div><span className="count-pill">{activeRelations.length}</span></div>
|
||||||
{loading ? <LoadingBlock label="Cargando empresas…" /> : activeRelations.length === 0 ? <div className="inline-empty">No hay empresas con vínculo vigente.</div> : <div className="territory-company-list">
|
{loading ? <LoadingBlock label="Cargando empresas…" /> : activeRelations.length === 0 ? <div className="inline-empty">No hay empresas con vínculo vigente en el Área {relationAreaContext.name}.</div> : <div className="territory-company-list">
|
||||||
{activeRelations.map((relation) => <Link to={`/inventarios/${relation.company.id}`} key={relation.id} className="territory-company-row"><span><strong>{relation.company.name}</strong><small>{relationRoleLabel(relation.relationRole)}</small></span><Icon name="chevron" size={16} /></Link>)}
|
{activeRelations.map((relation) => <Link to={`/inventarios/${relation.company.id}`} key={relation.id} className="territory-company-row"><span><strong>{relation.company.name}</strong><small>{relationRoleLabel(relation.relationRole)}{!area ? ` · ${relation.area.name}` : ''}</small></span><Icon name="chevron" size={16} /></Link>)}
|
||||||
</div>}
|
</div>}
|
||||||
</article>}
|
</article>}
|
||||||
|
|
||||||
@@ -239,7 +268,7 @@ export function TerritorialInventoryPage({ initialAsset }: { initialAsset: Asset
|
|||||||
</div>
|
</div>
|
||||||
</article>}
|
</article>}
|
||||||
|
|
||||||
<div className="territory-advanced-link"><span>Los datos estructurales, procedencia, ubicación y opciones técnicas quedan fuera de la vista cotidiana.</span><Link to={`/inventarios/${asset.id}?advanced=1`}>Administración avanzada</Link></div>
|
{canAdvanced && <div className="territory-advanced-link"><span>Los datos estructurales, procedencia, ubicación y opciones técnicas quedan fuera de la vista cotidiana.</span><Link to={`/inventarios/${asset.id}?advanced=1`}>Administración avanzada</Link></div>}
|
||||||
</div>}
|
</div>}
|
||||||
|
|
||||||
{tab === 'history' && canReadHistory && <AssetHistoryPanel assetId={asset.id} />}
|
{tab === 'history' && canReadHistory && <AssetHistoryPanel assetId={asset.id} />}
|
||||||
|
|||||||
Reference in New Issue
Block a user