From 136430defd9a555426256a2b715794ce797cdadd Mon Sep 17 00:00:00 2001 From: alaa-yahia Date: Thu, 5 Dec 2024 00:14:05 +0200 Subject: [PATCH 01/42] feat: enable non gregorian calendars in views and lists --- package.json | 3 +- src/components/AppLoader/init.js | 4 +-- .../configs/dateField/getDateFieldConfig.js | 3 +- .../getDateFieldConfigForCustomForm.js | 3 +- .../EnrollmentDataEntry.component.js | 7 +++-- .../DateAndTime/D2Date/D2Date.component.js | 4 +-- .../capture-core/converters/clientToForm.js | 11 ++++---- .../capture-core/converters/clientToList.js | 10 ++++--- .../capture-core/converters/clientToView.js | 12 +++++--- .../capture-core/converters/formToClient.js | 19 +++++++++---- .../metaData/SystemSettings/SystemSettings.js | 1 + .../systemSettings/cacheSystemSetttings.js | 6 +++- .../date/convertIsoToLocalCalendar.js | 28 +++++++++++++++++++ .../date/convertLocalToIsoCalendar.js | 26 +++++++++++++++++ .../date/dateObjectToDateFormatString.js | 10 +++---- .../utils/converters/date/index.js | 3 ++ .../utils/converters/date/padWithZeros.js | 12 ++++++++ .../DateField/Date.component.js | 9 ++---- yarn.lock | 2 +- 19 files changed, 129 insertions(+), 44 deletions(-) create mode 100644 src/core_modules/capture-core/utils/converters/date/convertIsoToLocalCalendar.js create mode 100644 src/core_modules/capture-core/utils/converters/date/convertLocalToIsoCalendar.js create mode 100644 src/core_modules/capture-core/utils/converters/date/padWithZeros.js diff --git a/package.json b/package.json index e4caffa666..1f22447823 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "packages/rules-engine" ], "dependencies": { - "@dhis2/rules-engine-javascript": "101.20.3", + "@dhis2-ui/calendar": "^10.0.3", "@dhis2/app-runtime": "^3.9.3", "@dhis2/d2-i18n": "^1.1.0", "@dhis2/d2-icons": "^1.0.1", @@ -19,7 +19,6 @@ "@dhis2/d2-ui-rich-text": "^7.4.0", "@dhis2/d2-ui-sharing-dialog": "^7.3.3", "@dhis2/ui": "^9.10.1", - "@dhis2-ui/calendar": "^10.0.3", "@joakim_sm/react-infinite-calendar": "^2.4.2", "@material-ui/core": "3.9.4", "@material-ui/icons": "3", diff --git a/src/components/AppLoader/init.js b/src/components/AppLoader/init.js index f7106fabc4..de3a3153fc 100644 --- a/src/components/AppLoader/init.js +++ b/src/components/AppLoader/init.js @@ -131,7 +131,7 @@ async function initializeMetaDataAsync(dbLocale: string, onQueryApi: Function, m async function initializeSystemSettingsAsync( uiLocale: string, - systemSettings: { dateFormat: string, serverTimeZoneId: string }, + systemSettings: { dateFormat: string, serverTimeZoneId: string, calendar: string, }, ) { const systemSettingsCacheData = await cacheSystemSettings(uiLocale, systemSettings); await buildSystemSettingsAsync(systemSettingsCacheData); @@ -158,7 +158,7 @@ export async function initializeAsync( const systemSettings = await onQueryApi({ resource: 'system/info', params: { - fields: 'dateFormat,serverTimeZoneId', + fields: 'dateFormat,serverTimeZoneId,calendar', }, }); diff --git a/src/core_modules/capture-core/components/D2Form/field/configs/dateField/getDateFieldConfig.js b/src/core_modules/capture-core/components/D2Form/field/configs/dateField/getDateFieldConfig.js index 7c6f480ae7..7415bd7d5f 100644 --- a/src/core_modules/capture-core/components/D2Form/field/configs/dateField/getDateFieldConfig.js +++ b/src/core_modules/capture-core/components/D2Form/field/configs/dateField/getDateFieldConfig.js @@ -2,6 +2,7 @@ import moment from 'moment'; import { createFieldConfig, createProps } from '../base/configBaseDefaultForm'; import { DateFieldForForm } from '../../Components'; +import { convertDateObjectToDateFormatString } from '../../../../../../capture-core/utils/converters/date'; import type { DateDataElement } from '../../../../../metaData'; import type { QuerySingleResource } from '../../../../../utils/api/api.types'; @@ -15,7 +16,7 @@ export const getDateFieldConfig = (metaData: DateDataElement, options: Object, q maxWidth: options.formHorizontal ? 150 : 350, calendarWidth: options.formHorizontal ? 250 : 350, popupAnchorPosition: getCalendarAnchorPosition(options.formHorizontal), - calendarMaxMoment: !metaData.allowFutureDate ? moment() : undefined, + calendarMaxMoment: !metaData.allowFutureDate ? convertDateObjectToDateFormatString(moment()) : undefined, }, options, metaData); return createFieldConfig({ diff --git a/src/core_modules/capture-core/components/D2Form/field/configs/dateField/getDateFieldConfigForCustomForm.js b/src/core_modules/capture-core/components/D2Form/field/configs/dateField/getDateFieldConfigForCustomForm.js index b1b93fe119..8cb139fb73 100644 --- a/src/core_modules/capture-core/components/D2Form/field/configs/dateField/getDateFieldConfigForCustomForm.js +++ b/src/core_modules/capture-core/components/D2Form/field/configs/dateField/getDateFieldConfigForCustomForm.js @@ -2,6 +2,7 @@ import moment from 'moment'; import { createFieldConfig, createProps } from '../base/configBaseCustomForm'; import { DateFieldForCustomForm } from '../../Components'; +import { convertDateObjectToDateFormatString } from '../../../../../../capture-core/utils/converters/date'; import type { DateDataElement } from '../../../../../metaData'; import type { QuerySingleResource } from '../../../../../utils/api/api.types'; @@ -10,7 +11,7 @@ export const getDateFieldConfigForCustomForm = (metaData: DateDataElement, optio width: 350, maxWidth: 350, calendarWidth: 350, - calendarMaxMoment: !metaData.allowFutureDate ? moment() : undefined, + calendarMaxMoment: !metaData.allowFutureDate ? convertDateObjectToDateFormatString(moment()) : undefined, }, metaData); return createFieldConfig({ diff --git a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentDataEntry.component.js b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentDataEntry.component.js index 1444a98aea..76ed900ccc 100644 --- a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentDataEntry.component.js +++ b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentDataEntry.component.js @@ -42,6 +42,7 @@ import { withAOCFieldBuilder, withDataEntryFields, } from '../../DataEntryDhis2Helpers'; +import { convertDateObjectToDateFormatString } from '../../../../capture-core/utils/converters/date'; const overrideMessagePropNames = { errorMessage: 'validationError', @@ -111,7 +112,7 @@ const getEnrollmentDateSettings = () => { required: true, calendarWidth: props.formHorizontal ? 250 : 350, popupAnchorPosition: getCalendarAnchorPosition(props.formHorizontal), - calendarMaxMoment: !props.enrollmentMetadata.allowFutureEnrollmentDate ? moment() : undefined, + calendarMaxMoment: !props.enrollmentMetadata.allowFutureEnrollmentDate ? convertDateObjectToDateFormatString(moment()) : undefined, }), getPropName: () => 'enrolledAt', getValidatorContainers: getEnrollmentDateValidatorContainer, @@ -159,7 +160,9 @@ const getIncidentDateSettings = () => { required: true, calendarWidth: props.formHorizontal ? 250 : 350, popupAnchorPosition: getCalendarAnchorPosition(props.formHorizontal), - calendarMaxMoment: !props.enrollmentMetadata.allowFutureIncidentDate ? moment() : undefined, + calendarMaxMoment: !props.enrollmentMetadata.allowFutureIncidentDate ? + convertDateObjectToDateFormatString(moment()) : + undefined, }), getPropName: () => 'occurredAt', getPassOnFieldData: () => true, diff --git a/src/core_modules/capture-core/components/FormFields/DateAndTime/D2Date/D2Date.component.js b/src/core_modules/capture-core/components/FormFields/DateAndTime/D2Date/D2Date.component.js index eacca0a466..2adb875ce7 100644 --- a/src/core_modules/capture-core/components/FormFields/DateAndTime/D2Date/D2Date.component.js +++ b/src/core_modules/capture-core/components/FormFields/DateAndTime/D2Date/D2Date.component.js @@ -7,7 +7,6 @@ import { type DateValue } from '../../../FiltersForTypes/Date/types/date.types'; type Props = { label?: ?string, value: ?string, - calendar?: string, calendarWidth?: ?number, inputWidth?: ?number, onBlur: (value: DateValue) => void, @@ -50,7 +49,6 @@ export class D2Date extends React.Component { render() { const { - calendar, calendarWidth, inputWidth, classes, @@ -62,7 +60,7 @@ export class D2Date extends React.Component { ...passOnProps } = this.props; - const calendarType = calendar || 'gregory'; + const calendarType = systemSettingsStore.get().calendar || 'gregory'; const format = systemSettingsStore.get().dateFormat; return ( diff --git a/src/core_modules/capture-core/converters/clientToForm.js b/src/core_modules/capture-core/converters/clientToForm.js index f8e227b6d9..606b7e3f04 100644 --- a/src/core_modules/capture-core/converters/clientToForm.js +++ b/src/core_modules/capture-core/converters/clientToForm.js @@ -1,6 +1,6 @@ // @flow import moment from 'moment'; -import { convertMomentToDateFormatString } from '../utils/converters/date'; +import { convertMomentToDateFormatString, convertIsoToLocalCalendar } from '../utils/converters/date'; import { dataElementTypes } from '../metaData'; import { stringifyNumber } from './common/stringifyNumber'; @@ -23,16 +23,17 @@ type RangeValue = { } function convertDateForEdit(rawValue: string): string { - const momentInstance = moment(rawValue); - return convertMomentToDateFormatString(momentInstance); + const momentDate = moment(rawValue); + const dateString = momentDate.format('YYYY-MM-DD'); + return convertIsoToLocalCalendar(dateString); } function convertDateTimeForEdit(rawValue: string): DateTimeFormValue { const dateTime = moment(rawValue); - const dateString = convertMomentToDateFormatString(dateTime); + const dateString = dateTime.format('YYYY-MM-DD'); const timeString = dateTime.format('HH:mm'); return { - date: dateString, + date: convertIsoToLocalCalendar(dateString), time: timeString, }; } diff --git a/src/core_modules/capture-core/converters/clientToList.js b/src/core_modules/capture-core/converters/clientToList.js index e72b837179..6ee3cb78a7 100644 --- a/src/core_modules/capture-core/converters/clientToList.js +++ b/src/core_modules/capture-core/converters/clientToList.js @@ -5,21 +5,23 @@ import i18n from '@dhis2/d2-i18n'; import { Tag } from '@dhis2/ui'; import { PreviewImage } from 'capture-ui'; import { dataElementTypes, type DataElement } from '../metaData'; -import { convertMomentToDateFormatString } from '../utils/converters/date'; +import { convertIsoToLocalCalendar } from '../utils/converters/date'; import { stringifyNumber } from './common/stringifyNumber'; import { MinimalCoordinates } from '../components/MinimalCoordinates'; import { TooltipOrgUnit } from '../components/Tooltips/TooltipOrgUnit'; function convertDateForListDisplay(rawValue: string): string { const momentDate = moment(rawValue); - return convertMomentToDateFormatString(momentDate); + const dateString = momentDate.format('YYYY-MM-DD'); + return convertIsoToLocalCalendar(dateString); } function convertDateTimeForListDisplay(rawValue: string): string { const momentDate = moment(rawValue); - const dateString = convertMomentToDateFormatString(momentDate); + const dateString = momentDate.format('YYYY-MM-DD'); const timeString = momentDate.format('HH:mm'); - return `${dateString} ${timeString}`; + const localDate = convertIsoToLocalCalendar(dateString); + return `${localDate} ${timeString}`; } function convertTimeForListDisplay(rawValue: string): string { diff --git a/src/core_modules/capture-core/converters/clientToView.js b/src/core_modules/capture-core/converters/clientToView.js index fb7728f723..6f4e2b7e14 100644 --- a/src/core_modules/capture-core/converters/clientToView.js +++ b/src/core_modules/capture-core/converters/clientToView.js @@ -4,7 +4,7 @@ import moment from 'moment'; import i18n from '@dhis2/d2-i18n'; import { PreviewImage } from 'capture-ui'; import { dataElementTypes, type DataElement } from '../metaData'; -import { convertMomentToDateFormatString } from '../utils/converters/date'; +import { convertIsoToLocalCalendar } from '../utils/converters/date'; import { stringifyNumber } from './common/stringifyNumber'; import { MinimalCoordinates } from '../components/MinimalCoordinates'; import { TooltipOrgUnit } from '../components/Tooltips/TooltipOrgUnit'; @@ -12,14 +12,18 @@ import { TooltipOrgUnit } from '../components/Tooltips/TooltipOrgUnit'; function convertDateForView(rawValue: string): string { const momentDate = moment(rawValue); - return convertMomentToDateFormatString(momentDate); + const dateString = momentDate.format('YYYY-MM-DD'); + return convertIsoToLocalCalendar(dateString); } function convertDateTimeForView(rawValue: string): string { const momentDate = moment(rawValue); - const dateString = convertMomentToDateFormatString(momentDate); + const dateString = momentDate.format('YYYY-MM-DD'); const timeString = momentDate.format('HH:mm'); - return `${dateString} ${timeString}`; + + const localDate = convertIsoToLocalCalendar(dateString); + return `${localDate} ${timeString}`; } + function convertTimeForView(rawValue: string): string { const momentDate = moment(rawValue, 'HH:mm', true); return momentDate.format('HH:mm'); diff --git a/src/core_modules/capture-core/converters/formToClient.js b/src/core_modules/capture-core/converters/formToClient.js index 61c286d4d2..61b37923fd 100644 --- a/src/core_modules/capture-core/converters/formToClient.js +++ b/src/core_modules/capture-core/converters/formToClient.js @@ -1,8 +1,9 @@ // @flow +import moment from 'moment'; import isString from 'd2-utilizr/lib/isString'; import { parseNumber, parseTime } from 'capture-core-utils/parsers'; import { dataElementTypes } from '../metaData'; -import { parseDate } from '../utils/converters/date'; +import { parseDate, convertLocalToIsoCalendar } from '../utils/converters/date'; type DateTimeValue = { date: string, @@ -25,9 +26,11 @@ function convertDateTime(formValue: DateTimeValue): ?string { const minutes = momentTime.minute(); const parsedDate = editedDate ? parseDate(editedDate) : null; - if (!(parsedDate && parsedDate.isValid)) return null; - // $FlowFixMe[incompatible-type] automated comment - const momentDateTime: moment$Moment = parsedDate.momentDate; + if (!(parsedDate && parsedDate.isValid && parsedDate.momentDate)) return null; + + const formattedDate = parsedDate.momentDate.format('YYYY-MM-DD'); + const isoDate = convertLocalToIsoCalendar(formattedDate); + const momentDateTime = moment(isoDate); momentDateTime.hour(hours); momentDateTime.minute(minutes); return momentDateTime.toISOString(); @@ -35,8 +38,12 @@ function convertDateTime(formValue: DateTimeValue): ?string { function convertDate(dateValue: string) { const parsedDate = parseDate(dateValue); - // $FlowFixMe[incompatible-use] automated comment - return parsedDate.isValid ? parsedDate.momentDate.toISOString() : null; + if (!parsedDate.isValid || !parsedDate.momentDate) { + return null; + } + const formattedDate = parsedDate.momentDate.format('YYYY-MM-DD'); + + return convertLocalToIsoCalendar(formattedDate); } function convertTime(timeValue: string) { diff --git a/src/core_modules/capture-core/metaData/SystemSettings/SystemSettings.js b/src/core_modules/capture-core/metaData/SystemSettings/SystemSettings.js index 6231d3b637..ec8334b437 100644 --- a/src/core_modules/capture-core/metaData/SystemSettings/SystemSettings.js +++ b/src/core_modules/capture-core/metaData/SystemSettings/SystemSettings.js @@ -4,4 +4,5 @@ export class SystemSettings { dateFormat: string; dir: string; trackerAppRelativePath: string; + calendar: string; } diff --git a/src/core_modules/capture-core/metaDataStoreLoaders/systemSettings/cacheSystemSetttings.js b/src/core_modules/capture-core/metaDataStoreLoaders/systemSettings/cacheSystemSetttings.js index 7ff99e54aa..225c4261ab 100644 --- a/src/core_modules/capture-core/metaDataStoreLoaders/systemSettings/cacheSystemSetttings.js +++ b/src/core_modules/capture-core/metaDataStoreLoaders/systemSettings/cacheSystemSetttings.js @@ -10,7 +10,7 @@ function isLangRTL(code) { export async function cacheSystemSettings( uiLocale: string, - systemSettings: { dateFormat: string, serverTimeZoneId: string }, + systemSettings: { dateFormat: string, serverTimeZoneId: string, calendar: string, }, ) { const systemSettingsArray = [ { @@ -25,6 +25,10 @@ export async function cacheSystemSettings( id: 'serverTimeZoneId', value: systemSettings.serverTimeZoneId, }, + { + id: 'calendar', + value: systemSettings.calendar, + }, ]; const storageController = getMainStorageController(); diff --git a/src/core_modules/capture-core/utils/converters/date/convertIsoToLocalCalendar.js b/src/core_modules/capture-core/utils/converters/date/convertIsoToLocalCalendar.js new file mode 100644 index 0000000000..3f9f99e34b --- /dev/null +++ b/src/core_modules/capture-core/utils/converters/date/convertIsoToLocalCalendar.js @@ -0,0 +1,28 @@ +// @flow +import { + convertFromIso8601, +} from '@dhis2/multi-calendar-dates'; +import { systemSettingsStore } from '../../../../capture-core/metaDataMemoryStores'; +import { padWithZeros } from './padWithZeros'; + +/** + * Converts a date from ISO calendar to local calendar + * @export + * @param {string} isoDate - date in ISO format + * @returns {string} + */ + +export function convertIsoToLocalCalendar(isoDate: string): string { + if (!isoDate) { + return ''; + } + const calendar = systemSettingsStore.get().calendar; + const dateFormat = systemSettingsStore.get().dateFormat; + + const { year, eraYear, month, day } = convertFromIso8601(isoDate, calendar); + const localYear = calendar === 'ethiopian' ? eraYear : year; + + return dateFormat === 'DD-MM-YYYY' + ? `${padWithZeros(day, 2)}-${padWithZeros(month, 2)}-${padWithZeros(localYear, 4)}` + : `${padWithZeros(localYear, 4)}-${padWithZeros(month, 2)}-${padWithZeros(day, 2)}`; +} diff --git a/src/core_modules/capture-core/utils/converters/date/convertLocalToIsoCalendar.js b/src/core_modules/capture-core/utils/converters/date/convertLocalToIsoCalendar.js new file mode 100644 index 0000000000..99b1215919 --- /dev/null +++ b/src/core_modules/capture-core/utils/converters/date/convertLocalToIsoCalendar.js @@ -0,0 +1,26 @@ +// @flow +import moment from 'moment'; +import { + convertToIso8601, +} from '@dhis2/multi-calendar-dates'; +import { systemSettingsStore } from '../../../../capture-core/metaDataMemoryStores'; +import { padWithZeros } from './padWithZeros'; + +/** + * Converts a date from local calendar to ISO calendar + * @export + * @param {string} localDate - date in local calendar format + * @returns {string} + */ +export function convertLocalToIsoCalendar(localDate: string): string { + if (!localDate) { + return ''; + } + const calendar = systemSettingsStore.get().calendar; + + const { year, month, day } = convertToIso8601(localDate, calendar); + const dateString = `${padWithZeros(year, 4)}-${padWithZeros(month, 2)}-${padWithZeros(day, 2)}`; + const parsedMoment = moment(dateString); + + return parsedMoment.isValid() ? parsedMoment.toISOString() : ''; +} diff --git a/src/core_modules/capture-core/utils/converters/date/dateObjectToDateFormatString.js b/src/core_modules/capture-core/utils/converters/date/dateObjectToDateFormatString.js index de59f62d24..bbe748a579 100644 --- a/src/core_modules/capture-core/utils/converters/date/dateObjectToDateFormatString.js +++ b/src/core_modules/capture-core/utils/converters/date/dateObjectToDateFormatString.js @@ -1,6 +1,6 @@ // @flow import moment from 'moment'; -import { systemSettingsStore } from '../../../metaDataMemoryStores'; +import { convertIsoToLocalCalendar } from './convertIsoToLocalCalendar'; /** * Converts a date instance to a string based on the system date format @@ -8,8 +8,8 @@ import { systemSettingsStore } from '../../../metaDataMemoryStores'; * @param {Date} dateValue: the date instance * @returns {string} */ -export function convertDateObjectToDateFormatString(dateValue: Date) { - const dateFormat = systemSettingsStore.get().dateFormat; - const formattedDateString = moment(dateValue).format(dateFormat); - return formattedDateString; +export function convertDateObjectToDateFormatString(dateValue: Date | moment$Moment) { + const momentDate = moment(dateValue); + const dateString = momentDate.format('YYYY-MM-DD'); + return convertIsoToLocalCalendar(dateString); } diff --git a/src/core_modules/capture-core/utils/converters/date/index.js b/src/core_modules/capture-core/utils/converters/date/index.js index f7e46c2971..511298b89d 100644 --- a/src/core_modules/capture-core/utils/converters/date/index.js +++ b/src/core_modules/capture-core/utils/converters/date/index.js @@ -3,3 +3,6 @@ export { parseDate } from './parser'; export { convertDateObjectToDateFormatString } from './dateObjectToDateFormatString'; export { convertMomentToDateFormatString } from './momentToDateFormatString'; export { convertStringToDateFormat } from './stringToMomentDateFormat'; +export { padWithZeros } from './padWithZeros'; +export { convertIsoToLocalCalendar } from './convertIsoToLocalCalendar'; +export { convertLocalToIsoCalendar } from './convertLocalToIsoCalendar'; diff --git a/src/core_modules/capture-core/utils/converters/date/padWithZeros.js b/src/core_modules/capture-core/utils/converters/date/padWithZeros.js new file mode 100644 index 0000000000..5edc85befd --- /dev/null +++ b/src/core_modules/capture-core/utils/converters/date/padWithZeros.js @@ -0,0 +1,12 @@ +// @flow + +/** + * Pads a string or number with zeros at the start to reach a minimum length + * @export + * @param {string|number} value - the value to pad + * @param {number} length - length required + * @returns {string} + */ +export function padWithZeros(value: string | number, length: number): string { + return String(value).padStart(length, '0'); +} diff --git a/src/core_modules/capture-ui/DateAndTimeFields/DateField/Date.component.js b/src/core_modules/capture-ui/DateAndTimeFields/DateField/Date.component.js index b48ee77ae5..95bf289714 100644 --- a/src/core_modules/capture-ui/DateAndTimeFields/DateField/Date.component.js +++ b/src/core_modules/capture-ui/DateAndTimeFields/DateField/Date.component.js @@ -18,7 +18,6 @@ type Props = { onBlur: (value: Object, options: ValidationOptions) => void, onFocus?: ?() => void, onDateSelectedFromCalendar?: () => void, - calendar?: string, placeholder?: string, label?: string, calendarMaxMoment?: any, @@ -36,9 +35,6 @@ type State = { calendarError: ?Validation, }; -const formatDate = (date: any, dateFormat: string): ?string => - (dateFormat === 'dd-MM-yyyy' ? date?.format('DD-MM-YYYY') : date?.format('YYYY-MM-DD')); - export class DateField extends React.Component { handleDateSelected: (value: {calendarDateString: string}) => void; @@ -65,7 +61,6 @@ export class DateField extends React.Component { maxWidth, calendarWidth, inputWidth, - calendar, calendarMaxMoment, value, innerMessage, @@ -73,7 +68,7 @@ export class DateField extends React.Component { const calculatedInputWidth = inputWidth || width; const calculatedCalendarWidth = calendarWidth || width; - const calendarType = calendar || 'gregory'; + const calendarType = systemSettingsStore.get().calendar || 'gregory'; const format = systemSettingsStore.get().dateFormat; const errorProps = innerMessage && innerMessage.messageType === 'error' ? { error: !!innerMessage.message?.dateInnerErrorMessage, @@ -99,7 +94,7 @@ export class DateField extends React.Component { onFocus={this.props.onFocus} disabled={this.props.disabled} {...errorProps} - maxDate={calendarMaxMoment && formatDate(calendarMaxMoment, format)} + maxDate={calendarMaxMoment} /> ); diff --git a/yarn.lock b/yarn.lock index 4c2c7b3910..9a8bd281de 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2718,7 +2718,7 @@ recompose "^0.26.0" rxjs "^5.5.7" -"@dhis2/multi-calendar-dates@2.0.0": +"@dhis2/multi-calendar-dates@2.0.0", "@dhis2/multi-calendar-dates@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@dhis2/multi-calendar-dates/-/multi-calendar-dates-2.0.0.tgz#febf04f873670960804d38c9ebaa1cadf8050db3" integrity sha512-pxu81kkkh70tB+CyAub41ulpNJPHyxDGwH2pdcc+NUqrKu4OTQr5ScdCBL2MndShrEKj9J6qj9zKVagvvymH5w== From b8fd9411c421044c6738e0aa2bcef4895e88e9e8 Mon Sep 17 00:00:00 2001 From: alaa-yahia Date: Thu, 5 Dec 2024 01:36:14 +0200 Subject: [PATCH 02/42] fix: working list filters to use gregorian --- .../Date/DateFilter.component.js | 28 +++++++++++++++---- .../date/convertIsoToLocalCalendar.js | 2 +- .../date/convertLocalToIsoCalendar.js | 2 +- .../date/dateObjectToDateFormatString.js | 2 +- 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/core_modules/capture-core/components/FiltersForTypes/Date/DateFilter.component.js b/src/core_modules/capture-core/components/FiltersForTypes/Date/DateFilter.component.js index b0fa8f967e..20fc8fe9fd 100644 --- a/src/core_modules/capture-core/components/FiltersForTypes/Date/DateFilter.component.js +++ b/src/core_modules/capture-core/components/FiltersForTypes/Date/DateFilter.component.js @@ -1,6 +1,7 @@ // @flow import React, { Component } from 'react'; import classNames from 'classnames'; +import moment from 'moment'; import { withStyles } from '@material-ui/core/styles'; import i18n from '@dhis2/d2-i18n'; import { isValidZeroOrPositiveInteger } from 'capture-core-utils/validators/form'; @@ -16,7 +17,8 @@ import './calendarFilterStyles.css'; import { mainOptionKeys, mainOptionTranslatedTexts } from './options'; import { getDateFilterData } from './dateFilterDataGetter'; import { RangeFilter } from './RangeFilter.component'; -import { parseDate } from '../../../utils/converters/date'; +import { parseDate, convertLocalToIsoCalendar, convertIsoToLocalCalendar } from '../../../utils/converters/date'; +import { systemSettingsStore } from '../../../metaDataMemoryStores'; const getStyles = (theme: Theme) => ({ fromToContainer: { @@ -265,12 +267,28 @@ class DateFilterPlain extends Component implements UpdatableFilter return !values || DateFilter.isFilterValid(values.main, values.from, values.to, values.start, values.end); } - getUpdatedValue(valuePart: { [key: string]: string }) { - // $FlowFixMe[cannot-spread-indexer] automated comment + // eslint-disable-next-line complexity + getUpdatedValue(valuePart: Object) { const valueObject = { ...this.props.value, ...valuePart, }; + const dateFormat = systemSettingsStore.get().dateFormat; + + if (valuePart.from && valueObject?.from?.value) { + valueObject.from = { + ...valueObject.from, + value: moment(convertLocalToIsoCalendar(valueObject.from.value)).format(dateFormat), + }; + } + + if (valuePart.to && valueObject?.to?.value) { + valueObject.to = { + ...valueObject.to, + value: moment(convertLocalToIsoCalendar(valueObject.to.value)).format(dateFormat), + }; + } + const isRelativeRangeValue = () => valueObject?.start || valuePart?.start || valuePart?.end; const isAbsoluteRangevalue = () => valueObject?.from || valuePart?.from || valuePart?.to; @@ -366,7 +384,7 @@ class DateFilterPlain extends Component implements UpdatableFilter {/* $FlowSuppress: Flow not working 100% with HOCs */} {/* $FlowFixMe[prop-missing] automated comment */} implements UpdatableFilter {/* $FlowSuppress: Flow not working 100% with HOCs */} {/* $FlowFixMe[prop-missing] automated comment */} Date: Tue, 10 Dec 2024 14:18:33 +0200 Subject: [PATCH 03/42] fix: working list filter runtime error when dd-mm-yyyy format is passed --- .../converters/date/convertIsoToLocalCalendar.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/core_modules/capture-core/utils/converters/date/convertIsoToLocalCalendar.js b/src/core_modules/capture-core/utils/converters/date/convertIsoToLocalCalendar.js index 019872c197..442ad4a60c 100644 --- a/src/core_modules/capture-core/utils/converters/date/convertIsoToLocalCalendar.js +++ b/src/core_modules/capture-core/utils/converters/date/convertIsoToLocalCalendar.js @@ -1,4 +1,5 @@ // @flow +import moment from 'moment'; import { convertFromIso8601, } from '@dhis2/multi-calendar-dates'; @@ -16,10 +17,18 @@ export function convertIsoToLocalCalendar(isoDate: ?string): string { if (!isoDate) { return ''; } + + const momentDate = moment(isoDate); + if (!momentDate.isValid()) { + return ''; + } + + const formattedIsoDate = momentDate.format('YYYY-MM-DD'); + const calendar = systemSettingsStore.get().calendar; const dateFormat = systemSettingsStore.get().dateFormat; - const { year, eraYear, month, day } = convertFromIso8601(isoDate, calendar); + const { year, eraYear, month, day } = convertFromIso8601(formattedIsoDate, calendar); const localYear = calendar === 'ethiopian' ? eraYear : year; return dateFormat === 'DD-MM-YYYY' From 8e44c3cad99d9ddc07f7f7a6dc7efc747b716ebf Mon Sep 17 00:00:00 2001 From: alaa-yahia Date: Wed, 11 Dec 2024 11:27:52 +0200 Subject: [PATCH 04/42] fix: missing rules engine in dependencies --- package.json | 1 + yarn.lock | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 1f22447823..1178b6e2e1 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "packages/rules-engine" ], "dependencies": { + "@dhis2/rules-engine-javascript": "101.19.1", "@dhis2-ui/calendar": "^10.0.3", "@dhis2/app-runtime": "^3.9.3", "@dhis2/d2-i18n": "^1.1.0", diff --git a/yarn.lock b/yarn.lock index 9a8bd281de..4c2c7b3910 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2718,7 +2718,7 @@ recompose "^0.26.0" rxjs "^5.5.7" -"@dhis2/multi-calendar-dates@2.0.0", "@dhis2/multi-calendar-dates@^2.0.0": +"@dhis2/multi-calendar-dates@2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@dhis2/multi-calendar-dates/-/multi-calendar-dates-2.0.0.tgz#febf04f873670960804d38c9ebaa1cadf8050db3" integrity sha512-pxu81kkkh70tB+CyAub41ulpNJPHyxDGwH2pdcc+NUqrKu4OTQr5ScdCBL2MndShrEKj9J6qj9zKVagvvymH5w== From 1f2518d9be14690500b64a8a5a3b0954fa62d130 Mon Sep 17 00:00:00 2001 From: alaa-yahia Date: Wed, 11 Dec 2024 13:13:00 +0200 Subject: [PATCH 05/42] fix: working list filter not displaying local date --- .../buttonTextBuilder/converters/dateConverter.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/core_modules/capture-core/components/ListView/Filters/FilterButton/buttonTextBuilder/converters/dateConverter.js b/src/core_modules/capture-core/components/ListView/Filters/FilterButton/buttonTextBuilder/converters/dateConverter.js index 4dce8ba7a9..adbc7c367d 100644 --- a/src/core_modules/capture-core/components/ListView/Filters/FilterButton/buttonTextBuilder/converters/dateConverter.js +++ b/src/core_modules/capture-core/components/ListView/Filters/FilterButton/buttonTextBuilder/converters/dateConverter.js @@ -6,6 +6,8 @@ import { convertMomentToDateFormatString } from '../../../../../../utils/convert import type { DateFilterData, AbsoluteDateFilterData } from '../../../../../FiltersForTypes'; import { areRelativeRangeValuesSupported } from '../../../../../../utils/validation/validators/areRelativeRangeValuesSupported'; +import { convertClientToView } from '../../../../../../../capture-core/converters'; +import { dataElementTypes } from '../../../../../../metaData'; const periods = { TODAY: 'TODAY', @@ -37,8 +39,8 @@ const convertToViewValue = (filterValue: string) => pipe( function translateAbsoluteDate(filter: AbsoluteDateFilterData) { let appliedText = ''; - const fromValue = filter.ge; - const toValue = filter.le; + const fromValue = convertClientToView(filter.ge, dataElementTypes.DATE); + const toValue = convertClientToView(filter.le, dataElementTypes.DATE); if (fromValue && toValue) { const momentFrom = moment(fromValue); From 9a3d60960dc8c8228b6fd8cf1446bf0953e3fc60 Mon Sep 17 00:00:00 2001 From: alaa-yahia Date: Wed, 11 Dec 2024 13:27:26 +0200 Subject: [PATCH 06/42] fix: reduce the complexity of getUpdatedValue --- .../FiltersForTypes/Date/DateFilter.component.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/core_modules/capture-core/components/FiltersForTypes/Date/DateFilter.component.js b/src/core_modules/capture-core/components/FiltersForTypes/Date/DateFilter.component.js index 20fc8fe9fd..a38d9cd704 100644 --- a/src/core_modules/capture-core/components/FiltersForTypes/Date/DateFilter.component.js +++ b/src/core_modules/capture-core/components/FiltersForTypes/Date/DateFilter.component.js @@ -267,22 +267,23 @@ class DateFilterPlain extends Component implements UpdatableFilter return !values || DateFilter.isFilterValid(values.main, values.from, values.to, values.start, values.end); } - // eslint-disable-next-line complexity getUpdatedValue(valuePart: Object) { const valueObject = { ...this.props.value, ...valuePart, }; const dateFormat = systemSettingsStore.get().dateFormat; + const hasFromValue = () => valuePart.from && valueObject?.from?.value; + const hasToValue = () => valuePart.to && valueObject?.to?.value; - if (valuePart.from && valueObject?.from?.value) { + if (hasFromValue()) { valueObject.from = { ...valueObject.from, value: moment(convertLocalToIsoCalendar(valueObject.from.value)).format(dateFormat), }; } - if (valuePart.to && valueObject?.to?.value) { + if (hasToValue()) { valueObject.to = { ...valueObject.to, value: moment(convertLocalToIsoCalendar(valueObject.to.value)).format(dateFormat), From 3a3d0b2c320e2c6d48071b1ec3909450a6e9bc48 Mon Sep 17 00:00:00 2001 From: alaa-yahia Date: Wed, 11 Dec 2024 23:37:38 +0200 Subject: [PATCH 07/42] chore: refactor working lists filter component --- .../Date/DateFilter.component.js | 53 +++++++++++-------- .../converters/dateConverter.js | 24 +++------ 2 files changed, 39 insertions(+), 38 deletions(-) diff --git a/src/core_modules/capture-core/components/FiltersForTypes/Date/DateFilter.component.js b/src/core_modules/capture-core/components/FiltersForTypes/Date/DateFilter.component.js index a38d9cd704..80b002c049 100644 --- a/src/core_modules/capture-core/components/FiltersForTypes/Date/DateFilter.component.js +++ b/src/core_modules/capture-core/components/FiltersForTypes/Date/DateFilter.component.js @@ -273,22 +273,18 @@ class DateFilterPlain extends Component implements UpdatableFilter ...valuePart, }; const dateFormat = systemSettingsStore.get().dateFormat; - const hasFromValue = () => valuePart.from && valueObject?.from?.value; - const hasToValue = () => valuePart.to && valueObject?.to?.value; - if (hasFromValue()) { - valueObject.from = { - ...valueObject.from, - value: moment(convertLocalToIsoCalendar(valueObject.from.value)).format(dateFormat), - }; - } - - if (hasToValue()) { - valueObject.to = { - ...valueObject.to, - value: moment(convertLocalToIsoCalendar(valueObject.to.value)).format(dateFormat), - }; - } + ['from', 'to'].forEach((key) => { + if (valuePart[key] && valueObject[key]?.value) { + const isValidDate = valuePart[key]?.isValid; + valueObject[key] = { + ...valueObject[key], + value: isValidDate ? + moment(convertLocalToIsoCalendar(valueObject[key].value)).format(dateFormat) : + valueObject[key].value, + }; + } + }); const isRelativeRangeValue = () => valueObject?.start || valuePart?.start || valuePart?.end; const isAbsoluteRangevalue = () => valueObject?.from || valuePart?.from || valuePart?.to; @@ -362,10 +358,23 @@ class DateFilterPlain extends Component implements UpdatableFilter render() { const { value, classes, onFocusUpdateButton } = this.props; - const fromValue = value?.from; - const toValue = value?.to; const { startValueError, endValueError, dateLogicError, bufferLogicError } = - this.getErrors(); + this.getErrors(); + + const dateFormat = systemSettingsStore.get().dateFormat; + + const formatDate = (dateValue) => { + if (!dateValue) return ''; + const momentValue = moment(dateValue, dateFormat); + if (!momentValue.isValid()) return dateValue; + const isoDate = momentValue.format('YYYY-MM-DD'); + return convertIsoToLocalCalendar(isoDate); + }; + + const from = value?.from; + const to = value?.to; + const fromDate = formatDate(from?.value); + const toDate = formatDate(to?.value); return (
@@ -385,11 +394,11 @@ class DateFilterPlain extends Component implements UpdatableFilter {/* $FlowSuppress: Flow not working 100% with HOCs */} {/* $FlowFixMe[prop-missing] automated comment */}
@@ -398,11 +407,11 @@ class DateFilterPlain extends Component implements UpdatableFilter {/* $FlowSuppress: Flow not working 100% with HOCs */} {/* $FlowFixMe[prop-missing] automated comment */} diff --git a/src/core_modules/capture-core/components/ListView/Filters/FilterButton/buttonTextBuilder/converters/dateConverter.js b/src/core_modules/capture-core/components/ListView/Filters/FilterButton/buttonTextBuilder/converters/dateConverter.js index adbc7c367d..35c60d8d2e 100644 --- a/src/core_modules/capture-core/components/ListView/Filters/FilterButton/buttonTextBuilder/converters/dateConverter.js +++ b/src/core_modules/capture-core/components/ListView/Filters/FilterButton/buttonTextBuilder/converters/dateConverter.js @@ -1,13 +1,10 @@ // @flow import i18n from '@dhis2/d2-i18n'; -import { pipe } from 'capture-core-utils'; import moment from 'moment'; -import { convertMomentToDateFormatString } from '../../../../../../utils/converters/date'; +import { convertIsoToLocalCalendar } from '../../../../../../utils/converters/date'; import type { DateFilterData, AbsoluteDateFilterData } from '../../../../../FiltersForTypes'; import { areRelativeRangeValuesSupported } from '../../../../../../utils/validation/validators/areRelativeRangeValuesSupported'; -import { convertClientToView } from '../../../../../../../capture-core/converters'; -import { dataElementTypes } from '../../../../../../metaData'; const periods = { TODAY: 'TODAY', @@ -32,32 +29,27 @@ const translatedPeriods = { [periods.RELATIVE_RANGE]: i18n.t('Relative range'), }; -const convertToViewValue = (filterValue: string) => pipe( - value => moment(value), - momentDate => convertMomentToDateFormatString(momentDate), -)(filterValue); - function translateAbsoluteDate(filter: AbsoluteDateFilterData) { let appliedText = ''; - const fromValue = convertClientToView(filter.ge, dataElementTypes.DATE); - const toValue = convertClientToView(filter.le, dataElementTypes.DATE); + const fromValue = filter.ge; + const toValue = filter.le; if (fromValue && toValue) { const momentFrom = moment(fromValue); const momentTo = moment(toValue); if (momentFrom.isSame(momentTo)) { - appliedText = convertMomentToDateFormatString(momentFrom); + appliedText = convertIsoToLocalCalendar(fromValue); } else { - const appliedTextFrom = convertMomentToDateFormatString(momentFrom); - const appliedTextTo = convertMomentToDateFormatString(momentTo); + const appliedTextFrom = convertIsoToLocalCalendar(fromValue); + const appliedTextTo = convertIsoToLocalCalendar(toValue); appliedText = i18n.t('{{fromDate}} to {{toDate}}', { fromDate: appliedTextFrom, toDate: appliedTextTo }); } } else if (fromValue) { - const appliedTextFrom = convertToViewValue(fromValue); + const appliedTextFrom = convertIsoToLocalCalendar(fromValue); appliedText = i18n.t('after or equal to {{date}}', { date: appliedTextFrom }); } else { // $FlowFixMe[incompatible-call] automated comment - const appliedTextTo = convertToViewValue(toValue); + const appliedTextTo = convertIsoToLocalCalendar(toValue); appliedText = i18n.t('before or equal to {{date}}', { date: appliedTextTo }); } return appliedText; From 53ef00edf71809d50e44332171ffdebeca1af7a9 Mon Sep 17 00:00:00 2001 From: alaa-yahia Date: Wed, 11 Dec 2024 23:45:12 +0200 Subject: [PATCH 08/42] fix: convert age values to local date --- src/core_modules/capture-core/converters/clientToForm.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core_modules/capture-core/converters/clientToForm.js b/src/core_modules/capture-core/converters/clientToForm.js index 606b7e3f04..c0b0d10948 100644 --- a/src/core_modules/capture-core/converters/clientToForm.js +++ b/src/core_modules/capture-core/converters/clientToForm.js @@ -1,6 +1,6 @@ // @flow import moment from 'moment'; -import { convertMomentToDateFormatString, convertIsoToLocalCalendar } from '../utils/converters/date'; +import { convertIsoToLocalCalendar } from '../utils/converters/date'; import { dataElementTypes } from '../metaData'; import { stringifyNumber } from './common/stringifyNumber'; @@ -56,7 +56,7 @@ function convertAgeForEdit(rawValue: string): AgeFormValue { const days = now.diff(age, 'days'); return { - date: convertMomentToDateFormatString(moment(rawValue)), + date: convertIsoToLocalCalendar(rawValue), years: years.toString(), months: months.toString(), days: days.toString(), From 1d3faa6538664fd29c9143751d2e24cf1c7f6369 Mon Sep 17 00:00:00 2001 From: Alaa Yahia <6881345+alaa-yahia@users.noreply.github.com> Date: Mon, 16 Dec 2024 10:44:21 +0200 Subject: [PATCH 09/42] feat: [DHIS2 15466] typing the date when editing enrollment and incident date (#3905) feat: typing the date when editing enrollment and incident date --- .../WidgetEnrollment/Date/Date.component.js | 45 ++++++++++++------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/Date/Date.component.js b/src/core_modules/capture-core/components/WidgetEnrollment/Date/Date.component.js index 0fdba91aea..1b8dc31ee1 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/Date/Date.component.js +++ b/src/core_modules/capture-core/components/WidgetEnrollment/Date/Date.component.js @@ -1,9 +1,8 @@ // @flow import React, { useState, useCallback } from 'react'; -import moment from 'moment'; +import { DateField } from 'capture-core/components/FormFields/New'; import { Button, - CalendarInput, IconCalendar16, IconEdit16, colors, @@ -12,8 +11,11 @@ import { import i18n from '@dhis2/d2-i18n'; import { withStyles } from '@material-ui/core'; import { convertValue as convertValueClientToView } from '../../../converters/clientToView'; +import { convertValue as convertValueFormToClient } from '../../../converters/formToClient'; +import { convertValue as convertValueClientToServer } from '../../../converters/clientToServer'; import { dataElementTypes } from '../../../metaData'; + type Props = { date: string, dateLabel: string, @@ -24,7 +26,7 @@ type Props = { ...CssClasses, } -const styles = { +const styles = (theme: Theme) => ({ editButton: { display: 'inline-flex', alignItems: 'center', @@ -62,7 +64,11 @@ const styles = { fontSize: '12px', color: colors.grey700, }, -}; + error: { + ...theme.typography.caption, + color: theme.palette.error.main, + }, +}); const DateComponentPlain = ({ date, @@ -75,21 +81,23 @@ const DateComponentPlain = ({ }: Props) => { const [editMode, setEditMode] = useState(false); const [selectedDate, setSelectedDate] = useState(); - const dateChangeHandler = useCallback(({ calendarDateString }) => { - setSelectedDate(calendarDateString); + const [validation, setValidation] = useState(); + + const dateChangeHandler = useCallback((dateString, internalComponentError) => { + setSelectedDate(dateString); + setValidation(internalComponentError); }, [setSelectedDate]); const displayDate = String(convertValueClientToView(date, dataElementTypes.DATE)); const onOpenEdit = () => { - // CalendarInput component only supports the YYYY-MM-DD format - setSelectedDate(moment(date).format('YYYY-MM-DD')); + setSelectedDate(String(convertValueClientToView(date, dataElementTypes.DATE))); setEditMode(true); }; const saveHandler = () => { - // CalendarInput component only supports the YYYY-MM-DD format if (selectedDate) { - const newDate = moment.utc(selectedDate, 'YYYY-MM-DD').format('YYYY-MM-DDTHH:mm:ss.SSS'); - if (newDate !== date) { + const newClientDate = convertValueFormToClient(selectedDate, dataElementTypes.DATE); + const newDate = convertValueClientToServer(newClientDate, dataElementTypes.DATE); + if (typeof newDate === 'string' && newDate !== date) { onSave(newDate); } } @@ -99,21 +107,24 @@ const DateComponentPlain = ({ return editMode ? (
- +
+ {validation && validation.error ? i18n.t('Please provide a valid date') : ''} +
From 7968ea734503ae0e14d3c13a61baead5e67161f7 Mon Sep 17 00:00:00 2001 From: alaa-yahia Date: Mon, 16 Dec 2024 11:45:42 +0200 Subject: [PATCH 10/42] fix: age values not filled correctly --- .../capture-ui/AgeField/AgeField.component.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/core_modules/capture-ui/AgeField/AgeField.component.js b/src/core_modules/capture-ui/AgeField/AgeField.component.js index a125ea8900..78abcf4a21 100644 --- a/src/core_modules/capture-ui/AgeField/AgeField.component.js +++ b/src/core_modules/capture-ui/AgeField/AgeField.component.js @@ -1,6 +1,8 @@ // @flow import React, { Component } from 'react'; import { isValidPositiveInteger } from 'capture-core-utils/validators/form'; +import { convertDateObjectToDateFormatString } from 'capture-core/utils/converters/date'; +import { systemSettingsStore } from 'capture-core/metaDataMemoryStores'; import i18n from '@dhis2/d2-i18n'; import classNames from 'classnames'; import { IconButton } from 'capture-ui'; @@ -72,7 +74,8 @@ function getCalculatedValues( days: '', }; } - const now = moment(); + const dateFormat = systemSettingsStore.get().dateFormat; + const now = moment(convertDateObjectToDateFormatString(moment()), dateFormat); const age = moment(parseData.momentDate); const years = now.diff(age, 'years'); @@ -133,8 +136,8 @@ class D2AgeFieldPlain extends Component { this.props.onBlur({ ...values, date: '' }); return; } - - const momentDate = moment(undefined, undefined, true); + const dateFormat = systemSettingsStore.get().dateFormat; + const momentDate = moment(convertDateObjectToDateFormatString(moment(undefined, undefined, true)), dateFormat); momentDate.subtract(D2AgeFieldPlain.getNumberOrZero(values.years), 'years'); momentDate.subtract(D2AgeFieldPlain.getNumberOrZero(values.months), 'months'); momentDate.subtract(D2AgeFieldPlain.getNumberOrZero(values.days), 'days'); From e8d226f9b13a93abee91e2c0f30a09bb81e88c9c Mon Sep 17 00:00:00 2001 From: alaa-yahia Date: Sun, 22 Dec 2024 23:51:24 +0200 Subject: [PATCH 11/42] fix: keep local calendar date in state in working list filters --- .../Date/DateFilter.component.js | 82 +++++++------------ .../Date/DateFilterManager.component.js | 7 +- .../Date/dateFilterDataGetter.js | 6 +- .../date/stringToMomentDateFormat.js | 4 +- 4 files changed, 36 insertions(+), 63 deletions(-) diff --git a/src/core_modules/capture-core/components/FiltersForTypes/Date/DateFilter.component.js b/src/core_modules/capture-core/components/FiltersForTypes/Date/DateFilter.component.js index 80b002c049..1e03d9167e 100644 --- a/src/core_modules/capture-core/components/FiltersForTypes/Date/DateFilter.component.js +++ b/src/core_modules/capture-core/components/FiltersForTypes/Date/DateFilter.component.js @@ -1,9 +1,9 @@ // @flow import React, { Component } from 'react'; import classNames from 'classnames'; -import moment from 'moment'; import { withStyles } from '@material-ui/core/styles'; import i18n from '@dhis2/d2-i18n'; +import { Temporal } from '@js-temporal/polyfill'; import { isValidZeroOrPositiveInteger } from 'capture-core-utils/validators/form'; import { SelectBoxes, orientations } from '../../FormFields/Options/SelectBoxes'; import { OptionSet } from '../../../metaData/OptionSet/OptionSet'; @@ -17,8 +17,7 @@ import './calendarFilterStyles.css'; import { mainOptionKeys, mainOptionTranslatedTexts } from './options'; import { getDateFilterData } from './dateFilterDataGetter'; import { RangeFilter } from './RangeFilter.component'; -import { parseDate, convertLocalToIsoCalendar, convertIsoToLocalCalendar } from '../../../utils/converters/date'; -import { systemSettingsStore } from '../../../metaDataMemoryStores'; +import { convertStringToDateFormat } from '../../../utils/converters/date'; const getStyles = (theme: Theme) => ({ fromToContainer: { @@ -119,24 +118,27 @@ const getRelativeRangeErrors = (startValue, endValue, submitAttempted) => { return errors; }; +// eslint-disable-next-line complexity const isAbsoluteRangeFilterValid = (from, to) => { - if (!from?.value && !to?.value) { - return false; - } const fromValue = from?.value; const toValue = to?.value; - const parseResultFrom = fromValue ? parseDate(fromValue) : { isValid: true, moment: null }; - const parseResultTo = toValue ? parseDate(toValue) : { isValid: true, moment: null }; - if (!(parseResultFrom.isValid && parseResultTo.isValid)) { + if (!fromValue && !toValue) { + return false; + } + + const isFromValueValid = from ? from.isValid : true; + const isToValueValid = to ? to.isValid : true; + + if (!isFromValueValid || !isToValueValid) { return false; } - const isValidMomentDate = () => - parseResultFrom.momentDate && - parseResultTo.momentDate && - parseResultFrom.momentDate.isAfter(parseResultTo.momentDate); - return !isValidMomentDate(); + if ((!fromValue && toValue) || (fromValue && !toValue)) { + return true; + } + + return !DateFilter.isFromAfterTo(fromValue, toValue); }; const isRelativeRangeFilterValid = (startValue, endValue) => { @@ -188,11 +190,9 @@ class DateFilterPlain extends Component implements UpdatableFilter } static isFromAfterTo(valueFrom: string, valueTo: string) { - const momentFrom = parseDate(valueFrom).momentDate; - const momentTo = parseDate(valueTo).momentDate; - // $FlowFixMe[incompatible-use] automated comment - // $FlowFixMe[incompatible-call] automated comment - return momentFrom.isAfter(momentTo); + const formattedFrom = convertStringToDateFormat(valueFrom, 'YYYY-MM-DD'); + const fromattedTo = convertStringToDateFormat(valueTo, 'YYYY-MM-DD'); + return Temporal.PlainDate.compare(formattedFrom, fromattedTo) > 0; } toD2DateTextFieldInstance: any; @@ -267,25 +267,12 @@ class DateFilterPlain extends Component implements UpdatableFilter return !values || DateFilter.isFilterValid(values.main, values.from, values.to, values.start, values.end); } - getUpdatedValue(valuePart: Object) { + getUpdatedValue(valuePart: { [key: string]: string }) { + // $FlowFixMe[cannot-spread-indexer] automated comment const valueObject = { ...this.props.value, ...valuePart, }; - const dateFormat = systemSettingsStore.get().dateFormat; - - ['from', 'to'].forEach((key) => { - if (valuePart[key] && valueObject[key]?.value) { - const isValidDate = valuePart[key]?.isValid; - valueObject[key] = { - ...valueObject[key], - value: isValidDate ? - moment(convertLocalToIsoCalendar(valueObject[key].value)).format(dateFormat) : - valueObject[key].value, - }; - } - }); - const isRelativeRangeValue = () => valueObject?.start || valuePart?.start || valuePart?.end; const isAbsoluteRangevalue = () => valueObject?.from || valuePart?.from || valuePart?.to; @@ -358,23 +345,10 @@ class DateFilterPlain extends Component implements UpdatableFilter render() { const { value, classes, onFocusUpdateButton } = this.props; + const fromValue = value?.from; + const toValue = value?.to; const { startValueError, endValueError, dateLogicError, bufferLogicError } = - this.getErrors(); - - const dateFormat = systemSettingsStore.get().dateFormat; - - const formatDate = (dateValue) => { - if (!dateValue) return ''; - const momentValue = moment(dateValue, dateFormat); - if (!momentValue.isValid()) return dateValue; - const isoDate = momentValue.format('YYYY-MM-DD'); - return convertIsoToLocalCalendar(isoDate); - }; - - const from = value?.from; - const to = value?.to; - const fromDate = formatDate(from?.value); - const toDate = formatDate(to?.value); + this.getErrors(); return (
@@ -394,11 +368,11 @@ class DateFilterPlain extends Component implements UpdatableFilter {/* $FlowSuppress: Flow not working 100% with HOCs */} {/* $FlowFixMe[prop-missing] automated comment */}
@@ -407,11 +381,11 @@ class DateFilterPlain extends Component implements UpdatableFilter {/* $FlowSuppress: Flow not working 100% with HOCs */} {/* $FlowFixMe[prop-missing] automated comment */}
diff --git a/src/core_modules/capture-core/components/FiltersForTypes/Date/DateFilterManager.component.js b/src/core_modules/capture-core/components/FiltersForTypes/Date/DateFilterManager.component.js index 928d9fd754..c0464eb8a6 100644 --- a/src/core_modules/capture-core/components/FiltersForTypes/Date/DateFilterManager.component.js +++ b/src/core_modules/capture-core/components/FiltersForTypes/Date/DateFilterManager.component.js @@ -1,8 +1,7 @@ // @flow import * as React from 'react'; -import moment from 'moment'; import log from 'loglevel'; -import { convertMomentToDateFormatString } from '../../../utils/converters/date'; +import { convertIsoToLocalCalendar } from '../../../utils/converters/date'; import { DateFilter } from './DateFilter.component'; import { mainOptionKeys } from './options'; import { dateFilterTypes } from './constants'; @@ -22,8 +21,8 @@ type State = { export class DateFilterManager extends React.Component { static convertDateForEdit(rawValue: string) { - const momentInstance = moment(rawValue); - return convertMomentToDateFormatString(momentInstance); + const localDate = convertIsoToLocalCalendar(rawValue); + return localDate; } static calculateAbsoluteRangeValueState(filter: DateFilterData) { return { diff --git a/src/core_modules/capture-core/components/FiltersForTypes/Date/dateFilterDataGetter.js b/src/core_modules/capture-core/components/FiltersForTypes/Date/dateFilterDataGetter.js index f7758cd1b1..6be16601ed 100644 --- a/src/core_modules/capture-core/components/FiltersForTypes/Date/dateFilterDataGetter.js +++ b/src/core_modules/capture-core/components/FiltersForTypes/Date/dateFilterDataGetter.js @@ -2,7 +2,7 @@ import { parseNumber } from 'capture-core-utils/parsers'; import { mainOptionKeys } from './options'; import { dateFilterTypes } from './constants'; -import { parseDate } from '../../../utils/converters/date'; +import { convertLocalToIsoCalendar } from '../../../utils/converters/date'; import { type AbsoluteDateFilterData, type RelativeDateFilterData, type DateValue } from './types'; type Value = { @@ -20,13 +20,13 @@ function convertAbsoluteDate(fromValue: ?string, toValue: ?string) { if (fromValue) { // $FlowFixMe[incompatible-type] automated comment - const fromClientValue: string = parseDate(fromValue).momentDate; + const fromClientValue: string = convertLocalToIsoCalendar(fromValue); rangeData.ge = fromClientValue; } if (toValue) { // $FlowFixMe[incompatible-type] automated comment - const toClientValue: string = parseDate(toValue).momentDate; + const toClientValue: string = convertLocalToIsoCalendar(toValue); rangeData.le = toClientValue; } diff --git a/src/core_modules/capture-core/utils/converters/date/stringToMomentDateFormat.js b/src/core_modules/capture-core/utils/converters/date/stringToMomentDateFormat.js index eed1df7957..1e718a5119 100644 --- a/src/core_modules/capture-core/utils/converters/date/stringToMomentDateFormat.js +++ b/src/core_modules/capture-core/utils/converters/date/stringToMomentDateFormat.js @@ -8,9 +8,9 @@ import { systemSettingsStore } from '../../../metaDataMemoryStores'; * @param {*} string - the string instance * @returns {string} */ -export function convertStringToDateFormat(date: string) { +export function convertStringToDateFormat(date: string, format) { if (!date || !date.length) { return ''; } - const dateFormat = systemSettingsStore.get().dateFormat; + const dateFormat = format || systemSettingsStore.get().dateFormat; const formattedDateString = moment(date, dateFormat).format(dateFormat); return formattedDateString; } From 8ce68dd3714d1201b1bacc94055c03e58539de3c Mon Sep 17 00:00:00 2001 From: alaa-yahia Date: Sun, 22 Dec 2024 23:55:07 +0200 Subject: [PATCH 12/42] fix: rename calendarMaxMoment to calendarMax --- .../D2Form/field/configs/dateField/getDateFieldConfig.js | 2 +- .../configs/dateField/getDateFieldConfigForCustomForm.js | 2 +- .../Enrollment/EnrollmentDataEntry.component.js | 4 ++-- .../DateAndTimeFields/DateField/Date.component.js | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/core_modules/capture-core/components/D2Form/field/configs/dateField/getDateFieldConfig.js b/src/core_modules/capture-core/components/D2Form/field/configs/dateField/getDateFieldConfig.js index 7415bd7d5f..355346d87d 100644 --- a/src/core_modules/capture-core/components/D2Form/field/configs/dateField/getDateFieldConfig.js +++ b/src/core_modules/capture-core/components/D2Form/field/configs/dateField/getDateFieldConfig.js @@ -16,7 +16,7 @@ export const getDateFieldConfig = (metaData: DateDataElement, options: Object, q maxWidth: options.formHorizontal ? 150 : 350, calendarWidth: options.formHorizontal ? 250 : 350, popupAnchorPosition: getCalendarAnchorPosition(options.formHorizontal), - calendarMaxMoment: !metaData.allowFutureDate ? convertDateObjectToDateFormatString(moment()) : undefined, + calendarMax: !metaData.allowFutureDate ? convertDateObjectToDateFormatString(moment()) : undefined, }, options, metaData); return createFieldConfig({ diff --git a/src/core_modules/capture-core/components/D2Form/field/configs/dateField/getDateFieldConfigForCustomForm.js b/src/core_modules/capture-core/components/D2Form/field/configs/dateField/getDateFieldConfigForCustomForm.js index 8cb139fb73..efeea1412c 100644 --- a/src/core_modules/capture-core/components/D2Form/field/configs/dateField/getDateFieldConfigForCustomForm.js +++ b/src/core_modules/capture-core/components/D2Form/field/configs/dateField/getDateFieldConfigForCustomForm.js @@ -11,7 +11,7 @@ export const getDateFieldConfigForCustomForm = (metaData: DateDataElement, optio width: 350, maxWidth: 350, calendarWidth: 350, - calendarMaxMoment: !metaData.allowFutureDate ? convertDateObjectToDateFormatString(moment()) : undefined, + calendarMax: !metaData.allowFutureDate ? convertDateObjectToDateFormatString(moment()) : undefined, }, metaData); return createFieldConfig({ diff --git a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentDataEntry.component.js b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentDataEntry.component.js index 76ed900ccc..5ca70266c2 100644 --- a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentDataEntry.component.js +++ b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentDataEntry.component.js @@ -112,7 +112,7 @@ const getEnrollmentDateSettings = () => { required: true, calendarWidth: props.formHorizontal ? 250 : 350, popupAnchorPosition: getCalendarAnchorPosition(props.formHorizontal), - calendarMaxMoment: !props.enrollmentMetadata.allowFutureEnrollmentDate ? convertDateObjectToDateFormatString(moment()) : undefined, + calendarMax: !props.enrollmentMetadata.allowFutureEnrollmentDate ? convertDateObjectToDateFormatString(moment()) : undefined, }), getPropName: () => 'enrolledAt', getValidatorContainers: getEnrollmentDateValidatorContainer, @@ -160,7 +160,7 @@ const getIncidentDateSettings = () => { required: true, calendarWidth: props.formHorizontal ? 250 : 350, popupAnchorPosition: getCalendarAnchorPosition(props.formHorizontal), - calendarMaxMoment: !props.enrollmentMetadata.allowFutureIncidentDate ? + calendarMax: !props.enrollmentMetadata.allowFutureIncidentDate ? convertDateObjectToDateFormatString(moment()) : undefined, }), diff --git a/src/core_modules/capture-ui/DateAndTimeFields/DateField/Date.component.js b/src/core_modules/capture-ui/DateAndTimeFields/DateField/Date.component.js index 95bf289714..c8ba299197 100644 --- a/src/core_modules/capture-ui/DateAndTimeFields/DateField/Date.component.js +++ b/src/core_modules/capture-ui/DateAndTimeFields/DateField/Date.component.js @@ -20,7 +20,7 @@ type Props = { onDateSelectedFromCalendar?: () => void, placeholder?: string, label?: string, - calendarMaxMoment?: any, + calendarMax?: any, innerMessage?: any }; @@ -61,11 +61,11 @@ export class DateField extends React.Component { maxWidth, calendarWidth, inputWidth, - calendarMaxMoment, + calendarMax, value, innerMessage, } = this.props; - +console.log(calendarMax,"calendarMax") const calculatedInputWidth = inputWidth || width; const calculatedCalendarWidth = calendarWidth || width; const calendarType = systemSettingsStore.get().calendar || 'gregory'; @@ -94,7 +94,7 @@ export class DateField extends React.Component { onFocus={this.props.onFocus} disabled={this.props.disabled} {...errorProps} - maxDate={calendarMaxMoment} + maxDate={calendarMax} />
); From b0dc55c8b770280b8ee0165ce64c06235db4fb59 Mon Sep 17 00:00:00 2001 From: alaa-yahia Date: Mon, 23 Dec 2024 00:52:08 +0200 Subject: [PATCH 13/42] fix: remove formating lines --- .../capture-core/converters/clientToForm.js | 7 ++----- .../capture-core/converters/clientToList.js | 7 ++----- .../capture-core/converters/clientToView.js | 7 ++----- .../capture-core/converters/formToClient.js | 8 ++------ .../utils/converters/date/convertLocalToIsoCalendar.js | 10 +++++++++- .../utils/converters/date/stringToMomentDateFormat.js | 3 ++- .../DateAndTimeFields/DateField/Date.component.js | 1 - 7 files changed, 19 insertions(+), 24 deletions(-) diff --git a/src/core_modules/capture-core/converters/clientToForm.js b/src/core_modules/capture-core/converters/clientToForm.js index c0b0d10948..d8cec38fd3 100644 --- a/src/core_modules/capture-core/converters/clientToForm.js +++ b/src/core_modules/capture-core/converters/clientToForm.js @@ -23,17 +23,14 @@ type RangeValue = { } function convertDateForEdit(rawValue: string): string { - const momentDate = moment(rawValue); - const dateString = momentDate.format('YYYY-MM-DD'); - return convertIsoToLocalCalendar(dateString); + return convertIsoToLocalCalendar(rawValue); } function convertDateTimeForEdit(rawValue: string): DateTimeFormValue { const dateTime = moment(rawValue); - const dateString = dateTime.format('YYYY-MM-DD'); const timeString = dateTime.format('HH:mm'); return { - date: convertIsoToLocalCalendar(dateString), + date: convertIsoToLocalCalendar(rawValue), time: timeString, }; } diff --git a/src/core_modules/capture-core/converters/clientToList.js b/src/core_modules/capture-core/converters/clientToList.js index 6ee3cb78a7..f026191a4e 100644 --- a/src/core_modules/capture-core/converters/clientToList.js +++ b/src/core_modules/capture-core/converters/clientToList.js @@ -11,16 +11,13 @@ import { MinimalCoordinates } from '../components/MinimalCoordinates'; import { TooltipOrgUnit } from '../components/Tooltips/TooltipOrgUnit'; function convertDateForListDisplay(rawValue: string): string { - const momentDate = moment(rawValue); - const dateString = momentDate.format('YYYY-MM-DD'); - return convertIsoToLocalCalendar(dateString); + return convertIsoToLocalCalendar(rawValue); } function convertDateTimeForListDisplay(rawValue: string): string { const momentDate = moment(rawValue); - const dateString = momentDate.format('YYYY-MM-DD'); const timeString = momentDate.format('HH:mm'); - const localDate = convertIsoToLocalCalendar(dateString); + const localDate = convertIsoToLocalCalendar(rawValue); return `${localDate} ${timeString}`; } diff --git a/src/core_modules/capture-core/converters/clientToView.js b/src/core_modules/capture-core/converters/clientToView.js index 6f4e2b7e14..0eb7efc21e 100644 --- a/src/core_modules/capture-core/converters/clientToView.js +++ b/src/core_modules/capture-core/converters/clientToView.js @@ -11,16 +11,13 @@ import { TooltipOrgUnit } from '../components/Tooltips/TooltipOrgUnit'; function convertDateForView(rawValue: string): string { - const momentDate = moment(rawValue); - const dateString = momentDate.format('YYYY-MM-DD'); - return convertIsoToLocalCalendar(dateString); + return convertIsoToLocalCalendar(rawValue); } function convertDateTimeForView(rawValue: string): string { const momentDate = moment(rawValue); - const dateString = momentDate.format('YYYY-MM-DD'); const timeString = momentDate.format('HH:mm'); - const localDate = convertIsoToLocalCalendar(dateString); + const localDate = convertIsoToLocalCalendar(rawValue); return `${localDate} ${timeString}`; } diff --git a/src/core_modules/capture-core/converters/formToClient.js b/src/core_modules/capture-core/converters/formToClient.js index 61b37923fd..d0a4460e24 100644 --- a/src/core_modules/capture-core/converters/formToClient.js +++ b/src/core_modules/capture-core/converters/formToClient.js @@ -38,12 +38,8 @@ function convertDateTime(formValue: DateTimeValue): ?string { function convertDate(dateValue: string) { const parsedDate = parseDate(dateValue); - if (!parsedDate.isValid || !parsedDate.momentDate) { - return null; - } - const formattedDate = parsedDate.momentDate.format('YYYY-MM-DD'); - - return convertLocalToIsoCalendar(formattedDate); + // $FlowFixMe[incompatible-use] automated comment + return parsedDate.isValid ? convertLocalToIsoCalendar(parsedDate.momentDate.toISOString()) : null; } function convertTime(timeValue: string) { diff --git a/src/core_modules/capture-core/utils/converters/date/convertLocalToIsoCalendar.js b/src/core_modules/capture-core/utils/converters/date/convertLocalToIsoCalendar.js index b3f323d178..2c2ce3d192 100644 --- a/src/core_modules/capture-core/utils/converters/date/convertLocalToIsoCalendar.js +++ b/src/core_modules/capture-core/utils/converters/date/convertLocalToIsoCalendar.js @@ -16,9 +16,17 @@ export function convertLocalToIsoCalendar(localDate: ?string): string { if (!localDate) { return ''; } + + const momentDate = moment(localDate); + if (!momentDate.isValid()) { + return ''; + } + + const formattedIsoDate = momentDate.format('YYYY-MM-DD'); + const calendar = systemSettingsStore.get().calendar; - const { year, month, day } = convertToIso8601(localDate, calendar); + const { year, month, day } = convertToIso8601(formattedIsoDate, calendar); const dateString = `${padWithZeros(year, 4)}-${padWithZeros(month, 2)}-${padWithZeros(day, 2)}`; const parsedMoment = moment(dateString); diff --git a/src/core_modules/capture-core/utils/converters/date/stringToMomentDateFormat.js b/src/core_modules/capture-core/utils/converters/date/stringToMomentDateFormat.js index 1e718a5119..29c7b2c929 100644 --- a/src/core_modules/capture-core/utils/converters/date/stringToMomentDateFormat.js +++ b/src/core_modules/capture-core/utils/converters/date/stringToMomentDateFormat.js @@ -6,9 +6,10 @@ import { systemSettingsStore } from '../../../metaDataMemoryStores'; * Converts a string date to a string date with default format based on the system date format * @export * @param {*} string - the string instance + * @param {string} [format] - optional date format. If not provided, the function uses system date format * @returns {string} */ -export function convertStringToDateFormat(date: string, format) { +export function convertStringToDateFormat(date: string, format?: string) { if (!date || !date.length) { return ''; } const dateFormat = format || systemSettingsStore.get().dateFormat; const formattedDateString = moment(date, dateFormat).format(dateFormat); diff --git a/src/core_modules/capture-ui/DateAndTimeFields/DateField/Date.component.js b/src/core_modules/capture-ui/DateAndTimeFields/DateField/Date.component.js index c8ba299197..1c4b2ee4b0 100644 --- a/src/core_modules/capture-ui/DateAndTimeFields/DateField/Date.component.js +++ b/src/core_modules/capture-ui/DateAndTimeFields/DateField/Date.component.js @@ -65,7 +65,6 @@ export class DateField extends React.Component { value, innerMessage, } = this.props; -console.log(calendarMax,"calendarMax") const calculatedInputWidth = inputWidth || width; const calculatedCalendarWidth = calendarWidth || width; const calendarType = systemSettingsStore.get().calendar || 'gregory'; From 2cc70be916491eb0988cd81d2110ec77f299a939 Mon Sep 17 00:00:00 2001 From: alaa-yahia Date: Mon, 23 Dec 2024 02:57:17 +0200 Subject: [PATCH 14/42] fix: day value not giving correct result in age field --- .../New/Fields/AgeField/AgeField.component.js | 5 -- .../date/convertStringToTemporal.js | 39 +++++++++ .../date/convertTemporalToString.js | 29 +++++++ .../utils/converters/date/index.js | 2 + .../capture-ui/AgeField/AgeField.component.js | 85 +++++++------------ 5 files changed, 102 insertions(+), 58 deletions(-) create mode 100644 src/core_modules/capture-core/utils/converters/date/convertStringToTemporal.js create mode 100644 src/core_modules/capture-core/utils/converters/date/convertTemporalToString.js diff --git a/src/core_modules/capture-core/components/FormFields/New/Fields/AgeField/AgeField.component.js b/src/core_modules/capture-core/components/FormFields/New/Fields/AgeField/AgeField.component.js index 2306b0854d..c160c34475 100644 --- a/src/core_modules/capture-core/components/FormFields/New/Fields/AgeField/AgeField.component.js +++ b/src/core_modules/capture-core/components/FormFields/New/Fields/AgeField/AgeField.component.js @@ -2,8 +2,6 @@ import * as React from 'react'; import { withStyles, withTheme } from '@material-ui/core/styles'; import { AgeField as UIAgeField } from 'capture-ui'; -import moment from 'moment'; -import { parseDate, convertMomentToDateFormatString } from '../../../../../utils/converters/date'; import { systemSettingsStore } from '../../../../../metaDataMemoryStores'; const getStyles = (theme: Theme) => ({ @@ -50,9 +48,6 @@ const AgeFieldPlain = (props: Props) => { return ( // $FlowFixMe[cannot-spread-inexact] automated comment diff --git a/src/core_modules/capture-core/utils/converters/date/convertStringToTemporal.js b/src/core_modules/capture-core/utils/converters/date/convertStringToTemporal.js new file mode 100644 index 0000000000..5eee8cc7a1 --- /dev/null +++ b/src/core_modules/capture-core/utils/converters/date/convertStringToTemporal.js @@ -0,0 +1,39 @@ +// @flow +import { Temporal } from '@js-temporal/polyfill'; +import { systemSettingsStore } from '../../../metaDataMemoryStores'; + +/** + * Converts a date string into a Temporal.PlainDate object using the system set calendar + * @export + * @param {*} string - dateString + * @returns {(Temporal.PlainDate | null)} + */ + +export function convertStringToTemporal(dateString: string): Temporal.PlainDate | null { + if (!dateString) { + return null; + } + try { + const dateWithHyphen = dateString.replace(/[\/\.]/g, '-'); + + const calendar = systemSettingsStore.get().calendar; + const dateFormat = systemSettingsStore.get().dateFormat; + + let year; let month; let day; + + if (dateFormat === 'YYYY-MM-DD') { + [year, month, day] = dateWithHyphen.split('-').map(Number); + } + if (dateFormat === 'DD-MM-YYYY') { + [day, month, year] = dateWithHyphen.split('-').map(Number); + } + return Temporal.PlainDate.from({ + year, + month, + day, + calendar, + }); + } catch (error) { + return ''; + } +} diff --git a/src/core_modules/capture-core/utils/converters/date/convertTemporalToString.js b/src/core_modules/capture-core/utils/converters/date/convertTemporalToString.js new file mode 100644 index 0000000000..a295b7984d --- /dev/null +++ b/src/core_modules/capture-core/utils/converters/date/convertTemporalToString.js @@ -0,0 +1,29 @@ +// @flow +import { Temporal } from '@js-temporal/polyfill'; +import { padWithZeros } from './padWithZeros'; +import { systemSettingsStore } from '../../../../capture-core/metaDataMemoryStores'; + +/** + * Converts a Temporal.PlainDate to a formatted date string (YYYY-MM-DD || DD-MM-YYYY) + * @param {Temporal.PlainDate} temporalDate - The Temporal date to convert + * @returns {string} Formatted date string, or empty string if invalid + */ + +export function convertTemporalToString(temporalDate: Temporal.PlainDate | null): string { + if (!temporalDate) { + return ''; + } + const dateFormat = systemSettingsStore.get().dateFormat; + + try { + const year = temporalDate.year; + const month = temporalDate.month; + const day = temporalDate.day; + + return dateFormat === 'YYYY-MM-DD' ? + `${padWithZeros(year, 4)}-${padWithZeros(month, 2)}-${padWithZeros(day, 2)}` : + `${padWithZeros(day, 2)}-${padWithZeros(month, 2)}-${padWithZeros(year, 4)}`; + } catch (error) { + return ''; + } +} diff --git a/src/core_modules/capture-core/utils/converters/date/index.js b/src/core_modules/capture-core/utils/converters/date/index.js index 511298b89d..11f200fb34 100644 --- a/src/core_modules/capture-core/utils/converters/date/index.js +++ b/src/core_modules/capture-core/utils/converters/date/index.js @@ -6,3 +6,5 @@ export { convertStringToDateFormat } from './stringToMomentDateFormat'; export { padWithZeros } from './padWithZeros'; export { convertIsoToLocalCalendar } from './convertIsoToLocalCalendar'; export { convertLocalToIsoCalendar } from './convertLocalToIsoCalendar'; +export { convertStringToTemporal } from './convertStringToTemporal'; +export { convertTemporalToString } from './convertTemporalToString'; diff --git a/src/core_modules/capture-ui/AgeField/AgeField.component.js b/src/core_modules/capture-ui/AgeField/AgeField.component.js index 78abcf4a21..b04b755613 100644 --- a/src/core_modules/capture-ui/AgeField/AgeField.component.js +++ b/src/core_modules/capture-ui/AgeField/AgeField.component.js @@ -1,7 +1,7 @@ // @flow import React, { Component } from 'react'; +import { Temporal } from '@js-temporal/polyfill'; import { isValidPositiveInteger } from 'capture-core-utils/validators/form'; -import { convertDateObjectToDateFormatString } from 'capture-core/utils/converters/date'; import { systemSettingsStore } from 'capture-core/metaDataMemoryStores'; import i18n from '@dhis2/d2-i18n'; import classNames from 'classnames'; @@ -12,6 +12,7 @@ import { AgeDateInput } from '../internal/AgeInput/AgeDateInput.component'; import defaultClasses from './ageField.module.css'; import { orientations } from '../constants/orientations.const'; import { withInternalChangeHandler } from '../HOC/withInternalChangeHandler'; +import { convertStringToTemporal, convertTemporalToString } from '../../capture-core/utils/converters/date'; type AgeValues = { date?: ?string, @@ -47,9 +48,6 @@ type Props = { inputMessageClasses: ?InputMessageClasses, inFocus?: ?boolean, shrinkDisabled?: ?boolean, - onParseDate: DateParser, - onGetFormattedDateStringFromMoment: DateStringFromMomentFormatter, - moment: any, dateCalendarTheme: Object, dateCalendarWidth?: ?any, datePopupAnchorPosition?: ?string, @@ -59,38 +57,26 @@ type Props = { datePlaceholder?: ?string, disabled?: ?boolean, }; -function getCalculatedValues( - dateValue: ?string, - onParseDate: DateParser, - onGetFormattedDateStringFromMoment: DateStringFromMomentFormatter, - moment: any, -): AgeValues { - const parseData = dateValue && onParseDate(dateValue); - if (!parseData || !parseData.isValid) { - return { - date: dateValue, - years: '', - months: '', - days: '', - }; - } - const dateFormat = systemSettingsStore.get().dateFormat; - const now = moment(convertDateObjectToDateFormatString(moment()), dateFormat); - const age = moment(parseData.momentDate); - const years = now.diff(age, 'years'); - age.add(years, 'years'); +function getCalculatedValues(dateValue: ?string): AgeValues { + const calendar = systemSettingsStore.get().calendar; + + const now = Temporal.Now.plainDateISO().withCalendar(calendar); - const months = now.diff(age, 'months'); - age.add(months, 'months'); + const age = convertStringToTemporal(dateValue); - const days = now.diff(age, 'days'); + const diff = now.since(age, { + largestUnit: 'years', + smallestUnit: 'days', + }); + + const date = convertTemporalToString(age); return { - date: onGetFormattedDateStringFromMoment(parseData.momentDate), - years: years.toString(), - months: months.toString(), - days: days.toString(), + date, + years: diff.years.toString(), + months: diff.months.toString(), + days: diff.days.toString(), }; } @@ -124,7 +110,7 @@ class D2AgeFieldPlain extends Component { } handleNumberBlur = (values: AgeValues) => { - const { onParseDate, onGetFormattedDateStringFromMoment, onRemoveFocus, moment } = this.props; + const { onRemoveFocus } = this.props; onRemoveFocus && onRemoveFocus(); if (D2AgeFieldPlain.isEmptyNumbers(values)) { @@ -136,28 +122,25 @@ class D2AgeFieldPlain extends Component { this.props.onBlur({ ...values, date: '' }); return; } - const dateFormat = systemSettingsStore.get().dateFormat; - const momentDate = moment(convertDateObjectToDateFormatString(moment(undefined, undefined, true)), dateFormat); - momentDate.subtract(D2AgeFieldPlain.getNumberOrZero(values.years), 'years'); - momentDate.subtract(D2AgeFieldPlain.getNumberOrZero(values.months), 'months'); - momentDate.subtract(D2AgeFieldPlain.getNumberOrZero(values.days), 'days'); - const calculatedValues = getCalculatedValues( - onGetFormattedDateStringFromMoment(momentDate), - onParseDate, - onGetFormattedDateStringFromMoment, - moment, - ); + + const calendar = systemSettingsStore.get().calendar; + + const now = Temporal.Now.plainDateISO().withCalendar(calendar); + + const calculatedDate = now.subtract({ + years: D2AgeFieldPlain.getNumberOrZero(values.years), + months: D2AgeFieldPlain.getNumberOrZero(values.months), + days: D2AgeFieldPlain.getNumberOrZero(values.days), + }); + + const calculatedValues = getCalculatedValues(convertTemporalToString(calculatedDate)); this.props.onBlur(calculatedValues); } handleDateBlur = (date: ?string, options: ?ValidationOptions) => { - const { onParseDate, onGetFormattedDateStringFromMoment, onRemoveFocus, moment } = this.props; + const { onRemoveFocus } = this.props; onRemoveFocus && onRemoveFocus(); - const calculatedValues = date ? getCalculatedValues( - date, - onParseDate, - onGetFormattedDateStringFromMoment, - moment) : null; + const calculatedValues = date ? getCalculatedValues(date) : null; this.props.onBlur(calculatedValues, options); } @@ -184,8 +167,6 @@ class D2AgeFieldPlain extends Component { datePopupAnchorPosition, dateCalendarTheme, dateCalendarLocale, - moment, - onParseDate, ...passOnProps } = this.props; return (
@@ -211,8 +192,6 @@ class D2AgeFieldPlain extends Component { shrinkDisabled, dateCalendarWidth, datePlaceholder, - moment, - onParseDate, ...passOnProps } = this.props; From 383c66fc571b6b4220fc0abf43f748744b62191f Mon Sep 17 00:00:00 2001 From: alaa-yahia Date: Mon, 23 Dec 2024 03:13:11 +0200 Subject: [PATCH 15/42] fix: flow errors --- .../utils/converters/date/convertStringToTemporal.js | 10 ++++++++-- .../utils/converters/date/convertTemporalToString.js | 9 +++++++-- .../capture-ui/AgeField/AgeField.component.js | 4 ---- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/core_modules/capture-core/utils/converters/date/convertStringToTemporal.js b/src/core_modules/capture-core/utils/converters/date/convertStringToTemporal.js index 5eee8cc7a1..a50c92be11 100644 --- a/src/core_modules/capture-core/utils/converters/date/convertStringToTemporal.js +++ b/src/core_modules/capture-core/utils/converters/date/convertStringToTemporal.js @@ -9,7 +9,13 @@ import { systemSettingsStore } from '../../../metaDataMemoryStores'; * @returns {(Temporal.PlainDate | null)} */ -export function convertStringToTemporal(dateString: string): Temporal.PlainDate | null { +type PlainDate = { + year: number, + month: number, + day: number +}; + +export function convertStringToTemporal(dateString: ?string): PlainDate | null { if (!dateString) { return null; } @@ -34,6 +40,6 @@ export function convertStringToTemporal(dateString: string): Temporal.PlainDate calendar, }); } catch (error) { - return ''; + return null; } } diff --git a/src/core_modules/capture-core/utils/converters/date/convertTemporalToString.js b/src/core_modules/capture-core/utils/converters/date/convertTemporalToString.js index a295b7984d..e413468c66 100644 --- a/src/core_modules/capture-core/utils/converters/date/convertTemporalToString.js +++ b/src/core_modules/capture-core/utils/converters/date/convertTemporalToString.js @@ -1,5 +1,4 @@ // @flow -import { Temporal } from '@js-temporal/polyfill'; import { padWithZeros } from './padWithZeros'; import { systemSettingsStore } from '../../../../capture-core/metaDataMemoryStores'; @@ -9,7 +8,13 @@ import { systemSettingsStore } from '../../../../capture-core/metaDataMemoryStor * @returns {string} Formatted date string, or empty string if invalid */ -export function convertTemporalToString(temporalDate: Temporal.PlainDate | null): string { +type PlainDate = { + year: number, + month: number, + day: number +}; + +export function convertTemporalToString(temporalDate: PlainDate | null): string { if (!temporalDate) { return ''; } diff --git a/src/core_modules/capture-ui/AgeField/AgeField.component.js b/src/core_modules/capture-ui/AgeField/AgeField.component.js index b04b755613..fccf96d0e9 100644 --- a/src/core_modules/capture-ui/AgeField/AgeField.component.js +++ b/src/core_modules/capture-ui/AgeField/AgeField.component.js @@ -28,10 +28,6 @@ type InputMessageClasses = { validating?: ?string, } -type DateParser = (value: string) => { isValid: boolean, momentDate: any }; - -type DateStringFromMomentFormatter = (momentValue: Object) => string; - type ValidationOptions = { error?: ?string, errorCode?: ?string, From 013db2ccccfea8aae6363a471820c3bec70193d3 Mon Sep 17 00:00:00 2001 From: alaa-yahia Date: Mon, 6 Jan 2025 12:37:16 +0200 Subject: [PATCH 16/42] fix: display local time in tooltips --- package.json | 4 +- .../WidgetEnrollment.component.js | 3 +- .../WidgetNote/NoteSection/NoteSection.js | 41 +++++++++++-------- .../StageOverview/StageOverview.component.js | 5 ++- 4 files changed, 31 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index 1178b6e2e1..e4caffa666 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,7 @@ "packages/rules-engine" ], "dependencies": { - "@dhis2/rules-engine-javascript": "101.19.1", - "@dhis2-ui/calendar": "^10.0.3", + "@dhis2/rules-engine-javascript": "101.20.3", "@dhis2/app-runtime": "^3.9.3", "@dhis2/d2-i18n": "^1.1.0", "@dhis2/d2-icons": "^1.0.1", @@ -20,6 +19,7 @@ "@dhis2/d2-ui-rich-text": "^7.4.0", "@dhis2/d2-ui-sharing-dialog": "^7.3.3", "@dhis2/ui": "^9.10.1", + "@dhis2-ui/calendar": "^10.0.3", "@joakim_sm/react-infinite-calendar": "^2.4.2", "@material-ui/core": "3.9.4", "@material-ui/icons": "3", diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.js b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.js index 1cd597f02d..66b9321c2a 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.js +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.js @@ -74,6 +74,7 @@ export const WidgetEnrollmentPlain = ({ }: PlainProps) => { const [open, setOpenStatus] = useState(true); const { fromServerDate } = useTimeZoneConversion(); + const localDateTime: string = (convertValue(enrollment?.updatedAt, dataElementTypes.DATETIME): any); const geometryType = getGeometryType(enrollment?.geometry?.type); const { displayName: orgUnitName, ancestors } = useOrgUnitNameWithAncestors(enrollment?.orgUnit); const { displayName: ownerOrgUnitName, ancestors: ownerAncestors } = useOrgUnitNameWithAncestors(ownerOrgUnit?.id); @@ -157,7 +158,7 @@ export const WidgetEnrollmentPlain = ({ {i18n.t('Last updated')} - + {moment(fromServerDate(enrollment.updatedAt)).fromNow()}
diff --git a/src/core_modules/capture-core/components/WidgetNote/NoteSection/NoteSection.js b/src/core_modules/capture-core/components/WidgetNote/NoteSection/NoteSection.js index f908d1d3ed..825d27d527 100644 --- a/src/core_modules/capture-core/components/WidgetNote/NoteSection/NoteSection.js +++ b/src/core_modules/capture-core/components/WidgetNote/NoteSection/NoteSection.js @@ -9,6 +9,8 @@ import { colors, spacersNum, Button, Tooltip } from '@dhis2/ui'; import moment from 'moment'; import { useTimeZoneConversion } from '@dhis2/app-runtime'; import { TextField } from '../../FormFields/New'; +import { convertValue as convertValueClientToView } from '../../../converters/clientToView'; +import { dataElementTypes } from '../../../metaData'; const FocusTextField = withFocusSaver()(TextField); @@ -97,26 +99,29 @@ const NoteSectionPlain = ({ setEditing(false); }, [handleAddNote, newNoteValue]); - const NoteItem = ({ value, storedAt, createdBy }) => ( -
- {/* TODO: add avatar */} -
-
- {createdBy && - {createdBy.firstName} {' '} {createdBy.surname} - } - - - {moment(fromServerDate(storedAt)).fromNow()} - - -
-
- {value} + const NoteItem = ({ value, storedAt, createdBy }) => { + const localDateTime: string = (convertValueClientToView(storedAt, dataElementTypes.DATETIME): any); + return ( +
+ {/* TODO: add avatar */} +
+
+ {createdBy && + {createdBy.firstName} {' '} {createdBy.surname} + } + + + {moment(fromServerDate(storedAt)).fromNow()} + + +
+
+ {value} +
-
- ); + ); + }; return ( diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.js index 907e049840..5c72a76670 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.js +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.js @@ -12,6 +12,8 @@ import { statusTypes } from 'capture-core/events/statusTypes'; import { NonBundledDhis2Icon } from '../../../../NonBundledDhis2Icon'; import type { Props } from './stageOverview.types'; import { isEventOverdue } from '../StageDetail/hooks/helpers'; +import { convertValue as convertValueClientToView } from '../../../../../converters/clientToView'; +import { dataElementTypes } from '../../../../../metaData'; const styles = { container: { @@ -71,11 +73,12 @@ const getLastUpdatedAt = (events, fromServerDate) => { if (lastEventUpdated) { const { updatedAt } = lastEventUpdated; + const localDateTime: string = (convertValueClientToView(updatedAt, dataElementTypes.DATETIME): any); return lastEventUpdated?.updatedAt && moment(updatedAt).isValid() ? ( <> {i18n.t('Last updated')}  - + {moment(fromServerDate(updatedAt)).fromNow()} From 8dd111092a4f17190c4c1cd9de8b2d00cca89989 Mon Sep 17 00:00:00 2001 From: alaa-yahia Date: Mon, 6 Jan 2025 15:22:02 +0200 Subject: [PATCH 17/42] fix: date is not valid error not displayed --- .../capture-ui/AgeField/AgeField.component.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/core_modules/capture-ui/AgeField/AgeField.component.js b/src/core_modules/capture-ui/AgeField/AgeField.component.js index fccf96d0e9..65085efe6b 100644 --- a/src/core_modules/capture-ui/AgeField/AgeField.component.js +++ b/src/core_modules/capture-ui/AgeField/AgeField.component.js @@ -7,6 +7,7 @@ import i18n from '@dhis2/d2-i18n'; import classNames from 'classnames'; import { IconButton } from 'capture-ui'; import { IconCross24 } from '@dhis2/ui'; +import { parseDate } from 'capture-core/utils/converters/date'; import { AgeNumberInput } from '../internal/AgeInput/AgeNumberInput.component'; import { AgeDateInput } from '../internal/AgeInput/AgeDateInput.component'; import defaultClasses from './ageField.module.css'; @@ -55,6 +56,16 @@ type Props = { }; function getCalculatedValues(dateValue: ?string): AgeValues { + const parseData = dateValue && parseDate(dateValue); + if (!parseData || !parseData.isValid) { + return { + date: dateValue, + years: '', + months: '', + days: '', + }; + } + const calendar = systemSettingsStore.get().calendar; const now = Temporal.Now.plainDateISO().withCalendar(calendar); From 209860c630fce98c981aff7413513b7bda13676d Mon Sep 17 00:00:00 2001 From: alaa-yahia Date: Thu, 9 Jan 2025 13:42:31 +0200 Subject: [PATCH 18/42] fix: remove app specific objects --- .../capture-core-utils/date/index.js | 3 + .../date/padWithZeros.js | 0 .../date/stringToTemporal.js | 45 +++++++++++++++ .../date/temporalToString.js | 33 +++++++++++ .../configs/ageField/getAgeFieldConfig.js | 3 + .../configs/dateField/getDateFieldConfig.js | 3 + .../date/convertIsoToLocalCalendar.js | 2 +- .../date/convertLocalToIsoCalendar.js | 2 +- .../date/convertStringToTemporal.js | 28 ++-------- .../date/convertTemporalToString.js | 15 +---- .../utils/converters/date/index.js | 1 - .../capture-ui/AgeField/AgeField.component.js | 55 ++++++++++--------- .../DateField/Date.component.js | 13 +++-- 13 files changed, 131 insertions(+), 72 deletions(-) rename src/core_modules/{capture-core/utils/converters => capture-core-utils}/date/padWithZeros.js (100%) create mode 100644 src/core_modules/capture-core-utils/date/stringToTemporal.js create mode 100644 src/core_modules/capture-core-utils/date/temporalToString.js diff --git a/src/core_modules/capture-core-utils/date/index.js b/src/core_modules/capture-core-utils/date/index.js index 8af83e8b8d..f29e3a8760 100644 --- a/src/core_modules/capture-core-utils/date/index.js +++ b/src/core_modules/capture-core-utils/date/index.js @@ -1,2 +1,5 @@ // @flow export { getFormattedStringFromMomentUsingEuropeanGlyphs } from './date.utils'; +export { padWithZeros } from './padWithZeros'; +export { temporalToString } from './temporalToString'; +export { stringToTemporal } from './stringToTemporal'; diff --git a/src/core_modules/capture-core/utils/converters/date/padWithZeros.js b/src/core_modules/capture-core-utils/date/padWithZeros.js similarity index 100% rename from src/core_modules/capture-core/utils/converters/date/padWithZeros.js rename to src/core_modules/capture-core-utils/date/padWithZeros.js diff --git a/src/core_modules/capture-core-utils/date/stringToTemporal.js b/src/core_modules/capture-core-utils/date/stringToTemporal.js new file mode 100644 index 0000000000..8063305d0f --- /dev/null +++ b/src/core_modules/capture-core-utils/date/stringToTemporal.js @@ -0,0 +1,45 @@ +// @flow +import { Temporal } from '@js-temporal/polyfill'; + +/** + * Converts a date string into a Temporal.PlainDate object using the specified calendar + * @export + * @param {?string} dateString - The date string to convert + * @param {?string} calendarType - The calendar type to use + * @param {?string} dateFormat - The current system date format ('YYYY-MM-DD' or 'DD-MM-YYYY') + * @returns {(Temporal.PlainDate | null)} + */ + +type PlainDate = { + year: number, + month: number, + day: number +}; + +export function stringToTemporal(dateString: ?string, + calendarType: ?string, + dateFormat: ?string): PlainDate | null { + if (!dateString) { + return null; + } + try { + const dateWithHyphen = dateString.replace(/[\/\.]/g, '-'); + + let year; let month; let day; + + if (dateFormat === 'YYYY-MM-DD') { + [year, month, day] = dateWithHyphen.split('-').map(Number); + } + if (dateFormat === 'DD-MM-YYYY') { + [day, month, year] = dateWithHyphen.split('-').map(Number); + } + return Temporal.PlainDate.from({ + year, + month, + day, + calendarType, + }); + } catch (error) { + return null; + } +} diff --git a/src/core_modules/capture-core-utils/date/temporalToString.js b/src/core_modules/capture-core-utils/date/temporalToString.js new file mode 100644 index 0000000000..72f650b149 --- /dev/null +++ b/src/core_modules/capture-core-utils/date/temporalToString.js @@ -0,0 +1,33 @@ +// @flow +import { padWithZeros } from './padWithZeros'; + +/** + * Converts a Temporal.PlainDate to a formatted date string (YYYY-MM-DD or DD-MM-YYYY) + * @param {Temporal.PlainDate | null} temporalDate - The Temporal date to convert + * @param {?string} dateFormat - The current system date format ('YYYY-MM-DD' or 'DD-MM-YYYY') + * @returns {string} Formatted date string, or empty string if invalid + */ + +type PlainDate = { + year: number, + month: number, + day: number +}; + +export function temporalToString(temporalDate: PlainDate | null, dateFormat: ?string): string { + if (!temporalDate) { + return ''; + } + + try { + const year = temporalDate.year; + const month = temporalDate.month; + const day = temporalDate.day; + + return dateFormat === 'YYYY-MM-DD' ? + `${padWithZeros(year, 4)}-${padWithZeros(month, 2)}-${padWithZeros(day, 2)}` : + `${padWithZeros(day, 2)}-${padWithZeros(month, 2)}-${padWithZeros(year, 4)}`; + } catch (error) { + return ''; + } +} diff --git a/src/core_modules/capture-core/components/D2Form/field/configs/ageField/getAgeFieldConfig.js b/src/core_modules/capture-core/components/D2Form/field/configs/ageField/getAgeFieldConfig.js index 4c55b92527..e9ffbf41b6 100644 --- a/src/core_modules/capture-core/components/D2Form/field/configs/ageField/getAgeFieldConfig.js +++ b/src/core_modules/capture-core/components/D2Form/field/configs/ageField/getAgeFieldConfig.js @@ -2,6 +2,7 @@ import { orientations } from '../../../../FormFields/New'; import { createFieldConfig, createProps } from '../base/configBaseDefaultForm'; import { AgeFieldForForm } from '../../Components'; +import { systemSettingsStore } from '../../../../../metaDataMemoryStores'; import { type DataElement } from '../../../../../metaData'; import type { QuerySingleResource } from '../../../../../utils/api/api.types'; @@ -15,6 +16,8 @@ export const getAgeFieldConfig = (metaData: DataElement, options: Object, queryS shrinkDisabled: options.formHorizontal, dateCalendarWidth: options.formHorizontal ? 250 : 350, datePopupAnchorPosition: getCalendarAnchorPosition(options.formHorizontal), + calendarType: systemSettingsStore.get().calendar, + dateFormat: systemSettingsStore.get().dateFormat, }, options, metaData); return createFieldConfig({ diff --git a/src/core_modules/capture-core/components/D2Form/field/configs/dateField/getDateFieldConfig.js b/src/core_modules/capture-core/components/D2Form/field/configs/dateField/getDateFieldConfig.js index 355346d87d..4af9e783c5 100644 --- a/src/core_modules/capture-core/components/D2Form/field/configs/dateField/getDateFieldConfig.js +++ b/src/core_modules/capture-core/components/D2Form/field/configs/dateField/getDateFieldConfig.js @@ -3,6 +3,7 @@ import moment from 'moment'; import { createFieldConfig, createProps } from '../base/configBaseDefaultForm'; import { DateFieldForForm } from '../../Components'; import { convertDateObjectToDateFormatString } from '../../../../../../capture-core/utils/converters/date'; +import { systemSettingsStore } from '../../../../../metaDataMemoryStores'; import type { DateDataElement } from '../../../../../metaData'; import type { QuerySingleResource } from '../../../../../utils/api/api.types'; @@ -17,6 +18,8 @@ export const getDateFieldConfig = (metaData: DateDataElement, options: Object, q calendarWidth: options.formHorizontal ? 250 : 350, popupAnchorPosition: getCalendarAnchorPosition(options.formHorizontal), calendarMax: !metaData.allowFutureDate ? convertDateObjectToDateFormatString(moment()) : undefined, + calendarType: systemSettingsStore.get().calendar, + dateFormat: systemSettingsStore.get().dateFormat, }, options, metaData); return createFieldConfig({ diff --git a/src/core_modules/capture-core/utils/converters/date/convertIsoToLocalCalendar.js b/src/core_modules/capture-core/utils/converters/date/convertIsoToLocalCalendar.js index 442ad4a60c..fa09f77c53 100644 --- a/src/core_modules/capture-core/utils/converters/date/convertIsoToLocalCalendar.js +++ b/src/core_modules/capture-core/utils/converters/date/convertIsoToLocalCalendar.js @@ -4,7 +4,7 @@ import { convertFromIso8601, } from '@dhis2/multi-calendar-dates'; import { systemSettingsStore } from '../../../../capture-core/metaDataMemoryStores'; -import { padWithZeros } from './padWithZeros'; +import { padWithZeros } from '../../../../capture-core-utils/date'; /** * Converts a date from ISO calendar to local calendar diff --git a/src/core_modules/capture-core/utils/converters/date/convertLocalToIsoCalendar.js b/src/core_modules/capture-core/utils/converters/date/convertLocalToIsoCalendar.js index 2c2ce3d192..2323f01370 100644 --- a/src/core_modules/capture-core/utils/converters/date/convertLocalToIsoCalendar.js +++ b/src/core_modules/capture-core/utils/converters/date/convertLocalToIsoCalendar.js @@ -4,7 +4,7 @@ import { convertToIso8601, } from '@dhis2/multi-calendar-dates'; import { systemSettingsStore } from '../../../../capture-core/metaDataMemoryStores'; -import { padWithZeros } from './padWithZeros'; +import { padWithZeros } from '../../../../capture-core-utils/date'; /** * Converts a date from local calendar to ISO calendar diff --git a/src/core_modules/capture-core/utils/converters/date/convertStringToTemporal.js b/src/core_modules/capture-core/utils/converters/date/convertStringToTemporal.js index a50c92be11..57c0dec668 100644 --- a/src/core_modules/capture-core/utils/converters/date/convertStringToTemporal.js +++ b/src/core_modules/capture-core/utils/converters/date/convertStringToTemporal.js @@ -1,6 +1,6 @@ // @flow -import { Temporal } from '@js-temporal/polyfill'; import { systemSettingsStore } from '../../../metaDataMemoryStores'; +import { stringToTemporal } from '../../../../capture-core-utils/date'; /** * Converts a date string into a Temporal.PlainDate object using the system set calendar @@ -19,27 +19,7 @@ export function convertStringToTemporal(dateString: ?string): PlainDate | null { if (!dateString) { return null; } - try { - const dateWithHyphen = dateString.replace(/[\/\.]/g, '-'); - - const calendar = systemSettingsStore.get().calendar; - const dateFormat = systemSettingsStore.get().dateFormat; - - let year; let month; let day; - - if (dateFormat === 'YYYY-MM-DD') { - [year, month, day] = dateWithHyphen.split('-').map(Number); - } - if (dateFormat === 'DD-MM-YYYY') { - [day, month, year] = dateWithHyphen.split('-').map(Number); - } - return Temporal.PlainDate.from({ - year, - month, - day, - calendar, - }); - } catch (error) { - return null; - } + const calendar = systemSettingsStore.get().calendar; + const dateFormat = systemSettingsStore.get().dateFormat; + return stringToTemporal(dateString, calendar, dateFormat); } diff --git a/src/core_modules/capture-core/utils/converters/date/convertTemporalToString.js b/src/core_modules/capture-core/utils/converters/date/convertTemporalToString.js index e413468c66..9ba772b23a 100644 --- a/src/core_modules/capture-core/utils/converters/date/convertTemporalToString.js +++ b/src/core_modules/capture-core/utils/converters/date/convertTemporalToString.js @@ -1,6 +1,6 @@ // @flow -import { padWithZeros } from './padWithZeros'; import { systemSettingsStore } from '../../../../capture-core/metaDataMemoryStores'; +import { temporalToString } from '../../../../capture-core-utils/date'; /** * Converts a Temporal.PlainDate to a formatted date string (YYYY-MM-DD || DD-MM-YYYY) @@ -19,16 +19,5 @@ export function convertTemporalToString(temporalDate: PlainDate | null): string return ''; } const dateFormat = systemSettingsStore.get().dateFormat; - - try { - const year = temporalDate.year; - const month = temporalDate.month; - const day = temporalDate.day; - - return dateFormat === 'YYYY-MM-DD' ? - `${padWithZeros(year, 4)}-${padWithZeros(month, 2)}-${padWithZeros(day, 2)}` : - `${padWithZeros(day, 2)}-${padWithZeros(month, 2)}-${padWithZeros(year, 4)}`; - } catch (error) { - return ''; - } + return temporalToString(temporalDate, dateFormat); } diff --git a/src/core_modules/capture-core/utils/converters/date/index.js b/src/core_modules/capture-core/utils/converters/date/index.js index 11f200fb34..8f878a421d 100644 --- a/src/core_modules/capture-core/utils/converters/date/index.js +++ b/src/core_modules/capture-core/utils/converters/date/index.js @@ -3,7 +3,6 @@ export { parseDate } from './parser'; export { convertDateObjectToDateFormatString } from './dateObjectToDateFormatString'; export { convertMomentToDateFormatString } from './momentToDateFormatString'; export { convertStringToDateFormat } from './stringToMomentDateFormat'; -export { padWithZeros } from './padWithZeros'; export { convertIsoToLocalCalendar } from './convertIsoToLocalCalendar'; export { convertLocalToIsoCalendar } from './convertLocalToIsoCalendar'; export { convertStringToTemporal } from './convertStringToTemporal'; diff --git a/src/core_modules/capture-ui/AgeField/AgeField.component.js b/src/core_modules/capture-ui/AgeField/AgeField.component.js index 65085efe6b..f2b96b19a5 100644 --- a/src/core_modules/capture-ui/AgeField/AgeField.component.js +++ b/src/core_modules/capture-ui/AgeField/AgeField.component.js @@ -2,18 +2,16 @@ import React, { Component } from 'react'; import { Temporal } from '@js-temporal/polyfill'; import { isValidPositiveInteger } from 'capture-core-utils/validators/form'; -import { systemSettingsStore } from 'capture-core/metaDataMemoryStores'; import i18n from '@dhis2/d2-i18n'; import classNames from 'classnames'; import { IconButton } from 'capture-ui'; import { IconCross24 } from '@dhis2/ui'; -import { parseDate } from 'capture-core/utils/converters/date'; import { AgeNumberInput } from '../internal/AgeInput/AgeNumberInput.component'; import { AgeDateInput } from '../internal/AgeInput/AgeDateInput.component'; import defaultClasses from './ageField.module.css'; import { orientations } from '../constants/orientations.const'; import { withInternalChangeHandler } from '../HOC/withInternalChangeHandler'; -import { convertStringToTemporal, convertTemporalToString } from '../../capture-core/utils/converters/date'; +import { stringToTemporal, temporalToString } from '../../capture-core-utils/date'; type AgeValues = { date?: ?string, @@ -53,31 +51,21 @@ type Props = { dateCalendarOnConvertValueOut: (value: string) => string, datePlaceholder?: ?string, disabled?: ?boolean, + dateFormat: ?string, + calendarType: ?string, }; -function getCalculatedValues(dateValue: ?string): AgeValues { - const parseData = dateValue && parseDate(dateValue); - if (!parseData || !parseData.isValid) { - return { - date: dateValue, - years: '', - months: '', - days: '', - }; - } - - const calendar = systemSettingsStore.get().calendar; - - const now = Temporal.Now.plainDateISO().withCalendar(calendar); +function getCalculatedValues(dateValue: ?string, calendarType: ?string, dateFormat: ?string): AgeValues { + const now = Temporal.Now.plainDateISO().withCalendar(calendarType); - const age = convertStringToTemporal(dateValue); + const age = stringToTemporal(dateValue, calendarType, dateFormat); const diff = now.since(age, { largestUnit: 'years', smallestUnit: 'days', }); - const date = convertTemporalToString(age); + const date = temporalToString(age, dateFormat); return { date, @@ -117,7 +105,7 @@ class D2AgeFieldPlain extends Component { } handleNumberBlur = (values: AgeValues) => { - const { onRemoveFocus } = this.props; + const { onRemoveFocus, calendarType = 'gregory', dateFormat = 'YYYY-MM-DD' } = this.props; onRemoveFocus && onRemoveFocus(); if (D2AgeFieldPlain.isEmptyNumbers(values)) { @@ -130,24 +118,37 @@ class D2AgeFieldPlain extends Component { return; } - const calendar = systemSettingsStore.get().calendar; - - const now = Temporal.Now.plainDateISO().withCalendar(calendar); + const now = Temporal.Now.plainDateISO().withCalendar(calendarType); const calculatedDate = now.subtract({ years: D2AgeFieldPlain.getNumberOrZero(values.years), months: D2AgeFieldPlain.getNumberOrZero(values.months), days: D2AgeFieldPlain.getNumberOrZero(values.days), }); - - const calculatedValues = getCalculatedValues(convertTemporalToString(calculatedDate)); + const dateString = temporalToString(calculatedDate, dateFormat); + const calculatedValues = getCalculatedValues(dateString, calendarType, dateFormat); this.props.onBlur(calculatedValues); } handleDateBlur = (date: ?string, options: ?ValidationOptions) => { - const { onRemoveFocus } = this.props; + const { onRemoveFocus, calendarType = 'gregory', dateFormat = 'YYYY-MM-DD' } = this.props; onRemoveFocus && onRemoveFocus(); - const calculatedValues = date ? getCalculatedValues(date) : null; + const isDateValid = options && !options.error; + if (!date) { + this.props.onBlur(null, options); + return; + } + if (!isDateValid) { + const calculatedValues = { + date, + years: '', + months: '', + days: '', + }; + this.props.onBlur(calculatedValues, options); + return; + } + const calculatedValues = getCalculatedValues(date, calendarType, dateFormat); this.props.onBlur(calculatedValues, options); } diff --git a/src/core_modules/capture-ui/DateAndTimeFields/DateField/Date.component.js b/src/core_modules/capture-ui/DateAndTimeFields/DateField/Date.component.js index 1c4b2ee4b0..8fa5d01b6e 100644 --- a/src/core_modules/capture-ui/DateAndTimeFields/DateField/Date.component.js +++ b/src/core_modules/capture-ui/DateAndTimeFields/DateField/Date.component.js @@ -1,7 +1,6 @@ // @flow import React from 'react'; import { CalendarInput } from '@dhis2/ui'; -import { systemSettingsStore } from '../../../capture-core/metaDataMemoryStores'; type ValidationOptions = { error?: ?string, @@ -21,7 +20,9 @@ type Props = { placeholder?: string, label?: string, calendarMax?: any, - innerMessage?: any + innerMessage?: any, + dateFormat: ?string, + calendarType: ?string, }; type Validation = {| @@ -64,11 +65,13 @@ export class DateField extends React.Component { calendarMax, value, innerMessage, + calendarType, + dateFormat, } = this.props; const calculatedInputWidth = inputWidth || width; const calculatedCalendarWidth = calendarWidth || width; - const calendarType = systemSettingsStore.get().calendar || 'gregory'; - const format = systemSettingsStore.get().dateFormat; + const calendar = calendarType || 'gregory'; + const format = dateFormat || 'YYYY-MM-DD'; const errorProps = innerMessage && innerMessage.messageType === 'error' ? { error: !!innerMessage.message?.dateInnerErrorMessage, validationText: innerMessage.message?.dateInnerErrorMessage } @@ -86,7 +89,7 @@ export class DateField extends React.Component { placeholder={this.props.placeholder} format={format} onDateSelect={this.handleDateSelected} - calendar={calendarType} + calendar={calendar} date={value} width={String(calculatedCalendarWidth)} inputWidth={String(calculatedInputWidth)} From c8d1fbd06d059a4eac71255c0812b4988b85523c Mon Sep 17 00:00:00 2001 From: alaa-yahia Date: Mon, 13 Jan 2025 11:21:00 +0200 Subject: [PATCH 19/42] refactor: simplify converters logic --- .../capture-core-utils/date/stringToTemporal.js | 4 ++-- .../configs/dateTimeField/getDateTimeFieldConfig.js | 3 +++ .../Enrollment/EnrollmentDataEntry.component.js | 9 ++++++++- .../EditEventDataEntry.component.js | 3 +++ .../capture-core/converters/formToClient.js | 13 +++++-------- .../converters/date/convertLocalToIsoCalendar.js | 9 +-------- .../converters/date/convertStringToTemporal.js | 8 ++++---- .../converters/date/convertTemporalToString.js | 4 ++-- 8 files changed, 28 insertions(+), 25 deletions(-) diff --git a/src/core_modules/capture-core-utils/date/stringToTemporal.js b/src/core_modules/capture-core-utils/date/stringToTemporal.js index 8063305d0f..3be34f0775 100644 --- a/src/core_modules/capture-core-utils/date/stringToTemporal.js +++ b/src/core_modules/capture-core-utils/date/stringToTemporal.js @@ -17,7 +17,7 @@ type PlainDate = { }; export function stringToTemporal(dateString: ?string, - calendarType: ?string, + calendar: ?string, dateFormat: ?string): PlainDate | null { if (!dateString) { return null; @@ -37,7 +37,7 @@ export function stringToTemporal(dateString: ?string, year, month, day, - calendarType, + calendar, }); } catch (error) { return null; diff --git a/src/core_modules/capture-core/components/D2Form/field/configs/dateTimeField/getDateTimeFieldConfig.js b/src/core_modules/capture-core/components/D2Form/field/configs/dateTimeField/getDateTimeFieldConfig.js index d7a24cb7f3..939efefb95 100644 --- a/src/core_modules/capture-core/components/D2Form/field/configs/dateTimeField/getDateTimeFieldConfig.js +++ b/src/core_modules/capture-core/components/D2Form/field/configs/dateTimeField/getDateTimeFieldConfig.js @@ -2,6 +2,7 @@ import { orientations } from '../../../../FormFields/New'; import { createFieldConfig, createProps } from '../base/configBaseDefaultForm'; import { DateTimeFieldForForm } from '../../Components'; +import { systemSettingsStore } from '../../../../../metaDataMemoryStores'; import type { DataElement as MetaDataElement } from '../../../../../metaData'; import type { QuerySingleResource } from '../../../../../utils/api/api.types'; @@ -17,6 +18,8 @@ export const getDateTimeFieldConfig = (metaData: MetaDataElement, options: Objec shrinkDisabled: options.formHorizontal, calendarWidth: options.formHorizontal ? '250px' : '350px', popupAnchorPosition: getCalendarAnchorPosition(options.formHorizontal), + calendarType: systemSettingsStore.get().calendar, + dateFormat: systemSettingsStore.get().dateFormat, }, options, metaData); return createFieldConfig({ diff --git a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentDataEntry.component.js b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentDataEntry.component.js index 5ca70266c2..6f9a52fb42 100644 --- a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentDataEntry.component.js +++ b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentDataEntry.component.js @@ -43,6 +43,7 @@ import { withDataEntryFields, } from '../../DataEntryDhis2Helpers'; import { convertDateObjectToDateFormatString } from '../../../../capture-core/utils/converters/date'; +import { systemSettingsStore } from '../../../metaDataMemoryStores'; const overrideMessagePropNames = { errorMessage: 'validationError', @@ -112,7 +113,11 @@ const getEnrollmentDateSettings = () => { required: true, calendarWidth: props.formHorizontal ? 250 : 350, popupAnchorPosition: getCalendarAnchorPosition(props.formHorizontal), - calendarMax: !props.enrollmentMetadata.allowFutureEnrollmentDate ? convertDateObjectToDateFormatString(moment()) : undefined, + calendarMax: !props.enrollmentMetadata.allowFutureEnrollmentDate ? + convertDateObjectToDateFormatString(moment()) : + undefined, + calendarType: systemSettingsStore.get().calendar, + dateFormat: systemSettingsStore.get().dateFormat, }), getPropName: () => 'enrolledAt', getValidatorContainers: getEnrollmentDateValidatorContainer, @@ -163,6 +168,8 @@ const getIncidentDateSettings = () => { calendarMax: !props.enrollmentMetadata.allowFutureIncidentDate ? convertDateObjectToDateFormatString(moment()) : undefined, + calendarType: systemSettingsStore.get().calendar, + dateFormat: systemSettingsStore.get().dateFormat, }), getPropName: () => 'occurredAt', getPassOnFieldData: () => true, diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.component.js b/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.component.js index e958a69644..96b1e4ff0a 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.component.js +++ b/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.component.js @@ -47,6 +47,7 @@ import { withAOCFieldBuilder, withDataEntryFields, } from '../../DataEntryDhis2Helpers/'; +import { systemSettingsStore } from '../../../metaDataMemoryStores'; import type { UserFormField } from '../../FormFields/UserField'; const tabMode = Object.freeze({ @@ -137,6 +138,8 @@ const buildReportDateSettingsFn = () => { calendarWidth: 350, label: props.formFoundation.getLabel('occurredAt'), required: true, + calendarType: systemSettingsStore.get().calendar, + dateFormat: systemSettingsStore.get().dateFormat, }), getPropName: () => 'occurredAt', getValidatorContainers: () => getEventDateValidatorContainers(), diff --git a/src/core_modules/capture-core/converters/formToClient.js b/src/core_modules/capture-core/converters/formToClient.js index d0a4460e24..187549d140 100644 --- a/src/core_modules/capture-core/converters/formToClient.js +++ b/src/core_modules/capture-core/converters/formToClient.js @@ -3,7 +3,7 @@ import moment from 'moment'; import isString from 'd2-utilizr/lib/isString'; import { parseNumber, parseTime } from 'capture-core-utils/parsers'; import { dataElementTypes } from '../metaData'; -import { parseDate, convertLocalToIsoCalendar } from '../utils/converters/date'; +import { convertLocalToIsoCalendar } from '../utils/converters/date'; type DateTimeValue = { date: string, @@ -25,21 +25,18 @@ function convertDateTime(formValue: DateTimeValue): ?string { const hours = momentTime.hour(); const minutes = momentTime.minute(); - const parsedDate = editedDate ? parseDate(editedDate) : null; - if (!(parsedDate && parsedDate.isValid && parsedDate.momentDate)) return null; + const isoDate = convertDate(editedDate); + if (!isoDate) return null; - const formattedDate = parsedDate.momentDate.format('YYYY-MM-DD'); - const isoDate = convertLocalToIsoCalendar(formattedDate); const momentDateTime = moment(isoDate); + if (!momentDateTime.isValid()) return null; momentDateTime.hour(hours); momentDateTime.minute(minutes); return momentDateTime.toISOString(); } function convertDate(dateValue: string) { - const parsedDate = parseDate(dateValue); - // $FlowFixMe[incompatible-use] automated comment - return parsedDate.isValid ? convertLocalToIsoCalendar(parsedDate.momentDate.toISOString()) : null; + return convertLocalToIsoCalendar(dateValue); } function convertTime(timeValue: string) { diff --git a/src/core_modules/capture-core/utils/converters/date/convertLocalToIsoCalendar.js b/src/core_modules/capture-core/utils/converters/date/convertLocalToIsoCalendar.js index 2323f01370..408116d011 100644 --- a/src/core_modules/capture-core/utils/converters/date/convertLocalToIsoCalendar.js +++ b/src/core_modules/capture-core/utils/converters/date/convertLocalToIsoCalendar.js @@ -17,16 +17,9 @@ export function convertLocalToIsoCalendar(localDate: ?string): string { return ''; } - const momentDate = moment(localDate); - if (!momentDate.isValid()) { - return ''; - } - - const formattedIsoDate = momentDate.format('YYYY-MM-DD'); - const calendar = systemSettingsStore.get().calendar; - const { year, month, day } = convertToIso8601(formattedIsoDate, calendar); + const { year, month, day } = convertToIso8601(localDate, calendar); const dateString = `${padWithZeros(year, 4)}-${padWithZeros(month, 2)}-${padWithZeros(day, 2)}`; const parsedMoment = moment(dateString); diff --git a/src/core_modules/capture-core/utils/converters/date/convertStringToTemporal.js b/src/core_modules/capture-core/utils/converters/date/convertStringToTemporal.js index 57c0dec668..1c53443cc6 100644 --- a/src/core_modules/capture-core/utils/converters/date/convertStringToTemporal.js +++ b/src/core_modules/capture-core/utils/converters/date/convertStringToTemporal.js @@ -15,11 +15,11 @@ type PlainDate = { day: number }; -export function convertStringToTemporal(dateString: ?string): PlainDate | null { +export function convertStringToTemporal(dateString: ?string, calendar: ?string, format: ?string): PlainDate | null { if (!dateString) { return null; } - const calendar = systemSettingsStore.get().calendar; - const dateFormat = systemSettingsStore.get().dateFormat; - return stringToTemporal(dateString, calendar, dateFormat); + const calendarType = calendar || systemSettingsStore.get().calendar; + const dateFormat = format || systemSettingsStore.get().dateFormat; + return stringToTemporal(dateString, calendarType, dateFormat); } diff --git a/src/core_modules/capture-core/utils/converters/date/convertTemporalToString.js b/src/core_modules/capture-core/utils/converters/date/convertTemporalToString.js index 9ba772b23a..0030ce0d68 100644 --- a/src/core_modules/capture-core/utils/converters/date/convertTemporalToString.js +++ b/src/core_modules/capture-core/utils/converters/date/convertTemporalToString.js @@ -14,10 +14,10 @@ type PlainDate = { day: number }; -export function convertTemporalToString(temporalDate: PlainDate | null): string { +export function convertTemporalToString(temporalDate: PlainDate | null, format: ?string): string { if (!temporalDate) { return ''; } - const dateFormat = systemSettingsStore.get().dateFormat; + const dateFormat = format || systemSettingsStore.get().dateFormat; return temporalToString(temporalDate, dateFormat); } From 98054358fe0936f29471ea47e7ce2aae444f246f Mon Sep 17 00:00:00 2001 From: alaa-yahia Date: Mon, 13 Jan 2025 11:52:52 +0200 Subject: [PATCH 20/42] fix: logic of dateRange & dateTimeRange validators --- .../capture-core-utils/parsers/date.parser.js | 88 ------------------- .../capture-core-utils/parsers/index.js | 1 - .../utils/converters/date/index.js | 1 - .../utils/converters/date/parser.js | 14 --- .../date/stringToMomentDateFormat.js | 2 +- .../validators/form/getDateRangeValidator.js | 8 +- .../form/getDateTimeRangeValidator.js | 38 ++++---- 7 files changed, 25 insertions(+), 127 deletions(-) delete mode 100644 src/core_modules/capture-core-utils/parsers/date.parser.js delete mode 100644 src/core_modules/capture-core/utils/converters/date/parser.js diff --git a/src/core_modules/capture-core-utils/parsers/date.parser.js b/src/core_modules/capture-core-utils/parsers/date.parser.js deleted file mode 100644 index 98bbef55f0..0000000000 --- a/src/core_modules/capture-core-utils/parsers/date.parser.js +++ /dev/null @@ -1,88 +0,0 @@ -// @flow -import moment from 'moment'; - -const getReturnObject = (momentDate: ?moment$Moment) => ({ - momentDate, - isValid: momentDate ? momentDate.isValid() : false, -}); - -function manipulateFormatAndParseWithSeparator(dateString: string, inputFormat: string, separator: string) { - const dateSplitted = dateString.split(separator); - const formatSplitted = inputFormat.split(separator); - if (dateSplitted.length === formatSplitted.length) { - const newLocaleFormat = formatSplitted - .map((formatPart, index) => { - let newFormatPart = ''; - const datePart = dateSplitted[index]; - if (formatPart && datePart) { - const partKey = formatPart.charAt(0); - if (partKey === 'M') { - if (datePart.length === 1) { - newFormatPart = 'M'; - } else if (datePart.length === 2) { - newFormatPart = 'MM'; - } - } else if (partKey === 'D') { - if (datePart.length === 1) { - newFormatPart = 'D'; - } else if (datePart.length === 2) { - newFormatPart = 'DD'; - } - } else if (partKey === 'Y') { - if (datePart.length === 2) { - newFormatPart = 'YY'; - } else if (datePart.length === 4) { - newFormatPart = 'YYYY'; - } - } - } - return newFormatPart || formatPart; - }) - .join(separator); - const momentDate = moment(dateString, newLocaleFormat, true); - return getReturnObject(momentDate); - } - - return getReturnObject(null); -} - -function parseWithSeparator(dateString: string, localeFormat: string, separatorPattern: RegExp) { - const specialCharactersInLocaleFormat = localeFormat.match(separatorPattern); - // $FlowFixMe[incompatible-type] automated comment - const separator: string = specialCharactersInLocaleFormat && specialCharactersInLocaleFormat[0]; - const dateStringWithLocaleSeparator = dateString.replace(separatorPattern, separator); - const localeFormatSameSeparator = localeFormat.replace(separatorPattern, separator); - - const momentDate = moment(dateStringWithLocaleSeparator, localeFormatSameSeparator, true); - if (momentDate.isValid()) { - return getReturnObject(momentDate); - } - - const parseData = manipulateFormatAndParseWithSeparator( - dateStringWithLocaleSeparator, - localeFormatSameSeparator, - separator, - ); - return parseData; -} - -function parseWithoutSeparator(dateString: string, localeFormat: string, separatorPattern: RegExp) { - const dateStringWithoutSeparator = dateString.replace(separatorPattern, ''); - const localeFormatWithoutSeparator = localeFormat.replace(separatorPattern, ''); - - const momentDate = moment(dateStringWithoutSeparator, localeFormatWithoutSeparator, true); - if (momentDate.isValid()) { - return getReturnObject(momentDate); - } - - return getReturnObject(null); -} - -export function parseDate(dateString: string, format: string) { - const separatorPattern = /[.,\-_/\\]/g; - if (separatorPattern.test(dateString) && separatorPattern.test(format)) { - return parseWithSeparator(dateString, format, separatorPattern); - } - - return parseWithoutSeparator(dateString, format, separatorPattern); -} diff --git a/src/core_modules/capture-core-utils/parsers/index.js b/src/core_modules/capture-core-utils/parsers/index.js index e28120d581..46502500df 100644 --- a/src/core_modules/capture-core-utils/parsers/index.js +++ b/src/core_modules/capture-core-utils/parsers/index.js @@ -1,4 +1,3 @@ // @flow -export { parseDate } from './date.parser'; export { parseNumber } from './number.parser'; export { parseTime } from './time.parser'; diff --git a/src/core_modules/capture-core/utils/converters/date/index.js b/src/core_modules/capture-core/utils/converters/date/index.js index 8f878a421d..6c755de6c3 100644 --- a/src/core_modules/capture-core/utils/converters/date/index.js +++ b/src/core_modules/capture-core/utils/converters/date/index.js @@ -1,5 +1,4 @@ // @flow -export { parseDate } from './parser'; export { convertDateObjectToDateFormatString } from './dateObjectToDateFormatString'; export { convertMomentToDateFormatString } from './momentToDateFormatString'; export { convertStringToDateFormat } from './stringToMomentDateFormat'; diff --git a/src/core_modules/capture-core/utils/converters/date/parser.js b/src/core_modules/capture-core/utils/converters/date/parser.js deleted file mode 100644 index a487155ca0..0000000000 --- a/src/core_modules/capture-core/utils/converters/date/parser.js +++ /dev/null @@ -1,14 +0,0 @@ -// @flow -import { parseDate as parseDateCore } from 'capture-core-utils/parsers'; -import { systemSettingsStore } from '../../../metaDataMemoryStores'; - -/** - * Parse a string in date format - * @export - * @param {string} value - the string in date format - * @returns {date} - */ -export function parseDate(value: string) { - const format = systemSettingsStore.get().dateFormat; - return parseDateCore(value, format); -} diff --git a/src/core_modules/capture-core/utils/converters/date/stringToMomentDateFormat.js b/src/core_modules/capture-core/utils/converters/date/stringToMomentDateFormat.js index 29c7b2c929..f75fb68255 100644 --- a/src/core_modules/capture-core/utils/converters/date/stringToMomentDateFormat.js +++ b/src/core_modules/capture-core/utils/converters/date/stringToMomentDateFormat.js @@ -9,7 +9,7 @@ import { systemSettingsStore } from '../../../metaDataMemoryStores'; * @param {string} [format] - optional date format. If not provided, the function uses system date format * @returns {string} */ -export function convertStringToDateFormat(date: string, format?: string) { +export function convertStringToDateFormat(date: ?string, format?: string) { if (!date || !date.length) { return ''; } const dateFormat = format || systemSettingsStore.get().dateFormat; const formattedDateString = moment(date, dateFormat).format(dateFormat); diff --git a/src/core_modules/capture-core/utils/validation/validators/form/getDateRangeValidator.js b/src/core_modules/capture-core/utils/validation/validators/form/getDateRangeValidator.js index c76d738184..f54f241d7d 100644 --- a/src/core_modules/capture-core/utils/validation/validators/form/getDateRangeValidator.js +++ b/src/core_modules/capture-core/utils/validation/validators/form/getDateRangeValidator.js @@ -1,6 +1,7 @@ // @flow +import { Temporal } from '@js-temporal/polyfill'; import { isValidDate } from './dateValidator'; -import { parseDate } from '../../../converters/date'; +import { convertStringToDateFormat } from '../../../converters/date'; /** * * @export @@ -30,6 +31,9 @@ export const getDateRangeValidator = (invalidDateMessage: string) => errorMessage: errorResult.reduce((map, error) => ({ ...map, ...error }), {}), }; } + const { from, to } = value; // $FlowFixMe - return parseDate(value.from).momentDate <= parseDate(value.to).momentDate; + const formattedFrom = convertStringToDateFormat(from, 'YYYY-MM-DD'); + const fromattedTo = convertStringToDateFormat(to, 'YYYY-MM-DD'); + return Temporal.PlainDate.compare(formattedFrom, fromattedTo) <= 0; }; diff --git a/src/core_modules/capture-core/utils/validation/validators/form/getDateTimeRangeValidator.js b/src/core_modules/capture-core/utils/validation/validators/form/getDateTimeRangeValidator.js index 154749b666..f238469b0f 100644 --- a/src/core_modules/capture-core/utils/validation/validators/form/getDateTimeRangeValidator.js +++ b/src/core_modules/capture-core/utils/validation/validators/form/getDateTimeRangeValidator.js @@ -1,34 +1,32 @@ // @flow +import { Temporal } from '@js-temporal/polyfill'; import { isValidDateTime } from './dateTimeValidator'; -import { parseDate } from '../../../converters/date'; function isValidDateTimeWithEmptyCheck(value: ?Object) { return value && isValidDateTime(value); } -const convertDateTimeToMoment = (value: Object) => { - const date = value.date; - const time = value.time; +const convertDateTimeToTemporal = (value: Object) => { + const { date, time } = value; + + const [year, month, day] = date.split('-').map(Number); + let hour; let minutes; if (/[:.]/.test(time)) { - [hour, minutes] = time.split(/[:.]/); + [hour, minutes] = time.split(/[:.]/).map(Number); } else if (time.length === 3) { - hour = time.substring(0, 1); - minutes = time.substring(2, 3); + hour = Number(time.substring(0, 1)); + minutes = Number(time.substring(2, 3)); } else { - hour = time.substring(0, 2); - minutes = time.substring(3, 4); + hour = Number(time.substring(0, 2)); + minutes = Number(time.substring(3, 4)); } - const momentDateTime = parseDate(date).momentDate; - // $FlowFixMe[incompatible-use] automated comment - momentDateTime.hour(hour); - // $FlowFixMe[incompatible-use] automated comment - momentDateTime.minute(minutes); - return momentDateTime; + + return new Temporal.PlainDateTime(year, month, day, hour, minutes); }; export const getDateTimeRangeValidator = (invalidDateTimeMessage: string) => - (value: { from?: ?Object, to?: ?Object}) => { + (value: { from?: ?Object, to?: ?Object }) => { const errorResult = []; if (!isValidDateTimeWithEmptyCheck(value.from)) { errorResult.push({ from: invalidDateTimeMessage }); @@ -46,8 +44,8 @@ export const getDateTimeRangeValidator = (invalidDateTimeMessage: string) => }; } - const fromDateTime = convertDateTimeToMoment(value.from); - const toDateTime = convertDateTimeToMoment(value.to); - // $FlowFixMe[invalid-compare] automated comment - return fromDateTime <= toDateTime; + const fromDateTime = convertDateTimeToTemporal(value.from); + const toDateTime = convertDateTimeToTemporal(value.to); + + return Temporal.PlainDateTime.compare(fromDateTime, toDateTime) <= 0; }; From d67810bedf6a73c62ebe17735231f5be4c60366e Mon Sep 17 00:00:00 2001 From: alaa-yahia Date: Mon, 13 Jan 2025 12:54:15 +0200 Subject: [PATCH 21/42] chore: pass format and calendar as props --- .../dateField/getDateFieldConfigForCustomForm.js | 3 +++ .../dateRangeField/getDateRangeFieldConfig.js | 3 +++ .../getDateTimeFieldConfigForCustomForm.js | 3 +++ .../getDateTimeRangeFieldConfig.js | 3 +++ .../EnrollmentWithFirstStageDataEntry.component.js | 3 +++ .../DataEntry/DataEntry.component.js | 3 +++ .../DataEntry/DataEntry.component.js | 3 +++ .../EditEventDataEntry.component.js | 2 ++ .../ScheduleDate/ScheduleDate.component.js | 3 +++ .../FormComponents/DateFieldForRelatedStages.js | 6 +++++- .../validators/form/getDateTimeRangeValidator.js | 12 ++++++++++-- 11 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/core_modules/capture-core/components/D2Form/field/configs/dateField/getDateFieldConfigForCustomForm.js b/src/core_modules/capture-core/components/D2Form/field/configs/dateField/getDateFieldConfigForCustomForm.js index efeea1412c..9f5b057d0d 100644 --- a/src/core_modules/capture-core/components/D2Form/field/configs/dateField/getDateFieldConfigForCustomForm.js +++ b/src/core_modules/capture-core/components/D2Form/field/configs/dateField/getDateFieldConfigForCustomForm.js @@ -3,6 +3,7 @@ import moment from 'moment'; import { createFieldConfig, createProps } from '../base/configBaseCustomForm'; import { DateFieldForCustomForm } from '../../Components'; import { convertDateObjectToDateFormatString } from '../../../../../../capture-core/utils/converters/date'; +import { systemSettingsStore } from '../../../../../metaDataMemoryStores'; import type { DateDataElement } from '../../../../../metaData'; import type { QuerySingleResource } from '../../../../../utils/api/api.types'; @@ -12,6 +13,8 @@ export const getDateFieldConfigForCustomForm = (metaData: DateDataElement, optio maxWidth: 350, calendarWidth: 350, calendarMax: !metaData.allowFutureDate ? convertDateObjectToDateFormatString(moment()) : undefined, + calendarType: systemSettingsStore.get().calendar, + dateFormat: systemSettingsStore.get().dateFormat, }, metaData); return createFieldConfig({ diff --git a/src/core_modules/capture-core/components/D2Form/field/configs/dateRangeField/getDateRangeFieldConfig.js b/src/core_modules/capture-core/components/D2Form/field/configs/dateRangeField/getDateRangeFieldConfig.js index 283da64708..90fec23265 100644 --- a/src/core_modules/capture-core/components/D2Form/field/configs/dateRangeField/getDateRangeFieldConfig.js +++ b/src/core_modules/capture-core/components/D2Form/field/configs/dateRangeField/getDateRangeFieldConfig.js @@ -1,6 +1,7 @@ // @flow import { createFieldConfig, createProps } from '../base/configBaseDefaultForm'; import { DateRangeFieldForForm } from '../../Components'; +import { systemSettingsStore } from '../../../../../metaDataMemoryStores'; import type { DataElement as MetaDataElement } from '../../../../../metaData'; import type { QuerySingleResource } from '../../../../../utils/api/api.types'; @@ -14,6 +15,8 @@ export const getDateRangeFieldConfig = (metaData: MetaDataElement, options: Obje maxWidth: options.formHorizontal ? 150 : 350, calendarWidth: options.formHorizontal ? 250 : 350, popupAnchorPosition: getCalendarAnchorPosition(options.formHorizontal), + calendarType: systemSettingsStore.get().calendar, + dateFormat: systemSettingsStore.get().dateFormat, }, options, metaData); return createFieldConfig({ diff --git a/src/core_modules/capture-core/components/D2Form/field/configs/dateTimeField/getDateTimeFieldConfigForCustomForm.js b/src/core_modules/capture-core/components/D2Form/field/configs/dateTimeField/getDateTimeFieldConfigForCustomForm.js index 86024056fc..c061ea81e7 100644 --- a/src/core_modules/capture-core/components/D2Form/field/configs/dateTimeField/getDateTimeFieldConfigForCustomForm.js +++ b/src/core_modules/capture-core/components/D2Form/field/configs/dateTimeField/getDateTimeFieldConfigForCustomForm.js @@ -2,6 +2,7 @@ import { orientations } from '../../../../FormFields/New'; import { createFieldConfig, createProps } from '../base/configBaseCustomForm'; import { DateTimeFieldForCustomForm } from '../../Components'; +import { systemSettingsStore } from '../../../../../metaDataMemoryStores'; import type { DataElement as MetaDataElement } from '../../../../../metaData'; import type { QuerySingleResource } from '../../../../../utils/api/api.types'; @@ -12,6 +13,8 @@ export const getDateTimeFieldConfigForCustomForm = (metaData: MetaDataElement, o calendarWidth: '350px', orientation: orientations.HORIZONTAL, shrinkDisabled: false, + calendarType: systemSettingsStore.get().calendar, + dateFormat: systemSettingsStore.get().dateFormat, }, metaData); return createFieldConfig({ diff --git a/src/core_modules/capture-core/components/D2Form/field/configs/dateTimeRangeField/getDateTimeRangeFieldConfig.js b/src/core_modules/capture-core/components/D2Form/field/configs/dateTimeRangeField/getDateTimeRangeFieldConfig.js index cc6e6c1a1a..b4919fd991 100644 --- a/src/core_modules/capture-core/components/D2Form/field/configs/dateTimeRangeField/getDateTimeRangeFieldConfig.js +++ b/src/core_modules/capture-core/components/D2Form/field/configs/dateTimeRangeField/getDateTimeRangeFieldConfig.js @@ -2,6 +2,7 @@ import { orientations } from '../../../../FormFields/New'; import { createFieldConfig, createProps } from '../base/configBaseDefaultForm'; import { DateTimeRangeFieldForForm } from '../../Components'; +import { systemSettingsStore } from '../../../../../metaDataMemoryStores'; import type { DataElement as MetaDataElement } from '../../../../../metaData'; import type { QuerySingleResource } from '../../../../../utils/api/api.types'; @@ -17,6 +18,8 @@ export const getDateTimeRangeFieldConfig = (metaData: MetaDataElement, options: shrinkDisabled: options.formHorizontal, calendarWidth: options.formHorizontal ? 250 : 350, popupAnchorPosition: getCalendarAnchorPosition(options.formHorizontal), + calendarType: systemSettingsStore.get().calendar, + dateFormat: systemSettingsStore.get().dateFormat, }, options, metaData); return createFieldConfig({ diff --git a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.component.js b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.component.js index cff88329d0..e547c41eaf 100644 --- a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.component.js +++ b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.component.js @@ -25,6 +25,7 @@ import { withCleanUp } from './withCleanUp'; import { getEventDateValidatorContainers } from './fieldValidators/eventDate.validatorContainersGetter'; import { stageMainDataIds } from './getDataEntryPropsToInclude'; import { withTransformPropName } from '../../../../HOC'; +import { systemSettingsStore } from '../../../../metaDataMemoryStores'; const overrideMessagePropNames = { errorMessage: 'validationError', @@ -219,6 +220,8 @@ const getReportDateSettingsFn = () => { required: true, calendarWidth: props.formHorizontal ? 250 : 350, popupAnchorPosition: getCalendarAnchorPosition(props.formHorizontal), + calendarType: systemSettingsStore.get().calendar, + dateFormat: systemSettingsStore.get().dateFormat, }), getPropName: () => stageMainDataIds.OCCURRED_AT, getValidatorContainers: () => getEventDateValidatorContainers(), diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.component.js b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.component.js index 3743fa5dc4..ac5fb4c65d 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.component.js +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.component.js @@ -52,6 +52,7 @@ import { attributeOptionsKey, getCategoryOptionsValidatorContainers, withAOCFieldBuilder, withDataEntryFields, } from '../../../../DataEntryDhis2Helpers'; +import { systemSettingsStore } from '../../../../../metaDataMemoryStores'; const getStyles = theme => ({ savingContextContainer: { @@ -162,6 +163,8 @@ const buildReportDateSettingsFn = () => { required: true, calendarWidth: props.formHorizontal ? 250 : 350, popupAnchorPosition: getCalendarAnchorPosition(props.formHorizontal), + calendarType: systemSettingsStore.get().calendar, + dateFormat: systemSettingsStore.get().dateFormat, }), getPropName: () => 'occurredAt', getValidatorContainers: () => getEventDateValidatorContainers(), diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.component.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.component.js index aa4520ca1c..3954899320 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.component.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.component.js @@ -42,6 +42,7 @@ import { attributeOptionsKey, getCategoryOptionsValidatorContainers, } from '../../DataEntryDhis2Helpers'; +import { systemSettingsStore } from '../../../metaDataMemoryStores'; const getStyles = theme => ({ savingContextContainer: { @@ -149,6 +150,8 @@ const buildReportDateSettingsFn = () => { required: true, calendarWidth: props.formHorizontal ? 250 : 350, popupAnchorPosition: getCalendarAnchorPosition(props.formHorizontal), + calendarType: systemSettingsStore.get().calendar, + dateFormat: systemSettingsStore.get().dateFormat, }), getPropName: () => 'occurredAt', getValidatorContainers: () => getEventDateValidatorContainers(), diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.component.js b/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.component.js index 96b1e4ff0a..5a112438b1 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.component.js +++ b/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.component.js @@ -187,6 +187,8 @@ const buildScheduleDateSettingsFn = () => { calendarWidth: 350, label: props.formFoundation.getLabel('scheduledAt'), disabled: true, + calendarType: systemSettingsStore.get().calendar, + dateFormat: systemSettingsStore.get().dateFormat, }), getIsHidden: (props: Object) => props.id !== dataEntryIds.ENROLLMENT_EVENT || props.hideDueDate, getPropName: () => 'scheduledAt', diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleDate/ScheduleDate.component.js b/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleDate/ScheduleDate.component.js index ca3f806f2d..cd5d2ab5f1 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleDate/ScheduleDate.component.js +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleDate/ScheduleDate.component.js @@ -4,6 +4,7 @@ import { spacersNum } from '@dhis2/ui'; import withStyles from '@material-ui/core/styles/withStyles'; import { DateField } from 'capture-core/components/FormFields/New'; import { InfoBox } from '../InfoBox'; +import { systemSettingsStore } from '../../../metaDataMemoryStores'; import type { Props } from './scheduleDate.types'; const styles = { @@ -42,6 +43,8 @@ const ScheduleDatePlain = ({ } setScheduleDate(e); }} + calendarType={systemSettingsStore.get().calendar} + dateFormat={systemSettingsStore.get().dateFormat} />
} ); }; diff --git a/src/core_modules/capture-core/utils/validation/validators/form/getDateTimeRangeValidator.js b/src/core_modules/capture-core/utils/validation/validators/form/getDateTimeRangeValidator.js index f238469b0f..45b497a108 100644 --- a/src/core_modules/capture-core/utils/validation/validators/form/getDateTimeRangeValidator.js +++ b/src/core_modules/capture-core/utils/validation/validators/form/getDateTimeRangeValidator.js @@ -1,5 +1,6 @@ // @flow import { Temporal } from '@js-temporal/polyfill'; +import { convertStringToTemporal } from 'capture-core/utils/converters/date'; import { isValidDateTime } from './dateTimeValidator'; function isValidDateTimeWithEmptyCheck(value: ?Object) { @@ -8,7 +9,12 @@ function isValidDateTimeWithEmptyCheck(value: ?Object) { const convertDateTimeToTemporal = (value: Object) => { const { date, time } = value; - const [year, month, day] = date.split('-').map(Number); + const dateInTemporal = convertStringToTemporal(date); + + if (!dateInTemporal) { + return null; + } + const { year, month, day } = dateInTemporal; let hour; let minutes; @@ -46,6 +52,8 @@ export const getDateTimeRangeValidator = (invalidDateTimeMessage: string) => const fromDateTime = convertDateTimeToTemporal(value.from); const toDateTime = convertDateTimeToTemporal(value.to); - + if (!fromDateTime || !toDateTime) { + return false; + } return Temporal.PlainDateTime.compare(fromDateTime, toDateTime) <= 0; }; From 068c3ef7ef988110ed3aff212ae853209aff6c6e Mon Sep 17 00:00:00 2001 From: alaa-yahia Date: Thu, 16 Jan 2025 01:36:54 +0200 Subject: [PATCH 22/42] fix: status label in the StagesAndEvents widget display ISO date --- .../Stages/Stage/StageDetail/hooks/helpers.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/hooks/helpers.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/hooks/helpers.js index 9d7f31f63f..eb5c2726b7 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/hooks/helpers.js +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/hooks/helpers.js @@ -3,13 +3,14 @@ import React from 'react'; import moment from 'moment'; import i18n from '@dhis2/d2-i18n'; import { statusTypes, translatedStatusTypes } from 'capture-core/events/statusTypes'; -import { convertMomentToDateFormatString } from '../../../../../../utils/converters/date'; +import { convertClientToList } from '../../../../../../converters'; import { getSubValues } from '../../getEventDataWithSubValue'; import type { StageDataElement } from '../../../../types/common.types'; import { Notes } from '../Notes.component'; import type { QuerySingleResource } from '../../../../../../utils/api/api.types'; import { isEventOverdue } from '../../../../../../utils/isEventOverdue'; import { TooltipOrgUnit } from '../../../../../Tooltips/TooltipOrgUnit/TooltipOrgUnit.component'; +import { dataElementTypes } from '../../../../../../metaData'; const getEventStatus = (event: ApiEnrollmentEvent) => { const today = moment().startOf('day'); @@ -37,7 +38,7 @@ const getEventStatus = (event: ApiEnrollmentEvent) => { if (daysUntilDueDate < 14) { return { status: statusTypes.SCHEDULE, options: dueDateFromNow }; } - return { status: statusTypes.SCHEDULE, options: convertMomentToDateFormatString(dueDate) }; + return { status: statusTypes.SCHEDULE, options: convertClientToList(dueDate, dataElementTypes.DATE) }; } return { status: event.status, options: undefined }; }; From cda3dab8b651e6dbf5d69d16e4b8fb60ce593be1 Mon Sep 17 00:00:00 2001 From: alaa-yahia Date: Thu, 16 Jan 2025 02:05:34 +0200 Subject: [PATCH 23/42] fix: currentSearchTerms are not converted correctly --- src/core_modules/capture-core/converters/clientToList.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core_modules/capture-core/converters/clientToList.js b/src/core_modules/capture-core/converters/clientToList.js index f026191a4e..c64c30db9e 100644 --- a/src/core_modules/capture-core/converters/clientToList.js +++ b/src/core_modules/capture-core/converters/clientToList.js @@ -62,7 +62,7 @@ function convertImageForDisplay(clientValue: ImageClientValue) { return ; } -function convertRangeForDisplay(parser: any, clientValue: any) { +function convertRangeForDisplay(parser: any = (value: any) => value, clientValue: any) { return ( {parser(clientValue.from)} {'->'} {parser(clientValue.to)} @@ -106,9 +106,9 @@ const valueConvertersForType = { [dataElementTypes.INTEGER_NEGATIVE_RANGE]: value => convertRangeForDisplay(stringifyNumber, value), [dataElementTypes.PERCENTAGE]: (value: number) => `${stringifyNumber(value)} %`, [dataElementTypes.DATE]: convertDateForListDisplay, - [dataElementTypes.DATE_RANGE]: value => convertRangeForDisplay(convertDateForListDisplay, value), + [dataElementTypes.DATE_RANGE]: value => convertRangeForDisplay(undefined, value), [dataElementTypes.DATETIME]: convertDateTimeForListDisplay, - [dataElementTypes.DATETIME_RANGE]: value => convertRangeForDisplay(convertDateTimeForListDisplay, value), + [dataElementTypes.DATETIME_RANGE]: value => convertRangeForDisplay(undefined, value), [dataElementTypes.TIME]: convertTimeForListDisplay, [dataElementTypes.TIME_RANGE]: value => convertRangeForDisplay(convertTimeForListDisplay, value), [dataElementTypes.TRUE_ONLY]: () => i18n.t('Yes'), From 217dc2509ee7eb5814eff48ce6d46e7ede6b6fa1 Mon Sep 17 00:00:00 2001 From: alaa-yahia Date: Thu, 16 Jan 2025 02:19:37 +0200 Subject: [PATCH 24/42] fix: enrollment and incident date display in iso --- .../components/WidgetEnrollment/Date/Date.component.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/Date/Date.component.js b/src/core_modules/capture-core/components/WidgetEnrollment/Date/Date.component.js index 1b8dc31ee1..5d4f6e83b8 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/Date/Date.component.js +++ b/src/core_modules/capture-core/components/WidgetEnrollment/Date/Date.component.js @@ -10,6 +10,7 @@ import { } from '@dhis2/ui'; import i18n from '@dhis2/d2-i18n'; import { withStyles } from '@material-ui/core'; +import { systemSettingsStore } from '../../../metaDataMemoryStores'; import { convertValue as convertValueClientToView } from '../../../converters/clientToView'; import { convertValue as convertValueFormToClient } from '../../../converters/formToClient'; import { convertValue as convertValueClientToServer } from '../../../converters/clientToServer'; @@ -114,6 +115,8 @@ const DateComponentPlain = ({ label={dateLabel} dense locale={locale} + calendarType={systemSettingsStore.get().calendar} + dateFormat={systemSettingsStore.get().dateFormat} />
{validation && validation.error ? i18n.t('Please provide a valid date') : ''} From d05011a73441b58cd3310a659d50fbdb9db9e7b8 Mon Sep 17 00:00:00 2001 From: alaa-yahia Date: Thu, 16 Jan 2025 03:01:51 +0200 Subject: [PATCH 25/42] fix: runtime error when selecting a date in age field in ethiopian calendar --- .../date/temporalToString.js | 4 +++- .../capture-ui/AgeField/AgeField.component.js | 23 +++++++++++-------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/core_modules/capture-core-utils/date/temporalToString.js b/src/core_modules/capture-core-utils/date/temporalToString.js index 72f650b149..69caedbe7a 100644 --- a/src/core_modules/capture-core-utils/date/temporalToString.js +++ b/src/core_modules/capture-core-utils/date/temporalToString.js @@ -1,4 +1,5 @@ // @flow +import { systemSettingsStore } from 'capture-core/metaDataMemoryStores'; import { padWithZeros } from './padWithZeros'; /** @@ -20,7 +21,8 @@ export function temporalToString(temporalDate: PlainDate | null, dateFormat: ?st } try { - const year = temporalDate.year; + const calendar = systemSettingsStore.get().calendar; + const year = calendar === 'ethiopian' ? temporalDate.eraYear : temporalDate.year; const month = temporalDate.month; const day = temporalDate.day; diff --git a/src/core_modules/capture-ui/AgeField/AgeField.component.js b/src/core_modules/capture-ui/AgeField/AgeField.component.js index f2b96b19a5..1684c97f8a 100644 --- a/src/core_modules/capture-ui/AgeField/AgeField.component.js +++ b/src/core_modules/capture-ui/AgeField/AgeField.component.js @@ -1,6 +1,10 @@ // @flow import React, { Component } from 'react'; import { Temporal } from '@js-temporal/polyfill'; +import { + convertFromIso8601, + convertToIso8601, +} from '@dhis2/multi-calendar-dates'; import { isValidPositiveInteger } from 'capture-core-utils/validators/form'; import i18n from '@dhis2/d2-i18n'; import classNames from 'classnames'; @@ -11,7 +15,7 @@ import { AgeDateInput } from '../internal/AgeInput/AgeDateInput.component'; import defaultClasses from './ageField.module.css'; import { orientations } from '../constants/orientations.const'; import { withInternalChangeHandler } from '../HOC/withInternalChangeHandler'; -import { stringToTemporal, temporalToString } from '../../capture-core-utils/date'; +import { temporalToString } from '../../capture-core-utils/date'; type AgeValues = { date?: ?string, @@ -56,19 +60,17 @@ type Props = { }; function getCalculatedValues(dateValue: ?string, calendarType: ?string, dateFormat: ?string): AgeValues { - const now = Temporal.Now.plainDateISO().withCalendar(calendarType); + const nowIso = Temporal.Now.plainDateISO(); - const age = stringToTemporal(dateValue, calendarType, dateFormat); + const ageIso = convertToIso8601(dateValue, calendarType); - const diff = now.since(age, { + const diff = nowIso.since(ageIso, { largestUnit: 'years', smallestUnit: 'days', }); - const date = temporalToString(age, dateFormat); - return { - date, + date: dateValue, years: diff.years.toString(), months: diff.months.toString(), days: diff.days.toString(), @@ -118,14 +120,15 @@ class D2AgeFieldPlain extends Component { return; } - const now = Temporal.Now.plainDateISO().withCalendar(calendarType); + const nowIso = Temporal.Now.plainDateISO(); - const calculatedDate = now.subtract({ + const calculatedDateIso = nowIso.subtract({ years: D2AgeFieldPlain.getNumberOrZero(values.years), months: D2AgeFieldPlain.getNumberOrZero(values.months), days: D2AgeFieldPlain.getNumberOrZero(values.days), }); - const dateString = temporalToString(calculatedDate, dateFormat); + const localCalculatedDate = convertFromIso8601(calculatedDateIso.toString(), calendarType); + const dateString = temporalToString(localCalculatedDate, dateFormat); const calculatedValues = getCalculatedValues(dateString, calendarType, dateFormat); this.props.onBlur(calculatedValues); } From fbdfe2039c50961222b8a341c4d661062b2bf34e Mon Sep 17 00:00:00 2001 From: alaa-yahia Date: Tue, 21 Jan 2025 06:36:56 +0200 Subject: [PATCH 26/42] fix: use localDate in chip component --- .../components/WidgetEnrollment/WidgetEnrollment.component.js | 2 +- .../components/WidgetNote/NoteSection/NoteSection.js | 2 +- .../Stages/Stage/StageOverview/StageOverview.component.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.js b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.js index 66b9321c2a..b5eae0842b 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.js +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.js @@ -158,7 +158,7 @@ export const WidgetEnrollmentPlain = ({ {i18n.t('Last updated')} - + {moment(fromServerDate(enrollment.updatedAt)).fromNow()}
diff --git a/src/core_modules/capture-core/components/WidgetNote/NoteSection/NoteSection.js b/src/core_modules/capture-core/components/WidgetNote/NoteSection/NoteSection.js index 825d27d527..0069b42f3e 100644 --- a/src/core_modules/capture-core/components/WidgetNote/NoteSection/NoteSection.js +++ b/src/core_modules/capture-core/components/WidgetNote/NoteSection/NoteSection.js @@ -110,7 +110,7 @@ const NoteSectionPlain = ({ {createdBy.firstName} {' '} {createdBy.surname}
} - + {moment(fromServerDate(storedAt)).fromNow()} diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.js index 5c72a76670..a9e6a225fa 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.js +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.js @@ -78,7 +78,7 @@ const getLastUpdatedAt = (events, fromServerDate) => { ? ( <> {i18n.t('Last updated')}  - + {moment(fromServerDate(updatedAt)).fromNow()} From 4ceacde6c84b3b90a2dc8b58d5779329e4fbf4db Mon Sep 17 00:00:00 2001 From: alaa-yahia Date: Thu, 23 Jan 2025 11:22:03 +0200 Subject: [PATCH 27/42] fix: revert to iso date calculations when calendar isn't supported --- .../capture-core-utils/date/index.js | 1 + .../date/isCalendarSupported.js | 10 ++++ .../date/stringToTemporal.js | 3 +- .../capture-ui/AgeField/AgeField.component.js | 57 ++++++++++++++----- 4 files changed, 57 insertions(+), 14 deletions(-) create mode 100644 src/core_modules/capture-core-utils/date/isCalendarSupported.js diff --git a/src/core_modules/capture-core-utils/date/index.js b/src/core_modules/capture-core-utils/date/index.js index f29e3a8760..d21873de1f 100644 --- a/src/core_modules/capture-core-utils/date/index.js +++ b/src/core_modules/capture-core-utils/date/index.js @@ -3,3 +3,4 @@ export { getFormattedStringFromMomentUsingEuropeanGlyphs } from './date.utils'; export { padWithZeros } from './padWithZeros'; export { temporalToString } from './temporalToString'; export { stringToTemporal } from './stringToTemporal'; +export { isCalendarSupported } from './isCalendarSupported'; diff --git a/src/core_modules/capture-core-utils/date/isCalendarSupported.js b/src/core_modules/capture-core-utils/date/isCalendarSupported.js new file mode 100644 index 0000000000..691a09cadf --- /dev/null +++ b/src/core_modules/capture-core-utils/date/isCalendarSupported.js @@ -0,0 +1,10 @@ +const supportedTemporalCalendars = ['coptic', 'gregory', 'islamic', 'persian', 'islamic-umalqura', 'islamic-tbla', + 'islamic-civil', 'islamic-rgsa', 'hebrew', 'chinese', 'indian', 'buddhist', 'japanese', 'roc', 'dangi']; + +/** + * Checks if a calendar is supported by Temporal. + * + * @param {string} calendar - The calendar to check (e.g., 'islamic') + * @returns {boolean} - True if the calendar is supported, false otherwise. + */ +export const isCalendarSupported = calendar => supportedTemporalCalendars.includes(calendar); diff --git a/src/core_modules/capture-core-utils/date/stringToTemporal.js b/src/core_modules/capture-core-utils/date/stringToTemporal.js index 3be34f0775..38d62a12c2 100644 --- a/src/core_modules/capture-core-utils/date/stringToTemporal.js +++ b/src/core_modules/capture-core-utils/date/stringToTemporal.js @@ -13,7 +13,8 @@ import { Temporal } from '@js-temporal/polyfill'; type PlainDate = { year: number, month: number, - day: number + day: number, + eraYear: number, }; export function stringToTemporal(dateString: ?string, diff --git a/src/core_modules/capture-ui/AgeField/AgeField.component.js b/src/core_modules/capture-ui/AgeField/AgeField.component.js index 215e51ea87..245dbbc0fa 100644 --- a/src/core_modules/capture-ui/AgeField/AgeField.component.js +++ b/src/core_modules/capture-ui/AgeField/AgeField.component.js @@ -15,7 +15,7 @@ import { AgeDateInput } from '../internal/AgeInput/AgeDateInput.component'; import defaultClasses from './ageField.module.css'; import { orientations } from '../constants/orientations.const'; import { withInternalChangeHandler } from '../HOC/withInternalChangeHandler'; -import { temporalToString } from '../../capture-core-utils/date'; +import { stringToTemporal, temporalToString, isCalendarSupported } from '../../capture-core-utils/date'; type AgeValues = { date?: ?string, @@ -59,24 +59,42 @@ type Props = { calendarType: ?string, }; -function getCalculatedValues(dateValue: ?string, calendarType: ?string): AgeValues { - const nowIso = Temporal.Now.plainDateISO(); +function getCalculatedValues(dateValue: ?string, calendarType: ?string, dateFormat: ?string): AgeValues { + if (!isCalendarSupported(calendarType)) { + const nowIso = Temporal.Now.plainDateISO(); + + const ageIso = convertToIso8601(dateValue, calendarType); - const ageIso = convertToIso8601(dateValue, calendarType); + const diff = nowIso.since(ageIso, { + largestUnit: 'years', + smallestUnit: 'days', + }); - const diff = nowIso.since(ageIso, { + return { + date: dateValue, + years: diff.years.toString(), + months: diff.months.toString(), + days: diff.days.toString(), + }; + } + const age = stringToTemporal(dateValue, calendarType, dateFormat); + const now = Temporal.Now.plainDateISO().withCalendar(calendarType); + const diff = now.since(age, { largestUnit: 'years', smallestUnit: 'days', }); + const date = temporalToString(age, dateFormat); + return { - date: dateValue, + date, years: diff.years.toString(), months: diff.months.toString(), days: diff.days.toString(), }; } + const messageTypeClass = { error: 'innerInputError', info: 'innerInputInfo', @@ -120,21 +138,34 @@ class D2AgeFieldPlain extends Component { return; } - const nowIso = Temporal.Now.plainDateISO(); + if (!isCalendarSupported(calendarType)) { + const nowIso = Temporal.Now.plainDateISO(); + const calculatedDateIso = nowIso.subtract({ + years: D2AgeFieldPlain.getNumberOrZero(values.years), + months: D2AgeFieldPlain.getNumberOrZero(values.months), + days: D2AgeFieldPlain.getNumberOrZero(values.days), + }); + + const localCalculatedDate = convertFromIso8601(calculatedDateIso.toString(), calendarType); + const dateString = temporalToString(localCalculatedDate, dateFormat); + const calculatedValues = getCalculatedValues(dateString, calendarType, dateFormat); + this.props.onBlur(calculatedValues); + return; + } - const calculatedDateIso = nowIso.subtract({ + const now = Temporal.Now.plainDateISO().withCalendar(calendarType); + const calculatedDate = now.subtract({ years: D2AgeFieldPlain.getNumberOrZero(values.years), months: D2AgeFieldPlain.getNumberOrZero(values.months), days: D2AgeFieldPlain.getNumberOrZero(values.days), }); - const localCalculatedDate = convertFromIso8601(calculatedDateIso.toString(), calendarType); - const dateString = temporalToString(localCalculatedDate, dateFormat); - const calculatedValues = getCalculatedValues(dateString, calendarType); + const dateString = temporalToString(calculatedDate, dateFormat); + const calculatedValues = getCalculatedValues(dateString, calendarType, dateFormat); this.props.onBlur(calculatedValues); } handleDateBlur = (date: ?string, options: ?ValidationOptions) => { - const { onRemoveFocus, calendarType = 'gregory' } = this.props; + const { onRemoveFocus, calendarType = 'iso8601', dateFormat = 'YYYY-MM-DD' } = this.props; onRemoveFocus && onRemoveFocus(); const isDateValid = options && !options.error; if (!date) { @@ -151,7 +182,7 @@ class D2AgeFieldPlain extends Component { this.props.onBlur(calculatedValues, options); return; } - const calculatedValues = getCalculatedValues(date, calendarType); + const calculatedValues = getCalculatedValues(date, calendarType, dateFormat); this.props.onBlur(calculatedValues, options); } From 79102e20e125d3d187f20951d7eba69d59ae8933 Mon Sep 17 00:00:00 2001 From: alaa-yahia Date: Fri, 24 Jan 2025 11:37:24 +0200 Subject: [PATCH 28/42] fix: age calculations --- .../capture-core-utils/date/index.js | 1 + .../date/isCalendarSupported.js | 4 +- .../date/mapDhis2CalendarToTemporal.js | 17 +++++ .../date/stringToTemporal.js | 7 +- .../capture-ui/AgeField/AgeField.component.js | 65 +++++++++++++------ 5 files changed, 71 insertions(+), 23 deletions(-) create mode 100644 src/core_modules/capture-core-utils/date/mapDhis2CalendarToTemporal.js diff --git a/src/core_modules/capture-core-utils/date/index.js b/src/core_modules/capture-core-utils/date/index.js index d21873de1f..fdb9dd94e3 100644 --- a/src/core_modules/capture-core-utils/date/index.js +++ b/src/core_modules/capture-core-utils/date/index.js @@ -3,4 +3,5 @@ export { getFormattedStringFromMomentUsingEuropeanGlyphs } from './date.utils'; export { padWithZeros } from './padWithZeros'; export { temporalToString } from './temporalToString'; export { stringToTemporal } from './stringToTemporal'; +export { mapDhis2CalendarToTemporal } from './mapDhis2CalendarToTemporal'; export { isCalendarSupported } from './isCalendarSupported'; diff --git a/src/core_modules/capture-core-utils/date/isCalendarSupported.js b/src/core_modules/capture-core-utils/date/isCalendarSupported.js index 691a09cadf..5c4c8ab19b 100644 --- a/src/core_modules/capture-core-utils/date/isCalendarSupported.js +++ b/src/core_modules/capture-core-utils/date/isCalendarSupported.js @@ -1,10 +1,10 @@ -const supportedTemporalCalendars = ['coptic', 'gregory', 'islamic', 'persian', 'islamic-umalqura', 'islamic-tbla', +const supportedTemporalCalendars = ['coptic', 'gregory', 'ethiopic', 'islamic', 'persian', 'islamic-umalqura', 'islamic-tbla', 'islamic-civil', 'islamic-rgsa', 'hebrew', 'chinese', 'indian', 'buddhist', 'japanese', 'roc', 'dangi']; /** * Checks if a calendar is supported by Temporal. * - * @param {string} calendar - The calendar to check (e.g., 'islamic') + * @param {string} calendar - The calendar to check in temporal names (e.g., 'islamic') * @returns {boolean} - True if the calendar is supported, false otherwise. */ export const isCalendarSupported = calendar => supportedTemporalCalendars.includes(calendar); diff --git a/src/core_modules/capture-core-utils/date/mapDhis2CalendarToTemporal.js b/src/core_modules/capture-core-utils/date/mapDhis2CalendarToTemporal.js new file mode 100644 index 0000000000..9986de88b9 --- /dev/null +++ b/src/core_modules/capture-core-utils/date/mapDhis2CalendarToTemporal.js @@ -0,0 +1,17 @@ +// @flow + +/** + * A mapping of DHIS2 calendar system names to Temporal API calendar names. + * + * @typedef {string} SupportedCalendar - Temporal-compatible calendar identifier + */ +export const mapDhis2CalendarToTemporal = { + ethiopian: 'ethiopic', + coptic: 'coptic', + gregorian: 'gregory', + islamic: 'islamic', + iso8601: 'iso8601', + nepali: 'nepali', + thai: 'buddhist', + persian: 'persian', +}; diff --git a/src/core_modules/capture-core-utils/date/stringToTemporal.js b/src/core_modules/capture-core-utils/date/stringToTemporal.js index 38d62a12c2..c3e74b5318 100644 --- a/src/core_modules/capture-core-utils/date/stringToTemporal.js +++ b/src/core_modules/capture-core-utils/date/stringToTemporal.js @@ -10,16 +10,19 @@ import { Temporal } from '@js-temporal/polyfill'; * @returns {(Temporal.PlainDate | null)} */ -type PlainDate = { +type PlainDateType = { year: number, month: number, day: number, eraYear: number, + with: (fields: { year?: number }) => PlainDateType, + since: (other: PlainDateType, options: { largestUnit: string, smallestUnit: string }) => + { years: number, months: number, days: number } }; export function stringToTemporal(dateString: ?string, calendar: ?string, - dateFormat: ?string): PlainDate | null { + dateFormat: ?string): PlainDateType | null { if (!dateString) { return null; } diff --git a/src/core_modules/capture-ui/AgeField/AgeField.component.js b/src/core_modules/capture-ui/AgeField/AgeField.component.js index 245dbbc0fa..076e40ae64 100644 --- a/src/core_modules/capture-ui/AgeField/AgeField.component.js +++ b/src/core_modules/capture-ui/AgeField/AgeField.component.js @@ -15,7 +15,7 @@ import { AgeDateInput } from '../internal/AgeInput/AgeDateInput.component'; import defaultClasses from './ageField.module.css'; import { orientations } from '../constants/orientations.const'; import { withInternalChangeHandler } from '../HOC/withInternalChangeHandler'; -import { stringToTemporal, temporalToString, isCalendarSupported } from '../../capture-core-utils/date'; +import { stringToTemporal, temporalToString, mapDhis2CalendarToTemporal, isCalendarSupported } from '../../capture-core-utils/date'; type AgeValues = { date?: ?string, @@ -59,8 +59,8 @@ type Props = { calendarType: ?string, }; -function getCalculatedValues(dateValue: ?string, calendarType: ?string, dateFormat: ?string): AgeValues { - if (!isCalendarSupported(calendarType)) { +function getCalculatedValues(dateValue: ?string, calendarType: string, dateFormat: string): AgeValues { + if (!isCalendarSupported(mapDhis2CalendarToTemporal[calendarType])) { const nowIso = Temporal.Now.plainDateISO(); const ageIso = convertToIso8601(dateValue, calendarType); @@ -77,17 +77,41 @@ function getCalculatedValues(dateValue: ?string, calendarType: ?string, dateForm days: diff.days.toString(), }; } - const age = stringToTemporal(dateValue, calendarType, dateFormat); - const now = Temporal.Now.plainDateISO().withCalendar(calendarType); + const now = Temporal.Now.plainDateISO().withCalendar(mapDhis2CalendarToTemporal[calendarType]); + const age = stringToTemporal(dateValue, mapDhis2CalendarToTemporal[calendarType], dateFormat); + + if (calendarType === 'ethiopian') { + if (!age || !age.eraYear) { + return { + date: dateValue, + years: '', + months: '', + days: '', + }; + } + const ethiopianNow = now.with({ year: now.eraYear }); + const ethiopianAge = age.with({ year: age.eraYear }); + + const diff = ethiopianNow.since(ethiopianAge, { + largestUnit: 'years', + smallestUnit: 'days', + }); + + return { + date: temporalToString(ethiopianAge, dateFormat), + years: diff.years.toString(), + months: diff.months.toString(), + days: diff.days.toString(), + }; + } + const diff = now.since(age, { largestUnit: 'years', smallestUnit: 'days', }); - const date = temporalToString(age, dateFormat); - return { - date, + date: temporalToString(age, dateFormat), years: diff.years.toString(), months: diff.months.toString(), days: diff.days.toString(), @@ -125,8 +149,9 @@ class D2AgeFieldPlain extends Component { } handleNumberBlur = (values: AgeValues) => { - const { onRemoveFocus, calendarType = 'gregory', dateFormat = 'YYYY-MM-DD' } = this.props; - + const { onRemoveFocus, calendarType, dateFormat } = this.props; + const calendar = calendarType || 'iso8601'; + const format = dateFormat || 'YYYY-MM-DD'; onRemoveFocus && onRemoveFocus(); if (D2AgeFieldPlain.isEmptyNumbers(values)) { this.props.onBlur(values.date ? { date: values.date } : null); @@ -138,7 +163,7 @@ class D2AgeFieldPlain extends Component { return; } - if (!isCalendarSupported(calendarType)) { + if (!isCalendarSupported(mapDhis2CalendarToTemporal[calendar])) { const nowIso = Temporal.Now.plainDateISO(); const calculatedDateIso = nowIso.subtract({ years: D2AgeFieldPlain.getNumberOrZero(values.years), @@ -146,26 +171,28 @@ class D2AgeFieldPlain extends Component { days: D2AgeFieldPlain.getNumberOrZero(values.days), }); - const localCalculatedDate = convertFromIso8601(calculatedDateIso.toString(), calendarType); - const dateString = temporalToString(localCalculatedDate, dateFormat); - const calculatedValues = getCalculatedValues(dateString, calendarType, dateFormat); + const localCalculatedDate = convertFromIso8601(calculatedDateIso.toString(), calendar); + const dateString = temporalToString(localCalculatedDate, format); + const calculatedValues = getCalculatedValues(dateString, calendar, format); this.props.onBlur(calculatedValues); return; } - const now = Temporal.Now.plainDateISO().withCalendar(calendarType); + const now = Temporal.Now.plainDateISO().withCalendar(mapDhis2CalendarToTemporal[calendar]); const calculatedDate = now.subtract({ years: D2AgeFieldPlain.getNumberOrZero(values.years), months: D2AgeFieldPlain.getNumberOrZero(values.months), days: D2AgeFieldPlain.getNumberOrZero(values.days), }); - const dateString = temporalToString(calculatedDate, dateFormat); - const calculatedValues = getCalculatedValues(dateString, calendarType, dateFormat); + const dateString = temporalToString(calculatedDate, format); + const calculatedValues = getCalculatedValues(dateString, calendar, format); this.props.onBlur(calculatedValues); } handleDateBlur = (date: ?string, options: ?ValidationOptions) => { - const { onRemoveFocus, calendarType = 'iso8601', dateFormat = 'YYYY-MM-DD' } = this.props; + const { onRemoveFocus, calendarType, dateFormat } = this.props; + const calendar = calendarType || 'iso8601'; + const format = dateFormat || 'YYYY-MM-DD'; onRemoveFocus && onRemoveFocus(); const isDateValid = options && !options.error; if (!date) { @@ -182,7 +209,7 @@ class D2AgeFieldPlain extends Component { this.props.onBlur(calculatedValues, options); return; } - const calculatedValues = getCalculatedValues(date, calendarType, dateFormat); + const calculatedValues = getCalculatedValues(date, calendar, format); this.props.onBlur(calculatedValues, options); } From 3493da4c8a29332163faf8eaa87ae214f25142d1 Mon Sep 17 00:00:00 2001 From: alaa-yahia Date: Tue, 28 Jan 2025 03:25:04 +0200 Subject: [PATCH 29/42] fix: age and dateTimeRange searchterms --- .../SearchForm/SearchForm.container.js | 19 +++++- .../form/getDateTimeRangeValidator.js | 62 ++++++++++++++----- 2 files changed, 64 insertions(+), 17 deletions(-) diff --git a/src/core_modules/capture-core/components/SearchBox/SearchForm/SearchForm.container.js b/src/core_modules/capture-core/components/SearchBox/SearchForm/SearchForm.container.js index 7628f46705..5a40fadd62 100644 --- a/src/core_modules/capture-core/components/SearchBox/SearchForm/SearchForm.container.js +++ b/src/core_modules/capture-core/components/SearchBox/SearchForm/SearchForm.container.js @@ -2,6 +2,7 @@ import { connect } from 'react-redux'; import type { ComponentType } from 'react'; import { isObject, isString } from 'd2-utilizr/src'; +import { convertFormToClient } from 'capture-core/converters'; import { SearchFormComponent } from './SearchForm.component'; import type { CurrentSearchTerms, DispatchersFromRedux, OwnProps, Props, PropsFromRedux } from './SearchForm.types'; import { @@ -14,6 +15,7 @@ import { } from '../SearchBox.actions'; import { addFormData, removeFormData } from '../../D2Form/actions/form.actions'; +// eslint-disable-next-line complexity const isValueContainingCharacter = (value: any) => { if (!value) { return false; @@ -22,6 +24,20 @@ const isValueContainingCharacter = (value: any) => { return Boolean(value.replace(/\s/g, '').length); } + if (isObject(value) && ('from' in value && 'to' in value)) { + const fromValid = value.from && + isObject(value.from) && + value.from.time && + value.from.date; + + const toValid = value.to && + isObject(value.to) && + value.to.time && + value.to.date; + + return fromValid && toValid; + } + if (isObject(value)) { const numberOfValuesWithLength = Object.values(value) .filter(v => isString(v)) @@ -48,7 +64,8 @@ const collectCurrentSearchTerms = (searchGroupsForSelectedScope, formsValues): C const { name, id, type } = attributeSearchForm.getElement(attributeValueKey); const value = searchTerms[attributeValueKey]; if (isValueContainingCharacter(value)) { - return [...accumulated, { name, value, id, type }]; + const convertedValue = convertFormToClient(value, type); + return [...accumulated, { name, value: convertedValue, id, type }]; } return accumulated; }, []); diff --git a/src/core_modules/capture-core/utils/validation/validators/form/getDateTimeRangeValidator.js b/src/core_modules/capture-core/utils/validation/validators/form/getDateTimeRangeValidator.js index 45b497a108..a92c7af0c4 100644 --- a/src/core_modules/capture-core/utils/validation/validators/form/getDateTimeRangeValidator.js +++ b/src/core_modules/capture-core/utils/validation/validators/form/getDateTimeRangeValidator.js @@ -6,33 +6,54 @@ import { isValidDateTime } from './dateTimeValidator'; function isValidDateTimeWithEmptyCheck(value: ?Object) { return value && isValidDateTime(value); } -const convertDateTimeToTemporal = (value: Object) => { +/* eslint-disable complexity */ +const convertDateTimeToTemporal = (value: ?Object) => { + if (!value || !value.date || !value.time) { + return null; + } + const { date, time } = value; - const dateInTemporal = convertStringToTemporal(date); + const dateInTemporalFormat = convertStringToTemporal(date); - if (!dateInTemporal) { + if (!dateInTemporalFormat) { return null; } - const { year, month, day } = dateInTemporal; + const { year, month, day } = dateInTemporalFormat; let hour; let minutes; - if (/[:.]/.test(time)) { - [hour, minutes] = time.split(/[:.]/).map(Number); - } else if (time.length === 3) { - hour = Number(time.substring(0, 1)); - minutes = Number(time.substring(2, 3)); - } else { - hour = Number(time.substring(0, 2)); - minutes = Number(time.substring(3, 4)); - } + try { + if (/[:.]/.test(time)) { + [hour, minutes] = time.split(/[:.]/).map(Number); + } else if (time.length === 3) { + hour = Number(time.substring(0, 1)); + minutes = Number(time.substring(1, 3)); + } else if (time.length === 4) { + hour = Number(time.substring(0, 2)); + minutes = Number(time.substring(2, 4)); + } else { + return null; + } + + if (isNaN(hour) || isNaN(minutes) || hour < 0 || hour > 23 || minutes < 0 || minutes > 59) { + return null; + } - return new Temporal.PlainDateTime(year, month, day, hour, minutes); + return new Temporal.PlainDateTime(year, month, day, hour, minutes); + } catch (error) { + return null; + } }; export const getDateTimeRangeValidator = (invalidDateTimeMessage: string) => (value: { from?: ?Object, to?: ?Object }) => { + if (!value) { + return { + valid: false, + errorMessage: { from: invalidDateTimeMessage, to: invalidDateTimeMessage }, + }; + } const errorResult = []; if (!isValidDateTimeWithEmptyCheck(value.from)) { errorResult.push({ from: invalidDateTimeMessage }); @@ -53,7 +74,16 @@ export const getDateTimeRangeValidator = (invalidDateTimeMessage: string) => const fromDateTime = convertDateTimeToTemporal(value.from); const toDateTime = convertDateTimeToTemporal(value.to); if (!fromDateTime || !toDateTime) { - return false; + return { + valid: false, + errorMessage: { + from: !fromDateTime ? invalidDateTimeMessage : undefined, + to: !toDateTime ? invalidDateTimeMessage : undefined, + }, + }; } - return Temporal.PlainDateTime.compare(fromDateTime, toDateTime) <= 0; + return { + valid: Temporal.PlainDateTime.compare(fromDateTime, toDateTime) <= 0, + errorMessage: undefined, + }; }; From 79f33683bb9292e9929fd348c6be020749541974 Mon Sep 17 00:00:00 2001 From: alaa-yahia Date: Tue, 28 Jan 2025 03:27:05 +0200 Subject: [PATCH 30/42] fix: invalidDate in event notes --- .../components/WidgetEventNote/WidgetEventNote.epics.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.epics.js b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.epics.js index 8aae3f0f24..315c0dba5b 100644 --- a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.epics.js +++ b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.epics.js @@ -5,8 +5,6 @@ import { featureAvailable, FEATURES } from 'capture-core-utils'; import { map, switchMap } from 'rxjs/operators'; import uuid from 'd2-utilizr/lib/uuid'; import moment from 'moment'; -import { convertValue as convertListValue } from '../../converters/clientToList'; -import { dataElementTypes } from '../../metaData'; import { actionTypes, batchActionTypes, startAddNoteForEvent } from './WidgetEventNote.actions'; import { @@ -58,7 +56,7 @@ export const addNoteForEventEpic = (action$: InputObservable, store: ReduxStore, }; const formNote = { ...clientNote, - storedAt: convertListValue(clientNote.storedAt, dataElementTypes.DATETIME), + storedAt: clientNote.storedAt, createdBy: { firstName, surname, From 8aee1e6265bb0db0cbba5a57f6554a74e96a7b54 Mon Sep 17 00:00:00 2001 From: alaa-yahia Date: Wed, 29 Jan 2025 20:43:33 +0200 Subject: [PATCH 31/42] fix: dateTime search query --- .../SearchForm/SearchForm.container.js | 29 ++++++++++--------- .../SearchFormElementConverter.js | 3 +- .../capture-core/converters/clientToList.js | 4 +-- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/core_modules/capture-core/components/SearchBox/SearchForm/SearchForm.container.js b/src/core_modules/capture-core/components/SearchBox/SearchForm/SearchForm.container.js index 5a40fadd62..f2fc157af6 100644 --- a/src/core_modules/capture-core/components/SearchBox/SearchForm/SearchForm.container.js +++ b/src/core_modules/capture-core/components/SearchBox/SearchForm/SearchForm.container.js @@ -15,7 +15,6 @@ import { } from '../SearchBox.actions'; import { addFormData, removeFormData } from '../../D2Form/actions/form.actions'; -// eslint-disable-next-line complexity const isValueContainingCharacter = (value: any) => { if (!value) { return false; @@ -24,21 +23,25 @@ const isValueContainingCharacter = (value: any) => { return Boolean(value.replace(/\s/g, '').length); } - if (isObject(value) && ('from' in value && 'to' in value)) { - const fromValid = value.from && - isObject(value.from) && - value.from.time && - value.from.date; + if (isObject(value)) { + if ('from' in value && 'to' in value) { + const fromValues = isObject(value.from) ? + [value.from.time, value.from.date] : + [value.from]; - const toValid = value.to && - isObject(value.to) && - value.to.time && - value.to.date; + const toValues = isObject(value.to) ? + [value.to.time, value.to.date] : + [value.to]; - return fromValid && toValid; - } + const allValues = [...fromValues, ...toValues]; + const validValues = allValues + .filter(v => isString(v)) + .filter(v => Boolean(v?.replace(/\s/g, '').length)) + .length; + + return validValues === allValues.length; + } - if (isObject(value)) { const numberOfValuesWithLength = Object.values(value) .filter(v => isString(v)) .filter((v: any) => Boolean(v.replace(/\s/g, '').length)) diff --git a/src/core_modules/capture-core/components/SearchBox/SearchForm/SearchFormElementConverter/SearchFormElementConverter.js b/src/core_modules/capture-core/components/SearchBox/SearchForm/SearchFormElementConverter/SearchFormElementConverter.js index 5153fc5454..d887acf182 100644 --- a/src/core_modules/capture-core/components/SearchBox/SearchForm/SearchFormElementConverter/SearchFormElementConverter.js +++ b/src/core_modules/capture-core/components/SearchBox/SearchForm/SearchFormElementConverter/SearchFormElementConverter.js @@ -22,7 +22,8 @@ const convertRange = (formValues: FormValues, dataElement: DataElement) => { const convertedFrom = from && (dataElement.convertValue(from, pipeD2(convertFormToClient, convertClientToServer))); const convertedTo = to && (dataElement.convertValue(to, pipeD2(convertFormToClient, convertClientToServer))); if (from || to) { - return `${dataElement.id}${convertedFrom ? (`:ge:${convertedFrom}`) : ''}${convertedTo ? (`:le:${convertedTo}`) : ''}`; + return `${dataElement.id}${convertedFrom ? `:ge:${escapeString(String(convertedFrom))}` + : ''}${convertedTo ? `:le:${escapeString(String(convertedTo))}` : ''}`; } return null; }; diff --git a/src/core_modules/capture-core/converters/clientToList.js b/src/core_modules/capture-core/converters/clientToList.js index d148d1f871..18359666b7 100644 --- a/src/core_modules/capture-core/converters/clientToList.js +++ b/src/core_modules/capture-core/converters/clientToList.js @@ -103,9 +103,9 @@ const valueConvertersForType = { [dataElementTypes.BOOLEAN]: (rawValue: boolean) => (rawValue ? i18n.t('Yes') : i18n.t('No')), [dataElementTypes.COORDINATE]: MinimalCoordinates, [dataElementTypes.DATE]: convertDateForListDisplay, - [dataElementTypes.DATE_RANGE]: value => convertRangeForDisplay(undefined, value), + [dataElementTypes.DATE_RANGE]: value => convertRangeForDisplay(convertIsoToLocalCalendar, value), [dataElementTypes.DATETIME]: convertDateTimeForListDisplay, - [dataElementTypes.DATETIME_RANGE]: value => convertRangeForDisplay(undefined, value), + [dataElementTypes.DATETIME_RANGE]: value => convertRangeForDisplay(convertIsoToLocalCalendar, value), [dataElementTypes.FILE_RESOURCE]: convertFileForDisplay, [dataElementTypes.IMAGE]: convertImageForDisplay, [dataElementTypes.INTEGER]: stringifyNumber, From ea0aca4079f7863338c5d2d0271428f05b43c8b9 Mon Sep 17 00:00:00 2001 From: alaa-yahia Date: Wed, 29 Jan 2025 20:48:49 +0200 Subject: [PATCH 32/42] fix: dateRange validator --- .../dateRangeField/getDateRangeFieldConfig.js | 6 +- .../utils/validation/validateValue.js | 2 +- .../validators/form/getDateRangeValidator.js | 21 ++++--- .../DateRangeField.component.js | 57 ++++++++++++++----- 4 files changed, 58 insertions(+), 28 deletions(-) diff --git a/src/core_modules/capture-core/components/D2Form/field/configs/dateRangeField/getDateRangeFieldConfig.js b/src/core_modules/capture-core/components/D2Form/field/configs/dateRangeField/getDateRangeFieldConfig.js index 90fec23265..9f2aa22cc6 100644 --- a/src/core_modules/capture-core/components/D2Form/field/configs/dateRangeField/getDateRangeFieldConfig.js +++ b/src/core_modules/capture-core/components/D2Form/field/configs/dateRangeField/getDateRangeFieldConfig.js @@ -11,9 +11,9 @@ export const getDateRangeFieldConfig = (metaData: MetaDataElement, options: Obje const props = createProps({ formHorizontal: options.formHorizontal, fieldLabelMediaBasedClass: options.fieldLabelMediaBasedClass, - width: options.formHorizontal ? 150 : '100%', - maxWidth: options.formHorizontal ? 150 : 350, - calendarWidth: options.formHorizontal ? 250 : 350, + dateWidth: options.formHorizontal ? '150px' : '100%', + dateMaxWidth: options.formHorizontal ? '150px' : '350px', + calendarWidth: options.formHorizontal ? '250px' : '350px', popupAnchorPosition: getCalendarAnchorPosition(options.formHorizontal), calendarType: systemSettingsStore.get().calendar, dateFormat: systemSettingsStore.get().dateFormat, diff --git a/src/core_modules/capture-core/utils/validation/validateValue.js b/src/core_modules/capture-core/utils/validation/validateValue.js index c91bf08fda..50f8c466ec 100644 --- a/src/core_modules/capture-core/utils/validation/validateValue.js +++ b/src/core_modules/capture-core/utils/validation/validateValue.js @@ -33,7 +33,7 @@ export const validateValue = async ({ if (pass === true) { let result = currentValidator.validator( value, - { error: commitOptions?.error, errorCode: commitOptions?.errorCode }, + commitOptions, validationContext, ); if (result instanceof Promise) { diff --git a/src/core_modules/capture-core/utils/validation/validators/form/getDateRangeValidator.js b/src/core_modules/capture-core/utils/validation/validators/form/getDateRangeValidator.js index f54f241d7d..36f6618d0c 100644 --- a/src/core_modules/capture-core/utils/validation/validators/form/getDateRangeValidator.js +++ b/src/core_modules/capture-core/utils/validation/validators/form/getDateRangeValidator.js @@ -1,7 +1,7 @@ // @flow import { Temporal } from '@js-temporal/polyfill'; import { isValidDate } from './dateValidator'; -import { convertStringToDateFormat } from '../../../converters/date'; +import { convertStringToTemporal } from '../../../converters/date'; /** * * @export @@ -9,18 +9,18 @@ import { convertStringToDateFormat } from '../../../converters/date'; * @returns {boolean} */ -function isValidDateWithEmptyCheck(value: ?string) { - return value && isValidDate(value); +function isValidDateWithEmptyCheck(value: ?string, internalError?: ?{error: ?string, errorCode: ?string}) { + return isValidDate(value, internalError); } export const getDateRangeValidator = (invalidDateMessage: string) => - (value: { from?: ?string, to?: ?string}) => { + (value: { from?: ?string, to?: ?string}, internalComponentError?: ?{fromError: ?{error: ?string, errorCode: ?string}, toError: ?{error: ?string, errorCode: ?string}}) => { const errorResult = []; - if (!isValidDateWithEmptyCheck(value.from)) { + if (!isValidDateWithEmptyCheck(value.from, internalComponentError?.fromError).valid) { errorResult.push({ from: invalidDateMessage }); } - if (!isValidDateWithEmptyCheck(value.to)) { + if (!isValidDateWithEmptyCheck(value.to, internalComponentError?.toError).valid) { errorResult.push({ to: invalidDateMessage }); } @@ -33,7 +33,10 @@ export const getDateRangeValidator = (invalidDateMessage: string) => } const { from, to } = value; // $FlowFixMe - const formattedFrom = convertStringToDateFormat(from, 'YYYY-MM-DD'); - const fromattedTo = convertStringToDateFormat(to, 'YYYY-MM-DD'); - return Temporal.PlainDate.compare(formattedFrom, fromattedTo) <= 0; + const formattedFrom = convertStringToTemporal(from); + const fromattedTo = convertStringToTemporal(to); + return { + valid: Temporal.PlainDateTime.compare(formattedFrom, fromattedTo) <= 0, + errorMessage: undefined, + }; }; diff --git a/src/core_modules/capture-ui/DateAndTimeFields/DateRangeField/DateRangeField.component.js b/src/core_modules/capture-ui/DateAndTimeFields/DateRangeField/DateRangeField.component.js index c069700c64..884a9eb8c2 100644 --- a/src/core_modules/capture-ui/DateAndTimeFields/DateRangeField/DateRangeField.component.js +++ b/src/core_modules/capture-ui/DateAndTimeFields/DateRangeField/DateRangeField.component.js @@ -15,6 +15,11 @@ type DateRangeValue = { to?: ?string, } +type State = { + fromError: { error: ?string, errorCode: ?string }, + toError: { error: ?string, errorCode: ?string }, +}; + type Props = { value: DateRangeValue, onBlur: (value: ?DateRangeValue, opts: any) => void, @@ -29,11 +34,15 @@ const inputKeys = { }; -export class DateRangeField extends React.Component { +export class DateRangeField extends React.Component { touchedFields: Set; constructor(props: Props) { super(props); this.touchedFields = new Set(); + this.state = { + fromError: { error: null, errorCode: null }, + toError: { error: null, errorCode: null }, + }; } getValue = () => this.props.value || {}; @@ -52,25 +61,41 @@ export class DateRangeField extends React.Component { }); } - handleFromBlur = (value: string) => { + handleFromBlur = (value: string, options: ?Object) => { this.touchedFields.add('fromTouched'); - const currentValue = this.getValue(); - this.handleBlur({ - from: value, - to: currentValue.to, - }, !!currentValue.to); + this.setState(() => ({ + fromError: { error: options?.error, errorCode: options?.errorCode }, + }), () => { + const currentValue = this.getValue(); + this.handleBlur({ + from: value, + to: currentValue.to, + }, { + touched: !!currentValue.to, + fromError: this.state.fromError, + toError: this.state.toError, + }); + }); } - handleToBlur = (value: string) => { + handleToBlur = (value: string, options: ?Object) => { this.touchedFields.add('toTouched'); - const currentValue = this.getValue(); - this.handleBlur({ - from: currentValue.from, - to: value, - }, !!currentValue.from); + this.setState(() => ({ + toError: { error: options?.error, errorCode: options?.errorCode }, + }), () => { + const currentValue = this.getValue(); + this.handleBlur({ + from: currentValue.from, + to: value, + }, { + touched: !!currentValue.to, + fromError: this.state.fromError, + toError: this.state.toError, + }); + }); } - handleBlur = (value: DateRangeValue, otherFieldHasValue: boolean) => { + handleBlur = (value: DateRangeValue, options: ?Object) => { const touched = this.touchedFields.size === 2; if (!value.from && !value.to) { this.props.onBlur(undefined, { @@ -79,7 +104,9 @@ export class DateRangeField extends React.Component { return; } this.props.onBlur(value, { - touched: touched || otherFieldHasValue, + touched: touched || options?.touched, + fromError: options?.fromError, + toError: options?.toError, }); } From 47ebd75111ea69225619ccc89301e565d7822b71 Mon Sep 17 00:00:00 2001 From: alaa-yahia Date: Fri, 31 Jan 2025 10:30:26 +0200 Subject: [PATCH 33/42] chore: remove convertStringToDateFormat function --- .../Date/DateFilter.component.js | 6 +++--- .../EnrollmentEditEventPage.container.js | 3 ++- .../hooks/useDetermineSuggestedScheduleDate.js | 6 ++---- .../capture-core/utils/converters/date/index.js | 1 - .../converters/date/stringToMomentDateFormat.js | 17 ----------------- 5 files changed, 7 insertions(+), 26 deletions(-) delete mode 100644 src/core_modules/capture-core/utils/converters/date/stringToMomentDateFormat.js diff --git a/src/core_modules/capture-core/components/FiltersForTypes/Date/DateFilter.component.js b/src/core_modules/capture-core/components/FiltersForTypes/Date/DateFilter.component.js index 1e03d9167e..605ca6b6cc 100644 --- a/src/core_modules/capture-core/components/FiltersForTypes/Date/DateFilter.component.js +++ b/src/core_modules/capture-core/components/FiltersForTypes/Date/DateFilter.component.js @@ -17,7 +17,7 @@ import './calendarFilterStyles.css'; import { mainOptionKeys, mainOptionTranslatedTexts } from './options'; import { getDateFilterData } from './dateFilterDataGetter'; import { RangeFilter } from './RangeFilter.component'; -import { convertStringToDateFormat } from '../../../utils/converters/date'; +import { convertStringToTemporal } from '../../../utils/converters/date'; const getStyles = (theme: Theme) => ({ fromToContainer: { @@ -190,8 +190,8 @@ class DateFilterPlain extends Component implements UpdatableFilter } static isFromAfterTo(valueFrom: string, valueTo: string) { - const formattedFrom = convertStringToDateFormat(valueFrom, 'YYYY-MM-DD'); - const fromattedTo = convertStringToDateFormat(valueTo, 'YYYY-MM-DD'); + const formattedFrom = convertStringToTemporal(valueFrom); + const fromattedTo = convertStringToTemporal(valueTo); return Temporal.PlainDate.compare(formattedFrom, fromattedTo) > 0; } diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js index f459cff54d..79d7e14bf0 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js @@ -46,6 +46,7 @@ import { ReactQueryAppNamespace } from '../../../utils/reactQueryHelpers'; import { statusTypes } from '../../../enrollment'; import { cancelEditEventDataEntry } from '../../WidgetEventEdit/EditEventDataEntry/editEventDataEntry.actions'; import { setCurrentDataEntry } from '../../DataEntry/actions/dataEntry.actions'; +import { convertIsoToLocalCalendar } from '../../../utils/converters/date' const getEventDate = (event) => { const eventDataConvertValue = convertDateWithTimeForView(event?.occurredAt || event?.scheduledAt); @@ -55,7 +56,7 @@ const getEventDate = (event) => { const getEventScheduleDate = (event) => { if (!event?.scheduledAt) { return undefined; } - const eventDataConvertValue = convertValue(event?.scheduledAt, dataElementTypes.DATETIME); + const eventDataConvertValue = convertIsoToLocalCalendar(event?.scheduledAt); return eventDataConvertValue?.toString(); }; diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/hooks/useDetermineSuggestedScheduleDate.js b/src/core_modules/capture-core/components/WidgetEventSchedule/hooks/useDetermineSuggestedScheduleDate.js index 455c2f590c..5ad7f42d16 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/hooks/useDetermineSuggestedScheduleDate.js +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/hooks/useDetermineSuggestedScheduleDate.js @@ -2,8 +2,6 @@ import moment from 'moment'; import { convertServerToClient, convertClientToForm } from '../../../converters'; import { dataElementTypes } from '../../../metaData'; -import { convertStringToDateFormat } from '../../../utils/converters/date'; - const convertDate = (date): any => convertServerToClient(date, dataElementTypes.DATE); @@ -84,7 +82,7 @@ export const useDetermineSuggestedScheduleDate = ({ initialScheduleDate, hideDueDate, }: Props) => { - if (initialScheduleDate && !hideDueDate) { return convertStringToDateFormat(initialScheduleDate); } + if (initialScheduleDate && !hideDueDate) { return initialScheduleDate; } if (!programStageScheduleConfig) { return undefined; } const { @@ -127,5 +125,5 @@ export const useDetermineSuggestedScheduleDate = ({ , undefined); // $FlowFixMe dataElementTypes flow error - return convertStringToDateFormat(convertClientToForm(suggestedDate, dataElementTypes.DATE)); + return convertClientToForm(suggestedDate, dataElementTypes.DATE); }; diff --git a/src/core_modules/capture-core/utils/converters/date/index.js b/src/core_modules/capture-core/utils/converters/date/index.js index 6c755de6c3..ec6c4b5690 100644 --- a/src/core_modules/capture-core/utils/converters/date/index.js +++ b/src/core_modules/capture-core/utils/converters/date/index.js @@ -1,7 +1,6 @@ // @flow export { convertDateObjectToDateFormatString } from './dateObjectToDateFormatString'; export { convertMomentToDateFormatString } from './momentToDateFormatString'; -export { convertStringToDateFormat } from './stringToMomentDateFormat'; export { convertIsoToLocalCalendar } from './convertIsoToLocalCalendar'; export { convertLocalToIsoCalendar } from './convertLocalToIsoCalendar'; export { convertStringToTemporal } from './convertStringToTemporal'; diff --git a/src/core_modules/capture-core/utils/converters/date/stringToMomentDateFormat.js b/src/core_modules/capture-core/utils/converters/date/stringToMomentDateFormat.js deleted file mode 100644 index f75fb68255..0000000000 --- a/src/core_modules/capture-core/utils/converters/date/stringToMomentDateFormat.js +++ /dev/null @@ -1,17 +0,0 @@ -// @flow -import moment from 'moment'; -import { systemSettingsStore } from '../../../metaDataMemoryStores'; - -/** - * Converts a string date to a string date with default format based on the system date format - * @export - * @param {*} string - the string instance - * @param {string} [format] - optional date format. If not provided, the function uses system date format - * @returns {string} - */ -export function convertStringToDateFormat(date: ?string, format?: string) { - if (!date || !date.length) { return ''; } - const dateFormat = format || systemSettingsStore.get().dateFormat; - const formattedDateString = moment(date, dateFormat).format(dateFormat); - return formattedDateString; -} From 76af22d16c8dd1d59a448a7e7e3450abbff97c01 Mon Sep 17 00:00:00 2001 From: alaa-yahia Date: Fri, 31 Jan 2025 16:57:44 +0200 Subject: [PATCH 34/42] fix: dateTimeRange validator to recieve internalError --- .../EnrollmentEditEventPage.container.js | 4 +- .../validators/form/dateTimeValidator.js | 2 +- .../validators/form/getDateRangeValidator.js | 2 +- .../form/getDateTimeRangeValidator.js | 35 ++++----- .../DateTimeRange.component.js | 72 +++++++++++++------ 5 files changed, 73 insertions(+), 42 deletions(-) diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js index 79d7e14bf0..5568707b09 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js @@ -24,7 +24,7 @@ import { buildUrlQueryString, useLocationQuery } from '../../../utils/routing'; import { deleteEnrollment, fetchEnrollments } from '../Enrollment/EnrollmentPage.actions'; import { changeEventFromUrl } from '../ViewEvent/ViewEventComponent/viewEvent.actions'; import { buildEnrollmentsAsOptions } from '../../ScopeSelector'; -import { convertDateWithTimeForView, convertValue } from '../../../converters/clientToView'; +import { convertDateWithTimeForView } from '../../../converters/clientToView'; import { dataElementTypes } from '../../../metaData/DataElement'; import { useAssignedUserSaveContext, useAssignee, useEvent } from './hooks'; import type { Props } from './EnrollmentEditEventPage.types'; @@ -46,7 +46,7 @@ import { ReactQueryAppNamespace } from '../../../utils/reactQueryHelpers'; import { statusTypes } from '../../../enrollment'; import { cancelEditEventDataEntry } from '../../WidgetEventEdit/EditEventDataEntry/editEventDataEntry.actions'; import { setCurrentDataEntry } from '../../DataEntry/actions/dataEntry.actions'; -import { convertIsoToLocalCalendar } from '../../../utils/converters/date' +import { convertIsoToLocalCalendar } from '../../../utils/converters/date'; const getEventDate = (event) => { const eventDataConvertValue = convertDateWithTimeForView(event?.occurredAt || event?.scheduledAt); diff --git a/src/core_modules/capture-core/utils/validation/validators/form/dateTimeValidator.js b/src/core_modules/capture-core/utils/validation/validators/form/dateTimeValidator.js index f35bd67c25..0e47945b95 100644 --- a/src/core_modules/capture-core/utils/validation/validators/form/dateTimeValidator.js +++ b/src/core_modules/capture-core/utils/validation/validators/form/dateTimeValidator.js @@ -23,7 +23,7 @@ const CUSTOM_VALIDATION_MESSAGES = { MISSING_DATE: i18n.t('Please enter a date'), }; -export function isValidDateTime(value: DateTimeValue, +export function isValidDateTime(value: ?DateTimeValue, internalComponentError?: ?{error: ?string, errorCode: ?string}): ValidationResult { if (!value) { return { valid: true }; diff --git a/src/core_modules/capture-core/utils/validation/validators/form/getDateRangeValidator.js b/src/core_modules/capture-core/utils/validation/validators/form/getDateRangeValidator.js index 36f6618d0c..856b970b11 100644 --- a/src/core_modules/capture-core/utils/validation/validators/form/getDateRangeValidator.js +++ b/src/core_modules/capture-core/utils/validation/validators/form/getDateRangeValidator.js @@ -36,7 +36,7 @@ export const getDateRangeValidator = (invalidDateMessage: string) => const formattedFrom = convertStringToTemporal(from); const fromattedTo = convertStringToTemporal(to); return { - valid: Temporal.PlainDateTime.compare(formattedFrom, fromattedTo) <= 0, + valid: Temporal.PlainDate.compare(formattedFrom, fromattedTo) <= 0, errorMessage: undefined, }; }; diff --git a/src/core_modules/capture-core/utils/validation/validators/form/getDateTimeRangeValidator.js b/src/core_modules/capture-core/utils/validation/validators/form/getDateTimeRangeValidator.js index a92c7af0c4..a00be8f396 100644 --- a/src/core_modules/capture-core/utils/validation/validators/form/getDateTimeRangeValidator.js +++ b/src/core_modules/capture-core/utils/validation/validators/form/getDateTimeRangeValidator.js @@ -1,10 +1,11 @@ // @flow import { Temporal } from '@js-temporal/polyfill'; +import { systemSettingsStore } from 'capture-core/metaDataMemoryStores'; import { convertStringToTemporal } from 'capture-core/utils/converters/date'; import { isValidDateTime } from './dateTimeValidator'; -function isValidDateTimeWithEmptyCheck(value: ?Object) { - return value && isValidDateTime(value); +function isValidDateTimeWithEmptyCheck(value: ?{date?: ?string, time?: ?string}, internalError?: ?{error: ?string, errorCode: ?string}) { + return isValidDateTime(value, internalError); } /* eslint-disable complexity */ const convertDateTimeToTemporal = (value: ?Object) => { @@ -13,7 +14,7 @@ const convertDateTimeToTemporal = (value: ?Object) => { } const { date, time } = value; - + const calendar = systemSettingsStore.get().calendar; const dateInTemporalFormat = convertStringToTemporal(date); if (!dateInTemporalFormat) { @@ -24,14 +25,15 @@ const convertDateTimeToTemporal = (value: ?Object) => { let hour; let minutes; try { - if (/[:.]/.test(time)) { - [hour, minutes] = time.split(/[:.]/).map(Number); - } else if (time.length === 3) { - hour = Number(time.substring(0, 1)); - minutes = Number(time.substring(1, 3)); - } else if (time.length === 4) { - hour = Number(time.substring(0, 2)); - minutes = Number(time.substring(2, 4)); + const timeStr = time.toString(); + if (/[:.]/.test(timeStr)) { + [hour, minutes] = timeStr.split(/[:.]/).map(Number); + } else if (timeStr.length === 3) { + hour = Number(timeStr.substring(0, 1)); + minutes = Number(timeStr.substring(1, 3)); + } else if (timeStr.length === 4) { + hour = Number(timeStr.substring(0, 2)); + minutes = Number(timeStr.substring(2, 4)); } else { return null; } @@ -40,26 +42,27 @@ const convertDateTimeToTemporal = (value: ?Object) => { return null; } - return new Temporal.PlainDateTime(year, month, day, hour, minutes); + return new Temporal.PlainDateTime(year, month, day, hour, minutes, 0, 0, 0, 0, calendar); } catch (error) { return null; } }; export const getDateTimeRangeValidator = (invalidDateTimeMessage: string) => - (value: { from?: ?Object, to?: ?Object }) => { - if (!value) { + // eslint-disable-next-line complexity + (value: { from?: ?Object, to?: ?Object }, internalComponentError?: ?{fromDateError: ?{error: ?string, errorCode: ?string}, toDateError: ?{error: ?string, errorCode: ?string}}) => { + if (!value?.from && value?.to) { return { valid: false, errorMessage: { from: invalidDateTimeMessage, to: invalidDateTimeMessage }, }; } const errorResult = []; - if (!isValidDateTimeWithEmptyCheck(value.from)) { + if (!isValidDateTimeWithEmptyCheck(value?.from, internalComponentError?.fromDateError).valid) { errorResult.push({ from: invalidDateTimeMessage }); } - if (!isValidDateTimeWithEmptyCheck(value.to)) { + if (!isValidDateTimeWithEmptyCheck(value?.to, internalComponentError?.toDateError).valid) { errorResult.push({ to: invalidDateTimeMessage }); } diff --git a/src/core_modules/capture-ui/DateAndTimeFields/DateTimeRangeField/DateTimeRange.component.js b/src/core_modules/capture-ui/DateAndTimeFields/DateTimeRangeField/DateTimeRange.component.js index ff41792839..e16a915a59 100644 --- a/src/core_modules/capture-ui/DateAndTimeFields/DateTimeRangeField/DateTimeRange.component.js +++ b/src/core_modules/capture-ui/DateAndTimeFields/DateTimeRangeField/DateTimeRange.component.js @@ -17,11 +17,7 @@ type DateTimeValue = { type DateTimeRangeValue = { from?: ?DateTimeValue, to?: ?DateTimeValue, -} - -type BlurOpts = { - touched?: ?boolean, -} +}; type Props = { classes?: ?Object, @@ -29,21 +25,35 @@ type Props = { value: DateTimeRangeValue, onBlur: (value: ?DateTimeRangeValue, options: Object) => void, onChange: (value: ?DateTimeRangeValue) => void, -} +}; + +type State = { + fromDateError: { + error: ?string, + errorCode: ?string + }, + toDateError: { + error: ?string, + errorCode: ?string + } +}; const inputKeys = { FROM: 'from', TO: 'to', }; -export class DateTimeRangeField extends React.Component { +export class DateTimeRangeField extends React.Component { touchedFields: Set; constructor(props: Props) { super(props); this.touchedFields = new Set(); + this.state = { + fromDateError: { error: null, errorCode: null }, + toDateError: { error: null, errorCode: null }, + }; } - getValue = () => this.props.value || {}; @@ -82,27 +92,43 @@ export class DateTimeRangeField extends React.Component { return !!(value.to && value.to.date && value.to.time); } - handleFromBlur = (value: ?DateTimeValue, opts?: ?BlurOpts) => { - if (opts && opts.touched) { + handleFromBlur = (value: ?DateTimeValue, options: ?Object) => { + if (options?.touched) { this.touchedFields.add('fromTouched'); } - this.handleBlur({ - from: value, - to: this.getValue().to, - }, this.toHasValue()); + this.setState(() => ({ + fromDateError: { error: options?.error, errorCode: options?.errorCode }, + }), () => { + this.handleBlur({ + from: value, + to: this.getValue().to, + }, { + touched: !!this.toHasValue(), + fromDateError: this.state.fromDateError, + toDateError: this.state.toDateError, + }); + }); } - handleToBlur = (value: ?DateTimeValue, opts?: ?BlurOpts) => { - if (opts && opts.touched) { + handleToBlur = (value: ?DateTimeValue, options: ?Object) => { + if (options?.touched) { this.touchedFields.add('toTouched'); } - this.handleBlur({ - from: this.getValue().from, - to: value, - }, this.fromHasValue()); + this.setState(() => ({ + toDateError: { error: options?.error, errorCode: options?.errorCode }, + }), () => { + this.handleBlur({ + from: this.getValue().from, + to: value, + }, { + touched: !!this.fromHasValue(), + fromDateError: this.state.fromDateError, + toDateError: this.state.toDateError, + }); + }); } - handleBlur = (value: DateTimeRangeValue, otherFieldHasValue: boolean) => { + handleBlur = (value: DateTimeRangeValue, options: ?Object) => { const touched = this.touchedFields.size === 2; if (!value.from && !value.to) { this.props.onBlur(undefined, { @@ -111,7 +137,9 @@ export class DateTimeRangeField extends React.Component { return; } this.props.onBlur(value, { - touched: touched || otherFieldHasValue, + touched: touched || options?.touched, + fromDateError: options?.fromDateError, + toDateError: options?.toDateError, }); } From ace2fe6b957f4cf8c7ec7a9ae50281ba42314e38 Mon Sep 17 00:00:00 2001 From: alaa-yahia Date: Mon, 3 Feb 2025 17:05:50 +0200 Subject: [PATCH 35/42] fix: notes --- .../epics/addNoteForNewSingleEvent.epics.js | 2 +- .../DataEntry/actions/dataEntryLoad.utils.js | 4 +- .../components/Notes/Notes.component.js | 269 +++++++++--------- .../ViewEvent/Notes/viewEventNotes.epics.js | 10 +- .../DataEntry/epics/dataEntryNote.epics.js | 2 +- .../WidgetEnrollmentNote.epics.js | 2 +- .../WidgetEventNote/WidgetEventNote.epics.js | 2 +- .../WidgetEventSchedule.container.js | 2 +- .../WidgetNote/NoteSection/NoteSection.js | 4 +- 9 files changed, 139 insertions(+), 158 deletions(-) diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/addNoteForNewSingleEvent.epics.js b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/addNoteForNewSingleEvent.epics.js index 15108d291a..a1506e6cc3 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/addNoteForNewSingleEvent.epics.js +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/addNoteForNewSingleEvent.epics.js @@ -37,7 +37,7 @@ export const addNoteForNewSingleEventEpic = (action$: InputObservable, store: Re uid: clientId, }, storedBy: userName, - storedAt: convertListValue(storedAt, dataElementTypes.DATETIME), + storedAt: moment.utc().format('YYYY-MM-DDTHH:mm:ss.SSS'), clientId: uuid(), }; diff --git a/src/core_modules/capture-core/components/DataEntry/actions/dataEntryLoad.utils.js b/src/core_modules/capture-core/components/DataEntry/actions/dataEntryLoad.utils.js index 88e7bb9208..51d78ede59 100644 --- a/src/core_modules/capture-core/components/DataEntry/actions/dataEntryLoad.utils.js +++ b/src/core_modules/capture-core/components/DataEntry/actions/dataEntryLoad.utils.js @@ -1,6 +1,6 @@ // @flow import { convertValue } from '../../../converters/clientToForm'; -import { convertValue as convertListValue } from '../../../converters/clientToList'; +import { convertServerToClient } from '../../../converters'; import { type RenderFoundation, dataElementTypes, DataElement } from '../../../metaData'; import { getValidationError } from '../dataEntryField/internal/dataEntryField.utils'; @@ -88,7 +88,7 @@ export function getDataEntryNotes( const notes = clientValuesForDataEntry.notes || []; return notes.map((note, index) => ({ ...note, - storedAt: convertListValue(note.storedAt, dataElementTypes.DATETIME), + storedAt: convertServerToClient(note.storedAt, dataElementTypes.DATETIME), key: index, })); } diff --git a/src/core_modules/capture-core/components/Notes/Notes.component.js b/src/core_modules/capture-core/components/Notes/Notes.component.js index ec25efb246..c695651729 100644 --- a/src/core_modules/capture-core/components/Notes/Notes.component.js +++ b/src/core_modules/capture-core/components/Notes/Notes.component.js @@ -1,13 +1,18 @@ // @flow import * as React from 'react'; +import { useState, useEffect } from 'react'; +import moment from 'moment'; import { Editor, Parser } from '@dhis2/d2-ui-rich-text'; import { withStyles } from '@material-ui/core'; -import { colors, spacersNum, Menu, MenuItem, Button } from '@dhis2/ui'; +import { colors, spacersNum, Menu, MenuItem, Button, Tooltip } from '@dhis2/ui'; import i18n from '@dhis2/d2-i18n'; +import { useTimeZoneConversion } from '@dhis2/app-runtime'; import { withFocusSaver } from 'capture-ui'; import { ConditionalTooltip } from 'capture-core/components/Tooltips/ConditionalTooltip'; import { TextField } from '../FormFields/New'; +import { convertClientToList } from '../../converters'; +import { dataElementTypes } from '../../metaData'; import type { Note } from './notes.types'; @@ -82,152 +87,134 @@ type Props = { }, }; -type State = { - addIsOpen: boolean, - value: ?string, -} - -class NotesPlain extends React.Component { - static defaultProps = { - entityAccess: { read: true, write: true }, - } - constructor(props: Props) { - super(props); - this.state = { - addIsOpen: !!this.props.value, - value: this.props.value || null, - }; - } - - UNSAFE_componentWillReceiveProps(nextProps: Props) { - if (nextProps.value !== this.props.value - || this.props.value !== this.state.value) { - this.setState({ - value: nextProps.value, - }); - } - } - - toggleIsOpen = () => { - this.setState(prevState => ({ - addIsOpen: !prevState.addIsOpen, - })); - } - - onCancel = () => { - this.props.onBlur(null, { touched: false }); - this.toggleIsOpen(); - } - - onNewNoteEditorBlur = (value: string) => { - this.props.onBlur(value, { touched: false }); - } - - handleAddNote = () => { - if (this.props.value) { - this.props.onAddNote(this.props.value); +const NotesPlain = ({ + notes, + onAddNote, + onBlur, + value: propValue, + entityAccess = { read: true, write: true }, + smallMainButton, + classes, +}: Props) => { + const [addIsOpen, setAddIsOpen] = useState(false); + const [inputValue, setInputValue] = useState(''); + const { fromServerDate } = useTimeZoneConversion(); + + useEffect(() => { + setAddIsOpen(!!propValue); + setInputValue(propValue || ''); + }, [propValue]); + + const toggleIsOpen = () => { + setAddIsOpen(prev => !prev); + }; + + const onCancel = () => { + onBlur(null, { touched: false }); + toggleIsOpen(); + }; + + const onNewNoteEditorBlur = (value: string) => { + onBlur(value, { touched: false }); + }; + + const handleAddNote = () => { + if (propValue) { + onAddNote(propValue); } - this.toggleIsOpen(); - this.props.onBlur(null, { touched: false }); - } - - handleChange = (value: ?string) => { - this.setState({ value }); - } - - renderInput = () => { - const { classes } = this.props; - return ( -
- - - - -
- - -
- + toggleIsOpen(); + onBlur(null, { touched: false }); + }; + + const handleChange = (value: ?string) => { + setInputValue(value); + }; + + const renderInput = () => ( +
+ + + +
+ +
- ); - } - - renderButton = (canAddNote: boolean) => { - const { smallMainButton } = this.props; - return ( -
+ ); + + + const renderButton = (canAddNote: boolean) => ( +
+ - - - -
- ); - } - - render = () => { - const { notes, classes, entityAccess } = this.props; - return ( -
- - {notes.map(n => ( - -
-
- {n.createdBy ? `${n.createdBy.firstName} ${n.createdBy.surname}` : `${n.storedBy}` } -
-
- {n.storedDate} -
-
-
- {n.value} + {i18n.t('Write note')} + + +
+ ); + + return ( +
+ + {notes.map(n => ( + +
+
+ {n.createdBy ? `${n.createdBy.firstName} ${n.createdBy.surname}` : `${n.storedBy}` }
- } - /> - ))} -
- { -
- { this.state.addIsOpen ? this.renderInput() : this.renderButton(entityAccess.write) } -
- } +
+ + + {moment(fromServerDate(n.storedAt)).fromNow()} + + +
+
+
+ {n.value} +
+ } + /> + ))} +
+
+ {addIsOpen ? renderInput() : renderButton(entityAccess.write)}
- ); - } -} +
+ ); +}; export const Notes = withStyles(styles)(NotesPlain); diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/Notes/viewEventNotes.epics.js b/src/core_modules/capture-core/components/Pages/ViewEvent/Notes/viewEventNotes.epics.js index 2c2c78fd8b..c3f8b33dfc 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/Notes/viewEventNotes.epics.js +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/Notes/viewEventNotes.epics.js @@ -5,8 +5,6 @@ import { featureAvailable, FEATURES } from 'capture-core-utils'; import { map, switchMap } from 'rxjs/operators'; import uuid from 'd2-utilizr/lib/uuid'; import moment from 'moment'; -import { convertValue as convertListValue } from '../../../../converters/clientToList'; -import { dataElementTypes } from '../../../../metaData'; import { actionTypes as viewEventNotesActionTypes, batchActionTypes as viewEventNotesBatchActionTypes, @@ -41,15 +39,11 @@ export const loadNotesForViewEventEpic = (action$: InputObservable) => map((action) => { const eventContainer = action.payload.eventContainer; const notes = (eventContainer && eventContainer.event && eventContainer.event.notes) || []; - const convertedNotes = notes.map(note => ({ - ...note, - storedDate: convertListValue(note.storedAt, dataElementTypes.DATETIME), - })); // Load event relationships return batchActions([ eventNotesLoaded(), - setNotes(noteKey, convertedNotes), + setNotes(noteKey, notes), ], viewEventNotesBatchActionTypes.LOAD_EVENT_NOTES_BATCH); })); @@ -80,7 +74,7 @@ export const addNoteForViewEventEpic = (action$: InputObservable, store: ReduxSt uid: clientId, }, storedBy: userName, - storedDate: convertListValue(moment().toISOString(), dataElementTypes.DATETIME), + storedAt: moment.utc().format('YYYY-MM-DDTHH:mm:ss.SSS'), clientId: uuid(), }; return batchActions([ diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/epics/dataEntryNote.epics.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/epics/dataEntryNote.epics.js index 6fec8522f7..39c0a16f11 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/epics/dataEntryNote.epics.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/epics/dataEntryNote.epics.js @@ -36,7 +36,7 @@ export const addNoteForNewEnrollmentEventEpic = (action$: InputObservable, store uid: clientId, }, storedBy: userName, - storedAt: convertListValue(storedAt, dataElementTypes.DATETIME), + storedAt: moment.utc().format('YYYY-MM-DDTHH:mm:ss.SSS'), clientId: uuid(), }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.epics.js b/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.epics.js index 5a0fb28ebb..d39bb599a3 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.epics.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.epics.js @@ -42,7 +42,7 @@ export const addNoteForEnrollmentEpic = (action$: InputObservable, store: ReduxS }, updatedAt: moment().toISOString(), storedBy: userName, - storedAt: moment().toISOString(), + storedAt: moment.utc().format('YYYY-MM-DDTHH:mm:ss.SSS'), }; const saveContext = { diff --git a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.epics.js b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.epics.js index 315c0dba5b..d4aea8bc06 100644 --- a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.epics.js +++ b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.epics.js @@ -52,7 +52,7 @@ export const addNoteForEventEpic = (action$: InputObservable, store: ReduxStore, }, lastUpdated: moment().toISOString(), storedBy: userName, - storedAt: moment().toISOString(), + storedAt: moment.utc().format('YYYY-MM-DDTHH:mm:ss.SSS'), }; const formNote = { ...clientNote, diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js index 4132d024e9..022de2e577 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js @@ -123,7 +123,7 @@ export const WidgetEventSchedule = ({ const onAddNote = (note) => { const newNote = { storedBy: currentUser.userName, - storedAt: moment().toISOString(), + storedAt: moment.utc().format('YYYY-MM-DDTHH:mm:ss.SSS'), value: note, createdBy: { firstName: currentUser.firstName, diff --git a/src/core_modules/capture-core/components/WidgetNote/NoteSection/NoteSection.js b/src/core_modules/capture-core/components/WidgetNote/NoteSection/NoteSection.js index 0069b42f3e..078f3a78d5 100644 --- a/src/core_modules/capture-core/components/WidgetNote/NoteSection/NoteSection.js +++ b/src/core_modules/capture-core/components/WidgetNote/NoteSection/NoteSection.js @@ -9,7 +9,7 @@ import { colors, spacersNum, Button, Tooltip } from '@dhis2/ui'; import moment from 'moment'; import { useTimeZoneConversion } from '@dhis2/app-runtime'; import { TextField } from '../../FormFields/New'; -import { convertValue as convertValueClientToView } from '../../../converters/clientToView'; +import { convertClientToList } from '../../../converters'; import { dataElementTypes } from '../../../metaData'; const FocusTextField = withFocusSaver()(TextField); @@ -100,7 +100,7 @@ const NoteSectionPlain = ({ }, [handleAddNote, newNoteValue]); const NoteItem = ({ value, storedAt, createdBy }) => { - const localDateTime: string = (convertValueClientToView(storedAt, dataElementTypes.DATETIME): any); + const localDateTime: string = convertClientToList(fromServerDate(storedAt), dataElementTypes.DATETIME); return (
{/* TODO: add avatar */} From 680fa4c8b5ac3d71401d94ef6fd95cd287cbea2d Mon Sep 17 00:00:00 2001 From: alaa-yahia Date: Wed, 5 Feb 2025 14:46:19 +0200 Subject: [PATCH 36/42] fix: search when adding relationship --- .../SearchResults/TeiRelationshipSearchResults.component.js | 3 ++- .../common/TEIRelationshipsWidget/TeiSearch/serverToFilters.js | 2 +- .../capture-core/components/TeiSearch/serverToFilters.js | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/core_modules/capture-core/components/Pages/NewRelationship/TeiRelationship/SearchResults/TeiRelationshipSearchResults.component.js b/src/core_modules/capture-core/components/Pages/NewRelationship/TeiRelationship/SearchResults/TeiRelationshipSearchResults.component.js index 461424f302..0edbfbfd1f 100644 --- a/src/core_modules/capture-core/components/Pages/NewRelationship/TeiRelationship/SearchResults/TeiRelationshipSearchResults.component.js +++ b/src/core_modules/capture-core/components/Pages/NewRelationship/TeiRelationship/SearchResults/TeiRelationshipSearchResults.component.js @@ -5,6 +5,7 @@ import i18n from '@dhis2/d2-i18n'; import { withStyles } from '@material-ui/core'; import { Pagination } from 'capture-ui'; import { Button } from '@dhis2/ui'; +import { convertFormToClient } from 'capture-core/converters'; import { withNavigation } from '../../../../Pagination/withDefaultNavigation'; import { makeAttributesSelector } from './teiRelationshipSearchResults.selectors'; import { CardList } from '../../../../CardList'; @@ -130,7 +131,7 @@ class TeiRelationshipSearchResultsPlain extends React.Component { .filter(key => searchValues[key] !== null) .map((key) => { const element = searchForm.getElement(key); - const value = searchValues[key]; + const value = convertFormToClient(searchValues[key], element.type); return { name: element.formName, value, id: element.id, type: element.type }; }); } diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/serverToFilters.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/serverToFilters.js index 78763397f6..e8380896bc 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/serverToFilters.js +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/serverToFilters.js @@ -12,7 +12,7 @@ const like = (value: any, elementId: string) => `${elementId}:like:${escapeStrin function convertRange(value: RangeValue, elementId: string) { - return `${elementId}:ge:${value.from}:le:${value.to}`; + return `${elementId}:ge:${escapeString(String(value.from))}:le:${escapeString(String(value.to))}`; } const valueConvertersForType = { diff --git a/src/core_modules/capture-core/components/TeiSearch/serverToFilters.js b/src/core_modules/capture-core/components/TeiSearch/serverToFilters.js index afbf8a2b49..c6027af214 100644 --- a/src/core_modules/capture-core/components/TeiSearch/serverToFilters.js +++ b/src/core_modules/capture-core/components/TeiSearch/serverToFilters.js @@ -12,7 +12,7 @@ const like = (value: any, elementId: string) => `${elementId}:like:${escapeStrin const convertRange = (value: RangeValue, { id: elementId }: DataElement) => ( - `${elementId}:ge:${value.from}:le:${value.to}` + `${elementId}:ge:${escapeString(String(value.from))}:le:${escapeString(String(value.to))}` ); const convertString = (value: any, metaElement: DataElement) => { From 1e444306f0acc8997aa27d370dc8ebfcc7a75c29 Mon Sep 17 00:00:00 2001 From: alaa-yahia Date: Thu, 13 Feb 2025 03:18:57 +0200 Subject: [PATCH 37/42] fix: notes not working when changing server timeZone --- .../epics/addNoteForNewSingleEvent.epics.js | 2 +- .../DataEntry/actions/dataEntryLoad.utils.js | 5 +- .../components/Notes/Notes.component.js | 56 +++++++++++-------- .../ViewEvent/Notes/viewEventNotes.epics.js | 2 +- .../DataEntry/epics/dataEntryNote.epics.js | 2 +- .../WidgetEnrollmentNote.epics.js | 2 +- .../WidgetEventNote/WidgetEventNote.epics.js | 2 +- .../WidgetEventSchedule.container.js | 2 +- .../WidgetNote/NoteSection/NoteSection.js | 10 ++-- .../systemSettings/cacheSystemSetttings.js | 2 +- 10 files changed, 47 insertions(+), 38 deletions(-) diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/addNoteForNewSingleEvent.epics.js b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/addNoteForNewSingleEvent.epics.js index 457ff951cf..b7ae51ceb5 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/addNoteForNewSingleEvent.epics.js +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/addNoteForNewSingleEvent.epics.js @@ -33,7 +33,7 @@ export const addNoteForNewSingleEventEpic = (action$: InputObservable, store: Re uid: clientId, }, storedBy: userName, - storedAt: moment.utc().format('YYYY-MM-DDTHH:mm:ss.SSS'), + storedAt: moment().toISOString(), clientId: uuid(), }; diff --git a/src/core_modules/capture-core/components/DataEntry/actions/dataEntryLoad.utils.js b/src/core_modules/capture-core/components/DataEntry/actions/dataEntryLoad.utils.js index 51d78ede59..9bf9778d2c 100644 --- a/src/core_modules/capture-core/components/DataEntry/actions/dataEntryLoad.utils.js +++ b/src/core_modules/capture-core/components/DataEntry/actions/dataEntryLoad.utils.js @@ -1,7 +1,6 @@ // @flow import { convertValue } from '../../../converters/clientToForm'; -import { convertServerToClient } from '../../../converters'; -import { type RenderFoundation, dataElementTypes, DataElement } from '../../../metaData'; +import { type RenderFoundation, DataElement } from '../../../metaData'; import { getValidationError } from '../dataEntryField/internal/dataEntryField.utils'; import type { ValidatorContainer } from '../dataEntryField/internal/dataEntryField.utils'; @@ -88,7 +87,7 @@ export function getDataEntryNotes( const notes = clientValuesForDataEntry.notes || []; return notes.map((note, index) => ({ ...note, - storedAt: convertServerToClient(note.storedAt, dataElementTypes.DATETIME), + storedAt: note.storedAt, key: index, })); } diff --git a/src/core_modules/capture-core/components/Notes/Notes.component.js b/src/core_modules/capture-core/components/Notes/Notes.component.js index c695651729..169c9e85aa 100644 --- a/src/core_modules/capture-core/components/Notes/Notes.component.js +++ b/src/core_modules/capture-core/components/Notes/Notes.component.js @@ -98,7 +98,7 @@ const NotesPlain = ({ }: Props) => { const [addIsOpen, setAddIsOpen] = useState(false); const [inputValue, setInputValue] = useState(''); - const { fromServerDate } = useTimeZoneConversion(); + const { fromServerDate, fromClientDate } = useTimeZoneConversion(); useEffect(() => { setAddIsOpen(!!propValue); @@ -184,31 +184,39 @@ const NotesPlain = ({ return (
- {notes.map(n => ( - -
-
- {n.createdBy ? `${n.createdBy.firstName} ${n.createdBy.surname}` : `${n.storedBy}` } + {notes.map((n) => { + const formattedDate = n.storedAt && n.storedAt.endsWith('Z') ? + fromClientDate(n.storedAt) : + fromServerDate(n.storedAt); + + return ( + +
+
+ {n.createdBy ? + `${n.createdBy.firstName} ${n.createdBy.surname}` + : `${n.storedBy}` } +
+
+ + + {moment(formattedDate).fromNow()} + + + +
-
- - - {moment(fromServerDate(n.storedAt)).fromNow()} - - - +
+ {n.value}
-
-
- {n.value} -
- } - /> - ))} + } + /> + ); + })}
{addIsOpen ? renderInput() : renderButton(entityAccess.write)} diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/Notes/viewEventNotes.epics.js b/src/core_modules/capture-core/components/Pages/ViewEvent/Notes/viewEventNotes.epics.js index c3f8b33dfc..50666730e1 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/Notes/viewEventNotes.epics.js +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/Notes/viewEventNotes.epics.js @@ -74,7 +74,7 @@ export const addNoteForViewEventEpic = (action$: InputObservable, store: ReduxSt uid: clientId, }, storedBy: userName, - storedAt: moment.utc().format('YYYY-MM-DDTHH:mm:ss.SSS'), + storedAt: moment().toISOString(), clientId: uuid(), }; return batchActions([ diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/epics/dataEntryNote.epics.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/epics/dataEntryNote.epics.js index b4d5f90de7..03a28a5517 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/epics/dataEntryNote.epics.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/epics/dataEntryNote.epics.js @@ -33,7 +33,7 @@ export const addNoteForNewEnrollmentEventEpic = (action$: InputObservable, store uid: clientId, }, storedBy: userName, - storedAt: moment.utc().format('YYYY-MM-DDTHH:mm:ss.SSS'), + storedAt: moment().toISOString(), clientId: uuid(), }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.epics.js b/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.epics.js index d39bb599a3..5a0fb28ebb 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.epics.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.epics.js @@ -42,7 +42,7 @@ export const addNoteForEnrollmentEpic = (action$: InputObservable, store: ReduxS }, updatedAt: moment().toISOString(), storedBy: userName, - storedAt: moment.utc().format('YYYY-MM-DDTHH:mm:ss.SSS'), + storedAt: moment().toISOString(), }; const saveContext = { diff --git a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.epics.js b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.epics.js index d4aea8bc06..315c0dba5b 100644 --- a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.epics.js +++ b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.epics.js @@ -52,7 +52,7 @@ export const addNoteForEventEpic = (action$: InputObservable, store: ReduxStore, }, lastUpdated: moment().toISOString(), storedBy: userName, - storedAt: moment.utc().format('YYYY-MM-DDTHH:mm:ss.SSS'), + storedAt: moment().toISOString(), }; const formNote = { ...clientNote, diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js index 9a9b393ec9..aefbecd9ed 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js @@ -127,7 +127,7 @@ export const WidgetEventSchedule = ({ const onAddNote = (note) => { const newNote = { storedBy: currentUser.userName, - storedAt: moment.utc().format('YYYY-MM-DDTHH:mm:ss.SSS'), + storedAt: moment().toISOString(), value: note, createdBy: { firstName: currentUser.firstName, diff --git a/src/core_modules/capture-core/components/WidgetNote/NoteSection/NoteSection.js b/src/core_modules/capture-core/components/WidgetNote/NoteSection/NoteSection.js index 79b8dd113c..e05a66555d 100644 --- a/src/core_modules/capture-core/components/WidgetNote/NoteSection/NoteSection.js +++ b/src/core_modules/capture-core/components/WidgetNote/NoteSection/NoteSection.js @@ -81,7 +81,7 @@ const NoteSectionPlain = ({ }: Props) => { const [isEditing, setEditing] = useState(false); const [newNoteValue, setNewNoteValue] = useState(''); - const { fromServerDate } = useTimeZoneConversion(); + const { fromServerDate, fromClientDate } = useTimeZoneConversion(); const handleChange = useCallback((value) => { setEditing(true); @@ -100,7 +100,9 @@ const NoteSectionPlain = ({ }, [handleAddNote, newNoteValue]); const NoteItem = ({ value, storedAt, createdBy }) => { - const localDateTime = convertClientToList(fromServerDate(storedAt), dataElementTypes.DATETIME); + const formattedDate = storedAt && storedAt.endsWith('Z') ? + fromClientDate(storedAt) : + fromServerDate(storedAt); return (
{/* TODO: add avatar */} @@ -110,8 +112,8 @@ const NoteSectionPlain = ({ {createdBy.firstName} {' '} {createdBy.surname} } - - {moment(fromServerDate(storedAt)).fromNow()} + + {moment(formattedDate).fromNow()}
diff --git a/src/core_modules/capture-core/metaDataStoreLoaders/systemSettings/cacheSystemSetttings.js b/src/core_modules/capture-core/metaDataStoreLoaders/systemSettings/cacheSystemSetttings.js index 225c4261ab..308f785765 100644 --- a/src/core_modules/capture-core/metaDataStoreLoaders/systemSettings/cacheSystemSetttings.js +++ b/src/core_modules/capture-core/metaDataStoreLoaders/systemSettings/cacheSystemSetttings.js @@ -27,7 +27,7 @@ export async function cacheSystemSettings( }, { id: 'calendar', - value: systemSettings.calendar, + value: systemSettings.calendar !== 'julian' ? systemSettings.calendar : 'iso8601', }, ]; From fe85e5df00064d5665950ffb028c0cfb1072a2a6 Mon Sep 17 00:00:00 2001 From: alaa-yahia Date: Thu, 13 Feb 2025 12:59:56 +0200 Subject: [PATCH 38/42] chore: pass fromClientDate to storedAt --- src/components/AppLoader/AppLoader.component.js | 10 +++++++--- .../epics/addNoteForNewSingleEvent.epics.js | 4 ++-- .../components/Notes/Notes.component.js | 17 ++++++++++------- .../ViewEvent/Notes/viewEventNotes.epics.js | 4 ++-- 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/components/AppLoader/AppLoader.component.js b/src/components/AppLoader/AppLoader.component.js index 49af80126f..1bad4586ce 100644 --- a/src/components/AppLoader/AppLoader.component.js +++ b/src/components/AppLoader/AppLoader.component.js @@ -2,7 +2,7 @@ import React, { useCallback, useMemo, useEffect } from 'react'; import log from 'loglevel'; import { useHistory } from 'react-router-dom'; -import { useDataEngine, useConfig } from '@dhis2/app-runtime'; +import { useDataEngine, useConfig, useTimeZoneConversion } from '@dhis2/app-runtime'; import { LoadingMaskForPage } from 'capture-core/components/LoadingMasks'; import { DisplayException } from 'capture-core/utils/exceptions'; import { makeQuerySingleResource } from 'capture-core/utils/api'; @@ -20,18 +20,20 @@ type Props = { const useApiUtils = () => { const dataEngine = useDataEngine(); const { serverVersion } = useConfig(); + const { fromClientDate } = useTimeZoneConversion(); return useMemo(() => ({ querySingleResource: makeQuerySingleResource(dataEngine.query.bind(dataEngine)), mutate: dataEngine.mutate.bind(dataEngine), absoluteApiPath: buildUrl(dataEngine.link.config.baseUrl, dataEngine.link.versionedApiPath), serverVersion, - }), [dataEngine, serverVersion]); + fromClientDate, + }), [dataEngine, serverVersion, fromClientDate]); }; export const AppLoader = (props: Props) => { const { onRunApp, onCacheExpired } = props; const [loadError, setLoadError] = React.useState(null); - const { querySingleResource, mutate, absoluteApiPath, serverVersion } = useApiUtils(); + const { querySingleResource, mutate, absoluteApiPath, serverVersion, fromClientDate } = useApiUtils(); const history = useHistory(); const logError = useCallback((error) => { @@ -56,6 +58,7 @@ export const AppLoader = (props: Props) => { mutate, absoluteApiPath, serverVersion, + fromClientDate, }, // $FlowFixMe[prop-missing] automated comment () => onRunApp(store)); @@ -83,6 +86,7 @@ export const AppLoader = (props: Props) => { absoluteApiPath, history, serverVersion, + fromClientDate, ]); useEffect(() => { diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/addNoteForNewSingleEvent.epics.js b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/addNoteForNewSingleEvent.epics.js index b7ae51ceb5..2e1c534ca4 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/addNoteForNewSingleEvent.epics.js +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/addNoteForNewSingleEvent.epics.js @@ -11,7 +11,7 @@ import { addNote, } from '../../../../../DataEntry/actions/dataEntry.actions'; -export const addNoteForNewSingleEventEpic = (action$: InputObservable, store: ReduxStore, { querySingleResource }: ApiUtils) => +export const addNoteForNewSingleEventEpic = (action$: InputObservable, store: ReduxStore, { querySingleResource, fromClientDate }: ApiUtils) => action$.pipe( ofType(newEventDataEntryActionTypes.ADD_NEW_EVENT_NOTE), switchMap((action) => { @@ -33,7 +33,7 @@ export const addNoteForNewSingleEventEpic = (action$: InputObservable, store: Re uid: clientId, }, storedBy: userName, - storedAt: moment().toISOString(), + storedAt: fromClientDate(moment().toISOString()), clientId: uuid(), }; diff --git a/src/core_modules/capture-core/components/Notes/Notes.component.js b/src/core_modules/capture-core/components/Notes/Notes.component.js index 169c9e85aa..765d1d8dc7 100644 --- a/src/core_modules/capture-core/components/Notes/Notes.component.js +++ b/src/core_modules/capture-core/components/Notes/Notes.component.js @@ -184,12 +184,12 @@ const NotesPlain = ({ return (
- {notes.map((n) => { - const formattedDate = n.storedAt && n.storedAt.endsWith('Z') ? + {notes.map(n => + /* const formattedDate = n.storedAt && n.storedAt.endsWith('Z') ? fromClientDate(n.storedAt) : - fromServerDate(n.storedAt); + fromServerDate(n.storedAt); */ - return ( + (
- + {/* {moment(formattedDate).fromNow()} + */} + + {moment(fromServerDate(n.storedAt)).fromNow()} @@ -215,8 +218,8 @@ const NotesPlain = ({
} /> - ); - })} + ), + )}
{addIsOpen ? renderInput() : renderButton(entityAccess.write)} diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/Notes/viewEventNotes.epics.js b/src/core_modules/capture-core/components/Pages/ViewEvent/Notes/viewEventNotes.epics.js index 50666730e1..1e31dcb4c9 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/Notes/viewEventNotes.epics.js +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/Notes/viewEventNotes.epics.js @@ -47,7 +47,7 @@ export const loadNotesForViewEventEpic = (action$: InputObservable) => ], viewEventNotesBatchActionTypes.LOAD_EVENT_NOTES_BATCH); })); -export const addNoteForViewEventEpic = (action$: InputObservable, store: ReduxStore, { querySingleResource }: ApiUtils) => +export const addNoteForViewEventEpic = (action$: InputObservable, store: ReduxStore, { querySingleResource, fromClientDate }: ApiUtils) => action$.pipe( ofType(viewEventNotesActionTypes.REQUEST_SAVE_EVENT_NOTE), switchMap((action) => { @@ -74,7 +74,7 @@ export const addNoteForViewEventEpic = (action$: InputObservable, store: ReduxSt uid: clientId, }, storedBy: userName, - storedAt: moment().toISOString(), + storedAt: fromClientDate(moment().toISOString()), clientId: uuid(), }; return batchActions([ From c59a0f3e75d9b37b24241fd0e84dd966eea1e923 Mon Sep 17 00:00:00 2001 From: alaa-yahia Date: Thu, 13 Feb 2025 13:56:36 +0200 Subject: [PATCH 39/42] fix: notes --- .../epics/addNoteForNewSingleEvent.epics.js | 2 +- .../components/Notes/Notes.component.js | 9 +--- .../ViewEvent/Notes/viewEventNotes.epics.js | 2 +- .../DataEntry/epics/dataEntryNote.epics.js | 4 +- .../WidgetEnrollmentNote.epics.js | 4 +- .../WidgetEventNote/WidgetEventNote.epics.js | 4 +- .../WidgetEventSchedule.container.js | 4 +- .../WidgetNote/NoteSection/NoteSection.js | 43 ++++++++----------- .../capture-core/flow/typeDeclarations.js | 3 ++ 9 files changed, 34 insertions(+), 41 deletions(-) diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/addNoteForNewSingleEvent.epics.js b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/addNoteForNewSingleEvent.epics.js index 2e1c534ca4..0fd0029628 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/addNoteForNewSingleEvent.epics.js +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/addNoteForNewSingleEvent.epics.js @@ -33,7 +33,7 @@ export const addNoteForNewSingleEventEpic = (action$: InputObservable, store: Re uid: clientId, }, storedBy: userName, - storedAt: fromClientDate(moment().toISOString()), + storedAt: fromClientDate(moment().toISOString()).getServerZonedISOString(), clientId: uuid(), }; diff --git a/src/core_modules/capture-core/components/Notes/Notes.component.js b/src/core_modules/capture-core/components/Notes/Notes.component.js index 765d1d8dc7..1f1174accb 100644 --- a/src/core_modules/capture-core/components/Notes/Notes.component.js +++ b/src/core_modules/capture-core/components/Notes/Notes.component.js @@ -98,7 +98,7 @@ const NotesPlain = ({ }: Props) => { const [addIsOpen, setAddIsOpen] = useState(false); const [inputValue, setInputValue] = useState(''); - const { fromServerDate, fromClientDate } = useTimeZoneConversion(); + const { fromServerDate } = useTimeZoneConversion(); useEffect(() => { setAddIsOpen(!!propValue); @@ -185,10 +185,6 @@ const NotesPlain = ({
{notes.map(n => - /* const formattedDate = n.storedAt && n.storedAt.endsWith('Z') ? - fromClientDate(n.storedAt) : - fromServerDate(n.storedAt); */ - (
- {/* - {moment(formattedDate).fromNow()} - */} {moment(fromServerDate(n.storedAt)).fromNow()} diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/Notes/viewEventNotes.epics.js b/src/core_modules/capture-core/components/Pages/ViewEvent/Notes/viewEventNotes.epics.js index 1e31dcb4c9..2c473aaf9e 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/Notes/viewEventNotes.epics.js +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/Notes/viewEventNotes.epics.js @@ -74,7 +74,7 @@ export const addNoteForViewEventEpic = (action$: InputObservable, store: ReduxSt uid: clientId, }, storedBy: userName, - storedAt: fromClientDate(moment().toISOString()), + storedAt: fromClientDate(moment().toISOString()).getServerZonedISOString(), clientId: uuid(), }; return batchActions([ diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/epics/dataEntryNote.epics.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/epics/dataEntryNote.epics.js index 03a28a5517..868eb36689 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/epics/dataEntryNote.epics.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/epics/dataEntryNote.epics.js @@ -11,7 +11,7 @@ import { addNote, } from '../../../DataEntry/actions/dataEntry.actions'; -export const addNoteForNewEnrollmentEventEpic = (action$: InputObservable, store: ReduxStore, { querySingleResource }: ApiUtils) => +export const addNoteForNewEnrollmentEventEpic = (action$: InputObservable, store: ReduxStore, { querySingleResource, fromClientDate }: ApiUtils) => action$.pipe( ofType(newEventWidgetDataEntryActionTypes.EVENT_NOTE_ADD), switchMap((action) => { @@ -33,7 +33,7 @@ export const addNoteForNewEnrollmentEventEpic = (action$: InputObservable, store uid: clientId, }, storedBy: userName, - storedAt: moment().toISOString(), + storedAt: fromClientDate(moment().toISOString()).getServerZonedISOString(), clientId: uuid(), }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.epics.js b/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.epics.js index 5a0fb28ebb..46e60fc834 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.epics.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.epics.js @@ -15,7 +15,7 @@ const createServerData = (note, useNewEndpoint) => { return { notes: [{ value: note }] }; }; -export const addNoteForEnrollmentEpic = (action$: InputObservable, store: ReduxStore, { querySingleResource }: ApiUtils) => +export const addNoteForEnrollmentEpic = (action$: InputObservable, store: ReduxStore, { querySingleResource, fromClientDate }: ApiUtils) => action$.pipe( ofType(actionTypes.REQUEST_ADD_NOTE_FOR_ENROLLMENT), switchMap((action) => { @@ -42,7 +42,7 @@ export const addNoteForEnrollmentEpic = (action$: InputObservable, store: ReduxS }, updatedAt: moment().toISOString(), storedBy: userName, - storedAt: moment().toISOString(), + storedAt: fromClientDate(moment().toISOString()).getServerZonedISOString(), }; const saveContext = { diff --git a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.epics.js b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.epics.js index 315c0dba5b..69be90b443 100644 --- a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.epics.js +++ b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.epics.js @@ -24,7 +24,7 @@ const createServerData = (eventId, note, useNewEndpoint) => { return { event: eventId, notes: [{ value: note }] }; }; -export const addNoteForEventEpic = (action$: InputObservable, store: ReduxStore, { querySingleResource }: ApiUtils) => +export const addNoteForEventEpic = (action$: InputObservable, store: ReduxStore, { querySingleResource, fromClientDate }: ApiUtils) => action$.pipe( ofType(actionTypes.REQUEST_ADD_NOTE_FOR_EVENT), switchMap((action) => { @@ -52,7 +52,7 @@ export const addNoteForEventEpic = (action$: InputObservable, store: ReduxStore, }, lastUpdated: moment().toISOString(), storedBy: userName, - storedAt: moment().toISOString(), + storedAt: fromClientDate(moment().toISOString()).getServerZonedISOString(), }; const formNote = { ...clientNote, diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js index aefbecd9ed..69f03772c9 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js @@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import i18n from '@dhis2/d2-i18n'; import { useDispatch } from 'react-redux'; +import { useTimeZoneConversion } from '@dhis2/app-runtime'; import moment from 'moment'; import { getProgramAndStageForProgram, TrackerProgram, getProgramEventAccess, dataElementTypes } from '../../metaData'; import { getCachedOrgUnitName } from '../../metadataRetrieval/orgUnitName'; @@ -43,6 +44,7 @@ export const WidgetEventSchedule = ({ const suggestedScheduleDate = useDetermineSuggestedScheduleDate({ programStageScheduleConfig, programConfig, initialScheduleDate, ...passOnProps, }); + const { fromClientDate } = useTimeZoneConversion(); const orgUnitName = getCachedOrgUnitName(initialOrgUnitId); const { currentUser, noteId } = useNoteDetails(); const [scheduleDate, setScheduleDate] = useState(''); @@ -127,7 +129,7 @@ export const WidgetEventSchedule = ({ const onAddNote = (note) => { const newNote = { storedBy: currentUser.userName, - storedAt: moment().toISOString(), + storedAt: fromClientDate(moment().toISOString()).getServerZonedISOString(), value: note, createdBy: { firstName: currentUser.firstName, diff --git a/src/core_modules/capture-core/components/WidgetNote/NoteSection/NoteSection.js b/src/core_modules/capture-core/components/WidgetNote/NoteSection/NoteSection.js index e05a66555d..b800cacc28 100644 --- a/src/core_modules/capture-core/components/WidgetNote/NoteSection/NoteSection.js +++ b/src/core_modules/capture-core/components/WidgetNote/NoteSection/NoteSection.js @@ -81,7 +81,7 @@ const NoteSectionPlain = ({ }: Props) => { const [isEditing, setEditing] = useState(false); const [newNoteValue, setNewNoteValue] = useState(''); - const { fromServerDate, fromClientDate } = useTimeZoneConversion(); + const { fromServerDate } = useTimeZoneConversion(); const handleChange = useCallback((value) => { setEditing(true); @@ -99,31 +99,26 @@ const NoteSectionPlain = ({ setEditing(false); }, [handleAddNote, newNoteValue]); - const NoteItem = ({ value, storedAt, createdBy }) => { - const formattedDate = storedAt && storedAt.endsWith('Z') ? - fromClientDate(storedAt) : - fromServerDate(storedAt); - return ( -
- {/* TODO: add avatar */} -
-
- {createdBy && - {createdBy.firstName} {' '} {createdBy.surname} - } - - - {moment(formattedDate).fromNow()} - - -
-
- {value} -
+ const NoteItem = ({ value, storedAt, createdBy }) => ( +
+ {/* TODO: add avatar */} +
+
+ {createdBy && + {createdBy.firstName} {' '} {createdBy.surname} + } + + + {moment(fromServerDate(storedAt)).fromNow()} + + +
+
+ {value}
- ); - }; +
+ ); return ( diff --git a/src/core_modules/capture-core/flow/typeDeclarations.js b/src/core_modules/capture-core/flow/typeDeclarations.js index 44d8d4f9ce..ad0065f3b7 100644 --- a/src/core_modules/capture-core/flow/typeDeclarations.js +++ b/src/core_modules/capture-core/flow/typeDeclarations.js @@ -1,5 +1,6 @@ // @flow import type { QuerySingleResource } from '../utils/api/api.types'; +import type { DHIS2Date } from '@dhis2/app-runtime'; declare type D2 = { models: Object, @@ -203,6 +204,7 @@ declare type ApiUtilsWithoutHistory = {| mutate: DataEngineMutate, absoluteApiPath: string, serverVersion: { minor: number }, + fromClientDate: (date?: string | Date | number | null) => DHIS2Date, |} declare type ApiUtils = {| @@ -214,4 +216,5 @@ declare type ApiUtils = {| push: () => void, ...PassOnProps, }, + fromClientDate: (date?: string | Date | number | null) => DHIS2Date, |}; From 5581ce5d65f6acd4e7df81c7a3bad10005393305 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Storl=C3=B8kken=20Melseth?= Date: Fri, 14 Feb 2025 18:00:27 +0100 Subject: [PATCH 40/42] fix: calendar locale, moment locale, note client value format --- .../New/Fields/AgeField/AgeField.component.js | 1 + .../DateAndTimeFields/DateField/DateField.component.js | 1 + .../DateRangeField/DateRangeField.component.js | 2 ++ .../DateTimeField/DateTimeField.component.js | 2 ++ .../DateTimeRangeField/DateTimeRangeField.component.js | 2 ++ .../capture-core/components/Notes/Notes.component.js | 2 +- .../components/WidgetNote/NoteSection/NoteSection.js | 2 +- .../capture-core/converters/clientToList.js | 10 ++-------- .../capture-core/converters/clientToView.js | 7 +------ .../metaData/SystemSettings/SystemSettings.js | 1 + .../systemSettings/cacheSystemSetttings.js | 5 +++++ .../utils/converters/date/convertIsoToLocalCalendar.js | 2 +- .../capture-ui/AgeField/AgeField.component.js | 1 + .../DateAndTimeFields/DateField/Date.component.js | 3 +++ .../DateRangeField/DateRangeField.component.js | 1 + .../DateTimeField/DateTime.component.js | 3 ++- .../DateTimeRangeField/DateTimeRange.component.js | 1 + 17 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/core_modules/capture-core/components/FormFields/New/Fields/AgeField/AgeField.component.js b/src/core_modules/capture-core/components/FormFields/New/Fields/AgeField/AgeField.component.js index c160c34475..ea0b0f53b6 100644 --- a/src/core_modules/capture-core/components/FormFields/New/Fields/AgeField/AgeField.component.js +++ b/src/core_modules/capture-core/components/FormFields/New/Fields/AgeField/AgeField.component.js @@ -49,6 +49,7 @@ const AgeFieldPlain = (props: Props) => { // $FlowFixMe[cannot-spread-inexact] automated comment ); diff --git a/src/core_modules/capture-core/components/FormFields/New/Fields/DateAndTimeFields/DateField/DateField.component.js b/src/core_modules/capture-core/components/FormFields/New/Fields/DateAndTimeFields/DateField/DateField.component.js index 8ed4bf9b8c..9bb08ad56f 100644 --- a/src/core_modules/capture-core/components/FormFields/New/Fields/DateAndTimeFields/DateField/DateField.component.js +++ b/src/core_modules/capture-core/components/FormFields/New/Fields/DateAndTimeFields/DateField/DateField.component.js @@ -37,6 +37,7 @@ class DateFieldPlain extends React.Component { // $FlowFixMe[cannot-spread-inexact] automated comment ); diff --git a/src/core_modules/capture-core/components/FormFields/New/Fields/DateAndTimeFields/DateRangeField/DateRangeField.component.js b/src/core_modules/capture-core/components/FormFields/New/Fields/DateAndTimeFields/DateRangeField/DateRangeField.component.js index 496d330d03..5dcb8217a3 100644 --- a/src/core_modules/capture-core/components/FormFields/New/Fields/DateAndTimeFields/DateRangeField/DateRangeField.component.js +++ b/src/core_modules/capture-core/components/FormFields/New/Fields/DateAndTimeFields/DateRangeField/DateRangeField.component.js @@ -2,6 +2,7 @@ import * as React from 'react'; import { withStyles, withTheme } from '@material-ui/core/styles'; import { DateRangeField as UIDateRangeField } from 'capture-ui'; +import { systemSettingsStore } from '../../../../../../metaDataMemoryStores'; const getStyles = (theme: Theme) => ({ innerInputError: { @@ -44,6 +45,7 @@ const DateRangeFieldPlain = (props: Props) => { return ( ); }; diff --git a/src/core_modules/capture-core/components/FormFields/New/Fields/DateAndTimeFields/DateTimeField/DateTimeField.component.js b/src/core_modules/capture-core/components/FormFields/New/Fields/DateAndTimeFields/DateTimeField/DateTimeField.component.js index a8d7a72d82..ca751a3c12 100644 --- a/src/core_modules/capture-core/components/FormFields/New/Fields/DateAndTimeFields/DateTimeField/DateTimeField.component.js +++ b/src/core_modules/capture-core/components/FormFields/New/Fields/DateAndTimeFields/DateTimeField/DateTimeField.component.js @@ -2,6 +2,7 @@ import * as React from 'react'; import { withStyles, withTheme } from '@material-ui/core/styles'; import { DateTimeField as UIDateTimeField } from 'capture-ui'; +import { systemSettingsStore } from '../../../../../../metaDataMemoryStores'; const getStyles = (theme: Theme) => ({ innerInputError: { @@ -36,6 +37,7 @@ class DateTimeFieldPlain extends React.Component { return ( ); } diff --git a/src/core_modules/capture-core/components/FormFields/New/Fields/DateAndTimeFields/DateTimeRangeField/DateTimeRangeField.component.js b/src/core_modules/capture-core/components/FormFields/New/Fields/DateAndTimeFields/DateTimeRangeField/DateTimeRangeField.component.js index 852b33525f..3b8d9abbed 100644 --- a/src/core_modules/capture-core/components/FormFields/New/Fields/DateAndTimeFields/DateTimeRangeField/DateTimeRangeField.component.js +++ b/src/core_modules/capture-core/components/FormFields/New/Fields/DateAndTimeFields/DateTimeRangeField/DateTimeRangeField.component.js @@ -2,6 +2,7 @@ import * as React from 'react'; import { withStyles, withTheme } from '@material-ui/core/styles'; import { DateTimeRangeField as UIDateTimeRangeField } from 'capture-ui'; +import { systemSettingsStore } from '../../../../../../metaDataMemoryStores'; const getStyles = (theme: Theme) => ({ innerInputError: { @@ -36,6 +37,7 @@ class DateTimeRangeFieldPlain extends React.Component { return ( ); } diff --git a/src/core_modules/capture-core/components/Notes/Notes.component.js b/src/core_modules/capture-core/components/Notes/Notes.component.js index 1f1174accb..9895d0ca03 100644 --- a/src/core_modules/capture-core/components/Notes/Notes.component.js +++ b/src/core_modules/capture-core/components/Notes/Notes.component.js @@ -199,7 +199,7 @@ const NotesPlain = ({
- + {moment(fromServerDate(n.storedAt)).fromNow()} diff --git a/src/core_modules/capture-core/components/WidgetNote/NoteSection/NoteSection.js b/src/core_modules/capture-core/components/WidgetNote/NoteSection/NoteSection.js index b800cacc28..91d18f4c3e 100644 --- a/src/core_modules/capture-core/components/WidgetNote/NoteSection/NoteSection.js +++ b/src/core_modules/capture-core/components/WidgetNote/NoteSection/NoteSection.js @@ -108,7 +108,7 @@ const NoteSectionPlain = ({ {createdBy.firstName} {' '} {createdBy.surname} } - + {moment(fromServerDate(storedAt)).fromNow()} diff --git a/src/core_modules/capture-core/converters/clientToList.js b/src/core_modules/capture-core/converters/clientToList.js index 18359666b7..5f7f347fa7 100644 --- a/src/core_modules/capture-core/converters/clientToList.js +++ b/src/core_modules/capture-core/converters/clientToList.js @@ -15,17 +15,12 @@ function convertDateForListDisplay(rawValue: string): string { } function convertDateTimeForListDisplay(rawValue: string): string { - const momentDate = moment(rawValue); + const momentDate = moment(rawValue).locale('en'); const timeString = momentDate.format('HH:mm'); const localDate = convertIsoToLocalCalendar(rawValue); return `${localDate} ${timeString}`; } -function convertTimeForListDisplay(rawValue: string): string { - const momentDate = moment(rawValue, 'HH:mm', true); - return momentDate.format('HH:mm'); -} - type FileClientValue = { name: string, url: string, @@ -122,8 +117,7 @@ const valueConvertersForType = { [dataElementTypes.PERCENTAGE]: (value: number) => `${stringifyNumber(value)} %`, [dataElementTypes.POLYGON]: convertPolygonForDisplay, [dataElementTypes.STATUS]: convertStatusForDisplay, - [dataElementTypes.TIME]: convertTimeForListDisplay, - [dataElementTypes.TIME_RANGE]: value => convertRangeForDisplay(convertTimeForListDisplay, value), + [dataElementTypes.TIME_RANGE]: value => convertRangeForDisplay(undefined, value), [dataElementTypes.TRUE_ONLY]: () => i18n.t('Yes'), }; diff --git a/src/core_modules/capture-core/converters/clientToView.js b/src/core_modules/capture-core/converters/clientToView.js index 68c7bed02c..24530798c0 100644 --- a/src/core_modules/capture-core/converters/clientToView.js +++ b/src/core_modules/capture-core/converters/clientToView.js @@ -14,17 +14,13 @@ function convertDateForView(rawValue: string): string { return convertIsoToLocalCalendar(rawValue); } function convertDateTimeForView(rawValue: string): string { - const momentDate = moment(rawValue); + const momentDate = moment(rawValue).locale('en'); const timeString = momentDate.format('HH:mm'); const localDate = convertIsoToLocalCalendar(rawValue); return `${localDate} ${timeString}`; } -function convertTimeForView(rawValue: string): string { - const momentDate = moment(rawValue, 'HH:mm', true); - return momentDate.format('HH:mm'); -} type FileClientValue = { name: string, url: string, @@ -67,7 +63,6 @@ const valueConvertersForType = { [dataElementTypes.PERCENTAGE]: (value: number) => `${stringifyNumber(value)} %`, [dataElementTypes.DATE]: convertDateForView, [dataElementTypes.DATETIME]: convertDateTimeForView, - [dataElementTypes.TIME]: convertTimeForView, [dataElementTypes.TRUE_ONLY]: () => i18n.t('Yes'), [dataElementTypes.BOOLEAN]: (rawValue: boolean) => (rawValue ? i18n.t('Yes') : i18n.t('No')), [dataElementTypes.COORDINATE]: MinimalCoordinates, diff --git a/src/core_modules/capture-core/metaData/SystemSettings/SystemSettings.js b/src/core_modules/capture-core/metaData/SystemSettings/SystemSettings.js index ec8334b437..5e481a3239 100644 --- a/src/core_modules/capture-core/metaData/SystemSettings/SystemSettings.js +++ b/src/core_modules/capture-core/metaData/SystemSettings/SystemSettings.js @@ -5,4 +5,5 @@ export class SystemSettings { dir: string; trackerAppRelativePath: string; calendar: string; + uiLocale: string; } diff --git a/src/core_modules/capture-core/metaDataStoreLoaders/systemSettings/cacheSystemSetttings.js b/src/core_modules/capture-core/metaDataStoreLoaders/systemSettings/cacheSystemSetttings.js index 308f785765..e09850360d 100644 --- a/src/core_modules/capture-core/metaDataStoreLoaders/systemSettings/cacheSystemSetttings.js +++ b/src/core_modules/capture-core/metaDataStoreLoaders/systemSettings/cacheSystemSetttings.js @@ -17,6 +17,11 @@ export async function cacheSystemSettings( id: 'dateFormat', value: systemSettings.dateFormat.toUpperCase(), }, + // This is a user setting, and both this and the dir property below should be placed somewhere else. Will do this in https://dhis2.atlassian.net/browse/DHIS2-19015. + { + id: 'uiLocale', + value: uiLocale, + }, { id: 'dir', value: isLangRTL(uiLocale) ? 'rtl' : 'ltr', diff --git a/src/core_modules/capture-core/utils/converters/date/convertIsoToLocalCalendar.js b/src/core_modules/capture-core/utils/converters/date/convertIsoToLocalCalendar.js index fa09f77c53..eb2fdbce78 100644 --- a/src/core_modules/capture-core/utils/converters/date/convertIsoToLocalCalendar.js +++ b/src/core_modules/capture-core/utils/converters/date/convertIsoToLocalCalendar.js @@ -18,7 +18,7 @@ export function convertIsoToLocalCalendar(isoDate: ?string): string { return ''; } - const momentDate = moment(isoDate); + const momentDate = moment(isoDate).locale('en'); if (!momentDate.isValid()) { return ''; } diff --git a/src/core_modules/capture-ui/AgeField/AgeField.component.js b/src/core_modules/capture-ui/AgeField/AgeField.component.js index a772560e5b..133eeaf928 100644 --- a/src/core_modules/capture-ui/AgeField/AgeField.component.js +++ b/src/core_modules/capture-ui/AgeField/AgeField.component.js @@ -57,6 +57,7 @@ type Props = { disabled?: ?boolean, dateFormat: ?string, calendarType: ?string, + locale?: string, }; function getCalculatedValues(dateValue: ?string, calendarType: string, dateFormat: string): AgeValues { diff --git a/src/core_modules/capture-ui/DateAndTimeFields/DateField/Date.component.js b/src/core_modules/capture-ui/DateAndTimeFields/DateField/Date.component.js index 8fa5d01b6e..05b5e6897b 100644 --- a/src/core_modules/capture-ui/DateAndTimeFields/DateField/Date.component.js +++ b/src/core_modules/capture-ui/DateAndTimeFields/DateField/Date.component.js @@ -23,6 +23,7 @@ type Props = { innerMessage?: any, dateFormat: ?string, calendarType: ?string, + locale?: string, }; type Validation = {| @@ -67,6 +68,7 @@ export class DateField extends React.Component { innerMessage, calendarType, dateFormat, + locale, } = this.props; const calculatedInputWidth = inputWidth || width; const calculatedCalendarWidth = calendarWidth || width; @@ -97,6 +99,7 @@ export class DateField extends React.Component { disabled={this.props.disabled} {...errorProps} maxDate={calendarMax} + locale={locale} />
); diff --git a/src/core_modules/capture-ui/DateAndTimeFields/DateRangeField/DateRangeField.component.js b/src/core_modules/capture-ui/DateAndTimeFields/DateRangeField/DateRangeField.component.js index 884a9eb8c2..23045c3865 100644 --- a/src/core_modules/capture-ui/DateAndTimeFields/DateRangeField/DateRangeField.component.js +++ b/src/core_modules/capture-ui/DateAndTimeFields/DateRangeField/DateRangeField.component.js @@ -26,6 +26,7 @@ type Props = { onChange: (value: ?DateRangeValue) => void, classes: Object, innerMessage?: ?Object, + locale?: string, } const inputKeys = { diff --git a/src/core_modules/capture-ui/DateAndTimeFields/DateTimeField/DateTime.component.js b/src/core_modules/capture-ui/DateAndTimeFields/DateTimeField/DateTime.component.js index a97077e1ff..4d2c5f282c 100644 --- a/src/core_modules/capture-ui/DateAndTimeFields/DateTimeField/DateTime.component.js +++ b/src/core_modules/capture-ui/DateAndTimeFields/DateTimeField/DateTime.component.js @@ -23,7 +23,8 @@ type Props = { classes: Object, dateLabel: string, timeLabel: string, - innerMessage: Object + innerMessage: Object, + locale?: string, }; type State = { diff --git a/src/core_modules/capture-ui/DateAndTimeFields/DateTimeRangeField/DateTimeRange.component.js b/src/core_modules/capture-ui/DateAndTimeFields/DateTimeRangeField/DateTimeRange.component.js index e16a915a59..1c604336a2 100644 --- a/src/core_modules/capture-ui/DateAndTimeFields/DateTimeRangeField/DateTimeRange.component.js +++ b/src/core_modules/capture-ui/DateAndTimeFields/DateTimeRangeField/DateTimeRange.component.js @@ -25,6 +25,7 @@ type Props = { value: DateTimeRangeValue, onBlur: (value: ?DateTimeRangeValue, options: Object) => void, onChange: (value: ?DateTimeRangeValue) => void, + locale?: string, }; type State = { From 77813733cd6ee2cbe171dce7344317278f6928e6 Mon Sep 17 00:00:00 2001 From: alaa-yahia Date: Tue, 18 Feb 2025 10:14:47 +0200 Subject: [PATCH 41/42] fix: remove unused updateAt --- .../WidgetEnrollmentNote/WidgetEnrollmentNote.epics.js | 1 - .../components/WidgetEventNote/WidgetEventNote.epics.js | 1 - 2 files changed, 2 deletions(-) diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.epics.js b/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.epics.js index 46e60fc834..87d308c9ba 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.epics.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.epics.js @@ -40,7 +40,6 @@ export const addNoteForEnrollmentEpic = (action$: InputObservable, store: ReduxS surname, uid: clientId, }, - updatedAt: moment().toISOString(), storedBy: userName, storedAt: fromClientDate(moment().toISOString()).getServerZonedISOString(), }; diff --git a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.epics.js b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.epics.js index 69be90b443..843f59d51c 100644 --- a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.epics.js +++ b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.epics.js @@ -50,7 +50,6 @@ export const addNoteForEventEpic = (action$: InputObservable, store: ReduxStore, surname, uid: clientId, }, - lastUpdated: moment().toISOString(), storedBy: userName, storedAt: fromClientDate(moment().toISOString()).getServerZonedISOString(), }; From 2318917cb362bd544733430a7fe0b2c5b5f8adde Mon Sep 17 00:00:00 2001 From: alaa-yahia Date: Wed, 26 Feb 2025 13:26:15 +0200 Subject: [PATCH 42/42] fix: runtime error when rules run --- src/core_modules/capture-core/converters/formToClient.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/core_modules/capture-core/converters/formToClient.js b/src/core_modules/capture-core/converters/formToClient.js index 187549d140..446f53a0d1 100644 --- a/src/core_modules/capture-core/converters/formToClient.js +++ b/src/core_modules/capture-core/converters/formToClient.js @@ -36,7 +36,11 @@ function convertDateTime(formValue: DateTimeValue): ?string { } function convertDate(dateValue: string) { - return convertLocalToIsoCalendar(dateValue); + try { + return convertLocalToIsoCalendar(dateValue); + } catch (error) { + return ''; + } } function convertTime(timeValue: string) {