From 496bb785c12ff284fc67ab69d177128fd1669d29 Mon Sep 17 00:00:00 2001 From: DH Android Finalizer Date: Fri, 11 Sep 2026 15:36:42 +0000 Subject: [PATCH] fix(android): preservar ViewModel estable en release final --- .../workflows/restore-mainviewmodel-once.yml | 86 ---- .../korexlabs/dhinspeccion/MainViewModel.kt | 372 +++++++++++------- 2 files changed, 224 insertions(+), 234 deletions(-) delete mode 100644 .github/workflows/restore-mainviewmodel-once.yml diff --git a/.github/workflows/restore-mainviewmodel-once.yml b/.github/workflows/restore-mainviewmodel-once.yml deleted file mode 100644 index 1a2ef95..0000000 --- a/.github/workflows/restore-mainviewmodel-once.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: Restore Android MainViewModel once - -on: - push: - branches: - - 'release/android-0.19.0-final-field-flow' - -permissions: - contents: write - -jobs: - restore: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: release/android-0.19.0-final-field-flow - fetch-depth: 0 - - - name: Restore MainViewModel from main and apply only 0.19.0 changes - run: | - set -Eeuo pipefail - git fetch origin main - git show origin/main:android-app/app/src/main/java/com/korexlabs/dhinspeccion/MainViewModel.kt > android-app/app/src/main/java/com/korexlabs/dhinspeccion/MainViewModel.kt - python - <<'PY' - from pathlib import Path - p = Path('android-app/app/src/main/java/com/korexlabs/dhinspeccion/MainViewModel.kt') - text = p.read_text() - replacements = [ - ( - ' accuracyM: Double?,\n ) {\n val visitId = visit?.id ?: return\n if (visit?.status != "IN_PROGRESS") {', - ' accuracyM: Double?,\n description: String? = null,\n ) {\n val visitId = visit?.id ?: return\n if (visit?.status != "IN_PROGRESS") {' - ), - ( - ' commonName = commonName?.trim()?.takeIf { it.isNotBlank() },\n attributes = attributes,', - ' commonName = commonName?.trim()?.takeIf { it.isNotBlank() },\n description = description?.trim()?.takeIf { it.isNotBlank() },\n attributes = attributes,' - ), - ( - 'error = "Ya existe un Acta en borrador. Bloqueala o cancelala antes de crear la siguiente."', - 'error = "Ya existe un Acta en elaboración. Cerrala o cancelala antes de crear la siguiente."' - ), - ( - 'error = "Creá o seleccioná el Acta en borrador antes de registrar Hallazgos."', - 'error = "Creá o seleccioná el Acta en elaboración antes de registrar Hallazgos."' - ), - ( - 'error = "No hay un Acta en borrador seleccionada."', - 'error = "No hay un Acta en elaboración seleccionada."' - ), - ( - 'notice = "Ausencia del responsable registrada. La manifestación de empresa quedará pendiente y deberá resolverse antes de sellar el Acta."', - 'notice = "Ausencia del responsable registrada. La manifestación de empresa quedará pendiente y deberá resolverse antes de firmar y cerrar el Acta."' - ), - ( - 'notice = "Acta bloqueada. Su contenido quedó inmutable; ahora deben resolverse las firmas y manifestaciones."', - 'notice = "Acta cerrada y pendiente de firma. Su contenido quedó inmutable; las firmas y manifestaciones pueden completarse a continuación."' - ), - ( - 'error = "Un Acta bloqueada es inmutable y no puede volver a borrador."', - 'error = "Un Acta pendiente de firma es inmutable y no puede volver a edición."' - ), - ( - 'notice = "${selectedAct?.code ?: "Acta"} SELLADA e inmutable. Podés crear otra Acta o continuar hacia el cierre de la Inspección."', - 'notice = "${selectedAct?.code ?: "Acta"} firmada y cerrada. Podés crear otra Acta o continuar hacia el cierre de la Inspección."' - ), - ( - 'notice = "Inspección cerrada. Todas sus Actas quedaron SELLADAS y disponibles para el circuito de oficina."', - 'notice = "Inspección cerrada. Todas sus Actas quedaron firmadas y disponibles para el circuito de oficina."' - ), - ] - for old, new in replacements: - if old not in text: - raise SystemExit(f'Pattern not found: {old}') - text = text.replace(old, new, 1) - p.write_text(text) - PY - - - name: Commit restore and remove temporary workflow - run: | - set -Eeuo pipefail - git config user.name 'DH Android Finalizer' - git config user.email 'android-finalizer@dhv2.local' - git rm .github/workflows/restore-mainviewmodel-once.yml - git add android-app/app/src/main/java/com/korexlabs/dhinspeccion/MainViewModel.kt - git commit -m 'fix(android): preservar ViewModel estable en release final' - git push origin HEAD:release/android-0.19.0-final-field-flow 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 5201dad..4d08601 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 @@ -1,174 +1,230 @@ package com.korexlabs.dhinspeccion import android.app.Application +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope -import com.korexlabs.dhinspeccion.data.ActAdministrationClosure -import com.korexlabs.dhinspeccion.data.CompanyOutcomeRequest import com.korexlabs.dhinspeccion.data.CreateFieldFindingRequest import com.korexlabs.dhinspeccion.data.CreateFieldInventoryRequest import com.korexlabs.dhinspeccion.data.DhRepository import com.korexlabs.dhinspeccion.data.FieldAssetDetail import com.korexlabs.dhinspeccion.data.FieldFindingEvidence +import com.korexlabs.dhinspeccion.data.FieldFindingItem import com.korexlabs.dhinspeccion.data.FieldFindingOptionsResponse +import com.korexlabs.dhinspeccion.data.FieldFindingsRepository +import com.korexlabs.dhinspeccion.data.FieldInventoryItem import com.korexlabs.dhinspeccion.data.FieldType -import com.korexlabs.dhinspeccion.data.InspectionAct -import com.korexlabs.dhinspeccion.data.InspectionFinding -import com.korexlabs.dhinspeccion.data.MobileActClosureRepository -import com.korexlabs.dhinspeccion.data.MobileActRepository -import com.korexlabs.dhinspeccion.data.MobileCompanyOutcomeRequest -import com.korexlabs.dhinspeccion.data.MobileFieldFindingRepository -import com.korexlabs.dhinspeccion.data.MobileInspectionRepository +import com.korexlabs.dhinspeccion.data.MobileActClosure +import com.korexlabs.dhinspeccion.data.MobileActDetail +import com.korexlabs.dhinspeccion.data.MobileActSummary +import com.korexlabs.dhinspeccion.data.MobileActsRepository import com.korexlabs.dhinspeccion.data.MobileResponsibleRequest -import com.korexlabs.dhinspeccion.data.MobileSession +import com.korexlabs.dhinspeccion.data.StoredSession import com.korexlabs.dhinspeccion.data.VisitDetail import com.korexlabs.dhinspeccion.data.VisitSummary +import kotlinx.coroutines.launch import java.io.File import java.time.Instant -import kotlinx.coroutines.launch class MainViewModel(application: Application) : AndroidViewModel(application) { private val repository = DhRepository(application) - private val inspectionRepository = MobileInspectionRepository(application) - private val actsRepository = MobileActRepository(application) - private val closureRepository = MobileActClosureRepository(application) - private val findingsRepository = MobileFieldFindingRepository(application) + private val findingsRepository = FieldFindingsRepository(application) + private val actsRepository = MobileActsRepository(application) - var session: MobileSession? = null + var session: StoredSession? by mutableStateOf(repository.currentSession()) private set - var visits: List = emptyList() + var busy by mutableStateOf(false) private set - var visit: VisitDetail? = null + var error: String? by mutableStateOf(null) private set - var busy: Boolean = false - private set - var error: String? = null - private set - var notice: String? = null + var notice: String? by mutableStateOf(null) private set - var acts: List = emptyList() + var visits: List by mutableStateOf(emptyList()) private set - var selectedAct: InspectionAct? = null + var visit: VisitDetail? by mutableStateOf(null) private set - var actClosure: ActAdministrationClosure? = null + + var inventory: List by mutableStateOf(emptyList()) private set - var findingOptions: FieldFindingOptionsResponse? = null + var inventoryParentId: String? by mutableStateOf(null) private set - var selectedFieldAsset: FieldAssetDetail? = null + var fieldTypes: List by mutableStateOf(emptyList()) private set - var lastCreatedFinding: InspectionFinding? = null + var selectedFieldAsset: FieldAssetDetail? by mutableStateOf(null) private set - var findingEvidence: List = emptyList() + + var acts: List by mutableStateOf(emptyList()) + private set + var selectedAct: MobileActDetail? by mutableStateOf(null) + private set + var actClosure: MobileActClosure? by mutableStateOf(null) + private set + + var fieldFindingOptions: FieldFindingOptionsResponse? by mutableStateOf(null) + private set + var lastCreatedFinding: FieldFindingItem? by mutableStateOf(null) + private set + var fieldFindingEvidence: Map> by mutableStateOf(emptyMap()) private set init { - session = repository.currentSession() if (session != null) loadVisits() } - fun login(identifier: String, password: String, onSuccess: () -> Unit = {}) { + fun clearMessages() { + error = null + notice = null + } + + fun login(identifier: String, password: String) { if (identifier.isBlank() || password.isBlank()) { - error = "Completá usuario y contraseña." + error = "Ingresá usuario y contraseña." return } launchBusy { - session = repository.login(identifier.trim(), password) + session = repository.login(identifier, password) + notice = "Sesión iniciada." loadVisitsInternal() - onSuccess() } } fun logout() { - repository.logout() - session = null - visits = emptyList() - visit = null - clearActState() - error = null - notice = null + viewModelScope.launch { + runCatching { repository.logout() } + session = null + visits = emptyList() + visit = null + inventory = emptyList() + inventoryParentId = null + fieldTypes = emptyList() + selectedFieldAsset = null + clearActState() + clearFindingState() + } } fun loadVisits() = launchBusy { loadVisitsInternal() } + private suspend fun loadVisitsInternal() { + visits = repository.visits().data + } + fun openVisit(id: String) = launchBusy { - visit = inspectionRepository.detail(id) - loadActsInternal(id) - reloadCurrentInventory(id) + visit = repository.visit(id) + inventory = emptyList() + inventoryParentId = visit?.scopeAsset?.id ?: visit?.operationalArea?.id + fieldTypes = emptyList() + selectedFieldAsset = null clearFindingState() + loadActsInternal(id, selectDraft = true) } - fun refreshVisit() { - val id = visit?.id ?: return - launchBusy { - visit = inspectionRepository.detail(id) - loadActsInternal(id, selectDraft = false) - reloadCurrentInventory(id) - } - } - - fun backToVisits() { + fun closeVisitView() { visit = null + inventory = emptyList() + inventoryParentId = null + fieldTypes = emptyList() + selectedFieldAsset = null clearActState() clearFindingState() - notice = null - error = null + loadVisits() } - fun startCurrentVisit() { + fun startVisit() { val id = visit?.id ?: return launchBusy { - visit = inspectionRepository.start(id) - loadActsInternal(id) - reloadCurrentInventory(id) - notice = "Inspección iniciada. Ya podés abrir un Acta y registrar Hallazgos." + visit = repository.startVisit(id) + inventoryParentId = visit?.scopeAsset?.id ?: visit?.operationalArea?.id + notice = "Inspección iniciada." + loadActsInternal(id, selectDraft = true) + loadVisitsInternal() } } - fun createAct(urgency: String) { + fun reloadActs() { val visitId = visit?.id ?: return - launchBusy { - val created = actsRepository.create(visitId, urgency) - loadActsInternal(visitId, selectDraft = false) - selectedAct = created - actClosure = null - clearFindingState() - notice = "Acta ${created.code} creada. Seleccioná una Instalación o Subinstalación y agregá Hallazgos." - } + launchBusy { loadActsInternal(visitId, selectDraft = selectedAct == null) } } fun selectAct(actId: String) { - val found = acts.firstOrNull { it.id == actId } ?: return - selectedAct = found - clearFindingState() - actClosure = null - if (found.status != "DRAFT") { - reloadSelectedActClosure() + launchBusy { + selectedAct = actsRepository.get(actId) + actClosure = actsRepository.closure(actId) + clearFindingState() } } - fun closeSelectedActView() { - selectedAct = null - clearFindingState() - actClosure = null + fun createActForSelectedInventory(urgency: String = "NON_URGENT") { + val currentVisit = visit ?: return + val asset = selectedFieldAsset?.asset + if (currentVisit.status != "IN_PROGRESS") { + error = "La Inspección debe estar en curso para crear un Acta." + return + } + if (urgency !in setOf("URGENT", "NON_URGENT")) { + error = "Elegí si el Acta es urgente o no urgente." + return + } + if (acts.any { it.status == "DRAFT" }) { + error = "Ya existe un Acta en elaboración. Cerrala o cancelala antes de crear la siguiente." + return + } + launchBusy { + val created = actsRepository.create( + currentVisit.id, + asset?.id, + currentVisit.code, + urgency, + ) + selectedAct = created + actClosure = actsRepository.closure(created.id) + loadActsInternal(currentVisit.id, selectDraft = false) + val urgencyLabel = if (urgency == "URGENT") "urgente" else "no urgente" + notice = "${created.code} creada como $urgencyLabel. Los Hallazgos nuevos quedarán vinculados explícitamente a esta Acta." + if (asset != null && selectedFieldAsset?.capture?.readyForFinding == true) { + loadFindingOptionsInternal(currentVisit.id, asset.id, created.id) + } + } } - fun reloadSelectedActClosure() { - val actId = selectedAct?.id ?: return - launchBusy { actClosure = closureRepository.get(actId) } + fun createAct(urgency: String = "NON_URGENT") = createActForSelectedInventory(urgency) + + fun searchInventory(search: String, parentId: String? = null) { + val currentVisit = visit ?: return + launchBusy { + inventory = repository.fieldInventory(currentVisit.id, search, parentId).data + } } - fun selectExistingFieldAsset(assetId: String) { + fun loadFieldTypes(parentId: String? = null) { + val currentVisit = visit ?: return + launchBusy { + val effectiveParentId = parentId ?: currentVisit.scopeAsset?.id ?: currentVisit.operationalArea?.id + inventoryParentId = effectiveParentId + fieldTypes = repository.fieldTypes(currentVisit.id, parentId).data + inventory = repository.fieldInventory(currentVisit.id, null, effectiveParentId).data + } + } + + private suspend fun reloadCurrentInventory(visitId: String) { + val effectiveParentId = inventoryParentId ?: visit?.scopeAsset?.id ?: visit?.operationalArea?.id + inventory = repository.fieldInventory(visitId, null, effectiveParentId).data + } + + fun selectExisting(item: FieldInventoryItem) { val visitId = visit?.id ?: return launchBusy { - selectedFieldAsset = repository.selectFieldAsset(visitId, assetId) + selectedFieldAsset = repository.selectFieldAsset(visitId, item.id) notice = "Inventario agregado a la Inspección." reloadCurrentInventory(visitId) val draft = selectedDraftAct() if (selectedFieldAsset?.capture?.readyForFinding == true && draft != null) { - selectedAct = actsRepository.ensureAsset(draft.id, assetId) + selectedAct = actsRepository.ensureAsset(draft.id, item.id) loadActsInternal(visitId, selectDraft = false) - loadFindingOptionsInternal(visitId, assetId, draft.id) + loadFindingOptionsInternal(visitId, item.id, draft.id) } else if (selectedFieldAsset?.capture?.readyForFinding == true) { notice = "Inventario listo. Creá o seleccioná un Acta antes de registrar Hallazgos." } @@ -484,19 +540,19 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { } } - fun sealSelectedAct() { + fun closeSelectedAct() { + val currentVisit = visit ?: return val actId = selectedAct?.id ?: return - val visitId = visit?.id ?: return launchBusy { - selectedAct = actsRepository.seal(actId) - actClosure = closureRepository.get(actId) - loadActsInternal(visitId, selectDraft = false) + actClosure = actsRepository.seal(actId) + refreshSelectedActInternal(actId) + loadActsInternal(currentVisit.id, selectDraft = false) clearFindingState() notice = "${selectedAct?.code ?: "Acta"} firmada y cerrada. Podés crear otra Acta o continuar hacia el cierre de la Inspección." } } - fun closeCurrentVisit() { + fun closeInspection() { val visitId = visit?.id ?: return launchBusy { visit = actsRepository.closeVisit(visitId) @@ -505,9 +561,80 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { } } - fun clearMessages() { + fun clearFindingFlow() { + fieldFindingOptions = null + lastCreatedFinding = null + fieldFindingEvidence = emptyMap() error = null - notice = null + } + + fun clearSelectedFieldAsset() { + selectedFieldAsset = null + clearFindingState() + } + + private fun selectedDraftAct(): MobileActDetail? = + selectedAct?.takeIf { it.status == "DRAFT" } + ?: acts.firstOrNull { it.status == "DRAFT" }?.let { summary -> + selectedAct?.takeIf { it.id == summary.id && it.status == "DRAFT" } + } + + private suspend fun loadActsInternal(visitId: String, selectDraft: Boolean) { + acts = actsRepository.list(visitId).data + val currentId = selectedAct?.id + val current = currentId?.let { id -> acts.firstOrNull { it.id == id } } + val target = when { + current != null -> current.id + selectDraft -> acts.firstOrNull { it.status == "DRAFT" }?.id + else -> null + } + if (target != null) { + selectedAct = actsRepository.get(target) + actClosure = actsRepository.closure(target) + } else if (current == null) { + selectedAct = null + actClosure = null + } + } + + private suspend fun refreshSelectedActInternal(actId: String) { + selectedAct = actsRepository.get(actId) + actClosure = actsRepository.closure(actId) + visit?.id?.let { loadActsInternal(it, selectDraft = false) } + } + + private suspend fun loadFindingOptionsInternal( + visitId: String, + assetId: String, + actId: String, + keepLastCreated: Boolean = false, + ) { + val options = findingsRepository.options(visitId, assetId, actId) + fieldFindingOptions = options + if (!keepLastCreated) lastCreatedFinding = null + val loaded = linkedMapOf>() + for (finding in options.findings) { + loaded[finding.id] = findingsRepository.evidence(finding.id).data + } + fieldFindingEvidence = loaded + } + + private suspend fun loadEvidenceInternal(findingId: String) { + fieldFindingEvidence = fieldFindingEvidence + ( + findingId to findingsRepository.evidence(findingId).data + ) + } + + private fun clearActState() { + acts = emptyList() + selectedAct = null + actClosure = null + } + + private fun clearFindingState() { + fieldFindingOptions = null + lastCreatedFinding = null + fieldFindingEvidence = emptyMap() } private fun launchBusy(block: suspend () -> Unit) { @@ -518,61 +645,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { block() } catch (throwable: Throwable) { error = DhRepository.humanError(throwable) + if (repository.currentSession() == null) session = null } finally { busy = false } } } - - private suspend fun loadVisitsInternal() { - visits = inspectionRepository.mine().data - } - - private suspend fun loadActsInternal(visitId: String, selectDraft: Boolean = true) { - acts = actsRepository.list(visitId) - if (selectDraft) selectedAct = acts.firstOrNull { it.status == "DRAFT" } - } - - private suspend fun refreshSelectedActInternal(actId: String) { - selectedAct = actsRepository.detail(actId) - val visitId = visit?.id ?: return - loadActsInternal(visitId, selectDraft = false) - } - - private suspend fun reloadCurrentInventory(visitId: String) { - // El inventario de campo se recarga desde la pantalla moderna según el contexto actual. - } - - private fun selectedDraftAct(): InspectionAct? = when { - selectedAct?.status == "DRAFT" -> selectedAct - else -> acts.firstOrNull { it.status == "DRAFT" } - } - - private suspend fun loadFindingOptionsInternal( - visitId: String, - assetId: String, - actId: String, - keepLastCreated: Boolean = false, - ) { - findingOptions = findingsRepository.options(visitId, assetId, actId) - if (!keepLastCreated) lastCreatedFinding = null - if (lastCreatedFinding != null) loadEvidenceInternal(lastCreatedFinding!!.id) - } - - private suspend fun loadEvidenceInternal(findingId: String) { - findingEvidence = findingsRepository.evidence(findingId) - } - - private fun clearFindingState() { - findingOptions = null - selectedFieldAsset = null - lastCreatedFinding = null - findingEvidence = emptyList() - } - - private fun clearActState() { - acts = emptyList() - selectedAct = null - actClosure = null - } }