From a964e294497812e882cd9cc34265dcde38f1548a Mon Sep 17 00:00:00 2001 From: Jacob John Jeevan Date: Fri, 31 Jan 2025 02:32:54 +0530 Subject: [PATCH 1/4] Resources redesign --- public/locale/en.json | 13 +- public/locale/hi.json | 1 - public/locale/kn.json | 1 - public/locale/ml.json | 1 - public/locale/ta.json | 1 - src/Routers/routes/ResourceRoutes.tsx | 6 +- src/common/constants.tsx | 19 +- src/components/Resource/ResourceBadges.tsx | 66 --- src/components/Resource/ResourceBlock.tsx | 97 ---- src/components/Resource/ResourceBoard.tsx | 159 ------ src/components/Resource/ResourceCommons.tsx | 41 -- .../Resource/ResourceDetailsUpdate.tsx | 8 +- src/components/Resource/ResourceFilter.tsx | 222 -------- src/components/Resource/ResourceList.tsx | 515 +++++++++--------- 14 files changed, 293 insertions(+), 857 deletions(-) delete mode 100644 src/components/Resource/ResourceBadges.tsx delete mode 100644 src/components/Resource/ResourceBlock.tsx delete mode 100644 src/components/Resource/ResourceBoard.tsx delete mode 100644 src/components/Resource/ResourceCommons.tsx delete mode 100644 src/components/Resource/ResourceFilter.tsx diff --git a/public/locale/en.json b/public/locale/en.json index b4baf2c5f19..794d07c5993 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -323,6 +323,7 @@ "additional_instructions": "Additional Instructions", "address": "Address", "address_is_required": "Address is required", + "adjust_resource_filters": "Try adjusting your filters or create a new resource", "administer": "Administer", "administer_medicine": "Administer Medicine", "administer_medicines": "Administer Medicines", @@ -1363,6 +1364,7 @@ "no_records_found": "No Records Found", "no_remarks": "No remarks", "no_resource_requests_found": "No requests found", + "no_resources_found": "No resources found", "no_results": "No results", "no_results_found": "No Results Found", "no_schedule_templates_found": "No schedule templates found for this month.", @@ -1729,6 +1731,14 @@ "resource_request_details_description": "Provide detailed information about what resource is needed and why.", "resource_requests": "Requests", "resource_status": "Request Status", + "resource_status__approved": "Approved", + "resource_status__cancelled": "Cancelled", + "resource_status__completed": "Completed", + "resource_status__pending": "Pending", + "resource_status__rejected": "Rejected", + "resource_status__transfer_in_progress": "Transfer in progress", + "resource_status__transportation_to_be_arranged": "Transportation to be arranged", + "resource_title": "Resource Title", "resource_type": "Request Type", "result": "Result", "result_date": "Result Date", @@ -1799,6 +1809,7 @@ "search_by_patient_name": "Search by Patient Name", "search_by_patient_no": "Search by Patient Number", "search_by_phone_number": "Search by Phone Number", + "search_by_resource_title": "Search by resource title", "search_by_username": "Search by username", "search_country": "Search country...", "search_encounters": "Search Encounters", @@ -1993,6 +2004,7 @@ "third_party_software_licenses": "Third Party Software Licenses", "time": "Time", "time_slot": "Time Slot", + "title": "Title", "title_of_request": "Title of Request", "titrate_dosage": "Titrate Dosage", "to": "to", @@ -2011,7 +2023,6 @@ "total_users": "Total Users", "transfer_allowed": "Transfer Allowed", "transfer_blocked": "Transfer Blocked", - "transfer_in_progress": "TRANSFER IN PROGRESS", "transfer_status_updated": "Transfer Status Updated", "transfer_to_receiving_facility": "Transfer to receiving facility", "travel_within_last_28_days": "Domestic/international Travel (within last 28 days)", diff --git a/public/locale/hi.json b/public/locale/hi.json index 5afef1fa777..28b6d1f3531 100644 --- a/public/locale/hi.json +++ b/public/locale/hi.json @@ -733,7 +733,6 @@ "to_be_conducted": "संचालित किया जाना है", "total_beds": "कुल बिस्तर", "total_users": "कुल उपयोगकर्ता", - "transfer_in_progress": "स्थानांतरण प्रगति पर है", "transfer_to_receiving_facility": "प्राप्ति सुविधा में स्थानांतरण", "travel_within_last_28_days": "घरेलू/अंतर्राष्ट्रीय यात्रा (पिछले 28 दिनों के भीतर)", "treating_doctor": "इलाज करने वाला डॉक्टर", diff --git a/public/locale/kn.json b/public/locale/kn.json index 0d73df7f8b8..45c08a5568a 100644 --- a/public/locale/kn.json +++ b/public/locale/kn.json @@ -735,7 +735,6 @@ "to_be_conducted": "ನಡೆಸಲಾಗುವುದು", "total_beds": "ಒಟ್ಟು ಹಾಸಿಗೆಗಳು", "total_users": "ಒಟ್ಟು ಬಳಕೆದಾರರು", - "transfer_in_progress": "ವರ್ಗಾವಣೆ ಪ್ರಗತಿಯಲ್ಲಿದೆ", "transfer_to_receiving_facility": "ಸ್ವೀಕರಿಸುವ ಸೌಲಭ್ಯಕ್ಕೆ ವರ್ಗಾಯಿಸಿ", "travel_within_last_28_days": "ದೇಶೀಯ/ಅಂತರರಾಷ್ಟ್ರೀಯ ಪ್ರಯಾಣ (ಕಳೆದ 28 ದಿನಗಳಲ್ಲಿ)", "treating_doctor": "ಚಿಕಿತ್ಸೆ ನೀಡುತ್ತಿರುವ ವೈದ್ಯರು", diff --git a/public/locale/ml.json b/public/locale/ml.json index 3d18c5890ae..c341eed70ab 100644 --- a/public/locale/ml.json +++ b/public/locale/ml.json @@ -1778,7 +1778,6 @@ "transcript_information": "ഇതാണ് നമ്മൾ കേട്ടത്", "transfer_allowed": "കൈമാറ്റം അനുവദിച്ചു", "transfer_blocked": "കൈമാറ്റം തടഞ്ഞു", - "transfer_in_progress": "കൈമാറ്റം പുരോഗമിക്കുന്നു", "transfer_status_updated": "ട്രാൻസ്ഫർ സ്റ്റാറ്റസ് അപ്ഡേറ്റ് ചെയ്തു", "transfer_to_receiving_facility": "സ്വീകരിക്കാനുള്ള സൗകര്യത്തിലേക്ക് ട്രാൻസ്ഫർ ചെയ്യുക", "travel_within_last_28_days": "ആഭ്യന്തര/അന്താരാഷ്ട്ര യാത്ര (കഴിഞ്ഞ 28 ദിവസത്തിനുള്ളിൽ)", diff --git a/public/locale/ta.json b/public/locale/ta.json index 50e884b2d1d..2813031dc81 100644 --- a/public/locale/ta.json +++ b/public/locale/ta.json @@ -733,7 +733,6 @@ "to_be_conducted": "நடத்தப்பட வேண்டும்", "total_beds": "மொத்த படுக்கைகள்", "total_users": "மொத்த பயனர்கள்", - "transfer_in_progress": "இடமாற்றம் நடைபெறுகிறது", "transfer_to_receiving_facility": "பெறும் வசதிக்கு இடமாற்றம்", "travel_within_last_28_days": "உள்நாட்டு/சர்வதேச பயணம் (கடந்த 28 நாட்களுக்குள்)", "treating_doctor": "சிகிச்சை அளிக்கும் மருத்துவர்", diff --git a/src/Routers/routes/ResourceRoutes.tsx b/src/Routers/routes/ResourceRoutes.tsx index 357b7812513..74d96ba622b 100644 --- a/src/Routers/routes/ResourceRoutes.tsx +++ b/src/Routers/routes/ResourceRoutes.tsx @@ -1,13 +1,11 @@ -import View from "@/components/Common/View"; -import BoardView from "@/components/Resource/ResourceBoard"; import ResourceDetails from "@/components/Resource/ResourceDetails"; import { ResourceDetailsUpdate } from "@/components/Resource/ResourceDetailsUpdate"; -import ListView from "@/components/Resource/ResourceList"; +import ResourceList from "@/components/Resource/ResourceList"; import { AppRoutes } from "@/Routers/AppRouter"; const ResourceRoutes: AppRoutes = { - "/resource": () => , + "/resource": () => , "/resource/:id": ({ id }) => , "/resource/:id/update": ({ id }) => , }; diff --git a/src/common/constants.tsx b/src/common/constants.tsx index 820ace57839..4ed7bb19f19 100644 --- a/src/common/constants.tsx +++ b/src/common/constants.tsx @@ -162,16 +162,15 @@ export const RESOURCE_CATEGORY_CHOICES = [ { id: "FINANCIAL", text: "Financial" }, { id: "OTHERS", text: "Other" }, ]; - -export const RESOURCE_CHOICES: Array = [ - { id: 10, text: "PENDING" }, - { id: 15, text: "ON HOLD" }, - { id: 20, text: "APPROVED" }, - { id: 30, text: "REJECTED" }, - { id: 55, text: "TRANSPORTATION TO BE ARRANGED" }, - { id: 70, text: "TRANSFER IN PROGRESS" }, - { id: 80, text: "COMPLETED" }, -]; +export const RESOURCE_STATUS_CHOICES = [ + { icon: "l-clock", text: "pending" }, + { icon: "l-check", text: "approved" }, + { icon: "l-ban", text: "rejected" }, + { icon: "l-file-slash", text: "cancelled" }, + { icon: "l-truck", text: "transportation_to_be_arranged" }, + { icon: "l-spinner", text: "transfer_in_progress" }, + { icon: "l-check-circle", text: "completed" }, +] as const; export const RESOURCE_FILTER_ORDER: Array = [ { id: 1, text: "created_date", desc: "ASC Created Date" }, diff --git a/src/components/Resource/ResourceBadges.tsx b/src/components/Resource/ResourceBadges.tsx deleted file mode 100644 index 7be946c27a9..00000000000 --- a/src/components/Resource/ResourceBadges.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { SHIFTING_FILTER_ORDER } from "@/common/constants"; - -import routes from "@/Utils/request/api"; -import useTanStackQueryInstead from "@/Utils/request/useQuery"; - -export function useFacilityQuery(facilityId: string | undefined) { - return useTanStackQueryInstead(routes.getAnyFacility, { - pathParams: { id: facilityId as string }, - prefetch: !!facilityId, - }); -} - -export default function BadgesList(props: any) { - const { appliedFilters, FilterBadges } = props; - const originFacility = useFacilityQuery(appliedFilters.origin_facility); - const approvingFacility = useFacilityQuery(appliedFilters.approving_facility); - const assignedFacility = useFacilityQuery(appliedFilters.assigned_facility); - - const getDescShiftingFilterOrder = (ordering: any) => { - const foundItem = SHIFTING_FILTER_ORDER.find( - (item) => item.text === ordering, - ); - return foundItem ? foundItem.desc : ""; - }; - - return ( - [ - value( - "Ordering", - "ordering", - getDescShiftingFilterOrder(appliedFilters.ordering), - ), - badge("Status", "status"), - badge("Title", "title"), - boolean("Emergency", "emergency", { - trueValue: "yes", - falseValue: "no", - }), - ...dateRange("Modified", "modified_date"), - ...dateRange("Created", "created_date"), - value( - "Origin facility", - "origin_facility", - appliedFilters.origin_facility - ? originFacility?.data?.name || "" - : "", - ), - value( - "Approving facility", - "approving_facility", - appliedFilters.approving_facility - ? approvingFacility?.data?.name || "" - : "", - ), - value( - "Assigned facility", - "assigned_facility", - appliedFilters.assigned_facility - ? assignedFacility?.data?.name || "" - : "", - ), - ]} - /> - ); -} diff --git a/src/components/Resource/ResourceBlock.tsx b/src/components/Resource/ResourceBlock.tsx deleted file mode 100644 index 76420952b97..00000000000 --- a/src/components/Resource/ResourceBlock.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import dayjs from "dayjs"; -import { Link } from "raviger"; -import { useTranslation } from "react-i18next"; - -import CareIcon from "@/CAREUI/icons/CareIcon"; - -import { classNames, formatDateTime, formatName } from "@/Utils/utils"; -import { ResourceRequest } from "@/types/resourceRequest/resourceRequest"; - -export default function ResourceBlock(props: { resource: ResourceRequest }) { - const { resource } = props; - const { t } = useTranslation(); - - return ( -
-
-
-
-
{resource.title}
-
-
- {resource.emergency && ( - - {t("emergency")} - - )} -
-
-
- {( - [ - { - title: "origin_facility", - icon: "l-plane-departure", - data: resource?.origin_facility?.name, - }, - { - title: "resource_approving_facility", - icon: "l-user-check", - data: resource?.approving_facility?.name, - }, - { - title: "assigned_facility", - icon: "l-plane-arrival", - data: - resource.assigned_facility?.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 - ? formatName(resource.assigned_to) + - " - " + - resource.assigned_to.user_type - : undefined, - }, - ] as const - ) - .filter((d) => d.data) - .map((datapoint, i) => ( -
-
- -
-
{datapoint.data}
-
- ))} -
-
-
- - {t("all_details")} - -
-
- ); -} diff --git a/src/components/Resource/ResourceBoard.tsx b/src/components/Resource/ResourceBoard.tsx deleted file mode 100644 index b048c3c6cd4..00000000000 --- a/src/components/Resource/ResourceBoard.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import { navigate } from "raviger"; -import { Suspense, lazy, useState } from "react"; -import { useTranslation } from "react-i18next"; - -import CareIcon from "@/CAREUI/icons/CareIcon"; -import { AdvancedFilterButton } from "@/CAREUI/interactive/FiltersSlideover"; - -import { Button } from "@/components/ui/button"; - -import { ExportButton } from "@/components/Common/Export"; -import Loading from "@/components/Common/Loading"; -import PageTitle from "@/components/Common/PageTitle"; -import Tabs from "@/components/Common/Tabs"; -import SearchInput from "@/components/Form/SearchInput"; -import type { KanbanBoardType } from "@/components/Kanban/Board"; -import BadgesList from "@/components/Resource/ResourceBadges"; -import ResourceBlock from "@/components/Resource/ResourceBlock"; -import { formatFilter } from "@/components/Resource/ResourceCommons"; -import ListFilter from "@/components/Resource/ResourceFilter"; - -import useFilters from "@/hooks/useFilters"; - -import { RESOURCE_CHOICES } from "@/common/constants"; - -import routes from "@/Utils/request/api"; -import request from "@/Utils/request/request"; -import { useView } from "@/Utils/useView"; -import { ResourceRequest } from "@/types/resourceRequest/resourceRequest"; - -const KanbanBoard = lazy( - () => import("@/components/Kanban/Board"), -) as KanbanBoardType; - -const resourceStatusOptions = RESOURCE_CHOICES.map((obj) => obj.text); - -const COMPLETED = ["COMPLETED", "REJECTED"]; -const ACTIVE = resourceStatusOptions.filter((o) => !COMPLETED.includes(o)); - -export default function BoardView() { - const [, setView] = useView("resource", "board"); - const { qParams, FilterBadges, advancedFilter, updateQuery } = useFilters({ - limit: -1, - cacheBlacklist: ["title"], - }); - const [boardFilter, setBoardFilter] = useState(ACTIVE); - - const appliedFilters = formatFilter(qParams); - const { t } = useTranslation(); - - return ( -
-
-
- { - const { data } = await request( - routes.downloadResourceRequests, - { - query: { ...appliedFilters, csv: true }, - }, - ); - return data ?? null; - }} - filenamePrefix="resource_requests" - /> - } - breadcrumbs={false} - /> -
- -
- updateQuery({ [e.name]: e.value })} - placeholder={t("search_resource")} - className="w-full md:w-60" - /> - setBoardFilter(tab ? COMPLETED : ACTIVE)} - currentTab={boardFilter !== ACTIVE ? 1 : 0} - /> -
- - advancedFilter.setShow(true)} - /> -
-
-
- }> - - 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) => } - /> -
- - -
- ); -} diff --git a/src/components/Resource/ResourceCommons.tsx b/src/components/Resource/ResourceCommons.tsx deleted file mode 100644 index e63eb9a71b2..00000000000 --- a/src/components/Resource/ResourceCommons.tsx +++ /dev/null @@ -1,41 +0,0 @@ -export const initialFilterData = { - status: "--", - facility: "", - origin_facility: "", - approving_facility: "", - assigned_facility: "", - emergency: "--", - limit: 14, - created_date_before: null, - created_date_after: null, - modified_date_before: null, - modified_date_after: null, - offset: 0, - ordering: null, - title: "", -}; - -export const formatFilter = (params: any) => { - const filter = { ...initialFilterData, ...params }; - return { - status: filter.status === "--" ? null : filter.status, - facility: "", - origin_facility: filter.origin_facility || undefined, - approving_facility: filter.approving_facility || undefined, - assigned_facility: filter.assigned_facility || undefined, - emergency: - (filter.emergency && filter.emergency) === "--" - ? "" - : filter.emergency === "yes" - ? "true" - : "false", - limit: filter.limit || 14, - offset: filter.offset, - created_date_before: filter.created_date_before || undefined, - created_date_after: filter.created_date_after || undefined, - modified_date_before: filter.modified_date_before || undefined, - modified_date_after: filter.modified_date_after || undefined, - ordering: filter.ordering || undefined, - title: filter.title || undefined, - }; -}; diff --git a/src/components/Resource/ResourceDetailsUpdate.tsx b/src/components/Resource/ResourceDetailsUpdate.tsx index 8d3bb755796..8af456caeaa 100644 --- a/src/components/Resource/ResourceDetailsUpdate.tsx +++ b/src/components/Resource/ResourceDetailsUpdate.tsx @@ -23,7 +23,7 @@ import { UserModel } from "@/components/Users/models"; import useAppHistory from "@/hooks/useAppHistory"; -import { RESOURCE_CHOICES } from "@/common/constants"; +import { RESOURCE_STATUS_CHOICES } from "@/common/constants"; import routes from "@/Utils/request/api"; import request from "@/Utils/request/request"; @@ -34,8 +34,6 @@ interface resourceProps { id: string; } -const resourceStatusOptions = RESOURCE_CHOICES.map((obj) => obj.text); - const initForm: Partial = { assigned_facility: null, emergency: false, @@ -205,9 +203,9 @@ export const ResourceDetailsUpdate = (props: resourceProps) => { label="Status" name="status" value={state.form.status} - options={resourceStatusOptions} + options={RESOURCE_STATUS_CHOICES} onChange={handleChange} - optionLabel={(option) => option} + optionLabel={(option) => t(`resource_status__${option}`)} />
diff --git a/src/components/Resource/ResourceFilter.tsx b/src/components/Resource/ResourceFilter.tsx deleted file mode 100644 index e161fcf819a..00000000000 --- a/src/components/Resource/ResourceFilter.tsx +++ /dev/null @@ -1,222 +0,0 @@ -import dayjs from "dayjs"; - -import FiltersSlideover from "@/CAREUI/interactive/FiltersSlideover"; - -import CircularProgress from "@/components/Common/CircularProgress"; -import { DateRange } from "@/components/Common/DateRangeInputV2"; -import { FacilitySelect } from "@/components/Common/FacilitySelect"; -import DateRangeFormField from "@/components/Form/FormFields/DateRangeFormField"; -import { FieldLabel } from "@/components/Form/FormFields/FormField"; -import { SelectFormField } from "@/components/Form/FormFields/SelectFormField"; -import { FieldChangeEvent } from "@/components/Form/FormFields/Utils"; - -import useMergeState from "@/hooks/useMergeState"; - -import { RESOURCE_FILTER_ORDER } from "@/common/constants"; -import { RESOURCE_CHOICES } from "@/common/constants"; - -import routes from "@/Utils/request/api"; -import useTanStackQueryInstead from "@/Utils/request/useQuery"; -import { dateQueryString } from "@/Utils/utils"; - -const getDate = (value: any) => - value && dayjs(value).isValid() && dayjs(value).toDate(); - -export default function ListFilter(props: any) { - const { filter, onChange, closeFilter, removeFilters } = props; - const [filterState, setFilterState] = useMergeState({ - origin_facility: filter.origin_facility || null, - origin_facility_ref: null, - approving_facility: filter.approving_facility || null, - approving_facility_ref: null, - assigned_facility: filter.assigned_facility || null, - assigned_facility_ref: null, - emergency: filter.emergency || null, - created_date_before: filter.created_date_before || null, - created_date_after: filter.created_date_after || null, - modified_date_before: filter.modified_date_before || null, - modified_date_after: filter.modified_date_after || null, - ordering: filter.ordering || null, - status: filter.status || null, - }); - - const { loading: orginFacilityLoading } = useTanStackQueryInstead( - routes.getAnyFacility, - { - prefetch: filter.origin_facility !== undefined, - pathParams: { id: filter.origin_facility }, - onResponse: ({ res, data }) => { - if (res && data) { - setFilterState({ - origin_facility_ref: filter.origin_facility === "" ? "" : data, - }); - } - }, - }, - ); - - const { loading: resourceFacilityLoading } = useTanStackQueryInstead( - routes.getAnyFacility, - { - prefetch: filter.approving_facility !== undefined, - pathParams: { id: filter.approving_facility }, - onResponse: ({ res, data }) => { - if (res && data) { - setFilterState({ - approving_facility_ref: - filter.approving_facility === "" ? "" : data, - }); - } - }, - }, - ); - - const setFacility = (selected: any, name: string) => { - setFilterState({ - ...filterState, - [`${name}_ref`]: selected, - [name]: (selected || {}).id, - }); - }; - - const handleChange = (e: FieldChangeEvent) => { - setFilterState({ ...filterState, [e.name]: e.value }); - }; - - const applyFilter = () => { - const { - origin_facility, - approving_facility, - assigned_facility, - emergency, - created_date_before, - created_date_after, - modified_date_before, - modified_date_after, - ordering, - status, - } = filterState; - const data = { - origin_facility: origin_facility || "", - approving_facility: approving_facility || "", - assigned_facility: assigned_facility || "", - emergency: emergency || "", - created_date_before: dateQueryString(created_date_before), - created_date_after: dateQueryString(created_date_after), - modified_date_before: dateQueryString(modified_date_before), - modified_date_after: dateQueryString(modified_date_after), - ordering: ordering || "", - status: status || "", - }; - onChange(data); - }; - - const handleDateRangeChange = (event: FieldChangeEvent) => { - const filterData = { ...filterState }; - filterData[`${event.name}_after`] = event.value.start?.toString(); - filterData[`${event.name}_before`] = event.value.end?.toString(); - setFilterState(filterData); - }; - - return ( - { - removeFilters(); - closeFilter(); - }} - > - {props.showResourceStatus && ( - option.text} - optionValue={(option) => option.text} - onChange={handleChange} - placeholder="Show all" - errorClassName="hidden" - /> - )} - -
- Origin facility - {orginFacilityLoading && filter.origin_facility ? ( - - ) : ( - setFacility(obj, "origin_facility")} - className="resource-page-filter-dropdown" - errors={""} - /> - )} -
- -
- Request approving facility - {filter.approving_facility && resourceFacilityLoading ? ( - - ) : ( - setFacility(obj, "approving_facility")} - className="resource-page-filter-dropdown" - errors={""} - /> - )} -
- - option.desc} - optionValue={(option) => option.text} - onChange={handleChange} - placeholder="None" - errorClassName="hidden" - /> - - option} - optionValue={(option) => option} - onChange={handleChange} - placeholder="Show all" - errorClassName="hidden" - /> - - - -
- ); -} diff --git a/src/components/Resource/ResourceList.tsx b/src/components/Resource/ResourceList.tsx index a754455b772..acf363305ac 100644 --- a/src/components/Resource/ResourceList.tsx +++ b/src/components/Resource/ResourceList.tsx @@ -1,285 +1,304 @@ +import { useQuery } from "@tanstack/react-query"; +import { t } from "i18next"; import { Link } from "raviger"; -import { useTranslation } from "react-i18next"; + +import { cn } from "@/lib/utils"; import CareIcon from "@/CAREUI/icons/CareIcon"; -import { AdvancedFilterButton } from "@/CAREUI/interactive/FiltersSlideover"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Separator } from "@/components/ui/separator"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { ExportButton } from "@/components/Common/Export"; -import Loading from "@/components/Common/Loading"; import Page from "@/components/Common/Page"; -import SearchInput from "@/components/Form/SearchInput"; -import BadgesList from "@/components/Resource/ResourceBadges"; -import { formatFilter } from "@/components/Resource/ResourceCommons"; -import ListFilter from "@/components/Resource/ResourceFilter"; +import SearchByMultipleFields from "@/components/Common/SearchByMultipleFields"; +import { CardGridSkeleton } from "@/components/Common/SkeletonLoading"; import useFilters from "@/hooks/useFilters"; +import { + RESOURCE_CATEGORY_CHOICES, + RESOURCE_STATUS_CHOICES, +} from "@/common/constants"; + import routes from "@/Utils/request/api"; -import request from "@/Utils/request/request"; -import useTanStackQueryInstead from "@/Utils/request/useQuery"; -import { useView } from "@/Utils/useView"; -import { formatDateTime } from "@/Utils/utils"; +import query from "@/Utils/request/query"; +import { PaginatedResponse } from "@/Utils/request/types"; import { ResourceRequest } from "@/types/resourceRequest/resourceRequest"; -export default function ListView() { - const [, setView] = useView("resource", "list"); - const { - qParams, - Pagination, - FilterBadges, - advancedFilter, - resultsPerPage, - updateQuery, - } = useFilters({ cacheBlacklist: ["title"], limit: 12 }); +const COMPLETED = ["completed", "rejected", "cancelled"]; +const ACTIVE = RESOURCE_STATUS_CHOICES.map((o) => o.text).filter( + (o) => !COMPLETED.includes(o), +); - const { t } = useTranslation(); +function EmptyState() { + return ( + +
+ +
+

{t("no_resources_found")}

+

+ {t("adjust_resource_filters")} +

+
+ ); +} - const appliedFilters = formatFilter(qParams); +export default function ResourceList() { + const { qParams, updateQuery, Pagination, resultsPerPage } = useFilters({ + limit: 15, + cacheBlacklist: ["title"], + }); + const { status, title } = qParams; - const { loading, data, refetch } = useTanStackQueryInstead( - routes.listResourceRequests, + const searchOptions = [ { - query: formatFilter({ - ...qParams, - limit: resultsPerPage, - offset: (qParams.page ? qParams.page - 1 : 0) * resultsPerPage, - }), + key: "title", + label: "Title", + type: "text" as const, + placeholder: t("search_by_resource_title"), + value: title || "", }, - ); + ]; - const showResourceCardList = (data: ResourceRequest[]) => { - if (data && !data.length) { - return ( -
- {t("no_results_found")} -
- ); - } + const isActive = !status || !COMPLETED.includes(status); + const currentStatuses = isActive ? ACTIVE : COMPLETED; - return data.map((resource: ResourceRequest, i) => ( -
-
-
-
{resource.title}
-
+ // Set default status based on active/completed tab + const defaultStatus = isActive ? "pending" : "completed"; + const currentStatus = status || defaultStatus; -
-
-
- -
- {resource.category || ""} -
- -
-
+ const { data: queryResources, isLoading } = useQuery< + PaginatedResponse + >({ + queryKey: ["resources", qParams], + queryFn: query.debounced(routes.listResourceRequests, { + queryParams: { + status: currentStatus, + title, + limit: resultsPerPage, + offset: ((qParams.page || 1) - 1) * resultsPerPage, + }, + }), + }); -
-
- {resource.status === "TRANSPORTATION TO BE ARRANGED" ? ( -
- - - - - {t(`${resource.status}`)} - -
- ) : ( -
- - - - - {t(`${resource.status}`)} - -
- )} + const resources = queryResources?.results || []; -
- {resource.emergency && ( - - {t("emergency")} - - )} + return ( + +
+
+
+
+
+ + + + + event.preventDefault()} + > +
+

+ {t("search_resource")} +

+ + updateQuery({ + status: currentStatus, + title: undefined, + }) + } + onSearch={(key, value) => + updateQuery({ + status: currentStatus, + [key]: value || undefined, + }) + } + className="w-full border-none shadow-none" + /> +
+
+
-
-
-
- -
- {formatDateTime(resource.modified_date) || "--"} -
- +
+ + + + updateQuery({ + status: "pending", + title, + }) + } + > + {t("active")} + + + updateQuery({ + status: "completed", + title, + }) + } + > + {t("completed")} + + + +
-
- -
-
- -
- {resource.origin_facility?.name} -
- -
- -
- {resource.approving_facility?.name} -
- + -
- -
- {resource.assigned_facility?.name || t("yet_to_be_decided")} -
- -
-
- - {t("all_details")} - +
+ + + {currentStatuses.map((statusOption) => ( + + updateQuery({ + status: statusOption, + title, + }) + } + > + o.text === statusOption, + )?.icon || "l-folder-open" + } + className="mr-2 h-4 w-4" + /> + {t(`resource_status__${statusOption}`)} + + ))} + + +
-
- )); - }; - - 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")} - /> -
- -
- - advancedFilter.setShow(true)} - /> -
- - } - > - -
- {loading ? ( - - ) : ( -
-
- +
+ {isLoading ? ( + + ) : resources.length === 0 ? ( +
+
-
-
- {t("resource")} -
-
- {t("LOG_UPDATE_FIELD_LABEL__patient_category")} -
-
- {t("consent__status")} -
-
- {t("facilities")} -
-
- {t("LOG_UPDATE_FIELD_LABEL__action")} -
-
-
{showResourceCardList(data?.results || [])}
-
- -
-
- )} + ) : ( + <> + {resources.map((resource: ResourceRequest) => ( + + +
+ + {resource.title} + +
+ + {resource.reason} + +
+ +
+
+ {resource.emergency && ( + + {t("emergency")} + + )} + + { + RESOURCE_CATEGORY_CHOICES.find( + (o) => o.id === resource.category, + )?.text + } + +
+ + + View Details + + +
+
+
+ ))} + {queryResources?.count && + queryResources.count > resultsPerPage && ( +
+ +
+ )} + + )} +
- ); } From d3580a98a285254e902cde187b23d32af25531dd Mon Sep 17 00:00:00 2001 From: Jacob John Jeevan Date: Fri, 31 Jan 2025 15:58:19 +0530 Subject: [PATCH 2/4] switching urls, styling tweaks, outgoing/incoming --- public/locale/en.json | 2 + src/Routers/routes/ResourceRoutes.tsx | 12 +- .../PatientDetailsTab/ResourceRequests.tsx | 6 +- src/components/Resource/ResourceCreate.tsx | 2 +- src/components/Resource/ResourceDetails.tsx | 24 +++- .../Resource/ResourceDetailsUpdate.tsx | 8 +- src/components/Resource/ResourceList.tsx | 123 +++++++++++++----- src/components/ui/sidebar/facility-nav.tsx | 6 +- 8 files changed, 135 insertions(+), 48 deletions(-) diff --git a/public/locale/en.json b/public/locale/en.json index 794d07c5993..5df70ced084 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -1072,6 +1072,7 @@ "immunisation-records": "Immunisation", "in_consultation": "In-Consultation", "inactive": "Inactive", + "incoming": "Incoming", "incomplete_patient_details_warning": "Patient details are incomplete. Please update the details before proceeding.", "inconsistent_dosage_units_error": "Dosage units must be same", "indian_mobile": "Indian Mobile", @@ -1439,6 +1440,7 @@ "otp_verification_error": "Failed to verify OTP. Please try again later.", "otp_verification_success": "OTP has been verified successfully.", "out_of_range_error": "Value must be between {{ start }} and {{ end }}.", + "outgoing": "Outgoing", "overview": "Overview", "oxygen_information": "Oxygen Information", "packages": "Packages", diff --git a/src/Routers/routes/ResourceRoutes.tsx b/src/Routers/routes/ResourceRoutes.tsx index 74d96ba622b..56172198879 100644 --- a/src/Routers/routes/ResourceRoutes.tsx +++ b/src/Routers/routes/ResourceRoutes.tsx @@ -5,9 +5,15 @@ import ResourceList from "@/components/Resource/ResourceList"; import { AppRoutes } from "@/Routers/AppRouter"; const ResourceRoutes: AppRoutes = { - "/resource": () => , - "/resource/:id": ({ id }) => , - "/resource/:id/update": ({ id }) => , + "/facility/:facilityId/resource": ({ facilityId }) => ( + + ), + "/facility/:facilityId/resource/:id": ({ facilityId, id }) => ( + + ), + "/facility/:facilityId/resource/:id/update": ({ facilityId, id }) => ( + + ), }; export default ResourceRoutes; diff --git a/src/components/Patient/PatientDetailsTab/ResourceRequests.tsx b/src/components/Patient/PatientDetailsTab/ResourceRequests.tsx index 0df4ec5b404..19c2cfe6586 100644 --- a/src/components/Patient/PatientDetailsTab/ResourceRequests.tsx +++ b/src/components/Patient/PatientDetailsTab/ResourceRequests.tsx @@ -112,7 +112,11 @@ export const ResourceRequests = (props: PatientProps) => { - -
-
- } - dialogClass="w-full max-w-[28rem]" - > -
{children}
- - ); -} - -export const AdvancedFilterButton = ({ onClick }: { onClick: () => void }) => { - const { t } = useTranslation(); - return ( - - ); -}; diff --git a/src/CAREUI/interactive/SlideOver.tsx b/src/CAREUI/interactive/SlideOver.tsx deleted file mode 100644 index bff3226a438..00000000000 --- a/src/CAREUI/interactive/SlideOver.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import { - Dialog, - DialogPanel, - Transition, - TransitionChild, -} from "@headlessui/react"; - -import CareIcon from "@/CAREUI/icons/CareIcon"; - -import { classNames } from "@/Utils/utils"; - -export type SlideFromEdges = "left" | "top" | "right" | "bottom"; - -export type SlideOverProps = { - open: boolean; - setOpen: (state: boolean) => void; - children: React.ReactNode; - slideFrom?: SlideFromEdges; - dialogClass?: string; - title?: React.ReactNode; - onlyChild?: boolean; - backdropBlur?: boolean; - closeOnBackdropClick?: boolean; - onCloseClick?: () => void; -}; - -export default function SlideOver({ - open, - setOpen, - children, - slideFrom = "right", - dialogClass, - title, - onlyChild = false, - backdropBlur = true, - closeOnBackdropClick = true, - onCloseClick, -}: SlideOverProps) { - const directionClasses = { - left: { - stick: "left-0 top-0 h-full", - animateStart: "-translate-x-20", - animateEnd: "translate-x-0", - proportions: " cui-slideover-x", - }, - right: { - stick: "right-0 top-0 h-full", - animateStart: "translate-x-20", - animateEnd: "-translate-x-0", - proportions: "cui-slideover-x", - }, - top: { - stick: "top-0 left-0 w-full", - animateStart: "-translate-y-20", - animateEnd: "translate-y-0", - proportions: "cui-slideover-y", - }, - bottom: { - stick: "bottom-0 left-0 w-full", - animateStart: "translate-y-20", - animateEnd: "-translate-y-0", - proportions: "cui-slideover-y", - }, - }; - - return ( - - {}} - > - -
- - - - {onlyChild ? ( - children - ) : ( -
-
- -
-

{title}

-
-
-
- {children} -
-
- )} -
-
-
-
- ); -} diff --git a/src/Routers/types.ts b/src/Routers/types.ts deleted file mode 100644 index dc7138b8df7..00000000000 --- a/src/Routers/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type RouteParams = Record; - -export interface DetailRoute { - id: string; -} diff --git a/src/Utils/AutoSave.tsx b/src/Utils/AutoSave.tsx deleted file mode 100644 index f621a58802d..00000000000 --- a/src/Utils/AutoSave.tsx +++ /dev/null @@ -1,201 +0,0 @@ -import React, { - Dispatch, - ReactNode, - useContext, - useEffect, - useReducer, - useRef, - useState, -} from "react"; - -import CareIcon from "@/CAREUI/icons/CareIcon"; - -import { Button } from "@/components/ui/button"; - -import { FormAction, FormReducer, FormState } from "@/components/Form/Utils"; - -import { relativeTime } from "@/Utils/utils"; - -type Draft = { - timestamp: number; - draft: { - [key: string]: any; - }; -}; - -export function useAutoSaveReducer( - reducer: any, - initialState: any, -): [FormState, Dispatch>] { - const saveInterval = 1000; - const saveKey = useRef(`form_draft_${window.location.pathname}`); - const sessionStartTime = useRef(Date.now()); - const [canStartDraft, setCanStartDraft] = useState(false); - const [draftStarted, setDraftStarted] = useState(false); - const [state, dispatch] = useReducer>(reducer, initialState); - - useEffect(() => { - if (!canStartDraft) return; - setDraftStarted(true); - }, [canStartDraft, state]); - - useEffect(() => { - const timeoutId = setTimeout(() => { - setCanStartDraft(true); - }, 3000); - - return () => { - clearTimeout(timeoutId); - }; - }, []); - - useEffect(() => { - const intervalId = setInterval(() => { - if (!draftStarted) return; - const savedDrafts = localStorage.getItem(saveKey.current); - const drafts = savedDrafts ? JSON.parse(savedDrafts) : []; - const existingIndex = drafts.findIndex( - (draft: Draft) => draft.timestamp === sessionStartTime.current, - ); - const currentDraft = { - timestamp: sessionStartTime.current, - draft: state, - }; - if (existingIndex !== -1) { - drafts[existingIndex] = currentDraft; - } else { - drafts.push(currentDraft); - } - if (drafts.length > 2) drafts.shift(); - localStorage.setItem(saveKey.current, JSON.stringify(drafts)); - }, saveInterval); - - return () => { - clearInterval(intervalId); - }; - }, [state, draftStarted]); - - return [state, dispatch]; -} - -export function useAutoSaveState(initialState: any) { - const [state, dispatch] = useAutoSaveReducer((state: any, action: any) => { - if (action.type === "set_state") { - return action.state; - } - return state; - }, initialState); - - const setState = (newState: any) => { - dispatch({ type: "set_state", state: newState }); - }; - - return [state, setState]; -} - -type RestoreDraftContextValue = { - handleDraftSelect: (formState: any) => void; - draftStarted: boolean; - drafts: Draft[]; -}; - -const RestoreDraftContext = - React.createContext(null); - -export function DraftSection(props: { - handleDraftSelect: (formState: any) => void; - formData: any; - hidden?: boolean; - children?: ReactNode; -}) { - const { handleDraftSelect } = props; - const [drafts, setDrafts] = useState([]); - const saveKey = useRef(`form_draft_${window.location.pathname}`); - const draftStarted = - drafts.length > 0 - ? drafts[drafts.length - 1].draft == props.formData - : false; - - useEffect(() => { - const timeoutId = setTimeout(() => { - const savedDrafts = localStorage.getItem(saveKey.current); - const drafts = savedDrafts ? JSON.parse(savedDrafts) : []; - setDrafts(drafts); - }, 1000); - - return () => { - clearTimeout(timeoutId); - }; - }, []); - - // Remove drafts older than 24 hours - useEffect(() => { - const keys = Object.keys(localStorage); - const now = Date.now(); - keys.forEach((key) => { - if (key.startsWith("form_draft_")) { - const savedDrafts = localStorage.getItem(key); - const drafts = savedDrafts ? JSON.parse(savedDrafts) : []; - const newDrafts = drafts.filter( - (draft: Draft) => now - draft.timestamp < 24 * 60 * 60 * 1000, - ); - localStorage.setItem(key, JSON.stringify(newDrafts)); - if (newDrafts.length === 0) localStorage.removeItem(key); - } - }); - }, []); - - return ( - - {!props.hidden && drafts && drafts.length > 0 && ( -
- -
- )} - {props.children} -
- ); -} - -export const RestoreDraftButton = () => { - const ctx = useContext(RestoreDraftContext); - - if (!ctx) { - throw new Error( - "RestoreDraftButton must be used within a RestoreDraftProvider", - ); - } - - const { handleDraftSelect, draftStarted, drafts } = ctx; - - if (!(drafts && drafts.length > 0)) { - return null; - } - - return ( - - ); -}; diff --git a/src/Utils/primitives.ts b/src/Utils/primitives.ts deleted file mode 100644 index c0c094a3fd8..00000000000 --- a/src/Utils/primitives.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const parseQueryParams = (url: string) => - Object.fromEntries(new URLSearchParams(new URL(url).search).entries()); diff --git a/src/Utils/useSegmentedRecorder.ts b/src/Utils/useSegmentedRecorder.ts deleted file mode 100644 index 4b0ea11b208..00000000000 --- a/src/Utils/useSegmentedRecorder.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { toast } from "sonner"; - -const useSegmentedRecording = () => { - const [isRecording, setIsRecording] = useState(false); - const [recorder, setRecorder] = useState(null); - const [audioBlobs, setAudioBlobs] = useState([]); - const [restart, setRestart] = useState(false); - const [microphoneAccess, setMicrophoneAccess] = useState(false); // New state - const { t } = useTranslation(); - - const bufferInterval = 1 * 1000; - const splitSizeLimit = 20 * 1000000; // 20MB - - useEffect(() => { - if (!isRecording && recorder && audioBlobs.length > 0) { - setRecorder(null); - } - }, [isRecording, recorder, audioBlobs]); - - useEffect(() => { - if (recorder === null) { - if (isRecording || restart) { - requestRecorder().then( - (newRecorder) => { - setRecorder(newRecorder); - setMicrophoneAccess(true); // Set access to true on success - if (restart) { - setIsRecording(true); - } - }, - () => { - toast.error(t("audio__permission_message")); - setIsRecording(false); - setMicrophoneAccess(false); // Set access to false on failure - }, - ); - } - return; - } - - if (isRecording) { - if (recorder.state === "inactive") recorder.start(bufferInterval); - } else { - if (restart) { - setIsRecording(true); - } else { - recorder?.stream?.getTracks()?.forEach((i) => i?.stop()); - recorder.stop(); - } - if (recorder.state === "recording") recorder.stop(); - } - - // Obtain the audio when ready. - const handleData = (e: { data: Blob }) => { - const newChunk = e.data; - let currentBlob: Blob | undefined = audioBlobs[audioBlobs.length - 1]; - if (restart) { - currentBlob = undefined; - } - if ((currentBlob?.size || 0) + newChunk.size < splitSizeLimit) { - // Audio size is less than 20MB, appending to current blob - if (!currentBlob) { - // Current blob is null, setting new blob - if (restart) { - setAudioBlobs((prev) => [ - ...prev, - new Blob([newChunk], { type: recorder.mimeType }), - ]); - setRestart(false); - return; - } - setAudioBlobs([new Blob([newChunk], { type: recorder.mimeType })]); - return; - } - // Appending new chunk to current blob - const newBlob = new Blob([currentBlob, newChunk], { - type: recorder.mimeType, - }); - setAudioBlobs((prev) => [...prev.slice(0, prev.length - 1), newBlob]); - } else { - // Audio size exceeded 20MB, starting new recording - if (currentBlob) - setAudioBlobs((prev) => [ - ...prev.slice(0, prev.length - 1), - new Blob([currentBlob ?? new Blob([]), newChunk], { - type: recorder.mimeType, - }), - ]); - recorder.stop(); - setRecorder(null); - setRestart(true); - setIsRecording(false); - } - }; - recorder.addEventListener("dataavailable", handleData); - return () => recorder.removeEventListener("dataavailable", handleData); - }, [recorder, isRecording, bufferInterval, audioBlobs, restart]); - - const startRecording = async () => { - try { - const newRecorder = await requestRecorder(); - setRecorder(newRecorder); - setMicrophoneAccess(true); - setIsRecording(true); - } catch { - setMicrophoneAccess(false); - throw new Error("Microphone access denied"); - } - }; - - const stopRecording = () => { - setIsRecording(false); - }; - - const resetRecording = () => { - setAudioBlobs([]); - }; - - return { - isRecording, - startRecording, - stopRecording, - resetRecording, - audioBlobs, - microphoneAccess, // Return microphoneAccess - }; -}; - -async function requestRecorder() { - return new Promise((resolve, reject) => { - navigator.mediaDevices - .getUserMedia({ audio: true }) - .then((stream) => { - const recorder = new MediaRecorder(stream, { - audioBitsPerSecond: 128000, - }); - resolve(recorder); - }) - .catch((error) => { - reject(error); - }); - }); -} - -export default useSegmentedRecording; diff --git a/src/components/Common/DateInputV2.tsx b/src/components/Common/DateInputV2.tsx deleted file mode 100644 index 689199376be..00000000000 --- a/src/components/Common/DateInputV2.tsx +++ /dev/null @@ -1,799 +0,0 @@ -import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react"; -import { t } from "i18next"; -import { MutableRefObject, useEffect, useRef, useState } from "react"; -import { toast } from "sonner"; - -import CareIcon from "@/CAREUI/icons/CareIcon"; - -import DateTextInput from "@/components/Common/DateTextInput"; - -import dayjs from "@/Utils/dayjs"; -import { classNames } from "@/Utils/utils"; - -type DatePickerType = "date" | "month" | "year"; - -interface Props { - id?: string; - name?: string; - className?: string; - containerClassName?: string; - value: Date | undefined; - min?: Date; - max?: Date; - outOfLimitsErrorMessage?: string; - 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"]; - -const DateInputV2: React.FC = ({ - id, - name, - className, - containerClassName, - value, - min, - max, - outOfLimitsErrorMessage, - onChange, - disabled, - placeholder, - setIsOpen, - allowTime, - isOpen, - popOverClassName, -}) => { - const [dayCount, setDayCount] = useState>([]); - const [blankDays, setBlankDays] = useState>([]); - - const [datePickerHeaderDate, setDatePickerHeaderDate] = useState( - value || new Date(), - ); - const [type, setType] = useState("date"); - const [year, setYear] = useState(new Date()); - - 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) { - case "date": - setDatePickerHeaderDate((prev) => - dayjs(prev).subtract(1, "month").toDate(), - ); - break; - case "month": - setDatePickerHeaderDate((prev) => { - const newDate = dayjs(prev).subtract(1, "year").toDate(); - if (min && newDate < min) { - return new Date(min.getFullYear(), min.getMonth(), 1); - } - return newDate; - }); - break; - case "year": - if (!min || year.getFullYear() - 10 >= min.getFullYear()) { - setYear((prev) => dayjs(prev).subtract(10, "year").toDate()); - } - break; - } - }; - - const increment = () => { - switch (type) { - case "date": - setDatePickerHeaderDate((prev) => dayjs(prev).add(1, "month").toDate()); - break; - case "month": - setDatePickerHeaderDate((prev) => { - const newDate = dayjs(prev).add(1, "year").toDate(); - if (max && newDate > max) { - return new Date(max.getFullYear(), max.getMonth(), 1); - } - return newDate; - }); - break; - case "year": - if (!max || year.getFullYear() + 10 <= max.getFullYear()) { - setYear((prev) => dayjs(prev).add(10, "year").toDate()); - } - break; - } - }; - - type CloseFunction = ( - focusableElement?: HTMLElement | MutableRefObject, - ) => void; - - const setDateValue = (date: number, close: CloseFunction) => () => { - isDateWithinConstraints(date) - ? (() => { - handleChange( - new Date( - datePickerHeaderDate.getFullYear(), - datePickerHeaderDate.getMonth(), - date, - datePickerHeaderDate.getHours(), - datePickerHeaderDate.getMinutes(), - datePickerHeaderDate.getSeconds(), - ), - ); - if (!allowTime) { - close(); - setIsOpen?.(false); - } - })() - : toast.error( - outOfLimitsErrorMessage ?? t("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(); - - const dayOfWeek = dayjs( - new Date(date.getFullYear(), date.getMonth(), 1), - ).day(); - const blankDaysArray = []; - for (let i = 1; i <= dayOfWeek; i++) { - blankDaysArray.push(i); - } - - const daysArray = []; - for (let i = 1; i <= daysInMonth; i++) { - daysArray.push(i); - } - - setBlankDays(blankDaysArray); - setDayCount(daysArray); - }; - - const getLastDay = ( - year = datePickerHeaderDate.getFullYear(), - month = datePickerHeaderDate.getMonth(), - ) => { - return new Date(year, month + 1, 0).getDate(); - }; - - const isDateWithinConstraints = ( - day = datePickerHeaderDate.getDate(), - month = datePickerHeaderDate.getMonth(), - year = datePickerHeaderDate.getFullYear(), - ) => { - const date = new Date(year, month, day); - if ( - min && - max && - min.getDate() === max.getDate() && - day === min.getDate() && - month === min.getMonth() && - year === min.getFullYear() - ) { - return true; - } - if (min) if (date < min) return false; - if (max) if (date > max) return false; - return true; - }; - - const isMonthWithinConstraints = (month: number) => { - const year = datePickerHeaderDate.getFullYear(); - - if (min && year < min.getFullYear()) return false; - if (max && year > max.getFullYear()) return false; - - const firstDay = new Date(year, month, 1); - const lastDay = new Date(year, month + 1, 0); - if (min && lastDay < min) return false; - if (max && firstDay > max) return false; - - return true; - }; - - const isYearWithinConstraints = (year: number) => { - if (min && year < min.getFullYear()) return false; - if (max && year > max.getFullYear()) return false; - - const yearStart = new Date(year, 0, 1); - const yearEnd = new Date(year, 11, 31); - - if (min && yearEnd < min) return false; - if (max && yearStart > max) return false; - - return true; - }; - - const isSelectedMonth = (month: number) => - month === datePickerHeaderDate.getMonth(); - - const isSelectedYear = (year: number) => - year === datePickerHeaderDate.getFullYear(); - - const setMonthValue = (month: number) => () => { - if (isMonthWithinConstraints(month)) { - const lastDayOfMonth = new Date( - datePickerHeaderDate.getFullYear(), - month + 1, - 0, - ).getDate(); - const newDate = Math.min(datePickerHeaderDate.getDate(), lastDayOfMonth); - setDatePickerHeaderDate( - new Date(datePickerHeaderDate.getFullYear(), month, newDate), - ); - setType("date"); - } else { - toast.error( - outOfLimitsErrorMessage ?? t("cannot_select_month_out_of_range"), - ); - } - }; - //min and max setting for year - const setYearValue = (year: number) => () => { - if (isYearWithinConstraints(year)) { - const newDate = new Date( - year, - datePickerHeaderDate.getMonth(), - datePickerHeaderDate.getDate(), - ); - if (min && year === min.getFullYear() && newDate < min) { - setDatePickerHeaderDate( - new Date(min.getFullYear(), min.getMonth(), min.getDate()), - ); - } else if (max && year === max.getFullYear() && newDate > max) { - setDatePickerHeaderDate( - new Date(max.getFullYear(), max.getMonth(), max.getDate()), - ); - } else { - setDatePickerHeaderDate(newDate); - } - setType("date"); - } else { - toast.error( - outOfLimitsErrorMessage ?? t("cannot_select_year_out_of_range"), - ); - } - }; - - 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(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 = () => { - 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 ? "sm:-translate-x-1/2" : ""} ${top ? "md:-translate-y-[calc(100%+50px)]" : ""} ${right ? "max-sm:-translate-x-1/2" : ""} ${top ? "max-sm:-translate-y-[calc(100%+50px)]" : ""}`.trim(); - }; - - return ( -
-
- - {({ open, close }) => { - setPopOverOpen(open); - return ( -
- - - -
- -
-
- {open && ( - -
- close()} - error={ - value && - (!dayjs(value).isValid() || - (!!max && value > max) || - (!!min && value < min)) - ? "Cannot select date out of range" - : undefined - } - /> - -
-
-
- {type === "date" && ( - - )} - {type === "month" && ( - - )} - - {type === "year" && ( - - )} - -
- {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" - > -

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

-
-
- {type === "date" && ( - - )} - {type === "month" && ( - - )} - - {type === "year" && ( - - )} -
- - {type === "date" && ( - <> -
- {DAYS.map((day, i) => ( -
-
- {day} -
-
- ))} -
-
- {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() - 10 + 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, - }); - } - }} - > - {[ - ...input.options, - ...(input.name === "am/pm" - ? [] - : input.options), - ...(input.name === "am/pm" - ? [] - : input.options), - ].map((option, j) => ( - - ))} -
- ))} -
- )} -
-
- - )} -
- ); - }} - -
-
- ); -}; - -export default DateInputV2; diff --git a/src/components/Common/DateRangeInputV2.tsx b/src/components/Common/DateRangeInputV2.tsx deleted file mode 100644 index d3f57e59884..00000000000 --- a/src/components/Common/DateRangeInputV2.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { useState } from "react"; - -import DateInputV2 from "@/components/Common/DateInputV2"; - -export type DateRange = { - start: Date | undefined; - end: Date | undefined; -}; - -type Props = { - name?: string; - value?: DateRange; - onChange: (value: DateRange) => void; - className?: string; - disabled?: boolean; - max?: Date; - min?: Date; - allowTime?: boolean; -}; - -const DateRangeInputV2 = ({ value, onChange, ...props }: Props) => { - const { start, end } = value ?? { start: undefined, end: undefined }; - const [showEndPicker, setShowEndPicker] = useState(false); - - return ( -
-
- { - onChange({ start, end: start }); // This is to make the end date picker open at the start date by default - setShowEndPicker(true); - }} - min={props.min} - max={end || props.max} - placeholder="Start date" - disabled={props.disabled} - allowTime={props.allowTime} - /> -
-
- onChange({ start, end })} - min={start || props.min} - max={props.max} - disabled={props.disabled || !start} - placeholder="End date" - isOpen={showEndPicker} - setIsOpen={setShowEndPicker} - allowTime={props.allowTime} - /> -
-
- ); -}; - -export default DateRangeInputV2; diff --git a/src/components/Common/DateTextInput.tsx b/src/components/Common/DateTextInput.tsx deleted file mode 100644 index f2cd4844a1c..00000000000 --- a/src/components/Common/DateTextInput.tsx +++ /dev/null @@ -1,205 +0,0 @@ -import dayjs from "dayjs"; -import { Fragment, KeyboardEvent, useEffect, useState } from "react"; - -import CareIcon from "@/CAREUI/icons/CareIcon"; - -import { classNames } from "@/Utils/utils"; - -/** - * 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/GLocationPicker.tsx b/src/components/Common/GLocationPicker.tsx deleted file mode 100644 index cee05ce4245..00000000000 --- a/src/components/Common/GLocationPicker.tsx +++ /dev/null @@ -1,315 +0,0 @@ -import careConfig from "@careConfig"; -import { Status, Wrapper } from "@googlemaps/react-wrapper"; -import { isLatLngLiteral } from "@googlemaps/typescript-guards"; -import { PopoverButton } from "@headlessui/react"; -import React from "react"; - -import CareIcon from "@/CAREUI/icons/CareIcon"; - -import Spinner from "@/components/Common/Spinner"; - -import { deepEqual } from "@/common/utils"; - -interface GLocationPickerProps { - lat: number; - lng: number; - handleOnChange: (location: google.maps.LatLng) => void; - handleOnClose?: () => void; - handleOnSelectCurrentLocation?: ( - setCenter: (lat: number, lng: number) => void, - ) => void; -} - -const GLocationPicker = ({ - lat, - lng, - handleOnChange, - handleOnClose, - handleOnSelectCurrentLocation, -}: GLocationPickerProps) => { - const [location, setLocation] = React.useState( - null, - ); - const [zoom, setZoom] = React.useState(4); - const [center, setCenter] = React.useState< - google.maps.LatLngLiteral | undefined - >({ - lat, - lng, - }); - - React.useEffect(() => { - const setLatLng = async () => { - const latLng = await new google.maps.LatLng(lat, lng); - setLocation(latLng); - }; - - if (lat && lng) - setLatLng().catch((err) => { - if (err instanceof ReferenceError) { - console.info("Google Maps API not loaded yet"); - } - }); - }, [lat, lng, window?.google]); - - const onClick = (e: google.maps.MapMouseEvent) => { - if (e.latLng) handleOnChange(e.latLng); - }; - - const onIdle = (m: google.maps.Map) => { - setZoom(m?.getZoom() ?? 0); - setCenter(m?.getCenter()?.toJSON()); - }; - - const render = (status: Status) => { - switch (status) { - case Status.LOADING: - return ; - case Status.SUCCESS: - return ( - - {location && } - - ); - default: - return

{status}

; - } - }; - - return ( -
- -
- ); -}; -interface MapProps extends google.maps.MapOptions { - style: { [key: string]: string }; - onClick?: (e: google.maps.MapMouseEvent) => void; - onIdle?: (map: google.maps.Map) => void; - handleOnChange?: (location: google.maps.LatLng) => void; - handleOnClose?: () => void; - handleOnSelectCurrentLocation?: ( - setCenter: (lat: number, lng: number) => void, - ) => void; - children?: React.ReactNode; -} - -const Map: React.FC = ({ - onClick, - onIdle, - handleOnChange, - handleOnClose, - handleOnSelectCurrentLocation, - children, - style, - ...options -}) => { - const ref = React.useRef(null); - const searchRef = React.useRef(null); - const [map, setMap] = React.useState>(); - const mapCloseRef = React.useRef(null); - const currentLocationSelectRef = React.useRef(null); - const [searchBox, setSearchBox] = - React.useState(); - - React.useEffect(() => { - if (ref.current && !map) { - setMap( - new window.google.maps.Map(ref.current, { - mapTypeControl: false, - }), - ); - } - }, [ref, map]); - - React.useEffect(() => { - if (searchRef.current && map && !searchBox) { - map.controls[google.maps.ControlPosition.TOP_CENTER].push( - searchRef.current, - ); - - setSearchBox(new window.google.maps.places.SearchBox(searchRef.current)); - } - - if (searchBox) { - map?.addListener("bounds_changed", () => { - searchBox.setBounds(map?.getBounds() as google.maps.LatLngBounds); - }); - - searchBox.addListener("places_changed", () => { - const places = searchBox.getPlaces(); - - if ( - handleOnChange && - places && - places.length > 0 && - places[0].geometry?.location - ) { - const selectedLocation = places[0].geometry.location; - handleOnChange(selectedLocation); - map?.setCenter(selectedLocation); - } - }); - } - }, [searchRef, map, searchBox, handleOnChange]); - - React.useEffect(() => { - if (mapCloseRef.current && map) { - map.controls[google.maps.ControlPosition.TOP_RIGHT].push( - mapCloseRef.current, - ); - } - }, [mapCloseRef, map]); - - React.useEffect(() => { - if (currentLocationSelectRef.current && map) { - map.controls[google.maps.ControlPosition.TOP_LEFT].push( - currentLocationSelectRef.current, - ); - } - }, [currentLocationSelectRef, map]); - - useDeepCompareEffectForMaps(() => { - if (map) { - map.setOptions(options); - } - }, [map, options]); - - React.useEffect(() => { - if (map) { - ["click", "idle"].forEach((eventName) => - google.maps.event.clearListeners(map, eventName), - ); - - if (onClick) { - map.addListener("click", onClick); - } - - if (onIdle) { - map.addListener("idle", () => onIdle(map)); - } - } - }, [map, onClick, onIdle]); - - return ( - <> - <> - - {handleOnClose && ( - -
- -
-
- )} - {handleOnSelectCurrentLocation && ( -
- handleOnSelectCurrentLocation((lat: number, lng: number) => - map?.setCenter(new window.google.maps.LatLng(lat, lng)), - ) - } - > - -
- )} - - -
- {React.Children.map(children, (child) => { - if (React.isValidElement(child)) { - return React.cloneElement(child as React.ReactElement, { map }); - } - })} - - ); -}; - -const Marker: React.FC = (options) => { - const [marker, setMarker] = React.useState(); - - React.useEffect(() => { - if (!marker) { - setMarker(new google.maps.Marker()); - } - - return () => { - if (marker) { - marker.setMap(null); - } - }; - }, [marker]); - - React.useEffect(() => { - if (marker) { - marker.setOptions(options); - } - }, [marker, options]); - - return null; -}; - -const deepCompareEqualsForMaps = (a: any, b: any) => { - if ( - isLatLngLiteral(a) || - a instanceof google.maps.LatLng || - isLatLngLiteral(b) || - b instanceof google.maps.LatLng - ) { - return new google.maps.LatLng(a).equals(new google.maps.LatLng(b)); - } - - return deepEqual(a, b); -}; - -function useDeepCompareMemoize(value: any) { - const ref = React.useRef(); - - if (!deepCompareEqualsForMaps(value, ref.current)) { - ref.current = value; - } - - return ref.current; -} - -function useDeepCompareEffectForMaps( - callback: React.EffectCallback, - dependencies: any[], -) { - React.useEffect(callback, dependencies.map(useDeepCompareMemoize)); -} - -export default GLocationPicker; diff --git a/src/components/Common/QuantityInput.tsx b/src/components/Common/QuantityInput.tsx deleted file mode 100644 index 07d19dfba61..00000000000 --- a/src/components/Common/QuantityInput.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { t } from "i18next"; - -import { Input } from "@/components/ui/input"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; - -interface QuantityValue { - value?: number; - unit?: TUnit; -} - -interface Props { - quantity?: QuantityValue | null; - onChange: (quantity: QuantityValue) => void; - units: readonly TUnit[]; - disabled?: boolean; - placeholder?: string; - autoFocus?: boolean; -} - -const QuantityInput = ({ - units, - quantity = { value: undefined, unit: units[0] }, - onChange, - disabled, - placeholder, - autoFocus, -}: Props) => { - const handleChange = (update: Partial>) => { - onChange({ ...quantity, ...update }); - }; - - return ( -
- - handleChange({ - value: e.target.value ? Number(e.target.value) : undefined, - }) - } - autoFocus={autoFocus} - /> - -
- ); -}; - -QuantityInput.displayName = "QuantityInput"; - -export { QuantityInput }; diff --git a/src/components/Common/SortDropdown.tsx b/src/components/Common/SortDropdown.tsx deleted file mode 100644 index d1d753a70a1..00000000000 --- a/src/components/Common/SortDropdown.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useTranslation } from "react-i18next"; - -import CareIcon from "@/CAREUI/icons/CareIcon"; - -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; - -export interface SortOption { - isAscending: boolean; - value: string; -} - -interface Props { - label?: string; - options: SortOption[]; - onSelect: (query: { ordering: string }) => void; - selected?: string; -} - -/** - * Ensure the sort option values are present in the locale. - */ -export default function SortDropdownMenu(props: Props) { - const { t } = useTranslation(); - return ( - - ); -} diff --git a/src/components/Common/Spinner.tsx b/src/components/Common/Spinner.tsx deleted file mode 100644 index f69fe7a0b39..00000000000 --- a/src/components/Common/Spinner.tsx +++ /dev/null @@ -1,37 +0,0 @@ -const Spinner = ({ - className = "", - path: { fill: pathFill = "white", className: pathClassName = "" } = {}, - circle: { - fill: circleFill = "white", - className: circleClassName = "opacity-75", - stroke: circleStroke = "#f1edf7", - cx = "12", - cy = "12", - r = "10", - strokeWidth: circleStrokeWidth = "4", - } = {}, -}) => { - return ( - - - - - ); -}; - -export default Spinner; diff --git a/src/components/Common/UserDetails.tsx b/src/components/Common/UserDetails.tsx deleted file mode 100644 index b304ac7216d..00000000000 --- a/src/components/Common/UserDetails.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { ReactNode } from "react"; - -function UserDetails(props: { - children: ReactNode; - title: string; - id?: string; -}) { - return ( -
-
- {props.title} -
- {props.children} -
- ); -} - -export default UserDetails; diff --git a/src/components/Common/View.tsx b/src/components/Common/View.tsx deleted file mode 100644 index cff36841db5..00000000000 --- a/src/components/Common/View.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React, { ComponentType } from "react"; - -import { useView } from "@/Utils/useView"; - -export default function View({ - name, - board, - list, -}: { - name: "resource"; - board: ComponentType; - list: ComponentType; -}) { - const [view] = useView(name, "board"); - - const views: Record<"board" | "list", ComponentType> = { - board, - list, - }; - - const SelectedView = views[view as keyof typeof views] || board; - - return ; -} diff --git a/src/components/Facility/FacilityBlock.tsx b/src/components/Facility/FacilityBlock.tsx deleted file mode 100644 index dfee858cfbe..00000000000 --- a/src/components/Facility/FacilityBlock.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { Link } from "raviger"; -import { ReactNode } from "react"; - -import { Avatar } from "@/components/Common/Avatar"; -import { FacilityModel } from "@/components/Facility/models"; - -export default function FacilityBlock(props: { - facility: FacilityModel; - redirect?: boolean; -}) { - const { facility, redirect = true } = props; - - const Element = (props: { children: ReactNode; className?: string }) => - redirect ? ( - - {props.children} - - ) : ( - - ); - - return ( - -
- -
-
- {facility.name} -
-
- ); -} diff --git a/src/components/Form/FormFields/DateRangeFormField.tsx b/src/components/Form/FormFields/DateRangeFormField.tsx deleted file mode 100644 index d478e770042..00000000000 --- a/src/components/Form/FormFields/DateRangeFormField.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import DateRangeInputV2, { - DateRange, -} from "@/components/Common/DateRangeInputV2"; -import FormField from "@/components/Form/FormFields/FormField"; -import { - FormFieldBaseProps, - useFormFieldPropsResolver, -} from "@/components/Form/FormFields/Utils"; - -import { classNames } from "@/Utils/utils"; - -type Props = FormFieldBaseProps & { - max?: Date; - min?: Date; - disableFuture?: boolean; - disablePast?: boolean; - allowTime?: boolean; -}; - -/** - * A FormField to pick a date range. - * - * Example usage: - * - * ```jsx - * - * ``` - */ -const DateRangeFormField = (props: Props) => { - const field = useFormFieldPropsResolver(props); - return ( - - - - ); -}; - -export default DateRangeFormField; diff --git a/src/components/Form/SearchInput.tsx b/src/components/Form/SearchInput.tsx deleted file mode 100644 index 69334c0f4db..00000000000 --- a/src/components/Form/SearchInput.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { createRef, useEffect, useState } from "react"; -import useKeyboardShortcut from "use-keyboard-shortcut"; - -import CareIcon from "@/CAREUI/icons/CareIcon"; - -import TextFormField, { - TextFormFieldProps, -} from "@/components/Form/FormFields/TextFormField"; - -import { isAppleDevice } from "@/Utils/utils"; - -type SearchInputProps = TextFormFieldProps & { - debouncePeriod?: number; - secondary?: true | undefined; -} & ( - | { - hotkey: string[]; - hotkeyIcon: React.ReactNode; - } - | { - hotkey?: undefined; - hotkeyIcon?: undefined; - } - ); - -const SearchInput = ({ - debouncePeriod = 500, - className = "w-full md:max-w-sm", - onChange, - name = "search", - ...props -}: SearchInputProps) => { - // Debounce related - const [value, setValue] = useState(() => props.value); - useEffect(() => setValue(props.value), [props.value]); - useEffect(() => { - if (value !== props.value) { - const timeoutId = setTimeout( - () => onChange && onChange({ name, value: value || "" }), - debouncePeriod, - ); - return () => clearTimeout(timeoutId); - } - }, [value, debouncePeriod, name, onChange, props.value]); - - // Focus hotkey related - const ref = createRef(); - useKeyboardShortcut( - props.hotkey || [isAppleDevice ? "Meta" : "Control", "K"], - () => !props.secondary && ref.current?.focus(), - { overrideSystem: !props.secondary }, - ); - - const shortcutKeyIcon = - props.hotkeyIcon || - (isAppleDevice ? ( - "⌘K" - ) : ( -
- Ctrl - K -
- )); - - // Escape hotkey to clear related - useKeyboardShortcut( - ["Escape"], - () => { - if (value) { - setValue(""); - } - ref.current?.blur(); - }, - { - ignoreInputFields: false, - }, - ); - - return ( - - ) - } - trailing={ - props.trailing || - (!props.secondary && ( -
- - {shortcutKeyIcon} - -
- )) - } - trailingFocused={ -
- - Esc - -
- } - value={value || ""} - onChange={({ value }) => setValue(value)} - /> - ); -}; - -export default SearchInput; diff --git a/src/components/Form/Utils.ts b/src/components/Form/Utils.ts deleted file mode 100644 index 21dc9d8b8aa..00000000000 --- a/src/components/Form/Utils.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { FieldError } from "@/components/Form/FieldValidators"; - -export type FormDetails = { [key: string]: any }; -export type FormErrors = Partial< - Record ->; -export type FormState = { form: T; errors: FormErrors }; -export type FormAction = - | { type: "set_form"; form: T } - | { type: "set_errors"; errors: FormErrors } - | { type: "set_field"; name: keyof T; value: any; error: FieldError } - | { type: "set_state"; state: FormState }; -export type FormReducer = ( - prevState: FormState, - action: FormAction, -) => FormState; diff --git a/src/components/Kanban/Board.tsx b/src/components/Kanban/Board.tsx deleted file mode 100644 index e6024ec0632..00000000000 --- a/src/components/Kanban/Board.tsx +++ /dev/null @@ -1,198 +0,0 @@ -import { - DragDropContext, - Draggable, - Droppable, - OnDragEndResponder, -} from "@hello-pangea/dnd"; -import { useInfiniteQuery } from "@tanstack/react-query"; -import { ReactNode, RefObject, useEffect, useRef } from "react"; -import { useTranslation } from "react-i18next"; - -import CareIcon from "@/CAREUI/icons/CareIcon"; - -import { callApi } from "@/Utils/request/query"; -import { ApiRoute } from "@/Utils/request/types"; -import { QueryOptions } from "@/Utils/request/useQuery"; - -interface KanbanBoardProps { - title?: ReactNode; - onDragEnd: OnDragEndResponder; - sections: { - id: string; - title: ReactNode; - fetchOptions: ( - id: string, - ...args: unknown[] - ) => { - route: ApiRoute; - 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} - /> - ))} -
-
-
-
- ); -} - -interface QueryResponse { - results: T[]; - next: string | null; - count: number; -} - -export function KanbanSection( - props: Omit, "sections" | "onDragEnd"> & { - section: KanbanBoardProps["sections"][number]; - boardRef: RefObject; - }, -) { - const { section } = props; - const sectionRef = useRef(null); - const defaultLimit = 14; - const { t } = useTranslation(); - const options = section.fetchOptions(section.id); - const fetchPage = async ({ pageParam = 0 }) => { - try { - const data = await callApi(options.route, { - ...options.options, - queryParams: { - ...options.options?.query, - offset: pageParam, - limit: defaultLimit, - }, - }); - return data as QueryResponse; - } catch (error) { - console.error("Error fetching section data:", error); - return { results: [], next: null, count: 0 }; - } - }; - - const { - data, - fetchNextPage, - hasNextPage, - isFetchingNextPage, - isLoading, - refetch, - } = useInfiniteQuery({ - queryKey: ["board", section.id, options.options?.query], - queryFn: fetchPage, - getNextPageParam: (lastPage, pages) => { - if (!lastPage.next) return undefined; - return pages.length * defaultLimit; - }, - initialPageParam: 0, - }); - - const items = data?.pages?.flatMap((page) => page.results || []) ?? []; - const totalCount = data?.pages[0]?.count ?? 0; - - useEffect(() => { - refetch(); - }, [section.id, refetch]); - - return ( - - {(provided, _snapshot) => ( -
-
-
-
{section.title}
-
- - {isLoading ? "..." : totalCount} - -
-
-
-
{ - const target = e.target as HTMLDivElement; - if ( - target.scrollTop + target.clientHeight >= - target.scrollHeight - 100 - ) { - if (hasNextPage && !isFetchingNextPage) { - fetchNextPage(); - } - } - }} - > - {!isLoading && items.length === 0 && ( -
- {t("no_results_found")} -
- )} - {items.map((item, index) => ( - - {(provided, _snapshot) => ( -
- {props.itemRender(item)} -
- )} -
- ))} - {provided.placeholder} - {isFetchingNextPage && ( -
- )} -
-
- )} - - ); -} - -export type KanbanBoardType = typeof KanbanBoard; diff --git a/src/pages/Encounters/tabs/EncounterFeedTab.tsx b/src/pages/Encounters/tabs/EncounterFeedTab.tsx deleted file mode 100644 index c893336fc1d..00000000000 --- a/src/pages/Encounters/tabs/EncounterFeedTab.tsx +++ /dev/null @@ -1,267 +0,0 @@ -// import { useEffect, useRef, useState } from "react"; -// import { useTranslation } from "react-i18next"; - -// import CareIcon from "@/CAREUI/icons/CareIcon"; - -// import CameraFeed from "@/components/CameraFeed/CameraFeed"; -// import CameraPresetSelect from "@/components/CameraFeed/CameraPresetSelect"; -// import StillWatching from "@/components/CameraFeed/StillWatching"; -// import { -// CameraPreset, -// FeedRoutes, -// GetStatusResponse, -// } from "@/components/CameraFeed/routes"; -// import useOperateCamera, { -// PTZPayload, -// } from "@/components/CameraFeed/useOperateCamera"; -// import ConfirmDialog from "@/components/Common/ConfirmDialog"; -// import Loading from "@/components/Common/Loading"; -// import { ConsultationTabProps } from "@/components/Facility/ConsultationDetails/index"; - -// import useAuthUser from "@/hooks/useAuthUser"; -// import useBreakpoints from "@/hooks/useBreakpoints"; - -// import { Warn } from "@/Utils/Notifications"; -// import request from "@/Utils/request/request"; -// import useTanStackQueryInstead from "@/Utils/request/useQuery"; -// import { classNames, isIOS } from "@/Utils/utils"; - -// export const ConsultationFeedTab = (props: ConsultationTabProps) => { -// const { t } = useTranslation(); -// const authUser = useAuthUser(); -// const bed = props.consultationData.current_bed?.bed_object; -// const feedStateSessionKey = `encounterFeedState[${props.consultationId}]`; -// const [preset, setPreset] = useState(); -// const [selectedPreset, setSelectedPreset] = useState(); -// const [showPresetSaveConfirmation, setShowPresetSaveConfirmation] = -// useState(false); -// const [isUpdatingPreset, setIsUpdatingPreset] = useState(false); -// const [hasMoved, setHasMoved] = useState(false); -// const divRef = useRef(); - -// const suggestOptimalExperience = useBreakpoints({ default: true, sm: false }); - -// useEffect(() => { -// if (suggestOptimalExperience) { -// Warn({ -// msg: t( -// isIOS -// ? "feed_optimal_experience_for_apple_phones" -// : "feed_optimal_experience_for_phones", -// ), -// }); -// } -// }, []); - -// const asset = preset?.asset_bed.asset_object; - -// const { key, operate } = useOperateCamera(asset?.id ?? ""); - -// const presetsQuery = useTanStackQueryInstead(FeedRoutes.listBedPresets, { -// pathParams: { bed_id: bed?.id ?? "" }, -// query: { limit: 100 }, -// prefetch: !!bed, -// onResponse: ({ data }) => { -// if (!data) { -// return; -// } - -// const presets = data.results; -// const lastStateJSON = sessionStorage.getItem(feedStateSessionKey); - -// const preset = -// (() => { -// if (lastStateJSON) { -// const lastState = JSON.parse(lastStateJSON) as LastFeedState; -// if (lastState.type === "preset") { -// return presets.find((obj) => obj.id === lastState.value); -// } -// if (lastState.type === "position") { -// return; -// } -// } -// })() ?? presets[0]; - -// if (preset) { -// setPreset(preset); -// setSelectedPreset(preset); -// } -// }, -// }); - -// const presets = presetsQuery.data?.results; - -// const handleUpdatePreset = async () => { -// if (!preset) return; - -// setIsUpdatingPreset(true); - -// const { data } = await operate({ type: "get_status" }); -// const { position } = (data as { result: { position: PTZPayload } }).result; -// const { data: updated } = await request(FeedRoutes.updatePreset, { -// pathParams: { -// assetbed_id: preset.asset_bed.id, -// id: preset.id, -// }, -// body: { -// position, -// }, -// }); - -// await presetsQuery.refetch(); - -// setPreset(updated); -// setSelectedPreset(updated); -// setHasMoved(false); -// setIsUpdatingPreset(false); -// setShowPresetSaveConfirmation(false); -// }; - -// useEffect(() => { -// if (divRef.current) { -// divRef.current.scrollIntoView({ behavior: "smooth" }); -// } -// }, [!!bed, presetsQuery.loading, !!asset, divRef.current]); - -// useEffect(() => { -// if (preset?.id) { -// sessionStorage.setItem( -// feedStateSessionKey, -// JSON.stringify({ -// type: "preset", -// value: preset.id, -// } satisfies LastAccessedPreset), -// ); -// } -// }, [feedStateSessionKey, preset]); - -// if (presetsQuery.loading) { -// return ; -// } - -// if (!bed || !asset) { -// return {t("no_bed_asset_linked_allocated")}; -// } - -// const cannotSaveToPreset = !hasMoved || !preset?.id; - -// return ( -// -// setShowPresetSaveConfirmation(false)} -// onConfirm={handleUpdatePreset} -// /> - -//
-// { -// setSelectedPreset(undefined); -// setHasMoved(true); -// setTimeout(async () => { -// const { data } = await operate({ type: "get_status" }); -// if (data) { -// sessionStorage.setItem( -// feedStateSessionKey, -// JSON.stringify({ -// type: "position", -// value: (data as GetStatusResponse).result.position, -// } satisfies LastAccessedPosition), -// ); -// } -// }, 3000); -// }} -// operate={operate} -// onStreamError={() => { -// triggerGoal("Camera Feed Viewed", { -// consultationId: props.consultationId, -// userId: authUser.id, -// result: "error", -// }); -// }} -// onStreamSuccess={() => { -// triggerGoal("Camera Feed Viewed", { -// consultationId: props.consultationId, -// userId: authUser.id, -// result: "success", -// }); -// }} -// > -//
-// {presets ? ( -// <> -// obj.name} -// value={selectedPreset} -// onChange={(value) => { -// triggerGoal("Camera Preset Clicked", { -// presetName: selectedPreset?.name, -// consultationId: props.consultationId, -// userId: authUser.id, -// result: "success", -// }); -// setHasMoved(false); -// setPreset(value); -// setSelectedPreset(value); -// }} -// /> -// {isUpdatingPreset ? ( -// -// ) : ( -// -// )} -// -// ) : ( -// loading presets... -// )} -//
-//
-//
-//
-// ); -// }; - -// type LastAccessedPreset = { -// type: "preset"; -// value: string; -// }; - -// type LastAccessedPosition = { -// type: "position"; -// value: PTZPayload; -// }; - -// type LastFeedState = LastAccessedPosition | LastAccessedPreset; diff --git a/src/types/notes/notes.ts b/src/types/notes/notes.ts deleted file mode 100644 index 8b6d6d85d85..00000000000 --- a/src/types/notes/notes.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type Note = { - id: number; - title: string; - description: string; -}; diff --git a/src/types/permission/permission.ts b/src/types/permission/permission.ts deleted file mode 100644 index cfcb8e61833..00000000000 --- a/src/types/permission/permission.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type Permission = { - slug: string; - name: string; - description: string; - context: string; -}; From 090f5fc3da86ddf88a4e16923c6686bebaaba2cf Mon Sep 17 00:00:00 2001 From: Jacob John Jeevan Date: Fri, 31 Jan 2025 17:50:22 +0530 Subject: [PATCH 4/4] minor styling --- src/components/Resource/ResourceList.tsx | 34 ++++++++---------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/src/components/Resource/ResourceList.tsx b/src/components/Resource/ResourceList.tsx index 552e9383fc8..d4f4071a361 100644 --- a/src/components/Resource/ResourceList.tsx +++ b/src/components/Resource/ResourceList.tsx @@ -311,29 +311,17 @@ export default function ResourceList({ facilityId }: { facilityId: string }) {
- {outgoing ? ( - - - {resource.assigned_facility?.name} - - ) : ( - - - {resource.origin_facility?.name} - - )} + + {resource.origin_facility?.name} + + {resource.assigned_facility?.name} +