diff --git a/android-app/FINAL_RELEASE_CHECKLIST.md b/android-app/FINAL_RELEASE_CHECKLIST.md index e94611a..3cb3e0d 100644 --- a/android-app/FINAL_RELEASE_CHECKLIST.md +++ b/android-app/FINAL_RELEASE_CHECKLIST.md @@ -1,15 +1,16 @@ -# Checklist final Android 0.19.1 +# Checklist final Android 0.19.2 La candidata sólo se promueve si el mismo SHA cumple: - iniciar Inspección; -- crear/abrir Acta; -- seleccionar o crear Instalación/Subinstalación; +- crear/abrir Acta sin pedir urgencia ni Inventario; +- entrar a **Agregar Hallazgo** y recién allí 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**; +- al cerrar el contenido, exigir Urgente/No urgente, persistir esa decisión 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 mostrar **Inventario de campo** como acción paralela de la Inspección; - 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. diff --git a/android-app/RELEASE.md b/android-app/RELEASE.md index d7eca57..e3ad1c2 100644 --- a/android-app/RELEASE.md +++ b/android-app/RELEASE.md @@ -1,10 +1,10 @@ -# DH Inspección Android · release final de campo 0.19.1 +# DH Inspección Android · release final de campo 0.19.2 ## Candidata vigente - Fase funcional: **Flujo final de campo · Inspección → Acta → Hallazgos → Firma**. -- `versionName`: **0.19.1**. -- `versionCode`: **29**. +- `versionName`: **0.19.2**. +- `versionCode`: **30**. - Application ID release: `com.korexlabs.dhinspeccion`. - Application ID debug/QA: `com.korexlabs.dhinspeccion.debug`. - API: `https://dhv2.korexlabs.com/api/v3/`. @@ -13,15 +13,15 @@ La variante debug es independiente de la app productiva y puede instalarse para ## Procedimiento operativo validado -La APK 0.19.1 fija como recorrido principal de campo: +La APK 0.19.2 fija como recorrido principal de campo: -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. +1. **Iniciar Inspección**. Al iniciarla se habilita el circuito de **Actas y Hallazgos**; Inventario no es una acción independiente de la Inspección. +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. La urgencia todavía no se define. +3. **Agregar Hallazgos**. Recién desde esta acción se selecciona la Instalación/Subinstalación afectada. 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**. +7. Al terminar el contenido del Acta se elige **Urgente / No urgente** y luego se usa **Cerrar Acta y dejar pendiente de firma**. La urgencia queda fijada con el cierre; 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. @@ -57,7 +57,7 @@ La terminología visible se simplifica: - `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. +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. La urgencia se decide en ese cierre, nunca al crear el borrador. 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. @@ -115,3 +115,12 @@ Esta candidata requiere conexión. No implementa trabajo offline ni cola persist - Yacimiento seleccionable para Hallazgos y como padre explícito de nuevas Instalaciones. Se elimina el fallback que podía presentar un tipo Yacimiento como alta de Instalación. - Formulario Datos técnicos sobre el elemento seleccionado: carga y guarda las definiciones/valores por familia mediante los endpoints existentes del Dashboard; valida obligatorios, números, Sí/No, fechas y opciones. Es un paso separado del alta estructural y GPS/foto. - Se agregan siete pruebas para coordenadas y valores técnicos. + + +## Revisión 0.19.2 · flujo conceptual corregido + +- Se elimina **Inventario de campo** como acción independiente de la pantalla de Inspección. +- El alta o selección de Inventario existe únicamente dentro de **Agregar Hallazgo**. +- El borrador de Acta nace vacío, sin Inventario y sin urgencia predeterminada. +- La urgencia se exige al cerrar el Acta y se persiste en la misma operación que la vuelve inmutable. +- `inspection_acts.urgency` puede quedar pendiente durante el borrador; el bloqueo exige la decisión y calcula el vencimiento desde allí. diff --git a/android-app/app/build.gradle.kts b/android-app/app/build.gradle.kts index 9457ceb..323ade8 100644 --- a/android-app/app/build.gradle.kts +++ b/android-app/app/build.gradle.kts @@ -12,8 +12,8 @@ android { applicationId = "com.korexlabs.dhinspeccion" minSdk = 26 targetSdk = 36 - versionCode = 29 - versionName = "0.19.1" + versionCode = 30 + versionName = "0.19.2" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true 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 2ed20e4..dfb0e71 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 @@ -165,41 +165,25 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { } } - fun createActForSelectedInventory(urgency: String = "NON_URGENT") { + fun createAct() { val currentVisit = visit ?: return - val asset = selectedFieldAsset?.asset if (currentVisit.status != "IN_PROGRESS") { error = "La Inspección debe estar en curso para crear un Acta." return } - if (urgency !in setOf("URGENT", "NON_URGENT")) { - error = "Elegí si el Acta es urgente o no urgente." - return - } if (acts.any { it.status == "DRAFT" }) { error = "Ya existe un Acta en elaboración. Cerrala o cancelala antes de crear la siguiente." return } launchBusy(mutation = true) { - val created = actsRepository.create( - currentVisit.id, - asset?.id, - currentVisit.code, - urgency, - ) + val created = actsRepository.create(currentVisit.id, currentVisit.code) selectedAct = created actClosure = actsRepository.closure(created.id) loadActsInternal(currentVisit.id, selectDraft = false) - val urgencyLabel = if (urgency == "URGENT") "urgente" else "no urgente" - notice = "${created.code} creada como $urgencyLabel. Los Hallazgos nuevos quedarán vinculados explícitamente a esta Acta." - if (asset != null && selectedFieldAsset?.capture?.readyForFinding == true) { - loadFindingOptionsInternal(currentVisit.id, asset.id, created.id) - } + notice = "${created.code} creada. La urgencia se define recién al cerrar el Acta; los Hallazgos se agregan desde su propio flujo." } } - fun createAct(urgency: String = "NON_URGENT") = createActForSelectedInventory(urgency) - fun searchInventory(search: String, parentId: String? = null) { val currentVisit = visit ?: return val generation = ++inventorySearchGeneration @@ -488,12 +472,17 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { } } - fun prepareSelectedAct() { + fun prepareSelectedAct(urgency: String) { val actId = selectedAct?.id ?: return + if (urgency !in setOf("URGENT", "NON_URGENT")) { + error = "Definí la urgencia antes de cerrar el Acta." + return + } launchBusy(mutation = true) { - actClosure = actsRepository.lock(actId) + actClosure = actsRepository.lock(actId, urgency) refreshSelectedActInternal(actId) - notice = "Acta cerrada y pendiente de firma. Su contenido quedó inmutable; las firmas y manifestaciones pueden completarse a continuación." + val urgencyLabel = if (urgency == "URGENT") "urgente" else "no urgente" + notice = "Acta cerrada como $urgencyLabel y pendiente de firma. Su contenido quedó inmutable." } } diff --git a/android-app/app/src/main/java/com/korexlabs/dhinspeccion/data/MobileActs.kt b/android-app/app/src/main/java/com/korexlabs/dhinspeccion/data/MobileActs.kt index a7d2d78..1b060eb 100644 --- a/android-app/app/src/main/java/com/korexlabs/dhinspeccion/data/MobileActs.kt +++ b/android-app/app/src/main/java/com/korexlabs/dhinspeccion/data/MobileActs.kt @@ -35,7 +35,7 @@ data class MobileActSummary( val title: String, val summary: String, val observations: String? = null, - val urgency: String = "NON_URGENT", + val urgency: String? = null, val deadlineDays: Int? = null, val deadlineDayType: String? = null, val deadlineBasis: String? = null, @@ -60,7 +60,7 @@ data class MobileActDetail( val title: String, val summary: String, val observations: String? = null, - val urgency: String = "NON_URGENT", + val urgency: String? = null, val deadlineDays: Int? = null, val deadlineDayType: String? = null, val deadlineBasis: String? = null, @@ -91,13 +91,16 @@ data class MobileActListResponse( data class CreateMobileActRequest( val occurredAt: String, - val urgency: String, val title: String, val summary: String, val observations: String? = null, val assetIds: List, ) +data class PrepareMobileActRequest( + val urgency: String, +) + data class UpdateMobileActRequest( val assetIds: List, ) @@ -130,7 +133,7 @@ data class MobileActClosureHeader( val code: String, val status: String, val visitId: String, - val urgency: String = "NON_URGENT", + val urgency: String? = null, val deadlineDays: Int? = null, val deadlineDayType: String? = null, val deadlineBasis: String? = null, @@ -249,6 +252,7 @@ private interface MobileActsApi { suspend fun lock( @Header("Authorization") authorization: String, @Path("actId") actId: String, + @Body request: PrepareMobileActRequest, ): MobileActClosure @Multipart @@ -326,19 +330,16 @@ class MobileActsRepository(context: Context) { suspend fun create( visitId: String, - assetId: String? = null, visitCode: String, - urgency: String = "NON_URGENT", ): MobileActDetail = authorized { session -> api.createAct( "Bearer ${session.accessToken}", visitId, CreateMobileActRequest( occurredAt = Instant.now().toString(), - urgency = urgency, title = "Acta de inspección $visitCode", summary = "Acta de inspección en curso. Los Hallazgos y observaciones se incorporan de forma trazable durante la inspección.", - assetIds = listOfNotNull(assetId), + assetIds = emptyList(), ), ) } @@ -360,8 +361,8 @@ class MobileActsRepository(context: Context) { api.responsible("Bearer ${session.accessToken}", actId, request) } - suspend fun lock(actId: String): MobileActClosure = authorized { session -> - api.lock("Bearer ${session.accessToken}", actId) + suspend fun lock(actId: String, urgency: String): MobileActClosure = authorized { session -> + api.lock("Bearer ${session.accessToken}", actId, PrepareMobileActRequest(urgency)) } suspend fun signInspector( diff --git a/android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/MobileActsScreen.kt b/android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/MobileActsScreen.kt index fcc56fd..7a3295b 100644 --- a/android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/MobileActsScreen.kt +++ b/android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/MobileActsScreen.kt @@ -60,7 +60,7 @@ fun MobileActsScreen( val context = LocalContext.current val scope = rememberCoroutineScope() - var newActUrgency by rememberSaveable(visit.id) { mutableStateOf("NON_URGENT") } + var closingUrgency by rememberSaveable(selected?.id) { mutableStateOf("") } var attendance by rememberSaveable(selected?.id) { mutableStateOf(closure?.responsible?.attendanceStatus ?: "PRESENT") } @@ -137,7 +137,11 @@ fun MobileActsScreen( } model.acts.forEach { act -> val active = selected?.id == act.id - val urgency = if (act.urgency == "URGENT") "URGENTE" else "No urgente" + val urgency = when (act.urgency) { + "URGENT" -> "URGENTE" + "NON_URGENT" -> "No urgente" + else -> "Urgencia pendiente" + } OutlinedButton( onClick = { model.selectAct(act.id) }, modifier = Modifier.fillMaxWidth(), @@ -165,23 +169,8 @@ fun MobileActsScreen( style = MaterialTheme.typography.bodySmall, ) } - Text("Urgencia", fontWeight = FontWeight.Bold) - Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) { - AssistChip( - onClick = { newActUrgency = "NON_URGENT" }, - label = { Text(if (newActUrgency == "NON_URGENT") "✓ No urgente" else "No urgente") }, - ) - AssistChip( - onClick = { newActUrgency = "URGENT" }, - label = { Text(if (newActUrgency == "URGENT") "✓ Urgente" else "Urgente") }, - ) - } Text( - if (newActUrgency == "URGENT") { - "El plazo urgente se computará desde el Acta según la política institucional vigente al bloquearla." - } else { - "El plazo no urgente se computará desde la oficialización GEDO según la política institucional vigente al bloquearla." - }, + "La urgencia se define al cerrar el Acta, después de completar los Hallazgos.", style = MaterialTheme.typography.bodySmall, ) Text( @@ -189,7 +178,7 @@ fun MobileActsScreen( style = MaterialTheme.typography.bodySmall, ) Button( - onClick = { model.createAct(newActUrgency) }, + onClick = { model.createAct() }, enabled = !model.busy, modifier = Modifier.fillMaxWidth(), ) { Text("Crear nueva Acta") } @@ -203,7 +192,13 @@ fun MobileActsScreen( Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(5.dp)) { Text(selected.code, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) Text(actStatusLabel(selected.status)) - Text(if (selected.urgency == "URGENT") "Urgente" else "No urgente") + Text( + when (selected.urgency) { + "URGENT" -> "Urgencia: Urgente" + "NON_URGENT" -> "Urgencia: No urgente" + else -> "Urgencia: se define al cerrar el Acta" + }, + ) Text("${selected.findingCount} Hallazgo${if (selected.findingCount == 1) "" else "s"} · ${selected.assetCount} elemento${if (selected.assetCount == 1) "" else "s"} de Inventario") selected.deadlineAt?.let { Text("Vencimiento calculado: $it", style = MaterialTheme.typography.bodySmall) } if (selected.deadlineAt == null && selected.deadlineBasis == "GEDO_DATE") { @@ -270,10 +265,20 @@ fun MobileActsScreen( HorizontalDivider() Text("Finalizar contenido", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) - Text("Al BLOQUEAR el Acta, el contenido y los Hallazgos quedan inmutables. Esta acción no se puede deshacer.") + Text("Al cerrar el Acta, el contenido y los Hallazgos quedan inmutables. Definí ahora la urgencia según lo constatado.") + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) { + AssistChip( + onClick = { closingUrgency = "NON_URGENT" }, + label = { Text(if (closingUrgency == "NON_URGENT") "✓ No urgente" else "No urgente") }, + ) + AssistChip( + onClick = { closingUrgency = "URGENT" }, + label = { Text(if (closingUrgency == "URGENT") "✓ Urgente" else "Urgente") }, + ) + } Button( - onClick = { model.prepareSelectedAct() }, - enabled = !model.busy && closure?.responsible != null, + onClick = { model.prepareSelectedAct(closingUrgency) }, + enabled = !model.busy && closure?.responsible != null && closingUrgency.isNotBlank(), modifier = Modifier.fillMaxWidth(), ) { Text("Finalizar y BLOQUEAR Acta") } } 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 01b63a6..dd295b4 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 @@ -75,7 +75,7 @@ fun ModernMobileActsScreen( val context = LocalContext.current val scope = rememberCoroutineScope() - var newActUrgency by rememberSaveable(visit.id) { mutableStateOf("NON_URGENT") } + var closingUrgency by rememberSaveable(selected?.id) { mutableStateOf("") } var attendance by rememberSaveable(selected?.id) { mutableStateOf(closure?.responsible?.attendanceStatus ?: "PRESENT") } var fullName by rememberSaveable(selected?.id) { mutableStateOf(closure?.responsible?.fullName.orEmpty()) } var documentType by rememberSaveable(selected?.id) { mutableStateOf(closure?.responsible?.documentType ?: "DNI") } @@ -195,7 +195,7 @@ fun ModernMobileActsScreen( Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(3.dp)) { Text(act.code, fontWeight = FontWeight.Bold) Text( - "${modernActStatusLabel(act.status)} · ${if (act.urgency == "URGENT") "Urgente" else "No urgente"}", + "${modernActStatusLabel(act.status)} · ${when (act.urgency) { "URGENT" -> "Urgente"; "NON_URGENT" -> "No urgente"; else -> "Urgencia pendiente" }}", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -225,36 +225,13 @@ fun ModernMobileActsScreen( } } - Text("Urgencia", fontWeight = FontWeight.SemiBold) - Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) { - FilterChip( - selected = newActUrgency == "NON_URGENT", - onClick = { newActUrgency = "NON_URGENT" }, - label = { Text("No urgente") }, - ) - FilterChip( - selected = newActUrgency == "URGENT", - onClick = { newActUrgency = "URGENT" }, - label = { Text("Urgente") }, - ) - } - Surface( - Modifier.fillMaxWidth(), - shape = MaterialTheme.shapes.medium, - color = MaterialTheme.colorScheme.surfaceVariant, - ) { - Text( - if (newActUrgency == "URGENT") { - "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." - }, - Modifier.padding(12.dp), - style = MaterialTheme.typography.bodySmall, - ) - } + Text( + "La urgencia se define recién al cerrar el Acta, cuando ya se conoce el resultado de la inspección.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) Button( - onClick = { model.createAct(newActUrgency) }, + onClick = { model.createAct() }, enabled = !model.busy, modifier = Modifier.fillMaxWidth(), ) { @@ -278,7 +255,14 @@ fun ModernMobileActsScreen( Text(selected.code, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) StatusPill(modernActStatusLabel(selected.status)) } - Text(if (selected.urgency == "URGENT") "Urgente" else "No urgente", color = MaterialTheme.colorScheme.onSurfaceVariant) + Text( + when (selected.urgency) { + "URGENT" -> "Urgencia · Urgente" + "NON_URGENT" -> "Urgencia · No urgente" + else -> "Urgencia · Se define al cerrar el Acta" + }, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) Text("${selected.findingCount} Hallazgos · ${selected.assetCount} elementos de Inventario", style = MaterialTheme.typography.bodySmall) selected.deadlineAt?.let { Text("Vencimiento · $it", style = MaterialTheme.typography.bodySmall) } if (selected.deadlineAt == null && selected.deadlineBasis == "GEDO_DATE") { @@ -356,12 +340,34 @@ fun ModernMobileActsScreen( Text("Cerrar Acta", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) } Text( - "Al cerrar el contenido, el Acta queda inmutable y pasa a Pendiente de firma. Podés firmarla ahora o continuar con otra Acta.", + "Al cerrar el contenido, el Acta queda inmutable y pasa a Pendiente de firma. En este momento definí la urgencia según lo constatado en campo.", + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text("Urgencia del Acta", fontWeight = FontWeight.SemiBold) + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) { + FilterChip( + selected = closingUrgency == "NON_URGENT", + onClick = { closingUrgency = "NON_URGENT" }, + label = { Text("No urgente") }, + ) + FilterChip( + selected = closingUrgency == "URGENT", + onClick = { closingUrgency = "URGENT" }, + label = { Text("Urgente") }, + ) + } + Text( + when (closingUrgency) { + "URGENT" -> "El plazo urgente se computará desde el Acta según la política vigente." + "NON_URGENT" -> "El plazo no urgente comenzará con la oficialización GEDO." + else -> "Elegí una opción para habilitar el cierre." + }, + style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) Button( - onClick = { model.prepareSelectedAct() }, - enabled = !model.busy && closure?.responsible != null, + onClick = { model.prepareSelectedAct(closingUrgency) }, + enabled = !model.busy && closure?.responsible != null && closingUrgency.isNotBlank(), modifier = Modifier.fillMaxWidth(), ) { Text("Cerrar Acta y dejar pendiente de firma") } } 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 0e56467..57fc48e 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 @@ -106,7 +106,6 @@ fun ModernVisitRoot(model: MainViewModel) { ModernVisitScreen.OVERVIEW -> ModernVisitOverview( model = model, onActs = { screenName = ModernVisitScreen.ACTS.name }, - onInventory = { screenName = ModernVisitScreen.INVENTORY.name }, ) ModernVisitScreen.ACTS -> ModernMobileActsScreen( model = model, @@ -124,7 +123,6 @@ fun ModernVisitRoot(model: MainViewModel) { private fun ModernVisitOverview( model: MainViewModel, onActs: () -> Unit, - onInventory: () -> Unit, ) { val visit = model.visit ?: return Column( @@ -183,7 +181,7 @@ private fun ModernVisitOverview( Column(Modifier.padding(18.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { Text("Listo para iniciar", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) Text( - "Al iniciar se habilitan Actas, Hallazgos y el Inventario de campo.", + "Al iniciar se habilitan Actas y Hallazgos.", color = MaterialTheme.colorScheme.onSurfaceVariant, ) Button( @@ -217,25 +215,6 @@ private fun ModernVisitOverview( } } } - ElevatedCard(onClick = onInventory, modifier = Modifier.fillMaxWidth()) { - Row( - Modifier.padding(16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(14.dp), - ) { - Surface(shape = MaterialTheme.shapes.medium, color = MaterialTheme.colorScheme.secondaryContainer) { - Icon(Icons.Filled.Factory, null, Modifier.padding(11.dp), tint = MaterialTheme.colorScheme.secondary) - } - Column(Modifier.weight(1f)) { - Text("Inventario de campo", fontWeight = FontWeight.Bold) - Text( - "Buscar existente o cargar Instalación / Subinstalación", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - } - } } else if (visit.status == "CLOSED") { OutlinedButton(onClick = onActs, modifier = Modifier.fillMaxWidth()) { Text("Ver Actas") } } @@ -533,7 +512,7 @@ private fun ModernFieldInventoryScreen(model: MainViewModel, onBack: () -> Unit) ModernInventoryMode.PICK_PARENT -> "Elegir Instalación" ModernInventoryMode.CREATE_INSTALLATION -> "Nueva Instalación" ModernInventoryMode.CREATE_SUBINSTALLATION -> "Nueva Subinstalación" - ModernInventoryMode.BROWSE -> "Inventario de campo" + ModernInventoryMode.BROWSE -> "Ubicación del Hallazgo" }, subtitle = visit.scopeAsset?.name ?: visit.code, onBack = { if (mode == ModernInventoryMode.BROWSE) onBack() else backToBrowse() }, @@ -912,7 +891,7 @@ private fun ModernInventoryBrowse( OutlinedTextField( value = search, onValueChange = onSearchChange, - label = { Text("Buscar Inventario") }, + label = { Text("Buscar Instalación o Subinstalación") }, placeholder = { Text("Nombre, código o dato técnico") }, leadingIcon = { Icon(Icons.Filled.Search, null) }, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), diff --git a/android-app/app/src/test/java/com/korexlabs/dhinspeccion/ReleaseMetadataTest.kt b/android-app/app/src/test/java/com/korexlabs/dhinspeccion/ReleaseMetadataTest.kt index ccb74d4..e175eb9 100644 --- a/android-app/app/src/test/java/com/korexlabs/dhinspeccion/ReleaseMetadataTest.kt +++ b/android-app/app/src/test/java/com/korexlabs/dhinspeccion/ReleaseMetadataTest.kt @@ -8,8 +8,8 @@ class ReleaseMetadataTest { @Test fun debugBuildKeepsSeparateApplicationIdentity() { assertEquals("com.korexlabs.dhinspeccion.debug", BuildConfig.APPLICATION_ID) - assertEquals(29, BuildConfig.VERSION_CODE) - assertEquals("0.19.1-debug", BuildConfig.VERSION_NAME) + assertEquals(30, BuildConfig.VERSION_CODE) + assertEquals("0.19.2-debug", BuildConfig.VERSION_NAME) } @Test diff --git a/api-v3/package-lock.json b/api-v3/package-lock.json index a491901..d59ccea 100644 --- a/api-v3/package-lock.json +++ b/api-v3/package-lock.json @@ -1,12 +1,12 @@ { "name": "dhv2-api", - "version": "0.29.0-1", + "version": "0.29.0-2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dhv2-api", - "version": "0.29.0-1", + "version": "0.29.0-2", "license": "UNLICENSED", "dependencies": { "@nestjs/common": "^11.0.0", diff --git a/api-v3/package.json b/api-v3/package.json index 119ab08..49be4ae 100644 --- a/api-v3/package.json +++ b/api-v3/package.json @@ -1,6 +1,6 @@ { "name": "dhv2-api", - "version": "0.29.0-1", + "version": "0.29.0-2", "private": true, "license": "UNLICENSED", "scripts": { diff --git a/api-v3/src/database/entities/inspection-act.entity.ts b/api-v3/src/database/entities/inspection-act.entity.ts index 27f2846..cbe2807 100644 --- a/api-v3/src/database/entities/inspection-act.entity.ts +++ b/api-v3/src/database/entities/inspection-act.entity.ts @@ -71,8 +71,8 @@ export class InspectionAct extends TimestampedEntity { @Column({ type: 'text', nullable: true }) observations!: string | null; - @Column({ name: 'urgency', type: 'varchar', length: 24, default: InspectionActUrgency.NON_URGENT }) - urgency!: InspectionActUrgency; + @Column({ name: 'urgency', type: 'varchar', length: 24, nullable: true }) + urgency!: InspectionActUrgency | null; @Column({ name: 'deadline_days', type: 'integer', nullable: true }) deadlineDays!: number | null; diff --git a/api-v3/src/database/migrations/1790113800000-f6-1-act-urgency-at-lock.ts b/api-v3/src/database/migrations/1790113800000-f6-1-act-urgency-at-lock.ts new file mode 100644 index 0000000..8415e80 --- /dev/null +++ b/api-v3/src/database/migrations/1790113800000-f6-1-act-urgency-at-lock.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class F61ActUrgencyAtLock1790113800000 implements MigrationInterface { + name = 'F61ActUrgencyAtLock1790113800000'; + + public async up(q: QueryRunner): Promise { + await q.query(`ALTER TABLE inspection_acts ALTER COLUMN urgency DROP DEFAULT`); + await q.query(`ALTER TABLE inspection_acts ALTER COLUMN urgency DROP NOT NULL`); + } + + public async down(q: QueryRunner): Promise { + await q.query(`UPDATE inspection_acts SET urgency='NON_URGENT' WHERE urgency IS NULL`); + await q.query(`ALTER TABLE inspection_acts ALTER COLUMN urgency SET NOT NULL`); + await q.query(`ALTER TABLE inspection_acts ALTER COLUMN urgency SET DEFAULT 'NON_URGENT'`); + } +} diff --git a/api-v3/src/inspection-acts/dto/create-inspection-act.dto.ts b/api-v3/src/inspection-acts/dto/create-inspection-act.dto.ts index 16ae744..a8c38e6 100644 --- a/api-v3/src/inspection-acts/dto/create-inspection-act.dto.ts +++ b/api-v3/src/inspection-acts/dto/create-inspection-act.dto.ts @@ -3,7 +3,6 @@ import { ArrayMaxSize, ArrayUnique, IsArray, - IsEnum, IsISO8601, IsOptional, IsString, @@ -11,15 +10,11 @@ import { MaxLength, MinLength, } from 'class-validator'; -import { InspectionActUrgency } from '../../database/entities'; export class CreateInspectionActDto { @IsISO8601({ strict: true }) occurredAt!: string; - @IsEnum(InspectionActUrgency) - urgency!: InspectionActUrgency; - @Transform(({ value }) => (typeof value === 'string' ? value.trim() : value)) @IsString() @MinLength(1) diff --git a/api-v3/src/inspection-acts/dto/update-inspection-act.dto.ts b/api-v3/src/inspection-acts/dto/update-inspection-act.dto.ts index af2be32..7a780aa 100644 --- a/api-v3/src/inspection-acts/dto/update-inspection-act.dto.ts +++ b/api-v3/src/inspection-acts/dto/update-inspection-act.dto.ts @@ -4,7 +4,6 @@ import { ArrayMinSize, ArrayUnique, IsArray, - IsEnum, IsISO8601, IsOptional, IsString, @@ -12,17 +11,12 @@ import { MaxLength, MinLength, } from 'class-validator'; -import { InspectionActUrgency } from '../../database/entities'; export class UpdateInspectionActDto { @IsOptional() @IsISO8601({ strict: true }) occurredAt?: string; - @IsOptional() - @IsEnum(InspectionActUrgency) - urgency?: InspectionActUrgency; - @IsOptional() @Transform(({ value }) => (typeof value === 'string' ? value.trim() : value)) @IsString() diff --git a/api-v3/src/inspection-acts/inspection-acts.service.ts b/api-v3/src/inspection-acts/inspection-acts.service.ts index 4f0aa01..caa757b 100644 --- a/api-v3/src/inspection-acts/inspection-acts.service.ts +++ b/api-v3/src/inspection-acts/inspection-acts.service.ts @@ -74,7 +74,7 @@ export interface InspectionActListItem { title: string; summary: string; observations: string | null; - urgency: InspectionActUrgency; + urgency: InspectionActUrgency | null; deadlineDays: number | null; deadlineDayType: InspectionDeadlineDayType | null; deadlineBasis: InspectionDeadlineBasis | null; @@ -314,7 +314,7 @@ export class InspectionActsService { title: dto.title, summary: dto.summary, observations: dto.observations ?? null, - urgency: dto.urgency, + urgency: null, deadlineDays: null, deadlineDayType: null, deadlineBasis: null, @@ -379,7 +379,6 @@ export class InspectionActsService { await this.assertVisitAssets(manager, visit.id, nextAssetIds); const before = await this.loadView(manager, id); if (dto.occurredAt !== undefined) act.occurredAt = nextOccurredAt; - if (dto.urgency !== undefined) act.urgency = dto.urgency; if (dto.title !== undefined) act.title = dto.title; if (dto.summary !== undefined) act.summary = dto.summary; if (dto.observations !== undefined) act.observations = dto.observations; diff --git a/api-v3/src/inspection-closing/dto/prepare-inspection-act.dto.ts b/api-v3/src/inspection-closing/dto/prepare-inspection-act.dto.ts new file mode 100644 index 0000000..a354bb0 --- /dev/null +++ b/api-v3/src/inspection-closing/dto/prepare-inspection-act.dto.ts @@ -0,0 +1,7 @@ +import { IsEnum } from 'class-validator'; +import { InspectionActUrgency } from '../../database/entities'; + +export class PrepareInspectionActDto { + @IsEnum(InspectionActUrgency) + urgency!: InspectionActUrgency; +} diff --git a/api-v3/src/inspection-closing/inspection-closing.controller.ts b/api-v3/src/inspection-closing/inspection-closing.controller.ts index ef36886..9cbb9d0 100644 --- a/api-v3/src/inspection-closing/inspection-closing.controller.ts +++ b/api-v3/src/inspection-closing/inspection-closing.controller.ts @@ -17,6 +17,7 @@ import { CurrentAuth } from '../auth/decorators/current-auth.decorator'; import { RequirePermissions } from '../authorization/decorators/require-permissions.decorator'; import type { AuthPrincipal, RequestWithContext } from '../common/http/request-context'; import { CloseInspectionActDto } from './dto/close-inspection-act.dto'; +import { PrepareInspectionActDto } from './dto/prepare-inspection-act.dto'; import { CreateCompanyOutcomeDto } from './dto/create-company-outcome.dto'; import { CreateInspectionSignatureDto } from './dto/create-inspection-signature.dto'; import { CreateCompanySignatureDto } from './dto/create-company-signature.dto'; @@ -52,10 +53,11 @@ export class InspectionClosingController { @RequirePermissions('inspection_closure.prepare') lock( @Param('actId', new ParseUUIDPipe({ version: '4' })) actId: string, + @Body() dto: PrepareInspectionActDto, @CurrentAuth() principal: AuthPrincipal, @Req() request: RequestWithContext, ) { - return this.closing.prepare(actId, principal, request); + return this.closing.prepare(actId, dto, principal, request); } @Post('signatures/inspector') diff --git a/api-v3/src/inspection-closing/inspection-closing.service.ts b/api-v3/src/inspection-closing/inspection-closing.service.ts index be1c906..74241d9 100644 --- a/api-v3/src/inspection-closing/inspection-closing.service.ts +++ b/api-v3/src/inspection-closing/inspection-closing.service.ts @@ -35,6 +35,7 @@ import { assertMobileInspector } from '../inspection-operations/mobile-inspector import { InspectionReportsService } from '../inspection-reports/inspection-reports.service'; import { sha256CanonicalJson } from './canonical-json'; import type { CloseInspectionActDto } from './dto/close-inspection-act.dto'; +import type { PrepareInspectionActDto } from './dto/prepare-inspection-act.dto'; import type { CreateCompanyOutcomeDto } from './dto/create-company-outcome.dto'; import type { CreateCompanySignatureDto } from './dto/create-company-signature.dto'; import type { CreateInspectionSignatureDto } from './dto/create-inspection-signature.dto'; @@ -130,7 +131,7 @@ export interface InspectionClosureView { code: string; status: InspectionActStatus; visitId: string; - urgency: InspectionActUrgency; + urgency: InspectionActUrgency | null; deadlineDays: number | null; deadlineDayType: InspectionDeadlineDayType | null; deadlineBasis: InspectionDeadlineBasis | null; @@ -268,6 +269,7 @@ export class InspectionClosingService { /** Finaliza el contenido: desde este punto el Acta queda inmutable. */ async prepare( actId: string, + dto: PrepareInspectionActDto, principal: AuthPrincipal, request: RequestWithContext, ): Promise { @@ -285,16 +287,16 @@ export class InspectionClosingService { } const policy = await this.deadlinePolicy(manager); - const deadlineDays = act.urgency === InspectionActUrgency.URGENT + const deadlineDays = dto.urgency === InspectionActUrgency.URGENT ? policy.urgentDays : policy.nonUrgentDays; - const deadlineDayType = act.urgency === InspectionActUrgency.URGENT + const deadlineDayType = dto.urgency === InspectionActUrgency.URGENT ? policy.urgentDayType : policy.nonUrgentDayType; - const deadlineBasis = act.urgency === InspectionActUrgency.URGENT + const deadlineBasis = dto.urgency === InspectionActUrgency.URGENT ? InspectionDeadlineBasis.ACT_DATE : InspectionDeadlineBasis.GEDO_DATE; - const deadlineBaseAt = act.urgency === InspectionActUrgency.URGENT + const deadlineBaseAt = dto.urgency === InspectionActUrgency.URGENT ? act.occurredAt : null; const deadlineAt = deadlineBaseAt @@ -305,20 +307,22 @@ export class InspectionClosingService { const [updated] = (await manager.query(` UPDATE inspection_acts SET status='LOCKED', - deadline_days=$2, - deadline_day_type=$3, - deadline_basis=$4, - deadline_base_at=$5, - deadline_at=$6, - locked_at=$7, - locked_by=$8, + urgency=$2, + deadline_days=$3, + deadline_day_type=$4, + deadline_basis=$5, + deadline_base_at=$6, + deadline_at=$7, + locked_at=$8, + locked_by=$9, current_version=current_version+1, - updated_by=$8, - updated_at=$7 + updated_by=$9, + updated_at=$8 WHERE id=$1 RETURNING current_version AS "versionNumber" `, [ actId, + dto.urgency, deadlineDays, deadlineDayType, deadlineBasis, @@ -1009,7 +1013,7 @@ export class InspectionClosingService { WHERE act.id=$1 `, [actId]) as Array<{ id: string; code: string; status: InspectionActStatus; visitId: string; - urgency: InspectionActUrgency; deadlineDays: number | null; + urgency: InspectionActUrgency | null; deadlineDays: number | null; deadlineDayType: InspectionDeadlineDayType | null; deadlineBasis: InspectionDeadlineBasis | null; deadlineBaseAt: Date | null; deadlineAt: Date | null; lockedAt: Date | null; lockedSha256: string | null; sealedAt: Date | null; currentVersion: number; diff --git a/api-v3/test/unit/f4-act-web-lifecycle-contract.test.ts b/api-v3/test/unit/f4-act-web-lifecycle-contract.test.ts index fd6a50e..7f9de98 100644 --- a/api-v3/test/unit/f4-act-web-lifecycle-contract.test.ts +++ b/api-v3/test/unit/f4-act-web-lifecycle-contract.test.ts @@ -20,7 +20,7 @@ test('F4 WEB has an explicit LOCKED and SEALED act contract', () => { assert.match(f4Api, /lockedAt: string \| null/); assert.match(f4Api, /lockedSha256: string \| null/); assert.match(f4Api, /sealedAt: string \| null/); - assert.match(f4Api, /urgency: InspectionActUrgencyF4/); + assert.match(f4Api, /urgency: InspectionActUrgencyF4 \| null/); assert.match(f4Api, /deadlineAt: string \| null/); }); diff --git a/api-v3/test/unit/f4-android-act-contract.test.ts b/api-v3/test/unit/f4-android-act-contract.test.ts index f50fcd8..d0f2634 100644 --- a/api-v3/test/unit/f4-android-act-contract.test.ts +++ b/api-v3/test/unit/f4-android-act-contract.test.ts @@ -35,7 +35,7 @@ test('F4 Android uses only lock and seal endpoints for the active Act lifecycle' }); test('F4 Android ViewModel calls lock and seal directly', () => { - assert.match(viewModel, /actsRepository\.lock\(actId\)/); + assert.match(viewModel, /actsRepository\.lock\(actId, urgency\)/); assert.match(viewModel, /actsRepository\.seal\(actId\)/); assert.doesNotMatch(viewModel, /actsRepository\.prepare\(/); assert.doesNotMatch(viewModel, /actsRepository\.reopen\(/); diff --git a/api-v3/test/unit/f5-android-test-cut.test.ts b/api-v3/test/unit/f5-android-test-cut.test.ts index 1fec29b..e8deca0 100644 --- a/api-v3/test/unit/f5-android-test-cut.test.ts +++ b/api-v3/test/unit/f5-android-test-cut.test.ts @@ -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', () => { const gradle = mountedRepoFile('android-app/app/build.gradle.kts'); - assert.match(gradle, /versionCode = 29/); - assert.match(gradle, /versionName = "0\.19\.1"/); + assert.match(gradle, /versionCode = 30/); + assert.match(gradle, /versionName = "0\.19\.2"/); assert.match(gradle, /https:\/\/dhv2\.korexlabs\.com\/api\/v3\//); assert.match(gradle, /applicationIdSuffix = "\.debug"/); }); diff --git a/api-v3/test/unit/f6-3-mobile-act-finding-flow.test.ts b/api-v3/test/unit/f6-3-mobile-act-finding-flow.test.ts index 0adf814..7ef413e 100644 --- a/api-v3/test/unit/f6-3-mobile-act-finding-flow.test.ts +++ b/api-v3/test/unit/f6-3-mobile-act-finding-flow.test.ts @@ -20,7 +20,7 @@ test('F6.3 an Acta can start empty and receive Inventory when Hallazgos are adde assert.doesNotMatch(dto, /ArrayMinSize\(1\)/); assert.match(service, /if \(assetIds\.length === 0\) return/); - assert.match(mobile, /assetIds = listOfNotNull\(assetId\)/); + assert.match(mobile, /assetIds = emptyList\(\)/); }); test('F6.3 every Installation and Subinstallation receives the common field card', () => { @@ -35,15 +35,30 @@ test('F6.3 every Installation and Subinstallation receives the common field card }); test('F6.3 Android follows Inspección → Acta → Hallazgo → Inventario', () => { - const root = source('../android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/F3VisitRoot.kt'); - const acts = source('../android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/MobileActsScreen.kt'); + const root = source('../android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/ModernVisitRoot.kt'); + const acts = source('../android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/ModernMobileActsScreen.kt'); const vm = source('../android-app/app/src/main/java/com/korexlabs/dhinspeccion/MainViewModel.kt'); assert.match(root, /model\.startVisit\(\); onActs\(\)/); - assert.match(root, /Text\("Nuevo Hallazgo"/); - assert.match(root, /Buscar instalación o subinstalación/); - assert.match(acts, /Text\("\+ Agregar Hallazgo"\)/); - assert.match(acts, /model\.createAct\(newActUrgency\)/); - assert.match(vm, /fun createAct\(urgency: String = "NON_URGENT"\)/); + assert.doesNotMatch(root, /Text\("Inventario de campo"/); + assert.match(acts, /Text\("Agregar Hallazgo"\)/); + assert.match(acts, /model\.createAct\(\)/); + assert.match(acts, /model\.prepareSelectedAct\(closingUrgency\)/); + assert.match(vm, /fun createAct\(\)/); + assert.match(vm, /fun prepareSelectedAct\(urgency: String\)/); assert.match(vm, /repository\.fieldInventory\(currentVisit\.id, search, parentId\)/); }); + +test('F6.1 Acta urgency is null while drafting and is persisted atomically at lock', () => { + const createDto = source('src/inspection-acts/dto/create-inspection-act.dto.ts'); + const prepareDto = source('src/inspection-closing/dto/prepare-inspection-act.dto.ts'); + const actsService = source('src/inspection-acts/inspection-acts.service.ts'); + const closingService = source('src/inspection-closing/inspection-closing.service.ts'); + const migration = source('src/database/migrations/1790113800000-f6-1-act-urgency-at-lock.ts'); + + assert.doesNotMatch(createDto, /urgency/); + assert.match(prepareDto, /urgency!: InspectionActUrgency/); + assert.match(actsService, /urgency: null/); + assert.match(closingService, /urgency=\$2/); + assert.match(migration, /ALTER COLUMN urgency DROP NOT NULL/); +}); diff --git a/api-v3/test/unit/inspection-acts-dto.test.ts b/api-v3/test/unit/inspection-acts-dto.test.ts index dc06787..50c97bc 100644 --- a/api-v3/test/unit/inspection-acts-dto.test.ts +++ b/api-v3/test/unit/inspection-acts-dto.test.ts @@ -5,21 +5,18 @@ import { plainToInstance } from 'class-transformer'; import { validate } from 'class-validator'; import { CancelInspectionActDto } from '../../src/inspection-acts/dto/cancel-inspection-act.dto'; import { CreateInspectionActDto } from '../../src/inspection-acts/dto/create-inspection-act.dto'; -import { InspectionActUrgency } from '../../src/database/entities'; const ASSET_ID = '16e54e65-60cf-4739-b0d1-ccdd904fbfd5'; -test('inspection act DTO accepts urgency, content and selected visit assets', async () => { +test('inspection act DTO accepts content and selected visit assets without deciding urgency', async () => { const dto = plainToInstance(CreateInspectionActDto, { occurredAt: '2026-08-14T15:30:00.000Z', - urgency: InspectionActUrgency.NON_URGENT, title: ' Verificación de instalación ', summary: ' Se inspeccionó el activo seleccionado. ', observations: ' Sin novedades. ', assetIds: [ASSET_ID], }); assert.deepEqual(await validate(dto), []); - assert.equal(dto.urgency, InspectionActUrgency.NON_URGENT); assert.equal(dto.title, 'Verificación de instalación'); assert.equal(dto.summary, 'Se inspeccionó el activo seleccionado.'); assert.equal(dto.observations, 'Sin novedades.'); @@ -28,7 +25,6 @@ test('inspection act DTO accepts urgency, content and selected visit assets', as test('inspection act DTO rejects duplicate assets', async () => { const dto = plainToInstance(CreateInspectionActDto, { occurredAt: '2026-08-14T15:30:00.000Z', - urgency: InspectionActUrgency.NON_URGENT, title: 'Acta', summary: 'Contenido', assetIds: [ASSET_ID, ASSET_ID], diff --git a/api-v3/test/unit/inspection-closing-dto.test.ts b/api-v3/test/unit/inspection-closing-dto.test.ts index ae74bcf..fc9c4aa 100644 --- a/api-v3/test/unit/inspection-closing-dto.test.ts +++ b/api-v3/test/unit/inspection-closing-dto.test.ts @@ -5,6 +5,8 @@ import { plainToInstance } from 'class-transformer'; import { validate } from 'class-validator'; import { CreateCompanyOutcomeDto } from '../../src/inspection-closing/dto/create-company-outcome.dto'; import { CreateInspectionSignatureDto } from '../../src/inspection-closing/dto/create-inspection-signature.dto'; +import { PrepareInspectionActDto } from '../../src/inspection-closing/dto/prepare-inspection-act.dto'; +import { InspectionActUrgency } from '../../src/database/entities'; import { UpsertInspectionResponsibleDto } from '../../src/inspection-closing/dto/upsert-inspection-responsible.dto'; test('D5 present responsible requires identity and position', async () => { @@ -54,3 +56,13 @@ test('D5 signature DTO transforms multipart consent and coordinates', async () = assert.equal(dto.consentAccepted, true); assert.equal(dto.latitude, -32.889458); }); + + +test('F6.1 urgency is selected when the Acta is closed, not when it is created', async () => { + const urgent = plainToInstance(PrepareInspectionActDto, { urgency: InspectionActUrgency.URGENT }); + const nonUrgent = plainToInstance(PrepareInspectionActDto, { urgency: InspectionActUrgency.NON_URGENT }); + const invalid = plainToInstance(PrepareInspectionActDto, { urgency: 'UNKNOWN' }); + assert.deepEqual(await validate(urgent), []); + assert.deepEqual(await validate(nonUrgent), []); + assert.equal((await validate(invalid)).some((error) => error.property === 'urgency'), true); +}); diff --git a/web-v2/src/features/inspections/InspectionClosurePanel.tsx b/web-v2/src/features/inspections/InspectionClosurePanel.tsx index 9984f33..50d680e 100644 --- a/web-v2/src/features/inspections/InspectionClosurePanel.tsx +++ b/web-v2/src/features/inspections/InspectionClosurePanel.tsx @@ -90,7 +90,7 @@ export function InspectionClosurePanel({ act }: { act: InspectionActF4 }) {
-
Urgencia{closure.act.urgency === 'URGENT' ? 'Urgente' : 'No urgente'}
+
Urgencia{closure.act.urgency === 'URGENT' ? 'Urgente' : closure.act.urgency === 'NON_URGENT' ? 'No urgente' : 'Pendiente de cierre'}
Plazo configurado{closure.act.deadlineDays ? `${closure.act.deadlineDays} días ${closure.act.deadlineDayType === 'BUSINESS' ? 'hábiles' : 'corridos'}` : 'Pendiente'}
Inicio del plazo{closure.act.deadlineBaseAt ? formatDate(closure.act.deadlineBaseAt) : 'Pendiente de evento válido'}
Vencimiento{closure.act.deadlineAt ? formatDate(closure.act.deadlineAt) : 'Todavía no iniciado'}
diff --git a/web-v2/src/lib/inspectionActF4Api.ts b/web-v2/src/lib/inspectionActF4Api.ts index e4da94c..3cb66c0 100644 --- a/web-v2/src/lib/inspectionActF4Api.ts +++ b/web-v2/src/lib/inspectionActF4Api.ts @@ -53,7 +53,7 @@ export interface InspectionActListItemF4 { title: string; summary: string; observations: string | null; - urgency: InspectionActUrgencyF4; + urgency: InspectionActUrgencyF4 | null; deadlineDays: number | null; deadlineDayType: InspectionDeadlineDayTypeF4 | null; deadlineBasis: InspectionDeadlineBasisF4 | null; @@ -107,7 +107,7 @@ export interface InspectionClosureF4 { code: string; status: InspectionActStatusF4; visitId: string; - urgency: InspectionActUrgencyF4; + urgency: InspectionActUrgencyF4 | null; deadlineDays: number | null; deadlineDayType: InspectionDeadlineDayTypeF4 | null; deadlineBasis: InspectionDeadlineBasisF4 | null; diff --git a/web-v2/src/pages/InspectionActEditorPage.tsx b/web-v2/src/pages/InspectionActEditorPage.tsx index a01e830..6fb5513 100644 --- a/web-v2/src/pages/InspectionActEditorPage.tsx +++ b/web-v2/src/pages/InspectionActEditorPage.tsx @@ -63,7 +63,7 @@ export function InspectionActEditorPage() {
Fecha y hora{formatDate(act.occurredAt)}
Título{act.title}
-
Urgencia{act.urgency === 'URGENT' ? 'Urgente' : 'No urgente'}
+
Urgencia{act.urgency === 'URGENT' ? 'Urgente' : act.urgency === 'NON_URGENT' ? 'No urgente' : 'Pendiente de cierre'}
Hallazgos{act.findingCount}
DESCRIPCIÓN DE LO ACTUADO

{act.summary}

{act.observations && <>OBSERVACIONES

{act.observations}

}