Android 0.19.4: tipo de instalación único con desplegable y buscador #2
@@ -12,8 +12,8 @@ android {
|
|||||||
applicationId = "com.korexlabs.dhinspeccion"
|
applicationId = "com.korexlabs.dhinspeccion"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 31
|
versionCode = 32
|
||||||
versionName = "0.19.3"
|
versionName = "0.19.4"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.Spacer
|
|||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
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.Search
|
||||||
import androidx.compose.material.icons.filled.WarningAmber
|
import androidx.compose.material.icons.filled.WarningAmber
|
||||||
import androidx.compose.material3.Button
|
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.ElevatedCard
|
||||||
import androidx.compose.material3.FilledTonalButton
|
import androidx.compose.material3.FilledTonalButton
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
@@ -259,6 +265,7 @@ private fun ModernChecklistCard(visit: VisitDetail) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun ModernFieldInventoryScreen(model: MainViewModel, onBack: () -> Unit) {
|
private fun ModernFieldInventoryScreen(model: MainViewModel, onBack: () -> Unit) {
|
||||||
val context = LocalContext.current
|
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 selectedTypeId by rememberSaveable(visit.id) { mutableStateOf<String?>(null) }
|
||||||
var selectedFamilyId by rememberSaveable(visit.id) { mutableStateOf<String?>(null) }
|
var selectedFamilyId by rememberSaveable(visit.id) { mutableStateOf<String?>(null) }
|
||||||
var familySearch by rememberSaveable(visit.id) { mutableStateOf("") }
|
var familySearch by rememberSaveable(visit.id) { mutableStateOf("") }
|
||||||
|
var familyExpanded by remember { mutableStateOf(false) }
|
||||||
val attributeValues = remember { mutableStateMapOf<String, String>() }
|
val attributeValues = remember { mutableStateMapOf<String, String>() }
|
||||||
var pendingAutoPhoto by rememberSaveable(visit.id) { mutableStateOf(false) }
|
var pendingAutoPhoto by rememberSaveable(visit.id) { mutableStateOf(false) }
|
||||||
var localError by rememberSaveable(visit.id) { mutableStateOf<String?>(null) }
|
var localError by rememberSaveable(visit.id) { mutableStateOf<String?>(null) }
|
||||||
@@ -295,6 +303,7 @@ private fun ModernFieldInventoryScreen(model: MainViewModel, onBack: () -> Unit)
|
|||||||
selectedTypeId = null
|
selectedTypeId = null
|
||||||
selectedFamilyId = null
|
selectedFamilyId = null
|
||||||
familySearch = ""
|
familySearch = ""
|
||||||
|
familyExpanded = false
|
||||||
attributeValues.clear()
|
attributeValues.clear()
|
||||||
localError = null
|
localError = null
|
||||||
}
|
}
|
||||||
@@ -379,6 +388,9 @@ private fun ModernFieldInventoryScreen(model: MainViewModel, onBack: () -> Unit)
|
|||||||
}
|
}
|
||||||
|
|
||||||
val selectedType = model.fieldTypes.firstOrNull { it.id == selectedTypeId }
|
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 selectedFamily = selectedType?.families?.firstOrNull { it.id == selectedFamilyId }
|
||||||
val filteredFamilies = remember(selectedType, familySearch) {
|
val filteredFamilies = remember(selectedType, familySearch) {
|
||||||
val needle = modernNormalize(familySearch)
|
val needle = modernNormalize(familySearch)
|
||||||
@@ -746,37 +758,51 @@ private fun ModernFieldInventoryScreen(model: MainViewModel, onBack: () -> Unit)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (selectedType?.familyRequired == true) {
|
if (selectedType?.familyRequired == true) {
|
||||||
Text("Clasificación técnica", fontWeight = FontWeight.Bold)
|
ExposedDropdownMenuBox(
|
||||||
|
expanded = familyExpanded && !model.busy,
|
||||||
|
onExpandedChange = {
|
||||||
|
if (!model.busy) {
|
||||||
|
familyExpanded = it
|
||||||
|
familySearch = ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = familySearch,
|
value = if (familyExpanded) familySearch else selectedFamily?.name.orEmpty(),
|
||||||
onValueChange = { familySearch = it },
|
onValueChange = { familySearch = it; familyExpanded = true },
|
||||||
label = { Text("Buscar clasificación") },
|
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) },
|
leadingIcon = { Icon(Icons.Filled.Search, null) },
|
||||||
supportingText = { Text("${filteredFamilies.size} opciones compatibles") },
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = familyExpanded) },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
enabled = !model.busy,
|
||||||
|
modifier = Modifier.fillMaxWidth().menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable),
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
)
|
)
|
||||||
filteredFamilies.take(12).forEach { family ->
|
ExposedDropdownMenu(
|
||||||
Surface(
|
expanded = familyExpanded && !model.busy,
|
||||||
onClick = { selectedFamilyId = family.id },
|
onDismissRequest = { familyExpanded = false; familySearch = "" },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.heightIn(max = 280.dp),
|
||||||
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),
|
|
||||||
) {
|
) {
|
||||||
Row(Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
if (filteredFamilies.isEmpty()) {
|
||||||
Text(
|
DropdownMenuItem(text = { Text("Sin coincidencias") }, onClick = {}, enabled = false)
|
||||||
(if (family.isOther) "Otro · " else "") + family.name,
|
}
|
||||||
Modifier.weight(1f),
|
filteredFamilies.forEach { family ->
|
||||||
fontWeight = if (selectedFamilyId == family.id) FontWeight.Bold else FontWeight.Normal,
|
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 ->
|
selectedFamily?.let { family ->
|
||||||
if (family.isOther) {
|
if (family.isOther) {
|
||||||
Text("Quedará marcado para revisión en oficina.", color = MaterialTheme.colorScheme.secondary)
|
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,
|
minLines = 2,
|
||||||
)
|
)
|
||||||
|
|
||||||
selectedType?.attributes?.forEach { definition ->
|
editableAttributes.forEach { definition ->
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = attributeValues[definition.code].orEmpty(),
|
value = attributeValues[definition.code].orEmpty(),
|
||||||
onValueChange = { attributeValues[definition.code] = it },
|
onValueChange = { attributeValues[definition.code] = it },
|
||||||
@@ -840,10 +866,9 @@ private fun ModernFieldInventoryScreen(model: MainViewModel, onBack: () -> Unit)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val attributesReady = selectedType?.attributes
|
val attributesReady = editableAttributes
|
||||||
?.filter { it.isRequired }
|
.filter { it.isRequired }
|
||||||
?.all { attributeValues[it.code].orEmpty().isNotBlank() }
|
.all { attributeValues[it.code].orEmpty().isNotBlank() }
|
||||||
?: false
|
|
||||||
val familyReady = selectedType?.familyRequired != true || selectedFamilyId != null
|
val familyReady = selectedType?.familyRequired != true || selectedFamilyId != null
|
||||||
Button(
|
Button(
|
||||||
onClick = { requestCreate() },
|
onClick = { requestCreate() },
|
||||||
|
|||||||
@@ -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(31, BuildConfig.VERSION_CODE)
|
assertEquals(32, BuildConfig.VERSION_CODE)
|
||||||
assertEquals("0.19.3-debug", BuildConfig.VERSION_NAME)
|
assertEquals("0.19.4-debug", BuildConfig.VERSION_NAME)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -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 = 31/);
|
assert.match(gradle, /versionCode = 32/);
|
||||||
assert.match(gradle, /versionName = "0\.19\.3"/);
|
assert.match(gradle, /versionName = "0\.19\.4"/);
|
||||||
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"/);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user