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

Epic: Program Registries Phase 2 #7231

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,7 @@ Reference to the [Clinician](#!/source/source.tamanu.tamanu.users) recording tha
{% docs patient_program_registration_conditions__deletion_clinician_id %}
Reference to the [Clinician](#!/source/source.tamanu.tamanu.users) that removed the condition.
{% enddocs %}

{% docs patient_program_registration_conditions__condition_category %}
Used to store the category of the condition.
{% enddocs %}
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,10 @@ sources:
patient_program_registration_conditions."
data_tests:
- not_null
- name: condition_category
data_type: character varying(255)
description: "{{
doc('patient_program_registration_conditions__condition_category')
}}"
data_tests:
- not_null
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,11 @@ TODO
{% docs patient_program_registration_conditions__deletion_clinician_id %}
TODO
{% enddocs %}

{% docs patient_program_registration_conditions__condition_category %}
Used to store the category of the condition.
{% enddocs %}

{% docs patient_program_registration_conditions__condition %}

{% enddocs %}
58 changes: 30 additions & 28 deletions packages/constants/src/enumRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,50 +5,51 @@ import {
EDUCATIONAL_ATTAINMENT_LABELS,
SOCIAL_MEDIA_LABELS,
TITLE_LABELS,
} from './patientFields.js';
} from './patientFields';
import {
INVOICE_STATUS_LABELS,
INVOICE_INSURER_PAYMENT_STATUS_LABELS,
INVOICE_ITEMS_CATEGORY_LABELS,
INVOICE_PATIENT_PAYMENT_STATUSES_LABELS,
} from './invoices.js';
import { ENCOUNTER_TYPE_LABELS } from './encounters.js';
import { NOTE_TYPE_LABELS } from './notes.js';
} from './invoices';
import { ENCOUNTER_TYPE_LABELS } from './encounters';
import { NOTE_TYPE_LABELS } from './notes';
import {
REFERRAL_STATUS_LABELS,
APPOINTMENT_STATUSES,
IMAGING_REQUEST_STATUS_LABELS,
} from './statuses.js';
import { VACCINE_STATUS_LABELS, INJECTION_SITE_LABELS, VACCINE_CATEGORY_LABELS } from './vaccines.js';
} from './statuses';
import { VACCINE_STATUS_LABELS, INJECTION_SITE_LABELS, VACCINE_CATEGORY_LABELS } from './vaccines';
import {
ATTENDANT_OF_BIRTH_LABELS,
BIRTH_DELIVERY_TYPE_LABELS,
BIRTH_TYPE_LABELS,
PLACE_OF_BIRTH_LABELS,
} from './births.js';
} from './births';
import {
REPORT_DATA_SOURCE_LABELS,
REPORT_DEFAULT_DATE_RANGES_LABELS,
REPORT_DB_SCHEMA_LABELS,
REPORT_STATUS_LABELS,
} from './reports.js';
import { TEMPLATE_TYPE_LABELS } from './templates.js';
import { LAB_REQUEST_STATUS_LABELS } from './labs.js';
import { ASSET_NAME_LABELS } from './importable.js';
import { DIAGNOSIS_CERTAINTY_LABELS, PATIENT_ISSUE_LABELS } from './diagnoses.js';
import { DRUG_ROUTE_LABELS, REPEATS_LABELS } from './medications.js';
import { PLACE_OF_DEATHS, MANNER_OF_DEATHS } from './deaths.js';
import { LOCATION_AVAILABILITY_STATUS_LABELS } from './locations.js';
import { TASK_FREQUENCY_UNIT_LABELS } from './tasks.js';
import { IMAGING_TYPES } from './imaging.js';
} from './reports';
import { TEMPLATE_TYPE_LABELS } from './templates';
import { LAB_REQUEST_STATUS_LABELS } from './labs';
import { ASSET_NAME_LABELS } from './importable';
import { DIAGNOSIS_CERTAINTY_LABELS, PATIENT_ISSUE_LABELS } from './diagnoses';
import { DRUG_ROUTE_LABELS, REPEATS_LABELS } from './medications';
import { PLACE_OF_DEATHS, MANNER_OF_DEATHS } from './deaths';
import { LOCATION_AVAILABILITY_STATUS_LABELS } from './locations';
import { TASK_FREQUENCY_UNIT_LABELS } from './tasks';
import { IMAGING_TYPES } from './imaging';
import {
REPEAT_FREQUENCY_LABELS,
REPEAT_FREQUENCY_UNIT_LABELS,
REPEAT_FREQUENCY_UNIT_PLURAL_LABELS,
} from './appointments.js';
} from './appointments';
import { PROGRAM_REGISTRY_CONDITION_CATEGORIES } from './programRegistry';

