From e4e6b9bbaccfe0506adc0d29b5cc8b6202dbc232 Mon Sep 17 00:00:00 2001 From: henrikmv <henrik@devOtta.no> Date: Thu, 2 Jan 2025 13:54:38 +0100 Subject: [PATCH 1/7] feat: add org unit selector in schedule event form --- i18n/en.pot | 31 +-- .../DataEntry/dataEntryFieldLabels.module.css | 2 +- .../InfoBox/InfoBox.component.js | 71 ++++--- .../ScheduleDate/ScheduleDate.component.js | 112 +++++++---- .../dataEntryFieldLabels.module.css | 3 + .../ScheduleDate/scheduleDate.types.js | 1 + .../ScheduleOrgUnit.component.js | 74 +++++++ .../ScheduleOrgUnit/commonProps.js | 6 + .../dataEntryFieldLabels.module.css | 3 + .../WidgetEventSchedule.component.js | 180 +++++++++--------- .../WidgetEventSchedule.container.js | 38 ++-- .../widgetEventSchedule.types.js | 4 +- 12 files changed, 336 insertions(+), 189 deletions(-) create mode 100644 src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleDate/dataEntryFieldLabels.module.css create mode 100644 src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleOrgUnit/ScheduleOrgUnit.component.js create mode 100644 src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleOrgUnit/commonProps.js create mode 100644 src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleOrgUnit/dataEntryFieldLabels.module.css diff --git a/i18n/en.pot b/i18n/en.pot index 2f414dc91a..438f69e05b 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2024-12-05T11:39:04.447Z\n" -"PO-Revision-Date: 2024-12-05T11:39:04.447Z\n" +"POT-Creation-Date: 2025-01-02T12:54:39.984Z\n" +"PO-Revision-Date: 2025-01-02T12:54:39.984Z\n" msgid "Choose one or more dates..." msgstr "Choose one or more dates..." @@ -1270,6 +1270,12 @@ msgstr "Write a note about this event" msgid "This event doesn't have any notes" msgstr "This event doesn't have any notes" +msgid "after" +msgstr "after" + +msgid "before" +msgstr "before" + msgid "Schedule date info" msgstr "Schedule date info" @@ -1284,32 +1290,32 @@ msgid_plural "The scheduled date is {{count}} days {{position}} the suggested da msgstr[0] "The scheduled date is {{count}} day {{position}} the suggested date." msgstr[1] "The scheduled date is {{count}} days {{position}} the suggested date." -msgid "after" -msgstr "after" - -msgid "before" -msgstr "before" - msgid "There are {{count}} scheduled event in {{orgUnitName}} on this day." msgid_plural "There are {{count}} scheduled event in {{orgUnitName}} on this day." msgstr[0] "There are {{count}} scheduled event in {{orgUnitName}} on this day." msgstr[1] "There are {{count}} scheduled events in {{orgUnitName}} on this day." +msgid "Schedule date / Due date" +msgstr "Schedule date / Due date" + +msgid "Please provide a valid organisation unit" +msgstr "Please provide a valid organisation unit" + msgid "Scheduling an event in {{stageName}} for {{programName}} in {{orgUnitName}}" msgstr "Scheduling an event in {{stageName}} for {{programName}} in {{orgUnitName}}" msgid "Schedule info" msgstr "Schedule info" -msgid "Schedule date / Due date" -msgstr "Schedule date / Due date" - msgid "Event notes" msgstr "Event notes" msgid "Write a note about this scheduled event" msgstr "Write a note about this scheduled event" +msgid "Program or stage is invalid" +msgstr "Program or stage is invalid" + msgid "Save note" msgstr "Save note" @@ -1406,9 +1412,6 @@ msgstr "Report date" msgid "Please enter a date" msgstr "Please enter a date" -msgid "Please provide a valid organisation unit" -msgstr "Please provide a valid organisation unit" - msgid "Please select a valid event" msgstr "Please select a valid event" diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/dataEntryFieldLabels.module.css b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/dataEntryFieldLabels.module.css index 19bf2d5799..27d4299344 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/dataEntryFieldLabels.module.css +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/dataEntryFieldLabels.module.css @@ -41,7 +41,7 @@ } .orgUnitLabel { - padding-top: 3px; + padding-top: 13px; } .selectLabel { diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/InfoBox/InfoBox.component.js b/src/core_modules/capture-core/components/WidgetEventSchedule/InfoBox/InfoBox.component.js index d62a901d0d..eacbbf5588 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/InfoBox/InfoBox.component.js +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/InfoBox/InfoBox.component.js @@ -10,9 +10,13 @@ const styles = { infoBox: { marginTop: spacersNum.dp16, padding: spacersNum.dp16, + width: 'fit-content', }, }; +const getDayDifference = (startDate: string, endDate: string): number => + moment(startDate).diff(moment(endDate), 'days'); + const InfoBoxPlain = ({ scheduleDate, suggestedScheduleDate, @@ -21,38 +25,49 @@ const InfoBoxPlain = ({ orgUnitName, classes, }: Props) => { - if (!scheduleDate || !suggestedScheduleDate) { return null; } - const differenceScheduleDateAndSuggestedDate = moment(scheduleDate).diff(moment(suggestedScheduleDate), 'days'); + if (!scheduleDate || !suggestedScheduleDate) { + return null; + } + + const dayDifference = getDayDifference(scheduleDate, suggestedScheduleDate); + const absoluteDifference = Math.abs(dayDifference); + const position = dayDifference > 0 ? i18n.t('after') : i18n.t('before'); + const scheduledDateMatchesSuggested = scheduleDate === suggestedScheduleDate; return ( <NoticeBox className={classes.infoBox} title={i18n.t('Schedule date info')}> - {hideDueDate ? <> - {i18n.t('Scheduled automatically for {{suggestedScheduleDate}}', { suggestedScheduleDate })} - </> : <> - {scheduleDate === suggestedScheduleDate ? - i18n.t('The scheduled date matches the suggested date, but can be changed if needed.') - : - i18n.t( - 'The scheduled date is {{count}} days {{position}} the suggested date.', - { - position: differenceScheduleDateAndSuggestedDate > 0 ? i18n.t('after') : i18n.t('before'), - count: Math.abs(differenceScheduleDateAndSuggestedDate), - defaultValue: 'The scheduled date is {{count}} day {{position}} the suggested date.', - defaultValue_plural: 'The scheduled date is {{count}} days {{position}} the suggested date.', - }) - } - {' '} - {i18n.t('There are {{count}} scheduled event in {{orgUnitName}} on this day.', { - count: eventCountInOrgUnit, - orgUnitName, - defaultValue: 'There are {{count}} scheduled event in {{orgUnitName}} on this day.', - defaultValue_plural: 'There are {{count}} scheduled events in {{orgUnitName}} on this day.', - interpolation: { - escapeValue: false, - }, - })}</>} + {hideDueDate ? ( + <> + {i18n.t('Scheduled automatically for {{suggestedScheduleDate}}', { suggestedScheduleDate })} + </> + ) : ( + <> + {scheduledDateMatchesSuggested + ? i18n.t('The scheduled date matches the suggested date, but can be changed if needed.') + : i18n.t( + 'The scheduled date is {{count}} days {{position}} the suggested date.', + { + position, + count: absoluteDifference, + defaultValue: 'The scheduled date is {{count}} day {{position}} the suggested date.', + defaultValue_plural: 'The scheduled date is {{count}} days {{position}} the suggested date.', + }, + ) + } + {' '} + {i18n.t('There are {{count}} scheduled event in {{orgUnitName}} on this day.', { + count: eventCountInOrgUnit, + orgUnitName, + defaultValue: 'There are {{count}} scheduled event in {{orgUnitName}} on this day.', + defaultValue_plural: 'There are {{count}} scheduled events in {{orgUnitName}} on this day.', + interpolation: { + escapeValue: false, + }, + })} + </> + )} </NoticeBox> ); }; -export const InfoBox: ComponentType<$Diff<Props, CssClasses>> = (withStyles(styles)(InfoBoxPlain)); +export const InfoBox: ComponentType<$Diff<Props, CssClasses>> = withStyles(styles)(InfoBoxPlain); 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..d775ecae20 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 @@ -1,18 +1,46 @@ // @flow import React, { type ComponentType } from 'react'; -import { spacersNum } from '@dhis2/ui'; +import i18n from '@dhis2/d2-i18n'; import withStyles from '@material-ui/core/styles/withStyles'; -import { DateField } from 'capture-core/components/FormFields/New'; -import { InfoBox } from '../InfoBox'; +import { spacers, colors } from '@dhis2/ui'; +import { + DateField, + withDefaultFieldContainer, + withLabel, + withDisplayMessages, + withInternalChangeHandler, +} from 'capture-core/components/FormFields/New'; + import type { Props } from './scheduleDate.types'; +import labelTypeClasses from './dataEntryFieldLabels.module.css'; +import { InfoBox } from '../InfoBox'; +import { baseInputStyles } from '../ScheduleOrgUnit/commonProps'; + + +const ScheduleDataField = withDefaultFieldContainer()( + withLabel({ + onGetCustomFieldLabeClass: () => labelTypeClasses.dateLabel, + })( + withDisplayMessages()( + withInternalChangeHandler()( + DateField, + ), + ), + ), +); const styles = { - container: { + infoBox: { + padding: `0 ${spacers.dp16} ${spacers.dp16} ${spacers.dp16}`, + }, + fieldWrapper: { display: 'flex', - marginTop: spacersNum.dp4, + flexWrap: 'wrap', }, - button: { - paddingRight: spacersNum.dp16, + fieldLabel: { + color: colors.grey900, + padding: `${spacers.dp16} ${spacers.dp24} 0 ${spacers.dp16}`, + fontSize: '14px', }, }; @@ -22,35 +50,51 @@ const ScheduleDatePlain = ({ setScheduleDate, orgUnit, serverSuggestedScheduleDate, + displayDueDateLabel, eventCountInOrgUnit, classes, hideDueDate, -}: Props) => (<> - {!hideDueDate && <div className={classes.container}> - <DateField - value={scheduleDate} - width="100%" - calendarWidth={350} - onSetFocus={() => {}} - onFocus={() => { }} - onRemoveFocus={() => { }} - onBlur={(e, internalComponentError) => { - const { error } = internalComponentError; - if (error) { - setScheduleDate(''); - return; - } - setScheduleDate(e); - }} - /> - </div>} - <InfoBox - scheduleDate={serverScheduleDate} - suggestedScheduleDate={serverSuggestedScheduleDate} - eventCountInOrgUnit={eventCountInOrgUnit} - orgUnitName={orgUnit?.name} - hideDueDate={hideDueDate} - /> -</>); +}: Props) => ( + <div className={classes.fieldWrapper}> + {!hideDueDate ? + <ScheduleDataField + label={i18n.t('Schedule date / Due date')} + required + value={scheduleDate} + width="100%" + calendarWidth={350} + styles={baseInputStyles} + onSetFocus={() => { }} + onFocus={() => { }} + onRemoveFocus={() => { }} + onBlur={(e, internalComponentError) => { + const { error } = internalComponentError; + if (error) { + setScheduleDate(''); + return; + } + setScheduleDate(e); + }} + /> + : + <div className={classes.fieldLabel}> + {displayDueDateLabel ?? i18n.t('Schedule date / Due date', { + interpolation: { escapeValue: false }, + }, + )} + </div> + } + <div className={classes.infoBox}> + <InfoBox + scheduleDate={serverScheduleDate} + suggestedScheduleDate={serverSuggestedScheduleDate} + eventCountInOrgUnit={eventCountInOrgUnit} + orgUnitName={orgUnit?.name} + hideDueDate={hideDueDate} + /> + </div> + </div> +); + export const ScheduleDate: ComponentType<$Diff<Props, CssClasses>> = (withStyles(styles)(ScheduleDatePlain)); diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleDate/dataEntryFieldLabels.module.css b/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleDate/dataEntryFieldLabels.module.css new file mode 100644 index 0000000000..f4cae64aa1 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleDate/dataEntryFieldLabels.module.css @@ -0,0 +1,3 @@ +.dateLabel { + padding-top: 13px; +} \ No newline at end of file diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleDate/scheduleDate.types.js b/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleDate/scheduleDate.types.js index ede74d4e37..6776f07c43 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleDate/scheduleDate.types.js +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleDate/scheduleDate.types.js @@ -5,6 +5,7 @@ export type Props = {| stageId: string, programId: string, enrolledAt: string, + displayDueDateLabel: string, scheduleDate?: ?string, serverScheduleDate?: ?string, setScheduleDate: (date: string) => void, diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleOrgUnit/ScheduleOrgUnit.component.js b/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleOrgUnit/ScheduleOrgUnit.component.js new file mode 100644 index 0000000000..9718127671 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleOrgUnit/ScheduleOrgUnit.component.js @@ -0,0 +1,74 @@ +// @flow +import React, { useState } from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { isValidOrgUnit } from 'capture-core-utils/validators/form'; +import labelTypeClasses from './dataEntryFieldLabels.module.css'; +import { baseInputStyles } from './commonProps'; +import { + SingleOrgUnitSelectField, + withDefaultFieldContainer, + withDisplayMessages, + withInternalChangeHandler, + withLabel, +} from '../../FormFields/New'; + +type OrgUnitValue = {| + checked: boolean, + id: string, + children: number, + name: string, + displayName: string, + path: string, + selected: string[], +|} + +type Props = { + onSelectOrgUnit: (orgUnit: OrgUnitValue) => void, + onDeselectOrgUnit: () => void, + orgUnit: OrgUnitValue, +}; + +const OrgUnitFieldForForm = withDefaultFieldContainer()( + withLabel({ + onGetCustomFieldLabeClass: () => labelTypeClasses.dateLabel, + })( + withDisplayMessages()( + withInternalChangeHandler()( + SingleOrgUnitSelectField, + ), + ), + ), +); + +export const ScheduleOrgUnit = ({ + onSelectOrgUnit, + onDeselectOrgUnit, + orgUnit, +}: Props) => { + const [touched, setTouched] = useState(false); + + const handleSelect = (event) => { + setTouched(true); + onSelectOrgUnit(event); + }; + + const handleDeselect = () => { + setTouched(true); + onDeselectOrgUnit(); + }; + + const shouldShowError = (!isValidOrgUnit(orgUnit) && touched); + const errorMessages = i18n.t('Please provide a valid organisation unit'); + + return ( + <OrgUnitFieldForForm + label={i18n.t('Organisation unit')} + value={orgUnit} + required + onSelectClick={handleSelect} + onBlur={handleDeselect} + styles={baseInputStyles} + errorMessage={shouldShowError && errorMessages} + /> + ); +}; diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleOrgUnit/commonProps.js b/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleOrgUnit/commonProps.js new file mode 100644 index 0000000000..909d053c6d --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleOrgUnit/commonProps.js @@ -0,0 +1,6 @@ +// @flow + +export const baseInputStyles = { + inputContainerStyle: { flexBasis: 150 }, + labelContainerStyle: { flexBasis: 200 }, +}; diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleOrgUnit/dataEntryFieldLabels.module.css b/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleOrgUnit/dataEntryFieldLabels.module.css new file mode 100644 index 0000000000..f4cae64aa1 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleOrgUnit/dataEntryFieldLabels.module.css @@ -0,0 +1,3 @@ +.dateLabel { + padding-top: 13px; +} \ No newline at end of file diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.component.js b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.component.js index d29cbd8a3a..7d4b8f6919 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.component.js +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.component.js @@ -1,9 +1,11 @@ // @flow -import React, { type ComponentType } from 'react'; -import { spacersNum, spacers, colors } from '@dhis2/ui'; +import React, { type ComponentType, useEffect } from 'react'; +import { spacersNum } from '@dhis2/ui'; import withStyles from '@material-ui/core/styles/withStyles'; import i18n from '@dhis2/d2-i18n'; +import { isValidOrgUnit } from 'capture-core-utils/validators/form'; import { DataSection } from '../DataSection'; +import { Widget } from '../Widget'; import { ScheduleButtons } from './ScheduleButtons'; import { ScheduleDate } from './ScheduleDate'; import { ScheduleText } from './ScheduleText'; @@ -11,43 +13,12 @@ import { NoteSection } from '../WidgetNote'; import type { Props } from './widgetEventSchedule.types'; import { CategoryOptions } from './CategoryOptions/CategoryOptions.component'; import { Assignee } from './Assignee'; +import { ScheduleOrgUnit } from './ScheduleOrgUnit/ScheduleOrgUnit.component'; const styles = () => ({ wrapper: { - padding: `${spacers.dp16} 0`, - maxWidth: '55.75rem', - }, - fieldWrapper: { - display: 'flex', - flexWrap: 'wrap', - justifyContent: 'space-between', - padding: `${spacers.dp8} ${spacers.dp16}`, - }, - fieldLabel: { - color: colors.grey900, - paddingTop: spacersNum.dp16, - paddingRight: spacersNum.dp16, - }, - fieldContent: { - flexBasis: '200px', - flexGrow: 1, - }, - containerWrapper: { - padding: `${spacers.dp8} ${spacers.dp16}`, - }, - container: { - display: 'flex', - alignItems: 'center', - paddingTop: 8, - paddingBottom: 8, - }, - label: { - flexBasis: 200, - paddingLeft: 5, - }, - field: { - flexBasis: 150, - flexGrow: 1, + paddingLeft: spacersNum.dp16, + minWidth: '300px', }, }); @@ -64,7 +35,9 @@ const WidgetEventSchedulePlain = ({ classes, scheduleDate, suggestedScheduleDate, + setScheduledOrgUnit, serverSuggestedScheduleDate, + setIsFormValid, notes, programCategory, enableUserAssignment, @@ -75,70 +48,95 @@ const WidgetEventSchedulePlain = ({ assignee, categoryOptionsError, ...passOnProps -}: Props) => ( - <div className={classes.wrapper}> - <DataSection - dataTest="schedule-section" - sectionName={i18n.t('Schedule info')} +}: Props) => { + const onSelectOrgUnit = (e: { id: string, displayName: string, path: string }) => { + setScheduledOrgUnit({ + id: e.id, + name: e.displayName, + path: e.path, + }); + }; + + const onDeselectOrgUnit = () => { + setScheduledOrgUnit(undefined); + }; + + useEffect(() => { + const formIsValid = () => Boolean(isValidOrgUnit(orgUnit) && scheduleDate); + setIsFormValid(formIsValid()); + }, [orgUnit, scheduleDate, setIsFormValid]); + + return ( + <Widget + noncollapsible + borderless + header={ + <></> + } > - <div className={classes.fieldWrapper}> - <div className={classes.fieldLabel}> - {displayDueDateLabel ?? i18n.t('Schedule date / Due date', { - interpolation: { escapeValue: false } }, - )} - </div> - <div className={classes.fieldContent}> + <div className={classes.wrapper}> + <DataSection + dataTest="schedule-section" + sectionName={i18n.t('Schedule info')} + > + <ScheduleOrgUnit + orgUnit={orgUnit} + onSelectOrgUnit={onSelectOrgUnit} + onDeselectOrgUnit={onDeselectOrgUnit} + {...passOnProps} + /> <ScheduleDate programId={programId} stageId={stageId} orgUnit={orgUnit} scheduleDate={scheduleDate} + displayDueDateLabel={displayDueDateLabel} serverSuggestedScheduleDate={serverSuggestedScheduleDate} {...passOnProps} /> - </div> + </DataSection> + {programCategory && <DataSection + dataTest="category-options-section" + sectionName={programCategory.displayName} + > + <CategoryOptions + categories={programCategory.categories} + selectedOrgUnitId={orgUnit?.id} + selectedCategories={selectedCategories} + categoryOptionsError={categoryOptionsError} + onClickCategoryOption={onClickCategoryOption} + onResetCategoryOption={onResetCategoryOption} + required + /> + </DataSection>} + <DataSection + dataTest="note-section" + sectionName={i18n.t('Event notes')} + > + <NoteSection + notes={notes} + placeholder={i18n.t('Write a note about this scheduled event')} + handleAddNote={onAddNote} + /> + </DataSection> + {enableUserAssignment && ( + <DataSection dataTest="assignee-section" sectionName={i18n.t('Assignee')}> + <Assignee onSet={onSetAssignee} assignee={assignee} /> + </DataSection> + )} + <ScheduleButtons + hasChanges={scheduleDate !== suggestedScheduleDate} + onCancel={onCancel} + onSchedule={onSchedule} + /> + <ScheduleText + programName={programName} + stageName={stageName} + orgUnitName={orgUnit?.name} + /> </div> - </DataSection> - {programCategory && <DataSection - dataTest="category-options-section" - sectionName={programCategory.displayName} - > - <CategoryOptions - categories={programCategory.categories} - selectedOrgUnitId={orgUnit?.id} - selectedCategories={selectedCategories} - categoryOptionsError={categoryOptionsError} - onClickCategoryOption={onClickCategoryOption} - onResetCategoryOption={onResetCategoryOption} - required - /> - </DataSection>} - <DataSection - dataTest="note-section" - sectionName={i18n.t('Event notes')} - > - <NoteSection - notes={notes} - placeholder={i18n.t('Write a note about this scheduled event')} - handleAddNote={onAddNote} - /> - </DataSection> - {enableUserAssignment && ( - <DataSection dataTest="assignee-section" sectionName={i18n.t('Assignee')}> - <Assignee onSet={onSetAssignee} assignee={assignee} /> - </DataSection> - )} - <ScheduleButtons - hasChanges={scheduleDate !== suggestedScheduleDate} - onCancel={onCancel} - onSchedule={onSchedule} - /> - <ScheduleText - programName={programName} - stageName={stageName} - orgUnitName={orgUnit?.name || ''} - /> - </div> -); + </Widget> + ); +}; export const WidgetEventScheduleComponent: ComponentType<$Diff<Props, CssClasses>> = withStyles(styles)(WidgetEventSchedulePlain); diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js index 4132d024e9..c6ecee2a48 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js @@ -21,13 +21,12 @@ import { useCategoryCombinations } from '../DataEntryDhis2Helpers/AOC/useCategor import { convertFormToClient, convertClientToServer } from '../../converters'; import { pipe } from '../../../capture-core-utils'; - export const WidgetEventSchedule = ({ enrollmentId, teiId, stageId, programId, - orgUnitId, + orgUnitId: initialOrgUnitId, onSave, onSaveSuccessActionType, onSaveErrorActionType, @@ -39,26 +38,32 @@ export const WidgetEventSchedule = ({ }: ContainerProps) => { const { program, stage } = useMemo(() => getProgramAndStageForProgram(programId, stageId), [programId, stageId]); const dispatch = useDispatch(); - const orgUnit = { id: orgUnitId, name: useOrgUnitNameWithAncestors(orgUnitId).displayName }; const { programStageScheduleConfig } = useScheduleConfigFromProgramStage(stageId); const { programConfig } = useScheduleConfigFromProgram(programId); const suggestedScheduleDate = useDetermineSuggestedScheduleDate({ programStageScheduleConfig, programConfig, initialScheduleDate, ...passOnProps, }); + const orgUnitData = useOrgUnitNameWithAncestors(initialOrgUnitId); + const orgUnit = initialOrgUnitId && orgUnitData + ? { id: initialOrgUnitId, name: orgUnitData.displayName } : undefined; const { currentUser, noteId } = useNoteDetails(); const [scheduleDate, setScheduleDate] = useState(''); + const [scheduledOrgUnit, setScheduledOrgUnit] = useState(orgUnit); + const [isFormValid, setIsFormValid] = useState(false); const convertFn = pipe(convertFormToClient, convertClientToServer); const serverScheduleDate = convertFn(scheduleDate, dataElementTypes.DATE); const serverSuggestedScheduleDate = convertFn(suggestedScheduleDate, dataElementTypes.DATE); const [notes, setNotes] = useState([]); const [assignee, setAssignee] = useState(storedAssignee); - const { events } = useEventsInOrgUnit(orgUnitId, serverScheduleDate); const { eventId } = useLocationQuery(); + const selectedOrgUnitId = scheduledOrgUnit?.id || initialOrgUnitId; + const { events = [] } = useEventsInOrgUnit(selectedOrgUnitId, serverScheduleDate); const eventCountInOrgUnit = events .filter(event => moment(event.scheduledAt).format('YYYY-MM-DD') === serverScheduleDate).length; const [selectedCategories, setSelectedCategories] = useState({}); const [categoryOptionsError, setCategoryOptionsError] = useState(); const { programCategory } = useCategoryCombinations(programId); + useEffect(() => { if (!scheduleDate && suggestedScheduleDate) { setScheduleDate(suggestedScheduleDate); } }, [suggestedScheduleDate, scheduleDate]); @@ -68,6 +73,7 @@ export const WidgetEventSchedule = ({ }, [storedAssignee]); const onHandleSchedule = useCallback(() => { + if (!isFormValid) { return; } if (programCategory?.categories && Object.keys(selectedCategories).length !== programCategory?.categories?.length) { const errors = programCategory.categories @@ -81,9 +87,9 @@ export const WidgetEventSchedule = ({ } dispatch(requestScheduleEvent({ scheduleDate: serverScheduleDate, + orgUnitId: selectedOrgUnitId, notes, programId, - orgUnitId, stageId, teiId, enrollmentId, @@ -100,7 +106,7 @@ export const WidgetEventSchedule = ({ serverScheduleDate, notes, programId, - orgUnitId, + selectedOrgUnitId, stageId, teiId, enrollmentId, @@ -111,15 +117,9 @@ export const WidgetEventSchedule = ({ onSaveErrorActionType, programCategory, assignee, + isFormValid, ]); - React.useEffect(() => { - if (suggestedScheduleDate && !scheduleDate) { - setScheduleDate(suggestedScheduleDate); - } - }, [scheduleDate, suggestedScheduleDate]); - - const onAddNote = (note) => { const newNote = { storedBy: currentUser.userName, @@ -159,7 +159,7 @@ export const WidgetEventSchedule = ({ if (!program || !stage || !(program instanceof TrackerProgram)) { return ( <div> - {i18n.t('program or stage is invalid')}; + {i18n.t('Program or stage is invalid')}; </div> ); } @@ -167,9 +167,7 @@ export const WidgetEventSchedule = ({ const eventAccess = getProgramEventAccess(programId, stageId); if (!eventAccess?.write) { return ( - <NoAccess - onCancel={onCancel} - /> + <NoAccess onCancel={onCancel} /> ); } @@ -189,10 +187,12 @@ export const WidgetEventSchedule = ({ serverSuggestedScheduleDate={serverSuggestedScheduleDate} onCancel={onCancel} setScheduleDate={setScheduleDate} + setScheduledOrgUnit={setScheduledOrgUnit} + setIsFormValid={setIsFormValid} // Viktig å beholde onSchedule={onHandleSchedule} onAddNote={onAddNote} eventCountInOrgUnit={eventCountInOrgUnit} - orgUnit={orgUnit} + orgUnit={scheduledOrgUnit} notes={notes} selectedCategories={selectedCategories} categoryOptionsError={categoryOptionsError} @@ -201,7 +201,5 @@ export const WidgetEventSchedule = ({ onSetAssignee={onSetAssignee} {...passOnProps} /> - ); }; - diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/widgetEventSchedule.types.js b/src/core_modules/capture-core/components/WidgetEventSchedule/widgetEventSchedule.types.js index 7b89143d02..72f3f7d54d 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/widgetEventSchedule.types.js +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/widgetEventSchedule.types.js @@ -36,6 +36,8 @@ export type Props = {| scheduleDate?: ?string, serverScheduleDate?: ?string, suggestedScheduleDate?: ?string, + setScheduledOrgUnit: (orgUnit: Object) => void, + setIsFormValid: (valid: boolean) => void, serverSuggestedScheduleDate?: ?string, eventCountInOrgUnit: number, notes: Array<{value: string}>, @@ -48,7 +50,7 @@ export type Props = {| onSetAssignee: () => void, assignee?: UserFormField | null, onCancel: () => void, - setScheduleDate: (date: string) => void, + setScheduleDate: (date: ?string) => void, onAddNote: (note: string) => void, onResetCategoryOption: (categoryId: string) => void, onClickCategoryOption: (optionId: string, categoryId: string) => void, From 45c9652473f9fe5853c0e42afa8909a47a5d01d0 Mon Sep 17 00:00:00 2001 From: henrikmv <henrik@devOtta.no> Date: Thu, 9 Jan 2025 08:40:22 +0100 Subject: [PATCH 2/7] fix: review comments --- .../components/Widget/widgetCollapsible.types.js | 2 +- .../components/Widget/widgetNonCollapsible.types.js | 2 +- .../ScheduleDate/ScheduleDate.component.js | 4 ++-- .../ScheduleDate/dataEntryFieldLabels.module.css | 2 +- .../ScheduleOrgUnit/dataEntryFieldLabels.module.css | 2 +- .../WidgetEventSchedule/WidgetEventSchedule.component.js | 3 --- .../WidgetEventSchedule/WidgetEventSchedule.container.js | 2 +- .../WidgetEventSchedule/widgetEventSchedule.types.js | 6 +++++- 8 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/core_modules/capture-core/components/Widget/widgetCollapsible.types.js b/src/core_modules/capture-core/components/Widget/widgetCollapsible.types.js index 3f182d8f8e..6392f0fc94 100644 --- a/src/core_modules/capture-core/components/Widget/widgetCollapsible.types.js +++ b/src/core_modules/capture-core/components/Widget/widgetCollapsible.types.js @@ -2,7 +2,7 @@ import type { Node } from 'react'; export type WidgetCollapsibleProps = {| - header: Node, + header?: Node, children: Node, open: boolean, onOpen: () => void, diff --git a/src/core_modules/capture-core/components/Widget/widgetNonCollapsible.types.js b/src/core_modules/capture-core/components/Widget/widgetNonCollapsible.types.js index 3af55c409b..4e1e8e4f3d 100644 --- a/src/core_modules/capture-core/components/Widget/widgetNonCollapsible.types.js +++ b/src/core_modules/capture-core/components/Widget/widgetNonCollapsible.types.js @@ -2,7 +2,7 @@ import type { Node } from 'react'; export type WidgetNonCollapsibleProps = {| - header: Node, + header?: Node, children: Node, color?: string, borderless?: boolean, 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 d775ecae20..9f8c790587 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 @@ -17,7 +17,7 @@ import { InfoBox } from '../InfoBox'; import { baseInputStyles } from '../ScheduleOrgUnit/commonProps'; -const ScheduleDataField = withDefaultFieldContainer()( +const ScheduleDateField = withDefaultFieldContainer()( withLabel({ onGetCustomFieldLabeClass: () => labelTypeClasses.dateLabel, })( @@ -57,7 +57,7 @@ const ScheduleDatePlain = ({ }: Props) => ( <div className={classes.fieldWrapper}> {!hideDueDate ? - <ScheduleDataField + <ScheduleDateField label={i18n.t('Schedule date / Due date')} required value={scheduleDate} diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleDate/dataEntryFieldLabels.module.css b/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleDate/dataEntryFieldLabels.module.css index f4cae64aa1..c795dd8efc 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleDate/dataEntryFieldLabels.module.css +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleDate/dataEntryFieldLabels.module.css @@ -1,3 +1,3 @@ .dateLabel { padding-top: 13px; -} \ No newline at end of file +} diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleOrgUnit/dataEntryFieldLabels.module.css b/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleOrgUnit/dataEntryFieldLabels.module.css index f4cae64aa1..c795dd8efc 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleOrgUnit/dataEntryFieldLabels.module.css +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleOrgUnit/dataEntryFieldLabels.module.css @@ -1,3 +1,3 @@ .dateLabel { padding-top: 13px; -} \ No newline at end of file +} diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.component.js b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.component.js index 7d4b8f6919..a86780b8c4 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.component.js +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.component.js @@ -70,9 +70,6 @@ const WidgetEventSchedulePlain = ({ <Widget noncollapsible borderless - header={ - <></> - } > <div className={classes.wrapper}> <DataSection diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js index c6ecee2a48..0ca4cd2d70 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js @@ -188,7 +188,7 @@ export const WidgetEventSchedule = ({ onCancel={onCancel} setScheduleDate={setScheduleDate} setScheduledOrgUnit={setScheduledOrgUnit} - setIsFormValid={setIsFormValid} // Viktig å beholde + setIsFormValid={setIsFormValid} onSchedule={onHandleSchedule} onAddNote={onAddNote} eventCountInOrgUnit={eventCountInOrgUnit} diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/widgetEventSchedule.types.js b/src/core_modules/capture-core/components/WidgetEventSchedule/widgetEventSchedule.types.js index 72f3f7d54d..3ba2bc967a 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/widgetEventSchedule.types.js +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/widgetEventSchedule.types.js @@ -36,7 +36,11 @@ export type Props = {| scheduleDate?: ?string, serverScheduleDate?: ?string, suggestedScheduleDate?: ?string, - setScheduledOrgUnit: (orgUnit: Object) => void, + setScheduledOrgUnit: (orgUnit: ?{ + id: string, + name: string, + path: string, + }) => void, setIsFormValid: (valid: boolean) => void, serverSuggestedScheduleDate?: ?string, eventCountInOrgUnit: number, From b1d31ed713f95c490ac8f1b7f50677a4f7264749 Mon Sep 17 00:00:00 2001 From: henrikmv <henrik@devOtta.no> Date: Wed, 15 Jan 2025 09:33:18 +0100 Subject: [PATCH 3/7] fix: missing org unit name in info box --- .../components/WidgetEventSchedule/InfoBox/InfoBox.component.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/InfoBox/InfoBox.component.js b/src/core_modules/capture-core/components/WidgetEventSchedule/InfoBox/InfoBox.component.js index eacbbf5588..58b3ac9807 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/InfoBox/InfoBox.component.js +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/InfoBox/InfoBox.component.js @@ -25,7 +25,7 @@ const InfoBoxPlain = ({ orgUnitName, classes, }: Props) => { - if (!scheduleDate || !suggestedScheduleDate) { + if (!scheduleDate || !suggestedScheduleDate || !orgUnitName) { return null; } From f212d78c9b011bc84155a7648d209b5778282d68 Mon Sep 17 00:00:00 2001 From: henrikmv <henrik@devOtta.no> Date: Wed, 15 Jan 2025 13:45:38 +0100 Subject: [PATCH 4/7] fix: missing org unit name --- .../WidgetEventSchedule.container.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js index 0ca4cd2d70..aefbecd9ed 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js @@ -4,7 +4,7 @@ import i18n from '@dhis2/d2-i18n'; import { useDispatch } from 'react-redux'; import moment from 'moment'; import { getProgramAndStageForProgram, TrackerProgram, getProgramEventAccess, dataElementTypes } from '../../metaData'; -import { useOrgUnitNameWithAncestors } from '../../metadataRetrieval/orgUnitName'; +import { getCachedOrgUnitName } from '../../metadataRetrieval/orgUnitName'; import { useLocationQuery } from '../../utils/routing'; import type { ContainerProps } from './widgetEventSchedule.types'; import { WidgetEventScheduleComponent } from './WidgetEventSchedule.component'; @@ -43,12 +43,16 @@ export const WidgetEventSchedule = ({ const suggestedScheduleDate = useDetermineSuggestedScheduleDate({ programStageScheduleConfig, programConfig, initialScheduleDate, ...passOnProps, }); - const orgUnitData = useOrgUnitNameWithAncestors(initialOrgUnitId); - const orgUnit = initialOrgUnitId && orgUnitData - ? { id: initialOrgUnitId, name: orgUnitData.displayName } : undefined; + const orgUnitName = getCachedOrgUnitName(initialOrgUnitId); const { currentUser, noteId } = useNoteDetails(); const [scheduleDate, setScheduleDate] = useState(''); - const [scheduledOrgUnit, setScheduledOrgUnit] = useState(orgUnit); + const [scheduledOrgUnit, setScheduledOrgUnit] = useState(); + useEffect(() => { + if (initialOrgUnitId && orgUnitName) { + const orgUnit = { id: initialOrgUnitId, name: orgUnitName }; + setScheduledOrgUnit(orgUnit); + } + }, [orgUnitName, initialOrgUnitId]); const [isFormValid, setIsFormValid] = useState(false); const convertFn = pipe(convertFormToClient, convertClientToServer); const serverScheduleDate = convertFn(scheduleDate, dataElementTypes.DATE); From d4e588504a3c8eecf0249e3680e219046a4249ea Mon Sep 17 00:00:00 2001 From: henrikmv <henrik@devOtta.no> Date: Thu, 16 Jan 2025 11:08:18 +0100 Subject: [PATCH 5/7] fix: review comment --- .../WidgetEventSchedule/WidgetEventSchedule.container.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js index aefbecd9ed..09bcd84627 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js @@ -46,13 +46,8 @@ export const WidgetEventSchedule = ({ const orgUnitName = getCachedOrgUnitName(initialOrgUnitId); const { currentUser, noteId } = useNoteDetails(); const [scheduleDate, setScheduleDate] = useState(''); - const [scheduledOrgUnit, setScheduledOrgUnit] = useState(); - useEffect(() => { - if (initialOrgUnitId && orgUnitName) { - const orgUnit = { id: initialOrgUnitId, name: orgUnitName }; - setScheduledOrgUnit(orgUnit); - } - }, [orgUnitName, initialOrgUnitId]); + const [scheduledOrgUnit, setScheduledOrgUnit] = useState( + initialOrgUnitId && orgUnitName ? { id: initialOrgUnitId, name: orgUnitName } : undefined); const [isFormValid, setIsFormValid] = useState(false); const convertFn = pipe(convertFormToClient, convertClientToServer); const serverScheduleDate = convertFn(scheduleDate, dataElementTypes.DATE); From fc96d0e5f017fb1876fba7863c24ef0aa08ef743 Mon Sep 17 00:00:00 2001 From: henrikmv <henrik@devOtta.no> Date: Thu, 16 Jan 2025 15:29:52 +0100 Subject: [PATCH 6/7] Revert "fix: review comment" This reverts commit d4e588504a3c8eecf0249e3680e219046a4249ea. --- .../WidgetEventSchedule/WidgetEventSchedule.container.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js index 09bcd84627..aefbecd9ed 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js @@ -46,8 +46,13 @@ export const WidgetEventSchedule = ({ const orgUnitName = getCachedOrgUnitName(initialOrgUnitId); const { currentUser, noteId } = useNoteDetails(); const [scheduleDate, setScheduleDate] = useState(''); - const [scheduledOrgUnit, setScheduledOrgUnit] = useState( - initialOrgUnitId && orgUnitName ? { id: initialOrgUnitId, name: orgUnitName } : undefined); + const [scheduledOrgUnit, setScheduledOrgUnit] = useState(); + useEffect(() => { + if (initialOrgUnitId && orgUnitName) { + const orgUnit = { id: initialOrgUnitId, name: orgUnitName }; + setScheduledOrgUnit(orgUnit); + } + }, [orgUnitName, initialOrgUnitId]); const [isFormValid, setIsFormValid] = useState(false); const convertFn = pipe(convertFormToClient, convertClientToServer); const serverScheduleDate = convertFn(scheduleDate, dataElementTypes.DATE); From 2854ec855f54186cc9c35e42dcf5c9228864d8dc Mon Sep 17 00:00:00 2001 From: henrikmv <henrik@devOtta.no> Date: Fri, 17 Jan 2025 13:02:13 +0100 Subject: [PATCH 7/7] fix: info box missing org unit optimisation --- .../InfoBox/InfoBox.component.js | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/InfoBox/InfoBox.component.js b/src/core_modules/capture-core/components/WidgetEventSchedule/InfoBox/InfoBox.component.js index 58b3ac9807..e124738453 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/InfoBox/InfoBox.component.js +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/InfoBox/InfoBox.component.js @@ -25,7 +25,7 @@ const InfoBoxPlain = ({ orgUnitName, classes, }: Props) => { - if (!scheduleDate || !suggestedScheduleDate || !orgUnitName) { + if (!scheduleDate || !suggestedScheduleDate) { return null; } @@ -54,17 +54,22 @@ const InfoBoxPlain = ({ }, ) } - {' '} - {i18n.t('There are {{count}} scheduled event in {{orgUnitName}} on this day.', { - count: eventCountInOrgUnit, - orgUnitName, - defaultValue: 'There are {{count}} scheduled event in {{orgUnitName}} on this day.', - defaultValue_plural: 'There are {{count}} scheduled events in {{orgUnitName}} on this day.', - interpolation: { - escapeValue: false, - }, - })} + {!!orgUnitName && ( + <> + {' '} + {i18n.t('There are {{count}} scheduled event in {{orgUnitName}} on this day.', { + count: eventCountInOrgUnit, + orgUnitName, + defaultValue: 'There are {{count}} scheduled event in {{orgUnitName}} on this day.', + defaultValue_plural: 'There are {{count}} scheduled events in {{orgUnitName}} on this day.', + interpolation: { + escapeValue: false, + }, + })} + </> + )} </> + )} </NoticeBox> );