From 778351dee911909631c3283be75d5d3c60dbc204 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Thu, 30 May 2024 15:18:46 +0530 Subject: [PATCH 01/14] Disable switching to "Refer to another facility"/"Declare Death" in Decision after consultation in edit consultation form (#7940) * Adds support for disabling individual options in dropdown menu * Disable Refer to another hospital and declare death in edit consultation form * correct colors disabled option description * add to i18n --- src/Common/constants.tsx | 4 +-- src/Components/Facility/ConsultationForm.tsx | 8 ++++++ .../Form/FormFields/Autocomplete.tsx | 14 ++++++++-- .../FormFields/AutocompleteMultiselect.tsx | 17 +++++++++--- .../Form/FormFields/SelectFormField.tsx | 4 +++ src/Components/Form/MultiSelectMenuV2.tsx | 27 +++++++++++++------ src/Components/Form/SelectMenuV2.tsx | 19 +++++++++---- src/Locale/en/Consultation.json | 3 ++- 8 files changed, 74 insertions(+), 22 deletions(-) diff --git a/src/Common/constants.tsx b/src/Common/constants.tsx index 4eb3b51d012..f0d0ab08f04 100644 --- a/src/Common/constants.tsx +++ b/src/Common/constants.tsx @@ -363,10 +363,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/Facility/ConsultationForm.tsx b/src/Components/Facility/ConsultationForm.tsx index d362abe1ecc..a5e333e43c6 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 + } /> 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/Locale/en/Consultation.json b/src/Locale/en/Consultation.json index 42f31433495..fc1b5ae5f7d 100644 --- a/src/Locale/en/Consultation.json +++ b/src/Locale/en/Consultation.json @@ -34,5 +34,6 @@ "generate_report": "Generate Report", "prev_sessions": "Prev Sessions", "next_sessions": "Next Sessions", - "no_changes": "No changes" + "no_changes": "No changes", + "encounter_suggestion_edit_disallowed": "Not allowed to switch to this option in edit consultation" } \ No newline at end of file From da03963c6a1fc1217a1209119950eadfd465666e Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Thu, 30 May 2024 15:19:35 +0530 Subject: [PATCH 02/14] Camera Feed: Improve watermark and preset sizes in mobile view (#7935) * Camera Feed: Improve watermark and preset sizes in mobile view * adds notification and correct sizes * decrease watermark opacity to 20 * fixed width for preset sizes * add warning for char. limit of preset name * add banding for mobile landscape view to maintain aspect ratio and fit feed content * min width for preset buttons * improve responsiveness --- src/CAREUI/display/NetworkSignal.tsx | 8 +++- .../Assets/configure/CameraConfigure.tsx | 9 ++++- src/Components/CameraFeed/AssetBedSelect.tsx | 38 +++++++++++-------- src/Components/CameraFeed/CameraFeed.tsx | 8 ++-- src/Components/CameraFeed/FeedWatermark.tsx | 6 +-- .../ConsultationFeedTab.tsx | 6 ++- 6 files changed, 49 insertions(+), 26 deletions(-) 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/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..56def3d41c9 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 */}
@@ -54,17 +58,19 @@ export const CameraPresetDropdown = (props: Props) => { return (
- - + + {selected ? label(selected) : "Select preset"} - - + + { 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..a5f0a3d80e0 100644 --- a/src/Components/CameraFeed/CameraFeed.tsx +++ b/src/Components/CameraFeed/CameraFeed.tsx @@ -96,13 +96,13 @@ export default function CameraFeed(props: Props) { props.className, )} > -
+
{props.children}
- + {props.asset.name} @@ -170,7 +170,7 @@ export default function CameraFeed(props: Props) { ) : (
+ {((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)} + +

+ )} +
+ )}
From fa5e3e6903c008174a213366ccb0ab50eafe3854 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Fri, 31 May 2024 18:09:44 +0530 Subject: [PATCH 05/14] Reorder fields in Daily Rounds (#7954) * move symptoms up * move diagnosis up --- src/Components/Patient/DailyRounds.tsx | 36 +++++++++++++------------- 1 file changed, 18 insertions(+), 18 deletions(-) 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... -
- )} -
)} From cef2795ed3d1f1771e052d0343820e5036bd3315 Mon Sep 17 00:00:00 2001 From: Shivank Kacker Date: Mon, 3 Jun 2024 03:58:34 +0530 Subject: [PATCH 06/14] fix button widths --- src/CAREUI/interactive/FiltersSlideover.tsx | 2 +- src/Components/Common/SortDropdown.tsx | 3 ++- src/Components/Patient/ManagePatients.tsx | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) 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/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 }) => ( { selected={qParams.ordering} onSelect={updateQuery} /> -
+
{!isExportAllowed ? ( { From 6e4114bf376bda1821e2a18bf4ee3804110f1351 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 3 Jun 2024 18:29:05 +0530 Subject: [PATCH 07/14] clean special characters in phone number in whatsapp link (#7964) --- src/Components/Facility/DoctorVideoSlideover.tsx | 4 ++-- src/Components/Patient/PatientHome.tsx | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) 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/Patient/PatientHome.tsx b/src/Components/Patient/PatientHome.tsx index e92d043dff4..53a6a8215aa 100644 --- a/src/Components/Patient/PatientHome.tsx +++ b/src/Components/Patient/PatientHome.tsx @@ -287,7 +287,7 @@ export const PatientHome = (props: any) => { {patientData?.last_consultation?.assigned_to_object .alt_phone_number && ( @@ -433,7 +433,7 @@ export const PatientHome = (props: any) => {
Date: Mon, 3 Jun 2024 21:27:08 +0530 Subject: [PATCH 08/14] Camera Feed Fixes: Gracefully handle full-screen errors for unsupported devices; fixes clipping of content in certain sizes in landscape mode (#7965) * fix screen height of camera feed * change label to more preset * fix fullscreen issues with iOS devices * fix presets placeholder and disable if no options * fixes feed alert z index * corrections for feed alert * hacks for iOS * fix missing placeholder for preset dropdown in live feed --- src/CAREUI/misc/Fullscreen.tsx | 15 +++++-- src/Components/CameraFeed/AssetBedSelect.tsx | 19 +++++++-- src/Components/CameraFeed/CameraFeed.tsx | 41 ++++++++++++++----- .../CameraFeed/CameraFeedWithBedPresets.tsx | 1 + src/Components/CameraFeed/FeedAlert.tsx | 10 ++--- .../ConsultationFeedTab.tsx | 9 +++- 6 files changed, 71 insertions(+), 24 deletions(-) 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/Components/CameraFeed/AssetBedSelect.tsx b/src/Components/CameraFeed/AssetBedSelect.tsx index 56def3d41c9..f970a920abc 100644 --- a/src/Components/CameraFeed/AssetBedSelect.tsx +++ b/src/Components/CameraFeed/AssetBedSelect.tsx @@ -35,6 +35,7 @@ export default function CameraPresetSelect(props: Props) { {props.options.length > 5 && ( o.id === props.value?.id)} /> @@ -42,13 +43,15 @@ export default function CameraPresetSelect(props: Props) {
{/* 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"); @@ -56,7 +59,11 @@ 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} diff --git a/src/Components/CameraFeed/CameraFeed.tsx b/src/Components/CameraFeed/CameraFeed.tsx index a5f0a3d80e0..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,14 +88,29 @@ 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.", + }); + } + }} + >
@@ -106,14 +123,16 @@ export default function CameraFeed(props: Props) { /> {props.asset.name} -
- -
+ {!isIOS && ( +
+ +
+ )}
diff --git a/src/Components/CameraFeed/CameraFeedWithBedPresets.tsx b/src/Components/CameraFeed/CameraFeedWithBedPresets.tsx index e3fc2ab2129..8ce9c9ef67f 100644 --- a/src/Components/CameraFeed/CameraFeedWithBedPresets.tsx +++ b/src/Components/CameraFeed/CameraFeedWithBedPresets.tsx @@ -33,6 +33,7 @@ export default function LocationFeedTile(props: Props) { options={data?.results ?? []} value={preset} onChange={setPreset} + placeholder="Select preset" /> )}
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/Facility/ConsultationDetails/ConsultationFeedTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationFeedTab.tsx index 3d12441eb10..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); @@ -106,9 +107,15 @@ export const ConsultationFeedTab = (props: ConsultationTabProps) => {
setHasMoved(true)} + onReset={() => { + if (isIOS) { + setKey(key + 1); + } + }} onStreamError={() => { triggerGoal("Camera Feed Viewed", { consultationId: props.consultationId, From 339e2a63c68f7fd4546457c49f925ee9fcc47f73 Mon Sep 17 00:00:00 2001 From: Ashesh <3626859+Ashesh3@users.noreply.github.com> Date: Tue, 4 Jun 2024 17:33:55 +0000 Subject: [PATCH 09/14] Support retries in Scribe --- src/Components/Scribe/Scribe.tsx | 115 ++++++++++++++++++++++--------- 1 file changed, 84 insertions(+), 31 deletions(-) diff --git a/src/Components/Scribe/Scribe.tsx b/src/Components/Scribe/Scribe.tsx index 6e876a4762c..b5149267e24 100644 --- a/src/Components/Scribe/Scribe.tsx +++ b/src/Components/Scribe/Scribe.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useRef } from "react"; import { Popover } from "@headlessui/react"; import ButtonV2 from "../Common/components/ButtonV2"; import CareIcon from "../../CAREUI/icons/CareIcon"; @@ -59,7 +59,7 @@ export const Scribe: React.FC = ({ 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) +

+