Skip to content
Open
12 changes: 9 additions & 3 deletions app/components/clinic/PatientForm/PatientForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,13 @@ export const PatientForm = (props) => {
const dispatch = useDispatch();
const selectedClinicId = useSelector((state) => state.blip.selectedClinicId);
const clinic = useSelector(state => state.blip.clinics?.[selectedClinicId]);
const mrnSettings = clinic?.mrnSettings ?? {};
const mrnSettings = useMemo(() => clinic?.mrnSettings ?? {}, [clinic?.mrnSettings]);

const existingMRNs = useSelector(state => state.blip.clinicMRNsForPatientFormValidation)?.filter(mrn => mrn !== patient?.mrn) || [];
const clinicMRNsForPatientFormValidation = useSelector(state => state.blip.clinicMRNsForPatientFormValidation);
// Patient's pre-existing MRN will already exist in clinic, so it needs to not trip the validator
const existingMRNs = useMemo(() => (
clinicMRNsForPatientFormValidation?.filter(mrn => mrn !== patient?.mrn) || []
), [clinicMRNsForPatientFormValidation, patient?.mrn]);

const dateInputFormat = 'MM/DD/YYYY';
const dateMaskFormat = dateInputFormat.replace(/[A-Z]/g, '9');
Expand All @@ -99,6 +103,8 @@ export const PatientForm = (props) => {
const diagnosisTypeSectionRef = useRef(null);
const targetRangePresetSectionRef = useRef(null);

const schema = useMemo(() => validationSchema({ mrnSettings, existingMRNs }), [mrnSettings, existingMRNs]);

const formikContext = useFormik({
initialValues: getFormValues(patient, clinicPatientTags, clinicSites),
onSubmit: (values) => {
Expand Down Expand Up @@ -144,7 +150,7 @@ export const PatientForm = (props) => {

dispatch(actions.async[actionMap[action][context].handler](api, ...handlerArgs));
},
validationSchema: validationSchema({ mrnSettings, existingMRNs }),
validationSchema: schema,
});

const {
Expand Down
205 changes: 49 additions & 156 deletions app/pages/clinicworkspace/ClinicPatients.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ import { TagList } from '../../components/elements/Tag';
import Pagination from '../../components/elements/Pagination';
import TextInput from '../../components/elements/TextInput';
import BgSummaryCell from '../../components/clinic/BgSummaryCell';
import PatientForm from '../../components/clinic/PatientForm';
import PatientLastReviewed from '../../components/clinic/PatientLastReviewed';
import TideDashboardConfigForm, { validateTideConfig } from '../../components/clinic/TideDashboardConfigForm';
import RpmReportConfigForm, { exportRpmReport } from '../../components/clinic/RpmReportConfigForm';
Expand Down Expand Up @@ -101,7 +100,6 @@ import { useIsFirstRender, useLocalStorage, usePrevious, useScrollToTop } from '
import { fieldsAreValid, getCommonFormikFieldProps } from '../../core/forms';

import {
patientSchema as validationSchema,
clinicSiteSchema,
clinicPatientTagSchema,
lastDataFilterOptions,
Expand All @@ -120,6 +118,8 @@ import colorPalette from '../../themes/colorPalette';
import noop from 'lodash/noop';
import { getGlycemicRangesPreset } from '../../core/glycemicRangesUtils';
import ClinicPatientsPrintModal from './ClinicPatientsPrintModal';
import AddPatientDialog from './clinicPatientsDialogs/AddPatientDialog';
import EditPatientDialog from './clinicPatientsDialogs/EditPatientDialog';
Comment thread
henry-tp marked this conversation as resolved.

const { Loader } = vizComponents;
const { reshapeBgClassesToBgBounds, generateBgRangeLabels, formatBgValue } = vizUtils.bg;
Expand Down Expand Up @@ -713,7 +713,6 @@ export const ClinicPatients = (props) => {
const loggedInUserId = useSelector((state) => state.blip.loggedInUserId);
const pdf = useSelector((state) => state.blip.pdf);
const clinic = useSelector(state => state.blip.clinics?.[selectedClinicId]);
const mrnSettings = useMemo(() => clinic?.mrnSettings ?? {}, [clinic?.mrnSettings]);
const timePrefs = useSelector((state) => state.blip.timePrefs);
const rpmReportPatients = useSelector(state => state.blip.rpmReportPatients);
const isClinicAdmin = includes(get(clinic, ['clinicians', loggedInUserId, 'roles'], []), 'CLINIC_ADMIN');
Expand All @@ -736,7 +735,6 @@ export const ClinicPatients = (props) => {
const [selectedClinicSite, setSelectedClinicSite] = useState(null);
const [selectedPatientTag, setSelectedPatientTag] = useState(null);
const [loading, setLoading] = useState(false);
const [patientFormContext, setPatientFormContext] = useState();
const [rpmReportFormContext, setRpmReportFormContext] = useState();
const [tideDashboardFormContext, setTideDashboardFormContext] = useState();
const [clinicSiteFormContext, setClinicSiteFormContext] = useState();
Expand All @@ -759,8 +757,6 @@ export const ClinicPatients = (props) => {
const ldClient = useLDClient();
const ldContext = ldClient.getContext();

const existingMRNs = useSelector(state => state.blip.clinicMRNsForPatientFormValidation)?.filter(mrn => mrn !== selectedPatient?.mrn) || [];

const defaultPatientFetchOptions = useMemo(
() => {
const options = {
Expand Down Expand Up @@ -892,22 +888,20 @@ export const ClinicPatients = (props) => {
});
}, searchDebounceMs), [patientFetchOptions, searchDebounceMs]);

const {
fetchingPatientFromClinic,
fetchingPatientsForClinic,
deletingPatientFromClinic,
updatingClinicPatient,
creatingClinicCustodialAccount,
sendingPatientUploadReminder,
creatingClinicSite,
updatingClinicSite,
creatingClinicPatientTag,
updatingClinicPatientTag,
deletingClinicSite,
deletingClinicPatientTag,
fetchingTideDashboardPatients,
fetchingRpmReportPatients,
} = useSelector((state) => state.blip.working);
const fetchingPatientFromClinic = useSelector((state) => state.blip.working.fetchingPatientFromClinic);
const fetchingPatientsForClinic = useSelector((state) => state.blip.working.fetchingPatientsForClinic);
const deletingPatientFromClinic = useSelector((state) => state.blip.working.deletingPatientFromClinic);
const updatingClinicPatient = useSelector((state) => state.blip.working.updatingClinicPatient);
const creatingClinicCustodialAccount = useSelector((state) => state.blip.working.creatingClinicCustodialAccount);
const sendingPatientUploadReminder = useSelector((state) => state.blip.working.sendingPatientUploadReminder);
const creatingClinicSite = useSelector((state) => state.blip.working.creatingClinicSite);
const updatingClinicSite = useSelector((state) => state.blip.working.updatingClinicSite);
const creatingClinicPatientTag = useSelector((state) => state.blip.working.creatingClinicPatientTag);
const updatingClinicPatientTag = useSelector((state) => state.blip.working.updatingClinicPatientTag);
const deletingClinicSite = useSelector((state) => state.blip.working.deletingClinicSite);
const deletingClinicPatientTag = useSelector((state) => state.blip.working.deletingClinicPatientTag);
const fetchingTideDashboardPatients = useSelector((state) => state.blip.working.fetchingTideDashboardPatients);
const fetchingRpmReportPatients = useSelector((state) => state.blip.working.fetchingRpmReportPatients);

@henry-tp henry-tp Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By calling the non-specific useSelector(state => state.blip.working) which returns the entire working state, we cause the component to re-render every time any part of state.blip.working changes, regardless of whether it is a slice we are interested in. So we may reduce re-rendering by returning specific slices instead of state.blip.working as a whole. It may be marginally helpful.


const { patientListSearchTextInput, isPatientListVisible } = useSelector(({ blip }) => blip.patientListFilters);

Expand Down Expand Up @@ -952,7 +946,6 @@ export const ClinicPatients = (props) => {
}

setTimeout(() => {
setPatientFormContext(null);
setSelectedPatient(null);
});
}, [
Expand Down Expand Up @@ -1399,22 +1392,6 @@ export const ClinicPatients = (props) => {
setShowAddPatientDialog(true);
}

const handleAddPatientConfirm = useCallback(() => {
trackMetric('Clinic - Add patient confirmed', { clinicId: selectedClinicId });
patientFormContext?.handleSubmit();
}, [patientFormContext, selectedClinicId, trackMetric]);

const handleEditPatientConfirm = useCallback(() => {
trackMetric('Clinic - Edit patient confirmed', { clinicId: selectedClinicId });
const updatedTags = [...(patientFormContext?.values?.tags || [])];
const existingTags = [...(selectedPatient?.tags || [])];

if (!isEqual(updatedTags.sort(), existingTags.sort())) {
trackMetric(prefixPopHealthMetric('Edit patient tags confirm'), { clinicId: selectedClinicId });
}
patientFormContext?.handleSubmit();
}, [patientFormContext, selectedClinicId, trackMetric, selectedPatient?.tags, prefixPopHealthMetric]);

function handleConfigureTideDashboard() {
if (validateTideConfig(tideDashboardConfig[localConfigKey], patientTags)) {
trackMetric('Clinic - Navigate to Tide Dashboard', { clinicId: selectedClinicId, source: 'Patients list' });
Expand Down Expand Up @@ -1504,10 +1481,6 @@ export const ClinicPatients = (props) => {
dispatch(actions.async.sendPatientUploadReminder(api, selectedClinicId, selectedPatient?.id));
}, [api, dispatch, prefixPopHealthMetric, selectedClinicId, selectedPatient?.id, trackMetric]);

function handlePatientFormChange(formikContext) {
setPatientFormContext({ ...formikContext });
}

function handleTideDashboardConfigFormChange(formikContext) {
setTideDashboardFormContext({ ...formikContext });
}
Expand Down Expand Up @@ -1568,11 +1541,11 @@ export const ClinicPatients = (props) => {
setActiveSort,
]);

function handleClearSearch() {
const handleClearSearch = useCallback(() => {
dispatch(actions.sync.setPatientListSearchTextInput(''));
setLoading(true);
debounceSearch('');
}
}, [debounceSearch, dispatch]);

const handlePageChange = useCallback((event, page) => {
setPatientFetchOptions({
Expand All @@ -1581,11 +1554,11 @@ export const ClinicPatients = (props) => {
});
}, [patientFetchOptions]);

function handleResetFilters() {
const handleResetFilters = useCallback(() => {
trackMetric(prefixPopHealthMetric('Clear all filters'), { clinicId: selectedClinicId });
setActiveFilters(defaultFilterState);
setPendingFilters(defaultFilterState);
}
}, [prefixPopHealthMetric, selectedClinicId, setActiveFilters, trackMetric]);

const handleFilterTimeInRange = useCallback(() => {
trackMetric(prefixPopHealthMetric('Time in range apply filter'), {
Expand Down Expand Up @@ -3053,110 +3026,20 @@ export const ClinicPatients = (props) => {
}, [handleDeleteClinicPatientTagConfirm, handleCloseClinicPatientTagUpdateDialog, selectedPatientTag?.name, showDeleteClinicPatientTagDialog, t]);

const renderAddPatientDialog = useCallback(() => {
return (
<Dialog
id="addPatient"
aria-labelledby="dialog-title"
open={showAddPatientDialog}
onClose={handleCloseOverlays}
>
<DialogTitle onClose={handleCloseOverlays}>
<MediumTitle id="dialog-title">{t('Add New Patient Account')}</MediumTitle>
</DialogTitle>
if (!showAddPatientDialog) return null;

<DialogContent>
<PatientForm api={api} trackMetric={trackMetric} onFormChange={handlePatientFormChange} searchDebounceMs={searchDebounceMs} action="create" />
</DialogContent>

<DialogActions>
<Button id="addPatientCancel" variant="secondary" onClick={handleCloseOverlays}>
{t('Cancel')}
</Button>
<Button
id="addPatientConfirm"
variant="primary"
onClick={handleAddPatientConfirm}
processing={creatingClinicCustodialAccount.inProgress}
disabled={!fieldsAreValid(keys(patientFormContext?.values), validationSchema({mrnSettings, existingMRNs}), patientFormContext?.values)}
>
{t('Add Patient')}
</Button>
</DialogActions>
</Dialog>
return (
<AddPatientDialog api={api} onClose={handleCloseOverlays} />
);
}, [
api,
creatingClinicCustodialAccount.inProgress,
handleAddPatientConfirm,
mrnSettings,
existingMRNs,
patientFormContext?.values,
showAddPatientDialog,
t,
trackMetric
]);
}, [api, showAddPatientDialog, handleCloseOverlays]);

const renderEditPatientDialog = useCallback(() => {
return (
<Dialog
id="editPatient"
aria-labelledby="dialog-title"
open={showEditPatientDialog}
onClose={handleCloseOverlays}
>
<DialogTitle onClose={() => {
trackMetric('Clinic - Edit patient close', { clinicId: selectedClinicId });
handleCloseOverlays()
}}>
<MediumTitle id="dialog-title">{t('Edit Patient Details')}</MediumTitle>
</DialogTitle>
if (!showEditPatientDialog || !selectedPatient) return null;

<DialogContent>
<PatientForm
api={api}
trackMetric={trackMetric}
onFormChange={handlePatientFormChange}
patient={selectedPatient}
searchDebounceMs={searchDebounceMs}
action="edit"
/>
</DialogContent>

<DialogActions>
<Button id="editPatientCancel" variant="secondary" onClick={() => {
trackMetric('Clinic - Edit patient cancel', { clinicId: selectedClinicId, source: 'Patients list' });
handleCloseOverlays()
}}>
{t('Cancel')}
</Button>

<Button
id="editPatientConfirm"
variant="primary"
onClick={handleEditPatientConfirm}
processing={updatingClinicPatient.inProgress}
disabled={!fieldsAreValid(keys(patientFormContext?.values), validationSchema({mrnSettings, existingMRNs}), patientFormContext?.values)}
>
{t('Save Changes')}
</Button>
</DialogActions>
</Dialog>
return (
<EditPatientDialog api={api} patient={selectedPatient} onClose={handleCloseOverlays} />
);
}, [
api,
handleEditPatientConfirm,
mrnSettings,
existingMRNs,
handleCloseOverlays,
patientFormContext,
searchDebounceMs,
selectedClinicId,
selectedPatient,
showEditPatientDialog,
t,
trackMetric,
updatingClinicPatient.inProgress
]);
}, [api, showEditPatientDialog, selectedPatient, handleCloseOverlays]);

const renderTideDashboardConfigDialog = useCallback(() => {
return (
Expand Down Expand Up @@ -4241,13 +4124,26 @@ export const ClinicPatients = (props) => {
},
}), [data?.length, showSummaryData]);

const patientListQueryState = useMemo(
() => getPatientListQueryState(activeFilters, patientListSearchTextInput),
[activeFilters, patientListSearchTextInput]
);

const emptyContentNode = useMemo(() => (
<EmptyContentNode patientListQueryState={patientListQueryState}>
<ClearFilterButtons
patientListQueryState={patientListQueryState}
onClearSearch={handleClearSearch}
onResetFilters={handleResetFilters}
/>
</EmptyContentNode>
), [patientListQueryState, handleClearSearch, handleResetFilters]);

const renderPeopleTable = useCallback(() => {
const pageCount = Math.ceil(clinic?.fetchedPatientCount / patientFetchOptions.limit);
const page = Math.ceil(patientFetchOptions.offset / patientFetchOptions.limit) + 1;
const sort = patientFetchOptions.sort || defaultPatientFetchOptions.sort;

const patientListQueryState = getPatientListQueryState(activeFilters, patientListSearchTextInput);

// Show the Filter Reset Bar only if data exists and any filters/search are applied
const showFilterResetBar = (data?.length > 0) && patientListQueryState !== PATIENT_LIST_QUERY_STATE.NONE;

Expand Down Expand Up @@ -4279,15 +4175,7 @@ export const ClinicPatients = (props) => {
order={sort?.substring(0, 1) === '+' ? 'asc' : 'desc'}
orderBy={sort?.substring(1)}
onClickRow={handleClickPatient}
emptyContentNode={
<EmptyContentNode patientListQueryState={patientListQueryState}>
<ClearFilterButtons
patientListQueryState={patientListQueryState}
onClearSearch={handleClearSearch}
onResetFilters={handleResetFilters}
/>
</EmptyContentNode>
}
emptyContentNode={emptyContentNode}
/>

{pageCount > 1 && (
Expand All @@ -4310,10 +4198,15 @@ export const ClinicPatients = (props) => {
columns,
data,
defaultPatientFetchOptions.sort,
emptyContentNode,
handleClearSearch,
handleClickPatient,
handlePageChange,
handleResetFilters,
handleSortChange,
loading,
patientFetchOptions,
patientListQueryState,
showSummaryData,
tableStyle,
]);
Expand Down
Loading