From ce3938c3b6081bc3abebd5bee3e2913444288472 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Wed, 30 Aug 2023 06:15:01 +0000 Subject: [PATCH] Grouped Medicine Administrations in Prescriptions Table (#6176) * Grouped Administrations in Prescriptions Table * fix dependency change issue * fix refresh logic * sort discontinued down --- src/CAREUI/display/SubHeading.tsx | 33 + src/Common/hooks/useRangePagination.ts | 112 ++++ .../Facility/ConsultationDetails.tsx | 38 +- .../Medicine/AdministerMedicine.tsx | 2 +- .../Medicine/DiscontinuePrescription.tsx | 2 +- .../PrescriptionAdministrationsTable.tsx | 562 ++++++++++++++++++ src/Components/Medicine/models.ts | 8 +- src/Redux/actions.tsx | 12 + src/Utils/dayjs.ts | 2 + 9 files changed, 739 insertions(+), 32 deletions(-) create mode 100644 src/CAREUI/display/SubHeading.tsx create mode 100644 src/Common/hooks/useRangePagination.ts create mode 100644 src/Components/Medicine/PrescriptionAdministrationsTable.tsx diff --git a/src/CAREUI/display/SubHeading.tsx b/src/CAREUI/display/SubHeading.tsx new file mode 100644 index 00000000000..75d8710fdef --- /dev/null +++ b/src/CAREUI/display/SubHeading.tsx @@ -0,0 +1,33 @@ +import { ReactNode } from "react"; +import CareIcon from "../icons/CareIcon"; +import RecordMeta from "./RecordMeta"; + +interface Props { + title: ReactNode; + lastModified?: string; + className?: string; + options?: ReactNode; +} + +export default function SubHeading(props: Props) { + return ( +
+
+ + {props.title} + + {props.lastModified && ( +
+ + +
+ )} +
+ {props.options && ( +
+ {props.options} +
+ )} +
+ ); +} diff --git a/src/Common/hooks/useRangePagination.ts b/src/Common/hooks/useRangePagination.ts new file mode 100644 index 00000000000..7652ae546c1 --- /dev/null +++ b/src/Common/hooks/useRangePagination.ts @@ -0,0 +1,112 @@ +import { useEffect, useMemo, useState } from "react"; + +interface DateRange { + start: Date; + end: Date; +} + +interface Props { + bounds: DateRange; + perPage: number; + slots?: number; + defaultEnd?: boolean; +} + +const useRangePagination = ({ bounds, perPage, ...props }: Props) => { + const [currentRange, setCurrentRange] = useState( + getInitialBounds(bounds, perPage, props.defaultEnd) + ); + + useEffect(() => { + setCurrentRange(getInitialBounds(bounds, perPage, props.defaultEnd)); + }, [bounds, perPage, props.defaultEnd]); + + const next = () => { + const { end } = currentRange; + const deltaBounds = bounds.end.valueOf() - bounds.start.valueOf(); + const deltaCurrent = end.valueOf() - bounds.start.valueOf(); + + if (deltaCurrent + perPage > deltaBounds) { + setCurrentRange({ + start: new Date(bounds.end.valueOf() - perPage), + end: bounds.end, + }); + } else { + setCurrentRange({ + start: new Date(end.valueOf()), + end: new Date(end.valueOf() + perPage), + }); + } + }; + + const previous = () => { + const { start } = currentRange; + const deltaCurrent = start.valueOf() - bounds.start.valueOf(); + + if (deltaCurrent - perPage < 0) { + setCurrentRange({ + start: bounds.start, + end: new Date(bounds.start.valueOf() + perPage), + }); + } else { + setCurrentRange({ + start: new Date(start.valueOf() - perPage), + end: new Date(start.valueOf()), + }); + } + }; + + const slots = useMemo(() => { + if (!props.slots) { + return []; + } + + const slots: DateRange[] = []; + const { start } = currentRange; + const delta = perPage / props.slots; + + for (let i = 0; i < props.slots; i++) { + slots.push({ + start: new Date(start.valueOf() + delta * i), + end: new Date(start.valueOf() + delta * (i + 1)), + }); + } + + return slots; + }, [currentRange, props.slots, perPage]); + + return { + currentRange, + hasNext: currentRange.end < bounds.end, + hasPrevious: currentRange.start > bounds.start, + previous, + next, + slots, + }; +}; + +export default useRangePagination; + +const getInitialBounds = ( + bounds: DateRange, + perPage: number, + defaultEnd?: boolean +) => { + const deltaBounds = bounds.end.valueOf() - bounds.start.valueOf(); + + if (deltaBounds < perPage) { + return bounds; + } + + if (defaultEnd) { + return { + start: new Date(bounds.end.valueOf() - perPage), + end: bounds.end, + }; + } + + return { + start: bounds.start, + end: new Date(bounds.start.valueOf() + perPage), + }; +}; diff --git a/src/Components/Facility/ConsultationDetails.tsx b/src/Components/Facility/ConsultationDetails.tsx index 8058fe3cc4d..257d3712544 100644 --- a/src/Components/Facility/ConsultationDetails.tsx +++ b/src/Components/Facility/ConsultationDetails.tsx @@ -35,7 +35,6 @@ import { FileUpload } from "../Patient/FileUpload"; import HL7PatientVitalsMonitor from "../VitalsMonitor/HL7PatientVitalsMonitor"; import InvestigationTab from "./Investigations/investigationsTab"; import { make as Link } from "../Common/components/Link.bs"; -import MedicineAdministrationsTable from "../Medicine/MedicineAdministrationsTable"; import { NeurologicalTable } from "./Consultations/NeurologicalTables"; import { NonReadOnlyUsers } from "../../Utils/AuthorizeFor"; import { NursingPlot } from "./Consultations/NursingPlot"; @@ -57,13 +56,13 @@ import { useTranslation } from "react-i18next"; import { triggerGoal } from "../Common/Plausible"; import useVitalsAspectRatioConfig from "../VitalsMonitor/useVitalsAspectRatioConfig"; import useAuthUser from "../../Common/hooks/useAuthUser"; +import PrescriptionAdministrationsTable from "../Medicine/PrescriptionAdministrationsTable"; const Loading = lazy(() => import("../Common/Loading")); const PageTitle = lazy(() => import("../Common/PageTitle")); const symptomChoices = [...SYMPTOM_CHOICES]; export const ConsultationDetails = (props: any) => { - const [medicinesKey, setMedicinesKey] = useState(0); const { t } = useTranslation(); const { facilityId, patientId, consultationId } = props; const tab = props.tab.toUpperCase(); @@ -1150,30 +1149,17 @@ export const ConsultationDetails = (props: any) => { )} {tab === "MEDICINES" && ( -
-
- setMedicinesKey((k) => k + 1)} - readonly={!!consultationData.discharge_date} - /> -
-
- setMedicinesKey((k) => k + 1)} - readonly={!!consultationData.discharge_date} - /> -
-
- -
+
+ +
)} {tab === "FILES" && ( diff --git a/src/Components/Medicine/AdministerMedicine.tsx b/src/Components/Medicine/AdministerMedicine.tsx index ec749e7a535..0159bc4b7a8 100644 --- a/src/Components/Medicine/AdministerMedicine.tsx +++ b/src/Components/Medicine/AdministerMedicine.tsx @@ -53,7 +53,7 @@ export default function AdministerMedicine({ prescription, ...props }: Props) { setIsLoading(false); props.onClose(true); }} - className="w-full max-w-4xl" + className="w-full md:max-w-4xl" >
(); + const { t } = useTranslation(); + + const [state, setState] = useState(); + const pagination = useRangePagination({ + bounds: state?.administrationsTimeBounds ?? { + start: new Date(), + end: new Date(), + }, + perPage: 24 * 60 * 60 * 1000, + slots: 24, + defaultEnd: true, + }); + const [showBulkAdminister, setShowBulkAdminister] = useState(false); + + const { list, prescription } = useMemo( + () => PrescriptionActions(consultation_id), + [consultation_id] + ); + + const refetch = useCallback(async () => { + const res = await dispatch( + list({ is_prn: prn, prescription_type: "REGULAR" }) + ); + + setState({ + prescriptions: (res.data.results as Prescription[]).sort( + (a, b) => (a.discontinued ? 1 : 0) - (b.discontinued ? 1 : 0) + ), + administrationsTimeBounds: getAdministrationBounds(res.data.results), + }); + }, [consultation_id, dispatch]); + + useEffect(() => { + refetch(); + }, [refetch]); + + return ( +
+ {state?.prescriptions && ( + + { + setShowBulkAdminister(false); + refetch(); + }} + /> + + )} + + + + + + {t("edit_prescriptions")} + + {t("edit")} + + setShowBulkAdminister(true)} + className="w-full" + > + + + {t("administer_medicines")} + + {t("administer")} + + + ) + } + /> + +
+ + + + + + + + + {state === undefined + ? Array.from({ length: 24 }, (_, i) => i).map((i) => ( + + )) + : pagination.slots?.map(({ start, end }, index) => ( + + ))} + + + + + + + + {state?.prescriptions?.map((item) => ( + + ))} + {state?.prescriptions.length === 0 && ( +
+ +

+ {prn + ? "No PRN Prescriptions Prescribed" + : "No Prescriptions Prescribed"} +

+
+ )} +
+
{t("medicine")} +

