diff --git a/.env b/.env index fcabed6db34..38fd950b0d8 100644 --- a/.env +++ b/.env @@ -1,11 +1,10 @@ # Whitelabelling envs -REACT_APP_TITLE="CARE" -REACT_APP_META_DESCRIPTION="CoronaSafe Network is an open-source public utility designed by a multi-disciplinary team of innovators and volunteers. CoronaSafe Care is a Digital Public Good recognised by United Nations." +REACT_APP_TITLE=CARE +REACT_APP_META_DESCRIPTION=CoronaSafe Network is an open-source public utility designed by a multi-disciplinary team of innovators and volunteers. CoronaSafe Care is a Digital Public Good recognised by United Nations. REACT_APP_COVER_IMAGE=https://cdn.coronasafe.network/care_logo.svg REACT_APP_COVER_IMAGE_ALT=https://cdn.coronasafe.network/care_logo.svg -REACT_APP_CONFIG="" -REACT_PUBLIC_URL="https://care.coronasafe.in" +REACT_PUBLIC_URL=https://care.coronasafe.in # Dev envs ESLINT_NO_DEV_ERRORS=true diff --git a/.github/workflows/cypress.yaml b/.github/workflows/cypress.yaml index 4f23fbe4937..130f360d2ef 100644 --- a/.github/workflows/cypress.yaml +++ b/.github/workflows/cypress.yaml @@ -24,13 +24,22 @@ jobs: repository: coronasafe/care path: care - - name: Run docker compose up on care 🐳 - run: cd care && touch .env && make docker_config_file=docker-compose.pre-built.yaml up && cd .. && sleep 60s - # Voluntarily kept 60 seconds delay to wait for migrations to complete. + - name: Start care docker containers 🐳 + run: | + cd care + make docker_config_file=docker-compose.pre-built.yaml up + while docker compose exec backend bash -c "python manage.py showmigrations 2>/dev/null | cat | grep -q '\[ \]'"; do + >&2 echo "Migrations are not yet applied - sleeping" + sleep 5 + done + echo "Migrations are applied" + cd .. - - name: Run Django collectstatic and load dummy data on care 🐍 + - name: Load dummy data into care backend 📂 run: | - docker exec care python manage.py load_dummy_data + cd care + docker compose exec backend bash -c "python manage.py load_dummy_data" + cd .. - name: Check care is up ♻ run: curl -o /dev/null -s -w "%{http_code}\n" http://localhost:9000 @@ -92,10 +101,3 @@ jobs: name: cypress-screenshots path: cypress/screenshots - # Test run video was always captured, so this action uses "always()" condition - - name: Upload cypress videos 📹 - uses: actions/upload-artifact@v3 - if: always() - with: - name: cypress-videos - path: cypress/videos diff --git a/.gitignore b/.gitignore index b97356ed20f..4ca589aab39 100644 --- a/.gitignore +++ b/.gitignore @@ -54,6 +54,7 @@ public/build-meta.json # Using NPM yarn.lock pnpm-lock.yaml +bun.lockb # Cypress cypress/downloads diff --git a/cypress/e2e/assets_spec/asset_homepage.cy.ts b/cypress/e2e/assets_spec/asset_homepage.cy.ts new file mode 100644 index 00000000000..fa24adf5b21 --- /dev/null +++ b/cypress/e2e/assets_spec/asset_homepage.cy.ts @@ -0,0 +1,89 @@ +/// + +import { cy, describe, before, beforeEach, it, afterEach } from "local-cypress"; +import { AssetSearchPage } from "../../pageobject/Asset/AssetSearch"; +import { AssetQRScanPage } from "../../pageobject/Asset/AssetQRScan"; +import { AssetPagination } from "../../pageobject/Asset/AssetPagination"; +import { AssetFilters } from "../../pageobject/Asset/AssetFilters"; +import LoginPage from "../../pageobject/Login/LoginPage"; +import { v4 as uuidv4 } from "uuid"; + +describe("Asset Tab", () => { + const assetSearchPage = new AssetSearchPage(); + const assetQRScanPage = new AssetQRScanPage(); + const assetPagination = new AssetPagination(); + const assetFilters = new AssetFilters(); + const loginPage = new LoginPage(); + const assetName = "Dummy Camera 10"; + const qrCode = uuidv4(); + const serialNumber = Math.floor(Math.random() * 10 ** 10).toString(); + + before(() => { + loginPage.loginAsDisctrictAdmin(); + cy.saveLocalStorage(); + }); + + beforeEach(() => { + cy.restoreLocalStorage(); + cy.awaitUrl("/assets"); + }); + + // search for a element + + it("Search Asset Name/QR_ID/Serial_number", () => { + assetSearchPage.typeSearchKeyword(assetName); + assetSearchPage.pressEnter(); + assetSearchPage.verifyBadgeContent(assetName); + assetSearchPage.clickAssetByName(assetName); + assetSearchPage.clickUpdateButton(); + assetSearchPage.clearAndTypeQRCode(qrCode); + assetSearchPage.clearAndTypeSerialNumber(serialNumber); + assetSearchPage.clickAssetSubmitButton(); + assetSearchPage.visitAssetsPage(); + assetSearchPage.typeSearchKeyword(qrCode); + assetSearchPage.pressEnter(); + assetSearchPage.verifyAssetListContains(assetName); + assetSearchPage.verifyBadgeContent(qrCode); + assetSearchPage.typeSearchKeyword(serialNumber); + assetSearchPage.verifyAssetListContains(assetName); + assetSearchPage.verifyBadgeContent(serialNumber); + }); + + // scan a asset qr code + + it("Scan Asset QR", () => { + assetQRScanPage.scanAssetQR(); + }); + + // filter the asset and verify the badges are there + + it("Filter Asset", () => { + assetFilters.filterAssets( + "Dummy Facility 1", + "INTERNAL", + "ACTIVE", + "ONVIF Camera", + "Camera Loc" + ); + assetFilters.clickadvancefilter(); + assetFilters.clickslideoverbackbutton(); // to verify the back button doesn't clear applied filters + assetFilters.assertFacilityText("Dummy Facility 1"); + assetFilters.assertAssetTypeText("INTERNAL"); + assetFilters.assertAssetClassText("ONVIF"); + assetFilters.assertStatusText("ACTIVE"); + assetFilters.assertLocationText("Camera Locations"); + assetFilters.clickadvancefilter(); + assetFilters.clearFilters(); + }); + + // Verify the pagination in the page + + it("Next/Previous Page", () => { + assetPagination.navigateToNextPage(); + assetPagination.navigateToPreviousPage(); + }); + + afterEach(() => { + cy.saveLocalStorage(); + }); +}); diff --git a/cypress/e2e/assets_spec/asset_tab.cy.ts b/cypress/e2e/assets_spec/asset_tab.cy.ts deleted file mode 100644 index 0abf11fcaf0..00000000000 --- a/cypress/e2e/assets_spec/asset_tab.cy.ts +++ /dev/null @@ -1,61 +0,0 @@ -/// - -import { cy, describe, before, beforeEach, it, afterEach } from "local-cypress"; -import { AssetSearchPage } from "../../pageobject/Asset/AssetSearch"; -import { AssetQRScanPage } from "../../pageobject/Asset/AssetQRScan"; -import { AssetPagination } from "../../pageobject/Asset/AssetPagination"; -import { AssetFilters } from "../../pageobject/Asset/AssetFilters"; - -describe("Asset Tab", () => { - const assetSearchPage = new AssetSearchPage(); - const assetQRScanPage = new AssetQRScanPage(); - const assetPagination = new AssetPagination(); - const assetFilters = new AssetFilters(); - - before(() => { - cy.loginByApi("devdistrictadmin", "Coronasafe@123"); - cy.saveLocalStorage(); - }); - - beforeEach(() => { - cy.restoreLocalStorage(); - cy.awaitUrl("/assets"); - }); - - // search for a element - - it("Search Asset Name", () => { - const initialUrl = cy.url(); - assetSearchPage.typeSearchKeyword("dummy camera 30"); - assetSearchPage.pressEnter(); - assetSearchPage.verifyUrlChanged(initialUrl); - }); - - // scan a asset qr code - - it("Scan Asset QR", () => { - assetQRScanPage.scanAssetQR(); - }); - - // filter the asset and verify the badges are there - - it("Filter Asset", () => { - assetFilters.filterAssets( - "Dummy Facility 1", - "INTERNAL", - "ACTIVE", - "ONVIF Camera" - ); - }); - - // Verify the pagination in the page - - it("Next/Previous Page", () => { - assetPagination.navigateToNextPage(); - assetPagination.navigateToPreviousPage(); - }); - - afterEach(() => { - cy.saveLocalStorage(); - }); -}); diff --git a/cypress/pageobject/Asset/AssetFilters.ts b/cypress/pageobject/Asset/AssetFilters.ts index 57cc893bdb7..a16b61f4fc5 100644 --- a/cypress/pageobject/Asset/AssetFilters.ts +++ b/cypress/pageobject/Asset/AssetFilters.ts @@ -3,7 +3,8 @@ export class AssetFilters { facilityName: string, assetType: string, assetStatus: string, - assetClass: string + assetClass: string, + assetLocation: string ) { cy.contains("Advanced Filters").click(); cy.get("input[name='Facilities']") @@ -27,6 +28,42 @@ export class AssetFilters { .then(() => { cy.get("[role='option']").contains(assetClass).click(); }); + cy.get("#Facilities-location") + .click() + .type(assetLocation) + .then(() => { + cy.get("[role='option']").contains(assetLocation).click(); + }); cy.contains("Apply").click(); } + clearFilters() { + cy.intercept("GET", "**/api/v1/asset/**").as("clearAssets"); + cy.get("#clear-filter").click(); + cy.wait("@clearAssets").its("response.statusCode").should("eq", 200); + cy.url().should("match", /\/assets$/); + } + clickadvancefilter() { + cy.intercept("GET", "**/api/v1/getallfacilities/**").as("advancefilter"); + cy.get("#advanced-filter").click(); + cy.wait("@advancefilter").its("response.statusCode").should("eq", 200); + } + clickslideoverbackbutton() { + cy.get("#close-slide-over").click(); + } + // Assertions + assertFacilityText(text) { + cy.get("[data-testid=Facility]").should("contain", text); + } + assertAssetTypeText(text) { + cy.get("[data-testid='Asset Type']").should("contain", text); + } + assertAssetClassText(text) { + cy.get("[data-testid='Asset Class']").should("contain", text); + } + assertStatusText(text) { + cy.get("[data-testid=Status]").should("contain", text); + } + assertLocationText(text) { + cy.get("[data-testid=Location]").should("contain", text); + } } diff --git a/cypress/pageobject/Asset/AssetSearch.ts b/cypress/pageobject/Asset/AssetSearch.ts index 315a414a62b..b1ccb2f71c9 100644 --- a/cypress/pageobject/Asset/AssetSearch.ts +++ b/cypress/pageobject/Asset/AssetSearch.ts @@ -1,16 +1,24 @@ export class AssetSearchPage { typeSearchKeyword(keyword: string) { - cy.get("[name='search']").type(keyword); + cy.get("#search").click().clear(); + cy.get("#search").click().type(keyword); } pressEnter() { cy.get("[name='search']").type("{enter}"); } - verifyUrlChanged(initialUrl: string) { - cy.url().should((currentUrl) => { - expect(currentUrl).not.to.equal(initialUrl); - }); + clickAssetByName(assetName: string) { + cy.intercept("GET", "**/api/v1/asset/**").as("clearAssets"); + cy.get("[data-testid='created-asset-list']").contains(assetName).click(); + cy.wait("@clearAssets").its("response.statusCode").should("eq", 200); + } + + verifyBadgeContent(expectedText: string) { + cy.get("[data-testid='Name/Serial No./QR ID']").should( + "contain", + expectedText + ); } verifyAssetIsPresent(assetName: string) { @@ -18,4 +26,35 @@ export class AssetSearchPage { .first() .should("contain", assetName); } + + clickUpdateButton() { + cy.get("[data-testid='asset-update-button']").contains("Update").click(); + } + + clearAndTypeQRCode(qrCode: string) { + cy.get("#qr_code_id").clear(); + cy.get("#qr_code_id").click().type(qrCode); + } + + clearAndTypeSerialNumber(serialNumber: string) { + cy.get("#serial-number").clear(); + cy.get("#serial-number").click().type(serialNumber); + } + + clickAssetSubmitButton() { + cy.intercept("GET", "**/api/v1/asset/**").as("getAssets"); + cy.get("#submit").click(); + cy.wait("@getAssets").its("response.statusCode").should("eq", 200); + } + + visitAssetsPage() { + cy.visit("/assets"); + } + + verifyAssetListContains(dummyCameraText: string) { + cy.get("[data-testid='created-asset-list']").should( + "contain", + dummyCameraText + ); + } } diff --git a/src/CAREUI/interactive/SlideOver.tsx b/src/CAREUI/interactive/SlideOver.tsx index 3eae13869cb..34e1c615f94 100644 --- a/src/CAREUI/interactive/SlideOver.tsx +++ b/src/CAREUI/interactive/SlideOver.tsx @@ -109,6 +109,7 @@ export default function SlideOver({ >
-
+
navigate(`/facility/${facilityId}/location`, { diff --git a/src/Components/Facility/ConsultationCard.tsx b/src/Components/Facility/ConsultationCard.tsx index 467729f0f73..f6b4484b477 100644 --- a/src/Components/Facility/ConsultationCard.tsx +++ b/src/Components/Facility/ConsultationCard.tsx @@ -5,6 +5,7 @@ import ButtonV2 from "../Common/components/ButtonV2"; import { NonReadOnlyUsers } from "../../Utils/AuthorizeFor"; import RelativeDateUserMention from "../Common/RelativeDateUserMention"; import useConfig from "../../Common/hooks/useConfig"; +import Chip from "../../CAREUI/display/Chip"; interface ConsultationProps { itemData: ConsultationModel; @@ -70,6 +71,15 @@ export const ConsultationCard = (props: ConsultationProps) => {
{formatDateTime(itemData.admission_date)} + {itemData.is_readmission && ( + + )}
diff --git a/src/Components/Facility/ConsultationDetails.tsx b/src/Components/Facility/ConsultationDetails.tsx deleted file mode 100644 index 401f113a812..00000000000 --- a/src/Components/Facility/ConsultationDetails.tsx +++ /dev/null @@ -1,1324 +0,0 @@ -import { AssetBedModel, AssetClass, AssetData } from "../Assets/AssetTypes"; -import { - CONSULTATION_TABS, - DISCHARGE_REASONS, - GENDER_TYPES, - OptionsType, - SYMPTOM_CHOICES, -} from "../../Common/constants"; -import { - BedModel, - ConsultationModel, - FacilityModel, - ICD11DiagnosisModel, -} from "./models"; -import { - getConsultation, - getPatient, - getPermittedFacility, - listAssetBeds, -} from "../../Redux/actions"; -import { statusType, useAbortableEffect } from "../../Common/utils"; -import { lazy, useCallback, useEffect, useState } from "react"; - -import { ABGPlots } from "./Consultations/ABGPlots"; -import ButtonV2 from "../Common/components/ButtonV2"; -import CareIcon from "../../CAREUI/icons/CareIcon"; -import Chip from "../../CAREUI/display/Chip"; -import { DailyRoundsList } from "./Consultations/DailyRoundsList"; -import { DialysisPlots } from "./Consultations/DialysisPlots"; -import DischargeModal from "./DischargeModal"; -import DischargeSummaryModal from "./DischargeSummaryModal"; -import DoctorVideoSlideover from "./DoctorVideoSlideover"; -import { Feed } from "./Consultations/Feed"; -import { FileUpload } from "../Patient/FileUpload"; -import HL7PatientVitalsMonitor from "../VitalsMonitor/HL7PatientVitalsMonitor"; -import InvestigationTab from "./Investigations/investigationsTab"; -import { make as Link } from "../Common/components/Link.bs"; -import { NeurologicalTable } from "./Consultations/NeurologicalTables"; -import { NonReadOnlyUsers } from "../../Utils/AuthorizeFor"; -import { NursingPlot } from "./Consultations/NursingPlot"; -import { NutritionPlots } from "./Consultations/NutritionPlots"; -import PatientInfoCard from "../Patient/PatientInfoCard"; -import { PatientModel } from "../Patient/models"; -import PrescriptionsTable from "../Medicine/PrescriptionsTable"; -import { PressureSoreDiagrams } from "./Consultations/PressureSoreDiagrams"; -import { PrimaryParametersPlot } from "./Consultations/PrimaryParametersPlot"; -import ReadMore from "../Common/components/Readmore"; -import VentilatorPatientVitalsMonitor from "../VitalsMonitor/VentilatorPatientVitalsMonitor"; -import { VentilatorPlot } from "./Consultations/VentilatorPlot"; -import { formatDate, formatDateTime, relativeTime } from "../../Utils/utils"; - -import { navigate } from "raviger"; -import { useDispatch } from "react-redux"; -import { useQueryParams } from "raviger"; -import { useTranslation } from "react-i18next"; -import { triggerGoal } from "../Common/Plausible"; -import useVitalsAspectRatioConfig from "../VitalsMonitor/useVitalsAspectRatioConfig"; -import useAuthUser from "../../Common/hooks/useAuthUser"; -import PrescriptionAdministrationsTable from "../Medicine/PrescriptionAdministrationsTable"; - -const Loading = lazy(() => import("../Common/Loading")); -const PageTitle = lazy(() => import("../Common/PageTitle")); -const symptomChoices = [...SYMPTOM_CHOICES]; - -export const ConsultationDetails = (props: any) => { - const { t } = useTranslation(); - const { facilityId, patientId, consultationId } = props; - const tab = props.tab.toUpperCase(); - const dispatch: any = useDispatch(); - const [isLoading, setIsLoading] = useState(false); - const [showDoctors, setShowDoctors] = useState(false); - const [qParams, _] = useQueryParams(); - - const [consultationData, setConsultationData] = useState( - {} as ConsultationModel - ); - const [patientData, setPatientData] = useState({}); - const [openDischargeSummaryDialog, setOpenDischargeSummaryDialog] = - useState(false); - const [openDischargeDialog, setOpenDischargeDialog] = useState(false); - const [showAutomatedRounds, setShowAutomatedRounds] = useState(true); - - const getPatientGender = (patientData: any) => - GENDER_TYPES.find((i) => i.id === patientData.gender)?.text; - - const getPatientAddress = (patientData: 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?.medical_history?.length) { - const medHis = patientData.medical_history; - return medHis.map((item: any) => item.disease).join(", "); - } else { - return "None"; - } - }; - - const [hl7SocketUrl, setHL7SocketUrl] = useState(); - const [ventilatorSocketUrl, setVentilatorSocketUrl] = useState(); - const [monitorBedData, setMonitorBedData] = useState(); - const [ventilatorBedData, setVentilatorBedData] = useState(); - const authUser = useAuthUser(); - - useEffect(() => { - if ( - !consultationData.facility || - !consultationData.current_bed?.bed_object.id - ) - return; - - const fetchData = async () => { - const [facilityRes, assetBedRes] = await Promise.all([ - dispatch(getPermittedFacility(consultationData.facility as any)), - dispatch( - listAssetBeds({ - facility: consultationData.facility as any, - bed: consultationData.current_bed?.bed_object.id, - }) - ), - ]); - - const { middleware_address } = facilityRes.data as FacilityModel; - const assetBeds = assetBedRes?.data?.results as AssetBedModel[]; - - const monitorBedData = assetBeds?.find( - (i) => i.asset_object?.asset_class === AssetClass.HL7MONITOR - ); - setMonitorBedData(monitorBedData); - const assetDataForMonitor = monitorBedData?.asset_object; - const hl7Meta = assetDataForMonitor?.meta; - const hl7Middleware = hl7Meta?.middleware_hostname || middleware_address; - if (hl7Middleware && hl7Meta?.local_ip_address) { - setHL7SocketUrl( - `wss://${hl7Middleware}/observations/${hl7Meta.local_ip_address}` - ); - } - - const consultationBedVentilator = - consultationData?.current_bed?.assets_objects?.find( - (i) => i.asset_class === AssetClass.VENTILATOR - ); - let ventilatorBedData; - if (consultationBedVentilator) { - ventilatorBedData = { - asset_object: consultationBedVentilator, - bed_object: consultationData?.current_bed?.bed_object, - } as AssetBedModel; - } else { - ventilatorBedData = assetBeds?.find( - (i) => i.asset_object.asset_class === AssetClass.VENTILATOR - ); - } - setVentilatorBedData(ventilatorBedData); - const ventilatorMeta = ventilatorBedData?.asset_object?.meta; - const ventilatorMiddleware = - ventilatorMeta?.middleware_hostname || middleware_address; - if (ventilatorMiddleware && ventilatorMeta?.local_ip_address) { - setVentilatorSocketUrl( - `wss://${ventilatorMiddleware}/observations/${ventilatorMeta?.local_ip_address}` - ); - } - - if ( - !(hl7Middleware && hl7Meta?.local_ip_address) && - !(ventilatorMiddleware && ventilatorMeta?.local_ip_address) - ) { - setHL7SocketUrl(undefined); - setVentilatorSocketUrl(undefined); - } - }; - - fetchData(); - }, [consultationData]); - - const fetchData = useCallback( - async (status: statusType) => { - setIsLoading(true); - const res = await dispatch(getConsultation(consultationId)); - if (!status.aborted) { - if (res?.data) { - const data: ConsultationModel = { - ...res.data, - symptoms_text: "", - }; - if (res.data.symptoms?.length) { - const symptoms = res.data.symptoms - .filter((symptom: number) => symptom !== 9) - .map((symptom: number) => { - const option = symptomChoices.find((i) => i.id === symptom); - return option ? option.text.toLowerCase() : symptom; - }); - data.symptoms_text = symptoms.join(", "); - } - setConsultationData(data); - const id = res.data.patient; - const patientRes = await dispatch(getPatient({ id })); - if (patientRes?.data) { - const patientGender = getPatientGender(patientRes.data); - const patientAddress = getPatientAddress(patientRes.data); - const patientComorbidities = getPatientComorbidities( - patientRes.data - ); - const data = { - ...patientRes.data, - gender: patientGender, - address: patientAddress, - comorbidities: patientComorbidities, - is_declared_positive: patientRes.data.is_declared_positive - ? "Yes" - : "No", - is_vaccinated: patientData.is_vaccinated ? "Yes" : "No", - }; - setPatientData(data); - } - } else { - navigate("/not-found"); - } - setIsLoading(false); - } - }, - [consultationId, dispatch, patientData.is_vaccinated] - ); - - useAbortableEffect((status: statusType) => { - fetchData(status); - triggerGoal("Patient Consultation Viewed", { - facilityId: facilityId, - consultationId: consultationId, - userId: authUser.id, - }); - }, []); - - const vitals = useVitalsAspectRatioConfig({ - default: undefined, - md: 8 / 11, - lg: 15 / 11, - xl: 13 / 11, - "2xl": 19 / 11, - "3xl": 23 / 11, - }); - - if (isLoading) { - return ; - } - - const tabButtonClasses = (selected: boolean) => - `capitalize min-w-max-content cursor-pointer border-transparent text-gray-700 hover:text-gray-700 hover:border-gray-300 font-bold whitespace-nowrap ${ - selected === true ? "border-primary-500 text-primary-600 border-b-2" : "" - }`; - - const ShowDiagnosis = ({ - diagnoses = [], - label = "Diagnosis", - nshow = 2, - }: { - diagnoses: ICD11DiagnosisModel[] | undefined; - label: string; - nshow?: number; - }) => { - const [showMore, setShowMore] = useState(false); - - return diagnoses.length ? ( -
-

{label}

- - {diagnoses.slice(0, !showMore ? nshow : undefined).map((diagnosis) => ( -

{diagnosis.label}

- ))} - {diagnoses.length > nshow && ( - <> - {!showMore ? ( - setShowMore(true)} - className="cursor-pointer text-sm text-blue-600 hover:text-blue-300" - > - show more - - ) : ( - setShowMore(false)} - className="cursor-pointer text-sm text-blue-600 hover:text-blue-300" - > - show less - - )} - - )} -
- ) : null; - }; - - return ( -
- setOpenDischargeSummaryDialog(false)} - /> - - setOpenDischargeDialog(false)} - consultationData={consultationData} - /> - -
- -
-
- - -
- {consultationData.admitted_to && ( -
-
- Patient - {consultationData.discharge_date - ? " Discharged from" - : " Admitted to"} - - {consultationData.admitted_to} - -
- {(consultationData.admission_date ?? - consultationData.discharge_date) && ( -
- {relativeTime( - consultationData.discharge_date - ? consultationData.discharge_date - : consultationData.admission_date - )} -
- )} -
- {consultationData.admission_date && - formatDateTime(consultationData.admission_date)} - {consultationData.discharge_date && - ` - ${formatDateTime(consultationData.discharge_date)}`} -
-
- )} -
- -
-
- {/*consultationData.other_symptoms && ( -
- - Other Symptoms:{" "} - - {consultationData.other_symptoms} -
- )*/} - - {consultationData.icd11_principal_diagnosis && ( - - d.id === consultationData.icd11_principal_diagnosis - )!, - ]} - /> - )} - - - - - - {(consultationData.verified_by_object || - consultationData.deprecated_verified_by) && ( -
- - Verified By:{" "} - - {consultationData.verified_by_object - ? `${consultationData.verified_by_object.first_name} ${consultationData.verified_by_object.last_name}` - : consultationData.deprecated_verified_by} - -
- )} -
-
- setOpenDischargeSummaryDialog(true)}> - - {t("discharge_summary")} - - - setOpenDischargeDialog(true)} - disabled={!!consultationData.discharge_date} - > - - {t("discharge_from_care")} - -
-
-
-
-
- Created: - {consultationData.created_date - ? formatDateTime(consultationData.created_date) - : "--:--"}{" "} - | -
- {consultationData.created_by && ( -
- {` ${consultationData.created_by.first_name} ${consultationData.created_by.last_name} `} - {`@${consultationData.created_by.username} (${consultationData.created_by.user_type})`} -
- )} -
-
-
- Last Modified: - {consultationData.modified_date - ? formatDateTime(consultationData.modified_date) - : "--:--"}{" "} - | -
- {consultationData.last_edited_by && ( -
- {` ${consultationData.last_edited_by.first_name} ${consultationData.last_edited_by.last_name} `} - {`@${consultationData.last_edited_by.username} (${consultationData.last_edited_by.user_type})`} -
- )} -
-
-
-
- -
-
-
- -
-
-
- {tab === "UPDATES" && ( -
- {!consultationData.discharge_date && - hl7SocketUrl && - ventilatorSocketUrl && ( -
-
-
- -
-
- -
-
-
- )} -
-
- -
- {!consultationData.discharge_date && - ((hl7SocketUrl && !ventilatorSocketUrl) || - (!hl7SocketUrl && ventilatorSocketUrl)) && ( -
- {(hl7SocketUrl || ventilatorSocketUrl) && ( -
- {hl7SocketUrl && ( -
- -
- )} - {ventilatorSocketUrl && ( -
- -
- )} -
- )} -
- )} - {consultationData.discharge_date && ( -
-
-

- Discharge Information -

-
-
- Reason {" - "} - - {DISCHARGE_REASONS.find( - (d) => - d.id === consultationData.discharge_reason - )?.text ?? "--"} - -
- {consultationData.discharge_reason === "REF" && ( -
- Referred Facility {" - "} - - {consultationData.referred_to_external || - consultationData.referred_to_object?.name || - "--"} - -
- )} - {consultationData.discharge_reason === "REC" && ( -
-
- Discharge Date {" - "} - - {consultationData.discharge_date - ? formatDate( - consultationData.discharge_date - ) - : "--/--/----"} - -
-
- Advice {" - "} - - {consultationData.discharge_notes ?? "--"} - -
-
- -
-
-
- -
-
- )} - {consultationData.discharge_reason === "EXP" && ( -
-
- Date of Death {" - "} - - {consultationData.death_datetime - ? formatDateTime( - consultationData.death_datetime - ) - : "--:--"} - -
-
- Cause of death {" - "} - - {consultationData.discharge_notes ?? "--"} - -
-
- Confirmed By {" - "} - - {consultationData.death_confirmed_doctor ?? - "--"} - -
-
- )} - {["REF", "LAMA"].includes( - consultationData.discharge_reason ?? "" - ) && ( -
-
- Discharge Date {" - "} - - {consultationData.discharge_date - ? formatDate( - consultationData.discharge_date - ) - : "--/--/----"} - -
-
- Notes {" - "} - - {consultationData.discharge_notes ?? "--"} - -
-
- )} -
-
-
- )} - {consultationData.symptoms_text && ( -
-
-

- Symptoms -

-
-
- Last Daily Update -
- {consultationData.last_daily_round - ?.additional_symptoms && ( - <> -
- {consultationData.last_daily_round?.additional_symptoms.map( - (symptom: any, index: number) => ( - choice.id === symptom - )?.text ?? "Err. Unknown" - } - size="small" - /> - ) - )} -
- {consultationData.last_daily_round - ?.other_symptoms && ( -
-
- Other Symptoms: -
- { - consultationData.last_daily_round - ?.other_symptoms - } -
- )} - - from{" "} - {formatDate( - consultationData.last_daily_round.created_at - )} - - - )} -
-
- Consultation Update -
-
- {consultationData.symptoms?.map( - (symptom, index) => ( - choice.id === symptom - )?.text ?? "Err. Unknown" - } - size="small" - /> - ) - )} -
- {consultationData.other_symptoms && ( -
-
- Other Symptoms: -
- {consultationData.other_symptoms} -
- )} - - from{" "} - {consultationData.symptoms_onset_date - ? formatDate(consultationData.symptoms_onset_date) - : "--/--/----"} - -
-
-
- )} - - {consultationData.history_of_present_illness && ( -
-
-

- History of Present Illness -

-
- -
-
-
- )} - - {consultationData.examination_details && ( -
-
-

- Examination details and Clinical conditions:{" "} -

-
- -
-
-
- )} - {consultationData.treatment_plan && ( -
-
-

- Treatment Summary -

-
- -
-
-
- )} - {consultationData.consultation_notes && ( -
-
-

- General Instructions -

-
- -
-
-
- )} - - {(consultationData.operation ?? - consultationData.special_instruction) && ( -
-
-

- Notes -

-
- {consultationData.operation && ( -
-
Operation
- -
- )} - - {consultationData.special_instruction && ( -
-
Special Instruction
- -
- )} -
-
-
- )} -
- {consultationData.procedure && - consultationData.procedure.length > 0 && ( -
-
- - - - - - - - - - - {consultationData.procedure?.map( - (procedure, index) => ( - - - - - - - ) - )} - -
- Procedure - - Notes - - Repetitive - - Time / Frequency -
- {procedure.procedure} - - {procedure.notes} - - {procedure.repetitive ? "Yes" : "No"} - - {procedure.repetitive - ? procedure.frequency - : formatDateTime(String(procedure.time))} -
-
-
- )} - {consultationData.intubation_start_date && ( -
-
-

- Date/Size/LL:{" "} -

-
-
- Intubation Date{" - "} - - {formatDateTime( - consultationData.intubation_start_date - )} - -
-
- Extubation Date{" - "} - - {consultationData.intubation_end_date && - formatDateTime( - consultationData.intubation_end_date - )} - -
-
- ETT/TT (mmid){" - "} - - {consultationData.ett_tt} - -
-
- Cuff Pressure (mmhg){" - "} - - {consultationData.cuff_pressure} - -
-
-
-
- )} - - {consultationData.lines?.length > 0 && ( -
-
-

- Lines and Catheters -

-
- {consultationData.lines?.map( - (line: any, idx: number) => ( -
-
{line.type}
-

- Details: -
- {line.other_type} -

-

- Insertion Date:{" "} - - {formatDateTime(line.start_date)} - -

-

- Site/Level of Fixation:
- - {line.site} - -

-
- ) - )} -
-
-
- )} - -
-
-

