From 53568617d35fe49060c69c75393af7f482161aa1 Mon Sep 17 00:00:00 2001 From: Bhavik Agarwal <bhavikiitian@gmail.com> Date: Fri, 7 Jul 2023 11:26:55 +0530 Subject: [PATCH 01/17] feat: add patient notes pop-up on consultation page --- package-lock.json | 33 +++++ package.json | 1 + .../Facility/ConsultationDetails.tsx | 3 + src/Components/Facility/PatientNoteCard.tsx | 39 ++++++ src/Components/Facility/PatientNotesList.tsx | 125 ++++++++++++++++++ .../Facility/PatientNotesSlideover.tsx | 117 ++++++++++++++++ 6 files changed, 318 insertions(+) create mode 100644 src/Components/Facility/PatientNoteCard.tsx create mode 100644 src/Components/Facility/PatientNotesList.tsx create mode 100644 src/Components/Facility/PatientNotesSlideover.tsx diff --git a/package-lock.json b/package-lock.json index 1d07e4fe19e..817cd903efe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,6 +47,7 @@ "react-dom": "18.2.0", "react-google-recaptcha": "^2.1.0", "react-i18next": "^11.18.6", + "react-infinite-scroll-component": "^6.1.0", "react-player": "^2.11.0", "react-qr-reader": "^2.2.1", "react-redux": "^8.0.4", @@ -14062,6 +14063,17 @@ } } }, + "node_modules/react-infinite-scroll-component": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.0.tgz", + "integrity": "sha512-SQu5nCqy8DxQWpnUVLx7V7b7LcA37aM7tvoWjTLZp1dk6EJibM5/4EJKzOnl07/BsM1Y40sKLuqjCwwH/xV0TQ==", + "dependencies": { + "throttle-debounce": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.0.0" + } + }, "node_modules/react-inspector": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/react-inspector/-/react-inspector-6.0.1.tgz", @@ -15870,6 +15882,14 @@ "dev": true, "license": "MIT" }, + "node_modules/throttle-debounce": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-2.3.0.tgz", + "integrity": "sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==", + "engines": { + "node": ">=8" + } + }, "node_modules/throttleit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", @@ -27049,6 +27069,14 @@ "html-parse-stringify": "^3.0.1" } }, + "react-infinite-scroll-component": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.0.tgz", + "integrity": "sha512-SQu5nCqy8DxQWpnUVLx7V7b7LcA37aM7tvoWjTLZp1dk6EJibM5/4EJKzOnl07/BsM1Y40sKLuqjCwwH/xV0TQ==", + "requires": { + "throttle-debounce": "^2.1.0" + } + }, "react-inspector": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/react-inspector/-/react-inspector-6.0.1.tgz", @@ -28397,6 +28425,11 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "throttle-debounce": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-2.3.0.tgz", + "integrity": "sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==" + }, "throttleit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", diff --git a/package.json b/package.json index fb232281bad..64145215a96 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "react-dom": "18.2.0", "react-google-recaptcha": "^2.1.0", "react-i18next": "^11.18.6", + "react-infinite-scroll-component": "^6.1.0", "react-player": "^2.11.0", "react-qr-reader": "^2.2.1", "react-redux": "^8.0.4", diff --git a/src/Components/Facility/ConsultationDetails.tsx b/src/Components/Facility/ConsultationDetails.tsx index 08681ba55fc..acdae779bab 100644 --- a/src/Components/Facility/ConsultationDetails.tsx +++ b/src/Components/Facility/ConsultationDetails.tsx @@ -53,6 +53,7 @@ import moment from "moment"; import { navigate } from "raviger"; import { useDispatch } from "react-redux"; import { useTranslation } from "react-i18next"; +import PatientNotesSlideover from "./PatientNotesSlideover"; const Loading = loadable(() => import("../Common/Loading")); const PageTitle = loadable(() => import("../Common/PageTitle")); @@ -1236,6 +1237,8 @@ export const ConsultationDetails = (props: any) => { show={showDoctors} setShow={setShowDoctors} /> + + <PatientNotesSlideover patientId={patientId} facilityId={facilityId} /> </div> ); }; diff --git a/src/Components/Facility/PatientNoteCard.tsx b/src/Components/Facility/PatientNoteCard.tsx new file mode 100644 index 00000000000..b90b089352c --- /dev/null +++ b/src/Components/Facility/PatientNoteCard.tsx @@ -0,0 +1,39 @@ +import { relativeDate, formatDate } from "../../Utils/utils"; + +const PatientNoteCard = ({ + note, + facilityId, +}: { + note: any; + facilityId: any; +}) => { + return ( + <div + key={note.id} + className="flex p-3 bg-white rounded-lg text-gray-800 mt-4 flex-col w-full border border-gray-300" + > + <div className="flex justify-between"> + <span className="text-gray-700 text-sm font-semibold"> + {note.created_by_object?.first_name || "Unknown"}{" "} + {note.created_by_object?.last_name} + </span> + <span className="text-gray-700 text-sm"> + {note.created_by_object.id === facilityId + ? "Remote Specialist" + : "Local Doctor"} + </span> + </div> + <span className="whitespace-pre-wrap break-words">{note.note}</span> + <div className="mt-3 text-xs text-gray-500 text-end"> + <div className="tooltip inline"> + <span className="tooltip-text tooltip-left"> + {formatDate(note.created_date)} + </span> + {relativeDate(note.created_date)} + </div> + </div> + </div> + ); +}; + +export default PatientNoteCard; diff --git a/src/Components/Facility/PatientNotesList.tsx b/src/Components/Facility/PatientNotesList.tsx new file mode 100644 index 00000000000..45464895697 --- /dev/null +++ b/src/Components/Facility/PatientNotesList.tsx @@ -0,0 +1,125 @@ +import { useCallback, useState, useEffect } from "react"; +import { useDispatch } from "react-redux"; +import { statusType, useAbortableEffect } from "../../Common/utils"; +import { getPatientNotes } from "../../Redux/actions"; +import { RESULTS_PER_PAGE_LIMIT } from "../../Common/constants"; +import CircularProgress from "../Common/components/CircularProgress"; +import PatientNoteCard from "./PatientNoteCard"; +import InfiniteScroll from "react-infinite-scroll-component"; + +interface PatientNotesProps { + patientId: any; + facilityId: any; + reload?: boolean; + setReload?: any; +} + +const pageSize = RESULTS_PER_PAGE_LIMIT; + +const PatientNotesList = (props: PatientNotesProps) => { + const { facilityId, reload, setReload } = props; + + const dispatch: any = useDispatch(); + const initialData: any = { notes: [], cPage: 1, totalPages: 1 }; + const [state, setState] = useState(initialData); + const [isLoading, setIsLoading] = useState(true); + + const fetchData = useCallback( + async (page = 1, status: statusType = { aborted: false }) => { + setIsLoading(true); + const res = await dispatch( + getPatientNotes(props.patientId, pageSize, (page - 1) * pageSize) + ); + if (!status.aborted) { + if (res && res.data) { + if (page === 1) { + setState({ + notes: res.data?.results, + cPage: page, + totalPages: Math.ceil(res.data.count / pageSize), + }); + } else { + setState((prevState: any) => ({ + ...prevState, + notes: [...prevState.notes, ...res.data.results], + cPage: page, + totalPages: Math.ceil(res.data.count / pageSize), + })); + } + } + setIsLoading(false); + } + }, + [props.patientId, dispatch] + ); + + useEffect(() => { + if (reload) { + fetchData(1); + setReload(false); + } + }, [reload]); + + useAbortableEffect( + (status: statusType) => { + fetchData(1, status); + }, + [fetchData] + ); + + const handleNext = () => { + if (state.cPage < state.totalPages) { + fetchData(state.cPage + 1); + setState((prevState: any) => ({ + ...prevState, + cPage: prevState.cPage + 1, + })); + } + }; + + if (isLoading && !state.notes.length) { + return ( + <div className="flex items-center justify-center w-full h-[400px]"> + <CircularProgress /> + </div> + ); + } + + return ( + <div + className="flex flex-col-reverse h-[390px] overflow-auto m-2 bg-white" + id="patient-notes-list" + > + {state.notes.length ? ( + <InfiniteScroll + next={handleNext} + hasMore={state.cPage < state.totalPages} + height={385} + loader={ + <div className="flex items-center justify-center"> + <CircularProgress /> + </div> + } + className="flex flex-col-reverse p-2" + inverse={true} + dataLength={state.notes.length} + scrollableTarget="patient-notes-list" + > + {state.notes.map((note: any) => ( + <PatientNoteCard + note={note} + key={note.id} + facilityId={facilityId} + /> + ))} + </InfiniteScroll> + ) : ( + <div className="text-gray-500 text-2xl font-bold flex justify-center items-center mt-2"> + No Notes Found + </div> + )} + </div> + ); +}; + +export default PatientNotesList; diff --git a/src/Components/Facility/PatientNotesSlideover.tsx b/src/Components/Facility/PatientNotesSlideover.tsx new file mode 100644 index 00000000000..a9a9ac72a26 --- /dev/null +++ b/src/Components/Facility/PatientNotesSlideover.tsx @@ -0,0 +1,117 @@ +import { useState, useEffect } from "react"; +import { getPatient, addPatientNote } from "../../Redux/actions"; +import * as Notification from "../../Utils/Notifications.js"; +import { useDispatch } from "react-redux"; +import PatientNotesList from "./PatientNotesList"; +import AuthorizedChild from "../../CAREUI/misc/AuthorizedChild"; +import { NonReadOnlyUsers } from "../../Utils/AuthorizeFor"; +import CareIcon from "../../CAREUI/icons/CareIcon"; + +interface PatientNotesProps { + patientId: any; + facilityId: any; +} + +export default function PatientNotesSlideover(props: PatientNotesProps) { + const [show, setShow] = useState(false); + const [patientActive, setPatientActive] = useState(true); + const [noteField, setNoteField] = useState(""); + const [reload, setReload] = useState(false); + + const dispatch = useDispatch(); + + const { facilityId, patientId } = props; + + const onAddNote = () => { + const payload = { + note: noteField, + }; + if (!/\S+/.test(noteField)) { + Notification.Error({ + msg: "Note Should Contain At Least 1 Character", + }); + return; + } + dispatch(addPatientNote(patientId, payload)).then(() => { + Notification.Success({ msg: "Note added successfully" }); + setNoteField(""); + setReload(!reload); + }); + }; + + useEffect(() => { + async function fetchPatientName() { + if (patientId) { + const res = await dispatch(getPatient({ id: patientId })); + if (res.data) { + setPatientActive(res.data.is_active); + } + } + } + fetchPatientName(); + }, [dispatch, patientId]); + + return ( + <div + className={`fixed right-0 sm:right-20 bottom-0 ${ + show ? "w-3/12 min-w-[400px]" : "w-1/6 min-w-[250px]" + }`} + > + {!show ? ( + <div className="flex justify-around items-center w-full p-2 rounded-t-md bg-primary-800 text-white"> + <span className="font-semibold">Doctor's Notes</span> + <div + className={`flex items-center justify-center w-8 h-8 cursor-pointer rounded text-gray-100 text-opacity-70 hover:text-opacity-100 bg-primary-800 hover:bg-primary-700 ${ + show ? "rotate-180" : "" + }`} + onClick={() => setShow(!show)} + > + <i className="fa-solid fa-chevron-up transition-all duration-300 delay-150 ease-out" /> + </div> + </div> + ) : ( + <div className="bg-white rounded-t-md w-full h-[500px] flex flex-col border-2 border-primary-800 transition-all -translate-y-0 "> + {/* Doctor Notes Header */} + <div className="flex justify-between items-center w-full p-2 px-4 rounded-t-md bg-primary-800 text-white"> + <span className="font-semibold">Doctor's Notes</span> + <div + className={`flex items-center justify-center w-8 h-8 cursor-pointer rounded text-gray-100 text-opacity-70 hover:text-opacity-100 bg-primary-800 hover:bg-primary-700 ${ + show ? "rotate-180" : "" + }`} + onClick={() => setShow(!show)} + > + <i className="fa-solid fa-chevron-up transition-all duration-300 delay-150 ease-out" /> + </div> + </div> + {/* Doctor Notes Body */} + <PatientNotesList + facilityId={facilityId} + patientId={patientId} + reload={reload} + setReload={setReload} + /> + <AuthorizedChild authorizeFor={NonReadOnlyUsers}> + {({ isAuthorized }) => ( + <div className="mx-4 h-fit relative"> + <input + placeholder="Type your Note" + className=" inline-block w-full border border-gray-500 rounded-lg p-2 focus:outline-none focus:ring-primary-500 focus:border-primary-500" + value={noteField} + onChange={(e) => setNoteField(e.target.value)} + disabled={!patientActive || !isAuthorized} + /> + <button + className="absolute top-2.5 right-2.5 text-primary-500" + onClick={onAddNote} + disabled={!patientActive || !isAuthorized} + > + <CareIcon className="care-l-message" /> + </button> + </div> + )} + </AuthorizedChild> + </div> + )} + </div> + ); +} From a7155e4d59e2d8b5bee9942c96ebe5c801694445 Mon Sep 17 00:00:00 2001 From: Bhavik Agarwal <bhavikiitian@gmail.com> Date: Fri, 7 Jul 2023 13:23:00 +0530 Subject: [PATCH 02/17] Migrate form elements to CAREUI --- src/Components/Facility/PatientNotesList.tsx | 4 +- .../Facility/PatientNotesSlideover.tsx | 72 ++++++++++--------- 2 files changed, 42 insertions(+), 34 deletions(-) diff --git a/src/Components/Facility/PatientNotesList.tsx b/src/Components/Facility/PatientNotesList.tsx index 45464895697..95ada03f458 100644 --- a/src/Components/Facility/PatientNotesList.tsx +++ b/src/Components/Facility/PatientNotesList.tsx @@ -79,7 +79,7 @@ const PatientNotesList = (props: PatientNotesProps) => { if (isLoading && !state.notes.length) { return ( - <div className="flex items-center justify-center w-full h-[400px]"> + <div className=" bg-white flex items-center justify-center w-full h-[400px]"> <CircularProgress /> </div> ); @@ -94,7 +94,7 @@ const PatientNotesList = (props: PatientNotesProps) => { <InfiniteScroll next={handleNext} hasMore={state.cPage < state.totalPages} - height={385} + height={380} loader={ <div className="flex items-center justify-center"> <CircularProgress /> diff --git a/src/Components/Facility/PatientNotesSlideover.tsx b/src/Components/Facility/PatientNotesSlideover.tsx index a9a9ac72a26..45b6857b412 100644 --- a/src/Components/Facility/PatientNotesSlideover.tsx +++ b/src/Components/Facility/PatientNotesSlideover.tsx @@ -3,9 +3,11 @@ import { getPatient, addPatientNote } from "../../Redux/actions"; import * as Notification from "../../Utils/Notifications.js"; import { useDispatch } from "react-redux"; import PatientNotesList from "./PatientNotesList"; -import AuthorizedChild from "../../CAREUI/misc/AuthorizedChild"; import { NonReadOnlyUsers } from "../../Utils/AuthorizeFor"; import CareIcon from "../../CAREUI/icons/CareIcon"; +import { classNames } from "../../Utils/utils"; +import TextFormField from "../Form/FormFields/TextFormField"; +import ButtonV2 from "../Common/components/ButtonV2"; interface PatientNotesProps { patientId: any; @@ -53,34 +55,37 @@ export default function PatientNotesSlideover(props: PatientNotesProps) { return ( <div - className={`fixed right-0 sm:right-20 bottom-0 ${ - show ? "w-3/12 min-w-[400px]" : "w-1/6 min-w-[250px]" - }`} + className={classNames( + "fixed right-0 sm:right-20 bottom-0", + show ? "w-[400px]" : "w-[250px]" + )} > {!show ? ( <div className="flex justify-around items-center w-full p-2 rounded-t-md bg-primary-800 text-white"> <span className="font-semibold">Doctor's Notes</span> <div - className={`flex items-center justify-center w-8 h-8 cursor-pointer rounded text-gray-100 text-opacity-70 hover:text-opacity-100 bg-primary-800 hover:bg-primary-700 ${ + className={classNames( + "flex items-center justify-center w-8 h-8 cursor-pointer rounded text-gray-100 text-opacity-70 hover:text-opacity-100 bg-primary-800 hover:bg-primary-700", show ? "rotate-180" : "" - }`} + )} onClick={() => setShow(!show)} > - <i className="fa-solid fa-chevron-up transition-all duration-300 delay-150 ease-out" /> + <CareIcon className="care-l-angle-up text-lg transition-all duration-300 delay-150 ease-out" /> </div> </div> ) : ( - <div className="bg-white rounded-t-md w-full h-[500px] flex flex-col border-2 border-primary-800 transition-all -translate-y-0 "> + <div className="bg-white rounded-t-md w-full h-[500px] flex flex-col border-2 border-b-0 pb-3 border-primary-800 transition-all overflow-clip -translate-y-0 "> {/* Doctor Notes Header */} - <div className="flex justify-between items-center w-full p-2 px-4 rounded-t-md bg-primary-800 text-white"> + <div className="flex justify-between items-center w-full p-2 px-4 bg-primary-800 text-white"> <span className="font-semibold">Doctor's Notes</span> <div - className={`flex items-center justify-center w-8 h-8 cursor-pointer rounded text-gray-100 text-opacity-70 hover:text-opacity-100 bg-primary-800 hover:bg-primary-700 ${ + className={classNames( + "flex items-center justify-center w-8 h-8 cursor-pointer rounded text-gray-100 text-opacity-70 hover:text-opacity-100 bg-primary-800 hover:bg-primary-700", show ? "rotate-180" : "" - }`} + )} onClick={() => setShow(!show)} > - <i className="fa-solid fa-chevron-up transition-all duration-300 delay-150 ease-out" /> + <CareIcon className="care-l-angle-up text-lg transition-all duration-300 delay-150 ease-out" /> </div> </div> {/* Doctor Notes Body */} @@ -90,26 +95,29 @@ export default function PatientNotesSlideover(props: PatientNotesProps) { reload={reload} setReload={setReload} /> - <AuthorizedChild authorizeFor={NonReadOnlyUsers}> - {({ isAuthorized }) => ( - <div className="mx-4 h-fit relative"> - <input - placeholder="Type your Note" - className=" inline-block w-full border border-gray-500 rounded-lg p-2 focus:outline-none focus:ring-primary-500 focus:border-primary-500" - value={noteField} - onChange={(e) => setNoteField(e.target.value)} - disabled={!patientActive || !isAuthorized} - /> - <button - className="absolute top-2.5 right-2.5 text-primary-500" - onClick={onAddNote} - disabled={!patientActive || !isAuthorized} - > - <CareIcon className="care-l-message" /> - </button> - </div> - )} - </AuthorizedChild> + <div className="flex items-center mx-4 relative"> + <TextFormField + name="note" + value={noteField} + onChange={(e) => setNoteField(e.value)} + className="grow" + type="text" + errorClassName="hidden" + placeholder="Type your Note" + disabled={!patientActive} + /> + <ButtonV2 + onClick={onAddNote} + border={false} + className="absolute right-2" + ghost + size="small" + disabled={!patientActive} + authorizeFor={NonReadOnlyUsers} + > + <CareIcon className="care-l-message text-lg" /> + </ButtonV2> + </div> </div> )} </div> From 10a77613a36cce4ac77cf20044efc8f8dd6551b9 Mon Sep 17 00:00:00 2001 From: Bhavik Agarwal <bhavikiitian@gmail.com> Date: Sat, 8 Jul 2023 13:33:02 +0530 Subject: [PATCH 03/17] made patient notes popup responsive --- src/Components/Facility/PatientNotesList.tsx | 5 ++--- src/Components/Facility/PatientNotesSlideover.tsx | 8 +++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Components/Facility/PatientNotesList.tsx b/src/Components/Facility/PatientNotesList.tsx index 95ada03f458..69dda827f4c 100644 --- a/src/Components/Facility/PatientNotesList.tsx +++ b/src/Components/Facility/PatientNotesList.tsx @@ -87,20 +87,19 @@ const PatientNotesList = (props: PatientNotesProps) => { return ( <div - className="flex flex-col-reverse h-[390px] overflow-auto m-2 bg-white" + className="flex flex-col-reverse grow h-[390px] overflow-auto m-2 bg-white" id="patient-notes-list" > {state.notes.length ? ( <InfiniteScroll next={handleNext} hasMore={state.cPage < state.totalPages} - height={380} loader={ <div className="flex items-center justify-center"> <CircularProgress /> </div> } - className="flex flex-col-reverse p-2" + className="flex flex-col-reverse p-2 h-full" inverse={true} dataLength={state.notes.length} scrollableTarget="patient-notes-list" diff --git a/src/Components/Facility/PatientNotesSlideover.tsx b/src/Components/Facility/PatientNotesSlideover.tsx index 45b6857b412..d377064c6dc 100644 --- a/src/Components/Facility/PatientNotesSlideover.tsx +++ b/src/Components/Facility/PatientNotesSlideover.tsx @@ -56,8 +56,10 @@ export default function PatientNotesSlideover(props: PatientNotesProps) { return ( <div className={classNames( - "fixed right-0 sm:right-20 bottom-0", - show ? "w-[400px]" : "w-[250px]" + "fixed sm:right-20 bottom-0 z-20", + show + ? "w-screen h-screen sm:h-fit sm:w-[400px] right-0" + : "w-[250px] right-10" )} > {!show ? ( @@ -74,7 +76,7 @@ export default function PatientNotesSlideover(props: PatientNotesProps) { </div> </div> ) : ( - <div className="bg-white rounded-t-md w-full h-[500px] flex flex-col border-2 border-b-0 pb-3 border-primary-800 transition-all overflow-clip -translate-y-0 "> + <div className="bg-white sm:rounded-t-md w-full h-screen sm:h-[500px] flex flex-col border-2 border-b-0 pb-3 border-primary-800 transition-all overflow-clip -translate-y-0 "> {/* Doctor Notes Header */} <div className="flex justify-between items-center w-full p-2 px-4 bg-primary-800 text-white"> <span className="font-semibold">Doctor's Notes</span> From e3c3966fabc82224b1fd78407f4e53415aa177d7 Mon Sep 17 00:00:00 2001 From: Bhavik Agarwal <bhavikiitian@gmail.com> Date: Wed, 12 Jul 2023 13:05:35 +0530 Subject: [PATCH 04/17] feat: updated ui of patient notes dedicated page --- src/Components/Facility/PatientNoteCard.tsx | 8 +- src/Components/Patient/PatientNotes.tsx | 196 ++++++-------------- 2 files changed, 57 insertions(+), 147 deletions(-) diff --git a/src/Components/Facility/PatientNoteCard.tsx b/src/Components/Facility/PatientNoteCard.tsx index b90b089352c..8029ec915c1 100644 --- a/src/Components/Facility/PatientNoteCard.tsx +++ b/src/Components/Facility/PatientNoteCard.tsx @@ -18,9 +18,11 @@ const PatientNoteCard = ({ {note.created_by_object?.last_name} </span> <span className="text-gray-700 text-sm"> - {note.created_by_object.id === facilityId - ? "Remote Specialist" - : "Local Doctor"} + {note.created_by_object.user_type === "Doctor" + ? note.created_by_object.home_facility !== facilityId + ? "Remote Specialist" + : "" + : note.created_by_object.user_type} </span> </div> <span className="whitespace-pre-wrap break-words">{note.note}</span> diff --git a/src/Components/Patient/PatientNotes.tsx b/src/Components/Patient/PatientNotes.tsx index 0756883d1d2..f5d09c80473 100644 --- a/src/Components/Patient/PatientNotes.tsx +++ b/src/Components/Patient/PatientNotes.tsx @@ -1,88 +1,29 @@ -import React, { useCallback, useState, useEffect } from "react"; +import { useState, useEffect } from "react"; import { useDispatch } from "react-redux"; -import { statusType, useAbortableEffect } from "../../Common/utils"; -import { - getPatientNotes, - addPatientNote, - getPatient, -} from "../../Redux/actions"; +import { addPatientNote, getPatient } from "../../Redux/actions"; import * as Notification from "../../Utils/Notifications.js"; import PageTitle from "../Common/PageTitle"; -import Pagination from "../Common/Pagination"; -import { navigate } from "raviger"; -import { RESULTS_PER_PAGE_LIMIT } from "../../Common/constants"; -import Loading from "../Common/Loading"; -import { formatDate } from "../../Utils/utils"; +import CareIcon from "../../CAREUI/icons/CareIcon"; +import TextFormField from "../Form/FormFields/TextFormField"; import ButtonV2 from "../Common/components/ButtonV2"; import { NonReadOnlyUsers } from "../../Utils/AuthorizeFor"; +import PatientNotesList from "../Facility/PatientNotesList"; interface PatientNotesProps { patientId: any; facilityId: any; } -const pageSize = RESULTS_PER_PAGE_LIMIT; - const PatientNotes = (props: PatientNotesProps) => { const { patientId, facilityId } = props; - const dispatch: any = useDispatch(); - const initialData: any = { notes: [], cPage: 1, count: 1 }; - const [state, setState] = useState(initialData); + const [patientActive, setPatientActive] = useState(true); const [noteField, setNoteField] = useState(""); - const [isLoading, setIsLoading] = useState(true); + const [reload, setReload] = useState(false); const [facilityName, setFacilityName] = useState(""); const [patientName, setPatientName] = useState(""); - const [patientActive, setPatientActive] = useState(true); - - const fetchData = useCallback( - async (page = 1, status: statusType = { aborted: false }) => { - setIsLoading(true); - const res = await dispatch( - getPatientNotes(props.patientId, pageSize, (page - 1) * pageSize) - ); - if (!status.aborted) { - if (res && res.data) { - setState({ - ...state, - count: res.data?.count, - notes: res.data?.results, - cPage: page, - }); - } - setIsLoading(false); - } - }, - [props.patientId, dispatch] - ); - - useAbortableEffect( - (status: statusType) => { - fetchData(1, status); - }, - [fetchData] - ); - - useEffect(() => { - async function fetchPatientName() { - if (patientId) { - const res = await dispatch(getPatient({ id: patientId })); - if (res.data) { - setPatientName(res.data.name); - setFacilityName(res.data.facility_object.name); - setPatientActive(res.data.is_active); - } - } else { - setPatientName(""); - setFacilityName(""); - } - } - fetchPatientName(); - }, [dispatch, patientId]); - function handlePagination(page: number) { - fetchData(page); - } + const dispatch = useDispatch(); const onAddNote = () => { const payload = { @@ -94,16 +35,26 @@ const PatientNotes = (props: PatientNotesProps) => { }); return; } - dispatch(addPatientNote(props.patientId, payload)).then(() => { + dispatch(addPatientNote(patientId, payload)).then(() => { Notification.Success({ msg: "Note added successfully" }); setNoteField(""); - fetchData(); + setReload(!reload); }); }; - if (isLoading) { - return <Loading />; - } + useEffect(() => { + async function fetchPatientName() { + if (patientId) { + const res = await dispatch(getPatient({ id: patientId })); + if (res.data) { + setPatientActive(res.data.is_active); + setPatientName(res.data.name); + setFacilityName(res.data.facility_object.name); + } + } + } + fetchPatientName(); + }, [dispatch, patientId]); return ( <div className="w-full flex flex-col"> @@ -116,80 +67,37 @@ const PatientNotes = (props: PatientNotesProps) => { }} backUrl={`/facility/${facilityId}/patient/${patientId}`} /> - <h3 className="text-lg pl-10">Add new notes</h3> - <textarea - rows={3} - placeholder="Type your Note" - className="mx-10 my-4 border border-gray-500 rounded-lg p-4 focus:outline-none focus:ring-primary-500 focus:border-primary-500" - onChange={(e) => setNoteField(e.target.value)} - /> - <div className="flex w-full justify-end pr-10"> - <ButtonV2 - authorizeFor={NonReadOnlyUsers} - onClick={onAddNote} - disabled={!patientActive} - > - Post Your Note - </ButtonV2> - </div> - <div className="px-10 py-5"> - <h3 className="text-lg">Added Notes</h3> - <div className="w-full"> - {state.notes.length ? ( - state.notes.map((note: any) => ( - <div - key={note.id} - className="flex p-4 bg-white rounded-lg text-gray-800 mt-4 flex-col w-full border border-gray-300" - > - <span className="whitespace-pre-wrap break-words"> - {note.note} - </span> - <div className="mt-3"> - <span className="text-xs text-gray-500"> - {formatDate(note.created_date) || "-"} - </span> - </div> - <div className="sm:flex space-y-2 sm:space-y-0"> - <div className="mr-2 inline-flex w-full md:w-auto justify-center bg-gray-100 border items-center rounded-md py-1 pl-2 pr-3"> - <div className="flex justify-center items-center w-8 h-8 rounded-full"> - <i className="fas fa-user" /> - </div> - <span className="text-gray-700 text-sm"> - {note.created_by_object?.first_name || "Unknown"}{" "} - {note.created_by_object?.last_name} - </span> - </div> + <div className="mx-3 my-2 px-2 py-2 sm:mx-10 sm:my-5 bg-white sm:px-5 sm:py-5 rounded-lg grow"> + <PatientNotesList + patientId={patientId} + facilityId={facilityId} + reload={reload} + setReload={setReload} + /> - <div - className="inline-flex w-full md:w-auto justify-center bg-gray-100 border items-center rounded-md py-1 pl-2 pr-3 cursor-pointer" - onClick={() => navigate(`/facility/${note.facility?.id}`)} - > - <div className="flex justify-center items-center w-8 h-8 rounded-full"> - <i className="fas fa-hospital" /> - </div> - <span className="text-gray-700 text-sm"> - {note.facility?.name || "Unknown"} - </span> - </div> - </div> - </div> - )) - ) : ( - <div className="text-gray-500 text-2xl font-bold flex justify-center items-center mt-2"> - No Notes Found - </div> - )} - {state.count > pageSize && ( - <div className="mt-4 flex w-full justify-center"> - <Pagination - data={{ totalCount: state.count }} - onChange={handlePagination} - defaultPerPage={pageSize} - cPage={state.cPage} - /> - </div> - )} + <div className="flex items-center mx-4 relative"> + <TextFormField + name="note" + value={noteField} + onChange={(e) => setNoteField(e.value)} + className="grow" + type="text" + errorClassName="hidden" + placeholder="Type your Note" + disabled={!patientActive} + /> + <ButtonV2 + onClick={onAddNote} + border={false} + className="absolute right-2" + ghost + size="small" + disabled={!patientActive} + authorizeFor={NonReadOnlyUsers} + > + <CareIcon className="care-l-message text-lg" /> + </ButtonV2> </div> </div> </div> From 16bc28bd4a7630b9e67ba638f26c7706fdbd462a Mon Sep 17 00:00:00 2001 From: Bhavik Agarwal <bhavikiitian@gmail.com> Date: Wed, 19 Jul 2023 09:47:58 +0530 Subject: [PATCH 05/17] resolved merge conflicts --- package-lock.json | 1503 ++++++++++++++++++++++++++++++++++++++++++++- package.json | 2 +- 2 files changed, 1476 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index 817cd903efe..303bceb6668 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,6 +48,7 @@ "react-google-recaptcha": "^2.1.0", "react-i18next": "^11.18.6", "react-infinite-scroll-component": "^6.1.0", + "react-markdown": "^8.0.7", "react-player": "^2.11.0", "react-qr-reader": "^2.2.1", "react-redux": "^8.0.4", @@ -58,7 +59,6 @@ "redux": "^4.1.0", "redux-thunk": "^2.3.0", "rescript-webapi": "^0.6.1", - "screenfull": "^5.1.0", "use-keyboard-shortcut": "^1.1.6", "uuid": "^8.3.2" }, @@ -5265,6 +5265,14 @@ "cypress": "*" } }, + "node_modules/@types/debug": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz", + "integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/detect-port": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/detect-port/-/detect-port-1.3.2.tgz", @@ -5356,6 +5364,14 @@ "@types/node": "*" } }, + "node_modules/@types/hast": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.5.tgz", + "integrity": "sha512-SvQi0L/lNpThgPoleH53cdjB3y9zpLlVjRbqB3rH8hx1jiRSBGAhyjV3H+URFjNVRqt2EdYNrbZE5IsGlNfpRg==", + "dependencies": { + "@types/unist": "^2" + } + }, "node_modules/@types/hoist-non-react-statics": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", @@ -5430,6 +5446,14 @@ "@types/lodash": "*" } }, + "node_modules/@types/mdast": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.12.tgz", + "integrity": "sha512-DT+iNIRNX884cx0/Q1ja7NyUPpZuv0KPyL5rGNxm1WC1OtHstl7n4Jb7nk+xacNShQMbczJjt8uFzznpp6kYBg==", + "dependencies": { + "@types/unist": "^2" + } + }, "node_modules/@types/mdx": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.3.tgz", @@ -5452,6 +5476,11 @@ "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==" }, + "node_modules/@types/ms": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", + "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" + }, "node_modules/@types/node": { "version": "18.11.18", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", @@ -5659,8 +5688,7 @@ "node_modules/@types/unist": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", - "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==", - "dev": true + "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==" }, "node_modules/@types/use-sync-external-store": { "version": "0.0.3", @@ -6623,6 +6651,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -7133,6 +7170,15 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/check-more-types": { "version": "2.24.0", "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", @@ -7395,6 +7441,15 @@ "node": ">= 0.8" } }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -7927,6 +7982,18 @@ } } }, + "node_modules/decode-named-character-reference": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", + "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/deep-equal": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.0.tgz", @@ -8158,6 +8225,14 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -9500,7 +9575,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true, "license": "MIT" }, "node_modules/extract-zip": { @@ -10554,6 +10628,15 @@ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" }, + "node_modules/hast-util-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz", + "integrity": "sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -10811,6 +10894,11 @@ "node": ">=10" } }, + "node_modules/inline-style-parser": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", + "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==" + }, "node_modules/internal-slot": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", @@ -10934,6 +11022,28 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "engines": { + "node": ">=4" + } + }, "node_modules/is-callable": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", @@ -11154,6 +11264,17 @@ "node": ">=8" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-plain-object": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", @@ -12297,6 +12418,113 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-from-markdown": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", + "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "mdast-util-to-string": "^3.1.0", + "micromark": "^3.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-decode-string": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-stringify-position": "^3.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown/node_modules/mdast-util-to-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "dependencies": { + "@types/mdast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-12.3.0.tgz", + "integrity": "sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "mdast-util-definitions": "^5.0.0", + "micromark-util-sanitize-uri": "^1.1.0", + "trim-lines": "^3.0.0", + "unist-util-generated": "^2.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast/node_modules/mdast-util-definitions": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz", + "integrity": "sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA==", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-to-string": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-1.1.0.tgz", @@ -12359,6 +12587,427 @@ "node": ">= 0.6" } }, + "node_modules/micromark": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", + "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "micromark-core-commonmark": "^1.0.1", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", + "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-factory-destination": "^1.0.0", + "micromark-factory-label": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-factory-title": "^1.0.0", + "micromark-factory-whitespace": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-html-tag-name": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-factory-destination": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", + "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", + "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", + "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", + "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", + "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", + "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", + "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", + "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", + "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", + "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", + "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", + "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-html-tag-name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", + "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", + "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", + "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", + "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", + "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", + "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, "node_modules/micromatch": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", @@ -12567,7 +13216,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "dev": true, "engines": { "node": ">=4" } @@ -13518,6 +14166,15 @@ "react-is": "^16.13.1" } }, + "node_modules/property-information": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.2.0.tgz", + "integrity": "sha512-kma4U7AFCTwpqq5twzC1YVIDXSqg6qQK6JN0smOw8fgRy1OkMi0CYSzFmsy6dnqSenamAtj0CyXMUJ1Mf6oROg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -14089,6 +14746,89 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/react-markdown": { + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-8.0.7.tgz", + "integrity": "sha512-bvWbzG4MtOU62XqBx3Xx+zB2raaFFsq4mYiAzfjXJMEz2sixgeAfraA3tvzULF02ZdOMUOKTBFFaZJDDrq+BJQ==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/prop-types": "^15.0.0", + "@types/unist": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^2.0.0", + "prop-types": "^15.0.0", + "property-information": "^6.0.0", + "react-is": "^18.0.0", + "remark-parse": "^10.0.0", + "remark-rehype": "^10.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-object": "^0.4.0", + "unified": "^10.0.0", + "unist-util-visit": "^4.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=16", + "react": ">=16" + } + }, + "node_modules/react-markdown/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, + "node_modules/react-markdown/node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/react-markdown/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/react-markdown/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/react-markdown/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/react-player": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/react-player/-/react-player-2.11.0.tgz", @@ -14631,6 +15371,35 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark-parse": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.2.tgz", + "integrity": "sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-10.1.0.tgz", + "integrity": "sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "mdast-util-to-hast": "^12.1.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-slug": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/remark-slug/-/remark-slug-6.1.0.tgz", @@ -14878,6 +15647,17 @@ "tslib": "^2.1.0" } }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -14898,18 +15678,6 @@ "loose-envify": "^1.1.0" } }, - "node_modules/screenfull": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/screenfull/-/screenfull-5.2.0.tgz", - "integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/sdp": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/sdp/-/sdp-2.12.0.tgz", @@ -15569,6 +16337,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-to-object": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.1.tgz", + "integrity": "sha512-HFpbb5gr2ypci7Qw+IOhnP2zOU7e77b+rzM+wTzXzfi1PrtBCX0E7Pk4wL4iTLnhzZ+JgEGAhX81ebTg/aYjQw==", + "dependencies": { + "inline-style-parser": "0.1.1" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -15984,6 +16760,24 @@ "node": "*" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.1.0.tgz", + "integrity": "sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ts-dedent": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", @@ -16196,6 +16990,24 @@ "node": ">=4" } }, + "node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unique-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", @@ -16208,6 +17020,15 @@ "node": ">=8" } }, + "node_modules/unist-util-generated": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.1.tgz", + "integrity": "sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unist-util-is": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", @@ -16218,6 +17039,30 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-position": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", + "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unist-util-visit": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.3.tgz", @@ -16424,6 +17269,31 @@ "integrity": "sha512-dsNgbLaTrd6l3MMxTtouOCFw4CBFc/3a+GgYA2YyrJvyQ1u6q4pcu3ktLoUZ/VN/Aw9WsauazbgsgdfVWgAKQg==", "dev": true }, + "node_modules/uvu": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", + "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==", + "dependencies": { + "dequal": "^2.0.0", + "diff": "^5.0.0", + "kleur": "^4.0.3", + "sade": "^1.7.3" + }, + "bin": { + "uvu": "bin.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/uvu/node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "engines": { + "node": ">=6" + } + }, "node_modules/uzip": { "version": "0.20201231.0", "resolved": "https://registry.npmjs.org/uzip/-/uzip-0.20201231.0.tgz", @@ -16484,6 +17354,34 @@ "extsprintf": "^1.2.0" } }, + "node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vite": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/vite/-/vite-4.2.1.tgz", @@ -20802,6 +21700,14 @@ "cypress": "*" } }, + "@types/debug": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz", + "integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==", + "requires": { + "@types/ms": "*" + } + }, "@types/detect-port": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/detect-port/-/detect-port-1.3.2.tgz", @@ -20892,6 +21798,14 @@ "@types/node": "*" } }, + "@types/hast": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.5.tgz", + "integrity": "sha512-SvQi0L/lNpThgPoleH53cdjB3y9zpLlVjRbqB3rH8hx1jiRSBGAhyjV3H+URFjNVRqt2EdYNrbZE5IsGlNfpRg==", + "requires": { + "@types/unist": "^2" + } + }, "@types/hoist-non-react-statics": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", @@ -20961,6 +21875,14 @@ "@types/lodash": "*" } }, + "@types/mdast": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.12.tgz", + "integrity": "sha512-DT+iNIRNX884cx0/Q1ja7NyUPpZuv0KPyL5rGNxm1WC1OtHstl7n4Jb7nk+xacNShQMbczJjt8uFzznpp6kYBg==", + "requires": { + "@types/unist": "^2" + } + }, "@types/mdx": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.3.tgz", @@ -20983,6 +21905,11 @@ "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==" }, + "@types/ms": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", + "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" + }, "@types/node": { "version": "18.11.18", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", @@ -21179,8 +22106,7 @@ "@types/unist": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", - "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==", - "dev": true + "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==" }, "@types/use-sync-external-store": { "version": "0.0.3", @@ -21818,6 +22744,11 @@ "@babel/helper-define-polyfill-provider": "^0.3.3" } }, + "bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==" + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -22171,6 +23102,11 @@ "supports-color": "^7.1.0" } }, + "character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==" + }, "check-more-types": { "version": "2.24.0", "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", @@ -22354,6 +23290,11 @@ "delayed-stream": "~1.0.0" } }, + "comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==" + }, "commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -22729,6 +23670,14 @@ "ms": "2.1.2" } }, + "decode-named-character-reference": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", + "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", + "requires": { + "character-entities": "^2.0.0" + } + }, "deep-equal": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.0.tgz", @@ -22899,6 +23848,11 @@ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "dev": true }, + "diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==" + }, "dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -23870,8 +24824,7 @@ "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, "extract-zip": { "version": "2.0.1", @@ -24627,6 +25580,11 @@ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" }, + "hast-util-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz", + "integrity": "sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==" + }, "hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -24796,6 +25754,11 @@ "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", "dev": true }, + "inline-style-parser": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", + "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==" + }, "internal-slot": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", @@ -24882,6 +25845,11 @@ "has-tostringtag": "^1.0.0" } }, + "is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==" + }, "is-callable": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", @@ -25013,6 +25981,11 @@ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true }, + "is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==" + }, "is-plain-object": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", @@ -25857,6 +26830,89 @@ "unist-util-visit": "^2.0.0" } }, + "mdast-util-from-markdown": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", + "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", + "requires": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "mdast-util-to-string": "^3.1.0", + "micromark": "^3.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-decode-string": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-stringify-position": "^3.0.0", + "uvu": "^0.5.0" + }, + "dependencies": { + "mdast-util-to-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "requires": { + "@types/mdast": "^3.0.0" + } + } + } + }, + "mdast-util-to-hast": { + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-12.3.0.tgz", + "integrity": "sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==", + "requires": { + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "mdast-util-definitions": "^5.0.0", + "micromark-util-sanitize-uri": "^1.1.0", + "trim-lines": "^3.0.0", + "unist-util-generated": "^2.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0" + }, + "dependencies": { + "mdast-util-definitions": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz", + "integrity": "sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA==", + "requires": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "unist-util-visit": "^4.0.0" + } + }, + "unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "requires": { + "@types/unist": "^2.0.0" + } + }, + "unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "requires": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + } + }, + "unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "requires": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + } + } + } + }, "mdast-util-to-string": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-1.1.0.tgz", @@ -25903,6 +26959,217 @@ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" }, + "micromark": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", + "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", + "requires": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "micromark-core-commonmark": "^1.0.1", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "micromark-core-commonmark": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", + "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", + "requires": { + "decode-named-character-reference": "^1.0.0", + "micromark-factory-destination": "^1.0.0", + "micromark-factory-label": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-factory-title": "^1.0.0", + "micromark-factory-whitespace": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-html-tag-name": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "micromark-factory-destination": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", + "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", + "requires": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "micromark-factory-label": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", + "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", + "requires": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "micromark-factory-space": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", + "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", + "requires": { + "micromark-util-character": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "micromark-factory-title": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", + "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", + "requires": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "micromark-factory-whitespace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", + "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", + "requires": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "micromark-util-character": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", + "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", + "requires": { + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "micromark-util-chunked": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", + "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", + "requires": { + "micromark-util-symbol": "^1.0.0" + } + }, + "micromark-util-classify-character": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", + "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", + "requires": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "micromark-util-combine-extensions": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", + "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", + "requires": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "micromark-util-decode-numeric-character-reference": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", + "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", + "requires": { + "micromark-util-symbol": "^1.0.0" + } + }, + "micromark-util-decode-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", + "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", + "requires": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "micromark-util-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", + "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==" + }, + "micromark-util-html-tag-name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", + "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==" + }, + "micromark-util-normalize-identifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", + "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", + "requires": { + "micromark-util-symbol": "^1.0.0" + } + }, + "micromark-util-resolve-all": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", + "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", + "requires": { + "micromark-util-types": "^1.0.0" + } + }, + "micromark-util-sanitize-uri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", + "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", + "requires": { + "micromark-util-character": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "micromark-util-subtokenize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", + "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", + "requires": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "micromark-util-symbol": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", + "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==" + }, + "micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==" + }, "micromatch": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", @@ -26046,8 +27313,7 @@ "mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "dev": true + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==" }, "ms": { "version": "2.1.2", @@ -26680,6 +27946,11 @@ "react-is": "^16.13.1" } }, + "property-information": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.2.0.tgz", + "integrity": "sha512-kma4U7AFCTwpqq5twzC1YVIDXSqg6qQK6JN0smOw8fgRy1OkMi0CYSzFmsy6dnqSenamAtj0CyXMUJ1Mf6oROg==" + }, "proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -27088,6 +28359,67 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "react-markdown": { + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-8.0.7.tgz", + "integrity": "sha512-bvWbzG4MtOU62XqBx3Xx+zB2raaFFsq4mYiAzfjXJMEz2sixgeAfraA3tvzULF02ZdOMUOKTBFFaZJDDrq+BJQ==", + "requires": { + "@types/hast": "^2.0.0", + "@types/prop-types": "^15.0.0", + "@types/unist": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^2.0.0", + "prop-types": "^15.0.0", + "property-information": "^6.0.0", + "react-is": "^18.0.0", + "remark-parse": "^10.0.0", + "remark-rehype": "^10.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-object": "^0.4.0", + "unified": "^10.0.0", + "unist-util-visit": "^4.0.0", + "vfile": "^5.0.0" + }, + "dependencies": { + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, + "space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==" + }, + "unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "requires": { + "@types/unist": "^2.0.0" + } + }, + "unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "requires": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + } + }, + "unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "requires": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + } + } + } + }, "react-player": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/react-player/-/react-player-2.11.0.tgz", @@ -27496,6 +28828,27 @@ "unist-util-visit": "^2.0.0" } }, + "remark-parse": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.2.tgz", + "integrity": "sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==", + "requires": { + "@types/mdast": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "unified": "^10.0.0" + } + }, + "remark-rehype": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-10.1.0.tgz", + "integrity": "sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==", + "requires": { + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "mdast-util-to-hast": "^12.1.0", + "unified": "^10.0.0" + } + }, "remark-slug": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/remark-slug/-/remark-slug-6.1.0.tgz", @@ -27660,6 +29013,14 @@ "tslib": "^2.1.0" } }, + "sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "requires": { + "mri": "^1.1.0" + } + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -27678,11 +29039,6 @@ "loose-envify": "^1.1.0" } }, - "screenfull": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/screenfull/-/screenfull-5.2.0.tgz", - "integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==" - }, "sdp": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/sdp/-/sdp-2.12.0.tgz", @@ -28179,6 +29535,14 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, + "style-to-object": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.1.tgz", + "integrity": "sha512-HFpbb5gr2ypci7Qw+IOhnP2zOU7e77b+rzM+wTzXzfi1PrtBCX0E7Pk4wL4iTLnhzZ+JgEGAhX81ebTg/aYjQw==", + "requires": { + "inline-style-parser": "0.1.1" + } + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -28503,6 +29867,16 @@ "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==" }, + "trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==" + }, + "trough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.1.0.tgz", + "integrity": "sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==" + }, "ts-dedent": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", @@ -28656,6 +30030,20 @@ "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", "dev": true }, + "unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "requires": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + } + }, "unique-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", @@ -28665,12 +30053,33 @@ "crypto-random-string": "^2.0.0" } }, + "unist-util-generated": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.1.tgz", + "integrity": "sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==" + }, "unist-util-is": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==", "dev": true }, + "unist-util-position": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", + "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "requires": { + "@types/unist": "^2.0.0" + } + }, + "unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "requires": { + "@types/unist": "^2.0.0" + } + }, "unist-util-visit": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.3.tgz", @@ -28820,6 +30229,24 @@ "integrity": "sha512-dsNgbLaTrd6l3MMxTtouOCFw4CBFc/3a+GgYA2YyrJvyQ1u6q4pcu3ktLoUZ/VN/Aw9WsauazbgsgdfVWgAKQg==", "dev": true }, + "uvu": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", + "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==", + "requires": { + "dequal": "^2.0.0", + "diff": "^5.0.0", + "kleur": "^4.0.3", + "sade": "^1.7.3" + }, + "dependencies": { + "kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==" + } + } + }, "uzip": { "version": "0.20201231.0", "resolved": "https://registry.npmjs.org/uzip/-/uzip-0.20201231.0.tgz", @@ -28868,6 +30295,26 @@ "extsprintf": "^1.2.0" } }, + "vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "requires": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + } + }, + "vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "requires": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + } + }, "vite": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/vite/-/vite-4.2.1.tgz", diff --git a/package.json b/package.json index 64145215a96..79225302cf8 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "react-google-recaptcha": "^2.1.0", "react-i18next": "^11.18.6", "react-infinite-scroll-component": "^6.1.0", + "react-markdown": "^8.0.7", "react-player": "^2.11.0", "react-qr-reader": "^2.2.1", "react-redux": "^8.0.4", @@ -98,7 +99,6 @@ "redux": "^4.1.0", "redux-thunk": "^2.3.0", "rescript-webapi": "^0.6.1", - "screenfull": "^5.1.0", "use-keyboard-shortcut": "^1.1.6", "uuid": "^8.3.2" }, From 6eeca25d1b559e66759128c94454e2cfb91fe00c Mon Sep 17 00:00:00 2001 From: Bhavik Agarwal <bhavikiitian@gmail.com> Date: Wed, 19 Jul 2023 10:24:39 +0530 Subject: [PATCH 06/17] fix merge develop --- cypress/e2e/assets_spec/assets_manage.cy.ts | 80 +++- cypress/e2e/patient_spec/patient_crud.cy.ts | 8 +- cypress/pageobject/Asset/AssetCreation.ts | 79 ++++ cypress/pageobject/Asset/AssetSearch.ts | 6 + cypress/pageobject/Login/LoginPage.ts | 12 + public/config.json | 15 +- src/Common/hooks/useConfig.ts | 28 +- src/Common/hooks/useFullscreen.ts | 16 +- src/Components/Assets/AssetManage.tsx | 13 +- src/Components/Assets/AssetTypes.tsx | 19 + src/Components/Assets/AssetsList.tsx | 21 +- .../Assets/configure/CameraConfigure.tsx | 2 +- src/Components/Auth/Login.tsx | 66 ++-- src/Components/Common/DateInputV2.tsx | 2 +- src/Components/Common/Sidebar/Sidebar.tsx | 4 +- src/Components/Common/TopBar.tsx | 4 +- src/Components/Common/Uptime.tsx | 350 ++++++++++++++++++ .../CriticalCare__API.tsx | 8 +- src/Components/Facility/AssetCreate.tsx | 1 + .../Facility/ConsultationDetails.tsx | 50 ++- src/Components/Facility/ConsultationForm.tsx | 33 +- .../Facility/Consultations/ABGPlots.tsx | 9 +- .../Facility/Consultations/Beds.tsx | 2 +- .../DailyRounds/LogUpdateCardAttribute.tsx | 2 +- .../Facility/Consultations/DialysisPlots.tsx | 12 +- .../Facility/Consultations/Feed.tsx | 20 +- .../Facility/Consultations/LiveFeed.tsx | 8 +- .../Facility/Consultations/NursingPlot.tsx | 17 +- src/Components/Facility/DischargeModal.tsx | 14 +- src/Components/Facility/TreatmentSummary.tsx | 16 +- src/Components/Facility/models.tsx | 8 +- src/Components/Form/FieldValidators.tsx | 41 +- .../Form/FormFields/Autocomplete.tsx | 38 +- .../Form/FormFields/DateFormField.tsx | 6 +- .../Form/FormFields/PhoneNumberFormField.tsx | 64 +++- src/Components/Form/SelectMenuV2.tsx | 4 +- src/Components/HCX/CreateClaimCard.tsx | 8 +- src/Components/Hub/LiveFeedTile.tsx | 9 +- .../Medicine/CreatePrescriptionForm.tsx | 17 +- .../MedibaseAutocompleteFormField.tsx | 71 ++++ .../Medicine/MedicineAdministrationsTable.tsx | 4 +- .../Medicine/PrescriptionDetailCard.tsx | 2 +- .../Medicine/PrescriptionsTable.tsx | 1 + src/Components/Medicine/models.ts | 15 +- .../Notifications/ShowPushNotification.tsx | 9 +- .../Patient/DailyRoundListDetails.tsx | 33 +- src/Components/Patient/PatientInfoCard.tsx | 11 +- src/Components/Patient/PatientRegister.tsx | 6 +- src/Components/Patient/SamplePreview.tsx | 4 +- src/Components/Patient/models.tsx | 6 + .../Resource/ResourceDetailsUpdate.tsx | 26 +- src/Components/Shifting/BadgesList.tsx | 19 +- src/Components/Shifting/ShiftDetails.tsx | 10 +- src/Components/Users/ManageUsers.tsx | 6 +- src/Components/Users/UserAdd.tsx | 24 +- src/Components/Users/UserFilter.tsx | 4 +- src/Locale/update_locale.js | 18 +- src/Redux/actions.tsx | 15 +- src/Redux/api.tsx | 15 + src/Router/AppRouter.tsx | 4 +- src/style/CAREUI.css | 2 +- 61 files changed, 1069 insertions(+), 348 deletions(-) create mode 100644 cypress/pageobject/Login/LoginPage.ts create mode 100644 src/Components/Common/Uptime.tsx create mode 100644 src/Components/Medicine/MedibaseAutocompleteFormField.tsx diff --git a/cypress/e2e/assets_spec/assets_manage.cy.ts b/cypress/e2e/assets_spec/assets_manage.cy.ts index 464617b3303..27843d7fa85 100644 --- a/cypress/e2e/assets_spec/assets_manage.cy.ts +++ b/cypress/e2e/assets_spec/assets_manage.cy.ts @@ -1,14 +1,18 @@ /// <reference types="cypress" /> import { AssetPage } from "../../pageobject/Asset/AssetCreation"; import { v4 as uuidv4 } from "uuid"; +import LoginPage from "../../pageobject/Login/LoginPage"; +import { AssetSearchPage } from "../../pageobject/Asset/AssetSearch"; describe("Asset", () => { const assetPage = new AssetPage(); + const assetSearchPage = new AssetSearchPage(); + const loginPage = new LoginPage(); const phone_number = "9999999999"; const serialNumber = Math.floor(Math.random() * 10 ** 10).toString(); before(() => { - cy.loginByApi("devdistrictadmin", "Coronasafe@123"); + loginPage.loginAsDisctrictAdmin(); cy.saveLocalStorage(); }); @@ -17,6 +21,18 @@ describe("Asset", () => { cy.awaitUrl("/assets"); }); + it("Verify asset creation fields throws error if empty", () => { + assetPage.createAsset(); + assetPage.selectFacility("Dummy Facility 1"); + assetPage.clickCreateAsset(); + + assetPage.verifyEmptyAssetNameError(); + assetPage.verifyEmptyAssetTypeError(); + assetPage.verifyEmptyLocationError(); + assetPage.verifyEmptyStatusError(); + assetPage.verifyEmptyPhoneError(); + }); + //Create an asset it("Create an Asset", () => { @@ -26,13 +42,13 @@ describe("Asset", () => { assetPage.selectAssetType("Internal"); assetPage.selectAssetClass("ONVIF Camera"); - const qr_id = uuidv4(); + const qr_id_1 = uuidv4(); assetPage.enterAssetDetails( - "New Test Asset", + "New Test Asset 1", "Test Description", "Working", - qr_id, + qr_id_1, "Manufacturer's Name", "2025-12-25", "Customer Support's Name", @@ -44,12 +60,64 @@ describe("Asset", () => { "Test note for asset creation!" ); - assetPage.clickCreateAsset(); + assetPage.clickCreateAddMore(); + assetPage.verifySuccessNotification("Asset created successfully"); + + const qr_id_2 = uuidv4(); + assetPage.selectLocation("Camera Loc"); + assetPage.selectAssetType("Internal"); + assetPage.selectAssetClass("ONVIF Camera"); + assetPage.enterAssetDetails( + "New Test Asset 2", + "Test Description", + "Working", + qr_id_2, + "Manufacturer's Name", + "2025-12-25", + "Customer Support's Name", + phone_number, + "email@support.com", + "Vendor's Name", + serialNumber, + "2021-12-25", + "Test note for asset creation!" + ); + + assetPage.clickCreateAsset(); assetPage.verifySuccessNotification("Asset created successfully"); + + assetSearchPage.typeSearchKeyword("New Test Asset 2"); + assetSearchPage.pressEnter(); + assetSearchPage.verifyAssetIsPresent("New Test Asset 2"); + }); + + it("Edit an Asset", () => { + assetPage.openCreatedAsset(); + + const qr_id = uuidv4(); + + assetPage.editAssetDetails( + "New Test Asset Edited", + "Test Description Edited", + qr_id, + "Manufacturer's Name Edited", + "Customer Support's Name Edited", + "Vendor's Name Edited", + "Test note for asset creation edited!" + ); + + assetPage.clickUpdateAsset(); + + assetPage.verifySuccessNotification("Asset updated successfully"); }); - // Edit an exisit asset + it("Delete an Asset", () => { + assetPage.openCreatedAsset(); + assetPage.deleteAsset(); + + assetPage.verifySuccessNotification("Asset deleted successfully"); + }); afterEach(() => { cy.saveLocalStorage(); diff --git a/cypress/e2e/patient_spec/patient_crud.cy.ts b/cypress/e2e/patient_spec/patient_crud.cy.ts index 53fe7912b32..16ae59e80a6 100644 --- a/cypress/e2e/patient_spec/patient_crud.cy.ts +++ b/cypress/e2e/patient_spec/patient_crud.cy.ts @@ -190,11 +190,11 @@ describe("Patient Creation with consultation", () => { cy.contains("button", "Add Prescription Medication") .should("be.visible") .click(); - cy.get("div#medicine input[placeholder='Select'][role='combobox']") + cy.get("div#medicine_object input[placeholder='Select'][role='combobox']") .click() - .type("paracet"); - cy.get("div#medicine [role='option']") - .contains("PARACETAMOL TAB IP ,500 mg.") + .type("dolo"); + cy.get("div#medicine_object [role='option']") + .contains("DOLO") .should("be.visible") .click(); cy.get("#dosage").click().type("3"); diff --git a/cypress/pageobject/Asset/AssetCreation.ts b/cypress/pageobject/Asset/AssetCreation.ts index 2ccd3e0c367..44841a34a65 100644 --- a/cypress/pageobject/Asset/AssetCreation.ts +++ b/cypress/pageobject/Asset/AssetCreation.ts @@ -77,7 +77,86 @@ export class AssetPage { cy.get("#submit").contains("Create Asset").click(); } + clickCreateAddMore() { + cy.get("[data-testid=create-asset-add-more-button]").click(); + } + verifySuccessNotification(message: string) { cy.verifyNotification(message); } + + openCreatedAsset() { + cy.get("[data-testid=created-asset-list]").first().click(); + } + + editAssetDetails( + name: string, + description: string, + qrId: string, + manufacturer: string, + supportName: string, + vendorName: string, + notes: string + ) { + cy.get("[data-testid=asset-update-button]").click(); + cy.get("[data-testid=asset-name-input] input").clear().type(name); + cy.get("[data-testid=asset-description-input] textarea") + .clear() + .type(description); + cy.get("[data-testid=asset-qr-id-input] input").clear().type(qrId); + cy.get("[data-testid=asset-manufacturer-input] input") + .clear() + .type(manufacturer); + cy.get("[data-testid=asset-support-name-input] input") + .clear() + .type(supportName); + cy.get("[data-testid=asset-vendor-name-input] input") + .clear() + .type(vendorName); + cy.get("[data-testid=asset-notes-input] textarea").clear().type(notes); + } + + clickUpdateAsset() { + cy.get("#submit").contains("Update").click(); + } + + deleteAsset() { + cy.get("[data-testid=asset-delete-button]").click(); + cy.get("#submit").contains("Confirm").click(); + } + + verifyEmptyAssetNameError() { + cy.get("[data-testid=asset-name-input] span").should( + "contain", + "Asset name can't be empty" + ); + } + + verifyEmptyLocationError() { + cy.get("[data-testid=asset-location-input] span").should( + "contain", + "Select a location" + ); + } + + verifyEmptyAssetTypeError() { + cy.get("[data-testid=asset-type-input] span").should( + "contain", + "Select an asset type" + ); + } + + verifyEmptyStatusError() { + cy.get("[data-testid=asset-working-status-input] span").should( + "contain", + "Field is required" + ); + } + + verifyEmptyPhoneError() { + cy.get("#customer-support-phone-div span").should( + "contain", + "Please enter valid phone number" + ); + } } diff --git a/cypress/pageobject/Asset/AssetSearch.ts b/cypress/pageobject/Asset/AssetSearch.ts index 26b9a81c140..315a414a62b 100644 --- a/cypress/pageobject/Asset/AssetSearch.ts +++ b/cypress/pageobject/Asset/AssetSearch.ts @@ -12,4 +12,10 @@ export class AssetSearchPage { expect(currentUrl).not.to.equal(initialUrl); }); } + + verifyAssetIsPresent(assetName: string) { + cy.get("[data-testid=created-asset-list]") + .first() + .should("contain", assetName); + } } diff --git a/cypress/pageobject/Login/LoginPage.ts b/cypress/pageobject/Login/LoginPage.ts new file mode 100644 index 00000000000..e75524ad3f4 --- /dev/null +++ b/cypress/pageobject/Login/LoginPage.ts @@ -0,0 +1,12 @@ +// LoginPage.ts +class LoginPage { + loginAsDisctrictAdmin(): void { + cy.loginByApi("devdistrictadmin", "Coronasafe@123"); + } + + login(username: string, password: string): void { + cy.loginByApi(username, password); + } +} + +export default LoginPage; diff --git a/public/config.json b/public/config.json index 3661597f64b..8bbf94a1d8b 100644 --- a/public/config.json +++ b/public/config.json @@ -4,13 +4,14 @@ "coronasafe_url": "https://coronasafe.network?ref=care", "site_url": "care.coronasafe.in", "analytics_server_url": "https://plausible.10bedicu.in", - "static_header_logo": "https://cdn.coronasafe.network/header_logo.png", - "static_light_logo": "https://cdn.coronasafe.network/light-logo.svg", - "static_ohc_light_logo": "https://cdn.coronasafe.network/ohc_logo_light.png", - "static_ohc_green_logo": "https://cdn.coronasafe.network/ohc_logo_green.png", - "static_black_logo": "https://cdn.coronasafe.network/black-logo.svg", - "static_dpg_white_logo": "https://digitalpublicgoods.net/wp-content/themes/dpga/images/logo-w.svg", - "static_coronasafe_logo": "https://3451063158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-M233b0_JITp4nk0uAFp%2F-M2Dx6gKxOSU45cjfgNX%2F-M2DxFOkMmkPNn0I6U9P%2FCoronasafe-logo.png?alt=media&token=178cc96d-76d9-4e27-9efb-88f3186368e8", + "header_logo": { + "light":"https://cdn.coronasafe.network/header_logo.png", + "dark":"https://cdn.coronasafe.network/header_logo.png" + }, + "main_logo": { + "light":"https://cdn.coronasafe.network/light-logo.svg", + "dark":"https://cdn.coronasafe.network/black-logo.svg" + }, "gmaps_api_key": "AIzaSyDsBAc3y7deI5ZO3NtK5GuzKwtUzQNJNUk", "gov_data_api_key": "579b464db66ec23bdd000001cdd3946e44ce4aad7209ff7b23ac571b", "recaptcha_site_key": "6LdvxuQUAAAAADDWVflgBqyHGfq-xmvNJaToM0pN", diff --git a/src/Common/hooks/useConfig.ts b/src/Common/hooks/useConfig.ts index 7d064ee92ef..f826a06879c 100644 --- a/src/Common/hooks/useConfig.ts +++ b/src/Common/hooks/useConfig.ts @@ -1,22 +1,26 @@ import { useSelector } from "react-redux"; +interface ILogo { + light: string; + dark: string; +} export interface IConfig { dashboard_url: string; github_url: string; coronasafe_url: string; - dpg_url: string; site_url: string; analytics_server_url: string; - static_header_logo: string; - static_ohc_light_logo: string; - static_ohc_green_logo: string; - static_light_logo: string; - static_black_logo: string; + + header_logo: ILogo; + main_logo: ILogo; + /** - * White logo of Digital Public Goods. + * Logo and description for custom deployment. (This overrides the state logo) */ - static_dpg_white_logo: string; - static_coronasafe_logo: string; + custom_logo?: ILogo; + custom_logo_alt?: ILogo; + custom_description?: string; + /** * The API key for the Google Maps API used for location picker. */ @@ -40,11 +44,7 @@ export interface IConfig { /** * If present, the image will be displayed in the login page. */ - state_logo?: string; - /** - * If true, the state logo will be white by applying "invert brightness-0" classes. - */ - state_logo_white?: boolean; + state_logo?: ILogo; /** * URL of the sample format for asset import. */ diff --git a/src/Common/hooks/useFullscreen.ts b/src/Common/hooks/useFullscreen.ts index d6be0c5eeac..e409af1174c 100644 --- a/src/Common/hooks/useFullscreen.ts +++ b/src/Common/hooks/useFullscreen.ts @@ -1,6 +1,9 @@ import { useEffect, useState } from "react"; -export default function useFullscreen(): [boolean, (value: boolean) => void] { +export default function useFullscreen(): [ + boolean, + (value: boolean, element?: HTMLElement) => void +] { const [isFullscreen, _setIsFullscreen] = useState( !!document.fullscreenElement ); @@ -15,12 +18,11 @@ export default function useFullscreen(): [boolean, (value: boolean) => void] { document.removeEventListener("fullscreenchange", onFullscreenChange); }, []); - const setFullscreen = (value: boolean) => { - if (value) { - document.documentElement.requestFullscreen(); - } else { - document.exitFullscreen(); - } + const setFullscreen = (value: boolean, element?: HTMLElement) => { + const fullscreenElement = element ?? document.documentElement; + + if (value) fullscreenElement.requestFullscreen(); + else document.exitFullscreen(); }; return [isFullscreen, setFullscreen]; diff --git a/src/Components/Assets/AssetManage.tsx b/src/Components/Assets/AssetManage.tsx index 9f340b54850..1146cac5960 100644 --- a/src/Components/Assets/AssetManage.tsx +++ b/src/Components/Assets/AssetManage.tsx @@ -26,6 +26,7 @@ const PageTitle = loadable(() => import("../Common/PageTitle")); const Loading = loadable(() => import("../Common/Loading")); import * as Notification from "../../Utils/Notifications.js"; import { NonReadOnlyUsers } from "../../Utils/AuthorizeFor"; +import Uptime from "../Common/Uptime"; interface AssetManageProps { assetId: string; @@ -63,7 +64,7 @@ const AssetManage = (props: AssetManageProps) => { const assetData = await dispatch(getAsset(assetId)); if (!status.aborted) { setIsLoading(false); - if (assetData && assetData.data) { + if (assetData?.data) { setAsset(assetData.data); const transactionFilter = assetData.qr_code_id @@ -77,7 +78,7 @@ const AssetManage = (props: AssetManageProps) => { offset, }) ); - if (transactionsData && transactionsData.data) { + if (transactionsData?.data) { setTransactions(transactionsData.data.results); setTotalCount(transactionsData.data.count); } else { @@ -124,7 +125,7 @@ const AssetManage = (props: AssetManageProps) => { </div> <h2 className="text-center">Print Preview</h2> <div id="section-to-print" className="print flex justify-center"> - <QRCode size={200} value={asset?.id || ""} /> + <QRCode size={200} value={asset?.id ?? ""} /> </div> </div> ); @@ -215,6 +216,9 @@ const AssetManage = (props: AssetManageProps) => { if (asset) { const response = await dispatch(deleteAsset(asset.id)); if (response && response.status === 204) { + Notification.Success({ + msg: "Asset deleted successfully", + }); navigate("/assets"); } } @@ -325,6 +329,7 @@ const AssetManage = (props: AssetManageProps) => { ) } id="update-asset" + data-testid="asset-update-button" authorizeFor={NonReadOnlyUsers} > <CareIcon className="care-l-pen h-4 mr-1" /> @@ -349,6 +354,7 @@ const AssetManage = (props: AssetManageProps) => { authorizeFor={NonReadOnlyUsers} onClick={() => setShowDeleteDialog(true)} variant="danger" + data-testid="asset-delete-button" className="inline-flex" > <CareIcon className="care-l-trash h-4" /> @@ -394,6 +400,7 @@ const AssetManage = (props: AssetManageProps) => { </div> )} </div> + {asset?.id && <Uptime assetId={asset?.id} />} <div className="text-xl font-semibold mt-8 mb-4">Transaction History</div> <div className="align-middle min-w-full overflow-x-auto shadow overflow-hidden sm:rounded-lg"> <table className="min-w-full divide-y divide-gray-200"> diff --git a/src/Components/Assets/AssetTypes.tsx b/src/Components/Assets/AssetTypes.tsx index 2e739a0a03e..9f3ae5e57ad 100644 --- a/src/Components/Assets/AssetTypes.tsx +++ b/src/Components/Assets/AssetTypes.tsx @@ -26,6 +26,13 @@ export enum AssetClass { VENTILATOR = "VENTILATOR", } +export enum AssetStatus { + "not_monitored" = 0, + "operational" = 1, + "down" = 2, + "maintenance" = 3, +} + export const assetClassProps = { ONVIF: { name: "ONVIF Camera", @@ -83,6 +90,18 @@ export interface AssetsResponse { results: AssetData[]; } +export interface AssetUptimeRecord { + id: string; + asset: { + id: string; + name: string; + }; + status: number; + timestamp: string; + created_date: string; + modified_date: string; +} + export interface AssetTransaction { id: string; asset: { diff --git a/src/Components/Assets/AssetsList.tsx b/src/Components/Assets/AssetsList.tsx index c16b2e0a524..88d276b06a2 100644 --- a/src/Components/Assets/AssetsList.tsx +++ b/src/Components/Assets/AssetsList.tsx @@ -6,9 +6,9 @@ import { getAnyFacility, listAssets, getFacilityAssetLocation, + getAsset, } from "../../Redux/actions"; import { assetClassProps, AssetData } from "./AssetTypes"; -import { getAsset } from "../../Redux/actions"; import { useState, useCallback, useEffect } from "react"; import { Link, navigate } from "raviger"; import loadable from "@loadable/component"; @@ -242,8 +242,10 @@ const AssetsList = () => { <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 md:-mx-8 gap-2"> {assets.map((asset: AssetData) => ( <Link + key={asset.id} href={`/facility/${asset?.location_object.facility.id}/assets/${asset.id}`} className="text-inherit" + data-testid="created-asset-list" > <div key={asset.id} @@ -262,7 +264,12 @@ const AssetsList = () => { } text-2xl`} /> </span> - <p className="truncate w-48">{asset.name}</p> + <p + className="truncate w-48" + data-testid="created-asset-list-name" + > + {asset.name} + </p> </p> </div> <p className="font-normal text-sm"> @@ -395,12 +402,12 @@ const AssetsList = () => { <> <FilterBadges badges={({ badge, value }) => [ - value("Facility", "facility", facilityName || ""), + value("Facility", "facility", facilityName ?? ""), badge("Name/Serial No./QR ID", "search"), - value("Asset Type", "asset_type", asset_type || ""), - value("Asset Class", "asset_class", asset_class || ""), - value("Status", "status", status?.replace(/_/g, " ") || ""), - value("Location", "location", locationName || ""), + value("Asset Type", "asset_type", asset_type ?? ""), + value("Asset Class", "asset_class", asset_class ?? ""), + value("Status", "status", status?.replace(/_/g, " ") ?? ""), + value("Location", "location", locationName ?? ""), ]} /> <div className="grow"> diff --git a/src/Components/Assets/configure/CameraConfigure.tsx b/src/Components/Assets/configure/CameraConfigure.tsx index ca3389c0c18..d8d9d66fec5 100644 --- a/src/Components/Assets/configure/CameraConfigure.tsx +++ b/src/Components/Assets/configure/CameraConfigure.tsx @@ -57,7 +57,7 @@ export default function CameraConfigure(props: CameraConfigureProps) { id="location" type="text" value={newPreset} - className="mt-2" + className="mt-1" onChange={(e) => setNewPreset(e.value)} error="" /> diff --git a/src/Components/Auth/Login.tsx b/src/Components/Auth/Login.tsx index f7e6a37b8d3..1747de569de 100644 --- a/src/Components/Auth/Login.tsx +++ b/src/Components/Auth/Login.tsx @@ -9,22 +9,20 @@ import LegendInput from "../../CAREUI/interactive/LegendInput"; import LanguageSelectorLogin from "../Common/LanguageSelectorLogin"; import CareIcon from "../../CAREUI/icons/CareIcon"; import useConfig from "../../Common/hooks/useConfig"; -import { classNames } from "../../Utils/utils"; import CircularProgress from "../Common/components/CircularProgress"; import { LocalStorageKeys } from "../../Common/constants"; +import ReactMarkdown from "react-markdown"; export const Login = (props: { forgot?: boolean }) => { const { - static_black_logo, - static_dpg_white_logo, - static_light_logo, - static_ohc_light_logo, + main_logo, recaptcha_site_key, github_url, coronasafe_url, - dpg_url, state_logo, - state_logo_white, + custom_logo, + custom_logo_alt, + custom_description, } = useConfig(); const dispatch: any = useDispatch(); const initForm: any = { @@ -184,14 +182,11 @@ export const Login = (props: { forgot?: boolean }) => { <div></div> <div className="mt-4 md:mt-12 rounded-lg py-4 flex flex-col items-start"> <div className="hidden md:flex items-center gap-6 mb-4"> - {state_logo && ( + {(custom_logo || state_logo) && ( <> <img - src={state_logo} - className={classNames( - "rounded-lg p-3 h-24", - state_logo_white && "invert brightness-0" - )} + src={custom_logo?.light ?? state_logo?.light} + className="rounded-lg py-3 h-16" alt="state logo" /> <div className="w-0.5 bg-white/50 h-10 rounded-full" /> @@ -204,27 +199,39 @@ export const Login = (props: { forgot?: boolean }) => { rel="noopener noreferrer" > <img - src={static_light_logo} + src={custom_logo_alt?.light ?? main_logo.light} className="h-8" alt="coronasafe logo" /> </a> </div> <div className="max-w-lg"> - <h1 className="text-4xl md:text-5xl lg:text-7xl font-black text-white leading-tight tracking-wider"> + <h1 className="text-4xl lg:text-5xl font-black text-white leading-tight tracking-wider"> {t("care")} </h1> - <div className="text-base md:text-lg lg:text-xl font-semibold py-6 max-w-xl text-gray-400 pl-1"> - {t("goal")} - </div> + {custom_description ? ( + <div className="py-6"> + <ReactMarkdown className="max-w-xl text-gray-400"> + {custom_description || t("goal")} + </ReactMarkdown> + </div> + ) : ( + <div className="text-base md:text-lg lg:text-xl font-semibold py-6 max-w-xl text-gray-400 pl-1"> + {t("goal")} + </div> + )} </div> </div> <div className="flex items-center mb-6"> <div className="text-xs md:text-sm max-w-lg"> <div className="ml-1 flex items-center gap-4 mb-2"> - <a href={dpg_url} rel="noopener noreferrer" target="_blank"> + <a + href="https://digitalpublicgoods.net/registry/coronasafe-care.html" + rel="noopener noreferrer" + target="_blank" + > <img - src={static_dpg_white_logo} + src="https://digitalpublicgoods.net/wp-content/themes/dpga/images/logo-w.svg" className="h-12" alt="Logo of Digital Public Goods Alliance" /> @@ -236,7 +243,7 @@ export const Login = (props: { forgot?: boolean }) => { target="_blank" > <img - src={static_ohc_light_logo} + src="https://cdn.coronasafe.network/ohc_logo_light.png" className="h-10 inline-block" alt="coronasafe logo" /> @@ -275,22 +282,19 @@ export const Login = (props: { forgot?: boolean }) => { } > <div className="flex items-center gap-1"> - {state_logo && ( + {(custom_logo || state_logo) && ( <> <img - src={state_logo} - className={classNames( - "rounded-lg p-3 h-24 md:hidden", - state_logo_white && "invert brightness-0" - )} + src={custom_logo?.dark ?? state_logo?.dark} + className="rounded-lg py-3 h-14 md:hidden" alt="state logo" /> <div className="mx-4 w-[1px] md:hidden bg-gray-600 h-8 rounded-full" /> </> )} <img - src={static_black_logo} - className="h-8 w-auto md:hidden brightness-0 contrast-[0%]" + src={custom_logo_alt?.dark ?? main_logo.dark} + className="h-8 w-auto md:hidden" alt="care logo" /> </div>{" "} @@ -371,8 +375,8 @@ export const Login = (props: { forgot?: boolean }) => { } > <img - src={static_black_logo} - className="h-8 w-auto mb-4 md:hidden brightness-0 contrast-[0%]" + src={main_logo.dark} + className="h-8 w-auto mb-4 md:hidden" alt="care logo" />{" "} <button diff --git a/src/Components/Common/DateInputV2.tsx b/src/Components/Common/DateInputV2.tsx index 6a81f2e917d..97395c23c6c 100644 --- a/src/Components/Common/DateInputV2.tsx +++ b/src/Components/Common/DateInputV2.tsx @@ -227,7 +227,7 @@ const DateInputV2: React.FC<Props> = ({ readOnly disabled={disabled} className={`cui-input-base cursor-pointer disabled:cursor-not-allowed ${className}`} - placeholder={placeholder || "Select date"} + placeholder={placeholder ?? "Select date"} value={value && format(value, "yyyy-MM-dd")} /> <div className="absolute top-1/2 right-0 p-2 -translate-y-1/2"> diff --git a/src/Components/Common/Sidebar/Sidebar.tsx b/src/Components/Common/Sidebar/Sidebar.tsx index 812348fb601..51398f21f42 100644 --- a/src/Components/Common/Sidebar/Sidebar.tsx +++ b/src/Components/Common/Sidebar/Sidebar.tsx @@ -49,7 +49,7 @@ const StatelessSidebar = ({ setShrinked, onItemClick, }: StatelessSidebarProps) => { - const { static_light_logo } = useConfig(); + const { main_logo } = useConfig(); const activeLink = useActiveLink(); const Item = shrinked ? ShrinkedSidebarItem : SidebarItem; const { dashboard_url } = useConfig(); @@ -130,7 +130,7 @@ const StatelessSidebar = ({ className={`${ shrinked ? "mx-auto" : "ml-5" } h-5 md:h-8 self-start transition mb-2 md:mb-5`} - src={shrinked ? LOGO_COLLAPSE : static_light_logo} + src={shrinked ? LOGO_COLLAPSE : main_logo.light} /> </Link> <div className="h-3" /> {/* flexible spacing */} diff --git a/src/Components/Common/TopBar.tsx b/src/Components/Common/TopBar.tsx index 0a1974df5b9..47fe8008309 100644 --- a/src/Components/Common/TopBar.tsx +++ b/src/Components/Common/TopBar.tsx @@ -3,14 +3,14 @@ import useConfig from "../../Common/hooks/useConfig"; import LanguageSelector from "./LanguageSelector"; const TopBar = () => { - const { static_black_logo } = useConfig(); + const { main_logo } = useConfig(); return ( <div className="bg-white shadow-md"> <div className="max-w-6xl mx-auto py-4 px-2 flex items-center justify-between"> <div> <a href={"/"}> <img - src={static_black_logo} + src={main_logo.dark} style={{ height: "25px" }} alt="care logo" /> diff --git a/src/Components/Common/Uptime.tsx b/src/Components/Common/Uptime.tsx new file mode 100644 index 00000000000..f07c1e0a71c --- /dev/null +++ b/src/Components/Common/Uptime.tsx @@ -0,0 +1,350 @@ +import { Popover } from "@headlessui/react"; +import moment from "moment"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { listAssetAvailability } from "../../Redux/actions"; +import { useDispatch } from "react-redux"; +import * as Notification from "../../Utils/Notifications.js"; +import { AssetStatus, AssetUptimeRecord } from "../Assets/AssetTypes"; + +const STATUS_COLORS = { + operational: "bg-green-500", + not_monitored: "bg-gray-400", + down: "bg-red-500", + maintenance: "bg-blue-500", +}; + +const STATUS_COLORS_TEXT = { + operational: "text-green-500", + not_monitored: "text-gray-400", + down: "text-red-500", + maintenance: "text-blue-500", +}; + +const now = moment(); +const formatDateBeforeDays = Array.from({ length: 100 }, (_, index) => + now.clone().subtract(index, "days").format("Do MMMM YYYY") +); + +export default function Uptime(props: { assetId: string }) { + const [summary, setSummary] = useState<{ + [key: number]: AssetUptimeRecord[]; + }>([]); + const [loading, setLoading] = useState(true); + const graphElem = useRef<HTMLDivElement>(null); + const [numDays, setNumDays] = useState( + Math.floor((window.innerWidth - 1024) / 20) + ); + const [hoveredDay, setHoveredDay] = useState(-1); + const dispatch = useDispatch<any>(); + + function StatusPopover({ + records, + day, + date, + numDays, + }: { + records: AssetUptimeRecord[]; + day: number; + date: string; + numDays: number; + }) { + const incidents = + records?.filter( + (record) => + record.status !== AssetStatus.operational && + record.status !== AssetStatus.not_monitored + ) || []; + + return ( + <Popover className="mt-10 relative"> + <Popover.Panel + className={`absolute z-50 w-80 transform px-4 sm:px-0 ${ + day > numDays - 7 + ? "-translate-x-6" + : day < 4 + ? "-translate-x-full" + : "-translate-x-1/2" + }`} + static + > + <div className="rounded-lg shadow-lg ring-1 ring-gray-400"> + <div className="rounded-lg bg-white px-6 py-4"> + <div className="flow-root rounded-md"> + <div className="block text-sm text-gray-800 text-center"> + <span className="font-bold ">{date}</span> + <div className="border-t border-gray-200 my-2"></div> + {incidents.length === 0 ? ( + <span>No incidents for the day</span> + ) : ( + <> + <span className="font-bold ">Incidents</span> + {incidents.map((incident, index) => { + const nextIncident = incidents[index + 1]; + const endTimestamp = nextIncident + ? moment(nextIncident.timestamp).format("h:mmA") + : moment().format("h:mmA"); + const duration = nextIncident + ? moment + .duration( + moment(endTimestamp).diff( + moment(incident.timestamp) + ) + ) + .humanize() + : "Ongoing"; + + return ( + <div className="flex justify-between" key={index}> + <span + className={`capitalize ${ + STATUS_COLORS_TEXT[ + AssetStatus[ + incident.status + ] as keyof typeof STATUS_COLORS_TEXT + ] + }`} + > + {AssetStatus[incident.status]} + </span> + <span> + {moment(incident.timestamp).format("h:mmA")} -{" "} + {endTimestamp} + </span> + <span>{duration}</span> + </div> + ); + })} + <div className="border-t border-gray-200 my-2"></div> + <div className="flex justify-between mt-1"> + <span className="font-bold">Total</span> + <span> + {incidents.length === 0 + ? "Ongoing" + : moment + .duration( + incidents.reduce( + (totalDuration, incident) => + totalDuration + + moment().diff(moment(incident.timestamp)), + 0 + ) + ) + .humanize()} + </span> + </div> + </> + )} + </div> + </div> + </div> + </div> + </Popover.Panel> + </Popover> + ); + } + + const handleResize = () => { + const containerWidth = graphElem.current?.clientWidth ?? window.innerWidth; + const newNumDays = Math.floor(containerWidth / 20); + setNumDays(newNumDays); + }; + + const setUptimeRecord = (records: AssetUptimeRecord[]): void => { + const recordsByDayBefore: { [key: number]: AssetUptimeRecord[] } = {}; + records.forEach((record) => { + const timestamp = moment(record.timestamp); + const today = moment(); + const diffDays = today.diff(timestamp, "days"); + if (diffDays < 100) { + recordsByDayBefore[diffDays] = recordsByDayBefore[diffDays] + ? [...recordsByDayBefore[diffDays], record] + : [record]; + } + }); + + setSummary(recordsByDayBefore); + }; + + function getUptimePercent(totalDays: number) { + let upStatus = 0; + + for (let i = 0; i < totalDays; i++) { + const index = numDays - i - 1; + const dayRecords = summary[index]; + if ( + dayRecords && + dayRecords[dayRecords.length - 1].status === AssetStatus.operational + ) { + upStatus++; + } + } + + return Math.round((upStatus / totalDays) * 100); + } + + const fetchData = useCallback(async () => { + setLoading(true); + setLoading(false); + + const availabilityData = await dispatch( + listAssetAvailability({ + external_id: props.assetId, + }) + ); + if (availabilityData?.data) { + setUptimeRecord(availabilityData.data.results); + } else { + Notification.Error({ + msg: "Error fetching availability history", + }); + } + }, [dispatch, props.assetId]); + + useEffect(() => { + setTimeout(() => { + handleResize(); + }, 100); + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + }, [graphElem, loading]); + + useEffect(() => { + handleResize(); + fetchData(); + }, [props.assetId, fetchData]); + + const getStatusColor = (day: number) => { + if (summary[day]) { + const dayRecords = summary[day]; + const statusColors = []; + for (let i = 0; i < 3; i++) { + const start = moment() + .startOf("day") + .add(i * 8, "hours"); + const end = moment() + .startOf("day") + .add((i + 1) * 8, "hours"); + const recordsInPeriod = dayRecords.filter((record) => + moment(record.timestamp).isBetween(start, end) + ); + if ( + recordsInPeriod.some( + (record) => AssetStatus[record.status] === "down" + ) + ) { + statusColors.push(STATUS_COLORS["down"]); + } else if ( + recordsInPeriod.some( + (record) => AssetStatus[record.status] === "maintenance" + ) + ) { + statusColors.push(STATUS_COLORS["maintenance"]); + } else if ( + recordsInPeriod.some( + (record) => AssetStatus[record.status] === "operational" + ) + ) { + statusColors.push(STATUS_COLORS["operational"]); + } else { + statusColors.push(STATUS_COLORS["not_monitored"]); + } + } + return statusColors; + } else { + return [ + STATUS_COLORS["not_monitored"], + STATUS_COLORS["not_monitored"], + STATUS_COLORS["not_monitored"], + ]; + } + }; + if (loading) { + return ( + <div className="mt-8 flex flex-col bg-white w-full sm:rounded-lg shadow-sm p-4"> + <p>Loading status...</p> + </div> + ); + } else if (summary) { + return ( + <div className="mt-8 flex flex-col bg-white w-full sm:rounded-lg shadow-sm p-4"> + <div className="mx-2 w-full"> + <div className="grid grid-cols-1"> + <div className="text-xl font-semibold">Availability History</div> + <div> + <div className="mt-2 px-5 overflow-x-clip"> + <div className="flex text-gray-700 text-xs mt-2 opacity-70 justify-center mb-1"> + {getUptimePercent(numDays)}% uptime + </div> + <div + className="flex flex-row" + ref={graphElem} + onMouseLeave={() => setHoveredDay(-1)} + > + {Array.from({ length: numDays }, (_, revIndex) => { + const index = numDays - revIndex - 1; + const dayStatus = getStatusColor(index); + return ( + <> + <span + onMouseEnter={() => setHoveredDay(index)} + key={index} + className="h-8 w-3 flex-1 mx-1" + > + <div + className={`h-[11px] w-3 rounded-t-sm ${ + hoveredDay === index + ? "bg-gray-700" + : dayStatus[0] + }`} + ></div> + <div + className={`h-[11px] w-3 ${ + hoveredDay === index + ? "bg-gray-700" + : dayStatus[1] + }`} + ></div> + <div + className={`h-[11px] w-3 rounded-b-sm ${ + hoveredDay === index + ? "bg-gray-700" + : dayStatus[2] + }`} + ></div> + </span> + {hoveredDay === index && ( + <> + <StatusPopover + records={summary[index]} + day={index} + date={formatDateBeforeDays[index]} + numDays={numDays} + /> + </> + )} + </> + ); + })} + </div> + <div + className={`flex text-gray-700 text-xs opacity-70 ${ + hoveredDay == -1 && "mt-2" + }`} + > + <span className="ml-0 mr-auto">{numDays} days ago</span> + <span className="mr-0 ml-auto">Today</span> + </div> + </div> + </div> + </div> + </div> + </div> + ); + } else { + return ( + <div className="mt-8 flex flex-col bg-white w-full sm:rounded-lg shadow-sm p-4"> + <p>No status information available.</p> + </div> + ); + } +} diff --git a/src/Components/CriticalCareRecording/CriticalCare__API.tsx b/src/Components/CriticalCareRecording/CriticalCare__API.tsx index a1bbdfbc9e6..48b58faa167 100644 --- a/src/Components/CriticalCareRecording/CriticalCare__API.tsx +++ b/src/Components/CriticalCareRecording/CriticalCare__API.tsx @@ -3,8 +3,8 @@ import { fireRequestV2 } from "../../Redux/fireRequest"; export const loadDailyRound = ( consultationId: string, id: string, - successCB: any = () => {}, - errorCB: any = () => {} + successCB: any = () => null, + errorCB: any = () => null ) => { fireRequestV2("getDailyReport", [], {}, successCB, errorCB, { consultationId, @@ -16,8 +16,8 @@ export const updateDailyRound = ( consultationId: string, id: string, params: object, - successCB: any = () => {}, - errorCB: any = () => {} + successCB: any = () => null, + errorCB: any = () => null ) => { fireRequestV2("updateDailyRound", [], params, successCB, errorCB, { consultationId, diff --git a/src/Components/Facility/AssetCreate.tsx b/src/Components/Facility/AssetCreate.tsx index f8c2c5fce19..96ba51ad73d 100644 --- a/src/Components/Facility/AssetCreate.tsx +++ b/src/Components/Facility/AssetCreate.tsx @@ -914,6 +914,7 @@ const AssetCreate = (props: AssetProps) => { /> {!assetId && ( <Submit + data-testid="create-asset-add-more-button" onClick={(e) => handleSubmit(e, true)} label="Create & Add More" /> diff --git a/src/Components/Facility/ConsultationDetails.tsx b/src/Components/Facility/ConsultationDetails.tsx index acdae779bab..06681cc7e10 100644 --- a/src/Components/Facility/ConsultationDetails.tsx +++ b/src/Components/Facility/ConsultationDetails.tsx @@ -84,11 +84,7 @@ export const ConsultationDetails = (props: any) => { `${patientData.address},\n${patientData.ward_object?.name},\n${patientData.local_body_object?.name},\n${patientData.district_object?.name},\n${patientData.state_object?.name}`; const getPatientComorbidities = (patientData: any) => { - if ( - patientData && - patientData.medical_history && - patientData.medical_history.length - ) { + if (patientData?.medical_history?.length) { const medHis = patientData.medical_history; return medHis.map((item: any) => item.disease).join(", "); } else { @@ -158,12 +154,12 @@ export const ConsultationDetails = (props: any) => { setIsLoading(true); const res = await dispatch(getConsultation(consultationId)); if (!status.aborted) { - if (res && res.data) { + if (res?.data) { const data: ConsultationModel = { ...res.data, symptoms_text: "", }; - if (res.data.symptoms && res.data.symptoms.length) { + if (res.data.symptoms?.length) { const symptoms = res.data.symptoms .filter((symptom: number) => symptom !== 9) .map((symptom: number) => { @@ -175,7 +171,7 @@ export const ConsultationDetails = (props: any) => { setConsultationData(data); const id = res.data.patient; const patientRes = await dispatch(getPatient({ id })); - if (patientRes && patientRes.data) { + if (patientRes?.data) { const patientGender = getPatientGender(patientRes.data); const patientAddress = getPatientAddress(patientRes.data); const patientComorbidities = getPatientComorbidities( @@ -355,7 +351,7 @@ export const ConsultationDetails = (props: any) => { {consultationData.admitted_to} </span> </div> - {(consultationData.admission_date || + {(consultationData.admission_date ?? consultationData.discharge_date) && ( <div className="text-3xl font-bold"> {moment( @@ -404,7 +400,7 @@ export const ConsultationDetails = (props: any) => { }, ] : []), - ...(consultationData?.icd11_diagnoses_object || []), + ...(consultationData?.icd11_diagnoses_object ?? []), ]} label="Diagnosis (as per ICD-11 recommended by WHO)" /> @@ -480,7 +476,7 @@ export const ConsultationDetails = (props: any) => { {CONSULTATION_TABS.map((p: OptionsType) => { if (p.text === "FEED") { if ( - !consultationData?.current_bed?.bed_object?.id || + !consultationData?.current_bed?.bed_object?.id ?? consultationData?.discharge_date !== null ) return null; @@ -563,7 +559,7 @@ export const ConsultationDetails = (props: any) => { {DISCHARGE_REASONS.find( (d) => d.id === consultationData.discharge_reason - )?.text || "--"} + )?.text ?? "--"} </span> </div> {consultationData.discharge_reason === "REC" && ( @@ -582,12 +578,12 @@ export const ConsultationDetails = (props: any) => { <div> Advice {" - "} <span className="font-semibold"> - {consultationData.discharge_notes || "--"} + {consultationData.discharge_notes ?? "--"} </span> </div> <div className="overflow-x-auto overflow-y-hidden"> <PrescriptionsTable - consultation_id={consultationData.id} + consultation_id={consultationData.id ?? ""} is_prn={false} readonly prescription_type="DISCHARGE" @@ -596,7 +592,7 @@ export const ConsultationDetails = (props: any) => { <hr className="border border-gray-300 my-2"></hr> <div className="overflow-x-auto overflow-y-hidden"> <PrescriptionsTable - consultation_id={consultationData.id} + consultation_id={consultationData.id ?? ""} is_prn readonly prescription_type="DISCHARGE" @@ -619,20 +615,20 @@ export const ConsultationDetails = (props: any) => { <div> Cause of death {" - "} <span className="font-semibold"> - {consultationData.discharge_notes || "--"} + {consultationData.discharge_notes ?? "--"} </span> </div> <div> Confirmed By {" - "} <span className="font-semibold"> - {consultationData.death_confirmed_doctor || + {consultationData.death_confirmed_doctor ?? "--"} </span> </div> </div> )} {["REF", "LAMA"].includes( - consultationData.discharge_reason || "" + consultationData.discharge_reason ?? "" ) && ( <div className="grid gap-4"> <div> @@ -649,7 +645,7 @@ export const ConsultationDetails = (props: any) => { <div> Notes {" - "} <span className="font-semibold"> - {consultationData.discharge_notes || "--"} + {consultationData.discharge_notes ?? "--"} </span> </div> </div> @@ -679,7 +675,7 @@ export const ConsultationDetails = (props: any) => { text={ SYMPTOM_CHOICES.find( (choice) => choice.id === symptom - )?.text || "Err. Unknown" + )?.text ?? "Err. Unknown" } color={"primary"} size={"small"} @@ -719,7 +715,7 @@ export const ConsultationDetails = (props: any) => { text={ SYMPTOM_CHOICES.find( (choice) => choice.id === symptom - )?.text || "Err. Unknown" + )?.text ?? "Err. Unknown" } color={"primary"} size={"small"} @@ -810,7 +806,7 @@ export const ConsultationDetails = (props: any) => { </div> )} - {(consultationData.operation || + {(consultationData.operation ?? consultationData.special_instruction) && ( <div className="bg-white overflow-hidden shadow rounded-lg"> <div className="px-4 py-5 sm:p-6"> @@ -971,25 +967,25 @@ export const ConsultationDetails = (props: any) => { <div> Gender {" - "} <span className="font-semibold"> - {patientData.gender || "-"} + {patientData.gender ?? "-"} </span> </div> <div> Age {" - "} <span className="font-semibold"> - {patientData.age || "-"} + {patientData.age ?? "-"} </span> </div> <div> Weight {" - "} <span className="font-semibold"> - {consultationData.weight || "-"} Kg + {consultationData.weight ?? "-"} Kg </span> </div> <div> Height {" - "} <span className="font-semibold"> - {consultationData.height || "-"} cm + {consultationData.height ?? "-"} cm </span> </div> <div> @@ -1006,7 +1002,7 @@ export const ConsultationDetails = (props: any) => { <div> Blood Group {" - "} <span className="font-semibold"> - {patientData.blood_group || "-"} + {patientData.blood_group ?? "-"} </span> </div> </div> diff --git a/src/Components/Facility/ConsultationForm.tsx b/src/Components/Facility/ConsultationForm.tsx index fa141aae059..cf187561df1 100644 --- a/src/Components/Facility/ConsultationForm.tsx +++ b/src/Components/Facility/ConsultationForm.tsx @@ -16,14 +16,14 @@ import { getConsultation, updateConsultation, getPatient, + dischargePatient, } from "../../Redux/actions"; import * as Notification from "../../Utils/Notifications.js"; import { FacilitySelect } from "../Common/FacilitySelect"; -import { BedModel, FacilityModel } from "./models"; +import { BedModel, FacilityModel, ICD11DiagnosisModel } from "./models"; import { OnlineUsersSelect } from "../Common/OnlineUsersSelect"; import { UserModel } from "../Users/models"; import { BedSelect } from "../Common/BedSelect"; -import { dischargePatient } from "../../Redux/actions"; import Beds from "./Consultations/Beds"; import InvestigationBuilder, { InvestigationType, @@ -31,7 +31,6 @@ import InvestigationBuilder, { import ProcedureBuilder, { ProcedureType, } from "../Common/prescription-builder/ProcedureBuilder"; -import { ICD11DiagnosisModel } from "./models"; import { Cancel, Submit } from "../Common/components/ButtonV2"; import TextAreaFormField from "../Form/FormFields/TextAreaFormField"; import { FieldChangeEventHandler } from "../Form/FormFields/Utils"; @@ -47,11 +46,8 @@ import useAppHistory from "../../Common/hooks/useAppHistory"; import useVisibility from "../../Utils/useVisibility"; import CareIcon from "../../CAREUI/icons/CareIcon"; import CheckBoxFormField from "../Form/FormFields/CheckBoxFormField"; -import { - DraftSection, - FormReducerAction, - useAutoSaveReducer, -} from "../../Utils/AutoSave"; +import { DraftSection, useAutoSaveReducer } from "../../Utils/AutoSave"; +import { FormAction } from "../Form/Utils"; const Loading = loadable(() => import("../Common/Loading")); const PageTitle = loadable(() => import("../Common/PageTitle")); @@ -171,10 +167,7 @@ const fieldRef = formErrorKeys.reduce( {} ); -const consultationFormReducer = ( - state = initialState, - action: FormReducerAction -) => { +const consultationFormReducer = (state = initialState, action: FormAction) => { switch (action.type) { case "set_form": { return { @@ -301,7 +294,7 @@ export const ConsultationForm = (props: any) => { else setSelectedFacility(res.data.referred_to_object); } if (!status.aborted) { - if (res && res.data) { + if (res?.data) { const formData = { ...res.data, symptoms_onset_date: isoStringToDate(res.data.symptoms_onset_date), @@ -310,7 +303,7 @@ export const ConsultationForm = (props: any) => { admitted_to: res.data.admitted_to ? res.data.admitted_to : "", category: res.data.category ? PATIENT_CATEGORIES.find((i) => i.text === res.data.category) - ?.id || "Comfort" + ?.id ?? "Comfort" : "Comfort", ip_no: res.data.ip_no ? res.data.ip_no : "", op_no: res.data.op_no ? res.data.op_no : "", @@ -343,11 +336,7 @@ export const ConsultationForm = (props: any) => { useAbortableEffect( (status: statusType) => { - if (id && patientId && patientName) { - fetchData(status); - } else if (id && !patientId) { - fetchData(status); - } + if (id && ((patientId && patientName) || !patientId)) fetchData(status); }, [fetchData, id, patientId, patientName] ); @@ -640,7 +629,7 @@ export const ConsultationForm = (props: any) => { id ? updateConsultation(id, data) : createConsultation(data) ); setIsLoading(false); - if (res && res.data && res.status !== 400) { + if (res?.data && res.status !== 400) { dispatch({ type: "set_form", form: initForm }); if (data.suggestion === "DD") { @@ -755,9 +744,9 @@ export const ConsultationForm = (props: any) => { const selectedFacility = selected as FacilityModel; setSelectedFacility(selectedFacility); const form: FormDetails = { ...state.form }; - if (selectedFacility && selectedFacility.id) { + if (selectedFacility?.id) { if (selectedFacility.id === -1) { - form.referred_to_external = selectedFacility.name || ""; + form.referred_to_external = selectedFacility.name ?? ""; delete form.referred_to; } else { form.referred_to = selectedFacility.id.toString() || ""; diff --git a/src/Components/Facility/Consultations/ABGPlots.tsx b/src/Components/Facility/Consultations/ABGPlots.tsx index 280bd0acf36..8d534c1413c 100644 --- a/src/Components/Facility/Consultations/ABGPlots.tsx +++ b/src/Components/Facility/Consultations/ABGPlots.tsx @@ -8,16 +8,14 @@ import { PAGINATION_LIMIT } from "../../../Common/constants"; import { formatDate } from "../../../Utils/utils"; export const ABGPlots = (props: any) => { - const { facilityId, patientId, consultationId } = props; + const { consultationId } = props; const dispatch: any = useDispatch(); - const [isLoading, setIsLoading] = useState(false); const [results, setResults] = useState({}); const [currentPage, setCurrentPage] = useState(1); const [totalCount, setTotalCount] = useState(0); const fetchDailyRounds = useCallback( async (status: statusType) => { - setIsLoading(true); const res = await dispatch( dailyRoundsAnalyse( { @@ -38,11 +36,10 @@ export const ABGPlots = (props: any) => { ) ); if (!status.aborted) { - if (res && res.data) { + if (res?.data) { setResults(res.data.results); setTotalCount(res.data.count); } - setIsLoading(false); } }, [consultationId, dispatch, currentPage] @@ -55,7 +52,7 @@ export const ABGPlots = (props: any) => { [currentPage] ); - const handlePagination = (page: number, limit: number) => { + const handlePagination = (page: number, _limit: number) => { setCurrentPage(page); }; diff --git a/src/Components/Facility/Consultations/Beds.tsx b/src/Components/Facility/Consultations/Beds.tsx index ab472cf514e..ac6fa22fc5b 100644 --- a/src/Components/Facility/Consultations/Beds.tsx +++ b/src/Components/Facility/Consultations/Beds.tsx @@ -31,7 +31,7 @@ const formatDateTime: () => string = () => { interface BedsProps { facilityId: string; patientId: number; - consultationId: number; + consultationId: string; smallLoader?: boolean; discharged?: boolean; setState?: Dispatch<SetStateAction<boolean>>; diff --git a/src/Components/Facility/Consultations/DailyRounds/LogUpdateCardAttribute.tsx b/src/Components/Facility/Consultations/DailyRounds/LogUpdateCardAttribute.tsx index 3ee16b09618..72ca01b3fb3 100644 --- a/src/Components/Facility/Consultations/DailyRounds/LogUpdateCardAttribute.tsx +++ b/src/Components/Facility/Consultations/DailyRounds/LogUpdateCardAttribute.tsx @@ -48,7 +48,7 @@ const LogUpdateCardAttribute = <T extends keyof DailyRoundsModel>({ <AttributeLabel attributeKey={attributeKey} /> <span className="flex gap-x-2 gap-y-1 flex-wrap text-sm text-gray-700"> {attributeValue.map((output: any) => ( - <span className="font-semibold"> + <span className="font-semibold" key={output.name}> {output.name}: {output.quantity} </span> ))} diff --git a/src/Components/Facility/Consultations/DialysisPlots.tsx b/src/Components/Facility/Consultations/DialysisPlots.tsx index 1b08420c2c7..392235f8929 100644 --- a/src/Components/Facility/Consultations/DialysisPlots.tsx +++ b/src/Components/Facility/Consultations/DialysisPlots.tsx @@ -8,17 +8,14 @@ import { PAGINATION_LIMIT } from "../../../Common/constants"; import { formatDate } from "../../../Utils/utils"; export const DialysisPlots = (props: any) => { - const { facilityId, patientId, consultationId } = props; + const { consultationId } = props; const dispatch: any = useDispatch(); - const [isLoading, setIsLoading] = useState(false); - const [offset, setOffset] = useState(0); const [results, setResults] = useState({}); const [currentPage, setCurrentPage] = useState(1); const [totalCount, setTotalCount] = useState(0); const fetchDailyRounds = useCallback( async (status: statusType) => { - setIsLoading(true); const res = await dispatch( dailyRoundsAnalyse( { @@ -29,11 +26,10 @@ export const DialysisPlots = (props: any) => { ) ); if (!status.aborted) { - if (res && res.data) { + if (res?.data) { setTotalCount(res.data.count); setResults(res.data.results); } - setIsLoading(false); } }, [consultationId, dispatch, currentPage] @@ -46,10 +42,8 @@ export const DialysisPlots = (props: any) => { [consultationId, currentPage] ); - const handlePagination = (page: number, limit: number) => { - const offset = (page - 1) * limit; + const handlePagination = (page: number, _limit: number) => { setCurrentPage(page); - setOffset(offset); }; const dates = Object.keys(results) diff --git a/src/Components/Facility/Consultations/Feed.tsx b/src/Components/Facility/Consultations/Feed.tsx index 7352a9bcd0b..cdfbdb40e9d 100644 --- a/src/Components/Facility/Consultations/Feed.tsx +++ b/src/Components/Facility/Consultations/Feed.tsx @@ -26,10 +26,10 @@ import FeedButton from "./FeedButton"; import Loading from "../../Common/Loading"; import ReactPlayer from "react-player"; import { classNames } from "../../../Utils/utils"; -import screenfull from "screenfull"; import { useDispatch } from "react-redux"; import { useHLSPLayer } from "../../../Common/hooks/useHLSPlayer"; import useKeyboardShortcut from "use-keyboard-shortcut"; +import useFullscreen from "../../../Common/hooks/useFullscreen.js"; interface IFeedProps { facilityId: string; @@ -54,6 +54,7 @@ export const Feed: React.FC<IFeedProps> = ({ consultationId, facilityId }) => { const [bed, setBed] = useState<any>(); const [precision, setPrecision] = useState(1); const [cameraState, setCameraState] = useState<PTZState | null>(null); + const [isFullscreen, setFullscreen] = useFullscreen(); useEffect(() => { const fetchFacility = async () => { @@ -299,14 +300,13 @@ export const Feed: React.FC<IFeedProps> = ({ consultationId, facilityId }) => { }); }, fullScreen: () => { - if (!(screenfull.isEnabled && liveFeedPlayerRef.current)) return; - !screenfull.isFullscreen - ? screenfull.request( - videoWrapper.current - ? videoWrapper.current - : (liveFeedPlayerRef.current as HTMLElement) - ) - : screenfull.exit(); + if (!liveFeedPlayerRef.current) return; + setFullscreen( + !isFullscreen, + videoWrapper.current + ? videoWrapper.current + : (liveFeedPlayerRef.current as HTMLElement) + ); }, updatePreset: (option) => { getCameraStatus({ @@ -576,7 +576,7 @@ export const FeedCameraPTZHelpButton = (props: { cameraPTZ: CameraPTZ[] }) => { return ( <button key="option.action" - className="tooltip rounded text-2xl text-white/40" + className="tooltip rounded text-2xl text-gray-600" > <CareIcon className="care-l-question-circle" /> diff --git a/src/Components/Facility/Consultations/LiveFeed.tsx b/src/Components/Facility/Consultations/LiveFeed.tsx index 51c7fae894c..f9ffc0a7cfb 100644 --- a/src/Components/Facility/Consultations/LiveFeed.tsx +++ b/src/Components/Facility/Consultations/LiveFeed.tsx @@ -1,6 +1,5 @@ import { useEffect, useState, useRef } from "react"; import { useDispatch } from "react-redux"; -import screenfull from "screenfull"; import useKeyboardShortcut from "use-keyboard-shortcut"; import { listAssetBeds, @@ -23,6 +22,7 @@ import CareIcon from "../../../CAREUI/icons/CareIcon"; import Page from "../../Common/components/Page"; import ConfirmDialog from "../../Common/ConfirmDialog"; import { FieldLabel } from "../../Form/FormFields/FormField"; +import useFullscreen from "../../../Common/hooks/useFullscreen"; const LiveFeed = (props: any) => { const middlewareHostname = @@ -47,6 +47,8 @@ const LiveFeed = (props: any) => { }); const [toDelete, setToDelete] = useState<any>(null); const [toUpdate, setToUpdate] = useState<any>(null); + const [_isFullscreen, setFullscreen] = useFullscreen(); + const { width } = useWindowDimensions(); const extremeSmallScreenBreakpoint = 320; const isExtremeSmallScreen = @@ -227,8 +229,8 @@ const LiveFeed = (props: any) => { }); }, fullScreen: () => { - if (!(screenfull.isEnabled && liveFeedPlayerRef.current)) return; - screenfull.request(liveFeedPlayerRef.current); + if (!liveFeedPlayerRef.current) return; + setFullscreen(true, liveFeedPlayerRef.current); }, updatePreset: (option) => { getCameraStatus({ diff --git a/src/Components/Facility/Consultations/NursingPlot.tsx b/src/Components/Facility/Consultations/NursingPlot.tsx index 19b5ae0f90c..6da46b7ae25 100644 --- a/src/Components/Facility/Consultations/NursingPlot.tsx +++ b/src/Components/Facility/Consultations/NursingPlot.tsx @@ -1,23 +1,23 @@ import { useCallback, useState } from "react"; import { useDispatch } from "react-redux"; -import { NURSING_CARE_FIELDS } from "../../../Common/constants"; +import { + NURSING_CARE_FIELDS, + PAGINATION_LIMIT, +} from "../../../Common/constants"; import { statusType, useAbortableEffect } from "../../../Common/utils"; import { dailyRoundsAnalyse } from "../../../Redux/actions"; import Pagination from "../../Common/Pagination"; -import { PAGINATION_LIMIT } from "../../../Common/constants"; import { formatDate } from "../../../Utils/utils"; export const NursingPlot = (props: any) => { - const { facilityId, patientId, consultationId } = props; + const { consultationId } = props; const dispatch: any = useDispatch(); - const [isLoading, setIsLoading] = useState(false); const [results, setResults] = useState<any>({}); const [currentPage, setCurrentPage] = useState(1); const [totalCount, setTotalCount] = useState(0); const fetchDailyRounds = useCallback( async (status: statusType) => { - setIsLoading(true); const res = await dispatch( dailyRoundsAnalyse( { @@ -28,11 +28,10 @@ export const NursingPlot = (props: any) => { ) ); if (!status.aborted) { - if (res && res.data) { + if (res?.data) { setResults(res.data.results); setTotalCount(res.data.count); } - setIsLoading(false); } }, [consultationId, dispatch, currentPage] @@ -74,8 +73,8 @@ export const NursingPlot = (props: any) => { const areFieldsEmpty = () => { let emptyFieldCount = 0; - for (let i = 0; i < NURSING_CARE_FIELDS.length; i++) { - if (!filterEmpty(NURSING_CARE_FIELDS[i])) emptyFieldCount++; + for (const field of NURSING_CARE_FIELDS) { + if (!filterEmpty(field)) emptyFieldCount++; } if (emptyFieldCount === NURSING_CARE_FIELDS.length) return true; else return false; diff --git a/src/Components/Facility/DischargeModal.tsx b/src/Components/Facility/DischargeModal.tsx index d15d88a3353..45f6278b9cd 100644 --- a/src/Components/Facility/DischargeModal.tsx +++ b/src/Components/Facility/DischargeModal.tsx @@ -167,7 +167,7 @@ const DischargeModal = ({ }); }; - const prescriptionActions = PrescriptionActions(consultationData.id); + const prescriptionActions = PrescriptionActions(consultationData.id ?? ""); return ( <DialogModal @@ -229,7 +229,7 @@ const DischargeModal = ({ name="discharge_date" value={moment(preDischargeForm?.discharge_date).toDate()} min={moment( - consultationData?.admission_date || + consultationData?.admission_date ?? consultationData?.created_date ).toDate()} disableFuture={true} @@ -262,7 +262,7 @@ const DischargeModal = ({ <input type="datetime-local" className="w-[calc(100%-5px)] focus:ring-primary-500 focus:border-primary-500 block border border-gray-400 rounded py-2 px-4 text-sm bg-gray-100 hover:bg-gray-200 focus:outline-none focus:bg-white" - value={preDischargeForm.death_datetime || ""} + value={preDischargeForm.death_datetime ?? ""} required min={consultationData?.admission_date?.substring(0, 16)} max={moment(new Date()).format("YYYY-MM-DDThh:mm")} @@ -279,7 +279,7 @@ const DischargeModal = ({ <TextFormField name="death_confirmed_by" label="Confirmed By" - value={preDischargeForm.death_confirmed_doctor || ""} + value={preDischargeForm.death_confirmed_doctor ?? ""} onChange={(e) => { setPreDischargeForm((form) => { return { @@ -300,7 +300,7 @@ const DischargeModal = ({ name="discharge_date" value={moment(preDischargeForm.discharge_date).toDate()} min={moment( - consultationData?.admission_date || + consultationData?.admission_date ?? consultationData?.created_date ).toDate()} disableFuture={true} @@ -319,8 +319,8 @@ const DischargeModal = ({ <ClaimDetailCard claim={latestClaim} /> ) : ( <CreateClaimCard - consultationId={consultationData.id} - patientId={consultationData.patient} + consultationId={consultationData.id ?? ""} + patientId={consultationData.patient ?? ""} use="claim" isCreating={isCreateClaimLoading} setIsCreating={setIsCreateClaimLoading} diff --git a/src/Components/Facility/TreatmentSummary.tsx b/src/Components/Facility/TreatmentSummary.tsx index 2696b0d7031..5322d072454 100644 --- a/src/Components/Facility/TreatmentSummary.tsx +++ b/src/Components/Facility/TreatmentSummary.tsx @@ -1,12 +1,15 @@ import React, { useCallback, useState } from "react"; import { useDispatch } from "react-redux"; -import { getPatient, getInvestigation } from "../../Redux/actions"; +import { + getPatient, + getInvestigation, + getConsultation, +} from "../../Redux/actions"; import { ConsultationModel } from "./models"; import { statusType, useAbortableEffect } from "../../Common/utils"; import { PatientModel } from "../Patient/models"; import loadable from "@loadable/component"; import moment from "moment"; -import { getConsultation } from "../../Redux/actions"; import { GENDER_TYPES } from "../../Common/constants"; import { formatDate } from "../../Utils/utils"; const Loading = loadable(() => import("../Common/Loading")); @@ -28,7 +31,7 @@ const TreatmentSummary = (props: any) => { setIsLoading(true); const res = await dispatch(getPatient({ id: patientId })); if (!status.aborted) { - if (res && res.data) { + if (res?.data) { setPatientData(res.data); } else { setPatientData({}); @@ -45,7 +48,7 @@ const TreatmentSummary = (props: any) => { const res = await dispatch(getInvestigation({}, consultationId)); if (!status.aborted) { - if (res && res?.data?.results) { + if (res?.data?.results) { const valueMap = res.data.results.reduce( (acc: any, cur: { id: any }) => ({ ...acc, [cur.id]: cur }), {} @@ -67,7 +70,7 @@ const TreatmentSummary = (props: any) => { dispatch(getConsultation(consultationId)), ]); if (!status.aborted) { - if (res && res.data) { + if (res?.data) { setConsultationData(res.data); if (res.data.last_daily_round) { setDailyRounds(res.data.last_daily_round); @@ -255,8 +258,7 @@ const TreatmentSummary = (props: any) => { <div className="border-b-2 border-gray-800 px-5 py-2"> <b>General Instructions :</b> - {patientData.last_consultation && - patientData.last_consultation.consultation_notes ? ( + {patientData?.last_consultation?.consultation_notes ? ( <div className="mx-5"> {patientData.last_consultation.consultation_notes} </div> diff --git a/src/Components/Facility/models.tsx b/src/Components/Facility/models.tsx index 52fdc73e860..488d195354b 100644 --- a/src/Components/Facility/models.tsx +++ b/src/Components/Facility/models.tsx @@ -88,17 +88,17 @@ export interface ConsultationModel { created_date?: string; discharge_date?: string; discharge_reason?: string; - discharge_prescription: NormalPrescription; - discharge_prn_prescription: PRNPrescription; + discharge_prescription?: NormalPrescription; + discharge_prn_prescription?: PRNPrescription; discharge_notes?: string; examination_details?: string; history_of_present_illness?: string; facility?: number; facility_name?: string; - id: string; + id?: string; modified_date?: string; other_symptoms?: string; - patient: string; + patient?: string; prescribed_medication?: string; referred_to?: number | null; suggestion?: string; diff --git a/src/Components/Form/FieldValidators.tsx b/src/Components/Form/FieldValidators.tsx index 7a7bf72d067..1cd1a67e4e5 100644 --- a/src/Components/Form/FieldValidators.tsx +++ b/src/Components/Form/FieldValidators.tsx @@ -29,6 +29,19 @@ export const MultiValidator = <T,>( return validator; }; +export const AnyValidator = <T,>( + validators: FieldValidator<T>[] +): FieldValidator<T> => { + const validator = (value: T) => { + for (const validate of validators) { + const error = validate(value); + if (!error) return; + } + return validators[0](value); + }; + return validator; +}; + export const RequiredFieldValidator = (message = "Field is required") => { return <T,>(value: T): FieldError => { if (!value) return message; @@ -36,10 +49,30 @@ export const RequiredFieldValidator = (message = "Field is required") => { }; }; -export const EmailValidator = (message = "Invalid email address") => { +export const RegexValidator = (regex: RegExp, message = "Invalid input") => { return (value: string): FieldError => { - const pattern = - /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; - return pattern.test(value) ? undefined : message; + return regex.test(value) ? undefined : message; }; }; + +const EMAIL_REGEX = + /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + +export const EmailValidator = (message = "Invalid email address") => { + return RegexValidator(EMAIL_REGEX, message); +}; + +const PHONE_NUMBER_REGEX = + /^(?:(?:(?:\+|0{0,2})91|0{0,2})(?:\()?\d{3}(?:\))?[-]?\d{3}[-]?\d{4})$/; + +export const PhoneNumberValidator = (message = "Invalid phone number") => { + return RegexValidator(PHONE_NUMBER_REGEX, message); +}; + +const SUPPORT_PHONE_NUMBER_REGEX = /^1800[-]?\d{3}[-]?\d{3,4}$/; + +export const SupportPhoneNumberValidator = ( + message = "Invalid support phone number" +) => { + return RegexValidator(SUPPORT_PHONE_NUMBER_REGEX, message); +}; diff --git a/src/Components/Form/FormFields/Autocomplete.tsx b/src/Components/Form/FormFields/Autocomplete.tsx index c50d9d772cd..7622f59da42 100644 --- a/src/Components/Form/FormFields/Autocomplete.tsx +++ b/src/Components/Form/FormFields/Autocomplete.tsx @@ -5,6 +5,7 @@ import CareIcon from "../../../CAREUI/icons/CareIcon"; import { dropdownOptionClassNames } from "../MultiSelectMenuV2"; import { FormFieldBaseProps, useFormFieldPropsResolver } from "./Utils"; import FormField from "./FormField"; +import { classNames } from "../../../Utils/utils"; type OptionCallback<T, R> = (option: T) => R; @@ -62,7 +63,7 @@ type AutocompleteProps<T, V = T> = { optionLabel: OptionCallback<T, string>; optionIcon?: OptionCallback<T, React.ReactNode>; optionValue?: OptionCallback<T, V>; - optionDescription?: OptionCallback<T, string>; + optionDescription?: OptionCallback<T, React.ReactNode>; className?: string; onQuery?: (query: string) => void; requiredError?: boolean; @@ -100,8 +101,7 @@ export const Autocomplete = <T, V>(props: AutocompleteProps<T, V>) => { return { label, description, - search: - label.toLowerCase() + (description ? description.toLowerCase() : ""), + search: label.toLowerCase(), icon: props.optionIcon && props.optionIcon(option), value: props.optionValue ? props.optionValue(option) : option, }; @@ -130,7 +130,10 @@ export const Autocomplete = <T, V>(props: AutocompleteProps<T, V>) => { const options = props.allowRawInput ? getOptions() : mappedOptions; const value = options.find((o) => props.value == o.value); - const filteredOptions = options.filter((o) => o.search.includes(query)); + const filteredOptions = + props.onQuery === undefined + ? options.filter((o) => o.search.includes(query)) + : options; return ( <div @@ -181,17 +184,24 @@ export const Autocomplete = <T, V>(props: AutocompleteProps<T, V>) => { className={dropdownOptionClassNames} value={option} > - <div className="flex flex-col"> - <div className="flex justify-between"> - {option.label} - {option.icon} - </div> - {option.description && ( - <div className="text-sm text-gray-500"> - {option.description} + {({ active }) => ( + <div className="flex flex-col"> + <div className="flex justify-between"> + <span>{option.label}</span> + <span>{option.icon}</span> </div> - )} - </div> + {option.description && ( + <div + className={classNames( + "text-sm", + active ? "text-primary-200" : "text-gray-700" + )} + > + {option.description} + </div> + )} + </div> + )} </Combobox.Option> ))} </Combobox.Options> diff --git a/src/Components/Form/FormFields/DateFormField.tsx b/src/Components/Form/FormFields/DateFormField.tsx index a95b6b79db6..adb8c2538ae 100644 --- a/src/Components/Form/FormFields/DateFormField.tsx +++ b/src/Components/Form/FormFields/DateFormField.tsx @@ -44,9 +44,9 @@ const DateFormField = (props: Props) => { } onChange={field.handleChange} disabled={field.disabled} - max={props.max || (props.disableFuture ? new Date() : undefined)} - min={props.min || (props.disablePast ? yesterday() : undefined)} - position={props.position || "RIGHT"} + max={props.max ?? (props.disableFuture ? new Date() : undefined)} + min={props.min ?? (props.disablePast ? yesterday() : undefined)} + position={props.position ?? "RIGHT"} placeholder={props.placeholder} /> </FormField> diff --git a/src/Components/Form/FormFields/PhoneNumberFormField.tsx b/src/Components/Form/FormFields/PhoneNumberFormField.tsx index 5299cf7550f..a6f472e51e6 100644 --- a/src/Components/Form/FormFields/PhoneNumberFormField.tsx +++ b/src/Components/Form/FormFields/PhoneNumberFormField.tsx @@ -1,9 +1,19 @@ import { FormFieldBaseProps, useFormFieldPropsResolver } from "./Utils"; import FormField from "./FormField"; -import { AsYouType } from "libphonenumber-js"; -import { useMemo } from "react"; +import { + AsYouType, + isValidPhoneNumber, + parsePhoneNumber, +} from "libphonenumber-js"; +import { useMemo, useState } from "react"; import { classNames } from "../../../Utils/utils"; import phoneCodesJson from "../../../Common/static/countryPhoneAndFlags.json"; +import { + AnyValidator, + FieldError, + PhoneNumberValidator, + SupportPhoneNumberValidator, +} from "../FieldValidators"; interface CountryData { flag: string; @@ -17,10 +27,12 @@ interface Props extends FormFieldBaseProps<string> { placeholder?: string; autoComplete?: string; disableCountry?: boolean; + disableValidation?: boolean; } export default function PhoneNumberFormField(props: Props) { const field = useFormFieldPropsResolver(props as any); + const [error, setError] = useState<FieldError>(); const asYouType = useMemo(() => { const asYouType = new AsYouType(); @@ -37,14 +49,40 @@ export default function PhoneNumberFormField(props: Props) { return asYouType; }, []); + const validate = useMemo( + () => (value: string | undefined, event: "blur" | "change") => { + if (!value || props.disableValidation) { + return; + } + + const newError = AnyValidator([ + PhoneNumberValidator(), + SupportPhoneNumberValidator(), + ])(value); + + if (!newError) { + return; + } else if (event === "blur") { + return newError; + } + }, + [props.disableValidation] + ); + const setValue = (value: string) => { + value = value.replaceAll(" ", ""); + asYouType.reset(); asYouType.input(value); + + const error = validate(value, "change"); field.handleChange(value); + + setError(error); }; return ( - <FormField field={field}> + <FormField field={{ ...field, error: field.error || error }}> <div className="relative rounded-md shadow-sm"> <input type="tel" @@ -58,9 +96,10 @@ export default function PhoneNumberFormField(props: Props) { )} maxLength={field.value?.startsWith("1800") ? 11 : 15} placeholder={props.placeholder} - value={field.value} + value={formatPhoneNumber(field.value, props.disableCountry)} onChange={(e) => setValue(e.target.value)} disabled={field.disabled} + onBlur={() => setError(validate(field.value, "blur"))} /> {!props.disableCountry && ( <div className="absolute inset-y-0 right-0 flex items-center"> @@ -83,9 +122,9 @@ export default function PhoneNumberFormField(props: Props) { setValue(conditionPhoneCode(phoneCodes[e.target.value].code)); }} > - {Object.entries(phoneCodes).map(([country, { flag, code }]) => ( + {Object.entries(phoneCodes).map(([country, { flag }]) => ( <option key={country} value={country}> - {flag} {conditionPhoneCode(code)} + {flag} {country} </option> ))} <option value="Other">Other</option> @@ -102,3 +141,16 @@ const conditionPhoneCode = (code: string) => { code = code.split(" ")[0]; return code.startsWith("+") ? code : "+" + code; }; + +const formatPhoneNumber = (value: string, disableCountry?: boolean) => { + if (value === undefined || value === null) { + return disableCountry ? "" : "+91 "; + } + + if (!isValidPhoneNumber(value) || disableCountry) { + return value; + } + + const phoneNumber = parsePhoneNumber(value); + return phoneNumber.formatInternational(); +}; diff --git a/src/Components/Form/SelectMenuV2.tsx b/src/Components/Form/SelectMenuV2.tsx index e9800d733c5..44917b23b26 100644 --- a/src/Components/Form/SelectMenuV2.tsx +++ b/src/Components/Form/SelectMenuV2.tsx @@ -59,7 +59,7 @@ const SelectMenuV2 = <T, V>(props: SelectMenuProps<T, V>) => { const showChevronIcon = props.showChevronIcon ?? true; - const placeholder = props.placeholder || "Select"; + const placeholder = props.placeholder ?? "Select"; const defaultOption = { label: placeholder, selectedLabel: <p className="font-normal text-gray-600">{placeholder}</p>, @@ -72,7 +72,7 @@ const SelectMenuV2 = <T, V>(props: SelectMenuProps<T, V>) => { ? valueOptions : [defaultOption, ...valueOptions]; - const value = options.find((o) => props.value == o.value) || defaultOption; + const value = options.find((o) => props.value == o.value) ?? defaultOption; return ( <div className={props.className} id={props.id}> diff --git a/src/Components/HCX/CreateClaimCard.tsx b/src/Components/HCX/CreateClaimCard.tsx index c0a10f00216..75f726c6e57 100644 --- a/src/Components/HCX/CreateClaimCard.tsx +++ b/src/Components/HCX/CreateClaimCard.tsx @@ -52,7 +52,7 @@ export default function CreateClaimCard({ ).find((o) => o.outcome === "Processing Complete"); if (latestApprovedPreAuth) { setPolicy(latestApprovedPreAuth.policy_object); - setItems(latestApprovedPreAuth.items || []); + setItems(latestApprovedPreAuth.items ?? []); return; } } @@ -174,7 +174,7 @@ export default function CreateClaimCard({ ghost={items?.length !== 0} disabled={items === undefined || !policy} onClick={() => - setItems([...(items || []), { name: "", id: "", price: 0 }]) + setItems([...(items ?? []), { name: "", id: "", price: 0 }]) } > <CareIcon className="care-l-plus text-lg" /> @@ -219,9 +219,7 @@ export default function CreateClaimCard({ { id: "claim", label: "Claim" }, ]} value={use_} - onChange={({ value }) => - setUse_(value as "preauthorization" | "claim") - } + onChange={({ value }) => setUse_(value)} position="below" optionLabel={(value) => value.label} optionValue={(value) => value.id as "preauthorization" | "claim"} diff --git a/src/Components/Hub/LiveFeedTile.tsx b/src/Components/Hub/LiveFeedTile.tsx index a51e284073a..27a9e2d2bd1 100644 --- a/src/Components/Hub/LiveFeedTile.tsx +++ b/src/Components/Hub/LiveFeedTile.tsx @@ -3,10 +3,10 @@ import React, { useEffect, useState, useRef, useCallback } from "react"; import * as Notification from "../../Utils/Notifications.js"; import { useDispatch } from "react-redux"; import ReactPlayer from "react-player"; -import screenfull from "screenfull"; import { getAsset, listAssetBeds } from "../../Redux/actions"; import { statusType, useAbortableEffect } from "../../Common/utils"; import { useTranslation } from "react-i18next"; +import useFullscreen from "../../Common/hooks/useFullscreen.js"; interface LiveFeedTileProps { assetId: string; } @@ -38,6 +38,7 @@ export default function LiveFeedTile(props: LiveFeedTileProps) { zoom: 0, }); const { t } = useTranslation(); + const [_isFullscreen, setFullscreen] = useFullscreen(); useEffect(() => { let loadingTimeout: any; @@ -238,10 +239,8 @@ export default function LiveFeedTile(props: LiveFeedTileProps) { const liveFeedPlayerRef = useRef<any>(null); const handleClickFullscreen = () => { - if (screenfull.isEnabled) { - if (liveFeedPlayerRef.current) { - screenfull.request(liveFeedPlayerRef.current.wrapper); - } + if (liveFeedPlayerRef.current) { + setFullscreen(true, liveFeedPlayerRef.current.wrapper); } }; diff --git a/src/Components/Medicine/CreatePrescriptionForm.tsx b/src/Components/Medicine/CreatePrescriptionForm.tsx index 42e205c63b6..eb2939da049 100644 --- a/src/Components/Medicine/CreatePrescriptionForm.tsx +++ b/src/Components/Medicine/CreatePrescriptionForm.tsx @@ -8,12 +8,9 @@ import { MedicineAdministrationRecord, Prescription } from "./models"; import { PrescriptionActions } from "../../Redux/actions"; import { useDispatch } from "react-redux"; import { useState } from "react"; -import AutocompleteFormField from "../Form/FormFields/Autocomplete"; -import medicines_list from "../Common/prescription-builder/assets/medicines.json"; import NumericWithUnitsFormField from "../Form/FormFields/NumericWithUnitsFormField"; import { useTranslation } from "react-i18next"; - -export const medicines = medicines_list; +import MedibaseAutocompleteFormField from "./MedibaseAutocompleteFormField"; export default function CreatePrescriptionForm(props: { prescription: Prescription; @@ -30,6 +27,9 @@ export default function CreatePrescriptionForm(props: { defaults={props.prescription} onCancel={props.onDone} onSubmit={async (obj) => { + obj["medicine"] = obj.medicine_object?.id; + delete obj.medicine_object; + setIsCreating(true); const res = await dispatch(props.create(obj)); setIsCreating(false); @@ -42,7 +42,7 @@ export default function CreatePrescriptionForm(props: { noPadding validate={(form) => { const errors: Partial<Record<keyof Prescription, FieldError>> = {}; - errors.medicine = RequiredFieldValidator()(form.medicine); + errors.medicine_object = RequiredFieldValidator()(form.medicine_object); errors.dosage = RequiredFieldValidator()(form.dosage); if (form.is_prn) errors.indicator = RequiredFieldValidator()(form.indicator); @@ -54,13 +54,10 @@ export default function CreatePrescriptionForm(props: { > {(field) => ( <> - <AutocompleteFormField + <MedibaseAutocompleteFormField label={t("medicine")} - {...field("medicine", RequiredFieldValidator())} + {...field("medicine_object", RequiredFieldValidator())} required - options={medicines} - optionLabel={(medicine) => medicine} - optionValue={(medicine) => medicine} /> <div className="flex gap-4 items-center"> <SelectFormField diff --git a/src/Components/Medicine/MedibaseAutocompleteFormField.tsx b/src/Components/Medicine/MedibaseAutocompleteFormField.tsx new file mode 100644 index 00000000000..62d0d3fff29 --- /dev/null +++ b/src/Components/Medicine/MedibaseAutocompleteFormField.tsx @@ -0,0 +1,71 @@ +import { useAsyncOptions } from "../../Common/hooks/useAsyncOptions"; +import { listMedibaseMedicines } from "../../Redux/actions"; +import { Autocomplete } from "../Form/FormFields/Autocomplete"; +import FormField from "../Form/FormFields/FormField"; +import { + FormFieldBaseProps, + useFormFieldPropsResolver, +} from "../Form/FormFields/Utils"; +import { MedibaseMedicine } from "./models"; + +export default function MedibaseAutocompleteFormField( + props: FormFieldBaseProps<MedibaseMedicine> +) { + const field = useFormFieldPropsResolver(props); + const { isLoading, options, fetchOptions } = + useAsyncOptions<MedibaseMedicine>("id"); + + return ( + <FormField field={field}> + <Autocomplete + id={field.id} + disabled={field.disabled} + value={field.value} + required + onChange={field.handleChange} + options={options(field.value && [field.value])} + optionLabel={(option) => option.name.toUpperCase()} + optionDescription={(option) => <OptionDescription medicine={option} />} + optionValue={(option) => option} + optionIcon={(option) => + option.type === "brand" ? ( + <OptionChip name="Brand" value={option.company || ""} /> + ) : ( + <OptionChip value={option.type} /> + ) + } + onQuery={(query) => fetchOptions(listMedibaseMedicines(query))} + isLoading={isLoading} + /> + </FormField> + ); +} + +const OptionDescription = ({ medicine }: { medicine: MedibaseMedicine }) => { + return ( + <div className="p-1"> + {medicine.atc_classification && ( + <span className="text-xs"> + ATC Class: {medicine.atc_classification} + </span> + )} + <div className="mt-2 flex flex-wrap w-full gap-2"> + <OptionChip name="CIMS Class" value={medicine.cims_class} /> + {medicine.generic && ( + <OptionChip name="Generic" value={medicine.generic} /> + )} + </div> + </div> + ); +}; + +const OptionChip = (props: { name?: string; value: string }) => { + return ( + <div className="flex gap-1 px-2 mt-1 sm:mt-0 uppercase text-center bg-secondary-100 h-fit max-w-fit rounded-full text-xs border border-secondary-400 whitespace-nowrap"> + <span className="text-gray-800 font-normal"> + {props.name && props.name + ":"} + </span> + <span className="text-gray-900 font-medium">{props.value}</span> + </div> + ); +}; diff --git a/src/Components/Medicine/MedicineAdministrationsTable.tsx b/src/Components/Medicine/MedicineAdministrationsTable.tsx index 718f9d037dd..75233639bc4 100644 --- a/src/Components/Medicine/MedicineAdministrationsTable.tsx +++ b/src/Components/Medicine/MedicineAdministrationsTable.tsx @@ -59,7 +59,9 @@ export default function MedicineAdministrationsTable({ list={ items?.map((obj) => ({ ...obj, - medicine: obj.prescription?.medicine, + medicine: + obj.prescription?.medicine_object?.name ?? + obj.prescription?.medicine_old, created_date__pretty: ( <span className="flex gap-1"> <RecordMeta time={obj.created_date} /> by{" "} diff --git a/src/Components/Medicine/PrescriptionDetailCard.tsx b/src/Components/Medicine/PrescriptionDetailCard.tsx index d6be22ad1c1..aa1c1ba643d 100644 --- a/src/Components/Medicine/PrescriptionDetailCard.tsx +++ b/src/Components/Medicine/PrescriptionDetailCard.tsx @@ -85,7 +85,7 @@ export default function PrescriptionDetailCard({ <div className="grid grid-cols-9 gap-2 items-center mt-2"> <Detail className="col-span-9 md:col-span-5" label={t("medicine")}> - {prescription.medicine} + {prescription.medicine_object?.name ?? prescription.medicine_old} </Detail> <Detail className="col-span-5 md:col-span-2" label={t("route")}> {prescription.route && diff --git a/src/Components/Medicine/PrescriptionsTable.tsx b/src/Components/Medicine/PrescriptionsTable.tsx index 7b6272675b9..42908847eda 100644 --- a/src/Components/Medicine/PrescriptionsTable.tsx +++ b/src/Components/Medicine/PrescriptionsTable.tsx @@ -200,6 +200,7 @@ export default function PrescriptionsTable({ list={ prescriptions?.map((obj) => ({ ...obj, + medicine: obj.medicine_object?.name ?? obj.medicine_old, route__pretty: obj.route && t("PRESCRIPTION_ROUTE_" + obj.route), frequency__pretty: diff --git a/src/Components/Medicine/models.ts b/src/Components/Medicine/models.ts index 2f02e6a84b2..21c52b4a6ec 100644 --- a/src/Components/Medicine/models.ts +++ b/src/Components/Medicine/models.ts @@ -2,7 +2,9 @@ import { PerformedByModel } from "../HCX/misc"; interface BasePrescription { readonly id?: string; - medicine: string; + medicine?: string; + medicine_object?: MedibaseMedicine; + medicine_old?: string; route?: "ORAL" | "IV" | "IM" | "SC"; dosage: string; notes?: string; @@ -56,3 +58,14 @@ export type MedicineAdministrationRecord = { readonly created_date?: string; readonly modified_date?: string; }; + +export type MedibaseMedicine = { + id: string; + name: string; + type: "brand" | "generic"; + company?: string; + contents: string; + cims_class: string; + atc_classification?: string; + generic?: string; +}; diff --git a/src/Components/Notifications/ShowPushNotification.tsx b/src/Components/Notifications/ShowPushNotification.tsx index 2352b9ccdd9..616ebb5ef2b 100644 --- a/src/Components/Notifications/ShowPushNotification.tsx +++ b/src/Components/Notifications/ShowPushNotification.tsx @@ -1,13 +1,11 @@ -import React, { useState } from "react"; +import React from "react"; import { useDispatch } from "react-redux"; import { getNotificationData } from "../../Redux/actions"; export default function ShowPushNotification({ external_id }: any) { - const [isLoading, setIsLoading] = useState(true); const dispatch: any = useDispatch(); - let resultUrl = async () => { - setIsLoading(true); + const resultUrl = async () => { console.log("ID:", external_id.id); const res = await dispatch(getNotificationData({ id: external_id.id })); const data = res.data.caused_objects; @@ -27,7 +25,7 @@ export default function ShowPushNotification({ external_id }: any) { case "INVESTIGATION_SESSION_CREATED": return `/facility/${data.facility}/patient/${data.patient}/consultation/${data.consultation}/investigation/${data.session}`; case "MESSAGE": - return `/notice_board/`; + return "/notice_board/"; default: return "#"; } @@ -35,7 +33,6 @@ export default function ShowPushNotification({ external_id }: any) { resultUrl() .then((url) => { - setIsLoading(false); window.location.href = url; }) .catch((err) => console.log(err)); diff --git a/src/Components/Patient/DailyRoundListDetails.tsx b/src/Components/Patient/DailyRoundListDetails.tsx index 8e355f7b313..486544e0fb8 100644 --- a/src/Components/Patient/DailyRoundListDetails.tsx +++ b/src/Components/Patient/DailyRoundListDetails.tsx @@ -45,10 +45,7 @@ export const DailyRoundListDetails = (props: any) => { ? currentHealth.desc : res.data.current_health, }; - if ( - res.data.additional_symptoms && - res.data.additional_symptoms.length - ) { + if (res.data.additional_symptoms?.length) { const symptoms = res.data.additional_symptoms.map( (symptom: number) => { const option = symptomChoices.find((i) => i.id === symptom); @@ -87,7 +84,7 @@ export const DailyRoundListDetails = (props: any) => { <span className="font-semibold leading-relaxed"> Patient Category:{" "} </span> - {dailyRoundListDetailsData.patient_category || "-"} + {dailyRoundListDetailsData.patient_category ?? "-"} </div> </div> @@ -105,7 +102,7 @@ export const DailyRoundListDetails = (props: any) => { <div className="mt-4 grid gap-4 grid-cols-1 md:grid-cols-2"> <div> <span className="font-semibold leading-relaxed">Temperature: </span> - {dailyRoundListDetailsData.temperature || "-"} + {dailyRoundListDetailsData.temperature ?? "-"} </div> <div> <span className="font-semibold leading-relaxed">Taken at: </span> @@ -115,41 +112,41 @@ export const DailyRoundListDetails = (props: any) => { </div> <div> <span className="font-semibold leading-relaxed">SpO2: </span> - {dailyRoundListDetailsData.ventilator_spo2 || "-"} + {dailyRoundListDetailsData.ventilator_spo2 ?? "-"} </div> <div className="md:col-span-2 capitalize"> <span className="font-semibold leading-relaxed"> Additional Symptoms:{" "} </span> - {dailyRoundListDetailsData.additional_symptoms_text || "-"} + {dailyRoundListDetailsData.additional_symptoms_text ?? "-"} </div> <div className="md:col-span-2 capitalize"> <span className="font-semibold leading-relaxed"> Admitted To *:{" "} </span> - {dailyRoundListDetailsData.admitted_to || "-"} + {dailyRoundListDetailsData.admitted_to ?? "-"} </div> <div className="md:col-span-2"> <span className="font-semibold leading-relaxed"> Physical Examination Info:{" "} </span> - {dailyRoundListDetailsData.physical_examination_info || "-"} + {dailyRoundListDetailsData.physical_examination_info ?? "-"} </div> <div className="md:col-span-2"> <span className="font-semibold leading-relaxed"> Other Symptoms:{" "} </span> - {dailyRoundListDetailsData.other_symptoms || "-"} + {dailyRoundListDetailsData.other_symptoms ?? "-"} </div> <div className="md:col-span-2"> <span className="font-semibold leading-relaxed"> Other Details:{" "} </span> - {dailyRoundListDetailsData.other_details || "-"} + {dailyRoundListDetailsData.other_details ?? "-"} </div> <div className="md:col-span-2"> <span className="font-semibold leading-relaxed">Pulse(bpm): </span> - {dailyRoundListDetailsData.pulse || "-"} + {dailyRoundListDetailsData.pulse ?? "-"} </div> <div className="md:col-span-2 "> <span className="font-semibold leading-relaxed">BP</span> @@ -158,14 +155,14 @@ export const DailyRoundListDetails = (props: any) => { <span className="font-semibold leading-relaxed"> Systolic:{" "} </span> - {dailyRoundListDetailsData.bp?.systolic || "-"} + {dailyRoundListDetailsData.bp?.systolic ?? "-"} </div> <div className="flex"> {" "} <span className="font-semibold leading-relaxed"> Diastolic: </span> - {dailyRoundListDetailsData.bp?.diastolic || "-"} + {dailyRoundListDetailsData.bp?.diastolic ?? "-"} </div> </div> </div> @@ -175,17 +172,17 @@ export const DailyRoundListDetails = (props: any) => { Respiratory Rate (bpm): </span> - {dailyRoundListDetailsData.resp || "-"} + {dailyRoundListDetailsData.resp ?? "-"} </div> <div className="md:col-span-2"> <span className="font-semibold leading-relaxed">Rhythm: </span> - {dailyRoundListDetailsData.rhythm || "-"} + {dailyRoundListDetailsData.rhythm ?? "-"} </div> <div className="md:col-span-2"> <span className="font-semibold leading-relaxed"> Rhythm Description:{" "} </span> - {dailyRoundListDetailsData.rhythm_detail || "-"} + {dailyRoundListDetailsData.rhythm_detail ?? "-"} </div> <div> <span className="font-semibold leading-relaxed"> diff --git a/src/Components/Patient/PatientInfoCard.tsx b/src/Components/Patient/PatientInfoCard.tsx index 2077a2de5e9..3e2db9b2b7d 100644 --- a/src/Components/Patient/PatientInfoCard.tsx +++ b/src/Components/Patient/PatientInfoCard.tsx @@ -57,7 +57,7 @@ export default function PatientInfoCard(props: { facilityId={patient?.facility} patientId={patient?.id} discharged={!!consultation?.discharge_date} - consultationId={consultation?.id} + consultationId={consultation?.id ?? ""} setState={setOpen} fetchPatientData={props.fetchPatientData} smallLoader @@ -74,8 +74,7 @@ export default function PatientInfoCard(props: { <div className={`w-24 h-24 min-w-[5rem] bg-gray-200 ${categoryClass}-profile`} > - {consultation && - consultation?.current_bed && + {consultation?.current_bed && consultation?.discharge_date === null ? ( <div className="flex flex-col items-center justify-center h-full" @@ -185,7 +184,7 @@ export default function PatientInfoCard(props: { (resp) => resp.text === consultation?.last_daily_round?.ventilator_interface - )?.id || "UNKNOWN", + )?.id ?? "UNKNOWN", consultation?.last_daily_round?.ventilator_interface, ], ].map((stat, i) => { @@ -279,7 +278,7 @@ export default function PatientInfoCard(props: { !consultation?.discharge_date, [ !(consultation?.facility !== patient.facility) && - !(consultation?.discharge_date || !patient.is_active) && + !(consultation?.discharge_date ?? !patient.is_active) && moment(consultation?.modified_date).isBefore( new Date().getTime() - 24 * 60 * 60 * 1000 ), @@ -320,7 +319,7 @@ export default function PatientInfoCard(props: { <div className="relative" key={i}> <ButtonV2 key={i} - variant={action[4] && action[4][0] ? "danger" : "primary"} + variant={action?.[4]?.[0] ? "danger" : "primary"} href={ consultation?.admitted && !consultation?.current_bed && diff --git a/src/Components/Patient/PatientRegister.tsx b/src/Components/Patient/PatientRegister.tsx index 7fec6b87519..ab73f52739b 100644 --- a/src/Components/Patient/PatientRegister.tsx +++ b/src/Components/Patient/PatientRegister.tsx @@ -285,7 +285,7 @@ export const PatientRegister = (props: PatientRegisterProps) => { if (!careExtId) return; const res = await dispatchAction(externalResult({ id: careExtId })); - if (res && res.data) { + if (res?.data) { const form = { ...state.form }; form["name"] = res.data.name ? res.data.name : state.form.name; form["address"] = res.data.address @@ -451,7 +451,7 @@ export const PatientRegister = (props: PatientRegisterProps) => { const res = await dispatchAction( HCXActions.policies.list({ patient: id }) ); - if (res && res.data) { + if (res?.data) { setInsuranceDetails(res.data.results); } }; @@ -846,7 +846,7 @@ export const PatientRegister = (props: PatientRegisterProps) => { phone_number: parsePhoneNumberFromString(phoneNo)?.format("E.164"), }; const res = await dispatchAction(searchPatient(query)); - if (res && res.data && res.data.results) { + if (res?.data?.results) { const duplicateList = !id ? res.data.results : res.data.results.filter( diff --git a/src/Components/Patient/SamplePreview.tsx b/src/Components/Patient/SamplePreview.tsx index 480e2f1b7da..cf1edb0d87c 100644 --- a/src/Components/Patient/SamplePreview.tsx +++ b/src/Components/Patient/SamplePreview.tsx @@ -7,7 +7,6 @@ import Page from "../Common/components/Page"; import { SampleReportModel } from "./models"; import loadable from "@loadable/component"; import { sampleReport } from "../../Redux/actions"; -import useConfig from "../../Common/hooks/useConfig"; import { useDispatch } from "react-redux"; const Loading = loadable(() => import("../Common/Loading")); @@ -61,7 +60,6 @@ export default function SampleReport(props: ISamplePreviewProps) { const { id, sampleId } = props; const [isLoading, setIsLoading] = useState(false); const [sampleData, setSampleData] = useState<SampleReportModel>({}); - const { static_ohc_green_logo } = useConfig(); let report: JSX.Element = <></>; let reportData: JSX.Element = <></>; @@ -103,7 +101,7 @@ export default function SampleReport(props: ISamplePreviewProps) { <div className="flex justify-end"> <div className="p-2"> <img - src={static_ohc_green_logo} + src="https://cdn.coronasafe.network/ohc_logo_green.png" className="max-w-[400px] h-[50px] object-contain" alt="Open HealthCare Network" /> diff --git a/src/Components/Patient/models.tsx b/src/Components/Patient/models.tsx index d8e624fe543..d0b75e43bb4 100644 --- a/src/Components/Patient/models.tsx +++ b/src/Components/Patient/models.tsx @@ -249,6 +249,11 @@ export interface SampleListModel { fast_track?: string; } +export interface DailyRoundsOutput { + name: string; + quantity: number; +} + export interface DailyRoundsModel { ventilator_spo2?: number; spo2?: string; @@ -274,6 +279,7 @@ export interface DailyRoundsModel { other_symptoms?: string; admitted_to?: string; patient_category?: PatientCategory; + output?: DailyRoundsOutput; recommend_discharge?: boolean; created_date?: string; modified_date?: string; diff --git a/src/Components/Resource/ResourceDetailsUpdate.tsx b/src/Components/Resource/ResourceDetailsUpdate.tsx index a6ae7261aaa..d090a350cc0 100644 --- a/src/Components/Resource/ResourceDetailsUpdate.tsx +++ b/src/Components/Resource/ResourceDetailsUpdate.tsx @@ -217,12 +217,11 @@ export const ResourceDetailsUpdate = (props: resourceProps) => { } return ( - - <Page - title="Update Resource Request" - backUrl={`/resource/${props.id}`} - crumbsReplacements={{ [props.id]: { name: requestTitle } }}> - + <Page + title="Update Resource Request" + backUrl={`/resource/${props.id}`} + crumbsReplacements={{ [props.id]: { name: requestTitle } }} + > <div className="mt-4"> <Card className="w-full flex flex-col"> <div className="grid gap-4 grid-cols-1 md:grid-cols-2"> @@ -242,12 +241,12 @@ export const ResourceDetailsUpdate = (props: resourceProps) => { <CircularProgress /> ) : ( <UserAutocompleteFormField - label="Assigned To" - value = {assignedUser===null?undefined:assignedUser} - onChange={handleOnSelect} - error="" - name="assigned_to" - /> + label="Assigned To" + value={assignedUser === null ? undefined : assignedUser} + onChange={handleOnSelect} + error="" + name="assigned_to" + /> )} </div> </div> @@ -332,10 +331,9 @@ export const ResourceDetailsUpdate = (props: resourceProps) => { options={[true, false]} optionDisplay={(o) => (o ? "Yes" : "No")} optionValue={(o) => String(o)} - value={state.form.emergency} + value={String(state.form.emergency)} error={state.errors.emergency} /> - </div> <div className="md:col-span-2 flex flex-col md:flex-row gap-2 justify-between mt-4"> diff --git a/src/Components/Shifting/BadgesList.tsx b/src/Components/Shifting/BadgesList.tsx index 748fb15316c..38ce8434945 100644 --- a/src/Components/Shifting/BadgesList.tsx +++ b/src/Components/Shifting/BadgesList.tsx @@ -22,7 +22,9 @@ export default function BadgesList(props: any) { useEffect(() => { async function fetchData() { if (!qParams.assigned_to) return setAssignedUsername(""); - const res = await dispatch(getUserList({ id: qParams.assigned_to })); + const res = await dispatch( + getUserList({ id: qParams.assigned_to }, "assigned_user_name") + ); const { first_name, last_name } = res?.data?.results[0]; setAssignedUsername(`${first_name} ${last_name}`); } @@ -77,19 +79,12 @@ export default function BadgesList(props: any) { return ( <FilterBadges - badges={({ - badge, - boolean, - phoneNumber, - dateRange, - kasp, - value, - }: any) => [ + badges={({ badge, phoneNumber, dateRange, kasp, value }: any) => [ badge(t("status"), "status"), - boolean(t("emergency"), "emergency", booleanFilterOptions), + badge(t("emergency"), "emergency", booleanFilterOptions), kasp(), - boolean(t("up_shift"), "is_up_shift", booleanFilterOptions), - boolean(t("antenatal"), "is_antenatal", booleanFilterOptions), + badge(t("up_shift"), "is_up_shift", booleanFilterOptions), + badge(t("antenatal"), "is_antenatal", booleanFilterOptions), phoneNumber(t("phone_no"), "patient_phone_number"), badge(t("patient_name"), "patient_name"), ...dateRange(t("created"), "created_date"), diff --git a/src/Components/Shifting/ShiftDetails.tsx b/src/Components/Shifting/ShiftDetails.tsx index dbc51218b4b..51c1b326422 100644 --- a/src/Components/Shifting/ShiftDetails.tsx +++ b/src/Components/Shifting/ShiftDetails.tsx @@ -27,12 +27,8 @@ import { useTranslation } from "react-i18next"; const Loading = loadable(() => import("../Common/Loading")); export default function ShiftDetails(props: { id: string }) { - const { - static_header_logo, - kasp_full_string, - wartime_shifting, - kasp_enabled, - } = useConfig(); + const { header_logo, kasp_full_string, wartime_shifting, kasp_enabled } = + useConfig(); const dispatch: any = useDispatch(); const initialData: any = {}; const [data, setData] = useState(initialData); @@ -344,7 +340,7 @@ export default function ShiftDetails(props: { id: string }) { return ( <div id="section-to-print" className="print bg-white "> - <div>{data.is_kasp && <img alt="logo" src={static_header_logo} />}</div> + <div>{data.is_kasp && <img alt="logo" src={header_logo.dark} />}</div> <div className="mx-2"> <div className="mt-6"> <span className="font-semibold leading-relaxed mt-4"> diff --git a/src/Components/Users/ManageUsers.tsx b/src/Components/Users/ManageUsers.tsx index fb79ca6b7f2..d3e055d0700 100644 --- a/src/Components/Users/ManageUsers.tsx +++ b/src/Components/Users/ManageUsers.tsx @@ -413,11 +413,7 @@ export default function ManageUsers() { } return ( - <Page - title="User Management" - hideBack={true} - breadcrumbs={false} - > + <Page title="User Management" hideBack={true} breadcrumbs={false}> {expandSkillList && ( <SkillsSlideOver show={expandSkillList} diff --git a/src/Components/Users/UserAdd.tsx b/src/Components/Users/UserAdd.tsx index dfa0c0e8f93..edb1f61a0bd 100644 --- a/src/Components/Users/UserAdd.tsx +++ b/src/Components/Users/UserAdd.tsx @@ -59,7 +59,7 @@ type UserForm = { gender: string; password: string; c_password: string; - facilities: Array<FacilityModel>; + facilities: Array<number>; home_facility: FacilityModel | null; username: string; first_name: string; @@ -120,7 +120,7 @@ const user_create_reducer = (state = initialState, action: any) => { form: action.form, }; } - case "set_error": { + case "set_errors": { return { ...state, errors: action.errors, @@ -163,7 +163,7 @@ export const UserAdd = (props: UserProps) => { const dispatchAction: any = useDispatch(); const { userId } = props; - const [state, dispatch] = useAutoSaveReducer( + const [state, dispatch] = useAutoSaveReducer<UserForm>( user_create_reducer, initialState ); @@ -384,7 +384,7 @@ export const UserAdd = (props: UserProps) => { setSelectedFacility(selected as FacilityModel[]); const form = { ...state.form }; form.facilities = selected - ? (selected as FacilityModel[]).map((i) => i.id) + ? (selected as FacilityModel[]).map((i) => i.id ?? -1) : []; dispatch({ type: "set_form", form }); }; @@ -531,7 +531,7 @@ export const UserAdd = (props: UserProps) => { } return; case "district": - if (!Number(state.form[field]) || state.form[field] === "") { + if (!Number(state.form[field])) { errors[field] = "Please select the district"; invalidForm = true; } @@ -548,10 +548,10 @@ export const UserAdd = (props: UserProps) => { } }); if (invalidForm) { - dispatch({ type: "set_error", errors }); + dispatch({ type: "set_errors", errors }); return false; } - dispatch({ type: "set_error", errors }); + dispatch({ type: "set_errors", errors }); return true; }; @@ -581,7 +581,7 @@ export const UserAdd = (props: UserProps) => { state.form.phone_number_is_whatsapp ? state.form.phone_number : state.form.alt_phone_number - )?.format("E.164") || "", + )?.format("E.164") ?? "", date_of_birth: moment(state.form.date_of_birth).format("YYYY-MM-DD"), age: Number(moment().diff(state.form.date_of_birth, "years", false)), doctor_qualification: @@ -601,14 +601,12 @@ export const UserAdd = (props: UserProps) => { }; const res = await dispatchAction(addUser(data)); - // userId ? updateUser(userId, data) : addUser(data) if ( res && (res.data || res.data === "") && res.status >= 200 && res.status < 300 ) { - // const id = res.data.id; dispatch({ type: "set_form", form: initForm }); if (!userId) { Notification.Success({ @@ -678,9 +676,7 @@ export const UserAdd = (props: UserProps) => { required label="User Type" options={userTypes} - optionLabel={(o) => - o.role + ((o.readOnly && " (Read Only)") || "") - } + optionLabel={(o) => o.role + (o.readOnly ? " (Read Only)" : "")} optionValue={(o) => o.id} /> @@ -714,7 +710,7 @@ export const UserAdd = (props: UserProps) => { <SelectFormField {...field("home_facility")} label="Home facility" - options={selectedFacility || []} + options={selectedFacility ?? []} optionLabel={(option) => option.name} optionValue={(option) => option.id} onChange={handleFieldChange} diff --git a/src/Components/Users/UserFilter.tsx b/src/Components/Users/UserFilter.tsx index da93cc37179..b8884b314d5 100644 --- a/src/Components/Users/UserFilter.tsx +++ b/src/Components/Users/UserFilter.tsx @@ -14,7 +14,9 @@ import FiltersSlideover from "../../CAREUI/interactive/FiltersSlideover"; const parsePhoneNumberForFilterParam = (phoneNumber: string) => { if (!phoneNumber) return ""; - return parsePhoneNumberFromString(phoneNumber)?.format("E.164") || ""; + if (phoneNumber.startsWith("+")) + return parsePhoneNumberFromString(phoneNumber)?.format("E.164") || ""; + return phoneNumber; }; export default function UserFilter(props: any) { diff --git a/src/Locale/update_locale.js b/src/Locale/update_locale.js index 319cca6bf95..b6c0b17c5e3 100644 --- a/src/Locale/update_locale.js +++ b/src/Locale/update_locale.js @@ -1,6 +1,12 @@ /* eslint-disable no-undef */ -const fs = require("fs"); +import { + existsSync, + mkdirSync, + readdirSync, + writeFileSync, + readFileSync, +} from "fs"; const DEFAULT_LOCALE = "en"; @@ -18,11 +24,11 @@ if (lng === DEFAULT_LOCALE) { const defaultEntryFile = readFile(`./${DEFAULT_LOCALE}/index.js`); const defaultAllJsonFiles = getAllJSONFiles(DEFAULT_LOCALE); -if (fs.existsSync(lng)) { +if (existsSync(lng)) { const allJsonFiles = getAllJSONFiles(lng); compareBothFiles(defaultAllJsonFiles, allJsonFiles); } else { - fs.mkdirSync(`./${lng}`); + mkdirSync(`./${lng}`); for (const file in defaultAllJsonFiles) { const defaultJSON = defaultAllJsonFiles[file]; @@ -51,7 +57,7 @@ function compareBothFiles(defaultFile, newFile) { } function getAllJSONFiles(folderName) { - const dir = fs.readdirSync(folderName).filter((e) => e.includes(".json")); + const dir = readdirSync(folderName).filter((e) => e.includes(".json")); const files = {}; dir.forEach((file) => { try { @@ -64,8 +70,8 @@ function getAllJSONFiles(folderName) { } function writeFile(name, data) { - return fs.writeFileSync(name, data); + return writeFileSync(name, data); } function readFile(name) { - return fs.readFileSync(name, { encoding: "utf-8" }); + return readFileSync(name, { encoding: "utf-8" }); } diff --git a/src/Redux/actions.tsx b/src/Redux/actions.tsx index 0a80040dc72..06c92e6662d 100644 --- a/src/Redux/actions.tsx +++ b/src/Redux/actions.tsx @@ -65,8 +65,8 @@ export const deleteFacility = (id: string) => { export const deleteFacilityCoverImage = (id: string) => { return fireRequest("deleteFacilityCoverImage", [], {}, { id }); }; -export const getUserList = (params: object) => { - return fireRequest("userList", [], params); +export const getUserList = (params: object, key?: string) => { + return fireRequest("userList", [], params, null, key); }; export const getUserListSkills = (pathParam: object) => { @@ -290,7 +290,7 @@ export const listConsultationBeds = (params: object) => fireRequest("listConsultationBeds", [], params, {}); export const createConsultationBed = ( params: object, - consultation_id: number, + consultation_id: string, bed_id: string ) => fireRequest( @@ -800,6 +800,10 @@ export const editInvestigation = ( export const listICD11Diagnosis = (params: object, key: string) => { return fireRequest("listICD11Diagnosis", [], params, null, key); }; +// Medibase +export const listMedibaseMedicines = (query: string) => { + return fireRequest("listMedibaseMedicines", [], { query }); +}; // Resource export const createResource = (params: object) => { @@ -851,6 +855,11 @@ export const listAssetTransaction = (params: object) => export const getAssetTransaction = (id: string) => fireRequest("getAssetTransaction", [], {}, { id }); +export const listAssetAvailability = (params: object) => + fireRequest("listAssetAvailability", [], params); +export const getAssetAvailability = (id: string) => + fireRequest("getAssetAvailability", [], {}, { id }); + export const listPMJYPackages = (query?: string) => fireRequest("listPMJYPackages", [], { query }); diff --git a/src/Redux/api.tsx b/src/Redux/api.tsx index 3918fabe0da..ddda010630b 100644 --- a/src/Redux/api.tsx +++ b/src/Redux/api.tsx @@ -733,6 +733,10 @@ const routes: Routes = { listICD11Diagnosis: { path: "/api/v1/icd/", }, + // Medibase + listMedibaseMedicines: { + path: "/api/v1/medibase/", + }, // Resource createResource: { @@ -813,6 +817,17 @@ const routes: Routes = { method: "GET", }, + // Asset Availability endpoints + + listAssetAvailability: { + path: "/api/v1/asset_availability/", + method: "GET", + }, + getAssetAvailability: { + path: "/api/v1/asset_availability/{id}", + method: "GET", + }, + // Prescription endpoints listPrescriptions: { diff --git a/src/Router/AppRouter.tsx b/src/Router/AppRouter.tsx index cbd9bff1738..6e07ae2da76 100644 --- a/src/Router/AppRouter.tsx +++ b/src/Router/AppRouter.tsx @@ -76,7 +76,7 @@ import ManagePrescriptions from "../Components/Medicine/ManagePrescriptions"; import CentralNursingStation from "../Components/Facility/CentralNursingStation"; export default function AppRouter() { - const { static_black_logo, enable_hcx } = useConfig(); + const { main_logo, enable_hcx } = useConfig(); const routes = { "/hub": () => <HubDashboard />, @@ -486,7 +486,7 @@ export default function AppRouter() { > <img className="h-6 w-auto" - src={static_black_logo} + src={main_logo.dark} alt="care logo" /> </a> diff --git a/src/style/CAREUI.css b/src/style/CAREUI.css index ba57bbb502e..b61a52e3fc5 100644 --- a/src/style/CAREUI.css +++ b/src/style/CAREUI.css @@ -25,7 +25,7 @@ } .cui-dropdown-base { - @apply z-40 w-full rounded-md xl:rounded-lg shadow-lg overflow-auto max-h-96 bg-gray-100 divide-y divide-gray-300 ring-1 ring-gray-400 focus:outline-none + @apply z-40 w-full rounded-b-md xl:rounded-lg shadow-lg overflow-auto max-h-96 bg-gray-100 divide-y divide-gray-300 ring-1 ring-gray-400 focus:outline-none } .cui-card { From 91561153a97b21454d5cb85efa0854a8506f75d0 Mon Sep 17 00:00:00 2001 From: Bhavik Agarwal <bhavikiitian@gmail.com> Date: Fri, 21 Jul 2023 10:31:00 +0530 Subject: [PATCH 07/17] added doctor location --- src/Components/Facility/PatientNoteCard.tsx | 14 ++++---------- src/Components/Facility/PatientNotesList.tsx | 8 ++------ 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/src/Components/Facility/PatientNoteCard.tsx b/src/Components/Facility/PatientNoteCard.tsx index 8029ec915c1..e3e9234afd3 100644 --- a/src/Components/Facility/PatientNoteCard.tsx +++ b/src/Components/Facility/PatientNoteCard.tsx @@ -1,12 +1,6 @@ import { relativeDate, formatDate } from "../../Utils/utils"; -const PatientNoteCard = ({ - note, - facilityId, -}: { - note: any; - facilityId: any; -}) => { +const PatientNoteCard = ({ note }: { note: any }) => { return ( <div key={note.id} @@ -19,9 +13,9 @@ const PatientNoteCard = ({ </span> <span className="text-gray-700 text-sm"> {note.created_by_object.user_type === "Doctor" - ? note.created_by_object.home_facility !== facilityId - ? "Remote Specialist" - : "" + ? note.created_by_local_user + ? "" + : "Remote Specialist" : note.created_by_object.user_type} </span> </div> diff --git a/src/Components/Facility/PatientNotesList.tsx b/src/Components/Facility/PatientNotesList.tsx index 69dda827f4c..92dacb16958 100644 --- a/src/Components/Facility/PatientNotesList.tsx +++ b/src/Components/Facility/PatientNotesList.tsx @@ -17,7 +17,7 @@ interface PatientNotesProps { const pageSize = RESULTS_PER_PAGE_LIMIT; const PatientNotesList = (props: PatientNotesProps) => { - const { facilityId, reload, setReload } = props; + const { reload, setReload } = props; const dispatch: any = useDispatch(); const initialData: any = { notes: [], cPage: 1, totalPages: 1 }; @@ -105,11 +105,7 @@ const PatientNotesList = (props: PatientNotesProps) => { scrollableTarget="patient-notes-list" > {state.notes.map((note: any) => ( - <PatientNoteCard - note={note} - key={note.id} - facilityId={facilityId} - /> + <PatientNoteCard note={note} key={note.id} /> ))} </InfiniteScroll> ) : ( From 746f19e0b2b6980d4833e462a94ab1aa912ae8ae Mon Sep 17 00:00:00 2001 From: Bhavik Agarwal <bhavikiitian@gmail.com> Date: Fri, 21 Jul 2023 12:18:31 +0530 Subject: [PATCH 08/17] used Page component in PatientNotes page --- src/Components/Patient/PatientNotes.tsx | 26 ++++++++++++------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/Components/Patient/PatientNotes.tsx b/src/Components/Patient/PatientNotes.tsx index f5d09c80473..3b67cc87b84 100644 --- a/src/Components/Patient/PatientNotes.tsx +++ b/src/Components/Patient/PatientNotes.tsx @@ -2,12 +2,12 @@ import { useState, useEffect } from "react"; import { useDispatch } from "react-redux"; import { addPatientNote, getPatient } from "../../Redux/actions"; import * as Notification from "../../Utils/Notifications.js"; -import PageTitle from "../Common/PageTitle"; import CareIcon from "../../CAREUI/icons/CareIcon"; import TextFormField from "../Form/FormFields/TextFormField"; import ButtonV2 from "../Common/components/ButtonV2"; import { NonReadOnlyUsers } from "../../Utils/AuthorizeFor"; import PatientNotesList from "../Facility/PatientNotesList"; +import Page from "../Common/components/Page"; interface PatientNotesProps { patientId: any; @@ -57,18 +57,16 @@ const PatientNotes = (props: PatientNotesProps) => { }, [dispatch, patientId]); return ( - <div className="w-full flex flex-col"> - <PageTitle - title="Patient Notes" - className="mb-5" - crumbsReplacements={{ - [facilityId]: { name: facilityName }, - [patientId]: { name: patientName }, - }} - backUrl={`/facility/${facilityId}/patient/${patientId}`} - /> - - <div className="mx-3 my-2 px-2 py-2 sm:mx-10 sm:my-5 bg-white sm:px-5 sm:py-5 rounded-lg grow"> + <Page + title="Patient Notes" + className="h-screen flex flex-col" + crumbsReplacements={{ + [facilityId]: { name: facilityName }, + [patientId]: { name: patientName }, + }} + backUrl={`/facility/${facilityId}/patient/${patientId}`} + > + <div className="mx-3 my-2 px-2 py-2 sm:mx-10 sm:my-5 bg-white sm:px-5 sm:py-5 rounded-lg grow flex flex-col"> <PatientNotesList patientId={patientId} facilityId={facilityId} @@ -100,7 +98,7 @@ const PatientNotes = (props: PatientNotesProps) => { </ButtonV2> </div> </div> - </div> + </Page> ); }; From 4cf45f61e529eca23ea81fc60e2c772336abbfb6 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad <rithvikn2001@gmail.com> Date: Fri, 21 Jul 2023 07:05:29 +0000 Subject: [PATCH 09/17] Apply suggestions from code review --- src/Components/Facility/PatientNotesSlideover.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Components/Facility/PatientNotesSlideover.tsx b/src/Components/Facility/PatientNotesSlideover.tsx index d377064c6dc..e35004932d2 100644 --- a/src/Components/Facility/PatientNotesSlideover.tsx +++ b/src/Components/Facility/PatientNotesSlideover.tsx @@ -64,7 +64,7 @@ export default function PatientNotesSlideover(props: PatientNotesProps) { > {!show ? ( <div className="flex justify-around items-center w-full p-2 rounded-t-md bg-primary-800 text-white"> - <span className="font-semibold">Doctor's Notes</span> + <span className="font-semibold">{"Doctor's Notes"}</span> <div className={classNames( "flex items-center justify-center w-8 h-8 cursor-pointer rounded text-gray-100 text-opacity-70 hover:text-opacity-100 bg-primary-800 hover:bg-primary-700", @@ -79,7 +79,7 @@ export default function PatientNotesSlideover(props: PatientNotesProps) { <div className="bg-white sm:rounded-t-md w-full h-screen sm:h-[500px] flex flex-col border-2 border-b-0 pb-3 border-primary-800 transition-all overflow-clip -translate-y-0 "> {/* Doctor Notes Header */} <div className="flex justify-between items-center w-full p-2 px-4 bg-primary-800 text-white"> - <span className="font-semibold">Doctor's Notes</span> + <span className="font-semibold">{"Doctor's Notes"}</span> <div className={classNames( "flex items-center justify-center w-8 h-8 cursor-pointer rounded text-gray-100 text-opacity-70 hover:text-opacity-100 bg-primary-800 hover:bg-primary-700", From 5afb43f87163de346c1fe6701aaeaffef3d16b9d Mon Sep 17 00:00:00 2001 From: Bhavik Agarwal <bhavikiitian@gmail.com> Date: Wed, 26 Jul 2023 22:38:28 +0530 Subject: [PATCH 10/17] added close button --- .../Facility/ConsultationDetails.tsx | 15 +++- .../Facility/PatientNotesSlideover.tsx | 76 ++++++++++++------- 2 files changed, 58 insertions(+), 33 deletions(-) diff --git a/src/Components/Facility/ConsultationDetails.tsx b/src/Components/Facility/ConsultationDetails.tsx index 6848c6cbe3a..24655a2f7de 100644 --- a/src/Components/Facility/ConsultationDetails.tsx +++ b/src/Components/Facility/ConsultationDetails.tsx @@ -78,6 +78,7 @@ export const ConsultationDetails = (props: any) => { useState(false); const [openDischargeDialog, setOpenDischargeDialog] = useState(false); const [showAutomatedRounds, setShowAutomatedRounds] = useState(true); + const [showPatientNotesPopup, setShowPatientNotesPopup] = useState(false); const getPatientGender = (patientData: any) => GENDER_TYPES.find((i) => i.id === patientData.gender)?.text; @@ -325,12 +326,12 @@ export const ConsultationDetails = (props: any) => { > Patient Details </Link> - <Link - href={`/facility/${patientData.facility}/patient/${patientData.id}/notes`} + <ButtonV2 + onClick={() => setShowPatientNotesPopup(true)} className="btn btn-primary m-1 w-full hover:text-white" > Doctor's Notes - </Link> + </ButtonV2> </div> </div> </nav> @@ -1239,7 +1240,13 @@ export const ConsultationDetails = (props: any) => { setShow={setShowDoctors} /> - <PatientNotesSlideover patientId={patientId} facilityId={facilityId} /> + {showPatientNotesPopup && ( + <PatientNotesSlideover + patientId={patientId} + facilityId={facilityId} + setShowPatientNotesPopup={setShowPatientNotesPopup} + /> + )} </div> ); }; diff --git a/src/Components/Facility/PatientNotesSlideover.tsx b/src/Components/Facility/PatientNotesSlideover.tsx index e35004932d2..784d856b01f 100644 --- a/src/Components/Facility/PatientNotesSlideover.tsx +++ b/src/Components/Facility/PatientNotesSlideover.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, Dispatch, SetStateAction } from "react"; import { getPatient, addPatientNote } from "../../Redux/actions"; import * as Notification from "../../Utils/Notifications.js"; import { useDispatch } from "react-redux"; @@ -8,10 +8,12 @@ import CareIcon from "../../CAREUI/icons/CareIcon"; import { classNames } from "../../Utils/utils"; import TextFormField from "../Form/FormFields/TextFormField"; import ButtonV2 from "../Common/components/ButtonV2"; +import { make as Link } from "../Common/components/Link.bs"; interface PatientNotesProps { - patientId: any; - facilityId: any; + patientId: string; + facilityId: string; + setShowPatientNotesPopup: Dispatch<SetStateAction<boolean>>; } export default function PatientNotesSlideover(props: PatientNotesProps) { @@ -22,7 +24,7 @@ export default function PatientNotesSlideover(props: PatientNotesProps) { const dispatch = useDispatch(); - const { facilityId, patientId } = props; + const { facilityId, patientId, setShowPatientNotesPopup } = props; const onAddNote = () => { const payload = { @@ -53,42 +55,58 @@ export default function PatientNotesSlideover(props: PatientNotesProps) { fetchPatientName(); }, [dispatch, patientId]); + const notesActionIcons = ( + <div className="flex gap-1"> + {show && ( + <Link + className={classNames( + "flex h-8 w-8 cursor-pointer items-center justify-center rounded bg-primary-800 text-gray-100 text-opacity-70 hover:bg-primary-700 hover:text-opacity-100" + )} + href={`/facility/${facilityId}/patient/${patientId}/notes`} + > + <CareIcon className="care-l-window-maximize text-lg transition-all delay-150 duration-300 ease-out" /> + </Link> + )} + <div + className={classNames( + "flex h-8 w-8 cursor-pointer items-center justify-center rounded bg-primary-800 text-gray-100 text-opacity-70 hover:bg-primary-700 hover:text-opacity-100", + show ? "rotate-180" : "" + )} + onClick={() => setShow(!show)} + > + <CareIcon className="care-l-angle-up text-lg transition-all delay-150 duration-300 ease-out" /> + </div> + <div + className={classNames( + "flex h-8 w-8 cursor-pointer items-center justify-center rounded bg-primary-800 text-gray-100 text-opacity-70 hover:bg-primary-700 hover:text-opacity-100" + )} + onClick={() => setShowPatientNotesPopup(false)} + > + <CareIcon className="care-l-times text-lg transition-all delay-150 duration-300 ease-out" /> + </div> + </div> + ); + return ( <div className={classNames( - "fixed sm:right-20 bottom-0 z-20", + "fixed bottom-0 z-20 sm:right-8", show - ? "w-screen h-screen sm:h-fit sm:w-[400px] right-0" - : "w-[250px] right-10" + ? "right-0 h-screen w-screen sm:h-fit sm:w-[400px]" + : "right-8 w-[250px]" )} > {!show ? ( - <div className="flex justify-around items-center w-full p-2 rounded-t-md bg-primary-800 text-white"> + <div className="flex w-full items-center justify-around rounded-t-md bg-primary-800 p-2 text-white"> <span className="font-semibold">{"Doctor's Notes"}</span> - <div - className={classNames( - "flex items-center justify-center w-8 h-8 cursor-pointer rounded text-gray-100 text-opacity-70 hover:text-opacity-100 bg-primary-800 hover:bg-primary-700", - show ? "rotate-180" : "" - )} - onClick={() => setShow(!show)} - > - <CareIcon className="care-l-angle-up text-lg transition-all duration-300 delay-150 ease-out" /> - </div> + {notesActionIcons} </div> ) : ( - <div className="bg-white sm:rounded-t-md w-full h-screen sm:h-[500px] flex flex-col border-2 border-b-0 pb-3 border-primary-800 transition-all overflow-clip -translate-y-0 "> + <div className="flex h-screen w-full -translate-y-0 flex-col text-clip border-2 border-b-0 border-primary-800 bg-white pb-3 transition-all sm:h-[500px] sm:rounded-t-md "> {/* Doctor Notes Header */} - <div className="flex justify-between items-center w-full p-2 px-4 bg-primary-800 text-white"> + <div className="flex w-full items-center justify-between bg-primary-800 p-2 px-4 text-white"> <span className="font-semibold">{"Doctor's Notes"}</span> - <div - className={classNames( - "flex items-center justify-center w-8 h-8 cursor-pointer rounded text-gray-100 text-opacity-70 hover:text-opacity-100 bg-primary-800 hover:bg-primary-700", - show ? "rotate-180" : "" - )} - onClick={() => setShow(!show)} - > - <CareIcon className="care-l-angle-up text-lg transition-all duration-300 delay-150 ease-out" /> - </div> + {notesActionIcons} </div> {/* Doctor Notes Body */} <PatientNotesList @@ -97,7 +115,7 @@ export default function PatientNotesSlideover(props: PatientNotesProps) { reload={reload} setReload={setReload} /> - <div className="flex items-center mx-4 relative"> + <div className="relative mx-4 flex items-center"> <TextFormField name="note" value={noteField} From 5ee889da2f08c5ec857432b940b6270abfcb3654 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad <rithvikn2001@gmail.com> Date: Thu, 27 Jul 2023 04:44:07 +0000 Subject: [PATCH 11/17] Apply suggestions from code review --- src/Components/Facility/PatientNotesSlideover.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Components/Facility/PatientNotesSlideover.tsx b/src/Components/Facility/PatientNotesSlideover.tsx index 784d856b01f..2832898e20b 100644 --- a/src/Components/Facility/PatientNotesSlideover.tsx +++ b/src/Components/Facility/PatientNotesSlideover.tsx @@ -59,9 +59,7 @@ export default function PatientNotesSlideover(props: PatientNotesProps) { <div className="flex gap-1"> {show && ( <Link - className={classNames( - "flex h-8 w-8 cursor-pointer items-center justify-center rounded bg-primary-800 text-gray-100 text-opacity-70 hover:bg-primary-700 hover:text-opacity-100" - )} + className="flex h-8 w-8 cursor-pointer items-center justify-center rounded bg-primary-800 text-gray-100 text-opacity-70 hover:bg-primary-700 hover:text-opacity-100" href={`/facility/${facilityId}/patient/${patientId}/notes`} > <CareIcon className="care-l-window-maximize text-lg transition-all delay-150 duration-300 ease-out" /> @@ -70,16 +68,14 @@ export default function PatientNotesSlideover(props: PatientNotesProps) { <div className={classNames( "flex h-8 w-8 cursor-pointer items-center justify-center rounded bg-primary-800 text-gray-100 text-opacity-70 hover:bg-primary-700 hover:text-opacity-100", - show ? "rotate-180" : "" + show && "rotate-180" )} onClick={() => setShow(!show)} > <CareIcon className="care-l-angle-up text-lg transition-all delay-150 duration-300 ease-out" /> </div> <div - className={classNames( - "flex h-8 w-8 cursor-pointer items-center justify-center rounded bg-primary-800 text-gray-100 text-opacity-70 hover:bg-primary-700 hover:text-opacity-100" - )} + className="flex h-8 w-8 cursor-pointer items-center justify-center rounded bg-primary-800 text-gray-100 text-opacity-70 hover:bg-primary-700 hover:text-opacity-100" onClick={() => setShowPatientNotesPopup(false)} > <CareIcon className="care-l-times text-lg transition-all delay-150 duration-300 ease-out" /> From f41f7f6097e9df59b67c5250f564611c667a5de3 Mon Sep 17 00:00:00 2001 From: Bhavik Agarwal <bhavikiitian@gmail.com> Date: Thu, 27 Jul 2023 20:27:19 +0530 Subject: [PATCH 12/17] enchance doctor notes ux --- src/Components/Facility/PatientNotesSlideover.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Components/Facility/PatientNotesSlideover.tsx b/src/Components/Facility/PatientNotesSlideover.tsx index 2832898e20b..ccfc950e674 100644 --- a/src/Components/Facility/PatientNotesSlideover.tsx +++ b/src/Components/Facility/PatientNotesSlideover.tsx @@ -93,7 +93,10 @@ export default function PatientNotesSlideover(props: PatientNotesProps) { )} > {!show ? ( - <div className="flex w-full items-center justify-around rounded-t-md bg-primary-800 p-2 text-white"> + <div + className="flex w-full cursor-pointer items-center justify-around rounded-t-md bg-primary-800 p-2 text-white" + onClick={() => setShow(!show)} + > <span className="font-semibold">{"Doctor's Notes"}</span> {notesActionIcons} </div> From f63731d937bb865a7bb596d7d1ea04e67af9812b Mon Sep 17 00:00:00 2001 From: Bhavik Agarwal <bhavikiitian@gmail.com> Date: Tue, 8 Aug 2023 00:41:22 +0530 Subject: [PATCH 13/17] Merge branch 'develop' into doctor-notes-redesign From 73df9f487a6cc6fcc1e7fa82dbaf8e64906fc1af Mon Sep 17 00:00:00 2001 From: Bhavik Agarwal <bhavikiitian@gmail.com> Date: Wed, 9 Aug 2023 16:47:10 +0530 Subject: [PATCH 14/17] replace usages of moment with dayjs --- src/Components/Facility/PatientNoteCard.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Components/Facility/PatientNoteCard.tsx b/src/Components/Facility/PatientNoteCard.tsx index e3e9234afd3..6308869dffc 100644 --- a/src/Components/Facility/PatientNoteCard.tsx +++ b/src/Components/Facility/PatientNoteCard.tsx @@ -1,17 +1,17 @@ -import { relativeDate, formatDate } from "../../Utils/utils"; +import { relativeDate, formatDateTime } from "../../Utils/utils"; const PatientNoteCard = ({ note }: { note: any }) => { return ( <div key={note.id} - className="flex p-3 bg-white rounded-lg text-gray-800 mt-4 flex-col w-full border border-gray-300" + className="mt-4 flex w-full flex-col rounded-lg border border-gray-300 bg-white p-3 text-gray-800" > <div className="flex justify-between"> - <span className="text-gray-700 text-sm font-semibold"> + <span className="text-sm font-semibold text-gray-700"> {note.created_by_object?.first_name || "Unknown"}{" "} {note.created_by_object?.last_name} </span> - <span className="text-gray-700 text-sm"> + <span className="text-sm text-gray-700"> {note.created_by_object.user_type === "Doctor" ? note.created_by_local_user ? "" @@ -20,10 +20,10 @@ const PatientNoteCard = ({ note }: { note: any }) => { </span> </div> <span className="whitespace-pre-wrap break-words">{note.note}</span> - <div className="mt-3 text-xs text-gray-500 text-end"> + <div className="mt-3 text-end text-xs text-gray-500"> <div className="tooltip inline"> <span className="tooltip-text tooltip-left"> - {formatDate(note.created_date)} + {formatDateTime(note.created_date)} </span> {relativeDate(note.created_date)} </div> From 3bea8c1c9987bfd9c5bc3dc97af5fa5a7bbf0baf Mon Sep 17 00:00:00 2001 From: Bhavik Agarwal <bhavikiitian@gmail.com> Date: Tue, 22 Aug 2023 19:12:41 +0530 Subject: [PATCH 15/17] Added user role in brackets --- src/Components/Facility/PatientNoteCard.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Components/Facility/PatientNoteCard.tsx b/src/Components/Facility/PatientNoteCard.tsx index 6308869dffc..c1249c2021a 100644 --- a/src/Components/Facility/PatientNoteCard.tsx +++ b/src/Components/Facility/PatientNoteCard.tsx @@ -6,17 +6,19 @@ const PatientNoteCard = ({ note }: { note: any }) => { key={note.id} className="mt-4 flex w-full flex-col rounded-lg border border-gray-300 bg-white p-3 text-gray-800" > - <div className="flex justify-between"> + <div className="flex"> <span className="text-sm font-semibold text-gray-700"> {note.created_by_object?.first_name || "Unknown"}{" "} {note.created_by_object?.last_name} </span> - <span className="text-sm text-gray-700"> + <span className="pl-1 text-sm text-gray-700"> + {" ("} {note.created_by_object.user_type === "Doctor" ? note.created_by_local_user ? "" : "Remote Specialist" : note.created_by_object.user_type} + {")"} </span> </div> <span className="whitespace-pre-wrap break-words">{note.note}</span> From 5ce435aebe8141d51fdc58c52c5a36ea6c05a2b6 Mon Sep 17 00:00:00 2001 From: Bhavik Agarwal <bhavikiitian@gmail.com> Date: Thu, 24 Aug 2023 16:41:54 +0530 Subject: [PATCH 16/17] added message listener for patient notes --- src/Components/Facility/PatientNotesSlideover.tsx | 11 +++++++++++ src/Components/Patient/PatientNotes.tsx | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/Components/Facility/PatientNotesSlideover.tsx b/src/Components/Facility/PatientNotesSlideover.tsx index ccfc950e674..be95d768188 100644 --- a/src/Components/Facility/PatientNotesSlideover.tsx +++ b/src/Components/Facility/PatientNotesSlideover.tsx @@ -9,6 +9,7 @@ import { classNames } from "../../Utils/utils"; import TextFormField from "../Form/FormFields/TextFormField"; import ButtonV2 from "../Common/components/ButtonV2"; import { make as Link } from "../Common/components/Link.bs"; +import { useMessageListener } from "../../Common/hooks/useMessageListener"; interface PatientNotesProps { patientId: string; @@ -43,6 +44,16 @@ export default function PatientNotesSlideover(props: PatientNotesProps) { }); }; + useMessageListener((data) => { + if ( + data?.status == "updated" && + data?.facility_id == facilityId && + data?.patient_id == patientId + ) { + setReload(true); + } + }); + useEffect(() => { async function fetchPatientName() { if (patientId) { diff --git a/src/Components/Patient/PatientNotes.tsx b/src/Components/Patient/PatientNotes.tsx index 6011be6b30d..4513c196d9f 100644 --- a/src/Components/Patient/PatientNotes.tsx +++ b/src/Components/Patient/PatientNotes.tsx @@ -8,6 +8,7 @@ import ButtonV2 from "../Common/components/ButtonV2"; import { NonReadOnlyUsers } from "../../Utils/AuthorizeFor"; import PatientNotesList from "../Facility/PatientNotesList"; import Page from "../Common/components/Page"; +import { useMessageListener } from "../../Common/hooks/useMessageListener"; interface PatientNotesProps { patientId: any; @@ -56,6 +57,16 @@ const PatientNotes = (props: PatientNotesProps) => { fetchPatientName(); }, [dispatch, patientId]); + useMessageListener((data) => { + if ( + data?.status == "updated" && + data?.facility_id == facilityId && + data?.patient_id == patientId + ) { + setReload(true); + } + }); + return ( <Page title="Patient Notes" From 580a5777c479090f87251d812ade6345504b6e16 Mon Sep 17 00:00:00 2001 From: Bhavik Agarwal <bhavikiitian@gmail.com> Date: Mon, 28 Aug 2023 21:19:26 +0530 Subject: [PATCH 17/17] added notification on patient note creation --- src/Common/constants.tsx | 5 +++++ src/Components/Notifications/NotificationsList.tsx | 2 ++ 2 files changed, 7 insertions(+) diff --git a/src/Common/constants.tsx b/src/Common/constants.tsx index e8ab4867764..09ce7463f71 100644 --- a/src/Common/constants.tsx +++ b/src/Common/constants.tsx @@ -608,6 +608,11 @@ export const NOTIFICATION_EVENTS = [ text: "Shifting Updated", icon: "fa-solid fa-truck-medical", }, + { + id: "PATIENT_NOTE_ADDED", + text: "Patient Note Added", + icon: "fa-solid fa-message", + }, ]; export const BREATHLESSNESS_LEVEL = [ diff --git a/src/Components/Notifications/NotificationsList.tsx b/src/Components/Notifications/NotificationsList.tsx index f6afa6cccd8..141038340a0 100644 --- a/src/Components/Notifications/NotificationsList.tsx +++ b/src/Components/Notifications/NotificationsList.tsx @@ -66,6 +66,8 @@ const NotificationTile = ({ return `/facility/${data.facility}/patient/${data.patient}/consultation/${data.consultation}/daily-rounds/${data.daily_round}`; case "INVESTIGATION_SESSION_CREATED": return `/facility/${data.facility}/patient/${data.patient}/consultation/${data.consultation}/investigation/${data.session}`; + case "PATIENT_NOTE_ADDED": + return `/facility/${data.facility}/patient/${data.patient}/notes`; case "MESSAGE": return "/notice_board/"; default: