diff --git a/.env b/.env index dfd7243cc32..7e27e19a0d2 100644 --- a/.env +++ b/.env @@ -4,16 +4,6 @@ REACT_APP_TITLE="CARE" REACT_APP_META_DESCRIPTION="CoronaSafe Network is an open-source public utility designed by a multi-disciplinary team of innovators and volunteers. CoronaSafe Care is a Digital Public Good recognised by United Nations." REACT_APP_COVER_IMAGE=https://cdn.coronasafe.network/care_logo.svg REACT_APP_COVER_IMAGE_ALT=https://cdn.coronasafe.network/care_logo.svg -REACT_APP_GITHUB_URL=https://github.com/coronasafe -REACT_APP_HEADER_LOGO=https://cdn.coronasafe.network/header_logo.png -REACT_APP_DEPLOYED_URL=care.coronasafe.network -REACT_APP_LIGHT_LOGO=https://cdn.coronasafe.network/light-logo.svg -REACT_APP_LIGHT_COLLAPSE_LOGO= -REACT_APP_BLACK_LOGO=https://cdn.coronasafe.network/black-logo.svg -REACT_APP_RECAPTCHA_SITE_KEY=6LdvxuQUAAAAADDWVflgBqyHGfq-xmvNJaToM0pN -REACT_APP_KASP_ENABLED=false -REACT_APP_KASP_STRING=KASP -REACT_APP_KASP_FULL_STRING="Karunya Arogya Suraksha Padhathi" # Dev envs ESLINT_NO_DEV_ERRORS=true \ No newline at end of file diff --git a/netlify.toml b/netlify.toml index 223ddccd64f..0747b7a495a 100644 --- a/netlify.toml +++ b/netlify.toml @@ -33,3 +33,11 @@ force = true from = "/*" to = "/index.html" status = 200 + +[[headers]] + for = "/*" + [headers.values] + cache-control = ''' + max-age=0, + no-store''' + diff --git a/public/config.json b/public/config.json index 01c49881256..8a915c55c96 100644 --- a/public/config.json +++ b/public/config.json @@ -1,13 +1,18 @@ { "dashboard_url": "https://dashboard.coronasafe.in", "github_url": "https://github.com/coronasafe", + "coronasafe_url": "https://coronasafe.network?ref=care", "static_header_logo": "https://cdn.coronasafe.network/header_logo.png", "static_light_logo": "https://cdn.coronasafe.network/light-logo.svg", "static_black_logo": "https://cdn.coronasafe.network/black-logo.svg", + "static_dpg_white_logo": "https://digitalpublicgoods.net/wp-content/themes/dpga/images/logo-w.svg", + "static_coronasafe_logo": "https://3451063158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-M233b0_JITp4nk0uAFp%2F-M2Dx6gKxOSU45cjfgNX%2F-M2DxFOkMmkPNn0I6U9P%2FCoronasafe-logo.png?alt=media&token=178cc96d-76d9-4e27-9efb-88f3186368e8", "gmaps_api_key": "AIzaSyDsBAc3y7deI5ZO3NtK5GuzKwtUzQNJNUk", + "gov_data_api_key": "579b464db66ec23bdd000001cdd3946e44ce4aad7209ff7b23ac571b", "recaptcha_site_key": "6LdvxuQUAAAAADDWVflgBqyHGfq-xmvNJaToM0pN", "kasp_enabled": false, "kasp_string": "KASP", "kasp_full_string": "Karunya Arogya Suraksha Padhathi", - "state_logo": "https://digitalpublicgoods.net/wp-content/themes/dpga/images/logo.svg" + "sample_format_asset_import": "https://spreadsheets.google.com/feeds/download/spreadsheets/Export?key=11JaEhNHdyCHth4YQs_44YaRlP77Rrqe81VSEfg1glko&exportFormat=xlsx", + "sample_format_external_result_import": "https://docs.google.com/spreadsheets/d/17VfgryA6OYSYgtQZeXU9mp7kNvLySeEawvnLBO_1nuE/export?format=csv&id=17VfgryA6OYSYgtQZeXU9mp7kNvLySeEawvnLBO_1nuE" } \ No newline at end of file diff --git a/public/favicon-dark.ico b/public/favicon-dark.ico new file mode 100644 index 00000000000..b1722d8d36b Binary files /dev/null and b/public/favicon-dark.ico differ diff --git a/public/favicon-light.ico b/public/favicon-light.ico new file mode 100644 index 00000000000..c4fddf60349 Binary files /dev/null and b/public/favicon-light.ico differ diff --git a/public/index.html b/public/index.html index 711d4c4e613..dce4c90a2d7 100644 --- a/public/index.html +++ b/public/index.html @@ -10,7 +10,7 @@ - + - - + + - + diff --git a/src/App.tsx b/src/App.tsx index 9d2e3da6122..13c46f3886b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,6 +6,7 @@ import { useDispatch, useSelector } from "react-redux"; import { getConfig, getCurrentUser } from "./Redux/actions"; import { useAbortableEffect, statusType } from "./Common/utils"; import axios from "axios"; +import { HistoryAPIProvider } from "./CAREUI/misc/HistoryAPIProvider"; const Loading = loadable(() => import("./Components/Common/Loading")); @@ -57,6 +58,17 @@ const App: React.FC = () => { [dispatch] ); + useEffect(() => { + const darkThemeMq = window.matchMedia("(prefers-color-scheme: dark)"); + const favicon: any = document.querySelector("link[rel~='icon']"); + console.log(favicon); + if (darkThemeMq.matches) { + favicon.href = "/favicon-light.ico"; + } else { + favicon.href = "/favicon-dark.ico"; + } + }, []); + if ( !currentUser || currentUser.isFetching || @@ -68,7 +80,11 @@ const App: React.FC = () => { } if (currentUser?.data) { - return ; + return ( + + + + ); } else { return ; } diff --git a/src/CAREUI/misc/HistoryAPIProvider.tsx b/src/CAREUI/misc/HistoryAPIProvider.tsx new file mode 100644 index 00000000000..3048aa22f02 --- /dev/null +++ b/src/CAREUI/misc/HistoryAPIProvider.tsx @@ -0,0 +1,38 @@ +import { useLocationChange } from "raviger"; +import { createContext, ReactNode, useState } from "react"; + +export const HistoryContext = createContext([]); +// eslint-disable-next-line @typescript-eslint/no-empty-function +export const ResetHistoryContext = createContext(() => {}); + +export const HistoryAPIProvider = (props: { children: ReactNode }) => { + const [history, setHistory] = useState([]); + + useLocationChange( + (newLocation) => { + setHistory((history) => { + if (history.length && newLocation.fullPath === history[0]) + // Ignore push if navigate to same path (for some weird unknown reasons?) + return history; + + if (history.length > 1 && newLocation.fullPath === history[1]) + // Pop current path if navigate back to previous path + return history.slice(1); + + // Otherwise just push the current path + return [newLocation.fullPath, ...history]; + }); + }, + { onInitial: true } + ); + + const resetHistory = () => setHistory((history) => history.slice(0, 1)); + + return ( + + + {props.children} + + + ); +}; diff --git a/src/Common/constants.tsx b/src/Common/constants.tsx index 4dd4dc0a32e..e8dca566f0e 100644 --- a/src/Common/constants.tsx +++ b/src/Common/constants.tsx @@ -1,6 +1,7 @@ import { PatientCategory } from "../Components/Facility/models"; import { parsePhoneNumberFromString } from "libphonenumber-js"; import moment from "moment"; +import { IConfig } from "./hooks/useConfig"; export const KeralaLogo = "images/kerala-logo.png"; @@ -13,10 +14,6 @@ export interface OptionsType { disabled?: boolean; } -export const KASP_STRING = process.env.REACT_APP_KASP_STRING ?? ""; -export const KASP_FULL_STRING = process.env.REACT_APP_KASP_FULL_STRING ?? ""; -export const KASP_ENABLED = process.env.REACT_APP_KASP_ENABLED === "true"; - export type UserRole = | "Pharmacist" | "Volunteer" @@ -168,28 +165,33 @@ export const PATIENT_FILTER_ORDER: (OptionsType & { order: string })[] = [ { id: 6, text: "-review_time", desc: "Review Time", order: "Descending" }, ]; -const KASP_BED_TYPES = KASP_ENABLED - ? [ - { id: 40, text: KASP_STRING + " Ordinary Beds" }, - { id: 60, text: KASP_STRING + " Oxygen beds" }, - { id: 50, text: KASP_STRING + " ICU (ICU without ventilator)" }, - { id: 70, text: KASP_STRING + " ICU (ICU with ventilator)" }, - ] - : []; - -export const BED_TYPES: Array = [ - { id: 1, text: "Non-Covid Ordinary Beds" }, - { id: 150, text: "Non-Covid Oxygen beds" }, - { id: 10, text: "Non-Covid ICU (ICU without ventilator)" }, - { id: 20, text: "Non-Covid Ventilator (ICU with ventilator)" }, - { id: 30, text: "Covid Ordinary Beds" }, - { id: 120, text: "Covid Oxygen beds" }, - { id: 110, text: "Covid ICU (ICU without ventilator)" }, - { id: 100, text: "Covid Ventilators (ICU with ventilator)" }, - ...KASP_BED_TYPES, - { id: 2, text: "Hostel" }, - { id: 3, text: "Single Room with Attached Bathroom" }, -]; +export const getBedTypes = ({ + kasp_enabled, + kasp_string, +}: Pick) => { + const kaspBedTypes = kasp_enabled + ? [ + { id: 40, text: kasp_string + " Ordinary Beds" }, + { id: 60, text: kasp_string + " Oxygen beds" }, + { id: 50, text: kasp_string + " ICU (ICU without ventilator)" }, + { id: 70, text: kasp_string + " ICU (ICU with ventilator)" }, + ] + : []; + + return [ + { id: 1, text: "Non-Covid Ordinary Beds" }, + { id: 150, text: "Non-Covid Oxygen beds" }, + { id: 10, text: "Non-Covid ICU (ICU without ventilator)" }, + { id: 20, text: "Non-Covid Ventilator (ICU with ventilator)" }, + { id: 30, text: "Covid Ordinary Beds" }, + { id: 120, text: "Covid Oxygen beds" }, + { id: 110, text: "Covid ICU (ICU without ventilator)" }, + { id: 100, text: "Covid Ventilators (ICU with ventilator)" }, + ...kaspBedTypes, + { id: 2, text: "Hostel" }, + { id: 3, text: "Single Room with Attached Bathroom" }, + ]; +}; export const DOCTOR_SPECIALIZATION: Array = [ { id: 1, text: "General Medicine", desc: "bg-doctors-general" }, @@ -282,6 +284,14 @@ export const CONSULTATION_SUGGESTION = [ { id: "DD", text: "Declare Death" }, ]; +export const CONSULTATION_STATUS = [ + { id: "1", text: "Brought Dead" }, + { id: "2", text: "Transferred from ward" }, + { id: "3", text: "Transferred from ICU" }, + { id: "4", text: "Referred from other hospital" }, + { id: "5", text: "Out-patient (walk in)" }, +]; + export const ADMITTED_TO = [ { id: "1", text: "Isolation" }, { id: "2", text: "ICU" }, @@ -367,7 +377,6 @@ export const DISEASE_STATUS = [ "SUSPECTED", "NEGATIVE", "RECOVERED", - "EXPIRED", ]; export const TEST_TYPE = [ diff --git a/src/Common/env.tsx b/src/Common/env.tsx deleted file mode 100644 index 55e7b1ffdbe..00000000000 --- a/src/Common/env.tsx +++ /dev/null @@ -1,6 +0,0 @@ -export const GMAPS_API_KEY = "AIzaSyDsBAc3y7deI5ZO3NtK5GuzKwtUzQNJNUk"; - -export const RECAPTCHA_SITE_KEY = "6LdvxuQUAAAAADDWVflgBqyHGfq-xmvNJaToM0pN"; - -export const GOV_DATA_API_KEY = - "579b464db66ec23bdd000001cdd3946e44ce4aad7209ff7b23ac571b"; diff --git a/src/Common/hooks/useAppHistory.ts b/src/Common/hooks/useAppHistory.ts new file mode 100644 index 00000000000..893b1a06cfd --- /dev/null +++ b/src/Common/hooks/useAppHistory.ts @@ -0,0 +1,26 @@ +import { navigate } from "raviger"; +import { useContext } from "react"; +import { + HistoryContext, + ResetHistoryContext, +} from "../../CAREUI/misc/HistoryAPIProvider"; + +export default function useAppHistory() { + const history = useContext(HistoryContext); + const resetHistory = useContext(ResetHistoryContext); + + const goBack = (fallbackUrl?: string) => { + if (history.length > 1) + // Navigate to history present in the app navigation history stack. + return navigate(history[1]); + + if (fallbackUrl) + // Otherwise, use provided fallback url if provided. + return navigate(fallbackUrl); + + // Otherwise, fallback to browser's go back behaviour. + window.history.back(); + }; + + return { history, resetHistory, goBack }; +} diff --git a/src/Common/hooks/useConfig.ts b/src/Common/hooks/useConfig.ts index 45803b0fb7b..914485f7ce7 100644 --- a/src/Common/hooks/useConfig.ts +++ b/src/Common/hooks/useConfig.ts @@ -2,14 +2,45 @@ import { useSelector } from "react-redux"; export interface IConfig { dashboard_url: string; - // gmaps_api_key?: string; - // sentry_api_key?: string; - // recaptcha_site_key?: string; - - // KASP related - // kasp_label?: string; - // kasp_long_label?: string; - // kasp_enabled?: boolean; + github_url: string; + coronasafe_url: string; + dpg_url: string; + static_header_logo: string; + static_light_logo: string; + static_black_logo: string; + /** + * White logo of Digital Public Goods. + */ + static_dpg_white_logo: string; + static_coronasafe_logo: string; + /** + * The API key for the Google Maps API used for location picker. + */ + gmaps_api_key: string; + /** + * The API key for the data.gov.in API used for pincode auto-complete. + */ + gov_data_api_key: string; + recaptcha_site_key: string; + kasp_enabled: boolean; + kasp_string: string; + kasp_full_string: string; + /** + * If present, the image will be displayed in the login page. + */ + state_logo?: string; + /** + * If true, the state logo will be white by applying "invert brightness-0" classes. + */ + state_logo_white?: boolean; + /** + * URL of the sample format for asset import. + */ + sample_format_asset_import: string; + /** + * URL of the sample format for external result import. + */ + sample_format_external_result_import: string; } const useConfig = () => { diff --git a/src/Common/hooks/useExport.tsx b/src/Common/hooks/useExport.tsx index fbc72fe7b31..4d39880e8ba 100644 --- a/src/Common/hooks/useExport.tsx +++ b/src/Common/hooks/useExport.tsx @@ -21,27 +21,35 @@ export default function useExport() { const getTimestamp = () => new Date().toISOString(); - const exportCSV = async (filenamePrefix: string, action: any) => { + const exportCSV = async ( + filenamePrefix: string, + action: any, + parse = (data: string) => data + ) => { setIsExporting(true); const filename = `${filenamePrefix}_${getTimestamp()}.csv`; const res = await dispatch(action); if (res.status === 200) { - setCsvLinkProps({ ...csvLinkProps, filename, data: res.data }); + setCsvLinkProps({ ...csvLinkProps, filename, data: parse(res.data) }); document.getElementById(csvLinkProps.id)?.click(); } setIsExporting(false); }; - const exportJSON = async (filenamePrefix: string, action: any) => { + const exportJSON = async ( + filenamePrefix: string, + action: any, + parse = (data: string) => data + ) => { setIsExporting(true); const res = await dispatch(action); if (res.status === 200) { const a = document.createElement("a"); - const blob = new Blob([JSON.stringify(res.data.results)], { + const blob = new Blob([parse(JSON.stringify(res.data.results))], { type: "application/json", }); a.href = URL.createObjectURL(blob); @@ -52,18 +60,23 @@ export default function useExport() { setIsExporting(false); }; - const exportFile = (action: any, filePrefix = "export", type = "csv") => { + const exportFile = ( + action: any, + filePrefix = "export", + type = "csv", + parse = (data: string) => data + ) => { if (!action) return; switch (type) { case "csv": - exportCSV(filePrefix, action()); + exportCSV(filePrefix, action(), parse); break; case "json": - exportJSON(filePrefix, action()); + exportJSON(filePrefix, action(), parse); break; default: - exportCSV(filePrefix, action()); + exportCSV(filePrefix, action(), parse); } }; diff --git a/src/Common/hooks/useFilters.tsx b/src/Common/hooks/useFilters.tsx index a34b0caf833..703d384281b 100644 --- a/src/Common/hooks/useFilters.tsx +++ b/src/Common/hooks/useFilters.tsx @@ -3,7 +3,7 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import GenericFilterBadge from "../../CAREUI/display/FilterBadge"; import PaginationComponent from "../../Components/Common/Pagination"; -import { KASP_STRING } from "../constants"; +import useConfig from "./useConfig"; export type FilterState = Record; export type FilterParamKeys = string | string[]; @@ -18,6 +18,7 @@ interface FilterBadgeProps { * of pagination and filters. */ export default function useFilters({ limit = 14 }: { limit?: number }) { + const { kasp_string } = useConfig(); const hasPagination = limit > 0; const [showFilters, setShowFilters] = useState(false); const [qParams, setQueryParams] = useQueryParams(); @@ -106,8 +107,8 @@ export default function useFilters({ limit = 14 }: { limit?: number }) { return { name, value, paramKey }; }, kasp(nameSuffix = "", paramKey = "is_kasp") { - const name = nameSuffix ? KASP_STRING + " " + nameSuffix : KASP_STRING; - const [trueLabel, falseLabel] = [KASP_STRING, "Non " + KASP_STRING]; + const name = nameSuffix ? kasp_string + " " + nameSuffix : kasp_string; + const [trueLabel, falseLabel] = [kasp_string, "Non " + kasp_string]; return badgeUtils.boolean(name, paramKey, { trueLabel, falseLabel }); }, }; diff --git a/src/Components/Assets/AssetFilter.tsx b/src/Components/Assets/AssetFilter.tsx index e917a967c22..8cc9fde6819 100644 --- a/src/Components/Assets/AssetFilter.tsx +++ b/src/Components/Assets/AssetFilter.tsx @@ -1,4 +1,3 @@ -import React from "react"; import { useState, useEffect, useCallback } from "react"; import { useAbortableEffect, statusType } from "../../Common/utils"; import { navigate, useQueryParams } from "raviger"; @@ -7,12 +6,12 @@ import { FacilityModel } from "../Facility/models"; import { useDispatch } from "react-redux"; import { getAnyFacility, getFacilityAssetLocation } from "../../Redux/actions"; import * as Notification from "../../Utils/Notifications.js"; -import { SelectField } from "../Common/HelperInputFields"; import { LocationSelect } from "../Common/LocationSelect"; -import { AssetLocationObject } from "./AssetTypes"; +import { AssetClass, AssetLocationObject } from "./AssetTypes"; import FilterButtons from "../Common/FilterButtons"; import { FieldLabel } from "../Form/FormFields/FormField"; import CareIcon from "../../CAREUI/icons/CareIcon"; +import { SelectFormField } from "../Form/FormFields/SelectFormField"; const initialLocation = { id: "", @@ -34,6 +33,9 @@ function AssetFilter(props: any) { filter.asset_type ? filter.asset_type : "" ); const [asset_status, setAssetStatus] = useState(filter.status || ""); + const [asset_class, setAssetClass] = useState( + filter.asset_class || "" + ); const [facilityId, setFacilityId] = useState(filter.facility); const [locationId, setLocationId] = useState(filter.location); const [qParams, _] = useQueryParams(); @@ -101,6 +103,7 @@ function AssetFilter(props: any) { const data = { facility: facilityId, asset_type: asset_type, + asset_class: asset_class, status: asset_status, location: locationId, }; @@ -159,63 +162,64 @@ function AssetFilter(props: any) { )}
Asset Type - title} + optionValue={({ value }) => value} value={asset_type} - onChange={(e: React.ChangeEvent) => - setAssetType(e.target.value) - } + onChange={({ value }) => setAssetType(value)} />
Asset Status - title} + optionValue={({ value }) => value} + value={asset_status} + onChange={({ value }) => setAssetStatus(value)} + /> +
+ +
+ Asset Class + ) => - setAssetStatus(e.target.value) - } + optionLabel={({ title }) => title} + optionValue={({ value }) => value} + value={asset_class} + onChange={({ value }) => setAssetClass(value)} />
diff --git a/src/Components/Assets/AssetImportModal.tsx b/src/Components/Assets/AssetImportModal.tsx index 095dea8905f..41cd1303f26 100644 --- a/src/Components/Assets/AssetImportModal.tsx +++ b/src/Components/Assets/AssetImportModal.tsx @@ -13,6 +13,7 @@ import SelectMenuV2 from "../Form/SelectMenuV2"; import readXlsxFile from "read-excel-file"; import { XLSXAssetImportSchema } from "../../Common/constants"; import { parseCsvFile } from "../../Utils/utils"; +import useConfig from "../../Common/hooks/useConfig"; interface Props { open: boolean; @@ -27,6 +28,7 @@ const AssetImportModal = ({ open, onClose, facility }: Props) => { const [location, setLocation] = useState(""); const [locations, setLocations] = useState([]); const dispatchAction: any = useDispatch(); + const { sample_format_asset_import } = useConfig(); const closeModal = () => { setPreview(undefined); @@ -281,7 +283,7 @@ const AssetImportModal = ({ open, onClose, facility }: Props) => {

{" "} Sample Format diff --git a/src/Components/Assets/AssetManage.tsx b/src/Components/Assets/AssetManage.tsx index 775f014f74f..9fd48476bcd 100644 --- a/src/Components/Assets/AssetManage.tsx +++ b/src/Components/Assets/AssetManage.tsx @@ -216,6 +216,7 @@ const AssetManage = (props: AssetManageProps) => { title="Delete Asset" description="Are you sure you want to delete this asset?" action="Confirm" + variant="danger" show={showDeleteDialog} onClose={() => setShowDeleteDialog(false)} onConfirm={handleDelete} diff --git a/src/Components/Assets/AssetsList.tsx b/src/Components/Assets/AssetsList.tsx index eda93c2dea0..34a03682965 100644 --- a/src/Components/Assets/AssetsList.tsx +++ b/src/Components/Assets/AssetsList.tsx @@ -49,6 +49,7 @@ const AssetsList = () => { const [totalCount, setTotalCount] = useState(0); const [facility, setFacility] = useState(); const [asset_type, setAssetType] = useState(); + const [asset_class, setAssetClass] = useState(); const [locationName, setLocationName] = useState(); const [importAssetModalOpen, setImportAssetModalOpen] = useState(false); const dispatch: any = useDispatch(); @@ -68,6 +69,7 @@ const AssetsList = () => { search_text: qParams.search || "", facility: qParams.facility, asset_type: qParams.asset_type, + asset_class: qParams.asset_class, location: qParams.location, status: qParams.status, }; @@ -90,6 +92,7 @@ const AssetsList = () => { qParams.search, qParams.facility, qParams.asset_type, + qParams.asset_class, qParams.location, qParams.status, ] @@ -99,6 +102,10 @@ const AssetsList = () => { setAssetType(qParams.asset_type); }, [qParams.asset_type]); + useEffect(() => { + setAssetClass(qParams.asset_class); + }, [qParams.asset_class]); + useAbortableEffect( (status: statusType) => { fetchData(status); @@ -380,6 +387,7 @@ const AssetsList = () => { value("Facility", ["facility", "location"], facility?.name || ""), badge("Name", "search"), value("Asset Type", "asset_type", asset_type || ""), + value("Asset Class", "asset_class", asset_class || ""), badge("Status", "status"), value("Location", "location", locationName || ""), ]} diff --git a/src/Components/Assets/configure/CameraConfigure.tsx b/src/Components/Assets/configure/CameraConfigure.tsx index 97351983dae..1bff11b7c31 100644 --- a/src/Components/Assets/configure/CameraConfigure.tsx +++ b/src/Components/Assets/configure/CameraConfigure.tsx @@ -1,4 +1,3 @@ -import { Fragment } from "react"; import React from "react"; import { AssetData } from "../AssetTypes"; import { Card, CardContent } from "@material-ui/core"; @@ -32,7 +31,7 @@ export default function CameraConfigure(props: CameraConfigureProps) { } = props; return ( - +
@@ -79,6 +78,6 @@ export default function CameraConfigure(props: CameraConfigureProps) { /> - +
); } diff --git a/src/Components/Auth/Login.tsx b/src/Components/Auth/Login.tsx index c4e6424a884..8954328d777 100644 --- a/src/Components/Auth/Login.tsx +++ b/src/Components/Auth/Login.tsx @@ -4,14 +4,27 @@ import { postForgotPassword, postLogin } from "../../Redux/actions"; import { Grid, CircularProgress } from "@material-ui/core"; import { useTranslation } from "react-i18next"; import ReCaptcha from "react-google-recaptcha"; -import { RECAPTCHA_SITE_KEY } from "../../Common/env"; import * as Notification from "../../Utils/Notifications.js"; import { get } from "lodash"; import LegendInput from "../../CAREUI/interactive/LegendInput"; import LanguageSelectorLogin from "../Common/LanguageSelectorLogin"; import CareIcon from "../../CAREUI/icons/CareIcon"; +import useConfig from "../../Common/hooks/useConfig"; +import { classNames } from "../../Utils/utils"; export const Login = (props: { forgot?: boolean }) => { + const { + static_light_logo, + static_black_logo, + static_dpg_white_logo, + static_coronasafe_logo, + recaptcha_site_key, + github_url, + coronasafe_url, + dpg_url, + state_logo, + state_logo_white, + } = useConfig(); const dispatch: any = useDispatch(); const initForm: any = { username: "", @@ -22,7 +35,6 @@ export const Login = (props: { forgot?: boolean }) => { const [form, setForm] = useState(initForm); const [errors, setErrors] = useState(initErr); const [isCaptchaEnabled, setCaptcha] = useState(false); - const captchaKey = RECAPTCHA_SITE_KEY ?? ""; const { t } = useTranslation(); // display spinner while login is under progress const [loading, setLoading] = useState(false); @@ -168,18 +180,33 @@ export const Login = (props: { forgot?: boolean }) => { return (
- - coronasafe logo - +
+ {state_logo && ( + <> + state logo +
+ + )} + + coronasafe logo + +

@@ -192,22 +219,30 @@ export const Login = (props: { forgot?: boolean }) => {

+
+ + Logo of Digital Public Goods Alliance + +
+ + coronasafe logo + +
- {t("powered_by")} - coronasafe logo - - @@ -215,8 +250,8 @@ export const Login = (props: { forgot?: boolean }) => {
@@ -238,7 +273,7 @@ export const Login = (props: { forgot?: boolean }) => { } > care logo{" "} @@ -274,7 +309,7 @@ export const Login = (props: { forgot?: boolean }) => { {isCaptchaEnabled && ( {errors.captcha} @@ -319,7 +354,7 @@ export const Login = (props: { forgot?: boolean }) => { } > care logo{" "} diff --git a/src/Components/Common/ConfirmDialogV2.tsx b/src/Components/Common/ConfirmDialogV2.tsx index c77a786d12f..fd6164b0410 100644 --- a/src/Components/Common/ConfirmDialogV2.tsx +++ b/src/Components/Common/ConfirmDialogV2.tsx @@ -30,17 +30,17 @@ const ConfirmDialogV2 = ({ + {description} + + } show={show} > {children} -
+
- + {action}
diff --git a/src/Components/Common/Dialog.tsx b/src/Components/Common/Dialog.tsx index ff6de6e5713..80e137b5d89 100644 --- a/src/Components/Common/Dialog.tsx +++ b/src/Components/Common/Dialog.tsx @@ -53,7 +53,7 @@ const DialogModal = (props: DialogProps) => {

{title}

-

{description}

+

{description}

{children} diff --git a/src/Components/Common/Export.tsx b/src/Components/Common/Export.tsx index 58e56a03ee1..85abef7c583 100644 --- a/src/Components/Common/Export.tsx +++ b/src/Components/Common/Export.tsx @@ -26,6 +26,7 @@ interface ExportButtonProps { tooltipClassName?: string; type?: "csv" | "json"; action?: any; + parse?: (data: string) => string; filenamePrefix: string; } @@ -61,6 +62,7 @@ export const ExportMenu = ({ export const ExportButton = ({ tooltipClassName = "tooltip-bottom -translate-x-7", type = "csv", + parse, ...props }: ExportButtonProps) => { const { isExporting, exportFile, _CSVLink } = useExport(); @@ -70,7 +72,9 @@ export const ExportButton = ({ <_CSVLink /> exportFile(props.action, props.filenamePrefix, type)} + onClick={() => + exportFile(props.action, props.filenamePrefix, type, parse) + } className="mx-2 tooltip p-4 text-lg text-secondary-800 disabled:text-secondary-500 disabled:bg-transparent" variant="secondary" ghost diff --git a/src/Components/Common/FilterButtons.tsx b/src/Components/Common/FilterButtons.tsx index a0291c591b5..2f9fa6c7512 100644 --- a/src/Components/Common/FilterButtons.tsx +++ b/src/Components/Common/FilterButtons.tsx @@ -1,3 +1,5 @@ +import { useTranslation } from "react-i18next"; +import CareIcon from "../../CAREUI/icons/CareIcon"; import ButtonV2 from "./components/ButtonV2"; type Callback = () => void; type Props = { @@ -7,28 +9,20 @@ type Props = { }; const FilterButtons = ({ onClose, onClear, onApply }: Props) => { + const { t } = useTranslation(); + return ( -
- - Cancel +
+ + + {t("Cancel")} - - Clear Filters + + + {t("Clear Filters")}
- - Apply - + {t("Apply")}
); }; diff --git a/src/Components/Common/GLocationPicker.tsx b/src/Components/Common/GLocationPicker.tsx index 6705cb487e0..34d7ceace07 100644 --- a/src/Components/Common/GLocationPicker.tsx +++ b/src/Components/Common/GLocationPicker.tsx @@ -3,9 +3,9 @@ import { Wrapper, Status } from "@googlemaps/react-wrapper"; import { deepEqual } from "../../Common/utils"; import { isLatLngLiteral } from "@googlemaps/typescript-guards"; import PersonPinIcon from "@material-ui/icons/PersonPin"; -import { GMAPS_API_KEY } from "../../Common/env"; import Spinner from "./Spinner"; import CareIcon from "../../CAREUI/icons/CareIcon"; +import useConfig from "../../Common/hooks/useConfig"; const render = (status: Status) => { if (status === "LOADING") { @@ -32,6 +32,7 @@ const GLocationPicker = ({ handleOnClose, handleOnSelectCurrentLocation, }: GLocationPickerProps) => { + const { gmaps_api_key } = useConfig(); const [location, setLocation] = React.useState( null ); @@ -66,7 +67,7 @@ const GLocationPicker = ({ return (
- + { const onChangeHandler = debounce(onChange, 500); useEffect(() => { - setMaxLength(() => (value.slice(4, 8) === "1800" ? 16 : 15)); + setMaxLength(() => (value?.slice(4, 8) === "1800" ? 16 : 15)); }, [value]); const handleChange = ( diff --git a/src/Components/Common/PageTitle.tsx b/src/Components/Common/PageTitle.tsx index 6ed7c442344..52dd839454e 100644 --- a/src/Components/Common/PageTitle.tsx +++ b/src/Components/Common/PageTitle.tsx @@ -1,15 +1,19 @@ import React, { useEffect, useRef } from "react"; import Breadcrumbs from "./Breadcrumbs"; import PageHeadTitle from "./PageHeadTitle"; -import { classNames, goBack } from "../../Utils/utils"; +import { classNames } from "../../Utils/utils"; +import useAppHistory from "../../Common/hooks/useAppHistory"; interface PageTitleProps { title: string; hideBack?: boolean; backUrl?: string; - backButtonCB?: () => number | void; className?: string; componentRight?: React.ReactNode; + /** + * If `false` is returned, prevents from going back. + */ + onBackClick?: () => boolean | void; justifyContents?: | "justify-center" | "justify-start" @@ -26,8 +30,8 @@ export default function PageTitle({ title, hideBack = false, backUrl, - backButtonCB, className = "", + onBackClick, componentRight = <>, breadcrumbs = true, crumbsReplacements = {}, @@ -42,8 +46,7 @@ export default function PageTitle({ } }, [divRef, focusOnLoad]); - const onBackButtonClick = () => - backButtonCB ? goBack(backButtonCB()) : goBack(backUrl); + const { goBack } = useAppHistory(); return (
@@ -51,7 +54,12 @@ export default function PageTitle({
{!hideBack && ( - - - - + setOpenDeleteDialog(false)} + title={`Delete ${label}?`} + description="You will not be able to access this bed type later." + action="Delete" + variant="danger" + onConfirm={handleDeleteSubmit} + /> {open && ( { const { itemData, isLastConsultation } = props; + const { kasp_string } = useConfig(); return (
{itemData.is_kasp && (
- {KASP_STRING} + {kasp_string}
)} @@ -57,7 +58,7 @@ export const ConsultationCard = (props: ConsultationProps) => {
- {KASP_STRING} Enabled date{" "} + {kasp_string} Enabled date{" "}
{itemData.kasp_enabled_date diff --git a/src/Components/Facility/ConsultationDetails.tsx b/src/Components/Facility/ConsultationDetails.tsx index 9bf7beaae6a..fb6ac8fd50a 100644 --- a/src/Components/Facility/ConsultationDetails.tsx +++ b/src/Components/Facility/ConsultationDetails.tsx @@ -1167,6 +1167,34 @@ export const ConsultationDetails = (props: any) => {
)} + {consultationData.procedure && ( +
+
+
+
+ +
+
+
+
+ )} import("../Common/Loading")); const PageTitle = loadable(() => import("../Common/PageTitle")); @@ -70,6 +70,7 @@ type FormDetails = { other_symptoms: string; symptoms_onset_date?: Date; suggestion: string; + consultation_status: number; patient: string; facility: string; admitted: BooleanStrings; @@ -88,6 +89,7 @@ type FormDetails = { prescribed_medication: string; consultation_notes: string; ip_no: string; + procedure: ProcedureType[]; discharge_advice: PrescriptionType[]; prn_prescription: PRNPrescriptionType[]; investigation: InvestigationType[]; @@ -115,6 +117,7 @@ const initForm: FormDetails = { other_symptoms: "", symptoms_onset_date: undefined, suggestion: "A", + consultation_status: 0, patient: "", facility: "", admitted: "false", @@ -133,6 +136,7 @@ const initForm: FormDetails = { prescribed_medication: "", consultation_notes: "", ip_no: "", + procedure: [], discharge_advice: [], prn_prescription: [], investigation: [], @@ -187,6 +191,7 @@ const scrollTo = (id: any) => { }; export const ConsultationForm = (props: any) => { + const { kasp_enabled, kasp_string } = useConfig(); const dispatchAction: any = useDispatch(); const { facilityId, patientId, id } = props; const [state, dispatch] = useReducer(consultationFormReducer, initialState); @@ -329,6 +334,13 @@ export const ConsultationForm = (props: any) => { invalidForm = true; } return; + case "consultation_status": + if (!state.form[field]) { + errors[field] = "Please select the consultation status"; + if (!error_div) error_div = field; + invalidForm = true; + } + return; case "ip_no": if (!state.form[field]) { errors[field] = "Please enter IP Number"; @@ -416,7 +428,7 @@ export const ConsultationForm = (props: any) => { if (!state.form[field]) { errors[ field - ] = `Please select an option, ${KASP_STRING} is mandatory`; + ] = `Please select an option, ${kasp_string} is mandatory`; if (!error_div) error_div = field; invalidForm = true; } @@ -443,44 +455,67 @@ export const ConsultationForm = (props: any) => { } return; } + case "procedure": { + for (const p of procedures) { + if (!p.procedure?.replace(/\s/g, "").length) { + errors[field] = "Procedure field can not be empty"; + if (!error_div) error_div = field; + invalidForm = true; + break; + } + if (!p.repetitive && !p.time?.replace(/\s/g, "").length) { + errors[field] = "Time field can not be empty"; + if (!error_div) error_div = field; + invalidForm = true; + break; + } + if (p.repetitive && !p.frequency?.replace(/\s/g, "").length) { + errors[field] = "Frequency field can not be empty"; + if (!error_div) error_div = field; + invalidForm = true; + break; + } + } + return; + } case "prn_prescription": { - let invalid = false; for (const f of PRNAdvice) { - if ( - !f.dosage?.replace(/\s/g, "").length || - !f.medicine?.replace(/\s/g, "").length || - f.indicator === "" || - f.indicator === " " - ) { - invalid = true; + if (!f.medicine?.replace(/\s/g, "").length) { + errors[field] = "Medicine field can not be empty"; + if (!error_div) error_div = field; + invalidForm = true; + break; + } + if (!f.indicator?.replace(/\s/g, "").length) { + errors[field] = "Indicator field can not be empty"; + if (!error_div) error_div = field; + invalidForm = true; break; } - } - if (invalid) { - errors[field] = "PRN Prescription field can not be empty"; - if (!error_div) error_div = field; - invalidForm = true; } return; } case "investigation": { - let invalid = false; - for (const f of InvestigationAdvice) { - if ( - f.type?.length === 0 || - (f.repetitive - ? !f.frequency?.replace(/\s/g, "").length - : !f.time?.replace(/\s/g, "").length) - ) { - invalid = true; + for (const i of InvestigationAdvice) { + if (!i.type?.length) { + errors[field] = "Investigation field can not be empty"; + if (!error_div) error_div = field; + invalidForm = true; + break; + } + if (!i.repetitive && !i.time?.replace(/\s/g, "").length) { + errors[field] = "Time field can not be empty"; + if (!error_div) error_div = field; + invalidForm = true; + break; + } + if (i.repetitive && !i.frequency?.replace(/\s/g, "").length) { + errors[field] = "Frequency field can not be empty"; + if (!error_div) error_div = field; + invalidForm = true; break; } - } - if (invalid) { - errors[field] = "Investigation Suggestion field can not be empty"; - if (!error_div) error_div = field; - invalidForm = true; } return; } @@ -540,6 +575,7 @@ export const ConsultationForm = (props: any) => { ? state.form.symptoms_onset_date : undefined, suggestion: state.form.suggestion, + consultation_status: Number(state.form.consultation_status), admitted: state.form.suggestion === "A", admission_date: state.form.suggestion === "A" ? state.form.admission_date : undefined, @@ -746,6 +782,13 @@ export const ConsultationForm = (props: any) => { options={CONSULTATION_SUGGESTION} /> + + {state.form.suggestion === "R" && (
Referred To Facility @@ -793,7 +836,6 @@ export const ConsultationForm = (props: any) => { <> @@ -831,15 +873,6 @@ export const ConsultationForm = (props: any) => {
-
- Procedures - - -
-
Prescription Medication {
-
+
PRN Prescription {
+
+ Procedures + + +
+ { label="Diagnosis (as per ICD-11 recommended by WHO)" /> - {KASP_ENABLED && ( + {kasp_enabled && (
- {KASP_STRING} + {kasp_string} {
{/* End of Telemedicine fields */} -
+
navigate(`/facility/${facilityId}/patient/${patientId}`) diff --git a/src/Components/Facility/DoctorVideoSlideover.tsx b/src/Components/Facility/DoctorVideoSlideover.tsx index ec9ddcbc689..54b08e8eb81 100644 --- a/src/Components/Facility/DoctorVideoSlideover.tsx +++ b/src/Components/Facility/DoctorVideoSlideover.tsx @@ -1,8 +1,8 @@ import moment from "moment"; -import React, { useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { useDispatch } from "react-redux"; +import SlideOver from "../../CAREUI/interactive/SlideOver"; import { getFacilityUsers } from "../../Redux/actions"; -import { make as SlideOver } from "../Common/SlideOver.gen"; import { UserAssignedModel } from "../Users/models"; export default function DoctorVideoSlideover(props: { @@ -37,69 +37,57 @@ export default function DoctorVideoSlideover(props: { }, [show, facilityId]); return ( - -
- {/* Title and close button */} -
+ + {/* Title and close button */} +

+ Select a doctor to connect via video +

+ {[ + { + title: "Doctors", + user_type: "Doctor", + home: true, + }, + { + title: "Staff", + user_type: "Staff", + home: true, + }, + { + title: "TeleICU Hub", + user_type: "Doctor", + home: false, + }, + ].map((type, i) => ( +
-

Doctor Connect

-

- Select a doctor to connect via video -

+ {type.title}
- + {doctors + .filter((doc) => { + const isHomeUser = + (doc.home_facility_object?.id || "") === facilityId; + return ( + doc.user_type === type.user_type && isHomeUser === type.home + ); + }) + .map((doctor) => { + return ; + })} +
- {[ - { - title: "Doctors", - user_type: "Doctor", - home: true, - }, - { - title: "Staff", - user_type: "Staff", - home: true, - }, - { - title: "TeleICU Hub", - user_type: "Doctor", - home: false, - }, - ].map((type, i) => ( -
-
- {type.title} -
- -
    - {doctors - .filter((doc) => { - const isHomeUser = - (doc.home_facility_object?.id || "") === facilityId; - return ( - doc.user_type === type.user_type && isHomeUser === type.home - ); - }) - .map((doctor) => { - return ; - })} -
-
- ))} -
+ ))} ); } @@ -148,8 +136,13 @@ function UserListItem(props: { user: UserAssignedModel }) { }
-

- {user.first_name} {user.last_name} +

+ + {user.first_name} {user.last_name} + + {user.user_type === "Doctor" && ( + {user.doctor_qualification} + )}

{user.alt_phone_number} diff --git a/src/Components/Facility/DoctorsCountCard.tsx b/src/Components/Facility/DoctorsCountCard.tsx index 5757950658c..81be9505830 100644 --- a/src/Components/Facility/DoctorsCountCard.tsx +++ b/src/Components/Facility/DoctorsCountCard.tsx @@ -5,16 +5,10 @@ import { RoleButton } from "../Common/RoleButton"; import { useDispatch } from "react-redux"; import { deleteDoctor } from "../../Redux/actions"; import * as Notification from "../../Utils/Notifications"; -import { - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, -} from "@material-ui/core"; import { DoctorIcon } from "../TeleIcu/Icons/DoctorIcon"; import { DoctorCapacity } from "./DoctorCapacity"; import DialogModal from "../Common/Dialog"; +import ConfirmDialogV2 from "../Common/ConfirmDialogV2"; interface DoctorsCountProps extends DoctorModal { facilityId: string; @@ -88,33 +82,15 @@ const DoctorsCountCard = (props: DoctorsCountProps) => { Delete

- - - Are you sure you want to delete {specialization?.text} doctors? - - - - You will not be able to access this docter specialization type - later. - - - - - - - + title={`Delete ${specialization?.text} doctors`} + description="You will not be able to access this docter specialization type later." + action="Delete" + variant="danger" + onConfirm={handleDeleteSubmit} + />
{open && ( { const { facility, userType } = props; + const { kasp_string } = useConfig(); const { t } = useTranslation(); const dispatchAction: any = useDispatch(); @@ -82,7 +84,7 @@ export const FacilityCard = (props: { facility: any; userType: any }) => {
{facility.kasp_empanelled && (
- {KASP_STRING} + {kasp_string}
)} import("../Common/Loading")); const PageTitle = loadable(() => import("../Common/PageTitle")); @@ -148,6 +153,7 @@ const facilityCreateReducer = ( }; export const FacilityCreate = (props: FacilityProps) => { + const { gov_data_api_key, kasp_string, kasp_enabled } = useConfig(); const dispatchAction: any = useDispatch(); const { facilityId } = props; @@ -165,6 +171,10 @@ export const FacilityCreate = (props: FacilityProps) => { const [createdFacilityId, setCreatedFacilityId] = useState(""); const { width } = useWindowDimensions(); const [showAutoFilledPincode, setShowAutoFilledPincode] = useState(false); + const [capacityData, setCapacityData] = useState>([]); + const [doctorData, setDoctorData] = useState>([]); + const [bedCapacityKey, setBedCapacityKey] = useState(0); + const [docCapacityKey, setDocCapacityKey] = useState(0); const [anchorEl, setAnchorEl] = React.useState< (EventTarget & Element) | null @@ -351,7 +361,7 @@ export const FacilityCreate = (props: FacilityProps) => { if (!validatePincode(e.value)) return; - const pincodeDetails = await getPincodeDetails(e.value); + const pincodeDetails = await getPincodeDetails(e.value, gov_data_api_key); if (!pincodeDetails) return; const matchedState = states.find((state) => { @@ -569,6 +579,110 @@ export const FacilityCreate = (props: FacilityProps) => { const open = Boolean(anchorEl); const id = open ? "map-popover" : undefined; + let capacityList: any = null; + let totalBedCount = 0; + let totalOccupiedBedCount = 0; + + if (!capacityData || !capacityData.length) { + capacityList = ( +
+ No Bed Types Found +
+ ); + } else { + capacityData.forEach((x) => { + totalBedCount += x.total_capacity ? x.total_capacity : 0; + totalOccupiedBedCount += x.current_capacity ? x.current_capacity : 0; + }); + + capacityList = ( +
+ { + return; + }} + /> + {getBedTypes({ kasp_string, kasp_enabled }).map((x) => { + const res = capacityData.find((data) => { + return data.room_type === x.id; + }); + if (res && res.current_capacity && res.total_capacity) { + const removeCurrentBedType = (bedTypeId: number | undefined) => { + setCapacityData((state) => + state.filter((i) => i.id !== bedTypeId) + ); + setBedCapacityKey((bedCapacityKey) => bedCapacityKey + 1); + }; + return ( + { + const capacityRes = await dispatchAction( + listCapacity({}, { facilityId: createdFacilityId }) + ); + if (capacityRes && capacityRes.data) { + setCapacityData(capacityRes.data.results); + } + }} + /> + ); + } + })} +
+ ); + } + + let doctorList: any = null; + if (!doctorData || !doctorData.length) { + doctorList = ( +
+ No Doctors Found +
+ ); + } else { + doctorList = ( +
+ {doctorData.map((data: DoctorModal) => { + const removeCurrentDoctorData = (doctorId: number | undefined) => { + setDoctorData((state) => + state.filter((i: DoctorModal) => i.id !== doctorId) + ); + setDocCapacityKey((docCapacityKey) => docCapacityKey + 1); + }; + + return ( + { + const doctorRes = await dispatchAction( + listDoctor({}, { facilityId: createdFacilityId }) + ); + if (doctorRes && doctorRes.data) { + setDoctorData(doctorRes.data.results); + } + }} + {...data} + removeDoctor={removeCurrentDoctorData} + /> + ); + })} +
+ ); + } + switch (currentStep) { case 3: return ( @@ -582,16 +696,28 @@ export const FacilityCreate = (props: FacilityProps) => {
{ navigate(`/facility/${createdFacilityId}`); }} - handleUpdate={() => { - return; + handleUpdate={async () => { + const doctorRes = await dispatchAction( + listDoctor({}, { facilityId: createdFacilityId }) + ); + if (doctorRes && doctorRes.data) { + setDoctorData(doctorRes.data.results); + } }} />
+
+
+
Doctors List
+
+
{doctorList}
+
); case 2: @@ -606,16 +732,28 @@ export const FacilityCreate = (props: FacilityProps) => {
{ setCurrentStep(3); }} - handleUpdate={() => { - return; + handleUpdate={async () => { + const capacityRes = await dispatchAction( + listCapacity({}, { facilityId: createdFacilityId }) + ); + if (capacityRes && capacityRes.data) { + setCapacityData(capacityRes.data.results); + } }} />
+
+
+
Bed Capacity
+
+
{capacityList}
+
); case 1: @@ -1027,13 +1165,13 @@ export const FacilityCreate = (props: FacilityProps) => {
- {KASP_ENABLED && ( + {kasp_enabled && (
- Is this facility {KASP_STRING} empanelled? + Is this facility {kasp_string} empanelled? - {KASP_STRING} Empanelled + {kasp_string} Empanelled import("../Common/Loading")); const PageTitle = loadable(() => import("../Common/PageTitle")); @@ -70,6 +71,7 @@ export const FacilityHome = (props: any) => { >([]); const [bedCapacityModalOpen, setBedCapacityModalOpen] = useState(false); const [doctorCapacityModalOpen, setDoctorCapacityModalOpen] = useState(false); + const config = useConfig(); const fetchData = useCallback( async (status: statusType) => { @@ -170,7 +172,7 @@ export const FacilityHome = (props: any) => { return; }} /> - {BED_TYPES.map((x) => { + {getBedTypes(config).map((x) => { const res = capacityData.find((data) => { return data.room_type === x.id; }); diff --git a/src/Components/Facility/models.tsx b/src/Components/Facility/models.tsx index 8acafb2a323..fb72fb0e248 100644 --- a/src/Components/Facility/models.tsx +++ b/src/Components/Facility/models.tsx @@ -1,4 +1,5 @@ import { PRNPrescriptionType } from "../Common/prescription-builder/PRNPrescriptionBuilder"; +import { ProcedureType } from "../Common/prescription-builder/ProcedureBuilder"; import { AssignedToObjectModel } from "../Patient/models"; export interface LocalBodyModel { @@ -113,6 +114,7 @@ export interface ConsultationModel { is_telemedicine?: boolean; discharge_advice?: any; prn_prescription?: PRNPrescriptionType[]; + procedure?: ProcedureType[]; assigned_to_object?: AssignedToObjectModel; created_by?: any; last_edited_by?: any; diff --git a/src/Components/Form/FormFields/FormField.tsx b/src/Components/Form/FormFields/FormField.tsx index 194946c12c8..199b4e9ffbf 100644 --- a/src/Components/Form/FormFields/FormField.tsx +++ b/src/Components/Form/FormFields/FormField.tsx @@ -47,6 +47,7 @@ const FormField = ({ }: { field: FormFieldBaseProps; children: React.ReactNode; + className?: string; }) => { return (
@@ -59,8 +60,8 @@ const FormField = ({ {field.label} )} - {children} - {} +
{children}
+
); }; diff --git a/src/Components/Form/FormFields/Month.tsx b/src/Components/Form/FormFields/Month.tsx new file mode 100644 index 00000000000..c8776439a5b --- /dev/null +++ b/src/Components/Form/FormFields/Month.tsx @@ -0,0 +1,75 @@ +import { useEffect, useState } from "react"; +import AutocompleteFormField from "./Autocomplete"; +import FormField from "./FormField"; +import TextFormField from "./TextFormField"; +import { + FormFieldBaseProps, + resolveFormFieldChangeEventHandler, +} from "./Utils"; + +type Props = FormFieldBaseProps & { + suffix?: (value?: Date) => React.ReactNode; +}; + +const MonthLabels = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", +]; + +const MonthFormField = (props: Props) => { + const handleChange = resolveFormFieldChangeEventHandler(props); + + const [month, setMonth] = useState(props.value?.getMonth()); + const [year, setYear] = useState(props.value?.getFullYear()); + + useEffect(() => { + if (month === undefined || year === undefined) return; + handleChange({ + name: props.name, + value: new Date(year, month), + }); + }, [month, year]); + + return ( + + MonthLabels[month]} + optionValue={(month) => month} + onChange={(event) => setMonth(event.value)} + /> + setYear(parseInt(event.value))} + /> + {props.suffix && ( + {props.suffix(props.value)} + )} + + ); +}; + +export default MonthFormField; diff --git a/src/Components/Patient/FileUpload.tsx b/src/Components/Patient/FileUpload.tsx index 864cea51a1a..2fe42854849 100644 --- a/src/Components/Patient/FileUpload.tsx +++ b/src/Components/Patient/FileUpload.tsx @@ -141,11 +141,11 @@ export const FileUpload = (props: FileUploadProps) => { const [reload, setReload] = useState(false); const [uploadPercent, setUploadPercent] = useState(0); const [uploadFileName, setUploadFileName] = useState(""); - const [uploadFileNameError, setUploadFileNameError] = useState(""); + const [uploadFileError, setUploadFileError] = useState(""); const [url, seturl] = useState({}); const [fileUrl, setFileUrl] = useState(""); const [audioName, setAudioName] = useState(""); - const [audioNameError, setAudioNameError] = useState(""); + const [audioFileError, setAudioFileError] = useState(""); const [contentType, setcontentType] = useState(""); const [downloadURL, setDownloadURL] = useState(); const initialState = { @@ -833,6 +833,7 @@ export const FileUpload = (props: FileUploadProps) => { setUploadFileName( fileName.substring(0, fileName.lastIndexOf(".")) || fileName ); + const ext: string = fileName.split(".")[1]; setcontentType(header_content_type[ext]); @@ -880,7 +881,7 @@ export const FileUpload = (props: FileUploadProps) => { Notification.Success({ msg: "File Uploaded Successfully", }); - setUploadFileNameError(""); + setUploadFileError(""); resolve(response); }) .catch((e) => { @@ -896,12 +897,16 @@ export const FileUpload = (props: FileUploadProps) => { const validateFileUpload = () => { const filenameLength = uploadFileName.trim().length; const f = file; - if (f === undefined) { - setUploadFileNameError("Please choose a file to upload"); + if (f === undefined || f === null) { + setUploadFileError("Please choose a file to upload"); return false; } if (filenameLength === 0) { - setUploadFileNameError("Please give a name !!"); + setUploadFileError("Please give a name !!"); + return false; + } + if (f.size > 10e7) { + setUploadFileError("Maximum size of files is 100 MB"); return false; } return true; @@ -982,7 +987,12 @@ export const FileUpload = (props: FileUploadProps) => { const validateAudioUpload = () => { const f = audioBlob; - if (f === undefined) { + if (f === undefined || f === null) { + setAudioFileError("Please upload a file"); + return false; + } + if (f.size > 10e7) { + setAudioFileError("File size must not exceed 100 MB"); return false; } return true; @@ -990,7 +1000,7 @@ export const FileUpload = (props: FileUploadProps) => { const handleAudioUpload = async () => { if (!validateAudioUpload()) return; - setAudioNameError(""); + setAudioFileError(""); const category = "AUDIO"; const name = "audio.mp3"; const filename = @@ -1276,7 +1286,7 @@ export const FileUpload = (props: FileUploadProps) => { onChange={(e: any) => { setAudioName(e.target.value); }} - errors={audioNameError} + errors={audioFileError} /> {audiouploadStarted ? ( @@ -1309,7 +1319,7 @@ export const FileUpload = (props: FileUploadProps) => {

Upload New File

- Enter File Name + Enter File Name* { onChange={(e: any) => { setUploadFileName(e.target.value); }} - errors={uploadFileNameError} + errors={uploadFileError} />
diff --git a/src/Components/Patient/ManagePatients.tsx b/src/Components/Patient/ManagePatients.tsx index 79886a54f6d..7065b9403d7 100644 --- a/src/Components/Patient/ManagePatients.tsx +++ b/src/Components/Patient/ManagePatients.tsx @@ -19,6 +19,7 @@ import { ADMITTED_TO, GENDER_TYPES, PATIENT_CATEGORIES, + PATIENT_FILTER_ORDER, TELEMEDICINE_ACTIONS, } from "../../Common/constants"; import { make as SlideOver } from "../Common/SlideOver.gen"; @@ -35,6 +36,7 @@ import ButtonV2 from "../Common/components/ButtonV2"; import { ExportMenu } from "../Common/Export"; import PhoneNumberFormField from "../Form/FormFields/PhoneNumberFormField"; import { FieldChangeEvent } from "../Form/FormFields/Utils"; +import DropdownMenu, { DropdownItem } from "../Common/components/Menu"; const Loading = loadable(() => import("../Common/Loading")); const PageTitle = loadable(() => import("../Common/PageTitle")); @@ -71,8 +73,7 @@ const PatientCategoryDisplayText: Record = { export const PatientManager = () => { const dispatch: any = useDispatch(); - - const [data, setData] = useState([]); + const [data, setData] = useState(); const [isLoading, setIsLoading] = useState(false); const [totalCount, setTotalCount] = useState(0); const { @@ -89,7 +90,6 @@ export const PatientManager = () => { name: "", }); const [showDialog, setShowDialog] = useState(false); - const [districtName, setDistrictName] = useState(""); const [localbodyName, setLocalbodyName] = useState(""); const [facilityBadgeName, setFacilityBadge] = useState(""); @@ -239,20 +239,14 @@ export const PatientManager = () => { }; useEffect(() => { - if (params.page === 1) return; - setIsLoading(true); - dispatch(getAllPatient(params, "listPatients")) - .then((res: any) => { - if (res && res.data) { - setData(res.data.results); - setTotalCount(res.data.count); - } + dispatch(getAllPatient(params, "listPatients")).then((res: any) => { + if (res && res.data) { + setData(res.data.results); + setTotalCount(res.data.count); setIsLoading(false); - }) - .catch(() => { - setIsLoading(false); - }); + } + }); }, [ dispatch, qParams.last_consultation_admission_date_before, @@ -648,7 +642,6 @@ export const PatientManager = () => {
{ qParams.facility ? navigate(`/facility/${qParams.facility}/patient`) @@ -658,8 +651,10 @@ export const PatientManager = () => {

Add Patient Details

- + + } + > + {PATIENT_FILTER_ORDER.map((ordering) => { + return ( + updateQuery({ ordering: ordering.text })} + icon={ + + } + > + {ordering.desc} + + {ordering.order} + + + ); + })} +
value && moment(value).isValid() && moment(value).toDate(); export default function PatientFilterV2(props: any) { + const { kasp_enabled, kasp_string } = useConfig(); const { filter, onChange, closeFilter } = props; const [filterState, setFilterState] = useMergeState({ @@ -56,7 +55,6 @@ export default function PatientFilterV2(props: any) { 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, category: filter.category || null, gender: filter.gender || null, disease_status: filter.disease_status || null, @@ -109,7 +107,6 @@ export default function PatientFilterV2(props: any) { created_date_after: "", modified_date_before: "", modified_date_after: "", - ordering: "", category: null, gender: null, disease_status: null, @@ -211,7 +208,6 @@ export default function PatientFilterV2(props: any) { created_date_after, modified_date_before, modified_date_after, - ordering, category, gender, disease_status, @@ -298,7 +294,6 @@ export default function PatientFilterV2(props: any) { moment(last_consultation_discharge_date_after).isValid() ? moment(last_consultation_discharge_date_after).format("YYYY-MM-DD") : "", - ordering: ordering || "", category: category || "", gender: gender || "", disease_status: @@ -364,31 +359,7 @@ export default function PatientFilterV2(props: any) { closeFilter(); }} /> -
-
- -

Ordering

-
- o.desc} - optionSelectedLabel={(option) => `${option.desc} (${option.order})`} - optionDescription={(o) => o.order} - optionIcon={(option) => ( - - )} - value={filterState.ordering || undefined} - optionValue={(o) => o.text} - onChange={(v) => setFilterState({ ...filterState, ordering: v })} - /> -
-
+

Filter by

@@ -477,14 +448,14 @@ export default function PatientFilterV2(props: any) { } />
- {KASP_ENABLED && ( + {kasp_enabled && (
- {KASP_STRING} + {kasp_string} - o ? `Show ${KASP_STRING}` : `Show Non ${KASP_STRING}` + o ? `Show ${kasp_string}` : `Show Non ${kasp_string}` } value={filterState.is_kasp} onChange={(v) => setFilterState({ ...filterState, is_kasp: v })} diff --git a/src/Components/Patient/PatientInfoCard.tsx b/src/Components/Patient/PatientInfoCard.tsx index 590d06775ee..61f7ad6b4f9 100644 --- a/src/Components/Patient/PatientInfoCard.tsx +++ b/src/Components/Patient/PatientInfoCard.tsx @@ -9,6 +9,7 @@ import { PATIENT_CATEGORIES } from "../../Common/constants"; import moment from "moment"; import ButtonV2 from "../Common/components/ButtonV2"; import CareIcon from "../../CAREUI/icons/CareIcon"; +import * as Notification from "../../Utils/Notifications.js"; export default function PatientInfoCard(props: { patient: PatientModel; @@ -227,7 +228,19 @@ export default function PatientInfoCard(props: { { + if (!patient.last_consultation?.current_bed && i === 1) { + Notification.Error({ + msg: "Please assign a bed to the patient", + }); + setOpen(true); + } + }} align="start" className="w-full" > diff --git a/src/Components/Patient/PatientRegister.tsx b/src/Components/Patient/PatientRegister.tsx index 781d8df70ea..f0eea40c327 100644 --- a/src/Components/Patient/PatientRegister.tsx +++ b/src/Components/Patient/PatientRegister.tsx @@ -70,6 +70,7 @@ import TextAreaFormField from "../Form/FormFields/TextAreaFormField"; import { FieldLabel } from "../Form/FormFields/FormField"; import PhoneNumberFormField from "../Form/FormFields/PhoneNumberFormField"; import { FieldChangeEvent } from "../Form/FormFields/Utils"; +import useConfig from "../../Common/hooks/useConfig"; // const debounce = require("lodash.debounce"); interface PatientRegisterProps extends PatientModel { @@ -193,6 +194,7 @@ const scrollTo = (id: any) => { }; export const PatientRegister = (props: PatientRegisterProps) => { + const { gov_data_api_key } = useConfig(); const dispatchAction: any = useDispatch(); const { facilityId, id } = props; const [state, dispatch] = useReducer(patientFormReducer, initialState); @@ -686,7 +688,10 @@ export const PatientRegister = (props: PatientRegisterProps) => { if (!validatePincode(e.target.value)) return; - const pincodeDetails = await getPincodeDetails(e.target.value); + const pincodeDetails = await getPincodeDetails( + e.target.value, + gov_data_api_key + ); if (!pincodeDetails) return; const matchedState = states.find((state) => { @@ -1012,9 +1017,10 @@ export const PatientRegister = (props: PatientRegisterProps) => { { + onBackClick={() => { if (showImport) { setShowImport(false); + return false; } }} crumbsReplacements={{ diff --git a/src/Components/Patient/SampleDetails.tsx b/src/Components/Patient/SampleDetails.tsx index 42e1f0c08bd..ca4a4964ace 100644 --- a/src/Components/Patient/SampleDetails.tsx +++ b/src/Components/Patient/SampleDetails.tsx @@ -431,7 +431,7 @@ export const SampleDetails = (props: SampleDetailsProps) => { {yesornoBadge(sampleDetails.patient_has_suspected_contact)}
{sampleDetails.patient_travel_history && - sampleDetails.patient_travel_history !== "[]" && ( + sampleDetails.patient_travel_history.length !== 0 && (
Countries travelled:{" "} diff --git a/src/Components/Patient/SampleViewAdmin.tsx b/src/Components/Patient/SampleViewAdmin.tsx index 4cc4966ec63..7bf151446cd 100644 --- a/src/Components/Patient/SampleViewAdmin.tsx +++ b/src/Components/Patient/SampleViewAdmin.tsx @@ -147,6 +147,23 @@ export default function SampleViewAdmin() { }); }; + const parseExportData = (data: string) => + data + .trim() + .split("\n") + .map((row: string) => + row + .trim() + .split(",") + .map((field: string) => + new Date(field).toString() === "Invalid Date" + ? field + : formatDate(field, "DD/MM/YYYY hh:mm A") + ) + .join(",") + ) + .join("\n"); + let sampleList: any[] = []; if (sample && sample.length) { sampleList = sample.map((item) => { @@ -324,6 +341,7 @@ export default function SampleViewAdmin() { componentRight={ downloadSampleTests({ ...qParams })} + parse={parseExportData} filenamePrefix="samples" /> } diff --git a/src/Components/Resource/ResourceBoard.tsx b/src/Components/Resource/ResourceBoard.tsx index 07d5e0d48be..01dfb049c52 100644 --- a/src/Components/Resource/ResourceBoard.tsx +++ b/src/Components/Resource/ResourceBoard.tsx @@ -8,10 +8,11 @@ import CircularProgress from "@material-ui/core/CircularProgress"; import { navigate } from "raviger"; import moment from "moment"; import { CSVLink } from "react-csv"; -import GetAppIcon from "@material-ui/icons/GetApp"; import { classNames } from "../../Utils/utils"; import { useDrag, useDrop } from "react-dnd"; import { formatDate } from "../../Utils/utils"; +import ButtonV2 from "../Common/components/ButtonV2"; +import CareIcon from "../../CAREUI/icons/CareIcon"; const limit = 14; @@ -266,10 +267,18 @@ export default function ResourceBoard({ {downloadLoading ? ( ) : ( - + className="tooltip p-4" + variant="secondary" + ghost + circle + > + + + Download + + )} diff --git a/src/Components/Shifting/ListFilter.tsx b/src/Components/Shifting/ListFilter.tsx index 6b71325b0aa..fdd240ff4ee 100644 --- a/src/Components/Shifting/ListFilter.tsx +++ b/src/Components/Shifting/ListFilter.tsx @@ -5,9 +5,7 @@ import { SelectField } from "../Common/HelperInputFields"; import { SHIFTING_FILTER_ORDER, DISEASE_STATUS, - KASP_STRING, BREATHLESSNESS_LEVEL, - KASP_ENABLED, } from "../../Common/constants"; import moment from "moment"; import { getAnyFacility, getUserList } from "../../Redux/actions"; @@ -20,10 +18,12 @@ import parsePhoneNumberFromString from "libphonenumber-js"; import useMergeState from "../../Common/hooks/useMergeState"; import PhoneNumberFormField from "../Form/FormFields/PhoneNumberFormField"; import { FieldChangeEvent } from "../Form/FormFields/Utils"; +import useConfig from "../../Common/hooks/useConfig"; const shiftStatusOptions = SHIFTING_CHOICES.map((obj) => obj.text); export default function ListFilter(props: any) { + const { kasp_enabled, kasp_string } = useConfig(); const { filter, onChange, closeFilter } = props; const [isOriginLoading, setOriginLoading] = useState(false); const [isShiftingLoading, setShiftingLoading] = useState(false); @@ -374,9 +374,9 @@ export default function ListFilter(props: any) { />
- {KASP_ENABLED && ( + {kasp_enabled && (
- Is {KASP_STRING} + Is {kasp_string} import("../Common/Loading")); const PageTitle = loadable(() => import("../Common/PageTitle")); export default function ShiftDetails(props: { id: string }) { + const { static_header_logo, kasp_full_string } = useConfig(); const dispatch: any = useDispatch(); const initialData: any = {}; const [data, setData] = useState(initialData); @@ -387,11 +385,7 @@ export default function ShiftDetails(props: { id: string }) { return (
-
- {data.is_kasp && ( - logo - )} -
+
{data.is_kasp && logo}
@@ -538,7 +532,7 @@ export default function ShiftDetails(props: { id: string }) {
@@ -553,7 +547,7 @@ export default function ShiftDetails(props: { id: string }) { Auto Generated for Care
- {process.env.REACT_APP_DEPLOYED_URL}/shifting/{data.id} + {window.location.origin}/shifting/{data.id}
@@ -703,7 +697,7 @@ export default function ShiftDetails(props: { id: string }) {
- {KASP_FULL_STRING}:{" "} + {kasp_full_string}:{" "} {" "} diff --git a/src/Components/Shifting/ShiftDetailsUpdate.tsx b/src/Components/Shifting/ShiftDetailsUpdate.tsx index e917a3bbe03..42a7d9d785d 100644 --- a/src/Components/Shifting/ShiftDetailsUpdate.tsx +++ b/src/Components/Shifting/ShiftDetailsUpdate.tsx @@ -16,7 +16,6 @@ import { FACILITY_TYPES, SHIFTING_VEHICLE_CHOICES, BREATHLESSNESS_LEVEL, - KASP_FULL_STRING, } from "../../Common/constants"; import { UserSelect } from "../Common/UserSelect"; import { CircularProgress } from "@material-ui/core"; @@ -32,6 +31,7 @@ import { } from "@material-ui/core"; import { goBack } from "../../Utils/utils"; import { Cancel, Submit } from "../Common/components/ButtonV2"; +import useConfig from "../../Common/hooks/useConfig"; const Loading = loadable(() => import("../Common/Loading")); const PageTitle = loadable(() => import("../Common/PageTitle")); @@ -82,6 +82,7 @@ const initialState = { }; export const ShiftDetailsUpdate = (props: patientShiftProps) => { + const { kasp_full_string } = useConfig(); const dispatchAction: any = useDispatch(); const [qParams, _] = useQueryParams(); const [isLoading, setIsLoading] = useState(true); @@ -329,7 +330,7 @@ export const ShiftDetailsUpdate = (props: patientShiftProps) => {
- Is {KASP_FULL_STRING}? + Is {kasp_full_string}? ) : ( - + className="tooltip p-4" + variant="secondary" + ghost + circle + > + + + Download + + )} diff --git a/src/Components/Users/ManageUsers.tsx b/src/Components/Users/ManageUsers.tsx index 47340bac6ac..03b58a808b6 100644 --- a/src/Components/Users/ManageUsers.tsx +++ b/src/Components/Users/ManageUsers.tsx @@ -16,7 +16,7 @@ import { navigate } from "raviger"; import { USER_TYPES } from "../../Common/constants"; import { FacilityModel } from "../Facility/models"; -import { IconButton, CircularProgress } from "@material-ui/core"; +import { IconButton, CircularProgress, Button } from "@material-ui/core"; import CloseIcon from "@material-ui/icons/Close"; import LinkFacilityDialog from "./LinkFacilityDialog"; import UserDeleteDialog from "./UserDeleteDialog"; @@ -27,10 +27,12 @@ import UserDetails from "../Common/UserDetails"; import UnlinkFacilityDialog from "./UnlinkFacilityDialog"; import useWindowDimensions from "../../Common/hooks/useWindowDimensions"; import SearchInput from "../Form/SearchInput"; +import SlideOverCustom from "../../CAREUI/interactive/SlideOver"; import useFilters from "../../Common/hooks/useFilters"; import { classNames } from "../../Utils/utils"; import ButtonV2 from "../Common/components/ButtonV2"; import CareIcon from "../../CAREUI/icons/CareIcon"; +import { FacilitySelect } from "../Common/FacilitySelect"; const Loading = loadable(() => import("../Common/Loading")); const PageTitle = loadable(() => import("../Common/PageTitle")); @@ -53,6 +55,10 @@ export default function ManageUsers() { const [isFacilityLoading, setIsFacilityLoading] = useState(false); const [totalCount, setTotalCount] = useState(0); const [districtName, setDistrictName] = useState(); + + const [expandFacilityList, setExpandFacilityList] = useState(false); + const [facility, setFacility] = useState(null); + const state: any = useSelector((state) => state); const { currentUser } = state; const isSuperuser = currentUser.data.is_superuser; @@ -65,6 +71,7 @@ export default function ManageUsers() { show: boolean; username: string; }>({ show: false, username: "" }); + const [linkedFacilityLoading, setLinkedFacilityLoading] = useState(false); const [userData, setUserData] = useState<{ show: boolean; @@ -150,7 +157,9 @@ export default function ManageUsers() { if (isFacilityLoading) { return; } + setLinkedFacilityLoading(true); setIsFacilityLoading(true); + setExpandFacilityList(true); const res = await dispatch(getUserListFacility({ username })); if (res && res.data) { const updated = users.map((user) => { @@ -164,13 +173,7 @@ export default function ManageUsers() { setUsers(updated); } setIsFacilityLoading(false); - }; - - const showLinkFacilityModal = (username: string) => { - setLinkFacility({ - show: true, - username, - }); + setLinkedFacilityLoading(false); }; const hideUnlinkFacilityModal = () => { @@ -230,20 +233,28 @@ export default function ManageUsers() { }); }; - const facilityClassname = classNames( - "align-baseline font-bold text-sm", - isFacilityLoading ? "text-gray-500" : "text-blue-500 hover:text-blue-800" - ); - const showLinkFacility = (username: string) => { return ( - showLinkFacilityModal(username)} - className={facilityClassname} - href="#" - > - Link new facility - +
+ + +
); }; @@ -254,35 +265,45 @@ export default function ManageUsers() { fetchData({ aborted: false }); }; - const showFacilities = ( - username: string, - facilities: FacilityModel[], - district_name: string - ) => { + const showFacilities = (username: string, facilities: FacilityModel[]) => { if (!facilities || !facilities.length) { return ( <> -
No Facilities!
{showLinkFacility(username)} +
+
+ Error 404 +
+

+ Select and add some facilities +

+
); } return ( -
-
-
+ <> +
+ {showLinkFacility(username)} +
{facilities.map((facility, i) => (
-
-
{facility.name}
- updateHomeFacility(username, facility)} - > - {currentUser.data.district_object.name === district_name && ( +
+
{facility.name}
+
+ updateHomeFacility(username, facility)} + > - )} +
))}
- {showLinkFacility(username)} -
+ ); }; @@ -323,11 +343,7 @@ export default function ManageUsers() { hideLinkFacilityModal(); setIsFacilityLoading(true); const res = await dispatch(addUserFacility(username, String(facility.id))); - if (res?.status === 201) { - Notification.Success({ - msg: "Facility linked successfully", - }); - } else { + if (res?.status !== 201) { Notification.Error({ msg: "Error while linking facility", }); @@ -454,6 +470,54 @@ export default function ManageUsers() {
)} + {user.user_type === "Doctor" && ( + <> +
+ + {user.doctor_qualification ? ( + + {user.doctor_qualification} + + ) : ( + Unknown + )} + +
+
+ + {user.doctor_experience_commenced_on ? ( + + {moment().diff( + user.doctor_experience_commenced_on, + "years", + false + )}{" "} + years + + ) : ( + Unknown + )} + +
+
+ + {user.doctor_medical_council_registration ? ( + + {user.doctor_medical_council_registration} + + ) : ( + Unknown + )} + +
+ + )}
{user.local_body_object && ( @@ -495,6 +559,7 @@ export default function ManageUsers() { { @@ -509,21 +574,37 @@ export default function ManageUsers() { className={`${ !user.facilities ? "care-l-eye" - : "care-l-eye-slash" + : expandFacilityList + ? "care-l-eye-slash" + : "care-l-eye" } text-xl`} /> - {!user.facilities ? "View" : "Hide"} Linked - Facilities + {!user.facilities + ? "View" + : expandFacilityList + ? "Hide" + : "View"}{" "} + Linked Facilities
- {user.facilities && - showFacilities( - user.username, - user.facilities, - user.district_object.name - )} + {user.facilities && ( +
+ true} + > +
+ {showFacilities(user.username, user.facilities)} +
+
+
+ )}
)}
diff --git a/src/Components/Users/UserAdd.tsx b/src/Components/Users/UserAdd.tsx index 1285c34a2bd..967c684e4bb 100644 --- a/src/Components/Users/UserAdd.tsx +++ b/src/Components/Users/UserAdd.tsx @@ -27,16 +27,18 @@ import { } from "../../Redux/actions"; import * as Notification from "../../Utils/Notifications.js"; import { FacilitySelect } from "../Common/FacilitySelect"; -import { PhoneNumberField, ErrorHelperText } from "../Common/HelperInputFields"; import { FacilityModel } from "../Facility/models"; -import { classNames, goBack } from "../../Utils/utils"; + +import { classNames, getExperienceSuffix, goBack } from "../../Utils/utils"; import { Cancel, Submit } from "../Common/components/ButtonV2"; -import SelectMenuV2 from "../Form/SelectMenuV2"; -import { FieldChangeEvent } from "../Form/FormFields/Utils"; +import PhoneNumberFormField from "../Form/FormFields/PhoneNumberFormField"; import TextFormField from "../Form/FormFields/TextFormField"; -import DateFormField from "../Form/FormFields/DateFormField"; -import Checkbox from "../Common/components/CheckBox"; +import { FieldChangeEvent } from "../Form/FormFields/Utils"; import { SelectFormField } from "../Form/FormFields/SelectFormField"; +import MonthFormField from "../Form/FormFields/Month"; +import Checkbox from "../Common/components/CheckBox"; +import DateFormField from "../Form/FormFields/DateFormField"; +import { FieldLabel } from "../Form/FormFields/FormField"; const Loading = loadable(() => import("../Common/Loading")); const PageTitle = loadable(() => import("../Common/PageTitle")); @@ -68,6 +70,9 @@ type UserForm = { state: number; district: number; local_body: number; + doctor_qualification: string | undefined; + doctor_experience_commenced_on: string | undefined; + doctor_medical_council_registration: string | undefined; }; const initForm: UserForm = { @@ -88,6 +93,9 @@ const initForm: UserForm = { state: 0, district: 0, local_body: 0, + doctor_qualification: undefined, + doctor_experience_commenced_on: undefined, + doctor_medical_council_registration: undefined, }; const initError = Object.assign( @@ -199,15 +207,15 @@ export const UserAdd = (props: UserProps) => { const userTypes = isSuperuser ? [...USER_TYPE_OPTIONS] : userType === "StaffReadOnly" - ? readOnlyUsers.slice(0, 1) - : userType === "DistrictReadOnlyAdmin" - ? readOnlyUsers.slice(0, 2) - : userType === "StateReadOnlyAdmin" - ? readOnlyUsers.slice(0, 3) - : userType === "Pharmacist" - ? USER_TYPE_OPTIONS.slice(0, 1) - : // Exception to allow Staff to Create Doctors - defaultAllowedUserTypes; + ? readOnlyUsers.slice(0, 1) + : userType === "DistrictReadOnlyAdmin" + ? readOnlyUsers.slice(0, 2) + : userType === "StateReadOnlyAdmin" + ? readOnlyUsers.slice(0, 3) + : userType === "Pharmacist" + ? USER_TYPE_OPTIONS.slice(0, 1) + : // Exception to allow Staff to Create Doctors + defaultAllowedUserTypes; const headerText = !userId ? "Add User" : "Update User"; const buttonText = !userId ? "Save User" : "Update Details"; @@ -310,16 +318,6 @@ export const UserAdd = (props: UserProps) => { [dispatch] ); - const handleChange = (e: FieldChangeEvent) => { - dispatch({ - type: "set_form", - form: { - ...state.form, - [e.name]: e.name === "username" ? e.value.toLowerCase() : e.value, - }, - }); - }; - const handleDateChange = (e: FieldChangeEvent) => { if (moment(e.value).isValid()) { dispatch({ @@ -332,19 +330,22 @@ export const UserAdd = (props: UserProps) => { } }; - const handleValueChange = (value: any, name: string) => { + const handleFieldChange = (event: FieldChangeEvent) => { dispatch({ type: "set_form", form: { ...state.form, - [name]: value, + [event.name]: event.value, }, }); }; useAbortableEffect(() => { phoneIsWhatsApp && - handleValueChange(state.form.phone_number, "alt_phone_number"); + handleFieldChange({ + name: "alt_phone_number", + value: state.form.phone_number, + }); }, [phoneIsWhatsApp, state.form.phone_number]); const setFacility = (selected: FacilityModel | FacilityModel[] | null) => { @@ -379,6 +380,14 @@ export const UserAdd = (props: UserProps) => { invalidForm = true; } return; + case "doctor_qualification": + case "doctor_experience_commenced_on": + case "doctor_medical_council_registration": + if (state.form.user_type === "Doctor" && !state.form[field]) { + errors[field] = "Field is required"; + invalidForm = true; + } + return; case "first_name": case "last_name": if (!state.form[field]) { @@ -554,6 +563,12 @@ export const UserAdd = (props: UserProps) => { ) || "", date_of_birth: moment(state.form.date_of_birth).format("YYYY-MM-DD"), age: Number(moment().diff(state.form.date_of_birth, "years", false)), + doctor_qualification: state.form.doctor_qualification, + doctor_experience_commenced_on: moment( + state.form.doctor_experience_commenced_on + ).format("YYYY-MM-DD"), + doctor_medical_council_registration: + state.form.doctor_medical_council_registration, }; const res = await dispatchAction(addUser(data)); @@ -585,6 +600,16 @@ export const UserAdd = (props: UserProps) => { return ; } + const field = (name: string) => { + return { + id: name, + name, + onChange: handleFieldChange, + value: state.form[name], + error: state.errors[name], + }; + }; + return (
{ handleSubmit(e)}>
- + Facilities { showAll={false} />
+ option.role} + optionValue={(option) => option.role} + /> + + {state.form.user_type === "Doctor" && ( + <> + + + ( + + {getExperienceSuffix(date)} + + )} + /> + + + + )} + + option.name} + optionValue={(option) => option.id} + onChange={handleFieldChange} + /> +
- - o.id} - optionLabel={(o) => - o.role + ((o.readOnly && " (Read Only)") || "") - } - onChange={(e) => handleValueChange(e, "user_type")} - /> - -
-
- - o.name} - optionValue={(o) => o.id} - onChange={(e) => handleValueChange(e, "home_facility")} - /> - -
-
- - - handleValueChange(value, "phone_number") - } - errors={state.errors.phone_number} - onlyIndia={true} + label="Phone Number" + required + onlyIndia /> { - setPhoneIsWhatsApp(checked); - !checked && handleValueChange("+91", "alt_phone_number"); - }} + onCheck={setPhoneIsWhatsApp} label="Is the phone number a WhatsApp number?" />
+ + +
- - - handleValueChange(value, "alt_phone_number") - } - disabled={phoneIsWhatsApp} - errors={state.errors.alt_phone_number} - onlyIndia={true} - /> -
-
- { - handleChange(e); + handleFieldChange(e); setUsernameInput(e.value); }} onFocus={() => setUsernameInputInFocus(true)} onBlur={() => { setUsernameInputInFocus(false); }} - error={state.errors.username} /> {usernameInputInFocus && (
@@ -752,34 +779,25 @@ export const UserAdd = (props: UserProps) => {
)}
+ + +
- - -
-
- setPasswordInputInFocus(true)} onBlur={() => setPasswordInputInFocus(false)} /> @@ -805,18 +823,13 @@ export const UserAdd = (props: UserProps) => { )}
- setConfirmPasswordInputInFocus(true)} onBlur={() => setConfirmPasswordInputInFocus(false)} /> @@ -827,148 +840,87 @@ export const UserAdd = (props: UserProps) => { "Confirm password should match the entered password" )}
-
- - -
-
- - -
-
- - + + + o.text} + optionValue={(o) => o.text} + /> + + {isStateLoading ? ( + + ) : ( + o.name} + optionValue={(o) => o.id} + onChange={(e) => { + handleFieldChange(e); + if (e) fetchDistricts(e.value); + }} /> -
-
- + )} + + {isDistrictLoading ? ( + + ) : ( o.text} - optionValue={(o) => o.text} - onChange={(e) => handleValueChange(e.value, "gender")} + {...field("district")} + label="District" + required + placeholder="Choose District" + options={districts} + optionLabel={(o) => o.name} + optionValue={(o) => o.id} + onChange={(e) => { + handleFieldChange(e); + if (e) fetchLocalBody(e.value); + }} /> -
-
- - {isStateLoading ? ( - - ) : ( - <> - o.name} - optionValue={(o) => o.id} - value={state.form.state} - onChange={(e) => { - if (e) { - return [ - handleValueChange(e.value, "state"), - fetchDistricts(e.value), - ]; - } - }} - /> - - - )} -
-
- - {isDistrictLoading ? ( + )} + + {showLocalbody && + (isLocalbodyLoading ? ( ) : ( <> o.name} optionValue={(o) => o.id} - value={state.form.district} - onChange={(e) => { - if (e) { - return [ - handleValueChange(e.value, "district"), - fetchLocalBody(e.value), - ]; - } - }} /> - - )} -
- {showLocalbody && ( -
- - {isLocalbodyLoading ? ( - - ) : ( - <> - o.name} - optionValue={(o) => o.id} - value={state.form.local_body} - onChange={(e) => - handleValueChange(e.value, "local_body") - } - /> - - - )} -
- )} + ))}
goBack()} /> diff --git a/src/Components/Users/UserProfile.tsx b/src/Components/Users/UserProfile.tsx index f0aafd72c82..aed3a14d6ea 100644 --- a/src/Components/Users/UserProfile.tsx +++ b/src/Components/Users/UserProfile.tsx @@ -8,18 +8,19 @@ import { partialUpdateUser, updateUserPassword, } from "../../Redux/actions"; -import { ErrorHelperText } from "../Common/HelperInputFields"; import { parsePhoneNumberFromString } from "libphonenumber-js/max"; import { validateEmailAddress } from "../../Common/validation"; import * as Notification from "../../Utils/Notifications.js"; import LanguageSelector from "../../Components/Common/LanguageSelector"; -import SelectMenuV2 from "../Form/SelectMenuV2"; -import { FieldLabel } from "../Form/FormFields/FormField"; import TextFormField from "../Form/FormFields/TextFormField"; import ButtonV2, { Submit } from "../Common/components/ButtonV2"; -import { handleSignOut } from "../../Utils/utils"; +import { getExperienceSuffix, handleSignOut } from "../../Utils/utils"; import CareIcon from "../../CAREUI/icons/CareIcon"; import PhoneNumberFormField from "../Form/FormFields/PhoneNumberFormField"; +import { FieldChangeEvent } from "../Form/FormFields/Utils"; +import { SelectFormField } from "../Form/FormFields/SelectFormField"; +import MonthFormField from "../Form/FormFields/Month"; +import moment from "moment"; const Loading = loadable(() => import("../Common/Loading")); @@ -31,6 +32,9 @@ type EditForm = { email: string; phoneNumber: string; altPhoneNumber: string; + doctor_qualification: string | undefined; + doctor_experience_commenced_on: string | undefined; + doctor_medical_council_registration: string | undefined; }; type State = { form: EditForm; @@ -48,6 +52,9 @@ const initForm: EditForm = { email: "", phoneNumber: "", altPhoneNumber: "", + doctor_qualification: undefined, + doctor_experience_commenced_on: undefined, + doctor_medical_council_registration: undefined, }; const initError: EditForm = Object.assign( @@ -127,6 +134,11 @@ export default function UserProfile() { email: res.data.email, phoneNumber: res.data.phone_number, altPhoneNumber: res.data.alt_phone_number, + doctor_qualification: res.data.doctor_qualification, + doctor_experience_commenced_on: + res.data.doctor_experience_commenced_on, + doctor_medical_council_registration: + res.data.doctor_medical_council_registration, }; dispatch({ type: "set_form", @@ -216,15 +228,35 @@ export default function UserProfile() { invalidForm = true; } return; + case "doctor_qualification": + case "doctor_experience_commenced_on": + case "doctor_medical_council_registration": + if (details.user_type === "Doctor" && !states.form[field]) { + errors[field] = "Field is required"; + invalidForm = true; + } + return; } }); dispatch({ type: "set_error", errors }); return !invalidForm; }; - const handleValueChange = (value: any, name: string) => { - const form: EditForm = { ...states.form, [name]: value }; - dispatch({ type: "set_form", form }); + const handleFieldChange = (event: FieldChangeEvent) => { + dispatch({ + type: "set_form", + form: { ...states.form, [event.name]: event.value }, + }); + }; + + const fieldProps = (name: string) => { + return { + name, + id: name, + value: (states.form as any)[name], + onChange: handleFieldChange, + error: (states.errors as any)[name], + }; }; const handleSubmit = async (e: React.FormEvent) => { @@ -245,6 +277,12 @@ export default function UserProfile() { ) || "", gender: states.form.gender, age: states.form.age, + doctor_qualification: states.form.doctor_qualification, + doctor_experience_commenced_on: moment( + states.form.doctor_experience_commenced_on + ).format("YYYY-MM-DD"), + doctor_medical_council_registration: + states.form.doctor_medical_council_registration, }; const res = await dispatchAction(partialUpdateUser(username, data)); if (res && res.data) { @@ -438,103 +476,100 @@ export default function UserProfile() { )} {showEdit && ( -
+
-
+
-
- handleValueChange(e, "firstName")} - error={states.errors.firstName} - required - /> -
- -
- handleValueChange(e, "lastName")} - error={states.errors.lastName} - required - /> -
-
- handleValueChange(e, "age")} - error={states.errors.age} - required - /> -
- -
- Gender - o.text} - optionValue={(o) => o.text} - optionIcon={(o) => ( - {o.icon} - )} - value={states.form.gender} - options={GENDER_TYPES} - onChange={(v) => { - dispatch({ - type: "set_form", - form: { - ...states.form, - gender: v, - }, - }); - }} - /> - -
- -
- - handleValueChange(event.value, event.name) - } - error={states.errors.phoneNumber} - /> -
- -
- - handleValueChange(event.value, event.name) - } - error={states.errors.altPhoneNumber} - /> -
-
- handleValueChange(e, "email")} - error={states.errors.email} - /> -
+ + + + o.text} + optionValue={(o) => o.text} + optionIcon={(o) => ( + {o.icon} + )} + options={GENDER_TYPES} + /> + + + + {details.user_type === "Doctor" && ( + <> + + ( + + {getExperienceSuffix(date)} + + )} + /> + + + )}
@@ -546,53 +581,50 @@ export default function UserProfile() {
-
- - setChangePasswordForm({ - ...changePasswordForm, - old_password: e.value, - }) - } - error={changePasswordErrors.old_password} - required - /> -
-
- - setChangePasswordForm({ - ...changePasswordForm, - new_password_1: e.value, - }) - } - error="" - required - /> -
-
- - setChangePasswordForm({ - ...changePasswordForm, - new_password_2: e.value, - }) - } - error={changePasswordErrors.password_confirmation} - /> -
+ + setChangePasswordForm({ + ...changePasswordForm, + old_password: e.value, + }) + } + error={changePasswordErrors.old_password} + required + /> + + setChangePasswordForm({ + ...changePasswordForm, + new_password_1: e.value, + }) + } + error="" + required + /> + + setChangePasswordForm({ + ...changePasswordForm, + new_password_2: e.value, + }) + } + error={changePasswordErrors.password_confirmation} + />
diff --git a/src/Components/Users/models.tsx b/src/Components/Users/models.tsx index 3396c84be7b..2d2acbfa847 100644 --- a/src/Components/Users/models.tsx +++ b/src/Components/Users/models.tsx @@ -20,6 +20,9 @@ export interface UserModel { verified?: boolean; last_login?: Date; home_facility_object?: HomeFacilityObjectModel; + doctor_qualification?: string; + doctor_experience_commenced_on?: string; + doctor_medical_council_registration?: string; } export interface UserAssignedModel { @@ -40,4 +43,7 @@ export interface UserAssignedModel { verified?: boolean; last_login?: Date; home_facility_object?: HomeFacilityObjectModel; + doctor_qualification?: string; + doctor_experience_commenced_on?: Date; + doctor_medical_council_registration?: string; } diff --git a/src/Locale/en/Common.json b/src/Locale/en/Common.json index f88d503ec29..024ba3cfe85 100644 --- a/src/Locale/en/Common.json +++ b/src/Locale/en/Common.json @@ -17,6 +17,8 @@ "Notifications": "Notifications", "Submit": "Submit", "Cancel": "Cancel", - "powered_by":"Powered By", - "care":"CARE" -} + "Apply": "Apply", + "Clear Filters": "Clear Filters", + "powered_by": "Powered By", + "care": "CARE" +} \ No newline at end of file diff --git a/src/Router/AppRouter.tsx b/src/Router/AppRouter.tsx index 9641b6db674..3284243916a 100644 --- a/src/Router/AppRouter.tsx +++ b/src/Router/AppRouter.tsx @@ -66,8 +66,7 @@ import { } from "../Components/Common/Sidebar/Sidebar"; import { BLACKLISTED_PATHS } from "../Common/constants"; import { UpdateFacilityMiddleware } from "../Components/Facility/UpdateFacilityMiddleware"; - -const logoBlack = process.env.REACT_APP_BLACK_LOGO; +import useConfig from "../Common/hooks/useConfig"; const routes = { "/hub": () => , @@ -379,6 +378,7 @@ const routes = { }; export default function AppRouter() { + const { static_black_logo } = useConfig(); useRedirect("/", "/facility"); useRedirect("/user", "/users"); const pages = useRoutes(routes) || ; @@ -435,7 +435,11 @@ export default function AppRouter() { href="/" className="md:hidden flex h-full w-full items-center px-4" > - care logo + care logo
diff --git a/src/Utils/utils.ts b/src/Utils/utils.ts index a95a87173cc..3519ebe269a 100644 --- a/src/Utils/utils.ts +++ b/src/Utils/utils.ts @@ -1,6 +1,5 @@ import moment from "moment"; import { navigate } from "raviger"; -import { GOV_DATA_API_KEY } from "../Common/env"; interface ApacheParams { age: number; @@ -67,6 +66,9 @@ export const calculateApache2Score = (apacheParams: ApacheParams): number => { return totalScore; }; +/** + * Deprecated. Use `goBack` from the `useAppHistory` hook instead. + */ export const goBack = (deltaOrUrl?: string | number | false | void) => { if (typeof deltaOrUrl === "number") { window.history.go(-deltaOrUrl); @@ -81,8 +83,11 @@ export const goBack = (deltaOrUrl?: string | number | false | void) => { window.history.back(); }; -export const formatDate = (date: string | Date) => { - return moment(date).format("hh:mm A; DD/MM/YYYY"); +export const formatDate = ( + date: string | Date, + format = "hh:mm A; DD/MM/YYYY" +) => { + return moment(date).format(format); }; export const relativeDate = (date: string | Date) => { @@ -190,9 +195,9 @@ export const parseCsvFile = async ( return parsed; }; -export const getPincodeDetails = async (pincode: string) => { +export const getPincodeDetails = async (pincode: string, apiKey: string) => { const response = await fetch( - `https://api.data.gov.in/resource/5c2f62fe-5afa-4119-a499-fec9d604d5bd?api-key=${GOV_DATA_API_KEY}&format=json&filters[pincode]=${pincode}&limit=1` + `https://api.data.gov.in/resource/5c2f62fe-5afa-4119-a499-fec9d604d5bd?api-key=${apiKey}&format=json&filters[pincode]=${pincode}&limit=1` ); const data = await response.json(); return data.records[0]; @@ -206,3 +211,23 @@ export const includesIgnoreCase = (str1: string, str2: string) => { lowerCaseStr2.includes(lowerCaseStr1) ); }; + +export const getExperienceSuffix = (date?: Date) => { + if (!date) return "0 Years"; + + const today = new Date(); + + let m = (today.getFullYear() - date.getFullYear()) * 12; + m -= date.getMonth(); + m += today.getMonth(); + + let str = ""; + + const years = Math.floor(m / 12); + const months = m % 12; + + if (years) str += `${years} years `; + if (months) str += `${months} months`; + + return str; +}; diff --git a/src/style/CAREUI.css b/src/style/CAREUI.css index dd64c5b95b5..37a0b1665af 100644 --- a/src/style/CAREUI.css +++ b/src/style/CAREUI.css @@ -102,7 +102,7 @@ .button-primary-ghost { @apply accent-primary-500 hover:bg-primary-100 text-primary-500 !important } .button-primary-border { @apply border border-primary-500 } -.button-secondary-default { @apply accent-secondary-200 bg-secondary-300 hover:bg-secondary-200 text-secondary-800 !important } +.button-secondary-default { @apply accent-secondary-200 bg-white hover:bg-secondary-200 text-secondary-800 !important } .button-secondary-ghost { @apply accent-secondary-200 hover:bg-secondary-100 text-secondary-700 !important } .button-secondary-border { @apply border border-secondary-300 }