Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
632 changes: 329 additions & 303 deletions i18n/en.pot

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import i18n from '@dhis2/d2-i18n';
import moment from 'moment';
import { type OrgUnit } from '@dhis2/rules-engine-javascript';
import { convertDateObjectToDateFormatString } from 'capture-core/utils/converters/date';
import {
getOrgUnitOpeningCalendarMin,
getOrgUnitClosingCalendarMax,
} from 'capture-core/utils/orgUnits/getOrgUnitCalendarBounds';
import { getOrgUnitLabel } from 'capture-core/utils/orgUnits/getOrgUnitLabel';
import { isLangRtl } from '../../../utils/rtl';
import {
DataEntry,
Expand Down Expand Up @@ -118,14 +123,14 @@ const getEnrollmentDateSettings = () => {
required: true,
calendarWidth: props.formHorizontal ? 250 : 350,
popupAnchorPosition: getCalendarAnchorPosition(props.formHorizontal),
calendarMax: !props.enrollmentMetadata.allowFutureEnrollmentDate ?
convertDateObjectToDateFormatString(moment()) :
undefined,
calendarMax: getOrgUnitClosingCalendarMax(props.orgUnit, !props.enrollmentMetadata.allowFutureEnrollmentDate),
calendarMin: getOrgUnitOpeningCalendarMin(props.orgUnit),
calendarType: systemSettingsStore.get().calendar,
dateFormat: systemSettingsStore.get().dateFormat,
}),
getPropName: () => 'enrolledAt',
getValidatorContainers: getEnrollmentDateValidatorContainer,
getValidatorContainers: (props: any) =>
getEnrollmentDateValidatorContainer(props?.orgUnit, getOrgUnitLabel(props?.programId)),
getPassOnFieldData: () => true,
getMeta: () => ({
placement: placements.TOP,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import i18n from '@dhis2/d2-i18n';
import { hasValue } from 'capture-core-utils/validators/form';
import { isValidDate, isValidNonFutureDate } from '../../../../utils/validation/validators/form';
import {
isValidDate,
isValidNonFutureDate,
getWithinOrgUnitDateRangeValidator,
} from '../../../../utils/validation/validators/form';
import type { OrgUnitDateRange } from '../../../../utils/orgUnits/getOrgUnitCalendarBounds';

const isValidEnrollmentDate = (value: string, internalComponentError?: {error?: string, errorCode?: string}) => {
if (!value) {
Expand All @@ -10,7 +15,7 @@ const isValidEnrollmentDate = (value: string, internalComponentError?: {error?:
return isValidDate(value, internalComponentError);
};

export const getEnrollmentDateValidatorContainer = () => {
export const getEnrollmentDateValidatorContainer = (orgUnit?: OrgUnitDateRange | null, orgUnitLabel?: string | null) => {
const validatorContainers = [
{
validator: hasValue,
Expand All @@ -24,6 +29,12 @@ export const getEnrollmentDateValidatorContainer = () => {
{ validator: isValidNonFutureDate,
errorMessage: i18n.t('A date in the future is not allowed'),
},
{
// The message is built by the validator itself (it interpolates the org unit's
// opening/closing dates and label), so the static fallback is intentionally empty.
validator: getWithinOrgUnitDateRangeValidator(orgUnit, orgUnitLabel),
errorMessage: '',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something is not right here. I am guessing there should have been a i18n string for error message here as well if the date is outside range.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: "Good question — it's already internationalized, just not as a static container string. The message is built dynamically inside the validator (getWithinOrgUnitDateRangeValidator → buildErrorMessage), where every variant goes through i18n.t(...) and interpolates the org unit's opening/closing dates and label. That's why the validator container's static errorMessage is intentionally left ''.

The framework picks the dynamic message over the static one: getValidationError in dataEntryField.utils.ts uses result.errorMessage || validatorContainer.errorMessage, so the static '' is only a fallback for when a validator returns no message — and this one always returns a non-empty, translated message on failure. It's the same pattern validateNotExpired already uses. I've added a clarifying comment at the container so this isn't mistaken for a missing string. Let me know if you'd still prefer a static string as well."

},
];
return validatorContainers;
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import type { ReduxAction } from 'capture-core-utils/types';
import i18n from '@dhis2/d2-i18n';
import { type OrgUnit } from '@dhis2/rules-engine-javascript';
import { isLangRtl } from '../../../../../utils/rtl';
import {
getOrgUnitOpeningCalendarMin,
getOrgUnitClosingCalendarMax,
} from '../../../../../utils/orgUnits/getOrgUnitCalendarBounds';
import { DataEntry as DataEntryContainer } from '../../../../DataEntry/DataEntry.container';
import { withCancelButton } from '../../../../DataEntry/withCancelButton';
import { withDataEntryField } from '../../../../DataEntry/dataEntryField/withDataEntryField';
Expand Down Expand Up @@ -175,6 +179,8 @@ const buildReportDateSettingsFn = () => {
calendarWidth: props.formHorizontal ? 250 : 350,
orientation: getOrientation(props.formHorizontal),
popupAnchorPosition: getCalendarAnchorPosition(props.formHorizontal),
calendarMax: getOrgUnitClosingCalendarMax(props.orgUnit),
calendarMin: getOrgUnitOpeningCalendarMin(props.orgUnit),
calendarType: systemSettingsStore.get().calendar,
dateFormat: systemSettingsStore.get().dateFormat,
}),
Expand Down Expand Up @@ -521,6 +527,7 @@ type Props = {
programName: string,
orgUnitId: string,
orgUnit: OrgUnit,
orgUnitWithDates?: OrgUnit,
orgUnitName: string,
stageName: string,
onUpdateDataEntryField: (orgUnit: OrgUnit) => (innerAction: ReduxAction<any, any>) => void,
Expand Down Expand Up @@ -650,6 +657,7 @@ class NewEventDataEntry extends Component<Props & WithStyles<typeof getStyles>>
onStartAsyncUpdateField,
programName, // eslint-disable-line
orgUnit,
orgUnitWithDates,
orgUnitName, // eslint-disable-line
classes,
onSave,
Expand All @@ -671,7 +679,7 @@ class NewEventDataEntry extends Component<Props & WithStyles<typeof getStyles>>
fieldOptions={this.fieldOptions}
dataEntrySections={this.dataEntrySections}
relationshipsRef={this.setRelationshipsInstance}
orgUnit={orgUnit}
orgUnit={orgUnitWithDates ?? orgUnit}
// @ts-expect-error - keeping original functionality as before ts rewrite
orgUnitId={orgUnit?.id}
{...passOnProps}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import { v4 as uuid } from 'uuid';
import { connect } from 'react-redux';
import { connect, useSelector } from 'react-redux';
import i18n from '@dhis2/d2-i18n';
import { batchActions } from 'redux-batched-actions';
import type { OrgUnit } from '@dhis2/rules-engine-javascript';
Expand All @@ -24,6 +25,7 @@ import {
} from './dataEntry.selectors';
import type { RenderFoundation } from '../../../../../metaData';
import { withLoadingIndicator, withErrorMessageHandler } from '../../../../../HOC';
import { useOrgUnitWithDates } from '../../../../../metadataRetrieval/coreOrgUnit';
import { newEventSaveTypes } from './newEventSaveTypes';

const makeMapStateToProps = () => {
Expand All @@ -32,8 +34,8 @@ const makeMapStateToProps = () => {
const mapStateToProps = (state: any, props: any) => ({
recentlyAddedRelationshipId: state.newEventPage.recentlyAddedRelationshipId,
ready: !state.activePage.isDataEntryLoading,
error: !props.formFoundation ?
i18n.t('This is not an event program or the metadata is corrupt. See log for details.') : null,
error: props.formFoundation ?
null : i18n.t('This is not an event program or the metadata is corrupt. See log for details.'),
programName: programNameSelector(state),
orgUnit: state.dataEntriesFieldsValue['singleEvent-newEvent']?.orgUnit,
orgUnitName: state.dataEntriesFieldsValue['singleEvent-newEvent']?.orgUnit?.name,
Expand Down Expand Up @@ -110,6 +112,17 @@ const mapDispatchToProps = (dispatch: any) => ({
},
});

export const DataEntry = connect(makeMapStateToProps, mapDispatchToProps)(
const ConnectedDataEntry = connect(makeMapStateToProps, mapDispatchToProps)(
withLoadingIndicator()(withErrorMessageHandler()(DataEntryComponent)),
);

// The org unit is selected within the form (and lives in redux), so we enrich the field value
// here — not in the parent wrapper — with its opening/closing dates, allowing the occurred-date
// field to enforce the org unit's open/close range. The bare `orgUnit` (save/rules) is untouched.
export const DataEntry = (props: any) => {
const orgUnitFieldValue = useSelector(
(state: any) => state.dataEntriesFieldsValue['singleEvent-newEvent']?.orgUnit,
);
const orgUnitWithDates = useOrgUnitWithDates(orgUnitFieldValue);
return <ConnectedDataEntry {...props} orgUnitWithDates={orgUnitWithDates} />;
};
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { hasValue } from 'capture-core-utils/validators/form';
import i18n from '@dhis2/d2-i18n';
import { isValidDate, isValidPeriod } from '../../../../../../utils/validation/validators/form';
import {
isValidDate,
isValidPeriod,
getWithinOrgUnitDateRangeValidator,
} from '../../../../../../utils/validation/validators/form';
import { convertFormToClient } from '../../../../../../converters';
import { dataElementTypes } from '../../../../../../metaData';
import { getOrgUnitLabel } from '../../../../../../utils/orgUnits/getOrgUnitLabel';

const preValidateDate = (
value?: string,
Expand Down Expand Up @@ -47,4 +52,10 @@ export const getEventDateValidatorContainers = (props?: any) => [
validator: (value: string) => validateNotExpired(value, props),
errorMessage: '',
},
{
// The message is built by the validator itself (it interpolates the org unit's
// opening/closing dates and label), so the static fallback is intentionally empty.
validator: getWithinOrgUnitDateRangeValidator(props?.orgUnit, getOrgUnitLabel(props?.programId)),
errorMessage: '',
},
];
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ import {
ModalActions,
ButtonStrip,
Button,
NoticeBox,
} from '@dhis2/ui';
import i18n from '@dhis2/d2-i18n';
import { isIsoDateWithinOrgUnitRange } from 'capture-core/utils/validation/validators/form';
import { getOrgUnitLabel } from 'capture-core/utils/orgUnits/getOrgUnitLabel';
import type { TransferModalProps } from './TransferModal.types';
import { OrgUnitField } from './OrgUnitField';
import { useTransferValidation } from './hooks/useTransferValidation';
import { InfoBoxes } from './InfoBoxes';
import { useCoreOrgUnit } from '../../../metadataRetrieval/coreOrgUnit';

export const TransferModal = ({
enrollment,
Expand All @@ -31,6 +35,12 @@ export const TransferModal = ({
ownerOrgUnitId,
});

// The enrollment date must fall within the destination org unit's opening/closing range
const { orgUnit: destinationOrgUnit } = useCoreOrgUnit(selectedOrgUnit?.id ?? '');
const enrollmentDateOutsideRange = !!selectedOrgUnit
&& !isIsoDateWithinOrgUnitRange(enrollment.enrolledAt, destinationOrgUnit);
const orgUnitLabel = getOrgUnitLabel(enrollment.program) || i18n.t('organisation unit');

const handleOnUpdateOwnership = async () => {
if (!selectedOrgUnit) return;
await onUpdateOwnership({
Expand Down Expand Up @@ -65,6 +75,22 @@ export const TransferModal = ({
programAccessLevel={programAccessLevel}
orgUnitScopes={orgUnitScopes}
/>

{enrollmentDateOutsideRange && (
<NoticeBox
error
title={i18n.t('Cannot transfer to this {{orgUnitLabel}}', {
orgUnitLabel,
interpolation: { escapeValue: false },
})}
dataTest={'transfer-enrollment-date-out-of-range'}
>
{i18n.t(
"The enrollment date is outside the selected {{orgUnitLabel}}'s opening and closing dates.",
{ orgUnitLabel, interpolation: { escapeValue: false } },
)}
</NoticeBox>
)}
</ModalContent>

<ModalActions>
Expand All @@ -76,7 +102,7 @@ export const TransferModal = ({
<Button
dataTest={'widget-enrollment-transfer-button'}
primary
disabled={!ready || !selectedOrgUnit}
disabled={!ready || !selectedOrgUnit || enrollmentDateOutsideRange}
loading={isTransferLoading}
onClick={handleOnUpdateOwnership}
>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { UpdateEnrollmentOwnership } from '../Actions/Transfer/hooks/useUpdateOwnership';

export type TransferModalProps = {
enrollment: { program: string };
enrollment: { program: string, enrolledAt?: string };
ownerOrgUnitId: string;
setOpenTransfer: (toggle: boolean) => void;
onUpdateOwnership: UpdateEnrollmentOwnership;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import type { WithStyles } from 'capture-core-utils/styles';
import { withStyles, withTheme } from 'capture-core-utils/styles';
import i18n from '@dhis2/d2-i18n';
import { isLangRtl } from '../../../utils/rtl';
import {
getOrgUnitOpeningCalendarMin,
getOrgUnitClosingCalendarMax,
} from '../../../utils/orgUnits/getOrgUnitCalendarBounds';
import { DataEntry as DataEntryContainer } from '../../DataEntry/DataEntry.container';
import { withDataEntryField } from '../../DataEntry/dataEntryField/withDataEntryField';
import { withDataEntryNotesHandler } from '../../DataEntry/dataEntryNotes/withDataEntryNotesHandler';
Expand Down Expand Up @@ -160,6 +164,8 @@ const buildReportDateSettingsFn = () => {
calendarWidth: props.formHorizontal ? 250 : 350,
orientation: getOrientation(props.formHorizontal),
popupAnchorPosition: getCalendarAnchorPosition(props.formHorizontal),
calendarMax: getOrgUnitClosingCalendarMax(props.orgUnit),
calendarMin: getOrgUnitOpeningCalendarMin(props.orgUnit),
calendarType: systemSettingsStore.get().calendar,
dateFormat: systemSettingsStore.get().dateFormat,
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@ import {
import type { AddEventSaveType } from './addEventSaveTypes';
import type { ContainerProps } from './dataEntry.types';
import { useProgramExpiryForUser } from '../../../hooks';
import { useOrgUnitWithDates } from '../../../metadataRetrieval/coreOrgUnit';

export const DataEntry = ({ rulesExecutionDependenciesClientFormatted, id, ...passOnProps }: ContainerProps) => {
const dispatch = useDispatch();
const { programId } = useSelector(({ currentSelections }: any) => currentSelections);
const dataEntryItemId = useSelector(({ dataEntries }: any) => dataEntries[id] && dataEntries[id].itemId);
const dataEntryKey = getDataEntryKey(id, dataEntryItemId);
const orgUnitFieldValue = useSelector(({ dataEntriesFieldsValue }: any) => dataEntriesFieldsValue[dataEntryKey].orgUnit);
// Enrich the selected org unit with its opening/closing dates so event date fields can enforce them
const orgUnitFieldValueWithDates = useOrgUnitWithDates(orgUnitFieldValue);
const expiryPeriod = useProgramExpiryForUser(programId);
const onUpdateDataEntryField = useCallback((innerAction: ReduxAction<any, any>) => {
const { dataEntryId, itemId } = innerAction.payload;
Expand Down Expand Up @@ -80,7 +83,7 @@ export const DataEntry = ({ rulesExecutionDependenciesClientFormatted, id, ...pa
<DataEntryComponent
{...passOnProps}
id={id}
orgUnitFieldValue={orgUnitFieldValue}
orgUnitFieldValue={orgUnitFieldValueWithDates}
programId={programId}
expiryPeriod={expiryPeriod}
onUpdateDataEntryField={onUpdateDataEntryField}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { hasValue } from 'capture-core-utils/validators/form';
import i18n from '@dhis2/d2-i18n';
import { isValidDate, isValidPeriod } from '../../../../utils/validation/validators/form';
import {
isValidDate,
isValidPeriod,
getWithinOrgUnitDateRangeValidator,
} from '../../../../utils/validation/validators/form';
import { convertFormToClient } from '../../../../converters';
import { dataElementTypes } from '../../../../metaData';
import { getOrgUnitLabel } from '../../../../utils/orgUnits/getOrgUnitLabel';

const preValidateDate = (
value?: string | null,
Expand Down Expand Up @@ -47,4 +52,10 @@ export const getEventDateValidatorContainers = (props?: any) => [
validator: (value: string) => validateNotExpired(value, props),
errorMessage: '',
},
{
// The message is built by the validator itself (it interpolates the org unit's
// opening/closing dates and label), so the static fallback is intentionally empty.
validator: getWithinOrgUnitDateRangeValidator(props?.orgUnit, getOrgUnitLabel(props?.programId)),
errorMessage: '',
},
];
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import i18n from '@dhis2/d2-i18n';
import { hasValue } from 'capture-core-utils/validators/form';
import { isValidDate, isValidPeriod } from '../../../../utils/validation/validators/form';
import {
isValidDate,
isValidPeriod,
getWithinOrgUnitDateRangeValidator,
} from '../../../../utils/validation/validators/form';
import { convertFormToClient } from '../../../../converters';
import { dataElementTypes } from '../../../../metaData';
import { getOrgUnitLabel } from '../../../../utils/orgUnits/getOrgUnitLabel';

const preValidateDate = (value?: string, internalComponentError?: {error?: string, errorCode?: string}) => {
if (!value) {
Expand Down Expand Up @@ -43,4 +48,13 @@ export const getEventDateValidatorContainers = (props: Record<string, unknown> =
validator: (value: string) => validateNotExpired(value, props),
errorMessage: '',
},
{
// The message is built by the validator itself (it interpolates the org unit's
// opening/closing dates and label), so the static fallback is intentionally empty.
validator: getWithinOrgUnitDateRangeValidator(
props?.orgUnit as { openingDate?: string, closedDate?: string },
getOrgUnitLabel(props?.programId as string | undefined),
),
errorMessage: '',
},
];
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import i18n from '@dhis2/d2-i18n';
import type { OrgUnit } from '@dhis2/rules-engine-javascript';
import type { ReduxAction } from 'capture-core-utils/types';
import { getEventDateValidatorContainers } from '../DataEntry/fieldValidators/eventDate.validatorContainersGetter';
import {
getOrgUnitOpeningCalendarMin,
getOrgUnitClosingCalendarMax,
} from '../../../utils/orgUnits/getOrgUnitCalendarBounds';
import { withMainButton } from '../DataEntry/withMainButton';
import type { RenderFoundation } from '../../../metaData';
import { WidgetEventSchedule } from '../../WidgetEventSchedule';
Expand Down Expand Up @@ -139,6 +143,8 @@ const buildReportDateSettingsFn = () => {
calendarWidth: 350,
label: props.formFoundation.getLabel('occurredAt'),
required: true,
calendarMax: getOrgUnitClosingCalendarMax(props.orgUnit),
calendarMin: getOrgUnitOpeningCalendarMin(props.orgUnit),
calendarType: systemSettingsStore.get().calendar,
dateFormat: systemSettingsStore.get().dateFormat,
}),
Expand Down
Loading
Loading