Skip to content
Draft
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
1c23af4
feat: add uncomplete event functionality to view event
henrikmv Jul 20, 2026
ee06506
feat: add icon to changelog meny item
henrikmv Jul 20, 2026
82231d5
fix: complexity error
henrikmv Jul 20, 2026
d43d0d7
feat: implement event completion functionality
henrikmv Jul 20, 2026
9374438
fix: remove added changelog icon
henrikmv Jul 22, 2026
6a87302
feat: add hook to determine event completion status permissions
henrikmv Jul 22, 2026
54e7180
feat: optimize query client usage for event completion updates
henrikmv Jul 22, 2026
dc68891
fix: remove unused query client
henrikmv Jul 22, 2026
240810f
Merge branch 'master' into hv/feat/DHIS2-21655_uncomplete-event-view-…
henrikmv Jul 22, 2026
8f4186f
Merge remote-tracking branch 'origin/master' into hv/feat/DHIS2-21655…
henrikmv Jul 23, 2026
f0f539f
feat: add onMutate and onError callbacks to EventCompletionMenuItem
henrikmv Jul 24, 2026
0e9aa8e
feat: implement event status management in EventCompletionMenuItem
henrikmv Jul 27, 2026
5f6ac26
feat: allign pr with unify menu items pr
henrikmv Jul 28, 2026
4ed2084
feat: add onMutate and onError props to EventCompletionMenuItem
henrikmv Jul 28, 2026
2abdcd2
fix: remove atomicMode from resource query in EventCompletionMenuItem
henrikmv Jul 28, 2026
8ddf18a
feat: remove feature flag for changelog
henrikmv Jul 28, 2026
43ea9b9
fix: status needs refresh to update
henrikmv Jul 28, 2026
46085b6
feat: integrate updateField action for event completion status mutation
henrikmv Jul 28, 2026
2e7cd12
feat: refactor event status handling and update related actions
henrikmv Jul 28, 2026
0f7443e
feat: add event completion status mutation handling in EventRow compo…
henrikmv Jul 28, 2026
73f1e22
feat: replace useEventEditPermissions with useCanChangeCompletionStat…
henrikmv Jul 28, 2026
42e667d
feat: dispatch changeEventFromUrl action on completion status update
henrikmv Jul 28, 2026
206e43c
feat: rename onUpdated to onSuccess
henrikmv Jul 31, 2026
4c34ad1
fix: roll back and devin review
henrikmv Jul 31, 2026
ad09c52
Merge branch 'master' into hv/feat/DHIS2-21655_uncomplete-event-view-…
henrikmv Jul 31, 2026
dfe3c2e
fix: replace EventStatuses with eventStatuses constant in EventRow an…
henrikmv Jul 31, 2026
1c7b39b
feat: add index file and manu item folder
henrikmv Aug 4, 2026
c1d4aad
Merge remote-tracking branch 'origin/master' into hv/feat/DHIS2-21655…
henrikmv Aug 4, 2026
1be294d
Merge remote-tracking branch 'origin/master' into hv/feat/DHIS2-21655…
henrikmv Aug 4, 2026
06daabf
fix: clean up
henrikmv Aug 4, 2026
a702df4
feat: refactor event status handling to use centralised statusTypes
henrikmv Aug 4, 2026
88bae47
feat: centralise event status handling
henrikmv Aug 4, 2026
ae2f15b
feat: enhance event row loading state with circular loader
henrikmv Aug 4, 2026
687ed3b
Merge remote-tracking branch 'origin/master' into hv/feat/DHIS2-21655…
henrikmv Aug 12, 2026
a9d409e
feat: refactor event edit permissions
henrikmv Aug 13, 2026
da15a0c
feat: refactor event edit permissions and authority management
henrikmv Aug 14, 2026
7d27954
feat: consolidate authority imports across components and hooks
henrikmv Aug 14, 2026
25ff87e
feat: refactor event edit permissions to use isEventReadOnly flag
henrikmv Aug 14, 2026
573c6ff
feat: enhance event edit permissions and streamline delete action han…
henrikmv Aug 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions i18n/en.pot
Original file line number Diff line number Diff line change
Expand Up @@ -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-07-21T13:35:51.419Z\n"
"PO-Revision-Date: 2026-07-21T13:35:51.419Z\n"
"POT-Creation-Date: 2026-07-24T08:18:43.711Z\n"
"PO-Revision-Date: 2026-07-24T08:18:43.711Z\n"

