diff --git a/package-lock.json b/package-lock.json index f7bb5637ead..c30b92874fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@googlemaps/react-wrapper": "^1.1.35", "@googlemaps/typescript-guards": "^2.0.3", "@headlessui/react": "^2.1.2", + "@hello-pangea/dnd": "^17.0.0", "@pnotify/core": "^5.2.0", "@pnotify/mobile": "^5.2.0", "@radix-ui/react-icons": "^1.3.0", @@ -45,9 +46,6 @@ "raviger": "^4.1.2", "react": "18.3.1", "react-copy-to-clipboard": "^5.1.0", - "react-dnd": "^16.0.1", - "react-dnd-html5-backend": "^16.0.1", - "react-dnd-scrolling": "^1.3.8", "react-dom": "18.3.1", "react-google-recaptcha": "^3.1.0", "react-i18next": "^15.0.2", @@ -2703,6 +2701,51 @@ "react-dom": "^18" } }, + "node_modules/@hello-pangea/dnd": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-17.0.0.tgz", + "integrity": "sha512-LDDPOix/5N0j5QZxubiW9T0M0+1PR0rTDWeZF5pu1Tz91UQnuVK4qQ/EjY83Qm2QeX0eM8qDXANfDh3VVqtR4Q==", + "dependencies": { + "@babel/runtime": "^7.25.6", + "css-box-model": "^1.2.1", + "memoize-one": "^6.0.0", + "raf-schd": "^4.0.3", + "react-redux": "^9.1.2", + "redux": "^5.0.1", + "use-memo-one": "^1.1.3" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/@hello-pangea/dnd/node_modules/react-redux": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz", + "integrity": "sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==", + "dependencies": { + "@types/use-sync-external-store": "^0.0.3", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25", + "react": "^18.0", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/@hello-pangea/dnd/node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -3716,21 +3759,6 @@ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" } }, - "node_modules/@react-dnd/asap": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz", - "integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==" - }, - "node_modules/@react-dnd/invariant": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz", - "integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==" - }, - "node_modules/@react-dnd/shallowequal": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz", - "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==" - }, "node_modules/@react-stately/utils": { "version": "3.10.2", "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.2.tgz", @@ -6667,6 +6695,14 @@ "node": ">=8" } }, + "node_modules/css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "dependencies": { + "tiny-invariant": "^1.0.6" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -7268,16 +7304,6 @@ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" }, - "node_modules/dnd-core": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz", - "integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==", - "dependencies": { - "@react-dnd/asap": "^5.0.1", - "@react-dnd/invariant": "^4.0.1", - "redux": "^4.2.0" - } - }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -11071,11 +11097,6 @@ "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", "dev": true }, - "node_modules/lodash.throttle": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", - "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==" - }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -12200,6 +12221,11 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" + }, "node_modules/merge-refs": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/merge-refs/-/merge-refs-1.3.0.tgz", @@ -13684,7 +13710,8 @@ "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "dev": true }, "node_modules/picocolors": { "version": "1.1.0", @@ -14264,13 +14291,10 @@ "integrity": "sha512-twwRO/ilhlG/FIgYeKGFqyHhoEhqgnKVkcmqMKi2r524gz3ZbDTcyFt38E9xjJI2vT+KbRNHVbnJ/e0I25Azwg==", "dev": true }, - "node_modules/raf": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", - "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", - "dependencies": { - "performance-now": "^2.1.0" - } + "node_modules/raf-schd": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==" }, "node_modules/ramda": { "version": "0.29.0", @@ -17990,6 +18014,14 @@ "react-dom": ">=16.8" } }, + "node_modules/use-memo-one": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", + "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", diff --git a/package.json b/package.json index 6c9576f2fa3..90a66763a57 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "@googlemaps/react-wrapper": "^1.1.35", "@googlemaps/typescript-guards": "^2.0.3", "@headlessui/react": "^2.1.2", + "@hello-pangea/dnd": "^17.0.0", "@pnotify/core": "^5.2.0", "@pnotify/mobile": "^5.2.0", "@radix-ui/react-icons": "^1.3.0", @@ -83,9 +84,6 @@ "raviger": "^4.1.2", "react": "18.3.1", "react-copy-to-clipboard": "^5.1.0", - "react-dnd": "^16.0.1", - "react-dnd-html5-backend": "^16.0.1", - "react-dnd-scrolling": "^1.3.8", "react-dom": "18.3.1", "react-google-recaptcha": "^3.1.0", "react-i18next": "^15.0.2", diff --git a/src/Components/Facility/models.tsx b/src/Components/Facility/models.tsx index 56c70f2ac94..8103ebb6729 100644 --- a/src/Components/Facility/models.tsx +++ b/src/Components/Facility/models.tsx @@ -5,6 +5,7 @@ import { ConsultationSuggestionValue, DISCHARGE_REASONS, PATIENT_NOTES_THREADS, + SHIFTING_CHOICES_PEACETIME, UserRole, } from "../../Common/constants"; import { FeatureFlag } from "../../Utils/featureFlags"; @@ -15,12 +16,13 @@ import { DailyRoundsModel, FacilityNameModel, FileUploadModel, + PatientModel, } from "../Patient/models"; import { EncounterSymptom } from "../Symptoms/types"; +import { UserBareMinimum, UserModel } from "../Users/models"; import { InvestigationType } from "../Common/prescription-builder/InvestigationBuilder"; import { ProcedureType } from "../Common/prescription-builder/ProcedureBuilder"; import { RouteToFacility } from "../Common/RouteToFacilitySelect"; -import { UserBareMinimum } from "../Users/models"; export interface LocalBodyModel { id: number; @@ -676,3 +678,50 @@ export type PatientTransferResponse = { date_of_birth: string; facility_object: BaseFacilityModel; }; + +export interface ShiftingModel { + assigned_facility: string; + assigned_facility_external: string | null; + assigned_facility_object: FacilityModel; + created_date: string; + emergency: boolean; + external_id: string; + id: string; + 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; +} + +export interface ResourceModel { + approving_facility: string | null; + approving_facility_object: FacilityModel | null; + assigned_facility: string | null; + 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; + priority: number | null; + reason: string; + refering_facility_contact_name: string; + refering_facility_contact_number: string; + requested_quantity: number; + status: string; + sub_category: string; + title: string; +} diff --git a/src/Components/Kanban/Board.tsx b/src/Components/Kanban/Board.tsx new file mode 100644 index 00000000000..d2ab5da998f --- /dev/null +++ b/src/Components/Kanban/Board.tsx @@ -0,0 +1,190 @@ +import { + DragDropContext, + Draggable, + Droppable, + OnDragEndResponder, +} from "@hello-pangea/dnd"; +import { ReactNode, RefObject, useEffect, useRef, useState } from "react"; +import { QueryRoute } from "../../Utils/request/types"; +import { QueryOptions } from "../../Utils/request/useQuery"; +import CareIcon from "../../CAREUI/icons/CareIcon"; +import request from "../../Utils/request/request"; +import { useTranslation } from "react-i18next"; + +interface KanbanBoardProps { + title?: ReactNode; + onDragEnd: OnDragEndResponder; + sections: { + id: string; + title: ReactNode; + fetchOptions: ( + id: string, + ...args: unknown[] + ) => { + route: QueryRoute; + options?: QueryOptions; + }; + }[]; + itemRender: (item: T) => ReactNode; +} + +export default function KanbanBoard( + props: KanbanBoardProps, +) { + const board = useRef(null); + + return ( +
+
+
{props.title}
+
+ {[0, 1].map((button, i) => ( + + ))} +
+
+ +
+
+ {props.sections.map((section, i) => ( + + key={i} + section={section} + itemRender={props.itemRender} + boardRef={board} + /> + ))} +
+
+
+
+ ); +} + +export function KanbanSection( + props: Omit, "sections" | "onDragEnd"> & { + section: KanbanBoardProps["sections"][number]; + boardRef: RefObject; + }, +) { + const { section } = props; + const [offset, setOffset] = useState(0); + const [pages, setPages] = useState([]); + const [fetchingNextPage, setFetchingNextPage] = useState(false); + const [hasMore, setHasMore] = useState(true); + const [totalCount, setTotalCount] = useState(); + + const options = section.fetchOptions(section.id); + const sectionRef = useRef(null); + const defaultLimit = 14; + const { t } = useTranslation(); + + // should be replaced with useInfiniteQuery when we move over to react query + + const fetchNextPage = async (refresh: boolean = false) => { + if (!refresh && (fetchingNextPage || !hasMore)) return; + if (refresh) setPages([]); + const offsetToUse = refresh ? 0 : offset; + setFetchingNextPage(true); + const res = await request(options.route, { + ...options.options, + query: { ...options.options?.query, offsetToUse, limit: defaultLimit }, + }); + const newPages = refresh ? [] : [...pages]; + const page = Math.floor(offsetToUse / defaultLimit); + if (res.error) return; + newPages[page] = (res.data as any).results; + setPages(newPages); + setHasMore(!!(res.data as any)?.next); + setTotalCount((res.data as any)?.count); + setOffset(offsetToUse + defaultLimit); + setFetchingNextPage(false); + }; + + const items = pages.flat(); + + useEffect(() => { + const onBoardReachEnd = async () => { + const sectionElementHeight = + sectionRef.current?.getBoundingClientRect().height; + const scrolled = props.boardRef.current?.scrollTop; + // if user has scrolled 3/4th of the current items + if ( + scrolled && + sectionElementHeight && + scrolled > sectionElementHeight * (3 / 4) + ) { + fetchNextPage(); + } + }; + + props.boardRef.current?.addEventListener("scroll", onBoardReachEnd); + return () => + props.boardRef.current?.removeEventListener("scroll", onBoardReachEnd); + }, [props.boardRef, fetchingNextPage, hasMore]); + + useEffect(() => { + fetchNextPage(true); + }, [props.section]); + + return ( + + {(provided) => ( +
+
+
+
{section.title}
+
+ + {typeof totalCount === "undefined" ? "..." : totalCount} + +
+
+
+
+ {!fetchingNextPage && totalCount === 0 && ( +
+ {t("no_results_found")} +
+ )} + {items + .filter((item) => item) + .map((item, i) => ( + + {(provided) => ( +
+ {props.itemRender(item)} +
+ )} +
+ ))} + {fetchingNextPage && ( +
+ )} +
+
+ )} + + ); +} diff --git a/src/Components/LogUpdate/Sections/PressureSore/PressureSore.tsx b/src/Components/LogUpdate/Sections/PressureSore/PressureSore.tsx index a56457ef257..63970d4c25c 100644 --- a/src/Components/LogUpdate/Sections/PressureSore/PressureSore.tsx +++ b/src/Components/LogUpdate/Sections/PressureSore/PressureSore.tsx @@ -14,6 +14,7 @@ import { IPressureSore } from "../../../Patient/models"; import { Error } from "../../../../Utils/Notifications"; import { classNames, getValueDescription } from "../../../../Utils/utils"; import { calculatePushScore } from "./utils"; +import { useTranslation } from "react-i18next"; const PressureSore = ({ log, onChange, readonly }: LogUpdateSectionProps) => { const value = log.pressure_sore ?? []; @@ -104,6 +105,8 @@ const RegionEditor = (props: RegionEditorProps) => { const isReadOnly = !props.onSave; + const { t } = useTranslation(); + return ( {
{ onChange={(e) => update({ width: parseFloat(e.value) })} /> = { dosage_type: "REGULAR", + route: "ORAL", +}; +const DefaultPRNPrescription: Partial = { + dosage_type: "PRN", + route: "ORAL", }; -const DefaultPRNPrescription: Partial = { dosage_type: "PRN" }; diff --git a/src/Components/Patient/models.tsx b/src/Components/Patient/models.tsx index cf35680de11..d9e275fdc4a 100644 --- a/src/Components/Patient/models.tsx +++ b/src/Components/Patient/models.tsx @@ -135,6 +135,7 @@ export interface PatientModel { assigned_to?: { first_name?: string; username?: string; last_name?: string }; assigned_to_object?: AssignedToObjectModel; meta_info?: PatientMeta; + age?: string; } export interface SampleTestModel { diff --git a/src/Components/Resource/ResourceBoard.tsx b/src/Components/Resource/ResourceBoard.tsx deleted file mode 100644 index aa0b031ba0e..00000000000 --- a/src/Components/Resource/ResourceBoard.tsx +++ /dev/null @@ -1,301 +0,0 @@ -import { useState, useEffect } from "react"; -import { navigate } from "raviger"; -import { classNames, formatName } from "../../Utils/utils"; -import { useDrag, useDrop } from "react-dnd"; -import { formatDateTime } from "../../Utils/utils"; -import { ExportButton } from "../Common/Export"; -import dayjs from "../../Utils/dayjs"; -import useQuery from "../../Utils/request/useQuery"; -import routes from "../../Redux/api"; -import { PaginatedResponse } from "../../Utils/request/types"; -import { IResource } from "./models"; -import request from "../../Utils/request/request"; -import CareIcon from "../../CAREUI/icons/CareIcon"; - -interface boardProps { - board: string; - filterProp: any; - formatFilter: any; -} - -const renderBoardTitle = (board: string) => board; - -const reduceLoading = (action: string, current: any) => { - switch (action) { - case "MORE": - return { ...current, more: true }; - case "BOARD": - return { ...current, board: true }; - case "COMPLETE": - return { board: false, more: false }; - } -}; - -const ResourceCard = ({ resource }: any) => { - const [{ isDragging }, drag] = useDrag(() => ({ - type: "resource-card", - item: resource, - collect: (monitor) => ({ isDragging: !!monitor.isDragging() }), - })); - - return ( -
-
-
-
-
-
- {resource.title} -
-
- {resource.emergency && ( - - Emergency - - )} -
-
-
-
-
- -
- {(resource.origin_facility_object || {}).name} -
- -
-
-
- -
- {(resource.approving_facility_object || {}).name} -
- -
- {resource.assigned_facility_object && ( -
-
- - -
- {(resource.assigned_facility_object || {}).name || - "Yet to be decided"} -
- -
- )} -
-
- -
- {formatDateTime(resource.modified_date) || "--"} -
- -
- {resource.assigned_to_object && ( -
-
- -
- {formatName(resource.assigned_to_object)} -{" "} - {resource.assigned_to_object.user_type} -
- -
- )} -
-
-
- -
-
-
-
- ); -}; - -export default function ResourceBoard({ - board, - filterProp, - formatFilter, -}: boardProps) { - const [isLoading, setIsLoading] = useState({ board: "BOARD", more: false }); - const [{ isOver }, drop] = useDrop(() => ({ - accept: "resource-card", - drop: (item: any) => { - if (item.status !== board) { - navigate(`/resource/${item.id}/update?status=${board}`); - } - }, - collect: (monitor) => ({ isOver: !!monitor.isOver() }), - })); - const [offset, setOffSet] = useState(0); - const [data, setData] = useState>(); - - useEffect(() => { - setIsLoading((loading) => reduceLoading("BOARD", loading)); - }, [ - board, - filterProp.title, - filterProp.facility, - filterProp.origin_facility, - filterProp.approving_facility, - filterProp.assigned_facility, - filterProp.emergency, - filterProp.created_date_before, - filterProp.created_date_after, - filterProp.modified_date_before, - filterProp.modified_date_after, - filterProp.ordering, - ]); - - useQuery(routes.listResourceRequests, { - query: formatFilter({ - ...filterProp, - status: board, - }), - onResponse: ({ res, data: listResourceData }) => { - if (res?.ok && listResourceData) { - setData(listResourceData); - } - setIsLoading((loading) => reduceLoading("COMPLETE", loading)); - }, - }); - - const handlePagination = async () => { - setIsLoading((loading) => reduceLoading("MORE", loading)); - setOffSet(offset + 14); - const { res, data: newPageData } = await request( - routes.listResourceRequests, - { - query: formatFilter({ - ...filterProp, - status: board, - offset: offset, - }), - }, - ); - if (res?.ok && newPageData) { - setData((prev) => - prev - ? { ...prev, results: [...prev.results, ...newPageData.results] } - : newPageData, - ); - } - setIsLoading((loading) => reduceLoading("COMPLETE", loading)); - }; - - const boardFilter = (filter: string) => { - return data?.results - .filter(({ status }) => status === filter) - .map((resource: any) => ( - - )); - }; - - return ( -
-
-
-

- {renderBoardTitle(board)}{" "} - { - const { data } = await request( - routes.downloadResourceRequests, - { - query: { - ...formatFilter({ ...filterProp, status: board }), - csv: true, - }, - }, - ); - return data ?? null; - }} - filenamePrefix={`resource_requests_${board}`} - /> -

- - {data?.count || "0"} - -
-
-
- {isLoading.board ? ( -
-
-
-
-
-
-
-
-
-
-
-
-
- ) : data && data?.results.length > 0 ? ( - boardFilter(board) - ) : ( -

No requests to show.

- )} - {!isLoading.board && - data && - data?.results.length < (data?.count || 0) && - (isLoading.more ? ( -
- Loading -
- ) : ( - - ))} -
-
- ); -} diff --git a/src/Components/Resource/ResourceBoardView.tsx b/src/Components/Resource/ResourceBoardView.tsx index bd28cc5f5d2..ed6d63bbd77 100644 --- a/src/Components/Resource/ResourceBoardView.tsx +++ b/src/Components/Resource/ResourceBoardView.tsx @@ -1,9 +1,7 @@ import { useState } from "react"; -import { navigate } from "raviger"; +import { Link, navigate } from "raviger"; import ListFilter from "./ListFilter"; -import ResourceBoard from "./ResourceBoard"; import { RESOURCE_CHOICES } from "../../Common/constants"; -import withScrolling from "react-dnd-scrolling"; import BadgesList from "./BadgesList"; import { formatFilter } from "./Commons"; import useFilters from "../../Common/hooks/useFilters"; @@ -16,10 +14,12 @@ import SearchInput from "../Form/SearchInput"; import Tabs from "../Common/components/Tabs"; 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 Loading from "@/Components/Common/Loading"; import PageTitle from "@/Components/Common/PageTitle"; -const ScrollingComponent = withScrolling("div"); const resourceStatusOptions = RESOURCE_CHOICES.map((obj) => obj.text); const COMPLETED = ["COMPLETED", "REJECTED"]; @@ -32,7 +32,6 @@ export default function BoardView() { }); const [boardFilter, setBoardFilter] = useState(ACTIVE); // eslint-disable-next-line - const [isLoading, setIsLoading] = useState(false); const appliedFilters = formatFilter(qParams); const { t } = useTranslation(); @@ -42,11 +41,11 @@ export default function BoardView() { }; return ( -
+
- - -
- {isLoading ? ( - - ) : ( - boardFilter.map((board) => ( - + title={} + sections={boardFilter.map((board) => ({ + id: board, + title: ( +

+ {board}{" "} + { + const { data } = await request( + routes.downloadResourceRequests, + { + query: { + ...formatFilter({ ...qParams, status: board }), + csv: true, + }, + }, + ); + return data ?? null; + }} + filenamePrefix={`resource_requests_${board}`} /> - )) - )} -

-
+ + ), + fetchOptions: (id) => ({ + route: routes.listResourceRequests, + options: { + query: formatFilter({ + ...qParams, + status: id, + }), + }, + }), + }))} + onDragEnd={(result) => { + if (result.source.droppableId !== result.destination?.droppableId) + navigate( + `/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")} + +
+
+ )} + />
); diff --git a/src/Components/Shifting/BoardView.tsx b/src/Components/Shifting/BoardView.tsx index dbd911483dc..f83e0a4974e 100644 --- a/src/Components/Shifting/BoardView.tsx +++ b/src/Components/Shifting/BoardView.tsx @@ -7,25 +7,26 @@ import BadgesList from "./BadgesList"; import { ExportButton } from "../Common/Export"; import ListFilter from "./ListFilter"; import SearchInput from "../Form/SearchInput"; -import ShiftingBoard from "./ShiftingBoard"; import { formatFilter } from "./Commons"; -import { navigate } from "raviger"; +import { Link, navigate } from "raviger"; import useFilters from "../../Common/hooks/useFilters"; -import { useLayoutEffect, useRef, useState } from "react"; +import { useState } from "react"; import { useTranslation } from "react-i18next"; -import withScrolling from "react-dnd-scrolling"; import ButtonV2 from "../Common/components/ButtonV2"; import { AdvancedFilterButton } from "../../CAREUI/interactive/FiltersSlideover"; import CareIcon from "../../CAREUI/icons/CareIcon"; import Tabs from "../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 "../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 Loading from "@/Components/Common/Loading"; import PageTitle from "@/Components/Common/PageTitle"; -const ScrollingComponent = withScrolling("div"); export default function BoardView() { const { qParams, updateQuery, FilterBadges, advancedFilter } = useFilters({ @@ -33,6 +34,26 @@ export default function BoardView() { 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; @@ -55,79 +76,10 @@ export default function BoardView() { ); const [boardFilter, setBoardFilter] = useState(activeBoards); - const [isLoading] = useState(false); const { t } = useTranslation(); - const containerRef = useRef(null); - const [containerHeight, setContainerHeight] = useState(0); - const [isLeftScrollable, setIsLeftScrollable] = useState(false); - const [isRightScrollable, setIsRightScrollable] = useState(false); - - useLayoutEffect(() => { - const container = containerRef.current; - - if (!container) return; - - const handleScroll = () => { - setIsLeftScrollable(container.scrollLeft > 0); - setIsRightScrollable( - container.scrollLeft + container.clientWidth < - container.scrollWidth - 10, - ); - }; - - container.addEventListener("scroll", handleScroll); - - handleScroll(); - - return () => { - container.removeEventListener("scroll", handleScroll); - }; - }, []); - - const handleOnClick = (direction: "right" | "left") => { - const container = containerRef.current; - if (direction === "left" ? !isLeftScrollable : !isRightScrollable) return; - - if (container) { - const scrollAmount = 300; - const currentScrollLeft = container.scrollLeft; - - if (direction === "left") { - container.scrollTo({ - left: currentScrollLeft - scrollAmount, - behavior: "smooth", - }); - } else if (direction === "right") { - container.scrollTo({ - left: currentScrollLeft + scrollAmount, - behavior: "smooth", - }); - } - } - }; - - const renderArrowIcons = (direction: "right" | "left") => { - const isIconEnable = - direction === "left" ? isLeftScrollable : isRightScrollable; - return ( - isIconEnable && ( -
- handleOnClick(direction)} - /> -
- ) - ); - }; return ( -
+
- - -
- {isLoading ? ( - - ) : ( - <> - {renderArrowIcons("left")} -
- {boardFilter.map((board) => ( - - ))} + + title={} + sections={boardFilter.map((board) => ({ + id: board.text, + title: ( +

+ {board.label || board.text}{" "} + { + const { data } = await request(routes.downloadShiftRequests, { + query: { ...formatFilter(qParams), 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")} + + )} +
- {renderArrowIcons("right")} - - )} -
- +
+ {( + [ + { + 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/ShiftingBoard.tsx b/src/Components/Shifting/ShiftingBoard.tsx deleted file mode 100644 index 6f2eb3b77a3..00000000000 --- a/src/Components/Shifting/ShiftingBoard.tsx +++ /dev/null @@ -1,395 +0,0 @@ -import { - Dispatch, - SetStateAction, - useEffect, - useLayoutEffect, - useRef, - useState, -} from "react"; -import { classNames, formatDateTime, formatName } from "../../Utils/utils"; -import { useDrag, useDrop } from "react-dnd"; -import ButtonV2 from "../Common/components/ButtonV2"; -import ConfirmDialog from "../Common/ConfirmDialog"; -import { navigate } from "raviger"; -import { useTranslation } from "react-i18next"; -import { ExportButton } from "../Common/Export"; -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 { PaginatedResponse } from "../../Utils/request/types"; -import { IShift } from "./models"; -import CareIcon from "../../CAREUI/icons/CareIcon"; -import careConfig from "@careConfig"; - -interface boardProps { - board: string; - title?: string; - filterProp: any; - formatFilter: any; - setContainerHeight: Dispatch>; - containerHeight: number; -} - -const ShiftCard = ({ shift, filter }: any) => { - const [modalFor, setModalFor] = useState({ - externalId: undefined, - loading: false, - }); - const [{ isDragging }, drag] = useDrag(() => ({ - type: "shift-card", - item: shift, - collect: (monitor) => ({ isDragging: !!monitor.isDragging() }), - })); - const authUser = useAuthUser(); - const { t } = useTranslation(); - - 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`, - ); - }; - return ( -
-
-
-
-
-
- {shift.patient_object.name} - {shift.patient_object.age} -
-
- {shift.emergency && ( - - {t("emergency")} - - )} -
-
-
-
-
- -
- {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 || "--"} -
- -
- - {shift.assigned_to_object && ( -
-
- -
- {formatName(shift.assigned_to_object)} - {" - "} - {shift.assigned_to_object.user_type} -
- -
- )} - -
-
- -
- {shift.patient_object.state_object.name || "--"} -
- -
-
-
- -
- -
- {filter === "COMPLETED" && shift.assigned_facility && ( -
- setModalFor(shift.external_id)} - > - {t("transfer_to_receiving_facility")} - - - - setModalFor({ externalId: undefined, loading: false }) - } - action={t("confirm")} - onConfirm={() => handleTransferComplete(shift)} - > -

- {t("redirected_to_create_consultation")} -

-
-
- )} -
-
-
- ); -}; - -export default function ShiftingBoard({ - board, - title, - filterProp, - formatFilter, - setContainerHeight, - containerHeight, -}: boardProps) { - const containerRef = useRef(null); - const [offset, setOffSet] = useState(0); - const [pages, setPages] = useState[]>([]); - const [isLoading, setIsLoading] = useState(true); - const [{ isOver }, drop] = useDrop(() => ({ - accept: "shift-card", - drop: (item: any) => { - if (item.status !== board) { - navigate(`/shifting/${item.id}/update?status=${board}`); - } - }, - collect: (monitor) => ({ isOver: !!monitor.isOver() }), - })); - - const query = useQuery(routes.listShiftRequests, { - query: formatFilter({ - ...filterProp, - status: board, - }), - onResponse: ({ res, data: listShiftData }) => { - setIsLoading(false); - if (res?.ok && listShiftData) { - setPages((prev) => [...prev, listShiftData]); - } - }, - }); - - useEffect(() => { - setPages([]); - setIsLoading(true); - query.refetch(); - }, [ - filterProp.facility, - filterProp.origin_facility, - filterProp.shifting_approving_facility, - filterProp.assigned_facility, - filterProp.emergency, - filterProp.is_up_shift, - filterProp.patient_name, - filterProp.created_date_before, - filterProp.created_date_after, - filterProp.modified_date_before, - filterProp.modified_date_after, - filterProp.patient_phone_number, - filterProp.ordering, - filterProp.is_kasp, - filterProp.assigned_to, - filterProp.is_antenatal, - filterProp.breathlessness_level, - ]); - - const handlePagination = async () => { - setIsLoading(true); - setOffSet(offset + 14); - const { res, data: newPageData } = await request(routes.listShiftRequests, { - query: formatFilter({ - ...filterProp, - status: board, - offset: offset, - }), - }); - if (res?.ok && newPageData) { - setPages((prev) => [...prev, newPageData]); - } - setIsLoading(false); - }; - const { t } = useTranslation(); - - const patientFilter = (filter: string) => { - return pages - .flatMap((p) => p.results) - .filter(({ status }) => status === filter) - .map((shift: any) => ( - - )); - }; - - useLayoutEffect(() => { - const container = containerRef.current; - if (container) { - const { height } = container.getBoundingClientRect(); - containerHeight < height && setContainerHeight(height); - } - }, [containerRef.current, pages.flatMap((p) => p.results).length]); - - return ( -
-
-
-

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

- - {pages[0] ? pages[0].count : "..."} - -
-
-
- {pages[0]?.count > 0 - ? patientFilter(board) - : !isLoading && ( -

{t("no_patients_to_show")}

- )} - {isLoading ? ( -
-
-
-
-
-
-
-
-
-
-
-
-
- ) : ( - pages.at(-1)?.next && ( - handlePagination()} className="m-2 block"> - Load More - - ) - )} -
-
- ); -} diff --git a/src/Locale/en.json b/src/Locale/en.json index 64cdea9bd7a..4102a480721 100644 --- a/src/Locale/en.json +++ b/src/Locale/en.json @@ -961,7 +961,7 @@ "modified_date": "Modified Date", "no_patients_to_show": "No patients to show.", "shifting_status": "Shifting status", - "transfer_to_receiving_facility": "TRANSFER TO RECEIVING FACILITY", + "transfer_to_receiving_facility": "Transfer to receiving facility", "confirm_transfer_complete": "Confirm Transfer Complete!", "mark_transfer_complete_confirmation": "Are you sure you want to mark this transfer as complete? The Origin facility will no longer have access to this patient", "board_view": "Board View", @@ -1042,6 +1042,10 @@ "date_declared_positive": "Date of declaring positive", "date_of_result": "Covid confirmation date", "is_vaccinated": "Whether vaccinated", + "resource_origin_facility": "Origin Facility", + "resource_approving_facility" : "Resource approving facility", "consultation_not_filed": "You have not filed any consultation for this patient yet.", - "consultation_not_filed_description": "Please file a consultation for this patient to continue." + "consultation_not_filed_description": "Please file a consultation for this patient to continue.", + "width": "Width ({{unit}})", + "length": "Length ({{unit}})" } diff --git a/src/Routers/routes/ResourceRoutes.tsx b/src/Routers/routes/ResourceRoutes.tsx index 4ffa5bd94ad..d75c933f760 100644 --- a/src/Routers/routes/ResourceRoutes.tsx +++ b/src/Routers/routes/ResourceRoutes.tsx @@ -1,5 +1,3 @@ -import { DndProvider } from "react-dnd"; -import { HTML5Backend } from "react-dnd-html5-backend"; import ResourceDetails from "../../Components/Resource/ResourceDetails"; import { ResourceDetailsUpdate } from "../../Components/Resource/ResourceDetailsUpdate"; import ListView from "../../Components/Resource/ListView"; @@ -12,11 +10,7 @@ const getDefaultView = () => const ResourceRoutes: AppRoutes = { "/resource": () => , - "/resource/board": () => ( - - - - ), + "/resource/board": () => , "/resource/list": () => , "/resource/:id": ({ id }) => , "/resource/:id/update": ({ id }) => , diff --git a/src/Routers/routes/ShiftingRoutes.tsx b/src/Routers/routes/ShiftingRoutes.tsx index 5118b246430..dc11ff2b8ee 100644 --- a/src/Routers/routes/ShiftingRoutes.tsx +++ b/src/Routers/routes/ShiftingRoutes.tsx @@ -1,5 +1,3 @@ -import { DndProvider } from "react-dnd"; -import { HTML5Backend } from "react-dnd-html5-backend"; import { ShiftCreate } from "../../Components/Patient/ShiftCreate"; import ShiftDetails from "../../Components/Shifting/ShiftDetails"; import { ShiftDetailsUpdate } from "../../Components/Shifting/ShiftDetailsUpdate"; @@ -13,11 +11,7 @@ const getDefaultView = () => const ShiftingRoutes: AppRoutes = { "/shifting": () => , - "/shifting/board": () => ( - - - - ), + "/shifting/board": () => , "/shifting/list": () => , "/shifting/:id": ({ id }) => , "/shifting/:id/update": ({ id }) => ,