diff --git a/app/build.gradle b/app/build.gradle index f9f3166..91d80e8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,6 +7,7 @@ plugins { id 'androidx.navigation.safeargs.kotlin' id 'kotlin-kapt' id 'com.google.firebase.crashlytics' + id 'kotlin-parcelize' } //apply plugin: 'kotlin-android' diff --git a/app/src/main/java/com/example/hpostesting/FHIRFormater/FHIR converter.kt b/app/src/main/java/com/example/hpostesting/FHIRFormater/FHIR converter.kt new file mode 100644 index 0000000..7de6487 --- /dev/null +++ b/app/src/main/java/com/example/hpostesting/FHIRFormater/FHIR converter.kt @@ -0,0 +1,254 @@ +package com.example.hpostesting.FHIRFormater + +import com.example.hpostesting.data.model.patient.HemoCubeTestData +import com.example.hpostesting.data.model.patient.UserData +import org.json.JSONArray +import org.json.JSONObject + + + +class RecommendedFHIRConverter { + + private fun createPatient(data: HemoCubeTestData, useStandardCodes: Boolean): JSONObject { + val patient = JSONObject() + patient.put("resourceType", "Patient") + patient.put("id", "patient-${data.sampleid}") + + // Structured name + val nameArray = JSONArray() + val nameObj = JSONObject() + nameObj.put("use", "official") + nameObj.put("text", data.name) + nameArray.put(nameObj) + patient.put("name", nameArray) + + // Gender (FHIR uses "male", "female", "other", "unknown") + patient.put("gender", data.gender.lowercase()) + + // Birth date estimation from age (optional: validate or remove if unknown) + val birthYear = try { + 2025 - data.age.toInt() + } catch (e: Exception) { + null + } + if (birthYear != null) patient.put("birthDate", "$birthYear-01-01") + + // ABHA ID as identifier + if (data.abhaId.isNotBlank()) { + val identifiers = JSONArray() + val identifier = JSONObject() + identifier.put("system", "https://healthid.ndhm.gov.in") + identifier.put("value", data.abhaId) + identifiers.put(identifier) + patient.put("identifier", identifiers) + } + + return patient + } + + + private fun createDevice(data: HemoCubeTestData): JSONObject { + val device = JSONObject() + device.put("resourceType", "Device") + device.put("id", "device-${data.deviceSerialNumber}") + device.put("serialNumber", data.deviceSerialNumber) + + // Device type as codeable concept + val type = JSONObject() + type.put("text", data.deviceType) + device.put("type", type) + + // Device version + if (!data.appVersion.isNullOrBlank()) { + val versions = JSONArray() + val version = JSONObject() + version.put("type", JSONObject().put("text", "appVersion")) + version.put("value", data.appVersion) + versions.put(version) + device.put("version", versions) + } + + return device + } + + + private fun createHemoglobinObservation(data: HemoCubeTestData, useStandardCodes: Boolean): JSONObject { + val observation = JSONObject() + observation.put("resourceType", "Observation") + observation.put("id", "hemoglobin-${data.sampleid}") + observation.put("status", if (data.testStatus == true) "final" else "preliminary") + + // Standard LOINC code or fallback + val code = JSONObject() + val codingArray = JSONArray() + if (useStandardCodes) { + val coding = JSONObject() + coding.put("system", "http://loinc.org") + coding.put("code", "718-7") // LOINC for Hemoglobin [Mass/volume] in Blood + coding.put("display", "Hemoglobin [Mass/volume] in Blood") + codingArray.put(coding) + } + code.put("coding", codingArray) + code.put("text", "Hemoglobin Analysis") + observation.put("code", code) + + // Subject reference to patient + observation.put("subject", JSONObject().put("reference", "Patient/patient-${data.sampleid}")) + + // Device reference + observation.put("device", JSONObject().put("reference", "Device/device-${data.deviceSerialNumber}")) + + // Time of observation + if (!data.testTime.isNullOrBlank()) { + observation.put("effectiveDateTime", data.testTime) + } + + // Main result: if hb3 or hb4 exists, use as value + val hbValue = data.hb3 ?: data.hb4 + if (hbValue != null) { + val valueQuantity = JSONObject() + valueQuantity.put("value", hbValue) + valueQuantity.put("unit", "g/dL") + valueQuantity.put("system", "http://unitsofmeasure.org") + valueQuantity.put("code", "g/dL") + observation.put("valueQuantity", valueQuantity) + } + + // Optional extensions + val components = JSONArray() + + fun addComponent(codeText: String, value: Double?) { + if (value != null) { + val comp = JSONObject() + comp.put("code", JSONObject().put("text", codeText)) + comp.put("valueQuantity", JSONObject().put("value", value)) + components.put(comp) + } + } + + addComponent("HB3", data.hb3) + addComponent("HB4", data.hb4) + addComponent("Device Ratio", data.deviceRatio) + addComponent("Blood Group", null) // can't use non-numeric here unless coded properly + + if (components.length() > 0) { + observation.put("component", components) + } + + return observation + } + + private fun createBundleEntry(resource: JSONObject): JSONObject { + val entry = JSONObject() + entry.put("fullUrl", "urn:uuid:${resource.getString("id")}") + entry.put("resource", resource) + entry.put("request", JSONObject().apply { + put("method", "POST") + put("url", resource.getString("resourceType")) + }) + return entry + } + + fun convertToFHIR(data: HemoCubeTestData, useStandardCodes: Boolean = false): String { + val bundle = JSONObject() + bundle.put("resourceType", "Bundle") + bundle.put("id", "hemocube-test-${data.sampleid}") + bundle.put("type", "collection") + + val entries = JSONArray() + + entries.put(createBundleEntry(createPatient(data, useStandardCodes))) + entries.put(createBundleEntry(createDevice(data))) + entries.put(createBundleEntry(createHemoglobinObservation(data, useStandardCodes))) + + bundle.put("entry", entries) + return bundle.toString(2) + } + +} + + + + +fun sus() { + val sampleData = HemoCubeTestData( + sampleid = 1234, + _id = "local-001", + name = "John Doe", + incubationTime = "15", + bloodGroup = "B+", + age = "30", + state = "Karnataka", + abhaId = "29-1234-4567", + userImageURL = "https://example.com/user.jpg", + location = UserData.Location(latitude = 12.9716, longitude = 77.5946), + reportUploadTime = "2025-07-15T09:30:00+05:30", + testType = "HEMOCUBE", + testTime = "2025-07-15T09:15:00+05:30", + testStatus = true, + gender = "male", + localFlag = true, + deviceId = "device-5678", + appVersion = "1.4.2", + deviceSerialNumber = "SN-001-XY", + deviceType = "HEMOCUBE", + kitSerial = "KIT-20250715", + resultData = "Raw data goes here", + led1Buffer = 0.123, + led2Buffer = 0.456, + led3Buffer = 0.789, + led4Buffer = 0.321, + led1Sample = 0.654, + led2Sample = 0.987, + led3Sample = 0.432, + led4Sample = 0.210, + led1Average = 0.300, + led2Average = 0.500, + led3Average = 0.400, + led4Average = 0.450, + abs1 = 0.12, + abs2 = 0.23, + abs3 = 0.34, + abs4 = 0.45, + hb3 = 13.5, + hb4 = 13.7, + led1Gain1 = 1.1, + led2Gain1 = 1.2, + led3Gain1 = 1.3, + led4Gain1 = 1.4, + deviceRatio = 0.87, + calculatedRatio = 0.89, + predictedDenovixRatio = 0.91, + slopeRatio = 0.93, + coefficients = "a=1.0,b=2.0,c=3.0", + classificationResult = "Normal", + prdClassification = "PRD1", + deviceRatioClass = "A", + slopeRatioClass = "B", + borderlineMethod2Class = "C", + errorMessages = "", + batteryLevel = "85%", + batteryCapacity = "2800mAh", + batteryMaxCapacity = "3000mAh", + batteryTemperature = "35C", + batteryVoltage = "3.7V", + molbioFlag = false, + quickCapture = false, + solution = "Blood Sample", + concentration = "13.5g/dL", + filter = "None", + volume = "20uL", + isCSVCreated = true, + labName = "ABC Labs", + cuvetteSize = "Standard", + district = "Bangalore Urban", + centerName = "Health Center 1", + ipAddress = "192.168.1.101", + configUpdatedRecent = "true" + ) + + val converter = RecommendedFHIRConverter() + val standardFhir = converter.convertToFHIR(sampleData, useStandardCodes = true) + + println("FHIR Bundle Output:\n$standardFhir") +} diff --git a/app/src/main/java/com/example/hpostesting/data/model/reportgen/ReportActivity.kt b/app/src/main/java/com/example/hpostesting/data/model/reportgen/ReportActivity.kt new file mode 100644 index 0000000..4400af1 --- /dev/null +++ b/app/src/main/java/com/example/hpostesting/data/model/reportgen/ReportActivity.kt @@ -0,0 +1,21 @@ +package com.example.hpostesting.data.model.reportgen + +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import com.example.hpostesting.data.model.reportgen.ui.report.ReportFragment +import dagger.hilt.android.AndroidEntryPoint +import `in`.sminnovations.hpostesting.R +@AndroidEntryPoint +class ReportActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.report_gen) + + if (savedInstanceState == null) { + supportFragmentManager.beginTransaction() + .replace(R.id.container, ReportFragment()) //Direct constructor + .commitNow() + } + } +} diff --git a/app/src/main/java/com/example/hpostesting/data/model/reportgen/ui/report/BufferAdapter.kt b/app/src/main/java/com/example/hpostesting/data/model/reportgen/ui/report/BufferAdapter.kt new file mode 100644 index 0000000..c8ca75e --- /dev/null +++ b/app/src/main/java/com/example/hpostesting/data/model/reportgen/ui/report/BufferAdapter.kt @@ -0,0 +1,70 @@ +package com.example.hpostesting.data.model.reportgen.ui.report + +import android.graphics.Color +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.cardview.widget.CardView +import androidx.recyclerview.widget.RecyclerView +import com.example.hpostesting.data.dao.HemoCubeBufferDao +import com.example.hpostesting.data.model.patient.BufferCheckData +import com.example.hpostesting.data.model.patient.HemoCubeTestData +import `in`.sminnovations.hpostesting.R +class BufferAdapter( + private var dataList: List, + private val onItemClick: (HemoCubeTestData) -> Unit +) : RecyclerView.Adapter() { + + private var selectedItems: List = emptyList() + + inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { + val cardView: CardView = view.findViewById(R.id.cardView) + val sampleId: TextView = view.findViewById(R.id.sampleId) + val testTime: TextView = view.findViewById(R.id.testTime) + val result: TextView = view.findViewById(R.id.result) + val selectionIndicator: View = view.findViewById(R.id.selectionIndicator) + + init { + cardView.setOnClickListener { + if (adapterPosition != RecyclerView.NO_POSITION) { + onItemClick(dataList[adapterPosition]) + } + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.item_buffer_card, parent, false) + ) + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = dataList[position] + val isSelected = selectedItems.contains(item) + + holder.sampleId.text = "Sample ID: ${item._id}" + holder.testTime.text = "Time: ${item.testTime}" + holder.result.text = "Result: ${item.classificationResult}" + + // Update selection appearance + if (isSelected) { + holder.cardView.setCardBackgroundColor(Color.parseColor("#E3F2FD")) + holder.selectionIndicator.visibility = View.VISIBLE + } else { + holder.cardView.setCardBackgroundColor(Color.WHITE) + holder.selectionIndicator.visibility = View.GONE + } + } + + override fun getItemCount() = dataList.size + + fun updateList(newList: List) { + dataList = newList + notifyDataSetChanged() + } + + fun updateSelectedItems(newSelectedItems: List) { + selectedItems = newSelectedItems + notifyDataSetChanged() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/hpostesting/data/model/reportgen/ui/report/ReportFragment.kt b/app/src/main/java/com/example/hpostesting/data/model/reportgen/ui/report/ReportFragment.kt new file mode 100644 index 0000000..623d01e --- /dev/null +++ b/app/src/main/java/com/example/hpostesting/data/model/reportgen/ui/report/ReportFragment.kt @@ -0,0 +1,234 @@ +package com.example.hpostesting.data.model.reportgen.ui.report + +import android.app.DatePickerDialog +import android.content.Context +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.Button +import android.widget.EditText +import android.widget.Spinner +import android.widget.TextView +import android.widget.Toast +import androidx.core.widget.addTextChangedListener +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.example.hpostesting.data.model.patient.HemoCubeTestData +import dagger.hilt.android.AndroidEntryPoint +import `in`.sminnovations.hpostesting.R +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Date +import java.util.Locale +@AndroidEntryPoint +class ReportFragment : Fragment() { + + companion object { + fun newInstance() = ReportFragment() + } + + private val viewModel: ReportViewModel by viewModels() + + private lateinit var recyclerView: RecyclerView + private lateinit var searchBar: EditText + private lateinit var filterSpinner: Spinner + private lateinit var generateBtn: Button + private lateinit var selectedCountText: TextView + private lateinit var calendarButton: Button + + private lateinit var adapter: BufferAdapter + private var selectedItems: MutableList = mutableListOf() + private var allData: List = emptyList() + private var currentFilter = "Today" + private var customDate: String? = null + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? + ): View = inflater.inflate(R.layout.fragment_report, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + initViews(view) + setupRecyclerView() + setupFilterSpinner() + setupObservers() + setupListeners() + + // Set default filter to Today + setDefaultFilter() + } + + private fun initViews(view: View) { + recyclerView = view.findViewById(R.id.bufferRecyclerView) + searchBar = view.findViewById(R.id.searchBar) + filterSpinner = view.findViewById(R.id.filterSpinner) + generateBtn = view.findViewById(R.id.generateReportButton) + selectedCountText = view.findViewById(R.id.selectedCountText) + calendarButton = view.findViewById(R.id.calendarButton) + } + + private fun setupRecyclerView() { + recyclerView.layoutManager = LinearLayoutManager(requireContext()) + adapter = BufferAdapter(emptyList()) { item -> + toggleItemSelection(item) + updateUI() + } + recyclerView.adapter = adapter + } + + private fun setupFilterSpinner() { + val filterOptions = arrayOf("Today", "Yesterday", "All", "Custom Date") + val spinnerAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, filterOptions) + spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + filterSpinner.adapter = spinnerAdapter + } + + private fun setDefaultFilter() { + filterSpinner.setSelection(0) // Today + currentFilter = "Today" + calendarButton.visibility = View.GONE + } + + private fun setupObservers() { + viewModel.allKitTestData.observe(viewLifecycleOwner) { list -> + allData = list + filterList(searchBar.text.toString(), currentFilter) + } + } + + private fun setupListeners() { + searchBar.addTextChangedListener { editable -> + filterList(editable.toString(), currentFilter) + } + + filterSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>, view: View?, pos: Int, id: Long) { + currentFilter = parent.getItemAtPosition(pos).toString() + + if (currentFilter == "Custom Date") { + calendarButton.visibility = View.VISIBLE + if (customDate == null) { + showDatePicker() + } else { + filterList(searchBar.text.toString(), currentFilter) + } + } else { + calendarButton.visibility = View.GONE + customDate = null + filterList(searchBar.text.toString(), currentFilter) + } + } + + override fun onNothingSelected(p0: AdapterView<*>?) {} + } + + calendarButton.setOnClickListener { + showDatePicker() + } + + generateBtn.setOnClickListener { + if (selectedItems.isNotEmpty()) { + ReportGen.generate(requireContext(), selectedItems) + } + } + } + + private fun showDatePicker() { + val calendar = Calendar.getInstance() + val year = calendar.get(Calendar.YEAR) + val month = calendar.get(Calendar.MONTH) + val day = calendar.get(Calendar.DAY_OF_MONTH) + + DatePickerDialog(requireContext(), { _, selectedYear, selectedMonth, selectedDay -> + val sdf = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + val selectedCalendar = Calendar.getInstance() + selectedCalendar.set(selectedYear, selectedMonth, selectedDay) + customDate = sdf.format(selectedCalendar.time) + + // Update calendar button text + val displayFormat = SimpleDateFormat("MMM dd, yyyy", Locale.getDefault()) + calendarButton.text = displayFormat.format(selectedCalendar.time) + + filterList(searchBar.text.toString(), currentFilter) + }, year, month, day).show() + } + + private fun toggleItemSelection(item: HemoCubeTestData) { + if (selectedItems.contains(item)) { + selectedItems.remove(item) + } else { + selectedItems.add(item) + } + adapter.updateSelectedItems(selectedItems) + } + + private fun updateUI() { + selectedCountText.text = "Selected: ${selectedItems.size}" + generateBtn.visibility = if (selectedItems.isNotEmpty()) View.VISIBLE else View.GONE + } + + private fun filterList(query: String, filter: String) { + val sdf = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + val today = sdf.format(Date()) + + val calendar = Calendar.getInstance() + calendar.add(Calendar.DAY_OF_MONTH, -1) + val yesterday = sdf.format(calendar.time) + + val filtered = allData.filter { item -> + val matchesSearch = item._id.contains(query, true) || + item.classificationResult.contains(query, true) + val matchesDate = when (filter) { + "Today" -> item.testTime?.contains(today) + "Yesterday" -> item.testTime?.contains(yesterday) + "Custom Date" -> customDate?.let { item.testTime?.contains(it) } ?: true + "All" -> true + else -> true + } + + matchesSearch && matchesDate ?: true + } + + adapter.updateList(filtered) + + // Clear selections if items are no longer visible + selectedItems.removeAll { selectedItem -> + !filtered.contains(selectedItem) + } + adapter.updateSelectedItems(selectedItems) + updateUI() + } +} + + + +object ReportGen { + fun generate(context: Context, dataList: List) { + if (dataList.isEmpty()) { + Toast.makeText(context, "No items selected for report generation", Toast.LENGTH_SHORT).show() + return + } + + Log.d("ReportGen", "Generating report for ${dataList.size} items") + + val kitNumbers = dataList.map { it._id } + val message = if (dataList.size == 1) { + "Report for kit ${kitNumbers.first()} started." + } else { + "Report for ${dataList.size} kits started." + } + + Toast.makeText(context, message, Toast.LENGTH_SHORT).show() + + // TODO: Add PDF generation, email, or preview logic here + // You can iterate through dataList to include all selected items in the report + dataList.forEach { item-> + Log.d("ReportGen", "Processing kit: ${item._id}, Result: ${item.classificationResult}, Time: ${item.testTime}") + } + } +} diff --git a/app/src/main/java/com/example/hpostesting/data/model/reportgen/ui/report/ReportViewModel.kt b/app/src/main/java/com/example/hpostesting/data/model/reportgen/ui/report/ReportViewModel.kt new file mode 100644 index 0000000..925fa78 --- /dev/null +++ b/app/src/main/java/com/example/hpostesting/data/model/reportgen/ui/report/ReportViewModel.kt @@ -0,0 +1,19 @@ +package com.example.hpostesting.data.model.reportgen.ui.report + +import androidx.lifecycle.LiveData +import androidx.lifecycle.ViewModel +import com.example.hpostesting.data.dao.HemoCubeBufferDao +import com.example.hpostesting.data.dao.HemoCubeDao +import com.example.hpostesting.data.model.patient.BufferCheckData +import com.example.hpostesting.data.model.patient.HemoCubeTestData +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + + +@HiltViewModel +class ReportViewModel @Inject constructor( + private val hemoCubeDao: HemoCubeDao, +) : ViewModel() { + + val allKitTestData: LiveData> = hemoCubeDao.getAll() +} diff --git a/app/src/main/java/com/example/hpostesting/presentation/main_base/PanelFragment.kt b/app/src/main/java/com/example/hpostesting/presentation/main_base/PanelFragment.kt index 3c3d1be..41ce1be 100644 --- a/app/src/main/java/com/example/hpostesting/presentation/main_base/PanelFragment.kt +++ b/app/src/main/java/com/example/hpostesting/presentation/main_base/PanelFragment.kt @@ -25,6 +25,7 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import com.example.hpostesting.data.constant.Constants +import com.example.hpostesting.data.model.reportgen.ReportActivity import com.example.hpostesting.presentation.autodac.AutoDacActivity import com.example.hpostesting.presentation.buffercheck.HemocubeBufferCheckActivity import com.example.hpostesting.presentation.calibration.CalibrationActivity @@ -89,7 +90,7 @@ class PanelFragment : Fragment() { binding.btnDeviceInfo.visibility = View.VISIBLE binding.btnResetPassword.visibility = View.VISIBLE binding.btnUpdateValues.visibility = View.VISIBLE - binding.btnRnd.visibility = View.GONE + binding.btnPrintReport.visibility = View.VISIBLE }else{ binding.btnResetPassword.visibility = View.GONE binding.btnDeviceProvision.visibility = View.GONE @@ -103,7 +104,7 @@ class PanelFragment : Fragment() { binding.btnSubmit.visibility = View.GONE binding.btnDeviceInfo.visibility = View.VISIBLE binding.btnUpdateValues.visibility = View.GONE - binding.btnRnd.visibility = View.GONE + binding.btnPrintReport.visibility = View.VISIBLE } binding.btnResetPassword.setOnClickListener{ @@ -138,6 +139,10 @@ class PanelFragment : Fragment() { startActivity(Intent(requireContext(), DeviceActivity::class.java)) } + binding.btnPrintReport.setOnClickListener{ + startActivity(Intent(requireContext(),ReportActivity::class.java)) + } + binding.btnFirefox.setOnClickListener { val intent = Intent(Intent.ACTION_VIEW) intent.component = ComponentName("org.mozilla.firefox", "org.mozilla.gecko.BrowserApp") diff --git a/app/src/main/res/drawable/baseline_add_circle_24.xml b/app/src/main/res/drawable/baseline_add_circle_24.xml new file mode 100644 index 0000000..d448b37 --- /dev/null +++ b/app/src/main/res/drawable/baseline_add_circle_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/layout/fragment_gallery.xml b/app/src/main/res/layout/fragment_gallery.xml index bcd9663..b4100ed 100644 --- a/app/src/main/res/layout/fragment_gallery.xml +++ b/app/src/main/res/layout/fragment_gallery.xml @@ -56,13 +56,13 @@