diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c59942be7..6c25c182f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -78,6 +78,10 @@ android:name="com.jnj.vaccinetracker.reportsoverview.vaccinesoverview.activity.VaccinesOverviewFlowActivity" android:windowSoftInputMode="adjustResize" /> + + (DiffCallback) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val inflater = LayoutInflater.from(parent.context) + val binding = ItemHmis105ReportRowBinding.inflate(inflater, parent, false) + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = getItem(position) + holder.bind(item) + } + + inner class ViewHolder(private val binding: ItemHmis105ReportRowBinding) : RecyclerView.ViewHolder(binding.root) { + fun bind(report: Hmis105ReportDTO) { + binding.textViewDoses.text = report.doses + if (isSectionHeader(report)) { + setNumericViewsVisibility(View.GONE) + binding.textViewUnder1Static.text = "" + binding.textViewUnder1Outreach.text = "" + binding.textView1to4Static.text = "" + binding.textView1to4Outreach.text = "" + binding.textView5to14Static.text = "" + binding.textView5to14Outreach.text = "" + binding.textViewTotal.text = "" + } else { + setNumericViewsVisibility(View.VISIBLE) + binding.textViewUnder1Static.text = report.under1Static.toString() + binding.textViewUnder1Outreach.text = report.under1Outreach.toString() + binding.textView1to4Static.text = report.age1to4Static.toString() + binding.textView1to4Outreach.text = report.age1to4Outreach.toString() + binding.textView5to14Static.text = report.age5to14Static.toString() + binding.textView5to14Outreach.text = report.age5to14Outreach.toString() + binding.textViewTotal.text = report.total.toString() + } + } + + private fun setNumericViewsVisibility(visibility: Int) { + binding.textViewUnder1Static.visibility = visibility + binding.textViewUnder1Outreach.visibility = visibility + binding.textView1to4Static.visibility = visibility + binding.textView1to4Outreach.visibility = visibility + binding.textView5to14Static.visibility = visibility + binding.textView5to14Outreach.visibility = visibility + binding.textViewTotal.visibility = visibility + } + + private fun isSectionHeader(report: Hmis105ReportDTO): Boolean { + return report.under1Static == 0 && + report.under1Outreach == 0 && + report.age1to4Static == 0 && + report.age1to4Outreach == 0 && + report.age5to14Static == 0 && + report.age5to14Outreach == 0 && + report.total == 0 && + report.doses == report.doses.uppercase() + } + } + + companion object DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: Hmis105ReportDTO, + newItem: Hmis105ReportDTO + ): Boolean = oldItem.doses == newItem.doses + + override fun areContentsTheSame( + oldItem: Hmis105ReportDTO, + newItem: Hmis105ReportDTO + ): Boolean = oldItem == newItem + } +} + diff --git a/app/src/main/java/com/jnj/vaccinetracker/reportsoverview/hmis105/dto/Hmis105ReportDTO.kt b/app/src/main/java/com/jnj/vaccinetracker/reportsoverview/hmis105/dto/Hmis105ReportDTO.kt new file mode 100644 index 000000000..5b00c2537 --- /dev/null +++ b/app/src/main/java/com/jnj/vaccinetracker/reportsoverview/hmis105/dto/Hmis105ReportDTO.kt @@ -0,0 +1,13 @@ +package com.jnj.vaccinetracker.reportsoverview.hmis105.dto + +data class Hmis105ReportDTO( + val doses: String, + val under1Static: Int = 0, + val under1Outreach: Int = 0, + val age1to4Static: Int = 0, + val age1to4Outreach: Int = 0, + val age5to14Static: Int = 0, + val age5to14Outreach: Int = 0, + val total: Int = 0 +) + diff --git a/app/src/main/java/com/jnj/vaccinetracker/reportsoverview/hmis105/model/Hmis105ViewModel.kt b/app/src/main/java/com/jnj/vaccinetracker/reportsoverview/hmis105/model/Hmis105ViewModel.kt new file mode 100644 index 000000000..e1b7d8633 --- /dev/null +++ b/app/src/main/java/com/jnj/vaccinetracker/reportsoverview/hmis105/model/Hmis105ViewModel.kt @@ -0,0 +1,485 @@ +package com.jnj.vaccinetracker.reportsoverview.hmis105.model + +import android.os.Bundle +import android.util.Log +import androidx.annotation.StringRes +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import com.jnj.vaccinetracker.R +import com.jnj.vaccinetracker.common.data.database.repositories.VisitRepository +import com.jnj.vaccinetracker.common.data.database.typealiases.addDaysToDate +import com.jnj.vaccinetracker.common.data.database.typealiases.getTodayMidnight +import com.jnj.vaccinetracker.common.data.models.Constants +import com.jnj.vaccinetracker.common.data.models.NavigationDirection +import com.jnj.vaccinetracker.common.data.repositories.UserRepository +import com.jnj.vaccinetracker.common.domain.entities.BirthDate +import com.jnj.vaccinetracker.common.domain.entities.ParticipantBase +import com.jnj.vaccinetracker.common.domain.entities.Visit +import com.jnj.vaccinetracker.common.domain.usecases.FindParticipantByParticipantUuidUseCase +import com.jnj.vaccinetracker.common.helpers.AppCoroutineDispatchers +import com.jnj.vaccinetracker.common.viewmodel.ViewModelWithState +import com.jnj.vaccinetracker.reportsoverview.hmis105.dto.Hmis105ReportDTO +import com.soywiz.klock.DateTime +import com.soywiz.klock.jvm.toDate +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.util.Date +import javax.inject.Inject + +class Hmis105ViewModel @Inject constructor( + userRepository: UserRepository, + private val visitRepository: VisitRepository, + private val findParticipantByParticipantUuidUseCase: FindParticipantByParticipantUuidUseCase, + override val dispatchers: AppCoroutineDispatchers +) : ViewModelWithState() { + + val reportDTOs = mutableLiveData>(emptyList()) + val isLoading = mutableLiveData(false) + val currentScreen = mutableLiveData() + val selectedStartDate = MutableLiveData(null) + val selectedEndDate = MutableLiveData(null) + var navigationDirection = NavigationDirection.NONE + + private var screens = listOf() + private val currentLocationUuid = userRepository.getDeviceNameSiteUuid() + + companion object { + private val HMIS105_VACCINES = mapOf( + "BCG Vxnaid Date" to "CL01. BCG", + "Hep B BD Vxnaid Date" to "CL02. Hep B BD", + "PAB for Td Vxnaid Date" to "CL03. PAB for Td", + "Polio 0 Vxnaid Date" to "CL04. Polio 0", + "Polio 1 Vxnaid Date" to "CL05. Polio 1", + "Polio 2 Vxnaid Date" to "CL06. Polio 2", + "Polio 3 Vxnaid Date" to "CL07. Polio 3", + "IPV 1 Vxnaid Date" to "CL08. IPV 1", + "IPV 2 Vxnaid Date" to "CL09. IPV 2", + "DPT-HepB-Hib 1 Vxnaid Date" to "CL10. DPT-HepB-Hib 1", + "DPT-HepB-Hib 2 Vxnaid Date" to "CL11. DPT-HepB-Hib 2", + "DPT-HepB-Hib 3 Vxnaid Date" to "CL12. DPT-HepB-Hib 3", + "PCV 1 Vxnaid Date" to "CL13. PCV 1", + "PCV 2 Vxnaid Date" to "CL14. PCV 2", + "PCV 3 Vxnaid Date" to "CL15. PCV 3", + "Rota 1 Vxnaid Date" to "CL16. Rota 1", + "Rota 2 Vxnaid Date" to "CL17. Rota 2", + "Rota 3 Vxnaid Date" to "CL18. Rota 3", + "Yellow Fever Vxnaid Date" to "CL22. Yellow Fever", + "Measles Rubella 1 (MR1) Vxnaid Date" to "CL23. Measles Rubella 1 (MR1)" + ) + + private const val KEY_MR1 = "Measles Rubella 1 (MR1) Vxnaid Date" + private const val KEY_MR2 = "Measles Rubella 2 (MR2) Vxnaid Date" + private const val KEY_YELLOW_FEVER = "Yellow Fever Vxnaid Date" + private const val UUID_LLINS = "6de53ec6-bf3f-41fe-bf2e-e61447a6557a" + } + + init { + initScreens() + } + + fun getHMIS105Data(startDate: DateTime?, endDate: DateTime?) { + Log.d("Hmis105ViewModel", "getHMIS105Data called") + isLoading.value = true + viewModelScope.launch { + try { + val start = startDate?.toDate() ?: getTodayMidnight() + val end = endDate?.toDate() ?: addDaysToDate(getTodayMidnight(), 1) + + Log.d("Hmis105ViewModel", "Loading report data from $start to $end") + + val data = withContext(dispatchers.io) { + try { + val occurredVisits = visitRepository + .findAllVisitsByAttributeTypeAndValue( + Constants.ATTRIBUTE_VISIT_STATUS, + Constants.VISIT_STATUS_OCCURRED + ) + val participantsMap = buildParticipantsMap(occurredVisits) + val candidateVisits = occurredVisits.filter { visit -> + participantsMap[visit.participantUuid]?.locationUuid == currentLocationUuid + } + val allVisits = candidateVisits.filter { visit -> + visit.observations.values.any { obsValue -> + obsValue.dateTime.time in start.time until end.time + } + } + + Log.d("Hmis105ViewModel", "Total visits found: ${allVisits.size}") + + val allKeys = allVisits.flatMap { it.observations.keys }.toSet() + Log.d("Hmis105ViewModel", "All obs keys in filtered visits: $allKeys") + val reportData = mutableListOf() + + reportData.addAll(createHmis105ReportDTOList(allVisits, participantsMap, start, end)) + reportData.add(createFullyImmunized1Year(allVisits, participantsMap, start, end)) + reportData.add(createLLINSReport(allVisits, participantsMap, start, end)) + reportData.add(Hmis105ReportDTO(doses = "SECOND YEAR OF LIFE")) + reportData.add(createMR2Report(allVisits, participantsMap, start, end)) + reportData.add(createFullyImmunized2Years(allVisits, participantsMap, start, end)) + reportData + } catch (ex: Exception) { + Log.e("Hmis105ViewModel", "Repository error: ${ex.message}", ex) + emptyList() + } + } + + Log.d("Hmis105ViewModel", "Report data loaded: ${data.size} rows") + reportDTOs.value = data + isLoading.value = false + } catch (ex: Exception) { + Log.e("Hmis105ViewModel", "Unexpected error loading report data", ex) + reportDTOs.value = emptyList() + isLoading.value = false + } + } + } + + private suspend fun buildParticipantsMap(visits: List): Map { + val map = mutableMapOf() + for (visit in visits) { + if (!map.containsKey(visit.participantUuid)) { + map[visit.participantUuid] = + findParticipantByParticipantUuidUseCase.findByParticipantUuid(visit.participantUuid) + } + } + return map + } + + private fun calculateAgeInMonthsAt(birthDate: BirthDate, referenceDate: DateTime): Int { + val birth = birthDate.toDateTime() + return (referenceDate.yearInt - birth.yearInt) * 12 + + (referenceDate.month0 - birth.month0) + } + + private fun calculateAgeInYearsAt(birthDate: BirthDate, referenceDate: DateTime): Int { + return calculateAgeInMonthsAt(birthDate, referenceDate) / 12 + } + + private fun calculateChildAgeGroupAt(birthDate: BirthDate, visitDate: DateTime): String { + val ageInMonths = calculateAgeInMonthsAt(birthDate, visitDate) + val ageInYears = calculateAgeInYearsAt(birthDate, visitDate) + return when { + ageInMonths in 0..11 -> Constants.GROUP_AGE_FIRST + ageInMonths in 12..59 -> Constants.GROUP_AGE_SECOND + ageInYears in 5..14 -> Constants.GROUP_AGE_THIRD + else -> Constants.GROUP_AGE_FOURTH + } + } + + private fun createHmis105ReportDTOList( + visits: List, + participantsMap: Map, + startDate: Date, + endDate: Date + ): List { + + val reportRowsMap = mutableMapOf() + val nowDateTime = DateTime.now() // Use current time for age, matching SQL + + for (visit in visits) { + val participant = participantsMap[visit.participantUuid] ?: continue + + for ((key, obsValue) in visit.observations) { + if (obsValue.dateTime.time !in startDate.time until endDate.time) { + continue + } + + val hmisLabel = HMIS105_VACCINES[key] ?: continue + val ageGroup = calculateChildAgeGroupAt(participant.birthDate, nowDateTime) + val visitLocation = visit.visitLocation + + updateReportRow(reportRowsMap, hmisLabel, ageGroup, visitLocation) + Log.d("Hmis105ViewModel", "Mapped [$key] → [$hmisLabel] | age: $ageGroup | loc: $visitLocation") + } + } + return reportRowsMap.values.toList().sortedBy { it.doses } + } + private fun createFullyImmunized1Year( + visits: List, + participantsMap: Map, + startDate: Date, + endDate: Date + ): Hmis105ReportDTO { + + val yellowFeverRecipients = visits + .filter { visit -> + visit.observations.any { (key, obsValue) -> + key == KEY_YELLOW_FEVER && obsValue.dateTime.time in startDate.time until endDate.time + } + } + .map { it.participantUuid } + .toSet() + + val mr1Recipients = visits + .filter { visit -> + visit.observations.any { (key, obsValue) -> + key == KEY_MR1 && obsValue.dateTime.time in startDate.time until endDate.time + } + } + .map { it.participantUuid } + .toSet() + + Log.d("Hmis105ViewModel", "CL24 Yellow Fever recipients: ${yellowFeverRecipients.size}") + Log.d("Hmis105ViewModel", "CL24 MR1 recipients: ${mr1Recipients.size}") + + // Must have received BOTH vaccines (intersection) + val bothVaccinesUuids = yellowFeverRecipients.intersect(mr1Recipients) + Log.d("Hmis105ViewModel", "CL24 received both YF + MR1: ${bothVaccinesUuids.size}") + + var under1Static = 0 + var under1Outreach = 0 + + for (participantUuid in bothVaccinesUuids) { + val participant = participantsMap[participantUuid] ?: continue + + val mr1Visit = visits.firstOrNull { visit -> + visit.participantUuid == participantUuid && + visit.observations.any { (key, obsValue) -> + key == KEY_MR1 && obsValue.dateTime.time in startDate.time until endDate.time + } + } ?: continue + + val mr1ObsValue = mr1Visit.observations[KEY_MR1] ?: continue + val visitDateTime = DateTime(mr1ObsValue.dateTime.time) + val ageAtMR1 = calculateAgeInMonthsAt(participant.birthDate, visitDateTime) + + if (ageAtMR1 !in 8..12) { + Log.d("Hmis105ViewModel", "CL24 skip $participantUuid — age at MR1 was $ageAtMR1 months") + continue + } + + when (mr1Visit.visitLocation) { + Constants.VISIT_PLACE_STATIC -> under1Static++ + Constants.VISIT_PLACE_OUTREACH, + Constants.VISIT_PLACE_SCHOOL -> under1Outreach++ + else -> under1Static++ + } + } + + Log.d("Hmis105ViewModel", "CL24 Final — Static: $under1Static, Outreach: $under1Outreach") + return Hmis105ReportDTO( + doses = "CL24. Fully immunized by 1 year", + under1Static = under1Static, + under1Outreach = under1Outreach, + total = under1Static + under1Outreach + ) + } + + private fun createLLINSReport( + visits: List, + participantsMap: Map, + startDate: Date, + endDate: Date + ): Hmis105ReportDTO { + + var under1Static = 0 + var under1Outreach = 0 + + for (visit in visits) { + val participant = participantsMap[visit.participantUuid] ?: continue + val llinsEntry = visit.observations.entries.firstOrNull { (key, obs) -> + (key.contains(UUID_LLINS, ignoreCase = true) || + key.contains("LLIN", ignoreCase = true)) && + obs.value.trim().equals("yes", ignoreCase = true) && + obs.dateTime.time in startDate.time until endDate.time + } ?: continue + + val obsDateTime = DateTime(llinsEntry.value.dateTime.time) + val ageInMonths = calculateAgeInMonthsAt(participant.birthDate, obsDateTime) + if (ageInMonths !in 0..11) continue + + when (visit.visitLocation) { + Constants.VISIT_PLACE_STATIC -> under1Static++ + Constants.VISIT_PLACE_OUTREACH, + Constants.VISIT_PLACE_SCHOOL -> under1Outreach++ + } + } + + Log.d("Hmis105ViewModel", "CL25 LLINs — Static: $under1Static, Outreach: $under1Outreach") + return Hmis105ReportDTO( + doses = "CL25. No. received LLINs", + under1Static = under1Static, + under1Outreach = under1Outreach, + total = under1Static + under1Outreach + ) + } + + private fun createMR2Report( + visits: List, + participantsMap: Map, + startDate: Date, + endDate: Date + ): Hmis105ReportDTO { + + var age1to4Static = 0 + var age1to4Outreach = 0 + var age5to14Static = 0 + var age5to14Outreach = 0 + + for (visit in visits) { + val mr2Obs = visit.observations[KEY_MR2]?.takeIf { + it.dateTime.time in startDate.time until endDate.time + } ?: continue + + val participant = participantsMap[visit.participantUuid] ?: continue + val mr2DateTime = DateTime(mr2Obs.dateTime.time) + val ageGroup = calculateChildAgeGroupAt(participant.birthDate, mr2DateTime) + val visitLocation = visit.visitLocation + + when { + ageGroup == Constants.GROUP_AGE_SECOND && + visitLocation == Constants.VISIT_PLACE_STATIC -> age1to4Static++ + + ageGroup == Constants.GROUP_AGE_SECOND && + (visitLocation == Constants.VISIT_PLACE_OUTREACH || + visitLocation == Constants.VISIT_PLACE_SCHOOL) -> age1to4Outreach++ + + ageGroup == Constants.GROUP_AGE_THIRD && + visitLocation == Constants.VISIT_PLACE_STATIC -> age5to14Static++ + + ageGroup == Constants.GROUP_AGE_THIRD && + (visitLocation == Constants.VISIT_PLACE_OUTREACH || + visitLocation == Constants.VISIT_PLACE_SCHOOL) -> age5to14Outreach++ + } + } + + val total = age1to4Static + age1to4Outreach + age5to14Static + age5to14Outreach + Log.d("Hmis105ViewModel", "CL27 MR2 — 1-4 Static: $age1to4Static, 1-4 Outreach: $age1to4Outreach, " + + "5-14 Static: $age5to14Static, 5-14 Outreach: $age5to14Outreach, Total: $total") + + return Hmis105ReportDTO( + doses = "CL27. Measles Rubella 2 (MR2)", + age1to4Static = age1to4Static, + age1to4Outreach = age1to4Outreach, + age5to14Static = age5to14Static, + age5to14Outreach = age5to14Outreach, + total = total + ) + } + + private fun createFullyImmunized2Years( + visits: List, + participantsMap: Map, + startDate: Date, + endDate: Date + ): Hmis105ReportDTO { + + var age1to4Static = 0 + var age1to4Outreach = 0 + + for (visit in visits) { + val mr2Obs = visit.observations[KEY_MR2]?.takeIf { + it.dateTime.time in startDate.time until endDate.time + } ?: continue + val participant = participantsMap[visit.participantUuid] ?: continue + val mr2DateTime = DateTime(mr2Obs.dateTime.time) + val ageInMonths = calculateAgeInMonthsAt(participant.birthDate, mr2DateTime) + + if (ageInMonths !in 17..24) { + Log.d("Hmis105ViewModel", "CL28 skip ${visit.participantUuid} — age at MR2 was $ageInMonths months") + continue + } + + when (visit.visitLocation) { + Constants.VISIT_PLACE_STATIC -> age1to4Static++ + Constants.VISIT_PLACE_OUTREACH, + Constants.VISIT_PLACE_SCHOOL -> age1to4Outreach++ + else -> age1to4Static++ + } + } + + Log.d("Hmis105ViewModel", "CL28 Final — Static: $age1to4Static, Outreach: $age1to4Outreach") + return Hmis105ReportDTO( + doses = "CL28. Fully immunized by 2 years", + age1to4Static = age1to4Static, + age1to4Outreach = age1to4Outreach, + total = age1to4Static + age1to4Outreach + ) + } + + private fun updateReportRow( + reportRows: MutableMap, + vaccine: String, + ageGroup: String, + deliveryMode: String? + ) { + val currentRow = reportRows[vaccine] ?: Hmis105ReportDTO(doses = vaccine) + val mode = deliveryMode ?: Constants.VISIT_PLACE_STATIC + + val updatedRow = when { + ageGroup == Constants.GROUP_AGE_FIRST && mode == Constants.VISIT_PLACE_STATIC -> + currentRow.copy(under1Static = currentRow.under1Static + 1) + + ageGroup == Constants.GROUP_AGE_FIRST && + (mode == Constants.VISIT_PLACE_OUTREACH || mode == Constants.VISIT_PLACE_SCHOOL) -> + currentRow.copy(under1Outreach = currentRow.under1Outreach + 1) + + ageGroup == Constants.GROUP_AGE_SECOND && mode == Constants.VISIT_PLACE_STATIC -> + currentRow.copy(age1to4Static = currentRow.age1to4Static + 1) + + ageGroup == Constants.GROUP_AGE_SECOND && + (mode == Constants.VISIT_PLACE_OUTREACH || mode == Constants.VISIT_PLACE_SCHOOL) -> + currentRow.copy(age1to4Outreach = currentRow.age1to4Outreach + 1) + + ageGroup == Constants.GROUP_AGE_THIRD && mode == Constants.VISIT_PLACE_STATIC -> + currentRow.copy(age5to14Static = currentRow.age5to14Static + 1) + + ageGroup == Constants.GROUP_AGE_THIRD && + (mode == Constants.VISIT_PLACE_OUTREACH || mode == Constants.VISIT_PLACE_SCHOOL) -> + currentRow.copy(age5to14Outreach = currentRow.age5to14Outreach + 1) + + else -> currentRow + } + + val total = updatedRow.under1Static + updatedRow.under1Outreach + + updatedRow.age1to4Static + updatedRow.age1to4Outreach + + updatedRow.age5to14Static + updatedRow.age5to14Outreach + + reportRows[vaccine] = updatedRow.copy(total = total) + } + + + private fun initScreens() { + screens = createScreens() + setInitialScreen() + } + + private fun createScreens(): List { + return mutableListOf(Screen.HMIS105_REPORT) + } + + private fun setInitialScreen() { + if (currentScreen.get() == null) { + currentScreen.set(screens.firstOrNull()) + } + } + + enum class Screen(@StringRes val label: Int) { + HMIS105_REPORT(R.string.hmis105_report_title) + } + + override fun saveInstanceState(outState: Bundle) { + selectedStartDate.value?.let { outState.putString("selectedStartDate", it.toString()) } + selectedEndDate.value?.let { outState.putString("selectedEndDate", it.toString()) } + } + + override fun restoreInstanceState(savedInstanceState: Bundle) { + val startDateStr = savedInstanceState.getString("selectedStartDate") + val endDateStr = savedInstanceState.getString("selectedEndDate") + + if (startDateStr != null) { + try { + selectedStartDate.value = DateTime.parse(startDateStr).local + } catch (e: Exception) { + Log.e("Hmis105ViewModel", "Failed to parse start date: $startDateStr", e) + } + } + if (endDateStr != null) { + try { + selectedEndDate.value = DateTime.parse(endDateStr).local + } catch (e: Exception) { + Log.e("Hmis105ViewModel", "Failed to parse end date: $endDateStr", e) + } + } + } +} diff --git a/app/src/main/java/com/jnj/vaccinetracker/reportsoverview/hmis105/screens/Hmis105ReportFragment.kt b/app/src/main/java/com/jnj/vaccinetracker/reportsoverview/hmis105/screens/Hmis105ReportFragment.kt new file mode 100644 index 000000000..157b00dca --- /dev/null +++ b/app/src/main/java/com/jnj/vaccinetracker/reportsoverview/hmis105/screens/Hmis105ReportFragment.kt @@ -0,0 +1,285 @@ +package com.jnj.vaccinetracker.reportsoverview.hmis105.screens + +import android.annotation.SuppressLint +import android.os.Build +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.annotation.RequiresApi +import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.viewModels +import androidx.lifecycle.LifecycleOwner +import androidx.recyclerview.widget.LinearLayoutManager +import com.jnj.vaccinetracker.R +import com.jnj.vaccinetracker.common.data.database.typealiases.dateNow +import com.jnj.vaccinetracker.common.dialogs.ReportOverviewDatePickerDialog +import com.jnj.vaccinetracker.common.ui.BaseFragment +import com.jnj.vaccinetracker.common.util.DateUtil +import com.jnj.vaccinetracker.common.util.FileUtil +import com.jnj.vaccinetracker.databinding.FragmentHmis105ReportBinding +import com.jnj.vaccinetracker.reportsoverview.hmis105.adapters.Hmis105Adapter +import com.jnj.vaccinetracker.reportsoverview.hmis105.dto.Hmis105ReportDTO +import com.jnj.vaccinetracker.reportsoverview.hmis105.model.Hmis105ViewModel +import com.soywiz.klock.DateTime +import com.soywiz.klock.DateFormat +import com.soywiz.klock.jvm.toDate +import org.apache.poi.hssf.usermodel.HSSFWorkbook + +@RequiresApi(Build.VERSION_CODES.Q) +class Hmis105ReportFragment : BaseFragment(), + ReportOverviewDatePickerDialog.VisitsOverviewDatePickerListener { + + companion object { + private const val TAG = "Hmis105ReportFragment" + private const val START_DATE_PICKER_DIALOG_TAG = "startDatePickerHmis105" + private const val END_DATE_PICKER_DIALOG_TAG = "endDatePickerHmis105" + } + + private lateinit var binding: FragmentHmis105ReportBinding + private lateinit var adapter: Hmis105Adapter + private val viewModel: Hmis105ViewModel by viewModels { viewModelFactory } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + Log.d(TAG, "onCreateView called, savedInstanceState: ${savedInstanceState != null}") + setHasOptionsMenu(true) + binding = DataBindingUtil.inflate(inflater, R.layout.fragment_hmis105_report, container, false) + binding.lifecycleOwner = viewLifecycleOwner + + try { + setupRecyclerView() + if (savedInstanceState != null) { + Log.d(TAG, "Restoring ViewModel state from savedInstanceState") + viewModel.restoreInstanceState(savedInstanceState) + } + if (viewModel.selectedStartDate.value == null && viewModel.selectedEndDate.value == null) { + Log.d(TAG, "Initializing default dates") + initializeDefaultDates() + } else { + Log.d(TAG, "Dates already set, updating labels") + updateDateLabels() + } + setupDateButtons() + setupDownloadButton() + Log.d(TAG, "Fragment setup completed successfully") + } catch (ex: Exception) { + Log.e(TAG, "Error during fragment setup", ex) + Toast.makeText(requireContext(), getString(R.string.hmis105_initialization_error), Toast.LENGTH_LONG).show() + } + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + Log.d(TAG, "onViewCreated called, savedInstanceState: ${savedInstanceState != null}") + + try { + (activity as? AppCompatActivity)?.supportActionBar?.apply { + title = getString(R.string.hmis105_report_title) + setDisplayHomeAsUpEnabled(true) + setHomeButtonEnabled(true) + } + if (viewModel.reportDTOs.value.isNullOrEmpty() && + viewModel.selectedStartDate.value != null && + viewModel.selectedEndDate.value != null) { + Log.d(TAG, "Loading report data") + loadReportData() + } else if (!viewModel.reportDTOs.value.isNullOrEmpty()) { + Log.d(TAG, "Data already loaded, not reloading (configuration change)") + } + } catch (ex: Exception) { + Log.e(TAG, "Error in onViewCreated", ex) + Toast.makeText(requireContext(), getString(R.string.hmis105_load_error), Toast.LENGTH_LONG).show() + } + } + + override fun observeViewModel(lifecycleOwner: LifecycleOwner) { + Log.d(TAG, "observeViewModel called") + try { + viewModel.reportDTOs.observe(lifecycleOwner) { data -> + Log.d(TAG, "Report data updated: ${data?.size ?: 0} rows") + if (::adapter.isInitialized) { + adapter.submitList(data) + } else { + Log.w(TAG, "Adapter not initialized when data arrived") + } + } + + viewModel.isLoading.observe(lifecycleOwner) { isLoading -> + Log.d(TAG, "Loading state: $isLoading") + if (::binding.isInitialized) { + binding.progressBar.visibility = if (isLoading == true) View.VISIBLE else View.GONE + } + } + } catch (ex: Exception) { + Log.e(TAG, "Error in observeViewModel: ${ex.message}", ex) + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + android.R.id.home -> { + activity?.onBackPressedDispatcher?.onBackPressed() + true + } + else -> super.onOptionsItemSelected(item) + } + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + Log.d(TAG, "onSaveInstanceState called") + viewModel.saveInstanceState(outState) + } + + private fun setupRecyclerView() { + adapter = Hmis105Adapter() + binding.recyclerViewReport.layoutManager = LinearLayoutManager(requireContext()) + binding.recyclerViewReport.adapter = adapter + } + + private fun initializeDefaultDates() { + val today = DateTime.now() + val monthStart = DateTime(today.yearInt, today.month, 1) + viewModel.selectedStartDate.value = monthStart + viewModel.selectedEndDate.value = today + updateDateLabels() + } + + private fun updateDateLabels() { + binding.labelStartDate.text = formatDate(viewModel.selectedStartDate.value) + binding.labelEndDate.text = formatDate(viewModel.selectedEndDate.value) + } + + private fun setupDateButtons() { + binding.btnStartDate.setOnClickListener { + showDatePickerDialog(true) + } + binding.btnEndDate.setOnClickListener { + showDatePickerDialog(false) + } + } + + private fun setupDownloadButton() { + binding.btnExportExcel.setOnClickListener { + exportToExcel() + } + } + + private fun showDatePickerDialog(isStartDate: Boolean) { + val datePickerDialog = ReportOverviewDatePickerDialog( + selectedDate = if (isStartDate) viewModel.selectedStartDate.value else viewModel.selectedEndDate.value + ) + datePickerDialog.show( + childFragmentManager, + if (isStartDate) START_DATE_PICKER_DIALOG_TAG else END_DATE_PICKER_DIALOG_TAG + ) + } + + override fun onDatePicked(date: DateTime?, tag: String?) { + date?.let { + if (tag == START_DATE_PICKER_DIALOG_TAG) { + viewModel.selectedStartDate.value = it + binding.labelStartDate.text = formatDate(it) + } else if (tag == END_DATE_PICKER_DIALOG_TAG) { + viewModel.selectedEndDate.value = it + binding.labelEndDate.text = formatDate(it) + } + if (viewModel.selectedStartDate.value != null && viewModel.selectedEndDate.value != null) { + loadReportData() + } + } + } + + private fun loadReportData() { + Log.d(TAG, "loadReportData called") + viewModel.getHMIS105Data(viewModel.selectedStartDate.value, viewModel.selectedEndDate.value) + } + + + @SuppressLint("SimpleDateFormat") + private fun exportToExcel() { + val data = viewModel.reportDTOs.value ?: emptyList() + if (data.isEmpty()) { + Toast.makeText(requireContext(), getString(R.string.no_data_to_export), Toast.LENGTH_SHORT).show() + return + } + + val fileName = "${buildFileName()}.xls" + val mimeType = "application/vnd.ms-excel" + + FileUtil.exportToFile(requireContext(), fileName, mimeType) { outputStream -> + HSSFWorkbook().use { workbook -> + val sheet = workbook.createSheet(getString(R.string.hmis105_report_title).replace(" ", "_")) + + val headerRow = sheet.createRow(0) + headerRow.createCell(0).setCellValue("Doses") + headerRow.createCell(1).setCellValue("Under 1 - Static") + headerRow.createCell(2).setCellValue("Under 1 - Outreach/School") + headerRow.createCell(3).setCellValue("1-4 Years - Static") + headerRow.createCell(4).setCellValue("1-4 Years - Outreach/School") + headerRow.createCell(5).setCellValue("5-14 Years - Static") + headerRow.createCell(6).setCellValue("5-14 Years - Outreach/School") + headerRow.createCell(7).setCellValue("Total") + + data.forEachIndexed { index, report -> + val row = sheet.createRow(index + 1) + row.createCell(0).setCellValue(report.doses) + if (!isSectionHeader(report)) { + row.createCell(1).setCellValue(report.under1Static.toDouble()) + row.createCell(2).setCellValue(report.under1Outreach.toDouble()) + row.createCell(3).setCellValue(report.age1to4Static.toDouble()) + row.createCell(4).setCellValue(report.age1to4Outreach.toDouble()) + row.createCell(5).setCellValue(report.age5to14Static.toDouble()) + row.createCell(6).setCellValue(report.age5to14Outreach.toDouble()) + row.createCell(7).setCellValue(report.total.toDouble()) + } + } + + sheet.setColumnWidth(0, 4000) + sheet.setColumnWidth(1, 4000) + sheet.setColumnWidth(2, 4500) + sheet.setColumnWidth(3, 4000) + sheet.setColumnWidth(4, 4500) + sheet.setColumnWidth(5, 4000) + sheet.setColumnWidth(6, 4500) + sheet.setColumnWidth(7, 4000) + + workbook.write(outputStream) + } + } + } + + private fun isSectionHeader(report: Hmis105ReportDTO): Boolean { + return report.under1Static == 0 && + report.under1Outreach == 0 && + report.age1to4Static == 0 && + report.age1to4Outreach == 0 && + report.age5to14Static == 0 && + report.age5to14Outreach == 0 && + report.total == 0 && + report.doses == report.doses.uppercase() + } + + private fun buildFileName(): String { + return "${getString(R.string.hmis105_report_title).replace(" ", "_")}_${ + DateUtil.convertDateToString( + dateNow(), + DateFormat.FORMAT_DATE.toString() + ) + }" + } + + private fun formatDate(date: DateTime?): String { + return date?.let { DateUtil.convertDateToString(it.toDate(), DateFormat.FORMAT_DATE.toString()) } ?: "N/A" + } +} diff --git a/app/src/main/res/layout-sw600dp-land/fragment_hmis105_report.xml b/app/src/main/res/layout-sw600dp-land/fragment_hmis105_report.xml new file mode 100644 index 000000000..9e635f7f8 --- /dev/null +++ b/app/src/main/res/layout-sw600dp-land/fragment_hmis105_report.xml @@ -0,0 +1,231 @@ + + + + + + + + + + + +