-
Notifications
You must be signed in to change notification settings - Fork 42
feat: [DHIS2-21655] Uncomplete events from view mode #4649
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 45 commits
1c23af4
ee06506
82231d5
d43d0d7
9374438
6a87302
54e7180
dc68891
240810f
8f4186f
f0f539f
0e9aa8e
5f6ac26
4ed2084
2abdcd2
8ddf18a
43ea9b9
46085b6
2e7cd12
0f7443e
73f1e22
42e667d
206e43c
4c34ad1
ad09c52
dfe3c2e
1c7b39b
c1d4aad
1be294d
06daabf
a702df4
88bae47
ae2f15b
687ed3b
a9d409e
da15a0c
7d27954
25ff87e
573c6ff
98cbc60
3aa3a6c
0ebf4cc
e97fdd0
ce82994
8522012
6899e6f
c95920b
625d0ea
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| 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 { statusTypes as eventStatuses } from 'capture-core/events/statusTypes'; | ||
|
|
||
| type Props = { | ||
| eventId: string; | ||
| eventStatus?: string; | ||
| onMutate?: (newStatus: string) => void; | ||
| onSuccess?: (newStatus: string) => void; | ||
| onError?: () => void; | ||
| onClose: () => void; | ||
| }; | ||
|
|
||
| export const CompletionMenuItem = ({ | ||
| eventId, | ||
| eventStatus, | ||
| onMutate, | ||
| onSuccess, | ||
| 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 } = useMutation( | ||
| async () => { | ||
| const { event: apiEvent } = await dataEngine.query({ | ||
| event: { | ||
| resource: 'tracker/events', | ||
| id: eventId, | ||
| params: { | ||
| fields: 'event,status,program,programStage,orgUnit,occurredAt,scheduledAt,' + | ||
| 'enrollment,trackedEntity,attributeOptionCombo,notes,assignedUser,geometry,followUp', | ||
| }, | ||
| }, | ||
| }) as any; | ||
| return dataEngine.mutate({ | ||
| resource: 'tracker?async=false&importStrategy=UPDATE', | ||
| type: 'create', | ||
| data: { | ||
| events: [{ | ||
| ...apiEvent, | ||
| status: newStatus, | ||
| }], | ||
| }, | ||
| }); | ||
| }, | ||
| { | ||
| onMutate: () => { | ||
| onMutate?.(newStatus); | ||
| }, | ||
| onError: (error: unknown) => { | ||
| showError({ message: i18n.t('An error occurred when updating event status') }); | ||
|
henrikmv marked this conversation as resolved.
|
||
| log.error(errorCreator('An error occurred when updating event status')({ error, eventId, newStatus })); | ||
| onError?.(); | ||
| }, | ||
| onSuccess: () => { | ||
| onSuccess?.(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(); | ||
| updateCompletionStatus(); | ||
| }} | ||
| /> | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import React from 'react'; | ||
| import i18n from '@dhis2/d2-i18n'; | ||
| import { | ||
| colors, | ||
| IconDelete16, | ||
| MenuItem, | ||
| } from '@dhis2/ui'; | ||
|
|
||
| type Props = { | ||
| setActionsOpen: (open: boolean) => void; | ||
| setDeleteModalOpen: (open: boolean) => void; | ||
| }; | ||
|
|
||
| export const DeleteActionButton = ({ | ||
| setActionsOpen, | ||
| setDeleteModalOpen, | ||
| }: Props) => ( | ||
| <MenuItem | ||
| dense | ||
| icon={<IconDelete16 color={colors.red600} />} | ||
| label={i18n.t('Delete')} | ||
| dataTest="stages-and-events-delete" | ||
| onClick={() => { | ||
| setDeleteModalOpen(true); | ||
| setActionsOpen(false); | ||
| }} | ||
| suffix={null} | ||
| /> | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| export { DeleteActionButton } from './DeleteMenuItem'; | ||
| export { DeleteActionModal } from './DeleteEventModal'; | ||
| export { CompletionMenuItem } from './CompletionMenuItem'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { DeleteActionButton, DeleteActionModal, CompletionMenuItem } from './MenuItems'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,8 +24,9 @@ import { ReactQueryAppNamespace } from '../../../../utils/reactQueryHelpers'; | |
| import { CHANGELOG_ENTITY_TYPES } from '../../../WidgetsChangelog'; | ||
| import { useCategoryCombinations } from '../../../DataEntryDhis2Helpers/AOC/useCategoryCombinations'; | ||
| import { useMetadataForProgramStage } from '../../../DataEntries/common/ProgramStage/useMetadataForProgramStage'; | ||
| import { useProgramExpiryForUser } from '../../../../hooks'; | ||
| import { useAuthorities } from '../../../../utils/authority/useAuthorities'; | ||
| import { useProgramExpiryForUser, useEventEditPermissions } from '../../../../hooks'; | ||
| import { convertFormToClient } from '../../../../converters'; | ||
| import { dataElementTypes } from '../../../../metaData'; | ||
| import type { PlainProps } from './EventDetailsSection.types'; | ||
|
|
||
| const getStyles: any = () => ({ | ||
|
|
@@ -81,7 +82,14 @@ const EventDetailsSectionPlain = (props: PlainProps & { classes: any }) => { | |
| const [changeLogIsOpen, setChangeLogIsOpen] = useState(false); | ||
| const [actionsIsOpen, setActionsIsOpen] = useState(false); | ||
| const expiryPeriod = useProgramExpiryForUser(programId); | ||
| const { hasAuthority: canUncompleteEvent } = useAuthorities({ authorities: ['F_UNCOMPLETE_EVENT'] }); | ||
| const { isEventBlockedByCompletion, isEventBlockedByExpiry } = useEventEditPermissions({ | ||
| programId, | ||
| stage: programStage, | ||
| eventStatus: eventData?.eventContainer?.event?.status, | ||
| occurredAtClient: convertFormToClient(eventData?.dataEntryValues?.occurredAt, dataElementTypes.DATE) as string, | ||
| completedAtClient: eventData?.eventContainer?.event?.completedAt, | ||
| }); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔍 completedAtClient passed as raw server value in some callers
Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| const canUncompleteEvent = !isEventBlockedByCompletion && !isEventBlockedByExpiry; | ||
|
|
||
| const onSaveExternal = useCallback(() => { | ||
| const queryKey = [ReactQueryAppNamespace, 'changelog', CHANGELOG_ENTITY_TYPES.EVENT, eventId]; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔍 Single-event completion mutation omits dataValues from UPDATE payload
CompletionMenuItemfetches the event with a fixed field list that excludesdataValuesand then re-submits it viatracker?importStrategy=UPDATE. This mirrors the established bulk-complete pattern (useBulkCompleteEvents.ts, which was also narrowed in this PR to the same field set), so it is presumably safe with the server's merge behavior. However, if the tracker UPDATE strategy treats a missingdataValuesarray as an intent to clear them, toggling completion from view mode could drop event data values. Recommend confirming server behavior since this now runs for individual events triggered directly by users, not just the pre-existing bulk flow.Was this helpful? React with 👍 or 👎 to provide feedback.