From ffeae965302db2e23e23b411518f928e74f98982 Mon Sep 17 00:00:00 2001 From: KoreX Labs Date: Mon, 14 Sep 2026 17:58:16 -0300 Subject: [PATCH] fix(android): unify installation type in searchable dropdown --- android-app/app/build.gradle.kts | 4 +- .../dhinspeccion/ui/ModernVisitRoot.kt | 87 ++++++++++++------- .../dhinspeccion/ReleaseMetadataTest.kt | 4 +- api-v3/test/unit/f5-android-test-cut.test.ts | 4 +- 4 files changed, 62 insertions(+), 37 deletions(-) diff --git a/android-app/app/build.gradle.kts b/android-app/app/build.gradle.kts index 2a57c6d..d99b23b 100644 --- a/android-app/app/build.gradle.kts +++ b/android-app/app/build.gradle.kts @@ -12,8 +12,8 @@ android { applicationId = "com.korexlabs.dhinspeccion" minSdk = 26 targetSdk = 36 - versionCode = 31 - versionName = "0.19.3" + versionCode = 32 + versionName = "0.19.4" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true diff --git a/android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/ModernVisitRoot.kt b/android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/ModernVisitRoot.kt index 57fc48e..821f4b7 100644 --- a/android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/ModernVisitRoot.kt +++ b/android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/ModernVisitRoot.kt @@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn @@ -36,6 +37,11 @@ import androidx.compose.material.icons.filled.LocationOn import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.WarningAmber import androidx.compose.material3.Button +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuAnchorType +import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.ElevatedCard import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.HorizontalDivider @@ -259,6 +265,7 @@ private fun ModernChecklistCard(visit: VisitDetail) { } } +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun ModernFieldInventoryScreen(model: MainViewModel, onBack: () -> Unit) { val context = LocalContext.current @@ -279,6 +286,7 @@ private fun ModernFieldInventoryScreen(model: MainViewModel, onBack: () -> Unit) var selectedTypeId by rememberSaveable(visit.id) { mutableStateOf(null) } var selectedFamilyId by rememberSaveable(visit.id) { mutableStateOf(null) } var familySearch by rememberSaveable(visit.id) { mutableStateOf("") } + var familyExpanded by remember { mutableStateOf(false) } val attributeValues = remember { mutableStateMapOf() } var pendingAutoPhoto by rememberSaveable(visit.id) { mutableStateOf(false) } var localError by rememberSaveable(visit.id) { mutableStateOf(null) } @@ -295,6 +303,7 @@ private fun ModernFieldInventoryScreen(model: MainViewModel, onBack: () -> Unit) selectedTypeId = null selectedFamilyId = null familySearch = "" + familyExpanded = false attributeValues.clear() localError = null } @@ -379,6 +388,9 @@ 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" + } val selectedFamily = selectedType?.families?.firstOrNull { it.id == selectedFamilyId } val filteredFamilies = remember(selectedType, familySearch) { val needle = modernNormalize(familySearch) @@ -746,37 +758,51 @@ private fun ModernFieldInventoryScreen(model: MainViewModel, onBack: () -> Unit) } if (selectedType?.familyRequired == true) { - Text("Clasificación técnica", fontWeight = FontWeight.Bold) - OutlinedTextField( - value = familySearch, - onValueChange = { familySearch = it }, - label = { Text("Buscar clasificación") }, - leadingIcon = { Icon(Icons.Filled.Search, null) }, - supportingText = { Text("${filteredFamilies.size} opciones compatibles") }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - ) - filteredFamilies.take(12).forEach { family -> - Surface( - onClick = { selectedFamilyId = family.id }, - modifier = Modifier.fillMaxWidth(), - shape = MaterialTheme.shapes.medium, - color = if (selectedFamilyId == family.id) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surface, - border = BorderStroke(1.dp, if (selectedFamilyId == family.id) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outlineVariant), + ExposedDropdownMenuBox( + expanded = familyExpanded && !model.busy, + onExpandedChange = { + if (!model.busy) { + familyExpanded = it + familySearch = "" + } + }, + ) { + OutlinedTextField( + value = if (familyExpanded) familySearch else selectedFamily?.name.orEmpty(), + onValueChange = { familySearch = it; familyExpanded = true }, + label = { Text(if (isSubinstallation) "Tipo de subinstalación" else "Tipo de instalación") }, + placeholder = { Text("Buscar por nombre o código") }, + leadingIcon = { Icon(Icons.Filled.Search, null) }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = familyExpanded) }, + enabled = !model.busy, + modifier = Modifier.fillMaxWidth().menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable), + singleLine = true, + ) + ExposedDropdownMenu( + expanded = familyExpanded && !model.busy, + onDismissRequest = { familyExpanded = false; familySearch = "" }, + modifier = Modifier.heightIn(max = 280.dp), ) { - Row(Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) { - Text( - (if (family.isOther) "Otro · " else "") + family.name, - Modifier.weight(1f), - fontWeight = if (selectedFamilyId == family.id) FontWeight.Bold else FontWeight.Normal, + if (filteredFamilies.isEmpty()) { + DropdownMenuItem(text = { Text("Sin coincidencias") }, onClick = {}, enabled = false) + } + filteredFamilies.forEach { family -> + DropdownMenuItem( + text = { Text((if (family.isOther) "Otro · " else "") + family.name) }, + trailingIcon = { + if (selectedFamilyId == family.id) Icon(Icons.Filled.CheckCircle, "Seleccionado") + }, + onClick = { + selectedFamilyId = family.id + familyExpanded = false + familySearch = "" + keyboard?.hide() + focusManager.clearFocus() + }, ) - if (selectedFamilyId == family.id) Icon(Icons.Filled.CheckCircle, null, tint = MaterialTheme.colorScheme.primary) } } } - if (filteredFamilies.size > 12) { - Text("Seguí escribiendo para reducir la lista.", style = MaterialTheme.typography.bodySmall) - } selectedFamily?.let { family -> if (family.isOther) { Text("Quedará marcado para revisión en oficina.", color = MaterialTheme.colorScheme.secondary) @@ -811,7 +837,7 @@ private fun ModernFieldInventoryScreen(model: MainViewModel, onBack: () -> Unit) minLines = 2, ) - selectedType?.attributes?.forEach { definition -> + editableAttributes.forEach { definition -> OutlinedTextField( value = attributeValues[definition.code].orEmpty(), onValueChange = { attributeValues[definition.code] = it }, @@ -840,10 +866,9 @@ private fun ModernFieldInventoryScreen(model: MainViewModel, onBack: () -> Unit) } } - val attributesReady = selectedType?.attributes - ?.filter { it.isRequired } - ?.all { attributeValues[it.code].orEmpty().isNotBlank() } - ?: false + val attributesReady = editableAttributes + .filter { it.isRequired } + .all { attributeValues[it.code].orEmpty().isNotBlank() } val familyReady = selectedType?.familyRequired != true || selectedFamilyId != null Button( onClick = { requestCreate() }, diff --git a/android-app/app/src/test/java/com/korexlabs/dhinspeccion/ReleaseMetadataTest.kt b/android-app/app/src/test/java/com/korexlabs/dhinspeccion/ReleaseMetadataTest.kt index 8e263e2..32ce11a 100644 --- a/android-app/app/src/test/java/com/korexlabs/dhinspeccion/ReleaseMetadataTest.kt +++ b/android-app/app/src/test/java/com/korexlabs/dhinspeccion/ReleaseMetadataTest.kt @@ -8,8 +8,8 @@ class ReleaseMetadataTest { @Test fun debugBuildKeepsSeparateApplicationIdentity() { assertEquals("com.korexlabs.dhinspeccion.debug", BuildConfig.APPLICATION_ID) - assertEquals(31, BuildConfig.VERSION_CODE) - assertEquals("0.19.3-debug", BuildConfig.VERSION_NAME) + assertEquals(32, BuildConfig.VERSION_CODE) + assertEquals("0.19.4-debug", BuildConfig.VERSION_NAME) } @Test diff --git a/api-v3/test/unit/f5-android-test-cut.test.ts b/api-v3/test/unit/f5-android-test-cut.test.ts index 0b2d424..0df88fa 100644 --- a/api-v3/test/unit/f5-android-test-cut.test.ts +++ b/api-v3/test/unit/f5-android-test-cut.test.ts @@ -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 = 31/); - assert.match(gradle, /versionName = "0\.19\.3"/); + assert.match(gradle, /versionCode = 32/); + assert.match(gradle, /versionName = "0\.19\.4"/); assert.match(gradle, /https:\/\/dhv2\.korexlabs\.com\/api\/v3\//); assert.match(gradle, /applicationIdSuffix = "\.debug"/); });