From b7fc53c670bb85e68631f8ce3b02defcda675ae7 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Tue, 3 Oct 2023 12:15:41 +0530 Subject: [PATCH 01/16] Revert "Prescriptions: Shrink discontinued prescriptions + Flip MAR timeline + Freeze primary columns in horizontal scroll (#6282)" (#6386) This reverts commit 5009a86abfb96e9b21f8371635176e4c3f73c5a7. --- src/Common/hooks/useRangePagination.ts | 32 +-- .../PrescriptionAdministrationsTable.tsx | 266 +++++++----------- src/Redux/actions.tsx | 2 +- 3 files changed, 120 insertions(+), 180 deletions(-) diff --git a/src/Common/hooks/useRangePagination.ts b/src/Common/hooks/useRangePagination.ts index e6bbe9f573e..7652ae546c1 100644 --- a/src/Common/hooks/useRangePagination.ts +++ b/src/Common/hooks/useRangePagination.ts @@ -9,18 +9,17 @@ interface Props { bounds: DateRange; perPage: number; slots?: number; - snapToLatest?: boolean; - reverse?: boolean; + defaultEnd?: boolean; } const useRangePagination = ({ bounds, perPage, ...props }: Props) => { const [currentRange, setCurrentRange] = useState( - getInitialBounds(bounds, perPage, props.snapToLatest) + getInitialBounds(bounds, perPage, props.defaultEnd) ); useEffect(() => { - setCurrentRange(getInitialBounds(bounds, perPage, props.snapToLatest)); - }, [bounds, perPage, props.snapToLatest]); + setCurrentRange(getInitialBounds(bounds, perPage, props.defaultEnd)); + }, [bounds, perPage, props.defaultEnd]); const next = () => { const { end } = currentRange; @@ -63,24 +62,17 @@ const useRangePagination = ({ bounds, perPage, ...props }: Props) => { } const slots: DateRange[] = []; - const { start, end } = currentRange; + const { start } = currentRange; const delta = perPage / props.slots; for (let i = 0; i < props.slots; i++) { - if (props.snapToLatest) { - slots.push({ - start: new Date(end.valueOf() - delta * (i - 1)), - end: new Date(end.valueOf() - delta * i), - }); - } else { - slots.push({ - start: new Date(start.valueOf() + delta * i), - end: new Date(start.valueOf() + delta * (i + 1)), - }); - } + slots.push({ + start: new Date(start.valueOf() + delta * i), + end: new Date(start.valueOf() + delta * (i + 1)), + }); } - return props.reverse ? slots.reverse() : slots; + return slots; }, [currentRange, props.slots, perPage]); return { @@ -98,7 +90,7 @@ export default useRangePagination; const getInitialBounds = ( bounds: DateRange, perPage: number, - snapToLatest?: boolean + defaultEnd?: boolean ) => { const deltaBounds = bounds.end.valueOf() - bounds.start.valueOf(); @@ -106,7 +98,7 @@ const getInitialBounds = ( return bounds; } - if (snapToLatest) { + if (defaultEnd) { return { start: new Date(bounds.end.valueOf() - perPage), end: bounds.end, diff --git a/src/Components/Medicine/PrescriptionAdministrationsTable.tsx b/src/Components/Medicine/PrescriptionAdministrationsTable.tsx index 470caa1042b..81282126d7c 100644 --- a/src/Components/Medicine/PrescriptionAdministrationsTable.tsx +++ b/src/Components/Medicine/PrescriptionAdministrationsTable.tsx @@ -47,10 +47,6 @@ export default function PrescriptionAdministrationsTable({ const { t } = useTranslation(); const [state, setState] = useState(); - - const [showDiscontinued, setShowDiscontinued] = useState(false); - const [discontinuedCount, setDiscontinuedCount] = useState(); - const pagination = useRangePagination({ bounds: state?.administrationsTimeBounds ?? { start: new Date(), @@ -58,8 +54,7 @@ export default function PrescriptionAdministrationsTable({ }, perPage: 24 * 60 * 60 * 1000, slots: 24, - snapToLatest: true, - reverse: true, + defaultEnd: true, }); const [showBulkAdminister, setShowBulkAdminister] = useState(false); @@ -69,13 +64,8 @@ export default function PrescriptionAdministrationsTable({ ); const refetch = useCallback(async () => { - const filters = { - is_prn: prn, - prescription_type: "REGULAR", - }; - const res = await dispatch( - list(showDiscontinued ? filters : { ...filters, discontinued: false }) + list({ is_prn: prn, prescription_type: "REGULAR" }) ); setState({ @@ -84,14 +74,7 @@ export default function PrescriptionAdministrationsTable({ ), administrationsTimeBounds: getAdministrationBounds(res.data.results), }); - - if (showDiscontinued === false) { - const discontinuedRes = await dispatch( - list({ ...filters, discontinued: true, limit: 0 }) - ); - setDiscontinuedCount(discontinuedRes.data.count); - } - }, [consultation_id, showDiscontinued, dispatch]); + }, [consultation_id, dispatch]); useEffect(() => { refetch(); @@ -158,22 +141,17 @@ export default function PrescriptionAdministrationsTable({ } /> -
- +
+
- + + )) - : pagination.slots - ?.map(({ start, end }, index) => ( - - )) - .reverse()} + : pagination.slots?.map(({ start, end }, index) => ( + + ))}
-
- {t("medicine")} - -

Dosage &

-

- {!state?.prescriptions[0]?.is_prn - ? "Frequency" - : "Indicator"} -

-
-
+
{t("medicine")} +

Dosage &

+

+ {!state?.prescriptions[0]?.is_prn ? "Frequency" : "Indicator"} +

