diff --git a/android-app/RELEASE.md b/android-app/RELEASE.md index 42a9bae..46a445b 100644 --- a/android-app/RELEASE.md +++ b/android-app/RELEASE.md @@ -1,20 +1,20 @@ -# DH Inspección Android · release final de campo 0.19.3 +# DH Inspección Android · release final de campo 0.19.7 ## Candidata vigente - Fase funcional: **Flujo final de campo · Inspección → Acta → Hallazgos → Firma**. -- `versionName`: **0.19.3**. -- `versionCode`: **31**. +- `versionName`: **0.19.7**. +- `versionCode`: **35**. - Application ID release: `com.korexlabs.dhinspeccion`. - Application ID debug/QA: `com.korexlabs.dhinspeccion.debug`. - API: `https://dhv2.korexlabs.com/api/v3/`. -- Servidor compatible de esta candidata: **API 0.29.0-4 / WEB 0.23.0-3**. +- Servidor compatible de esta candidata: **API 0.29.0-5 / WEB 0.23.0-3**. La variante debug es independiente de la app productiva y puede instalarse para QA/presentación sin sobrescribir una instalación release histórica. ## Procedimiento operativo validado -La APK 0.19.3 fija como recorrido principal de campo: +La APK 0.19.7 fija como recorrido principal de campo: 1. **Iniciar Inspección**. Al iniciarla se habilita el circuito de **Actas y Hallazgos**; Inventario no es una acción independiente de la Inspección. 2. **Crear o abrir un Acta**. Puede haber varias Actas dentro de una misma Inspección, pero sólo una puede permanecer en elaboración al mismo tiempo. La urgencia todavía no se define. @@ -51,6 +51,10 @@ Además de lo anterior requiere elegir primero la **Instalación padre**. El bus La clasificación y los atributos se obtienen dinámicamente desde el mismo catálogo administrado por el Dashboard; la APK no mantiene listas técnicas paralelas. +## Alta de Hallazgos + +El alta de Hallazgos usa un selector desplegable con búsqueda por nombre. La pantalla muestra sólo la información necesaria para decidir, mantiene **Otro / No está en la lista** como salida manual y evita exponer códigos o leyendas internas del catálogo. La fecha de corrección no se solicita al inspector al crear un Hallazgo; los plazos administrativos se gestionan fuera de este alta de campo. Los campos de carga son compactos y el teclado avanza con **Siguiente** entre ellos. + ## Actas y firma La terminología visible se simplifica: diff --git a/android-app/app/build.gradle.kts b/android-app/app/build.gradle.kts index f8093e2..b941cfd 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 = 34 - versionName = "0.19.6" + versionCode = 35 + versionName = "0.19.7" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true diff --git a/android-app/app/src/main/java/com/korexlabs/dhinspeccion/MainViewModel.kt b/android-app/app/src/main/java/com/korexlabs/dhinspeccion/MainViewModel.kt index 3e8caf8..21f5f89 100644 --- a/android-app/app/src/main/java/com/korexlabs/dhinspeccion/MainViewModel.kt +++ b/android-app/app/src/main/java/com/korexlabs/dhinspeccion/MainViewModel.kt @@ -316,7 +316,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { ) selectedFieldAsset = selectedFieldAsset?.copy(capture = response.capture) notice = if (response.capture.readyForFinding) { - "Captura completa: GPS y fotografía registrados." + "Inventario listo: ubicación y foto registradas." } else { "Fotografía registrada." } @@ -353,7 +353,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { customLegalBasis: String?, description: String, severity: Int?, - correctionDueOn: String?, ) { val visitId = visit?.id ?: return val assetId = selectedFieldAsset?.asset?.id ?: return @@ -386,7 +385,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { customLegalBasis = customLegalBasis?.trim()?.takeIf { it.isNotBlank() }, description = description.trim(), severity = severity, - correctionDueOn = correctionDueOn?.trim()?.takeIf { it.isNotBlank() }, ), ) lastCreatedFinding = response.finding diff --git a/android-app/app/src/main/java/com/korexlabs/dhinspeccion/data/DhMobile.kt b/android-app/app/src/main/java/com/korexlabs/dhinspeccion/data/DhMobile.kt index 1c95deb..293c173 100644 --- a/android-app/app/src/main/java/com/korexlabs/dhinspeccion/data/DhMobile.kt +++ b/android-app/app/src/main/java/com/korexlabs/dhinspeccion/data/DhMobile.kt @@ -561,8 +561,10 @@ class DhRepository(context: Context) { if (error is HttpException) { val body = runCatching { error.response()?.errorBody()?.string() }.getOrNull() val message = runCatching { JSONObject(body.orEmpty()).optString("message") }.getOrNull() + if (message == "Error interno") return "No se pudo completar la operación. Intentá nuevamente." if (!message.isNullOrBlank()) return message - return "Error HTTP ${error.code()}" + if (error.code() >= 500) return "No se pudo completar la operación. Intentá nuevamente." + return "No se pudo procesar la solicitud (${error.code()})." } return error.message ?: "Ocurrió un error inesperado" } diff --git a/android-app/app/src/main/java/com/korexlabs/dhinspeccion/data/FieldFindingsMobile.kt b/android-app/app/src/main/java/com/korexlabs/dhinspeccion/data/FieldFindingsMobile.kt index f90b88f..0f8bf0c 100644 --- a/android-app/app/src/main/java/com/korexlabs/dhinspeccion/data/FieldFindingsMobile.kt +++ b/android-app/app/src/main/java/com/korexlabs/dhinspeccion/data/FieldFindingsMobile.kt @@ -113,7 +113,6 @@ data class CreateFieldFindingRequest( val customLegalBasis: String? = null, val description: String, val severity: Int? = null, - val correctionDueOn: String? = null, ) data class FieldFindingCreateResponse( diff --git a/android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/FieldFindingScreen.kt b/android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/FieldFindingScreen.kt index c002404..a2dff43 100644 --- a/android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/FieldFindingScreen.kt +++ b/android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/FieldFindingScreen.kt @@ -7,21 +7,27 @@ import android.net.Uri import android.os.Environment import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text @@ -34,8 +40,12 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat @@ -60,20 +70,23 @@ private data class FindingGeoSnapshot( val accuracyM: Double?, ) +@OptIn(ExperimentalMaterial3Api::class) @Composable fun FieldFindingScreen(model: MainViewModel) { val options = model.fieldFindingOptions ?: return val asset = model.selectedFieldAsset?.asset ?: return val context = LocalContext.current val scope = rememberCoroutineScope() + val focusManager = LocalFocusManager.current + val keyboard = LocalSoftwareKeyboardController.current var search by rememberSaveable(asset.id) { mutableStateOf("") } + var catalogExpanded by rememberSaveable(asset.id) { mutableStateOf(false) } var selectedCatalogId by rememberSaveable(asset.id) { mutableStateOf(null) } var other by rememberSaveable(asset.id) { mutableStateOf(false) } var customTitle by rememberSaveable(asset.id) { mutableStateOf("") } var customLegalBasis by rememberSaveable(asset.id) { mutableStateOf("") } var description by rememberSaveable(asset.id) { mutableStateOf("") } var severityText by rememberSaveable(asset.id) { mutableStateOf("") } - var correctionDueOn by rememberSaveable(asset.id) { mutableStateOf("") } var localError by rememberSaveable { mutableStateOf(null) } var requestedFindingId by remember { mutableStateOf(null) } @@ -164,8 +177,8 @@ fun FieldFindingScreen(model: MainViewModel) { customLegalBasis = "" description = "" severityText = "" - correctionDueOn = "" search = "" + catalogExpanded = false } } @@ -180,15 +193,15 @@ fun FieldFindingScreen(model: MainViewModel) { OutlinedButton(onClick = { model.clearFindingFlow() }, enabled = !model.busy) { Text("Volver") } - Text("Hallazgo de campo", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) + Text("Nuevo Hallazgo", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) } Card(Modifier.fillMaxWidth()) { Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) { Text(asset.name, fontWeight = FontWeight.Bold) - Text("${asset.code} · Acta ${options.act.code}", style = MaterialTheme.typography.bodySmall) + Text("${asset.code} · ${options.act.code}", style = MaterialTheme.typography.bodySmall) Text( - "GPS + foto del Inventario: ${if (options.capture.readyForFinding) "completo" else "pendiente"}", + if (options.capture.readyForFinding) "Inventario listo para registrar Hallazgos" else "Completá GPS y foto antes de continuar", color = if (options.capture.readyForFinding) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall, ) @@ -238,74 +251,105 @@ fun FieldFindingScreen(model: MainViewModel) { HorizontalDivider() } - Text("1. Elegí el tipo de Hallazgo", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) + Text("1. Tipo de Hallazgo", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) if (!options.catalog.typeConfigured) { Text( - options.catalog.configurationReason - ?: "Este tipo de Inventario todavía no tiene un catálogo contextual configurado. Podés usar OTROS.", - color = MaterialTheme.colorScheme.secondary, + "No hay tipos sugeridos para este Inventario. Elegí Otro para cargarlo manualmente.", + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodySmall, ) } - OutlinedTextField( - value = search, - onValueChange = { search = it }, - label = { Text("Buscar en catálogo") }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - ) - - filtered.forEach { item -> - val chosen = !other && selectedCatalogId == item.id - Card( - modifier = Modifier.fillMaxWidth().clickable { - selectedCatalogId = item.id - other = false - severityText = item.suggestedSeverity?.toString().orEmpty() - }, - colors = if (chosen) { - CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer) - } else { - CardDefaults.cardColors() + ExposedDropdownMenuBox( + expanded = catalogExpanded && !model.busy, + onExpandedChange = { + if (!model.busy) { + catalogExpanded = it + if (it) search = "" + } + }, + ) { + OutlinedTextField( + value = if (catalogExpanded) search else when { + other -> "Otro / No está en la lista" + selected != null -> selected.title + else -> "" }, + onValueChange = { search = it; catalogExpanded = true }, + label = { Text("Tipo de Hallazgo *") }, + placeholder = { Text("Buscar o seleccionar") }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = catalogExpanded) }, + modifier = Modifier.fillMaxWidth().menuAnchor(MenuAnchorType.PrimaryEditable), + enabled = !model.busy, + singleLine = true, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + keyboardActions = KeyboardActions(onNext = { + catalogExpanded = false + focusManager.moveFocus(FocusDirection.Down) + }), + ) + ExposedDropdownMenu( + expanded = catalogExpanded && !model.busy, + onDismissRequest = { catalogExpanded = false; search = "" }, + modifier = Modifier.heightIn(max = 320.dp), ) { - Column(Modifier.padding(10.dp), verticalArrangement = Arrangement.spacedBy(3.dp)) { - Text(if (chosen) "✓ ${item.title}" else item.title, fontWeight = FontWeight.SemiBold) - Text( - listOfNotNull(item.categoryName, item.code, item.suggestedSeverity?.let { "Gravedad sugerida $it" }) - .joinToString(" · "), - style = MaterialTheme.typography.bodySmall, + filtered.forEach { item -> + DropdownMenuItem( + text = { + Column { + Text(item.title, fontWeight = FontWeight.SemiBold) + item.suggestedSeverity?.let { + Text("Gravedad sugerida: $it/10", style = MaterialTheme.typography.bodySmall) + } + } + }, + onClick = { + selectedCatalogId = item.id + other = false + severityText = item.suggestedSeverity?.toString().orEmpty() + catalogExpanded = false + search = "" + keyboard?.hide() + focusManager.moveFocus(FocusDirection.Down) + }, ) } + DropdownMenuItem( + text = { Text("Otro / No está en la lista", fontWeight = FontWeight.SemiBold) }, + onClick = { + other = true + selectedCatalogId = null + severityText = "" + catalogExpanded = false + search = "" + keyboard?.hide() + focusManager.moveFocus(FocusDirection.Down) + }, + ) } } - - OutlinedButton( - onClick = { - other = true - selectedCatalogId = null - severityText = "" - }, - modifier = Modifier.fillMaxWidth(), - ) { - Text(if (other) "✓ OTROS · Hallazgo no catalogado" else "OTROS · No está en el catálogo") - } if (other) { - options.catalog.other.help?.let { Text(it, style = MaterialTheme.typography.bodySmall) } + Text("Cargá un nombre claro para identificar el Hallazgo.", style = MaterialTheme.typography.bodySmall) OutlinedTextField( value = customTitle, onValueChange = { customTitle = it }, - label = { Text("Título del nuevo Hallazgo *") }, + label = { Text("Nombre del Hallazgo *") }, modifier = Modifier.fillMaxWidth(), + singleLine = true, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Down) }), ) OutlinedTextField( value = customLegalBasis, onValueChange = { customLegalBasis = it }, - label = { Text("Base legal / normativa (opcional)") }, + label = { Text("Normativa (opcional)") }, modifier = Modifier.fillMaxWidth(), + singleLine = true, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Down) }), ) } - Text("2. Describí lo observado", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) + Text("2. Qué observaste", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) selected?.let { Card(Modifier.fillMaxWidth()) { Column(Modifier.padding(10.dp), verticalArrangement = Arrangement.spacedBy(3.dp)) { @@ -318,23 +362,20 @@ fun FieldFindingScreen(model: MainViewModel) { OutlinedTextField( value = description, onValueChange = { description = it }, - label = { Text("Descripción del Hallazgo *") }, + label = { Text("Qué observaste *") }, + placeholder = { Text("Describí brevemente el problema") }, modifier = Modifier.fillMaxWidth(), - minLines = 3, + singleLine = true, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Down) }), ) OutlinedTextField( value = severityText, onValueChange = { value -> severityText = value.filter(Char::isDigit).take(2) }, - label = { Text("Gravedad 1 a 10") }, - modifier = Modifier.fillMaxWidth(), - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - singleLine = true, - ) - OutlinedTextField( - value = correctionDueOn, - onValueChange = { correctionDueOn = it }, - label = { Text("Fecha de corrección AAAA-MM-DD (opcional)") }, + label = { Text("Gravedad (1 a 10, opcional)") }, modifier = Modifier.fillMaxWidth(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number, imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = { keyboard?.hide(); focusManager.clearFocus() }), singleLine = true, ) @@ -348,7 +389,6 @@ fun FieldFindingScreen(model: MainViewModel) { customLegalBasis = if (other) customLegalBasis else null, description = description, severity = severity, - correctionDueOn = correctionDueOn, ) }, enabled = choiceReady && description.isNotBlank() && (severity == null || severity in 1..10) && !model.busy, diff --git a/android-app/app/src/test/java/com/korexlabs/dhinspeccion/FinalFieldFlowContractTest.kt b/android-app/app/src/test/java/com/korexlabs/dhinspeccion/FinalFieldFlowContractTest.kt index 235fa5d..ec44040 100644 --- a/android-app/app/src/test/java/com/korexlabs/dhinspeccion/FinalFieldFlowContractTest.kt +++ b/android-app/app/src/test/java/com/korexlabs/dhinspeccion/FinalFieldFlowContractTest.kt @@ -63,4 +63,14 @@ class FinalFieldFlowContractTest { assertFalse(acts.contains("Acta SELLADA")) assertFalse(acts.contains("Text(\"Email\")")) } + @Test + fun findingCreationIsCompactSearchableAndHasNoCorrectionDate() { + assertTrue(findings.contains("ExposedDropdownMenuBox")) + assertTrue(findings.contains("Tipo de Hallazgo *")) + assertTrue(findings.contains("Buscar o seleccionar")) + assertTrue(findings.contains("Otro / No está en la lista")) + assertFalse(findings.contains("Fecha de corrección")) + assertFalse(findings.contains("Hallazgos del modelo autoritativo")) + } + } 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 fe1b198..26727de 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(34, BuildConfig.VERSION_CODE) - assertEquals("0.19.6-debug", BuildConfig.VERSION_NAME) + assertEquals(35, BuildConfig.VERSION_CODE) + assertEquals("0.19.7-debug", BuildConfig.VERSION_NAME) } @Test diff --git a/api-v3/package-lock.json b/api-v3/package-lock.json index a8c907e..4569f16 100644 --- a/api-v3/package-lock.json +++ b/api-v3/package-lock.json @@ -1,12 +1,12 @@ { "name": "dhv2-api", - "version": "0.29.0-4", + "version": "0.29.0-5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dhv2-api", - "version": "0.29.0-4", + "version": "0.29.0-5", "license": "UNLICENSED", "dependencies": { "@nestjs/common": "^11.0.0", diff --git a/api-v3/package.json b/api-v3/package.json index 22042f9..4866a63 100644 --- a/api-v3/package.json +++ b/api-v3/package.json @@ -1,6 +1,6 @@ { "name": "dhv2-api", - "version": "0.29.0-4", + "version": "0.29.0-5", "private": true, "license": "UNLICENSED", "scripts": { diff --git a/api-v3/src/database/migrations/1790121000000-f6-5-field-finding-code-contract.ts b/api-v3/src/database/migrations/1790121000000-f6-5-field-finding-code-contract.ts new file mode 100644 index 0000000..35dc450 --- /dev/null +++ b/api-v3/src/database/migrations/1790121000000-f6-5-field-finding-code-contract.ts @@ -0,0 +1,45 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +/** + * F4 cambió el código visible del Acta a ACT-NNNNN-DD-MM-YY, pero el CHECK + * histórico de Hallazgos seguía aceptando sólo ACTA-YYYY-NNNNNN-HNNN. + * El servicio genera el Hallazgo a partir del código real del Acta, por lo que + * PostgreSQL rechazaba altas válidas de campo con un 500. + */ +export class F65FieldFindingCodeContract1790121000000 implements MigrationInterface { + name = 'F65FieldFindingCodeContract1790121000000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE inspection_findings + DROP CONSTRAINT IF EXISTS chk_inspection_findings_code + `); + await queryRunner.query(` + ALTER TABLE inspection_findings + ADD CONSTRAINT chk_inspection_findings_code CHECK ( + code ~ '^(ACTA-[0-9]{4}-[0-9]{6}|ACT-[0-9]{5}-[0-9]{2}-[0-9]{2}-[0-9]{2})-H[0-9]{3}$' + ) + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + UPDATE inspection_findings finding + SET code = 'ACTA-' || act.act_year::text || '-' || LPAD(act.act_number::text, 6, '0') + || '-H' || LPAD(finding.finding_number::text, 3, '0') + FROM inspection_acts act + WHERE finding.act_id = act.id + AND finding.code ~ '^ACT-[0-9]{5}-[0-9]{2}-[0-9]{2}-[0-9]{2}-H[0-9]{3}$' + `); + await queryRunner.query(` + ALTER TABLE inspection_findings + DROP CONSTRAINT IF EXISTS chk_inspection_findings_code + `); + await queryRunner.query(` + ALTER TABLE inspection_findings + ADD CONSTRAINT chk_inspection_findings_code CHECK ( + code ~ '^ACTA-[0-9]{4}-[0-9]{6}-H[0-9]{3}$' + ) + `); + } +} diff --git a/api-v3/src/inspection-visits/dto/create-field-finding.dto.ts b/api-v3/src/inspection-visits/dto/create-field-finding.dto.ts index abbce3a..5359dc2 100644 --- a/api-v3/src/inspection-visits/dto/create-field-finding.dto.ts +++ b/api-v3/src/inspection-visits/dto/create-field-finding.dto.ts @@ -4,7 +4,6 @@ import { IsOptional, IsString, IsUUID, - Matches, Max, MaxLength, Min, @@ -56,8 +55,4 @@ export class CreateFieldFindingDto { @Max(10) severity?: number; - @IsOptional() - @Transform(optionalText) - @Matches(/^\d{4}-\d{2}-\d{2}$/) - correctionDueOn?: string | null; } diff --git a/api-v3/src/inspection-visits/field-findings.service.ts b/api-v3/src/inspection-visits/field-findings.service.ts index bd4f66e..030baa0 100644 --- a/api-v3/src/inspection-visits/field-findings.service.ts +++ b/api-v3/src/inspection-visits/field-findings.service.ts @@ -105,7 +105,7 @@ export class FieldFindingsService { customLegalBasis: dto.customLegalBasis ?? null, description: dto.description, severity: dto.severity, - correctionDueOn: dto.correctionDueOn ?? null, + correctionDueOn: null, }; const finding = await this.findings.create(act.id, payload, principal, request); return { diff --git a/api-v3/src/version.ts b/api-v3/src/version.ts index 3d33292..6664a02 100644 --- a/api-v3/src/version.ts +++ b/api-v3/src/version.ts @@ -1,2 +1,2 @@ -export const API_VERSION = '0.29.0-4'; +export const API_VERSION = '0.29.0-5'; export const API_PHASE = 'F6.2'; diff --git a/api-v3/test/unit/f4-health-metadata.test.ts b/api-v3/test/unit/f4-health-metadata.test.ts index e107c8f..dfbdd03 100644 --- a/api-v3/test/unit/f4-health-metadata.test.ts +++ b/api-v3/test/unit/f4-health-metadata.test.ts @@ -8,5 +8,5 @@ test('health metadata reports the current F6.1 release', () => { assert.equal(API_PHASE, 'F6.2'); 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-4'); + assert.equal(API_VERSION, '0.29.0-5'); }); \ No newline at end of file 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 65512e7..cad00c4 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 = 34/); - assert.match(gradle, /versionName = "0\.19\.6"/); + assert.match(gradle, /versionCode = 35/); + assert.match(gradle, /versionName = "0\.19\.7"/); assert.match(gradle, /https:\/\/dhv2\.korexlabs\.com\/api\/v3\//); assert.match(gradle, /applicationIdSuffix = "\.debug"/); }); diff --git a/api-v3/test/unit/f6-1-findings-mobile-open.test.ts b/api-v3/test/unit/f6-1-findings-mobile-open.test.ts index 0fe7347..998b84d 100644 --- a/api-v3/test/unit/f6-1-findings-mobile-open.test.ts +++ b/api-v3/test/unit/f6-1-findings-mobile-open.test.ts @@ -31,7 +31,8 @@ test('F6.1 uses contextual type catalog for Yacimiento and keeps OTROS/add-anoth assert.match(catalog, /finding_catalog_asset_overrides override/); assert.match(resolver, /code: 'OTHER'/); assert.match(resolver, /label: 'OTROS'/); - assert.match(androidFinding, /OTROS · No está en el catálogo/); + assert.match(androidFinding, /ExposedDropdownMenuBox/); + assert.match(androidFinding, /Otro \/ No está en la lista/); assert.match(androidFinding, /También podés registrar otro Hallazgo sobre el mismo Inventario/); }); diff --git a/api-v3/test/unit/f6-5-field-finding-mobile-contract.test.ts b/api-v3/test/unit/f6-5-field-finding-mobile-contract.test.ts new file mode 100644 index 0000000..2574c2a --- /dev/null +++ b/api-v3/test/unit/f6-5-field-finding-mobile-contract.test.ts @@ -0,0 +1,31 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import test from 'node:test'; + +function source(path: string): string { + return readFileSync(resolve(process.cwd(), path), 'utf8'); +} + +test('F6.5 finding code CHECK accepts the current visible Acta code', () => { + const migration = source('src/database/migrations/1790121000000-f6-5-field-finding-code-contract.ts'); + const service = source('src/inspection-findings/inspection-findings.service.ts'); + + assert.match(migration, /ACT-\[0-9\]\{5\}-\[0-9\]\{2\}-\[0-9\]\{2\}-\[0-9\]\{2\}/); + assert.match(migration, /-H\[0-9\]\{3\}/); + assert.match(service, /code: `\$\{act\.code\}-H\$\{String\(findingNumber\)\.padStart\(3, '0'\)\}`/); +}); + +test('F6.5 mobile finding creation never asks the inspector for a correction date', () => { + const dto = source('src/inspection-visits/dto/create-field-finding.dto.ts'); + const adapter = source('src/inspection-visits/field-findings.service.ts'); + const androidRequest = source('../android-app/app/src/main/java/com/korexlabs/dhinspeccion/data/FieldFindingsMobile.kt'); + const androidScreen = source('../android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/FieldFindingScreen.kt'); + + assert.doesNotMatch(dto, /correctionDueOn/); + assert.match(adapter, /correctionDueOn: null/); + assert.doesNotMatch(androidRequest.match(/data class CreateFieldFindingRequest\([\s\S]*?\n\)/)?.[0] ?? '', /correctionDueOn/); + assert.doesNotMatch(androidScreen, /Fecha de corrección/); + assert.match(androidScreen, /ExposedDropdownMenuBox/); + assert.match(androidScreen, /Tipo de Hallazgo \*/); +});