type EnumKeys = keyof typeof registeredEnums;
type EnumValues = typeof registeredEnums[EnumKeys];
type EnumValues = (typeof registeredEnums)[EnumKeys];
type EnumEntries = [EnumKeys, EnumValues][];

/**
Expand Down Expand Up @@ -82,6 +83,7 @@ export const registeredEnums = {
PATIENT_ISSUE_LABELS,
PLACE_OF_BIRTH_LABELS,
PLACE_OF_DEATHS,
PROGRAM_REGISTRY_CONDITION_CATEGORIES,
REFERRAL_STATUS_LABELS,
REPEATS_LABELS,
REPEAT_FREQUENCY_LABELS,
Expand Down Expand Up @@ -132,6 +134,7 @@ export const translationPrefixes: Record<EnumKeys, string> = {
PATIENT_ISSUE_LABELS: 'patient.property.issue',
PLACE_OF_BIRTH_LABELS: 'birth.property.placeOfBirth',
PLACE_OF_DEATHS: 'death.property.placeOfDeath',
PROGRAM_REGISTRY_CONDITION_CATEGORIES: 'programRegistry.property.conditionCategory',
REFERRAL_STATUS_LABELS: 'referral.property.status',
REPEATS_LABELS: 'medication.property.repeats',
REPEAT_FREQUENCY_LABELS: 'scheduling.property.repeatFrequency',
Expand Down Expand Up @@ -161,13 +164,12 @@ export const prefixMap = new Map(
);

/** The list of all translatable enums string id and fallback */
export const enumTranslations = (Object.entries(
registeredEnums,
) as EnumEntries).flatMap(([key, value]) =>
Object.entries(value).map(([enumKey, enumValue]) => [
`${translationPrefixes[key]}.${enumKey
.toLowerCase()
.replace(/[^a-zA-Z0-9]+(.)/g, (_, char) => char.toUpperCase())}`,
enumValue,
]),
export const enumTranslations = (Object.entries(registeredEnums) as EnumEntries).flatMap(
([key, value]) =>
Object.entries(value).map(([enumKey, enumValue]) => [
`${translationPrefixes[key]}.${enumKey
.toLowerCase()
.replace(/[^a-zA-Z0-9]+(.)/g, (_, char) => char.toUpperCase())}`,
enumValue,
]),
);
12 changes: 12 additions & 0 deletions packages/constants/src/programRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,15 @@ export const STATUS_COLOR = {
brown: '#7A492E',
teal: '#125E7E',
};

export const PROGRAM_REGISTRY_CONDITION_CATEGORIES = {
suspected: 'Suspected',
underInvestigation: 'Under investigation',
confirmed: 'Confirmed',
unknown: 'Unknown',
disproven: 'Disproven',
resolved: 'Resolved',
inRemission: 'In remission',
notApplicable: 'Not applicable',
recordedInError: 'Recorded in error',
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { DataTypes, QueryInterface } from 'sequelize';

export async function up(query: QueryInterface) {
await query.addColumn('patient_program_registration_conditions', 'condition_category', {
type: DataTypes.STRING,
allowNull: false,
defaultValue: 'Unknown',
});
}

export async function down(query: QueryInterface) {
await query.removeColumn('patient_program_registration_conditions', 'condition_category');
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { SYNC_DIRECTIONS } from '@tamanu/constants';
import { DataTypes } from 'sequelize';
import { getCurrentDateTimeString } from '@tamanu/utils/dateTime';
import { Model } from './Model';
import { buildPatientLinkedLookupFilter } from '../sync/buildPatientLinkedLookupFilter';
Expand All @@ -13,6 +14,7 @@ export class PatientProgramRegistrationCondition extends Model {
declare programRegistryConditionId?: string;
declare clinicianId?: string;
declare deletionClinicianId?: string;
declare conditionCategory?: string;

static initModel({ primaryKey, ...options }: InitOptions) {
super.init(
Expand All @@ -25,6 +27,11 @@ export class PatientProgramRegistrationCondition extends Model {
deletionDate: dateTimeType('deletionDate', {
defaultValue: null,
}),
conditionCategory: {
type: DataTypes.STRING,
defaultValue: 'Unknown',
allowNull: false,
},
},
{
...options,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import config from 'config';
import { afterAll, beforeAll } from '@jest/globals';

import { REGISTRATION_STATUSES } from '@tamanu/constants';
import { REGISTRATION_STATUSES, PROGRAM_REGISTRY_CONDITION_CATEGORIES } from '@tamanu/constants';
import { selectFacilityIds } from '@tamanu/utils/selectFacilityIds';
import { disableHardcodedPermissionsForSuite, fake } from '@tamanu/shared/test-helpers';

Expand Down Expand Up @@ -163,7 +162,12 @@ describe('PatientProgramRegistration', () => {
clinicianId: clinician.id,
patientId: patient.id,
date: '2023-09-02 08:00:00',
conditionIds: [programRegistryCondition.id],
conditions: [
{
conditionId: programRegistryCondition.id,
category: PROGRAM_REGISTRY_CONDITION_CATEGORIES.confirmed,
},
],
registeringFacilityId: facilityId,
});

Expand All @@ -178,9 +182,8 @@ describe('PatientProgramRegistration', () => {
date: '2023-09-02 08:00:00',
});

const createdRegistrationCondition = await models.PatientProgramRegistrationCondition.findByPk(
result.body.conditions[0].id,
);
const createdRegistrationCondition =
await models.PatientProgramRegistrationCondition.findByPk(result.body.conditions[0].id);

expect(createdRegistrationCondition).toMatchObject({
programRegistryId: programRegistry1.id,
Expand Down Expand Up @@ -208,7 +211,7 @@ describe('PatientProgramRegistration', () => {
);

// Add a small delay so the registrations are definitely created at distinctly different times.
await new Promise(resolve => {
await new Promise((resolve) => {
setTimeout(resolve, 100);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ patientProgramRegistration.get(
req.checkPermission('read', 'PatientProgramRegistration');
req.checkPermission('list', 'PatientProgramRegistration');

const registrationData = await models.PatientProgramRegistration.getMostRecentRegistrationsForPatient(
params.patientId,
);
const registrationData =
await models.PatientProgramRegistration.getMostRecentRegistrationsForPatient(
params.patientId,
);

const filteredData = registrationData.filter(x => req.ability.can('read', x.programRegistry));
const filteredData = registrationData.filter((x) => req.ability.can('read', x.programRegistry));
res.send({ data: filteredData });
}),
);
Expand Down Expand Up @@ -53,28 +54,31 @@ patientProgramRegistration.post(
req.checkPermission('create', 'PatientProgramRegistration');
}

const { conditionIds = [], ...registrationData } = body;
const { conditions = [], ...registrationData } = body;

if (conditionIds.length > 0) {
if (conditions.length > 0) {
req.checkPermission('create', 'PatientProgramRegistrationCondition');
}

// Run in a transaction so it either fails or succeeds together
const [registration, conditions] = await db.transaction(async () => {
const [registration, conditionsRecords] = await db.transaction(async () => {
return Promise.all([
models.PatientProgramRegistration.create({
patientId,
programRegistryId,
...registrationData,
}),
models.PatientProgramRegistrationCondition.bulkCreate(
conditionIds.map(conditionId => ({
patientId,
programRegistryId,
clinicianId: registrationData.clinicianId,
date: registrationData.date,
programRegistryConditionId: conditionId,
})),
conditions
.filter((condition) => condition.conditionId)
.map((condition) => ({
patientId,
programRegistryId,
clinicianId: registrationData.clinicianId,
date: registrationData.date,
programRegistryConditionId: condition.conditionId,
conditionCategory: condition.category,
})),
),
// as a side effect, mark for sync in the current facility
models.PatientFacility.upsert({
Expand All @@ -87,7 +91,7 @@ patientProgramRegistration.post(
// Convert Sequelize model to use a custom object as response
const responseObject = {
...registration.get({ plain: true }),
conditions,
conditions: conditionsRecords,
};

res.send(responseObject);
Expand All @@ -104,15 +108,15 @@ const getChangingFieldRecords = (allRecords, field) =>
return currentValue !== prevValue;
});

const getRegistrationRecords = allRecords =>
const getRegistrationRecords = (allRecords) =>
getChangingFieldRecords(allRecords, 'registrationStatus').filter(
({ registrationStatus }) => registrationStatus === REGISTRATION_STATUSES.ACTIVE,
);
const getDeactivationRecords = allRecords =>
const getDeactivationRecords = (allRecords) =>
getChangingFieldRecords(allRecords, 'registrationStatus').filter(
({ registrationStatus }) => registrationStatus === REGISTRATION_STATUSES.INACTIVE,
);
const getStatusChangeRecords = allRecords =>
const getStatusChangeRecords = (allRecords) =>
getChangingFieldRecords(allRecords, 'clinicalStatusId');

patientProgramRegistration.get(
Expand Down Expand Up @@ -214,11 +218,11 @@ patientProgramRegistration.get(
.reverse();

const statusChangeRecords = getStatusChangeRecords(fullHistory);
const historyWithRegistrationDate = statusChangeRecords.map(data => ({
const historyWithRegistrationDate = statusChangeRecords.map((data) => ({
...data,
// Find the latest registrationDate that is not after the date of interest
registrationDate: registrationDates.find(
registrationDate => !isAfter(new Date(registrationDate), new Date(data.date)),
(registrationDate) => !isAfter(new Date(registrationDate), new Date(data.date)),
),
}));

Expand Down Expand Up @@ -283,7 +287,6 @@ patientProgramRegistration.get(
programRegistryId,
},
include: PatientProgramRegistrationCondition.getFullReferenceAssociations(),
order: [['date', 'DESC']],
});

res.send({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
import { getTable } from './utils/queryRunner';

export class addPatientProgramRegistrationConditionCategoryColumn1739395962000
implements MigrationInterface
{
async up(queryRunner: QueryRunner): Promise<void> {
const table = await getTable(queryRunner, 'patient_program_registration_conditions');
await queryRunner.addColumn(
table,
new TableColumn({
name: 'conditionCategory',
type: 'string',
isNullable: false,
default: "'Unknown'",
}),
);
}

async down(queryRunner: QueryRunner): Promise<void> {
const table = await getTable(queryRunner, 'patient_program_registration_conditions');
await queryRunner.dropColumn(table, 'conditionCategory');
}
}
2 changes: 2 additions & 0 deletions packages/mobile/App/migrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import { updateReferenceDataRelationIndex1714605577000 } from './1731998974975-u
import { removeLabTestStatus1734072605000 } from './1734072605000-removeLabTestStatus';
import { standardiseCaseAndPluralityOfAllTables1734080053767 } from './1734080053767-standardiseCaseAndPluralityOfAllTables';
import { addIsSensitiveColumnToLabTestTypes1738620786000 } from './1738620786000-addIsSensitiveColumnToLabTestTypes';
import { addPatientProgramRegistrationConditionCategoryColumn1739395962000 } from './1739395962000-addPatientProgramRegistrationConditionCategoryColumn';

export const migrationList = [
databaseSetup1661160427226,
Expand Down Expand Up @@ -121,4 +122,5 @@ export const migrationList = [
removeLabTestStatus1734072605000,
standardiseCaseAndPluralityOfAllTables1734080053767,
addIsSensitiveColumnToLabTestTypes1738620786000,
addPatientProgramRegistrationConditionCategoryColumn1739395962000,
];
Loading
Loading