F6.9 capture act narrative before seal in offline Android flow
Android CI / RC / Android · lint, tests, debug APK, release compile (push) Successful in 3m11s
DH V2 CI / API · typecheck, tests, build (push) Failing after 25s
DH V2 CI / WEB · typecheck, build (push) Successful in 18s
DH V2 CI / Docker / scripts contract (push) Skipped
Production dependency audit / API · production dependencies (push) Successful in 9s
Production dependency audit / WEB · production dependencies (push) Successful in 8s

This commit is contained in:
DH V2
2026-09-15 19:44:02 -03:00
parent 60ba37c287
commit c78c2b8029
8 changed files with 68 additions and 15 deletions
@@ -260,7 +260,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
status = "DRAFT",
occurredAt = occurredAt,
title = "Acta de inspección ${currentVisit.code}",
summary = "Acta de inspección en curso. Los Hallazgos y observaciones se incorporan de forma trazable durante la inspección.",
summary = "Se inició la inspección ${currentVisit.code}.",
)
selectedAct = local
acts = acts + MobileActSummary(
@@ -733,16 +733,22 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
}
}
fun prepareSelectedAct(urgency: String) {
fun prepareSelectedAct(urgency: String, narrative: String) {
val actId = selectedAct?.id ?: return
if (urgency !in setOf("URGENT", "NON_URGENT")) {
error = "Definí la urgencia antes de cerrar el Acta."
return
}
val summary = narrative.trim()
if (summary.length !in 10..20000) {
error = "Describí lo actuado antes de cerrar el Acta (entre 10 y 20000 caracteres)."
return
}
launchBusy(mutation = true) {
offlineQueue.enqueueActSummary(actId, summary)
offlineQueue.enqueueActLock(actId, urgency)
selectedAct = selectedAct?.copy(status = "LOCKED", urgency = urgency)
acts = acts.map { if (it.id == actId) it.copy(status = "LOCKED", urgency = urgency) else it }
selectedAct = selectedAct?.copy(status = "LOCKED", urgency = urgency, summary = summary)
acts = acts.map { if (it.id == actId) it.copy(status = "LOCKED", urgency = urgency, summary = summary) else it }
actClosure = actClosure?.copy(act = actClosure!!.act.copy(status = "LOCKED", urgency = urgency))
val result = offlineQueue.syncAll()
refreshOfflineStateInternal()
@@ -106,7 +106,8 @@ data class PrepareMobileActRequest(
)
data class UpdateMobileActRequest(
val assetIds: List<String>,
val assetIds: List<String>? = null,
val summary: String? = null,
)
data class MobileResponsibleRequest(
@@ -367,7 +368,7 @@ class MobileActsRepository(context: Context) {
clientGeneratedId = clientGeneratedId,
occurredAt = occurredAt,
title = "Acta de inspección $visitCode",
summary = "Acta de inspección en curso. Los Hallazgos y observaciones se incorporan de forma trazable durante la inspección.",
summary = "Se inició la inspección $visitCode.",
assetIds = emptyList(),
),
)
@@ -376,6 +377,10 @@ class MobileActsRepository(context: Context) {
return created
}
suspend fun updateSummary(actId: String, summary: String): MobileActDetail = authorized { session ->
api.updateAct("Bearer ${session.accessToken}", actId, UpdateMobileActRequest(summary = summary))
}
suspend fun ensureAsset(actId: String, assetId: String): MobileActDetail {
val detail = get(actId)
if (detail.assets.any { it.id == assetId }) return detail
@@ -256,6 +256,12 @@ class OfflineMutationQueue(private val context: Context) {
.toString(),
)
suspend fun enqueueActSummary(actId: String, summary: String): String = enqueueJson(
type = "ACT_SUMMARY",
resourceId = actId,
payload = JSONObject().put("summary", summary).toString(),
)
suspend fun enqueueActAsset(actId: String, assetId: String): String = enqueueJson(
type = "ACT_ASSET_ENSURE",
resourceId = actId,
@@ -450,6 +456,10 @@ class OfflineMutationQueue(private val context: Context) {
clientGeneratedId = operation.resourceId,
occurredAt = payload.getString("occurredAt"),
)
"ACT_SUMMARY" -> MobileActsRepository(context).updateSummary(
operation.resourceId,
payload.getString("summary"),
)
"ACT_ASSET_ENSURE" -> MobileActsRepository(context).ensureAsset(
operation.resourceId,
payload.getString("assetId"),
@@ -62,6 +62,9 @@ fun MobileActsScreen(
val scope = rememberCoroutineScope()
var closingUrgency by rememberSaveable(selected?.id) { mutableStateOf("") }
var actNarrative by rememberSaveable(selected?.id) {
mutableStateOf(selected?.summary.takeUnless { it == null || it.startsWith("Se inició la inspección ") || it.startsWith("Acta de inspección en curso.") }.orEmpty())
}
var attendance by rememberSaveable(selected?.id) {
mutableStateOf(representativeSeed?.attendanceStatus ?: "PRESENT")
}
@@ -233,6 +236,13 @@ fun MobileActsScreen(
HorizontalDivider()
Text("Finalizar contenido", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
Text("Al cerrar el contenido, el Acta queda inmutable y pasa a Para firmar. La firma del inspector se toma de Mi perfil del Dashboard; en la tablet firma el acompañante de la empresa.")
OutlinedTextField(
actNarrative,
{ actNarrative = it },
label = { Text("Descripción de lo actuado *") },
minLines = 4,
modifier = Modifier.fillMaxWidth(),
)
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
AssistChip(
onClick = { closingUrgency = "NON_URGENT" },
@@ -244,8 +254,8 @@ fun MobileActsScreen(
)
}
Button(
onClick = { model.prepareSelectedAct(closingUrgency) },
enabled = !model.busy && closure?.responsible != null && closingUrgency.isNotBlank(),
onClick = { model.prepareSelectedAct(closingUrgency, actNarrative) },
enabled = !model.busy && closure?.responsible != null && closingUrgency.isNotBlank() && actNarrative.trim().length in 10..20000,
modifier = Modifier.fillMaxWidth(),
) { Text("Cerrar contenido y pasar a firma") }
}
@@ -76,6 +76,9 @@ fun ModernMobileActsScreen(
val scope = rememberCoroutineScope()
var closingUrgency by rememberSaveable(selected?.id) { mutableStateOf("") }
var actNarrative by rememberSaveable(selected?.id) {
mutableStateOf(selected?.summary.takeUnless { it == null || it.startsWith("Se inició la inspección ") || it.startsWith("Acta de inspección en curso.") }.orEmpty())
}
var fullName by rememberSaveable(selected?.id) { mutableStateOf(representativeSeed?.fullName.orEmpty()) }
var documentType by rememberSaveable(selected?.id) { mutableStateOf("DNI") }
var documentNumber by rememberSaveable(selected?.id) { mutableStateOf(representativeSeed?.documentNumber.orEmpty()) }
@@ -307,6 +310,14 @@ fun ModernMobileActsScreen(
"Al cerrar el contenido, el Acta queda inmutable y pasa a Para firmar. La firma del inspector se toma de Mi perfil del Dashboard; en la tablet sólo firma el acompañante de la empresa. La urgencia pertenece al Acta completa. La urgencia no se asigna a cada Hallazgo.",
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
OutlinedTextField(
actNarrative,
{ actNarrative = it },
label = { Text("Descripción de lo actuado *") },
supportingText = { Text("Relatá la inspección y sus observaciones. Se incorpora al acta antes de sellarla.") },
minLines = 4,
modifier = Modifier.fillMaxWidth(),
)
Text("Urgencia del Acta", fontWeight = FontWeight.SemiBold)
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
FilterChip(
@@ -330,8 +341,8 @@ fun ModernMobileActsScreen(
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Button(
onClick = { model.prepareSelectedAct(closingUrgency) },
enabled = !model.busy && closure?.responsible != null && closingUrgency.isNotBlank(),
onClick = { model.prepareSelectedAct(closingUrgency, actNarrative) },
enabled = !model.busy && closure?.responsible != null && closingUrgency.isNotBlank() && actNarrative.trim().length in 10..20000,
modifier = Modifier.fillMaxWidth(),
) { Text("Cerrar contenido y pasar a firma") }
}
@@ -8,8 +8,8 @@ class ReleaseMetadataTest {
@Test
fun debugBuildKeepsSeparateApplicationIdentity() {
assertEquals("com.korexlabs.dhinspeccion.debug", BuildConfig.APPLICATION_ID)
assertEquals(38, BuildConfig.VERSION_CODE)
assertEquals("0.19.10-debug", BuildConfig.VERSION_NAME)
assertEquals(39, BuildConfig.VERSION_CODE)
assertEquals("0.19.11-debug", BuildConfig.VERSION_NAME)
}
@Test