From 1f5040f3c5aea96a11c13043127007bc9d10e7bd Mon Sep 17 00:00:00 2001 From: enlineawork Date: Mon, 7 Sep 2026 08:30:39 -0300 Subject: [PATCH] F2.3 Android: crear pantalla de Hallazgo desde Inventario --- .../dhinspeccion/ui/FieldFindingScreen.kt | 254 ++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/FieldFindingScreen.kt diff --git a/android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/FieldFindingScreen.kt b/android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/FieldFindingScreen.kt new file mode 100644 index 0000000..7744fb2 --- /dev/null +++ b/android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/FieldFindingScreen.kt @@ -0,0 +1,254 @@ +package com.korexlabs.dhinspeccion.ui + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.Button +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.unit.dp +import com.korexlabs.dhinspeccion.MainViewModel + +@Composable +fun FieldFindingScreen(model: MainViewModel) { + val options = model.fieldFindingOptions ?: return + val asset = model.selectedFieldAsset?.asset ?: return + var search by rememberSaveable(asset.id) { mutableStateOf("") } + var selectedCatalogId by rememberSaveable(asset.id) { mutableStateOf(null) } + var other by rememberSaveable(asset.id) { mutableStateOf(false) } + var customTitle by rememberSaveable(asset.id) { mutableStateOf("") } + var customLegalBasis by rememberSaveable(asset.id) { mutableStateOf("") } + var description by rememberSaveable(asset.id) { mutableStateOf("") } + var severityText by rememberSaveable(asset.id) { mutableStateOf("") } + var correctionDueOn by rememberSaveable(asset.id) { mutableStateOf("") } + + val selected = options.catalog.items.firstOrNull { it.id == selectedCatalogId } + val filtered = options.catalog.items.filter { + search.isBlank() || + it.title.contains(search, ignoreCase = true) || + it.code.contains(search, ignoreCase = true) || + it.categoryName.orEmpty().contains(search, ignoreCase = true) + } + + LaunchedEffect(selectedCatalogId, other) { + if (!other && selected != null && severityText.isBlank() && selected.suggestedSeverity != null) { + severityText = selected.suggestedSeverity.toString() + } + } + LaunchedEffect(model.lastCreatedFinding?.id) { + if (model.lastCreatedFinding != null) { + selectedCatalogId = null + other = false + customTitle = "" + customLegalBasis = "" + description = "" + severityText = "" + correctionDueOn = "" + search = "" + } + } + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(top = 30.dp, start = 16.dp, end = 16.dp, bottom = 36.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + OutlinedButton(onClick = { model.clearFindingFlow() }, enabled = !model.busy) { + Text("Volver") + } + Text("Hallazgo de campo", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) + } + + Card(Modifier.fillMaxWidth()) { + Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text(asset.name, fontWeight = FontWeight.Bold) + Text("${asset.code} · Acta ${options.act.code}", style = MaterialTheme.typography.bodySmall) + Text( + "GPS + foto: ${if (options.capture.readyForFinding) "OK" else "pendiente"}", + color = if (options.capture.readyForFinding) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall, + ) + } + } + + model.error?.let { + Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer)) { + Text(it, Modifier.padding(12.dp)) + } + } + model.notice?.let { + Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)) { + Text(it, Modifier.padding(12.dp)) + } + } + + if (options.findings.isNotEmpty()) { + Text("Hallazgos ya registrados en este Inventario", fontWeight = FontWeight.Bold) + options.findings.forEach { finding -> + Card(Modifier.fillMaxWidth()) { + Column(Modifier.padding(10.dp)) { + Text("${finding.code} · ${finding.title}", fontWeight = FontWeight.SemiBold) + Text("Gravedad: ${finding.severity ?: "s/d"} · ${finding.status}", style = MaterialTheme.typography.bodySmall) + } + } + } + HorizontalDivider() + } + + Text("1. Elegí el tipo de Hallazgo", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) + if (!options.catalog.typeConfigured) { + Text( + options.catalog.configurationReason + ?: "Este tipo de Inventario todavía no tiene un catálogo contextual configurado. Podés usar OTROS.", + color = MaterialTheme.colorScheme.secondary, + ) + } + OutlinedTextField( + value = search, + onValueChange = { search = it }, + label = { Text("Buscar en catálogo") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + ) + + filtered.forEach { item -> + val chosen = !other && selectedCatalogId == item.id + Card( + modifier = Modifier.fillMaxWidth().clickable { + selectedCatalogId = item.id + other = false + severityText = item.suggestedSeverity?.toString().orEmpty() + }, + colors = if (chosen) { + CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer) + } else { + CardDefaults.cardColors() + }, + ) { + Column(Modifier.padding(10.dp), verticalArrangement = Arrangement.spacedBy(3.dp)) { + Text(if (chosen) "✓ ${item.title}" else item.title, fontWeight = FontWeight.SemiBold) + Text( + listOfNotNull(item.categoryName, item.code, item.suggestedSeverity?.let { "Gravedad sugerida $it" }) + .joinToString(" · "), + style = MaterialTheme.typography.bodySmall, + ) + } + } + } + + OutlinedButton( + onClick = { + other = true + selectedCatalogId = null + severityText = "" + }, + modifier = Modifier.fillMaxWidth(), + ) { + Text(if (other) "✓ OTROS · Hallazgo no catalogado" else "OTROS · No está en el catálogo") + } + if (other) { + options.catalog.other.help?.let { Text(it, style = MaterialTheme.typography.bodySmall) } + OutlinedTextField( + value = customTitle, + onValueChange = { customTitle = it }, + label = { Text("Título del nuevo Hallazgo *") }, + modifier = Modifier.fillMaxWidth(), + ) + OutlinedTextField( + value = customLegalBasis, + onValueChange = { customLegalBasis = it }, + label = { Text("Base legal / normativa (opcional)") }, + modifier = Modifier.fillMaxWidth(), + ) + } + + Text("2. Describí lo observado", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) + selected?.let { + Card(Modifier.fillMaxWidth()) { + Column(Modifier.padding(10.dp), verticalArrangement = Arrangement.spacedBy(3.dp)) { + Text(it.title, fontWeight = FontWeight.SemiBold) + it.legalBasis?.takeIf(String::isNotBlank)?.let { basis -> Text(basis, style = MaterialTheme.typography.bodySmall) } + it.glossary?.takeIf(String::isNotBlank)?.let { glossary -> Text(glossary, style = MaterialTheme.typography.bodySmall) } + } + } + } + OutlinedTextField( + value = description, + onValueChange = { description = it }, + label = { Text("Descripción del Hallazgo *") }, + modifier = Modifier.fillMaxWidth(), + minLines = 3, + ) + OutlinedTextField( + value = severityText, + onValueChange = { value -> severityText = value.filter(Char::isDigit).take(2) }, + label = { Text("Gravedad 1 a 10") }, + modifier = Modifier.fillMaxWidth(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + singleLine = true, + ) + OutlinedTextField( + value = correctionDueOn, + onValueChange = { correctionDueOn = it }, + label = { Text("Fecha de corrección AAAA-MM-DD (opcional)") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + ) + + val severity = severityText.toIntOrNull() + val choiceReady = selectedCatalogId != null || (other && customTitle.isNotBlank()) + Button( + onClick = { + model.createFieldFinding( + catalogItemId = if (other) null else selectedCatalogId, + customTitle = if (other) customTitle else null, + customLegalBasis = if (other) customLegalBasis else null, + description = description, + severity = severity, + correctionDueOn = correctionDueOn, + ) + }, + enabled = choiceReady && description.isNotBlank() && (severity == null || severity in 1..10) && !model.busy, + modifier = Modifier.fillMaxWidth(), + ) { + Text(if (model.busy) "Guardando…" else "Guardar Hallazgo") + } + + model.lastCreatedFinding?.let { finding -> + Card( + Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer), + ) { + Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text("Hallazgo registrado", fontWeight = FontWeight.Bold) + Text("${finding.code} · ${finding.title}") + Text("Podés registrar otro Hallazgo sobre el mismo Inventario.", style = MaterialTheme.typography.bodySmall) + } + } + } + } +}