From 8752069e403b019851ec745508c5e461da9df49d Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 19 Feb 2024 20:10:53 +0530 Subject: [PATCH] Events UI (#6990) Co-authored-by: Aakash Singh Co-authored-by: Mohammed Nihal <57055998+nihal467@users.noreply.github.com> Co-authored-by: Ashesh <3626859+Ashesh3@users.noreply.github.com> Co-authored-by: Rithvik Nishad --- .../Common/components/SwitchTabs.tsx | 15 +++- .../ConsultationUpdatesTab.tsx | 24 ++++- .../ConsultationDetails/Events/EventsList.tsx | 78 ++++++++++++++++ .../Events/GenericEvent.tsx | 90 +++++++++++++++++++ .../ConsultationDetails/Events/iconMap.ts | 13 +++ .../ConsultationDetails/Events/types.ts | 30 +++++++ .../Consultations/DailyRoundsList.tsx | 6 +- src/Components/HCX/misc.ts | 12 +-- src/Components/Patient/PatientInfoCard.tsx | 2 +- src/Redux/api.tsx | 7 ++ 10 files changed, 259 insertions(+), 18 deletions(-) create mode 100644 src/Components/Facility/ConsultationDetails/Events/EventsList.tsx create mode 100644 src/Components/Facility/ConsultationDetails/Events/GenericEvent.tsx create mode 100644 src/Components/Facility/ConsultationDetails/Events/iconMap.ts create mode 100644 src/Components/Facility/ConsultationDetails/Events/types.ts diff --git a/src/Components/Common/components/SwitchTabs.tsx b/src/Components/Common/components/SwitchTabs.tsx index 047dba53b4b..a7872d5a400 100644 --- a/src/Components/Common/components/SwitchTabs.tsx +++ b/src/Components/Common/components/SwitchTabs.tsx @@ -1,12 +1,21 @@ +import type { ReactNode } from "react"; +import { classNames } from "../../../Utils/utils"; + export default function SwitchTabs(props: { + className?: string; isTab2Active: boolean; onClickTab1: () => void; onClickTab2: () => void; - tab1: string; - tab2: string; + tab1: ReactNode; + tab2: ReactNode; }) { return ( -
+
import("../../Common/PageTitle")); @@ -23,6 +25,7 @@ export const ConsultationUpdatesTab = (props: ConsultationTabProps) => { const [ventilatorSocketUrl, setVentilatorSocketUrl] = useState(); const [monitorBedData, setMonitorBedData] = useState(); const [ventilatorBedData, setVentilatorBedData] = useState(); + const [showEvents, setShowEvents] = useState(false); const vitals = useVitalsAspectRatioConfig({ default: undefined, @@ -665,7 +668,26 @@ export const ConsultationUpdatesTab = (props: ConsultationTabProps) => {
- + + Events + + beta + +
+ } + tab1="Daily Rounds" + onClickTab1={() => setShowEvents(false)} + onClickTab2={() => setShowEvents(true)} + isTab2Active={showEvents} + /> + {showEvents ? ( + + ) : ( + + )}
diff --git a/src/Components/Facility/ConsultationDetails/Events/EventsList.tsx b/src/Components/Facility/ConsultationDetails/Events/EventsList.tsx new file mode 100644 index 00000000000..82a806dfc6b --- /dev/null +++ b/src/Components/Facility/ConsultationDetails/Events/EventsList.tsx @@ -0,0 +1,78 @@ +import { useTranslation } from "react-i18next"; +import { useSlugs } from "../../../../Common/hooks/useSlug"; +import { ConsultationModel } from "../../models"; +import PaginatedList from "../../../../CAREUI/misc/PaginatedList"; +import routes from "../../../../Redux/api"; +import { TimelineNode } from "../../../../CAREUI/display/Timeline"; +import LoadingLogUpdateCard from "../../Consultations/DailyRounds/LoadingCard"; +import GenericEvent from "./GenericEvent"; +import { EventGeneric } from "./types"; +import { getEventIcon } from "./iconMap"; + +interface Props { + consultation: ConsultationModel; +} + +export default function EventsList({ consultation }: Props) { + const [consultationId] = useSlugs("consultation"); + const { t } = useTranslation(); + + + return ( + + {() => ( + <> +
+
+ + + {t("no_consultation_updates")} + + + + + + className="flex grow flex-col gap-3"> + {(item, items) => ( + + text[0].toUpperCase() + text.toLowerCase().slice(1) + ) + .join(" ") + " Event" + } + event={{ + type: item.change_type.replace(/_/g, " ").toLowerCase(), + timestamp: item.created_date?.toString() ?? "", + by: item.caused_by, + icon: getEventIcon(item.event_type.name), + }} + isLast={items.indexOf(item) == items.length - 1} + > + {(() => { + switch (item.event_type.name) { + case "INTERNAL_TRANSFER": + case "CLINICAL": + case "DIAGNOSIS": + case "ENCOUNTER_SUMMARY": + case "HEALTH": + default: + return ; + } + })()} + + )} + +
+ +
+
+
+ + )} +
+ ); +} diff --git a/src/Components/Facility/ConsultationDetails/Events/GenericEvent.tsx b/src/Components/Facility/ConsultationDetails/Events/GenericEvent.tsx new file mode 100644 index 00000000000..4296bd4c34b --- /dev/null +++ b/src/Components/Facility/ConsultationDetails/Events/GenericEvent.tsx @@ -0,0 +1,90 @@ +import type { ReactNode } from "react"; +import { EventGeneric } from "./types"; + +interface IProps { + event: EventGeneric; +} + +/** + * object - array, date + */ + +const formatValue = (value: unknown, key?: string): ReactNode => { + if (value === undefined || value === null) { + return "N/A"; + } + + if (typeof value === "boolean") { + return value ? "Yes" : "No"; + } + + if (typeof value === "number") { + return value; + } + + if (typeof value === "string") { + const trimmed = value.trim(); + + if (trimmed === "") { + return "Empty"; + } + + if (!isNaN(Number(trimmed))) { + return trimmed; + } + + if (new Date(trimmed).toString() !== "Invalid Date") { + return new Date(trimmed).toLocaleString(); + } + + return trimmed; + } + + if (typeof value === "object") { + if (Array.isArray(value)) { + if (value.length === 0) { + return `No ${key?.replace(/_/g, " ")}`; + } + + return value.map((v) => formatValue(v, key)).join(", "); + } + + if (value instanceof Date) { + return value.toLocaleString(); + } + + if (Object.entries(value).length === 0) { + return `No ${key?.replace(/_/g, " ")}`; + } + + return Object.entries(value).map(([key, value]) => ( +
+ + {key.replace(/_/g, " ")} + + + {formatValue(value, key)} + +
+ )); + } + + return JSON.stringify(value); +}; + +export default function GenericEvent({ event }: IProps) { + return ( +
+ {Object.entries(event.value).map(([key, value]) => ( +
+ + {key.replace(/_/g, " ")} + + + {formatValue(value, key)} + +
+ ))} +
+ ); +} diff --git a/src/Components/Facility/ConsultationDetails/Events/iconMap.ts b/src/Components/Facility/ConsultationDetails/Events/iconMap.ts new file mode 100644 index 00000000000..edf097def22 --- /dev/null +++ b/src/Components/Facility/ConsultationDetails/Events/iconMap.ts @@ -0,0 +1,13 @@ +import { IconName } from "../../../../CAREUI/icons/CareIcon"; + +const eventIconMap: Record = { + INTERNAL_TRANSFER: "l-exchange-alt", + CLINICAL: "l-stethoscope", + DIAGNOSIS: "l-tablets", + ENCOUNTER_SUMMARY: "l-file-medical-alt", + HEALTH: "l-heartbeat", +}; + +export const getEventIcon = (eventType: string): IconName => { + return eventIconMap[eventType] || "l-robot"; +}; diff --git a/src/Components/Facility/ConsultationDetails/Events/types.ts b/src/Components/Facility/ConsultationDetails/Events/types.ts new file mode 100644 index 00000000000..f5cf3c9abec --- /dev/null +++ b/src/Components/Facility/ConsultationDetails/Events/types.ts @@ -0,0 +1,30 @@ +import { UserBareMinimum } from "../../../Users/models"; + +export type Type = { + id: number; + parent: number | null; + name: string; + description: string | null; + model: string; + fields: string[]; +}; + +export type CausedBy = UserBareMinimum; + +export type EventGeneric = { + id: string; + event_type: Type; + created_date: string; + object_model: string; + object_id: number; + is_latest: boolean; + meta: { + external_id: string; + }; + value: Record; + change_type: "CREATED" | "UPDATED" | "DELETED"; + consultation: number; + caused_by: UserBareMinimum; +}; + +// TODO: Once event types are finalized, define specific types for each event diff --git a/src/Components/Facility/Consultations/DailyRoundsList.tsx b/src/Components/Facility/Consultations/DailyRoundsList.tsx index 0d569eade41..76d48ec86ba 100644 --- a/src/Components/Facility/Consultations/DailyRoundsList.tsx +++ b/src/Components/Facility/Consultations/DailyRoundsList.tsx @@ -6,7 +6,6 @@ import { useTranslation } from "react-i18next"; import LoadingLogUpdateCard from "./DailyRounds/LoadingCard"; import routes from "../../../Redux/api"; import PaginatedList from "../../../CAREUI/misc/PaginatedList"; -import PageTitle from "../../Common/PageTitle"; import DailyRoundsFilter from "./DailyRoundsFilter"; import { ConsultationModel } from "../models"; import { useSlugs } from "../../../Common/hooks/useSlug"; @@ -34,8 +33,7 @@ export default function DailyRoundsList({ consultation }: Props) { > {() => ( <> -
- +
{ setQuery(query); @@ -43,7 +41,7 @@ export default function DailyRoundsList({ consultation }: Props) { />
-
+
diff --git a/src/Components/HCX/misc.ts b/src/Components/HCX/misc.ts index 9476f19c081..dba0a290b85 100644 --- a/src/Components/HCX/misc.ts +++ b/src/Components/HCX/misc.ts @@ -1,9 +1,3 @@ -export interface PerformedByModel { - id: string; - first_name: string; - last_name: string; - username: string; - email: string; - user_type: string; - last_login: string; -} +import { UserBareMinimum } from "../Users/models"; + +export type PerformedByModel = UserBareMinimum; diff --git a/src/Components/Patient/PatientInfoCard.tsx b/src/Components/Patient/PatientInfoCard.tsx index 25683e1a461..101d11113d1 100644 --- a/src/Components/Patient/PatientInfoCard.tsx +++ b/src/Components/Patient/PatientInfoCard.tsx @@ -531,7 +531,7 @@ export default function PatientInfoCard(props: { title={"Manage Patient"} icon={} className="xl:justify-center" - containerClassName="w-full lg:w-auto mt-2 2xl:mt-0 flex justify-center" + containerClassName="w-full lg:w-auto mt-2 2xl:mt-0 flex justify-center z-20" >
{[ diff --git a/src/Redux/api.tsx b/src/Redux/api.tsx index 8b2a3ae08c2..eb75f23c77c 100644 --- a/src/Redux/api.tsx +++ b/src/Redux/api.tsx @@ -95,6 +95,7 @@ import { } from "../Components/Facility/Investigations"; import { Investigation } from "../Components/Facility/Investigations/Reports/types"; import { ICD11DiagnosisModel } from "../Components/Diagnosis/types"; +import { EventGeneric } from "../Components/Facility/ConsultationDetails/Events/types"; /** * A fake function that returns an empty object casted to type T @@ -554,6 +555,12 @@ const routes = { TRes: Type>(), }, + getEvents: { + path: "/api/v1/consultation/{consultationId}/events/", + method: "GET", + TRes: Type>(), + }, + getDailyReport: { path: "/api/v1/consultation/{consultationId}/daily_rounds/{id}/", method: "GET",