diff --git a/cypress/pageobject/Patient/PatientInvestigation.ts b/cypress/pageobject/Patient/PatientInvestigation.ts index c226c358676..8f73cf908bc 100644 --- a/cypress/pageobject/Patient/PatientInvestigation.ts +++ b/cypress/pageobject/Patient/PatientInvestigation.ts @@ -10,7 +10,7 @@ class PatientInvestigation { } selectInvestigation(investigation: string) { - cy.get("#search-patient-investigation").click(); + cy.get("#search-patient-investigation").type(investigation); cy.verifyAndClickElement("#investigation-group", investigation); cy.verifyAndClickElement("#investigation", "Investigation No. 1"); } diff --git a/src/CAREUI/display/NetworkSignal.tsx b/src/CAREUI/display/NetworkSignal.tsx index 2bcd2744acb..1d5f2d49623 100644 --- a/src/CAREUI/display/NetworkSignal.tsx +++ b/src/CAREUI/display/NetworkSignal.tsx @@ -19,7 +19,7 @@ export default function NetworkSignal({ strength, children }: Props) { return (
)) )} + {!!strength && strength < 2 && ( + + )}
{children} diff --git a/src/CAREUI/display/RecordMeta.tsx b/src/CAREUI/display/RecordMeta.tsx index 662c61fd73f..2363ddbd551 100644 --- a/src/CAREUI/display/RecordMeta.tsx +++ b/src/CAREUI/display/RecordMeta.tsx @@ -1,8 +1,7 @@ import CareIcon from "../icons/CareIcon"; import { - formatDate, + formatDateTime, formatName, - formatTime, isUserOnline, relativeTime, } from "../../Utils/utils"; @@ -39,8 +38,9 @@ const RecordMeta = ({
{relativeTime(time)} - {formatTime(time)}
- {formatDate(time)} + + {formatDateTime(time).replace(";", "")} + {user && !inlineUser && ( by diff --git a/src/CAREUI/interactive/FiltersSlideover.tsx b/src/CAREUI/interactive/FiltersSlideover.tsx index 83f92e2bd90..496f1b3e516 100644 --- a/src/CAREUI/interactive/FiltersSlideover.tsx +++ b/src/CAREUI/interactive/FiltersSlideover.tsx @@ -58,7 +58,7 @@ export const AdvancedFilterButton = ({ onClick }: { onClick: () => void }) => { diff --git a/src/CAREUI/misc/Fullscreen.tsx b/src/CAREUI/misc/Fullscreen.tsx index 5cfa7865128..82c6d9e91ed 100644 --- a/src/CAREUI/misc/Fullscreen.tsx +++ b/src/CAREUI/misc/Fullscreen.tsx @@ -5,17 +5,25 @@ interface Props { fullscreenClassName?: string; children: React.ReactNode; fullscreen: boolean; - onExit: () => void; + onExit: (reason?: "DEVICE_UNSUPPORTED") => void; } export default function Fullscreen(props: Props) { const ref = useRef(null); useEffect(() => { + if (!ref.current) { + return; + } + if (props.fullscreen) { - ref.current?.requestFullscreen(); + if (ref.current.requestFullscreen) { + ref.current.requestFullscreen(); + } else { + props.onExit("DEVICE_UNSUPPORTED"); + } } else { - document.exitFullscreen(); + document.exitFullscreen?.(); } }, [props.fullscreen]); @@ -27,6 +35,7 @@ export default function Fullscreen(props: Props) { }; document.addEventListener("fullscreenchange", listener); + return () => { document.removeEventListener("fullscreenchange", listener); }; diff --git a/src/Common/constants.tsx b/src/Common/constants.tsx index d63c3bebf31..c2ef605f5ae 100644 --- a/src/Common/constants.tsx +++ b/src/Common/constants.tsx @@ -388,10 +388,10 @@ export const SAMPLE_TEST_RESULT = [ export const CONSULTATION_SUGGESTION = [ { id: "HI", text: "Home Isolation", deprecated: true }, // # Deprecated. Preserving option for backward compatibility (use only for readonly operations) { id: "A", text: "Admission" }, - { id: "R", text: "Refer to another Hospital" }, + { id: "R", text: "Refer to another Hospital", editDisabled: true }, { id: "OP", text: "OP Consultation" }, { id: "DC", text: "Domiciliary Care" }, - { id: "DD", text: "Declare Death" }, + { id: "DD", text: "Declare Death", editDisabled: true }, ] as const; export type ConsultationSuggestionValue = diff --git a/src/Components/Assets/AssetsList.tsx b/src/Components/Assets/AssetsList.tsx index 796e532a41e..f6f83e7030f 100644 --- a/src/Components/Assets/AssetsList.tsx +++ b/src/Components/Assets/AssetsList.tsx @@ -105,40 +105,63 @@ const AssetsList = () => { prefetch: !!(qParams.facility && qParams.location), }); - const getAssetIdFromQR = async (assetUrl: string) => { + function isValidURL(url: string) { + try { + new URL(url); + return true; + } catch (_) { + return false; + } + } + + const accessAssetIdFromQR = async (assetURL: string) => { try { setIsLoading(true); setIsScannerActive(false); - const params = parseQueryParams(assetUrl); + if (!isValidURL(assetURL)) { + setIsLoading(false); + Notification.Error({ + msg: "Invalid QR code scanned !!!", + }); + return; + } + const params = parseQueryParams(assetURL); // QR Maybe searchParams "asset" or "assetQR" + // If no params found, then use assetText const assetId = params.asset || params.assetQR; + if (assetId) { - const { data } = await request(routes.listAssets, { - query: { qr_code_id: assetId }, + const { data } = await request(routes.listAssetQR, { + pathParams: { qr_code_id: assetId }, + }); + if (!data) { + setIsLoading(false); + Notification.Error({ + msg: "Invalid QR code scanned !!!", + }); + return; + } + const { data: assetData } = await request(routes.listAssets, { + query: { qr_code_id: assetId, limit: 1 }, + }); + if (assetData?.results.length === 1) { + navigate( + `/facility/${assetData.results[0].location_object.facility?.id}/assets/${assetData.results[0].id}`, + ); + } else { + setIsLoading(false); + Notification.Error({ + msg: "Asset not found !!!", + }); + } + } else { + setIsLoading(false); + Notification.Error({ + msg: "Invalid QR code scanned !!!", }); - return data?.results[0].id; - } - } catch (err) { - console.log(err); - } - }; - - const checkValidAssetId = async (assetId: string) => { - const { data: assetData } = await request(routes.getAsset, { - pathParams: { external_id: assetId }, - }); - try { - if (assetData) { - navigate( - `/facility/${assetData.location_object.facility?.id}/assets/${assetId}`, - ); } } catch (err) { console.log(err); - setIsLoading(false); - Notification.Error({ - msg: "Invalid QR code scanned !!!", - }); } }; @@ -159,8 +182,7 @@ const AssetsList = () => { { if (text) { - const assetId = await getAssetIdFromQR(text); - checkValidAssetId(assetId ?? text); + await accessAssetIdFromQR(text); } }} onError={(e) => { diff --git a/src/Components/Assets/configure/CameraConfigure.tsx b/src/Components/Assets/configure/CameraConfigure.tsx index c3ba434ef3f..5a8ccd5c184 100644 --- a/src/Components/Assets/configure/CameraConfigure.tsx +++ b/src/Components/Assets/configure/CameraConfigure.tsx @@ -7,6 +7,7 @@ import { getCameraConfig } from "../../../Utils/transformUtils"; import { Submit } from "../../Common/components/ButtonV2"; import TextFormField from "../../Form/FormFields/TextFormField"; import Card from "../../../CAREUI/display/Card"; +import { FieldErrorText } from "../../Form/FormFields/FormField"; interface CameraConfigureProps { asset: AssetData; @@ -59,8 +60,14 @@ export default function CameraConfigure(props: CameraConfigureProps) { value={newPreset} className="mt-1" onChange={(e) => setNewPreset(e.value)} - error="" + errorClassName="hidden" /> + {newPreset.length > 12 && ( + + )}
diff --git a/src/Components/CameraFeed/AssetBedSelect.tsx b/src/Components/CameraFeed/AssetBedSelect.tsx index 715c326c35d..f970a920abc 100644 --- a/src/Components/CameraFeed/AssetBedSelect.tsx +++ b/src/Components/CameraFeed/AssetBedSelect.tsx @@ -15,17 +15,17 @@ export default function CameraPresetSelect(props: Props) { const label = props.label ?? defaultLabel; return ( <> -
+
{/* Desktop View */} {props.options .slice(0, props.options.length > 5 ? 4 : 5) .map((option) => ( ))} {props.options.length > 5 && ( - + o.id === props.value?.id)} + /> )}
-
+
{/* Mobile View */} - +
); } -export const CameraPresetDropdown = (props: Props) => { +export const CameraPresetDropdown = ( + props: Props & { placeholder: string }, +) => { const selected = props.value; const options = props.options.filter(({ meta }) => meta.type !== "boundary"); @@ -52,19 +59,29 @@ export const CameraPresetDropdown = (props: Props) => { const label = props.label ?? defaultLabel; return ( - +
- - - {selected ? label(selected) : "Select preset"} + + + {options.length === 0 + ? "No presets" + : selected + ? label(selected) + : props.placeholder} - - + + { leaveFrom="opacity-100" leaveTo="opacity-0" > - + {options?.map((obj) => ( { {({ selected }) => ( <> diff --git a/src/Components/CameraFeed/CameraFeed.tsx b/src/Components/CameraFeed/CameraFeed.tsx index 81b526363b9..1c6781ee51b 100644 --- a/src/Components/CameraFeed/CameraFeed.tsx +++ b/src/Components/CameraFeed/CameraFeed.tsx @@ -4,7 +4,7 @@ import useOperateCamera, { PTZPayload } from "./useOperateCamera"; import usePlayer from "./usePlayer"; import { getStreamUrl } from "./utils"; import ReactPlayer from "react-player"; -import { classNames, isIOS } from "../../Utils/utils"; +import { classNames, isAppleDevice, isIOS } from "../../Utils/utils"; import FeedAlert, { FeedAlertState } from "./FeedAlert"; import FeedNetworkSignal from "./FeedNetworkSignal"; import NoFeedAvailable from "./NoFeedAvailable"; @@ -12,6 +12,7 @@ import FeedControls from "./FeedControls"; import Fullscreen from "../../CAREUI/misc/Fullscreen"; import FeedWatermark from "./FeedWatermark"; import CareIcon from "../../CAREUI/icons/CareIcon"; +import { Error } from "../../Utils/Notifications"; interface Props { children?: React.ReactNode; @@ -27,6 +28,7 @@ interface Props { constrolsDisabled?: boolean; shortcutsDisabled?: boolean; onMove?: () => void; + onReset?: () => void; } export default function CameraFeed(props: Props) { @@ -86,34 +88,51 @@ export default function CameraFeed(props: Props) { const resetStream = () => { setState("loading"); + props.onReset?.(); initializeStream(); }; return ( - setFullscreen(false)}> + { + setFullscreen(false); + + if (reason === "DEVICE_UNSUPPORTED") { + // iOS webkit allows only video/iframe elements to call full-screen + // APIs. But we need to show controls too, not just the video element. + Error({ + msg: "This device does not support viewing this content in full-screen.", + }); + } + }} + >
-
+
{props.children}
- + {props.asset.name} -
- -
+ {!isIOS && ( +
+ +
+ )}
@@ -170,7 +189,7 @@ export default function CameraFeed(props: Props) { ) : (
diff --git a/src/Components/CameraFeed/FeedAlert.tsx b/src/Components/CameraFeed/FeedAlert.tsx index a4f8a3beb18..138af509c8c 100644 --- a/src/Components/CameraFeed/FeedAlert.tsx +++ b/src/Components/CameraFeed/FeedAlert.tsx @@ -15,12 +15,12 @@ interface Props { state?: FeedAlertState; } -const ALERT_ICON_MAP: Record = { +const ALERT_ICON_MAP: Partial> = { playing: "l-play-circle", stop: "l-stop-circle", offline: "l-exclamation-triangle", loading: "l-spinner", - moving: "l-expand-from-corner", + // moving: "l-expand-from-corner", zooming: "l-search", saving_preset: "l-save", host_unreachable: "l-exclamation-triangle", @@ -53,14 +53,14 @@ export default function FeedAlert({ state }: Props) { leaveFrom="opacity-100 translate-y-0" leaveTo="opacity-0 -translate-y-5" > -
- {state && ( +
+ {state && ALERT_ICON_MAP[state] && ( )} diff --git a/src/Components/CameraFeed/FeedWatermark.tsx b/src/Components/CameraFeed/FeedWatermark.tsx index e80c756ba3f..90ca2d15863 100644 --- a/src/Components/CameraFeed/FeedWatermark.tsx +++ b/src/Components/CameraFeed/FeedWatermark.tsx @@ -8,12 +8,12 @@ export default function FeedWatermark() { {me.username} - + {/* {me.username} {me.username} - + */} {me.username} @@ -47,7 +47,7 @@ const Watermark = (props: { children: string; className: string }) => { return ( {props.children} diff --git a/src/Components/Common/SortDropdown.tsx b/src/Components/Common/SortDropdown.tsx index b29662de0a7..8ffd09ba269 100644 --- a/src/Components/Common/SortDropdown.tsx +++ b/src/Components/Common/SortDropdown.tsx @@ -23,8 +23,9 @@ export default function SortDropdownMenu(props: Props) { } + containerClassName="w-full md:w-auto" > {props.options.map(({ isAscending, value }) => ( d?.id === selected?.id); - const { data, loading, refetch } = useQuery(routes.listICD11Diagnosis); + const { res, data, loading, refetch } = useQuery(routes.listICD11Diagnosis, { + silent: true, + }); + + useEffect(() => { + if (res?.status === 500) { + Error({ msg: "ICD-11 Diagnosis functionality is facing issues." }); + } + }, [res?.status]); const handleAdd = async (status: CreateDiagnosis["verification_status"]) => { if (!selected) return; diff --git a/src/Components/Diagnosis/ConsultationDiagnosisBuilder/ConsultationDiagnosisEntry.tsx b/src/Components/Diagnosis/ConsultationDiagnosisBuilder/ConsultationDiagnosisEntry.tsx index c2354503bea..d431890cd17 100644 --- a/src/Components/Diagnosis/ConsultationDiagnosisBuilder/ConsultationDiagnosisEntry.tsx +++ b/src/Components/Diagnosis/ConsultationDiagnosisBuilder/ConsultationDiagnosisEntry.tsx @@ -80,9 +80,11 @@ export default function ConsultationDiagnosisEntry(props: Props) { ? "font-semibold text-primary-500" : "font-normal", !isActive && "text-gray-500 line-through", + !object.diagnosis_object?.label && "italic text-gray-500", )} > - {object.diagnosis_object?.label} + {object.diagnosis_object?.label || + "Unable to retrieve this ICD-11 diagnosis at the moment"}
diff --git a/src/Components/Diagnosis/DiagnosesListAccordion.tsx b/src/Components/Diagnosis/DiagnosesListAccordion.tsx index 0e6d62774c7..ec641d720b5 100644 --- a/src/Components/Diagnosis/DiagnosesListAccordion.tsx +++ b/src/Components/Diagnosis/DiagnosesListAccordion.tsx @@ -4,7 +4,7 @@ import { ConsultationDiagnosis, } from "./types"; import { useTranslation } from "react-i18next"; -import { compareBy } from "../../Utils/utils"; +import { classNames, compareBy } from "../../Utils/utils"; import { useState } from "react"; import CareIcon from "../../CAREUI/icons/CareIcon"; import ButtonV2 from "../Common/components/ButtonV2"; @@ -96,7 +96,14 @@ const DiagnosesOfStatus = ({ diagnoses }: Props) => {
    {diagnoses.map((diagnosis) => (
  • - {diagnosis.diagnosis_object?.label} + + {diagnosis.diagnosis_object?.label || + "Unable to resolve ICD-11 diagnosis at the moment"} +
  • ))}
diff --git a/src/Components/Facility/ConsultationDetails/ConsultationFeedTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationFeedTab.tsx index 40e5ffdc610..91cd86c606e 100644 --- a/src/Components/Facility/ConsultationDetails/ConsultationFeedTab.tsx +++ b/src/Components/Facility/ConsultationDetails/ConsultationFeedTab.tsx @@ -16,7 +16,7 @@ import useOperateCamera, { PTZPayload, } from "../../CameraFeed/useOperateCamera"; import request from "../../../Utils/request/request"; -import { classNames } from "../../../Utils/utils"; +import { classNames, isIOS } from "../../../Utils/utils"; export const ConsultationFeedTab = (props: ConsultationTabProps) => { const authUser = useAuthUser(); @@ -27,6 +27,7 @@ export const ConsultationFeedTab = (props: ConsultationTabProps) => { const [preset, setPreset] = useState(); const [isUpdatingPreset, setIsUpdatingPreset] = useState(false); const [hasMoved, setHasMoved] = useState(false); + const [key, setKey] = useState(0); const divRef = useRef(); const operate = useOperateCamera(asset?.id ?? "", true); @@ -100,11 +101,21 @@ export const ConsultationFeedTab = (props: ConsultationTabProps) => { hideBack={true} focusOnLoad={false} /> + + + For better experience, rotate your device. +
setHasMoved(true)} + onReset={() => { + if (isIOS) { + setKey(key + 1); + } + }} onStreamError={() => { triggerGoal("Camera Feed Viewed", { consultationId: props.consultationId, @@ -161,7 +172,7 @@ export const ConsultationFeedTab = (props: ConsultationTabProps) => { diff --git a/src/Components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx index be2751f0033..7a4080f53dd 100644 --- a/src/Components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx +++ b/src/Components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx @@ -311,59 +311,6 @@ export const ConsultationUpdatesTab = (props: ConsultationTabProps) => {
)} - {((props.patientData.is_antenatal && - isAntenatal(props.patientData.last_menstruation_start_date)) || - isPostPartum(props.patientData.date_of_delivery)) && ( -
-

- Perinatal Status -

- -
- {props.patientData.is_antenatal && - isAntenatal( - props.patientData.last_menstruation_start_date, - ) && ( - - )} - {isPostPartum(props.patientData.date_of_delivery) && ( - - )} -
- - {props.patientData.last_menstruation_start_date && ( -

- - Last Menstruation: - - {formatDate( - props.patientData.last_menstruation_start_date, - )} - -

- )} - - {props.patientData.date_of_delivery && ( -

- - Date of Delivery: - - {formatDate(props.patientData.date_of_delivery)} - -

- )} -
- )}
@@ -637,6 +584,59 @@ export const ConsultationUpdatesTab = (props: ConsultationTabProps) => {
+ {((props.patientData.is_antenatal && + isAntenatal(props.patientData.last_menstruation_start_date)) || + isPostPartum(props.patientData.date_of_delivery)) && ( +
+

+ Perinatal Status +

+ +
+ {props.patientData.is_antenatal && + isAntenatal( + props.patientData.last_menstruation_start_date, + ) && ( + + )} + {isPostPartum(props.patientData.date_of_delivery) && ( + + )} +
+ + {props.patientData.last_menstruation_start_date && ( +

+ + Last Menstruation: + + {formatDate( + props.patientData.last_menstruation_start_date, + )} + +

+ )} + + {props.patientData.date_of_delivery && ( +

+ + Date of Delivery: + + {formatDate(props.patientData.date_of_delivery)} + +

+ )} +
+ )}
diff --git a/src/Components/Facility/ConsultationDetails/index.tsx b/src/Components/Facility/ConsultationDetails/index.tsx index ee1f7bda7ff..e46db70189a 100644 --- a/src/Components/Facility/ConsultationDetails/index.tsx +++ b/src/Components/Facility/ConsultationDetails/index.tsx @@ -37,6 +37,7 @@ import PatientInfoCard from "../../Patient/PatientInfoCard"; import RelativeDateUserMention from "../../Common/RelativeDateUserMention"; import DiagnosesListAccordion from "../../Diagnosis/DiagnosesListAccordion"; import { CameraFeedPermittedUserTypes } from "../../../Utils/permissions"; +import Error404 from "../../ErrorPages/404"; const Loading = lazy(() => import("../../Common/Loading")); const PageTitle = lazy(() => import("../../Common/PageTitle")); @@ -68,7 +69,10 @@ const TABS = { export const ConsultationDetails = (props: any) => { const { facilityId, patientId, consultationId } = props; - const tab = props.tab.toUpperCase() as keyof typeof TABS; + let tab = undefined; + if (Object.keys(TABS).includes(props.tab.toUpperCase())) { + tab = props.tab.toUpperCase() as keyof typeof TABS; + } const dispatch: any = useDispatch(); const [isLoading, setIsLoading] = useState(false); const [showDoctors, setShowDoctors] = useState(false); @@ -194,6 +198,10 @@ export const ConsultationDetails = (props: any) => { patientData, }; + if (!tab) { + return ; + } + const SelectedTab = TABS[tab]; if (isLoading) { diff --git a/src/Components/Facility/ConsultationForm.tsx b/src/Components/Facility/ConsultationForm.tsx index d362abe1ecc..6e1a976de35 100644 --- a/src/Components/Facility/ConsultationForm.tsx +++ b/src/Components/Facility/ConsultationForm.tsx @@ -1148,6 +1148,14 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { options={CONSULTATION_SUGGESTION.filter( (option) => !("deprecated" in option), )} + optionDisabled={(option) => + isUpdate && "editDisabled" in option + } + optionDescription={(option) => + isUpdate && "editDisabled" in option + ? t("encounter_suggestion_edit_disallowed") + : undefined + } />
@@ -1351,7 +1359,11 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { > Procedures { handleFormFieldChange({ name: "procedure", diff --git a/src/Components/Facility/DoctorVideoSlideover.tsx b/src/Components/Facility/DoctorVideoSlideover.tsx index a77a1017b6b..140de6985c1 100644 --- a/src/Components/Facility/DoctorVideoSlideover.tsx +++ b/src/Components/Facility/DoctorVideoSlideover.tsx @@ -172,11 +172,11 @@ function UserListItem({ user }: { user: UserAnnotatedWithGroup }) { function connectOnWhatsApp(e: React.MouseEvent) { e.stopPropagation(); if (!user.alt_phone_number) return; - const phoneNumber = user.alt_phone_number; + const phoneNumber = user.alt_phone_number?.replace(/\D+/g, ""); const message = `${courtesyTitle(user)} ${user.first_name} ${user.last_name}, I have a query regarding a patient.\n\nPatient Link: ${window.location.href}`; const encodedMessage = encodeURIComponent(message); const whatsappAppURL = `whatsapp://send?phone=${phoneNumber}&text=${encodedMessage}`; - const whatsappWebURL = `https://web.whatsapp.com/send?phone=${phoneNumber}&text=${encodedMessage}`; + const whatsappWebURL = `https://wa.me/${phoneNumber}?text=${encodedMessage}`; const userAgent = navigator.userAgent; const isEdge = /edge\/\d+/i.test(userAgent); diff --git a/src/Components/Form/FormFields/Autocomplete.tsx b/src/Components/Form/FormFields/Autocomplete.tsx index e414fda233a..67b1224cddf 100644 --- a/src/Components/Form/FormFields/Autocomplete.tsx +++ b/src/Components/Form/FormFields/Autocomplete.tsx @@ -17,6 +17,7 @@ type AutocompleteFormFieldProps = FormFieldBaseProps & { optionValue?: OptionCallback; optionDescription?: OptionCallback; optionIcon?: OptionCallback; + optionDisabled?: OptionCallback; onQuery?: (query: string) => void; dropdownIcon?: React.ReactNode | undefined; isLoading?: boolean; @@ -43,6 +44,7 @@ const AutocompleteFormField = ( optionIcon={props.optionIcon} optionValue={props.optionValue} optionDescription={props.optionDescription} + optionDisabled={props.optionDisabled} onQuery={props.onQuery} isLoading={props.isLoading} allowRawInput={props.allowRawInput} @@ -65,6 +67,7 @@ type AutocompleteProps = { optionIcon?: OptionCallback; optionValue?: OptionCallback; optionDescription?: OptionCallback; + optionDisabled?: OptionCallback; className?: string; onQuery?: (query: string) => void; requiredError?: boolean; @@ -105,6 +108,7 @@ export const Autocomplete = (props: AutocompleteProps) => { search: label.toLowerCase(), icon: props.optionIcon?.(option), value: props.optionValue ? props.optionValue(option) : option, + disabled: props.optionDisabled?.(option), }; }); @@ -123,6 +127,7 @@ export const Autocomplete = (props: AutocompleteProps) => { search: query.toLowerCase(), icon: , value: query, + disabled: undefined, }, ...mappedOptions, ]; @@ -204,6 +209,7 @@ export const Autocomplete = (props: AutocompleteProps) => { key={`${props.id}-option-${option.label}-value-${index}`} className={dropdownOptionClassNames} value={option} + disabled={option.disabled} > {({ active }) => (
@@ -214,8 +220,12 @@ export const Autocomplete = (props: AutocompleteProps) => { {option.description && (
{option.description} diff --git a/src/Components/Form/FormFields/AutocompleteMultiselect.tsx b/src/Components/Form/FormFields/AutocompleteMultiselect.tsx index 3bdbffdc6cb..7857e96889d 100644 --- a/src/Components/Form/FormFields/AutocompleteMultiselect.tsx +++ b/src/Components/Form/FormFields/AutocompleteMultiselect.tsx @@ -17,6 +17,7 @@ type AutocompleteMultiSelectFormFieldProps = FormFieldBaseProps & { options: T[]; optionLabel: OptionCallback; optionValue?: OptionCallback; + optionDisabled?: OptionCallback; onQuery?: (query: string) => void; dropdownIcon?: React.ReactNode | undefined; isLoading?: boolean; @@ -50,6 +51,7 @@ type AutocompleteMutliSelectProps = { optionDescription?: OptionCallback; optionLabel: OptionCallback; optionValue?: OptionCallback; + optionDisabled?: OptionCallback; className?: string; onChange: OptionCallback; onQuery?: (query: string) => void; @@ -87,6 +89,7 @@ export const AutocompleteMutliSelect = ( description: props.optionDescription?.(option), search: label.toLowerCase(), value: (props.optionValue ? props.optionValue(option) : option) as V, + disabled: props.optionDisabled?.(option), }; }); @@ -187,8 +190,9 @@ export const AutocompleteMutliSelect = ( onClick={() => { handleSingleSelect(option); }} + disabled={option.disabled} > - {({ selected }) => ( + {({ active, selected }) => ( <>
{option.label} @@ -198,9 +202,14 @@ export const AutocompleteMutliSelect = (
{option.description && (

{option.description}

diff --git a/src/Components/Form/FormFields/SelectFormField.tsx b/src/Components/Form/FormFields/SelectFormField.tsx index 42ba82fa5de..47c28777368 100644 --- a/src/Components/Form/FormFields/SelectFormField.tsx +++ b/src/Components/Form/FormFields/SelectFormField.tsx @@ -14,6 +14,7 @@ type SelectFormFieldProps = FormFieldBaseProps & { optionDescription?: OptionCallback; optionIcon?: OptionCallback; optionValue?: OptionCallback; + optionDisabled?: OptionCallback; }; export const SelectFormField = (props: SelectFormFieldProps) => { @@ -34,6 +35,7 @@ export const SelectFormField = (props: SelectFormFieldProps) => { optionDescription={props.optionDescription} optionIcon={props.optionIcon} optionValue={props.optionValue} + optionDisabled={props.optionDisabled} requiredError={field.error ? props.required : false} /> @@ -48,6 +50,7 @@ type MultiSelectFormFieldProps = FormFieldBaseProps & { optionDescription?: OptionCallback; optionIcon?: OptionCallback; optionValue?: OptionCallback; + optionDisabled?: OptionCallback; }; export const MultiSelectFormField = ( @@ -67,6 +70,7 @@ export const MultiSelectFormField = ( optionSelectedLabel={props.optionSelectedLabel} optionDescription={props.optionDescription} optionIcon={props.optionIcon} + optionDisabled={props.optionDisabled} optionValue={props.optionValue} /> diff --git a/src/Components/Form/MultiSelectMenuV2.tsx b/src/Components/Form/MultiSelectMenuV2.tsx index 1568c3f6e84..419664f98e9 100644 --- a/src/Components/Form/MultiSelectMenuV2.tsx +++ b/src/Components/Form/MultiSelectMenuV2.tsx @@ -16,6 +16,7 @@ type Props = { optionDescription?: OptionCallback; optionIcon?: OptionCallback; optionValue?: OptionCallback; + optionDisabled?: OptionCallback; className?: string; disabled?: boolean; renderSelectedOptions?: OptionCallback; @@ -42,9 +43,10 @@ const MultiSelectMenuV2 = (props: Props) => { option, label, selectedLabel, - description: props.optionDescription && props.optionDescription(option), - icon: props.optionIcon && props.optionIcon(option), + description: props.optionDescription?.(option), + icon: props.optionIcon?.(option), value, + disabled: props.optionDisabled?.(option), isSelected: props.value?.includes(value as any) ?? false, displayChip: (
@@ -138,6 +140,7 @@ const MultiSelectMenuV2 = (props: Props) => { className={dropdownOptionClassNames} value={option} onClick={() => handleSingleSelect(option)} + disabled={option.disabled} > {({ active }) => (
@@ -152,9 +155,14 @@ const MultiSelectMenuV2 = (props: Props) => {
{option.description && (

{option.description}

@@ -205,17 +213,20 @@ export const MultiSelectOptionChip = ({ interface OptionRenderPropArg { active: boolean; selected: boolean; + disabled: boolean; } export const dropdownOptionClassNames = ({ active, selected, + disabled, }: OptionRenderPropArg) => { return classNames( "group/option relative w-full cursor-default select-none p-4 text-sm transition-colors duration-75 ease-in-out", - active && "bg-primary-500 text-white", - !active && selected && "text-primary-500", - !active && !selected && "text-gray-900", + !disabled && active && "bg-primary-500 text-white", + !disabled && !active && selected && "text-primary-500", + !disabled && !active && !selected && "text-gray-900", + disabled && "cursor-not-allowed text-gray-800", selected ? "font-semibold" : "font-normal", ); }; diff --git a/src/Components/Form/SelectMenuV2.tsx b/src/Components/Form/SelectMenuV2.tsx index 03e0312c602..4483c92fe4c 100644 --- a/src/Components/Form/SelectMenuV2.tsx +++ b/src/Components/Form/SelectMenuV2.tsx @@ -19,6 +19,7 @@ type SelectMenuProps = { optionDescription?: OptionCallback; optionIcon?: OptionCallback; optionValue?: OptionCallback; + optionDisabled?: OptionCallback; showIconWhenSelected?: boolean; showChevronIcon?: boolean; className?: string; @@ -51,9 +52,10 @@ const SelectMenuV2 = (props: SelectMenuProps) => { selectedLabel: props.optionSelectedLabel ? props.optionSelectedLabel(option) : label, - description: props.optionDescription && props.optionDescription(option), - icon: props.optionIcon && props.optionIcon(option), + description: props.optionDescription?.(option), + icon: props.optionIcon?.(option), value: props.optionValue ? props.optionValue(option) : option, + disabled: props.optionDisabled?.(option), }; }); @@ -67,6 +69,7 @@ const SelectMenuV2 = (props: SelectMenuProps) => { description: undefined, icon: undefined, value: undefined, + disabled: undefined, }; const options = props.required @@ -128,6 +131,7 @@ const SelectMenuV2 = (props: SelectMenuProps) => { key={index} className={dropdownOptionClassNames} value={option} + disabled={option.disabled} > {({ active, selected }) => (
@@ -144,9 +148,14 @@ const SelectMenuV2 = (props: SelectMenuProps) => {
{option.description && (

{option.description}

diff --git a/src/Components/Patient/DailyRounds.tsx b/src/Components/Patient/DailyRounds.tsx index d0b2e321898..35e9828f9d1 100644 --- a/src/Components/Patient/DailyRounds.tsx +++ b/src/Components/Patient/DailyRounds.tsx @@ -502,6 +502,11 @@ export const DailyRounds = (props: any) => {
+
+ Symptoms + +
+ { rows={5} /> -
- Symptoms - -
- {state.form.rounds_type !== "DOCTORS_LOG" && ( <> { {state.form.rounds_type === "DOCTORS_LOG" && ( <>
+
+

+ {t("diagnosis")} +

+ {/* */} + {diagnoses ? ( + + ) : ( +
+ Fetching existing diagnosis of patient... +
+ )} +

{t("investigations")} @@ -701,19 +714,6 @@ export const DailyRounds = (props: any) => {

-
-

- {t("diagnosis")} -

- {/* */} - {diagnoses ? ( - - ) : ( -
- Fetching existing diagnosis of patient... -
- )} -
)} diff --git a/src/Components/Patient/DiagnosesFilter.tsx b/src/Components/Patient/DiagnosesFilter.tsx index e8bd1afb722..c4c4872fdda 100644 --- a/src/Components/Patient/DiagnosesFilter.tsx +++ b/src/Components/Patient/DiagnosesFilter.tsx @@ -7,6 +7,7 @@ import useQuery from "../../Utils/request/useQuery"; import routes from "../../Redux/api"; import { mergeQueryOptions } from "../../Utils/utils"; import { debounce } from "lodash-es"; +import { Error } from "../../Utils/Notifications"; export const FILTER_BY_DIAGNOSES_KEYS = [ "diagnoses", @@ -34,7 +35,15 @@ interface Props { export default function DiagnosesFilter(props: Props) { const { t } = useTranslation(); const [diagnoses, setDiagnoses] = useState([]); - const { data, loading, refetch } = useQuery(routes.listICD11Diagnosis); + const { res, data, loading, refetch } = useQuery(routes.listICD11Diagnosis, { + silent: true, + }); + + useEffect(() => { + if (res?.status === 500) { + Error({ msg: "ICD-11 Diagnosis functionality is facing issues." }); + } + }, [res?.status]); useEffect(() => { if (!props.value) { diff --git a/src/Components/Patient/ManagePatients.tsx b/src/Components/Patient/ManagePatients.tsx index a99d12c09fc..ae227bbbb6b 100644 --- a/src/Components/Patient/ManagePatients.tsx +++ b/src/Components/Patient/ManagePatients.tsx @@ -821,7 +821,7 @@ export const PatientManager = () => { selected={qParams.ordering} onSelect={updateQuery} /> -
+
{!isExportAllowed ? ( { diff --git a/src/Components/Patient/PatientHome.tsx b/src/Components/Patient/PatientHome.tsx index bdf71648e5e..630d0823309 100644 --- a/src/Components/Patient/PatientHome.tsx +++ b/src/Components/Patient/PatientHome.tsx @@ -297,7 +297,7 @@ export const PatientHome = (props: any) => { {patientData?.last_consultation?.assigned_to_object .alt_phone_number && ( @@ -443,7 +443,7 @@ export const PatientHome = (props: any) => {
= ({ fields, onFormUpdate }) => { const [open, setOpen] = useState(false); const [_progress, setProgress] = useState(0); const [stage, setStage] = useState("start"); - const [editableTranscript, setEditableTranscript] = useState(""); + const [_editableTranscript, setEditableTranscript] = useState(""); const [errors, setErrors] = useState([]); const [isTranscribing, setIsTranscribing] = useState(false); const [formFields, setFormFields] = useState<{ @@ -70,6 +70,13 @@ export const Scribe: React.FC = ({ fields, onFormUpdate }) => { const [isAudioUploading, setIsAudioUploading] = useState(false); const [updatedTranscript, setUpdatedTranscript] = useState(""); const [scribeID, setScribeID] = useState(""); + const stageRef = useRef(stage); + + useEffect(() => { + if (stageRef.current === "cancelled") { + setStage("start"); + } + }, [stage]); const { isRecording, @@ -81,6 +88,7 @@ export const Scribe: React.FC = ({ fields, onFormUpdate }) => { const uploadAudioData = (response: any, audioBlob: Blob) => { return new Promise((resolve, reject) => { + if (stageRef.current === "cancelled") resolve(); const url = response.data.signed_url; const internal_name = response.data.internal_name; const f = audioBlob; @@ -109,6 +117,7 @@ export const Scribe: React.FC = ({ fields, onFormUpdate }) => { const uploadAudio = async (audioBlob: Blob, associatingId: string) => { return new Promise((resolve, reject) => { + if (stageRef.current === "cancelled") resolve({}); const category = "AUDIO"; const name = "audio.mp3"; const filename = Date.now().toString(); @@ -127,6 +136,8 @@ export const Scribe: React.FC = ({ fields, onFormUpdate }) => { uploadAudioData(response, audioBlob) .then(() => { if (!response?.data?.id) throw Error("Error uploading audio"); + + if (stageRef.current === "cancelled") resolve({}); markUploadComplete(response?.data.id, associatingId).then(() => { resolve(response.data); }); @@ -145,12 +156,14 @@ export const Scribe: React.FC = ({ fields, onFormUpdate }) => { startRecording(); setProgress(25); setStage("recording"); + stageRef.current = "recording"; }; const handleStopRecordingClick = () => { stopRecording(); setProgress(50); setStage("recording-review"); + stageRef.current = "recording-end"; }; const handleEditChange = (e: { @@ -161,6 +174,7 @@ export const Scribe: React.FC = ({ fields, onFormUpdate }) => { const waitForTranscript = async (externalId: string): Promise => { return new Promise((resolve, reject) => { + if (stageRef.current === "cancelled") resolve(""); const interval = setInterval(async () => { try { const res = await request(routes.getScribe, { @@ -191,9 +205,29 @@ export const Scribe: React.FC = ({ fields, onFormUpdate }) => { }); }; + const reProcessTranscript = async () => { + setErrors([]); + setEditableTranscript(updatedTranscript); + setStage("review"); + const res = await request(routes.updateScribe, { + body: { + status: "READY", + transcript: updatedTranscript, + ai_response: null, + }, + pathParams: { + external_id: scribeID, + }, + }); + if (res.error || !res.data) throw Error("Error updating scribe instance"); + setStage("review"); + await handleSubmitTranscript(res.data.external_id); + }; + const waitForFormData = async (externalId: string): Promise => { return new Promise((resolve, reject) => { const interval = setInterval(async () => { + if (stageRef.current === "cancelled") resolve(""); try { const res = await request(routes.getScribe, { pathParams: { @@ -270,12 +304,14 @@ export const Scribe: React.FC = ({ fields, onFormUpdate }) => { }; const handleSubmitTranscript = async (external_id: string) => { + if (stageRef.current === "cancelled") return; setProgress(75); setIsGPTProcessing(true); try { const updatedFieldsResponse = await waitForFormData(external_id); setProgress(100); const parsedFormData = JSON.parse(updatedFieldsResponse ?? "{}"); + if (stageRef.current === "cancelled") return; setFormFields(parsedFormData); onFormUpdate(parsedFormData); setStage("final-review"); @@ -286,6 +322,7 @@ export const Scribe: React.FC = ({ fields, onFormUpdate }) => { }; const startTranscription = async (scribeID: string) => { + if (stageRef.current === "cancelled") return; setIsTranscribing(true); const errors = []; try { @@ -302,6 +339,7 @@ export const Scribe: React.FC = ({ fields, onFormUpdate }) => { //poll for transcript const transcriptResponse = await waitForTranscript(res.data.external_id); + if (stageRef.current === "cancelled") return; setEditableTranscript(transcriptResponse); setUpdatedTranscript(transcriptResponse); setStage("review"); @@ -329,6 +367,10 @@ export const Scribe: React.FC = ({ fields, onFormUpdate }) => { setFormFields({}); setEditableTranscript(""); setUpdatedTranscript(""); + setIsAudioUploading(false); + setScribeID(""); + setIsHoveringCancelRecord(false); + stageRef.current = "cancelled"; }; const processFormField = ( @@ -485,44 +527,47 @@ export const Scribe: React.FC = ({ fields, onFormUpdate }) => { src={URL.createObjectURL(blob)} /> ))} + { + handleRerecordClick(); + }} + className="w-full" + > + Restart Recording +
)} {(stage === "review" || stage === "final-review") && (
-

- Transcript -

+
+

+ Transcript +

+

+ (Edit if needed) +

+