diff --git a/cypress/e2e/facility_spec/locations.cy.ts b/cypress/e2e/facility_spec/locations.cy.ts index d5b3cc9990f..cac9adbdde1 100644 --- a/cypress/e2e/facility_spec/locations.cy.ts +++ b/cypress/e2e/facility_spec/locations.cy.ts @@ -13,6 +13,7 @@ describe("Location Management Section", () => { cy.intercept("GET", "**/api/v1/facility/**").as("getFacilities"); cy.get("[id='facility-details']").first().click(); cy.wait("@getFacilities").its("response.statusCode").should("eq", 200); + cy.get("h1.text-3xl.font-bold", { timeout: 10000 }).should("be.visible"); cy.get("#manage-facility-dropdown button").should("be.visible"); cy.get("[id='manage-facility-dropdown']").scrollIntoView().click(); cy.get("[id=location-management]").click(); diff --git a/cypress/e2e/patient_spec/patient_crud.cy.ts b/cypress/e2e/patient_spec/patient_crud.cy.ts index b22ccdbce13..fcfd97039f7 100644 --- a/cypress/e2e/patient_spec/patient_crud.cy.ts +++ b/cypress/e2e/patient_spec/patient_crud.cy.ts @@ -115,6 +115,7 @@ describe("Patient Creation with consultation", () => { patientConsultationPage.selectSymptoms("ASYMPTOMATIC"); patientConsultationPage.enterConsultationDetails( + "Stable", "Examination details and Clinical conditions", "70", "170", diff --git a/cypress/fixtures/sampleAsset.xlsx b/cypress/fixtures/sampleAsset.xlsx index f8e234ce447..49421f62bac 100644 Binary files a/cypress/fixtures/sampleAsset.xlsx and b/cypress/fixtures/sampleAsset.xlsx differ diff --git a/cypress/pageobject/Facility/FacilityCreation.ts b/cypress/pageobject/Facility/FacilityCreation.ts index 42ec6d8fd7c..e0a572ccaec 100644 --- a/cypress/pageobject/Facility/FacilityCreation.ts +++ b/cypress/pageobject/Facility/FacilityCreation.ts @@ -106,6 +106,7 @@ class FacilityPage { } clickManageFacilityDropdown() { + cy.get("h1.text-3xl.font-bold", { timeout: 20000 }).should("be.visible"); cy.get("#manage-facility-dropdown button").scrollIntoView(); cy.get("#manage-facility-dropdown button") .contains("Manage Facility") @@ -198,8 +199,6 @@ class FacilityPage { cy.intercept("GET", "**/api/v1/facility/**").as("getManagePage"); cy.go("back"); cy.wait("@getManagePage").its("response.statusCode").should("eq", 200); - cy.get("#manage-facility-dropdown").scrollIntoView(); - cy.get("#manage-facility-dropdown").should("exist"); } verifyfacilityviewassetredirection() { diff --git a/cypress/pageobject/Patient/PatientConsultation.ts b/cypress/pageobject/Patient/PatientConsultation.ts index b1c95d19140..6937559d548 100644 --- a/cypress/pageobject/Patient/PatientConsultation.ts +++ b/cypress/pageobject/Patient/PatientConsultation.ts @@ -23,6 +23,7 @@ export class PatientConsultationPage { } enterConsultationDetails( + category: string, examinationDetails: string, weight: string, height: string, @@ -31,6 +32,11 @@ export class PatientConsultationPage { verificationBy: string ) { cy.get("#symptoms").click(); + cy.get("#category") + .click() + .then(() => { + cy.get("[role='option']").contains(category).click(); + }); cy.get("#examination_details").click().type(examinationDetails); cy.get("#weight").click().type(height); cy.get("#height").click().type(weight); @@ -39,10 +45,12 @@ export class PatientConsultationPage { cy.get( "#icd11_diagnoses_object input[placeholder='Select'][role='combobox']" ) + .scrollIntoView() .click() .type("1A"); cy.get("#icd11_diagnoses_object [role='option']") .contains("1A03 Intestinal infections due to Escherichia coli") + .scrollIntoView() .click(); cy.get("label[for='icd11_diagnoses_object']").click(); cy.wait("@getIcdResults").its("response.statusCode").should("eq", 200); diff --git a/package-lock.json b/package-lock.json index 4871fbe374b..2a9fe3f520f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14496,8 +14496,6 @@ }, "node_modules/npm/node_modules/cross-spawn/node_modules/which": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "inBundle": true, "license": "ISC", @@ -15254,8 +15252,6 @@ }, "node_modules/npm/node_modules/minipass": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", "dev": true, "inBundle": true, "license": "ISC", @@ -16398,8 +16394,6 @@ }, "node_modules/npm/node_modules/tar": { "version": "6.1.15", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.15.tgz", - "integrity": "sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A==", "dev": true, "inBundle": true, "license": "ISC", @@ -16500,8 +16494,6 @@ }, "node_modules/npm/node_modules/util-deprecate": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true, "inBundle": true, "license": "MIT" @@ -16569,8 +16561,6 @@ }, "node_modules/npm/node_modules/wrap-ansi": { "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, "inBundle": true, "license": "MIT", @@ -16589,8 +16579,6 @@ "node_modules/npm/node_modules/wrap-ansi-cjs": { "name": "wrap-ansi", "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "inBundle": true, "license": "MIT", @@ -16670,8 +16658,6 @@ }, "node_modules/npm/node_modules/wrappy": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true, "inBundle": true, "license": "ISC" @@ -16691,8 +16677,6 @@ }, "node_modules/npm/node_modules/yallist": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true, "inBundle": true, "license": "ISC" diff --git a/src/CAREUI/display/RecordMeta.tsx b/src/CAREUI/display/RecordMeta.tsx index 83928f1f23a..48cc8d370ad 100644 --- a/src/CAREUI/display/RecordMeta.tsx +++ b/src/CAREUI/display/RecordMeta.tsx @@ -11,13 +11,14 @@ interface Props { last_name: string; last_login: string | undefined; }; + inlineUser?: boolean; } /** * A generic component to display relative time along with a tooltip and a user * if provided. */ -const RecordMeta = ({ time, user, prefix, className }: Props) => { +const RecordMeta = ({ time, user, prefix, className, inlineUser }: Props) => { const isOnline = user && isUserOnline(user); let child = ( @@ -25,14 +26,15 @@ const RecordMeta = ({ time, user, prefix, className }: Props) => { {relativeTime(time)} {formatDateTime(time)} - {user && ( - <> + {user && !inlineUser && ( + + by {user.first_name} {user.last_name} {isOnline && ( -
+
)} - + )}
@@ -43,7 +45,13 @@ const RecordMeta = ({ time, user, prefix, className }: Props) => {
{prefix} {child} + {user && inlineUser && by} {user && } + {user && inlineUser && ( + + {user.first_name} {user.last_name} + + )}
); } diff --git a/src/Common/constants.tsx b/src/Common/constants.tsx index f64a9fa27ae..e51494f6bac 100644 --- a/src/Common/constants.tsx +++ b/src/Common/constants.tsx @@ -896,7 +896,7 @@ export const XLSXAssetImportSchema = { Class: { prop: "asset_class", type: String, - oneOf: ["HL7MONITOR", "ONVIF"], + oneOf: ["HL7MONITOR", "ONVIF", "VENTILATOR", ""], }, Description: { prop: "description", type: String }, "Working Status": { @@ -908,7 +908,7 @@ export const XLSXAssetImportSchema = { } else if (status === "NOT WORKING") { return false; } else { - throw new Error("Invalid Working Status"); + throw new Error("Invalid Working Status: " + status); } }, required: true, @@ -917,6 +917,7 @@ export const XLSXAssetImportSchema = { prop: "not_working_reason", type: String, }, + "Serial Number": { prop: "serial_number", type: String }, "QR Code ID": { prop: "qr_code_id", type: String }, Manufacturer: { prop: "manufacturer", type: String }, "Vendor Name": { prop: "vendor_name", type: String }, @@ -925,10 +926,11 @@ export const XLSXAssetImportSchema = { prop: "support_email", type: String, parse: (email: string) => { + if (!email) return null; const isValid = /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/.test(email); if (!isValid) { - throw new Error("Invalid Support Email"); + throw new Error("Invalid Support Email: " + email); } return email; @@ -938,23 +940,38 @@ export const XLSXAssetImportSchema = { prop: "support_phone", type: String, parse: (phone: number | string) => { - phone = "+91" + String(phone); - if (!PhoneNumberValidator(["support"])(phone) === undefined) { - throw new Error("Invalid Support Phone Number"); + phone = String(phone); + if (phone.length === 10 && !phone.startsWith("1800")) { + phone = "+91" + phone; + } + if (phone.startsWith("91") && phone.length === 12) { + phone = "+" + phone; + } + if (phone.startsWith("+911800")) { + phone = "1800" + phone.slice(6); + } + if ( + PhoneNumberValidator(["mobile", "landline", "support"])(phone) !== + undefined + ) { + throw new Error("Invalid Support Phone Number: " + phone); } return phone ? phone : undefined; }, required: true, }, - "Warrenty End Date": { + "Warranty End Date": { prop: "warranty_amc_end_of_validity", type: String, parse: (date: string) => { - const parsed = new Date(date); + if (!date) return null; + const parts = date.split("-"); + const reformattedDateStr = `${parts[2]}-${parts[1]}-${parts[0]}`; + const parsed = new Date(reformattedDateStr); if (String(parsed) === "Invalid Date") { - throw new Error("Invalid Warrenty End Date"); + throw new Error("Invalid Warranty End Date:" + date); } return dateQueryString(parsed); @@ -964,10 +981,13 @@ export const XLSXAssetImportSchema = { prop: "last_serviced_on", type: String, parse: (date: string) => { - const parsed = new Date(date); + if (!date) return null; + const parts = date.split("-"); + const reformattedDateStr = `${parts[2]}-${parts[1]}-${parts[0]}`; + const parsed = new Date(reformattedDateStr); if (String(parsed) === "Invalid Date") { - throw new Error("Invalid Last Service Date"); + throw new Error("Invalid Last Service Date:" + date); } return dateQueryString(parsed); @@ -981,13 +1001,14 @@ export const XLSXAssetImportSchema = { prop: "local_ip_address", type: String, parse: (ip: string) => { + if (!ip) return null; const isValid = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test( ip ); if (!isValid) { - throw new Error("Invalid Config IP Address"); + throw new Error("Invalid Config IP Address: " + ip); } return ip; diff --git a/src/Common/hooks/useRangePagination.ts b/src/Common/hooks/useRangePagination.ts index e6bbe9f573e..7652ae546c1 100644 --- a/src/Common/hooks/useRangePagination.ts +++ b/src/Common/hooks/useRangePagination.ts @@ -9,18 +9,17 @@ interface Props { bounds: DateRange; perPage: number; slots?: number; - snapToLatest?: boolean; - reverse?: boolean; + defaultEnd?: boolean; } const useRangePagination = ({ bounds, perPage, ...props }: Props) => { const [currentRange, setCurrentRange] = useState( - getInitialBounds(bounds, perPage, props.snapToLatest) + getInitialBounds(bounds, perPage, props.defaultEnd) ); useEffect(() => { - setCurrentRange(getInitialBounds(bounds, perPage, props.snapToLatest)); - }, [bounds, perPage, props.snapToLatest]); + setCurrentRange(getInitialBounds(bounds, perPage, props.defaultEnd)); + }, [bounds, perPage, props.defaultEnd]); const next = () => { const { end } = currentRange; @@ -63,24 +62,17 @@ const useRangePagination = ({ bounds, perPage, ...props }: Props) => { } const slots: DateRange[] = []; - const { start, end } = currentRange; + const { start } = currentRange; const delta = perPage / props.slots; for (let i = 0; i < props.slots; i++) { - if (props.snapToLatest) { - slots.push({ - start: new Date(end.valueOf() - delta * (i - 1)), - end: new Date(end.valueOf() - delta * i), - }); - } else { - slots.push({ - start: new Date(start.valueOf() + delta * i), - end: new Date(start.valueOf() + delta * (i + 1)), - }); - } + slots.push({ + start: new Date(start.valueOf() + delta * i), + end: new Date(start.valueOf() + delta * (i + 1)), + }); } - return props.reverse ? slots.reverse() : slots; + return slots; }, [currentRange, props.slots, perPage]); return { @@ -98,7 +90,7 @@ export default useRangePagination; const getInitialBounds = ( bounds: DateRange, perPage: number, - snapToLatest?: boolean + defaultEnd?: boolean ) => { const deltaBounds = bounds.end.valueOf() - bounds.start.valueOf(); @@ -106,7 +98,7 @@ const getInitialBounds = ( return bounds; } - if (snapToLatest) { + if (defaultEnd) { return { start: new Date(bounds.end.valueOf() - perPage), end: bounds.end, diff --git a/src/Common/hooks/useSlug.ts b/src/Common/hooks/useSlug.ts new file mode 100644 index 00000000000..69d8f591c84 --- /dev/null +++ b/src/Common/hooks/useSlug.ts @@ -0,0 +1,45 @@ +import { usePath } from "raviger"; + +/** + * Returns the slug from the current path. + * @param prefix The prefix of the slug. + * @returns The slug. + * @example + * // Current path: /consultation/94b9a + * const consultation = useSlug("consultation"); // consultation = "94b9a" + */ +export default function useSlug(prefix: string) { + const path = usePath() ?? ""; + return findSlug(path.split("/"), prefix); +} + +/** + * Returns the slugs from the current path. + * @param prefix The prefixes of the slug. + * @returns The slugs + * @example + * // Current path: /facility/5b0a/consultation/94b9a + * const [facility, consultation] = useSlug("facility", "consultation"); + * // facility = "5b0a" + * // consultation = "94b9a" + */ +export const useSlugs = (...prefix: string[]) => { + const path = usePath() ?? ""; + return prefix.map((p) => findSlug(path.split("/"), p)); +}; + +const findSlug = (segments: string[], prefix: string) => { + const index = segments.findIndex((segment) => segment === prefix); + if (index === -1) { + throw new Error( + `Prefix "${prefix}" not found in path "${segments.join("/")}"` + ); + } + + const slug = segments[index + 1]; + if (!slug) { + throw new Error(`Slug not found in path "${segments.join("/")}"`); + } + + return slug; +}; diff --git a/src/Components/Assets/AssetImportModal.tsx b/src/Components/Assets/AssetImportModal.tsx index 548df2901ec..7382c7eff4f 100644 --- a/src/Components/Assets/AssetImportModal.tsx +++ b/src/Components/Assets/AssetImportModal.tsx @@ -85,11 +85,11 @@ const AssetImportModal = ({ open, onClose, facility }: Props) => { } } } - } catch (e) { + } catch (e: any) { setPreview(undefined); console.log(e); Notification.Error({ - msg: "Invalid file", + msg: "Invalid file: " + e.message, }); setSelectedFile(undefined); } @@ -113,7 +113,7 @@ const AssetImportModal = ({ open, onClose, facility }: Props) => { let error = false; for (const asset of preview || []) { - const asset_data = JSON.stringify({ + const asset_data: any = { name: asset.name, asset_type: asset.asset_type, asset_class: asset.asset_class, @@ -129,11 +129,15 @@ const AssetImportModal = ({ open, onClose, facility }: Props) => { qr_code_id: asset.qr_code_id, manufacturer: asset.manufacturer, meta: { ...asset.meta }, - warranty_amc_end_of_validity: asset.warranty_amc_end_of_validity, - last_serviced_on: asset.last_serviced_on, note: asset.notes, - cancelToken: { promise: {} }, - }); + }; + + if (asset.last_serviced_on) + asset_data["last_serviced_on"] = asset.last_serviced_on; + + if (asset.warranty_amc_end_of_validity) + asset_data["warranty_amc_end_of_validity"] = + asset.warranty_amc_end_of_validity; const response = await fetch("/api/v1/asset/", { method: "POST", @@ -142,7 +146,7 @@ const AssetImportModal = ({ open, onClose, facility }: Props) => { Authorization: "Bearer " + localStorage.getItem(LocalStorageKeys.accessToken), }, - body: asset_data, + body: JSON.stringify(asset_data), }); const data = await response.json(); if (response.status !== 201) { diff --git a/src/Components/Assets/AssetServiceEditModal.tsx b/src/Components/Assets/AssetServiceEditModal.tsx index 66d44d11907..690524e471b 100644 --- a/src/Components/Assets/AssetServiceEditModal.tsx +++ b/src/Components/Assets/AssetServiceEditModal.tsx @@ -7,10 +7,10 @@ import DialogModal from "../Common/Dialog"; import { AssetData, AssetService, AssetServiceEdit } from "./AssetTypes"; import dayjs from "dayjs"; import TextAreaFormField from "../Form/FormFields/TextAreaFormField"; -import DateInputV2 from "../Common/DateInputV2"; -import { FieldLabel } from "../Form/FormFields/FormField"; import { formatDate, formatDateTime } from "../../Utils/utils"; import CareIcon from "../../CAREUI/icons/CareIcon"; +import DateFormField from "../Form/FormFields/DateFormField"; +import { t } from "i18next"; export const AssetServiceEditModal = (props: { asset?: AssetData; @@ -61,12 +61,12 @@ export const AssetServiceEditModal = (props: {

- Update record for asset + {t("update_record_for_asset")} {props.asset?.name}

@@ -110,13 +110,17 @@ export const AssetServiceEditModal = (props: {
-

Edited On

+

+ {t("edited_on")} +

{formatDateTime(editRecord.edited_on)}

-

Edited By

+

+ {t("edited_by")} +

{editRecord.edited_by.username}

@@ -125,7 +129,7 @@ export const AssetServiceEditModal = (props: {

- Serviced On + {t("serviced_on")}

-

Notes

+

+ {t("notes")} +

{editRecord.note || "-"}

@@ -151,7 +157,7 @@ export const AssetServiceEditModal = (props: { editRecord ? setEditRecord(undefined) : props.handleClose(); }} > - {editRecord ? "Back" : "Close"} + {editRecord ? t("back") : t("close")}
@@ -163,12 +169,12 @@ export const AssetServiceEditModal = (props: {

- Update record for asset + {t("update_record_for_asset")} {props.asset?.name}

@@ -178,19 +184,31 @@ export const AssetServiceEditModal = (props: { className="col-span-6 sm:col-span-3" data-testid="asset-last-serviced-on-input" > - Serviced On - { - setForm({ - ...form, - serviced_on: dayjs(date).format("YYYY-MM-DD"), - }); + if ( + dayjs(date.value).format("YYYY-MM-DD") > + new Date( + props.service_record.created_date + ).toLocaleDateString("en-ca") + ) { + Notification.Error({ + msg: `Service date can't be after ${formatDate( + props.service_record.created_date + )} (Creation date)`, + }); + } else { + setForm({ + ...form, + serviced_on: dayjs(date.value).format("YYYY-MM-DD"), + }); + } }} - max={new Date(props.service_record.created_date)} />
@@ -198,8 +216,8 @@ export const AssetServiceEditModal = (props: { { setForm({ ...form, note: e.value }); @@ -210,7 +228,7 @@ export const AssetServiceEditModal = (props: {
diff --git a/src/Components/Auth/Login.tsx b/src/Components/Auth/Login.tsx index cb7ea7f08f8..dd6a42d3983 100644 --- a/src/Components/Auth/Login.tsx +++ b/src/Components/Auth/Login.tsx @@ -1,10 +1,9 @@ import { useEffect, useState } from "react"; -import { useDispatch } from "react-redux"; -import { postForgotPassword, postLogin } from "../../Redux/actions"; +import request from "../../Utils/request/request"; +import routes from "../../Redux/api"; import { useTranslation } from "react-i18next"; import ReCaptcha from "react-google-recaptcha"; 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"; @@ -25,7 +24,6 @@ export const Login = (props: { forgot?: boolean }) => { custom_logo_alt, custom_description, } = useConfig(); - const dispatch: any = useDispatch(); const initForm: any = { username: "", password: "", @@ -90,37 +88,35 @@ export const Login = (props: { forgot?: boolean }) => { }; }, []); - const handleSubmit = (e: any) => { + const handleSubmit = async (e: any) => { e.preventDefault(); const valid = validateData(); if (valid) { // replaces button with spinner setLoading(true); - dispatch(postLogin(valid)).then((resp: any) => { - const res = get(resp, "data", null); - const statusCode = get(resp, "status", ""); - if (res && statusCode === 429) { - setCaptcha(true); - // captcha displayed set back to login button - setLoading(false); - } else if (res && statusCode === 200) { - localStorage.setItem(LocalStorageKeys.accessToken, res.access); - localStorage.setItem(LocalStorageKeys.refreshToken, res.refresh); - - if ( - window.location.pathname === "/" || - window.location.pathname === "/login" - ) { - window.location.href = "/facility"; - } else { - window.location.href = window.location.pathname.toString(); - } + const { res, data } = await request(routes.login, { + body: { ...valid }, + }); + if (res && res.status === 429) { + setCaptcha(true); + // captcha displayed set back to login button + setLoading(false); + } else if (res && res.status === 200 && data) { + localStorage.setItem(LocalStorageKeys.accessToken, data.access); + localStorage.setItem(LocalStorageKeys.refreshToken, data.refresh); + if ( + window.location.pathname === "/" || + window.location.pathname === "/login" + ) { + window.location.href = "/facility"; } else { - // error from server set back to login button - setLoading(false); + window.location.href = window.location.pathname.toString(); } - }); + } else { + // error from server set back to login button + setLoading(false); + } } }; @@ -146,26 +142,22 @@ export const Login = (props: { forgot?: boolean }) => { return form; }; - const handleForgetSubmit = (e: any) => { + const handleForgetSubmit = async (e: any) => { e.preventDefault(); const valid = validateForgetData(); if (valid) { setLoading(true); - dispatch(postForgotPassword(valid)).then((resp: any) => { - setLoading(false); - const res = resp && resp.data; - if (res && res.status === "OK") { - Notification.Success({ - msg: t("password_sent"), - }); - } else if (res && res.data) { - setErrors(res.data); - } else { - Notification.Error({ - msg: t("something_wrong"), - }); - } + const { res, error } = await request(routes.forgotPassword, { + body: { ...valid }, }); + setLoading(false); + if (res && res.statusText === "OK") { + Notification.Success({ + msg: t("password_sent"), + }); + } else if (res && error) { + setErrors(error); + } } }; diff --git a/src/Components/Auth/ResetPassword.tsx b/src/Components/Auth/ResetPassword.tsx index 08dd1da44a0..2f02737f6de 100644 --- a/src/Components/Auth/ResetPassword.tsx +++ b/src/Components/Auth/ResetPassword.tsx @@ -1,7 +1,6 @@ import { useEffect, useState } from "react"; -import { useDispatch } from "react-redux"; +import request from "../../Utils/request/request.js"; import * as Notification from "../../Utils/Notifications.js"; -import { postResetPassword, checkResetToken } from "../../Redux/actions"; import { navigate } from "raviger"; import { useTranslation } from "react-i18next"; import { LocalStorageKeys } from "../../Common/constants"; @@ -9,9 +8,9 @@ import { Cancel, Submit } from "../Common/components/ButtonV2"; import TextFormField from "../Form/FormFields/TextFormField"; import { validateRule } from "../Users/UserAdd"; import { validatePassword } from "../../Common/validation.js"; +import routes from "../../Redux/api.js"; export const ResetPassword = (props: any) => { - const dispatch: any = useDispatch(); const initForm: any = { password: "", confirm: "", @@ -65,36 +64,37 @@ export const ResetPassword = (props: any) => { return form; }; - const handleSubmit = (e: any) => { + const handleSubmit = async (e: any) => { e.preventDefault(); const valid = validateData(); if (valid) { valid.token = props.token; - dispatch(postResetPassword(valid)).then((resp: any) => { - const res = resp && resp.data; - if (res && res.status === "OK") { - localStorage.removeItem(LocalStorageKeys.accessToken); - Notification.Success({ - msg: t("password_reset_success"), - }); - navigate("/login"); - } else if (res && res.data) { - setErrors(res.data); - } else { - Notification.Error({ - msg: t("password_reset_failure"), - }); - } + const { res, error } = await request(routes.resetPassword, { + body: { ...valid }, }); + if (res && res.statusText === "OK") { + localStorage.removeItem(LocalStorageKeys.accessToken); + Notification.Success({ + msg: t("password_reset_success"), + }); + navigate("/login"); + } else if (res && error) { + setErrors(error); + } } }; useEffect(() => { - if (props.token) { - dispatch(checkResetToken({ token: props.token })).then((resp: any) => { - const res = resp && resp.data; - if (!res || res.status !== "OK") navigate("/invalid-reset"); + const checkResetToken = async () => { + const { res } = await request(routes.checkResetToken, { + body: { token: props.token }, }); + if (!res || res.statusText !== "OK") { + navigate("/invalid-reset"); + } + }; + if (props.token) { + checkResetToken(); } else { navigate("/invalid-reset"); } diff --git a/src/Components/Common/DateInputV2.tsx b/src/Components/Common/DateInputV2.tsx index bcebd4e0055..7036d5c8bfb 100644 --- a/src/Components/Common/DateInputV2.tsx +++ b/src/Components/Common/DateInputV2.tsx @@ -1,4 +1,4 @@ -import { MutableRefObject, useEffect, useRef, useState } from "react"; +import { MutableRefObject, useEffect, useState } from "react"; import { addMonths, addYears, @@ -60,7 +60,6 @@ const DateInputV2: React.FC = ({ const [displayValue, setDisplayValue] = useState( value ? dayjs(value).format("DDMMYYYY") : "" ); - const popover = useRef(null); const decrement = () => { switch (type) { @@ -241,7 +240,6 @@ const DateInputV2: React.FC = ({ onBlur={() => { setIsOpen?.(false); }} - ref={popover} static className={classNames( "cui-dropdown-base absolute mt-0.5 w-72 divide-y-0 p-4", @@ -252,10 +250,6 @@ const DateInputV2: React.FC = ({ { - popover.current?.focus(); - e.preventDefault(); - }} className="cui-input-base bg-gray-50" value={ displayValue.replace( diff --git a/src/Components/Common/GLocationPicker.tsx b/src/Components/Common/GLocationPicker.tsx index 5356d28fa2a..fc121b8519f 100644 --- a/src/Components/Common/GLocationPicker.tsx +++ b/src/Components/Common/GLocationPicker.tsx @@ -7,14 +7,6 @@ import CareIcon from "../../CAREUI/icons/CareIcon"; import useConfig from "../../Common/hooks/useConfig"; import { Popover } from "@headlessui/react"; -const render = (status: Status) => { - if (status === "LOADING") { - return ; - } - - return

{status}

; -}; - interface GLocationPickerProps { lat: number; lng: number; @@ -67,22 +59,37 @@ const GLocationPicker = ({ setCenter(m?.getCenter()?.toJSON()); }; + const render = (status: Status) => { + switch (status) { + case Status.LOADING: + return ; + case Status.SUCCESS: + return ( + + {location && } + + ); + default: + return

{status}

; + } + }; + return (
- - - {location && } - - +
); }; @@ -149,7 +156,9 @@ const Map: React.FC = ({ places.length > 0 && places[0].geometry?.location ) { - handleOnChange(places[0].geometry.location); + const selectedLocation = places[0].geometry.location; + handleOnChange(selectedLocation); + map?.setCenter(selectedLocation); } }); } diff --git a/src/Components/ExternalResult/FacilitiesSelectDialogue.tsx b/src/Components/ExternalResult/FacilitiesSelectDialogue.tsx index 763d92f2d1f..7239e0b912d 100644 --- a/src/Components/ExternalResult/FacilitiesSelectDialogue.tsx +++ b/src/Components/ExternalResult/FacilitiesSelectDialogue.tsx @@ -34,7 +34,7 @@ const FacilitiesSelectDialog = (props: Props) => { diff --git a/src/Components/Facility/AssetCreate.tsx b/src/Components/Facility/AssetCreate.tsx index 156d738857a..beafe8d33c8 100644 --- a/src/Components/Facility/AssetCreate.tsx +++ b/src/Components/Facility/AssetCreate.tsx @@ -37,7 +37,8 @@ import useVisibility from "../../Utils/useVisibility"; import { validateEmailAddress } from "../../Common/validation"; import { dateQueryString, parsePhoneNumber } from "../../Utils/utils.js"; import dayjs from "../../Utils/dayjs"; -import DateInputV2 from "../Common/DateInputV2.js"; +import DateFormField from "../Form/FormFields/DateFormField.js"; +import { t } from "i18next"; const Loading = lazy(() => import("../Common/Loading")); @@ -404,7 +405,7 @@ const AssetCreate = (props: AssetProps) => { if (locations.length === 0) { return ( {

- You need at least a location to create an assest. + {t("you_need_at_least_a_location_to_create_an_assest")}

@@ -440,7 +441,8 @@ const AssetCreate = (props: AssetProps) => { onClick={() => setIsScannerActive(false)} className="btn btn-default mb-2" > - Close Scanner + + {t("close_scanner")} { } style={{ width: "100%" }} /> -

Scan Asset QR!

+

+ {t("scan_asset_qr")} +

); @@ -479,7 +483,7 @@ const AssetCreate = (props: AssetProps) => { return (
{ > setName(value)} @@ -544,7 +548,7 @@ const AssetCreate = (props: AssetProps) => { {/* Location */} - Asset Location + {t("asset_location")}
{ data-testid="asset-type-input" > { { > setDescription(value)} error={state.errors.description} @@ -664,7 +668,7 @@ const AssetCreate = (props: AssetProps) => { className="col-span-6" required name="is_working" - label="Working Status" + label={t("working_status")} options={["true", "false"]} optionLabel={(option) => { return ( @@ -692,8 +696,8 @@ const AssetCreate = (props: AssetProps) => { > setNotWorkingReason(e.value)} error={state.errors.not_working_reason} @@ -717,7 +721,7 @@ const AssetCreate = (props: AssetProps) => { id="qr_code_id" name="qr_code_id" placeholder="" - label="Asset QR ID" + label={t("asset_qr_id")} value={qrCodeId} onChange={(e) => setQrCodeId(e.value)} error={state.errors.qr_code_id} @@ -743,9 +747,9 @@ const AssetCreate = (props: AssetProps) => { setManufacturer(e.value)} error={state.errors.manufacturer} /> @@ -760,7 +764,7 @@ const AssetCreate = (props: AssetProps) => { { const value = dayjs(event.value); @@ -788,8 +792,8 @@ const AssetCreate = (props: AssetProps) => { setSupportName(e.value)} error={state.errors.support_name} @@ -804,7 +808,7 @@ const AssetCreate = (props: AssetProps) => { > setSupportPhone(e.value)} @@ -822,8 +826,8 @@ const AssetCreate = (props: AssetProps) => { setSupportEmail(e.value)} error={state.errors.support_email} @@ -841,9 +845,9 @@ const AssetCreate = (props: AssetProps) => { setVendorName(e.value)} error={state.errors.vendor_name} /> @@ -858,7 +862,7 @@ const AssetCreate = (props: AssetProps) => { setSerialNumber(e.value)} error={state.errors.serial_number} @@ -874,25 +878,26 @@ const AssetCreate = (props: AssetProps) => { ref={fieldRef["last_serviced_on"]} data-testid="asset-last-serviced-on-input" > - Last Serviced On - { if ( - dayjs(date).format("YYYY-MM-DD") > + dayjs(date.value).format("YYYY-MM-DD") > new Date().toLocaleDateString("en-ca") ) { Notification.Error({ msg: "Last Serviced date can't be in future", }); } else { - setLastServicedOn(dayjs(date).format("YYYY-MM-DD")); + setLastServicedOn( + dayjs(date.value).format("YYYY-MM-DD") + ); } }} - max={new Date()} /> { > setNotes(e.value)} error={state.errors.notes} @@ -928,13 +935,13 @@ const AssetCreate = (props: AssetProps) => { /> handleSubmit(e, false)} - label={assetId ? "Update" : "Create Asset"} + label={assetId ? t("update") : t("create_asset")} /> {!assetId && ( handleSubmit(e, true)} - label="Create & Add More" + label={t("create_add_more")} /> )}
diff --git a/src/Components/Facility/ConsultationForm.tsx b/src/Components/Facility/ConsultationForm.tsx index 6c7e8a6760e..01a5f02b7b1 100644 --- a/src/Components/Facility/ConsultationForm.tsx +++ b/src/Components/Facility/ConsultationForm.tsx @@ -123,7 +123,7 @@ const initForm: FormDetails = { facility: "", admitted: "false", admitted_to: "", - category: "Comfort", + category: "", admission_date: new Date(), discharge_date: null, referred_to: "", @@ -373,8 +373,8 @@ export const ConsultationForm = (props: any) => { admitted_to: res.data.admitted_to ? res.data.admitted_to : "", category: res.data.category ? PATIENT_CATEGORIES.find((i) => i.text === res.data.category) - ?.id ?? "Comfort" - : "Comfort", + ?.id ?? "" + : "", patient_no: res.data.patient_no ?? "", OPconsultation: res.data.consultation_notes, is_telemedicine: `${res.data.is_telemedicine}`, diff --git a/src/Components/Facility/Consultations/LiveFeed.tsx b/src/Components/Facility/Consultations/LiveFeed.tsx index c6ba749b471..aba473e958d 100644 --- a/src/Components/Facility/Consultations/LiveFeed.tsx +++ b/src/Components/Facility/Consultations/LiveFeed.tsx @@ -36,6 +36,7 @@ const LiveFeed = (props: any) => { const [streamStatus, setStreamStatus] = useState( StreamStatus.Offline ); + const [videoStartTime, setVideoStartTime] = useState(null); const [bed, setBed] = useState({}); const [preset, setNewPreset] = useState(""); const [loading, setLoading] = useState(); @@ -100,6 +101,16 @@ const LiveFeed = (props: any) => { }, }); + const calculateVideoLiveDelay = () => { + const video = liveFeedPlayerRef.current as HTMLVideoElement; + if (!video || !videoStartTime) return 0; + + const timeDifference = + (new Date().getTime() - videoStartTime.getTime()) / 1000; + + return timeDifference - video.currentTime; + }; + const getBedPresets = async (id: any) => { const bedAssets = await dispatch( listAssetBeds({ @@ -223,6 +234,7 @@ const LiveFeed = (props: any) => { }, reset: () => { setStreamStatus(StreamStatus.Loading); + setVideoStartTime(null); startStream({ onSuccess: () => setStreamStatus(StreamStatus.Playing), onError: () => setStreamStatus(StreamStatus.Offline), @@ -344,8 +356,25 @@ const LiveFeed = (props: any) => { playsInline className="z-10 h-full w-full" ref={liveFeedPlayerRef} + onPlay={() => { + setVideoStartTime(() => new Date()); + }} + onWaiting={() => { + const delay = calculateVideoLiveDelay(); + if (delay > 5) { + setStreamStatus(StreamStatus.Loading); + } + }} > + {streamStatus === StreamStatus.Playing && + calculateVideoLiveDelay() > 3 && ( +
+ + Slow Network Detected +
+ )} + {loading && (
diff --git a/src/Components/Facility/UpdateFacilityMiddleware.tsx b/src/Components/Facility/FacilityConfigure.tsx similarity index 98% rename from src/Components/Facility/UpdateFacilityMiddleware.tsx rename to src/Components/Facility/FacilityConfigure.tsx index 211d8cf458a..6f0a8a9869c 100644 --- a/src/Components/Facility/UpdateFacilityMiddleware.tsx +++ b/src/Components/Facility/FacilityConfigure.tsx @@ -46,7 +46,7 @@ const FormReducer = (state = initialState, action: any) => { } }; -export const UpdateFacilityMiddleware = (props: any) => { +export const FacilityConfigure = (props: any) => { const [state, dispatch] = useReducer(FormReducer, initialState); const { facilityId } = props; const dispatchAction: any = useDispatch(); diff --git a/src/Components/Facility/FacilityHome.tsx b/src/Components/Facility/FacilityHome.tsx index 15317b1b56c..68990e64416 100644 --- a/src/Components/Facility/FacilityHome.tsx +++ b/src/Components/Facility/FacilityHome.tsx @@ -323,7 +323,7 @@ export const FacilityHome = (props: any) => { StaffUserTypeIndex; const editCoverImageTooltip = hasPermissionToEditCoverImage && ( -
+
{`${hasCoverImage ? "Edit" : "Upload"}`}
@@ -541,9 +541,7 @@ export const FacilityHome = (props: any) => { - navigate(`/facility/${facilityId}/middleware/update`) - } + onClick={() => navigate(`/facility/${facilityId}/configure`)} authorizeFor={NonReadOnlyUsers} icon={} > diff --git a/src/Components/Form/Form.tsx b/src/Components/Form/Form.tsx index 5b1cf018965..e934c4ffe0e 100644 --- a/src/Components/Form/Form.tsx +++ b/src/Components/Form/Form.tsx @@ -7,6 +7,7 @@ import { FormContextValue, createFormContext } from "./FormContext"; import { FieldChangeEvent } from "./FormFields/Utils"; import { FormDetails, FormErrors, FormState, formReducer } from "./Utils"; import { DraftSection, useAutoSaveReducer } from "../../Utils/AutoSave"; +import * as Notification from "../../Utils/Notifications"; type Props = { className?: string; @@ -51,6 +52,10 @@ const Form = ({ if (Object.keys(errors).length) { dispatch({ type: "set_errors", errors }); + + if (errors.$all) { + Notification.Error({ msg: errors.$all }); + } return; } } diff --git a/src/Components/Form/FormFields/NumericWithUnitsFormField.tsx b/src/Components/Form/FormFields/NumericWithUnitsFormField.tsx index 31ac781e018..02aa03fdf71 100644 --- a/src/Components/Form/FormFields/NumericWithUnitsFormField.tsx +++ b/src/Components/Form/FormFields/NumericWithUnitsFormField.tsx @@ -34,6 +34,7 @@ export default function NumericWithUnitsFormField(props: Props) { max={props.max} autoComplete={props.autoComplete} required={field.required} + value={numValue} onChange={(e) => field.handleChange(e.target.value + " " + unitValue)} />
diff --git a/src/Components/Form/Utils.ts b/src/Components/Form/Utils.ts index 2ec5d4b60e5..0592e81a06c 100644 --- a/src/Components/Form/Utils.ts +++ b/src/Components/Form/Utils.ts @@ -1,7 +1,9 @@ import { FieldError } from "./FieldValidators"; export type FormDetails = { [key: string]: any }; -export type FormErrors = Partial>; +export type FormErrors = Partial< + Record +>; export type FormState = { form: T; errors: FormErrors }; export type FormAction = | { type: "set_form"; form: T } diff --git a/src/Components/Medicine/CreatePrescriptionForm.tsx b/src/Components/Medicine/CreatePrescriptionForm.tsx index fb7fec5f431..2a58c632d20 100644 --- a/src/Components/Medicine/CreatePrescriptionForm.tsx +++ b/src/Components/Medicine/CreatePrescriptionForm.tsx @@ -1,4 +1,4 @@ -import { FieldError, RequiredFieldValidator } from "../Form/FieldValidators"; +import { RequiredFieldValidator } from "../Form/FieldValidators"; import Form from "../Form/Form"; import { SelectFormField } from "../Form/FormFields/SelectFormField"; import TextAreaFormField from "../Form/FormFields/TextAreaFormField"; @@ -11,6 +11,7 @@ import NumericWithUnitsFormField from "../Form/FormFields/NumericWithUnitsFormFi import { useTranslation } from "react-i18next"; import MedibaseAutocompleteFormField from "./MedibaseAutocompleteFormField"; import dayjs from "../../Utils/dayjs"; +import { PrescriptionFormValidator } from "./validators"; export default function CreatePrescriptionForm(props: { prescription: Prescription; @@ -40,16 +41,7 @@ export default function CreatePrescriptionForm(props: { } }} noPadding - validate={(form) => { - const errors: Partial> = {}; - errors.medicine_object = RequiredFieldValidator()(form.medicine_object); - errors.dosage = RequiredFieldValidator()(form.dosage); - if (form.is_prn) - errors.indicator = RequiredFieldValidator()(form.indicator); - if (!form.is_prn) - errors.frequency = RequiredFieldValidator()(form.frequency); - return errors; - }} + validate={PrescriptionFormValidator()} className="max-w-3xl" > {(field) => ( diff --git a/src/Components/Medicine/EditPrescriptionForm.tsx b/src/Components/Medicine/EditPrescriptionForm.tsx new file mode 100644 index 00000000000..d5261f70c7e --- /dev/null +++ b/src/Components/Medicine/EditPrescriptionForm.tsx @@ -0,0 +1,158 @@ +import { useState } from "react"; +import Form from "../Form/Form"; +import { Prescription } from "./models"; +import request from "../../Utils/request/request"; +import routes from "../../Redux/api"; +import * as Notification from "../../Utils/Notifications"; +import useSlug from "../../Common/hooks/useSlug"; +import { RequiredFieldValidator } from "../Form/FieldValidators"; +import { useTranslation } from "react-i18next"; +import { SelectFormField } from "../Form/FormFields/SelectFormField"; +import NumericWithUnitsFormField from "../Form/FormFields/NumericWithUnitsFormField"; +import { + PRESCRIPTION_FREQUENCIES, + PRESCRIPTION_ROUTES, +} from "./CreatePrescriptionForm"; +import TextFormField from "../Form/FormFields/TextFormField"; +import TextAreaFormField from "../Form/FormFields/TextAreaFormField"; +import { EditPrescriptionFormValidator } from "./validators"; + +interface Props { + initial: Prescription; + onDone: (created: boolean) => void; +} + +const handleSubmit = async ( + consultation_external_id: string, + oldObj: Prescription, + { discontinued_reason, ...newObj }: Prescription +) => { + const discontinue = await request(routes.discontinuePrescription, { + pathParams: { consultation_external_id, external_id: oldObj.id }, + body: { + discontinued_reason: discontinued_reason + ? `Edit: ${discontinued_reason}` + : "Edited", + }, + }); + + if (discontinue.res?.status !== 200) { + Notification.Error({ + msg: "Failed to discontinue previous prescription", + }); + return; + } + + const { res } = await request(routes.createPrescription, { + pathParams: { consultation_external_id }, + body: { + ...newObj, + // Forcing the medicine to be the same as the old one + medicine: oldObj.medicine_object?.id, + medicine_old: oldObj.medicine_old, + }, + }); + + return res?.status === 201; +}; + +export default function EditPrescriptionForm(props: Props) { + const consultation = useSlug("consultation"); + const [isLoading, setIsLoading] = useState(false); + const { t } = useTranslation(); + + return ( + + disabled={isLoading} + defaults={props.initial} + onCancel={() => props.onDone(false)} + onSubmit={async (obj) => { + setIsLoading(true); + const success = await handleSubmit(consultation, props.initial, obj); + setIsLoading(false); + + if (success) { + props.onDone(true); + } + }} + noPadding + validate={EditPrescriptionFormValidator(props.initial)} + > + {(field) => ( + <> + + +
+ t("PRESCRIPTION_ROUTE_" + key)} + optionValue={(key) => key} + /> + +
+ + {props.initial.is_prn ? ( + <> + + + `${hours} hrs.`} + optionValue={(hours) => hours} + position="above" + /> + + ) : ( +
+ + t("PRESCRIPTION_FREQUENCY_" + key.toUpperCase()) + } + optionValue={([key]) => key} + /> + +
+ )} + + + + )} + + ); +} diff --git a/src/Components/Medicine/PrescriptionAdministrationsTable.tsx b/src/Components/Medicine/PrescriptionAdministrationsTable.tsx index 470caa1042b..6a018f8a4f3 100644 --- a/src/Components/Medicine/PrescriptionAdministrationsTable.tsx +++ b/src/Components/Medicine/PrescriptionAdministrationsTable.tsx @@ -20,6 +20,7 @@ import { formatTime, } from "../../Utils/utils"; import useRangePagination from "../../Common/hooks/useRangePagination"; +import EditPrescriptionForm from "./EditPrescriptionForm"; interface DateRange { start: Date; @@ -47,10 +48,6 @@ export default function PrescriptionAdministrationsTable({ const { t } = useTranslation(); const [state, setState] = useState(); - - const [showDiscontinued, setShowDiscontinued] = useState(false); - const [discontinuedCount, setDiscontinuedCount] = useState(); - const pagination = useRangePagination({ bounds: state?.administrationsTimeBounds ?? { start: new Date(), @@ -58,8 +55,7 @@ export default function PrescriptionAdministrationsTable({ }, perPage: 24 * 60 * 60 * 1000, slots: 24, - snapToLatest: true, - reverse: true, + defaultEnd: true, }); const [showBulkAdminister, setShowBulkAdminister] = useState(false); @@ -69,13 +65,8 @@ export default function PrescriptionAdministrationsTable({ ); const refetch = useCallback(async () => { - const filters = { - is_prn: prn, - prescription_type: "REGULAR", - }; - const res = await dispatch( - list(showDiscontinued ? filters : { ...filters, discontinued: false }) + list({ is_prn: prn, prescription_type: "REGULAR" }) ); setState({ @@ -84,14 +75,7 @@ export default function PrescriptionAdministrationsTable({ ), administrationsTimeBounds: getAdministrationBounds(res.data.results), }); - - if (showDiscontinued === false) { - const discontinuedRes = await dispatch( - list({ ...filters, discontinued: true, limit: 0 }) - ); - setDiscontinuedCount(discontinuedRes.data.count); - } - }, [consultation_id, showDiscontinued, dispatch]); + }, [consultation_id, dispatch]); useEffect(() => { refetch(); @@ -158,22 +142,17 @@ export default function PrescriptionAdministrationsTable({ } /> -
- +
+
- + + )) - : pagination.slots - ?.map(({ start, end }, index) => ( - - )) - .reverse()} + : pagination.slots?.map(({ start, end }, index) => ( + + ))}
-
- {t("medicine")} - -

Dosage &

-

- {!state?.prescriptions[0]?.is_prn - ? "Frequency" - : "Indicator"} -

-
-
+
{t("medicine")} +

Dosage &

+

+ {!state?.prescriptions[0]?.is_prn ? "Frequency" : "Indicator"} +

@@ -184,10 +163,8 @@ export default function PrescriptionAdministrationsTable({ border className="mx-2 px-1" variant="secondary" - disabled={!pagination.hasNext} - onClick={pagination.next} - tooltip="Next 24 hours" - tooltipClassName="tooltip-bottom -translate-x-1/2 text-xs" + disabled={!pagination.hasPrevious} + onClick={pagination.previous} > @@ -201,26 +178,24 @@ export default function PrescriptionAdministrationsTable({

-

{formatDateTime(end, "DD/MM")}

-

{formatDateTime(end, "HH:mm")}

- - - Administration(s) between -
- {formatTime(start)} and{" "} - {formatTime(end)} -
- on {formatDate(start)} -
-
+

{formatDateTime(start, "DD/MM")}

+

{formatDateTime(start, "HH:mm")}

+ + + Administration(s) between +
+ {formatTime(start)} and{" "} + {formatTime(end)} +
+ on {formatDate(start)} +
+
@@ -255,23 +228,6 @@ export default function PrescriptionAdministrationsTable({
- {showDiscontinued === false && !!discontinuedCount && ( - setShowDiscontinued(true)} - > - - - - Show {discontinuedCount} other discontinued - prescription(s) - - - - )} - {state?.prescriptions.length === 0 && (
@@ -299,6 +255,7 @@ const PrescriptionRow = ({ prescription, ...props }: PrescriptionRowProps) => { const { t } = useTranslation(); // const [showActions, setShowActions] = useState(false); const [showDetails, setShowDetails] = useState(false); + const [showEdit, setShowEdit] = useState(false); const [showAdminister, setShowAdminister] = useState(false); const [showDiscontinue, setShowDiscontinue] = useState(false); const [administrations, setAdministrations] = @@ -328,7 +285,12 @@ const PrescriptionRow = ({ prescription, ...props }: PrescriptionRowProps) => { }, [prescription.id, dispatch, props.intervals]); return ( - <> + {showDiscontinue && ( { {t("discontinue")} + { + setShowDetails(false); + setShowEdit(true); + }} + > + + {t("edit")} + {
)} - - setShowDetails(true)} - > -
-
- - {prescription.medicine_object?.name ?? - prescription.medicine_old} - - - {prescription.discontinued && ( - - {t("discontinued")} - - )} - - {prescription.route && ( - - {t(prescription.route)} - - )} + {showEdit && ( + setShowEdit(false)} + show={showEdit} + title={`${t("edit")} ${t( + prescription.is_prn ? "prn_prescription" : "prescription_medication" + )}: ${ + prescription.medicine_object?.name ?? prescription.medicine_old + }`} + description={ +
+ + {t("edit_caution_note")}
+ } + className="w-full max-w-3xl lg:min-w-[600px]" + > + { + setShowEdit(false); + if (success) { + props.refetch(); + } + }} + /> +
+ )} + setShowDetails(true)} + > +
+ + {prescription.medicine_object?.name ?? prescription.medicine_old} + -
-

{prescription.dosage}

-

- {!prescription.is_prn - ? t("PRESCRIPTION_FREQUENCY_" + prescription.frequency) - : prescription.indicator} -

-
-
- + {prescription.discontinued && ( + + {t("discontinued")} + + )} - - {/* Administration Cells */} - {props.intervals - .map(({ start, end }, index) => ( - - {administrations === undefined ? ( - - ) : ( - - )} - - )) - .reverse()} - - - {/* Action Buttons */} - - setShowAdminister(true)} - > - {t("administer")} - + {prescription.route && ( + + {t(prescription.route)} + + )} +
+ + + +

{prescription.dosage}

+

+ {!prescription.is_prn + ? t("PRESCRIPTION_FREQUENCY_" + prescription.frequency) + : prescription.indicator} +

+ + + + {/* Administration Cells */} + {props.intervals.map(({ start, end }, index) => ( + + {administrations === undefined ? ( + + ) : ( + + )} - - + ))} + + + {/* Action Buttons */} + + setShowAdminister(true)} + > + {t("administer")} + + + ); }; diff --git a/src/Components/Medicine/PrescriptionDetailCard.tsx b/src/Components/Medicine/PrescriptionDetailCard.tsx index 6da4fa7ae8d..bf27aa34068 100644 --- a/src/Components/Medicine/PrescriptionDetailCard.tsx +++ b/src/Components/Medicine/PrescriptionDetailCard.tsx @@ -5,6 +5,7 @@ import ReadMore from "../Common/components/Readmore"; import ButtonV2 from "../Common/components/ButtonV2"; import { PrescriptionActions } from "../../Redux/actions"; import { useTranslation } from "react-i18next"; +import RecordMeta from "../../CAREUI/display/RecordMeta"; export default function PrescriptionDetailCard({ prescription, @@ -29,7 +30,7 @@ export default function PrescriptionDetailCard({ prescription.discontinued && "bg-gray-200 opacity-80" )} > -
+
@@ -83,7 +84,7 @@ export default function PrescriptionDetailCard({
-
+
{prescription.medicine_object?.name ?? prescription.medicine_old} @@ -146,6 +147,23 @@ export default function PrescriptionDetailCard({ )}
+ +
+ + Prescribed + + + {prescription.discontinued && ( + + and was discontinued + + + )} +
{props.children} diff --git a/src/Components/Medicine/models.ts b/src/Components/Medicine/models.ts index 62aea46b6d2..0c49d199b21 100644 --- a/src/Components/Medicine/models.ts +++ b/src/Components/Medicine/models.ts @@ -1,7 +1,7 @@ import { PerformedByModel } from "../HCX/misc"; interface BasePrescription { - readonly id?: string; + readonly id: string; medicine?: string; medicine_object?: MedibaseMedicine; medicine_old?: string; diff --git a/src/Components/Medicine/validators.ts b/src/Components/Medicine/validators.ts new file mode 100644 index 00000000000..40261646d05 --- /dev/null +++ b/src/Components/Medicine/validators.ts @@ -0,0 +1,49 @@ +import { FieldError, RequiredFieldValidator } from "../Form/FieldValidators"; +import { FormErrors } from "../Form/Utils"; +import { Prescription } from "./models"; + +export const PrescriptionFormValidator = () => { + return (form: Prescription): FormErrors => { + const errors: Partial> = {}; + errors.medicine_object = RequiredFieldValidator()(form.medicine_object); + errors.dosage = RequiredFieldValidator()(form.dosage); + if (form.is_prn) + errors.indicator = RequiredFieldValidator()(form.indicator); + if (!form.is_prn) + errors.frequency = RequiredFieldValidator()(form.frequency); + return errors; + }; +}; + +export const EditPrescriptionFormValidator = (old: Prescription) => { + return (form: Prescription): FormErrors => { + const errors = PrescriptionFormValidator()(form); + + if (comparePrescriptions(old, form)) { + errors.$all = "No changes made"; + } + + return errors; + }; +}; + +const PRESCRIPTION_COMPARE_FIELDS: (keyof Prescription)[] = [ + "medicine", + "days", + "discontinued", + "dosage", + "frequency", + "indicator", + "is_prn", + "max_dosage", + "min_hours_between_doses", + "prescription_type", + "route", +]; + +export const comparePrescriptions = (a: Prescription, b: Prescription) => { + return ( + PRESCRIPTION_COMPARE_FIELDS.every((field) => a[field] === b[field]) && + a.medicine_object?.id === b.medicine_object?.id + ); +}; diff --git a/src/Components/Notifications/ShowPushNotification.tsx b/src/Components/Notifications/ShowPushNotification.tsx index fe88c087d5e..09b62bb9333 100644 --- a/src/Components/Notifications/ShowPushNotification.tsx +++ b/src/Components/Notifications/ShowPushNotification.tsx @@ -1,13 +1,13 @@ import { useDispatch } from "react-redux"; import { getNotificationData } from "../../Redux/actions"; import { useEffect } from "react"; +import { DetailRoute } from "../../Routers/types"; -export default function ShowPushNotification({ external_id }: any) { +export default function ShowPushNotification({ id }: DetailRoute) { const dispatch: any = useDispatch(); const resultUrl = async () => { - console.log("ID:", external_id.id); - const res = await dispatch(getNotificationData({ id: external_id.id })); + const res = await dispatch(getNotificationData({ id })); const data = res.data.caused_objects; switch (res.data.event) { case "PATIENT_CREATED": diff --git a/src/Components/Patient/DailyRounds.tsx b/src/Components/Patient/DailyRounds.tsx index 351136e32d4..a84ad4a3e8b 100644 --- a/src/Components/Patient/DailyRounds.tsx +++ b/src/Components/Patient/DailyRounds.tsx @@ -42,7 +42,7 @@ const initForm: any = { other_symptoms: "", physical_examination_info: "", other_details: "", - patient_category: "Comfort", + patient_category: "", current_health: 0, recommend_discharge: false, action: null, @@ -142,14 +142,14 @@ export const DailyRounds = (props: any) => { ); if (!status.aborted) { - if (res && res.data) { + if (res?.data) { const data = { ...res.data, patient_category: res.data.patient_category ? PATIENT_CATEGORIES.find( (i) => i.text === res.data.patient_category - )?.id || "Comfort" - : "Comfort", + )?.id ?? "" + : "", rhythm: (res.data.rhythm && RHYTHM_CHOICES.find((i) => i.text === res.data.rhythm)?.id) || @@ -186,8 +186,8 @@ export const DailyRounds = (props: any) => { patient_category: res.data.patient_category ? PATIENT_CATEGORIES.find( (i) => i.text === res.data.patient_category - )?.id || "Comfort" - : "Comfort", + )?.id ?? "" + : "", rhythm: (res.data.rhythm && RHYTHM_CHOICES.find((i) => i.text === res.data.rhythm)?.id) || @@ -205,6 +205,12 @@ export const DailyRounds = (props: any) => { let invalidForm = false; Object.keys(state.form).forEach((field) => { switch (field) { + case "patient_category": + if (!state.form[field]) { + errors[field] = "Please select a category"; + invalidForm = true; + } + return; case "other_symptoms": if ( state.form.additional_symptoms?.includes(9) && diff --git a/src/Components/Patient/FileUpload.tsx b/src/Components/Patient/FileUpload.tsx index fe0881e6994..2b0b068b0ef 100644 --- a/src/Components/Patient/FileUpload.tsx +++ b/src/Components/Patient/FileUpload.tsx @@ -100,7 +100,7 @@ interface FileUploadProps { hideBack: boolean; audio?: boolean; unspecified: boolean; - sampleId?: number; + sampleId?: string; claimId?: string; } diff --git a/src/Components/Patient/PatientRegister.tsx b/src/Components/Patient/PatientRegister.tsx index 41f36bffd35..c515e7584c1 100644 --- a/src/Components/Patient/PatientRegister.tsx +++ b/src/Components/Patient/PatientRegister.tsx @@ -72,8 +72,6 @@ import { FormContextValue } from "../Form/FormContext.js"; const Loading = lazy(() => import("../Common/Loading")); const PageTitle = lazy(() => import("../Common/PageTitle")); -// const debounce = require("lodash.debounce"); - interface PatientRegisterProps extends PatientModel { facilityId: string; } @@ -1260,7 +1258,7 @@ export const PatientRegister = (props: PatientRegisterProps) => { required label="Phone Number" onChange={(event) => { - duplicateCheck(event.value); + if (!id) duplicateCheck(event.value); field("phone_number").onChange(event); }} types={["mobile", "landline"]} diff --git a/src/Components/Patient/SampleDetails.tsx b/src/Components/Patient/SampleDetails.tsx index 7814109978b..e29df92f47f 100644 --- a/src/Components/Patient/SampleDetails.tsx +++ b/src/Components/Patient/SampleDetails.tsx @@ -13,14 +13,11 @@ import { getTestSample } from "../../Redux/actions"; import { navigate } from "raviger"; import { useDispatch } from "react-redux"; +import { DetailRoute } from "../../Routers/types"; const Loading = lazy(() => import("../Common/Loading")); -interface SampleDetailsProps { - id: number; -} - -export const SampleDetails = ({ id }: SampleDetailsProps) => { +export const SampleDetails = ({ id }: DetailRoute) => { const dispatch: any = useDispatch(); const [isLoading, setIsLoading] = useState(false); const [sampleDetails, setSampleDetails] = useState({}); diff --git a/src/Components/Resource/ListView.tsx b/src/Components/Resource/ListView.tsx index b368c9bf3ea..24b4e263039 100644 --- a/src/Components/Resource/ListView.tsx +++ b/src/Components/Resource/ListView.tsx @@ -31,7 +31,7 @@ export default function ListView() { const { t } = useTranslation(); const onBoardViewBtnClick = () => - navigate("/resource/board-view", { query: qParams }); + navigate("/resource/board", { query: qParams }); const appliedFilters = formatFilter(qParams); const refreshList = () => { diff --git a/src/Components/Resource/ResourceBoardView.tsx b/src/Components/Resource/ResourceBoardView.tsx index de3058f9406..d3b2b202649 100644 --- a/src/Components/Resource/ResourceBoardView.tsx +++ b/src/Components/Resource/ResourceBoardView.tsx @@ -32,7 +32,7 @@ export default function BoardView() { const { t } = useTranslation(); const onListViewBtnClick = () => { - navigate("/resource/list-view", { query: qParams }); + navigate("/resource/list", { query: qParams }); localStorage.setItem("defaultResourceView", "list"); }; diff --git a/src/Components/Resource/ResourceDetails.tsx b/src/Components/Resource/ResourceDetails.tsx index 591f6b48bdb..67adc7edd82 100644 --- a/src/Components/Resource/ResourceDetails.tsx +++ b/src/Components/Resource/ResourceDetails.tsx @@ -231,7 +231,7 @@ export default function ResourceDetails(props: { id: string }) { {isPrintMode ? (
diff --git a/src/Components/Shifting/BoardView.tsx b/src/Components/Shifting/BoardView.tsx index 0eddfddf745..85815770acf 100644 --- a/src/Components/Shifting/BoardView.tsx +++ b/src/Components/Shifting/BoardView.tsx @@ -95,9 +95,7 @@ export default function BoardView() {
- navigate("/shifting/list-view", { query: qParams }) - } + onClick={() => navigate("/shifting/list", { query: qParams })} > {t("list_view")} diff --git a/src/Components/Shifting/ListView.tsx b/src/Components/Shifting/ListView.tsx index 6d55122ea11..f3fb14a4c0a 100644 --- a/src/Components/Shifting/ListView.tsx +++ b/src/Components/Shifting/ListView.tsx @@ -312,9 +312,7 @@ export default function ListView() {
- navigate("/shifting/board-view", { query: qParams }) - } + onClick={() => navigate("/shifting/board", { query: qParams })} > {t("board_view")} diff --git a/src/Components/Shifting/ShiftDetails.tsx b/src/Components/Shifting/ShiftDetails.tsx index 03e09702aac..7bffe429960 100644 --- a/src/Components/Shifting/ShiftDetails.tsx +++ b/src/Components/Shifting/ShiftDetails.tsx @@ -557,7 +557,7 @@ export default function ShiftDetails(props: { id: string }) { ) : ( { export const getTestList = (params: object) => { return fireRequest("getTestSampleList", [], params); }; -export const getTestSample = (id: number) => { +export const getTestSample = (id: string) => { return fireRequest("getTestSample", [id], {}); }; export const patchSample = (params: object, pathParam: object) => { @@ -1003,7 +1003,7 @@ export const PrescriptionActions = (consultation_external_id: string) => { const pathParams = { consultation_external_id }; return { - list: (query?: Record) => { + list: (query?: Partial) => { let altKey; if (query?.is_prn !== undefined) { altKey = query?.is_prn diff --git a/src/Redux/api.tsx b/src/Redux/api.tsx index 8eff2594af0..9cf9a0b6643 100644 --- a/src/Redux/api.tsx +++ b/src/Redux/api.tsx @@ -1,6 +1,7 @@ import { IConfig } from "../Common/hooks/useConfig"; import { AssetData } from "../Components/Assets/AssetTypes"; import { LocationModel } from "../Components/Facility/models"; +import { Prescription } from "../Components/Medicine/models"; import { UserModel } from "../Components/Users/models"; import { PaginatedResponse } from "../Utils/request/types"; @@ -8,7 +9,7 @@ import { PaginatedResponse } from "../Utils/request/types"; * A fake function that returns an empty object casted to type T * @returns Empty object as type T */ -function Res(): T { +function Type(): T { return {} as T; } @@ -17,12 +18,17 @@ interface JwtTokenObtainPair { refresh: string; } +interface LoginInput { + username: string; + password: string; +} + const routes = { config: { path: import.meta.env.REACT_APP_CONFIG ?? "/config.json", method: "GET", noAuth: true, - TRes: Res(), + TRes: Type(), }, // Auth Endpoints @@ -30,12 +36,15 @@ const routes = { path: "/api/v1/auth/login/", method: "POST", noAuth: true, + TRes: Type(), + TBody: Type(), }, token_refresh: { path: "/api/v1/auth/token/refresh/", method: "POST", - TRes: Res(), + TRes: Type(), + TBody: Type<{ refresh: string }>(), }, token_verify: { @@ -46,16 +55,22 @@ const routes = { checkResetToken: { path: "/api/v1/password_reset/check/", method: "POST", + TRes: Type>(), + TBody: Type<{ token: string }>(), }, resetPassword: { path: "/api/v1/password_reset/confirm/", method: "POST", + TRes: Type>(), + TBody: Type<{ password: string; confirm: string }>(), }, forgotPassword: { path: "/api/v1/password_reset/", method: "POST", + TRes: Type>(), + TBody: Type<{ username: string }>(), }, updatePassword: { @@ -65,7 +80,7 @@ const routes = { // User Endpoints currentUser: { path: "/api/v1/users/getcurrentuser/", - TRes: Res(), + TRes: Type(), }, userList: { @@ -199,7 +214,7 @@ const routes = { listFacilityAssetLocation: { path: "/api/v1/facility/{facility_external_id}/asset_location/", method: "GET", - TRes: Res>(), + TRes: Type>(), }, createFacilityAssetLocation: { path: "/api/v1/facility/{facility_external_id}/asset_location/", @@ -805,7 +820,7 @@ const routes = { getAsset: { path: "/api/v1/asset/{external_id}/", method: "GET", - TRes: Res(), + TRes: Type(), }, deleteAsset: { path: "/api/v1/asset/{external_id}/", @@ -971,6 +986,8 @@ const routes = { createPrescription: { path: "/api/v1/consultation/{consultation_external_id}/prescriptions/", method: "POST", + TBody: Type(), + TRes: Type(), }, listAdministrations: { @@ -996,6 +1013,8 @@ const routes = { discontinuePrescription: { path: "/api/v1/consultation/{consultation_external_id}/prescriptions/{external_id}/discontinue/", method: "POST", + TBody: Type<{ discontinued_reason: string }>(), + TRes: Type>(), }, // HCX Endpoints diff --git a/src/Routers/AppRouter.tsx b/src/Routers/AppRouter.tsx index f1449f13bc2..0f6108b00e3 100644 --- a/src/Routers/AppRouter.tsx +++ b/src/Routers/AppRouter.tsx @@ -1,64 +1,9 @@ import { useRedirect, useRoutes, usePath, Redirect } from "raviger"; import { useState, useEffect } from "react"; -import { ConsultationDetails } from "../Components/Facility/ConsultationDetails"; -import TreatmentSummary from "../Components/Facility/TreatmentSummary"; -import { ConsultationForm } from "../Components/Facility/ConsultationForm"; -import { FacilityCreate } from "../Components/Facility/FacilityCreate"; -import { FacilityHome } from "../Components/Facility/FacilityHome"; -import { HospitalList } from "../Components/Facility/HospitalList"; -import { TriageForm } from "../Components/Facility/TriageForm"; -import { DailyRounds } from "../Components/Patient/DailyRounds"; -import { PatientManager } from "../Components/Patient/ManagePatients"; -import PatientNotes from "../Components/Patient/PatientNotes"; -import { PatientHome } from "../Components/Patient/PatientHome"; -import { PatientRegister } from "../Components/Patient/PatientRegister"; -import { SampleDetails } from "../Components/Patient/SampleDetails"; -import SampleReport from "../Components/Patient/SamplePreview"; -import { SampleTest } from "../Components/Patient/SampleTest"; -import SampleViewAdmin from "../Components/Patient/SampleViewAdmin"; -import ManageUsers from "../Components/Users/ManageUsers"; -import { UserAdd } from "../Components/Users/UserAdd"; -import InventoryList from "../Components/Facility/InventoryList"; -import InventoryLog from "../Components/Facility/InventoryLog"; -import { AddInventoryForm } from "../Components/Facility/AddInventoryForm"; -import { SetInventoryForm } from "../Components/Facility/SetInventoryForm"; -import MinQuantityList from "../Components/Facility/MinQuantityList"; -import { ShiftCreate } from "../Components/Patient/ShiftCreate"; -import UserProfile from "../Components/Users/UserProfile"; -import ShiftBoardView from "../Components/Shifting/BoardView"; -import ShiftListView from "../Components/Shifting/ListView"; -import ShiftDetails from "../Components/Shifting/ShiftDetails"; -import { ShiftDetailsUpdate } from "../Components/Shifting/ShiftDetailsUpdate"; -import ResourceCreate from "../Components/Resource/ResourceCreate"; -import ResourceBoardView from "../Components/Resource/ResourceBoardView"; -import ResourceListView from "../Components/Resource/ListView"; -import ResourceDetails from "../Components/Resource/ResourceDetails"; -import { ResourceDetailsUpdate } from "../Components/Resource/ResourceDetailsUpdate"; -import ResultList from "../Components/ExternalResult/ResultList"; -import ResultItem from "../Components/ExternalResult/ResultItem"; -import ExternalResultUpload from "../Components/ExternalResult/ExternalResultUpload"; -import ResultUpdate from "../Components/ExternalResult/ResultUpdate"; -import { FileUpload } from "../Components/Patient/FileUpload"; -import Investigation from "../Components/Facility/Investigations"; -import ShowInvestigation from "../Components/Facility/Investigations/ShowInvestigation"; -import InvestigationReports from "../Components/Facility/Investigations/Reports"; -import AssetCreate from "../Components/Facility/AssetCreate"; -import DeathReport from "../Components/DeathReport/DeathReport"; -import { make as CriticalCareRecording } from "../Components/CriticalCareRecording/CriticalCareRecording.bs"; + import ShowPushNotification from "../Components/Notifications/ShowPushNotification"; import { NoticeBoard } from "../Components/Notifications/NoticeBoard"; -import { AddLocationForm } from "../Components/Facility/AddLocationForm"; -import { AddBedForm } from "../Components/Facility/AddBedForm"; -import LocationManagement from "../Components/Facility/LocationManagement"; -import { BedManagement } from "../Components/Facility/BedManagement"; -import AssetsList from "../Components/Assets/AssetsList"; -import AssetManage from "../Components/Assets/AssetManage"; -import AssetConfigure from "../Components/Assets/AssetConfigure"; -import { DailyRoundListDetails } from "../Components/Patient/DailyRoundListDetails"; import Error404 from "../Components/ErrorPages/404"; -import { DndProvider } from "react-dnd"; -import { HTML5Backend } from "react-dnd-html5-backend"; -import FacilityUsers from "../Components/Facility/FacilityUsers"; import { DesktopSidebar, MobileSidebar, @@ -66,349 +11,55 @@ import { SidebarShrinkContext, } from "../Components/Common/Sidebar/Sidebar"; import { BLACKLISTED_PATHS, LocalStorageKeys } from "../Common/constants"; -import { UpdateFacilityMiddleware } from "../Components/Facility/UpdateFacilityMiddleware"; import useConfig from "../Common/hooks/useConfig"; -import ConsultationClaims from "../Components/Facility/ConsultationClaims"; import { handleSignOut } from "../Utils/utils"; import SessionExpired from "../Components/ErrorPages/SessionExpired"; -import ManagePrescriptions from "../Components/Medicine/ManagePrescriptions"; -import CentralNursingStation from "../Components/Facility/CentralNursingStation"; -export default function AppRouter() { - const { main_logo, enable_hcx } = useConfig(); +import UserRoutes from "./routes/UserRoutes"; +import PatientRoutes from "./routes/PatientRoutes"; +import SampleRoutes from "./routes/SampleRoutes"; +import FacilityRoutes from "./routes/FacilityRoutes"; +import ConsultationRoutes from "./routes/ConsultationRoutes"; +import HCXRoutes from "./routes/HCXRoutes"; +import ShiftingRoutes from "./routes/ShiftingRoutes"; +import AssetRoutes from "./routes/AssetRoutes"; +import ResourceRoutes from "./routes/ResourceRoutes"; +import ExternalResultRoutes from "./routes/ExternalResultRoutes"; +import { DetailRoute } from "./types"; + +const Routes = { + "/": () => , - const routes = { - "/": () => , - "/users": () => , - "/users/add": () => , - "/user/profile": () => , - "/patients": () => , - "/patient/:id": ({ id }: any) => , - "/patient/:id/investigation_reports": ({ id }: any) => ( - - ), - "/sample": () => , - "/sample/:id": ({ id }: any) => , - "/patient/:patientId/test_sample/:sampleId/icmr_sample": ({ - patientId, - sampleId, - }: any) => , - "/facility": () => , - "/facility/create": () => , - "/facility/:facilityId/update": ({ facilityId }: any) => ( - - ), - "/facility/:facilityId/middleware/update": ({ facilityId }: any) => ( - - ), - "/facility/:facilityId": ({ facilityId }: any) => ( - - ), - "/facility/:facilityId/users": ({ facilityId }: any) => ( - - ), - "/facility/:facilityId/resource/new": ({ facilityId }: any) => ( - - ), - "/facility/:facilityId/triage": ({ facilityId }: any) => ( - - ), - "/facility/:facilityId/patient": ({ facilityId }: any) => ( - - ), - "/facility/:facilityId/patient/:id": ({ facilityId, id }: any) => ( - - ), - "/facility/:facilityId/patient/:id/update": ({ facilityId, id }: any) => ( - - ), - "/facility/:facilityId/patient/:patientId/sample-test": ({ - facilityId, - patientId, - }: any) => , - "/facility/:facilityId/patient/:patientId/sample/:id": ({ id }: any) => ( - - ), - "/facility/:facilityId/patient/:patientId/notes": ({ - facilityId, - patientId, - }: any) => , - "/facility/:facilityId/patient/:patientId/files": ({ - facilityId, - patientId, - }: any) => ( - - ), - "/facility/:facilityId/triage/:id": ({ facilityId, id }: any) => ( - - ), - "/facility/:facilityId/patient/:patientId/consultation": ({ - facilityId, - patientId, - }: any) => ( - - ), - "/facility/:facilityId/patient/:patientId/consultation/:id/update": ({ - facilityId, - patientId, - id, - }: any) => ( - - ), - "/facility/:facilityId/patient/:patientId/consultation/:id/files/": ({ - facilityId, - patientId, - id, - }: any) => ( - - ), - "/facility/:facilityId/patient/:patientId/consultation/:consultationId/prescriptions": - (path: any) => , - "/facility/:facilityId/patient/:patientId/consultation/:id/investigation": - ({ facilityId, patientId, id }: any) => ( - - ), - "/facility/:facilityId/patient/:patientId/consultation/:id/investigation/:sessionId": - ({ facilityId, patientId, id, sessionId }: any) => ( - - ), - "/facility/:facilityId/patient/:patientId/consultation/:id/daily-rounds": ({ - facilityId, - patientId, - id, - }: any) => ( - - ), - ...(enable_hcx - ? { - "/facility/:facilityId/patient/:patientId/consultation/:consultationId/claims": - (pathParams: any) => , - } - : {}), - "/facility/:facilityId/patient/:patientId/consultation/:consultationId/daily-rounds/:id/update": - ({ facilityId, patientId, consultationId, id }: any) => ( - - ), - "/facility/:facilityId/patient/:patientId/consultation/:consultationId/daily-rounds/:id": - ({ facilityId, patientId, consultationId, id }: any) => ( - - ), + ...AssetRoutes, + ...ConsultationRoutes, + ...ExternalResultRoutes, + ...FacilityRoutes, + ...PatientRoutes, + ...ResourceRoutes, + ...SampleRoutes, + ...ShiftingRoutes, + ...UserRoutes, - "/facility/:facilityId/patient/:patientId/consultation/:consultationId/daily_rounds/:id": - ({ facilityId, patientId, consultationId, id }: any) => ( - - ), - "/facility/:facilityId/patient/:patientId/consultation/:consultationId/daily_rounds/:id/update": - ({ facilityId, patientId, consultationId, id }: any) => ( - - ), - "/facility/:facilityId/patient/:patientId/shift/new": ({ - facilityId, - patientId, - }: any) => , - "/facility/:facilityId/inventory": ({ facilityId }: any) => ( - - ), - "/facility/:facilityId/location": ({ facilityId }: any) => ( - - ), - "/facility/:facilityId/location/:locationId/beds": ({ - facilityId, - locationId, - }: any) => ( - - ), - "/facility/:facilityId/inventory/add": ({ facilityId }: any) => ( - - ), - "/facility/:facilityId/location/add": ({ facilityId }: any) => ( - - ), - "/facility/:facilityId/location/:locationId/update": ({ - facilityId, - locationId, - }: any) => ( - - ), - "/facility/:facilityId/location/:locationId/beds/add": ({ - facilityId, - locationId, - }: any) => , - "/facility/:facilityId/location/:locationId/beds/:bedId/update": ({ - facilityId, - locationId, - bedId, - }: any) => ( - - ), - "/facility/:facilityId/inventory/min_quantity/set": ({ - facilityId, - }: any) => , - "/facility/:facilityId/inventory/min_quantity/list": ({ - facilityId, - }: any) => , - "/facility/:facilityId/inventory/min_quantity": ({ facilityId }: any) => ( - - ), - "/facility/:facilityId/inventory/:inventoryId": ({ - facilityId, - inventoryId, - }: any) => ( - - ), - "/facility/:facilityId/assets/new": ({ facilityId }: any) => ( - - ), - "/facility/:facilityId/assets/:assetId/update": ({ - facilityId, - assetId, - }: any) => , - "/assets": () => , - "/facility/:facilityId/assets/:assetId": ({ assetId, facilityId }: any) => ( - - ), - "/facility/:facilityId/assets/:assetId/configure": ({ - assetId, - facilityId, - }: any) => , - "/facility/:facilityId/cns": ({ facilityId }: any) => ( - - ), + "/notifications/:id": ({ id }: DetailRoute) => ( + + ), + "/notice_board": () => , + + "/session-expired": () => , + "/not-found": () => , +}; + +export default function AppRouter() { + const { main_logo, enable_hcx } = useConfig(); - "/shifting": () => - localStorage.getItem("defaultShiftView") === "list" ? ( - - ) : ( - - - - ), - "/shifting/board-view": () => ( - - - - ), - "/shifting/list-view": () => , - "/shifting/:id": ({ id }: any) => , - "/shifting/:id/update": ({ id }: any) => , - "/resource": () => - localStorage.getItem("defaultResourceView") === "list" ? ( - - ) : ( - - - - ), + let routes = Routes; - "/resource/board-view": () => ( - - - - ), - "/resource/list-view": () => , - "/resource/:id": ({ id }: any) => , - "/resource/:id/update": ({ id }: any) => , - "/external_results": () => , - "/external_results/upload": () => , - "/external_results/:id": ({ id }: any) => , - "/external_results/:id/update": ({ id }: any) => , - "/death_report/:id": ({ id }: any) => , - "/notifications/:id": (id: any) => ( - - ), - "/notice_board": () => , - "/facility/:facilityId/patient/:patientId/consultation/:consultationId": ({ - facilityId, - patientId, - consultationId, - }: any) => ( - - ), - "/facility/:facilityId/patient/:patientId/consultation/:consultationId/treatment-summary": - ({ facilityId, patientId, consultationId }: any) => ( - - ), - "/facility/:facilityId/patient/:patientId/consultation/:consultationId/:tab": - ({ facilityId, patientId, consultationId, tab }: any) => ( - - ), - "/session-expired": () => , - "/not-found": () => , - }; + if (enable_hcx) { + routes = { ...routes, ...HCXRoutes }; + } - useRedirect("/", "/facility"); useRedirect("/user", "/users"); - const pages = useRoutes(routes as any) || ; + const pages = useRoutes(routes) || ; const path = usePath(); const [sidebarOpen, setSidebarOpen] = useState(false); diff --git a/src/Routers/routes/AssetRoutes.tsx b/src/Routers/routes/AssetRoutes.tsx new file mode 100644 index 00000000000..d3bd96ca437 --- /dev/null +++ b/src/Routers/routes/AssetRoutes.tsx @@ -0,0 +1,21 @@ +import AssetConfigure from "../../Components/Assets/AssetConfigure"; +import AssetManage from "../../Components/Assets/AssetManage"; +import AssetsList from "../../Components/Assets/AssetsList"; +import AssetCreate from "../../Components/Facility/AssetCreate"; + +export default { + "/assets": () => , + + "/facility/:facilityId/assets/new": (params: any) => ( + + ), + "/facility/:facilityId/assets/:assetId/update": (params: any) => ( + + ), + "/facility/:facilityId/assets/:assetId": (params: any) => ( + + ), + "/facility/:facilityId/assets/:assetId/configure": (params: any) => ( + + ), +}; diff --git a/src/Routers/routes/ConsultationRoutes.tsx b/src/Routers/routes/ConsultationRoutes.tsx new file mode 100644 index 00000000000..4f1d6e7d75d --- /dev/null +++ b/src/Routers/routes/ConsultationRoutes.tsx @@ -0,0 +1,141 @@ +import { ConsultationForm } from "../../Components/Facility/ConsultationForm"; +import Investigation from "../../Components/Facility/Investigations"; +import ShowInvestigation from "../../Components/Facility/Investigations/ShowInvestigation"; +import ManagePrescriptions from "../../Components/Medicine/ManagePrescriptions"; +import { DailyRoundListDetails } from "../../Components/Patient/DailyRoundListDetails"; +import { DailyRounds } from "../../Components/Patient/DailyRounds"; +import { FileUpload } from "../../Components/Patient/FileUpload"; +import { make as CriticalCareRecording } from "../../Components/CriticalCareRecording/CriticalCareRecording.bs"; +import { ConsultationDetails } from "../../Components/Facility/ConsultationDetails"; +import TreatmentSummary from "../../Components/Facility/TreatmentSummary"; + +export default { + "/facility/:facilityId/patient/:patientId/consultation": ({ + facilityId, + patientId, + }: any) => , + "/facility/:facilityId/patient/:patientId/consultation/:id/update": ({ + facilityId, + patientId, + id, + }: any) => ( + + ), + "/facility/:facilityId/patient/:patientId/consultation/:id/files/": ({ + facilityId, + patientId, + id, + }: any) => ( + + ), + "/facility/:facilityId/patient/:patientId/consultation/:consultationId/prescriptions": + (path: any) => , + "/facility/:facilityId/patient/:patientId/consultation/:id/investigation": ({ + facilityId, + patientId, + id, + }: any) => ( + + ), + "/facility/:facilityId/patient/:patientId/consultation/:id/investigation/:sessionId": + ({ facilityId, patientId, id, sessionId }: any) => ( + + ), + "/facility/:facilityId/patient/:patientId/consultation/:id/daily-rounds": ({ + facilityId, + patientId, + id, + }: any) => ( + + ), + "/facility/:facilityId/patient/:patientId/consultation/:consultationId/daily-rounds/:id/update": + ({ facilityId, patientId, consultationId, id }: any) => ( + + ), + "/facility/:facilityId/patient/:patientId/consultation/:consultationId/daily-rounds/:id": + ({ facilityId, patientId, consultationId, id }: any) => ( + + ), + + "/facility/:facilityId/patient/:patientId/consultation/:consultationId/daily_rounds/:id": + ({ facilityId, patientId, consultationId, id }: any) => ( + + ), + "/facility/:facilityId/patient/:patientId/consultation/:consultationId/daily_rounds/:id/update": + ({ facilityId, patientId, consultationId, id }: any) => ( + + ), + "/facility/:facilityId/patient/:patientId/consultation/:consultationId": ({ + facilityId, + patientId, + consultationId, + }: any) => ( + + ), + "/facility/:facilityId/patient/:patientId/consultation/:consultationId/treatment-summary": + ({ facilityId, patientId, consultationId }: any) => ( + + ), + "/facility/:facilityId/patient/:patientId/consultation/:consultationId/:tab": + ({ facilityId, patientId, consultationId, tab }: any) => ( + + ), +}; diff --git a/src/Routers/routes/ExternalResultRoutes.tsx b/src/Routers/routes/ExternalResultRoutes.tsx new file mode 100644 index 00000000000..af4bf090d78 --- /dev/null +++ b/src/Routers/routes/ExternalResultRoutes.tsx @@ -0,0 +1,14 @@ +import ExternalResultUpload from "../../Components/ExternalResult/ExternalResultUpload"; +import ResultItem from "../../Components/ExternalResult/ResultItem"; +import ResultList from "../../Components/ExternalResult/ResultList"; +import ResultUpdate from "../../Components/ExternalResult/ResultUpdate"; +import { DetailRoute } from "../types"; + +export default { + "/external_results": () => , + "/external_results/upload": () => , + "/external_results/:id": ({ id }: DetailRoute) => , + "/external_results/:id/update": ({ id }: DetailRoute) => ( + + ), +}; diff --git a/src/Routers/routes/FacilityInventoryRoutes.tsx b/src/Routers/routes/FacilityInventoryRoutes.tsx new file mode 100644 index 00000000000..17e93b2bc60 --- /dev/null +++ b/src/Routers/routes/FacilityInventoryRoutes.tsx @@ -0,0 +1,24 @@ +import { Redirect } from "raviger"; +import InventoryList from "../../Components/Facility/InventoryList"; +import InventoryLog from "../../Components/Facility/InventoryLog"; +import MinQuantityList from "../../Components/Facility/MinQuantityList"; +import { SetInventoryForm } from "../../Components/Facility/SetInventoryForm"; + +export default { + "/facility/:facilityId/inventory": ({ facilityId }: any) => ( + + ), + "/facility/:facilityId/inventory/min_quantity/set": ({ facilityId }: any) => ( + + ), + "/facility/:facilityId/inventory/min_quantity/list": ({ + facilityId, + }: any) => , + "/facility/:facilityId/inventory/min_quantity": ({ facilityId }: any) => ( + + ), + "/facility/:facilityId/inventory/:inventoryId": ({ + facilityId, + inventoryId, + }: any) => , +}; diff --git a/src/Routers/routes/FacilityLocationRoutes.tsx b/src/Routers/routes/FacilityLocationRoutes.tsx new file mode 100644 index 00000000000..c43673b60f5 --- /dev/null +++ b/src/Routers/routes/FacilityLocationRoutes.tsx @@ -0,0 +1,38 @@ +import { AddBedForm } from "../../Components/Facility/AddBedForm"; +import { AddInventoryForm } from "../../Components/Facility/AddInventoryForm"; +import { AddLocationForm } from "../../Components/Facility/AddLocationForm"; +import { BedManagement } from "../../Components/Facility/BedManagement"; +import LocationManagement from "../../Components/Facility/LocationManagement"; + +export default { + "/facility/:facilityId/location": ({ facilityId }: any) => ( + + ), + "/facility/:facilityId/location/:locationId/beds": ({ + facilityId, + locationId, + }: any) => , + "/facility/:facilityId/inventory/add": ({ facilityId }: any) => ( + + ), + "/facility/:facilityId/location/add": ({ facilityId }: any) => ( + + ), + "/facility/:facilityId/location/:locationId/update": ({ + facilityId, + locationId, + }: any) => ( + + ), + "/facility/:facilityId/location/:locationId/beds/add": ({ + facilityId, + locationId, + }: any) => , + "/facility/:facilityId/location/:locationId/beds/:bedId/update": ({ + facilityId, + locationId, + bedId, + }: any) => ( + + ), +}; diff --git a/src/Routers/routes/FacilityRoutes.tsx b/src/Routers/routes/FacilityRoutes.tsx new file mode 100644 index 00000000000..77247df9189 --- /dev/null +++ b/src/Routers/routes/FacilityRoutes.tsx @@ -0,0 +1,45 @@ +import { FacilityConfigure } from "../../Components/Facility/FacilityConfigure"; +import { FacilityCreate } from "../../Components/Facility/FacilityCreate"; +import { FacilityHome } from "../../Components/Facility/FacilityHome"; +import FacilityUsers from "../../Components/Facility/FacilityUsers"; +import { HospitalList } from "../../Components/Facility/HospitalList"; +import { TriageForm } from "../../Components/Facility/TriageForm"; +import ResourceCreate from "../../Components/Resource/ResourceCreate"; +import CentralNursingStation from "../../Components/Facility/CentralNursingStation"; +import FacilityLocationRoutes from "./FacilityLocationRoutes"; +import FacilityInventoryRoutes from "./FacilityInventoryRoutes"; + +export default { + "/facility": () => , + "/facility/create": () => , + "/facility/:facilityId/update": ({ facilityId }: any) => ( + + ), + "/facility/:facilityId/configure": ({ facilityId }: any) => ( + + ), + "/facility/:facilityId/cns": ({ facilityId }: any) => ( + + ), + "/facility/:facilityId": ({ facilityId }: any) => ( + + ), + + "/facility/:facilityId/users": ({ facilityId }: any) => ( + + ), + "/facility/:facilityId/resource/new": ({ facilityId }: any) => ( + + ), + + // Triage related routes + "/facility/:facilityId/triage": ({ facilityId }: any) => ( + + ), + "/facility/:facilityId/triage/:id": ({ facilityId, id }: any) => ( + + ), + + ...FacilityLocationRoutes, + ...FacilityInventoryRoutes, +}; diff --git a/src/Routers/routes/HCXRoutes.tsx b/src/Routers/routes/HCXRoutes.tsx new file mode 100644 index 00000000000..8a36e033c15 --- /dev/null +++ b/src/Routers/routes/HCXRoutes.tsx @@ -0,0 +1,6 @@ +import ConsultationClaims from "../../Components/Facility/ConsultationClaims"; + +export default { + "/facility/:facilityId/patient/:patientId/consultation/:consultationId/claims": + (pathParams: any) => , +}; diff --git a/src/Routers/routes/PatientRoutes.tsx b/src/Routers/routes/PatientRoutes.tsx new file mode 100644 index 00000000000..ae594d767ec --- /dev/null +++ b/src/Routers/routes/PatientRoutes.tsx @@ -0,0 +1,46 @@ +import InvestigationReports from "../../Components/Facility/Investigations/Reports"; +import { FileUpload } from "../../Components/Patient/FileUpload"; +import { PatientManager } from "../../Components/Patient/ManagePatients"; +import { PatientHome } from "../../Components/Patient/PatientHome"; +import PatientNotes from "../../Components/Patient/PatientNotes"; +import { PatientRegister } from "../../Components/Patient/PatientRegister"; +import { DetailRoute } from "../types"; +import DeathReport from "../../Components/DeathReport/DeathReport"; + +export default { + "/patients": () => , + "/patient/:id": ({ id }: DetailRoute) => , + "/patient/:id/investigation_reports": ({ id }: DetailRoute) => ( + + ), + + // Facility Scoped Routes + "/facility/:facilityId/patient": ({ facilityId }: any) => ( + + ), + "/facility/:facilityId/patient/:id": ({ facilityId, id }: any) => ( + + ), + "/facility/:facilityId/patient/:id/update": ({ facilityId, id }: any) => ( + + ), + "/facility/:facilityId/patient/:patientId/notes": ({ + facilityId, + patientId, + }: any) => , + "/facility/:facilityId/patient/:patientId/files": ({ + facilityId, + patientId, + }: any) => ( + + ), + "/death_report/:id": ({ id }: any) => , +}; diff --git a/src/Routers/routes/ResourceRoutes.tsx b/src/Routers/routes/ResourceRoutes.tsx new file mode 100644 index 00000000000..8408ab4d79d --- /dev/null +++ b/src/Routers/routes/ResourceRoutes.tsx @@ -0,0 +1,25 @@ +import { DndProvider } from "react-dnd"; +import { HTML5Backend } from "react-dnd-html5-backend"; +import ResourceDetails from "../../Components/Resource/ResourceDetails"; +import { ResourceDetailsUpdate } from "../../Components/Resource/ResourceDetailsUpdate"; +import ListView from "../../Components/Resource/ListView"; +import BoardView from "../../Components/Resource/ResourceBoardView"; +import { Redirect } from "raviger"; +import { DetailRoute } from "../types"; + +const getDefaultView = () => + localStorage.getItem("defaultResourceView") === "list" ? "list" : "board"; + +export default { + "/resource": () => , + "/resource/board": () => ( + + + + ), + "/resource/list": () => , + "/resource/:id": ({ id }: DetailRoute) => , + "/resource/:id/update": ({ id }: DetailRoute) => ( + + ), +}; diff --git a/src/Routers/routes/SampleRoutes.tsx b/src/Routers/routes/SampleRoutes.tsx new file mode 100644 index 00000000000..290a34fd4eb --- /dev/null +++ b/src/Routers/routes/SampleRoutes.tsx @@ -0,0 +1,25 @@ +import { SampleDetails } from "../../Components/Patient/SampleDetails"; +import SampleReport from "../../Components/Patient/SamplePreview"; +import { SampleTest } from "../../Components/Patient/SampleTest"; +import SampleViewAdmin from "../../Components/Patient/SampleViewAdmin"; +import { DetailRoute, RouteParams } from "../types"; + +export default { + "/sample": () => , + "/sample/:id": ({ id }: DetailRoute) => , + "/patient/:patientId/test_sample/:sampleId/icmr_sample": ({ + patientId, + sampleId, + }: RouteParams<"patientId" | "sampleId">) => ( + + ), + "/facility/:facilityId/patient/:patientId/sample-test": ({ + facilityId, + patientId, + }: RouteParams<"facilityId" | "patientId">) => ( + + ), + "/facility/:facilityId/patient/:patientId/sample/:id": ({ + id, + }: DetailRoute) => , +}; diff --git a/src/Routers/routes/ShiftingRoutes.tsx b/src/Routers/routes/ShiftingRoutes.tsx new file mode 100644 index 00000000000..9b20b4a1a0b --- /dev/null +++ b/src/Routers/routes/ShiftingRoutes.tsx @@ -0,0 +1,27 @@ +import { DndProvider } from "react-dnd"; +import { HTML5Backend } from "react-dnd-html5-backend"; +import { ShiftCreate } from "../../Components/Patient/ShiftCreate"; +import ShiftDetails from "../../Components/Shifting/ShiftDetails"; +import { ShiftDetailsUpdate } from "../../Components/Shifting/ShiftDetailsUpdate"; +import ListView from "../../Components/Shifting/ListView"; +import BoardView from "../../Components/Shifting/BoardView"; +import { Redirect } from "raviger"; + +const getDefaultView = () => + localStorage.getItem("defaultShiftView") === "list" ? "list" : "board"; + +export default { + "/shifting": () => , + "/shifting/board": () => ( + + + + ), + "/shifting/list": () => , + "/shifting/:id": ({ id }: any) => , + "/shifting/:id/update": ({ id }: any) => , + "/facility/:facilityId/patient/:patientId/shift/new": ({ + facilityId, + patientId, + }: any) => , +}; diff --git a/src/Routers/routes/UserRoutes.tsx b/src/Routers/routes/UserRoutes.tsx new file mode 100644 index 00000000000..56877ca4c78 --- /dev/null +++ b/src/Routers/routes/UserRoutes.tsx @@ -0,0 +1,9 @@ +import ManageUsers from "../../Components/Users/ManageUsers"; +import { UserAdd } from "../../Components/Users/UserAdd"; +import UserProfile from "../../Components/Users/UserProfile"; + +export default { + "/users": () => , + "/users/add": () => , + "/user/profile": () => , +}; diff --git a/src/Routers/types.ts b/src/Routers/types.ts new file mode 100644 index 00000000000..dc7138b8df7 --- /dev/null +++ b/src/Routers/types.ts @@ -0,0 +1,5 @@ +export type RouteParams = Record; + +export interface DetailRoute { + id: string; +} diff --git a/src/Utils/request/README.md b/src/Utils/request/README.md index a9948d48f41..b0fa4459ef7 100644 --- a/src/Utils/request/README.md +++ b/src/Utils/request/README.md @@ -56,7 +56,8 @@ const FooRoutes = { path: "/api/v1/foo/{id}/", // 👈 The path to the endpoint. Slug parameters can be specified using curly braces. method: "GET", // 👈 The HTTP method to use. Optional; defaults to "GET". - TRes: Res, // 👈 The type of the response body (for type inference). + TRes: Type(), // 👈 The type of the response body (for type inference). + TBody: Type(), // 👈 The type of the request body (for type inference). noAuth: true, // 👈 Whether to skip adding the Authorization header to the request. }, } as const; // 👈 This is important for type inference to work properly. @@ -78,7 +79,7 @@ const options = { // thrown before the request is made. query: { limit: 10 }, // 👈 The query parameters to be added to the request URL. - body: { name: "foo" }, // 👈 The body to be sent with the request. + body: { name: "foo" }, // 👈 The body to be sent with the request. Should be compatible with the TBody type of the route. headers: { "X-Foo": "bar" }, // 👈 Additional headers to be sent with the request. (Coming soon...) silent: true, // 👈 Whether to suppress notifications for this request. @@ -103,7 +104,7 @@ The `useQuery` hook returns an object with the following properties: ```ts { - res: Res | undefined; // 👈 The response object. `undefined` if the request has not been made yet. + res: Type | undefined; // 👈 The response object. `undefined` if the request has not been made yet. data: TRes | null; // 👈 The response body. `null` if the request has not been made yet. @@ -129,7 +130,7 @@ import FooRoutes from "@foo/routes"; export default async function updateFoo(id: string, object: Foo) { const { res, data } = await request(FooRoutes.updateFoo, { pathParams: { id }, - body: object, // 👈 The body is automatically serialized to JSON. + body: object, // 👈 The body is automatically serialized to JSON. Should be compatible with the TBody type of the route. }); if (res.status === 403) { diff --git a/src/Utils/request/request.ts b/src/Utils/request/request.ts index c946f524ab9..2d735734aae 100644 --- a/src/Utils/request/request.ts +++ b/src/Utils/request/request.ts @@ -2,12 +2,12 @@ import handleResponse from "./handleResponse"; import { RequestOptions, RequestResult, Route } from "./types"; import { makeHeaders, makeUrl } from "./utils"; -interface Options extends RequestOptions { +interface Options extends RequestOptions { controller?: AbortController; } -export default async function request( - { path, method, noAuth }: Route, +export default async function request( + { path, method, noAuth }: Route, { query, body, @@ -16,7 +16,7 @@ export default async function request( onResponse, silent, reattempts = 3, - }: Options = {} + }: Options = {} ): Promise> { const signal = controller?.signal; const url = makeUrl(path, query, pathParams); diff --git a/src/Utils/request/types.ts b/src/Utils/request/types.ts index 7f01ef91d9c..e22d37f3090 100644 --- a/src/Utils/request/types.ts +++ b/src/Utils/request/types.ts @@ -12,11 +12,14 @@ export interface QueryRoute extends RouteBase { method?: "GET"; } -export interface MutationRoute extends RouteBase { +export interface MutationRoute extends RouteBase { method: "POST" | "PUT" | "PATCH" | "DELETE"; + TBody?: TBody; } -export type Route = QueryRoute | MutationRoute; +export type Route = + | QueryRoute + | MutationRoute; export interface RequestResult { res: Response | undefined; @@ -24,9 +27,9 @@ export interface RequestResult { error: undefined | Record; } -export interface RequestOptions { +export interface RequestOptions { query?: QueryParams; - body?: object; + body?: TBody; pathParams?: Record; onResponse?: (res: RequestResult) => void; silent?: boolean;