Dosage &

+

+ {!state?.prescriptions[0]?.is_prn ? "Frequency" : "Indicator"} +

+
+ + + + +

+

+

{formatDateTime(start, "DD/MM")}

+

{formatDateTime(start, "HH:mm")}

+ + + Administration(s) between +
+ {formatTime(start)} and{" "} + {formatTime(end)} +
+ on {formatDate(start)} +
+
+ + + +
+
+
+ ); +} + +interface PrescriptionRowProps { + prescription: Prescription; + intervals: DateRange[]; + actions: ReturnType["prescription"]>; + refetch: () => void; +} + +const PrescriptionRow = ({ prescription, ...props }: PrescriptionRowProps) => { + const dispatch = useDispatch(); + const { t } = useTranslation(); + // const [showActions, setShowActions] = useState(false); + const [showDetails, setShowDetails] = useState(false); + const [showAdminister, setShowAdminister] = useState(false); + const [showDiscontinue, setShowDiscontinue] = useState(false); + const [administrations, setAdministrations] = + useState(); + + useEffect(() => { + setAdministrations(undefined); + + const getAdministrations = async () => { + const res = await dispatch( + props.actions.listAdministrations({ + administered_date_after: formatDateTime( + props.intervals[0].start, + "YYYY-MM-DD" + ), + administered_date_before: formatDateTime( + props.intervals[props.intervals.length - 1].end, + "YYYY-MM-DD" + ), + }) + ); + + setAdministrations(res.data.results); + }; + + getAdministrations(); + }, [prescription.id, dispatch, props.intervals]); + + return ( + + {showDiscontinue && ( + { + setShowDiscontinue(false); + if (success) { + props.refetch(); + } + }} + /> + )} + {showAdminister && ( + { + setShowAdminister(false); + if (success) { + props.refetch(); + } + }} + /> + )} + {showDetails && ( + setShowDetails(false)} + className="w-full md:max-w-4xl" + show + > +
+ +
+ setShowDetails(false)} + label={t("close")} + /> + setShowDiscontinue(true)} + > + + {t("discontinue")} + + setShowAdminister(true)} + > + + {t("administer")} + +
+
+
+ )} + setShowDetails(true)} + > +
+ + {prescription.medicine_object?.name ?? prescription.medicine_old} + + + {prescription.discontinued && ( + + {t("discontinued")} + + )} + + {prescription.route && ( + + {t(prescription.route)} + + )} +
+ + + +

