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
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:
@@ -1,5 +1,5 @@
|
|||||||
name: Android CI / RC
|
name: Android CI / RC
|
||||||
# F6.13 client-demo barrier: lint + real tests + debug artifact + release compile.
|
# F6.14 yacimiento-scope barrier: lint + real tests + debug artifact + release compile.
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -111,7 +111,7 @@ jobs:
|
|||||||
cp android-app/app/build/outputs/apk/debug/app-debug.apk "$apk"
|
cp android-app/app/build/outputs/apk/debug/app-debug.apk "$apk"
|
||||||
sha256sum "$apk" > "${apk}.sha256"
|
sha256sum "$apk" > "${apk}.sha256"
|
||||||
{
|
{
|
||||||
echo "phase=F6.13"
|
echo "phase=F6.14"
|
||||||
echo "version=$version"
|
echo "version=$version"
|
||||||
echo "versionCode=$code"
|
echo "versionCode=$code"
|
||||||
echo "commit=$GITHUB_SHA"
|
echo "commit=$GITHUB_SHA"
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# DH Inspección Android · release de campo 0.19.12
|
# DH Inspección Android · release de campo 0.19.13
|
||||||
|
|
||||||
## Candidata vigente
|
## Candidata vigente
|
||||||
|
|
||||||
- `versionName`: **0.19.12**; `versionCode`: **40**.
|
- `versionName`: **0.19.13**; `versionCode`: **41**.
|
||||||
- API: `https://dhv2.korexlabs.com/api/v3/`; compatible con API 0.29.0-13 / WEB 0.23.0-10.
|
- API: `https://dhv2.korexlabs.com/api/v3/`; compatible con API 0.29.0-14 / WEB 0.23.0-10.
|
||||||
- Antes de cerrar el contenido, el inspector escribe una descripción real de lo actuado.
|
- Antes de cerrar el contenido, el inspector escribe una descripción real de lo actuado.
|
||||||
- La descripción se sincroniza antes del bloqueo, también cuando se trabajó sin conexión.
|
- La descripción se sincroniza antes del bloqueo, también cuando se trabajó sin conexión.
|
||||||
- Un acta anterior, ya sellada, conserva el texto originalmente registrado.
|
- Un acta anterior, ya sellada, conserva el texto originalmente registrado.
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ android {
|
|||||||
applicationId = "com.korexlabs.dhinspeccion"
|
applicationId = "com.korexlabs.dhinspeccion"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 40
|
versionCode = 41
|
||||||
versionName = "0.19.12"
|
versionName = "0.19.13"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
|||||||
+37
-11
@@ -25,22 +25,36 @@ data class MobilePlanningAssetResponse(
|
|||||||
)
|
)
|
||||||
|
|
||||||
data class OpenMobileInspectionRequest(
|
data class OpenMobileInspectionRequest(
|
||||||
|
val departmentId: String,
|
||||||
val operationalAreaId: String,
|
val operationalAreaId: String,
|
||||||
|
val scopeAssetId: String,
|
||||||
val operatorCompanyId: String,
|
val operatorCompanyId: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
private interface MobileInspectionOpenApi {
|
private interface MobileInspectionOpenApi {
|
||||||
@GET("inspection-visits/mobile/planning-context/areas")
|
@GET("inspection-visits/mobile/planning-context/departments")
|
||||||
suspend fun areas(
|
suspend fun departments(
|
||||||
@Header("Authorization") authorization: String,
|
@Header("Authorization") authorization: String,
|
||||||
): MobilePlanningAssetResponse
|
): MobilePlanningAssetResponse
|
||||||
|
|
||||||
@GET("inspection-visits/mobile/planning-context/areas/{areaId}/operators")
|
@GET("inspection-visits/mobile/planning-context/departments/{departmentId}/areas")
|
||||||
suspend fun operators(
|
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,
|
@Header("Authorization") authorization: String,
|
||||||
@Path("areaId") areaId: String,
|
@Path("areaId") areaId: String,
|
||||||
): MobilePlanningAssetResponse
|
): 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")
|
@POST("inspection-visits/mobile/open")
|
||||||
suspend fun open(
|
suspend fun open(
|
||||||
@Header("Authorization") authorization: String,
|
@Header("Authorization") authorization: String,
|
||||||
@@ -61,18 +75,31 @@ class MobileInspectionOpenRepository(context: Context) {
|
|||||||
.build()
|
.build()
|
||||||
.create(MobileInspectionOpenApi::class.java)
|
.create(MobileInspectionOpenApi::class.java)
|
||||||
|
|
||||||
suspend fun areas(): MobilePlanningAssetResponse = authorized { session ->
|
suspend fun departments(): MobilePlanningAssetResponse = authorized { session ->
|
||||||
api.areas("Bearer ${session.accessToken}")
|
api.departments("Bearer ${session.accessToken}")
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun operators(areaId: String): MobilePlanningAssetResponse = authorized { session ->
|
suspend fun areas(departmentId: String): MobilePlanningAssetResponse = authorized { session ->
|
||||||
api.operators("Bearer ${session.accessToken}", areaId)
|
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(
|
api.open(
|
||||||
"Bearer ${session.accessToken}",
|
"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 =
|
private suspend fun refresh(previous: StoredSession): StoredSession =
|
||||||
MobileSessionCoordinator.refresh(previous, store::load, store::save, store::clear, api::refresh)
|
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) }
|
val openRepository = remember(context) { MobileInspectionOpenRepository(context) }
|
||||||
|
|
||||||
var showOpen by rememberSaveable { mutableStateOf(false) }
|
var showOpen by rememberSaveable { mutableStateOf(false) }
|
||||||
|
var departments by remember { mutableStateOf<List<MobilePlanningAsset>>(emptyList()) }
|
||||||
var areas 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 operators by remember { mutableStateOf<List<MobilePlanningAsset>>(emptyList()) }
|
||||||
|
var selectedDepartmentId by rememberSaveable { mutableStateOf<String?>(null) }
|
||||||
var selectedAreaId 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 selectedCompanyId by rememberSaveable { mutableStateOf<String?>(null) }
|
||||||
var loadingContext by remember { mutableStateOf(false) }
|
var loadingContext by remember { mutableStateOf(false) }
|
||||||
var opening by remember { mutableStateOf(false) }
|
var opening by remember { mutableStateOf(false) }
|
||||||
var localError by remember { mutableStateOf<String?>(null) }
|
var localError by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
fun loadAreas() {
|
fun loadDepartments() {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
loadingContext = true
|
loadingContext = true
|
||||||
localError = null
|
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 }
|
.onSuccess { areas = it }
|
||||||
.onFailure { localError = DhRepository.humanError(it) }
|
.onFailure { localError = DhRepository.humanError(it) }
|
||||||
loadingContext = false
|
loadingContext = false
|
||||||
@@ -68,20 +90,39 @@ fun MobileHomeScreen(model: MainViewModel) {
|
|||||||
|
|
||||||
fun chooseArea(areaId: String) {
|
fun chooseArea(areaId: String) {
|
||||||
selectedAreaId = areaId
|
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
|
selectedCompanyId = null
|
||||||
operators = emptyList()
|
operators = emptyList()
|
||||||
scope.launch {
|
scope.launch {
|
||||||
loadingContext = true
|
loadingContext = true
|
||||||
localError = null
|
localError = null
|
||||||
runCatching { openRepository.operators(areaId).data }
|
runCatching { openRepository.operators(yacimientoId).data }
|
||||||
.onSuccess { operators = it }
|
.onSuccess {
|
||||||
|
operators = it
|
||||||
|
if (it.size == 1) selectedCompanyId = it.first().id
|
||||||
|
}
|
||||||
.onFailure { localError = DhRepository.humanError(it) }
|
.onFailure { localError = DhRepository.humanError(it) }
|
||||||
loadingContext = false
|
loadingContext = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(showOpen) {
|
LaunchedEffect(showOpen) {
|
||||||
if (showOpen && areas.isEmpty()) loadAreas()
|
if (showOpen && departments.isEmpty()) loadDepartments()
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(Modifier.fillMaxSize().padding(top = 28.dp)) {
|
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("Abrir inspección en campo", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
||||||
Text(
|
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,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
)
|
)
|
||||||
|
|
||||||
Text("1. Área", fontWeight = FontWeight.SemiBold)
|
Text("1. Departamento", fontWeight = FontWeight.SemiBold)
|
||||||
if (loadingContext && areas.isEmpty()) CircularProgressIndicator()
|
if (loadingContext && departments.isEmpty()) CircularProgressIndicator()
|
||||||
LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
items(areas, key = { it.id }) { area ->
|
items(departments, key = { it.id }) { department ->
|
||||||
AssistChip(
|
AssistChip(
|
||||||
onClick = { chooseArea(area.id) },
|
onClick = { chooseDepartment(department.id) },
|
||||||
label = { Text(if (area.id == selectedAreaId) "✓ ${area.name}" else area.name) },
|
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) {
|
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()) {
|
if (loadingContext && operators.isEmpty()) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
} else if (operators.isEmpty()) {
|
} 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)) {
|
LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
items(operators, key = { it.id }) { company ->
|
items(operators, key = { it.id }) { company ->
|
||||||
@@ -176,12 +247,14 @@ fun MobileHomeScreen(model: MainViewModel) {
|
|||||||
|
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
|
val departmentId = selectedDepartmentId ?: return@Button
|
||||||
val areaId = selectedAreaId ?: return@Button
|
val areaId = selectedAreaId ?: return@Button
|
||||||
|
val yacimientoId = selectedYacimientoId ?: return@Button
|
||||||
val companyId = selectedCompanyId ?: return@Button
|
val companyId = selectedCompanyId ?: return@Button
|
||||||
scope.launch {
|
scope.launch {
|
||||||
opening = true
|
opening = true
|
||||||
localError = null
|
localError = null
|
||||||
runCatching { openRepository.open(areaId, companyId) }
|
runCatching { openRepository.open(departmentId, areaId, yacimientoId, companyId) }
|
||||||
.onSuccess { opened ->
|
.onSuccess { opened ->
|
||||||
showOpen = false
|
showOpen = false
|
||||||
model.openVisit(opened.id)
|
model.openVisit(opened.id)
|
||||||
@@ -191,9 +264,9 @@ fun MobileHomeScreen(model: MainViewModel) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxWidth(),
|
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(visit.code, fontWeight = FontWeight.Bold)
|
||||||
Text(visitStatusLabelEs(visit.status))
|
Text(visitStatusLabelEs(visit.status))
|
||||||
}
|
}
|
||||||
|
Text(visit.scopeAsset?.name ?: "Yacimiento sin definir", fontWeight = FontWeight.SemiBold)
|
||||||
Text(visit.operatorCompany?.name ?: "Operadora sin definir")
|
Text(visit.operatorCompany?.name ?: "Operadora sin definir")
|
||||||
Text(visit.operationalArea?.name ?: "Área sin definir", style = MaterialTheme.typography.bodySmall)
|
Text(visit.operationalArea?.name ?: "Área sin definir", style = MaterialTheme.typography.bodySmall)
|
||||||
visit.plannedStartAt?.let {
|
visit.plannedStartAt?.let {
|
||||||
|
|||||||
+1
-1
@@ -261,7 +261,7 @@ fun ModernMobileActsScreen(
|
|||||||
Column(Modifier.padding(18.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
Column(Modifier.padding(18.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
Text("Hallazgos", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
Text("Hallazgos", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
||||||
Text(
|
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,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
Button(onClick = onGoInventory, modifier = Modifier.fillMaxWidth()) {
|
Button(onClick = onGoInventory, modifier = Modifier.fillMaxWidth()) {
|
||||||
|
|||||||
@@ -107,7 +107,9 @@ private data class ModernGeoSnapshot(
|
|||||||
@Composable
|
@Composable
|
||||||
fun ModernVisitRoot(model: MainViewModel) {
|
fun ModernVisitRoot(model: MainViewModel) {
|
||||||
val visit = model.visit ?: return
|
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)
|
val screen = runCatching { ModernVisitScreen.valueOf(screenName) }.getOrDefault(ModernVisitScreen.OVERVIEW)
|
||||||
|
|
||||||
when (screen) {
|
when (screen) {
|
||||||
@@ -938,7 +940,7 @@ private fun ModernInventoryBrowse(
|
|||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = search,
|
value = search,
|
||||||
onValueChange = onSearchChange,
|
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") },
|
placeholder = { Text("Nombre, código o dato técnico") },
|
||||||
leadingIcon = { Icon(Icons.Filled.Search, null) },
|
leadingIcon = { Icon(Icons.Filled.Search, null) },
|
||||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ class ReleaseMetadataTest {
|
|||||||
@Test
|
@Test
|
||||||
fun debugBuildKeepsSeparateApplicationIdentity() {
|
fun debugBuildKeepsSeparateApplicationIdentity() {
|
||||||
assertEquals("com.korexlabs.dhinspeccion.debug", BuildConfig.APPLICATION_ID)
|
assertEquals("com.korexlabs.dhinspeccion.debug", BuildConfig.APPLICATION_ID)
|
||||||
assertEquals(40, BuildConfig.VERSION_CODE)
|
assertEquals(41, BuildConfig.VERSION_CODE)
|
||||||
assertEquals("0.19.12-debug", BuildConfig.VERSION_NAME)
|
assertEquals("0.19.13-debug", BuildConfig.VERSION_NAME)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "dhv2-api",
|
"name": "dhv2-api",
|
||||||
"version": "0.29.0-13",
|
"version": "0.29.0-14",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "dhv2-api",
|
"name": "dhv2-api",
|
||||||
"version": "0.29.0-13",
|
"version": "0.29.0-14",
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/common": "^11.0.0",
|
"@nestjs/common": "^11.0.0",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dhv2-api",
|
"name": "dhv2-api",
|
||||||
"version": "0.29.0-13",
|
"version": "0.29.0-14",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import { IsUUID } from 'class-validator';
|
import { IsUUID } from 'class-validator';
|
||||||
|
|
||||||
export class OpenMobileInspectionDto {
|
export class OpenMobileInspectionDto {
|
||||||
|
@IsUUID('4')
|
||||||
|
departmentId!: string;
|
||||||
|
|
||||||
@IsUUID('4')
|
@IsUUID('4')
|
||||||
operationalAreaId!: string;
|
operationalAreaId!: string;
|
||||||
|
|
||||||
|
@IsUUID('4')
|
||||||
|
scopeAssetId!: string;
|
||||||
|
|
||||||
@IsUUID('4')
|
@IsUUID('4')
|
||||||
operatorCompanyId!: string;
|
operatorCompanyId!: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,67 @@ export class InspectionPlanningHierarchyService {
|
|||||||
return { data };
|
return { data };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async operatorsForYacimiento(yacimientoId: string): Promise<{ data: InspectionPlanningHierarchyItem[] }> {
|
||||||
|
await this.requireType(yacimientoId, 'yacimiento', 'El Yacimiento seleccionado no es válido');
|
||||||
|
const data = await this.dataSource.query(`
|
||||||
|
SELECT company.id, company.code, COALESCE(profile.legal_name,company.name) AS name
|
||||||
|
FROM assets yacimiento
|
||||||
|
INNER JOIN assets company ON company.id=yacimiento.operator_company_id
|
||||||
|
INNER JOIN asset_types company_type ON company_type.id=company.asset_type_id
|
||||||
|
LEFT JOIN organization_profiles profile ON profile.asset_id=company.id
|
||||||
|
WHERE yacimiento.id=$1::uuid
|
||||||
|
AND yacimiento.information_status<>'INACTIVE'
|
||||||
|
AND company_type.operational_role='COMPANY'
|
||||||
|
AND company_type.is_active=true
|
||||||
|
AND company.information_status<>'INACTIVE'
|
||||||
|
ORDER BY name, company.code
|
||||||
|
`, [yacimientoId]) as InspectionPlanningHierarchyItem[];
|
||||||
|
return { data };
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateMobileSelection(
|
||||||
|
departmentId: string,
|
||||||
|
areaId: string,
|
||||||
|
yacimientoId: string,
|
||||||
|
companyId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const [row] = await this.dataSource.query(`
|
||||||
|
SELECT 1
|
||||||
|
FROM assets department
|
||||||
|
INNER JOIN asset_types department_type ON department_type.id=department.asset_type_id
|
||||||
|
INNER JOIN assets area ON area.parent_id=department.id
|
||||||
|
INNER JOIN asset_types area_type ON area_type.id=area.asset_type_id
|
||||||
|
INNER JOIN assets yacimiento ON yacimiento.parent_id=area.id
|
||||||
|
INNER JOIN asset_types yacimiento_type ON yacimiento_type.id=yacimiento.asset_type_id
|
||||||
|
INNER JOIN assets company ON company.id=yacimiento.operator_company_id
|
||||||
|
INNER JOIN asset_types company_type ON company_type.id=company.asset_type_id
|
||||||
|
WHERE department.id=$1::uuid
|
||||||
|
AND area.id=$2::uuid
|
||||||
|
AND yacimiento.id=$3::uuid
|
||||||
|
AND company.id=$4::uuid
|
||||||
|
AND lower(department_type.code)='departamento'
|
||||||
|
AND lower(area_type.code)='area'
|
||||||
|
AND lower(yacimiento_type.code)='yacimiento'
|
||||||
|
AND department_type.is_active=true
|
||||||
|
AND area_type.is_active=true
|
||||||
|
AND yacimiento_type.is_active=true
|
||||||
|
AND company_type.operational_role='COMPANY'
|
||||||
|
AND company_type.is_active=true
|
||||||
|
AND department.information_status<>'INACTIVE'
|
||||||
|
AND area.information_status<>'INACTIVE'
|
||||||
|
AND yacimiento.information_status<>'INACTIVE'
|
||||||
|
AND company.information_status<>'INACTIVE'
|
||||||
|
AND yacimiento.concession_type_id IS NOT NULL
|
||||||
|
LIMIT 1
|
||||||
|
`, [departmentId, areaId, yacimientoId, companyId]) as Array<{ '?column?': number }>;
|
||||||
|
if (!row) {
|
||||||
|
throw new BadRequestException({
|
||||||
|
code: 'INSPECTION_MOBILE_CONTEXT_INVALID',
|
||||||
|
message: 'La selección debe respetar Departamento → Área → Yacimiento → Operadora',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async operatorsForArea(
|
async operatorsForArea(
|
||||||
areaId: string,
|
areaId: string,
|
||||||
at?: string,
|
at?: string,
|
||||||
|
|||||||
@@ -87,21 +87,41 @@ export class InspectionVisitsController {
|
|||||||
return this.planningHierarchy.operatorsForArea(areaId, at);
|
return this.planningHierarchy.operatorsForArea(areaId, at);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('mobile/planning-context/areas')
|
@Get('mobile/planning-context/departments')
|
||||||
@RequirePermissions('inspections.execute')
|
@RequirePermissions('inspections.execute')
|
||||||
mobilePlanningAreas(@CurrentAuth() principal: AuthPrincipal) {
|
mobilePlanningDepartments(@CurrentAuth() principal: AuthPrincipal) {
|
||||||
assertMobileInspector(principal);
|
assertMobileInspector(principal);
|
||||||
return this.visits.listPlanningAreas();
|
return this.planningHierarchy.departments();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('mobile/planning-context/areas/:areaId/operators')
|
@Get('mobile/planning-context/departments/:departmentId/areas')
|
||||||
@RequirePermissions('inspections.execute')
|
@RequirePermissions('inspections.execute')
|
||||||
mobilePlanningOperators(
|
mobilePlanningAreasForDepartment(
|
||||||
|
@Param('departmentId', new ParseUUIDPipe({ version: '4' })) departmentId: string,
|
||||||
|
@CurrentAuth() principal: AuthPrincipal,
|
||||||
|
) {
|
||||||
|
assertMobileInspector(principal);
|
||||||
|
return this.planningHierarchy.areasForDepartment(departmentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('mobile/planning-context/areas/:areaId/yacimientos')
|
||||||
|
@RequirePermissions('inspections.execute')
|
||||||
|
mobilePlanningYacimientos(
|
||||||
@Param('areaId', new ParseUUIDPipe({ version: '4' })) areaId: string,
|
@Param('areaId', new ParseUUIDPipe({ version: '4' })) areaId: string,
|
||||||
@CurrentAuth() principal: AuthPrincipal,
|
@CurrentAuth() principal: AuthPrincipal,
|
||||||
) {
|
) {
|
||||||
assertMobileInspector(principal);
|
assertMobileInspector(principal);
|
||||||
return this.visits.listPlanningOperators(areaId);
|
return this.planningHierarchy.yacimientosForArea(areaId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('mobile/planning-context/yacimientos/:yacimientoId/operators')
|
||||||
|
@RequirePermissions('inspections.execute')
|
||||||
|
mobilePlanningOperatorsForYacimiento(
|
||||||
|
@Param('yacimientoId', new ParseUUIDPipe({ version: '4' })) yacimientoId: string,
|
||||||
|
@CurrentAuth() principal: AuthPrincipal,
|
||||||
|
) {
|
||||||
|
assertMobileInspector(principal);
|
||||||
|
return this.planningHierarchy.operatorsForYacimiento(yacimientoId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('mobile/open')
|
@Post('mobile/open')
|
||||||
@@ -112,10 +132,15 @@ export class InspectionVisitsController {
|
|||||||
@Req() request: RequestWithContext,
|
@Req() request: RequestWithContext,
|
||||||
) {
|
) {
|
||||||
assertMobileInspector(principal);
|
assertMobileInspector(principal);
|
||||||
// Android conserva por compatibilidad el alcance a nivel Área hasta que su
|
await this.planningHierarchy.validateMobileSelection(
|
||||||
// flujo también solicite Yacimiento. No se mezcla con la creación WEB F6.1.
|
dto.departmentId,
|
||||||
const created = await this.visits.create({
|
dto.operationalAreaId,
|
||||||
|
dto.scopeAssetId,
|
||||||
|
dto.operatorCompanyId,
|
||||||
|
);
|
||||||
|
const created = await this.planningCreate.create({
|
||||||
operationalAreaId: dto.operationalAreaId,
|
operationalAreaId: dto.operationalAreaId,
|
||||||
|
scopeAssetId: dto.scopeAssetId,
|
||||||
operatorCompanyId: dto.operatorCompanyId,
|
operatorCompanyId: dto.operatorCompanyId,
|
||||||
plannedStartAt: new Date().toISOString(),
|
plannedStartAt: new Date().toISOString(),
|
||||||
leadInspectorUserId: principal.userId,
|
leadInspectorUserId: principal.userId,
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export const API_VERSION = '0.29.0-13';
|
export const API_VERSION = '0.29.0-14';
|
||||||
export const API_PHASE = 'F6.12';
|
export const API_PHASE = 'F6.14';
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { readFileSync } from 'node:fs';
|
|||||||
import { resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
import { API_PHASE, API_VERSION } from '../../src/version';
|
import { API_PHASE, API_VERSION } from '../../src/version';
|
||||||
|
|
||||||
test('health metadata reports the current F6.12 release', () => {
|
test('health metadata reports the current F6.14 release', () => {
|
||||||
assert.equal(API_PHASE, 'F6.12');
|
assert.equal(API_PHASE, 'F6.14');
|
||||||
const pkg = JSON.parse(readFileSync(resolve(process.cwd(), 'package.json'), 'utf8')) as { version: string };
|
const pkg = JSON.parse(readFileSync(resolve(process.cwd(), 'package.json'), 'utf8')) as { version: string };
|
||||||
assert.equal(API_VERSION, pkg.version);
|
assert.equal(API_VERSION, pkg.version);
|
||||||
assert.equal(API_VERSION, '0.29.0-13');
|
assert.equal(API_VERSION, '0.29.0-14');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ function mountedRepoFile(path: string): string {
|
|||||||
test('F6.3 Android test cut targets production API and has a distinct installable debug version', () => {
|
test('F6.3 Android test cut targets production API and has a distinct installable debug version', () => {
|
||||||
const gradle = mountedRepoFile('android-app/app/build.gradle.kts');
|
const gradle = mountedRepoFile('android-app/app/build.gradle.kts');
|
||||||
|
|
||||||
assert.match(gradle, /versionCode = 40/);
|
assert.match(gradle, /versionCode = 41/);
|
||||||
assert.match(gradle, /versionName = "0\.19\.12"/);
|
assert.match(gradle, /versionName = "0\.19\.13"/);
|
||||||
assert.match(gradle, /https:\/\/dhv2\.korexlabs\.com\/api\/v3\//);
|
assert.match(gradle, /https:\/\/dhv2\.korexlabs\.com\/api\/v3\//);
|
||||||
assert.match(gradle, /applicationIdSuffix = "\.debug"/);
|
assert.match(gradle, /applicationIdSuffix = "\.debug"/);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,33 +36,48 @@ test('F6.1 uses contextual type catalog for Yacimiento and keeps OTROS/add-anoth
|
|||||||
assert.match(androidFinding, /También podés registrar otro Hallazgo sobre el mismo Inventario/);
|
assert.match(androidFinding, /También podés registrar otro Hallazgo sobre el mismo Inventario/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('F6.1 exposes mobile planning context and opens an inspection self-assigned to the current inspector', () => {
|
test('F6.14 mobile planning requires Departamento → Área → Yacimiento → Operadora before opening', () => {
|
||||||
const controller = source('src/inspection-visits/inspection-visits.controller.ts');
|
const controller = source('src/inspection-visits/inspection-visits.controller.ts');
|
||||||
|
const hierarchy = source('src/inspection-visits/inspection-planning-hierarchy.service.ts');
|
||||||
const dto = source('src/inspection-visits/dto/open-mobile-inspection.dto.ts');
|
const dto = source('src/inspection-visits/dto/open-mobile-inspection.dto.ts');
|
||||||
|
|
||||||
assert.match(controller, /@Get\('mobile\/planning-context\/areas'\)/);
|
assert.match(controller, /@Get\('mobile\/planning-context\/departments'\)/);
|
||||||
assert.match(controller, /@Get\('mobile\/planning-context\/areas\/:areaId\/operators'\)/);
|
assert.match(controller, /@Get\('mobile\/planning-context\/departments\/:departmentId\/areas'\)/);
|
||||||
|
assert.match(controller, /@Get\('mobile\/planning-context\/areas\/:areaId\/yacimientos'\)/);
|
||||||
|
assert.match(controller, /@Get\('mobile\/planning-context\/yacimientos\/:yacimientoId\/operators'\)/);
|
||||||
assert.match(controller, /@Post\('mobile\/open'\)/);
|
assert.match(controller, /@Post\('mobile\/open'\)/);
|
||||||
assert.match(controller, /@RequirePermissions\('inspections\.execute'\)/);
|
assert.match(controller, /validateMobileSelection/);
|
||||||
assert.match(controller, /assertMobileInspector\(principal\)/);
|
assert.match(controller, /scopeAssetId: dto\.scopeAssetId/);
|
||||||
assert.match(controller, /leadInspectorUserId: principal\.userId/);
|
assert.match(controller, /leadInspectorUserId: principal\.userId/);
|
||||||
assert.match(controller, /plannedStartAt: new Date\(\)\.toISOString\(\)/);
|
|
||||||
assert.match(controller, /this\.lifecycle\.plan\(created\.id/);
|
assert.match(controller, /this\.lifecycle\.plan\(created\.id/);
|
||||||
assert.match(controller, /this\.lifecycle\.start\(created\.id/);
|
assert.match(controller, /this\.lifecycle\.start\(created\.id/);
|
||||||
|
assert.match(hierarchy, /Departamento → Área → Yacimiento → Operadora/);
|
||||||
|
assert.match(dto, /departmentId!: string/);
|
||||||
assert.match(dto, /operationalAreaId!: string/);
|
assert.match(dto, /operationalAreaId!: string/);
|
||||||
|
assert.match(dto, /scopeAssetId!: string/);
|
||||||
assert.match(dto, /operatorCompanyId!: string/);
|
assert.match(dto, /operatorCompanyId!: string/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('F6.1 Android lets the inspector choose Area and current Operator and open the inspection now', () => {
|
test('F6.14 Android selects the full territorial chain and enters Actas for an active inspection', () => {
|
||||||
const client = source('../android-app/app/src/main/java/com/korexlabs/dhinspeccion/data/MobileInspectionOpen.kt');
|
const client = source('../android-app/app/src/main/java/com/korexlabs/dhinspeccion/data/MobileInspectionOpen.kt');
|
||||||
const home = source('../android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/MobileHomeScreen.kt');
|
const home = source('../android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/MobileHomeScreen.kt');
|
||||||
|
const root = source('../android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/ModernVisitRoot.kt');
|
||||||
|
const acts = source('../android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/ModernMobileActsScreen.kt');
|
||||||
const gate = source('../android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/LoginGate.kt');
|
const gate = source('../android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/LoginGate.kt');
|
||||||
|
|
||||||
assert.match(client, /inspection-visits\/mobile\/planning-context\/areas/);
|
assert.match(client, /planning-context\/departments/);
|
||||||
assert.match(client, /inspection-visits\/mobile\/open/);
|
assert.match(client, /departments\/\{departmentId\}\/areas/);
|
||||||
assert.match(home, /Abrir inspección/);
|
assert.match(client, /areas\/\{areaId\}\/yacimientos/);
|
||||||
assert.match(home, /Abrir inspección ahora/);
|
assert.match(client, /yacimientos\/\{yacimientoId\}\/operators/);
|
||||||
assert.match(home, /Operadora vigente/);
|
assert.match(client, /OpenMobileInspectionRequest\(departmentId, areaId, yacimientoId, companyId\)/);
|
||||||
|
assert.match(home, /1\. Departamento/);
|
||||||
|
assert.match(home, /2\. Área/);
|
||||||
|
assert.match(home, /3\. Yacimiento/);
|
||||||
|
assert.match(home, /4\. Operadora/);
|
||||||
|
assert.match(home, /Abrir inspección y continuar al Acta/);
|
||||||
assert.match(home, /model\.openVisit\(opened\.id\)/);
|
assert.match(home, /model\.openVisit\(opened\.id\)/);
|
||||||
|
assert.match(root, /if \(visit\.status == "IN_PROGRESS"\) ModernVisitScreen\.ACTS/);
|
||||||
|
assert.match(root, /Buscar Yacimiento, Instalación o Subinstalación/);
|
||||||
|
assert.match(acts, /Elegí el Yacimiento, una Instalación o una Subinstalación/);
|
||||||
assert.match(gate, /else -> MobileHomeScreen\(model\)/);
|
assert.match(gate, /else -> MobileHomeScreen\(model\)/);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# F6.14 · Apertura móvil por Yacimiento
|
||||||
|
|
||||||
|
La APK abre nuevas Inspecciones únicamente después de seleccionar el contexto completo:
|
||||||
|
Departamento → Área → Yacimiento → Operadora.
|
||||||
|
|
||||||
|
El Yacimiento seleccionado se persiste como `scopeAssetId` de la Inspección. La API valida toda la cadena territorial y que la Operadora corresponda al Yacimiento antes de crear e iniciar la Inspección.
|
||||||
|
|
||||||
|
Una Inspección en curso entra directamente al espacio de Actas. Los Hallazgos pueden registrarse únicamente sobre el Yacimiento de alcance, sus Instalaciones o sus Subinstalaciones. Ningún elemento fuera de esa rama puede incorporarse al trabajo de campo.
|
||||||
Reference in New Issue
Block a user