From 7c8c15c7997f0b6c9aa79484a19c5a5aa09e9814 Mon Sep 17 00:00:00 2001 From: "[vinald]" Date: Mon, 16 Mar 2026 06:44:43 +0300 Subject: [PATCH 01/23] feat: Add HMIS 105 Vaccine & Malaria Report feature - Create new HMIS 105 report module with data aggregation by age groups and delivery modes - Implement date range filtering with start/end date pickers - Add Excel export functionality for report data - Create ViewModel with LiveData for reactive state management - Add Repository layer for data fetching and aggregation - Implement RecyclerView adapter for tabular display - Create HorizontalScrollView layout for wide table - Add HMIS 105 button to Reports Overview - Register HmisMalaria105FlowActivity in AndroidManifest - Add string resources for UI text - Fix all resource linking errors (dimensions and colors) Age groups: 0-11mo, 12-59mo, 5-14yr Delivery modes: Static, Outreach, School Follows MVVM architecture and integrates seamlessly with existing Reports. --- app/src/main/AndroidManifest.xml | 4 + .../screens/ReportsOverviewFragment.kt | 10 + .../hmis105/activity/Hmis105FlowActivity.kt | 36 +++ .../hmis105/adapters/Hmis105Adapter.kt | 49 ++++ .../hmis105/dto/Hmis105ObservationDTO.kt | 13 ++ .../hmis105/dto/Hmis105ReportDTO.kt | 13 ++ .../hmis105/model/Hmis105ViewModel.kt | 79 +++++++ .../hmis105/repository/Hmis105Repository.kt | 159 +++++++++++++ .../hmis105/screens/Hmis105ReportFragment.kt | 214 ++++++++++++++++++ app/src/main/res/layout/activity_flow.xml | 20 ++ .../res/layout/fragment_hmis105_report.xml | 211 +++++++++++++++++ .../res/layout/fragment_reports_overview.xml | 14 ++ .../res/layout/item_hmis105_report_row.xml | 78 +++++++ app/src/main/res/values/strings.xml | 9 +- 14 files changed, 908 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/jnj/vaccinetracker/reportsoverview/hmis105/activity/Hmis105FlowActivity.kt create mode 100644 app/src/main/java/com/jnj/vaccinetracker/reportsoverview/hmis105/adapters/Hmis105Adapter.kt create mode 100644 app/src/main/java/com/jnj/vaccinetracker/reportsoverview/hmis105/dto/Hmis105ObservationDTO.kt create mode 100644 app/src/main/java/com/jnj/vaccinetracker/reportsoverview/hmis105/dto/Hmis105ReportDTO.kt create mode 100644 app/src/main/java/com/jnj/vaccinetracker/reportsoverview/hmis105/model/Hmis105ViewModel.kt create mode 100644 app/src/main/java/com/jnj/vaccinetracker/reportsoverview/hmis105/repository/Hmis105Repository.kt create mode 100644 app/src/main/java/com/jnj/vaccinetracker/reportsoverview/hmis105/screens/Hmis105ReportFragment.kt create mode 100644 app/src/main/res/layout/activity_flow.xml create mode 100644 app/src/main/res/layout/fragment_hmis105_report.xml create mode 100644 app/src/main/res/layout/item_hmis105_report_row.xml 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 + 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() + } + } + + 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/Hmis105ObservationDTO.kt b/app/src/main/java/com/jnj/vaccinetracker/reportsoverview/hmis105/dto/Hmis105ObservationDTO.kt new file mode 100644 index 000000000..98854b326 --- /dev/null +++ b/app/src/main/java/com/jnj/vaccinetracker/reportsoverview/hmis105/dto/Hmis105ObservationDTO.kt @@ -0,0 +1,13 @@ +package com.jnj.vaccinetracker.reportsoverview.hmis105.dto + +import com.jnj.vaccinetracker.common.domain.entities.BirthDate +import java.util.Date + +data class Hmis105ObservationDTO( + val vaccineName: String, + val visitDate: Date, + val birthDate: BirthDate, + val visitLocation: String, // Static, Outreach, School + val participantUuid: String +) + 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..6b3fbba83 --- /dev/null +++ b/app/src/main/java/com/jnj/vaccinetracker/reportsoverview/hmis105/model/Hmis105ViewModel.kt @@ -0,0 +1,79 @@ +package com.jnj.vaccinetracker.reportsoverview.hmis105.model + +import android.os.Bundle +import androidx.annotation.StringRes +import androidx.lifecycle.viewModelScope +import com.jnj.vaccinetracker.R +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.NavigationDirection +import com.jnj.vaccinetracker.common.data.repositories.UserRepository +import com.jnj.vaccinetracker.common.helpers.AppCoroutineDispatchers +import com.jnj.vaccinetracker.common.viewmodel.ViewModelWithState +import com.jnj.vaccinetracker.reportsoverview.hmis105.dto.Hmis105ReportDTO +import com.jnj.vaccinetracker.reportsoverview.hmis105.repository.Hmis105Repository +import com.soywiz.klock.DateTime +import com.soywiz.klock.jvm.toDate +import kotlinx.coroutines.launch +import javax.inject.Inject + +class Hmis105ViewModel @Inject constructor( + userRepository: UserRepository, + private val hmisMalaria105Repository: Hmis105Repository, + override val dispatchers: AppCoroutineDispatchers +) : ViewModelWithState() { + + val reportDTOs = mutableLiveData>() + val isLoading = mutableLiveData(false) + val currentScreen = mutableLiveData() + var navigationDirection = NavigationDirection.NONE + + private var screens = listOf() + private val currentLocationUuid = userRepository.getDeviceNameSiteUuid() + + init { + initScreens() + } + + fun getHmisMalaria105Data(startDate: DateTime?, endDate: DateTime?) { + isLoading.value = true + viewModelScope.launch { + try { + val start = startDate?.toDate() ?: getTodayMidnight() + val end = endDate?.toDate() ?: addDaysToDate(getTodayMidnight(), 1) + + val data = hmisMalaria105Repository.getHmisMalaria105ReportData(start, end, currentLocationUuid) + reportDTOs.value = data + } catch (ex: Exception) { + reportDTOs.value = emptyList() + } finally { + isLoading.value = false + } + } + } + + private fun initScreens() { + screens = createScreens() + setInitialScreen() + } + + private fun createScreens(): List { + return mutableListOf(Screen.HMIS105_REPORT) + } + + private fun setInitialScreen() { + if (currentScreen.get() == null) { + val screen = screens.firstOrNull() + currentScreen.set(screen) + } + } + + enum class Screen(@StringRes val label: Int) { + HMIS105_REPORT(R.string.hmis105_report_title) + } + + override fun saveInstanceState(outState: Bundle) {} + + override fun restoreInstanceState(savedInstanceState: Bundle) {} +} + diff --git a/app/src/main/java/com/jnj/vaccinetracker/reportsoverview/hmis105/repository/Hmis105Repository.kt b/app/src/main/java/com/jnj/vaccinetracker/reportsoverview/hmis105/repository/Hmis105Repository.kt new file mode 100644 index 000000000..722daabfb --- /dev/null +++ b/app/src/main/java/com/jnj/vaccinetracker/reportsoverview/hmis105/repository/Hmis105Repository.kt @@ -0,0 +1,159 @@ +package com.jnj.vaccinetracker.reportsoverview.hmis105.repository + +import com.jnj.vaccinetracker.common.data.database.repositories.DraftVisitEncounterRepository +import com.jnj.vaccinetracker.common.data.database.repositories.VisitRepository +import com.jnj.vaccinetracker.common.data.managers.ConfigurationManager +import com.jnj.vaccinetracker.common.data.models.Constants +import com.jnj.vaccinetracker.common.domain.entities.BirthDate +import com.jnj.vaccinetracker.common.domain.entities.DraftVisitEncounter +import com.jnj.vaccinetracker.common.domain.entities.ObservationValue +import com.jnj.vaccinetracker.common.domain.entities.Visit +import com.jnj.vaccinetracker.common.domain.usecases.FindParticipantByParticipantUuidUseCase +import com.jnj.vaccinetracker.reportsoverview.hmis105.dto.Hmis105ReportDTO +import com.soywiz.klock.DateTime +import java.util.Date +import javax.inject.Inject + +class Hmis105Repository @Inject constructor( + private val visitRepository: VisitRepository, + private val draftVisitEncounterRepository: DraftVisitEncounterRepository, + private val findParticipantByParticipantUuidUseCase: FindParticipantByParticipantUuidUseCase, + private val configurationManager: ConfigurationManager +) { + + companion object { + // HMIS 105 Report Vaccine Concept Names (These should be mapped to actual concept UUIDs in your system) + private val HMIS105_VACCINES = mapOf( + "BCG Vxnaid" to "CL01. BCG", + "Hep B BD Vxnaid" to "CL02. Hep B BD", + "PAB for Td Vxnaid" to "CL03. PAB for Td", + "Polio 0 Vxnaid" to "CL04. Polio 0", + "Polio 1 Vxnaid" to "CL05. Polio 1", + "Polio 2 Vxnaid" to "CL06. Polio 2", + "Polio 3 Vxnaid" to "CL07. Polio 3", + "IPV 1 Vxnaid" to "CL08. IPV 1", + "IPV 2 Vxnaid" to "CL09. IPV 2", + "DPT-HepB-Hib 1 Vxnaid" to "CL10. DPT-HepB-Hib 1", + "DPT-HepB-Hib 2 Vxnaid" to "CL11. DPT-HepB-Hib 2", + "DPT-HepB-Hib 3 Vxnaid" to "CL12. DPT-HepB-Hib 3", + "PCV 1 Vxnaid" to "CL13. PCV 1", + "PCV 2 Vxnaid" to "CL14. PCV 2", + "PCV 3 Vxnaid" to "CL15. PCV 3", + "Rota 1 Vxnaid" to "CL16. Rota 1", + "Rota 2 Vxnaid" to "CL17. Rota 2", + "Rota 3 Vxnaid" to "CL18. Rota 3", + "Yellow Fever Vxnaid" to "CL22. Yellow Fever", + "Measles Rubella 1 Vxnaid" to "CL23. Measles Rubella 1 (MR1)", + "Measles Rubella 2 Vxnaid" to "CL27. Measles Rubella 2 (MR2)" + ) + } + + suspend fun getHmisMalaria105ReportData( + startDate: Date, + endDate: Date, + locationUuid: String? = null + ): List { + try { + val vaccineConfig = configurationManager.getSubstancesConfig() + .filter { it.category == Constants.VACCINES_CATEGORY_NAME } + + val visitDates = getVisitDatesInRange(startDate, endDate) + val reportRows = mutableMapOf() + + for (visit in visitDates) { + val participant = findParticipantByParticipantUuidUseCase.findByParticipantUuid(visit.participantUuid) + if (participant == null) continue + + val ageGroup = calculateAgeGroup(participant.birthDate, visit.startDatetime) + val deliveryMode = visit.visitLocation ?: Constants.VISIT_PLACE_STATIC + + for ((key, observation) in visit.observations) { + val vaccineConceptName = vaccineConfig.find { key == "${it.conceptName} ${Constants.DATE_STR}" } + if (vaccineConceptName != null) { + val reportLabel = HMIS105_VACCINES[vaccineConceptName.conceptName] ?: vaccineConceptName.label + updateReportRow(reportRows, reportLabel, ageGroup, deliveryMode) + } + } + } + + return reportRows.values.toList() + .sortedWith(compareBy { it.doses }.thenBy { it.doses }) + } catch (ex: Exception) { + return emptyList() + } + } + + private suspend fun getVisitDatesInRange(startDate: Date, endDate: Date): List { + val occurredVisits = visitRepository + .findAllVisitsByAttributeTypeAndValue(Constants.ATTRIBUTE_VISIT_STATUS, Constants.VISIT_STATUS_OCCURRED) + .filter { visit -> + visit.startDatetime.time in startDate.time..endDate.time + } + + val draftVisitEncounters = draftVisitEncounterRepository.findVisitsBeforeDate(endDate) + .filter { it.startDatetime.time >= startDate.time } + val draftVisitEncountersAsVisits = draftVisitEncounters.map { convertDraftVisitEncounterToVisit(it) } + + return occurredVisits + draftVisitEncountersAsVisits + } + + private fun convertDraftVisitEncounterToVisit(draftVisitEncounter: DraftVisitEncounter): Visit { + return Visit( + visitUuid = draftVisitEncounter.visitUuid, + startDatetime = draftVisitEncounter.startDatetime, + visitType = draftVisitEncounter.visitType, + participantUuid = draftVisitEncounter.participantUuid, + attributes = draftVisitEncounter.attributes, + observations = draftVisitEncounter.observations.mapValues { entry -> + ObservationValue(entry.value, draftVisitEncounter.startDatetime) + }, + dateModified = Date(System.currentTimeMillis()) + ) + } + + private fun calculateAgeGroup(birthDate: BirthDate, visitDate: Date): String { + val visitDateTime = DateTime.fromUnix(visitDate.time) + val birthDateTime = birthDate.toDateTime() + + val ageInMonths = (visitDateTime.yearInt - birthDateTime.yearInt) * 12 + (visitDateTime.month0 - birthDateTime.month0) + val ageInYears = ageInMonths / 12 + + return when { + ageInMonths < 12 -> Constants.GROUP_AGE_FIRST // "0-11 months" + ageInMonths < 60 -> Constants.GROUP_AGE_SECOND // "12-59 months" + ageInYears <= 14 -> Constants.GROUP_AGE_THIRD // "5-14 years" + else -> Constants.GROUP_AGE_FOURTH // "14+ years" + } + } + + private fun updateReportRow( + reportRows: MutableMap, + label: String, + ageGroup: String, + deliveryMode: String + ) { + val currentRow = reportRows[label] ?: Hmis105ReportDTO(doses = label) + + val updatedRow = when { + ageGroup == Constants.GROUP_AGE_FIRST && deliveryMode == Constants.VISIT_PLACE_STATIC -> + currentRow.copy(under1Static = currentRow.under1Static + 1) + ageGroup == Constants.GROUP_AGE_FIRST && (deliveryMode == Constants.VISIT_PLACE_OUTREACH || deliveryMode == Constants.VISIT_PLACE_SCHOOL) -> + currentRow.copy(under1Outreach = currentRow.under1Outreach + 1) + ageGroup == Constants.GROUP_AGE_SECOND && deliveryMode == Constants.VISIT_PLACE_STATIC -> + currentRow.copy(age1to4Static = currentRow.age1to4Static + 1) + ageGroup == Constants.GROUP_AGE_SECOND && (deliveryMode == Constants.VISIT_PLACE_OUTREACH || deliveryMode == Constants.VISIT_PLACE_SCHOOL) -> + currentRow.copy(age1to4Outreach = currentRow.age1to4Outreach + 1) + ageGroup == Constants.GROUP_AGE_THIRD && deliveryMode == Constants.VISIT_PLACE_STATIC -> + currentRow.copy(age5to14Static = currentRow.age5to14Static + 1) + ageGroup == Constants.GROUP_AGE_THIRD && (deliveryMode == Constants.VISIT_PLACE_OUTREACH || deliveryMode == 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[label] = updatedRow.copy(total = total) + } +} + 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..8974e3a06 --- /dev/null +++ b/app/src/main/java/com/jnj/vaccinetracker/reportsoverview/hmis105/screens/Hmis105ReportFragment.kt @@ -0,0 +1,214 @@ +package com.jnj.vaccinetracker.reportsoverview.hmis105.screens + +import android.annotation.SuppressLint +import android.os.Build +import android.os.Bundle +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.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.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 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 } + + private var selectedStartDate: DateTime? = null + private var selectedEndDate: DateTime? = null + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + setHasOptionsMenu(true) + binding = DataBindingUtil.inflate(inflater, R.layout.fragment_hmis105_report, container, false) + binding.lifecycleOwner = viewLifecycleOwner + + setupRecyclerView() + initializeDefaultDates() + setupDateButtons() + setupDownloadButton() + loadReportData() + observeViewModel() + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + (activity as AppCompatActivity).supportActionBar?.apply { + title = getString(R.string.hmis105_report_title) + setDisplayHomeAsUpEnabled(true) + setHomeButtonEnabled(true) + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + android.R.id.home -> { + activity?.onBackPressedDispatcher?.onBackPressed() + true + } + else -> super.onOptionsItemSelected(item) + } + } + + 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) + selectedStartDate = monthStart + selectedEndDate = today + + binding.labelStartDate.text = formatDate(selectedStartDate) + binding.labelEndDate.text = formatDate(selectedEndDate) + } + + 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) selectedStartDate else selectedEndDate + ) + 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) { + selectedStartDate = it + binding.labelStartDate.text = formatDate(selectedStartDate) + } else if (tag == END_DATE_PICKER_DIALOG_TAG) { + selectedEndDate = it + binding.labelEndDate.text = formatDate(selectedEndDate) + } + if (selectedStartDate != null && selectedEndDate != null) { + loadReportData() + } + } + } + + private fun loadReportData() { + viewModel.getHmisMalaria105Data(selectedStartDate, selectedEndDate) + } + + private fun observeViewModel() { + viewModel.reportDTOs.observe(viewLifecycleOwner) { data -> + adapter.submitList(data) + } + + viewModel.isLoading.observe(viewLifecycleOwner) { isLoading -> + binding.progressBar.visibility = if (isLoading == true) View.VISIBLE else View.GONE + } + } + + @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 = "HMIS105_Report_${DateUtil.convertDateToString(dateNow(), DateFormat.FORMAT_DATE.toString())}.xls" + val mimeType = "application/vnd.ms-excel" + + FileUtil.exportToFile(requireContext(), fileName, mimeType) { outputStream -> + val workbook = HSSFWorkbook() + val sheet = workbook.createSheet(getString(R.string.hmis105_report_title).replace(" ", "_")) + + // Create header row + val headerRow = sheet.createRow(0) + val headers = listOf( + "Doses", + "Under 1 - Static", + "Under 1 - Outreach/School", + "1-4 Years - Static", + "1-4 Years - Outreach/School", + "5-14 Years - Static", + "5-14 Years - Outreach/School", + "Total" + ) + + headers.forEachIndexed { index, header -> + headerRow.createCell(index).setCellValue(header) + } + + // Create data rows + data.forEachIndexed { index, report -> + val row = sheet.createRow(index + 1) + row.createCell(0).setCellValue(report.doses) + 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()) + } + + // Auto-resize columns + repeat(headers.size) { sheet.autoSizeColumn(it) } + + workbook.write(outputStream) + workbook.close() + + Toast.makeText(requireContext(), getString(R.string.file_exported_successfully), Toast.LENGTH_SHORT).show() + } + } + + 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/activity_flow.xml b/app/src/main/res/layout/activity_flow.xml new file mode 100644 index 000000000..56e5507a5 --- /dev/null +++ b/app/src/main/res/layout/activity_flow.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_hmis105_report.xml b/app/src/main/res/layout/fragment_hmis105_report.xml new file mode 100644 index 000000000..3fb78122b --- /dev/null +++ b/app/src/main/res/layout/fragment_hmis105_report.xml @@ -0,0 +1,211 @@ + + + + + + + + + + + + +