fix(android): Acta → Hallazgo → Inventario en campo
Simplifica el flujo operativo móvil, corrige el alta desde el Yacimiento congelado, permite crear Actas antes de seleccionar Inventario y precarga Marca, Modelo, Capacidad, Número de serie y Función en Instalaciones/Subinstalaciones. Android 0.16.0.
This commit is contained in:
@@ -168,10 +168,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
error = "Elegí si el Acta es urgente o no urgente."
|
||||
return
|
||||
}
|
||||
if (asset == null) {
|
||||
error = "Seleccioná primero una Instalación o Subinstalación para iniciar el Acta."
|
||||
return
|
||||
}
|
||||
if (acts.any { it.status == "DRAFT" }) {
|
||||
error = "Ya existe un Acta en borrador. Bloqueala o cancelala antes de crear la siguiente."
|
||||
return
|
||||
@@ -179,7 +175,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
launchBusy {
|
||||
val created = actsRepository.create(
|
||||
currentVisit.id,
|
||||
asset.id,
|
||||
asset?.id,
|
||||
currentVisit.code,
|
||||
urgency,
|
||||
)
|
||||
@@ -188,17 +184,18 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
loadActsInternal(currentVisit.id, selectDraft = false)
|
||||
val urgencyLabel = if (urgency == "URGENT") "urgente" else "no urgente"
|
||||
notice = "${created.code} creada como $urgencyLabel. Los Hallazgos nuevos quedarán vinculados explícitamente a esta Acta."
|
||||
if (selectedFieldAsset?.capture?.readyForFinding == true) {
|
||||
if (asset != null && selectedFieldAsset?.capture?.readyForFinding == true) {
|
||||
loadFindingOptionsInternal(currentVisit.id, asset.id, created.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createAct(urgency: String = "NON_URGENT") = createActForSelectedInventory(urgency)
|
||||
|
||||
fun searchInventory(search: String, parentId: String? = null) {
|
||||
val currentVisit = visit ?: return
|
||||
launchBusy {
|
||||
val effectiveParentId = parentId ?: inventoryParentId ?: currentVisit.scopeAsset?.id ?: currentVisit.operationalArea?.id
|
||||
inventory = repository.fieldInventory(currentVisit.id, search, effectiveParentId).data
|
||||
inventory = repository.fieldInventory(currentVisit.id, search, parentId).data
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -329,7 +329,7 @@ class MobileActsRepository(context: Context) {
|
||||
|
||||
suspend fun create(
|
||||
visitId: String,
|
||||
assetId: String,
|
||||
assetId: String? = null,
|
||||
visitCode: String,
|
||||
urgency: String = "NON_URGENT",
|
||||
): MobileActDetail = authorized { session ->
|
||||
@@ -341,7 +341,7 @@ class MobileActsRepository(context: Context) {
|
||||
urgency = urgency,
|
||||
title = "Acta de inspección $visitCode",
|
||||
summary = "Acta de inspección en curso. Los Hallazgos y observaciones se incorporan de forma trazable durante la inspección.",
|
||||
assetIds = listOf(assetId),
|
||||
assetIds = listOfNotNull(assetId),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<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 commonName by rememberSaveable(visit.id) { mutableStateOf("") }
|
||||
var selectedTypeId by rememberSaveable(visit.id) { mutableStateOf<String?>(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) {
|
||||
|
||||
@@ -126,13 +126,14 @@ fun MobileActsScreen(
|
||||
Text("Actas", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
Text("${visit.code} · ${visit.operatorCompany?.name.orEmpty()}")
|
||||
visit.scopeAsset?.let { Text("Yacimiento: ${it.name}", style = MaterialTheme.typography.bodySmall) }
|
||||
F32ActMessage(model)
|
||||
|
||||
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.")
|
||||
Text("Todavía no hay Actas. Creá la primera y después agregá los Hallazgos.")
|
||||
}
|
||||
model.acts.forEach { act ->
|
||||
val active = selected?.id == act.id
|
||||
@@ -156,15 +157,15 @@ fun MobileActsScreen(
|
||||
Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer),
|
||||
) {
|
||||
Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text("Nueva Acta", fontWeight = FontWeight.Bold)
|
||||
Column(Modifier.padding(14.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
Text("Nueva Acta", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
||||
if (model.acts.any { it.status == "LOCKED" }) {
|
||||
Text(
|
||||
"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)
|
||||
Text("Urgencia", fontWeight = FontWeight.Bold)
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
AssistChip(
|
||||
onClick = { newActUrgency = "NON_URGENT" },
|
||||
@@ -183,20 +184,15 @@ fun MobileActsScreen(
|
||||
},
|
||||
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.")
|
||||
Button(onClick = onGoInventory, modifier = Modifier.fillMaxWidth()) {
|
||||
Text("Ir a Inventario y elegir")
|
||||
}
|
||||
} else {
|
||||
Text("Inventario inicial: ${selectedInventory.name} · ${selectedInventory.code}")
|
||||
Button(
|
||||
onClick = { model.createActForSelectedInventory(newActUrgency) },
|
||||
enabled = !model.busy,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) { Text("Crear nueva Acta") }
|
||||
}
|
||||
Text(
|
||||
"Abrí el Acta primero. Después elegís la Instalación o Subinstalación al agregar cada Hallazgo.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
Button(
|
||||
onClick = { model.createAct(newActUrgency) },
|
||||
enabled = !model.busy,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) { Text("Crear nueva Acta") }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -220,7 +216,21 @@ fun MobileActsScreen(
|
||||
|
||||
when (selected.status) {
|
||||
"DRAFT" -> {
|
||||
Text("1. Responsable de la empresa", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
||||
Card(
|
||||
Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer),
|
||||
) {
|
||||
Column(Modifier.padding(14.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text("Hallazgos", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
||||
Text("Buscá una Instalación o Subinstalación existente. Si no está, podés agregarla en campo en el mismo flujo.")
|
||||
Button(onClick = onGoInventory, modifier = Modifier.fillMaxWidth()) {
|
||||
Text("+ Agregar Hallazgo")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
Text("Responsable de la empresa", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
AssistChip(onClick = { attendance = "PRESENT" }, label = { Text(if (attendance == "PRESENT") "✓ Presente" else "Presente") })
|
||||
AssistChip(onClick = { attendance = "ABSENT" }, label = { Text(if (attendance == "ABSENT") "✓ Ausente" else "Ausente") })
|
||||
@@ -259,12 +269,7 @@ fun MobileActsScreen(
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
Text("2. Hallazgos / verificaciones", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
||||
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. Finalizar contenido", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
||||
Text("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() },
|
||||
|
||||
@@ -8,8 +8,8 @@ class ReleaseMetadataTest {
|
||||
@Test
|
||||
fun debugBuildKeepsSeparateApplicationIdentity() {
|
||||
assertEquals("com.korexlabs.dhinspeccion.debug", BuildConfig.APPLICATION_ID)
|
||||
assertEquals(24, BuildConfig.VERSION_CODE)
|
||||
assertEquals("0.15.2-debug", BuildConfig.VERSION_NAME)
|
||||
assertEquals(25, BuildConfig.VERSION_CODE)
|
||||
assertEquals("0.16.0-debug", BuildConfig.VERSION_NAME)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user