-
+ |
{dayjs(service.serviced_on).format("DD MMM YYYY")}
@@ -229,8 +205,9 @@ const AssetManage = (props: AssetManageProps) => {
)}
|
-
+ |
{
setServiceEditData({ ...service, open: true });
@@ -240,6 +217,7 @@ const AssetManage = (props: AssetManageProps) => {
{
};
useEffect(() => {
- populateTableRows(transactions);
+ if (transactions) populateTableRows(transactions.results);
}, [transactions]);
useEffect(() => {
- populateServiceTableRows(services);
+ if (services) populateServiceTableRows(services?.results);
}, [services]);
- if (isLoading) return ;
+ if (loading) return ;
if (isPrintMode) return ;
const assetClassProp =
@@ -296,7 +274,10 @@ const AssetManage = (props: AssetManageProps) => {
{item.label}
-
@@ -318,29 +299,43 @@ const AssetManage = (props: AssetManageProps) => {
const handleDelete = async () => {
if (asset) {
- const response = await dispatch(deleteAsset(asset.id));
- if (response && response.status === 204) {
- Notification.Success({
- msg: "Asset deleted successfully",
- });
- navigate("/assets");
- }
+ await request(routes.deleteAsset, {
+ pathParams: {
+ external_id: asset.id,
+ },
+ onResponse: () => {
+ Notification.Success({
+ msg: "Asset deleted successfully",
+ });
+ navigate("/assets");
+ },
+ });
}
};
return (
-
-
+
+
+ Export as JSON
+
+ }
+ >
{
{asset?.name}
-
-
-
- Export as JSON
-
-
+
+
+ {assetClassProp.name}
+
+
+
+ {asset?.description}
+ {asset?.asset_type === "INTERNAL" ? (
+
+ ) : (
+
+ )}
{asset?.status === "ACTIVE" ? (
) : (
@@ -391,9 +388,14 @@ const AssetManage = (props: AssetManageProps) => {
startIcon="l-times"
/>
)}
+ {warrantyAmcValidityChip(
+ asset?.warranty_amc_end_of_validity as string
+ )}
- {asset?.description}
+
+ {asset?.description}
+
{[
@@ -402,19 +404,6 @@ const AssetManage = (props: AssetManageProps) => {
icon: "location-pin-alt",
content: asset?.location_object.name,
},
- {
- label: "Asset Type",
- icon: "apps",
- content:
- asset?.asset_type === "INTERNAL"
- ? "Internal Asset"
- : "External Asset",
- },
- {
- label: "Asset Class",
- icon: assetClassProp.icon,
- content: assetClassProp.name,
- },
{
label: "Asset QR Code ID",
icon: "qrcode-scan",
@@ -443,20 +432,24 @@ const AssetManage = (props: AssetManageProps) => {
{t("update")}
- {asset?.asset_class && (
-
- navigate(
- `/facility/${asset?.location_object.facility.id}/assets/${asset?.id}/configure`
- )
- }
- id="configure-asset"
- authorizeFor={NonReadOnlyUsers}
- >
-
- {t("configure")}
-
- )}
+ {asset?.asset_class &&
+ (asset?.asset_class !== "ONVIF" ||
+ ["DistrictAdmin", "StateAdmin", "Doctor"].includes(
+ authUser.user_type
+ )) && (
+
+ navigate(
+ `/facility/${asset?.location_object.facility.id}/assets/${asset?.id}/configure`
+ )
+ }
+ id="configure-asset"
+ data-testid="asset-configure-button"
+ >
+
+ {t("configure")}
+
+ )}
{checkAuthority(authUser.user_type, "DistrictAdmin") && (
{
asset?.asset_class &&
asset?.asset_class != AssetClass.NONE && }
Service History
-
+
-
+ |
Serviced on
|
@@ -528,7 +524,7 @@ const AssetManage = (props: AssetManageProps) => {
|
Last Updated
|
-
+ |
Edit
|
@@ -539,20 +535,23 @@ const AssetManage = (props: AssetManageProps) => {
Transaction History
-
+
Moved from
|
-
+ |
Moved to
|
-
+ |
Moved By
|
-
+ |
Moved On
|
@@ -579,12 +578,12 @@ const AssetManage = (props: AssetManageProps) => {
handleClose={() =>
setServiceEditData({ ...serviceEditData, open: false })
}
- handleUpdate={() => fetchData({ aborted: false })}
+ handleUpdate={() => serviceRefetch()}
show={serviceEditData.open}
viewOnly={serviceEditData.viewOnly}
/>
)}
-
+
);
};
diff --git a/src/Components/Assets/AssetServiceEditModal.tsx b/src/Components/Assets/AssetServiceEditModal.tsx
index 75c9dd9fc60..3964dfe0770 100644
--- a/src/Components/Assets/AssetServiceEditModal.tsx
+++ b/src/Components/Assets/AssetServiceEditModal.tsx
@@ -1,16 +1,16 @@
import { useEffect, useState } from "react";
-import { useDispatch } from "react-redux";
-import { updateAssetService } from "../../Redux/actions";
import * as Notification from "../../Utils/Notifications.js";
import ButtonV2, { Cancel, Submit } from "../Common/components/ButtonV2";
import DialogModal from "../Common/Dialog";
import { AssetData, AssetService, AssetServiceEdit } from "./AssetTypes";
import dayjs from "dayjs";
import TextAreaFormField from "../Form/FormFields/TextAreaFormField";
-import DateInputV2 from "../Common/DateInputV2";
-import { FieldLabel } from "../Form/FormFields/FormField";
import { formatDate, formatDateTime } from "../../Utils/utils";
import CareIcon from "../../CAREUI/icons/CareIcon";
+import request from "../../Utils/request/request";
+import routes from "../../Redux/api";
+import DateFormField from "../Form/FormFields/DateFormField";
+import { t } from "i18next";
export const AssetServiceEditModal = (props: {
asset?: AssetData;
@@ -24,23 +24,25 @@ export const AssetServiceEditModal = (props: {
serviced_on: props.service_record?.serviced_on,
note: props.service_record?.note,
});
- const dispatchAction: any = useDispatch();
const [isLoading, setIsLoading] = useState(false);
const [editRecord, setEditRecord] = useState();
const handleSubmit = async (e: any) => {
e.preventDefault();
setIsLoading(true);
- const data = {
+ const body = {
serviced_on: form.serviced_on,
note: form.note,
};
-
- const res = await dispatchAction(
- updateAssetService(props.asset?.id ?? "", props.service_record.id, data)
- );
+ const { data } = await request(routes.updateAssetService, {
+ pathParams: {
+ asset_external_id: props.asset?.id ?? "",
+ external_id: props.service_record.id,
+ },
+ body: body,
+ });
setIsLoading(false);
- if (res?.data) {
+ if (data) {
Notification.Success({
msg: "Asset service record updated successfully",
});
@@ -61,12 +63,12 @@ export const AssetServiceEditModal = (props: {
- Update record for asset
+ {t("update_record_for_asset")}
{props.asset?.name}
@@ -97,7 +99,10 @@ export const AssetServiceEditModal = (props: {
{edit.edited_by.username}
-
@@ -107,13 +112,17 @@ export const AssetServiceEditModal = (props: {
- Edited On
+
+ {t("edited_on")}
+
{formatDateTime(editRecord.edited_on)}
- Edited By
+
+ {t("edited_by")}
+
{editRecord.edited_by.username}
@@ -122,27 +131,35 @@ export const AssetServiceEditModal = (props: {
- Serviced On
+ {t("serviced_on")}
-
+
{formatDate(editRecord.serviced_on)}
- Notes
- {editRecord.note || "-"}
+
+ {t("notes")}
+
+
+ {editRecord.note || "-"}
+
)}
{
editRecord ? setEditRecord(undefined) : props.handleClose();
}}
>
- {editRecord ? "Back" : "Close"}
+ {editRecord ? t("back") : t("close")}
@@ -154,12 +171,12 @@ export const AssetServiceEditModal = (props: {
- Update record for asset
+ {t("update_record_for_asset")}
{props.asset?.name}
@@ -169,19 +186,31 @@ export const AssetServiceEditModal = (props: {
className="col-span-6 sm:col-span-3"
data-testid="asset-last-serviced-on-input"
>
- Serviced On
- {
- setForm({
- ...form,
- serviced_on: dayjs(date).format("YYYY-MM-DD"),
- });
+ if (
+ dayjs(date.value).format("YYYY-MM-DD") >
+ new Date(
+ props.service_record.created_date
+ ).toLocaleDateString("en-ca")
+ ) {
+ Notification.Error({
+ msg: `Service date can't be after ${formatDate(
+ props.service_record.created_date
+ )} (Creation date)`,
+ });
+ } else {
+ setForm({
+ ...form,
+ serviced_on: dayjs(date.value).format("YYYY-MM-DD"),
+ });
+ }
}}
- max={new Date(props.service_record.created_date)}
/>
@@ -189,8 +218,8 @@ export const AssetServiceEditModal = (props: {
{
setForm({ ...form, note: e.value });
@@ -201,7 +230,7 @@ export const AssetServiceEditModal = (props: {
diff --git a/src/Components/Assets/AssetType/HL7Monitor.tsx b/src/Components/Assets/AssetType/HL7Monitor.tsx
index b19190ed410..b4fefbc90b2 100644
--- a/src/Components/Assets/AssetType/HL7Monitor.tsx
+++ b/src/Components/Assets/AssetType/HL7Monitor.tsx
@@ -1,10 +1,5 @@
import { SyntheticEvent, useEffect, useState } from "react";
import { AssetData } from "../AssetTypes";
-import { useDispatch } from "react-redux";
-import {
- partialUpdateAsset,
- getPermittedFacility,
-} from "../../../Redux/actions";
import * as Notification from "../../../Utils/Notifications.js";
import MonitorConfigure from "../configure/MonitorConfigure";
import Loading from "../../Common/Loading";
@@ -15,6 +10,10 @@ import CareIcon from "../../../CAREUI/icons/CareIcon";
import TextFormField from "../../Form/FormFields/TextFormField";
import HL7PatientVitalsMonitor from "../../VitalsMonitor/HL7PatientVitalsMonitor";
import VentilatorPatientVitalsMonitor from "../../VitalsMonitor/VentilatorPatientVitalsMonitor";
+import useAuthUser from "../../../Common/hooks/useAuthUser";
+import request from "../../../Utils/request/request";
+import routes from "../../../Redux/api";
+import useQuery from "../../../Utils/request/useQuery";
interface HL7MonitorProps {
assetId: string;
@@ -31,20 +30,15 @@ const HL7Monitor = (props: HL7MonitorProps) => {
const [isLoading, setIsLoading] = useState(true);
const [localipAddress, setLocalIPAddress] = useState("");
const [ipadrdress_error, setIpAddress_error] = useState("");
-
- const dispatch = useDispatch ();
-
- useEffect(() => {
- const fetchFacility = async () => {
- const res = await dispatch(getPermittedFacility(facilityId));
-
- if (res.status === 200 && res.data) {
- setFacilityMiddlewareHostname(res.data.middleware_address);
+ const authUser = useAuthUser();
+ const { data: facility, loading } = useQuery(routes.getPermittedFacility, {
+ pathParams: { id: facilityId },
+ onResponse: ({ res, data }) => {
+ if (res?.status === 200 && data && data.middleware_address) {
+ setFacilityMiddlewareHostname(data.middleware_address);
}
- };
-
- if (facilityId) fetchFacility();
- }, [dispatch, facilityId]);
+ },
+ });
useEffect(() => {
setAssetType(asset?.asset_class);
@@ -64,72 +58,98 @@ const HL7Monitor = (props: HL7MonitorProps) => {
local_ip_address: localipAddress,
},
};
- const res: any = await Promise.resolve(
- dispatch(partialUpdateAsset(assetId, data))
- );
+ const { res } = await request(routes.partialUpdateAsset, {
+ pathParams: { external_id: assetId },
+ body: data,
+ });
if (res?.status === 200) {
Notification.Success({
msg: "Asset Configured Successfully",
});
} else {
Notification.Error({
- msg: "Something went wrong..!",
+ msg: "Something went wrong!",
});
}
} else {
- setIpAddress_error("Please Enter a Valid IP address !!");
+ setIpAddress_error("IP address is invalid");
}
};
- const middleware = middlewareHostname || facilityMiddlewareHostname;
+ const fallbackMiddleware =
+ asset?.location_object?.middleware_address || facilityMiddlewareHostname;
- if (isLoading) return ;
+ if (isLoading || loading || !facility) return ;
return (
-
-
-
-
- {["HL7MONITOR"].includes(assetType) && (
-
-
+ {["DistrictAdmin", "StateAdmin"].includes(authUser.user_type) && (
+
+
- )}
-
+ {["HL7MONITOR"].includes(assetType) && (
+
+
+
+ )}
+
+ )}
{assetType === "HL7MONITOR" && (
)}
{assetType === "VENTILATOR" && (
)}
diff --git a/src/Components/Assets/AssetType/ONVIFCamera.tsx b/src/Components/Assets/AssetType/ONVIFCamera.tsx
index fa0a6c3762e..8c99e043698 100644
--- a/src/Components/Assets/AssetType/ONVIFCamera.tsx
+++ b/src/Components/Assets/AssetType/ONVIFCamera.tsx
@@ -2,10 +2,8 @@ import { useEffect, useState } from "react";
import { AssetData } from "../AssetTypes";
import { useDispatch } from "react-redux";
import {
- partialUpdateAsset,
partialUpdateAssetBed,
createAssetBed,
- getPermittedFacility,
listAssetBeds,
deleteAssetBed,
} from "../../../Redux/actions";
@@ -21,15 +19,22 @@ import { SyntheticEvent } from "react";
import LiveFeed from "../../Facility/Consultations/LiveFeed";
import Card from "../../../CAREUI/display/Card";
import { BoundaryRange } from "../../../Common/constants";
+import useAuthUser from "../../../Common/hooks/useAuthUser";
-interface ONVIFCameraProps {
+import request from "../../../Utils/request/request";
+import routes from "../../../Redux/api";
+import useQuery from "../../../Utils/request/useQuery";
+
+import CareIcon from "../../../CAREUI/icons/CareIcon";
+
+interface Props {
assetId: string;
facilityId: string;
asset: any;
+ onUpdated?: () => void;
}
-const ONVIFCamera = (props: ONVIFCameraProps) => {
- const { assetId, facilityId, asset } = props;
+const ONVIFCamera = ({ assetId, facilityId, asset, onUpdated }: Props) => {
const [isLoading, setIsLoading] = useState(true);
const [assetType, setAssetType] = useState("");
const [middlewareHostname, setMiddlewareHostname] = useState("");
@@ -133,17 +138,20 @@ const ONVIFCamera = (props: ONVIFCameraProps) => {
}
return boundary;
};
+ const { data: facility, loading } = useQuery(routes.getPermittedFacility, {
+ pathParams: { id: facilityId },
+ });
+ const authUser = useAuthUser();
useEffect(() => {
- const fetchFacility = async () => {
- const res = await dispatch(getPermittedFacility(facilityId));
+ if (facility?.middleware_address) {
+ setFacilityMiddlewareHostname(facility.middleware_address);
+ }
+ }, [facility, facilityId]);
- if (res.status === 200 && res.data) {
- setFacilityMiddlewareHostname(res.data.middleware_address);
- }
- };
+ const fallbackMiddleware =
+ asset?.location_object?.middleware_address || facilityMiddlewareHostname;
- if (facilityId) fetchFacility();
- }, [dispatch, facilityId]);
+ const currentMiddleware = middlewareHostname || fallbackMiddleware;
useEffect(() => {
if (asset) {
@@ -202,22 +210,19 @@ const ONVIFCamera = (props: ONVIFCameraProps) => {
camera_access_key: `${username}:${password}:${streamUuid}`,
},
};
- const res: any = await Promise.resolve(
- dispatch(partialUpdateAsset(assetId, data))
- );
+ const { res } = await request(routes.partialUpdateAsset, {
+ pathParams: { external_id: assetId },
+ body: data,
+ });
if (res?.status === 200) {
- Notification.Success({
- msg: "Asset Configured Successfully",
- });
- window.location.reload();
+ Notification.Success({ msg: "Asset Configured Successfully" });
+ onUpdated?.();
} else {
- Notification.Error({
- msg: "Something went wrong..!",
- });
+ Notification.Error({ msg: "Something went wrong!" });
}
setLoadingSetConfiguration(false);
} else {
- setIpAddress_error("Please Enter a Valid Camera address !!");
+ setIpAddress_error("IP address is invalid");
}
};
@@ -335,17 +340,16 @@ const ONVIFCamera = (props: ONVIFCameraProps) => {
try {
setLoadingAddPreset(true);
const presetData = await axios.get(
- `https://${facilityMiddlewareHostname}/status?hostname=${config.hostname}&port=${config.port}&username=${config.username}&password=${config.password}`
- );
- const res: any = await Promise.resolve(
- dispatch(
- createAssetBed(
- { meta: { ...data, ...presetData.data } },
- assetId,
- bed?.id as string
- )
- )
+ `https://${currentMiddleware}/status?hostname=${config.hostname}&port=${config.port}&username=${config.username}&password=${config.password}`
);
+
+ const { res } = await request(routes.createAssetBed, {
+ body: {
+ meta: { ...data, ...presetData.data },
+ asset: assetId,
+ bed: bed?.id as string,
+ },
+ });
if (res?.status === 201) {
Notification.Success({
msg: "Preset Added Successfully",
@@ -364,60 +368,81 @@ const ONVIFCamera = (props: ONVIFCameraProps) => {
}
setLoadingAddPreset(false);
};
- if (isLoading) return ;
+ if (isLoading || loading || !facility) return ;
+
return (
-
+ {["DistrictAdmin", "StateAdmin"].includes(authUser.user_type) && (
+
+
+
+
+
+ )}
{assetType === "ONVIF" ? (
<>
diff --git a/src/Components/Assets/AssetTypes.tsx b/src/Components/Assets/AssetTypes.tsx
index 52c7e45003c..8b96b6beeb7 100644
--- a/src/Components/Assets/AssetTypes.tsx
+++ b/src/Components/Assets/AssetTypes.tsx
@@ -91,6 +91,8 @@ export interface AssetData {
};
}
+export type AssetUpdate = Partial;
+
export interface AssetsResponse {
count: number;
next?: string;
@@ -137,8 +139,12 @@ export interface AssetBedModel {
created_date: string;
modified_date: string;
meta: Record;
+ asset?: string;
+ bed?: string;
}
+export type AssetBedBody = Partial;
+
export interface AssetServiceEdit {
id: string;
asset_service: AssetService;
@@ -166,3 +172,9 @@ export interface PatientAssetBed {
patient?: PatientModel;
meta?: Record;
}
+
+export interface AssetServiceUpdate {
+ external_id: string;
+ serviced_on: string;
+ note: string;
+}
diff --git a/src/Components/Assets/AssetWarrantyCard.tsx b/src/Components/Assets/AssetWarrantyCard.tsx
index 5d10d4096c8..2e5fa74b9c6 100644
--- a/src/Components/Assets/AssetWarrantyCard.tsx
+++ b/src/Components/Assets/AssetWarrantyCard.tsx
@@ -1,36 +1,68 @@
import CareIcon from "../../CAREUI/icons/CareIcon";
import { AssetData } from "./AssetTypes";
import { classNames, formatDate } from "../../Utils/utils";
+import CopyToClipboard from "react-copy-to-clipboard";
+import { t } from "i18next";
+import { useEffect, useState } from "react";
export default function AssetWarrantyCard(props: { asset: AssetData }) {
const { asset } = props;
const details = {
"Serial Number": asset.serial_number,
- Expiry:
+ "Warranty/AMC Expiry":
asset.warranty_amc_end_of_validity &&
formatDate(asset.warranty_amc_end_of_validity),
Vendor: asset.vendor_name,
};
+ const [isCopied, setIsCopied] = useState(false);
+
+ useEffect(() => {
+ if (isCopied) {
+ const timeout = setTimeout(() => {
+ setIsCopied(false);
+ }, 2000);
+ return () => clearTimeout(timeout);
+ }
+ }, [isCopied]);
+
return (
{asset.manufacturer}
-
-
+
+
{Object.keys(details).map((key) => (
{key}
-
+
{details[key as keyof typeof details] || "--"}
+ {key === "Serial Number" && (
+
+ )}
))}
+
diff --git a/src/Components/Assets/AssetsList.tsx b/src/Components/Assets/AssetsList.tsx
index 5b820f93e29..8c19928fbee 100644
--- a/src/Components/Assets/AssetsList.tsx
+++ b/src/Components/Assets/AssetsList.tsx
@@ -1,15 +1,8 @@
-import { useDispatch } from "react-redux";
import QrReader from "react-qr-reader";
-import { statusType, useAbortableEffect } from "../../Common/utils";
import * as Notification from "../../Utils/Notifications.js";
-import {
- getAnyFacility,
- listAssets,
- getFacilityAssetLocation,
- getAsset,
-} from "../../Redux/actions";
+import { listAssets } from "../../Redux/actions";
import { assetClassProps, AssetData } from "./AssetTypes";
-import { useState, useCallback, useEffect, lazy } from "react";
+import { useState, useEffect, lazy } from "react";
import { Link, navigate } from "raviger";
import AssetFilter from "./AssetFilter";
import { parseQueryParams } from "../../Utils/primitives";
@@ -28,6 +21,9 @@ import AssetImportModal from "./AssetImportModal";
import Page from "../Common/components/Page";
import { AdvancedFilterButton } from "../../CAREUI/interactive/FiltersSlideover";
import { useTranslation } from "react-i18next";
+import request from "../../Utils/request/request";
+import routes from "../../Redux/api";
+import useQuery from "../../Utils/request/useQuery";
const Loading = lazy(() => import("../Common/Loading"));
@@ -50,62 +46,45 @@ const AssetsList = () => {
const [facility, setFacility] = useState ();
const [asset_type, setAssetType] = useState();
const [status, setStatus] = useState();
- const [facilityName, setFacilityName] = useState();
const [asset_class, setAssetClass] = useState();
- const [locationName, setLocationName] = useState();
const [importAssetModalOpen, setImportAssetModalOpen] = useState(false);
- const dispatch: any = useDispatch();
const assetsExist = assets.length > 0 && Object.keys(assets[0]).length > 0;
const [showFacilityDialog, setShowFacilityDialog] = useState(false);
const [selectedFacility, setSelectedFacility] = useState({
name: "",
});
+ const params = {
+ limit: resultsPerPage,
+ page: qParams.page,
+ offset: (qParams.page ? qParams.page - 1 : 0) * resultsPerPage,
+ search_text: qParams.search || "",
+ facility: qParams.facility || "",
+ asset_type: qParams.asset_type || "",
+ asset_class: qParams.asset_class || "",
+ location: qParams.facility ? qParams.location || "" : "",
+ status: qParams.status || "",
+ };
- const fetchData = useCallback(
- async (status: statusType) => {
- setIsLoading(true);
- const params = {
- limit: resultsPerPage,
- page: qParams.page,
- offset: (qParams.page ? qParams.page - 1 : 0) * resultsPerPage,
- search_text: qParams.search || "",
- facility: qParams.facility || "",
- asset_type: qParams.asset_type || "",
- asset_class: qParams.asset_class || "",
- location: qParams.facility ? qParams.location || "" : "",
- status: qParams.status || "",
- };
- const { data } = await dispatch(listAssets(params));
- if (!status.aborted) {
- setIsLoading(false);
- if (!data)
- Notification.Error({
- msg: "Something went wrong..!",
- });
- else {
- setAssets(data.results);
- setTotalCount(data.count);
- if (qParams.facility) {
- const fetchFacility = await dispatch(
- getAnyFacility(qParams.facility)
- );
- setSelectedFacility(fetchFacility.data as FacilityModel);
- }
- }
+ const { loading } = useQuery(routes.listAssets, {
+ query: params,
+ onResponse: ({ res, data }) => {
+ if (res?.status === 200 && data) {
+ setAssets(data.results);
+ setTotalCount(data.count);
}
},
- [
- resultsPerPage,
- qParams.page,
- qParams.search,
- qParams.facility,
- qParams.asset_type,
- qParams.asset_class,
- qParams.location,
- qParams.status,
- dispatch,
- ]
- );
+ });
+
+ const { data: facilityObject } = useQuery(routes.getAnyFacility, {
+ pathParams: { id: qParams.facility },
+ onResponse: ({ res, data }) => {
+ if (res?.status === 200 && data) {
+ setFacility(data);
+ setSelectedFacility(data);
+ }
+ },
+ prefetch: !!qParams.facility,
+ });
useEffect(() => {
setAssetType(qParams.asset_type);
@@ -119,56 +98,13 @@ const AssetsList = () => {
setAssetClass(qParams.asset_class);
}, [qParams.asset_class]);
- useAbortableEffect(
- (status: statusType) => {
- fetchData(status);
- },
- [dispatch, fetchData]
- );
- useEffect(() => {
- async function fetchFacilityName() {
- if (!qParams.facility) return setFacilityName("");
- const res = await dispatch(getAnyFacility(qParams.facility, "facility"));
- setFacilityName(res?.data?.name);
- }
- fetchFacilityName();
- }, [dispatch, qParams.facility]);
-
- const fetchFacility = useCallback(
- async (status: statusType) => {
- if (!qParams.facility) return setFacility(undefined);
- setIsLoading(true);
- const res = await dispatch(getAnyFacility(qParams.facility));
- if (!status.aborted) {
- setFacility(res?.data);
- setIsLoading(false);
- }
- },
- [dispatch, qParams.facility]
- );
- const fetchLocationName = useCallback(
- async (status: statusType) => {
- if (!qParams.location || !qParams.facility)
- return setLocationName(undefined);
- setIsLoading(true);
- const res = await dispatch(
- getFacilityAssetLocation(qParams.facility, qParams.location)
- );
- if (!status.aborted) {
- setLocationName(res?.data?.name);
- setIsLoading(false);
- }
- },
- [dispatch, qParams.facility, qParams.location]
- );
-
- useAbortableEffect(
- (status: statusType) => {
- fetchFacility(status);
- fetchLocationName(status);
+ const { data: locationObject } = useQuery(routes.getFacilityAssetLocation, {
+ pathParams: {
+ facility_external_id: String(qParams.facility),
+ external_id: String(qParams.location),
},
- [fetchFacility, fetchLocationName]
- );
+ prefetch: !!(qParams.facility && qParams.location),
+ });
const getAssetIdFromQR = async (assetUrl: string) => {
try {
@@ -178,8 +114,10 @@ const AssetsList = () => {
// QR Maybe searchParams "asset" or "assetQR"
const assetId = params.asset || params.assetQR;
if (assetId) {
- const { data } = await dispatch(listAssets({ qr_code_id: assetId }));
- return data.results[0].id;
+ const { data } = await request(routes.listAssets, {
+ query: { qr_code_id: assetId },
+ });
+ return data?.results[0].id;
}
} catch (err) {
console.log(err);
@@ -187,11 +125,13 @@ const AssetsList = () => {
};
const checkValidAssetId = async (assetId: string) => {
- const assetData = await dispatch(getAsset(assetId));
+ const { data: assetData } = await request(routes.getAsset, {
+ pathParams: { id: assetId },
+ });
try {
- if (assetData.data) {
+ if (assetData) {
navigate(
- `/facility/${assetData.data.location_object.facility.id}/assets/${assetId}`
+ `/facility/${assetData.location_object.facility.id}/assets/${assetId}`
);
}
} catch (err) {
@@ -236,7 +176,13 @@ const AssetsList = () => {
);
let manageAssets = null;
- if (assetsExist) {
+ if (loading) {
+ manageAssets = (
+
+
+
+ );
+ } else if (assetsExist) {
manageAssets = (
{assets.map((asset: AssetData) => (
@@ -288,6 +234,7 @@ const AssetsList = () => {
) : (
)}
+ {warrantyAmcValidityChip(asset.warranty_amc_end_of_validity)}
@@ -310,19 +257,21 @@ const AssetsList = () => {
options={
<>
{authorizedForImportExport && (
-
+
,
+ icon: (
+
+ ),
onClick: () => setImportAssetModalOpen(true),
},
},
{
- label: "Export Assets",
+ label: "Export Assets (JSON)",
action: () =>
authorizedForImportExport &&
listAssets({
@@ -331,10 +280,28 @@ const AssetsList = () => {
limit: totalCount,
}),
type: "json",
- filePrefix: `assets_${facility?.name}`,
+ filePrefix: `assets_${facility?.name ?? "all"}`,
+ options: {
+ icon: ,
+ disabled: totalCount === 0 || !authorizedForImportExport,
+ id: "export-json-option",
+ },
+ },
+ {
+ label: "Export Assets (CSV)",
+ action: () =>
+ authorizedForImportExport &&
+ listAssets({
+ ...qParams,
+ csv: true,
+ limit: totalCount,
+ }),
+ type: "csv",
+ filePrefix: `assets_${facility?.name ?? "all"}`,
options: {
icon: ,
disabled: totalCount === 0 || !authorizedForImportExport,
+ id: "export-csv-option",
},
},
]}
@@ -348,7 +315,7 @@ const AssetsList = () => {
@@ -402,12 +369,30 @@ const AssetsList = () => {
<>
[
- value("Facility", "facility", facilityName ?? ""),
+ value(
+ "Facility",
+ "facility",
+ qParams.facility && facilityObject?.name
+ ),
badge("Name/Serial No./QR ID", "search"),
value("Asset Type", "asset_type", asset_type ?? ""),
value("Asset Class", "asset_class", asset_class ?? ""),
value("Status", "status", status?.replace(/_/g, " ") ?? ""),
- value("Location", "location", locationName ?? ""),
+ value(
+ "Location",
+ "location",
+ qParams.location && locationObject?.name
+ ),
+ value(
+ "Warranty AMC End Of Validity Before",
+ "warranty_amc_end_of_validity_before",
+ qParams.warranty_amc_end_of_validity_before ?? ""
+ ),
+ value(
+ "Warranty AMC End Of Validity After",
+ "warranty_amc_end_of_validity_after",
+ qParams.warranty_amc_end_of_validity_after ?? ""
+ ),
]}
/>
@@ -464,4 +449,47 @@ const AssetsList = () => {
);
};
+export const warrantyAmcValidityChip = (
+ warranty_amc_end_of_validity: string
+) => {
+ if (warranty_amc_end_of_validity === "" || !warranty_amc_end_of_validity)
+ return;
+ const today = new Date();
+ const warrantyAmcEndDate = new Date(warranty_amc_end_of_validity);
+
+ const days = Math.ceil(
+ Math.abs(Number(warrantyAmcEndDate) - Number(today)) / (1000 * 60 * 60 * 24)
+ );
+
+ if (warrantyAmcEndDate < today) {
+ return (
+
+ );
+ } else if (days <= 30) {
+ return (
+
+ );
+ } else if (days <= 90) {
+ return (
+
+ );
+ }
+};
+
export default AssetsList;
diff --git a/src/Components/Assets/configure/MonitorConfigure.tsx b/src/Components/Assets/configure/MonitorConfigure.tsx
index 1d469335ce2..d1b24b24959 100644
--- a/src/Components/Assets/configure/MonitorConfigure.tsx
+++ b/src/Components/Assets/configure/MonitorConfigure.tsx
@@ -1,62 +1,50 @@
-import { Dispatch, useEffect, useState } from "react";
+import { useState } from "react";
import { BedSelect } from "../../Common/BedSelect";
import { BedModel } from "../../Facility/models";
import { AssetData } from "../AssetTypes";
-import {
- createAssetBed,
- listAssetBeds,
- partialUpdateAssetBed,
-} from "../../../Redux/actions";
import * as Notification from "../../../Utils/Notifications.js";
-import { useDispatch } from "react-redux";
import { Submit } from "../../Common/components/ButtonV2";
import { FieldLabel } from "../../Form/FormFields/FormField";
+import request from "../../../Utils/request/request";
+import routes from "../../../Redux/api";
+import useQuery from "../../../Utils/request/useQuery";
-const saveLink = (assetId: string, bedId: string, dispatch: Dispatch ) => {
- dispatch(createAssetBed({}, assetId, bedId));
+const saveLink = async (assetId: string, bedId: string) => {
+ await request(routes.createAssetBed, {
+ body: {
+ asset: assetId,
+ bed: bedId,
+ },
+ });
Notification.Success({ msg: "AssetBed Link created successfully" });
};
-const update_Link = (
+const update_Link = async (
assetbedId: string,
assetId: string,
- bed: BedModel,
- assetBed: any,
- dispatch: Dispatch
+ bed: BedModel
) => {
- dispatch(
- partialUpdateAssetBed(
- {
- asset: assetId,
- bed: bed.id,
- },
- assetbedId
- )
- );
+ await request(routes.partialUpdateAssetBed, {
+ pathParams: { external_id: assetbedId },
+ body: {
+ asset: assetId,
+ bed: bed.id ?? "",
+ },
+ });
Notification.Success({ msg: "AssetBed Link updated successfully" });
};
export default function MonitorConfigure({ asset }: { asset: AssetData }) {
const [bed, setBed] = useState({});
const [updateLink, setUpdateLink] = useState(false);
- const [assetBed, setAssetBed] = useState();
- const dispatch: any = useDispatch();
-
- const getAssetBeds = async (id: string) => {
- const assetBeds = await dispatch(listAssetBeds({ asset: id }));
- if (assetBeds.data?.results?.length > 0) {
- setUpdateLink(true);
- setAssetBed(assetBeds.data.results[0]);
- setBed(assetBeds.data.results[0].bed_object);
- } else {
- setUpdateLink(false);
- }
- };
-
- useEffect(() => {
- if (asset.id) {
- getAssetBeds(asset.id);
- }
- }, [asset]);
+ const { data: assetBed } = useQuery(routes.listAssetBeds, {
+ query: { asset: asset.id },
+ onResponse: ({ res, data }) => {
+ if (res?.status === 200 && data && data.results.length > 0) {
+ setBed(data.results[0].bed_object);
+ setUpdateLink(true);
+ }
+ },
+ });
return (
@@ -317,7 +301,7 @@ const DateInputV2: React.FC = ({
new Date().getFullYear() === year.getFullYear()) ||
!isDateWithinConstraints(getLastDay())
}
- className="aspect-square inline-flex cursor-pointer items-center justify-center rounded p-2 transition duration-100 ease-in-out hover:bg-gray-300"
+ className="inline-flex aspect-square cursor-pointer items-center justify-center rounded p-2 transition duration-100 ease-in-out hover:bg-gray-300"
onClick={increment}
>
@@ -385,14 +369,9 @@ const DateInputV2: React.FC = ({
)}
onClick={setMonthValue(i)}
>
- {format(
- new Date(
- datePickerHeaderDate.getFullYear(),
- i,
- 1
- ),
- "MMM"
- )}
+ {dayjs(
+ new Date(datePickerHeaderDate.getFullYear(), i, 1)
+ ).format("MMM")}
))}
diff --git a/src/Components/Common/DiagnosisSelectFormField.tsx b/src/Components/Common/DiagnosisSelectFormField.tsx
deleted file mode 100644
index 4086664d4ba..00000000000
--- a/src/Components/Common/DiagnosisSelectFormField.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import { useAsyncOptions } from "../../Common/hooks/useAsyncOptions";
-import { listICD11Diagnosis } from "../../Redux/actions";
-import { ICD11DiagnosisModel } from "../Facility/models";
-import { AutocompleteMutliSelect } from "../Form/FormFields/AutocompleteMultiselect";
-import FormField from "../Form/FormFields/FormField";
-import {
- FormFieldBaseProps,
- useFormFieldPropsResolver,
-} from "../Form/FormFields/Utils";
-
-type Props =
- // | ({ multiple?: false | undefined } & FormFieldBaseProps ) // uncomment when single select form field is required and implemented.
- { multiple: true } & FormFieldBaseProps;
-
-export function DiagnosisSelectFormField(props: Props) {
- const field = useFormFieldPropsResolver(props);
- const { fetchOptions, isLoading, options } =
- useAsyncOptions("id");
-
- if (!props.multiple) {
- return (
-
- Component not implemented
-
- );
- }
-
- return (
-
- option.label}
- optionValue={(option) => option}
- onQuery={(query) =>
- fetchOptions(listICD11Diagnosis({ query }, field.id || ""))
- }
- isLoading={isLoading}
- />
-
- );
-}
diff --git a/src/Components/Common/Dialog.tsx b/src/Components/Common/Dialog.tsx
index ffe17606de2..c1dcf5afb70 100644
--- a/src/Components/Common/Dialog.tsx
+++ b/src/Components/Common/Dialog.tsx
@@ -26,7 +26,7 @@ const DialogModal = (props: DialogProps) => {
return (
-
-
+
import("../Common/Loading"));
@@ -63,79 +57,70 @@ export default function UpdateResult(props: any) {
const { id } = props;
const { goBack } = useAppHistory();
- const dispatchAction: any = useDispatch();
const [state, dispatch] = useReducer(FormReducer, initialState);
- const [isLoading, setIsLoading] = useState(false);
+ const [isLoading, setIsLoading] = useState(true);
const [isLocalbodyLoading, setIsLocalbodyLoading] = useState(false);
const [isWardLoading, setIsWardLoading] = useState(false);
const [localBody, setLocalBody] = useState(initialLocalbodies);
const [ward, setWard] = useState(initialLocalbodies);
- const fetchData = useCallback(
- async (status: statusType) => {
- setIsLoading(true);
- const res = await dispatchAction(externalResult({ id: id }));
- if (!status.aborted) {
- if (res && res.data) {
- const form = { ...state.form };
- form["name"] = res.data.name;
- form["age"] = res.data.age;
- form["age_in"] = res.data.age_in;
- form["srf_id"] = res.data.srf_id;
- form["address"] = res.data.address;
- form["district"] = res.data.district_object.name;
- form["local_body"] = String(res.data.local_body);
- form["ward"] = String(res.data.ward);
- form["patient_created"] = String(res.data.patient_created);
+ const { loading } = useQuery(routes.externalResult, {
+ pathParams: { id },
+ onResponse: async ({ res, data }) => {
+ if (res && data) {
+ const form = { ...state.form };
+ form["name"] = data.name;
+ form["age"] = data.age;
+ form["age_in"] = data.age_in;
+ form["srf_id"] = data.srf_id;
+ form["address"] = data.address;
+ form["district"] = data.district_object.name;
+ form["local_body"] = String(data.local_body);
+ form["ward"] = String(data.ward);
+ form["patient_created"] = String(data.patient_created);
- dispatch({ type: "set_form", form });
+ dispatch({ type: "set_form", form });
- Promise.all([
- fetchLocalBody(res.data.district),
- fetchWards(res.data.local_body),
- ]);
- }
+ Promise.all([
+ fetchLocalBody(data.district),
+ fetchWards(data.local_body),
+ ]);
setIsLoading(false);
}
},
- [props.id, dispatchAction]
- );
+ });
- const fetchLocalBody = useCallback(
- async (id: string) => {
- if (Number(id) > 0) {
- setIsLocalbodyLoading(true);
- const localBodyList = await dispatchAction(
- getLocalbodyByDistrict({ id })
- );
+ const fetchLocalBody = async (id: number) => {
+ if (Number(id) > 0) {
+ setIsLocalbodyLoading(true);
+ const { res, data } = await request(routes.getLocalbodyByDistrict, {
+ pathParams: { id: String(id) },
+ });
+ if (res && data) {
setIsLocalbodyLoading(false);
- setLocalBody([...initialLocalbodies, ...localBodyList.data]);
- } else {
- setLocalBody(initialLocalbodies);
+ setLocalBody([...initialLocalbodies, ...data]);
}
- },
- [dispatchAction]
- );
+ } else {
+ setLocalBody(initialLocalbodies);
+ }
+ };
const fetchWards = useCallback(
- async (id: string) => {
+ async (id: number) => {
if (Number(id) > 0) {
setIsWardLoading(true);
- const wardList = await dispatchAction(getWardByLocalBody({ id }));
+ const { res, data } = await request(routes.getWardByLocalBody, {
+ pathParams: { id: String(id) },
+ });
+ if (res && data) {
+ setWard([...initialWard, ...data.results]);
+ }
setIsWardLoading(false);
- setWard([...initialWard, ...wardList.data.results]);
} else {
setWard(initialLocalbodies);
}
},
- [dispatchAction]
- );
-
- useAbortableEffect(
- (status: statusType) => {
- fetchData(status);
- },
- [fetchData]
+ [props.id]
);
const validateForm = () => {
@@ -195,15 +180,20 @@ export default function UpdateResult(props: any) {
const validForm = validateForm();
if (validForm) {
setIsLoading(true);
- const data = {
+ const rdata = {
address: state.form.address ? state.form.address : undefined,
local_body: state.form.local_body ? state.form.local_body : undefined,
ward: state.form.ward,
patient_created: state.form.patient_created === "true",
};
- const res = await dispatchAction(partialUpdateExternalResult(id, data));
+
+ const { res, data } = await request(routes.partialUpdateExternalResult, {
+ pathParams: { id },
+ body: rdata,
+ });
+
setIsLoading(false);
- if (res && res.data) {
+ if (res && data) {
dispatch({ type: "set_form", form: initForm });
Notification.Success({
msg: "External Result updated successfully",
@@ -213,7 +203,7 @@ export default function UpdateResult(props: any) {
}
};
- if (isLoading) {
+ if (isLoading || loading) {
return ;
}
@@ -262,10 +252,7 @@ export default function UpdateResult(props: any) {
options={localBody}
optionLabel={(localBody) => localBody.name}
optionValue={(localBody) => localBody.id}
- onChange={(e) => [
- handleChange(e),
- fetchWards(String(e.value)),
- ]}
+ onChange={(e) => [handleChange(e), fetchWards(e.value)]}
error={state.errors.local_body}
/>
)}
diff --git a/src/Components/ExternalResult/models.ts b/src/Components/ExternalResult/models.ts
new file mode 100644
index 00000000000..8ccaba04d05
--- /dev/null
+++ b/src/Components/ExternalResult/models.ts
@@ -0,0 +1,72 @@
+export interface IExternalResultUploadCsv {
+ sample_tests: any[];
+}
+
+export interface IExternalResult {
+ id: number;
+ name: string;
+ age: number;
+ age_in: string;
+ test_type: string;
+ result: string;
+ result_date: string;
+ patient_created: boolean;
+ gender: string;
+ source: string;
+ is_repeat: boolean;
+ mobile_number: string;
+ patient_status: string;
+ sample_type: string;
+ sample_collection_date: string;
+ patient_category: string;
+ srf_id: string;
+ district_object: {
+ id: number;
+ name: string;
+ state: number;
+ };
+ district: number;
+ ward: number;
+ local_body: number;
+ address: string;
+ ward_object: {
+ id: number;
+ number: number;
+ name: string;
+ };
+ local_body_object: {
+ id: number;
+ name: string;
+ };
+}
+
+export interface ILocalBodies {
+ id: number;
+ name: string;
+ state: number;
+ number: number;
+ body_type: number;
+ localbody_code: string;
+ district: number;
+}
+
+export interface IDeleteExternalResult {
+ detail: string;
+}
+
+export interface IPartialUpdateExternalResult {
+ address: string;
+ ward: number;
+ local_body: number;
+ patient_created: boolean;
+}
+
+export interface ILocalBodyByDistrict {
+ id: number;
+ name: string;
+ state: number;
+}
+
+export interface IExternalResultCsv {
+ sample_tests: Partial[];
+}
diff --git a/src/Components/Facility/AddLocationForm.tsx b/src/Components/Facility/AddLocationForm.tsx
index d3c64ec9e04..e71b68cc95c 100644
--- a/src/Components/Facility/AddLocationForm.tsx
+++ b/src/Components/Facility/AddLocationForm.tsx
@@ -25,12 +25,14 @@ export const AddLocationForm = (props: LocationFormProps) => {
const dispatchAction: any = useDispatch();
const [isLoading, setIsLoading] = useState(false);
const [name, setName] = useState("");
+ const [middlewareAddress, setMiddlewareAddress] = useState("");
const [description, setDescription] = useState("");
const [facilityName, setFacilityName] = useState("");
const [locationName, setLocationName] = useState("");
const [errors, setErrors] = useState({
name: "",
description: "",
+ middlewareAddress: "",
});
const headerText = !locationId ? "Add Location" : "Update Location";
const buttonText = !locationId ? "Add Location" : "Update Location";
@@ -51,6 +53,7 @@ export const AddLocationForm = (props: LocationFormProps) => {
setName(res?.data?.name || "");
setLocationName(res?.data?.name || "");
setDescription(res?.data?.description || "");
+ setMiddlewareAddress(res?.data?.middleware_address || "");
}
setIsLoading(false);
}
@@ -62,6 +65,7 @@ export const AddLocationForm = (props: LocationFormProps) => {
const error = {
name: "",
description: "",
+ middlewareAddress: "",
};
if (name.trim().length === 0) {
@@ -69,6 +73,16 @@ export const AddLocationForm = (props: LocationFormProps) => {
formValid = false;
}
+ if (
+ middlewareAddress &&
+ middlewareAddress.match(
+ /^(?!https?:\/\/)[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)*\.[a-zA-Z]{2,}$/
+ ) === null
+ ) {
+ error.middlewareAddress = "Invalid Middleware Address";
+ formValid = false;
+ }
+
setErrors(error);
return formValid;
};
@@ -83,6 +97,7 @@ export const AddLocationForm = (props: LocationFormProps) => {
const data = {
name,
description,
+ middleware_address: middlewareAddress,
};
const res = await dispatchAction(
@@ -157,8 +172,18 @@ export const AddLocationForm = (props: LocationFormProps) => {
error={errors.description}
/>
+
+ setMiddlewareAddress(e.value)}
+ error={errors.middlewareAddress}
+ />
+
-
+
navigate(`/facility/${facilityId}/location`, {
diff --git a/src/Components/Facility/AssetCreate.tsx b/src/Components/Facility/AssetCreate.tsx
index 1e6eead5242..84fc09188d0 100644
--- a/src/Components/Facility/AssetCreate.tsx
+++ b/src/Components/Facility/AssetCreate.tsx
@@ -30,15 +30,15 @@ import TextAreaFormField from "../Form/FormFields/TextAreaFormField";
import TextFormField from "../Form/FormFields/TextFormField";
import { navigate } from "raviger";
-import { parsePhoneNumberFromString } from "libphonenumber-js";
import { parseQueryParams } from "../../Utils/primitives";
import useAppHistory from "../../Common/hooks/useAppHistory";
import { useDispatch } from "react-redux";
import useVisibility from "../../Utils/useVisibility";
import { validateEmailAddress } from "../../Common/validation";
-import { dateQueryString } from "../../Utils/utils.js";
+import { dateQueryString, parsePhoneNumber } from "../../Utils/utils.js";
import dayjs from "../../Utils/dayjs";
-import DateInputV2 from "../Common/DateInputV2.js";
+import DateFormField from "../Form/FormFields/DateFormField.js";
+import { t } from "i18next";
const Loading = lazy(() => import("../Common/Loading"));
@@ -306,7 +306,7 @@ const AssetCreate = (props: AssetProps) => {
setLocation("");
setAssetType(assetTypeInitial);
setAssetClass(assetClassInitial);
- setIsWorking("");
+ setIsWorking(undefined);
setNotWorkingReason("");
setSerialNumber("");
setVendorName("");
@@ -341,7 +341,7 @@ const AssetCreate = (props: AssetProps) => {
support_email: support_email,
support_phone: support_phone.startsWith("1800")
? support_phone
- : parsePhoneNumberFromString(support_phone)?.format("E.164"),
+ : parsePhoneNumber(support_phone),
qr_code_id: qrCodeId !== "" ? qrCodeId : null,
manufacturer: manufacturer,
warranty_amc_end_of_validity: warranty_amc_end_of_validity
@@ -405,7 +405,7 @@ const AssetCreate = (props: AssetProps) => {
if (locations.length === 0) {
return (
{
- You need at least a location to create an assest.
+ {t("you_need_at_least_a_location_to_create_an_assest")}
@@ -441,7 +441,8 @@ const AssetCreate = (props: AssetProps) => {
onClick={() => setIsScannerActive(false)}
className="btn btn-default mb-2"
>
- Close Scanner
+
+ {t("close_scanner")}
{
}
style={{ width: "100%" }}
/>
- Scan Asset QR!
+
+ {t("scan_asset_qr")}
+
);
@@ -480,7 +483,7 @@ const AssetCreate = (props: AssetProps) => {
return (
{
>
setName(value)}
@@ -545,7 +548,7 @@ const AssetCreate = (props: AssetProps) => {
{/* Location */}
- Asset Location
+ {t("asset_location")}
{
data-testid="asset-type-input"
>
{
{
>
setDescription(value)}
error={state.errors.description}
@@ -665,7 +668,7 @@ const AssetCreate = (props: AssetProps) => {
className="col-span-6"
required
name="is_working"
- label="Working Status"
+ label={t("working_status")}
options={["true", "false"]}
optionLabel={(option) => {
return (
@@ -693,8 +696,8 @@ const AssetCreate = (props: AssetProps) => {
>
setNotWorkingReason(e.value)}
error={state.errors.not_working_reason}
@@ -718,7 +721,7 @@ const AssetCreate = (props: AssetProps) => {
id="qr_code_id"
name="qr_code_id"
placeholder=""
- label="Asset QR ID"
+ label={t("asset_qr_id")}
value={qrCodeId}
onChange={(e) => setQrCodeId(e.value)}
error={state.errors.qr_code_id}
@@ -744,9 +747,9 @@ const AssetCreate = (props: AssetProps) => {
setManufacturer(e.value)}
error={state.errors.manufacturer}
/>
@@ -761,7 +764,7 @@ const AssetCreate = (props: AssetProps) => {
{
const value = dayjs(event.value);
@@ -789,8 +792,8 @@ const AssetCreate = (props: AssetProps) => {
setSupportName(e.value)}
error={state.errors.support_name}
@@ -805,7 +808,7 @@ const AssetCreate = (props: AssetProps) => {
>
setSupportPhone(e.value)}
@@ -823,8 +826,8 @@ const AssetCreate = (props: AssetProps) => {
setSupportEmail(e.value)}
error={state.errors.support_email}
@@ -842,9 +845,9 @@ const AssetCreate = (props: AssetProps) => {
setVendorName(e.value)}
error={state.errors.vendor_name}
/>
@@ -859,7 +862,7 @@ const AssetCreate = (props: AssetProps) => {
setSerialNumber(e.value)}
error={state.errors.serial_number}
@@ -875,25 +878,26 @@ const AssetCreate = (props: AssetProps) => {
ref={fieldRef["last_serviced_on"]}
data-testid="asset-last-serviced-on-input"
>
- Last Serviced On
- {
if (
- dayjs(date).format("YYYY-MM-DD") >
+ dayjs(date.value).format("YYYY-MM-DD") >
new Date().toLocaleDateString("en-ca")
) {
Notification.Error({
msg: "Last Serviced date can't be in future",
});
} else {
- setLastServicedOn(dayjs(date).format("YYYY-MM-DD"));
+ setLastServicedOn(
+ dayjs(date.value).format("YYYY-MM-DD")
+ );
}
}}
- max={new Date()}
/>
{
>
setNotes(e.value)}
error={state.errors.notes}
@@ -929,13 +935,13 @@ const AssetCreate = (props: AssetProps) => {
/>
handleSubmit(e, false)}
- label={assetId ? "Update" : "Create Asset"}
+ label={assetId ? t("update") : t("create_asset")}
/>
{!assetId && (
handleSubmit(e, true)}
- label="Create & Add More"
+ label={t("create_add_more")}
/>
)}
diff --git a/src/Components/Facility/CentralNursingStation.tsx b/src/Components/Facility/CentralNursingStation.tsx
index 0d510d8459e..0eae504399c 100644
--- a/src/Components/Facility/CentralNursingStation.tsx
+++ b/src/Components/Facility/CentralNursingStation.tsx
@@ -168,11 +168,15 @@ export default function CentralNursingStation({ facilityId }: Props) {
Filter by Location
-
+
updateQuery({ location })}
+ setSelected={(location) => {
+ location
+ ? updateQuery({ location })
+ : removeFilter("location");
+ }}
selected={qParams.location}
showAll={false}
multiple={false}
@@ -180,16 +184,6 @@ export default function CentralNursingStation({ facilityId }: Props) {
errors=""
errorClassName="hidden"
/>
- {qParams.location && (
- removeFilter("location")}
- >
- Clear
-
- )}
(
{
{formatDateTime(itemData.admission_date)}
+ {itemData.is_readmission && (
+
+ )}
@@ -121,6 +131,7 @@ export const ConsultationCard = (props: ConsultationProps) => {
navigate(
diff --git a/src/Components/Facility/ConsultationDetails.tsx b/src/Components/Facility/ConsultationDetails.tsx
deleted file mode 100644
index e60f8f9c553..00000000000
--- a/src/Components/Facility/ConsultationDetails.tsx
+++ /dev/null
@@ -1,1316 +0,0 @@
-import { AssetBedModel, AssetClass, AssetData } from "../Assets/AssetTypes";
-import {
- CONSULTATION_TABS,
- DISCHARGE_REASONS,
- GENDER_TYPES,
- OptionsType,
- SYMPTOM_CHOICES,
-} from "../../Common/constants";
-import {
- BedModel,
- ConsultationModel,
- FacilityModel,
- ICD11DiagnosisModel,
-} from "./models";
-import {
- getConsultation,
- getPatient,
- getPermittedFacility,
- listAssetBeds,
-} from "../../Redux/actions";
-import { statusType, useAbortableEffect } from "../../Common/utils";
-import { lazy, useCallback, useEffect, useState } from "react";
-
-import { ABGPlots } from "./Consultations/ABGPlots";
-import ButtonV2 from "../Common/components/ButtonV2";
-import CareIcon from "../../CAREUI/icons/CareIcon";
-import Chip from "../../CAREUI/display/Chip";
-import { DailyRoundsList } from "./Consultations/DailyRoundsList";
-import { DialysisPlots } from "./Consultations/DialysisPlots";
-import DischargeModal from "./DischargeModal";
-import DischargeSummaryModal from "./DischargeSummaryModal";
-import DoctorVideoSlideover from "./DoctorVideoSlideover";
-import { Feed } from "./Consultations/Feed";
-import { FileUpload } from "../Patient/FileUpload";
-import HL7PatientVitalsMonitor from "../VitalsMonitor/HL7PatientVitalsMonitor";
-import InvestigationTab from "./Investigations/investigationsTab";
-import { make as Link } from "../Common/components/Link.bs";
-import { NeurologicalTable } from "./Consultations/NeurologicalTables";
-import { NonReadOnlyUsers } from "../../Utils/AuthorizeFor";
-import { NursingPlot } from "./Consultations/NursingPlot";
-import { NutritionPlots } from "./Consultations/NutritionPlots";
-import PatientInfoCard from "../Patient/PatientInfoCard";
-import { PatientModel } from "../Patient/models";
-import PrescriptionsTable from "../Medicine/PrescriptionsTable";
-import { PressureSoreDiagrams } from "./Consultations/PressureSoreDiagrams";
-import { PrimaryParametersPlot } from "./Consultations/PrimaryParametersPlot";
-import ReadMore from "../Common/components/Readmore";
-import VentilatorPatientVitalsMonitor from "../VitalsMonitor/VentilatorPatientVitalsMonitor";
-import { VentilatorPlot } from "./Consultations/VentilatorPlot";
-import { formatDate, formatDateTime, relativeTime } from "../../Utils/utils";
-import Page from "../Common/components/Page";
-import PatientPrivacyToggle from "../Patient/PatientPrivacyToggle";
-
-import { navigate } from "raviger";
-import { useDispatch } from "react-redux";
-import { useQueryParams } from "raviger";
-import { useTranslation } from "react-i18next";
-import { triggerGoal } from "../Common/Plausible";
-import useVitalsAspectRatioConfig from "../VitalsMonitor/useVitalsAspectRatioConfig";
-import useAuthUser from "../../Common/hooks/useAuthUser";
-import PrescriptionAdministrationsTable from "../Medicine/PrescriptionAdministrationsTable";
-
-const Loading = lazy(() => import("../Common/Loading"));
-const PageTitle = lazy(() => import("../Common/PageTitle"));
-const symptomChoices = [...SYMPTOM_CHOICES];
-
-export const ConsultationDetails = (props: any) => {
- const { t } = useTranslation();
- const { facilityId, patientId, consultationId } = props;
- const tab = props.tab.toUpperCase();
- const dispatch: any = useDispatch();
- const [isLoading, setIsLoading] = useState(false);
- const [showDoctors, setShowDoctors] = useState(false);
- const [qParams, _] = useQueryParams();
-
- const [consultationData, setConsultationData] = useState(
- {} as ConsultationModel
- );
- const [patientData, setPatientData] = useState({});
- const [openDischargeSummaryDialog, setOpenDischargeSummaryDialog] =
- useState(false);
- const [openDischargeDialog, setOpenDischargeDialog] = useState(false);
- const [showAutomatedRounds, setShowAutomatedRounds] = useState(true);
-
- const getPatientGender = (patientData: any) =>
- GENDER_TYPES.find((i) => i.id === patientData.gender)?.text;
-
- const getPatientAddress = (patientData: any) =>
- `${patientData.address},\n${patientData.ward_object?.name},\n${patientData.local_body_object?.name},\n${patientData.district_object?.name},\n${patientData.state_object?.name}`;
-
- const getPatientComorbidities = (patientData: any) => {
- if (patientData?.medical_history?.length) {
- const medHis = patientData.medical_history;
- return medHis.map((item: any) => item.disease).join(", ");
- } else {
- return "None";
- }
- };
-
- const [hl7SocketUrl, setHL7SocketUrl] = useState();
- const [ventilatorSocketUrl, setVentilatorSocketUrl] = useState();
- const [monitorBedData, setMonitorBedData] = useState();
- const [ventilatorBedData, setVentilatorBedData] = useState();
- const authUser = useAuthUser();
-
- useEffect(() => {
- if (
- !consultationData.facility ||
- !consultationData.current_bed?.bed_object.id
- )
- return;
-
- const fetchData = async () => {
- const [facilityRes, assetBedRes] = await Promise.all([
- dispatch(getPermittedFacility(consultationData.facility as any)),
- dispatch(
- listAssetBeds({
- facility: consultationData.facility as any,
- bed: consultationData.current_bed?.bed_object.id,
- })
- ),
- ]);
-
- const { middleware_address } = facilityRes.data as FacilityModel;
- const assetBeds = assetBedRes.data.results as AssetBedModel[];
-
- const monitorBedData = assetBeds.find(
- (i) => i.asset_object.asset_class === AssetClass.HL7MONITOR
- );
- setMonitorBedData(monitorBedData);
- const assetDataForMonitor = monitorBedData?.asset_object;
- const hl7Meta = assetDataForMonitor?.meta;
- const hl7Middleware = hl7Meta?.middleware_hostname || middleware_address;
- if (hl7Middleware && hl7Meta?.local_ip_address) {
- setHL7SocketUrl(
- `wss://${hl7Middleware}/observations/${hl7Meta.local_ip_address}`
- );
- }
-
- const consultationBedVentilator =
- consultationData?.current_bed?.assets_objects?.find(
- (i) => i.asset_class === AssetClass.VENTILATOR
- );
- let ventilatorBedData;
- if (consultationBedVentilator) {
- ventilatorBedData = {
- asset_object: consultationBedVentilator,
- bed_object: consultationData?.current_bed?.bed_object,
- } as AssetBedModel;
- } else {
- ventilatorBedData = assetBeds.find(
- (i) => i.asset_object.asset_class === AssetClass.VENTILATOR
- );
- }
- setVentilatorBedData(ventilatorBedData);
- const ventilatorMeta = ventilatorBedData?.asset_object?.meta;
- const ventilatorMiddleware =
- ventilatorMeta?.middleware_hostname || middleware_address;
- if (ventilatorMiddleware && ventilatorMeta?.local_ip_address) {
- setVentilatorSocketUrl(
- `wss://${ventilatorMiddleware}/observations/${ventilatorMeta?.local_ip_address}`
- );
- }
-
- if (
- !(hl7Middleware && hl7Meta?.local_ip_address) &&
- !(ventilatorMiddleware && ventilatorMeta?.local_ip_address)
- ) {
- setHL7SocketUrl(undefined);
- setVentilatorSocketUrl(undefined);
- }
- };
-
- fetchData();
- }, [consultationData]);
-
- const fetchData = useCallback(
- async (status: statusType) => {
- setIsLoading(true);
- const res = await dispatch(getConsultation(consultationId));
- if (!status.aborted) {
- if (res?.data) {
- const data: ConsultationModel = {
- ...res.data,
- symptoms_text: "",
- };
- if (res.data.symptoms?.length) {
- const symptoms = res.data.symptoms
- .filter((symptom: number) => symptom !== 9)
- .map((symptom: number) => {
- const option = symptomChoices.find((i) => i.id === symptom);
- return option ? option.text.toLowerCase() : symptom;
- });
- data.symptoms_text = symptoms.join(", ");
- }
- setConsultationData(data);
- const id = res.data.patient;
- const patientRes = await dispatch(getPatient({ id }));
- if (patientRes?.data) {
- const patientGender = getPatientGender(patientRes.data);
- const patientAddress = getPatientAddress(patientRes.data);
- const patientComorbidities = getPatientComorbidities(
- patientRes.data
- );
- const data = {
- ...patientRes.data,
- gender: patientGender,
- address: patientAddress,
- comorbidities: patientComorbidities,
- is_declared_positive: patientRes.data.is_declared_positive
- ? "Yes"
- : "No",
- is_vaccinated: patientData.is_vaccinated ? "Yes" : "No",
- };
- setPatientData(data);
- }
- } else {
- navigate("/not-found");
- }
- setIsLoading(false);
- }
- },
- [consultationId, dispatch, patientData.is_vaccinated]
- );
-
- useAbortableEffect((status: statusType) => {
- fetchData(status);
- triggerGoal("Patient Consultation Viewed", {
- facilityId: facilityId,
- consultationId: consultationId,
- userID: authUser.id,
- });
- }, []);
-
- const vitals = useVitalsAspectRatioConfig({
- default: undefined,
- md: 8 / 11,
- lg: 15 / 11,
- xl: 13 / 11,
- "2xl": 19 / 11,
- "3xl": 23 / 11,
- });
-
- if (isLoading) {
- return ;
- }
-
- const tabButtonClasses = (selected: boolean) =>
- `capitalize min-w-max-content cursor-pointer border-transparent text-gray-700 hover:text-gray-700 hover:border-gray-300 font-bold whitespace-nowrap ${
- selected === true ? "border-primary-500 text-primary-600 border-b-2" : ""
- }`;
-
- const ShowDiagnosis = ({
- diagnoses = [],
- label = "Diagnosis",
- nshow = 2,
- }: {
- diagnoses: ICD11DiagnosisModel[] | undefined;
- label: string;
- nshow?: number;
- }) => {
- const [showMore, setShowMore] = useState(false);
-
- return diagnoses.length ? (
-
- ) : null;
- };
-
- return (
-
- setOpenDischargeSummaryDialog(false)}
- />
-
- setOpenDischargeDialog(false)}
- consultationData={consultationData}
- />
-
-
-
-
-
-
-
-
- {consultationData.admitted_to && (
-
-
- Patient
- {consultationData.discharge_date
- ? " Discharged from"
- : " Admitted to"}
-
- {consultationData.admitted_to}
-
-
- {(consultationData.admission_date ??
- consultationData.discharge_date) && (
-
- {relativeTime(
- consultationData.discharge_date
- ? consultationData.discharge_date
- : consultationData.admission_date
- )}
-
- )}
-
- {consultationData.admission_date &&
- formatDateTime(consultationData.admission_date)}
- {consultationData.discharge_date &&
- ` - ${formatDateTime(consultationData.discharge_date)}`}
-
-
- )}
-
-
-
-
- {/*consultationData.other_symptoms && (
-
-
- Other Symptoms:{" "}
-
- {consultationData.other_symptoms}
-
- )*/}
-
-
-
-
-
- {consultationData.verified_by && (
-
-
- Verified By:{" "}
-
- {consultationData.verified_by}
-
-
- )}
-
-
- setOpenDischargeSummaryDialog(true)}>
-
- {t("discharge_summary")}
-
-
- setOpenDischargeDialog(true)}
- disabled={!!consultationData.discharge_date}
- >
-
- {t("discharge_from_care")}
-
-
-
-
-
-
- Created:
- {consultationData.created_date
- ? formatDateTime(consultationData.created_date)
- : "--:--"}{" "}
- |
-
- {consultationData.created_by && (
-
- {` ${consultationData.created_by.first_name} ${consultationData.created_by.last_name} `}
- {`@${consultationData.created_by.username} (${consultationData.created_by.user_type})`}
-
- )}
-
-
-
- Last Modified:
- {consultationData.modified_date
- ? formatDateTime(consultationData.modified_date)
- : "--:--"}{" "}
- |
-
- {consultationData.last_edited_by && (
-
- {` ${consultationData.last_edited_by.first_name} ${consultationData.last_edited_by.last_name} `}
- {`@${consultationData.last_edited_by.username} (${consultationData.last_edited_by.user_type})`}
-
- )}
-
-
-
-
-
-
-
-
-
-
-
-
- {tab === "UPDATES" && (
-
- {!consultationData.discharge_date &&
- hl7SocketUrl &&
- ventilatorSocketUrl && (
-
- )}
-
-
-
-
- {!consultationData.discharge_date &&
- ((hl7SocketUrl && !ventilatorSocketUrl) ||
- (!hl7SocketUrl && ventilatorSocketUrl)) && (
-
- {(hl7SocketUrl || ventilatorSocketUrl) && (
-
- {hl7SocketUrl && (
-
-
-
- )}
- {ventilatorSocketUrl && (
-
-
-
- )}
-
- )}
-
- )}
- {consultationData.discharge_date && (
-
-
-
- Discharge Information
-
-
-
- Reason {" - "}
-
- {DISCHARGE_REASONS.find(
- (d) =>
- d.id === consultationData.discharge_reason
- )?.text ?? "--"}
-
-
- {consultationData.discharge_reason === "REF" && (
-
- Referred Facility {" - "}
-
- {consultationData.referred_to_external ||
- consultationData.referred_to_object?.name ||
- "--"}
-
-
- )}
- {consultationData.discharge_reason === "REC" && (
-
-
- Discharge Date {" - "}
-
- {consultationData.discharge_date
- ? formatDate(
- consultationData.discharge_date
- )
- : "--/--/----"}
-
-
-
- Advice {" - "}
-
- {consultationData.discharge_notes ?? "--"}
-
-
-
-
-
-
- )}
- {consultationData.discharge_reason === "EXP" && (
-
-
- Date of Death {" - "}
-
- {consultationData.death_datetime
- ? formatDateTime(
- consultationData.death_datetime
- )
- : "--:--"}
-
-
-
- Cause of death {" - "}
-
- {consultationData.discharge_notes ?? "--"}
-
-
-
- Confirmed By {" - "}
-
- {consultationData.death_confirmed_doctor ??
- "--"}
-
-
-
- )}
- {["REF", "LAMA"].includes(
- consultationData.discharge_reason ?? ""
- ) && (
-
-
- Discharge Date {" - "}
-
- {consultationData.discharge_date
- ? formatDate(
- consultationData.discharge_date
- )
- : "--/--/----"}
-
-
-
- Notes {" - "}
-
- {consultationData.discharge_notes ?? "--"}
-
-
-
- )}
-
-
-
- )}
- {consultationData.symptoms_text && (
-
-
-
- Symptoms
-
-
-
- Last Daily Update
-
- {consultationData.last_daily_round
- ?.additional_symptoms && (
- <>
-
- {consultationData.last_daily_round?.additional_symptoms.map(
- (symptom: any, index: number) => (
- choice.id === symptom
- )?.text ?? "Err. Unknown"
- }
- size="small"
- />
- )
- )}
-
- {consultationData.last_daily_round
- ?.other_symptoms && (
-
-
- Other Symptoms:
-
- {
- consultationData.last_daily_round
- ?.other_symptoms
- }
-
- )}
-
- from{" "}
- {formatDate(
- consultationData.last_daily_round.created_at
- )}
-
- >
- )}
-
-
- Consultation Update
-
-
- {consultationData.symptoms?.map(
- (symptom, index) => (
- choice.id === symptom
- )?.text ?? "Err. Unknown"
- }
- size="small"
- />
- )
- )}
-
- {consultationData.other_symptoms && (
-
-
- Other Symptoms:
-
- {consultationData.other_symptoms}
-
- )}
-
- from{" "}
- {consultationData.symptoms_onset_date
- ? formatDate(consultationData.symptoms_onset_date)
- : "--/--/----"}
-
-
-
-
- )}
-
- {consultationData.history_of_present_illness && (
-
-
-
- History of Present Illness
-
-
-
-
-
-
- )}
-
- {consultationData.examination_details && (
-
-
-
- Examination details and Clinical conditions:{" "}
-
-
-
-
-
-
- )}
- {consultationData.treatment_plan && (
-
-
-
- Treatment Summary
-
-
-
-
-
-
- )}
- {consultationData.consultation_notes && (
-
-
-
- General Instructions
-
-
-
-
-
-
- )}
-
- {(consultationData.operation ??
- consultationData.special_instruction) && (
-
-
-
- Notes
-
-
- {consultationData.operation && (
-
- Operation
-
-
- )}
-
- {consultationData.special_instruction && (
-
- Special Instruction
-
-
- )}
-
-
-
- )}
-
- {consultationData.procedure &&
- consultationData.procedure.length > 0 && (
-
-
-
-
-
-
- Procedure
- |
-
- Notes
- |
-
- Repetitive
- |
-
- Time / Frequency
- |
-
-
-
- {consultationData.procedure?.map(
- (procedure, index) => (
-
-
- {procedure.procedure}
- |
-
- {procedure.notes}
- |
-
- {procedure.repetitive ? "Yes" : "No"}
- |
-
- {procedure.repetitive
- ? procedure.frequency
- : formatDateTime(String(procedure.time))}
- |
-
- )
- )}
-
-
-
-
- )}
- {consultationData.intubation_start_date && (
-
-
-
- Date/Size/LL:{" "}
-
-
-
- Intubation Date{" - "}
-
- {formatDateTime(
- consultationData.intubation_start_date
- )}
-
-
-
- Extubation Date{" - "}
-
- {consultationData.intubation_end_date &&
- formatDateTime(
- consultationData.intubation_end_date
- )}
-
-
-
- ETT/TT (mmid){" - "}
-
- {consultationData.ett_tt}
-
-
-
- Cuff Pressure (mmhg){" - "}
-
- {consultationData.cuff_pressure}
-
-
-
-
-
- )}
-
- {consultationData.lines?.length > 0 && (
-
-
-
- Lines and Catheters
-
-
- {consultationData.lines?.map(
- (line: any, idx: number) => (
-
- {line.type}
-
- Details:
-
- {line.other_type}
-
-
- Insertion Date:{" "}
-
- {formatDateTime(line.start_date)}
-
-
-
- Site/Level of Fixation:
-
- {line.site}
-
-
-
- )
- )}
-
-
-
- )}
-
-
-
-
- Body Details
-
-
-
- Gender {" - "}
-
- {patientData.gender ?? "-"}
-
-
-
- Age {" - "}
-
- {patientData.age ?? "-"}
-
-
-
- Weight {" - "}
-
- {consultationData.weight ?? "-"} Kg
-
-
-
- Height {" - "}
-
- {consultationData.height ?? "-"} cm
-
-
-
- Body Surface Area {" - "}
-
- {Math.sqrt(
- (Number(consultationData.weight) *
- Number(consultationData.height)) /
- 3600
- ).toFixed(2)}{" "}
- m2
-
-
-
- Blood Group {" - "}
-
- {patientData.blood_group ?? "-"}
-
-
-
-
-
-
-
-
-
-
- setShowAutomatedRounds((s) => !s)}
- />
-
-
-
-
-
-
-
- )}
- {tab === "FEED" && (
-
- )}
- {tab === "SUMMARY" && (
-
- )}
- {tab === "MEDICINES" && (
-
- )}
- {tab === "FILES" && (
-
-
-
- )}
-
- {tab === "ABG" && (
-
- )}
- {tab === "NURSING" && (
-
- )}
- {tab === "NEUROLOGICAL_MONITORING" && (
-
- )}
- {tab === "VENTILATOR" && (
-
- )}
- {tab === "NUTRITION" && (
-
- )}
- {tab === "PRESSURE_SORE" && (
-
- )}
- {tab === "DIALYSIS" && (
-
- )}
- {tab === "INVESTIGATIONS" && (
-
-
-
-
-
- navigate(
- `/facility/${facilityId}/patient/${patientId}/consultation/${consultationId}/investigation/`
- )
- }
- >
-
- {t("log_lab_results")}
-
-
-
-
-
- )}
-
-
-
-
- );
-};
diff --git a/src/Components/Facility/ConsultationDetails/ConsultationABGTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationABGTab.tsx
new file mode 100644
index 00000000000..cf0ce7bd6af
--- /dev/null
+++ b/src/Components/Facility/ConsultationDetails/ConsultationABGTab.tsx
@@ -0,0 +1,22 @@
+import { lazy } from "react";
+import { ConsultationTabProps } from "./index";
+import { ABGPlots } from "../Consultations/ABGPlots";
+
+const PageTitle = lazy(() => import("../../Common/PageTitle"));
+
+export const ConsultationABGTab = (props: ConsultationTabProps) => {
+ return (
+
+ );
+};
diff --git a/src/Components/Facility/ConsultationDetails/ConsultationDialysisTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationDialysisTab.tsx
new file mode 100644
index 00000000000..c3515a80953
--- /dev/null
+++ b/src/Components/Facility/ConsultationDetails/ConsultationDialysisTab.tsx
@@ -0,0 +1,14 @@
+import { lazy } from "react";
+import { ConsultationTabProps } from "./index";
+import { DialysisPlots } from "../Consultations/DialysisPlots";
+
+const PageTitle = lazy(() => import("../../Common/PageTitle"));
+
+export const ConsultationDialysisTab = (props: ConsultationTabProps) => {
+ return (
+
+ );
+};
diff --git a/src/Components/Facility/ConsultationDetails/ConsultationFeedTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationFeedTab.tsx
new file mode 100644
index 00000000000..f4fbc08331c
--- /dev/null
+++ b/src/Components/Facility/ConsultationDetails/ConsultationFeedTab.tsx
@@ -0,0 +1,22 @@
+import { lazy } from "react";
+import { Feed } from "../Consultations/Feed";
+import { ConsultationTabProps } from "./index";
+
+const PageTitle = lazy(() => import("../../Common/PageTitle"));
+
+export const ConsultationFeedTab = (props: ConsultationTabProps) => {
+ return (
+
+ );
+};
diff --git a/src/Components/Facility/ConsultationDetails/ConsultationFilesTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationFilesTab.tsx
new file mode 100644
index 00000000000..754d4bf7b86
--- /dev/null
+++ b/src/Components/Facility/ConsultationDetails/ConsultationFilesTab.tsx
@@ -0,0 +1,18 @@
+import { ConsultationTabProps } from "./index";
+import { FileUpload } from "../../Patient/FileUpload";
+
+export const ConsultationFilesTab = (props: ConsultationTabProps) => {
+ return (
+
+
+
+ );
+};
diff --git a/src/Components/Facility/ConsultationDetails/ConsultationInvestigationsTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationInvestigationsTab.tsx
new file mode 100644
index 00000000000..69b11a30ed5
--- /dev/null
+++ b/src/Components/Facility/ConsultationDetails/ConsultationInvestigationsTab.tsx
@@ -0,0 +1,39 @@
+import { lazy } from "react";
+import { ConsultationTabProps } from "./index";
+import { NonReadOnlyUsers } from "../../../Utils/AuthorizeFor";
+import ButtonV2 from "../../Common/components/ButtonV2";
+import { navigate } from "raviger";
+import CareIcon from "../../../CAREUI/icons/CareIcon";
+import InvestigationTab from "../Investigations/investigationsTab";
+import { t } from "i18next";
+
+const PageTitle = lazy(() => import("../../Common/PageTitle"));
+export const ConsultationInvestigationsTab = (props: ConsultationTabProps) => {
+ return (
+
+
+
+
+
+ navigate(
+ `/facility/${props.facilityId}/patient/${props.patientId}/consultation/${props.consultationId}/investigation/`
+ )
+ }
+ >
+
+ {t("log_lab_results")}
+
+
+
+
+
+ );
+};
diff --git a/src/Components/Facility/ConsultationDetails/ConsultationMedicinesTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationMedicinesTab.tsx
new file mode 100644
index 00000000000..e1e72c2f936
--- /dev/null
+++ b/src/Components/Facility/ConsultationDetails/ConsultationMedicinesTab.tsx
@@ -0,0 +1,20 @@
+import { ConsultationTabProps } from "./index";
+import PageTitle from "../../Common/PageHeadTitle";
+import MedicineAdministrationSheet from "../../Medicine/MedicineAdministrationSheet";
+
+export const ConsultationMedicinesTab = (props: ConsultationTabProps) => {
+ return (
+
+ {/* eslint-disable-next-line i18next/no-literal-string */}
+
+
+
+
+ );
+};
diff --git a/src/Components/Facility/ConsultationDetails/ConsultationNeurologicalMonitoringTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationNeurologicalMonitoringTab.tsx
new file mode 100644
index 00000000000..85f34f0ec63
--- /dev/null
+++ b/src/Components/Facility/ConsultationDetails/ConsultationNeurologicalMonitoringTab.tsx
@@ -0,0 +1,24 @@
+import { lazy } from "react";
+import { NeurologicalTable } from "../Consultations/NeurologicalTables";
+import { ConsultationTabProps } from "./index";
+
+const PageTitle = lazy(() => import("../../Common/PageTitle"));
+
+export const ConsultationNeurologicalMonitoringTab = (
+ props: ConsultationTabProps
+) => {
+ return (
+
+ );
+};
diff --git a/src/Components/Facility/ConsultationDetails/ConsultationNursingTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationNursingTab.tsx
new file mode 100644
index 00000000000..721ee18d13e
--- /dev/null
+++ b/src/Components/Facility/ConsultationDetails/ConsultationNursingTab.tsx
@@ -0,0 +1,18 @@
+import { lazy } from "react";
+import { ConsultationTabProps } from "./index";
+import { NursingPlot } from "../Consultations/NursingPlot";
+
+const PageTitle = lazy(() => import("../../Common/PageTitle"));
+
+export const ConsultationNursingTab = (props: ConsultationTabProps) => {
+ return (
+
+ );
+};
diff --git a/src/Components/Facility/ConsultationDetails/ConsultationNutritionTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationNutritionTab.tsx
new file mode 100644
index 00000000000..74e250f0577
--- /dev/null
+++ b/src/Components/Facility/ConsultationDetails/ConsultationNutritionTab.tsx
@@ -0,0 +1,18 @@
+import { lazy } from "react";
+import { ConsultationTabProps } from "./index";
+import { NutritionPlots } from "../Consultations/NutritionPlots";
+
+const PageTitle = lazy(() => import("../../Common/PageTitle"));
+
+export const ConsultationNutritionTab = (props: ConsultationTabProps) => {
+ return (
+
+ );
+};
diff --git a/src/Components/Facility/ConsultationDetails/ConsultationPressureSoreTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationPressureSoreTab.tsx
new file mode 100644
index 00000000000..05327ca6870
--- /dev/null
+++ b/src/Components/Facility/ConsultationDetails/ConsultationPressureSoreTab.tsx
@@ -0,0 +1,14 @@
+import { lazy } from "react";
+import { ConsultationTabProps } from "./index";
+import { PressureSoreDiagrams } from "../Consultations/PressureSoreDiagrams";
+
+const PageTitle = lazy(() => import("../../Common/PageTitle"));
+
+export const ConsultationPressureSoreTab = (props: ConsultationTabProps) => {
+ return (
+
+ );
+};
diff --git a/src/Components/Facility/ConsultationDetails/ConsultationSummaryTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationSummaryTab.tsx
new file mode 100644
index 00000000000..69d20dd64c9
--- /dev/null
+++ b/src/Components/Facility/ConsultationDetails/ConsultationSummaryTab.tsx
@@ -0,0 +1,22 @@
+import { lazy } from "react";
+import { ConsultationTabProps } from "./index";
+import { PrimaryParametersPlot } from "../Consultations/PrimaryParametersPlot";
+
+const PageTitle = lazy(() => import("../../Common/PageTitle"));
+
+export const ConsultationSummaryTab = (props: ConsultationTabProps) => {
+ return (
+
+ );
+};
diff --git a/src/Components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx
new file mode 100644
index 00000000000..736a7c81575
--- /dev/null
+++ b/src/Components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx
@@ -0,0 +1,701 @@
+import { lazy, useEffect, useState } from "react";
+import { ConsultationTabProps } from "./index";
+import { AssetBedModel, AssetClass, AssetData } from "../../Assets/AssetTypes";
+import { useDispatch } from "react-redux";
+import { getPermittedFacility, listAssetBeds } from "../../../Redux/actions";
+import { BedModel, FacilityModel } from "../models";
+import HL7PatientVitalsMonitor from "../../VitalsMonitor/HL7PatientVitalsMonitor";
+import VentilatorPatientVitalsMonitor from "../../VitalsMonitor/VentilatorPatientVitalsMonitor";
+import useVitalsAspectRatioConfig from "../../VitalsMonitor/useVitalsAspectRatioConfig";
+import { DISCHARGE_REASONS, SYMPTOM_CHOICES } from "../../../Common/constants";
+import PrescriptionsTable from "../../Medicine/PrescriptionsTable";
+import Chip from "../../../CAREUI/display/Chip";
+import { formatAge, formatDate, formatDateTime } from "../../../Utils/utils";
+import ReadMore from "../../Common/components/Readmore";
+import { DailyRoundsList } from "../Consultations/DailyRoundsList";
+
+const PageTitle = lazy(() => import("../../Common/PageTitle"));
+
+export const ConsultationUpdatesTab = (props: ConsultationTabProps) => {
+ const dispatch: any = useDispatch();
+ const [showAutomatedRounds, setShowAutomatedRounds] = useState(true);
+ const [hl7SocketUrl, setHL7SocketUrl] = useState();
+ const [ventilatorSocketUrl, setVentilatorSocketUrl] = useState();
+ const [monitorBedData, setMonitorBedData] = useState();
+ const [ventilatorBedData, setVentilatorBedData] = useState();
+
+ const vitals = useVitalsAspectRatioConfig({
+ default: undefined,
+ md: 8 / 11,
+ lg: 15 / 11,
+ xl: 13 / 11,
+ "2xl": 19 / 11,
+ "3xl": 23 / 11,
+ });
+
+ useEffect(() => {
+ if (
+ !props.consultationData.facility ||
+ !props.consultationData.current_bed?.bed_object.id
+ )
+ return;
+
+ const fetchData = async () => {
+ const [facilityRes, assetBedRes] = await Promise.all([
+ dispatch(getPermittedFacility(props.consultationData.facility as any)),
+ dispatch(
+ listAssetBeds({
+ facility: props.consultationData.facility as any,
+ bed: props.consultationData.current_bed?.bed_object.id,
+ })
+ ),
+ ]);
+
+ const { middleware_address } = facilityRes.data as FacilityModel;
+ const assetBeds = assetBedRes?.data?.results as AssetBedModel[];
+
+ const monitorBedData = assetBeds?.find(
+ (i) => i.asset_object?.asset_class === AssetClass.HL7MONITOR
+ );
+ setMonitorBedData(monitorBedData);
+ const assetDataForMonitor = monitorBedData?.asset_object;
+ const hl7Meta = assetDataForMonitor?.meta;
+ const hl7Middleware = hl7Meta?.middleware_hostname || middleware_address;
+ if (hl7Middleware && hl7Meta?.local_ip_address) {
+ setHL7SocketUrl(
+ `wss://${hl7Middleware}/observations/${hl7Meta.local_ip_address}`
+ );
+ }
+
+ const consultationBedVentilator =
+ props.consultationData?.current_bed?.assets_objects?.find(
+ (i) => i.asset_class === AssetClass.VENTILATOR
+ );
+ let ventilatorBedData;
+ if (consultationBedVentilator) {
+ ventilatorBedData = {
+ asset_object: consultationBedVentilator,
+ bed_object: props.consultationData?.current_bed?.bed_object,
+ } as AssetBedModel;
+ } else {
+ ventilatorBedData = assetBeds?.find(
+ (i) => i.asset_object.asset_class === AssetClass.VENTILATOR
+ );
+ }
+ setVentilatorBedData(ventilatorBedData);
+ const ventilatorMeta = ventilatorBedData?.asset_object?.meta;
+ const ventilatorMiddleware =
+ ventilatorMeta?.middleware_hostname || middleware_address;
+ if (ventilatorMiddleware && ventilatorMeta?.local_ip_address) {
+ setVentilatorSocketUrl(
+ `wss://${ventilatorMiddleware}/observations/${ventilatorMeta?.local_ip_address}`
+ );
+ }
+
+ if (
+ !(hl7Middleware && hl7Meta?.local_ip_address) &&
+ !(ventilatorMiddleware && ventilatorMeta?.local_ip_address)
+ ) {
+ setHL7SocketUrl(undefined);
+ setVentilatorSocketUrl(undefined);
+ }
+ };
+
+ fetchData();
+ }, [props.consultationData]);
+
+ return (
+
+ {!props.consultationData.discharge_date &&
+ hl7SocketUrl &&
+ ventilatorSocketUrl && (
+
+ )}
+
+
+
+
+ {!props.consultationData.discharge_date &&
+ ((hl7SocketUrl && !ventilatorSocketUrl) ||
+ (!hl7SocketUrl && ventilatorSocketUrl)) && (
+
+ {(hl7SocketUrl || ventilatorSocketUrl) && (
+
+ {hl7SocketUrl && (
+
+
+
+ )}
+ {ventilatorSocketUrl && (
+
+
+
+ )}
+
+ )}
+
+ )}
+ {props.consultationData.discharge_date && (
+
+
+
+ Discharge Information
+
+
+
+ Reason {" - "}
+
+ {DISCHARGE_REASONS.find(
+ (d) =>
+ d.id === props.consultationData.discharge_reason
+ )?.text ?? "--"}
+
+
+ {props.consultationData.discharge_reason === "REF" && (
+
+ Referred Facility {" - "}
+
+ {props.consultationData.referred_to_external ||
+ props.consultationData.referred_to_object?.name ||
+ "--"}
+
+
+ )}
+ {props.consultationData.discharge_reason === "REC" && (
+
+
+ Discharge Date {" - "}
+
+ {props.consultationData.discharge_date
+ ? formatDate(
+ props.consultationData.discharge_date
+ )
+ : "--/--/----"}
+
+
+
+ Advice {" - "}
+
+ {props.consultationData.discharge_notes ?? "--"}
+
+
+
+
+
+
+ )}
+ {props.consultationData.discharge_reason === "EXP" && (
+
+
+ Date of Death {" - "}
+
+ {props.consultationData.death_datetime
+ ? formatDateTime(
+ props.consultationData.death_datetime
+ )
+ : "--:--"}
+
+
+
+ Cause of death {" - "}
+
+ {props.consultationData.discharge_notes ?? "--"}
+
+
+
+ Confirmed By {" - "}
+
+ {props.consultationData.death_confirmed_doctor ??
+ "--"}
+
+
+
+ )}
+ {["REF", "LAMA"].includes(
+ props.consultationData.discharge_reason ?? ""
+ ) && (
+
+
+ Discharge Date {" - "}
+
+ {props.consultationData.discharge_date
+ ? formatDate(
+ props.consultationData.discharge_date
+ )
+ : "--/--/----"}
+
+
+
+ Notes {" - "}
+
+ {props.consultationData.discharge_notes ?? "--"}
+
+
+
+ )}
+
+
+
+ )}
+ {props.consultationData.symptoms_text && (
+
+
+
+ Symptoms
+
+
+
+ Last Daily Update
+
+ {props.consultationData.last_daily_round
+ ?.additional_symptoms && (
+ <>
+
+ {props.consultationData.last_daily_round?.additional_symptoms.map(
+ (symptom: any, index: number) => (
+ choice.id === symptom
+ )?.text ?? "Err. Unknown"
+ }
+ size="small"
+ />
+ )
+ )}
+
+ {props.consultationData.last_daily_round
+ ?.other_symptoms && (
+
+
+ Other Symptoms:
+
+ {
+ props.consultationData.last_daily_round
+ ?.other_symptoms
+ }
+
+ )}
+
+ from{" "}
+ {formatDate(
+ props.consultationData.last_daily_round.created_at
+ )}
+
+ >
+ )}
+
+
+ Consultation Update
+
+
+ {props.consultationData.symptoms?.map(
+ (symptom, index) => (
+ choice.id === symptom
+ )?.text ?? "Err. Unknown"
+ }
+ size="small"
+ />
+ )
+ )}
+
+ {props.consultationData.other_symptoms && (
+
+
+ Other Symptoms:
+
+ {props.consultationData.other_symptoms}
+
+ )}
+
+ from{" "}
+ {props.consultationData.symptoms_onset_date
+ ? formatDate(props.consultationData.symptoms_onset_date)
+ : "--/--/----"}
+
+
+
+
+ )}
+
+ {props.consultationData.history_of_present_illness && (
+
+
+
+ History of Present Illness
+
+
+
+
+
+
+ )}
+
+ {props.consultationData.examination_details && (
+
+
+
+ Examination details and Clinical conditions:{" "}
+
+
+
+
+
+
+ )}
+ {props.consultationData.treatment_plan && (
+
+
+
+ Treatment Summary
+
+
+
+
+
+
+ )}
+ {props.consultationData.consultation_notes && (
+
+
+
+ General Instructions
+
+
+
+
+
+
+ )}
+
+ {(props.consultationData.operation ??
+ props.consultationData.special_instruction) && (
+
+
+
+ Notes
+
+
+ {props.consultationData.operation && (
+
+ Operation
+
+
+ )}
+
+ {props.consultationData.special_instruction && (
+
+ Special Instruction
+
+
+ )}
+
+
+
+ )}
+
+ {props.consultationData.procedure &&
+ props.consultationData.procedure.length > 0 && (
+
+
+
+
+
+
+ Procedure
+ |
+
+ Notes
+ |
+
+ Repetitive
+ |
+
+ Time / Frequency
+ |
+
+
+
+ {props.consultationData.procedure?.map(
+ (procedure, index) => (
+
+
+ {procedure.procedure}
+ |
+
+ {procedure.notes}
+ |
+
+ {procedure.repetitive ? "Yes" : "No"}
+ |
+
+ {procedure.repetitive
+ ? procedure.frequency
+ : formatDateTime(String(procedure.time))}
+ |
+
+ )
+ )}
+
+
+
+
+ )}
+ {props.consultationData.intubation_start_date && (
+
+
+
+ Date/Size/LL:{" "}
+
+
+
+ Intubation Date{" - "}
+
+ {formatDateTime(
+ props.consultationData.intubation_start_date
+ )}
+
+
+
+ Extubation Date{" - "}
+
+ {props.consultationData.intubation_end_date &&
+ formatDateTime(
+ props.consultationData.intubation_end_date
+ )}
+
+
+
+ ETT/TT (mmid){" - "}
+
+ {props.consultationData.ett_tt}
+
+
+
+ Cuff Pressure (mmhg){" - "}
+
+ {props.consultationData.cuff_pressure}
+
+
+
+
+
+ )}
+
+ {props.consultationData.lines?.length > 0 && (
+
+
+
+ Lines and Catheters
+
+
+ {props.consultationData.lines?.map(
+ (line: any, idx: number) => (
+
+ {line.type}
+
+ Details:
+
+ {line.other_type}
+
+
+ Insertion Date:{" "}
+
+ {formatDateTime(line.start_date)}
+
+
+
+ Site/Level of Fixation:
+
+ {line.site}
+
+
+
+ )
+ )}
+
+
+
+ )}
+
+
+
+
+ Body Details
+
+
+
+ Gender {" - "}
+
+ {props.patientData.gender ?? "-"}
+
+
+
+ Age {" - "}
+
+ {props.patientData.age !== undefined // 0 is a valid age, so we need to check for undefined
+ ? formatAge(
+ props.patientData.age,
+ props.patientData.date_of_birth
+ )
+ : "-"}
+
+
+
+ Weight {" - "}
+
+ {props.consultationData.weight ?? "-"} Kg
+
+
+
+ Height {" - "}
+
+ {props.consultationData.height ?? "-"} cm
+
+
+
+ Body Surface Area {" - "}
+
+ {Math.sqrt(
+ (Number(props.consultationData.weight) *
+ Number(props.consultationData.height)) /
+ 3600
+ ).toFixed(2)}{" "}
+ m2
+
+
+
+ Blood Group {" - "}
+
+ {props.patientData.blood_group ?? "-"}
+
+
+
+
+
+
+
+
+
+
+ setShowAutomatedRounds((s) => !s)}
+ />
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/Components/Facility/ConsultationDetails/ConsultationVentilatorTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationVentilatorTab.tsx
new file mode 100644
index 00000000000..d14b54cc096
--- /dev/null
+++ b/src/Components/Facility/ConsultationDetails/ConsultationVentilatorTab.tsx
@@ -0,0 +1,22 @@
+import { lazy } from "react";
+import { ConsultationTabProps } from "./index";
+import { VentilatorPlot } from "../Consultations/VentilatorPlot";
+
+const PageTitle = lazy(() => import("../../Common/PageTitle"));
+
+export const ConsultationVentilatorTab = (props: ConsultationTabProps) => {
+ return (
+
+ );
+};
diff --git a/src/Components/Facility/ConsultationDetails/index.tsx b/src/Components/Facility/ConsultationDetails/index.tsx
new file mode 100644
index 00000000000..6a5a5da242c
--- /dev/null
+++ b/src/Components/Facility/ConsultationDetails/index.tsx
@@ -0,0 +1,545 @@
+import {
+ CONSULTATION_TABS,
+ GENDER_TYPES,
+ OptionsType,
+ SYMPTOM_CHOICES,
+} from "../../../Common/constants";
+import { ConsultationModel } from "../models";
+import {
+ getConsultation,
+ getPatient,
+ listAssetBeds,
+ listShiftRequests,
+} from "../../../Redux/actions";
+import { statusType, useAbortableEffect } from "../../../Common/utils";
+import { lazy, useCallback, useState } from "react";
+import ButtonV2 from "../../Common/components/ButtonV2";
+import CareIcon from "../../../CAREUI/icons/CareIcon";
+import DischargeModal from "../DischargeModal";
+import DischargeSummaryModal from "../DischargeSummaryModal";
+import DoctorVideoSlideover from "../DoctorVideoSlideover";
+import { make as Link } from "../../Common/components/Link.bs";
+import PatientInfoCard from "../../Patient/PatientInfoCard";
+import { PatientModel } from "../../Patient/models";
+import { formatDateTime, relativeTime } from "../../../Utils/utils";
+
+import { navigate } from "raviger";
+import { useDispatch } from "react-redux";
+import { useQueryParams } from "raviger";
+import { useTranslation } from "react-i18next";
+import { triggerGoal } from "../../../Integrations/Plausible";
+import useAuthUser from "../../../Common/hooks/useAuthUser";
+import { ConsultationUpdatesTab } from "./ConsultationUpdatesTab";
+import { ConsultationABGTab } from "./ConsultationABGTab";
+import { ConsultationNursingTab } from "./ConsultationNursingTab";
+import { ConsultationFeedTab } from "./ConsultationFeedTab";
+import { ConsultationSummaryTab } from "./ConsultationSummaryTab";
+import { ConsultationFilesTab } from "./ConsultationFilesTab";
+import { ConsultationMedicinesTab } from "./ConsultationMedicinesTab";
+import { ConsultationInvestigationsTab } from "./ConsultationInvestigationsTab";
+import { ConsultationVentilatorTab } from "./ConsultationVentilatorTab";
+import { ConsultationPressureSoreTab } from "./ConsultationPressureSoreTab";
+import { ConsultationDialysisTab } from "./ConsultationDialysisTab";
+import { ConsultationNeurologicalMonitoringTab } from "./ConsultationNeurologicalMonitoringTab";
+import { ConsultationNutritionTab } from "./ConsultationNutritionTab";
+import LegacyDiagnosesList from "../../Diagnosis/LegacyDiagnosesList";
+
+const Loading = lazy(() => import("../../Common/Loading"));
+const PageTitle = lazy(() => import("../../Common/PageTitle"));
+const symptomChoices = [...SYMPTOM_CHOICES];
+
+export interface ConsultationTabProps {
+ consultationId: string;
+ facilityId: string;
+ patientId: string;
+ consultationData: ConsultationModel;
+ patientData: PatientModel;
+}
+
+const TABS = {
+ UPDATES: ConsultationUpdatesTab,
+ FEED: ConsultationFeedTab,
+ SUMMARY: ConsultationSummaryTab,
+ MEDICINES: ConsultationMedicinesTab,
+ FILES: ConsultationFilesTab,
+ INVESTIGATIONS: ConsultationInvestigationsTab,
+ ABG: ConsultationABGTab,
+ NURSING: ConsultationNursingTab,
+ NEUROLOGICAL_MONITORING: ConsultationNeurologicalMonitoringTab,
+ VENTILATOR: ConsultationVentilatorTab,
+ NUTRITION: ConsultationNutritionTab,
+ PRESSURE_SORE: ConsultationPressureSoreTab,
+ DIALYSIS: ConsultationDialysisTab,
+};
+
+export const ConsultationDetails = (props: any) => {
+ const { t } = useTranslation();
+ const { facilityId, patientId, consultationId } = props;
+ const tab = props.tab.toUpperCase() as keyof typeof TABS;
+ const dispatch: any = useDispatch();
+ const [isLoading, setIsLoading] = useState(false);
+ const [showDoctors, setShowDoctors] = useState(false);
+ const [qParams, _] = useQueryParams();
+
+ const [consultationData, setConsultationData] = useState(
+ {} as ConsultationModel
+ );
+ const [patientData, setPatientData] = useState({});
+ const [activeShiftingData, setActiveShiftingData] = useState>([]);
+ const [openDischargeSummaryDialog, setOpenDischargeSummaryDialog] =
+ useState(false);
+ const [openDischargeDialog, setOpenDischargeDialog] = useState(false);
+ const [isCameraAttached, setIsCameraAttached] = useState(false);
+
+ const getPatientGender = (patientData: any) =>
+ GENDER_TYPES.find((i) => i.id === patientData.gender)?.text;
+
+ const getPatientAddress = (patientData: any) =>
+ `${patientData.address},\n${patientData.ward_object?.name},\n${patientData.local_body_object?.name},\n${patientData.district_object?.name},\n${patientData.state_object?.name}`;
+
+ const getPatientComorbidities = (patientData: any) => {
+ if (patientData?.medical_history?.length) {
+ const medHis = patientData.medical_history;
+ return medHis.map((item: any) => item.disease).join(", ");
+ } else {
+ return "None";
+ }
+ };
+
+ const authUser = useAuthUser();
+
+ const fetchData = useCallback(
+ async (status: statusType) => {
+ setIsLoading(true);
+ const res = await dispatch(getConsultation(consultationId));
+ if (!status.aborted) {
+ if (res?.data) {
+ const data: ConsultationModel = {
+ ...res.data,
+ symptoms_text: "",
+ };
+ if (res.data.symptoms?.length) {
+ const symptoms = res.data.symptoms
+ .filter((symptom: number) => symptom !== 9)
+ .map((symptom: number) => {
+ const option = symptomChoices.find((i) => i.id === symptom);
+ return option ? option.text.toLowerCase() : symptom;
+ });
+ data.symptoms_text = symptoms.join(", ");
+ }
+ setConsultationData(data);
+ const assetRes = await dispatch(
+ listAssetBeds({
+ bed: data?.current_bed?.bed_object?.id,
+ })
+ );
+ const isCameraAttachedRes = assetRes.data.results.some(
+ (asset: { asset_object: { asset_class: string } }) => {
+ return asset?.asset_object?.asset_class === "ONVIF";
+ }
+ );
+ setIsCameraAttached(isCameraAttachedRes);
+ const id = res.data.patient;
+ const patientRes = await dispatch(getPatient({ id }));
+ if (patientRes?.data) {
+ const patientGender = getPatientGender(patientRes.data);
+ const patientAddress = getPatientAddress(patientRes.data);
+ const patientComorbidities = getPatientComorbidities(
+ patientRes.data
+ );
+ const data = {
+ ...patientRes.data,
+ gender: patientGender,
+ address: patientAddress,
+ comorbidities: patientComorbidities,
+ is_declared_positive: patientRes.data.is_declared_positive
+ ? "Yes"
+ : "No",
+ is_vaccinated: patientData.is_vaccinated ? "Yes" : "No",
+ };
+ setPatientData(data);
+ }
+
+ // Get shifting data
+ const shiftingRes = await dispatch(
+ listShiftRequests({ patient: id }, "shift-list-call")
+ );
+ if (shiftingRes?.data?.results) {
+ const data = shiftingRes.data.results;
+ setActiveShiftingData(data);
+ }
+ } else {
+ navigate("/not-found");
+ }
+ setIsLoading(false);
+ }
+ },
+ [consultationId, dispatch, patientData.is_vaccinated]
+ );
+
+ useAbortableEffect((status: statusType) => {
+ fetchData(status);
+ triggerGoal("Patient Consultation Viewed", {
+ facilityId: facilityId,
+ consultationId: consultationId,
+ userId: authUser.id,
+ });
+ }, []);
+
+ const consultationTabProps: ConsultationTabProps = {
+ consultationId,
+ facilityId,
+ patientId,
+ consultationData,
+ patientData,
+ };
+
+ const SelectedTab = TABS[tab];
+
+ const hasActiveShiftingRequest = () => {
+ if (activeShiftingData.length > 0) {
+ return [
+ "PENDING",
+ "APPROVED",
+ "DESTINATION APPROVED",
+ "PATIENT TO BE PICKED UP",
+ ].includes(activeShiftingData[activeShiftingData.length - 1].status);
+ }
+
+ return false;
+ };
+
+ if (isLoading) {
+ return ;
+ }
+
+ const tabButtonClasses = (selected: boolean) =>
+ `capitalize min-w-max-content cursor-pointer border-transparent text-gray-700 hover:text-gray-700 hover:border-gray-300 font-bold whitespace-nowrap ${
+ selected === true ? "border-primary-500 text-primary-600 border-b-2" : ""
+ }`;
+
+ // const ShowDiagnosis = ({
+ // diagnoses = [],
+ // label = "Diagnosis",
+ // nshow = 2,
+ // }: {
+ // diagnoses: ICD11DiagnosisModel[] | undefined;
+ // label: string;
+ // nshow?: number;
+ // }) => {
+ // const [showMore, setShowMore] = useState(false);
+
+ // return diagnoses.length ? (
+ //
+ // ) : null;
+ // };
+
+ return (
+
+ setOpenDischargeSummaryDialog(false)}
+ />
+
+ setOpenDischargeDialog(false)}
+ consultationData={consultationData}
+ />
+
+
+
+
+
+
+
+
+ {consultationData.admitted_to && (
+
+
+ Patient
+ {consultationData.discharge_date
+ ? " Discharged from"
+ : " Admitted to"}
+
+ {consultationData.admitted_to}
+
+
+ {(consultationData.admission_date ??
+ consultationData.discharge_date) && (
+
+ {relativeTime(
+ consultationData.discharge_date
+ ? consultationData.discharge_date
+ : consultationData.admission_date
+ )}
+
+ )}
+
+ {consultationData.admission_date &&
+ formatDateTime(consultationData.admission_date)}
+ {consultationData.discharge_date &&
+ ` - ${formatDateTime(consultationData.discharge_date)}`}
+
+
+ )}
+
+
+
+
+ {/*consultationData.other_symptoms && (
+
+
+ Other Symptoms:{" "}
+
+ {consultationData.other_symptoms}
+
+ )*/}
+
+
+
+ {(consultationData.verified_by_object ||
+ consultationData.deprecated_verified_by) && (
+
+
+ Treating Physician:{" "}
+
+ {consultationData.verified_by_object
+ ? `${consultationData.verified_by_object.first_name} ${consultationData.verified_by_object.last_name}`
+ : consultationData.deprecated_verified_by}
+
+
+ )}
+
+
+ setOpenDischargeSummaryDialog(true)}>
+
+ {t("discharge_summary")}
+
+
+ setOpenDischargeDialog(true)}
+ disabled={!!consultationData.discharge_date}
+ >
+
+ {t("discharge_from_care")}
+
+
+
+
+
+
+ Created:
+ {consultationData.created_date
+ ? formatDateTime(consultationData.created_date)
+ : "--:--"}{" "}
+ |
+
+ {consultationData.created_by && (
+
+ {` ${consultationData.created_by.first_name} ${consultationData.created_by.last_name} `}
+ {`@${consultationData.created_by.username} (${consultationData.created_by.user_type})`}
+
+ )}
+
+
+
+ Last Modified:
+ {consultationData.modified_date
+ ? formatDateTime(consultationData.modified_date)
+ : "--:--"}{" "}
+ |
+
+ {consultationData.last_edited_by && (
+
+ {` ${consultationData.last_edited_by.first_name} ${consultationData.last_edited_by.last_name} `}
+ {`@${consultationData.last_edited_by.username} (${consultationData.last_edited_by.user_type})`}
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/Components/Facility/ConsultationForm.tsx b/src/Components/Facility/ConsultationForm.tsx
index c71c70b162c..10397f4e79a 100644
--- a/src/Components/Facility/ConsultationForm.tsx
+++ b/src/Components/Facility/ConsultationForm.tsx
@@ -1,6 +1,6 @@
import * as Notification from "../../Utils/Notifications.js";
-import { BedModel, FacilityModel, ICD11DiagnosisModel } from "./models";
+import { BedModel, FacilityModel } from "./models";
import {
CONSULTATION_STATUS,
CONSULTATION_SUGGESTION,
@@ -38,9 +38,11 @@ import Beds from "./Consultations/Beds";
import CareIcon from "../../CAREUI/icons/CareIcon";
import CheckBoxFormField from "../Form/FormFields/CheckBoxFormField";
import DateFormField from "../Form/FormFields/DateFormField";
-import { DiagnosisSelectFormField } from "../Common/DiagnosisSelectFormField";
import { FacilitySelect } from "../Common/FacilitySelect";
-import { FieldChangeEventHandler } from "../Form/FormFields/Utils";
+import {
+ FieldChangeEvent,
+ FieldChangeEventHandler,
+} from "../Form/FormFields/Utils";
import { FormAction } from "../Form/Utils";
import PatientCategorySelect from "../Patient/PatientCategorySelect";
import { SelectFormField } from "../Form/FormFields/SelectFormField";
@@ -57,6 +59,15 @@ import useConfig from "../../Common/hooks/useConfig";
import { useDispatch } from "react-redux";
import useVisibility from "../../Utils/useVisibility";
import dayjs from "../../Utils/dayjs";
+import {
+ ConditionVerificationStatuses,
+ ConsultationDiagnosis,
+ CreateDiagnosis,
+} from "../Diagnosis/types.js";
+import {
+ CreateDiagnosesBuilder,
+ EditDiagnosesBuilder,
+} from "../Diagnosis/ConsultationDiagnosisBuilder/ConsultationDiagnosisBuilder.js";
const Loading = lazy(() => import("../Common/Loading"));
const PageTitle = lazy(() => import("../Common/PageTitle"));
@@ -78,9 +89,10 @@ type FormDetails = {
discharge_date: null;
referred_to?: string;
referred_to_external?: string;
- icd11_diagnoses_object: ICD11DiagnosisModel[];
- icd11_provisional_diagnoses_object: ICD11DiagnosisModel[];
+ create_diagnoses: CreateDiagnosis[];
+ diagnoses: ConsultationDiagnosis[];
verified_by: string;
+ verified_by_object: UserModel | null;
is_kasp: BooleanStrings;
kasp_enabled_date: null;
examination_details: string;
@@ -91,7 +103,7 @@ type FormDetails = {
procedure: ProcedureType[];
investigation: InvestigationType[];
is_telemedicine: BooleanStrings;
- action?: string;
+ action?: number;
assigned_to: string;
assigned_to_object: UserModel | null;
special_instruction: string;
@@ -117,14 +129,15 @@ const initForm: FormDetails = {
facility: "",
admitted: "false",
admitted_to: "",
- category: "Comfort",
+ category: "",
admission_date: new Date(),
discharge_date: null,
referred_to: "",
referred_to_external: "",
- icd11_diagnoses_object: [],
- icd11_provisional_diagnoses_object: [],
+ create_diagnoses: [],
+ diagnoses: [],
verified_by: "",
+ verified_by_object: null,
is_kasp: "false",
kasp_enabled_date: null,
examination_details: "",
@@ -135,7 +148,7 @@ const initForm: FormDetails = {
procedure: [],
investigation: [],
is_telemedicine: "false",
- action: "NO_ACTION",
+ action: 10,
assigned_to: "",
assigned_to_object: null,
special_instruction: "",
@@ -271,15 +284,14 @@ export const ConsultationForm = (props: any) => {
setIsLoading(true);
const res = await dispatchAction(getPatient({ id: patientId }));
if (res.data) {
- setPatientName(res.data.name);
- setFacilityName(res.data.facility_object.name);
if (isUpdate) {
- const form = { ...state.form };
- form.action = TELEMEDICINE_ACTIONS.find(
- (a) => a.id === res.data.action
- )?.text;
- dispatch({ type: "set_form", form });
+ dispatch({
+ type: "set_form",
+ form: { ...state.form, action: res.data.action },
+ });
}
+ setPatientName(res.data.name);
+ setFacilityName(res.data.facility_object.name);
}
} else {
setPatientName("");
@@ -294,6 +306,37 @@ export const ConsultationForm = (props: any) => {
!!state.form.symptoms.length && !state.form.symptoms.includes(1);
const isOtherSymptomsSelected = state.form.symptoms.includes(9);
+ const handleFormFieldChange: FieldChangeEventHandler = (event) => {
+ if (event.name === "consultation_status" && event.value === "1") {
+ dispatch({
+ type: "set_form",
+ form: {
+ ...state.form,
+ consultation_status: 1,
+ symptoms: [1],
+ symptoms_onset_date: new Date(),
+ category: "Critical",
+ suggestion: "DD",
+ },
+ });
+ } else if (event.name === "suggestion" && event.value === "DD") {
+ dispatch({
+ type: "set_form",
+ form: {
+ ...state.form,
+ suggestion: "DD",
+ consultation_notes: "Patient declared dead",
+ verified_by: "Declared Dead",
+ },
+ });
+ } else {
+ dispatch({
+ type: "set_form",
+ form: { ...state.form, [event.name]: event.value },
+ });
+ }
+ };
+
const fetchData = useCallback(
async (status: statusType) => {
if (!patientId) setIsLoading(true);
@@ -323,15 +366,16 @@ export const ConsultationForm = (props: any) => {
admitted_to: res.data.admitted_to ? res.data.admitted_to : "",
category: res.data.category
? PATIENT_CATEGORIES.find((i) => i.text === res.data.category)
- ?.id ?? "Comfort"
- : "Comfort",
+ ?.id ?? ""
+ : "",
patient_no: res.data.patient_no ?? "",
- verified_by: res.data.verified_by ? res.data.verified_by : "",
OPconsultation: res.data.consultation_notes,
is_telemedicine: `${res.data.is_telemedicine}`,
is_kasp: `${res.data.is_kasp}`,
assigned_to: res.data.assigned_to || "",
assigned_to_object: res.data.assigned_to_object,
+ verified_by: res.data.verified_by || "",
+ verified_by_object: res.data.verified_by_object,
ett_tt: res.data.ett_tt ? Number(res.data.ett_tt) : 3,
special_instruction: res.data.special_instruction || "",
weight: res.data.weight ? res.data.weight : "",
@@ -342,8 +386,13 @@ export const ConsultationForm = (props: any) => {
death_datetime: res.data?.death_datetime || "",
death_confirmed_doctor: res.data?.death_confirmed_doctor || "",
InvestigationAdvice: res.data.investigation,
+ diagnoses: res.data.diagnoses.sort(
+ (a: ConsultationDiagnosis, b: ConsultationDiagnosis) =>
+ ConditionVerificationStatuses.indexOf(a.verification_status) -
+ ConditionVerificationStatuses.indexOf(b.verification_status)
+ ),
};
- dispatch({ type: "set_form", form: formData });
+ dispatch({ type: "set_form", form: { ...state.form, ...formData } });
setBed(formData.bed);
if (res.data.last_daily_round) {
@@ -355,7 +404,7 @@ export const ConsultationForm = (props: any) => {
setIsLoading(false);
}
},
- [dispatchAction, id]
+ [dispatchAction, id, patientName, patientId]
);
useAbortableEffect(
@@ -529,27 +578,65 @@ export const ConsultationForm = (props: any) => {
}
case "verified_by": {
- if (!state.form[field].replace(/\s/g, "").length) {
- errors[field] = "Please fill verified by";
+ if (state.form.suggestion !== "DD" && !state.form[field]) {
+ errors[field] = "Please fill treating physician";
invalidForm = true;
break;
}
return;
}
- case "icd11_provisional_diagnoses_object": {
- if (
- state.form[field].length === 0 &&
- state.form["icd11_diagnoses_object"].length === 0
- ) {
- for (const err_field of [field, "icd11_diagnoses_object"])
- errors[err_field] =
- "Please select either Provisional Diagnosis or Final Diagnosis";
- invalidForm = true;
- break;
- }
- return;
- }
+ // case "icd11_provisional_diagnoses_object": {
+ // if (
+ // state.form[field].length === 0 &&
+ // state.form["icd11_diagnoses_object"].length === 0
+ // ) {
+ // for (const err_field of [field, "icd11_diagnoses_object"])
+ // errors[err_field] =
+ // "Please select either Provisional Diagnosis or Final Diagnosis";
+ // invalidForm = true;
+ // break;
+ // }
+ // return;
+ // }
+
+ // case "icd11_principal_diagnosis": {
+ // if (!state.form[field]) {
+ // errors[field] = "Please select Principal Diagnosis";
+ // invalidForm = true;
+ // break;
+ // }
+
+ // if (
+ // state.form[field] &&
+ // state.form["icd11_diagnoses_object"].length &&
+ // !state.form["icd11_provisional_diagnoses_object"] &&
+ // !state.form["icd11_diagnoses_object"]
+ // .map((d) => d.id)
+ // .includes(state.form[field]!)
+ // ) {
+ // errors[field] =
+ // "Please select Principal Diagnosis from Final Diagnosis";
+ // invalidForm = true;
+ // break;
+ // }
+
+ // if (
+ // state.form[field] &&
+ // state.form["icd11_provisional_diagnoses_object"].length &&
+ // !state.form["icd11_diagnoses_object"] &&
+ // !state.form["icd11_provisional_diagnoses_object"]
+ // .map((d) => d.id)
+ // .includes(state.form[field]!)
+ // ) {
+ // errors[field] =
+ // "Please select Principal Diagnosis from Provisional Diagnosis";
+ // invalidForm = true;
+ // break;
+ // }
+
+ // return;
+ // }
default:
return;
@@ -571,6 +658,7 @@ export const ConsultationForm = (props: any) => {
};
const declareThePatientDead = async (
+ id: string,
cause_of_death: string,
death_datetime: string,
death_confirmed_doctor: string
@@ -582,6 +670,7 @@ export const ConsultationForm = (props: any) => {
discharge_notes: cause_of_death,
death_datetime: death_datetime,
death_confirmed_doctor: death_confirmed_doctor,
+ discharge_date: dayjs().toISOString(),
},
{ id }
)
@@ -623,13 +712,7 @@ export const ConsultationForm = (props: any) => {
treatment_plan: state.form.treatment_plan,
discharge_date: state.form.discharge_date,
patient_no: state.form.patient_no,
- icd11_diagnoses: state.form.icd11_diagnoses_object.map(
- (o: ICD11DiagnosisModel) => o.id
- ),
- icd11_provisional_diagnoses:
- state.form.icd11_provisional_diagnoses_object.map(
- (o: ICD11DiagnosisModel) => o.id
- ),
+ create_diagnoses: isUpdate ? undefined : state.form.create_diagnoses,
verified_by: state.form.verified_by,
investigation: state.form.InvestigationAdvice,
procedure: state.form.procedures,
@@ -666,6 +749,7 @@ export const ConsultationForm = (props: any) => {
if (data.suggestion === "DD") {
await declareThePatientDead(
+ res.data.id,
state.form.cause_of_death,
state.form.death_datetime,
state.form.death_confirmed_doctor
@@ -694,46 +778,14 @@ export const ConsultationForm = (props: any) => {
}
};
- const handleFormFieldChange: FieldChangeEventHandler = (event) => {
- if (event.name === "consultation_status" && event.value === "1") {
- dispatch({
- type: "set_form",
- form: {
- ...state.form,
- consultation_status: 1,
- symptoms: [1],
- symptoms_onset_date: new Date(),
- category: "Critical",
- suggestion: "DD",
- verified_by: "Brought Dead",
- },
- });
- } else if (event.name === "suggestion" && event.value === "DD") {
+ const handleDoctorSelect = (event: FieldChangeEvent) => {
+ if (event.value?.id) {
dispatch({
type: "set_form",
form: {
...state.form,
- suggestion: "DD",
- consultation_notes: "Patient declared dead",
- verified_by: "Declared Dead",
- },
- });
- } else {
- dispatch({
- type: "set_form",
- form: { ...state.form, [event.name]: event.value },
- });
- }
- };
-
- const handleDoctorSelect = (doctor: UserModel | null) => {
- if (doctor?.id) {
- dispatch({
- type: "set_form",
- form: {
- ...state.form,
- assigned_to: doctor.id.toString(),
- assigned_to_object: doctor,
+ [event.name]: event.value.id.toString(),
+ [`${event.name}_object`]: event.value,
},
});
} else {
@@ -741,8 +793,8 @@ export const ConsultationForm = (props: any) => {
type: "set_form",
form: {
...state.form,
- assigned_to: "",
- assigned_to_object: null,
+ [event.name]: "",
+ [`${event.name}_object`]: null,
},
});
}
@@ -1129,28 +1181,25 @@ export const ConsultationForm = (props: any) => {
{sectionTitle("Diagnosis", true)}
-
-
- Either Provisional or Final Diagnosis is mandatory
-
- | Diagnoses as per ICD-11 recommended by WHO
+
+ Diagnoses as per ICD-11 recommended by WHO
-
-
-
-
-
-
+
+ {isUpdate ? (
+
+ ) : (
+ {
+ handleFormFieldChange({
+ name: "create_diagnoses",
+ value: diagnoses,
+ });
+ }}
+ />
+ )}
@@ -1241,11 +1290,17 @@ export const ConsultationForm = (props: any) => {
className="col-span-6"
ref={fieldRef["verified_by"]}
>
-
@@ -1263,12 +1318,12 @@ export const ConsultationForm = (props: any) => {
option.desc}
- optionValue={(option) => option.text}
+ optionDescription={() => ""}
/>
@@ -1290,11 +1345,10 @@ export const ConsultationForm = (props: any) => {
value={
state.form.assigned_to_object ?? undefined
}
- onChange={(option) =>
- handleDoctorSelect(option.value)
- }
+ onChange={handleDoctorSelect}
userType={"Doctor"}
name={"assigned_to"}
+ label="Assigned to"
/>
)}
diff --git a/src/Components/Facility/Consultations/Feed.tsx b/src/Components/Facility/Consultations/Feed.tsx
index d57339abb28..b763831bf74 100644
--- a/src/Components/Facility/Consultations/Feed.tsx
+++ b/src/Components/Facility/Consultations/Feed.tsx
@@ -31,9 +31,9 @@ import { useDispatch } from "react-redux";
import { useHLSPLayer } from "../../../Common/hooks/useHLSPlayer";
import useKeyboardShortcut from "use-keyboard-shortcut";
import useFullscreen from "../../../Common/hooks/useFullscreen.js";
-import { triggerGoal } from "../../Common/Plausible.js";
import { useMessageListener } from "../../../Common/hooks/useMessageListener.js";
import useNotificationSubscribe from "../../../Common/hooks/useNotificationSubscribe.js";
+import { triggerGoal } from "../../../Integrations/Plausible.js";
import useAuthUser from "../../../Common/hooks/useAuthUser.js";
interface IFeedProps {
@@ -59,8 +59,11 @@ export const Feed: React.FC = ({ consultationId, facilityId }) => {
const [cameraAsset, setCameraAsset] = useState({
id: "",
accessKey: "",
+ middleware_address: "",
+ location_middleware: "",
});
- const [cameraMiddlewareHostname, setCameraMiddlewareHostname] = useState("");
+ const [facilityMiddlewareHostname, setFacilityMiddlewareHostname] =
+ useState("");
const [cameraConfig, setCameraConfig] = useState({});
const [isLoading, setIsLoading] = useState(true);
const [bedPresets, setBedPresets] = useState([]);
@@ -79,6 +82,8 @@ export const Feed: React.FC = ({ consultationId, facilityId }) => {
const [borderAlert, setBorderAlert] = useState(null);
const [privacy, setPrivacy] = useState(false);
+ const [videoStartTime, setVideoStartTime] = useState(null);
+ const [statusReported, setStatusReported] = useState(false);
const authUser = useAuthUser();
// Notification hook to get subscription info
@@ -201,13 +206,19 @@ export const Feed: React.FC = ({ consultationId, facilityId }) => {
const res = await dispatch(getPermittedFacility(facilityId));
if (res.status === 200 && res.data) {
- setCameraMiddlewareHostname(res.data.middleware_address);
+ setFacilityMiddlewareHostname(res.data.middleware_address);
}
};
if (facilityId) fetchFacility();
}, [dispatch, facilityId]);
+ const fallbackMiddleware =
+ cameraAsset.location_middleware || facilityMiddlewareHostname;
+
+ const currentMiddleware =
+ cameraAsset.middleware_address || fallbackMiddleware;
+
useEffect(() => {
if (cameraState) {
setCameraState({
@@ -250,9 +261,7 @@ export const Feed: React.FC = ({ consultationId, facilityId }) => {
...bedAssets.data,
results: bedAssets.data.results.filter(
(asset: { asset_object: { meta: { asset_type: string } } }) => {
- return asset?.asset_object?.meta?.asset_type === "CAMERA"
- ? true
- : false;
+ return asset?.asset_object?.meta?.asset_type === "CAMERA";
}
),
},
@@ -269,6 +278,12 @@ export const Feed: React.FC = ({ consultationId, facilityId }) => {
setCameraAsset({
id: bedAssets.data.results[0].asset_object.id,
accessKey: config[2] || "",
+ middleware_address:
+ bedAssets.data.results[0].asset_object?.meta
+ ?.middleware_hostname,
+ location_middleware:
+ bedAssets.data.results[0].asset_object.location_object
+ ?.middleware_address,
});
setCameraConfig(bedAssets.data.results[0].meta);
setCameraState({
@@ -313,8 +328,8 @@ export const Feed: React.FC = ({ consultationId, facilityId }) => {
);
const url = !isIOS
- ? `wss://${cameraMiddlewareHostname}/stream/${cameraAsset?.accessKey}/channel/0/mse?uuid=${cameraAsset?.accessKey}&channel=0`
- : `https://${cameraMiddlewareHostname}/stream/${cameraAsset?.accessKey}/channel/0/hls/live/index.m3u8?uuid=${cameraAsset?.accessKey}&channel=0`;
+ ? `wss://${currentMiddleware}/stream/${cameraAsset?.accessKey}/channel/0/mse?uuid=${cameraAsset?.accessKey}&channel=0`
+ : `https://${currentMiddleware}/stream/${cameraAsset?.accessKey}/channel/0/hls/live/index.m3u8?uuid=${cameraAsset?.accessKey}&channel=0`;
const {
startStream,
@@ -325,7 +340,7 @@ export const Feed: React.FC = ({ consultationId, facilityId }) => {
: // eslint-disable-next-line react-hooks/rules-of-hooks
useMSEMediaPlayer({
config: {
- middlewareHostname: cameraMiddlewareHostname,
+ middlewareHostname: currentMiddleware,
...cameraAsset,
},
url,
@@ -346,6 +361,16 @@ export const Feed: React.FC = ({ consultationId, facilityId }) => {
dispatch,
});
+ const calculateVideoLiveDelay = () => {
+ const video = liveFeedPlayerRef.current as HTMLVideoElement;
+ if (!video || !videoStartTime) return 0;
+
+ const timeDifference =
+ (new Date().getTime() - videoStartTime.getTime()) / 1000;
+
+ return timeDifference - video.currentTime;
+ };
+
const getBedPresets = async (asset: any) => {
if (asset.id && bed) {
const bedAssets = await dispatch(listAssetBeds({ asset: asset.id, bed }));
@@ -388,7 +413,7 @@ export const Feed: React.FC = ({ consultationId, facilityId }) => {
});
getBedPresets(cameraAsset);
}
- }, [cameraAsset, cameraMiddlewareHostname]);
+ }, [cameraAsset, currentMiddleware]);
//lock and unlock asset on mount and unmount
useEffect(() => {
@@ -493,13 +518,32 @@ export const Feed: React.FC = ({ consultationId, facilityId }) => {
useEffect(() => {
let tId: any;
if (streamStatus !== StreamStatus.Playing) {
- setStreamStatus(StreamStatus.Loading);
+ if (streamStatus !== StreamStatus.Offline) {
+ setStreamStatus(StreamStatus.Loading);
+ }
tId = setTimeout(() => {
startStream({
onSuccess: () => setStreamStatus(StreamStatus.Playing),
- onError: () => setStreamStatus(StreamStatus.Offline),
+ onError: () => {
+ setStreamStatus(StreamStatus.Offline);
+ if (!statusReported) {
+ triggerGoal("Camera Feed Viewed", {
+ consultationId,
+ userId: authUser.id,
+ result: "error",
+ });
+ setStatusReported(true);
+ }
+ },
});
}, 100);
+ } else if (!statusReported) {
+ triggerGoal("Camera Feed Viewed", {
+ consultationId,
+ userId: authUser.id,
+ result: "success",
+ });
+ setStatusReported(true);
}
return () => {
@@ -512,7 +556,7 @@ export const Feed: React.FC = ({ consultationId, facilityId }) => {
}, []);
useEffect(() => {
- if (streamStatus === StreamStatus.Playing) {
+ if (!currentPreset && streamStatus === StreamStatus.Playing) {
setLoading(CAMERA_STATES.MOVING.GENERIC);
const preset =
bedPresets?.find(
@@ -584,12 +628,19 @@ export const Feed: React.FC = ({ consultationId, facilityId }) => {
},
reset: () => {
setStreamStatus(StreamStatus.Loading);
+ setVideoStartTime(null);
startStream({
onSuccess: () => setStreamStatus(StreamStatus.Playing),
onError: () => setStreamStatus(StreamStatus.Offline),
});
},
fullScreen: () => {
+ if (isIOS) {
+ const element = document.querySelector("video");
+ if (!element) return;
+ setFullscreen(true, element as HTMLElement);
+ return;
+ }
if (!liveFeedPlayerRef.current) return;
setFullscreen(
!isFullscreen,
@@ -880,10 +931,16 @@ export const Feed: React.FC = ({ consultationId, facilityId }) => {
playsinline={true}
playing={true}
muted={true}
+ onPlay={() => {
+ setVideoStartTime(() => new Date());
+ }}
width="100%"
height="100%"
onBuffer={() => {
- setStreamStatus(StreamStatus.Loading);
+ const delay = calculateVideoLiveDelay();
+ if (delay > 5) {
+ setStreamStatus(StreamStatus.Loading);
+ }
}}
onError={(e: any, _: any, hlsInstance: any) => {
if (e === "hlsError") {
@@ -902,6 +959,15 @@ export const Feed: React.FC = ({ consultationId, facilityId }) => {
muted
playsInline
className="max-h-full max-w-full"
+ onPlay={() => {
+ setVideoStartTime(() => new Date());
+ }}
+ onWaiting={() => {
+ const delay = calculateVideoLiveDelay();
+ if (delay > 5) {
+ setStreamStatus(StreamStatus.Loading);
+ }
+ }}
ref={liveFeedPlayerRef as any}
/>
)}
@@ -921,8 +987,9 @@ export const Feed: React.FC = ({ consultationId, facilityId }) => {
STATUS: OFFLINE
Feed is currently not live.
-
- Click refresh button to try again.
+ Trying to connect...
+
+
)}
@@ -973,6 +1040,13 @@ export const Feed: React.FC = ({ consultationId, facilityId }) => {
clickAction={() => cameraPTZ[4].callback()}
/>
+ {streamStatus === StreamStatus.Playing &&
+ calculateVideoLiveDelay() > 3 && (
+
+
+ Slow Network Detected
+
+ )}
{[
false,
diff --git a/src/Components/Facility/Consultations/LiveFeed.tsx b/src/Components/Facility/Consultations/LiveFeed.tsx
index 369c6d8ce79..56d8e2754cd 100644
--- a/src/Components/Facility/Consultations/LiveFeed.tsx
+++ b/src/Components/Facility/Consultations/LiveFeed.tsx
@@ -43,6 +43,7 @@ const LiveFeed = (props: any) => {
);
const [bedTransfer, setBedTransfer] = useState ({});
const [newPresetName, setNewPresetName] = useState("");
+ const [videoStartTime, setVideoStartTime] = useState(null);
const [loading, setLoading] = useState();
const dispatch: any = useDispatch();
const [page, setPage] = useState({
@@ -138,6 +139,16 @@ const LiveFeed = (props: any) => {
},
});
+ const calculateVideoLiveDelay = () => {
+ const video = liveFeedPlayerRef.current as HTMLVideoElement;
+ if (!video || !videoStartTime) return 0;
+
+ const timeDifference =
+ (new Date().getTime() - videoStartTime.getTime()) / 1000;
+
+ return timeDifference - video.currentTime;
+ };
+
const getBedPresets = async (id: any) => {
const bedAssets = bed.id
? await dispatch(
@@ -313,6 +324,7 @@ const LiveFeed = (props: any) => {
},
reset: () => {
setStreamStatus(StreamStatus.Loading);
+ setVideoStartTime(null);
startStream({
onSuccess: () => setStreamStatus(StreamStatus.Playing),
onError: () => setStreamStatus(StreamStatus.Offline),
@@ -541,7 +553,25 @@ const LiveFeed = (props: any) => {
playsInline
className="z-10 h-full w-full"
ref={liveFeedPlayerRef}
+ onPlay={() => {
+ setVideoStartTime(() => new Date());
+ }}
+ onWaiting={() => {
+ const delay = calculateVideoLiveDelay();
+ if (delay > 5) {
+ setStreamStatus(StreamStatus.Loading);
+ }
+ }}
>
+
+ {streamStatus === StreamStatus.Playing &&
+ calculateVideoLiveDelay() > 3 && (
+
+
+ Slow Network Detected
+
+ )}
+
{loading && (
diff --git a/src/Components/Facility/Consultations/NeurologicalTables.tsx b/src/Components/Facility/Consultations/NeurologicalTables.tsx
index 79a0675a708..36fae175480 100644
--- a/src/Components/Facility/Consultations/NeurologicalTables.tsx
+++ b/src/Components/Facility/Consultations/NeurologicalTables.tsx
@@ -28,10 +28,7 @@ const DataTable = (props: any) => {
Right
-
+
{data.map((x: any, i: any) => {
return (
{
const LOC_OPTIONS = [
{ id: 0, value: "Unknown" },
{ id: 5, value: "Alert" },
- { id: 10, value: "Drowsy" },
- { id: 15, value: "Stuporous" },
- { id: 20, value: "Comatose" },
- { id: 25, value: "Cannot Be Assessed" },
+ { id: 10, value: "Responds to Voice" },
+ { id: 15, value: "Responds to Pain" },
+ { id: 20, value: "Unresponsive" },
+ { id: 25, value: "Agitated or Confused" },
+ { id: 30, value: "Onset of Agitation and Confusion" },
];
const REACTION_OPTIONS = [
@@ -297,16 +295,13 @@ export const NeurologicalTable = (props: any) => {
Level Of Consciousness
-
+
{locData.map((x: any, i: any) => (
-
+
{x.date}
@@ -376,10 +371,7 @@ export const NeurologicalTable = (props: any) => {
Total
-
+
{glasgowData.map((x: any, i: any) => {
return (
{
Scale Description
- Eye Open
+
+ Eye Opening Response
+
{EYE_OPEN_SCALE.map((x: any) => (
{
const {
@@ -197,7 +225,8 @@ export const LinePlot = (props: any) => {
}
return (
- {
@@ -81,5 +108,5 @@ export const StackedLinePlot = (props: any) => {
},
series: series,
};
- return ;
+ return ;
};
diff --git a/src/Components/Facility/DischargeModal.tsx b/src/Components/Facility/DischargeModal.tsx
index 5e13b038bab..138e74fb9b9 100644
--- a/src/Components/Facility/DischargeModal.tsx
+++ b/src/Components/Facility/DischargeModal.tsx
@@ -12,7 +12,7 @@ import DateFormField from "../Form/FormFields/DateFormField";
import DialogModal from "../Common/Dialog";
import { FieldChangeEvent } from "../Form/FormFields/Utils";
import { FieldLabel } from "../Form/FormFields/FormField";
-import { HCXActions, PrescriptionActions } from "../../Redux/actions";
+import { HCXActions } from "../../Redux/actions";
import { HCXClaimModel } from "../HCX/models";
import { SelectFormField } from "../Form/FormFields/SelectFormField";
import TextAreaFormField from "../Form/FormFields/TextAreaFormField";
@@ -75,7 +75,7 @@ const DischargeModal = ({
const [latestClaim, setLatestClaim] = useState();
const [isCreateClaimLoading, setIsCreateClaimLoading] = useState(false);
const [isSendingDischargeApi, setIsSendingDischargeApi] = useState(false);
- const [facility, setFacility] = useState({ id: 0, name: "" }); // for referred to external
+ const [facility, setFacility] = useState();
const [errors, setErrors] = useState({});
const fetchLatestClaim = useCallback(async () => {
@@ -87,7 +87,7 @@ const DischargeModal = ({
})
);
- if (res.data?.results?.length) {
+ if (res?.data?.results?.length > 0) {
setLatestClaim(res.data.results[0]);
if (isCreateClaimLoading)
Notification.Success({ msg: "Fetched Claim Approval Results" });
@@ -183,15 +183,14 @@ const DischargeModal = ({
});
};
- const prescriptionActions = PrescriptionActions(consultationData.id ?? "");
-
const handleFacilitySelect = (selected: FacilityModel) => {
- setFacility(selected ? selected : facility);
- const { id, name } = selected;
+ setFacility(selected);
+ const { id, name } = selected || {};
const isExternal = id === -1;
setPreDischargeForm((prev) => ({
...prev,
- ...(isExternal ? { referred_to_external: name } : { referred_to: id }),
+ referred_to: isExternal ? null : id,
+ referred_to_external: isExternal ? name : null,
}));
};
@@ -237,9 +236,9 @@ const DischargeModal = ({
setSelected={(selected) =>
handleFacilitySelect(selected as FacilityModel)
}
- selected={facility}
- showAll={true}
- freeText={true}
+ selected={facility ?? null}
+ showAll
+ freeText
multiple={false}
errors={errors?.referred_to}
className="mb-4"
@@ -283,18 +282,11 @@ const DischargeModal = ({
Discharge Prescription Medications
-
+
Discharge PRN Prescriptions
-
+
)}
diff --git a/src/Components/Facility/DoctorVideoSlideover.tsx b/src/Components/Facility/DoctorVideoSlideover.tsx
index 59c443cb64e..5302f7f9d53 100644
--- a/src/Components/Facility/DoctorVideoSlideover.tsx
+++ b/src/Components/Facility/DoctorVideoSlideover.tsx
@@ -68,7 +68,13 @@ export default function DoctorVideoSlideover(props: {
home: false,
},
].map((type, i) => (
-
+
{type.title}
diff --git a/src/Components/Facility/FacilityCard.tsx b/src/Components/Facility/FacilityCard.tsx
index e66fc4f3cc2..ab37ac20c96 100644
--- a/src/Components/Facility/FacilityCard.tsx
+++ b/src/Components/Facility/FacilityCard.tsx
@@ -9,7 +9,7 @@ import ButtonV2, { Cancel, Submit } from "../Common/components/ButtonV2";
import * as Notification from "../../Utils/Notifications.js";
import Chip from "../../CAREUI/display/Chip";
import CareIcon from "../../CAREUI/icons/CareIcon";
-import { parsePhoneNumber } from "libphonenumber-js";
+import { formatPhoneNumber, parsePhoneNumber } from "../../Utils/utils";
import DialogModal from "../Common/Dialog";
import TextAreaFormField from "../Form/FormFields/TextAreaFormField";
import useConfig from "../../Common/hooks/useConfig";
@@ -48,11 +48,11 @@ export const FacilityCard = (props: { facility: any; userType: any }) => {
return (
-
+
{(facility.read_cover_image_url && (
{
{(facility.read_cover_image_url && (
{
)}
-
+
{facility.kasp_empanelled && (
@@ -88,7 +88,10 @@ export const FacilityCard = (props: { facility: any; userType: any }) => {
{kasp_string}
)}
-
+
{
href={`tel:${facility.phone_number}`}
className="text-sm font-semibold tracking-wider"
>
- {parsePhoneNumber(
- facility.phone_number as string,
- "IN"
- ).formatInternational() || "-"}
+ {formatPhoneNumber(
+ parsePhoneNumber(facility.phone_number as string) ?? "-"
+ )}
-
+
-
+
0.85
@@ -165,7 +167,7 @@ export const FacilityCard = (props: { facility: any; userType: any }) => {
: "button-primary-border bg-primary-100"
}`}
>
-
+
Live Patients / Total beds
{" "}
{
-
+
{userType !== "Staff" ? (
import("../Common/Loading"));
+
+const initForm = {
+ name: "",
+ state: 0,
+ district: 0,
+ localbody: 0,
+ ward: 0,
+ middleware_address: "",
+};
+
+const initialState = {
+ form: { ...initForm },
+ errors: {},
+};
+
+const FormReducer = (state = initialState, action: any) => {
+ switch (action.type) {
+ case "set_form": {
+ return {
+ ...state,
+ form: action.form,
+ };
+ }
+ case "set_error": {
+ return {
+ ...state,
+ errors: action.errors,
+ };
+ }
+ default:
+ return state;
+ }
+};
+
+interface IProps {
+ facilityId: string;
+}
+
+export const FacilityConfigure = (props: IProps) => {
+ const [state, dispatch] = useReducer(FormReducer, initialState);
+ const { facilityId } = props;
+ const [isLoading, setIsLoading] = useState(false);
+
+ const { loading } = useQuery(routes.getPermittedFacility, {
+ pathParams: { id: facilityId },
+ onResponse: (res) => {
+ if (res.data) {
+ const formData = {
+ name: res.data.name,
+ state: res.data.state,
+ district: res.data.district,
+ local_body: res.data.local_body,
+ ward: res.data.ward,
+ middleware_address: res.data.middleware_address,
+ };
+ dispatch({ type: "set_form", form: formData });
+ }
+ },
+ });
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setIsLoading(true);
+ if (!state.form.middleware_address) {
+ dispatch({
+ type: "set_error",
+ errors: { middleware_address: ["Middleware Address is required"] },
+ });
+ setIsLoading(false);
+ return;
+ }
+ if (
+ state.form.middleware_address.match(
+ /^(?!https?:\/\/)[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)*\.[a-zA-Z]{2,}$/
+ ) === null
+ ) {
+ dispatch({
+ type: "set_error",
+ errors: {
+ middleware_address: ["Invalid Middleware Address"],
+ },
+ });
+ setIsLoading(false);
+ return;
+ }
+
+ const data = {
+ ...state.form,
+ middleware_address: state.form.middleware_address,
+ };
+
+ const { res, error } = await request(routes.partialUpdateFacility, {
+ pathParams: { id: facilityId },
+ body: data,
+ });
+
+ setIsLoading(false);
+ if (res?.ok) {
+ Notification.Success({
+ msg: "Facility updated successfully",
+ });
+ navigate(`/facility/${facilityId}`);
+ } else {
+ Notification.Error({
+ msg: error?.detail ?? "Something went wrong",
+ });
+ }
+ setIsLoading(false);
+ };
+
+ const handleChange = (e: FieldChangeEvent) => {
+ dispatch({
+ type: "set_form",
+ form: { ...state.form, [e.name]: e.value },
+ });
+ };
+
+ if (isLoading || loading) {
+ return ;
+ }
+
+ return (
+
+
+
+ );
+};
diff --git a/src/Components/Facility/FacilityCreate.tsx b/src/Components/Facility/FacilityCreate.tsx
index d1243daaf1d..e742caefb82 100644
--- a/src/Components/Facility/FacilityCreate.tsx
+++ b/src/Components/Facility/FacilityCreate.tsx
@@ -26,7 +26,11 @@ import {
listDoctor,
updateFacility,
} from "../../Redux/actions";
-import { getPincodeDetails, includesIgnoreCase } from "../../Utils/utils";
+import {
+ getPincodeDetails,
+ includesIgnoreCase,
+ parsePhoneNumber,
+} from "../../Utils/utils";
import {
phonePreg,
validateLatitude,
@@ -51,11 +55,11 @@ import TextAreaFormField from "../Form/FormFields/TextAreaFormField";
import TextFormField from "../Form/FormFields/TextFormField";
import { navigate } from "raviger";
-import { parsePhoneNumberFromString } from "libphonenumber-js";
import useAppHistory from "../../Common/hooks/useAppHistory";
import useConfig from "../../Common/hooks/useConfig";
import { useDispatch } from "react-redux";
import { useTranslation } from "react-i18next";
+import { PhoneNumberValidator } from "../Form/FieldValidators.js";
const Loading = lazy(() => import("../Common/Loading"));
@@ -434,11 +438,11 @@ export const FacilityCreate = (props: FacilityProps) => {
return;
case "phone_number":
// eslint-disable-next-line no-case-declarations
- const phoneNumber = parsePhoneNumberFromString(state.form[field]);
+ const phoneNumber = state.form[field];
if (
- !state.form[field] ||
- !phoneNumber?.isPossible() ||
- !phonePreg(String(phoneNumber?.number))
+ !phoneNumber ||
+ !PhoneNumberValidator()(phoneNumber) === undefined ||
+ !phonePreg(phoneNumber)
) {
errors[field] = t("invalid_phone_number");
invalidForm = true;
@@ -488,9 +492,7 @@ export const FacilityCreate = (props: FacilityProps) => {
kasp_empanelled: JSON.parse(state.form.kasp_empanelled),
latitude: state.form.latitude || null,
longitude: state.form.longitude || null,
- phone_number: parsePhoneNumberFromString(
- state.form.phone_number
- )?.format("E.164"),
+ phone_number: parsePhoneNumber(state.form.phone_number),
oxygen_capacity: state.form.oxygen_capacity
? state.form.oxygen_capacity
: 0,
diff --git a/src/Components/Facility/FacilityHome.tsx b/src/Components/Facility/FacilityHome.tsx
index ec157d61884..405ba236f05 100644
--- a/src/Components/Facility/FacilityHome.tsx
+++ b/src/Components/Facility/FacilityHome.tsx
@@ -62,7 +62,7 @@ export const FacilityHome = (props: any) => {
const { t } = useTranslation();
const { facilityId } = props;
const dispatch: any = useDispatch();
- const [facilityData, setFacilityData] = useState({});
+ const [facilityData, setFacilityData] = useState({});
const [capacityData, setCapacityData] = useState>([]);
const [doctorData, setDoctorData] = useState>([]);
const [isLoading, setIsLoading] = useState(false);
@@ -323,7 +323,7 @@ export const FacilityHome = (props: any) => {
StaffUserTypeIndex;
const editCoverImageTooltip = hasPermissionToEditCoverImage && (
-
+
{`${hasCoverImage ? "Edit" : "Upload"}`}
@@ -495,7 +495,7 @@ export const FacilityHome = (props: any) => {
- {facilityData.features?.some((feature) =>
+ {facilityData.features?.some((feature: any) =>
FACILITY_FEATURE_TYPES.some((f) => f.id === feature)
) && (
Available features
@@ -541,9 +541,7 @@ export const FacilityHome = (props: any) => {
- navigate(`/facility/${facilityId}/middleware/update`)
- }
+ onClick={() => navigate(`/facility/${facilityId}/configure`)}
authorizeFor={NonReadOnlyUsers}
icon={}
>
@@ -565,6 +563,7 @@ export const FacilityHome = (props: any) => {
Location Management
navigate(`/facility/${facilityId}/resource/new`)
}
@@ -574,6 +573,7 @@ export const FacilityHome = (props: any) => {
Resource Request
navigate(`/facility/${facilityId}/assets/new`)}
authorizeFor={NonReadOnlyUsers}
icon={}
@@ -581,18 +581,21 @@ export const FacilityHome = (props: any) => {
Create Asset
navigate(`/assets?facility=${facilityId}`)}
icon={}
>
View Assets
navigate(`/facility/${facilityId}/users`)}
icon={}
>
View Users
setOpenDeleteDialog(true)}
className="flex items-center gap-3"
@@ -614,6 +617,16 @@ export const FacilityHome = (props: any) => {
Central Nursing Station
+ navigate(`/facility/${facilityId}/livefeed`)}
+ >
+
+ Live Monitoring
+
{
Add Details of a Patient
{user.username && (
-
+
{user.username}
)}
diff --git a/src/Components/Facility/Investigations/InvestigationTable.tsx b/src/Components/Facility/Investigations/InvestigationTable.tsx
index 8149e18496b..54e7e50bdc7 100644
--- a/src/Components/Facility/Investigations/InvestigationTable.tsx
+++ b/src/Components/Facility/Investigations/InvestigationTable.tsx
@@ -2,7 +2,6 @@ import ButtonV2 from "../../Common/components/ButtonV2";
import CareIcon from "../../../CAREUI/icons/CareIcon";
import { SelectFormField } from "../../Form/FormFields/SelectFormField";
import TextFormField from "../../Form/FormFields/TextFormField";
-import _ from "lodash";
import { classNames } from "../../../Utils/utils";
import { useState } from "react";
diff --git a/src/Components/Facility/Investigations/Reports/ReportTable.tsx b/src/Components/Facility/Investigations/Reports/ReportTable.tsx
index 0826ff904fc..1f20ec94180 100644
--- a/src/Components/Facility/Investigations/Reports/ReportTable.tsx
+++ b/src/Components/Facility/Investigations/Reports/ReportTable.tsx
@@ -2,7 +2,7 @@ import { getColorIndex, rowColor, transformData } from "./utils";
import ButtonV2 from "../../../Common/components/ButtonV2";
import { InvestigationResponse } from "./types";
-import { formatDateTime } from "../../../../Utils/utils";
+import { formatAge, formatDateTime } from "../../../../Utils/utils";
import { FC } from "react";
const ReportRow = ({ data, name, min, max }: any) => {
@@ -53,6 +53,7 @@ interface ReportTableProps {
patientDetails?: {
name: string;
age: number;
+ date_of_birth: string;
hospitalName: string;
};
investigationData: InvestigationResponse;
@@ -83,7 +84,14 @@ const ReportTable: FC = ({
{patientDetails && (
Name: {patientDetails.name}
- Age: {patientDetails.age}
+
+ Age:{" "}
+ {formatAge(
+ patientDetails.age,
+ patientDetails.date_of_birth,
+ true
+ )}
+
Hospital: {patientDetails.hospitalName}
)}
diff --git a/src/Components/Facility/Investigations/Reports/index.tsx b/src/Components/Facility/Investigations/Reports/index.tsx
index c4c69bc9169..9b3c29d40f8 100644
--- a/src/Components/Facility/Investigations/Reports/index.tsx
+++ b/src/Components/Facility/Investigations/Reports/index.tsx
@@ -1,5 +1,5 @@
import * as Notification from "../../../../Utils/Notifications";
-
+import _ from "lodash-es";
import { Group, InvestigationType } from "..";
import {
getPatient,
@@ -17,7 +17,6 @@ import { InvestigationResponse } from "./types";
import Loading from "../../../Common/Loading";
import Page from "../../../Common/components/Page";
import ReportTable from "./ReportTable";
-import _ from "lodash";
import { useDispatch } from "react-redux";
import { useRef } from "react";
@@ -100,8 +99,9 @@ const InvestigationReports = ({ id }: any) => {
const [patientDetails, setPatientDetails] = useState<{
name: string;
age: number;
+ date_of_birth: string;
hospitalName: string;
- }>({ name: "", age: -1, hospitalName: "" });
+ }>({ name: "", age: -1, date_of_birth: "", hospitalName: "" });
const [state, dispatch] = useReducer(
investigationReportsReducer,
initialState
@@ -220,6 +220,7 @@ const InvestigationReports = ({ id }: any) => {
setPatientDetails({
name: res.data.name,
age: res.data.age,
+ date_of_birth: res.data.date_of_birth,
hospitalName: res.data.facility_object.name,
});
}
@@ -227,6 +228,7 @@ const InvestigationReports = ({ id }: any) => {
setPatientDetails({
name: "",
age: -1,
+ date_of_birth: "",
hospitalName: "",
});
}
diff --git a/src/Components/Facility/Investigations/Reports/utils.tsx b/src/Components/Facility/Investigations/Reports/utils.tsx
index 485a3e1d5f4..e57f3c42c53 100644
--- a/src/Components/Facility/Investigations/Reports/utils.tsx
+++ b/src/Components/Facility/Investigations/Reports/utils.tsx
@@ -1,4 +1,4 @@
-import _ from "lodash";
+import _ from "lodash-es";
import { InvestigationResponse } from "./types";
export const transformData = _.memoize((data: InvestigationResponse) => {
diff --git a/src/Components/Facility/Investigations/ShowInvestigation.tsx b/src/Components/Facility/Investigations/ShowInvestigation.tsx
index 4c2826f6eea..00196bb6678 100644
--- a/src/Components/Facility/Investigations/ShowInvestigation.tsx
+++ b/src/Components/Facility/Investigations/ShowInvestigation.tsx
@@ -8,8 +8,8 @@ import {
} from "../../../Redux/actions";
import PageTitle from "../../Common/PageTitle";
import InvestigationTable from "./InvestigationTable";
-
-import _ from "lodash";
+import _ from "lodash-es";
+import { set } from "lodash-es";
import { navigate } from "raviger";
import * as Notification from "../../../Utils/Notifications.js";
@@ -110,7 +110,7 @@ export default function ShowInvestigation(props: any) {
const handleValueChange = (value: any, name: string) => {
const changedFields = { ...state.changedFields };
- _.set(changedFields, name, value);
+ set(changedFields, name, value);
dispatch({ type: "set_changed_fields", changedFields });
};
diff --git a/src/Components/Facility/Investigations/Table.tsx b/src/Components/Facility/Investigations/Table.tsx
index 4d6ce4c2340..4bd7e841543 100644
--- a/src/Components/Facility/Investigations/Table.tsx
+++ b/src/Components/Facility/Investigations/Table.tsx
@@ -1,7 +1,7 @@
import { FieldChangeEvent } from "../../Form/FormFields/Utils";
import { SelectFormField } from "../../Form/FormFields/SelectFormField";
import TextFormField from "../../Form/FormFields/TextFormField";
-import _ from "lodash";
+import { set } from "lodash-es";
import { useState } from "react";
const TestRow = ({ data, value, onChange, i }: any) => {
@@ -59,7 +59,7 @@ export const TestTable = ({ title, data, state, dispatch }: any) => {
const handleValueChange = (value: any, name: string) => {
const form = { ...state };
- _.set(form, name, value);
+ set(form, name, value);
dispatch({ type: "set_form", form });
};
diff --git a/src/Components/Facility/LegacyFacilityCNS.tsx b/src/Components/Facility/LegacyFacilityCNS.tsx
index b3a933b9a9f..5b0005c9daf 100644
--- a/src/Components/Facility/LegacyFacilityCNS.tsx
+++ b/src/Components/Facility/LegacyFacilityCNS.tsx
@@ -16,7 +16,7 @@ import Pagination from "../Common/Pagination";
import { PatientModel } from "../Patient/models";
import { FacilityModel } from "./models";
import AutocompleteFormField from "../Form/FormFields/Autocomplete";
-import { uniqBy } from "lodash";
+import { uniqBy } from "lodash-es";
import DialogModal from "../Common/Dialog";
import { LegacyMonitorCard } from "./LegacyMonitorCard";
diff --git a/src/Components/Facility/LegacyMonitorCard.tsx b/src/Components/Facility/LegacyMonitorCard.tsx
index 9f9fb8f1ea0..61bff3d607b 100644
--- a/src/Components/Facility/LegacyMonitorCard.tsx
+++ b/src/Components/Facility/LegacyMonitorCard.tsx
@@ -4,6 +4,7 @@ import CareIcon from "../../CAREUI/icons/CareIcon";
import { PatientModel } from "../Patient/models";
import LegacyPatientVitalsCard from "../Patient/LegacyPatientVitalsCard";
import { AssetLocationObject } from "../Assets/AssetTypes";
+import { formatAge } from "../../Utils/utils";
interface MonitorCardProps {
facilityId: string;
@@ -28,7 +29,7 @@ export const LegacyMonitorCard = ({
{patient.name}
- {patient.age}y |{" "}
+ {formatAge(patient.age, patient.date_of_birth)} |{" "}
{GENDER_TYPES.find((g) => g.id === patient.gender)?.icon}
diff --git a/src/Components/Facility/LiveFeedScreen.tsx b/src/Components/Facility/LiveFeedScreen.tsx
new file mode 100644
index 00000000000..e744ef404cd
--- /dev/null
+++ b/src/Components/Facility/LiveFeedScreen.tsx
@@ -0,0 +1,230 @@
+import { Fragment, useContext, useEffect, useState } from "react";
+import useFilters from "../../Common/hooks/useFilters";
+import useFullscreen from "../../Common/hooks/useFullscreen";
+import { FacilityModel } from "./models";
+import Loading from "../Common/Loading";
+import Page from "../Common/components/Page";
+import ButtonV2 from "../Common/components/ButtonV2";
+import CareIcon from "../../CAREUI/icons/CareIcon";
+import { classNames } from "../../Utils/utils";
+import { LocationSelect } from "../Common/LocationSelect";
+import Pagination from "../Common/Pagination";
+import { SidebarShrinkContext } from "../Common/Sidebar/Sidebar";
+import { AssetData } from "../Assets/AssetTypes";
+import { Popover, Transition } from "@headlessui/react";
+import { FieldLabel } from "../Form/FormFields/FormField";
+import CheckBoxFormField from "../Form/FormFields/CheckBoxFormField";
+import LiveFeedTile from "./LiveFeedTile";
+import { getCameraConfig } from "../../Utils/transformUtils";
+import { getPermittedFacility, listAssets } from "../../Redux/actions";
+import { useDispatch } from "react-redux";
+
+const PER_PAGE_LIMIT = 6;
+
+interface Props {
+ facilityId: string;
+}
+
+export default function LiveFeedScreen({ facilityId }: Props) {
+ const dispatch = useDispatch();
+ const [isFullscreen, setFullscreen] = useFullscreen();
+ const sidebar = useContext(SidebarShrinkContext);
+
+ const [facility, setFacility] = useState();
+ const [assets, setAssets] = useState();
+ const [totalCount, setTotalCount] = useState(0);
+ const { qParams, updateQuery, removeFilter, updatePage } = useFilters({
+ limit: PER_PAGE_LIMIT,
+ });
+
+ const [refresh_presets_hash, setRefreshPresetsHash] = useState(
+ Number(new Date())
+ );
+
+ // To automatically collapse sidebar.
+ useEffect(() => {
+ sidebar.setShrinked(true);
+
+ return () => {
+ sidebar.setShrinked(sidebar.shrinked);
+ };
+ }, []);
+
+ useEffect(() => {
+ async function fetchFacilityOrObject() {
+ if (facility) return facility;
+ const res = await dispatch(getPermittedFacility(facilityId));
+ if (res.status !== 200) return;
+ setFacility(res.data);
+ return res.data as FacilityModel;
+ }
+
+ async function fetchData() {
+ setAssets(undefined);
+
+ const filters = {
+ ...qParams,
+ page: qParams.page || 1,
+ limit: PER_PAGE_LIMIT,
+ offset: (qParams.page ? qParams.page - 1 : 0) * PER_PAGE_LIMIT,
+ asset_class: "ONVIF",
+ facility: facilityId || "",
+ location: qParams.location,
+ bed_is_occupied: qParams.bed_is_occupied,
+ };
+
+ const [facilityObj, res] = await Promise.all([
+ fetchFacilityOrObject(),
+ dispatch(listAssets(filters)),
+ ]);
+
+ if (!facilityObj || res.status !== 200) {
+ return;
+ }
+ console.log(facilityObj, res.data);
+ const entries = res.data.results;
+
+ setTotalCount(entries.length);
+ setAssets(entries);
+ }
+ fetchData();
+ setRefreshPresetsHash(Number(new Date()));
+ }, [
+ dispatch,
+ facilityId,
+ qParams.page,
+ qParams.location,
+ qParams.ordering,
+ qParams.bed_is_occupied,
+ ]);
+
+ return (
+
+
+
+
+
+ Settings and Filters
+
+
+
+
+
+
+
+
+ {totalCount} Camera
+ present
+
+
+
+
+
+
+ Filter by Location
+
+
+ {
+ location
+ ? updateQuery({ location })
+ : removeFilter("location");
+ }}
+ selected={qParams.location}
+ showAll={false}
+ multiple={false}
+ facilityId={facilityId}
+ errors=""
+ errorClassName="hidden"
+ />
+
+
+ {
+ if (value) {
+ updateQuery({ [name]: value });
+ } else {
+ removeFilter(name);
+ }
+ }}
+ labelClassName="text-sm"
+ errorClassName="hidden"
+ />
+ setFullscreen(!isFullscreen)}
+ className="tooltip !h-11"
+ >
+
+ {isFullscreen ? "Exit Fullscreen" : "Fullscreen"}
+
+
+
+
+
+
+
+ updatePage(page)}
+ />
+
+ }
+ >
+ {assets === undefined ? (
+
+ ) : assets.length === 0 ? (
+
+ No Camera present in this location or facility.
+
+ ) : (
+
+ {assets.map((asset, idx) => (
+
+ {/* */}
+
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/src/Components/Facility/LiveFeedTile.tsx b/src/Components/Facility/LiveFeedTile.tsx
new file mode 100644
index 00000000000..945240b9c6c
--- /dev/null
+++ b/src/Components/Facility/LiveFeedTile.tsx
@@ -0,0 +1,1045 @@
+// import axios from "axios";
+// import React, { useEffect, useState, useRef, useCallback } from "react";
+// import * as Notification from "../../Utils/Notifications.js";
+// import { useDispatch } from "react-redux";
+// import ReactPlayer from "react-player";
+// import { getAsset, listAssetBeds } from "../../Redux/actions";
+// import { statusType, useAbortableEffect } from "../../Common/utils";
+// import { useTranslation } from "react-i18next";
+// import useFullscreen from "../../Common/hooks/useFullscreen.js";
+// interface LiveFeedTileProps {
+// assetId: string;
+// }
+
+// interface CameraPosition {
+// x: number;
+// y: number;
+// zoom: number;
+// }
+
+// // string:string dictionary
+// interface CameraPreset {
+// [key: string]: string;
+// }
+
+// export default function LiveFeedTile(props: LiveFeedTileProps) {
+// const dispatch: any = useDispatch();
+// const { assetId } = props;
+// const [sourceUrl, setSourceUrl] = useState ();
+// const [asset, setAsset] = useState();
+// const [presets, setPresets] = useState([]);
+// const [bedPresets, setBedPresets] = useState([]);
+// const [loading, setLoading] = useState(true);
+// // const [showControls, setShowControls] = useState(false);
+// const [showDefaultPresets, setShowDefaultPresets] = useState(false);
+// const [position, setPosition] = useState({
+// x: 0,
+// y: 0,
+// zoom: 0,
+// });
+// const { t } = useTranslation();
+// const [_isFullscreen, setFullscreen] = useFullscreen();
+// // const [toggle, setToggle] = useState(false);
+
+// useEffect(() => {
+// let loadingTimeout: any;
+// if (loading === true)
+// loadingTimeout = setTimeout(() => {
+// setLoading(false);
+// }, 6000);
+// return () => {
+// if (loadingTimeout) clearTimeout(loadingTimeout);
+// };
+// }, [loading]);
+
+// const fetchData = useCallback(
+// async (status: statusType) => {
+// setLoading(true);
+// console.log("fetching asset");
+// const assetData: any = await dispatch(getAsset(assetId));
+// if (!status.aborted) {
+// // setLoading(false);
+// if (!assetData.data)
+// Notification.Error({
+// msg: t("something_went_wrong"),
+// });
+// else {
+// setAsset(assetData.data);
+// }
+// }
+// },
+// [dispatch, assetId]
+// );
+
+// useAbortableEffect(
+// (status: statusType) => fetchData(status),
+// [dispatch, fetchData]
+// );
+// const requestStream = () => {
+// axios
+// .post(`https://${asset.meta.middleware_hostname}/start`, {
+// uri: "rtsp://remote:qwerty123@192.168.1.64:554/",
+// })
+// .then((resp: any) => {
+// setSourceUrl(
+// `https://${asset.meta.middleware_hostname}${resp.data.uri}`
+// );
+// })
+// .catch((_ex: any) => {
+// // console.error('Error while refreshing',ex);
+// });
+// };
+// const stopStream = (url: string | undefined) => {
+// console.log("stop", url);
+// if (url) {
+// const urlSegments = url.split("/");
+// const id = urlSegments?.pop();
+// axios
+// .post(`https://${asset.meta.middleware_hostname}/stop`, {
+// id,
+// })
+// .then((resp: any) => {
+// console.log(resp);
+// // setSourceUrl(`https://${middlewareHostname}${resp.data.uri}`);
+// })
+// .catch((_ex: any) => {
+// // console.error('Error while refreshing',ex);
+// });
+// }
+// };
+// const getCameraStatus = (asset: any) => {
+// axios
+// .get(
+// `https://${asset.meta.middleware_hostname}/status?hostname=${asset.hostname}&port=${asset.port}&username=${asset.username}&password=${asset.password}`
+// )
+// .then((resp: any) => {
+// setPosition(resp.data.position);
+// })
+// .catch((_ex: any) => {
+// // console.error('Error while refreshing',ex);
+// });
+// };
+// const getPresets = (asset: any) => {
+// const url = `https://${asset.meta.middleware_hostname}/presets?hostname=${asset.hostname}&port=${asset.port}&username=${asset.username}&password=${asset.password}`;
+// axios
+// .get(url)
+// .then((resp: any) => {
+// setPresets(resp.data);
+// })
+// .catch((_ex: any) => {
+// // console.error("Error while refreshing", ex);
+// });
+// };
+// const getBedPresets = async (_asset: any) => {
+// const bedAssets = await dispatch(listAssetBeds({ asset: props.assetId }));
+// setBedPresets(bedAssets.data.results);
+// };
+// const gotoBedPreset = (preset: any) => {
+// absoluteMove(preset.meta.position);
+// };
+// const gotoPreset = (preset: number) => {
+// axios
+// .post(`https://${asset.meta.middleware_hostname}/gotoPreset`, {
+// ...asset,
+// preset,
+// })
+// .then((resp: any) => {
+// console.log(resp.data);
+// })
+// .catch((_ex: any) => {
+// // console.error('Error while refreshing',ex);
+// });
+// };
+// const requestPTZ = (action: string) => {
+// setLoading(true);
+// if (!position) {
+// getCameraStatus(asset);
+// } else {
+// const data = {
+// x: 0,
+// y: 0,
+// zoom: 0,
+// } as any;
+// console.log(action);
+// // Relative X Y Coordinates
+// switch (action) {
+// case "up":
+// data.y = 0.05;
+// break;
+// case "down":
+// data.y = -0.05;
+// break;
+// case "left":
+// data.x = -0.05;
+// break;
+// case "right":
+// data.x = 0.05;
+// break;
+// case "zoomIn":
+// data.zoom = 0.05;
+// break;
+// case "zoomOut":
+// data.zoom = -0.05;
+// break;
+// case "stop":
+// stopStream(sourceUrl);
+// setSourceUrl(undefined);
+// return;
+// case "reset":
+// setSourceUrl(undefined);
+// requestStream();
+// return;
+// default:
+// break;
+// }
+// axios
+// .post(`https://${asset.meta.middleware_hostname}/relativeMove`, {
+// ...data,
+// ...asset,
+// })
+// .then((resp: any) => {
+// console.log(resp.data);
+// getCameraStatus(asset);
+// })
+// .catch((_ex: any) => {
+// // console.error('Error while refreshing',ex);
+// });
+// }
+// };
+
+// const absoluteMove = (data: any) => {
+// setLoading(true);
+// axios
+// .post(`https://${asset.meta.middleware_hostname}/absoluteMove`, {
+// ...data,
+// ...asset,
+// })
+// .then((_resp: any) => {
+// getCameraStatus(asset);
+// })
+// .catch((ex: any) => {
+// console.error("Error while absolute move", ex);
+// });
+// };
+
+// useEffect(() => {
+// if (asset) {
+// getPresets(asset);
+// getBedPresets(asset);
+// requestStream();
+// }
+// }, [asset]);
+
+// useEffect(() => {
+// if (bedPresets.length > 0) absoluteMove(bedPresets[0].meta.position);
+// }, [bedPresets]);
+
+// // useEffect(() => {
+// // const timer = setTimeout(() => {
+// // setShowControls(toggle);
+// // }, 300);
+// // return () => clearTimeout(timer);
+// // }, [toggle]);
+
+// const liveFeedPlayerRef = useRef(null);
+// const handleClickFullscreen = () => {
+// if (liveFeedPlayerRef.current) {
+// setFullscreen(true, liveFeedPlayerRef.current.wrapper);
+// }
+// };
+
+// const viewOptions = presets
+// ? Object.entries(presets)
+// .map(([key, value]) => ({ label: key, value }))
+// .slice(0, 10)
+// : Array.from(Array(10), (_, i) => ({
+// label: t("monitor") + (i + 1),
+// value: i + 1,
+// }));
+
+// const cameraPTZ = [
+// { icon: "fa fa-arrow-up", label: t("up"), action: "up" },
+// { icon: "fa fa-arrow-down", label: t("down"), action: "down" },
+// { icon: "fa fa-arrow-left", label: t("left"), action: "left" },
+// { icon: "fa fa-arrow-right", label: t("right"), action: "right" },
+// { icon: "fa fa-search-plus", label: t("zoom_in"), action: "zoomIn" },
+// { icon: "fa fa-search-minus", label: t("zoom_out"), action: "zoomOut" },
+// { icon: "fa fa-stop", label: t("stop"), action: "stop" },
+// { icon: "fa fa-undo", label: t("reset"), action: "reset" },
+// ];
+
+// return (
+//
+//
+//
+//
+// {sourceUrl ? (
+//
+// {
+// // requestStream();
+// console.log("Error", e);
+// console.log("Data", data);
+// console.log("HLS Instance", hlsInstance);
+// console.log("HLS Global", hlsGlobal);
+// if (e === "hlsError") {
+// const recovered = hlsInstance.recoverMediaError();
+// console.log(recovered);
+// }
+// }}
+// />
+//
+// ) : (
+//
+//
+// STATUS: OFFLINE
+//
+//
+// {t("feed_is_currently_not_live")}
+//
+//
+// )}
+//
+//
+//
+//
+//
+// {cameraPTZ.map((option: any) => (
+// {
+// // console.log(option.action);
+// requestPTZ(option.action);
+// }}
+// >
+//
+//
+// ))}
+//
+//
+// {/*
+//
+// */}
+//
+//
+//
+//
+// {/* div with "Loading" at the center */}
+//
+//
+// {t("moving_camera")}
+//
+//
+//
+//
+//
+// {showDefaultPresets
+// ? viewOptions.map((option: any) => (
+// {
+// setLoading(true);
+// gotoPreset(option.value);
+// }}
+// >
+//
+//
+// ))
+// : bedPresets.map((preset: any, index: number) => (
+// {
+// setLoading(true);
+// gotoBedPreset(preset);
+// }}
+// key={preset.id}
+// >
+//
+//
+// ))}
+//
+//
+//
+// );
+// }
+
+import { useEffect, useState, useRef } from "react";
+import { useDispatch } from "react-redux";
+import useKeyboardShortcut from "use-keyboard-shortcut";
+import {
+ listAssetBeds,
+ partialUpdateAssetBed,
+ deleteAssetBed,
+} from "../../Redux/actions";
+import { getCameraPTZ } from "../../Common/constants";
+import {
+ StreamStatus,
+ useMSEMediaPlayer,
+} from "../../Common/hooks/useMSEplayer";
+import { useFeedPTZ } from "../../Common/hooks/useFeedPTZ";
+import * as Notification from "../../Utils/Notifications.js";
+import { AxiosError } from "axios";
+import { BedSelect } from "../Common/BedSelect";
+import { BedModel } from "./models";
+import useWindowDimensions from "../../Common/hooks/useWindowDimensions";
+import CareIcon from "../../CAREUI/icons/CareIcon";
+import ConfirmDialog from "../Common/ConfirmDialog";
+import { FieldLabel } from "../Form/FormFields/FormField";
+import useFullscreen from "../../Common/hooks/useFullscreen";
+import { FeedCameraPTZHelpButton } from "./Consultations/Feed";
+
+const LiveFeed = (props: any) => {
+ const middlewareHostname =
+ props.middlewareHostname || "dev_middleware.coronasafe.live";
+ const [presetsPage, setPresetsPage] = useState(0);
+ const cameraAsset = props.asset;
+ const [presets, setPresets] = useState([]);
+ const [bedPresets, setBedPresets] = useState([]);
+ const [showDefaultPresets, setShowDefaultPresets] = useState(false);
+ const [precision, setPrecision] = useState(1);
+ const [streamStatus, setStreamStatus] = useState(
+ StreamStatus.Offline
+ );
+ const [videoStartTime, setVideoStartTime] = useState(null);
+ const [bed, setBed] = useState({});
+ const [preset, setNewPreset] = useState("");
+ const [loading, setLoading] = useState();
+ const dispatch: any = useDispatch();
+ const [page, setPage] = useState({
+ count: 0,
+ limit: 8,
+ offset: 0,
+ });
+ const [toDelete, setToDelete] = useState(null);
+ const [toUpdate, setToUpdate] = useState(null);
+ const [_isFullscreen, setFullscreen] = useFullscreen();
+
+ const { width } = useWindowDimensions();
+ const extremeSmallScreenBreakpoint = 320;
+ const isExtremeSmallScreen =
+ width <= extremeSmallScreenBreakpoint ? true : false;
+ const liveFeedPlayerRef = useRef(null);
+
+ const videoEl = liveFeedPlayerRef.current as HTMLVideoElement;
+
+ const url = `wss://${middlewareHostname}/stream/${cameraAsset?.accessKey}/channel/0/mse?uuid=${cameraAsset?.accessKey}&channel=0`;
+
+ const { startStream } = useMSEMediaPlayer({
+ config: {
+ middlewareHostname,
+ ...cameraAsset,
+ },
+ url,
+ videoEl,
+ });
+
+ const refreshPresetsHash = props.refreshPresetsHash;
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const [currentPreset, setCurrentPreset] = useState();
+ const {
+ absoluteMove,
+ getCameraStatus,
+ getPTZPayload,
+ getPresets,
+ gotoPreset,
+ relativeMove,
+ } = useFeedPTZ({
+ config: {
+ middlewareHostname,
+ ...cameraAsset,
+ },
+ dispatch,
+ });
+
+ const fetchCameraPresets = () =>
+ getPresets({
+ onSuccess: (resp) => {
+ setPresets(resp);
+ },
+ onError: (resp) => {
+ resp instanceof AxiosError &&
+ Notification.Error({
+ msg: "Camera is offline",
+ });
+ },
+ });
+
+ const calculateVideoLiveDelay = () => {
+ const video = liveFeedPlayerRef.current as HTMLVideoElement;
+ if (!video || !videoStartTime) return 0;
+
+ const timeDifference =
+ (new Date().getTime() - videoStartTime.getTime()) / 1000;
+
+ return timeDifference - video.currentTime;
+ };
+
+ const getBedPresets = async (id: any) => {
+ const bedAssets = await dispatch(
+ listAssetBeds({
+ asset: id,
+ limit: page.limit,
+ offset: page.offset,
+ })
+ );
+ setBedPresets(bedAssets?.data?.results);
+ setPage({
+ ...page,
+ count: bedAssets?.data?.count,
+ });
+ };
+
+ const deletePreset = async (id: any) => {
+ const res = await dispatch(deleteAssetBed(id));
+ if (res?.status === 204) {
+ Notification.Success({ msg: "Preset deleted successfully" });
+ getBedPresets(cameraAsset.id);
+ } else {
+ Notification.Error({
+ msg: "Error while deleting Preset: " + (res?.data?.detail || ""),
+ });
+ }
+ setToDelete(null);
+ };
+
+ const updatePreset = async (currentPreset: any) => {
+ const data = {
+ bed_id: bed.id,
+ preset_name: preset,
+ };
+ const response = await dispatch(
+ partialUpdateAssetBed(
+ {
+ asset: currentPreset.asset_object.id,
+ bed: bed.id,
+ meta: {
+ ...currentPreset.meta,
+ ...data,
+ },
+ },
+ currentPreset?.id
+ )
+ );
+ if (response && response.status === 200) {
+ Notification.Success({ msg: "Preset Updated" });
+ } else {
+ Notification.Error({ msg: "Something Went Wrong" });
+ }
+ getBedPresets(cameraAsset?.id);
+ fetchCameraPresets();
+ setToUpdate(null);
+ };
+
+ const gotoBedPreset = (preset: any) => {
+ setLoading("Moving");
+ absoluteMove(preset.meta.position, {
+ onSuccess: () => setLoading(undefined),
+ });
+ };
+
+ useEffect(() => {
+ if (cameraAsset?.hostname) {
+ fetchCameraPresets();
+ }
+ }, []);
+
+ useEffect(() => {
+ setNewPreset(toUpdate?.meta?.preset_name);
+ setBed(toUpdate?.bed_object);
+ }, [toUpdate]);
+
+ useEffect(() => {
+ getBedPresets(cameraAsset.id);
+ if (bedPresets?.[0]?.position) {
+ absoluteMove(bedPresets[0]?.position, {});
+ }
+ }, [page.offset, cameraAsset.id, refreshPresetsHash]);
+
+ const viewOptions = (page: number) => {
+ return presets
+ ? Object.entries(presets)
+ .map(([key, value]) => ({ label: key, value }))
+ .slice(page, page + 10)
+ : Array.from(Array(10), (_, i) => ({
+ label: "Monitor " + (i + 1),
+ value: i + 1,
+ }));
+ };
+ useEffect(() => {
+ let tId: any;
+ if (streamStatus !== StreamStatus.Playing) {
+ setStreamStatus(StreamStatus.Loading);
+ tId = setTimeout(() => {
+ startStream({
+ onSuccess: () => setStreamStatus(StreamStatus.Playing),
+ onError: () => setStreamStatus(StreamStatus.Offline),
+ });
+ }, 500);
+ }
+
+ return () => {
+ clearTimeout(tId);
+ };
+ }, [startStream, streamStatus]);
+
+ const handlePagination = (cOffset: number) => {
+ setPage({
+ ...page,
+ offset: cOffset,
+ });
+ };
+
+ const cameraPTZActionCBs: { [key: string]: (option: any) => void } = {
+ precision: () => {
+ setPrecision((precision: number) =>
+ precision === 16 ? 1 : precision * 2
+ );
+ },
+ reset: () => {
+ setStreamStatus(StreamStatus.Loading);
+ setVideoStartTime(null);
+ startStream({
+ onSuccess: () => setStreamStatus(StreamStatus.Playing),
+ onError: () => setStreamStatus(StreamStatus.Offline),
+ });
+ },
+ fullScreen: () => {
+ if (!liveFeedPlayerRef.current) return;
+ setFullscreen(true, liveFeedPlayerRef.current);
+ },
+ updatePreset: (option) => {
+ getCameraStatus({
+ onSuccess: async (data) => {
+ console.log({ currentPreset, data });
+ if (currentPreset?.asset_object?.id && data?.position) {
+ setLoading(option.loadingLabel);
+ console.log("Updating Preset");
+ const response = await dispatch(
+ partialUpdateAssetBed(
+ {
+ asset: currentPreset.asset_object.id,
+ bed: currentPreset.bed_object.id,
+ meta: {
+ ...currentPreset.meta,
+ position: data?.position,
+ },
+ },
+ currentPreset?.id
+ )
+ );
+ if (response && response.status === 200) {
+ Notification.Success({ msg: "Preset Updated" });
+ getBedPresets(cameraAsset?.id);
+ fetchCameraPresets();
+ }
+ setLoading(undefined);
+ }
+ },
+ });
+ },
+ other: (option) => {
+ setLoading(option.loadingLabel);
+ relativeMove(getPTZPayload(option.action, precision), {
+ onSuccess: () => setLoading(undefined),
+ });
+ },
+ };
+
+ const cameraPTZ = getCameraPTZ(precision).map((option) => {
+ const cb =
+ cameraPTZActionCBs[
+ cameraPTZActionCBs[option.action] ? option.action : "other"
+ ];
+ return { ...option, callback: () => cb(option) };
+ });
+
+ // Voluntarily disabling eslint, since length of `cameraPTZ` is constant and
+ // hence shall not cause issues. (https://news.ycombinator.com/item?id=24363703)
+ for (const option of cameraPTZ) {
+ if (!option.shortcutKey) continue;
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ useKeyboardShortcut(option.shortcutKey, option.callback);
+ }
+
+ return (
+
+ {toDelete && (
+
+
+ Preset: {toDelete.meta.preset_name}
+
+
+ Bed: {toDelete.bed_object.name}
+
+
+ }
+ action="Delete"
+ variant="danger"
+ onClose={() => setToDelete(null)}
+ onConfirm={() => deletePreset(toDelete.id)}
+ />
+ )}
+ {toUpdate && (
+ setToUpdate(null)}
+ onConfirm={() => updatePreset(toUpdate)}
+ >
+
+ Bed
+ setBed(selected as BedModel)}
+ selected={bed}
+ error=""
+ multiple={false}
+ location={cameraAsset.location_id}
+ facility={cameraAsset.facility_id}
+ />
+
+
+ )}
+
+
+
+ {/* ADD VIDEO PLAYER HERE */}
+
+
+
+ {streamStatus === StreamStatus.Playing &&
+ calculateVideoLiveDelay() > 3 && (
+
+
+ Slow Network Detected
+
+ )}
+
+ {loading && (
+
+ )}
+ {/* { streamStatus > 0 && */}
+
+ {streamStatus === StreamStatus.Offline && (
+
+
+ STATUS: OFFLINE
+
+
+ Feed is currently not live.
+
+
+ Click refresh button to try again.
+
+
+ )}
+ {streamStatus === StreamStatus.Stop && (
+
+
+ STATUS: STOPPED
+
+ Feed is Stooped.
+
+ Click refresh button to start feed.
+
+
+ )}
+ {streamStatus === StreamStatus.Loading && (
+
+
+ STATUS: LOADING
+
+
+ Fetching latest feed.
+
+
+ )}
+
+
+
+ {cameraPTZ.map((option) => {
+ const shortcutKeyDescription =
+ option.shortcutKey &&
+ option.shortcutKey
+ .join(" + ")
+ .replace("Control", "Ctrl")
+ .replace("ArrowUp", "↑")
+ .replace("ArrowDown", "↓")
+ .replace("ArrowLeft", "←")
+ .replace("ArrowRight", "→");
+
+ return (
+
+ );
+ })}
+
+
+
+
+
+
+
+
+
+
+ {showDefaultPresets ? (
+ <>
+ {viewOptions(presetsPage)?.map((option: any, i) => (
+
+ ))}
+ >
+ ) : (
+ <>
+ {bedPresets?.map((preset: any, index: number) => (
+
+
+
+
+
+
+
+ ))}
+ >
+ )}
+
+ {/* Page Number Next and Prev buttons */}
+ {showDefaultPresets ? (
+
+
+
+
+ ) : (
+
+
+
+
+ )}
+ {props?.showRefreshButton && (
+
+ )}
+
+
+
+
+
+ );
+};
+
+export default LiveFeed;
diff --git a/src/Components/Facility/LocationManagement.tsx b/src/Components/Facility/LocationManagement.tsx
index 01e0c246ecb..c018a7c222b 100644
--- a/src/Components/Facility/LocationManagement.tsx
+++ b/src/Components/Facility/LocationManagement.tsx
@@ -1,176 +1,111 @@
-import { useCallback, useState, ReactElement, lazy } from "react";
-
-import { useDispatch } from "react-redux";
-import { statusType, useAbortableEffect } from "../../Common/utils";
-import { listFacilityAssetLocation, getAnyFacility } from "../../Redux/actions";
-import Pagination from "../Common/Pagination";
-import { LocationModel } from "./models";
+import { lazy } from "react";
import ButtonV2 from "../Common/components/ButtonV2";
import { NonReadOnlyUsers } from "../../Utils/AuthorizeFor";
import CareIcon from "../../CAREUI/icons/CareIcon";
import Page from "../Common/components/Page";
-const Loading = lazy(() => import("../Common/Loading"));
+import routes from "../../Redux/api";
+import PaginatedList from "../../CAREUI/misc/PaginatedList";
+import { LocationModel } from "./models";
-interface LocationManagementProps {
- facilityId: string;
-}
+const Loading = lazy(() => import("../Common/Loading"));
-interface LocationRowProps {
- id: string;
+interface Props {
facilityId: string;
- name: string;
- description: string;
}
-const LocationRow = (props: LocationRowProps) => {
- const { id, facilityId, name, description } = props;
-
+export default function LocationManagement({ facilityId }: Props) {
return (
-
-
-
- {name}
- {description}
-
-
-
-
-
- Edit
-
- (
+
+
+ Add New Location
+
+ }
>
-
- Manage Beds
-
-
-
- );
-};
-
-export const LocationManagement = (props: LocationManagementProps) => {
- const { facilityId } = props;
- const dispatchAction: any = useDispatch();
- const [isLoading, setIsLoading] = useState(false);
- let location: ReactElement | null = null;
- let locationsList: ReactElement[] | ReactElement = [];
- const [locations, setLocations] = useState([]);
- const [offset, setOffset] = useState(0);
- const [currentPage, setCurrentPage] = useState(1);
- const [totalCount, setTotalCount] = useState(0);
- const [facilityName, setFacilityName] = useState("");
- const limit = 14;
-
- const fetchData = useCallback(
- async (status: statusType) => {
- setIsLoading(true);
- const facility = await dispatchAction(getAnyFacility(facilityId));
-
- setFacilityName(facility?.data?.name || "");
-
- const res = await dispatchAction(
- listFacilityAssetLocation(
- { limit, offset },
- { facility_external_id: facilityId }
- )
- );
- if (!status.aborted) {
- if (res?.data) {
- setLocations(res.data.results);
- setTotalCount(res.data.count);
- }
- setIsLoading(false);
- }
- },
- [dispatchAction, offset, facilityId]
- );
-
- useAbortableEffect(
- (status: statusType) => {
- fetchData(status);
- },
- [fetchData]
- );
+
+
+
+ Add New Location
+
+
+
+ No locations available
+
- const handlePagination = (page: number, limit: number) => {
- const offset = (page - 1) * limit;
- setCurrentPage(page);
- setOffset(offset);
- };
+
+
+
- if (locations?.length) {
- locationsList = locations.map((locationItem: LocationModel) => (
-
- ));
- } else if (locations && locations.length === 0) {
- locationsList = (
-
- No locations available
-
- );
- }
+ className="my-8 flex grow flex-col gap-3 lg:mx-8">
+ {(item) => }
+
- if (locations) {
- location = (
- <>
-
- {locationsList}
-
- {totalCount > limit && (
-
-
+
- )}
- >
- );
- }
-
- if (isLoading || !locations) {
- return ;
- }
+
+ )}
+
+ );
+}
- return (
-
-
-
-
-
- Add New Location
-
-
- {location}
+const Location = ({
+ name,
+ description,
+ middleware_address,
+ id,
+}: LocationModel) => (
+
+
+
+
+ {name}
+
+ {description || "-"}
+
+
+
+ {middleware_address}
+
-
- );
-};
+
+
+
+
+
+ Edit
+
+
+
+ Manage Beds
+
+
+
+);
diff --git a/src/Components/Facility/TreatmentSummary.tsx b/src/Components/Facility/TreatmentSummary.tsx
index dbca307d38b..674c161428d 100644
--- a/src/Components/Facility/TreatmentSummary.tsx
+++ b/src/Components/Facility/TreatmentSummary.tsx
@@ -10,7 +10,7 @@ import { statusType, useAbortableEffect } from "../../Common/utils";
import { PatientModel } from "../Patient/models";
import { GENDER_TYPES } from "../../Common/constants";
-import { formatDate, formatDateTime } from "../../Utils/utils";
+import { formatAge, formatDate, formatDateTime } from "../../Utils/utils";
const Loading = lazy(() => import("../Common/Loading"));
const TreatmentSummary = (props: any) => {
@@ -132,7 +132,8 @@ const TreatmentSummary = (props: any) => {
- Age : {patientData.age}
+ Age :{" "}
+ {formatAge(patientData.age, patientData.date_of_birth, true)}
Date of admission :
@@ -235,13 +236,6 @@ const TreatmentSummary = (props: any) => {
: " ---"}
-
- Diagnosis :
- {consultationData.diagnosis
- ? consultationData.diagnosis
- : " ---"}
-
-
Physical Examination info :
{dailyRounds.physical_examination_info
diff --git a/src/Components/Facility/UpdateFacilityMiddleware.tsx b/src/Components/Facility/UpdateFacilityMiddleware.tsx
deleted file mode 100644
index 4fd5a4f24b0..00000000000
--- a/src/Components/Facility/UpdateFacilityMiddleware.tsx
+++ /dev/null
@@ -1,179 +0,0 @@
-import { lazy, useCallback, useReducer, useState } from "react";
-import { useDispatch } from "react-redux";
-
-import { statusType, useAbortableEffect } from "../../Common/utils";
-import {
- getPermittedFacility,
- partialUpdateFacility,
-} from "../../Redux/actions";
-import * as Notification from "../../Utils/Notifications.js";
-import { navigate } from "raviger";
-import { Cancel, Submit } from "../Common/components/ButtonV2";
-import TextFormField from "../Form/FormFields/TextFormField";
-import Page from "../Common/components/Page";
-import useConfig from "../../Common/hooks/useConfig";
-import { ConfigureHealthFacility } from "../ABDM/ConfigureHealthFacility";
-const Loading = lazy(() => import("../Common/Loading"));
-
-const initForm = {
- name: "",
- state: 0,
- district: 0,
- localbody: 0,
- ward: 0,
- middleware_address: "",
-};
-const initialState = {
- form: { ...initForm },
- errors: {},
-};
-
-const FormReducer = (state = initialState, action: any) => {
- switch (action.type) {
- case "set_form": {
- return {
- ...state,
- form: action.form,
- };
- }
- case "set_error": {
- return {
- ...state,
- errors: action.errors,
- };
- }
- default:
- return state;
- }
-};
-
-export const UpdateFacilityMiddleware = (props: any) => {
- const [state, dispatch] = useReducer(FormReducer, initialState);
- const { facilityId } = props;
- const dispatchAction: any = useDispatch();
- const [isLoading, setIsLoading] = useState(false);
- const config = useConfig();
-
- const fetchData = useCallback(
- async (status: statusType) => {
- if (facilityId) {
- setIsLoading(true);
- const res = await dispatchAction(getPermittedFacility(facilityId));
- if (!status.aborted && res.data) {
- const formData = {
- name: res.data.name,
- state: res.data.state,
- district: res.data.district,
- local_body: res.data.local_body,
- ward: res.data.ward,
- middleware_address: res.data.middleware_address,
- };
- dispatch({ type: "set_form", form: formData });
- } else {
- navigate(`/facility/${facilityId}`);
- }
- setIsLoading(false);
- }
- },
- [dispatchAction, facilityId]
- );
-
- useAbortableEffect(
- (status: statusType) => {
- fetchData(status);
- },
- [dispatch, fetchData]
- );
-
- const handleSubmit = async (e: any) => {
- e.preventDefault();
- setIsLoading(true);
- if (!state.form.middleware_address) {
- dispatch({
- type: "set_error",
- errors: { middleware_address: ["Middleware Address is required"] },
- });
- setIsLoading(false);
- return;
- }
- if (
- state.form.middleware_address.match(
- /^(?!https?:\/\/)[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)*\.[a-zA-Z]{2,}$/
- ) === null
- ) {
- dispatch({
- type: "set_error",
- errors: {
- middleware_address: ["Invalid Middleware Address"],
- },
- });
- setIsLoading(false);
- return;
- }
- const data: any = {
- ...state.form,
- middleware_address: state.form.middleware_address,
- };
-
- const res = await dispatchAction(partialUpdateFacility(facilityId, data));
- setIsLoading(false);
- if (res && res.data) {
- Notification.Success({
- msg: "Facility updated successfully",
- });
- navigate(`/facility/${facilityId}`);
- } else {
- if (res?.data)
- Notification.Error({
- msg: "Something went wrong: " + (res.data.detail || ""),
- });
- }
- setIsLoading(false);
- };
-
- const handleChange = (e: any) => {
- dispatch({
- type: "set_form",
- form: { ...state.form, [e.name]: e.value },
- });
- };
-
- if (isLoading) {
- return ;
- }
-
- return (
-
-
- {config.enable_abdm ? (
-
- ) : (
- <>>
- )}
-
- );
-};
diff --git a/src/Components/Facility/models.tsx b/src/Components/Facility/models.tsx
index 31ce0709afc..e63e64ed552 100644
--- a/src/Components/Facility/models.tsx
+++ b/src/Components/Facility/models.tsx
@@ -2,6 +2,8 @@ import { AssignedToObjectModel } from "../Patient/models";
import { ProcedureType } from "../Common/prescription-builder/ProcedureBuilder";
import { NormalPrescription, PRNPrescription } from "../Medicine/models";
import { AssetData } from "../Assets/AssetTypes";
+import { UserBareMinimum } from "../Users/models";
+import { ConsultationDiagnosis, CreateDiagnosis } from "../Diagnosis/types";
export interface LocalBodyModel {
name: string;
@@ -27,7 +29,6 @@ export interface WardModel {
export interface FacilityModel {
id?: number;
name?: string;
- district?: number;
read_cover_image_url?: string;
facility_type?: string;
address?: string;
@@ -52,6 +53,10 @@ export interface FacilityModel {
ward_object?: WardModel;
modified_date?: string;
created_date?: string;
+ state?: number;
+ district?: number;
+ local_body?: number;
+ ward?: number;
}
export interface CapacityModal {
@@ -109,10 +114,11 @@ export interface ConsultationModel {
consultation_status?: number;
is_kasp?: boolean;
kasp_enabled_date?: string;
- diagnosis?: string;
- icd11_diagnoses_object?: ICD11DiagnosisModel[];
- icd11_provisional_diagnoses_object?: ICD11DiagnosisModel[];
+ readonly diagnoses?: ConsultationDiagnosis[];
+ create_diagnoses?: CreateDiagnosis[]; // Used for bulk creating diagnoses upon consultation creation
+ deprecated_verified_by?: string;
verified_by?: string;
+ verified_by_object?: UserBareMinimum;
suggestion_text?: string;
symptoms?: Array ;
symptoms_text?: string;
@@ -138,6 +144,8 @@ export interface ConsultationModel {
cause_of_death?: string;
death_datetime?: string;
death_confirmed_doctor?: string;
+ is_readmission?: boolean;
+ medico_legal_case?: boolean;
}
export interface PatientStatsModel {
id?: number;
@@ -181,6 +189,7 @@ export interface LocationModel {
id?: string;
name?: string;
description?: string;
+ middleware_address?: string;
facility?: {
name: string;
};
@@ -214,11 +223,3 @@ export interface CurrentBed {
privacy?: boolean;
meta: Record;
}
-
-// Voluntarily made as `type` for it to achieve type-safety when used with
-// `useAsyncOptions`
-export type ICD11DiagnosisModel = {
- id: string;
- label: string;
- parentId: string | null;
-};
diff --git a/src/Components/Form/AutoCompleteAsync.tsx b/src/Components/Form/AutoCompleteAsync.tsx
index d06067af957..4ad68e96d73 100644
--- a/src/Components/Form/AutoCompleteAsync.tsx
+++ b/src/Components/Form/AutoCompleteAsync.tsx
@@ -1,14 +1,16 @@
import { useEffect, useState, useMemo } from "react";
import { Combobox } from "@headlessui/react";
-import { debounce } from "lodash";
+import { debounce } from "lodash-es";
import { DropdownTransition } from "../Common/components/HelperComponents";
import CareIcon from "../../CAREUI/icons/CareIcon";
import {
MultiSelectOptionChip,
dropdownOptionClassNames,
} from "./MultiSelectMenuV2";
+import { useTranslation } from "react-i18next";
interface Props {
+ id?: string;
name?: string;
selected: any | any[];
fetchData: (search: string) => Promise | undefined;
@@ -23,12 +25,14 @@ interface Props {
placeholder?: string;
disabled?: boolean;
error?: string;
+ required?: boolean;
onBlur?: () => void;
onFocus?: () => void;
}
const AutoCompleteAsync = (props: Props) => {
const {
+ id,
name,
selected,
fetchData,
@@ -42,11 +46,13 @@ const AutoCompleteAsync = (props: Props) => {
className = "",
placeholder,
disabled = false,
+ required = false,
error,
} = props;
const [data, setData] = useState([]);
const [query, setQuery] = useState("");
const [loading, setLoading] = useState(false);
+ const { t } = useTranslation();
const hasSelection =
(!multiple && selected) || (multiple && selected?.length > 0);
@@ -78,6 +84,7 @@ const AutoCompleteAsync = (props: Props) => {
{
: placeholder || "Start typing to search..."
}
displayValue={() =>
- hasSelection && !multiple
- ? optionLabel && optionLabel(selected)
- : ""
+ hasSelection && !multiple ? optionLabel?.(selected) : ""
}
onChange={({ target }) => setQuery(target.value)}
onFocus={props.onFocus}
@@ -98,15 +103,31 @@ const AutoCompleteAsync = (props: Props) => {
}}
autoComplete="off"
/>
-
-
- {loading ? (
-
- ) : (
-
- )}
-
-
+ {!disabled && (
+
+
+ {hasSelection && !loading && !required && (
+
+ {
+ e.preventDefault();
+ onChange(null);
+ }}
+ />
+
+ {t("clear_selection")}
+
+
+ )}
+ {loading ? (
+
+ ) : (
+
+ )}
+
+
+ )}
diff --git a/src/Components/Form/Form.tsx b/src/Components/Form/Form.tsx
index 5b1cf018965..66383616034 100644
--- a/src/Components/Form/Form.tsx
+++ b/src/Components/Form/Form.tsx
@@ -1,4 +1,4 @@
-import { isEmpty, omitBy } from "lodash";
+import { isEmpty, omitBy } from "lodash-es";
import { useEffect, useMemo, useState } from "react";
import { classNames } from "../../Utils/utils";
import { Cancel, Submit } from "../Common/components/ButtonV2";
@@ -7,6 +7,7 @@ import { FormContextValue, createFormContext } from "./FormContext";
import { FieldChangeEvent } from "./FormFields/Utils";
import { FormDetails, FormErrors, FormState, formReducer } from "./Utils";
import { DraftSection, useAutoSaveReducer } from "../../Utils/AutoSave";
+import * as Notification from "../../Utils/Notifications";
type Props = {
className?: string;
@@ -51,6 +52,10 @@ const Form = ({
if (Object.keys(errors).length) {
dispatch({ type: "set_errors", errors });
+
+ if (errors.$all) {
+ Notification.Error({ msg: errors.$all });
+ }
return;
}
}
diff --git a/src/Components/Form/FormFields/Autocomplete.tsx b/src/Components/Form/FormFields/Autocomplete.tsx
index dcf3ab5e8b6..bf3840b2c44 100644
--- a/src/Components/Form/FormFields/Autocomplete.tsx
+++ b/src/Components/Form/FormFields/Autocomplete.tsx
@@ -157,34 +157,37 @@ export const Autocomplete = (props: AutocompleteProps) => {
placeholder={props.placeholder ?? "Select"}
displayValue={(value: any) => value?.label || ""}
onChange={(event) => setQuery(event.target.value.toLowerCase())}
+ onBlur={() => value && setQuery("")}
autoComplete="off"
/>
-
-
- {value?.icon}
-
- {value && !props.isLoading && !props.required && (
-
- {
- e.preventDefault();
- props.onChange(undefined);
- }}
- />
-
- {t("clear_selection")}
-
-
- )}
-
- {props.isLoading ? (
-
- ) : (
-
- )}
-
-
+ {!props.disabled && (
+
+
+ {value?.icon}
+
+ {value && !props.isLoading && !props.required && (
+
+ {
+ e.preventDefault();
+ props.onChange(undefined);
+ }}
+ />
+
+ {t("clear_selection")}
+
+
+ )}
+
+ {props.isLoading ? (
+
+ ) : (
+
+ )}
+
+
+ )}
diff --git a/src/Components/Form/FormFields/AutocompleteMultiselect.tsx b/src/Components/Form/FormFields/AutocompleteMultiselect.tsx
index 35a125ae07d..9004dea939d 100644
--- a/src/Components/Form/FormFields/AutocompleteMultiselect.tsx
+++ b/src/Components/Form/FormFields/AutocompleteMultiselect.tsx
@@ -119,15 +119,17 @@ export const AutocompleteMutliSelect = (
onChange={(event) => setQuery(event.target.value.toLowerCase())}
autoComplete="off"
/>
-
-
- {props.isLoading ? (
-
- ) : (
-
- )}
-
-
+ {!props.disabled && (
+
+
+ {props.isLoading ? (
+
+ ) : (
+
+ )}
+
+
+ )}
{value.length !== 0 && (
diff --git a/src/Components/Form/FormFields/NumericWithUnitsFormField.tsx b/src/Components/Form/FormFields/NumericWithUnitsFormField.tsx
index 31ac781e018..02aa03fdf71 100644
--- a/src/Components/Form/FormFields/NumericWithUnitsFormField.tsx
+++ b/src/Components/Form/FormFields/NumericWithUnitsFormField.tsx
@@ -34,6 +34,7 @@ export default function NumericWithUnitsFormField(props: Props) {
max={props.max}
autoComplete={props.autoComplete}
required={field.required}
+ value={numValue}
onChange={(e) => field.handleChange(e.target.value + " " + unitValue)}
/>
diff --git a/src/Components/Form/FormFields/PhoneNumberFormField.tsx b/src/Components/Form/FormFields/PhoneNumberFormField.tsx
index 64c93c51500..b6af2bfaa24 100644
--- a/src/Components/Form/FormFields/PhoneNumberFormField.tsx
+++ b/src/Components/Form/FormFields/PhoneNumberFormField.tsx
@@ -1,12 +1,13 @@
import { FormFieldBaseProps, useFormFieldPropsResolver } from "./Utils";
import FormField from "./FormField";
+import { useEffect, useMemo, useState } from "react";
import {
- AsYouType,
- isValidPhoneNumber,
+ classNames,
parsePhoneNumber,
-} from "libphonenumber-js";
-import { useMemo, useState } from "react";
-import { classNames } from "../../../Utils/utils";
+ formatPhoneNumber as formatPhoneNumberUtil,
+ getCountryCode,
+ CountryData,
+} from "../../../Utils/utils";
import phoneCodesJson from "../../../Common/static/countryPhoneAndFlags.json";
import {
FieldError,
@@ -15,12 +16,6 @@ import {
} from "../FieldValidators";
import CareIcon from "../../../CAREUI/icons/CareIcon";
-interface CountryData {
- flag: string;
- name: string;
- code: string;
-}
-
const phoneCodes: Record = phoneCodesJson;
interface Props extends FormFieldBaseProps {
@@ -39,21 +34,6 @@ export default function PhoneNumberFormField(props: Props) {
[props.types]
);
- const asYouType = useMemo(() => {
- const asYouType = new AsYouType();
-
- asYouType.reset();
-
- if (field.value) {
- asYouType.input(field.value);
- } else {
- asYouType.input("+91");
- field.handleChange(asYouType.getNumberValue());
- }
-
- return asYouType;
- }, []);
-
const validate = useMemo(
() => (value: string | undefined, event: "blur" | "change") => {
if (!value || props.disableValidation) {
@@ -73,9 +53,9 @@ export default function PhoneNumberFormField(props: Props) {
const setValue = (value: string) => {
value = value.replaceAll(/[^0-9+]/g, "");
-
- asYouType.reset();
- asYouType.input(value);
+ if (value.length > 12 && value.startsWith("+910")) {
+ value = "+91" + value.slice(4);
+ }
const error = validate(value, "change");
field.handleChange(value);
@@ -83,6 +63,8 @@ export default function PhoneNumberFormField(props: Props) {
setError(error);
};
+ useEffect(() => setValue(field.value || "+91"), []);
+
return (
setValue(e.target.value)}
disabled={field.disabled}
onBlur={() => setError(validate(field.value, "blur"))}
@@ -122,7 +104,7 @@ export default function PhoneNumberFormField(props: Props) {
autoComplete="country"
className="cui-input-base h-full border-0 bg-transparent pl-2 pr-8 text-end font-medium tracking-wider text-gray-700 focus:ring-2 focus:ring-inset"
value={
- asYouType.getCountry() ??
+ getCountryCode(field.value) ??
(field.value?.startsWith("1800") ? "1800" : "Other")
}
onChange={(e) => {
@@ -156,7 +138,7 @@ const phoneNumberTypeIcons: Record = {
const PhoneNumberTypesHelp = ({ types }: { types: PhoneNumberType[] }) => (
{types.map((type) => (
-
+
{
return code.startsWith("+") ? code : "+" + code;
};
-const formatPhoneNumber = (value: string) => {
+const formatPhoneNumber = (value: string, types: PhoneNumberType[]) => {
if (value === undefined || value === null) {
return "+91 ";
}
- if (!isValidPhoneNumber(value)) {
+ if (PhoneNumberValidator(types)(value) !== undefined || value.length < 13) {
return value;
}
const phoneNumber = parsePhoneNumber(value);
- return phoneNumber.formatInternational();
+ return phoneNumber ? formatPhoneNumberUtil(phoneNumber) : value;
};
diff --git a/src/Components/Form/SelectMenuV2.tsx b/src/Components/Form/SelectMenuV2.tsx
index f26ce03f0c6..464a754cebe 100644
--- a/src/Components/Form/SelectMenuV2.tsx
+++ b/src/Components/Form/SelectMenuV2.tsx
@@ -9,7 +9,7 @@ type OptionCallback = (option: T) => R;
type SelectMenuProps = {
id?: string;
- options: T[];
+ options: readonly T[];
disabled?: boolean | undefined;
value: V | undefined;
placeholder?: ReactNode;
diff --git a/src/Components/Form/Utils.ts b/src/Components/Form/Utils.ts
index 2ec5d4b60e5..0592e81a06c 100644
--- a/src/Components/Form/Utils.ts
+++ b/src/Components/Form/Utils.ts
@@ -1,7 +1,9 @@
import { FieldError } from "./FieldValidators";
export type FormDetails = { [key: string]: any };
-export type FormErrors = Partial>;
+export type FormErrors = Partial<
+ Record
+>;
export type FormState = { form: T; errors: FormErrors };
export type FormAction =
| { type: "set_form"; form: T }
diff --git a/src/Components/Medicine/AdministerMedicine.tsx b/src/Components/Medicine/AdministerMedicine.tsx
index 5141d8f26bd..8798b476b06 100644
--- a/src/Components/Medicine/AdministerMedicine.tsx
+++ b/src/Components/Medicine/AdministerMedicine.tsx
@@ -1,10 +1,8 @@
import { useState } from "react";
-import { PrescriptionActions } from "../../Redux/actions";
import ConfirmDialog from "../Common/ConfirmDialog";
import { Prescription } from "./models";
import TextAreaFormField from "../Form/FormFields/TextAreaFormField";
import { Success } from "../../Utils/Notifications";
-import { useDispatch } from "react-redux";
import PrescriptionDetailCard from "./PrescriptionDetailCard";
import CareIcon from "../../CAREUI/icons/CareIcon";
import { formatDateTime } from "../../Utils/utils";
@@ -12,16 +10,18 @@ import { useTranslation } from "react-i18next";
import CheckBoxFormField from "../Form/FormFields/CheckBoxFormField";
import TextFormField from "../Form/FormFields/TextFormField";
import dayjs from "../../Utils/dayjs";
+import useSlug from "../../Common/hooks/useSlug";
+import request from "../../Utils/request/request";
+import MedicineRoutes from "./routes";
interface Props {
prescription: Prescription;
- actions: ReturnType["prescription"]>;
onClose: (success: boolean) => void;
}
export default function AdministerMedicine({ prescription, ...props }: Props) {
const { t } = useTranslation();
- const dispatch = useDispatch();
+ const consultation = useSlug("consultation");
const [isLoading, setIsLoading] = useState(false);
const [notes, setNotes] = useState("");
const [isCustomTime, setIsCustomTime] = useState(false);
@@ -52,13 +52,14 @@ export default function AdministerMedicine({ prescription, ...props }: Props) {
onClose={() => props.onClose(false)}
onConfirm={async () => {
setIsLoading(true);
- const res = await dispatch(
- props.actions.administer({
+ const { res } = await request(MedicineRoutes.administerPrescription, {
+ pathParams: { consultation, external_id: prescription.id },
+ body: {
notes,
administered_date: isCustomTime ? customTime : undefined,
- })
- );
- if (res.status === 201) {
+ },
+ });
+ if (res?.ok) {
Success({ msg: t("medicines_administered") });
}
setIsLoading(false);
@@ -67,11 +68,7 @@ export default function AdministerMedicine({ prescription, ...props }: Props) {
className="w-full md:max-w-4xl"
>
-
+
["create"];
onDone: () => void;
}) {
- const dispatch = useDispatch();
- const [isCreating, setIsCreating] = useState(false);
const { t } = useTranslation();
+ const consultation = useSlug("consultation");
+ const [isCreating, setIsCreating] = useState(false);
return (
diff --git a/src/Components/Medicine/MedibaseAutocompleteFormField.tsx b/src/Components/Medicine/MedibaseAutocompleteFormField.tsx
index 337546691f9..2b5612f6f32 100644
--- a/src/Components/Medicine/MedibaseAutocompleteFormField.tsx
+++ b/src/Components/Medicine/MedibaseAutocompleteFormField.tsx
@@ -49,7 +49,7 @@ export default function MedibaseAutocompleteFormField(
value={field.value}
required
onChange={field.handleChange}
- options={options(field.value && [field.value])}
+ options={options(field.value && !query && [field.value])}
optionLabel={(option) => option.name.toUpperCase()}
optionDescription={(option) => }
optionValue={(option) => option}
diff --git a/src/Components/Medicine/MedicineAdministration.tsx b/src/Components/Medicine/MedicineAdministration.tsx
index 5d8347ba5a5..d899a3800fb 100644
--- a/src/Components/Medicine/MedicineAdministration.tsx
+++ b/src/Components/Medicine/MedicineAdministration.tsx
@@ -1,27 +1,27 @@
import { useEffect, useMemo, useState } from "react";
-import { PrescriptionActions } from "../../Redux/actions";
import PrescriptionDetailCard from "./PrescriptionDetailCard";
import { MedicineAdministrationRecord, Prescription } from "./models";
import TextAreaFormField from "../Form/FormFields/TextAreaFormField";
import CheckBoxFormField from "../Form/FormFields/CheckBoxFormField";
import ButtonV2 from "../Common/components/ButtonV2";
import CareIcon from "../../CAREUI/icons/CareIcon";
-import { useDispatch } from "react-redux";
import { Error, Success } from "../../Utils/Notifications";
-import { formatDateTime } from "../../Utils/utils";
+import { classNames, formatDateTime } from "../../Utils/utils";
import { useTranslation } from "react-i18next";
import dayjs from "../../Utils/dayjs";
import TextFormField from "../Form/FormFields/TextFormField";
+import request from "../../Utils/request/request";
+import MedicineRoutes from "./routes";
+import useSlug from "../../Common/hooks/useSlug";
interface Props {
prescriptions: Prescription[];
- action: ReturnType ["prescription"];
onDone: () => void;
}
export default function MedicineAdministration(props: Props) {
const { t } = useTranslation();
- const dispatch = useDispatch();
+ const consultation = useSlug("consultation");
const [shouldAdminister, setShouldAdminister] = useState([]);
const [notes, setNotes] = useState(
[]
@@ -46,34 +46,35 @@ export default function MedicineAdministration(props: Props) {
);
}, [props.prescriptions]);
- const handleSubmit = () => {
- const records: MedicineAdministrationRecord[] = [];
- prescriptions.forEach((prescription, i) => {
- if (shouldAdminister[i]) {
- records.push({
- prescription,
- notes: notes[i],
- administered_date: isCustomTime[i] ? customTime[i] : undefined,
- });
- }
- });
+ const handleSubmit = async () => {
+ const administrations = prescriptions
+ .map((prescription, i) => ({
+ prescription,
+ notes: notes[i],
+ administered_date: isCustomTime[i] ? customTime[i] : undefined,
+ }))
+ .filter((_, i) => shouldAdminister[i]);
- Promise.all(
- records.map(async ({ prescription, ...record }) => {
- const res = await dispatch(
- props.action(prescription?.id ?? "").administer(record)
- );
- if (res.status !== 201) {
- Error({ msg: t("medicines_administered_error") });
- }
- })
- ).then(() => {
- Success({ msg: t("medicines_administered") });
- props.onDone();
- });
+ const ok = await Promise.all(
+ administrations.map(({ prescription, ...body }) =>
+ request(MedicineRoutes.administerPrescription, {
+ pathParams: { consultation, external_id: prescription.id },
+ body,
+ }).then(({ res }) => !!res?.ok)
+ )
+ );
+
+ if (!ok) {
+ Error({ msg: t("medicines_administered_error") });
+ return;
+ }
+
+ Success({ msg: t("medicines_administered") });
+ props.onDone();
};
const selectedCount = shouldAdminister.filter(Boolean).length;
+ const is_prn = prescriptions.some((obj) => obj.is_prn);
return (
@@ -82,10 +83,14 @@ export default function MedicineAdministration(props: Props) {
key={obj.id}
prescription={obj}
readonly
- actions={props.action(obj?.id ?? "")}
selected={shouldAdminister[index]}
>
-
+
-
+
void;
+}
+
+export default function AdministrationEventCell({
+ administrations,
+ interval: { start, end },
+ prescription,
+ refetch,
+}: Props) {
+ const [showTimeline, setShowTimeline] = useState(false);
+ // Check if cell belongs to an administered prescription
+ const administered = administrations
+ .filter((administration) =>
+ dayjs(administration.administered_date).isBetween(start, end)
+ )
+ .sort(
+ (a, b) =>
+ new Date(a.administered_date!).getTime() -
+ new Date(b.administered_date!).getTime()
+ );
+
+ const hasComment = administered.some((obj) => !!obj.notes);
+
+ if (administered.length) {
+ return (
+ <>
+ setShowTimeline(false)}
+ title={
+
+ }
+ className="w-full md:max-w-4xl"
+ show={showTimeline}
+ >
+
+ Administrations between{" "}
+ {formatTime(start, "HH:mm")} and{" "}
+ {formatTime(end, "HH:mm")} on{" "}
+
+ {formatDateTime(start, "DD/MM/YYYY")}
+
+
+
+
+
+ >
+ );
+ }
+
+ // Check if cell belongs to after prescription.created_date
+ if (dayjs(start).isAfter(prescription.created_date)) {
+ return ;
+ }
+
+ // Check if cell belongs to a discontinued prescription
+ if (
+ prescription.discontinued &&
+ dayjs(end).isAfter(prescription.discontinued_date)
+ ) {
+ if (!dayjs(prescription.discontinued_date).isBetween(start, end)) return;
+
+ return (
+
+
+
+
+ Discontinued on{" "}
+ {formatDateTime(prescription.discontinued_date)}
+
+
+ Reason:{" "}
+ {prescription.discontinued_reason ? (
+ {prescription.discontinued_reason}
+ ) : (
+ Not specified
+ )}
+
+
+
+ );
+ }
+}
diff --git a/src/Components/Medicine/MedicineAdministrationSheet/AdministrationEventSeperator.tsx b/src/Components/Medicine/MedicineAdministrationSheet/AdministrationEventSeperator.tsx
new file mode 100644
index 00000000000..a83fa38bd9c
--- /dev/null
+++ b/src/Components/Medicine/MedicineAdministrationSheet/AdministrationEventSeperator.tsx
@@ -0,0 +1,22 @@
+import { formatDateTime } from "../../../Utils/utils";
+
+export default function AdministrationEventSeperator({ date }: { date: Date }) {
+ // Show date if it's 00:00
+ if (date.getHours() === 0) {
+ return (
+
+
+ {formatDateTime(date, "DD/MM")}
+
+
+ );
+ }
+
+ return (
+
+
+ {/* {formatDateTime(date, "HH")} */}
+
+
+ );
+}
diff --git a/src/Components/Medicine/MedicineAdministrationSheet/AdministrationTable.tsx b/src/Components/Medicine/MedicineAdministrationSheet/AdministrationTable.tsx
new file mode 100644
index 00000000000..9de207146de
--- /dev/null
+++ b/src/Components/Medicine/MedicineAdministrationSheet/AdministrationTable.tsx
@@ -0,0 +1,106 @@
+import { useTranslation } from "react-i18next";
+import CareIcon from "../../../CAREUI/icons/CareIcon";
+import useRangePagination from "../../../Common/hooks/useRangePagination";
+import { classNames, formatDateTime } from "../../../Utils/utils";
+import ButtonV2 from "../../Common/components/ButtonV2";
+import { Prescription } from "../models";
+import MedicineAdministrationTableRow from "./AdministrationTableRow";
+
+interface Props {
+ prescriptions: Prescription[];
+ pagination: ReturnType;
+ onRefetch: () => void;
+}
+
+export default function MedicineAdministrationTable({
+ pagination,
+ prescriptions,
+ onRefetch,
+}: Props) {
+ const { t } = useTranslation();
+
+ return (
+
+
+
+
+
+
+ {t("medicine")}
+
+ Dosage &
+ {!prescriptions[0]?.is_prn ? "Frequency" : "Indicator"}
+
+
+ |
+
+
+
+
+
+ |
+ {pagination.slots?.map(({ start }, index) => (
+ <>
+
+ {formatDateTime(
+ start,
+ start.getHours() === 0 ? "DD/MM" : "h a"
+ )}
+ |
+ |
+ >
+ ))}
+
+
+
+
+ |
+
+ |
+
+
+
+
+ {prescriptions.map((obj) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/src/Components/Medicine/MedicineAdministrationSheet/AdministrationTableRow.tsx b/src/Components/Medicine/MedicineAdministrationSheet/AdministrationTableRow.tsx
new file mode 100644
index 00000000000..40675640411
--- /dev/null
+++ b/src/Components/Medicine/MedicineAdministrationSheet/AdministrationTableRow.tsx
@@ -0,0 +1,249 @@
+import { useTranslation } from "react-i18next";
+import { Prescription } from "../models";
+import { useState } from "react";
+import useQuery from "../../../Utils/request/useQuery";
+import MedicineRoutes from "../routes";
+import { classNames, formatDateTime } from "../../../Utils/utils";
+import useSlug from "../../../Common/hooks/useSlug";
+import DiscontinuePrescription from "../DiscontinuePrescription";
+import AdministerMedicine from "../AdministerMedicine";
+import DialogModal from "../../Common/Dialog";
+import PrescriptionDetailCard from "../PrescriptionDetailCard";
+import ButtonV2, { Cancel, Submit } from "../../Common/components/ButtonV2";
+import CareIcon from "../../../CAREUI/icons/CareIcon";
+import EditPrescriptionForm from "../EditPrescriptionForm";
+import AdministrationEventSeperator from "./AdministrationEventSeperator";
+import AdministrationEventCell from "./AdministrationEventCell";
+
+interface Props {
+ prescription: Prescription;
+ intervals: { start: Date; end: Date }[];
+ refetch: () => void;
+}
+
+export default function MedicineAdministrationTableRow({
+ prescription,
+ ...props
+}: Props) {
+ const { t } = useTranslation();
+ const consultation = useSlug("consultation");
+ // const [showActions, setShowActions] = useState(false);
+ const [showDetails, setShowDetails] = useState(false);
+ const [showEdit, setShowEdit] = useState(false);
+ const [showAdminister, setShowAdminister] = useState(false);
+ const [showDiscontinue, setShowDiscontinue] = useState(false);
+
+ const { data, loading, refetch } = useQuery(
+ MedicineRoutes.listAdministrations,
+ {
+ pathParams: { consultation },
+ query: {
+ prescription: prescription.id,
+ administered_date_after: formatDateTime(
+ props.intervals[0].start,
+ "YYYY-MM-DD"
+ ),
+ administered_date_before: formatDateTime(
+ props.intervals[props.intervals.length - 1].end,
+ "YYYY-MM-DD"
+ ),
+ archived: false,
+ },
+ key: `${prescription.last_administered_on}`,
+ }
+ );
+
+ return (
+
+ {showDiscontinue && (
+ {
+ setShowDiscontinue(false);
+ if (success) {
+ props.refetch();
+ }
+ }}
+ />
+ )}
+ {showAdminister && (
+ {
+ setShowAdminister(false);
+ if (success) {
+ props.refetch();
+ }
+ }}
+ />
+ )}
+ {showDetails && (
+ setShowDetails(false)}
+ className="w-full md:max-w-4xl"
+ show
+ >
+
+
+
+ setShowDetails(false)}
+ label={t("close")}
+ />
+ setShowDiscontinue(true)}
+ >
+
+ {t("discontinue")}
+
+ {
+ setShowDetails(false);
+ setShowEdit(true);
+ }}
+ >
+
+ {t("edit")}
+
+ setShowAdminister(true)}
+ >
+
+ {t("administer")}
+
+
+
+
+ )}
+ {showEdit && (
+ setShowEdit(false)}
+ show={showEdit}
+ title={`${t("edit")} ${t(
+ prescription.is_prn ? "prn_prescription" : "prescription_medication"
+ )}: ${
+ prescription.medicine_object?.name ?? prescription.medicine_old
+ }`}
+ description={
+
+
+ {t("edit_caution_note")}
+
+ }
+ className="w-full max-w-3xl lg:min-w-[600px]"
+ >
+ {
+ setShowEdit(false);
+ if (success) {
+ props.refetch();
+ }
+ }}
+ />
+
+ )}
+ setShowDetails(true)}
+ >
+
+
+
+ {prescription.medicine_object?.name ?? prescription.medicine_old}
+
+
+ {prescription.discontinued && (
+
+ {t("discontinued")}
+
+ )}
+
+ {prescription.route && (
+
+ {t(prescription.route)}
+
+ )}
+
+
+
+ {prescription.dosage}
+
+ {!prescription.is_prn
+ ? t("PRESCRIPTION_FREQUENCY_" + prescription.frequency)
+ : prescription.indicator}
+
+
+
+ |
+
+ |
+
+ {/* Administration Cells */}
+ {props.intervals.map(({ start, end }, index) => (
+ <>
+
+
+ |
+
+
+ {!data?.results ? (
+
+ ) : (
+
+ )}
+ |
+ >
+ ))}
+ |
+
+ {/* Action Buttons */}
+
+ setShowAdminister(true)}
+ >
+ {t("administer")}
+
+ |
+
+ );
+}
diff --git a/src/Components/Medicine/MedicineAdministrationSheet/BulkAdminister.tsx b/src/Components/Medicine/MedicineAdministrationSheet/BulkAdminister.tsx
new file mode 100644
index 00000000000..abd609871c8
--- /dev/null
+++ b/src/Components/Medicine/MedicineAdministrationSheet/BulkAdminister.tsx
@@ -0,0 +1,47 @@
+import { useTranslation } from "react-i18next";
+import { Prescription } from "../models";
+import { useState } from "react";
+import ButtonV2 from "../../Common/components/ButtonV2";
+import CareIcon from "../../../CAREUI/icons/CareIcon";
+import SlideOver from "../../../CAREUI/interactive/SlideOver";
+import MedicineAdministration from "../MedicineAdministration";
+
+interface Props {
+ prescriptions: Prescription[];
+ onDone: () => void;
+}
+
+export default function BulkAdminister({ prescriptions, onDone }: Props) {
+ const { t } = useTranslation();
+ const [showBulkAdminister, setShowBulkAdminister] = useState(false);
+
+ return (
+ <>
+ setShowBulkAdminister(true)}
+ className="w-full"
+ disabled={prescriptions.length === 0}
+ >
+
+ {t("administer_medicines")}
+ {t("administer")}
+
+
+ {
+ setShowBulkAdminister(false);
+ onDone();
+ }}
+ />
+
+ >
+ );
+}
diff --git a/src/Components/Medicine/MedicineAdministrationSheet/index.tsx b/src/Components/Medicine/MedicineAdministrationSheet/index.tsx
new file mode 100644
index 00000000000..187d5447314
--- /dev/null
+++ b/src/Components/Medicine/MedicineAdministrationSheet/index.tsx
@@ -0,0 +1,158 @@
+import { useTranslation } from "react-i18next";
+import useSlug from "../../../Common/hooks/useSlug";
+import useQuery from "../../../Utils/request/useQuery";
+import MedicineRoutes from "../routes";
+import { useMemo, useState } from "react";
+import { computeActivityBounds } from "./utils";
+import useBreakpoints from "../../../Common/hooks/useBreakpoints";
+import SubHeading from "../../../CAREUI/display/SubHeading";
+import ButtonV2 from "../../Common/components/ButtonV2";
+import CareIcon from "../../../CAREUI/icons/CareIcon";
+import BulkAdminister from "./BulkAdminister";
+import useRangePagination from "../../../Common/hooks/useRangePagination";
+import MedicineAdministrationTable from "./AdministrationTable";
+import Loading from "../../Common/Loading";
+import ScrollOverlay from "../../../CAREUI/interactive/ScrollOverlay";
+
+interface Props {
+ readonly?: boolean;
+ is_prn: boolean;
+}
+
+const DEFAULT_BOUNDS = { start: new Date(), end: new Date() };
+
+const MedicineAdministrationSheet = ({ readonly, is_prn }: Props) => {
+ const { t } = useTranslation();
+ const consultation = useSlug("consultation");
+
+ const [showDiscontinued, setShowDiscontinued] = useState(false);
+
+ const filters = { is_prn, prescription_type: "REGULAR", limit: 100 };
+
+ const { data, loading, refetch } = useQuery(
+ MedicineRoutes.listPrescriptions,
+ {
+ pathParams: { consultation },
+ query: { ...filters, discontinued: showDiscontinued ? undefined : false },
+ }
+ );
+
+ const discontinuedPrescriptions = useQuery(MedicineRoutes.listPrescriptions, {
+ pathParams: { consultation },
+ query: { ...filters, discontinued: true },
+ prefetch: !showDiscontinued,
+ });
+
+ const discontinuedCount = discontinuedPrescriptions.data?.count;
+
+ const { activityTimelineBounds, prescriptions } = useMemo(
+ () => ({
+ prescriptions: data?.results?.sort(
+ (a, b) => +a.discontinued - +b.discontinued
+ ),
+ activityTimelineBounds: data
+ ? computeActivityBounds(data.results)
+ : undefined,
+ }),
+ [data]
+ );
+
+ const daysPerPage = useBreakpoints({ default: 1, "2xl": 2 });
+ const pagination = useRangePagination({
+ bounds: activityTimelineBounds ?? DEFAULT_BOUNDS,
+ perPage: daysPerPage * 24 * 60 * 60 * 1000,
+ slots: (daysPerPage * 24) / 4, // Grouped by 4 hours
+ defaultEnd: true,
+ });
+
+ return (
+
+
+
+
+
+ {t("edit_prescriptions")}
+
+ {t("edit")}
+
+ refetch()}
+ />
+ >
+ )
+ }
+ />
+
+
+ Scroll to view more prescriptions
+
+
+ }
+ disableOverlay={loading || !prescriptions?.length}
+ >
+ {loading && }
+ {prescriptions?.length === 0 && }
+
+ {!!prescriptions?.length && (
+ {
+ refetch();
+ discontinuedPrescriptions.refetch();
+ }}
+ />
+ )}
+
+ {!showDiscontinued && !!discontinuedCount && (
+ setShowDiscontinued(true)}
+ >
+
+
+
+ Show {discontinuedCount} discontinued
+ prescription(s)
+
+
+
+ )}
+
+
+ );
+};
+
+export default MedicineAdministrationSheet;
+
+const NoPrescriptions = ({ prn }: { prn: boolean }) => {
+ return (
+
+
+
+ {prn
+ ? "No PRN Prescriptions Prescribed"
+ : "No Prescriptions Prescribed"}
+
+
+ );
+};
diff --git a/src/Components/Medicine/MedicineAdministrationSheet/utils.ts b/src/Components/Medicine/MedicineAdministrationSheet/utils.ts
new file mode 100644
index 00000000000..93ee5fb4b08
--- /dev/null
+++ b/src/Components/Medicine/MedicineAdministrationSheet/utils.ts
@@ -0,0 +1,34 @@
+import { Prescription } from "../models";
+
+export function computeActivityBounds(prescriptions: Prescription[]) {
+ // get start by finding earliest of all presciption's created_date
+ const start = new Date(
+ prescriptions.reduce(
+ (earliest, curr) =>
+ earliest < curr.created_date ? earliest : curr.created_date,
+ prescriptions[0]?.created_date ?? new Date()
+ )
+ );
+
+ // get end by finding latest of all presciption's last_administered_on
+ const end = new Date(
+ prescriptions
+ .filter((prescription) => prescription.last_administered_on)
+ .reduce(
+ (latest, curr) =>
+ curr.last_administered_on && curr.last_administered_on > latest
+ ? curr.last_administered_on
+ : latest,
+ prescriptions[0]?.created_date ?? new Date()
+ )
+ );
+
+ // floor start to 00:00 of the day
+ start.setHours(0, 0, 0, 0);
+
+ // ceil end to 00:00 of the next day
+ end.setDate(end.getDate() + 1);
+ end.setHours(0, 0, 0, 0);
+
+ return { start, end };
+}
diff --git a/src/Components/Medicine/MedicineAdministrationsTable.tsx b/src/Components/Medicine/MedicineAdministrationsTable.tsx
deleted file mode 100644
index 4ffc25e43f0..00000000000
--- a/src/Components/Medicine/MedicineAdministrationsTable.tsx
+++ /dev/null
@@ -1,88 +0,0 @@
-import { useCallback, useEffect, useMemo, useState } from "react";
-import ResponsiveMedicineTable from "../Common/components/ResponsiveMedicineTables";
-import { formatDateTime } from "../../Utils/utils";
-import { PrescriptionActions } from "../../Redux/actions";
-import { useDispatch } from "react-redux";
-import { MedicineAdministrationRecord } from "./models";
-import CareIcon from "../../CAREUI/icons/CareIcon";
-import RecordMeta from "../../CAREUI/display/RecordMeta";
-import { useTranslation } from "react-i18next";
-
-interface Props {
- consultation_id: string;
-}
-
-export default function MedicineAdministrationsTable({
- consultation_id,
-}: Props) {
- const { t } = useTranslation();
- const dispatch = useDispatch ();
- const [items, setItems] = useState();
-
- const { listAdministrations } = useMemo(
- () => PrescriptionActions(consultation_id),
- [consultation_id]
- );
-
- const fetchItems = useCallback(() => {
- dispatch(listAdministrations()).then((res: any) =>
- setItems(res.data.results)
- );
- }, [consultation_id]);
-
- useEffect(() => {
- fetchItems();
- }, [consultation_id]);
-
- const lastModified = items?.[0]?.modified_date;
-
- return (
-
-
-
-
- {t("medicine_administration_history")}
-
-
-
-
- {lastModified && formatDateTime(lastModified)}
-
-
-
-
-
-
-
- t(_))}
- list={
- items?.map((obj) => ({
- ...obj,
- medicine:
- obj.prescription?.medicine_object?.name ??
- obj.prescription?.medicine_old,
- created_date__pretty: (
-
- by{" "}
- {obj.administered_by?.first_name}{" "}
- {obj.administered_by?.last_name}
-
- ),
- ...obj,
- })) || []
- }
- objectKeys={["medicine", "notes", "created_date__pretty"]}
- fieldsToDisplay={[2, 3]}
- />
- {items?.length === 0 && (
-
- {t("no_data_found")}
-
- )}
-
-
-
-
- );
-}
diff --git a/src/Components/Medicine/PrescriptionAdministrationsTable.tsx b/src/Components/Medicine/PrescriptionAdministrationsTable.tsx
deleted file mode 100644
index 81282126d7c..00000000000
--- a/src/Components/Medicine/PrescriptionAdministrationsTable.tsx
+++ /dev/null
@@ -1,566 +0,0 @@
-import { useCallback, useEffect, useMemo, useState } from "react";
-import { PrescriptionActions } from "../../Redux/actions";
-import { useDispatch } from "react-redux";
-import { MedicineAdministrationRecord, Prescription } from "./models";
-import CareIcon from "../../CAREUI/icons/CareIcon";
-import ButtonV2, { Cancel, Submit } from "../Common/components/ButtonV2";
-import SlideOver from "../../CAREUI/interactive/SlideOver";
-import MedicineAdministration from "./MedicineAdministration";
-import DiscontinuePrescription from "./DiscontinuePrescription";
-import AdministerMedicine from "./AdministerMedicine";
-import DialogModal from "../Common/Dialog";
-import PrescriptionDetailCard from "./PrescriptionDetailCard";
-import { useTranslation } from "react-i18next";
-import SubHeading from "../../CAREUI/display/SubHeading";
-import dayjs from "../../Utils/dayjs";
-import {
- classNames,
- formatDate,
- formatDateTime,
- formatTime,
-} from "../../Utils/utils";
-import useRangePagination from "../../Common/hooks/useRangePagination";
-
-interface DateRange {
- start: Date;
- end: Date;
-}
-
-interface Props {
- prn: boolean;
- prescription_type?: Prescription["prescription_type"];
- consultation_id: string;
- readonly?: boolean;
-}
-
-interface State {
- prescriptions: Prescription[];
- administrationsTimeBounds: DateRange;
-}
-
-export default function PrescriptionAdministrationsTable({
- prn,
- consultation_id,
- readonly,
-}: Props) {
- const dispatch = useDispatch();
- const { t } = useTranslation();
-
- const [state, setState] = useState();
- const pagination = useRangePagination({
- bounds: state?.administrationsTimeBounds ?? {
- start: new Date(),
- end: new Date(),
- },
- perPage: 24 * 60 * 60 * 1000,
- slots: 24,
- defaultEnd: true,
- });
- const [showBulkAdminister, setShowBulkAdminister] = useState(false);
-
- const { list, prescription } = useMemo(
- () => PrescriptionActions(consultation_id),
- [consultation_id]
- );
-
- const refetch = useCallback(async () => {
- const res = await dispatch(
- list({ is_prn: prn, prescription_type: "REGULAR" })
- );
-
- setState({
- prescriptions: (res.data.results as Prescription[]).sort(
- (a, b) => (a.discontinued ? 1 : 0) - (b.discontinued ? 1 : 0)
- ),
- administrationsTimeBounds: getAdministrationBounds(res.data.results),
- });
- }, [consultation_id, dispatch]);
-
- useEffect(() => {
- refetch();
- }, [refetch]);
-
- return (
-
- {state?.prescriptions && (
-
- {
- setShowBulkAdminister(false);
- refetch();
- }}
- />
-
- )}
-
-
-
-
-
- {t("edit_prescriptions")}
-
- {t("edit")}
-
- setShowBulkAdminister(true)}
- className="w-full"
- disabled={
- state === undefined || state.prescriptions.length === 0
- }
- >
-
-
- {t("administer_medicines")}
-
- {t("administer")}
-
- >
- )
- }
- />
-
-
-
-
-
- {t("medicine")} |
-
-
- Dosage &
-
- {!state?.prescriptions[0]?.is_prn ? "Frequency" : "Indicator"}
-
- |
-
-
-
-
-
- |
- {state === undefined
- ? Array.from({ length: 24 }, (_, i) => i).map((i) => (
-
-
- |
- ))
- : pagination.slots?.map(({ start, end }, index) => (
-
- {formatDateTime(start, "DD/MM")}
- {formatDateTime(start, "HH:mm")}
-
-
- Administration(s) between
-
- {formatTime(start)} and{" "}
- {formatTime(end)}
-
- on {formatDate(start)}
-
- |
- ))}
-
-
-
-
- |
-
- |
-
-
-
-
- {state?.prescriptions?.map((item) => (
-
- ))}
-
-
-
- {state?.prescriptions.length === 0 && (
-
-
-
- {prn
- ? "No PRN Prescriptions Prescribed"
- : "No Prescriptions Prescribed"}
-
-
- )}
-
-
- );
-}
-
-interface PrescriptionRowProps {
- prescription: Prescription;
- intervals: DateRange[];
- actions: ReturnType["prescription"]>;
- refetch: () => void;
-}
-
-const PrescriptionRow = ({ prescription, ...props }: PrescriptionRowProps) => {
- const dispatch = useDispatch();
- const { t } = useTranslation();
- // const [showActions, setShowActions] = useState(false);
- const [showDetails, setShowDetails] = useState(false);
- const [showAdminister, setShowAdminister] = useState(false);
- const [showDiscontinue, setShowDiscontinue] = useState(false);
- const [administrations, setAdministrations] =
- useState();
-
- useEffect(() => {
- setAdministrations(undefined);
-
- const getAdministrations = async () => {
- const res = await dispatch(
- props.actions.listAdministrations({
- administered_date_after: formatDateTime(
- props.intervals[0].start,
- "YYYY-MM-DD"
- ),
- administered_date_before: formatDateTime(
- props.intervals[props.intervals.length - 1].end,
- "YYYY-MM-DD"
- ),
- })
- );
-
- setAdministrations(res.data.results);
- };
-
- getAdministrations();
- }, [prescription.id, dispatch, props.intervals]);
-
- return (
-
- {showDiscontinue && (
- {
- setShowDiscontinue(false);
- if (success) {
- props.refetch();
- }
- }}
- />
- )}
- {showAdminister && (
- {
- setShowAdminister(false);
- if (success) {
- props.refetch();
- }
- }}
- />
- )}
- {showDetails && (
- setShowDetails(false)}
- className="w-full md:max-w-4xl"
- show
- >
-
-
-
- setShowDetails(false)}
- label={t("close")}
- />
- setShowDiscontinue(true)}
- >
-
- {t("discontinue")}
-
- setShowAdminister(true)}
- >
-
- {t("administer")}
-
-
-
-
- )}
- setShowDetails(true)}
- >
-
-
- {prescription.medicine_object?.name ?? prescription.medicine_old}
-
-
- {prescription.discontinued && (
-
- {t("discontinued")}
-
- )}
-
- {prescription.route && (
-
- {t(prescription.route)}
-
- )}
-
- |
-
-
- {prescription.dosage}
-
- {!prescription.is_prn
- ? t("PRESCRIPTION_FREQUENCY_" + prescription.frequency)
- : prescription.indicator}
-
- |
-
- |
- {/* Administration Cells */}
- {props.intervals.map(({ start, end }, index) => (
-
- {administrations === undefined ? (
-
- ) : (
-
- )}
- |
- ))}
- |
-
- {/* Action Buttons */}
-
- setShowAdminister(true)}
- >
- {t("administer")}
-
- |
-
- );
-};
-
-interface AdministrationCellProps {
- administrations: MedicineAdministrationRecord[];
- interval: DateRange;
- prescription: Prescription;
-}
-
-const AdministrationCell = ({
- administrations,
- interval: { start, end },
- prescription,
-}: AdministrationCellProps) => {
- // Check if cell belongs to an administered prescription
- const administered = administrations.filter((administration) =>
- dayjs(administration.administered_date).isBetween(start, end)
- );
-
- if (administered.length) {
- return (
-
-
-
- {administered.length > 1 && (
-
- {administered.length}
-
- )}
-
-
-
- Administered on{" "}
- {formatDateTime(administered[0].administered_date)}
-
-
- {administered.length > 1
- ? `Administered ${administered.length} times`
- : `Administered ${formatTime(administered[0].administered_date)}`}
-
-
-
- );
- }
-
- // Check if cell belongs to a discontinued prescription
- if (
- prescription.discontinued &&
- dayjs(end).isAfter(prescription.discontinued_date)
- ) {
- if (!dayjs(prescription.discontinued_date).isBetween(start, end)) return;
-
- return (
-
-
-
-
- Discontinued on{" "}
- {formatDateTime(prescription.discontinued_date)}
-
-
- Reason:{" "}
- {prescription.discontinued_reason ? (
- {prescription.discontinued_reason}
- ) : (
- Not specified
- )}
-
-
-
- );
- }
-
- // Check if cell belongs to after prescription.created_date
- if (dayjs(start).isAfter(prescription.created_date)) {
- return ;
- }
-
- // Check if prescription.created_date is between start and end
- // if (dayjs(prescription.created_date).isBetween(start, end)) {
- // return (
- //
- //
- //
- //
- // Prescribed on{" "}
- // {formatDateTime(prescription.created_date)}
- //
- //
- //
- // );
- // }
-};
-
-function getAdministrationBounds(prescriptions: Prescription[]) {
- // get start by finding earliest of all presciption's created_date
- const start = new Date(
- prescriptions.reduce(
- (earliest, curr) =>
- earliest < curr.created_date ? earliest : curr.created_date,
- prescriptions[0]?.created_date ?? new Date()
- )
- );
-
- // get end by finding latest of all presciption's last_administered_on
- const end = new Date(
- prescriptions
- .filter((prescription) => prescription.last_administered_on)
- .reduce(
- (latest, curr) =>
- curr.last_administered_on && curr.last_administered_on > latest
- ? curr.last_administered_on
- : latest,
- prescriptions[0]?.created_date ?? new Date()
- )
- );
-
- // floor start to previous hour
- start.setMinutes(0, 0, 0);
-
- // ceil end to next hour
- end.setMinutes(0, 0, 0);
- end.setHours(end.getHours() + 1);
-
- return { start, end };
-}
diff --git a/src/Components/Medicine/PrescriptionBuilder.tsx b/src/Components/Medicine/PrescriptionBuilder.tsx
index f7a4dd49c93..39bf9b2f506 100644
--- a/src/Components/Medicine/PrescriptionBuilder.tsx
+++ b/src/Components/Medicine/PrescriptionBuilder.tsx
@@ -1,56 +1,49 @@
-import { useCallback, useEffect, useState } from "react";
+import { useState } from "react";
import CareIcon from "../../CAREUI/icons/CareIcon";
import ButtonV2 from "../Common/components/ButtonV2";
import { NormalPrescription, Prescription, PRNPrescription } from "./models";
import DialogModal from "../Common/Dialog";
import CreatePrescriptionForm from "./CreatePrescriptionForm";
import PrescriptionDetailCard from "./PrescriptionDetailCard";
-import { PrescriptionActions } from "../../Redux/actions";
-import { useDispatch } from "react-redux";
import DiscontinuePrescription from "./DiscontinuePrescription";
import AdministerMedicine from "./AdministerMedicine";
import { useTranslation } from "react-i18next";
+import useQuery from "../../Utils/request/useQuery";
+import MedicineRoutes from "./routes";
+import useSlug from "../../Common/hooks/useSlug";
interface Props {
prescription_type?: Prescription["prescription_type"];
- actions: ReturnType;
is_prn?: boolean;
disabled?: boolean;
}
export default function PrescriptionBuilder({
prescription_type,
- actions,
is_prn = false,
disabled,
}: Props) {
const { t } = useTranslation();
- const dispatch = useDispatch();
-
- const [prescriptions, setPrescriptions] = useState();
+ const consultation = useSlug("consultation");
const [showCreate, setShowCreate] = useState(false);
const [showDiscontinueFor, setShowDiscontinueFor] = useState();
const [showAdministerFor, setShowAdministerFor] = useState();
- const fetchPrescriptions = useCallback(() => {
- dispatch(actions.list({ is_prn, prescription_type })).then((res: any) =>
- setPrescriptions(res.data.results)
- );
- }, [dispatch, is_prn]);
-
- useEffect(() => {
- fetchPrescriptions();
- }, []);
+ const { data, refetch } = useQuery(MedicineRoutes.listPrescriptions, {
+ pathParams: { consultation },
+ query: { is_prn, prescription_type, limit: 100 },
+ });
return (
{showDiscontinueFor && (
{
setShowDiscontinueFor(undefined);
- if (success) fetchPrescriptions();
+ if (success) {
+ refetch();
+ }
}}
key={showDiscontinueFor.id}
/>
@@ -58,20 +51,20 @@ export default function PrescriptionBuilder({
{showAdministerFor && (
{
setShowAdministerFor(undefined);
- if (success) fetchPrescriptions();
+ if (success) {
+ refetch();
+ }
}}
key={showAdministerFor.id}
/>
)}
- {prescriptions?.map((obj, index) => (
+ {data?.results.map((obj, index) => (
setShowDiscontinueFor(obj)}
onAdministerClick={() => setShowAdministerFor(obj)}
readonly={disabled}
@@ -114,10 +107,9 @@ export default function PrescriptionBuilder({
prescription_type,
} as Prescription
}
- create={actions.create}
onDone={() => {
setShowCreate(false);
- fetchPrescriptions();
+ refetch();
}}
/>
diff --git a/src/Components/Medicine/PrescriptionDetailCard.tsx b/src/Components/Medicine/PrescriptionDetailCard.tsx
index 6da4fa7ae8d..4333eeb1a88 100644
--- a/src/Components/Medicine/PrescriptionDetailCard.tsx
+++ b/src/Components/Medicine/PrescriptionDetailCard.tsx
@@ -3,8 +3,8 @@ import CareIcon from "../../CAREUI/icons/CareIcon";
import { classNames } from "../../Utils/utils";
import ReadMore from "../Common/components/Readmore";
import ButtonV2 from "../Common/components/ButtonV2";
-import { PrescriptionActions } from "../../Redux/actions";
import { useTranslation } from "react-i18next";
+import RecordMeta from "../../CAREUI/display/RecordMeta";
export default function PrescriptionDetailCard({
prescription,
@@ -13,7 +13,6 @@ export default function PrescriptionDetailCard({
prescription: Prescription;
readonly?: boolean;
children?: React.ReactNode;
- actions: ReturnType["prescription"]>;
onDiscontinueClick?: () => void;
onAdministerClick?: () => void;
selected?: boolean;
@@ -29,7 +28,7 @@ export default function PrescriptionDetailCard({
prescription.discontinued && "bg-gray-200 opacity-80"
)}
>
-
+
@@ -83,7 +82,7 @@ export default function PrescriptionDetailCard({
-
+
{prescription.medicine_object?.name ?? prescription.medicine_old}
@@ -104,16 +103,17 @@ export default function PrescriptionDetailCard({
{prescription.indicator}
{prescription.max_dosage}
- {prescription.max_dosage}
+ {prescription.min_hours_between_doses &&
+ prescription.min_hours_between_doses + " hrs."}
>
) : (
@@ -146,6 +146,23 @@ export default function PrescriptionDetailCard({
)}
+
+
+
+ Prescribed
+
+
+ {prescription.discontinued && (
+
+ and was discontinued
+
+
+ )}
+
{props.children}
@@ -166,7 +183,9 @@ const Detail = (props: {
{props.children ? (
{props.children}
) : (
- {t("not_specified")}
+
+ {t("not_specified")}
+
)}
diff --git a/src/Components/Medicine/PrescriptionsTable.tsx b/src/Components/Medicine/PrescriptionsTable.tsx
index 903f01b32a7..a1b039e71dd 100644
--- a/src/Components/Medicine/PrescriptionsTable.tsx
+++ b/src/Components/Medicine/PrescriptionsTable.tsx
@@ -1,8 +1,6 @@
-import { useCallback, useEffect, useMemo, useState } from "react";
-import ResponsiveMedicineTable from "../Common/components/ResponsiveMedicineTables";
+import { useState } from "react";
+import ResponsiveMedicineTable from "./ResponsiveMedicineTables";
import { formatDateTime } from "../../Utils/utils";
-import { PrescriptionActions } from "../../Redux/actions";
-import { useDispatch } from "react-redux";
import { Prescription } from "./models";
import CareIcon from "../../CAREUI/icons/CareIcon";
import ButtonV2, { Cancel, Submit } from "../Common/components/ButtonV2";
@@ -14,11 +12,13 @@ import AdministerMedicine from "./AdministerMedicine";
import DialogModal from "../Common/Dialog";
import PrescriptionDetailCard from "./PrescriptionDetailCard";
import { useTranslation } from "react-i18next";
+import useSlug from "../../Common/hooks/useSlug";
+import useQuery from "../../Utils/request/useQuery";
+import MedicineRoutes from "./routes";
interface Props {
is_prn?: boolean;
prescription_type?: Prescription["prescription_type"];
- consultation_id: string;
onChange?: () => void;
readonly?: boolean;
}
@@ -26,35 +26,22 @@ interface Props {
export default function PrescriptionsTable({
is_prn = false,
prescription_type = "REGULAR",
- consultation_id,
onChange,
readonly,
}: Props) {
- const dispatch = useDispatch ();
+ const consultation = useSlug("consultation");
const { t } = useTranslation();
-
- const [prescriptions, setPrescriptions] = useState();
const [showBulkAdminister, setShowBulkAdminister] = useState(false);
const [showDiscontinueFor, setShowDiscontinueFor] = useState();
const [showAdministerFor, setShowAdministerFor] = useState();
const [detailedViewFor, setDetailedViewFor] = useState();
- const { list, prescription } = useMemo(
- () => PrescriptionActions(consultation_id),
- [consultation_id]
- );
-
- const fetchPrescriptions = useCallback(() => {
- dispatch(list({ is_prn, prescription_type })).then((res: any) =>
- setPrescriptions(res.data.results)
- );
- }, [consultation_id]);
-
- useEffect(() => {
- fetchPrescriptions();
- }, [consultation_id]);
+ const { data } = useQuery(MedicineRoutes.listPrescriptions, {
+ pathParams: { consultation },
+ query: { is_prn, prescription_type, limit: 100 },
+ });
- const lastModified = prescriptions?.[0]?.modified_date;
+ const lastModified = data?.results[0]?.modified_date;
const tkeys =
prescription_type === "REGULAR"
? is_prn
@@ -66,7 +53,7 @@ export default function PrescriptionsTable({
return (
- {prescriptions && (
+ {data?.results && (
{
setShowBulkAdminister(false);
onChange?.();
@@ -86,7 +72,6 @@ export default function PrescriptionsTable({
{showDiscontinueFor && (
{
setShowDiscontinueFor(undefined);
if (success) onChange?.();
@@ -97,7 +82,6 @@ export default function PrescriptionsTable({
{showAdministerFor && (
{
setShowAdministerFor(undefined);
if (success) onChange?.();
@@ -115,7 +99,6 @@ export default function PrescriptionsTable({
@@ -198,7 +181,7 @@ export default function PrescriptionsTable({
maxWidthColumn={0}
theads={Object.keys(tkeys).map((_) => t(_))}
list={
- prescriptions?.map((obj) => ({
+ data?.results.map((obj) => ({
...obj,
medicine: obj.medicine_object?.name ?? obj.medicine_old,
route__pretty:
@@ -277,7 +260,7 @@ export default function PrescriptionsTable({
: undefined
}
/>
- {prescriptions?.length === 0 && (
+ {data?.results.length === 0 && (
{t("no_data_found")}
diff --git a/src/Components/Medicine/PrescrpitionTimeline.tsx b/src/Components/Medicine/PrescrpitionTimeline.tsx
new file mode 100644
index 00000000000..425bdecd894
--- /dev/null
+++ b/src/Components/Medicine/PrescrpitionTimeline.tsx
@@ -0,0 +1,232 @@
+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;
+ onRefetch?: () => void;
+}
+
+export default function PrescrpitionTimeline({
+ prescription,
+ interval,
+ onRefetch,
+}: 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 (
+ {
+ onRefetch?.();
+ refetch();
+ }}
+ isLastNode={index === events.length - 1}
+ hideArchive={prescription.discontinued}
+ />
+ );
+ }
+ })}
+
+ );
+}
+
+const MedicineAdministeredNode = ({
+ event,
+ onArchived,
+ isLastNode,
+ hideArchive,
+}: {
+ event: MedicineAdministeredEvent;
+ onArchived: () => void;
+ isLastNode: boolean;
+ hideArchive?: boolean;
+}) => {
+ const consultation = useSlug("consultation");
+ const [showArchiveConfirmation, setShowArchiveConfirmation] = useState(false);
+ const [isArchiving, setIsArchiving] = useState(false);
+
+ return (
+ <>
+ setShowArchiveConfirmation(true)}
+ >
+ Archive
+
+ )
+ }
+ isLast={isLastNode}
+ >
+ {event.cancelled && (
+
+
+ Prescription was archived{" "}
+
+
+
+ )}
+
+ {
+ 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)}
+ />
+ >
+ );
+};
+
+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/Common/components/ResponsiveMedicineTables.tsx b/src/Components/Medicine/ResponsiveMedicineTables.tsx
similarity index 97%
rename from src/Components/Common/components/ResponsiveMedicineTables.tsx
rename to src/Components/Medicine/ResponsiveMedicineTables.tsx
index bbed23e7c07..ccec9a7c69d 100644
--- a/src/Components/Common/components/ResponsiveMedicineTables.tsx
+++ b/src/Components/Medicine/ResponsiveMedicineTables.tsx
@@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
-import AccordionV2 from "./AccordionV2";
-import { classNames } from "../../../Utils/utils";
+import AccordionV2 from "../Common/components/AccordionV2";
+import { classNames } from "../../Utils/utils";
function getWindowSize() {
const { innerWidth, innerHeight } = window;
diff --git a/src/Components/Medicine/models.ts b/src/Components/Medicine/models.ts
index 62aea46b6d2..ee42b03eca2 100644
--- a/src/Components/Medicine/models.ts
+++ b/src/Components/Medicine/models.ts
@@ -1,7 +1,7 @@
import { PerformedByModel } from "../HCX/misc";
interface BasePrescription {
- readonly id?: string;
+ readonly id: string;
medicine?: string;
medicine_object?: MedibaseMedicine;
medicine_old?: string;
@@ -10,7 +10,7 @@ interface BasePrescription {
notes?: string;
meta?: object;
readonly prescription_type?: "DISCHARGE" | "REGULAR";
- readonly discontinued?: boolean;
+ readonly discontinued: boolean;
discontinued_reason?: string;
readonly prescribed_by: PerformedByModel;
readonly discontinued_date: string;
@@ -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
new file mode 100644
index 00000000000..c0c4a602227
--- /dev/null
+++ b/src/Components/Medicine/routes.ts
@@ -0,0 +1,59 @@
+import { Type } from "../../Redux/api";
+import { PaginatedResponse } from "../../Utils/request/types";
+import { MedicineAdministrationRecord, Prescription } from "./models";
+
+const MedicineRoutes = {
+ listPrescriptions: {
+ path: "/api/v1/consultation/{consultation}/prescriptions/",
+ method: "GET",
+ TRes: Type>(),
+ },
+
+ createPrescription: {
+ path: "/api/v1/consultation/{consultation}/prescriptions/",
+ method: "POST",
+ TBody: Type(),
+ TRes: Type(),
+ },
+
+ listAdministrations: {
+ path: "/api/v1/consultation/{consultation}/prescription_administration/",
+ method: "GET",
+ TRes: Type>(),
+ },
+
+ getAdministration: {
+ path: "/api/v1/consultation/{consultation}/prescription_administration/{external_id}/",
+ method: "GET",
+ TRes: Type(),
+ },
+
+ getPrescription: {
+ path: "/api/v1/consultation/{consultation}/prescriptions/{external_id}/",
+ method: "GET",
+ TRes: Type(),
+ },
+
+ administerPrescription: {
+ path: "/api/v1/consultation/{consultation}/prescriptions/{external_id}/administer/",
+ method: "POST",
+ TBody: Type>(),
+ TRes: Type(),
+ },
+
+ discontinuePrescription: {
+ path: "/api/v1/consultation/{consultation}/prescriptions/{external_id}/discontinue/",
+ method: "POST",
+ 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/Components/Medicine/validators.ts b/src/Components/Medicine/validators.ts
new file mode 100644
index 00000000000..40261646d05
--- /dev/null
+++ b/src/Components/Medicine/validators.ts
@@ -0,0 +1,49 @@
+import { FieldError, RequiredFieldValidator } from "../Form/FieldValidators";
+import { FormErrors } from "../Form/Utils";
+import { Prescription } from "./models";
+
+export const PrescriptionFormValidator = () => {
+ return (form: Prescription): FormErrors => {
+ const errors: Partial> = {};
+ errors.medicine_object = RequiredFieldValidator()(form.medicine_object);
+ errors.dosage = RequiredFieldValidator()(form.dosage);
+ if (form.is_prn)
+ errors.indicator = RequiredFieldValidator()(form.indicator);
+ if (!form.is_prn)
+ errors.frequency = RequiredFieldValidator()(form.frequency);
+ return errors;
+ };
+};
+
+export const EditPrescriptionFormValidator = (old: Prescription) => {
+ return (form: Prescription): FormErrors => {
+ const errors = PrescriptionFormValidator()(form);
+
+ if (comparePrescriptions(old, form)) {
+ errors.$all = "No changes made";
+ }
+
+ return errors;
+ };
+};
+
+const PRESCRIPTION_COMPARE_FIELDS: (keyof Prescription)[] = [
+ "medicine",
+ "days",
+ "discontinued",
+ "dosage",
+ "frequency",
+ "indicator",
+ "is_prn",
+ "max_dosage",
+ "min_hours_between_doses",
+ "prescription_type",
+ "route",
+];
+
+export const comparePrescriptions = (a: Prescription, b: Prescription) => {
+ return (
+ PRESCRIPTION_COMPARE_FIELDS.every((field) => a[field] === b[field]) &&
+ a.medicine_object?.id === b.medicine_object?.id
+ );
+};
diff --git a/src/Components/Notifications/NoticeBoard.tsx b/src/Components/Notifications/NoticeBoard.tsx
index 7fd16efb3cf..8ad3afba0f8 100644
--- a/src/Components/Notifications/NoticeBoard.tsx
+++ b/src/Components/Notifications/NoticeBoard.tsx
@@ -1,43 +1,23 @@
-import { useEffect, useState } from "react";
-import { useDispatch } from "react-redux";
-import { getNotifications } from "../../Redux/actions";
import Page from "../Common/components/Page";
import Loading from "../Common/Loading";
import { formatDateTime } from "../../Utils/utils";
import { useTranslation } from "react-i18next";
import CareIcon from "../../CAREUI/icons/CareIcon";
+import useQuery from "../../Utils/request/useQuery";
+import routes from "../../Redux/api";
export const NoticeBoard = () => {
- const dispatch: any = useDispatch();
- const [isLoading, setIsLoading] = useState(true);
- const [data, setData] = useState([]);
const { t } = useTranslation();
-
- useEffect(() => {
- setIsLoading(true);
- dispatch(
- getNotifications(
- { offset: 0, event: "MESSAGE", medium_sent: "SYSTEM" },
- new Date().getTime().toString()
- )
- )
- .then((res: any) => {
- if (res && res.data) {
- setData(res.data.results);
- }
- setIsLoading(false);
- })
- .catch(() => {
- setIsLoading(false);
- });
- }, [dispatch]);
+ const { data, loading } = useQuery(routes.getNotifications, {
+ query: { offset: 0, event: "MESSAGE", medium_sent: "SYSTEM" },
+ });
let notices;
- if (data?.length) {
+ if (data?.results.length) {
notices = (
- {data.map((item) => (
+ {data.results.map((item) => (
{
);
}
- if (isLoading) return |