- Body Details -

-
-
- Gender {" - "} - - {patientData.gender ?? "-"} - -
-
- Age {" - "} - - {patientData.age ?? "-"} - -
-
- Weight {" - "} - - {consultationData.weight ?? "-"} Kg - -
-
- Height {" - "} - - {consultationData.height ?? "-"} cm - -
-
- Body Surface Area {" - "} - - {Math.sqrt( - (Number(consultationData.weight) * - Number(consultationData.height)) / - 3600 - ).toFixed(2)}{" "} - m2 - -
-
- Blood Group {" - "} - - {patientData.blood_group ?? "-"} - -
-
-
-
-
-
-
- -
- setShowAutomatedRounds((s) => !s)} - /> - -
-
- -
-
-
- )} - {tab === "FEED" && ( -
- - -
- )} - {tab === "SUMMARY" && ( -
- - -
- )} - {tab === "MEDICINES" && ( -
- - -
- )} - {tab === "FILES" && ( -
- -
- )} - - {tab === "ABG" && ( -
- - -
- )} - {tab === "NURSING" && ( -
- - -
- )} - {tab === "NEUROLOGICAL_MONITORING" && ( -
- - -
- )} - {tab === "VENTILATOR" && ( -
- - -
- )} - {tab === "NUTRITION" && ( -
- - -
- )} - {tab === "PRESSURE_SORE" && ( -
- - -
- )} - {tab === "DIALYSIS" && ( -
- - -
- )} - {tab === "INVESTIGATIONS" && ( -
-
- -
- - navigate( - `/facility/${facilityId}/patient/${patientId}/consultation/${consultationId}/investigation/` - ) - } - > - - {t("log_lab_results")} - -
-
- -
- )} -
- - -
- ); -}; diff --git a/src/Components/Facility/ConsultationDetails/ConsultationABGTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationABGTab.tsx new file mode 100644 index 00000000000..cf0ce7bd6af --- /dev/null +++ b/src/Components/Facility/ConsultationDetails/ConsultationABGTab.tsx @@ -0,0 +1,22 @@ +import { lazy } from "react"; +import { ConsultationTabProps } from "./index"; +import { ABGPlots } from "../Consultations/ABGPlots"; + +const PageTitle = lazy(() => import("../../Common/PageTitle")); + +export const ConsultationABGTab = (props: ConsultationTabProps) => { + return ( +
+ + +
+ ); +}; diff --git a/src/Components/Facility/ConsultationDetails/ConsultationDialysisTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationDialysisTab.tsx new file mode 100644 index 00000000000..c3515a80953 --- /dev/null +++ b/src/Components/Facility/ConsultationDetails/ConsultationDialysisTab.tsx @@ -0,0 +1,14 @@ +import { lazy } from "react"; +import { ConsultationTabProps } from "./index"; +import { DialysisPlots } from "../Consultations/DialysisPlots"; + +const PageTitle = lazy(() => import("../../Common/PageTitle")); + +export const ConsultationDialysisTab = (props: ConsultationTabProps) => { + return ( +
+ + +
+ ); +}; diff --git a/src/Components/Facility/ConsultationDetails/ConsultationFeedTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationFeedTab.tsx new file mode 100644 index 00000000000..f4fbc08331c --- /dev/null +++ b/src/Components/Facility/ConsultationDetails/ConsultationFeedTab.tsx @@ -0,0 +1,22 @@ +import { lazy } from "react"; +import { Feed } from "../Consultations/Feed"; +import { ConsultationTabProps } from "./index"; + +const PageTitle = lazy(() => import("../../Common/PageTitle")); + +export const ConsultationFeedTab = (props: ConsultationTabProps) => { + return ( +
+ + +
+ ); +}; diff --git a/src/Components/Facility/ConsultationDetails/ConsultationFilesTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationFilesTab.tsx new file mode 100644 index 00000000000..754d4bf7b86 --- /dev/null +++ b/src/Components/Facility/ConsultationDetails/ConsultationFilesTab.tsx @@ -0,0 +1,18 @@ +import { ConsultationTabProps } from "./index"; +import { FileUpload } from "../../Patient/FileUpload"; + +export const ConsultationFilesTab = (props: ConsultationTabProps) => { + return ( +
+ +
+ ); +}; diff --git a/src/Components/Facility/ConsultationDetails/ConsultationInvestigationsTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationInvestigationsTab.tsx new file mode 100644 index 00000000000..69b11a30ed5 --- /dev/null +++ b/src/Components/Facility/ConsultationDetails/ConsultationInvestigationsTab.tsx @@ -0,0 +1,39 @@ +import { lazy } from "react"; +import { ConsultationTabProps } from "./index"; +import { NonReadOnlyUsers } from "../../../Utils/AuthorizeFor"; +import ButtonV2 from "../../Common/components/ButtonV2"; +import { navigate } from "raviger"; +import CareIcon from "../../../CAREUI/icons/CareIcon"; +import InvestigationTab from "../Investigations/investigationsTab"; +import { t } from "i18next"; + +const PageTitle = lazy(() => import("../../Common/PageTitle")); +export const ConsultationInvestigationsTab = (props: ConsultationTabProps) => { + return ( +
+
+ +
+ + navigate( + `/facility/${props.facilityId}/patient/${props.patientId}/consultation/${props.consultationId}/investigation/` + ) + } + > + + {t("log_lab_results")} + +
+
+ +
+ ); +}; diff --git a/src/Components/Facility/ConsultationDetails/ConsultationMedicinesTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationMedicinesTab.tsx new file mode 100644 index 00000000000..19810102833 --- /dev/null +++ b/src/Components/Facility/ConsultationDetails/ConsultationMedicinesTab.tsx @@ -0,0 +1,19 @@ +import { ConsultationTabProps } from "./index"; +import PrescriptionAdministrationsTable from "../../Medicine/PrescriptionAdministrationsTable"; + +export const ConsultationMedicinesTab = (props: ConsultationTabProps) => { + return ( +
+ + +
+ ); +}; diff --git a/src/Components/Facility/ConsultationDetails/ConsultationNeurologicalMonitoringTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationNeurologicalMonitoringTab.tsx new file mode 100644 index 00000000000..85f34f0ec63 --- /dev/null +++ b/src/Components/Facility/ConsultationDetails/ConsultationNeurologicalMonitoringTab.tsx @@ -0,0 +1,24 @@ +import { lazy } from "react"; +import { NeurologicalTable } from "../Consultations/NeurologicalTables"; +import { ConsultationTabProps } from "./index"; + +const PageTitle = lazy(() => import("../../Common/PageTitle")); + +export const ConsultationNeurologicalMonitoringTab = ( + props: ConsultationTabProps +) => { + return ( +
+ + +
+ ); +}; diff --git a/src/Components/Facility/ConsultationDetails/ConsultationNeutritionTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationNeutritionTab.tsx new file mode 100644 index 00000000000..69f130aca0d --- /dev/null +++ b/src/Components/Facility/ConsultationDetails/ConsultationNeutritionTab.tsx @@ -0,0 +1,18 @@ +import { lazy } from "react"; +import { ConsultationTabProps } from "./index"; +import { NutritionPlots } from "../Consultations/NutritionPlots"; + +const PageTitle = lazy(() => import("../../Common/PageTitle")); + +export const ConsultationNeutritionTab = (props: ConsultationTabProps) => { + return ( +
+ + +
+ ); +}; diff --git a/src/Components/Facility/ConsultationDetails/ConsultationNursingTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationNursingTab.tsx new file mode 100644 index 00000000000..721ee18d13e --- /dev/null +++ b/src/Components/Facility/ConsultationDetails/ConsultationNursingTab.tsx @@ -0,0 +1,18 @@ +import { lazy } from "react"; +import { ConsultationTabProps } from "./index"; +import { NursingPlot } from "../Consultations/NursingPlot"; + +const PageTitle = lazy(() => import("../../Common/PageTitle")); + +export const ConsultationNursingTab = (props: ConsultationTabProps) => { + return ( +
+ + +
+ ); +}; diff --git a/src/Components/Facility/ConsultationDetails/ConsultationPressureSoreTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationPressureSoreTab.tsx new file mode 100644 index 00000000000..05327ca6870 --- /dev/null +++ b/src/Components/Facility/ConsultationDetails/ConsultationPressureSoreTab.tsx @@ -0,0 +1,14 @@ +import { lazy } from "react"; +import { ConsultationTabProps } from "./index"; +import { PressureSoreDiagrams } from "../Consultations/PressureSoreDiagrams"; + +const PageTitle = lazy(() => import("../../Common/PageTitle")); + +export const ConsultationPressureSoreTab = (props: ConsultationTabProps) => { + return ( +
+ + +
+ ); +}; diff --git a/src/Components/Facility/ConsultationDetails/ConsultationSummaryTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationSummaryTab.tsx new file mode 100644 index 00000000000..69d20dd64c9 --- /dev/null +++ b/src/Components/Facility/ConsultationDetails/ConsultationSummaryTab.tsx @@ -0,0 +1,22 @@ +import { lazy } from "react"; +import { ConsultationTabProps } from "./index"; +import { PrimaryParametersPlot } from "../Consultations/PrimaryParametersPlot"; + +const PageTitle = lazy(() => import("../../Common/PageTitle")); + +export const ConsultationSummaryTab = (props: ConsultationTabProps) => { + return ( +
+ + +
+ ); +}; diff --git a/src/Components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx new file mode 100644 index 00000000000..705b3d9479a --- /dev/null +++ b/src/Components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx @@ -0,0 +1,690 @@ +import { lazy, useEffect, useState } from "react"; +import { ConsultationTabProps } from "./index"; +import { AssetBedModel, AssetClass, AssetData } from "../../Assets/AssetTypes"; +import { useDispatch } from "react-redux"; +import { getPermittedFacility, listAssetBeds } from "../../../Redux/actions"; +import { BedModel, FacilityModel } from "../models"; +import HL7PatientVitalsMonitor from "../../VitalsMonitor/HL7PatientVitalsMonitor"; +import VentilatorPatientVitalsMonitor from "../../VitalsMonitor/VentilatorPatientVitalsMonitor"; +import useVitalsAspectRatioConfig from "../../VitalsMonitor/useVitalsAspectRatioConfig"; +import { DISCHARGE_REASONS, SYMPTOM_CHOICES } from "../../../Common/constants"; +import PrescriptionsTable from "../../Medicine/PrescriptionsTable"; +import Chip from "../../../CAREUI/display/Chip"; +import { formatDate, formatDateTime } from "../../../Utils/utils"; +import ReadMore from "../../Common/components/Readmore"; +import { DailyRoundsList } from "../Consultations/DailyRoundsList"; + +const PageTitle = lazy(() => import("../../Common/PageTitle")); + +export const ConsultationUpdatesTab = (props: ConsultationTabProps) => { + const dispatch: any = useDispatch(); + const [showAutomatedRounds, setShowAutomatedRounds] = useState(true); + const [hl7SocketUrl, setHL7SocketUrl] = useState(); + const [ventilatorSocketUrl, setVentilatorSocketUrl] = useState(); + const [monitorBedData, setMonitorBedData] = useState(); + const [ventilatorBedData, setVentilatorBedData] = useState(); + + const vitals = useVitalsAspectRatioConfig({ + default: undefined, + md: 8 / 11, + lg: 15 / 11, + xl: 13 / 11, + "2xl": 19 / 11, + "3xl": 23 / 11, + }); + + useEffect(() => { + if ( + !props.consultationData.facility || + !props.consultationData.current_bed?.bed_object.id + ) + return; + + const fetchData = async () => { + const [facilityRes, assetBedRes] = await Promise.all([ + dispatch(getPermittedFacility(props.consultationData.facility as any)), + dispatch( + listAssetBeds({ + facility: props.consultationData.facility as any, + bed: props.consultationData.current_bed?.bed_object.id, + }) + ), + ]); + + const { middleware_address } = facilityRes.data as FacilityModel; + const assetBeds = assetBedRes?.data?.results as AssetBedModel[]; + + const monitorBedData = assetBeds?.find( + (i) => i.asset_object?.asset_class === AssetClass.HL7MONITOR + ); + setMonitorBedData(monitorBedData); + const assetDataForMonitor = monitorBedData?.asset_object; + const hl7Meta = assetDataForMonitor?.meta; + const hl7Middleware = hl7Meta?.middleware_hostname || middleware_address; + if (hl7Middleware && hl7Meta?.local_ip_address) { + setHL7SocketUrl( + `wss://${hl7Middleware}/observations/${hl7Meta.local_ip_address}` + ); + } + + const consultationBedVentilator = + props.consultationData?.current_bed?.assets_objects?.find( + (i) => i.asset_class === AssetClass.VENTILATOR + ); + let ventilatorBedData; + if (consultationBedVentilator) { + ventilatorBedData = { + asset_object: consultationBedVentilator, + bed_object: props.consultationData?.current_bed?.bed_object, + } as AssetBedModel; + } else { + ventilatorBedData = assetBeds?.find( + (i) => i.asset_object.asset_class === AssetClass.VENTILATOR + ); + } + setVentilatorBedData(ventilatorBedData); + const ventilatorMeta = ventilatorBedData?.asset_object?.meta; + const ventilatorMiddleware = + ventilatorMeta?.middleware_hostname || middleware_address; + if (ventilatorMiddleware && ventilatorMeta?.local_ip_address) { + setVentilatorSocketUrl( + `wss://${ventilatorMiddleware}/observations/${ventilatorMeta?.local_ip_address}` + ); + } + + if ( + !(hl7Middleware && hl7Meta?.local_ip_address) && + !(ventilatorMiddleware && ventilatorMeta?.local_ip_address) + ) { + setHL7SocketUrl(undefined); + setVentilatorSocketUrl(undefined); + } + }; + + fetchData(); + }, [props.consultationData]); + + return ( +
+ {!props.consultationData.discharge_date && + hl7SocketUrl && + ventilatorSocketUrl && ( +
+
+
+ +
+
+ +
+
+
+ )} +
+
+ +
+ {!props.consultationData.discharge_date && + ((hl7SocketUrl && !ventilatorSocketUrl) || + (!hl7SocketUrl && ventilatorSocketUrl)) && ( +
+ {(hl7SocketUrl || ventilatorSocketUrl) && ( +
+ {hl7SocketUrl && ( +
+ +
+ )} + {ventilatorSocketUrl && ( +
+ +
+ )} +
+ )} +
+ )} + {props.consultationData.discharge_date && ( +
+
+

+ Discharge Information +

+
+
+ Reason {" - "} + + {DISCHARGE_REASONS.find( + (d) => + d.id === props.consultationData.discharge_reason + )?.text ?? "--"} + +
+ {props.consultationData.discharge_reason === "REF" && ( +
+ Referred Facility {" - "} + + {props.consultationData.referred_to_external || + props.consultationData.referred_to_object?.name || + "--"} + +
+ )} + {props.consultationData.discharge_reason === "REC" && ( +
+
+ Discharge Date {" - "} + + {props.consultationData.discharge_date + ? formatDate( + props.consultationData.discharge_date + ) + : "--/--/----"} + +
+
+ Advice {" - "} + + {props.consultationData.discharge_notes ?? "--"} + +
+
+ +
+
+
+ +
+
+ )} + {props.consultationData.discharge_reason === "EXP" && ( +
+
+ Date of Death {" - "} + + {props.consultationData.death_datetime + ? formatDateTime( + props.consultationData.death_datetime + ) + : "--:--"} + +
+
+ Cause of death {" - "} + + {props.consultationData.discharge_notes ?? "--"} + +
+
+ Confirmed By {" - "} + + {props.consultationData.death_confirmed_doctor ?? + "--"} + +
+
+ )} + {["REF", "LAMA"].includes( + props.consultationData.discharge_reason ?? "" + ) && ( +
+
+ Discharge Date {" - "} + + {props.consultationData.discharge_date + ? formatDate( + props.consultationData.discharge_date + ) + : "--/--/----"} + +
+
+ Notes {" - "} + + {props.consultationData.discharge_notes ?? "--"} + +
+
+ )} +
+
+
+ )} + {props.consultationData.symptoms_text && ( +
+
+

+ Symptoms +

+
+
+ Last Daily Update +
+ {props.consultationData.last_daily_round + ?.additional_symptoms && ( + <> +
+ {props.consultationData.last_daily_round?.additional_symptoms.map( + (symptom: any, index: number) => ( + choice.id === symptom + )?.text ?? "Err. Unknown" + } + size="small" + /> + ) + )} +
+ {props.consultationData.last_daily_round + ?.other_symptoms && ( +
+
+ Other Symptoms: +
+ { + props.consultationData.last_daily_round + ?.other_symptoms + } +
+ )} + + from{" "} + {formatDate( + props.consultationData.last_daily_round.created_at + )} + + + )} +
+
+ Consultation Update +
+
+ {props.consultationData.symptoms?.map( + (symptom, index) => ( + choice.id === symptom + )?.text ?? "Err. Unknown" + } + size="small" + /> + ) + )} +
+ {props.consultationData.other_symptoms && ( +
+
+ Other Symptoms: +
+ {props.consultationData.other_symptoms} +
+ )} + + from{" "} + {props.consultationData.symptoms_onset_date + ? formatDate(props.consultationData.symptoms_onset_date) + : "--/--/----"} + +
+
+
+ )} + + {props.consultationData.history_of_present_illness && ( +
+
+

+ History of Present Illness +

+
+ +
+
+
+ )} + + {props.consultationData.examination_details && ( +
+
+

+ Examination details and Clinical conditions:{" "} +

+
+ +
+
+
+ )} + {props.consultationData.treatment_plan && ( +
+
+

+ Treatment Summary +

+
+ +
+
+
+ )} + {props.consultationData.consultation_notes && ( +
+
+

+ General Instructions +

+
+ +
+
+
+ )} + + {(props.consultationData.operation ?? + props.consultationData.special_instruction) && ( +
+
+

+ Notes +

+
+ {props.consultationData.operation && ( +
+
Operation
+ +
+ )} + + {props.consultationData.special_instruction && ( +
+
Special Instruction
+ +
+ )} +
+
+
+ )} +
+ {props.consultationData.procedure && + props.consultationData.procedure.length > 0 && ( +
+
+ + + + + + + + + + + {props.consultationData.procedure?.map( + (procedure, index) => ( + + + + + + + ) + )} + +
+ Procedure + + Notes + + Repetitive + + Time / Frequency +
+ {procedure.procedure} + + {procedure.notes} + + {procedure.repetitive ? "Yes" : "No"} + + {procedure.repetitive + ? procedure.frequency + : formatDateTime(String(procedure.time))} +
+
+
+ )} + {props.consultationData.intubation_start_date && ( +
+
+

+ Date/Size/LL:{" "} +

+
+
+ Intubation Date{" - "} + + {formatDateTime( + props.consultationData.intubation_start_date + )} + +
+
+ Extubation Date{" - "} + + {props.consultationData.intubation_end_date && + formatDateTime( + props.consultationData.intubation_end_date + )} + +
+
+ ETT/TT (mmid){" - "} + + {props.consultationData.ett_tt} + +
+
+ Cuff Pressure (mmhg){" - "} + + {props.consultationData.cuff_pressure} + +
+
+
+
+ )} + + {props.consultationData.lines?.length > 0 && ( +
+
+

+ Lines and Catheters +

+
+ {props.consultationData.lines?.map( + (line: any, idx: number) => ( +
+
{line.type}
+

+ Details: +
+ {line.other_type} +

+

+ Insertion Date:{" "} + + {formatDateTime(line.start_date)} + +

+

+ Site/Level of Fixation:
+ + {line.site} + +

+
+ ) + )} +
+
+
+ )} + +
+
+

