Skip to content

Commit

Permalink
Events UI (ohcnetwork#6990)
Browse files Browse the repository at this point in the history
Co-authored-by: Aakash Singh <[email protected]>
Co-authored-by: Mohammed Nihal <[email protected]>
Co-authored-by: Ashesh <[email protected]>
Co-authored-by: Rithvik Nishad <[email protected]>
  • Loading branch information
5 people authored Feb 19, 2024
1 parent dacc7f8 commit 8752069
Show file tree
Hide file tree
Showing 10 changed files with 259 additions and 18 deletions.
15 changes: 12 additions & 3 deletions src/Components/Common/components/SwitchTabs.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="relative grid w-full grid-cols-2 items-center gap-4 rounded-md bg-primary-500/10 px-4 py-3 lg:w-52">
<div
className={classNames(
"relative grid w-full grid-cols-2 items-center gap-4 rounded-md bg-primary-500/10 px-4 py-3 lg:w-52",
props.className
)}
>
<div
className={`absolute z-0 w-[50%] origin-left rounded bg-primary-500 py-4 transition-all duration-200 ease-out lg:left-1.5 ${
props.isTab2Active ? "right-1.5 lg:translate-x-[89%]" : "left-1.5"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import Chip from "../../../CAREUI/display/Chip";
import { formatAge, formatDate, formatDateTime } from "../../../Utils/utils";
import ReadMore from "../../Common/components/Readmore";
import DailyRoundsList from "../Consultations/DailyRoundsList";
import EventsList from "./Events/EventsList";
import SwitchTabs from "../../Common/components/SwitchTabs";
import { getVitalsMonitorSocketUrl } from "../../VitalsMonitor/utils";

const PageTitle = lazy(() => import("../../Common/PageTitle"));
Expand All @@ -23,6 +25,7 @@ export const ConsultationUpdatesTab = (props: ConsultationTabProps) => {
const [ventilatorSocketUrl, setVentilatorSocketUrl] = useState<string>();
const [monitorBedData, setMonitorBedData] = useState<AssetBedModel>();
const [ventilatorBedData, setVentilatorBedData] = useState<AssetBedModel>();
const [showEvents, setShowEvents] = useState<boolean>(false);

const vitals = useVitalsAspectRatioConfig({
default: undefined,
Expand Down Expand Up @@ -665,7 +668,26 @@ export const ConsultationUpdatesTab = (props: ConsultationTabProps) => {
</div>
</div>
<div className="w-full pl-0 md:pl-4 xl:w-1/3">
<DailyRoundsList consultation={props.consultationData} />
<SwitchTabs
className="mt-3 w-full lg:w-full"
tab2={
<div className="flex items-center justify-center gap-1 text-sm">
Events
<span className="rounded-lg bg-warning-400 p-[1px] px-1 text-[10px] text-white">
beta
</span>
</div>
}
tab1="Daily Rounds"
onClickTab1={() => setShowEvents(false)}
onClickTab2={() => setShowEvents(true)}
isTab2Active={showEvents}
/>
{showEvents ? (
<EventsList consultation={props.consultationData} />
) : (
<DailyRoundsList consultation={props.consultationData} />
)}
</div>
</div>
</div>
Expand Down
78 changes: 78 additions & 0 deletions src/Components/Facility/ConsultationDetails/Events/EventsList.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<PaginatedList route={routes.getEvents} pathParams={{ consultationId }}>
{() => (
<>
<div className="mt-4 flex w-full flex-col gap-4">
<div className="flex max-h-[85vh] flex-col gap-4 overflow-y-auto overflow-x-hidden px-3">
<PaginatedList.WhenEmpty className="flex w-full justify-center border-b border-gray-200 bg-white p-5 text-center text-2xl font-bold text-gray-500">
<span className="flex justify-center rounded-lg bg-white p-3 text-gray-700 ">
{t("no_consultation_updates")}
</span>
</PaginatedList.WhenEmpty>
<PaginatedList.WhenLoading>
<LoadingLogUpdateCard />
</PaginatedList.WhenLoading>
<PaginatedList.Items<EventGeneric> className="flex grow flex-col gap-3">
{(item, items) => (
<TimelineNode
name={
item.event_type.name
.split("_")
.map(
(text) =>
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 <GenericEvent event={item} />;
}
})()}
</TimelineNode>
)}
</PaginatedList.Items>
<div className="flex w-full items-center justify-center">
<PaginatedList.Paginator hideIfSinglePage />
</div>
</div>
</div>
</>
)}
</PaginatedList>
);
}
Original file line number Diff line number Diff line change
@@ -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]) => (
<div className="flex flex-col items-center gap-2 md:flex-row">
<span className="text-xs uppercase text-gray-700">
{key.replace(/_/g, " ")}
</span>
<span className="text-sm font-semibold text-gray-700">
{formatValue(value, key)}
</span>
</div>
));
}

return JSON.stringify(value);
};

export default function GenericEvent({ event }: IProps) {
return (
<div className="flex w-full flex-col gap-4 rounded-lg border border-gray-400 p-4 @container">
{Object.entries(event.value).map(([key, value]) => (
<div className="flex w-full flex-col items-center gap-2 md:flex-row">
<span className="text-xs uppercase text-gray-700">
{key.replace(/_/g, " ")}
</span>
<span className="break-all text-sm font-semibold text-gray-700">
{formatValue(value, key)}
</span>
</div>
))}
</div>
);
}
13 changes: 13 additions & 0 deletions src/Components/Facility/ConsultationDetails/Events/iconMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { IconName } from "../../../../CAREUI/icons/CareIcon";

const eventIconMap: Record<string, IconName> = {
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";
};
30 changes: 30 additions & 0 deletions src/Components/Facility/ConsultationDetails/Events/types.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>;
change_type: "CREATED" | "UPDATED" | "DELETED";
consultation: number;
caused_by: UserBareMinimum;
};

// TODO: Once event types are finalized, define specific types for each event
6 changes: 2 additions & 4 deletions src/Components/Facility/Consultations/DailyRoundsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -34,16 +33,15 @@ export default function DailyRoundsList({ consultation }: Props) {
>
{() => (
<>
<div className="flex flex-1 justify-between">
<PageTitle title="Update Log" hideBack breadcrumbs={false} />
<div className="m-1 flex flex-1 justify-end">
<DailyRoundsFilter
onApply={(query) => {
setQuery(query);
}}
/>
</div>

<div className="-mt-2 flex w-full flex-col gap-4">
<div className="flex w-full flex-col gap-4">
<div className="flex max-h-[85vh] flex-col gap-4 overflow-y-auto overflow-x-hidden px-3">
<PaginatedList.WhenEmpty className="flex w-full justify-center border-b border-gray-200 bg-white p-5 text-center text-2xl font-bold text-gray-500">
<span className="flex justify-center rounded-lg bg-white p-3 text-gray-700 ">
Expand Down
12 changes: 3 additions & 9 deletions src/Components/HCX/misc.ts
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 1 addition & 1 deletion src/Components/Patient/PatientInfoCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -531,7 +531,7 @@ export default function PatientInfoCard(props: {
title={"Manage Patient"}
icon={<CareIcon icon="l-setting" className="text-xl" />}
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"
>
<div>
{[
Expand Down
7 changes: 7 additions & 0 deletions src/Redux/api.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -554,6 +555,12 @@ const routes = {
TRes: Type<PaginatedResponse<DailyRoundsModel>>(),
},

getEvents: {
path: "/api/v1/consultation/{consultationId}/events/",
method: "GET",
TRes: Type<PaginatedResponse<EventGeneric>>(),
},

getDailyReport: {
path: "/api/v1/consultation/{consultationId}/daily_rounds/{id}/",
method: "GET",
Expand Down

0 comments on commit 8752069

Please sign in to comment.