From 78e157e36a35aec7ad51fd48fa8b32544644ef67 Mon Sep 17 00:00:00 2001 From: jwnasambu <33891016+jwnasambu@users.noreply.github.com> Date: Sat, 17 Aug 2024 23:10:42 +0300 Subject: [PATCH] (fix) O3-3618: Prevent submitting empty lab results form (#79) * (fix) O3-3618: Validates against submission of empty results form * Fixup --------- Co-authored-by: Dennis Kigen --- package.json | 2 +- .../list-order-details.component.tsx | 17 +- .../orders-table/order-detail.component.tsx | 2 +- .../orders-data-table.component.tsx | 26 +-- .../orders-table/orders-data-table.scss | 5 +- src/results/result-form-field.component.tsx | 28 +-- src/results/result-form.component.tsx | 167 +++++++++++------- src/results/result-form.resource.ts | 2 +- src/results/result-form.scss | 12 +- translations/en.json | 4 +- yarn.lock | 12 +- 11 files changed, 154 insertions(+), 123 deletions(-) diff --git a/package.json b/package.json index 3654a2c0..24d6cdd0 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "dependencies": { "@carbon/react": "^1.14.0", "lodash-es": "^4.17.21", - "react-hook-form": "^7.49.3" + "react-hook-form": "^7.52.1" }, "peerDependencies": { "@openmrs/esm-framework": "*", diff --git a/src/components/orders-table/list-order-details.component.tsx b/src/components/orders-table/list-order-details.component.tsx index b3654e4a..f11f540a 100644 --- a/src/components/orders-table/list-order-details.component.tsx +++ b/src/components/orders-table/list-order-details.component.tsx @@ -9,9 +9,10 @@ import ResultForm from "../../results/result-form.component"; import styles from "./list-order-details.scss"; const ListOrderDetails: React.FC = (props) => { + const { t } = useTranslation(); const orders = props.groupedOrders?.orders; const patientId = props.groupedOrders?.patientId; - const { t } = useTranslation(); + return (
{orders && @@ -31,6 +32,7 @@ const ListOrderDetails: React.FC = (props) => { return (
= (props) => { value={row.orderNumber} /> - = ({

{label} - {" : "} + {": "} {value}

diff --git a/src/components/orders-table/orders-data-table.component.tsx b/src/components/orders-table/orders-data-table.component.tsx index d788def5..4cfd9228 100644 --- a/src/components/orders-table/orders-data-table.component.tsx +++ b/src/components/orders-table/orders-data-table.component.tsx @@ -1,4 +1,5 @@ import React, { useMemo, useState } from "react"; +import dayjs from "dayjs"; import { useTranslation } from "react-i18next"; import { DataTable, @@ -23,7 +24,6 @@ import { TableToolbarSearch, Tile, } from "@carbon/react"; -import dayjs from "dayjs"; import { formatDate, parseDate, @@ -57,18 +57,18 @@ const OrdersDataTable: React.FC = (props) => { activatedOnOrAfterDate ); - const flattenedLabOrders = useMemo(() => { - return labOrders.map((eachObject) => { - return { - ...eachObject, - dateActivated: formatDate(parseDate(eachObject.dateActivated)), - patientName: eachObject.patient?.display.split("-")[1], - patientUuid: eachObject.patient?.uuid, - status: eachObject.fulfillerStatus ?? "--", - orderer: eachObject.orderer?.display.split("-")[1], - }; - }); - }, [labOrders]); + const flattenedLabOrders = useMemo( + () => + labOrders.map((labOrder) => ({ + ...labOrder, + dateActivated: formatDate(parseDate(labOrder.dateActivated)), + patientName: labOrder.patient?.display.split("-")[1], + patientUuid: labOrder.patient?.uuid, + status: labOrder.fulfillerStatus ?? "--", + orderer: labOrder.orderer, + })), + [labOrders] + ); function groupOrdersById(orders) { if (orders && orders.length > 0) { diff --git a/src/components/orders-table/orders-data-table.scss b/src/components/orders-table/orders-data-table.scss index eb139651..fb1c2651 100644 --- a/src/components/orders-table/orders-data-table.scss +++ b/src/components/orders-table/orders-data-table.scss @@ -26,7 +26,6 @@ :global(.cds--table-toolbar) { position: static; - margin: 0.5rem; } :global(.cds--overflow-menu) { @@ -48,12 +47,12 @@ } .toolbarItem { + margin-left: 1rem; display: flex; - margin: 5px 10px; align-items: center; & p { - padding-right: 5px; + padding-right: 0.5rem; @include type.type-style('body-01'); color: colors.$gray-70; } diff --git a/src/results/result-form-field.component.tsx b/src/results/result-form-field.component.tsx index 15acc2df..dff89db5 100644 --- a/src/results/result-form-field.component.tsx +++ b/src/results/result-form-field.component.tsx @@ -1,22 +1,22 @@ import React from "react"; -import styles from "./result-form.scss"; -import { TextInput, Select, SelectItem } from "@carbon/react"; +import { Select, SelectItem, TextInput } from "@carbon/react"; +import { Controller } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { ConceptReference } from "./result-form.resource"; -import { Controller } from "react-hook-form"; +import styles from "./result-form.scss"; interface ResultFormFieldProps { concept: ConceptReference; control: any; register: any; - errors: any; } + const ResultFormField: React.FC = ({ concept, control, - errors, }) => { const { t } = useTranslation(); + const isTextOrNumeric = (concept) => concept.datatype?.display === "Text" || concept.datatype?.display === "Numeric"; @@ -44,17 +44,9 @@ const ResultFormField: React.FC = ({ return ( <> - {Object.keys(errors).length > 0 && ( -
- {t("allFieldsRequired", "All fields are required")} -
- )} {isTextOrNumeric(concept) && ( ( = ({ ( = ({ order, patientUuid }) => { const { t } = useTranslation(); const session = useSession(); const { laboratoryOrderTypeUuid, encounterTypeUuid } = useConfig(); + const [showEmptyFormErrorNotification, setShowEmptyFormErrorNotification] = + useState(false); + const { control, - register, - formState: { isSubmitting, errors }, + formState: { isSubmitting }, getValues, handleSubmit, - } = useForm<{ testResult: string }>({ + register, + } = useForm<{ + testResult: string; + }>({ defaultValues: {}, }); + const isTablet = useLayoutType() === "tablet"; const { patient, isLoading: isLoadingPatient } = usePatient(patientUuid); const { concept, isLoading: isLoadingConcepts } = useGetOrderConceptByUuid( order.concept.uuid ); - const bannerState = useMemo(() => { - if (patient) { - return { - patient, - patientUuid, - hideActionsOverflow: true, - }; + + const bannerState = useMemo( + () => ({ + patient, + patientUuid, + hideActionsOverflow: true, + }), + [patient, patientUuid] + ); + + const onSubmit = () => { + const formValues = getValues(); + const isEmptyForm = Object.values(formValues).every( + (value) => value === "" || value === null || value === undefined + ); + + if (isEmptyForm) { + setShowEmptyFormErrorNotification(true); + return; } - }, [patient, patientUuid]); - const onSubmit = (data, e) => { - e.preventDefault(); let obsValue = []; - const submissionDatetime = new Date().toISOString(); if (concept.set && concept.setMembers.length > 0) { @@ -72,10 +93,10 @@ const ResultForm: React.FC = ({ order, patientUuid }) => { member.datatype.display === "Numeric" || member.datatype.display === "Text" ) { - value = getValues()[`${member.uuid}`]; + value = getValues()[member.uuid]; } else if (member.datatype.display === "Coded") { value = { - uuid: getValues()[`${member.uuid}`], + uuid: getValues()[member.uuid], }; } const groupMember = { @@ -95,14 +116,15 @@ const ResultForm: React.FC = ({ order, patientUuid }) => { }); } else if (!concept.set && concept.setMembers.length === 0) { let value; + if ( concept.datatype.display === "Numeric" || concept.datatype.display === "Text" ) { - value = getValues()[`${concept.uuid}`]; + value = getValues()[concept.uuid]; } else if (concept.datatype.display === "Coded") { value = { - uuid: getValues()[`${concept.uuid}`], + uuid: getValues()[concept.uuid], }; } @@ -113,7 +135,10 @@ const ResultForm: React.FC = ({ order, patientUuid }) => { value: value, obsDatetime: submissionDatetime, }); + + setShowEmptyFormErrorNotification(false); } + const encounterPayload = { encounterDatetime: submissionDatetime, patient: patientUuid, @@ -130,12 +155,12 @@ const ResultForm: React.FC = ({ order, patientUuid }) => { encounter: order.encounter.uuid, patient: order.patient.uuid, concept: order.concept.uuid, - orderer: order.orderer, + orderer: order.orderer?.uuid, }; - updateOrderResult(encounterPayload, orderDiscontinuationPayload).then( - (response) => { - if (response.ok) { + updateOrderResult(encounterPayload, orderDiscontinuationPayload) + .then( + (res) => { showSnackbar({ isLowContrast: true, title: t("updateEncounter", "Update lab results"), @@ -158,6 +183,7 @@ const ResultForm: React.FC = ({ order, patientUuid }) => { "You have successfully completed the test order" ), }); + mutate( (key) => typeof key === "string" && @@ -167,12 +193,11 @@ const ResultForm: React.FC = ({ order, patientUuid }) => { undefined, { revalidate: true } ); - closeOverlay(); }, (err) => { showNotification({ title: t( - `errorMarkingOrderFulfillStatus`, + "errorMarkingOrderFulfillStatus", "Error occurred while marking order fulfill status" ), kind: "error", @@ -182,51 +207,67 @@ const ResultForm: React.FC = ({ order, patientUuid }) => { } ); - return response; + return res; + }, + (err) => { + showNotification({ + title: t("errorUpdatingEncounter", "Error updating test results"), + kind: "error", + critical: true, + description: err?.message, + }); } - }, - (err) => { - showNotification({ - title: t("errorUpdatingEncounter", "Error updating test results"), - kind: "error", - critical: true, - description: err?.message, - }); - } - ); + ) + .finally(() => { + closeOverlay(); + }); + + setShowEmptyFormErrorNotification(false); }; + if (isLoadingPatient || isLoadingConcepts) { return ; } + return (
- - - - -
- {concept.setMembers.length > 0 &&
{concept.display}
} - {concept && ( - - )} -
+ + + {concept.setMembers.length > 0 &&
{concept.display}
} + {concept && ( + + )} + {showEmptyFormErrorNotification && ( + + )}
diff --git a/src/results/result-form.resource.ts b/src/results/result-form.resource.ts index 5693cfaa..4be536ca 100644 --- a/src/results/result-form.resource.ts +++ b/src/results/result-form.resource.ts @@ -308,7 +308,7 @@ export interface ObPayload { } // get order concept -export async function GetOrderConceptByUuid(uuid: string) { +export async function getOrderConceptByUuid(uuid: string) { const abortController = new AbortController(); return openmrsFetch(`${restBaseUrl}/concept/${uuid}?v=full`, { headers: { diff --git a/src/results/result-form.scss b/src/results/result-form.scss index 3adf0f33..44ae9c6a 100644 --- a/src/results/result-form.scss +++ b/src/results/result-form.scss @@ -1,9 +1,9 @@ -@use '@carbon/styles/scss/spacing'; -@use '@carbon/styles/scss/type'; +@use '@carbon/layout'; +@use '@carbon/type'; @import '~@openmrs/esm-styleguide/src/vars'; .container { - margin: spacing.$spacing-05; + margin: layout.$spacing-05; } .button { @@ -23,7 +23,7 @@ } .tablet { - padding: 1.5rem spacing.$spacing-05; + padding: 1.5rem layout.$spacing-05; background-color: $ui-02; margin-top: auto; } @@ -33,6 +33,10 @@ margin-top: auto; } +.emptyFormError { + margin: 0.625rem 0; +} + .form { display: flex; flex-direction: column; diff --git a/translations/en.json b/translations/en.json index ad4b72df..b51a9158 100644 --- a/translations/en.json +++ b/translations/en.json @@ -1,6 +1,5 @@ { "all": "All", - "allFieldsRequired": "All fields are required", "cancel": "Cancel", "checkFilters": "Please check the filters above and try again", "chooseOption": "Choose an option", @@ -9,6 +8,7 @@ "date": "Date", "declinedStatus": "DECLINED", "discard": "Discard", + "error": "Error", "errorMarkingOrderFulfillStatus": "Error occurred while marking order fulfill status", "errorPickingOrder', 'Error picking order": "", "errorRejectingRequest": "Error rejecting lab request", @@ -39,6 +39,7 @@ "pickRequest": "Pick lab request", "pickRequestConfirmationText": "Continuing will update the request status to \"In Progress\" and advance it to the next stage. Are you sure you want to proceed?", "pickupLabRequest": "Pick up lab request", + "pleaseFillField": "Please fill at least one field", "previousPage": "Previous page", "procedure": "procedure", "receivedStatus": "RECEIVED", @@ -48,6 +49,7 @@ "rejectLabRequestTitle": "Lab request rejected", "results": "Results", "save": "Save", + "saving": "Saving", "searchThisList": "Search this list", "status": "Status", "tabletOverlay": "Tablet overlay", diff --git a/yarn.lock b/yarn.lock index e50d2250..7f63929f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2974,7 +2974,7 @@ __metadata: raw-loader: "npm:^4.0.2" react: "npm:^18.2.0" react-dom: "npm:^18.2.0" - react-hook-form: "npm:^7.49.3" + react-hook-form: "npm:^7.52.1" react-i18next: "npm:^11.18.6" react-router-dom: "npm:^6.11.2" swc-loader: "npm:^0.2.3" @@ -16237,12 +16237,12 @@ __metadata: languageName: node linkType: hard -"react-hook-form@npm:^7.49.3": - version: 7.50.0 - resolution: "react-hook-form@npm:7.50.0" +"react-hook-form@npm:^7.52.1": + version: 7.52.2 + resolution: "react-hook-form@npm:7.52.2" peerDependencies: - react: ^16.8.0 || ^17 || ^18 - checksum: 10/3b85cc179053af72a2734f2e77767de8f9b3ecbefeee282b73e81141c4b7bb97308ec00da61fdc25a28299a2defb74bff66417bb85a66357f5ceddba7b697ae7 + react: ^16.8.0 || ^17 || ^18 || ^19 + checksum: 10/91a738881d9463fb73794374a5aec17b3fac41aac92ddf64ccfa205fd2ebc211376a6a41a8c579fd256cda4aae4d64b0c1f3e872e361b9673edef5e2f0b75c35 languageName: node linkType: hard