feat(android): simplify field hallazgo inventory ux

This commit is contained in:
2026-09-10 22:57:42 -03:00
parent 21790ca250
commit 3c5845bf6b
@@ -86,7 +86,10 @@ fun F3VisitRoot(model: MainViewModel) {
inventoryMode = true inventoryMode = true
}, },
) )
inventoryMode -> F3FieldInventoryScreen(model, onBack = { inventoryMode = false }) inventoryMode -> F3FieldInventoryScreen(model, onBack = {
inventoryMode = false
actsMode = true
})
else -> F3VisitOverview( else -> F3VisitOverview(
model = model, model = model,
onInventory = { inventoryMode = true }, onInventory = { inventoryMode = true },
@@ -119,6 +122,9 @@ private fun F3VisitOverview(
} }
Text(visit.code, style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold) Text(visit.code, style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold)
Text("${visit.operatorCompany?.name ?: "Sin operadora"} · ${visit.operationalArea?.name ?: "Sin área"}") 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 { visit.plannedStartAt?.let {
Text("Planificada: ${f3ShortDate(it)}", style = MaterialTheme.typography.bodySmall) Text("Planificada: ${f3ShortDate(it)}", style = MaterialTheme.typography.bodySmall)
} }
@@ -138,10 +144,10 @@ private fun F3VisitOverview(
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer),
) { ) {
Column(Modifier.padding(14.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { Column(Modifier.padding(14.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("La inspección todavía no comenzó", fontWeight = FontWeight.Bold) Text("Todo listo para comenzar", fontWeight = FontWeight.Bold)
Text("Iniciarla registra fecha/hora real y tu usuario como actor de campo.") Text("Al iniciar, pasás directamente a las Actas de esta inspección.")
Button( Button(
onClick = { model.startVisit() }, onClick = { model.startVisit(); onActs() },
enabled = !model.busy, enabled = !model.busy,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) { Text(if (model.busy) "Iniciando…" else "Iniciar inspección") } ) { Text(if (model.busy) "Iniciando…" else "Iniciar inspección") }
@@ -150,12 +156,12 @@ private fun F3VisitOverview(
} }
if (visit.status == "IN_PROGRESS") { if (visit.status == "IN_PROGRESS") {
Button(onClick = onInventory, modifier = Modifier.fillMaxWidth()) { Button(onClick = onActs, modifier = Modifier.fillMaxWidth()) {
Text("Abrir Inventario de campo")
}
OutlinedButton(onClick = onActs, modifier = Modifier.fillMaxWidth()) {
val open = model.acts.count { it.status == "DRAFT" || it.status == "READY" } 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") { } else if (visit.status == "CLOSED") {
OutlinedButton(onClick = onActs, modifier = Modifier.fillMaxWidth()) { OutlinedButton(onClick = onActs, modifier = Modifier.fillMaxWidth()) {
@@ -163,7 +169,7 @@ private fun F3VisitOverview(
} }
} else if (visit.status == "PLANNED") { } else if (visit.status == "PLANNED") {
Text( 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, style = MaterialTheme.typography.bodySmall,
) )
} }
@@ -209,7 +215,9 @@ private fun F3FieldInventoryScreen(model: MainViewModel, onBack: () -> Unit) {
var search by rememberSaveable(visit.id) { mutableStateOf("") } var search by rememberSaveable(visit.id) { mutableStateOf("") }
var showCreate by rememberSaveable(visit.id) { mutableStateOf(false) } var showCreate by rememberSaveable(visit.id) { mutableStateOf(false) }
var parentId by rememberSaveable(visit.id) { mutableStateOf<String?>(null) } var parentId by rememberSaveable(visit.id) { mutableStateOf<String?>(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 name by rememberSaveable(visit.id) { mutableStateOf("") }
var commonName by rememberSaveable(visit.id) { mutableStateOf("") } var commonName by rememberSaveable(visit.id) { mutableStateOf("") }
var selectedTypeId by rememberSaveable(visit.id) { mutableStateOf<String?>(null) } var selectedTypeId by rememberSaveable(visit.id) { mutableStateOf<String?>(null) }
@@ -323,10 +331,10 @@ private fun F3FieldInventoryScreen(model: MainViewModel, onBack: () -> Unit) {
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
OutlinedButton(onClick = onBack) { Text("Volver") } OutlinedButton(onClick = onBack) { Text("Acta") }
Column(horizontalAlignment = Alignment.End) { Column(horizontalAlignment = Alignment.End) {
Text("Inventario de campo", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) Text("Nuevo Hallazgo", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
Text("Área → Yacimiento → Instalación Subinstalación", style = MaterialTheme.typography.bodySmall) Text("Elegí una Instalación o Subinstalación", style = MaterialTheme.typography.bodySmall)
} }
} }
Column(Modifier.padding(horizontal = 16.dp)) { F3MessageStrip(model) } Column(Modifier.padding(horizontal = 16.dp)) { F3MessageStrip(model) }
@@ -410,59 +418,62 @@ private fun F3FieldInventoryScreen(model: MainViewModel, onBack: () -> Unit) {
} }
} }
Row( Card(Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 4.dp)) {
Modifier.fillMaxWidth().padding(horizontal = 16.dp), Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
verticalAlignment = Alignment.CenterVertically, Text("¿Dónde encontraste el Hallazgo?", fontWeight = FontWeight.Bold)
) {
OutlinedTextField( OutlinedTextField(
value = search, value = search,
onValueChange = { search = it }, onValueChange = { search = it },
label = { Text("Buscar por nombre o código") }, label = { Text("Buscar instalación o subinstalación") },
modifier = Modifier.weight(1f), modifier = Modifier.fillMaxWidth(),
singleLine = true, singleLine = true,
) )
Spacer(Modifier.width(8.dp))
Button(onClick = { model.searchInventory(search) }, enabled = !model.busy) { Text("Buscar") }
}
Row( Row(
Modifier.fillMaxWidth().padding(16.dp), Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) { ) {
Column { Button(
Text("Alta en campo", fontWeight = FontWeight.Bold) onClick = { model.searchInventory(search) },
Text("Padre: $parentLabel", style = MaterialTheme.typography.bodySmall) enabled = !model.busy,
} modifier = Modifier.weight(1f),
OutlinedButton(onClick = { ) { Text("Buscar") }
OutlinedButton(
onClick = {
showCreate = !showCreate showCreate = !showCreate
if (showCreate) model.loadFieldTypes(parentId) if (showCreate) model.loadFieldTypes(parentId)
}) { Text(if (showCreate) "Ocultar" else "Agregar") } },
modifier = Modifier.weight(1f),
) { Text(if (showCreate) "Cancelar" else "+ Agregar") }
}
}
} }
if (showCreate) { if (showCreate) {
Card(Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 6.dp)) {
Column( Column(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp) .padding(12.dp)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.weight(1f, fill = false), .weight(1f, fill = false),
verticalArrangement = Arrangement.spacedBy(10.dp), 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) { if (parentId != null) {
OutlinedButton(onClick = { OutlinedButton(onClick = {
parentId = null parentId = null
parentLabel = "Área de la inspección" parentLabel = visit.scopeAsset?.name ?: "Yacimiento de la inspección"
resetCreateForm() resetCreateForm()
selectedTypeId = null selectedTypeId = null
model.loadFieldTypes(null) model.loadFieldTypes(null)
}) { Text("Volver al Área") } }) { Text("Volver al Yacimiento") }
} }
if (model.fieldTypes.isEmpty()) { if (model.fieldTypes.isEmpty()) {
Text("Este nivel no admite más hijos estructurales.") Text("No hay un tipo disponible para esta ubicación.")
} else { } else {
Text("Vas a crear", style = MaterialTheme.typography.bodySmall) Text("Tipo de registro", style = MaterialTheme.typography.bodySmall)
LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
items(model.fieldTypes, key = { it.id }) { type -> items(model.fieldTypes, key = { it.id }) { type ->
AssistChip( AssistChip(
@@ -478,9 +489,9 @@ private fun F3FieldInventoryScreen(model: MainViewModel, onBack: () -> Unit) {
} }
if (selectedType?.familyRequired == true) { if (selectedType?.familyRequired == true) {
Text("Familia técnica", fontWeight = FontWeight.Bold) Text("Clasificación", fontWeight = FontWeight.Bold)
Text( Text(
"Elegí la que corresponda al Excel. Si no existe, usá Otro / no catalogado; nunca quedás bloqueado.", "Elegí el tipo técnico. Si no está catalogado, usá Otro / no catalogado.",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
) )
LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
@@ -501,7 +512,7 @@ private fun F3FieldInventoryScreen(model: MainViewModel, onBack: () -> Unit) {
selectedFamily?.let { family -> selectedFamily?.let { family ->
if (family.isOther) { if (family.isOther) {
Text( Text(
"Se registrará como familia no catalogada para revisión posterior en oficina.", "Se registrará como no catalogado para revisión posterior en oficina.",
color = MaterialTheme.colorScheme.secondary, color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
) )
@@ -518,7 +529,7 @@ private fun F3FieldInventoryScreen(model: MainViewModel, onBack: () -> Unit) {
OutlinedTextField( OutlinedTextField(
value = name, value = name,
onValueChange = { name = it }, onValueChange = { name = it },
label = { Text("Nombre identificable *") }, label = { Text("Nombre o código identificable *") },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
OutlinedTextField( OutlinedTextField(
@@ -563,17 +574,26 @@ private fun F3FieldInventoryScreen(model: MainViewModel, onBack: () -> Unit) {
}, },
enabled = selectedType != null && name.isNotBlank() && attributesReady && familyReady && !model.busy, enabled = selectedType != null && name.isNotBlank() && attributesReady && familyReady && !model.busy,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) { Text("Capturar GPS y crear") } ) { Text("Guardar alta y capturar GPS") }
HorizontalDivider() }
} }
} }
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( LazyColumn(
Modifier.fillMaxSize().padding(horizontal = 16.dp), Modifier.fillMaxSize().padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.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( F3InventoryCard(
item = item, item = item,
onInspect = { model.selectExisting(item) }, onInspect = { model.selectExisting(item) },
@@ -611,10 +631,10 @@ private fun F3CaptureCard(
if (captureRequired) { if (captureRequired) {
Text("GPS de alta: ${if (gps) "OK" else "pendiente"} · Fotos: $photos") Text("GPS de alta: ${if (gps) "OK" else "pendiente"} · Fotos: $photos")
if (!ready) { 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") } Button(onClick = onPhoto, modifier = Modifier.fillMaxWidth()) { Text("Tomar foto obligatoria") }
} else { } else {
Text("Captura completa · listo para Hallazgos", color = MaterialTheme.colorScheme.primary) Text("Captura completa · listo para el Hallazgo", color = MaterialTheme.colorScheme.primary)
} }
} else { } else {
Text("Registro existente seleccionado.", style = MaterialTheme.typography.bodySmall) Text("Registro existente seleccionado.", style = MaterialTheme.typography.bodySmall)
@@ -631,8 +651,8 @@ private fun F3InventoryCard(
) { ) {
val typeCode = item.type?.let(::f3TypeCode).orEmpty() val typeCode = item.type?.let(::f3TypeCode).orEmpty()
val canHaveFinding = typeCode in setOf("instalacion", "subinstalacion") val canHaveFinding = typeCode in setOf("instalacion", "subinstalacion")
val canHaveChild = typeCode in setOf("yacimiento", "instalacion") val canHaveChild = typeCode == "instalacion"
val childLabel = if (typeCode == "yacimiento") "Agregar instalación aquí" else "Agregar subinstalación aquí" val childLabel = "+ Agregar subinstalación"
Card(Modifier.fillMaxWidth()) { Card(Modifier.fillMaxWidth()) {
Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(5.dp)) { Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(5.dp)) {
@@ -646,12 +666,12 @@ private fun F3InventoryCard(
} }
if (canHaveFinding) { if (canHaveFinding) {
Text( 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, color = if (item.readyForFinding) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
) )
OutlinedButton(onClick = onInspect, modifier = Modifier.fillMaxWidth()) { Button(onClick = onInspect, modifier = Modifier.fillMaxWidth()) {
Text(if (item.selectedInInspection) "Abrir Hallazgos" else "Usar en esta inspección") Text(if (item.selectedInInspection) "Seleccionar para Hallazgo" else "Usar para Hallazgo")
} }
} }
if (canHaveChild) { if (canHaveChild) {