diff --git a/src/actions.js b/src/actions.js index b261a96..d1d4ec8 100644 --- a/src/actions.js +++ b/src/actions.js @@ -51,6 +51,24 @@ const TASK_PROJECTION = () => [ 'jsonExt', ]; +const TASK_HISTORY_FULL_PROJECTION = () => [ + 'id', + 'entityId', + 'source', + 'status', + 'executorActionEvent', + 'businessEvent', + 'businessStatus', + 'dateCreated', + 'isDeleted', + 'taskGroup{id, code, completionPolicy, taskexecutorSet {edges{node{id, user{id}}}}}', + 'data', + 'businessData', + 'jsonExt', + 'version', + 'dateUpdated', +]; + export const formatTaskGroupGQL = (taskGroup) => { const executors = taskGroup?.taskexecutorSet?.map((executor) => decodeId(executor.id)); const taskSources = taskGroup?.taskSources?.map((taskSource) => taskSource.name); @@ -104,6 +122,11 @@ export function fetchTasks(modulesManager, params) { return graphql(payload, ACTION_TYPE.SEARCH_TASKS); } +export function fetchTaskHistory(modulesManager, params) { + const payload = formatPageQueryWithCount('taskHistory', params, TASK_HISTORY_FULL_PROJECTION()); + return graphql(payload, ACTION_TYPE.SEARCH_TASK_HISTORY); +} + export function fetchTask(modulesManager, params) { const payload = formatPageQueryWithCount('task', params, TASK_FULL_PROJECTION()); return graphql(payload, ACTION_TYPE.GET_TASK); diff --git a/src/components/TaskHeadPanel.js b/src/components/TaskHeadPanel.js index b27618a..d04558f 100644 --- a/src/components/TaskHeadPanel.js +++ b/src/components/TaskHeadPanel.js @@ -14,6 +14,7 @@ import TaskStatusPicker from '../pickers/TaskStatusPicker'; import TaskGroupPicker from '../pickers/TaskGroupPicker'; import { TASK_STATUS, TASK_UPDATE } from '../constants'; import trimBusinessEvent from '../utils/trimBusinessEvent'; +import TaskHistoryDialog from './dialogs/TaskHistoryDialog'; const styles = (theme) => ({ tableTitle: theme.table.title, @@ -23,21 +24,26 @@ const styles = (theme) => ({ }, }); -const renderHeadPanelTitle = (classes) => ( +const renderHeadPanelTitle = (classes, rights, task) => ( - - - - - - - + + + + + + + + @@ -51,7 +57,7 @@ class TaskHeadPanel extends FormPanel { const task = { ...edited }; return ( <> - {renderHeadPanelTitle(classes)} + {renderHeadPanelTitle(classes, rights, task)} diff --git a/src/components/TaskHistoryFilter.js b/src/components/TaskHistoryFilter.js new file mode 100644 index 0000000..e44b268 --- /dev/null +++ b/src/components/TaskHistoryFilter.js @@ -0,0 +1,146 @@ +import React from 'react'; +import { injectIntl } from 'react-intl'; +import { Grid } from '@material-ui/core'; +import { withTheme, withStyles } from '@material-ui/core/styles'; +import _debounce from 'lodash/debounce'; +import { + TextInput, PublishedComponent, formatMessage, decodeId, toISODateTime, +} from '@openimis/fe-core'; +import { defaultFilterStyles } from '../utils/styles'; +import { + CONTAINS_LOOKUP, DEFAULT_DEBOUNCE_TIME, EMPTY_STRING, MODULE_NAME, +} from '../constants'; + +function TaskHistoryFilter({ + intl, classes, filters, onChangeFilters, +}) { + const debouncedOnChangeFilters = _debounce(onChangeFilters, DEFAULT_DEBOUNCE_TIME); + + const filterValue = (filterName) => filters?.[filterName]?.value; + + const filterTextFieldValue = (filterName) => filters?.[filterName]?.value ?? EMPTY_STRING; + + const onChangeStringFilter = (filterName, lookup = null) => (value) => { + if (lookup) { + debouncedOnChangeFilters([ + { + id: filterName, + value, + filter: `${filterName}_${lookup}: "${value}"`, + }, + ]); + } else { + onChangeFilters([ + { + id: filterName, + value, + filter: `${filterName}: "${value}"`, + }, + ]); + } + }; + + return ( + + + onChangeFilters([ + { + id: 'source', + value, + filter: value ? `source: "${value}"` : EMPTY_STRING, + }, + ])} + /> + + + + + + + + + onChangeFilters([ + { + id: 'taskGroupId', + value, + filter: value?.id ? `taskGroupId: "${decodeId(value.id)}"` : '', + }, + ])} + /> + + + onChangeFilters([ + { + id: 'status', + value, + filter: value ? `status: ${value}` : EMPTY_STRING, + }, + ])} + /> + + + onChangeFilters([ + { + id: 'dateCreated_Gte', + value: v, + filter: `dateCreated_Gte: "${toISODateTime(v)}"`, + }, + ])} + /> + + + onChangeFilters([ + { + id: 'dateCreated_Lte', + value: v, + filter: `dateCreated_Lte: "${toISODateTime(v)}"`, + }, + ])} + /> + + + ); +} + +export default injectIntl(withTheme(withStyles(defaultFilterStyles)(TaskHistoryFilter))); diff --git a/src/components/TaskHistorySearcher.js b/src/components/TaskHistorySearcher.js new file mode 100644 index 0000000..a8220ca --- /dev/null +++ b/src/components/TaskHistorySearcher.js @@ -0,0 +1,139 @@ +import React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { + Searcher, + useHistory, + historyPush, + useModulesManager, + useTranslations, +} from '@openimis/fe-core'; +import { + RIGHT_TASKS_MANAGEMENT_SEARCH, DEFAULT_PAGE_SIZE, ROWS_PER_PAGE_OPTIONS, TASK_STATUS, TASK_ROUTE, +} from '../constants'; +import { fetchTaskHistory } from '../actions'; +import trimBusinessEvent from '../utils/trimBusinessEvent'; +import TaskHistoryFilter from './TaskHistoryFilter'; + +function TaskHistorySearcher({ + rights, showFilters = true, taskId, +}) { + const history = useHistory(); + const modulesManager = useModulesManager(); + const dispatch = useDispatch(); + const { + formatMessageWithValues, + formatDateTimeFromISO, + } = useTranslations('tasksManagement', modulesManager); + + const fetchingTaskHistory = useSelector((state) => state?.tasksManagement?.fetchingTaskHistory); + const fetchedTaskHistory = useSelector((state) => state?.tasksManagement?.fetchedTaskHistory); + const errorTaskHistory = useSelector((state) => state?.tasksManagement?.errorTaskHistory); + const taskHistory = useSelector((state) => state?.tasksManagement?.taskHistory); + const taskHistoryPageInfo = useSelector((state) => state?.tasksManagement?.taskHistoryPageInfo); + const taskHistoryTotalCount = useSelector((state) => state?.tasksManagement?.taskHistoryTotalCount); + + const openTask = (task, newTab = false) => historyPush( + modulesManager, + history, + TASK_ROUTE, + [task?.id], + newTab, + ); + + const onDoubleClick = (task) => openTask(task); + const fetch = (params) => dispatch(fetchTaskHistory(modulesManager, params)); + + const rowIdentifier = (task) => task.id; + + const isRowDisabled = (_, task) => task.status !== TASK_STATUS.ACCEPTED; + + const headers = () => { + const headers = [ + 'task.source', + 'task.type', + 'task.entity', + 'task.assignee', + 'task.version', + 'task.dateUpdated', + 'task.status', + ]; + if (rights.includes(RIGHT_TASKS_MANAGEMENT_SEARCH)) { + headers.push('emptyLabel'); + } + return headers; + }; + + const sorts = () => [ + ['source', true], + ['type', false], + ['businessStatus', true], + ['taskGroup', true], + ['version', true], + ['dateUpdated', true], + ['status', true], + ]; + + const itemFormatters = () => [ + (taskHistory) => taskHistory.source, + (taskHistory) => trimBusinessEvent(taskHistory.businessEvent), + (taskHistory) => taskHistory.businessStatus, + (taskHistory) => taskHistory?.taskGroup?.code, + (taskHistory) => taskHistory.version, + (taskHistory) => formatDateTimeFromISO(taskHistory?.dateUpdated), + (taskHistory) => taskHistory.status, + ]; + + const defaultFilters = () => { + const filters = { + isDeleted: { + value: false, + filter: 'isDeleted: false', + }, + }; + if (taskId !== null && taskId !== undefined) { + filters.taskId = { + value: taskId, + filter: `id: "${taskId}"`, + }; + } + return filters; + }; + + const taskFilter = (props) => ( + + ); + + return ( + + ); +} + +export default TaskHistorySearcher; diff --git a/src/components/dialogs/TaskHistoryDialog.js b/src/components/dialogs/TaskHistoryDialog.js new file mode 100644 index 0000000..82cd24b --- /dev/null +++ b/src/components/dialogs/TaskHistoryDialog.js @@ -0,0 +1,124 @@ +/* eslint-disable max-len */ +import React, { useState } from 'react'; +import Button from '@material-ui/core/Button'; +import Dialog from '@material-ui/core/Dialog'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import { + useModulesManager, + useTranslations, +} from '@openimis/fe-core'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import { MODULE_NAME } from '../../constants'; +// eslint-disable-next-line import/no-named-as-default, import/no-named-as-default-member +import TaskHistorySearcher from '../TaskHistorySearcher'; + +function TaskHistoryDialog({ + classes, + rights, + taskId, +}) { + const [isOpen, setIsOpen] = useState(false); + + const handleOpen = () => { + setIsOpen(true); + }; + + const handleClose = () => { + setIsOpen(false); + }; + + const modulesManager = useModulesManager(); + const { formatMessage } = useTranslations(MODULE_NAME, modulesManager); + + return ( + <> + + + + {formatMessage('tasksManagement.tasks.changelog')} + + +
+ +
+
+ +
+
+
+ +
+
+ +
+ + ); +} + +const mapStateToProps = (state) => ({ + rights: !!state.core && !!state.core.user && !!state.core.user.i_user ? state.core.user.i_user.rights : [], + confirmed: state.core.confirmed, +}); + +const mapDispatchToProps = (dispatch) => bindActionCreators({ +}, dispatch); + +export default connect(mapStateToProps, mapDispatchToProps)(TaskHistoryDialog); diff --git a/src/reducer.js b/src/reducer.js index 8be83b1..518c366 100644 --- a/src/reducer.js +++ b/src/reducer.js @@ -31,6 +31,7 @@ export const ACTION_TYPE = { UPDATE_TASK: 'TASK_MANAGEMENT_UPDATE_TASK', RESOLVE_TASK: 'TASK_MANAGEMENT_RESOLVE_TASK', SEARCH_TASKS: 'TASK_MANAGEMENT_SEARCH_TASKS', + SEARCH_TASK_HISTORY: 'TASK_MANAGEMENT_SEARCH_TASK_HISTORY', }; export const MUTATION_SERVICE = { @@ -68,6 +69,12 @@ const STORE_STATE = { tasks: [], tasksPageInfo: {}, tasksTotalCount: 0, + fetchingTaskHistory: false, + fetchedTaskHistory: false, + errorTaskHistory: null, + taskHistory: [], + taskHistoryPageInfo: {}, + taskHistoryTotalCount: 0, }; function reducer( @@ -97,6 +104,16 @@ function reducer( tasksTotalCount: 0, errorTasks: null, }; + case REQUEST(ACTION_TYPE.SEARCH_TASK_HISTORY): + return { + ...state, + fetchingTaskHistory: true, + fetchedTaskHistory: false, + taskHistory: [], + taskHistoryPageInfo: {}, + taskHistoryTotalCount: 0, + errorTaskHistory: null, + }; case SUCCESS(ACTION_TYPE.SEARCH_TASK_GROUPS): return { ...state, @@ -133,6 +150,20 @@ function reducer( action.payload.data.task ? action.payload.data.task.totalCount : null, errorTasks: formatGraphQLError(action.payload), }; + case SUCCESS(ACTION_TYPE.SEARCH_TASK_HISTORY): + return { + ...state, + fetchingTaskHistory: false, + fetchedTaskHistory: true, + taskHistory: parseData(action.payload.data.taskHistory)?.map((task) => ({ + ...task, + id: decodeId(task.id), + })), + taskHistoryPageInfo: pageInfo(action.payload.data.taskHistory), + taskHistoryTotalCount: + action.payload.data.taskHistory ? action.payload.data.taskHistory.totalCount : null, + errorTaskHistory: formatGraphQLError(action.payload), + }; case ERROR(ACTION_TYPE.SEARCH_TASK_GROUPS): return { ...state, @@ -151,6 +182,12 @@ function reducer( fetchingTasks: false, errorTasks: formatServerError(action.payload), }; + case ERROR(ACTION_TYPE.SEARCH_TASK_HISTORY): + return { + ...state, + fetchingTaskHistory: false, + errorTaskHistory: formatServerError(action.payload), + }; case REQUEST(ACTION_TYPE.GET_TASK_GROUP): return { ...state, diff --git a/src/translations/en.json b/src/translations/en.json index e4f352b..1661a7e 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -10,11 +10,13 @@ "tasksManagement.task.type": "Type", "tasksManagement.task.entity": "Entity", "tasksManagement.task.dateCreated": "Date Created", + "tasksManagement.task.dateUpdated": "Date Updated", "tasksManagement.task.dateCreated.after": "Date Created From", "tasksManagement.task.dateCreated.before": "Date Created To", "tasksManagement.task.assignee": "Task Group", "tasksManagement.task.businessStatus": "Business Status", "tasksManagement.task.status": "Status", + "tasksManagement.task.version": "Version", "tasksManagement.task.status.RECEIVED": "RECEIVED", "tasksManagement.task.status.ACCEPTED": "ACCEPTED", "tasksManagement.task.status.COMPLETED": "COMPLETED", @@ -74,5 +76,9 @@ "tasksManagement.task.type.accept_payroll": "Accept Payroll", "tasksManagement.task.type.payroll_reconciliation": "Payroll Reconciliation", "tasksManagement.task.type.payroll_reject": "Reject Payroll", - "tasksManagement.task.type.payroll_delete": "Delete Payroll" + "tasksManagement.task.type.payroll_delete": "Delete Payroll", + "tasksManagement.tasks.changelog": "Task Changelog", + "tasksManagement.tasks.changelog.showChangelog": "Show Changelog", + "tasksManagement.tasks.changelog.close": "Close", + "tasksManagement.taskHistory.searcherResultsTitle": "{taskHistoryTotalCount} Historical Tasks Found" }