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 6d2a2dc..2f2d3d9 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,6 +60,7 @@ fun MobileActsScreen( val context = LocalContext.current val scope = rememberCoroutineScope() + var newActUrgency by rememberSaveable(visit.id) { mutableStateOf("NON_URGENT") } var attendance by rememberSaveable(selected?.id) { mutableStateOf(closure?.responsible?.attendanceStatus ?: "PRESENT") } @@ -129,19 +130,20 @@ fun MobileActsScreen( Card(Modifier.fillMaxWidth()) { Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { - Text("Actas de esta inspección", fontWeight = FontWeight.Bold) + Text("Actas de esta Inspección", fontWeight = FontWeight.Bold) if (model.acts.isEmpty()) { Text("Todavía no hay Actas. La primera se inicia sobre una Instalación/Subinstalación seleccionada.") } model.acts.forEach { act -> val active = selected?.id == act.id + val urgency = if (act.urgency == "URGENT") "URGENTE" else "No urgente" OutlinedButton( onClick = { model.selectAct(act.id) }, modifier = Modifier.fillMaxWidth(), ) { Text( (if (active) "✓ " else "") + - "${act.code} · ${actStatusLabel(act.status)} · ${act.findingCount} Hallazgo${if (act.findingCount == 1) "" else "s"}", + "${act.code} · ${actStatusLabel(act.status)} · $urgency · ${act.findingCount} Hallazgo${if (act.findingCount == 1) "" else "s"}", ) } } @@ -156,12 +158,31 @@ fun MobileActsScreen( ) { Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { Text("Nueva Acta", fontWeight = FontWeight.Bold) - if (model.acts.any { it.status == "READY" }) { + if (model.acts.any { it.status == "LOCKED" }) { Text( - "Puede existir una nueva Acta en borrador aunque haya Actas preparadas pendientes de firma de empresa. Sólo se permite un borrador a la vez.", + "Podés abrir una nueva Acta aunque otra esté BLOQUEADA esperando firmas. Sólo se permite un borrador a la vez.", style = MaterialTheme.typography.bodySmall, ) } + Text("Urgencia del Acta", 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." + }, + style = MaterialTheme.typography.bodySmall, + ) val selectedInventory = model.selectedFieldAsset?.asset if (selectedInventory == null) { Text("Primero elegí una Instalación o Subinstalación desde Inventario de campo. Ese registro será el primer elemento del Acta.") @@ -171,7 +192,7 @@ fun MobileActsScreen( } else { Text("Inventario inicial: ${selectedInventory.name} · ${selectedInventory.code}") Button( - onClick = { model.createActForSelectedInventory() }, + onClick = { model.createActForSelectedInventory(newActUrgency) }, enabled = !model.busy, modifier = Modifier.fillMaxWidth(), ) { Text("Crear nueva Acta") } @@ -186,9 +207,14 @@ 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("${selected.findingCount} Hallazgo${if (selected.findingCount == 1) "" else "s"} · ${selected.assetCount} elemento${if (selected.assetCount == 1) "" else "s"} de Inventario") - Text(selected.summary, style = MaterialTheme.typography.bodySmall) - selected.closureSha256?.let { Text("Hash final: $it", style = MaterialTheme.typography.bodySmall) } + selected.deadlineAt?.let { Text("Vencimiento calculado: $it", style = MaterialTheme.typography.bodySmall) } + if (selected.deadlineAt == null && selected.deadlineBasis == "GEDO_DATE") { + Text("Vencimiento pendiente de fecha GEDO.", style = MaterialTheme.typography.bodySmall) + } + selected.lockedSha256?.let { Text("Hash bloqueado: $it", style = MaterialTheme.typography.bodySmall) } + selected.closureSha256?.let { Text("Hash sellado: $it", style = MaterialTheme.typography.bodySmall) } } } @@ -234,31 +260,28 @@ fun MobileActsScreen( HorizontalDivider() Text("2. Hallazgos / verificaciones", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) - Text("Los Hallazgos se cargan desde Inventario y quedan vinculados a ${selected.code} de forma explícita. Un Acta de verificación puede prepararse sin Hallazgos nuevos si la verificación ya fue registrada.") + Text("Los Hallazgos se cargan desde Inventario y quedan vinculados explícitamente a ${selected.code}. El Acta también puede finalizar sin Hallazgos cuando corresponde dejar constancia de una inspección o verificación sin nuevos incumplimientos.") Button(onClick = onGoInventory, modifier = Modifier.fillMaxWidth()) { Text("Ir a Inventario / Hallazgos") } HorizontalDivider() - Text("3. Preparar Acta", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) - Text("Al preparar, el contenido se congela y se calcula su hash. El servidor exige al menos un Hallazgo o una verificación registrada.") + Text("3. 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.") Button( onClick = { model.prepareSelectedAct() }, enabled = !model.busy && closure?.responsible != null, modifier = Modifier.fillMaxWidth(), - ) { Text("Preparar Acta para firmas") } + ) { Text("Finalizar y BLOQUEAR Acta") } } - "READY" -> { + "LOCKED" -> { val signatures = closure?.signatures.orEmpty() val inspectorSigned = signatures.any { it.signerType == "INSPECTOR" && it.status == "SIGNED" } val companyOutcome = signatures.firstOrNull { it.signerType == "COMPANY_RESPONSIBLE" } + val companyResolved = companyOutcome?.status == "SIGNED" || companyOutcome?.status == "REFUSED" - Text("Acta preparada", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) - closure?.closure?.preparedSha256?.let { Text("Hash preparado: $it", style = MaterialTheme.typography.bodySmall) } - if (signatures.isEmpty()) { - OutlinedButton(onClick = { model.reopenSelectedAct() }, enabled = !model.busy, modifier = Modifier.fillMaxWidth()) { - Text("Volver a borrador") - } - } + Text("Acta BLOQUEADA", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) + Text("El contenido ya es inmutable. Sólo resta resolver firmas y manifestaciones para poder SELLARLA.") + closure?.closure?.preparedSha256?.let { Text("Hash bloqueado: $it", style = MaterialTheme.typography.bodySmall) } HorizontalDivider() Text("Firma del inspector", fontWeight = FontWeight.Bold) @@ -274,29 +297,21 @@ fun MobileActsScreen( } HorizontalDivider() - Text("Recepción de la empresa", fontWeight = FontWeight.Bold) - if (companyOutcome != null) { + Text("Manifestación de la empresa", fontWeight = FontWeight.Bold) + if (companyOutcome != null && companyResolved) { val detail = when (companyOutcome.status) { - "SIGNED" -> if (companyOutcome.companyManifestation == "DISSENT") "Firma en disidencia" else "Firma registrada" + "SIGNED" -> if (companyOutcome.companyManifestation == "DISSENT") "Firma en disidencia" else "Firma en conformidad" "REFUSED" -> "Negativa a firmar" - "ABSENT" -> "Responsable ausente" else -> companyOutcome.status } Text("✓ $detail", color = MaterialTheme.colorScheme.primary) companyOutcome.reason?.let { Text(it, style = MaterialTheme.typography.bodySmall) } companyOutcome.companyStatement?.let { Text(it, style = MaterialTheme.typography.bodySmall) } } else if (closure?.responsible?.attendanceStatus == "ABSENT") { - Text("El responsable fue registrado como ausente.") - Button( - onClick = { - model.recordCompanyOutcome( - "ABSENT", - closure.responsible.absenceReason ?: "Responsable de empresa ausente durante la inspección", - ) - }, - enabled = !model.busy && inspectorSigned, - modifier = Modifier.fillMaxWidth(), - ) { Text("Asentar ausencia en el Acta") } + Text( + "El responsable fue registrado como ausente. La ausencia NO resuelve la manifestación: deberá obtenerse firma o negativa posteriormente antes de SELLAR el Acta.", + style = MaterialTheme.typography.bodyMedium, + ) } else { Text(closure?.consents?.company.orEmpty(), style = MaterialTheme.typography.bodySmall) Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) { @@ -333,28 +348,28 @@ fun MobileActsScreen( } HorizontalDivider() - if (inspectorSigned && companyOutcome != null) { + if (inspectorSigned && companyResolved) { Button( onClick = { model.closeSelectedAct() }, enabled = !model.busy, modifier = Modifier.fillMaxWidth(), - ) { Text("Cerrar Acta definitivamente") } - } else if (inspectorSigned) { + ) { Text("SELLAR Acta definitivamente") } + } else { Text( - "La inspección puede finalizar con la firma de empresa pendiente; el Acta permanecerá preparada hasta registrar firma, negativa o ausencia.", + "Esta Acta bloquea el cierre de la Inspección hasta tener firma de inspector y firma o negativa válida de la empresa.", style = MaterialTheme.typography.bodySmall, ) } } - "CLOSED" -> { + "SEALED" -> { Card( Modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer), ) { Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(5.dp)) { - Text("Acta cerrada e inmutable", fontWeight = FontWeight.Bold) - Text("El PDF, informe Word y entregas documentales se generan desde este cierre.") + Text("Acta SELLADA e inmutable", fontWeight = FontWeight.Bold) + Text("Desde este sellado se genera el PDF del Acta y el INF Word editable para el Inspector.") closure?.closure?.finalSha256?.let { Text("SHA-256: $it", style = MaterialTheme.typography.bodySmall) } } } @@ -366,16 +381,19 @@ fun MobileActsScreen( if (visit.status == "IN_PROGRESS" && model.acts.isNotEmpty()) { HorizontalDivider() - val drafts = model.acts.count { it.status == "DRAFT" } - Text("Finalizar inspección", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) - Text( - "No puede quedar ninguna Acta en borrador. Al cerrar, el servidor verifica que cada Acta preparada tenga firma de inspector; si falta alguna, indicará cuál debe completarse.", - ) + val activeActs = model.acts.filter { it.status != "CANCELLED" } + val pendingActs = activeActs.filter { it.status != "SEALED" } + Text("Finalizar Inspección", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) + if (pendingActs.isEmpty()) { + Text("Todas las Actas están SELLADAS. La Inspección ya puede cerrarse.") + } else { + Text("No puede cerrarse todavía. Falta SELLAR: ${pendingActs.joinToString { it.code }}") + } Button( onClick = { model.closeInspection() }, - enabled = !model.busy && drafts == 0, + enabled = !model.busy && activeActs.isNotEmpty() && pendingActs.isEmpty(), modifier = Modifier.fillMaxWidth(), - ) { Text("Cerrar inspección y salir de la empresa") } + ) { Text("Cerrar Inspección y salir de la empresa") } } } } @@ -397,8 +415,8 @@ private fun F32ActMessage(model: MainViewModel) { private fun actStatusLabel(status: String): String = when (status) { "DRAFT" -> "Borrador" - "READY" -> "Preparada para firmas" - "CLOSED" -> "Cerrada" + "LOCKED" -> "BLOQUEADA" + "SEALED" -> "SELLADA" "CANCELLED" -> "Cancelada" else -> status }