From f575710c34a1ea65119de1f27f4bb7a60949d1b5 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Thu, 9 Nov 2023 17:08:20 +0530 Subject: [PATCH] Refactor Consultation Diagnosis (M2M relation, additional verification statuses and other improvements) (#6528) * useSlug: support for fallbacks for graceful handling * Miscellaneous changes / cleanup * Add API routes * remove old diagnoses from Consultation Form * Add component: `AddICD11Diagnosis` * Add component: `ConditionVerificationStatusMenu` * uncomplicate things * Add component `ConsultationDiangosisEntry` * Basic implementation * i have no idea what these are, but lots of bugs gone * goodnight for today * responsiveness * pre-sort by verification status * Adds help text * responsiveness * fix overflow * show diagnoses in consultation dashboard * fix consultation create * Show chapter of diagnosis for principal diagnosis (#6541) * Add PrincipalDiagnosisCard component and update ConsultationDiagnosisBuilder * Add chapter field to ICD11DiagnosisModel * fixes #6544; Principal Diagnosis as Dropdown * fix cypress * minor fix --- .../pageobject/Patient/PatientConsultation.ts | 15 +- src/Common/hooks/useSlug.ts | 10 +- .../Common/DiagnosisSelectFormField.tsx | 45 ---- src/Components/Common/components/Menu.tsx | 18 +- .../Common/components/SelectMenu.tsx | 125 ----------- .../ConditionVerificationStatusMenu.tsx | 121 +++++++++++ .../AddICD11Diagnosis.tsx | 75 +++++++ .../ConsultationDiagnosisBuilder.tsx | 193 +++++++++++++++++ .../ConsultationDiagnosisEntry.tsx | 120 +++++++++++ .../PrincipalDiagnosisSelect.tsx | 67 ++++++ .../Diagnosis/LegacyDiagnosesList.tsx | 86 ++++++++ src/Components/Diagnosis/routes.ts | 37 ++++ src/Components/Diagnosis/types.ts | 46 ++++ .../Facility/ConsultationDetails/index.tsx | 121 +++++------ src/Components/Facility/ConsultationForm.tsx | 202 ++++++++---------- src/Components/Facility/TreatmentSummary.tsx | 7 - src/Components/Facility/models.tsx | 14 +- src/Components/Form/SelectMenuV2.tsx | 2 +- src/Components/Shifting/ShiftDetails.tsx | 9 +- src/Locale/en/Common.json | 8 +- src/Locale/en/Diagnosis.json | 21 ++ src/Locale/en/index.js | 2 + src/Redux/actions.tsx | 4 +- 23 files changed, 946 insertions(+), 402 deletions(-) delete mode 100644 src/Components/Common/DiagnosisSelectFormField.tsx delete mode 100644 src/Components/Common/components/SelectMenu.tsx create mode 100644 src/Components/Diagnosis/ConditionVerificationStatusMenu.tsx create mode 100644 src/Components/Diagnosis/ConsultationDiagnosisBuilder/AddICD11Diagnosis.tsx create mode 100644 src/Components/Diagnosis/ConsultationDiagnosisBuilder/ConsultationDiagnosisBuilder.tsx create mode 100644 src/Components/Diagnosis/ConsultationDiagnosisBuilder/ConsultationDiagnosisEntry.tsx create mode 100644 src/Components/Diagnosis/ConsultationDiagnosisBuilder/PrincipalDiagnosisSelect.tsx create mode 100644 src/Components/Diagnosis/LegacyDiagnosesList.tsx create mode 100644 src/Components/Diagnosis/routes.ts create mode 100644 src/Components/Diagnosis/types.ts create mode 100644 src/Locale/en/Diagnosis.json diff --git a/cypress/pageobject/Patient/PatientConsultation.ts b/cypress/pageobject/Patient/PatientConsultation.ts index bf301641898..ce1fdc1e393 100644 --- a/cypress/pageobject/Patient/PatientConsultation.ts +++ b/cypress/pageobject/Patient/PatientConsultation.ts @@ -44,23 +44,20 @@ export class PatientConsultationPage { cy.get("#height").click().type(weight); cy.get("#patient_no").type(ipNumber); cy.intercept("GET", "**/icd/**").as("getIcdResults"); - cy.get( - "#icd11_diagnoses_object input[placeholder='Select'][role='combobox']" - ) + cy.get("#icd11-search input[role='combobox']") .scrollIntoView() .click() .type("1A"); - cy.get("#icd11_diagnoses_object [role='option']") + cy.get("#icd11-search [role='option']") .contains("1A00 Cholera") .scrollIntoView() .click(); - cy.get("label[for='icd11_diagnoses_object']").click(); + cy.get("#condition-verification-status-menu").click(); + cy.get("#add-icd11-diagnosis-as-confirmed").click(); cy.wait("@getIcdResults").its("response.statusCode").should("eq", 200); - cy.get("#icd11_principal_diagnosis [role='combobox']").click().type("1A"); - cy.get("#icd11_principal_diagnosis [role='option']") - .contains("1A00 Cholera") - .click(); + cy.get("#principal-diagnosis-select").click(); + cy.get("#principal-diagnosis-select [role='option']").first().click(); cy.get("#consultation_notes").click().type(consulationNotes); cy.get("#verified_by") diff --git a/src/Common/hooks/useSlug.ts b/src/Common/hooks/useSlug.ts index 69d8f591c84..8885a074678 100644 --- a/src/Common/hooks/useSlug.ts +++ b/src/Common/hooks/useSlug.ts @@ -8,9 +8,9 @@ import { usePath } from "raviger"; * // Current path: /consultation/94b9a * const consultation = useSlug("consultation"); // consultation = "94b9a" */ -export default function useSlug(prefix: string) { +export default function useSlug(prefix: string, fallback?: string) { const path = usePath() ?? ""; - return findSlug(path.split("/"), prefix); + return findSlug(path.split("/"), prefix, fallback); } /** @@ -28,7 +28,7 @@ export const useSlugs = (...prefix: string[]) => { return prefix.map((p) => findSlug(path.split("/"), p)); }; -const findSlug = (segments: string[], prefix: string) => { +const findSlug = (segments: string[], prefix: string, fallback?: string) => { const index = segments.findIndex((segment) => segment === prefix); if (index === -1) { throw new Error( @@ -36,8 +36,8 @@ const findSlug = (segments: string[], prefix: string) => { ); } - const slug = segments[index + 1]; - if (!slug) { + const slug = segments[index + 1] ?? fallback; + if (slug === undefined) { throw new Error(`Slug not found in path "${segments.join("/")}"`); } diff --git a/src/Components/Common/DiagnosisSelectFormField.tsx b/src/Components/Common/DiagnosisSelectFormField.tsx deleted file mode 100644 index 4086664d4ba..00000000000 --- a/src/Components/Common/DiagnosisSelectFormField.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { useAsyncOptions } from "../../Common/hooks/useAsyncOptions"; -import { listICD11Diagnosis } from "../../Redux/actions"; -import { ICD11DiagnosisModel } from "../Facility/models"; -import { AutocompleteMutliSelect } from "../Form/FormFields/AutocompleteMultiselect"; -import FormField from "../Form/FormFields/FormField"; -import { - FormFieldBaseProps, - useFormFieldPropsResolver, -} from "../Form/FormFields/Utils"; - -type Props = - // | ({ multiple?: false | undefined } & FormFieldBaseProps) // uncomment when single select form field is required and implemented. - { multiple: true } & FormFieldBaseProps; - -export function DiagnosisSelectFormField(props: Props) { - const field = useFormFieldPropsResolver(props); - const { fetchOptions, isLoading, options } = - useAsyncOptions("id"); - - if (!props.multiple) { - return ( -
- Component not implemented -
- ); - } - - return ( - - option.label} - optionValue={(option) => option} - onQuery={(query) => - fetchOptions(listICD11Diagnosis({ query }, field.id || "")) - } - isLoading={isLoading} - /> - - ); -} diff --git a/src/Components/Common/components/Menu.tsx b/src/Components/Common/components/Menu.tsx index 49ee02b393e..3292031eebb 100644 --- a/src/Components/Common/components/Menu.tsx +++ b/src/Components/Common/components/Menu.tsx @@ -1,6 +1,6 @@ import { Anyone, AuthorizedElementProps } from "../../../Utils/AuthorizeFor"; -import { ButtonVariant } from "./ButtonV2"; +import { ButtonSize, ButtonVariant } from "./ButtonV2"; import CareIcon from "../../../CAREUI/icons/CareIcon"; import { DropdownTransition } from "./HelperComponents"; import { Menu } from "@headlessui/react"; @@ -12,6 +12,7 @@ interface DropdownMenuProps { id?: string; title: string; variant?: ButtonVariant; + size?: ButtonSize; icon?: JSX.Element | undefined; children: JSX.Element | JSX.Element[]; disabled?: boolean | undefined; @@ -21,6 +22,7 @@ interface DropdownMenuProps { export default function DropdownMenu({ variant = "primary", + size = "default", ...props }: DropdownMenuProps) { return ( @@ -28,13 +30,21 @@ export default function DropdownMenu({ -
+
{props.icon} {props.title || "Dropdown"}
- + = { - options: { - title: string; - description?: string; - value: T; - }[]; - onSelect: (value: T) => void; - selected?: T; - label?: string; - position?: string; - parentRelative?: boolean; -}; - -/** Deprecated. Use SelectMenuV2. */ -export default function SelectMenu(props: Props) { - const options = props.options.map((option) => { - return { - ...option, - current: option.value === props.selected, - }; - }); - - const selected = options.find((option) => option.current) || options[0]; - - return ( - { - props.onSelect(selection.value); - }} - > - {({ open }) => ( - <> - {props.label} -
- -
-
- {selected.value && ( - - )} -

{selected.title}

-
-
- -
-
-
- - - - {options.map((option) => ( - - `relative cursor-default select-none p-4 text-sm transition-all duration-100 ease-in-out ${ - active ? "bg-primary-500 text-white" : "text-gray-900" - }` - } - value={option} - > - {({ selected, active }) => ( -
-
-

- {option.title} -

- {selected ? ( - - - - ) : null} -
- {option.description && ( -

- {option.description} -

- )} -
- )} -
- ))} -
-
-
- - )} -
- ); -} diff --git a/src/Components/Diagnosis/ConditionVerificationStatusMenu.tsx b/src/Components/Diagnosis/ConditionVerificationStatusMenu.tsx new file mode 100644 index 00000000000..9f80d47f232 --- /dev/null +++ b/src/Components/Diagnosis/ConditionVerificationStatusMenu.tsx @@ -0,0 +1,121 @@ +import { useTranslation } from "react-i18next"; +import CareIcon from "../../CAREUI/icons/CareIcon"; +import DropdownMenu, { DropdownItem } from "../Common/components/Menu"; +import { + ConditionVerificationStatus, + InactiveConditionVerificationStatuses, +} from "./types"; +import { classNames } from "../../Utils/utils"; +import { ButtonSize } from "../Common/components/ButtonV2"; + +interface Props { + disabled?: boolean; + value?: T; + placeholder?: string; + options: readonly T[]; + onSelect: (option: T) => void; + onRemove?: () => void; + className?: string; + size?: ButtonSize; +} + +export default function ConditionVerificationStatusMenu< + T extends ConditionVerificationStatus +>(props: Props) { + const { t } = useTranslation(); + + return ( + + <> + {props.options.map((status) => ( + props.onSelect(status)} + icon={ + + } + className="group" + disabled={props.value === status} + > +
+ + {InactiveConditionVerificationStatuses.includes( + status as (typeof InactiveConditionVerificationStatuses)[number] + ) + ? "Remove as " + : ""} + {t(status)} + + + {t(`help_${status}`)} + +
+
+ ))} + + {props.value && props.onRemove && ( + props.onRemove?.()} + icon={} + > + {t("remove")} + + )} + +
+ ); +} + +export const StatusStyle = { + unconfirmed: { + variant: "warning", + // icon: "l-question", + colors: "text-yellow-500 border-yellow-500", + }, + provisional: { + variant: "warning", + // icon: "l-question", + colors: "text-secondary-800 border-secondary-800", + }, + differential: { + variant: "warning", + // icon: "l-question", + colors: "text-secondary-800 border-secondary-800", + }, + confirmed: { + variant: "primary", + // icon: "l-check", + colors: "text-primary-500 border-primary-500", + }, + refuted: { + variant: "danger", + icon: "l-times", + colors: "text-danger-500 border-danger-500", + }, + "entered-in-error": { + variant: "danger", + // icon: "l-ban", + colors: "text-danger-500 border-danger-500", + }, +} as const; diff --git a/src/Components/Diagnosis/ConsultationDiagnosisBuilder/AddICD11Diagnosis.tsx b/src/Components/Diagnosis/ConsultationDiagnosisBuilder/AddICD11Diagnosis.tsx new file mode 100644 index 00000000000..4cf55a82b13 --- /dev/null +++ b/src/Components/Diagnosis/ConsultationDiagnosisBuilder/AddICD11Diagnosis.tsx @@ -0,0 +1,75 @@ +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import AutocompleteFormField from "../../Form/FormFields/Autocomplete"; +import { + ActiveConditionVerificationStatuses, + CreateDiagnosis, + ICD11DiagnosisModel, +} from "../types"; +import { useAsyncOptions } from "../../../Common/hooks/useAsyncOptions"; +import { listICD11Diagnosis } from "../../../Redux/actions"; +import ConditionVerificationStatusMenu from "../ConditionVerificationStatusMenu"; +import { classNames } from "../../../Utils/utils"; + +interface AddICD11DiagnosisProps { + className?: string; + onAdd: (object: CreateDiagnosis) => Promise; + disallowed: ICD11DiagnosisModel[]; + disabled?: boolean; +} + +export default function AddICD11Diagnosis(props: AddICD11DiagnosisProps) { + const { t } = useTranslation(); + const [selected, setSelected] = useState(); + const [adding, setAdding] = useState(false); + const hasError = !!props.disallowed.find((d) => d.id === selected?.id); + + const { fetchOptions, isLoading, options } = + useAsyncOptions("id"); + + const handleAdd = async (status: CreateDiagnosis["verification_status"]) => { + if (!selected) return; + + setAdding(true); + const added = await props.onAdd({ + diagnosis_object: selected, + diagnosis: selected.id, + verification_status: status, + is_principal: false, + }); + setAdding(false); + + if (added) { + setSelected(undefined); + } + }; + + return ( +
+ setSelected(e.value)} + options={options(selected ? [selected] : undefined)} + optionLabel={(option) => option.label} + optionValue={(option) => option} + onQuery={(query) => fetchOptions(listICD11Diagnosis({ query }))} + isLoading={isLoading} + error={hasError ? t("diagnosis_already_added") : undefined} + /> + handleAdd(status)} + size="default" + /> +
+ ); +} diff --git a/src/Components/Diagnosis/ConsultationDiagnosisBuilder/ConsultationDiagnosisBuilder.tsx b/src/Components/Diagnosis/ConsultationDiagnosisBuilder/ConsultationDiagnosisBuilder.tsx new file mode 100644 index 00000000000..04a3eb24e18 --- /dev/null +++ b/src/Components/Diagnosis/ConsultationDiagnosisBuilder/ConsultationDiagnosisBuilder.tsx @@ -0,0 +1,193 @@ +import { useState } from "react"; +import useSlug from "../../../Common/hooks/useSlug"; +import { + ConsultationDiagnosis, + CreateDiagnosis, + ICD11DiagnosisModel, +} from "../types"; +import AddICD11Diagnosis from "./AddICD11Diagnosis"; +import ConsultationDiagnosisEntry from "./ConsultationDiagnosisEntry"; +import request from "../../../Utils/request/request"; +import DiagnosesRoutes from "../routes"; +import * as Notification from "../../../Utils/Notifications"; +import PrincipalDiagnosisSelect from "./PrincipalDiagnosisSelect"; + +interface CreateDiagnosesProps { + className?: string; + value: CreateDiagnosis[]; + onChange: (diagnoses: CreateDiagnosis[]) => void; +} + +export const CreateDiagnosesBuilder = (props: CreateDiagnosesProps) => { + return ( +
+
+
+ {props.value.map((diagnosis, index) => ( + { + if (action.type === "remove") { + props.onChange(props.value.toSpliced(index, 1)); + } + + if (action.type === "edit") { + const diagnoses = [...props.value]; + diagnoses[index] = action.value as CreateDiagnosis; + props.onChange(diagnoses); + } + }} + /> + ))} +
+ + {props.value.length === 0 && } + +
+ obj.diagnosis_object as ICD11DiagnosisModel + )} + onAdd={async (diagnosis) => { + props.onChange([...props.value, diagnosis]); + return true; + }} + /> +
+
+ + { + props.onChange( + props.value.map((d) => ({ + ...d, + is_principal: + d.diagnosis_object?.id === value?.diagnosis_object?.id, + })) + ); + }} + /> +
+ ); +}; + +interface EditDiagnosesProps { + className?: string; + value: ConsultationDiagnosis[]; +} + +export const EditDiagnosesBuilder = (props: EditDiagnosesProps) => { + const consultation = useSlug("consultation"); + const [diagnoses, setDiagnoses] = useState(props.value); + return ( +
+
+
+ {diagnoses.map((diagnosis, index) => ( + { + setDiagnoses( + diagnoses.map((diagnose, i) => + i === index + ? (action.value as ConsultationDiagnosis) + : diagnose + ) + ); + }} + /> + ))} +
+ + {diagnoses.length === 0 && } + +
+ obj.diagnosis_object as ICD11DiagnosisModel + )} + onAdd={async (diagnosis) => { + const { res, data, error } = await request( + DiagnosesRoutes.createConsultationDiagnosis, + { + pathParams: { consultation }, + body: diagnosis, + } + ); + + if (res?.ok && data) { + setDiagnoses([...diagnoses, data]); + return true; + } + + if (error) { + Notification.Error({ msg: error }); + } + + return false; + }} + /> +
+
+ + { + // Unset existing principal diagnoses + await Promise.all( + diagnoses + .filter((d) => d.is_principal) + .map((d) => { + return request(DiagnosesRoutes.updateConsultationDiagnosis, { + pathParams: { consultation, id: d.id }, + body: { ...d, is_principal: false }, + }); + }) + ); + + if (!value) { + setDiagnoses((diagnoses) => + diagnoses.map((d) => ({ ...d, is_principal: false })) + ); + return; + } + + // Set new principal diagnosis + const { res, data, error } = await request( + DiagnosesRoutes.updateConsultationDiagnosis, + { + pathParams: { consultation, id: value.id }, + body: { ...value, is_principal: true }, + } + ); + + if (res?.ok && data) { + setDiagnoses((diagnoses) => + diagnoses.map((d) => + d.id === data.id ? data : { ...d, is_principal: false } + ) + ); + } + + if (error) { + Notification.Error({ msg: error }); + } + }} + /> +
+ ); +}; + +const NoDiagnosisAdded = () => { + return ( +
+ Atleast one diagnosis must be added +
+ ); +}; diff --git a/src/Components/Diagnosis/ConsultationDiagnosisBuilder/ConsultationDiagnosisEntry.tsx b/src/Components/Diagnosis/ConsultationDiagnosisBuilder/ConsultationDiagnosisEntry.tsx new file mode 100644 index 00000000000..a650358c518 --- /dev/null +++ b/src/Components/Diagnosis/ConsultationDiagnosisBuilder/ConsultationDiagnosisEntry.tsx @@ -0,0 +1,120 @@ +import ConditionVerificationStatusMenu from "../ConditionVerificationStatusMenu"; +import { + ActiveConditionVerificationStatuses, + ConditionVerificationStatuses, + ConsultationDiagnosis, + CreateDiagnosis, +} from "../types"; +import DiagnosesRoutes from "../routes"; +import { useState } from "react"; +import request from "../../../Utils/request/request"; +import { classNames } from "../../../Utils/utils"; + +interface RemoveAction { + type: "remove"; +} + +interface EditAction { + type: "edit"; + value: CreateDiagnosis | ConsultationDiagnosis; +} + +interface BaseProps { + className?: string; +} + +interface ConsultationCreateProps extends BaseProps { + consultationId?: undefined; + value: CreateDiagnosis; + onChange: (action: EditAction | RemoveAction) => void; +} + +interface ConsultationEditProps extends BaseProps { + consultationId: string; + value: ConsultationDiagnosis; + onChange: (action: EditAction) => void; +} + +type Props = ConsultationCreateProps | ConsultationEditProps; + +export default function ConsultationDiagnosisEntry(props: Props) { + const [disabled, setDisabled] = useState(false); + + const handleUpdate = async (value: ConsultationDiagnosis) => { + setDisabled(true); + const { res, data } = await request( + DiagnosesRoutes.updateConsultationDiagnosis, + { + pathParams: { + consultation: props.consultationId as string, + id: value.id, + }, + body: value, + } + ); + setDisabled(false); + + if (res?.ok && data) { + props.onChange({ type: "edit", value: data }); + } + }; + + const object = props.value; + const isActive = ActiveConditionVerificationStatuses.includes( + object.verification_status as (typeof ActiveConditionVerificationStatuses)[number] + ); + + return ( +
+
+ + {object.diagnosis_object?.label} + +
+
+ { + const value = { ...object, verification_status }; + if (props.consultationId) { + await handleUpdate(value as ConsultationDiagnosis); + } else { + props.onChange({ + type: "edit", + value: value as CreateDiagnosis | ConsultationDiagnosis, + }); + } + }} + onRemove={ + props.consultationId === undefined + ? () => props.onChange({ type: "remove" }) + : undefined + } + /> +
+
+
+
+ ); +} diff --git a/src/Components/Diagnosis/ConsultationDiagnosisBuilder/PrincipalDiagnosisSelect.tsx b/src/Components/Diagnosis/ConsultationDiagnosisBuilder/PrincipalDiagnosisSelect.tsx new file mode 100644 index 00000000000..8018c6480ed --- /dev/null +++ b/src/Components/Diagnosis/ConsultationDiagnosisBuilder/PrincipalDiagnosisSelect.tsx @@ -0,0 +1,67 @@ +import { useState } from "react"; +import { + ActiveConditionVerificationStatuses, + ConsultationDiagnosis, + CreateDiagnosis, +} from "../types"; +import { SelectFormField } from "../../Form/FormFields/SelectFormField"; + +type Option = CreateDiagnosis | ConsultationDiagnosis; + +interface Props { + className?: string; + diagnoses: T[]; + onChange: (value?: T) => Promise; +} + +const PrincipalDiagnosisSelect = (props: Props) => { + const [disabled, setDisabled] = useState(false); + const value = props.diagnoses.find((d) => d.is_principal); + const diagnosis = value?.diagnosis_object; + + const options = props.diagnoses.some(isConfirmed) + ? props.diagnoses.filter(isConfirmedOrPrincipal) + : props.diagnoses.filter(isActive); + + return ( +
+
+ d.diagnosis_object?.label} + optionDescription={(d) => ( +

+ Categorised under:{" "} + {d.diagnosis_object?.chapter} +

+ )} + optionValue={(d) => JSON.stringify(d)} // TODO: momentary hack, figure out a better way to do this + onChange={async ({ value }) => { + setDisabled(true); + await props.onChange(value ? (JSON.parse(value) as T) : undefined); + setDisabled(false); + }} + errorClassName="hidden" + /> + {diagnosis && ( + +

This encounter will be categorised under:

+

{diagnosis.chapter}

+
+ )} +
+
+ ); +}; + +export default PrincipalDiagnosisSelect; + +const isConfirmed = (d: Option) => d.verification_status === "confirmed"; +const isConfirmedOrPrincipal = (d: Option) => isConfirmed(d) || d.is_principal; +const isActive = (d: Option) => + ActiveConditionVerificationStatuses.includes(d.verification_status as any); diff --git a/src/Components/Diagnosis/LegacyDiagnosesList.tsx b/src/Components/Diagnosis/LegacyDiagnosesList.tsx new file mode 100644 index 00000000000..2a2ed7b9c3a --- /dev/null +++ b/src/Components/Diagnosis/LegacyDiagnosesList.tsx @@ -0,0 +1,86 @@ +import { useState } from "react"; +import { + ActiveConditionVerificationStatuses, + ConditionVerificationStatus, + ConsultationDiagnosis, +} from "./types"; +import { useTranslation } from "react-i18next"; +import CareIcon from "../../CAREUI/icons/CareIcon"; + +interface Props { + diagnoses: ConsultationDiagnosis[]; +} + +type GroupedDiagnoses = Record< + ConditionVerificationStatus, + ConsultationDiagnosis[] +>; + +function groupDiagnoses(diagnoses: ConsultationDiagnosis[]) { + const groupedDiagnoses = {} as GroupedDiagnoses; + + for (const status of ActiveConditionVerificationStatuses) { + groupedDiagnoses[status] = diagnoses + .filter((d) => d.verification_status === status) + .sort((a, b) => Number(b.is_principal) - Number(a.is_principal)); + } + + return groupedDiagnoses; +} + +export default function LegacyDiagnosesList(props: Props) { + const diagnoses = groupDiagnoses(props.diagnoses); + + return ( +
+ {Object.entries(diagnoses).map( + ([status, diagnoses]) => + !!diagnoses.length && ( + + ) + )} +
+ ); +} + +const DefaultShowLimit = 3; + +const DiagnosesOfStatus = ({ diagnoses }: Props) => { + const { t } = useTranslation(); + const [showMore, setShowMore] = useState(false); + + const queryset = showMore ? diagnoses : diagnoses.slice(0, DefaultShowLimit); + + return ( +
+

+ {t(queryset[0].verification_status)} {t("diagnoses")}{" "} + ({t("icd11_as_recommended")}) +

+
    + {queryset.map((diagnosis) => ( +
  • + {diagnosis.diagnosis_object.label} + {diagnosis.is_principal && ( + + + {t("principal")} + + )} +
  • + ))} +
+ + {diagnoses.length > DefaultShowLimit && ( + setShowMore(!showMore)} + className="cursor-pointer text-sm text-blue-600 hover:text-blue-300" + > + {showMore + ? t("hide") + : `... and ${diagnoses.length - queryset.length} more.`} + + )} +
+ ); +}; diff --git a/src/Components/Diagnosis/routes.ts b/src/Components/Diagnosis/routes.ts new file mode 100644 index 00000000000..159999fac5f --- /dev/null +++ b/src/Components/Diagnosis/routes.ts @@ -0,0 +1,37 @@ +import { Type } from "../../Redux/api"; +import { PaginatedResponse } from "../../Utils/request/types"; +import { ConsultationDiagnosis, CreateDiagnosis } from "./types"; + +const DiagnosesRoutes = { + // ICD-11 + searchICD11Diagnoses: { + path: "/api/v1/icd/", + }, + + // Consultation Diagnoses + listConsultationDiagnoses: { + path: "/api/v1/consultation/{consultation}/diagnoses/", + TRes: Type>(), + }, + + createConsultationDiagnosis: { + path: "/api/v1/consultation/{consultation}/diagnoses/", + method: "POST", + TBody: Type(), + TRes: Type(), + }, + + getConsultationDiagnosis: { + path: "/api/v1/consultation/{consultation}/diagnoses/{id}/", + TRes: Type(), + }, + + updateConsultationDiagnosis: { + path: "/api/v1/consultation/{consultation}/diagnoses/{id}/", + method: "PATCH", + TBody: Type>(), + TRes: Type(), + }, +} as const; + +export default DiagnosesRoutes; diff --git a/src/Components/Diagnosis/types.ts b/src/Components/Diagnosis/types.ts new file mode 100644 index 00000000000..006bb2feb6c --- /dev/null +++ b/src/Components/Diagnosis/types.ts @@ -0,0 +1,46 @@ +import { PerformedByModel } from "../HCX/misc"; + +export type ICD11DiagnosisModel = { + id: string; + label: string; + chapter: string; +}; + +export const ActiveConditionVerificationStatuses = [ + "unconfirmed", + "provisional", + "differential", + "confirmed", +] as const; + +export const InactiveConditionVerificationStatuses = [ + "refuted", + "entered-in-error", +] as const; + +export const ConditionVerificationStatuses = [ + ...ActiveConditionVerificationStatuses, + ...InactiveConditionVerificationStatuses, +] as const; + +export type ConditionVerificationStatus = + (typeof ConditionVerificationStatuses)[number]; + +export interface ConsultationDiagnosis { + readonly id: string; + diagnosis?: ICD11DiagnosisModel["id"]; + readonly diagnosis_object: ICD11DiagnosisModel; + verification_status: ConditionVerificationStatus; + is_principal: boolean; + readonly is_migrated: boolean; + readonly created_by: PerformedByModel; + readonly created_date: string; + readonly modified_date: string; +} + +export interface CreateDiagnosis { + diagnosis: ICD11DiagnosisModel["id"]; + readonly diagnosis_object?: ICD11DiagnosisModel; + verification_status: (typeof ActiveConditionVerificationStatuses)[number]; + is_principal: boolean; +} diff --git a/src/Components/Facility/ConsultationDetails/index.tsx b/src/Components/Facility/ConsultationDetails/index.tsx index f2ef13fa833..6a5a5da242c 100644 --- a/src/Components/Facility/ConsultationDetails/index.tsx +++ b/src/Components/Facility/ConsultationDetails/index.tsx @@ -4,7 +4,7 @@ import { OptionsType, SYMPTOM_CHOICES, } from "../../../Common/constants"; -import { ConsultationModel, ICD11DiagnosisModel } from "../models"; +import { ConsultationModel } from "../models"; import { getConsultation, getPatient, @@ -13,7 +13,6 @@ import { } from "../../../Redux/actions"; import { statusType, useAbortableEffect } from "../../../Common/utils"; import { lazy, useCallback, useState } from "react"; -import ToolTip from "../../Common/utils/Tooltip"; import ButtonV2 from "../../Common/components/ButtonV2"; import CareIcon from "../../../CAREUI/icons/CareIcon"; import DischargeModal from "../DischargeModal"; @@ -43,6 +42,7 @@ import { ConsultationPressureSoreTab } from "./ConsultationPressureSoreTab"; import { ConsultationDialysisTab } from "./ConsultationDialysisTab"; import { ConsultationNeurologicalMonitoringTab } from "./ConsultationNeurologicalMonitoringTab"; import { ConsultationNutritionTab } from "./ConsultationNutritionTab"; +import LegacyDiagnosesList from "../../Diagnosis/LegacyDiagnosesList"; const Loading = lazy(() => import("../../Common/Loading")); const PageTitle = lazy(() => import("../../Common/PageTitle")); @@ -218,56 +218,56 @@ export const ConsultationDetails = (props: any) => { selected === true ? "border-primary-500 text-primary-600 border-b-2" : "" }`; - const ShowDiagnosis = ({ - diagnoses = [], - label = "Diagnosis", - nshow = 2, - }: { - diagnoses: ICD11DiagnosisModel[] | undefined; - label: string; - nshow?: number; - }) => { - const [showMore, setShowMore] = useState(false); - - return diagnoses.length ? ( -
-

{label}

- {diagnoses.slice(0, !showMore ? nshow : undefined).map((diagnosis) => - diagnosis.id === consultationData.icd11_principal_diagnosis ? ( -
-

{diagnosis.label}

-
- - - -
-
- ) : ( -

{diagnosis.label}

- ) - )} - {diagnoses.length > nshow && ( - <> - {!showMore ? ( - setShowMore(true)} - className="cursor-pointer text-sm text-blue-600 hover:text-blue-300" - > - show more - - ) : ( - setShowMore(false)} - className="cursor-pointer text-sm text-blue-600 hover:text-blue-300" - > - show less - - )} - - )} -
- ) : null; - }; + // const ShowDiagnosis = ({ + // diagnoses = [], + // label = "Diagnosis", + // nshow = 2, + // }: { + // diagnoses: ICD11DiagnosisModel[] | undefined; + // label: string; + // nshow?: number; + // }) => { + // const [showMore, setShowMore] = useState(false); + + // return diagnoses.length ? ( + //
+ //

