From d84caf0c3a958273887831bb6af1c0ad61b921bc Mon Sep 17 00:00:00 2001 From: Jacob John Jeevan <40040905+Jacobjeevan@users.noreply.github.com> Date: Sun, 10 Nov 2024 09:09:43 +0530 Subject: [PATCH 1/9] Ventilator mode/oxygen modality data on consultation page (#8781) * Ventilator Data - Added ventilator data as table on Ventilator tab - Added Marker Area to graphs under ventilator tab * MarkerLine and MarkerArea - To do: cleanup (only keep whichever one is chosen, cleanup commented out code) * MarkLine, rm MarkArea code - Choosing markLine as that represents the data accurately (when switching to bar graph) * Rm markArea import * added overflow to table/mobile view * render most recent mode/modality on graph * log fixes * IV/NIV log text modification * Fixes and cleanup - Switched VentilatorPlot to use getDailyReports (instead of getDailyRoundsAnalyse). - Changed end_date to be based on next daily round (rather than round with ventilator data) - Pagination on top level (in the tab, instead of in Plots) - Applies to both Table and the Graphs * Cleanup - Removed filtering items in VentorTable beforehand - allowing to fetch the right end date - as well as only combining rounds with same NIV/IV/Oxygen mode/modality if there are consecutive (i.e. no round in between them) - VentilatorPlot - adjust label position for the first data point (to avoid overflow of line label into y axis labels) * Pagination Limit - Added limit of 36 (same as before for pagination in VentilatorPlot) * using date utils * improvement suggestions * Nitpick fixes * type fix * lint fix --- public/locale/en.json | 20 ++ .../ConsultationVentilatorTab.tsx | 34 ++- .../ConsultationDetails/Events/EventsList.tsx | 6 + .../Facility/Consultations/VentilatorPlot.tsx | 275 ++++++++++++++---- .../Consultations/VentilatorTable.tsx | 129 ++++++++ .../components/BinaryChronologicalChart.tsx | 2 +- .../Consultations/components/LinePlot.tsx | 32 +- .../Consultations/components/ReactEcharts.tsx | 2 + 8 files changed, 443 insertions(+), 57 deletions(-) create mode 100644 src/components/Facility/Consultations/VentilatorTable.tsx diff --git a/public/locale/en.json b/public/locale/en.json index b6ccb84de08..ef591130213 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -133,9 +133,13 @@ "ORAL_ISSUE__NO_ISSUE": "No issues", "ORAL_ISSUE__ODYNOPHAGIA": "Odynophagia", "OXYGEN_MODALITY__HIGH_FLOW_NASAL_CANNULA": "High Flow Nasal Cannula", + "OXYGEN_MODALITY__HIGH_FLOW_NASAL_CANNULA_short": "HFNC", "OXYGEN_MODALITY__NASAL_PRONGS": "Nasal Prongs", + "OXYGEN_MODALITY__NASAL_PRONGS_short": "NP", "OXYGEN_MODALITY__NON_REBREATHING_MASK": "Non Rebreathing Mask", + "OXYGEN_MODALITY__NON_REBREATHING_MASK_short": "NRM", "OXYGEN_MODALITY__SIMPLE_FACE_MASK": "Simple Face Mask", + "OXYGEN_MODALITY__SIMPLE_FACE_MASK_short": "SFM", "PRESCRIPTION_FREQUENCY_BD": "Twice daily", "PRESCRIPTION_FREQUENCY_HS": "Night only", "PRESCRIPTION_FREQUENCY_OD": "Once daily", @@ -206,12 +210,19 @@ "URINATION_FREQUENCY__NORMAL": "Normal", "VENTILATOR": "Detailed Update", "VENTILATOR_MODE__CMV": "Control Mechanical Ventilation (CMV)", + "VENTILATOR_MODE__CMV_short": "CMV", "VENTILATOR_MODE__PCV": "Pressure Control Ventilation (PCV)", + "VENTILATOR_MODE__PCV_short": "PCV", "VENTILATOR_MODE__PC_SIMV": "Pressure Controlled SIMV (PC-SIMV)", + "VENTILATOR_MODE__PC_SIMV_short": "PC-SIMV", "VENTILATOR_MODE__PSV": "C-PAP / Pressure Support Ventilation (PSV)", + "VENTILATOR_MODE__PSV_short": "C-PAP/PSV", "VENTILATOR_MODE__SIMV": "Synchronised Intermittent Mandatory Ventilation (SIMV)", + "VENTILATOR_MODE__SIMV_short": "SIMV", "VENTILATOR_MODE__VCV": "Volume Control Ventilation (VCV)", + "VENTILATOR_MODE__VCV_short": "VCV", "VENTILATOR_MODE__VC_SIMV": "Volume Controlled SIMV (VC-SIMV)", + "VENTILATOR_MODE__VC_SIMV_short": "VC-SIMV", "View Facility": "View Facility", "aadhaar_number": "Aadhaar Number", "aadhaar_number_will_not_be_stored": "Aadhaar number will not be stored by CARE", @@ -611,6 +622,7 @@ "encounter_suggestion__OP": "Out-patient visit", "encounter_suggestion__R": "Consultation", "encounter_suggestion_edit_disallowed": "Not allowed to switch to this option in edit consultation", + "end_datetime": "End Date/Time", "enter_aadhaar_number": "Enter a 12-digit Aadhaar ID", "enter_aadhaar_otp": "Enter OTP sent to the registered mobile with Aadhaar", "enter_abha_address": "Enter ABHA Address", @@ -1145,6 +1157,7 @@ "spokes": "Spoke Facilities", "srf_id": "SRF ID", "staff_list": "Staff List", + "start_datetime": "Start Date/Time", "start_dosage": "Start Dosage", "state": "State", "status": "Status", @@ -1236,6 +1249,13 @@ "vacant": "Vacant", "vehicle_preference": "Vehicle preference", "vendor_name": "Vendor Name", + "ventilator_interface": "Respiratory Support Type", + "ventilator_log": "Ventilator Log", + "ventilator_modality": "Modality", + "ventilator_mode": "Ventilator Mode", + "ventilator_oxygen_modality": "Oxygen Modality", + "ventilator_oxygen_modality_oxygen_rate": "Oxygen Flow Rate", + "ventilator_spo2": "SpO₂", "verify_and_link": "Verify and Link", "verify_otp": "Verify OTP", "verify_otp_error": "Failed to verify OTP. Please try again later.", diff --git a/src/components/Facility/ConsultationDetails/ConsultationVentilatorTab.tsx b/src/components/Facility/ConsultationDetails/ConsultationVentilatorTab.tsx index 1ded0ba7684..b26ea6e0e53 100644 --- a/src/components/Facility/ConsultationDetails/ConsultationVentilatorTab.tsx +++ b/src/components/Facility/ConsultationDetails/ConsultationVentilatorTab.tsx @@ -1,8 +1,30 @@ +import Loading from "@/components/Common/Loading"; import PageTitle from "@/components/Common/PageTitle"; import { ConsultationTabProps } from "@/components/Facility/ConsultationDetails/index"; import { VentilatorPlot } from "@/components/Facility/Consultations/VentilatorPlot"; +import VentilatorTable from "@/components/Facility/Consultations/VentilatorTable"; + +import useFilters from "@/hooks/useFilters"; + +import routes from "@/Utils/request/api"; +import useQuery from "@/Utils/request/useQuery"; export const ConsultationVentilatorTab = (props: ConsultationTabProps) => { + const { consultationId } = props; + const { qParams, Pagination, resultsPerPage } = useFilters({ limit: 36 }); + + const { loading: isLoading, data } = useQuery(routes.getDailyReports, { + pathParams: { consultationId }, + query: { + limit: resultsPerPage, + offset: (qParams.page ? qParams.page - 1 : 0) * resultsPerPage, + }, + }); + + if (isLoading) { + return ; + } + return (
{ hideBack={true} breadcrumbs={false} /> - + + + {Boolean(data?.count && data.count > 0) && ( +
+ +
+ )}
); }; diff --git a/src/components/Facility/ConsultationDetails/Events/EventsList.tsx b/src/components/Facility/ConsultationDetails/Events/EventsList.tsx index e3f7072391e..47c68636a1a 100644 --- a/src/components/Facility/ConsultationDetails/Events/EventsList.tsx +++ b/src/components/Facility/ConsultationDetails/Events/EventsList.tsx @@ -68,6 +68,12 @@ export default function EventsList({ query }: { query: QueryParams }) { } const values = Object.fromEntries(entries); + if ( + values.ventilator_interface === "INVASIVE" || + values.ventilator_interface === "NON_INVASIVE" + ) { + values.ventilator_interface += " VENTILATOR"; + } switch (item.event_type.name) { case "INTERNAL_TRANSFER": diff --git a/src/components/Facility/Consultations/VentilatorPlot.tsx b/src/components/Facility/Consultations/VentilatorPlot.tsx index 6991fd02b8d..38948f165ea 100644 --- a/src/components/Facility/Consultations/VentilatorPlot.tsx +++ b/src/components/Facility/Consultations/VentilatorPlot.tsx @@ -1,14 +1,11 @@ import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; -import Pagination from "@/components/Common/Pagination"; +import Loading from "@/components/Common/Loading"; import BinaryChronologicalChart from "@/components/Facility/Consultations/components/BinaryChronologicalChart"; import { LinePlot } from "@/components/Facility/Consultations/components/LinePlot"; -import { VentilatorPlotFields } from "@/components/Facility/models"; +import { DailyRoundsModel } from "@/components/Patient/models"; -import { PAGINATION_LIMIT } from "@/common/constants"; - -import routes from "@/Utils/request/api"; -import request from "@/Utils/request/request"; import { formatDateTime } from "@/Utils/utils"; /* @@ -31,48 +28,222 @@ const modality: Array = [ ]; */ -export const VentilatorPlot = (props: any) => { - const { consultationId } = props; - const [results, setResults] = useState({}); - const [currentPage, setCurrentPage] = useState(1); - const [totalCount, setTotalCount] = useState(0); +interface graphDataProps { + [key: string]: { + bilateral_air_entry?: boolean; + etco2?: number; + id?: string; + ventilator_fio2?: number; + ventilator_mean_airway_pressure?: number; + ventilator_oxygen_modality_flow_rate?: number; + ventilator_oxygen_modality_oxygen_rate?: number; + ventilator_peep?: number | null; + ventilator_pip?: number; + ventilator_pressure_support?: number; + ventilator_resp_rate?: number; + ventilator_spo2?: number; + ventilator_tidal_volume?: number; + }; +} - useEffect(() => { - const fetchDailyRounds = async ( - currentPage: number, - consultationId: string, - ) => { - const { res, data } = await request(routes.dailyRoundsAnalyse, { - body: { page: currentPage, fields: VentilatorPlotFields }, - pathParams: { - consultationId, - }, +export const VentilatorPlot = ({ + dailyRoundsList, +}: { + dailyRoundsList?: DailyRoundsModel[]; +}) => { + const [results, setResults] = useState({}); + const { t } = useTranslation(); + + const getGraphData = (dailyRoundsData?: DailyRoundsModel[]) => { + const graphData: graphDataProps = {}; + const graphDataCount = dailyRoundsData?.length ?? 0; + if (dailyRoundsData) { + dailyRoundsData.forEach((currentRound: DailyRoundsModel) => { + // @ts-expect-error taken_at should always be available + graphData[currentRound.taken_at] = { + bilateral_air_entry: currentRound.bilateral_air_entry, + etco2: currentRound.etco2, + id: currentRound.id, + ventilator_fio2: currentRound.ventilator_fio2, + ventilator_mean_airway_pressure: + currentRound.ventilator_mean_airway_pressure, + ventilator_oxygen_modality_flow_rate: + currentRound.ventilator_oxygen_modality_flow_rate, + ventilator_oxygen_modality_oxygen_rate: + currentRound.ventilator_oxygen_modality_oxygen_rate, + ventilator_peep: currentRound.ventilator_peep + ? Number(currentRound.ventilator_peep) + : null, + ventilator_pip: currentRound.ventilator_pip, + ventilator_pressure_support: currentRound.ventilator_pressure_support, + ventilator_resp_rate: currentRound.ventilator_resp_rate, + ventilator_spo2: currentRound.ventilator_spo2, + ventilator_tidal_volume: currentRound.ventilator_tidal_volume, + }; }); - if (res && res.ok && data) { - setResults(data.results); - setTotalCount(data.count); - } - }; + } + return { graphData, graphDataCount }; + }; + + useEffect(() => { + const { graphData } = getGraphData(dailyRoundsList); + setResults(graphData); + }, [dailyRoundsList]); - fetchDailyRounds(currentPage, consultationId); - }, [consultationId, currentPage]); + if (!dailyRoundsList) { + return ; + } - const handlePagination = (page: number) => { - setCurrentPage(page); + const dates = Object.keys(results).map((p: string) => formatDateTime(p)); + + const getConditionAndLegend = ( + name: string, + currentRound: DailyRoundsModel, + ) => { + let condition = false; + let legend = ""; + switch (name) { + case "ventilator_pip": + case "ventilator_mean_airway_pressure": + case "ventilator_resp_rate": + case "ventilator_pressure_support": + case "ventilator_tidal_volume": + case "ventilator_peep": + condition = + (currentRound.ventilator_interface === "INVASIVE" || + currentRound.ventilator_interface === "NON_INVASIVE") && + !!currentRound.ventilator_mode; + break; + case "ventilator_fio2": + condition = + currentRound.ventilator_interface === "OXYGEN_SUPPORT" && + currentRound.ventilator_oxygen_modality === "HIGH_FLOW_NASAL_CANNULA"; + break; + case "ventilator_spo2": + condition = + currentRound.ventilator_interface === "OXYGEN_SUPPORT" && + (currentRound.ventilator_oxygen_modality === "NASAL_PRONGS" || + currentRound.ventilator_oxygen_modality === "SIMPLE_FACE_MASK" || + currentRound.ventilator_oxygen_modality === + "NON_REBREATHING_MASK" || + currentRound.ventilator_oxygen_modality === + "HIGH_FLOW_NASAL_CANNULA"); + break; + case "etco2": + case "ventilator_oxygen_modality_flow_rate": + condition = + !!currentRound.ventilator_mode || + !!currentRound.ventilator_oxygen_modality || + false; + break; + case "ventilator_oxygen_modality_oxygen_rate": + condition = + currentRound.ventilator_interface === "OXYGEN_SUPPORT" && + (currentRound.ventilator_oxygen_modality === "NASAL_PRONGS" || + currentRound.ventilator_oxygen_modality === "SIMPLE_FACE_MASK" || + currentRound.ventilator_oxygen_modality === "NON_REBREATHING_MASK"); + break; + } + switch (currentRound.ventilator_interface) { + case "OXYGEN_SUPPORT": + legend = + t( + `OXYGEN_MODALITY__${currentRound.ventilator_oxygen_modality}_short`, + ) + + " (" + + t("RESPIRATORY_SUPPORT_SHORT__OXYGEN_SUPPORT") + + ")"; + break; + case "INVASIVE": + legend = + t(`VENTILATOR_MODE__${currentRound.ventilator_mode}_short`) + + " (" + + t("RESPIRATORY_SUPPORT_SHORT__INVASIVE") + + ")"; + break; + case "NON_INVASIVE": + legend = + t(`VENTILATOR_MODE__${currentRound.ventilator_mode}_short`) + + " (" + + t("RESPIRATORY_SUPPORT_SHORT__NON_INVASIVE") + + ")"; + break; + } + return { condition, legend }; + }; + + const getModeOrModality = (round: DailyRoundsModel) => { + const ventilatorInterface = round.ventilator_interface; + if (!ventilatorInterface) return null; + switch (ventilatorInterface) { + case "INVASIVE": + case "NON_INVASIVE": + return round.ventilator_mode; + case "OXYGEN_SUPPORT": + return round.ventilator_oxygen_modality; + default: + return null; + } }; - const dates = Object.keys(results) - .map((p: string) => formatDateTime(p)) - .reverse(); + const getMarkLineData = (name: string) => { + const markLineData = []; + if (!dailyRoundsList) return []; + let index = 0; + while (index < dailyRoundsList.length) { + const currentRound = dailyRoundsList[index]; + const { condition, legend } = getConditionAndLegend(name, currentRound); + const currentInterfaceOrModality = getModeOrModality(currentRound); + if (condition) { + const startIndex = dates.findIndex( + (element) => element === formatDateTime(currentRound.taken_at), + ); + if (startIndex !== -1) { + let nextIndex = index + 1; + while (nextIndex < dailyRoundsList.length) { + const nextRound = dailyRoundsList[nextIndex]; + const nextInterfaceOrModality = getModeOrModality(nextRound); + if ( + currentRound.ventilator_interface === + nextRound.ventilator_interface && + currentInterfaceOrModality === nextInterfaceOrModality + ) { + nextIndex += 1; + } else { + break; + } + } + const position = + startIndex === 0 ? "insideMiddleBottom" : "insideMiddleTop"; + markLineData.push({ + name: legend, + xAxis: dates[startIndex], + label: { + show: true, + position, + formatter: "{b}", + color: "#000000", + textBorderColor: "#ffffff", + textBorderWidth: 2, + }, + }); + index = nextIndex; + } else { + index += 1; + } + } else { + index += 1; + } + } + return markLineData; + }; - const yAxisData = (name: string) => { - return Object.values(results) - .map((p: any) => p[name]) - .reverse(); + const yAxisData = (name: keyof graphDataProps[string]) => { + return Object.values(results).map((p) => p[name]); }; const bilateral = Object.values(results) - .map((p: any, i) => { + .map((p, i) => { return { value: p.bilateral_air_entry, timestamp: Object.keys(results)[i], @@ -91,6 +262,7 @@ export const VentilatorPlot = (props: any) => { yData={yAxisData("ventilator_pip")} low={12} high={30} + verticalMarkerData={getMarkLineData("ventilator_pip")} />
@@ -101,6 +273,9 @@ export const VentilatorPlot = (props: any) => { yData={yAxisData("ventilator_mean_airway_pressure")} low={12} high={25} + verticalMarkerData={getMarkLineData( + "ventilator_mean_airway_pressure", + )} />
@@ -111,6 +286,7 @@ export const VentilatorPlot = (props: any) => { yData={yAxisData("ventilator_resp_rate")} low={12} high={20} + verticalMarkerData={getMarkLineData("ventilator_resp_rate")} />
@@ -121,6 +297,7 @@ export const VentilatorPlot = (props: any) => { yData={yAxisData("ventilator_pressure_support")} low={5} high={15} + verticalMarkerData={getMarkLineData("ventilator_pressure_support")} />
@@ -129,6 +306,7 @@ export const VentilatorPlot = (props: any) => { name="Tidal Volume" xData={dates} yData={yAxisData("ventilator_tidal_volume")} + verticalMarkerData={getMarkLineData("ventilator_tidal_volume")} />
@@ -139,6 +317,7 @@ export const VentilatorPlot = (props: any) => { yData={yAxisData("ventilator_peep")} low={5} high={10} + verticalMarkerData={getMarkLineData("ventilator_peep")} />
@@ -149,6 +328,7 @@ export const VentilatorPlot = (props: any) => { yData={yAxisData("ventilator_fio2")} low={21} high={60} + verticalMarkerData={getMarkLineData("ventilator_fio2")} />
@@ -159,6 +339,7 @@ export const VentilatorPlot = (props: any) => { yData={yAxisData("ventilator_spo2")} low={90} high={100} + verticalMarkerData={getMarkLineData("ventilator_spo2")} />
@@ -169,6 +350,7 @@ export const VentilatorPlot = (props: any) => { yData={yAxisData("etco2")} low={35} high={45} + verticalMarkerData={getMarkLineData("etco2")} />
@@ -185,6 +367,9 @@ export const VentilatorPlot = (props: any) => { name="Oxygen Flow Rate" xData={dates} yData={yAxisData("ventilator_oxygen_modality_oxygen_rate")} + verticalMarkerData={getMarkLineData( + "ventilator_oxygen_modality_oxygen_rate", + )} />
@@ -193,20 +378,12 @@ export const VentilatorPlot = (props: any) => { name="Flow Rate" xData={dates} yData={yAxisData("ventilator_oxygen_modality_flow_rate")} + verticalMarkerData={getMarkLineData( + "ventilator_oxygen_modality_flow_rate", + )} />
- - {totalCount > PAGINATION_LIMIT && ( -
- -
- )} ); }; diff --git a/src/components/Facility/Consultations/VentilatorTable.tsx b/src/components/Facility/Consultations/VentilatorTable.tsx new file mode 100644 index 00000000000..2059f14bac0 --- /dev/null +++ b/src/components/Facility/Consultations/VentilatorTable.tsx @@ -0,0 +1,129 @@ +import { useTranslation } from "react-i18next"; + +import { compareByDateString, formatDateTime } from "@/Utils/utils"; + +import { DailyRoundsModel } from "../../Patient/models"; + +type VentilatorTableProps = { + dailyRoundsList?: DailyRoundsModel[]; +}; + +export default function VentilatorTable(props: VentilatorTableProps) { + const { t } = useTranslation(); + const { dailyRoundsList } = props; + + const VentilatorTableRow = ({ + dailyRound, + start_date, + end_date, + }: { + dailyRound: DailyRoundsModel; + start_date: string; + end_date: string; + }) => { + const getModeText = () => { + const { + ventilator_interface, + ventilator_mode, + ventilator_oxygen_modality, + } = dailyRound; + switch (ventilator_interface) { + case "INVASIVE": + case "NON_INVASIVE": + return t(`VENTILATOR_MODE__${ventilator_mode}`); + case "OXYGEN_SUPPORT": + return t(`OXYGEN_MODALITY__${ventilator_oxygen_modality}`); + default: + return null; + } + }; + return ( + + {start_date} + {end_date} + + {t(`RESPIRATORY_SUPPORT__${dailyRound?.ventilator_interface}`)} + + {getModeText()} + + ); + }; + + const getModeOrModality = (round: DailyRoundsModel) => { + const ventilatorInterface = round.ventilator_interface; + if (!ventilatorInterface) return null; + switch (ventilatorInterface) { + case "INVASIVE": + case "NON_INVASIVE": + return round.ventilator_mode; + case "OXYGEN_SUPPORT": + return round.ventilator_oxygen_modality; + default: + return null; + } + }; + + const VentilatorTableBody = (dailyRoundsList: DailyRoundsModel[]) => { + const rows = []; + for (let index = 0; index < dailyRoundsList.length; index++) { + const currentRound = dailyRoundsList[index]; + const currentInterfaceOrModality = getModeOrModality(currentRound); + if (!currentInterfaceOrModality) continue; + while (index < dailyRoundsList.length - 1) { + const nextRound = dailyRoundsList[index + 1]; + const nextInterfaceOrModality = getModeOrModality(nextRound); + if ( + nextInterfaceOrModality && + currentRound.ventilator_interface == nextRound.ventilator_interface && + currentInterfaceOrModality == nextInterfaceOrModality + ) { + index += 1; + } else { + break; + } + } + const end_date = + index + 1 < dailyRoundsList.length + ? formatDateTime(dailyRoundsList[index + 1].taken_at) + : ""; + const start_date = formatDateTime(currentRound.taken_at); + rows.push( + , + ); + } + return rows; + }; + + if (!dailyRoundsList?.length) { + return; + } + const sortedData: DailyRoundsModel[] = dailyRoundsList.sort( + compareByDateString("taken_at"), + ); + + return ( +
+ + + + + + + + + + + {VentilatorTableBody(sortedData)} +
+ {t("ventilator_log")} +
{t("start_datetime")}{t("end_datetime")}{t("ventilator_modality")} + {`${t("ventilator_mode")} / ${t("ventilator_oxygen_modality")}`} +
+
+ ); +} diff --git a/src/components/Facility/Consultations/components/BinaryChronologicalChart.tsx b/src/components/Facility/Consultations/components/BinaryChronologicalChart.tsx index 82eb7f3da4d..6183f19368d 100644 --- a/src/components/Facility/Consultations/components/BinaryChronologicalChart.tsx +++ b/src/components/Facility/Consultations/components/BinaryChronologicalChart.tsx @@ -4,7 +4,7 @@ import { formatDateTime } from "@/Utils/utils"; export default function BinaryChronologicalChart(props: { data: { - value: boolean; + value: boolean | undefined; timestamp: string; notes?: string; }[]; diff --git a/src/components/Facility/Consultations/components/LinePlot.tsx b/src/components/Facility/Consultations/components/LinePlot.tsx index 0829527faae..e9f1adf731f 100644 --- a/src/components/Facility/Consultations/components/LinePlot.tsx +++ b/src/components/Facility/Consultations/components/LinePlot.tsx @@ -12,12 +12,21 @@ export const LinePlot = (props: any) => { const { title, name, - xData, - yData, low = null, high = null, defaultSpace, + verticalMarkerData = null, } = props; + let { xData, yData } = props; + const yDatacount = yData.filter( + (item: number | null): item is number => + item !== null && !Number.isNaN(item), + ).length; + if (yDatacount === 0) { + yData = []; + xData = []; + } + let generalOptions: any = { grid: { top: "40px", @@ -106,6 +115,25 @@ export const LinePlot = (props: any) => { ], }; + if (verticalMarkerData && yDatacount > 0) { + let series = generalOptions.series[0]; + series = { + ...series, + markLine: { + silent: true, + data: verticalMarkerData, + symbol: "none", + lineStyle: { + color: "#000000", + }, + }, + }; + generalOptions = { + ...generalOptions, + series, + }; + } + if (props.type && props.type === "WAVEFORM") { generalOptions = { ...generalOptions, diff --git a/src/components/Facility/Consultations/components/ReactEcharts.tsx b/src/components/Facility/Consultations/components/ReactEcharts.tsx index 326bd23661a..4215b34fa4d 100644 --- a/src/components/Facility/Consultations/components/ReactEcharts.tsx +++ b/src/components/Facility/Consultations/components/ReactEcharts.tsx @@ -5,6 +5,7 @@ import { DataZoomComponent, GridComponent, LegendComponent, + MarkLineComponent, TitleComponent, ToolboxComponent, TooltipComponent, @@ -27,6 +28,7 @@ echarts.use([ TooltipComponent, VisualMapComponent, VisualMapPiecewiseComponent, + MarkLineComponent, ]); interface ReactEchartsProps extends EChartsReactProps { From 45ae794dc0ad99285dd66a4d6cb778652d3013fb Mon Sep 17 00:00:00 2001 From: JavidSumra <112365664+JavidSumra@users.noreply.github.com> Date: Sun, 10 Nov 2024 09:26:01 +0530 Subject: [PATCH 2/9] Fix Conditional Rendering of TextAreaFormField for Sample Test Type (#8960) --- public/locale/en.json | 23 ++- src/components/Patient/SampleDetails.tsx | 170 ++++++++++++++-------- src/components/Patient/SampleTest.tsx | 2 +- src/components/Patient/SampleTestCard.tsx | 5 +- 4 files changed, 130 insertions(+), 70 deletions(-) diff --git a/public/locale/en.json b/public/locale/en.json index ef591130213..ce1d79aa3dd 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -314,6 +314,7 @@ "are_you_still_watching": "Are you still watching?", "are_you_sure_want_to_delete": "Are you sure you want to delete {{name}}?", "are_you_sure_want_to_delete_this_record": "Are you sure want to delete this record?", + "ari": "ARI - Acute Respiratory illness", "asset_class": "Asset Class", "asset_location": "Asset Location", "asset_name": "Asset Name", @@ -324,6 +325,7 @@ "assigned_facility": "Facility assigned", "assigned_to": "Assigned to", "async_operation_warning": "This operation may take some time. Please check back later.", + "atypical_presentation_details": "Atypical presentation details", "audio__allow_permission": "Please allow microphone permission in site settings", "audio__allow_permission_button": "Click here to know how to allow", "audio__allow_permission_helper": "You might have denied microphone access in the past.", @@ -490,6 +492,8 @@ "contact_person_at_the_facility": "Contact person at the current facility", "contact_person_number": "Contact person number", "contact_phone": "Contact Person Number", + "contact_with_confirmed_carrier": "Contact with confirmed carrier", + "contact_with_suspected_carrier": "Contact with suspected carrier", "contact_your_admin_to_add_skills": "Contact your admin to add skills", "continue": "Continue", "continue_watching": "Continue watching", @@ -554,6 +558,7 @@ "diagnosis_at_discharge": "Diagnosis at Discharge", "diastolic": "Diastolic", "differential": "Differential", + "differential_diagnosis": "Differential diagnosis", "discard": "Discard", "discharge": "Discharge", "discharge_from_care": "Discharge from CARE", @@ -571,7 +576,9 @@ "district": "District", "district_program_management_supporting_unit": "District Program Management Supporting Unit", "doctor_s_medical_council_registration": "Doctor's Medical Council Registration", + "doctors_name": "Doctor's Name", "domestic_healthcare_support": "Domestic healthcare support", + "domestic_international_travel": "Domestic/international Travel (within last 28 days)", "done": "Done", "dosage": "Dosage", "down": "Down", @@ -639,6 +646,7 @@ "error_while_deleting_record": "Error while deleting record", "escape": "Escape", "estimated_contact_date": "Estimated contact date", + "etiology_identified": "Etiology identified", "events": "Events", "expand_sidebar": "Expand Sidebar", "expected_burn_rate": "Expected Burn Rate", @@ -652,6 +660,7 @@ "facility_search_placeholder": "Search by Facility / District Name", "facility_type": "Facility Type", "failed_to_link_abha_number": "Failed to link ABHA Number. Please try again later.", + "fast_track_testing_reason": "Fast track testing reason", "features": "Features", "feed_configurations": "Feed Configurations", "feed_is_currently_not_live": "Feed is currently not live", @@ -697,6 +706,7 @@ "goal": "Our goal is to continuously improve the quality and accessibility of public healthcare services using digital tools.", "granted_on": "Granted On", "has_domestic_healthcare_support": "Has domestic healthcare support?", + "has_sari": "Has SARI (Severe Acute Respiratory illness)?", "health_facility__config_registration_error": "Health ID registration failed", "health_facility__config_update_error": "Health Facility config update failed", "health_facility__config_update_success": "Health Facility config updated successfully", @@ -730,6 +740,7 @@ "hubs": "Hub Facilities", "i_declare": "I hereby declare that:", "icd11_as_recommended": "As per ICD-11 recommended by WHO", + "icmr_specimen_referral_form": "ICMR Specimen Referral Form", "incomplete_patient_details_warning": "Patient details are incomplete. Please update the details before proceeding.", "inconsistent_dosage_units_error": "Dosage units must be same", "indian_mobile": "Indian Mobile", @@ -764,15 +775,18 @@ "investigations_suggested": "Investigations Suggested", "is": "Is", "is_antenatal": "Is Antenatal", + "is_atypical_presentation": "Is Atypical presentation", "is_declared_positive": "Whether declared positive", "is_emergency": "Is emergency", "is_emergency_case": "Is emergency case", "is_it_upshift": "is it upshift", "is_this_an_emergency": "Is this an emergency?", "is_this_an_upshift": "Is this an upshift?", + "is_unusual_course": "Is unusual course", "is_up_shift": "Is up shift", "is_upshift_case": "Is upshift case", "is_vaccinated": "Whether vaccinated", + "label": "Label", "landline": "Indian landline", "language_selection": "Language Selection", "last_administered": "Last administered", @@ -902,7 +916,7 @@ "notice_board": "Notice Board", "notification_permission_denied": "Notification permission denied", "notification_permission_granted": "Notification permission granted", - "number_of_aged_dependents_above_60": "Number Of Aged Dependents (Above 60)", + "number_of_aged_dependents": "Number of Aged Dependents (Above 60)", "number_of_beds": "Number of beds", "number_of_beds_out_of_range_error": "Number of beds cannot be greater than 100", "number_of_chronic_diseased_dependents": "Number Of Chronic Diseased Dependents", @@ -936,6 +950,7 @@ "password_reset_failure": "Password Reset Failed", "password_reset_success": "Password Reset successfully", "password_sent": "Password Reset Email Sent", + "patient": "Patient", "patient_address": "Patient Address", "patient_body": "Patient Body", "patient_category": "Patient Category", @@ -1077,6 +1092,7 @@ "result": "Result", "result_date": "Result Date", "result_details": "Result details", + "result_on": "Result on", "resume": "Resume", "retake": "Retake", "return_to_care": "Return to CARE", @@ -1090,7 +1106,11 @@ "sample_collection_date": "Sample Collection Date", "sample_format": "Sample Format", "sample_test": "Sample Test", + "sample_test_details": "Sample Test Details", + "sample_test_history": "Sample Test History", "sample_type": "Sample Type", + "sample_type_description": "Sample Type Description", + "sari": "SARI - Severe Acute Respiratory illness", "save": "Save", "save_and_continue": "Save and Continue", "save_investigation": "Save Investigation", @@ -1180,6 +1200,7 @@ "tachycardia": "Tachycardia", "target_dosage": "Target Dosage", "test_type": "Type of test done", + "tested_on": "Tested on", "third_party_software_licenses": "Third Party Software Licenses", "titrate_dosage": "Titrate Dosage", "to_be_conducted": "To be conducted", diff --git a/src/components/Patient/SampleDetails.tsx b/src/components/Patient/SampleDetails.tsx index c556177ec13..c16ef1c0e86 100644 --- a/src/components/Patient/SampleDetails.tsx +++ b/src/components/Patient/SampleDetails.tsx @@ -1,5 +1,6 @@ import { camelCase, capitalize, startCase } from "lodash-es"; import { navigate } from "raviger"; +import { useTranslation } from "react-i18next"; import Card from "@/CAREUI/display/Card"; @@ -17,6 +18,7 @@ import useQuery from "@/Utils/request/useQuery"; import { formatDateTime, formatPatientAge } from "@/Utils/utils"; export const SampleDetails = ({ id }: DetailRoute) => { + const { t } = useTranslation(); const { loading: isLoading, data: sampleDetails } = useQuery( routes.getTestSample, { @@ -33,9 +35,9 @@ export const SampleDetails = ({ id }: DetailRoute) => { const yesOrNoBadge = (param: any) => param ? ( - Yes + {t("yes")} ) : ( - No + {t("no")} ); const showPatientCard = (patientData: any) => { @@ -51,20 +53,24 @@ export const SampleDetails = ({ id }: DetailRoute) => {
- Name: + + {t("name")}:{" "} + {patientData?.name}
{patientData?.is_medical_worker && (
- Medical Worker:{" "} + {t("medical_worker")}:{" "} + + + {t("yes")} - Yes
)}
- Disease Status:{" "} + {t("disease_status")}:{" "} {patientData?.disease_status} @@ -72,16 +78,20 @@ export const SampleDetails = ({ id }: DetailRoute) => {
- SRF ID: + + {t("srf_id")}:{" "} + {(patientData?.srf_id && patientData?.srf_id) || "-"}
- Test Type: + + {t("test_type")}:{" "} + {(patientData?.test_type && testType) || "-"}
- Date of Test:{" "} + {t("date_of_test")}:{" "} {(patientData?.date_of_test && formatDateTime(patientData?.date_of_test)) || @@ -89,35 +99,43 @@ export const SampleDetails = ({ id }: DetailRoute) => {
- Facility: + + {t("facility")}:{" "} + {patientData?.facility_object?.name || "-"}
{patientData?.date_of_birth ? (
- Date of birth:{" "} + {t("date_of_birth")}:{" "} {patientData?.date_of_birth}
) : (
- Age: + + {t("age")}:{" "} + {formatPatientAge(patientData)}
)}
- Gender: + + {t("gender")}:{" "} + {patientGender}
- Phone: + + {t("phone")}:{" "} + {patientData?.phone_number || "-"}
- Nationality:{" "} + {t("nationality")}:{" "} {patientData?.nationality || "-"}
@@ -125,14 +143,14 @@ export const SampleDetails = ({ id }: DetailRoute) => {
- Blood Group:{" "} + {t("blood_group")}:{" "} {patientData?.blood_group || "-"}
{patientData?.nationality !== "India" && (
- Passport Number:{" "} + {t("passport_number")}:{" "} {patientData?.passport_no || "-"}
@@ -140,56 +158,60 @@ export const SampleDetails = ({ id }: DetailRoute) => { {patientData?.nationality === "India" && ( <>
- State: + + {t("state")}:{" "} + {patientData?.state_object?.name}
- District:{" "} + {t("district")}:{" "} {patientData?.district_object?.name || "-"}
- Local Body:{" "} + {t("local_body")}:{" "} {patientData?.local_body_object?.name || "-"}
)}
- Address: + + {t("address")}:{" "} + {patientData?.address || "-"}
- Contact with confirmed carrier:{" "} + {t("contact_with_confirmed_carrier")}:{" "} {yesOrNoBadge(patientData?.contact_with_confirmed_carrier)}
- Contact with suspected carrier:{" "} + {t("contact_with_suspected_carrier")}:{" "} {yesOrNoBadge(patientData?.contact_with_suspected_carrier)}
{patientData?.estimated_contact_date && (
- Estimated contact date:{" "} + {t("estimated_contact_date")}:{" "} {formatDateTime(patientData?.estimated_contact_date)}
)}
- Has SARI (Severe Acute Respiratory illness)?:{" "} + {t("has_sari")}:{" "} {yesOrNoBadge(patientData?.has_SARI)}
- Domestic/international Travel (within last 28 days):{" "} + {t("domestic_international_travel")}:{" "} {yesOrNoBadge(patientData?.past_travel)}
@@ -197,7 +219,7 @@ export const SampleDetails = ({ id }: DetailRoute) => { !!patientData?.countries_travelled.length && (
- Countries travelled:{" "} + {t("countries_travelled")}:{" "} {patientData?.countries_travelled.join(", ")}
@@ -205,7 +227,7 @@ export const SampleDetails = ({ id }: DetailRoute) => { {patientData?.ongoing_medication && (
- Ongoing Medications{" "} + {t("ongoing_medications")}{" "} {patientData?.ongoing_medication}
@@ -213,7 +235,7 @@ export const SampleDetails = ({ id }: DetailRoute) => { {patientData?.allergies && (
- Allergies:{" "} + {t("allergies")}:{" "} {patientData?.allergies}
@@ -221,7 +243,7 @@ export const SampleDetails = ({ id }: DetailRoute) => { {!!patientData?.number_of_aged_dependents && (
- Number Of Aged Dependents (Above 60):{" "} + {t("number_of_aged_dependents")}:{" "} {patientData?.number_of_aged_dependents}
@@ -229,7 +251,7 @@ export const SampleDetails = ({ id }: DetailRoute) => { {!!patientData?.number_of_chronic_diseased_dependents && (
- Number Of Chronic Diseased Dependents:{" "} + {t("number_of_chronic_diseased_dependents")}:{" "} {patientData?.number_of_chronic_diseased_dependents}
@@ -245,19 +267,25 @@ export const SampleDetails = ({ id }: DetailRoute) => {
- Status: {" "} + + {t("status")}:{" "} + {" "} {startCase(camelCase(flow.status))}
- Label:{" "} + {t("label")}:{" "} {capitalize(flow.notes)}
- Created On :{" "} + + {t("created_on")}: + {" "} {flow.created_date ? formatDateTime(flow.created_date) : "-"}
- Modified on:{" "} + + {t("modified_on")}: + {" "} {flow.modified_date ? formatDateTime(flow.modified_date) : "-"}
@@ -271,7 +299,7 @@ export const SampleDetails = ({ id }: DetailRoute) => { return ( { - ICMR Specimen Referral Form + {t("icmr_specimen_referral_form")}
) @@ -289,34 +317,42 @@ export const SampleDetails = ({ id }: DetailRoute) => {
- Status:{" "} + {t("status")}:{" "} {sampleDetails?.status}
- Result:{" "} + {t("result")}:{" "} {sampleDetails?.result}
- Patient: + + {t("patient")}:{" "} + {sampleDetails?.patient_name}
{sampleDetails?.facility_object && (
- Facility: + + {t("facility")}:{" "} + {sampleDetails?.facility_object.name}
)}
- Tested on: + + {t("tested_on")}:{" "} + {sampleDetails?.date_of_result ? formatDateTime(sampleDetails.date_of_result) : "-"}
- Result on: + + {t("result_on")}:{" "} + {sampleDetails?.date_of_result ? formatDateTime(sampleDetails.date_of_result) : "-"} @@ -324,7 +360,7 @@ export const SampleDetails = ({ id }: DetailRoute) => { {sampleDetails?.fast_track && (
- Fast track testing reason:{" "} + {t("fast_track_testing_reason")}:{" "} {sampleDetails.fast_track}
@@ -332,21 +368,23 @@ export const SampleDetails = ({ id }: DetailRoute) => { {sampleDetails?.doctor_name && (
- Doctor's Name:{" "} + {t("doctors_name")}:{" "} {startCase(camelCase(sampleDetails.doctor_name))}
)} {sampleDetails?.diagnosis && (
- Diagnosis: + + {t("diagnosis")}:{" "} + {sampleDetails.diagnosis}
)} {sampleDetails?.diff_diagnosis && (
- Differential diagnosis:{" "} + {t("differential_diagnosis")}:{" "} {sampleDetails?.diff_diagnosis}
@@ -354,52 +392,48 @@ export const SampleDetails = ({ id }: DetailRoute) => { {sampleDetails?.etiology_identified && (
- Etiology identified:{" "} + {t("etiology_identified")}:{" "} {sampleDetails.etiology_identified}
)}
- Is Atypical presentation{" "} + {t("is_atypical_presentation")}{" "} {yesOrNoBadge(sampleDetails?.is_atypical_presentation)}
- Is unusual course{" "} + {t("is_unusual_course")}{" "} {yesOrNoBadge(sampleDetails?.is_unusual_course)}
{sampleDetails?.atypical_presentation && (
- Atypical presentation details:{" "} + {t("atypical_presentation_details")}:{" "} {sampleDetails.atypical_presentation}
)}
- - SARI - Severe Acute Respiratory illness{" "} - + {t("sari")} {yesOrNoBadge(sampleDetails?.has_sari)}
- - ARI - Acute Respiratory illness{" "} - + {t("ari")} {yesOrNoBadge(sampleDetails?.has_ari)}
- Contact with confirmed carrier{" "} + {t("contact_with_confirmed_carrier")}{" "} {yesOrNoBadge(sampleDetails?.patient_has_confirmed_contact)}
- Contact with suspected carrier{" "} + {t("contact_with_suspected_carrier")}{" "} {yesOrNoBadge(sampleDetails?.patient_has_suspected_contact)}
@@ -407,29 +441,37 @@ export const SampleDetails = ({ id }: DetailRoute) => { sampleDetails.patient_travel_history.length !== 0 && (
- Countries travelled:{" "} + {t("countries_travelled")}:{" "} {sampleDetails.patient_travel_history}
)} {sampleDetails?.sample_type && ( -
+
- Sample Type:{" "} + {t("sample_type")}:{" "} {startCase(camelCase(sampleDetails.sample_type))}
)} + {sampleDetails?.sample_type === "OTHER TYPE" && ( +
+ + {t("sample_type_description")}:{" "} + + {sampleDetails?.sample_type_other} +
+ )}
-

Details of patient

+

{t("details_of_patient")}

{showPatientCard(sampleDetails?.patient_object)}
-

Sample Test History

+

{t("sample_test_history")}

{sampleDetails?.flow && sampleDetails.flow.map((flow: FlowModel) => renderFlow(flow))}
diff --git a/src/components/Patient/SampleTest.tsx b/src/components/Patient/SampleTest.tsx index afddb81b654..0714de28492 100644 --- a/src/components/Patient/SampleTest.tsx +++ b/src/components/Patient/SampleTest.tsx @@ -218,7 +218,7 @@ export const SampleTest = ({ facilityId, patientId }: any) => { optionValue={(option) => option.id} /> - {state.form.sample_type === "OTHER TYPE" && ( + {state.form.sample_type === "9" && ( { Sample Type{" "}
- {(itemData.sample_type !== "OTHER TYPE" - ? itemData.sample_type - : itemData.sample_type_other - )?.toLowerCase()} + {itemData.sample_type?.toLowerCase()}
From 5502f1691adcb29da15a1a2ea05e178adf117ef4 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Mon, 11 Nov 2024 11:01:14 +0530 Subject: [PATCH 3/9] fixes auto needs testing label (#9025) --- .github/workflows/auto-testing-label.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auto-testing-label.yml b/.github/workflows/auto-testing-label.yml index 6c6fc1002a0..98cfd46dab3 100644 --- a/.github/workflows/auto-testing-label.yml +++ b/.github/workflows/auto-testing-label.yml @@ -38,7 +38,7 @@ jobs: } if (isChangesRequired) { - await github.issues.createComment({ + await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: pr.number, From 68bfde477ed7f50af2f87e27ff2edb2a4b38cf6b Mon Sep 17 00:00:00 2001 From: Shaurya Gupta Date: Mon, 11 Nov 2024 11:52:47 +0530 Subject: [PATCH 4/9] Update tooltip position and rotation in Discussion Notes panel (#8938) --- .../Facility/PatientNotesSlideover.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/components/Facility/PatientNotesSlideover.tsx b/src/components/Facility/PatientNotesSlideover.tsx index 05deef36b91..89d38a5f168 100644 --- a/src/components/Facility/PatientNotesSlideover.tsx +++ b/src/components/Facility/PatientNotesSlideover.tsx @@ -161,15 +161,19 @@ export default function PatientNotesSlideover(props: PatientNotesProps) { id="expand_doctor_notes" className={classNames( "tooltip flex h-8 w-8 cursor-pointer items-center justify-center rounded bg-primary-800 text-secondary-100 text-opacity-70 hover:bg-primary-700 hover:text-opacity-100", - show && "rotate-180", )} onClick={() => setShow(!show)} > - + {t("minimize")}
@@ -181,7 +185,12 @@ export default function PatientNotesSlideover(props: PatientNotesProps) { icon="l-times" className="tooltip text-lg transition-all delay-150 duration-300 ease-out" /> - + {t("close")} From cc9d0c977b3ebc03b4a86bb385bc03e299834f32 Mon Sep 17 00:00:00 2001 From: JavidSumra <112365664+JavidSumra@users.noreply.github.com> Date: Mon, 11 Nov 2024 12:46:38 +0530 Subject: [PATCH 5/9] Add Cypress Test Suite for Facility Notice Board Functionality Verification (#9045) --- .../e2e/facility_spec/FacilityHomepage.cy.ts | 53 ++++++++++++++++- cypress/pageobject/Facility/FacilityHome.ts | 3 +- cypress/pageobject/Facility/FacilityNotify.ts | 58 +++++++++++++++++++ cypress/pageobject/Login/LoginPage.ts | 4 ++ cypress/pageobject/Users/ManageUserPage.ts | 8 +++ src/components/Common/Sidebar/SidebarItem.tsx | 2 + src/components/Facility/FacilityCard.tsx | 5 +- src/components/Notifications/NoticeBoard.tsx | 4 +- .../Notifications/NotificationsList.tsx | 5 +- 9 files changed, 135 insertions(+), 7 deletions(-) create mode 100644 cypress/pageobject/Facility/FacilityNotify.ts diff --git a/cypress/e2e/facility_spec/FacilityHomepage.cy.ts b/cypress/e2e/facility_spec/FacilityHomepage.cy.ts index bc84aea4882..7db3b308a53 100644 --- a/cypress/e2e/facility_spec/FacilityHomepage.cy.ts +++ b/cypress/e2e/facility_spec/FacilityHomepage.cy.ts @@ -2,6 +2,7 @@ import { AssetPagination } from "../../pageobject/Asset/AssetPagination"; import FacilityPage from "../../pageobject/Facility/FacilityCreation"; import FacilityHome from "../../pageobject/Facility/FacilityHome"; +import FacilityNotify from "../../pageobject/Facility/FacilityNotify"; import LoginPage from "../../pageobject/Login/LoginPage"; import ManageUserPage from "../../pageobject/Users/ManageUserPage"; import { UserPage } from "../../pageobject/Users/UserSearch"; @@ -9,6 +10,7 @@ import { UserPage } from "../../pageobject/Users/UserSearch"; describe("Facility Homepage Function", () => { const loginPage = new LoginPage(); const facilityHome = new FacilityHome(); + const facilityNotify = new FacilityNotify(); const facilityPage = new FacilityPage(); const manageUserPage = new ManageUserPage(); const userPage = new UserPage(); @@ -22,6 +24,8 @@ describe("Facility Homepage Function", () => { const district = "Ernakulam"; const localBody = "Aikaranad"; const facilityType = "Private Hospital"; + const notificationErrorMsg = "Message cannot be empty"; + const notificationMessage = "Test Notification"; before(() => { loginPage.loginAsDistrictAdmin(); @@ -30,6 +34,7 @@ describe("Facility Homepage Function", () => { beforeEach(() => { cy.restoreLocalStorage(); + cy.clearLocalStorage(/filters--.+/); cy.awaitUrl("/facility"); }); @@ -41,9 +46,6 @@ describe("Facility Homepage Function", () => { facilityHome.clickViewCnsButton(); facilityHome.verifyCnsUrl(); facilityHome.navigateBack(); - // view notify button - facilityHome.clickFacilityNotifyButton(); - facilityHome.verifyAndCloseNotifyModal(); // view facility button facilityHome.clickViewFacilityDetails(); facilityPage.getFacilityName().should("be.visible"); @@ -134,6 +136,51 @@ describe("Facility Homepage Function", () => { facilityHome.verifyLiveMonitorUrl(); }); + it("Verify Notice Board Functionality", () => { + // search facility and verify it's loaded or not + manageUserPage.interceptFacilitySearchReq(); + manageUserPage.typeFacilitySearch(facilityName); + manageUserPage.verifyFacilitySearchReq(); + // verify facility name and card reflection + facilityNotify.verifyUrlContains("Dummy+Facility+40"); + facilityPage.verifyFacilityBadgeContent(facilityName); + manageUserPage.assertFacilityInCard(facilityName); + // send notification to a facility + facilityHome.clickFacilityNotifyButton(); + facilityNotify.verifyFacilityName(facilityName); + facilityNotify.fillNotifyText(notificationMessage); + facilityNotify.interceptPostNotificationReq(); + cy.submitButton("Notify"); + facilityNotify.verifyPostNotificationReq(); + cy.verifyNotification("Facility Notified"); + cy.closeNotification(); + cy.wait(2000); + // Verify the frontend error on empty message + facilityHome.clickFacilityNotifyButton(); + facilityNotify.verifyFacilityName(facilityName); + cy.submitButton("Notify"); + facilityNotify.verifyErrorMessage(notificationErrorMsg); + // close pop-up and verify + facilityHome.verifyAndCloseNotifyModal(); + // signout as district admin and login as a Nurse + loginPage.ensureLoggedIn(); + loginPage.clickSignOutBtn(); + loginPage.loginManuallyAsNurse(); + // Verify Notice Board Reflection + facilityNotify.interceptGetNotificationReq("MESSAGE"); + facilityNotify.visitNoticeBoard(); + facilityNotify.verifyGetNotificationReq(); + facilityNotify.verifyFacilityNoticeBoardMessage(notificationMessage); + facilityNotify.interceptGetNotificationReq(); + // Verify Sidebar Notification Reflection + facilityNotify.openNotificationSlide(); + facilityNotify.verifyGetNotificationReq(); + cy.verifyContentPresence("#notification-slide-msg", [notificationMessage]); + facilityNotify.closeNotificationSlide(); + loginPage.ensureLoggedIn(); + loginPage.clickSignOutBtn(); + }); + afterEach(() => { cy.saveLocalStorage(); }); diff --git a/cypress/pageobject/Facility/FacilityHome.ts b/cypress/pageobject/Facility/FacilityHome.ts index dea7de0e7b6..30f51052370 100644 --- a/cypress/pageobject/Facility/FacilityHome.ts +++ b/cypress/pageobject/Facility/FacilityHome.ts @@ -35,7 +35,8 @@ class FacilityHome { } clickFacilityNotifyButton() { - cy.get("#facility-notify").first().click(); + cy.get("#facility-notify", { timeout: 10000 }).should("be.visible"); + cy.get("#facility-notify").focus().click(); } clickLiveMonitorButton() { diff --git a/cypress/pageobject/Facility/FacilityNotify.ts b/cypress/pageobject/Facility/FacilityNotify.ts new file mode 100644 index 00000000000..08c44b32e84 --- /dev/null +++ b/cypress/pageobject/Facility/FacilityNotify.ts @@ -0,0 +1,58 @@ +export default class FacilityNotify { + verifyFacilityName(facilityName: string) { + cy.verifyContentPresence("#notify-facility-name", [facilityName]); + } + + verifyErrorMessage(errorMessage: string) { + cy.verifyContentPresence(".error-text", [errorMessage]); + } + + fillNotifyText(message: string) { + cy.get("#NotifyModalMessageInput").scrollIntoView(); + cy.get("#NotifyModalMessageInput").click().type(message); + } + + verifyFacilityNoticeBoardMessage(message: string) { + cy.get("#notification-message", { timeout: 10000 }).should("be.visible"); + cy.verifyContentPresence("#notification-message", [message]); + } + + openNotificationSlide() { + cy.get("#notification-slide-btn").should("be.visible").click(); + } + + closeNotificationSlide() { + cy.get("#close-slide-over").should("be.visible").click(); + } + + visitNoticeBoard() { + cy.get("a[href='/notice_board']").should("be.visible").click(); + } + + visitNotificationSideBar() { + cy.get("#notification-slide-btn").should("be.visible").click(); + } + + verifyUrlContains(substring: string) { + cy.url().should("include", substring); + } + + interceptPostNotificationReq() { + cy.intercept("POST", "**/api/v1/notification/notify").as("notifyFacility"); + } + + verifyPostNotificationReq() { + cy.wait("@notifyFacility").its("response.statusCode").should("eq", 204); + } + + interceptGetNotificationReq(event: string = "") { + cy.intercept( + "GET", + `**/api/v1/notification/?offset=0&event=${event}&*=SYSTEM`, + ).as("getNotifications"); + } + + verifyGetNotificationReq() { + cy.wait("@getNotifications").its("response.statusCode").should("eq", 200); + } +} diff --git a/cypress/pageobject/Login/LoginPage.ts b/cypress/pageobject/Login/LoginPage.ts index cd5230a7772..38b8aeee2af 100644 --- a/cypress/pageobject/Login/LoginPage.ts +++ b/cypress/pageobject/Login/LoginPage.ts @@ -34,6 +34,10 @@ class LoginPage { cy.get("#sign-out-button").scrollIntoView(); cy.get("#sign-out-button").contains("Sign Out").should("exist"); } + + clickSignOutBtn(): void { + cy.verifyAndClickElement("#sign-out-button", "Sign Out"); + } } export default LoginPage; diff --git a/cypress/pageobject/Users/ManageUserPage.ts b/cypress/pageobject/Users/ManageUserPage.ts index 470862693a8..a3a6e72fbc3 100644 --- a/cypress/pageobject/Users/ManageUserPage.ts +++ b/cypress/pageobject/Users/ManageUserPage.ts @@ -99,6 +99,14 @@ export class ManageUserPage { cy.get("#search").click().type(facilityName); } + interceptFacilitySearchReq() { + cy.intercept("GET", "**/api/v1/facility/**").as("searchFacility"); + } + + verifyFacilitySearchReq() { + cy.wait("@searchFacility").its("response.statusCode").should("eq", 200); + } + assertFacilityInCard(facilityName: string) { cy.get("#facility-name-card").should("contain", facilityName); } diff --git a/src/components/Common/Sidebar/SidebarItem.tsx b/src/components/Common/Sidebar/SidebarItem.tsx index 31f64754ffd..7262c6a103b 100644 --- a/src/components/Common/Sidebar/SidebarItem.tsx +++ b/src/components/Common/Sidebar/SidebarItem.tsx @@ -9,6 +9,7 @@ import useAppHistory from "@/hooks/useAppHistory"; export type SidebarIcon = React.ReactNode; type SidebarItemProps = { + id?: string; ref?: React.Ref; text: string; icon: SidebarIcon; @@ -31,6 +32,7 @@ const SidebarItemBase = forwardRef( return ( + Notify: {facility.name} } diff --git a/src/components/Notifications/NoticeBoard.tsx b/src/components/Notifications/NoticeBoard.tsx index e4472107c8f..e1f00075000 100644 --- a/src/components/Notifications/NoticeBoard.tsx +++ b/src/components/Notifications/NoticeBoard.tsx @@ -26,7 +26,9 @@ export const NoticeBoard = () => { className="overflow-hidden rounded shadow-md" >
-
{item.message}
+
+ {item.message} +
{formatName(item.caused_by)} -{" "} diff --git a/src/components/Notifications/NotificationsList.tsx b/src/components/Notifications/NotificationsList.tsx index 5d88cc8d603..0808877a444 100644 --- a/src/components/Notifications/NotificationsList.tsx +++ b/src/components/Notifications/NotificationsList.tsx @@ -115,7 +115,9 @@ const NotificationTile = ({ />
-
{result.message}
+
+ {result.message} +
{formatDateTime(result.created_date)} @@ -474,6 +476,7 @@ export default function NotificationsList({ <> setOpen(!open)} icon={} badgeCount={unreadCount} From 5d95dc68bbbe3868f1338fbb3e3b3ff3f127ed81 Mon Sep 17 00:00:00 2001 From: Mohammed Nihal <57055998+nihal467@users.noreply.github.com> Date: Mon, 11 Nov 2024 14:46:03 +0530 Subject: [PATCH 6/9] New Cypress Test | Create Partial Investigation Log Update (#9034) --- .../e2e/patient_spec/PatientHomepage.cy.ts | 1 - .../patient_spec/PatientInvestigation.cy.ts | 40 +++++++++++++++++++ .../Patient/PatientInvestigation.ts | 10 ++++- .../ConsultationInvestigationsTab.tsx | 1 + .../Investigations/ViewInvestigations.tsx | 2 + 5 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 cypress/e2e/patient_spec/PatientInvestigation.cy.ts diff --git a/cypress/e2e/patient_spec/PatientHomepage.cy.ts b/cypress/e2e/patient_spec/PatientHomepage.cy.ts index 32d869b8f50..53fb8732712 100644 --- a/cypress/e2e/patient_spec/PatientHomepage.cy.ts +++ b/cypress/e2e/patient_spec/PatientHomepage.cy.ts @@ -43,7 +43,6 @@ describe("Patient Homepage present functionalities", () => { patientHome.typePatientAdmitedBeforeDate(patientFromDate); patientHome.typePatientAdmitedAfterDate(patientToDate); patientHome.clickPatientFilterApply(); - patientHome.verifyTotalPatientCount("1"); // verify the badge and clear the count patientHome.verifyPatientCreatedBeforeDate(patientToDateBadge); patientHome.verifyPatientCreatedAfterDate(patientFromDateBadge); diff --git a/cypress/e2e/patient_spec/PatientInvestigation.cy.ts b/cypress/e2e/patient_spec/PatientInvestigation.cy.ts new file mode 100644 index 00000000000..380adf2c37e --- /dev/null +++ b/cypress/e2e/patient_spec/PatientInvestigation.cy.ts @@ -0,0 +1,40 @@ +import { PatientPage } from "pageobject/Patient/PatientCreation"; +import PatientInvestigation from "pageobject/Patient/PatientInvestigation"; + +import LoginPage from "../../pageobject/Login/LoginPage"; + +describe("Patient Investigation Creation from Patient consultation page", () => { + const loginPage = new LoginPage(); + const patientPage = new PatientPage(); + const patientInvestigation = new PatientInvestigation(); + const patientName = "Dummy Patient 12"; + + before(() => { + loginPage.loginAsDistrictAdmin(); + cy.saveLocalStorage(); + }); + + beforeEach(() => { + cy.restoreLocalStorage(); + cy.clearLocalStorage(/filters--.+/); + cy.awaitUrl("/patients"); + }); + + it("Create a investigation for a patient and verify its reflection", () => { + patientPage.visitPatient(patientName); + patientInvestigation.clickInvestigationTab(); + patientInvestigation.clickLogLabResults(); + patientInvestigation.selectInvestigationOption([ + "Haematology", + "Urine Test", + ]); + cy.submitButton("Save Investigation"); + cy.verifyNotification("Please Enter at least one value"); + cy.closeNotification(); + // Temporary workflow for investigation since we dont have dummy data and moving away from existing module + }); + + afterEach(() => { + cy.saveLocalStorage(); + }); +}); diff --git a/cypress/pageobject/Patient/PatientInvestigation.ts b/cypress/pageobject/Patient/PatientInvestigation.ts index 8f73cf908bc..a9f8f38435a 100644 --- a/cypress/pageobject/Patient/PatientInvestigation.ts +++ b/cypress/pageobject/Patient/PatientInvestigation.ts @@ -1,11 +1,9 @@ class PatientInvestigation { clickAddInvestigation() { - cy.get("#investigation").scrollIntoView(); cy.verifyAndClickElement("#investigation", "Add Investigation"); } clickInvestigationTab() { - cy.get("#consultation_tab_nav").scrollIntoView(); cy.verifyAndClickElement("#consultation_tab_nav", "Investigations"); } @@ -19,6 +17,14 @@ class PatientInvestigation { cy.get("#investigation-checkbox").click(); } + selectInvestigationOption(options: string[]) { + cy.clickAndMultiSelectOption("#investigations", options); + } + + clickLogLabResults() { + cy.verifyAndClickElement("#log-lab-results", "Log Lab Results"); + } + selectInvestigationFrequency(frequency: string) { cy.get("#investigation-frequency").click(); cy.contains("button", frequency).should("be.visible").click(); diff --git a/src/components/Facility/ConsultationDetails/ConsultationInvestigationsTab.tsx b/src/components/Facility/ConsultationDetails/ConsultationInvestigationsTab.tsx index ed1157f2c95..bbc17d925a9 100644 --- a/src/components/Facility/ConsultationDetails/ConsultationInvestigationsTab.tsx +++ b/src/components/Facility/ConsultationDetails/ConsultationInvestigationsTab.tsx @@ -17,6 +17,7 @@ export const ConsultationInvestigationsTab = (props: ConsultationTabProps) => {
diff --git a/src/components/Facility/Investigations/ViewInvestigations.tsx b/src/components/Facility/Investigations/ViewInvestigations.tsx index 162fd6d87e0..3bd80b0bb28 100644 --- a/src/components/Facility/Investigations/ViewInvestigations.tsx +++ b/src/components/Facility/Investigations/ViewInvestigations.tsx @@ -62,6 +62,7 @@ export default function ViewInvestigations(props: {
navigate( `/facility/${facilityId}/patient/${patientId}/consultation/${consultationId}/investigation/${investigationSession.session_external_id}`, @@ -73,6 +74,7 @@ export default function ViewInvestigations(props: { {t("view")} navigate( `/facility/${facilityId}/patient/${patientId}/consultation/${consultationId}/investigation/${investigationSession.session_external_id}/print`, From 693184592c15d3c5f287213fde120c412faefe32 Mon Sep 17 00:00:00 2001 From: Srayash <146334722+Srayash@users.noreply.github.com> Date: Mon, 11 Nov 2024 14:48:08 +0530 Subject: [PATCH 7/9] "fix icon overlap in edit consultation page." #9077 (#9078) --- src/components/Facility/ConsultationForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Facility/ConsultationForm.tsx b/src/components/Facility/ConsultationForm.tsx index 34cc5973832..d6d94593049 100644 --- a/src/components/Facility/ConsultationForm.tsx +++ b/src/components/Facility/ConsultationForm.tsx @@ -936,7 +936,7 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => {
{ From fc4f94debbc0b41394ca01087a90bbfc35b01c37 Mon Sep 17 00:00:00 2001 From: Nithish Kumar Siliveru Date: Mon, 11 Nov 2024 17:11:59 +0530 Subject: [PATCH 8/9] Fixed search icon and removed delete option (#9059) --- src/Utils/request/api.tsx | 12 ------ src/components/Form/SearchInput.tsx | 2 +- src/components/Resource/ResourceDetails.tsx | 46 --------------------- src/components/Shifting/ShiftDetails.tsx | 43 ------------------- 4 files changed, 1 insertion(+), 102 deletions(-) diff --git a/src/Utils/request/api.tsx b/src/Utils/request/api.tsx index 441b9a3d8c8..e1777364b3f 100644 --- a/src/Utils/request/api.tsx +++ b/src/Utils/request/api.tsx @@ -1065,11 +1065,6 @@ const routes = { TBody: Type(), TRes: Type(), }, - deleteShiftRecord: { - path: "/api/v1/shift/{id}/", - method: "DELETE", - TRes: Type<{ detail: string }>(), - }, listShiftRequests: { path: "/api/v1/shift/", method: "GET", @@ -1241,13 +1236,6 @@ const routes = { TRes: Type(), TBody: Type>(), }, - deleteResourceRecord: { - path: "/api/v1/resource/{id}/", - method: "DELETE", - TRes: Type<{ - detail?: string; - }>(), - }, listResourceRequests: { path: "/api/v1/resource/", method: "GET", diff --git a/src/components/Form/SearchInput.tsx b/src/components/Form/SearchInput.tsx index bbf59305a0a..69334c0f4db 100644 --- a/src/components/Form/SearchInput.tsx +++ b/src/components/Form/SearchInput.tsx @@ -87,7 +87,7 @@ const SearchInput = ({ className={className} leading={ props.leading || ( - + ) } trailing={ diff --git a/src/components/Resource/ResourceDetails.tsx b/src/components/Resource/ResourceDetails.tsx index 2495c64433d..d77bdcc3fea 100644 --- a/src/components/Resource/ResourceDetails.tsx +++ b/src/components/Resource/ResourceDetails.tsx @@ -4,21 +4,16 @@ import { useState } from "react"; import CareIcon from "@/CAREUI/icons/CareIcon"; import ButtonV2 from "@/components/Common/ButtonV2"; -import ConfirmDialog from "@/components/Common/ConfirmDialog"; import Loading from "@/components/Common/Loading"; import Page from "@/components/Common/Page"; import CommentSection from "@/components/Resource/ResourceCommentSection"; -import * as Notification from "@/Utils/Notifications"; import routes from "@/Utils/request/api"; -import request from "@/Utils/request/request"; import useQuery from "@/Utils/request/useQuery"; import { classNames, formatDateTime, formatName } from "@/Utils/utils"; export default function ResourceDetails(props: { id: string }) { const [isPrintMode, setIsPrintMode] = useState(false); - const [openDeleteResourceDialog, setOpenDeleteResourceDialog] = - useState(false); const { data, loading } = useQuery(routes.getResourceDetails, { pathParams: { id: props.id }, onResponse: ({ res, data }) => { @@ -27,25 +22,6 @@ export default function ResourceDetails(props: { id: string }) { } }, }); - - const handleResourceDelete = async () => { - setOpenDeleteResourceDialog(true); - const { res, data } = await request(routes.deleteResourceRecord, { - pathParams: { id: props.id }, - }); - if (res?.status === 204) { - Notification.Success({ - msg: "Resource record has been deleted successfully.", - }); - } else { - Notification.Error({ - msg: "Error while deleting Resource: " + (data?.detail || ""), - }); - } - - navigate("/resource"); - }; - const showFacilityCard = (facilityData: any) => { return (
@@ -329,28 +305,6 @@ export default function ResourceDetails(props: { id: string }) {
{data.reason || "--"}
- -
-
- setOpenDeleteResourceDialog(true)} - > - Delete Record - - - setOpenDeleteResourceDialog(false)} - onConfirm={handleResourceDelete} - /> -
-

Audit Log

diff --git a/src/components/Shifting/ShiftDetails.tsx b/src/components/Shifting/ShiftDetails.tsx index d5b251eeb8d..8e85fda3d19 100644 --- a/src/components/Shifting/ShiftDetails.tsx +++ b/src/components/Shifting/ShiftDetails.tsx @@ -9,7 +9,6 @@ import RecordMeta from "@/CAREUI/display/RecordMeta"; import CareIcon from "@/CAREUI/icons/CareIcon"; import ButtonV2 from "@/components/Common/ButtonV2"; -import ConfirmDialog from "@/components/Common/ConfirmDialog"; import Loading from "@/components/Common/Loading"; import Page from "@/components/Common/Page"; import { ConsultationModel } from "@/components/Facility/models"; @@ -22,16 +21,13 @@ import { SHIFTING_CHOICES_WARTIME, } from "@/common/constants"; -import * as Notification from "@/Utils/Notifications"; import routes from "@/Utils/request/api"; -import request from "@/Utils/request/request"; import useQuery from "@/Utils/request/useQuery"; import { formatDateTime, formatName, formatPatientAge } from "@/Utils/utils"; export default function ShiftDetails(props: { id: string }) { const [isPrintMode, setIsPrintMode] = useState(false); const [isCopied, setIsCopied] = useState(false); - const [openDeleteShiftDialog, setOpenDeleteShiftDialog] = useState(false); const { t } = useTranslation(); const shiftStatusOptions = careConfig.wartimeShifting @@ -41,26 +37,6 @@ export default function ShiftDetails(props: { id: string }) { const { data, loading } = useQuery(routes.getShiftDetails, { pathParams: { id: props.id }, }); - - const handleShiftDelete = async () => { - setOpenDeleteShiftDialog(true); - - const { res, data } = await request(routes.deleteShiftRecord, { - pathParams: { id: props.id }, - }); - if (res?.status == 204) { - Notification.Success({ - msg: t("shifting_deleted"), - }); - } else { - Notification.Error({ - msg: t("error_deleting_shifting") + (data?.detail || ""), - }); - } - - navigate("/shifting"); - }; - const showCopyToclipBoard = (data: any) => { return ( @@ -732,25 +708,6 @@ export default function ShiftDetails(props: { id: string }) { time={data?.modified_date} />
- -
-
- setOpenDeleteShiftDialog(true)} - > - {t("delete_record")} - - setOpenDeleteShiftDialog(false)} - onConfirm={handleShiftDelete} - /> -
-
From 8a9078d16b6dd5189c7919bb3fbd83ea7aa53a18 Mon Sep 17 00:00:00 2001 From: Sulochan Khadka <122200551+Sulochan-khadka@users.noreply.github.com> Date: Mon, 11 Nov 2024 17:24:51 +0530 Subject: [PATCH 9/9] Dosage heading is misaligned in the prescription module #8645 (#8751) --- .../AdministrationTable.tsx | 22 +++++------ .../AdministrationTableRow.tsx | 39 ++++++++++++------- 2 files changed, 35 insertions(+), 26 deletions(-) diff --git a/src/components/Medicine/MedicineAdministrationSheet/AdministrationTable.tsx b/src/components/Medicine/MedicineAdministrationSheet/AdministrationTable.tsx index 86db7353832..b1eac32409d 100644 --- a/src/components/Medicine/MedicineAdministrationSheet/AdministrationTable.tsx +++ b/src/components/Medicine/MedicineAdministrationSheet/AdministrationTable.tsx @@ -31,17 +31,17 @@ export default function MedicineAdministrationTable({ -
- {t("medicine")} - -

Dosage &

-

- {prescriptions[0]?.dosage_type !== "PRN" - ? "Frequency" - : "Indicator"} -

-
-
+ {t("medicine")} + + + +

{t("dosage")} &

+

+ {prescriptions[0]?.dosage_type !== "PRN" + ? t("frequency") + : t("indicator")} +

+
diff --git a/src/components/Medicine/MedicineAdministrationSheet/AdministrationTableRow.tsx b/src/components/Medicine/MedicineAdministrationSheet/AdministrationTableRow.tsx index d08506d7fbe..7544f405b08 100644 --- a/src/components/Medicine/MedicineAdministrationSheet/AdministrationTableRow.tsx +++ b/src/components/Medicine/MedicineAdministrationSheet/AdministrationTableRow.tsx @@ -59,6 +59,25 @@ export default function MedicineAdministrationTableRow({ key: `${prescription.last_administration?.administered_date}`, }, ); + const DosageFrequencyInfo = () => ( +
+
+ {prescription.dosage_type !== "TITRATED" ? ( +

{prescription.base_dosage}

+ ) : ( +

+ {prescription.base_dosage} - {prescription.target_dosage} +

+ )} + +

+ {prescription.dosage_type !== "PRN" + ? t("PRESCRIPTION_FREQUENCY_" + prescription.frequency) + : prescription.indicator} +

+
+
+ ); return ( <> @@ -216,24 +235,14 @@ export default function MedicineAdministrationTableRow({ {prescription.medicine_object?.generic}
- -
- {prescription.dosage_type !== "TITRATED" ? ( -

{prescription.base_dosage}

- ) : ( -

- {prescription.base_dosage} - {prescription.target_dosage} -

- )} - -

- {prescription.dosage_type !== "PRN" - ? t("PRESCRIPTION_FREQUENCY_" + prescription.frequency) - : prescription.indicator} -

+
+
+ + +