msgid "The application could not be loaded."
msgstr "The application could not be loaded."
Expand Down Expand Up @@ -437,6 +437,15 @@ msgstr ""
"This event has unsaved changes. Leaving this page without saving will lose "
"these changes. Are you sure you want to discard unsaved changes?"

msgid "An error occurred when updating event status"
msgstr "An error occurred when updating event status"

msgid "Mark incomplete"
msgstr "Mark incomplete"

msgid "Mark complete"
msgstr "Mark complete"

msgid "No events to display"
msgstr "No events to display"

Expand Down Expand Up @@ -1347,9 +1356,6 @@ msgstr "Reactivate"
msgid "Mark as cancelled"
msgstr "Mark as cancelled"

msgid "Mark incomplete"
msgstr "Mark incomplete"

msgid "You do not have access to delete this enrollment"
msgstr "You do not have access to delete this enrollment"

Expand Down Expand Up @@ -1763,9 +1769,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 "An error occurred when updating event status"
msgstr "An error occurred when updating event status"

msgid "Unskip"
msgstr "Unskip"

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import React from 'react';
import i18n from '@dhis2/d2-i18n';
import log from 'loglevel';
import { MenuItem, IconCheckmark16, IconUndo16 } from '@dhis2/ui';
import { useMutation } from '@tanstack/react-query';
import { useAlert, useDataEngine } from '@dhis2/app-runtime';
import { errorCreator } from 'capture-core-utils';
import { eventStatuses } from '../WidgetEventEdit/constants/status.const';

type Props = {
eventId: string;
eventStatus?: string;
onMutate?: (newStatus: string) => void;
onUpdated: (newStatus: string) => void;
onError?: () => void;
onClose: () => void;
};

