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-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/date/padWithZeros.js b/src/core_modules/capture-core-utils/date/padWithZeros.js new file mode 100644 index 0000000000..5edc85befd --- /dev/null +++ b/src/core_modules/capture-core-utils/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-core-utils/date/stringToTemporal.js b/src/core_modules/capture-core-utils/date/stringToTemporal.js new file mode 100644 index 0000000000..3be34f0775 --- /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, + calendar: ?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, + calendar, + }); + } 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..9acf29bb09 --- /dev/null +++ b/src/core_modules/capture-core-utils/date/temporalToString.js @@ -0,0 +1,36 @@ +// @flow +import { systemSettingsStore } from 'capture-core/metaDataMemoryStores'; +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, + eraYear: number +}; + +export function temporalToString(temporalDate: PlainDate | null, dateFormat: ?string): string { + if (!temporalDate) { + return ''; + } + + try { + const calendar = systemSettingsStore.get().calendar; + const year = calendar === 'ethiopian' ? temporalDate.eraYear : 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/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/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 7c6f480ae7..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 @@ -2,6 +2,8 @@ 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'; @@ -15,7 +17,9 @@ 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, + 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/components/D2Form/field/configs/dateField/getDateFieldConfigForCustomForm.js b/src/core_modules/capture-core/components/D2Form/field/configs/dateField/getDateFieldConfigForCustomForm.js index b1b93fe119..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 @@ -2,6 +2,8 @@ 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'; @@ -10,7 +12,9 @@ export const getDateFieldConfigForCustomForm = (metaData: DateDataElement, optio width: 350, maxWidth: 350, calendarWidth: 350, - calendarMaxMoment: !metaData.allowFutureDate ? moment() : undefined, + 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/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/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/EnrollmentDataEntry.component.js b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentDataEntry.component.js index 1444a98aea..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 @@ -42,6 +42,8 @@ import { withAOCFieldBuilder, withDataEntryFields, } from '../../DataEntryDhis2Helpers'; +import { convertDateObjectToDateFormatString } from '../../../../capture-core/utils/converters/date'; +import { systemSettingsStore } from '../../../metaDataMemoryStores'; const overrideMessagePropNames = { errorMessage: 'validationError', @@ -111,7 +113,11 @@ const getEnrollmentDateSettings = () => { required: true, calendarWidth: props.formHorizontal ? 250 : 350, popupAnchorPosition: getCalendarAnchorPosition(props.formHorizontal), - calendarMaxMoment: !props.enrollmentMetadata.allowFutureEnrollmentDate ? moment() : undefined, + calendarMax: !props.enrollmentMetadata.allowFutureEnrollmentDate ? + convertDateObjectToDateFormatString(moment()) : + undefined, + calendarType: systemSettingsStore.get().calendar, + dateFormat: systemSettingsStore.get().dateFormat, }), getPropName: () => 'enrolledAt', getValidatorContainers: getEnrollmentDateValidatorContainer, @@ -159,7 +165,11 @@ const getIncidentDateSettings = () => { required: true, calendarWidth: props.formHorizontal ? 250 : 350, popupAnchorPosition: getCalendarAnchorPosition(props.formHorizontal), - calendarMaxMoment: !props.enrollmentMetadata.allowFutureIncidentDate ? moment() : undefined, + 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/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/FiltersForTypes/Date/DateFilter.component.js b/src/core_modules/capture-core/components/FiltersForTypes/Date/DateFilter.component.js index b0fa8f967e..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 @@ -3,6 +3,7 @@ import React, { Component } from 'react'; import classNames from 'classnames'; 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'; @@ -16,7 +17,7 @@ import './calendarFilterStyles.css'; import { mainOptionKeys, mainOptionTranslatedTexts } from './options'; import { getDateFilterData } from './dateFilterDataGetter'; import { RangeFilter } from './RangeFilter.component'; -import { parseDate } from '../../../utils/converters/date'; +import { convertStringToDateFormat } from '../../../utils/converters/date'; const getStyles = (theme: Theme) => ({ fromToContainer: { @@ -117,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) => { @@ -186,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; 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/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/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/components/ListView/Filters/FilterButton/buttonTextBuilder/converters/dateConverter.js b/src/core_modules/capture-core/components/ListView/Filters/FilterButton/buttonTextBuilder/converters/dateConverter.js index 4dce8ba7a9..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,8 +1,7 @@ // @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'; @@ -30,11 +29,6 @@ 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 = filter.ge; @@ -44,18 +38,18 @@ function translateAbsoluteDate(filter: AbsoluteDateFilterData) { 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; 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..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 @@ -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, @@ -11,9 +10,13 @@ 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'; import { dataElementTypes } from '../../../metaData'; + type Props = { date: string, dateLabel: string, @@ -24,7 +27,7 @@ type Props = { ...CssClasses, } -const styles = { +const styles = (theme: Theme) => ({ editButton: { display: 'inline-flex', alignItems: 'center', @@ -62,7 +65,11 @@ const styles = { fontSize: '12px', color: colors.grey700, }, -}; + error: { + ...theme.typography.caption, + color: theme.palette.error.main, + }, +}); const DateComponentPlain = ({ date, @@ -75,21 +82,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 +108,26 @@ const DateComponentPlain = ({ return editMode ? (
- +
+ {validation && validation.error ? i18n.t('Please provide a valid date') : ''} +
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/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 e958a69644..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 @@ -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(), @@ -184,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} />
} ( -
- {/* 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/WidgetRelatedStages/FormComponents/DateFieldForRelatedStages.js b/src/core_modules/capture-core/components/WidgetRelatedStages/FormComponents/DateFieldForRelatedStages.js index cbb55a1ef5..df5c37f2d3 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/FormComponents/DateFieldForRelatedStages.js +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/FormComponents/DateFieldForRelatedStages.js @@ -9,6 +9,7 @@ import { } from '../../FormFields/New'; import labelTypeClasses from './dataEntryFieldLabels.module.css'; import { baseInputStyles } from './commonProps'; +import { systemSettingsStore } from '../../../metaDataMemoryStores'; import type { ErrorMessagesForRelatedStages } from '../RelatedStagesActions'; import type { RelatedStageDataValueStates } from '../WidgetRelatedStages.types'; @@ -46,7 +47,8 @@ export const DateFieldForRelatedStages = ({ setTouched(true); onBlurDateField(event, internalComponentError); }; - + const calendarType = systemSettingsStore.get().calendar; + const dateFormat = systemSettingsStore.get().dateFormat; const shouldShowError = (touched || saveAttempted); return ( ); }; 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 f80831a9a7..ef519fe761 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,7 +3,7 @@ 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 { convertIsoToLocalCalendar } from '../../../../../../utils/converters/date'; import { getSubValues } from '../../getEventDataWithSubValue'; import type { StageDataElement } from '../../../../types/common.types'; import { Notes } from '../Notes.component'; @@ -36,7 +36,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: convertIsoToLocalCalendar(event.scheduledAt) }; } return { status: event.status, options: undefined }; }; 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()} diff --git a/src/core_modules/capture-core/converters/clientToForm.js b/src/core_modules/capture-core/converters/clientToForm.js index f8e227b6d9..d8cec38fd3 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 { convertIsoToLocalCalendar } from '../utils/converters/date'; import { dataElementTypes } from '../metaData'; import { stringifyNumber } from './common/stringifyNumber'; @@ -23,16 +23,14 @@ type RangeValue = { } function convertDateForEdit(rawValue: string): string { - const momentInstance = moment(rawValue); - return convertMomentToDateFormatString(momentInstance); + return convertIsoToLocalCalendar(rawValue); } function convertDateTimeForEdit(rawValue: string): DateTimeFormValue { const dateTime = moment(rawValue); - const dateString = convertMomentToDateFormatString(dateTime); const timeString = dateTime.format('HH:mm'); return { - date: dateString, + date: convertIsoToLocalCalendar(rawValue), time: timeString, }; } @@ -55,7 +53,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(), diff --git a/src/core_modules/capture-core/converters/clientToList.js b/src/core_modules/capture-core/converters/clientToList.js index abbfbf6975..d148d1f871 100644 --- a/src/core_modules/capture-core/converters/clientToList.js +++ b/src/core_modules/capture-core/converters/clientToList.js @@ -5,21 +5,20 @@ 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, PolygonCoordinates } from '../components/Coordinates'; import { TooltipOrgUnit } from '../components/Tooltips/TooltipOrgUnit'; function convertDateForListDisplay(rawValue: string): string { - const momentDate = moment(rawValue); - return convertMomentToDateFormatString(momentDate); + return convertIsoToLocalCalendar(rawValue); } function convertDateTimeForListDisplay(rawValue: string): string { const momentDate = moment(rawValue); - const dateString = convertMomentToDateFormatString(momentDate); const timeString = momentDate.format('HH:mm'); - return `${dateString} ${timeString}`; + const localDate = convertIsoToLocalCalendar(rawValue); + return `${localDate} ${timeString}`; } function convertTimeForListDisplay(rawValue: string): string { @@ -63,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)} @@ -104,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(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.FILE_RESOURCE]: convertFileForDisplay, [dataElementTypes.IMAGE]: convertImageForDisplay, [dataElementTypes.INTEGER]: stringifyNumber, diff --git a/src/core_modules/capture-core/converters/clientToView.js b/src/core_modules/capture-core/converters/clientToView.js index 826c637456..68c7bed02c 100644 --- a/src/core_modules/capture-core/converters/clientToView.js +++ b/src/core_modules/capture-core/converters/clientToView.js @@ -4,22 +4,23 @@ 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/Coordinates'; import { TooltipOrgUnit } from '../components/Tooltips/TooltipOrgUnit'; function convertDateForView(rawValue: string): string { - const momentDate = moment(rawValue); - return convertMomentToDateFormatString(momentDate); + return convertIsoToLocalCalendar(rawValue); } function convertDateTimeForView(rawValue: string): string { const momentDate = moment(rawValue); - const dateString = convertMomentToDateFormatString(momentDate); const timeString = momentDate.format('HH:mm'); - return `${dateString} ${timeString}`; + + const localDate = convertIsoToLocalCalendar(rawValue); + 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..187549d140 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 { convertLocalToIsoCalendar } from '../utils/converters/date'; type DateTimeValue = { date: string, @@ -24,19 +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)) return null; - // $FlowFixMe[incompatible-type] automated comment - const momentDateTime: moment$Moment = parsedDate.momentDate; + const isoDate = convertDate(editedDate); + if (!isoDate) return null; + + 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 ? parsedDate.momentDate.toISOString() : null; + return convertLocalToIsoCalendar(dateValue); } 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..fa09f77c53 --- /dev/null +++ b/src/core_modules/capture-core/utils/converters/date/convertIsoToLocalCalendar.js @@ -0,0 +1,37 @@ +// @flow +import moment from 'moment'; +import { + convertFromIso8601, +} from '@dhis2/multi-calendar-dates'; +import { systemSettingsStore } from '../../../../capture-core/metaDataMemoryStores'; +import { padWithZeros } from '../../../../capture-core-utils/date'; + +/** + * 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 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(formattedIsoDate, 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..408116d011 --- /dev/null +++ b/src/core_modules/capture-core/utils/converters/date/convertLocalToIsoCalendar.js @@ -0,0 +1,27 @@ +// @flow +import moment from 'moment'; +import { + convertToIso8601, +} from '@dhis2/multi-calendar-dates'; +import { systemSettingsStore } from '../../../../capture-core/metaDataMemoryStores'; +import { padWithZeros } from '../../../../capture-core-utils/date'; + +/** + * 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/convertStringToTemporal.js b/src/core_modules/capture-core/utils/converters/date/convertStringToTemporal.js new file mode 100644 index 0000000000..1c53443cc6 --- /dev/null +++ b/src/core_modules/capture-core/utils/converters/date/convertStringToTemporal.js @@ -0,0 +1,25 @@ +// @flow +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 + * @export + * @param {*} string - dateString + * @returns {(Temporal.PlainDate | null)} + */ + +type PlainDate = { + year: number, + month: number, + day: number +}; + +export function convertStringToTemporal(dateString: ?string, calendar: ?string, format: ?string): PlainDate | null { + if (!dateString) { + return null; + } + 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 new file mode 100644 index 0000000000..2f2672c3e6 --- /dev/null +++ b/src/core_modules/capture-core/utils/converters/date/convertTemporalToString.js @@ -0,0 +1,24 @@ +// @flow +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) + * @param {Temporal.PlainDate} temporalDate - The Temporal date to convert + * @returns {string} Formatted date string, or empty string if invalid + */ + +type PlainDate = { + year: number, + month: number, + day: number, + eraYear: number +}; + +export function convertTemporalToString(temporalDate: PlainDate | null, format: ?string): string { + if (!temporalDate) { + return ''; + } + const dateFormat = format || systemSettingsStore.get().dateFormat; + return temporalToString(temporalDate, dateFormat); +} 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..1f91c521cc 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..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,8 @@ // @flow -export { parseDate } from './parser'; export { convertDateObjectToDateFormatString } from './dateObjectToDateFormatString'; export { convertMomentToDateFormatString } from './momentToDateFormatString'; export { convertStringToDateFormat } from './stringToMomentDateFormat'; +export { convertIsoToLocalCalendar } from './convertIsoToLocalCalendar'; +export { convertLocalToIsoCalendar } from './convertLocalToIsoCalendar'; +export { convertStringToTemporal } from './convertStringToTemporal'; +export { convertTemporalToString } from './convertTemporalToString'; 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 eed1df7957..f75fb68255 100644 --- a/src/core_modules/capture-core/utils/converters/date/stringToMomentDateFormat.js +++ b/src/core_modules/capture-core/utils/converters/date/stringToMomentDateFormat.js @@ -6,11 +6,12 @@ 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) { +export function convertStringToDateFormat(date: ?string, format?: string) { 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; } 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..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,34 +1,38 @@ // @flow +import { Temporal } from '@js-temporal/polyfill'; +import { convertStringToTemporal } from 'capture-core/utils/converters/date'; 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 dateInTemporal = convertStringToTemporal(date); + + if (!dateInTemporal) { + return null; + } + const { year, month, day } = dateInTemporal; + 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 +50,10 @@ 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); + if (!fromDateTime || !toDateTime) { + return false; + } + return Temporal.PlainDateTime.compare(fromDateTime, toDateTime) <= 0; }; diff --git a/src/core_modules/capture-ui/AgeField/AgeField.component.js b/src/core_modules/capture-ui/AgeField/AgeField.component.js index a125ea8900..215e51ea87 100644 --- a/src/core_modules/capture-ui/AgeField/AgeField.component.js +++ b/src/core_modules/capture-ui/AgeField/AgeField.component.js @@ -1,5 +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'; @@ -10,6 +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'; type AgeValues = { date?: ?string, @@ -25,10 +31,6 @@ type InputMessageClasses = { validating?: ?string, } -type DateParser = (value: string) => { isValid: boolean, momentDate: any }; - -type DateStringFromMomentFormatter = (momentValue: Object) => string; - type ValidationOptions = { error?: ?string, errorCode?: ?string, @@ -45,9 +47,6 @@ type Props = { inputMessageClasses: ?InputMessageClasses, inFocus?: ?boolean, shrinkDisabled?: ?boolean, - onParseDate: DateParser, - onGetFormattedDateStringFromMoment: DateStringFromMomentFormatter, - moment: any, dateCalendarTheme: Object, dateCalendarWidth?: ?any, datePopupAnchorPosition?: ?string, @@ -56,38 +55,25 @@ type Props = { dateCalendarOnConvertValueOut: (value: string) => string, datePlaceholder?: ?string, disabled?: ?boolean, + dateFormat: ?string, + calendarType: ?string, }; -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 now = moment(); - const age = moment(parseData.momentDate); - const years = now.diff(age, 'years'); - age.add(years, 'years'); +function getCalculatedValues(dateValue: ?string, calendarType: ?string): AgeValues { + const nowIso = Temporal.Now.plainDateISO(); - const months = now.diff(age, 'months'); - age.add(months, 'months'); + const ageIso = convertToIso8601(dateValue, calendarType); - const days = now.diff(age, 'days'); + const diff = nowIso.since(ageIso, { + largestUnit: 'years', + smallestUnit: 'days', + }); return { - date: onGetFormattedDateStringFromMoment(parseData.momentDate), - years: years.toString(), - months: months.toString(), - days: days.toString(), + date: dateValue, + years: diff.years.toString(), + months: diff.months.toString(), + days: diff.days.toString(), }; } @@ -121,7 +107,7 @@ class D2AgeFieldPlain extends Component { } handleNumberBlur = (values: AgeValues) => { - const { onParseDate, onGetFormattedDateStringFromMoment, onRemoveFocus, moment } = this.props; + const { onRemoveFocus, calendarType = 'gregory', dateFormat = 'YYYY-MM-DD' } = this.props; onRemoveFocus && onRemoveFocus(); if (D2AgeFieldPlain.isEmptyNumbers(values)) { @@ -134,27 +120,38 @@ class D2AgeFieldPlain extends Component { return; } - const momentDate = moment(undefined, undefined, true); - 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 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); this.props.onBlur(calculatedValues); } handleDateBlur = (date: ?string, options: ?ValidationOptions) => { - const { onParseDate, onGetFormattedDateStringFromMoment, onRemoveFocus, moment } = this.props; + const { onRemoveFocus, calendarType = 'gregory' } = this.props; onRemoveFocus && onRemoveFocus(); - const calculatedValues = date ? getCalculatedValues( - date, - onParseDate, - onGetFormattedDateStringFromMoment, - moment) : 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); this.props.onBlur(calculatedValues, options); } @@ -181,8 +178,6 @@ class D2AgeFieldPlain extends Component { datePopupAnchorPosition, dateCalendarTheme, dateCalendarLocale, - moment, - onParseDate, ...passOnProps } = this.props; return (
@@ -208,8 +203,6 @@ class D2AgeFieldPlain extends Component { shrinkDisabled, dateCalendarWidth, datePlaceholder, - moment, - onParseDate, ...passOnProps } = this.props; 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..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, @@ -18,11 +17,12 @@ type Props = { onBlur: (value: Object, options: ValidationOptions) => void, onFocus?: ?() => void, onDateSelectedFromCalendar?: () => void, - calendar?: string, placeholder?: string, label?: string, - calendarMaxMoment?: any, - innerMessage?: any + calendarMax?: any, + innerMessage?: any, + dateFormat: ?string, + calendarType: ?string, }; type Validation = {| @@ -36,9 +36,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,16 +62,16 @@ export class DateField extends React.Component { maxWidth, calendarWidth, inputWidth, - calendar, - calendarMaxMoment, + calendarMax, value, innerMessage, + calendarType, + dateFormat, } = this.props; - const calculatedInputWidth = inputWidth || width; const calculatedCalendarWidth = calendarWidth || width; - const calendarType = 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 } @@ -92,14 +89,14 @@ 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)} onFocus={this.props.onFocus} disabled={this.props.disabled} {...errorProps} - maxDate={calendarMaxMoment && formatDate(calendarMaxMoment, format)} + maxDate={calendarMax} />
);