Skip to content

Commit

Permalink
Added Timeline UI for Bed Activity (ohcnetwork#6901)
Browse files Browse the repository at this point in the history
* BedActivityTimeline component added

* In Use indicator added to BedActivityTimeLine

* fixed deepscan failure due to missing key prop

* UI revision | reduced IN USE size | created BedTitleSuffix component

* moved BedActivityTimeline one level up

* Timeline note accepts react node | Bed timeline ui changed

* improved logic for showing asset changes

* moved all input fields to one row

* changed in use badge color

* Introduced i button popover to BedActivityTimeline

* Added Asset diffing function

* minor css change

* added icons for adding and removing asset

* no assets linked text added

* moved input fields back to different lines

* changed in use bed timeline node icon to l-bed

* added missing key props to map objects

* added icon style customizability for Timeline Node

* fixed null value error

* icon color changed for first node

* added divider between form and timeline | moved button to right

* Refactor BedAllocationNode and BedTimelineAsset components

* fixed linting

* fixed linting

* minor fixes based on review

---------

Co-authored-by: rithviknishad <[email protected]>
  • Loading branch information
thedevildude and rithviknishad authored Apr 24, 2024
1 parent 30a97d1 commit 90dc755
Show file tree
Hide file tree
Showing 3 changed files with 287 additions and 61 deletions.
16 changes: 13 additions & 3 deletions src/CAREUI/display/Timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ export interface TimelineEvent<TType = string> {
timestamp: string;
by: PerformedByModel | undefined;
icon: IconName;
notes?: string;
iconStyle?: string;
iconWrapperStyle?: string;
notes?: string | React.ReactNode;
cancelled?: boolean;
}

Expand Down Expand Up @@ -126,9 +128,17 @@ interface TimelineNodeTitleProps {
export const TimelineNodeTitle = (props: TimelineNodeTitleProps) => {
return (
<>
<div className="relative flex h-6 w-6 flex-none items-center justify-center rounded-full bg-gray-200 transition-all duration-200 ease-in-out group-hover:bg-primary-500">
<div
className={classNames(
props.event.iconWrapperStyle,
"relative flex h-6 w-6 flex-none items-center justify-center rounded-full bg-gray-200 transition-all duration-200 ease-in-out group-hover:bg-primary-500",
)}
>
<CareIcon
className="text-base text-gray-700 transition-all duration-200 ease-in-out group-hover:text-white"
className={classNames(
props.event.iconStyle,
"text-base text-gray-700 transition-all duration-200 ease-in-out group-hover:text-white",
)}
aria-hidden="true"
icon={props.event.icon}
/>
Expand Down
260 changes: 260 additions & 0 deletions src/Components/Facility/Consultations/BedActivityTimeline.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
import Chip from "../../../CAREUI/display/Chip";
import Timeline, {
TimelineEvent,
TimelineNode,
TimelineNodeTitle,
} from "../../../CAREUI/display/Timeline";
import CareIcon from "../../../CAREUI/icons/CareIcon";
import { classNames, formatDateTime, relativeTime } from "../../../Utils/utils";
import { AssetData } from "../../Assets/AssetTypes";
import { CurrentBed } from "../models";
import { Popover, Transition } from "@headlessui/react";
import { Fragment } from "react";

interface AssetDiff {
newlyLinkedAssets: AssetData[];
existingAssets: AssetData[];
unlinkedAssets: AssetData[];
}

const getAssetDiff = (a: AssetData[], b: AssetData[]): AssetDiff => {
const newlyLinkedAssets: AssetData[] = [];
const existingAssets: AssetData[] = [];
const unlinkedAssets: AssetData[] = [];

const bMap: Map<string, AssetData> = new Map();
b.forEach((asset) => bMap.set(asset.id, asset));
a.forEach((asset) => {
if (!bMap.has(asset.id)) {
unlinkedAssets.push(asset);
} else {
existingAssets.push(asset);
}
});
b.forEach((asset) => {
if (!a.find((aAsset) => aAsset.id === asset.id)) {
newlyLinkedAssets.push(asset);
}
});

return {
newlyLinkedAssets,
existingAssets,
unlinkedAssets,
};
};

interface Props {
consultationBeds: CurrentBed[];
loading?: boolean;
}

export default function BedActivityTimeline({
consultationBeds,
loading,
}: Props) {
return (
<>
<Timeline
className={classNames(
"py-4 md:px-3",
loading && "animate-pulse opacity-70",
)}
name="bed-allocation"
>
{consultationBeds.map((bed, index) => {
return (
<BedAllocationNode
key={`activity-${bed.id}`}
bed={bed}
prevBed={consultationBeds[index + 1] ?? undefined}
isLastNode={index === consultationBeds.length - 1}
/>
);
})}
</Timeline>
</>
);
}

const BedAllocationNode = ({
bed,
prevBed,
isLastNode,
}: {
bed: CurrentBed;
prevBed?: CurrentBed;
isLastNode: boolean;
}) => {
const { newlyLinkedAssets, existingAssets, unlinkedAssets } = getAssetDiff(
prevBed?.assets_objects ?? [],
bed.assets_objects ?? [],
);
const event: TimelineEvent = {
type: "allocated",
timestamp: bed.start_date,
by: undefined,
icon: "l-bed",
iconWrapperStyle: bed.end_date === null ? "bg-green-500" : "",
iconStyle: bed.end_date === null ? "text-white" : "",
notes:
newlyLinkedAssets.length === 0 &&
existingAssets.length === 0 &&
unlinkedAssets.length === 0 ? (
""
) : (
<BedTimelineAsset
newlyLinkedAssets={newlyLinkedAssets}
existingAssets={existingAssets}
unlinkedAssets={unlinkedAssets}
/>
),
};

return (
<>
<TimelineNode
name="bed"
event={event}
title={
<BedTimelineNodeTitle
event={event}
titleSuffix={<BedTitleSuffix bed={bed} prevBed={prevBed} />}
bed={bed}
/>
}
isLast={isLastNode}
/>
</>
);
};

const BedTimelineAsset = ({
newlyLinkedAssets,
existingAssets,
unlinkedAssets,
}: {
newlyLinkedAssets: AssetData[];
existingAssets: AssetData[];
unlinkedAssets: AssetData[];
}) => {
return (
<div className="flex flex-col gap-1">
<p className="text-md font-semibold">Assets</p>
{newlyLinkedAssets.length === 0 &&
existingAssets.length === 0 &&
unlinkedAssets.length === 0 && (
<p className="text-gray-500">No assets linked</p>
)}
{newlyLinkedAssets.length > 0 &&
newlyLinkedAssets.map((newAsset) => (
<div key={newAsset.id} className="flex gap-1 text-primary">
<CareIcon icon="l-plus-circle" />
<span>{newAsset.name}</span>
</div>
))}
{existingAssets.length > 0 &&
existingAssets.map((existingAsset) => (
<div key={existingAsset.id} className="flex gap-1">
<CareIcon icon="l-check-circle" />
<span>{existingAsset.name}</span>
</div>
))}
{unlinkedAssets.length > 0 &&
unlinkedAssets.map((unlinkedAsset) => (
<div key={unlinkedAsset.id} className="flex gap-1 text-gray-500">
<CareIcon icon="l-minus-circle" />
<span className="line-through">{unlinkedAsset.name}</span>
</div>
))}
</div>
);
};

const BedTimelineNodeTitle = (props: {
event: TimelineEvent;
titleSuffix: React.ReactNode;
bed: CurrentBed;
}) => {
const { event, titleSuffix, bed } = props;

return (
<TimelineNodeTitle event={event}>
<div className="flex w-full justify-between gap-2">
<p className="flex-auto py-0.5 text-xs leading-5 text-gray-600 md:w-2/3">
{titleSuffix}
</p>
<div className="md:w-fit">
<BedActivityIButtonPopover bed={bed} />
</div>
</div>
</TimelineNodeTitle>
);
};

const BedTitleSuffix = ({
bed,
prevBed,
}: {
bed: CurrentBed;
isLastNode?: boolean;
prevBed?: CurrentBed;
}) => {
return (
<div className="flex flex-col">
<div className="flex gap-x-2">
<span>{formatDateTime(bed.start_date).split(";")[0]}</span>
<span className="text-gray-500">-</span>
<span>{formatDateTime(bed.start_date).split(";")[1]}</span>
</div>
<p>
{bed.bed_object.id === prevBed?.bed_object.id
? "Asset changed in" + " "
: "Transferred to" + " "}
<span className="font-semibold">{`${bed.bed_object.name} (${bed.bed_object.bed_type}) in ${bed.bed_object.location_object?.name}`}</span>
{bed.end_date === null && (
<Chip
text="In Use"
startIcon="l-notes"
size="small"
variant="primary"
className="ml-5"
/>
)}
</p>
</div>
);
};

const BedActivityIButtonPopover = ({
bed,
}: {
event?: TimelineEvent;
bed?: CurrentBed;
}) => {
return (
<Popover className="relative text-sm text-gray-500 md:text-base">
<Popover.Button>
<CareIcon
icon="l-info-circle"
className="cursor-pointer text-gray-500 hover:text-gray-600"
/>
</Popover.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute z-10 -ml-20 mt-2 w-48 -translate-x-1/2 rounded-lg border border-gray-200 bg-gray-100 p-2 shadow">
<p className="text-xs text-gray-600">
updated {relativeTime(bed?.start_date)}
</p>
</Popover.Panel>
</Transition>
</Popover>
);
};
Loading

0 comments on commit 90dc755

Please sign in to comment.