Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

(feat) O3-3840 : Improvements to the registration form Death info section #1290

Merged
merged 1 commit into from
Aug 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,5 +84,8 @@
"*.{ts,tsx}": "eslint --cache --fix --max-warnings 0",
"*.{css,scss,ts,tsx}": "prettier --cache --write --list-different"
},
"resolutions": {
"zustand": "4.3.6"
},
"packageManager": "[email protected]"
}
2 changes: 1 addition & 1 deletion packages/esm-patient-registration-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
30 changes: 28 additions & 2 deletions packages/esm-patient-registration-app/src/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export interface RegistrationConfig {
sectionDefinitions: Array<SectionDefinition>;
fieldDefinitions: Array<FieldDefinition>;
fieldConfigurations: {
causeOfDeath: {
conceptUuid: string;
required?: boolean;
};
name: {
displayMiddleName: boolean;
allowUnidentifiedPatients: boolean;
Expand Down Expand Up @@ -78,6 +82,7 @@ export interface RegistrationConfig {
encounterProviderRoleUuid: string;
registrationFormUuid: string | null;
};
freeTextFieldConceptUuid: string;
}

export const builtInSections: Array<SectionDefinition> = [
Expand All @@ -87,12 +92,21 @@ export const builtInSections: Array<SectionDefinition> = [
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: {
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<RegistrationConfig>();
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 (
<div className={classNames(styles.customField, styles.halfWidthInDesktopView)}>
<h4 className={styles.productiveHeading02Light}>{t('causeOfDeathInputLabel', 'Cause of death')}</h4>
<SelectSkeleton />
</div>
);
}

return (
<div className={classNames(styles.customField, styles.halfWidthInDesktopView)}>
<h4 className={styles.productiveHeading02Light}>{t('causeOfDeathInputLabel', 'Cause of death')}</h4>
{errorLoadingConceptAnswers || !conceptUuid ? (
<InlineNotification
hideCloseButton
kind="error"
title={t('errorFetchingCodedCausesOfDeath', 'Error fetching coded causes of death')}
subtitle={t('refreshOrContactAdmin', 'Try refreshing the page or contact your system administrator')}
/>
) : (
<>
<Field name="deathCause">
{({ field, form: { touched, errors }, meta }) => {
return (
<Layer>
<Select
{...field}
id="deathCause"
invalid={errors.deathCause && touched.deathCause}
invalidText={errors.deathCause?.message}
labelText={t('causeOfDeathInputLabel', 'Cause of Death')}
name="deathCause"
required={required}>
<SelectItem id="empty-default-option" value={null} text={t('selectAnOption', 'Select an option')} />
{answers.map((answer) => (
<SelectItem id={answer.uuid} key={answer.uuid} text={answer.label} value={answer.uuid} />
))}
</Select>
</Layer>
);
}}
</Field>
{deathCause.value === freeTextFieldConceptUuid && (
<div className={styles.nonCodedCauseOfDeath}>
<Field name="nonCodedCauseOfDeath">
{({ field, form: { touched, errors }, meta }) => {
return (
<Layer>
<TextInput
{...field}
id="nonCodedCauseOfDeath"
invalid={errors?.nonCodedCauseOfDeath && touched.nonCodedCauseOfDeath}
invalidText={errors?.nonCodedCauseOfDeath?.message}
labelText={t('nonCodedCauseOfDeath', 'Non-coded cause of death')}
placeholder={t('enterNonCodedCauseOfDeath', 'Enter non-coded cause of death')}
/>
</Layer>
);
}}
</Field>
</div>
)}
</>
)}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -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 (
<div className={classNames(styles.dodField, styles.halfWidthInDesktopView)}>
<h4 className={styles.productiveHeading02Light}>{t('deathDateInputLabel', 'Date of Death')}</h4>
<span>
<DeathDateField />
<DeathTimeField />
</span>
</div>
);
};

function DeathDateField() {
const { values, setFieldValue } = useContext(PatientRegistrationContext);
const [deathDate, deathDateMeta] = useField<keyof FormValues>('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 (
<Layer>
<OpenmrsDatePicker
{...deathDate}
id="deathDate"
invalidText={t(deathDateMeta.error)}
invalid={!!(deathDateMeta.touched && deathDateMeta.error)}
isRequired={values.isDead}
labelText={t('deathDateInputLabel', 'Date of death')}
maxDate={today}
onChange={onDateChange}
/>
</Layer>
);
}

function DeathTimeField() {
const { t } = useTranslation();
const [deathTimeField, deathTimeMeta] = useField<keyof FormValues>('deathTime');
const [deathTimeFormatField, deathTimeFormatMeta] = useField<keyof FormValues>('deathTimeFormat');

return (
<Layer>
<TimePicker
{...deathTimeField}
id="time-picker"
labelText={t('timeOfDeathInputLabel', 'Time of death (hh:mm)')}
className={styles.timeOfDeathField}
pattern="^(1[0-2]|0?[1-9]):([0-5]?[0-9])$"
invalid={!!(deathTimeMeta.touched && deathTimeMeta.error)}
invalidText={t(deathTimeMeta.error)}>
<TimePickerSelect
{...deathTimeFormatField}
id="time-format-picker"
aria-label={t('timeFormat', 'Time Format')}
invalid={!!deathTimeFormatMeta.touched && deathTimeFormatMeta.error}
invalidText={t(deathTimeFormatMeta.error)}>
<SelectItem value="AM" text="AM" />
<SelectItem value="PM" text="PM" />
</TimePickerSelect>
</TimePicker>
</Layer>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export const DobField: React.FC = () => {
{(allowEstimatedBirthDate || dobUnknown) && (
<div className={styles.dobField}>
<div className={styles.dobContentSwitcherLabel}>
<span className={styles.label01}>{t('dobToggleLabelText', 'Date of Birth Known?')}</span>
<span className={styles.label01}>{t('dobToggleLabelText', 'Date of birth known?')}</span>
</div>
<ContentSwitcher onChange={onToggle} selectedIndex={dobUnknown ? 1 : 0}>
<Switch name="known" text={t('yes', 'Yes')} />
Expand All @@ -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}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -35,6 +37,10 @@ export function Field({ name }: FieldProps) {
return <GenderField />;
case 'dob':
return <DobField />;
case 'dateAndTimeOfDeath':
return <DateAndTimeOfDeathField />;
case 'causeOfDeath':
return <CauseOfDeathField />;
case 'address':
return <AddressComponent />;
case 'id':
Expand Down
Original file line number Diff line number Diff line change
@@ -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 !== '';
Expand All @@ -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<ConceptAnswers>; isLoading: boolean } {
export function useConceptAnswers(conceptUuid: string): {
data: Array<ConceptAnswers>;
isLoading: boolean;
error: Error;
} {
const shouldFetch = typeof conceptUuid === 'string' && conceptUuid !== '';
const { data, error, isLoading } = useSWRImmutable<FetchResponse<ConceptResponse>, Error>(
shouldFetch ? `${restBaseUrl}/concept/${conceptUuid}` : null,
Expand All @@ -31,5 +37,6 @@ export function useConceptAnswers(conceptUuid: string): { data: Array<ConceptAns
kind: 'error',
});
}
return { data: data?.data?.answers, isLoading };
const results = useMemo(() => ({ data: data?.data?.answers, isLoading, error }), [isLoading, error, data]);
return results;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ const formValues: FormValues = {
telephoneNumber: '',
isDead: false,
deathDate: 'string',
deathTime: '',
deathTimeFormat: 'AM',
deathCause: 'string',
nonCodedCauseOfDeath: '',
relationships: [],
address: {
address1: '',
Expand Down
Loading
Loading