diff --git a/src/Common/constants.tsx b/src/Common/constants.tsx index 99cb3a7988f..4eb3b51d012 100644 --- a/src/Common/constants.tsx +++ b/src/Common/constants.tsx @@ -318,42 +318,6 @@ export const REVIEW_AT_CHOICES: Array = [ { id: 30 * 24 * 60, text: "1 month" }, ]; -export const SYMPTOM_CHOICES = [ - { id: 1, text: "ASYMPTOMATIC", isSingleSelect: true }, - { id: 2, text: "FEVER" }, - { id: 3, text: "SORE THROAT" }, - { id: 4, text: "COUGH" }, - { id: 5, text: "BREATHLESSNESS" }, - { id: 6, text: "MYALGIA" }, - { id: 7, text: "ABDOMINAL DISCOMFORT" }, - { id: 8, text: "VOMITING" }, - { id: 11, text: "SPUTUM" }, - { id: 12, text: "NAUSEA" }, - { id: 13, text: "CHEST PAIN" }, - { id: 14, text: "HEMOPTYSIS" }, - { id: 15, text: "NASAL DISCHARGE" }, - { id: 16, text: "BODY ACHE" }, - { id: 17, text: "DIARRHOEA" }, - { id: 18, text: "PAIN" }, - { id: 19, text: "PEDAL EDEMA" }, - { id: 20, text: "WOUND" }, - { id: 21, text: "CONSTIPATION" }, - { id: 22, text: "HEAD ACHE" }, - { id: 23, text: "BLEEDING" }, - { id: 24, text: "DIZZINESS" }, - { id: 25, text: "CHILLS" }, - { id: 26, text: "GENERAL WEAKNESS" }, - { id: 27, text: "IRRITABILITY" }, - { id: 28, text: "CONFUSION" }, - { id: 29, text: "ABDOMINAL PAIN" }, - { id: 30, text: "JOINT PAIN" }, - { id: 31, text: "REDNESS OF EYES" }, - { id: 32, text: "ANOREXIA" }, - { id: 33, text: "NEW LOSS OF TASTE" }, - { id: 34, text: "NEW LOSS OF SMELL" }, - { id: 9, text: "OTHERS" }, -]; - export const DISCHARGE_REASONS = [ { id: 1, text: "Recovered" }, { id: 2, text: "Referred" }, diff --git a/src/Components/Common/SymptomsSelect.tsx b/src/Components/Common/SymptomsSelect.tsx deleted file mode 100644 index cdca3fe60dc..00000000000 --- a/src/Components/Common/SymptomsSelect.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import CareIcon from "../../CAREUI/icons/CareIcon"; -import { SYMPTOM_CHOICES } from "../../Common/constants"; -import { AutocompleteMutliSelect } from "../Form/FormFields/AutocompleteMultiselect"; -import FormField from "../Form/FormFields/FormField"; -import { - FormFieldBaseProps, - useFormFieldPropsResolver, -} from "../Form/FormFields/Utils"; - -const ASYMPTOMATIC_ID = 1; - -/** - * A `FormField` component to select symptoms. - * - * - If "Asymptomatic" is selected, every other selections are unselected. - * - If any non "Asymptomatic" value is selected, ensures "Asymptomatic" is - * unselected. - * - For other scenarios, this simply works like a `MultiSelect`. - */ -export const SymptomsSelect = (props: FormFieldBaseProps) => { - const field = useFormFieldPropsResolver(props); - - const updateSelection = (value: number[]) => { - // Skip the complexities if no initial value was present - if (!props.value?.length) return field.handleChange(value); - - const initialValue = props.value || []; - - if (initialValue.includes(ASYMPTOMATIC_ID) && value.length > 1) { - // If asym. already selected, and new selections have more than one value - const asymptomaticIndex = value.indexOf(1); - if (asymptomaticIndex > -1) { - // unselect asym. - value.splice(asymptomaticIndex, 1); - return field.handleChange(value); - } - } - - if (!initialValue.includes(ASYMPTOMATIC_ID) && value.includes(1)) { - // If new selections have asym., unselect everything else - return field.handleChange([ASYMPTOMATIC_ID]); - } - - field.handleChange(value); - }; - - const getDescription = ({ id }: { id: number }) => { - const value = props.value || []; - if (!value.length) return; - - if (value.includes(ASYMPTOMATIC_ID) && id !== ASYMPTOMATIC_ID) - return ( -
- - - also unselects Asymptomatic - -
- ); - - if (!value.includes(ASYMPTOMATIC_ID) && id === ASYMPTOMATIC_ID) - return ( - - - {`also unselects the other ${value.length} option(s)`} - - ); - }; - - return ( - - option.text} - optionValue={(option) => option.id} - value={props.value || []} - optionDescription={getDescription} - onChange={updateSelection} - /> - - ); -}; diff --git a/src/Components/Diagnosis/utils.ts b/src/Components/Diagnosis/utils.ts index c53f9b81bc1..1cac3cecbca 100644 --- a/src/Components/Diagnosis/utils.ts +++ b/src/Components/Diagnosis/utils.ts @@ -2,8 +2,6 @@ import routes from "../../Redux/api"; import request from "../../Utils/request/request"; import { ICD11DiagnosisModel } from "./types"; -// TODO: cache ICD11 responses and hit the cache if present instead of making an API call. - export const getDiagnosisById = async (id: ICD11DiagnosisModel["id"]) => { return (await request(routes.getICD11Diagnosis, { pathParams: { id } })).data; }; diff --git a/src/Components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx index 2ba4f7929ae..be2751f0033 100644 --- a/src/Components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx +++ b/src/Components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx @@ -5,7 +5,7 @@ import { BedModel } from "../models"; import HL7PatientVitalsMonitor from "../../VitalsMonitor/HL7PatientVitalsMonitor"; import VentilatorPatientVitalsMonitor from "../../VitalsMonitor/VentilatorPatientVitalsMonitor"; import useVitalsAspectRatioConfig from "../../VitalsMonitor/useVitalsAspectRatioConfig"; -import { DISCHARGE_REASONS, SYMPTOM_CHOICES } from "../../../Common/constants"; +import { DISCHARGE_REASONS } from "../../../Common/constants"; import PrescriptionsTable from "../../Medicine/PrescriptionsTable"; import Chip from "../../../CAREUI/display/Chip"; import { @@ -23,6 +23,7 @@ import { getVitalsMonitorSocketUrl } from "../../VitalsMonitor/utils"; import useQuery from "../../../Utils/request/useQuery"; import routes from "../../../Redux/api"; import CareIcon from "../../../CAREUI/icons/CareIcon"; +import EncounterSymptomsCard from "../../Symptoms/SymptomsCard"; const PageTitle = lazy(() => import("../../Common/PageTitle")); @@ -363,91 +364,10 @@ export const ConsultationUpdatesTab = (props: ConsultationTabProps) => { )} )} - {props.consultationData.symptoms_text && ( -
-
-

- Symptoms -

-
-
- Last Daily Update -
- {props.consultationData.last_daily_round - ?.additional_symptoms && ( - <> -
- {props.consultationData.last_daily_round?.additional_symptoms.map( - (symptom: any, index: number) => ( - choice.id === symptom, - )?.text ?? "Err. Unknown" - } - size="small" - /> - ), - )} -
- {props.consultationData.last_daily_round - ?.other_symptoms && ( -
-
- Other Symptoms: -
- { - props.consultationData.last_daily_round - ?.other_symptoms - } -
- )} - - from{" "} - {formatDate( - props.consultationData.last_daily_round.taken_at, - )} - - - )} -
-
- Consultation Update -
-
- {props.consultationData.symptoms?.map( - (symptom, index) => ( - choice.id === symptom, - )?.text ?? "Err. Unknown" - } - size="small" - /> - ), - )} -
- {props.consultationData.other_symptoms && ( -
-
- Other Symptoms: -
- {props.consultationData.other_symptoms} -
- )} - - from{" "} - {props.consultationData.symptoms_onset_date - ? formatDate(props.consultationData.symptoms_onset_date) - : "--/--/----"} - -
-
-
- )} + +
+ +
{props.consultationData.history_of_present_illness && (
diff --git a/src/Components/Facility/ConsultationDetails/index.tsx b/src/Components/Facility/ConsultationDetails/index.tsx index 38882c66f33..ee1f7bda7ff 100644 --- a/src/Components/Facility/ConsultationDetails/index.tsx +++ b/src/Components/Facility/ConsultationDetails/index.tsx @@ -1,8 +1,4 @@ -import { - CONSULTATION_TABS, - GENDER_TYPES, - SYMPTOM_CHOICES, -} from "../../../Common/constants"; +import { CONSULTATION_TABS, GENDER_TYPES } from "../../../Common/constants"; import { ConsultationModel } from "../models"; import { getConsultation, @@ -44,7 +40,6 @@ import { CameraFeedPermittedUserTypes } from "../../../Utils/permissions"; const Loading = lazy(() => import("../../Common/Loading")); const PageTitle = lazy(() => import("../../Common/PageTitle")); -const symptomChoices = [...SYMPTOM_CHOICES]; export interface ConsultationTabProps { consultationId: string; @@ -114,15 +109,15 @@ export const ConsultationDetails = (props: any) => { ...res.data, symptoms_text: "", }; - if (res.data.symptoms?.length) { - const symptoms = res.data.symptoms - .filter((symptom: number) => symptom !== 9) - .map((symptom: number) => { - const option = symptomChoices.find((i) => i.id === symptom); - return option ? option.text.toLowerCase() : symptom; - }); - data.symptoms_text = symptoms.join(", "); - } + // if (res.data.symptoms?.length) { + // const symptoms = res.data.symptoms + // .filter((symptom: number) => symptom !== 9) + // .map((symptom: number) => { + // const option = symptomChoices.find((i) => i.id === symptom); + // return option ? option.text.toLowerCase() : symptom; + // }); + // data.symptoms_text = symptoms.join(", "); + // } if (facilityId != data.facility || patientId != data.patient) { navigate( `/facility/${data.facility}/patient/${data.patient}/consultation/${data?.id}`, diff --git a/src/Components/Facility/ConsultationForm.tsx b/src/Components/Facility/ConsultationForm.tsx index b7b99d85448..594769ec1c4 100644 --- a/src/Components/Facility/ConsultationForm.tsx +++ b/src/Components/Facility/ConsultationForm.tsx @@ -23,7 +23,6 @@ import { BedSelect } from "../Common/BedSelect"; import Beds from "./Consultations/Beds"; import CareIcon from "../../CAREUI/icons/CareIcon"; import CheckBoxFormField from "../Form/FormFields/CheckBoxFormField"; -import DateFormField from "../Form/FormFields/DateFormField"; import { FacilitySelect } from "../Common/FacilitySelect"; import { FieldChangeEvent, @@ -32,7 +31,6 @@ import { import { FormAction } from "../Form/Utils"; import PatientCategorySelect from "../Patient/PatientCategorySelect"; import { SelectFormField } from "../Form/FormFields/SelectFormField"; -import { SymptomsSelect } from "../Common/SymptomsSelect"; import TextAreaFormField from "../Form/FormFields/TextAreaFormField"; import TextFormField from "../Form/FormFields/TextFormField"; import UserAutocompleteFormField from "../Common/UserAutocompleteFormField"; @@ -61,6 +59,12 @@ import request from "../../Utils/request/request.js"; import routes from "../../Redux/api.js"; import useQuery from "../../Utils/request/useQuery.js"; import { t } from "i18next"; +import { Writable } from "../../Utils/types.js"; +import { EncounterSymptom } from "../Symptoms/types.js"; +import { + EncounterSymptomsBuilder, + CreateSymptomsBuilder, +} from "../Symptoms/SymptomsBuilder.js"; const Loading = lazy(() => import("../Common/Loading")); const PageTitle = lazy(() => import("../Common/PageTitle")); @@ -68,9 +72,7 @@ const PageTitle = lazy(() => import("../Common/PageTitle")); type BooleanStrings = "true" | "false"; type FormDetails = { - symptoms: number[]; - other_symptoms: string; - symptoms_onset_date?: Date; + is_asymptomatic: boolean; suggestion: ConsultationSuggestionValue; route_to_facility?: RouteToFacility; patient: string; @@ -91,6 +93,8 @@ type FormDetails = { treating_physician_object: UserModel | null; create_diagnoses: CreateDiagnosis[]; diagnoses: ConsultationDiagnosis[]; + symptoms: EncounterSymptom[]; + create_symptoms: Writable[]; is_kasp: BooleanStrings; kasp_enabled_date: null; examination_details: string; @@ -119,9 +123,9 @@ type FormDetails = { }; const initForm: FormDetails = { + is_asymptomatic: false, + create_symptoms: [], symptoms: [], - other_symptoms: "", - symptoms_onset_date: undefined, suggestion: "A", route_to_facility: undefined, patient: "", @@ -315,10 +319,6 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { }); }, []); - const hasSymptoms = - !!state.form.symptoms.length && !state.form.symptoms.includes(1); - const isOtherSymptomsSelected = state.form.symptoms.includes(9); - const handleFormFieldChange: FieldChangeEventHandler = (event) => { if (event.name === "suggestion" && event.value === "DD") { dispatch({ @@ -329,12 +329,21 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { consultation_notes: "Patient declared dead", }, }); - } else { + return; + } + + if (event.name === "is_asymptomatic" && event.value === true) { dispatch({ type: "set_form", - form: { ...state.form, [event.name]: event.value }, + form: { ...state.form, [event.name]: event.value, create_symptoms: [] }, }); + return; } + + dispatch({ + type: "set_form", + form: { ...state.form, [event.name]: event.value }, + }); }; const { loading: consultationLoading, refetch } = useQuery( @@ -372,9 +381,6 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { if (data) { const formData = { ...data, - symptoms_onset_date: - data.symptoms_onset_date && - isoStringToDate(data.symptoms_onset_date), encounter_date: isoStringToDate(data.encounter_date), icu_admission_date: data.icu_admission_date && @@ -435,12 +441,6 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { Object.keys(state.form).forEach((field) => { switch (field) { - case "symptoms": - if (!state.form[field] || !state.form[field].length) { - errors[field] = "Please select the symptoms"; - invalidForm = true; - } - return; case "category": if (!state.form[field]) { errors[field] = "Please select a category"; @@ -469,18 +469,6 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { invalidForm = true; } return; - case "other_symptoms": - if (isOtherSymptomsSelected && !state.form[field]) { - errors[field] = "Please enter the other symptom details"; - invalidForm = true; - } - return; - case "symptoms_onset_date": - if (hasSymptoms && !state.form[field]) { - errors[field] = "Please enter date of onset of the above symptoms"; - invalidForm = true; - } - return; case "encounter_date": if (!state.form[field]) { errors[field] = "Field is required"; @@ -501,6 +489,17 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { invalidForm = true; } return; + case "create_symptoms": + if ( + !isUpdate && + !state.form.is_asymptomatic && + state.form[field].length === 0 + ) { + errors[field] = + "Symptoms needs to be added as the patient is symptomatic"; + invalidForm = true; + } + return; case "death_datetime": if (state.form.suggestion === "DD" && !state.form[field]) { errors[field] = "Please enter the date & time of death"; @@ -679,13 +678,6 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { if (validated) { setIsLoading(true); const data: any = { - symptoms: state.form.symptoms, - other_symptoms: isOtherSymptomsSelected - ? state.form.other_symptoms - : undefined, - symptoms_onset_date: hasSymptoms - ? state.form.symptoms_onset_date - : undefined, suggestion: state.form.suggestion, route_to_facility: state.form.route_to_facility, admitted: state.form.suggestion === "A", @@ -698,6 +690,7 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { treatment_plan: state.form.treatment_plan, discharge_date: state.form.discharge_date, create_diagnoses: isUpdate ? undefined : state.form.create_diagnoses, + create_symptoms: isUpdate ? undefined : state.form.create_symptoms, treating_physician: state.form.treating_physician, investigation: state.form.InvestigationAdvice, procedure: state.form.procedure, @@ -1025,41 +1018,48 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => {
)} -
- -
- {isOtherSymptomsSelected && ( -
- -
- )} +
+
+ Symptoms - {hasSymptoms && ( -
- + {!isUpdate && ( + + )} + +
+ {isUpdate ? ( + + ) : ( + { + handleFormFieldChange({ + name: "create_symptoms", + value: symptoms, + }); + }} + /> + )} + +
- )} +
+
; - symptoms_text?: string; - symptoms_onset_date?: string; consultation_notes?: string; is_telemedicine?: boolean; procedure?: ProcedureType[]; diff --git a/src/Components/Form/FormFields/AutocompleteMultiselect.tsx b/src/Components/Form/FormFields/AutocompleteMultiselect.tsx index bed6a41f3c0..3bdbffdc6cb 100644 --- a/src/Components/Form/FormFields/AutocompleteMultiselect.tsx +++ b/src/Components/Form/FormFields/AutocompleteMultiselect.tsx @@ -84,7 +84,7 @@ export const AutocompleteMutliSelect = ( return { option, label, - description: props.optionDescription && props.optionDescription(option), + description: props.optionDescription?.(option), search: label.toLowerCase(), value: (props.optionValue ? props.optionValue(option) : option) as V, }; diff --git a/src/Components/Patient/DailyRoundListDetails.tsx b/src/Components/Patient/DailyRoundListDetails.tsx index 3c5e70044ed..66536c986ca 100644 --- a/src/Components/Patient/DailyRoundListDetails.tsx +++ b/src/Components/Patient/DailyRoundListDetails.tsx @@ -1,5 +1,5 @@ import { lazy, useState } from "react"; -import { CONSCIOUSNESS_LEVEL, SYMPTOM_CHOICES } from "../../Common/constants"; +import { CONSCIOUSNESS_LEVEL } from "../../Common/constants"; import { DailyRoundsModel } from "./models"; import Page from "../Common/components/Page"; import ButtonV2 from "../Common/components/ButtonV2"; @@ -7,7 +7,6 @@ import { formatDateTime } from "../../Utils/utils"; import useQuery from "../../Utils/request/useQuery"; import routes from "../../Redux/api"; const Loading = lazy(() => import("../Common/Loading")); -const symptomChoices = [...SYMPTOM_CHOICES]; export const DailyRoundListDetails = (props: any) => { const { facilityId, patientId, consultationId, id } = props; @@ -21,16 +20,8 @@ export const DailyRoundListDetails = (props: any) => { const tdata: DailyRoundsModel = { ...data, temperature: Number(data.temperature) ? data.temperature : "", - additional_symptoms_text: "", medication_given: data.medication_given ?? [], }; - if (data.additional_symptoms?.length) { - const symptoms = data.additional_symptoms.map((symptom: number) => { - const option = symptomChoices.find((i) => i.id === symptom); - return option ? option.text.toLowerCase() : symptom; - }); - tdata.additional_symptoms_text = symptoms.join(", "); - } setDailyRoundListDetails(tdata); } }, @@ -85,12 +76,6 @@ export const DailyRoundListDetails = (props: any) => { SpO2: {dailyRoundListDetailsData.ventilator_spo2 ?? "-"}
-
- - Additional Symptoms:{" "} - - {dailyRoundListDetailsData.additional_symptoms_text ?? "-"} -
Admitted To *:{" "} @@ -103,12 +88,6 @@ export const DailyRoundListDetails = (props: any) => { {dailyRoundListDetailsData.physical_examination_info ?? "-"}
-
- - Other Symptoms:{" "} - - {dailyRoundListDetailsData.other_symptoms ?? "-"} -
Other Details:{" "} diff --git a/src/Components/Patient/DailyRounds.tsx b/src/Components/Patient/DailyRounds.tsx index dd15d44facc..66ebdca4fd2 100644 --- a/src/Components/Patient/DailyRounds.tsx +++ b/src/Components/Patient/DailyRounds.tsx @@ -17,7 +17,6 @@ import { capitalize } from "lodash-es"; import BloodPressureFormField, { BloodPressureValidator, } from "../Common/BloodPressureFormField"; -import { SymptomsSelect } from "../Common/SymptomsSelect"; import TemperatureFormField from "../Common/TemperatureFormField"; import { Cancel, Submit } from "../Common/components/ButtonV2"; import Page from "../Common/components/Page"; @@ -33,11 +32,11 @@ import routes from "../../Redux/api"; import { Scribe } from "../Scribe/Scribe"; import { DAILY_ROUND_FORM_SCRIBE_DATA } from "../Scribe/formDetails"; import { DailyRoundsModel } from "./models"; +import { EncounterSymptomsBuilder } from "../Symptoms/SymptomsBuilder"; +import { FieldLabel } from "../Form/FormFields/FormField"; const Loading = lazy(() => import("../Common/Loading")); const initForm: any = { - additional_symptoms: [], - other_symptoms: "", physical_examination_info: "", other_details: "", patient_category: "", @@ -120,7 +119,6 @@ export const DailyRounds = (props: any) => { const formFields = [ "physical_examination_info", "other_details", - "additional_symptoms", "action", "review_interval", "bp", @@ -201,15 +199,6 @@ export const DailyRounds = (props: any) => { invalidForm = true; } return; - case "other_symptoms": - if ( - state.form.additional_symptoms?.includes(9) && - !state.form[field] - ) { - errors[field] = "Please enter the other symptom details"; - invalidForm = true; - } - return; case "bp": { const error = BloodPressureValidator(state.form.bp); if (error) { @@ -237,11 +226,6 @@ export const DailyRounds = (props: any) => { taken_at: state.form.taken_at ? state.form.taken_at : new Date().toISOString(), - - additional_symptoms: state.form.additional_symptoms, - other_symptoms: state.form.additional_symptoms?.includes(9) - ? state.form.other_symptoms - : undefined, admitted_to: (state.form.admitted === "Select" ? undefined @@ -436,22 +420,11 @@ export const DailyRounds = (props: any) => { label="Other Details" rows={5} /> - - {state.form.additional_symptoms?.includes(9) && ( -
- -
- )} +
+ Symptoms + +
; medication_given?: Array; - additional_symptoms_text?: string; action?: string; review_interval?: number; id?: string; - other_symptoms?: string; admitted_to?: string; patient_category?: PatientCategory; output?: DailyRoundsOutput[]; diff --git a/src/Components/Scribe/formDetails.ts b/src/Components/Scribe/formDetails.ts index 2b56ce90df5..74673adea70 100644 --- a/src/Components/Scribe/formDetails.ts +++ b/src/Components/Scribe/formDetails.ts @@ -3,9 +3,9 @@ import { PATIENT_CATEGORIES, REVIEW_AT_CHOICES, RHYTHM_CHOICES, - SYMPTOM_CHOICES, TELEMEDICINE_ACTIONS, } from "../../Common/constants"; +import { SYMPTOM_CHOICES } from "../Symptoms/types"; import { Field } from "./Scribe"; export const DAILY_ROUND_FORM_SCRIBE_DATA: Field[] = [ diff --git a/src/Components/Symptoms/SymptomsBuilder.tsx b/src/Components/Symptoms/SymptomsBuilder.tsx new file mode 100644 index 00000000000..4d142a67841 --- /dev/null +++ b/src/Components/Symptoms/SymptomsBuilder.tsx @@ -0,0 +1,379 @@ +import { useState } from "react"; +import { Writable } from "../../Utils/types"; +import { + EncounterSymptom, + OTHER_SYMPTOM_CHOICE, + SYMPTOM_CHOICES, +} from "./types"; +import AutocompleteMultiSelectFormField from "../Form/FormFields/AutocompleteMultiselect"; +import DateFormField from "../Form/FormFields/DateFormField"; +import ButtonV2 from "../Common/components/ButtonV2"; +import TextAreaFormField from "../Form/FormFields/TextAreaFormField"; +import { classNames, dateQueryString } from "../../Utils/utils"; +import { FieldChangeEvent } from "../Form/FormFields/Utils"; +import CareIcon from "../../CAREUI/icons/CareIcon"; +import useSlug from "../../Common/hooks/useSlug"; +import useQuery from "../../Utils/request/useQuery"; +import SymptomsApi from "./api"; +import request from "../../Utils/request/request"; +import { Success } from "../../Utils/Notifications"; +import { sortByOnsetDate } from "./utils"; + +export const CreateSymptomsBuilder = (props: { + value: Writable[]; + onChange: (value: Writable[]) => void; +}) => { + return ( +
+
    + {props.value.map((obj, index, arr) => { + const handleUpdate = (event: FieldChangeEvent) => { + const updated = { ...obj, [event.name]: event.value }; + props.onChange(arr.map((old, i) => (i === index ? updated : old))); + }; + + const handleRemove = () => { + props.onChange(arr.filter((_, i) => i !== index)); + }; + + return ( +
  • + +
  • + ); + })} +
+ + {props.value.length === 0 && ( +
+ No symptoms added +
+ )} + +
+ props.onChange([...props.value, ...objects])} + /> +
+
+ ); +}; + +export const EncounterSymptomsBuilder = (props: { showAll?: boolean }) => { + const consultationId = useSlug("consultation"); + + const [isProcessing, setIsProcessing] = useState(false); + const { data, loading, refetch } = useQuery(SymptomsApi.list, { + pathParams: { consultationId }, + query: { limit: 100 }, + }); + + if (!data) { + return ( +
+ + Fetching symptom records... +
+ ); + } + + let items = sortByOnsetDate(data.results); + if (!props.showAll) { + items = items.filter( + (i) => i.clinical_impression_status !== "entered-in-error", + ); + } + + return ( +
+
    + {items.map((symptom) => { + const handleUpdate = async (event: FieldChangeEvent) => { + setIsProcessing(true); + await request(SymptomsApi.partialUpdate, { + pathParams: { consultationId, external_id: symptom.id }, + body: { [event.name]: event.value }, + }); + await refetch(); + setIsProcessing(false); + }; + + const handleMarkAsEnteredInError = async () => { + setIsProcessing(true); + await request(SymptomsApi.markAsEnteredInError, { + pathParams: { consultationId, external_id: symptom.id }, + }); + await refetch(); + setIsProcessing(false); + }; + + return ( +
  • + +
  • + ); + })} +
+ + {items.length === 0 && ( +
+ Patient is Asymptomatic +
+ )} + +
+ refetch()} + /> +
+
+ ); +}; + +const SymptomEntry = (props: { + disabled?: boolean; + value: Writable | EncounterSymptom; + onChange: (event: FieldChangeEvent) => void; + onRemove: () => void; +}) => { + const symptom = props.value; + const disabled = + props.disabled || symptom.clinical_impression_status === "entered-in-error"; + return ( +
+ + +
+
+ + + + {symptom.clinical_impression_status === "entered-in-error" && ( + + Entered in Error + + )} +
+ + + +
+
+ ); +}; + +const AddSymptom = (props: { + disabled?: boolean; + existing: (Writable | EncounterSymptom)[]; + onAdd?: (value: Writable[]) => void; + consultationId?: string; +}) => { + const [processing, setProcessing] = useState(false); + const [selected, setSelected] = useState([]); + const [otherSymptom, setOtherSymptom] = useState(""); + const [onsetDate, setOnsetDate] = useState(); + + const activeSymptomIds = props.existing + .filter((o) => o.symptom !== OTHER_SYMPTOM_CHOICE.id && !o.cure_date) + .map((o) => o.symptom); + + const handleAdd = async () => { + const objects = selected.map((symptom) => { + return { + symptom, + onset_date: dateQueryString(onsetDate), + other_symptom: + symptom === OTHER_SYMPTOM_CHOICE.id ? otherSymptom : undefined, + }; + }); + + if (props.consultationId) { + const responses = await Promise.all( + objects.map((body) => + request(SymptomsApi.add, { + body, + pathParams: { consultationId: props.consultationId! }, + }), + ), + ); + + if (responses.every(({ res }) => !!res?.ok)) { + Success({ msg: "Symptoms records updated successfully" }); + } + } + props.onAdd?.(objects); + + setSelected([]); + setOtherSymptom(""); + }; + + const hasSymptoms = !!selected.length; + const otherSymptomValid = selected.includes(OTHER_SYMPTOM_CHOICE.id) + ? !!otherSymptom.trim() + : true; + + return ( +
+ setOnsetDate(value)} + errorClassName="hidden" + /> +
+ setSelected(e.value)} + options={SYMPTOM_CHOICES.filter( + ({ id }) => !activeSymptomIds.includes(id), + )} + optionLabel={(option) => option.text} + optionValue={(option) => option.id} + errorClassName="hidden" + /> + {selected.includes(OTHER_SYMPTOM_CHOICE.id) && ( + setOtherSymptom(value)} + errorClassName="hidden" + /> + )} +
+ { + setProcessing(true); + await handleAdd(); + setProcessing(false); + }} + > + {processing ? ( + <> + + Adding... + + ) : ( + Add Symptom(s) + )} + +
+ ); +}; + +export const SymptomText = (props: { + value: Writable | EncounterSymptom; +}) => { + const symptom = + SYMPTOM_CHOICES.find(({ id }) => props.value.symptom === id) || + OTHER_SYMPTOM_CHOICE; + + const isOtherSymptom = symptom.id === OTHER_SYMPTOM_CHOICE.id; + + return isOtherSymptom ? ( + <> + Other: + + {props.value.other_symptom || "Not specified"} + + + ) : ( + symptom.text + ); +}; diff --git a/src/Components/Symptoms/SymptomsCard.tsx b/src/Components/Symptoms/SymptomsCard.tsx new file mode 100644 index 00000000000..3ea124c17b2 --- /dev/null +++ b/src/Components/Symptoms/SymptomsCard.tsx @@ -0,0 +1,84 @@ +import RecordMeta from "../../CAREUI/display/RecordMeta"; +import useSlug from "../../Common/hooks/useSlug"; +import useQuery from "../../Utils/request/useQuery"; +import { SymptomText } from "./SymptomsBuilder"; +import SymptomsApi from "./api"; +import { type EncounterSymptom } from "./types"; +import { groupAndSortSymptoms } from "./utils"; +import CareIcon from "../../CAREUI/icons/CareIcon"; + +// TODO: switch to list from events as timeline view instead once filter event by event type name is done +const EncounterSymptomsCard = () => { + const consultationId = useSlug("consultation"); + + const { data } = useQuery(SymptomsApi.list, { + pathParams: { consultationId }, + query: { limit: 100 }, + }); + + if (!data) { + return ( +
+ + Fetching symptom records... +
+ ); + } + + const records = groupAndSortSymptoms(data.results); + + return ( +
+

+ Symptoms +

+ +
+ +
+ +
+
+
+ ); +}; + +const SymptomsSection = (props: { + title: string; + symptoms: EncounterSymptom[]; +}) => { + return ( +
+

+ {props.title} +

+
    + {props.symptoms.map((record) => ( +
  • +
    + +
    + + + Onset + + {record.cure_date && ( + + {"; Cured"} + + )} + +
    +
  • + ))} +
+ {!props.symptoms.length && ( +
+ No symptoms +
+ )} +
+ ); +}; + +export default EncounterSymptomsCard; diff --git a/src/Components/Symptoms/api.ts b/src/Components/Symptoms/api.ts new file mode 100644 index 00000000000..1cce062cb1e --- /dev/null +++ b/src/Components/Symptoms/api.ts @@ -0,0 +1,47 @@ +import { Type } from "../../Redux/api"; +import { PaginatedResponse } from "../../Utils/request/types"; +import { WritableOnly } from "../../Utils/types"; +import { EncounterSymptom } from "./types"; + +const SymptomsApi = { + list: { + method: "GET", + path: "/api/v1/consultation/{consultationId}/symptoms/", + TRes: Type>(), + }, + + add: { + path: "/api/v1/consultation/{consultationId}/symptoms/", + method: "POST", + TRes: Type(), + TBody: Type>(), + }, + + retrieve: { + method: "GET", + path: "/api/v1/consultation/{consultationId}/symptoms/{external_id}/", + TRes: Type(), + }, + + update: { + method: "PUT", + path: "/api/v1/consultation/{consultationId}/symptoms/{external_id}/", + TBody: Type>(), + TRes: Type(), + }, + + partialUpdate: { + method: "PATCH", + path: "/api/v1/consultation/{consultationId}/symptoms/{external_id}/", + TBody: Type>>(), + TRes: Type(), + }, + + markAsEnteredInError: { + method: "DELETE", + path: "/api/v1/consultation/{consultationId}/symptoms/{external_id}/", + TRes: Type(), + }, +} as const; + +export default SymptomsApi; diff --git a/src/Components/Symptoms/types.ts b/src/Components/Symptoms/types.ts new file mode 100644 index 00000000000..78e769ce9a9 --- /dev/null +++ b/src/Components/Symptoms/types.ts @@ -0,0 +1,52 @@ +import { BaseModel } from "../../Utils/types"; + +export const OTHER_SYMPTOM_CHOICE = { id: 9, text: "Other Symptom" } as const; + +export const SYMPTOM_CHOICES = [ + { id: 2, text: "Fever" }, + { id: 3, text: "Sore throat" }, + { id: 4, text: "Cough" }, + { id: 5, text: "Breathlessness" }, + { id: 6, text: "Myalgia" }, + { id: 7, text: "Abdominal discomfort" }, + { id: 8, text: "Vomiting" }, + { id: 11, text: "Sputum" }, + { id: 12, text: "Nausea" }, + { id: 13, text: "Chest pain" }, + { id: 14, text: "Hemoptysis" }, + { id: 15, text: "Nasal discharge" }, + { id: 16, text: "Body ache" }, + { id: 17, text: "Diarrhoea" }, + { id: 18, text: "Pain" }, + { id: 19, text: "Pedal Edema" }, + { id: 20, text: "Wound" }, + { id: 21, text: "Constipation" }, + { id: 22, text: "Head ache" }, + { id: 23, text: "Bleeding" }, + { id: 24, text: "Dizziness" }, + { id: 25, text: "Chills" }, + { id: 26, text: "General weakness" }, + { id: 27, text: "Irritability" }, + { id: 28, text: "Confusion" }, + { id: 29, text: "Abdominal pain" }, + { id: 30, text: "Join pain" }, + { id: 31, text: "Redness of eyes" }, + { id: 32, text: "Anorexia" }, + { id: 33, text: "New loss of taste" }, + { id: 34, text: "New loss of smell" }, + OTHER_SYMPTOM_CHOICE, +] as const; + +type ClinicalImpressionStatus = + | "in-progress" + | "completed" + | "entered-in-error"; + +export interface EncounterSymptom extends BaseModel { + symptom: (typeof SYMPTOM_CHOICES)[number]["id"]; + other_symptom?: string | null; + onset_date: string; + cure_date?: string | null; + readonly clinical_impression_status: ClinicalImpressionStatus; + readonly is_migrated: boolean; +} diff --git a/src/Components/Symptoms/utils.ts b/src/Components/Symptoms/utils.ts new file mode 100644 index 00000000000..997acfc914b --- /dev/null +++ b/src/Components/Symptoms/utils.ts @@ -0,0 +1,37 @@ +import { Writable } from "../../Utils/types"; +import { compareByDateString } from "../../Utils/utils"; +import { EncounterSymptom } from "./types"; + +// TODO: switch to using Object.groupBy(...) instead once upgraded to node v22 +export const groupAndSortSymptoms = < + T extends Writable | EncounterSymptom, +>( + records: T[], +) => { + const result: Record = { + "entered-in-error": [], + "in-progress": [], + completed: [], + }; + + for (const record of records) { + const status = + record.clinical_impression_status || + (record.cure_date ? "completed" : "in-progress"); + result[status].push(record); + } + + result["completed"] = sortByOnsetDate(result["completed"]); + result["in-progress"] = sortByOnsetDate(result["in-progress"]); + result["entered-in-error"] = sortByOnsetDate(result["entered-in-error"]); + + return result; +}; + +export const sortByOnsetDate = < + T extends Writable | EncounterSymptom, +>( + records: T[], +) => { + return records.sort(compareByDateString("onset_date")); +}; diff --git a/src/Utils/types.ts b/src/Utils/types.ts new file mode 100644 index 00000000000..519ede36ffc --- /dev/null +++ b/src/Utils/types.ts @@ -0,0 +1,36 @@ +import { PerformedByModel } from "../Components/HCX/misc"; + +export interface BaseModel { + readonly id: string; + readonly modified_date: string; + readonly created_date: string; + readonly created_by: PerformedByModel; + readonly updated_by: PerformedByModel; +} + +export type Writable = { + [P in keyof T as IfEquals< + { [Q in P]: T[P] }, + { -readonly [Q in P]: T[P] }, + never, + P + >]?: undefined; +} & { + [P in keyof T as IfEquals< + { [Q in P]: T[P] }, + { -readonly [Q in P]: T[P] }, + P, + never + >]: T[P]; +}; + +export type WritableOnly = { + [P in keyof T as IfEquals< + { [Q in P]: T[P] }, + { -readonly [Q in P]: T[P] }, + P + >]: T[P]; +}; + +type IfEquals = + (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2 ? A : B; diff --git a/src/Utils/utils.ts b/src/Utils/utils.ts index 38ffc2e1c66..d599a494b1c 100644 --- a/src/Utils/utils.ts +++ b/src/Utils/utils.ts @@ -409,6 +409,14 @@ export const compareBy = (key: keyof T) => { }; }; +export const compareByDateString = (key: keyof T) => { + return (a: T, b: T) => { + const aV = new Date(a[key] as string); + const bV = new Date(b[key] as string); + return aV < bV ? -1 : aV > bV ? 1 : 0; + }; +}; + export const isValidUrl = (url?: string) => { try { new URL(url ?? "");