@@ -184,10 +162,8 @@ export default function PrescriptionAdministrationsTable({ border className="mx-2 px-1" variant="secondary" - disabled={!pagination.hasNext} - onClick={pagination.next} - tooltip="Next 24 hours" - tooltipClassName="tooltip-bottom -translate-x-1/2 text-xs" + disabled={!pagination.hasPrevious} + onClick={pagination.previous} > @@ -201,26 +177,24 @@ export default function PrescriptionAdministrationsTable({

-

{formatDateTime(end, "DD/MM")}

-

{formatDateTime(end, "HH:mm")}

- - - Administration(s) between -
- {formatTime(start)} and{" "} - {formatTime(end)} -
- on {formatDate(start)} -
-
+

{formatDateTime(start, "DD/MM")}

+

{formatDateTime(start, "HH:mm")}

+ + + Administration(s) between +
+ {formatTime(start)} and{" "} + {formatTime(end)} +
+ on {formatDate(start)} +
+
@@ -255,23 +227,6 @@ export default function PrescriptionAdministrationsTable({
- {showDiscontinued === false && !!discontinuedCount && ( - setShowDiscontinued(true)} - > - - - - Show {discontinuedCount} other discontinued - prescription(s) - - - - )} - {state?.prescriptions.length === 0 && (
@@ -328,7 +283,12 @@ const PrescriptionRow = ({ prescription, ...props }: PrescriptionRowProps) => { }, [prescription.id, dispatch, props.intervals]); return ( - <> + {showDiscontinue && ( {
)} - setShowDetails(true)} > - setShowDetails(true)} - > -
-
- - {prescription.medicine_object?.name ?? - prescription.medicine_old} - - - {prescription.discontinued && ( - - {t("discontinued")} - - )} - - {prescription.route && ( - - {t(prescription.route)} - - )} -
+
+ + {prescription.medicine_object?.name ?? prescription.medicine_old} + -
-

{prescription.dosage}

-

- {!prescription.is_prn - ? t("PRESCRIPTION_FREQUENCY_" + prescription.frequency) - : prescription.indicator} -

-
-
- + {prescription.discontinued && ( + + {t("discontinued")} + + )} - - {/* Administration Cells */} - {props.intervals - .map(({ start, end }, index) => ( - - {administrations === undefined ? ( - - ) : ( - - )} - - )) - .reverse()} - - - {/* Action Buttons */} - - setShowAdminister(true)} - > - {t("administer")} - + {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")} + + + ); }; diff --git a/src/Redux/actions.tsx b/src/Redux/actions.tsx index 1a5bd7e4fb1..6e0d91fc59d 100644 --- a/src/Redux/actions.tsx +++ b/src/Redux/actions.tsx @@ -1003,7 +1003,7 @@ export const PrescriptionActions = (consultation_external_id: string) => { const pathParams = { consultation_external_id }; return { - list: (query?: Record) => { + list: (query?: Partial) => { let altKey; if (query?.is_prn !== undefined) { altKey = query?.is_prn From 8589460e826b3d3262dd2d22ee0d623846f0b26d Mon Sep 17 00:00:00 2001 From: Ashesh <3626859+Ashesh3@users.noreply.github.com> Date: Tue, 3 Oct 2023 20:22:47 +0530 Subject: [PATCH 02/16] Refactor Asset Model Import Formatting (#6388) * Refactor Asset Model Import Formatting * fixes to warranty_amc_end_of_validity * Fix asset import file for cypress --- cypress/fixtures/sampleAsset.xlsx | Bin 45398 -> 35942 bytes src/Common/constants.tsx | 45 +++++++++++++++------ src/Components/Assets/AssetImportModal.tsx | 20 +++++---- 3 files changed, 45 insertions(+), 20 deletions(-) diff --git a/cypress/fixtures/sampleAsset.xlsx b/cypress/fixtures/sampleAsset.xlsx index f8e234ce4477934a0426a088783ba4988500f7aa..49421f62baca164b5908d5b182ce11f8776aefb3 100644 GIT binary patch literal 35942 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%0DwfYI@bY)VNaG;z9w`%`XRb#^tALRKi~d%t+gfXc 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 OHExE3z-aI91n~g)q&wgM literal 45398 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

3{GwPx#=zj(sy?FWW-|fEUs= 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 diff --git a/src/Common/constants.tsx b/src/Common/constants.tsx index f64a9fa27ae..e51494f6bac 100644 --- a/src/Common/constants.tsx +++ b/src/Common/constants.tsx @@ -896,7 +896,7 @@ export const XLSXAssetImportSchema = { Class: { prop: "asset_class", type: String, - oneOf: ["HL7MONITOR", "ONVIF"], + oneOf: ["HL7MONITOR", "ONVIF", "VENTILATOR", ""], }, Description: { prop: "description", type: String }, "Working Status": { @@ -908,7 +908,7 @@ export const XLSXAssetImportSchema = { } else if (status === "NOT WORKING") { return false; } else { - throw new Error("Invalid Working Status"); + throw new Error("Invalid Working Status: " + status); } }, required: true, @@ -917,6 +917,7 @@ export const XLSXAssetImportSchema = { prop: "not_working_reason", type: String, }, + "Serial Number": { prop: "serial_number", type: String }, "QR Code ID": { prop: "qr_code_id", type: String }, Manufacturer: { prop: "manufacturer", type: String }, "Vendor Name": { prop: "vendor_name", type: String }, @@ -925,10 +926,11 @@ export const XLSXAssetImportSchema = { prop: "support_email", type: String, parse: (email: string) => { + if (!email) return null; const isValid = /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/.test(email); if (!isValid) { - throw new Error("Invalid Support Email"); + throw new Error("Invalid Support Email: " + email); } return email; @@ -938,23 +940,38 @@ export const XLSXAssetImportSchema = { prop: "support_phone", type: String, parse: (phone: number | string) => { - phone = "+91" + String(phone); - if (!PhoneNumberValidator(["support"])(phone) === undefined) { - throw new Error("Invalid Support Phone Number"); + phone = String(phone); + if (phone.length === 10 && !phone.startsWith("1800")) { + phone = "+91" + phone; + } + if (phone.startsWith("91") && phone.length === 12) { + phone = "+" + phone; + } + if (phone.startsWith("+911800")) { + phone = "1800" + phone.slice(6); + } + if ( + PhoneNumberValidator(["mobile", "landline", "support"])(phone) !== + undefined + ) { + throw new Error("Invalid Support Phone Number: " + phone); } return phone ? phone : undefined; }, required: true, }, - "Warrenty End Date": { + "Warranty End Date": { prop: "warranty_amc_end_of_validity", type: String, parse: (date: string) => { - const parsed = new Date(date); + if (!date) return null; + const parts = date.split("-"); + const reformattedDateStr = `${parts[2]}-${parts[1]}-${parts[0]}`; + const parsed = new Date(reformattedDateStr); if (String(parsed) === "Invalid Date") { - throw new Error("Invalid Warrenty End Date"); + throw new Error("Invalid Warranty End Date:" + date); } return dateQueryString(parsed); @@ -964,10 +981,13 @@ export const XLSXAssetImportSchema = { prop: "last_serviced_on", type: String, parse: (date: string) => { - const parsed = new Date(date); + if (!date) return null; + const parts = date.split("-"); + const reformattedDateStr = `${parts[2]}-${parts[1]}-${parts[0]}`; + const parsed = new Date(reformattedDateStr); if (String(parsed) === "Invalid Date") { - throw new Error("Invalid Last Service Date"); + throw new Error("Invalid Last Service Date:" + date); } return dateQueryString(parsed); @@ -981,13 +1001,14 @@ export const XLSXAssetImportSchema = { prop: "local_ip_address", type: String, parse: (ip: string) => { + if (!ip) return null; const isValid = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test( ip ); if (!isValid) { - throw new Error("Invalid Config IP Address"); + throw new Error("Invalid Config IP Address: " + ip); } return ip; diff --git a/src/Components/Assets/AssetImportModal.tsx b/src/Components/Assets/AssetImportModal.tsx index 548df2901ec..7382c7eff4f 100644 --- a/src/Components/Assets/AssetImportModal.tsx +++ b/src/Components/Assets/AssetImportModal.tsx @@ -85,11 +85,11 @@ const AssetImportModal = ({ open, onClose, facility }: Props) => { } } } - } catch (e) { + } catch (e: any) { setPreview(undefined); console.log(e); Notification.Error({ - msg: "Invalid file", + msg: "Invalid file: " + e.message, }); setSelectedFile(undefined); } @@ -113,7 +113,7 @@ const AssetImportModal = ({ open, onClose, facility }: Props) => { let error = false; for (const asset of preview || []) { - const asset_data = JSON.stringify({ + const asset_data: any = { name: asset.name, asset_type: asset.asset_type, asset_class: asset.asset_class, @@ -129,11 +129,15 @@ const AssetImportModal = ({ open, onClose, facility }: Props) => { qr_code_id: asset.qr_code_id, manufacturer: asset.manufacturer, meta: { ...asset.meta }, - warranty_amc_end_of_validity: asset.warranty_amc_end_of_validity, - last_serviced_on: asset.last_serviced_on, note: asset.notes, - cancelToken: { promise: {} }, - }); + }; + + if (asset.last_serviced_on) + asset_data["last_serviced_on"] = asset.last_serviced_on; + + if (asset.warranty_amc_end_of_validity) + asset_data["warranty_amc_end_of_validity"] = + asset.warranty_amc_end_of_validity; const response = await fetch("/api/v1/asset/", { method: "POST", @@ -142,7 +146,7 @@ const AssetImportModal = ({ open, onClose, facility }: Props) => { Authorization: "Bearer " + localStorage.getItem(LocalStorageKeys.accessToken), }, - body: asset_data, + body: JSON.stringify(asset_data), }); const data = await response.json(); if (response.status !== 201) { From d53eed9adb58ff6d4aba183815050b644dcee948 Mon Sep 17 00:00:00 2001 From: Aaron Jevil Nazareth <93522968+jevil25@users.noreply.github.com> Date: Wed, 4 Oct 2023 09:12:04 +0530 Subject: [PATCH 03/16] =?UTF-8?q?Refactor:=20replaced=20Dispatch=20to=20us?= =?UTF-8?q?eQuery/Request=20of=20src/Components/Auth/log=E2=80=A6=20(#6333?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor: replaced useDispatch to useQuery of src/Components/Auth/login.tsx * fix:useQuery changed to Request * feat: replaced dispatch with request * fix: types and added pathparams * fix: request body error * Update Login.tsx * change:Tres to Tbody * fixes: response type change * Update package-lock.json --- src/Components/Auth/Login.tsx | 76 ++++++++++++--------------- src/Components/Auth/ResetPassword.tsx | 46 ++++++++-------- src/Redux/api.tsx | 13 +++++ 3 files changed, 70 insertions(+), 65 deletions(-) diff --git a/src/Components/Auth/Login.tsx b/src/Components/Auth/Login.tsx index cb7ea7f08f8..dd6a42d3983 100644 --- a/src/Components/Auth/Login.tsx +++ b/src/Components/Auth/Login.tsx @@ -1,10 +1,9 @@ import { useEffect, useState } from "react"; -import { useDispatch } from "react-redux"; -import { postForgotPassword, postLogin } from "../../Redux/actions"; +import request from "../../Utils/request/request"; +import routes from "../../Redux/api"; import { useTranslation } from "react-i18next"; import ReCaptcha from "react-google-recaptcha"; import * as Notification from "../../Utils/Notifications.js"; -import { get } from "lodash"; import LegendInput from "../../CAREUI/interactive/LegendInput"; import LanguageSelectorLogin from "../Common/LanguageSelectorLogin"; import CareIcon from "../../CAREUI/icons/CareIcon"; @@ -25,7 +24,6 @@ export const Login = (props: { forgot?: boolean }) => { custom_logo_alt, custom_description, } = useConfig(); - const dispatch: any = useDispatch(); const initForm: any = { username: "", password: "", @@ -90,37 +88,35 @@ export const Login = (props: { forgot?: boolean }) => { }; }, []); - const handleSubmit = (e: any) => { + const handleSubmit = async (e: any) => { e.preventDefault(); const valid = validateData(); if (valid) { // replaces button with spinner setLoading(true); - dispatch(postLogin(valid)).then((resp: any) => { - const res = get(resp, "data", null); - const statusCode = get(resp, "status", ""); - if (res && statusCode === 429) { - setCaptcha(true); - // captcha displayed set back to login button - setLoading(false); - } else if (res && statusCode === 200) { - localStorage.setItem(LocalStorageKeys.accessToken, res.access); - localStorage.setItem(LocalStorageKeys.refreshToken, res.refresh); - - if ( - window.location.pathname === "/" || - window.location.pathname === "/login" - ) { - window.location.href = "/facility"; - } else { - window.location.href = window.location.pathname.toString(); - } + const { res, data } = await request(routes.login, { + body: { ...valid }, + }); + if (res && res.status === 429) { + setCaptcha(true); + // captcha displayed set back to login button + setLoading(false); + } else if (res && res.status === 200 && data) { + localStorage.setItem(LocalStorageKeys.accessToken, data.access); + localStorage.setItem(LocalStorageKeys.refreshToken, data.refresh); + if ( + window.location.pathname === "/" || + window.location.pathname === "/login" + ) { + window.location.href = "/facility"; } else { - // error from server set back to login button - setLoading(false); + window.location.href = window.location.pathname.toString(); } - }); + } else { + // error from server set back to login button + setLoading(false); + } } }; @@ -146,26 +142,22 @@ export const Login = (props: { forgot?: boolean }) => { return form; }; - const handleForgetSubmit = (e: any) => { + const handleForgetSubmit = async (e: any) => { e.preventDefault(); const valid = validateForgetData(); if (valid) { setLoading(true); - dispatch(postForgotPassword(valid)).then((resp: any) => { - setLoading(false); - const res = resp && resp.data; - if (res && res.status === "OK") { - Notification.Success({ - msg: t("password_sent"), - }); - } else if (res && res.data) { - setErrors(res.data); - } else { - Notification.Error({ - msg: t("something_wrong"), - }); - } + const { res, error } = await request(routes.forgotPassword, { + body: { ...valid }, }); + setLoading(false); + if (res && res.statusText === "OK") { + Notification.Success({ + msg: t("password_sent"), + }); + } else if (res && error) { + setErrors(error); + } } }; diff --git a/src/Components/Auth/ResetPassword.tsx b/src/Components/Auth/ResetPassword.tsx index 08dd1da44a0..2f02737f6de 100644 --- a/src/Components/Auth/ResetPassword.tsx +++ b/src/Components/Auth/ResetPassword.tsx @@ -1,7 +1,6 @@ import { useEffect, useState } from "react"; -import { useDispatch } from "react-redux"; +import request from "../../Utils/request/request.js"; import * as Notification from "../../Utils/Notifications.js"; -import { postResetPassword, checkResetToken } from "../../Redux/actions"; import { navigate } from "raviger"; import { useTranslation } from "react-i18next"; import { LocalStorageKeys } from "../../Common/constants"; @@ -9,9 +8,9 @@ import { Cancel, Submit } from "../Common/components/ButtonV2"; import TextFormField from "../Form/FormFields/TextFormField"; import { validateRule } from "../Users/UserAdd"; import { validatePassword } from "../../Common/validation.js"; +import routes from "../../Redux/api.js"; export const ResetPassword = (props: any) => { - const dispatch: any = useDispatch(); const initForm: any = { password: "", confirm: "", @@ -65,36 +64,37 @@ export const ResetPassword = (props: any) => { return form; }; - const handleSubmit = (e: any) => { + const handleSubmit = async (e: any) => { e.preventDefault(); const valid = validateData(); if (valid) { valid.token = props.token; - dispatch(postResetPassword(valid)).then((resp: any) => { - const res = resp && resp.data; - if (res && res.status === "OK") { - localStorage.removeItem(LocalStorageKeys.accessToken); - Notification.Success({ - msg: t("password_reset_success"), - }); - navigate("/login"); - } else if (res && res.data) { - setErrors(res.data); - } else { - Notification.Error({ - msg: t("password_reset_failure"), - }); - } + const { res, error } = await request(routes.resetPassword, { + body: { ...valid }, }); + if (res && res.statusText === "OK") { + localStorage.removeItem(LocalStorageKeys.accessToken); + Notification.Success({ + msg: t("password_reset_success"), + }); + navigate("/login"); + } else if (res && error) { + setErrors(error); + } } }; useEffect(() => { - if (props.token) { - dispatch(checkResetToken({ token: props.token })).then((resp: any) => { - const res = resp && resp.data; - if (!res || res.status !== "OK") navigate("/invalid-reset"); + const checkResetToken = async () => { + const { res } = await request(routes.checkResetToken, { + body: { token: props.token }, }); + if (!res || res.statusText !== "OK") { + navigate("/invalid-reset"); + } + }; + if (props.token) { + checkResetToken(); } else { navigate("/invalid-reset"); } diff --git a/src/Redux/api.tsx b/src/Redux/api.tsx index 1aa06c7e1bc..f26457183dd 100644 --- a/src/Redux/api.tsx +++ b/src/Redux/api.tsx @@ -17,6 +17,11 @@ interface JwtTokenObtainPair { refresh: string; } +interface LoginInput { + username: string; + password: string; +} + const routes = { config: { path: import.meta.env.REACT_APP_CONFIG ?? "/config.json", @@ -30,6 +35,8 @@ const routes = { path: "/api/v1/auth/login/", method: "POST", noAuth: true, + TRes: Type(), + TBody: Type(), }, token_refresh: { @@ -47,16 +54,22 @@ const routes = { checkResetToken: { path: "/api/v1/password_reset/check/", method: "POST", + TRes: Type>(), + TBody: Type<{ token: string }>(), }, resetPassword: { path: "/api/v1/password_reset/confirm/", method: "POST", + TRes: Type>(), + TBody: Type<{ password: string; confirm: string }>(), }, forgotPassword: { path: "/api/v1/password_reset/", method: "POST", + TRes: Type>(), + TBody: Type<{ username: string }>(), }, updatePassword: { From 66e5d9192f9ce7fadeb81fae58f5bff502480b33 Mon Sep 17 00:00:00 2001 From: Pranshu Aggarwal <70687348+Pranshu1902@users.noreply.github.com> Date: Wed, 4 Oct 2023 09:44:56 +0530 Subject: [PATCH 04/16] fix hover height (#6352) --- src/Components/Facility/FacilityHome.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/Facility/FacilityHome.tsx b/src/Components/Facility/FacilityHome.tsx index 15317b1b56c..4fa61514c64 100644 --- a/src/Components/Facility/FacilityHome.tsx +++ b/src/Components/Facility/FacilityHome.tsx @@ -323,7 +323,7 @@ export const FacilityHome = (props: any) => { StaffUserTypeIndex; const editCoverImageTooltip = hasPermissionToEditCoverImage && ( -

+
{`${hasCoverImage ? "Edit" : "Upload"}`}
From 93f20fb77e452a3f3d93982ccfd79518e181d2a9 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Wed, 4 Oct 2023 09:55:30 +0530 Subject: [PATCH 05/16] Split routes of App Router (#6363) --- ...tyMiddleware.tsx => FacilityConfigure.tsx} | 2 +- src/Components/Facility/FacilityHome.tsx | 4 +- .../Notifications/ShowPushNotification.tsx | 6 +- src/Components/Patient/FileUpload.tsx | 2 +- src/Components/Patient/SampleDetails.tsx | 7 +- src/Components/Resource/ListView.tsx | 2 +- src/Components/Resource/ResourceBoardView.tsx | 2 +- src/Components/Resource/ResourceDetails.tsx | 2 +- src/Components/Shifting/BoardView.tsx | 4 +- src/Components/Shifting/ListView.tsx | 4 +- src/Components/Shifting/ShiftDetails.tsx | 2 +- src/Redux/actions.tsx | 2 +- src/Routers/AppRouter.tsx | 429 ++---------------- src/Routers/routes/AssetRoutes.tsx | 21 + src/Routers/routes/ConsultationRoutes.tsx | 141 ++++++ src/Routers/routes/ExternalResultRoutes.tsx | 14 + .../routes/FacilityInventoryRoutes.tsx | 24 + src/Routers/routes/FacilityLocationRoutes.tsx | 38 ++ src/Routers/routes/FacilityRoutes.tsx | 45 ++ src/Routers/routes/HCXRoutes.tsx | 6 + src/Routers/routes/PatientRoutes.tsx | 46 ++ src/Routers/routes/ResourceRoutes.tsx | 25 + src/Routers/routes/SampleRoutes.tsx | 25 + src/Routers/routes/ShiftingRoutes.tsx | 27 ++ src/Routers/routes/UserRoutes.tsx | 9 + src/Routers/types.ts | 5 + 26 files changed, 481 insertions(+), 413 deletions(-) rename src/Components/Facility/{UpdateFacilityMiddleware.tsx => FacilityConfigure.tsx} (98%) create mode 100644 src/Routers/routes/AssetRoutes.tsx create mode 100644 src/Routers/routes/ConsultationRoutes.tsx create mode 100644 src/Routers/routes/ExternalResultRoutes.tsx create mode 100644 src/Routers/routes/FacilityInventoryRoutes.tsx create mode 100644 src/Routers/routes/FacilityLocationRoutes.tsx create mode 100644 src/Routers/routes/FacilityRoutes.tsx create mode 100644 src/Routers/routes/HCXRoutes.tsx create mode 100644 src/Routers/routes/PatientRoutes.tsx create mode 100644 src/Routers/routes/ResourceRoutes.tsx create mode 100644 src/Routers/routes/SampleRoutes.tsx create mode 100644 src/Routers/routes/ShiftingRoutes.tsx create mode 100644 src/Routers/routes/UserRoutes.tsx create mode 100644 src/Routers/types.ts diff --git a/src/Components/Facility/UpdateFacilityMiddleware.tsx b/src/Components/Facility/FacilityConfigure.tsx similarity index 98% rename from src/Components/Facility/UpdateFacilityMiddleware.tsx rename to src/Components/Facility/FacilityConfigure.tsx index 211d8cf458a..6f0a8a9869c 100644 --- a/src/Components/Facility/UpdateFacilityMiddleware.tsx +++ b/src/Components/Facility/FacilityConfigure.tsx @@ -46,7 +46,7 @@ const FormReducer = (state = initialState, action: any) => { } }; -export const UpdateFacilityMiddleware = (props: any) => { +export const FacilityConfigure = (props: any) => { const [state, dispatch] = useReducer(FormReducer, initialState); const { facilityId } = props; const dispatchAction: any = useDispatch(); diff --git a/src/Components/Facility/FacilityHome.tsx b/src/Components/Facility/FacilityHome.tsx index 4fa61514c64..68990e64416 100644 --- a/src/Components/Facility/FacilityHome.tsx +++ b/src/Components/Facility/FacilityHome.tsx @@ -541,9 +541,7 @@ export const FacilityHome = (props: any) => { - navigate(`/facility/${facilityId}/middleware/update`) - } + onClick={() => navigate(`/facility/${facilityId}/configure`)} authorizeFor={NonReadOnlyUsers} icon={} > diff --git a/src/Components/Notifications/ShowPushNotification.tsx b/src/Components/Notifications/ShowPushNotification.tsx index fe88c087d5e..09b62bb9333 100644 --- a/src/Components/Notifications/ShowPushNotification.tsx +++ b/src/Components/Notifications/ShowPushNotification.tsx @@ -1,13 +1,13 @@ import { useDispatch } from "react-redux"; import { getNotificationData } from "../../Redux/actions"; import { useEffect } from "react"; +import { DetailRoute } from "../../Routers/types"; -export default function ShowPushNotification({ external_id }: any) { +export default function ShowPushNotification({ id }: DetailRoute) { const dispatch: any = useDispatch(); const resultUrl = async () => { - console.log("ID:", external_id.id); - const res = await dispatch(getNotificationData({ id: external_id.id })); + const res = await dispatch(getNotificationData({ id })); const data = res.data.caused_objects; switch (res.data.event) { case "PATIENT_CREATED": diff --git a/src/Components/Patient/FileUpload.tsx b/src/Components/Patient/FileUpload.tsx index fe0881e6994..2b0b068b0ef 100644 --- a/src/Components/Patient/FileUpload.tsx +++ b/src/Components/Patient/FileUpload.tsx @@ -100,7 +100,7 @@ interface FileUploadProps { hideBack: boolean; audio?: boolean; unspecified: boolean; - sampleId?: number; + sampleId?: string; claimId?: string; } diff --git a/src/Components/Patient/SampleDetails.tsx b/src/Components/Patient/SampleDetails.tsx index 7814109978b..e29df92f47f 100644 --- a/src/Components/Patient/SampleDetails.tsx +++ b/src/Components/Patient/SampleDetails.tsx @@ -13,14 +13,11 @@ import { getTestSample } from "../../Redux/actions"; import { navigate } from "raviger"; import { useDispatch } from "react-redux"; +import { DetailRoute } from "../../Routers/types"; const Loading = lazy(() => import("../Common/Loading")); -interface SampleDetailsProps { - id: number; -} - -export const SampleDetails = ({ id }: SampleDetailsProps) => { +export const SampleDetails = ({ id }: DetailRoute) => { const dispatch: any = useDispatch(); const [isLoading, setIsLoading] = useState(false); const [sampleDetails, setSampleDetails] = useState({}); diff --git a/src/Components/Resource/ListView.tsx b/src/Components/Resource/ListView.tsx index b368c9bf3ea..24b4e263039 100644 --- a/src/Components/Resource/ListView.tsx +++ b/src/Components/Resource/ListView.tsx @@ -31,7 +31,7 @@ export default function ListView() { const { t } = useTranslation(); const onBoardViewBtnClick = () => - navigate("/resource/board-view", { query: qParams }); + navigate("/resource/board", { query: qParams }); const appliedFilters = formatFilter(qParams); const refreshList = () => { diff --git a/src/Components/Resource/ResourceBoardView.tsx b/src/Components/Resource/ResourceBoardView.tsx index de3058f9406..d3b2b202649 100644 --- a/src/Components/Resource/ResourceBoardView.tsx +++ b/src/Components/Resource/ResourceBoardView.tsx @@ -32,7 +32,7 @@ export default function BoardView() { const { t } = useTranslation(); const onListViewBtnClick = () => { - navigate("/resource/list-view", { query: qParams }); + navigate("/resource/list", { query: qParams }); localStorage.setItem("defaultResourceView", "list"); }; diff --git a/src/Components/Resource/ResourceDetails.tsx b/src/Components/Resource/ResourceDetails.tsx index 591f6b48bdb..67adc7edd82 100644 --- a/src/Components/Resource/ResourceDetails.tsx +++ b/src/Components/Resource/ResourceDetails.tsx @@ -231,7 +231,7 @@ export default function ResourceDetails(props: { id: string }) { {isPrintMode ? (
diff --git a/src/Components/Shifting/BoardView.tsx b/src/Components/Shifting/BoardView.tsx index 0eddfddf745..85815770acf 100644 --- a/src/Components/Shifting/BoardView.tsx +++ b/src/Components/Shifting/BoardView.tsx @@ -95,9 +95,7 @@ export default function BoardView() {
- navigate("/shifting/list-view", { query: qParams }) - } + onClick={() => navigate("/shifting/list", { query: qParams })} > {t("list_view")} diff --git a/src/Components/Shifting/ListView.tsx b/src/Components/Shifting/ListView.tsx index 6d55122ea11..f3fb14a4c0a 100644 --- a/src/Components/Shifting/ListView.tsx +++ b/src/Components/Shifting/ListView.tsx @@ -312,9 +312,7 @@ export default function ListView() {
- navigate("/shifting/board-view", { query: qParams }) - } + onClick={() => navigate("/shifting/board", { query: qParams })} > {t("board_view")} diff --git a/src/Components/Shifting/ShiftDetails.tsx b/src/Components/Shifting/ShiftDetails.tsx index 03e09702aac..7bffe429960 100644 --- a/src/Components/Shifting/ShiftDetails.tsx +++ b/src/Components/Shifting/ShiftDetails.tsx @@ -557,7 +557,7 @@ export default function ShiftDetails(props: { id: string }) { ) : ( { export const getTestList = (params: object) => { return fireRequest("getTestSampleList", [], params); }; -export const getTestSample = (id: number) => { +export const getTestSample = (id: string) => { return fireRequest("getTestSample", [id], {}); }; export const patchSample = (params: object, pathParam: object) => { diff --git a/src/Routers/AppRouter.tsx b/src/Routers/AppRouter.tsx index f1449f13bc2..0f6108b00e3 100644 --- a/src/Routers/AppRouter.tsx +++ b/src/Routers/AppRouter.tsx @@ -1,64 +1,9 @@ import { useRedirect, useRoutes, usePath, Redirect } from "raviger"; import { useState, useEffect } from "react"; -import { ConsultationDetails } from "../Components/Facility/ConsultationDetails"; -import TreatmentSummary from "../Components/Facility/TreatmentSummary"; -import { ConsultationForm } from "../Components/Facility/ConsultationForm"; -import { FacilityCreate } from "../Components/Facility/FacilityCreate"; -import { FacilityHome } from "../Components/Facility/FacilityHome"; -import { HospitalList } from "../Components/Facility/HospitalList"; -import { TriageForm } from "../Components/Facility/TriageForm"; -import { DailyRounds } from "../Components/Patient/DailyRounds"; -import { PatientManager } from "../Components/Patient/ManagePatients"; -import PatientNotes from "../Components/Patient/PatientNotes"; -import { PatientHome } from "../Components/Patient/PatientHome"; -import { PatientRegister } from "../Components/Patient/PatientRegister"; -import { SampleDetails } from "../Components/Patient/SampleDetails"; -import SampleReport from "../Components/Patient/SamplePreview"; -import { SampleTest } from "../Components/Patient/SampleTest"; -import SampleViewAdmin from "../Components/Patient/SampleViewAdmin"; -import ManageUsers from "../Components/Users/ManageUsers"; -import { UserAdd } from "../Components/Users/UserAdd"; -import InventoryList from "../Components/Facility/InventoryList"; -import InventoryLog from "../Components/Facility/InventoryLog"; -import { AddInventoryForm } from "../Components/Facility/AddInventoryForm"; -import { SetInventoryForm } from "../Components/Facility/SetInventoryForm"; -import MinQuantityList from "../Components/Facility/MinQuantityList"; -import { ShiftCreate } from "../Components/Patient/ShiftCreate"; -import UserProfile from "../Components/Users/UserProfile"; -import ShiftBoardView from "../Components/Shifting/BoardView"; -import ShiftListView from "../Components/Shifting/ListView"; -import ShiftDetails from "../Components/Shifting/ShiftDetails"; -import { ShiftDetailsUpdate } from "../Components/Shifting/ShiftDetailsUpdate"; -import ResourceCreate from "../Components/Resource/ResourceCreate"; -import ResourceBoardView from "../Components/Resource/ResourceBoardView"; -import ResourceListView from "../Components/Resource/ListView"; -import ResourceDetails from "../Components/Resource/ResourceDetails"; -import { ResourceDetailsUpdate } from "../Components/Resource/ResourceDetailsUpdate"; -import ResultList from "../Components/ExternalResult/ResultList"; -import ResultItem from "../Components/ExternalResult/ResultItem"; -import ExternalResultUpload from "../Components/ExternalResult/ExternalResultUpload"; -import ResultUpdate from "../Components/ExternalResult/ResultUpdate"; -import { FileUpload } from "../Components/Patient/FileUpload"; -import Investigation from "../Components/Facility/Investigations"; -import ShowInvestigation from "../Components/Facility/Investigations/ShowInvestigation"; -import InvestigationReports from "../Components/Facility/Investigations/Reports"; -import AssetCreate from "../Components/Facility/AssetCreate"; -import DeathReport from "../Components/DeathReport/DeathReport"; -import { make as CriticalCareRecording } from "../Components/CriticalCareRecording/CriticalCareRecording.bs"; + import ShowPushNotification from "../Components/Notifications/ShowPushNotification"; import { NoticeBoard } from "../Components/Notifications/NoticeBoard"; -import { AddLocationForm } from "../Components/Facility/AddLocationForm"; -import { AddBedForm } from "../Components/Facility/AddBedForm"; -import LocationManagement from "../Components/Facility/LocationManagement"; -import { BedManagement } from "../Components/Facility/BedManagement"; -import AssetsList from "../Components/Assets/AssetsList"; -import AssetManage from "../Components/Assets/AssetManage"; -import AssetConfigure from "../Components/Assets/AssetConfigure"; -import { DailyRoundListDetails } from "../Components/Patient/DailyRoundListDetails"; import Error404 from "../Components/ErrorPages/404"; -import { DndProvider } from "react-dnd"; -import { HTML5Backend } from "react-dnd-html5-backend"; -import FacilityUsers from "../Components/Facility/FacilityUsers"; import { DesktopSidebar, MobileSidebar, @@ -66,349 +11,55 @@ import { SidebarShrinkContext, } from "../Components/Common/Sidebar/Sidebar"; import { BLACKLISTED_PATHS, LocalStorageKeys } from "../Common/constants"; -import { UpdateFacilityMiddleware } from "../Components/Facility/UpdateFacilityMiddleware"; import useConfig from "../Common/hooks/useConfig"; -import ConsultationClaims from "../Components/Facility/ConsultationClaims"; import { handleSignOut } from "../Utils/utils"; import SessionExpired from "../Components/ErrorPages/SessionExpired"; -import ManagePrescriptions from "../Components/Medicine/ManagePrescriptions"; -import CentralNursingStation from "../Components/Facility/CentralNursingStation"; -export default function AppRouter() { - const { main_logo, enable_hcx } = useConfig(); +import UserRoutes from "./routes/UserRoutes"; +import PatientRoutes from "./routes/PatientRoutes"; +import SampleRoutes from "./routes/SampleRoutes"; +import FacilityRoutes from "./routes/FacilityRoutes"; +import ConsultationRoutes from "./routes/ConsultationRoutes"; +import HCXRoutes from "./routes/HCXRoutes"; +import ShiftingRoutes from "./routes/ShiftingRoutes"; +import AssetRoutes from "./routes/AssetRoutes"; +import ResourceRoutes from "./routes/ResourceRoutes"; +import ExternalResultRoutes from "./routes/ExternalResultRoutes"; +import { DetailRoute } from "./types"; + +const Routes = { + "/": () => , - const routes = { - "/": () => , - "/users": () => , - "/users/add": () => , - "/user/profile": () => , - "/patients": () => , - "/patient/:id": ({ id }: any) => , - "/patient/:id/investigation_reports": ({ id }: any) => ( - - ), - "/sample": () => , - "/sample/:id": ({ id }: any) => , - "/patient/:patientId/test_sample/:sampleId/icmr_sample": ({ - patientId, - sampleId, - }: any) => , - "/facility": () => , - "/facility/create": () => , - "/facility/:facilityId/update": ({ facilityId }: any) => ( - - ), - "/facility/:facilityId/middleware/update": ({ facilityId }: any) => ( - - ), - "/facility/:facilityId": ({ facilityId }: any) => ( - - ), - "/facility/:facilityId/users": ({ facilityId }: any) => ( - - ), - "/facility/:facilityId/resource/new": ({ facilityId }: any) => ( - - ), - "/facility/:facilityId/triage": ({ facilityId }: any) => ( - - ), - "/facility/:facilityId/patient": ({ facilityId }: any) => ( - - ), - "/facility/:facilityId/patient/:id": ({ facilityId, id }: any) => ( - - ), - "/facility/:facilityId/patient/:id/update": ({ facilityId, id }: any) => ( - - ), - "/facility/:facilityId/patient/:patientId/sample-test": ({ - facilityId, - patientId, - }: any) => , - "/facility/:facilityId/patient/:patientId/sample/:id": ({ id }: any) => ( - - ), - "/facility/:facilityId/patient/:patientId/notes": ({ - facilityId, - patientId, - }: any) => , - "/facility/:facilityId/patient/:patientId/files": ({ - facilityId, - patientId, - }: any) => ( - - ), - "/facility/:facilityId/triage/:id": ({ facilityId, id }: any) => ( - - ), - "/facility/:facilityId/patient/:patientId/consultation": ({ - facilityId, - patientId, - }: any) => ( - - ), - "/facility/:facilityId/patient/:patientId/consultation/:id/update": ({ - facilityId, - patientId, - id, - }: any) => ( - - ), - "/facility/:facilityId/patient/:patientId/consultation/:id/files/": ({ - facilityId, - patientId, - id, - }: any) => ( - - ), - "/facility/:facilityId/patient/:patientId/consultation/:consultationId/prescriptions": - (path: any) => , - "/facility/:facilityId/patient/:patientId/consultation/:id/investigation": - ({ facilityId, patientId, id }: any) => ( - - ), - "/facility/:facilityId/patient/:patientId/consultation/:id/investigation/:sessionId": - ({ facilityId, patientId, id, sessionId }: any) => ( - - ), - "/facility/:facilityId/patient/:patientId/consultation/:id/daily-rounds": ({ - facilityId, - patientId, - id, - }: any) => ( - - ), - ...(enable_hcx - ? { - "/facility/:facilityId/patient/:patientId/consultation/:consultationId/claims": - (pathParams: any) => , - } - : {}), - "/facility/:facilityId/patient/:patientId/consultation/:consultationId/daily-rounds/:id/update": - ({ facilityId, patientId, consultationId, id }: any) => ( - - ), - "/facility/:facilityId/patient/:patientId/consultation/:consultationId/daily-rounds/:id": - ({ facilityId, patientId, consultationId, id }: any) => ( - - ), + ...AssetRoutes, + ...ConsultationRoutes, + ...ExternalResultRoutes, + ...FacilityRoutes, + ...PatientRoutes, + ...ResourceRoutes, + ...SampleRoutes, + ...ShiftingRoutes, + ...UserRoutes, - "/facility/:facilityId/patient/:patientId/consultation/:consultationId/daily_rounds/:id": - ({ facilityId, patientId, consultationId, id }: any) => ( - - ), - "/facility/:facilityId/patient/:patientId/consultation/:consultationId/daily_rounds/:id/update": - ({ facilityId, patientId, consultationId, id }: any) => ( - - ), - "/facility/:facilityId/patient/:patientId/shift/new": ({ - facilityId, - patientId, - }: any) => , - "/facility/:facilityId/inventory": ({ facilityId }: any) => ( - - ), - "/facility/:facilityId/location": ({ facilityId }: any) => ( - - ), - "/facility/:facilityId/location/:locationId/beds": ({ - facilityId, - locationId, - }: any) => ( - - ), - "/facility/:facilityId/inventory/add": ({ facilityId }: any) => ( - - ), - "/facility/:facilityId/location/add": ({ facilityId }: any) => ( - - ), - "/facility/:facilityId/location/:locationId/update": ({ - facilityId, - locationId, - }: any) => ( - - ), - "/facility/:facilityId/location/:locationId/beds/add": ({ - facilityId, - locationId, - }: any) => , - "/facility/:facilityId/location/:locationId/beds/:bedId/update": ({ - facilityId, - locationId, - bedId, - }: any) => ( - - ), - "/facility/:facilityId/inventory/min_quantity/set": ({ - facilityId, - }: any) => , - "/facility/:facilityId/inventory/min_quantity/list": ({ - facilityId, - }: any) => , - "/facility/:facilityId/inventory/min_quantity": ({ facilityId }: any) => ( - - ), - "/facility/:facilityId/inventory/:inventoryId": ({ - facilityId, - inventoryId, - }: any) => ( - - ), - "/facility/:facilityId/assets/new": ({ facilityId }: any) => ( - - ), - "/facility/:facilityId/assets/:assetId/update": ({ - facilityId, - assetId, - }: any) => , - "/assets": () => , - "/facility/:facilityId/assets/:assetId": ({ assetId, facilityId }: any) => ( - - ), - "/facility/:facilityId/assets/:assetId/configure": ({ - assetId, - facilityId, - }: any) => , - "/facility/:facilityId/cns": ({ facilityId }: any) => ( - - ), + "/notifications/:id": ({ id }: DetailRoute) => ( + + ), + "/notice_board": () => , + + "/session-expired": () => , + "/not-found": () => , +}; + +export default function AppRouter() { + const { main_logo, enable_hcx } = useConfig(); - "/shifting": () => - localStorage.getItem("defaultShiftView") === "list" ? ( - - ) : ( - - - - ), - "/shifting/board-view": () => ( - - - - ), - "/shifting/list-view": () => , - "/shifting/:id": ({ id }: any) => , - "/shifting/:id/update": ({ id }: any) => , - "/resource": () => - localStorage.getItem("defaultResourceView") === "list" ? ( - - ) : ( - - - - ), + let routes = Routes; - "/resource/board-view": () => ( - - - - ), - "/resource/list-view": () => , - "/resource/:id": ({ id }: any) => , - "/resource/:id/update": ({ id }: any) => , - "/external_results": () => , - "/external_results/upload": () => , - "/external_results/:id": ({ id }: any) => , - "/external_results/:id/update": ({ id }: any) => , - "/death_report/:id": ({ id }: any) => , - "/notifications/:id": (id: any) => ( - - ), - "/notice_board": () => , - "/facility/:facilityId/patient/:patientId/consultation/:consultationId": ({ - facilityId, - patientId, - consultationId, - }: any) => ( - - ), - "/facility/:facilityId/patient/:patientId/consultation/:consultationId/treatment-summary": - ({ facilityId, patientId, consultationId }: any) => ( - - ), - "/facility/:facilityId/patient/:patientId/consultation/:consultationId/:tab": - ({ facilityId, patientId, consultationId, tab }: any) => ( - - ), - "/session-expired": () => , - "/not-found": () => , - }; + if (enable_hcx) { + routes = { ...routes, ...HCXRoutes }; + } - useRedirect("/", "/facility"); useRedirect("/user", "/users"); - const pages = useRoutes(routes as any) || ; + const pages = useRoutes(routes) || ; const path = usePath(); const [sidebarOpen, setSidebarOpen] = useState(false); diff --git a/src/Routers/routes/AssetRoutes.tsx b/src/Routers/routes/AssetRoutes.tsx new file mode 100644 index 00000000000..d3bd96ca437 --- /dev/null +++ b/src/Routers/routes/AssetRoutes.tsx @@ -0,0 +1,21 @@ +import AssetConfigure from "../../Components/Assets/AssetConfigure"; +import AssetManage from "../../Components/Assets/AssetManage"; +import AssetsList from "../../Components/Assets/AssetsList"; +import AssetCreate from "../../Components/Facility/AssetCreate"; + +export default { + "/assets": () => , + + "/facility/:facilityId/assets/new": (params: any) => ( + + ), + "/facility/:facilityId/assets/:assetId/update": (params: any) => ( + + ), + "/facility/:facilityId/assets/:assetId": (params: any) => ( + + ), + "/facility/:facilityId/assets/:assetId/configure": (params: any) => ( + + ), +}; diff --git a/src/Routers/routes/ConsultationRoutes.tsx b/src/Routers/routes/ConsultationRoutes.tsx new file mode 100644 index 00000000000..4f1d6e7d75d --- /dev/null +++ b/src/Routers/routes/ConsultationRoutes.tsx @@ -0,0 +1,141 @@ +import { ConsultationForm } from "../../Components/Facility/ConsultationForm"; +import Investigation from "../../Components/Facility/Investigations"; +import ShowInvestigation from "../../Components/Facility/Investigations/ShowInvestigation"; +import ManagePrescriptions from "../../Components/Medicine/ManagePrescriptions"; +import { DailyRoundListDetails } from "../../Components/Patient/DailyRoundListDetails"; +import { DailyRounds } from "../../Components/Patient/DailyRounds"; +import { FileUpload } from "../../Components/Patient/FileUpload"; +import { make as CriticalCareRecording } from "../../Components/CriticalCareRecording/CriticalCareRecording.bs"; +import { ConsultationDetails } from "../../Components/Facility/ConsultationDetails"; +import TreatmentSummary from "../../Components/Facility/TreatmentSummary"; + +export default { + "/facility/:facilityId/patient/:patientId/consultation": ({ + facilityId, + patientId, + }: any) => , + "/facility/:facilityId/patient/:patientId/consultation/:id/update": ({ + facilityId, + patientId, + id, + }: any) => ( + + ), + "/facility/:facilityId/patient/:patientId/consultation/:id/files/": ({ + facilityId, + patientId, + id, + }: any) => ( + + ), + "/facility/:facilityId/patient/:patientId/consultation/:consultationId/prescriptions": + (path: any) => , + "/facility/:facilityId/patient/:patientId/consultation/:id/investigation": ({ + facilityId, + patientId, + id, + }: any) => ( + + ), + "/facility/:facilityId/patient/:patientId/consultation/:id/investigation/:sessionId": + ({ facilityId, patientId, id, sessionId }: any) => ( + + ), + "/facility/:facilityId/patient/:patientId/consultation/:id/daily-rounds": ({ + facilityId, + patientId, + id, + }: any) => ( + + ), + "/facility/:facilityId/patient/:patientId/consultation/:consultationId/daily-rounds/:id/update": + ({ facilityId, patientId, consultationId, id }: any) => ( + + ), + "/facility/:facilityId/patient/:patientId/consultation/:consultationId/daily-rounds/:id": + ({ facilityId, patientId, consultationId, id }: any) => ( + + ), + + "/facility/:facilityId/patient/:patientId/consultation/:consultationId/daily_rounds/:id": + ({ facilityId, patientId, consultationId, id }: any) => ( + + ), + "/facility/:facilityId/patient/:patientId/consultation/:consultationId/daily_rounds/:id/update": + ({ facilityId, patientId, consultationId, id }: any) => ( + + ), + "/facility/:facilityId/patient/:patientId/consultation/:consultationId": ({ + facilityId, + patientId, + consultationId, + }: any) => ( + + ), + "/facility/:facilityId/patient/:patientId/consultation/:consultationId/treatment-summary": + ({ facilityId, patientId, consultationId }: any) => ( + + ), + "/facility/:facilityId/patient/:patientId/consultation/:consultationId/:tab": + ({ facilityId, patientId, consultationId, tab }: any) => ( + + ), +}; diff --git a/src/Routers/routes/ExternalResultRoutes.tsx b/src/Routers/routes/ExternalResultRoutes.tsx new file mode 100644 index 00000000000..af4bf090d78 --- /dev/null +++ b/src/Routers/routes/ExternalResultRoutes.tsx @@ -0,0 +1,14 @@ +import ExternalResultUpload from "../../Components/ExternalResult/ExternalResultUpload"; +import ResultItem from "../../Components/ExternalResult/ResultItem"; +import ResultList from "../../Components/ExternalResult/ResultList"; +import ResultUpdate from "../../Components/ExternalResult/ResultUpdate"; +import { DetailRoute } from "../types"; + +export default { + "/external_results": () => , + "/external_results/upload": () => , + "/external_results/:id": ({ id }: DetailRoute) => , + "/external_results/:id/update": ({ id }: DetailRoute) => ( + + ), +}; diff --git a/src/Routers/routes/FacilityInventoryRoutes.tsx b/src/Routers/routes/FacilityInventoryRoutes.tsx new file mode 100644 index 00000000000..17e93b2bc60 --- /dev/null +++ b/src/Routers/routes/FacilityInventoryRoutes.tsx @@ -0,0 +1,24 @@ +import { Redirect } from "raviger"; +import InventoryList from "../../Components/Facility/InventoryList"; +import InventoryLog from "../../Components/Facility/InventoryLog"; +import MinQuantityList from "../../Components/Facility/MinQuantityList"; +import { SetInventoryForm } from "../../Components/Facility/SetInventoryForm"; + +export default { + "/facility/:facilityId/inventory": ({ facilityId }: any) => ( + + ), + "/facility/:facilityId/inventory/min_quantity/set": ({ facilityId }: any) => ( + + ), + "/facility/:facilityId/inventory/min_quantity/list": ({ + facilityId, + }: any) => , + "/facility/:facilityId/inventory/min_quantity": ({ facilityId }: any) => ( + + ), + "/facility/:facilityId/inventory/:inventoryId": ({ + facilityId, + inventoryId, + }: any) => , +}; diff --git a/src/Routers/routes/FacilityLocationRoutes.tsx b/src/Routers/routes/FacilityLocationRoutes.tsx new file mode 100644 index 00000000000..c43673b60f5 --- /dev/null +++ b/src/Routers/routes/FacilityLocationRoutes.tsx @@ -0,0 +1,38 @@ +import { AddBedForm } from "../../Components/Facility/AddBedForm"; +import { AddInventoryForm } from "../../Components/Facility/AddInventoryForm"; +import { AddLocationForm } from "../../Components/Facility/AddLocationForm"; +import { BedManagement } from "../../Components/Facility/BedManagement"; +import LocationManagement from "../../Components/Facility/LocationManagement"; + +export default { + "/facility/:facilityId/location": ({ facilityId }: any) => ( + + ), + "/facility/:facilityId/location/:locationId/beds": ({ + facilityId, + locationId, + }: any) => , + "/facility/:facilityId/inventory/add": ({ facilityId }: any) => ( + + ), + "/facility/:facilityId/location/add": ({ facilityId }: any) => ( + + ), + "/facility/:facilityId/location/:locationId/update": ({ + facilityId, + locationId, + }: any) => ( + + ), + "/facility/:facilityId/location/:locationId/beds/add": ({ + facilityId, + locationId, + }: any) => , + "/facility/:facilityId/location/:locationId/beds/:bedId/update": ({ + facilityId, + locationId, + bedId, + }: any) => ( + + ), +}; diff --git a/src/Routers/routes/FacilityRoutes.tsx b/src/Routers/routes/FacilityRoutes.tsx new file mode 100644 index 00000000000..77247df9189 --- /dev/null +++ b/src/Routers/routes/FacilityRoutes.tsx @@ -0,0 +1,45 @@ +import { FacilityConfigure } from "../../Components/Facility/FacilityConfigure"; +import { FacilityCreate } from "../../Components/Facility/FacilityCreate"; +import { FacilityHome } from "../../Components/Facility/FacilityHome"; +import FacilityUsers from "../../Components/Facility/FacilityUsers"; +import { HospitalList } from "../../Components/Facility/HospitalList"; +import { TriageForm } from "../../Components/Facility/TriageForm"; +import ResourceCreate from "../../Components/Resource/ResourceCreate"; +import CentralNursingStation from "../../Components/Facility/CentralNursingStation"; +import FacilityLocationRoutes from "./FacilityLocationRoutes"; +import FacilityInventoryRoutes from "./FacilityInventoryRoutes"; + +export default { + "/facility": () => , + "/facility/create": () => , + "/facility/:facilityId/update": ({ facilityId }: any) => ( + + ), + "/facility/:facilityId/configure": ({ facilityId }: any) => ( + + ), + "/facility/:facilityId/cns": ({ facilityId }: any) => ( + + ), + "/facility/:facilityId": ({ facilityId }: any) => ( + + ), + + "/facility/:facilityId/users": ({ facilityId }: any) => ( + + ), + "/facility/:facilityId/resource/new": ({ facilityId }: any) => ( + + ), + + // Triage related routes + "/facility/:facilityId/triage": ({ facilityId }: any) => ( + + ), + "/facility/:facilityId/triage/:id": ({ facilityId, id }: any) => ( + + ), + + ...FacilityLocationRoutes, + ...FacilityInventoryRoutes, +}; diff --git a/src/Routers/routes/HCXRoutes.tsx b/src/Routers/routes/HCXRoutes.tsx new file mode 100644 index 00000000000..8a36e033c15 --- /dev/null +++ b/src/Routers/routes/HCXRoutes.tsx @@ -0,0 +1,6 @@ +import ConsultationClaims from "../../Components/Facility/ConsultationClaims"; + +export default { + "/facility/:facilityId/patient/:patientId/consultation/:consultationId/claims": + (pathParams: any) => , +}; diff --git a/src/Routers/routes/PatientRoutes.tsx b/src/Routers/routes/PatientRoutes.tsx new file mode 100644 index 00000000000..ae594d767ec --- /dev/null +++ b/src/Routers/routes/PatientRoutes.tsx @@ -0,0 +1,46 @@ +import InvestigationReports from "../../Components/Facility/Investigations/Reports"; +import { FileUpload } from "../../Components/Patient/FileUpload"; +import { PatientManager } from "../../Components/Patient/ManagePatients"; +import { PatientHome } from "../../Components/Patient/PatientHome"; +import PatientNotes from "../../Components/Patient/PatientNotes"; +import { PatientRegister } from "../../Components/Patient/PatientRegister"; +import { DetailRoute } from "../types"; +import DeathReport from "../../Components/DeathReport/DeathReport"; + +export default { + "/patients": () => , + "/patient/:id": ({ id }: DetailRoute) => , + "/patient/:id/investigation_reports": ({ id }: DetailRoute) => ( + + ), + + // Facility Scoped Routes + "/facility/:facilityId/patient": ({ facilityId }: any) => ( + + ), + "/facility/:facilityId/patient/:id": ({ facilityId, id }: any) => ( + + ), + "/facility/:facilityId/patient/:id/update": ({ facilityId, id }: any) => ( + + ), + "/facility/:facilityId/patient/:patientId/notes": ({ + facilityId, + patientId, + }: any) => , + "/facility/:facilityId/patient/:patientId/files": ({ + facilityId, + patientId, + }: any) => ( + + ), + "/death_report/:id": ({ id }: any) => , +}; diff --git a/src/Routers/routes/ResourceRoutes.tsx b/src/Routers/routes/ResourceRoutes.tsx new file mode 100644 index 00000000000..8408ab4d79d --- /dev/null +++ b/src/Routers/routes/ResourceRoutes.tsx @@ -0,0 +1,25 @@ +import { DndProvider } from "react-dnd"; +import { HTML5Backend } from "react-dnd-html5-backend"; +import ResourceDetails from "../../Components/Resource/ResourceDetails"; +import { ResourceDetailsUpdate } from "../../Components/Resource/ResourceDetailsUpdate"; +import ListView from "../../Components/Resource/ListView"; +import BoardView from "../../Components/Resource/ResourceBoardView"; +import { Redirect } from "raviger"; +import { DetailRoute } from "../types"; + +const getDefaultView = () => + localStorage.getItem("defaultResourceView") === "list" ? "list" : "board"; + +export default { + "/resource": () => , + "/resource/board": () => ( + + + + ), + "/resource/list": () => , + "/resource/:id": ({ id }: DetailRoute) => , + "/resource/:id/update": ({ id }: DetailRoute) => ( + + ), +}; diff --git a/src/Routers/routes/SampleRoutes.tsx b/src/Routers/routes/SampleRoutes.tsx new file mode 100644 index 00000000000..290a34fd4eb --- /dev/null +++ b/src/Routers/routes/SampleRoutes.tsx @@ -0,0 +1,25 @@ +import { SampleDetails } from "../../Components/Patient/SampleDetails"; +import SampleReport from "../../Components/Patient/SamplePreview"; +import { SampleTest } from "../../Components/Patient/SampleTest"; +import SampleViewAdmin from "../../Components/Patient/SampleViewAdmin"; +import { DetailRoute, RouteParams } from "../types"; + +export default { + "/sample": () => , + "/sample/:id": ({ id }: DetailRoute) => , + "/patient/:patientId/test_sample/:sampleId/icmr_sample": ({ + patientId, + sampleId, + }: RouteParams<"patientId" | "sampleId">) => ( + + ), + "/facility/:facilityId/patient/:patientId/sample-test": ({ + facilityId, + patientId, + }: RouteParams<"facilityId" | "patientId">) => ( + + ), + "/facility/:facilityId/patient/:patientId/sample/:id": ({ + id, + }: DetailRoute) => , +}; diff --git a/src/Routers/routes/ShiftingRoutes.tsx b/src/Routers/routes/ShiftingRoutes.tsx new file mode 100644 index 00000000000..9b20b4a1a0b --- /dev/null +++ b/src/Routers/routes/ShiftingRoutes.tsx @@ -0,0 +1,27 @@ +import { DndProvider } from "react-dnd"; +import { HTML5Backend } from "react-dnd-html5-backend"; +import { ShiftCreate } from "../../Components/Patient/ShiftCreate"; +import ShiftDetails from "../../Components/Shifting/ShiftDetails"; +import { ShiftDetailsUpdate } from "../../Components/Shifting/ShiftDetailsUpdate"; +import ListView from "../../Components/Shifting/ListView"; +import BoardView from "../../Components/Shifting/BoardView"; +import { Redirect } from "raviger"; + +const getDefaultView = () => + localStorage.getItem("defaultShiftView") === "list" ? "list" : "board"; + +export default { + "/shifting": () => , + "/shifting/board": () => ( + + + + ), + "/shifting/list": () => , + "/shifting/:id": ({ id }: any) => , + "/shifting/:id/update": ({ id }: any) => , + "/facility/:facilityId/patient/:patientId/shift/new": ({ + facilityId, + patientId, + }: any) => , +}; diff --git a/src/Routers/routes/UserRoutes.tsx b/src/Routers/routes/UserRoutes.tsx new file mode 100644 index 00000000000..56877ca4c78 --- /dev/null +++ b/src/Routers/routes/UserRoutes.tsx @@ -0,0 +1,9 @@ +import ManageUsers from "../../Components/Users/ManageUsers"; +import { UserAdd } from "../../Components/Users/UserAdd"; +import UserProfile from "../../Components/Users/UserProfile"; + +export default { + "/users": () => , + "/users/add": () => , + "/user/profile": () => , +}; diff --git a/src/Routers/types.ts b/src/Routers/types.ts new file mode 100644 index 00000000000..dc7138b8df7 --- /dev/null +++ b/src/Routers/types.ts @@ -0,0 +1,5 @@ +export type RouteParams = Record; + +export interface DetailRoute { + id: string; +} From e4d2a0f2fc405e611393f34544758ff2c166a1dc Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Wed, 4 Oct 2023 09:56:13 +0530 Subject: [PATCH 06/16] Prescription: show prescribed on & by and discontinued date in detail card (#6365) --- src/CAREUI/display/RecordMeta.tsx | 18 ++++++++++----- .../Medicine/PrescriptionDetailCard.tsx | 22 +++++++++++++++++-- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/CAREUI/display/RecordMeta.tsx b/src/CAREUI/display/RecordMeta.tsx index 83928f1f23a..48cc8d370ad 100644 --- a/src/CAREUI/display/RecordMeta.tsx +++ b/src/CAREUI/display/RecordMeta.tsx @@ -11,13 +11,14 @@ interface Props { last_name: string; last_login: string | undefined; }; + inlineUser?: boolean; } /** * A generic component to display relative time along with a tooltip and a user * if provided. */ -const RecordMeta = ({ time, user, prefix, className }: Props) => { +const RecordMeta = ({ time, user, prefix, className, inlineUser }: Props) => { const isOnline = user && isUserOnline(user); let child = ( @@ -25,14 +26,15 @@ const RecordMeta = ({ time, user, prefix, className }: Props) => { {relativeTime(time)} {formatDateTime(time)} - {user && ( - <> + {user && !inlineUser && ( + + by {user.first_name} {user.last_name} {isOnline && ( -
+
)} - + )}
@@ -43,7 +45,13 @@ const RecordMeta = ({ time, user, prefix, className }: Props) => {
{prefix} {child} + {user && inlineUser && by} {user && } + {user && inlineUser && ( + + {user.first_name} {user.last_name} + + )}
); } diff --git a/src/Components/Medicine/PrescriptionDetailCard.tsx b/src/Components/Medicine/PrescriptionDetailCard.tsx index 6da4fa7ae8d..bf27aa34068 100644 --- a/src/Components/Medicine/PrescriptionDetailCard.tsx +++ b/src/Components/Medicine/PrescriptionDetailCard.tsx @@ -5,6 +5,7 @@ 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, @@ -29,7 +30,7 @@ export default function PrescriptionDetailCard({ prescription.discontinued && "bg-gray-200 opacity-80" )} > -
+
@@ -83,7 +84,7 @@ export default function PrescriptionDetailCard({
-
+
{prescription.medicine_object?.name ?? prescription.medicine_old} @@ -146,6 +147,23 @@ export default function PrescriptionDetailCard({ )}
+ +
+ + Prescribed + + + {prescription.discontinued && ( + + and was discontinued + + + )} +
{props.children} From f8e9647ac60b86b0c42beac7fd37fd60625ca322 Mon Sep 17 00:00:00 2001 From: Gampa Sri Harsh <114745442+sriharsh05@users.noreply.github.com> Date: Wed, 4 Oct 2023 09:58:28 +0530 Subject: [PATCH 07/16] Fixed bug in location picker in update facility page (#6377) * add all cases for map rendering * implement location pointer on searched location --- package-lock.json | 16 ------- src/Components/Common/GLocationPicker.tsx | 55 +++++++++++++---------- 2 files changed, 32 insertions(+), 39 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4871fbe374b..2a9fe3f520f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14496,8 +14496,6 @@ }, "node_modules/npm/node_modules/cross-spawn/node_modules/which": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "inBundle": true, "license": "ISC", @@ -15254,8 +15252,6 @@ }, "node_modules/npm/node_modules/minipass": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", "dev": true, "inBundle": true, "license": "ISC", @@ -16398,8 +16394,6 @@ }, "node_modules/npm/node_modules/tar": { "version": "6.1.15", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.15.tgz", - "integrity": "sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A==", "dev": true, "inBundle": true, "license": "ISC", @@ -16500,8 +16494,6 @@ }, "node_modules/npm/node_modules/util-deprecate": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true, "inBundle": true, "license": "MIT" @@ -16569,8 +16561,6 @@ }, "node_modules/npm/node_modules/wrap-ansi": { "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, "inBundle": true, "license": "MIT", @@ -16589,8 +16579,6 @@ "node_modules/npm/node_modules/wrap-ansi-cjs": { "name": "wrap-ansi", "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "inBundle": true, "license": "MIT", @@ -16670,8 +16658,6 @@ }, "node_modules/npm/node_modules/wrappy": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true, "inBundle": true, "license": "ISC" @@ -16691,8 +16677,6 @@ }, "node_modules/npm/node_modules/yallist": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true, "inBundle": true, "license": "ISC" diff --git a/src/Components/Common/GLocationPicker.tsx b/src/Components/Common/GLocationPicker.tsx index 5356d28fa2a..fc121b8519f 100644 --- a/src/Components/Common/GLocationPicker.tsx +++ b/src/Components/Common/GLocationPicker.tsx @@ -7,14 +7,6 @@ import CareIcon from "../../CAREUI/icons/CareIcon"; import useConfig from "../../Common/hooks/useConfig"; import { Popover } from "@headlessui/react"; -const render = (status: Status) => { - if (status === "LOADING") { - return ; - } - - return

{status}

; -}; - interface GLocationPickerProps { lat: number; lng: number; @@ -67,22 +59,37 @@ const GLocationPicker = ({ setCenter(m?.getCenter()?.toJSON()); }; + const render = (status: Status) => { + switch (status) { + case Status.LOADING: + return ; + case Status.SUCCESS: + return ( + + {location && } + + ); + default: + return

{status}

; + } + }; + return (
- - - {location && } - - +
); }; @@ -149,7 +156,9 @@ const Map: React.FC = ({ places.length > 0 && places[0].geometry?.location ) { - handleOnChange(places[0].geometry.location); + const selectedLocation = places[0].geometry.location; + handleOnChange(selectedLocation); + map?.setCenter(selectedLocation); } }); } From a4a46716d367b8f0554b4ed6237028327120f3b2 Mon Sep 17 00:00:00 2001 From: "Tasnimul H. Tauhid" Date: Wed, 4 Oct 2023 10:02:50 +0530 Subject: [PATCH 08/16] Added cam auto reset in asset config page (#6375) --- .../Facility/Consultations/LiveFeed.tsx | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/Components/Facility/Consultations/LiveFeed.tsx b/src/Components/Facility/Consultations/LiveFeed.tsx index c6ba749b471..aba473e958d 100644 --- a/src/Components/Facility/Consultations/LiveFeed.tsx +++ b/src/Components/Facility/Consultations/LiveFeed.tsx @@ -36,6 +36,7 @@ const LiveFeed = (props: any) => { const [streamStatus, setStreamStatus] = useState( StreamStatus.Offline ); + const [videoStartTime, setVideoStartTime] = useState(null); const [bed, setBed] = useState({}); const [preset, setNewPreset] = useState(""); const [loading, setLoading] = useState(); @@ -100,6 +101,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 = await dispatch( listAssetBeds({ @@ -223,6 +234,7 @@ const LiveFeed = (props: any) => { }, reset: () => { setStreamStatus(StreamStatus.Loading); + setVideoStartTime(null); startStream({ onSuccess: () => setStreamStatus(StreamStatus.Playing), onError: () => setStreamStatus(StreamStatus.Offline), @@ -344,8 +356,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 && (
From c7c37c6e15d988dab5a09580a9eb653fdc7af2c3 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Wed, 4 Oct 2023 10:03:29 +0530 Subject: [PATCH 09/16] =?UTF-8?q?=F0=9F=92=8A=20Adds=20support=20for=20edi?= =?UTF-8?q?ting=20prescriptions=20+=20=20Adds=20`useSlug`=20hook=20(#6369)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Adds hook: `useSlug` * bug fix: NumericWithUnits field not showing intial value * Form: support for showing global errors * Adds support for editing prescriptions (fixes #6340) --- src/Common/hooks/useSlug.ts | 45 +++++ src/Components/Form/Form.tsx | 5 + .../FormFields/NumericWithUnitsFormField.tsx | 1 + src/Components/Form/Utils.ts | 4 +- .../Medicine/CreatePrescriptionForm.tsx | 14 +- .../Medicine/EditPrescriptionForm.tsx | 158 ++++++++++++++++++ .../PrescriptionAdministrationsTable.tsx | 45 +++++ src/Components/Medicine/models.ts | 2 +- src/Components/Medicine/validators.ts | 49 ++++++ src/Locale/en/Medicine.json | 4 +- src/Redux/api.tsx | 5 + 11 files changed, 318 insertions(+), 14 deletions(-) create mode 100644 src/Common/hooks/useSlug.ts create mode 100644 src/Components/Medicine/EditPrescriptionForm.tsx create mode 100644 src/Components/Medicine/validators.ts diff --git a/src/Common/hooks/useSlug.ts b/src/Common/hooks/useSlug.ts new file mode 100644 index 00000000000..69d8f591c84 --- /dev/null +++ b/src/Common/hooks/useSlug.ts @@ -0,0 +1,45 @@ +import { usePath } from "raviger"; + +/** + * Returns the slug from the current path. + * @param prefix The prefix of the slug. + * @returns The slug. + * @example + * // Current path: /consultation/94b9a + * const consultation = useSlug("consultation"); // consultation = "94b9a" + */ +export default function useSlug(prefix: string) { + const path = usePath() ?? ""; + return findSlug(path.split("/"), prefix); +} + +/** + * Returns the slugs from the current path. + * @param prefix The prefixes of the slug. + * @returns The slugs + * @example + * // Current path: /facility/5b0a/consultation/94b9a + * const [facility, consultation] = useSlug("facility", "consultation"); + * // facility = "5b0a" + * // consultation = "94b9a" + */ +export const useSlugs = (...prefix: string[]) => { + const path = usePath() ?? ""; + return prefix.map((p) => findSlug(path.split("/"), p)); +}; + +const findSlug = (segments: string[], prefix: string) => { + const index = segments.findIndex((segment) => segment === prefix); + if (index === -1) { + throw new Error( + `Prefix "${prefix}" not found in path "${segments.join("/")}"` + ); + } + + const slug = segments[index + 1]; + if (!slug) { + throw new Error(`Slug not found in path "${segments.join("/")}"`); + } + + return slug; +}; diff --git a/src/Components/Form/Form.tsx b/src/Components/Form/Form.tsx index 5b1cf018965..e934c4ffe0e 100644 --- a/src/Components/Form/Form.tsx +++ b/src/Components/Form/Form.tsx @@ -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/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/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/CreatePrescriptionForm.tsx b/src/Components/Medicine/CreatePrescriptionForm.tsx index fb7fec5f431..2a58c632d20 100644 --- a/src/Components/Medicine/CreatePrescriptionForm.tsx +++ b/src/Components/Medicine/CreatePrescriptionForm.tsx @@ -1,4 +1,4 @@ -import { FieldError, RequiredFieldValidator } from "../Form/FieldValidators"; +import { RequiredFieldValidator } from "../Form/FieldValidators"; import Form from "../Form/Form"; import { SelectFormField } from "../Form/FormFields/SelectFormField"; import TextAreaFormField from "../Form/FormFields/TextAreaFormField"; @@ -11,6 +11,7 @@ import NumericWithUnitsFormField from "../Form/FormFields/NumericWithUnitsFormFi import { useTranslation } from "react-i18next"; import MedibaseAutocompleteFormField from "./MedibaseAutocompleteFormField"; import dayjs from "../../Utils/dayjs"; +import { PrescriptionFormValidator } from "./validators"; export default function CreatePrescriptionForm(props: { prescription: Prescription; @@ -40,16 +41,7 @@ export default function CreatePrescriptionForm(props: { } }} noPadding - validate={(form) => { - 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; - }} + validate={PrescriptionFormValidator()} className="max-w-3xl" > {(field) => ( diff --git a/src/Components/Medicine/EditPrescriptionForm.tsx b/src/Components/Medicine/EditPrescriptionForm.tsx new file mode 100644 index 00000000000..d5261f70c7e --- /dev/null +++ b/src/Components/Medicine/EditPrescriptionForm.tsx @@ -0,0 +1,158 @@ +import { useState } from "react"; +import Form from "../Form/Form"; +import { Prescription } from "./models"; +import request from "../../Utils/request/request"; +import routes from "../../Redux/api"; +import * as Notification from "../../Utils/Notifications"; +import useSlug from "../../Common/hooks/useSlug"; +import { RequiredFieldValidator } from "../Form/FieldValidators"; +import { useTranslation } from "react-i18next"; +import { SelectFormField } from "../Form/FormFields/SelectFormField"; +import NumericWithUnitsFormField from "../Form/FormFields/NumericWithUnitsFormField"; +import { + PRESCRIPTION_FREQUENCIES, + PRESCRIPTION_ROUTES, +} from "./CreatePrescriptionForm"; +import TextFormField from "../Form/FormFields/TextFormField"; +import TextAreaFormField from "../Form/FormFields/TextAreaFormField"; +import { EditPrescriptionFormValidator } from "./validators"; + +interface Props { + initial: Prescription; + onDone: (created: boolean) => void; +} + +const handleSubmit = async ( + consultation_external_id: string, + oldObj: Prescription, + { discontinued_reason, ...newObj }: Prescription +) => { + const discontinue = await request(routes.discontinuePrescription, { + pathParams: { consultation_external_id, external_id: oldObj.id }, + body: { + discontinued_reason: discontinued_reason + ? `Edit: ${discontinued_reason}` + : "Edited", + }, + }); + + if (discontinue.res?.status !== 200) { + Notification.Error({ + msg: "Failed to discontinue previous prescription", + }); + return; + } + + const { res } = await request(routes.createPrescription, { + pathParams: { consultation_external_id }, + body: { + ...newObj, + // Forcing the medicine to be the same as the old one + medicine: oldObj.medicine_object?.id, + medicine_old: oldObj.medicine_old, + }, + }); + + return res?.status === 201; +}; + +export default function EditPrescriptionForm(props: Props) { + const consultation = useSlug("consultation"); + const [isLoading, setIsLoading] = useState(false); + const { t } = useTranslation(); + + return ( + + disabled={isLoading} + defaults={props.initial} + onCancel={() => props.onDone(false)} + onSubmit={async (obj) => { + setIsLoading(true); + const success = await handleSubmit(consultation, props.initial, obj); + setIsLoading(false); + + if (success) { + props.onDone(true); + } + }} + noPadding + validate={EditPrescriptionFormValidator(props.initial)} + > + {(field) => ( + <> + + +
+ t("PRESCRIPTION_ROUTE_" + key)} + optionValue={(key) => key} + /> + +
+ + {props.initial.is_prn ? ( + <> + + + `${hours} hrs.`} + optionValue={(hours) => hours} + position="above" + /> + + ) : ( +
+ + t("PRESCRIPTION_FREQUENCY_" + key.toUpperCase()) + } + optionValue={([key]) => key} + /> + +
+ )} + + + + )} + + ); +} diff --git a/src/Components/Medicine/PrescriptionAdministrationsTable.tsx b/src/Components/Medicine/PrescriptionAdministrationsTable.tsx index 81282126d7c..6a018f8a4f3 100644 --- a/src/Components/Medicine/PrescriptionAdministrationsTable.tsx +++ b/src/Components/Medicine/PrescriptionAdministrationsTable.tsx @@ -20,6 +20,7 @@ import { formatTime, } from "../../Utils/utils"; import useRangePagination from "../../Common/hooks/useRangePagination"; +import EditPrescriptionForm from "./EditPrescriptionForm"; interface DateRange { start: Date; @@ -254,6 +255,7 @@ const PrescriptionRow = ({ prescription, ...props }: PrescriptionRowProps) => { const { t } = useTranslation(); // 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 [administrations, setAdministrations] = @@ -342,6 +344,21 @@ const PrescriptionRow = ({ prescription, ...props }: PrescriptionRowProps) => { {t("discontinue")} + { + setShowDetails(false); + setShowEdit(true); + }} + > + + {t("edit")} + {
)} + {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)} diff --git a/src/Components/Medicine/models.ts b/src/Components/Medicine/models.ts index 62aea46b6d2..0c49d199b21 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; 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/Locale/en/Medicine.json b/src/Locale/en/Medicine.json index 8cc80234b74..d32015e7618 100644 --- a/src/Locale/en/Medicine.json +++ b/src/Locale/en/Medicine.json @@ -33,7 +33,9 @@ "last_administered": "Last administered", "modification_caution_note": "No modifications possible once added", "discontinue_caution_note": "Are you sure you want to discontinue this prescription?", + "edit_caution_note": "A new prescription will be added to the consultation with the edited details and the current prescription will be discontinued.", "reason_for_discontinuation": "Reason for discontinuation", + "reason_for_edit": "Reason for edit", "PRESCRIPTION_ROUTE_ORAL": "Oral", "PRESCRIPTION_ROUTE_IV": "IV", "PRESCRIPTION_ROUTE_IM": "IM", @@ -47,4 +49,4 @@ "PRESCRIPTION_FREQUENCY_Q4H": "4th hourly", "PRESCRIPTION_FREQUENCY_QOD": "Alternate day", "PRESCRIPTION_FREQUENCY_QWK": "Once a week" -} +} \ No newline at end of file diff --git a/src/Redux/api.tsx b/src/Redux/api.tsx index f26457183dd..9cf9a0b6643 100644 --- a/src/Redux/api.tsx +++ b/src/Redux/api.tsx @@ -1,6 +1,7 @@ import { IConfig } from "../Common/hooks/useConfig"; import { AssetData } from "../Components/Assets/AssetTypes"; import { LocationModel } from "../Components/Facility/models"; +import { Prescription } from "../Components/Medicine/models"; import { UserModel } from "../Components/Users/models"; import { PaginatedResponse } from "../Utils/request/types"; @@ -985,6 +986,8 @@ const routes = { createPrescription: { path: "/api/v1/consultation/{consultation_external_id}/prescriptions/", method: "POST", + TBody: Type(), + TRes: Type(), }, listAdministrations: { @@ -1010,6 +1013,8 @@ const routes = { discontinuePrescription: { path: "/api/v1/consultation/{consultation_external_id}/prescriptions/{external_id}/discontinue/", method: "POST", + TBody: Type<{ discontinued_reason: string }>(), + TRes: Type>(), }, // HCX Endpoints From 4f0246915be891ba3c598d19317c8d1dc55e92e8 Mon Sep 17 00:00:00 2001 From: Suprabath <34211797+suprabathk@users.noreply.github.com> Date: Wed, 4 Oct 2023 18:50:07 +0530 Subject: [PATCH 10/16] Assets date picker (#6338) * Using DateFormField instead of DateInputV2 * Using Label from DateFormField * Prevent Datepicker focus * Removed unused Ref * Using formatDate instead of .format * Added translations * Added translations for AssetServiceEditModal * Added translations for AssetCreate --------- Co-authored-by: Mohammed Nihal <57055998+nihal467@users.noreply.github.com> Co-authored-by: Khavin Shankar --- .../Assets/AssetServiceEditModal.tsx | 60 +++++++++----- src/Components/Common/DateInputV2.tsx | 8 +- src/Components/Facility/AssetCreate.tsx | 81 ++++++++++--------- src/Locale/en/Asset.json | 14 +++- src/Locale/en/Facility.json | 34 +++++++- 5 files changed, 130 insertions(+), 67 deletions(-) diff --git a/src/Components/Assets/AssetServiceEditModal.tsx b/src/Components/Assets/AssetServiceEditModal.tsx index 66d44d11907..690524e471b 100644 --- a/src/Components/Assets/AssetServiceEditModal.tsx +++ b/src/Components/Assets/AssetServiceEditModal.tsx @@ -7,10 +7,10 @@ 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 DateFormField from "../Form/FormFields/DateFormField"; +import { t } from "i18next"; export const AssetServiceEditModal = (props: { asset?: AssetData; @@ -61,12 +61,12 @@ export const AssetServiceEditModal = (props: {

- Update record for asset + {t("update_record_for_asset")} {props.asset?.name}

@@ -110,13 +110,17 @@ export const AssetServiceEditModal = (props: {
-

Edited On

+

+ {t("edited_on")} +

{formatDateTime(editRecord.edited_on)}

-

Edited By

+

+ {t("edited_by")} +

{editRecord.edited_by.username}

@@ -125,7 +129,7 @@ export const AssetServiceEditModal = (props: {

- Serviced On + {t("serviced_on")}

-

Notes

+

+ {t("notes")} +

{editRecord.note || "-"}

@@ -151,7 +157,7 @@ export const AssetServiceEditModal = (props: { editRecord ? setEditRecord(undefined) : props.handleClose(); }} > - {editRecord ? "Back" : "Close"} + {editRecord ? t("back") : t("close")}
@@ -163,12 +169,12 @@ export const AssetServiceEditModal = (props: {

- Update record for asset + {t("update_record_for_asset")} {props.asset?.name}

@@ -178,19 +184,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)} />
@@ -198,8 +216,8 @@ export const AssetServiceEditModal = (props: { { setForm({ ...form, note: e.value }); @@ -210,7 +228,7 @@ export const AssetServiceEditModal = (props: {
diff --git a/src/Components/Common/DateInputV2.tsx b/src/Components/Common/DateInputV2.tsx index bcebd4e0055..7036d5c8bfb 100644 --- a/src/Components/Common/DateInputV2.tsx +++ b/src/Components/Common/DateInputV2.tsx @@ -1,4 +1,4 @@ -import { MutableRefObject, useEffect, useRef, useState } from "react"; +import { MutableRefObject, useEffect, useState } from "react"; import { addMonths, addYears, @@ -60,7 +60,6 @@ const DateInputV2: React.FC = ({ const [displayValue, setDisplayValue] = useState( value ? dayjs(value).format("DDMMYYYY") : "" ); - const popover = useRef(null); const decrement = () => { switch (type) { @@ -241,7 +240,6 @@ const DateInputV2: React.FC = ({ onBlur={() => { setIsOpen?.(false); }} - ref={popover} static className={classNames( "cui-dropdown-base absolute mt-0.5 w-72 divide-y-0 p-4", @@ -252,10 +250,6 @@ const DateInputV2: React.FC = ({ { - popover.current?.focus(); - e.preventDefault(); - }} className="cui-input-base bg-gray-50" value={ displayValue.replace( diff --git a/src/Components/Facility/AssetCreate.tsx b/src/Components/Facility/AssetCreate.tsx index 156d738857a..beafe8d33c8 100644 --- a/src/Components/Facility/AssetCreate.tsx +++ b/src/Components/Facility/AssetCreate.tsx @@ -37,7 +37,8 @@ import useVisibility from "../../Utils/useVisibility"; import { validateEmailAddress } from "../../Common/validation"; 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")); @@ -404,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")}

@@ -440,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")} +

); @@ -479,7 +483,7 @@ const AssetCreate = (props: AssetProps) => { return (
{ > setName(value)} @@ -544,7 +548,7 @@ const AssetCreate = (props: AssetProps) => { {/* Location */} - Asset Location + {t("asset_location")}
{ data-testid="asset-type-input" > { { > setDescription(value)} error={state.errors.description} @@ -664,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 ( @@ -692,8 +696,8 @@ const AssetCreate = (props: AssetProps) => { > setNotWorkingReason(e.value)} error={state.errors.not_working_reason} @@ -717,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} @@ -743,9 +747,9 @@ const AssetCreate = (props: AssetProps) => { setManufacturer(e.value)} error={state.errors.manufacturer} /> @@ -760,7 +764,7 @@ const AssetCreate = (props: AssetProps) => { { const value = dayjs(event.value); @@ -788,8 +792,8 @@ const AssetCreate = (props: AssetProps) => { setSupportName(e.value)} error={state.errors.support_name} @@ -804,7 +808,7 @@ const AssetCreate = (props: AssetProps) => { > setSupportPhone(e.value)} @@ -822,8 +826,8 @@ const AssetCreate = (props: AssetProps) => { setSupportEmail(e.value)} error={state.errors.support_email} @@ -841,9 +845,9 @@ const AssetCreate = (props: AssetProps) => { setVendorName(e.value)} error={state.errors.vendor_name} /> @@ -858,7 +862,7 @@ const AssetCreate = (props: AssetProps) => { setSerialNumber(e.value)} error={state.errors.serial_number} @@ -874,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} @@ -928,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/Locale/en/Asset.json b/src/Locale/en/Asset.json index cf13de5cd47..f24549ee0b6 100644 --- a/src/Locale/en/Asset.json +++ b/src/Locale/en/Asset.json @@ -1,3 +1,15 @@ { - "create_asset": "Create Asset" + "create_asset": "Create Asset", + "edit_history": "Edit History", + "update_record_for_asset": "Update record for asset", + "edited_on": "Edited On", + "edited_by": "Edited By", + "serviced_on": "Serviced On", + "notes": "Notes", + "back": "Back", + "close": "Close", + "update_asset_service_record": "Update Asset Service Record", + "eg_details_on_functionality_service_etc": "Eg. Details on functionality, service, etc.", + "updating": "Updating", + "update": "Update" } diff --git a/src/Locale/en/Facility.json b/src/Locale/en/Facility.json index aa44d86dda5..5e69f8108af 100644 --- a/src/Locale/en/Facility.json +++ b/src/Locale/en/Facility.json @@ -20,5 +20,37 @@ "type_b_cylinders": "B Type Cylinders", "type_c_cylinders": "C Type Cylinders", "type_d_cylinders": "D Type Cylinders", - "select_local_body": "Select Local Body" + "select_local_body": "Select Local Body", + "update_asset": "Update Asset", + "create_new_asset": "Create New Asset", + "you_need_at_least_a_location_to_create_an_assest": "You need at least a location to create an assest.", + "add_location": "Add Location", + "close_scanner": "Close Scanner", + "scan_asset_qr": "Scan Asset QR!", + "update": "Update", + "create": "Create", + "asset_name": "Asset Name", + "asset_location": "Asset Location", + "asset_type": "Asset Type", + "asset_class": "Asset Class", + "description": "Description", + "details_about_the_equipment": "Details about the equipment", + "working_status": "Working Status", + "why_the_asset_is_not_working": "Why the asset is not working?", + "describe_why_the_asset_is_not_working": "Describe why the asset is not working", + "asset_qr_id": "Asset QR ID", + "manufacturer": "Manufacturer", + "eg_xyz": "Eg. XYZ", + "eg_abc": "Eg. ABC", + "warranty_amc_expiry": "Warranty / AMC Expiry", + "customer_support_name": "Customer Support Name", + "customer_support_number": "Customer support number", + "customer_support_email": "Customer Support Email", + "eg_mail_example_com": "Eg. mail@example.com", + "vendor_name": "Vendor Name", + "serial_number": "Serial Number", + "last_serviced_on": "Last Serviced On", + "notes": "Notes", + "create_asset": "Create Asset", + "create_add_more": "Create & Add More" } From 7df0f13ea5f3a22dc8c9c4570f675e09dae1450a Mon Sep 17 00:00:00 2001 From: Pranshu Aggarwal <70687348+Pranshu1902@users.noreply.github.com> Date: Wed, 4 Oct 2023 19:58:46 +0530 Subject: [PATCH 11/16] Add show unread notifications filter button (#6356) * add show unread filter button * refactor Co-authored-by: Khavin Shankar --------- Co-authored-by: Khavin Shankar --- .../Notifications/NotificationsList.tsx | 64 ++++++++++++------- src/Locale/en/Notifications.json | 2 + 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/src/Components/Notifications/NotificationsList.tsx b/src/Components/Notifications/NotificationsList.tsx index f6afa6cccd8..5e3aa65b3ec 100644 --- a/src/Components/Notifications/NotificationsList.tsx +++ b/src/Components/Notifications/NotificationsList.tsx @@ -165,6 +165,7 @@ export default function NotificationsList({ const [isMarkingAllAsRead, setIsMarkingAllAsRead] = useState(false); const [isSubscribed, setIsSubscribed] = useState(""); const [isSubscribing, setIsSubscribing] = useState(false); + const [showUnread, setShowUnread] = useState(false); const { t } = useTranslation(); useEffect(() => { @@ -351,33 +352,37 @@ export default function NotificationsList({ } else if (data?.length) { manageResults = ( <> - {data.map((result: any) => ( - - ))} + {data + .filter((notification: any) => showUnread ? notification.read_at === null : true) + .map((result: any) => ( + + ))} {isLoading && (
)} - {totalCount > RESULT_LIMIT && offset < totalCount - RESULT_LIMIT && ( -
- setOffset((prev) => prev + RESULT_LIMIT)} - > - {isLoading ? t("loading") : t("load_more")} - -
- )} + {!showUnread && + totalCount > RESULT_LIMIT && + offset < totalCount - RESULT_LIMIT && ( +
+ setOffset((prev) => prev + RESULT_LIMIT)} + > + {isLoading ? t("loading") : t("load_more")} + +
+ )} ); } else if (data && data.length === 0) { @@ -448,6 +453,21 @@ export default function NotificationsList({ /> {t("mark_all_as_read")} + setShowUnread(!showUnread)} + > + + + + {showUnread + ? t("show_all_notifications") + : t("show_unread_notifications")} + +
Date: Thu, 5 Oct 2023 12:48:31 +0530 Subject: [PATCH 12/16] fixes #6397; reset is working state on asset create more (#6398) Co-authored-by: Mohammed Nihal <57055998+nihal467@users.noreply.github.com> --- src/Components/Facility/AssetCreate.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/Facility/AssetCreate.tsx b/src/Components/Facility/AssetCreate.tsx index beafe8d33c8..84fc09188d0 100644 --- a/src/Components/Facility/AssetCreate.tsx +++ b/src/Components/Facility/AssetCreate.tsx @@ -306,7 +306,7 @@ const AssetCreate = (props: AssetProps) => { setLocation(""); setAssetType(assetTypeInitial); setAssetClass(assetClassInitial); - setIsWorking(""); + setIsWorking(undefined); setNotWorkingReason(""); setSerialNumber(""); setVendorName(""); From 8473f350b5a03a8c2eeef365d4b1a670ba0ec7f4 Mon Sep 17 00:00:00 2001 From: Ashesh <3626859+Ashesh3@users.noreply.github.com> Date: Thu, 5 Oct 2023 12:49:48 +0530 Subject: [PATCH 13/16] Show asset import progress (#6400) * Show asset import progress * Fix cypress --- cypress/support/commands.ts | 2 +- src/Components/Assets/AssetImportModal.tsx | 68 ++++++++++++++-------- 2 files changed, 44 insertions(+), 26 deletions(-) diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 7f5484564fc..2dd8c477233 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -81,7 +81,7 @@ Cypress.Commands.add( Cypress.Commands.add("verifyNotification", (text) => { cy.get(".pnotify-container").should("exist").contains(text); - return cy.get(".pnotify-container").click({ force: true }); + return cy.get(".pnotify-container").contains(text).click({ force: true }); }); Cypress.on("uncaught:exception", () => { diff --git a/src/Components/Assets/AssetImportModal.tsx b/src/Components/Assets/AssetImportModal.tsx index 7382c7eff4f..54ac25e2ac8 100644 --- a/src/Components/Assets/AssetImportModal.tsx +++ b/src/Components/Assets/AssetImportModal.tsx @@ -8,7 +8,6 @@ import { Cancel, Submit } from "../Common/components/ButtonV2"; import { listFacilityAssetLocation } from "../../Redux/actions"; import { useDispatch } from "react-redux"; import { Link } from "raviger"; -import SelectMenuV2 from "../Form/SelectMenuV2"; import readXlsxFile from "read-excel-file"; import { LocalStorageKeys, @@ -17,6 +16,7 @@ import { import { parseCsvFile } from "../../Utils/utils"; import useConfig from "../../Common/hooks/useConfig"; import DialogModal from "../Common/Dialog"; +import { SelectFormField } from "../Form/FormFields/SelectFormField"; interface Props { open: boolean; @@ -25,14 +25,18 @@ interface Props { } const AssetImportModal = ({ open, onClose, facility }: Props) => { - const [isImporting, setIsUploading] = useState(false); + const [isImporting, setIsImporting] = useState(false); const [selectedFile, setSelectedFile] = useState(); const [preview, setPreview] = useState<(AssetData & { notes?: string; last_serviced_on?: string })[]>(); const [location, setLocation] = useState(""); + const [errors, setErrors] = useState({ + location: "", + }); const [locations, setLocations] = useState([]); const dispatchAction: any = useDispatch(); const { sample_format_asset_import } = useConfig(); + const [locationsLoading, setLocationsLoading] = useState(false); const closeModal = () => { setPreview(undefined); @@ -41,9 +45,11 @@ const AssetImportModal = ({ open, onClose, facility }: Props) => { }; useEffect(() => { + setLocationsLoading(true); dispatchAction( listFacilityAssetLocation({}, { facility_external_id: facility.id }) ).then(({ data }: any) => { + setLocationsLoading(false); if (data.count > 0) { setLocations(data.results); } @@ -110,7 +116,16 @@ const AssetImportModal = ({ open, onClose, facility }: Props) => { closeModal(); return; } + if (!location) { + setErrors({ + ...errors, + location: "Please select a location", + }); + return; + } + setIsImporting(true); let error = false; + Notification.Success({ msg: "Importing assets..." }); for (const asset of preview || []) { const asset_data: any = { @@ -156,16 +171,22 @@ const AssetImportModal = ({ open, onClose, facility }: Props) => { }); error = true; } else { - if (preview) setPreview(preview.filter((a) => a.id !== asset.id)); + setPreview((preview) => { + return preview?.slice(1); + }); } } if (!error) { Notification.Success({ msg: "Assets imported successfully" }); await sleep(1000); - setIsUploading(false); - closeModal(); + setIsImporting(false); window.location.reload(); - } else Notification.Error({ msg: "Error importing some assets" }); + } else { + Notification.Error({ msg: "Error importing some assets" }); + await sleep(1000); + setIsImporting(false); + closeModal(); + } }; const dragProps = useDragAndDrop(); @@ -193,7 +214,7 @@ const AssetImportModal = ({ open, onClose, facility }: Props) => { fixedWidth={false} > {facility.name} - {locations.length === 0 ? ( + {!locationsLoading && locations.length === 0 ? ( <>

@@ -218,31 +239,28 @@ const AssetImportModal = ({ open, onClose, facility }: Props) => { {preview && preview?.length > 0 ? (

- {preview.length} assets will be imported + {preview.length} assets {isImporting ? "are being" : "will be"}{" "} + imported

-
From 64c0b248499382fcd01bc4f5c225b69fb4ce0841 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Thu, 5 Oct 2023 12:52:06 +0530 Subject: [PATCH 14/16] prescription, freeze columns and shrink discontinued (#6389) --- .../PrescriptionAdministrationsTable.tsx | 123 ++++++++++++------ src/Redux/actions.tsx | 2 +- 2 files changed, 83 insertions(+), 42 deletions(-) diff --git a/src/Components/Medicine/PrescriptionAdministrationsTable.tsx b/src/Components/Medicine/PrescriptionAdministrationsTable.tsx index 6a018f8a4f3..c80b66f44c6 100644 --- a/src/Components/Medicine/PrescriptionAdministrationsTable.tsx +++ b/src/Components/Medicine/PrescriptionAdministrationsTable.tsx @@ -48,6 +48,8 @@ export default function PrescriptionAdministrationsTable({ const { t } = useTranslation(); const [state, setState] = useState(); + const [showDiscontinued, setShowDiscontinued] = useState(false); + const [discontinuedCount, setDiscontinuedCount] = useState(); const pagination = useRangePagination({ bounds: state?.administrationsTimeBounds ?? { start: new Date(), @@ -65,8 +67,13 @@ export default function PrescriptionAdministrationsTable({ ); const refetch = useCallback(async () => { + const filters = { + is_prn: prn, + prescription_type: "REGULAR", + }; + const res = await dispatch( - list({ is_prn: prn, prescription_type: "REGULAR" }) + list(showDiscontinued ? filters : { ...filters, discontinued: false }) ); setState({ @@ -75,7 +82,14 @@ export default function PrescriptionAdministrationsTable({ ), administrationsTimeBounds: getAdministrationBounds(res.data.results), }); - }, [consultation_id, dispatch]); + + if (showDiscontinued === false) { + const discontinuedRes = await dispatch( + list({ ...filters, discontinued: true, limit: 0 }) + ); + setDiscontinuedCount(discontinuedRes.data.count); + } + }, [consultation_id, showDiscontinued, dispatch]); useEffect(() => { refetch(); @@ -142,17 +156,22 @@ export default function PrescriptionAdministrationsTable({ } /> -
- +
+
- - -
{t("medicine")} -

Dosage &

-

- {!state?.prescriptions[0]?.is_prn ? "Frequency" : "Indicator"} -

+
+
+ {t("medicine")} + +

Dosage &

+

+ {!state?.prescriptions[0]?.is_prn + ? "Frequency" + : "Indicator"} +

+
+
@@ -165,6 +184,8 @@ export default function PrescriptionAdministrationsTable({ variant="secondary" disabled={!pagination.hasPrevious} onClick={pagination.previous} + tooltip="Previous 24 hours" + tooltipClassName="tooltip-bottom -translate-x-1/2 text-xs" > @@ -206,6 +227,8 @@ export default function PrescriptionAdministrationsTable({ variant="secondary" disabled={!pagination.hasNext} onClick={pagination.next} + tooltip="Next 24 hours" + tooltipClassName="tooltip-bottom -translate-x-1/2 text-xs" > @@ -228,6 +251,23 @@ export default function PrescriptionAdministrationsTable({
+ {showDiscontinued === false && !!discontinuedCount && ( + setShowDiscontinued(true)} + > + + + + Show {discontinuedCount} other discontinued + prescription(s) + + + + )} + {state?.prescriptions.length === 0 && (
@@ -287,8 +327,7 @@ const PrescriptionRow = ({ prescription, ...props }: PrescriptionRowProps) => { return ( {showDiscontinue && ( @@ -402,42 +441,44 @@ const PrescriptionRow = ({ prescription, ...props }: PrescriptionRowProps) => { )} setShowDetails(true)} > -
- +
+ + {prescription.medicine_object?.name ?? prescription.medicine_old} + + + {prescription.discontinued && ( + + {t("discontinued")} + )} - > - {prescription.medicine_object?.name ?? prescription.medicine_old} - - {prescription.discontinued && ( - - {t("discontinued")} - - )} + {prescription.route && ( + + {t(prescription.route)} + + )} +
- {prescription.route && ( - - {t(prescription.route)} - - )} +
+

{prescription.dosage}

+

+ {!prescription.is_prn + ? t("PRESCRIPTION_FREQUENCY_" + prescription.frequency) + : prescription.indicator} +

+
- -

{prescription.dosage}

-

- {!prescription.is_prn - ? t("PRESCRIPTION_FREQUENCY_" + prescription.frequency) - : prescription.indicator} -

- - {/* Administration Cells */} {props.intervals.map(({ start, end }, index) => ( diff --git a/src/Redux/actions.tsx b/src/Redux/actions.tsx index 7d9779e7cd6..26e006f4cea 100644 --- a/src/Redux/actions.tsx +++ b/src/Redux/actions.tsx @@ -1003,7 +1003,7 @@ export const PrescriptionActions = (consultation_external_id: string) => { const pathParams = { consultation_external_id }; return { - list: (query?: Partial) => { + list: (query?: Record) => { let altKey; if (query?.is_prn !== undefined) { altKey = query?.is_prn From 91abee0b8ba7f0922a68999bd39bd0a6a1888879 Mon Sep 17 00:00:00 2001 From: Ashesh <3626859+Ashesh3@users.noreply.github.com> Date: Fri, 6 Oct 2023 16:20:11 +0530 Subject: [PATCH 15/16] Fix password reset (#6404) * Fix password reset * fix response status check * Update ResetPassword.tsx --- src/Components/Auth/Login.tsx | 2 +- src/Components/Auth/ResetPassword.tsx | 4 ++-- src/Redux/api.tsx | 3 +++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Components/Auth/Login.tsx b/src/Components/Auth/Login.tsx index dd6a42d3983..140a0013fd9 100644 --- a/src/Components/Auth/Login.tsx +++ b/src/Components/Auth/Login.tsx @@ -151,7 +151,7 @@ export const Login = (props: { forgot?: boolean }) => { body: { ...valid }, }); setLoading(false); - if (res && res.statusText === "OK") { + if (res?.ok) { Notification.Success({ msg: t("password_sent"), }); diff --git a/src/Components/Auth/ResetPassword.tsx b/src/Components/Auth/ResetPassword.tsx index 2f02737f6de..47d120e1a97 100644 --- a/src/Components/Auth/ResetPassword.tsx +++ b/src/Components/Auth/ResetPassword.tsx @@ -72,7 +72,7 @@ export const ResetPassword = (props: any) => { const { res, error } = await request(routes.resetPassword, { body: { ...valid }, }); - if (res && res.statusText === "OK") { + if (res?.ok) { localStorage.removeItem(LocalStorageKeys.accessToken); Notification.Success({ msg: t("password_reset_success"), @@ -89,7 +89,7 @@ export const ResetPassword = (props: any) => { const { res } = await request(routes.checkResetToken, { body: { token: props.token }, }); - if (!res || res.statusText !== "OK") { + if (!res || !res.ok) { navigate("/invalid-reset"); } }; diff --git a/src/Redux/api.tsx b/src/Redux/api.tsx index 9cf9a0b6643..8a4ca5cf1df 100644 --- a/src/Redux/api.tsx +++ b/src/Redux/api.tsx @@ -55,6 +55,7 @@ const routes = { checkResetToken: { path: "/api/v1/password_reset/check/", method: "POST", + noAuth: true, TRes: Type>(), TBody: Type<{ token: string }>(), }, @@ -62,6 +63,7 @@ const routes = { resetPassword: { path: "/api/v1/password_reset/confirm/", method: "POST", + noAuth: true, TRes: Type>(), TBody: Type<{ password: string; confirm: string }>(), }, @@ -69,6 +71,7 @@ const routes = { forgotPassword: { path: "/api/v1/password_reset/", method: "POST", + noAuth: true, TRes: Type>(), TBody: Type<{ username: string }>(), }, From 9d26e02aee3ae0b012fefb2aad9f796aa8e04e64 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Fri, 6 Oct 2023 17:40:51 +0530 Subject: [PATCH 16/16] Vitals Monitor: Shows relative time for blood pressure and hide if stale (30 mins) (#6407) * shows relative time for bp (part of #6393) * add comments --- .../VitalsMonitor/HL7PatientVitalsMonitor.tsx | 36 ++++++++++++++++--- src/Components/VitalsMonitor/types.ts | 2 +- .../VitalsMonitor/useHL7VitalsMonitor.ts | 3 +- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/Components/VitalsMonitor/HL7PatientVitalsMonitor.tsx b/src/Components/VitalsMonitor/HL7PatientVitalsMonitor.tsx index 1b45fd80ddc..44cadd8263c 100644 --- a/src/Components/VitalsMonitor/HL7PatientVitalsMonitor.tsx +++ b/src/Components/VitalsMonitor/HL7PatientVitalsMonitor.tsx @@ -8,6 +8,15 @@ import { classNames } from "../../Utils/utils"; import { IVitalsComponentProps, VitalsValueBase } from "./types"; import { triggerGoal } from "../../Integrations/Plausible"; import useAuthUser from "../../Common/hooks/useAuthUser"; +import dayjs from "dayjs"; + +const minutesAgo = (timestamp: string) => { + return `${dayjs().diff(dayjs(timestamp), "minute")}m ago`; +}; + +const isWithinMinutes = (timestamp: string, minutes: number) => { + return dayjs().diff(dayjs(timestamp), "minute") < minutes; +}; export default function HL7PatientVitalsMonitor(props: IVitalsComponentProps) { const { connect, waveformCanvas, data, isOnline } = useHL7VitalsMonitor( @@ -30,6 +39,10 @@ export default function HL7PatientVitalsMonitor(props: IVitalsComponentProps) { connect(props.socketUrl); }, [props.socketUrl]); + const bpWithinMaxPersistence = !!( + (data.bp?.["date-time"] && isWithinMinutes(data.bp?.["date-time"], 30)) // Max blood pressure persistence is 30 minutes + ); + return (
{props.patientAssetBed && ( @@ -97,24 +110,37 @@ export default function HL7PatientVitalsMonitor(props: IVitalsComponentProps) { {/* Blood Pressure */}
-
+
NIBP - {data.bp?.systolic.unit ?? "--"} + + {bpWithinMaxPersistence ? data.bp?.systolic.unit ?? "--" : "--"} + + + {data.bp?.["date-time"] && minutesAgo(data.bp?.["date-time"])} +
Sys / Dia
- {data.bp?.systolic.value ?? "--"} + + {bpWithinMaxPersistence + ? data.bp?.systolic.value ?? "--" + : "--"} + / - {data.bp?.diastolic.value ?? "--"} + + {bpWithinMaxPersistence + ? data.bp?.diastolic.value ?? "--" + : "--"} +
Mean - {data.bp?.map.value ?? "--"} + {bpWithinMaxPersistence ? data.bp?.map.value ?? "--" : "--"}
diff --git a/src/Components/VitalsMonitor/types.ts b/src/Components/VitalsMonitor/types.ts index 066b7a7cc78..60979a6f9b0 100644 --- a/src/Components/VitalsMonitor/types.ts +++ b/src/Components/VitalsMonitor/types.ts @@ -8,7 +8,7 @@ export interface VitalsDataBase { "patient-name": string; } -export interface VitalsValueBase { +export interface VitalsValueBase extends VitalsDataBase { value: number; unit: string; interpretation: string; diff --git a/src/Components/VitalsMonitor/useHL7VitalsMonitor.ts b/src/Components/VitalsMonitor/useHL7VitalsMonitor.ts index 8b74a2d05d2..ed16cc2edfd 100644 --- a/src/Components/VitalsMonitor/useHL7VitalsMonitor.ts +++ b/src/Components/VitalsMonitor/useHL7VitalsMonitor.ts @@ -8,11 +8,12 @@ import useCanvas from "../../Common/hooks/useCanvas"; import { ChannelOptions, IVitalsComponentProps, + VitalsDataBase, VitalsValueBase as VitalsValue, } from "./types"; import { getChannel, getVitalsCanvasSizeAndDuration } from "./utils"; -interface VitalsBPValue { +interface VitalsBPValue extends VitalsDataBase { systolic: VitalsValue; diastolic: VitalsValue; map: VitalsValue;