import React, { useCallback, useEffect, useMemo, useState } from 'react'; import classNames from 'classnames'; import { Button, ButtonSet, Form, Layer, InlineLoading, InlineNotification, Stack } from '@carbon/react'; import { type Control, useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { useSWRConfig } from 'swr'; import { zodResolver } from '@hookform/resolvers/zod'; import { ExtensionSlot, restBaseUrl, showSnackbar, useAbortController, useConfig, useLayoutType, Workspace2, type Workspace2DefinitionProps, } from '@openmrs/esm-framework'; import { useOrderBasket, type Order, type OrderBasketItem } from '@openmrs/esm-patient-common-lib'; import { type ConfigObject } from '../config-schema'; import { type ObservationValue } from '../types/encounter'; import { createCompositeObservationPayload, isCoded, isNumeric, isPanel, isText, updateObservation, updateOrderResult, useCompletedLabResultsArray, useOrderConceptsByUuids, } from './lab-results.resource'; import { createLabResultsFormCompositeSchema } from './lab-results-schema.resource'; import ResultFormField from './lab-results-form-field.component'; import styles from './lab-results-form.scss'; import orderStyles from '../order-basket/order-basket.scss'; export interface LabResultsFormProps { patient: fhir.Patient; order: Order; launchLabOrderForm?: (orderTypeUuid: string, order?: OrderBasketItem) => void; labOrderWorkspaceName?: string; /** Callback to refresh lab orders in the Laboratory app after results are saved. * This ensures the orders list stays in sync across the different tabs in the Laboratory app. * @see https://github.com/openmrs/openmrs-esm-laboratory-app/pull/117 */ invalidateLabOrders?: () => void; } const ExportedLabResultsForm: React.FC> = ({ workspaceProps: { patient, order, invalidateLabOrders, launchLabOrderForm, labOrderWorkspaceName }, launchChildWorkspace, closeWorkspace, }) => { const { t } = useTranslation(); const { enableAddTestsDuringResultEntry } = useConfig(); const abortController = useAbortController(); const isTablet = useLayoutType() === 'tablet'; const isEditMode = order.fulfillerStatus === 'COMPLETED'; const [orderConceptUuids, setOrderConceptUuids] = useState([order.concept.uuid]); const { isLoading: isLoadingResultConcepts, concepts: conceptArray } = useOrderConceptsByUuids(orderConceptUuids); const [showEmptyFormErrorNotification, setShowEmptyFormErrorNotification] = useState(false); const compositeSchema = useMemo(() => createLabResultsFormCompositeSchema(conceptArray), [conceptArray]); const { mutate } = useSWRConfig(); const { orders, clearOrders } = useOrderBasket(patient); const [isSavingOrders, setIsSavingOrders] = useState(false); const { isLoading, completeLabResults, mutate: mutateResults } = useCompletedLabResultsArray(order); const mutateOrderData = useCallback(() => { mutate( (key) => typeof key === 'string' && key.startsWith(`${restBaseUrl}/order?patient=${order.patient.uuid}`), undefined, { revalidate: true }, ); }, [mutate, order.patient.uuid]); const mutateObstreeData = useCallback(() => { mutate( (key) => { if (typeof key === 'string') { // Match both regular SWR keys and useSWRInfinite ('$inf$' is an internal SWR namespace prefix) const obstreePattern = `${restBaseUrl}/obstree?patient=${order.patient.uuid}`; return key.startsWith(obstreePattern) || key.startsWith(`$inf$${obstreePattern}`); } return false; }, undefined, { revalidate: true, }, ); }, [mutate, order.patient.uuid]); const handleCancel = useCallback(() => { clearOrders(); }, [clearOrders]); const handleSave = useCallback(() => { const newConceptUuids = orders.map((order) => order['testType']['conceptUuid']); setOrderConceptUuids([order.concept.uuid, ...newConceptUuids]); clearOrders(); }, [clearOrders, orders, order.concept.uuid]); const { control, formState: { errors, isDirty, isSubmitting }, setValue, handleSubmit, } = useForm>({ defaultValues: {} as Record, resolver: zodResolver(compositeSchema), mode: 'all', }); useEffect(() => { if (Array.isArray(completeLabResults) && completeLabResults.length > 1) { const conceptUuids = completeLabResults.map((r) => r.concept.uuid); setOrderConceptUuids(conceptUuids); } }, [completeLabResults]); const resolvedLaunchLabOrderForm = useMemo(() => { if (launchLabOrderForm) { return launchLabOrderForm; } if (labOrderWorkspaceName) { return (orderTypeUuid: string, basketOrder?: OrderBasketItem) => { launchChildWorkspace(labOrderWorkspaceName, { orderTypeUuid, order: basketOrder }); }; } }, [labOrderWorkspaceName, launchChildWorkspace, launchLabOrderForm]); const extensionProps = useMemo( () => ({ patient, launchLabOrderForm: resolvedLaunchLabOrderForm, }), [patient, resolvedLaunchLabOrderForm], ); const canUseResultEntryAddTests = enableAddTestsDuringResultEntry && !isEditMode && !!resolvedLaunchLabOrderForm; useEffect(() => { conceptArray.forEach((concept, index) => { const completeLabResult = completeLabResults.find((r) => r.concept.uuid === concept.uuid); if (concept && completeLabResult && isEditMode) { if (isCoded(concept) && typeof completeLabResult?.value === 'object' && completeLabResult?.value?.uuid) { setValue(concept.uuid, completeLabResult.value.uuid); } else if (isNumeric(concept) && completeLabResult?.value) { setValue(concept.uuid, parseFloat(completeLabResult.value as string)); } else if (isText(concept) && completeLabResult?.value) { setValue(concept.uuid, completeLabResult?.value); } else if (isPanel(concept)) { concept.setMembers.forEach((member) => { const obs = completeLabResult.groupMembers.find((v) => v.concept.uuid === member.uuid); let value: ObservationValue; if (isCoded(member)) { value = typeof obs?.value === 'object' ? obs.value.uuid : obs?.value; } else if (isNumeric(member)) { value = obs?.value ? parseFloat(obs.value as string) : undefined; } else if (isText(member)) { value = obs?.value; } if (value) setValue(member.uuid, value); }); } } }); }, [conceptArray, completeLabResults, isEditMode, setValue]); if (isLoadingResultConcepts) { return (
); } const saveLabResults = async (formValues: Record) => { const isEmptyForm = Object.values(formValues).every( (value) => value === '' || value === null || value === undefined, ); if (isEmptyForm) { setShowEmptyFormErrorNotification(true); return; } const showNotification = (kind: 'error' | 'success', message: string) => { showSnackbar({ title: kind === 'success' ? t('saveLabResults', 'Save lab results') : t('errorSavingLabResults', 'Error saving lab results'), kind: kind, subtitle: message, }); }; // Handle update operation for completed lab order results if (isEditMode) { const updateTasks = Object.entries(formValues) .filter(([, value]) => value !== undefined && value !== null && value !== '') .map(([conceptUuid, value]) => { let obs = completeLabResults.find((r) => r.concept.uuid === conceptUuid); if (!obs) { for (const result of completeLabResults) { obs = result.groupMembers?.find((m) => m.concept.uuid === conceptUuid); if (obs) break; } } return updateObservation(obs?.uuid, { value }); }); const updateResults = await Promise.allSettled(updateTasks); const failedObsconceptUuids = updateResults.reduce((prev, curr, index) => { if (curr.status === 'rejected') { return [...prev, Object.keys(formValues).at(index)]; } return prev; }, []); // Invalidate caches before closing workspace mutateResults(); mutateObstreeData(); if (failedObsconceptUuids.length) { showNotification('error', 'Could not save obs with concept uuids ' + failedObsconceptUuids.join(', ')); } else { closeWorkspace({ discardUnsavedChanges: true }); showNotification( 'success', t('successfullySavedLabResults', 'Lab results for {{orderNumber}} have been successfully updated', { orderNumber: order?.orderNumber, }), ); } return setShowEmptyFormErrorNotification(false); } // Handle Creation logic // Set the observation status to 'FINAL' as we're not capturing it in the form const obsPayload = createCompositeObservationPayload(conceptArray, order, formValues, 'FINAL'); const orderDiscontinuationPayload = { previousOrder: order.uuid, type: 'testorder', action: 'DISCONTINUE', careSetting: order.careSetting.uuid, encounter: order.encounter.uuid, patient: order.patient.uuid, concept: order.concept.uuid, orderer: order.orderer, }; const resultsStatusPayload = { fulfillerStatus: 'COMPLETED', fulfillerComment: 'Test Results Entered', }; try { await updateOrderResult( order.uuid, order.encounter.uuid, obsPayload, resultsStatusPayload, orderDiscontinuationPayload, abortController, ); // Invalidate all caches before closing workspace mutateOrderData(); mutateResults(); invalidateLabOrders?.(); mutateObstreeData(); closeWorkspace({ discardUnsavedChanges: true }); showNotification( 'success', t('successfullySavedLabResults', 'Lab results for {{orderNumber}} have been successfully updated', { orderNumber: order?.orderNumber, }), ); } catch (err) { showNotification('error', err?.message); } finally { setShowEmptyFormErrorNotification(false); } }; return (
{conceptArray?.length > 0 && ( {!isLoading ? ( conceptArray.map((c) => ( r.concept.uuid === c.uuid)} concept={c} control={control as unknown as Control>} /> )) ) : ( )} {canUseResultEntryAddTests && (
{t('addOrderTests', 'Add Tests to this order')}
)} {canUseResultEntryAddTests && orders?.length > 0 && (
)}
)} {showEmptyFormErrorNotification && ( )}
); }; export default ExportedLabResultsForm;