From 7aa24f0e9b759fbe78f0e62bac7b6eed34fb98d9 Mon Sep 17 00:00:00 2001 From: henrikmv Date: Fri, 17 Apr 2026 12:54:17 +0200 Subject: [PATCH 01/60] fix: disable enrollement actions button on ready only access --- i18n/en.pot | 7 +++- .../Actions/Actions.component.tsx | 38 ++++++++++--------- .../WidgetEnrollment/Actions/actions.types.ts | 2 + .../WidgetEnrollment.component.tsx | 9 +++-- .../WidgetEnrollment.container.tsx | 2 +- .../WidgetEnrollment/enrollment.types.ts | 2 +- 6 files changed, 35 insertions(+), 25 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 6246a00596..5218bdfa38 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: 2026-03-17T09:58:03.655Z\n" -"PO-Revision-Date: 2026-03-17T09:58:03.655Z\n" +"POT-Creation-Date: 2026-04-17T10:54:22.115Z\n" +"PO-Revision-Date: 2026-04-17T10:54:22.115Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -1308,6 +1308,9 @@ msgstr "Unsaved changes" msgid "Continue data entry" msgstr "Continue data entry" +msgid "You do not have access to modify this enrollment" +msgstr "You do not have access to modify this enrollment" + msgid "Enrollment actions" msgstr "Enrollment actions" diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.component.tsx index 358666f2c4..969e716153 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.component.tsx @@ -10,6 +10,7 @@ import { AddNew } from './AddNew'; import { AddLocation } from './AddLocation'; import type { PlainProps } from './actions.types'; import { LoadingMaskForButton } from '../../LoadingMasks'; +import { ConditionalTooltip } from '../../Tooltips/ConditionalTooltip'; import { MapModal } from '../MapModal'; import { Transfer } from './Transfer'; import { TransferModal } from '../TransferModal'; @@ -27,13 +28,14 @@ const styles = { }, }; -export const ActionsPlain = ({ +const ActionsPlain = ({ enrollment = {}, events, programStages, ownerOrgUnitId, tetName, canAddNew, + programDataWriteAccess, onUpdateStatus, onUpdate, onDelete, @@ -65,16 +67,19 @@ export const ActionsPlain = ({ return ( <> - setOpenActions(prev => !prev)} - component={ - loading ? undefined : ( + + setOpenActions(prev => !prev)} + component={ - - ) - } - > - {i18n.t('Enrollment actions')} - + } + > + {i18n.t('Enrollment actions')} + + {loading && (
diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/actions.types.ts b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/actions.types.ts index 3c5fa97643..5daa1499f0 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/actions.types.ts +++ b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/actions.types.ts @@ -16,6 +16,7 @@ export type Props = { onError?: (message: string) => void; onSuccess?: () => void; canAddNew: boolean; + programDataWriteAccess: boolean; onlyEnrollOnce: boolean; tetName: string; onAccessLostFromTransfer?: () => void; @@ -35,6 +36,7 @@ export type PlainProps = { isTransferLoading: boolean; loading: boolean; canAddNew: boolean; + programDataWriteAccess: boolean; onlyEnrollOnce: boolean; tetName: string; }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx index 603a55d31b..df0d0a42ec 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx @@ -45,7 +45,7 @@ const getGeometryType = geometryType => const getEnrollmentDateLabel = program => program.displayEnrollmentDateLabel ?? i18n.t('Enrollment date'); const getIncidentDateLabel = program => program.displayIncidentDateLabel ?? i18n.t('Incident date'); -export const WidgetEnrollmentPlain = ({ +const WidgetEnrollmentPlain = ({ classes, events, enrollment, @@ -57,7 +57,7 @@ export const WidgetEnrollmentPlain = ({ initError, loading, canAddNew, - editDateEnabled, + programDataWriteAccess, displayAutoGeneratedEventWarning, onDelete, onAddNew, @@ -110,7 +110,7 @@ export const WidgetEnrollmentPlain = ({ date={enrollment.enrolledAt} dateLabel={getEnrollmentDateLabel(program)} locale={locale} - editEnabled={editDateEnabled} + editEnabled={programDataWriteAccess} displayAutoGeneratedEventWarning={displayAutoGeneratedEventWarning} onSave={updateEnrollmentDate} allowFutureDate={program.selectEnrollmentDatesInFuture} @@ -123,7 +123,7 @@ export const WidgetEnrollmentPlain = ({ date={enrollment.occurredAt} dateLabel={getIncidentDateLabel(program)} locale={locale} - editEnabled={editDateEnabled} + editEnabled={programDataWriteAccess} displayAutoGeneratedEventWarning={displayAutoGeneratedEventWarning} onSave={updateIncidentDate} allowFutureDate={program.selectIncidentDatesInFuture} @@ -174,6 +174,7 @@ export const WidgetEnrollmentPlain = ({
)} void; updateIncidentDate: (incidentDate: string) => void; From 68866e5ed8720eff203f2df1bcedc12008e6eca9 Mon Sep 17 00:00:00 2001 From: henrikmv Date: Fri, 17 Apr 2026 14:59:56 +0200 Subject: [PATCH 02/60] fix: prop change --- .../WidgetEnrollment/WidgetEnrollment.component.tsx | 5 +++-- .../WidgetEnrollment/WidgetEnrollment.container.tsx | 3 ++- .../components/WidgetEnrollment/enrollment.types.ts | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx index df0d0a42ec..c1b1000972 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx @@ -57,6 +57,7 @@ const WidgetEnrollmentPlain = ({ initError, loading, canAddNew, + readOnlyMode, programDataWriteAccess, displayAutoGeneratedEventWarning, onDelete, @@ -110,7 +111,7 @@ const WidgetEnrollmentPlain = ({ date={enrollment.enrolledAt} dateLabel={getEnrollmentDateLabel(program)} locale={locale} - editEnabled={programDataWriteAccess} + editEnabled={!readOnlyMode && programDataWriteAccess} displayAutoGeneratedEventWarning={displayAutoGeneratedEventWarning} onSave={updateEnrollmentDate} allowFutureDate={program.selectEnrollmentDatesInFuture} @@ -123,7 +124,7 @@ const WidgetEnrollmentPlain = ({ date={enrollment.occurredAt} dateLabel={getIncidentDateLabel(program)} locale={locale} - editEnabled={programDataWriteAccess} + editEnabled={!readOnlyMode && programDataWriteAccess} displayAutoGeneratedEventWarning={displayAutoGeneratedEventWarning} onSave={updateIncidentDate} allowFutureDate={program.selectIncidentDatesInFuture} diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.container.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.container.tsx index bae7ba7c70..c8acf44885 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.container.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.container.tsx @@ -88,7 +88,8 @@ export const WidgetEnrollment = ({ enrollment={enrollment} events={events} canAddNew={canAddNew} - programDataWriteAccess={!readOnlyMode && program?.access.data.write} + readOnlyMode={readOnlyMode} + programDataWriteAccess={program?.access.data.write} displayAutoGeneratedEventWarning={containsAutoGeneratedEvent} program={program} refetchEnrollment={refetchEnrollment} diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/enrollment.types.ts b/src/core_modules/capture-core/components/WidgetEnrollment/enrollment.types.ts index e4384eca16..21ed405b80 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/enrollment.types.ts +++ b/src/core_modules/capture-core/components/WidgetEnrollment/enrollment.types.ts @@ -53,6 +53,7 @@ export type PlainProps = { initError?: boolean; loading: boolean; canAddNew: boolean; + readOnlyMode: boolean; programDataWriteAccess: boolean; displayAutoGeneratedEventWarning: boolean; updateEnrollmentDate: (enrollmentDate: string) => void; From 3e1b925510c3a96be956f26fd387dcfe06ffcc1d Mon Sep 17 00:00:00 2001 From: henrikmv Date: Tue, 28 Apr 2026 20:09:20 +0200 Subject: [PATCH 03/60] feat: add read only prop --- i18n/en.pot | 10 ++- .../EnrollmentPageDefault.container.tsx | 7 ++ .../EnrollmentPageDefault.types.ts | 3 +- .../EnrollmentQuickActions.component.tsx | 7 +- .../EnrollmentQuickActions.types.ts | 1 + .../QuickActionButton/QuickActionButton.tsx | 12 ++- .../QuickActionButton.types.ts | 1 + .../EnrollmentEditEventPage.component.tsx | 2 + .../EnrollmentEditEventPage.container.tsx | 8 ++ .../EnrollmentEditEventPage.types.ts | 3 +- .../LayoutComponentConfig.ts | 31 +++++--- .../renderPageComponents.ts | 2 +- .../common/EnrollmentOverviewDomain/index.ts | 1 + .../useCommonEnrollmentDomainData/index.ts | 1 + .../useCommonEnrollmentDomainData.types.ts | 2 + ...edEntityRelationshipsWrapper.component.tsx | 2 + ...TrackedEntityRelationshipsWrapper.types.ts | 1 + .../MiniMap/MiniMap.component.tsx | 2 + .../WidgetEnrollment/MiniMap/MiniMap.types.ts | 1 + .../WidgetEnrollment.component.tsx | 41 +++++----- .../WidgetEnrollmentNote.component.tsx | 7 +- .../WidgetEventEdit.container.tsx | 2 + .../WidgetHeader/WidgetHeader.container.tsx | 8 +- .../WidgetHeader/WidgetHeader.types.ts | 1 + .../WidgetEventEdit/widgetEventEdit.types.ts | 1 + .../WidgetEventNote.component.tsx | 3 +- .../WidgetEventNote/WidgetEventNote.types.ts | 1 + .../WidgetNote/NoteSection/NoteSection.tsx | 7 +- .../NoteSection/NoteSection.types.ts | 1 + .../components/WidgetNote/WidgetNote.types.ts | 1 + .../WidgetProfile/WidgetProfile.component.tsx | 29 ++++++-- .../WidgetProfile/widgetProfile.types.ts | 1 + .../Stages/Stage/Stage.component.tsx | 4 +- .../StageCreateNewButton.tsx | 10 ++- .../Stage/StageDetail/EventRow/EventRow.tsx | 74 +++++++++++-------- .../StageDetail/EventRow/EventRow.types.ts | 1 + .../StageDetail/StageDetail.component.tsx | 3 + .../Stage/StageDetail/stageDetail.types.ts | 1 + .../Stages/Stage/stage.types.ts | 1 + .../stagesAndEvents.types.ts | 1 + ...NewTrackedEntityRelationship.container.tsx | 20 +++-- .../NewTrackedEntityRelationship.types.ts | 1 + ...getTrackedEntityRelationship.component.tsx | 3 + .../WidgetTrackedEntityRelationship.types.ts | 1 + .../DeleteRelationship/DeleteRelationship.tsx | 23 ++++-- .../DeleteRelationship.types.ts | 1 + .../LinkedEntitiesViewer.component.tsx | 2 + .../LinkedEntityTable.component.tsx | 2 + .../LinkedEntityTableBody.component.tsx | 4 +- .../RelationshipsWidget.component.tsx | 2 + .../linkedEntitiesViewer.types.ts | 1 + .../linkedEntityTable.types.ts | 1 + .../linkedEntityTableBody.types.ts | 1 + .../relationshipsWidget.types.ts | 1 + 54 files changed, 258 insertions(+), 100 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 6246a00596..bb6008361e 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: 2026-03-17T09:58:03.655Z\n" -"PO-Revision-Date: 2026-03-17T09:58:03.655Z\n" +"POT-Creation-Date: 2026-04-28T18:09:21.231Z\n" +"PO-Revision-Date: 2026-04-28T18:09:21.231Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -715,6 +715,9 @@ msgstr "Notice" msgid "Close the notice" msgstr "Close the notice" +msgid "You do not have access to edit this enrollment" +msgstr "You do not have access to edit this enrollment" + msgid "Quick actions" msgstr "Quick actions" @@ -1713,6 +1716,9 @@ msgstr "An error occurred while deleting the event" msgid "Are you sure you want to delete this event?" msgstr "Are you sure you want to delete this event?" +msgid "You do not have access to perform actions on this event" +msgstr "You do not have access to perform actions on this event" + msgid "An error occurred when updating event status" msgstr "An error occurred when updating event status" diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.tsx b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.tsx index ae89ad0475..ca94cb669c 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.tsx +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.tsx @@ -180,6 +180,12 @@ export const EnrollmentPageDefault = () => { navigate(`/?${buildUrlQueryString({ orgUnitId, programId })}`); }, [navigate, orgUnitId, programId]); + const hasProgramWrite = Boolean(program?.access?.data?.write); + const hasTETWrite = Boolean((program as any)?.trackedEntityType?.access?.data?.write); + const readOnly = !hasProgramWrite || !hasTETWrite + ? { tooltipContent: i18n.t('You do not have access to edit this enrollment') } + : undefined; + if (isLoading) { return ( @@ -196,6 +202,7 @@ export const EnrollmentPageDefault = () => { currentPage={EnrollmentPageKeys.OVERVIEW} availableWidgets={WidgetsForEnrollmentPageDefault} + readOnly={readOnly} teiId={teiId} orgUnitId={orgUnitId} program={program} diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.ts b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.ts index c07ac06edd..21f19f3983 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.ts +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.ts @@ -1,7 +1,7 @@ import { effectActions } from '@dhis2/rules-engine-javascript'; import type { TrackerProgram } from 'capture-core/metaData'; import type { HideWidgets, WidgetEffects } from '../../common/EnrollmentOverviewDomain'; -import type { Event } from '../../common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; +import type { Event, ReadOnlyState } from '../../common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; import type { LinkedRecordClick } from '../../../WidgetsRelationship/WidgetTrackedEntityRelationship'; import type { PageLayoutConfig, @@ -49,6 +49,7 @@ export type Props = { pageLayout: PageLayoutConfig; availableWidgets: Readonly<{ [key: string]: WidgetConfig }>; onDeleteTrackedEntitySuccess: () => void; + readOnly?: ReadOnlyState; }; diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/EnrollmentQuickActions.component.tsx b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/EnrollmentQuickActions.component.tsx index 3ce36927d4..677b648352 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/EnrollmentQuickActions.component.tsx +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/EnrollmentQuickActions.component.tsx @@ -23,6 +23,7 @@ const EnrollmentQuickActionsComponentPlain = ({ stages, events, ruleEffects, + readOnly, classes, }: Props) => { const [open, setOpen] = useState(true); @@ -78,7 +79,8 @@ const EnrollmentQuickActionsComponentPlain = ({ label={i18n.t('New event')} onClickAction={() => onNavigationFromQuickActions(tabMode.REPORT)} dataTest={'quick-action-button-report'} - disabled={noStageAvailable} + disabled={noStageAvailable || Boolean(readOnly)} + tooltipContent={readOnly?.tooltipContent} /> onNavigationFromQuickActions(tabMode.SCHEDULE)} dataTest={'quick-action-button-schedule'} - disabled={noStageAvailable} + disabled={noStageAvailable || Boolean(readOnly)} + tooltipContent={readOnly?.tooltipContent} /> {/* DHIS2-13016: Should hide Make referral until the feature is developped diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/EnrollmentQuickActions.types.ts b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/EnrollmentQuickActions.types.ts index 2fedaf010f..13af75ca0b 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/EnrollmentQuickActions.types.ts +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/EnrollmentQuickActions.types.ts @@ -16,4 +16,5 @@ export type OwnProps = { stages: Array; events: Array; ruleEffects?: Array; + readOnly?: { tooltipContent: string }; }; diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/QuickActionButton/QuickActionButton.tsx b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/QuickActionButton/QuickActionButton.tsx index 608b0e6e46..8e0eb46c47 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/QuickActionButton/QuickActionButton.tsx +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/QuickActionButton/QuickActionButton.tsx @@ -15,9 +15,17 @@ const styles = { type Props = QuickActionButtonTypes & WithStyles; -const QuickActionButtonPlain = ({ icon, label, onClickAction, dataTest, disabled = false, classes }: Props) => ( +const QuickActionButtonPlain = ({ + icon, + label, + onClickAction, + dataTest, + disabled = false, + tooltipContent, + classes, +}: Props) => ( + {showEditButton && ( + + + )} , teiDisplayName: string) => void; onDeleteSuccess?: () => void; }; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/Stage.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/Stage.component.tsx index cea6d7ec4d..1beb26e906 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/Stage.component.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/Stage.component.tsx @@ -26,7 +26,7 @@ const rulesEffectHideProgramStage = (ruleEffects: Array<{id: string, type: strin ); export const StagePlain = ({ - stage, events, classes, onCreateNew, ruleEffects, ...passOnProps + stage, events, classes, onCreateNew, ruleEffects, readOnly, ...passOnProps }: Props & WithStyles) => { const [open, setOpenStatus] = useState(true); const { id, name, icon, description, dataElements, hideDueDate, repeatable, enableUserAssignment } = stage; @@ -64,6 +64,7 @@ export const StagePlain = ({ enableUserAssignment={enableUserAssignment} onCreateNew={onCreateNew} hiddenProgramStage={preventAddingNewEvents} + readOnly={readOnly} {...passOnProps} /> : (
@@ -74,6 +75,7 @@ export const StagePlain = ({ repeatable={repeatable} preventAddingEventActionInEffect={preventAddingNewEvents} eventName={name} + readOnly={readOnly} />
)} diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageCreateNewButton/StageCreateNewButton.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageCreateNewButton/StageCreateNewButton.tsx index 0a503dc2dd..eba5605b15 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageCreateNewButton/StageCreateNewButton.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageCreateNewButton/StageCreateNewButton.tsx @@ -10,6 +10,7 @@ type Props = { repeatable?: boolean; preventAddingEventActionInEffect?: boolean; eventName: string; + readOnly?: { tooltipContent: string }; }; export const StageCreateNewButton = ({ @@ -19,8 +20,15 @@ export const StageCreateNewButton = ({ repeatable, preventAddingEventActionInEffect, eventName, + readOnly, }: Props) => { const { isDisabled, tooltipContent } = useMemo(() => { + if (readOnly) { + return { + isDisabled: true, + tooltipContent: readOnly.tooltipContent, + }; + } if (!stageWriteAccess) { return ({ isDisabled: true, @@ -49,7 +57,7 @@ export const StageCreateNewButton = ({ isDisabled: false, tooltipContent: '', }; - }, [eventCount, eventName, preventAddingEventActionInEffect, repeatable, stageWriteAccess]); + }, [eventCount, eventName, preventAddingEventActionInEffect, repeatable, stageWriteAccess, readOnly]); return ( ) => { const [actionsOpen, setActionsOpen] = useState(false); @@ -60,39 +63,48 @@ const EventRowPlain = ({ <> - setActionsOpen(prev => !prev)} - dataTest={'overflow-button'} - secondary - small - icon={} - disabled={pendingApiResponse || !stageWriteAccess} - component={( - - {(eventDetails.status === EventStatuses.SCHEDULE || - eventDetails.status === EventStatuses.SKIPPED) && ( - + setActionsOpen(prev => !prev)} + dataTest={'overflow-button'} + secondary + small + icon={} + disabled={pendingApiResponse || !stageWriteAccess || Boolean(readOnly)} + component={( + + {(eventDetails.status === EventStatuses.SCHEDULE || + eventDetails.status === EventStatuses.SKIPPED) && ( + + )} + + - )} - - - - )} - /> + + )} + /> + {deleteModalOpen && ( ) => { onViewAll, onCreateNew, hiddenProgramStage, + readOnly, classes, } = props; const defaultSortState = { @@ -227,6 +228,7 @@ const StageDetailPlain = (props: Props & WithStyles) => { onDeleteEvent={onDeleteEvent} onRollbackDeleteEvent={onRollbackDeleteEvent} onUpdateEventStatus={onUpdateEventStatus} + readOnly={readOnly} /> ); }); @@ -263,6 +265,7 @@ const StageDetailPlain = (props: Props & WithStyles) => { repeatable={repeatable} stageWriteAccess={stage?.access?.data?.write} eventName={eventName} + readOnly={readOnly} /> ); diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/stageDetail.types.ts b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/stageDetail.types.ts index aa93f0fbcb..b9308e8787 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/stageDetail.types.ts +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/stageDetail.types.ts @@ -14,6 +14,7 @@ type ExtractedProps = { onUpdateEventStatus: (eventId: string, status: string) => void; onRollbackDeleteEvent: (event: ApiEnrollmentEvent) => void; hiddenProgramStage?: boolean; + readOnly?: { tooltipContent: string }; }; export type Props = ExtractedProps & StageCommonProps; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/stage.types.ts b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/stage.types.ts index f1f0b30787..75e19cb375 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/stage.types.ts +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/stage.types.ts @@ -9,6 +9,7 @@ type ExtractedProps = { onDeleteEvent: (eventId: string) => void; onUpdateEventStatus: (eventId: string, status: string) => void; onRollbackDeleteEvent: (eventId: ApiEnrollmentEvent) => void; + readOnly?: { tooltipContent: string }; }; export type Props = ExtractedProps & StageCommonProps; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/stagesAndEvents.types.ts b/src/core_modules/capture-core/components/WidgetStagesAndEvents/stagesAndEvents.types.ts index ac9dc3e5c2..51fa778330 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/stagesAndEvents.types.ts +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/stagesAndEvents.types.ts @@ -9,6 +9,7 @@ type ExtractedProps = { onUpdateEventStatus: (eventId: string, status: string) => void; onRollbackDeleteEvent: (eventId: ApiEnrollmentEvent) => void; className?: string; + readOnly?: { tooltipContent: string }; }; export type Props = ExtractedProps & StageCommonProps; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.tsx b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.tsx index 908cb6b49d..1120e83f28 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.tsx +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.tsx @@ -2,6 +2,7 @@ import React, { useCallback, useState } from 'react'; import { Button, spacers } from '@dhis2/ui'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import i18n from '@dhis2/d2-i18n'; +import { ConditionalTooltip } from 'capture-core/components/Tooltips/ConditionalTooltip'; import { NewTrackedEntityRelationshipPortal } from './NewTrackedEntityRelationship.portal'; import type { ContainerProps } from './NewTrackedEntityRelationship.types'; @@ -24,6 +25,7 @@ export const NewTrackedEntityRelationshipPlain = ({ renderTrackedEntitySearch, renderTrackedEntityRegistration, onSelectFindMode, + readOnly, classes, }: ContainerProps & WithStyles) => { const [addWizardVisible, setAddWizardVisible] = useState(false); @@ -40,13 +42,19 @@ export const NewTrackedEntityRelationshipPlain = ({ return (
- + + { addWizardVisible && ( diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.ts b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.ts index 5b87f3d76b..e44c5ab783 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.ts +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.ts @@ -33,6 +33,7 @@ export type ContainerProps = Readonly<{ onCloseAddRelationship?: () => void; onOpenAddRelationship?: () => void; onSelectFindMode?: OnSelectFindMode; + readOnly?: { tooltipContent: string }; }>; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.tsx b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.tsx index 67426794c1..96ce914c14 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.tsx +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.tsx @@ -20,6 +20,7 @@ export const WidgetTrackedEntityRelationship = ({ onSelectFindMode, renderTrackedEntitySearch, renderTrackedEntityRegistration, + readOnly, }: WidgetTrackedEntityRelationshipProps) => { const { data: relationshipTypes } = useRelationshipTypes(cachedRelationshipTypes); const { data: trackedEntityTypeName, isLoading: isLoadingTEType } = useTrackedEntityTypeName(trackedEntityTypeId); @@ -60,6 +61,7 @@ export const WidgetTrackedEntityRelationship = ({ relationshipTypes={relationshipTypes} sourceId={teiId} onLinkedRecordClick={onLinkedRecordClick} + readOnly={readOnly} > ); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.ts b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.ts index 84fcf8923e..acb62db1d3 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.ts +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.ts @@ -44,4 +44,5 @@ export type WidgetTrackedEntityRelationshipProps = { programId: string, onLinkToTrackedEntityFromSearch: OnLinkToTrackedEntityFromSearch, ) => React.ReactElement; + readOnly?: { tooltipContent: string }; }; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/DeleteRelationship/DeleteRelationship.tsx b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/DeleteRelationship/DeleteRelationship.tsx index 9ef59cd548..eab285f14d 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/DeleteRelationship/DeleteRelationship.tsx +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/DeleteRelationship/DeleteRelationship.tsx @@ -13,6 +13,7 @@ import { } from '@dhis2/ui'; import { IconButton } from 'capture-ui'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; +import { ConditionalTooltip } from '../../../../Tooltips/ConditionalTooltip'; import type { Props } from './DeleteRelationship.types'; const styles: Readonly = { @@ -25,21 +26,27 @@ const styles: Readonly = { export const DeleteRelationshipPlain = ({ handleDeleteRelationship, disabled, + tooltipContent, classes, }: Props & WithStyles) => { const [isModalOpen, setIsModalOpen] = useState(false); return ( <> - { - if (disabled) return; - setIsModalOpen(true); - }} - dataTest={'delete-relationship-button'} + - - + { + if (disabled) return; + setIsModalOpen(true); + }} + dataTest={'delete-relationship-button'} + > + + + {isModalOpen && ( diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/DeleteRelationship/DeleteRelationship.types.ts b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/DeleteRelationship/DeleteRelationship.types.ts index 0a05640d23..92fd46e63d 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/DeleteRelationship/DeleteRelationship.types.ts +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/DeleteRelationship/DeleteRelationship.types.ts @@ -1,4 +1,5 @@ export type Props = { handleDeleteRelationship: () => void; disabled?: boolean; + tooltipContent?: string; }; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntitiesViewer.component.tsx b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntitiesViewer.component.tsx index d33f38adf8..654666e98f 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntitiesViewer.component.tsx +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntitiesViewer.component.tsx @@ -25,6 +25,7 @@ const LinkedEntitiesViewerPlain = ({ groupedLinkedEntities, onLinkedRecordClick, onDeleteRelationship, + readOnly, classes, }: Props & WithStyles) => (
); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTable.component.tsx b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTable.component.tsx index 8d3ae8e1d3..3f6ad67b98 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTable.component.tsx +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTable.component.tsx @@ -29,6 +29,7 @@ const LinkedEntityTablePlain = ({ onLinkedRecordClick, onDeleteRelationship, context, + readOnly, classes, }: Props & WithStyles) => { const [visibleRowsCount, setVisibleRowsCount] = useState(DEFAULT_VISIBLE_ROWS_COUNT); @@ -54,6 +55,7 @@ const LinkedEntityTablePlain = ({ onLinkedRecordClick={onLinkedRecordClick} context={context} onDeleteRelationship={onDeleteRelationship} + readOnly={readOnly} /> {showMoreButtonVisible && ( diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTableBody.component.tsx b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTableBody.component.tsx index 8e1d74bbe3..6a9c9fd2f5 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTableBody.component.tsx +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTableBody.component.tsx @@ -29,6 +29,7 @@ const LinkedEntityTableBodyPlain = ({ onLinkedRecordClick, context, onDeleteRelationship, + readOnly, classes, }: Props & WithStyles) => ( @@ -89,7 +90,8 @@ const LinkedEntityTableBodyPlain = ({ handleDeleteRelationship={() => onDeleteRelationship({ relationshipId: relationshipId! }) } - disabled={pendingApiResponse} + disabled={pendingApiResponse || Boolean(readOnly)} + tooltipContent={readOnly?.tooltipContent} /> ) : null} diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/RelationshipsWidget.component.tsx b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/RelationshipsWidget.component.tsx index 7e75048551..bbea26e5cd 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/RelationshipsWidget.component.tsx +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/RelationshipsWidget.component.tsx @@ -20,6 +20,7 @@ const RelationshipsWidgetPlain = ({ relationshipTypes, onLinkedRecordClick, children, + readOnly, classes, }: Props & WithStyles) => { const [open, setOpenStatus] = useState(true); @@ -66,6 +67,7 @@ const RelationshipsWidgetPlain = ({ groupedLinkedEntities={groupedLinkedEntities} onLinkedRecordClick={onLinkedRecordClick} onDeleteRelationship={onDeleteRelationship} + readOnly={readOnly} /> ) } diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntitiesViewer.types.ts b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntitiesViewer.types.ts index d97b1f9951..f50ecea54b 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntitiesViewer.types.ts +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntitiesViewer.types.ts @@ -5,5 +5,6 @@ export type Props = Readonly<{ groupedLinkedEntities: GroupedLinkedEntities; onLinkedRecordClick: LinkedRecordClick; onDeleteRelationship: OnDeleteRelationship; + readOnly?: { tooltipContent: string }; }>; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTable.types.ts b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTable.types.ts index 61cf1ce414..64cc816a5b 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTable.types.ts +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTable.types.ts @@ -7,5 +7,6 @@ export type Props = Readonly<{ onLinkedRecordClick: LinkedRecordClick; onDeleteRelationship: OnDeleteRelationship; context: Context; + readOnly?: { tooltipContent: string }; }>; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTableBody.types.ts b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTableBody.types.ts index 0e2dd28649..5b1088ec68 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTableBody.types.ts +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTableBody.types.ts @@ -7,4 +7,5 @@ export type Props = Readonly<{ onLinkedRecordClick: LinkedRecordClick; context: Context; onDeleteRelationship: OnDeleteRelationship; + readOnly?: { tooltipContent: string }; }>; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/relationshipsWidget.types.ts b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/relationshipsWidget.types.ts index 56aad27cad..81642fa3f1 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/relationshipsWidget.types.ts +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/relationshipsWidget.types.ts @@ -10,4 +10,5 @@ export type Props = Readonly<{ sourceId: string; onLinkedRecordClick: LinkedRecordClick; children: ReactNode; + readOnly?: { tooltipContent: string }; }>; From 3329b82e444761679743443b81016957cfa01cad Mon Sep 17 00:00:00 2001 From: henrikmv Date: Tue, 28 Apr 2026 21:13:27 +0200 Subject: [PATCH 04/60] fix: remove unused prop --- .../components/WidgetEnrollment/WidgetEnrollment.component.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx index bc54ba4bf2..c1b1000972 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx @@ -171,7 +171,6 @@ const WidgetEnrollmentPlain = ({ refetchEnrollment={refetchEnrollment} refetchTEI={refetchTEI} onError={onError} - readOnly={!editDateEnabled} />
)} From 5ce334e2e2ec7f4709ae037eba48b8a6197c2054 Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 19:55:41 +0000 Subject: [PATCH 05/60] fix: restore readOnly prop on MiniMap types --- .../components/WidgetEnrollment/WidgetEnrollment.component.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx index c1b1000972..ca365fee66 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx @@ -171,11 +171,12 @@ const WidgetEnrollmentPlain = ({ refetchEnrollment={refetchEnrollment} refetchTEI={refetchTEI} onError={onError} + readOnly={readOnlyMode || !programDataWriteAccess} /> )} Date: Tue, 28 Apr 2026 20:02:17 +0000 Subject: [PATCH 06/60] fix: mini map read only --- .../MapModal/Coordinates/Coordinates.component.tsx | 5 ++++- .../MapModal/Coordinates/Coordinates.types.ts | 1 + .../WidgetEnrollment/MapModal/MapModal.component.tsx | 4 +++- .../WidgetEnrollment/MapModal/MapModal.container.tsx | 2 ++ .../components/WidgetEnrollment/MapModal/MapModal.types.ts | 2 ++ .../WidgetEnrollment/MapModal/Polygon/Polygon.component.tsx | 3 ++- .../WidgetEnrollment/MapModal/Polygon/Polygon.types.ts | 1 + .../WidgetEnrollment/MiniMap/MiniMap.component.tsx | 2 +- 8 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/Coordinates.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/Coordinates.component.tsx index 6a0c4df9bf..744f1b4636 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/Coordinates.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/Coordinates.component.tsx @@ -50,6 +50,7 @@ const CoordinatesPlain = ({ setOpen, defaultValues, onSetCoordinates, + readOnly, }: Props) => { const [position, setPosition] = useState<[number, number] | null>(defaultValues || null); const [center, setCenter] = useState<[number, number] | undefined>(undefined); @@ -190,6 +191,7 @@ const CoordinatesPlain = ({ -
+ + + )} {supportsChangelog && ( + const isEditable = useMemo(() => !hasNoAttributes && - trackedEntityTypeAccess?.data?.write, - [hasNoAttributes, trackedEntityTypeAccess]); - - const isEditable = showEditButton && !readOnlyMode; + trackedEntityTypeAccess?.data?.write && + !readOnlyMode, + [hasNoAttributes, readOnlyMode, trackedEntityTypeAccess]); const loading = programsLoading || trackedEntityInstancesLoading || userRolesLoading || !configIsFetched; const error = programsError || trackedEntityInstancesError || userRolesError; @@ -188,20 +185,10 @@ const WidgetProfilePlain = ({ : i18n.t('Profile')}
- {showEditButton && ( - - - + {isEditable && ( + )} { const { isDisabled, tooltipContent } = useMemo(() => { - if (readOnly) { - return { - isDisabled: true, - tooltipContent: readOnly.tooltipContent, - }; - } if (!stageWriteAccess) { return ({ isDisabled: true, @@ -57,7 +51,11 @@ export const StageCreateNewButton = ({ isDisabled: false, tooltipContent: '', }; - }, [eventCount, eventName, preventAddingEventActionInEffect, repeatable, stageWriteAccess, readOnly]); + }, [eventCount, eventName, preventAddingEventActionInEffect, repeatable, stageWriteAccess]); + + if (readOnly) { + return null; + } return ( <> - - setActionsOpen(prev => !prev)} - dataTest={'overflow-button'} - secondary - small - icon={} - disabled={pendingApiResponse || !stageWriteAccess || Boolean(readOnly)} - component={( - - {(eventDetails.status === EventStatuses.SCHEDULE || - eventDetails.status === EventStatuses.SKIPPED) && ( - + setActionsOpen(prev => !prev)} + dataTest={'overflow-button'} + secondary + small + icon={} + disabled={pendingApiResponse || !stageWriteAccess} + component={( + + {(eventDetails.status === EventStatuses.SCHEDULE || + eventDetails.status === EventStatuses.SKIPPED) && ( + + )} + + - )} - - - - )} - /> - + + )} + /> + + )} {deleteModalOpen && ( - + {!readOnly && ( - + )} { addWizardVisible && ( diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTableBody.component.tsx b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTableBody.component.tsx index 6a9c9fd2f5..a3daa9eef7 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTableBody.component.tsx +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTableBody.component.tsx @@ -85,13 +85,12 @@ const LinkedEntityTableBodyPlain = ({ ); })} - {context.display.showDeleteButton ? ( + {context.display.showDeleteButton && !readOnly ? ( onDeleteRelationship({ relationshipId: relationshipId! }) } - disabled={pendingApiResponse || Boolean(readOnly)} - tooltipContent={readOnly?.tooltipContent} + disabled={pendingApiResponse} /> ) : null} From f06e5ebace9c0bd64332f1c03901d51c784fcd88 Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 12:14:50 +0000 Subject: [PATCH 12/60] fix: clean up --- i18n/en.pot | 3 --- .../EnrollmentPageDefault.container.tsx | 4 +--- .../EnrollmentQuickActions.component.tsx | 3 +-- .../EnrollmentQuickActions.types.ts | 1 - .../QuickActionButton/QuickActionButton.tsx | 3 +-- .../QuickActionButton.types.ts | 1 - .../EnrollmentEditEventPage.container.tsx | 5 +--- .../LayoutComponentConfig.ts | 5 ++-- .../useCommonEnrollmentDomainData.types.ts | 2 +- ...TrackedEntityRelationshipsWrapper.types.ts | 2 +- .../WidgetEnrollment/Actions/actions.types.ts | 4 ++-- .../WidgetEnrollment/enrollment.types.ts | 2 +- .../WidgetEnrollmentNote.component.tsx | 2 +- .../WidgetHeader/WidgetHeader.types.ts | 2 +- .../WidgetEventEdit/widgetEventEdit.types.ts | 2 +- .../WidgetEventNote/WidgetEventNote.types.ts | 2 +- .../WidgetProfile/widgetProfile.types.ts | 1 - .../StageCreateNewButton.tsx | 2 +- .../StageDetail/EventRow/EventRow.types.ts | 2 +- .../Stage/StageDetail/stageDetail.types.ts | 2 +- .../Stages/Stage/stage.types.ts | 2 +- .../stagesAndEvents.types.ts | 2 +- .../NewTrackedEntityRelationship.types.ts | 2 +- .../WidgetTrackedEntityRelationship.types.ts | 2 +- .../DeleteRelationship/DeleteRelationship.tsx | 23 +++++++------------ .../DeleteRelationship.types.ts | 1 - .../linkedEntitiesViewer.types.ts | 2 +- .../linkedEntityTable.types.ts | 2 +- .../linkedEntityTableBody.types.ts | 2 +- .../relationshipsWidget.types.ts | 2 +- 30 files changed, 34 insertions(+), 56 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 28a9ca2e43..0f9b800692 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -715,9 +715,6 @@ msgstr "Notice" msgid "Close the notice" msgstr "Close the notice" -msgid "You do not have access to edit this enrollment" -msgstr "You do not have access to edit this enrollment" - msgid "Quick actions" msgstr "Quick actions" diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.tsx b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.tsx index ca94cb669c..3584f96870 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.tsx +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.tsx @@ -182,9 +182,7 @@ export const EnrollmentPageDefault = () => { const hasProgramWrite = Boolean(program?.access?.data?.write); const hasTETWrite = Boolean((program as any)?.trackedEntityType?.access?.data?.write); - const readOnly = !hasProgramWrite || !hasTETWrite - ? { tooltipContent: i18n.t('You do not have access to edit this enrollment') } - : undefined; + const readOnly = !hasProgramWrite || !hasTETWrite; if (isLoading) { return ( diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/EnrollmentQuickActions.component.tsx b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/EnrollmentQuickActions.component.tsx index 97576804ce..3ce36927d4 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/EnrollmentQuickActions.component.tsx +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/EnrollmentQuickActions.component.tsx @@ -23,7 +23,6 @@ const EnrollmentQuickActionsComponentPlain = ({ stages, events, ruleEffects, - readOnly, classes, }: Props) => { const [open, setOpen] = useState(true); @@ -69,7 +68,7 @@ const EnrollmentQuickActionsComponentPlain = ({ onClose={() => setOpen(false)} onOpen={() => setOpen(true)} > - {ready && !readOnly && ( + {ready && (
; events: Array; ruleEffects?: Array; - readOnly?: { tooltipContent: string }; }; diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/QuickActionButton/QuickActionButton.tsx b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/QuickActionButton/QuickActionButton.tsx index 8e0eb46c47..b345980851 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/QuickActionButton/QuickActionButton.tsx +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/QuickActionButton/QuickActionButton.tsx @@ -21,11 +21,10 @@ const QuickActionButtonPlain = ({ onClickAction, dataTest, disabled = false, - tooltipContent, classes, }: Props) => (
)} = { emptyNotes: { fontSize: 14, color: colors.grey600, + margin: 0, }, name: { fontSize: '13px', diff --git a/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx b/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx index 8c70d98c5c..cda38a4b08 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx +++ b/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx @@ -162,12 +162,23 @@ const WidgetProfilePlain = ({ ); } + if (displayInListAttributes.length === 0) { + return ( +
+

+ {i18n.t('This profile doesn\'t have any values')} +

+
+ ); + } + return (
); }; + const handleOnDisable = useCallback(() => setTeiModalState(TEI_MODAL_STATE.OPEN_DISABLE), [setTeiModalState]); const handleOnEnable = useCallback(() => setTeiModalState(TEI_MODAL_STATE.OPEN), [setTeiModalState]); diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stages.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stages.component.tsx index 03190695a3..e29d6be26f 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stages.component.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stages.component.tsx @@ -1,10 +1,23 @@ import React, { type ComponentType, useMemo } from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { colors, spacersNum } from '@dhis2/ui'; import { compose } from 'redux'; import { Stage } from './Stage'; import type { PlainProps, InputProps } from './stages.types'; import { withLoadingIndicator } from '../../../HOC'; +const emptyStateStyle = { + padding: `0 ${spacersNum.dp12}px`, + color: colors.grey600, + fontWeight: 400, + fontSize: '14px', + lineHeight: '19px', + margin: 0, + marginBottom: spacersNum.dp12, +}; + export const StagesPlain = ({ stages, events, ...passOnProps }: PlainProps) => { + const readableStages = useMemo(() => stages.filter(stage => stage.dataAccess.read), [stages]); const eventsByStage = useMemo( () => stages.reduce( (acc, stage) => { @@ -27,10 +40,17 @@ export const StagesPlain = ({ stages, events, ...passOnProps }: PlainProps) => { [stages, events], ); + if (!readableStages.length) { + return ( +

+ {i18n.t('No stages found in this program')} +

+ ); + } + return (<> { - stages - .filter(stage => stage.dataAccess.read) + readableStages .map(stage => ( + {!groupedLinkedEntities?.length && ( +

+ {i18n.t('This enrollment doesn\'t have any relationships')} +

+ )} {groupedLinkedEntities?.map((linkedEntityGroup) => { const { id, name, linkedEntities, columns, context } = linkedEntityGroup; return ( From cd80fe40f3f15ad49bd2144df5cd33c84ab3d576 Mon Sep 17 00:00:00 2001 From: henrikmv Date: Thu, 30 Apr 2026 16:40:15 +0200 Subject: [PATCH 14/60] feat: add read-only badge EnrollmentPageLayout --- i18n/en.pot | 7 ++++-- .../EnrollmentPageLayout.tsx | 22 +++++++++++++++++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index ffe94392d1..f4de401059 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: 2026-04-30T13:59:36.803Z\n" -"PO-Revision-Date: 2026-04-30T13:59:36.803Z\n" +"POT-Creation-Date: 2026-04-30T14:29:37.878Z\n" +"PO-Revision-Date: 2026-04-30T14:29:37.878Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -1000,6 +1000,9 @@ msgstr "Event could not be loaded" msgid "Organisation unit could not be loaded" msgstr "Organisation unit could not be loaded" +msgid "You can only view this enrollment" +msgstr "You can only view this enrollment" + msgid "tracked entity instance" msgstr "tracked entity instance" diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx index c5e036e078..f4fdaefc80 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx @@ -1,5 +1,6 @@ import React, { useCallback, useMemo, useState } from 'react'; -import { colors, spacers, spacersNum } from '@dhis2/ui'; +import { colors, spacers, spacersNum, IconInfo16, Tag } from '@dhis2/ui'; +import i18n from '@dhis2/d2-i18n'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { useWidgetColumns } from './hooks/useWidgetColumns'; import { AddRelationshipRefWrapper } from './AddRelationshipRefWrapper'; @@ -49,6 +50,17 @@ const getEnrollmentPageStyles: Readonly = () => ({ fontWeight: 500, paddingTop: spacersNum.dp8, }, + breadcrumbRow: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + }, + readOnlyBadge: { + display: 'flex', + alignItems: 'center', + gap: spacersNum.dp4, + flexShrink: 0, + }, }); const isValidHex = (color: string) => /^#[0-9A-F]{6}$/i.test(color); @@ -66,6 +78,7 @@ const EnrollmentPageLayoutPlain = ({ onBackToMainPage, onBackToDashboard, onBackToViewEvent, + readOnly, classes, ...passOnProps }: Props) => { @@ -115,7 +128,7 @@ const EnrollmentPageLayoutPlain = ({ className={classes.contentContainer} style={!mainContentVisible ? { display: 'none' } : undefined} > -
+
+ {readOnly && ( + }> + {i18n.t('You can only view this enrollment')} + + )}
{pageLayout.leftColumn && !!leftColumnWidgets?.length && ( From ef447f9bb60150c641c1216b4600f55b50d4f870 Mon Sep 17 00:00:00 2001 From: henrikmv Date: Fri, 1 May 2026 19:44:31 +0200 Subject: [PATCH 15/60] fix: review --- i18n/en.pot | 8 +-- .../EnrollmentPageDefault.types.ts | 4 +- .../EnrollmentEditEventPage.types.ts | 4 +- .../EnrollmentPageLayout.tsx | 6 ++- .../common/EnrollmentOverviewDomain/index.ts | 1 - .../useCommonEnrollmentDomainData/index.ts | 1 - .../useCommonEnrollmentDomainData.types.ts | 2 - .../Coordinates/Coordinates.component.tsx | 16 +++--- .../MapModal/Polygon/Polygon.component.tsx | 50 +++++++++++-------- .../WidgetEnrollment.component.tsx | 4 +- .../WidgetEnrollment.container.tsx | 2 + .../WidgetEnrollment/enrollment.types.ts | 1 + .../WidgetEventEdit.container.tsx | 2 +- .../Stages/Stage/Stage.component.tsx | 23 +++++---- .../StageCreateNewButton.tsx | 6 --- .../StageDetail/StageDetail.component.tsx | 28 ++++++----- 16 files changed, 81 insertions(+), 77 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index f4de401059..7867c55c06 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: 2026-04-30T14:29:37.878Z\n" -"PO-Revision-Date: 2026-04-30T14:29:37.878Z\n" +"POT-Creation-Date: 2026-05-01T17:38:33.516Z\n" +"PO-Revision-Date: 2026-05-01T17:38:33.516Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -1000,8 +1000,8 @@ msgstr "Event could not be loaded" msgid "Organisation unit could not be loaded" msgstr "Organisation unit could not be loaded" -msgid "You can only view this enrollment" -msgstr "You can only view this enrollment" +msgid "Read only - You can only view this enrollment" +msgstr "Read only - You can only view this enrollment" msgid "tracked entity instance" msgstr "tracked entity instance" diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.ts b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.ts index 21f19f3983..73be671945 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.ts +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.ts @@ -1,7 +1,7 @@ import { effectActions } from '@dhis2/rules-engine-javascript'; import type { TrackerProgram } from 'capture-core/metaData'; import type { HideWidgets, WidgetEffects } from '../../common/EnrollmentOverviewDomain'; -import type { Event, ReadOnlyState } from '../../common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; +import type { Event } from '../../common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; import type { LinkedRecordClick } from '../../../WidgetsRelationship/WidgetTrackedEntityRelationship'; import type { PageLayoutConfig, @@ -49,7 +49,7 @@ export type Props = { pageLayout: PageLayoutConfig; availableWidgets: Readonly<{ [key: string]: WidgetConfig }>; onDeleteTrackedEntitySuccess: () => void; - readOnly?: ReadOnlyState; + readOnly?: boolean; }; diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.ts b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.ts index 86ae840e2e..2ec3fa9228 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.ts +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.ts @@ -1,6 +1,6 @@ import type { ProgramStage } from '../../../metaData'; import { Program } from '../../../metaData'; -import type { HideWidgets, WidgetEffects, ReadOnlyState } from '../common/EnrollmentOverviewDomain'; +import type { HideWidgets, WidgetEffects } from '../common/EnrollmentOverviewDomain'; import type { UserFormField } from '../../FormFields/UserField'; import type { LinkedRecordClick } from '../../WidgetsRelationship/WidgetTrackedEntityRelationship'; import type { @@ -60,7 +60,7 @@ export type PlainProps = { onUpdateOrAddEnrollmentEvents: (events: Array) => void; onUpdateEnrollmentEventsSuccess: (events: Array) => void; onUpdateEnrollmentEventsError: (events: Array) => void; - readOnly?: ReadOnlyState; + readOnly?: boolean; }; export type Props = { diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx index f4fdaefc80..a1747b9751 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx @@ -94,12 +94,14 @@ const EnrollmentPageLayoutPlain = ({ eventStatus, toggleVisibility, addRelationShipContainerElement, + readOnly, }), [ addRelationShipContainerElement, currentPage, eventStatus, passOnProps, program, + readOnly, toggleVisibility, ]); @@ -140,8 +142,8 @@ const EnrollmentPageLayoutPlain = ({ eventStatus={eventStatus} /> {readOnly && ( - }> - {i18n.t('You can only view this enrollment')} + }> + {i18n.t('Read only - You can only view this enrollment')} )}
diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/index.ts b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/index.ts index ebde658854..f8a7d316da 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/index.ts +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/index.ts @@ -20,5 +20,4 @@ export { rollbackEnrollmentEvents, } from './enrollment.actions'; export { useCommonEnrollmentDomainData } from './useCommonEnrollmentDomainData'; -export type { ReadOnlyState } from './useCommonEnrollmentDomainData'; export { useRuleEffects } from './useRuleEffects'; diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/index.ts b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/index.ts index 9dcf062c08..530ec3d807 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/index.ts +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/index.ts @@ -5,5 +5,4 @@ export type { EnrollmentData, AttributeValue, Output, - ReadOnlyState, } from './useCommonEnrollmentDomainData.types'; diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.ts b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.ts index 1aebfe601a..20cdf66a7d 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.ts +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.ts @@ -32,8 +32,6 @@ export type AttributeValue = { value: string; }; -export type ReadOnlyState = boolean; - export type Output = { error?: any; enrollment?: EnrollmentData; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/Coordinates.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/Coordinates.component.tsx index 744f1b4636..04cad00d72 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/Coordinates.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/Coordinates.component.tsx @@ -77,14 +77,13 @@ const CoordinatesPlain = ({ }; const onHandleMapClicked = (mapCoordinates: { latlng: { lat: number; lng: number } }) => { - if (isEditing) { - const { lat, lng } = mapCoordinates.latlng; - const newPosition: [number, number] = [lat, lng]; - setValid(true); - setPosition(newPosition); - setTempLatitude(lat); - setTempLongitude(lng); - } + if (readOnly || !isEditing) return; + const { lat, lng } = mapCoordinates.latlng; + const newPosition: [number, number] = [lat, lng]; + setValid(true); + setPosition(newPosition); + setTempLatitude(lat); + setTempLongitude(lng); }; const onSearch = (searchPosition: [number, number]) => { @@ -99,6 +98,7 @@ const CoordinatesPlain = ({ { if (ref?.leafletElement) { ref.leafletElement.invalidateSize(); diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/Polygon.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/Polygon.component.tsx index c368b43afe..d82620b62c 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/Polygon.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/Polygon.component.tsx @@ -76,6 +76,7 @@ const PolygonPlain = ({ }; const onMapPolygonCreated = (e: any) => { + if (readOnly) return; const polygonCoordinates = e.layer.toGeoJSON().geometry.coordinates[0].map((c: number[]) => [c[1], c[0]]); setPolygonArea(polygonCoordinates); setDrawingState(drawing.FINISHED); @@ -83,6 +84,7 @@ const PolygonPlain = ({ }; const onMapPolygonDelete = () => { + if (readOnly) return; setPolygonArea(null); setDrawingState(drawing.FINISHED); prevDrawingState.current = drawing.FINISHED; @@ -129,28 +131,32 @@ const PolygonPlain = ({ onFeatureGroupReady(reactFGref, getFeatureCollection()); }} > - setDrawingState(drawing.STARTED)} - onDrawStop={() => setDrawingState(prevDrawingState.current)} - draw={{ - rectangle: false, - polyline: false, - circle: false, - marker: false, - circlemarker: false, - }} - edit={{ - remove: false, - edit: false, - }} - /> - + {!readOnly && ( + <> + setDrawingState(drawing.STARTED)} + onDrawStop={() => setDrawingState(prevDrawingState.current)} + draw={{ + rectangle: false, + polyline: false, + circle: false, + marker: false, + circlemarker: false, + }} + edit={{ + remove: false, + edit: false, + }} + /> + + + )} ); diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx index 092a1f72b1..b0bcf1b3c2 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx @@ -178,8 +178,8 @@ const WidgetEnrollmentPlain = ({
)} > | null }; onDelete: () => void; onAddNew: () => void; diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/WidgetEventEdit.container.tsx b/src/core_modules/capture-core/components/WidgetEventEdit/WidgetEventEdit.container.tsx index b8a6ec701d..f7803e3b5e 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/WidgetEventEdit.container.tsx +++ b/src/core_modules/capture-core/components/WidgetEventEdit/WidgetEventEdit.container.tsx @@ -137,7 +137,7 @@ const WidgetEventEditPlain = ({ borderless >
- {currentPageMode === dataEntryKeys.VIEW ? ( + {currentPageMode === dataEntryKeys.VIEW || readOnly ? (
: ( -
- onCreateNew(id)} - stageWriteAccess={stage.dataAccess.write} - eventCount={events.length} - repeatable={repeatable} - preventAddingEventActionInEffect={preventAddingNewEvents} - eventName={name} - readOnly={readOnly} - /> -
+ !readOnly && ( +
+ onCreateNew(id)} + stageWriteAccess={stage.dataAccess.write} + eventCount={events.length} + repeatable={repeatable} + preventAddingEventActionInEffect={preventAddingNewEvents} + eventName={name} + /> +
+ ) )}
diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageCreateNewButton/StageCreateNewButton.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageCreateNewButton/StageCreateNewButton.tsx index 8583114753..0a503dc2dd 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageCreateNewButton/StageCreateNewButton.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageCreateNewButton/StageCreateNewButton.tsx @@ -10,7 +10,6 @@ type Props = { repeatable?: boolean; preventAddingEventActionInEffect?: boolean; eventName: string; - readOnly?: boolean; }; export const StageCreateNewButton = ({ @@ -20,7 +19,6 @@ export const StageCreateNewButton = ({ repeatable, preventAddingEventActionInEffect, eventName, - readOnly, }: Props) => { const { isDisabled, tooltipContent } = useMemo(() => { if (!stageWriteAccess) { @@ -53,10 +51,6 @@ export const StageCreateNewButton = ({ }; }, [eventCount, eventName, preventAddingEventActionInEffect, repeatable, stageWriteAccess]); - if (readOnly) { - return null; - } - return ( ) => { onClick={handleViewAll} >{i18n.t('Go to full {{ eventName }}', { eventName, interpolation: { escapeValue: false } })} : null); - const renderCreateNewButton = () => ( -
- -
- ); + const renderCreateNewButton = () => { + if (readOnly) return null; + return ( +
+ +
+ ); + }; return (
From 7854f4131a28955e649ba2f9e8e78545b01d82bd Mon Sep 17 00:00:00 2001 From: henrikmv Date: Fri, 1 May 2026 20:33:23 +0200 Subject: [PATCH 16/60] feat: propagate readOnly prop to LinkedEntityTable components --- .../RelationshipsWidget/LinkedEntityTable.component.tsx | 1 + .../RelationshipsWidget/LinkedEntityTableHeader.component.tsx | 4 ++-- .../RelationshipsWidget/linkedEntityTableHeader.types.ts | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTable.component.tsx b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTable.component.tsx index 3f6ad67b98..30c1a6d9ee 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTable.component.tsx +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTable.component.tsx @@ -48,6 +48,7 @@ const LinkedEntityTablePlain = ({ ( +export const LinkedEntityTableHeader = ({ columns, context, readOnly }: Props) => ( { @@ -19,7 +19,7 @@ export const LinkedEntityTableHeader = ({ columns, context }: Props) => ( )) } - {context.display.showDeleteButton ? ( + {context.display.showDeleteButton && !readOnly ? ( ) : null} diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTableHeader.types.ts b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTableHeader.types.ts index 28fa0952a9..299af3815d 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTableHeader.types.ts +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTableHeader.types.ts @@ -3,4 +3,5 @@ import type { Context, TableColumn } from './types'; export type Props = Readonly<{ columns: readonly TableColumn[]; context: Context; + readOnly?: boolean; }>; From 75970480032c9f3b8b64beb0e98b079c05590bb2 Mon Sep 17 00:00:00 2001 From: henrikmv Date: Fri, 1 May 2026 21:02:40 +0200 Subject: [PATCH 17/60] feat: temp --- i18n/en.pot | 13 +- .../EnrollmentPageDefault.container.tsx | 1 - .../QuickActionButton/QuickActionButton.tsx | 9 +- .../EnrollmentPageLayout.tsx | 10 +- .../ReadOnlyBadge/ReadOnlyBadge.tsx | 9 ++ .../components/ReadOnlyBadge/index.ts | 1 + .../Actions/Actions.component.tsx | 121 +++++++++--------- .../WidgetEnrollment/Actions/actions.types.ts | 2 - .../WidgetEnrollment.component.tsx | 1 - 9 files changed, 76 insertions(+), 91 deletions(-) create mode 100644 src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx create mode 100644 src/core_modules/capture-core/components/ReadOnlyBadge/index.ts diff --git a/i18n/en.pot b/i18n/en.pot index 7867c55c06..e32a0bc7d2 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: 2026-05-01T17:38:33.516Z\n" -"PO-Revision-Date: 2026-05-01T17:38:33.516Z\n" +"POT-Creation-Date: 2026-05-01T19:02:41.896Z\n" +"PO-Revision-Date: 2026-05-01T19:02:41.896Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -1000,9 +1000,6 @@ msgstr "Event could not be loaded" msgid "Organisation unit could not be loaded" msgstr "Organisation unit could not be loaded" -msgid "Read only - You can only view this enrollment" -msgstr "Read only - You can only view this enrollment" - msgid "tracked entity instance" msgstr "tracked entity instance" @@ -1071,6 +1068,9 @@ msgstr "Possible duplicates found" msgid "An error occurred loading possible duplicates" msgstr "An error occurred loading possible duplicates" +msgid "Read only - You can only view this enrollment" +msgstr "Read only - You can only view this enrollment" + msgid "You don't have access to delete this relationship" msgstr "You don't have access to delete this relationship" @@ -1311,9 +1311,6 @@ msgstr "Unsaved changes" msgid "Continue data entry" msgstr "Continue data entry" -msgid "You do not have access to modify this enrollment" -msgstr "You do not have access to modify this enrollment" - msgid "Enrollment actions" msgstr "Enrollment actions" diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.tsx b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.tsx index 3584f96870..d5fd21d2a8 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.tsx +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.tsx @@ -199,7 +199,6 @@ export const EnrollmentPageDefault = () => { pageLayout={pageLayout} currentPage={EnrollmentPageKeys.OVERVIEW} availableWidgets={WidgetsForEnrollmentPageDefault} - readOnly={readOnly} teiId={teiId} orgUnitId={orgUnitId} diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/QuickActionButton/QuickActionButton.tsx b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/QuickActionButton/QuickActionButton.tsx index b345980851..608b0e6e46 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/QuickActionButton/QuickActionButton.tsx +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/QuickActionButton/QuickActionButton.tsx @@ -15,14 +15,7 @@ const styles = { type Props = QuickActionButtonTypes & WithStyles; -const QuickActionButtonPlain = ({ - icon, - label, - onClickAction, - dataTest, - disabled = false, - classes, -}: Props) => ( +const QuickActionButtonPlain = ({ icon, label, onClickAction, dataTest, disabled = false, classes }: Props) => ( = () => ({ @@ -141,11 +141,7 @@ const EnrollmentPageLayoutPlain = ({ userInteractionInProgress={userInteractionInProgress} eventStatus={eventStatus} /> - {readOnly && ( - }> - {i18n.t('Read only - You can only view this enrollment')} - - )} + {readOnly && }
{pageLayout.leftColumn && !!leftColumnWidgets?.length && ( diff --git a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx new file mode 100644 index 0000000000..4fb40eebb7 --- /dev/null +++ b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import { colors, IconInfo16, Tag } from '@dhis2/ui'; +import i18n from '@dhis2/d2-i18n'; + +export const ReadOnlyBadge = () => ( + }> + {i18n.t('Read only - You can only view this enrollment')} + +); diff --git a/src/core_modules/capture-core/components/ReadOnlyBadge/index.ts b/src/core_modules/capture-core/components/ReadOnlyBadge/index.ts new file mode 100644 index 0000000000..12396a40ff --- /dev/null +++ b/src/core_modules/capture-core/components/ReadOnlyBadge/index.ts @@ -0,0 +1 @@ +export { ReadOnlyBadge } from './ReadOnlyBadge'; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.component.tsx index 3c4707a1d7..0414f6e2ea 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.component.tsx @@ -10,7 +10,6 @@ import { AddNew } from './AddNew'; import { AddLocation } from './AddLocation'; import type { PlainProps } from './actions.types'; import { LoadingMaskForButton } from '../../LoadingMasks'; -import { ConditionalTooltip } from '../../Tooltips/ConditionalTooltip'; import { MapModal } from '../MapModal'; import { Transfer } from './Transfer'; import { TransferModal } from '../TransferModal'; @@ -35,7 +34,6 @@ const ActionsPlain = ({ ownerOrgUnitId, tetName, canAddNew, - programDataWriteAccess, readOnly, onUpdateStatus, onUpdate, @@ -72,69 +70,64 @@ const ActionsPlain = ({ return ( <> - setOpenActions(prev => !prev)} + component={ + + + { + setOpenCompleteModal(modalState); + setOpenActions(!modalState); + }} + /> + + { + setOpenTransfer(true); + setOpenActions(false); + }} + /> + { + setOpenMap(true); + setOpenActions(false); + }} + /> + + + + + } > - setOpenActions(prev => !prev)} - component={ - - - { - setOpenCompleteModal(modalState); - setOpenActions(!modalState); - }} - /> - - { - setOpenTransfer(true); - setOpenActions(false); - }} - /> - { - setOpenMap(true); - setOpenActions(false); - }} - /> - - - - - } - > - {i18n.t('Enrollment actions')} - - + {i18n.t('Enrollment actions')} + {loading && (
diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/actions.types.ts b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/actions.types.ts index ff11549f8c..6aa4f590aa 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/actions.types.ts +++ b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/actions.types.ts @@ -16,7 +16,6 @@ export type Props = { onError?: (message: string) => void; onSuccess?: () => void; canAddNew: boolean; - programDataWriteAccess: boolean; onlyEnrollOnce: boolean; tetName: string; onAccessLostFromTransfer?: () => void; @@ -37,7 +36,6 @@ export type PlainProps = { isTransferLoading: boolean; loading: boolean; canAddNew: boolean; - programDataWriteAccess: boolean; onlyEnrollOnce: boolean; tetName: string; readOnly?: boolean; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx index b0bcf1b3c2..0cbf7d1099 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx @@ -179,7 +179,6 @@ const WidgetEnrollmentPlain = ({ )} Date: Fri, 1 May 2026 21:22:54 +0200 Subject: [PATCH 18/60] feat: clean up --- .../EnrollmentPageLayout.tsx | 2 +- .../components/ReadOnlyBadge/ReadOnlyBadge.tsx | 17 ++++++++++++----- .../WidgetEnrollmentNote.component.tsx | 2 +- .../WidgetHeader/WidgetHeader.container.tsx | 10 +++------- .../WidgetEventNote.component.tsx | 2 +- .../WidgetNote/NoteSection/NoteSection.tsx | 6 +++--- .../WidgetNote/NoteSection/NoteSection.types.ts | 2 +- .../components/WidgetNote/WidgetNote.types.ts | 2 +- 8 files changed, 23 insertions(+), 20 deletions(-) diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx index 3149130224..97608185bd 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx @@ -141,7 +141,7 @@ const EnrollmentPageLayoutPlain = ({ userInteractionInProgress={userInteractionInProgress} eventStatus={eventStatus} /> - {readOnly && } +
{pageLayout.leftColumn && !!leftColumnWidgets?.length && ( diff --git a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx index 4fb40eebb7..b6c647dc79 100644 --- a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx +++ b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx @@ -2,8 +2,15 @@ import React from 'react'; import { colors, IconInfo16, Tag } from '@dhis2/ui'; import i18n from '@dhis2/d2-i18n'; -export const ReadOnlyBadge = () => ( - }> - {i18n.t('Read only - You can only view this enrollment')} - -); +type Props = { + readOnly?: boolean; +}; + +export const ReadOnlyBadge = ({ readOnly }: Props) => { + if (!readOnly) return null; + return ( + }> + {i18n.t('Read only - You can only view this enrollment')} + + ); +}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx index f6aa659a4b..3ec39af5d2 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx @@ -27,7 +27,7 @@ export const WidgetEnrollmentNote = ({ readOnly }: Props) => { emptyNoteMessage={i18n.t('This enrollment doesn\'t have any notes')} notes={notes} onAddNote={onAddNote} - disabled={Boolean(readOnly)} + readOnly={Boolean(readOnly)} />
); diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/WidgetHeader/WidgetHeader.container.tsx b/src/core_modules/capture-core/components/WidgetEventEdit/WidgetHeader/WidgetHeader.container.tsx index 7afecd6060..83624c6a1b 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/WidgetHeader/WidgetHeader.container.tsx +++ b/src/core_modules/capture-core/components/WidgetEventEdit/WidgetHeader/WidgetHeader.container.tsx @@ -10,7 +10,7 @@ import { ConditionalTooltip } from 'capture-core/components/Tooltips/Conditional import { useEnrollmentEditEventPageMode, useProgramExpiryForUser } from 'capture-core/hooks'; import { startShowEditEventDataEntry } from '../WidgetEventEdit.actions'; import { NonBundledDhis2Icon } from '../../NonBundledDhis2Icon'; -import { dataElementTypes, getProgramEventAccess } from '../../../metaData'; +import { dataElementTypes } from '../../../metaData'; import { useCategoryCombinations } from '../../DataEntryDhis2Helpers/AOC/useCategoryCombinations'; import { OverflowButton } from '../../Buttons'; import { inMemoryFileStore } from '../../DataEntry/file/inMemoryFileStore'; @@ -55,7 +55,6 @@ const WidgetHeaderPlain = ({ const { currentPageMode } = useEnrollmentEditEventPageMode(eventStatus); const [actionsIsOpen, setActionsIsOpen] = useState(false); - const eventAccess = getProgramEventAccess(programId, stage.id); const { hasAuthority } = useAuthorities({ authorities: ['F_UNCOMPLETE_EVENT'] }); const blockEntryForm = stage.blockEntryForm && !hasAuthority && eventStatus === eventStatuses.COMPLETED; @@ -63,14 +62,11 @@ const WidgetHeaderPlain = ({ const occurredAtClient = convertFormToClient(occurredAt, dataElementTypes.DATE) as string; const { isWithinValidPeriod } = isValidPeriod(occurredAtClient, expiryPeriod); - const disableEdit = !eventAccess?.write || blockEntryForm || !isWithinValidPeriod; + const disableEdit = blockEntryForm || !isWithinValidPeriod; const tooltipContent = useMemo(() => { if (blockEntryForm) { return i18n.t('The event cannot be edited after it has been completed'); } - if (!eventAccess?.write) { - return i18n.t('You don\'t have access to edit this event'); - } if (!isWithinValidPeriod) { return i18n.t('{{occurredAt}} belongs to an expired period. Event cannot be edited', { occurredAt, @@ -78,7 +74,7 @@ const WidgetHeaderPlain = ({ }); } return ''; - }, [blockEntryForm, eventAccess?.write, isWithinValidPeriod, occurredAt]); + }, [blockEntryForm, isWithinValidPeriod, occurredAt]); const { programCategory } = useCategoryCombinations(programId); diff --git a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx index 26510df976..6f199668a7 100644 --- a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx @@ -22,7 +22,7 @@ export const WidgetEventNote = ({ dataEntryKey, dataEntryId, readOnly }: Props) emptyNoteMessage={i18n.t('This event doesn\'t have any notes')} notes={notes} onAddNote={onAddNote} - disabled={Boolean(readOnly)} + readOnly={Boolean(readOnly)} />
); diff --git a/src/core_modules/capture-core/components/WidgetNote/NoteSection/NoteSection.tsx b/src/core_modules/capture-core/components/WidgetNote/NoteSection/NoteSection.tsx index e6b42c6666..54ec743fd3 100644 --- a/src/core_modules/capture-core/components/WidgetNote/NoteSection/NoteSection.tsx +++ b/src/core_modules/capture-core/components/WidgetNote/NoteSection/NoteSection.tsx @@ -71,7 +71,7 @@ const NoteSectionPlain = ({ emptyNoteMessage, notes, handleAddNote, - disabled, + readOnly, classes, }: Props) => { const [isEditing, setEditing] = useState(false); @@ -132,7 +132,7 @@ const NoteSectionPlain = ({
}
- {!disabled &&
+ {!readOnly &&
} - {!disabled && isEditing &&
+ {!readOnly && isEditing &&
)} Date: Mon, 4 May 2026 10:08:42 +0200 Subject: [PATCH 21/60] fix: update readOnly prop logic in WidgetEnrollment component to use programDataWriteAccess --- .../components/WidgetEnrollment/WidgetEnrollment.component.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx index 3413d800ff..ae5a95dd82 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx @@ -181,7 +181,7 @@ const WidgetEnrollmentPlain = ({
)} Date: Mon, 4 May 2026 10:48:04 +0200 Subject: [PATCH 22/60] feat: type clean up --- .../EnrollmentPageDefault/EnrollmentPageDefault.types.ts | 2 +- .../LayoutComponentConfig/LayoutComponentConfig.ts | 2 +- .../TrackedEntityRelationshipsWrapper.types.ts | 2 +- .../capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx | 6 +----- .../WidgetEnrollment/Actions/Actions.component.tsx | 1 + .../components/WidgetEnrollment/Actions/actions.types.ts | 4 ++-- .../MapModal/Coordinates/Coordinates.types.ts | 2 +- .../components/WidgetEnrollment/MapModal/MapModal.types.ts | 4 ++-- .../WidgetEnrollment/MapModal/Polygon/Polygon.types.ts | 2 +- .../components/WidgetEnrollment/MiniMap/MiniMap.types.ts | 2 +- .../WidgetEnrollment/WidgetEnrollment.component.tsx | 2 +- 11 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.ts b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.ts index 73be671945..2711a194c5 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.ts +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.ts @@ -49,7 +49,7 @@ export type Props = { pageLayout: PageLayoutConfig; availableWidgets: Readonly<{ [key: string]: WidgetConfig }>; onDeleteTrackedEntitySuccess: () => void; - readOnly?: boolean; + readOnly: boolean; }; diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts index ff341ed9d8..48346fcb63 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts @@ -142,7 +142,7 @@ export const EnrollmentNote: WidgetConfig = { export const ProfileWidget: WidgetConfig = { Component: WidgetProfile, getCustomSettings: ({ readOnlyMode = true }: any, props?: any) => ({ - readOnlyMode: readOnlyMode || Boolean(props?.readOnly), + readOnlyMode: readOnlyMode || props?.readOnly, }), getProps: ({ teiId, diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types.ts b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types.ts index e88011891c..f9ac35de0d 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types.ts +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types.ts @@ -9,5 +9,5 @@ export type Props = { onOpenAddRelationship: () => void; onCloseAddRelationship: () => void; onLinkedRecordClick: LinkedRecordClick; - readOnly?: boolean; + readOnly: boolean; }; diff --git a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx index b6c647dc79..0bc6fdf7fb 100644 --- a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx +++ b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx @@ -2,11 +2,7 @@ import React from 'react'; import { colors, IconInfo16, Tag } from '@dhis2/ui'; import i18n from '@dhis2/d2-i18n'; -type Props = { - readOnly?: boolean; -}; - -export const ReadOnlyBadge = ({ readOnly }: Props) => { +export const ReadOnlyBadge = ({ readOnly }: { readOnly: boolean }) => { if (!readOnly) return null; return ( }> diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.component.tsx index 0414f6e2ea..1f594bb5c8 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.component.tsx @@ -136,6 +136,7 @@ const ActionsPlain = ({
)} {isOpenMap && void; - readOnly?: boolean; + readOnly: boolean; }; export type PlainProps = { @@ -38,5 +38,5 @@ export type PlainProps = { canAddNew: boolean; onlyEnrollOnce: boolean; tetName: string; - readOnly?: boolean; + readOnly: boolean; }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/Coordinates.types.ts b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/Coordinates.types.ts index 7205dff80d..135a129c70 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/Coordinates.types.ts +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/Coordinates.types.ts @@ -3,5 +3,5 @@ export type CoordinatesProps = { setOpen: (open: boolean) => void; onSetCoordinates: (coordinates: [number, number] | Array> | null) => void; defaultValues?: [number, number] | null; - readOnly?: boolean; + readOnly: boolean; }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/MapModal.types.ts b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/MapModal.types.ts index 00f86d12f6..e5556bbd74 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/MapModal.types.ts +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/MapModal.types.ts @@ -6,7 +6,7 @@ export type MapModalComponentProps = { defaultValues?: number[][] | [number, number] | null; setOpen: (open: boolean) => void; onSetCoordinates: (coordinates: [number, number] | Array> | null) => void; - readOnly?: boolean; + readOnly: boolean; } export type MapModalProps = { @@ -15,5 +15,5 @@ export type MapModalProps = { onUpdate: (arg: Record) => void; setOpenMap: (toggle: boolean) => void; defaultValues?: number[][] | [number, number] | null; - readOnly?: boolean; + readOnly: boolean; }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/Polygon.types.ts b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/Polygon.types.ts index 28ddb5ed47..7cc9e14b4e 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/Polygon.types.ts +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/Polygon.types.ts @@ -17,5 +17,5 @@ export type PolygonProps = { setOpen: (open: boolean) => void; onSetCoordinates: (coordinates: [number, number] | Array> | null) => void; defaultValues?: Array> | null; - readOnly?: boolean; + readOnly: boolean; } diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MiniMap/MiniMap.types.ts b/src/core_modules/capture-core/components/WidgetEnrollment/MiniMap/MiniMap.types.ts index 24051b4ca1..033162987b 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/MiniMap/MiniMap.types.ts +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MiniMap/MiniMap.types.ts @@ -8,5 +8,5 @@ export type OwnProps = { refetchTEI: QueryRefetchFunction; onError?: (message: string) => void; geometryType: typeof dataElementTypes.COORDINATE | typeof dataElementTypes.POLYGON; - readOnly?: boolean; + readOnly: boolean; }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx index ae5a95dd82..4447be36e9 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx @@ -181,7 +181,7 @@ const WidgetEnrollmentPlain = ({ )} Date: Mon, 4 May 2026 13:46:24 +0200 Subject: [PATCH 23/60] feat: clean up --- i18n/en.pot | 13 +- .../EnrollmentEditEventPage.types.ts | 2 +- .../LayoutComponentConfig.ts | 6 +- .../Widget/widgetNonCollapsible.types.ts | 2 +- .../WidgetEnrollment/Date/Date.component.tsx | 8 +- .../WidgetEnrollment.component.tsx | 7 +- .../WidgetEnrollment/enrollment.types.ts | 2 +- .../WidgetEnrollmentNote.component.tsx | 8 +- .../WidgetHeader/WidgetHeader.types.ts | 2 +- .../WidgetEventEdit/widgetEventEdit.types.ts | 2 +- .../WidgetEventNote.component.tsx | 2 +- .../WidgetEventNote/WidgetEventNote.types.ts | 2 +- .../WidgetEventSchedule.component.tsx | 1 + .../WidgetNote/NoteSection/NoteSection.tsx | 1 - .../NoteSection/NoteSection.types.ts | 2 +- .../components/WidgetNote/WidgetNote.types.ts | 2 +- .../DataEntry/DataEntry.component.tsx | 43 ++++--- .../DataEntry/DataEntry.container.tsx | 2 + .../DataEntry/dataEntry.types.ts | 2 + .../WidgetProfile/WidgetProfile.component.tsx | 111 ++++++++++-------- ...NewTrackedEntityRelationship.container.tsx | 4 +- .../LinkedEntitiesViewer.component.tsx | 24 ++-- 22 files changed, 131 insertions(+), 117 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index d195677fc3..673cd7825b 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: 2026-05-04T07:37:42.584Z\n" -"PO-Revision-Date: 2026-05-04T07:37:42.584Z\n" +"POT-Creation-Date: 2026-05-04T11:46:25.703Z\n" +"PO-Revision-Date: 2026-05-04T11:46:25.703Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -1587,6 +1587,9 @@ msgstr "Indicators" msgid "Save note" msgstr "Save note" +msgid "{{trackedEntityName}} details" +msgstr "{{trackedEntityName}} details" + msgid "Edit {{trackedEntityName}}" msgstr "Edit {{trackedEntityName}}" @@ -1638,6 +1641,9 @@ msgstr "There was a problem deleting the {{trackedEntityTypeName}}" msgid "Yes, delete {{trackedEntityTypeName}}" msgstr "Yes, delete {{trackedEntityTypeName}}" +msgid "Show details" +msgstr "Show details" + msgid "Profile widget could not be loaded. Please try again later" msgstr "Profile widget could not be loaded. Please try again later" @@ -1647,9 +1653,6 @@ msgstr "No attributes configured for {{trackedEntityTypeName}}" msgid "No attributes configured" msgstr "No attributes configured" -msgid "This profile doesn't have any values" -msgstr "This profile doesn't have any values" - msgid "{{trackedEntityTypeName}} profile" msgstr "{{trackedEntityTypeName}} profile" diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.ts b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.ts index 2ec3fa9228..4f84f9c8f2 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.ts +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.ts @@ -60,7 +60,7 @@ export type PlainProps = { onUpdateOrAddEnrollmentEvents: (events: Array) => void; onUpdateEnrollmentEventsSuccess: (events: Array) => void; onUpdateEnrollmentEventsError: (events: Array) => void; - readOnly?: boolean; + readOnly: boolean; }; export type Props = { diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts index 48346fcb63..8de4787bf0 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts @@ -141,8 +141,8 @@ export const EnrollmentNote: WidgetConfig = { export const ProfileWidget: WidgetConfig = { Component: WidgetProfile, - getCustomSettings: ({ readOnlyMode = true }: any, props?: any) => ({ - readOnlyMode: readOnlyMode || props?.readOnly, + getCustomSettings: ({ readOnlyMode = true }: any) => ({ + readOnlyMode, }), getProps: ({ teiId, @@ -190,7 +190,7 @@ export const EnrollmentWidget: WidgetConfig = { Component: WidgetEnrollment, shouldHideWidget: ({ enrollmentId }: any) => enrollmentId === 'AUTO', getCustomSettings: ({ readOnlyMode }: any, props?: any) => ({ - readOnlyMode: readOnlyMode || Boolean(props?.readOnly), + readOnlyMode, readOnly: props?.readOnly, }), getProps: ({ diff --git a/src/core_modules/capture-core/components/Widget/widgetNonCollapsible.types.ts b/src/core_modules/capture-core/components/Widget/widgetNonCollapsible.types.ts index fcc8fe106d..755d105bec 100644 --- a/src/core_modules/capture-core/components/Widget/widgetNonCollapsible.types.ts +++ b/src/core_modules/capture-core/components/Widget/widgetNonCollapsible.types.ts @@ -2,7 +2,7 @@ import type { ReactNode } from 'react'; export type WidgetNonCollapsiblePropsPlain = { header?: ReactNode; - children: ReactNode; + children?: ReactNode; color?: string; borderless?: boolean; }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/Date/Date.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/Date/Date.component.tsx index b84d797d48..0845ccee9f 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/Date/Date.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/Date/Date.component.tsx @@ -23,7 +23,7 @@ type OwnProps = { date: string; dateLabel: string; locale: string; - editEnabled: boolean; + readOnly: boolean; displayAutoGeneratedEventWarning: boolean; onSave: (date: string) => void; allowFutureDate?: boolean; @@ -91,11 +91,11 @@ const DateComponentPlain = ({ date, dateLabel, locale, - editEnabled, + readOnly, displayAutoGeneratedEventWarning, onSave, - classes, allowFutureDate, + classes, }: Props) => { const [editMode, setEditMode] = useState(false); const [selectedDate, setSelectedDate] = useState(); @@ -209,7 +209,7 @@ const DateComponentPlain = ({ {displayDate} - {editEnabled && ( + {!readOnly && ( - {modalState === TEI_MODAL_STATE.OPEN_DISABLE && ( + {!readOnly && modalState === TEI_MODAL_STATE.OPEN_DISABLE && ( )} - - {(modalState === TEI_MODAL_STATE.OPEN || modalState === TEI_MODAL_STATE.OPEN_ERROR) && ( + {!readOnly && (modalState === TEI_MODAL_STATE.OPEN || modalState === TEI_MODAL_STATE.OPEN_ERROR) && ( diff --git a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.container.tsx b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.container.tsx index 6a4ae8911d..c39bbc995f 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.container.tsx +++ b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.container.tsx @@ -32,6 +32,7 @@ export const DataEntry = ({ geometry, trackedEntityName, dataEntryFormConfig, + readOnly, }: Props) => { const dataEntryId = 'trackedEntityProfile'; const itemId = 'edit'; @@ -163,6 +164,7 @@ export const DataEntry = ({ warningsMessages={warningsMessages} orgUnitId={orgUnitId} pluginContext={pluginContext} + readOnly={readOnly} /> ) ); diff --git a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/dataEntry.types.ts b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/dataEntry.types.ts index c7f33e3d05..fe1acc3741 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/dataEntry.types.ts +++ b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/dataEntry.types.ts @@ -21,6 +21,7 @@ export type PlainProps = { center?: Array; orgUnitId: string; pluginContext?: PluginContext; + readOnly?: boolean; }; export type Props = { @@ -39,4 +40,5 @@ export type Props = { geometry?: Geometry; userRoles: Array; trackedEntityName: string; + readOnly?: boolean; }; diff --git a/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx b/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx index 1fa28951ed..ff509ce602 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx +++ b/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx @@ -109,6 +109,13 @@ const WidgetProfilePlain = ({ !readOnlyMode, [hasNoAttributes, readOnlyMode, trackedEntityTypeAccess]); + const profileButtonLabel = useMemo(() => { + if (hasNoAttributes) return null; + if (readOnlyMode) return i18n.t('Show details'); + if (isEditable) return i18n.t('Edit'); + return null; + }, [hasNoAttributes, isEditable, readOnlyMode]); + const loading = computeLoadingState(programsLoading, trackedEntityInstancesLoading, userRolesLoading, configIsFetched); const error = computeError(programsError, trackedEntityInstancesError, userRolesError); const clientAttributesWithSubvalues = useClientAttributesWithSubvalues( @@ -174,16 +181,6 @@ const WidgetProfilePlain = ({ ); } - if (displayInListAttributes.length === 0) { - return ( -
-

- {i18n.t('This profile doesn\'t have any values')} -

-
- ); - } - return (
@@ -193,50 +190,61 @@ const WidgetProfilePlain = ({ const handleOnDisable = useCallback(() => setTeiModalState(TEI_MODAL_STATE.OPEN_DISABLE), [setTeiModalState]); const handleOnEnable = useCallback(() => setTeiModalState(TEI_MODAL_STATE.OPEN), [setTeiModalState]); + const handleOpen = useCallback(() => setOpenStatus(true), [setOpenStatus]); + const handleClose = useCallback(() => setOpenStatus(false), [setOpenStatus]); + + const isEmptyList = !loading && !error && !hasNoAttributes && displayInListAttributes.length === 0; + + const trackedEntityProp = useMemo(() => ({ + trackedEntity: trackedEntity ? (trackedEntity.trackedEntity || teiId) : teiId, + }), [trackedEntity, teiId]); + + const widgetHeader = ( +
+
+ {trackedEntityTypeName + ? i18n.t('{{trackedEntityTypeName}} profile', { + trackedEntityTypeName, + interpolation: { escapeValue: false }, + }) + : i18n.t('Profile')} +
+
+ {profileButtonLabel && ( + + )} + +
+
+ ); return (
- -
- {trackedEntityTypeName - ? i18n.t('{{trackedEntityTypeName}} profile', { - trackedEntityTypeName, - interpolation: { escapeValue: false }, - }) - : i18n.t('Profile')} -
-
- {isEditable && ( - - )} - -
-
- } - onOpen={useCallback(() => setOpenStatus(true), [setOpenStatus])} - onClose={useCallback(() => setOpenStatus(false), [setOpenStatus])} - open={open} - > - {renderProfile()} - - {showEditModal(loading, error, isEditable, modalState, program) && ( + {isEmptyList ? ( + + ) : ( + + {renderProfile()} + + )} + {showEditModal(loading, error, Boolean(profileButtonLabel), modalState, program) && ( <> setTeiModalState(TEI_MODAL_STATE.CLOSE)} @@ -254,6 +262,7 @@ const WidgetProfilePlain = ({ modalState={modalState} geometry={geometry} trackedEntityName={trackedEntityTypeName} + readOnly={readOnlyMode} /> diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.tsx b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.tsx index b34647d105..e3bbaed31c 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.tsx +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useState } from 'react'; -import { Button, spacers } from '@dhis2/ui'; +import { Button, spacersNum } from '@dhis2/ui'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import i18n from '@dhis2/d2-i18n'; import { NewTrackedEntityRelationshipPortal } from './NewTrackedEntityRelationship.portal'; @@ -7,7 +7,7 @@ import type { ContainerProps } from './NewTrackedEntityRelationship.types'; const styles = { container: { - padding: `0 ${spacers.dp12} ${spacers.dp12} ${spacers.dp12}`, + padding: `${spacersNum.dp8}px ${spacersNum.dp12}px ${spacersNum.dp12}px`, }, }; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntitiesViewer.component.tsx b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntitiesViewer.component.tsx index bb23ec1dc7..ac55a35fc7 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntitiesViewer.component.tsx +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntitiesViewer.component.tsx @@ -2,30 +2,24 @@ import React, { type ComponentType } from 'react'; import i18n from '@dhis2/d2-i18n'; import type { WithStyles } from 'capture-core-utils/styles'; import { withStyles } from 'capture-core-utils/styles'; -import { spacersNum, spacers, colors } from '@dhis2/ui'; +import { spacersNum, colors } from '@dhis2/ui'; import { LinkedEntityTable } from './LinkedEntityTable.component'; import type { Props } from './linkedEntitiesViewer.types'; const styles = { container: { - padding: `0 ${spacers.dp12} 0 ${spacers.dp12}`, + padding: `0 ${spacersNum.dp12}px`, + }, + section: { + marginBottom: spacersNum.dp16, }, title: { - fontWeight: 500, fontSize: 14, color: colors.grey900, - paddingBottom: spacersNum.dp8, - }, - wrapper: { - paddingBottom: spacersNum.dp16, }, emptyText: { color: colors.grey600, - fontWeight: 400, - fontSize: '14px', - lineHeight: '19px', - margin: 0, - marginBottom: spacersNum.dp8, + fontSize: 14, }, }; @@ -42,14 +36,14 @@ const LinkedEntitiesViewerPlain = ({ className={classes.container} > {!groupedLinkedEntities?.length && ( -

+

{i18n.t('This enrollment doesn\'t have any relationships')} -

+
)} {groupedLinkedEntities?.map((linkedEntityGroup) => { const { id, name, linkedEntities, columns, context } = linkedEntityGroup; return ( -
+
{name}
Date: Mon, 4 May 2026 14:26:15 +0200 Subject: [PATCH 24/60] feat: improve props and styling --- i18n/en.pot | 7 ++---- .../LayoutComponentConfig.ts | 3 +-- .../LinkedEntitiesViewer.component.tsx | 24 ++++++------------- 3 files changed, 10 insertions(+), 24 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 673cd7825b..3d8350ff69 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: 2026-05-04T11:46:25.703Z\n" -"PO-Revision-Date: 2026-05-04T11:46:25.703Z\n" +"POT-Creation-Date: 2026-05-04T12:26:17.283Z\n" +"PO-Revision-Date: 2026-05-04T12:26:17.283Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -1897,9 +1897,6 @@ msgstr "Yes, delete relationship" msgid "An error occurred while deleting the relationship." msgstr "An error occurred while deleting the relationship." -msgid "This enrollment doesn't have any relationships" -msgstr "This enrollment doesn't have any relationships" - msgid "To open this relationship, please wait until saving is complete" msgstr "To open this relationship, please wait until saving is complete" diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts index 8de4787bf0..88669c59fb 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts @@ -189,9 +189,8 @@ export const NewEventWorkspace: WidgetConfig = { export const EnrollmentWidget: WidgetConfig = { Component: WidgetEnrollment, shouldHideWidget: ({ enrollmentId }: any) => enrollmentId === 'AUTO', - getCustomSettings: ({ readOnlyMode }: any, props?: any) => ({ + getCustomSettings: ({ readOnlyMode }: any) => ({ readOnlyMode, - readOnly: props?.readOnly, }), getProps: ({ teiId, diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntitiesViewer.component.tsx b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntitiesViewer.component.tsx index ac55a35fc7..e731b5988d 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntitiesViewer.component.tsx +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntitiesViewer.component.tsx @@ -1,25 +1,22 @@ import React, { type ComponentType } from 'react'; -import i18n from '@dhis2/d2-i18n'; import type { WithStyles } from 'capture-core-utils/styles'; import { withStyles } from 'capture-core-utils/styles'; -import { spacersNum, colors } from '@dhis2/ui'; +import { spacersNum, spacers, colors } from '@dhis2/ui'; import { LinkedEntityTable } from './LinkedEntityTable.component'; import type { Props } from './linkedEntitiesViewer.types'; const styles = { container: { - padding: `0 ${spacersNum.dp12}px`, - }, - section: { - marginBottom: spacersNum.dp16, + padding: `0 ${spacers.dp12} 0 ${spacers.dp12}`, }, title: { + fontWeight: 500, fontSize: 14, color: colors.grey900, + paddingBottom: spacersNum.dp4, }, - emptyText: { - color: colors.grey600, - fontSize: 14, + wrapper: { + paddingBottom: spacersNum.dp16, }, }; @@ -28,22 +25,16 @@ const LinkedEntitiesViewerPlain = ({ groupedLinkedEntities, onLinkedRecordClick, onDeleteRelationship, - readOnly, classes, }: Props & WithStyles) => (
- {!groupedLinkedEntities?.length && ( -
- {i18n.t('This enrollment doesn\'t have any relationships')} -
- )} {groupedLinkedEntities?.map((linkedEntityGroup) => { const { id, name, linkedEntities, columns, context } = linkedEntityGroup; return ( -
+
{name}
); From 39ebf6accad90dee7338e1fe5cf8ea4517853f6f Mon Sep 17 00:00:00 2001 From: henrikmv Date: Tue, 5 May 2026 14:07:33 +0200 Subject: [PATCH 25/60] feat: temp --- i18n/en.pot | 15 +++++++----- .../DataEntry/DataEntry.component.tsx | 1 + .../ReadOnlyBadge/ReadOnlyBadge.tsx | 9 +++++-- .../WidgetEnrollment.component.tsx | 9 +++---- .../DataEntry/DataEntry.component.tsx | 24 +++++++++++++++---- .../WidgetProfile/WidgetProfile.component.tsx | 8 +++---- .../LinkedEntitiesViewer.component.tsx | 2 ++ 7 files changed, 46 insertions(+), 22 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 3d8350ff69..e1e65e6aa6 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: 2026-05-04T12:26:17.283Z\n" -"PO-Revision-Date: 2026-05-04T12:26:17.283Z\n" +"POT-Creation-Date: 2026-05-05T08:55:29.835Z\n" +"PO-Revision-Date: 2026-05-05T08:55:29.835Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -1587,12 +1587,15 @@ msgstr "Indicators" msgid "Save note" msgstr "Save note" -msgid "{{trackedEntityName}} details" -msgstr "{{trackedEntityName}} details" +msgid "{{trackedEntityName}} profile" +msgstr "{{trackedEntityName}} profile" msgid "Edit {{trackedEntityName}}" msgstr "Edit {{trackedEntityName}}" +msgid "Read only - You can only view this {{trackedEntityName}}" +msgstr "Read only - You can only view this {{trackedEntityName}}" + msgid "Change information about this {{trackedEntityName}} here." msgstr "Change information about this {{trackedEntityName}} here." @@ -1641,8 +1644,8 @@ msgstr "There was a problem deleting the {{trackedEntityTypeName}}" msgid "Yes, delete {{trackedEntityTypeName}}" msgstr "Yes, delete {{trackedEntityTypeName}}" -msgid "Show details" -msgstr "Show details" +msgid "Show profile" +msgstr "Show profile" msgid "Profile widget could not be loaded. Please try again later" msgstr "Profile widget could not be loaded. Please try again later" diff --git a/src/core_modules/capture-core/components/DataEntry/DataEntry.component.tsx b/src/core_modules/capture-core/components/DataEntry/DataEntry.component.tsx index f6c9003adc..7e23cda0b4 100644 --- a/src/core_modules/capture-core/components/DataEntry/DataEntry.component.tsx +++ b/src/core_modules/capture-core/components/DataEntry/DataEntry.component.tsx @@ -142,6 +142,7 @@ export type DataEntryOutputProps = { onGetValidationContext?: () => any, orgUnitId?: string, pluginContext?: PluginContext, + viewMode?: boolean, }; type OwnProps = DataEntryOutputProps & { diff --git a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx index 0bc6fdf7fb..5290c85aa8 100644 --- a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx +++ b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx @@ -2,11 +2,16 @@ import React from 'react'; import { colors, IconInfo16, Tag } from '@dhis2/ui'; import i18n from '@dhis2/d2-i18n'; -export const ReadOnlyBadge = ({ readOnly }: { readOnly: boolean }) => { +type Props = { + readOnly: boolean; + label?: string; +}; + +export const ReadOnlyBadge = ({ readOnly, label }: Props) => { if (!readOnly) return null; return ( }> - {i18n.t('Read only - You can only view this enrollment')} + {label ?? i18n.t('Read only - You can only view this enrollment')} ); }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx index 9d705869dd..4859fef1a9 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx @@ -57,8 +57,6 @@ const WidgetEnrollmentPlain = ({ initError, loading, canAddNew, - readOnlyMode, - readOnly, programDataWriteAccess, displayAutoGeneratedEventWarning, onDelete, @@ -73,7 +71,6 @@ const WidgetEnrollmentPlain = ({ onAccessLostFromTransfer, }: PlainProps & WithStyles) => { const [open, setOpenStatus] = useState(true); - const isReadOnly = readOnlyMode || Boolean(readOnly); const { fromServerDate } = useTimeZoneConversion(); const updatedAtDateTime: string = convertValue( fromServerDate(enrollment?.updatedAt).toISOString(), dataElementTypes.DATETIME, @@ -115,7 +112,7 @@ const WidgetEnrollmentPlain = ({ date={enrollment.enrolledAt} dateLabel={getEnrollmentDateLabel(program)} locale={locale} - readOnly={isReadOnly} + readOnly={!programDataWriteAccess} displayAutoGeneratedEventWarning={displayAutoGeneratedEventWarning} onSave={updateEnrollmentDate} allowFutureDate={program.selectEnrollmentDatesInFuture} @@ -128,7 +125,7 @@ const WidgetEnrollmentPlain = ({ date={enrollment.occurredAt} dateLabel={getIncidentDateLabel(program)} locale={locale} - readOnly={isReadOnly} + readOnly={!programDataWriteAccess} displayAutoGeneratedEventWarning={displayAutoGeneratedEventWarning} onSave={updateIncidentDate} allowFutureDate={program.selectIncidentDatesInFuture} @@ -175,7 +172,7 @@ const WidgetEnrollmentPlain = ({ refetchEnrollment={refetchEnrollment} refetchTEI={refetchTEI} onError={onError} - readOnly={isReadOnly} + readOnly={!programDataWriteAccess} />
)} diff --git a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.component.tsx b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.component.tsx index b6258cb038..785622343f 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.component.tsx +++ b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.component.tsx @@ -4,6 +4,7 @@ import i18n from '@dhis2/d2-i18n'; import { NoticeBoxes } from './NoticeBoxes.container'; import type { PlainProps } from './dataEntry.types'; import { DataEntry } from '../../DataEntry'; +import { ReadOnlyBadge } from '../../ReadOnlyBadge'; import { TEI_MODAL_STATE } from './dataEntry.actions'; export const DataEntryComponent = ({ @@ -25,10 +26,24 @@ export const DataEntryComponent = ({ }: PlainProps) => ( - {readOnly - ? i18n.t('{{trackedEntityName}} details', { trackedEntityName, interpolation: { escapeValue: false } }) - : i18n.t('Edit {{trackedEntityName}}', { trackedEntityName, interpolation: { escapeValue: false } }) - } +
+ + {readOnly + ? i18n.t( + '{{trackedEntityName}} profile', + { trackedEntityName, interpolation: { escapeValue: false } }, + ) + : i18n.t('Edit {{trackedEntityName}}', { trackedEntityName, interpolation: { escapeValue: false } }) + } + + +
{!readOnly && ( @@ -50,6 +65,7 @@ export const DataEntryComponent = ({ onGetValidationContext={onGetValidationContext} orgUnitId={orgUnitId} pluginContext={pluginContext} + viewMode={readOnly} /> {!readOnly && ( { - if (hasNoAttributes) return null; - if (readOnlyMode) return i18n.t('Show details'); + if (readOnlyMode) return null; + if (!isEditable) return i18n.t('Show profile'); if (isEditable) return i18n.t('Edit'); return null; - }, [hasNoAttributes, isEditable, readOnlyMode]); + }, [isEditable, readOnlyMode]); const loading = computeLoadingState(programsLoading, trackedEntityInstancesLoading, userRolesLoading, configIsFetched); const error = computeError(programsError, trackedEntityInstancesError, userRolesError); @@ -262,7 +262,7 @@ const WidgetProfilePlain = ({ modalState={modalState} geometry={geometry} trackedEntityName={trackedEntityTypeName} - readOnly={readOnlyMode} + readOnly={!isEditable} /> diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntitiesViewer.component.tsx b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntitiesViewer.component.tsx index e731b5988d..d4a89d60cc 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntitiesViewer.component.tsx +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntitiesViewer.component.tsx @@ -25,6 +25,7 @@ const LinkedEntitiesViewerPlain = ({ groupedLinkedEntities, onLinkedRecordClick, onDeleteRelationship, + readOnly, classes, }: Props & WithStyles) => (
); From 71c541c905bb693fb6e4fc0fb18664af30958593 Mon Sep 17 00:00:00 2001 From: henrikmv Date: Tue, 5 May 2026 17:31:15 +0200 Subject: [PATCH 26/60] feat: updates access --- .../EnrollmentPageDefault.container.tsx | 8 +- .../EnrollmentPageDefault.types.ts | 3 +- .../EnrollmentEditEventPage.component.tsx | 6 +- .../EnrollmentEditEventPage.container.tsx | 8 +- .../EnrollmentEditEventPage.types.ts | 3 +- .../EnrollmentPageLayout.tsx | 11 ++- .../LayoutComponentConfig.ts | 25 +++---- .../WidgetEnrollment.container.tsx | 2 - .../WidgetEnrollment/enrollment.types.ts | 2 - .../WidgetEnrollmentNote.component.tsx | 2 +- .../WidgetEventEdit.container.tsx | 4 +- .../WidgetHeader/WidgetHeader.container.tsx | 43 +++++------ .../WidgetHeader/WidgetHeader.types.ts | 1 - .../WidgetEventEdit/widgetEventEdit.types.ts | 1 - .../WidgetEventNote/WidgetEventNote.types.ts | 2 +- .../WidgetEventSchedule.component.tsx | 1 - .../NoteSection/NoteSection.types.ts | 2 +- .../components/WidgetNote/WidgetNote.types.ts | 2 +- .../Stages/Stage/Stage.component.tsx | 25 +++---- .../Stage/StageDetail/EventRow/EventRow.tsx | 75 +++++++++---------- .../StageDetail/EventRow/EventRow.types.ts | 1 - .../StageDetail/StageDetail.component.tsx | 29 +++---- .../Stage/StageDetail/stageDetail.types.ts | 1 - .../Stages/Stage/stage.types.ts | 1 - .../stagesAndEvents.types.ts | 1 - 25 files changed, 118 insertions(+), 141 deletions(-) diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.tsx b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.tsx index f56f32f6fd..1de960d8ee 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.tsx +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.tsx @@ -180,9 +180,8 @@ export const EnrollmentPageDefault = () => { navigate(`/?${buildUrlQueryString({ orgUnitId, programId })}`); }, [navigate, orgUnitId, programId]); - const hasProgramWrite = Boolean(program?.access?.data?.write); - const hasTETWrite = Boolean((program as any)?.trackedEntityType?.access?.data?.write); - const readOnly = !hasProgramWrite || !hasTETWrite; + const programWriteAccess = Boolean(program?.access?.data?.write); + const trackedEntityTypeWriteAccess = Boolean((program as any)?.trackedEntityType?.access?.data?.write); if (isLoading) { return ( @@ -199,7 +198,8 @@ export const EnrollmentPageDefault = () => { pageLayout={pageLayout} currentPage={EnrollmentPageKeys.OVERVIEW} availableWidgets={WidgetsForEnrollmentPageDefault} - readOnly={readOnly} + programWriteAccess={programWriteAccess} + trackedEntityTypeWriteAccess={trackedEntityTypeWriteAccess} teiId={teiId} orgUnitId={orgUnitId} program={program} diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.ts b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.ts index 2711a194c5..0bf3dc65a9 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.ts +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.ts @@ -49,7 +49,8 @@ export type Props = { pageLayout: PageLayoutConfig; availableWidgets: Readonly<{ [key: string]: WidgetConfig }>; onDeleteTrackedEntitySuccess: () => void; - readOnly: boolean; + programWriteAccess: boolean; + trackedEntityTypeWriteAccess: boolean; }; diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.tsx b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.tsx index c3d468c24e..0e80a977ca 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.tsx +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.tsx @@ -62,7 +62,8 @@ export const EnrollmentEditEventPageComponent = ({ onUpdateEnrollmentEventsSuccess, onUpdateEnrollmentEventsError, userInteractionInProgress, - readOnly, + programWriteAccess, + trackedEntityTypeWriteAccess, }: PlainProps) => ( diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.tsx b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.tsx index 2e33b8f98d..8de4a6b0fc 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.tsx +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.tsx @@ -295,9 +295,8 @@ const EnrollmentEditEventPageWithContextPlain = ({ return ; } - const hasProgramWrite = Boolean((program as any)?.access?.data?.write); - const hasTETWrite = Boolean((program as any)?.trackedEntityType?.access?.data?.write); - const readOnly = !hasProgramWrite || !hasTETWrite; + const programWriteAccess = Boolean((program as any)?.access?.data?.write); + const trackedEntityTypeWriteAccess = Boolean((program as any)?.trackedEntityType?.access?.data?.write); return ( ); }; diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.ts b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.ts index 4f84f9c8f2..73b22eebdb 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.ts +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.ts @@ -60,7 +60,8 @@ export type PlainProps = { onUpdateOrAddEnrollmentEvents: (events: Array) => void; onUpdateEnrollmentEventsSuccess: (events: Array) => void; onUpdateEnrollmentEventsError: (events: Array) => void; - readOnly: boolean; + programWriteAccess: boolean; + trackedEntityTypeWriteAccess: boolean; }; export type Props = { diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx index 97608185bd..9d2293c8f6 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx @@ -78,7 +78,8 @@ const EnrollmentPageLayoutPlain = ({ onBackToMainPage, onBackToDashboard, onBackToViewEvent, - readOnly, + programWriteAccess, + trackedEntityTypeWriteAccess, classes, ...passOnProps }: Props) => { @@ -94,14 +95,16 @@ const EnrollmentPageLayoutPlain = ({ eventStatus, toggleVisibility, addRelationShipContainerElement, - readOnly, + programWriteAccess, + trackedEntityTypeWriteAccess, }), [ addRelationShipContainerElement, currentPage, eventStatus, passOnProps, program, - readOnly, + programWriteAccess, + trackedEntityTypeWriteAccess, toggleVisibility, ]); @@ -141,7 +144,7 @@ const EnrollmentPageLayoutPlain = ({ userInteractionInProgress={userInteractionInProgress} eventStatus={eventStatus} /> - +
{pageLayout.leftColumn && !!leftColumnWidgets?.length && ( diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts index 88669c59fb..8d16281430 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts @@ -39,7 +39,8 @@ import { export const QuickActions: WidgetConfig = { Component: EnrollmentQuickActions, - shouldHideWidget: ({ readOnly }: any) => Boolean(readOnly), + shouldHideWidget: ({ stages }: any) => + !stages?.some((stage: any) => stage?.dataAccess?.write), getProps: ({ stages, events, ruleEffects }: any) => ({ stages, events, @@ -60,7 +61,6 @@ export const StagesAndEvents: WidgetConfig = { onRollbackDeleteEvent, onEventClick, ruleEffects, - readOnly, }: any): StagesAndEventProps => ({ programId: program.id, stages, @@ -72,7 +72,6 @@ export const StagesAndEvents: WidgetConfig = { onRollbackDeleteEvent, onEventClick, ruleEffects, - readOnly, }), }; @@ -86,7 +85,7 @@ export const TrackedEntityRelationship: WidgetConfig = { toggleVisibility, teiId, onLinkedRecordClick, - readOnly, + trackedEntityTypeWriteAccess, }: any): TrackedEntityRelationshipProps => ({ trackedEntityTypeId: program.trackedEntityType.id, programId: program.id, @@ -96,7 +95,7 @@ export const TrackedEntityRelationship: WidgetConfig = { onCloseAddRelationship: toggleVisibility, teiId, onLinkedRecordClick, - readOnly, + readOnly: !trackedEntityTypeWriteAccess, }), }; @@ -134,9 +133,7 @@ export const IndicatorWidget: WidgetConfig = { export const EnrollmentNote: WidgetConfig = { Component: WidgetEnrollmentNote, - getProps: ({ readOnly }: any) => ({ - readOnly, - }), + getProps: () => ({}), }; export const ProfileWidget: WidgetConfig = { @@ -244,7 +241,6 @@ export const EditEventWorkspace: WidgetConfig = { onSaveAndCompleteEnrollmentSuccessActionType, onDeleteEvent, onDeleteEventRelationship, - readOnly, }: any): WidgetEventEditProps => ({ programId: program.id, stageId, @@ -263,7 +259,6 @@ export const EditEventWorkspace: WidgetConfig = { onSaveAndCompleteEnrollmentSuccessActionType, onDeleteEvent, onDeleteEventRelationship, - readOnly, }), }; @@ -287,12 +282,11 @@ export const AssigneeWidget: WidgetConfig = { eventAccess, onSaveAssignee, onSaveAssigneeError, - readOnly, }: any) => ({ enabled: programStage?.enableUserAssignment || false, assignee, getSaveContext: getAssignedUserSaveContext, - writeAccess: (eventAccess?.write || false) && !readOnly, + writeAccess: eventAccess?.write || false, onSave: onSaveAssignee, onSaveError: onSaveAssigneeError, }), @@ -300,17 +294,16 @@ export const AssigneeWidget: WidgetConfig = { export const EventNote: WidgetConfig = { Component: WidgetEventNote, - getProps: ({ dataEntryKey, dataEntryId, readOnly }: any) => ({ + getProps: ({ dataEntryKey, dataEntryId }: any) => ({ dataEntryKey, dataEntryId, - readOnly, }), }; export const RelatedStagesWorkspace: WidgetConfig = { Component: WidgetRelatedStages, - shouldHideWidget: ({ currentPage, readOnly }: any) => - currentPage === EnrollmentPageKeys.EDIT_EVENT || Boolean(readOnly), + shouldHideWidget: ({ currentPage }: any) => + currentPage === EnrollmentPageKeys.EDIT_EVENT, getProps: ({ program, stageId, diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.container.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.container.tsx index b64e48c4ca..c8acf44885 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.container.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.container.tsx @@ -39,7 +39,6 @@ export const WidgetEnrollment = ({ enrollmentId, programId, readOnlyMode = false, - readOnly, onDelete, onAddNew, onUpdateEnrollmentDate, @@ -90,7 +89,6 @@ export const WidgetEnrollment = ({ events={events} canAddNew={canAddNew} readOnlyMode={readOnlyMode} - readOnly={readOnly} programDataWriteAccess={program?.access.data.write} displayAutoGeneratedEventWarning={containsAutoGeneratedEvent} program={program} diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/enrollment.types.ts b/src/core_modules/capture-core/components/WidgetEnrollment/enrollment.types.ts index b31b5f3fd5..21ed405b80 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/enrollment.types.ts +++ b/src/core_modules/capture-core/components/WidgetEnrollment/enrollment.types.ts @@ -11,7 +11,6 @@ export type Props = { enrollmentId: string; programId: string; readOnlyMode?: boolean; - readOnly?: boolean; externalData: { status: { value: string | null }; events?: Array> | null }; onDelete: () => void; onAddNew: () => void; @@ -55,7 +54,6 @@ export type PlainProps = { loading: boolean; canAddNew: boolean; readOnlyMode: boolean; - readOnly: boolean; programDataWriteAccess: boolean; displayAutoGeneratedEventWarning: boolean; updateEnrollmentDate: (enrollmentDate: string) => void; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx index 61b031cc5d..806b9e22f1 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx @@ -5,7 +5,7 @@ import { requestAddNoteForEnrollment } from './WidgetEnrollmentNote.actions'; import { WidgetNote } from '../WidgetNote'; import { useLocationQuery } from '../../utils/routing'; -export const WidgetEnrollmentNote = ({ readOnly }: { readOnly: boolean }) => { +export const WidgetEnrollmentNote = ({ readOnly }: { readOnly?: boolean }) => { const dispatch = useDispatch(); const { enrollmentId } = useLocationQuery(); const notes = useSelector(({ enrollmentDomain }: { enrollmentDomain?: { enrollment?: { notes?: Array } } }) => diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/WidgetEventEdit.container.tsx b/src/core_modules/capture-core/components/WidgetEventEdit/WidgetEventEdit.container.tsx index f7803e3b5e..a4bb9b7194 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/WidgetEventEdit.container.tsx +++ b/src/core_modules/capture-core/components/WidgetEventEdit/WidgetEventEdit.container.tsx @@ -90,7 +90,6 @@ const WidgetEventEditPlain = ({ onSaveAndCompleteEnrollmentErrorActionType, onDeleteEvent, onDeleteEventRelationship, - readOnly, classes, }: Props) => { useEffect(() => inMemoryFileStore.clear, []); @@ -130,14 +129,13 @@ const WidgetEventEditPlain = ({ orgUnit={orgUnit} setChangeLogIsOpen={setChangeLogIsOpen} occurredAt={occurredAt} - readOnly={readOnly} /> } noncollapsible borderless >
- {currentPageMode === dataEntryKeys.VIEW || readOnly ? ( + {currentPageMode === dataEntryKeys.VIEW ? (
{ useEffect(() => inMemoryFileStore.clear, []); const dispatch = useDispatch(); @@ -55,6 +54,7 @@ const WidgetHeaderPlain = ({ const { currentPageMode } = useEnrollmentEditEventPageMode(eventStatus); const [actionsIsOpen, setActionsIsOpen] = useState(false); + const eventAccess = getProgramEventAccess(programId, stage.id); const { hasAuthority } = useAuthorities({ authorities: ['F_UNCOMPLETE_EVENT'] }); const blockEntryForm = stage.blockEntryForm && !hasAuthority && eventStatus === eventStatuses.COMPLETED; @@ -62,11 +62,14 @@ const WidgetHeaderPlain = ({ const occurredAtClient = convertFormToClient(occurredAt, dataElementTypes.DATE) as string; const { isWithinValidPeriod } = isValidPeriod(occurredAtClient, expiryPeriod); - const disableEdit = blockEntryForm || !isWithinValidPeriod; + const disableEdit = !eventAccess?.write || blockEntryForm || !isWithinValidPeriod; const tooltipContent = useMemo(() => { if (blockEntryForm) { return i18n.t('The event cannot be edited after it has been completed'); } + if (!eventAccess?.write) { + return i18n.t('You don\'t have access to edit this event'); + } if (!isWithinValidPeriod) { return i18n.t('{{occurredAt}} belongs to an expired period. Event cannot be edited', { occurredAt, @@ -74,7 +77,7 @@ const WidgetHeaderPlain = ({ }); } return ''; - }, [blockEntryForm, isWithinValidPeriod, occurredAt]); + }, [blockEntryForm, eventAccess?.write, isWithinValidPeriod, occurredAt]); const { programCategory } = useCategoryCombinations(programId); @@ -97,24 +100,22 @@ const WidgetHeaderPlain = ({
{currentPageMode === dataEntryKeys.VIEW && (
- {!readOnly && ( - + - - )} + {i18n.t('Edit event')} + + {supportsChangelog && ( void, occurredAt: string, - readOnly: boolean, }; diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/widgetEventEdit.types.ts b/src/core_modules/capture-core/components/WidgetEventEdit/widgetEventEdit.types.ts index d3131d95ab..672624ce43 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/widgetEventEdit.types.ts +++ b/src/core_modules/capture-core/components/WidgetEventEdit/widgetEventEdit.types.ts @@ -20,7 +20,6 @@ export type Props = { onSaveAndCompleteEnrollmentErrorActionType?: string, onDeleteEvent?: (eventId: string) => void, onDeleteEventRelationship?: (relationshipId: string) => void, - readOnly: boolean, }; export type ComponentProps = Props & { diff --git a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.types.ts b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.types.ts index aaf72c757e..4abf251106 100644 --- a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.types.ts +++ b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.types.ts @@ -1,7 +1,7 @@ export type Props = { dataEntryKey: string; dataEntryId: string; - readOnly: boolean; + readOnly?: boolean; }; export type ClientNote = { diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.component.tsx b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.component.tsx index ba6c729a7c..38efaab9b0 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.component.tsx @@ -126,7 +126,6 @@ const WidgetEventSchedulePlain = ({ > void; placeholder: string; emptyNoteMessage: string; - readOnly: boolean; + readOnly?: boolean; }; diff --git a/src/core_modules/capture-core/components/WidgetNote/WidgetNote.types.ts b/src/core_modules/capture-core/components/WidgetNote/WidgetNote.types.ts index df20863b82..d7b83b6fd5 100644 --- a/src/core_modules/capture-core/components/WidgetNote/WidgetNote.types.ts +++ b/src/core_modules/capture-core/components/WidgetNote/WidgetNote.types.ts @@ -11,5 +11,5 @@ export type Props = { }; }>; onAddNote: (note: string) => void; - readOnly: boolean; + readOnly?: boolean; }; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/Stage.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/Stage.component.tsx index 4a57220444..cea6d7ec4d 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/Stage.component.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/Stage.component.tsx @@ -26,7 +26,7 @@ const rulesEffectHideProgramStage = (ruleEffects: Array<{id: string, type: strin ); export const StagePlain = ({ - stage, events, classes, onCreateNew, ruleEffects, readOnly, ...passOnProps + stage, events, classes, onCreateNew, ruleEffects, ...passOnProps }: Props & WithStyles) => { const [open, setOpenStatus] = useState(true); const { id, name, icon, description, dataElements, hideDueDate, repeatable, enableUserAssignment } = stage; @@ -64,21 +64,18 @@ export const StagePlain = ({ enableUserAssignment={enableUserAssignment} onCreateNew={onCreateNew} hiddenProgramStage={preventAddingNewEvents} - readOnly={readOnly} {...passOnProps} /> : ( - !readOnly && ( -
- onCreateNew(id)} - stageWriteAccess={stage.dataAccess.write} - eventCount={events.length} - repeatable={repeatable} - preventAddingEventActionInEffect={preventAddingNewEvents} - eventName={name} - /> -
- ) +
+ onCreateNew(id)} + stageWriteAccess={stage.dataAccess.write} + eventCount={events.length} + repeatable={repeatable} + preventAddingEventActionInEffect={preventAddingNewEvents} + eventName={name} + /> +
)}
diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/EventRow.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/EventRow.tsx index 15013a26d9..1945d52b68 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/EventRow.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/EventRow.tsx @@ -46,7 +46,6 @@ const EventRowPlain = ({ teiId, programId, enrollmentId, - readOnly, classes, }: EventRowProps & WithStyles) => { const [actionsOpen, setActionsOpen] = useState(false); @@ -63,46 +62,44 @@ const EventRowPlain = ({ <> - {!readOnly && ( - - setActionsOpen(prev => !prev)} - dataTest={'overflow-button'} - secondary - small - icon={} - disabled={pendingApiResponse || !stageWriteAccess} - component={( - - {(eventDetails.status === EventStatuses.SCHEDULE || - eventDetails.status === EventStatuses.SKIPPED) && ( - - )} - - + setActionsOpen(prev => !prev)} + dataTest={'overflow-button'} + secondary + small + icon={} + disabled={pendingApiResponse || !stageWriteAccess} + component={( + + {(eventDetails.status === EventStatuses.SCHEDULE || + eventDetails.status === EventStatuses.SKIPPED) && ( + - - )} - /> - - )} + )} + + + + )} + /> + {deleteModalOpen && ( ) => { onViewAll, onCreateNew, hiddenProgramStage, - readOnly, classes, } = props; const defaultSortState = { @@ -228,7 +227,6 @@ const StageDetailPlain = (props: Props & WithStyles) => { onDeleteEvent={onDeleteEvent} onRollbackDeleteEvent={onRollbackDeleteEvent} onUpdateEventStatus={onUpdateEventStatus} - readOnly={readOnly} /> ); }); @@ -256,21 +254,18 @@ const StageDetailPlain = (props: Props & WithStyles) => { onClick={handleViewAll} >{i18n.t('Go to full {{ eventName }}', { eventName, interpolation: { escapeValue: false } })} : null); - const renderCreateNewButton = () => { - if (readOnly) return null; - return ( -
- -
- ); - }; + const renderCreateNewButton = () => ( +
+ +
+ ); return (
diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/stageDetail.types.ts b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/stageDetail.types.ts index 0017617b81..aa93f0fbcb 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/stageDetail.types.ts +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/stageDetail.types.ts @@ -14,7 +14,6 @@ type ExtractedProps = { onUpdateEventStatus: (eventId: string, status: string) => void; onRollbackDeleteEvent: (event: ApiEnrollmentEvent) => void; hiddenProgramStage?: boolean; - readOnly?: boolean; }; export type Props = ExtractedProps & StageCommonProps; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/stage.types.ts b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/stage.types.ts index c8e2cc25cd..f1f0b30787 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/stage.types.ts +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/stage.types.ts @@ -9,7 +9,6 @@ type ExtractedProps = { onDeleteEvent: (eventId: string) => void; onUpdateEventStatus: (eventId: string, status: string) => void; onRollbackDeleteEvent: (eventId: ApiEnrollmentEvent) => void; - readOnly?: boolean; }; export type Props = ExtractedProps & StageCommonProps; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/stagesAndEvents.types.ts b/src/core_modules/capture-core/components/WidgetStagesAndEvents/stagesAndEvents.types.ts index 93d8a6ec68..ac9dc3e5c2 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/stagesAndEvents.types.ts +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/stagesAndEvents.types.ts @@ -9,7 +9,6 @@ type ExtractedProps = { onUpdateEventStatus: (eventId: string, status: string) => void; onRollbackDeleteEvent: (eventId: ApiEnrollmentEvent) => void; className?: string; - readOnly?: boolean; }; export type Props = ExtractedProps & StageCommonProps; From 9e468b348eaa7fd9e5b2437e26aa2bdc2bc6f51f Mon Sep 17 00:00:00 2001 From: henrikmv Date: Tue, 5 May 2026 19:33:14 +0200 Subject: [PATCH 27/60] feat: update labels --- i18n/en.pot | 20 +++- .../EnrollmentPageDefault.container.tsx | 10 +- .../EnrollmentPageDefault.types.ts | 1 + .../EnrollmentEditEventPage.component.tsx | 2 + .../EnrollmentEditEventPage.container.tsx | 12 ++- .../EnrollmentEditEventPage.types.ts | 1 + .../EnrollmentPageLayout.tsx | 10 +- .../ReadOnlyBadge/ReadOnlyBadge.tsx | 91 ++++++++++++++++++- .../Actions/Actions.container.tsx | 9 +- .../WidgetEnrollment.container.tsx | 19 +++- .../DataEntry/DataEntry.component.tsx | 2 +- .../OverflowMenu/OverflowMenu.container.tsx | 8 +- .../WidgetProfile/WidgetProfile.component.tsx | 12 +++ src/core_modules/capture-core/hooks/index.ts | 1 + .../capture-core/hooks/useEnrollmentAccess.ts | 45 +++++++++ 15 files changed, 223 insertions(+), 20 deletions(-) create mode 100644 src/core_modules/capture-core/hooks/useEnrollmentAccess.ts diff --git a/i18n/en.pot b/i18n/en.pot index e1e65e6aa6..68bbc4ccaf 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: 2026-05-05T08:55:29.835Z\n" -"PO-Revision-Date: 2026-05-05T08:55:29.835Z\n" +"POT-Creation-Date: 2026-05-05T17:26:45.666Z\n" +"PO-Revision-Date: 2026-05-05T17:26:45.666Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -1071,6 +1071,18 @@ msgstr "An error occurred loading possible duplicates" msgid "Read only - You can only view this enrollment" msgstr "Read only - You can only view this enrollment" +msgid "Read only - Cannot edit enrollment or events" +msgstr "Read only - Cannot edit enrollment or events" + +msgid "Read only - Cannot edit enrollment" +msgstr "Read only - Cannot edit enrollment" + +msgid "Read only - Cannot edit events" +msgstr "Read only - Cannot edit events" + +msgid "person" +msgstr "person" + msgid "You don't have access to delete this relationship" msgstr "You don't have access to delete this relationship" @@ -1593,8 +1605,8 @@ msgstr "{{trackedEntityName}} profile" msgid "Edit {{trackedEntityName}}" msgstr "Edit {{trackedEntityName}}" -msgid "Read only - You can only view this {{trackedEntityName}}" -msgstr "Read only - You can only view this {{trackedEntityName}}" +msgid "Read only - {{trackedEntityName}} profile" +msgstr "Read only - {{trackedEntityName}} profile" msgid "Change information about this {{trackedEntityName}} here." msgstr "Change information about this {{trackedEntityName}} here." diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.tsx b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.tsx index 1de960d8ee..3c88fb9366 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.tsx +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.tsx @@ -42,7 +42,7 @@ import { deleteEnrollmentEvent, updateEnrollmentEventStatus, } from '../../common/EnrollmentOverviewDomain/enrollment.actions'; -import { useHideWidgetByRuleLocations } from '../../../../hooks'; +import { useHideWidgetByRuleLocations, useEnrollmentAccess } from '../../../../hooks'; export const EnrollmentPageDefault = () => { @@ -180,8 +180,11 @@ export const EnrollmentPageDefault = () => { navigate(`/?${buildUrlQueryString({ orgUnitId, programId })}`); }, [navigate, orgUnitId, programId]); - const programWriteAccess = Boolean(program?.access?.data?.write); - const trackedEntityTypeWriteAccess = Boolean((program as any)?.trackedEntityType?.access?.data?.write); + const { + programWriteAccess, + trackedEntityTypeWriteAccess, + programStageWriteAccess, + } = useEnrollmentAccess(programId); if (isLoading) { return ( @@ -200,6 +203,7 @@ export const EnrollmentPageDefault = () => { availableWidgets={WidgetsForEnrollmentPageDefault} programWriteAccess={programWriteAccess} trackedEntityTypeWriteAccess={trackedEntityTypeWriteAccess} + programStageWriteAccess={programStageWriteAccess} teiId={teiId} orgUnitId={orgUnitId} program={program} diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.ts b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.ts index 0bf3dc65a9..a55c138ca3 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.ts +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.ts @@ -51,6 +51,7 @@ export type Props = { onDeleteTrackedEntitySuccess: () => void; programWriteAccess: boolean; trackedEntityTypeWriteAccess: boolean; + programStageWriteAccess: boolean; }; diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.tsx b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.tsx index 0e80a977ca..ba25df16fe 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.tsx +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.tsx @@ -64,6 +64,7 @@ export const EnrollmentEditEventPageComponent = ({ userInteractionInProgress, programWriteAccess, trackedEntityTypeWriteAccess, + programStageWriteAccess, }: PlainProps) => ( diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.tsx b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.tsx index 8de4a6b0fc..2380200bdd 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.tsx +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.tsx @@ -3,7 +3,7 @@ import type { ProgramRule } from '@dhis2/rules-engine-javascript'; import { useQueryClient } from '@tanstack/react-query'; import { useDispatch, useSelector } from 'react-redux'; import { dataEntryIds } from 'capture-core/constants'; -import { useEnrollmentEditEventPageMode, useHideWidgetByRuleLocations } from '../../../hooks'; +import { useEnrollmentAccess, useEnrollmentEditEventPageMode, useHideWidgetByRuleLocations } from '../../../hooks'; import type { ReduxState } from '../../App/withAppUrlSync.types'; import { commitEnrollmentAndEvents, @@ -291,13 +291,16 @@ const EnrollmentEditEventPageWithContextPlain = ({ dispatch(rollbackAssignee(assignedUser, prevAssignee, eventId)); }; + const { + programWriteAccess, + trackedEntityTypeWriteAccess, + programStageWriteAccess, + } = useEnrollmentAccess(programId); + if (pageStatus === pageStatuses.LOADING) { return ; } - const programWriteAccess = Boolean((program as any)?.access?.data?.write); - const trackedEntityTypeWriteAccess = Boolean((program as any)?.trackedEntityType?.access?.data?.write); - return ( ); }; diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.ts b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.ts index 73b22eebdb..743f8daaa4 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.ts +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.ts @@ -62,6 +62,7 @@ export type PlainProps = { onUpdateEnrollmentEventsError: (events: Array) => void; programWriteAccess: boolean; trackedEntityTypeWriteAccess: boolean; + programStageWriteAccess: boolean; }; export type Props = { diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx index 9d2293c8f6..b0d7ce43bd 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx @@ -80,6 +80,7 @@ const EnrollmentPageLayoutPlain = ({ onBackToViewEvent, programWriteAccess, trackedEntityTypeWriteAccess, + programStageWriteAccess, classes, ...passOnProps }: Props) => { @@ -97,6 +98,7 @@ const EnrollmentPageLayoutPlain = ({ addRelationShipContainerElement, programWriteAccess, trackedEntityTypeWriteAccess, + programStageWriteAccess, }), [ addRelationShipContainerElement, currentPage, @@ -105,6 +107,7 @@ const EnrollmentPageLayoutPlain = ({ program, programWriteAccess, trackedEntityTypeWriteAccess, + programStageWriteAccess, toggleVisibility, ]); @@ -144,7 +147,12 @@ const EnrollmentPageLayoutPlain = ({ userInteractionInProgress={userInteractionInProgress} eventStatus={eventStatus} /> - +
{pageLayout.leftColumn && !!leftColumnWidgets?.length && ( diff --git a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx index 5290c85aa8..c4ed02d3e6 100644 --- a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx +++ b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx @@ -3,15 +3,98 @@ import { colors, IconInfo16, Tag } from '@dhis2/ui'; import i18n from '@dhis2/d2-i18n'; type Props = { - readOnly: boolean; + readOnly?: boolean; + programWriteAccess?: boolean; + trackedEntityTypeWriteAccess?: boolean; + programStageWriteAccess?: boolean; + trackedEntityName?: string; label?: string; }; -export const ReadOnlyBadge = ({ readOnly, label }: Props) => { - if (!readOnly) return null; +const interpolate = (key: string, trackedEntityName: string) => + i18n.t(key, { trackedEntityName, interpolation: { escapeValue: false } }); + +const getMultiMissingLabel = ( + missingProgram: boolean, + missingTET: boolean, + missingStage: boolean, + trackedEntityName: string, +): string | null => { + if (missingProgram && missingTET && missingStage) { + return i18n.t('Read only - You can only view this enrollment'); + } + if (missingProgram && missingTET) { + return interpolate( + 'Read only - Cannot edit enrollment, {{trackedEntityName}} profile, or relationships', + trackedEntityName, + ); + } + if (missingProgram && missingStage) { + return i18n.t('Read only - Cannot edit enrollment or events'); + } + if (missingTET && missingStage) { + return interpolate( + 'Read only - Cannot edit {{trackedEntityName}} profile, relationships, or events', + trackedEntityName, + ); + } + return null; +}; + +const getSingleMissingLabel = ( + missingProgram: boolean, + missingTET: boolean, + missingStage: boolean, + trackedEntityName: string, +): string | null => { + if (missingProgram) return i18n.t('Read only - Cannot edit enrollment'); + if (missingTET) { + return interpolate( + 'Read only - Cannot edit {{trackedEntityName}} profile or relationships', + trackedEntityName, + ); + } + if (missingStage) return i18n.t('Read only - Cannot edit events'); + return null; +}; + +const getDefaultLabel = ( + programWriteAccess: boolean, + trackedEntityTypeWriteAccess: boolean, + programStageWriteAccess: boolean, + trackedEntityName: string, +): string | null => { + const mp = !programWriteAccess; + const mt = !trackedEntityTypeWriteAccess; + const ms = !programStageWriteAccess; + return getMultiMissingLabel(mp, mt, ms, trackedEntityName) + ?? getSingleMissingLabel(mp, mt, ms, trackedEntityName); +}; + +export const ReadOnlyBadge = ({ + readOnly, + programWriteAccess = true, + trackedEntityTypeWriteAccess = true, + programStageWriteAccess = true, + trackedEntityName, + label, +}: Props) => { + const isReadOnly = readOnly + || !programWriteAccess + || !trackedEntityTypeWriteAccess + || !programStageWriteAccess; + if (!isReadOnly) return null; + const text = label + ?? getDefaultLabel( + programWriteAccess, + trackedEntityTypeWriteAccess, + programStageWriteAccess, + trackedEntityName ?? i18n.t('person'), + ) + ?? i18n.t('Read only - You can only view this enrollment'); return ( }> - {label ?? i18n.t('Read only - You can only view this enrollment')} + {text} ); }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.container.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.container.tsx index 7b2b73b0ab..9360930cb5 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.container.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.container.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { ActionsComponent } from './Actions.component'; import type { Props } from './actions.types'; import { useUpdateEnrollment, useDeleteEnrollment } from '../dataMutation/dataMutation'; @@ -21,6 +21,13 @@ export const Actions = ({ const { updateMutation, updateLoading } = useUpdateEnrollment(refetchEnrollment, refetchTEI, onError, onSuccess); const { deleteMutation, deleteLoading } = useDeleteEnrollment(onDelete, onError, onSuccess); const { hasAuthority } = useAuthorities({ authorities: ['F_ENROLLMENT_CASCADE_DELETE'] }); + + useEffect(() => { + const yn = (v?: boolean) => (v ? 'Yes' : 'No'); + // eslint-disable-next-line no-console + console.log(`Cascade delete enrollment: ${yn(hasAuthority)}`); + }, [hasAuthority]); + const { updateEnrollmentOwnership, isTransferLoading } = useUpdateOwnership({ teiId: enrollment.trackedEntity, programId: enrollment.program, diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.container.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.container.tsx index c8acf44885..76b14cd9ca 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.container.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.container.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React, { useMemo, useEffect } from 'react'; import { errorCreator } from 'capture-core-utils'; import log from 'loglevel'; import { WidgetEnrollment as WidgetEnrollmentNote } from './WidgetEnrollment.component'; @@ -79,6 +79,23 @@ export const WidgetEnrollment = ({ const error = useError(errorEnrollment, errorProgram, errorOwnerOrgUnit, errorOrgUnit, errorLocale); const events = useEnrollmentEvents(externalData); + useEffect(() => { + if (!program) return; + const a = program?.access; + const t = program?.trackedEntityType?.access; + const yn = (v?: boolean) => (v ? 'Yes' : 'No'); + const stagesLine = (program?.programStages ?? []) + .map(({ displayName: stageName, id, access }: any) => + `Program stage "${stageName ?? id}": write=${yn(access?.data?.write)}`) + .join('\n'); + // eslint-disable-next-line no-console + console.log( + `${`Program: write=${yn(a?.data?.write)}\n` + + `TET: write=${yn(t?.data?.write)}\n`}${ + stagesLine}`, + ); + }, [program, canAddNew]); + if (error) { log.error(errorCreator('Enrollment widget could not be loaded')({ error })); } diff --git a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.component.tsx b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.component.tsx index 785622343f..f16f1b3210 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.component.tsx +++ b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.component.tsx @@ -38,7 +38,7 @@ export const DataEntryComponent = ({ { const { hasAuthority } = useAuthorities({ authorities: ['F_TEI_CASCADE_DELETE'] }); + useEffect(() => { + const yn = (v?: boolean) => (v ? 'Yes' : 'No'); + // eslint-disable-next-line no-console + console.log(`Cascade delete TEI: ${yn(hasAuthority)}`); + }, [canWriteData, hasAuthority]); + return ( { + if (!program && !trackedEntityTypeAccess) return; + const a = program?.access; + const t = trackedEntityTypeAccess; + const yn = (v?: boolean) => (v ? 'Yes' : 'No'); + // eslint-disable-next-line no-console + console.log( + `Program: write=${yn(a?.data?.write)}\n` + + `TET: write=${yn(t?.data?.write)}`, + ); + }, [program, trackedEntityTypeAccess, canWriteData, isEditable]); + const renderProfile = () => { if (loading) { return ; diff --git a/src/core_modules/capture-core/hooks/index.ts b/src/core_modules/capture-core/hooks/index.ts index 061192723d..9c0bd08a30 100644 --- a/src/core_modules/capture-core/hooks/index.ts +++ b/src/core_modules/capture-core/hooks/index.ts @@ -7,3 +7,4 @@ export { useScopeInfo } from './useScopeInfo'; export { useScopeTitleText } from './useScopeTitleText'; export { useProgramExpiryForUser } from './useProgramExpiryForUser'; export { useHideWidgetByRuleLocations } from './useHideWidgetByRuleLocations'; +export { useEnrollmentAccess } from './useEnrollmentAccess'; diff --git a/src/core_modules/capture-core/hooks/useEnrollmentAccess.ts b/src/core_modules/capture-core/hooks/useEnrollmentAccess.ts new file mode 100644 index 0000000000..6feaea9b70 --- /dev/null +++ b/src/core_modules/capture-core/hooks/useEnrollmentAccess.ts @@ -0,0 +1,45 @@ +import { useMemo } from 'react'; +import { useDataQuery } from '@dhis2/app-runtime'; + +type ProgramResponse = { + access?: { data?: { read?: boolean; write?: boolean } }; + trackedEntityType?: { access?: { data?: { read?: boolean; write?: boolean } } }; + programStages?: Array<{ access?: { data?: { read?: boolean; write?: boolean } } }>; +}; + +type Result = { + programWriteAccess: boolean; + trackedEntityTypeWriteAccess: boolean; + programStageWriteAccess: boolean; + isLoading: boolean; + error?: any; +}; + +export const useEnrollmentAccess = (programId?: string): Result => { + const { error, loading, data } = useDataQuery( + useMemo( + () => ({ + program: { + resource: `programs/${programId}`, + params: { + fields: ['access,trackedEntityType[access],programStages[access]'], + }, + }, + }), + [programId], + ), + { lazy: !programId } as any, + ); + + const program = data?.program as ProgramResponse | undefined; + + return { + programWriteAccess: Boolean(program?.access?.data?.write), + trackedEntityTypeWriteAccess: Boolean(program?.trackedEntityType?.access?.data?.write), + programStageWriteAccess: Boolean( + program?.programStages?.some(stage => stage?.access?.data?.write), + ), + isLoading: loading, + error, + }; +}; From 78bdfc20074a1ab126608dee4b972ba8f3a156cb Mon Sep 17 00:00:00 2001 From: henrikmv Date: Wed, 6 May 2026 14:01:51 +0200 Subject: [PATCH 28/60] feat: temp --- i18n/en.pot | 7 +++++-- .../WidgetEnrollmentNote.component.tsx | 8 +++++--- .../WidgetEventNote/WidgetEventNote.component.tsx | 8 ++++++-- .../WidgetEventNote/WidgetEventNote.types.ts | 1 - .../RelationshipsWidget.component.tsx | 13 +++++++++++++ 5 files changed, 29 insertions(+), 8 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 68bbc4ccaf..7cf35c8c1e 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: 2026-05-05T17:26:45.666Z\n" -"PO-Revision-Date: 2026-05-05T17:26:45.666Z\n" +"POT-Creation-Date: 2026-05-06T11:24:59.790Z\n" +"PO-Revision-Date: 2026-05-06T11:24:59.790Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -1915,6 +1915,9 @@ msgstr "An error occurred while deleting the relationship." msgid "To open this relationship, please wait until saving is complete" msgstr "To open this relationship, please wait until saving is complete" +msgid "This enrollment doesn't have any relationships" +msgstr "This enrollment doesn't have any relationships" + msgid "Type" msgstr "Type" diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx index 806b9e22f1..f7461f9fd6 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx @@ -3,11 +3,13 @@ import i18n from '@dhis2/d2-i18n'; import { useDispatch, useSelector } from 'react-redux'; import { requestAddNoteForEnrollment } from './WidgetEnrollmentNote.actions'; import { WidgetNote } from '../WidgetNote'; +import { useProgram } from '../WidgetEnrollment/hooks/useProgram'; import { useLocationQuery } from '../../utils/routing'; -export const WidgetEnrollmentNote = ({ readOnly }: { readOnly?: boolean }) => { +export const WidgetEnrollmentNote = () => { const dispatch = useDispatch(); - const { enrollmentId } = useLocationQuery(); + const { enrollmentId, programId } = useLocationQuery(); + const { program } = useProgram(programId); const notes = useSelector(({ enrollmentDomain }: { enrollmentDomain?: { enrollment?: { notes?: Array } } }) => enrollmentDomain?.enrollment?.notes ?? []); @@ -23,7 +25,7 @@ export const WidgetEnrollmentNote = ({ readOnly }: { readOnly?: boolean }) => { emptyNoteMessage={i18n.t('This enrollment doesn\'t have any notes')} notes={notes} onAddNote={onAddNote} - readOnly={readOnly} + readOnly={!program?.access?.data?.write} />
); diff --git a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx index 461caa4254..3f22059b38 100644 --- a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx @@ -4,9 +4,13 @@ import i18n from '@dhis2/d2-i18n'; import type { Props } from './WidgetEventNote.types'; import { requestAddNoteForEvent } from './WidgetEventNote.actions'; import { WidgetNote } from '../WidgetNote'; +import { useProgram } from '../WidgetEnrollment/hooks/useProgram'; +import { useLocationQuery } from '../../utils/routing'; -export const WidgetEventNote = ({ dataEntryKey, dataEntryId, readOnly }: Props) => { +export const WidgetEventNote = ({ dataEntryKey, dataEntryId }: Props) => { const dispatch = useDispatch(); + const { programId } = useLocationQuery(); + const { program } = useProgram(programId); const notes = useSelector(({ dataEntriesNotes }: { dataEntriesNotes: Record }) => dataEntriesNotes[`${dataEntryId}-${dataEntryKey}`] ?? []); @@ -22,7 +26,7 @@ export const WidgetEventNote = ({ dataEntryKey, dataEntryId, readOnly }: Props) emptyNoteMessage={i18n.t('This event doesn\'t have any notes')} notes={notes} onAddNote={onAddNote} - readOnly={readOnly} + readOnly={!program?.access?.data?.write} />
); diff --git a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.types.ts b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.types.ts index 4abf251106..87957e6627 100644 --- a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.types.ts +++ b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.types.ts @@ -1,7 +1,6 @@ export type Props = { dataEntryKey: string; dataEntryId: string; - readOnly?: boolean; }; export type ClientNote = { diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/RelationshipsWidget.component.tsx b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/RelationshipsWidget.component.tsx index bbea26e5cd..effbf92b58 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/RelationshipsWidget.component.tsx +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/RelationshipsWidget.component.tsx @@ -1,4 +1,6 @@ import React, { type ComponentType, useState } from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { colors, spacersNum } from '@dhis2/ui'; import { withStyles } from 'capture-core-utils/styles'; import type { WithStyles } from 'capture-core-utils/styles'; import { Widget, WidgetHeaderCountBadge } from '../../../Widget'; @@ -10,6 +12,12 @@ import { useDeleteRelationship } from './DeleteRelationship/useDeleteRelationshi const styles = { header: {}, + emptyMessage: { + padding: `0 ${spacersNum.dp12}px`, + color: colors.grey600, + fontSize: 14, + lineHeight: '19px', + }, }; const RelationshipsWidgetPlain = ({ @@ -71,6 +79,11 @@ const RelationshipsWidgetPlain = ({ /> ) } + {(relationships?.length ?? 0) === 0 && ( +
+ {i18n.t("This enrollment doesn't have any relationships")} +
+ )} {children}
From d9528f4ac076d89b6033736756b9e08698e3d564 Mon Sep 17 00:00:00 2001 From: henrikmv Date: Wed, 6 May 2026 15:02:00 +0200 Subject: [PATCH 29/60] feat: read only tag with tooltip --- i18n/en.pot | 35 +++++++---- .../LayoutComponentConfig.ts | 10 +++- ...edEntityRelationshipsWrapper.component.tsx | 2 + ...TrackedEntityRelationshipsWrapper.types.ts | 1 + .../ReadOnlyBadge/ReadOnlyBadge.tsx | 58 +++++++++---------- .../WidgetEnrollment/Date/Date.component.tsx | 24 +++++--- .../MiniMap/MiniMap.component.tsx | 3 +- .../WidgetEnrollment/MiniMap/MiniMap.types.ts | 1 + .../WidgetEnrollment.component.tsx | 19 +++++- .../WidgetEnrollment.container.tsx | 6 +- .../WidgetEnrollment/enrollment.types.ts | 1 + .../WidgetEnrollmentNote.component.tsx | 5 +- .../WidgetEventNote.component.tsx | 9 +-- .../WidgetEventNote/WidgetEventNote.types.ts | 1 + .../WidgetNote/WidgetNote.component.tsx | 25 +++++++- .../components/WidgetNote/WidgetNote.types.ts | 4 ++ ...NewTrackedEntityRelationship.container.tsx | 22 ++++--- .../NewTrackedEntityRelationship.types.ts | 1 + ...getTrackedEntityRelationship.component.tsx | 2 + .../WidgetTrackedEntityRelationship.types.ts | 1 + 20 files changed, 159 insertions(+), 71 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 7cf35c8c1e..e5c2a19994 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: 2026-05-06T11:24:59.790Z\n" -"PO-Revision-Date: 2026-05-06T11:24:59.790Z\n" +"POT-Creation-Date: 2026-05-06T12:39:03.039Z\n" +"PO-Revision-Date: 2026-05-06T12:39:03.039Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -1068,21 +1068,30 @@ msgstr "Possible duplicates found" msgid "An error occurred loading possible duplicates" msgstr "An error occurred loading possible duplicates" -msgid "Read only - You can only view this enrollment" -msgstr "Read only - You can only view this enrollment" +msgid "program" +msgstr "program" -msgid "Read only - Cannot edit enrollment or events" -msgstr "Read only - Cannot edit enrollment or events" +msgid "this program stage" +msgstr "this program stage" -msgid "Read only - Cannot edit enrollment" -msgstr "Read only - Cannot edit enrollment" +msgid "and" +msgstr "and" -msgid "Read only - Cannot edit events" -msgstr "Read only - Cannot edit events" +msgid "You only have view access to {{targets}}" +msgstr "You only have view access to {{targets}}" + +msgid "You only have view access to program" +msgstr "You only have view access to program" + +msgid "You only have view access to this program stage" +msgstr "You only have view access to this program stage" msgid "person" msgstr "person" +msgid "Read only" +msgstr "Read only" + msgid "You don't have access to delete this relationship" msgstr "You don't have access to delete this relationship" @@ -1374,6 +1383,9 @@ msgstr "An error occurred while transferring ownership" msgid "Existing dates for auto-generated events will not be updated." msgstr "Existing dates for auto-generated events will not be updated." +msgid "You do not have access to edit this date" +msgstr "You do not have access to edit this date" + msgid "Latitude" msgstr "Latitude" @@ -1882,6 +1894,9 @@ msgstr "Missing implementation step" msgid "Go back without saving relationship" msgstr "Go back without saving relationship" +msgid "You do not have access to add relationships" +msgstr "You do not have access to add relationships" + msgid "New Relationship" msgstr "New Relationship" diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts index 8d16281430..3a6072f0cc 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts @@ -1,8 +1,8 @@ import { WidgetStagesAndEvents } from '../../../../../WidgetStagesAndEvents'; import type { Props as StagesAndEventProps } from '../../../../../WidgetStagesAndEvents/stagesAndEvents.types'; import { TrackedEntityRelationshipsWrapper } from '../../../TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper'; -import type { Props as TrackedEntityRelationshipProps } from - '../../../TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types'; +import type { Props as TrackedEntityRelationshipProps } + from '../../../TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types'; import { WidgetError } from '../../../../../WidgetErrorAndWarning/WidgetError'; import type { Props as WidgetErrorProps } from '../../../../../WidgetErrorAndWarning/WidgetError/WidgetError.types'; import { EnrollmentQuickActions } from '../../../../Enrollment/EnrollmentPageDefault/EnrollmentQuickActions'; @@ -85,7 +85,9 @@ export const TrackedEntityRelationship: WidgetConfig = { toggleVisibility, teiId, onLinkedRecordClick, + programWriteAccess, trackedEntityTypeWriteAccess, + programStageWriteAccess, }: any): TrackedEntityRelationshipProps => ({ trackedEntityTypeId: program.trackedEntityType.id, programId: program.id, @@ -96,6 +98,7 @@ export const TrackedEntityRelationship: WidgetConfig = { teiId, onLinkedRecordClick, readOnly: !trackedEntityTypeWriteAccess, + hideButton: !programWriteAccess && !trackedEntityTypeWriteAccess && !programStageWriteAccess, }), }; @@ -294,9 +297,10 @@ export const AssigneeWidget: WidgetConfig = { export const EventNote: WidgetConfig = { Component: WidgetEventNote, - getProps: ({ dataEntryKey, dataEntryId }: any) => ({ + getProps: ({ dataEntryKey, dataEntryId, programStage }: any) => ({ dataEntryKey, dataEntryId, + stageWriteAccess: Boolean(programStage?.stageForm?.access?.data?.write), }), }; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.tsx b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.tsx index 4fdf01d933..4bbaa4f30e 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.tsx +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.tsx @@ -33,6 +33,7 @@ export const TrackedEntityRelationshipsWrapper = ({ onCloseAddRelationship, onLinkedRecordClick, readOnly, + hideButton, }: Props) => { const dispatch = useDispatch(); const { relationshipTypes, isError } = useTEIRelationshipsWidgetMetadata(); @@ -76,6 +77,7 @@ export const TrackedEntityRelationshipsWrapper = ({ onSelectFindMode={onSelectFindMode} relationshipTypes={relationshipTypes} readOnly={readOnly} + hideButton={hideButton} renderTrackedEntityRegistration={( selectedTrackedEntityTypeId, suggestedProgramId, diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types.ts b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types.ts index f9ac35de0d..98d2f02bae 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types.ts +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types.ts @@ -10,4 +10,5 @@ export type Props = { onCloseAddRelationship: () => void; onLinkedRecordClick: LinkedRecordClick; readOnly: boolean; + hideButton?: boolean; }; diff --git a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx index c4ed02d3e6..93b56ab233 100644 --- a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx +++ b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { colors, IconInfo16, Tag } from '@dhis2/ui'; import i18n from '@dhis2/d2-i18n'; +import { ConditionalTooltip } from '../Tooltips/ConditionalTooltip'; type Props = { readOnly?: boolean; @@ -20,25 +21,17 @@ const getMultiMissingLabel = ( missingStage: boolean, trackedEntityName: string, ): string | null => { - if (missingProgram && missingTET && missingStage) { - return i18n.t('Read only - You can only view this enrollment'); - } - if (missingProgram && missingTET) { - return interpolate( - 'Read only - Cannot edit enrollment, {{trackedEntityName}} profile, or relationships', - trackedEntityName, - ); - } - if (missingProgram && missingStage) { - return i18n.t('Read only - Cannot edit enrollment or events'); - } - if (missingTET && missingStage) { - return interpolate( - 'Read only - Cannot edit {{trackedEntityName}} profile, relationships, or events', - trackedEntityName, - ); - } - return null; + const parts: Array = []; + if (missingProgram) parts.push(i18n.t('program')); + if (missingTET) parts.push(trackedEntityName); + if (missingStage) parts.push(i18n.t('this program stage')); + if (parts.length < 2) return null; + const last = parts.pop() as string; + const joined = `${parts.join(', ')} ${i18n.t('and')} ${last}`; + return i18n.t('You only have view access to {{targets}}', { + targets: joined, + interpolation: { escapeValue: false }, + }); }; const getSingleMissingLabel = ( @@ -47,14 +40,14 @@ const getSingleMissingLabel = ( missingStage: boolean, trackedEntityName: string, ): string | null => { - if (missingProgram) return i18n.t('Read only - Cannot edit enrollment'); + if (missingProgram) return i18n.t('You only have view access to program'); if (missingTET) { return interpolate( - 'Read only - Cannot edit {{trackedEntityName}} profile or relationships', + 'You only have view access to {{trackedEntityName}}', trackedEntityName, ); } - if (missingStage) return i18n.t('Read only - Cannot edit events'); + if (missingStage) return i18n.t('You only have view access to this program stage'); return null; }; @@ -79,22 +72,23 @@ export const ReadOnlyBadge = ({ trackedEntityName, label, }: Props) => { - const isReadOnly = readOnly - || !programWriteAccess - || !trackedEntityTypeWriteAccess - || !programStageWriteAccess; + const allAccessMissing = !programWriteAccess + && !trackedEntityTypeWriteAccess + && !programStageWriteAccess; + const isReadOnly = readOnly || allAccessMissing; if (!isReadOnly) return null; - const text = label + const tooltipContent = label ?? getDefaultLabel( programWriteAccess, trackedEntityTypeWriteAccess, programStageWriteAccess, trackedEntityName ?? i18n.t('person'), - ) - ?? i18n.t('Read only - You can only view this enrollment'); + ); return ( - }> - {text} - + + }> + {i18n.t('Read only')} + + ); }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/Date/Date.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/Date/Date.component.tsx index 0845ccee9f..7fa9d05b6a 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/Date/Date.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/Date/Date.component.tsx @@ -18,12 +18,14 @@ import { convertValue as convertValueClientToView } from '../../../converters/cl import { convertValue as convertValueFormToClient } from '../../../converters/formToClient'; import { convertValue as convertValueClientToServer } from '../../../converters/clientToServer'; import { dataElementTypes } from '../../../metaData'; +import { ConditionalTooltip } from '../../Tooltips/ConditionalTooltip'; type OwnProps = { date: string; dateLabel: string; locale: string; readOnly: boolean; + hideEdit?: boolean; displayAutoGeneratedEventWarning: boolean; onSave: (date: string) => void; allowFutureDate?: boolean; @@ -92,6 +94,7 @@ const DateComponentPlain = ({ dateLabel, locale, readOnly, + hideEdit, displayAutoGeneratedEventWarning, onSave, allowFutureDate, @@ -209,14 +212,21 @@ const DateComponentPlain = ({ {displayDate} - {!readOnly && ( - + + )}
); diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MiniMap/MiniMap.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/MiniMap/MiniMap.component.tsx index f858a40581..c7ce68c3f6 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/MiniMap/MiniMap.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MiniMap/MiniMap.component.tsx @@ -30,6 +30,7 @@ const MiniMapPlain = ({ refetchTEI, onError, readOnly, + hideEdit, classes, }: Props) => { const [isOpenMap, setOpenMap] = useState(false); @@ -56,7 +57,7 @@ const MiniMapPlain = ({ zoomControl={false} attributionControl={false} key="minimap" - onClick={() => { + onClick={hideEdit ? undefined : () => { setOpenMap(true); }} > diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MiniMap/MiniMap.types.ts b/src/core_modules/capture-core/components/WidgetEnrollment/MiniMap/MiniMap.types.ts index 033162987b..ee43dcc6a8 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/MiniMap/MiniMap.types.ts +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MiniMap/MiniMap.types.ts @@ -9,4 +9,5 @@ export type OwnProps = { onError?: (message: string) => void; geometryType: typeof dataElementTypes.COORDINATE | typeof dataElementTypes.POLYGON; readOnly: boolean; + hideEdit?: boolean; }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx index 4859fef1a9..8889e9a059 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx @@ -13,6 +13,7 @@ import { useTimeZoneConversion } from '@dhis2/app-runtime'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { LoadingMaskElementCenter } from '../LoadingMasks'; import { Widget } from '../Widget'; +import { ReadOnlyBadge } from '../ReadOnlyBadge'; import type { PlainProps } from './enrollment.types'; import { Status } from './Status'; import { dataElementTypes } from '../../metaData'; @@ -58,6 +59,7 @@ const WidgetEnrollmentPlain = ({ loading, canAddNew, programDataWriteAccess, + trackedEntityTypeDataWriteAccess, displayAutoGeneratedEventWarning, onDelete, onAddNew, @@ -85,7 +87,19 @@ const WidgetEnrollmentPlain = ({ return (
+ {i18n.t('Enrollment')} +
+ +
+
+ } onOpen={useCallback(() => setOpenStatus(true), [setOpenStatus])} onClose={useCallback(() => setOpenStatus(false), [setOpenStatus])} open={open} @@ -113,6 +127,7 @@ const WidgetEnrollmentPlain = ({ dateLabel={getEnrollmentDateLabel(program)} locale={locale} readOnly={!programDataWriteAccess} + hideEdit={!programDataWriteAccess} displayAutoGeneratedEventWarning={displayAutoGeneratedEventWarning} onSave={updateEnrollmentDate} allowFutureDate={program.selectEnrollmentDatesInFuture} @@ -126,6 +141,7 @@ const WidgetEnrollmentPlain = ({ dateLabel={getIncidentDateLabel(program)} locale={locale} readOnly={!programDataWriteAccess} + hideEdit={!programDataWriteAccess} displayAutoGeneratedEventWarning={displayAutoGeneratedEventWarning} onSave={updateIncidentDate} allowFutureDate={program.selectIncidentDatesInFuture} @@ -173,6 +189,7 @@ const WidgetEnrollmentPlain = ({ refetchTEI={refetchTEI} onError={onError} readOnly={!programDataWriteAccess} + hideEdit={!programDataWriteAccess} />
)} diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.container.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.container.tsx index 76b14cd9ca..0dcfdc0f5c 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.container.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.container.tsx @@ -100,13 +100,17 @@ export const WidgetEnrollment = ({ log.error(errorCreator('Enrollment widget could not be loaded')({ error })); } + const programDataWriteAccess = Boolean(program?.access?.data?.write); + const trackedEntityTypeDataWriteAccess = Boolean(program?.trackedEntityType?.access?.data?.write); + return ( void; updateIncidentDate: (incidentDate: string) => void; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx index f7461f9fd6..5521425ab1 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx @@ -17,6 +17,8 @@ export const WidgetEnrollmentNote = () => { dispatch(requestAddNoteForEnrollment(enrollmentId, newNoteValue)); }; + const programWriteAccess = Boolean(program?.access?.data?.write); + return (
{ emptyNoteMessage={i18n.t('This enrollment doesn\'t have any notes')} notes={notes} onAddNote={onAddNote} - readOnly={!program?.access?.data?.write} + readOnly={!programWriteAccess} + programWriteAccess={programWriteAccess} />
); diff --git a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx index 3f22059b38..b6a372c0bf 100644 --- a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx @@ -4,13 +4,9 @@ import i18n from '@dhis2/d2-i18n'; import type { Props } from './WidgetEventNote.types'; import { requestAddNoteForEvent } from './WidgetEventNote.actions'; import { WidgetNote } from '../WidgetNote'; -import { useProgram } from '../WidgetEnrollment/hooks/useProgram'; -import { useLocationQuery } from '../../utils/routing'; -export const WidgetEventNote = ({ dataEntryKey, dataEntryId }: Props) => { +export const WidgetEventNote = ({ dataEntryKey, dataEntryId, stageWriteAccess = true }: Props) => { const dispatch = useDispatch(); - const { programId } = useLocationQuery(); - const { program } = useProgram(programId); const notes = useSelector(({ dataEntriesNotes }: { dataEntriesNotes: Record }) => dataEntriesNotes[`${dataEntryId}-${dataEntryKey}`] ?? []); @@ -26,7 +22,8 @@ export const WidgetEventNote = ({ dataEntryKey, dataEntryId }: Props) => { emptyNoteMessage={i18n.t('This event doesn\'t have any notes')} notes={notes} onAddNote={onAddNote} - readOnly={!program?.access?.data?.write} + readOnly={!stageWriteAccess} + programStageWriteAccess={stageWriteAccess} />
); diff --git a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.types.ts b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.types.ts index 87957e6627..8476226fc9 100644 --- a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.types.ts +++ b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.types.ts @@ -1,6 +1,7 @@ export type Props = { dataEntryKey: string; dataEntryId: string; + stageWriteAccess?: boolean; }; export type ClientNote = { diff --git a/src/core_modules/capture-core/components/WidgetNote/WidgetNote.component.tsx b/src/core_modules/capture-core/components/WidgetNote/WidgetNote.component.tsx index 33e33a6a05..76faed034f 100644 --- a/src/core_modules/capture-core/components/WidgetNote/WidgetNote.component.tsx +++ b/src/core_modules/capture-core/components/WidgetNote/WidgetNote.component.tsx @@ -1,16 +1,36 @@ import React, { useState, useCallback } from 'react'; import { Widget, WidgetHeaderCountBadge } from '../Widget'; +import { ReadOnlyBadge } from '../ReadOnlyBadge'; import type { Props } from './WidgetNote.types'; import { NoteSection } from './NoteSection/NoteSection'; -export const WidgetNote = ({ title, notes, onAddNote, ...passOnProps }: Props) => { +export const WidgetNote = ({ + title, + notes, + onAddNote, + readOnly, + programWriteAccess, + trackedEntityTypeWriteAccess, + programStageWriteAccess, + trackedEntityName, + ...passOnProps +}: Props) => { const [open, setOpenStatus] = useState(true); return ( + header={
{title} {notes.length > 0 && } +
+ +
} onOpen={useCallback(() => setOpenStatus(true), [setOpenStatus])} onClose={useCallback(() => setOpenStatus(false), [setOpenStatus])} @@ -19,6 +39,7 @@ export const WidgetNote = ({ title, notes, onAddNote, ...passOnProps }: Props) =
diff --git a/src/core_modules/capture-core/components/WidgetNote/WidgetNote.types.ts b/src/core_modules/capture-core/components/WidgetNote/WidgetNote.types.ts index d7b83b6fd5..bad9bba800 100644 --- a/src/core_modules/capture-core/components/WidgetNote/WidgetNote.types.ts +++ b/src/core_modules/capture-core/components/WidgetNote/WidgetNote.types.ts @@ -12,4 +12,8 @@ export type Props = { }>; onAddNote: (note: string) => void; readOnly?: boolean; + programWriteAccess?: boolean; + trackedEntityTypeWriteAccess?: boolean; + programStageWriteAccess?: boolean; + trackedEntityName?: string; }; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.tsx b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.tsx index e3bbaed31c..265a238d1a 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.tsx +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.tsx @@ -3,6 +3,7 @@ import { Button, spacersNum } from '@dhis2/ui'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import i18n from '@dhis2/d2-i18n'; import { NewTrackedEntityRelationshipPortal } from './NewTrackedEntityRelationship.portal'; +import { ConditionalTooltip } from '../../../Tooltips/ConditionalTooltip'; import type { ContainerProps } from './NewTrackedEntityRelationship.types'; const styles = { @@ -25,6 +26,7 @@ export const NewTrackedEntityRelationshipPlain = ({ renderTrackedEntityRegistration, onSelectFindMode, readOnly, + hideButton, classes, }: ContainerProps & WithStyles) => { const [addWizardVisible, setAddWizardVisible] = useState(false); @@ -41,14 +43,20 @@ export const NewTrackedEntityRelationshipPlain = ({ return (
- {!readOnly && ( - + + )} { diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.ts b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.ts index 8385d75970..4e051ba0ae 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.ts +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.ts @@ -34,6 +34,7 @@ export type ContainerProps = Readonly<{ onOpenAddRelationship?: () => void; onSelectFindMode?: OnSelectFindMode; readOnly?: boolean; + hideButton?: boolean; }>; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.tsx b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.tsx index 96ce914c14..78ec779665 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.tsx +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.tsx @@ -21,6 +21,7 @@ export const WidgetTrackedEntityRelationship = ({ renderTrackedEntitySearch, renderTrackedEntityRegistration, readOnly, + hideButton, }: WidgetTrackedEntityRelationshipProps) => { const { data: relationshipTypes } = useRelationshipTypes(cachedRelationshipTypes); const { data: trackedEntityTypeName, isLoading: isLoadingTEType } = useTrackedEntityTypeName(trackedEntityTypeId); @@ -77,6 +78,7 @@ export const WidgetTrackedEntityRelationship = ({ renderTrackedEntitySearch={renderTrackedEntitySearch} renderTrackedEntityRegistration={renderTrackedEntityRegistration} readOnly={readOnly} + hideButton={hideButton} /> ); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.ts b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.ts index 85ff46a398..ba44ee879b 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.ts +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.ts @@ -45,4 +45,5 @@ export type WidgetTrackedEntityRelationshipProps = { onLinkToTrackedEntityFromSearch: OnLinkToTrackedEntityFromSearch, ) => React.ReactElement; readOnly?: boolean; + hideButton?: boolean; }; From 58663c85b16c5967bb0fd9281cbea5e51eaabb33 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Thu, 7 May 2026 15:47:13 +0200 Subject: [PATCH 30/60] fix: [DHIS2-21392] Form elements visble when no access to form (#4535) * fix: simplify NoWriteAccessMessage * fix: clean up * feat: enhance NoWriteAccessMessage with back navigation functionality * fix: clean up * feat clean up * feat: rename back navigation to cancel in NoWriteAccessMessage and related components * feat: update Cypress test for New event quick action button visibility when no write access --- .../ProgramStageSelector.feature | 2 +- .../ProgramStageSelector.js | 5 +- i18n/en.pot | 9 ++- .../dataEntrySelectionsNoAccess.component.tsx | 68 ------------------- .../dataEntrySelectionsNoAccess.container.tsx | 16 ----- ...SingleEventRegistrationEntry.component.tsx | 12 +++- .../NoWriteAccessMessage.component.tsx | 34 +++++----- .../NewEventWorkspace.component.tsx | 41 ++++++++--- .../Pages/New/NewPage.component.tsx | 1 + .../AccessVerification.component.tsx | 21 ------ .../AccessVerification.container.tsx | 30 -------- .../AccessVerification/NoAccess.component.tsx | 67 ------------------ .../accessVerification.selectors.ts | 11 --- .../accessVerification.types.ts | 26 ------- .../AccessVerification/index.ts | 1 - .../OrgUnitFetcher.container.tsx | 9 +++ .../WidgetEnrollmentEventNew.container.tsx | 4 +- .../AccessVerification/NoAccess.component.tsx | 66 ------------------ .../AccessVerification/index.ts | 1 - .../WidgetEventSchedule.container.tsx | 10 +-- 20 files changed, 81 insertions(+), 353 deletions(-) delete mode 100644 src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/SelectionsNoAccess/dataEntrySelectionsNoAccess.component.tsx delete mode 100644 src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/SelectionsNoAccess/dataEntrySelectionsNoAccess.container.tsx delete mode 100644 src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/AccessVerification.component.tsx delete mode 100644 src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/AccessVerification.container.tsx delete mode 100644 src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/NoAccess.component.tsx delete mode 100644 src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/accessVerification.selectors.ts delete mode 100644 src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/accessVerification.types.ts delete mode 100644 src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/index.ts create mode 100644 src/core_modules/capture-core/components/WidgetEnrollmentEventNew/OrgUnitFetcher/OrgUnitFetcher.container.tsx delete mode 100644 src/core_modules/capture-core/components/WidgetEventSchedule/AccessVerification/NoAccess.component.tsx delete mode 100644 src/core_modules/capture-core/components/WidgetEventSchedule/AccessVerification/index.ts diff --git a/cypress/e2e/EnrollmentAddEventPage/ProgramStageSelector/ProgramStageSelector.feature b/cypress/e2e/EnrollmentAddEventPage/ProgramStageSelector/ProgramStageSelector.feature index 0e93937044..4824d5b066 100644 --- a/cypress/e2e/EnrollmentAddEventPage/ProgramStageSelector/ProgramStageSelector.feature +++ b/cypress/e2e/EnrollmentAddEventPage/ProgramStageSelector/ProgramStageSelector.feature @@ -19,4 +19,4 @@ Feature: Program stage selector when navigating to EnrollmentEventNew without st @user:trackerAutoTestRestricted Scenario: Stages buttons should not be displayed when no data write access Given user lands on the Enrollment dashboard page by typing #/enrollmentEventNew?enrollmentId=X7g83OFRALm&orgUnitId=DiszpKrYNg8&programId=WSGAb5XwJ3Y&teiId=YsKjdOcl9Cd - Then the New event quick action button is disabled + Then the New event quick action button should not be visible diff --git a/cypress/e2e/EnrollmentAddEventPage/ProgramStageSelector/ProgramStageSelector.js b/cypress/e2e/EnrollmentAddEventPage/ProgramStageSelector/ProgramStageSelector.js index c28e147c37..dea0defe03 100644 --- a/cypress/e2e/EnrollmentAddEventPage/ProgramStageSelector/ProgramStageSelector.js +++ b/cypress/e2e/EnrollmentAddEventPage/ProgramStageSelector/ProgramStageSelector.js @@ -33,6 +33,7 @@ Then('only three program stages are displayed in the stage selector widget', () cy.get('[data-test=program-stage-selector-button]').should('have.length', 3); }); -Then('the New event quick action button is disabled', () => { - cy.get('[data-test=quick-action-button-report]').should('be.disabled'); +Then('the New event quick action button should not be visible', () => { + cy.get('[data-test="enrollment-overview-page"]').should('exist'); + cy.get('[data-test=quick-action-button-container]').should('not.exist'); }); diff --git a/i18n/en.pot b/i18n/en.pot index e5c2a19994..ea04857587 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -312,9 +312,6 @@ msgstr "No, cancel" msgid "You don't have access to create an event in the current selections" msgstr "You don't have access to create an event in the current selections" -msgid "Save" -msgstr "Save" - msgid "Saving a {{trackedEntityName}}" msgstr "Saving a {{trackedEntityName}}" @@ -626,6 +623,9 @@ msgstr "Select columns" msgid "Columns to show in table" msgstr "Columns to show in table" +msgid "Save" +msgstr "Save" + msgid "Column" msgstr "Column" @@ -811,6 +811,9 @@ msgstr "There was an error loading the page" msgid "Program stage is invalid" msgstr "Program stage is invalid" +msgid "Stage not found" +msgstr "Stage not found" + msgid "Report" msgstr "Report" diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/SelectionsNoAccess/dataEntrySelectionsNoAccess.component.tsx b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/SelectionsNoAccess/dataEntrySelectionsNoAccess.component.tsx deleted file mode 100644 index 43e5b7fc55..0000000000 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/SelectionsNoAccess/dataEntrySelectionsNoAccess.component.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import React, { Component } from 'react'; -import { withStyles, WithStyles } from 'capture-core-utils/styles'; -import i18n from '@dhis2/d2-i18n'; -import { Button } from '@dhis2/ui'; -import { NoWriteAccessMessage } from '../../../NoWriteAccessMessage'; - -const getStyles = (theme: any) => ({ - contents: { - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - paddingTop: 50, - paddingBottom: 50, - }, - buttonRow: { - display: 'flex', - flexWrap: 'wrap', - paddingTop: 10, - marginInlineStart: '-8px', - }, - buttonContainer: { - paddingInlineEnd: theme.spacing.unit * 2, - }, -}) as const; - -type Props = { - onCancel: () => void; -}; - -class DataEntrySelectionsNoAccessPlain extends Component> { - render() { - const { classes, onCancel } = this.props; - return ( -
- -
-
- -
-
- -
-
-
- ); - } -} - -export const DataEntrySelectionsNoAccess = withStyles(getStyles)(DataEntrySelectionsNoAccessPlain); diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/SelectionsNoAccess/dataEntrySelectionsNoAccess.container.tsx b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/SelectionsNoAccess/dataEntrySelectionsNoAccess.container.tsx deleted file mode 100644 index dc20b9bef9..0000000000 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/SelectionsNoAccess/dataEntrySelectionsNoAccess.container.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { connect } from 'react-redux'; -import { DataEntrySelectionsNoAccess } from './dataEntrySelectionsNoAccess.component'; -import { - cancelNewEventAndReturnToMainPage, -} from '../DataEntryWrapper/DataEntry/actions/dataEntry.actions'; - -const mapStateToProps = () => ({ -}); - -const mapDispatchToProps = (dispatch: any) => ({ - onCancel: () => { - dispatch(cancelNewEventAndReturnToMainPage()); - }, -}); - -export const SelectionsNoAccess = connect(mapStateToProps, mapDispatchToProps)(DataEntrySelectionsNoAccess); diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/SingleEventRegistrationEntry.component.tsx b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/SingleEventRegistrationEntry.component.tsx index 948832929a..6843b28f2a 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/SingleEventRegistrationEntry.component.tsx +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/SingleEventRegistrationEntry.component.tsx @@ -1,13 +1,21 @@ import React from 'react'; +import { useDispatch } from 'react-redux'; +import i18n from '@dhis2/d2-i18n'; +import { NoWriteAccessMessage } from '../../NoWriteAccessMessage'; import { NewEventDataEntryWrapper } from './DataEntryWrapper/NewEventDataEntryWrapper.container'; import { NewRelationshipWrapper } from './NewRelationshipWrapper/NewEventNewRelationshipWrapper.container'; -import { SelectionsNoAccess } from './SelectionsNoAccess/dataEntrySelectionsNoAccess.container'; +import { cancelNewEventAndReturnToMainPage } from './DataEntryWrapper/DataEntry/actions/dataEntry.actions'; import type { Props } from './SingleEventRegistrationEntry.types'; export const SingleEventRegistrationEntryComponent = ({ showAddRelationship, eventAccess }: Props) => { + const dispatch = useDispatch(); + if (!eventAccess.write) { return ( - + dispatch(cancelNewEventAndReturnToMainPage())} + /> ); } diff --git a/src/core_modules/capture-core/components/NoWriteAccessMessage/NoWriteAccessMessage.component.tsx b/src/core_modules/capture-core/components/NoWriteAccessMessage/NoWriteAccessMessage.component.tsx index 141f68fbca..d8c1ce8238 100644 --- a/src/core_modules/capture-core/components/NoWriteAccessMessage/NoWriteAccessMessage.component.tsx +++ b/src/core_modules/capture-core/components/NoWriteAccessMessage/NoWriteAccessMessage.component.tsx @@ -1,36 +1,36 @@ import React, { type ComponentType } from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { Button } from '@dhis2/ui'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { IncompleteSelectionsMessage } from '../IncompleteSelectionsMessage'; const styles = () => ({ - header: { - flexGrow: 1, - fontSize: 16, - fontWeight: 500, - }, message: { marginTop: 10, }, }); type Props = { - title?: string; message: string; + onCancel?: () => void; }; type PropsWithStyles = Props & WithStyles; -export const NoWriteAccessMessagePlain = ({ title, message, classes }: PropsWithStyles) => ( - <> -
- {title} -
-
- - {message} - -
- +const NoWriteAccessMessagePlain = ({ message, onCancel, classes }: PropsWithStyles) => ( +
+ + {message} + + {onCancel && ( + + )} +
); export const NoWriteAccessMessage = diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/NewEventWorkspace/NewEventWorkspace.component.tsx b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/NewEventWorkspace/NewEventWorkspace.component.tsx index 1ea05063ae..fd6f703bb5 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/NewEventWorkspace/NewEventWorkspace.component.tsx +++ b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/NewEventWorkspace/NewEventWorkspace.component.tsx @@ -4,9 +4,10 @@ import i18n from '@dhis2/d2-i18n'; import { useSelector } from 'react-redux'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { tabMode } from './newEventWorkspace.constants'; -import { getProgramAndStageForProgram } from '../../../../metaData'; +import { getProgramAndStageForProgram, getProgramEventAccess } from '../../../../metaData'; import { WidgetEnrollmentEventNew } from '../../../WidgetEnrollmentEventNew'; import { DiscardDialog } from '../../../Dialogs/DiscardDialog.component'; +import { NoWriteAccessMessage } from '../../../NoWriteAccessMessage'; import { Widget } from '../../../Widget'; import { WidgetStageHeader } from './WidgetStageHeader'; import { WidgetEventSchedule } from '../../../WidgetEventSchedule'; @@ -21,6 +22,9 @@ const styles: Readonly = () => ({ innerWrapper: { padding: `0 ${spacersNum.dp12}px`, }, + errorWrapper: { + padding: `${spacersNum.dp16}px ${spacersNum.dp12}px`, + }, tabs: { marginBottom: spacersNum.dp16, }, @@ -54,18 +58,35 @@ const NewEventWorkspacePlain = ({ } }; + const renderWidget = (content: React.ReactNode) => ( + : null} + > + {content} + + ); + if (!stage) { - return null; + return renderWidget( +
{i18n.t('Stage not found')}
, + ); + } + + const eventAccess = getProgramEventAccess(programId, stageId); + if (!eventAccess?.write) { + return renderWidget( +
+ +
, + ); } return ( <> - - } - > + {renderWidget(
@@ -123,8 +144,8 @@ const NewEventWorkspacePlain = ({ hideDueDate={stage?.hideDueDate} enableUserAssignment />} -
- +
, + )} { setMode(tempMode.current); setWarningVisible(false); }} diff --git a/src/core_modules/capture-core/components/Pages/New/NewPage.component.tsx b/src/core_modules/capture-core/components/Pages/New/NewPage.component.tsx index e181a47ed6..686c9b99b6 100644 --- a/src/core_modules/capture-core/components/Pages/New/NewPage.component.tsx +++ b/src/core_modules/capture-core/components/Pages/New/NewPage.component.tsx @@ -82,6 +82,7 @@ const NewPagePlain = ({ interpolation: { escapeValue: false }, }, )} + onCancel={handleMainPageNavigation} /> : diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/AccessVerification.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/AccessVerification.component.tsx deleted file mode 100644 index 0bad2b745c..0000000000 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/AccessVerification.component.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import { OrgUnitFetcher } from '../OrgUnitFetcher/OrgUnitFetcher.component'; -import { NoAccess } from './NoAccess.component'; -import type { Props } from './accessVerification.types'; - -export const AccessVerificationComponent = ({ eventAccess, onCancel, ...passOnProps }: Props) => { - if (!eventAccess.write) { - return ( - - ); - } - - return ( - - ); -}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/AccessVerification.container.tsx b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/AccessVerification.container.tsx deleted file mode 100644 index 771d2d8c12..0000000000 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/AccessVerification.container.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { connect } from 'react-redux'; -import type { ComponentType } from 'react'; -import { - AccessVerificationComponent, -} from './AccessVerification.component'; -import { withBrowserBackWarning } from '../../../HOC/withBrowserBackWarning'; -import { dataEntryHasChanges } from '../../DataEntry/common/dataEntryHasChanges'; -import { makeEventAccessSelector } from './accessVerification.selectors'; -import type { ContainerProps } from './accessVerification.types'; -import { defaultDialogProps } from '../../Dialogs/DiscardDialog.constants'; - -const inEffect = (state: any, ownProps: any) => - dataEntryHasChanges(state, ownProps.widgetReducerName) || state.newEventPage.showAddRelationship; - -const makeMapStateToProps = () => { - const eventAccessSelector = makeEventAccessSelector(); - return (state: any, { program, stage }: any) => ({ - eventAccess: eventAccessSelector(state, { programId: program.id, stageId: stage.id }), - }); -}; - -const mapDispatchToProps = () => ({ -}); - -const AccessVerificationWithConnect = connect(makeMapStateToProps, mapDispatchToProps)(AccessVerificationComponent as any); - -export const AccessVerification: ComponentType = withBrowserBackWarning( - defaultDialogProps, - inEffect, -)(AccessVerificationWithConnect); diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/NoAccess.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/NoAccess.component.tsx deleted file mode 100644 index cf7513b6c8..0000000000 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/NoAccess.component.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React, { Component } from 'react'; -import { withStyles, type WithStyles } from 'capture-core-utils/styles'; -import i18n from '@dhis2/d2-i18n'; -import { Button } from '@dhis2/ui'; -import { NoWriteAccessMessage } from '../../NoWriteAccessMessage'; - -const getStyles: any = (theme: any) => ({ - contents: { - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - paddingTop: 50, - paddingBottom: 50, - }, - buttonRow: { - display: 'flex', - flexWrap: 'wrap', - paddingTop: 10, - marginInlineStart: '-8px', - }, - buttonContainer: { - paddingInlineEnd: theme.spacing.unit * 2, - }, -}); - -type Props = { - onCancel: () => void; -} & WithStyles; - -class NoAccessPlain extends Component { - render() { - const { classes, onCancel } = this.props; - return ( -
- -
-
- -
-
- -
-
-
- ); - } -} - -export const NoAccess = withStyles(getStyles)(NoAccessPlain); diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/accessVerification.selectors.ts b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/accessVerification.selectors.ts deleted file mode 100644 index 3f83887bde..0000000000 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/accessVerification.selectors.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { createSelector } from 'reselect'; -import { getProgramEventAccess } from '../../../metaData'; - -const programIdSelector = (state: any, { programId }: any) => programId; -const programStageIdSelector = (state: any, { stageId }: any) => stageId; - -export const makeEventAccessSelector = () => createSelector( - programIdSelector, - programStageIdSelector, - (programId: string, programStageId: string | null) => - programId && getProgramEventAccess(programId, programStageId)); diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/accessVerification.types.ts b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/accessVerification.types.ts deleted file mode 100644 index 2be09d715a..0000000000 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/accessVerification.types.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { ProgramStage, RenderFoundation, TrackerProgram } from '../../../metaData'; -import type { ExternalSaveHandler, RulesExecutionDependencies } from '../common.types'; - -export type ContainerProps = { - program: TrackerProgram; - stage: ProgramStage; - formFoundation: RenderFoundation; - teiId: string; - enrollmentId: string; - orgUnitId: string; - rulesExecutionDependencies: RulesExecutionDependencies; - onSaveExternal?: ExternalSaveHandler; - onSaveSuccessActionType?: string; - onSaveErrorActionType?: string; - onSaveAndCompleteEnrollmentSuccessActionType?: string; - onSaveAndCompleteEnrollmentErrorActionType?: string; - widgetReducerName: string; - onCancel?: () => void; -}; - -export type Props = { - eventAccess: { - read: boolean; - write: boolean; - }; -} & ContainerProps; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/index.ts b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/index.ts deleted file mode 100644 index 919db1509a..0000000000 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { AccessVerification } from './AccessVerification.container'; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/OrgUnitFetcher/OrgUnitFetcher.container.tsx b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/OrgUnitFetcher/OrgUnitFetcher.container.tsx new file mode 100644 index 0000000000..7f69682e41 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/OrgUnitFetcher/OrgUnitFetcher.container.tsx @@ -0,0 +1,9 @@ +import { OrgUnitFetcher as OrgUnitFetcherComponent } from './OrgUnitFetcher.component'; +import { withBrowserBackWarning } from '../../../HOC/withBrowserBackWarning'; +import { dataEntryHasChanges } from '../../DataEntry/common/dataEntryHasChanges'; +import { defaultDialogProps } from '../../Dialogs/DiscardDialog.constants'; + +const inEffect = (state: any, ownProps: any) => + dataEntryHasChanges(state, ownProps.widgetReducerName) || state.newEventPage.showAddRelationship; + +export const OrgUnitFetcher = withBrowserBackWarning(defaultDialogProps, inEffect)(OrgUnitFetcherComponent); diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/WidgetEnrollmentEventNew.container.tsx b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/WidgetEnrollmentEventNew.container.tsx index 65d683e2e5..4e48426865 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/WidgetEnrollmentEventNew.container.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/WidgetEnrollmentEventNew.container.tsx @@ -1,7 +1,7 @@ import React, { useMemo } from 'react'; import i18n from '@dhis2/d2-i18n'; import { getProgramAndStageForProgram, TrackerProgram } from '../../metaData'; -import { AccessVerification } from './AccessVerification'; +import { OrgUnitFetcher } from './OrgUnitFetcher/OrgUnitFetcher.container'; import type { WidgetProps } from './WidgetEnrollmentEventNew.types'; import { useMetadataForProgramStage } from '../DataEntries/common/ProgramStage/useMetadataForProgramStage'; @@ -36,7 +36,7 @@ export const WidgetEnrollmentEventNew = ({ } return ( - ({ - contents: { - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - paddingTop: 50, - paddingBottom: 50, - }, - buttonRow: { - display: 'flex', - flexWrap: 'wrap', - paddingTop: 10, - marginInlineStart: '-8px', - }, - buttonContainer: { - paddingInlineEnd: theme.spacing.unit * 2, - }, -}) as const; - -type Props = { - onCancel: () => void; -} & WithStyles; - -class NoAccessPlain extends Component { - render() { - const { classes, onCancel } = this.props; - return ( -
- -
-
- -
-
- -
-
-
- ); - } -} - -export const NoAccess = withStyles(getStyles)(NoAccessPlain); diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/AccessVerification/index.ts b/src/core_modules/capture-core/components/WidgetEventSchedule/AccessVerification/index.ts deleted file mode 100644 index 47d0512497..0000000000 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/AccessVerification/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { NoAccess } from './NoAccess.component'; diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.tsx b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.tsx index 4f46356c5d..f0e2ad9fd6 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.tsx +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.tsx @@ -4,7 +4,7 @@ import { useDispatch } from 'react-redux'; import { useTimeZoneConversion } from '@dhis2/app-runtime'; import moment from 'moment'; import { pipe } from 'capture-core-utils'; -import { getProgramAndStageForProgram, TrackerProgram, getProgramEventAccess, dataElementTypes } from '../../metaData'; +import { getProgramAndStageForProgram, TrackerProgram, dataElementTypes } from '../../metaData'; import { getCachedOrgUnitName } from '../../metadataRetrieval/orgUnitName'; import { useLocationQuery } from '../../utils/routing'; import type { ContainerProps } from './widgetEventSchedule.types'; @@ -17,7 +17,6 @@ import { useNoteDetails, } from './hooks'; import { requestScheduleEvent } from './WidgetEventSchedule.actions'; -import { NoAccess } from './AccessVerification'; import { useCategoryCombinations } from '../DataEntryDhis2Helpers/AOC/useCategoryCombinations'; import { convertFormToClient, convertClientToServer } from '../../converters'; import { useProgramExpiryForUser } from '../../hooks'; @@ -193,13 +192,6 @@ export const WidgetEventSchedule = ({ ); } - const eventAccess = getProgramEventAccess(programId, stageId); - if (!eventAccess?.write) { - return ( - - ); - } - return ( Date: Thu, 7 May 2026 17:17:31 +0200 Subject: [PATCH 31/60] feat: program stage access handeling --- i18n/en.pot | 37 ++++---- .../EnrollmentPageDefault.container.tsx | 2 + .../EnrollmentPageDefault.types.ts | 1 + .../PageLayout/DefaultPageLayout.constants.ts | 1 + .../EnrollmentEditEventPage.component.tsx | 2 + .../EnrollmentEditEventPage.container.tsx | 2 + .../EnrollmentEditEventPage.types.ts | 1 + .../PageLayout/DefaultPageLayout.constants.ts | 1 + .../EnrollmentPageLayout.tsx | 84 +++++++++++++++++- .../LayoutComponentConfig.ts | 22 +++-- ...edEntityRelationshipsWrapper.component.tsx | 7 +- ...TrackedEntityRelationshipsWrapper.types.ts | 3 + .../ReadOnlyBadge/ReadOnlyBadge.tsx | 58 +++++++++++-- .../Status/Status.component.tsx | 2 +- .../WidgetEnrollment.component.tsx | 35 ++++---- .../WidgetEnrollment.container.tsx | 2 + .../WidgetEnrollment/enrollment.types.ts | 2 + .../WidgetEnrollmentNote.component.tsx | 7 +- .../WidgetHeader/WidgetHeader.container.tsx | 45 ++++++---- .../WidgetEventNote.component.tsx | 8 +- .../WidgetEventNote/WidgetEventNote.types.ts | 4 +- .../WidgetNote/WidgetNote.component.tsx | 21 +++-- .../components/WidgetNote/WidgetNote.types.ts | 1 + .../DataEntry/DataEntry.component.tsx | 6 +- .../DataEntry/DataEntry.container.tsx | 2 + .../DataEntry/dataEntry.types.ts | 2 + .../DeleteMenuItem.component.tsx | 4 + .../WidgetProfile/WidgetProfile.component.tsx | 1 + .../WidgetRelatedStages.container.tsx | 8 ++ .../Stages/Stage/Stage.component.tsx | 10 ++- .../StageCreateNewButton.tsx | 15 ++-- .../Stage/StageDetail/EventRow/EventRow.tsx | 39 ++++----- .../StageDetail/StageDetail.component.tsx | 26 ++++-- .../Stage/StageDetail/stageDetail.types.ts | 1 + .../StageOverview/StageOverview.component.tsx | 11 ++- .../StageOverview/stageOverview.types.ts | 2 + .../Stages/Stage/stage.types.ts | 2 + .../Stages/Stages.component.tsx | 21 ++++- .../Stages/stages.types.ts | 8 ++ .../WidgetStagesAndEvents.component.tsx | 51 ++++++++++- .../stagesAndEvents.types.ts | 1 + .../WidgetTwoEventWorkspace.container.tsx | 86 ++++++++++++------- ...NewTrackedEntityRelationship.container.tsx | 21 ++--- ...getTrackedEntityRelationship.component.tsx | 4 + .../WidgetTrackedEntityRelationship.types.ts | 2 + .../RelationshipsWidget.component.tsx | 10 ++- .../relationshipsWidget.types.ts | 2 + .../capture-core/hooks/useEnrollmentAccess.ts | 4 + 48 files changed, 512 insertions(+), 175 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index ea04857587..0ede078384 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: 2026-05-06T12:39:03.039Z\n" -"PO-Revision-Date: 2026-05-06T12:39:03.039Z\n" +"POT-Creation-Date: 2026-05-07T15:17:32.940Z\n" +"PO-Revision-Date: 2026-05-07T15:17:32.940Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -1071,12 +1071,15 @@ msgstr "Possible duplicates found" msgid "An error occurred loading possible duplicates" msgstr "An error occurred loading possible duplicates" -msgid "program" -msgstr "program" +msgid "these program stages" +msgstr "these program stages" msgid "this program stage" msgstr "this program stage" +msgid "program" +msgstr "program" + msgid "and" msgstr "and" @@ -1086,14 +1089,23 @@ msgstr "You only have view access to {{targets}}" msgid "You only have view access to program" msgstr "You only have view access to program" +msgid "You only have view access to these program stages" +msgstr "You only have view access to these program stages" + msgid "You only have view access to this program stage" msgstr "You only have view access to this program stage" +msgid "You only have view access to enrollment" +msgstr "You only have view access to enrollment" + msgid "person" msgstr "person" -msgid "Read only" -msgstr "Read only" +msgid "View only" +msgstr "View only" + +msgid "You only have view access" +msgstr "You only have view access" msgid "You don't have access to delete this relationship" msgstr "You don't have access to delete this relationship" @@ -1620,8 +1632,8 @@ msgstr "{{trackedEntityName}} profile" msgid "Edit {{trackedEntityName}}" msgstr "Edit {{trackedEntityName}}" -msgid "Read only - {{trackedEntityName}} profile" -msgstr "Read only - {{trackedEntityName}} profile" +msgid "You only have view access to this {{trackedEntityName}}" +msgstr "You only have view access to this {{trackedEntityName}}" msgid "Change information about this {{trackedEntityName}} here." msgstr "Change information about this {{trackedEntityName}} here." @@ -1734,9 +1746,6 @@ msgstr "Please enter a date" msgid "Please select a valid event" msgstr "Please select a valid event" -msgid "You do not have access to create events in this stage" -msgstr "You do not have access to create events in this stage" - msgid "This stage can only have one event" msgstr "This stage can only have one event" @@ -1752,9 +1761,6 @@ msgstr "An error occurred while deleting the event" msgid "Are you sure you want to delete this event?" msgstr "Are you sure you want to delete this event?" -msgid "You do not have access to perform actions on this event" -msgstr "You do not have access to perform actions on this event" - msgid "An error occurred when updating event status" msgstr "An error occurred when updating event status" @@ -1897,9 +1903,6 @@ msgstr "Missing implementation step" msgid "Go back without saving relationship" msgstr "Go back without saving relationship" -msgid "You do not have access to add relationships" -msgstr "You do not have access to add relationships" - msgid "New Relationship" msgstr "New Relationship" diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.tsx b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.tsx index 3c88fb9366..267467aa2e 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.tsx +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.tsx @@ -184,6 +184,7 @@ export const EnrollmentPageDefault = () => { programWriteAccess, trackedEntityTypeWriteAccess, programStageWriteAccess, + programStageReadAccess, } = useEnrollmentAccess(programId); if (isLoading) { @@ -204,6 +205,7 @@ export const EnrollmentPageDefault = () => { programWriteAccess={programWriteAccess} trackedEntityTypeWriteAccess={trackedEntityTypeWriteAccess} programStageWriteAccess={programStageWriteAccess} + programStageReadAccess={programStageReadAccess} teiId={teiId} orgUnitId={orgUnitId} program={program} diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.ts b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.ts index a55c138ca3..57b75c1caa 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.ts +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.ts @@ -52,6 +52,7 @@ export type Props = { programWriteAccess: boolean; trackedEntityTypeWriteAccess: boolean; programStageWriteAccess: boolean; + programStageReadAccess: boolean; }; diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/PageLayout/DefaultPageLayout.constants.ts b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/PageLayout/DefaultPageLayout.constants.ts index 88c5c90724..5028187fef 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/PageLayout/DefaultPageLayout.constants.ts +++ b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/PageLayout/DefaultPageLayout.constants.ts @@ -24,6 +24,7 @@ export const DefaultPageLayout: PageLayoutConfig = { { type: WidgetTypes.COMPONENT, name: 'TrackedEntityRelationship', + settings: { readOnlyMode: true }, }, { type: WidgetTypes.COMPONENT, diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.tsx b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.tsx index ba25df16fe..4f8a8fd0f8 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.tsx +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.tsx @@ -65,6 +65,7 @@ export const EnrollmentEditEventPageComponent = ({ programWriteAccess, trackedEntityTypeWriteAccess, programStageWriteAccess, + programStageReadAccess, }: PlainProps) => ( diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.tsx b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.tsx index 2380200bdd..4a1707b659 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.tsx +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.tsx @@ -295,6 +295,7 @@ const EnrollmentEditEventPageWithContextPlain = ({ programWriteAccess, trackedEntityTypeWriteAccess, programStageWriteAccess, + programStageReadAccess, } = useEnrollmentAccess(programId); if (pageStatus === pageStatuses.LOADING) { @@ -355,6 +356,7 @@ const EnrollmentEditEventPageWithContextPlain = ({ programWriteAccess={programWriteAccess} trackedEntityTypeWriteAccess={trackedEntityTypeWriteAccess} programStageWriteAccess={programStageWriteAccess} + programStageReadAccess={programStageReadAccess} /> ); }; diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.ts b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.ts index 743f8daaa4..6fa48c77bd 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.ts +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.ts @@ -63,6 +63,7 @@ export type PlainProps = { programWriteAccess: boolean; trackedEntityTypeWriteAccess: boolean; programStageWriteAccess: boolean; + programStageReadAccess: boolean; }; export type Props = { diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/PageLayout/DefaultPageLayout.constants.ts b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/PageLayout/DefaultPageLayout.constants.ts index a005529792..262150b907 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/PageLayout/DefaultPageLayout.constants.ts +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/PageLayout/DefaultPageLayout.constants.ts @@ -60,6 +60,7 @@ export const DefaultPageLayout: PageLayoutConfig = { { type: WidgetTypes.COMPONENT, name: 'TrackedEntityRelationship', + settings: { readOnlyMode: true }, }, { type: WidgetTypes.COMPONENT, diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx index b0d7ce43bd..fbbdfe4c62 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx @@ -6,6 +6,7 @@ import { AddRelationshipRefWrapper } from './AddRelationshipRefWrapper'; import type { Props as EnrollmentPageProps } from '../../../Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types'; import { EnrollmentBreadcrumb } from '../../../../Breadcrumbs/EnrollmentBreadcrumb'; import { ReadOnlyBadge } from '../../../../ReadOnlyBadge'; +import { useProgram } from '../../../../WidgetEnrollment/hooks/useProgram'; import './enrollmentPageLayout.css'; const getEnrollmentPageStyles: Readonly = () => ({ @@ -65,9 +66,72 @@ const getEnrollmentPageStyles: Readonly = () => ({ const isValidHex = (color: string) => /^#[0-9A-F]{6}$/i.test(color); +const resolveStageBadgeAccess = ( + onEventPage: boolean, + currentStageWriteAccess: boolean, + programStageWriteAccess: boolean, + programStageReadAccess: boolean, +) => (onEventPage + ? currentStageWriteAccess + : programStageWriteAccess || !programStageReadAccess); + +type BreadcrumbBadgeProgram = { + programStages?: Array; + trackedEntityType?: { name?: string }; +}; + +type BreadcrumbBadgeProps = { + onEventPage: boolean; + currentStageWriteAccess: boolean; + programWriteAccess: boolean; + trackedEntityTypeWriteAccess: boolean; + programStageWriteAccess: boolean; + programStageReadAccess: boolean; + program: BreadcrumbBadgeProgram; +}; + +const BreadcrumbBadge = ({ + onEventPage, + currentStageWriteAccess, + programWriteAccess, + trackedEntityTypeWriteAccess, + programStageWriteAccess, + programStageReadAccess, + program, +}: BreadcrumbBadgeProps) => ( + 1} + trackedEntityName={program?.trackedEntityType?.name} + inlineLabel + /> +); + type OwnProps = EnrollmentPageProps; type Props = OwnProps & WithStyles; +const useStageAccess = ( + programId: string, + currentStageId: string | undefined, +) => { + const { program: liveProgram } = useProgram(programId); + const liveCurrentStage = currentStageId + ? liveProgram?.programStages?.find((s: any) => s.id === currentStageId) + : undefined; + const currentStageWriteAccess = liveCurrentStage + ? Boolean(liveCurrentStage?.access?.data?.write) + : true; + return { currentStageWriteAccess }; +}; + const EnrollmentPageLayoutPlain = ({ pageLayout, availableWidgets, @@ -81,6 +145,7 @@ const EnrollmentPageLayoutPlain = ({ programWriteAccess, trackedEntityTypeWriteAccess, programStageWriteAccess, + programStageReadAccess, classes, ...passOnProps }: Props) => { @@ -89,6 +154,14 @@ const EnrollmentPageLayoutPlain = ({ useState(undefined); const toggleVisibility = useCallback(() => setMainContentVisibility(current => !current), []); + const currentStageId = (passOnProps as any).stageId as string | undefined; + const { currentStageWriteAccess } = useStageAccess(program.id, currentStageId); + const onEventPage = Boolean(currentStageId); + const allWriteAccessMissing = !programWriteAccess + && !trackedEntityTypeWriteAccess + && !programStageWriteAccess; + const hideWidgetReadOnlyBadge = onEventPage || allWriteAccessMissing; + const allProps = useMemo(() => ({ ...passOnProps, program, @@ -99,6 +172,8 @@ const EnrollmentPageLayoutPlain = ({ programWriteAccess, trackedEntityTypeWriteAccess, programStageWriteAccess, + programStageReadAccess, + hideEventStageBadge: hideWidgetReadOnlyBadge, }), [ addRelationShipContainerElement, currentPage, @@ -108,6 +183,8 @@ const EnrollmentPageLayoutPlain = ({ programWriteAccess, trackedEntityTypeWriteAccess, programStageWriteAccess, + programStageReadAccess, + hideWidgetReadOnlyBadge, toggleVisibility, ]); @@ -147,11 +224,14 @@ const EnrollmentPageLayoutPlain = ({ userInteractionInProgress={userInteractionInProgress} eventStatus={eventStatus} /> -
diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts index 3a6072f0cc..005d79edfe 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts @@ -39,8 +39,7 @@ import { export const QuickActions: WidgetConfig = { Component: EnrollmentQuickActions, - shouldHideWidget: ({ stages }: any) => - !stages?.some((stage: any) => stage?.dataAccess?.write), + shouldHideWidget: ({ programStageWriteAccess }: any) => !programStageWriteAccess, getProps: ({ stages, events, ruleEffects }: any) => ({ stages, events, @@ -61,6 +60,7 @@ export const StagesAndEvents: WidgetConfig = { onRollbackDeleteEvent, onEventClick, ruleEffects, + hideEventStageBadge, }: any): StagesAndEventProps => ({ programId: program.id, stages, @@ -72,12 +72,16 @@ export const StagesAndEvents: WidgetConfig = { onRollbackDeleteEvent, onEventClick, ruleEffects, + hideReadOnlyBadge: Boolean(hideEventStageBadge), }), }; export const TrackedEntityRelationship: WidgetConfig = { Component: TrackedEntityRelationshipsWrapper, shouldHideWidget: ({ addRelationShipContainerElement }: any) => !addRelationShipContainerElement, + getCustomSettings: ({ readOnlyMode }: any) => ({ + readOnlyMode, + }), getProps: ({ program, orgUnitId, @@ -88,6 +92,7 @@ export const TrackedEntityRelationship: WidgetConfig = { programWriteAccess, trackedEntityTypeWriteAccess, programStageWriteAccess, + hideEventStageBadge, }: any): TrackedEntityRelationshipProps => ({ trackedEntityTypeId: program.trackedEntityType.id, programId: program.id, @@ -99,6 +104,7 @@ export const TrackedEntityRelationship: WidgetConfig = { onLinkedRecordClick, readOnly: !trackedEntityTypeWriteAccess, hideButton: !programWriteAccess && !trackedEntityTypeWriteAccess && !programStageWriteAccess, + hideReadOnlyBadge: Boolean(hideEventStageBadge), }), }; @@ -136,7 +142,9 @@ export const IndicatorWidget: WidgetConfig = { export const EnrollmentNote: WidgetConfig = { Component: WidgetEnrollmentNote, - getProps: () => ({}), + getProps: ({ hideEventStageBadge }: any) => ({ + hideReadOnlyBadge: Boolean(hideEventStageBadge), + }), }; export const ProfileWidget: WidgetConfig = { @@ -207,6 +215,7 @@ export const EnrollmentWidget: WidgetConfig = { onUpdateEnrollmentStatusError, onEnrollmentError, onAccessLostFromTransfer, + hideEventStageBadge, }: any): WidgetEnrollmentProps => ({ teiId, enrollmentId, @@ -221,6 +230,7 @@ export const EnrollmentWidget: WidgetConfig = { externalData: { status: widgetEnrollmentStatus, events }, onError: onEnrollmentError, onAccessLostFromTransfer, + hideReadOnlyBadge: Boolean(hideEventStageBadge), }), }; @@ -297,10 +307,12 @@ export const AssigneeWidget: WidgetConfig = { export const EventNote: WidgetConfig = { Component: WidgetEventNote, - getProps: ({ dataEntryKey, dataEntryId, programStage }: any) => ({ + getProps: ({ dataEntryKey, dataEntryId, program, stageId, programStage, hideEventStageBadge }: any) => ({ dataEntryKey, dataEntryId, - stageWriteAccess: Boolean(programStage?.stageForm?.access?.data?.write), + programId: program?.id, + stageId: stageId ?? programStage?.id, + hideReadOnlyBadge: Boolean(hideEventStageBadge), }), }; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.tsx b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.tsx index 4bbaa4f30e..37f363142f 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.tsx +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.tsx @@ -33,8 +33,11 @@ export const TrackedEntityRelationshipsWrapper = ({ onCloseAddRelationship, onLinkedRecordClick, readOnly, + readOnlyMode, hideButton, + hideReadOnlyBadge, }: Props) => { + const effectiveReadOnly = Boolean(readOnlyMode) || readOnly; const dispatch = useDispatch(); const { relationshipTypes, isError } = useTEIRelationshipsWidgetMetadata(); const { orgUnit } = useCoreOrgUnit(orgUnitId); @@ -76,8 +79,10 @@ export const TrackedEntityRelationshipsWrapper = ({ onCloseAddRelationship={onCloseAddRelationship} onSelectFindMode={onSelectFindMode} relationshipTypes={relationshipTypes} - readOnly={readOnly} + readOnly={effectiveReadOnly} + accessReadOnly={readOnly} hideButton={hideButton} + hideReadOnlyBadge={hideReadOnlyBadge} renderTrackedEntityRegistration={( selectedTrackedEntityTypeId, suggestedProgramId, diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types.ts b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types.ts index 98d2f02bae..e3b12111a9 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types.ts +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types.ts @@ -10,5 +10,8 @@ export type Props = { onCloseAddRelationship: () => void; onLinkedRecordClick: LinkedRecordClick; readOnly: boolean; + readOnlyMode?: boolean; hideButton?: boolean; + accessReadOnly?: boolean; + hideReadOnlyBadge?: boolean; }; diff --git a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx index 93b56ab233..75364a1809 100644 --- a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx +++ b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx @@ -8,23 +8,29 @@ type Props = { programWriteAccess?: boolean; trackedEntityTypeWriteAccess?: boolean; programStageWriteAccess?: boolean; + multipleStages?: boolean; trackedEntityName?: string; label?: string; + inlineLabel?: boolean; }; const interpolate = (key: string, trackedEntityName: string) => i18n.t(key, { trackedEntityName, interpolation: { escapeValue: false } }); +const stageTarget = (multipleStages: boolean) => + (multipleStages ? i18n.t('these program stages') : i18n.t('this program stage')); + const getMultiMissingLabel = ( missingProgram: boolean, missingTET: boolean, missingStage: boolean, + multipleStages: boolean, trackedEntityName: string, ): string | null => { const parts: Array = []; if (missingProgram) parts.push(i18n.t('program')); if (missingTET) parts.push(trackedEntityName); - if (missingStage) parts.push(i18n.t('this program stage')); + if (missingStage) parts.push(stageTarget(multipleStages)); if (parts.length < 2) return null; const last = parts.pop() as string; const joined = `${parts.join(', ')} ${i18n.t('and')} ${last}`; @@ -38,6 +44,7 @@ const getSingleMissingLabel = ( missingProgram: boolean, missingTET: boolean, missingStage: boolean, + multipleStages: boolean, trackedEntityName: string, ): string | null => { if (missingProgram) return i18n.t('You only have view access to program'); @@ -47,7 +54,11 @@ const getSingleMissingLabel = ( trackedEntityName, ); } - if (missingStage) return i18n.t('You only have view access to this program stage'); + if (missingStage) { + return multipleStages + ? i18n.t('You only have view access to these program stages') + : i18n.t('You only have view access to this program stage'); + } return null; }; @@ -55,13 +66,15 @@ const getDefaultLabel = ( programWriteAccess: boolean, trackedEntityTypeWriteAccess: boolean, programStageWriteAccess: boolean, + multipleStages: boolean, trackedEntityName: string, ): string | null => { const mp = !programWriteAccess; const mt = !trackedEntityTypeWriteAccess; const ms = !programStageWriteAccess; - return getMultiMissingLabel(mp, mt, ms, trackedEntityName) - ?? getSingleMissingLabel(mp, mt, ms, trackedEntityName); + if (mp && mt && ms) return i18n.t('You only have view access to enrollment'); + return getMultiMissingLabel(mp, mt, ms, multipleStages, trackedEntityName) + ?? getSingleMissingLabel(mp, mt, ms, multipleStages, trackedEntityName); }; export const ReadOnlyBadge = ({ @@ -69,25 +82,56 @@ export const ReadOnlyBadge = ({ programWriteAccess = true, trackedEntityTypeWriteAccess = true, programStageWriteAccess = true, + multipleStages = false, trackedEntityName, label, + inlineLabel = false, }: Props) => { const allAccessMissing = !programWriteAccess && !trackedEntityTypeWriteAccess && !programStageWriteAccess; const isReadOnly = readOnly || allAccessMissing; if (!isReadOnly) return null; - const tooltipContent = label + const message = label ?? getDefaultLabel( programWriteAccess, trackedEntityTypeWriteAccess, programStageWriteAccess, + multipleStages, trackedEntityName ?? i18n.t('person'), ); + if (inlineLabel) { + return ( + + + + + + {i18n.t('View only')} + {' - '} + {message ?? i18n.t('You only have view access')} + + + ); + } return ( - + }> - {i18n.t('Read only')} + {i18n.t('View only')} ); diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/Status/Status.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/Status/Status.component.tsx index cf76fbb02b..1e0eef37ab 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/Status/Status.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/Status/Status.component.tsx @@ -13,7 +13,7 @@ const styles = { export const StatusPlain = ({ status = '', classes }: Props & WithStyles) => ( {translatedStatus[status] ?? status} diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx index 8889e9a059..daf4bf83e2 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx @@ -58,6 +58,8 @@ const WidgetEnrollmentPlain = ({ initError, loading, canAddNew, + readOnlyMode, + hideReadOnlyBadge, programDataWriteAccess, trackedEntityTypeDataWriteAccess, displayAutoGeneratedEventWarning, @@ -72,6 +74,7 @@ const WidgetEnrollmentPlain = ({ onUpdateEnrollmentStatusSuccess, onAccessLostFromTransfer, }: PlainProps & WithStyles) => { + const enrollmentReadOnly = readOnlyMode || !programDataWriteAccess; const [open, setOpenStatus] = useState(true); const { fromServerDate } = useTimeZoneConversion(); const updatedAtDateTime: string = convertValue( @@ -90,14 +93,16 @@ const WidgetEnrollmentPlain = ({ header={
{i18n.t('Enrollment')} -
- -
+ {!hideReadOnlyBadge && ( +
+ +
+ )}
} onOpen={useCallback(() => setOpenStatus(true), [setOpenStatus])} @@ -126,8 +131,8 @@ const WidgetEnrollmentPlain = ({ date={enrollment.enrolledAt} dateLabel={getEnrollmentDateLabel(program)} locale={locale} - readOnly={!programDataWriteAccess} - hideEdit={!programDataWriteAccess} + readOnly={enrollmentReadOnly} + hideEdit={enrollmentReadOnly} displayAutoGeneratedEventWarning={displayAutoGeneratedEventWarning} onSave={updateEnrollmentDate} allowFutureDate={program.selectEnrollmentDatesInFuture} @@ -140,8 +145,8 @@ const WidgetEnrollmentPlain = ({ date={enrollment.occurredAt} dateLabel={getIncidentDateLabel(program)} locale={locale} - readOnly={!programDataWriteAccess} - hideEdit={!programDataWriteAccess} + readOnly={enrollmentReadOnly} + hideEdit={enrollmentReadOnly} displayAutoGeneratedEventWarning={displayAutoGeneratedEventWarning} onSave={updateIncidentDate} allowFutureDate={program.selectIncidentDatesInFuture} @@ -188,13 +193,13 @@ const WidgetEnrollmentPlain = ({ refetchEnrollment={refetchEnrollment} refetchTEI={refetchTEI} onError={onError} - readOnly={!programDataWriteAccess} - hideEdit={!programDataWriteAccess} + readOnly={enrollmentReadOnly} + hideEdit={enrollmentReadOnly} />
)} > | null }; onDelete: () => void; onAddNew: () => void; @@ -54,6 +55,7 @@ export type PlainProps = { loading: boolean; canAddNew: boolean; readOnlyMode: boolean; + hideReadOnlyBadge?: boolean; programDataWriteAccess: boolean; trackedEntityTypeDataWriteAccess: boolean; displayAutoGeneratedEventWarning: boolean; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx index 5521425ab1..e15d6ce643 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx @@ -6,7 +6,11 @@ import { WidgetNote } from '../WidgetNote'; import { useProgram } from '../WidgetEnrollment/hooks/useProgram'; import { useLocationQuery } from '../../utils/routing'; -export const WidgetEnrollmentNote = () => { +type Props = { + hideReadOnlyBadge?: boolean; +}; + +export const WidgetEnrollmentNote = ({ hideReadOnlyBadge }: Props) => { const dispatch = useDispatch(); const { enrollmentId, programId } = useLocationQuery(); const { program } = useProgram(programId); @@ -29,6 +33,7 @@ export const WidgetEnrollmentNote = () => { onAddNote={onAddNote} readOnly={!programWriteAccess} programWriteAccess={programWriteAccess} + hideReadOnlyBadge={hideReadOnlyBadge} />
); diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/WidgetHeader/WidgetHeader.container.tsx b/src/core_modules/capture-core/components/WidgetEventEdit/WidgetHeader/WidgetHeader.container.tsx index 0bf662be38..3339bb5b94 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/WidgetHeader/WidgetHeader.container.tsx +++ b/src/core_modules/capture-core/components/WidgetEventEdit/WidgetHeader/WidgetHeader.container.tsx @@ -8,6 +8,7 @@ import { FEATURES, useFeature } from 'capture-core-utils'; import { useAuthorities } from 'capture-core/utils/authority/useAuthorities'; import { ConditionalTooltip } from 'capture-core/components/Tooltips/ConditionalTooltip'; import { useEnrollmentEditEventPageMode, useProgramExpiryForUser } from 'capture-core/hooks'; +import { useProgram } from '../../WidgetEnrollment/hooks/useProgram'; import { startShowEditEventDataEntry } from '../WidgetEventEdit.actions'; import { NonBundledDhis2Icon } from '../../NonBundledDhis2Icon'; import { dataElementTypes, getProgramEventAccess } from '../../../metaData'; @@ -38,6 +39,16 @@ const styles: Readonly = { type Props = PlainProps & WithStyles; +const useLiveEventAccess = (programId: string, stageId: string) => { + const cachedEventAccess = getProgramEventAccess(programId, stageId); + const { program } = useProgram(programId); + const liveStage = program?.programStages?.find((s: any) => s.id === stageId); + const liveStageWriteAccess = liveStage ? Boolean(liveStage?.access?.data?.write) : undefined; + return liveStageWriteAccess === undefined + ? cachedEventAccess + : { ...cachedEventAccess, write: liveStageWriteAccess }; +}; + const WidgetHeaderPlain = ({ eventStatus, stage, @@ -54,7 +65,7 @@ const WidgetHeaderPlain = ({ const { currentPageMode } = useEnrollmentEditEventPageMode(eventStatus); const [actionsIsOpen, setActionsIsOpen] = useState(false); - const eventAccess = getProgramEventAccess(programId, stage.id); + const eventAccess = useLiveEventAccess(programId, stage.id); const { hasAuthority } = useAuthorities({ authorities: ['F_UNCOMPLETE_EVENT'] }); const blockEntryForm = stage.blockEntryForm && !hasAuthority && eventStatus === eventStatuses.COMPLETED; @@ -100,22 +111,24 @@ const WidgetHeaderPlain = ({
{currentPageMode === dataEntryKeys.VIEW && (
- - - + + + )} {supportsChangelog && ( { +export const WidgetEventNote = ({ dataEntryKey, dataEntryId, programId, stageId, hideReadOnlyBadge }: Props) => { const dispatch = useDispatch(); + const { program } = useProgram(programId ?? ''); + const liveStage = program?.programStages?.find((s: any) => s.id === stageId); + const stageWriteAccess = program ? Boolean(liveStage?.access?.data?.write) : true; + const notes = useSelector(({ dataEntriesNotes }: { dataEntriesNotes: Record }) => dataEntriesNotes[`${dataEntryId}-${dataEntryKey}`] ?? []); @@ -24,6 +29,7 @@ export const WidgetEventNote = ({ dataEntryKey, dataEntryId, stageWriteAccess = onAddNote={onAddNote} readOnly={!stageWriteAccess} programStageWriteAccess={stageWriteAccess} + hideReadOnlyBadge={hideReadOnlyBadge} />
); diff --git a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.types.ts b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.types.ts index 8476226fc9..1ce0910d0e 100644 --- a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.types.ts +++ b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.types.ts @@ -1,7 +1,9 @@ export type Props = { dataEntryKey: string; dataEntryId: string; - stageWriteAccess?: boolean; + programId?: string; + stageId?: string; + hideReadOnlyBadge?: boolean; }; export type ClientNote = { diff --git a/src/core_modules/capture-core/components/WidgetNote/WidgetNote.component.tsx b/src/core_modules/capture-core/components/WidgetNote/WidgetNote.component.tsx index 76faed034f..c4587b898a 100644 --- a/src/core_modules/capture-core/components/WidgetNote/WidgetNote.component.tsx +++ b/src/core_modules/capture-core/components/WidgetNote/WidgetNote.component.tsx @@ -13,6 +13,7 @@ export const WidgetNote = ({ trackedEntityTypeWriteAccess, programStageWriteAccess, trackedEntityName, + hideReadOnlyBadge, ...passOnProps }: Props) => { const [open, setOpenStatus] = useState(true); @@ -22,15 +23,17 @@ export const WidgetNote = ({ header={
{title} {notes.length > 0 && } -
- -
+ {!hideReadOnlyBadge && ( +
+ +
+ )}
} onOpen={useCallback(() => setOpenStatus(true), [setOpenStatus])} onClose={useCallback(() => setOpenStatus(false), [setOpenStatus])} diff --git a/src/core_modules/capture-core/components/WidgetNote/WidgetNote.types.ts b/src/core_modules/capture-core/components/WidgetNote/WidgetNote.types.ts index bad9bba800..10f15a3ba8 100644 --- a/src/core_modules/capture-core/components/WidgetNote/WidgetNote.types.ts +++ b/src/core_modules/capture-core/components/WidgetNote/WidgetNote.types.ts @@ -16,4 +16,5 @@ export type Props = { trackedEntityTypeWriteAccess?: boolean; programStageWriteAccess?: boolean; trackedEntityName?: string; + hideReadOnlyBadge?: boolean; }; diff --git a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.component.tsx b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.component.tsx index f16f1b3210..706250304a 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.component.tsx +++ b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.component.tsx @@ -23,6 +23,7 @@ export const DataEntryComponent = ({ orgUnitId, pluginContext, readOnly, + accessReadOnly, }: PlainProps) => ( @@ -37,11 +38,12 @@ export const DataEntryComponent = ({ }
diff --git a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.container.tsx b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.container.tsx index c39bbc995f..d99d83c60c 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.container.tsx +++ b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.container.tsx @@ -33,6 +33,7 @@ export const DataEntry = ({ trackedEntityName, dataEntryFormConfig, readOnly, + accessReadOnly, }: Props) => { const dataEntryId = 'trackedEntityProfile'; const itemId = 'edit'; @@ -165,6 +166,7 @@ export const DataEntry = ({ orgUnitId={orgUnitId} pluginContext={pluginContext} readOnly={readOnly} + accessReadOnly={accessReadOnly} /> ) ); diff --git a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/dataEntry.types.ts b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/dataEntry.types.ts index fe1acc3741..90f3dee1ee 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/dataEntry.types.ts +++ b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/dataEntry.types.ts @@ -22,6 +22,7 @@ export type PlainProps = { orgUnitId: string; pluginContext?: PluginContext; readOnly?: boolean; + accessReadOnly?: boolean; }; export type Props = { @@ -41,4 +42,5 @@ export type Props = { userRoles: Array; trackedEntityName: string; readOnly?: boolean; + accessReadOnly?: boolean; }; diff --git a/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/Delete/DeleteMenuItem/DeleteMenuItem.component.tsx b/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/Delete/DeleteMenuItem/DeleteMenuItem.component.tsx index 3c8b8eae25..b3d954d244 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/Delete/DeleteMenuItem/DeleteMenuItem.component.tsx +++ b/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/Delete/DeleteMenuItem/DeleteMenuItem.component.tsx @@ -24,6 +24,10 @@ export const DeleteMenuItem = ({ const disabled = useMemo(() => !canWriteData || !canCascadeDeleteTei, [canWriteData, canCascadeDeleteTei]); const tooltipContent = getTooltipContent(disabled, trackedEntityTypeName); + if (!canWriteData) { + return null; + } + return ( diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.container.tsx b/src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.container.tsx index 087b2a5081..2e87d052d5 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.container.tsx +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.container.tsx @@ -14,6 +14,7 @@ import { } from './hooks'; import { relatedStageStatus } from './constants'; import { useCommonEnrollmentDomainData } from '../Pages/common/EnrollmentOverviewDomain'; +import { useProgram } from '../WidgetEnrollment/hooks/useProgram'; import type { RequestEvent } from '../DataEntries'; const styles = { @@ -49,6 +50,9 @@ export const WidgetRelatedStagesPlain = ({ const [isLinking, setIsLinking] = useState(false); const { enrollment } = useCommonEnrollmentDomainData(teiId, enrollmentId, programId); const { currentRelatedStagesStatus } = useRelatedStages({ programStageId, programId }); + const { program } = useProgram(programId); + const liveStage = program?.programStages?.find((s: any) => s.id === programStageId); + const stageWriteAccess = Boolean(liveStage?.access?.data?.write); const { linkedEvent, isLoading: isLinkedEventLoading, @@ -106,6 +110,10 @@ export const WidgetRelatedStagesPlain = ({ return null; } + if (program && !stageWriteAccess) { + return null; + } + return ( ) => { const [open, setOpenStatus] = useState(true); const { id, name, icon, description, dataElements, hideDueDate, repeatable, enableUserAssignment } = stage; const preventAddingNewEvents = rulesEffectHideProgramStage(ruleEffects, id); const hideProgramStage = preventAddingNewEvents && events.length === 0; + const effectiveStageWriteAccess = stageWriteAccess ?? stage.dataAccess.write; const handleOpen = useCallback(() => setOpenStatus(true), [setOpenStatus]); const handleClose = useCallback(() => setOpenStatus(false), [setOpenStatus]); @@ -49,6 +50,8 @@ export const StagePlain = ({ icon={icon} description={description} events={events} + stageWriteAccess={effectiveStageWriteAccess} + hideReadOnlyBadge={hideReadOnlyBadge} />} onOpen={handleOpen} onClose={handleClose} @@ -62,14 +65,15 @@ export const StagePlain = ({ hideDueDate={hideDueDate} repeatable={repeatable} enableUserAssignment={enableUserAssignment} + stageWriteAccess={effectiveStageWriteAccess} onCreateNew={onCreateNew} hiddenProgramStage={preventAddingNewEvents} {...passOnProps} - /> : ( + /> : effectiveStageWriteAccess && (
onCreateNew(id)} - stageWriteAccess={stage.dataAccess.write} + stageWriteAccess={effectiveStageWriteAccess} eventCount={events.length} repeatable={repeatable} preventAddingEventActionInEffect={preventAddingNewEvents} diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageCreateNewButton/StageCreateNewButton.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageCreateNewButton/StageCreateNewButton.tsx index 0a503dc2dd..9202f1c7ca 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageCreateNewButton/StageCreateNewButton.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageCreateNewButton/StageCreateNewButton.tsx @@ -21,15 +21,6 @@ export const StageCreateNewButton = ({ eventName, }: Props) => { const { isDisabled, tooltipContent } = useMemo(() => { - if (!stageWriteAccess) { - return ({ - isDisabled: true, - tooltipContent: i18n.t('You do not have access to create events in this stage', { - programStageName: eventName, - interpolation: { escapeValue: false }, - }), - }); - } if (preventAddingEventActionInEffect) { return { isDisabled: true, @@ -49,7 +40,11 @@ export const StageCreateNewButton = ({ isDisabled: false, tooltipContent: '', }; - }, [eventCount, eventName, preventAddingEventActionInEffect, repeatable, stageWriteAccess]); + }, [eventCount, eventName, preventAddingEventActionInEffect, repeatable]); + + if (!stageWriteAccess) { + return null; + } return ( - <> - + {stageWriteAccess && ( + <> setActionsOpen(prev => !prev)} @@ -73,7 +68,7 @@ const EventRowPlain = ({ secondary small icon={} - disabled={pendingApiResponse || !stageWriteAccess} + disabled={pendingApiResponse} component={( )} /> - - {deleteModalOpen && ( - - )} - + {deleteModalOpen && ( + + )} + + )} ); diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.tsx index 2e4b4a63f2..0a1ef853bc 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.tsx @@ -92,6 +92,7 @@ const StageDetailPlain = (props: Props & WithStyles) => { hideDueDate = false, repeatable = false, enableUserAssignment = false, + stageWriteAccess: stageWriteAccessProp, onEventClick, onDeleteEvent, onUpdateEventStatus, @@ -106,6 +107,7 @@ const StageDetailPlain = (props: Props & WithStyles) => { sortDirection: SORT_DIRECTION.DESC, }; const { stage } = getProgramAndStageForProgram(programId, stageId); + const stageWriteAccess = stageWriteAccessProp ?? stage?.access?.data?.write; const headerColumns = useComputeHeaderColumn(dataElements, hideDueDate, enableUserAssignment, stage?.stageForm); const dataElementsClient = useClientDataElements(dataElements); const { loading, value: dataSource, error } = useComputeDataFromEvent(dataElementsClient, events); @@ -219,7 +221,7 @@ const StageDetailPlain = (props: Props & WithStyles) => { pendingApiResponse={row.pendingApiResponse as boolean} eventDetails={eventDetails} teiId={eventDetails.trackedEntity} - stageWriteAccess={stage?.access?.data?.write} + stageWriteAccess={stageWriteAccess} programId={programId} enrollmentId={eventDetails.enrollment} cells={cells} @@ -254,18 +256,18 @@ const StageDetailPlain = (props: Props & WithStyles) => { onClick={handleViewAll} >{i18n.t('Go to full {{ eventName }}', { eventName, interpolation: { escapeValue: false } })} : null); - const renderCreateNewButton = () => ( + const renderCreateNewButton = () => (stageWriteAccess ? (
- ); + ) : null); return (
@@ -284,6 +286,10 @@ const StageDetailPlain = (props: Props & WithStyles) => {
); } + const showMoreVisible = Boolean(dataSource && !loading + && events.length > DEFAULT_NUMBER_OF_ROW + && displayedRowNumber < events.length); + const showFooter = showMoreVisible || stageWriteAccess; return (
@@ -298,11 +304,13 @@ const StageDetailPlain = (props: Props & WithStyles) => {
- -
- {renderFooter()} -
-
+ {showFooter && ( + +
+ {renderFooter()} +
+
+ )}
); }; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/stageDetail.types.ts b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/stageDetail.types.ts index aa93f0fbcb..4262e7ebdc 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/stageDetail.types.ts +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/stageDetail.types.ts @@ -9,6 +9,7 @@ type ExtractedProps = { repeatable?: boolean; enableUserAssignment?: boolean; stageId: string; + stageWriteAccess?: boolean; onCreateNew: (stageId: string) => void; onDeleteEvent: (eventId: string) => void; onUpdateEventStatus: (eventId: string, status: string) => void; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.tsx index f0cb8dbbc3..94b3f92f2b 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.tsx @@ -10,6 +10,7 @@ import i18n from '@dhis2/d2-i18n'; import moment from 'moment'; import { statusTypes } from 'capture-core/events/statusTypes'; import { NonBundledDhis2Icon } from '../../../../NonBundledDhis2Icon'; +import { ReadOnlyBadge } from '../../../../ReadOnlyBadge'; import type { Props } from './stageOverview.types'; import { isEventOverdue } from '../StageDetail/hooks/helpers'; import { convertValue as convertValueClientToView } from '../../../../../converters/clientToView'; @@ -90,7 +91,9 @@ const getLastUpdatedAt = (events: Array, fromServerDate: (da return null; }; -export const StageOverviewPlain = ({ title, icon, description, events, classes }: Props & WithStyles) => { +export const StageOverviewPlain = ({ + title, icon, description, events, stageWriteAccess = true, hideReadOnlyBadge = false, classes, +}: Props & WithStyles) => { const { fromServerDate } = useTimeZoneConversion(); const totalEvents = events.length; const overdueEvents = events.filter(isEventOverdue).length; @@ -154,6 +157,12 @@ export const StageOverviewPlain = ({ title, icon, description, events, classes }
{getLastUpdatedAt(events, fromServerDate)}
} + {!hideReadOnlyBadge && ( + + )} ); }; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/stageOverview.types.ts b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/stageOverview.types.ts index 7fceabadc9..0b06ff720c 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/stageOverview.types.ts +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/stageOverview.types.ts @@ -6,4 +6,6 @@ export type Props = { events: Array; icon?: Icon; description?: string | null; + stageWriteAccess?: boolean; + hideReadOnlyBadge?: boolean; }; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/stage.types.ts b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/stage.types.ts index f1f0b30787..f9c79fc432 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/stage.types.ts +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/stage.types.ts @@ -4,6 +4,8 @@ import type { Stage, StageCommonProps } from '../../types/common.types'; type ExtractedProps = { programId: string; stage: Stage; + stageWriteAccess?: boolean; + hideReadOnlyBadge?: boolean; events: Array; onEventClick: (eventId: string) => void; onDeleteEvent: (eventId: string) => void; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stages.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stages.component.tsx index e29d6be26f..11b485a3e1 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stages.component.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stages.component.tsx @@ -16,8 +16,23 @@ const emptyStateStyle = { marginBottom: spacersNum.dp12, }; -export const StagesPlain = ({ stages, events, ...passOnProps }: PlainProps) => { - const readableStages = useMemo(() => stages.filter(stage => stage.dataAccess.read), [stages]); +export const StagesPlain = ({ + stages, + events, + stageWriteAccessById, + stageReadAccessById, + programLoaded, + hideReadOnlyBadge, + ...passOnProps +}: PlainProps) => { + const readableStages = useMemo( + () => stages.filter((stage) => { + const liveRead = stageReadAccessById?.[stage.id]; + if (programLoaded && liveRead !== undefined) return liveRead; + return stage.dataAccess.read; + }), + [stages, stageReadAccessById, programLoaded], + ); const eventsByStage = useMemo( () => stages.reduce( (acc, stage) => { @@ -56,6 +71,8 @@ export const StagesPlain = ({ stages, events, ...passOnProps }: PlainProps) => { events={eventsByStage[stage.id]} key={stage.id} stage={stage} + stageWriteAccess={stageWriteAccessById?.[stage.id] ?? stage.dataAccess.write} + hideReadOnlyBadge={hideReadOnlyBadge} {...passOnProps} /> )) diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/stages.types.ts b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/stages.types.ts index 581b141919..43049cf74f 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/stages.types.ts +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/stages.types.ts @@ -4,6 +4,10 @@ import type { Stage, StageCommonProps } from '../types/common.types'; export type PlainProps = { stages: Array; events: Array; + stageWriteAccessById?: Record; + stageReadAccessById?: Record; + programLoaded?: boolean; + hideReadOnlyBadge?: boolean; onEventClick: (eventId: string) => void; onDeleteEvent: (eventId: string) => void; onUpdateEventStatus: (eventId: string, status: string) => void; @@ -13,6 +17,10 @@ export type PlainProps = { export type InputProps = { stages?: Array; events?: Array | null; + stageWriteAccessById?: Record; + stageReadAccessById?: Record; + programLoaded?: boolean; + hideReadOnlyBadge?: boolean; onEventClick: (eventId: string) => void; onDeleteEvent: (eventId: string) => void; onUpdateEventStatus: (eventId: string, status: string) => void; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx index 93cb722716..9d04598fff 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx @@ -1,18 +1,58 @@ -import React, { useState, useCallback } from 'react'; +import React, { useState, useCallback, useMemo } from 'react'; import i18n from '@dhis2/d2-i18n'; import { Widget } from '../Widget'; +import { ReadOnlyBadge } from '../ReadOnlyBadge'; import { Stages } from './Stages'; +import { useProgram } from '../WidgetEnrollment/hooks/useProgram'; import type { Props } from './stagesAndEvents.types'; -export const WidgetStagesAndEvents = ({ className, stages, events, ...passOnProps }: Props) => { +export const WidgetStagesAndEvents = ({ + className, + stages, + events, + programId, + hideReadOnlyBadge, + ...passOnProps +}: Props) => { const [open, setOpenStatus] = useState(true); + const { program } = useProgram(programId); + const stageWriteAccessById = useMemo(() => { + const map: Record = {}; + (program?.programStages ?? []).forEach((stage: any) => { + map[stage.id] = Boolean(stage?.access?.data?.write); + }); + return map; + }, [program]); + const stageReadAccessById = useMemo(() => { + const map: Record = {}; + (program?.programStages ?? []).forEach((stage: any) => { + map[stage.id] = Boolean(stage?.access?.data?.read); + }); + return map; + }, [program]); + const anyStageWriteAccess = Object.values(stageWriteAccessById).some(Boolean); + const anyStageReadAccess = Object.values(stageReadAccessById).some(Boolean); + return (
+ {i18n.t('Stages and Events')} + {!hideReadOnlyBadge && ( +
+ 1} + /> +
+ )} +
+ } onOpen={useCallback(() => setOpenStatus(true), [setOpenStatus])} onClose={useCallback(() => setOpenStatus(false), [setOpenStatus])} open={open} @@ -21,6 +61,11 @@ export const WidgetStagesAndEvents = ({ className, stages, events, ...passOnProp stages={stages} ready={events !== undefined && stages !== undefined} events={events} + programId={programId} + stageWriteAccessById={stageWriteAccessById} + stageReadAccessById={stageReadAccessById} + programLoaded={Boolean(program)} + hideReadOnlyBadge={hideReadOnlyBadge || !anyStageWriteAccess} {...passOnProps} /> diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/stagesAndEvents.types.ts b/src/core_modules/capture-core/components/WidgetStagesAndEvents/stagesAndEvents.types.ts index ac9dc3e5c2..c264c27ef2 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/stagesAndEvents.types.ts +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/stagesAndEvents.types.ts @@ -9,6 +9,7 @@ type ExtractedProps = { onUpdateEventStatus: (eventId: string, status: string) => void; onRollbackDeleteEvent: (eventId: ApiEnrollmentEvent) => void; className?: string; + hideReadOnlyBadge?: boolean; }; export type Props = ExtractedProps & StageCommonProps; diff --git a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/WidgetTwoEventWorkspace.container.tsx b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/WidgetTwoEventWorkspace.container.tsx index 796a7ac8e1..f1b5a0864a 100644 --- a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/WidgetTwoEventWorkspace.container.tsx +++ b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/WidgetTwoEventWorkspace.container.tsx @@ -8,12 +8,56 @@ import { WidgetTwoEventWorkspaceComponent } from './WidgetTwoEventWorkspace.comp import { useClientDataValues } from './hooks/useClientDataValues'; import { WidgetWrapper } from './WidgetWrapper'; import { WidgetHeader } from './WidgetHeader'; +import { useProgram } from '../WidgetEnrollment/hooks/useProgram'; + +const stageHasWriteAccess = (program: any, id: string | undefined) => + Boolean(program?.programStages?.find((s: any) => s.id === id)?.access?.data?.write); + +const useTwoEventWorkspaceData = (eventId: string, programId: string, fallbackStageId: string | undefined) => { + const { program } = useProgram(programId); + + const linkedEventQuery = useLinkedEventByOriginId({ originEventId: eventId }); + const { linkedEvent, dataValues } = linkedEventQuery; + + const metadataQuery = useMetadataForProgramStage({ + programId, + stageId: linkedEvent?.programStage, + }); + const { formFoundation, stage: linkedStage } = metadataQuery; + + const clientValuesQuery = useClientDataValues({ + linkedEventId: linkedEvent?.event, + dataValues, + formFoundation, + }); + + const isLoading = linkedEventQuery.isLoading || metadataQuery.isLoading || clientValuesQuery.isLoading; + const isError = linkedEventQuery.isError || metadataQuery.isError || clientValuesQuery.isError; + const missingData = !linkedEvent || !formFoundation || !linkedStage; + const accessBlocked = Boolean(program) + && (!stageHasWriteAccess(program, fallbackStageId) || !stageHasWriteAccess(program, linkedEvent?.programStage)); + + return { + program, + linkedEvent, + linkedStage, + formFoundation, + relationship: linkedEventQuery.relationship, + relationshipType: linkedEventQuery.relationshipType, + clientValuesWithSubValues: clientValuesQuery.clientValuesWithSubValues, + isLoading, + isError, + missingData, + accessBlocked, + }; +}; export const WidgetTwoEventWorkspace = ({ eventId, programId, orgUnitId, currentPage, + stageId, stage, type, onDeleteEvent, @@ -21,48 +65,26 @@ export const WidgetTwoEventWorkspace = ({ }: Props) => { const { linkedEvent, - dataValues, + linkedStage, + formFoundation, relationship, relationshipType, - isError: isLinkedEventError, - isLoading: isLinkedEventLoading, - } = useLinkedEventByOriginId({ originEventId: eventId }); - - const { - formFoundation, - stage: linkedStage, - isLoading: isLoadingMetadata, - isError: isMetadataError, - } = useMetadataForProgramStage({ - programId, - stageId: linkedEvent?.programStage, - }); - - const { clientValuesWithSubValues, - isLoading: isLoadingClientValues, - isError: isClientValuesError, - } = useClientDataValues({ - linkedEventId: linkedEvent?.event, - dataValues, - formFoundation, - }); + isLoading, + isError, + missingData, + accessBlocked, + } = useTwoEventWorkspaceData(eventId, programId, stageId ?? stage?.id); - if (isLinkedEventLoading || isLoadingMetadata || isLoadingClientValues) { - return null; - } - - if (isLinkedEventError || isMetadataError || isClientValuesError) { + if (isLoading) return null; + if (isError) { return (
{i18n.t('An error occurred while loading the widget.')}
); } - - if (!linkedEvent || !formFoundation || !linkedStage) { - return null; - } + if (missingData || accessBlocked) return null; return ( - {!hideButton && ( - - - + {i18n.t('New Relationship')} + )} { diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.tsx b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.tsx index 78ec779665..5b457a006d 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.tsx +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.tsx @@ -22,6 +22,8 @@ export const WidgetTrackedEntityRelationship = ({ renderTrackedEntityRegistration, readOnly, hideButton, + accessReadOnly, + hideReadOnlyBadge, }: WidgetTrackedEntityRelationshipProps) => { const { data: relationshipTypes } = useRelationshipTypes(cachedRelationshipTypes); const { data: trackedEntityTypeName, isLoading: isLoadingTEType } = useTrackedEntityTypeName(trackedEntityTypeId); @@ -63,6 +65,8 @@ export const WidgetTrackedEntityRelationship = ({ sourceId={teiId} onLinkedRecordClick={onLinkedRecordClick} readOnly={readOnly} + accessReadOnly={accessReadOnly} + hideReadOnlyBadge={hideReadOnlyBadge} > React.ReactElement; readOnly?: boolean; hideButton?: boolean; + accessReadOnly?: boolean; + hideReadOnlyBadge?: boolean; }; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/RelationshipsWidget.component.tsx b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/RelationshipsWidget.component.tsx index effbf92b58..b789bbfbf5 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/RelationshipsWidget.component.tsx +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/RelationshipsWidget.component.tsx @@ -4,6 +4,7 @@ import { colors, spacersNum } from '@dhis2/ui'; import { withStyles } from 'capture-core-utils/styles'; import type { WithStyles } from 'capture-core-utils/styles'; import { Widget, WidgetHeaderCountBadge } from '../../../Widget'; +import { ReadOnlyBadge } from '../../../ReadOnlyBadge'; import { useGroupedLinkedEntities } from './useGroupedLinkedEntities'; import { LinkedEntitiesViewer } from './LinkedEntitiesViewer.component'; import type { Props } from './relationshipsWidget.types'; @@ -29,6 +30,8 @@ const RelationshipsWidgetPlain = ({ onLinkedRecordClick, children, readOnly, + accessReadOnly, + hideReadOnlyBadge, classes, }: Props & WithStyles) => { const [open, setOpenStatus] = useState(true); @@ -58,11 +61,16 @@ const RelationshipsWidgetPlain = ({ > +
{title} {(relationships?.length ?? 0) > 0 && ( )} + {!hideReadOnlyBadge && ( +
+ +
+ )}
)} onOpen={() => setOpenStatus(true)} diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/relationshipsWidget.types.ts b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/relationshipsWidget.types.ts index 1c7bccbc7d..fad0d6cad3 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/relationshipsWidget.types.ts +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/relationshipsWidget.types.ts @@ -11,4 +11,6 @@ export type Props = Readonly<{ onLinkedRecordClick: LinkedRecordClick; children: ReactNode; readOnly?: boolean; + accessReadOnly?: boolean; + hideReadOnlyBadge?: boolean; }>; diff --git a/src/core_modules/capture-core/hooks/useEnrollmentAccess.ts b/src/core_modules/capture-core/hooks/useEnrollmentAccess.ts index 6feaea9b70..405f54d36b 100644 --- a/src/core_modules/capture-core/hooks/useEnrollmentAccess.ts +++ b/src/core_modules/capture-core/hooks/useEnrollmentAccess.ts @@ -11,6 +11,7 @@ type Result = { programWriteAccess: boolean; trackedEntityTypeWriteAccess: boolean; programStageWriteAccess: boolean; + programStageReadAccess: boolean; isLoading: boolean; error?: any; }; @@ -39,6 +40,9 @@ export const useEnrollmentAccess = (programId?: string): Result => { programStageWriteAccess: Boolean( program?.programStages?.some(stage => stage?.access?.data?.write), ), + programStageReadAccess: Boolean( + program?.programStages?.some(stage => stage?.access?.data?.read), + ), isLoading: loading, error, }; From ab2e2aedda52fcdb5dc823e0c3dfb264be236f17 Mon Sep 17 00:00:00 2001 From: henrikmv Date: Thu, 7 May 2026 18:54:45 +0200 Subject: [PATCH 32/60] fix: remove console log --- .../Actions/Actions.container.tsx | 9 +------ .../WidgetEnrollment.container.tsx | 19 +------------- .../OverflowMenu/OverflowMenu.container.tsx | 8 +----- .../WidgetProfile/WidgetProfile.component.tsx | 12 --------- .../WidgetStagesAndEvents.component.tsx | 25 ++++++++++++++++--- 5 files changed, 24 insertions(+), 49 deletions(-) diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.container.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.container.tsx index 9360930cb5..7b2b73b0ab 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.container.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.container.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect } from 'react'; +import React, { useCallback } from 'react'; import { ActionsComponent } from './Actions.component'; import type { Props } from './actions.types'; import { useUpdateEnrollment, useDeleteEnrollment } from '../dataMutation/dataMutation'; @@ -21,13 +21,6 @@ export const Actions = ({ const { updateMutation, updateLoading } = useUpdateEnrollment(refetchEnrollment, refetchTEI, onError, onSuccess); const { deleteMutation, deleteLoading } = useDeleteEnrollment(onDelete, onError, onSuccess); const { hasAuthority } = useAuthorities({ authorities: ['F_ENROLLMENT_CASCADE_DELETE'] }); - - useEffect(() => { - const yn = (v?: boolean) => (v ? 'Yes' : 'No'); - // eslint-disable-next-line no-console - console.log(`Cascade delete enrollment: ${yn(hasAuthority)}`); - }, [hasAuthority]); - const { updateEnrollmentOwnership, isTransferLoading } = useUpdateOwnership({ teiId: enrollment.trackedEntity, programId: enrollment.program, diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.container.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.container.tsx index af47b1227d..761c3cc55f 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.container.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.container.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useEffect } from 'react'; +import React, { useMemo } from 'react'; import { errorCreator } from 'capture-core-utils'; import log from 'loglevel'; import { WidgetEnrollment as WidgetEnrollmentNote } from './WidgetEnrollment.component'; @@ -80,23 +80,6 @@ export const WidgetEnrollment = ({ const error = useError(errorEnrollment, errorProgram, errorOwnerOrgUnit, errorOrgUnit, errorLocale); const events = useEnrollmentEvents(externalData); - useEffect(() => { - if (!program) return; - const a = program?.access; - const t = program?.trackedEntityType?.access; - const yn = (v?: boolean) => (v ? 'Yes' : 'No'); - const stagesLine = (program?.programStages ?? []) - .map(({ displayName: stageName, id, access }: any) => - `Program stage "${stageName ?? id}": write=${yn(access?.data?.write)}`) - .join('\n'); - // eslint-disable-next-line no-console - console.log( - `${`Program: write=${yn(a?.data?.write)}\n` + - `TET: write=${yn(t?.data?.write)}\n`}${ - stagesLine}`, - ); - }, [program, canAddNew]); - if (error) { log.error(errorCreator('Enrollment widget could not be loaded')({ error })); } diff --git a/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/OverflowMenu.container.tsx b/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/OverflowMenu.container.tsx index 770ce90ddd..d8d23c83f3 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/OverflowMenu.container.tsx +++ b/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/OverflowMenu.container.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { useAuthorities } from 'capture-core/utils/authority/useAuthorities'; import type { Props } from './OverflowMenu.types'; import { OverflowMenuComponent } from './OverflowMenu.component'; @@ -16,12 +16,6 @@ export const OverflowMenu = ({ }: Props) => { const { hasAuthority } = useAuthorities({ authorities: ['F_TEI_CASCADE_DELETE'] }); - useEffect(() => { - const yn = (v?: boolean) => (v ? 'Yes' : 'No'); - // eslint-disable-next-line no-console - console.log(`Cascade delete TEI: ${yn(hasAuthority)}`); - }, [canWriteData, hasAuthority]); - return ( { - if (!program && !trackedEntityTypeAccess) return; - const a = program?.access; - const t = trackedEntityTypeAccess; - const yn = (v?: boolean) => (v ? 'Yes' : 'No'); - // eslint-disable-next-line no-console - console.log( - `Program: write=${yn(a?.data?.write)}\n` + - `TET: write=${yn(t?.data?.write)}`, - ); - }, [program, trackedEntityTypeAccess, canWriteData, isEditable]); - const renderProfile = () => { if (loading) { return ; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx index 9d04598fff..14021da975 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx @@ -1,19 +1,34 @@ import React, { useState, useCallback, useMemo } from 'react'; import i18n from '@dhis2/d2-i18n'; +import { spacersNum } from '@dhis2/ui'; +import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { Widget } from '../Widget'; import { ReadOnlyBadge } from '../ReadOnlyBadge'; import { Stages } from './Stages'; import { useProgram } from '../WidgetEnrollment/hooks/useProgram'; import type { Props } from './stagesAndEvents.types'; -export const WidgetStagesAndEvents = ({ +const styles = { + header: { + display: 'flex', + alignItems: 'center', + gap: `${spacersNum.dp8}px`, + flex: 1, + }, + badge: { + marginInlineStart: 'auto', + }, +}; + +const WidgetStagesAndEventsPlain = ({ + classes, className, stages, events, programId, hideReadOnlyBadge, ...passOnProps -}: Props) => { +}: Props & WithStyles) => { const [open, setOpenStatus] = useState(true); const { program } = useProgram(programId); const stageWriteAccessById = useMemo(() => { @@ -40,10 +55,10 @@ export const WidgetStagesAndEvents = ({ > +
{i18n.t('Stages and Events')} {!hideReadOnlyBadge && ( -
+
); }; + +export const WidgetStagesAndEvents = withStyles(styles)(WidgetStagesAndEventsPlain); From a0289b767283aeb96834f1fc6cb84d80222c626f Mon Sep 17 00:00:00 2001 From: henrikmv Date: Thu, 7 May 2026 18:59:18 +0200 Subject: [PATCH 33/60] Revert "fix: [DHIS2-21392] Form elements visble when no access to form (#4535)" This reverts commit 58663c85b16c5967bb0fd9281cbea5e51eaabb33. --- .../ProgramStageSelector.feature | 2 +- .../ProgramStageSelector.js | 5 +- i18n/en.pot | 9 +-- .../dataEntrySelectionsNoAccess.component.tsx | 68 +++++++++++++++++++ .../dataEntrySelectionsNoAccess.container.tsx | 16 +++++ ...SingleEventRegistrationEntry.component.tsx | 12 +--- .../NoWriteAccessMessage.component.tsx | 34 +++++----- .../NewEventWorkspace.component.tsx | 41 +++-------- .../Pages/New/NewPage.component.tsx | 1 - .../AccessVerification.component.tsx | 21 ++++++ .../AccessVerification.container.tsx | 30 ++++++++ .../AccessVerification/NoAccess.component.tsx | 67 ++++++++++++++++++ .../accessVerification.selectors.ts | 11 +++ .../accessVerification.types.ts | 26 +++++++ .../AccessVerification/index.ts | 1 + .../OrgUnitFetcher.container.tsx | 9 --- .../WidgetEnrollmentEventNew.container.tsx | 4 +- .../AccessVerification/NoAccess.component.tsx | 66 ++++++++++++++++++ .../AccessVerification/index.ts | 1 + .../WidgetEventSchedule.container.tsx | 10 ++- 20 files changed, 353 insertions(+), 81 deletions(-) create mode 100644 src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/SelectionsNoAccess/dataEntrySelectionsNoAccess.component.tsx create mode 100644 src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/SelectionsNoAccess/dataEntrySelectionsNoAccess.container.tsx create mode 100644 src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/AccessVerification.component.tsx create mode 100644 src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/AccessVerification.container.tsx create mode 100644 src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/NoAccess.component.tsx create mode 100644 src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/accessVerification.selectors.ts create mode 100644 src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/accessVerification.types.ts create mode 100644 src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/index.ts delete mode 100644 src/core_modules/capture-core/components/WidgetEnrollmentEventNew/OrgUnitFetcher/OrgUnitFetcher.container.tsx create mode 100644 src/core_modules/capture-core/components/WidgetEventSchedule/AccessVerification/NoAccess.component.tsx create mode 100644 src/core_modules/capture-core/components/WidgetEventSchedule/AccessVerification/index.ts diff --git a/cypress/e2e/EnrollmentAddEventPage/ProgramStageSelector/ProgramStageSelector.feature b/cypress/e2e/EnrollmentAddEventPage/ProgramStageSelector/ProgramStageSelector.feature index 4824d5b066..0e93937044 100644 --- a/cypress/e2e/EnrollmentAddEventPage/ProgramStageSelector/ProgramStageSelector.feature +++ b/cypress/e2e/EnrollmentAddEventPage/ProgramStageSelector/ProgramStageSelector.feature @@ -19,4 +19,4 @@ Feature: Program stage selector when navigating to EnrollmentEventNew without st @user:trackerAutoTestRestricted Scenario: Stages buttons should not be displayed when no data write access Given user lands on the Enrollment dashboard page by typing #/enrollmentEventNew?enrollmentId=X7g83OFRALm&orgUnitId=DiszpKrYNg8&programId=WSGAb5XwJ3Y&teiId=YsKjdOcl9Cd - Then the New event quick action button should not be visible + Then the New event quick action button is disabled diff --git a/cypress/e2e/EnrollmentAddEventPage/ProgramStageSelector/ProgramStageSelector.js b/cypress/e2e/EnrollmentAddEventPage/ProgramStageSelector/ProgramStageSelector.js index dea0defe03..c28e147c37 100644 --- a/cypress/e2e/EnrollmentAddEventPage/ProgramStageSelector/ProgramStageSelector.js +++ b/cypress/e2e/EnrollmentAddEventPage/ProgramStageSelector/ProgramStageSelector.js @@ -33,7 +33,6 @@ Then('only three program stages are displayed in the stage selector widget', () cy.get('[data-test=program-stage-selector-button]').should('have.length', 3); }); -Then('the New event quick action button should not be visible', () => { - cy.get('[data-test="enrollment-overview-page"]').should('exist'); - cy.get('[data-test=quick-action-button-container]').should('not.exist'); +Then('the New event quick action button is disabled', () => { + cy.get('[data-test=quick-action-button-report]').should('be.disabled'); }); diff --git a/i18n/en.pot b/i18n/en.pot index 0ede078384..7a6ffc91a5 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -312,6 +312,9 @@ msgstr "No, cancel" msgid "You don't have access to create an event in the current selections" msgstr "You don't have access to create an event in the current selections" +msgid "Save" +msgstr "Save" + msgid "Saving a {{trackedEntityName}}" msgstr "Saving a {{trackedEntityName}}" @@ -623,9 +626,6 @@ msgstr "Select columns" msgid "Columns to show in table" msgstr "Columns to show in table" -msgid "Save" -msgstr "Save" - msgid "Column" msgstr "Column" @@ -811,9 +811,6 @@ msgstr "There was an error loading the page" msgid "Program stage is invalid" msgstr "Program stage is invalid" -msgid "Stage not found" -msgstr "Stage not found" - msgid "Report" msgstr "Report" diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/SelectionsNoAccess/dataEntrySelectionsNoAccess.component.tsx b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/SelectionsNoAccess/dataEntrySelectionsNoAccess.component.tsx new file mode 100644 index 0000000000..43e5b7fc55 --- /dev/null +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/SelectionsNoAccess/dataEntrySelectionsNoAccess.component.tsx @@ -0,0 +1,68 @@ +import React, { Component } from 'react'; +import { withStyles, WithStyles } from 'capture-core-utils/styles'; +import i18n from '@dhis2/d2-i18n'; +import { Button } from '@dhis2/ui'; +import { NoWriteAccessMessage } from '../../../NoWriteAccessMessage'; + +const getStyles = (theme: any) => ({ + contents: { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + paddingTop: 50, + paddingBottom: 50, + }, + buttonRow: { + display: 'flex', + flexWrap: 'wrap', + paddingTop: 10, + marginInlineStart: '-8px', + }, + buttonContainer: { + paddingInlineEnd: theme.spacing.unit * 2, + }, +}) as const; + +type Props = { + onCancel: () => void; +}; + +class DataEntrySelectionsNoAccessPlain extends Component> { + render() { + const { classes, onCancel } = this.props; + return ( +
+ +
+
+ +
+
+ +
+
+
+ ); + } +} + +export const DataEntrySelectionsNoAccess = withStyles(getStyles)(DataEntrySelectionsNoAccessPlain); diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/SelectionsNoAccess/dataEntrySelectionsNoAccess.container.tsx b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/SelectionsNoAccess/dataEntrySelectionsNoAccess.container.tsx new file mode 100644 index 0000000000..dc20b9bef9 --- /dev/null +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/SelectionsNoAccess/dataEntrySelectionsNoAccess.container.tsx @@ -0,0 +1,16 @@ +import { connect } from 'react-redux'; +import { DataEntrySelectionsNoAccess } from './dataEntrySelectionsNoAccess.component'; +import { + cancelNewEventAndReturnToMainPage, +} from '../DataEntryWrapper/DataEntry/actions/dataEntry.actions'; + +const mapStateToProps = () => ({ +}); + +const mapDispatchToProps = (dispatch: any) => ({ + onCancel: () => { + dispatch(cancelNewEventAndReturnToMainPage()); + }, +}); + +export const SelectionsNoAccess = connect(mapStateToProps, mapDispatchToProps)(DataEntrySelectionsNoAccess); diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/SingleEventRegistrationEntry.component.tsx b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/SingleEventRegistrationEntry.component.tsx index 6843b28f2a..948832929a 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/SingleEventRegistrationEntry.component.tsx +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/SingleEventRegistrationEntry.component.tsx @@ -1,21 +1,13 @@ import React from 'react'; -import { useDispatch } from 'react-redux'; -import i18n from '@dhis2/d2-i18n'; -import { NoWriteAccessMessage } from '../../NoWriteAccessMessage'; import { NewEventDataEntryWrapper } from './DataEntryWrapper/NewEventDataEntryWrapper.container'; import { NewRelationshipWrapper } from './NewRelationshipWrapper/NewEventNewRelationshipWrapper.container'; -import { cancelNewEventAndReturnToMainPage } from './DataEntryWrapper/DataEntry/actions/dataEntry.actions'; +import { SelectionsNoAccess } from './SelectionsNoAccess/dataEntrySelectionsNoAccess.container'; import type { Props } from './SingleEventRegistrationEntry.types'; export const SingleEventRegistrationEntryComponent = ({ showAddRelationship, eventAccess }: Props) => { - const dispatch = useDispatch(); - if (!eventAccess.write) { return ( - dispatch(cancelNewEventAndReturnToMainPage())} - /> + ); } diff --git a/src/core_modules/capture-core/components/NoWriteAccessMessage/NoWriteAccessMessage.component.tsx b/src/core_modules/capture-core/components/NoWriteAccessMessage/NoWriteAccessMessage.component.tsx index d8c1ce8238..141f68fbca 100644 --- a/src/core_modules/capture-core/components/NoWriteAccessMessage/NoWriteAccessMessage.component.tsx +++ b/src/core_modules/capture-core/components/NoWriteAccessMessage/NoWriteAccessMessage.component.tsx @@ -1,36 +1,36 @@ import React, { type ComponentType } from 'react'; -import i18n from '@dhis2/d2-i18n'; -import { Button } from '@dhis2/ui'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { IncompleteSelectionsMessage } from '../IncompleteSelectionsMessage'; const styles = () => ({ + header: { + flexGrow: 1, + fontSize: 16, + fontWeight: 500, + }, message: { marginTop: 10, }, }); type Props = { + title?: string; message: string; - onCancel?: () => void; }; type PropsWithStyles = Props & WithStyles; -const NoWriteAccessMessagePlain = ({ message, onCancel, classes }: PropsWithStyles) => ( -
- - {message} - - {onCancel && ( - - )} -
+export const NoWriteAccessMessagePlain = ({ title, message, classes }: PropsWithStyles) => ( + <> +
+ {title} +
+
+ + {message} + +
+ ); export const NoWriteAccessMessage = diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/NewEventWorkspace/NewEventWorkspace.component.tsx b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/NewEventWorkspace/NewEventWorkspace.component.tsx index fd6f703bb5..1ea05063ae 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/NewEventWorkspace/NewEventWorkspace.component.tsx +++ b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/NewEventWorkspace/NewEventWorkspace.component.tsx @@ -4,10 +4,9 @@ import i18n from '@dhis2/d2-i18n'; import { useSelector } from 'react-redux'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { tabMode } from './newEventWorkspace.constants'; -import { getProgramAndStageForProgram, getProgramEventAccess } from '../../../../metaData'; +import { getProgramAndStageForProgram } from '../../../../metaData'; import { WidgetEnrollmentEventNew } from '../../../WidgetEnrollmentEventNew'; import { DiscardDialog } from '../../../Dialogs/DiscardDialog.component'; -import { NoWriteAccessMessage } from '../../../NoWriteAccessMessage'; import { Widget } from '../../../Widget'; import { WidgetStageHeader } from './WidgetStageHeader'; import { WidgetEventSchedule } from '../../../WidgetEventSchedule'; @@ -22,9 +21,6 @@ const styles: Readonly = () => ({ innerWrapper: { padding: `0 ${spacersNum.dp12}px`, }, - errorWrapper: { - padding: `${spacersNum.dp16}px ${spacersNum.dp12}px`, - }, tabs: { marginBottom: spacersNum.dp16, }, @@ -58,35 +54,18 @@ const NewEventWorkspacePlain = ({ } }; - const renderWidget = (content: React.ReactNode) => ( - : null} - > - {content} - - ); - if (!stage) { - return renderWidget( -
{i18n.t('Stage not found')}
, - ); - } - - const eventAccess = getProgramEventAccess(programId, stageId); - if (!eventAccess?.write) { - return renderWidget( -
- -
, - ); + return null; } return ( <> - {renderWidget( + + } + >
@@ -144,8 +123,8 @@ const NewEventWorkspacePlain = ({ hideDueDate={stage?.hideDueDate} enableUserAssignment />} -
, - )} +
+
{ setMode(tempMode.current); setWarningVisible(false); }} diff --git a/src/core_modules/capture-core/components/Pages/New/NewPage.component.tsx b/src/core_modules/capture-core/components/Pages/New/NewPage.component.tsx index 686c9b99b6..e181a47ed6 100644 --- a/src/core_modules/capture-core/components/Pages/New/NewPage.component.tsx +++ b/src/core_modules/capture-core/components/Pages/New/NewPage.component.tsx @@ -82,7 +82,6 @@ const NewPagePlain = ({ interpolation: { escapeValue: false }, }, )} - onCancel={handleMainPageNavigation} /> : diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/AccessVerification.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/AccessVerification.component.tsx new file mode 100644 index 0000000000..0bad2b745c --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/AccessVerification.component.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { OrgUnitFetcher } from '../OrgUnitFetcher/OrgUnitFetcher.component'; +import { NoAccess } from './NoAccess.component'; +import type { Props } from './accessVerification.types'; + +export const AccessVerificationComponent = ({ eventAccess, onCancel, ...passOnProps }: Props) => { + if (!eventAccess.write) { + return ( + + ); + } + + return ( + + ); +}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/AccessVerification.container.tsx b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/AccessVerification.container.tsx new file mode 100644 index 0000000000..771d2d8c12 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/AccessVerification.container.tsx @@ -0,0 +1,30 @@ +import { connect } from 'react-redux'; +import type { ComponentType } from 'react'; +import { + AccessVerificationComponent, +} from './AccessVerification.component'; +import { withBrowserBackWarning } from '../../../HOC/withBrowserBackWarning'; +import { dataEntryHasChanges } from '../../DataEntry/common/dataEntryHasChanges'; +import { makeEventAccessSelector } from './accessVerification.selectors'; +import type { ContainerProps } from './accessVerification.types'; +import { defaultDialogProps } from '../../Dialogs/DiscardDialog.constants'; + +const inEffect = (state: any, ownProps: any) => + dataEntryHasChanges(state, ownProps.widgetReducerName) || state.newEventPage.showAddRelationship; + +const makeMapStateToProps = () => { + const eventAccessSelector = makeEventAccessSelector(); + return (state: any, { program, stage }: any) => ({ + eventAccess: eventAccessSelector(state, { programId: program.id, stageId: stage.id }), + }); +}; + +const mapDispatchToProps = () => ({ +}); + +const AccessVerificationWithConnect = connect(makeMapStateToProps, mapDispatchToProps)(AccessVerificationComponent as any); + +export const AccessVerification: ComponentType = withBrowserBackWarning( + defaultDialogProps, + inEffect, +)(AccessVerificationWithConnect); diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/NoAccess.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/NoAccess.component.tsx new file mode 100644 index 0000000000..cf7513b6c8 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/NoAccess.component.tsx @@ -0,0 +1,67 @@ +import React, { Component } from 'react'; +import { withStyles, type WithStyles } from 'capture-core-utils/styles'; +import i18n from '@dhis2/d2-i18n'; +import { Button } from '@dhis2/ui'; +import { NoWriteAccessMessage } from '../../NoWriteAccessMessage'; + +const getStyles: any = (theme: any) => ({ + contents: { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + paddingTop: 50, + paddingBottom: 50, + }, + buttonRow: { + display: 'flex', + flexWrap: 'wrap', + paddingTop: 10, + marginInlineStart: '-8px', + }, + buttonContainer: { + paddingInlineEnd: theme.spacing.unit * 2, + }, +}); + +type Props = { + onCancel: () => void; +} & WithStyles; + +class NoAccessPlain extends Component { + render() { + const { classes, onCancel } = this.props; + return ( +
+ +
+
+ +
+
+ +
+
+
+ ); + } +} + +export const NoAccess = withStyles(getStyles)(NoAccessPlain); diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/accessVerification.selectors.ts b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/accessVerification.selectors.ts new file mode 100644 index 0000000000..3f83887bde --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/accessVerification.selectors.ts @@ -0,0 +1,11 @@ +import { createSelector } from 'reselect'; +import { getProgramEventAccess } from '../../../metaData'; + +const programIdSelector = (state: any, { programId }: any) => programId; +const programStageIdSelector = (state: any, { stageId }: any) => stageId; + +export const makeEventAccessSelector = () => createSelector( + programIdSelector, + programStageIdSelector, + (programId: string, programStageId: string | null) => + programId && getProgramEventAccess(programId, programStageId)); diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/accessVerification.types.ts b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/accessVerification.types.ts new file mode 100644 index 0000000000..2be09d715a --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/accessVerification.types.ts @@ -0,0 +1,26 @@ +import type { ProgramStage, RenderFoundation, TrackerProgram } from '../../../metaData'; +import type { ExternalSaveHandler, RulesExecutionDependencies } from '../common.types'; + +export type ContainerProps = { + program: TrackerProgram; + stage: ProgramStage; + formFoundation: RenderFoundation; + teiId: string; + enrollmentId: string; + orgUnitId: string; + rulesExecutionDependencies: RulesExecutionDependencies; + onSaveExternal?: ExternalSaveHandler; + onSaveSuccessActionType?: string; + onSaveErrorActionType?: string; + onSaveAndCompleteEnrollmentSuccessActionType?: string; + onSaveAndCompleteEnrollmentErrorActionType?: string; + widgetReducerName: string; + onCancel?: () => void; +}; + +export type Props = { + eventAccess: { + read: boolean; + write: boolean; + }; +} & ContainerProps; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/index.ts b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/index.ts new file mode 100644 index 0000000000..919db1509a --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/AccessVerification/index.ts @@ -0,0 +1 @@ +export { AccessVerification } from './AccessVerification.container'; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/OrgUnitFetcher/OrgUnitFetcher.container.tsx b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/OrgUnitFetcher/OrgUnitFetcher.container.tsx deleted file mode 100644 index 7f69682e41..0000000000 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/OrgUnitFetcher/OrgUnitFetcher.container.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { OrgUnitFetcher as OrgUnitFetcherComponent } from './OrgUnitFetcher.component'; -import { withBrowserBackWarning } from '../../../HOC/withBrowserBackWarning'; -import { dataEntryHasChanges } from '../../DataEntry/common/dataEntryHasChanges'; -import { defaultDialogProps } from '../../Dialogs/DiscardDialog.constants'; - -const inEffect = (state: any, ownProps: any) => - dataEntryHasChanges(state, ownProps.widgetReducerName) || state.newEventPage.showAddRelationship; - -export const OrgUnitFetcher = withBrowserBackWarning(defaultDialogProps, inEffect)(OrgUnitFetcherComponent); diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/WidgetEnrollmentEventNew.container.tsx b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/WidgetEnrollmentEventNew.container.tsx index 4e48426865..65d683e2e5 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/WidgetEnrollmentEventNew.container.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/WidgetEnrollmentEventNew.container.tsx @@ -1,7 +1,7 @@ import React, { useMemo } from 'react'; import i18n from '@dhis2/d2-i18n'; import { getProgramAndStageForProgram, TrackerProgram } from '../../metaData'; -import { OrgUnitFetcher } from './OrgUnitFetcher/OrgUnitFetcher.container'; +import { AccessVerification } from './AccessVerification'; import type { WidgetProps } from './WidgetEnrollmentEventNew.types'; import { useMetadataForProgramStage } from '../DataEntries/common/ProgramStage/useMetadataForProgramStage'; @@ -36,7 +36,7 @@ export const WidgetEnrollmentEventNew = ({ } return ( - ({ + contents: { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + paddingTop: 50, + paddingBottom: 50, + }, + buttonRow: { + display: 'flex', + flexWrap: 'wrap', + paddingTop: 10, + marginInlineStart: '-8px', + }, + buttonContainer: { + paddingInlineEnd: theme.spacing.unit * 2, + }, +}) as const; + +type Props = { + onCancel: () => void; +} & WithStyles; + +class NoAccessPlain extends Component { + render() { + const { classes, onCancel } = this.props; + return ( +
+ +
+
+ +
+
+ +
+
+
+ ); + } +} + +export const NoAccess = withStyles(getStyles)(NoAccessPlain); diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/AccessVerification/index.ts b/src/core_modules/capture-core/components/WidgetEventSchedule/AccessVerification/index.ts new file mode 100644 index 0000000000..47d0512497 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/AccessVerification/index.ts @@ -0,0 +1 @@ +export { NoAccess } from './NoAccess.component'; diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.tsx b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.tsx index f0e2ad9fd6..4f46356c5d 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.tsx +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.tsx @@ -4,7 +4,7 @@ import { useDispatch } from 'react-redux'; import { useTimeZoneConversion } from '@dhis2/app-runtime'; import moment from 'moment'; import { pipe } from 'capture-core-utils'; -import { getProgramAndStageForProgram, TrackerProgram, dataElementTypes } from '../../metaData'; +import { getProgramAndStageForProgram, TrackerProgram, getProgramEventAccess, dataElementTypes } from '../../metaData'; import { getCachedOrgUnitName } from '../../metadataRetrieval/orgUnitName'; import { useLocationQuery } from '../../utils/routing'; import type { ContainerProps } from './widgetEventSchedule.types'; @@ -17,6 +17,7 @@ import { useNoteDetails, } from './hooks'; import { requestScheduleEvent } from './WidgetEventSchedule.actions'; +import { NoAccess } from './AccessVerification'; import { useCategoryCombinations } from '../DataEntryDhis2Helpers/AOC/useCategoryCombinations'; import { convertFormToClient, convertClientToServer } from '../../converters'; import { useProgramExpiryForUser } from '../../hooks'; @@ -192,6 +193,13 @@ export const WidgetEventSchedule = ({ ); } + const eventAccess = getProgramEventAccess(programId, stageId); + if (!eventAccess?.write) { + return ( + + ); + } + return ( Date: Thu, 7 May 2026 19:50:22 +0200 Subject: [PATCH 34/60] feat: initial clean up --- .../StagesAndEventsWidget.feature | 2 +- .../StagesAndEventsWidget.js | 2 +- .../EnrollmentPageDefault.container.tsx | 88 ++++++------ .../EnrollmentPageDefault.types.ts | 4 - .../EnrollmentEditEventPage.component.tsx | 8 -- .../EnrollmentEditEventPage.container.tsx | 118 ++++++++-------- .../EnrollmentEditEventPage.types.ts | 4 - .../EnrollmentAccessContext.tsx | 105 ++++++++++++++ .../EnrollmentAccessContext/index.ts | 5 + .../EnrollmentPageLayout.tsx | 133 +++++------------- .../LayoutComponentConfig.ts | 20 +-- .../common/EnrollmentOverviewDomain/index.ts | 4 + .../ReadOnlyBadge/ReadOnlyBadge.tsx | 4 +- src/core_modules/capture-core/hooks/index.ts | 1 - .../capture-core/hooks/useEnrollmentAccess.ts | 49 ------- 15 files changed, 258 insertions(+), 289 deletions(-) create mode 100644 src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/EnrollmentAccessContext.tsx create mode 100644 src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/index.ts delete mode 100644 src/core_modules/capture-core/hooks/useEnrollmentAccess.ts diff --git a/cypress/e2e/EnrollmentPage/StagesAndEventsWidget/StagesAndEventsWidget.feature b/cypress/e2e/EnrollmentPage/StagesAndEventsWidget/StagesAndEventsWidget.feature index 4ddc615c6a..2c4a7fa670 100644 --- a/cypress/e2e/EnrollmentPage/StagesAndEventsWidget/StagesAndEventsWidget.feature +++ b/cypress/e2e/EnrollmentPage/StagesAndEventsWidget/StagesAndEventsWidget.feature @@ -85,4 +85,4 @@ Feature: User interacts with Stages and Events Widget @user:trackerAutoTestRestricted Scenario: Create new event buttons are hidden for users with read-only access Given you open the enrollment page by typing #enrollment?enrollmentId=WKPoiZxZxNG&orgUnitId=DiszpKrYNg8&programId=WSGAb5XwJ3Y&teiId=PgmUFEQYZdt - Then no create new event button should be visible + Then create new event button should not be visible diff --git a/cypress/e2e/EnrollmentPage/StagesAndEventsWidget/StagesAndEventsWidget.js b/cypress/e2e/EnrollmentPage/StagesAndEventsWidget/StagesAndEventsWidget.js index 1220490567..2575432141 100644 --- a/cypress/e2e/EnrollmentPage/StagesAndEventsWidget/StagesAndEventsWidget.js +++ b/cypress/e2e/EnrollmentPage/StagesAndEventsWidget/StagesAndEventsWidget.js @@ -186,7 +186,7 @@ Then(/^you should see the disabled button (.*)$/, (stageName) => { .should('be.disabled'); }); -Then('no create new event button should be visible', () => { +Then('create new event button should not be visible', () => { cy.get('[data-test="stages-and-events-widget"]').should('exist'); cy.get('[data-test="create-new-button"]').should('not.exist'); }); diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.tsx b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.tsx index 267467aa2e..1690f2c4df 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.tsx +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.tsx @@ -8,6 +8,7 @@ import { useTimeZoneConversion } from '@dhis2/app-runtime'; import type { ApiEnrollmentEvent } from 'capture-core-utils/types/api-types'; import { commitEnrollmentAndEvents, + EnrollmentAccessProvider, rollbackEnrollmentAndEvents, showEnrollmentError, updateEnrollmentAndEvents, @@ -42,7 +43,7 @@ import { deleteEnrollmentEvent, updateEnrollmentEventStatus, } from '../../common/EnrollmentOverviewDomain/enrollment.actions'; -import { useHideWidgetByRuleLocations, useEnrollmentAccess } from '../../../../hooks'; +import { useHideWidgetByRuleLocations } from '../../../../hooks'; export const EnrollmentPageDefault = () => { @@ -180,13 +181,6 @@ export const EnrollmentPageDefault = () => { navigate(`/?${buildUrlQueryString({ orgUnitId, programId })}`); }, [navigate, orgUnitId, programId]); - const { - programWriteAccess, - trackedEntityTypeWriteAccess, - programStageWriteAccess, - programStageReadAccess, - } = useEnrollmentAccess(programId); - if (isLoading) { return ( @@ -198,45 +192,43 @@ export const EnrollmentPageDefault = () => { } return ( - + + + ); }; diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.ts b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.ts index 57b75c1caa..c07ac06edd 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.ts +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.ts @@ -49,10 +49,6 @@ export type Props = { pageLayout: PageLayoutConfig; availableWidgets: Readonly<{ [key: string]: WidgetConfig }>; onDeleteTrackedEntitySuccess: () => void; - programWriteAccess: boolean; - trackedEntityTypeWriteAccess: boolean; - programStageWriteAccess: boolean; - programStageReadAccess: boolean; }; diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.tsx b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.tsx index 4f8a8fd0f8..b5a96db2d1 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.tsx +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.tsx @@ -62,10 +62,6 @@ export const EnrollmentEditEventPageComponent = ({ onUpdateEnrollmentEventsSuccess, onUpdateEnrollmentEventsError, userInteractionInProgress, - programWriteAccess, - trackedEntityTypeWriteAccess, - programStageWriteAccess, - programStageReadAccess, }: PlainProps) => ( diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.tsx b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.tsx index 4a1707b659..4d53903a38 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.tsx +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.tsx @@ -3,10 +3,11 @@ import type { ProgramRule } from '@dhis2/rules-engine-javascript'; import { useQueryClient } from '@tanstack/react-query'; import { useDispatch, useSelector } from 'react-redux'; import { dataEntryIds } from 'capture-core/constants'; -import { useEnrollmentAccess, useEnrollmentEditEventPageMode, useHideWidgetByRuleLocations } from '../../../hooks'; +import { useEnrollmentEditEventPageMode, useHideWidgetByRuleLocations } from '../../../hooks'; import type { ReduxState } from '../../App/withAppUrlSync.types'; import { commitEnrollmentAndEvents, + EnrollmentAccessProvider, rollbackEnrollmentAndEvents, setExternalEnrollmentStatus, showEnrollmentError, @@ -291,73 +292,64 @@ const EnrollmentEditEventPageWithContextPlain = ({ dispatch(rollbackAssignee(assignedUser, prevAssignee, eventId)); }; - const { - programWriteAccess, - trackedEntityTypeWriteAccess, - programStageWriteAccess, - programStageReadAccess, - } = useEnrollmentAccess(programId); - if (pageStatus === pageStatuses.LOADING) { return ; } return ( - + + + ); }; diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.ts b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.ts index 6fa48c77bd..8131897b94 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.ts +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.ts @@ -60,10 +60,6 @@ export type PlainProps = { onUpdateOrAddEnrollmentEvents: (events: Array) => void; onUpdateEnrollmentEventsSuccess: (events: Array) => void; onUpdateEnrollmentEventsError: (events: Array) => void; - programWriteAccess: boolean; - trackedEntityTypeWriteAccess: boolean; - programStageWriteAccess: boolean; - programStageReadAccess: boolean; }; export type Props = { diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/EnrollmentAccessContext.tsx b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/EnrollmentAccessContext.tsx new file mode 100644 index 0000000000..4342a7e964 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/EnrollmentAccessContext.tsx @@ -0,0 +1,105 @@ +import React, { createContext, useContext, useMemo } from 'react'; +import { useDataQuery } from '@dhis2/app-runtime'; + +type ProgramAccess = { data?: { read?: boolean; write?: boolean } }; + +type ProgramAccessResponse = { + access?: ProgramAccess; + trackedEntityType?: { + access?: ProgramAccess; + displayName?: string; + name?: string; + }; + programStages?: Array<{ id?: string; access?: ProgramAccess }>; +}; + +export type EnrollmentAccessContextValue = { + isLoading: boolean; + error: any; + programWriteAccess: boolean; + trackedEntityTypeWriteAccess: boolean; + programStageWriteAccess: boolean; + programStageReadAccess: boolean; + trackedEntityTypeName?: string; + currentStageId?: string; + currentStageWriteAccess: boolean; + isEventPage: boolean; + multipleStages: boolean; + allWriteAccessMissing: boolean; + hideWidgetBadge: boolean; +}; + +const fallback: EnrollmentAccessContextValue = { + isLoading: false, + error: undefined, + programWriteAccess: true, + trackedEntityTypeWriteAccess: true, + programStageWriteAccess: true, + programStageReadAccess: true, + currentStageWriteAccess: true, + isEventPage: false, + multipleStages: false, + allWriteAccessMissing: false, + hideWidgetBadge: false, +}; + +const Context = createContext(fallback); + +type ProviderProps = { + programId?: string; + currentStageId?: string; + children: React.ReactNode; +}; + +export const EnrollmentAccessProvider = ({ programId, currentStageId, children }: ProviderProps) => { + const { loading, error, data } = useDataQuery( + useMemo(() => ({ + program: { + resource: `programs/${programId}`, + params: { + fields: ['access,trackedEntityType[access,displayName,name],programStages[id,access]'], + }, + }, + }), [programId]), + { lazy: !programId } as any, + ); + + const value = useMemo(() => { + const program = data?.program as ProgramAccessResponse | undefined; + const stages = program?.programStages ?? []; + const programWriteAccess = Boolean(program?.access?.data?.write); + const trackedEntityTypeWriteAccess = Boolean(program?.trackedEntityType?.access?.data?.write); + const programStageWriteAccess = stages.some(s => s?.access?.data?.write); + const programStageReadAccess = stages.some(s => s?.access?.data?.read); + const isEventPage = Boolean(currentStageId); + const currentStage = currentStageId + ? stages.find(s => s?.id === currentStageId) + : undefined; + const currentStageWriteAccess = currentStage + ? Boolean(currentStage.access?.data?.write) + : true; + const allWriteAccessMissing = !programWriteAccess + && !trackedEntityTypeWriteAccess + && !programStageWriteAccess; + return { + isLoading: loading, + error, + programWriteAccess, + trackedEntityTypeWriteAccess, + programStageWriteAccess, + programStageReadAccess, + trackedEntityTypeName: + program?.trackedEntityType?.name ?? program?.trackedEntityType?.displayName, + currentStageId, + currentStageWriteAccess, + isEventPage, + multipleStages: stages.length > 1, + allWriteAccessMissing, + hideWidgetBadge: isEventPage || allWriteAccessMissing, + }; + }, [loading, error, data, currentStageId]); + + return {children}; +}; + +export const useEnrollmentAccessContext = (): EnrollmentAccessContextValue => useContext(Context); diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/index.ts b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/index.ts new file mode 100644 index 0000000000..dc0884c162 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/index.ts @@ -0,0 +1,5 @@ +export { + EnrollmentAccessProvider, + useEnrollmentAccessContext, +} from './EnrollmentAccessContext'; +export type { EnrollmentAccessContextValue } from './EnrollmentAccessContext'; diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx index fbbdfe4c62..c385dc0744 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx @@ -6,7 +6,7 @@ import { AddRelationshipRefWrapper } from './AddRelationshipRefWrapper'; import type { Props as EnrollmentPageProps } from '../../../Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types'; import { EnrollmentBreadcrumb } from '../../../../Breadcrumbs/EnrollmentBreadcrumb'; import { ReadOnlyBadge } from '../../../../ReadOnlyBadge'; -import { useProgram } from '../../../../WidgetEnrollment/hooks/useProgram'; +import { useEnrollmentAccessContext } from '../EnrollmentAccessContext'; import './enrollmentPageLayout.css'; const getEnrollmentPageStyles: Readonly = () => ({ @@ -56,82 +56,41 @@ const getEnrollmentPageStyles: Readonly = () => ({ alignItems: 'center', justifyContent: 'space-between', }, - readOnlyBadge: { - display: 'flex', - alignItems: 'center', - gap: spacersNum.dp4, - flexShrink: 0, - }, }); const isValidHex = (color: string) => /^#[0-9A-F]{6}$/i.test(color); -const resolveStageBadgeAccess = ( - onEventPage: boolean, - currentStageWriteAccess: boolean, - programStageWriteAccess: boolean, - programStageReadAccess: boolean, -) => (onEventPage - ? currentStageWriteAccess - : programStageWriteAccess || !programStageReadAccess); - -type BreadcrumbBadgeProgram = { - programStages?: Array; - trackedEntityType?: { name?: string }; -}; - -type BreadcrumbBadgeProps = { - onEventPage: boolean; - currentStageWriteAccess: boolean; - programWriteAccess: boolean; - trackedEntityTypeWriteAccess: boolean; - programStageWriteAccess: boolean; - programStageReadAccess: boolean; - program: BreadcrumbBadgeProgram; +const EnrollmentReadOnlyBadge = () => { + const access = useEnrollmentAccessContext(); + const { + isEventPage, + currentStageWriteAccess, + programWriteAccess, + trackedEntityTypeWriteAccess, + programStageWriteAccess, + programStageReadAccess, + multipleStages, + trackedEntityTypeName, + } = access; + const stageBadgeAccess = isEventPage + ? currentStageWriteAccess + : programStageWriteAccess || !programStageReadAccess; + return ( + + ); }; -const BreadcrumbBadge = ({ - onEventPage, - currentStageWriteAccess, - programWriteAccess, - trackedEntityTypeWriteAccess, - programStageWriteAccess, - programStageReadAccess, - program, -}: BreadcrumbBadgeProps) => ( - 1} - trackedEntityName={program?.trackedEntityType?.name} - inlineLabel - /> -); - type OwnProps = EnrollmentPageProps; type Props = OwnProps & WithStyles; -const useStageAccess = ( - programId: string, - currentStageId: string | undefined, -) => { - const { program: liveProgram } = useProgram(programId); - const liveCurrentStage = currentStageId - ? liveProgram?.programStages?.find((s: any) => s.id === currentStageId) - : undefined; - const currentStageWriteAccess = liveCurrentStage - ? Boolean(liveCurrentStage?.access?.data?.write) - : true; - return { currentStageWriteAccess }; -}; - const EnrollmentPageLayoutPlain = ({ pageLayout, availableWidgets, @@ -142,10 +101,6 @@ const EnrollmentPageLayoutPlain = ({ onBackToMainPage, onBackToDashboard, onBackToViewEvent, - programWriteAccess, - trackedEntityTypeWriteAccess, - programStageWriteAccess, - programStageReadAccess, classes, ...passOnProps }: Props) => { @@ -154,13 +109,7 @@ const EnrollmentPageLayoutPlain = ({ useState(undefined); const toggleVisibility = useCallback(() => setMainContentVisibility(current => !current), []); - const currentStageId = (passOnProps as any).stageId as string | undefined; - const { currentStageWriteAccess } = useStageAccess(program.id, currentStageId); - const onEventPage = Boolean(currentStageId); - const allWriteAccessMissing = !programWriteAccess - && !trackedEntityTypeWriteAccess - && !programStageWriteAccess; - const hideWidgetReadOnlyBadge = onEventPage || allWriteAccessMissing; + const access = useEnrollmentAccessContext(); const allProps = useMemo(() => ({ ...passOnProps, @@ -169,22 +118,18 @@ const EnrollmentPageLayoutPlain = ({ eventStatus, toggleVisibility, addRelationShipContainerElement, - programWriteAccess, - trackedEntityTypeWriteAccess, - programStageWriteAccess, - programStageReadAccess, - hideEventStageBadge: hideWidgetReadOnlyBadge, + programWriteAccess: access.programWriteAccess, + trackedEntityTypeWriteAccess: access.trackedEntityTypeWriteAccess, + programStageWriteAccess: access.programStageWriteAccess, + programStageReadAccess: access.programStageReadAccess, + hideWidgetBadge: access.hideWidgetBadge, }), [ addRelationShipContainerElement, currentPage, eventStatus, passOnProps, program, - programWriteAccess, - trackedEntityTypeWriteAccess, - programStageWriteAccess, - programStageReadAccess, - hideWidgetReadOnlyBadge, + access, toggleVisibility, ]); @@ -224,15 +169,7 @@ const EnrollmentPageLayoutPlain = ({ userInteractionInProgress={userInteractionInProgress} eventStatus={eventStatus} /> - +
{pageLayout.leftColumn && !!leftColumnWidgets?.length && ( diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts index 005d79edfe..e1d6510241 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts @@ -60,7 +60,7 @@ export const StagesAndEvents: WidgetConfig = { onRollbackDeleteEvent, onEventClick, ruleEffects, - hideEventStageBadge, + hideWidgetBadge, }: any): StagesAndEventProps => ({ programId: program.id, stages, @@ -72,7 +72,7 @@ export const StagesAndEvents: WidgetConfig = { onRollbackDeleteEvent, onEventClick, ruleEffects, - hideReadOnlyBadge: Boolean(hideEventStageBadge), + hideReadOnlyBadge: hideWidgetBadge, }), }; @@ -92,7 +92,7 @@ export const TrackedEntityRelationship: WidgetConfig = { programWriteAccess, trackedEntityTypeWriteAccess, programStageWriteAccess, - hideEventStageBadge, + hideWidgetBadge, }: any): TrackedEntityRelationshipProps => ({ trackedEntityTypeId: program.trackedEntityType.id, programId: program.id, @@ -104,7 +104,7 @@ export const TrackedEntityRelationship: WidgetConfig = { onLinkedRecordClick, readOnly: !trackedEntityTypeWriteAccess, hideButton: !programWriteAccess && !trackedEntityTypeWriteAccess && !programStageWriteAccess, - hideReadOnlyBadge: Boolean(hideEventStageBadge), + hideReadOnlyBadge: hideWidgetBadge, }), }; @@ -142,8 +142,8 @@ export const IndicatorWidget: WidgetConfig = { export const EnrollmentNote: WidgetConfig = { Component: WidgetEnrollmentNote, - getProps: ({ hideEventStageBadge }: any) => ({ - hideReadOnlyBadge: Boolean(hideEventStageBadge), + getProps: ({ hideWidgetBadge }: any) => ({ + hideReadOnlyBadge: hideWidgetBadge, }), }; @@ -215,7 +215,7 @@ export const EnrollmentWidget: WidgetConfig = { onUpdateEnrollmentStatusError, onEnrollmentError, onAccessLostFromTransfer, - hideEventStageBadge, + hideWidgetBadge, }: any): WidgetEnrollmentProps => ({ teiId, enrollmentId, @@ -230,7 +230,7 @@ export const EnrollmentWidget: WidgetConfig = { externalData: { status: widgetEnrollmentStatus, events }, onError: onEnrollmentError, onAccessLostFromTransfer, - hideReadOnlyBadge: Boolean(hideEventStageBadge), + hideReadOnlyBadge: hideWidgetBadge, }), }; @@ -307,12 +307,12 @@ export const AssigneeWidget: WidgetConfig = { export const EventNote: WidgetConfig = { Component: WidgetEventNote, - getProps: ({ dataEntryKey, dataEntryId, program, stageId, programStage, hideEventStageBadge }: any) => ({ + getProps: ({ dataEntryKey, dataEntryId, program, stageId, programStage, hideWidgetBadge }: any) => ({ dataEntryKey, dataEntryId, programId: program?.id, stageId: stageId ?? programStage?.id, - hideReadOnlyBadge: Boolean(hideEventStageBadge), + hideReadOnlyBadge: hideWidgetBadge, }), }; diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/index.ts b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/index.ts index f8a7d316da..fba2ed7c0a 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/index.ts +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/index.ts @@ -21,3 +21,7 @@ export { } from './enrollment.actions'; export { useCommonEnrollmentDomainData } from './useCommonEnrollmentDomainData'; export { useRuleEffects } from './useRuleEffects'; +export { + EnrollmentAccessProvider, + useEnrollmentAccessContext, +} from './EnrollmentAccessContext'; diff --git a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx index 75364a1809..d65efcfdcb 100644 --- a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx +++ b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx @@ -120,8 +120,8 @@ export const ReadOnlyBadge = ({ - - {i18n.t('View only')} + + {i18n.t('View only')} {' - '} {message ?? i18n.t('You only have view access')} diff --git a/src/core_modules/capture-core/hooks/index.ts b/src/core_modules/capture-core/hooks/index.ts index 9c0bd08a30..061192723d 100644 --- a/src/core_modules/capture-core/hooks/index.ts +++ b/src/core_modules/capture-core/hooks/index.ts @@ -7,4 +7,3 @@ export { useScopeInfo } from './useScopeInfo'; export { useScopeTitleText } from './useScopeTitleText'; export { useProgramExpiryForUser } from './useProgramExpiryForUser'; export { useHideWidgetByRuleLocations } from './useHideWidgetByRuleLocations'; -export { useEnrollmentAccess } from './useEnrollmentAccess'; diff --git a/src/core_modules/capture-core/hooks/useEnrollmentAccess.ts b/src/core_modules/capture-core/hooks/useEnrollmentAccess.ts deleted file mode 100644 index 405f54d36b..0000000000 --- a/src/core_modules/capture-core/hooks/useEnrollmentAccess.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { useMemo } from 'react'; -import { useDataQuery } from '@dhis2/app-runtime'; - -type ProgramResponse = { - access?: { data?: { read?: boolean; write?: boolean } }; - trackedEntityType?: { access?: { data?: { read?: boolean; write?: boolean } } }; - programStages?: Array<{ access?: { data?: { read?: boolean; write?: boolean } } }>; -}; - -type Result = { - programWriteAccess: boolean; - trackedEntityTypeWriteAccess: boolean; - programStageWriteAccess: boolean; - programStageReadAccess: boolean; - isLoading: boolean; - error?: any; -}; - -export const useEnrollmentAccess = (programId?: string): Result => { - const { error, loading, data } = useDataQuery( - useMemo( - () => ({ - program: { - resource: `programs/${programId}`, - params: { - fields: ['access,trackedEntityType[access],programStages[access]'], - }, - }, - }), - [programId], - ), - { lazy: !programId } as any, - ); - - const program = data?.program as ProgramResponse | undefined; - - return { - programWriteAccess: Boolean(program?.access?.data?.write), - trackedEntityTypeWriteAccess: Boolean(program?.trackedEntityType?.access?.data?.write), - programStageWriteAccess: Boolean( - program?.programStages?.some(stage => stage?.access?.data?.write), - ), - programStageReadAccess: Boolean( - program?.programStages?.some(stage => stage?.access?.data?.read), - ), - isLoading: loading, - error, - }; -}; From c3e04aca62a9aec0e1bcadfb10b8efc5a076c7fc Mon Sep 17 00:00:00 2001 From: henrikmv Date: Thu, 7 May 2026 20:02:48 +0200 Subject: [PATCH 35/60] fet: remove hideReadOnlyBadge prop --- .../EnrollmentQuickActions.component.tsx | 4 ++++ .../EnrollmentPageLayout.tsx | 8 ------- .../LayoutComponentConfig.ts | 21 ++----------------- ...edEntityRelationshipsWrapper.component.tsx | 14 ++++++------- ...TrackedEntityRelationshipsWrapper.types.ts | 4 ---- .../WidgetEnrollment.component.tsx | 5 +++-- .../WidgetEnrollment.container.tsx | 2 -- .../WidgetEnrollment/enrollment.types.ts | 2 -- .../WidgetEnrollmentNote.component.tsx | 16 +++++--------- .../WidgetEventNote.component.tsx | 14 ++++++------- .../WidgetEventNote/WidgetEventNote.types.ts | 3 --- .../Stages/Stage/Stage.component.tsx | 3 +-- .../StageOverview/StageOverview.component.tsx | 7 +++++-- .../StageOverview/stageOverview.types.ts | 1 - .../Stages/Stage/stage.types.ts | 1 - .../Stages/Stages.component.tsx | 2 -- .../Stages/stages.types.ts | 2 -- .../WidgetStagesAndEvents.component.tsx | 6 +++--- .../stagesAndEvents.types.ts | 1 - 19 files changed, 36 insertions(+), 80 deletions(-) diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/EnrollmentQuickActions.component.tsx b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/EnrollmentQuickActions.component.tsx index 3ce36927d4..0620465e84 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/EnrollmentQuickActions.component.tsx +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/EnrollmentQuickActions.component.tsx @@ -7,6 +7,7 @@ import { Widget } from '../../../../Widget'; import { QuickActionButton } from './QuickActionButton/QuickActionButton'; import { tabMode } from '../../../EnrollmentAddEvent/NewEventWorkspace/newEventWorkspace.constants'; import { useNavigate, buildUrlQueryString, useLocationQuery } from '../../../../../utils/routing'; +import { useEnrollmentAccessContext } from '../../../common/EnrollmentOverviewDomain/EnrollmentAccessContext'; import { OwnProps, ProgramStage, EventCount } from './EnrollmentQuickActions.types'; const styles = { @@ -28,6 +29,7 @@ const EnrollmentQuickActionsComponentPlain = ({ const [open, setOpen] = useState(true); const { navigate } = useNavigate(); const { enrollmentId, programId, teiId, orgUnitId } = useLocationQuery(); + const { programStageWriteAccess } = useEnrollmentAccessContext(); const stagesWithEventCount = useMemo(() => stages.map((stage) => { const mutatedStage = { ...stage }; @@ -61,6 +63,8 @@ const EnrollmentQuickActionsComponentPlain = ({ const ready: boolean = events !== undefined && stages !== undefined; + if (!programStageWriteAccess) return null; + return ( (undefined); const toggleVisibility = useCallback(() => setMainContentVisibility(current => !current), []); - const access = useEnrollmentAccessContext(); - const allProps = useMemo(() => ({ ...passOnProps, program, @@ -118,18 +116,12 @@ const EnrollmentPageLayoutPlain = ({ eventStatus, toggleVisibility, addRelationShipContainerElement, - programWriteAccess: access.programWriteAccess, - trackedEntityTypeWriteAccess: access.trackedEntityTypeWriteAccess, - programStageWriteAccess: access.programStageWriteAccess, - programStageReadAccess: access.programStageReadAccess, - hideWidgetBadge: access.hideWidgetBadge, }), [ addRelationShipContainerElement, currentPage, eventStatus, passOnProps, program, - access, toggleVisibility, ]); diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts index e1d6510241..f0e700a21f 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts @@ -39,7 +39,6 @@ import { export const QuickActions: WidgetConfig = { Component: EnrollmentQuickActions, - shouldHideWidget: ({ programStageWriteAccess }: any) => !programStageWriteAccess, getProps: ({ stages, events, ruleEffects }: any) => ({ stages, events, @@ -60,7 +59,6 @@ export const StagesAndEvents: WidgetConfig = { onRollbackDeleteEvent, onEventClick, ruleEffects, - hideWidgetBadge, }: any): StagesAndEventProps => ({ programId: program.id, stages, @@ -72,7 +70,6 @@ export const StagesAndEvents: WidgetConfig = { onRollbackDeleteEvent, onEventClick, ruleEffects, - hideReadOnlyBadge: hideWidgetBadge, }), }; @@ -89,10 +86,6 @@ export const TrackedEntityRelationship: WidgetConfig = { toggleVisibility, teiId, onLinkedRecordClick, - programWriteAccess, - trackedEntityTypeWriteAccess, - programStageWriteAccess, - hideWidgetBadge, }: any): TrackedEntityRelationshipProps => ({ trackedEntityTypeId: program.trackedEntityType.id, programId: program.id, @@ -102,9 +95,6 @@ export const TrackedEntityRelationship: WidgetConfig = { onCloseAddRelationship: toggleVisibility, teiId, onLinkedRecordClick, - readOnly: !trackedEntityTypeWriteAccess, - hideButton: !programWriteAccess && !trackedEntityTypeWriteAccess && !programStageWriteAccess, - hideReadOnlyBadge: hideWidgetBadge, }), }; @@ -142,9 +132,7 @@ export const IndicatorWidget: WidgetConfig = { export const EnrollmentNote: WidgetConfig = { Component: WidgetEnrollmentNote, - getProps: ({ hideWidgetBadge }: any) => ({ - hideReadOnlyBadge: hideWidgetBadge, - }), + getProps: () => ({}), }; export const ProfileWidget: WidgetConfig = { @@ -215,7 +203,6 @@ export const EnrollmentWidget: WidgetConfig = { onUpdateEnrollmentStatusError, onEnrollmentError, onAccessLostFromTransfer, - hideWidgetBadge, }: any): WidgetEnrollmentProps => ({ teiId, enrollmentId, @@ -230,7 +217,6 @@ export const EnrollmentWidget: WidgetConfig = { externalData: { status: widgetEnrollmentStatus, events }, onError: onEnrollmentError, onAccessLostFromTransfer, - hideReadOnlyBadge: hideWidgetBadge, }), }; @@ -307,12 +293,9 @@ export const AssigneeWidget: WidgetConfig = { export const EventNote: WidgetConfig = { Component: WidgetEventNote, - getProps: ({ dataEntryKey, dataEntryId, program, stageId, programStage, hideWidgetBadge }: any) => ({ + getProps: ({ dataEntryKey, dataEntryId }: any) => ({ dataEntryKey, dataEntryId, - programId: program?.id, - stageId: stageId ?? programStage?.id, - hideReadOnlyBadge: hideWidgetBadge, }), }; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.tsx b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.tsx index 37f363142f..5cfff417e2 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.tsx +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.tsx @@ -15,6 +15,7 @@ import { import { ResultsPageSizeContext } from '../../../shared-contexts'; import { RegisterTei } from '../RegisterTei'; import { useCoreOrgUnit } from '../../../../../metadataRetrieval/coreOrgUnit'; +import { useEnrollmentAccessContext } from '../../EnrollmentOverviewDomain/EnrollmentAccessContext'; const createResultsView = (onLinkToTrackedEntityFromSearch: any) => (viewProps: any) => ( { - const effectiveReadOnly = Boolean(readOnlyMode) || readOnly; + const { trackedEntityTypeWriteAccess, allWriteAccessMissing, hideWidgetBadge } = useEnrollmentAccessContext(); + const accessReadOnly = !trackedEntityTypeWriteAccess; + const effectiveReadOnly = Boolean(readOnlyMode) || accessReadOnly; const dispatch = useDispatch(); const { relationshipTypes, isError } = useTEIRelationshipsWidgetMetadata(); const { orgUnit } = useCoreOrgUnit(orgUnitId); @@ -80,9 +80,9 @@ export const TrackedEntityRelationshipsWrapper = ({ onSelectFindMode={onSelectFindMode} relationshipTypes={relationshipTypes} readOnly={effectiveReadOnly} - accessReadOnly={readOnly} - hideButton={hideButton} - hideReadOnlyBadge={hideReadOnlyBadge} + accessReadOnly={accessReadOnly} + hideButton={allWriteAccessMissing} + hideReadOnlyBadge={hideWidgetBadge} renderTrackedEntityRegistration={( selectedTrackedEntityTypeId, suggestedProgramId, diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types.ts b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types.ts index e3b12111a9..96636b7c10 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types.ts +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types.ts @@ -9,9 +9,5 @@ export type Props = { onOpenAddRelationship: () => void; onCloseAddRelationship: () => void; onLinkedRecordClick: LinkedRecordClick; - readOnly: boolean; readOnlyMode?: boolean; - hideButton?: boolean; - accessReadOnly?: boolean; - hideReadOnlyBadge?: boolean; }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx index daf4bf83e2..de2a6e9b57 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx @@ -14,6 +14,7 @@ import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { LoadingMaskElementCenter } from '../LoadingMasks'; import { Widget } from '../Widget'; import { ReadOnlyBadge } from '../ReadOnlyBadge'; +import { useEnrollmentAccessContext } from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; import type { PlainProps } from './enrollment.types'; import { Status } from './Status'; import { dataElementTypes } from '../../metaData'; @@ -59,7 +60,6 @@ const WidgetEnrollmentPlain = ({ loading, canAddNew, readOnlyMode, - hideReadOnlyBadge, programDataWriteAccess, trackedEntityTypeDataWriteAccess, displayAutoGeneratedEventWarning, @@ -75,6 +75,7 @@ const WidgetEnrollmentPlain = ({ onAccessLostFromTransfer, }: PlainProps & WithStyles) => { const enrollmentReadOnly = readOnlyMode || !programDataWriteAccess; + const { hideWidgetBadge } = useEnrollmentAccessContext(); const [open, setOpenStatus] = useState(true); const { fromServerDate } = useTimeZoneConversion(); const updatedAtDateTime: string = convertValue( @@ -93,7 +94,7 @@ const WidgetEnrollmentPlain = ({ header={
{i18n.t('Enrollment')} - {!hideReadOnlyBadge && ( + {!hideWidgetBadge && (
> | null }; onDelete: () => void; onAddNew: () => void; @@ -55,7 +54,6 @@ export type PlainProps = { loading: boolean; canAddNew: boolean; readOnlyMode: boolean; - hideReadOnlyBadge?: boolean; programDataWriteAccess: boolean; trackedEntityTypeDataWriteAccess: boolean; displayAutoGeneratedEventWarning: boolean; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx index e15d6ce643..d1d80feb2a 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx @@ -3,17 +3,13 @@ import i18n from '@dhis2/d2-i18n'; import { useDispatch, useSelector } from 'react-redux'; import { requestAddNoteForEnrollment } from './WidgetEnrollmentNote.actions'; import { WidgetNote } from '../WidgetNote'; -import { useProgram } from '../WidgetEnrollment/hooks/useProgram'; +import { useEnrollmentAccessContext } from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; import { useLocationQuery } from '../../utils/routing'; -type Props = { - hideReadOnlyBadge?: boolean; -}; - -export const WidgetEnrollmentNote = ({ hideReadOnlyBadge }: Props) => { +export const WidgetEnrollmentNote = () => { const dispatch = useDispatch(); - const { enrollmentId, programId } = useLocationQuery(); - const { program } = useProgram(programId); + const { enrollmentId } = useLocationQuery(); + const { programWriteAccess, hideWidgetBadge } = useEnrollmentAccessContext(); const notes = useSelector(({ enrollmentDomain }: { enrollmentDomain?: { enrollment?: { notes?: Array } } }) => enrollmentDomain?.enrollment?.notes ?? []); @@ -21,8 +17,6 @@ export const WidgetEnrollmentNote = ({ hideReadOnlyBadge }: Props) => { dispatch(requestAddNoteForEnrollment(enrollmentId, newNoteValue)); }; - const programWriteAccess = Boolean(program?.access?.data?.write); - return (
{ onAddNote={onAddNote} readOnly={!programWriteAccess} programWriteAccess={programWriteAccess} - hideReadOnlyBadge={hideReadOnlyBadge} + hideReadOnlyBadge={hideWidgetBadge} />
); diff --git a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx index bfce44ce51..36ffcdf4df 100644 --- a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx @@ -4,13 +4,11 @@ import i18n from '@dhis2/d2-i18n'; import type { Props } from './WidgetEventNote.types'; import { requestAddNoteForEvent } from './WidgetEventNote.actions'; import { WidgetNote } from '../WidgetNote'; -import { useProgram } from '../WidgetEnrollment/hooks/useProgram'; +import { useEnrollmentAccessContext } from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; -export const WidgetEventNote = ({ dataEntryKey, dataEntryId, programId, stageId, hideReadOnlyBadge }: Props) => { +export const WidgetEventNote = ({ dataEntryKey, dataEntryId }: Props) => { const dispatch = useDispatch(); - const { program } = useProgram(programId ?? ''); - const liveStage = program?.programStages?.find((s: any) => s.id === stageId); - const stageWriteAccess = program ? Boolean(liveStage?.access?.data?.write) : true; + const { currentStageWriteAccess, hideWidgetBadge } = useEnrollmentAccessContext(); const notes = useSelector(({ dataEntriesNotes }: { dataEntriesNotes: Record }) => dataEntriesNotes[`${dataEntryId}-${dataEntryKey}`] ?? []); @@ -27,9 +25,9 @@ export const WidgetEventNote = ({ dataEntryKey, dataEntryId, programId, stageId, emptyNoteMessage={i18n.t('This event doesn\'t have any notes')} notes={notes} onAddNote={onAddNote} - readOnly={!stageWriteAccess} - programStageWriteAccess={stageWriteAccess} - hideReadOnlyBadge={hideReadOnlyBadge} + readOnly={!currentStageWriteAccess} + programStageWriteAccess={currentStageWriteAccess} + hideReadOnlyBadge={hideWidgetBadge} />
); diff --git a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.types.ts b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.types.ts index 1ce0910d0e..87957e6627 100644 --- a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.types.ts +++ b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.types.ts @@ -1,9 +1,6 @@ export type Props = { dataEntryKey: string; dataEntryId: string; - programId?: string; - stageId?: string; - hideReadOnlyBadge?: boolean; }; export type ClientNote = { diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/Stage.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/Stage.component.tsx index 814862baac..683bc19052 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/Stage.component.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/Stage.component.tsx @@ -26,7 +26,7 @@ const rulesEffectHideProgramStage = (ruleEffects: Array<{id: string, type: strin ); export const StagePlain = ({ - stage, events, classes, onCreateNew, ruleEffects, stageWriteAccess, hideReadOnlyBadge, ...passOnProps + stage, events, classes, onCreateNew, ruleEffects, stageWriteAccess, ...passOnProps }: Props & WithStyles) => { const [open, setOpenStatus] = useState(true); const { id, name, icon, description, dataElements, hideDueDate, repeatable, enableUserAssignment } = stage; @@ -51,7 +51,6 @@ export const StagePlain = ({ description={description} events={events} stageWriteAccess={effectiveStageWriteAccess} - hideReadOnlyBadge={hideReadOnlyBadge} />} onOpen={handleOpen} onClose={handleClose} diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.tsx index 94b3f92f2b..6a17771b99 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.tsx @@ -11,6 +11,7 @@ import moment from 'moment'; import { statusTypes } from 'capture-core/events/statusTypes'; import { NonBundledDhis2Icon } from '../../../../NonBundledDhis2Icon'; import { ReadOnlyBadge } from '../../../../ReadOnlyBadge'; +import { useEnrollmentAccessContext } from '../../../../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; import type { Props } from './stageOverview.types'; import { isEventOverdue } from '../StageDetail/hooks/helpers'; import { convertValue as convertValueClientToView } from '../../../../../converters/clientToView'; @@ -92,9 +93,11 @@ const getLastUpdatedAt = (events: Array, fromServerDate: (da }; export const StageOverviewPlain = ({ - title, icon, description, events, stageWriteAccess = true, hideReadOnlyBadge = false, classes, + title, icon, description, events, stageWriteAccess = true, classes, }: Props & WithStyles) => { const { fromServerDate } = useTimeZoneConversion(); + const { hideWidgetBadge, programStageWriteAccess } = useEnrollmentAccessContext(); + const hideStageBadge = hideWidgetBadge || !programStageWriteAccess; const totalEvents = events.length; const overdueEvents = events.filter(isEventOverdue).length; const scheduledEvents = events.filter(event => event.status === statusTypes.SCHEDULE).length; @@ -157,7 +160,7 @@ export const StageOverviewPlain = ({
{getLastUpdatedAt(events, fromServerDate)}
} - {!hideReadOnlyBadge && ( + {!hideStageBadge && ( ; onEventClick: (eventId: string) => void; onDeleteEvent: (eventId: string) => void; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stages.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stages.component.tsx index 11b485a3e1..7021dcadc5 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stages.component.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stages.component.tsx @@ -22,7 +22,6 @@ export const StagesPlain = ({ stageWriteAccessById, stageReadAccessById, programLoaded, - hideReadOnlyBadge, ...passOnProps }: PlainProps) => { const readableStages = useMemo( @@ -72,7 +71,6 @@ export const StagesPlain = ({ key={stage.id} stage={stage} stageWriteAccess={stageWriteAccessById?.[stage.id] ?? stage.dataAccess.write} - hideReadOnlyBadge={hideReadOnlyBadge} {...passOnProps} /> )) diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/stages.types.ts b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/stages.types.ts index 43049cf74f..3ffe7bc75d 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/stages.types.ts +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/stages.types.ts @@ -7,7 +7,6 @@ export type PlainProps = { stageWriteAccessById?: Record; stageReadAccessById?: Record; programLoaded?: boolean; - hideReadOnlyBadge?: boolean; onEventClick: (eventId: string) => void; onDeleteEvent: (eventId: string) => void; onUpdateEventStatus: (eventId: string, status: string) => void; @@ -20,7 +19,6 @@ export type InputProps = { stageWriteAccessById?: Record; stageReadAccessById?: Record; programLoaded?: boolean; - hideReadOnlyBadge?: boolean; onEventClick: (eventId: string) => void; onDeleteEvent: (eventId: string) => void; onUpdateEventStatus: (eventId: string, status: string) => void; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx index 14021da975..fd6bd5fd0c 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx @@ -6,6 +6,7 @@ import { Widget } from '../Widget'; import { ReadOnlyBadge } from '../ReadOnlyBadge'; import { Stages } from './Stages'; import { useProgram } from '../WidgetEnrollment/hooks/useProgram'; +import { useEnrollmentAccessContext } from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; import type { Props } from './stagesAndEvents.types'; const styles = { @@ -26,11 +27,11 @@ const WidgetStagesAndEventsPlain = ({ stages, events, programId, - hideReadOnlyBadge, ...passOnProps }: Props & WithStyles) => { const [open, setOpenStatus] = useState(true); const { program } = useProgram(programId); + const { hideWidgetBadge } = useEnrollmentAccessContext(); const stageWriteAccessById = useMemo(() => { const map: Record = {}; (program?.programStages ?? []).forEach((stage: any) => { @@ -57,7 +58,7 @@ const WidgetStagesAndEventsPlain = ({ header={
{i18n.t('Stages and Events')} - {!hideReadOnlyBadge && ( + {!hideWidgetBadge && (
diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/stagesAndEvents.types.ts b/src/core_modules/capture-core/components/WidgetStagesAndEvents/stagesAndEvents.types.ts index c264c27ef2..ac9dc3e5c2 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/stagesAndEvents.types.ts +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/stagesAndEvents.types.ts @@ -9,7 +9,6 @@ type ExtractedProps = { onUpdateEventStatus: (eventId: string, status: string) => void; onRollbackDeleteEvent: (eventId: ApiEnrollmentEvent) => void; className?: string; - hideReadOnlyBadge?: boolean; }; export type Props = ExtractedProps & StageCommonProps; From 4dc0083dd5b4ce567e8e346f542c764e8dde8842 Mon Sep 17 00:00:00 2001 From: henrikmv Date: Fri, 8 May 2026 08:27:11 +0200 Subject: [PATCH 36/60] feat: simplify access handling in ReadOnlyBadge and related components --- i18n/en.pot | 41 ++--- .../EnrollmentPageLayout.tsx | 31 ++-- .../ReadOnlyBadge/ReadOnlyBadge.tsx | 142 +++++------------- .../Actions/Actions.component.tsx | 1 - .../WidgetEnrollment/Date/Date.component.tsx | 25 +-- .../Coordinates/Coordinates.component.tsx | 15 +- .../MapModal/Coordinates/Coordinates.types.ts | 2 +- .../MapModal/MapModal.component.tsx | 9 +- .../MapModal/MapModal.types.ts | 4 +- .../MapModal/Polygon/Polygon.component.tsx | 4 +- .../MapModal/Polygon/Polygon.types.ts | 2 +- .../MiniMap/MiniMap.component.tsx | 3 +- .../WidgetEnrollment/MiniMap/MiniMap.types.ts | 1 - .../WidgetEnrollment.component.tsx | 16 +- .../WidgetEnrollment.container.tsx | 5 - .../WidgetEnrollment/enrollment.types.ts | 2 - .../WidgetNote/WidgetNote.component.tsx | 1 - .../DataEntry/DataEntry.component.tsx | 7 +- .../StageOverview/StageOverview.component.tsx | 1 - .../WidgetStagesAndEvents.component.tsx | 3 +- ...getTrackedEntityRelationship.component.tsx | 1 + .../RelationshipsWidget.component.tsx | 6 +- .../relationshipsWidget.types.ts | 1 + 23 files changed, 115 insertions(+), 208 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 3de7199210..9639152821 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: 2026-05-07T15:17:32.940Z\n" -"PO-Revision-Date: 2026-05-07T15:17:32.940Z\n" +"POT-Creation-Date: 2026-05-08T06:27:13.394Z\n" +"PO-Revision-Date: 2026-05-08T06:27:13.394Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -1071,42 +1071,27 @@ msgstr "Possible duplicates found" msgid "An error occurred loading possible duplicates" msgstr "An error occurred loading possible duplicates" -msgid "these program stages" -msgstr "these program stages" - -msgid "this program stage" -msgstr "this program stage" - -msgid "program" -msgstr "program" - -msgid "and" -msgstr "and" - -msgid "You only have view access to {{targets}}" -msgstr "You only have view access to {{targets}}" +msgid "You only have view access to enrollment" +msgstr "You only have view access to enrollment" msgid "You only have view access to program" msgstr "You only have view access to program" +msgid "You only have view access to {{trackedEntityName}}" +msgstr "You only have view access to {{trackedEntityName}}" + +msgid "You only have view access to this tracked entity type" +msgstr "You only have view access to this tracked entity type" + msgid "You only have view access to these program stages" msgstr "You only have view access to these program stages" msgid "You only have view access to this program stage" msgstr "You only have view access to this program stage" -msgid "You only have view access to enrollment" -msgstr "You only have view access to enrollment" - -msgid "person" -msgstr "person" - msgid "View only" msgstr "View only" -msgid "You only have view access" -msgstr "You only have view access" - msgid "You don't have access to delete this relationship" msgstr "You don't have access to delete this relationship" @@ -1398,9 +1383,6 @@ msgstr "An error occurred while transferring ownership" msgid "Existing dates for auto-generated events will not be updated." msgstr "Existing dates for auto-generated events will not be updated." -msgid "You do not have access to edit this date" -msgstr "You do not have access to edit this date" - msgid "Latitude" msgstr "Latitude" @@ -1632,9 +1614,6 @@ msgstr "{{trackedEntityName}} profile" msgid "Edit {{trackedEntityName}}" msgstr "Edit {{trackedEntityName}}" -msgid "You only have view access to this {{trackedEntityName}}" -msgstr "You only have view access to this {{trackedEntityName}}" - msgid "Change information about this {{trackedEntityName}} here." msgstr "Change information about this {{trackedEntityName}} here." diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx index 83af393791..714f6dd79b 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx @@ -61,7 +61,6 @@ const getEnrollmentPageStyles: Readonly = () => ({ const isValidHex = (color: string) => /^#[0-9A-F]{6}$/i.test(color); const EnrollmentReadOnlyBadge = () => { - const access = useEnrollmentAccessContext(); const { isEventPage, currentStageWriteAccess, @@ -69,19 +68,29 @@ const EnrollmentReadOnlyBadge = () => { trackedEntityTypeWriteAccess, programStageWriteAccess, programStageReadAccess, - multipleStages, trackedEntityTypeName, - } = access; - const stageBadgeAccess = isEventPage - ? currentStageWriteAccess - : programStageWriteAccess || !programStageReadAccess; + } = useEnrollmentAccessContext(); + + if (isEventPage) { + if (currentStageWriteAccess) return null; + return ( + + ); + } + + const stageBadgeAccess = programStageWriteAccess || !programStageReadAccess; + const allMissing = !programWriteAccess && !trackedEntityTypeWriteAccess && !stageBadgeAccess; + if (!allMissing) return null; + return ( diff --git a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx index d65efcfdcb..f155c2f93a 100644 --- a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx +++ b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx @@ -1,137 +1,69 @@ import React from 'react'; -import { colors, IconInfo16, Tag } from '@dhis2/ui'; +import { IconInfo16, Tag } from '@dhis2/ui'; import i18n from '@dhis2/d2-i18n'; import { ConditionalTooltip } from '../Tooltips/ConditionalTooltip'; type Props = { - readOnly?: boolean; programWriteAccess?: boolean; trackedEntityTypeWriteAccess?: boolean; programStageWriteAccess?: boolean; multipleStages?: boolean; trackedEntityName?: string; - label?: string; inlineLabel?: boolean; }; -const interpolate = (key: string, trackedEntityName: string) => - i18n.t(key, { trackedEntityName, interpolation: { escapeValue: false } }); +type Access = { + program: boolean; + trackedEntityType: boolean; + programStage: boolean; +}; -const stageTarget = (multipleStages: boolean) => - (multipleStages ? i18n.t('these program stages') : i18n.t('this program stage')); +const getEnrollmentMessage = (): string => i18n.t('You only have view access to enrollment'); -const getMultiMissingLabel = ( - missingProgram: boolean, - missingTET: boolean, - missingStage: boolean, - multipleStages: boolean, - trackedEntityName: string, -): string | null => { - const parts: Array = []; - if (missingProgram) parts.push(i18n.t('program')); - if (missingTET) parts.push(trackedEntityName); - if (missingStage) parts.push(stageTarget(multipleStages)); - if (parts.length < 2) return null; - const last = parts.pop() as string; - const joined = `${parts.join(', ')} ${i18n.t('and')} ${last}`; - return i18n.t('You only have view access to {{targets}}', { - targets: joined, - interpolation: { escapeValue: false }, - }); -}; +const getProgramMessage = (): string => i18n.t('You only have view access to program'); -const getSingleMissingLabel = ( - missingProgram: boolean, - missingTET: boolean, - missingStage: boolean, - multipleStages: boolean, - trackedEntityName: string, -): string | null => { - if (missingProgram) return i18n.t('You only have view access to program'); - if (missingTET) { - return interpolate( - 'You only have view access to {{trackedEntityName}}', - trackedEntityName, - ); - } - if (missingStage) { - return multipleStages - ? i18n.t('You only have view access to these program stages') - : i18n.t('You only have view access to this program stage'); - } - return null; -}; +const getTrackedEntityMessage = (trackedEntityName: string | undefined): string => (trackedEntityName + ? i18n.t('You only have view access to {{trackedEntityName}}', { trackedEntityName, escapeValue: false }) + : i18n.t('You only have view access to this tracked entity type')); + +const getProgramStageMessage = (multipleStages: boolean): string => (multipleStages + ? i18n.t('You only have view access to these program stages') + : i18n.t('You only have view access to this program stage')); -const getDefaultLabel = ( - programWriteAccess: boolean, - trackedEntityTypeWriteAccess: boolean, - programStageWriteAccess: boolean, +const getMissingAccessMessage = ( + access: Access, + trackedEntityName: string | undefined, multipleStages: boolean, - trackedEntityName: string, -): string | null => { - const mp = !programWriteAccess; - const mt = !trackedEntityTypeWriteAccess; - const ms = !programStageWriteAccess; - if (mp && mt && ms) return i18n.t('You only have view access to enrollment'); - return getMultiMissingLabel(mp, mt, ms, multipleStages, trackedEntityName) - ?? getSingleMissingLabel(mp, mt, ms, multipleStages, trackedEntityName); +): string => { + if (!access.program && !access.trackedEntityType && !access.programStage) return getEnrollmentMessage(); + if (!access.program) return getProgramMessage(); + if (!access.trackedEntityType) return getTrackedEntityMessage(trackedEntityName); + if (!access.programStage) return getProgramStageMessage(multipleStages); + return ''; }; export const ReadOnlyBadge = ({ - readOnly, programWriteAccess = true, trackedEntityTypeWriteAccess = true, programStageWriteAccess = true, multipleStages = false, trackedEntityName, - label, inlineLabel = false, }: Props) => { - const allAccessMissing = !programWriteAccess - && !trackedEntityTypeWriteAccess - && !programStageWriteAccess; - const isReadOnly = readOnly || allAccessMissing; - if (!isReadOnly) return null; - const message = label - ?? getDefaultLabel( - programWriteAccess, - trackedEntityTypeWriteAccess, - programStageWriteAccess, - multipleStages, - trackedEntityName ?? i18n.t('person'), - ); - if (inlineLabel) { - return ( - - - - - - {i18n.t('View only')} - {' - '} - {message ?? i18n.t('You only have view access')} - - - ); - } + if (programWriteAccess && trackedEntityTypeWriteAccess && programStageWriteAccess) return null; + + const access: Access = { + program: programWriteAccess, + trackedEntityType: trackedEntityTypeWriteAccess, + programStage: programStageWriteAccess, + }; + const message = getMissingAccessMessage(access, trackedEntityName, multipleStages); + const labelText = inlineLabel && message ? `${i18n.t('View only')} - ${message}` : i18n.t('View only'); + return ( - - }> - {i18n.t('View only')} + + }> + {labelText} ); diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.component.tsx index 1f594bb5c8..0414f6e2ea 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.component.tsx @@ -136,7 +136,6 @@ const ActionsPlain = ({
)} {isOpenMap && void; allowFutureDate?: boolean; @@ -94,7 +92,6 @@ const DateComponentPlain = ({ dateLabel, locale, readOnly, - hideEdit, displayAutoGeneratedEventWarning, onSave, allowFutureDate, @@ -212,21 +209,15 @@ const DateComponentPlain = ({ {displayDate} - {!hideEdit && ( - - - + + )}
); diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/Coordinates.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/Coordinates.component.tsx index 04cad00d72..3f0f13357e 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/Coordinates.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/Coordinates.component.tsx @@ -77,13 +77,14 @@ const CoordinatesPlain = ({ }; const onHandleMapClicked = (mapCoordinates: { latlng: { lat: number; lng: number } }) => { - if (readOnly || !isEditing) return; - const { lat, lng } = mapCoordinates.latlng; - const newPosition: [number, number] = [lat, lng]; - setValid(true); - setPosition(newPosition); - setTempLatitude(lat); - setTempLongitude(lng); + if (isEditing) { + const { lat, lng } = mapCoordinates.latlng; + const newPosition: [number, number] = [lat, lng]; + setValid(true); + setPosition(newPosition); + setTempLatitude(lat); + setTempLongitude(lng); + } }; const onSearch = (searchPosition: [number, number]) => { diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/Coordinates.types.ts b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/Coordinates.types.ts index 135a129c70..7205dff80d 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/Coordinates.types.ts +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/Coordinates.types.ts @@ -3,5 +3,5 @@ export type CoordinatesProps = { setOpen: (open: boolean) => void; onSetCoordinates: (coordinates: [number, number] | Array> | null) => void; defaultValues?: [number, number] | null; - readOnly: boolean; + readOnly?: boolean; }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/MapModal.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/MapModal.component.tsx index 37b3a62ea2..b0332fc48d 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/MapModal.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/MapModal.component.tsx @@ -5,7 +5,14 @@ import './MapModal.css'; import { Coordinates } from './Coordinates'; import { Polygon } from './Polygon'; -export const MapModal = ({ type, center, setOpen, onSetCoordinates, defaultValues, readOnly }: MapModalComponentProps) => ( +export const MapModal = ({ + type, + center, + setOpen, + onSetCoordinates, + defaultValues, + readOnly, +}: MapModalComponentProps) => ( <> {type === dataElementTypes.COORDINATE && ( void; onSetCoordinates: (coordinates: [number, number] | Array> | null) => void; - readOnly: boolean; + readOnly?: boolean; } export type MapModalProps = { @@ -15,5 +15,5 @@ export type MapModalProps = { onUpdate: (arg: Record) => void; setOpenMap: (toggle: boolean) => void; defaultValues?: number[][] | [number, number] | null; - readOnly: boolean; + readOnly?: boolean; }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/Polygon.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/Polygon.component.tsx index d82620b62c..482f193f9f 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/Polygon.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/Polygon.component.tsx @@ -76,7 +76,6 @@ const PolygonPlain = ({ }; const onMapPolygonCreated = (e: any) => { - if (readOnly) return; const polygonCoordinates = e.layer.toGeoJSON().geometry.coordinates[0].map((c: number[]) => [c[1], c[0]]); setPolygonArea(polygonCoordinates); setDrawingState(drawing.FINISHED); @@ -84,7 +83,6 @@ const PolygonPlain = ({ }; const onMapPolygonDelete = () => { - if (readOnly) return; setPolygonArea(null); setDrawingState(drawing.FINISHED); prevDrawingState.current = drawing.FINISHED; @@ -208,7 +206,7 @@ const PolygonPlain = ({ enabled={drawingState === drawing.STARTED} >
} {!hideStageBadge && ( )} diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx index fd6bd5fd0c..6702f437ce 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx @@ -61,8 +61,7 @@ const WidgetStagesAndEventsPlain = ({ {!hideWidgetBadge && (
1} />
diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.tsx b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.tsx index 5b457a006d..34b9048387 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.tsx +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.tsx @@ -67,6 +67,7 @@ export const WidgetTrackedEntityRelationship = ({ readOnly={readOnly} accessReadOnly={accessReadOnly} hideReadOnlyBadge={hideReadOnlyBadge} + trackedEntityName={trackedEntityTypeName} > ) => { const [open, setOpenStatus] = useState(true); @@ -68,7 +69,10 @@ const RelationshipsWidgetPlain = ({ )} {!hideReadOnlyBadge && (
- +
)}
diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/relationshipsWidget.types.ts b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/relationshipsWidget.types.ts index fad0d6cad3..b2dd7f28a8 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/relationshipsWidget.types.ts +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/relationshipsWidget.types.ts @@ -13,4 +13,5 @@ export type Props = Readonly<{ readOnly?: boolean; accessReadOnly?: boolean; hideReadOnlyBadge?: boolean; + trackedEntityName?: string; }>; From 5f1ef8c708340ccfdf0024c36050f8648e01c418 Mon Sep 17 00:00:00 2001 From: henrikmv Date: Fri, 8 May 2026 09:02:22 +0200 Subject: [PATCH 37/60] feat: clean up --- .../EnrollmentPageDefault.container.tsx | 2 +- .../EnrollmentQuickActions.component.tsx | 4 +- .../EnrollmentEditEventPage.container.tsx | 15 ++- .../EnrollmentAccessContext.tsx | 93 ++++++++----------- .../EnrollmentPageLayout.tsx | 12 ++- .../WidgetEnrollment.component.tsx | 2 - .../WidgetEnrollmentNote.component.tsx | 5 - .../WidgetEventNote.component.tsx | 5 - .../WidgetNote/WidgetNote.component.tsx | 24 ++--- .../components/WidgetNote/WidgetNote.types.ts | 6 -- .../StageOverview/StageOverview.component.tsx | 4 +- .../Stages/Stages.component.tsx | 9 +- .../Stages/stages.types.ts | 2 - .../WidgetStagesAndEvents.component.tsx | 32 ++----- 14 files changed, 82 insertions(+), 133 deletions(-) diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.tsx b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.tsx index 1690f2c4df..ea486c8df5 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.tsx +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.tsx @@ -192,7 +192,7 @@ export const EnrollmentPageDefault = () => { } return ( - + (true); const { navigate } = useNavigate(); const { enrollmentId, programId, teiId, orgUnitId } = useLocationQuery(); - const { programStageWriteAccess } = useEnrollmentAccessContext(); + const { anyStageWriteAccess } = useEnrollmentAccessContext(); const stagesWithEventCount = useMemo(() => stages.map((stage) => { const mutatedStage = { ...stage }; @@ -63,7 +63,7 @@ const EnrollmentQuickActionsComponentPlain = ({ const ready: boolean = events !== undefined && stages !== undefined; - if (!programStageWriteAccess) return null; + if (!anyStageWriteAccess) return null; return ( item.id === stageId); + const program = useTrackerProgram(programId); + const programStage = [...program.stages.values()].find((item: any) => item.id === stageId); const hideWidgets = useHideWidgetByRuleLocations( - program?.programRules.concat(programStage?.programRules as ProgramRule[]), + program.programRules.concat(programStage?.programRules as ProgramRule[]), ); const onDeleteTrackedEntitySuccess = useCallback(() => { @@ -254,8 +254,7 @@ const EnrollmentEditEventPageWithContextPlain = ({ }; const { teiDisplayName } = useTeiDisplayName(teiId, programId); - const trackedEntityType = (program && program instanceof TrackerProgram) ? program.trackedEntityType : undefined; - const { name: trackedEntityName = '', id: trackedEntityTypeId = '' } = trackedEntityType ?? {}; + const { name: trackedEntityName = '', id: trackedEntityTypeId = '' } = program.trackedEntityType ?? {}; const enrollmentsAsOptions = buildEnrollmentsAsOptions([enrollmentSite ?? {}], programId); const eventDate = getEventDate(event); const scheduleDate = getEventScheduleDate(event); @@ -297,7 +296,7 @@ const EnrollmentEditEventPageWithContextPlain = ({ } return ( - + ; -}; +import type { TrackerProgram } from '../../../../../metaData'; +import type { Access } from '../../../../../metaData/Access'; export type EnrollmentAccessContextValue = { - isLoading: boolean; - error: any; programWriteAccess: boolean; trackedEntityTypeWriteAccess: boolean; - programStageWriteAccess: boolean; - programStageReadAccess: boolean; + anyStageWriteAccess: boolean; + anyStageReadAccess: boolean; + stageWriteAccessById: Record; + stageReadAccessById: Record; trackedEntityTypeName?: string; currentStageId?: string; currentStageWriteAccess: boolean; @@ -29,13 +18,14 @@ export type EnrollmentAccessContextValue = { hideWidgetBadge: boolean; }; +// Fail-open default for renders outside a provider (tests, plugin contexts). const fallback: EnrollmentAccessContextValue = { - isLoading: false, - error: undefined, programWriteAccess: true, trackedEntityTypeWriteAccess: true, - programStageWriteAccess: true, - programStageReadAccess: true, + anyStageWriteAccess: true, + anyStageReadAccess: true, + stageWriteAccessById: {}, + stageReadAccessById: {}, currentStageWriteAccess: true, isEventPage: false, multipleStages: false, @@ -46,58 +36,51 @@ const fallback: EnrollmentAccessContextValue = { const Context = createContext(fallback); type ProviderProps = { - programId?: string; + program?: TrackerProgram; currentStageId?: string; children: React.ReactNode; }; -export const EnrollmentAccessProvider = ({ programId, currentStageId, children }: ProviderProps) => { - const { loading, error, data } = useDataQuery( - useMemo(() => ({ - program: { - resource: `programs/${programId}`, - params: { - fields: ['access,trackedEntityType[access,displayName,name],programStages[id,access]'], - }, - }, - }), [programId]), - { lazy: !programId } as any, - ); - +export const EnrollmentAccessProvider = ({ program, currentStageId, children }: ProviderProps) => { const value = useMemo(() => { - const program = data?.program as ProgramAccessResponse | undefined; - const stages = program?.programStages ?? []; - const programWriteAccess = Boolean(program?.access?.data?.write); - const trackedEntityTypeWriteAccess = Boolean(program?.trackedEntityType?.access?.data?.write); - const programStageWriteAccess = stages.some(s => s?.access?.data?.write); - const programStageReadAccess = stages.some(s => s?.access?.data?.read); + if (!program) return fallback; + + const stages = Array.from(program.stages.values()); + const stageWriteAccessById: Record = {}; + const stageReadAccessById: Record = {}; + for (const stage of stages) { + const access = stage.access as Access | undefined; + stageWriteAccessById[stage.id] = Boolean(access?.data?.write); + stageReadAccessById[stage.id] = Boolean(access?.data?.read); + } + const programWriteAccess = Boolean(program.access?.data?.write); + const trackedEntityTypeWriteAccess = Boolean(program.trackedEntityType?.access?.data?.write); + const anyStageWriteAccess = Object.values(stageWriteAccessById).some(Boolean); + const anyStageReadAccess = Object.values(stageReadAccessById).some(Boolean); const isEventPage = Boolean(currentStageId); - const currentStage = currentStageId - ? stages.find(s => s?.id === currentStageId) - : undefined; - const currentStageWriteAccess = currentStage - ? Boolean(currentStage.access?.data?.write) + const currentStageWriteAccess = currentStageId + ? (stageWriteAccessById[currentStageId] ?? false) : true; const allWriteAccessMissing = !programWriteAccess && !trackedEntityTypeWriteAccess - && !programStageWriteAccess; + && !anyStageWriteAccess; + return { - isLoading: loading, - error, programWriteAccess, trackedEntityTypeWriteAccess, - programStageWriteAccess, - programStageReadAccess, - trackedEntityTypeName: - program?.trackedEntityType?.name ?? program?.trackedEntityType?.displayName, + anyStageWriteAccess, + anyStageReadAccess, + stageWriteAccessById, + stageReadAccessById, + trackedEntityTypeName: program.trackedEntityType?.name, currentStageId, currentStageWriteAccess, isEventPage, - multipleStages: stages.length > 1, + multipleStages: program.stages.size > 1, allWriteAccessMissing, hideWidgetBadge: isEventPage || allWriteAccessMissing, }; - }, [loading, error, data, currentStageId]); + }, [program, currentStageId]); return {children}; }; diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx index 714f6dd79b..83adebcddd 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx @@ -66,8 +66,8 @@ const EnrollmentReadOnlyBadge = () => { currentStageWriteAccess, programWriteAccess, trackedEntityTypeWriteAccess, - programStageWriteAccess, - programStageReadAccess, + anyStageWriteAccess, + anyStageReadAccess, trackedEntityTypeName, } = useEnrollmentAccessContext(); @@ -82,9 +82,11 @@ const EnrollmentReadOnlyBadge = () => { ); } - const stageBadgeAccess = programStageWriteAccess || !programStageReadAccess; - const allMissing = !programWriteAccess && !trackedEntityTypeWriteAccess && !stageBadgeAccess; - if (!allMissing) return null; + // No read access to any stage means the user can't see stages at all, + // so don't claim missing stage write access in the badge. + const stagesEffectivelyReadOnly = !anyStageWriteAccess && anyStageReadAccess; + const showAllMissing = !programWriteAccess && !trackedEntityTypeWriteAccess && stagesEffectivelyReadOnly; + if (!showAllMissing) return null; return ( )} diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx index d1d80feb2a..84f5e8e897 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx @@ -3,13 +3,11 @@ import i18n from '@dhis2/d2-i18n'; import { useDispatch, useSelector } from 'react-redux'; import { requestAddNoteForEnrollment } from './WidgetEnrollmentNote.actions'; import { WidgetNote } from '../WidgetNote'; -import { useEnrollmentAccessContext } from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; import { useLocationQuery } from '../../utils/routing'; export const WidgetEnrollmentNote = () => { const dispatch = useDispatch(); const { enrollmentId } = useLocationQuery(); - const { programWriteAccess, hideWidgetBadge } = useEnrollmentAccessContext(); const notes = useSelector(({ enrollmentDomain }: { enrollmentDomain?: { enrollment?: { notes?: Array } } }) => enrollmentDomain?.enrollment?.notes ?? []); @@ -25,9 +23,6 @@ export const WidgetEnrollmentNote = () => { emptyNoteMessage={i18n.t('This enrollment doesn\'t have any notes')} notes={notes} onAddNote={onAddNote} - readOnly={!programWriteAccess} - programWriteAccess={programWriteAccess} - hideReadOnlyBadge={hideWidgetBadge} /> ); diff --git a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx index 36ffcdf4df..971a842d25 100644 --- a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx @@ -4,11 +4,9 @@ import i18n from '@dhis2/d2-i18n'; import type { Props } from './WidgetEventNote.types'; import { requestAddNoteForEvent } from './WidgetEventNote.actions'; import { WidgetNote } from '../WidgetNote'; -import { useEnrollmentAccessContext } from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; export const WidgetEventNote = ({ dataEntryKey, dataEntryId }: Props) => { const dispatch = useDispatch(); - const { currentStageWriteAccess, hideWidgetBadge } = useEnrollmentAccessContext(); const notes = useSelector(({ dataEntriesNotes }: { dataEntriesNotes: Record }) => dataEntriesNotes[`${dataEntryId}-${dataEntryKey}`] ?? []); @@ -25,9 +23,6 @@ export const WidgetEventNote = ({ dataEntryKey, dataEntryId }: Props) => { emptyNoteMessage={i18n.t('This event doesn\'t have any notes')} notes={notes} onAddNote={onAddNote} - readOnly={!currentStageWriteAccess} - programStageWriteAccess={currentStageWriteAccess} - hideReadOnlyBadge={hideWidgetBadge} /> ); diff --git a/src/core_modules/capture-core/components/WidgetNote/WidgetNote.component.tsx b/src/core_modules/capture-core/components/WidgetNote/WidgetNote.component.tsx index 5d9d1dee01..3fd437ad29 100644 --- a/src/core_modules/capture-core/components/WidgetNote/WidgetNote.component.tsx +++ b/src/core_modules/capture-core/components/WidgetNote/WidgetNote.component.tsx @@ -1,6 +1,7 @@ import React, { useState, useCallback } from 'react'; import { Widget, WidgetHeaderCountBadge } from '../Widget'; import { ReadOnlyBadge } from '../ReadOnlyBadge'; +import { useEnrollmentAccessContext } from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; import type { Props } from './WidgetNote.types'; import { NoteSection } from './NoteSection/NoteSection'; @@ -8,28 +9,29 @@ export const WidgetNote = ({ title, notes, onAddNote, - readOnly, - programWriteAccess, - trackedEntityTypeWriteAccess, - programStageWriteAccess, - trackedEntityName, - hideReadOnlyBadge, ...passOnProps }: Props) => { const [open, setOpenStatus] = useState(true); + const { + isEventPage, + currentStageWriteAccess, + programWriteAccess, + trackedEntityTypeName, + hideWidgetBadge, + } = useEnrollmentAccessContext(); + const readOnly = isEventPage ? !currentStageWriteAccess : !programWriteAccess; return ( {title} {notes.length > 0 && } - {!hideReadOnlyBadge && ( + {!hideWidgetBadge && (
)} diff --git a/src/core_modules/capture-core/components/WidgetNote/WidgetNote.types.ts b/src/core_modules/capture-core/components/WidgetNote/WidgetNote.types.ts index 10f15a3ba8..e7be709c8a 100644 --- a/src/core_modules/capture-core/components/WidgetNote/WidgetNote.types.ts +++ b/src/core_modules/capture-core/components/WidgetNote/WidgetNote.types.ts @@ -11,10 +11,4 @@ export type Props = { }; }>; onAddNote: (note: string) => void; - readOnly?: boolean; - programWriteAccess?: boolean; - trackedEntityTypeWriteAccess?: boolean; - programStageWriteAccess?: boolean; - trackedEntityName?: string; - hideReadOnlyBadge?: boolean; }; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.tsx index d9c4667898..14c4e6df2b 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.tsx @@ -96,8 +96,8 @@ export const StageOverviewPlain = ({ title, icon, description, events, stageWriteAccess = true, classes, }: Props & WithStyles) => { const { fromServerDate } = useTimeZoneConversion(); - const { hideWidgetBadge, programStageWriteAccess } = useEnrollmentAccessContext(); - const hideStageBadge = hideWidgetBadge || !programStageWriteAccess; + const { hideWidgetBadge, anyStageWriteAccess } = useEnrollmentAccessContext(); + const hideStageBadge = hideWidgetBadge || !anyStageWriteAccess; const totalEvents = events.length; const overdueEvents = events.filter(isEventOverdue).length; const scheduledEvents = events.filter(event => event.status === statusTypes.SCHEDULE).length; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stages.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stages.component.tsx index 7021dcadc5..60ad5bec59 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stages.component.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stages.component.tsx @@ -21,16 +21,11 @@ export const StagesPlain = ({ events, stageWriteAccessById, stageReadAccessById, - programLoaded, ...passOnProps }: PlainProps) => { const readableStages = useMemo( - () => stages.filter((stage) => { - const liveRead = stageReadAccessById?.[stage.id]; - if (programLoaded && liveRead !== undefined) return liveRead; - return stage.dataAccess.read; - }), - [stages, stageReadAccessById, programLoaded], + () => stages.filter(stage => stageReadAccessById?.[stage.id] ?? stage.dataAccess.read), + [stages, stageReadAccessById], ); const eventsByStage = useMemo( () => stages.reduce( diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/stages.types.ts b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/stages.types.ts index 3ffe7bc75d..45e5f3f55f 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/stages.types.ts +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/stages.types.ts @@ -6,7 +6,6 @@ export type PlainProps = { events: Array; stageWriteAccessById?: Record; stageReadAccessById?: Record; - programLoaded?: boolean; onEventClick: (eventId: string) => void; onDeleteEvent: (eventId: string) => void; onUpdateEventStatus: (eventId: string, status: string) => void; @@ -18,7 +17,6 @@ export type InputProps = { events?: Array | null; stageWriteAccessById?: Record; stageReadAccessById?: Record; - programLoaded?: boolean; onEventClick: (eventId: string) => void; onDeleteEvent: (eventId: string) => void; onUpdateEventStatus: (eventId: string, status: string) => void; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx index 6702f437ce..1bdef495e0 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx @@ -1,11 +1,10 @@ -import React, { useState, useCallback, useMemo } from 'react'; +import React, { useState, useCallback } from 'react'; import i18n from '@dhis2/d2-i18n'; import { spacersNum } from '@dhis2/ui'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { Widget } from '../Widget'; import { ReadOnlyBadge } from '../ReadOnlyBadge'; import { Stages } from './Stages'; -import { useProgram } from '../WidgetEnrollment/hooks/useProgram'; import { useEnrollmentAccessContext } from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; import type { Props } from './stagesAndEvents.types'; @@ -30,24 +29,14 @@ const WidgetStagesAndEventsPlain = ({ ...passOnProps }: Props & WithStyles) => { const [open, setOpenStatus] = useState(true); - const { program } = useProgram(programId); - const { hideWidgetBadge } = useEnrollmentAccessContext(); - const stageWriteAccessById = useMemo(() => { - const map: Record = {}; - (program?.programStages ?? []).forEach((stage: any) => { - map[stage.id] = Boolean(stage?.access?.data?.write); - }); - return map; - }, [program]); - const stageReadAccessById = useMemo(() => { - const map: Record = {}; - (program?.programStages ?? []).forEach((stage: any) => { - map[stage.id] = Boolean(stage?.access?.data?.read); - }); - return map; - }, [program]); - const anyStageWriteAccess = Object.values(stageWriteAccessById).some(Boolean); - const anyStageReadAccess = Object.values(stageReadAccessById).some(Boolean); + const { + hideWidgetBadge, + anyStageWriteAccess, + anyStageReadAccess, + multipleStages, + stageWriteAccessById, + stageReadAccessById, + } = useEnrollmentAccessContext(); return (
1} + multipleStages={multipleStages} />
)} @@ -79,7 +68,6 @@ const WidgetStagesAndEventsPlain = ({ programId={programId} stageWriteAccessById={stageWriteAccessById} stageReadAccessById={stageReadAccessById} - programLoaded={Boolean(program)} {...passOnProps} />
From 7bdf4d6b46fc75a687b39abace2547eb767e63f2 Mon Sep 17 00:00:00 2001 From: henrikmv Date: Fri, 8 May 2026 10:03:37 +0200 Subject: [PATCH 38/60] feat: clean up settings --- .../LayoutComponentConfig/LayoutComponentConfig.ts | 7 +++---- .../renderPageComponents/renderPageComponents.ts | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts index f0e700a21f..3e17e1fc32 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts @@ -1,8 +1,8 @@ import { WidgetStagesAndEvents } from '../../../../../WidgetStagesAndEvents'; import type { Props as StagesAndEventProps } from '../../../../../WidgetStagesAndEvents/stagesAndEvents.types'; import { TrackedEntityRelationshipsWrapper } from '../../../TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper'; -import type { Props as TrackedEntityRelationshipProps } - from '../../../TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types'; +import type { Props as TrackedEntityRelationshipProps } from + '../../../TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.types'; import { WidgetError } from '../../../../../WidgetErrorAndWarning/WidgetError'; import type { Props as WidgetErrorProps } from '../../../../../WidgetErrorAndWarning/WidgetError/WidgetError.types'; import { EnrollmentQuickActions } from '../../../../Enrollment/EnrollmentPageDefault/EnrollmentQuickActions'; @@ -301,8 +301,7 @@ export const EventNote: WidgetConfig = { export const RelatedStagesWorkspace: WidgetConfig = { Component: WidgetRelatedStages, - shouldHideWidget: ({ currentPage }: any) => - currentPage === EnrollmentPageKeys.EDIT_EVENT, + shouldHideWidget: ({ currentPage }: any) => currentPage === EnrollmentPageKeys.EDIT_EVENT, getProps: ({ program, stageId, diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/renderPageComponents/renderPageComponents.ts b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/renderPageComponents/renderPageComponents.ts index 89fb3ef040..44c01cc0a4 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/renderPageComponents/renderPageComponents.ts +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/renderPageComponents/renderPageComponents.ts @@ -40,7 +40,7 @@ const renderComponent = ( log.error(errorCreator(`Error while getting widget props for widget ${name}`)({ error, props })); return null; } - const customSettings = getCustomSettings && getCustomSettings(settings, props); + const customSettings = getCustomSettings && getCustomSettings(settings); if (!MemoizedWidgets[name]) { MemoizedWidgets[name] = React.memo(widgetConfig.Component); From f297d62ab0bd6033e35913ad3260bb7fda7842c2 Mon Sep 17 00:00:00 2001 From: henrikmv Date: Fri, 8 May 2026 11:28:32 +0200 Subject: [PATCH 39/60] feat: clean up --- .../WidgetEnrollment.component.tsx | 50 ++++++++++--------- .../LinkedEntitiesViewer.component.tsx | 2 - .../LinkedEntityTable.component.tsx | 3 -- .../LinkedEntityTableBody.component.tsx | 3 +- .../LinkedEntityTableHeader.component.tsx | 4 +- .../RelationshipsWidget.component.tsx | 3 +- .../linkedEntitiesViewer.types.ts | 1 - .../linkedEntityTable.types.ts | 1 - .../linkedEntityTableBody.types.ts | 1 - .../linkedEntityTableHeader.types.ts | 1 - .../useGroupedLinkedEntities.ts | 15 ++++-- ...kerWorkingListsViewMenuSetup.component.tsx | 6 +-- 12 files changed, 42 insertions(+), 48 deletions(-) diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx index 5b9b376320..0152b6fa96 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx @@ -46,6 +46,10 @@ const getGeometryType = geometryType => (geometryType === 'Point' ? dataElementTypes.COORDINATE : dataElementTypes.POLYGON); const getEnrollmentDateLabel = program => program.displayEnrollmentDateLabel ?? i18n.t('Enrollment date'); const getIncidentDateLabel = program => program.displayIncidentDateLabel ?? i18n.t('Incident date'); +const isEnrollmentReadOnly = ( + readOnlyMode: boolean, + programWriteAccess: boolean, +) => readOnlyMode || !programWriteAccess; const WidgetEnrollmentPlain = ({ classes, @@ -74,10 +78,9 @@ const WidgetEnrollmentPlain = ({ }: PlainProps & WithStyles) => { const { programWriteAccess, - trackedEntityTypeWriteAccess, hideWidgetBadge, } = useEnrollmentAccessContext(); - const enrollmentReadOnly = readOnlyMode || !programWriteAccess; + const enrollmentReadOnly = isEnrollmentReadOnly(readOnlyMode, programWriteAccess); const [open, setOpenStatus] = useState(true); const { fromServerDate } = useTimeZoneConversion(); const updatedAtDateTime: string = convertValue( @@ -100,7 +103,6 @@ const WidgetEnrollmentPlain = ({
@@ -197,26 +199,28 @@ const WidgetEnrollmentPlain = ({ /> )} - + {!enrollmentReadOnly && ( + + )} )}
diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntitiesViewer.component.tsx b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntitiesViewer.component.tsx index d4a89d60cc..e731b5988d 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntitiesViewer.component.tsx +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntitiesViewer.component.tsx @@ -25,7 +25,6 @@ const LinkedEntitiesViewerPlain = ({ groupedLinkedEntities, onLinkedRecordClick, onDeleteRelationship, - readOnly, classes, }: Props & WithStyles) => (
); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTable.component.tsx b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTable.component.tsx index 30c1a6d9ee..8d3ae8e1d3 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTable.component.tsx +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTable.component.tsx @@ -29,7 +29,6 @@ const LinkedEntityTablePlain = ({ onLinkedRecordClick, onDeleteRelationship, context, - readOnly, classes, }: Props & WithStyles) => { const [visibleRowsCount, setVisibleRowsCount] = useState(DEFAULT_VISIBLE_ROWS_COUNT); @@ -48,7 +47,6 @@ const LinkedEntityTablePlain = ({ {showMoreButtonVisible && ( diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTableBody.component.tsx b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTableBody.component.tsx index a3daa9eef7..8e1d74bbe3 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTableBody.component.tsx +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTableBody.component.tsx @@ -29,7 +29,6 @@ const LinkedEntityTableBodyPlain = ({ onLinkedRecordClick, context, onDeleteRelationship, - readOnly, classes, }: Props & WithStyles) => ( @@ -85,7 +84,7 @@ const LinkedEntityTableBodyPlain = ({ ); })} - {context.display.showDeleteButton && !readOnly ? ( + {context.display.showDeleteButton ? ( onDeleteRelationship({ relationshipId: relationshipId! }) diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTableHeader.component.tsx b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTableHeader.component.tsx index f4343981b5..150020b627 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTableHeader.component.tsx +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTableHeader.component.tsx @@ -6,7 +6,7 @@ import { } from '@dhis2/ui'; import type { Props } from './linkedEntityTableHeader.types'; -export const LinkedEntityTableHeader = ({ columns, context, readOnly }: Props) => ( +export const LinkedEntityTableHeader = ({ columns, context }: Props) => ( { @@ -19,7 +19,7 @@ export const LinkedEntityTableHeader = ({ columns, context, readOnly }: Props) = )) } - {context.display.showDeleteButton && !readOnly ? ( + {context.display.showDeleteButton ? ( ) : null} diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/RelationshipsWidget.component.tsx b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/RelationshipsWidget.component.tsx index ab21a151ad..7948fbb2ae 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/RelationshipsWidget.component.tsx +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/RelationshipsWidget.component.tsx @@ -36,7 +36,7 @@ const RelationshipsWidgetPlain = ({ classes, }: Props & WithStyles) => { const [open, setOpenStatus] = useState(true); - const groupedLinkedEntities = useGroupedLinkedEntities(sourceId, relationshipTypes, relationships); + const groupedLinkedEntities = useGroupedLinkedEntities(sourceId, relationshipTypes, relationships, readOnly); const { onDeleteRelationship } = useDeleteRelationship({ sourceId }); if (isLoading) { @@ -87,7 +87,6 @@ const RelationshipsWidgetPlain = ({ groupedLinkedEntities={groupedLinkedEntities} onLinkedRecordClick={onLinkedRecordClick} onDeleteRelationship={onDeleteRelationship} - readOnly={readOnly} /> ) } diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntitiesViewer.types.ts b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntitiesViewer.types.ts index 62ee9fdfcf..d97b1f9951 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntitiesViewer.types.ts +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntitiesViewer.types.ts @@ -5,6 +5,5 @@ export type Props = Readonly<{ groupedLinkedEntities: GroupedLinkedEntities; onLinkedRecordClick: LinkedRecordClick; onDeleteRelationship: OnDeleteRelationship; - readOnly?: boolean; }>; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTable.types.ts b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTable.types.ts index dff3570188..61cf1ce414 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTable.types.ts +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTable.types.ts @@ -7,6 +7,5 @@ export type Props = Readonly<{ onLinkedRecordClick: LinkedRecordClick; onDeleteRelationship: OnDeleteRelationship; context: Context; - readOnly?: boolean; }>; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTableBody.types.ts b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTableBody.types.ts index bd431d4525..0e2dd28649 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTableBody.types.ts +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTableBody.types.ts @@ -7,5 +7,4 @@ export type Props = Readonly<{ onLinkedRecordClick: LinkedRecordClick; context: Context; onDeleteRelationship: OnDeleteRelationship; - readOnly?: boolean; }>; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTableHeader.types.ts b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTableHeader.types.ts index 299af3815d..28fa0952a9 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTableHeader.types.ts +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/linkedEntityTableHeader.types.ts @@ -3,5 +3,4 @@ import type { Context, TableColumn } from './types'; export type Props = Readonly<{ columns: readonly TableColumn[]; context: Context; - readOnly?: boolean; }>; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/useGroupedLinkedEntities.ts b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/useGroupedLinkedEntities.ts index 8c0d0abd20..bcd69e09cb 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/useGroupedLinkedEntities.ts +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/useGroupedLinkedEntities.ts @@ -50,7 +50,11 @@ const getColumns = ({ relationshipEntity, trackerDataView }: any) => { return fields; }; -const getContext = ({ relationshipEntity, program, programStage, trackedEntityType }: any, access: any) => { +const getContext = ( + { relationshipEntity, program, programStage, trackedEntityType }: any, + access: any, + readOnly?: boolean, +) => { if (relationshipEntity === RELATIONSHIP_ENTITIES.TRACKED_ENTITY_INSTANCE) { return { navigation: { @@ -58,7 +62,7 @@ const getContext = ({ relationshipEntity, program, programStage, trackedEntityTy }, display: { trackedEntityTypeName: trackedEntityType.name, - showDeleteButton: access.data.write, + showDeleteButton: !readOnly && access.data.write, }, }; } @@ -68,7 +72,7 @@ const getContext = ({ relationshipEntity, program, programStage, trackedEntityTy navigation: {}, display: { programStageName: programStage.name, - showDeleteButton: access.data.write && false, + showDeleteButton: false, }, }; } @@ -164,6 +168,7 @@ export const useGroupedLinkedEntities = ( sourceId: string, relationshipTypes: RelationshipTypes | null | undefined, relationships?: Array, + readOnly?: boolean, ): GroupedLinkedEntities => useMemo(() => { if (!relationships?.length || !relationshipTypes?.length) { return []; @@ -220,7 +225,7 @@ export const useGroupedLinkedEntities = ( { constraint: relationshipType.toConstraint, name: relationshipType.fromToName }; const columns = getColumns(constraint); - const context = getContext(constraint, relationshipType.access); + const context = getContext(constraint, relationshipType.access, readOnly); accGroupedLinkedEntities.push({ id: groupId, @@ -233,4 +238,4 @@ export const useGroupedLinkedEntities = ( return accGroupedLinkedEntities; }, [] as any); -}, [relationships, relationshipTypes, sourceId]); +}, [relationships, relationshipTypes, sourceId, readOnly]); diff --git a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/ViewMenuSetup/TrackerWorkingListsViewMenuSetup.component.tsx b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/ViewMenuSetup/TrackerWorkingListsViewMenuSetup.component.tsx index c06b48be07..cb3e70d3fa 100644 --- a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/ViewMenuSetup/TrackerWorkingListsViewMenuSetup.component.tsx +++ b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/ViewMenuSetup/TrackerWorkingListsViewMenuSetup.component.tsx @@ -25,10 +25,6 @@ export const TrackerWorkingListsViewMenuSetup = ({ ...passOnProps }: Props) => { const [customUpdateTrigger, setCustomUpdateTrigger] = useState(); - const selectableRecordIds = useMemo( - () => recordsOrder?.filter(id => records?.[id]?.inactive !== true), - [recordsOrder, records], - ); const { selectedRows, clearSelection, @@ -37,7 +33,7 @@ export const TrackerWorkingListsViewMenuSetup = ({ toggleRowSelected, allRowsAreSelected, removeRowsFromSelection, - } = useSelectedRowsController({ recordIds: selectableRecordIds }); + } = useSelectedRowsController({ recordIds: recordsOrder }); const downloadRequest = useSelector( ({ workingLists }: any) => workingLists[storeId] && workingLists[storeId].currentRequest, ); From 7e67dd88079204a593c03b09b104d1314ba943cc Mon Sep 17 00:00:00 2001 From: henrikmv Date: Fri, 8 May 2026 11:45:44 +0200 Subject: [PATCH 40/60] feat: clean up --- .../WidgetEnrollment.component.tsx | 13 ++++++++-- .../WidgetHeader/WidgetHeader.container.tsx | 13 +--------- .../WidgetEventNote.component.tsx | 1 - .../WidgetNote/WidgetNote.component.tsx | 25 ++++++++++++++++--- .../DataEntry/DataEntry.component.tsx | 21 +++++++++++++--- 5 files changed, 50 insertions(+), 23 deletions(-) diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx index 0152b6fa96..e6812623d5 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx @@ -40,6 +40,15 @@ const styles = { display: 'flex', gap: `${spacersNum.dp4}px`, }, + header: { + display: 'flex', + alignItems: 'center', + gap: `${spacersNum.dp8}px`, + flex: 1, + }, + badge: { + marginInlineStart: 'auto', + }, }; const getGeometryType = geometryType => @@ -97,10 +106,10 @@ const WidgetEnrollmentPlain = ({
+
{i18n.t('Enrollment')} {!hideWidgetBadge && ( -
+
= { type Props = PlainProps & WithStyles; -const useLiveEventAccess = (programId: string, stageId: string) => { - const cachedEventAccess = getProgramEventAccess(programId, stageId); - const { program } = useProgram(programId); - const liveStage = program?.programStages?.find((s: any) => s.id === stageId); - const liveStageWriteAccess = liveStage ? Boolean(liveStage?.access?.data?.write) : undefined; - return liveStageWriteAccess === undefined - ? cachedEventAccess - : { ...cachedEventAccess, write: liveStageWriteAccess }; -}; - const WidgetHeaderPlain = ({ eventStatus, stage, @@ -65,7 +54,7 @@ const WidgetHeaderPlain = ({ const { currentPageMode } = useEnrollmentEditEventPageMode(eventStatus); const [actionsIsOpen, setActionsIsOpen] = useState(false); - const eventAccess = useLiveEventAccess(programId, stage.id); + const eventAccess = getProgramEventAccess(programId, stage.id); const { hasAuthority } = useAuthorities({ authorities: ['F_UNCOMPLETE_EVENT'] }); const blockEntryForm = stage.blockEntryForm && !hasAuthority && eventStatus === eventStatuses.COMPLETED; diff --git a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx index 971a842d25..837777659d 100644 --- a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx @@ -7,7 +7,6 @@ import { WidgetNote } from '../WidgetNote'; export const WidgetEventNote = ({ dataEntryKey, dataEntryId }: Props) => { const dispatch = useDispatch(); - const notes = useSelector(({ dataEntriesNotes }: { dataEntriesNotes: Record }) => dataEntriesNotes[`${dataEntryId}-${dataEntryKey}`] ?? []); diff --git a/src/core_modules/capture-core/components/WidgetNote/WidgetNote.component.tsx b/src/core_modules/capture-core/components/WidgetNote/WidgetNote.component.tsx index 3fd437ad29..098936d77f 100644 --- a/src/core_modules/capture-core/components/WidgetNote/WidgetNote.component.tsx +++ b/src/core_modules/capture-core/components/WidgetNote/WidgetNote.component.tsx @@ -1,16 +1,31 @@ import React, { useState, useCallback } from 'react'; +import { spacersNum } from '@dhis2/ui'; +import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { Widget, WidgetHeaderCountBadge } from '../Widget'; import { ReadOnlyBadge } from '../ReadOnlyBadge'; import { useEnrollmentAccessContext } from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; import type { Props } from './WidgetNote.types'; import { NoteSection } from './NoteSection/NoteSection'; -export const WidgetNote = ({ +const styles = { + header: { + display: 'flex', + alignItems: 'center', + gap: `${spacersNum.dp8}px`, + flex: 1, + }, + badge: { + marginInlineStart: 'auto', + }, +}; + +const WidgetNotePlain = ({ + classes, title, notes, onAddNote, ...passOnProps -}: Props) => { +}: Props & WithStyles) => { const [open, setOpenStatus] = useState(true); const { isEventPage, @@ -23,11 +38,11 @@ export const WidgetNote = ({ return ( + header={
{title} {notes.length > 0 && } {!hideWidgetBadge && ( -
+
); }; + +export const WidgetNote = withStyles(styles)(WidgetNotePlain); diff --git a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.component.tsx b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.component.tsx index e0105a5a22..4b92172d87 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.component.tsx +++ b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.component.tsx @@ -1,13 +1,24 @@ import React from 'react'; -import { Modal, ModalTitle, ModalContent, ModalActions, ButtonStrip, Button } from '@dhis2/ui'; +import { Modal, ModalTitle, ModalContent, ModalActions, ButtonStrip, Button, spacersNum } from '@dhis2/ui'; import i18n from '@dhis2/d2-i18n'; +import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { NoticeBoxes } from './NoticeBoxes.container'; import type { PlainProps } from './dataEntry.types'; import { DataEntry } from '../../DataEntry'; import { ReadOnlyBadge } from '../../ReadOnlyBadge'; import { TEI_MODAL_STATE } from './dataEntry.actions'; -export const DataEntryComponent = ({ +const styles = { + title: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + gap: `${spacersNum.dp8}px`, + }, +}; + +const DataEntryComponentPlain = ({ + classes, dataEntryId, onCancel, onSave, @@ -24,10 +35,10 @@ export const DataEntryComponent = ({ pluginContext, readOnly, accessReadOnly, -}: PlainProps) => ( +}: PlainProps & WithStyles) => ( -
+
{readOnly ? i18n.t( @@ -93,3 +104,5 @@ export const DataEntryComponent = ({ ); + +export const DataEntryComponent = withStyles(styles)(DataEntryComponentPlain); From 3fe212c7e463b8fb2dfa98127d9766de693f6e99 Mon Sep 17 00:00:00 2001 From: henrikmv Date: Fri, 8 May 2026 12:50:28 +0200 Subject: [PATCH 41/60] fix: temp fix to clean up prop pasing --- .../EnrollmentAccessContext.tsx | 35 +++++++++++++++++++ .../EnrollmentAccessContext/index.ts | 3 +- .../common/EnrollmentOverviewDomain/index.ts | 1 + ...edEntityRelationshipsWrapper.component.tsx | 13 ++++--- .../Actions/Actions.component.tsx | 5 --- .../WidgetEnrollment/Actions/actions.types.ts | 2 -- .../WidgetEnrollment.component.tsx | 10 +++--- .../Stages/Stage/Stage.component.tsx | 8 ++--- .../StageCreateNewButton.tsx | 6 ---- .../StageDetail/StageDetail.component.tsx | 6 ++-- .../Stage/StageDetail/stageDetail.types.ts | 1 - .../Stages/Stage/stage.types.ts | 1 - .../Stages/Stages.component.tsx | 7 ++-- .../Stages/stages.types.ts | 4 --- .../WidgetStagesAndEvents.component.tsx | 4 --- 15 files changed, 61 insertions(+), 45 deletions(-) diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/EnrollmentAccessContext.tsx b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/EnrollmentAccessContext.tsx index 002dfd8d9d..429b0af3a2 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/EnrollmentAccessContext.tsx +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/EnrollmentAccessContext.tsx @@ -2,7 +2,13 @@ import React, { createContext, useContext, useMemo } from 'react'; import type { TrackerProgram } from '../../../../../metaData'; import type { Access } from '../../../../../metaData/Access'; +export type StageAccess = { + canWrite?: boolean; + canRead?: boolean; +}; + export type EnrollmentAccessContextValue = { + // Raw — kept for the rare case a consumer needs the underlying flag. programWriteAccess: boolean; trackedEntityTypeWriteAccess: boolean; anyStageWriteAccess: boolean; @@ -16,6 +22,14 @@ export type EnrollmentAccessContextValue = { multipleStages: boolean; allWriteAccessMissing: boolean; hideWidgetBadge: boolean; + + // Semantic — what widgets should consume. Each is an "access read-only" flag, + // separate from per-instance `readOnlyMode` (a layout/UX concern). + enrollmentAccessReadOnly: boolean; + notesAccessReadOnly: boolean; + relationshipsAccessReadOnly: boolean; + hideRelationshipNewButton: boolean; + quickActionsHidden: boolean; }; // Fail-open default for renders outside a provider (tests, plugin contexts). @@ -31,6 +45,11 @@ const fallback: EnrollmentAccessContextValue = { multipleStages: false, allWriteAccessMissing: false, hideWidgetBadge: false, + enrollmentAccessReadOnly: false, + notesAccessReadOnly: false, + relationshipsAccessReadOnly: false, + hideRelationshipNewButton: false, + quickActionsHidden: false, }; const Context = createContext(fallback); @@ -79,6 +98,11 @@ export const EnrollmentAccessProvider = ({ program, currentStageId, children }: multipleStages: program.stages.size > 1, allWriteAccessMissing, hideWidgetBadge: isEventPage || allWriteAccessMissing, + enrollmentAccessReadOnly: !programWriteAccess, + notesAccessReadOnly: isEventPage ? !currentStageWriteAccess : !programWriteAccess, + relationshipsAccessReadOnly: !trackedEntityTypeWriteAccess, + hideRelationshipNewButton: !trackedEntityTypeWriteAccess || allWriteAccessMissing, + quickActionsHidden: !anyStageWriteAccess, }; }, [program, currentStageId]); @@ -86,3 +110,14 @@ export const EnrollmentAccessProvider = ({ program, currentStageId, children }: }; export const useEnrollmentAccessContext = (): EnrollmentAccessContextValue => useContext(Context); + +export const useStageAccess = (stageId?: string): StageAccess => { + const { stageWriteAccessById, stageReadAccessById } = useContext(Context); + return useMemo(() => { + if (!stageId) return {}; + return { + canWrite: stageWriteAccessById[stageId], + canRead: stageReadAccessById[stageId], + }; + }, [stageId, stageWriteAccessById, stageReadAccessById]); +}; diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/index.ts b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/index.ts index dc0884c162..a42ed44580 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/index.ts +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/index.ts @@ -1,5 +1,6 @@ export { EnrollmentAccessProvider, useEnrollmentAccessContext, + useStageAccess, } from './EnrollmentAccessContext'; -export type { EnrollmentAccessContextValue } from './EnrollmentAccessContext'; +export type { EnrollmentAccessContextValue, StageAccess } from './EnrollmentAccessContext'; diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/index.ts b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/index.ts index fba2ed7c0a..d530d9f936 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/index.ts +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/index.ts @@ -24,4 +24,5 @@ export { useRuleEffects } from './useRuleEffects'; export { EnrollmentAccessProvider, useEnrollmentAccessContext, + useStageAccess, } from './EnrollmentAccessContext'; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.tsx b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.tsx index 5cfff417e2..ad5a8bb46c 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.tsx +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.tsx @@ -35,9 +35,12 @@ export const TrackedEntityRelationshipsWrapper = ({ onLinkedRecordClick, readOnlyMode, }: Props) => { - const { trackedEntityTypeWriteAccess, allWriteAccessMissing, hideWidgetBadge } = useEnrollmentAccessContext(); - const accessReadOnly = !trackedEntityTypeWriteAccess; - const effectiveReadOnly = Boolean(readOnlyMode) || accessReadOnly; + const { + relationshipsAccessReadOnly, + hideRelationshipNewButton, + hideWidgetBadge, + } = useEnrollmentAccessContext(); + const effectiveReadOnly = Boolean(readOnlyMode) || relationshipsAccessReadOnly; const dispatch = useDispatch(); const { relationshipTypes, isError } = useTEIRelationshipsWidgetMetadata(); const { orgUnit } = useCoreOrgUnit(orgUnitId); @@ -80,8 +83,8 @@ export const TrackedEntityRelationshipsWrapper = ({ onSelectFindMode={onSelectFindMode} relationshipTypes={relationshipTypes} readOnly={effectiveReadOnly} - accessReadOnly={accessReadOnly} - hideButton={allWriteAccessMissing} + accessReadOnly={relationshipsAccessReadOnly} + hideButton={hideRelationshipNewButton} hideReadOnlyBadge={hideWidgetBadge} renderTrackedEntityRegistration={( selectedTrackedEntityTypeId, diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.component.tsx index 0414f6e2ea..37078ab734 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.component.tsx @@ -34,7 +34,6 @@ const ActionsPlain = ({ ownerOrgUnitId, tetName, canAddNew, - readOnly, onUpdateStatus, onUpdate, onDelete, @@ -64,10 +63,6 @@ const ActionsPlain = ({ onUpdateStatus(arg, redirect); }; - if (readOnly) { - return null; - } - return ( <> void; - readOnly: boolean; }; export type PlainProps = { @@ -38,5 +37,4 @@ export type PlainProps = { canAddNew: boolean; onlyEnrollOnce: boolean; tetName: string; - readOnly: boolean; }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx index e6812623d5..dba9715c87 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx @@ -55,10 +55,10 @@ const getGeometryType = geometryType => (geometryType === 'Point' ? dataElementTypes.COORDINATE : dataElementTypes.POLYGON); const getEnrollmentDateLabel = program => program.displayEnrollmentDateLabel ?? i18n.t('Enrollment date'); const getIncidentDateLabel = program => program.displayIncidentDateLabel ?? i18n.t('Incident date'); -const isEnrollmentReadOnly = ( +const computeEnrollmentReadOnly = ( readOnlyMode: boolean, - programWriteAccess: boolean, -) => readOnlyMode || !programWriteAccess; + enrollmentAccessReadOnly: boolean, +) => readOnlyMode || enrollmentAccessReadOnly; const WidgetEnrollmentPlain = ({ classes, @@ -88,8 +88,9 @@ const WidgetEnrollmentPlain = ({ const { programWriteAccess, hideWidgetBadge, + enrollmentAccessReadOnly, } = useEnrollmentAccessContext(); - const enrollmentReadOnly = isEnrollmentReadOnly(readOnlyMode, programWriteAccess); + const enrollmentReadOnly = computeEnrollmentReadOnly(readOnlyMode, enrollmentAccessReadOnly); const [open, setOpenStatus] = useState(true); const { fromServerDate } = useTimeZoneConversion(); const updatedAtDateTime: string = convertValue( @@ -210,7 +211,6 @@ const WidgetEnrollmentPlain = ({ )} {!enrollmentReadOnly && ( ) => { const [open, setOpenStatus] = useState(true); const { id, name, icon, description, dataElements, hideDueDate, repeatable, enableUserAssignment } = stage; const preventAddingNewEvents = rulesEffectHideProgramStage(ruleEffects, id); const hideProgramStage = preventAddingNewEvents && events.length === 0; - const effectiveStageWriteAccess = stageWriteAccess ?? stage.dataAccess.write; + const { canWrite } = useStageAccess(id); + const effectiveStageWriteAccess = canWrite ?? stage.dataAccess.write; const handleOpen = useCallback(() => setOpenStatus(true), [setOpenStatus]); const handleClose = useCallback(() => setOpenStatus(false), [setOpenStatus]); @@ -64,7 +66,6 @@ export const StagePlain = ({ hideDueDate={hideDueDate} repeatable={repeatable} enableUserAssignment={enableUserAssignment} - stageWriteAccess={effectiveStageWriteAccess} onCreateNew={onCreateNew} hiddenProgramStage={preventAddingNewEvents} {...passOnProps} @@ -72,7 +73,6 @@ export const StagePlain = ({
onCreateNew(id)} - stageWriteAccess={effectiveStageWriteAccess} eventCount={events.length} repeatable={repeatable} preventAddingEventActionInEffect={preventAddingNewEvents} diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageCreateNewButton/StageCreateNewButton.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageCreateNewButton/StageCreateNewButton.tsx index 9202f1c7ca..891650a162 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageCreateNewButton/StageCreateNewButton.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageCreateNewButton/StageCreateNewButton.tsx @@ -5,7 +5,6 @@ import { ConditionalTooltip } from '../../../../Tooltips/ConditionalTooltip'; type Props = { onCreateNew: () => void; - stageWriteAccess?: boolean; eventCount: number; repeatable?: boolean; preventAddingEventActionInEffect?: boolean; @@ -14,7 +13,6 @@ type Props = { export const StageCreateNewButton = ({ onCreateNew, - stageWriteAccess, eventCount, repeatable, preventAddingEventActionInEffect, @@ -42,10 +40,6 @@ export const StageCreateNewButton = ({ }; }, [eventCount, eventName, preventAddingEventActionInEffect, repeatable]); - if (!stageWriteAccess) { - return null; - } - return ( = { @@ -92,7 +93,6 @@ const StageDetailPlain = (props: Props & WithStyles) => { hideDueDate = false, repeatable = false, enableUserAssignment = false, - stageWriteAccess: stageWriteAccessProp, onEventClick, onDeleteEvent, onUpdateEventStatus, @@ -107,7 +107,8 @@ const StageDetailPlain = (props: Props & WithStyles) => { sortDirection: SORT_DIRECTION.DESC, }; const { stage } = getProgramAndStageForProgram(programId, stageId); - const stageWriteAccess = stageWriteAccessProp ?? stage?.access?.data?.write; + const { canWrite } = useStageAccess(stageId); + const stageWriteAccess = canWrite ?? stage?.access?.data?.write; const headerColumns = useComputeHeaderColumn(dataElements, hideDueDate, enableUserAssignment, stage?.stageForm); const dataElementsClient = useClientDataElements(dataElements); const { loading, value: dataSource, error } = useComputeDataFromEvent(dataElementsClient, events); @@ -263,7 +264,6 @@ const StageDetailPlain = (props: Props & WithStyles) => { onCreateNew={handleCreateNew} preventAddingEventActionInEffect={hiddenProgramStage} repeatable={repeatable} - stageWriteAccess={stageWriteAccess} eventName={eventName} />
diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/stageDetail.types.ts b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/stageDetail.types.ts index 4262e7ebdc..aa93f0fbcb 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/stageDetail.types.ts +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/stageDetail.types.ts @@ -9,7 +9,6 @@ type ExtractedProps = { repeatable?: boolean; enableUserAssignment?: boolean; stageId: string; - stageWriteAccess?: boolean; onCreateNew: (stageId: string) => void; onDeleteEvent: (eventId: string) => void; onUpdateEventStatus: (eventId: string, status: string) => void; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/stage.types.ts b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/stage.types.ts index 6238e6d7da..f1f0b30787 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/stage.types.ts +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/stage.types.ts @@ -4,7 +4,6 @@ import type { Stage, StageCommonProps } from '../../types/common.types'; type ExtractedProps = { programId: string; stage: Stage; - stageWriteAccess?: boolean; events: Array; onEventClick: (eventId: string) => void; onDeleteEvent: (eventId: string) => void; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stages.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stages.component.tsx index 60ad5bec59..6ee363c119 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stages.component.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stages.component.tsx @@ -5,6 +5,7 @@ import { compose } from 'redux'; import { Stage } from './Stage'; import type { PlainProps, InputProps } from './stages.types'; import { withLoadingIndicator } from '../../../HOC'; +import { useEnrollmentAccessContext } from '../../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; const emptyStateStyle = { padding: `0 ${spacersNum.dp12}px`, @@ -19,12 +20,11 @@ const emptyStateStyle = { export const StagesPlain = ({ stages, events, - stageWriteAccessById, - stageReadAccessById, ...passOnProps }: PlainProps) => { + const { stageReadAccessById } = useEnrollmentAccessContext(); const readableStages = useMemo( - () => stages.filter(stage => stageReadAccessById?.[stage.id] ?? stage.dataAccess.read), + () => stages.filter(stage => stageReadAccessById[stage.id] ?? stage.dataAccess.read), [stages, stageReadAccessById], ); const eventsByStage = useMemo( @@ -65,7 +65,6 @@ export const StagesPlain = ({ events={eventsByStage[stage.id]} key={stage.id} stage={stage} - stageWriteAccess={stageWriteAccessById?.[stage.id] ?? stage.dataAccess.write} {...passOnProps} /> )) diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/stages.types.ts b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/stages.types.ts index 45e5f3f55f..581b141919 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/stages.types.ts +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/stages.types.ts @@ -4,8 +4,6 @@ import type { Stage, StageCommonProps } from '../types/common.types'; export type PlainProps = { stages: Array; events: Array; - stageWriteAccessById?: Record; - stageReadAccessById?: Record; onEventClick: (eventId: string) => void; onDeleteEvent: (eventId: string) => void; onUpdateEventStatus: (eventId: string, status: string) => void; @@ -15,8 +13,6 @@ export type PlainProps = { export type InputProps = { stages?: Array; events?: Array | null; - stageWriteAccessById?: Record; - stageReadAccessById?: Record; onEventClick: (eventId: string) => void; onDeleteEvent: (eventId: string) => void; onUpdateEventStatus: (eventId: string, status: string) => void; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx index 1bdef495e0..00518b3899 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx @@ -34,8 +34,6 @@ const WidgetStagesAndEventsPlain = ({ anyStageWriteAccess, anyStageReadAccess, multipleStages, - stageWriteAccessById, - stageReadAccessById, } = useEnrollmentAccessContext(); return ( @@ -66,8 +64,6 @@ const WidgetStagesAndEventsPlain = ({ ready={events !== undefined && stages !== undefined} events={events} programId={programId} - stageWriteAccessById={stageWriteAccessById} - stageReadAccessById={stageReadAccessById} {...passOnProps} /> From 0ce59c3a80575097e598572f3fdf609d44129bc3 Mon Sep 17 00:00:00 2001 From: henrikmv Date: Fri, 8 May 2026 13:05:07 +0200 Subject: [PATCH 42/60] feat: stipp for widget specific fields --- .../EnrollmentAccessContext.tsx | 53 +++++++++---------- .../EnrollmentAccessContext/index.ts | 1 + .../common/EnrollmentOverviewDomain/index.ts | 1 + ...edEntityRelationshipsWrapper.component.tsx | 18 +++---- .../useRelationshipsWidgetAccess.ts | 23 ++++++++ .../WidgetEnrollment.component.tsx | 15 ++---- .../useWidgetEnrollmentAccess.ts | 20 +++++++ .../WidgetEnrollmentNote.component.tsx | 1 + .../WidgetEventNote.component.tsx | 1 + .../WidgetNote/WidgetNote.component.tsx | 18 +++---- .../components/WidgetNote/WidgetNote.types.ts | 3 ++ .../WidgetNote/useWidgetNoteAccess.ts | 30 +++++++++++ .../WidgetProfile/WidgetProfile.component.tsx | 4 +- .../hooks/useTrackedEntityInstances.ts | 31 ----------- .../Stages/Stage/Stage.component.tsx | 3 +- .../StageDetail/StageDetail.component.tsx | 3 +- .../StageOverview/StageOverview.component.tsx | 12 +++-- .../WidgetStagesAndEvents.component.tsx | 9 ++-- 18 files changed, 145 insertions(+), 101 deletions(-) create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/useRelationshipsWidgetAccess.ts create mode 100644 src/core_modules/capture-core/components/WidgetEnrollment/useWidgetEnrollmentAccess.ts create mode 100644 src/core_modules/capture-core/components/WidgetNote/useWidgetNoteAccess.ts diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/EnrollmentAccessContext.tsx b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/EnrollmentAccessContext.tsx index 429b0af3a2..e5588f768d 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/EnrollmentAccessContext.tsx +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/EnrollmentAccessContext.tsx @@ -3,12 +3,11 @@ import type { TrackerProgram } from '../../../../../metaData'; import type { Access } from '../../../../../metaData/Access'; export type StageAccess = { - canWrite?: boolean; - canRead?: boolean; + canWrite: boolean; + canRead: boolean; }; export type EnrollmentAccessContextValue = { - // Raw — kept for the rare case a consumer needs the underlying flag. programWriteAccess: boolean; trackedEntityTypeWriteAccess: boolean; anyStageWriteAccess: boolean; @@ -21,15 +20,6 @@ export type EnrollmentAccessContextValue = { isEventPage: boolean; multipleStages: boolean; allWriteAccessMissing: boolean; - hideWidgetBadge: boolean; - - // Semantic — what widgets should consume. Each is an "access read-only" flag, - // separate from per-instance `readOnlyMode` (a layout/UX concern). - enrollmentAccessReadOnly: boolean; - notesAccessReadOnly: boolean; - relationshipsAccessReadOnly: boolean; - hideRelationshipNewButton: boolean; - quickActionsHidden: boolean; }; // Fail-open default for renders outside a provider (tests, plugin contexts). @@ -44,12 +34,6 @@ const fallback: EnrollmentAccessContextValue = { isEventPage: false, multipleStages: false, allWriteAccessMissing: false, - hideWidgetBadge: false, - enrollmentAccessReadOnly: false, - notesAccessReadOnly: false, - relationshipsAccessReadOnly: false, - hideRelationshipNewButton: false, - quickActionsHidden: false, }; const Context = createContext(fallback); @@ -97,12 +81,6 @@ export const EnrollmentAccessProvider = ({ program, currentStageId, children }: isEventPage, multipleStages: program.stages.size > 1, allWriteAccessMissing, - hideWidgetBadge: isEventPage || allWriteAccessMissing, - enrollmentAccessReadOnly: !programWriteAccess, - notesAccessReadOnly: isEventPage ? !currentStageWriteAccess : !programWriteAccess, - relationshipsAccessReadOnly: !trackedEntityTypeWriteAccess, - hideRelationshipNewButton: !trackedEntityTypeWriteAccess || allWriteAccessMissing, - quickActionsHidden: !anyStageWriteAccess, }; }, [program, currentStageId]); @@ -111,13 +89,30 @@ export const EnrollmentAccessProvider = ({ program, currentStageId, children }: export const useEnrollmentAccessContext = (): EnrollmentAccessContextValue => useContext(Context); -export const useStageAccess = (stageId?: string): StageAccess => { +// Resolves a stage's effective access. Falls back to the stage's own access +// data when the provider has no entry (plugin/test renders). +export const useStageAccess = (stage?: { + id: string; + access?: { data?: { write?: boolean; read?: boolean } }; + dataAccess?: { write?: boolean; read?: boolean }; +}): StageAccess => { const { stageWriteAccessById, stageReadAccessById } = useContext(Context); return useMemo(() => { - if (!stageId) return {}; + if (!stage) return { canWrite: true, canRead: true }; + const fromContextWrite = stageWriteAccessById[stage.id]; + const fromContextRead = stageReadAccessById[stage.id]; return { - canWrite: stageWriteAccessById[stageId], - canRead: stageReadAccessById[stageId], + canWrite: fromContextWrite ?? Boolean(stage.access?.data?.write ?? stage.dataAccess?.write), + canRead: fromContextRead ?? Boolean(stage.access?.data?.read ?? stage.dataAccess?.read), }; - }, [stageId, stageWriteAccessById, stageReadAccessById]); + }, [stage, stageWriteAccessById, stageReadAccessById]); +}; + +// Page-level coordination: widget-level access badges are suppressed on the +// event page (where the page-level badge takes over) and when no write +// access is available anywhere (where the page-level "all missing" badge +// covers everything). +export const useShouldShowWidgetAccessBadge = (): boolean => { + const { isEventPage, allWriteAccessMissing } = useContext(Context); + return !isEventPage && !allWriteAccessMissing; }; diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/index.ts b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/index.ts index a42ed44580..b43011838a 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/index.ts +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/index.ts @@ -2,5 +2,6 @@ export { EnrollmentAccessProvider, useEnrollmentAccessContext, useStageAccess, + useShouldShowWidgetAccessBadge, } from './EnrollmentAccessContext'; export type { EnrollmentAccessContextValue, StageAccess } from './EnrollmentAccessContext'; diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/index.ts b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/index.ts index d530d9f936..72143737d8 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/index.ts +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/index.ts @@ -25,4 +25,5 @@ export { EnrollmentAccessProvider, useEnrollmentAccessContext, useStageAccess, + useShouldShowWidgetAccessBadge, } from './EnrollmentAccessContext'; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.tsx b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.tsx index ad5a8bb46c..e5c180d706 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.tsx +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.tsx @@ -15,7 +15,7 @@ import { import { ResultsPageSizeContext } from '../../../shared-contexts'; import { RegisterTei } from '../RegisterTei'; import { useCoreOrgUnit } from '../../../../../metadataRetrieval/coreOrgUnit'; -import { useEnrollmentAccessContext } from '../../EnrollmentOverviewDomain/EnrollmentAccessContext'; +import { useRelationshipsWidgetAccess } from './useRelationshipsWidgetAccess'; const createResultsView = (onLinkToTrackedEntityFromSearch: any) => (viewProps: any) => ( { const { - relationshipsAccessReadOnly, - hideRelationshipNewButton, - hideWidgetBadge, - } = useEnrollmentAccessContext(); - const effectiveReadOnly = Boolean(readOnlyMode) || relationshipsAccessReadOnly; + readOnly: effectiveReadOnly, + accessReadOnly, + hideButton, + hideReadOnlyBadge, + } = useRelationshipsWidgetAccess(readOnlyMode); const dispatch = useDispatch(); const { relationshipTypes, isError } = useTEIRelationshipsWidgetMetadata(); const { orgUnit } = useCoreOrgUnit(orgUnitId); @@ -83,9 +83,9 @@ export const TrackedEntityRelationshipsWrapper = ({ onSelectFindMode={onSelectFindMode} relationshipTypes={relationshipTypes} readOnly={effectiveReadOnly} - accessReadOnly={relationshipsAccessReadOnly} - hideButton={hideRelationshipNewButton} - hideReadOnlyBadge={hideWidgetBadge} + accessReadOnly={accessReadOnly} + hideButton={hideButton} + hideReadOnlyBadge={hideReadOnlyBadge} renderTrackedEntityRegistration={( selectedTrackedEntityTypeId, suggestedProgramId, diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/useRelationshipsWidgetAccess.ts b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/useRelationshipsWidgetAccess.ts new file mode 100644 index 0000000000..e936af29e0 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/useRelationshipsWidgetAccess.ts @@ -0,0 +1,23 @@ +import { + useEnrollmentAccessContext, + useShouldShowWidgetAccessBadge, +} from '../../EnrollmentOverviewDomain/EnrollmentAccessContext'; + +type Result = { + readOnly: boolean; + accessReadOnly: boolean; + hideButton: boolean; + hideReadOnlyBadge: boolean; +}; + +export const useRelationshipsWidgetAccess = (readOnlyMode?: boolean): Result => { + const { trackedEntityTypeWriteAccess, allWriteAccessMissing } = useEnrollmentAccessContext(); + const showBadge = useShouldShowWidgetAccessBadge(); + const accessReadOnly = !trackedEntityTypeWriteAccess; + return { + readOnly: Boolean(readOnlyMode) || accessReadOnly, + accessReadOnly, + hideButton: accessReadOnly || allWriteAccessMissing, + hideReadOnlyBadge: !showBadge, + }; +}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx index dba9715c87..a6cd76af7c 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx @@ -14,7 +14,7 @@ import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { LoadingMaskElementCenter } from '../LoadingMasks'; import { Widget } from '../Widget'; import { ReadOnlyBadge } from '../ReadOnlyBadge'; -import { useEnrollmentAccessContext } from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; +import { useWidgetEnrollmentAccess } from './useWidgetEnrollmentAccess'; import type { PlainProps } from './enrollment.types'; import { Status } from './Status'; import { dataElementTypes } from '../../metaData'; @@ -55,10 +55,6 @@ const getGeometryType = geometryType => (geometryType === 'Point' ? dataElementTypes.COORDINATE : dataElementTypes.POLYGON); const getEnrollmentDateLabel = program => program.displayEnrollmentDateLabel ?? i18n.t('Enrollment date'); const getIncidentDateLabel = program => program.displayIncidentDateLabel ?? i18n.t('Incident date'); -const computeEnrollmentReadOnly = ( - readOnlyMode: boolean, - enrollmentAccessReadOnly: boolean, -) => readOnlyMode || enrollmentAccessReadOnly; const WidgetEnrollmentPlain = ({ classes, @@ -86,11 +82,10 @@ const WidgetEnrollmentPlain = ({ onAccessLostFromTransfer, }: PlainProps & WithStyles) => { const { + readOnly: enrollmentReadOnly, + showBadge, programWriteAccess, - hideWidgetBadge, - enrollmentAccessReadOnly, - } = useEnrollmentAccessContext(); - const enrollmentReadOnly = computeEnrollmentReadOnly(readOnlyMode, enrollmentAccessReadOnly); + } = useWidgetEnrollmentAccess(readOnlyMode); const [open, setOpenStatus] = useState(true); const { fromServerDate } = useTimeZoneConversion(); const updatedAtDateTime: string = convertValue( @@ -109,7 +104,7 @@ const WidgetEnrollmentPlain = ({ header={
{i18n.t('Enrollment')} - {!hideWidgetBadge && ( + {showBadge && (
{ + const { programWriteAccess } = useEnrollmentAccessContext(); + const showBadge = useShouldShowWidgetAccessBadge(); + return { + readOnly: readOnlyMode || !programWriteAccess, + showBadge, + programWriteAccess, + }; +}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx index 84f5e8e897..768221cff2 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx @@ -22,6 +22,7 @@ export const WidgetEnrollmentNote = () => { placeholder={i18n.t('Write a note about this enrollment')} emptyNoteMessage={i18n.t('This enrollment doesn\'t have any notes')} notes={notes} + scope="enrollment" onAddNote={onAddNote} />
diff --git a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx index 837777659d..4b2913a38e 100644 --- a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx @@ -21,6 +21,7 @@ export const WidgetEventNote = ({ dataEntryKey, dataEntryId }: Props) => { placeholder={i18n.t('Write a note about this event')} emptyNoteMessage={i18n.t('This event doesn\'t have any notes')} notes={notes} + scope="event" onAddNote={onAddNote} />
diff --git a/src/core_modules/capture-core/components/WidgetNote/WidgetNote.component.tsx b/src/core_modules/capture-core/components/WidgetNote/WidgetNote.component.tsx index 098936d77f..2bc80acf4e 100644 --- a/src/core_modules/capture-core/components/WidgetNote/WidgetNote.component.tsx +++ b/src/core_modules/capture-core/components/WidgetNote/WidgetNote.component.tsx @@ -3,7 +3,7 @@ import { spacersNum } from '@dhis2/ui'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { Widget, WidgetHeaderCountBadge } from '../Widget'; import { ReadOnlyBadge } from '../ReadOnlyBadge'; -import { useEnrollmentAccessContext } from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; +import { useWidgetNoteAccess } from './useWidgetNoteAccess'; import type { Props } from './WidgetNote.types'; import { NoteSection } from './NoteSection/NoteSection'; @@ -23,29 +23,29 @@ const WidgetNotePlain = ({ classes, title, notes, + scope, onAddNote, ...passOnProps }: Props & WithStyles) => { const [open, setOpenStatus] = useState(true); const { - isEventPage, - currentStageWriteAccess, + readOnly, + showBadge, programWriteAccess, + programStageWriteAccess, trackedEntityTypeName, - hideWidgetBadge, - } = useEnrollmentAccessContext(); - const readOnly = isEventPage ? !currentStageWriteAccess : !programWriteAccess; + } = useWidgetNoteAccess(scope); return ( {title} {notes.length > 0 && } - {!hideWidgetBadge && ( + {showBadge && (
diff --git a/src/core_modules/capture-core/components/WidgetNote/WidgetNote.types.ts b/src/core_modules/capture-core/components/WidgetNote/WidgetNote.types.ts index e7be709c8a..b2ceb42cfe 100644 --- a/src/core_modules/capture-core/components/WidgetNote/WidgetNote.types.ts +++ b/src/core_modules/capture-core/components/WidgetNote/WidgetNote.types.ts @@ -1,7 +1,10 @@ +export type NoteScope = 'enrollment' | 'event'; + export type Props = { title: string; placeholder: string; emptyNoteMessage: string; + scope: NoteScope; notes: Array<{ value: string; storedAt: string; diff --git a/src/core_modules/capture-core/components/WidgetNote/useWidgetNoteAccess.ts b/src/core_modules/capture-core/components/WidgetNote/useWidgetNoteAccess.ts new file mode 100644 index 0000000000..3d4d9ad98e --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetNote/useWidgetNoteAccess.ts @@ -0,0 +1,30 @@ +import { + useEnrollmentAccessContext, + useShouldShowWidgetAccessBadge, +} from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; +import type { NoteScope } from './WidgetNote.types'; + +type Result = { + readOnly: boolean; + showBadge: boolean; + programWriteAccess: boolean; + programStageWriteAccess: boolean; + trackedEntityTypeName?: string; +}; + +export const useWidgetNoteAccess = (scope: NoteScope): Result => { + const { + programWriteAccess, + currentStageWriteAccess, + trackedEntityTypeName, + } = useEnrollmentAccessContext(); + const showBadge = useShouldShowWidgetAccessBadge(); + const isEventScope = scope === 'event'; + return { + readOnly: isEventScope ? !currentStageWriteAccess : !programWriteAccess, + showBadge, + programWriteAccess: isEventScope ? true : programWriteAccess, + programStageWriteAccess: isEventScope ? currentStageWriteAccess : true, + trackedEntityTypeName, + }; +}; diff --git a/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx b/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx index 4c5c859dbf..28211ba6c3 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx +++ b/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx @@ -91,10 +91,10 @@ const WidgetProfilePlain = ({ error: trackedEntityInstancesError, trackedEntity, trackedEntityInstanceAttributes, - trackedEntityTypeName, - trackedEntityTypeAccess, geometry, } = useTrackedEntityInstances(teiId, programId, storedAttributeValues, storedGeometry); + const trackedEntityTypeAccess = program?.trackedEntityType?.access; + const trackedEntityTypeName = program?.trackedEntityType?.displayName; const { loading: userRolesLoading, error: userRolesError, diff --git a/src/core_modules/capture-core/components/WidgetProfile/hooks/useTrackedEntityInstances.ts b/src/core_modules/capture-core/components/WidgetProfile/hooks/useTrackedEntityInstances.ts index 368f94b5b7..6de9d9a339 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/hooks/useTrackedEntityInstances.ts +++ b/src/core_modules/capture-core/components/WidgetProfile/hooks/useTrackedEntityInstances.ts @@ -9,13 +9,6 @@ type TrackedEntityInstance = { [key: string]: any; }; -type TetData = { - trackedEntityType?: { - displayName?: string; - access?: any; - }; -}; - type QueryData = { trackedEntityInstance?: TrackedEntityInstance; }; @@ -44,22 +37,6 @@ export const useTrackedEntityInstances = ( ), ); - const { loading: tetLoading, data: tetData, refetch: refetchTET } = useDataQuery( - useMemo( - () => ({ - trackedEntityType: { - resource: 'trackedEntityTypes', - id: ({ variables }: any) => variables.tetId, - params: { - fields: 'displayName,access', - }, - }, - }), - [], - ), - { lazy: true }, - ); - useEffect(() => { const attributes = data?.trackedEntityInstance?.attributes; if (attributes && attributes.length > 0) { @@ -84,12 +61,6 @@ export const useTrackedEntityInstances = ( } }, [storedAttributeValues]); - useEffect(() => { - if (data?.trackedEntityInstance?.trackedEntityType) { - refetchTET({ variables: { tetId: data?.trackedEntityInstance?.trackedEntityType } }); - } - }, [data?.trackedEntityInstance?.trackedEntityType, refetchTET]); - useEffect(() => { if (storedGeometry !== undefined) { setGeometry(storedGeometry); @@ -101,8 +72,6 @@ export const useTrackedEntityInstances = ( loading, trackedEntity: !loading && data?.trackedEntityInstance, trackedEntityInstanceAttributes: !loading && trackedEntityInstanceAttributes, - trackedEntityTypeName: tetLoading ? undefined : (tetData?.trackedEntityType as any)?.displayName, - trackedEntityTypeAccess: !tetLoading && (tetData?.trackedEntityType as any)?.access, geometry, }; }; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/Stage.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/Stage.component.tsx index 53adc7971f..b817130e07 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/Stage.component.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/Stage.component.tsx @@ -33,8 +33,7 @@ export const StagePlain = ({ const { id, name, icon, description, dataElements, hideDueDate, repeatable, enableUserAssignment } = stage; const preventAddingNewEvents = rulesEffectHideProgramStage(ruleEffects, id); const hideProgramStage = preventAddingNewEvents && events.length === 0; - const { canWrite } = useStageAccess(id); - const effectiveStageWriteAccess = canWrite ?? stage.dataAccess.write; + const { canWrite: effectiveStageWriteAccess } = useStageAccess(stage); const handleOpen = useCallback(() => setOpenStatus(true), [setOpenStatus]); const handleClose = useCallback(() => setOpenStatus(false), [setOpenStatus]); diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.tsx index 612b7ea18c..b6e5883926 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.tsx @@ -107,8 +107,7 @@ const StageDetailPlain = (props: Props & WithStyles) => { sortDirection: SORT_DIRECTION.DESC, }; const { stage } = getProgramAndStageForProgram(programId, stageId); - const { canWrite } = useStageAccess(stageId); - const stageWriteAccess = canWrite ?? stage?.access?.data?.write; + const { canWrite: stageWriteAccess } = useStageAccess({ id: stageId, access: stage?.access }); const headerColumns = useComputeHeaderColumn(dataElements, hideDueDate, enableUserAssignment, stage?.stageForm); const dataElementsClient = useClientDataElements(dataElements); const { loading, value: dataSource, error } = useComputeDataFromEvent(dataElementsClient, events); diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.tsx index 14c4e6df2b..0e256efcfa 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.tsx @@ -11,7 +11,10 @@ import moment from 'moment'; import { statusTypes } from 'capture-core/events/statusTypes'; import { NonBundledDhis2Icon } from '../../../../NonBundledDhis2Icon'; import { ReadOnlyBadge } from '../../../../ReadOnlyBadge'; -import { useEnrollmentAccessContext } from '../../../../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; +import { + useEnrollmentAccessContext, + useShouldShowWidgetAccessBadge, +} from '../../../../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; import type { Props } from './stageOverview.types'; import { isEventOverdue } from '../StageDetail/hooks/helpers'; import { convertValue as convertValueClientToView } from '../../../../../converters/clientToView'; @@ -96,8 +99,9 @@ export const StageOverviewPlain = ({ title, icon, description, events, stageWriteAccess = true, classes, }: Props & WithStyles) => { const { fromServerDate } = useTimeZoneConversion(); - const { hideWidgetBadge, anyStageWriteAccess } = useEnrollmentAccessContext(); - const hideStageBadge = hideWidgetBadge || !anyStageWriteAccess; + const { anyStageWriteAccess } = useEnrollmentAccessContext(); + const showWidgetBadge = useShouldShowWidgetAccessBadge(); + const showStageBadge = showWidgetBadge && anyStageWriteAccess; const totalEvents = events.length; const overdueEvents = events.filter(isEventOverdue).length; const scheduledEvents = events.filter(event => event.status === statusTypes.SCHEDULE).length; @@ -160,7 +164,7 @@ export const StageOverviewPlain = ({
{getLastUpdatedAt(events, fromServerDate)}
} - {!hideStageBadge && ( + {showStageBadge && ( diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx index 00518b3899..70f962484e 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx @@ -5,7 +5,10 @@ import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { Widget } from '../Widget'; import { ReadOnlyBadge } from '../ReadOnlyBadge'; import { Stages } from './Stages'; -import { useEnrollmentAccessContext } from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; +import { + useEnrollmentAccessContext, + useShouldShowWidgetAccessBadge, +} from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; import type { Props } from './stagesAndEvents.types'; const styles = { @@ -30,11 +33,11 @@ const WidgetStagesAndEventsPlain = ({ }: Props & WithStyles) => { const [open, setOpenStatus] = useState(true); const { - hideWidgetBadge, anyStageWriteAccess, anyStageReadAccess, multipleStages, } = useEnrollmentAccessContext(); + const showBadge = useShouldShowWidgetAccessBadge(); return (
{i18n.t('Stages and Events')} - {!hideWidgetBadge && ( + {showBadge && (
Date: Fri, 8 May 2026 13:17:02 +0200 Subject: [PATCH 43/60] feat: revert chanes --- .../EnrollmentAccessContext.tsx | 21 ++++--------- .../EnrollmentAccessContext/index.ts | 3 +- .../common/EnrollmentOverviewDomain/index.ts | 1 - ...edEntityRelationshipsWrapper.component.tsx | 17 ++++++----- .../useRelationshipsWidgetAccess.ts | 23 -------------- .../WidgetEnrollment.component.tsx | 12 ++++---- .../useWidgetEnrollmentAccess.ts | 20 ------------- .../WidgetNote/WidgetNote.component.tsx | 17 ++++++----- .../WidgetNote/useWidgetNoteAccess.ts | 30 ------------------- .../StageOverview/StageOverview.component.tsx | 8 ++--- .../WidgetStagesAndEvents.component.tsx | 9 ++---- ...NewTrackedEntityRelationship.container.tsx | 2 +- 12 files changed, 36 insertions(+), 127 deletions(-) delete mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/useRelationshipsWidgetAccess.ts delete mode 100644 src/core_modules/capture-core/components/WidgetEnrollment/useWidgetEnrollmentAccess.ts delete mode 100644 src/core_modules/capture-core/components/WidgetNote/useWidgetNoteAccess.ts diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/EnrollmentAccessContext.tsx b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/EnrollmentAccessContext.tsx index e5588f768d..c098160a05 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/EnrollmentAccessContext.tsx +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/EnrollmentAccessContext.tsx @@ -2,11 +2,6 @@ import React, { createContext, useContext, useMemo } from 'react'; import type { TrackerProgram } from '../../../../../metaData'; import type { Access } from '../../../../../metaData/Access'; -export type StageAccess = { - canWrite: boolean; - canRead: boolean; -}; - export type EnrollmentAccessContextValue = { programWriteAccess: boolean; trackedEntityTypeWriteAccess: boolean; @@ -20,6 +15,9 @@ export type EnrollmentAccessContextValue = { isEventPage: boolean; multipleStages: boolean; allWriteAccessMissing: boolean; + // Widget-level access badges defer to the page-level badge on event pages + // and when all access is missing. + showWidgetBadge: boolean; }; // Fail-open default for renders outside a provider (tests, plugin contexts). @@ -34,6 +32,7 @@ const fallback: EnrollmentAccessContextValue = { isEventPage: false, multipleStages: false, allWriteAccessMissing: false, + showWidgetBadge: true, }; const Context = createContext(fallback); @@ -81,6 +80,7 @@ export const EnrollmentAccessProvider = ({ program, currentStageId, children }: isEventPage, multipleStages: program.stages.size > 1, allWriteAccessMissing, + showWidgetBadge: !isEventPage && !allWriteAccessMissing, }; }, [program, currentStageId]); @@ -95,7 +95,7 @@ export const useStageAccess = (stage?: { id: string; access?: { data?: { write?: boolean; read?: boolean } }; dataAccess?: { write?: boolean; read?: boolean }; -}): StageAccess => { +}): { canWrite: boolean; canRead: boolean } => { const { stageWriteAccessById, stageReadAccessById } = useContext(Context); return useMemo(() => { if (!stage) return { canWrite: true, canRead: true }; @@ -107,12 +107,3 @@ export const useStageAccess = (stage?: { }; }, [stage, stageWriteAccessById, stageReadAccessById]); }; - -// Page-level coordination: widget-level access badges are suppressed on the -// event page (where the page-level badge takes over) and when no write -// access is available anywhere (where the page-level "all missing" badge -// covers everything). -export const useShouldShowWidgetAccessBadge = (): boolean => { - const { isEventPage, allWriteAccessMissing } = useContext(Context); - return !isEventPage && !allWriteAccessMissing; -}; diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/index.ts b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/index.ts index b43011838a..492486da8f 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/index.ts +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/index.ts @@ -2,6 +2,5 @@ export { EnrollmentAccessProvider, useEnrollmentAccessContext, useStageAccess, - useShouldShowWidgetAccessBadge, } from './EnrollmentAccessContext'; -export type { EnrollmentAccessContextValue, StageAccess } from './EnrollmentAccessContext'; +export type { EnrollmentAccessContextValue } from './EnrollmentAccessContext'; diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/index.ts b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/index.ts index 72143737d8..d530d9f936 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/index.ts +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/index.ts @@ -25,5 +25,4 @@ export { EnrollmentAccessProvider, useEnrollmentAccessContext, useStageAccess, - useShouldShowWidgetAccessBadge, } from './EnrollmentAccessContext'; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.tsx b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.tsx index e5c180d706..ade68ccb00 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.tsx +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.tsx @@ -15,7 +15,7 @@ import { import { ResultsPageSizeContext } from '../../../shared-contexts'; import { RegisterTei } from '../RegisterTei'; import { useCoreOrgUnit } from '../../../../../metadataRetrieval/coreOrgUnit'; -import { useRelationshipsWidgetAccess } from './useRelationshipsWidgetAccess'; +import { useEnrollmentAccessContext } from '../../EnrollmentOverviewDomain/EnrollmentAccessContext'; const createResultsView = (onLinkToTrackedEntityFromSearch: any) => (viewProps: any) => ( { const { - readOnly: effectiveReadOnly, - accessReadOnly, - hideButton, - hideReadOnlyBadge, - } = useRelationshipsWidgetAccess(readOnlyMode); + trackedEntityTypeWriteAccess, + allWriteAccessMissing, + showWidgetBadge, + } = useEnrollmentAccessContext(); + const accessReadOnly = !trackedEntityTypeWriteAccess; + const effectiveReadOnly = Boolean(readOnlyMode) || accessReadOnly; const dispatch = useDispatch(); const { relationshipTypes, isError } = useTEIRelationshipsWidgetMetadata(); const { orgUnit } = useCoreOrgUnit(orgUnitId); @@ -84,8 +85,8 @@ export const TrackedEntityRelationshipsWrapper = ({ relationshipTypes={relationshipTypes} readOnly={effectiveReadOnly} accessReadOnly={accessReadOnly} - hideButton={hideButton} - hideReadOnlyBadge={hideReadOnlyBadge} + hideButton={accessReadOnly || allWriteAccessMissing} + hideReadOnlyBadge={!showWidgetBadge} renderTrackedEntityRegistration={( selectedTrackedEntityTypeId, suggestedProgramId, diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/useRelationshipsWidgetAccess.ts b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/useRelationshipsWidgetAccess.ts deleted file mode 100644 index e936af29e0..0000000000 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/useRelationshipsWidgetAccess.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { - useEnrollmentAccessContext, - useShouldShowWidgetAccessBadge, -} from '../../EnrollmentOverviewDomain/EnrollmentAccessContext'; - -type Result = { - readOnly: boolean; - accessReadOnly: boolean; - hideButton: boolean; - hideReadOnlyBadge: boolean; -}; - -export const useRelationshipsWidgetAccess = (readOnlyMode?: boolean): Result => { - const { trackedEntityTypeWriteAccess, allWriteAccessMissing } = useEnrollmentAccessContext(); - const showBadge = useShouldShowWidgetAccessBadge(); - const accessReadOnly = !trackedEntityTypeWriteAccess; - return { - readOnly: Boolean(readOnlyMode) || accessReadOnly, - accessReadOnly, - hideButton: accessReadOnly || allWriteAccessMissing, - hideReadOnlyBadge: !showBadge, - }; -}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx index a6cd76af7c..4f135cf70e 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx @@ -14,7 +14,7 @@ import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { LoadingMaskElementCenter } from '../LoadingMasks'; import { Widget } from '../Widget'; import { ReadOnlyBadge } from '../ReadOnlyBadge'; -import { useWidgetEnrollmentAccess } from './useWidgetEnrollmentAccess'; +import { useEnrollmentAccessContext } from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; import type { PlainProps } from './enrollment.types'; import { Status } from './Status'; import { dataElementTypes } from '../../metaData'; @@ -56,6 +56,7 @@ const getGeometryType = geometryType => const getEnrollmentDateLabel = program => program.displayEnrollmentDateLabel ?? i18n.t('Enrollment date'); const getIncidentDateLabel = program => program.displayIncidentDateLabel ?? i18n.t('Incident date'); +// eslint-disable-next-line complexity const WidgetEnrollmentPlain = ({ classes, events, @@ -81,11 +82,8 @@ const WidgetEnrollmentPlain = ({ onUpdateEnrollmentStatusSuccess, onAccessLostFromTransfer, }: PlainProps & WithStyles) => { - const { - readOnly: enrollmentReadOnly, - showBadge, - programWriteAccess, - } = useWidgetEnrollmentAccess(readOnlyMode); + const { programWriteAccess, showWidgetBadge } = useEnrollmentAccessContext(); + const enrollmentReadOnly = readOnlyMode || !programWriteAccess; const [open, setOpenStatus] = useState(true); const { fromServerDate } = useTimeZoneConversion(); const updatedAtDateTime: string = convertValue( @@ -104,7 +102,7 @@ const WidgetEnrollmentPlain = ({ header={
{i18n.t('Enrollment')} - {showBadge && ( + {showWidgetBadge && (
{ - const { programWriteAccess } = useEnrollmentAccessContext(); - const showBadge = useShouldShowWidgetAccessBadge(); - return { - readOnly: readOnlyMode || !programWriteAccess, - showBadge, - programWriteAccess, - }; -}; diff --git a/src/core_modules/capture-core/components/WidgetNote/WidgetNote.component.tsx b/src/core_modules/capture-core/components/WidgetNote/WidgetNote.component.tsx index 2bc80acf4e..0416aa848c 100644 --- a/src/core_modules/capture-core/components/WidgetNote/WidgetNote.component.tsx +++ b/src/core_modules/capture-core/components/WidgetNote/WidgetNote.component.tsx @@ -3,7 +3,7 @@ import { spacersNum } from '@dhis2/ui'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { Widget, WidgetHeaderCountBadge } from '../Widget'; import { ReadOnlyBadge } from '../ReadOnlyBadge'; -import { useWidgetNoteAccess } from './useWidgetNoteAccess'; +import { useEnrollmentAccessContext } from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; import type { Props } from './WidgetNote.types'; import { NoteSection } from './NoteSection/NoteSection'; @@ -29,23 +29,24 @@ const WidgetNotePlain = ({ }: Props & WithStyles) => { const [open, setOpenStatus] = useState(true); const { - readOnly, - showBadge, programWriteAccess, - programStageWriteAccess, + currentStageWriteAccess, trackedEntityTypeName, - } = useWidgetNoteAccess(scope); + showWidgetBadge, + } = useEnrollmentAccessContext(); + const isEventScope = scope === 'event'; + const readOnly = isEventScope ? !currentStageWriteAccess : !programWriteAccess; return ( {title} {notes.length > 0 && } - {showBadge && ( + {showWidgetBadge && (
diff --git a/src/core_modules/capture-core/components/WidgetNote/useWidgetNoteAccess.ts b/src/core_modules/capture-core/components/WidgetNote/useWidgetNoteAccess.ts deleted file mode 100644 index 3d4d9ad98e..0000000000 --- a/src/core_modules/capture-core/components/WidgetNote/useWidgetNoteAccess.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { - useEnrollmentAccessContext, - useShouldShowWidgetAccessBadge, -} from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; -import type { NoteScope } from './WidgetNote.types'; - -type Result = { - readOnly: boolean; - showBadge: boolean; - programWriteAccess: boolean; - programStageWriteAccess: boolean; - trackedEntityTypeName?: string; -}; - -export const useWidgetNoteAccess = (scope: NoteScope): Result => { - const { - programWriteAccess, - currentStageWriteAccess, - trackedEntityTypeName, - } = useEnrollmentAccessContext(); - const showBadge = useShouldShowWidgetAccessBadge(); - const isEventScope = scope === 'event'; - return { - readOnly: isEventScope ? !currentStageWriteAccess : !programWriteAccess, - showBadge, - programWriteAccess: isEventScope ? true : programWriteAccess, - programStageWriteAccess: isEventScope ? currentStageWriteAccess : true, - trackedEntityTypeName, - }; -}; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.tsx index 0e256efcfa..d87ef71abc 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.tsx @@ -11,10 +11,7 @@ import moment from 'moment'; import { statusTypes } from 'capture-core/events/statusTypes'; import { NonBundledDhis2Icon } from '../../../../NonBundledDhis2Icon'; import { ReadOnlyBadge } from '../../../../ReadOnlyBadge'; -import { - useEnrollmentAccessContext, - useShouldShowWidgetAccessBadge, -} from '../../../../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; +import { useEnrollmentAccessContext } from '../../../../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; import type { Props } from './stageOverview.types'; import { isEventOverdue } from '../StageDetail/hooks/helpers'; import { convertValue as convertValueClientToView } from '../../../../../converters/clientToView'; @@ -99,8 +96,7 @@ export const StageOverviewPlain = ({ title, icon, description, events, stageWriteAccess = true, classes, }: Props & WithStyles) => { const { fromServerDate } = useTimeZoneConversion(); - const { anyStageWriteAccess } = useEnrollmentAccessContext(); - const showWidgetBadge = useShouldShowWidgetAccessBadge(); + const { anyStageWriteAccess, showWidgetBadge } = useEnrollmentAccessContext(); const showStageBadge = showWidgetBadge && anyStageWriteAccess; const totalEvents = events.length; const overdueEvents = events.filter(isEventOverdue).length; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx index 70f962484e..6a410d71ff 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx @@ -5,10 +5,7 @@ import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { Widget } from '../Widget'; import { ReadOnlyBadge } from '../ReadOnlyBadge'; import { Stages } from './Stages'; -import { - useEnrollmentAccessContext, - useShouldShowWidgetAccessBadge, -} from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; +import { useEnrollmentAccessContext } from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; import type { Props } from './stagesAndEvents.types'; const styles = { @@ -36,8 +33,8 @@ const WidgetStagesAndEventsPlain = ({ anyStageWriteAccess, anyStageReadAccess, multipleStages, + showWidgetBadge, } = useEnrollmentAccessContext(); - const showBadge = useShouldShowWidgetAccessBadge(); return (
{i18n.t('Stages and Events')} - {showBadge && ( + {showWidgetBadge && (
Date: Fri, 8 May 2026 13:24:57 +0200 Subject: [PATCH 44/60] feat: remove useStageAccess and simplify access handling in EnrollmentAccessContext --- .../EnrollmentAccessContext.tsx | 19 ------------------- .../EnrollmentAccessContext/index.ts | 1 - .../common/EnrollmentOverviewDomain/index.ts | 1 - .../Stages/Stage/Stage.component.tsx | 5 +++-- .../StageDetail/StageDetail.component.tsx | 5 +++-- 5 files changed, 6 insertions(+), 25 deletions(-) diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/EnrollmentAccessContext.tsx b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/EnrollmentAccessContext.tsx index c098160a05..9404142e95 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/EnrollmentAccessContext.tsx +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/EnrollmentAccessContext.tsx @@ -88,22 +88,3 @@ export const EnrollmentAccessProvider = ({ program, currentStageId, children }: }; export const useEnrollmentAccessContext = (): EnrollmentAccessContextValue => useContext(Context); - -// Resolves a stage's effective access. Falls back to the stage's own access -// data when the provider has no entry (plugin/test renders). -export const useStageAccess = (stage?: { - id: string; - access?: { data?: { write?: boolean; read?: boolean } }; - dataAccess?: { write?: boolean; read?: boolean }; -}): { canWrite: boolean; canRead: boolean } => { - const { stageWriteAccessById, stageReadAccessById } = useContext(Context); - return useMemo(() => { - if (!stage) return { canWrite: true, canRead: true }; - const fromContextWrite = stageWriteAccessById[stage.id]; - const fromContextRead = stageReadAccessById[stage.id]; - return { - canWrite: fromContextWrite ?? Boolean(stage.access?.data?.write ?? stage.dataAccess?.write), - canRead: fromContextRead ?? Boolean(stage.access?.data?.read ?? stage.dataAccess?.read), - }; - }, [stage, stageWriteAccessById, stageReadAccessById]); -}; diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/index.ts b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/index.ts index 492486da8f..dc0884c162 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/index.ts +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/index.ts @@ -1,6 +1,5 @@ export { EnrollmentAccessProvider, useEnrollmentAccessContext, - useStageAccess, } from './EnrollmentAccessContext'; export type { EnrollmentAccessContextValue } from './EnrollmentAccessContext'; diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/index.ts b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/index.ts index d530d9f936..fba2ed7c0a 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/index.ts +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/index.ts @@ -24,5 +24,4 @@ export { useRuleEffects } from './useRuleEffects'; export { EnrollmentAccessProvider, useEnrollmentAccessContext, - useStageAccess, } from './EnrollmentAccessContext'; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/Stage.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/Stage.component.tsx index b817130e07..0950bee130 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/Stage.component.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/Stage.component.tsx @@ -6,7 +6,7 @@ import type { Props } from './stage.types'; import { Widget } from '../../../Widget'; import { StageDetail } from './StageDetail/StageDetail.component'; import { StageCreateNewButton } from './StageCreateNewButton'; -import { useStageAccess } from '../../../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; +import { useEnrollmentAccessContext } from '../../../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; const styles = { overview: { @@ -33,7 +33,8 @@ export const StagePlain = ({ const { id, name, icon, description, dataElements, hideDueDate, repeatable, enableUserAssignment } = stage; const preventAddingNewEvents = rulesEffectHideProgramStage(ruleEffects, id); const hideProgramStage = preventAddingNewEvents && events.length === 0; - const { canWrite: effectiveStageWriteAccess } = useStageAccess(stage); + const { stageWriteAccessById } = useEnrollmentAccessContext(); + const effectiveStageWriteAccess = stageWriteAccessById[stage.id] ?? stage.dataAccess.write; const handleOpen = useCallback(() => setOpenStatus(true), [setOpenStatus]); const handleClose = useCallback(() => setOpenStatus(false), [setOpenStatus]); diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.tsx index b6e5883926..f6feb0ce38 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.tsx @@ -26,7 +26,7 @@ import { getProgramAndStageForProgram } from '../../../../../metaData/helpers'; import type { Props } from './stageDetail.types'; import { EventRow } from './EventRow'; import { useClientDataElements } from './hooks/useClientDataElements'; -import { useStageAccess } from '../../../../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; +import { useEnrollmentAccessContext } from '../../../../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; const styles: Readonly = { @@ -107,7 +107,8 @@ const StageDetailPlain = (props: Props & WithStyles) => { sortDirection: SORT_DIRECTION.DESC, }; const { stage } = getProgramAndStageForProgram(programId, stageId); - const { canWrite: stageWriteAccess } = useStageAccess({ id: stageId, access: stage?.access }); + const { stageWriteAccessById } = useEnrollmentAccessContext(); + const stageWriteAccess = stageWriteAccessById[stageId] ?? stage?.access?.data?.write; const headerColumns = useComputeHeaderColumn(dataElements, hideDueDate, enableUserAssignment, stage?.stageForm); const dataElementsClient = useClientDataElements(dataElements); const { loading, value: dataSource, error } = useComputeDataFromEvent(dataElementsClient, events); From 121759be6d689a238408866f90b4600326f53c12 Mon Sep 17 00:00:00 2001 From: henrikmv Date: Fri, 8 May 2026 13:47:22 +0200 Subject: [PATCH 45/60] feat: enhance access handling in TrackedEntityRelationshipsWrapper and WidgetProfile components --- ...edEntityRelationshipsWrapper.component.tsx | 4 ++-- .../WidgetProfile/WidgetProfile.component.tsx | 21 +++++++++++-------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.tsx b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.tsx index ade68ccb00..720bd26f98 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.tsx +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.tsx @@ -33,7 +33,7 @@ export const TrackedEntityRelationshipsWrapper = ({ onOpenAddRelationship, onCloseAddRelationship, onLinkedRecordClick, - readOnlyMode, + readOnlyMode = false, }: Props) => { const { trackedEntityTypeWriteAccess, @@ -41,7 +41,7 @@ export const TrackedEntityRelationshipsWrapper = ({ showWidgetBadge, } = useEnrollmentAccessContext(); const accessReadOnly = !trackedEntityTypeWriteAccess; - const effectiveReadOnly = Boolean(readOnlyMode) || accessReadOnly; + const effectiveReadOnly = readOnlyMode || accessReadOnly; const dispatch = useDispatch(); const { relationshipTypes, isError } = useTEIRelationshipsWidgetMetadata(); const { orgUnit } = useCoreOrgUnit(orgUnitId); diff --git a/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx b/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx index 28211ba6c3..c04c7282b7 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx +++ b/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx @@ -25,6 +25,7 @@ import { OverflowMenu } from './OverflowMenu'; import { useDataEntryFormConfig, } from '../DataEntries/common/TEIAndEnrollment'; +import { useEnrollmentAccessContext } from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; const styles: Readonly = { header: { @@ -93,7 +94,10 @@ const WidgetProfilePlain = ({ trackedEntityInstanceAttributes, geometry, } = useTrackedEntityInstances(teiId, programId, storedAttributeValues, storedGeometry); - const trackedEntityTypeAccess = program?.trackedEntityType?.access; + const { + programWriteAccess, + trackedEntityTypeWriteAccess, + } = useEnrollmentAccessContext(); const trackedEntityTypeName = program?.trackedEntityType?.displayName; const { loading: userRolesLoading, @@ -103,11 +107,10 @@ const WidgetProfilePlain = ({ const hasNoAttributes = !program?.programTrackedEntityAttributes?.length; - const isEditable = useMemo(() => - !hasNoAttributes && - trackedEntityTypeAccess?.data?.write && - !readOnlyMode, - [hasNoAttributes, readOnlyMode, trackedEntityTypeAccess]); + const isEditable = useMemo( + () => !hasNoAttributes && trackedEntityTypeWriteAccess && !readOnlyMode, + [hasNoAttributes, trackedEntityTypeWriteAccess, readOnlyMode], + ); const profileButtonLabel = useMemo(() => { if (readOnlyMode) return null; @@ -152,8 +155,8 @@ const WidgetProfilePlain = ({ }, [storedAttributeValues, onUpdateTeiAttributeValues, teiDisplayName]); const canWriteData = useMemo( - () => trackedEntityTypeAccess?.data?.write && program?.access?.data?.write, - [trackedEntityTypeAccess, program], + () => trackedEntityTypeWriteAccess && programWriteAccess, + [trackedEntityTypeWriteAccess, programWriteAccess], ); const renderProfile = () => { @@ -263,7 +266,7 @@ const WidgetProfilePlain = ({ geometry={geometry} trackedEntityName={trackedEntityTypeName} readOnly={!isEditable} - accessReadOnly={!trackedEntityTypeAccess?.data?.write} + accessReadOnly={!trackedEntityTypeWriteAccess} /> From baa6a0f59652043b57e69e00be2cc8382845bd2d Mon Sep 17 00:00:00 2001 From: henrikmv Date: Fri, 8 May 2026 14:09:13 +0200 Subject: [PATCH 46/60] fix: read only prop required --- .../Widget/widgetNonCollapsible.types.ts | 2 +- .../Actions/Actions.component.tsx | 1 + .../MapModal/Coordinates/Coordinates.types.ts | 2 +- .../MapModal/MapModal.types.ts | 4 ++-- .../MapModal/Polygon/Polygon.types.ts | 2 +- .../WidgetProfile/WidgetProfile.component.tsx | 21 ++++++++----------- 6 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/core_modules/capture-core/components/Widget/widgetNonCollapsible.types.ts b/src/core_modules/capture-core/components/Widget/widgetNonCollapsible.types.ts index 755d105bec..fcc8fe106d 100644 --- a/src/core_modules/capture-core/components/Widget/widgetNonCollapsible.types.ts +++ b/src/core_modules/capture-core/components/Widget/widgetNonCollapsible.types.ts @@ -2,7 +2,7 @@ import type { ReactNode } from 'react'; export type WidgetNonCollapsiblePropsPlain = { header?: ReactNode; - children?: ReactNode; + children: ReactNode; color?: string; borderless?: boolean; }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.component.tsx index 37078ab734..cb3a9f8bbd 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.component.tsx @@ -134,6 +134,7 @@ const ActionsPlain = ({ enrollment={enrollment} onUpdate={handleOnUpdate} setOpenMap={setOpenMap} + readOnly={false} />} {isOpenTransfer && ( void; onSetCoordinates: (coordinates: [number, number] | Array> | null) => void; defaultValues?: [number, number] | null; - readOnly?: boolean; + readOnly: boolean; }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/MapModal.types.ts b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/MapModal.types.ts index 00f86d12f6..e5556bbd74 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/MapModal.types.ts +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/MapModal.types.ts @@ -6,7 +6,7 @@ export type MapModalComponentProps = { defaultValues?: number[][] | [number, number] | null; setOpen: (open: boolean) => void; onSetCoordinates: (coordinates: [number, number] | Array> | null) => void; - readOnly?: boolean; + readOnly: boolean; } export type MapModalProps = { @@ -15,5 +15,5 @@ export type MapModalProps = { onUpdate: (arg: Record) => void; setOpenMap: (toggle: boolean) => void; defaultValues?: number[][] | [number, number] | null; - readOnly?: boolean; + readOnly: boolean; }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/Polygon.types.ts b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/Polygon.types.ts index 28ddb5ed47..7cc9e14b4e 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/Polygon.types.ts +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/Polygon.types.ts @@ -17,5 +17,5 @@ export type PolygonProps = { setOpen: (open: boolean) => void; onSetCoordinates: (coordinates: [number, number] | Array> | null) => void; defaultValues?: Array> | null; - readOnly?: boolean; + readOnly: boolean; } diff --git a/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx b/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx index c04c7282b7..03fc78cef3 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx +++ b/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx @@ -235,18 +235,15 @@ const WidgetProfilePlain = ({ return (
- {isEmptyList ? ( - - ) : ( - - {renderProfile()} - - )} + + {renderProfile()} + {showEditModal(loading, error, Boolean(profileButtonLabel), modalState, program) && ( <> Date: Mon, 11 May 2026 11:56:06 +0200 Subject: [PATCH 47/60] fix: cypress test --- .../WidgetEnrollment/index.js | 12 ++++++++++++ .../WidgetsForEnrollmentAddEventPage.feature | 7 +++---- .../WidgetsForEnrollmentEditEvent.feature | 15 ++++++--------- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/cypress/e2e/WidgetsForEnrollmentPages/WidgetEnrollment/index.js b/cypress/e2e/WidgetsForEnrollmentPages/WidgetEnrollment/index.js index a2bd1da20a..943619c0cd 100644 --- a/cypress/e2e/WidgetsForEnrollmentPages/WidgetEnrollment/index.js +++ b/cypress/e2e/WidgetsForEnrollmentPages/WidgetEnrollment/index.js @@ -60,6 +60,18 @@ Then('the enrollment widget should be opened', () => { }); }); +Then('the enrollment actions button is not visible', () => { + cy.get('[data-test="widget-enrollment"]').within(() => { + cy.get('[data-test="widget-enrollment-actions-button"]').should('not.exist'); + }); +}); + +Then('the enrollment date edit buttons are not visible', () => { + cy.get('[data-test="widget-enrollment"]').within(() => { + cy.get('[data-test="widget-enrollment-icon-edit-date"]').should('not.exist'); + }); +}); + Then('the user sees the enrollment date', () => { cy.get('[data-test="widget-enrollment-enrollment-date"]').within(() => { cy.get('[data-test="widget-enrollment-icon-calendar"]').should('exist'); diff --git a/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentAddEventPage/WidgetsForEnrollmentAddEventPage.feature b/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentAddEventPage/WidgetsForEnrollmentAddEventPage.feature index f4dfa1cc08..edb46a6ca5 100644 --- a/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentAddEventPage/WidgetsForEnrollmentAddEventPage.feature +++ b/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentAddEventPage/WidgetsForEnrollmentAddEventPage.feature @@ -38,12 +38,11 @@ Feature: The user interacts with the widgets on the enrollment add event page And the user sees the owner organisation unit And the user sees the last update date - Scenario: User can open the delete modal + Scenario: The enrollment widget is in read-only mode Given you land on the enrollment add event page by having typed #/enrollmentEventNew?programId=IpHINAT79UW&orgUnitId=DiszpKrYNg8&teiId=EaOyKGOIGRp&enrollmentId=wBU0RAsYjKE&stageId=A03MvHHogjR Then the enrollment widget should be opened - When the user opens the enrollment actions menu - And the user clicks on the delete action - Then the user sees the delete enrollment modal + And the enrollment actions button is not visible + And the enrollment date edit buttons are not visible Scenario: User switch tab in add event page Given you land on the enrollment add event page by having typed #/enrollmentEventNew?programId=IpHINAT79UW&orgUnitId=DiszpKrYNg8&teiId=EaOyKGOIGRp&enrollmentId=wBU0RAsYjKE&stageId=A03MvHHogjR diff --git a/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentEditEvent/WidgetsForEnrollmentEditEvent.feature b/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentEditEvent/WidgetsForEnrollmentEditEvent.feature index 2603f14633..896acfb495 100644 --- a/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentEditEvent/WidgetsForEnrollmentEditEvent.feature +++ b/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentEditEvent/WidgetsForEnrollmentEditEvent.feature @@ -38,12 +38,11 @@ Feature: The user interacts with the widgets on the enrollment edit event And the user sees the owner organisation unit And the user sees the last update date - Scenario: User can open the delete modal + Scenario: The enrollment widget is in read-only mode Given you land on the enrollment edit event page by having typed /#/enrollmentEventEdit?eventId=XGLkLlOXgmE&orgUnitId=DiszpKrYNg8 Then the enrollment widget should be opened - When the user opens the enrollment actions menu - And the user clicks on the delete action - Then the user sees the delete enrollment modal + And the enrollment actions button is not visible + And the enrollment date edit buttons are not visible Scenario: User can add note on edit event page view mode Given you land on the enrollment edit event page by having typed /#/enrollmentEventEdit?eventId=XGLkLlOXgmE&orgUnitId=DiszpKrYNg8 @@ -72,13 +71,11 @@ Feature: The user interacts with the widgets on the enrollment edit event When you remove the assigned user Then the event has no assignd user - Scenario: User can complete the enrollment and the active events + Scenario: The enrollment widget is in read-only mode on enrollment event page Given you land on the enrollment edit event page by having typed #/enrollmentEventEdit?eventId=PyXThVzWJzL&orgUnitId=RzgSFJ9E46G And the enrollment widget should be opened - And the user sees the enrollment status and the Baby Postnatal event status is active - And the user opens the enrollment actions menu - When the user completes the enrollment and the active events - Then the user sees the enrollment status and the Baby Postnatal event status is completed + Then the enrollment actions button is not visible + And the enrollment date edit buttons are not visible Scenario: User can see the enrollment minimap Given you land on the enrollment edit event page by having typed #/enrollmentEventEdit?eventId=W1uHdJEjZUI&orgUnitId=DiszpKrYNg8 From cb42811117bea909f39aa6e8bb18f1bbb88d1202 Mon Sep 17 00:00:00 2001 From: henrikmv Date: Mon, 11 May 2026 12:21:25 +0200 Subject: [PATCH 48/60] fix: devin review --- .../WidgetProfile/WidgetProfile.component.tsx | 4 ++-- .../WidgetRelatedStages.container.tsx | 6 ++++-- .../OverflowMenu/OverflowMenu.component.tsx | 4 ++-- .../WidgetHeader/WidgetHeader.container.tsx | 10 ++++++++++ .../WidgetTwoEventWorkspace.container.tsx | 6 +++--- 5 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx b/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx index 03fc78cef3..90cfd1743a 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx +++ b/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx @@ -113,11 +113,11 @@ const WidgetProfilePlain = ({ ); const profileButtonLabel = useMemo(() => { - if (readOnlyMode) return null; + if (readOnlyMode || hasNoAttributes) return null; if (!isEditable) return i18n.t('Show profile'); if (isEditable) return i18n.t('Edit'); return null; - }, [isEditable, readOnlyMode]); + }, [isEditable, readOnlyMode, hasNoAttributes]); const loading = computeLoadingState(programsLoading, trackedEntityInstancesLoading, userRolesLoading, configIsFetched); const error = computeError(programsError, trackedEntityInstancesError, userRolesError); diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.container.tsx b/src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.container.tsx index 2e87d052d5..5190648c6f 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.container.tsx +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.container.tsx @@ -49,10 +49,12 @@ export const WidgetRelatedStagesPlain = ({ }: Props) => { const [isLinking, setIsLinking] = useState(false); const { enrollment } = useCommonEnrollmentDomainData(teiId, enrollmentId, programId); - const { currentRelatedStagesStatus } = useRelatedStages({ programStageId, programId }); + const { currentRelatedStagesStatus, constraint } = useRelatedStages({ programStageId, programId }); const { program } = useProgram(programId); const liveStage = program?.programStages?.find((s: any) => s.id === programStageId); + const linkedStage = program?.programStages?.find((s: any) => s.id === constraint?.programStage?.id); const stageWriteAccess = Boolean(liveStage?.access?.data?.write); + const linkedStageWriteAccess = Boolean(linkedStage?.access?.data?.write); const { linkedEvent, isLoading: isLinkedEventLoading, @@ -110,7 +112,7 @@ export const WidgetRelatedStagesPlain = ({ return null; } - if (program && !stageWriteAccess) { + if (program && (!stageWriteAccess || !linkedStageWriteAccess)) { return null; } diff --git a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/OverflowMenu.component.tsx b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/OverflowMenu.component.tsx index 6db237113c..11f07eb94b 100644 --- a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/OverflowMenu.component.tsx +++ b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/OverflowMenu.component.tsx @@ -68,12 +68,12 @@ export const OverflowMenuComponent = ({ } - disabled={!relationshipTypeWriteAccess} + disabled={!stageWriteAccess || !relationshipTypeWriteAccess} dense dataTest="event-overflow-unlink-event" onClick={handleUnlinkEvent} diff --git a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/WidgetHeader/WidgetHeader.container.tsx b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/WidgetHeader/WidgetHeader.container.tsx index d608c52b05..89f4b6200d 100644 --- a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/WidgetHeader/WidgetHeader.container.tsx +++ b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/WidgetHeader/WidgetHeader.container.tsx @@ -4,6 +4,7 @@ import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { EnrollmentPageKeys } from '../../Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/DefaultEnrollmentLayout.constants'; import { NonBundledDhis2Icon } from '../../NonBundledDhis2Icon'; +import { ReadOnlyBadge } from '../../ReadOnlyBadge'; import type { PlainProps } from './WidgetHeader.types'; import { OverflowMenuComponent } from '../OverflowMenu'; @@ -14,6 +15,9 @@ export const styles: Readonly = { icon: { marginInlineEnd: spacersNum.dp8, }, + badge: { + marginInlineStart: spacersNum.dp8, + }, }; const WidgetHeaderPlain = ({ @@ -30,6 +34,7 @@ const WidgetHeaderPlain = ({ onDeleteEventRelationship, }: PlainProps & WithStyles) => { const { icon } = linkedStage; + const linkedStageWriteAccess = Boolean(linkedStage?.access?.data?.write); return ( <> {icon && ( @@ -44,6 +49,11 @@ const WidgetHeaderPlain = ({
)} {linkedStage.name} + {!linkedStageWriteAccess && ( +
+ +
+ )} {currentPage === EnrollmentPageKeys.VIEW_EVENT && (
- Boolean(program?.programStages?.find((s: any) => s.id === id)?.access?.data?.write); +const stageHasReadAccess = (program: any, id: string | undefined) => + Boolean(program?.programStages?.find((s: any) => s.id === id)?.access?.data?.read); const useTwoEventWorkspaceData = (eventId: string, programId: string, fallbackStageId: string | undefined) => { const { program } = useProgram(programId); @@ -35,7 +35,7 @@ const useTwoEventWorkspaceData = (eventId: string, programId: string, fallbackSt const isError = linkedEventQuery.isError || metadataQuery.isError || clientValuesQuery.isError; const missingData = !linkedEvent || !formFoundation || !linkedStage; const accessBlocked = Boolean(program) - && (!stageHasWriteAccess(program, fallbackStageId) || !stageHasWriteAccess(program, linkedEvent?.programStage)); + && (!stageHasReadAccess(program, fallbackStageId) || !stageHasReadAccess(program, linkedEvent?.programStage)); return { program, From 4ac95d62f2f04a102d01cadc9a215b8b72ddbd0c Mon Sep 17 00:00:00 2001 From: henrikmv Date: Mon, 11 May 2026 12:36:16 +0200 Subject: [PATCH 49/60] fix: devin review finished --- .../WidgetRelatedStages.container.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.container.tsx b/src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.container.tsx index 5190648c6f..eeed2858bf 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.container.tsx +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.container.tsx @@ -14,7 +14,7 @@ import { } from './hooks'; import { relatedStageStatus } from './constants'; import { useCommonEnrollmentDomainData } from '../Pages/common/EnrollmentOverviewDomain'; -import { useProgram } from '../WidgetEnrollment/hooks/useProgram'; +import { useEnrollmentAccessContext } from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; import type { RequestEvent } from '../DataEntries'; const styles = { @@ -50,11 +50,10 @@ export const WidgetRelatedStagesPlain = ({ const [isLinking, setIsLinking] = useState(false); const { enrollment } = useCommonEnrollmentDomainData(teiId, enrollmentId, programId); const { currentRelatedStagesStatus, constraint } = useRelatedStages({ programStageId, programId }); - const { program } = useProgram(programId); - const liveStage = program?.programStages?.find((s: any) => s.id === programStageId); - const linkedStage = program?.programStages?.find((s: any) => s.id === constraint?.programStage?.id); - const stageWriteAccess = Boolean(liveStage?.access?.data?.write); - const linkedStageWriteAccess = Boolean(linkedStage?.access?.data?.write); + const { stageWriteAccessById } = useEnrollmentAccessContext(); + const stageWriteAccess = Boolean(stageWriteAccessById[programStageId]); + const linkedStageId = constraint?.programStage?.id; + const linkedStageWriteAccess = linkedStageId ? Boolean(stageWriteAccessById[linkedStageId]) : false; const { linkedEvent, isLoading: isLinkedEventLoading, @@ -112,7 +111,7 @@ export const WidgetRelatedStagesPlain = ({ return null; } - if (program && (!stageWriteAccess || !linkedStageWriteAccess)) { + if (!stageWriteAccess || !linkedStageWriteAccess) { return null; } From 92a88deffa49fbe89c6c7f3fcc322a1e9291004a Mon Sep 17 00:00:00 2001 From: henrikmv Date: Mon, 11 May 2026 13:00:48 +0200 Subject: [PATCH 50/60] fix: clean up --- .../ProgramStageSelector.feature | 2 +- .../ProgramStageSelector.js | 4 +- .../EnrollmentAccessContext.tsx | 3 -- .../EnrollmentPageLayout.tsx | 2 - .../Actions/Actions.container.tsx | 3 ++ .../WidgetEnrollment/Actions/actions.types.ts | 1 + .../WidgetEnrollment.component.tsx | 42 +++++++++---------- 7 files changed, 27 insertions(+), 30 deletions(-) diff --git a/cypress/e2e/EnrollmentAddEventPage/ProgramStageSelector/ProgramStageSelector.feature b/cypress/e2e/EnrollmentAddEventPage/ProgramStageSelector/ProgramStageSelector.feature index 0e93937044..da55e54d74 100644 --- a/cypress/e2e/EnrollmentAddEventPage/ProgramStageSelector/ProgramStageSelector.feature +++ b/cypress/e2e/EnrollmentAddEventPage/ProgramStageSelector/ProgramStageSelector.feature @@ -19,4 +19,4 @@ Feature: Program stage selector when navigating to EnrollmentEventNew without st @user:trackerAutoTestRestricted Scenario: Stages buttons should not be displayed when no data write access Given user lands on the Enrollment dashboard page by typing #/enrollmentEventNew?enrollmentId=X7g83OFRALm&orgUnitId=DiszpKrYNg8&programId=WSGAb5XwJ3Y&teiId=YsKjdOcl9Cd - Then the New event quick action button is disabled + Then the quick actions widget is not visible diff --git a/cypress/e2e/EnrollmentAddEventPage/ProgramStageSelector/ProgramStageSelector.js b/cypress/e2e/EnrollmentAddEventPage/ProgramStageSelector/ProgramStageSelector.js index c28e147c37..7b3ef8265c 100644 --- a/cypress/e2e/EnrollmentAddEventPage/ProgramStageSelector/ProgramStageSelector.js +++ b/cypress/e2e/EnrollmentAddEventPage/ProgramStageSelector/ProgramStageSelector.js @@ -33,6 +33,6 @@ Then('only three program stages are displayed in the stage selector widget', () cy.get('[data-test=program-stage-selector-button]').should('have.length', 3); }); -Then('the New event quick action button is disabled', () => { - cy.get('[data-test=quick-action-button-report]').should('be.disabled'); +Then('the quick actions widget is not visible', () => { + cy.get('[data-test=quick-action-button-container]').should('not.exist'); }); diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/EnrollmentAccessContext.tsx b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/EnrollmentAccessContext.tsx index 9404142e95..a422520e89 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/EnrollmentAccessContext.tsx +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext/EnrollmentAccessContext.tsx @@ -15,12 +15,9 @@ export type EnrollmentAccessContextValue = { isEventPage: boolean; multipleStages: boolean; allWriteAccessMissing: boolean; - // Widget-level access badges defer to the page-level badge on event pages - // and when all access is missing. showWidgetBadge: boolean; }; -// Fail-open default for renders outside a provider (tests, plugin contexts). const fallback: EnrollmentAccessContextValue = { programWriteAccess: true, trackedEntityTypeWriteAccess: true, diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx index 83adebcddd..00c8a418ee 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx @@ -82,8 +82,6 @@ const EnrollmentReadOnlyBadge = () => { ); } - // No read access to any stage means the user can't see stages at all, - // so don't claim missing stage write access in the badge. const stagesEffectivelyReadOnly = !anyStageWriteAccess && anyStageReadAccess; const showAllMissing = !programWriteAccess && !trackedEntityTypeWriteAccess && stagesEffectivelyReadOnly; if (!showAllMissing) return null; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.container.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.container.tsx index 7b2b73b0ab..833d422dd1 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.container.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.container.tsx @@ -16,6 +16,7 @@ export const Actions = ({ onUpdateEnrollmentStatusSuccess, onSuccess, onAccessLostFromTransfer, + readOnly, ...passOnProps }: Props) => { const { updateMutation, updateLoading } = useUpdateEnrollment(refetchEnrollment, refetchTEI, onError, onSuccess); @@ -47,6 +48,8 @@ export const Actions = ({ [updateStatusMutation, onUpdateEnrollmentStatus, changeRedirect], ); + if (readOnly) return null; + return ( void; + readOnly?: boolean; }; export type PlainProps = { diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx index 4f135cf70e..29b02e6023 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx @@ -56,7 +56,6 @@ const getGeometryType = geometryType => const getEnrollmentDateLabel = program => program.displayEnrollmentDateLabel ?? i18n.t('Enrollment date'); const getIncidentDateLabel = program => program.displayIncidentDateLabel ?? i18n.t('Incident date'); -// eslint-disable-next-line complexity const WidgetEnrollmentPlain = ({ classes, events, @@ -202,27 +201,26 @@ const WidgetEnrollmentPlain = ({ />
)} - {!enrollmentReadOnly && ( - - )} +
)} From 744a709cff15e7e1ebf989f68850e61e044340d9 Mon Sep 17 00:00:00 2001 From: henrikmv Date: Thu, 14 May 2026 15:22:00 +0200 Subject: [PATCH 51/60] feat: (review) keep widget note generic --- .../WidgetEnrollmentNote.component.tsx | 15 ++++++++++++- .../WidgetEventNote.component.tsx | 15 ++++++++++++- .../WidgetNote/WidgetNote.component.tsx | 21 ++++--------------- .../components/WidgetNote/WidgetNote.types.ts | 5 +++-- 4 files changed, 35 insertions(+), 21 deletions(-) diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx index 768221cff2..bc1d4e6f1d 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx @@ -3,6 +3,8 @@ import i18n from '@dhis2/d2-i18n'; import { useDispatch, useSelector } from 'react-redux'; import { requestAddNoteForEnrollment } from './WidgetEnrollmentNote.actions'; import { WidgetNote } from '../WidgetNote'; +import { ReadOnlyBadge } from '../ReadOnlyBadge'; +import { useEnrollmentAccessContext } from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; import { useLocationQuery } from '../../utils/routing'; export const WidgetEnrollmentNote = () => { @@ -10,6 +12,11 @@ export const WidgetEnrollmentNote = () => { const { enrollmentId } = useLocationQuery(); const notes = useSelector(({ enrollmentDomain }: { enrollmentDomain?: { enrollment?: { notes?: Array } } }) => enrollmentDomain?.enrollment?.notes ?? []); + const { + programWriteAccess, + trackedEntityTypeName, + showWidgetBadge, + } = useEnrollmentAccessContext(); const onAddNote = (newNoteValue: string) => { dispatch(requestAddNoteForEnrollment(enrollmentId, newNoteValue)); @@ -22,7 +29,13 @@ export const WidgetEnrollmentNote = () => { placeholder={i18n.t('Write a note about this enrollment')} emptyNoteMessage={i18n.t('This enrollment doesn\'t have any notes')} notes={notes} - scope="enrollment" + readOnly={!programWriteAccess} + badge={showWidgetBadge ? ( + + ) : null} onAddNote={onAddNote} />
diff --git a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx index 4b2913a38e..d2306069a7 100644 --- a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx @@ -4,11 +4,18 @@ import i18n from '@dhis2/d2-i18n'; import type { Props } from './WidgetEventNote.types'; import { requestAddNoteForEvent } from './WidgetEventNote.actions'; import { WidgetNote } from '../WidgetNote'; +import { ReadOnlyBadge } from '../ReadOnlyBadge'; +import { useEnrollmentAccessContext } from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; export const WidgetEventNote = ({ dataEntryKey, dataEntryId }: Props) => { const dispatch = useDispatch(); const notes = useSelector(({ dataEntriesNotes }: { dataEntriesNotes: Record }) => dataEntriesNotes[`${dataEntryId}-${dataEntryKey}`] ?? []); + const { + currentStageWriteAccess, + trackedEntityTypeName, + showWidgetBadge, + } = useEnrollmentAccessContext(); const onAddNote = (newNoteValue: string) => { dispatch(requestAddNoteForEvent(dataEntryKey, dataEntryId, newNoteValue)); @@ -21,7 +28,13 @@ export const WidgetEventNote = ({ dataEntryKey, dataEntryId }: Props) => { placeholder={i18n.t('Write a note about this event')} emptyNoteMessage={i18n.t('This event doesn\'t have any notes')} notes={notes} - scope="event" + readOnly={!currentStageWriteAccess} + badge={showWidgetBadge ? ( + + ) : null} onAddNote={onAddNote} />
diff --git a/src/core_modules/capture-core/components/WidgetNote/WidgetNote.component.tsx b/src/core_modules/capture-core/components/WidgetNote/WidgetNote.component.tsx index 0416aa848c..d2767fa53c 100644 --- a/src/core_modules/capture-core/components/WidgetNote/WidgetNote.component.tsx +++ b/src/core_modules/capture-core/components/WidgetNote/WidgetNote.component.tsx @@ -2,8 +2,6 @@ import React, { useState, useCallback } from 'react'; import { spacersNum } from '@dhis2/ui'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { Widget, WidgetHeaderCountBadge } from '../Widget'; -import { ReadOnlyBadge } from '../ReadOnlyBadge'; -import { useEnrollmentAccessContext } from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; import type { Props } from './WidgetNote.types'; import { NoteSection } from './NoteSection/NoteSection'; @@ -23,32 +21,21 @@ const WidgetNotePlain = ({ classes, title, notes, - scope, + readOnly, + badge, onAddNote, ...passOnProps }: Props & WithStyles) => { const [open, setOpenStatus] = useState(true); - const { - programWriteAccess, - currentStageWriteAccess, - trackedEntityTypeName, - showWidgetBadge, - } = useEnrollmentAccessContext(); - const isEventScope = scope === 'event'; - const readOnly = isEventScope ? !currentStageWriteAccess : !programWriteAccess; return ( {title} {notes.length > 0 && } - {showWidgetBadge && ( + {badge && (
- + {badge}
)}
} diff --git a/src/core_modules/capture-core/components/WidgetNote/WidgetNote.types.ts b/src/core_modules/capture-core/components/WidgetNote/WidgetNote.types.ts index b2ceb42cfe..289cddfaf1 100644 --- a/src/core_modules/capture-core/components/WidgetNote/WidgetNote.types.ts +++ b/src/core_modules/capture-core/components/WidgetNote/WidgetNote.types.ts @@ -1,10 +1,11 @@ -export type NoteScope = 'enrollment' | 'event'; +import type { ReactNode } from 'react'; export type Props = { title: string; placeholder: string; emptyNoteMessage: string; - scope: NoteScope; + readOnly: boolean; + badge?: ReactNode; notes: Array<{ value: string; storedAt: string; From bcb1672ecaa52336151d0347933fdfd985931cb2 Mon Sep 17 00:00:00 2001 From: henrikmv Date: Thu, 14 May 2026 15:25:05 +0200 Subject: [PATCH 52/60] feat: (review) include message in translated string --- i18n/en.pot | 7 +++++-- .../components/ReadOnlyBadge/ReadOnlyBadge.tsx | 4 +++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 9639152821..797a374962 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: 2026-05-08T06:27:13.394Z\n" -"PO-Revision-Date: 2026-05-08T06:27:13.394Z\n" +"POT-Creation-Date: 2026-05-14T13:25:06.803Z\n" +"PO-Revision-Date: 2026-05-14T13:25:06.803Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -1089,6 +1089,9 @@ msgstr "You only have view access to these program stages" msgid "You only have view access to this program stage" msgstr "You only have view access to this program stage" +msgid "View only - {{message}}" +msgstr "View only - {{message}}" + msgid "View only" msgstr "View only" diff --git a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx index f155c2f93a..c8ffa61817 100644 --- a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx +++ b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx @@ -58,7 +58,9 @@ export const ReadOnlyBadge = ({ programStage: programStageWriteAccess, }; const message = getMissingAccessMessage(access, trackedEntityName, multipleStages); - const labelText = inlineLabel && message ? `${i18n.t('View only')} - ${message}` : i18n.t('View only'); + const labelText = inlineLabel && message + ? i18n.t('View only - {{message}}', { message, escapeValue: false }) + : i18n.t('View only'); return ( From be65268396c99f3387ef1d3f205ef270e41c0e19 Mon Sep 17 00:00:00 2001 From: henrikmv Date: Thu, 14 May 2026 16:00:18 +0200 Subject: [PATCH 53/60] eat: (review) wrap EnrollmentAddEventPageDefault with EnrollmentAccessProvider --- ...nrollmentAddEventPageDefault.component.tsx | 51 +++++++++++-------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/EnrollmentAddEventPageDefault/EnrollmentAddEventPageDefault.component.tsx b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/EnrollmentAddEventPageDefault/EnrollmentAddEventPageDefault.component.tsx index 8d2cd506db..5d54cdf067 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/EnrollmentAddEventPageDefault/EnrollmentAddEventPageDefault.component.tsx +++ b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/EnrollmentAddEventPageDefault/EnrollmentAddEventPageDefault.component.tsx @@ -4,9 +4,11 @@ import { spacersNum } from '@dhis2/ui'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import type { Props } from './EnrollmentAddEventPageDefault.types'; import { EnrollmentPageLayout } from '../../common/EnrollmentOverviewDomain/EnrollmentPageLayout'; +import { EnrollmentAccessProvider } from '../../common/EnrollmentOverviewDomain'; import { EnrollmentPageKeys, } from '../../common/EnrollmentOverviewDomain/EnrollmentPageLayout/DefaultEnrollmentLayout.constants'; +import { TrackerProgram } from '../../../../metaData'; const styles: Readonly = ({ typography }: any) => ({ container: { @@ -67,28 +69,33 @@ const EnrollmentAddEventPagePain = ({ return null; } return ( -
- -
+ +
+ +
+
); }; From 144c2391f4a53de2cfb356bee5605dd56620a879 Mon Sep 17 00:00:00 2001 From: henrikmv Date: Fri, 15 May 2026 09:31:51 +0200 Subject: [PATCH 54/60] feat: (review) create file for EnrollmentReadOnlyBadge --- .../EnrollmentPageLayout.tsx | 40 +------------------ .../EnrollmentReadOnlyBadge.component.tsx | 40 +++++++++++++++++++ .../EnrollmentReadOnlyBadge/index.ts | 1 + 3 files changed, 42 insertions(+), 39 deletions(-) create mode 100644 src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentReadOnlyBadge/EnrollmentReadOnlyBadge.component.tsx create mode 100644 src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentReadOnlyBadge/index.ts diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx index 00c8a418ee..69ad18dab5 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.tsx @@ -5,8 +5,7 @@ import { useWidgetColumns } from './hooks/useWidgetColumns'; import { AddRelationshipRefWrapper } from './AddRelationshipRefWrapper'; import type { Props as EnrollmentPageProps } from '../../../Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types'; import { EnrollmentBreadcrumb } from '../../../../Breadcrumbs/EnrollmentBreadcrumb'; -import { ReadOnlyBadge } from '../../../../ReadOnlyBadge'; -import { useEnrollmentAccessContext } from '../EnrollmentAccessContext'; +import { EnrollmentReadOnlyBadge } from './EnrollmentReadOnlyBadge'; import './enrollmentPageLayout.css'; const getEnrollmentPageStyles: Readonly = () => ({ @@ -60,43 +59,6 @@ const getEnrollmentPageStyles: Readonly = () => ({ const isValidHex = (color: string) => /^#[0-9A-F]{6}$/i.test(color); -const EnrollmentReadOnlyBadge = () => { - const { - isEventPage, - currentStageWriteAccess, - programWriteAccess, - trackedEntityTypeWriteAccess, - anyStageWriteAccess, - anyStageReadAccess, - trackedEntityTypeName, - } = useEnrollmentAccessContext(); - - if (isEventPage) { - if (currentStageWriteAccess) return null; - return ( - - ); - } - - const stagesEffectivelyReadOnly = !anyStageWriteAccess && anyStageReadAccess; - const showAllMissing = !programWriteAccess && !trackedEntityTypeWriteAccess && stagesEffectivelyReadOnly; - if (!showAllMissing) return null; - - return ( - - ); -}; - type OwnProps = EnrollmentPageProps; type Props = OwnProps & WithStyles; diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentReadOnlyBadge/EnrollmentReadOnlyBadge.component.tsx b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentReadOnlyBadge/EnrollmentReadOnlyBadge.component.tsx new file mode 100644 index 0000000000..e1c71b26e2 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentReadOnlyBadge/EnrollmentReadOnlyBadge.component.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { ReadOnlyBadge } from '../../../../../ReadOnlyBadge'; +import { useEnrollmentAccessContext } from '../../EnrollmentAccessContext'; + +export const EnrollmentReadOnlyBadge = () => { + const { + isEventPage, + currentStageWriteAccess, + programWriteAccess, + trackedEntityTypeWriteAccess, + anyStageWriteAccess, + anyStageReadAccess, + trackedEntityTypeName, + } = useEnrollmentAccessContext(); + + if (isEventPage) { + if (currentStageWriteAccess) return null; + return ( + + ); + } + + const stagesEffectivelyReadOnly = !anyStageWriteAccess && anyStageReadAccess; + const showAllMissing = !programWriteAccess && !trackedEntityTypeWriteAccess && stagesEffectivelyReadOnly; + if (!showAllMissing) return null; + + return ( + + ); +}; diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentReadOnlyBadge/index.ts b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentReadOnlyBadge/index.ts new file mode 100644 index 0000000000..30131d8301 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentReadOnlyBadge/index.ts @@ -0,0 +1 @@ +export { EnrollmentReadOnlyBadge } from './EnrollmentReadOnlyBadge.component'; From 05a7a082f5a679670d73f12cb27e0768d9004943 Mon Sep 17 00:00:00 2001 From: henrikmv Date: Fri, 15 May 2026 10:19:28 +0200 Subject: [PATCH 55/60] feat: (review) split widget profile read only data entry from write access data entry --- i18n/en.pot | 14 +- .../DataEntry/DataEntry.component.tsx | 130 ++++++++---------- .../DataEntry/DataEntry.container.tsx | 39 ++++-- .../DataEntryModalWrapper.component.tsx | 51 +++++++ .../DataEntry/DataEntryReadOnly.component.tsx | 61 ++++++++ .../DataEntry/dataEntry.types.ts | 22 --- 6 files changed, 204 insertions(+), 113 deletions(-) create mode 100644 src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntryModalWrapper.component.tsx create mode 100644 src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntryReadOnly.component.tsx diff --git a/i18n/en.pot b/i18n/en.pot index 797a374962..c38dc7b70f 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: 2026-05-14T13:25:06.803Z\n" -"PO-Revision-Date: 2026-05-14T13:25:06.803Z\n" +"POT-Creation-Date: 2026-05-15T08:19:29.942Z\n" +"PO-Revision-Date: 2026-05-15T08:19:29.942Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -1611,20 +1611,20 @@ msgstr "Indicators" msgid "Save note" msgstr "Save note" -msgid "{{trackedEntityName}} profile" -msgstr "{{trackedEntityName}} profile" - msgid "Edit {{trackedEntityName}}" msgstr "Edit {{trackedEntityName}}" +msgid "Save changes" +msgstr "Save changes" + msgid "Change information about this {{trackedEntityName}} here." msgstr "Change information about this {{trackedEntityName}} here." msgid "Information about this enrollment can be edited in the Enrollment widget." msgstr "Information about this enrollment can be edited in the Enrollment widget." -msgid "Save changes" -msgstr "Save changes" +msgid "{{trackedEntityName}} profile" +msgstr "{{trackedEntityName}} profile" msgid "Profile" msgstr "Profile" diff --git a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.component.tsx b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.component.tsx index 4b92172d87..5860863d7e 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.component.tsx +++ b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.component.tsx @@ -1,24 +1,31 @@ import React from 'react'; -import { Modal, ModalTitle, ModalContent, ModalActions, ButtonStrip, Button, spacersNum } from '@dhis2/ui'; +import { Button } from '@dhis2/ui'; import i18n from '@dhis2/d2-i18n'; -import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { NoticeBoxes } from './NoticeBoxes.container'; -import type { PlainProps } from './dataEntry.types'; import { DataEntry } from '../../DataEntry'; -import { ReadOnlyBadge } from '../../ReadOnlyBadge'; +import { DataEntryModalWrapper } from './DataEntryModalWrapper.component'; import { TEI_MODAL_STATE } from './dataEntry.actions'; +import type { PluginContext } from '../../D2Form/FormFieldPlugin/FormFieldPlugin.types'; -const styles = { - title: { - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - gap: `${spacersNum.dp8}px`, - }, +type Props = { + dataEntryId: string; + trackedEntityName: string; + saveAttempted: boolean; + formFoundation: any; + onCancel: () => void; + onSave: () => void; + onUpdateFormField: (innerAction: any) => void; + onUpdateFormFieldAsync: (innerAction: any) => void; + onGetValidationContext: () => Record; + modalState: string; + errorsMessages: Array<{ id: string; message: string }>; + warningsMessages: Array<{ id: string; message: string }>; + orgUnitId: string; + pluginContext?: PluginContext; + accessReadOnly?: boolean; }; -const DataEntryComponentPlain = ({ - classes, +export const DataEntryComponent = ({ dataEntryId, onCancel, onSave, @@ -33,76 +40,51 @@ const DataEntryComponentPlain = ({ warningsMessages, orgUnitId, pluginContext, - readOnly, accessReadOnly, -}: PlainProps & WithStyles) => ( - - -
- - {readOnly - ? i18n.t( - '{{trackedEntityName}} profile', - { trackedEntityName, interpolation: { escapeValue: false } }, - ) - : i18n.t('Edit {{trackedEntityName}}', { trackedEntityName, interpolation: { escapeValue: false } }) - } - - -
-
- - {!readOnly && ( - <> - {i18n.t( - 'Change information about this {{trackedEntityName}} here.', - { trackedEntityName, interpolation: { escapeValue: false } }, - )} - {' '} - {i18n.t('Information about this enrollment can be edited in the Enrollment widget.')} - - )} - - {!readOnly && ( - - )} - - - +}: Props) => ( + - {!readOnly && modalState === TEI_MODAL_STATE.OPEN_DISABLE && ( + {modalState === TEI_MODAL_STATE.OPEN_DISABLE && ( )} - {!readOnly && (modalState === TEI_MODAL_STATE.OPEN || modalState === TEI_MODAL_STATE.OPEN_ERROR) && ( + {(modalState === TEI_MODAL_STATE.OPEN || modalState === TEI_MODAL_STATE.OPEN_ERROR) && ( )} - - -
+ + } + > + {i18n.t( + 'Change information about this {{trackedEntityName}} here.', + { trackedEntityName, interpolation: { escapeValue: false } }, + )} + {' '} + {i18n.t('Information about this enrollment can be edited in the Enrollment widget.')} + + + ); - -export const DataEntryComponent = withStyles(styles)(DataEntryComponentPlain); diff --git a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.container.tsx b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.container.tsx index d99d83c60c..5c547e8a55 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.container.tsx +++ b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.container.tsx @@ -9,6 +9,7 @@ import { dataElementTypes } from 'capture-core/metaData'; import { makeQuerySingleResource } from 'capture-core/utils/api'; import type { Props } from './dataEntry.types'; import { DataEntryComponent } from './DataEntry.component'; +import { DataEntryReadOnlyComponent } from './DataEntryReadOnly.component'; import { useLifecycle, useFormValidations } from './hooks'; import { getUpdateFieldActions, updateTeiRequest, setTeiModalError } from './dataEntry.actions'; import { startRunRulesPostUpdateField } from '../../DataEntry'; @@ -147,27 +148,45 @@ export const DataEntry = ({ onDisable, ]); - return ( - Object.entries(formFoundation).length > 0 && ( - - ) + ); + } + + return ( + ); }; diff --git a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntryModalWrapper.component.tsx b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntryModalWrapper.component.tsx new file mode 100644 index 0000000000..c768612d9d --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntryModalWrapper.component.tsx @@ -0,0 +1,51 @@ +import React, { type ReactNode } from 'react'; +import { Modal, ModalTitle, ModalContent, ModalActions, ButtonStrip, spacersNum } from '@dhis2/ui'; +import { withStyles, type WithStyles } from 'capture-core-utils/styles'; +import { ReadOnlyBadge } from '../../ReadOnlyBadge'; + +const styles = { + title: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + gap: `${spacersNum.dp8}px`, + }, +}; + +type Props = { + title: ReactNode; + actions: ReactNode; + onClose: () => void; + trackedEntityName: string; + accessReadOnly?: boolean; + children: ReactNode; +}; + +const DataEntryModalWrapperPlain = ({ + classes, + title, + actions, + onClose, + trackedEntityName, + accessReadOnly, + children, +}: Props & WithStyles) => ( + + +
+ {title} + +
+
+ {children} + + {actions} + +
+); + +export const DataEntryModalWrapper = withStyles(styles)(DataEntryModalWrapperPlain); diff --git a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntryReadOnly.component.tsx b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntryReadOnly.component.tsx new file mode 100644 index 0000000000..910e8ab1f5 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntryReadOnly.component.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { Button } from '@dhis2/ui'; +import i18n from '@dhis2/d2-i18n'; +import { DataEntry } from '../../DataEntry'; +import { DataEntryModalWrapper } from './DataEntryModalWrapper.component'; +import type { PluginContext } from '../../D2Form/FormFieldPlugin/FormFieldPlugin.types'; + +type Props = { + dataEntryId: string; + trackedEntityName: string; + saveAttempted: boolean; + formFoundation: any; + onCancel: () => void; + onUpdateFormField: (innerAction: any) => void; + onUpdateFormFieldAsync: (innerAction: any) => void; + onGetValidationContext: () => Record; + orgUnitId: string; + pluginContext?: PluginContext; + accessReadOnly?: boolean; +}; + +export const DataEntryReadOnlyComponent = ({ + dataEntryId, + onCancel, + saveAttempted, + onUpdateFormField, + onUpdateFormFieldAsync, + trackedEntityName, + formFoundation, + onGetValidationContext, + orgUnitId, + pluginContext, + accessReadOnly, +}: Props) => ( + + {i18n.t('Close')} + + } + > + + +); diff --git a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/dataEntry.types.ts b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/dataEntry.types.ts index 90f3dee1ee..1683e213f5 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/dataEntry.types.ts +++ b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/dataEntry.types.ts @@ -2,28 +2,6 @@ import type { Geometry } from './helpers/types'; import type { DataEntryFormConfig, } from '../../DataEntries/common/TEIAndEnrollment'; -import type { PluginContext } from '../../D2Form/FormFieldPlugin/FormFieldPlugin.types'; - -export type PlainProps = { - dataEntryId: string; - itemId: string; - trackedEntityName: string; - saveAttempted: boolean; - formFoundation: any; - onCancel: () => void; - onSave: () => void; - onUpdateFormField: (innerAction: any) => void; - onUpdateFormFieldAsync: (innerAction: any) => void; - onGetValidationContext: () => Record; - modalState: string; - errorsMessages: Array<{ id: string; message: string }>; - warningsMessages: Array<{ id: string; message: string }>; - center?: Array; - orgUnitId: string; - pluginContext?: PluginContext; - readOnly?: boolean; - accessReadOnly?: boolean; -}; export type Props = { programAPI: any; From c7fdc6381d4377b12315690294abc237feb85567 Mon Sep 17 00:00:00 2001 From: henrikmv Date: Mon, 18 May 2026 11:00:11 +0200 Subject: [PATCH 56/60] fix: (review) revert inline typing --- .../DataEntry/DataEntry.component.tsx | 22 ++----------- .../DataEntry/dataEntry.types.ts | 33 +++++++++++++++++++ 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.component.tsx b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.component.tsx index 5860863d7e..29816ed5ea 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.component.tsx +++ b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.component.tsx @@ -2,28 +2,10 @@ import React from 'react'; import { Button } from '@dhis2/ui'; import i18n from '@dhis2/d2-i18n'; import { NoticeBoxes } from './NoticeBoxes.container'; +import type { PlainProps } from './dataEntry.types'; import { DataEntry } from '../../DataEntry'; import { DataEntryModalWrapper } from './DataEntryModalWrapper.component'; import { TEI_MODAL_STATE } from './dataEntry.actions'; -import type { PluginContext } from '../../D2Form/FormFieldPlugin/FormFieldPlugin.types'; - -type Props = { - dataEntryId: string; - trackedEntityName: string; - saveAttempted: boolean; - formFoundation: any; - onCancel: () => void; - onSave: () => void; - onUpdateFormField: (innerAction: any) => void; - onUpdateFormFieldAsync: (innerAction: any) => void; - onGetValidationContext: () => Record; - modalState: string; - errorsMessages: Array<{ id: string; message: string }>; - warningsMessages: Array<{ id: string; message: string }>; - orgUnitId: string; - pluginContext?: PluginContext; - accessReadOnly?: boolean; -}; export const DataEntryComponent = ({ dataEntryId, @@ -41,7 +23,7 @@ export const DataEntryComponent = ({ orgUnitId, pluginContext, accessReadOnly, -}: Props) => ( +}: PlainProps) => ( void; + onSave: () => void; + onUpdateFormField: (innerAction: any) => void; + onUpdateFormFieldAsync: (innerAction: any) => void; + onGetValidationContext: () => Record; + modalState: string; + errorsMessages: Array<{ id: string; message: string }>; + warningsMessages: Array<{ id: string; message: string }>; + orgUnitId: string; + pluginContext?: PluginContext; + accessReadOnly?: boolean; +}; + +export type ReadOnlyPlainProps = { + dataEntryId: string; + trackedEntityName: string; + saveAttempted: boolean; + formFoundation: any; + onCancel: () => void; + onUpdateFormField: (innerAction: any) => void; + onUpdateFormFieldAsync: (innerAction: any) => void; + onGetValidationContext: () => Record; + orgUnitId: string; + pluginContext?: PluginContext; + accessReadOnly?: boolean; +}; export type Props = { programAPI: any; From 8838b7302c6dbf455eaf1e6039fccc272bf16852 Mon Sep 17 00:00:00 2001 From: henrikmv Date: Mon, 18 May 2026 20:29:22 +0200 Subject: [PATCH 57/60] feat: expired period and block data entry after completed read only badge --- i18n/en.pot | 13 +++-- .../EnrollmentEditEventPage.container.tsx | 23 +++++++-- .../EnrollmentAccessContext.tsx | 16 ++++++- .../EnrollmentReadOnlyBadge.component.tsx | 7 ++- .../ReadOnlyBadge/ReadOnlyBadge.tsx | 30 +++++++++--- .../WidgetHeader/WidgetHeader.container.tsx | 48 +++++-------------- 6 files changed, 82 insertions(+), 55 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index c38dc7b70f..86e3e66a6e 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: 2026-05-15T08:19:29.942Z\n" -"PO-Revision-Date: 2026-05-15T08:19:29.942Z\n" +"POT-Creation-Date: 2026-05-18T18:28:17.335Z\n" +"PO-Revision-Date: 2026-05-18T18:28:17.335Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -1089,6 +1089,12 @@ msgstr "You only have view access to these program stages" msgid "You only have view access to this program stage" msgstr "You only have view access to this program stage" +msgid "This event is outside the valid period" +msgstr "This event is outside the valid period" + +msgid "This event has been completed" +msgstr "This event has been completed" + msgid "View only - {{message}}" msgstr "View only - {{message}}" @@ -1533,9 +1539,6 @@ msgstr "No polygon captured" msgid "Event completed" msgstr "Event completed" -msgid "The event cannot be edited after it has been completed" -msgstr "The event cannot be edited after it has been completed" - msgid "Notes about this event" msgstr "Notes about this event" diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.tsx b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.tsx index 27631adbd6..7e32fe94f8 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.tsx +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.tsx @@ -3,7 +3,10 @@ import type { ProgramRule } from '@dhis2/rules-engine-javascript'; import { useQueryClient } from '@tanstack/react-query'; import { useDispatch, useSelector } from 'react-redux'; import { dataEntryIds } from 'capture-core/constants'; -import { useEnrollmentEditEventPageMode, useHideWidgetByRuleLocations } from '../../../hooks'; +import { useEnrollmentEditEventPageMode, useHideWidgetByRuleLocations, useProgramExpiryForUser } from '../../../hooks'; +import { isValidPeriod } from '../../../utils/validation/validators/form/expiredPeriod'; +import { useAuthorities } from '../../../utils/authority/useAuthorities'; +import { eventStatuses } from '../../WidgetEventEdit/constants/status.const'; import type { ReduxState } from '../../App/withAppUrlSync.types'; import { commitEnrollmentAndEvents, @@ -42,7 +45,7 @@ import { DataStoreKeyByPage, useEnrollmentPageLayout } from '../common/Enrollmen import { DefaultPageLayout } from './PageLayout/DefaultPageLayout.constants'; import { getProgramEventAccess } from '../../../metaData'; import { rollbackAssignee, setAssignee } from './EnrollmentEditEventPage.actions'; -import { convertClientToServer } from '../../../converters'; +import { convertClientToServer, convertServerToClient } from '../../../converters'; import { CHANGELOG_ENTITY_TYPES } from '../../WidgetsChangelog'; import { ReactQueryAppNamespace } from '../../../utils/reactQueryHelpers'; import { statusTypes } from '../../../enrollment'; @@ -265,6 +268,15 @@ const EnrollmentEditEventPageWithContextPlain = ({ const outputEffects = useWidgetDataFromStore(dataEntryKey); const eventAccess = getProgramEventAccess(programId, programStage?.id ?? null); + const expiryPeriod = useProgramExpiryForUser(programId); + const occurredAtClient = convertServerToClient(event?.occurredAt, dataElementTypes.DATE) as string; + const isEventWithinValidPeriod = isValidPeriod(occurredAtClient, expiryPeriod).isWithinValidPeriod; + + const { hasAuthority: canUncompleteEvent } = useAuthorities({ authorities: ['F_UNCOMPLETE_EVENT'] }); + const canEditCompletedEvent = !(programStage?.blockEntryForm + && !canUncompleteEvent + && event?.status === eventStatuses.COMPLETED); + const pageStatus = getPageStatus({ orgUnitId, @@ -296,7 +308,12 @@ const EnrollmentEditEventPageWithContextPlain = ({ } return ( - + (fallback); type ProviderProps = { program?: TrackerProgram; currentStageId?: string; + isEventWithinValidPeriod?: boolean; + canEditCompletedEvent?: boolean; children: React.ReactNode; }; -export const EnrollmentAccessProvider = ({ program, currentStageId, children }: ProviderProps) => { +export const EnrollmentAccessProvider = ({ + program, + currentStageId, + isEventWithinValidPeriod, + canEditCompletedEvent, + children, +}: ProviderProps) => { const value = useMemo(() => { if (!program) return fallback; @@ -78,8 +88,10 @@ export const EnrollmentAccessProvider = ({ program, currentStageId, children }: multipleStages: program.stages.size > 1, allWriteAccessMissing, showWidgetBadge: !isEventPage && !allWriteAccessMissing, + isEventWithinValidPeriod, + canEditCompletedEvent, }; - }, [program, currentStageId]); + }, [program, currentStageId, isEventWithinValidPeriod, canEditCompletedEvent]); return {children}; }; diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentReadOnlyBadge/EnrollmentReadOnlyBadge.component.tsx b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentReadOnlyBadge/EnrollmentReadOnlyBadge.component.tsx index e1c71b26e2..a106516b60 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentReadOnlyBadge/EnrollmentReadOnlyBadge.component.tsx +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentReadOnlyBadge/EnrollmentReadOnlyBadge.component.tsx @@ -11,13 +11,16 @@ export const EnrollmentReadOnlyBadge = () => { anyStageWriteAccess, anyStageReadAccess, trackedEntityTypeName, + isEventWithinValidPeriod, + canEditCompletedEvent, } = useEnrollmentAccessContext(); if (isEventPage) { - if (currentStageWriteAccess) return null; return ( diff --git a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx index c8ffa61817..ffa539a581 100644 --- a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx +++ b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx @@ -7,6 +7,8 @@ type Props = { programWriteAccess?: boolean; trackedEntityTypeWriteAccess?: boolean; programStageWriteAccess?: boolean; + eventWithinValidPeriod?: boolean; + canEditCompletedEvent?: boolean; multipleStages?: boolean; trackedEntityName?: string; inlineLabel?: boolean; @@ -30,15 +32,23 @@ const getProgramStageMessage = (multipleStages: boolean): string => (multipleSta ? i18n.t('You only have view access to these program stages') : i18n.t('You only have view access to this program stage')); -const getMissingAccessMessage = ( +const getExpiredPeriodMessage = (): string => i18n.t('This event is outside the valid period'); + +const getCompletedEventMessage = (): string => i18n.t('This event has been completed'); + +const getReadOnlyMessage = ( access: Access, trackedEntityName: string | undefined, multipleStages: boolean, + eventWithinValidPeriod: boolean, + canEditCompletedEvent: boolean, ): string => { if (!access.program && !access.trackedEntityType && !access.programStage) return getEnrollmentMessage(); if (!access.program) return getProgramMessage(); if (!access.trackedEntityType) return getTrackedEntityMessage(trackedEntityName); if (!access.programStage) return getProgramStageMessage(multipleStages); + if (!eventWithinValidPeriod) return getExpiredPeriodMessage(); + if (!canEditCompletedEvent) return getCompletedEventMessage(); return ''; }; @@ -46,24 +56,32 @@ export const ReadOnlyBadge = ({ programWriteAccess = true, trackedEntityTypeWriteAccess = true, programStageWriteAccess = true, + eventWithinValidPeriod = true, + canEditCompletedEvent = true, multipleStages = false, trackedEntityName, inlineLabel = false, }: Props) => { - if (programWriteAccess && trackedEntityTypeWriteAccess && programStageWriteAccess) return null; - const access: Access = { program: programWriteAccess, trackedEntityType: trackedEntityTypeWriteAccess, programStage: programStageWriteAccess, }; - const message = getMissingAccessMessage(access, trackedEntityName, multipleStages); - const labelText = inlineLabel && message + const message = getReadOnlyMessage( + access, + trackedEntityName, + multipleStages, + eventWithinValidPeriod, + canEditCompletedEvent, + ); + if (!message) return null; + + const labelText = inlineLabel ? i18n.t('View only - {{message}}', { message, escapeValue: false }) : i18n.t('View only'); return ( - + }> {labelText} diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/WidgetHeader/WidgetHeader.container.tsx b/src/core_modules/capture-core/components/WidgetEventEdit/WidgetHeader/WidgetHeader.container.tsx index e229b74fbb..d7e2513009 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/WidgetHeader/WidgetHeader.container.tsx +++ b/src/core_modules/capture-core/components/WidgetEventEdit/WidgetHeader/WidgetHeader.container.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useMemo } from 'react'; +import React, { useState, useEffect } from 'react'; import { dataEntryKeys } from 'capture-core/constants'; import { useDispatch } from 'react-redux'; import { spacersNum, Button, IconEdit24, IconMore16, FlyoutMenu, MenuItem, spacers } from '@dhis2/ui'; @@ -6,7 +6,6 @@ import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import i18n from '@dhis2/d2-i18n'; import { FEATURES, useFeature } from 'capture-core-utils'; import { useAuthorities } from 'capture-core/utils/authority/useAuthorities'; -import { ConditionalTooltip } from 'capture-core/components/Tooltips/ConditionalTooltip'; import { useEnrollmentEditEventPageMode, useProgramExpiryForUser } from 'capture-core/hooks'; import { startShowEditEventDataEntry } from '../WidgetEventEdit.actions'; import { NonBundledDhis2Icon } from '../../NonBundledDhis2Icon'; @@ -31,9 +30,6 @@ const styles: Readonly = { alignItems: 'center', gap: spacers.dp4, }, - tooltip: { - display: 'inline-flex', - }, }; type Props = PlainProps & WithStyles; @@ -62,22 +58,7 @@ const WidgetHeaderPlain = ({ const occurredAtClient = convertFormToClient(occurredAt, dataElementTypes.DATE) as string; const { isWithinValidPeriod } = isValidPeriod(occurredAtClient, expiryPeriod); - const disableEdit = !eventAccess?.write || blockEntryForm || !isWithinValidPeriod; - const tooltipContent = useMemo(() => { - if (blockEntryForm) { - return i18n.t('The event cannot be edited after it has been completed'); - } - if (!eventAccess?.write) { - return i18n.t('You don\'t have access to edit this event'); - } - if (!isWithinValidPeriod) { - return i18n.t('{{occurredAt}} belongs to an expired period. Event cannot be edited', { - occurredAt, - interpolation: { escapeValue: false }, - }); - } - return ''; - }, [blockEntryForm, eventAccess?.write, isWithinValidPeriod, occurredAt]); + const showEditButton = eventAccess?.write && isWithinValidPeriod && !blockEntryForm; const { programCategory } = useCategoryCombinations(programId); @@ -100,23 +81,16 @@ const WidgetHeaderPlain = ({
{currentPageMode === dataEntryKeys.VIEW && (
- {eventAccess?.write && ( - } + onClick={() => dispatch(startShowEditEventDataEntry(orgUnit, programCategory))} + data-test="widget-enrollment-event-edit-button" > - - + {i18n.t('Edit event')} + )} {supportsChangelog && ( From 0d56c2bf189ad69c566824a7173c74bbb0de442a Mon Sep 17 00:00:00 2001 From: henrikmv Date: Mon, 18 May 2026 21:07:32 +0200 Subject: [PATCH 58/60] fix: increase font weight --- .../components/ReadOnlyBadge/ReadOnlyBadge.tsx | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx index ffa539a581..4a8dfc5818 100644 --- a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx +++ b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx @@ -1,8 +1,15 @@ import React from 'react'; import { IconInfo16, Tag } from '@dhis2/ui'; import i18n from '@dhis2/d2-i18n'; +import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { ConditionalTooltip } from '../Tooltips/ConditionalTooltip'; +const styles = { + label: { + fontWeight: 500, + }, +} as const; + type Props = { programWriteAccess?: boolean; trackedEntityTypeWriteAccess?: boolean; @@ -52,7 +59,7 @@ const getReadOnlyMessage = ( return ''; }; -export const ReadOnlyBadge = ({ +const ReadOnlyBadgePlain = ({ programWriteAccess = true, trackedEntityTypeWriteAccess = true, programStageWriteAccess = true, @@ -61,7 +68,8 @@ export const ReadOnlyBadge = ({ multipleStages = false, trackedEntityName, inlineLabel = false, -}: Props) => { + classes, +}: Props & WithStyles) => { const access: Access = { program: programWriteAccess, trackedEntityType: trackedEntityTypeWriteAccess, @@ -83,8 +91,10 @@ export const ReadOnlyBadge = ({ return ( }> - {labelText} + {labelText} ); }; + +export const ReadOnlyBadge = withStyles(styles)(ReadOnlyBadgePlain); From 1af3a8ba4fa505ec97913ce19c49a9744fce1bb6 Mon Sep 17 00:00:00 2001 From: henrikmv Date: Sun, 24 May 2026 13:43:29 +0200 Subject: [PATCH 59/60] feat: (review) update strings --- i18n/en.pot | 24 +++++++++---------- .../ReadOnlyBadge/ReadOnlyBadge.tsx | 8 +++---- .../WidgetProfile/WidgetProfile.component.tsx | 2 +- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 86e3e66a6e..beeca74ad7 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: 2026-05-18T18:28:17.335Z\n" -"PO-Revision-Date: 2026-05-18T18:28:17.335Z\n" +"POT-Creation-Date: 2026-05-24T11:43:30.545Z\n" +"PO-Revision-Date: 2026-05-24T11:43:30.545Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -1071,14 +1071,14 @@ msgstr "Possible duplicates found" msgid "An error occurred loading possible duplicates" msgstr "An error occurred loading possible duplicates" -msgid "You only have view access to enrollment" -msgstr "You only have view access to enrollment" +msgid "You only have view access to this enrollment" +msgstr "You only have view access to this enrollment" -msgid "You only have view access to program" -msgstr "You only have view access to program" +msgid "You only have view access to this program" +msgstr "You only have view access to this program" -msgid "You only have view access to {{trackedEntityName}}" -msgstr "You only have view access to {{trackedEntityName}}" +msgid "You only have view access to this {{trackedEntityName}}" +msgstr "You only have view access to this {{trackedEntityName}}" msgid "You only have view access to this tracked entity type" msgstr "You only have view access to this tracked entity type" @@ -1089,8 +1089,8 @@ msgstr "You only have view access to these program stages" msgid "You only have view access to this program stage" msgstr "You only have view access to this program stage" -msgid "This event is outside the valid period" -msgstr "This event is outside the valid period" +msgid "This event is outside the valid editing period" +msgstr "This event is outside the valid editing period" msgid "This event has been completed" msgstr "This event has been completed" @@ -1668,8 +1668,8 @@ msgstr "There was a problem deleting the {{trackedEntityTypeName}}" msgid "Yes, delete {{trackedEntityTypeName}}" msgstr "Yes, delete {{trackedEntityTypeName}}" -msgid "Show profile" -msgstr "Show profile" +msgid "View profile" +msgstr "View profile" msgid "Profile widget could not be loaded. Please try again later" msgstr "Profile widget could not be loaded. Please try again later" diff --git a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx index 4a8dfc5818..bad84d8dfc 100644 --- a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx +++ b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx @@ -27,19 +27,19 @@ type Access = { programStage: boolean; }; -const getEnrollmentMessage = (): string => i18n.t('You only have view access to enrollment'); +const getEnrollmentMessage = (): string => i18n.t('You only have view access to this enrollment'); -const getProgramMessage = (): string => i18n.t('You only have view access to program'); +const getProgramMessage = (): string => i18n.t('You only have view access to this program'); const getTrackedEntityMessage = (trackedEntityName: string | undefined): string => (trackedEntityName - ? i18n.t('You only have view access to {{trackedEntityName}}', { trackedEntityName, escapeValue: false }) + ? i18n.t('You only have view access to this {{trackedEntityName}}', { trackedEntityName, escapeValue: false }) : i18n.t('You only have view access to this tracked entity type')); const getProgramStageMessage = (multipleStages: boolean): string => (multipleStages ? i18n.t('You only have view access to these program stages') : i18n.t('You only have view access to this program stage')); -const getExpiredPeriodMessage = (): string => i18n.t('This event is outside the valid period'); +const getExpiredPeriodMessage = (): string => i18n.t('This event is outside the valid editing period'); const getCompletedEventMessage = (): string => i18n.t('This event has been completed'); diff --git a/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx b/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx index 90cfd1743a..442f0be48c 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx +++ b/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx @@ -114,7 +114,7 @@ const WidgetProfilePlain = ({ const profileButtonLabel = useMemo(() => { if (readOnlyMode || hasNoAttributes) return null; - if (!isEditable) return i18n.t('Show profile'); + if (!isEditable) return i18n.t('View profile'); if (isEditable) return i18n.t('Edit'); return null; }, [isEditable, readOnlyMode, hasNoAttributes]); From e46946342605a11c3c1c96da63e7fa911b5869ca Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Mon, 1 Jun 2026 12:33:56 +0200 Subject: [PATCH 60/60] chore: [DHIS2-21583] Restore self-contained user roles fetching in WidgetProfile (#4593) --- .../useSearchScopeWithFallback.ts | 41 +++++++++++++------ .../WidgetEnrollment.container.tsx | 12 +++--- .../WidgetProfile/WidgetProfile.component.tsx | 18 +++++--- .../components/WidgetProfile/hooks/index.ts | 1 + .../WidgetProfile/hooks/useUserRoles.ts | 36 ++++++++++++++++ .../utils/localeData/useUserLocale.ts | 29 +++++++++++++ 6 files changed, 112 insertions(+), 25 deletions(-) create mode 100644 src/core_modules/capture-core/components/WidgetProfile/hooks/useUserRoles.ts create mode 100644 src/core_modules/capture-core/utils/localeData/useUserLocale.ts diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/TransferModal/OrgUnitField/useSearchScopeWithFallback.ts b/src/core_modules/capture-core/components/WidgetEnrollment/TransferModal/OrgUnitField/useSearchScopeWithFallback.ts index 679946ab8c..ee70ea1d9f 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/TransferModal/OrgUnitField/useSearchScopeWithFallback.ts +++ b/src/core_modules/capture-core/components/WidgetEnrollment/TransferModal/OrgUnitField/useSearchScopeWithFallback.ts @@ -1,18 +1,36 @@ -import { useMemo } from 'react'; import { useApiMetadataQuery } from '../../../../utils/reactQueryHelpers'; -import { CurrentUser } from '../../../../utils/userInfo/CurrentUser'; type Props = { searchText?: string; }; +type OrgUnit = { id: string; path: string }; +type MeOrgUnitScope = { + teiSearchOrganisationUnits: Array; + organisationUnits: Array; +}; +type OrgUnitsResponse = { organisationUnits: Array }; + export const useSearchScopeWithFallback = ({ searchText }: Props) => { - const orgUnitRootsFromUser = useMemo(() => { - const { teiSearchOrganisationUnits, organisationUnits } = CurrentUser.get(); - return teiSearchOrganisationUnits.length ? teiSearchOrganisationUnits : organisationUnits; - }, []); + const { data: orgUnitRoots, isInitialLoading } = useApiMetadataQuery>( + ['organisationUnits', 'userOrgUnitScope'], + { + resource: 'me', + params: { + fields: 'teiSearchOrganisationUnits[id,path],organisationUnits[id,path]', + }, + }, + { + enabled: !searchText, + select: ({ teiSearchOrganisationUnits, organisationUnits }) => + (teiSearchOrganisationUnits.length ? teiSearchOrganisationUnits : organisationUnits), + }, + ); - const { data: searchOrgUnits, isInitialLoading: isInitialLoadingSearch } = useApiMetadataQuery( + const { + data: searchOrgUnits, + isInitialLoading: isInitialLoadingSearch, + } = useApiMetadataQuery>( ['organisationUnits', 'userOrgUnitScope', 'search', searchText], { resource: 'organisationUnits', @@ -27,15 +45,12 @@ export const useSearchScopeWithFallback = ({ searchText }: Props) => { { enabled: Boolean(searchText), cacheTime: 120 * 60 * 1000, - select: (data) => { - const { organisationUnits } = data as any; - return organisationUnits; - }, + select: ({ organisationUnits }) => organisationUnits, }, ); return { - orgUnitRoots: searchText?.length ? searchOrgUnits : orgUnitRootsFromUser, - isLoading: searchText?.length ? isInitialLoadingSearch : false, + orgUnitRoots: searchText?.length ? searchOrgUnits : orgUnitRoots, + isLoading: searchText?.length ? isInitialLoadingSearch : isInitialLoading, }; }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.container.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.container.tsx index b12329ae40..0dc21430ee 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.container.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.container.tsx @@ -6,14 +6,14 @@ import { useOrgUnitNameWithAncestors } from '../../metadataRetrieval/orgUnitName import { useTrackedEntities } from './hooks/useTrackedEntities'; import { useEnrollment } from './hooks/useEnrollment'; import { useProgram } from './hooks/useProgram'; -import { CurrentUser } from '../../utils/userInfo/CurrentUser'; +import { useUserLocale } from '../../utils/localeData/useUserLocale'; import type { Props } from './enrollment.types'; import { plainStatus } from './constants/status.const'; -const useError = (errorEnrollment: any, errorProgram: any, errorOwnerOrgUnit: any, errorOrgUnit: any) => +const useError = (errorEnrollment: any, errorProgram: any, errorOwnerOrgUnit: any, errorOrgUnit: any, errorLocale: any) => useMemo( - () => errorEnrollment ?? errorProgram ?? errorOwnerOrgUnit ?? errorOrgUnit, - [errorEnrollment, errorProgram, errorOwnerOrgUnit, errorOrgUnit], + () => errorEnrollment ?? errorProgram ?? errorOwnerOrgUnit ?? errorOrgUnit ?? errorLocale, + [errorEnrollment, errorProgram, errorOwnerOrgUnit, errorOrgUnit, errorLocale], ); const useContainsAutoGeneratedEvent = (program: any) => @@ -73,10 +73,10 @@ export const WidgetEnrollment = ({ const { error: errorOrgUnit, displayName } = useOrgUnitNameWithAncestors( typeof ownerOrgUnit === 'string' ? ownerOrgUnit : undefined, ); - const locale = CurrentUser.get().uiLocale; + const { error: errorLocale, locale } = useUserLocale(); const canAddNew = useCanAddNew(enrollments, programId, program?.trackedEntityType.access); const containsAutoGeneratedEvent = useContainsAutoGeneratedEvent(program); - const error = useError(errorEnrollment, errorProgram, errorOwnerOrgUnit, errorOrgUnit); + const error = useError(errorEnrollment, errorProgram, errorOwnerOrgUnit, errorOrgUnit, errorLocale); const events = useEnrollmentEvents(externalData); if (error) { diff --git a/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx b/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx index 70a3c6c444..e4b5cc05df 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx +++ b/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx @@ -15,9 +15,9 @@ import { useProgram, useTrackedEntityInstances, useClientAttributesWithSubvalues, + useUserRoles, useTeiDisplayName, } from './hooks'; -import { CurrentUser } from '../../utils/userInfo/CurrentUser'; import { DataEntry, dataEntryActionTypes, TEI_MODAL_STATE, convertClientToView } from './DataEntry'; import { ReactQueryAppNamespace } from '../../utils/reactQueryHelpers'; import { CHANGELOG_ENTITY_TYPES } from '../WidgetsChangelog'; @@ -57,13 +57,15 @@ const showEditModal = (loading: boolean, error: any, showEdit: boolean, modalSta const computeLoadingState = ( programsLoading: boolean, trackedEntityInstancesLoading: boolean, + userRolesLoading: boolean, configIsFetched: boolean, -) => programsLoading || trackedEntityInstancesLoading || !configIsFetched; +) => programsLoading || trackedEntityInstancesLoading || userRolesLoading || !configIsFetched; const computeError = ( programsError: any, trackedEntityInstancesError: any, -) => programsError || trackedEntityInstancesError; + userRolesError: any, +) => programsError || trackedEntityInstancesError || userRolesError; const WidgetProfilePlain = ({ teiId, @@ -96,8 +98,12 @@ const WidgetProfilePlain = ({ programWriteAccess, trackedEntityTypeWriteAccess, } = useEnrollmentAccessContext(); + const { + loading: userRolesLoading, + error: userRolesError, + userRoles, + } = useUserRoles(); const trackedEntityTypeName = program?.trackedEntityType?.displayName; - const userRoles = CurrentUser.get().userRoles; const hasNoAttributes = !program?.programTrackedEntityAttributes?.length; @@ -113,8 +119,8 @@ const WidgetProfilePlain = ({ return null; }, [isEditable, readOnlyMode, hasNoAttributes]); - const loading = computeLoadingState(programsLoading, trackedEntityInstancesLoading, configIsFetched); - const error = computeError(programsError, trackedEntityInstancesError); + const loading = computeLoadingState(programsLoading, trackedEntityInstancesLoading, userRolesLoading, configIsFetched); + const error = computeError(programsError, trackedEntityInstancesError, userRolesError); const clientAttributesWithSubvalues = useClientAttributesWithSubvalues( teiId, program as any, diff --git a/src/core_modules/capture-core/components/WidgetProfile/hooks/index.ts b/src/core_modules/capture-core/components/WidgetProfile/hooks/index.ts index d5fc613397..9796657555 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/hooks/index.ts +++ b/src/core_modules/capture-core/components/WidgetProfile/hooks/index.ts @@ -1,4 +1,5 @@ export { useProgram } from './useProgram'; export { useTrackedEntityInstances } from './useTrackedEntityInstances'; export { useClientAttributesWithSubvalues } from './useClientAttributesWithSubvalues'; +export { useUserRoles } from './useUserRoles'; export { useTeiDisplayName } from './useTeiDisplayName'; diff --git a/src/core_modules/capture-core/components/WidgetProfile/hooks/useUserRoles.ts b/src/core_modules/capture-core/components/WidgetProfile/hooks/useUserRoles.ts new file mode 100644 index 0000000000..4a5e9d10f3 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetProfile/hooks/useUserRoles.ts @@ -0,0 +1,36 @@ +import { useMemo } from 'react'; +import { useDataQuery } from '@dhis2/app-runtime'; + +const fields = 'userCredentials[userRoles]'; + +export const useUserRoles = () => { + const { error, loading, data } = useDataQuery( + useMemo( + () => ({ + userData: { + resource: 'me.json', + params: { + fields, + }, + }, + }), + [], + ), + ); + + const userRoles = useMemo( + () => { + if (!loading && data?.userData) { + const userData = data.userData as { + userCredentials?: { + userRoles?: Array<{ id: string }> + } + }; + return userData.userCredentials?.userRoles?.map(({ id }) => id) ?? []; + } + return []; + }, + [loading, data], + ); + return { error, loading, userRoles }; +}; diff --git a/src/core_modules/capture-core/utils/localeData/useUserLocale.ts b/src/core_modules/capture-core/utils/localeData/useUserLocale.ts new file mode 100644 index 0000000000..902fcc507a --- /dev/null +++ b/src/core_modules/capture-core/utils/localeData/useUserLocale.ts @@ -0,0 +1,29 @@ +import { useDataEngine } from '@dhis2/app-runtime'; +import { useQuery } from '@tanstack/react-query'; + +export const useUserLocale = (): { + locale: any; + isLoading: boolean; + isError: boolean; + error: unknown; +} => { + const dataEngine = useDataEngine(); + + const { data, isInitialLoading, isError, error } = useQuery( + ['userLocale'], + () => dataEngine.query({ + userSettings: { + resource: 'me', + params: { + fields: 'settings[keyUiLocale]', + }, + }, + })); + + return { + locale: (data as any)?.userSettings?.settings?.keyUiLocale, + isLoading: isInitialLoading, + isError, + error, + }; +};