diff --git a/src/CAREUI/display/RecordMeta.tsx b/src/CAREUI/display/RecordMeta.tsx index 48cc8d370ad..944ddf27c8f 100644 --- a/src/CAREUI/display/RecordMeta.tsx +++ b/src/CAREUI/display/RecordMeta.tsx @@ -1,5 +1,10 @@ import CareIcon from "../icons/CareIcon"; -import { formatDateTime, isUserOnline, relativeTime } from "../../Utils/utils"; +import { + formatDateTime, + formatName, + isUserOnline, + relativeTime, +} from "../../Utils/utils"; import { ReactNode } from "react"; interface Props { @@ -30,7 +35,7 @@ const RecordMeta = ({ time, user, prefix, className, inlineUser }: Props) => { by - {user.first_name} {user.last_name} + {formatName(user)} {isOnline && (
)} @@ -48,9 +53,7 @@ const RecordMeta = ({ time, user, prefix, className, inlineUser }: Props) => { {user && inlineUser && by} {user && } {user && inlineUser && ( - - {user.first_name} {user.last_name} - + {formatName(user)} )}
); diff --git a/src/CAREUI/display/Timeline.tsx b/src/CAREUI/display/Timeline.tsx new file mode 100644 index 00000000000..7549fbfd69f --- /dev/null +++ b/src/CAREUI/display/Timeline.tsx @@ -0,0 +1,158 @@ +import { createContext, useContext } from "react"; +import { PerformedByModel } from "../../Components/HCX/misc"; +import { classNames, formatName } from "../../Utils/utils"; +import CareIcon, { IconName } from "../icons/CareIcon"; +import RecordMeta from "./RecordMeta"; + +export interface TimelineEvent { + type: TType; + timestamp: string; + by: PerformedByModel | undefined; + icon: IconName; + notes?: string; + cancelled?: boolean; +} + +interface TimelineProps { + className: string; + children: React.ReactNode | React.ReactNode[]; + name: string; +} + +const TimelineContext = createContext(""); + +export default function Timeline({ className, children, name }: TimelineProps) { + return ( +
+
    + + {children} + +
+
+ ); +} + +interface TimelineNodeProps { + event: TimelineEvent; + title?: React.ReactNode; + /** + * Used to add a suffix to the auto-generated title. Will be ignored if `title` is provided. + */ + titleSuffix?: React.ReactNode; + actions?: React.ReactNode; + className?: string; + children?: React.ReactNode; + name?: string; + isLast: boolean; +} + +export const TimelineNode = (props: TimelineNodeProps) => { + const name = useContext(TimelineContext); + + return ( +
  • +
    +
    +
    + +
    +
    +
    + {props.title || ( + +

    + {props.event.by && ( + + {formatName(props.event.by)}{" "} + + )} + {props.titleSuffix + ? props.titleSuffix + : `${props.event.type} the ${props.name || name}.`} +

    + {props.actions && ( + {props.actions} + )} + +
    + )} +
    +
    + +
    + {props.event.notes} + {props.children} +
    +
    +
  • + ); +}; + +interface TimelineNodeTitleProps { + children: React.ReactNode | React.ReactNode[]; + event: TimelineEvent; +} + +export const TimelineNodeTitle = (props: TimelineNodeTitleProps) => { + return ( + <> +
    +
    + +
    + {props.children} +
    + + ); +}; + +export const TimelineNodeActions = (props: { + children: React.ReactNode | React.ReactNode[]; +}) => { + return
    {props.children}
    ; +}; + +interface TimelineNodeNotesProps { + children?: React.ReactNode | React.ReactNode[]; + icon?: IconName; +} + +export const TimelineNodeNotes = ({ + children, + icon = "l-notes", +}: TimelineNodeNotesProps) => { + if (!children) { + return; + } + + return ( +
    + +
    {children}
    +
    + ); +}; diff --git a/src/CAREUI/interactive/ScrollOverlay.tsx b/src/CAREUI/interactive/ScrollOverlay.tsx new file mode 100644 index 00000000000..c49f7223149 --- /dev/null +++ b/src/CAREUI/interactive/ScrollOverlay.tsx @@ -0,0 +1,30 @@ +import useVisibility from "../../Utils/useVisibility"; +import { classNames } from "../../Utils/utils"; + +interface Props { + className?: string; + children: React.ReactNode; + overlay: React.ReactNode; + disableOverlay?: boolean; +} + +export default function ScrollOverlay(props: Props) { + const [bottomIsVisible, ref] = useVisibility(); + const hasScrollContent = !props.disableOverlay && !bottomIsVisible; + + return ( +
    + {props.children} + +
    +
    + {hasScrollContent && props.overlay} +
    +
    + ); +} diff --git a/src/CAREUI/interactive/SlideOver.tsx b/src/CAREUI/interactive/SlideOver.tsx index 34e1c615f94..9fae2588fca 100644 --- a/src/CAREUI/interactive/SlideOver.tsx +++ b/src/CAREUI/interactive/SlideOver.tsx @@ -61,7 +61,7 @@ export default function SlideOver({ {}} > diff --git a/src/Components/Auth/Login.tsx b/src/Components/Auth/Login.tsx index 140a0013fd9..58472c4ff25 100644 --- a/src/Components/Auth/Login.tsx +++ b/src/Components/Auth/Login.tsx @@ -12,6 +12,7 @@ import CircularProgress from "../Common/components/CircularProgress"; import { LocalStorageKeys } from "../../Common/constants"; import ReactMarkdown from "react-markdown"; import rehypeRaw from "rehype-raw"; +import { handleRedirection } from "../../Utils/utils"; export const Login = (props: { forgot?: boolean }) => { const { @@ -109,7 +110,7 @@ export const Login = (props: { forgot?: boolean }) => { window.location.pathname === "/" || window.location.pathname === "/login" ) { - window.location.href = "/facility"; + handleRedirection(); } else { window.location.href = window.location.pathname.toString(); } diff --git a/src/Components/Common/Dialog.tsx b/src/Components/Common/Dialog.tsx index ffe17606de2..c1dcf5afb70 100644 --- a/src/Components/Common/Dialog.tsx +++ b/src/Components/Common/Dialog.tsx @@ -26,7 +26,7 @@ const DialogModal = (props: DialogProps) => { return (
    - + { return (
    {/* eslint-disable-next-line i18next/no-literal-string */} - -
    diff --git a/src/Components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx index 259e8aca622..736a7c81575 100644 --- a/src/Components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx +++ b/src/Components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx @@ -240,7 +240,6 @@ export const ConsultationUpdatesTab = (props: ConsultationTabProps) => {
    {
    { setFacility(selected); const { id, name } = selected || {}; @@ -238,7 +236,7 @@ const DischargeModal = ({ setSelected={(selected) => handleFacilitySelect(selected as FacilityModel) } - selected={facility} + selected={facility ?? null} showAll freeText multiple={false} @@ -284,18 +282,11 @@ const DischargeModal = ({
    Discharge Prescription Medications - +
    Discharge PRN Prescriptions - +
    )} diff --git a/src/Components/Form/FormFields/PhoneNumberFormField.tsx b/src/Components/Form/FormFields/PhoneNumberFormField.tsx index 6aa70f801de..b6af2bfaa24 100644 --- a/src/Components/Form/FormFields/PhoneNumberFormField.tsx +++ b/src/Components/Form/FormFields/PhoneNumberFormField.tsx @@ -138,7 +138,7 @@ const phoneNumberTypeIcons: Record = { const PhoneNumberTypesHelp = ({ types }: { types: PhoneNumberType[] }) => (
    {types.map((type) => ( - + ["prescription"]>; onClose: (success: boolean) => void; } export default function AdministerMedicine({ prescription, ...props }: Props) { const { t } = useTranslation(); - const dispatch = useDispatch(); + const consultation = useSlug("consultation"); const [isLoading, setIsLoading] = useState(false); const [notes, setNotes] = useState(""); const [isCustomTime, setIsCustomTime] = useState(false); @@ -52,13 +52,14 @@ export default function AdministerMedicine({ prescription, ...props }: Props) { onClose={() => props.onClose(false)} onConfirm={async () => { setIsLoading(true); - const res = await dispatch( - props.actions.administer({ + const { res } = await request(MedicineRoutes.administerPrescription, { + pathParams: { consultation, external_id: prescription.id }, + body: { notes, administered_date: isCustomTime ? customTime : undefined, - }) - ); - if (res.status === 201) { + }, + }); + if (res?.ok) { Success({ msg: t("medicines_administered") }); } setIsLoading(false); @@ -67,11 +68,7 @@ export default function AdministerMedicine({ prescription, ...props }: Props) { className="w-full md:max-w-4xl" >
    - +
    ["create"]; onDone: () => void; }) { - const dispatch = useDispatch(); - const [isCreating, setIsCreating] = useState(false); const { t } = useTranslation(); + const consultation = useSlug("consultation"); + const [isCreating, setIsCreating] = useState(false); return ( disabled={isCreating} defaults={props.prescription} onCancel={props.onDone} - onSubmit={async (obj) => { - obj["medicine"] = obj.medicine_object?.id; - delete obj.medicine_object; + onSubmit={async (body) => { + body["medicine"] = body.medicine_object?.id; + delete body.medicine_object; setIsCreating(true); - const res = await dispatch(props.create(obj)); + const { res, error } = await request( + MedicineRoutes.createPrescription, + { + pathParams: { consultation }, + body, + } + ); setIsCreating(false); - if (res.status !== 201) { - return res.data; - } else { - props.onDone(); + + if (!res?.ok) { + return error; } + + Success({ msg: t("Medicine prescribed") }); + props.onDone(); }} noPadding validate={PrescriptionFormValidator()} diff --git a/src/Components/Medicine/DiscontinuePrescription.tsx b/src/Components/Medicine/DiscontinuePrescription.tsx index 38dd66a95a8..f54ba9974fc 100644 --- a/src/Components/Medicine/DiscontinuePrescription.tsx +++ b/src/Components/Medicine/DiscontinuePrescription.tsx @@ -1,22 +1,22 @@ import { useState } from "react"; -import { PrescriptionActions } from "../../Redux/actions"; import ConfirmDialog from "../Common/ConfirmDialog"; import { Prescription } from "./models"; import TextAreaFormField from "../Form/FormFields/TextAreaFormField"; import { Success } from "../../Utils/Notifications"; -import { useDispatch } from "react-redux"; import PrescriptionDetailCard from "./PrescriptionDetailCard"; import { useTranslation } from "react-i18next"; +import request from "../../Utils/request/request"; +import MedicineRoutes from "./routes"; +import useSlug from "../../Common/hooks/useSlug"; interface Props { prescription: Prescription; - actions: ReturnType["prescription"]>; onClose: (discontinued: boolean) => void; } export default function DiscontinuePrescription(props: Props) { const { t } = useTranslation(); - const dispatch = useDispatch(); + const consultation = useSlug("consultation"); const [isDiscontinuing, setIsDiscontinuing] = useState(false); const [discontinuedReason, setDiscontinuedReason] = useState(""); @@ -29,10 +29,13 @@ export default function DiscontinuePrescription(props: Props) { variant="danger" onConfirm={async () => { setIsDiscontinuing(true); - const res = await dispatch( - props.actions.discontinue(discontinuedReason) - ); - if (res.status === 201) { + const { res } = await request(MedicineRoutes.discontinuePrescription, { + pathParams: { consultation, external_id: props.prescription.id }, + body: { + discontinued_reason: discontinuedReason, + }, + }); + if (res?.ok) { Success({ msg: t("prescription_discontinued") }); } setIsDiscontinuing(false); @@ -41,11 +44,7 @@ export default function DiscontinuePrescription(props: Props) { className="w-full md:max-w-4xl" >
    - + { - const discontinue = await request(routes.discontinuePrescription, { - pathParams: { consultation_external_id, external_id: oldObj.id }, + const discontinue = await request(MedicineRoutes.discontinuePrescription, { + pathParams: { + consultation: consultation_external_id, + external_id: oldObj.id, + }, body: { discontinued_reason: discontinued_reason ? `Edit: ${discontinued_reason}` @@ -43,8 +46,8 @@ const handleSubmit = async ( return; } - const { res } = await request(routes.createPrescription, { - pathParams: { consultation_external_id }, + const { res } = await request(MedicineRoutes.createPrescription, { + pathParams: { consultation: consultation_external_id }, body: { ...newObj, // Forcing the medicine to be the same as the old one diff --git a/src/Components/Medicine/ManagePrescriptions.tsx b/src/Components/Medicine/ManagePrescriptions.tsx index 95d5b17d3c2..16e8ffedcb2 100644 --- a/src/Components/Medicine/ManagePrescriptions.tsx +++ b/src/Components/Medicine/ManagePrescriptions.tsx @@ -1,17 +1,11 @@ import { useTranslation } from "react-i18next"; import CareIcon from "../../CAREUI/icons/CareIcon"; import useAppHistory from "../../Common/hooks/useAppHistory"; -import { PrescriptionActions } from "../../Redux/actions"; import ButtonV2 from "../Common/components/ButtonV2"; import Page from "../Common/components/Page"; import PrescriptionBuilder from "./PrescriptionBuilder"; -interface Props { - consultationId: string; -} - -export default function ManagePrescriptions({ consultationId }: Props) { - const actions = PrescriptionActions(consultationId); +export default function ManagePrescriptions() { const { t } = useTranslation(); const { goBack } = useAppHistory(); @@ -23,13 +17,13 @@ export default function ManagePrescriptions({ consultationId }: Props) {

    {t("prescription_medications")}

    - +

    {t("prn_prescriptions")}

    - +
    diff --git a/src/Components/Medicine/MedicineAdministration.tsx b/src/Components/Medicine/MedicineAdministration.tsx index 5d8347ba5a5..d899a3800fb 100644 --- a/src/Components/Medicine/MedicineAdministration.tsx +++ b/src/Components/Medicine/MedicineAdministration.tsx @@ -1,27 +1,27 @@ import { useEffect, useMemo, useState } from "react"; -import { PrescriptionActions } from "../../Redux/actions"; import PrescriptionDetailCard from "./PrescriptionDetailCard"; import { MedicineAdministrationRecord, Prescription } from "./models"; import TextAreaFormField from "../Form/FormFields/TextAreaFormField"; import CheckBoxFormField from "../Form/FormFields/CheckBoxFormField"; import ButtonV2 from "../Common/components/ButtonV2"; import CareIcon from "../../CAREUI/icons/CareIcon"; -import { useDispatch } from "react-redux"; import { Error, Success } from "../../Utils/Notifications"; -import { formatDateTime } from "../../Utils/utils"; +import { classNames, formatDateTime } from "../../Utils/utils"; import { useTranslation } from "react-i18next"; import dayjs from "../../Utils/dayjs"; import TextFormField from "../Form/FormFields/TextFormField"; +import request from "../../Utils/request/request"; +import MedicineRoutes from "./routes"; +import useSlug from "../../Common/hooks/useSlug"; interface Props { prescriptions: Prescription[]; - action: ReturnType["prescription"]; onDone: () => void; } export default function MedicineAdministration(props: Props) { const { t } = useTranslation(); - const dispatch = useDispatch(); + const consultation = useSlug("consultation"); const [shouldAdminister, setShouldAdminister] = useState([]); const [notes, setNotes] = useState( [] @@ -46,34 +46,35 @@ export default function MedicineAdministration(props: Props) { ); }, [props.prescriptions]); - const handleSubmit = () => { - const records: MedicineAdministrationRecord[] = []; - prescriptions.forEach((prescription, i) => { - if (shouldAdminister[i]) { - records.push({ - prescription, - notes: notes[i], - administered_date: isCustomTime[i] ? customTime[i] : undefined, - }); - } - }); + const handleSubmit = async () => { + const administrations = prescriptions + .map((prescription, i) => ({ + prescription, + notes: notes[i], + administered_date: isCustomTime[i] ? customTime[i] : undefined, + })) + .filter((_, i) => shouldAdminister[i]); - Promise.all( - records.map(async ({ prescription, ...record }) => { - const res = await dispatch( - props.action(prescription?.id ?? "").administer(record) - ); - if (res.status !== 201) { - Error({ msg: t("medicines_administered_error") }); - } - }) - ).then(() => { - Success({ msg: t("medicines_administered") }); - props.onDone(); - }); + const ok = await Promise.all( + administrations.map(({ prescription, ...body }) => + request(MedicineRoutes.administerPrescription, { + pathParams: { consultation, external_id: prescription.id }, + body, + }).then(({ res }) => !!res?.ok) + ) + ); + + if (!ok) { + Error({ msg: t("medicines_administered_error") }); + return; + } + + Success({ msg: t("medicines_administered") }); + props.onDone(); }; const selectedCount = shouldAdminister.filter(Boolean).length; + const is_prn = prescriptions.some((obj) => obj.is_prn); return (
    @@ -82,10 +83,14 @@ export default function MedicineAdministration(props: Props) { key={obj.id} prescription={obj} readonly - actions={props.action(obj?.id ?? "")} selected={shouldAdminister[index]} > -
    +
    -
    +
    + dayjs(administration.administered_date).isBetween(start, end) + ) + .sort( + (a, b) => + new Date(a.administered_date!).getTime() - + new Date(b.administered_date!).getTime() + ); + + const hasComment = administered.some((obj) => !!obj.notes); + + if (administered.length) { + return ( + <> + setShowTimeline(false)} + title={ + + } + className="w-full md:max-w-4xl" + show={showTimeline} + > +
    + Administrations between{" "} + {formatTime(start, "HH:mm")} and{" "} + {formatTime(end, "HH:mm")} on{" "} + + {formatDateTime(start, "DD/MM/YYYY")} + +
    + +
    + + + ); + } + + // Check if cell belongs to after prescription.created_date + if (dayjs(start).isAfter(prescription.created_date)) { + return ; + } + + // 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 + )} +

    +
    +
    + ); + } +} diff --git a/src/Components/Medicine/MedicineAdministrationSheet/AdministrationEventSeperator.tsx b/src/Components/Medicine/MedicineAdministrationSheet/AdministrationEventSeperator.tsx new file mode 100644 index 00000000000..a83fa38bd9c --- /dev/null +++ b/src/Components/Medicine/MedicineAdministrationSheet/AdministrationEventSeperator.tsx @@ -0,0 +1,22 @@ +import { formatDateTime } from "../../../Utils/utils"; + +export default function AdministrationEventSeperator({ date }: { date: Date }) { + // Show date if it's 00:00 + if (date.getHours() === 0) { + return ( +
    + +

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

    +
    +
    + ); + } + + return ( +
    + + {/*

    {formatDateTime(date, "HH")}

    */} +
    +
    + ); +} diff --git a/src/Components/Medicine/MedicineAdministrationSheet/AdministrationTable.tsx b/src/Components/Medicine/MedicineAdministrationSheet/AdministrationTable.tsx new file mode 100644 index 00000000000..9de207146de --- /dev/null +++ b/src/Components/Medicine/MedicineAdministrationSheet/AdministrationTable.tsx @@ -0,0 +1,106 @@ +import { useTranslation } from "react-i18next"; +import CareIcon from "../../../CAREUI/icons/CareIcon"; +import useRangePagination from "../../../Common/hooks/useRangePagination"; +import { classNames, formatDateTime } from "../../../Utils/utils"; +import ButtonV2 from "../../Common/components/ButtonV2"; +import { Prescription } from "../models"; +import MedicineAdministrationTableRow from "./AdministrationTableRow"; + +interface Props { + prescriptions: Prescription[]; + pagination: ReturnType; + onRefetch: () => void; +} + +export default function MedicineAdministrationTable({ + pagination, + prescriptions, + onRefetch, +}: Props) { + const { t } = useTranslation(); + + return ( +
    + + + + + + + {pagination.slots?.map(({ start }, index) => ( + <> + + + + + + + + + {prescriptions.map((obj) => ( + + ))} + +
    +
    + {t("medicine")} + +

    Dosage &

    +

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

    +
    +
    +
    + + + + + {formatDateTime( + start, + start.getHours() === 0 ? "DD/MM" : "h a" + )} + + + ))} + + + + +
    +
    + ); +} diff --git a/src/Components/Medicine/MedicineAdministrationSheet/AdministrationTableRow.tsx b/src/Components/Medicine/MedicineAdministrationSheet/AdministrationTableRow.tsx new file mode 100644 index 00000000000..30ab68dcf0b --- /dev/null +++ b/src/Components/Medicine/MedicineAdministrationSheet/AdministrationTableRow.tsx @@ -0,0 +1,245 @@ +import { useTranslation } from "react-i18next"; +import { Prescription } from "../models"; +import { useState } from "react"; +import useQuery from "../../../Utils/request/useQuery"; +import MedicineRoutes from "../routes"; +import { classNames, formatDateTime } from "../../../Utils/utils"; +import useSlug from "../../../Common/hooks/useSlug"; +import DiscontinuePrescription from "../DiscontinuePrescription"; +import AdministerMedicine from "../AdministerMedicine"; +import DialogModal from "../../Common/Dialog"; +import PrescriptionDetailCard from "../PrescriptionDetailCard"; +import ButtonV2, { Cancel, Submit } from "../../Common/components/ButtonV2"; +import CareIcon from "../../../CAREUI/icons/CareIcon"; +import EditPrescriptionForm from "../EditPrescriptionForm"; +import AdministrationEventSeperator from "./AdministrationEventSeperator"; +import AdministrationEventCell from "./AdministrationEventCell"; + +interface Props { + prescription: Prescription; + intervals: { start: Date; end: Date }[]; + refetch: () => void; +} + +export default function MedicineAdministrationTableRow({ + prescription, + ...props +}: Props) { + const { t } = useTranslation(); + const consultation = useSlug("consultation"); + // const [showActions, setShowActions] = useState(false); + const [showDetails, setShowDetails] = useState(false); + const [showEdit, setShowEdit] = useState(false); + const [showAdminister, setShowAdminister] = useState(false); + const [showDiscontinue, setShowDiscontinue] = useState(false); + + const { data, loading } = useQuery(MedicineRoutes.listAdministrations, { + pathParams: { consultation }, + query: { + prescription: prescription.id, + 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" + ), + archived: false, + }, + key: `${prescription.last_administered_on}`, + }); + + 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")} + + { + setShowDetails(false); + setShowEdit(true); + }} + > + + {t("edit")} + + setShowAdminister(true)} + > + + {t("administer")} + +
    +
    +
    + )} + {showEdit && ( + setShowEdit(false)} + show={showEdit} + title={`${t("edit")} ${t( + prescription.is_prn ? "prn_prescription" : "prescription_medication" + )}: ${ + prescription.medicine_object?.name ?? prescription.medicine_old + }`} + description={ +
    + + {t("edit_caution_note")} +
    + } + className="w-full max-w-3xl lg:min-w-[600px]" + > + { + setShowEdit(false); + if (success) { + props.refetch(); + } + }} + /> +
    + )} + 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) => ( + <> + + + + + + {!data?.results ? ( + + ) : ( + + )} + + + ))} + + + {/* Action Buttons */} + + setShowAdminister(true)} + > + {t("administer")} + + + + ); +} diff --git a/src/Components/Medicine/MedicineAdministrationSheet/BulkAdminister.tsx b/src/Components/Medicine/MedicineAdministrationSheet/BulkAdminister.tsx new file mode 100644 index 00000000000..abd609871c8 --- /dev/null +++ b/src/Components/Medicine/MedicineAdministrationSheet/BulkAdminister.tsx @@ -0,0 +1,47 @@ +import { useTranslation } from "react-i18next"; +import { Prescription } from "../models"; +import { useState } from "react"; +import ButtonV2 from "../../Common/components/ButtonV2"; +import CareIcon from "../../../CAREUI/icons/CareIcon"; +import SlideOver from "../../../CAREUI/interactive/SlideOver"; +import MedicineAdministration from "../MedicineAdministration"; + +interface Props { + prescriptions: Prescription[]; + onDone: () => void; +} + +export default function BulkAdminister({ prescriptions, onDone }: Props) { + const { t } = useTranslation(); + const [showBulkAdminister, setShowBulkAdminister] = useState(false); + + return ( + <> + setShowBulkAdminister(true)} + className="w-full" + disabled={prescriptions.length === 0} + > + + {t("administer_medicines")} + {t("administer")} + + + { + setShowBulkAdminister(false); + onDone(); + }} + /> + + + ); +} diff --git a/src/Components/Medicine/MedicineAdministrationSheet/index.tsx b/src/Components/Medicine/MedicineAdministrationSheet/index.tsx new file mode 100644 index 00000000000..187d5447314 --- /dev/null +++ b/src/Components/Medicine/MedicineAdministrationSheet/index.tsx @@ -0,0 +1,158 @@ +import { useTranslation } from "react-i18next"; +import useSlug from "../../../Common/hooks/useSlug"; +import useQuery from "../../../Utils/request/useQuery"; +import MedicineRoutes from "../routes"; +import { useMemo, useState } from "react"; +import { computeActivityBounds } from "./utils"; +import useBreakpoints from "../../../Common/hooks/useBreakpoints"; +import SubHeading from "../../../CAREUI/display/SubHeading"; +import ButtonV2 from "../../Common/components/ButtonV2"; +import CareIcon from "../../../CAREUI/icons/CareIcon"; +import BulkAdminister from "./BulkAdminister"; +import useRangePagination from "../../../Common/hooks/useRangePagination"; +import MedicineAdministrationTable from "./AdministrationTable"; +import Loading from "../../Common/Loading"; +import ScrollOverlay from "../../../CAREUI/interactive/ScrollOverlay"; + +interface Props { + readonly?: boolean; + is_prn: boolean; +} + +const DEFAULT_BOUNDS = { start: new Date(), end: new Date() }; + +const MedicineAdministrationSheet = ({ readonly, is_prn }: Props) => { + const { t } = useTranslation(); + const consultation = useSlug("consultation"); + + const [showDiscontinued, setShowDiscontinued] = useState(false); + + const filters = { is_prn, prescription_type: "REGULAR", limit: 100 }; + + const { data, loading, refetch } = useQuery( + MedicineRoutes.listPrescriptions, + { + pathParams: { consultation }, + query: { ...filters, discontinued: showDiscontinued ? undefined : false }, + } + ); + + const discontinuedPrescriptions = useQuery(MedicineRoutes.listPrescriptions, { + pathParams: { consultation }, + query: { ...filters, discontinued: true }, + prefetch: !showDiscontinued, + }); + + const discontinuedCount = discontinuedPrescriptions.data?.count; + + const { activityTimelineBounds, prescriptions } = useMemo( + () => ({ + prescriptions: data?.results?.sort( + (a, b) => +a.discontinued - +b.discontinued + ), + activityTimelineBounds: data + ? computeActivityBounds(data.results) + : undefined, + }), + [data] + ); + + const daysPerPage = useBreakpoints({ default: 1, "2xl": 2 }); + const pagination = useRangePagination({ + bounds: activityTimelineBounds ?? DEFAULT_BOUNDS, + perPage: daysPerPage * 24 * 60 * 60 * 1000, + slots: (daysPerPage * 24) / 4, // Grouped by 4 hours + defaultEnd: true, + }); + + return ( +
    + + + + + {t("edit_prescriptions")} + + {t("edit")} + + refetch()} + /> + + ) + } + /> + + + Scroll to view more prescriptions + +
    + } + disableOverlay={loading || !prescriptions?.length} + > + {loading && } + {prescriptions?.length === 0 && } + + {!!prescriptions?.length && ( + { + refetch(); + discontinuedPrescriptions.refetch(); + }} + /> + )} + + {!showDiscontinued && !!discontinuedCount && ( + setShowDiscontinued(true)} + > + + + + Show {discontinuedCount} discontinued + prescription(s) + + + + )} + +
    + ); +}; + +export default MedicineAdministrationSheet; + +const NoPrescriptions = ({ prn }: { prn: boolean }) => { + return ( +
    + +

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

    +
    + ); +}; diff --git a/src/Components/Medicine/MedicineAdministrationSheet/utils.ts b/src/Components/Medicine/MedicineAdministrationSheet/utils.ts new file mode 100644 index 00000000000..93ee5fb4b08 --- /dev/null +++ b/src/Components/Medicine/MedicineAdministrationSheet/utils.ts @@ -0,0 +1,34 @@ +import { Prescription } from "../models"; + +export function computeActivityBounds(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 00:00 of the day + start.setHours(0, 0, 0, 0); + + // ceil end to 00:00 of the next day + end.setDate(end.getDate() + 1); + end.setHours(0, 0, 0, 0); + + return { start, end }; +} diff --git a/src/Components/Medicine/MedicineAdministrationsTable.tsx b/src/Components/Medicine/MedicineAdministrationsTable.tsx deleted file mode 100644 index 4ffc25e43f0..00000000000 --- a/src/Components/Medicine/MedicineAdministrationsTable.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; -import ResponsiveMedicineTable from "../Common/components/ResponsiveMedicineTables"; -import { formatDateTime } from "../../Utils/utils"; -import { PrescriptionActions } from "../../Redux/actions"; -import { useDispatch } from "react-redux"; -import { MedicineAdministrationRecord } from "./models"; -import CareIcon from "../../CAREUI/icons/CareIcon"; -import RecordMeta from "../../CAREUI/display/RecordMeta"; -import { useTranslation } from "react-i18next"; - -interface Props { - consultation_id: string; -} - -export default function MedicineAdministrationsTable({ - consultation_id, -}: Props) { - const { t } = useTranslation(); - const dispatch = useDispatch(); - const [items, setItems] = useState(); - - const { listAdministrations } = useMemo( - () => PrescriptionActions(consultation_id), - [consultation_id] - ); - - const fetchItems = useCallback(() => { - dispatch(listAdministrations()).then((res: any) => - setItems(res.data.results) - ); - }, [consultation_id]); - - useEffect(() => { - fetchItems(); - }, [consultation_id]); - - const lastModified = items?.[0]?.modified_date; - - return ( -
    -
    -
    - - {t("medicine_administration_history")} - -
    - - - {lastModified && formatDateTime(lastModified)} - -
    -
    -
    -
    -
    -
    - t(_))} - list={ - items?.map((obj) => ({ - ...obj, - medicine: - obj.prescription?.medicine_object?.name ?? - obj.prescription?.medicine_old, - created_date__pretty: ( - - by{" "} - {obj.administered_by?.first_name}{" "} - {obj.administered_by?.last_name} - - ), - ...obj, - })) || [] - } - objectKeys={["medicine", "notes", "created_date__pretty"]} - fieldsToDisplay={[2, 3]} - /> - {items?.length === 0 && ( -
    - {t("no_data_found")} -
    - )} -
    -
    -
    -
    - ); -} diff --git a/src/Components/Medicine/PrescriptionAdministrationsTable.tsx b/src/Components/Medicine/PrescriptionAdministrationsTable.tsx deleted file mode 100644 index c80b66f44c6..00000000000 --- a/src/Components/Medicine/PrescriptionAdministrationsTable.tsx +++ /dev/null @@ -1,652 +0,0 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; -import { PrescriptionActions } from "../../Redux/actions"; -import { useDispatch } from "react-redux"; -import { MedicineAdministrationRecord, Prescription } from "./models"; -import CareIcon from "../../CAREUI/icons/CareIcon"; -import ButtonV2, { Cancel, Submit } from "../Common/components/ButtonV2"; -import SlideOver from "../../CAREUI/interactive/SlideOver"; -import MedicineAdministration from "./MedicineAdministration"; -import DiscontinuePrescription from "./DiscontinuePrescription"; -import AdministerMedicine from "./AdministerMedicine"; -import DialogModal from "../Common/Dialog"; -import PrescriptionDetailCard from "./PrescriptionDetailCard"; -import { useTranslation } from "react-i18next"; -import SubHeading from "../../CAREUI/display/SubHeading"; -import dayjs from "../../Utils/dayjs"; -import { - classNames, - formatDate, - formatDateTime, - formatTime, -} from "../../Utils/utils"; -import useRangePagination from "../../Common/hooks/useRangePagination"; -import EditPrescriptionForm from "./EditPrescriptionForm"; - -interface DateRange { - start: Date; - end: Date; -} - -interface Props { - prn: boolean; - prescription_type?: Prescription["prescription_type"]; - consultation_id: string; - readonly?: boolean; -} - -interface State { - prescriptions: Prescription[]; - administrationsTimeBounds: DateRange; -} - -export default function PrescriptionAdministrationsTable({ - prn, - consultation_id, - readonly, -}: Props) { - const dispatch = useDispatch(); - const { t } = useTranslation(); - - const [state, setState] = useState(); - const [showDiscontinued, setShowDiscontinued] = useState(false); - const [discontinuedCount, setDiscontinuedCount] = 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 filters = { - is_prn: prn, - prescription_type: "REGULAR", - }; - - const res = await dispatch( - list(showDiscontinued ? filters : { ...filters, discontinued: false }) - ); - - setState({ - prescriptions: (res.data.results as Prescription[]).sort( - (a, b) => (a.discontinued ? 1 : 0) - (b.discontinued ? 1 : 0) - ), - administrationsTimeBounds: getAdministrationBounds(res.data.results), - }); - - if (showDiscontinued === false) { - const discontinuedRes = await dispatch( - list({ ...filters, discontinued: true, limit: 0 }) - ); - setDiscontinuedCount(discontinuedRes.data.count); - } - }, [consultation_id, showDiscontinued, dispatch]); - - useEffect(() => { - refetch(); - }, [refetch]); - - return ( -
    - {state?.prescriptions && ( - - { - setShowBulkAdminister(false); - refetch(); - }} - /> - - )} - - - - - - {t("edit_prescriptions")} - - {t("edit")} - - setShowBulkAdminister(true)} - className="w-full" - disabled={ - state === undefined || state.prescriptions.length === 0 - } - > - - - {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) => ( - - ))} - -
    -
    - {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)} -
    -
    - - - -
    - - {showDiscontinued === false && !!discontinuedCount && ( - setShowDiscontinued(true)} - > - - - - Show {discontinuedCount} other discontinued - prescription(s) - - - - )} - - {state?.prescriptions.length === 0 && ( -
    - -

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

    -
    - )} -
    -
    - ); -} - -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 [showEdit, setShowEdit] = 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")} - - { - setShowDetails(false); - setShowEdit(true); - }} - > - - {t("edit")} - - setShowAdminister(true)} - > - - {t("administer")} - -
    -
    -
    - )} - {showEdit && ( - setShowEdit(false)} - show={showEdit} - title={`${t("edit")} ${t( - prescription.is_prn ? "prn_prescription" : "prescription_medication" - )}: ${ - prescription.medicine_object?.name ?? prescription.medicine_old - }`} - description={ -
    - - {t("edit_caution_note")} -
    - } - className="w-full max-w-3xl lg:min-w-[600px]" - > - { - setShowEdit(false); - if (success) { - props.refetch(); - } - }} - /> -
    - )} - 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/PrescriptionBuilder.tsx b/src/Components/Medicine/PrescriptionBuilder.tsx index f7a4dd49c93..39bf9b2f506 100644 --- a/src/Components/Medicine/PrescriptionBuilder.tsx +++ b/src/Components/Medicine/PrescriptionBuilder.tsx @@ -1,56 +1,49 @@ -import { useCallback, useEffect, useState } from "react"; +import { useState } from "react"; import CareIcon from "../../CAREUI/icons/CareIcon"; import ButtonV2 from "../Common/components/ButtonV2"; import { NormalPrescription, Prescription, PRNPrescription } from "./models"; import DialogModal from "../Common/Dialog"; import CreatePrescriptionForm from "./CreatePrescriptionForm"; import PrescriptionDetailCard from "./PrescriptionDetailCard"; -import { PrescriptionActions } from "../../Redux/actions"; -import { useDispatch } from "react-redux"; import DiscontinuePrescription from "./DiscontinuePrescription"; import AdministerMedicine from "./AdministerMedicine"; import { useTranslation } from "react-i18next"; +import useQuery from "../../Utils/request/useQuery"; +import MedicineRoutes from "./routes"; +import useSlug from "../../Common/hooks/useSlug"; interface Props { prescription_type?: Prescription["prescription_type"]; - actions: ReturnType; is_prn?: boolean; disabled?: boolean; } export default function PrescriptionBuilder({ prescription_type, - actions, is_prn = false, disabled, }: Props) { const { t } = useTranslation(); - const dispatch = useDispatch(); - - const [prescriptions, setPrescriptions] = useState(); + const consultation = useSlug("consultation"); const [showCreate, setShowCreate] = useState(false); const [showDiscontinueFor, setShowDiscontinueFor] = useState(); const [showAdministerFor, setShowAdministerFor] = useState(); - const fetchPrescriptions = useCallback(() => { - dispatch(actions.list({ is_prn, prescription_type })).then((res: any) => - setPrescriptions(res.data.results) - ); - }, [dispatch, is_prn]); - - useEffect(() => { - fetchPrescriptions(); - }, []); + const { data, refetch } = useQuery(MedicineRoutes.listPrescriptions, { + pathParams: { consultation }, + query: { is_prn, prescription_type, limit: 100 }, + }); return (
    {showDiscontinueFor && ( { setShowDiscontinueFor(undefined); - if (success) fetchPrescriptions(); + if (success) { + refetch(); + } }} key={showDiscontinueFor.id} /> @@ -58,20 +51,20 @@ export default function PrescriptionBuilder({ {showAdministerFor && ( { setShowAdministerFor(undefined); - if (success) fetchPrescriptions(); + if (success) { + refetch(); + } }} key={showAdministerFor.id} /> )}
    - {prescriptions?.map((obj, index) => ( + {data?.results.map((obj, index) => ( setShowDiscontinueFor(obj)} onAdministerClick={() => setShowAdministerFor(obj)} readonly={disabled} @@ -114,10 +107,9 @@ export default function PrescriptionBuilder({ prescription_type, } as Prescription } - create={actions.create} onDone={() => { setShowCreate(false); - fetchPrescriptions(); + refetch(); }} /> diff --git a/src/Components/Medicine/PrescriptionDetailCard.tsx b/src/Components/Medicine/PrescriptionDetailCard.tsx index bf27aa34068..4333eeb1a88 100644 --- a/src/Components/Medicine/PrescriptionDetailCard.tsx +++ b/src/Components/Medicine/PrescriptionDetailCard.tsx @@ -3,7 +3,6 @@ import CareIcon from "../../CAREUI/icons/CareIcon"; import { classNames } from "../../Utils/utils"; import ReadMore from "../Common/components/Readmore"; import ButtonV2 from "../Common/components/ButtonV2"; -import { PrescriptionActions } from "../../Redux/actions"; import { useTranslation } from "react-i18next"; import RecordMeta from "../../CAREUI/display/RecordMeta"; @@ -14,7 +13,6 @@ export default function PrescriptionDetailCard({ prescription: Prescription; readonly?: boolean; children?: React.ReactNode; - actions: ReturnType["prescription"]>; onDiscontinueClick?: () => void; onAdministerClick?: () => void; selected?: boolean; @@ -105,16 +103,17 @@ export default function PrescriptionDetailCard({ {prescription.indicator} {prescription.max_dosage} - {prescription.max_dosage} + {prescription.min_hours_between_doses && + prescription.min_hours_between_doses + " hrs."} ) : ( @@ -149,7 +148,7 @@ export default function PrescriptionDetailCard({
    - + Prescribed {props.children} ) : ( - {t("not_specified")} + + {t("not_specified")} + )}
    diff --git a/src/Components/Medicine/PrescriptionsTable.tsx b/src/Components/Medicine/PrescriptionsTable.tsx index 903f01b32a7..a1b039e71dd 100644 --- a/src/Components/Medicine/PrescriptionsTable.tsx +++ b/src/Components/Medicine/PrescriptionsTable.tsx @@ -1,8 +1,6 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; -import ResponsiveMedicineTable from "../Common/components/ResponsiveMedicineTables"; +import { useState } from "react"; +import ResponsiveMedicineTable from "./ResponsiveMedicineTables"; import { formatDateTime } from "../../Utils/utils"; -import { PrescriptionActions } from "../../Redux/actions"; -import { useDispatch } from "react-redux"; import { Prescription } from "./models"; import CareIcon from "../../CAREUI/icons/CareIcon"; import ButtonV2, { Cancel, Submit } from "../Common/components/ButtonV2"; @@ -14,11 +12,13 @@ import AdministerMedicine from "./AdministerMedicine"; import DialogModal from "../Common/Dialog"; import PrescriptionDetailCard from "./PrescriptionDetailCard"; import { useTranslation } from "react-i18next"; +import useSlug from "../../Common/hooks/useSlug"; +import useQuery from "../../Utils/request/useQuery"; +import MedicineRoutes from "./routes"; interface Props { is_prn?: boolean; prescription_type?: Prescription["prescription_type"]; - consultation_id: string; onChange?: () => void; readonly?: boolean; } @@ -26,35 +26,22 @@ interface Props { export default function PrescriptionsTable({ is_prn = false, prescription_type = "REGULAR", - consultation_id, onChange, readonly, }: Props) { - const dispatch = useDispatch(); + const consultation = useSlug("consultation"); const { t } = useTranslation(); - - const [prescriptions, setPrescriptions] = useState(); const [showBulkAdminister, setShowBulkAdminister] = useState(false); const [showDiscontinueFor, setShowDiscontinueFor] = useState(); const [showAdministerFor, setShowAdministerFor] = useState(); const [detailedViewFor, setDetailedViewFor] = useState(); - const { list, prescription } = useMemo( - () => PrescriptionActions(consultation_id), - [consultation_id] - ); - - const fetchPrescriptions = useCallback(() => { - dispatch(list({ is_prn, prescription_type })).then((res: any) => - setPrescriptions(res.data.results) - ); - }, [consultation_id]); - - useEffect(() => { - fetchPrescriptions(); - }, [consultation_id]); + const { data } = useQuery(MedicineRoutes.listPrescriptions, { + pathParams: { consultation }, + query: { is_prn, prescription_type, limit: 100 }, + }); - const lastModified = prescriptions?.[0]?.modified_date; + const lastModified = data?.results[0]?.modified_date; const tkeys = prescription_type === "REGULAR" ? is_prn @@ -66,7 +53,7 @@ export default function PrescriptionsTable({ return (
    - {prescriptions && ( + {data?.results && ( { setShowBulkAdminister(false); onChange?.(); @@ -86,7 +72,6 @@ export default function PrescriptionsTable({ {showDiscontinueFor && ( { setShowDiscontinueFor(undefined); if (success) onChange?.(); @@ -97,7 +82,6 @@ export default function PrescriptionsTable({ {showAdministerFor && ( { setShowAdministerFor(undefined); if (success) onChange?.(); @@ -115,7 +99,6 @@ export default function PrescriptionsTable({
    @@ -198,7 +181,7 @@ export default function PrescriptionsTable({ maxWidthColumn={0} theads={Object.keys(tkeys).map((_) => t(_))} list={ - prescriptions?.map((obj) => ({ + data?.results.map((obj) => ({ ...obj, medicine: obj.medicine_object?.name ?? obj.medicine_old, route__pretty: @@ -277,7 +260,7 @@ export default function PrescriptionsTable({ : undefined } /> - {prescriptions?.length === 0 && ( + {data?.results.length === 0 && (
    {t("no_data_found")}
    diff --git a/src/Components/Medicine/PrescrpitionTimeline.tsx b/src/Components/Medicine/PrescrpitionTimeline.tsx new file mode 100644 index 00000000000..7c6d4479e3b --- /dev/null +++ b/src/Components/Medicine/PrescrpitionTimeline.tsx @@ -0,0 +1,227 @@ +import dayjs from "../../Utils/dayjs"; +import useSlug from "../../Common/hooks/useSlug"; +import useQuery from "../../Utils/request/useQuery"; +import { classNames, formatDateTime } from "../../Utils/utils"; +import { MedicineAdministrationRecord, Prescription } from "./models"; +import MedicineRoutes from "./routes"; +import Timeline, { + TimelineEvent, + TimelineNode, + TimelineNodeNotes, +} from "../../CAREUI/display/Timeline"; +import ButtonV2 from "../Common/components/ButtonV2"; +import { useState } from "react"; +import ConfirmDialog from "../Common/ConfirmDialog"; +import request from "../../Utils/request/request"; +import RecordMeta from "../../CAREUI/display/RecordMeta"; +import CareIcon from "../../CAREUI/icons/CareIcon"; + +interface MedicineAdministeredEvent extends TimelineEvent<"administered"> { + administration: MedicineAdministrationRecord; +} + +type PrescriptionTimelineEvents = + | TimelineEvent<"created" | "discontinued"> + | MedicineAdministeredEvent; + +interface Props { + interval: { start: Date; end: Date }; + prescription: Prescription; + showPrescriptionDetails?: boolean; +} + +export default function PrescrpitionTimeline({ + prescription, + interval, +}: Props) { + const consultation = useSlug("consultation"); + const { data, refetch, loading } = useQuery( + MedicineRoutes.listAdministrations, + { + pathParams: { consultation }, + query: { + prescription: prescription.id, + administered_date_after: formatDateTime(interval.start, "YYYY-MM-DD"), + administered_date_before: formatDateTime(interval.end, "YYYY-MM-DD"), + }, + } + ); + + const events = data && compileEvents(prescription, data.results, interval); + + if (loading && !data) { + return ( +
    + +
    + ); + } + + return ( + + {events?.map((event, index) => { + switch (event.type) { + case "created": + case "discontinued": + return ( + + ); + + case "administered": + return ( + + ); + } + })} + + ); +} + +const MedicineAdministeredNode = ({ + event, + onArchived, + isLastNode, + hideArchive, +}: { + event: MedicineAdministeredEvent; + onArchived: () => void; + isLastNode: boolean; + hideArchive?: boolean; +}) => { + const consultation = useSlug("consultation"); + const [showArchiveConfirmation, setShowArchiveConfirmation] = useState(false); + const [isArchiving, setIsArchiving] = useState(false); + + return ( + <> + setShowArchiveConfirmation(true)} + > + Archive + + ) + } + isLast={isLastNode} + > + {event.cancelled && ( + + + Prescription was archived{" "} + + + + )} + + { + setIsArchiving(true); + + const { res } = await request(MedicineRoutes.archiveAdministration, { + pathParams: { consultation, external_id: event.administration.id }, + }); + + if (res?.status === 200) { + setIsArchiving(false); + setShowArchiveConfirmation(false); + onArchived(); + } + }} + onClose={() => setShowArchiveConfirmation(false)} + /> + + ); +}; + +const compileEvents = ( + prescription: Prescription, + administrations: MedicineAdministrationRecord[], + interval: { start: Date; end: Date } +): PrescriptionTimelineEvents[] => { + const events: PrescriptionTimelineEvents[] = []; + + if ( + dayjs(prescription.created_date).isBetween(interval.start, interval.end) + ) { + events.push({ + type: "created", + icon: "l-plus-circle", + timestamp: prescription.created_date, + by: prescription.prescribed_by, + notes: prescription.notes, + }); + } + + administrations + .sort( + (a, b) => + new Date(a.created_date).getTime() - new Date(b.created_date).getTime() + ) + .forEach((administration) => { + events.push({ + type: "administered", + icon: "l-syringe", + timestamp: administration.created_date, + by: administration.administered_by, + cancelled: !!administration.archived_on, + administration, + notes: administration.notes, + }); + }); + + if ( + prescription?.discontinued && + dayjs(prescription.discontinued_date).isBetween( + interval.start, + interval.end + ) + ) { + events.push({ + type: "discontinued", + icon: "l-times-circle", + timestamp: prescription.discontinued_date, + by: undefined, + notes: prescription.discontinued_reason, + }); + } + + return events; +}; diff --git a/src/Components/Common/components/ResponsiveMedicineTables.tsx b/src/Components/Medicine/ResponsiveMedicineTables.tsx similarity index 97% rename from src/Components/Common/components/ResponsiveMedicineTables.tsx rename to src/Components/Medicine/ResponsiveMedicineTables.tsx index bbed23e7c07..ccec9a7c69d 100644 --- a/src/Components/Common/components/ResponsiveMedicineTables.tsx +++ b/src/Components/Medicine/ResponsiveMedicineTables.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; -import AccordionV2 from "./AccordionV2"; -import { classNames } from "../../../Utils/utils"; +import AccordionV2 from "../Common/components/AccordionV2"; +import { classNames } from "../../Utils/utils"; function getWindowSize() { const { innerWidth, innerHeight } = window; diff --git a/src/Components/Medicine/models.ts b/src/Components/Medicine/models.ts index 0c49d199b21..ee42b03eca2 100644 --- a/src/Components/Medicine/models.ts +++ b/src/Components/Medicine/models.ts @@ -10,7 +10,7 @@ interface BasePrescription { notes?: string; meta?: object; readonly prescription_type?: "DISCHARGE" | "REGULAR"; - readonly discontinued?: boolean; + readonly discontinued: boolean; discontinued_reason?: string; readonly prescribed_by: PerformedByModel; readonly discontinued_date: string; @@ -50,13 +50,15 @@ export interface PRNPrescription extends BasePrescription { export type Prescription = NormalPrescription | PRNPrescription; export type MedicineAdministrationRecord = { - readonly id?: string; - readonly prescription?: Prescription; + readonly id: string; + readonly prescription: Prescription; notes: string; administered_date?: string; - readonly administered_by?: PerformedByModel; - readonly created_date?: string; - readonly modified_date?: string; + readonly administered_by: PerformedByModel; + readonly archived_by: PerformedByModel | undefined; + readonly archived_on: string | undefined; + readonly created_date: string; + readonly modified_date: string; }; export type MedibaseMedicine = { diff --git a/src/Components/Medicine/routes.ts b/src/Components/Medicine/routes.ts new file mode 100644 index 00000000000..c0c4a602227 --- /dev/null +++ b/src/Components/Medicine/routes.ts @@ -0,0 +1,59 @@ +import { Type } from "../../Redux/api"; +import { PaginatedResponse } from "../../Utils/request/types"; +import { MedicineAdministrationRecord, Prescription } from "./models"; + +const MedicineRoutes = { + listPrescriptions: { + path: "/api/v1/consultation/{consultation}/prescriptions/", + method: "GET", + TRes: Type>(), + }, + + createPrescription: { + path: "/api/v1/consultation/{consultation}/prescriptions/", + method: "POST", + TBody: Type(), + TRes: Type(), + }, + + listAdministrations: { + path: "/api/v1/consultation/{consultation}/prescription_administration/", + method: "GET", + TRes: Type>(), + }, + + getAdministration: { + path: "/api/v1/consultation/{consultation}/prescription_administration/{external_id}/", + method: "GET", + TRes: Type(), + }, + + getPrescription: { + path: "/api/v1/consultation/{consultation}/prescriptions/{external_id}/", + method: "GET", + TRes: Type(), + }, + + administerPrescription: { + path: "/api/v1/consultation/{consultation}/prescriptions/{external_id}/administer/", + method: "POST", + TBody: Type>(), + TRes: Type(), + }, + + discontinuePrescription: { + path: "/api/v1/consultation/{consultation}/prescriptions/{external_id}/discontinue/", + method: "POST", + TBody: Type<{ discontinued_reason: string }>(), + TRes: Type>(), + }, + + archiveAdministration: { + path: "/api/v1/consultation/{consultation}/prescription_administration/{external_id}/archive/", + method: "POST", + TBody: Type>(), + TRes: Type>(), + }, +} as const; + +export default MedicineRoutes; diff --git a/src/Redux/actions.tsx b/src/Redux/actions.tsx index 15934d0957d..b066b98eec5 100644 --- a/src/Redux/actions.tsx +++ b/src/Redux/actions.tsx @@ -1,9 +1,5 @@ import { HCXClaimModel, HCXPolicyModel } from "../Components/HCX/models"; -import { - MedibaseMedicine, - MedicineAdministrationRecord, - Prescription, -} from "../Components/Medicine/models"; +import { MedibaseMedicine } from "../Components/Medicine/models"; import { fireRequest, fireRequestForFiles } from "./fireRequest"; export const getConfig = () => { @@ -858,74 +854,6 @@ export const getAssetAvailability = (id: string) => export const listPMJYPackages = (query?: string) => fireRequest("listPMJYPackages", [], { query }); -/** Prescription related actions */ -export const PrescriptionActions = (consultation_external_id: string) => { - const pathParams = { consultation_external_id }; - - return { - list: (query?: Record) => { - let altKey; - if (query?.is_prn !== undefined) { - altKey = query?.is_prn - ? "listPRNPrescriptions" - : "listNormalPrescriptions"; - } - return fireRequest("listPrescriptions", [], query, pathParams, altKey); - }, - - create: (obj: Prescription) => - fireRequest("createPrescription", [], obj, pathParams), - - listAdministrations: (query?: object) => - fireRequest("listAdministrations", [], query, pathParams), - - getAdministration: (external_id: string) => - fireRequest("getAdministration", [], {}, { ...pathParams, external_id }), - - /** Returns actions specific to a prescription */ - prescription(external_id: string) { - const pathParams = { consultation_external_id, external_id }; - - return { - /** Read a specific prescription of a consultation */ - get: () => fireRequest("getPrescription", [], {}, pathParams), - - /** Administer a prescription */ - administer: (obj: MedicineAdministrationRecord) => - fireRequest( - "administerPrescription", - [], - obj, - pathParams, - `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( - "discontinuePrescription", - [], - { discontinued_reason }, - pathParams, - `discontinue-medicine-${external_id}` - ), - }; - }, - }; -}; - // HCX Actions export const HCXActions = { checkEligibility: (policy: string) => { diff --git a/src/Redux/api.tsx b/src/Redux/api.tsx index efb4f91d4be..43a202e8d04 100644 --- a/src/Redux/api.tsx +++ b/src/Redux/api.tsx @@ -43,9 +43,6 @@ import { ILocalBodyByDistrict, IPartialUpdateExternalResult, } from "../Components/ExternalResult/models"; - -import { Prescription } from "../Components/Medicine/models"; - import { UserModel } from "../Components/Users/models"; import { PaginatedResponse } from "../Utils/request/types"; import { PatientModel } from "../Components/Patient/models"; @@ -55,7 +52,7 @@ import { IComment, IResource } from "../Components/Resource/models"; * A fake function that returns an empty object casted to type T * @returns Empty object as type T */ -function Type(): T { +export function Type(): T { return {} as T; } @@ -1118,47 +1115,6 @@ const routes = { method: "GET", }, - // Prescription endpoints - - listPrescriptions: { - path: "/api/v1/consultation/{consultation_external_id}/prescriptions/", - method: "GET", - }, - - createPrescription: { - path: "/api/v1/consultation/{consultation_external_id}/prescriptions/", - method: "POST", - TBody: Type(), - TRes: Type(), - }, - - listAdministrations: { - path: "/api/v1/consultation/{consultation_external_id}/prescription_administration/", - method: "GET", - }, - - getAdministration: { - path: "/api/v1/consultation/{consultation_external_id}/prescription_administration/{external_id}/", - method: "GET", - }, - - getPrescription: { - path: "/api/v1/consultation/{consultation_external_id}/prescriptions/{external_id}/", - method: "GET", - }, - - administerPrescription: { - path: "/api/v1/consultation/{consultation_external_id}/prescriptions/{external_id}/administer/", - method: "POST", - }, - - discontinuePrescription: { - path: "/api/v1/consultation/{consultation_external_id}/prescriptions/{external_id}/discontinue/", - method: "POST", - TBody: Type<{ discontinued_reason: string }>(), - TRes: Type>(), - }, - // HCX Endpoints listPMJYPackages: { diff --git a/src/Redux/fireRequest.tsx b/src/Redux/fireRequest.tsx index 3d8c677d47d..892e6bd2ee9 100644 --- a/src/Redux/fireRequest.tsx +++ b/src/Redux/fireRequest.tsx @@ -152,7 +152,7 @@ export const fireRequest = ( if (error.response.status > 400 && error.response.status < 500) { if (error.response.data && error.response.data.detail) { if (error.response.data.code === "token_not_valid") { - window.location.href = "/session-expired"; + window.location.href = `/session-expired?redirect=${window.location.href}`; } Notification.Error({ msg: error.response.data.detail, diff --git a/src/Utils/request/handleResponse.ts b/src/Utils/request/handleResponse.ts index 2ecad95ac88..8698919c869 100644 --- a/src/Utils/request/handleResponse.ts +++ b/src/Utils/request/handleResponse.ts @@ -29,7 +29,7 @@ export default function handleResponse( if (res.status >= 400) { // Invalid token if (!silent && error?.code === "token_not_valid") { - navigate("/session-expired"); + navigate(`/session-expired?redirect=${window.location.href}`); } notify?.Error({ msg: error?.detail || "Something went wrong...!" }); diff --git a/src/Utils/request/useQuery.ts b/src/Utils/request/useQuery.ts index 2a8cb2e2ad4..2dab2910278 100644 --- a/src/Utils/request/useQuery.ts +++ b/src/Utils/request/useQuery.ts @@ -6,6 +6,7 @@ import { mergeRequestOptions } from "./utils"; export interface QueryOptions extends RequestOptions { prefetch?: boolean; refetchOnWindowFocus?: boolean; + key?: string; } export default function useQuery( diff --git a/src/Utils/request/utils.ts b/src/Utils/request/utils.ts index f159df1b7a7..ec919c79490 100644 --- a/src/Utils/request/utils.ts +++ b/src/Utils/request/utils.ts @@ -1,4 +1,5 @@ import { LocalStorageKeys } from "../../Common/constants"; +import * as Notification from "../Notifications"; import { QueryParams, RequestOptions } from "./types"; export function makeUrl( @@ -38,6 +39,9 @@ const ensurePathNotMissingReplacements = (path: string) => { const missingParams = path.match(/\{.*\}/g); if (missingParams) { + Notification.Error({ + msg: `Missing path params: ${missingParams.join(", ")}`, + }); throw new Error(`Missing path params: ${missingParams.join(", ")}`); } }; @@ -78,7 +82,7 @@ export function mergeRequestOptions( ...overrides, query: { ...options.query, ...overrides.query }, - body: { ...options.body, ...overrides.body }, + body: { ...(options.body ?? {}), ...(overrides.body ?? {}) }, pathParams: { ...options.pathParams, ...overrides.pathParams }, onResponse: (res) => { diff --git a/src/Utils/utils.ts b/src/Utils/utils.ts index 847304553a8..34d4070dbe9 100644 --- a/src/Utils/utils.ts +++ b/src/Utils/utils.ts @@ -92,6 +92,10 @@ export const relativeDate = (date: DateLike) => { return `${obj.fromNow()} at ${obj.format(TIME_FORMAT)}`; }; +export const formatName = (user: { first_name: string; last_name: string }) => { + return `${user.first_name} ${user.last_name}`; +}; + export const relativeTime = (time?: DateLike) => { return `${dayjs(time).fromNow()}`; }; @@ -107,8 +111,31 @@ export const handleSignOut = (forceReload: boolean) => { Object.values(LocalStorageKeys).forEach((key) => localStorage.removeItem(key) ); + const redirectURL = new URLSearchParams(window.location.search).get( + "redirect" + ); + redirectURL ? navigate(`/?redirect=${redirectURL}`) : navigate("/"); if (forceReload) window.location.href = "/"; - else navigate("/"); +}; + +export const handleRedirection = () => { + const redirectParam = new URLSearchParams(window.location.search).get( + "redirect" + ); + try { + if (redirectParam) { + const redirectURL = new URL(redirectParam); + + if (redirectURL.origin === window.location.origin) { + const newPath = redirectURL.pathname + redirectURL.search; + window.location.href = `${window.location.origin}${newPath}`; + return; + } + } + window.location.href = "/facility"; + } catch { + window.location.href = "/facility"; + } }; /**