diff --git a/CHANGELOG.md b/CHANGELOG.md index 578b6b0f562..d922c96fbe4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ - Record audit action buttons are moved into action menu [#7390](https://github.com/opencrvs/opencrvs-core/issues/7390) - Reoder the sytem user add/edit field for surname to be first, also change labels from `Last name` to `User's surname` and lastly remove the NID question from the form [#6830](https://github.com/opencrvs/opencrvs-core/issues/6830) - Auth now allows exchanging user's token for a new record-specific token [#7728](https://github.com/opencrvs/opencrvs-core/issues/7728) +- Allow countries to customise the format of the full name in the sytem for `sytem users` and `citizens` e.g `{LastName} {MiddleName} {Firstname}`, in any case where one of the name is not provided e.g no `MiddleName`, we'll simply render e.g `{LastName} {FirstName}` without any extra spaces if that's the order set in `country-config`. [#6830](https://github.com/opencrvs/opencrvs-core/issues/6830) ## Bug fixes diff --git a/packages/client/src/i18n/messages/constants.ts b/packages/client/src/i18n/messages/constants.ts index 58101a09a2c..38957df0d13 100644 --- a/packages/client/src/i18n/messages/constants.ts +++ b/packages/client/src/i18n/messages/constants.ts @@ -163,6 +163,7 @@ interface IConstantsMessages refresh: MessageDescriptor duplicateOf: MessageDescriptor matchedTo: MessageDescriptor + humanName: MessageDescriptor } const messagesToDefine: IConstantsMessages = { action: { @@ -978,6 +979,11 @@ const messagesToDefine: IConstantsMessages = { defaultMessage: `{registrationTargetDays} days - 1 year`, description: `Label for registrations within {registrationTargetDays} days to 1 year`, id: 'constants.withinTargetDaysTo1Year' + }, + humanName: { + defaultMessage: `{lastName} {middleName} {firstName}`, + description: 'A localized order of the full name', + id: 'constants.humanName' } } export const constantsMessages: Record< diff --git a/packages/client/src/utils/data-formatting.ts b/packages/client/src/utils/data-formatting.ts index 001daa8bec9..4de65ff8694 100644 --- a/packages/client/src/utils/data-formatting.ts +++ b/packages/client/src/utils/data-formatting.ts @@ -11,6 +11,8 @@ import { getDefaultLanguage } from '@client/i18n/utils' import type { GQLComment } from '@client/utils/gateway-deprecated-do-not-use' import { HumanName } from './gateway' +import { constantsMessages } from '@client/i18n/messages' +import { IntlShape } from 'react-intl' interface INamesMap { [key: string]: string @@ -81,3 +83,17 @@ export const mergeArraysRemovingEmptyStrings = ( export function getPercentage(total: number, current: number) { return current === 0 || total === 0 ? 0 : (current / total) * 100 } + +export function getLocalisedName( + intl: IntlShape, + nameObject: HumanName +): string { + return intl + .formatMessage(constantsMessages.humanName, { + firstName: nameObject.firstNames, + middleName: nameObject.middleName, + lastName: nameObject.familyName + }) + .replace(/\s+/g, ' ') // Remove extra spaces + .trim() +} diff --git a/packages/client/src/utils/draftUtils.test.ts b/packages/client/src/utils/draftUtils.test.ts index b7be4734d51..0f3012f4201 100644 --- a/packages/client/src/utils/draftUtils.test.ts +++ b/packages/client/src/utils/draftUtils.test.ts @@ -17,29 +17,49 @@ import type { GQLBirthEventSearchSet, GQLDeathEventSearchSet } from '@client/utils/gateway-deprecated-do-not-use' +import { createIntl, createIntlCache } from 'react-intl' + +const cache = createIntlCache() +const intlEngish = createIntl( + { + locale: 'en', + messages: {} + }, + cache +) +const intlBangla = createIntl( + { + locale: 'en', + messages: {} + }, + cache +) describe('draftUtils tests', () => { describe('getDraftInformantFullName()', () => { describe('Birth event', () => { it('Returns child english name properly', () => { expect( - getDeclarationFullName({ - id: '7b57d8f9-4d2d-4f12-8d0a-b042fe14f3d4', - data: { - child: { - firstNames: 'মুশ্রাফুল', - familyName: 'হক', - firstNamesEng: 'Mushraful', - familyNameEng: 'Hoque' - } + getDeclarationFullName( + { + id: '7b57d8f9-4d2d-4f12-8d0a-b042fe14f3d4', + data: { + child: { + firstNames: 'মুশ্রাফুল', + familyName: 'হক', + firstNamesEng: 'Mushraful', + familyNameEng: 'Hoque' + } + }, + event: Event.Birth, + savedOn: 1558037863335, + modifiedOn: 1558037867987 }, - event: Event.Birth, - savedOn: 1558037863335, - modifiedOn: 1558037867987 - }) - ).toBe('Mushraful Hoque') + intlEngish + ) + ).toBe('Hoque Mushraful') }) - it('Returns child bangla name properly', () => { + it('Returns child English name properly even though localed is Bangla', () => { expect( getDeclarationFullName( { @@ -55,30 +75,33 @@ describe('draftUtils tests', () => { savedOn: 1558037863335, modifiedOn: 1558037867987 }, - 'bn' + intlBangla ) - ).toBe('হক') + ).toBe('Hoque Mushraful') }) }) describe('Death event', () => { it('Returns deceased english name properly', () => { expect( - getDeclarationFullName({ - id: '7b57d8f9-4d2d-4f12-8d0a-b042fe14f3d4', - data: { - deceased: { - firstNames: 'মুশ্রাফুল', - familyName: 'হক', - familyNameEng: 'Hoque' - } + getDeclarationFullName( + { + id: '7b57d8f9-4d2d-4f12-8d0a-b042fe14f3d4', + data: { + deceased: { + firstNames: 'মুশ্রাফুল', + familyName: 'হক', + familyNameEng: 'Hoque' + } + }, + event: Event.Death, + savedOn: 1558037863335, + modifiedOn: 1558037867987 }, - event: Event.Death, - savedOn: 1558037863335, - modifiedOn: 1558037867987 - }) + intlBangla + ) ).toBe('Hoque') }) - it('Returns child bangla name properly', () => { + it('Returns child English name properly even when the current locale is Bangla', () => { expect( getDeclarationFullName( { @@ -95,9 +118,9 @@ describe('draftUtils tests', () => { savedOn: 1558037863335, modifiedOn: 1558037867987 }, - 'bn' + intlEngish ) - ).toBe('মুশ্রাফুল হক') + ).toBe('Hoque Mushraful') }) }) }) diff --git a/packages/client/src/utils/draftUtils.ts b/packages/client/src/utils/draftUtils.ts index 47dfe4dbe57..10058046659 100644 --- a/packages/client/src/utils/draftUtils.ts +++ b/packages/client/src/utils/draftUtils.ts @@ -13,7 +13,6 @@ import { SUBMISSION_STATUS, IPrintableDeclaration } from '@client/declarations' -import { IFormSectionData } from '@client/forms' import { Event, History, RegStatus } from '@client/utils/gateway' import type { GQLBirthEventSearchSet, @@ -24,67 +23,48 @@ import type { import { getEvent } from '@client/views/PrintCertificate/utils' import { includes } from 'lodash' import { EMPTY_STRING } from '@client/utils/constants' - -const getEngName = ( - sectionData: IFormSectionData, - lastNameFirst: boolean -): string => { - if (lastNameFirst) { - return `${sectionData.familyNameEng ?? ''} ${ - sectionData.firstNamesEng ?? '' - }` - } - return [ - sectionData.firstNamesEng, - sectionData.middleNameEng, - sectionData.familyNameEng - ] - .filter(Boolean) - .join(' ') - .trim() -} - -const getOtherName = (sectionData: IFormSectionData): string => { - return [ - sectionData.firstNames, - sectionData.middleName, - sectionData.familyName - ] - .filter(Boolean) - .join(' ') - .trim() -} - -const getFullName = ( - sectionData: IFormSectionData, - language = 'en', - lastNameFirst = false -): string => { - if (!sectionData) { - return EMPTY_STRING - } - if (language === 'en') { - return getEngName(sectionData, lastNameFirst) - } - return getOtherName(sectionData) || getEngName(sectionData, lastNameFirst) -} +import { getLocalisedName } from './data-formatting' +import { IntlShape } from 'react-intl' /* * lastNameFirst needs to be removed in #4464 */ export const getDeclarationFullName = ( draft: IDeclaration, - language?: string, - lastNameFirst?: boolean + intl: IntlShape ) => { switch (draft.event) { case Event.Birth: - return getFullName(draft.data.child, language, lastNameFirst) + return draft.data.child + ? getLocalisedName(intl, { + firstNames: draft.data.child.firstNamesEng as string, + middleName: draft.data.child.middleNameEng as string, + familyName: draft.data.child.familyNameEng as string + }) + : EMPTY_STRING case Event.Death: - return getFullName(draft.data.deceased, language, lastNameFirst) + return draft.data.deceased + ? getLocalisedName(intl, { + firstNames: draft.data.deceased.firstNamesEng as string, + middleName: draft.data.deceased.middleNameEng as string, + familyName: draft.data.deceased.familyNameEng as string + }) + : EMPTY_STRING case Event.Marriage: - const brideName = getFullName(draft.data.bride, language, lastNameFirst) - const groomName = getFullName(draft.data.groom, language, lastNameFirst) + const brideName = draft.data.bride + ? getLocalisedName(intl, { + firstNames: draft.data.bride.firstNamesEng as string, + middleName: draft.data.bride.middleNameEng as string, + familyName: draft.data.bride.familyNameEng as string + }) + : EMPTY_STRING + const groomName = draft.data.groom + ? getLocalisedName(intl, { + firstNames: draft.data.groom.firstNamesEng as string, + middleName: draft.data.groom.middleNameEng as string, + familyName: draft.data.groom.familyNameEng as string + }) + : EMPTY_STRING if (brideName && groomName) { return `${groomName} & ${brideName}` } else { diff --git a/packages/client/src/views/OfficeHome/inProgress/InProgress.tsx b/packages/client/src/views/OfficeHome/inProgress/InProgress.tsx index bccebf39128..cd4d32b7e1d 100644 --- a/packages/client/src/views/OfficeHome/inProgress/InProgress.tsx +++ b/packages/client/src/views/OfficeHome/inProgress/InProgress.tsx @@ -344,7 +344,6 @@ class InProgressComponent extends React.Component< transformDraftContent = () => { const { intl } = this.props - const { locale } = intl if (!this.props.drafts || this.props.drafts.length <= 0) { return [] } @@ -361,7 +360,7 @@ class InProgressComponent extends React.Component< } else if (draft.event && draft.event.toString() === 'marriage') { pageRoute = DRAFT_MARRIAGE_FORM_PAGE } - const name = getDeclarationFullName(draft, locale) + const name = getDeclarationFullName(draft, intl) const lastModificationDate = draft.modifiedOn || draft.savedOn const actions: IAction[] = [] diff --git a/packages/client/src/views/OfficeHome/inProgress/inProgress.test.tsx b/packages/client/src/views/OfficeHome/inProgress/inProgress.test.tsx index 33b8b91e638..1032651d0a4 100644 --- a/packages/client/src/views/OfficeHome/inProgress/inProgress.test.tsx +++ b/packages/client/src/views/OfficeHome/inProgress/inProgress.test.tsx @@ -297,7 +297,7 @@ describe('In Progress tab', () => { const EXPECTED_DATE_OF_REJECTION = formattedDuration(TIME_STAMP) expect(data[0].id).toBe('e302f7c5-ad87-4117-91c1-35eaf2ea7be8') - expect(data[0].name).toBe('anik hoque') + expect(data[0].name).toBe('hoque anik') expect(data[0].lastUpdated).toBe(EXPECTED_DATE_OF_REJECTION) expect(data[0].event).toBe('Birth') expect(data[0].actions).toBeDefined() diff --git a/packages/client/src/views/OfficeHome/outbox/Outbox.tsx b/packages/client/src/views/OfficeHome/outbox/Outbox.tsx index c5b909a69b7..866a5b9cf85 100644 --- a/packages/client/src/views/OfficeHome/outbox/Outbox.tsx +++ b/packages/client/src/views/OfficeHome/outbox/Outbox.tsx @@ -168,7 +168,7 @@ export function Outbox() { function transformDeclarationsReadyToSend() { const items = declarations.map((declaration, index) => { - const name = getDeclarationFullName(declaration) + const name = getDeclarationFullName(declaration, intl) let dateOfEvent if (declaration.event && declaration.event.toString() === 'birth') { dateOfEvent = declaration.data?.child?.childBirthDate as string diff --git a/packages/client/src/views/RecordAudit/RecordAudit.test.tsx b/packages/client/src/views/RecordAudit/RecordAudit.test.tsx index 78eff975f4f..c3c88638a07 100644 --- a/packages/client/src/views/RecordAudit/RecordAudit.test.tsx +++ b/packages/client/src/views/RecordAudit/RecordAudit.test.tsx @@ -292,7 +292,7 @@ describe('Record audit summary for WorkQueue declarations', () => { component.find({ 'data-testid': 'type-value' }).hostNodes().text() ).toBe('Birth') expect(component.find('#content-name').hostNodes().text()).toBe( - 'Shakib Al Hasan' + 'Al Hasan Shakib' ) expect( component diff --git a/packages/client/src/views/RecordAudit/RecordAudit.tsx b/packages/client/src/views/RecordAudit/RecordAudit.tsx index 3b19ab2d0ae..560b773455e 100644 --- a/packages/client/src/views/RecordAudit/RecordAudit.tsx +++ b/packages/client/src/views/RecordAudit/RecordAudit.tsx @@ -555,7 +555,7 @@ const BodyContent = ({ assignment: data.fetchRegistration?.registration?.assignment } } else { - declaration = getGQLDeclaration(data.fetchRegistration, language) + declaration = getGQLDeclaration(data.fetchRegistration, intl) } return ( @@ -592,7 +592,7 @@ const BodyContent = ({ } : getWQDeclarationData( workqueueDeclaration as NonNullable, - language, + intl, trackingId ) const wqStatus = workqueueDeclaration?.registration diff --git a/packages/client/src/views/RecordAudit/utils.ts b/packages/client/src/views/RecordAudit/utils.ts index 5abb838320e..c25d31baaff 100644 --- a/packages/client/src/views/RecordAudit/utils.ts +++ b/packages/client/src/views/RecordAudit/utils.ts @@ -25,7 +25,7 @@ import type { GQLAssignmentData, GQLMarriageEventSearchSet } from '@client/utils/gateway-deprecated-do-not-use' -import { createNamesMap } from '@client/utils/data-formatting' +import { createNamesMap, getLocalisedName } from '@client/utils/data-formatting' import { IDynamicValues } from '@client/navigation' import { countryMessages } from '@client/i18n/messages/constants' import { @@ -337,8 +337,8 @@ export const getDraftDeclarationData = ( ): IDeclarationData => { return { id: declaration.id, - name: getDeclarationFullName(declaration), - type: declaration.event, + name: getDeclarationFullName(declaration, intl), + type: declaration.event || EMPTY_STRING, registrationNo: declaration.data?.registration?.registrationNumber?.toString() || EMPTY_STRING, @@ -365,7 +365,7 @@ export const getDraftDeclarationData = ( export const getWQDeclarationData = ( workqueueDeclaration: GQLEventSearchSet, - language: string, + intl: IntlShape, trackingId: string ) => { let name = EMPTY_STRING @@ -373,19 +373,35 @@ export const getWQDeclarationData = ( isBirthDeclaration(workqueueDeclaration) && workqueueDeclaration.childName ) { - name = getName(workqueueDeclaration.childName, language) + name = getLocalisedName(intl, { + firstNames: workqueueDeclaration.childName[0]?.firstNames, + middleName: workqueueDeclaration.childName[0]?.middleName, + familyName: workqueueDeclaration.childName[0]?.familyName + }) } else if ( isDeathDeclaration(workqueueDeclaration) && workqueueDeclaration.deceasedName ) { - name = getName(workqueueDeclaration.deceasedName, language) + name = getLocalisedName(intl, { + firstNames: workqueueDeclaration.deceasedName[0]?.firstNames, + middleName: workqueueDeclaration.deceasedName[0]?.middleName, + familyName: workqueueDeclaration.deceasedName[0]?.familyName + }) } else if ( isMarriageDeclaration(workqueueDeclaration) && workqueueDeclaration.brideName && workqueueDeclaration.groomName ) { - const groomName = getName(workqueueDeclaration.groomName, language) - const brideName = getName(workqueueDeclaration.brideName, language) + const groomName = getLocalisedName(intl, { + firstNames: workqueueDeclaration.groomName[0]?.firstNames, + middleName: workqueueDeclaration.groomName[0]?.middleName, + familyName: workqueueDeclaration.groomName[0]?.familyName + }) + const brideName = getLocalisedName(intl, { + firstNames: workqueueDeclaration.brideName[0]?.firstNames, + middleName: workqueueDeclaration.brideName[0]?.middleName, + familyName: workqueueDeclaration.brideName[0]?.familyName + }) name = brideName && groomName @@ -408,25 +424,48 @@ export const getWQDeclarationData = ( export const getGQLDeclaration = ( data: IGQLDeclaration, - language: string + intl: IntlShape ): IDeclarationData => { let name = EMPTY_STRING if (data.child) { - name = data.child.name ? getName(data.child.name, language) : EMPTY_STRING + name = data.child.name + ? getLocalisedName(intl, { + firstNames: data.child.name[0]?.firstNames, + middleName: data.child.name[0]?.middleName, + familyName: data.child.name[0]?.familyName + }) + : EMPTY_STRING } else if (data.deceased) { name = data.deceased.name - ? getName(data.deceased.name, language) + ? getLocalisedName(intl, { + firstNames: data.deceased.name[0]?.firstNames, + middleName: data.deceased.name[0]?.middleName, + familyName: data.deceased.name[0]?.familyName + }) : EMPTY_STRING } else if (data.groom || data.bride) { if (data.groom?.name && data.bride?.name) { - name = `${getName(data.groom.name, language)} & ${getName( - data.bride.name, - language - )}` + name = `${getLocalisedName(intl, { + firstNames: data.groom.name[0]?.firstNames, + middleName: data.groom.name[0]?.middleName, + familyName: data.groom.name[0]?.familyName + })} & ${getLocalisedName(intl, { + firstNames: data.bride.name[0]?.firstNames, + middleName: data.bride.name[0]?.middleName, + familyName: data.bride.name[0]?.firstNames + })}` } else if (data.groom?.name) { - name = getName(data.groom.name, language) + name = getLocalisedName(intl, { + firstNames: data.groom.name[0]?.firstNames, + middleName: data.groom.name[0]?.middleName, + familyName: data.groom.name[0]?.familyName + }) } else if (data.bride?.name) { - name = getName(data.bride.name, language) + name = getLocalisedName(intl, { + firstNames: data.bride.name[0]?.firstNames, + middleName: data.bride.name[0]?.middleName, + familyName: data.bride.name[0]?.familyName + }) } else { name = EMPTY_STRING } diff --git a/packages/client/src/views/RegisterForm/review/ReviewSection.test.tsx b/packages/client/src/views/RegisterForm/review/ReviewSection.test.tsx index 08ccebc578e..902f30d036b 100644 --- a/packages/client/src/views/RegisterForm/review/ReviewSection.test.tsx +++ b/packages/client/src/views/RegisterForm/review/ReviewSection.test.tsx @@ -178,7 +178,7 @@ describe('when in device of large viewport', () => { ).toBe('Government of the peoples republic of Bangladesh') expect( reviewSectionComponent.find('#review_header_subject').hostNodes().text() - ).toBe('Birth Declaration for John Doe') + ).toBe('Birth Declaration for Doe John') }) it('typing additional comments input triggers onchange review form', async () => { diff --git a/packages/client/src/views/RegisterForm/review/ReviewSection.tsx b/packages/client/src/views/RegisterForm/review/ReviewSection.tsx index c2406b6b80d..467a90dff6c 100644 --- a/packages/client/src/views/RegisterForm/review/ReviewSection.tsx +++ b/packages/client/src/views/RegisterForm/review/ReviewSection.tsx @@ -1699,11 +1699,7 @@ class ReviewSectionComp extends React.Component { '') as string } - const informantName = getDeclarationFullName( - declaration, - intl.locale, - this.isLastNameFirst() - ) + const informantName = getDeclarationFullName(declaration, intl) const draft = this.isDraft() const transformedSectionData = this.transformSectionData( formSections.filter( diff --git a/packages/client/src/views/SysAdmin/Team/user/UserList.tsx b/packages/client/src/views/SysAdmin/Team/user/UserList.tsx index 34a7659c9ab..c61076155aa 100644 --- a/packages/client/src/views/SysAdmin/Team/user/UserList.tsx +++ b/packages/client/src/views/SysAdmin/Team/user/UserList.tsx @@ -34,7 +34,7 @@ import { NATL_ADMIN_ROLES, SYS_ADMIN_ROLES } from '@client/utils/constants' -import { createNamesMap } from '@client/utils/data-formatting' +import { createNamesMap, getLocalisedName } from '@client/utils/data-formatting' import { SysAdminContentWrapper } from '@client/views/SysAdmin/SysAdminContentWrapper' import { getAddressName, @@ -552,12 +552,7 @@ function UserListComponent(props: IProps) { return data.searchUsers.results.map( (user: User | null, index: number) => { if (user !== null) { - const name = - (user && - user.name && - ((createNamesMap(user.name)[intl.locale] as string) || - (createNamesMap(user.name)[LANG_EN] as string))) || - '' + const name = getLocalisedName(intl, user.name[0]) const role = intl.formatMessage({ id: getUserRoleIntlKey(user.role._id) }) diff --git a/packages/client/src/views/UserAudit/UserAudit.tsx b/packages/client/src/views/UserAudit/UserAudit.tsx index 680ed4cec5d..d4c0b29b2e9 100644 --- a/packages/client/src/views/UserAudit/UserAudit.tsx +++ b/packages/client/src/views/UserAudit/UserAudit.tsx @@ -18,7 +18,7 @@ import { Frame } from '@opencrvs/components/lib/Frame' import { IntlShape, useIntl } from 'react-intl' import { useParams } from 'react-router' import { GET_USER } from '@client/user/queries' -import { createNamesMap } from '@client/utils/data-formatting' +import { getLocalisedName } from '@client/utils/data-formatting' import { AvatarSmall } from '@client/components/Avatar' import styled from 'styled-components' import { ToggleMenu } from '@opencrvs/components/lib/ToggleMenu' @@ -42,7 +42,6 @@ import { UserAuditActionModal } from '@client/views/SysAdmin/Team/user/UserAudit import { GetUserQuery, GetUserQueryVariables, - HumanName, User, SystemRoleType } from '@client/utils/gateway' @@ -83,9 +82,7 @@ const transformUserQueryResult = ( '')) || '' }, - name: - createNamesMap(userData.name as HumanName[])[locale] || - createNamesMap(userData.name as HumanName[])[LANG_EN], + name: getLocalisedName(intl, userData.name[0]), systemRole: userData.systemRole, role: userData.role, number: userData.mobile,