F4 Android: redesign Act screen for DRAFT LOCKED SEALED
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user