fix(android): scope mobile inspections to yacimiento
DH V2 CI / API · typecheck, tests, build (push) Successful in 5m16s
Android CI / RC / Android · lint, tests, debug APK, release compile (push) Failing after 6m13s
DH V2 CI / WEB · typecheck, build (push) Successful in 4m52s
DH V2 CI / Docker / migrations / production images (push) Skipped
DH V2 CI / Promote verified main to deploy (push) Skipped

This commit is contained in:
2026-09-16 07:33:03 -03:00
parent 29208f4e1f
commit c71afab9af
18 changed files with 287 additions and 70 deletions
@@ -25,22 +25,36 @@ data class MobilePlanningAssetResponse(
)
data class OpenMobileInspectionRequest(
val departmentId: String,
val operationalAreaId: String,
val scopeAssetId: String,
val operatorCompanyId: String,
)
private interface MobileInspectionOpenApi {
@GET("inspection-visits/mobile/planning-context/areas")
suspend fun areas(
@GET("inspection-visits/mobile/planning-context/departments")
suspend fun departments(
@Header("Authorization") authorization: String,
): MobilePlanningAssetResponse
@GET("inspection-visits/mobile/planning-context/areas/{areaId}/operators")
suspend fun operators(
@GET("inspection-visits/mobile/planning-context/departments/{departmentId}/areas")
suspend fun areas(
@Header("Authorization") authorization: String,
@Path("departmentId") departmentId: String,
): MobilePlanningAssetResponse
@GET("inspection-visits/mobile/planning-context/areas/{areaId}/yacimientos")
suspend fun yacimientos(
@Header("Authorization") authorization: String,
@Path("areaId") areaId: String,
): MobilePlanningAssetResponse
@GET("inspection-visits/mobile/planning-context/yacimientos/{yacimientoId}/operators")
suspend fun operators(
@Header("Authorization") authorization: String,
@Path("yacimientoId") yacimientoId: String,
): MobilePlanningAssetResponse
@POST("inspection-visits/mobile/open")
suspend fun open(
@Header("Authorization") authorization: String,
@@ -61,18 +75,31 @@ class MobileInspectionOpenRepository(context: Context) {
.build()
.create(MobileInspectionOpenApi::class.java)
suspend fun areas(): MobilePlanningAssetResponse = authorized { session ->
api.areas("Bearer ${session.accessToken}")
suspend fun departments(): MobilePlanningAssetResponse = authorized { session ->
api.departments("Bearer ${session.accessToken}")
}
suspend fun operators(areaId: String): MobilePlanningAssetResponse = authorized { session ->
api.operators("Bearer ${session.accessToken}", areaId)
suspend fun areas(departmentId: String): MobilePlanningAssetResponse = authorized { session ->
api.areas("Bearer ${session.accessToken}", departmentId)
}
suspend fun open(areaId: String, companyId: String): VisitDetail = authorized { session ->
suspend fun yacimientos(areaId: String): MobilePlanningAssetResponse = authorized { session ->
api.yacimientos("Bearer ${session.accessToken}", areaId)
}
suspend fun operators(yacimientoId: String): MobilePlanningAssetResponse = authorized { session ->
api.operators("Bearer ${session.accessToken}", yacimientoId)
}
suspend fun open(
departmentId: String,
areaId: String,
yacimientoId: String,
companyId: String,
): VisitDetail = authorized { session ->
api.open(
"Bearer ${session.accessToken}",
OpenMobileInspectionRequest(areaId, companyId),
OpenMobileInspectionRequest(departmentId, areaId, yacimientoId, companyId),
)
}
@@ -89,5 +116,4 @@ class MobileInspectionOpenRepository(context: Context) {
private suspend fun refresh(previous: StoredSession): StoredSession =
MobileSessionCoordinator.refresh(previous, store::load, store::save, store::clear, api::refresh)
}
@@ -47,19 +47,41 @@ fun MobileHomeScreen(model: MainViewModel) {
val openRepository = remember(context) { MobileInspectionOpenRepository(context) }
var showOpen by rememberSaveable { mutableStateOf(false) }
var departments by remember { mutableStateOf<List<MobilePlanningAsset>>(emptyList()) }
var areas by remember { mutableStateOf<List<MobilePlanningAsset>>(emptyList()) }
var yacimientos by remember { mutableStateOf<List<MobilePlanningAsset>>(emptyList()) }
var operators by remember { mutableStateOf<List<MobilePlanningAsset>>(emptyList()) }
var selectedDepartmentId by rememberSaveable { mutableStateOf<String?>(null) }
var selectedAreaId by rememberSaveable { mutableStateOf<String?>(null) }
var selectedYacimientoId by rememberSaveable { mutableStateOf<String?>(null) }
var selectedCompanyId by rememberSaveable { mutableStateOf<String?>(null) }
var loadingContext by remember { mutableStateOf(false) }
var opening by remember { mutableStateOf(false) }
var localError by remember { mutableStateOf<String?>(null) }
fun loadAreas() {
fun loadDepartments() {
scope.launch {
loadingContext = true
localError = null
runCatching { openRepository.areas().data }
runCatching { openRepository.departments().data }
.onSuccess { departments = it }
.onFailure { localError = DhRepository.humanError(it) }
loadingContext = false
}
}
fun chooseDepartment(departmentId: String) {
selectedDepartmentId = departmentId
selectedAreaId = null
selectedYacimientoId = null
selectedCompanyId = null
areas = emptyList()
yacimientos = emptyList()
operators = emptyList()
scope.launch {
loadingContext = true
localError = null
runCatching { openRepository.areas(departmentId).data }
.onSuccess { areas = it }
.onFailure { localError = DhRepository.humanError(it) }
loadingContext = false
@@ -68,20 +90,39 @@ fun MobileHomeScreen(model: MainViewModel) {
fun chooseArea(areaId: String) {
selectedAreaId = areaId
selectedYacimientoId = null
selectedCompanyId = null
yacimientos = emptyList()
operators = emptyList()
scope.launch {
loadingContext = true
localError = null
runCatching { openRepository.yacimientos(areaId).data }
.onSuccess { yacimientos = it }
.onFailure { localError = DhRepository.humanError(it) }
loadingContext = false
}
}
fun chooseYacimiento(yacimientoId: String) {
selectedYacimientoId = yacimientoId
selectedCompanyId = null
operators = emptyList()
scope.launch {
loadingContext = true
localError = null
runCatching { openRepository.operators(areaId).data }
.onSuccess { operators = it }
runCatching { openRepository.operators(yacimientoId).data }
.onSuccess {
operators = it
if (it.size == 1) selectedCompanyId = it.first().id
}
.onFailure { localError = DhRepository.humanError(it) }
loadingContext = false
}
}
LaunchedEffect(showOpen) {
if (showOpen && areas.isEmpty()) loadAreas()
if (showOpen && departments.isEmpty()) loadDepartments()
}
Column(Modifier.fillMaxSize().padding(top = 28.dp)) {
@@ -142,27 +183,57 @@ fun MobileHomeScreen(model: MainViewModel) {
) {
Text("Abrir inspección en campo", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
Text(
"Elegí el Área y la Operadora vigente. La inspección se crea autoasignada a vos y queda iniciada con la fecha y hora del servidor.",
"Definí el contexto completo antes de abrir el Acta: Departamento → Área → Yacimiento → Operadora. La inspección queda autoasignada a vos y se inicia con la hora del servidor.",
style = MaterialTheme.typography.bodySmall,
)
Text("1. Área", fontWeight = FontWeight.SemiBold)
if (loadingContext && areas.isEmpty()) CircularProgressIndicator()
Text("1. Departamento", fontWeight = FontWeight.SemiBold)
if (loadingContext && departments.isEmpty()) CircularProgressIndicator()
LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
items(areas, key = { it.id }) { area ->
items(departments, key = { it.id }) { department ->
AssistChip(
onClick = { chooseArea(area.id) },
label = { Text(if (area.id == selectedAreaId) "${area.name}" else area.name) },
onClick = { chooseDepartment(department.id) },
label = { Text(if (department.id == selectedDepartmentId) "${department.name}" else department.name) },
)
}
}
if (selectedDepartmentId != null) {
Text("2. Área", fontWeight = FontWeight.SemiBold)
if (loadingContext && areas.isEmpty()) CircularProgressIndicator()
LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
items(areas, key = { it.id }) { area ->
AssistChip(
onClick = { chooseArea(area.id) },
label = { Text(if (area.id == selectedAreaId) "${area.name}" else area.name) },
)
}
}
}
if (selectedAreaId != null) {
Text("2. Operadora", fontWeight = FontWeight.SemiBold)
Text("3. Yacimiento", fontWeight = FontWeight.SemiBold)
if (loadingContext && yacimientos.isEmpty()) {
CircularProgressIndicator()
} else if (yacimientos.isEmpty()) {
Text("No hay Yacimientos disponibles para el Área seleccionada.", color = MaterialTheme.colorScheme.error)
}
LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
items(yacimientos, key = { it.id }) { yacimiento ->
AssistChip(
onClick = { chooseYacimiento(yacimiento.id) },
label = { Text(if (yacimiento.id == selectedYacimientoId) "${yacimiento.name}" else yacimiento.name) },
)
}
}
}
if (selectedYacimientoId != null) {
Text("4. Operadora", fontWeight = FontWeight.SemiBold)
if (loadingContext && operators.isEmpty()) {
CircularProgressIndicator()
} else if (operators.isEmpty()) {
Text("No hay una Operadora vigente para el Área seleccionada.", color = MaterialTheme.colorScheme.error)
Text("El Yacimiento no tiene una Operadora válida configurada.", color = MaterialTheme.colorScheme.error)
}
LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
items(operators, key = { it.id }) { company ->
@@ -176,12 +247,14 @@ fun MobileHomeScreen(model: MainViewModel) {
Button(
onClick = {
val departmentId = selectedDepartmentId ?: return@Button
val areaId = selectedAreaId ?: return@Button
val yacimientoId = selectedYacimientoId ?: return@Button
val companyId = selectedCompanyId ?: return@Button
scope.launch {
opening = true
localError = null
runCatching { openRepository.open(areaId, companyId) }
runCatching { openRepository.open(departmentId, areaId, yacimientoId, companyId) }
.onSuccess { opened ->
showOpen = false
model.openVisit(opened.id)
@@ -191,9 +264,9 @@ fun MobileHomeScreen(model: MainViewModel) {
}
},
modifier = Modifier.fillMaxWidth(),
enabled = selectedAreaId != null && selectedCompanyId != null && !opening && !loadingContext,
enabled = selectedDepartmentId != null && selectedAreaId != null && selectedYacimientoId != null && selectedCompanyId != null && !opening && !loadingContext,
) {
Text(if (opening) "Abriendo…" else "Abrir inspección ahora")
Text(if (opening) "Abriendo…" else "Abrir inspección y continuar al Acta")
}
}
}
@@ -228,6 +301,7 @@ private fun MobileVisitCard(visit: VisitSummary, onOpen: () -> Unit) {
Text(visit.code, fontWeight = FontWeight.Bold)
Text(visitStatusLabelEs(visit.status))
}
Text(visit.scopeAsset?.name ?: "Yacimiento sin definir", fontWeight = FontWeight.SemiBold)
Text(visit.operatorCompany?.name ?: "Operadora sin definir")
Text(visit.operationalArea?.name ?: "Área sin definir", style = MaterialTheme.typography.bodySmall)
visit.plannedStartAt?.let {
@@ -261,7 +261,7 @@ fun ModernMobileActsScreen(
Column(Modifier.padding(18.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
Text("Hallazgos", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
Text(
"Elegí una Instalación o Subinstalación existente, o creala en campo si todavía no está registrada.",
"Elegí el Yacimiento, una Instalación o una Subinstalación. El Hallazgo siempre queda dentro del Yacimiento de esta Acta.",
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Button(onClick = onGoInventory, modifier = Modifier.fillMaxWidth()) {
@@ -107,7 +107,9 @@ private data class ModernGeoSnapshot(
@Composable
fun ModernVisitRoot(model: MainViewModel) {
val visit = model.visit ?: return
var screenName by rememberSaveable(visit.id) { mutableStateOf(ModernVisitScreen.OVERVIEW.name) }
var screenName by rememberSaveable(visit.id) {
mutableStateOf(if (visit.status == "IN_PROGRESS") ModernVisitScreen.ACTS.name else ModernVisitScreen.OVERVIEW.name)
}
val screen = runCatching { ModernVisitScreen.valueOf(screenName) }.getOrDefault(ModernVisitScreen.OVERVIEW)
when (screen) {
@@ -938,7 +940,7 @@ private fun ModernInventoryBrowse(
OutlinedTextField(
value = search,
onValueChange = onSearchChange,
label = { Text("Buscar Instalación o Subinstalación") },
label = { Text("Buscar Yacimiento, Instalación o Subinstalación") },
placeholder = { Text("Nombre, código o dato técnico") },
leadingIcon = { Icon(Icons.Filled.Search, null) },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
@@ -8,8 +8,8 @@ class ReleaseMetadataTest {
@Test
fun debugBuildKeepsSeparateApplicationIdentity() {
assertEquals("com.korexlabs.dhinspeccion.debug", BuildConfig.APPLICATION_ID)
assertEquals(40, BuildConfig.VERSION_CODE)
assertEquals("0.19.12-debug", BuildConfig.VERSION_NAME)
assertEquals(41, BuildConfig.VERSION_CODE)
assertEquals("0.19.13-debug", BuildConfig.VERSION_NAME)
}
@Test