Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(feat) O3-2180 & O3-2300: Allow creating past visits and set visit stop date and time in the form #1704

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { showModal } from '@openmrs/esm-framework';
import { launchPatientWorkspace } from '@openmrs/esm-patient-common-lib';

interface AddPastVisitOverflowMenuItemProps {
patientUuid?: string;
Expand All @@ -13,11 +14,10 @@ const AddPastVisitOverflowMenuItem: React.FC<AddPastVisitOverflowMenuItemProps>
}) => {
const { t } = useTranslation();

const openModal = useCallback(() => {
const dispose = showModal('start-visit-dialog', {
patientUuid,
launchPatientChart,
closeModal: () => dispose(),
const openVisitForm = useCallback(() => {
launchPatientWorkspace('start-visit-workspace-form', {
isCreatingVisit: true,
workspaceTitle: t('addPastVisit', 'Add past visit'),
});
}, [patientUuid, launchPatientChart]);
Copy link
Member

Choose a reason for hiding this comment

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

This dependency array no longer seems accurate.


Expand All @@ -28,7 +28,7 @@ const AddPastVisitOverflowMenuItem: React.FC<AddPastVisitOverflowMenuItemProps>
role="menuitem"
title={t('addPastVisit', 'Add past visit')}
data-floating-menu-primary-focus
onClick={openModal}
onClick={openVisitForm}
style={{
maxWidth: '100vw',
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import React from 'react';
import AddPastVisitOverflowMenuItem from './add-past-visit.component';
import { screen, render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { showModal } from '@openmrs/esm-framework';
import { launchPatientWorkspace } from '@openmrs/esm-patient-common-lib';

const mockShowModal = showModal as jest.Mock;
const mockLaunchPatientWorkspace = launchPatientWorkspace as jest.Mock;

jest.mock('@openmrs/esm-framework', () => {
const originalModule = jest.requireActual('@openmrs/esm-framework');
return { originalModule, showModal: jest.fn() };
jest.mock('@openmrs/esm-patient-common-lib', () => {
const originalModule = jest.requireActual('@openmrs/esm-patient-common-lib');
return { originalModule, launchPatientWorkspace: jest.fn() };
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
return { originalModule, launchPatientWorkspace: jest.fn() };
return { ...originalModule, launchPatientWorkspace: jest.fn() };

});

describe('AddPastVisitOverflowMenuItem', () => {
Expand All @@ -23,6 +23,9 @@ describe('AddPastVisitOverflowMenuItem', () => {
// should launch the form
await user.click(addPastVisitButton);

expect(mockShowModal).toHaveBeenCalled();
expect(mockLaunchPatientWorkspace).toHaveBeenCalledWith('start-visit-workspace-form', {
isCreatingVisit: true,
workspaceTitle: 'Add past visit',
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,38 @@ import React from 'react';
import styles from './visit-form.scss';
import { Controller, useFormContext } from 'react-hook-form';
import { type VisitFormData } from './visit-form.resource';
import { DatePicker, DatePickerInput, SelectItem, TimePicker, TimePickerSelect } from '@carbon/react';
import {
DatePicker,
DatePickerInput,
SelectItem,
TimePicker,
TimePickerSelect,
InlineNotification,
} from '@carbon/react';
import classNames from 'classnames';
import { useLayoutType, ResponsiveWrapper } from '@openmrs/esm-framework';
import { useTranslation } from 'react-i18next';
import { type amPm } from '@openmrs/esm-patient-common-lib';

interface VisitDateTimeFieldProps {
visitDatetimeLabel: string;
dateFieldName: 'visitStartDate' | 'visitStopDate';
timeFieldName: 'visitStartTime' | 'visitStopTime';
timeFormatFieldName: 'visitStartTimeFormat' | 'visitStopTimeFormat';
minDate?: number;
maxDate?: number;
datetimeFieldName: 'visitStartDatetime' | 'visitStopDatetime';
}

const VisitDateTimeField: React.FC<VisitDateTimeFieldProps> = ({
visitDatetimeLabel,
dateFieldName,
timeFieldName,
timeFormatFieldName,
minDate,
maxDate,
}) => {
const VisitDateTimeField: React.FC<VisitDateTimeFieldProps> = ({ visitDatetimeLabel, datetimeFieldName }) => {
const { t } = useTranslation();
const isTablet = useLayoutType() === 'tablet';
const {
control,
formState: { errors },
setError,
} = useFormContext<VisitFormData>();

// Since we have the separate date and time fields, the final validation needs to be done at the form
// submission, hence just using the min date with hours/ minutes/ seconds set to 0 and max date set to
// last second of the day. We want to just compare dates and not time.
minDate = minDate ? new Date(minDate).setHours(0, 0, 0, 0) : null;
maxDate = maxDate ? new Date(maxDate).setHours(23, 59, 59, 59) : null;
const dateFieldName: 'visitStartDatetime.date' | 'visitStopDatetime.date' = `${datetimeFieldName}.date`;
const timeFieldName: 'visitStartDatetime.time' | 'visitStopDatetime.time' = `${datetimeFieldName}.time`;
const timeFormatFieldName:
| 'visitStartDatetime.timeFormat'
| 'visitStopDatetime.timeFormat' = `${datetimeFieldName}.timeFormat`;

return (
<section>
Expand All @@ -45,25 +42,23 @@ const VisitDateTimeField: React.FC<VisitDateTimeFieldProps> = ({
<Controller
name={dateFieldName}
control={control}
render={({ field: { onBlur, onChange, value } }) => (
render={({ field: dateField }) => (
<ResponsiveWrapper>
<DatePicker
{...dateField}
dateFormat="d/m/Y"
datePickerType="single"
id={dateFieldName}
style={{ paddingBottom: '1rem' }}
minDate={minDate}
maxDate={maxDate}
onChange={([date]) => onChange(date)}
value={value}
onChange={([date]) => dateField.onChange(date)}
invalid={errors?.[datetimeFieldName]?.date?.message}
invalidText={errors?.[datetimeFieldName]?.date?.message}
>
<DatePickerInput
id={`${dateFieldName}Input`}
labelText={t('date', 'Date')}
placeholder="dd/mm/yyyy"
style={{ width: '100%' }}
invalid={errors[dateFieldName]?.message}
invalidText={errors[dateFieldName]?.message}
/>
</DatePicker>
</ResponsiveWrapper>
Expand All @@ -73,32 +68,31 @@ const VisitDateTimeField: React.FC<VisitDateTimeFieldProps> = ({
<Controller
name={timeFieldName}
control={control}
render={({ field: { onBlur, onChange, value } }) => (
render={({ field: timeField }) => (
<TimePicker
{...timeField}
id={timeFieldName}
labelText={t('time', 'Time')}
onChange={(event) => onChange(event.target.value as amPm)}
onChange={(event) => timeField.onChange(event.target.value as amPm)}
pattern="^(1[0-2]|0?[1-9]):([0-5]?[0-9])$"
style={{ marginLeft: '0.125rem', flex: 'none' }}
value={value}
onBlur={onBlur}
invalid={errors[timeFieldName]?.message}
invalidText={errors[timeFieldName]?.message}
invalid={errors?.[datetimeFieldName]?.time?.message}
invalidText={errors?.[datetimeFieldName]?.time?.message}
>
<Controller
name={timeFormatFieldName}
control={control}
render={({ field: { onChange, value } }) => (
render={({ field: timeFormatField }) => (
<TimePickerSelect
{...timeFormatField}
id={`${timeFormatFieldName}Input`}
onChange={(event) => onChange(event.target.value as amPm)}
value={value}
onChange={(event) => timeFormatField.onChange(event.target.value as amPm)}
aria-label={t('timeFormat ', 'Time Format')}
invalid={errors[timeFormatFieldName]?.message}
invalidText={errors[timeFormatFieldName]?.message}
invalid={errors?.[datetimeFieldName]?.timeFormat?.message}
invalidText={errors?.[datetimeFieldName]?.timeFormat?.message}
>
<SelectItem value="AM" text="AM" />
<SelectItem value="PM" text="PM" />
<SelectItem value="AM" text={t('AM', 'AM')} />
<SelectItem value="PM" text={t('PM', 'PM')} />
</TimePickerSelect>
)}
/>
Expand All @@ -107,6 +101,17 @@ const VisitDateTimeField: React.FC<VisitDateTimeFieldProps> = ({
/>
</ResponsiveWrapper>
</div>
{errors?.[datetimeFieldName]?.root && (
<InlineNotification
aria-label="Close notification"
kind="error"
onClose={() => setError(datetimeFieldName, null)}
statusIconDescription="notification"
subtitle={errors?.[datetimeFieldName]?.root?.message}
title={t('datetimeOutOfRange', 'Selected time is out of range')}
Copy link
Member

Choose a reason for hiding this comment

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

It would seem useful here to tell the user what the time range is?

lowContrast
/>
)}
</section>
);
};
Expand Down