diff --git a/android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/F3VisitRoot.kt b/android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/F3VisitRoot.kt index 7c28d8d..7f137d2 100644 --- a/android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/F3VisitRoot.kt +++ b/android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/F3VisitRoot.kt @@ -86,7 +86,10 @@ fun F3VisitRoot(model: MainViewModel) { inventoryMode = true }, ) - inventoryMode -> F3FieldInventoryScreen(model, onBack = { inventoryMode = false }) + inventoryMode -> F3FieldInventoryScreen(model, onBack = { + inventoryMode = false + actsMode = true + }) else -> F3VisitOverview( model = model, onInventory = { inventoryMode = true }, @@ -119,6 +122,9 @@ private fun F3VisitOverview( } Text(visit.code, style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold) Text("${visit.operatorCompany?.name ?: "Sin operadora"} · ${visit.operationalArea?.name ?: "Sin área"}") + visit.scopeAsset?.let { + Text("Yacimiento: ${it.name}", style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.SemiBold) + } visit.plannedStartAt?.let { Text("Planificada: ${f3ShortDate(it)}", style = MaterialTheme.typography.bodySmall) } @@ -138,10 +144,10 @@ private fun F3VisitOverview( colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer), ) { Column(Modifier.padding(14.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { - Text("La inspección todavía no comenzó", fontWeight = FontWeight.Bold) - Text("Iniciarla registra fecha/hora real y tu usuario como actor de campo.") + Text("Todo listo para comenzar", fontWeight = FontWeight.Bold) + Text("Al iniciar, pasás directamente a las Actas de esta inspección.") Button( - onClick = { model.startVisit() }, + onClick = { model.startVisit(); onActs() }, enabled = !model.busy, modifier = Modifier.fillMaxWidth(), ) { Text(if (model.busy) "Iniciando…" else "Iniciar inspección") } @@ -150,12 +156,12 @@ private fun F3VisitOverview( } if (visit.status == "IN_PROGRESS") { - Button(onClick = onInventory, modifier = Modifier.fillMaxWidth()) { - Text("Abrir Inventario de campo") - } - OutlinedButton(onClick = onActs, modifier = Modifier.fillMaxWidth()) { + Button(onClick = onActs, modifier = Modifier.fillMaxWidth()) { val open = model.acts.count { it.status == "DRAFT" || it.status == "READY" } - Text("Actas de la inspección · ${model.acts.size}${if (open > 0) " · $open abiertas" else ""}") + Text("Abrir Actas · ${model.acts.size}${if (open > 0) " · $open abiertas" else ""}") + } + OutlinedButton(onClick = onInventory, modifier = Modifier.fillMaxWidth()) { + Text("Inventario / Hallazgos") } } else if (visit.status == "CLOSED") { OutlinedButton(onClick = onActs, modifier = Modifier.fillMaxWidth()) { @@ -163,7 +169,7 @@ private fun F3VisitOverview( } } else if (visit.status == "PLANNED") { Text( - "Primero iniciá la inspección para habilitar altas, fotografías, Actas y Hallazgos.", + "Primero iniciá la inspección para habilitar Actas, Hallazgos y altas de campo.", style = MaterialTheme.typography.bodySmall, ) } @@ -209,7 +215,9 @@ private fun F3FieldInventoryScreen(model: MainViewModel, onBack: () -> Unit) { var search by rememberSaveable(visit.id) { mutableStateOf("") } var showCreate by rememberSaveable(visit.id) { mutableStateOf(false) } var parentId by rememberSaveable(visit.id) { mutableStateOf(null) } - var parentLabel by rememberSaveable(visit.id) { mutableStateOf("Área de la inspección") } + var parentLabel by rememberSaveable(visit.id) { + mutableStateOf(visit.scopeAsset?.name ?: "Yacimiento de la inspección") + } var name by rememberSaveable(visit.id) { mutableStateOf("") } var commonName by rememberSaveable(visit.id) { mutableStateOf("") } var selectedTypeId by rememberSaveable(visit.id) { mutableStateOf(null) } @@ -323,10 +331,10 @@ private fun F3FieldInventoryScreen(model: MainViewModel, onBack: () -> Unit) { horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { - OutlinedButton(onClick = onBack) { Text("Volver") } + OutlinedButton(onClick = onBack) { Text("Acta") } Column(horizontalAlignment = Alignment.End) { - Text("Inventario de campo", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) - Text("Área → Yacimiento → Instalación → Subinstalación", style = MaterialTheme.typography.bodySmall) + Text("Nuevo Hallazgo", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) + Text("Elegí una Instalación o Subinstalación", style = MaterialTheme.typography.bodySmall) } } Column(Modifier.padding(horizontal = 16.dp)) { F3MessageStrip(model) } @@ -410,170 +418,182 @@ private fun F3FieldInventoryScreen(model: MainViewModel, onBack: () -> Unit) { } } - Row( - Modifier.fillMaxWidth().padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - OutlinedTextField( - value = search, - onValueChange = { search = it }, - label = { Text("Buscar por nombre o código") }, - modifier = Modifier.weight(1f), - singleLine = true, - ) - Spacer(Modifier.width(8.dp)) - Button(onClick = { model.searchInventory(search) }, enabled = !model.busy) { Text("Buscar") } - } - - Row( - Modifier.fillMaxWidth().padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Column { - Text("Alta en campo", fontWeight = FontWeight.Bold) - Text("Padre: $parentLabel", style = MaterialTheme.typography.bodySmall) + Card(Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 4.dp)) { + Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text("¿Dónde encontraste el Hallazgo?", fontWeight = FontWeight.Bold) + OutlinedTextField( + value = search, + onValueChange = { search = it }, + label = { Text("Buscar instalación o subinstalación") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + ) + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Button( + onClick = { model.searchInventory(search) }, + enabled = !model.busy, + modifier = Modifier.weight(1f), + ) { Text("Buscar") } + OutlinedButton( + onClick = { + showCreate = !showCreate + if (showCreate) model.loadFieldTypes(parentId) + }, + modifier = Modifier.weight(1f), + ) { Text(if (showCreate) "Cancelar" else "+ Agregar") } + } } - OutlinedButton(onClick = { - showCreate = !showCreate - if (showCreate) model.loadFieldTypes(parentId) - }) { Text(if (showCreate) "Ocultar" else "Agregar") } } if (showCreate) { - Column( - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .verticalScroll(rememberScrollState()) - .weight(1f, fill = false), - verticalArrangement = Arrangement.spacedBy(10.dp), - ) { - if (parentId != null) { - OutlinedButton(onClick = { - parentId = null - parentLabel = "Área de la inspección" - resetCreateForm() - selectedTypeId = null - model.loadFieldTypes(null) - }) { Text("Volver al Área") } - } + Card(Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 6.dp)) { + Column( + Modifier + .fillMaxWidth() + .padding(12.dp) + .verticalScroll(rememberScrollState()) + .weight(1f, fill = false), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + Text("Nueva alta de campo", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) + Text("Ubicación: $parentLabel", style = MaterialTheme.typography.bodySmall) + if (parentId != null) { + OutlinedButton(onClick = { + parentId = null + parentLabel = visit.scopeAsset?.name ?: "Yacimiento de la inspección" + resetCreateForm() + selectedTypeId = null + model.loadFieldTypes(null) + }) { Text("Volver al Yacimiento") } + } - if (model.fieldTypes.isEmpty()) { - Text("Este nivel no admite más hijos estructurales.") - } else { - Text("Vas a crear", style = MaterialTheme.typography.bodySmall) - LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - items(model.fieldTypes, key = { it.id }) { type -> - AssistChip( - onClick = { - selectedTypeId = type.id - selectedFamilyId = null - attributeValues.clear() - }, - label = { Text(if (type.id == selectedTypeId) "✓ ${type.name}" else type.name) }, - ) + if (model.fieldTypes.isEmpty()) { + Text("No hay un tipo disponible para esta ubicación.") + } else { + Text("Tipo de registro", style = MaterialTheme.typography.bodySmall) + LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + items(model.fieldTypes, key = { it.id }) { type -> + AssistChip( + onClick = { + selectedTypeId = type.id + selectedFamilyId = null + attributeValues.clear() + }, + label = { Text(if (type.id == selectedTypeId) "✓ ${type.name}" else type.name) }, + ) + } } } - } - if (selectedType?.familyRequired == true) { - Text("Familia técnica", fontWeight = FontWeight.Bold) - Text( - "Elegí la que corresponda al Excel. Si no existe, usá Otro / no catalogado; nunca quedás bloqueado.", - style = MaterialTheme.typography.bodySmall, - ) - LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - items(selectedType.families, key = { it.id }) { family -> - AssistChip( - onClick = { selectedFamilyId = family.id }, - label = { - val prefix = when { - selectedFamilyId == family.id -> "✓ " - family.isOther -> "+ " - else -> "" - } - Text(prefix + family.name) - }, - ) + if (selectedType?.familyRequired == true) { + Text("Clasificación", fontWeight = FontWeight.Bold) + Text( + "Elegí el tipo técnico. Si no está catalogado, usá Otro / no catalogado.", + style = MaterialTheme.typography.bodySmall, + ) + LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + items(selectedType.families, key = { it.id }) { family -> + AssistChip( + onClick = { selectedFamilyId = family.id }, + label = { + val prefix = when { + selectedFamilyId == family.id -> "✓ " + family.isOther -> "+ " + else -> "" + } + Text(prefix + family.name) + }, + ) + } + } + selectedFamily?.let { family -> + if (family.isOther) { + Text( + "Se registrará como no catalogado para revisión posterior en oficina.", + color = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.bodySmall, + ) + } + if (family.informationLabels.isNotEmpty()) { + Text( + "Información esperada: ${family.informationLabels.joinToString(" · ")}", + style = MaterialTheme.typography.bodySmall, + ) + } } } - selectedFamily?.let { family -> - if (family.isOther) { - Text( - "Se registrará como familia no catalogada para revisión posterior en oficina.", - color = MaterialTheme.colorScheme.secondary, - style = MaterialTheme.typography.bodySmall, - ) - } - if (family.informationLabels.isNotEmpty()) { - Text( - "Información esperada: ${family.informationLabels.joinToString(" · ")}", - style = MaterialTheme.typography.bodySmall, - ) - } - } - } - OutlinedTextField( - value = name, - onValueChange = { name = it }, - label = { Text("Nombre identificable *") }, - modifier = Modifier.fillMaxWidth(), - ) - OutlinedTextField( - value = commonName, - onValueChange = { commonName = it }, - label = { Text("Nombre habitual") }, - modifier = Modifier.fillMaxWidth(), - ) - - selectedType?.attributes?.forEach { definition -> OutlinedTextField( - value = attributeValues[definition.code].orEmpty(), - onValueChange = { attributeValues[definition.code] = it }, - label = { Text(definition.name + if (definition.isRequired) " *" else "") }, - supportingText = { - val details = listOfNotNull(definition.unit, definition.options?.toString()).joinToString(" · ") - if (details.isNotBlank()) Text(details) - }, - keyboardOptions = KeyboardOptions( - keyboardType = if ( - definition.dataType.uppercase() in setOf("NUMBER", "DECIMAL", "INTEGER", "FLOAT") - ) KeyboardType.Decimal else KeyboardType.Text, - ), + value = name, + onValueChange = { name = it }, + label = { Text("Nombre o código identificable *") }, + modifier = Modifier.fillMaxWidth(), + ) + OutlinedTextField( + value = commonName, + onValueChange = { commonName = it }, + label = { Text("Nombre habitual") }, modifier = Modifier.fillMaxWidth(), ) - } - val attributesReady = selectedType?.attributes - ?.filter { it.isRequired } - ?.all { attributeValues[it.code].orEmpty().isNotBlank() } - ?: false - val familyReady = selectedType?.familyRequired != true || selectedFamilyId != null - Button( - onClick = { - if (f3HasLocation(context)) createWithLocation() - else locationPermissionLauncher.launch( - arrayOf( - Manifest.permission.ACCESS_FINE_LOCATION, - Manifest.permission.ACCESS_COARSE_LOCATION, + selectedType?.attributes?.forEach { definition -> + OutlinedTextField( + value = attributeValues[definition.code].orEmpty(), + onValueChange = { attributeValues[definition.code] = it }, + label = { Text(definition.name + if (definition.isRequired) " *" else "") }, + supportingText = { + val details = listOfNotNull(definition.unit, definition.options?.toString()).joinToString(" · ") + if (details.isNotBlank()) Text(details) + }, + keyboardOptions = KeyboardOptions( + keyboardType = if ( + definition.dataType.uppercase() in setOf("NUMBER", "DECIMAL", "INTEGER", "FLOAT") + ) KeyboardType.Decimal else KeyboardType.Text, ), + modifier = Modifier.fillMaxWidth(), ) - }, - enabled = selectedType != null && name.isNotBlank() && attributesReady && familyReady && !model.busy, - modifier = Modifier.fillMaxWidth(), - ) { Text("Capturar GPS y crear") } - HorizontalDivider() + } + + val attributesReady = selectedType?.attributes + ?.filter { it.isRequired } + ?.all { attributeValues[it.code].orEmpty().isNotBlank() } + ?: false + val familyReady = selectedType?.familyRequired != true || selectedFamilyId != null + Button( + onClick = { + if (f3HasLocation(context)) createWithLocation() + else locationPermissionLauncher.launch( + arrayOf( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION, + ), + ) + }, + enabled = selectedType != null && name.isNotBlank() && attributesReady && familyReady && !model.busy, + modifier = Modifier.fillMaxWidth(), + ) { Text("Guardar alta y capturar GPS") } + } } } - Text("Estructura disponible", modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), fontWeight = FontWeight.Bold) + Text( + "Instalaciones y subinstalaciones", + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + fontWeight = FontWeight.Bold, + ) LazyColumn( Modifier.fillMaxSize().padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(8.dp), ) { - items(model.inventory, key = { it.id }) { item -> + items( + model.inventory.filter { candidate -> + candidate.type?.let(::f3TypeCode) in setOf("instalacion", "subinstalacion") + }, + key = { it.id }, + ) { item -> F3InventoryCard( item = item, onInspect = { model.selectExisting(item) }, @@ -611,10 +631,10 @@ private fun F3CaptureCard( if (captureRequired) { Text("GPS de alta: ${if (gps) "OK" else "pendiente"} · Fotos: $photos") if (!ready) { - Text("Antes de registrar Hallazgos, completá GPS + foto.", color = MaterialTheme.colorScheme.error) + Text("Antes de registrar el Hallazgo, completá GPS + foto.", color = MaterialTheme.colorScheme.error) Button(onClick = onPhoto, modifier = Modifier.fillMaxWidth()) { Text("Tomar foto obligatoria") } } else { - Text("Captura completa · listo para Hallazgos", color = MaterialTheme.colorScheme.primary) + Text("Captura completa · listo para el Hallazgo", color = MaterialTheme.colorScheme.primary) } } else { Text("Registro existente seleccionado.", style = MaterialTheme.typography.bodySmall) @@ -631,8 +651,8 @@ private fun F3InventoryCard( ) { val typeCode = item.type?.let(::f3TypeCode).orEmpty() val canHaveFinding = typeCode in setOf("instalacion", "subinstalacion") - val canHaveChild = typeCode in setOf("yacimiento", "instalacion") - val childLabel = if (typeCode == "yacimiento") "Agregar instalación aquí" else "Agregar subinstalación aquí" + val canHaveChild = typeCode == "instalacion" + val childLabel = "+ Agregar subinstalación" Card(Modifier.fillMaxWidth()) { Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(5.dp)) { @@ -646,12 +666,12 @@ private fun F3InventoryCard( } if (canHaveFinding) { Text( - if (item.readyForFinding) "Disponible para Hallazgos" else "GPS/foto pendiente", + if (item.readyForFinding) "Disponible" else "GPS/foto pendiente", color = if (item.readyForFinding) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall, ) - OutlinedButton(onClick = onInspect, modifier = Modifier.fillMaxWidth()) { - Text(if (item.selectedInInspection) "Abrir Hallazgos" else "Usar en esta inspección") + Button(onClick = onInspect, modifier = Modifier.fillMaxWidth()) { + Text(if (item.selectedInInspection) "Seleccionar para Hallazgo" else "Usar para Hallazgo") } } if (canHaveChild) {