Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Apply Tracing and Appointment changes from Usability test #54

Merged
merged 11 commits into from
Apr 9, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@ import ca.uhn.fhir.rest.param.ParamPrefixEnum
import com.google.android.fhir.FhirEngine
import com.google.android.fhir.logicalId
import com.google.android.fhir.search.Operation
import com.google.android.fhir.search.Order
import com.google.android.fhir.search.Search
import com.google.android.fhir.search.filter.TokenParamFilterCriterion
import com.google.android.fhir.search.has
import com.google.android.fhir.search.search
import java.util.Calendar
import java.util.Date
import javax.inject.Inject
import javax.inject.Singleton
import org.hl7.fhir.r4.model.Appointment
Expand All @@ -33,7 +36,6 @@ import org.hl7.fhir.r4.model.Coding
import org.hl7.fhir.r4.model.DateTimeType
import org.hl7.fhir.r4.model.Identifier
import org.hl7.fhir.r4.model.Patient
import org.hl7.fhir.r4.model.Practitioner
import org.hl7.fhir.r4.model.ResourceType
import org.smartregister.fhircore.engine.appfeature.model.HealthModule
import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry
Expand Down Expand Up @@ -89,17 +91,11 @@ constructor(
configurationRegistry.retrieveConfiguration(AppConfigClassification.APPLICATION)

override suspend fun countRegisterData(appFeatureName: String?): Long {
val dateOfAppointment = Calendar.getInstance().time
return fhirEngine
.search<Appointment> {
filter(Appointment.STATUS, { value = of(Appointment.AppointmentStatus.BOOKED.toCode()) })
}
.search<Appointment> { genericFilter(dateOfAppointment) }
.map { it.resource }
.count {
it.status == Appointment.AppointmentStatus.BOOKED &&
it.hasStart() &&
it.patientRef() != null &&
it.practitionerRef() != null
}
.count { isAppointmentValid(it) }
.toLong()
}

Expand All @@ -122,103 +118,157 @@ constructor(
page: Int = -1,
): List<Appointment> {
filters as AppointmentRegisterFilter
val patientTypeFilterTag = applicationConfiguration().patientTypeFilterTagViaMetaCodingSystem

val searchResults =
fhirEngine.search<Appointment> {
if (!loadAll) count = PaginationConstant.DEFAULT_PAGE_SIZE

if (page >= 0) from = page * PaginationConstant.DEFAULT_PAGE_SIZE

filter(Appointment.STATUS, { value = of(Appointment.AppointmentStatus.BOOKED.toCode()) })

filter(
Appointment.DATE,
{
value = of(DateTimeType(filters.dateOfAppointment))
prefix = ParamPrefixEnum.GREATERTHAN_OR_EQUALS
},
genericFilter(
dateOfAppointment = filters.dateOfAppointment,
reasonCode = filters.reasonCode,
patientCategory = filters.patientCategory,
myPatients = filters.myPatients,
)
filter(
Appointment.DATE,
{
value = of(DateTimeType(filters.dateOfAppointment).apply { add(Calendar.DATE, 1) })
prefix = ParamPrefixEnum.LESSTHAN
},

sort(Appointment.DATE, Order.ASCENDING)
}

return searchResults
.map { it.resource }
.filter {
isAppointmentValid(
appointment = it,
reasonCode = filters.reasonCode,
patientCategory = filters.patientCategory,
myPatients = filters.myPatients,
)
}
}

if (filters.myPatients && currentPractitioner != null) {
filter(Appointment.PRACTITIONER, { value = currentPractitioner!! })
}
private suspend fun isAppointmentValid(
appointment: Appointment,
reasonCode: String? = null,
patientCategory: Iterable<HealthStatus>? = null,
myPatients: Boolean = false,
): Boolean {
val patientAssignmentFilter =
!myPatients ||
(appointment.practitionerRef()?.reference ==
currentPractitioner?.asReference(ResourceType.Practitioner)?.reference)

filters.patientCategory?.let {
val paramQueries: List<(TokenParamFilterCriterion.() -> Unit)> =
it.flatMap { healthStatus ->
val coding: Coding =
Coding().apply {
system = patientTypeFilterTag
code = healthStatus.name.lowercase().replace("_", "-")
}
val alternativeCoding: Coding =
Coding().apply {
system = patientTypeFilterTag
code = healthStatus.name.lowercase()
}

return@flatMap listOf<Coding>(coding, alternativeCoding).map<
Coding,
TokenParamFilterCriterion.() -> Unit,
> { c ->
{ value = of(c) }
}
}
val patientCategoryFilter =
patientCategory == null || (patientCategoryMatches(appointment, patientCategory))

has<Patient>(Appointment.PATIENT) {
filter(TokenClientParam("_tag"), *paramQueries.toTypedArray(), operation = Operation.OR)
}
val appointmentReasonFilter =
reasonCode == null ||
(appointment.reasonCode.flatMap { cc -> cc.coding }.any { c -> c.code == reasonCode })

return (appointment.status == Appointment.AppointmentStatus.BOOKED ||
appointment.status == Appointment.AppointmentStatus.NOSHOW) &&
appointment.hasStart() &&
patientAssignmentFilter &&
patientCategoryFilter &&
appointmentReasonFilter &&
appointment.patientRef() != null &&
appointment.practitionerRef() != null
}

private fun Search.genericFilter(
dateOfAppointment: Date? = null,
reasonCode: String? = null,
patientCategory: Iterable<HealthStatus>? = null,
myPatients: Boolean = false,
) {
filter(
Appointment.STATUS,
{ value = of(Appointment.AppointmentStatus.BOOKED.toCode()) },
{ value = of(Appointment.AppointmentStatus.NOSHOW.toCode()) },
)

if (dateOfAppointment != null) {
filter(
Appointment.DATE,
{
value = of(DateTimeType(dateOfAppointment))
prefix = ParamPrefixEnum.GREATERTHAN_OR_EQUALS
},
)
filter(
Appointment.DATE,
{
value =
of(
DateTimeType(dateOfAppointment).apply {
add(
Calendar.DATE,
1,
)
},
)
prefix = ParamPrefixEnum.LESSTHAN
},
)
}

if (myPatients && currentPractitioner != null) {
filter(
Appointment.PRACTITIONER,
{ value = currentPractitioner!!.asReference(ResourceType.Appointment).reference },
)
}

if (reasonCode != null) {
val codeAbleConcept =
CodeableConcept().apply {
addCoding(
Coding().apply {
system = "https://d-tree.org"
code = reasonCode
},
)
}
filter(Appointment.REASON_CODE, { value = of(codeAbleConcept) })
}

if (patientCategory != null) {
val patientTypeFilterTag = applicationConfiguration().patientTypeFilterTagViaMetaCodingSystem

filters.reasonCode?.let {
val codeableConcept =
CodeableConcept().apply {
addCoding(
Coding().apply {
system = "https://d-tree.org"
code = it
},
)
val paramQueries: List<(TokenParamFilterCriterion.() -> Unit)> =
patientCategory.flatMap { healthStatus ->
val coding: Coding =
Coding().apply {
system = patientTypeFilterTag
code = healthStatus.name.lowercase().replace("_", "-")
}
val alternativeCoding: Coding =
Coding().apply {
system = patientTypeFilterTag
code = healthStatus.name.lowercase()
}
filter(Appointment.REASON_CODE, { value = of(codeableConcept) })

return@flatMap listOf<Coding>(coding, alternativeCoding).map<
Coding,
TokenParamFilterCriterion.() -> Unit,
> { c ->
{ value = of(c) }
}
}
}

return searchResults
.map { it.resource }
.filter {
val patientAssignmentFilter =
!filters.myPatients ||
(it.practitionerRef()?.reference ==
currentPractitioner?.asReference(ResourceType.Practitioner)?.reference)
val patientCategoryFilter =
filters.patientCategory == null || (patientCategoryMatches(it, filters.patientCategory))

val appointmentReasonFilter =
filters.reasonCode == null ||
(it.reasonCode.flatMap { cc -> cc.coding }.any { c -> c.code == filters.reasonCode })
it.status == Appointment.AppointmentStatus.BOOKED &&
it.hasStart() &&
patientAssignmentFilter &&
patientCategoryFilter &&
appointmentReasonFilter &&
it.patientRef() != null
has<Patient>(Appointment.PATIENT) {
filter(TokenClientParam("_tag"), *paramQueries.toTypedArray(), operation = Operation.OR)
}
}
}

private suspend fun transformAppointment(appointment: Appointment): RegisterData {
val refPatient = appointment.patientRef()!!
val patient = defaultRepository.loadResource(refPatient) as Patient

return RegisterData.AppointmentRegisterData(
logicalId = appointment.logicalId,
logicalId = patient.logicalId,
appointmentLogicalId = appointment.logicalId,
name = patient.extractName(),
identifier =
patient.identifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,40 +172,43 @@ abstract class TracingRegisterDao(
}
.map { it.resource }
.filter {
if (filters.age != null) {
val today = LocalDate.now().atStartOfDay(ZoneId.systemDefault())
when (filters.age) {
TracingAgeFilterEnum.ZERO_TO_2 -> {
val date = Date.from(today.minusYears(2L).toInstant())
it.birthDate.after(date)
}
TracingAgeFilterEnum.ZERO_TO_18 -> {
val date = Date.from(today.minusYears(18L).toInstant())
it.birthDate.after(date)
}
TracingAgeFilterEnum.PLUS_18 -> {
val date = Date.from(today.minusYears(18L).toInstant())
it.birthDate.time <= date.time
val isInRange =
if (filters.age != null) {
val today = LocalDate.now().atStartOfDay(ZoneId.systemDefault())
when (filters.age) {
TracingAgeFilterEnum.ZERO_TO_2 -> {
val date = Date.from(today.minusYears(2L).toInstant())
it.birthDate?.after(date)
}
TracingAgeFilterEnum.ZERO_TO_18 -> {
val date = Date.from(today.minusYears(18L).toInstant())
it.birthDate?.after(date)
}
TracingAgeFilterEnum.PLUS_18 -> {
val date = Date.from(today.minusYears(18L).toInstant())
it.birthDate?.before(date)
}
}
} else {
true
}
} else {
true
}

isInRange ?: false
}

val patientrefs =
val patientRefs =
patients
.map<Patient, (ReferenceParamFilterCriterion.() -> Unit)> {
return@map { value = it.referenceValue() }
}
.toTypedArray()

val tasks: List<Task> =
if (patientrefs.isNotEmpty()) {
if (patientRefs.isNotEmpty()) {
fhirEngine
.search<Task> {
filtersForValidTask()
filter(Task.SUBJECT, *patientrefs, operation = Operation.OR)
filter(Task.SUBJECT, *patientRefs, operation = Operation.OR)
}
.map { it.resource }
.filter {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ sealed class ProfileData(open val logicalId: String, open val name: String) {
val identifier: String? = null,
val givenName: String = "",
val familyName: String = "",
val birthdate: Date,
val birthdate: Date?,
val age: String = birthdate.toAgeDisplay(),
val gender: Enumerations.AdministrativeGender,
val address: String,
Expand All @@ -109,7 +109,7 @@ sealed class ProfileData(open val logicalId: String, open val name: String) {
data class TracingProfileData(
override val logicalId: String,
override val name: String,
val birthdate: Date,
val birthdate: Date?,
val dueDate: Date?,
val address: String,
val identifier: String?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ sealed class RegisterData(open val logicalId: String, open val name: String) {
data class AppointmentRegisterData(
override val logicalId: String,
override val name: String,
val appointmentLogicalId: String,
val identifier: String? = null,
val gender: Enumerations.AdministrativeGender,
val age: String,
Expand Down
Loading
Loading