From ab9aa6e351e7fa3d37016299dabc4ce5ef1330e1 Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Thu, 26 Oct 2023 16:52:17 +0530 Subject: [PATCH] support for archive and fix responsiveness issues --- src/CAREUI/display/RecordMeta.tsx | 13 +- src/CAREUI/display/Timeline.tsx | 157 ++++++++++ .../AdministrationEventCell.tsx | 291 +++++++++++------- .../AdministrationTable.tsx | 154 ++++----- .../AdministrationTableRow.tsx | 2 +- .../MedicineAdministrationSheet/index.tsx | 4 +- .../Medicine/PrescrpitionTimeline.tsx | 223 ++++++++++++++ src/Components/Medicine/models.ts | 12 +- src/Components/Medicine/routes.ts | 9 +- src/Utils/utils.ts | 4 + 10 files changed, 666 insertions(+), 203 deletions(-) create mode 100644 src/CAREUI/display/Timeline.tsx create mode 100644 src/Components/Medicine/PrescrpitionTimeline.tsx 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..b3445f4fd97 --- /dev/null +++ b/src/CAREUI/display/Timeline.tsx @@ -0,0 +1,157 @@ +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/Components/Medicine/MedicineAdministrationSheet/AdministrationEventCell.tsx b/src/Components/Medicine/MedicineAdministrationSheet/AdministrationEventCell.tsx index e7b6507459a..b8a4addfb03 100644 --- a/src/Components/Medicine/MedicineAdministrationSheet/AdministrationEventCell.tsx +++ b/src/Components/Medicine/MedicineAdministrationSheet/AdministrationEventCell.tsx @@ -1,9 +1,11 @@ -import { Disclosure, Popover, Transition } from "@headlessui/react"; import dayjs from "../../../Utils/dayjs"; import { MedicineAdministrationRecord, Prescription } from "../models"; import CareIcon from "../../../CAREUI/icons/CareIcon"; -import { Fragment } from "react"; import { classNames, formatDateTime, formatTime } from "../../../Utils/utils"; +import DialogModal from "../../Common/Dialog"; +import PrescrpitionActivityTimeline from "../PrescrpitionTimeline"; +import { useState } from "react"; +import PrescriptionDetailCard from "../PrescriptionDetailCard"; interface Props { administrations: MedicineAdministrationRecord[]; @@ -16,6 +18,7 @@ export default function AdministrationEventCell({ interval: { start, end }, prescription, }: Props) { + const [showTimeline, setShowTimeline] = useState(false); // Check if cell belongs to an administered prescription const administered = administrations .filter((administration) => @@ -31,120 +34,182 @@ export default function AdministrationEventCell({ if (administered.length) { return ( - - - {({ open }) => ( -
    -
    - - {administered.length > 1 && ( - - {administered.length} - - )} -
    - {hasComment && ( - - )} - {!open && ( - - {administered.length === 1 ? ( -

    - Administered on{" "} - - {formatTime(administered[0].administered_date)} - -

    - ) : ( -

    - {administered.length} administrations -

    - )} -

    Click to view details

    -
    + <> + setShowTimeline(false)} + title={ + + } + className="w-full md:max-w-2xl" + show={showTimeline} + > +
    + Administrations between{" "} + {formatTime(start, "HH:mm")} and{" "} + {formatTime(end, "HH:mm")} on{" "} + + {formatDateTime(start, "DD/MM/YYYY")} + +
    + +
    +
    +
    )} - - - - -
    -
    - {administered.map((administration) => ( -
    - - {({ open }) => ( - <> - - - Administered on{" "} - - {formatTime(administration.administered_date)} - - - {administration.notes && ( - - )} - - - -
    - Administered by:{" "} - - {administration.administered_by?.first_name}{" "} - {administration.administered_by?.last_name} - -
    -
    - Notes:{" "} - - {administration.notes || ( - - No notes - - )} - -
    -
    - - )} -
    -
    - ))} -
    -
    -
    -
    - + + ); + //// TODO: use the below inline timeline popover when inline popover overflow clipping issue is solved when. + // return ( + // + // + // {({ open }) => ( + //
    + //
    + // + // {administered.length > 1 && ( + // + // {administered.length} + // + // )} + //
    + // {hasComment && ( + // + // )} + // {!open && ( + // + // {administered.length === 1 ? ( + //

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

    + // ) : ( + //

    + // {administered.length} administrations + //

    + // )} + //

    Click to view details

    + //
    + // )} + //
    + // )} + //
    + // + // + //
    + //
    + // {administered.map((administration) => ( + //
    + // + // {({ open }) => ( + // <> + // + // + // Administered on{" "} + // + // {formatTime(administration.administered_date)} + // + // + // {administration.notes && ( + // + // )} + // + // + // + //
    + // Administered by:{" "} + // + // {administration.administered_by?.first_name}{" "} + // {administration.administered_by?.last_name} + // + //
    + //
    + // Notes:{" "} + // + // {administration.notes || ( + // + // No notes + // + // )} + // + //
    + //
    + // + // )} + //
    + //
    + // ))} + //
    + //
    + //
    + //
    + //
    + // ); } // Check if cell belongs to after prescription.created_date diff --git a/src/Components/Medicine/MedicineAdministrationSheet/AdministrationTable.tsx b/src/Components/Medicine/MedicineAdministrationSheet/AdministrationTable.tsx index f4f3a12db09..9de207146de 100644 --- a/src/Components/Medicine/MedicineAdministrationSheet/AdministrationTable.tsx +++ b/src/Components/Medicine/MedicineAdministrationSheet/AdministrationTable.tsx @@ -20,85 +20,87 @@ export default function MedicineAdministrationTable({ const { t } = useTranslation(); return ( - - - - +
    +
    -
    - {t("medicine")} - -

    Dosage &

    -

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

    -
    -
    -
    + + + - - {pagination.slots?.map(({ start }, index) => ( - <> - - + + + + {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" - )} - - - ))} - - - - - + {formatDateTime( + start, + start.getHours() === 0 ? "DD/MM" : "h a" + )} + + + ))} + + + + +
    + + {prescriptions.map((obj) => ( + + ))} + + + ); } diff --git a/src/Components/Medicine/MedicineAdministrationSheet/AdministrationTableRow.tsx b/src/Components/Medicine/MedicineAdministrationSheet/AdministrationTableRow.tsx index cb36bd829e8..30ab68dcf0b 100644 --- a/src/Components/Medicine/MedicineAdministrationSheet/AdministrationTableRow.tsx +++ b/src/Components/Medicine/MedicineAdministrationSheet/AdministrationTableRow.tsx @@ -37,7 +37,6 @@ export default function MedicineAdministrationTableRow({ pathParams: { consultation }, query: { prescription: prescription.id, - administered_date_after: formatDateTime( props.intervals[0].start, "YYYY-MM-DD" @@ -46,6 +45,7 @@ export default function MedicineAdministrationTableRow({ props.intervals[props.intervals.length - 1].end, "YYYY-MM-DD" ), + archived: false, }, key: `${prescription.last_administered_on}`, }); diff --git a/src/Components/Medicine/MedicineAdministrationSheet/index.tsx b/src/Components/Medicine/MedicineAdministrationSheet/index.tsx index aba353c344d..65fb59799a1 100644 --- a/src/Components/Medicine/MedicineAdministrationSheet/index.tsx +++ b/src/Components/Medicine/MedicineAdministrationSheet/index.tsx @@ -123,10 +123,10 @@ const MedicineAdministrationSheet = ({ readonly, is_prn }: Props) => { className="group sticky left-0 w-full rounded-b-lg rounded-t-none bg-gray-100" onClick={() => setShowDiscontinued(true)} > - + - Show {discontinuedCount} other discontinued + Show {discontinuedCount} discontinued prescription(s) diff --git a/src/Components/Medicine/PrescrpitionTimeline.tsx b/src/Components/Medicine/PrescrpitionTimeline.tsx new file mode 100644 index 00000000000..31e7f4f47c1 --- /dev/null +++ b/src/Components/Medicine/PrescrpitionTimeline.tsx @@ -0,0 +1,223 @@ +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, +}: { + event: MedicineAdministeredEvent; + onArchived: () => void; + isLastNode: boolean; +}) => { + const consultation = useSlug("consultation"); + const [showArchiveConfirmation, setShowArchiveConfirmation] = useState(false); + const [isArchiving, setIsArchiving] = useState(false); + + return ( + <> + { + 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)} + /> + setShowArchiveConfirmation(true)} + > + Archive + + ) + } + isLast={isLastNode} + > + {event.cancelled && ( + + + Prescription was archived{" "} + + + + )} + + + ); +}; + +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/Medicine/models.ts b/src/Components/Medicine/models.ts index e73c686d83b..ee42b03eca2 100644 --- a/src/Components/Medicine/models.ts +++ b/src/Components/Medicine/models.ts @@ -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 index 580bdd3ec38..c0c4a602227 100644 --- a/src/Components/Medicine/routes.ts +++ b/src/Components/Medicine/routes.ts @@ -37,7 +37,7 @@ const MedicineRoutes = { administerPrescription: { path: "/api/v1/consultation/{consultation}/prescriptions/{external_id}/administer/", method: "POST", - TBody: Type(), + TBody: Type>(), TRes: Type(), }, @@ -47,6 +47,13 @@ const MedicineRoutes = { 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/Utils/utils.ts b/src/Utils/utils.ts index 3aef5b03d67..5f81a129dfc 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()}`; };