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 daeac63..5124554 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 @@ -16,6 +16,11 @@ 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.MobileResponsibleRequest import com.korexlabs.dhinspeccion.data.StoredSession import com.korexlabs.dhinspeccion.data.VisitDetail import com.korexlabs.dhinspeccion.data.VisitSummary @@ -26,6 +31,7 @@ import java.time.Instant class MainViewModel(application: Application) : AndroidViewModel(application) { private val repository = DhRepository(application) private val findingsRepository = FieldFindingsRepository(application) + private val actsRepository = MobileActsRepository(application) var session: StoredSession? by mutableStateOf(repository.currentSession()) private set @@ -48,6 +54,13 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { var selectedFieldAsset: FieldAssetDetail? by mutableStateOf(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) @@ -85,6 +98,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { inventory = emptyList() fieldTypes = emptyList() selectedFieldAsset = null + clearActState() clearFindingState() } } @@ -101,6 +115,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { fieldTypes = emptyList() selectedFieldAsset = null clearFindingState() + loadActsInternal(id, selectDraft = true) } fun closeVisitView() { @@ -108,6 +123,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { inventory = emptyList() fieldTypes = emptyList() selectedFieldAsset = null + clearActState() clearFindingState() loadVisits() } @@ -117,10 +133,51 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { launchBusy { visit = repository.startVisit(id) notice = "Inspección iniciada." + loadActsInternal(id, selectDraft = true) loadVisitsInternal() } } + fun reloadActs() { + val visitId = visit?.id ?: return + launchBusy { loadActsInternal(visitId, selectDraft = selectedAct == null) } + } + + fun selectAct(actId: String) { + launchBusy { + selectedAct = actsRepository.get(actId) + actClosure = actsRepository.closure(actId) + clearFindingState() + } + } + + fun createActForSelectedInventory() { + 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 (asset == null) { + error = "Seleccioná primero una Instalación o Subinstalación para iniciar el Acta." + return + } + if (acts.any { it.status == "DRAFT" }) { + error = "Ya existe un Acta en borrador. Cerrala o cancelala antes de crear la siguiente." + return + } + launchBusy { + val created = actsRepository.create(currentVisit.id, asset.id, currentVisit.code) + selectedAct = created + actClosure = actsRepository.closure(created.id) + loadActsInternal(currentVisit.id, selectDraft = false) + notice = "${created.code} creada. Los Hallazgos nuevos quedarán vinculados explícitamente a esta Acta." + if (selectedFieldAsset?.capture?.readyForFinding == true) { + loadFindingOptionsInternal(currentVisit.id, asset.id, created.id) + } + } + } + fun searchInventory(search: String, parentId: String? = null) { val id = visit?.id ?: return launchBusy { @@ -141,8 +198,13 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { selectedFieldAsset = repository.selectFieldAsset(visitId, item.id) notice = "Inventario agregado a la inspección." inventory = repository.fieldInventory(visitId, null, null).data - if (selectedFieldAsset?.capture?.readyForFinding == true) { - loadFindingOptionsInternal(visitId, item.id) + val draft = selectedDraftAct() + if (selectedFieldAsset?.capture?.readyForFinding == true && draft != null) { + selectedAct = actsRepository.ensureAsset(draft.id, item.id) + loadActsInternal(visitId, selectDraft = false) + loadFindingOptionsInternal(visitId, item.id, draft.id) + } else if (selectedFieldAsset?.capture?.readyForFinding == true) { + notice = "Inventario listo. Creá o seleccioná un Acta antes de registrar Hallazgos." } } } @@ -207,8 +269,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { selectedFieldAsset = repository.selectFieldAsset(visitId, result.canonical.id) notice = "Fusión registrada. Se conserva ${result.canonical.code} y la historia de ${result.source.code} permanece trazable." inventory = repository.fieldInventory(visitId, null, null).data - if (selectedFieldAsset?.capture?.readyForFinding == true) { - loadFindingOptionsInternal(visitId, result.canonical.id) + val draft = selectedDraftAct() + if (selectedFieldAsset?.capture?.readyForFinding == true && draft != null) { + selectedAct = actsRepository.ensureAsset(draft.id, result.canonical.id) + loadActsInternal(visitId, selectDraft = false) + loadFindingOptionsInternal(visitId, result.canonical.id, draft.id) } } } @@ -237,8 +302,13 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { "Fotografía registrada." } inventory = repository.fieldInventory(visitId, null, null).data - if (response.capture.readyForFinding) { - loadFindingOptionsInternal(visitId, asset.id) + val draft = selectedDraftAct() + if (response.capture.readyForFinding && draft != null) { + selectedAct = actsRepository.ensureAsset(draft.id, asset.id) + loadActsInternal(visitId, selectDraft = false) + loadFindingOptionsInternal(visitId, asset.id, draft.id) + } else if (response.capture.readyForFinding) { + notice = "Inventario listo. Creá o seleccioná un Acta antes de registrar Hallazgos." } } } @@ -246,7 +316,16 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { fun openFindingForSelected() { val visitId = visit?.id ?: return val assetId = selectedFieldAsset?.asset?.id ?: return - launchBusy { loadFindingOptionsInternal(visitId, assetId) } + val draft = selectedDraftAct() + if (draft == null) { + error = "Creá o seleccioná el Acta en borrador antes de registrar Hallazgos." + return + } + launchBusy { + selectedAct = actsRepository.ensureAsset(draft.id, assetId) + loadActsInternal(visitId, selectDraft = false) + loadFindingOptionsInternal(visitId, assetId, draft.id) + } } fun createFieldFinding( @@ -259,6 +338,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { ) { val visitId = visit?.id ?: return val assetId = selectedFieldAsset?.asset?.id ?: return + val actId = selectedDraftAct()?.id + if (actId == null) { + error = "No hay un Acta en borrador seleccionada." + return + } if (description.isBlank()) { error = "Describí el Hallazgo antes de guardarlo." return @@ -272,10 +356,12 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { return } launchBusy { + selectedAct = actsRepository.ensureAsset(actId, assetId) val response = findingsRepository.create( visitId, assetId, CreateFieldFindingRequest( + actId = actId, catalogItemId = catalogItemId, customTitle = customTitle?.trim()?.takeIf { it.isNotBlank() }, customLegalBasis = customLegalBasis?.trim()?.takeIf { it.isNotBlank() }, @@ -285,8 +371,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { ), ) lastCreatedFinding = response.finding - notice = "Hallazgo ${response.finding.code} registrado. Podés agregar evidencia fotográfica." - loadFindingOptionsInternal(visitId, assetId, keepLastCreated = true) + notice = "Hallazgo ${response.finding.code} registrado en ${response.act.code}. Podés agregar evidencia fotográfica." + loadFindingOptionsInternal(visitId, assetId, actId, keepLastCreated = true) + loadActsInternal(visitId, selectDraft = false) } } @@ -318,6 +405,139 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { launchBusy { loadEvidenceInternal(findingId) } } + fun setCompanyResponsiblePresent( + fullName: String, + documentType: String, + documentNumber: String, + position: String, + email: String?, + phone: String?, + ) { + val actId = selectedAct?.id ?: return + if (fullName.isBlank() || documentNumber.isBlank() || position.isBlank()) { + error = "Completá nombre, documento y cargo del responsable de la empresa." + return + } + launchBusy { + actClosure = actsRepository.setResponsible( + actId, + MobileResponsibleRequest( + attendanceStatus = "PRESENT", + fullName = fullName.trim(), + documentType = documentType, + documentNumber = documentNumber.trim(), + position = position.trim(), + email = email?.trim()?.takeIf { it.isNotBlank() }, + phone = phone?.trim()?.takeIf { it.isNotBlank() }, + ), + ) + notice = "Responsable de empresa registrado para el Acta." + } + } + + fun setCompanyResponsibleAbsent(reason: String) { + val actId = selectedAct?.id ?: return + if (reason.trim().length < 10) { + error = "Indicá un motivo de ausencia de al menos 10 caracteres." + return + } + launchBusy { + actClosure = actsRepository.setResponsible( + actId, + MobileResponsibleRequest( + attendanceStatus = "ABSENT", + absenceReason = reason.trim(), + ), + ) + notice = "Ausencia del responsable registrada." + } + } + + fun prepareSelectedAct() { + val actId = selectedAct?.id ?: return + launchBusy { + actClosure = actsRepository.prepare(actId) + refreshSelectedActInternal(actId) + notice = "Acta preparada. Su contenido quedó congelado para las firmas." + } + } + + fun reopenSelectedAct() { + val actId = selectedAct?.id ?: return + launchBusy { + actClosure = actsRepository.reopen(actId) + refreshSelectedActInternal(actId) + notice = "Acta reabierta. Podés corregirla antes de volver a preparar." + } + } + + fun signSelectedActAsInspector( + png: File, + latitude: Double?, + longitude: Double?, + accuracyM: Double?, + ) { + val actId = selectedAct?.id ?: return + launchBusy { + actClosure = actsRepository.signInspector(actId, png, latitude, longitude, accuracyM) + notice = "Firma del inspector incorporada al Acta." + } + } + + fun signSelectedActAsCompany( + png: File, + latitude: Double?, + longitude: Double?, + accuracyM: Double?, + manifestation: String, + statement: String?, + ) { + val actId = selectedAct?.id ?: return + launchBusy { + actClosure = actsRepository.signCompany( + actId, png, latitude, longitude, accuracyM, manifestation, statement, + ) + notice = if (manifestation == "DISSENT") { + "Firma de empresa registrada con disidencia." + } else { + "Firma de empresa registrada." + } + } + } + + fun recordCompanyOutcome(status: String, reason: String) { + val actId = selectedAct?.id ?: return + if (reason.trim().length < 10) { + error = "Indicá un motivo de al menos 10 caracteres." + return + } + launchBusy { + actClosure = actsRepository.companyOutcome(actId, status, reason) + notice = if (status == "ABSENT") "Ausencia de empresa asentada." else "Negativa a firmar asentada." + } + } + + fun closeSelectedAct() { + val currentVisit = visit ?: return + val actId = selectedAct?.id ?: return + launchBusy { + actClosure = actsRepository.closeAct(actId) + refreshSelectedActInternal(actId) + loadActsInternal(currentVisit.id, selectDraft = false) + clearFindingState() + notice = "${selectedAct?.code ?: "Acta"} cerrada e inmutable. Podés crear otra Acta o finalizar la inspección." + } + } + + fun closeInspection() { + val visitId = visit?.id ?: return + launchBusy { + visit = actsRepository.closeVisit(visitId) + loadVisitsInternal() + notice = "Inspección cerrada. Las Actas y documentos quedan disponibles para oficina." + } + } + fun clearFindingFlow() { fieldFindingOptions = null lastCreatedFinding = null @@ -330,12 +550,43 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { 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) + val options = findingsRepository.options(visitId, assetId, actId) fieldFindingOptions = options if (!keepLastCreated) lastCreatedFinding = null val loaded = linkedMapOf>() @@ -351,6 +602,12 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { ) } + private fun clearActState() { + acts = emptyList() + selectedAct = null + actClosure = null + } + private fun clearFindingState() { fieldFindingOptions = null lastCreatedFinding = null