{label}

+ // {diagnoses.slice(0, !showMore ? nshow : undefined).map((diagnosis) => + // diagnosis.id === consultationData.icd11_principal_diagnosis ? ( + //
+ //

{diagnosis.label}

+ //
+ // + // + // + //
+ //
+ // ) : ( + //

{diagnosis.label}

+ // ) + // )} + // {diagnoses.length > nshow && ( + // <> + // {!showMore ? ( + // setShowMore(true)} + // className="cursor-pointer text-sm text-blue-600 hover:text-blue-300" + // > + // show more + // + // ) : ( + // setShowMore(false)} + // className="cursor-pointer text-sm text-blue-600 hover:text-blue-300" + // > + // show less + // + // )} + // + // )} + //
+ // ) : null; + // }; return (
@@ -430,21 +430,8 @@ export const ConsultationDetails = (props: any) => {
)*/} - - - {(consultationData.verified_by_object || diff --git a/src/Components/Facility/ConsultationForm.tsx b/src/Components/Facility/ConsultationForm.tsx index 01a5f02b7b1..10397f4e79a 100644 --- a/src/Components/Facility/ConsultationForm.tsx +++ b/src/Components/Facility/ConsultationForm.tsx @@ -1,6 +1,6 @@ import * as Notification from "../../Utils/Notifications.js"; -import { BedModel, FacilityModel, ICD11DiagnosisModel } from "./models"; +import { BedModel, FacilityModel } from "./models"; import { CONSULTATION_STATUS, CONSULTATION_SUGGESTION, @@ -38,7 +38,6 @@ import Beds from "./Consultations/Beds"; import CareIcon from "../../CAREUI/icons/CareIcon"; import CheckBoxFormField from "../Form/FormFields/CheckBoxFormField"; import DateFormField from "../Form/FormFields/DateFormField"; -import { DiagnosisSelectFormField } from "../Common/DiagnosisSelectFormField"; import { FacilitySelect } from "../Common/FacilitySelect"; import { FieldChangeEvent, @@ -60,7 +59,15 @@ import useConfig from "../../Common/hooks/useConfig"; import { useDispatch } from "react-redux"; import useVisibility from "../../Utils/useVisibility"; import dayjs from "../../Utils/dayjs"; -import AutocompleteFormField from "../Form/FormFields/Autocomplete.js"; +import { + ConditionVerificationStatuses, + ConsultationDiagnosis, + CreateDiagnosis, +} from "../Diagnosis/types.js"; +import { + CreateDiagnosesBuilder, + EditDiagnosesBuilder, +} from "../Diagnosis/ConsultationDiagnosisBuilder/ConsultationDiagnosisBuilder.js"; const Loading = lazy(() => import("../Common/Loading")); const PageTitle = lazy(() => import("../Common/PageTitle")); @@ -82,9 +89,8 @@ type FormDetails = { discharge_date: null; referred_to?: string; referred_to_external?: string; - icd11_diagnoses_object: ICD11DiagnosisModel[]; - icd11_provisional_diagnoses_object: ICD11DiagnosisModel[]; - icd11_principal_diagnosis?: ICD11DiagnosisModel["id"]; + create_diagnoses: CreateDiagnosis[]; + diagnoses: ConsultationDiagnosis[]; verified_by: string; verified_by_object: UserModel | null; is_kasp: BooleanStrings; @@ -128,9 +134,8 @@ const initForm: FormDetails = { discharge_date: null, referred_to: "", referred_to_external: "", - icd11_diagnoses_object: [], - icd11_provisional_diagnoses_object: [], - icd11_principal_diagnosis: undefined, + create_diagnoses: [], + diagnoses: [], verified_by: "", verified_by_object: null, is_kasp: "false", @@ -324,18 +329,6 @@ export const ConsultationForm = (props: any) => { verified_by: "Declared Dead", }, }); - } else if ( - event.name === "icd11_diagnoses_object" || - event.name === "icd11_provisional_diagnoses_object" - ) { - dispatch({ - type: "set_form", - form: { - ...state.form, - [event.name]: event.value, - icd11_principal_diagnosis: undefined, - }, - }); } else { dispatch({ type: "set_form", @@ -393,6 +386,11 @@ export const ConsultationForm = (props: any) => { death_datetime: res.data?.death_datetime || "", death_confirmed_doctor: res.data?.death_confirmed_doctor || "", InvestigationAdvice: res.data.investigation, + diagnoses: res.data.diagnoses.sort( + (a: ConsultationDiagnosis, b: ConsultationDiagnosis) => + ConditionVerificationStatuses.indexOf(a.verification_status) - + ConditionVerificationStatuses.indexOf(b.verification_status) + ), }; dispatch({ type: "set_form", form: { ...state.form, ...formData } }); setBed(formData.bed); @@ -588,57 +586,57 @@ export const ConsultationForm = (props: any) => { return; } - case "icd11_provisional_diagnoses_object": { - if ( - state.form[field].length === 0 && - state.form["icd11_diagnoses_object"].length === 0 - ) { - for (const err_field of [field, "icd11_diagnoses_object"]) - errors[err_field] = - "Please select either Provisional Diagnosis or Final Diagnosis"; - invalidForm = true; - break; - } - return; - } - - case "icd11_principal_diagnosis": { - if (!state.form[field]) { - errors[field] = "Please select Principal Diagnosis"; - invalidForm = true; - break; - } - - if ( - state.form[field] && - state.form["icd11_diagnoses_object"].length && - !state.form["icd11_provisional_diagnoses_object"] && - !state.form["icd11_diagnoses_object"] - .map((d) => d.id) - .includes(state.form[field]!) - ) { - errors[field] = - "Please select Principal Diagnosis from Final Diagnosis"; - invalidForm = true; - break; - } - - if ( - state.form[field] && - state.form["icd11_provisional_diagnoses_object"].length && - !state.form["icd11_diagnoses_object"] && - !state.form["icd11_provisional_diagnoses_object"] - .map((d) => d.id) - .includes(state.form[field]!) - ) { - errors[field] = - "Please select Principal Diagnosis from Provisional Diagnosis"; - invalidForm = true; - break; - } - - return; - } + // case "icd11_provisional_diagnoses_object": { + // if ( + // state.form[field].length === 0 && + // state.form["icd11_diagnoses_object"].length === 0 + // ) { + // for (const err_field of [field, "icd11_diagnoses_object"]) + // errors[err_field] = + // "Please select either Provisional Diagnosis or Final Diagnosis"; + // invalidForm = true; + // break; + // } + // return; + // } + + // case "icd11_principal_diagnosis": { + // if (!state.form[field]) { + // errors[field] = "Please select Principal Diagnosis"; + // invalidForm = true; + // break; + // } + + // if ( + // state.form[field] && + // state.form["icd11_diagnoses_object"].length && + // !state.form["icd11_provisional_diagnoses_object"] && + // !state.form["icd11_diagnoses_object"] + // .map((d) => d.id) + // .includes(state.form[field]!) + // ) { + // errors[field] = + // "Please select Principal Diagnosis from Final Diagnosis"; + // invalidForm = true; + // break; + // } + + // if ( + // state.form[field] && + // state.form["icd11_provisional_diagnoses_object"].length && + // !state.form["icd11_diagnoses_object"] && + // !state.form["icd11_provisional_diagnoses_object"] + // .map((d) => d.id) + // .includes(state.form[field]!) + // ) { + // errors[field] = + // "Please select Principal Diagnosis from Provisional Diagnosis"; + // invalidForm = true; + // break; + // } + + // return; + // } default: return; @@ -714,14 +712,7 @@ export const ConsultationForm = (props: any) => { treatment_plan: state.form.treatment_plan, discharge_date: state.form.discharge_date, patient_no: state.form.patient_no, - icd11_diagnoses: state.form.icd11_diagnoses_object.map( - (o: ICD11DiagnosisModel) => o.id - ), - icd11_provisional_diagnoses: - state.form.icd11_provisional_diagnoses_object.map( - (o: ICD11DiagnosisModel) => o.id - ), - icd11_principal_diagnosis: state.form.icd11_principal_diagnosis, + create_diagnoses: isUpdate ? undefined : state.form.create_diagnoses, verified_by: state.form.verified_by, investigation: state.form.InvestigationAdvice, procedure: state.form.procedures, @@ -1190,44 +1181,25 @@ export const ConsultationForm = (props: any) => {
{sectionTitle("Diagnosis", true)} -

- - Either Provisional or Final Diagnosis is mandatory - - | Diagnoses as per ICD-11 recommended by WHO +

+ Diagnoses as per ICD-11 recommended by WHO

-
- -
- -
- -
- -
- option.label} - optionValue={(option) => option.id} - required - /> +
+ {isUpdate ? ( + + ) : ( + { + handleFormFieldChange({ + name: "create_diagnoses", + value: diagnoses, + }); + }} + /> + )}
diff --git a/src/Components/Facility/TreatmentSummary.tsx b/src/Components/Facility/TreatmentSummary.tsx index 3fbd80ac8b2..674c161428d 100644 --- a/src/Components/Facility/TreatmentSummary.tsx +++ b/src/Components/Facility/TreatmentSummary.tsx @@ -236,13 +236,6 @@ const TreatmentSummary = (props: any) => { : " ---"}
-
- Diagnosis : - {consultationData.diagnosis - ? consultationData.diagnosis - : " ---"} -
-
Physical Examination info : {dailyRounds.physical_examination_info diff --git a/src/Components/Facility/models.tsx b/src/Components/Facility/models.tsx index 7bf6069a61a..bd0f4eafeda 100644 --- a/src/Components/Facility/models.tsx +++ b/src/Components/Facility/models.tsx @@ -3,6 +3,7 @@ import { ProcedureType } from "../Common/prescription-builder/ProcedureBuilder"; import { NormalPrescription, PRNPrescription } from "../Medicine/models"; import { AssetData } from "../Assets/AssetTypes"; import { UserBareMinimum } from "../Users/models"; +import { ConsultationDiagnosis, CreateDiagnosis } from "../Diagnosis/types"; export interface LocalBodyModel { name: string; @@ -113,10 +114,8 @@ export interface ConsultationModel { consultation_status?: number; is_kasp?: boolean; kasp_enabled_date?: string; - diagnosis?: string; - icd11_diagnoses_object?: ICD11DiagnosisModel[]; - icd11_provisional_diagnoses_object?: ICD11DiagnosisModel[]; - icd11_principal_diagnosis?: ICD11DiagnosisModel["id"]; + readonly diagnoses?: ConsultationDiagnosis[]; + create_diagnoses?: CreateDiagnosis[]; // Used for bulk creating diagnoses upon consultation creation deprecated_verified_by?: string; verified_by?: string; verified_by_object?: UserBareMinimum; @@ -222,10 +221,3 @@ export interface CurrentBed { end_date: string; meta: Record; } - -// Voluntarily made as `type` for it to achieve type-safety when used with -// `useAsyncOptions` -export type ICD11DiagnosisModel = { - id: string; - label: string; -}; diff --git a/src/Components/Form/SelectMenuV2.tsx b/src/Components/Form/SelectMenuV2.tsx index f26ce03f0c6..464a754cebe 100644 --- a/src/Components/Form/SelectMenuV2.tsx +++ b/src/Components/Form/SelectMenuV2.tsx @@ -9,7 +9,7 @@ type OptionCallback = (option: T) => R; type SelectMenuProps = { id?: string; - options: T[]; + options: readonly T[]; disabled?: boolean | undefined; value: V | undefined; placeholder?: ReactNode; diff --git a/src/Components/Shifting/ShiftDetails.tsx b/src/Components/Shifting/ShiftDetails.tsx index bfc73adab78..3d2a1f60b8b 100644 --- a/src/Components/Shifting/ShiftDetails.tsx +++ b/src/Components/Shifting/ShiftDetails.tsx @@ -436,20 +436,13 @@ export default function ShiftDetails(props: { id: string }) {
-
+ {/*
{t("diagnosis")}:{" "} {consultation.diagnosis || "-"}
-
- - {/*
-
- Comorbidities (if any): - {consultation.diagnosis || '-'} -
*/}
diff --git a/src/Locale/en/Common.json b/src/Locale/en/Common.json index 5f71583abd7..0fe1ba1a8bf 100644 --- a/src/Locale/en/Common.json +++ b/src/Locale/en/Common.json @@ -112,6 +112,7 @@ "select": "Select", "lsg": "Lsg", "delete": "Delete", + "remove": "Remove", "max_size_for_image_uploaded_should_be": "Max size for image uploaded should be", "allowed_formats_are": "Allowed formats are", "recommended_aspect_ratio_for": "Recommended aspect ratio for", @@ -127,10 +128,12 @@ "submitting": "Submitting", "view_details": "View Details", "type_to_search": "Type to search", - "show_all": "Show All", + "show_all": "Show all", + "hide": "Hide", "select_skills": "Select and add some skills", "contact_your_admin_to_add_skills": "Contact your admin to add skills", "add": "Add", + "add_as": "Add as", "sort_by": "Sort By", "none": "None", "RESPIRATORY_SUPPORT_UNKNOWN": "None", @@ -153,5 +156,4 @@ "no_data_found": "No data found", "edit": "Edit", "clear_selection": "Clear selection" -} - +} \ No newline at end of file diff --git a/src/Locale/en/Diagnosis.json b/src/Locale/en/Diagnosis.json new file mode 100644 index 00000000000..6cb301c058e --- /dev/null +++ b/src/Locale/en/Diagnosis.json @@ -0,0 +1,21 @@ +{ + "diagnosis": "Diagnosis", + "diagnoses": "Diagnoses", + "diagnosis_already_added": "This diagnosis was already added", + "principal": "Principal", + "principal_diagnosis": "Principal diagnosis", + "unconfirmed": "Unconfirmed", + "provisional": "Provisional", + "differential": "Differential", + "confirmed": "Confirmed", + "refuted": "Refuted", + "entered-in-error": "Entered in error", + "help_unconfirmed": "There is not sufficient diagnostic and/or clinical evidence to treat this as a confirmed condition.", + "help_provisional": "This is a tentative diagnosis - still a candidate that is under consideration.", + "help_differential": "One of a set of potential (and typically mutually exclusive) diagnoses asserted to further guide the diagnostic process and preliminary treatment.", + "help_confirmed": "There is sufficient diagnostic and/or clinical evidence to treat this as a confirmed condition.", + "help_refuted": "This condition has been ruled out by subsequent diagnostic and clinical evidence.", + "help_entered-in-error": "The statement was entered in error and is not valid.", + "search_icd11_placeholder": "Search for ICD-11 Diagnoses", + "icd11_as_recommended": "As per ICD-11 recommended by WHO" +} \ No newline at end of file diff --git a/src/Locale/en/index.js b/src/Locale/en/index.js index 950b441ba84..781ce97b009 100644 --- a/src/Locale/en/index.js +++ b/src/Locale/en/index.js @@ -14,6 +14,7 @@ import Resource from "./Resource.json"; import SortOptions from "./SortOptions.json"; import Bed from "./Bed.json"; import Medicine from "./Medicine.json"; +import Diagnosis from "./Diagnosis.json"; export default { ...Auth, @@ -27,6 +28,7 @@ export default { ...Facility, ...Hub, ...Medicine, + ...Diagnosis, ...Notifications, ...Resource, ...Shifting, diff --git a/src/Redux/actions.tsx b/src/Redux/actions.tsx index 0b98645faf3..a120c384a3d 100644 --- a/src/Redux/actions.tsx +++ b/src/Redux/actions.tsx @@ -775,8 +775,8 @@ export const editInvestigation = ( }; // ICD11 -export const listICD11Diagnosis = (params: object, key: string) => { - return fireRequest("listICD11Diagnosis", [], params, null, key); +export const listICD11Diagnosis = (params: object) => { + return fireRequest("listICD11Diagnosis", [], params, null); }; // Medibase export const listMedibaseMedicines = (