diff --git a/cypress/e2e/assets_spec/asset_homepage.cy.ts b/cypress/e2e/assets_spec/asset_homepage.cy.ts index 388f19424a4..5710df08e83 100644 --- a/cypress/e2e/assets_spec/asset_homepage.cy.ts +++ b/cypress/e2e/assets_spec/asset_homepage.cy.ts @@ -63,7 +63,6 @@ describe("Asset Tab", () => { it("Filter Asset", () => { assetFilters.filterAssets( "Dummy Facility 40", - "INTERNAL", "ACTIVE", "ONVIF Camera", "Camera Loc" @@ -71,7 +70,6 @@ describe("Asset Tab", () => { assetFilters.clickadvancefilter(); assetFilters.clickslideoverbackbutton(); // to verify the back button doesn't clear applied filters assetFilters.assertFacilityText("Dummy Facility 40"); - assetFilters.assertAssetTypeText("INTERNAL"); assetFilters.assertAssetClassText("ONVIF"); assetFilters.assertStatusText("ACTIVE"); assetFilters.assertLocationText("Camera Loc"); diff --git a/cypress/e2e/assets_spec/assets_creation.cy.ts b/cypress/e2e/assets_spec/assets_creation.cy.ts index 63f8bee50c8..f8edc2bb172 100644 --- a/cypress/e2e/assets_spec/assets_creation.cy.ts +++ b/cypress/e2e/assets_spec/assets_creation.cy.ts @@ -29,7 +29,6 @@ describe("Asset", () => { assetPage.clickCreateAsset(); assetPage.verifyEmptyAssetNameError(); - assetPage.verifyEmptyAssetTypeError(); assetPage.verifyEmptyLocationError(); assetPage.verifyEmptyStatusError(); assetPage.verifyEmptyPhoneError(); @@ -41,7 +40,6 @@ describe("Asset", () => { assetPage.createAsset(); assetPage.selectFacility("Dummy Facility 40"); assetPage.selectLocation("Camera Loc"); - assetPage.selectAssetType("Internal"); assetPage.selectAssetClass("ONVIF Camera"); const qr_id_1 = uuidv4(); @@ -68,7 +66,6 @@ describe("Asset", () => { const qr_id_2 = uuidv4(); assetPage.selectLocation("Camera Loc"); - assetPage.selectAssetType("Internal"); assetPage.selectAssetClass("ONVIF Camera"); assetPage.enterAssetDetails( "New Test Asset 2", @@ -141,7 +138,6 @@ describe("Asset", () => { assetPage.createAsset(); assetPage.selectFacility("Dummy Facility 40"); assetPage.selectLocation("Camera Loc"); - assetPage.selectAssetType("Internal"); assetPage.selectAssetClass("HL7 Vitals Monitor"); const qr_id_1 = uuidv4(); diff --git a/cypress/e2e/facility_spec/facility_manage.cy.ts b/cypress/e2e/facility_spec/facility_manage.cy.ts index e47af2f6984..612a95001d6 100644 --- a/cypress/e2e/facility_spec/facility_manage.cy.ts +++ b/cypress/e2e/facility_spec/facility_manage.cy.ts @@ -13,11 +13,11 @@ describe("Facility Manage Functions", () => { const facilityUpdatedMiddleware = "updated.coronasafe.live"; const facilityMiddlewareSuccessfullNotification = "Facility updated successfully"; - const facilityHrfidUpdateButton = "Link Health Facility"; - const facilityHrfidSuccessfullNotification = + const facilityHfridUpdateButton = "Link Health Facility"; + const facilityHfridToastNotificationText = /Health Facility config updated successfully|Health ID registration failed/; - const facilityHrfId = uuidv4(); - const facilityUpdatedHrfId = uuidv4(); + const facilityHfrId = "IN180000018"; + const facilityUpdatedHfrId = uuidv4(); const doctorCapacity = "5"; const doctorModifiedCapacity = "7"; const totalCapacity = "100"; @@ -80,29 +80,29 @@ describe("Facility Manage Functions", () => { facilityPage.clickManageFacilityDropdown(); facilityManage.clickFacilityConfigureButton(); // verify mandatory field error message - facilityManage.clearHrfId(); - facilityManage.clickButtonWithText(facilityHrfidUpdateButton); + facilityManage.clearHfrId(); + facilityManage.clickButtonWithText(facilityHfridUpdateButton); facilityManage.checkErrorMessageVisibility( "Health Facility Id is required" ); // add facility health ID and verify notification - facilityManage.typeHrfId(facilityHrfId); - facilityManage.clickButtonWithText(facilityHrfidUpdateButton); + facilityManage.typeHfrId(facilityHfrId); + facilityManage.clickButtonWithText(facilityHfridUpdateButton); facilityManage.verifySuccessMessageVisibilityAndContent( - facilityHrfidSuccessfullNotification + facilityHfridToastNotificationText ); // update the existing middleware facilityPage.clickManageFacilityDropdown(); facilityManage.clickFacilityConfigureButton(); - facilityManage.typeHrfId(facilityUpdatedHrfId); - facilityManage.clickButtonWithText(facilityHrfidUpdateButton); + facilityManage.typeHfrId(facilityUpdatedHfrId); + facilityManage.clickButtonWithText(facilityHfridUpdateButton); facilityManage.verifySuccessMessageVisibilityAndContent( - facilityHrfidSuccessfullNotification + facilityHfridToastNotificationText ); // verify its reflection facilityPage.clickManageFacilityDropdown(); facilityManage.clickFacilityConfigureButton(); - facilityManage.verifyHrfIdValue(facilityUpdatedHrfId); + facilityManage.verifyHfrIdValue(facilityUpdatedHfrId); }); it("Modify doctor capacity in Facility detail page", () => { diff --git a/cypress/e2e/sample_test_spec/filter.cy.ts b/cypress/e2e/sample_test_spec/filter.cy.ts index a015d1ba7c5..df934c641bb 100644 --- a/cypress/e2e/sample_test_spec/filter.cy.ts +++ b/cypress/e2e/sample_test_spec/filter.cy.ts @@ -20,13 +20,6 @@ describe("Sample Filter", () => { .click(); }); - it("Filter by Asset Type", () => { - cy.get("#result").click(); - cy.get("li[role='option']") - .contains(/^POSITIVE$/) - .click(); - }); - it("Filter by sample type", () => { cy.get("#sample_type").click(); cy.get("li[role='option']") diff --git a/cypress/e2e/users_spec/user_creation.cy.ts b/cypress/e2e/users_spec/user_creation.cy.ts index 72d2e7f15f4..b8e0dfe63eb 100644 --- a/cypress/e2e/users_spec/user_creation.cy.ts +++ b/cypress/e2e/users_spec/user_creation.cy.ts @@ -25,7 +25,7 @@ describe("User Creation", () => { } return result; }; - const username = makeid(25); + const username = makeid(8); const alreadylinkedusersviews = [ "devdoctor", "devstaff2", diff --git a/cypress/pageobject/Asset/AssetCreation.ts b/cypress/pageobject/Asset/AssetCreation.ts index 6932b5ed15e..8f611e97d92 100644 --- a/cypress/pageobject/Asset/AssetCreation.ts +++ b/cypress/pageobject/Asset/AssetCreation.ts @@ -24,14 +24,6 @@ export class AssetPage { }); } - selectAssetType(assetType: string) { - cy.get("[data-testid=asset-type-input] button") - .click() - .then(() => { - cy.get("[role='option']").contains(assetType).click(); - }); - } - selectAssetClass(assetClass: string) { cy.get("[data-testid=asset-class-input] button") .click() @@ -205,13 +197,6 @@ export class AssetPage { ); } - verifyEmptyAssetTypeError() { - cy.get("[data-testid=asset-type-input] span").should( - "contain", - "Select an asset type" - ); - } - verifyEmptyStatusError() { cy.get("[data-testid=asset-working-status-input] span").should( "contain", diff --git a/cypress/pageobject/Asset/AssetFilters.ts b/cypress/pageobject/Asset/AssetFilters.ts index 5ded59f4f63..33363f2d161 100644 --- a/cypress/pageobject/Asset/AssetFilters.ts +++ b/cypress/pageobject/Asset/AssetFilters.ts @@ -1,7 +1,6 @@ export class AssetFilters { filterAssets( facilityName: string, - assetType: string, assetStatus: string, assetClass: string, assetLocation: string @@ -13,11 +12,6 @@ export class AssetFilters { .then(() => { cy.get("[role='option']").contains(facilityName).click(); }); - cy.get("#asset-type") - .click() - .then(() => { - cy.get("[role='option']").contains(assetType).click(); - }); cy.get("#asset-status") .click() .then(() => { @@ -65,9 +59,6 @@ export class AssetFilters { 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); } diff --git a/cypress/pageobject/Facility/FacilityManage.ts b/cypress/pageobject/Facility/FacilityManage.ts index 1b61fbe5f05..df379a53012 100644 --- a/cypress/pageobject/Facility/FacilityManage.ts +++ b/cypress/pageobject/Facility/FacilityManage.ts @@ -63,11 +63,11 @@ class FacilityManage { cy.get("#middleware_address").click().clear().click().type(address); } - clearHrfId() { + clearHfrId() { cy.get("#hf_id").click().clear(); } - typeHrfId(address) { + typeHfrId(address) { cy.get("#hf_id").click().clear().click().type(address); } @@ -79,7 +79,7 @@ class FacilityManage { cy.get("#middleware_address").should("have.value", expectedValue); } - verifyHrfIdValue(expectedValue) { + verifyHfrIdValue(expectedValue) { cy.get("#hf_id").should("have.value", expectedValue); } diff --git a/public/config.json b/public/config.json index 69d898af544..d02bd135cfe 100644 --- a/public/config.json +++ b/public/config.json @@ -22,5 +22,6 @@ "sample_format_asset_import": "https://spreadsheets.google.com/feeds/download/spreadsheets/Export?key=11JaEhNHdyCHth4YQs_44YaRlP77Rrqe81VSEfg1glko&exportFormat=xlsx", "sample_format_external_result_import": "/External-Results-Template.csv", "enable_abdm": true, - "enable_hcx": false + "enable_hcx": false, + "min_encounter_date": "2020-01-01" } diff --git a/src/CAREUI/display/Timeline.tsx b/src/CAREUI/display/Timeline.tsx index 276c437056c..02a0b81c373 100644 --- a/src/CAREUI/display/Timeline.tsx +++ b/src/CAREUI/display/Timeline.tsx @@ -14,7 +14,7 @@ export interface TimelineEvent { } interface TimelineProps { - className: string; + className?: string; children: React.ReactNode | React.ReactNode[]; name: string; } @@ -121,7 +121,7 @@ interface TimelineNodeTitleProps { export const TimelineNodeTitle = (props: TimelineNodeTitleProps) => { return ( <> -
+
)} - o} - optionValue={(o) => o} - value={asset_type} - onChange={({ value }) => setAssetType(value)} - /> - { {asset?.description}
- {asset?.asset_type === "INTERNAL" ? ( - - ) : ( - - )} {asset?.status === "ACTIVE" ? ( ) : ( @@ -503,7 +498,12 @@ const AssetManage = (props: AssetManageProps) => {
{asset?.id && asset?.asset_class && - asset?.asset_class != AssetClass.NONE && } + asset?.asset_class != AssetClass.NONE && ( + + )}
Service History
{ const [isScannerActive, setIsScannerActive] = useState(false); const [totalCount, setTotalCount] = useState(0); const [facility, setFacility] = useState(); - const [asset_type, setAssetType] = useState(); const [status, setStatus] = useState(); const [asset_class, setAssetClass] = useState(); const [importAssetModalOpen, setImportAssetModalOpen] = useState(false); @@ -60,7 +59,6 @@ const AssetsList = () => { offset: (qParams.page ? qParams.page - 1 : 0) * resultsPerPage, search_text: qParams.search || "", facility: qParams.facility || "", - asset_type: qParams.asset_type || "", asset_class: qParams.asset_class || "", location: qParams.facility ? qParams.location || "" : "", status: qParams.status || "", @@ -91,10 +89,6 @@ const AssetsList = () => { prefetch: !!qParams.facility, }); - useEffect(() => { - setAssetType(qParams.asset_type); - }, [qParams.asset_type]); - useEffect(() => { setStatus(qParams.status); }, [qParams.status]); @@ -199,7 +193,7 @@ const AssetsList = () => { >

@@ -240,6 +234,13 @@ const AssetsList = () => { )} {warrantyAmcValidityChip(asset.warranty_amc_end_of_validity)} + {asset?.latest_status === "Down" && ( + + )}{" "}

@@ -380,7 +381,6 @@ const AssetsList = () => { qParams.facility && facilityObject?.name ), badge("Name/Serial No./QR ID", "search"), - value("Asset Type", "asset_type", asset_type ?? ""), value("Asset Class", "asset_class", asset_class ?? ""), value("Status", "status", status?.replace(/_/g, " ") ?? ""), value( diff --git a/src/Components/Common/Uptime.tsx b/src/Components/Common/Uptime.tsx index e3c7dd7a439..e7272e2e1f4 100644 --- a/src/Components/Common/Uptime.tsx +++ b/src/Components/Common/Uptime.tsx @@ -1,10 +1,10 @@ import { Popover } from "@headlessui/react"; import { useEffect, useRef, useState } from "react"; -import { AssetStatus, AssetUptimeRecord } from "../Assets/AssetTypes"; +import { AssetStatus, AvailabilityRecord } from "../Assets/AssetTypes"; import { classNames } from "../../Utils/utils"; import dayjs from "../../Utils/dayjs"; import useQuery from "../../Utils/request/useQuery.js"; -import routes from "../../Redux/api.js"; +import { PaginatedResponse, QueryRoute } from "../../Utils/request/types"; const STATUS_COLORS = { Operational: "bg-green-500", @@ -37,7 +37,7 @@ function UptimeInfo({ records, date, }: { - records: AssetUptimeRecord[]; + records: AvailabilityRecord[]; date: string; }) { const incidents = @@ -66,7 +66,7 @@ function UptimeInfo({ let endTimestamp; let ongoing = false; - if (prevIncident?.id) { + if (prevIncident?.linked_id) { endTimestamp = dayjs(prevIncident.timestamp); } else if (dayjs(incident.timestamp).isSame(now, "day")) { endTimestamp = dayjs(); @@ -141,7 +141,7 @@ function UptimeInfoPopover({ date, numDays, }: { - records: AssetUptimeRecord[]; + records: AvailabilityRecord[]; day: number; date: string; numDays: number; @@ -165,12 +165,17 @@ function UptimeInfoPopover({ ); } -export default function Uptime(props: { assetId: string }) { +export default function Uptime( + props: Readonly<{ + route: QueryRoute>; + params?: Record; + }> +) { const [summary, setSummary] = useState<{ - [key: number]: AssetUptimeRecord[]; + [key: number]: AvailabilityRecord[]; }>([]); - const { data, loading } = useQuery(routes.listAssetAvailability, { - query: { external_id: props.assetId }, + const { data, loading } = useQuery(props.route, { + pathParams: props.params, onResponse: ({ data }) => setUptimeRecord(data?.results.reverse() ?? []), }); const availabilityData = data?.results ?? []; @@ -186,8 +191,8 @@ export default function Uptime(props: { assetId: string }) { setNumDays(Math.min(newNumDays, 100)); }; - const setUptimeRecord = (records: AssetUptimeRecord[]): void => { - const recordsByDayBefore: { [key: number]: AssetUptimeRecord[] } = {}; + const setUptimeRecord = (records: AvailabilityRecord[]): void => { + const recordsByDayBefore: { [key: number]: AvailabilityRecord[] } = {}; records.forEach((record) => { const timestamp = dayjs(record.timestamp).startOf("day"); @@ -207,10 +212,8 @@ export default function Uptime(props: { assetId: string }) { recordsByDayBefore[i] = []; if (statusToCarryOver) { recordsByDayBefore[i].push({ - id: "", - asset: { id: "", name: "" }, - created_date: "", - modified_date: "", + linked_id: "", + linked_model: "", status: statusToCarryOver, timestamp: dayjs() .subtract(i, "days") @@ -225,10 +228,8 @@ export default function Uptime(props: { assetId: string }) { ).length === 0 ) { recordsByDayBefore[i].unshift({ - id: "", - asset: { id: "", name: "" }, - created_date: "", - modified_date: "", + linked_id: "", + linked_model: "", status: statusToCarryOver, timestamp: dayjs() .subtract(i, "days") @@ -284,7 +285,7 @@ export default function Uptime(props: { assetId: string }) { const statusColors: (typeof STATUS_COLORS)[keyof typeof STATUS_COLORS][] = []; let dayUptimeScore = 0; - const recordsInPeriodCache: { [key: number]: AssetUptimeRecord[] } = {}; + const recordsInPeriodCache: { [key: number]: AvailabilityRecord[] } = {}; for (let i = 0; i < 3; i++) { const start = i * 8; const end = (i + 1) * 8; diff --git a/src/Components/Facility/AssetCreate.tsx b/src/Components/Facility/AssetCreate.tsx index 784174cd2f7..c6f26db5bba 100644 --- a/src/Components/Facility/AssetCreate.tsx +++ b/src/Components/Facility/AssetCreate.tsx @@ -40,7 +40,6 @@ const Loading = lazy(() => import("../Common/Loading")); const formErrorKeys = [ "name", - "asset_type", "asset_class", "description", "is_working", @@ -103,12 +102,10 @@ const AssetCreate = (props: AssetProps) => { const { goBack } = useAppHistory(); const { facilityId, assetId } = props; - let assetTypeInitial: AssetType; let assetClassInitial: AssetClass; const [state, dispatch] = useReducer(asset_create_reducer, initialState); const [name, setName] = useState(""); - const [asset_type, setAssetType] = useState(); const [asset_class, setAssetClass] = useState(); const [not_working_reason, setNotWorkingReason] = useState(""); const [description, setDescription] = useState(""); @@ -177,7 +174,6 @@ const AssetCreate = (props: AssetProps) => { setName(asset.name); setDescription(asset.description); setLocation(asset.location_object.id!); - setAssetType(asset.asset_type); setAssetClass(asset.asset_class); setIsWorking(String(asset.is_working)); setNotWorkingReason(asset.not_working_reason); @@ -219,12 +215,6 @@ const AssetCreate = (props: AssetProps) => { invalidForm = true; } return; - case "asset_type": - if (!asset_type || asset_type == "NONE") { - errors[field] = "Select an asset type"; - invalidForm = true; - } - return; case "support_phone": { if (!support_phone) { errors[field] = "Field is required"; @@ -282,7 +272,6 @@ const AssetCreate = (props: AssetProps) => { setName(""); setDescription(""); setLocation(""); - setAssetType(assetTypeInitial); setAssetClass(assetClassInitial); setIsWorking(undefined); setNotWorkingReason(""); @@ -307,7 +296,7 @@ const AssetCreate = (props: AssetProps) => { setIsLoading(true); const data: any = { name: name, - asset_type: asset_type, + asset_type: AssetType.INTERNAL, asset_class: asset_class || "", description: description, is_working: is_working, @@ -393,7 +382,7 @@ const AssetCreate = (props: AssetProps) => { >

-
+

@@ -500,7 +489,7 @@ const AssetCreate = (props: AssetProps) => { ); })}
-
+
handleSubmit(e, false)} @@ -547,68 +536,34 @@ const AssetCreate = (props: AssetProps) => { errors={state.errors.location} />
- {/* Asset Type */} -
-
- title} - optionDescription={({ description }) => description} - optionValue={({ value }) => value} - onChange={({ value }) => setAssetType(value)} - error={state.errors.asset_type} - /> -
- {/* Asset Class */} -
- title} - optionValue={({ value }) => value} - onChange={({ value }) => setAssetClass(value)} - error={state.errors.asset_class} - /> -
+ {/* Asset Class */} +
+ title} + optionValue={({ value }) => value} + onChange={({ value }) => setAssetClass(value)} + error={state.errors.asset_class} + />
{/* Description */}
{
-
+
{
-
-
- + {!!consultationData.diagnoses?.length && ( +
+
+ +
-
+ )}
diff --git a/src/Components/Facility/ConsultationDoctorNotes/index.tsx b/src/Components/Facility/ConsultationDoctorNotes/index.tsx index 8e39ee04e4e..cf0f78a39bb 100644 --- a/src/Components/Facility/ConsultationDoctorNotes/index.tsx +++ b/src/Components/Facility/ConsultationDoctorNotes/index.tsx @@ -1,7 +1,6 @@ import { useState } from "react"; import * as Notification from "../../../Utils/Notifications.js"; import Page from "../../Common/components/Page"; -import TextFormField from "../../Form/FormFields/TextFormField"; import ButtonV2 from "../../Common/components/ButtonV2"; import CareIcon from "../../../CAREUI/icons/CareIcon"; import { NonReadOnlyUsers } from "../../../Utils/AuthorizeFor"; @@ -13,6 +12,7 @@ import request from "../../../Utils/request/request.js"; import useQuery from "../../../Utils/request/useQuery.js"; import useKeyboardShortcut from "use-keyboard-shortcut"; import { isAppleDevice } from "../../../Utils/utils.js"; +import AutoExpandingTextInputFormField from "../../Form/FormFields/AutoExpandingTextInputFormField.js"; interface ConsultationDoctorNotesProps { patientId: string; @@ -34,6 +34,8 @@ const ConsultationDoctorNotes = (props: ConsultationDoctorNotesProps) => { notes: [], cPage: 1, totalPages: 1, + facilityId: facilityId, + patientId: patientId, }; const [state, setState] = useState(initialData); @@ -113,19 +115,19 @@ const ConsultationDoctorNotes = (props: ConsultationDoctorNotesProps) => {
- setNoteField(e.value)} className="grow" - type="text" errorClassName="hidden" placeholder="Type your Note" disabled={!patientActive} diff --git a/src/Components/Facility/ConsultationForm.tsx b/src/Components/Facility/ConsultationForm.tsx index 21329784197..72c6630976e 100644 --- a/src/Components/Facility/ConsultationForm.tsx +++ b/src/Components/Facility/ConsultationForm.tsx @@ -263,6 +263,8 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { const [bedStatusVisible, bedStatusRef] = useVisibility(-300); const [disabledFields, setDisabledFields] = useState([]); + const { min_encounter_date } = useConfig(); + const sections = { "Consultation Details": { iconClass: "care-l-medkit", @@ -504,8 +506,13 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { errors[field] = "Field is required"; invalidForm = true; } - if (dayjs(state.form.encounter_date).isBefore(dayjs("2000-01-01"))) { - errors[field] = "Admission date cannot be before 01/01/2000"; + if ( + min_encounter_date && + dayjs(state.form.encounter_date).isBefore(dayjs(min_encounter_date)) + ) { + errors[ + field + ] = `Admission date cannot be before ${min_encounter_date}`; invalidForm = true; } return; @@ -940,7 +947,7 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { ); })}
-
+
{ "YYYY-MM-DDTHH:mm" )} max={dayjs().format("YYYY-MM-DDTHH:mm")} + min={ + min_encounter_date + ? dayjs(min_encounter_date).format("YYYY-MM-DDTHH:mm") + : undefined + } />
diff --git a/src/Components/Facility/Consultations/DailyRoundsList.tsx b/src/Components/Facility/Consultations/DailyRoundsList.tsx index 828a41c8c24..0d569eade41 100644 --- a/src/Components/Facility/Consultations/DailyRoundsList.tsx +++ b/src/Components/Facility/Consultations/DailyRoundsList.tsx @@ -10,7 +10,10 @@ import PageTitle from "../../Common/PageTitle"; import DailyRoundsFilter from "./DailyRoundsFilter"; import { ConsultationModel } from "../models"; import { useSlugs } from "../../../Common/hooks/useSlug"; -import { TimelineNode } from "../../../CAREUI/display/Timeline"; + +import Timeline, { TimelineNode } from "../../../CAREUI/display/Timeline"; +import { useState } from "react"; +import { QueryParams } from "../../../Utils/request/types"; interface Props { consultation: ConsultationModel; @@ -19,6 +22,7 @@ interface Props { export default function DailyRoundsList({ consultation }: Props) { const [consultationId] = useSlugs("consultation"); const { t } = useTranslation(); + const [query, setQuery] = useState(); const consultationUrl = `/facility/${consultation.facility}/patient/${consultation.patient}/consultation/${consultation.id}`; @@ -26,12 +30,17 @@ export default function DailyRoundsList({ consultation }: Props) { - {({ refetch }) => ( + {() => ( <>
- refetch({ query })} /> + { + setQuery(query); + }} + />
@@ -44,69 +53,71 @@ export default function DailyRoundsList({ consultation }: Props) { - className="flex grow flex-col gap-3"> - {(item, items) => { - if (item.rounds_type === "AUTOMATED") { + + className="flex grow flex-col gap-3 rounded-lg bg-white p-2 shadow"> + {(item, items) => { + if (item.rounds_type === "AUTOMATED") { + return ( + + + + ); + } + + const itemUrl = ["NORMAL", "TELEMEDICINE"].includes( + item.rounds_type as string + ) + ? `${consultationUrl}/daily-rounds/${item.id}` + : `${consultationUrl}/daily_rounds/${item.id}`; + return ( - navigate(itemUrl)} + onUpdateLog={() => navigate(`${itemUrl}/update`)} /> ); - } - - const itemUrl = ["NORMAL", "TELEMEDICINE"].includes( - item.rounds_type - ) - ? `${consultationUrl}/daily-rounds/${item.id}` - : `${consultationUrl}/daily_rounds/${item.id}`; - - return ( - - navigate(itemUrl)} - onUpdateLog={() => navigate(`${itemUrl}/update`)} - /> - - ); - }} - + }} + +
diff --git a/src/Components/Facility/DoctorNote.tsx b/src/Components/Facility/DoctorNote.tsx index 85703a1e3d8..1fe28ba38d0 100644 --- a/src/Components/Facility/DoctorNote.tsx +++ b/src/Components/Facility/DoctorNote.tsx @@ -5,11 +5,13 @@ import { PatientNoteStateType } from "./models"; interface DoctorNoteProps { state: PatientNoteStateType; + setReload: any; handleNext: () => void; + disableEdit?: boolean; } const DoctorNote = (props: DoctorNoteProps) => { - const { state, handleNext } = props; + const { state, handleNext, setReload, disableEdit } = props; return (
{ scrollableTarget="patient-notes-list" > {state.notes.map((note: any) => ( - + ))} ) : ( diff --git a/src/Components/Facility/Investigations/Reports/ReportTable.tsx b/src/Components/Facility/Investigations/Reports/ReportTable.tsx index 1f20ec94180..c15eec4e33b 100644 --- a/src/Components/Facility/Investigations/Reports/ReportTable.tsx +++ b/src/Components/Facility/Investigations/Reports/ReportTable.tsx @@ -124,9 +124,17 @@ const ReportTable: FC = ({ - {formatDateTime(session.session_created_date)} +
+ {formatDateTime(session.session_created_date)} + + {session.facility_name} + +
))} { const sessions = _.chain(data) - .map((value) => value.session_object) + .map((value) => { + return { + ...value.session_object, + facility_name: value.consultation_object?.facility_name, + facility_id: value.consultation_object?.facility, + }; + }) .uniqBy("session_external_id") .orderBy("session_created_date", "desc") .value(); @@ -28,7 +34,6 @@ export const transformData = _.memoize((data: InvestigationResponse) => { } }); const { - consultation, group, group_object, id, @@ -40,7 +45,6 @@ export const transformData = _.memoize((data: InvestigationResponse) => { } = value[0]; return { - consultation, group, group_object, id, diff --git a/src/Components/Facility/PatientConsultationNotesList.tsx b/src/Components/Facility/PatientConsultationNotesList.tsx index f38de51110b..2b9df8c3902 100644 --- a/src/Components/Facility/PatientConsultationNotesList.tsx +++ b/src/Components/Facility/PatientConsultationNotesList.tsx @@ -10,16 +10,15 @@ import request from "../../Utils/request/request"; interface PatientNotesProps { state: PatientNoteStateType; setState: any; - patientId: string; - facilityId: string; reload?: boolean; setReload?: any; + disableEdit?: boolean; } const pageSize = RESULTS_PER_PAGE_LIMIT; const PatientConsultationNotesList = (props: PatientNotesProps) => { - const { state, setState, reload, setReload } = props; + const { state, setState, reload, setReload, disableEdit } = props; const consultationId = useSlug("consultation") ?? ""; const [isLoading, setIsLoading] = useState(true); @@ -28,7 +27,7 @@ const PatientConsultationNotesList = (props: PatientNotesProps) => { setIsLoading(true); const { data }: any = await request(routes.getPatientNotes, { pathParams: { - patientId: props.patientId, + patientId: props.state.patientId, }, query: { consultation: consultationId, @@ -81,7 +80,14 @@ const PatientConsultationNotesList = (props: PatientNotesProps) => { ); } - return ; + return ( + + ); }; export default PatientConsultationNotesList; diff --git a/src/Components/Facility/PatientNoteCard.tsx b/src/Components/Facility/PatientNoteCard.tsx index 2f07702504a..af550a117f1 100644 --- a/src/Components/Facility/PatientNoteCard.tsx +++ b/src/Components/Facility/PatientNoteCard.tsx @@ -1,36 +1,246 @@ import { relativeDate, formatDateTime, classNames } from "../../Utils/utils"; import { USER_TYPES_MAP } from "../../Common/constants"; -import { PatientNotesModel } from "./models"; +import { + PatientNoteStateType, + PatientNotesEditModel, + PatientNotesModel, +} from "./models"; +import ButtonV2 from "../Common/components/ButtonV2"; +import CareIcon from "../../CAREUI/icons/CareIcon"; +import { useState } from "react"; +import { Error, Success } from "../../Utils/Notifications"; +import request from "../../Utils/request/request"; +import routes from "../../Redux/api"; +import DialogModal from "../Common/Dialog"; +import { t } from "i18next"; +import dayjs from "dayjs"; +import Spinner from "../Common/Spinner"; +import useAuthUser from "../../Common/hooks/useAuthUser"; + +const PatientNoteCard = ({ + state, + note, + setReload, + disableEdit, +}: { + state: PatientNoteStateType; + note: PatientNotesModel; + setReload: any; + disableEdit?: boolean; +}) => { + const [isEditing, setIsEditing] = useState(false); + const [noteField, setNoteField] = useState(note.note); + const [showEditHistory, setShowEditHistory] = useState(false); + const [editHistory, setEditHistory] = useState([]); + const authUser = useAuthUser(); + + const fetchEditHistory = async () => { + const { res, data } = await request(routes.getPatientNoteEditHistory, { + pathParams: { patientId: state.patientId, noteId: note.id }, + }); + if (res?.status === 200) { + setEditHistory(data?.results ?? []); + } + }; + + const onUpdateNote = async () => { + if (noteField === note.note) { + setIsEditing(false); + return; + } + const payload = { + note: noteField, + }; + if (!/\S+/.test(noteField)) { + Error({ + msg: "Note Should Contain At Least 1 Character", + }); + return; + } + + const { res } = await request(routes.updatePatientNote, { + pathParams: { patientId: state.patientId, noteId: note.id }, + body: payload, + }); + if (res?.status === 200) { + Success({ msg: "Note updated successfully" }); + setIsEditing(false); + setReload(true); + } + }; -const PatientNoteCard = ({ note }: { note: PatientNotesModel }) => { return ( -
-
- - {note.created_by_object?.first_name || "Unknown"}{" "} - {note.created_by_object?.last_name} - - {note.user_type && ( - - {`(${USER_TYPES_MAP[note.user_type]})`} - + <> + {" "} +
- {note.note} -
-
- - {formatDateTime(note.created_date)} - - {relativeDate(note.created_date)} + > +
+
+
+ + {note.created_by_object?.first_name || "Unknown"}{" "} + {note.created_by_object?.last_name} + + {note.user_type && ( + + {`(${USER_TYPES_MAP[note.user_type]})`} + + )} +
+
+
+ + {formatDateTime(note.created_date)} + + Created {relativeDate(note.created_date, true)} +
+
+ { + // If last edited date is same as created date, then it is not edited + !dayjs(note.last_edited_date).isSame( + note.created_date, + "second" + ) && ( +
+
{ + fetchEditHistory(); + setShowEditHistory(true); + }} + > +
+ + {formatDateTime(note.last_edited_date)} + + Edited {relativeDate(note.last_edited_date, true)} +
+ +
+
+ ) + } +
+ + {!disableEdit && + note.created_by_object.id === authUser.id && + !isEditing && ( + { + setIsEditing(true); + }} + > + + + )}
+ { +
+ {isEditing ? ( +
+ +
+ { + setIsEditing(false); + setNoteField(note.note); + }} + id="cancel-update-note-button" + > + + Cancel + + + + Update Note + +
+
+ ) : ( +
{noteField}
+ )} +
+ }
-
+ {showEditHistory && ( + setShowEditHistory(false)} + title={t("edit_history")} + > +
+
+

+ Edit History for note + {note.id} +

+
+
+ {editHistory.length === 0 && ( +
+ +
+ )} + {editHistory?.map((edit, index) => { + const isLast = index === editHistory.length - 1; + return ( +
+
+
+

+ {isLast ? "Created" : "Edited"} On +

+

+ {formatDateTime(edit.edited_date)} +

+
+
+
+

Note

+

{edit.note}

+
+
+ ); + })} +
+
+ { + setShowEditHistory(false); + }} + > + {t("close")} + +
+
+
+ )} + ); }; diff --git a/src/Components/Facility/PatientNotesList.tsx b/src/Components/Facility/PatientNotesList.tsx index 96f9dcad871..a36762072b9 100644 --- a/src/Components/Facility/PatientNotesList.tsx +++ b/src/Components/Facility/PatientNotesList.tsx @@ -74,7 +74,9 @@ const PatientNotesList = (props: PatientNotesProps) => { ); } - return ; + return ( + + ); }; export default PatientNotesList; diff --git a/src/Components/Facility/PatientNotesSlideover.tsx b/src/Components/Facility/PatientNotesSlideover.tsx index 5ecf30cfcdf..ae4f08c2ee6 100644 --- a/src/Components/Facility/PatientNotesSlideover.tsx +++ b/src/Components/Facility/PatientNotesSlideover.tsx @@ -3,7 +3,6 @@ import * as Notification from "../../Utils/Notifications.js"; import { NonReadOnlyUsers } from "../../Utils/AuthorizeFor"; import CareIcon from "../../CAREUI/icons/CareIcon"; import { classNames, isAppleDevice } from "../../Utils/utils"; -import TextFormField from "../Form/FormFields/TextFormField"; import ButtonV2 from "../Common/components/ButtonV2"; import { make as Link } from "../Common/components/Link.bs"; import { useMessageListener } from "../../Common/hooks/useMessageListener"; @@ -12,6 +11,7 @@ import request from "../../Utils/request/request"; import routes from "../../Redux/api"; import { PatientNoteStateType } from "./models"; import useKeyboardShortcut from "use-keyboard-shortcut"; +import AutoExpandingTextInputFormField from "../Form/FormFields/AutoExpandingTextInputFormField.js"; interface PatientNotesProps { patientId: string; @@ -30,6 +30,8 @@ export default function PatientNotesSlideover(props: PatientNotesProps) { notes: [], cPage: 1, totalPages: 1, + patientId: props.patientId, + facilityId: props.facilityId, }; const [state, setState] = useState(initialData); @@ -106,7 +108,7 @@ export default function PatientNotesSlideover(props: PatientNotesProps) {
{show && ( @@ -123,7 +125,7 @@ export default function PatientNotesSlideover(props: PatientNotesProps) {
setShowPatientNotesPopup(false)} > @@ -163,19 +165,19 @@ export default function PatientNotesSlideover(props: PatientNotesProps) {
- setNoteField(e.value)} className="grow" - type="text" errorClassName="hidden" placeholder="Type your Note" disabled={!patientActive} diff --git a/src/Components/Facility/models.tsx b/src/Components/Facility/models.tsx index de035f9ffea..e3913f072cd 100644 --- a/src/Components/Facility/models.tsx +++ b/src/Components/Facility/models.tsx @@ -490,16 +490,28 @@ export interface BaseUserModel { last_login: string; } +export interface PatientNotesEditModel { + id: string; + edited_by: BaseUserModel; + edited_date: string; + note: string; +} + export interface PatientNotesModel { + id: string; note: string; facility: BaseFacilityModel; created_by_object: BaseUserModel; user_type?: UserRole | "RemoteSpecialist"; created_date: string; + last_edited_by?: BaseUserModel; + last_edited_date?: string; } export interface PatientNoteStateType { notes: PatientNotesModel[]; + patientId: string; + facilityId: string; cPage: number; totalPages: number; } diff --git a/src/Components/Form/FormFields/AutoExpandingTextInputFormField.tsx b/src/Components/Form/FormFields/AutoExpandingTextInputFormField.tsx new file mode 100644 index 00000000000..22707f133bc --- /dev/null +++ b/src/Components/Form/FormFields/AutoExpandingTextInputFormField.tsx @@ -0,0 +1,30 @@ +import React, { useEffect, useRef } from "react"; +import TextAreaFormField, { TextAreaFormFieldProps } from "./TextAreaFormField"; + +type AutoExpandingTextInputFormFieldProps = TextAreaFormFieldProps & { + maxHeight?: number; +}; + +const AutoExpandingTextInputFormField = ( + props: AutoExpandingTextInputFormFieldProps +) => { + const myref = useRef(null); + useEffect(() => { + if (myref.current == null) return; + const text = myref.current.textContent?.split("\n"); + const len = text?.length || 1; + // 46 is height of the textarea when there is only 1 line + // getting line height from window + const lineHeight = + window.getComputedStyle(myref.current).lineHeight.slice(0, -2) || "20"; + // added 26 for padding (20+26 = 46) + const height = + Math.min(len * parseInt(lineHeight), (props.maxHeight || 160) - 26) + 26; + // 160 is the max height of the textarea if not specified + myref.current.style.cssText = "height:" + height + "px"; + }); + + return ; +}; + +export default AutoExpandingTextInputFormField; diff --git a/src/Components/Form/FormFields/TextAreaFormField.tsx b/src/Components/Form/FormFields/TextAreaFormField.tsx index 23a7d025938..5c23bfec764 100644 --- a/src/Components/Form/FormFields/TextAreaFormField.tsx +++ b/src/Components/Form/FormFields/TextAreaFormField.tsx @@ -1,33 +1,44 @@ +import { forwardRef } from "react"; import FormField from "./FormField"; import { FormFieldBaseProps, useFormFieldPropsResolver } from "./Utils"; -type TextAreaFormFieldProps = FormFieldBaseProps & { +export type TextAreaFormFieldProps = FormFieldBaseProps & { placeholder?: string; value?: string | number; rows?: number; // prefixIcon?: React.ReactNode; // suffixIcon?: React.ReactNode; + onFocus?: (event: React.FocusEvent) => void; + onBlur?: (event: React.FocusEvent) => void; }; -const TextAreaFormField = ({ rows = 3, ...props }: TextAreaFormFieldProps) => { - const field = useFormFieldPropsResolver(props as any); - return ( - -