fix(android): limpiar firma y terminología final de Actas

This commit is contained in:
2026-09-11 12:35:02 -03:00
parent 753fa9915f
commit 92a21d8f15
@@ -1,230 +1,174 @@
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.MobileActClosure
import com.korexlabs.dhinspeccion.data.MobileActDetail
import com.korexlabs.dhinspeccion.data.MobileActSummary
import com.korexlabs.dhinspeccion.data.MobileActsRepository
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.MobileResponsibleRequest
import com.korexlabs.dhinspeccion.data.StoredSession
import com.korexlabs.dhinspeccion.data.MobileSession
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 findingsRepository = FieldFindingsRepository(application)
private val actsRepository = MobileActsRepository(application)
private val inspectionRepository = MobileInspectionRepository(application)
private val actsRepository = MobileActRepository(application)
private val closureRepository = MobileActClosureRepository(application)
private val findingsRepository = MobileFieldFindingRepository(application)
var session: StoredSession? by mutableStateOf(repository.currentSession())
var session: MobileSession? = null
private set
var busy by mutableStateOf(false)
var visits: List<VisitSummary> = emptyList()
private set
var error: String? by mutableStateOf(null)
var visit: VisitDetail? = null
private set
var notice: String? by mutableStateOf(null)
var busy: Boolean = false
private set
var error: String? = null
private set
var notice: String? = null
private set
var visits: List<VisitSummary> by mutableStateOf(emptyList())
var acts: List<InspectionAct> = emptyList()
private set
var visit: VisitDetail? by mutableStateOf(null)
var selectedAct: InspectionAct? = null
private set
var inventory: List<FieldInventoryItem> by mutableStateOf(emptyList())
var actClosure: ActAdministrationClosure? = null
private set
var inventoryParentId: String? by mutableStateOf(null)
var findingOptions: FieldFindingOptionsResponse? = null
private set
var fieldTypes: List<FieldType> by mutableStateOf(emptyList())
var selectedFieldAsset: FieldAssetDetail? = null
private set
var selectedFieldAsset: FieldAssetDetail? by mutableStateOf(null)
var lastCreatedFinding: InspectionFinding? = null
private set
var acts: List<MobileActSummary> 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<String, List<FieldFindingEvidence>> by mutableStateOf(emptyMap())
var findingEvidence: List<FieldFindingEvidence> = emptyList()
private set
init {
session = repository.currentSession()
if (session != null) loadVisits()
}
fun clearMessages() {
error = null
notice = null
}
fun login(identifier: String, password: String) {
fun login(identifier: String, password: String, onSuccess: () -> Unit = {}) {
if (identifier.isBlank() || password.isBlank()) {
error = "Ingresá usuario y contraseña."
error = "Completá usuario y contraseña."
return
}
launchBusy {
session = repository.login(identifier, password)
notice = "Sesión iniciada."
session = repository.login(identifier.trim(), password)
loadVisitsInternal()
onSuccess()
}
}
fun logout() {
viewModelScope.launch {
runCatching { repository.logout() }
session = null
visits = emptyList()
visit = null
inventory = emptyList()
inventoryParentId = null
fieldTypes = emptyList()
selectedFieldAsset = null
clearActState()
clearFindingState()
}
repository.logout()
session = null
visits = emptyList()
visit = null
clearActState()
error = null
notice = null
}
fun loadVisits() = launchBusy { loadVisitsInternal() }
private suspend fun loadVisitsInternal() {
visits = repository.visits().data
}
fun openVisit(id: String) = launchBusy {
visit = repository.visit(id)
inventory = emptyList()
inventoryParentId = visit?.scopeAsset?.id ?: visit?.operationalArea?.id
fieldTypes = emptyList()
selectedFieldAsset = null
visit = inspectionRepository.detail(id)
loadActsInternal(id)
reloadCurrentInventory(id)
clearFindingState()
loadActsInternal(id, selectDraft = true)
}
fun closeVisitView() {
visit = null
inventory = emptyList()
inventoryParentId = null
fieldTypes = emptyList()
selectedFieldAsset = null
clearActState()
clearFindingState()
loadVisits()
}
fun startVisit() {
fun refreshVisit() {
val id = visit?.id ?: return
launchBusy {
visit = repository.startVisit(id)
inventoryParentId = visit?.scopeAsset?.id ?: visit?.operationalArea?.id
notice = "Inspección iniciada."
loadActsInternal(id, selectDraft = true)
loadVisitsInternal()
visit = inspectionRepository.detail(id)
loadActsInternal(id, selectDraft = false)
reloadCurrentInventory(id)
}
}
fun reloadActs() {
fun backToVisits() {
visit = null
clearActState()
clearFindingState()
notice = null
error = null
}
fun startCurrentVisit() {
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."
}
}
fun createAct(urgency: String) {
val visitId = visit?.id ?: return
launchBusy { loadActsInternal(visitId, selectDraft = selectedAct == null) }
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."
}
}
fun selectAct(actId: String) {
launchBusy {
selectedAct = actsRepository.get(actId)
actClosure = actsRepository.closure(actId)
clearFindingState()
val found = acts.firstOrNull { it.id == actId } ?: return
selectedAct = found
clearFindingState()
actClosure = null
if (found.status != "DRAFT") {
reloadSelectedActClosure()
}
}
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 borrador. Bloqueala 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 closeSelectedActView() {
selectedAct = null
clearFindingState()
actClosure = null
}
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 reloadSelectedActClosure() {
val actId = selectedAct?.id ?: return
launchBusy { actClosure = closureRepository.get(actId) }
}
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) {
fun selectExistingFieldAsset(assetId: String) {
val visitId = visit?.id ?: return
launchBusy {
selectedFieldAsset = repository.selectFieldAsset(visitId, item.id)
selectedFieldAsset = repository.selectFieldAsset(visitId, assetId)
notice = "Inventario agregado a la Inspección."
reloadCurrentInventory(visitId)
val draft = selectedDraftAct()
if (selectedFieldAsset?.capture?.readyForFinding == true && draft != null) {
selectedAct = actsRepository.ensureAsset(draft.id, item.id)
selectedAct = actsRepository.ensureAsset(draft.id, assetId)
loadActsInternal(visitId, selectDraft = false)
loadFindingOptionsInternal(visitId, item.id, draft.id)
loadFindingOptionsInternal(visitId, assetId, draft.id)
} else if (selectedFieldAsset?.capture?.readyForFinding == true) {
notice = "Inventario listo. Creá o seleccioná un Acta antes de registrar Hallazgos."
}
@@ -342,7 +286,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
val assetId = selectedFieldAsset?.asset?.id ?: return
val draft = selectedDraftAct()
if (draft == null) {
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."
return
}
launchBusy {
@@ -364,7 +308,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
val assetId = selectedFieldAsset?.asset?.id ?: return
val actId = selectedDraftAct()?.id
if (actId == null) {
error = "No hay un Acta en borrador seleccionada."
error = "No hay un Acta en elaboración seleccionada."
return
}
if (description.isBlank()) {
@@ -473,7 +417,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
absenceReason = reason.trim(),
),
)
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."
}
}
@@ -495,7 +439,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
latitude: Double?,
longitude: Double?,
accuracyM: Double?,
description: String? = null,
) {
val actId = selectedAct?.id ?: return
launchBusy {
@@ -541,19 +484,19 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
}
}
fun closeSelectedAct() {
val currentVisit = visit ?: return
fun sealSelectedAct() {
val actId = selectedAct?.id ?: return
val visitId = visit?.id ?: return
launchBusy {
actClosure = actsRepository.seal(actId)
refreshSelectedActInternal(actId)
loadActsInternal(currentVisit.id, selectDraft = false)
selectedAct = actsRepository.seal(actId)
actClosure = closureRepository.get(actId)
loadActsInternal(visitId, 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 closeInspection() {
fun closeCurrentVisit() {
val visitId = visit?.id ?: return
launchBusy {
visit = actsRepository.closeVisit(visitId)
@@ -562,80 +505,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
}
}
fun clearFindingFlow() {
fieldFindingOptions = null
lastCreatedFinding = null
fieldFindingEvidence = emptyMap()
fun clearMessages() {
error = 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<String, List<FieldFindingEvidence>>()
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()
notice = null
}
private fun launchBusy(block: suspend () -> Unit) {
@@ -646,10 +518,61 @@ 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
}
}