From 6af5f6932d7ab766a5ce6e0464728e68c2f9e4a3 Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 30 Apr 2024 09:30:23 +0200 Subject: [PATCH] CM-879: add undo delete individual (#71) Co-authored-by: Jan --- src/actions.js | 16 ++++++ src/components/GroupIndividualFilter.js | 11 +--- src/components/GroupIndividualSearcher.js | 30 +++++++---- src/components/IndividualFilter.js | 40 ++++++++++++--- src/components/IndividualSearcher.js | 50 +++++++++++++++++-- ...idualUpdateTasks.js => IndividualTasks.js} | 6 +-- src/index.js | 12 ++--- src/pages/IndividualPage.js | 37 +++++++++++++- src/reducer.js | 3 ++ src/translations/en.json | 13 +++-- 10 files changed, 172 insertions(+), 46 deletions(-) rename src/components/tasks/{IndividualUpdateTasks.js => IndividualTasks.js} (69%) diff --git a/src/actions.js b/src/actions.js index 09b874a..10eb12a 100644 --- a/src/actions.js +++ b/src/actions.js @@ -163,6 +163,22 @@ export function deleteIndividual(individual, clientMutationLabel) { ); } +export function undoDeleteIndividual(individual, clientMutationLabel) { + const individualUuids = `ids: ["${individual?.id}"]`; + const mutation = formatMutation('undoDeleteIndividual', individualUuids, clientMutationLabel); + const requestedDateTime = new Date(); + return graphql( + mutation.payload, + [REQUEST(ACTION_TYPE.MUTATION), SUCCESS(ACTION_TYPE.UNDO_DELETE_INDIVIDUAL), ERROR(ACTION_TYPE.MUTATION)], + { + actionType: ACTION_TYPE.UNDO_DELETE_INDIVIDUAL, + clientMutationId: mutation.clientMutationId, + clientMutationLabel, + requestedDateTime, + }, + ); +} + export function deleteGroupIndividual(groupIndividual, clientMutationLabel) { const groupIndividualUuids = `ids: ["${groupIndividual?.id}"]`; const mutation = formatMutation('removeIndividualFromGroup', groupIndividualUuids, clientMutationLabel); diff --git a/src/components/GroupIndividualFilter.js b/src/components/GroupIndividualFilter.js index 02362e3..4488909 100644 --- a/src/components/GroupIndividualFilter.js +++ b/src/components/GroupIndividualFilter.js @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { injectIntl } from 'react-intl'; import { TextInput, PublishedComponent, formatMessage } from '@openimis/fe-core'; import { Grid } from '@material-ui/core'; @@ -9,7 +9,7 @@ import { defaultFilterStyles } from '../util/styles'; import GroupIndividualRolePicker from '../pickers/GroupIndividualRolePicker'; function GroupIndividualFilter({ - intl, classes, filters, onChangeFilters, groupId, + intl, classes, filters, onChangeFilters, }) { const debouncedOnChangeFilters = _debounce(onChangeFilters, DEFAULT_DEBOUNCE_TIME); @@ -37,13 +37,6 @@ function GroupIndividualFilter({ } }; - const handleGroupId = onChangeStringFilter('group_Id'); - useEffect(() => { - if (filters?.group_Id?.value !== groupId) { - handleGroupId(groupId); - } - }, [groupId]); - return ( diff --git a/src/components/GroupIndividualSearcher.js b/src/components/GroupIndividualSearcher.js index 8b86d9e..fd3f0ee 100644 --- a/src/components/GroupIndividualSearcher.js +++ b/src/components/GroupIndividualSearcher.js @@ -288,16 +288,25 @@ function GroupIndividualSearcher({ return setFailedExport(false); }, [groupIndividualExport]); - const defaultFilters = () => ({ - isDeleted: { - value: false, - filter: 'isDeleted: false', - }, - group_Id: { - value: groupId, - filter: `group_Id: "${groupId}"`, - }, - }); + const defaultFilters = () => { + const filters = { + isDeleted: { + value: false, + filter: 'isDeleted: false', + }, + individual_IsDeleted: { + value: false, + filter: 'individual_IsDeleted: false', + }, + }; + if (groupId) { + filters.group_Id = { + value: groupId, + filter: `group_Id: "${groupId}"`, + }; + } + return filters; + }; const groupBeneficiaryFilter = (props) => ( {failedExport && ( diff --git a/src/components/IndividualFilter.js b/src/components/IndividualFilter.js index a784a0c..13a64b7 100644 --- a/src/components/IndividualFilter.js +++ b/src/components/IndividualFilter.js @@ -1,18 +1,20 @@ import React from 'react'; import { injectIntl } from 'react-intl'; -import { TextInput, PublishedComponent } from '@openimis/fe-core'; -import { Grid } from '@material-ui/core'; +import { TextInput, PublishedComponent, formatMessage } from '@openimis/fe-core'; +import { Grid, FormControlLabel, Checkbox } from '@material-ui/core'; import { withTheme, withStyles } from '@material-ui/core/styles'; import _debounce from 'lodash/debounce'; -import { CONTAINS_LOOKUP, DEFAULT_DEBOUNCE_TIME, EMPTY_STRING } from '../constants'; +import { + CONTAINS_LOOKUP, DEFAULT_DEBOUNCE_TIME, EMPTY_STRING, INDIVIDUAL_MODULE_NAME, +} from '../constants'; import { defaultFilterStyles } from '../util/styles'; function IndividualFilter({ - classes, filters, onChangeFilters, + intl, classes, filters, onChangeFilters, }) { const debouncedOnChangeFilters = _debounce(onChangeFilters, DEFAULT_DEBOUNCE_TIME); - const filterValue = (filterName) => filters?.[filterName]?.value; + const filterValue = (k) => (!!filters && !!filters[k] ? filters[k].value : null); const filterTextFieldValue = (filterName) => filters?.[filterName]?.value ?? EMPTY_STRING; @@ -36,11 +38,21 @@ function IndividualFilter({ } }; + const onChangeFilter = (k, v) => { + onChangeFilters([ + { + id: k, + value: v, + filter: `${k}: ${v}`, + }, + ]); + }; + return ( onChangeFilters([ @@ -69,6 +81,18 @@ function IndividualFilter({ ])} /> + + onChangeFilter('isDeleted', event.target.checked)} + name="isDeleted" + /> + )} + label={formatMessage(intl, INDIVIDUAL_MODULE_NAME, 'isDeleted')} + /> + ); } diff --git a/src/components/IndividualSearcher.js b/src/components/IndividualSearcher.js index ff92bcd..791f8b2 100644 --- a/src/components/IndividualSearcher.js +++ b/src/components/IndividualSearcher.js @@ -26,8 +26,9 @@ import { } from '@material-ui/core'; import EditIcon from '@material-ui/icons/Edit'; import DeleteIcon from '@material-ui/icons/Delete'; +import UndoIcon from '@material-ui/icons/Undo'; import { - fetchIndividuals, deleteIndividual, downloadIndividuals, clearIndividualExport, + fetchIndividuals, deleteIndividual, downloadIndividuals, clearIndividualExport, undoDeleteIndividual, } from '../actions'; import { DEFAULT_PAGE_SIZE, @@ -74,12 +75,15 @@ function IndividualSearcher({ isModalEnrollment, advancedCriteria, benefitPlanToEnroll, + undoDeleteIndividual, }) { const dispatch = useDispatch(); const [individualToDelete, setIndividualToDelete] = useState(null); + const [individualToUndo, setIndividualToUndo] = useState(null); const [appliedCustomFilters, setAppliedCustomFilters] = useState([CLEARED_STATE_FILTER]); const [appliedFiltersRowStructure, setAppliedFiltersRowStructure] = useState([CLEARED_STATE_FILTER]); const [deletedIndividualUuids, setDeletedIndividualUuids] = useState([]); + const [undoIndividualUuids, setUndoIndividualUuids] = useState([]); const [exportFields, setExportFields] = useState([ 'id', 'first_name', @@ -123,13 +127,23 @@ function IndividualSearcher({ formatMessage(intl, 'individual', 'individual.delete.confirm.message'), ); + const openUndoIndividualConfirmDialog = () => coreConfirm( + formatMessageWithValues(intl, 'individual', 'individual.undo.confirm.title', { + firstName: individualToUndo.firstName, + lastName: individualToUndo.lastName, + }), + formatMessage(intl, 'individual', 'individual.undo.confirm.message'), + ); + const onDoubleClick = (individual, newTab = false) => rights.includes(RIGHT_INDIVIDUAL_UPDATE) && !deletedIndividualUuids.includes(individual.id) && historyPush(modulesManager, history, 'individual.route.individual', [individual?.id], newTab); const onDelete = (individual) => setIndividualToDelete(individual); + const onUndo = (individual) => setIndividualToUndo(individual); useEffect(() => individualToDelete && openDeleteIndividualConfirmDialog(), [individualToDelete]); + useEffect(() => individualToUndo && openUndoIndividualConfirmDialog(), [individualToUndo]); useEffect(() => { if (individualToDelete && confirmed) { @@ -141,9 +155,21 @@ function IndividualSearcher({ ); setDeletedIndividualUuids([...deletedIndividualUuids, individualToDelete.id]); } + if (individualToUndo && confirmed) { + undoDeleteIndividual( + individualToUndo, + formatMessageWithValues(intl, 'individual', 'individual.undo.mutationLabel', { + id: individualToUndo?.id, + }), + ); + setUndoIndividualUuids([...undoIndividualUuids, individualToUndo.id]); + } if (individualToDelete && confirmed !== null) { setIndividualToDelete(null); } + if (individualToUndo && confirmed !== null) { + setIndividualToUndo(null); + } return () => confirmed && clearConfirm(false); }, [confirmed]); @@ -168,6 +194,9 @@ function IndividualSearcher({ if (rights.includes(RIGHT_INDIVIDUAL_UPDATE)) { headers.push('emptyLabel'); } + if (rights.includes(RIGHT_INDIVIDUAL_DELETE)) { + headers.push('emptyLabel'); + } return headers; }; @@ -191,8 +220,8 @@ function IndividualSearcher({ )); } if (rights.includes(RIGHT_INDIVIDUAL_DELETE) && isModalEnrollment === false) { - formatters.push((individual) => ( - + formatters.push((individual) => (!individual?.isDeleted ? ( + onDelete(individual)} disabled={deletedIndividualUuids.includes(individual.id)} @@ -200,7 +229,16 @@ function IndividualSearcher({ - )); + ) : ( + + onUndo(individual)} + disabled={undoIndividualUuids.includes(individual.id)} + > + + + + ))); } return formatters; }; @@ -213,7 +251,8 @@ function IndividualSearcher({ ['dob', true], ]; - const isRowDisabled = (_, individual) => deletedIndividualUuids.includes(individual.id); + const isRowDisabled = (_, individual) => deletedIndividualUuids.includes(individual.id) + || undoIndividualUuids.includes(individual.id); const [failedExport, setFailedExport] = useState(false); @@ -355,6 +394,7 @@ const mapDispatchToProps = (dispatch) => bindActionCreators( deleteIndividual, downloadIndividuals, clearIndividualExport, + undoDeleteIndividual, coreConfirm, clearConfirm, journalize, diff --git a/src/components/tasks/IndividualUpdateTasks.js b/src/components/tasks/IndividualTasks.js similarity index 69% rename from src/components/tasks/IndividualUpdateTasks.js rename to src/components/tasks/IndividualTasks.js index 70bdc81..9c94bc8 100644 --- a/src/components/tasks/IndividualUpdateTasks.js +++ b/src/components/tasks/IndividualTasks.js @@ -1,16 +1,16 @@ import React from 'react'; import { FormattedMessage } from '@openimis/fe-core'; -const IndividualUpdateTaskTableHeaders = () => [ +const IndividualTaskTableHeaders = () => [ , , , ]; -const IndividualUpdateTaskItemFormatters = () => [ +const IndividualTaskItemFormatters = () => [ (individual) => individual?.first_name, (individual) => individual?.last_name, (individual) => individual?.dob, ]; -export { IndividualUpdateTaskTableHeaders, IndividualUpdateTaskItemFormatters }; +export { IndividualTaskTableHeaders, IndividualTaskItemFormatters }; diff --git a/src/index.js b/src/index.js index 44c4aee..ba9a610 100644 --- a/src/index.js +++ b/src/index.js @@ -26,9 +26,9 @@ import GroupIndividualSearcher from './components/GroupIndividualSearcher'; import { clearIndividualExport, downloadIndividuals, fetchIndividuals } from './actions'; import IndividualHistorySearcher from './components/IndividualHistorySearcher'; import { - IndividualUpdateTaskItemFormatters, - IndividualUpdateTaskTableHeaders, -} from './components/tasks/IndividualUpdateTasks'; + IndividualTaskItemFormatters, + IndividualTaskTableHeaders, +} from './components/tasks/IndividualTasks'; import GroupHistorySearcher from './components/GroupHistorySearcher'; import { GroupChangelogTabLabel, GroupChangelogTabPanel } from './components/GroupChangelogTab'; import { GroupTaskTabLabel, GroupTaskTabPanel } from './components/GroupTaskTab'; @@ -135,9 +135,9 @@ const DEFAULT_CONFIG = { 'individual.BenefitPlansListTabLabel': [BENEFIT_PLAN_TABS_LABEL_REF_KEY], 'individual.BenefitPlansListTabPanel': [BENEFIT_PLAN_TABS_PANEL_REF_KEY], 'tasksManagement.tasks': [{ - text: , - tableHeaders: IndividualUpdateTaskTableHeaders, - itemFormatters: IndividualUpdateTaskItemFormatters, + text: , + tableHeaders: IndividualTaskTableHeaders, + itemFormatters: IndividualTaskItemFormatters, taskSource: ['IndividualService'], taskCode: INDIVIDUAL_LABEL, }, diff --git a/src/pages/IndividualPage.js b/src/pages/IndividualPage.js index 7880a7a..1a19d0b 100644 --- a/src/pages/IndividualPage.js +++ b/src/pages/IndividualPage.js @@ -15,8 +15,11 @@ import { connect } from 'react-redux'; import _ from 'lodash'; import { withTheme, withStyles } from '@material-ui/core/styles'; import DeleteIcon from '@material-ui/icons/Delete'; +import UndoIcon from '@material-ui/icons/Undo'; import { RIGHT_INDIVIDUAL_UPDATE } from '../constants'; -import { fetchIndividual, deleteIndividual, updateIndividual } from '../actions'; +import { + fetchIndividual, deleteIndividual, updateIndividual, undoDeleteIndividual, +} from '../actions'; import IndividualHeadPanel from '../components/IndividualHeadPanel'; import IndividualTabPanel from '../components/IndividualTabPanel'; import { ACTION_TYPE } from '../reducer'; @@ -41,6 +44,7 @@ function IndividualPage({ submittingMutation, mutation, journalize, + undoDeleteIndividual, }) { const [editedIndividual, setEditedIndividual] = useState({}); const [confirmedAction, setConfirmedAction] = useState(() => null); @@ -65,6 +69,9 @@ function IndividualPage({ if (mutation?.actionType === ACTION_TYPE.DELETE_INDIVIDUAL) { back(); } + if (mutation?.actionType === ACTION_TYPE.UNDO_DELETE_INDIVIDUAL) { + window.location.reload(); + } } }, [submittingMutation]); @@ -118,6 +125,13 @@ function IndividualPage({ }), ); + const undoDeleteIndividualCallback = () => undoDeleteIndividual( + individual, + formatMessageWithValues(intl, 'individual', 'individual.undo.mutationLabel', { + id: individual?.id, + }), + ); + const openDeleteIndividualConfirmDialog = () => { setConfirmedAction(() => deleteIndividualCallback); coreConfirm( @@ -129,11 +143,29 @@ function IndividualPage({ ); }; + const openUndoIndividualConfirmDialog = () => { + setConfirmedAction(() => undoDeleteIndividualCallback); + coreConfirm( + formatMessageWithValues(intl, 'individual', 'individual.undo.confirm.title', { + firstName: individual?.firstName, + lastName: individual?.lastName, + }), + formatMessage(intl, 'individual', 'individual.undo.confirm.message'), + ); + }; + const actions = [ - !!individual && { + { doIt: openDeleteIndividualConfirmDialog, icon: , tooltip: formatMessage(intl, 'individual', 'deleteButtonTooltip'), + disabled: individual?.isDeleted, + }, + { + doIt: openUndoIndividualConfirmDialog, + icon: , + tooltip: formatMessage(intl, 'individual', 'undoButtonTooltip'), + disabled: !individual?.isDeleted, }, ]; @@ -181,6 +213,7 @@ const mapDispatchToProps = (dispatch) => bindActionCreators({ fetchIndividual, deleteIndividual, updateIndividual, + undoDeleteIndividual, coreConfirm, clearConfirm, journalize, diff --git a/src/reducer.js b/src/reducer.js index 291af1d..d92c30e 100644 --- a/src/reducer.js +++ b/src/reducer.js @@ -23,6 +23,7 @@ export const ACTION_TYPE = { GET_INDIVIDUAL: 'INDIVIDUAL_INDIVIDUAL', GET_GROUP: 'GROUP_GROUP', DELETE_INDIVIDUAL: 'INDIVIDUAL_DELETE_INDIVIDUAL', + UNDO_DELETE_INDIVIDUAL: 'INDIVIDUAL_UNDO_DELETE_INDIVIDUAL', DELETE_GROUP_INDIVIDUAL: 'GROUP_INDIVIDUAL_DELETE_GROUP_INDIVIDUAL', DELETE_GROUP: 'GROUP_DELETE_GROUP', UPDATE_INDIVIDUAL: 'INDIVIDUAL_UPDATE_INDIVIDUAL', @@ -578,6 +579,8 @@ function reducer( return dispatchMutationErr(state, action); case SUCCESS(ACTION_TYPE.DELETE_INDIVIDUAL): return dispatchMutationResp(state, 'deleteIndividual', action); + case SUCCESS(ACTION_TYPE.UNDO_DELETE_INDIVIDUAL): + return dispatchMutationResp(state, 'undoDeleteIndividual', action); case SUCCESS(ACTION_TYPE.UPDATE_INDIVIDUAL): return dispatchMutationResp(state, 'updateIndividual', action); case SUCCESS(ACTION_TYPE.DELETE_GROUP_INDIVIDUAL): diff --git a/src/translations/en.json b/src/translations/en.json index 989ab31..079be28 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -4,6 +4,7 @@ "any": "Any", "editButtonTooltip": "Edit", "deleteButtonTooltip": "Delete", + "undoButtonTooltip": "Undo Delete", "dialog": { "create": "Create", "update": "Save", @@ -19,6 +20,7 @@ "firstName": "First Name", "lastName": "Last Name", "dob": "Day of birth", + "isDeleted": "Show Deleted", "additonalFields": { "label": "Additional Fields", "showAdditionalFields": "Show Additional Fields", @@ -35,6 +37,13 @@ }, "mutationLabel": "Delete Individual {id}" }, + "undo": { + "confirm": { + "title": "Undo delete {firstName} {lastName}?", + "message": "Undoing deletion of individual." + }, + "mutationLabel": "Undo delete Individual {id}" + }, "update": { "label": "Update Individual", "mutationLabel":"Update Individual {id}" @@ -89,9 +98,7 @@ "label": "MEMBERS" }, "tasks": { - "update": { - "title": "Individual Update Tasks" - } + "title": "Individual Tasks" }, "any": "ANY", "ok": "ok",