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 (
+
+ );
+}
+
+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 (
+
+ );
+};
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"}
-
-
- |
+
+
+
+
+
+
+ {t("medicine")}
+
+ Dosage &
+ {!prescriptions[0]?.is_prn ? "Frequency" : "Indicator"}
+
+
+ |
-
-
-
-
- |
- {pagination.slots?.map(({ start }, index) => (
- <>
-
+
- {formatDateTime(
- start,
- start.getHours() === 0 ? "DD/MM" : "h a"
- )}
- |
- |
- >
- ))}
-
-
-
-
- |
+
+
+
+ {pagination.slots?.map(({ start }, index) => (
+ <>
+
+ {formatDateTime(
+ start,
+ start.getHours() === 0 ? "DD/MM" : "h a"
+ )}
+ |
+ |
+ >
+ ))}
+
+
+
+
+ |
- |
-
-
+ |
+
+
-
- {prescriptions.map((obj) => (
-
- ))}
-
-
+
+ {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()}`;
};