Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Events UI #6990

Merged
merged 12 commits into from
Feb 19, 2024
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">
in beta
sainak marked this conversation as resolved.
Show resolved Hide resolved
</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
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) {

Check failure on line 16 in src/Components/Facility/ConsultationDetails/Events/EventsList.tsx

View workflow job for this annotation

GitHub Actions / lint

'consultation' is defined but never used. Allowed unused args must match /^_/u
const [consultationId] = useSlugs("consultation");
const { t } = useTranslation();


Check failure on line 20 in src/Components/Facility/ConsultationDetails/Events/EventsList.tsx

View workflow job for this annotation

GitHub Actions / lint

Delete `⏎`
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} />;
Ashesh3 marked this conversation as resolved.
Show resolved Hide resolved
}
})()}
</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 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>
))}
</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
Loading