From 60f02ee9d1a367bd3916884f4090912468cebb82 Mon Sep 17 00:00:00 2001 From: Mohammed Nihal <57055998+nihal467@users.noreply.github.com> Date: Fri, 25 Oct 2024 18:17:31 +0500 Subject: [PATCH 01/15] Fixed the pagination at bed management page (#8928) --- src/components/Facility/BedManagement.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Facility/BedManagement.tsx b/src/components/Facility/BedManagement.tsx index 810a06e9c26..d7b47d89cce 100644 --- a/src/components/Facility/BedManagement.tsx +++ b/src/components/Facility/BedManagement.tsx @@ -164,7 +164,7 @@ const BedCard = ({ export const BedManagement = (props: BedManagementProps) => { const { facilityId, locationId } = props; - const { qParams, resultsPerPage } = useFilters({}); + const { qParams, resultsPerPage } = useFilters({ limit: 16 }); const { t } = useTranslation(); const { data: location } = useQuery(routes.getFacilityAssetLocation, { @@ -178,10 +178,10 @@ export const BedManagement = (props: BedManagementProps) => { From 59056d00c75f4332406964e1d1cc97d1b776b065 Mon Sep 17 00:00:00 2001 From: Nithish Kumar Siliveru Date: Mon, 28 Oct 2024 03:29:35 +0530 Subject: [PATCH 02/15] Fix the loading state when avatar upload fails (#8881) --- src/components/Common/AvatarEditModal.tsx | 4 +++- src/components/Facility/FacilityHome.tsx | 2 ++ src/components/Users/UserProfile.tsx | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/Common/AvatarEditModal.tsx b/src/components/Common/AvatarEditModal.tsx index 4fcd61b2b66..8884b62e3c2 100644 --- a/src/components/Common/AvatarEditModal.tsx +++ b/src/components/Common/AvatarEditModal.tsx @@ -117,8 +117,10 @@ const AvatarEditModal = ({ setIsProcessing(true); setIsCaptureImgBeingUploaded(true); - await handleUpload(selectedFile, () => { + setSelectedFile(undefined); + setPreview(undefined); + setPreviewImage(null); setIsCaptureImgBeingUploaded(false); setIsProcessing(false); }); diff --git a/src/components/Facility/FacilityHome.tsx b/src/components/Facility/FacilityHome.tsx index 13505889173..030763b24c4 100644 --- a/src/components/Facility/FacilityHome.tsx +++ b/src/components/Facility/FacilityHome.tsx @@ -133,6 +133,8 @@ export const FacilityHome = ({ facilityId }: Props) => { facilityFetch(); Notification.Success({ msg: "Cover image updated." }); setEditCoverImage(false); + } else { + onError(); } }, null, diff --git a/src/components/Users/UserProfile.tsx b/src/components/Users/UserProfile.tsx index 7aa3c9eff34..b84ad7e2bec 100644 --- a/src/components/Users/UserProfile.tsx +++ b/src/components/Users/UserProfile.tsx @@ -494,6 +494,8 @@ export default function UserProfile() { refetchUser(); Notification.Success({ msg: "Profile picture updated." }); setEditAvatar(false); + } else { + onError(); } }, null, From 20892e07d16233658214a760929498beb1b85435 Mon Sep 17 00:00:00 2001 From: Abhishek Patel Date: Mon, 28 Oct 2024 03:36:20 +0530 Subject: [PATCH 03/15] Conditionally disable Manage bed button in Location Management Page #8814 (#8827) --- src/components/Facility/LocationManagement.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/components/Facility/LocationManagement.tsx b/src/components/Facility/LocationManagement.tsx index 82184887332..6933a760969 100644 --- a/src/components/Facility/LocationManagement.tsx +++ b/src/components/Facility/LocationManagement.tsx @@ -14,9 +14,8 @@ import DialogModal from "@/components/Common/Dialog"; import Uptime from "@/components/Common/Uptime"; import useAuthUser from "@/common/hooks/useAuthUser"; import useQuery from "../../Utils/request/useQuery"; - import Loading from "@/components/Common/Loading"; -import { cn } from "@/lib/utils"; + interface Props { facilityId: string; } @@ -287,11 +286,9 @@ const Location = ({ id="manage-bed-button" variant="secondary" border - className={cn( - "mt-3 flex w-full items-center justify-between", - totalBeds != null && "opacity-50", - )} + className="mt-3 flex w-full items-center justify-between" href={`location/${id}/beds`} + disabled={totalBeds == null} > Manage Beds From 5ef6740b9092d1a9477a2c7b9846dcd79973124d Mon Sep 17 00:00:00 2001 From: JOSHIK ROSHAN <96165631+JOSHIK27@users.noreply.github.com> Date: Sun, 27 Oct 2024 22:13:24 +0000 Subject: [PATCH 04/15] Better UI for events log and tab switch (#8825) --- src/Locale/en.json | 5 + .../ConsultationUpdatesTab.tsx | 136 +++++++++++++--- .../ConsultationDetails/Events/EventsList.tsx | 14 +- .../Consultations/DailyRoundsFilter.tsx | 147 +++++++++--------- .../Consultations/DailyRoundsList.tsx | 16 +- 5 files changed, 196 insertions(+), 122 deletions(-) diff --git a/src/Locale/en.json b/src/Locale/en.json index fa5bdeb6aff..019ec5f29e7 100644 --- a/src/Locale/en.json +++ b/src/Locale/en.json @@ -348,6 +348,7 @@ "bed_type__300": "Oxygen Supported Bed", "bed_type__400": "Isolation Bed", "bed_type__500": "Others", + "beta": "beta", "bladder": "Bladder", "blood_group": "Blood Group", "blood_pressure_error": { @@ -508,6 +509,7 @@ "customer_support_number": "Customer support number", "cylinders": "Cylinders", "cylinders_per_day": "Cylinders/day", + "daily_rounds": "Daily Rounds", "date_and_time": "Date and Time", "date_declared_positive": "Date of declaring positive", "date_of_admission": "Date of Admission", @@ -624,6 +626,7 @@ "error_while_deleting_record": "Error while deleting record", "escape": "Escape", "estimated_contact_date": "Estimated contact date", + "events": "Events", "expand_sidebar": "Expand Sidebar", "expected_burn_rate": "Expected Burn Rate", "expired_on": "Expired On", @@ -808,6 +811,8 @@ "max_dosage_24_hrs": "Max. dosage in 24 hrs.", "max_dosage_in_24hrs_gte_base_dosage_error": "Max. dosage in 24 hours must be greater than or equal to base dosage", "max_size_for_image_uploaded_should_be": "Max size for image uploaded should be", + "measured_after": "Measured after", + "measured_before": "Measured before", "medical_council_registration": "Medical Council Registration", "medical_worker": "Medical Worker", "medicine": "Medicine", diff --git a/src/components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx b/src/components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx index de2fc6a7e5a..66cc40e4c71 100644 --- a/src/components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx +++ b/src/components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx @@ -23,6 +23,19 @@ import useQuery from "../../../Utils/request/useQuery"; import routes from "../../../Redux/api"; import CareIcon from "../../../CAREUI/icons/CareIcon"; import EncounterSymptomsCard from "../../Symptoms/SymptomsCard"; +import { QueryParams } from "../../../Utils/request/types"; +import { EVENTS_SORT_OPTIONS } from "@/common/constants"; +import DailyRoundsFilter from "../Consultations/DailyRoundsFilter"; +import ButtonV2 from "../../Common/components/ButtonV2"; +import { classNames } from "../../../Utils/utils"; + +import { useTranslation } from "react-i18next"; +import { + Popover, + PopoverButton, + PopoverPanel, + Transition, +} from "@headlessui/react"; import Tabs from "@/components/Common/components/Tabs"; import PageTitle from "@/components/Common/PageTitle"; @@ -33,6 +46,9 @@ export const ConsultationUpdatesTab = (props: ConsultationTabProps) => { const [monitorBedData, setMonitorBedData] = useState(); const [ventilatorBedData, setVentilatorBedData] = useState(); const [showEvents, setShowEvents] = useState(true); + const [eventsQuery, setEventsQuery] = useState(); + const [dailyRoundsQuery, setDailyRoundsQuery] = useState(); + const { t } = useTranslation(); const vitals = useVitalsAspectRatioConfig({ default: undefined, @@ -651,32 +667,110 @@ export const ConsultationUpdatesTab = (props: ConsultationTabProps) => {
- - Events - - beta - -
- ), - value: 1, - }, - { text: "Daily Rounds", value: 0 }, - ]} - onTabChange={(v) => setShowEvents(!!v)} - currentTab={showEvents ? 1 : 0} - /> +
+ + {t("events")} + + {t("beta")} + +
+ ), + value: 1, + }, + { text: t("daily_rounds"), value: 0 }, + ]} + onTabChange={(v) => setShowEvents(!!v)} + currentTab={showEvents ? 1 : 0} + /> + {showEvents ? ( + + + + + + + + +
+
+ {EVENTS_SORT_OPTIONS.map(({ isAscending, value }) => { + return ( +
{ + setEventsQuery({ + ordering: value, + }); + }} + > + + {t("SORT_OPTIONS__" + value)} +
+ ); + })} +
+
+
+
+
+ ) : ( + + )} + + {showEvents ? ( - + ) : ( - + )} ); }; + +function DailyRoundsSortDropdown({ + setDailyRoundsQuery, +}: { + setDailyRoundsQuery: (query: QueryParams) => void; +}) { + return ( + { + setDailyRoundsQuery(query); + }} + /> + ); +} diff --git a/src/components/Facility/ConsultationDetails/Events/EventsList.tsx b/src/components/Facility/ConsultationDetails/Events/EventsList.tsx index 81eced70e87..02c0c5131dd 100644 --- a/src/components/Facility/ConsultationDetails/Events/EventsList.tsx +++ b/src/components/Facility/ConsultationDetails/Events/EventsList.tsx @@ -7,15 +7,11 @@ import LoadingLogUpdateCard from "../../Consultations/DailyRounds/LoadingCard"; import GenericEvent from "./GenericEvent"; import { getEventIcon } from "./iconMap"; import { EventGeneric } from "./types"; -import SortDropdownMenu from "@/components/Common/SortDropdown"; -import { EVENTS_SORT_OPTIONS } from "@/common/constants"; import { QueryParams } from "../../../../Utils/request/types"; -import { useState } from "react"; -export default function EventsList() { +export default function EventsList({ query }: { query: QueryParams }) { const [consultationId] = useSlugs("consultation"); const { t } = useTranslation(); - const [query, setQuery] = useState(); return ( {() => ( <> -
- -
-
diff --git a/src/components/Facility/Consultations/DailyRoundsFilter.tsx b/src/components/Facility/Consultations/DailyRoundsFilter.tsx index 5e9104b5992..2df3b506d68 100644 --- a/src/components/Facility/Consultations/DailyRoundsFilter.tsx +++ b/src/components/Facility/Consultations/DailyRoundsFilter.tsx @@ -42,84 +42,81 @@ export default function DailyRoundsFilter(props: Props) { ); return ( -
- - - - - {t("filter")} - - - + + - -
-
-
- - {t("filter_by")} - -
+ + + + + +
+
+
+ + {t("filter_by")} +
-
- t(`ROUNDS_TYPE__${o}`)} - optionValue={(o) => o} - /> - - +
+
+ t(`ROUNDS_TYPE__${o}`)} + optionValue={(o) => o} + /> + + - - { - setFilter({}); - props.onApply({}); - }} - border - className="w-full" - > - {t("clear")} - - - - props.onApply(filter)} - border - className="w-full" - > - {t("apply")} - - -
+ + { + setFilter({}); + props.onApply({}); + }} + border + className="w-full" + > + {t("clear")} + + + + props.onApply(filter)} + border + className="w-full" + > + {t("apply")} + +
- - - -
+
+ + + ); } diff --git a/src/components/Facility/Consultations/DailyRoundsList.tsx b/src/components/Facility/Consultations/DailyRoundsList.tsx index 0ae6ffb1765..a91764e581b 100644 --- a/src/components/Facility/Consultations/DailyRoundsList.tsx +++ b/src/components/Facility/Consultations/DailyRoundsList.tsx @@ -5,23 +5,21 @@ import { useTranslation } from "react-i18next"; import LoadingLogUpdateCard from "./DailyRounds/LoadingCard"; import routes from "../../../Redux/api"; import PaginatedList from "../../../CAREUI/misc/PaginatedList"; -import DailyRoundsFilter from "./DailyRoundsFilter"; import { ConsultationModel } from "../models"; import { useSlugs } from "@/common/hooks/useSlug"; import Timeline, { TimelineNode } from "../../../CAREUI/display/Timeline"; -import { useState } from "react"; import { QueryParams } from "../../../Utils/request/types"; import { UserRole } from "@/common/constants"; interface Props { consultation: ConsultationModel; + query: QueryParams; } -export default function DailyRoundsList({ consultation }: Props) { +export default function DailyRoundsList({ consultation, query }: Props) { const [consultationId] = useSlugs("consultation"); const { t } = useTranslation(); - const [query, setQuery] = useState(); return ( {() => ( <> -
- { - setQuery(query); - }} - /> -
- -
+
From 9a0d27bae08a70aa6814d2fa3c471fb24595cf71 Mon Sep 17 00:00:00 2001 From: Jacob John Jeevan <40040905+Jacobjeevan@users.noreply.github.com> Date: Mon, 28 Oct 2024 03:45:51 +0530 Subject: [PATCH 05/15] Round off the PCO2 values to nearest integer in log update (#8776) --- src/components/Form/FormFields/RangeFormField.tsx | 3 ++- src/components/LogUpdate/Sections/BloodSugar.tsx | 2 +- src/components/LogUpdate/Sections/Vitals.tsx | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/Form/FormFields/RangeFormField.tsx b/src/components/Form/FormFields/RangeFormField.tsx index e7e9b1e9a53..c09cbfb2293 100644 --- a/src/components/Form/FormFields/RangeFormField.tsx +++ b/src/components/Form/FormFields/RangeFormField.tsx @@ -93,7 +93,8 @@ export default function RangeFormField(props: Props) { sliderDelta) * 100; - const handleChange = (v: number) => field.handleChange(unit.inversionFn(v)); + const handleChange = (v: number) => + field.handleChange(unit.inversionFn(props.step === 1 ? Math.round(v) : v)); const displayValue = value != null ? properRoundOf(value) : ""; diff --git a/src/components/LogUpdate/Sections/BloodSugar.tsx b/src/components/LogUpdate/Sections/BloodSugar.tsx index aecfe887f51..9af7ad32069 100644 --- a/src/components/LogUpdate/Sections/BloodSugar.tsx +++ b/src/components/LogUpdate/Sections/BloodSugar.tsx @@ -32,7 +32,7 @@ const BloodSugar = ({ log, onChange }: LogUpdateSectionProps) => { value={log.insulin_intake_dose} min={0} max={100} - step={1} + step={0.1} /> Date: Mon, 28 Oct 2024 03:53:38 +0530 Subject: [PATCH 06/15] Resolve permission handling Issues in microphone and camera capture dialogs (#8830) Co-authored-by: Bodhish Thomas --- src/Locale/en.json | 2 ++ src/Utils/useRecorder.js | 5 ++-- src/Utils/useSegmentedRecorder.ts | 4 ++- src/components/Files/AudioCaptureDialog.tsx | 23 ++++++++++++----- src/components/Files/CameraCaptureDialog.tsx | 27 +++++++++++++++++--- 5 files changed, 49 insertions(+), 12 deletions(-) diff --git a/src/Locale/en.json b/src/Locale/en.json index 019ec5f29e7..784dd9d3905 100644 --- a/src/Locale/en.json +++ b/src/Locale/en.json @@ -316,6 +316,7 @@ "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.", + "audio__permission_message": "Please grant microphone permission to record audio.", "audio__record": "Record Audio", "audio__record_helper": "Click the button to start recording", "audio__recorded": "Audio Recorded", @@ -1161,6 +1162,7 @@ "summary": "Summary", "support": "Support", "switch": "Switch", + "switch_camera_is_not_available": "Switch camera is not available.", "systolic": "Systolic", "tachycardia": "Tachycardia", "target_dosage": "Target Dosage", diff --git a/src/Utils/useRecorder.js b/src/Utils/useRecorder.js index 446f824259f..8ad90ebe8cb 100644 --- a/src/Utils/useRecorder.js +++ b/src/Utils/useRecorder.js @@ -2,13 +2,14 @@ import { useEffect, useState } from "react"; import { Error } from "./Notifications"; +import { useTranslation } from "react-i18next"; const useRecorder = (handleMicPermission) => { const [audioURL, setAudioURL] = useState(""); const [isRecording, setIsRecording] = useState(false); const [recorder, setRecorder] = useState(null); const [newBlob, setNewBlob] = useState(null); - + const { t } = useTranslation(); useEffect(() => { if (!isRecording && recorder && audioURL) { setRecorder(null); @@ -26,7 +27,7 @@ const useRecorder = (handleMicPermission) => { }, () => { Error({ - msg: "Please grant microphone permission to record audio.", + msg: t("audio__permission_message"), }); setIsRecording(false); handleMicPermission(false); diff --git a/src/Utils/useSegmentedRecorder.ts b/src/Utils/useSegmentedRecorder.ts index 9434ea8383c..fe38afb3b06 100644 --- a/src/Utils/useSegmentedRecorder.ts +++ b/src/Utils/useSegmentedRecorder.ts @@ -1,11 +1,13 @@ import { useState, useEffect } from "react"; import * as Notify from "./Notifications"; +import { useTranslation } from "react-i18next"; const useSegmentedRecording = () => { const [isRecording, setIsRecording] = useState(false); const [recorder, setRecorder] = useState(null); const [audioBlobs, setAudioBlobs] = useState([]); const [restart, setRestart] = useState(false); + const { t } = useTranslation(); const bufferInterval = 1 * 1000; const splitSizeLimit = 20 * 1000000; // 20MB @@ -28,7 +30,7 @@ const useSegmentedRecording = () => { }, () => { Notify.Error({ - msg: "Please grant microphone permission to record audio.", + msg: t("audio__permission_message"), }); setIsRecording(false); }, diff --git a/src/components/Files/AudioCaptureDialog.tsx b/src/components/Files/AudioCaptureDialog.tsx index 097de0088aa..f84fc01c28b 100644 --- a/src/components/Files/AudioCaptureDialog.tsx +++ b/src/components/Files/AudioCaptureDialog.tsx @@ -3,7 +3,8 @@ import useRecorder from "../../Utils/useRecorder"; import { Link } from "raviger"; import CareIcon from "../../CAREUI/icons/CareIcon"; import { useTimer } from "../../Utils/useTimer"; -import { t } from "i18next"; +import { useTranslation } from "react-i18next"; +import * as Notify from "../../Utils/Notifications"; export interface AudioCaptureDialogProps { show: boolean; @@ -20,8 +21,8 @@ export default function AudioCaptureDialog(props: AudioCaptureDialogProps) { | "RECORDED"; const { show, onHide, onCapture, autoRecord = false } = props; - const [status, setStatus] = useState(null); + const { t } = useTranslation(); const [audioURL, , startRecording, stopRecording, , resetRecording] = useRecorder((permission: boolean) => { @@ -35,9 +36,19 @@ export default function AudioCaptureDialog(props: AudioCaptureDialogProps) { const timer = useTimer(); const handleStartRecording = () => { - setStatus("RECORDING"); - startRecording(); - timer.start(); + navigator.mediaDevices + .getUserMedia({ audio: true }) + .then(() => { + setStatus("RECORDING"); + startRecording(); + timer.start(); + }) + .catch(() => { + Notify.Error({ + msg: t("audio__permission_message"), + }); + setStatus("PERMISSION_DENIED"); + }); }; const handleStopRecording = () => { @@ -87,7 +98,7 @@ export default function AudioCaptureDialog(props: AudioCaptureDialogProps) { }, [show]); useEffect(() => { - if (autoRecord && show && status === "WAITING_TO_RECORD") { + if (autoRecord && show && status === "RECORDING") { handleStartRecording(); } }, [autoRecord, status, show]); diff --git a/src/components/Files/CameraCaptureDialog.tsx b/src/components/Files/CameraCaptureDialog.tsx index b5af6ace8d5..81981c9bf3a 100644 --- a/src/components/Files/CameraCaptureDialog.tsx +++ b/src/components/Files/CameraCaptureDialog.tsx @@ -3,8 +3,9 @@ import CareIcon from "../../CAREUI/icons/CareIcon"; import DialogModal from "@/components/Common/Dialog"; import ButtonV2, { Submit } from "@/components/Common/components/ButtonV2"; import { t } from "i18next"; -import { useCallback, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import useWindowDimensions from "@/common/hooks/useWindowDimensions"; +import * as Notify from "../../Utils/Notifications"; export interface CameraCaptureDialogProps { show: boolean; @@ -24,9 +25,29 @@ export default function CameraCaptureDialog(props: CameraCaptureDialogProps) { height: { ideal: 2160 }, facingMode: "user", }; - + useEffect(() => { + if (!show) return; + navigator.mediaDevices.getUserMedia({ video: true }).catch(() => { + Notify.Warn({ + msg: t("camera_permission_denied"), + }); + onHide(); + }); + }, [show]); const handleSwitchCamera = useCallback(() => { - setCameraFacingFront((prevState) => !prevState); + const supportedConstraints = + navigator.mediaDevices.getSupportedConstraints(); + if ( + !isLaptopScreen && + typeof supportedConstraints.facingMode === "string" && + (supportedConstraints.facingMode as string).includes("environment") + ) { + setCameraFacingFront((prevState) => !prevState); + } else { + Notify.Warn({ + msg: t("switch_camera_is_not_available"), + }); + } }, []); const { width } = useWindowDimensions(); From eeed897a3d11b531a9c4c69488e144d11ee9e2ba Mon Sep 17 00:00:00 2001 From: Nithish Kumar Siliveru Date: Mon, 28 Oct 2024 10:47:30 +0530 Subject: [PATCH 07/15] Fixed session-expired page view and minor improvments (#8939) --- src/components/ErrorPages/SessionExpired.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ErrorPages/SessionExpired.tsx b/src/components/ErrorPages/SessionExpired.tsx index 441412ba1e9..2b50a8bcc6e 100644 --- a/src/components/ErrorPages/SessionExpired.tsx +++ b/src/components/ErrorPages/SessionExpired.tsx @@ -17,7 +17,7 @@ export default function SessionExpired() { {t("session_expired")}

{t("session_expired")}

@@ -26,7 +26,7 @@ export default function SessionExpired() {

{t("return_to_login")}
From 8eb4dc491ab75484f307d804419df91fea65a71e Mon Sep 17 00:00:00 2001 From: Shivank Kacker Date: Mon, 28 Oct 2024 12:56:37 +0530 Subject: [PATCH 08/15] Added support for time in dateinput component (#8504) --- .../PatientConsultationCreation.cy.ts | 8 +- .../patient_spec/PatientRegistration.cy.ts | 2 +- cypress/e2e/users_spec/UsersCreation.cy.ts | 2 +- cypress/pageobject/Asset/AssetCreation.ts | 19 +- .../pageobject/Facility/FacilityCreation.ts | 3 +- .../pageobject/Patient/PatientConsultation.ts | 2 +- cypress/pageobject/Patient/PatientCreation.ts | 10 +- .../Patient/PatientTreatmentPlan.ts | 2 +- cypress/pageobject/Shift/ShiftFilters.ts | 10 +- cypress/pageobject/Users/UserCreation.ts | 12 +- cypress/pageobject/Users/UserProfilePage.ts | 4 +- cypress/support/commands.ts | 10 +- src/CAREUI/interactive/SlideOver.tsx | 7 +- src/components/ABDM/FetchRecordsModal.tsx | 1 - .../Assets/AssetServiceEditModal.tsx | 1 - src/components/Common/DateInputV2.tsx | 706 +++++++++++------- src/components/Common/DateRangeInputV2.tsx | 5 +- src/components/Common/DateTextInput.tsx | 203 +++++ src/components/Common/SortDropdown.tsx | 4 +- .../InvestigationBuilder.tsx | 45 +- .../prescription-builder/ProcedureBuilder.tsx | 47 +- src/components/DeathReport/DeathReport.tsx | 6 - src/components/Facility/AssetCreate.tsx | 1 - src/components/Facility/ConsultationForm.tsx | 63 +- .../Facility/Consultations/Beds.tsx | 14 +- .../Consultations/DailyRoundsFilter.tsx | 42 +- src/components/Facility/DischargeModal.tsx | 28 +- src/components/Facility/TriageForm.tsx | 1 - .../Form/FormFields/DateFormField.tsx | 12 +- .../Form/FormFields/DateRangeFormField.tsx | 3 + src/components/Form/FormFields/Utils.ts | 3 + .../Medicine/AdministerMedicine.tsx | 17 +- .../Medicine/MedicineAdministration.tsx | 20 +- src/components/Patient/DailyRounds.tsx | 26 +- src/components/Patient/PatientRegister.tsx | 6 - src/components/Symptoms/SymptomsBuilder.tsx | 2 - src/components/Users/UserAdd.tsx | 1 - src/components/Users/UserProfile.tsx | 1 - src/style/index.css | 5 + 39 files changed, 885 insertions(+), 469 deletions(-) create mode 100644 src/components/Common/DateTextInput.tsx diff --git a/cypress/e2e/patient_spec/PatientConsultationCreation.cy.ts b/cypress/e2e/patient_spec/PatientConsultationCreation.cy.ts index 6f9bf2b03e0..9911090eba0 100644 --- a/cypress/e2e/patient_spec/PatientConsultationCreation.cy.ts +++ b/cypress/e2e/patient_spec/PatientConsultationCreation.cy.ts @@ -86,7 +86,7 @@ describe("Patient Consultation in multiple combination", () => { patientConsultationPage.selectPatientPrincipalDiagnosis(diagnosis4); patientTreatmentPlan.clickAddProcedure(); patientTreatmentPlan.typeProcedureName(procedureName); - patientTreatmentPlan.typeProcedureTime("2024-02-22T12:30"); + patientTreatmentPlan.typeProcedureTime("220220241230"); patientTreatmentPlan.typeTreatmentPlan(patientTreatment); patientTreatmentPlan.typePatientGeneralInstruction(generalInstruction); patientTreatmentPlan.typeSpecialInstruction(specialInstruction); @@ -182,12 +182,12 @@ describe("Patient Consultation in multiple combination", () => { patientConsultationPage.typeCauseOfDeath("Cause of Death"); patientConsultationPage.typePatientConsultationDate( "#death_datetime", - "2024-02-22T12:45", + "220220241230", ); patientConsultationPage.typeDeathConfirmedBy(doctorName); patientConsultationPage.typePatientConsultationDate( "#encounter_date", - "2024-02-22T12:30", + "220220241230", ); cy.submitButton("Create Consultation"); cy.verifyNotification( @@ -245,7 +245,7 @@ describe("Patient Consultation in multiple combination", () => { ); patientConsultationPage.typePatientConsultationDate( "#icu_admission_date", - "2024-02-23T12:30", + "230220241230", ); // add investigation patientInvestigation.clickAddInvestigation(); diff --git a/cypress/e2e/patient_spec/PatientRegistration.cy.ts b/cypress/e2e/patient_spec/PatientRegistration.cy.ts index 4e493a23bff..36c08497c2c 100644 --- a/cypress/e2e/patient_spec/PatientRegistration.cy.ts +++ b/cypress/e2e/patient_spec/PatientRegistration.cy.ts @@ -25,7 +25,7 @@ const getRelativeDateString = (deltaDays = 0) => { month: "2-digit", year: "numeric", }) - .replace("/", ""); + .replace(/\//g, ""); }; describe("Patient Creation with consultation", () => { diff --git a/cypress/e2e/users_spec/UsersCreation.cy.ts b/cypress/e2e/users_spec/UsersCreation.cy.ts index 392c9ff987d..8e7236835e7 100644 --- a/cypress/e2e/users_spec/UsersCreation.cy.ts +++ b/cypress/e2e/users_spec/UsersCreation.cy.ts @@ -145,7 +145,7 @@ describe("User Creation", () => { userCreationPage.typeIntoElementById("password", "Test@123"); userCreationPage.selectHomeFacility("Dummy Shifting Center"); userCreationPage.typeIntoElementById("phone_number", phone_number); - userCreationPage.setInputDate("date_of_birth", "date-input", "25081999"); + userCreationPage.setInputDate("date_of_birth", "25081999"); userCreationPage.selectDropdownOption("user_type", "Doctor"); userCreationPage.typeIntoElementById("c_password", "Test@123"); userCreationPage.typeIntoElementById("qualification", "MBBS"); diff --git a/cypress/pageobject/Asset/AssetCreation.ts b/cypress/pageobject/Asset/AssetCreation.ts index f0b2f1b74eb..ec631768154 100644 --- a/cypress/pageobject/Asset/AssetCreation.ts +++ b/cypress/pageobject/Asset/AssetCreation.ts @@ -59,10 +59,10 @@ export class AssetPage { cy.get("[data-testid=asset-support-email-input] input").type(supportEmail); cy.get("[data-testid=asset-vendor-name-input] input").type(vendorName); cy.get("[data-testid=asset-serial-number-input] input").type(serialNumber); - cy.get( - "[data-testid=asset-last-serviced-on-input] input[type='text']", - ).click(); - cy.get("#date-input").click().type(lastServicedOn); + cy.clickAndTypeDate( + "[data-testid=asset-last-serviced-on-input]", + lastServicedOn, + ); cy.get("[data-testid=asset-notes-input] textarea").type(notes); } @@ -117,10 +117,10 @@ export class AssetPage { cy.get("[data-testid=asset-vendor-name-input] input") .clear() .type(vendorName); - cy.get( - "[data-testid=asset-last-serviced-on-input] input[type='text']", - ).click(); - cy.get("#date-input").click().clear().type(lastServicedOn); + cy.clickAndTypeDate( + "[data-testid=asset-last-serviced-on-input]", + lastServicedOn, + ); cy.get("[data-testid=asset-notes-input] textarea").clear().type(notes); } @@ -267,8 +267,7 @@ export class AssetPage { } enterAssetservicedate(text: string) { - cy.get("input[name='last_serviced_on']").click(); - cy.get("#date-input").click().type(text); + cy.clickAndTypeDate("input[name='last_serviced_on']", text); } scrollintoWarrantyDetails() { diff --git a/cypress/pageobject/Facility/FacilityCreation.ts b/cypress/pageobject/Facility/FacilityCreation.ts index 159b8660b43..a48ffe0856b 100644 --- a/cypress/pageobject/Facility/FacilityCreation.ts +++ b/cypress/pageobject/Facility/FacilityCreation.ts @@ -245,8 +245,7 @@ class FacilityPage { } fillEntryDate(date: string) { - cy.get("#entry_date").click(); - cy.get("#date-input").click().type(date); + cy.clickAndTypeDate("#entry_date", date); } clickEditButton() { diff --git a/cypress/pageobject/Patient/PatientConsultation.ts b/cypress/pageobject/Patient/PatientConsultation.ts index 71a0fbb3909..e0b51b9265e 100644 --- a/cypress/pageobject/Patient/PatientConsultation.ts +++ b/cypress/pageobject/Patient/PatientConsultation.ts @@ -61,7 +61,7 @@ export class PatientConsultationPage { } typePatientConsultationDate(selector: string, date: string) { - cy.get(selector).clear().click().type(date); + cy.clickAndTypeDate(selector, date); } clickPatientDetails() { diff --git a/cypress/pageobject/Patient/PatientCreation.ts b/cypress/pageobject/Patient/PatientCreation.ts index a583844a632..41b3c02977d 100644 --- a/cypress/pageobject/Patient/PatientCreation.ts +++ b/cypress/pageobject/Patient/PatientCreation.ts @@ -52,9 +52,7 @@ export class PatientPage { typePatientDateOfBirth(dateOfBirth: string) { cy.clickAndSelectOption("#patientAge", "DOB"); - cy.get("#date_of_birth").scrollIntoView(); - cy.get("#date_of_birth").should("be.visible").click(); - cy.get("#date-input").click().type(dateOfBirth); + cy.clickAndTypeDate("#date_of_birth", dateOfBirth); } typePatientAge(age: string) { @@ -80,13 +78,11 @@ export class PatientPage { } typeLastMenstruationStartDate(date: string) { - cy.get("#last_menstruation_start_date").click(); - cy.get("#date-input").click().type(date); + cy.clickAndTypeDate("#last_menstruation_start_date", date); } typeDateOfDelivery(date: string) { - cy.get("#date_of_delivery").click(); - cy.get("#date-input").click().type(date); + cy.clickAndTypeDate("#date_of_delivery", date); } clickPermanentAddress() { diff --git a/cypress/pageobject/Patient/PatientTreatmentPlan.ts b/cypress/pageobject/Patient/PatientTreatmentPlan.ts index 0bbddbf70bc..02b2f9b150d 100644 --- a/cypress/pageobject/Patient/PatientTreatmentPlan.ts +++ b/cypress/pageobject/Patient/PatientTreatmentPlan.ts @@ -33,7 +33,7 @@ class PatientTreatmentPlan { } typeProcedureTime(time: string) { - cy.get("#procedure-time").type(time); + cy.clickAndTypeDate("#procedure-time", time); } typeTreatmentPlan(treatment: string) { diff --git a/cypress/pageobject/Shift/ShiftFilters.ts b/cypress/pageobject/Shift/ShiftFilters.ts index 1f824cebbb3..fe20b97bd9f 100644 --- a/cypress/pageobject/Shift/ShiftFilters.ts +++ b/cypress/pageobject/Shift/ShiftFilters.ts @@ -139,18 +139,12 @@ class ShiftingPage { modified_date_end: string, ) { this.createdDateStartInput().click(); - cy.get("[id^='headlessui-popover-panel-'] .care-l-angle-left-b") - .eq(0) - .closest("button") - .click(); + cy.get("[data-test-id='increment-date-range']").click(); cy.get(created_date_start).click(); cy.get(created_date_end).click(); this.modifiedDateStartInput().click(); - cy.get("[id^='headlessui-popover-panel-'] .care-l-angle-left-b") - .eq(0) - .closest("button") - .click(); + cy.get("[data-test-id='increment-date-range']").click(); cy.get(modified_date_start).click(); cy.get(modified_date_end).click(); diff --git a/cypress/pageobject/Users/UserCreation.ts b/cypress/pageobject/Users/UserCreation.ts index 7503ea3fb07..906c07e797a 100644 --- a/cypress/pageobject/Users/UserCreation.ts +++ b/cypress/pageobject/Users/UserCreation.ts @@ -18,8 +18,7 @@ export class UserCreationPage { .type(value); } typeIntoElementByIdPostClearDob(elementId: string, value: string) { - cy.get("#" + elementId).click(); - cy.get("#date-input").clear().type(value); + cy.clickAndTypeDate("#" + elementId, value); } clearIntoElementById(elementId: string) { cy.get("#" + elementId) @@ -54,13 +53,8 @@ export class UserCreationPage { this.selectOptionContainingText(name); } - setInputDate( - dateElementId: string, - inputElementId: string, - dateValue: string, - ) { - this.clickElementById(dateElementId); - this.typeIntoElementById(inputElementId, dateValue); + setInputDate(dateElementId: string, dateValue: string) { + cy.clickAndTypeDate("#" + dateElementId, dateValue); } selectDropdownOption(dropdownId: string, optionText: string) { diff --git a/cypress/pageobject/Users/UserProfilePage.ts b/cypress/pageobject/Users/UserProfilePage.ts index 20fd1911c49..3744c5a5d82 100644 --- a/cypress/pageobject/Users/UserProfilePage.ts +++ b/cypress/pageobject/Users/UserProfilePage.ts @@ -16,9 +16,7 @@ export default class UserProfilePage { } typedate_of_birth(date_of_birth: string) { - //check - cy.get("#date_of_birth").click(); - cy.get("#date-input").clear().type(date_of_birth); + cy.clickAndTypeDate("#date_of_birth", date_of_birth); } selectGender(gender: string) { diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index da78361f6e8..9af5f97e5d4 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -183,10 +183,16 @@ Cypress.Commands.add("selectRadioOption", (name: string, value: string) => { cy.get(`input[type='radio'][name='${name}'][value=${value}]`).click(); }); -Cypress.Commands.add("clickAndTypeDate", (selector: string, date: string) => { +Cypress.Commands.add("clickAndTypeDate", (selector, date) => { cy.get(selector).scrollIntoView(); cy.get(selector).click(); - cy.get("#date-input").click().type(date); + cy.get('[data-test-id="date-input"]:visible [data-time-input]').each((el) => + cy.wrap(el).clear(), + ); + cy.get(`[data-test-id="date-input"]:visible [data-time-input="0"]`) + .click() + .type(date); + cy.get("body").click(0, 0); }); Cypress.Commands.add( diff --git a/src/CAREUI/interactive/SlideOver.tsx b/src/CAREUI/interactive/SlideOver.tsx index bd38b32137a..78982efc39d 100644 --- a/src/CAREUI/interactive/SlideOver.tsx +++ b/src/CAREUI/interactive/SlideOver.tsx @@ -124,7 +124,12 @@ export default function SlideOver({

{title}

-
{children}
+
+ {children} +
)} diff --git a/src/components/ABDM/FetchRecordsModal.tsx b/src/components/ABDM/FetchRecordsModal.tsx index 34d70255846..441bee0ddbd 100644 --- a/src/components/ABDM/FetchRecordsModal.tsx +++ b/src/components/ABDM/FetchRecordsModal.tsx @@ -193,7 +193,6 @@ export default function FetchRecordsModal({ abha, show, onClose }: IProps) { label={t("consent_request__expiry")} required disablePast - position="TOP-RIGHT" />
diff --git a/src/components/Assets/AssetServiceEditModal.tsx b/src/components/Assets/AssetServiceEditModal.tsx index 9accf5c3d66..fecea8ad124 100644 --- a/src/components/Assets/AssetServiceEditModal.tsx +++ b/src/components/Assets/AssetServiceEditModal.tsx @@ -196,7 +196,6 @@ export const AssetServiceEditModal = (props: { label={t("serviced_on")} name="serviced_on" className="mt-2" - position="LEFT" value={new Date(form.serviced_on)} max={new Date(props.service_record.created_date)} onChange={(date) => { diff --git a/src/components/Common/DateInputV2.tsx b/src/components/Common/DateInputV2.tsx index 37f67c8ad80..87a29debe5b 100644 --- a/src/components/Common/DateInputV2.tsx +++ b/src/components/Common/DateInputV2.tsx @@ -1,4 +1,4 @@ -import { MutableRefObject, useEffect, useState } from "react"; +import { MutableRefObject, useEffect, useRef, useState } from "react"; import CareIcon from "../../CAREUI/icons/CareIcon"; import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react"; @@ -6,15 +6,9 @@ import { classNames } from "../../Utils/utils"; import dayjs from "../../Utils/dayjs"; import * as Notification from "../../Utils/Notifications"; import { t } from "i18next"; +import DateTextInput from "./DateTextInput"; type DatePickerType = "date" | "month" | "year"; -export type DatePickerPosition = - | "LEFT" - | "RIGHT" - | "CENTER" - | "TOP-LEFT" - | "TOP-RIGHT" - | "TOP-CENTER"; interface Props { id?: string; @@ -25,12 +19,13 @@ interface Props { min?: Date; max?: Date; outOfLimitsErrorMessage?: string; - onChange: (date: Date) => void; - position?: DatePickerPosition; + onChange: (date: Date | undefined) => void; disabled?: boolean; placeholder?: string; isOpen?: boolean; setIsOpen?: (isOpen: boolean) => void; + allowTime?: boolean; + popOverClassName?: string; } const DAYS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]; @@ -45,21 +40,42 @@ const DateInputV2: React.FC = ({ max, outOfLimitsErrorMessage, onChange, - position = "CENTER", disabled, placeholder, - isOpen, setIsOpen, + allowTime, + isOpen, + popOverClassName, }) => { const [dayCount, setDayCount] = useState>([]); const [blankDays, setBlankDays] = useState>([]); - const [datePickerHeaderDate, setDatePickerHeaderDate] = useState(new Date()); + const [datePickerHeaderDate, setDatePickerHeaderDate] = useState( + value || new Date(), + ); const [type, setType] = useState("date"); const [year, setYear] = useState(new Date()); - const [displayValue, setDisplayValue] = useState( - value ? dayjs(value).format("DDMMYYYY") : "", - ); + + const [popOverOpen, setPopOverOpen] = useState(false); + + const hours = dayjs(value).hour() % 12; + const minutes = dayjs(value).minute(); + const ampm = dayjs(value).hour() > 11 ? "PM" : "AM"; + + const hourScrollerRef = useRef(null); + const minuteScrollerRef = useRef(null); + + const popoverButtonRef = useRef(null); + + const getDayStart = (date: Date) => { + const newDate = new Date(date); + newDate.setHours(0, 0, 0, 0); + return newDate; + }; + + const handleChange = (date: Date) => { + onChange(allowTime ? date : getDayStart(date)); + }; const decrement = () => { switch (type) { @@ -97,14 +113,6 @@ const DateInputV2: React.FC = ({ } }; - const isSelectedDate = (date: number) => { - if (value) { - return dayjs( - new Date(value.getFullYear(), value.getMonth(), date), - ).isSame(dayjs(value)); - } - }; - type CloseFunction = ( focusableElement?: HTMLElement | MutableRefObject, ) => void; @@ -112,21 +120,43 @@ const DateInputV2: React.FC = ({ const setDateValue = (date: number, close: CloseFunction) => () => { isDateWithinConstraints(date) ? (() => { - onChange( + handleChange( new Date( datePickerHeaderDate.getFullYear(), datePickerHeaderDate.getMonth(), date, + datePickerHeaderDate.getHours(), + datePickerHeaderDate.getMinutes(), + datePickerHeaderDate.getSeconds(), ), ); - close(); - setIsOpen?.(false); + if (!allowTime) { + close(); + setIsOpen?.(false); + } })() : Notification.Error({ msg: outOfLimitsErrorMessage ?? "Cannot select date out of range", }); }; + const handleTimeChange = (options: { + newHours?: typeof hours; + newMinutes?: typeof minutes; + newAmpm?: typeof ampm; + }) => { + const { newHours = hours, newMinutes = minutes, newAmpm = ampm } = options; + handleChange( + new Date( + datePickerHeaderDate.getFullYear(), + datePickerHeaderDate.getMonth(), + datePickerHeaderDate.getDate(), + newAmpm === "PM" ? (newHours % 12) + 12 : newHours % 12, + newMinutes, + ), + ); + }; + const getDayCount = (date: Date) => { const daysInMonth = dayjs(date).daysInMonth(); @@ -165,22 +195,6 @@ const DateInputV2: React.FC = ({ return true; }; - const isDateWithinLimits = (parsedDate: dayjs.Dayjs): boolean => { - if (parsedDate?.isValid()) { - if ( - (max && parsedDate.toDate() > max) || - (min && parsedDate.toDate() < min) - ) { - Notification.Error({ - msg: outOfLimitsErrorMessage ?? "Cannot select date out of range", - }); - return false; - } - return true; - } - return false; - }; - const isSelectedMonth = (month: number) => month === datePickerHeaderDate.getMonth(); @@ -209,35 +223,58 @@ const DateInputV2: React.FC = ({ setType("date"); }; - const showMonthPicker = () => setType("month"); - - const showYearPicker = () => setType("year"); - useEffect(() => { getDayCount(datePickerHeaderDate); }, [datePickerHeaderDate]); + const scrollTime = (smooth: boolean = true) => { + const timeScrollers = [hourScrollerRef, minuteScrollerRef]; + timeScrollers.forEach((scroller) => { + if (!scroller.current) return; + const selected = scroller.current.querySelector("[data-selected=true]"); + if (selected) { + const selectedPosition = ( + selected as HTMLDivElement + ).getBoundingClientRect().top; + + const toScroll = + selectedPosition - scroller.current.getBoundingClientRect().top; + + selected.parentElement?.scrollBy({ + top: toScroll, + behavior: smooth ? "smooth" : "instant", + }); + } + }); + }; + useEffect(() => { - value && setDatePickerHeaderDate(new Date(value)); + value && setDatePickerHeaderDate(value); + scrollTime(); }, [value]); + useEffect(() => { + if (!popOverOpen) return; + scrollTime(false); + }, [popOverOpen]); + + useEffect(() => { + isOpen && popoverButtonRef.current?.click(); + }, [isOpen]); + + const dateFormat = `DD/MM/YYYY${allowTime ? " hh:mm a" : ""}`; + const getPosition = () => { - switch (position) { - case "LEFT": - return "left-0"; - case "RIGHT": - return "right-0 transform translate-x-1/2"; - case "CENTER": - return "transform -translate-x-1/2"; - case "TOP-LEFT": - return "bottom-full left-full"; - case "TOP-RIGHT": - return "bottom-full right-0"; - case "TOP-CENTER": - return "bottom-full left-1/2 transform -translate-x-1/2"; - default: - return "left-0"; - } + const viewportWidth = document.documentElement.clientWidth; + const viewportHeight = document.documentElement.clientHeight; + + const popOverX = popoverButtonRef.current?.getBoundingClientRect().x || 0; + const popOverY = popoverButtonRef.current?.getBoundingClientRect().y || 0; + + const right = popOverX > viewportWidth - (allowTime ? 420 : 300); + const top = popOverY > viewportHeight - 400; + + return `${right ? "md:-translate-x-1/2" : ""} ${top ? "md:-translate-y-[calc(100%+50px)]" : ""}`; }; return ( @@ -246,238 +283,357 @@ const DateInputV2: React.FC = ({ className={`${containerClassName ?? "container mx-auto text-black"}`} > - {({ open, close }) => ( -
- - - { + setPopOverOpen(open); + return ( +
+ -
- -
-
- - {(open || isOpen) && ( - -
+ +
+ +
+ + {open && ( + - - [dd, mm, yyyy].filter(Boolean).join("/"), - ) || "" - } // Display the value in DD/MM/YYYY format - placeholder={t("DD/MM/YYYY")} - onChange={(e) => { - setDisplayValue(e.target.value.replaceAll("/", "")); - const value = dayjs(e.target.value, "DD/MM/YYYY", true); - if (isDateWithinLimits(value)) { - onChange(value.toDate()); - close(); - setIsOpen?.(false); +
+ close()} + error={ + value && + (!dayjs(value).isValid() || + (!!max && value > max) || + (!!min && value < min)) + ? "Cannot select date out of range" + : undefined } - }} - /> -
-
- - -
- {type === "date" && ( -
+ +
+
+
+
- )} -
-

- {type == "year" - ? year.getFullYear() - : dayjs(datePickerHeaderDate).format("YYYY")} -

-
-
- -
+ + - {type === "date" && ( - <> -
- {DAYS.map((day, i) => ( +
+ {type === "date" && ( +
setType("month")} + className="cursor-pointer rounded px-3 py-1 text-center font-medium text-black hover:bg-secondary-300" + > + {dayjs(datePickerHeaderDate).format("MMMM")} +
+ )}
setType("year")} + className="cursor-pointer rounded px-3 py-1 font-medium text-black hover:bg-secondary-300" > -
- {day} -
+

+ {type == "year" + ? year.getFullYear() + : dayjs(datePickerHeaderDate).format( + "YYYY", + )} +

- ))} -
-
- {blankDays.map((_, i) => ( -
- ))} - {dayCount.map((d, i) => { - const withinConstraints = - isDateWithinConstraints(d); - const selected = value && isSelectedDate(d); - - const baseClasses = - "flex h-full items-center justify-center rounded text-center text-sm leading-loose transition duration-100 ease-in-out"; - let conditionalClasses = ""; - - if (withinConstraints) { - if (selected) { - conditionalClasses = - "bg-primary-500 font-bold text-white"; - } else { - conditionalClasses = - "hover:bg-secondary-300 cursor-pointer"; - } - } else { - conditionalClasses = - "!cursor-not-allowed !text-secondary-400"; +
+ +
+ + {type === "date" && ( + <> +
+ {DAYS.map((day, i) => (
- {d} +
+ {day} +
-
- ); - })} -
- - )} - {type === "month" && ( -
- {Array(12) - .fill(null) - .map((_, i) => ( + ))} +
+
+ {blankDays.map((_, i) => ( +
+ ))} + {dayCount.map((d, i) => { + const withinConstraints = + isDateWithinConstraints(d); + let selected; + if (value) { + const newDate = new Date( + datePickerHeaderDate, + ); + newDate.setDate(d); + selected = + value.toDateString() === + newDate.toDateString(); + } + + const baseClasses = + "flex h-full items-center justify-center rounded text-center text-sm leading-loose transition duration-100 ease-in-out"; + let conditionalClasses = ""; + + if (withinConstraints) { + if (selected) { + conditionalClasses = + "bg-primary-500 font-bold text-white"; + } else { + conditionalClasses = + "hover:bg-secondary-300 cursor-pointer"; + } + } else { + conditionalClasses = + "!cursor-not-allowed !text-secondary-400"; + } + return ( +
+ +
+ ); + })} +
+ + )} + {type === "month" && ( +
+ {Array(12) + .fill(null) + .map((_, i) => ( +
+ {dayjs( + new Date( + datePickerHeaderDate.getFullYear(), + i, + 1, + ), + ).format("MMM")} +
+ ))} +
+ )} + {type === "year" && ( +
+ {Array(12) + .fill(null) + .map((_, i) => { + const y = year.getFullYear() - 11 + i; + return ( +
+ {y} +
+ ); + })} +
+ )} +
+ {allowTime && ( +
+ {( + [ + { + name: "Hours", + value: hours, + options: Array.from( + { length: 12 }, + (_, i) => i + 1, + ), + onChange: (val: any) => { + handleTimeChange({ + newHours: val, + }); + }, + ref: hourScrollerRef, + }, + { + name: "Minutes", + value: minutes, + options: Array.from( + { length: 60 }, + (_, i) => i, + ), + onChange: (val: any) => { + handleTimeChange({ + newMinutes: val, + }); + }, + ref: minuteScrollerRef, + }, + { + name: "am/pm", + value: ampm, + options: ["AM", "PM"], + onChange: (val: any) => { + handleTimeChange({ + newAmpm: val, + }); + }, + ref: undefined, + }, + ] as const + ).map((input, i) => (
{ + const optionsHeight = + e.currentTarget.scrollHeight / 3; + const scrollTop = e.currentTarget.scrollTop; + const containerHeight = + e.currentTarget.clientHeight; + if (scrollTop >= optionsHeight * 2) { + e.currentTarget.scrollTo({ + top: optionsHeight, + }); + } + if ( + scrollTop + containerHeight <= + optionsHeight + ) { + e.currentTarget.scrollTo({ + top: optionsHeight + scrollTop, + }); + } + }} > - {dayjs( - new Date( - datePickerHeaderDate.getFullYear(), - i, - 1, - ), - ).format("MMM")} + {[ + ...input.options, + ...(input.name === "am/pm" + ? [] + : input.options), + ...(input.name === "am/pm" + ? [] + : input.options), + ].map((option, j) => ( + + ))}
))} -
- )} - {type === "year" && ( -
- {Array(12) - .fill(null) - .map((_, i) => { - const y = year.getFullYear() - 11 + i; - return ( -
- {y} -
- ); - })} -
- )} +
+ )} +
-
- - )} -
- )} +
+ )} +
+ ); + }}
diff --git a/src/components/Common/DateRangeInputV2.tsx b/src/components/Common/DateRangeInputV2.tsx index 0c3a7559ff9..aa426ad0cb2 100644 --- a/src/components/Common/DateRangeInputV2.tsx +++ b/src/components/Common/DateRangeInputV2.tsx @@ -14,6 +14,7 @@ type Props = { disabled?: boolean; max?: Date; min?: Date; + allowTime?: boolean; }; const DateRangeInputV2 = ({ value, onChange, ...props }: Props) => { @@ -33,9 +34,9 @@ const DateRangeInputV2 = ({ value, onChange, ...props }: Props) => { }} min={props.min} max={end || props.max} - position="RIGHT" placeholder="Start date" disabled={props.disabled} + allowTime={props.allowTime} />
@@ -46,11 +47,11 @@ const DateRangeInputV2 = ({ value, onChange, ...props }: Props) => { onChange={(end) => onChange({ start, end })} min={start || props.min} max={props.max} - position="CENTER" disabled={props.disabled || !start} placeholder="End date" isOpen={showEndPicker} setIsOpen={setShowEndPicker} + allowTime={props.allowTime} />
diff --git a/src/components/Common/DateTextInput.tsx b/src/components/Common/DateTextInput.tsx new file mode 100644 index 00000000000..38e13b23283 --- /dev/null +++ b/src/components/Common/DateTextInput.tsx @@ -0,0 +1,203 @@ +import CareIcon from "@/CAREUI/icons/CareIcon"; +import { classNames } from "@/Utils/utils"; +import dayjs from "dayjs"; +import { Fragment, KeyboardEvent, useEffect, useState } from "react"; + +/** + * DateTextInput component. + * + * @param {Object} props - Component properties. + * @param {boolean} props.allowTime - If true, shows time input fields (hour and minute). + * @param {Date} props.value - The current date value. + * @param {function(Date):void} props.onChange - Callback function when date value changes. + * @param {function():void} props.onFinishInitialTyping - Callback function when a user successfuly types in the date on the first input + * @param {String} props.error - Shows an error if specified + * + * @returns {JSX.Element} The date text input component. + */ +export default function DateTextInput(props: { + allowTime: boolean; + value?: Date; + onChange: (date: Date | undefined) => unknown; + onFinishInitialTyping?: () => unknown; + error?: string; +}) { + const { value, onChange, allowTime, error, onFinishInitialTyping } = props; + + const [editingText, setDirtyEditingText] = useState({ + date: `${value ? value?.getDate() : ""}`, + month: `${value ? value.getMonth() + 1 : ""} `, + year: `${value ? value.getFullYear() : ""}`, + hour: `${value ? value.getHours() : ""}`, + minute: `${value ? value.getMinutes() : ""}`, + }); + + const setEditingText = (et: typeof editingText) => { + setDirtyEditingText(et); + const newDate = new Date( + parseInt(et.year), + parseInt(et.month) - 1, + parseInt(et.date), + allowTime ? parseInt(et.hour) : 0, + allowTime ? parseInt(et.minute) : 0, + ); + if (et.year.length > 3 && dayjs(newDate).isValid()) { + if (!value && !allowTime) onFinishInitialTyping?.(); + if (!value && allowTime && et.minute.length > 1) + onFinishInitialTyping?.(); + onChange(newDate); + } + }; + + const handleBlur = (rawValue: string, key: string) => { + const val = getBlurredValue(rawValue, key); + setEditingText({ + ...editingText, + [key]: val, + }); + }; + + const getBlurredValue = (rawValue: string, key: string) => { + const maxMap = [31, 12, 2999, 23, 59]; + const index = Object.keys(editingText).findIndex((et) => et === key); + const value = Math.min(maxMap[index], parseInt(rawValue)); + const finalValue = + rawValue.trim() !== "" + ? ("000" + value).slice(key === "year" ? -4 : -2) + : ""; + return finalValue; + }; + + const goToInput = (i: number) => { + if (i < 0 || i > 4) return; + ( + document.querySelectorAll( + `[data-time-input]`, + ) as NodeListOf + ).forEach((i) => i.blur()); + ( + document.querySelector(`[data-time-input="${i}"]`) as HTMLInputElement + )?.focus(); + }; + + const handleKeyDown = (event: KeyboardEvent, i: number) => { + const keyboardKey: number = event.keyCode || event.charCode; + const target = event.target as HTMLInputElement; + + // check for backspace + if ([8].includes(keyboardKey) && target.value === "") goToInput(i - 1); + + // check for delete + if ([46].includes(keyboardKey) && target.value === "") goToInput(i + 1); + + // check for left arrow key + if ([37].includes(keyboardKey) && (target.selectionStart || 0) < 1) + goToInput(i - 1); + + // check for right arrow key + if ([39].includes(keyboardKey) && (target.selectionStart || 0) > 1) + goToInput(i + 1); + }; + + useEffect(() => { + const formatUnfocused = (value: number, id: number, digits: number = 2) => { + const activeElementIdRaw = + document.activeElement?.getAttribute("data-time-input"); + const activeElementId = activeElementIdRaw + ? parseInt(activeElementIdRaw) + : undefined; + if (id === activeElementId) return value; + return ("000" + value).slice(-digits); + }; + + setDirtyEditingText({ + date: `${value ? formatUnfocused(value.getDate(), 0) : ""}`, + month: `${value ? formatUnfocused(value.getMonth() + 1, 1) : ""}`, + year: `${value ? formatUnfocused(value.getFullYear(), 2, 4) : ""}`, + hour: `${value ? formatUnfocused(value.getHours(), 3) : ""}`, + minute: `${value ? formatUnfocused(value.getMinutes(), 4) : ""}`, + }); + }, [value]); + + return ( +
+
+ e.target === e.currentTarget && + (value ? goToInput(allowTime ? 4 : 2) : goToInput(0)) + } + data-test-id="date-input" + > + {Object.entries(editingText) + .slice(0, allowTime ? 5 : 3) + .map(([key, val], i) => ( + + handleKeyDown(e, i)} + data-time-input={i} + onChange={(e) => { + const value = e.target.value; + if ( + (value.endsWith("/") || + value.endsWith(" ") || + value.endsWith(":") || + value.length > (key === "year" ? 3 : 1)) && + i < 4 + ) { + goToInput(i + 1); + } else { + setEditingText({ + ...editingText, + [key]: value + .replace(/\D/g, "") + .slice(0, key === "year" ? 4 : 2), + }); + } + }} + onBlur={(e) => handleBlur(e.target.value, key)} + /> + + {["date", "month"].includes(key) + ? "/" + : key === "hour" + ? ":" + : " "} + + + ))} + + +
+ {error && {error}} +
+ ); +} diff --git a/src/components/Common/SortDropdown.tsx b/src/components/Common/SortDropdown.tsx index 0c4fddec58c..7f190fde646 100644 --- a/src/components/Common/SortDropdown.tsx +++ b/src/components/Common/SortDropdown.tsx @@ -27,9 +27,9 @@ export default function SortDropdownMenu(props: Props) { icon={} containerClassName="w-full md:w-auto" > - {props.options.map(({ isAscending, value }) => ( + {props.options.map(({ isAscending, value }, i) => (
) : ( -
- Time{" *"} - { - setItem( - { - ...investigation, - time: e.currentTarget.value, - }, - i, - ); - }} - onFocus={() => setActiveIdx(i)} - onBlur={() => setActiveIdx(null)} - /> -
+ + setItem( + { + ...investigation, + time: dayjs(e.value).format("YYYY-MM-DDTHH:mm"), + }, + i, + ) + } + allowTime + errorClassName="hidden" + className="w-full" + onFocus={() => setActiveIdx(i)} + onBlur={() => setActiveIdx(null)} + /> )}
diff --git a/src/components/Common/prescription-builder/ProcedureBuilder.tsx b/src/components/Common/prescription-builder/ProcedureBuilder.tsx index fe7ec3cda3c..36536c2435b 100644 --- a/src/components/Common/prescription-builder/ProcedureBuilder.tsx +++ b/src/components/Common/prescription-builder/ProcedureBuilder.tsx @@ -1,6 +1,8 @@ import { useState } from "react"; import { PrescriptionDropdown } from "./PrescriptionDropdown"; import CareIcon from "../../../CAREUI/icons/CareIcon"; +import dayjs from "dayjs"; +import DateFormField from "../../Form/FormFields/DateFormField"; export type ProcedureType = { procedure?: string; @@ -131,28 +133,29 @@ export default function ProcedureBuilder(props: Props) { />
) : ( -
-
- Time{" *"} -
- setActiveIdx(i)} - onBlur={() => setActiveIdx(null)} - onChange={(e) => { - setItem( - { - ...procedure, - time: e.currentTarget.value, - }, - i, - ); - }} - /> -
+ + setItem( + { + ...procedure, + time: dayjs(e.value).format("YYYY-MM-DDTHH:mm"), + }, + i, + ) + } + allowTime + errorClassName="hidden" + className="w-full" + onFocus={() => setActiveIdx(i)} + onBlur={() => setActiveIdx(null)} + /> )}
diff --git a/src/components/DeathReport/DeathReport.tsx b/src/components/DeathReport/DeathReport.tsx index 702a486bdfb..00f6d6bd773 100644 --- a/src/components/DeathReport/DeathReport.tsx +++ b/src/components/DeathReport/DeathReport.tsx @@ -410,7 +410,6 @@ export default function PrintDeathReport(props: { id: string }) { @@ -428,14 +427,12 @@ export default function PrintDeathReport(props: { id: string }) {
@@ -477,7 +474,6 @@ export default function PrintDeathReport(props: { id: string }) { @@ -485,7 +481,6 @@ export default function PrintDeathReport(props: { id: string }) { @@ -534,7 +529,6 @@ export default function PrintDeathReport(props: { id: string }) { diff --git a/src/components/Facility/AssetCreate.tsx b/src/components/Facility/AssetCreate.tsx index 40bf4d276dd..60676f9ca18 100644 --- a/src/components/Facility/AssetCreate.tsx +++ b/src/components/Facility/AssetCreate.tsx @@ -834,7 +834,6 @@ const AssetCreate = (props: AssetProps) => { label={t("last_serviced_on")} name="last_serviced_on" className="mt-2" - position="RIGHT" disableFuture value={last_serviced_on && new Date(last_serviced_on)} onChange={(date) => { diff --git a/src/components/Facility/ConsultationForm.tsx b/src/components/Facility/ConsultationForm.tsx index 47ba2eea77f..9fe281635f2 100644 --- a/src/components/Facility/ConsultationForm.tsx +++ b/src/components/Facility/ConsultationForm.tsx @@ -65,6 +65,7 @@ import { CreateSymptomsBuilder, } from "../Symptoms/SymptomsBuilder"; import careConfig from "@careConfig"; +import DateFormField from "../Form/FormFields/DateFormField.js"; import Loading from "@/components/Common/Loading"; import PageTitle from "@/components/Common/PageTitle"; @@ -1178,13 +1179,24 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { className="col-span-6" ref={fieldRef["death_datetime"]} > - + field("death_datetime").onChange({ + ...e, + value: dayjs(e.value).format("YYYY-MM-DDTHH:mm"), + }) + } + allowTime + errorClassName="hidden" />
{ )} ref={fieldRef["encounter_date"]} > - { label={t( `encounter_date_field_label__${state.form.suggestion}`, )} - type="datetime-local" - value={dayjs(state.form.encounter_date).format( - "YYYY-MM-DDTHH:mm", - )} - max={dayjs().format("YYYY-MM-DDTHH:mm")} - min={dayjs(careConfig.minEncounterDate).format( - "YYYY-MM-DDTHH:mm", - )} + value={ + !state.form.encounter_date + ? new Date() + : state.form.encounter_date + } + max={new Date()} + min={careConfig.minEncounterDate} + onChange={(e) => + field("encounter_date").onChange({ + ...e, + value: dayjs(e.value).format("YYYY-MM-DDTHH:mm"), + }) + } + allowTime + errorClassName="hidden" /> {dayjs().diff(state.form.encounter_date, "day") > 30 && (
@@ -1252,16 +1271,18 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { )} ref={fieldRef["icu_admission_date"]} > - + field("icu_admission_date").onChange({ + ...e, + value: dayjs(e.value).format("YYYY-MM-DDTHH:mm"), + }) } + allowTime + errorClassName="hidden" />
)} diff --git a/src/components/Facility/Consultations/Beds.tsx b/src/components/Facility/Consultations/Beds.tsx index 4c8409fbebf..5d43ffff7fd 100644 --- a/src/components/Facility/Consultations/Beds.tsx +++ b/src/components/Facility/Consultations/Beds.tsx @@ -12,7 +12,6 @@ import CareIcon from "../../../CAREUI/icons/CareIcon"; import CircularProgress from "@/components/Common/components/CircularProgress"; import { FieldLabel } from "../../Form/FormFields/FormField"; import Loading from "@/components/Common/Loading"; -import TextFormField from "../../Form/FormFields/TextFormField"; import dayjs from "../../../Utils/dayjs"; import { AssetSelect } from "@/components/Common/AssetSelect"; import DialogModal from "@/components/Common/Dialog"; @@ -24,6 +23,7 @@ import { } from "../../Assets/AssetTypes"; import Chip from "../../../CAREUI/display/Chip"; import BedActivityTimeline from "./BedActivityTimeline"; +import DateFormField from "../../Form/FormFields/DateFormField.js"; interface BedsProps { facilityId: string; @@ -204,16 +204,18 @@ const Beds = (props: BedsProps) => { unoccupiedOnly />
- setStartDate(e.value)} - max={dayjs().format("YYYY-MM-DDTHH:mm")} + value={startDate ? new Date(startDate) : new Date()} + onChange={(e) => + setStartDate(dayjs(e.value).format("YYYY-MM-DDTHH:mm")) + } + max={new Date()} error="" errorClassName="hidden" + allowTime />
Link Assets diff --git a/src/components/Facility/Consultations/DailyRoundsFilter.tsx b/src/components/Facility/Consultations/DailyRoundsFilter.tsx index 2df3b506d68..503952eacc1 100644 --- a/src/components/Facility/Consultations/DailyRoundsFilter.tsx +++ b/src/components/Facility/Consultations/DailyRoundsFilter.tsx @@ -6,13 +6,13 @@ import { } from "@headlessui/react"; import ButtonV2 from "@/components/Common/components/ButtonV2"; import { SelectFormField } from "../../Form/FormFields/SelectFormField"; -import TextFormField from "../../Form/FormFields/TextFormField"; import CareIcon from "../../../CAREUI/icons/CareIcon"; import dayjs from "dayjs"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import { DailyRoundTypes, DailyRoundsModel } from "../../Patient/models"; import { FieldChangeEvent } from "../../Form/FormFields/Utils"; +import DateFormField from "../../Form/FormFields/DateFormField"; type FilterState = { rounds_type?: DailyRoundsModel["rounds_type"]; @@ -77,17 +77,41 @@ export default function DailyRoundsFilter(props: Props) { optionLabel={(o) => t(`ROUNDS_TYPE__${o}`)} optionValue={(o) => o} /> - + field("taken_at_after").onChange({ + ...e, + value: dayjs(e.value).format("YYYY-MM-DDTHH:mm"), + }) + } + max={new Date()} + errorClassName="hidden" + allowTime /> - + field("taken_at_before").onChange({ + ...e, + value: dayjs(e.value).format("YYYY-MM-DDTHH:mm"), + }) + } + max={new Date()} + errorClassName="hidden" + allowTime /> diff --git a/src/components/Facility/DischargeModal.tsx b/src/components/Facility/DischargeModal.tsx index 122aacd1368..b5c15dc98d2 100644 --- a/src/components/Facility/DischargeModal.tsx +++ b/src/components/Facility/DischargeModal.tsx @@ -29,6 +29,7 @@ import routes from "../../Redux/api"; import { EditDiagnosesBuilder } from "../Diagnosis/ConsultationDiagnosisBuilder/ConsultationDiagnosisBuilder"; import Loading from "@/components/Common/Loading"; import careConfig from "@careConfig"; +import DateFormField from "../Form/FormFields/DateFormField"; import request from "../../Utils/request/request"; interface PreDischargeFormInterface { @@ -210,6 +211,13 @@ const DischargeModal = ({ const confirmationRequired = encounterDuration.asDays() >= 30; + const dischargeOrDeathTime = + preDischargeForm[ + discharge_reason === + DISCHARGE_REASONS.find((i) => i.text == "Expired")?.id + ? "death_datetime" + : "discharge_date" + ]; if (initialDiagnoses == null) { return ; } @@ -297,7 +305,7 @@ const DischargeModal = ({ />
)} - i.text == "Expired")?.id @@ -310,34 +318,28 @@ const DischargeModal = ({ ? "Date of Death" : "Date and Time of Discharge" } - type="datetime-local" value={ - preDischargeForm[ - discharge_reason === - DISCHARGE_REASONS.find((i) => i.text == "Expired")?.id - ? "death_datetime" - : "discharge_date" - ] + dischargeOrDeathTime ? new Date(dischargeOrDeathTime) : new Date() } + popOverClassName="max-h-[50vh]" onChange={(e) => { const updates: Record = { discharge_date: undefined, death_datetime: undefined, }; - updates[e.name] = e.value; + updates[e.name] = dayjs(e.value).format("YYYY-MM-DDTHH:mm"); setPreDischargeForm((form) => ({ ...form, ...updates })); }} required - min={dayjs(consultationData?.encounter_date).format( - "YYYY-MM-DDTHH:mm", - )} - max={dayjs().format("YYYY-MM-DDTHH:mm")} + min={new Date(consultationData?.encounter_date)} + max={new Date()} error={ discharge_reason === DISCHARGE_REASONS.find((i) => i.text == "Expired")?.id ? errors?.death_datetime : errors?.discharge_date } + allowTime /> {discharge_reason !== diff --git a/src/components/Facility/TriageForm.tsx b/src/components/Facility/TriageForm.tsx index 72b391f2318..fe4499e3758 100644 --- a/src/components/Facility/TriageForm.tsx +++ b/src/components/Facility/TriageForm.tsx @@ -250,7 +250,6 @@ export const TriageForm = ({ facilityId, id }: Props) => { value={state.form.entry_date} disableFuture onChange={handleFormFieldChange} - position="LEFT" placeholder="Entry Date" error={state.errors.entry_date} /> diff --git a/src/components/Form/FormFields/DateFormField.tsx b/src/components/Form/FormFields/DateFormField.tsx index 674d2572b6d..c27e7421cbb 100644 --- a/src/components/Form/FormFields/DateFormField.tsx +++ b/src/components/Form/FormFields/DateFormField.tsx @@ -1,6 +1,4 @@ -import DateInputV2, { - DatePickerPosition, -} from "@/components/Common/DateInputV2"; +import DateInputV2 from "@/components/Common/DateInputV2"; import { FormFieldBaseProps, useFormFieldPropsResolver } from "./Utils"; import FormField from "./FormField"; @@ -11,9 +9,10 @@ type Props = FormFieldBaseProps & { placeholder?: string; max?: Date; min?: Date; - position?: DatePickerPosition; disableFuture?: boolean; disablePast?: boolean; + allowTime?: boolean; + popOverClassName?: string; }; /** @@ -44,12 +43,13 @@ const DateFormField = (props: Props) => { ? new Date(field.value) : field.value } - onChange={field.handleChange} + onChange={field.handleChange as (d?: Date) => void} disabled={field.disabled} max={props.max ?? (props.disableFuture ? new Date() : undefined)} min={props.min ?? (props.disablePast ? yesterday() : undefined)} - position={props.position ?? "RIGHT"} placeholder={props.placeholder} + allowTime={props.allowTime} + popOverClassName={props.popOverClassName} /> ); diff --git a/src/components/Form/FormFields/DateRangeFormField.tsx b/src/components/Form/FormFields/DateRangeFormField.tsx index d7b7a62dbce..a5e814278b8 100644 --- a/src/components/Form/FormFields/DateRangeFormField.tsx +++ b/src/components/Form/FormFields/DateRangeFormField.tsx @@ -10,6 +10,7 @@ type Props = FormFieldBaseProps & { min?: Date; disableFuture?: boolean; disablePast?: boolean; + allowTime?: boolean; }; /** @@ -23,6 +24,7 @@ type Props = FormFieldBaseProps & { * label="Predicted date of birth" * required * disablePast // equivalent to min={new Date()} + * time={true} // allows picking time as well * /> * ``` */ @@ -38,6 +40,7 @@ const DateRangeFormField = (props: Props) => { disabled={field.disabled} min={props.min || (props.disableFuture ? new Date() : undefined)} max={props.max || (props.disablePast ? new Date() : undefined)} + allowTime={props.allowTime} /> ); diff --git a/src/components/Form/FormFields/Utils.ts b/src/components/Form/FormFields/Utils.ts index ed01f01c4ab..d7d2848a51e 100644 --- a/src/components/Form/FormFields/Utils.ts +++ b/src/components/Form/FormFields/Utils.ts @@ -1,3 +1,4 @@ +import { FocusEvent } from "react"; import { FieldError } from "../FieldValidators"; export type FieldChangeEvent = { name: string; value: T }; @@ -29,6 +30,8 @@ export type FormFieldBaseProps = { onChange: FieldChangeEventHandler; value?: T; error?: FieldError; + onFocus?: (event: FocusEvent) => void; + onBlur?: (event: FocusEvent) => void; }; /** diff --git a/src/components/Medicine/AdministerMedicine.tsx b/src/components/Medicine/AdministerMedicine.tsx index 2813999895b..13c6f4bab1a 100644 --- a/src/components/Medicine/AdministerMedicine.tsx +++ b/src/components/Medicine/AdministerMedicine.tsx @@ -8,13 +8,13 @@ import CareIcon from "../../CAREUI/icons/CareIcon"; import { formatDateTime } from "../../Utils/utils"; import { useTranslation } from "react-i18next"; import CheckBoxFormField from "../Form/FormFields/CheckBoxFormField"; -import TextFormField from "../Form/FormFields/TextFormField"; import dayjs from "../../Utils/dayjs"; import useSlug from "@/common/hooks/useSlug"; import request from "../../Utils/request/request"; import MedicineRoutes from "./routes"; import DosageFormField from "../Form/FormFields/DosageFormField"; import { AdministrationDosageValidator } from "./validators"; +import DateFormField from "../Form/FormFields/DateFormField"; interface Props { prescription: Prescription; @@ -141,14 +141,17 @@ export default function AdministerMedicine({ prescription, ...props }: Props) { }} errorClassName="hidden" /> - setCustomTime(value)} + value={customTime ? new Date(customTime) : new Date()} + onChange={({ value }) => + setCustomTime(dayjs(value).format("YYYY-MM-DDTHH:mm")) + } disabled={!isCustomTime} - min={dayjs(prescription.created_date).format("YYYY-MM-DDTHH:mm")} - max={dayjs().format("YYYY-MM-DDTHH:mm")} + min={new Date(prescription.created_date)} + max={new Date()} + errorClassName="hidden" + allowTime /> diff --git a/src/components/Medicine/MedicineAdministration.tsx b/src/components/Medicine/MedicineAdministration.tsx index 1b29fa7d108..d2daf370207 100644 --- a/src/components/Medicine/MedicineAdministration.tsx +++ b/src/components/Medicine/MedicineAdministration.tsx @@ -9,12 +9,12 @@ import { Error, Success } from "../../Utils/Notifications"; import { formatDateTime } from "../../Utils/utils"; import { useTranslation } from "react-i18next"; import dayjs from "../../Utils/dayjs"; -import TextFormField from "../Form/FormFields/TextFormField"; import request from "../../Utils/request/request"; import MedicineRoutes from "./routes"; import useSlug from "@/common/hooks/useSlug"; import DosageFormField from "../Form/FormFields/DosageFormField"; import { AdministrationDosageValidator } from "./validators"; +import DateFormField from "../Form/FormFields/DateFormField"; interface Props { prescriptions: Prescription[]; @@ -179,7 +179,7 @@ export default function MedicineAdministration(props: Props) { } errorClassName="hidden" /> -
+
- { setCustomTime((arr) => { const newArr = [...arr]; - newArr[index] = value; + newArr[index] = dayjs(value).format("YYYY-MM-DDTHH:mm"); return newArr; }); }} disabled={!shouldAdminister[index] || !isCustomTime[index]} - min={dayjs(obj.created_date).format("YYYY-MM-DDTHH:mm")} - max={dayjs().format("YYYY-MM-DDTHH:mm")} + min={new Date(obj.created_date)} + max={new Date()} + className="w-full" + errorClassName="hidden" + allowTime />
diff --git a/src/components/Patient/DailyRounds.tsx b/src/components/Patient/DailyRounds.tsx index d37e942c109..f6ac117c2ec 100644 --- a/src/components/Patient/DailyRounds.tsx +++ b/src/components/Patient/DailyRounds.tsx @@ -29,7 +29,6 @@ import Page from "@/components/Common/components/Page"; import RangeAutocompleteFormField from "../Form/FormFields/RangeAutocompleteFormField"; import { SelectFormField } from "../Form/FormFields/SelectFormField"; import TextAreaFormField from "../Form/FormFields/TextAreaFormField"; -import TextFormField from "../Form/FormFields/TextFormField"; import { FieldChangeEvent } from "../Form/FormFields/Utils"; import PatientCategorySelect from "./PatientCategorySelect"; import RadioFormField from "../Form/FormFields/RadioFormField"; @@ -55,6 +54,7 @@ import CheckBoxFormField from "../Form/FormFields/CheckBoxFormField"; import SymptomsApi from "../Symptoms/api"; import { scrollTo } from "../../Utils/utils"; import { ICD11DiagnosisModel } from "../Facility/models"; +import DateFormField from "../Form/FormFields/DateFormField"; import NursingCare from "../LogUpdate/Sections/NursingCare"; import Loading from "@/components/Common/Loading"; @@ -634,16 +634,24 @@ export const DailyRounds = (props: any) => { />
- + handleFormFieldChange({ + ...e, + value: dayjs(e.value).format("YYYY-MM-DDTHH:mm"), + }) + } + allowTime + errorClassName="hidden" />
diff --git a/src/components/Patient/PatientRegister.tsx b/src/components/Patient/PatientRegister.tsx index 77f43c04251..6aab7799772 100644 --- a/src/components/Patient/PatientRegister.tsx +++ b/src/components/Patient/PatientRegister.tsx @@ -1219,7 +1219,6 @@ export const PatientRegister = (props: PatientRegisterProps) => { {...field("date_of_birth")} errorClassName="hidden" required - position="LEFT" disableFuture />
@@ -1330,7 +1329,6 @@ export const PatientRegister = (props: PatientRegisterProps) => { containerClassName="w-full" {...field("last_menstruation_start_date")} label="Last Menstruation Start Date" - position="LEFT" disableFuture required /> @@ -1355,7 +1353,6 @@ export const PatientRegister = (props: PatientRegisterProps) => { containerClassName="w-full" {...field("date_of_delivery")} label="Date of Delivery" - position="LEFT" disableFuture required /> @@ -1727,7 +1724,6 @@ export const PatientRegister = (props: PatientRegisterProps) => { {...field("last_vaccinated_date")} label="Last Date of Vaccination" disableFuture={true} - position="LEFT" />
@@ -1759,7 +1755,6 @@ export const PatientRegister = (props: PatientRegisterProps) => { {...field("date_declared_positive")} label="Date Patient is Declared Positive for COVID" disableFuture - position="LEFT" /> @@ -1770,7 +1765,6 @@ export const PatientRegister = (props: PatientRegisterProps) => { id="date_of_test" label="Date of Sample given for COVID Test" disableFuture - position="LEFT" /> diff --git a/src/components/Symptoms/SymptomsBuilder.tsx b/src/components/Symptoms/SymptomsBuilder.tsx index 1be74797278..3676e2ad607 100644 --- a/src/components/Symptoms/SymptomsBuilder.tsx +++ b/src/components/Symptoms/SymptomsBuilder.tsx @@ -191,7 +191,6 @@ const SymptomEntry = (props: { name="cure_date" value={symptom.cure_date ? new Date(symptom.cure_date) : undefined} disableFuture - position="CENTER" placeholder="Date of cure" min={new Date(symptom.onset_date)} disabled={disabled} @@ -293,7 +292,6 @@ const AddSymptom = (props: { return (
{ required value={getDate(state.form.date_of_birth)} onChange={handleDateChange} - position="LEFT" disableFuture /> diff --git a/src/components/Users/UserProfile.tsx b/src/components/Users/UserProfile.tsx index b84ad7e2bec..edbb5dafdfa 100644 --- a/src/components/Users/UserProfile.tsx +++ b/src/components/Users/UserProfile.tsx @@ -777,7 +777,6 @@ export default function UserProfile() { required className="col-span-6 sm:col-span-3" value={getDate(states.form.date_of_birth)} - position="LEFT" disableFuture={true} /> Date: Mon, 28 Oct 2024 13:20:40 +0530 Subject: [PATCH 09/15] Added UserAvatar in userlist and users page (#8912) --- .../Common/UserAutocompleteFormField.tsx | 1 + .../Form/FormFields/Autocomplete.tsx | 54 +++++++++++++------ src/components/Users/ManageUsers.tsx | 10 +++- src/components/Users/models.tsx | 1 + 4 files changed, 48 insertions(+), 18 deletions(-) diff --git a/src/components/Common/UserAutocompleteFormField.tsx b/src/components/Common/UserAutocompleteFormField.tsx index 2bf5c49ffc8..a82174ac25d 100644 --- a/src/components/Common/UserAutocompleteFormField.tsx +++ b/src/components/Common/UserAutocompleteFormField.tsx @@ -80,6 +80,7 @@ export default function UserAutocomplete(props: UserSearchProps) { )} optionLabel={formatName} optionIcon={userOnlineDot} + optionImage={(option) => option.read_profile_picture_url} optionDescription={(option) => `${option.user_type} - ${option.username}` } diff --git a/src/components/Form/FormFields/Autocomplete.tsx b/src/components/Form/FormFields/Autocomplete.tsx index a93186ef080..9c9537a7901 100644 --- a/src/components/Form/FormFields/Autocomplete.tsx +++ b/src/components/Form/FormFields/Autocomplete.tsx @@ -14,7 +14,7 @@ import FormField from "./FormField"; import { classNames } from "../../../Utils/utils"; import { dropdownOptionClassNames } from "../MultiSelectMenuV2"; import { useTranslation } from "react-i18next"; - +import { Avatar } from "@/components/Common/Avatar"; type OptionCallback = (option: T) => R; type AutocompleteFormFieldProps = FormFieldBaseProps & { @@ -24,6 +24,7 @@ type AutocompleteFormFieldProps = FormFieldBaseProps & { optionValue?: OptionCallback; optionDescription?: OptionCallback; optionIcon?: OptionCallback; + optionImage?: OptionCallback; optionDisabled?: OptionCallback; minQueryLength?: number; onQuery?: (query: string) => void; @@ -50,6 +51,7 @@ const AutocompleteFormField = ( placeholder={props.placeholder} optionLabel={props.optionLabel} optionIcon={props.optionIcon} + optionImage={props.optionImage} optionValue={props.optionValue} optionDescription={props.optionDescription} optionDisabled={props.optionDisabled} @@ -74,6 +76,7 @@ type AutocompleteProps = { placeholder?: string; optionLabel: OptionCallback; optionIcon?: OptionCallback; + optionImage?: OptionCallback; optionValue?: OptionCallback; optionDescription?: OptionCallback; optionDisabled?: OptionCallback; @@ -118,6 +121,7 @@ export const Autocomplete = (props: AutocompleteProps) => { description, search: label.toLowerCase(), icon: props.optionIcon?.(option), + image: props.optionImage?.(option), value: props.optionValue ? props.optionValue(option) : option, disabled: props.optionDisabled?.(option), }; @@ -137,6 +141,7 @@ export const Autocomplete = (props: AutocompleteProps) => { description: undefined, search: query.toLowerCase(), icon: , + image: undefined, value: query, disabled: undefined, }, @@ -236,24 +241,39 @@ export const Autocomplete = (props: AutocompleteProps) => { > {({ focus }) => (
-
- {option.label} - {option.icon} -
- {option.description && ( -
+
+
+ + + {option.icon} + +
+
+
+
+ {option.label} +
+ {option.description && ( +
+ {option.description} +
)} - > - {option.description}
- )} +
)} diff --git a/src/components/Users/ManageUsers.tsx b/src/components/Users/ManageUsers.tsx index 7dc3eb8b5fe..6860a52f236 100644 --- a/src/components/Users/ManageUsers.tsx +++ b/src/components/Users/ManageUsers.tsx @@ -16,6 +16,7 @@ import request from "../../Utils/request/request"; import useQuery from "../../Utils/request/useQuery"; import { classNames, + formatDisplayName, formatName, isUserOnline, relativeTime, @@ -36,7 +37,7 @@ import UnlinkFacilityDialog from "./UnlinkFacilityDialog"; import UserDeleteDialog from "./UserDeleteDialog"; import UserFilter from "./UserFilter"; import { showUserDelete } from "../../Utils/permissions"; - +import { Avatar } from "../Common/Avatar"; import Loading from "@/components/Common/Loading"; export default function ManageUsers() { const { t } = useTranslation(); @@ -196,6 +197,13 @@ export default function ManageUsers() {
+
+ +
{user.username && (
Date: Mon, 28 Oct 2024 17:59:13 +0530 Subject: [PATCH 10/15] Show avatar in autocomplete conditionally (#8948) --- src/components/Common/UserAutocompleteFormField.tsx | 1 + src/components/Form/FormFields/Autocomplete.tsx | 13 ++++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/components/Common/UserAutocompleteFormField.tsx b/src/components/Common/UserAutocompleteFormField.tsx index a82174ac25d..626e0cca10f 100644 --- a/src/components/Common/UserAutocompleteFormField.tsx +++ b/src/components/Common/UserAutocompleteFormField.tsx @@ -81,6 +81,7 @@ export default function UserAutocomplete(props: UserSearchProps) { optionLabel={formatName} optionIcon={userOnlineDot} optionImage={(option) => option.read_profile_picture_url} + avatar optionDescription={(option) => `${option.user_type} - ${option.username}` } diff --git a/src/components/Form/FormFields/Autocomplete.tsx b/src/components/Form/FormFields/Autocomplete.tsx index 9c9537a7901..e7f53988fce 100644 --- a/src/components/Form/FormFields/Autocomplete.tsx +++ b/src/components/Form/FormFields/Autocomplete.tsx @@ -87,6 +87,7 @@ type AutocompleteProps = { isLoading?: boolean; allowRawInput?: boolean; error?: string; + avatar?: boolean; } & ( | { required?: false; @@ -244,11 +245,13 @@ export const Autocomplete = (props: AutocompleteProps) => {
- + {(option.image || props.avatar) && ( + + )} {option.icon} From 0925884508c14fdb9ab13c8b42102c7315492f2e Mon Sep 17 00:00:00 2001 From: JavidSumra <112365664+JavidSumra@users.noreply.github.com> Date: Tue, 29 Oct 2024 19:54:08 +0530 Subject: [PATCH 11/15] Fix Export Button Overflow (#8931) --- src/components/Patient/ManagePatients.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Patient/ManagePatients.tsx b/src/components/Patient/ManagePatients.tsx index 22399b081f1..2c1119be7e8 100644 --- a/src/components/Patient/ManagePatients.tsx +++ b/src/components/Patient/ManagePatients.tsx @@ -834,7 +834,7 @@ export const PatientManager = () => { >

- Add Patient Details + Add Patient

From a971668812a18084d8528d0edca1176d8c24df27 Mon Sep 17 00:00:00 2001 From: Aditya Jindal Date: Wed, 30 Oct 2024 05:36:16 +0530 Subject: [PATCH 12/15] Removed Avatar from AutoCompleteField (#8959) --- .../Common/UserAutocompleteFormField.tsx | 2 - .../Form/FormFields/Autocomplete.tsx | 55 ++++++------------- 2 files changed, 16 insertions(+), 41 deletions(-) diff --git a/src/components/Common/UserAutocompleteFormField.tsx b/src/components/Common/UserAutocompleteFormField.tsx index 626e0cca10f..2bf5c49ffc8 100644 --- a/src/components/Common/UserAutocompleteFormField.tsx +++ b/src/components/Common/UserAutocompleteFormField.tsx @@ -80,8 +80,6 @@ export default function UserAutocomplete(props: UserSearchProps) { )} optionLabel={formatName} optionIcon={userOnlineDot} - optionImage={(option) => option.read_profile_picture_url} - avatar optionDescription={(option) => `${option.user_type} - ${option.username}` } diff --git a/src/components/Form/FormFields/Autocomplete.tsx b/src/components/Form/FormFields/Autocomplete.tsx index e7f53988fce..0ed6bd4d1f1 100644 --- a/src/components/Form/FormFields/Autocomplete.tsx +++ b/src/components/Form/FormFields/Autocomplete.tsx @@ -14,7 +14,6 @@ import FormField from "./FormField"; import { classNames } from "../../../Utils/utils"; import { dropdownOptionClassNames } from "../MultiSelectMenuV2"; import { useTranslation } from "react-i18next"; -import { Avatar } from "@/components/Common/Avatar"; type OptionCallback = (option: T) => R; type AutocompleteFormFieldProps = FormFieldBaseProps & { @@ -24,7 +23,6 @@ type AutocompleteFormFieldProps = FormFieldBaseProps & { optionValue?: OptionCallback; optionDescription?: OptionCallback; optionIcon?: OptionCallback; - optionImage?: OptionCallback; optionDisabled?: OptionCallback; minQueryLength?: number; onQuery?: (query: string) => void; @@ -51,7 +49,6 @@ const AutocompleteFormField = ( placeholder={props.placeholder} optionLabel={props.optionLabel} optionIcon={props.optionIcon} - optionImage={props.optionImage} optionValue={props.optionValue} optionDescription={props.optionDescription} optionDisabled={props.optionDisabled} @@ -76,7 +73,6 @@ type AutocompleteProps = { placeholder?: string; optionLabel: OptionCallback; optionIcon?: OptionCallback; - optionImage?: OptionCallback; optionValue?: OptionCallback; optionDescription?: OptionCallback; optionDisabled?: OptionCallback; @@ -122,7 +118,6 @@ export const Autocomplete = (props: AutocompleteProps) => { description, search: label.toLowerCase(), icon: props.optionIcon?.(option), - image: props.optionImage?.(option), value: props.optionValue ? props.optionValue(option) : option, disabled: props.optionDisabled?.(option), }; @@ -142,7 +137,6 @@ export const Autocomplete = (props: AutocompleteProps) => { description: undefined, search: query.toLowerCase(), icon: , - image: undefined, value: query, disabled: undefined, }, @@ -242,41 +236,24 @@ export const Autocomplete = (props: AutocompleteProps) => { > {({ focus }) => (
-
-
-
- {(option.image || props.avatar) && ( - - )} - - {option.icon} - -
-
-
-
- {option.label} -
- {option.description && ( -
- {option.description} -
+
+ {option.label} + {option.icon} +
+ {option.description && ( +
+ {option.description}
-
+ )}
)} From c9343d5b56c4236fe3a88d43d2da4ddafcfd0892 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Wed, 30 Oct 2024 09:53:10 +0530 Subject: [PATCH 13/15] update lodash imports to be tree shakable (#8962) --- src/Utils/Notifications.js | 4 ++-- src/components/Common/ExcelFIleDragAndDrop.tsx | 4 ++-- .../Investigations/InvestigationsPrintPreview.tsx | 1 - .../Facility/Investigations/Reports/index.tsx | 4 ++-- .../Facility/Investigations/Reports/utils.tsx | 12 ++++++------ .../Facility/Investigations/ShowInvestigation.tsx | 6 +++--- src/components/Patient/PatientRegister.tsx | 4 ++-- src/components/Patient/SampleDetails.tsx | 10 +++++----- src/components/Patient/SampleTestCard.tsx | 6 +++--- 9 files changed, 25 insertions(+), 26 deletions(-) diff --git a/src/Utils/Notifications.js b/src/Utils/Notifications.js index 87de7991a6e..ce57ab5edd9 100644 --- a/src/Utils/Notifications.js +++ b/src/Utils/Notifications.js @@ -1,6 +1,6 @@ import { alert, Stack, defaultModules } from "@pnotify/core"; import * as PNotifyMobile from "@pnotify/mobile"; -import _ from "lodash-es"; +import { startCase, camelCase } from "lodash-es"; defaultModules.set(PNotifyMobile, {}); @@ -44,7 +44,7 @@ const notifyError = (error) => { errorMsg = error.detail; } else { for (let [key, value] of Object.entries(error)) { - let keyName = _.startCase(_.camelCase(key)); + let keyName = startCase(camelCase(key)); if (Array.isArray(value)) { const uniques = [...new Set(value)]; errorMsg += `${keyName} - ${uniques.splice(0, 5).join(", ")}`; diff --git a/src/components/Common/ExcelFIleDragAndDrop.tsx b/src/components/Common/ExcelFIleDragAndDrop.tsx index 566c8d0e600..d79cab62d34 100644 --- a/src/components/Common/ExcelFIleDragAndDrop.tsx +++ b/src/components/Common/ExcelFIleDragAndDrop.tsx @@ -1,4 +1,4 @@ -import * as _ from "lodash-es"; +import { forIn } from "lodash-es"; import { useEffect, useRef, useState } from "react"; import * as Notification from "../../Utils/Notifications"; import { useTranslation } from "react-i18next"; @@ -63,7 +63,7 @@ export default function ExcelFileDragAndDrop({ const data = XLSX.utils.sheet_to_json(worksheet, { defval: "" }); //converts the date to string data.forEach((row: any) => { - _.forIn(row, (value: any, key: string) => { + forIn(row, (value: any, key: string) => { if (value instanceof Date) { row[key] = value.toISOString().split("T")[0]; } diff --git a/src/components/Facility/Investigations/InvestigationsPrintPreview.tsx b/src/components/Facility/Investigations/InvestigationsPrintPreview.tsx index 107b0831bed..56f6f4a14e5 100644 --- a/src/components/Facility/Investigations/InvestigationsPrintPreview.tsx +++ b/src/components/Facility/Investigations/InvestigationsPrintPreview.tsx @@ -1,4 +1,3 @@ -import * as _ from "lodash-es"; import { lazy } from "react"; import routes from "../../../Redux/api"; import useQuery from "../../../Utils/request/useQuery"; diff --git a/src/components/Facility/Investigations/Reports/index.tsx b/src/components/Facility/Investigations/Reports/index.tsx index e4e54c36994..246d50f94bd 100644 --- a/src/components/Facility/Investigations/Reports/index.tsx +++ b/src/components/Facility/Investigations/Reports/index.tsx @@ -1,7 +1,7 @@ import { useCallback, useReducer, useState } from "react"; import { InvestigationGroup, InvestigationType } from ".."; -import _ from "lodash"; +import { chain } from "lodash-es"; import { useTranslation } from "react-i18next"; import routes from "../../../../Redux/api"; import * as Notification from "../../../../Utils/Notifications"; @@ -172,7 +172,7 @@ const InvestigationReports = ({ id }: any) => { ), ); - const investigationList = _.chain(data) + const investigationList = chain(data) .flatMap((i) => i?.data?.results) .compact() .flatten() diff --git a/src/components/Facility/Investigations/Reports/utils.tsx b/src/components/Facility/Investigations/Reports/utils.tsx index c6c6a0f6d45..eed1afc571e 100644 --- a/src/components/Facility/Investigations/Reports/utils.tsx +++ b/src/components/Facility/Investigations/Reports/utils.tsx @@ -1,8 +1,8 @@ -import * as _ from "lodash-es"; +import { memoize, chain, findIndex } from "lodash-es"; import { InvestigationResponse } from "./types"; -export const transformData = _.memoize((data: InvestigationResponse) => { - const sessions = _.chain(data) +export const transformData = memoize((data: InvestigationResponse) => { + const sessions = chain(data) .map((value: any) => { return { ...value.session_object, @@ -13,14 +13,14 @@ export const transformData = _.memoize((data: InvestigationResponse) => { .uniqBy("session_external_id") .orderBy("session_created_date", "desc") .value(); - const groupByInvestigation = _.chain(data) + const groupByInvestigation = chain(data) .groupBy("investigation_object.external_id") .values() .value(); const reqData = groupByInvestigation.map((value: any) => { const sessionValues = Array.from({ length: sessions.length }); value.forEach((val: any) => { - const sessionIndex = _.findIndex(sessions, [ + const sessionIndex = findIndex(sessions, [ "session_external_id", val.session_object.session_external_id, ]); @@ -59,7 +59,7 @@ export const transformData = _.memoize((data: InvestigationResponse) => { return { sessions, data: reqData }; }); -export const getColorIndex = _.memoize( +export const getColorIndex = memoize( ({ max, min, value }: { min?: number; max?: number; value?: number }) => { if (!max && min && value) { // 1 => yellow color diff --git a/src/components/Facility/Investigations/ShowInvestigation.tsx b/src/components/Facility/Investigations/ShowInvestigation.tsx index 0755b3687cd..00fe2c33549 100644 --- a/src/components/Facility/Investigations/ShowInvestigation.tsx +++ b/src/components/Facility/Investigations/ShowInvestigation.tsx @@ -1,4 +1,4 @@ -import * as _ from "lodash-es"; +import { set, chain } from "lodash-es"; import { useCallback, useReducer } from "react"; import routes from "../../../Redux/api"; import * as Notification from "../../../Utils/Notifications"; @@ -89,7 +89,7 @@ export default function ShowInvestigation(props: ShowInvestigationProps) { const handleValueChange = (value: any, name: string) => { const changedFields = { ...state.changedFields }; - _.set(changedFields, name, value); + set(changedFields, name, value); dispatch({ type: "set_changed_fields", changedFields }); }; @@ -147,7 +147,7 @@ export default function ShowInvestigation(props: ShowInvestigationProps) { }; const handleUpdateCancel = useCallback(() => { - const changedValues = _.chain(state.initialValues) + const changedValues = chain(state.initialValues) .map((val: any, _key: string) => ({ id: val?.id, initialValue: val?.notes || val?.value || null, diff --git a/src/components/Patient/PatientRegister.tsx b/src/components/Patient/PatientRegister.tsx index 6aab7799772..a3a8287dd34 100644 --- a/src/components/Patient/PatientRegister.tsx +++ b/src/components/Patient/PatientRegister.tsx @@ -59,7 +59,7 @@ import Spinner from "@/components/Common/Spinner"; import TextAreaFormField from "../Form/FormFields/TextAreaFormField"; import TextFormField from "../Form/FormFields/TextFormField"; import TransferPatientDialog from "../Facility/TransferPatientDialog"; -import _ from "lodash"; +import { startCase, toLower } from "lodash-es"; import countryList from "@/common/static/countries.json"; import { debounce } from "lodash-es"; import request from "../../Utils/request/request"; @@ -635,7 +635,7 @@ export const PatientRegister = (props: PatientRegisterProps) => { ? formData.last_vaccinated_date : null : null, - name: _.startCase(_.toLower(formData.name)), + name: startCase(toLower(formData.name)), pincode: formData.pincode ? formData.pincode : undefined, gender: Number(formData.gender), nationality: formData.nationality, diff --git a/src/components/Patient/SampleDetails.tsx b/src/components/Patient/SampleDetails.tsx index 2e8b93c7bb4..aaf524ef0ae 100644 --- a/src/components/Patient/SampleDetails.tsx +++ b/src/components/Patient/SampleDetails.tsx @@ -5,7 +5,7 @@ import ButtonV2 from "@/components/Common/components/ButtonV2"; import Card from "../../CAREUI/display/Card"; import { FileUpload } from "../Files/FileUpload"; import Page from "@/components/Common/components/Page"; -import * as _ from "lodash-es"; +import { startCase, camelCase, capitalize } from "lodash-es"; import { formatDateTime, formatPatientAge } from "../../Utils/utils"; import { navigate } from "raviger"; @@ -244,11 +244,11 @@ export const SampleDetails = ({ id }: DetailRoute) => {
Status: {" "} - {_.startCase(_.camelCase(flow.status))} + {startCase(camelCase(flow.status))}
Label:{" "} - {_.capitalize(flow.notes)} + {capitalize(flow.notes)}
Created On :{" "} @@ -332,7 +332,7 @@ export const SampleDetails = ({ id }: DetailRoute) => { Doctor's Name:{" "} - {_.startCase(_.camelCase(sampleDetails.doctor_name))} + {startCase(camelCase(sampleDetails.doctor_name))}
)} {sampleDetails?.diagnosis && ( @@ -415,7 +415,7 @@ export const SampleDetails = ({ id }: DetailRoute) => { Sample Type:{" "} - {_.startCase(_.camelCase(sampleDetails.sample_type))} + {startCase(camelCase(sampleDetails.sample_type))}
)}
diff --git a/src/components/Patient/SampleTestCard.tsx b/src/components/Patient/SampleTestCard.tsx index ad54850ea10..e4b29350ff8 100644 --- a/src/components/Patient/SampleTestCard.tsx +++ b/src/components/Patient/SampleTestCard.tsx @@ -4,7 +4,7 @@ import { SampleTestModel } from "./models"; import { SAMPLE_TEST_STATUS } from "@/common/constants"; import * as Notification from "../../Utils/Notifications"; import UpdateStatusDialog from "./UpdateStatusDialog"; -import * as _ from "lodash-es"; +import { startCase, camelCase } from "lodash-es"; import { formatDateTime } from "../../Utils/utils"; import ButtonV2 from "@/components/Common/components/ButtonV2"; import { NonReadOnlyUsers } from "../../Utils/AuthorizeFor"; @@ -98,7 +98,7 @@ export const SampleTestCard = (props: SampleDetailsProps) => { Status{" "}
- {_.startCase(_.camelCase(itemData.status))} + {startCase(camelCase(itemData.status))}
@@ -133,7 +133,7 @@ export const SampleTestCard = (props: SampleDetailsProps) => { Result{" "}
- {_.startCase(_.camelCase(itemData.result))} + {startCase(camelCase(itemData.result))}
From 09b12e1ccc167f3ab806e765e6e8a32897711192 Mon Sep 17 00:00:00 2001 From: Shivank Kacker Date: Wed, 30 Oct 2024 13:03:24 +0530 Subject: [PATCH 14/15] Updated UI for list view for Resources and Shifting (#8934) --- src/Redux/api.tsx | 41 +-- src/Routers/routes/ResourceRoutes.tsx | 4 +- src/Routers/routes/ShiftingRoutes.tsx | 4 +- src/components/Facility/models.tsx | 66 +++- src/components/Resource/ListView.tsx | 235 ------------- .../{BadgesList.tsx => ResourceBadges.tsx} | 0 src/components/Resource/ResourceBlock.tsx | 96 +++++ ...esourceBoardView.tsx => ResourceBoard.tsx} | 100 +----- ...Section.tsx => ResourceCommentSection.tsx} | 6 +- .../{Commons.tsx => ResourceCommons.tsx} | 2 +- src/components/Resource/ResourceDetails.tsx | 4 +- .../{ListFilter.tsx => ResourceFilter.tsx} | 0 src/components/Resource/ResourceList.tsx | 138 ++++++++ src/components/Resource/models.ts | 41 --- src/components/Shifting/BoardView.tsx | 326 ----------------- src/components/Shifting/ListView.tsx | 332 ------------------ src/components/Shifting/ShiftDetails.tsx | 2 +- .../Shifting/ShiftDetailsUpdate.tsx | 5 +- .../{BadgesList.tsx => ShiftingBadges.tsx} | 2 +- src/components/Shifting/ShiftingBlock.tsx | 144 ++++++++ src/components/Shifting/ShiftingBoard.tsx | 183 ++++++++++ ...ection.tsx => ShiftingCommentsSection.tsx} | 6 +- .../{Commons.tsx => ShiftingCommons.tsx} | 2 +- .../{ListFilter.tsx => ShiftingFilters.tsx} | 0 src/components/Shifting/ShiftingList.tsx | 179 ++++++++++ src/components/Shifting/models.ts | 44 --- 26 files changed, 835 insertions(+), 1127 deletions(-) delete mode 100644 src/components/Resource/ListView.tsx rename src/components/Resource/{BadgesList.tsx => ResourceBadges.tsx} (100%) create mode 100644 src/components/Resource/ResourceBlock.tsx rename src/components/Resource/{ResourceBoardView.tsx => ResourceBoard.tsx} (54%) rename src/components/Resource/{CommentSection.tsx => ResourceCommentSection.tsx} (96%) rename src/components/Resource/{Commons.tsx => ResourceCommons.tsx} (97%) rename src/components/Resource/{ListFilter.tsx => ResourceFilter.tsx} (100%) create mode 100644 src/components/Resource/ResourceList.tsx delete mode 100644 src/components/Resource/models.ts delete mode 100644 src/components/Shifting/BoardView.tsx delete mode 100644 src/components/Shifting/ListView.tsx rename src/components/Shifting/{BadgesList.tsx => ShiftingBadges.tsx} (97%) create mode 100644 src/components/Shifting/ShiftingBlock.tsx create mode 100644 src/components/Shifting/ShiftingBoard.tsx rename src/components/Shifting/{CommentsSection.tsx => ShiftingCommentsSection.tsx} (96%) rename src/components/Shifting/{Commons.tsx => ShiftingCommons.tsx} (98%) rename src/components/Shifting/{ListFilter.tsx => ShiftingFilters.tsx} (100%) create mode 100644 src/components/Shifting/ShiftingList.tsx delete mode 100644 src/components/Shifting/models.ts diff --git a/src/Redux/api.tsx b/src/Redux/api.tsx index 22f0285d22d..adb0f98a7b2 100644 --- a/src/Redux/api.tsx +++ b/src/Redux/api.tsx @@ -17,6 +17,7 @@ import { import { BedModel, CapacityModal, + CommentModel, ConsultationModel, CreateBedBody, CurrentBed, @@ -41,6 +42,8 @@ import { PatientNotesModel, PatientStatsModel, PatientTransferResponse, + ResourceModel, + ShiftingModel, StateModel, WardModel, } from "@/components/Facility/models"; @@ -50,7 +53,6 @@ import { SampleReportModel, SampleTestModel, } from "@/components/Patient/models"; -import { IComment, IResource } from "@/components/Resource/models"; import { IDeleteBedCapacity, ILocalBodies, @@ -76,7 +78,6 @@ import { HCXPolicyModel, } from "@/components/HCX/models"; import { ICD11DiagnosisModel } from "@/components/Diagnosis/types"; -import { IShift } from "@/components/Shifting/models"; import { Investigation } from "@/components/Facility/Investigations/Reports/types"; import { PaginatedResponse } from "../Utils/request/types"; import { @@ -1059,14 +1060,14 @@ const routes = { createShift: { path: "/api/v1/shift/", method: "POST", - TBody: Type>(), + TBody: Type>(), TRes: Type(), }, updateShift: { path: "/api/v1/shift/{id}/", method: "PUT", - TBody: Type(), - TRes: Type(), + TBody: Type(), + TRes: Type(), }, deleteShiftRecord: { path: "/api/v1/shift/{id}/", @@ -1076,17 +1077,17 @@ const routes = { listShiftRequests: { path: "/api/v1/shift/", method: "GET", - TRes: Type>(), + TRes: Type>(), }, getShiftDetails: { path: "/api/v1/shift/{id}/", method: "GET", - TRes: Type(), + TRes: Type(), }, completeTransfer: { path: "/api/v1/shift/{externalId}/transfer/", method: "POST", - TBody: Type(), + TBody: Type(), TRes: Type>(), }, downloadShiftRequests: { @@ -1097,13 +1098,13 @@ const routes = { getShiftComments: { path: "/api/v1/shift/{id}/comment/", method: "GET", - TRes: Type>(), + TRes: Type>(), }, addShiftComments: { path: "/api/v1/shift/{id}/comment/", method: "POST", - TBody: Type>(), - TRes: Type(), + TBody: Type>(), + TRes: Type(), }, // Notifications @@ -1235,14 +1236,14 @@ const routes = { createResource: { path: "/api/v1/resource/", method: "POST", - TRes: Type(), - TBody: Type>(), + TRes: Type(), + TBody: Type>(), }, updateResource: { path: "/api/v1/resource/{id}/", method: "PUT", - TRes: Type(), - TBody: Type>(), + TRes: Type(), + TBody: Type>(), }, deleteResourceRecord: { path: "/api/v1/resource/{id}/", @@ -1254,12 +1255,12 @@ const routes = { listResourceRequests: { path: "/api/v1/resource/", method: "GET", - TRes: Type>(), + TRes: Type>(), }, getResourceDetails: { path: "/api/v1/resource/{id}/", method: "GET", - TRes: Type(), + TRes: Type(), }, downloadResourceRequests: { path: "/api/v1/resource/", @@ -1269,13 +1270,13 @@ const routes = { getResourceComments: { path: "/api/v1/resource/{id}/comment/", method: "GET", - TRes: Type>(), + TRes: Type>(), }, addResourceComments: { path: "/api/v1/resource/{id}/comment/", method: "POST", - TRes: Type(), - TBody: Type>(), + TRes: Type(), + TBody: Type>(), }, // Assets endpoints diff --git a/src/Routers/routes/ResourceRoutes.tsx b/src/Routers/routes/ResourceRoutes.tsx index a1cc70f9aa7..24e60ec5ad9 100644 --- a/src/Routers/routes/ResourceRoutes.tsx +++ b/src/Routers/routes/ResourceRoutes.tsx @@ -1,9 +1,9 @@ import ResourceDetails from "@/components/Resource/ResourceDetails"; import { ResourceDetailsUpdate } from "@/components/Resource/ResourceDetailsUpdate"; -import ListView from "@/components/Resource/ListView"; -import BoardView from "@/components/Resource/ResourceBoardView"; import { Redirect } from "raviger"; import { AppRoutes } from "../AppRouter"; +import BoardView from "@/components/Resource/ResourceBoard"; +import ListView from "@/components/Resource/ResourceList"; const getDefaultView = () => localStorage.getItem("defaultResourceView") === "list" ? "list" : "board"; diff --git a/src/Routers/routes/ShiftingRoutes.tsx b/src/Routers/routes/ShiftingRoutes.tsx index c4a3235857a..b5448cc527f 100644 --- a/src/Routers/routes/ShiftingRoutes.tsx +++ b/src/Routers/routes/ShiftingRoutes.tsx @@ -1,8 +1,8 @@ import { ShiftCreate } from "@/components/Patient/ShiftCreate"; import ShiftDetails from "@/components/Shifting/ShiftDetails"; import { ShiftDetailsUpdate } from "@/components/Shifting/ShiftDetailsUpdate"; -import ListView from "@/components/Shifting/ListView"; -import BoardView from "@/components/Shifting/BoardView"; +import ListView from "@/components/Shifting/ShiftingList"; +import BoardView from "@/components/Shifting/ShiftingBoard"; import { Redirect } from "raviger"; import { AppRoutes } from "../AppRouter"; diff --git a/src/components/Facility/models.tsx b/src/components/Facility/models.tsx index e64bbfda99f..37461b056e8 100644 --- a/src/components/Facility/models.tsx +++ b/src/components/Facility/models.tsx @@ -19,10 +19,11 @@ import { PatientModel, } from "../Patient/models"; import { EncounterSymptom } from "../Symptoms/types"; -import { UserBareMinimum, UserModel } from "../Users/models"; +import { UserBareMinimum } from "../Users/models"; import { InvestigationType } from "@/components/Common/prescription-builder/InvestigationBuilder"; import { ProcedureType } from "@/components/Common/prescription-builder/ProcedureBuilder"; import { RouteToFacility } from "@/components/Common/RouteToFacilitySelect"; +import { PerformedByModel } from "../HCX/misc"; export interface LocalBodyModel { id: number; @@ -681,20 +682,44 @@ export type PatientTransferResponse = { }; export interface ShiftingModel { - assigned_facility: string; + shifting_approving_facility_object: FacilityModel | null; + status: (typeof SHIFTING_CHOICES_PEACETIME)[number]["text"]; + id: string; + patient_object: PatientModel; + emergency: boolean; + origin_facility_object: FacilityModel; + origin_facility: string; + shifting_approving_facility: string; assigned_facility_external: string | null; + assigned_facility: string | null; + is_up_shift: boolean; + assigned_to: number; + patient_category: string; assigned_facility_object: FacilityModel; - created_date: string; - emergency: boolean; - external_id: string; - id: string; + assigned_facility_external_object: FacilityModel; modified_date: string; - origin_facility_object: FacilityModel; - patient: string; - patient_object: PatientModel; - shifting_approving_facility_object: FacilityModel | null; - status: (typeof SHIFTING_CHOICES_PEACETIME)[number]["text"]; - assigned_to_object?: UserModel; + external_id: string; + assigned_to_object?: AssignedToObjectModel; + refering_facility_contact_name: string; + refering_facility_contact_number: string; + is_kasp: boolean; + vehicle_preference: string; + preferred_vehicle_choice: string; + assigned_facility_type: string; + breathlessness_level: string; + reason: string; + ambulance_driver_name: string; + ambulance_phone_number: string | undefined; + ambulance_number: string; + comments: string; + created_date: string; + created_by_object: PerformedByModel; + last_edited_by_object: PerformedByModel; + is_assigned_to_user: boolean; + created_by: number; + last_edited_by: number; + patient: string | PatientModel; + initial_status?: string; } export interface ResourceModel { @@ -704,16 +729,12 @@ export interface ResourceModel { assigned_facility_object: FacilityModel | null; assigned_quantity: number; assigned_to: string | null; - assigned_to_object: UserModel | null; category: string; created_by: number; - created_by_object: UserModel; - created_date: string; emergency: boolean; id: string; is_assigned_to_user: boolean; last_edited_by: number; - last_edited_by_object: UserModel; modified_date: string; origin_facility: string; origin_facility_object: FacilityModel; @@ -725,4 +746,17 @@ export interface ResourceModel { status: string; sub_category: string; title: string; + assigned_to_object: PerformedByModel | null; + created_by_object: PerformedByModel | null; + created_date: string; + last_edited_by_object: PerformedByModel; +} + +export interface CommentModel { + id: string; + created_by_object: PerformedByModel; + created_date: string; + modified_date: string; + comment: string; + created_by: number; } diff --git a/src/components/Resource/ListView.tsx b/src/components/Resource/ListView.tsx deleted file mode 100644 index 8c05c1c665f..00000000000 --- a/src/components/Resource/ListView.tsx +++ /dev/null @@ -1,235 +0,0 @@ -import { navigate } from "raviger"; -import ListFilter from "./ListFilter"; -import { formatFilter } from "./Commons"; -import BadgesList from "./BadgesList"; -import { formatDateTime } from "../../Utils/utils"; -import useFilters from "@/common/hooks/useFilters"; -import { ExportButton } from "@/components/Common/Export"; -import ButtonV2 from "@/components/Common/components/ButtonV2"; -import { useTranslation } from "react-i18next"; -import { AdvancedFilterButton } from "../../CAREUI/interactive/FiltersSlideover"; -import CareIcon from "../../CAREUI/icons/CareIcon"; -import dayjs from "../../Utils/dayjs"; -import useQuery from "../../Utils/request/useQuery"; -import routes from "../../Redux/api"; -import Page from "@/components/Common/components/Page"; -import SearchInput from "../Form/SearchInput"; -import request from "../../Utils/request/request"; - -import Loading from "@/components/Common/Loading"; -export default function ListView() { - const { - qParams, - Pagination, - FilterBadges, - advancedFilter, - resultsPerPage, - updateQuery, - } = useFilters({ cacheBlacklist: ["title"] }); - - const { t } = useTranslation(); - - const onBoardViewBtnClick = () => - navigate("/resource/board", { query: qParams }); - const appliedFilters = formatFilter(qParams); - - const { loading, data, refetch } = useQuery(routes.listResourceRequests, { - query: formatFilter({ - ...qParams, - offset: (qParams.page ? qParams.page - 1 : 0) * resultsPerPage, - }), - }); - - const showResourceCardList = (data: any) => { - if (data && !data.length) { - return ( -
- No requests to show. -
- ); - } - - return data.map((resource: any) => ( -
-
-
-
-
-
- {resource.title} -
-
- {resource.emergency && ( - - Emergency - - )} -
-
-
-
-
- -
- {resource.status} -
- -
-
-
- -
- {(resource.origin_facility_object || {}).name} -
- -
-
-
- -
- {(resource.approving_facility_object || {}).name} -
- -
-
-
- - -
- {(resource.assigned_facility_object || {}).name || - "Yet to be decided"} -
- -
- -
-
- -
- {formatDateTime(resource.modified_date) || "--"} -
- -
-
-
- -
- -
-
-
-
- )); - }; - - return ( - { - const { data } = await request(routes.downloadResourceRequests, { - query: { ...appliedFilters, csv: true }, - }); - return data ?? null; - }} - filenamePrefix="resource_requests" - /> - } - breadcrumbs={false} - options={ - <> -
- updateQuery({ [e.name]: e.value })} - placeholder={t("search_resource")} - /> -
-
- {/* dummy div to align space as per board view */} -
-
- - - {t("board_view")} - - - advancedFilter.setShow(true)} - /> -
- - } - > - - -
- {loading ? ( - - ) : ( -
-
- -
- -
- {data?.results && showResourceCardList(data?.results)} -
-
- -
-
- )} -
- -
- ); -} diff --git a/src/components/Resource/BadgesList.tsx b/src/components/Resource/ResourceBadges.tsx similarity index 100% rename from src/components/Resource/BadgesList.tsx rename to src/components/Resource/ResourceBadges.tsx diff --git a/src/components/Resource/ResourceBlock.tsx b/src/components/Resource/ResourceBlock.tsx new file mode 100644 index 00000000000..df6fa14934f --- /dev/null +++ b/src/components/Resource/ResourceBlock.tsx @@ -0,0 +1,96 @@ +import { useTranslation } from "react-i18next"; +import { ResourceModel } from "../Facility/models"; +import { classNames, formatDateTime, formatName } from "@/Utils/utils"; +import dayjs from "dayjs"; +import CareIcon from "@/CAREUI/icons/CareIcon"; +import { Link } from "raviger"; + +export default function ResourceBlock(props: { resource: ResourceModel }) { + const { resource } = props; + const { t } = useTranslation(); + + return ( +
+
+
+
+
{resource.title}
+
+
+ {resource.emergency && ( + + {t("emergency")} + + )} +
+
+
+ {( + [ + { + title: "origin_facility", + icon: "l-plane-departure", + data: resource.origin_facility_object.name, + }, + { + title: "resource_approving_facility", + icon: "l-user-check", + data: resource.approving_facility_object?.name, + }, + { + title: "assigned_facility", + icon: "l-plane-arrival", + data: + resource.assigned_facility_object?.name || + t("yet_to_be_decided"), + }, + { + title: "last_modified", + icon: "l-stopwatch", + data: formatDateTime(resource.modified_date), + className: dayjs() + .subtract(2, "hours") + .isBefore(resource.modified_date) + ? "text-secondary-900" + : "rounded bg-red-500 border border-red-600 text-white w-full font-bold", + }, + { + title: "assigned_to", + icon: "l-user", + data: resource.assigned_to_object + ? formatName(resource.assigned_to_object) + + " - " + + resource.assigned_to_object.user_type + : undefined, + }, + ] as const + ) + .filter((d) => d.data) + .map((datapoint, i) => ( +
+
+ +
+
{datapoint.data}
+
+ ))} +
+
+
+ + {t("all_details")} + +
+
+ ); +} diff --git a/src/components/Resource/ResourceBoardView.tsx b/src/components/Resource/ResourceBoard.tsx similarity index 54% rename from src/components/Resource/ResourceBoardView.tsx rename to src/components/Resource/ResourceBoard.tsx index 8d5299fd3f8..0f790a8b3b6 100644 --- a/src/components/Resource/ResourceBoardView.tsx +++ b/src/components/Resource/ResourceBoard.tsx @@ -1,9 +1,9 @@ import { useState } from "react"; -import { Link, navigate } from "raviger"; -import ListFilter from "./ListFilter"; +import { navigate } from "raviger"; +import ListFilter from "./ResourceFilter"; import { RESOURCE_CHOICES } from "@/common/constants"; -import BadgesList from "./BadgesList"; -import { formatFilter } from "./Commons"; +import BadgesList from "./ResourceBadges"; +import { formatFilter } from "./ResourceCommons"; import useFilters from "@/common/hooks/useFilters"; import { ExportButton } from "@/components/Common/Export"; import ButtonV2 from "@/components/Common/components/ButtonV2"; @@ -16,10 +16,9 @@ import request from "../../Utils/request/request"; import routes from "../../Redux/api"; import KanbanBoard from "../Kanban/Board"; import { ResourceModel } from "../Facility/models"; -import { classNames, formatDateTime, formatName } from "../../Utils/utils"; -import dayjs from "dayjs"; import PageTitle from "@/components/Common/PageTitle"; +import ResourceBlock from "./ResourceBlock"; const resourceStatusOptions = RESOURCE_CHOICES.map((obj) => obj.text); const COMPLETED = ["COMPLETED", "REJECTED"]; @@ -133,94 +132,7 @@ export default function BoardView() { `/resource/${result.draggableId}/update?status=${result.destination?.droppableId}`, ); }} - itemRender={(resource) => ( -
-
-
-
-
- {resource.title} -
-
-
- {resource.emergency && ( - - {t("emergency")} - - )} -
-
-
- {( - [ - { - title: "origin_facility", - icon: "l-plane-departure", - data: resource.origin_facility_object.name, - }, - { - title: "resource_approving_facility", - icon: "l-user-check", - data: resource.approving_facility_object?.name, - }, - { - title: "assigned_facility", - icon: "l-plane-arrival", - data: - resource.assigned_facility_object?.name || - t("yet_to_be_decided"), - }, - { - title: "last_modified", - icon: "l-stopwatch", - data: formatDateTime(resource.modified_date), - className: dayjs() - .subtract(2, "hours") - .isBefore(resource.modified_date) - ? "text-secondary-900" - : "rounded bg-red-500 border border-red-600 text-white w-full font-bold", - }, - { - title: "assigned_to", - icon: "l-user", - data: resource.assigned_to_object - ? formatName(resource.assigned_to_object) + - " - " + - resource.assigned_to_object.user_type - : undefined, - }, - ] as const - ) - .filter((d) => d.data) - .map((datapoint, i) => ( -
-
- -
-
- {datapoint.data} -
-
- ))} -
-
-
- - {t("all_details")} - -
-
- )} + itemRender={(resource) => } />
diff --git a/src/components/Resource/CommentSection.tsx b/src/components/Resource/ResourceCommentSection.tsx similarity index 96% rename from src/components/Resource/CommentSection.tsx rename to src/components/Resource/ResourceCommentSection.tsx index 632d608cd14..a2c10864b3b 100644 --- a/src/components/Resource/CommentSection.tsx +++ b/src/components/Resource/ResourceCommentSection.tsx @@ -6,8 +6,8 @@ import ButtonV2 from "@/components/Common/components/ButtonV2"; import TextAreaFormField from "../Form/FormFields/TextAreaFormField"; import routes from "../../Redux/api"; import PaginatedList from "../../CAREUI/misc/PaginatedList"; -import { IComment } from "./models"; import request from "../../Utils/request/request"; +import { CommentModel } from "../Facility/models"; const CommentSection = (props: { id: string }) => { const [commentBox, setCommentBox] = useState(""); @@ -63,7 +63,7 @@ const CommentSection = (props: { id: string }) => { - > + > {(item) => }
@@ -83,7 +83,7 @@ export const Comment = ({ comment, created_by_object, modified_date, -}: IComment) => ( +}: CommentModel) => (

{comment}

diff --git a/src/components/Resource/Commons.tsx b/src/components/Resource/ResourceCommons.tsx similarity index 97% rename from src/components/Resource/Commons.tsx rename to src/components/Resource/ResourceCommons.tsx index 853aa611c61..e63eb9a71b2 100644 --- a/src/components/Resource/Commons.tsx +++ b/src/components/Resource/ResourceCommons.tsx @@ -29,7 +29,7 @@ export const formatFilter = (params: any) => { : filter.emergency === "yes" ? "true" : "false", - limit: 14, + limit: filter.limit || 14, offset: filter.offset, created_date_before: filter.created_date_before || undefined, created_date_after: filter.created_date_after || undefined, diff --git a/src/components/Resource/ResourceDetails.tsx b/src/components/Resource/ResourceDetails.tsx index 4cfe959b7b9..f3c318102a6 100644 --- a/src/components/Resource/ResourceDetails.tsx +++ b/src/components/Resource/ResourceDetails.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import { classNames, formatDateTime, formatName } from "../../Utils/utils"; import { navigate } from "raviger"; import * as Notification from "../../Utils/Notifications"; -import CommentSection from "./CommentSection"; +import CommentSection from "./ResourceCommentSection"; import ButtonV2 from "@/components/Common/components/ButtonV2"; import Page from "@/components/Common/components/Page"; import ConfirmDialog from "@/components/Common/ConfirmDialog"; @@ -357,7 +357,7 @@ export default function ResourceDetails(props: { id: string }) {
- {formatName(data.created_by_object)} + {data.created_by_object && formatName(data.created_by_object)}
{data.created_date && formatDateTime(data.created_date)} diff --git a/src/components/Resource/ListFilter.tsx b/src/components/Resource/ResourceFilter.tsx similarity index 100% rename from src/components/Resource/ListFilter.tsx rename to src/components/Resource/ResourceFilter.tsx diff --git a/src/components/Resource/ResourceList.tsx b/src/components/Resource/ResourceList.tsx new file mode 100644 index 00000000000..e7dffb1dfe2 --- /dev/null +++ b/src/components/Resource/ResourceList.tsx @@ -0,0 +1,138 @@ +import { navigate } from "raviger"; +import ListFilter from "./ResourceFilter"; +import { formatFilter } from "./ResourceCommons"; +import BadgesList from "./ResourceBadges"; +import useFilters from "@/common/hooks/useFilters"; +import { ExportButton } from "@/components/Common/Export"; +import ButtonV2 from "@/components/Common/components/ButtonV2"; +import { useTranslation } from "react-i18next"; +import { AdvancedFilterButton } from "../../CAREUI/interactive/FiltersSlideover"; +import CareIcon from "../../CAREUI/icons/CareIcon"; +import useQuery from "../../Utils/request/useQuery"; +import routes from "../../Redux/api"; +import Page from "@/components/Common/components/Page"; +import SearchInput from "../Form/SearchInput"; +import request from "../../Utils/request/request"; +import Loading from "@/components/Common/Loading"; +import { ResourceModel } from "../Facility/models"; +import ResourceBlock from "./ResourceBlock"; +export default function ListView() { + const { + qParams, + Pagination, + FilterBadges, + advancedFilter, + resultsPerPage, + updateQuery, + } = useFilters({ cacheBlacklist: ["title"], limit: 12 }); + + const { t } = useTranslation(); + + const onBoardViewBtnClick = () => + navigate("/resource/board", { query: qParams }); + const appliedFilters = formatFilter(qParams); + + const { loading, data, refetch } = useQuery(routes.listResourceRequests, { + query: formatFilter({ + ...qParams, + limit: resultsPerPage, + offset: (qParams.page ? qParams.page - 1 : 0) * resultsPerPage, + }), + }); + + const showResourceCardList = (data: ResourceModel[]) => { + if (data && !data.length) { + return ( +
+ {t("no_results_found")} +
+ ); + } + + return data.map((resource, i) => ( +
+ +
+ )); + }; + + return ( + { + const { data } = await request(routes.downloadResourceRequests, { + query: { ...appliedFilters, csv: true }, + }); + return data ?? null; + }} + filenamePrefix="resource_requests" + /> + } + breadcrumbs={false} + options={ + <> +
+ updateQuery({ [e.name]: e.value })} + placeholder={t("search_resource")} + /> +
+
+ {/* dummy div to align space as per board view */} +
+
+ + + {t("board_view")} + + + advancedFilter.setShow(true)} + /> +
+ + } + > + + +
+ {loading ? ( + + ) : ( +
+
+ +
+ +
+ {data?.results && showResourceCardList(data?.results)} +
+
+ +
+
+ )} +
+ +
+ ); +} diff --git a/src/components/Resource/models.ts b/src/components/Resource/models.ts deleted file mode 100644 index f10ac988552..00000000000 --- a/src/components/Resource/models.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { PerformedByModel } from "../HCX/misc"; - -export interface IComment { - id: string; - created_by_object: PerformedByModel; - created_date: string; - modified_date: string; - comment: string; - created_by: number; -} - -export interface IResource { - id: string; - title: string; - emergency: boolean; - status?: string; - origin_facility_object: { - name: string; - }; - approving_facility_object: { - name: string; - }; - assigned_facility_object: { - name: string; - }; - assigned_quantity: number; - modified_date: string; - category: any; - sub_category: number; - origin_facility: string; - approving_facility: string; - assigned_facility: string; - reason: string; - refering_facility_contact_name: string; - refering_facility_contact_number: string; - requested_quantity: number; - assigned_to_object: PerformedByModel; - created_by_object: PerformedByModel; - created_date: string; - last_edited_by_object: PerformedByModel; -} diff --git a/src/components/Shifting/BoardView.tsx b/src/components/Shifting/BoardView.tsx deleted file mode 100644 index d05fcab2a9e..00000000000 --- a/src/components/Shifting/BoardView.tsx +++ /dev/null @@ -1,326 +0,0 @@ -import { - SHIFTING_CHOICES_PEACETIME, - SHIFTING_CHOICES_WARTIME, -} from "@/common/constants"; - -import BadgesList from "./BadgesList"; -import { ExportButton } from "@/components/Common/Export"; -import ListFilter from "./ListFilter"; -import SearchInput from "../Form/SearchInput"; -import { formatFilter } from "./Commons"; - -import { Link, navigate } from "raviger"; -import useFilters from "@/common/hooks/useFilters"; -import { useState } from "react"; -import { useTranslation } from "react-i18next"; -import ButtonV2 from "@/components/Common/components/ButtonV2"; -import { AdvancedFilterButton } from "../../CAREUI/interactive/FiltersSlideover"; -import CareIcon from "../../CAREUI/icons/CareIcon"; -import Tabs from "@/components/Common/components/Tabs"; -import careConfig from "@careConfig"; -import KanbanBoard from "../Kanban/Board"; -import { classNames, formatDateTime, formatName } from "../../Utils/utils"; -import dayjs from "dayjs"; -import ConfirmDialog from "@/components/Common/ConfirmDialog"; -import { ShiftingModel } from "../Facility/models"; -import useAuthUser from "@/common/hooks/useAuthUser"; -import request from "../../Utils/request/request"; -import routes from "../../Redux/api"; -import PageTitle from "@/components/Common/PageTitle"; - -export default function BoardView() { - const { qParams, updateQuery, FilterBadges, advancedFilter } = useFilters({ - limit: -1, - cacheBlacklist: ["patient_name"], - }); - - const [modalFor, setModalFor] = useState<{ - externalId?: string; - loading: boolean; - }>({ - externalId: undefined, - loading: false, - }); - - const authUser = useAuthUser(); - - const handleTransferComplete = async (shift: any) => { - setModalFor({ ...modalFor, loading: true }); - await request(routes.completeTransfer, { - pathParams: { externalId: shift.external_id }, - }); - navigate( - `/facility/${shift.assigned_facility}/patient/${shift.patient}/consultation`, - ); - }; - - const shiftStatusOptions = careConfig.wartimeShifting - ? SHIFTING_CHOICES_WARTIME - : SHIFTING_CHOICES_PEACETIME; - - const COMPLETED = careConfig.wartimeShifting - ? [ - "COMPLETED", - "REJECTED", - "CANCELLED", - "DESTINATION REJECTED", - "PATIENT EXPIRED", - ] - : ["CANCELLED", "PATIENT EXPIRED"]; - - const completedBoards = shiftStatusOptions.filter((option) => - COMPLETED.includes(option.text), - ); - const activeBoards = shiftStatusOptions.filter( - (option) => !COMPLETED.includes(option.text), - ); - - const [boardFilter, setBoardFilter] = useState(activeBoards); - const { t } = useTranslation(); - - return ( -
-
-
- { - const { data } = await request(routes.downloadShiftRequests, { - query: { ...formatFilter(qParams), csv: true }, - }); - return data ?? null; - }} - filenamePrefix="shift_requests" - /> - } - breadcrumbs={false} - /> -
-
- updateQuery({ [e.name]: e.value })} - placeholder={t("search_patient")} - /> - - - setBoardFilter(tab ? completedBoards : activeBoards) - } - currentTab={boardFilter[0].text !== activeBoards[0].text ? 1 : 0} - /> - -
- navigate("/shifting/list", { query: qParams })} - > - - {t("list_view")} - - advancedFilter.setShow(true)} - /> -
-
-
- - title={} - sections={boardFilter.map((board) => ({ - id: board.text, - title: ( -

- {board.label || board.text}{" "} - { - const { data } = await request(routes.downloadShiftRequests, { - query: { - ...formatFilter({ ...qParams, status: board.text }), - csv: true, - }, - }); - return data ?? null; - }} - filenamePrefix={`shift_requests_${board.label || board.text}`} - /> -

- ), - fetchOptions: (id) => ({ - route: routes.listShiftRequests, - options: { - query: formatFilter({ - ...qParams, - status: id, - }), - }, - }), - }))} - onDragEnd={(result) => { - if (result.source.droppableId !== result.destination?.droppableId) - navigate( - `/shifting/${result.draggableId}/update?status=${result.destination?.droppableId}`, - ); - }} - itemRender={(shift) => ( -
-
-
-
-
- {shift.patient_object.name} -
-
- {shift.patient_object.age} old -
-
-
- {shift.emergency && ( - - {t("emergency")} - - )} -
-
-
- {( - [ - { - title: "phone_number", - icon: "l-mobile-android", - data: shift.patient_object.phone_number, - }, - { - title: "origin_facility", - icon: "l-plane-departure", - data: shift.origin_facility_object.name, - }, - { - title: "shifting_approving_facility", - icon: "l-user-check", - data: careConfig.wartimeShifting - ? shift.shifting_approving_facility_object?.name - : undefined, - }, - { - title: "assigned_facility", - icon: "l-plane-arrival", - data: - shift.assigned_facility_external || - shift.assigned_facility_object?.name || - t("yet_to_be_decided"), - }, - { - title: "last_modified", - icon: "l-stopwatch", - data: formatDateTime(shift.modified_date), - className: dayjs() - .subtract(2, "hours") - .isBefore(shift.modified_date) - ? "text-secondary-900" - : "rounded bg-red-500 border border-red-600 text-white w-full font-bold", - }, - { - title: "patient_address", - icon: "l-home", - data: shift.patient_object.address, - }, - { - title: "assigned_to", - icon: "l-user", - data: shift.assigned_to_object - ? formatName(shift.assigned_to_object) + - " - " + - shift.assigned_to_object.user_type - : undefined, - }, - { - title: "patient_state", - icon: "l-map-marker", - data: shift.patient_object.state_object?.name, - }, - ] as const - ) - .filter((d) => d.data) - .map((datapoint, i) => ( -
-
- -
-
- {datapoint.data} -
-
- ))} -
-
-
- - {t("all_details")} - - - {shift.status === "COMPLETED" && shift.assigned_facility && ( - <> - - - - setModalFor({ externalId: undefined, loading: false }) - } - action={t("confirm")} - onConfirm={() => handleTransferComplete(shift)} - > -

- {t("redirected_to_create_consultation")} -

-
- - )} -
-
- )} - /> - -
- ); -} diff --git a/src/components/Shifting/ListView.tsx b/src/components/Shifting/ListView.tsx deleted file mode 100644 index e74f80107b0..00000000000 --- a/src/components/Shifting/ListView.tsx +++ /dev/null @@ -1,332 +0,0 @@ -import { useState } from "react"; -import BadgesList from "./BadgesList"; -import ButtonV2 from "@/components/Common/components/ButtonV2"; -import ConfirmDialog from "@/components/Common/ConfirmDialog"; -import { ExportButton } from "@/components/Common/Export"; -import ListFilter from "./ListFilter"; -import Page from "@/components/Common/components/Page"; -import SearchInput from "../Form/SearchInput"; -import { formatDateTime } from "../../Utils/utils"; -import { formatFilter } from "./Commons"; -import { navigate } from "raviger"; -import useFilters from "@/common/hooks/useFilters"; -import { useTranslation } from "react-i18next"; -import { AdvancedFilterButton } from "../../CAREUI/interactive/FiltersSlideover"; -import CareIcon from "../../CAREUI/icons/CareIcon"; -import dayjs from "../../Utils/dayjs"; -import useAuthUser from "@/common/hooks/useAuthUser"; -import request from "../../Utils/request/request"; -import routes from "../../Redux/api"; -import useQuery from "../../Utils/request/useQuery"; -import careConfig from "@careConfig"; -import Loading from "@/components/Common/Loading"; -import { IShift } from "./models"; - -export default function ListView() { - const { - qParams, - updateQuery, - Pagination, - FilterBadges, - advancedFilter, - resultsPerPage, - } = useFilters({ cacheBlacklist: ["patient_name"] }); - - const [modalFor, setModalFor] = useState<{ - external_id: string | undefined; - loading: boolean; - }>({ - external_id: undefined, - loading: false, - }); - const authUser = useAuthUser(); - const { t } = useTranslation(); - - const handleTransferComplete = async (shift: IShift) => { - setModalFor({ ...modalFor, loading: true }); - await request(routes.completeTransfer, { - pathParams: { externalId: shift.external_id }, - }); - navigate( - `/facility/${shift.assigned_facility}/patient/${shift.patient}/consultation`, - ); - }; - - const { - data: shiftData, - loading, - refetch: fetchData, - } = useQuery(routes.listShiftRequests, { - query: formatFilter({ - ...qParams, - offset: (qParams.page ? qParams.page - 1 : 0) * resultsPerPage, - }), - }); - - const showShiftingCardList = (data: IShift[]) => { - if (loading) { - return ; - } - - if (!data || data.length === 0) { - return ( -
-
- {t("no_patients_to_show")} -
-
- ); - } - - return ( -
- {data.map((shift: IShift) => ( -
-
-
-
-
-
- {shift.patient_object.name} - {shift.patient_object.age} -
-
- {shift.emergency && ( - - {t("emergency")} - - )} -
-
-
-
-
- -
- {shift.status} -
- -
-
-
- -
- {shift.patient_object.phone_number || ""} -
- -
-
-
- -
- {(shift.origin_facility_object || {}).name} -
- -
- {careConfig.wartimeShifting && ( -
-
- -
- { - (shift.shifting_approving_facility_object || {}) - .name - } -
- -
- )} -
-
- - -
- {shift.assigned_facility_external || - shift.assigned_facility_object?.name || - t("yet_to_be_decided")} -
- -
- -
-
- -
- {formatDateTime(shift.modified_date) || "--"} -
- -
- -
-
- -
- {shift.patient_object.address || "--"} -
- -
-
-
- -
- navigate(`/shifting/${shift.external_id}`)} - variant="secondary" - border - className="w-full" - > - {" "} - {t("all_details")} - -
- {shift.status === "COMPLETED" && shift.assigned_facility && ( -
- - setModalFor({ - external_id: shift.external_id, - loading: false, - }) - } - > - {t("transfer_to_receiving_facility")} - - - setModalFor({ external_id: undefined, loading: false }) - } - onConfirm={() => handleTransferComplete(shift)} - /> -
- )} -
-
-
- ))} -
- ); - }; - - return ( - { - const { data } = await request(routes.downloadShiftRequests, { - query: { ...formatFilter(qParams), csv: true }, - }); - return data ?? null; - }} - filenamePrefix="shift_requests" - /> - } - breadcrumbs={false} - options={ - <> -
- updateQuery({ [e.name]: e.value })} - placeholder={t("search_patient")} - /> -
-
- {/* dummy div to align space as per board view */} -
-
- navigate("/shifting/board", { query: qParams })} - > - - {t("board_view")} - - - advancedFilter.setShow(true)} - /> -
- - } - > - -
- {loading ? ( - - ) : ( -
-
- -
- - {showShiftingCardList(shiftData?.results || [])} - -
- -
-
- )} -
- -
- ); -} diff --git a/src/components/Shifting/ShiftDetails.tsx b/src/components/Shifting/ShiftDetails.tsx index 6698b37beb9..666bd1bcb38 100644 --- a/src/components/Shifting/ShiftDetails.tsx +++ b/src/components/Shifting/ShiftDetails.tsx @@ -8,7 +8,7 @@ import { import { Link, navigate } from "raviger"; import { useState } from "react"; import ButtonV2 from "@/components/Common/components/ButtonV2"; -import CommentSection from "./CommentsSection"; +import CommentSection from "./ShiftingCommentsSection"; import ConfirmDialog from "@/components/Common/ConfirmDialog"; import { CopyToClipboard } from "react-copy-to-clipboard"; import Page from "@/components/Common/components/Page"; diff --git a/src/components/Shifting/ShiftDetailsUpdate.tsx b/src/components/Shifting/ShiftDetailsUpdate.tsx index 4f30e3b345d..e34a3cb62f8 100644 --- a/src/components/Shifting/ShiftDetailsUpdate.tsx +++ b/src/components/Shifting/ShiftDetailsUpdate.tsx @@ -13,7 +13,7 @@ import { import { Cancel, Submit } from "@/components/Common/components/ButtonV2"; import { navigate, useQueryParams } from "raviger"; import { useReducer, useState } from "react"; -import { ConsultationModel } from "../Facility/models"; +import { ConsultationModel, ShiftingModel } from "../Facility/models"; import DischargeModal from "../Facility/DischargeModal"; import { FacilitySelect } from "@/components/Common/FacilitySelect"; import { FieldChangeEvent } from "../Form/FormFields/Utils"; @@ -34,7 +34,6 @@ import { LinkedFacilityUsers } from "@/components/Common/UserAutocompleteFormFie import { UserBareMinimum } from "../Users/models"; import useQuery from "../../Utils/request/useQuery"; import routes from "../../Redux/api"; -import { IShift } from "./models"; import request from "../../Utils/request/request"; import { PatientModel } from "../Patient/models"; import useAuthUser from "@/common/hooks/useAuthUser"; @@ -228,7 +227,7 @@ export const ShiftDetailsUpdate = (props: patientShiftProps) => { } setIsLoading(true); - const data: Partial = { + const data: Partial = { origin_facility: state.form.origin_facility_object?.id, shifting_approving_facility: state.form?.shifting_approving_facility_object?.id, diff --git a/src/components/Shifting/BadgesList.tsx b/src/components/Shifting/ShiftingBadges.tsx similarity index 97% rename from src/components/Shifting/BadgesList.tsx rename to src/components/Shifting/ShiftingBadges.tsx index a06788781c6..adc3bcd7752 100644 --- a/src/components/Shifting/BadgesList.tsx +++ b/src/components/Shifting/ShiftingBadges.tsx @@ -1,5 +1,5 @@ import { SHIFTING_FILTER_ORDER } from "@/common/constants"; -import { useFacilityQuery } from "../Resource/BadgesList"; +import { useFacilityQuery } from "../Resource/ResourceBadges"; import { useTranslation } from "react-i18next"; import useQuery from "../../Utils/request/useQuery"; import routes from "../../Redux/api"; diff --git a/src/components/Shifting/ShiftingBlock.tsx b/src/components/Shifting/ShiftingBlock.tsx new file mode 100644 index 00000000000..20cd431d9f6 --- /dev/null +++ b/src/components/Shifting/ShiftingBlock.tsx @@ -0,0 +1,144 @@ +import { useTranslation } from "react-i18next"; +import { ShiftingModel } from "../Facility/models"; +import careConfig from "@careConfig"; +import { classNames, formatDateTime, formatName } from "@/Utils/utils"; +import dayjs from "dayjs"; +import CareIcon from "@/CAREUI/icons/CareIcon"; +import { Link } from "raviger"; +import useAuthUser from "@/common/hooks/useAuthUser"; + +export default function ShiftingBlock(props: { + shift: ShiftingModel; + onTransfer: () => unknown; +}) { + const { shift, onTransfer } = props; + const { t } = useTranslation(); + const authUser = useAuthUser(); + + return ( +
+
+
+
+
+ {shift.patient_object.name} +
+
+ {shift.patient_object.age} old +
+
+
+ {shift.emergency && ( + + {t("emergency")} + + )} +
+
+
+ {( + [ + { + title: "phone_number", + icon: "l-mobile-android", + data: shift.patient_object.phone_number, + }, + { + title: "origin_facility", + icon: "l-plane-departure", + data: shift.origin_facility_object.name, + }, + { + title: "shifting_approving_facility", + icon: "l-user-check", + data: careConfig.wartimeShifting + ? shift.shifting_approving_facility_object?.name + : undefined, + }, + { + title: "assigned_facility", + icon: "l-plane-arrival", + data: + shift.assigned_facility_external || + shift.assigned_facility_object?.name || + t("yet_to_be_decided"), + }, + { + title: "last_modified", + icon: "l-stopwatch", + data: formatDateTime(shift.modified_date), + className: dayjs() + .subtract(2, "hours") + .isBefore(shift.modified_date) + ? "text-secondary-900" + : "rounded bg-red-500 border border-red-600 text-white w-full font-bold", + }, + { + title: "patient_address", + icon: "l-home", + data: shift.patient_object.address, + }, + { + title: "assigned_to", + icon: "l-user", + data: shift.assigned_to_object + ? formatName(shift.assigned_to_object) + + " - " + + shift.assigned_to_object.user_type + : undefined, + }, + { + title: "patient_state", + icon: "l-map-marker", + data: shift.patient_object.state_object?.name, + }, + ] as const + ) + .filter((d) => d.data) + .map((datapoint, i) => ( +
+
+ +
+
{datapoint.data}
+
+ ))} +
+
+
+ + {t("all_details")} + + + {shift.status === "COMPLETED" && shift.assigned_facility && ( + <> + + + )} +
+
+ ); +} diff --git a/src/components/Shifting/ShiftingBoard.tsx b/src/components/Shifting/ShiftingBoard.tsx new file mode 100644 index 00000000000..417f8ef747e --- /dev/null +++ b/src/components/Shifting/ShiftingBoard.tsx @@ -0,0 +1,183 @@ +import { + SHIFTING_CHOICES_PEACETIME, + SHIFTING_CHOICES_WARTIME, +} from "@/common/constants"; + +import BadgesList from "./ShiftingBadges"; +import { ExportButton } from "@/components/Common/Export"; +import ListFilter from "./ShiftingFilters"; +import SearchInput from "../Form/SearchInput"; +import { formatFilter } from "./ShiftingCommons"; + +import { navigate } from "raviger"; +import useFilters from "@/common/hooks/useFilters"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import ButtonV2 from "@/components/Common/components/ButtonV2"; +import { AdvancedFilterButton } from "../../CAREUI/interactive/FiltersSlideover"; +import CareIcon from "../../CAREUI/icons/CareIcon"; +import Tabs from "@/components/Common/components/Tabs"; +import careConfig from "@careConfig"; +import KanbanBoard from "../Kanban/Board"; + +import ConfirmDialog from "@/components/Common/ConfirmDialog"; +import { ShiftingModel } from "../Facility/models"; +import request from "../../Utils/request/request"; +import routes from "../../Redux/api"; +import PageTitle from "@/components/Common/PageTitle"; +import ShiftingBlock from "./ShiftingBlock"; + +export default function BoardView() { + const { qParams, updateQuery, FilterBadges, advancedFilter } = useFilters({ + limit: -1, + cacheBlacklist: ["patient_name"], + }); + + const [modalFor, setModalFor] = useState(); + + const handleTransferComplete = async (shift?: ShiftingModel) => { + if (!shift) return; + await request(routes.completeTransfer, { + pathParams: { externalId: shift.external_id }, + }); + navigate( + `/facility/${shift.assigned_facility}/patient/${shift.patient}/consultation`, + ); + }; + + const shiftStatusOptions = careConfig.wartimeShifting + ? SHIFTING_CHOICES_WARTIME + : SHIFTING_CHOICES_PEACETIME; + + const COMPLETED = careConfig.wartimeShifting + ? [ + "COMPLETED", + "REJECTED", + "CANCELLED", + "DESTINATION REJECTED", + "PATIENT EXPIRED", + ] + : ["CANCELLED", "PATIENT EXPIRED"]; + + const completedBoards = shiftStatusOptions.filter((option) => + COMPLETED.includes(option.text), + ); + const activeBoards = shiftStatusOptions.filter( + (option) => !COMPLETED.includes(option.text), + ); + + const [boardFilter, setBoardFilter] = useState(activeBoards); + const { t } = useTranslation(); + + return ( +
+
+
+ { + const { data } = await request(routes.downloadShiftRequests, { + query: { ...formatFilter(qParams), csv: true }, + }); + return data ?? null; + }} + filenamePrefix="shift_requests" + /> + } + breadcrumbs={false} + /> +
+
+ updateQuery({ [e.name]: e.value })} + placeholder={t("search_patient")} + /> + + + setBoardFilter(tab ? completedBoards : activeBoards) + } + currentTab={boardFilter[0].text !== activeBoards[0].text ? 1 : 0} + /> + +
+ navigate("/shifting/list", { query: qParams })} + > + + {t("list_view")} + + advancedFilter.setShow(true)} + /> +
+
+
+ + title={} + sections={boardFilter.map((board) => ({ + id: board.text, + title: ( +

+ {board.label || board.text}{" "} + { + const { data } = await request(routes.downloadShiftRequests, { + query: { + ...formatFilter({ ...qParams, status: board.text }), + csv: true, + }, + }); + return data ?? null; + }} + filenamePrefix={`shift_requests_${board.label || board.text}`} + /> +

+ ), + fetchOptions: (id) => ({ + route: routes.listShiftRequests, + options: { + query: formatFilter({ + ...qParams, + status: id, + }), + }, + }), + }))} + onDragEnd={(result) => { + if (result.source.droppableId !== result.destination?.droppableId) + navigate( + `/shifting/${result.draggableId}/update?status=${result.destination?.droppableId}`, + ); + }} + itemRender={(shift) => ( + setModalFor(shift)} shift={shift} /> + )} + /> + setModalFor(undefined)} + action={t("confirm")} + onConfirm={() => handleTransferComplete(modalFor)} + > +

