Skip to content

Commit

Permalink
feat: add delete enrollments in program
Browse files Browse the repository at this point in the history
  • Loading branch information
eirikhaugstulen committed Aug 26, 2024
1 parent 1344fb4 commit 40bf195
Show file tree
Hide file tree
Showing 13 changed files with 302 additions and 14 deletions.
25 changes: 20 additions & 5 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: 2024-08-10T10:42:21.141Z\n"
"PO-Revision-Date: 2024-08-10T10:42:21.141Z\n"
"POT-Creation-Date: 2024-08-26T17:34:22.205Z\n"
"PO-Revision-Date: 2024-08-26T17:34:22.205Z\n"

msgid "Choose one or more dates..."
msgstr "Choose one or more dates..."
Expand Down Expand Up @@ -1685,6 +1685,21 @@ msgstr "No active enrollments to complete"
msgid "An error occurred while completing the enrollments"
msgstr "An error occurred while completing the enrollments"

msgid "You do not have the required authority to delete enrollments"
msgstr "You do not have the required authority to delete enrollments"

msgid "Delete enrollments"
msgstr "Delete enrollments"

msgid "Are you sure you want to delete all enrollments in the selected program?"
msgstr "Are you sure you want to delete all enrollments in the selected program?"

msgid "Total enrollments to be deleted"
msgstr "Total enrollments to be deleted"

msgid "An error occurred when deleting enrollments"
msgstr "An error occurred when deleting enrollments"

msgid "You do not have the required authority to delete tracked entities"
msgstr "You do not have the required authority to delete tracked entities"

Expand Down Expand Up @@ -1720,12 +1735,12 @@ msgid_plural "{{count}} record selected"
msgstr[0] "{{count}} record selected"
msgstr[1] "{{count}} records selected"

msgid "Count"
msgstr "Count"

msgid "Total records to be completed"
msgstr "Total records to be completed"

msgid "Count"
msgstr "Count"

msgid "Update view"
msgstr "Update view"

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// @flow
import React, { useState } from 'react';
import i18n from '@dhis2/d2-i18n';
import {
Button,
} from '@dhis2/ui';
import { useAuthority } from '../../../../../../utils/userInfo/useAuthority';
import { ConditionalTooltip } from '../../../../../Tooltips/ConditionalTooltip';
import { EnrollmentDeleteModal } from './EnrollmentDeleteModal';

type Props = {
selectedRows: { [id: string]: boolean },
programId: string,
onUpdateList: () => void,
}

const CASCADE_DELETE_TEI_AUTHORITY = 'F_ENROLLMENT_CASCADE_DELETE';

