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/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 81282126d7c..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; @@ -254,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] = @@ -342,6 +344,21 @@ const PrescriptionRow = ({ prescription, ...props }: PrescriptionRowProps) => { {t("discontinue")} + { + setShowDetails(false); + setShowEdit(true); + }} + > + + {t("edit")} + {
)} + {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)} 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/Locale/en/Medicine.json b/src/Locale/en/Medicine.json index 8cc80234b74..d32015e7618 100644 --- a/src/Locale/en/Medicine.json +++ b/src/Locale/en/Medicine.json @@ -33,7 +33,9 @@ "last_administered": "Last administered", "modification_caution_note": "No modifications possible once added", "discontinue_caution_note": "Are you sure you want to discontinue this prescription?", + "edit_caution_note": "A new prescription will be added to the consultation with the edited details and the current prescription will be discontinued.", "reason_for_discontinuation": "Reason for discontinuation", + "reason_for_edit": "Reason for edit", "PRESCRIPTION_ROUTE_ORAL": "Oral", "PRESCRIPTION_ROUTE_IV": "IV", "PRESCRIPTION_ROUTE_IM": "IM", @@ -47,4 +49,4 @@ "PRESCRIPTION_FREQUENCY_Q4H": "4th hourly", "PRESCRIPTION_FREQUENCY_QOD": "Alternate day", "PRESCRIPTION_FREQUENCY_QWK": "Once a week" -} +} \ No newline at end of file diff --git a/src/Redux/api.tsx b/src/Redux/api.tsx index f26457183dd..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"; @@ -985,6 +986,8 @@ const routes = { createPrescription: { path: "/api/v1/consultation/{consultation_external_id}/prescriptions/", method: "POST", + TBody: Type(), + TRes: Type(), }, listAdministrations: { @@ -1010,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