Compare commits

...
Author SHA1 Message Date
admin 975f9ee13e fix(android): match dropdown anchor to installed Material version
Android CI / RC / Android · lint, tests, debug APK, release compile (pull_request) Successful in 3m5s
DH V2 CI / API · typecheck, tests, build (pull_request) Successful in 36s
DH V2 CI / WEB · typecheck, build (pull_request) Successful in 24s
Production dependency audit / API · production dependencies (pull_request) Successful in 16s
Production dependency audit / WEB · production dependencies (pull_request) Successful in 15s
DH V2 CI / Docker / scripts contract (pull_request) Successful in 1m45s
2026-09-14 18:02:11 -03:00
admin 39dcd6bd4a fix(ci): skip unavailable legacy Android tools package
Android CI / RC / Android · lint, tests, debug APK, release compile (pull_request) Failing after 1m4s
DH V2 CI / API · typecheck, tests, build (pull_request) Successful in 37s
DH V2 CI / Docker / scripts contract (pull_request) Canceled after 0s
Production dependency audit / API · production dependencies (pull_request) Canceled after 0s
DH V2 CI / WEB · typecheck, build (pull_request) Canceled after 22s
Production dependency audit / WEB · production dependencies (pull_request) Canceled after 0s
2026-09-14 17:59:57 -03:00
admin ffeae96530 fix(android): unify installation type in searchable dropdown
Android CI / RC / Android · lint, tests, debug APK, release compile (pull_request) Failing after 22s
DH V2 CI / API · typecheck, tests, build (pull_request) Successful in 36s
DH V2 CI / WEB · typecheck, build (pull_request) Successful in 22s
Production dependency audit / API · production dependencies (pull_request) Successful in 15s
DH V2 CI / Docker / scripts contract (pull_request) Canceled after 0s
Production dependency audit / WEB · production dependencies (pull_request) Canceled after 14s
2026-09-14 17:58:16 -03:00
5 changed files with 64 additions and 37 deletions
+2
View File
@@ -43,6 +43,8 @@ jobs:
- name: Android SDK
uses: android-actions/setup-android@v3
with:
packages: platform-tools
- name: Android API 36
run: sdkmanager 'platforms;android-36' 'build-tools;36.0.0'
+2 -2
View File
@@ -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
@@ -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.MenuAnchorType
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<String?>(null) }
var selectedFamilyId by rememberSaveable(visit.id) { mutableStateOf<String?>(null) }
var familySearch by rememberSaveable(visit.id) { mutableStateOf("") }
var familyExpanded by remember { mutableStateOf(false) }
val attributeValues = remember { mutableStateMapOf<String, String>() }
var pendingAutoPhoto by rememberSaveable(visit.id) { mutableStateOf(false) }
var localError by rememberSaveable(visit.id) { mutableStateOf<String?>(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(MenuAnchorType.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() },
@@ -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
+2 -2
View File
@@ -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"/);
});