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 e7ad119..5201dad 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,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 = 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 by mutableStateOf(emptyList()) + var acts: List = emptyList() private set - var visit: VisitDetail? by mutableStateOf(null) + var selectedAct: InspectionAct? = null private set - - var inventory: List by mutableStateOf(emptyList()) + var actClosure: ActAdministrationClosure? = null private set - var inventoryParentId: String? by mutableStateOf(null) + var findingOptions: FieldFindingOptionsResponse? = null private set - var fieldTypes: List by mutableStateOf(emptyList()) + var selectedFieldAsset: FieldAssetDetail? = null private set - var selectedFieldAsset: FieldAssetDetail? by mutableStateOf(null) + var lastCreatedFinding: InspectionFinding? = null private set - - 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()) + var findingEvidence: List = 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>() - 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 + } }