diff --git a/README.md b/README.md index f15427537..12021fe08 100644 --- a/README.md +++ b/README.md @@ -182,3 +182,14 @@ should be the version number (e.g., `3.2.1`). The creation of the GitHub release will cause GitHub Actions to publish the packages, completing the release process. > Don't run `npm publish` or `yarn publish`. Use the above process. + +### Bumping the Common Lib version + +Make sure to bump the Common Lib version used here each time you cut a release of Patient Chart. Because Common Lib is marked as a peer dependency and a Webpack module federation shared dependency in the [Appointments app](packages/esm-appointments-app/package.json), the copy of the Common Lib that the framework loads is the first one that gets loaded at runtime when frontend modules are registered. If this happens to be a different version than what the Patient Chart expects, you might get some unexpected behavior in the Patient Chart. You can bump the Common Lib version by running the following command: + +```sh +yarn up @openmrs/esm-patient-common-lib +git checkout package.json +yarn +``` + diff --git a/packages/esm-appointments-app/package.json b/packages/esm-appointments-app/package.json index 8070749a2..8b8574dbb 100644 --- a/packages/esm-appointments-app/package.json +++ b/packages/esm-appointments-app/package.json @@ -44,7 +44,7 @@ }, "peerDependencies": { "@openmrs/esm-framework": "5.x", - "@openmrs/esm-patient-common-lib": "7.x", + "@openmrs/esm-patient-common-lib": "8.x", "react": "18.x", "react-i18next": "11.x", "react-router-dom": "6.x", diff --git a/packages/esm-appointments-app/src/calendar/header/calendar-header.component.tsx b/packages/esm-appointments-app/src/calendar/header/calendar-header.component.tsx index a4a51c0cc..591616d5d 100644 --- a/packages/esm-appointments-app/src/calendar/header/calendar-header.component.tsx +++ b/packages/esm-appointments-app/src/calendar/header/calendar-header.component.tsx @@ -19,6 +19,7 @@ const CalendarHeader: React.FC = () => {
+ {formatDate(new Date(selectedDate), { day: false, time: false, noToday: true })} + +
+
+ {DAYS_IN_WEEK.map((day) => ( + + ))} +
+ + ); +}; + +export default MonthlyHeader; diff --git a/packages/esm-appointments-app/src/calendar/monthly/monthly-header.module.tsx b/packages/esm-appointments-app/src/calendar/monthly/monthly-header.module.tsx deleted file mode 100644 index ef8fe1bcf..000000000 --- a/packages/esm-appointments-app/src/calendar/monthly/monthly-header.module.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React, { useContext } from 'react'; -import dayjs from 'dayjs'; -import styles from './monthly-header.module.scss'; -import { Button } from '@carbon/react'; -import { useTranslation } from 'react-i18next'; -import DaysOfWeekCard from './days-of-week.component'; -import SelectedDateContext from '../../hooks/selectedDateContext'; -import { omrsDateFormat } from '../../constants'; - -const monthFormat = 'MMMM, YYYY'; -const daysInWeek = ['SUN', 'MON', 'TUE', 'WED', 'THUR', 'FRI', 'SAT']; -function MonthlyHeader() { - const { t } = useTranslation(); - const { selectedDate, setSelectedDate } = useContext(SelectedDateContext); - - return ( - <> -
- - {dayjs(selectedDate).format(monthFormat)} - - -
-
- {daysInWeek?.map((day, i) => )} -
- - ); -} -export default MonthlyHeader; diff --git a/packages/esm-appointments-app/src/calendar/monthly/monthly-header.module.scss b/packages/esm-appointments-app/src/calendar/monthly/monthly-header.scss similarity index 100% rename from packages/esm-appointments-app/src/calendar/monthly/monthly-header.module.scss rename to packages/esm-appointments-app/src/calendar/monthly/monthly-header.scss diff --git a/packages/esm-appointments-app/src/calendar/monthly/monthly-view-workload.scss b/packages/esm-appointments-app/src/calendar/monthly/monthly-view-workload.scss index 7a3eaa37e..262dacaa4 100644 --- a/packages/esm-appointments-app/src/calendar/monthly/monthly-view-workload.scss +++ b/packages/esm-appointments-app/src/calendar/monthly/monthly-view-workload.scss @@ -13,10 +13,6 @@ text-align: right; @include type.type-style('body-compact-02'); - &:nth-child(-n + 7) { - border-top: 1px solid colors.$gray-20; - } - &:nth-child(7n) { border-right: 1px solid colors.$gray-20; } diff --git a/packages/esm-appointments-app/src/form/appointments-form.component.tsx b/packages/esm-appointments-app/src/form/appointments-form.component.tsx index 879227ddb..b5c8855e1 100644 --- a/packages/esm-appointments-app/src/form/appointments-form.component.tsx +++ b/packages/esm-appointments-app/src/form/appointments-form.component.tsx @@ -181,11 +181,22 @@ const AppointmentsForm: React.FC = ({ ) .refine( (formValues) => { - const appointmentDate = formValues.appointmentDateTime?.startDate; - const dateAppointmentScheduled = formValues.dateAppointmentScheduled; + const { appointmentDateTime, dateAppointmentScheduled } = formValues; - if (!appointmentDate || !dateAppointmentScheduled) return true; - return dateAppointmentScheduled < appointmentDate; + const startDate = appointmentDateTime?.startDate; + + if (!startDate || !dateAppointmentScheduled) return true; + + const normalizeDate = (date: Date) => { + const normalizedDate = new Date(date); + normalizedDate.setHours(0, 0, 0, 0); + return normalizedDate; + }; + + const startDateObj = normalizeDate(startDate); + const scheduledDateObj = normalizeDate(dateAppointmentScheduled); + + return scheduledDateObj <= startDateObj; }, { path: ['dateAppointmentScheduled'], @@ -195,6 +206,7 @@ const AppointmentsForm: React.FC = ({ ), }, ); + type AppointmentFormData = z.infer; const defaultDateAppointmentScheduled = appointment?.dateAppointmentScheduled diff --git a/packages/esm-appointments-app/src/hooks/useClinicalMetrics.ts b/packages/esm-appointments-app/src/hooks/useClinicalMetrics.ts index 0470a972d..9d016ccb4 100644 --- a/packages/esm-appointments-app/src/hooks/useClinicalMetrics.ts +++ b/packages/esm-appointments-app/src/hooks/useClinicalMetrics.ts @@ -45,11 +45,13 @@ export function useAllAppointmentsByDate() { openmrsFetch, ); - const providersArray = data?.data?.filter(({ providers }) => providers !== null) ?? []; - const providersCount = uniqBy( - providersArray.map(({ providers }) => providers).flat(), - (provider) => provider.uuid, - ).length; + const providersArray = data?.data?.flatMap(({ providers }) => providers ?? []) ?? []; + + const validProviders = providersArray.filter((provider) => provider.response === 'ACCEPTED'); + + const uniqueProviders = uniqBy(validProviders, (provider) => provider.uuid); + const providersCount = uniqueProviders.length; + return { totalProviders: providersCount ? providersCount : 0, isLoading, diff --git a/packages/esm-appointments-app/translations/en.json b/packages/esm-appointments-app/translations/en.json index 5b39be5f0..666410df3 100644 --- a/packages/esm-appointments-app/translations/en.json +++ b/packages/esm-appointments-app/translations/en.json @@ -100,6 +100,8 @@ "location": "Location", "medications": "Medications", "missed": "Missed", + "next": "Next", + "nextMonth": "Next month", "nextPage": "Next page", "no": "No", "noAppointmentsToDisplay": "No appointments to display", @@ -120,6 +122,8 @@ "patientName": "Patient name", "patients": "Patients", "period": "Period", + "prev": "Prev", + "previousMonth": "Previous month", "previousPage": "Previous page", "provider": "Provider", "providers": "Providers", diff --git a/packages/esm-bed-management-app/src/bed-administration/bed-administration-form.component.tsx b/packages/esm-bed-management-app/src/bed-administration/bed-administration-form.component.tsx index 9451566b1..fb19ab3e3 100644 --- a/packages/esm-bed-management-app/src/bed-administration/bed-administration-form.component.tsx +++ b/packages/esm-bed-management-app/src/bed-administration/bed-administration-form.component.tsx @@ -118,8 +118,8 @@ const BedAdministrationForm: React.FC = ({ return ( onModalChange(false)} preventCloseOnClickOutside> -
- + + = ({ /> )} - - - - - - + +
+ + + +
); }; diff --git a/packages/esm-bed-management-app/src/bed-administration/bed-administration-table.scss b/packages/esm-bed-management-app/src/bed-administration/bed-administration-table.scss index d7c969ffe..b07e89456 100644 --- a/packages/esm-bed-management-app/src/bed-administration/bed-administration-table.scss +++ b/packages/esm-bed-management-app/src/bed-administration/bed-administration-table.scss @@ -6,10 +6,6 @@ border: 1px solid colors.$gray-20; margin: layout.$spacing-06; - :global(.cds--modal-content) { - margin-bottom: unset; - } - @media (min-width: 66rem) { :global(.cds--modal-container) { max-height: 100%; diff --git a/packages/esm-patient-registration-app/package.json b/packages/esm-patient-registration-app/package.json index c119ee10c..6a2f875ea 100644 --- a/packages/esm-patient-registration-app/package.json +++ b/packages/esm-patient-registration-app/package.json @@ -18,7 +18,7 @@ "test:watch": "cross-env TZ=UTC jest --watch --config jest.config.js --color", "coverage": "yarn test --coverage", "typescript": "tsc", - "extract-translations": "i18next 'src/**/*.component.tsx' 'src/**/*.extension.tsx' 'src/**/*modal.tsx' 'src/**/*.workspace.tsx' 'src/index.ts' --config ../../tools/i18next-parser.config.js" + "extract-translations": "i18next 'src/**/*.component.tsx' 'src/**/*.extension.tsx' 'src/**/*modal.tsx' 'src/**/*.workspace.tsx' 'src/index.ts' 'src/patient-registration/validation/patient-registration-validation.ts' --config ../../tools/i18next-parser.config.js" }, "browserslist": [ "extends browserslist-config-openmrs" diff --git a/packages/esm-patient-registration-app/src/config-schema.ts b/packages/esm-patient-registration-app/src/config-schema.ts index 205b9a2a6..1e2cadd78 100644 --- a/packages/esm-patient-registration-app/src/config-schema.ts +++ b/packages/esm-patient-registration-app/src/config-schema.ts @@ -37,6 +37,10 @@ export interface RegistrationConfig { sectionDefinitions: Array; fieldDefinitions: Array; fieldConfigurations: { + causeOfDeath: { + conceptUuid: string; + required?: boolean; + }; name: { displayMiddleName: boolean; allowUnidentifiedPatients: boolean; @@ -78,6 +82,7 @@ export interface RegistrationConfig { encounterProviderRoleUuid: string; registrationFormUuid: string | null; }; + freeTextFieldConceptUuid: string; } export const builtInSections: Array = [ @@ -87,12 +92,21 @@ export const builtInSections: Array = [ fields: ['name', 'gender', 'dob', 'id'], }, { id: 'contact', name: 'Contact Details', fields: ['address', 'phone'] }, - { id: 'death', name: 'Death Info', fields: [] }, + { id: 'death', name: 'Death Info', fields: ['dateAndTimeOfDeath', 'causeOfDeath'] }, { id: 'relationships', name: 'Relationships', fields: [] }, ]; // These fields are handled specially in field.component.tsx -export const builtInFields = ['name', 'gender', 'dob', 'id', 'address', 'phone'] as const; +export const builtInFields = [ + 'name', + 'gender', + 'dob', + 'id', + 'address', + 'phone', + 'causeOfDeath', + 'dateAndTimeOfDeath', +] as const; export const esmPatientRegistrationSchema = { sections: { @@ -199,6 +213,14 @@ export const esmPatientRegistrationSchema = { 'Definitions for custom fields that can be used in sectionDefinitions. Can also be used to override built-in fields.', }, fieldConfigurations: { + causeOfDeath: { + conceptUuid: { + _type: Type.ConceptUuid, + _description: 'The concept UUID to get cause of death answers', + _default: '9272a14b-7260-4353-9e5b-5787b5dead9d', + }, + required: { _type: Type.Boolean, _default: false }, + }, name: { displayMiddleName: { _type: Type.Boolean, _default: true }, allowUnidentifiedPatients: { @@ -359,6 +381,10 @@ export const esmPatientRegistrationSchema = { 'The form UUID to associate with the registration encounter. By default no form will be associated.', }, }, + freeTextFieldConceptUuid: { + _default: '5622AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + _type: Type.ConceptUuid, + }, _validators: [ validator( (config: RegistrationConfig) => diff --git a/packages/esm-patient-registration-app/src/index.ts b/packages/esm-patient-registration-app/src/index.ts index d31ceb58a..47a6d4e42 100644 --- a/packages/esm-patient-registration-app/src/index.ts +++ b/packages/esm-patient-registration-app/src/index.ts @@ -50,10 +50,7 @@ export const editPatient = getSyncLifecycle(rootComponent, { export const addPatientLink = getSyncLifecycle(addPatientLinkComponent, options); -export const cancelPatientEditModal = getAsyncLifecycle( - () => import('./widgets/cancel-patient-edit.component'), - options, -); +export const cancelPatientEditModal = getAsyncLifecycle(() => import('./widgets/cancel-patient-edit.modal'), options); export const patientPhotoExtension = getSyncLifecycle(PatientPhotoExtension, options); diff --git a/packages/esm-patient-registration-app/src/patient-registration/field/cause-of-death/cause-of-death.component.tsx b/packages/esm-patient-registration-app/src/patient-registration/field/cause-of-death/cause-of-death.component.tsx new file mode 100644 index 000000000..e764d0e81 --- /dev/null +++ b/packages/esm-patient-registration-app/src/patient-registration/field/cause-of-death/cause-of-death.component.tsx @@ -0,0 +1,98 @@ +import React, { useMemo } from 'react'; +import classNames from 'classnames'; +import { Field, useField } from 'formik'; +import { useTranslation } from 'react-i18next'; +import { InlineNotification, Layer, Select, SelectItem, SelectSkeleton, TextInput } from '@carbon/react'; +import { useConfig } from '@openmrs/esm-framework'; +import { type RegistrationConfig } from '../../../config-schema'; +import { useConceptAnswers } from '../field.resource'; +import styles from '../field.scss'; + +export const CauseOfDeathField: React.FC = () => { + const { t } = useTranslation(); + const { fieldConfigurations, freeTextFieldConceptUuid } = useConfig(); + const [deathCause, deathCauseMeta] = useField('deathCause'); + + const conceptUuid = fieldConfigurations?.causeOfDeath?.conceptUuid; + const required = fieldConfigurations?.causeOfDeath?.required; + + const { + data: conceptAnswers, + isLoading: isLoadingConceptAnswers, + error: errorLoadingConceptAnswers, + } = useConceptAnswers(conceptUuid); + + const answers = useMemo(() => { + if (!isLoadingConceptAnswers && conceptAnswers) { + return conceptAnswers.map((answer) => ({ ...answer, label: answer.display })); + } + return []; + }, [conceptAnswers, isLoadingConceptAnswers]); + + if (isLoadingConceptAnswers) { + return ( +
+

{t('causeOfDeathInputLabel', 'Cause of death')}

+ +
+ ); + } + + return ( +
+

{t('causeOfDeathInputLabel', 'Cause of death')}

+ {errorLoadingConceptAnswers || !conceptUuid ? ( + + ) : ( + <> + + {({ field, form: { touched, errors }, meta }) => { + return ( + + + + ); + }} + + {deathCause.value === freeTextFieldConceptUuid && ( +
+ + {({ field, form: { touched, errors }, meta }) => { + return ( + + + + ); + }} + +
+ )} + + )} +
+ ); +}; diff --git a/packages/esm-patient-registration-app/src/patient-registration/field/date-and-time-of-death/date-and-time-of-death.component.tsx b/packages/esm-patient-registration-app/src/patient-registration/field/date-and-time-of-death/date-and-time-of-death.component.tsx new file mode 100644 index 000000000..0a27740f4 --- /dev/null +++ b/packages/esm-patient-registration-app/src/patient-registration/field/date-and-time-of-death/date-and-time-of-death.component.tsx @@ -0,0 +1,84 @@ +import React, { useCallback, useContext } from 'react'; +import classNames from 'classnames'; +import dayjs from 'dayjs'; +import { Layer, SelectItem, TimePicker, TimePickerSelect } from '@carbon/react'; +import { useTranslation } from 'react-i18next'; +import { useField } from 'formik'; +import { OpenmrsDatePicker } from '@openmrs/esm-framework'; +import { PatientRegistrationContext } from '../../patient-registration-context'; +import type { FormValues } from '../../patient-registration.types'; +import styles from '../field.scss'; + +export const DateAndTimeOfDeathField: React.FC = () => { + const { t } = useTranslation(); + + return ( +
+

{t('deathDateInputLabel', 'Date of Death')}

+ + + + +
+ ); +}; + +function DeathDateField() { + const { values, setFieldValue } = useContext(PatientRegistrationContext); + const [deathDate, deathDateMeta] = useField('deathDate'); + const { t } = useTranslation(); + const today = dayjs().hour(23).minute(59).second(59).toDate(); + const onDateChange = useCallback( + (selectedDate: Date) => { + setFieldValue( + 'deathDate', + selectedDate ? dayjs(selectedDate).hour(0).minute(0).second(0).millisecond(0).toDate() : undefined, + ); + }, + [deathDate], + ); + + return ( + + + + ); +} + +function DeathTimeField() { + const { t } = useTranslation(); + const [deathTimeField, deathTimeMeta] = useField('deathTime'); + const [deathTimeFormatField, deathTimeFormatMeta] = useField('deathTimeFormat'); + + return ( + + + + + + + + + ); +} diff --git a/packages/esm-patient-registration-app/src/patient-registration/field/dob/dob.component.tsx b/packages/esm-patient-registration-app/src/patient-registration/field/dob/dob.component.tsx index f3c21415c..7b1500bf2 100644 --- a/packages/esm-patient-registration-app/src/patient-registration/field/dob/dob.component.tsx +++ b/packages/esm-patient-registration-app/src/patient-registration/field/dob/dob.component.tsx @@ -88,7 +88,7 @@ export const DobField: React.FC = () => { {(allowEstimatedBirthDate || dobUnknown) && (
- {t('dobToggleLabelText', 'Date of Birth Known?')} + {t('dobToggleLabelText', 'Date of birth known?')}
@@ -104,7 +104,7 @@ export const DobField: React.FC = () => { {...birthdate} onChange={onDateChange} maxDate={today} - labelText={t('dateOfBirthLabelText', 'Date of Birth')} + labelText={t('dateOfBirthLabelText', 'Date of birth')} isInvalid={!!(birthdateMeta.touched && birthdateMeta.error)} invalidText={t(birthdateMeta.error)} value={birthdate.value} diff --git a/packages/esm-patient-registration-app/src/patient-registration/field/field.component.tsx b/packages/esm-patient-registration-app/src/patient-registration/field/field.component.tsx index bdadee3d4..8c6baef18 100644 --- a/packages/esm-patient-registration-app/src/patient-registration/field/field.component.tsx +++ b/packages/esm-patient-registration-app/src/patient-registration/field/field.component.tsx @@ -1,12 +1,14 @@ import React from 'react'; -import { NameField } from './name/name-field.component'; -import { GenderField } from './gender/gender-field.component'; -import { Identifiers } from './id/id-field.component'; -import { DobField } from './dob/dob.component'; import { reportError, useConfig } from '@openmrs/esm-framework'; import { builtInFields, type RegistrationConfig } from '../../config-schema'; -import { CustomField } from './custom-field.component'; import { AddressComponent } from './address/address-field.component'; +import { CauseOfDeathField } from './cause-of-death/cause-of-death.component'; +import { CustomField } from './custom-field.component'; +import { DateAndTimeOfDeathField } from './date-and-time-of-death/date-and-time-of-death.component'; +import { DobField } from './dob/dob.component'; +import { GenderField } from './gender/gender-field.component'; +import { Identifiers } from './id/id-field.component'; +import { NameField } from './name/name-field.component'; import { PhoneField } from './phone/phone-field.component'; export interface FieldProps { @@ -35,6 +37,10 @@ export function Field({ name }: FieldProps) { return ; case 'dob': return ; + case 'dateAndTimeOfDeath': + return ; + case 'causeOfDeath': + return ; case 'address': return ; case 'id': diff --git a/packages/esm-patient-registration-app/src/patient-registration/field/field.resource.ts b/packages/esm-patient-registration-app/src/patient-registration/field/field.resource.ts index 217fdefc5..9d549fc47 100644 --- a/packages/esm-patient-registration-app/src/patient-registration/field/field.resource.ts +++ b/packages/esm-patient-registration-app/src/patient-registration/field/field.resource.ts @@ -1,6 +1,7 @@ -import { type FetchResponse, openmrsFetch, showSnackbar, restBaseUrl } from '@openmrs/esm-framework'; +import { type FetchResponse, openmrsFetch, restBaseUrl, showSnackbar } from '@openmrs/esm-framework'; import useSWRImmutable from 'swr/immutable'; import { type ConceptAnswers, type ConceptResponse } from '../patient-registration.types'; +import { useMemo } from 'react'; export function useConcept(conceptUuid: string): { data: ConceptResponse; isLoading: boolean } { const shouldFetch = typeof conceptUuid === 'string' && conceptUuid !== ''; @@ -15,10 +16,15 @@ export function useConcept(conceptUuid: string): { data: ConceptResponse; isLoad kind: 'error', }); } - return { data: data?.data, isLoading }; + const results = useMemo(() => ({ data: data?.data, isLoading }), [data, isLoading]); + return results; } -export function useConceptAnswers(conceptUuid: string): { data: Array; isLoading: boolean } { +export function useConceptAnswers(conceptUuid: string): { + data: Array; + isLoading: boolean; + error: Error; +} { const shouldFetch = typeof conceptUuid === 'string' && conceptUuid !== ''; const { data, error, isLoading } = useSWRImmutable, Error>( shouldFetch ? `${restBaseUrl}/concept/${conceptUuid}` : null, @@ -31,5 +37,6 @@ export function useConceptAnswers(conceptUuid: string): { data: Array ({ data: data?.data?.answers, isLoading, error }), [isLoading, error, data]); + return results; } diff --git a/packages/esm-patient-registration-app/src/patient-registration/field/field.scss b/packages/esm-patient-registration-app/src/patient-registration/field/field.scss index fe29c1bc1..7f3595392 100644 --- a/packages/esm-patient-registration-app/src/patient-registration/field/field.scss +++ b/packages/esm-patient-registration-app/src/patient-registration/field/field.scss @@ -64,8 +64,30 @@ } .sexField, -.dobField { +.dobField, +.dodField { margin-bottom: layout.$spacing-05; + + span { + display: flex; + flex-flow: row nowrap; + justify-content: space-between; + align-items: start; + } +} + +.nonCodedCauseOfDeath { + margin-top: layout.$spacing-04; +} + +.timeOfDeathContainer { + display: flex; + align-items: center; +} + +.timeOfDeathField { + flex: none; + margin-left: layout.$spacing-02; } .dobContentSwitcherLabel { diff --git a/packages/esm-patient-registration-app/src/patient-registration/form-manager.test.ts b/packages/esm-patient-registration-app/src/patient-registration/form-manager.test.ts index 56b4e528f..ae625319e 100644 --- a/packages/esm-patient-registration-app/src/patient-registration/form-manager.test.ts +++ b/packages/esm-patient-registration-app/src/patient-registration/form-manager.test.ts @@ -20,7 +20,10 @@ const formValues: FormValues = { telephoneNumber: '', isDead: false, deathDate: 'string', + deathTime: '', + deathTimeFormat: 'AM', deathCause: 'string', + nonCodedCauseOfDeath: '', relationships: [], address: { address1: '', diff --git a/packages/esm-patient-registration-app/src/patient-registration/form-manager.ts b/packages/esm-patient-registration-app/src/patient-registration/form-manager.ts index 2019fc4ad..b682d038f 100644 --- a/packages/esm-patient-registration-app/src/patient-registration/form-manager.ts +++ b/packages/esm-patient-registration-app/src/patient-registration/form-manager.ts @@ -1,23 +1,24 @@ import { type FetchResponse, - type Session, - type StyleguideConfigObject, getConfig, openmrsFetch, queueSynchronizationItem, restBaseUrl, + type Session, + type StyleguideConfigObject, + toOmrsIsoString, } from '@openmrs/esm-framework'; import { patientRegistration } from '../constants'; import { - type FormValues, type AttributeValue, - type PatientUuidMapType, - type Patient, type CapturePhotoProps, + type Encounter, + type FormValues, + type Patient, type PatientIdentifier, type PatientRegistration, + type PatientUuidMapType, type RelationshipValue, - type Encounter, } from './patient-registration.types'; import { addPatientIdentifier, @@ -25,14 +26,16 @@ import { deletePersonName, deleteRelationship, generateIdentifier, + getDatetime, + saveEncounter, savePatient, savePatientPhoto, saveRelationship, - updateRelationship, updatePatientIdentifier, - saveEncounter, + updateRelationship, } from './patient-registration.resource'; import { type RegistrationConfig } from '../config-schema'; +import dayjs from 'dayjs'; export type SavePatientForm = ( isNewPatient: boolean, @@ -62,7 +65,7 @@ export class FormManager { ) => { const syncItem: PatientRegistration = { fhirPatient: FormManager.mapPatientToFhirPatient( - FormManager.getPatientToCreate(isNewPatient, values, patientUuidMap, initialAddressFieldValues, []), + FormManager.getPatientToCreate(isNewPatient, values, patientUuidMap, initialAddressFieldValues, [], config), ), _patientRegistrationData: { isNewPatient, @@ -115,6 +118,7 @@ export class FormManager { patientUuidMap, initialAddressFieldValues, patientIdentifiers, + config, ); FormManager.getDeletedNames(values.patientUuid, patientUuidMap).forEach(async (name) => { @@ -297,6 +301,7 @@ export class FormManager { patientUuidMap: PatientUuidMapType, initialAddressFieldValues: Record, identifiers: Array, + config?: RegistrationConfig, ): Patient { let birthdate; if (values.birthdate instanceof Date) { @@ -317,7 +322,7 @@ export class FormManager { birthdateEstimated: values.birthdateEstimated, attributes: FormManager.getPatientAttributes(isNewPatient, values, patientUuidMap), addresses: [values.address], - ...FormManager.getPatientDeathInfo(values), + ...FormManager.getPatientDeathInfo(values, config), }, identifiers, }; @@ -376,12 +381,22 @@ export class FormManager { return attributes; } - static getPatientDeathInfo(values: FormValues) { - const { isDead, deathDate, deathCause } = values; + static getPatientDeathInfo(values: FormValues, config?: RegistrationConfig) { + const { isDead, deathDate, deathTime, deathTimeFormat, deathCause, nonCodedCauseOfDeath } = values; + + if (!isDead) { + return { + dead: false, + }; + } + const dateTimeOfDeath = toOmrsIsoString(getDatetime(deathDate, deathTime, deathTimeFormat)); + return { - dead: isDead, - deathDate: isDead ? deathDate : undefined, - causeOfDeath: isDead ? deathCause : undefined, + dead: true, + deathDate: dateTimeOfDeath, + ...(deathCause === config?.freeTextFieldConceptUuid + ? { causeOfDeathNonCoded: nonCodedCauseOfDeath, causeOfDeath: null } + : { causeOfDeath: deathCause, causeOfDeathNonCoded: null }), }; } diff --git a/packages/esm-patient-registration-app/src/patient-registration/input/basic-input/input/input.component.tsx b/packages/esm-patient-registration-app/src/patient-registration/input/basic-input/input/input.component.tsx index c2a31ab8f..fa6edca8a 100644 --- a/packages/esm-patient-registration-app/src/patient-registration/input/basic-input/input/input.component.tsx +++ b/packages/esm-patient-registration-app/src/patient-registration/input/basic-input/input/input.component.tsx @@ -66,7 +66,7 @@ export interface TextInputProps * `true` to use the light version. For use on $ui-01 backgrounds only. * Don't use this to make tile background color same as container background color. * 'The `light` prop for `TextInput` has ' + - 'been deprecated in favor of the new `Layer` component. It will be removed in the next major release.' + 'been deprecated in favor of the new `Layer` component. It will be removed in the next major release.' */ light?: boolean; @@ -145,6 +145,10 @@ export const Input: React.FC = ({ checkWarning, ...props }) => { t('invalidEmail') t('numberInNameDubious') t('yearsEstimateRequired') + t('deathdayIsRequired', 'Death date is required when the patient is marked as deceased.') + t('deathdayInvalidDate', 'Date of death is invalid') + t('deathCauseRequired', 'Cause of death is required') + t('nonCodedCauseOfDeathRequired', 'Non-coded cause of death is required') */ const value = field.value || ''; diff --git a/packages/esm-patient-registration-app/src/patient-registration/input/dummy-data/dummy-data-input.component.tsx b/packages/esm-patient-registration-app/src/patient-registration/input/dummy-data/dummy-data-input.component.tsx index 79f76218b..c87f5e31d 100644 --- a/packages/esm-patient-registration-app/src/patient-registration/input/dummy-data/dummy-data-input.component.tsx +++ b/packages/esm-patient-registration-app/src/patient-registration/input/dummy-data/dummy-data-input.component.tsx @@ -25,7 +25,10 @@ export const dummyFormValues: FormValues = { telephoneNumber: '0800001066', isDead: false, deathDate: '', + deathTime: '', + deathTimeFormat: 'AM', deathCause: '', + nonCodedCauseOfDeath: '', relationships: [], address: { address1: 'Bom Jesus Street', diff --git a/packages/esm-patient-registration-app/src/patient-registration/patient-registration-context.ts b/packages/esm-patient-registration-app/src/patient-registration/patient-registration-context.ts index d3e46eca2..2f29433f0 100644 --- a/packages/esm-patient-registration-app/src/patient-registration/patient-registration-context.ts +++ b/packages/esm-patient-registration-app/src/patient-registration/patient-registration-context.ts @@ -1,7 +1,7 @@ import { useConfig } from '@openmrs/esm-framework'; import { createContext, type SetStateAction } from 'react'; import { type RegistrationConfig } from '../config-schema'; -import { type FormValues, type CapturePhotoProps } from './patient-registration.types'; +import { type CapturePhotoProps, type FormValues } from './patient-registration.types'; export interface PatientRegistrationContextProps { currentPhoto: string; @@ -9,11 +9,12 @@ export interface PatientRegistrationContextProps { inEditMode: boolean; initialFormValues: FormValues; isOffline: boolean; - setCapturePhotoProps(value: SetStateAction): void; - setFieldValue(field: string, value: any, shouldValidate?: boolean): void; setInitialFormValues?: React.Dispatch>; validationSchema: any; values: FormValues; + setCapturePhotoProps(value: SetStateAction): void; + setFieldValue(field: string, value: any, shouldValidate?: boolean): void; + setFieldTouched(field: string, isTouched?: any, shouldValidate?: boolean): void; } export const PatientRegistrationContext = createContext(undefined); diff --git a/packages/esm-patient-registration-app/src/patient-registration/patient-registration-hooks.ts b/packages/esm-patient-registration-app/src/patient-registration/patient-registration-hooks.ts index 8768fc8a0..ff50d4b20 100644 --- a/packages/esm-patient-registration-app/src/patient-registration/patient-registration-hooks.ts +++ b/packages/esm-patient-registration-app/src/patient-registration/patient-registration-hooks.ts @@ -1,11 +1,11 @@ import { type FetchResponse, - type OpenmrsResource, getSynchronizationItems, openmrsFetch, + type OpenmrsResource, + restBaseUrl, useConfig, usePatient, - restBaseUrl, } from '@openmrs/esm-framework'; import camelCase from 'lodash-es/camelCase'; import { type Dispatch, useEffect, useMemo, useState } from 'react'; @@ -14,12 +14,12 @@ import { v4 } from 'uuid'; import { type RegistrationConfig } from '../config-schema'; import { patientRegistration } from '../constants'; import { + type Encounter, type FormValues, + type PatientIdentifierResponse, type PatientRegistration, type PatientUuidMapType, type PersonAttributeResponse, - type PatientIdentifierResponse, - type Encounter, } from './patient-registration.types'; import { getAddressFieldValuesFromFhirPatient, @@ -32,7 +32,9 @@ import { useInitialPatientRelationships } from './section/patient-relationships/ import dayjs from 'dayjs'; export function useInitialFormValues(patientUuid: string): [FormValues, Dispatch] { + const { freeTextFieldConceptUuid } = useConfig(); const { isLoading: isLoadingPatientToEdit, patient: patientToEdit } = usePatient(patientUuid); + const { data: deathInfo, isLoading: isLoadingDeathInfo } = useInitialPersonDeathInfo(patientUuid); const { data: attributes, isLoading: isLoadingAttributes } = useInitialPersonAttributes(patientUuid); const { data: identifiers, isLoading: isLoadingIdentifiers } = useInitialPatientIdentifiers(patientUuid); const { data: relationships, isLoading: isLoadingRelationships } = useInitialPatientRelationships(patientUuid); @@ -54,8 +56,11 @@ export function useInitialFormValues(patientUuid: string): [FormValues, Dispatch birthdateEstimated: false, telephoneNumber: '', isDead: false, - deathDate: '', + deathDate: undefined, + deathTime: undefined, + deathTimeFormat: 'AM', deathCause: '', + nonCodedCauseOfDeath: '', relationships: [], identifiers: {}, address: {}, @@ -96,6 +101,25 @@ export function useInitialFormValues(patientUuid: string): [FormValues, Dispatch })(); }, [isLoadingPatientToEdit, patientToEdit, patientUuid]); + // Set initial patient death info + useEffect(() => { + if (!isLoadingDeathInfo && deathInfo?.dead) { + const deathDatetime = deathInfo.deathDate || null; + const deathDate = deathDatetime ? new Date(deathDatetime) : undefined; + const time = deathDate ? dayjs(deathDate).format('hh:mm') : undefined; + const timeFormat = deathDate ? (dayjs(deathDate).hour() >= 12 ? 'PM' : 'AM') : 'AM'; + setInitialFormValues((initialFormValues) => ({ + ...initialFormValues, + isDead: deathInfo.dead || false, + deathDate: deathDate, + deathTime: time, + deathTimeFormat: timeFormat, + deathCause: deathInfo.causeOfDeathNonCoded ? freeTextFieldConceptUuid : deathInfo.causeOfDeath?.uuid, + nonCodedCauseOfDeath: deathInfo.causeOfDeathNonCoded, + })); + } + }, [isLoadingDeathInfo, deathInfo, setInitialFormValues]); + // Set initial patient relationships useEffect(() => { if (!isLoadingRelationships && relationships) { @@ -279,6 +303,32 @@ function useInitialPersonAttributes(personUuid: string) { return result; } +interface DeathInfoResults { + uuid: string; + display: string; + causeOfDeath: OpenmrsResource | null; + dead: boolean; + deathDate: string; + causeOfDeathNonCoded: string | null; +} + +function useInitialPersonDeathInfo(personUuid: string) { + const { data, error, isLoading } = useSWR, Error>( + !!personUuid + ? `${restBaseUrl}/person/${personUuid}?v=custom:(uuid,display,causeOfDeath,dead,deathDate,causeOfDeathNonCoded)` + : null, + openmrsFetch, + ); + + const result = useMemo(() => { + return { + data: data?.data, + isLoading, + }; + }, [data, error]); + return result; +} + function getPatientAttributeUuidMapForPatient(attributes: Array) { const attributeUuidMap = {}; attributes.forEach((attribute) => { diff --git a/packages/esm-patient-registration-app/src/patient-registration/patient-registration-utils.ts b/packages/esm-patient-registration-app/src/patient-registration/patient-registration-utils.ts index 57dddc818..3aa777a2c 100644 --- a/packages/esm-patient-registration-app/src/patient-registration/patient-registration-utils.ts +++ b/packages/esm-patient-registration-app/src/patient-registration/patient-registration-utils.ts @@ -3,11 +3,11 @@ import camelCase from 'lodash-es/camelCase'; import { parseDate } from '@openmrs/esm-framework'; import { type AddressValidationSchemaType, + type Encounter, type FormValues, type PatientIdentifier, - type PatientUuidMapType, type PatientIdentifierValue, - type Encounter, + type PatientUuidMapType, } from './patient-registration.types'; export function parseAddressTemplateXml(addressTemplate: string) { @@ -47,6 +47,7 @@ export function parseAddressTemplateXml(addressTemplate: string) { addressValidationSchema, }; } + export function parseAddressTemplateXmlOld(addressTemplate: string) { const templateXmlDoc = new DOMParser().parseFromString(addressTemplate, 'text/xml'); const nameMappings = templateXmlDoc.querySelector('nameMappings').querySelectorAll('property'); @@ -123,11 +124,6 @@ export function getFormValuesFromFhirPatient(patient: fhir.Patient) { result.birthdate = patient.birthDate ? parseDate(patient.birthDate) : undefined; result.telephoneNumber = patient.telecom ? patient.telecom[0].value : ''; - if (patient.deceasedBoolean || patient.deceasedDateTime) { - result.isDead = true; - result.deathDate = patient.deceasedDateTime ? patient.deceasedDateTime.split('T')[0] : ''; - } - return { ...result, ...patient.identifier.map((identifier) => { diff --git a/packages/esm-patient-registration-app/src/patient-registration/patient-registration.component.tsx b/packages/esm-patient-registration-app/src/patient-registration/patient-registration.component.tsx index abc3abac5..2db6ba576 100644 --- a/packages/esm-patient-registration-app/src/patient-registration/patient-registration.component.tsx +++ b/packages/esm-patient-registration-app/src/patient-registration/patient-registration.component.tsx @@ -1,20 +1,20 @@ -import React, { useState, useEffect, useContext, useMemo, useRef } from 'react'; +import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'; import classNames from 'classnames'; -import { Button, Link, InlineLoading } from '@carbon/react'; +import { Button, InlineLoading, Link } from '@carbon/react'; import { XAxis } from '@carbon/react/icons'; import { useLocation, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { Formik, Form, type FormikHelpers } from 'formik'; +import { Form, Formik, type FormikHelpers } from 'formik'; import { createErrorHandler, + interpolateUrl, showSnackbar, useConfig, - interpolateUrl, usePatient, usePatientPhoto, } from '@openmrs/esm-framework'; import { getValidationSchema } from './validation/patient-registration-validation'; -import { type FormValues, type CapturePhotoProps } from './patient-registration.types'; +import { type CapturePhotoProps, type FormValues } from './patient-registration.types'; import { PatientRegistrationContext } from './patient-registration-context'; import { type SavePatientForm, SavePatientTransactionManager } from './form-manager'; import { DummyDataInput } from './input/dummy-data/dummy-data-input.component'; @@ -132,16 +132,23 @@ export const PatientRegistration: React.FC = ({ savePa } }; + const getDescription = (errors) => { + return ( +
    + {Object.keys(errors).map((error, index) => { + return
  • {t(`${error}LabelText`, error)}
  • ; + })} +
+ ); + }; + const displayErrors = (errors) => { if (errors && typeof errors === 'object' && !!Object.keys(errors).length) { - Object.keys(errors).forEach((error) => { - showSnackbar({ - subtitle: t(`${error}LabelText`, error), - title: t('incompleteForm', 'The following field has errors:'), - kind: 'warning', - isLowContrast: true, - timeoutInMs: 5000, - }); + showSnackbar({ + isLowContrast: true, + kind: 'warning', + title: t('fieldsWithErrors', 'The following fields have errors:'), + subtitle: <>{getDescription(errors)}, }); } }; @@ -205,6 +212,7 @@ export const PatientRegistration: React.FC = ({ savePa values: props.values, inEditMode, setFieldValue: props.setFieldValue, + setFieldTouched: props.setFieldTouched, setCapturePhotoProps, currentPhoto: photo?.imageSrc, isOffline, diff --git a/packages/esm-patient-registration-app/src/patient-registration/patient-registration.resource.ts b/packages/esm-patient-registration-app/src/patient-registration/patient-registration.resource.ts index a1c766054..b18b2f985 100644 --- a/packages/esm-patient-registration-app/src/patient-registration/patient-registration.resource.ts +++ b/packages/esm-patient-registration-app/src/patient-registration/patient-registration.resource.ts @@ -1,5 +1,6 @@ import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework'; import { type Patient, type Relationship, type PatientIdentifier, type Encounter } from './patient-registration.types'; +import dayjs from 'dayjs'; export const uuidIdentifier = '05a29f94-c0ed-11e2-94be-8c13b969e334'; export const uuidTelephoneNumber = '14d4f066-15f5-102d-96e4-000c29c2a5d7'; @@ -188,3 +189,10 @@ export async function deletePatientIdentifier(patientUuid: string, patientIdenti signal: abortController.signal, }); } + +export function getDatetime(date: Date | string, time: string, timeFormat: 'AM' | 'PM') { + const datetime = new Date(date); + const [hours, minutes] = time.split(':').map(Number); + const fullHours = timeFormat === 'PM' ? (hours % 12) + 12 : hours % 12; + return dayjs(datetime).hour(fullHours).minute(minutes).second(0).millisecond(0).toDate(); +} diff --git a/packages/esm-patient-registration-app/src/patient-registration/patient-registration.test.tsx b/packages/esm-patient-registration-app/src/patient-registration/patient-registration.test.tsx index 3c376d752..f2c93fd80 100644 --- a/packages/esm-patient-registration-app/src/patient-registration/patient-registration.test.tsx +++ b/packages/esm-patient-registration-app/src/patient-registration/patient-registration.test.tsx @@ -14,7 +14,7 @@ import { import { mockedAddressTemplate } from '__mocks__'; import { mockPatient } from 'tools'; import { saveEncounter, savePatient } from './patient-registration.resource'; -import { type RegistrationConfig, esmPatientRegistrationSchema } from '../config-schema'; +import { esmPatientRegistrationSchema, type RegistrationConfig } from '../config-schema'; import type { AddressTemplate, Encounter } from './patient-registration.types'; import { ResourcesContext } from '../offline.resources'; import { FormManager } from './form-manager'; @@ -167,6 +167,9 @@ let mockOpenmrsConfig: RegistrationConfig = { searchAddressByLevel: true, }, }, + causeOfDeath: { + conceptUuid: 'cause-of-death-concept-uuid', + }, }, links: { submitButton: '#', @@ -407,7 +410,7 @@ describe('Updating an existing patient record', () => { const givenNameInput: HTMLInputElement = screen.getByLabelText(/First Name/); const familyNameInput: HTMLInputElement = screen.getByLabelText(/Family Name/); const middleNameInput: HTMLInputElement = screen.getByLabelText(/Middle Name/); - const dateOfBirthInput: HTMLInputElement = screen.getByLabelText('Date of Birth'); + const dateOfBirthInput: HTMLInputElement = screen.getByLabelText(/Date of Birth/i); const genderInput: HTMLInputElement = screen.getByLabelText(/Male/); // assert initial values @@ -443,7 +446,10 @@ describe('Updating an existing patient record', () => { birthdate: new Date('1972-04-04T00:00:00.000Z'), birthdateEstimated: false, deathCause: '', - deathDate: '', + nonCodedCauseOfDeath: '', + deathDate: undefined, + deathTime: undefined, + deathTimeFormat: 'AM', familyName: 'Smith', gender: expect.stringMatching(/male/i), givenName: 'Eric', diff --git a/packages/esm-patient-registration-app/src/patient-registration/patient-registration.types.ts b/packages/esm-patient-registration-app/src/patient-registration/patient-registration.types.ts index 35b6a4e2a..a185b16e0 100644 --- a/packages/esm-patient-registration-app/src/patient-registration/patient-registration.types.ts +++ b/packages/esm-patient-registration-app/src/patient-registration/patient-registration.types.ts @@ -167,7 +167,9 @@ export interface FormValues { birthdate: Date | string; birthdateEstimated: boolean; deathCause: string; - deathDate: string; + deathDate: string | Date; + deathTime: string; + deathTimeFormat: 'AM' | 'PM'; familyName: string; gender: string; givenName: string; @@ -177,6 +179,7 @@ export interface FormValues { isDead: boolean; middleName: string; monthsEstimated: number; + nonCodedCauseOfDeath: string; obs?: { [conceptUuid: string]: string; }; diff --git a/packages/esm-patient-registration-app/src/patient-registration/section/death-info/death-info-section.component.tsx b/packages/esm-patient-registration-app/src/patient-registration/section/death-info/death-info-section.component.tsx index 6b33cf3b0..3deb60b82 100644 --- a/packages/esm-patient-registration-app/src/patient-registration/section/death-info/death-info-section.component.tsx +++ b/packages/esm-patient-registration-app/src/patient-registration/section/death-info/death-info-section.component.tsx @@ -1,30 +1,35 @@ -import React from 'react'; -import classNames from 'classnames'; +import React, { useContext } from 'react'; import { useTranslation } from 'react-i18next'; -import { Input } from '../../input/basic-input/input/input.component'; -import { SelectInput } from '../../input/basic-input/select/select-input.component'; +import { Checkbox, Layer } from '@carbon/react'; +import { useField } from 'formik'; +import { Field } from '../../field/field.component'; import { PatientRegistrationContext } from '../../patient-registration-context'; import styles from './../section.scss'; -export const DeathInfoSection = () => { - const { values } = React.useContext(PatientRegistrationContext); +export interface DeathInfoSectionProps { + fields: Array; +} + +export const DeathInfoSection: React.FC = ({ fields }) => { const { t } = useTranslation(); + const { values, setFieldValue } = useContext(PatientRegistrationContext); + const [deathDate, deathDateMeta] = useField('deathDate'); + const today = new Date(); return (
-
Death Info
- - {values.isDead && ( - <> - - +
+ setFieldValue(id, checked)} /> - - )} +
+ + {values.isDead ? fields.map((field) => ) : null}
); diff --git a/packages/esm-patient-registration-app/src/patient-registration/section/death-info/death-info-section.test.tsx b/packages/esm-patient-registration-app/src/patient-registration/section/death-info/death-info-section.test.tsx index dc7bb909d..08824c88a 100644 --- a/packages/esm-patient-registration-app/src/patient-registration/section/death-info/death-info-section.test.tsx +++ b/packages/esm-patient-registration-app/src/patient-registration/section/death-info/death-info-section.test.tsx @@ -1,10 +1,10 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; -import { Formik, Form } from 'formik'; +import { Form, Formik } from 'formik'; import { initialFormValues } from '../../patient-registration.component'; -import { DeathInfoSection } from './death-info-section.component'; import { type FormValues } from '../../patient-registration.types'; import { PatientRegistrationContext } from '../../patient-registration-context'; +import { DeathInfoSection } from './death-info-section.component'; const initialContextValues = { currentPhoto: 'data:image/png;base64,1234567890', @@ -29,7 +29,7 @@ describe('Death info section', () => {
- +
, @@ -40,16 +40,6 @@ describe('Death info section', () => { renderDeathInfoSection(true); expect(screen.getByRole('region', { name: /death info section/i })).toBeInTheDocument(); - expect(screen.getByRole('heading', { name: /death info/i })).toBeInTheDocument(); - expect(screen.getByRole('textbox', { name: /is dead \(optional\)/i })).toBeInTheDocument(); - expect(screen.getByRole('textbox', { name: /date of death \(optional\)/i })).toBeInTheDocument(); - expect(screen.getByRole('combobox', { name: /cause of death \(optional\)/i })).toBeInTheDocument(); - }); - - it('has the correct number of inputs if is dead is not checked', async () => { - renderDeathInfoSection(false); - - expect(screen.queryByRole('textbox', { name: /date of death \(optional\)/i })).not.toBeInTheDocument(); - expect(screen.queryByRole('combobox', { name: /cause of death \(optional\)/i })).not.toBeInTheDocument(); + expect(screen.getByRole('checkbox', { name: /is dead/i })).toBeInTheDocument(); }); }); diff --git a/packages/esm-patient-registration-app/src/patient-registration/section/section.component.tsx b/packages/esm-patient-registration-app/src/patient-registration/section/section.component.tsx index 1f3429c67..5b8fec26a 100644 --- a/packages/esm-patient-registration-app/src/patient-registration/section/section.component.tsx +++ b/packages/esm-patient-registration-app/src/patient-registration/section/section.component.tsx @@ -14,7 +14,7 @@ export function Section({ sectionDefinition }: SectionProps) { case 'demographics': return ; case 'death': - return ; + return ; case 'relationships': return ; default: // includes 'contact' diff --git a/packages/esm-patient-registration-app/src/patient-registration/section/section.scss b/packages/esm-patient-registration-app/src/patient-registration/section/section.scss index ee749ce6e..a1eb191c1 100644 --- a/packages/esm-patient-registration-app/src/patient-registration/section/section.scss +++ b/packages/esm-patient-registration-app/src/patient-registration/section/section.scss @@ -1,3 +1,4 @@ +@use '@carbon/colors'; @use '@carbon/layout'; @use '@carbon/type'; @use '@openmrs/esm-styleguide/src/vars' as *; @@ -14,3 +15,7 @@ color: $ui-04; cursor: pointer; } + +.isDeadFieldContainer { + margin-bottom: layout.$spacing-05; +} diff --git a/packages/esm-patient-registration-app/src/patient-registration/validation/patient-registration-validation.test.tsx b/packages/esm-patient-registration-app/src/patient-registration/validation/patient-registration-validation.test.ts similarity index 98% rename from packages/esm-patient-registration-app/src/patient-registration/validation/patient-registration-validation.test.tsx rename to packages/esm-patient-registration-app/src/patient-registration/validation/patient-registration-validation.test.ts index 699642d3c..f9c70c9b0 100644 --- a/packages/esm-patient-registration-app/src/patient-registration/validation/patient-registration-validation.test.tsx +++ b/packages/esm-patient-registration-app/src/patient-registration/validation/patient-registration-validation.test.ts @@ -35,6 +35,8 @@ describe('Patient registration validation', () => { additionalGivenName: '', birthdate: new Date('1990-01-01'), birthdateEstimated: false, + isDead: false, + causeOfDeath: null, deathDate: null, email: 'john.doe@example.com', familyName: 'Doe', @@ -177,6 +179,6 @@ describe('Patient registration validation', () => { deathDate: new Date('2100-01-01'), }; const validationError = await validateFormValues(invalidFormValues); - expect(validationError.errors).toContain('deathdayNotInTheFuture'); + expect(validationError.errors).toContain('deathDateInFuture'); }); }); diff --git a/packages/esm-patient-registration-app/src/patient-registration/validation/patient-registration-validation.ts b/packages/esm-patient-registration-app/src/patient-registration/validation/patient-registration-validation.ts new file mode 100644 index 000000000..ebb5fc835 --- /dev/null +++ b/packages/esm-patient-registration-app/src/patient-registration/validation/patient-registration-validation.ts @@ -0,0 +1,121 @@ +import dayjs from 'dayjs'; +import * as Yup from 'yup'; +import mapValues from 'lodash/mapValues'; +import { translateFrom } from '@openmrs/esm-framework'; +import { type RegistrationConfig } from '../../config-schema'; +import { type FormValues } from '../patient-registration.types'; +import { getDatetime } from '../patient-registration.resource'; + +const t = (key: string, value: string) => translateFrom('@openmrs/esm-framework', key, value); + +export function getValidationSchema(config: RegistrationConfig) { + return Yup.object({ + givenName: Yup.string().required(t('givenNameRequired', 'Given name is required')), + familyName: Yup.string().required(t('familyNameRequired', 'Family name is required')), + additionalGivenName: Yup.string().when('addNameInLocalLanguage', { + is: true, + then: Yup.string().required(t('givenNameRequired', 'Given name is required')), + otherwise: Yup.string().notRequired(), + }), + additionalFamilyName: Yup.string().when('addNameInLocalLanguage', { + is: true, + then: Yup.string().required(t('familyNameRequired', 'Family name is required')), + otherwise: Yup.string().notRequired(), + }), + gender: Yup.string() + .oneOf( + config.fieldConfigurations.gender.map((g) => g.value), + t('genderUnspecified', 'Gender unspecified'), + ) + .required(t('genderRequired', 'Gender is required')), + birthdate: Yup.date().when('birthdateEstimated', { + is: false, + then: Yup.date() + .required(t('birthdayRequired', 'Birthday is required')) + .max(Date(), t('birthdayNotInTheFuture', 'Birthday cannot be in future')) + .nullable(), + otherwise: Yup.date().nullable(), + }), + yearsEstimated: Yup.number().when('birthdateEstimated', { + is: true, + then: Yup.number() + .required(t('yearsEstimateRequired', 'Estimated years required')) + .min(0, t('negativeYears', 'Estimated years cannot be negative')), + otherwise: Yup.number().nullable(), + }), + monthsEstimated: Yup.number().min(0, t('negativeMonths', 'Estimated months cannot be negative')), + isDead: Yup.boolean(), + deathDate: Yup.date() + .when('isDead', { + is: true, + then: Yup.date().required(t('deathDateRequired', 'Death date is required')), + otherwise: Yup.date().nullable(), + }) + .max(new Date(), 'deathDateInFuture') + .test( + 'deathDate-after-birthdate', + t('deathdayInvalidDate', 'Death date and time cannot be before the birthday'), + function (value) { + const { birthdate } = this.parent; + if (birthdate && value) { + return dayjs(value).isAfter(birthdate); + } + return true; + }, + ) + .test('deathDate-before-today', t('deathDateInFuture', 'Death date cannot be in future'), function (value) { + const { deathTime, deathTimeFormat } = this.parent; + if (value && deathTime && deathTimeFormat && /^(1[0-2]|0?[1-9]):([0-5]?[0-9])$/.test(deathTime)) { + return dayjs(getDatetime(value, deathTime, deathTimeFormat)).isBefore(dayjs()); + } + return true; + }), + deathTime: Yup.string() + .when('isDead', { + is: true, + then: Yup.string().required(t('deathTimeRequired', 'Death time is required')), + otherwise: Yup.string().nullable(), + }) + .matches(/^(1[0-2]|0?[1-9]):([0-5]?[0-9])$/, t('deathTimeInvalid', "Time doesn't match the format 'hh:mm'")), + + deathTimeFormat: Yup.string() + .when('isDead', { + is: true, + then: Yup.string().required(t('deathTimeFormatRequired', 'Time format is required')), + otherwise: Yup.string().nullable(), + }) + .oneOf(['AM', 'PM'], t('deathTimeFormatInvalid', 'Time format is invalid')), + + deathCause: Yup.string().when('isDead', { + is: true, + then: Yup.string().required(t('deathCauseRequired', 'Cause of death is required')), + otherwise: Yup.string().nullable(), + }), + nonCodedCauseOfDeath: Yup.string().when(['isDead', 'deathCause'], { + is: (isDead, deathCause) => isDead && deathCause === config.freeTextFieldConceptUuid, + then: Yup.string().required(t('nonCodedCauseOfDeathRequired', 'Cause of death is required')), + otherwise: Yup.string().nullable(), + }), + email: Yup.string().optional().email(t('invalidEmail', 'Invalid email')), + identifiers: Yup.lazy((obj: FormValues['identifiers']) => + Yup.object( + mapValues(obj, () => + Yup.object({ + required: Yup.bool(), + identifierValue: Yup.string().when('required', { + is: true, + then: Yup.string().required(t('identifierValueRequired', 'Identifier value is required')), + otherwise: Yup.string().notRequired(), + }), + }), + ), + ), + ), + relationships: Yup.array().of( + Yup.object().shape({ + relatedPersonUuid: Yup.string().required(), + relationshipType: Yup.string().required(), + }), + ), + }); +} diff --git a/packages/esm-patient-registration-app/src/patient-registration/validation/patient-registration-validation.tsx b/packages/esm-patient-registration-app/src/patient-registration/validation/patient-registration-validation.tsx deleted file mode 100644 index adba6a859..000000000 --- a/packages/esm-patient-registration-app/src/patient-registration/validation/patient-registration-validation.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import * as Yup from 'yup'; -import mapValues from 'lodash/mapValues'; -import { type FormValues } from '../patient-registration.types'; -import { type RegistrationConfig } from '../../config-schema'; - -export function getValidationSchema(config: RegistrationConfig) { - return Yup.object({ - givenName: Yup.string().required('givenNameRequired'), - familyName: Yup.string().required('familyNameRequired'), - additionalGivenName: Yup.string().when('addNameInLocalLanguage', { - is: true, - then: Yup.string().required('givenNameRequired'), - otherwise: Yup.string().notRequired(), - }), - additionalFamilyName: Yup.string().when('addNameInLocalLanguage', { - is: true, - then: Yup.string().required('familyNameRequired'), - otherwise: Yup.string().notRequired(), - }), - gender: Yup.string() - .oneOf( - config.fieldConfigurations.gender.map((g) => g.value), - 'genderUnspecified', - ) - .required('genderRequired'), - birthdate: Yup.date().when('birthdateEstimated', { - is: false, - then: Yup.date().required('birthdayRequired').max(Date(), 'birthdayNotInTheFuture').nullable(), - otherwise: Yup.date().nullable(), - }), - yearsEstimated: Yup.number().when('birthdateEstimated', { - is: true, - then: Yup.number().required('yearsEstimateRequired').min(0, 'negativeYears'), - otherwise: Yup.number().nullable(), - }), - monthsEstimated: Yup.number().min(0, 'negativeMonths'), - deathDate: Yup.date().max(Date(), 'deathdayNotInTheFuture').nullable(), - email: Yup.string().optional().email('invalidEmail'), - identifiers: Yup.lazy((obj: FormValues['identifiers']) => - Yup.object( - mapValues(obj, () => - Yup.object({ - required: Yup.bool(), - identifierValue: Yup.string().when('required', { - is: true, - then: Yup.string().required('identifierValueRequired'), - otherwise: Yup.string().notRequired(), - }), - }), - ), - ), - ), - relationships: Yup.array().of( - Yup.object().shape({ - relatedPersonUuid: Yup.string().required(), - relationshipType: Yup.string().required(), - }), - ), - }); -} diff --git a/packages/esm-patient-registration-app/src/routes.json b/packages/esm-patient-registration-app/src/routes.json index 3910bdbf1..8e55b60df 100644 --- a/packages/esm-patient-registration-app/src/routes.json +++ b/packages/esm-patient-registration-app/src/routes.json @@ -25,12 +25,6 @@ "online": true, "offline": true }, - { - "component": "cancelPatientEditModal", - "name": "cancel-patient-edit-modal", - "online": true, - "offline": true - }, { "component": "patientPhotoExtension", "name": "patient-photo-widget", @@ -58,5 +52,11 @@ "online": true, "offline": true } + ], + "modals": [ + { + "name": "cancel-patient-edit-modal", + "component": "cancelPatientEditModal" + } ] } diff --git a/packages/esm-patient-registration-app/src/widgets/cancel-patient-edit.component.tsx b/packages/esm-patient-registration-app/src/widgets/cancel-patient-edit.component.tsx deleted file mode 100644 index e7d1b86b9..000000000 --- a/packages/esm-patient-registration-app/src/widgets/cancel-patient-edit.component.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import { Button } from '@carbon/react'; -import { useTranslation } from 'react-i18next'; - -interface CancelPatientEditProps { - close(): void; - onConfirm(): void; -} - -const CancelPatientEdit: React.FC = ({ close, onConfirm }) => { - const { t } = useTranslation(); - return ( - <> -
-

{t('discardModalHeader', 'Confirm Discard Changes')}

-
-
-

- {t( - 'discardModalBody', - "The changes you made to this patient's details have not been saved. Discard changes?", - )} -

-
-
- - -
- - ); -}; - -export default CancelPatientEdit; diff --git a/packages/esm-patient-registration-app/src/widgets/cancel-patient-edit.modal.tsx b/packages/esm-patient-registration-app/src/widgets/cancel-patient-edit.modal.tsx new file mode 100644 index 000000000..26ffd7b59 --- /dev/null +++ b/packages/esm-patient-registration-app/src/widgets/cancel-patient-edit.modal.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Button, ModalBody, ModalFooter, ModalHeader } from '@carbon/react'; + +interface CancelPatientEditPropsModal { + close(): void; + onConfirm(): void; +} + +const CancelPatientEditModal: React.FC = ({ close, onConfirm }) => { + const { t } = useTranslation(); + return ( + <> + + +

{t('confirmDiscardChangesBody', 'Your unsaved changes will be lost if you proceed to discard the form')}.

+
+ + + + + + ); +}; + +export default CancelPatientEditModal; diff --git a/packages/esm-patient-registration-app/src/widgets/cancel-patient-edit.test.tsx b/packages/esm-patient-registration-app/src/widgets/cancel-patient-edit.test.tsx index e454fd16d..40380a3ca 100644 --- a/packages/esm-patient-registration-app/src/widgets/cancel-patient-edit.test.tsx +++ b/packages/esm-patient-registration-app/src/widgets/cancel-patient-edit.test.tsx @@ -1,15 +1,14 @@ import React from 'react'; import userEvent from '@testing-library/user-event'; import { screen, render } from '@testing-library/react'; -import CancelPatientEdit from './cancel-patient-edit.component'; +import CancelPatientEdit from './cancel-patient-edit.modal'; -describe('CancelPatientEdit component', () => { +describe('CancelPatientEdit modal', () => { const mockClose = jest.fn(); const mockOnConfirm = jest.fn(); it('renders the modal and triggers close and onConfirm functions', async () => { const user = userEvent.setup(); - render(); const cancelButton = screen.getByRole('button', { name: /Cancel/i }); diff --git a/packages/esm-patient-registration-app/translations/en.json b/packages/esm-patient-registration-app/translations/en.json index ed4e4dfc4..9a7e5b919 100644 --- a/packages/esm-patient-registration-app/translations/en.json +++ b/packages/esm-patient-registration-app/translations/en.json @@ -3,69 +3,83 @@ "addressHeader": "Address", "allFieldsRequiredText": "All fields are required unless marked optional", "autoGeneratedPlaceholderText": "Auto-generated", - "birthdayNotInTheFuture": "Birthday cannot be in the future", + "birthdayNotInTheFuture": "Birthday cannot be in future", "birthdayRequired": "Birthday is required", "birthFieldLabelText": "Birth", "cancel": "Cancel", - "causeOfDeathInputLabel": "Cause of Death", + "causeOfDeathInputLabel": "Cause of death", "closeOverlay": "Close overlay", "codedPersonAttributeAnswerSetEmpty": "The coded person attribute field '{{codedPersonAttributeFieldId}}' has been defined with an answer concept set UUID '{{answerConceptSetUuid}}' that does not have any concept answers.", "codedPersonAttributeAnswerSetInvalid": "The coded person attribute field '{{codedPersonAttributeFieldId}}' has been defined with an invalid answer concept set UUID '{{answerConceptSetUuid}}'.", "codedPersonAttributeNoAnswerSet": "The person attribute field '{{codedPersonAttributeFieldId}}' is of type 'coded' but has been defined without an answer concept set UUID. The 'answerConceptSetUuid' key is required.", "configure": "Configure", "configureIdentifiers": "Configure identifiers", + "confirmDiscardChangesBody": "Your unsaved changes will be lost if you proceed to discard the form", + "confirmDiscardChangesTitle": "Are you sure you want to discard these changes?", "confirmIdentifierDeletionText": "Are you sure you want to remove this identifier?", "contactSection": "Contact Details", "createNewPatient": "Create new patient", - "dateOfBirthLabelText": "Date of Birth", - "deathDateInputLabel": "Date of Death", - "deathdayNotInTheFuture": "Death day cannot be in the future", + "dateOfBirthLabelText": "Date of birth", + "deathCauseRequired": "Cause of death is required", + "deathDateInFuture": "Death date cannot be in future", + "deathDateInputLabel": "Date of death", + "deathDateRequired": "Death date is required", + "deathdayInvalidDate": "Death date and time cannot be before the birthday", + "deathdayIsRequired": "Death date is required when the patient is marked as deceased.", + "deathdayNotInTheFuture": "", "deathSection": "Death Info", + "deathTimeFormatInvalid": "Time format is invalid", + "deathTimeFormatRequired": "Time format is required", + "deathTimeInvalid": "Time doesn't match the format 'hh:mm'", + "deathTimeRequired": "Death time is required", "deleteIdentifierModalHeading": "Remove identifier?", "deleteIdentifierModalText": " has a value of ", "deleteIdentifierTooltip": "Delete", "deleteRelationshipTooltipText": "Delete", "demographicsSection": "Basic Info", "discard": "Discard", - "discardModalBody": "The changes you made to this patient's details have not been saved. Discard changes?", - "discardModalHeader": "Confirm Discard Changes", "dobToggleLabelText": "Date of Birth Known?", "editIdentifierTooltip": "Edit", "editPatientDetails": "Edit patient details", "editPatientDetailsBreadcrumb": "Edit patient details", + "enterNonCodedCauseOfDeath": "Enter non-coded cause of death", "error": "Error", + "errorFetchingCodedCausesOfDeath": "Error fetching coded causes of death", "errorFetchingOrderedFields": "Error occured fetching ordered fields for address hierarchy", "estimatedAgeInMonthsLabelText": "Estimated age in months", "estimatedAgeInYearsLabelText": "Estimated age in years", "familyNameLabelText": "Family Name", "familyNameRequired": "Family name is required", "female": "Female", + "fieldsWithErrors": "The following fields have errors: ", "fullNameLabelText": "Full Name", "genderLabelText": "Sex", "genderRequired": "Gender is required", - "genderUnspecified": "Gender is not specified", + "genderUnspecified": "Gender unspecified", "givenNameLabelText": "First Name", "givenNameRequired": "Given name is required", "identifierValueRequired": "Identifier value is required", "idFieldLabelText": "Identifiers", "IDInstructions": "Select the identifiers you'd like to add for this patient:", - "incompleteForm": "Incomplete form", - "invalidEmail": "A valid email has to be given", + "invalidEmail": "Invalid email", "invalidInput": "Invalid Input", - "isDeadInputLabel": "Is Dead", + "isDeadInputLabel": "Is dead", "jumpTo": "Jump to", "male": "Male", "middleNameLabelText": "Middle Name", - "negativeMonths": "Negative months", - "negativeYears": "Negative years", + "negativeMonths": "Estimated months cannot be negative", + "negativeYears": "Estimated years cannot be negative", "no": "No", + "nonCodedCauseOfDeath": "Non-coded cause of death", + "nonCodedCauseOfDeathRequired": "Cause of death is required", "numberInNameDubious": "Number in name is dubious", "obsFieldUnknownDatatype": "Concept for obs field '{{fieldDefinitionId}}' has unknown datatype '{{datatypeName}}'", "optional": "optional", "other": "Other", "patientNameKnown": "Patient's Name is Known?", "patientRegistrationBreadcrumb": "Patient Registration", - "registerPatient": "Register Patient", + "refreshOrContactAdmin": "Try refreshing the page or contact your system administrator", + "registerPatient": "Register patient", "registerPatientSuccessSnackbarSubtitle": "The patient can now be found by searching for them using their name or ID number", "registerPatientSuccessSnackbarTitle": "New Patient Created", "registrationErrorSnackbarTitle": "Patient Registration Failed", @@ -75,7 +89,7 @@ "relationshipRemovedText": "Relationship removed", "relationshipsSection": "Relationships", "relationshipToPatient": "Relationship to patient", - "relativeFullNameLabelText": "Related person", + "relativeFullNameLabelText": "Full name", "relativeNamePlaceholder": "Firstname Familyname", "removeIdentifierButton": "Remove Identifier", "resetIdentifierTooltip": "Reset", @@ -85,15 +99,16 @@ "selectAnOption": "Select an option", "sexFieldLabelText": "Sex", "source": "Source", - "stroke": "Stroke", "submitting": "Submitting", - "unableToFetch": "Unable to fetch person attribute type {{personattributetype}}", + "timeFormat": "Time Format", + "timeOfDeathInputLabel": "Time of death (hh:mm)", + "unableToFetch": "Unable to fetch person attribute type - {{personattributetype}}", "unknown": "Unknown", "unknownPatientAttributeType": "Patient attribute type has unknown format {{personAttributeTypeFormat}}", - "updatePatient": "Update Patient", + "updatePatient": "Update patient", "updatePatientErrorSnackbarTitle": "Patient Details Update Failed", "updatePatientSuccessSnackbarSubtitle": "The patient's information has been successfully updated", "updatePatientSuccessSnackbarTitle": "Patient Details Updated", - "yearsEstimateRequired": "Years estimate required", + "yearsEstimateRequired": "Estimated years required", "yes": "Yes" } diff --git a/packages/esm-ward-app/.fetch.swp b/packages/esm-ward-app/.fetch.swp new file mode 100644 index 000000000..9a648bb5e Binary files /dev/null and b/packages/esm-ward-app/.fetch.swp differ diff --git a/packages/esm-ward-app/src/ward-patient-card/row-elements/ward-patient-coded-obs-tags.tsx b/packages/esm-ward-app/src/ward-patient-card/row-elements/ward-patient-coded-obs-tags.tsx index ee1a2e1d3..93c5274f8 100644 --- a/packages/esm-ward-app/src/ward-patient-card/row-elements/ward-patient-coded-obs-tags.tsx +++ b/packages/esm-ward-app/src/ward-patient-card/row-elements/ward-patient-coded-obs-tags.tsx @@ -39,7 +39,7 @@ const WardPatientCodedObsTags: React.FC = ({ confi } else { const obsToDisplay = data?.filter((o) => { const matchVisit = o.encounter.visit?.uuid == visit?.uuid; - return matchVisit || visit == null; // TODO: remove visit == null hack when server API supports returning visit + return matchVisit; }); const summaryLabelToDisplay = summaryLabel != null ? t(summaryLabel) : obsToDisplay?.[0]?.concept?.display; diff --git a/packages/esm-ward-app/src/ward-view-header/ward-metrics.test.tsx b/packages/esm-ward-app/src/ward-view-header/ward-metrics.test.tsx index bfbbfc088..ef6ff8f86 100644 --- a/packages/esm-ward-app/src/ward-view-header/ward-metrics.test.tsx +++ b/packages/esm-ward-app/src/ward-view-header/ward-metrics.test.tsx @@ -8,6 +8,7 @@ import { useAdmissionLocation } from '../hooks/useAdmissionLocation'; import { mockAdmissionLocation, mockInpatientAdmissions } from '__mocks__'; import { useInpatientAdmission } from '../hooks/useInpatientAdmission'; import useWardLocation from '../hooks/useWardLocation'; +import { screen } from '@testing-library/react'; jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), @@ -68,9 +69,9 @@ describe('Ward Metrics', () => { invalidLocation: true, }); const bedMetrics = getWardMetrics(mockWardBeds); - const { getByText } = renderWithSwr(); + renderWithSwr(); for (let [key, value] of Object.entries(bedMetrics)) { - expect(getByText(value)).toBeInTheDocument(); + expect(screen.getByText(value)).toBeInTheDocument(); } }); }); diff --git a/packages/esm-ward-app/src/ward-view/ward-view.component.tsx b/packages/esm-ward-app/src/ward-view/ward-view.component.tsx index 525014c92..3bc67ced8 100644 --- a/packages/esm-ward-app/src/ward-view/ward-view.component.tsx +++ b/packages/esm-ward-app/src/ward-view/ward-view.component.tsx @@ -1,23 +1,20 @@ -import React from 'react'; import { InlineNotification } from '@carbon/react'; import { - ExtensionSlot, - openmrsFetch, useAppContext, useDefineAppContext, - WorkspaceContainer, + WorkspaceContainer } from '@openmrs/esm-framework'; +import React, { useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import EmptyBedSkeleton from '../beds/empty-bed-skeleton'; import UnassignedPatient from '../beds/unassigned-patient.component'; import useWardLocation from '../hooks/useWardLocation'; -import { type WardPatientGroupDetails, type WardPatient } from '../types'; +import { useWardPatientGrouping } from '../hooks/useWardPatientGrouping'; +import { type WardPatient, type WardPatientGroupDetails } from '../types'; import WardViewHeader from '../ward-view-header/ward-view-header.component'; import WardBed from './ward-bed.component'; import { bedLayoutToBed } from './ward-view.resource'; import styles from './ward-view.scss'; -import { useWardPatientGrouping } from '../hooks/useWardPatientGrouping'; -import useSWR from 'swr'; const WardView = () => { const response = useWardLocation(); @@ -46,22 +43,54 @@ const WardView = () => { const WardViewMain = () => { const { location } = useWardLocation(); + const { t } = useTranslation(); const wardPatientsGrouping = useAppContext('ward-patients-group'); const { bedLayouts, wardAdmittedPatientsWithBed, wardUnassignedPatientsList } = wardPatientsGrouping ?? {}; const { isLoading: isLoadingAdmissionLocation, error: errorLoadingAdmissionLocation } = wardPatientsGrouping?.admissionLocationResponse ?? {}; - const { isLoading: isLoadingInpatientAdmissions, error: errorLoadingInpatientAdmissions } = - wardPatientsGrouping?.inpatientAdmissionResponse ?? {}; + const { + isLoading: isLoadingInpatientAdmissions, + error: errorLoadingInpatientAdmissions, + hasMore: hasMoreInpatientAdmissions, + loadMore: loadMoreInpatientAdmissions + } = wardPatientsGrouping?.inpatientAdmissionResponse ?? {}; - const { t } = useTranslation(); + const scrollToLoadMoreTrigger = useRef(null); + useEffect( + function scrollToLoadMore() { + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + if (hasMoreInpatientAdmissions && !errorLoadingInpatientAdmissions && !isLoadingInpatientAdmissions) { + loadMoreInpatientAdmissions(); + } + } + }); + }, + { threshold: 1 }, + ); + + if (scrollToLoadMoreTrigger.current) { + observer.observe(scrollToLoadMoreTrigger.current); + } + return () => { + if (scrollToLoadMoreTrigger.current) { + observer.unobserve(scrollToLoadMoreTrigger.current); + } + }; + }, + [scrollToLoadMoreTrigger, hasMoreInpatientAdmissions, errorLoadingInpatientAdmissions, loadMoreInpatientAdmissions], + ); if (!wardPatientsGrouping) return <>; + const wardBeds = bedLayouts?.map((bedLayout) => { const { patients } = bedLayout; const bed = bedLayoutToBed(bedLayout); const wardPatients: WardPatient[] = patients.map((patient): WardPatient => { - const inpatientAdmission = wardAdmittedPatientsWithBed?.get(patient.uuid); + const inpatientAdmission = wardAdmittedPatientsWithBed.get(patient.uuid); if (inpatientAdmission) { const { patient, visit, currentInpatientRequest } = inpatientAdmission; return { patient, visit, bed, inpatientAdmission, inpatientRequest: currentInpatientRequest || null }; @@ -125,6 +154,7 @@ const WardViewMain = () => { subtitle={errorLoadingInpatientAdmissions?.message} /> )} +
); }; diff --git a/packages/esm-ward-app/src/ward-view/ward-view.resource.ts b/packages/esm-ward-app/src/ward-view/ward-view.resource.ts index 7b9bd7b09..b7c39ed8e 100644 --- a/packages/esm-ward-app/src/ward-view/ward-view.resource.ts +++ b/packages/esm-ward-app/src/ward-view/ward-view.resource.ts @@ -31,7 +31,7 @@ export function getWardMetrics(beds: Bed[]): WardMetrics { freeBeds: '--', capacity: '--', }; - if (!beds?.length) return bedMetrics; + if (beds == null || beds.length == 0) return bedMetrics; const total = beds.length; const occupiedBeds = beds.filter((bed) => bed.status === 'OCCUPIED'); const patients = occupiedBeds.length; diff --git a/packages/esm-ward-app/src/ward-workspace/admission-request-card/admission-request-card-header.component.tsx b/packages/esm-ward-app/src/ward-workspace/admission-request-card/admission-request-card-header.component.tsx index ed7410683..0a239a1ba 100644 --- a/packages/esm-ward-app/src/ward-workspace/admission-request-card/admission-request-card-header.component.tsx +++ b/packages/esm-ward-app/src/ward-workspace/admission-request-card/admission-request-card-header.component.tsx @@ -1,12 +1,11 @@ import { ExtensionSlot, formatDatetime, getLocale } from '@openmrs/esm-framework'; import classNames from 'classnames'; import React from 'react'; -import WardPatientWorkspaceBanner from '../patient-banner/patient-banner.component'; -import styles from './admission-request-card.scss'; -import type WardPatientCard from '../../ward-patient-card/ward-patient-card.component'; import { useCurrentWardCardConfig } from '../../hooks/useCurrentWardCardConfig'; -import { WardPatientCardElement } from '../../ward-patient-card/ward-patient-card-element.component'; import WardPatientName from '../../ward-patient-card/row-elements/ward-patient-name'; +import { WardPatientCardElement } from '../../ward-patient-card/ward-patient-card-element.component'; +import type WardPatientCard from '../../ward-patient-card/ward-patient-card.component'; +import styles from './admission-request-card.scss'; const AdmissionRequestCardHeader: WardPatientCard = (wardPatient) => { const { inpatientRequest } = wardPatient; @@ -42,7 +41,7 @@ const AdmissionRequestCardHeader: WardPatientCard = (wardPatient) => {
); diff --git a/packages/esm-ward-app/src/ward-workspace/admission-request-card/admission-request-card.scss b/packages/esm-ward-app/src/ward-workspace/admission-request-card/admission-request-card.scss index 067cf4be9..b7496b63c 100644 --- a/packages/esm-ward-app/src/ward-workspace/admission-request-card/admission-request-card.scss +++ b/packages/esm-ward-app/src/ward-workspace/admission-request-card/admission-request-card.scss @@ -25,10 +25,14 @@ } } -.admissionRequestCardRow { - padding: layout.$spacing-05; - margin: layout.$spacing-03; - background-color: white; +.admissionRequestCardExtensionSlot { + display: none; + + &:has(div:not(:empty)) { + display: block; + margin: layout.$spacing-03 0; + background-color: white; + } } .admissionEncounterDetails { diff --git a/packages/esm-ward-app/src/ward-workspace/admit-patient-form-workspace/admit-patient-form.test.tsx b/packages/esm-ward-app/src/ward-workspace/admit-patient-form-workspace/admit-patient-form.test.tsx index f8e767ff3..959017213 100644 --- a/packages/esm-ward-app/src/ward-workspace/admit-patient-form-workspace/admit-patient-form.test.tsx +++ b/packages/esm-ward-app/src/ward-workspace/admit-patient-form-workspace/admit-patient-form.test.tsx @@ -5,6 +5,7 @@ import { renderWithSwr } from '../../../../../tools'; import AdmitPatientFormWorkspace from './admit-patient-form.workspace'; import { mockAdmissionLocation, + mockInpatientAdmissions, mockInpatientRequest, mockLocationInpatientWard, mockPatientAlice, @@ -12,10 +13,13 @@ import { import type { DispositionType } from '../../types'; import type { AdmitPatientFormWorkspaceProps } from './types'; import { useAdmissionLocation } from '../../hooks/useAdmissionLocation'; -import { openmrsFetch, provide, showSnackbar, useFeatureFlag, useSession } from '@openmrs/esm-framework'; +import { openmrsFetch, provide, showSnackbar, useAppContext, useFeatureFlag, useSession } from '@openmrs/esm-framework'; import useEmrConfiguration from '../../hooks/useEmrConfiguration'; import useWardLocation from '../../hooks/useWardLocation'; import { useInpatientRequest } from '../../hooks/useInpatientRequest'; +import { useWardPatientGrouping } from '../../hooks/useWardPatientGrouping'; +import { getInpatientAdmissionsUuidMap, createAndGetWardPatientGrouping } from '../../ward-view/ward-view.resource'; +import { useInpatientAdmission } from '../../hooks/useInpatientAdmission'; jest.mock('../../hooks/useAdmissionLocation', () => ({ useAdmissionLocation: jest.fn(), @@ -29,14 +33,43 @@ jest.mock('../../hooks/useInpatientRequest', () => ({ useInpatientRequest: jest.fn(), })); +jest.mock('../../hooks/useWardPatientGrouping', () => ({ + useWardPatientGrouping: jest.fn(), +})); + +jest.mock('../../hooks/useInpatientAdmission', () => ({ + useInpatientAdmission: jest.fn(), +})); + +const inpatientAdmissionsUuidMap = getInpatientAdmissionsUuidMap(mockInpatientAdmissions); + const mockedUseInpatientRequest = jest.mocked(useInpatientRequest); const mockedUseEmrConfiguration = jest.mocked(useEmrConfiguration); const mockedUseWardLocation = jest.mocked(useWardLocation); const mockedOpenmrsFetch = jest.mocked(openmrsFetch); -const mockedUseAdmissionLocation = jest.mocked(useAdmissionLocation); +const mockedUseAdmissionLocation = jest.mocked(useAdmissionLocation).mockReturnValue({ + isLoading: false, + isValidating: false, + admissionLocation: mockAdmissionLocation, + mutate: jest.fn(), + error: undefined, +}); const mockedUseFeatureFlag = jest.mocked(useFeatureFlag); const mockedShowSnackbar = jest.mocked(showSnackbar); const mockedUseSession = jest.mocked(useSession); +const mockInpatientAdmissionResponse = jest.mocked(useInpatientAdmission).mockReturnValue({ + error: undefined, + mutate: jest.fn(), + isValidating: false, + isLoading: false, + inpatientAdmissions: mockInpatientAdmissions, +}); +const mockWardPatientGroupDetails = jest.mocked(useWardPatientGrouping).mockReturnValue({ + admissionLocationResponse: mockedUseAdmissionLocation(), + inpatientAdmissionResponse: mockInpatientAdmissionResponse(), + ...createAndGetWardPatientGrouping(mockInpatientAdmissions, mockAdmissionLocation, inpatientAdmissionsUuidMap), +}); +jest.mocked(useAppContext).mockReturnValue(mockWardPatientGroupDetails()); const mockWorkspaceProps: AdmitPatientFormWorkspaceProps = { patient: mockPatientAlice, @@ -56,13 +89,7 @@ const mockedMutateInpatientRequest = jest.fn(); describe('Testing AdmitPatientForm', () => { beforeEach(() => { jest.clearAllMocks(); - mockedUseAdmissionLocation.mockReturnValue({ - isLoading: false, - isValidating: false, - admissionLocation: mockAdmissionLocation, - mutate: jest.fn(), - error: undefined, - }); + mockedUseSession.mockReturnValue({ currentProvider: { uuid: 'current-provider-uuid', @@ -157,30 +184,15 @@ describe('Testing AdmitPatientForm', () => { it('should render admit patient form if bed management module is present, but no beds are configured', () => { mockedUseFeatureFlag.mockReturnValue(true); - mockedUseAdmissionLocation.mockReturnValueOnce({ - isLoading: false, - isValidating: false, - admissionLocation: { - ...mockAdmissionLocation, - totalBeds: 0, - bedLayouts: [], - }, - mutate: jest.fn(), - error: null, - }); + const replacedProperty = jest.replaceProperty(mockWardPatientGroupDetails(), 'bedLayouts', []); + // @ts-i renderAdmissionForm(); expect(screen.getByText('Select a bed')).toBeInTheDocument(); expect(screen.getByText('No beds configured for Inpatient Ward location')).toBeInTheDocument(); + replacedProperty.restore(); }); it('should submit the form, create encounter and submit bed', async () => { - mockedUseAdmissionLocation.mockReturnValueOnce({ - isLoading: false, - isValidating: false, - admissionLocation: mockAdmissionLocation, - mutate: jest.fn(), - error: null, - }); // @ts-ignore - we only need these two keys for now mockedOpenmrsFetch.mockResolvedValue({ ok: true, @@ -290,17 +302,7 @@ describe('Testing AdmitPatientForm', () => { }); it('should admit patient if no beds are configured', async () => { - mockedUseAdmissionLocation.mockReturnValueOnce({ - isLoading: false, - isValidating: false, - admissionLocation: { - ...mockAdmissionLocation, - totalBeds: 0, - bedLayouts: [], - }, - mutate: jest.fn(), - error: null, - }); + const replacedProperty = jest.replaceProperty(mockWardPatientGroupDetails(), 'bedLayouts', []); // @ts-ignore - we only need these two keys for now mockedOpenmrsFetch.mockResolvedValue({ ok: true, @@ -337,5 +339,6 @@ describe('Testing AdmitPatientForm', () => { subtitle: 'Patient admitted successfully to Inpatient Ward', title: 'Patient admitted successfully', }); + replacedProperty.restore(); }); }); diff --git a/packages/esm-ward-app/src/ward-workspace/admit-patient-form-workspace/admit-patient-form.workspace.tsx b/packages/esm-ward-app/src/ward-workspace/admit-patient-form-workspace/admit-patient-form.workspace.tsx index cc8d4845a..5799d0bde 100644 --- a/packages/esm-ward-app/src/ward-workspace/admit-patient-form-workspace/admit-patient-form.workspace.tsx +++ b/packages/esm-ward-app/src/ward-workspace/admit-patient-form-workspace/admit-patient-form.workspace.tsx @@ -4,11 +4,10 @@ import { Controller, useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { useTranslation } from 'react-i18next'; import { Button, ButtonSet, Column, Dropdown, DropdownSkeleton, Form, InlineNotification, Row } from '@carbon/react'; -import { showSnackbar, useFeatureFlag, useSession } from '@openmrs/esm-framework'; +import { showSnackbar, useAppContext, useFeatureFlag, useSession } from '@openmrs/esm-framework'; import { filterBeds } from '../../ward-view/ward-view.resource'; -import type { BedLayout } from '../../types'; +import type { BedLayout, WardPatientGroupDetails } from '../../types'; import { assignPatientToBed, createEncounter } from '../../ward.resource'; -import { useAdmissionLocation } from '../../hooks/useAdmissionLocation'; import { useInpatientRequest } from '../../hooks/useInpatientRequest'; import useEmrConfiguration from '../../hooks/useEmrConfiguration'; import useWardLocation from '../../hooks/useWardLocation'; @@ -29,8 +28,9 @@ const AdmitPatientFormWorkspace: React.FC = ({ const { mutate: mutateInpatientRequest } = useInpatientRequest(); const { emrConfiguration, isLoadingEmrConfiguration, errorFetchingEmrConfiguration } = useEmrConfiguration(); const [showErrorNotifications, setShowErrorNotifications] = useState(false); - const { isLoading, admissionLocation, mutate: mutateAdmissionLocation } = useAdmissionLocation(); - const beds = useMemo(() => (isLoading ? [] : filterBeds(admissionLocation)), [admissionLocation]); + const wardPatientGrouping = useAppContext('ward-patients-group'); + const { isLoading, mutate: mutateAdmissionLocation } = wardPatientGrouping?.admissionLocationResponse ?? {}; + const beds = isLoading ? [] : wardPatientGrouping?.bedLayouts ?? []; const isBedManagementModuleInstalled = useFeatureFlag('bedmanagement-module'); const getBedRepresentation = useCallback((bedLayout: BedLayout) => { const bedNumber = bedLayout.bedNumber; @@ -175,6 +175,7 @@ const AdmitPatientFormWorkspace: React.FC = ({ setIsSubmitting(false); }, []); + if (!wardPatientGrouping) return <>; return (
diff --git a/packages/esm-ward-app/src/ward-workspace/patient-discharge/patient-discharge.workspace.tsx b/packages/esm-ward-app/src/ward-workspace/patient-discharge/patient-discharge.workspace.tsx index 9643e2370..23e68198f 100644 --- a/packages/esm-ward-app/src/ward-workspace/patient-discharge/patient-discharge.workspace.tsx +++ b/packages/esm-ward-app/src/ward-workspace/patient-discharge/patient-discharge.workspace.tsx @@ -1,14 +1,13 @@ import React, { useCallback, useState } from 'react'; -import { ExtensionSlot, showSnackbar, useSession } from '@openmrs/esm-framework'; +import { ExtensionSlot, showSnackbar, useAppContext, useSession } from '@openmrs/esm-framework'; import { Button, ButtonSet, InlineNotification } from '@carbon/react'; import { useTranslation } from 'react-i18next'; import styles from './patient-discharge.scss'; import WardPatientWorkspaceBanner from '../patient-banner/patient-banner.component'; -import type { WardPatientWorkspaceProps } from '../../types'; +import {type WardPatientGroupDetails, type WardPatientWorkspaceProps } from '../../types'; import useEmrConfiguration from '../../hooks/useEmrConfiguration'; import { createEncounter, removePatientFromBed } from '../../ward.resource'; import useWardLocation from '../../hooks/useWardLocation'; -import { useAdmissionLocation } from '../../hooks/useAdmissionLocation'; import { useInpatientRequest } from '../../hooks/useInpatientRequest'; import { Exit } from '@carbon/react/icons'; @@ -19,7 +18,8 @@ export default function PatientDischargeWorkspace(props: WardPatientWorkspacePro const { currentProvider } = useSession(); const { location } = useWardLocation(); const { emrConfiguration, isLoadingEmrConfiguration, errorFetchingEmrConfiguration } = useEmrConfiguration(); - const { mutate: mutateAdmissionLocation } = useAdmissionLocation(); + const wardGroupingDetails = useAppContext('ward-patients-group'); + const { mutate: mutateAdmissionLocation } = wardGroupingDetails?.admissionLocationResponse ?? {}; const { mutate: mutateInpatientRequest } = useInpatientRequest(); const submitDischarge = useCallback(() => { @@ -74,6 +74,7 @@ export default function PatientDischargeWorkspace(props: WardPatientWorkspacePro mutateInpatientRequest, ]); + if (!wardGroupingDetails) return <>; return (
diff --git a/packages/esm-ward-app/src/ward-workspace/patient-transfer-bed-swap/patient-bed-swap-form.component.tsx b/packages/esm-ward-app/src/ward-workspace/patient-transfer-bed-swap/patient-bed-swap-form.component.tsx index 728c84813..9e387c015 100644 --- a/packages/esm-ward-app/src/ward-workspace/patient-transfer-bed-swap/patient-bed-swap-form.component.tsx +++ b/packages/esm-ward-app/src/ward-workspace/patient-transfer-bed-swap/patient-bed-swap-form.component.tsx @@ -1,15 +1,14 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import styles from './patient-transfer-swap.scss'; -import { useAdmissionLocation } from '../../hooks/useAdmissionLocation'; import { z } from 'zod'; import { useTranslation } from 'react-i18next'; import { Controller, useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { filterBeds } from '../../ward-view/ward-view.resource'; -import type { BedLayout, WardPatientWorkspaceProps } from '../../types'; +import type { BedLayout, WardPatientGroupDetails, WardPatientWorkspaceProps } from '../../types'; import { assignPatientToBed, createEncounter } from '../../ward.resource'; import useEmrConfiguration from '../../hooks/useEmrConfiguration'; -import { showSnackbar, useSession } from '@openmrs/esm-framework'; +import { showSnackbar, useAppContext, useSession } from '@openmrs/esm-framework'; import useWardLocation from '../../hooks/useWardLocation'; import { useInpatientRequest } from '../../hooks/useInpatientRequest'; import { @@ -31,12 +30,12 @@ export default function PatientBedSwapForm({ const { patient } = wardPatient; const { t } = useTranslation(); const [showErrorNotifications, setShowErrorNotifications] = useState(false); - const { isLoading, admissionLocation } = useAdmissionLocation(); const { emrConfiguration, isLoadingEmrConfiguration, errorFetchingEmrConfiguration } = useEmrConfiguration(); const [isSubmitting, setIsSubmitting] = useState(false); const { currentProvider } = useSession(); const { location } = useWardLocation(); - const { mutate: mutateAdmissionLocation } = useAdmissionLocation(); + const wardGroupingDetails = useAppContext('ward-patients-group'); + const { isLoading, mutate: mutateAdmissionLocation } = wardGroupingDetails?.admissionLocationResponse ?? {}; const { mutate: mutateInpatientRequest } = useInpatientRequest(); const zodSchema = useMemo( @@ -72,7 +71,7 @@ export default function PatientBedSwapForm({ [t], ); - const beds = useMemo(() => (admissionLocation ? filterBeds(admissionLocation) : []), [admissionLocation]); + const beds = wardGroupingDetails?.bedLayouts ?? []; const bedDetails = useMemo( () => beds.map((bed) => { @@ -148,6 +147,7 @@ export default function PatientBedSwapForm({ setShowErrorNotifications(true); }, []); + if (!wardGroupingDetails) return <>; return ( emrConfiguration?.dispositions.filter(({ type }) => type === 'TRANSFER'), [emrConfiguration], ); - const { mutate: mutateAdmissionLocation } = useAdmissionLocation(); + const wardGroupingDetails = useAppContext('ward-patients-group'); + const { mutate: mutateAdmissionLocation } = wardGroupingDetails?.admissionLocationResponse ?? {}; const { mutate: mutateInpatientRequest } = useInpatientRequest(); const zodSchema = useMemo( @@ -154,6 +154,7 @@ export default function PatientTransferForm({ setShowErrorNotifications(true); }, []); + if (!wardGroupingDetails) return <>; return ( =0.6.2 <2.0.0, source-map-js@npm:^1.0.2": - version: 1.0.2 - resolution: "source-map-js@npm:1.0.2" - checksum: 10/38e2d2dd18d2e331522001fc51b54127ef4a5d473f53b1349c5cca2123562400e0986648b52e9407e348eaaed53bce49248b6e2641e6d793ca57cb2c360d6d51 - languageName: node - linkType: hard - -"source-map-js@npm:^1.2.0": +"source-map-js@npm:>=0.6.2 <2.0.0, source-map-js@npm:^1.2.0": version: 1.2.0 resolution: "source-map-js@npm:1.2.0" checksum: 10/74f331cfd2d121c50790c8dd6d3c9de6be21926de80583b23b37029b0f37aefc3e019fa91f9a10a5e120c08135297e1ecf312d561459c45908cb1e0e365f49e5 @@ -16399,17 +16306,7 @@ __metadata: languageName: node linkType: hard -"swc-loader@npm:^0.2.3": - version: 0.2.3 - resolution: "swc-loader@npm:0.2.3" - peerDependencies: - "@swc/core": ^1.2.147 - webpack: ">=2" - checksum: 10/010d84d399525c0185d36d62c86c55ae017e7a90046bc8a39be4b7e07526924037868049f6037bc966da98151cb2600934b96a66279b742d3c413a718b427251 - languageName: node - linkType: hard - -"swc-loader@npm:^0.2.6": +"swc-loader@npm:^0.2.3, swc-loader@npm:^0.2.6": version: 0.2.6 resolution: "swc-loader@npm:0.2.6" dependencies: @@ -16421,19 +16318,7 @@ __metadata: languageName: node linkType: hard -"swr@npm:^2.0.1": - version: 2.2.4 - resolution: "swr@npm:2.2.4" - dependencies: - client-only: "npm:^0.0.1" - use-sync-external-store: "npm:^1.2.0" - peerDependencies: - react: ^16.11.0 || ^17.0.0 || ^18.0.0 - checksum: 10/feb2fb5d3feb5accf93ce81108d659b941f9df6114d7744ccd148349255ed4d072e228e269d5aa3f81e4198cc120672929f5d1709cd52169d8e279314a5af4fd - languageName: node - linkType: hard - -"swr@npm:^2.2.5": +"swr@npm:^2.0.1, swr@npm:^2.2.5": version: 2.2.5 resolution: "swr@npm:2.2.5" dependencies: @@ -17133,20 +17018,6 @@ __metadata: languageName: node linkType: hard -"update-browserslist-db@npm:^1.0.9": - version: 1.0.10 - resolution: "update-browserslist-db@npm:1.0.10" - dependencies: - escalade: "npm:^3.1.1" - picocolors: "npm:^1.0.0" - peerDependencies: - browserslist: ">= 4.21.0" - bin: - browserslist-lint: cli.js - checksum: 10/2c88096ca99918efc77a514458c4241b3f2a8e7882aa91b97251231240c30c71e82cb2043aaf12e40eba6bebda3369010e180a58bc11bbd0bca29094945c31cb - languageName: node - linkType: hard - "update-browserslist-db@npm:^1.1.0": version: 1.1.0 resolution: "update-browserslist-db@npm:1.1.0" @@ -17524,21 +17395,6 @@ __metadata: languageName: node linkType: hard -"webpack-dev-middleware@npm:^5.3.1": - version: 5.3.3 - resolution: "webpack-dev-middleware@npm:5.3.3" - dependencies: - colorette: "npm:^2.0.10" - memfs: "npm:^3.4.3" - mime-types: "npm:^2.1.31" - range-parser: "npm:^1.2.1" - schema-utils: "npm:^4.0.0" - peerDependencies: - webpack: ^4.0.0 || ^5.0.0 - checksum: 10/31a2f7a11e58a76bdcde1eb8da310b6643844d9b442f9916f48be5b46c103f23490c393c32a9af501ce68226fbb018b811f5a956635ed60a03f9481a4bcd6c76 - languageName: node - linkType: hard - "webpack-dev-middleware@npm:^5.3.4": version: 5.3.4 resolution: "webpack-dev-middleware@npm:5.3.4" @@ -17554,54 +17410,7 @@ __metadata: languageName: node linkType: hard -"webpack-dev-server@npm:^4.15.1": - version: 4.15.1 - resolution: "webpack-dev-server@npm:4.15.1" - dependencies: - "@types/bonjour": "npm:^3.5.9" - "@types/connect-history-api-fallback": "npm:^1.3.5" - "@types/express": "npm:^4.17.13" - "@types/serve-index": "npm:^1.9.1" - "@types/serve-static": "npm:^1.13.10" - "@types/sockjs": "npm:^0.3.33" - "@types/ws": "npm:^8.5.5" - ansi-html-community: "npm:^0.0.8" - bonjour-service: "npm:^1.0.11" - chokidar: "npm:^3.5.3" - colorette: "npm:^2.0.10" - compression: "npm:^1.7.4" - connect-history-api-fallback: "npm:^2.0.0" - default-gateway: "npm:^6.0.3" - express: "npm:^4.17.3" - graceful-fs: "npm:^4.2.6" - html-entities: "npm:^2.3.2" - http-proxy-middleware: "npm:^2.0.3" - ipaddr.js: "npm:^2.0.1" - launch-editor: "npm:^2.6.0" - open: "npm:^8.0.9" - p-retry: "npm:^4.5.0" - rimraf: "npm:^3.0.2" - schema-utils: "npm:^4.0.0" - selfsigned: "npm:^2.1.1" - serve-index: "npm:^1.9.1" - sockjs: "npm:^0.3.24" - spdy: "npm:^4.0.2" - webpack-dev-middleware: "npm:^5.3.1" - ws: "npm:^8.13.0" - peerDependencies: - webpack: ^4.37.0 || ^5.0.0 - peerDependenciesMeta: - webpack: - optional: true - webpack-cli: - optional: true - bin: - webpack-dev-server: bin/webpack-dev-server.js - checksum: 10/fd6dfb6c71eb94696b21930ea4c2f25e95ba85fac1bbc15aa5d03af0a90712eba057901fa9131ed3e901665c95b2379208279aca61e9c48e7cda276c3caa95dd - languageName: node - linkType: hard - -"webpack-dev-server@npm:^4.15.2": +"webpack-dev-server@npm:^4.15.1, webpack-dev-server@npm:^4.15.2": version: 4.15.2 resolution: "webpack-dev-server@npm:4.15.2" dependencies: