diff --git a/src/actions.js b/src/actions.js index 6e9b81c..f410e21 100644 --- a/src/actions.js +++ b/src/actions.js @@ -22,6 +22,7 @@ const TASK_GROUP_PROJECTION = () => [ const TASK_FULL_PROJECTION = () => [ 'id', 'entityId', + 'entityString', 'source', 'status', 'executorActionEvent', @@ -32,6 +33,22 @@ const TASK_FULL_PROJECTION = () => [ 'taskGroup{id, code, completionPolicy, taskexecutorSet {edges{node{id, user{id}}}}}', 'data', 'currentEntityData', + 'jsonExt', +]; + +const TASKS_FULL_PROJECTION = () => [ + 'id', + 'entityId', + 'entityString', + 'source', + 'status', + 'executorActionEvent', + 'businessEvent', + 'dateCreated', + 'isDeleted', + 'taskGroup{id, code, completionPolicy}', + 'data', + 'jsonExt', ]; export const formatTaskGroupGQL = (taskGroup) => { @@ -76,6 +93,11 @@ export function fetchTaskGroups(modulesManager, params) { return graphql(payload, ACTION_TYPE.SEARCH_TASK_GROUPS); } +export function fetchTasks(modulesManager, params) { + const payload = formatPageQueryWithCount('task', params, TASKS_FULL_PROJECTION()); + return graphql(payload, ACTION_TYPE.SEARCH_TASKS); +} + 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/TaskApprovementPanel.js b/src/components/TaskApprovementPanel.js index 874868b..87e957d 100644 --- a/src/components/TaskApprovementPanel.js +++ b/src/components/TaskApprovementPanel.js @@ -39,6 +39,7 @@ function TaskApprovementPanel({ mutation, journalize, confirmed, + setIsSaved, }) { const modulesManager = useModulesManager(); const classes = useStyles(); @@ -79,6 +80,7 @@ function TaskApprovementPanel({ user, approveOrFail, ); + setIsSaved(true); } } return () => confirmed && clearConfirm(false); diff --git a/src/components/TaskFilter.js b/src/components/TaskFilter.js new file mode 100644 index 0000000..fb9d652 --- /dev/null +++ b/src/components/TaskFilter.js @@ -0,0 +1,112 @@ +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, +} from '@openimis/fe-core'; +import { defaultFilterStyles } from '../utils/styles'; +import { + CONTAINS_LOOKUP, DEFAULT_DEBOUNCE_TIME, EMPTY_STRING, +} from '../constants'; + +function TaskFilter({ + 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: 'taskGroupId', + value, + filter: value?.id ? `taskGroupId: "${decodeId(value.id)}"` : '', + }, + ])} + /> + + + + + + onChangeFilters([ + { + id: 'status', + value, + filter: `status: ${value}`, + }, + ])} + /> + + + ); +} + +export default injectIntl(withTheme(withStyles(defaultFilterStyles)(TaskFilter))); diff --git a/src/components/TaskHeadPanel.js b/src/components/TaskHeadPanel.js index 3661ffd..b27618a 100644 --- a/src/components/TaskHeadPanel.js +++ b/src/components/TaskHeadPanel.js @@ -13,6 +13,7 @@ import { withTheme, withStyles } from '@material-ui/core/styles'; import TaskStatusPicker from '../pickers/TaskStatusPicker'; import TaskGroupPicker from '../pickers/TaskGroupPicker'; import { TASK_STATUS, TASK_UPDATE } from '../constants'; +import trimBusinessEvent from '../utils/trimBusinessEvent'; const styles = (theme) => ({ tableTitle: theme.table.title, @@ -34,7 +35,7 @@ const renderHeadPanelTitle = (classes) => ( > - + @@ -56,7 +57,7 @@ class TaskHeadPanel extends FormPanel { this.updateAttribute('source', source)} @@ -65,18 +66,18 @@ class TaskHeadPanel extends FormPanel { this.updateAttribute('type', type)} /> this.updateAttribute('entity', entity)} /> @@ -95,7 +96,7 @@ class TaskHeadPanel extends FormPanel { this.updateAttribute('businessStatus', businessStatus)} @@ -103,7 +104,7 @@ class TaskHeadPanel extends FormPanel { { - if (formatter(itemData, formatterIndex) === formatter(itemIncomingData, formatterIndex) - || !formatter(itemIncomingData, formatterIndex)) { + if (formatter(itemData, jsonExt, formatterIndex) === formatter(itemIncomingData, jsonExt, formatterIndex) + || !formatter(itemIncomingData, jsonExt, formatterIndex)) { return HYPHEN; } - return formatter(itemIncomingData, formatterIndex); + return formatter(itemIncomingData, jsonExt, formatterIndex); }; return ( <>

- {formatter(itemData, formatterIndex)} + {formatter(itemData, jsonExt, formatterIndex) ?? HYPHEN}

{showHistorical(incomingData)} diff --git a/src/components/TaskPreviewPanel.js b/src/components/TaskPreviewPanel.js index eb885c3..2f2ed53 100644 --- a/src/components/TaskPreviewPanel.js +++ b/src/components/TaskPreviewPanel.js @@ -1,51 +1,46 @@ import React, { useState, useEffect } from 'react'; import { makeStyles, Paper, Typography } from '@material-ui/core'; -import { - Contributions, useTranslations, - useModulesManager, -} from '@openimis/fe-core'; -import { - BENEFIT_PLAN_TASK_PREVIEW_TABLE_VALUE, - BENEFIT_PLAN_UPDATE_STRING, EMPTY_STRING, - TASKS_PREVIEW_CONTRIBUTION_KEY, -} from '../constants'; +import { useModulesManager } from '@openimis/fe-core'; +import { EMPTY_STRING, TASK_CONTRIBUTION_KEY } from '../constants'; +import TaskPreviewTable from './TaskPreviewTable'; const useStyles = makeStyles((theme) => ({ paper: theme.paper.paper, title: theme.paper.title, })); -function TaskPreviewPanel({ - rights, - edited, -}) { +function TaskPreviewPanel({ rights, edited }) { const modulesManager = useModulesManager(); const classes = useStyles(); - const { formatMessage } = useTranslations('tasksManagement', modulesManager); - const [taskTable, setTaskTable] = useState(EMPTY_STRING); + const [header, setHeader] = useState(EMPTY_STRING); + const [tableTaskHeaders, setTableTaskHeaders] = useState([]); + const [taskItemFormatters, setTaskItemFormatters] = useState([]); const task = { ...edited }; useEffect(() => { - switch (task?.source) { - case BENEFIT_PLAN_UPDATE_STRING: - setTaskTable(BENEFIT_PLAN_TASK_PREVIEW_TABLE_VALUE); - break; - default: - setTaskTable(EMPTY_STRING); + if (task.source) { + const contrib = modulesManager.getContribs(TASK_CONTRIBUTION_KEY) + .find((c) => c.taskSource.includes(task.source)); + + if (contrib) { + const { tableHeaders, itemFormatters, text } = contrib; + setHeader(text); + setTableTaskHeaders(tableHeaders); + setTaskItemFormatters(itemFormatters); + } } - }, [task]); + }, [task.source]); - return taskTable && ( + return ( - {formatMessage('benefitPlanTask.detailsPage.triage.preview')} + {header} - ); diff --git a/src/components/TaskPreviewTable.js b/src/components/TaskPreviewTable.js new file mode 100644 index 0000000..fe2eb36 --- /dev/null +++ b/src/components/TaskPreviewTable.js @@ -0,0 +1,97 @@ +/* eslint-disable react/no-array-index-key */ +import React from 'react'; + +import { injectIntl } from 'react-intl'; +import { withStyles, withTheme } from '@material-ui/core/styles'; +import { + Table, TableBody, TableCell, TableContainer, TableHead, TableRow, +} from '@material-ui/core'; +import { ProgressOrError } from '@openimis/fe-core'; +import { useSelector } from 'react-redux'; +import TaskPreviewCell from './TaskPreviewCell'; + +const styles = (theme) => ({ + table: theme.table, + tableTitle: theme.table.title, + tableHeader: theme.table.header, + tableRow: theme.table.row, + tableLockedRow: theme.table.lockedRow, + tableLockedCell: theme.table.lockedCell, + tableHighlightedRow: theme.table.highlightedRow, + tableHighlightedCell: theme.table.highlightedCell, + tableHighlightedAltRow: theme.table.highlightedAltRow, + tableHighlightedAltCell: theme.table.highlightedAltCell, + tableDisabledRow: theme.table.disabledRow, + tableDisabledCell: theme.table.disabledCell, + tableFooter: theme.table.footer, + pager: theme.table.pager, + left: { + textAlign: 'left', + }, + right: { + textAlign: 'right', + }, + center: { + textAlign: 'center', + }, + clickable: { + cursor: 'pointer', + }, + loader: { + position: 'absolute', + top: 0, + bottom: 0, + left: 0, + right: 0, + background: 'rgba(0, 0, 0, 0.12)', + }, +}); + +function TaskPreviewTable({ + classes, + previewItem, + itemFormatters, + tableHeaders, +}) { + const { fetchingTasks, errorTasks } = useSelector((state) => state?.tasksManagement); + + return previewItem && ( + + + + + {tableHeaders.map((column) => ( + {column} + ))} + + + + + + {itemFormatters.map((formatter, formatterIndex) => ( + + + + ))} + + +
+
+ ); +} + +export default injectIntl(withTheme(withStyles(styles)(TaskPreviewTable))); diff --git a/src/components/TaskSearcher.js b/src/components/TaskSearcher.js new file mode 100644 index 0000000..ee7b0ed --- /dev/null +++ b/src/components/TaskSearcher.js @@ -0,0 +1,136 @@ +import React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { + Searcher, + useHistory, + historyPush, + useModulesManager, + useTranslations, +} from '@openimis/fe-core'; +import { IconButton, Tooltip } from '@material-ui/core'; +import VisibilityIcon from '@material-ui/icons/Visibility'; +import { + RIGHT_TASKS_MANAGEMENT_SEARCH, DEFAULT_PAGE_SIZE, ROWS_PER_PAGE_OPTIONS, TASK_STATUS, TASK_ROUTE, +} from '../constants'; +import TaskFilter from './TaskFilter'; +import { fetchTasks } from '../actions'; +import trimBusinessEvent from '../utils/trimBusinessEvent'; + +function TaskSearcher({ + rights, contribution, +}) { + const history = useHistory(); + const modulesManager = useModulesManager(); + const dispatch = useDispatch(); + const { formatMessage, formatMessageWithValues } = useTranslations('tasksManagement', modulesManager); + + const fetchingTasks = useSelector((state) => state?.tasksManagement?.fetchingTasks); + const fetchedTasks = useSelector((state) => state?.tasksManagement?.fetchedTasks); + const errorTasks = useSelector((state) => state?.tasksManagement?.errorTasks); + const tasks = useSelector((state) => state?.tasksManagement?.tasks); + const tasksPageInfo = useSelector((state) => state?.tasksManagement?.tasksPageInfo); + + const openTask = (task, newTab = false) => historyPush( + modulesManager, + history, + TASK_ROUTE, + [task?.id], + newTab, + ); + + const onDoubleClick = (task) => openTask(task); + const fetch = (params) => dispatch(fetchTasks(modulesManager, params)); + + const rowIdentifier = (task) => task.id; + + const isRowDisabled = (_, task) => task.status !== TASK_STATUS.ACCEPTED; + + const filterTasks = (tasks) => tasks.filter((task) => contribution.taskSource.includes(task.source)); + + const headers = () => { + const headers = [ + 'task.source', + 'task.type', + 'task.entity', + 'task.assignee', + 'task.businessStatus', + 'task.status', + ]; + if (rights.includes(RIGHT_TASKS_MANAGEMENT_SEARCH)) { + headers.push('emptyLabel'); + } + return headers; + }; + + const sorts = () => [ + ['source', true], + ['type', true], + ['entity', true], + ['assignee', true], + ['businessStatus', true], + ['status', true], + ]; + + const itemFormatters = () => [ + (task) => task.source, + (task) => trimBusinessEvent(task.businessEvent), + (task) => task.entityString, + (task) => task?.taskGroup?.code, + (task) => task.businessStatus, + (task) => task.status, + (task) => ( + + openTask(task)} + > + + + + ), + ]; + + const defaultFilters = () => ({ + isDeleted: { + value: false, + filter: 'isDeleted: false', + }, + }); + + const taskFilter = (props) => ( + + ); + + return ( + + ); +} + +export default TaskSearcher; diff --git a/src/constants.js b/src/constants.js index 3519f27..5e88871 100644 --- a/src/constants.js +++ b/src/constants.js @@ -56,11 +56,11 @@ export const TASK_UPDATE = 191003; export const RIGHT_TASK_EXECUTIONER_GROUPS = 190001; -export const TASKS_PREVIEW_CONTRIBUTION_KEY = 'tasksManagement.taskPreview'; - -export const BENEFIT_PLAN_TASK_PREVIEW_TABLE_VALUE = 'BenefitPlanTaskPreviewTable'; - -export const BENEFIT_PLAN_UPDATE_STRING = 'Benefit Plan Update'; +export const TASK_CONTRIBUTION_KEY = 'tasksManagement.tasks'; export const APPROVED = 'APPROVED'; export const FAILED = 'FAILED'; + +export const DOT = '.'; + +export const TASK_ROUTE = 'tasksManagement.route.task'; diff --git a/src/index.js b/src/index.js index c537152..91858e7 100644 --- a/src/index.js +++ b/src/index.js @@ -13,6 +13,7 @@ import TaskStatusPicker from './pickers/TaskStatusPicker'; import TaskPreviewCell from './components/TaskPreviewCell'; import TaskGroupPicker from './pickers/TaskGroupPicker'; import getAdminMainMenuContributions from './contributions/AdminMainMenuContributions'; +import { TASK_ROUTE } from './constants'; const ROUTE_TASKS_MANAGEMENT = 'tasks'; const ROUTE_TASK_MANAGEMENT = 'tasks/task'; @@ -32,13 +33,12 @@ const DEFAULT_CONFIG = { { path: `${ROUTE_GROUP_MANAGEMENT}/:task_group_uuid?`, component: TaskGroupPage }, ], refs: [ - { key: 'tasksManagement.route.task', ref: ROUTE_TASK_MANAGEMENT }, + { key: TASK_ROUTE, ref: ROUTE_TASK_MANAGEMENT }, { key: 'tasksManagement.route.group', ref: ROUTE_GROUP_MANAGEMENT }, { key: 'tasksManagement.taskStatusPicker', ref: TaskStatusPicker }, { key: 'tasksManagement.taskPreviewCell', ref: TaskPreviewCell }, { key: 'tasksManagement.taskGroupPicker', ref: TaskGroupPicker }, ], - 'tasksManagement.taskPreview': ['socialProtection.BenefitPlanTaskPreviewTable'], }; export const TasksManagementModule = (cfg) => ({ ...DEFAULT_CONFIG, ...cfg }); diff --git a/src/pages/TaskDetailsPage.js b/src/pages/TaskDetailsPage.js index 4654bdd..73b5f52 100644 --- a/src/pages/TaskDetailsPage.js +++ b/src/pages/TaskDetailsPage.js @@ -33,13 +33,15 @@ function TaskDetailsPage({ const history = useHistory(); const { formatMessage } = useTranslations('tasksManagement', modulesManager); const [editedTask, setEditedTask] = useState({}); + const [isSaved, setIsSaved] = useState(false); const back = () => history.goBack(); useEffect(() => { if (taskUuid) { fetchTask(modulesManager, [`id: "${taskUuid}"`]); + setIsSaved(false); } - }, [taskUuid]); + }, [taskUuid, isSaved]); useEffect(() => { if (task) { @@ -62,6 +64,7 @@ function TaskDetailsPage({ editedTask, formatMessage('task.update.mutationLabel'), ); + setIsSaved(true); } }; @@ -80,10 +83,10 @@ function TaskDetailsPage({ return (

- +
({ +const useStyles = makeStyles((theme) => ({ page: theme.page, paper: theme.paper.paper, - title: theme.paper.title, -}); + title: { + ...theme.paper.title, + cursor: 'pointer', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + }, +})); function TasksManagementPage() { const rights = useSelector((store) => store.core?.user?.i_user?.rights ?? []); + const classes = useStyles(); + const modulesManager = useModulesManager(); + + const contributions = modulesManager.getContribs(TASK_CONTRIBUTION_KEY); + + const [expandedContributionId, setExpandedContributionId] = useState(null); + + const handleOpen = (contributionId) => { + setExpandedContributionId(contributionId === expandedContributionId ? null : contributionId); + }; return ( - <> - - {/* REST OF PAGES */} - + contributions && ( + contributions.map((contribution) => ( + + +
+ handleOpen(contribution.text)}> + {contribution.text} + {expandedContributionId === contribution.text ? : } + +
+ +
+ +
+
+
+
+ )) + ) ); } -export default injectIntl(withTheme(withStyles(styles)( - (TasksManagementPage), -))); +export default TasksManagementPage; diff --git a/src/pickers/TaskGroupPicker.js b/src/pickers/TaskGroupPicker.js index 3c0cfed..437904d 100644 --- a/src/pickers/TaskGroupPicker.js +++ b/src/pickers/TaskGroupPicker.js @@ -50,7 +50,7 @@ function TaskGroupPicker(props) { multiple={multiple} required={required} placeholder={placeholder ?? formatMessage('tasksManagement.taskGroup.placeholder')} - label={label ?? formatMessage('tasksManagement.benefitPlanTask.assignee')} + label={label ?? formatMessage('tasksManagement.task.assignee')} error={error} withLabel={withLabel} withPlaceholder={withPlaceholder} diff --git a/src/pickers/TaskStatusPicker.js b/src/pickers/TaskStatusPicker.js index fd0a591..96ab22c 100644 --- a/src/pickers/TaskStatusPicker.js +++ b/src/pickers/TaskStatusPicker.js @@ -9,7 +9,7 @@ function TaskStatusPicker(props) { return ( ({ + ...task, + id: decodeId(task.id), + })), + tasksPageInfo: pageInfo(action.payload.data.task), + tasksTotalCount: + action.payload.data.task ? action.payload.data.task.totalCount : null, + errorTasks: formatGraphQLError(action.payload), + }; case ERROR(ACTION_TYPE.SEARCH_TASK_GROUPS): return { ...state, @@ -114,6 +145,12 @@ function reducer( fetchingTask: false, errorTask: formatServerError(action.payload), }; + case ERROR(ACTION_TYPE.SEARCH_TASKS): + return { + ...state, + fetchingTasks: false, + errorTasks: 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 5d88783..fce43f5 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2,21 +2,20 @@ "tasksManagement.tasksMainMenu": "Tasks Management", "tasksManagement.defaultValue.any": "Any", "tasksManagement.entries.tasksManagementView": "Tasks", - "tasksManagement.benefitPlan.tasks.title": "Benefit Plan Update Tasks", - "tasksManagement.benefitPlan.tasks.searcherResultsTitle": "{tasksTotalCount} Tasks Found", - "tasksManagement.benefitPlanTask.detailsPage.triage.title": "Task", - "tasksManagement.benefitPlanTask.detailsPage.triage.preview": "Preview", - "tasksManagement.benefitPlanTask.detailsPage.triage.headPanelTitle": "General Information", - "tasksManagement.benefitPlanTask.source": "Source", - "tasksManagement.benefitPlanTask.type": "Type", - "tasksManagement.benefitPlanTask.entity": "Entity", - "tasksManagement.benefitPlanTask.assignee": "Task Group", - "tasksManagement.benefitPlanTask.businessStatus": "Business Status", - "tasksManagement.benefitPlanTask.status": "Status", - "tasksManagement.benefitPlanTask.status.RECEIVED": "RECEIVED", - "tasksManagement.benefitPlanTask.status.ACCEPTED": "ACCEPTED", - "tasksManagement.benefitPlanTask.status.COMPLETED": "COMPLETED", - "tasksManagement.benefitPlanTask.status.FAILED": "FAILED", + "tasksManagement.task.searcherResultsTitle": "{tasksTotalCount} Tasks Found", + "tasksManagement.task.detailsPage.triage.title": "Task", + "tasksManagement.task.detailsPage.triage.preview": "Preview", + "tasksManagement.task.detailsPage.triage.headPanelTitle": "General Information", + "tasksManagement.task.source": "Source", + "tasksManagement.task.type": "Type", + "tasksManagement.task.entity": "Entity", + "tasksManagement.task.assignee": "Task Group", + "tasksManagement.task.businessStatus": "Business Status", + "tasksManagement.task.status": "Status", + "tasksManagement.task.status.RECEIVED": "RECEIVED", + "tasksManagement.task.status.ACCEPTED": "ACCEPTED", + "tasksManagement.task.status.COMPLETED": "COMPLETED", + "tasksManagement.task.status.FAILED": "FAILED", "tasksManagement.TaskPreviewTable.rowsPerPage": "Rows per page", "tasksManagement.TaskPreviewTable.ofPages": "{from} - {to} of {count}", "tasksManagement.createButton.tooltip": "Create", diff --git a/src/utils/styles.js b/src/utils/styles.js new file mode 100644 index 0000000..8ebced6 --- /dev/null +++ b/src/utils/styles.js @@ -0,0 +1,24 @@ +export const defaultPageStyles = (theme) => ({ + page: theme.page, +}); + +export const defaultFilterStyles = (theme) => ({ + form: { + padding: 0, + }, + item: { + padding: theme.spacing(1), + }, +}); + +export const defaultHeadPanelStyles = (theme) => ({ + tableTitle: theme.table.title, + item: theme.paper.item, + fullHeight: { + height: '100%', + }, +}); + +export const defaultDialogStyles = (theme) => ({ + item: theme.paper.item, +}); diff --git a/src/utils/trimBusinessEvent.js b/src/utils/trimBusinessEvent.js new file mode 100644 index 0000000..ad76f5d --- /dev/null +++ b/src/utils/trimBusinessEvent.js @@ -0,0 +1,5 @@ +import { DOT } from '../constants'; + +const trimBusinessEvent = (event) => (event ? event.split(DOT).slice(1).join(DOT) : null); + +export default trimBusinessEvent;