export const EventCompletionMenuItem = ({
eventId,
eventStatus,
onMutate,
onUpdated,
onError,
onClose,
}: Props) => {
const dataEngine = useDataEngine();
const { show: showError } = useAlert(
({ message }) => message,
{ critical: true },
);

const isCompleted = eventStatus === eventStatuses.COMPLETED;
const newStatus = isCompleted ? eventStatuses.ACTIVE : eventStatuses.COMPLETED;

const { mutate: updateCompletionStatus, isLoading } = useMutation(
async () => {
const { event: apiEvent } = await dataEngine.query({
event: {
resource: 'tracker/events',
id: eventId,
params: {
fields: '*,!completedAt,!completedBy,!dataValues,!relationships',
},
},
}) as any;
return dataEngine.mutate({
resource: 'tracker?async=false&importStrategy=UPDATE&atomicMode=OBJECT',
type: 'create',
data: {
events: [{
...apiEvent,
status: newStatus,
}],
},
});
},
{
onMutate: () => {
onMutate?.(newStatus);
},
onError: (error: unknown) => {
showError({ message: i18n.t('An error occurred when updating event status') });
log.error(errorCreator('An error occurred when updating event status')({ error, eventId, newStatus }));
onError?.();
},
onSuccess: () => {
onUpdated(newStatus);
},
},
);

return (
<MenuItem
dense
dataTest={isCompleted ? 'uncomplete-event-menu-item' : 'complete-event-menu-item'}
icon={isCompleted ? <IconUndo16 /> : <IconCheckmark16 />}
label={isCompleted ? i18n.t('Mark incomplete') : i18n.t('Mark complete')}
suffix={null}
onClick={() => {
onClose();
!isLoading && updateCompletionStatus();
}}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { EventCompletionMenuItem } from './EventCompletionMenuItem.component';
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState, useCallback } from 'react';
import { useSelector } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { dataEntryIds, dataEntryKeys } from 'capture-core/constants';
import { withStyles } from 'capture-core-utils/styles';
import { FEATURES, useFeature } from 'capture-core-utils';
Expand Down Expand Up @@ -27,6 +27,9 @@ import { useCategoryCombinations } from '../../../DataEntryDhis2Helpers/AOC/useC
import { useMetadataForProgramStage } from '../../../DataEntries/common/ProgramStage/useMetadataForProgramStage';
import { useProgramExpiryForUser } from '../../../../hooks';
import { useAuthorities } from '../../../../utils/authority/useAuthorities';
import { EventCompletionMenuItem } from '../../../EventCompletionMenuItem';
import { changeEventFromUrl } from '../ViewEventComponent/viewEvent.actions';
import { pageKeys } from '../../../App/withAppUrlSync';
import type { PlainProps } from './EventDetailsSection.types';

const getStyles: any = () => ({
Expand Down Expand Up @@ -72,9 +75,12 @@ const EventDetailsSectionPlain = (props: PlainProps & { classes: any }) => {
onBackToAllEvents,
programId,
showEditButton,
canChangeCompletionStatus,
...passOnProps
} = props;
const dispatch = useDispatch();
const orgUnitId = useSelector((state: any) => state.viewEventPage.loadedValues?.orgUnit?.id);
const eventStatus = useSelector((state: any) => state.viewEventPage.loadedValues?.eventContainer?.event?.status);
const { formFoundation } = useMetadataForProgramStage({ programId });
const { orgUnit, error } = useCoreOrgUnit(orgUnitId);
const { programCategory, isLoading } = useCategoryCombinations(programId);
Expand All @@ -84,10 +90,16 @@ const EventDetailsSectionPlain = (props: PlainProps & { classes: any }) => {
const [actionsIsOpen, setActionsIsOpen] = useState(false);
const expiryPeriod = useProgramExpiryForUser(programId);
const { hasAuthority: canUncompleteEvent } = useAuthorities({ authorities: ['F_UNCOMPLETE_EVENT'] });
const showCompletionAction = !isEditEventPage && canChangeCompletionStatus;

const onCompletionStatusUpdated = useCallback(() => {
dispatch(changeEventFromUrl(eventId, pageKeys.VIEW_EVENT));
}, [dispatch, eventId]);
Comment thread
henrikmv marked this conversation as resolved.
Outdated

const onSaveExternal = useCallback(() => {
const queryKey = [ReactQueryAppNamespace, 'changelog', CHANGELOG_ENTITY_TYPES.EVENT, eventId];
queryClient.removeQueries(queryKey);
queryClient.removeQueries({
queryKey: [ReactQueryAppNamespace, 'changelog', CHANGELOG_ENTITY_TYPES.EVENT, eventId],
});
onBackToAllEvents();
}, [eventId, queryClient, onBackToAllEvents]);

Expand Down Expand Up @@ -131,7 +143,7 @@ const EventDetailsSectionPlain = (props: PlainProps & { classes: any }) => {
{i18n.t('Edit event')}
</Button>
</div>}
{supportsChangelog && (
{(supportsChangelog || showCompletionAction) && (
<OverflowButton
open={actionsIsOpen}
onClick={() => setActionsIsOpen(prev => !prev)}
Expand All @@ -145,14 +157,24 @@ const EventDetailsSectionPlain = (props: PlainProps & { classes: any }) => {
maxWidth="250px"
dataTest="event-program-event-overflow-menu"
>
<MenuItem
label={i18n.t('View changelog')}
suffix={null}
onClick={() => {
setChangeLogIsOpen(true);
setActionsIsOpen(false);
}}
/>
{showCompletionAction && (
<EventCompletionMenuItem
eventId={eventId}
eventStatus={eventStatus}
onUpdated={onCompletionStatusUpdated}
onClose={() => setActionsIsOpen(false)}
/>
)}
{supportsChangelog && (
<MenuItem
label={i18n.t('View changelog')}
suffix={null}
Comment thread
henrikmv marked this conversation as resolved.
Outdated
onClick={() => {
setChangeLogIsOpen(true);
setActionsIsOpen(false);
}}
/>
)}
</FlyoutMenu>
)}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ export type PlainProps = {
programId: string;
onBackToAllEvents: () => void;
showEditButton: boolean;
canChangeCompletionStatus: boolean;
};
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export const ViewEventPlain = (props: Props & WithStyles<typeof getStyles>) => {
isEventWithinValidPeriod,
isWithinCompleteExpiry,
canEditCompletedEvent,
canChangeCompletionStatus,
readOnly,
} = useEventEditPermissions({
programId,
Expand Down Expand Up @@ -135,6 +136,7 @@ export const ViewEventPlain = (props: Props & WithStyles<typeof getStyles>) => {
onBackToViewEvent={onBackToViewEvent}
onBackToAllEvents={onBackToAllEvents}
showEditButton={showEditButton}
canChangeCompletionStatus={canChangeCompletionStatus}
/>
<RightColumnWrapper
eventAccess={eventAccess}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ const WidgetEventEditPlain = ({

const availableProgramStages = useAvailableProgramStages(stage, teiId, enrollmentId, programId);

const { readOnly, expiryPeriod, canUncompleteEvent } = useEventEditPermissions({
const { readOnly, expiryPeriod, canUncompleteEvent, canChangeCompletionStatus } = useEventEditPermissions({
programId,
stage,
eventStatus,
Expand All @@ -135,12 +135,14 @@ const WidgetEventEditPlain = ({
<Widget
header={
<WidgetHeader
eventId={eventId}
eventStatus={eventStatus}
stage={stage}
programId={programId}
orgUnit={orgUnit}
setChangeLogIsOpen={setChangeLogIsOpen}
readOnly={readOnly}
canChangeCompletionStatus={canChangeCompletionStatus}
/>
}
noncollapsible
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import { dataEntryKeys } from 'capture-core/constants';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { spacersNum, Button, IconEdit24, IconMore16, FlyoutMenu, MenuItem, spacers } from '@dhis2/ui';
import { withStyles, type WithStyles } from 'capture-core-utils/styles';
import i18n from '@dhis2/d2-i18n';
Expand All @@ -11,6 +11,12 @@ import { NonBundledDhis2Icon } from '../../NonBundledDhis2Icon';
import { useCategoryCombinations } from '../../DataEntryDhis2Helpers/AOC/useCategoryCombinations';
import { OverflowButton } from '../../Buttons';
import { inMemoryFileStore } from '../../DataEntry/file/inMemoryFileStore';
import {
updateEnrollmentEvent,
commitEnrollmentEvent,
rollbackEnrollmentEvent,
} from '../../Pages/common/EnrollmentOverviewDomain';
import { EventCompletionMenuItem } from '../../EventCompletionMenuItem';
import type { PlainProps } from './WidgetHeader.types';

const styles: Readonly<any> = {
Expand All @@ -30,13 +36,15 @@ const styles: Readonly<any> = {
type Props = PlainProps & WithStyles<typeof styles>;

const WidgetHeaderPlain = ({
eventId,
eventStatus,
stage,
programId,
orgUnit,
setChangeLogIsOpen,
classes,
readOnly,
canChangeCompletionStatus,
}: Props) => {
useEffect(() => inMemoryFileStore.clear, []);
const dispatch = useDispatch();
Expand All @@ -48,6 +56,24 @@ const WidgetHeaderPlain = ({
const showEditButton = !readOnly;
const { programCategory } = useCategoryCombinations(programId);

const storedEvent = useSelector((state: any) =>
state.enrollmentDomain?.enrollment?.events?.find((event: any) => event.event === eventId));

const onCompletionStatusMutate = useCallback((newStatus: string) => {
if (storedEvent) {
const { completedAt, completedBy, ...eventWithoutCompletion } = storedEvent;
dispatch(updateEnrollmentEvent(eventId, { ...eventWithoutCompletion, status: newStatus }));
Comment thread
henrikmv marked this conversation as resolved.
}
}, [dispatch, storedEvent, eventId]);

const onCompletionStatusUpdated = useCallback(() => {
dispatch(commitEnrollmentEvent(eventId));
}, [dispatch, eventId]);

const onCompletionStatusError = useCallback(() => {
dispatch(rollbackEnrollmentEvent(eventId));
}, [dispatch, eventId]);

const { icon, name } = stage;

return (
Expand Down Expand Up @@ -79,7 +105,7 @@ const WidgetHeaderPlain = ({
</Button>
)}

{supportsChangelog && (
{(supportsChangelog || canChangeCompletionStatus) && (
<OverflowButton
open={actionsIsOpen}
onClick={() => setActionsIsOpen(prev => !prev)}
Expand All @@ -93,14 +119,26 @@ const WidgetHeaderPlain = ({
maxWidth="250px"
dataTest={'tracker-program-event-overflow-menu'}
>
<MenuItem
label={i18n.t('View changelog')}
suffix=""
onClick={() => {
setChangeLogIsOpen(true);
setActionsIsOpen(false);
}}
/>
{canChangeCompletionStatus && (
<EventCompletionMenuItem
eventId={eventId}
eventStatus={eventStatus}
onMutate={onCompletionStatusMutate}
onUpdated={onCompletionStatusUpdated}
onError={onCompletionStatusError}
onClose={() => setActionsIsOpen(false)}
/>
)}
{supportsChangelog && (
<MenuItem
label={i18n.t('View changelog')}
suffix={null}
onClick={() => {
setChangeLogIsOpen(true);
setActionsIsOpen(false);
}}
/>
)}
</FlyoutMenu>
}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import type { OrgUnit } from '@dhis2/rules-engine-javascript';
import { ProgramStage } from '../../../metaData';

export type PlainProps = {
eventId: string,
eventStatus?: string,
stage: ProgramStage,
programId: string,
orgUnit: OrgUnit,
setChangeLogIsOpen: (toggle: boolean) => void,
readOnly: boolean,
canChangeCompletionStatus: boolean,
};
Loading
Loading