feat(inventory): add function catalog and act data
DH V2 CI / WEB · typecheck, build (push) Successful in 45s
Production dependency audit / API · production dependencies (push) Successful in 27s
DH V2 CI / API · typecheck, tests, build (push) Successful in 1m15s
Production dependency audit / WEB · production dependencies (push) Successful in 15s
DH V2 CI / Docker / migrations / production images (push) Successful in 2m21s
DH V2 CI / Promote verified main to deploy (push) Successful in 9s
Android CI / RC / Android · lint, tests, debug APK, release compile (push) Failing after 6m25s
DH V2 CI / WEB · typecheck, build (push) Successful in 45s
Production dependency audit / API · production dependencies (push) Successful in 27s
DH V2 CI / API · typecheck, tests, build (push) Successful in 1m15s
Production dependency audit / WEB · production dependencies (push) Successful in 15s
DH V2 CI / Docker / migrations / production images (push) Successful in 2m21s
DH V2 CI / Promote verified main to deploy (push) Successful in 9s
Android CI / RC / Android · lint, tests, debug APK, release compile (push) Failing after 6m25s
This commit is contained in:
@@ -14,8 +14,8 @@ android {
|
||||
applicationId = "com.korexlabs.dhinspeccion"
|
||||
minSdk = 26
|
||||
targetSdk = 36
|
||||
versionCode = 41
|
||||
versionName = "0.19.13"
|
||||
versionCode = 42
|
||||
versionName = "0.19.14"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
|
||||
@@ -19,6 +19,7 @@ import com.korexlabs.dhinspeccion.data.FieldFindingOptionsResponse
|
||||
import com.korexlabs.dhinspeccion.data.FieldFindingsRepository
|
||||
import com.korexlabs.dhinspeccion.data.FieldInventoryItem
|
||||
import com.korexlabs.dhinspeccion.data.FieldType
|
||||
import com.korexlabs.dhinspeccion.data.InventoryFunctionOption
|
||||
import com.korexlabs.dhinspeccion.data.MobileActClosure
|
||||
import com.korexlabs.dhinspeccion.data.MobileActClosureHeader
|
||||
import com.korexlabs.dhinspeccion.data.MobileActDetail
|
||||
@@ -72,6 +73,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private set
|
||||
var fieldTypes: List<FieldType> by mutableStateOf(emptyList())
|
||||
private set
|
||||
var inventoryFunctions: List<InventoryFunctionOption> by mutableStateOf(emptyList())
|
||||
private set
|
||||
var selectedFieldAsset: FieldAssetDetail? by mutableStateOf(null)
|
||||
private set
|
||||
|
||||
@@ -296,6 +299,12 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
}
|
||||
}
|
||||
|
||||
fun loadInventoryFunctions() {
|
||||
launchBusy {
|
||||
inventoryFunctions = repository.inventoryFunctions().data
|
||||
}
|
||||
}
|
||||
|
||||
fun loadFieldTypes(parentId: String? = null) {
|
||||
val currentVisit = visit ?: return
|
||||
launchBusy {
|
||||
@@ -372,13 +381,15 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
type: FieldType,
|
||||
parentId: String?,
|
||||
familyId: String?,
|
||||
name: String,
|
||||
name: String?,
|
||||
commonName: String?,
|
||||
attributes: Map<String, Any?>,
|
||||
latitude: Double,
|
||||
longitude: Double,
|
||||
accuracyM: Double?,
|
||||
description: String? = null,
|
||||
functionId: String? = null,
|
||||
newFunctionName: String? = null,
|
||||
) {
|
||||
val visitId = visit?.id ?: return
|
||||
if (visit?.status != "IN_PROGRESS") {
|
||||
@@ -389,6 +400,14 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
error = "Elegí el Tipo de instalación o subinstalación correspondiente."
|
||||
return
|
||||
}
|
||||
if (commonName.isNullOrBlank()) {
|
||||
error = "Completá el Nombre habitual. Es obligatorio y se utilizará en el Acta."
|
||||
return
|
||||
}
|
||||
if (functionId != null && !newFunctionName.isNullOrBlank()) {
|
||||
error = "Elegí una función del catálogo o cargá Otra, no ambas."
|
||||
return
|
||||
}
|
||||
launchBusy(mutation = true) {
|
||||
val assetId = DhRepository.newOperationId()
|
||||
val capturedAt = Instant.now().toString()
|
||||
@@ -397,8 +416,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
typeId = type.id,
|
||||
parentId = parentId,
|
||||
familyId = familyId,
|
||||
name = name.trim(),
|
||||
name = name?.trim()?.takeIf { it.isNotBlank() },
|
||||
commonName = commonName?.trim()?.takeIf { it.isNotBlank() },
|
||||
functionId = functionId,
|
||||
newFunctionName = newFunctionName?.trim()?.takeIf { it.isNotBlank() },
|
||||
description = description?.trim()?.takeIf { it.isNotBlank() },
|
||||
attributes = attributes,
|
||||
deviceLatitude = FieldCoordinates.latitude(latitude),
|
||||
@@ -410,7 +431,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
val localAsset = FieldInventoryItem(
|
||||
id = assetId,
|
||||
code = "PEND-${assetId.take(6).uppercase()}",
|
||||
name = request.name,
|
||||
name = request.name ?: request.commonName ?: "Sin denominación",
|
||||
commonName = request.commonName,
|
||||
informationStatus = "DRAFT",
|
||||
dataOrigin = "FIELD_SURVEY",
|
||||
|
||||
@@ -256,14 +256,27 @@ data class FieldAssetDetail(
|
||||
val capture: CaptureStatus = CaptureStatus(),
|
||||
)
|
||||
|
||||
data class InventoryFunctionOption(
|
||||
val id: String,
|
||||
val code: String,
|
||||
val name: String,
|
||||
val description: String? = null,
|
||||
)
|
||||
|
||||
data class InventoryFunctionListResponse(
|
||||
val data: List<InventoryFunctionOption> = emptyList(),
|
||||
)
|
||||
|
||||
data class CreateFieldInventoryRequest(
|
||||
val clientGeneratedId: String? = null,
|
||||
val typeId: String,
|
||||
val parentId: String? = null,
|
||||
val familyId: String? = null,
|
||||
val code: String? = null,
|
||||
val name: String,
|
||||
val name: String? = null,
|
||||
val commonName: String? = null,
|
||||
val functionId: String? = null,
|
||||
val newFunctionName: String? = null,
|
||||
val description: String? = null,
|
||||
val discoveryNotes: String? = null,
|
||||
val attributes: Map<String, Any?>,
|
||||
@@ -336,6 +349,11 @@ interface DhApi {
|
||||
@Query("parentId") parentId: String? = null,
|
||||
): FieldTypeResponse
|
||||
|
||||
@GET("inventory-functions")
|
||||
suspend fun inventoryFunctions(
|
||||
@Header("Authorization") authorization: String,
|
||||
): InventoryFunctionListResponse
|
||||
|
||||
@POST("inspection-visits/{visitId}/field-inventory/{assetId}/select")
|
||||
suspend fun selectFieldAsset(
|
||||
@Header("Authorization") authorization: String,
|
||||
@@ -461,6 +479,7 @@ class DhRepository(context: Context) {
|
||||
private val visitAdapter = moshi.adapter(VisitDetail::class.java)
|
||||
private val inventoryAdapter = moshi.adapter(FieldInventoryListResponse::class.java)
|
||||
private val fieldTypeAdapter = moshi.adapter(FieldTypeResponse::class.java)
|
||||
private val inventoryFunctionAdapter = moshi.adapter(InventoryFunctionListResponse::class.java)
|
||||
private val api: DhApi = Retrofit.Builder()
|
||||
.baseUrl(BuildConfig.API_BASE_URL)
|
||||
.client(OkHttpClient.Builder().build())
|
||||
@@ -521,6 +540,11 @@ class DhRepository(context: Context) {
|
||||
authorized { session -> api.fieldTypes("Bearer ${session.accessToken}", visitId, parentId) }
|
||||
}
|
||||
|
||||
suspend fun inventoryFunctions(): InventoryFunctionListResponse =
|
||||
cached("inventory-functions", inventoryFunctionAdapter) {
|
||||
authorized { session -> api.inventoryFunctions("Bearer ${session.accessToken}") }
|
||||
}
|
||||
|
||||
suspend fun selectFieldAsset(visitId: String, assetId: String) = authorized { session ->
|
||||
api.selectFieldAsset("Bearer ${session.accessToken}", visitId, assetId)
|
||||
}
|
||||
|
||||
+5
-1
@@ -205,6 +205,8 @@ class OfflineMutationQueue(private val context: Context) {
|
||||
.put("code", request.code)
|
||||
.put("name", request.name)
|
||||
.put("commonName", request.commonName)
|
||||
.put("functionId", request.functionId)
|
||||
.put("newFunctionName", request.newFunctionName)
|
||||
.put("description", request.description)
|
||||
.put("discoveryNotes", request.discoveryNotes)
|
||||
.put("attributes", JSONObject(request.attributes))
|
||||
@@ -424,8 +426,10 @@ class OfflineMutationQueue(private val context: Context) {
|
||||
parentId = payload.optNullableString("parentId"),
|
||||
familyId = payload.optNullableString("familyId"),
|
||||
code = payload.optNullableString("code"),
|
||||
name = payload.getString("name"),
|
||||
name = payload.optNullableString("name"),
|
||||
commonName = payload.optNullableString("commonName"),
|
||||
functionId = payload.optNullableString("functionId"),
|
||||
newFunctionName = payload.optNullableString("newFunctionName"),
|
||||
description = payload.optNullableString("description"),
|
||||
discoveryNotes = payload.optNullableString("discoveryNotes"),
|
||||
attributes = jsonObjectToMap(payload.getJSONObject("attributes")),
|
||||
|
||||
@@ -287,6 +287,11 @@ private fun ModernFieldInventoryScreen(model: MainViewModel, onBack: () -> Unit)
|
||||
var name by rememberSaveable(visit.id) { mutableStateOf("") }
|
||||
var commonName by rememberSaveable(visit.id) { mutableStateOf("") }
|
||||
var description by rememberSaveable(visit.id) { mutableStateOf("") }
|
||||
var selectedFunctionId by rememberSaveable(visit.id) { mutableStateOf<String?>(null) }
|
||||
var functionSearch by rememberSaveable(visit.id) { mutableStateOf("") }
|
||||
var functionExpanded by remember { mutableStateOf(false) }
|
||||
var useNewFunction by rememberSaveable(visit.id) { mutableStateOf(false) }
|
||||
var newFunctionName by rememberSaveable(visit.id) { mutableStateOf("") }
|
||||
var selectedTypeId by rememberSaveable(visit.id) { mutableStateOf<String?>(null) }
|
||||
var selectedFamilyId by rememberSaveable(visit.id) { mutableStateOf<String?>(null) }
|
||||
var familySearch by rememberSaveable(visit.id) { mutableStateOf("") }
|
||||
@@ -304,6 +309,11 @@ private fun ModernFieldInventoryScreen(model: MainViewModel, onBack: () -> Unit)
|
||||
name = ""
|
||||
commonName = ""
|
||||
description = ""
|
||||
selectedFunctionId = null
|
||||
functionSearch = ""
|
||||
functionExpanded = false
|
||||
useNewFunction = false
|
||||
newFunctionName = ""
|
||||
selectedTypeId = null
|
||||
selectedFamilyId = null
|
||||
familySearch = ""
|
||||
@@ -356,6 +366,7 @@ private fun ModernFieldInventoryScreen(model: MainViewModel, onBack: () -> Unit)
|
||||
|
||||
LaunchedEffect(visit.id) {
|
||||
model.searchInventory("", visit.scopeAsset?.id)
|
||||
model.loadInventoryFunctions()
|
||||
}
|
||||
|
||||
LaunchedEffect(modeName, parentSearch) {
|
||||
@@ -393,7 +404,7 @@ private fun ModernFieldInventoryScreen(model: MainViewModel, onBack: () -> Unit)
|
||||
|
||||
val selectedType = model.fieldTypes.firstOrNull { it.id == selectedTypeId }
|
||||
val editableAttributes = selectedType?.attributes.orEmpty().filterNot {
|
||||
selectedType?.familyRequired == true && it.code == "tipo_instalacion"
|
||||
it.code == "campo_funcion" || (selectedType?.familyRequired == true && it.code == "tipo_instalacion")
|
||||
}
|
||||
val selectedFamily = selectedType?.families?.firstOrNull { it.id == selectedFamilyId }
|
||||
val filteredFamilies = remember(selectedType, familySearch) {
|
||||
@@ -403,6 +414,14 @@ private fun ModernFieldInventoryScreen(model: MainViewModel, onBack: () -> Unit)
|
||||
}
|
||||
}
|
||||
|
||||
val selectedFunction = model.inventoryFunctions.firstOrNull { it.id == selectedFunctionId }
|
||||
val filteredFunctions = remember(model.inventoryFunctions, functionSearch) {
|
||||
val needle = modernNormalize(functionSearch)
|
||||
model.inventoryFunctions.filter { item ->
|
||||
needle.isBlank() || modernNormalize("${item.code} ${item.name}").contains(needle)
|
||||
}
|
||||
}
|
||||
|
||||
var pendingPhotoFile by remember { mutableStateOf<File?>(null) }
|
||||
var pendingPhotoGeo by remember { mutableStateOf<ModernGeoSnapshot?>(null) }
|
||||
|
||||
@@ -443,8 +462,10 @@ private fun ModernFieldInventoryScreen(model: MainViewModel, onBack: () -> Unit)
|
||||
type = type,
|
||||
parentId = effectiveParent,
|
||||
familyId = selectedFamilyId,
|
||||
name = name,
|
||||
name = name.takeIf { it.isNotBlank() },
|
||||
commonName = commonName,
|
||||
functionId = if (useNewFunction) null else selectedFunctionId,
|
||||
newFunctionName = if (useNewFunction) newFunctionName else null,
|
||||
description = description,
|
||||
attributes = buildModernAttributes(type, attributeValues),
|
||||
latitude = geo.latitude,
|
||||
@@ -823,24 +844,98 @@ private fun ModernFieldInventoryScreen(model: MainViewModel, onBack: () -> Unit)
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
label = { Text("Nombre técnico *") },
|
||||
placeholder = { Text("Denominación técnica") },
|
||||
value = commonName,
|
||||
onValueChange = { commonName = it },
|
||||
label = { Text("Nombre habitual *") },
|
||||
supportingText = { Text("Es el nombre que se mostrará en el Acta.") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
||||
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Down) }),
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = commonName,
|
||||
onValueChange = { commonName = it },
|
||||
label = { Text("Nombre habitual (opcional)") },
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
label = { Text("Nombre técnico (opcional)") },
|
||||
placeholder = { Text("Denominación técnica, si corresponde") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
||||
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Down) }),
|
||||
)
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = functionExpanded && !model.busy,
|
||||
onExpandedChange = {
|
||||
if (!model.busy) {
|
||||
functionExpanded = it
|
||||
if (it) functionSearch = ""
|
||||
}
|
||||
},
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = if (functionExpanded) functionSearch else if (useNewFunction) "Otra función" else selectedFunction?.name.orEmpty(),
|
||||
onValueChange = { functionSearch = it; functionExpanded = true },
|
||||
label = { Text("Función (opcional)") },
|
||||
placeholder = { Text("Buscar función") },
|
||||
leadingIcon = { Icon(Icons.Filled.Search, null) },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = functionExpanded) },
|
||||
enabled = !model.busy,
|
||||
modifier = Modifier.fillMaxWidth().menuAnchor(MenuAnchorType.PrimaryEditable),
|
||||
singleLine = true,
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = functionExpanded && !model.busy,
|
||||
onDismissRequest = { functionExpanded = false; functionSearch = "" },
|
||||
modifier = Modifier.heightIn(max = 280.dp),
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Sin función") },
|
||||
onClick = {
|
||||
selectedFunctionId = null
|
||||
useNewFunction = false
|
||||
newFunctionName = ""
|
||||
functionExpanded = false
|
||||
},
|
||||
)
|
||||
filteredFunctions.forEach { item ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(item.name) },
|
||||
trailingIcon = { if (!useNewFunction && selectedFunctionId == item.id) Icon(Icons.Filled.CheckCircle, "Seleccionada") },
|
||||
onClick = {
|
||||
selectedFunctionId = item.id
|
||||
useNewFunction = false
|
||||
newFunctionName = ""
|
||||
functionExpanded = false
|
||||
functionSearch = ""
|
||||
},
|
||||
)
|
||||
}
|
||||
DropdownMenuItem(
|
||||
text = { Text("Otro · agregar función") },
|
||||
trailingIcon = { if (useNewFunction) Icon(Icons.Filled.CheckCircle, "Seleccionada") },
|
||||
onClick = {
|
||||
selectedFunctionId = null
|
||||
useNewFunction = true
|
||||
functionExpanded = false
|
||||
functionSearch = ""
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
if (useNewFunction) {
|
||||
OutlinedTextField(
|
||||
value = newFunctionName,
|
||||
onValueChange = { newFunctionName = it },
|
||||
label = { Text("Nueva función *") },
|
||||
supportingText = { Text("Se incorporará al Catálogo de funciones al sincronizar.") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
||||
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Down) }),
|
||||
)
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = description,
|
||||
onValueChange = { description = it },
|
||||
@@ -896,7 +991,7 @@ private fun ModernFieldInventoryScreen(model: MainViewModel, onBack: () -> Unit)
|
||||
val familyReady = selectedType?.familyRequired != true || selectedFamilyId != null
|
||||
Button(
|
||||
onClick = { requestCreate() },
|
||||
enabled = selectedType != null && name.isNotBlank() && attributesReady && familyReady && !model.busy,
|
||||
enabled = selectedType != null && commonName.isNotBlank() && (!useNewFunction || newFunctionName.isNotBlank()) && attributesReady && familyReady && !model.busy,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Icon(Icons.Filled.CameraAlt, null)
|
||||
|
||||
@@ -8,8 +8,8 @@ class ReleaseMetadataTest {
|
||||
@Test
|
||||
fun debugBuildKeepsSeparateApplicationIdentity() {
|
||||
assertEquals("com.korexlabs.dhinspeccion.debug", BuildConfig.APPLICATION_ID)
|
||||
assertEquals(41, BuildConfig.VERSION_CODE)
|
||||
assertEquals("0.19.13-debug", BuildConfig.VERSION_NAME)
|
||||
assertEquals(42, BuildConfig.VERSION_CODE)
|
||||
assertEquals("0.19.14-debug", BuildConfig.VERSION_NAME)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user