+ Body Details +

+
+
+ Gender {" - "} + + {props.patientData.gender ?? "-"} + +
+
+ Age {" - "} + + {props.patientData.age ?? "-"} + +
+
+ Weight {" - "} + + {props.consultationData.weight ?? "-"} Kg + +
+
+ Height {" - "} + + {props.consultationData.height ?? "-"} cm + +
+
+ Body Surface Area {" - "} + + {Math.sqrt( + (Number(props.consultationData.weight) * + Number(props.consultationData.height)) / + 3600 + ).toFixed(2)}{" "} + m2 + +
+
+ Blood Group {" - "} + + {props.patientData.blood_group ?? "-"} + +
+
+
+
+
+
+
+ +
+ setShowAutomatedRounds((s) => !s)} + /> + +
+
+ +
+
+
+ ); +}; diff --git a/src/Components/Facility/ConsultationDetails/ConsultationVentilatorTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationVentilatorTab.tsx new file mode 100644 index 00000000000..d14b54cc096 --- /dev/null +++ b/src/Components/Facility/ConsultationDetails/ConsultationVentilatorTab.tsx @@ -0,0 +1,22 @@ +import { lazy } from "react"; +import { ConsultationTabProps } from "./index"; +import { VentilatorPlot } from "../Consultations/VentilatorPlot"; + +const PageTitle = lazy(() => import("../../Common/PageTitle")); + +export const ConsultationVentilatorTab = (props: ConsultationTabProps) => { + return ( +
+ + +
+ ); +}; diff --git a/src/Components/Facility/ConsultationDetails/index.tsx b/src/Components/Facility/ConsultationDetails/index.tsx new file mode 100644 index 00000000000..affdb2756cd --- /dev/null +++ b/src/Components/Facility/ConsultationDetails/index.tsx @@ -0,0 +1,501 @@ +import { + CONSULTATION_TABS, + GENDER_TYPES, + OptionsType, + SYMPTOM_CHOICES, +} from "../../../Common/constants"; +import { ConsultationModel, ICD11DiagnosisModel } from "../models"; +import { getConsultation, getPatient } from "../../../Redux/actions"; +import { statusType, useAbortableEffect } from "../../../Common/utils"; +import { lazy, useCallback, useState } from "react"; + +import ButtonV2 from "../../Common/components/ButtonV2"; +import CareIcon from "../../../CAREUI/icons/CareIcon"; +import DischargeModal from "../DischargeModal"; +import DischargeSummaryModal from "../DischargeSummaryModal"; +import DoctorVideoSlideover from "../DoctorVideoSlideover"; +import { make as Link } from "../../Common/components/Link.bs"; +import PatientInfoCard from "../../Patient/PatientInfoCard"; +import { PatientModel } from "../../Patient/models"; +import { formatDateTime, relativeTime } from "../../../Utils/utils"; + +import { navigate } from "raviger"; +import { useDispatch } from "react-redux"; +import { useQueryParams } from "raviger"; +import { useTranslation } from "react-i18next"; +import { triggerGoal } from "../../Common/Plausible"; +import useAuthUser from "../../../Common/hooks/useAuthUser"; +import { ConsultationUpdatesTab } from "./ConsultationUpdatesTab"; +import { ConsultationABGTab } from "./ConsultationABGTab"; +import { ConsultationNursingTab } from "./ConsultationNursingTab"; +import { ConsultationFeedTab } from "./ConsultationFeedTab"; +import { ConsultationSummaryTab } from "./ConsultationSummaryTab"; +import { ConsultationFilesTab } from "./ConsultationFilesTab"; +import { ConsultationMedicinesTab } from "./ConsultationMedicinesTab"; +import { ConsultationInvestigationsTab } from "./ConsultationInvestigationsTab"; +import { ConsultationVentilatorTab } from "./ConsultationVentilatorTab"; +import { ConsultationPressureSoreTab } from "./ConsultationPressureSoreTab"; +import { ConsultationDialysisTab } from "./ConsultationDialysisTab"; +import { ConsultationNeurologicalMonitoringTab } from "./ConsultationNeurologicalMonitoringTab"; + +const Loading = lazy(() => import("../../Common/Loading")); +const PageTitle = lazy(() => import("../../Common/PageTitle")); +const symptomChoices = [...SYMPTOM_CHOICES]; + +export interface ConsultationTabProps { + consultationId: string; + facilityId: string; + patientId: string; + consultationData: ConsultationModel; + patientData: PatientModel; +} + +export const ConsultationDetails = (props: any) => { + const { t } = useTranslation(); + const { facilityId, patientId, consultationId } = props; + const tab = props.tab.toUpperCase(); + const dispatch: any = useDispatch(); + const [isLoading, setIsLoading] = useState(false); + const [showDoctors, setShowDoctors] = useState(false); + const [qParams, _] = useQueryParams(); + + const [consultationData, setConsultationData] = useState( + {} as ConsultationModel + ); + const [patientData, setPatientData] = useState({}); + const [openDischargeSummaryDialog, setOpenDischargeSummaryDialog] = + useState(false); + const [openDischargeDialog, setOpenDischargeDialog] = useState(false); + + const getPatientGender = (patientData: any) => + GENDER_TYPES.find((i) => i.id === patientData.gender)?.text; + + const getPatientAddress = (patientData: 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?.medical_history?.length) { + const medHis = patientData.medical_history; + return medHis.map((item: any) => item.disease).join(", "); + } else { + return "None"; + } + }; + + const authUser = useAuthUser(); + + const fetchData = useCallback( + async (status: statusType) => { + setIsLoading(true); + const res = await dispatch(getConsultation(consultationId)); + if (!status.aborted) { + if (res?.data) { + const data: ConsultationModel = { + ...res.data, + symptoms_text: "", + }; + if (res.data.symptoms?.length) { + const symptoms = res.data.symptoms + .filter((symptom: number) => symptom !== 9) + .map((symptom: number) => { + const option = symptomChoices.find((i) => i.id === symptom); + return option ? option.text.toLowerCase() : symptom; + }); + data.symptoms_text = symptoms.join(", "); + } + setConsultationData(data); + const id = res.data.patient; + const patientRes = await dispatch(getPatient({ id })); + if (patientRes?.data) { + const patientGender = getPatientGender(patientRes.data); + const patientAddress = getPatientAddress(patientRes.data); + const patientComorbidities = getPatientComorbidities( + patientRes.data + ); + const data = { + ...patientRes.data, + gender: patientGender, + address: patientAddress, + comorbidities: patientComorbidities, + is_declared_positive: patientRes.data.is_declared_positive + ? "Yes" + : "No", + is_vaccinated: patientData.is_vaccinated ? "Yes" : "No", + }; + setPatientData(data); + } + } else { + navigate("/not-found"); + } + setIsLoading(false); + } + }, + [consultationId, dispatch, patientData.is_vaccinated] + ); + + useAbortableEffect((status: statusType) => { + fetchData(status); + triggerGoal("Patient Consultation Viewed", { + facilityId: facilityId, + consultationId: consultationId, + userId: authUser.id, + }); + }, []); + + const TABS = { + UPDATES: ConsultationUpdatesTab, + FEED: ConsultationFeedTab, + SUMMARY: ConsultationSummaryTab, + MEDICINES: ConsultationMedicinesTab, + FILES: ConsultationFilesTab, + INVESTIGATIONS: ConsultationInvestigationsTab, + ABG: ConsultationABGTab, + NURSING: ConsultationNursingTab, + NEUROLOGICAL_MONITORING: ConsultationNeurologicalMonitoringTab, + VENTILATOR: ConsultationVentilatorTab, + NUTRITION: ConsultationNursingTab, + PRESSURE_SORE: ConsultationPressureSoreTab, + DIALYSIS: ConsultationDialysisTab, + }; + + const consultationTabProps: ConsultationTabProps = { + consultationId, + facilityId, + patientId, + consultationData, + patientData, + }; + + const SelectedTab = TABS[tab]; + + if (isLoading) { + return ; + } + + const tabButtonClasses = (selected: boolean) => + `capitalize min-w-max-content cursor-pointer border-transparent text-gray-700 hover:text-gray-700 hover:border-gray-300 font-bold whitespace-nowrap ${ + selected === true ? "border-primary-500 text-primary-600 border-b-2" : "" + }`; + + const ShowDiagnosis = ({ + diagnoses = [], + label = "Diagnosis", + nshow = 2, + }: { + diagnoses: ICD11DiagnosisModel[] | undefined; + label: string; + nshow?: number; + }) => { + const [showMore, setShowMore] = useState(false); + + return diagnoses.length ? ( +
+

{label}

+ + {diagnoses.slice(0, !showMore ? nshow : undefined).map((diagnosis) => ( +

{diagnosis.label}

+ ))} + {diagnoses.length > nshow && ( + <> + {!showMore ? ( + setShowMore(true)} + className="cursor-pointer text-sm text-blue-600 hover:text-blue-300" + > + show more + + ) : ( + setShowMore(false)} + className="cursor-pointer text-sm text-blue-600 hover:text-blue-300" + > + show less + + )} + + )} +
+ ) : null; + }; + + return ( +
+ setOpenDischargeSummaryDialog(false)} + /> + + setOpenDischargeDialog(false)} + consultationData={consultationData} + /> + +
+ +
+
+ + +
+ {consultationData.admitted_to && ( +
+
+ Patient + {consultationData.discharge_date + ? " Discharged from" + : " Admitted to"} + + {consultationData.admitted_to} + +
+ {(consultationData.admission_date ?? + consultationData.discharge_date) && ( +
+ {relativeTime( + consultationData.discharge_date + ? consultationData.discharge_date + : consultationData.admission_date + )} +
+ )} +
+ {consultationData.admission_date && + formatDateTime(consultationData.admission_date)} + {consultationData.discharge_date && + ` - ${formatDateTime(consultationData.discharge_date)}`} +
+
+ )} +
+ +
+
+ {/*consultationData.other_symptoms && ( +
+ + Other Symptoms:{" "} + + {consultationData.other_symptoms} +
+ )*/} + + {consultationData.icd11_principal_diagnosis && ( + + d.id === consultationData.icd11_principal_diagnosis + )!, + ]} + /> + )} + + + + + + {(consultationData.verified_by_object || + consultationData.deprecated_verified_by) && ( +
+ + Verified By:{" "} + + {consultationData.verified_by_object + ? `${consultationData.verified_by_object.first_name} ${consultationData.verified_by_object.last_name}` + : consultationData.deprecated_verified_by} + +
+ )} +
+
+ setOpenDischargeSummaryDialog(true)}> + + {t("discharge_summary")} + + + setOpenDischargeDialog(true)} + disabled={!!consultationData.discharge_date} + > + + {t("discharge_from_care")} + +
+
+
+
+
+ Created: + {consultationData.created_date + ? formatDateTime(consultationData.created_date) + : "--:--"}{" "} + | +
+ {consultationData.created_by && ( +
+ {` ${consultationData.created_by.first_name} ${consultationData.created_by.last_name} `} + {`@${consultationData.created_by.username} (${consultationData.created_by.user_type})`} +
+ )} +
+
+
+ Last Modified: + {consultationData.modified_date + ? formatDateTime(consultationData.modified_date) + : "--:--"}{" "} + | +
+ {consultationData.last_edited_by && ( +
+ {` ${consultationData.last_edited_by.first_name} ${consultationData.last_edited_by.last_name} `} + {`@${consultationData.last_edited_by.username} (${consultationData.last_edited_by.user_type})`} +
+ )} +
+
+
+
+ +
+
+
+ +
+
+
+ + +
+ + +
+ ); +}; diff --git a/src/Components/Facility/models.tsx b/src/Components/Facility/models.tsx index 012ca1d68b2..cc34140e7a9 100644 --- a/src/Components/Facility/models.tsx +++ b/src/Components/Facility/models.tsx @@ -142,6 +142,7 @@ export interface ConsultationModel { cause_of_death?: string; death_datetime?: string; death_confirmed_doctor?: string; + is_readmission?: boolean; } export interface PatientStatsModel { id?: number; diff --git a/src/Components/Patient/ManagePatients.tsx b/src/Components/Patient/ManagePatients.tsx index bdde6f8c7f1..71a3ad08c8c 100644 --- a/src/Components/Patient/ManagePatients.tsx +++ b/src/Components/Patient/ManagePatients.tsx @@ -601,6 +601,15 @@ export const PatientManager = () => { text="Review Missed" /> )} + {patient.last_consultation?.is_readmission && ( + + )} {patient.disease_status === "POSITIVE" && ( { ); }; + const isPatientEligibleForNewConsultation = (patientData: PatientModel) => { + return !patientData.last_consultation || + patientData.last_consultation?.discharge_date + ? true + : false; + }; + return ( {
- patientData.is_active && - (!patientData?.last_consultation || - patientData?.last_consultation?.discharge_date) && + isPatientEligibleForNewConsultation(patientData) && navigate( `/facility/${patientData?.facility}/patient/${id}/consultation` ) } > -
-
+
+
diff --git a/src/Components/Patient/PatientInfoCard.tsx b/src/Components/Patient/PatientInfoCard.tsx index 9df4c93051a..dbb4aac34ce 100644 --- a/src/Components/Patient/PatientInfoCard.tsx +++ b/src/Components/Patient/PatientInfoCard.tsx @@ -143,7 +143,7 @@ export default function PatientInfoCard(props: {
)}
-
+
- - Domiciliary Care{" "} - + + + Domiciliary Care + + + )} + {consultation?.is_readmission && ( + <> + + + + Readmitted )} diff --git a/src/Components/Users/ManageUsers.tsx b/src/Components/Users/ManageUsers.tsx index d2e0ca415e3..8625d442f52 100644 --- a/src/Components/Users/ManageUsers.tsx +++ b/src/Components/Users/ManageUsers.tsx @@ -73,6 +73,8 @@ export default function ManageUsers() { name: string; }>({ show: false, username: "", name: "" }); + const [weeklyHoursError, setWeeklyHoursError] = useState(""); + const extremeSmallScreenBreakpoint = 320; const isExtremeSmallScreen = width <= extremeSmallScreenBreakpoint ? true : false; @@ -144,7 +146,10 @@ export default function ManageUsers() { const handleWorkingHourSubmit = async () => { const username = selectedUser; - if (!username || weeklyHours < 0 || weeklyHours > 168) return; + if (!username || !weeklyHours || weeklyHours < 0 || weeklyHours > 168) { + setWeeklyHoursError("Value should be between 0 and 168"); + return; + } const res = await dispatch( partialUpdateUser(username, { weekly_working_hours: weeklyHours, @@ -163,6 +168,7 @@ export default function ManageUsers() { }); } setWeeklyHours(0); + setWeeklyHoursError(""); fetchData({ aborted: false }); }; @@ -493,13 +499,14 @@ export default function ManageUsers() { { + setExpandWorkingHours(state); + setWeeklyHours(0); + setWeeklyHoursError(""); + }} slideFrom="right" title="Average weekly working hours" dialogClass="md:w-[400px]" - onCloseClick={() => { - setWeeklyHours(0); - }} >
@@ -512,11 +519,7 @@ export default function ManageUsers() { onChange={(e) => { setWeeklyHours(e.value); }} - error={ - weeklyHours < 0 || weeklyHours > 168 - ? "Average weekly working hours should be between 0 and 168" - : "" - } + error={weeklyHoursError} required label="" type="number" diff --git a/src/Components/VitalsMonitor/HL7DeviceClient.ts b/src/Components/VitalsMonitor/HL7DeviceClient.ts index 72f6406193a..7e13622b31f 100644 --- a/src/Components/VitalsMonitor/HL7DeviceClient.ts +++ b/src/Components/VitalsMonitor/HL7DeviceClient.ts @@ -1,11 +1,31 @@ import { EventEmitter } from "events"; import { VitalsDataBase, VitalsValueBase, VitalsWaveformBase } from "./types"; -const WAVEFORM_KEY_MAP: Record = { - II: "ecg-waveform", - Pleth: "pleth-waveform", - Respiration: "spo2-waveform", -}; +const ECG_WAVENAME_KEYS = [ + "I", + "II", + "III", + "aVR", + "aVL", + "aVF", + "V1", + "V2", + "V3", + "V4", + "V5", + "V6", +] as const; + +const WAVEFORM_KEY_MAP: Record = + { + Pleth: "pleth-waveform", + Respiration: "spo2-waveform", + + // Maps each ECG wave name to the event "ecg-waveform" + ...(Object.fromEntries( + ECG_WAVENAME_KEYS.map((key) => [key, "ecg-waveform"]) + ) as Record), + }; /** * Provides the API for connecting to the Vitals Monitor WebSocket and emitting @@ -74,8 +94,10 @@ export interface HL7VitalsValueData extends VitalsDataBase, VitalsValueBase { | "body-temperature2"; } +type EcgWaveName = (typeof ECG_WAVENAME_KEYS)[number]; + export interface HL7VitalsWaveformData extends VitalsWaveformBase { - "wave-name": "II" | "Pleth" | "Respiration"; + "wave-name": EcgWaveName | "Pleth" | "Respiration"; } export interface HL7VitalsBloodPressureData extends VitalsDataBase { diff --git a/src/Components/VitalsMonitor/HL7PatientVitalsMonitor.tsx b/src/Components/VitalsMonitor/HL7PatientVitalsMonitor.tsx index a340c21e28b..c0b089df8b0 100644 --- a/src/Components/VitalsMonitor/HL7PatientVitalsMonitor.tsx +++ b/src/Components/VitalsMonitor/HL7PatientVitalsMonitor.tsx @@ -88,7 +88,7 @@ export default function HL7PatientVitalsMonitor(props: IVitalsComponentProps) { {/* Pulse Rate */} ❤️ diff --git a/src/Redux/api.tsx b/src/Redux/api.tsx index 393b9372ca5..01a91d4ce3a 100644 --- a/src/Redux/api.tsx +++ b/src/Redux/api.tsx @@ -10,7 +10,7 @@ interface Routes { const routes: Routes = { config: { - path: import.meta.env.REACT_APP_CONFIG || "/config.json", + path: import.meta.env.REACT_APP_CONFIG ?? "/config.json", method: "GET", noAuth: true, }, diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 529c2689aed..c899d453626 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -5,7 +5,7 @@ interface ImportMetaEnv { readonly REACT_APP_META_DESCRIPTION: string; readonly REACT_APP_COVER_IMAGE: string; readonly REACT_APP_COVER_IMAGE_ALT: string; - readonly REACT_APP_CONFIG: string; + readonly REACT_APP_CONFIG: string | undefined; readonly REACT_PUBLIC_URL: string; readonly REACT_APP_SITE_URL: string; readonly REACT_APP_ANALYTICS_SERVER_URL: string;