From 2c3783308e88510700c61aa7d3089384b7492c18 Mon Sep 17 00:00:00 2001 From: NitinPSingh <71833171+NitinPSingh@users.noreply.github.com> Date: Wed, 24 Jul 2024 13:57:00 +0530 Subject: [PATCH 1/5] Added primary color to selected section on consultation page fix #8186 (#8188) --- src/Components/Facility/ConsultationDetails/index.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Components/Facility/ConsultationDetails/index.tsx b/src/Components/Facility/ConsultationDetails/index.tsx index 0b0d2c01a50..62d3232729a 100644 --- a/src/Components/Facility/ConsultationDetails/index.tsx +++ b/src/Components/Facility/ConsultationDetails/index.tsx @@ -205,8 +205,10 @@ export const ConsultationDetails = (props: any) => { } const tabButtonClasses = (selected: boolean) => - `capitalize min-w-max-content cursor-pointer border-transparent text-secondary-700 hover:text-secondary-700 hover:border-secondary-300 font-bold whitespace-nowrap ${ - selected === true ? "border-primary-500 text-primary-600 border-b-2" : "" + `capitalize min-w-max-content cursor-pointer font-bold whitespace-nowrap ${ + selected === true + ? "border-primary-500 hover:border-secondary-300 text-primary-600 border-b-2" + : "text-secondary-700 hover:text-secondary-700" }`; return ( From abd95d86ffaf07ab970462b536163c2d7c3c8a5d Mon Sep 17 00:00:00 2001 From: NitinPSingh <71833171+NitinPSingh@users.noreply.github.com> Date: Wed, 24 Jul 2024 13:57:13 +0530 Subject: [PATCH 2/5] fix #8164 removed View Discharge Patients btn (#8184) --- src/Components/Facility/FacilityHome.tsx | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/Components/Facility/FacilityHome.tsx b/src/Components/Facility/FacilityHome.tsx index f53fa43efa8..0a885c0b6e3 100644 --- a/src/Components/Facility/FacilityHome.tsx +++ b/src/Components/Facility/FacilityHome.tsx @@ -445,19 +445,6 @@ export const FacilityHome = (props: any) => { View Patients - - navigate(`/facility/${facilityId}/discharged-patients`) - } - > - - View Discharged Patients - From d1a2dd79f2f1e497bdcd4ea9ace37c168fb469c6 Mon Sep 17 00:00:00 2001 From: Shivank Kacker Date: Wed, 24 Jul 2024 19:04:01 +0530 Subject: [PATCH 3/5] Added a last online filter for users (#8175) --- src/Common/constants.tsx | 9 +++++++++ src/Components/Users/ManageUsers.tsx | 8 ++++++++ src/Components/Users/UserFilter.tsx | 23 ++++++++++++++++++++++- 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/Common/constants.tsx b/src/Common/constants.tsx index 35790e77b16..4a490f34e78 100644 --- a/src/Common/constants.tsx +++ b/src/Common/constants.tsx @@ -46,6 +46,15 @@ export const USER_TYPE_OPTIONS = [ { id: "StateAdmin", role: "State Admin", readOnly: false }, ] as const; +export const USER_LAST_ACTIVE_OPTIONS = [ + { id: 1, text: "24 hours" }, + { id: 7, text: "7 days" }, + { id: 30, text: "30 days" }, + { id: 90, text: "90 days" }, + { id: 365, text: "1 Year" }, + { id: "never", text: "Never" }, +]; + export type UserRole = (typeof USER_TYPE_OPTIONS)[number]["id"]; export const USER_TYPES = USER_TYPE_OPTIONS.map((o) => o.id); diff --git a/src/Components/Users/ManageUsers.tsx b/src/Components/Users/ManageUsers.tsx index 446b210aa9d..9660f13c269 100644 --- a/src/Components/Users/ManageUsers.tsx +++ b/src/Components/Users/ManageUsers.tsx @@ -94,6 +94,7 @@ export default function ManageUsers() { user_type: qParams.user_type, district_id: qParams.district, home_facility: qParams.home_facility, + last_active_days: qParams.last_active_days, }, }); @@ -557,6 +558,13 @@ export default function ManageUsers() { "home_facility", qParams.home_facility ? homeFacilityData?.name || "" : "", ), + value( + "Last Active", + "last_active_days", + qParams.last_active_days === "never" + ? "Never" + : `in the last ${qParams.last_active_days} day${qParams.last_active_days > 1 ? "s" : ""}`, + ), ]} /> diff --git a/src/Components/Users/UserFilter.tsx b/src/Components/Users/UserFilter.tsx index d5c16d22571..f79968cef4f 100644 --- a/src/Components/Users/UserFilter.tsx +++ b/src/Components/Users/UserFilter.tsx @@ -2,7 +2,10 @@ import { parsePhoneNumber } from "../../Utils/utils"; import TextFormField from "../Form/FormFields/TextFormField"; import SelectMenuV2 from "../Form/SelectMenuV2"; import { FieldLabel } from "../Form/FormFields/FormField"; -import { USER_TYPE_OPTIONS } from "../../Common/constants"; +import { + USER_LAST_ACTIVE_OPTIONS, + USER_TYPE_OPTIONS, +} from "../../Common/constants"; import useMergeState from "../../Common/hooks/useMergeState"; import PhoneNumberFormField from "../Form/FormFields/PhoneNumberFormField"; import FiltersSlideover from "../../CAREUI/interactive/FiltersSlideover"; @@ -35,6 +38,7 @@ export default function UserFilter(props: any) { state: filter.state || "", home_facility: filter.home_facility || "", home_facility_ref: null, + last_active_days: filter.last_active_days || "", }); useQuery(routes.getAnyFacility, { @@ -53,6 +57,7 @@ export default function UserFilter(props: any) { district, state, home_facility, + last_active_days, } = filterState; const data = { first_name: first_name || "", @@ -63,6 +68,7 @@ export default function UserFilter(props: any) { district: district || "", state: district ? state || "" : "", home_facility: home_facility || "", + last_active_days: last_active_days || "", }; if (state && !district) { Notify.Warn({ @@ -142,6 +148,21 @@ export default function UserFilter(props: any) { /> +
+ Active in last... + o.text} + optionValue={(o) => o.id} + value={filterState.last_active_days} + onChange={(v) => + setFilterState({ ...filterState, last_active_days: v }) + } + /> +
+ Date: Wed, 24 Jul 2024 19:04:41 +0530 Subject: [PATCH 4/5] Add popup to acknowledge discharge (#8211) --- .../e2e/patient_spec/patient_discharge.cy.ts | 4 + src/Common/hooks/useConfirmedAction.ts | 17 + src/Components/Facility/DischargeModal.tsx | 442 +++++++++--------- .../Facility/DischargedPatientsList.tsx | 2 +- src/Locale/en/Common.json | 3 +- 5 files changed, 258 insertions(+), 210 deletions(-) create mode 100644 src/Common/hooks/useConfirmedAction.ts diff --git a/cypress/e2e/patient_spec/patient_discharge.cy.ts b/cypress/e2e/patient_spec/patient_discharge.cy.ts index 34ad423d1e8..242c936730d 100644 --- a/cypress/e2e/patient_spec/patient_discharge.cy.ts +++ b/cypress/e2e/patient_spec/patient_discharge.cy.ts @@ -36,6 +36,7 @@ describe("Patient Discharge based on multiple reason", () => { patientDischarge.clickDischarge(); patientDischarge.selectDischargeReason(patientDischargeReason4); cy.submitButton("Confirm Discharge"); + cy.submitButton("Acknowledge & Submit"); cy.verifyNotification("Patient Discharged Successfully"); cy.closeNotification(); // Verify the consultation dashboard reflection @@ -53,6 +54,7 @@ describe("Patient Discharge based on multiple reason", () => { patientDischarge.typeDischargeNote(patientDeathCause); patientDischarge.typeDoctorName(doctorName); cy.submitButton("Confirm Discharge"); + cy.submitButton("Acknowledge & Submit"); cy.verifyNotification("Patient Discharged Successfully"); cy.closeNotification(); // Verify the consultation dashboard reflection @@ -77,6 +79,7 @@ describe("Patient Discharge based on multiple reason", () => { patientDischarge.typeReferringFacility(referringFreetextFacility); cy.wait(2000); cy.submitButton("Confirm Discharge"); + cy.submitButton("Acknowledge & Submit"); cy.wait(2000); cy.verifyNotification("Patient Discharged Successfully"); cy.closeNotification(); @@ -108,6 +111,7 @@ describe("Patient Discharge based on multiple reason", () => { cy.closeNotification(); // submit the discharge pop-up cy.submitButton("Confirm Discharge"); + cy.submitButton("Acknowledge & Submit"); cy.wait(2000); cy.verifyNotification("Patient Discharged Successfully"); cy.closeNotification(); diff --git a/src/Common/hooks/useConfirmedAction.ts b/src/Common/hooks/useConfirmedAction.ts new file mode 100644 index 00000000000..ca88a5014ed --- /dev/null +++ b/src/Common/hooks/useConfirmedAction.ts @@ -0,0 +1,17 @@ +import { useState } from "react"; + +export default function useConfirmedAction(action: () => Promise) { + const [showConfirmation, setShowConfirmation] = useState(false); + + return { + requestConfirmation: () => setShowConfirmation(true), + submit: action, + + confirmationProps: { + onClose: () => setShowConfirmation(false), + show: showConfirmation, + onConfirm: action, + action: "Submit", + }, + }; +} diff --git a/src/Components/Facility/DischargeModal.tsx b/src/Components/Facility/DischargeModal.tsx index 2311ffd7bc4..567c21dfbb6 100644 --- a/src/Components/Facility/DischargeModal.tsx +++ b/src/Components/Facility/DischargeModal.tsx @@ -26,6 +26,8 @@ import { FacilityModel } from "./models"; import dayjs from "../../Utils/dayjs"; import { FieldError } from "../Form/FieldValidators"; import { useTranslation } from "react-i18next"; +import useConfirmedAction from "../../Common/hooks/useConfirmedAction"; +import ConfirmDialog from "../Common/ConfirmDialog"; interface PreDischargeFormInterface { new_discharge_reason: number | null; @@ -60,6 +62,7 @@ const DischargeModal = ({ }: IProps) => { const { t } = useTranslation(); const { enable_hcx } = useConfig(); + const dispatch: any = useDispatch(); const [preDischargeForm, setPreDischargeForm] = useState({ @@ -130,14 +133,12 @@ const DischargeModal = ({ } }); - const handlePatientDischarge = async (value: boolean) => { - setIsSendingDischargeApi(true); + const validate = () => { if (!new_discharge_reason && !discharge_reason) { setErrors({ ...errors, new_discharge_reason: "Please select a reason for discharge", }); - setIsSendingDischargeApi(false); return; } @@ -155,45 +156,32 @@ const DischargeModal = ({ if (Object.entries(newErrors).length) { setErrors({ ...errors, ...newErrors }); - setIsSendingDischargeApi(false); return; } } - const dischargeDetails = { - ...preDischargeForm, - discharge: value, - referred_to: referred_to?.id ?? preDischargeForm.referred_to, - discharge_date: dayjs(preDischargeForm.discharge_date).toISOString(), - }; - - if (dischargeDetails.referred_to != undefined) - delete dischargeDetails.referred_to_external; - - if (dischargeDetails.referred_to_external != undefined) - delete dischargeDetails.referred_to; + return true; + }; + const submitAction = useConfirmedAction(async () => { + setIsSendingDischargeApi(true); const dischargeResponse = await dispatch( dischargePatient( { ...preDischargeForm, - discharge: value, new_discharge_reason: discharge_reason, discharge_date: dayjs(preDischargeForm.discharge_date).toISOString(), }, { id: consultationData.id }, ), ); - setIsSendingDischargeApi(false); - if (dischargeResponse?.status === 200) { - Notification.Success({ - msg: "Patient Discharged Successfully", - }); + if (dischargeResponse?.status === 200) { + Notification.Success({ msg: "Patient Discharged Successfully" }); afterSubmit?.(); } - }; + }); const handleFacilitySelect = (selected?: FacilityModel) => { setFacility(selected ?? null); @@ -204,210 +192,248 @@ const DischargeModal = ({ })); }; - const encounterDuration = dayjs - .duration( - dayjs( - preDischargeForm[ - discharge_reason === - DISCHARGE_REASONS.find((i) => i.text == "Expired")?.id - ? "death_datetime" - : "discharge_date" - ], - ).diff(consultationData.encounter_date), - ) - .humanize(); + const encounterDuration = dayjs.duration( + dayjs( + preDischargeForm[ + discharge_reason === + DISCHARGE_REASONS.find((i) => i.text == "Expired")?.id + ? "death_datetime" + : "discharge_date" + ], + ).diff(consultationData.encounter_date), + ); + + const confirmationRequired = encounterDuration.asDays() >= 30; return ( - -

Discharge patient from CARE

- - -

- {new_discharge_reason === 3 // Expired - ? "Caution: Once a patient is marked as expired, the patient file cannot be transferred or edited. Please proceed with caution." - : "Caution: This action is irrevesible. Please proceed with caution."} -

-
+ <> + +
+

+ Are you sure you want to close this encounter, noting that the + patient has been admitted for{" "} + + {Math.ceil(encounterDuration.asDays())} days + + {" ?"} +

+

+ By confirming, you acknowledge that no further edits can be made to + this encounter and that the information entered is accurate to the + best of your knowledge. +

- } - show={show} - onClose={onClose} - className="md:max-w-3xl" - > -
- id} - optionLabel={({ text }) => text} - onChange={(e) => - setPreDischargeForm((prev) => ({ - ...prev, - new_discharge_reason: e.value, - })) - } - error={errors?.new_discharge_reason} - /> - {discharge_reason === - DISCHARGE_REASONS.find((i) => i.text == "Referred")?.id && ( -
- Referred to - - handleFacilitySelect(selected as FacilityModel | undefined) - } - disabled={!!referred_to} - selected={facility ?? null} - showAll - freeText - multiple={false} - errors={errors?.referred_to} - className="mb-4" - /> + + +

Discharge patient from CARE

+ + +

+ {t("caution")}: {t("action_irreversible")} +

+
- )} - i.text == "Expired")?.id - } - label={ - { - "3": "Cause of death", - "1": "Discharged Advice", - }[discharge_reason ?? 0] ?? "Notes" - } - name="discharge_notes" - value={preDischargeForm.discharge_notes} - onChange={(e) => - setPreDischargeForm((prev) => ({ - ...prev, - discharge_notes: e.value, - })) + } + show={show} + onClose={() => { + if (!submitAction.confirmationProps.show) { + onClose(); } - error={errors?.discharge_notes} - /> - i.text == "Expired")?.id - ? "death_datetime" - : "discharge_date" - } - label={ - discharge_reason === - DISCHARGE_REASONS.find((i) => i.text == "Expired")?.id - ? "Date of Death" - : "Date and Time of Discharge" - } - type="datetime-local" - value={ - preDischargeForm[ + }} + className="md:max-w-3xl" + > +
+ id} + optionLabel={({ text }) => text} + onChange={(e) => + setPreDischargeForm((prev) => ({ + ...prev, + new_discharge_reason: e.value, + })) + } + error={errors?.new_discharge_reason} + /> + {discharge_reason === + DISCHARGE_REASONS.find((i) => i.text == "Referred")?.id && ( +
+ Referred to + + handleFacilitySelect(selected as FacilityModel | undefined) + } + disabled={!!referred_to} + selected={facility ?? null} + showAll + freeText + multiple={false} + errors={errors?.referred_to} + className="mb-4" + /> +
+ )} + i.text == "Expired")?.id + } + label={ + { + "3": "Cause of death", + "1": "Discharged Advice", + }[discharge_reason ?? 0] ?? "Notes" + } + name="discharge_notes" + value={preDischargeForm.discharge_notes} + onChange={(e) => + setPreDischargeForm((prev) => ({ + ...prev, + discharge_notes: e.value, + })) + } + error={errors?.discharge_notes} + /> + i.text == "Expired")?.id ? "death_datetime" : "discharge_date" - ] - } - onChange={(e) => { - const updates: Record = { - discharge_date: undefined, - death_datetime: undefined, - }; - updates[e.name] = e.value; - setPreDischargeForm((form) => ({ ...form, ...updates })); - }} - required - min={dayjs(consultationData?.encounter_date).format( - "YYYY-MM-DDTHH:mm", - )} - max={dayjs().format("YYYY-MM-DDTHH:mm")} - error={ - discharge_reason === - DISCHARGE_REASONS.find((i) => i.text == "Expired")?.id - ? errors?.death_datetime - : errors?.discharge_date - } - /> - {discharge_reason === - DISCHARGE_REASONS.find((i) => i.text == "Recovered")?.id && ( - <> -
- Discharge Prescription Medications - -
-
- Discharge PRN Prescriptions - -
- - )} - {discharge_reason === - DISCHARGE_REASONS.find((i) => i.text == "Expired")?.id && ( - i.text == "Expired")?.id + ? "Date of Death" + : "Date and Time of Discharge" + } + type="datetime-local" + value={ + preDischargeForm[ + discharge_reason === + DISCHARGE_REASONS.find((i) => i.text == "Expired")?.id + ? "death_datetime" + : "discharge_date" + ] + } onChange={(e) => { - setPreDischargeForm((form) => { - return { - ...form, - death_confirmed_doctor: e.value, - }; - }); + const updates: Record = { + discharge_date: undefined, + death_datetime: undefined, + }; + updates[e.name] = e.value; + setPreDischargeForm((form) => ({ ...form, ...updates })); }} required - placeholder="Attending Doctor's Name and Designation" + min={dayjs(consultationData?.encounter_date).format( + "YYYY-MM-DDTHH:mm", + )} + max={dayjs().format("YYYY-MM-DDTHH:mm")} + error={ + discharge_reason === + DISCHARGE_REASONS.find((i) => i.text == "Expired")?.id + ? errors?.death_datetime + : errors?.discharge_date + } /> + {discharge_reason === + DISCHARGE_REASONS.find((i) => i.text == "Recovered")?.id && ( + <> +
+ Discharge Prescription Medications + +
+
+ Discharge PRN Prescriptions + +
+ + )} + {discharge_reason === + DISCHARGE_REASONS.find((i) => i.text == "Expired")?.id && ( + { + setPreDischargeForm((form) => { + return { + ...form, + death_confirmed_doctor: e.value, + }; + }); + }} + required + placeholder="Attending Doctor's Name and Designation" + /> + )} +
+ + {enable_hcx && ( + // TODO: if policy and approved pre-auth exists +
+

Claim Insurance

+ {latestClaim ? ( + + ) : ( + + )} +
)} -
- {enable_hcx && ( - // TODO: if policy and approved pre-auth exists -
-

Claim Insurance

- {latestClaim ? ( - +
+ + {t("encounter_duration_confirmation")}{" "} + {encounterDuration.humanize()}. + +
+
+ + {isSendingDischargeApi ? ( + ) : ( - { + if (!validate()) { + return; + } + + if (confirmationRequired) { + submitAction.requestConfirmation(); + return; + } + + submitAction.submit(); + }} + label="Confirm Discharge" + autoFocus /> )}
- )} - -
- - {t("encounter_duration_confirmation")}{" "} - {encounterDuration}. - -
-
- - {isSendingDischargeApi ? ( - - ) : ( - handlePatientDischarge(false)} - label="Confirm Discharge" - autoFocus - /> - )} -
- + + ); }; diff --git a/src/Components/Facility/DischargedPatientsList.tsx b/src/Components/Facility/DischargedPatientsList.tsx index 5d7a3c2975c..5a47a8ad6da 100644 --- a/src/Components/Facility/DischargedPatientsList.tsx +++ b/src/Components/Facility/DischargedPatientsList.tsx @@ -480,7 +480,7 @@ export default DischargedPatientsList; const PatientListItem = ({ patient }: { patient: PatientModel }) => { return (
-
+
Date: Wed, 24 Jul 2024 19:13:30 +0530 Subject: [PATCH 5/5] Enhancements to Camera Feed Component (#8140) --- src/CAREUI/display/NetworkSignal.tsx | 2 +- src/CAREUI/interactive/KeyboardShortcut.tsx | 42 ++- src/Components/CameraFeed/AssetBedSelect.tsx | 83 +++-- src/Components/CameraFeed/CameraFeed.tsx | 176 ++++++---- .../CameraFeed/CameraFeedWithBedPresets.tsx | 29 +- src/Components/CameraFeed/FeedButton.tsx | 32 +- src/Components/CameraFeed/FeedControls.tsx | 325 ++++++++++-------- src/Components/CameraFeed/useOperateCamera.ts | 39 ++- src/Components/Common/components/ButtonV2.tsx | 4 +- .../Diagnosis/DiagnosesListAccordion.tsx | 1 + .../ConsultationFeedTab.tsx | 190 +++++----- .../Facility/ConsultationDetails/index.tsx | 2 +- src/Locale/en/Common.json | 2 + 13 files changed, 536 insertions(+), 391 deletions(-) diff --git a/src/CAREUI/display/NetworkSignal.tsx b/src/CAREUI/display/NetworkSignal.tsx index 91ce6b58b4c..d241a37674a 100644 --- a/src/CAREUI/display/NetworkSignal.tsx +++ b/src/CAREUI/display/NetworkSignal.tsx @@ -45,7 +45,7 @@ export default function NetworkSignal({ strength, children }: Props) { i === 2 && "h-[15px]", // Whether to infill with strength color or not - strength > i ? "bg-current" : "bg-zinc-600", + strength > i ? "bg-current" : "bg-zinc-500/30", )} /> )) diff --git a/src/CAREUI/interactive/KeyboardShortcut.tsx b/src/CAREUI/interactive/KeyboardShortcut.tsx index 06ce149fb51..1d2bebeb316 100644 --- a/src/CAREUI/interactive/KeyboardShortcut.tsx +++ b/src/CAREUI/interactive/KeyboardShortcut.tsx @@ -2,32 +2,50 @@ import useKeyboardShortcut from "use-keyboard-shortcut"; import { classNames, isAppleDevice } from "../../Utils/utils"; interface Props { - children: React.ReactNode; + children?: React.ReactNode; shortcut: string[]; + altShortcuts?: string[][]; onTrigger: () => void; - shortcutSeperator?: string; helpText?: string; tooltipClassName?: string; } export default function KeyboardShortcut(props: Props) { - useKeyboardShortcut(props.shortcut, props.onTrigger, { - overrideSystem: true, - }); + useKeyboardShortcut(props.shortcut, props.onTrigger); + + if (!props.children) { + return null; + } return (
{props.children} - {props.helpText} - - {getShortcutKeyDescription(props.shortcut).join(" + ")} - + {props.helpText && ( + {props.helpText} + )} + {(props.altShortcuts || [props.shortcut]).map((shortcut, idx, arr) => ( + <> + + {shortcut.map((key, idx, keys) => ( + <> + {SHORTCUT_KEY_MAP[key] || key} + {idx !== keys.length - 1 && ( + + + )} + + ))} + + {idx !== arr.length - 1 && ( + or + )} + + ))}
); @@ -43,7 +61,3 @@ const SHORTCUT_KEY_MAP = { ArrowLeft: "←", ArrowRight: "→", } as Record; - -export const getShortcutKeyDescription = (shortcut: string[]) => { - return shortcut.map((key) => SHORTCUT_KEY_MAP[key] || key); -}; diff --git a/src/Components/CameraFeed/AssetBedSelect.tsx b/src/Components/CameraFeed/AssetBedSelect.tsx index 3d7b7ab0951..dafb28d133f 100644 --- a/src/Components/CameraFeed/AssetBedSelect.tsx +++ b/src/Components/CameraFeed/AssetBedSelect.tsx @@ -3,8 +3,11 @@ import { AssetBedModel } from "../Assets/AssetTypes"; import { Listbox, Transition } from "@headlessui/react"; import CareIcon from "../../CAREUI/icons/CareIcon"; import { classNames } from "../../Utils/utils"; +import { dropdownOptionClassNames } from "../Form/MultiSelectMenuV2"; +import ButtonV2 from "../Common/components/ButtonV2"; interface Props { + disabled?: boolean; options: AssetBedModel[]; value?: AssetBedModel; label?: (value: AssetBedModel) => string; @@ -15,34 +18,44 @@ export default function CameraPresetSelect(props: Props) { const label = props.label ?? defaultLabel; return ( <> -
- {/* Desktop View */} + {/* Desktop View */} +
{props.options .slice(0, props.options.length > 5 ? 4 : 5) - .map((option) => ( - - ))} + .map((option) => { + const selected = props.value?.id === option.id; + + return ( + props.onChange?.(option)} + border + size="small" + > + {label(option)} + {selected && ( + + )} + + ); + })} {props.options.length > 5 && ( o.id === props.value?.id)} /> )}
+ + {/* Mobile View */}
- {/* Mobile View */}
@@ -62,15 +75,15 @@ export const CameraPresetDropdown = (
@@ -80,38 +93,32 @@ export const CameraPresetDropdown = ( ? label(selected) : props.placeholder} + {selected && ( + + )} - + {options?.map((obj) => ( - `relative cursor-default select-none px-2 py-1 ${ - active ? "bg-zinc-700 text-white" : "text-zinc-400" - }` + className={(args) => + classNames(dropdownOptionClassNames(args), "px-2 py-1.5") } value={obj} > - {({ selected }) => ( - <> - - {label(obj)} - - - )} + {label(obj)} ))} diff --git a/src/Components/CameraFeed/CameraFeed.tsx b/src/Components/CameraFeed/CameraFeed.tsx index fdb1ccbc9f9..7c5cf7a8a19 100644 --- a/src/Components/CameraFeed/CameraFeed.tsx +++ b/src/Components/CameraFeed/CameraFeed.tsx @@ -4,20 +4,19 @@ import useOperateCamera, { PTZPayload } from "./useOperateCamera"; import usePlayer from "./usePlayer"; import { getStreamUrl } from "./utils"; import ReactPlayer from "react-player"; -import { classNames, isAppleDevice, isIOS } from "../../Utils/utils"; +import { classNames, isIOS } from "../../Utils/utils"; import FeedAlert, { FeedAlertState } from "./FeedAlert"; import FeedNetworkSignal from "./FeedNetworkSignal"; import NoFeedAvailable from "./NoFeedAvailable"; import FeedControls from "./FeedControls"; import FeedWatermark from "./FeedWatermark"; -import CareIcon from "../../CAREUI/icons/CareIcon"; import useFullscreen from "../../Common/hooks/useFullscreen"; +import useBreakpoints from "../../Common/hooks/useBreakpoints"; interface Props { children?: React.ReactNode; asset: AssetData; preset?: PTZPayload; - silent?: boolean; className?: string; // Callbacks onCameraPresetsObtained?: (presets: Record) => void; @@ -27,16 +26,16 @@ interface Props { constrolsDisabled?: boolean; shortcutsDisabled?: boolean; onMove?: () => void; - onReset?: () => void; + operate: ReturnType["operate"]; } export default function CameraFeed(props: Props) { const playerRef = useRef(null); const playerWrapperRef = useRef(null); const streamUrl = getStreamUrl(props.asset); + const inlineControls = useBreakpoints({ default: false, sm: true }); const player = usePlayer(streamUrl, playerRef); - const operate = useOperateCamera(props.asset.id, props.silent); const [isFullscreen, setFullscreen] = useFullscreen(); const [state, setState] = useState(); @@ -46,7 +45,10 @@ export default function CameraFeed(props: Props) { useEffect(() => { async function move(preset: PTZPayload) { setState("moving"); - const { res } = await operate({ type: "absolute_move", data: preset }); + const { res } = await props.operate({ + type: "absolute_move", + data: preset, + }); setTimeout(() => setState((s) => (s === "moving" ? undefined : s)), 4000); if (res?.status === 500) { setState("host_unreachable"); @@ -62,19 +64,19 @@ export default function CameraFeed(props: Props) { useEffect(() => { if (!props.onCameraPresetsObtained) return; async function getPresets(cb: (presets: Record) => void) { - const { res, data } = await operate({ type: "get_presets" }); + const { res, data } = await props.operate({ type: "get_presets" }); if (res?.ok && data) { cb((data as { result: Record }).result); } } getPresets(props.onCameraPresetsObtained); - }, [operate, props.onCameraPresetsObtained]); + }, [props.operate, props.onCameraPresetsObtained]); const initializeStream = useCallback(() => { player.initializeStream({ onSuccess: async () => { props.onStreamSuccess?.(); - const { res } = await operate({ type: "get_status" }); + const { res } = await props.operate({ type: "get_status" }); if (res?.status === 500) { setState("host_unreachable"); } @@ -88,31 +90,102 @@ export default function CameraFeed(props: Props) { const resetStream = () => { setState("loading"); - props.onReset?.(); initializeStream(); }; + const controls = !props.constrolsDisabled && ( + { + if (!value) { + setFullscreen(false); + return; + } + + if (isIOS) { + const element = document.querySelector("video"); + if (!element) { + return; + } + setFullscreen(true, element, true); + return; + } + + if (!playerRef.current) { + return; + } + + setFullscreen( + true, + playerWrapperRef.current || (playerRef.current as HTMLElement), + true, + ); + }} + onReset={resetStream} + onMove={async (data) => { + props.onMove?.(); + setState("moving"); + const { res } = await props.operate({ type: "relative_move", data }); + setTimeout(() => { + setState((state) => (state === "moving" ? undefined : state)); + }, 4000); + if (res?.status === 500) { + setState("host_unreachable"); + } + }} + /> + ); + return (
-
- {props.children} +
{ + if (player.status !== "playing") { + return "bg-black text-zinc-400"; + } + + if (isFullscreen) { + return "bg-zinc-900 text-white"; + } + + return "bg-zinc-500/20 text-zinc-800"; + })(), + )} + > +
+ {props.children} +
- - + {props.asset.name} {!isIOS && ( -
+
- -
+
{/* Notifications */} {player.status === "playing" && } @@ -177,7 +249,7 @@ export default function CameraFeed(props: Props) { ) : (
+ {!inlineControls && ( +
+ {controls} +
+ )}
); diff --git a/src/Components/CameraFeed/CameraFeedWithBedPresets.tsx b/src/Components/CameraFeed/CameraFeedWithBedPresets.tsx index 8ce9c9ef67f..7268397b81a 100644 --- a/src/Components/CameraFeed/CameraFeedWithBedPresets.tsx +++ b/src/Components/CameraFeed/CameraFeedWithBedPresets.tsx @@ -5,6 +5,8 @@ import useQuery from "../../Utils/request/useQuery"; import routes from "../../Redux/api"; import useSlug from "../../Common/hooks/useSlug"; import { CameraPresetDropdown } from "./AssetBedSelect"; +import useOperateCamera from "./useOperateCamera"; +import { classNames } from "../../Utils/utils"; interface Props { asset: AssetData; @@ -18,24 +20,29 @@ export default function LocationFeedTile(props: Props) { query: { limit: 100, facility, asset: props.asset?.id }, }); + const { operate, key } = useOperateCamera(props.asset.id, true); + return ( -
- {loading ? ( - loading presets... - ) : ( - +
+
); diff --git a/src/Components/CameraFeed/FeedButton.tsx b/src/Components/CameraFeed/FeedButton.tsx index f0e568d4ad4..e2ae2a8fe9e 100644 --- a/src/Components/CameraFeed/FeedButton.tsx +++ b/src/Components/CameraFeed/FeedButton.tsx @@ -4,7 +4,7 @@ import { classNames } from "../../Utils/utils"; interface Props { className?: string; children?: React.ReactNode; - readonly shortcut?: string[]; + shortcuts?: string[][]; onTrigger: () => void; helpText?: string; shortcutsDisabled?: boolean; @@ -15,7 +15,8 @@ export default function FeedButton(props: Props) { const child = (