export const DeleteEnrollmentsAction = ({
selectedRows,
programId,
onUpdateList,
}: Props) => {
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const { hasAuthority } = useAuthority({ authority: CASCADE_DELETE_TEI_AUTHORITY });

return (
<>
<ConditionalTooltip
enabled={!hasAuthority}
content={i18n.t('You do not have the required authority to delete enrollments')}
>
<Button
small
onClick={() => setIsDeleteDialogOpen(true)}
disabled={!hasAuthority}
>
{i18n.t('Delete enrollments')}
</Button>
</ConditionalTooltip>

{isDeleteDialogOpen && (
<EnrollmentDeleteModal
selectedRows={selectedRows}
programId={programId}
onUpdateList={onUpdateList}
setIsDeleteDialogOpen={setIsDeleteDialogOpen}
/>
)}
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// @flow
import React from 'react';
import {
Button,
ButtonStrip,
DataTableCell,
DataTableRow,
Modal,
ModalActions,
ModalContent,
ModalTitle,
} from '@dhis2/ui';
import i18n from '@dhis2/d2-i18n';
import { BulkActionCountTable } from '../../../../../WorkingListsBase/BulkActionBar';
import { useDeleteEnrollments } from '../hooks/useDeleteEnrollments';

type Props = {
selectedRows: { [id: string]: boolean },
programId: string,
onUpdateList: () => void,
setIsDeleteDialogOpen: (open: boolean) => void,
}

export const EnrollmentDeleteModal = ({
selectedRows,
programId,
onUpdateList,
setIsDeleteDialogOpen,
}: Props) => {
const {
deleteEnrollments,
isDeletingEnrollments,
enrollmentCounts,
isLoadingEnrollments,
} = useDeleteEnrollments({
selectedRows,
programId,
onUpdateList,
setIsDeleteDialogOpen,
});

return (
<Modal
small
onClose={() => setIsDeleteDialogOpen(false)}
>
<ModalTitle>
{i18n.t('Delete enrollments')}
</ModalTitle>

<ModalContent>
{i18n.t('Are you sure you want to delete all enrollments in the selected program?')}

<BulkActionCountTable
total={enrollmentCounts?.total}
isLoading={isLoadingEnrollments}
totalLabel={i18n.t('Total enrollments to be deleted')}
>
<DataTableRow>
<DataTableCell>
{i18n.t('Active')}
</DataTableCell>
<DataTableCell align={'center'}>
{enrollmentCounts?.active}
</DataTableCell>
</DataTableRow>

<DataTableRow>
<DataTableCell>
{i18n.t('Completed')}
</DataTableCell>
<DataTableCell align={'center'}>
{enrollmentCounts?.completed}
</DataTableCell>
</DataTableRow>
</BulkActionCountTable>
</ModalContent>

<ModalActions>
<ButtonStrip>
<Button
secondary
onClick={() => setIsDeleteDialogOpen(false)}
>
{i18n.t('Cancel')}
</Button>
<Button
destructive
loading={isDeletingEnrollments}
onClick={deleteEnrollments}
>
{i18n.t('Delete')}
</Button>
</ButtonStrip>
</ModalActions>
</Modal>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// @flow

export { EnrollmentDeleteModal } from './EnrollmentDeleteModal';
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// @flow
import { useMemo } from 'react';
import log from 'loglevel';
import i18n from '@dhis2/d2-i18n';
import { useMutation } from 'react-query';
import { useAlert, useDataEngine } from '@dhis2/app-runtime';
import { handleAPIResponse, REQUESTED_ENTITIES } from '../../../../../../../utils/api';
import { useApiDataQuery } from '../../../../../../../utils/reactQueryHelpers';
import { errorCreator } from '../../../../../../../../capture-core-utils';

type Props = {
selectedRows: { [id: string]: boolean },
programId: string,
onUpdateList: () => void,
setIsDeleteDialogOpen: (open: boolean) => void,
}

export const useDeleteEnrollments = ({
selectedRows,
programId,
onUpdateList,
setIsDeleteDialogOpen,
}: Props) => {
const dataEngine = useDataEngine();
const { show: showAlert } = useAlert(
({ message }) => message,
{ critical: true },
);

const { data: enrollments, isLoading: isLoadingEnrollments } = useApiDataQuery(
['WorkingLists', 'BulkActionBar', 'DeleteEnrollmentsAction', 'trackedEntities', selectedRows],
{
resource: 'tracker/trackedEntities',
params: {
fields: 'trackedEntity,enrollments[enrollment,program,status]',
trackedEntities: Object.keys(selectedRows).join(','),
program: programId,
},
},
{
enabled: Object.keys(selectedRows).length > 0,
select: (data: any) => {
const apiTrackedEntities = handleAPIResponse(REQUESTED_ENTITIES.trackedEntities, data);
if (!apiTrackedEntities) return [];

return apiTrackedEntities
.flatMap(apiTrackedEntity => apiTrackedEntity.enrollments)
// fallback in case the api returns an enrollment in another program
.filter(enrollment => enrollment.program === programId);
},
},
);

const { mutate: deleteEnrollments, isLoading: isDeletingEnrollments } = useMutation<any>(
() => dataEngine.mutate({
resource: 'tracker?async=false&importStrategy=DELETE',
type: 'create',
data: {
// $FlowFixMe - business logic dictates that enrollments is not undefined at this point
enrollments: enrollments.map(({ enrollment }) => ({ enrollment })),
},
}),
{
onError: (error) => {
log.error(errorCreator('An error occurred when deleting enrollments')({ error }));
showAlert({ message: i18n.t('An error occurred when deleting enrollments') });
},
onSuccess: () => {
onUpdateList();
setIsDeleteDialogOpen(false);
},
},
);

const enrollmentCounts = useMemo(() => {
if (!enrollments) {
return null;
}

const { activeEnrollments, completedEnrollments } = enrollments.reduce((acc, enrollment) => {
if (enrollment.status === 'ACTIVE') {
acc.activeEnrollments += 1;
} else {
acc.completedEnrollments += 1;
}

return acc;
}, { activeEnrollments: 0, completedEnrollments: 0 });

return {
active: activeEnrollments,
completed: completedEnrollments,
total: enrollments.length,
};
}, [enrollments]);

return {
deleteEnrollments,
isDeletingEnrollments,
isLoadingEnrollments,
enrollmentCounts,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// @flow

export { DeleteEnrollmentsAction } from './DeleteEnrollmentsAction';
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ type Props = {

const CASCADE_DELETE_TEI_AUTHORITY = 'F_TEI_CASCADE_DELETE';

export const DeleteAction = ({
export const DeleteTeiAction = ({
selectedRows,
selectedRowsCount,
onUpdateList,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// @flow

export { DeleteTeiAction } from './DeleteTeiAction';
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// @flow

export { CompleteAction } from './CompleteAction';
export { DeleteAction } from './DeleteAction';
export { DeleteTeiAction } from './DeleteTeiAction';
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// @flow
import React from 'react';
import { BulkActionBar } from '../../WorkingListsBase/BulkActionBar';
import { CompleteAction, DeleteAction } from './Actions';
import { CompleteAction, DeleteTeiAction } from './Actions';
import type { Props } from './TrackedEntityBulkActions.types';
import { DeleteEnrollmentsAction } from './Actions/DeleteEnrollmentsAction';

export const TrackedEntityBulkActionsComponent = ({
selectedRows,
Expand All @@ -27,7 +28,13 @@ export const TrackedEntityBulkActionsComponent = ({
onUpdateList={onUpdateList}
/>

<DeleteAction
<DeleteEnrollmentsAction
selectedRows={selectedRows}
programId={programId}
onUpdateList={onUpdateList}
/>

<DeleteTeiAction
selectedRows={selectedRows}
selectedRowsCount={selectedRowsCount}
onUpdateList={onUpdateList}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { withStyles } from '@material-ui/core/';
type Props = {
total: ?number,
isLoading: boolean,
totalLabel?: string,
children: React$Node,
classes: {
container: string,
Expand All @@ -33,7 +34,13 @@ const styles = {
},
};

export const EnrollmentTablePlain = ({ total, isLoading, children, classes }: Props) => (
export const EnrollmentTablePlain = ({
total,
isLoading,
totalLabel = i18n.t('Total records to be completed'),
children,
classes,
}: Props) => (
<div className={classes.container}>
<DataTable>
<DataTableHead>
Expand All @@ -52,7 +59,7 @@ export const EnrollmentTablePlain = ({ total, isLoading, children, classes }: Pr
<DataTableCell
style={{ background: colors.grey050, borderTop: `2px solid ${colors.grey300}` }}
>
{i18n.t('Total records to be completed')}
{totalLabel}
</DataTableCell>
<DataTableCell
style={{ background: colors.grey050, borderTop: `2px solid ${colors.grey300}` }}
Expand Down

0 comments on commit 40bf195

Please sign in to comment.