+ {t("redirected_to_create_consultation")} +

+
+ +
+ ); +} diff --git a/src/components/Shifting/CommentsSection.tsx b/src/components/Shifting/ShiftingCommentsSection.tsx similarity index 96% rename from src/components/Shifting/CommentsSection.tsx rename to src/components/Shifting/ShiftingCommentsSection.tsx index 5d6aefb98fb..9453052d2d9 100644 --- a/src/components/Shifting/CommentsSection.tsx +++ b/src/components/Shifting/ShiftingCommentsSection.tsx @@ -5,9 +5,9 @@ import { formatDateTime, formatName } from "../../Utils/utils"; import { useTranslation } from "react-i18next"; import ButtonV2 from "@/components/Common/components/ButtonV2"; import routes from "../../Redux/api"; -import { IComment } from "../Resource/models"; import PaginatedList from "../../CAREUI/misc/PaginatedList"; import request from "../../Utils/request/request"; +import { CommentModel } from "../Facility/models"; interface CommentSectionProps { id: string; @@ -67,7 +67,7 @@ const CommentSection = (props: CommentSectionProps) => { - > + > {(item) => }
@@ -87,7 +87,7 @@ export const Comment = ({ comment, created_by_object, modified_date, -}: IComment) => { +}: CommentModel) => { const { t } = useTranslation(); return (
{ : filter.is_up_shift === "yes" ? "true" : "false", - limit: limit, + limit: filter.limit || limit, offset: filter.offset, patient_name: filter.patient_name || undefined, created_date_before: filter.created_date_before || undefined, diff --git a/src/components/Shifting/ListFilter.tsx b/src/components/Shifting/ShiftingFilters.tsx similarity index 100% rename from src/components/Shifting/ListFilter.tsx rename to src/components/Shifting/ShiftingFilters.tsx diff --git a/src/components/Shifting/ShiftingList.tsx b/src/components/Shifting/ShiftingList.tsx new file mode 100644 index 00000000000..fda6d6fcaa6 --- /dev/null +++ b/src/components/Shifting/ShiftingList.tsx @@ -0,0 +1,179 @@ +import { useState } from "react"; +import BadgesList from "./ShiftingBadges"; +import ButtonV2 from "@/components/Common/components/ButtonV2"; +import ConfirmDialog from "@/components/Common/ConfirmDialog"; +import { ExportButton } from "@/components/Common/Export"; +import ListFilter from "./ShiftingFilters"; +import Page from "@/components/Common/components/Page"; +import SearchInput from "../Form/SearchInput"; +import { formatFilter } from "./ShiftingCommons"; +import { navigate } from "raviger"; +import useFilters from "@/common/hooks/useFilters"; +import { useTranslation } from "react-i18next"; +import { AdvancedFilterButton } from "../../CAREUI/interactive/FiltersSlideover"; +import CareIcon from "../../CAREUI/icons/CareIcon"; +import request from "../../Utils/request/request"; +import routes from "../../Redux/api"; +import useQuery from "../../Utils/request/useQuery"; +import Loading from "@/components/Common/Loading"; +import { ShiftingModel } from "../Facility/models"; +import ShiftingBlock from "./ShiftingBlock"; + +export default function ListView() { + const { + qParams, + updateQuery, + Pagination, + FilterBadges, + advancedFilter, + resultsPerPage, + } = useFilters({ cacheBlacklist: ["patient_name"], limit: 12 }); + + const [modalFor, setModalFor] = useState(); + const { t } = useTranslation(); + + const handleTransferComplete = async (shift?: ShiftingModel) => { + if (!shift) return; + await request(routes.completeTransfer, { + pathParams: { externalId: shift.external_id }, + }); + navigate( + `/facility/${shift.assigned_facility}/patient/${shift.patient}/consultation`, + ); + }; + + const { + data: shiftData, + loading, + refetch: fetchData, + } = useQuery(routes.listShiftRequests, { + query: formatFilter({ + ...qParams, + limit: resultsPerPage, + offset: (qParams.page ? qParams.page - 1 : 0) * resultsPerPage, + }), + }); + + const showShiftingCardList = (data: ShiftingModel[]) => { + if (loading) { + return ; + } + + if (!data || data.length === 0) { + return ( +
+
+ {t("no_patients_to_show")} +
+
+ ); + } + + return ( +
+
+ {data.map((shift, i) => ( +
+ setModalFor(shift)} + shift={shift} + /> +
+ ))} +
+ setModalFor(undefined)} + onConfirm={() => handleTransferComplete(modalFor)} + /> +
+ ); + }; + + return ( + { + const { data } = await request(routes.downloadShiftRequests, { + query: { ...formatFilter(qParams), csv: true }, + }); + return data ?? null; + }} + filenamePrefix="shift_requests" + /> + } + breadcrumbs={false} + options={ + <> +
+ updateQuery({ [e.name]: e.value })} + placeholder={t("search_patient")} + /> +
+
+ {/* dummy div to align space as per board view */} +
+
+ navigate("/shifting/board", { query: qParams })} + > + + {t("board_view")} + + + advancedFilter.setShow(true)} + /> +
+ + } + > + +
+ {loading ? ( + + ) : ( +
+
+ +
+ + {showShiftingCardList(shiftData?.results || [])} + +
+ +
+
+ )} +
+ +
+ ); +} diff --git a/src/components/Shifting/models.ts b/src/components/Shifting/models.ts deleted file mode 100644 index 010db870ab8..00000000000 --- a/src/components/Shifting/models.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { FacilityModel } from "../Facility/models"; -import { PerformedByModel } from "../HCX/misc"; -import { AssignedToObjectModel, PatientModel } from "../Patient/models"; - -export interface IShift { - id: string; - patient_object: PatientModel; - emergency: boolean; - status: string; - origin_facility_object: FacilityModel; - origin_facility: string; - shifting_approving_facility: string; - assigned_facility_external: string | null; - assigned_facility: string | null; - is_up_shift: boolean; - assigned_to: number; - patient_category: string; - shifting_approving_facility_object: FacilityModel; - assigned_facility_object: FacilityModel; - assigned_facility_external_object: FacilityModel; - modified_date: string; - external_id: string; - assigned_to_object?: AssignedToObjectModel; - refering_facility_contact_name: string; - refering_facility_contact_number: string; - is_kasp: boolean; - vehicle_preference: string; - preferred_vehicle_choice: string; - assigned_facility_type: string; - breathlessness_level: string; - reason: string; - ambulance_driver_name: string; - ambulance_phone_number: string | undefined; - ambulance_number: string; - comments: string; - created_date: string; - created_by_object: PerformedByModel; - last_edited_by_object: PerformedByModel; - is_assigned_to_user: boolean; - created_by: number; - last_edited_by: number; - patient: string | PatientModel; - initial_status?: string; -} From cb6dc92a36169ed164b670fac6e47d17d5088319 Mon Sep 17 00:00:00 2001 From: Jacob John Jeevan <40040905+Jacobjeevan@users.noreply.github.com> Date: Wed, 30 Oct 2024 13:51:30 +0530 Subject: [PATCH 15/15] Facility staff management bug fixes (#8952) --- src/Locale/en.json | 1 + src/common/constants.tsx | 2 +- src/components/Facility/FacilityStaffList.tsx | 36 ++++++++++--------- src/components/Facility/StaffCapacity.tsx | 3 ++ 4 files changed, 25 insertions(+), 17 deletions(-) diff --git a/src/Locale/en.json b/src/Locale/en.json index 784dd9d3905..9a7f4d750c2 100644 --- a/src/Locale/en.json +++ b/src/Locale/en.json @@ -1172,6 +1172,7 @@ "to_be_conducted": "To be conducted", "total_amount": "Total Amount", "total_beds": "Total Beds", + "total_staff": "Total Staff", "total_users": "Total Users", "transfer_in_progress": "TRANSFER IN PROGRESS", "transfer_to_receiving_facility": "Transfer to receiving facility", diff --git a/src/common/constants.tsx b/src/common/constants.tsx index 252d5cb22eb..87432fcaadb 100644 --- a/src/common/constants.tsx +++ b/src/common/constants.tsx @@ -236,7 +236,7 @@ export const DOCTOR_SPECIALIZATION: Array = [ { id: 16, text: "General Surgeon" }, { id: 17, text: "Geriatrician" }, { id: 18, text: "Hematologist" }, - { id: 29, text: "Immunologist" }, + { id: 19, text: "Immunologist" }, { id: 20, text: "Infectious Disease Specialist" }, { id: 21, text: "MBBS doctor" }, { id: 22, text: "Medical Officer" }, diff --git a/src/components/Facility/FacilityStaffList.tsx b/src/components/Facility/FacilityStaffList.tsx index 7b51568a871..2d116e0789f 100644 --- a/src/components/Facility/FacilityStaffList.tsx +++ b/src/components/Facility/FacilityStaffList.tsx @@ -11,14 +11,21 @@ import DoctorsCountCard from "./StaffCountCard"; import { DoctorIcon } from "../TeleIcu/Icons/DoctorIcon"; import CareIcon from "../../CAREUI/icons/CareIcon"; import { useTranslation } from "react-i18next"; +import useFilters from "@/common/hooks/useFilters"; +import Pagination from "../Common/Pagination"; export const FacilityStaffList = (props: any) => { const { t } = useTranslation(); const [doctorCapacityModalOpen, setDoctorCapacityModalOpen] = useState(false); + const { qParams, resultsPerPage, updatePage } = useFilters({ limit: 15 }); const [totalDoctors, setTotalDoctors] = useState(0); - const doctorQuery = useQuery(routes.listDoctor, { + const { data: doctorsList, refetch } = useQuery(routes.listDoctor, { pathParams: { facilityId: props.facilityId }, + query: { + limit: resultsPerPage, + offset: (qParams.page - 1) * resultsPerPage, + }, onResponse: ({ res, data }) => { if (res?.ok && data) { let totalCount = 0; @@ -33,7 +40,7 @@ export const FacilityStaffList = (props: any) => { }); let doctorList: any = null; - if (!doctorQuery.data || !doctorQuery.data.results.length) { + if (!doctorsList || !doctorsList.results.length) { doctorList = (
{t("no_staff")} @@ -50,7 +57,7 @@ export const FacilityStaffList = (props: any) => {
- Total Staff + {t("total_staff")}

{totalDoctors}

@@ -58,25 +65,16 @@ export const FacilityStaffList = (props: any) => {
- {doctorQuery.data.results.map((data: DoctorModal) => { - const removeCurrentDoctorData = (doctorId: number | undefined) => { - if (doctorQuery.data !== undefined) { - doctorQuery.data?.results.filter( - (i: DoctorModal) => i.id !== doctorId, - ); - doctorQuery.refetch(); - } - }; - + {doctorsList.results.map((data: DoctorModal) => { return ( { - doctorQuery.refetch(); + refetch(); }} {...data} - removeDoctor={removeCurrentDoctorData} + removeDoctor={() => refetch()} /> ); })} @@ -116,11 +114,17 @@ export const FacilityStaffList = (props: any) => { facilityId={props.facilityId} handleClose={() => setDoctorCapacityModalOpen(false)} handleUpdate={async () => { - doctorQuery.refetch(); + refetch(); }} /> )} + updatePage(page)} + /> ); }; diff --git a/src/components/Facility/StaffCapacity.tsx b/src/components/Facility/StaffCapacity.tsx index 602ec9a33f7..99be198dd74 100644 --- a/src/components/Facility/StaffCapacity.tsx +++ b/src/components/Facility/StaffCapacity.tsx @@ -66,6 +66,9 @@ export const StaffCapacity = (props: DoctorCapacityProps) => { const specializationsQuery = useQuery(routes.listDoctor, { pathParams: { facilityId }, + query: { + limit: DOCTOR_SPECIALIZATION.length - 1, + }, }); const { loading } = useQuery(routes.getDoctor, {