diff --git a/src/Components/Patient/DailyRounds.tsx b/src/Components/Patient/DailyRounds.tsx index 38285e1fe5b..ab8eea6526a 100644 --- a/src/Components/Patient/DailyRounds.tsx +++ b/src/Components/Patient/DailyRounds.tsx @@ -47,11 +47,10 @@ import useAuthUser from "../../Common/hooks/useAuthUser"; import CheckBoxFormField from "../Form/FormFields/CheckBoxFormField"; import SymptomsApi from "../Symptoms/api"; import DiagnosesRoutes from "../Diagnosis/routes"; -import MedicineRoutes from "../Medicine/routes"; import { scrollTo } from "../../Utils/utils"; import useQuery from "../../Utils/request/useQuery"; -import { EncounterSymptom } from "../Symptoms/types"; import _ from "lodash"; +import { scribeReducer } from "../Scribe/scribeutils"; const Loading = lazy(() => import("../Common/Loading")); @@ -61,7 +60,6 @@ export const DailyRounds = (props: any) => { const { goBack } = useAppHistory(); const { facilityId, patientId, consultationId, id } = props; const [symptomsSeed, setSymptomsSeed] = useState(1); - const [prescriptionSeed, setPrescriptionSeed] = useState(1); const initForm: any = { physical_examination_info: "", @@ -502,6 +500,7 @@ export const DailyRounds = (props: any) => { additional_symptoms: additionalSymptoms?.results.filter( (s) => s.clinical_impression_status !== "entered-in-error", ), + icd11_diagnosis: diagnoses, }} onFormUpdate={async (fields) => { // Symptoms @@ -511,143 +510,62 @@ export const DailyRounds = (props: any) => { ); if (fields.additional_symptoms) { - const coveredSymptoms: EncounterSymptom[] = []; - for (const symptom of fields.additional_symptoms) { - const existingSymptom = existingSymptoms?.find( - (s) => s.symptom === symptom.symptom, - ); - // Check if symptom is altered or added - if (!_.isEqual(symptom, existingSymptom)) { - if (existingSymptom) { - // symptom was altered - await request(SymptomsApi.partialUpdate, { - pathParams: { consultationId, external_id: symptom.id }, - body: _.pick(symptom, ["onset_date", "cure_date"]), - }); - } else { - // symptom does not exist, so must be added - await request(SymptomsApi.add, { - pathParams: { consultationId }, - body: { - ...symptom, - }, - }); - } - } - coveredSymptoms.push(symptom); - } - // check for deleted symptoms - const deletedSymptoms = - existingSymptoms?.filter( - (s) => !coveredSymptoms.find((c) => c.symptom === s.symptom), - ) || []; - for (const symptom of deletedSymptoms) { - //symptom was deleted - await request(SymptomsApi.markAsEnteredInError, { - pathParams: { consultationId, external_id: symptom.id }, - }); - } + await scribeReducer({ + existingData: existingSymptoms || [], + newData: fields.additional_symptoms, + comparer: (a, b) => a.symptom === b.symptom, + allowedFields: ["onset_date", "cure_date"], + onAdd: (stripped, item) => + request(SymptomsApi.add, { + pathParams: { consultationId }, + body: { + ...item, + }, + }), + onUpdate: (stripped, item) => + request(SymptomsApi.partialUpdate, { + pathParams: { consultationId, external_id: item.id }, + body: stripped, + }), + onDelete: (item) => + request(SymptomsApi.markAsEnteredInError, { + pathParams: { consultationId, external_id: item.id }, + }), + }); setSymptomsSeed((s) => s + 1); } // ICD11 Diagnosis if (fields.icd11_diagnosis) { - for (const diagnosis of fields.icd11_diagnosis) { - // Fetch available diagnoses - - const { res: icdRes, data: icdData } = await request( - routes.listICD11Diagnosis, - { - query: { query: diagnosis.diagnosis }, - }, - ); - - if (!icdRes?.ok) { - error({ - text: "Failed to fetch ICD11 Diagnosis", - }); - continue; - } - - const availableDiagnosis = icdData?.[0]?.id; - - if (!availableDiagnosis) { - error({ - text: "Could not find the requested diagnosis. Please enter manually.", - }); - continue; - } - - const { res, data } = await request( - DiagnosesRoutes.createConsultationDiagnosis, - { - pathParams: { consultation: consultationId }, - body: { - ...diagnosis, - diagnosis: availableDiagnosis, + await scribeReducer({ + existingData: diagnoses || [], + newData: fields.icd11_diagnosis, + comparer: (a, b) => + a.diagnosis_object.id === b.diagnosis_object.id, + allowedFields: ["verification_status"], + // waitng for chips PR to be merged + // onAdd: async (stripped, item) => { + // + // }, + onUpdate: async (stripped, item) => { + const { data, res } = await request( + DiagnosesRoutes.updateConsultationDiagnosis, + { + pathParams: { consultation: consultationId, id: item.id }, + body: stripped, }, - }, - ); - - if (res?.ok && data) - setDiagnoses((diagnoses) => [...(diagnoses || []), data]); - } - } - - // Prescriptions - if (fields.prescriptions || fields.prn_prescriptions) { - const combined_prescriptions = [ - ...(fields.prescriptions || []), - ...(fields.prn_prescriptions || []), - ]; - for (const prescription of combined_prescriptions) { - // fetch medicine - const { res: medicineRes, data: medicineData } = await request( - routes.listMedibaseMedicines, - { - query: { query: prescription.medicine }, - }, - ); - - if (!medicineRes?.ok) { - error({ - text: "Failed to fetch medicine", - }); - continue; - } - - const availableMedicine = medicineData?.[0]?.id; - - if (!availableMedicine) { - error({ - text: "Could not find the requested medicine. Please enter manually.", - }); - continue; - } - - const { res } = await request( - MedicineRoutes.createPrescription, - { - pathParams: { consultation: consultationId }, - body: { - ...prescription, - medicine: availableMedicine, - }, - }, - ); - - if (res?.ok) setPrescriptionSeed((s) => s + 1); - } + ); + if (res?.ok && data) + setDiagnoses((diagnoses) => + diagnoses?.map((d) => (d.id === data.id ? data : d)), + ); + }, + }); } if ( Object.keys(fields).some((f) => - [ - "investigations", - "icd11_diagnosis", - "prescriptions", - "prn_prescriptions", - ].includes(f), + ["investigations", "icd11_diagnosis"].includes(f), ) && roundTypes.some((t) => t.id === "DOCTORS_LOG") ) { @@ -939,7 +857,6 @@ export const DailyRounds = (props: any) => { discontinued={ showDiscontinuedPrescriptions ? undefined : false } - key={prescriptionSeed} actions={["discontinue"]} /> @@ -964,7 +881,6 @@ export const DailyRounds = (props: any) => { showDiscontinuedPrescriptions ? undefined : false } actions={["discontinue"]} - key={prescriptionSeed} /> diff --git a/src/Components/Scribe/Scribe.tsx b/src/Components/Scribe/Scribe.tsx index 8e042f177d2..53af032622c 100644 --- a/src/Components/Scribe/Scribe.tsx +++ b/src/Components/Scribe/Scribe.tsx @@ -9,6 +9,7 @@ import { UserModel } from "../Users/models"; import useConfig from "../../Common/hooks/useConfig"; import useSegmentedRecording from "../../Utils/useSegmentedRecorder"; import uploadFile from "../../Utils/request/uploadFile"; +import _ from "lodash"; interface FieldOption { id: string | number; @@ -23,7 +24,7 @@ export interface Field { example: string; current: any; options?: readonly FieldOption[]; - validator: (value: any) => boolean; + showFields?: string[]; } export interface ScribeForm { @@ -337,7 +338,6 @@ export const Scribe: React.FC = ({ const f = fields.find((f) => f.id === k); if (!f) return false; if (v === f.current) return false; - //if (f.validator) return f.validator(f.type === "number" ? Number(v) : v); return true; }) .map(([k, v]) => ({ [k]: v })) @@ -673,7 +673,12 @@ export const Scribe: React.FC = ({
{processFormField( fieldDetails, - formFields, + fieldDetails?.showFields + ? _.pick( + formFields, + fieldDetails?.showFields, + ) + : formFields, field, )}
diff --git a/src/Components/Scribe/formDetails.ts b/src/Components/Scribe/formDetails.ts index a475d49b137..72f66536148 100644 --- a/src/Components/Scribe/formDetails.ts +++ b/src/Components/Scribe/formDetails.ts @@ -19,13 +19,7 @@ const DAILY_ROUND_FORM_SCRIBE_DATA: Field[] = [ current: [], description: `An array of objects to store the patient's symptoms along with their date of onset and date of cure (if any). The symptom field should be an integer corresponding to the symptom's ID. The onset_date and cure_date fields should be date strings (e.g., '2022-01-01'). If no onset_date has been specified, use todays date which is '${new Date().toISOString().slice(0, 10)}'. If the symptom is ongoing, the cure_date field should not be included. If the user has 'Other Symptom', only then the other_symptom field should be included with a string value describing the symptom.`, options: SYMPTOM_CHOICES, - validator: (value) => { - if (!Array.isArray(value)) return false; - value.forEach((s) => { - if (!s.symptom || !s.onset_date) return false; - }); - return true; - }, + showFields: ["onset_date", "cure_date"], }, { friendlyName: "Physical Examination Info", @@ -36,9 +30,6 @@ const DAILY_ROUND_FORM_SCRIBE_DATA: Field[] = [ current: "", description: "This field is designated for storing detailed findings from the physical examination of the patient. It should include all observable physical attributes, conditions, or symptoms noted during the examination. When processing a doctor's transcript, identify and extract descriptions that pertain directly to the patient's physical state, such as visible conditions, physical symptoms, or any abnormalities noted by touch, sight, or measurement. This can include, but is not limited to, descriptions of skin conditions, swellings, lacerations, posture, mobility issues, and any other physically observable traits.", - validator: (value) => { - return typeof value === "string"; - }, }, { friendlyName: "Other Details", @@ -49,9 +40,6 @@ const DAILY_ROUND_FORM_SCRIBE_DATA: Field[] = [ "Patient reports trouble sleeping and a decreased appetite. Additionally, the patient is allergic to penicillin and has a history of asthma.", description: "This field is for capturing any supplementary details about the patient that are mentioned in the doctor's transcript but do not directly pertain to the physical examination findings. This includes, but is not limited to, behavioral observations, medical history, patient complaints, lifestyle factors, allergies, or any other non-physical observations that are relevant to the patient's overall health and well-being. When processing a transcript, extract information that describes the patient's health, habits, or conditions in a broader sense than what is observed through physical examination alone.", - validator: (value) => { - return typeof value === "string"; - }, }, { friendlyName: "Patient Category", @@ -66,9 +54,6 @@ const DAILY_ROUND_FORM_SCRIBE_DATA: Field[] = [ text: category.text, }), ), - validator: (value) => { - return typeof value === "string"; - }, }, { friendlyName: "Action", @@ -81,7 +66,6 @@ const DAILY_ROUND_FORM_SCRIBE_DATA: Field[] = [ id: action.text, text: action.desc, })), - validator: (value) => typeof value === "string", }, { friendlyName: "Review Interval", @@ -92,7 +76,6 @@ const DAILY_ROUND_FORM_SCRIBE_DATA: Field[] = [ description: "An integer to represent the interval at which the patient's condition is reviewed.", options: REVIEW_AT_CHOICES, - validator: (value) => typeof value === "number", }, { friendlyName: "Admitted To", @@ -102,7 +85,6 @@ const DAILY_ROUND_FORM_SCRIBE_DATA: Field[] = [ example: "General Ward", description: "A string to store the department or ward where the patient is admitted.", - validator: (value) => typeof value === "string", }, { friendlyName: "bp", @@ -112,12 +94,6 @@ const DAILY_ROUND_FORM_SCRIBE_DATA: Field[] = [ example: "{ systolic: 120 }", description: "An object to store the blood pressure of the patient. It may contain two integers, systolic and diastolic.", - validator: (value) => { - if (typeof value !== "object") return false; - if (value.systolic && typeof value.systolic !== "number") return false; - if (value.diastolic && typeof value.diastolic !== "number") return false; - return true; - }, }, { friendlyName: "Pulse", @@ -127,7 +103,6 @@ const DAILY_ROUND_FORM_SCRIBE_DATA: Field[] = [ example: "72", description: "An integer to store the pulse rate of the patient. It can be null if the pulse rate is not taken.", - validator: (value) => typeof value === "number", }, { friendlyName: "Respiratory Rate", @@ -137,7 +112,6 @@ const DAILY_ROUND_FORM_SCRIBE_DATA: Field[] = [ example: "16", description: "An integer to store the respiratory rate of the patient. It can be null if the respiratory rate is not taken.", - validator: (value) => typeof value === "number", }, { friendlyName: "Temperature", @@ -147,7 +121,6 @@ const DAILY_ROUND_FORM_SCRIBE_DATA: Field[] = [ example: "98.6", description: "A float to store the temperature of the patient. It can be null if the temperature is not taken.", - validator: (value) => typeof value === "number", }, { friendlyName: "SPO2", @@ -157,7 +130,6 @@ const DAILY_ROUND_FORM_SCRIBE_DATA: Field[] = [ example: "98", description: "An integer to store the SPO2 level of the patient. It can be null if the SPO2 level is not taken.", - validator: (value) => typeof value === "number", }, { friendlyName: "Rhythm", @@ -170,7 +142,6 @@ const DAILY_ROUND_FORM_SCRIBE_DATA: Field[] = [ id: rhythm.id, text: rhythm.desc ?? "", })), - validator: (value) => typeof value === "number", }, { friendlyName: "Rhythm Detail", @@ -180,7 +151,6 @@ const DAILY_ROUND_FORM_SCRIBE_DATA: Field[] = [ example: "Just minor irregularities.", description: "A string to store the details about the rhythm of the patient.", - validator: (value) => typeof value === "string", }, { friendlyName: "Level Of Consciousness", @@ -194,7 +164,6 @@ const DAILY_ROUND_FORM_SCRIBE_DATA: Field[] = [ id: loc.id, text: loc.value, })), - validator: (value) => typeof value === "string", }, { friendlyName: "Diagnosis", @@ -205,13 +174,7 @@ const DAILY_ROUND_FORM_SCRIBE_DATA: Field[] = [ "[{diagnosis: '4A42.0 Paediatric onset systemic sclerosis', verification_status: 'confirmed', is_principal: true}, {diagnosis: 2, verification_status: 'provisional', is_principal: false}]", description: "A list of objects to store the patient's diagnosis along with their verification status and whether it is the principal diagnosis. If not specifically said, set is_principal to false. NOTE: only one principal diagnosis can exist. The diagnosis field should be a string that may contain a corresponding diagnosis ID. The verification_status field should be a string with one of the following values: 'unconfirmed', 'provisional', 'differential', or 'confirmed'.", - validator: (value) => { - if (!Array.isArray(value)) return false; - value.forEach((d) => { - if (!d.diagnosis || !d.verification_status) return false; - }); - return true; - }, + showFields: ["diagnosis", "verification_status"], }, { friendlyName: "Investigations", @@ -240,14 +203,7 @@ const DAILY_ROUND_FORM_SCRIBE_DATA: Field[] = [ ]`, description: "A list of objects to store the patient's investigations. The type field should be an array of strings corresponding to the names of the investigations provided in the options. The repetitive field should be a boolean value. The time field should be a string and only be filled if repetitive field is false. The frequency field should be a string with one of the following values: '15 min', '30 min', '1 hr', '6 hrs', '12 hrs', '24 hrs', or '48 hrs' and should be only filled if this is a repititive investigation. The time field should be of the example format if present - (2024-07-31T18:10). The notes field should be a string. If the type is not available in options, DO NOT MAKE IT.", - validator: (value) => { - if (!Array.isArray(value)) return false; - value.forEach((i) => { - if (!i.type || !i.repetitive) return false; - if (i.repetitive && !i.frequency) return false; - }); - return true; - }, + showFields: ["type", "repetitive", "time", "frequency", "notes"], }, { friendlyName: "Prescriptions", @@ -280,10 +236,6 @@ const DAILY_ROUND_FORM_SCRIBE_DATA: Field[] = [ QOD: Alternate Day, QWK: Once a Week `, - validator: (value) => { - if (!Array.isArray(value)) return false; - return true; - }, }, { friendlyName: "PRN Prescriptions", @@ -303,10 +255,6 @@ const DAILY_ROUND_FORM_SCRIBE_DATA: Field[] = [ {base_dosage: "3 drop(s)", dosage_type:"PRN", indicator: "If patient gets fever", max_dosage: "5 drops(s)", min_hours_between_doses: 12, route: "IV", medicine: "Glentona", notes: "Example"} ]`, description: "A list of objects to store the patient's PRN prescriptions.", - validator: (value) => { - if (!Array.isArray(value)) return false; - return true; - }, }, /*{ friendlyName: "Round Type", diff --git a/src/Components/Scribe/scribeutils.ts b/src/Components/Scribe/scribeutils.ts new file mode 100644 index 00000000000..e0252c6c5d6 --- /dev/null +++ b/src/Components/Scribe/scribeutils.ts @@ -0,0 +1,49 @@ +import _ from "lodash"; + +export interface ScribeReducerOptions { + existingData: T[]; + newData: T[]; + comparer: (a: T, b: T) => boolean; + onUpdate?: (strippedItem: Partial, item: T) => Promise | unknown; + onDelete?: (item: T) => Promise | unknown; + onAdd?: (strippedItem: Partial, item: T) => Promise | unknown; + allowedFields: (keyof T)[]; +} + +export async function scribeReducer( + options: ScribeReducerOptions, +): Promise { + const { + existingData, + newData, + onUpdate, + onDelete, + onAdd, + allowedFields, + comparer, + } = options; + + const coveredItems: T[] = []; + for (const item of newData) { + const existingItem = existingData.find((d) => comparer(d, item)); + // Check if item is altered or added + if (!_.isEqual(item, existingItem)) { + if (existingItem) { + // item was altered + await onUpdate?.(_.pick(item, allowedFields), item); + } else { + // symptom does not exist, so must be added + await onAdd?.(_.pick(item, allowedFields), item); + } + } + coveredItems.push(item); + } + // check for deleted items + const deletedItems = + existingData?.filter((s) => !coveredItems.find((c) => comparer(c, s))) || + []; + for (const item of deletedItems) { + //item was deleted + await onDelete?.(item); + } +}