{prescription.dosage}

+

+ {!prescription.is_prn + ? t("PRESCRIPTION_FREQUENCY_" + prescription.frequency) + : prescription.indicator} +

+ + + + {/* Administration Cells */} + {props.intervals.map(({ start, end }, index) => ( + + {administrations === undefined ? ( + + ) : ( + + )} + + ))} + + + {/* Action Buttons */} + + setShowAdminister(true)} + > + {t("administer")} + + + + ); +}; + +interface AdministrationCellProps { + administrations: MedicineAdministrationRecord[]; + interval: DateRange; + prescription: Prescription; +} + +const AdministrationCell = ({ + administrations, + interval: { start, end }, + prescription, +}: AdministrationCellProps) => { + // Check if cell belongs to an administered prescription + const administered = administrations.filter((administration) => + dayjs(administration.administered_date).isBetween(start, end) + ); + + if (administered.length) { + return ( +
+
+ + {administered.length > 1 && ( + + {administered.length} + + )} +
+ +

+ Administered on{" "} + {formatDateTime(administered[0].administered_date)} +

+

+ {administered.length > 1 + ? `Administered ${administered.length} times` + : `Administered ${formatTime(administered[0].administered_date)}`} +

+
+
+ ); + } + + // Check if cell belongs to a discontinued prescription + if ( + prescription.discontinued && + dayjs(end).isAfter(prescription.discontinued_date) + ) { + if (!dayjs(prescription.discontinued_date).isBetween(start, end)) return; + + return ( +
+ + +

