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
|
||||
# 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:
|
||||
push:
|
||||
@@ -111,7 +111,7 @@ jobs:
|
||||
cp android-app/app/build/outputs/apk/debug/app-debug.apk "$apk"
|
||||
sha256sum "$apk" > "${apk}.sha256"
|
||||
{
|
||||
echo "phase=F6.13"
|
||||
echo "phase=F6.14"
|
||||
echo "version=$version"
|
||||
echo "versionCode=$code"
|
||||
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
|
||||
|
||||
- `versionName`: **0.19.12**; `versionCode`: **40**.
|
||||
- API: `https://dhv2.korexlabs.com/api/v3/`; compatible con API 0.29.0-13 / WEB 0.23.0-10.
|
||||
- `versionName`: **0.19.13**; `versionCode`: **41**.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
@@ -14,8 +14,8 @@ android {
|
||||
applicationId = "com.korexlabs.dhinspeccion"
|
||||
minSdk = 26
|
||||
targetSdk = 36
|
||||
versionCode = 40
|
||||
versionName = "0.19.12"
|
||||
versionCode = 41
|
||||
versionName = "0.19.13"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
|
||||
+37
-11
@@ -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 {
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "dhv2-api",
|
||||
"version": "0.29.0-13",
|
||||
"version": "0.29.0-14",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "dhv2-api",
|
||||
"version": "0.29.0-13",
|
||||
"version": "0.29.0-14",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^11.0.0",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dhv2-api",
|
||||
"version": "0.29.0-13",
|
||||
"version": "0.29.0-14",
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { IsUUID } from 'class-validator';
|
||||
|
||||
export class OpenMobileInspectionDto {
|
||||
@IsUUID('4')
|
||||
departmentId!: string;
|
||||
|
||||
@IsUUID('4')
|
||||
operationalAreaId!: string;
|
||||
|
||||
@IsUUID('4')
|
||||
scopeAssetId!: string;
|
||||
|
||||
@IsUUID('4')
|
||||
operatorCompanyId!: string;
|
||||
}
|
||||
|
||||
@@ -62,6 +62,67 @@ export class InspectionPlanningHierarchyService {
|
||||
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(
|
||||
areaId: string,
|
||||
at?: string,
|
||||
|
||||
@@ -87,21 +87,41 @@ export class InspectionVisitsController {
|
||||
return this.planningHierarchy.operatorsForArea(areaId, at);
|
||||
}
|
||||
|
||||
@Get('mobile/planning-context/areas')
|
||||
@Get('mobile/planning-context/departments')
|
||||
@RequirePermissions('inspections.execute')
|
||||
mobilePlanningAreas(@CurrentAuth() principal: AuthPrincipal) {
|
||||
mobilePlanningDepartments(@CurrentAuth() principal: AuthPrincipal) {
|
||||
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')
|
||||
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,
|
||||
@CurrentAuth() principal: AuthPrincipal,
|
||||
) {
|
||||
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')
|
||||
@@ -112,10 +132,15 @@ export class InspectionVisitsController {
|
||||
@Req() request: RequestWithContext,
|
||||
) {
|
||||
assertMobileInspector(principal);
|
||||
// Android conserva por compatibilidad el alcance a nivel Área hasta que su
|
||||
// flujo también solicite Yacimiento. No se mezcla con la creación WEB F6.1.
|
||||
const created = await this.visits.create({
|
||||
await this.planningHierarchy.validateMobileSelection(
|
||||
dto.departmentId,
|
||||
dto.operationalAreaId,
|
||||
dto.scopeAssetId,
|
||||
dto.operatorCompanyId,
|
||||
);
|
||||
const created = await this.planningCreate.create({
|
||||
operationalAreaId: dto.operationalAreaId,
|
||||
scopeAssetId: dto.scopeAssetId,
|
||||
operatorCompanyId: dto.operatorCompanyId,
|
||||
plannedStartAt: new Date().toISOString(),
|
||||
leadInspectorUserId: principal.userId,
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export const API_VERSION = '0.29.0-13';
|
||||
export const API_PHASE = 'F6.12';
|
||||
export const API_VERSION = '0.29.0-14';
|
||||
export const API_PHASE = 'F6.14';
|
||||
|
||||
@@ -4,9 +4,9 @@ import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { API_PHASE, API_VERSION } from '../../src/version';
|
||||
|
||||
test('health metadata reports the current F6.12 release', () => {
|
||||
assert.equal(API_PHASE, 'F6.12');
|
||||
test('health metadata reports the current F6.14 release', () => {
|
||||
assert.equal(API_PHASE, 'F6.14');
|
||||
const pkg = JSON.parse(readFileSync(resolve(process.cwd(), 'package.json'), 'utf8')) as { version: string };
|
||||
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', () => {
|
||||
const gradle = mountedRepoFile('android-app/app/build.gradle.kts');
|
||||
|
||||
assert.match(gradle, /versionCode = 40/);
|
||||
assert.match(gradle, /versionName = "0\.19\.12"/);
|
||||
assert.match(gradle, /versionCode = 41/);
|
||||
assert.match(gradle, /versionName = "0\.19\.13"/);
|
||||
assert.match(gradle, /https:\/\/dhv2\.korexlabs\.com\/api\/v3\//);
|
||||
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/);
|
||||
});
|
||||
|
||||
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 hierarchy = source('src/inspection-visits/inspection-planning-hierarchy.service.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\/areas\/:areaId\/operators'\)/);
|
||||
assert.match(controller, /@Get\('mobile\/planning-context\/departments'\)/);
|
||||
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, /@RequirePermissions\('inspections\.execute'\)/);
|
||||
assert.match(controller, /assertMobileInspector\(principal\)/);
|
||||
assert.match(controller, /validateMobileSelection/);
|
||||
assert.match(controller, /scopeAssetId: dto\.scopeAssetId/);
|
||||
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\.start\(created\.id/);
|
||||
assert.match(hierarchy, /Departamento → Área → Yacimiento → Operadora/);
|
||||
assert.match(dto, /departmentId!: string/);
|
||||
assert.match(dto, /operationalAreaId!: string/);
|
||||
assert.match(dto, /scopeAssetId!: 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 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');
|
||||
|
||||
assert.match(client, /inspection-visits\/mobile\/planning-context\/areas/);
|
||||
assert.match(client, /inspection-visits\/mobile\/open/);
|
||||
assert.match(home, /Abrir inspección/);
|
||||
assert.match(home, /Abrir inspección ahora/);
|
||||
assert.match(home, /Operadora vigente/);
|
||||
assert.match(client, /planning-context\/departments/);
|
||||
assert.match(client, /departments\/\{departmentId\}\/areas/);
|
||||
assert.match(client, /areas\/\{areaId\}\/yacimientos/);
|
||||
assert.match(client, /yacimientos\/\{yacimientoId\}\/operators/);
|
||||
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(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\)/);
|
||||
});
|
||||
|
||||
@@ -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