+ Discontinued on{" "} + {formatDateTime(prescription.discontinued_date)} +

+

+ Reason:{" "} + {prescription.discontinued_reason ? ( + {prescription.discontinued_reason} + ) : ( + Not specified + )} +

+
+
+ ); + } + + // Check if cell belongs to after prescription.created_date + if (dayjs(start).isAfter(prescription.created_date)) { + return ; + } + + // Check if prescription.created_date is between start and end + // if (dayjs(prescription.created_date).isBetween(start, end)) { + // return ( + //
+ // + // + //

+ // Prescribed on{" "} + // {formatDateTime(prescription.created_date)} + //

+ //
+ //
+ // ); + // } +}; + +function getAdministrationBounds(prescriptions: Prescription[]) { + // get start by finding earliest of all presciption's created_date + const start = new Date( + prescriptions.reduce( + (earliest, curr) => + earliest < curr.created_date ? earliest : curr.created_date, + prescriptions[0]?.created_date ?? new Date() + ) + ); + + // get end by finding latest of all presciption's last_administered_on + const end = new Date( + prescriptions + .filter((prescription) => prescription.last_administered_on) + .reduce( + (latest, curr) => + curr.last_administered_on && curr.last_administered_on > latest + ? curr.last_administered_on + : latest, + prescriptions[0].created_date ?? new Date() + ) + ); + + // floor start to previous hour + start.setMinutes(0, 0, 0); + + // ceil end to next hour + end.setMinutes(0, 0, 0); + end.setHours(end.getHours() + 1); + + return { start, end }; +} diff --git a/src/Components/Medicine/models.ts b/src/Components/Medicine/models.ts index 21c52b4a6ec..cb48e9cc174 100644 --- a/src/Components/Medicine/models.ts +++ b/src/Components/Medicine/models.ts @@ -12,12 +12,12 @@ interface BasePrescription { readonly prescription_type?: "DISCHARGE" | "REGULAR"; readonly discontinued?: boolean; discontinued_reason?: string; - readonly prescribed_by?: PerformedByModel; + readonly prescribed_by: PerformedByModel; readonly discontinued_date: string; readonly last_administered_on?: string; - readonly is_migrated?: boolean; - readonly created_date?: string; - readonly modified_date?: string; + readonly is_migrated: boolean; + readonly created_date: string; + readonly modified_date: string; } export interface NormalPrescription extends BasePrescription { diff --git a/src/Redux/actions.tsx b/src/Redux/actions.tsx index 93af658f938..0ea1270f741 100644 --- a/src/Redux/actions.tsx +++ b/src/Redux/actions.tsx @@ -1027,6 +1027,18 @@ export const PrescriptionActions = (consultation_external_id: string) => { `administer-medicine-${external_id}` ), + listAdministrations: (query?: { + administered_date_after?: string; + administered_date_before?: string; + }) => + fireRequest( + "listAdministrations", + [], + { prescription: external_id, ...query }, + pathParams, + `list-administrations-${external_id}` + ), + /** Discontinue a prescription */ discontinue: (discontinued_reason: string | undefined) => fireRequest( diff --git a/src/Utils/dayjs.ts b/src/Utils/dayjs.ts index f883229b45e..b70c2fc044b 100644 --- a/src/Utils/dayjs.ts +++ b/src/Utils/dayjs.ts @@ -2,9 +2,11 @@ import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; import duration from "dayjs/plugin/duration"; import customParseFormat from "dayjs/plugin/customParseFormat"; +import isBetween from "dayjs/plugin/isBetween"; dayjs.extend(relativeTime); dayjs.extend(duration); dayjs.extend(customParseFormat); +dayjs.extend(isBetween); export default dayjs;