From f6593f067c455b4f3a3eb0f2868d960ff3d2ca15 Mon Sep 17 00:00:00 2001 From: olewandowski1 Date: Mon, 14 Oct 2024 08:27:20 +0200 Subject: [PATCH] OM-342: groups init --- src/actions.js | 37 +++ src/components/groups/GroupFilter.js | 61 +++++ src/components/groups/GroupMasterPanel.js | 66 +++++ src/components/groups/GroupSearcher.js | 213 ++++++++++++++++ .../groups/GroupWorkerManagePanel.js | 238 ++++++++++++++++++ src/constants.js | 4 + src/index.js | 25 ++ src/pages/GroupDetailsPage.js | 109 ++++++++ src/pages/GroupsPage.js | 49 ++++ src/reducer.js | 84 +++++++ src/translations/en.json | 22 +- 11 files changed, 907 insertions(+), 1 deletion(-) create mode 100644 src/components/groups/GroupFilter.js create mode 100644 src/components/groups/GroupMasterPanel.js create mode 100644 src/components/groups/GroupSearcher.js create mode 100644 src/components/groups/GroupWorkerManagePanel.js create mode 100644 src/pages/GroupDetailsPage.js create mode 100644 src/pages/GroupsPage.js diff --git a/src/actions.js b/src/actions.js index 356a378..26a1ef1 100644 --- a/src/actions.js +++ b/src/actions.js @@ -44,6 +44,16 @@ const WORKER_PROJECTION = (modulesManager) => [ 'photo { photo }', ]; +// TODO: Adjust the group projection after BE changes +// eslint-disable-next-line no-unused-vars +export const GROUP_PROJECTION = (modulesManager) => [ + 'id', + 'uuid', + 'otherNames', + 'lastName', + // `workers {${WORKER_PROJECTION(modulesManager)}}`, +]; + function formatGraphQLDateRanges(dateRanges) { const rangeStrings = dateRanges.map((range) => `{ startDate: "${range.startDate}", endDate: "${range.endDate}" }`); return `[${rangeStrings.join(', ')}]`; @@ -533,3 +543,30 @@ export function validateMConnectWorker(nationalId, economicUnitCode) { { nationalId, economicUnitCode }, ); } + +export function fetchGroupsAction(modulesManager, params) { + const queryParams = [...params]; + // TODO: Change to `group` after BE changes + const payload = formatPageQueryWithCount('worker', queryParams, GROUP_PROJECTION(modulesManager)); + return graphql(payload, ACTION_TYPE.GET_GROUPS); +} + +export function fetchGroup(modulesManager, params) { + const queryParams = [...params]; + // TODO: Change to `group` after BE changes + const payload = formatPageQueryWithCount('worker', queryParams, GROUP_PROJECTION(modulesManager)); + return graphql(payload, ACTION_TYPE.GET_GROUP); +} + +export const clearGroup = () => (dispatch) => { + dispatch({ + type: CLEAR(ACTION_TYPE.GET_GROUP), + }); +}; + +// TODO: Adjust the group mutation after BE changes +export function createGroup() {} + +export function updateGroup() {} + +export function deleteGroup() {} diff --git a/src/components/groups/GroupFilter.js b/src/components/groups/GroupFilter.js new file mode 100644 index 0000000..af06d6e --- /dev/null +++ b/src/components/groups/GroupFilter.js @@ -0,0 +1,61 @@ +import React from 'react'; +import _debounce from 'lodash/debounce'; + +import { Grid } from '@material-ui/core'; +import { makeStyles } from '@material-ui/styles'; + +import { TextInput } from '@openimis/fe-core'; +import { CONTAINS_LOOKUP, DEFAULT_DEBOUNCE_TIME, EMPTY_STRING } from '../../constants'; + +export const useStyles = makeStyles((theme) => ({ + form: { + padding: '0 0 10px 0', + width: '100%', + }, + item: { + padding: theme.spacing(1), + }, +})); + +function GroupFilter({ filters, onChangeFilters }) { + const classes = useStyles(); + + const debouncedOnChangeFilters = _debounce(onChangeFilters, DEFAULT_DEBOUNCE_TIME); + + 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 ( + + + + + + ); +} + +export default GroupFilter; diff --git a/src/components/groups/GroupMasterPanel.js b/src/components/groups/GroupMasterPanel.js new file mode 100644 index 0000000..7cea1c4 --- /dev/null +++ b/src/components/groups/GroupMasterPanel.js @@ -0,0 +1,66 @@ +import React from 'react'; + +import { + Divider, + Grid, + Paper, + Typography, +} from '@material-ui/core'; +import { withStyles, withTheme } from '@material-ui/core/styles'; + +import { + FormattedMessage, FormPanel, TextInput, withModulesManager, +} from '@openimis/fe-core'; +import { EMPTY_STRING } from '../../constants'; +import GroupWorkerManagePanel from './GroupWorkerManagePanel'; + +const styles = (theme) => ({ + paper: theme.paper.paper, + tableTitle: theme.table.title, + item: theme.paper.item, +}); + +class GroupMasterPanel extends FormPanel { + render() { + const { + classes, edited, readOnly, onEditedChanged, + } = this.props; + + return ( + + + + + + + + + + + + this.updateAttribute('name', v)} + /> + + + + + + + + ); + } +} + +export default withModulesManager(withTheme(withStyles(styles)(GroupMasterPanel))); diff --git a/src/components/groups/GroupSearcher.js b/src/components/groups/GroupSearcher.js new file mode 100644 index 0000000..bc0a747 --- /dev/null +++ b/src/components/groups/GroupSearcher.js @@ -0,0 +1,213 @@ +import React, { + useCallback, useEffect, useRef, useState, +} from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import { IconButton, Tooltip } from '@material-ui/core'; +import DeleteIcon from '@material-ui/icons/Delete'; +import EditIcon from '@material-ui/icons/Edit'; + +import { + Searcher, SelectDialog, journalize, useHistory, useModulesManager, useTranslations, +} from '@openimis/fe-core'; +import { deleteGroup, fetchGroupsAction } from '../../actions'; +import { + DEFAULT_PAGE_SIZE, + MODULE_NAME, + RIGHT_GROUP_DELETE, + RIGHT_GROUP_EDIT, + RIGHT_GROUP_SEARCH, + ROWS_PER_PAGE_OPTIONS, +} from '../../constants'; +import GroupFilter from './GroupFilter'; + +function GroupSearcher() { + const history = useHistory(); + const dispatch = useDispatch(); + const modulesManager = useModulesManager(); + const prevSubmittingMutationRef = useRef(); + + const rights = useSelector((state) => state.core?.user?.i_user?.rights ?? []); + const { + fetchingGroups, + fetchedGroups, + errorGroups, + groups, + // TODO: Uncomment when BE is ready + // groupsPageInfo, + // groupsTotalCount, + mutation, + submittingMutation, + } = useSelector((state) => state.workerVoucher); + const { economicUnit } = useSelector((state) => state.policyHolder); + + const { formatMessage, formatMessageWithValues } = useTranslations(MODULE_NAME, modulesManager); + + const [queryParams, setQueryParams] = useState([]); + const [deleteGroupDialogOpen, setDeleteGroupDialogOpen] = useState(false); + const [groupToDelete, setGroupToDelete] = useState(null); + + const fetchGroups = useCallback( + (params) => { + try { + const actionParams = [...params]; + + if (economicUnit?.code) { + actionParams.push(`economicUnitCode:"${economicUnit.code}"`); + } + + dispatch(fetchGroupsAction(modulesManager, actionParams)); + } catch (error) { + throw new Error(`[GROUP_SEARCHER]: Fetching groups failed. ${error}`); + } + }, + [economicUnit], + ); + + const headers = () => [ + 'workerVoucher.GroupSearcher.groupName', + 'workerVoucher.GroupSearcher.dateCreated', + 'emptyLabel', + ]; + + const sorts = () => [ + ['groupName', true], + ['dateCreated', true], + ]; + + const rowIdentifier = (group) => group.uuid; + + const openGroup = (group) => rights.includes(RIGHT_GROUP_SEARCH) + && history.push(`/${modulesManager.getRef('workerVoucher.route.group')}/${group?.uuid}`); + + const onDoubleClick = (group) => openGroup(group); + + const onDeleteGroupDialogOpen = (group) => { + setDeleteGroupDialogOpen((prevState) => !prevState); + setGroupToDelete(group); + }; + + const onDeleteGroupDialogClose = () => { + setDeleteGroupDialogOpen((prevState) => !prevState); + setGroupToDelete(null); + }; + + const onDeleteGroupConfirm = () => { + try { + dispatch(deleteGroup(economicUnit, groupToDelete, 'Delete Group')); + fetchGroups(queryParams); + } catch (error) { + throw new Error(`[GROUP_SEARCHER]: Deletion failed. ${error}`); + } finally { + setDeleteGroupDialogOpen((prevState) => !prevState); + } + }; + + const itemFormatters = () => [ + (group) => group.chfId, + (group) => group.lastName, + (group) => group.otherNames, + (group) => ( +
+ {rights.includes(RIGHT_GROUP_EDIT) && ( + + openGroup(group)}> + + + + )} + {rights.includes(RIGHT_GROUP_DELETE) && ( + + onDeleteGroupDialogOpen(group)}> + + + + )} +
+ ), + ]; + + const groupFilters = ({ filters, onChangeFilters }) => ( + + ); + + const filtersToQueryParams = ({ + filters, pageSize, beforeCursor, afterCursor, orderBy, + }) => { + const queryParams = Object.keys(filters) + .filter((f) => !!filters[f].filter) + .map((f) => filters[f].filter); + if (!beforeCursor && !afterCursor) { + queryParams.push(`first: ${pageSize}`); + } + if (afterCursor) { + queryParams.push(`after: "${afterCursor}"`); + queryParams.push(`first: ${pageSize}`); + } + if (beforeCursor) { + queryParams.push(`before: "${beforeCursor}"`); + queryParams.push(`last: ${pageSize}`); + } + if (orderBy) { + queryParams.push(`orderBy: ["${orderBy}"]`); + } + setQueryParams(queryParams); + return queryParams; + }; + + useEffect(() => { + if (queryParams.length) { + fetchGroups(queryParams); + } + }, [economicUnit, queryParams]); + + useEffect(() => { + if (prevSubmittingMutationRef.current && !submittingMutation) { + dispatch(journalize(mutation)); + } + }, [submittingMutation]); + + useEffect(() => { + prevSubmittingMutationRef.current = submittingMutation; + }); + + return ( + <> + + onDeleteGroupConfirm()} + onClose={() => onDeleteGroupDialogClose()} + module="workerVoucher" + confirmTitle="GroupSearcher.dialog.title" + confirmMessage="GroupSearcher.dialog.message" + confirmationButton="GroupSearcher.dialog.confirm" + rejectionButton="GroupSearcher.dialog.abandon" + /> + + ); +} + +export default GroupSearcher; diff --git a/src/components/groups/GroupWorkerManagePanel.js b/src/components/groups/GroupWorkerManagePanel.js new file mode 100644 index 0000000..c42afe1 --- /dev/null +++ b/src/components/groups/GroupWorkerManagePanel.js @@ -0,0 +1,238 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import { + Avatar, + Grid, + IconButton, + InputAdornment, + List, + ListItem, + ListItemAvatar, + ListItemSecondaryAction, + ListItemText, + Paper, + TextField, + Tooltip, + Typography, +} from '@material-ui/core'; +import ClearIcon from '@material-ui/icons/Clear'; +import DoubleArrowIcon from '@material-ui/icons/DoubleArrow'; +import PersonAddIcon from '@material-ui/icons/PersonAdd'; +import SearchIcon from '@material-ui/icons/Search'; +import { makeStyles } from '@material-ui/styles'; + +import { + FormattedMessage, + ProgressOrError, + parseData, + useModulesManager, + useTranslations, +} from '@openimis/fe-core'; +import { fetchWorkers } from '../../actions'; +import { EMPTY_STRING, MODULE_NAME } from '../../constants'; +import { ACTION_TYPE } from '../../reducer'; + +const useStyles = makeStyles((theme) => ({ + paper: { ...theme.paper.paper, width: '100%' }, + tableTitle: theme.table.title, + item: theme.paper.item, + paperHeader: theme.paper.paperHeader, + list: { + width: '100%', + height: '368px', + position: 'relative', + overflow: 'auto', + }, + filter: { + width: '100%', + }, + listItemText: { + textTransform: 'capitalize', + }, + reversedArrow: { + transform: 'rotate(180deg)', + }, + listTitle: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + gap: '5px', + }, +})); + +function GroupWorkerManagePanel({ edited, onChange }) { + const modulesManager = useModulesManager(); + const classes = useStyles(); + const dispatch = useDispatch(); + const { formatMessage } = useTranslations(MODULE_NAME); + const [filterValue, setFilterValue] = useState(EMPTY_STRING); + const { economicUnit } = useSelector((state) => state.policyHolder); + const [isLoading, setIsLoading] = useState(false); + const [workers, setWorkers] = useState([]); + + const filterOutWorkers = (workers, filterValue) => { + if (filterValue === EMPTY_STRING) return workers; + return workers.filter((worker) => { + const workerName = `${worker.chfId} ${worker.otherNames} ${worker.lastName}`; + return workerName.toLowerCase().includes(filterValue.toLowerCase()); + }); + }; + + const filteredWorkers = filterOutWorkers(workers, filterValue); + + const fetchAllAvailableWorkers = useCallback(async () => { + setIsLoading(true); + try { + const workerData = await dispatch( + fetchWorkers(modulesManager, [`economicUnitCode:"${economicUnit.code}"`], ACTION_TYPE.REQUEST), + ); + const parsedWorkers = parseData(workerData.payload.data.worker); + setWorkers(parsedWorkers); + } catch (error) { + throw new Error(`[GROUP_WORKER_MANAGE_PANEL] Error fetching workers: ${error}`); + } finally { + setIsLoading(false); + } + }, [economicUnit.code]); + + useEffect(() => { + // TODO: If EU changes, apart from fetching workers, we should navigate back + fetchAllAvailableWorkers(); + }, [economicUnit.code, fetchAllAvailableWorkers]); + + const handleWorkerSelection = (selectedWorker) => { + if (edited.workers?.includes(selectedWorker)) return; + + setWorkers(workers.filter((w) => w !== selectedWorker)); + + const newWorkers = edited.workers ? [...edited.workers, selectedWorker] : [selectedWorker]; + onChange({ ...edited, workers: newWorkers }); + }; + + const handleWorkerRemoval = (workerToRemove) => { + setWorkers([...workers, workerToRemove]); + + const newWorkers = edited.workers.filter((worker) => worker !== workerToRemove); + onChange({ ...edited, workers: newWorkers }); + }; + + const addAllFilteredWorkers = () => { + const newWorkers = edited.workers ? [...edited.workers, ...filteredWorkers] : [...filteredWorkers]; + setWorkers([]); + onChange({ ...edited, workers: newWorkers }); + }; + + const removeAllWorkers = () => { + setWorkers([...workers, ...edited.workers]); + onChange({ ...edited, workers: [] }); + }; + + return ( + + + + + + + + + + + + + + + + ), + }} + onChange={(e) => setFilterValue(e.target.value)} + /> + + + + + + + + + + } + > + + + + + + + }> + + {filteredWorkers.map((worker) => ( + + + + + + + handleWorkerSelection(worker)}> + + + + + ))} + + + + + + } + > + + + + + + + + + + }> + + {edited?.workers?.map((worker) => ( + + + + + + + handleWorkerRemoval(worker)}> + + + + + ))} + + + + + + + ); +} + +export default GroupWorkerManagePanel; diff --git a/src/constants.js b/src/constants.js index 2341ac6..1a22944 100644 --- a/src/constants.js +++ b/src/constants.js @@ -7,6 +7,10 @@ export const RIGHT_WORKER_SEARCH = 101101; export const RIGHT_WORKER_ADD = 101102; export const RIGHT_WORKER_EDIT = 101103; export const RIGHT_WORKER_DELETE = 101104; +export const RIGHT_GROUP_SEARCH = 101101; +export const RIGHT_GROUP_ADD = 101102; +export const RIGHT_GROUP_EDIT = 101103; +export const RIGHT_GROUP_DELETE = 101104; export const MODULE_NAME = 'workerVoucher'; export const MAX_CELLS = 8; diff --git a/src/index.js b/src/index.js index fd5d300..c663dc6 100644 --- a/src/index.js +++ b/src/index.js @@ -10,12 +10,15 @@ import LocalAtmIcon from '@material-ui/icons/LocalAtm'; import MonetizationOnIcon from '@material-ui/icons/MonetizationOn'; import VpnLockIcon from '@material-ui/icons/VpnLock'; import People from '@material-ui/icons/People'; +import TransferWithinAStationIcon from '@material-ui/icons/TransferWithinAStation'; import { FormattedMessage } from '@openimis/fe-core'; import { ADMIN_RIGHT, EMPLOYER_RIGHT_SEARCH, INSPECTOR_RIGHT, + RIGHT_GROUP_EDIT, + RIGHT_GROUP_SEARCH, RIGHT_WORKER_ADD, RIGHT_WORKER_EDIT, RIGHT_WORKER_SEARCH, @@ -38,6 +41,8 @@ import BillVoucherHeadPanel from './components/BillVoucherHeadPanel'; import WorkersPage from './pages/WorkersPage'; import WorkerDetailsPage from './pages/WorkerDetailsPage'; import WorkerSearcherSelectActions from './components/WorkerSearcherSelectActions'; +import GroupsPage from './pages/GroupsPage'; +import GroupDetailsPage from './pages/GroupDetailsPage'; const ROUTE_WORKER_VOUCHERS_LIST = 'voucher/vouchers'; const ROUTE_WORKER_VOUCHER = 'voucher/vouchers/voucher'; @@ -47,6 +52,8 @@ const ROUTE_WORKER_VOUCHER_PRICE_MANAGEMENT = 'voucher/price'; const ROUTE_CHANGE_MOBILE_APP_PASSWORD = 'profile/mobile/password'; const ROUTE_WORKER_VOUCHER_WORKER_LIST = 'voucher/vouchers/workers'; const ROUTE_WORKER_VOUCHER_WORKER = 'voucher/vouchers/workers/worker'; +const ROUTE_GROUP_LIST = 'voucher/groups'; +const ROUTE_GROUP = 'voucher/groups/group'; const DEFAULT_CONFIG = { translations: [{ key: 'en', messages: messages_en }], @@ -56,12 +63,20 @@ const DEFAULT_CONFIG = { { key: 'workerVoucher.route.workerVoucher', ref: ROUTE_WORKER_VOUCHER }, { key: 'workerVoucher.route.workers', ref: ROUTE_WORKER_VOUCHER_WORKER_LIST }, { key: 'workerVoucher.route.worker', ref: ROUTE_WORKER_VOUCHER_WORKER }, + { key: 'workerVoucher.route.groups', ref: ROUTE_GROUP_LIST }, + { key: 'workerVoucher.route.group', ref: ROUTE_GROUP }, { key: 'workerVoucher.WorkerVoucherStatusPicker', ref: WorkerVoucherStatusPicker }, { key: 'workerVoucher.VoucherAcquirementMethodPicker', ref: VoucherAcquirementMethodPicker }, { key: 'workerVoucher.WorkerMultiplePicker', ref: WorkerMultiplePicker }, { key: 'workerVoucher.WorkerDateRangePicker', ref: WorkerDateRangePicker }, ], 'worker.MainMenu': [ + { + text: , + icon: , + route: `/${ROUTE_GROUP_LIST}`, + filter: (rights) => [RIGHT_GROUP_SEARCH].some((right) => rights.includes(right)), + }, { text: , icon: , @@ -99,6 +114,16 @@ const DEFAULT_CONFIG = { }, ], 'core.Router': [ + { + path: ROUTE_GROUP_LIST, + component: GroupsPage, + requiredRights: [RIGHT_GROUP_SEARCH], + }, + { + path: `${ROUTE_GROUP}/:group_uuid?`, + component: GroupDetailsPage, + requiredRights: [RIGHT_GROUP_SEARCH, RIGHT_GROUP_EDIT], + }, { path: ROUTE_WORKER_VOUCHERS_LIST, component: VouchersPage, diff --git a/src/pages/GroupDetailsPage.js b/src/pages/GroupDetailsPage.js new file mode 100644 index 0000000..1a871c0 --- /dev/null +++ b/src/pages/GroupDetailsPage.js @@ -0,0 +1,109 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import { makeStyles } from '@material-ui/styles'; + +import { + Form, Helmet, journalize, parseData, useHistory, useModulesManager, useTranslations, +} from '@openimis/fe-core'; +import { + clearGroup, createGroup, fetchGroup, updateGroup, +} from '../actions'; +import { + EMPTY_OBJECT, EMPTY_STRING, MODULE_NAME, RIGHT_GROUP_SEARCH, +} from '../constants'; +import GroupMasterPanel from '../components/groups/GroupMasterPanel'; + +const useStyles = makeStyles((theme) => ({ + page: theme.page, +})); + +function GroupDetailsPage({ match }) { + const classes = useStyles(); + const history = useHistory(); + const modulesManager = useModulesManager(); + const dispatch = useDispatch(); + const { economicUnit } = useSelector((state) => state.policyHolder); + const rights = useSelector((state) => state.core?.user?.i_user?.rights ?? []); + const { group } = useSelector((state) => state.workerVoucher); + const { mutation, submittingMutation } = useSelector((state) => state.workerVoucher); + const prevSubmittingMutationRef = useRef(); + const [edited, setEdited] = useState(group || EMPTY_OBJECT); + const groupUuid = match?.params?.group_uuid; + const { formatMessage, formatMessageWithValues } = useTranslations(MODULE_NAME, modulesManager); + + const titleParams = (group) => ({ + name: group?.name ?? EMPTY_STRING, + }); + + useEffect(async () => { + try { + if (groupUuid) { + const params = [`uuid: "${groupUuid}", economicUnitCode: "${economicUnit.code}"`]; + const groupData = await dispatch(fetchGroup(modulesManager, params)); + + const group = parseData(groupData.payload.data.group)?.[0]; + + setEdited(group); + } + } catch (error) { + throw new Error(`[GROUP_DETAILS_PAGE]: Fetching group failed. ${error}`); + } + }, [groupUuid, dispatch]); + + useEffect(() => () => dispatch(clearGroup()), []); + + const canSave = () => { + if (!edited?.name || !edited?.workers?.length) { + return false; + } + + return true; + }; + + const onSave = () => { + try { + if (groupUuid) { + dispatch(updateGroup(edited, 'UpdateGroup')); + } else { + dispatch(createGroup(edited, 'CreateGroup')); + } + } catch (error) { + throw new Error(`[GROUP_DETAILS_PAGE]: Saving group failed. ${error}`); + } + }; + + useEffect(() => { + if (prevSubmittingMutationRef.current && !submittingMutation) { + dispatch(journalize(mutation)); + } + }, [submittingMutation]); + + useEffect(() => { + prevSubmittingMutationRef.current = submittingMutation; + }); + + if (!rights.includes(RIGHT_GROUP_SEARCH)) return null; + + return ( +
+ +
history.goBack()} + Panels={[GroupMasterPanel]} + formatMessage={formatMessage} + rights={rights} + onEditedChanged={setEdited} + canSave={canSave} + save={onSave} + openDirty={!edited?.uuid} + /> +
+ ); +} + +export default GroupDetailsPage; diff --git a/src/pages/GroupsPage.js b/src/pages/GroupsPage.js new file mode 100644 index 0000000..a2b82c0 --- /dev/null +++ b/src/pages/GroupsPage.js @@ -0,0 +1,49 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; + +import { Fab } from '@material-ui/core'; +import { makeStyles } from '@material-ui/styles'; +import AddIcon from '@material-ui/icons/Add'; + +import { + historyPush, Helmet, useModulesManager, useTranslations, withTooltip, useHistory, +} from '@openimis/fe-core'; +import { MODULE_NAME, RIGHT_GROUP_ADD, RIGHT_GROUP_SEARCH } from '../constants'; +import GroupSearcher from '../components/groups/GroupSearcher'; + +export const useStyles = makeStyles((theme) => ({ + page: theme.page, + fab: theme.fab, +})); + +function GroupsPage() { + const modulesManager = useModulesManager(); + const history = useHistory(); + const classes = useStyles(); + const { formatMessage } = useTranslations(MODULE_NAME, modulesManager); + const rights = useSelector((state) => state.core?.user?.i_user?.rights ?? []); + + const onAddRedirect = () => { + historyPush(modulesManager, history, 'workerVoucher.route.group'); + }; + + if (!rights.includes(RIGHT_GROUP_SEARCH)) return null; + + return ( +
+ + + {rights.includes(RIGHT_GROUP_ADD) + && withTooltip( +
+ + + +
, + formatMessage('workerVoucher.GroupsPage.addTooltip'), + )} +
+ ); +} + +export default GroupsPage; diff --git a/src/reducer.js b/src/reducer.js index 992fa7b..9f44b94 100644 --- a/src/reducer.js +++ b/src/reducer.js @@ -33,6 +33,10 @@ export const ACTION_TYPE = { VOUCHER_COUNT: 'WORKER_VOUCHER_VOUCHER_COUNT', DELETE_WORKER: 'WORKER_VOUCHER_DELETE_WORKER', DELETE_WORKERS: 'WORKER_VOUCHER_DELETE_WORKERS', + GET_GROUPS: 'WORKER_VOUCHER_GET_GROUPS', + GET_GROUP: 'WORKER_VOUCHER_GET_GROUP', + DELETE_GROUP: 'WORKER_VOUCHER_DELETE_GROUP', + CREATE_GROUP: 'WORKER_VOUCHER_CREATE_GROUP', }; const STORE_STATE = { @@ -73,6 +77,16 @@ const STORE_STATE = { fetchedWorker: false, worker: {}, errorWorker: null, + groups: [], + groupsTotalCount: 0, + fetchingGroups: false, + fetchedGroups: false, + errorGroups: null, + groupsPageInfo: {}, + fetchingGroup: false, + fetchedGroup: false, + group: {}, + errorGroup: null, }; function reducer(state = STORE_STATE, action) { @@ -302,6 +316,72 @@ function reducer(state = STORE_STATE, action) { worker: {}, errorWorker: null, }; + case REQUEST(ACTION_TYPE.GET_GROUPS): + return { + ...state, + fetchingGroups: true, + fetchedGroups: false, + errorGroups: null, + groups: [], + groupsPageInfo: {}, + groupsTotalCount: 0, + }; + case SUCCESS(ACTION_TYPE.GET_GROUPS): + return { + ...state, + fetchingGroups: false, + fetchedGroups: true, + errorGroups: formatGraphQLError(action.payload), + groups: parseData(action.payload.data.group), + groupsPageInfo: pageInfo(action.payload.data.group), + groupsTotalCount: action.payload.data.group?.totalCount ?? 0, + }; + case ERROR(ACTION_TYPE.GET_GROUPS): + return { + ...state, + fetchingGroups: false, + errorGroups: formatServerError(action.payload), + }; + case CLEAR(ACTION_TYPE.GET_GROUPS): + return { + ...state, + fetchingGroups: false, + fetchedGroups: false, + errorGroups: null, + groups: [], + groupsPageInfo: {}, + groupsTotalCount: 0, + }; + case REQUEST(ACTION_TYPE.GET_GROUP): + return { + ...state, + fetchingGroup: true, + fetchedGroup: false, + group: {}, + errorGroup: null, + }; + case SUCCESS(ACTION_TYPE.GET_GROUP): + return { + ...state, + fetchingGroup: false, + fetchedGroup: true, + group: parseData(action.payload.data.group)?.[0], + errorGroup: formatGraphQLError(action.payload), + }; + case ERROR(ACTION_TYPE.GET_GROUP): + return { + ...state, + fetchingGroup: false, + errorGroup: formatServerError(action.payload), + }; + case CLEAR(ACTION_TYPE.GET_GROUP): + return { + ...state, + fetchingGroup: false, + fetchedGroup: false, + group: {}, + errorGroup: null, + }; case REQUEST(ACTION_TYPE.MUTATION): return dispatchMutationReq(state, action); case ERROR(ACTION_TYPE.MUTATION): @@ -318,6 +398,10 @@ function reducer(state = STORE_STATE, action) { return dispatchMutationResp(state, 'deleteWorker', action); case SUCCESS(ACTION_TYPE.DELETE_WORKERS): return dispatchMutationResp(state, 'deleteWorker', action); + case SUCCESS(ACTION_TYPE.CREATE_GROUP): + return dispatchMutationResp(state, 'createGroup', action); + case SUCCESS(ACTION_TYPE.DELETE_GROUP): + return dispatchMutationResp(state, 'deleteGroup', action); case SUCCESS(ACTION_TYPE.MANAGE_VOUCHER_PRICE): return dispatchMutationResp(state, 'createBusinessConfig', action); case SUCCESS(ACTION_TYPE.DELETE_VOUCHER_PRICE): diff --git a/src/translations/en.json b/src/translations/en.json index c0c8da2..36da83f 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1,6 +1,7 @@ { "workerVoucher.menu.voucherList": "Voucher List", "workerVoucher.menu.workersList": "Workers", + "workerVoucher.menu.groupList": "Groups", "workerVoucher.menu.voucherAcquirement": "Voucher Acquirement", "workerVoucher.menu.voucherAcquirementSuccess": "Voucher Acquirement Success", "workerVoucher.menu.voucherAssignment": "Voucher Assignment", @@ -18,6 +19,8 @@ "workerVoucher.employer.tradename": "Employer Trade Name", "workerVoucher.employer.code": "Employer Code", "workerVoucher.worker": "Worker", + "workerVoucher.group.new": "New Group", + "workerVoucher.group.edit": "Edit Group {name}", "workerVoucher.workers": "Workers", "workerVoucher.worker.chfId": "National ID", "workerVoucher.worker.assignedVouchers": "Vouchers This Year", @@ -32,6 +35,7 @@ "workerVoucher.expiryDate": "Expiry Date", "workerVoucher.createdDate": "Created Date", "workerVoucher.tooltip.details": "View details", + "workerVoucher.tooltip.edit": "Edit", "workerVoucher.tooltip.delete": "Delete Worker", "workerVoucher.tooltip.bulkDelete": "Delete Worker(s)", "workerVoucher.voucherPrice.delete": "Delete", @@ -145,5 +149,21 @@ "workerVoucher.WorkerSearcherSelectActions.dialog.title": "Are you sure you want to proceed?", "workerVoucher.WorkerSearcherSelectActions.dialog.message": "You are about to delete the selected workers ({count}). This action cannot be undone. Please confirm if you wish to proceed with this action.", "workerVoucher.WorkerSearcherSelectActions.dialog.confirm": "Delete", - "workerVoucher.WorkerSearcherSelectActions.dialog.abandon": "Cancel" + "workerVoucher.WorkerSearcherSelectActions.dialog.abandon": "Cancel", + "workerVoucher.GroupsPage.addTooltip": "Add Group", + "workerVoucher.GroupSearcher.groupName": "Group Name", + "workerVoucher.GroupSearcher.dateCreated": "Date Created", + "workerVoucher.GroupSearcher.resultsTitle": "{groupsTotalCount} Groups Found", + "workerVoucher.GroupSearcher.dialog.title": "Delete Group", + "workerVoucher.GroupSearcher.dialog.message": "You are about to delete the group. This action cannot be undone. Please confirm if you wish to proceed with this action.", + "workerVoucher.GroupSearcher.dialog.confirm": "Delete", + "workerVoucher.GroupSearcher.dialog.abandon": "Cancel", + "workerVoucher.group.name": "Group Name", + "workerVoucher.GroupDetailsPage.title": "{name} Group Details Page", + "workerVoucher.GroupWorkerManagePanel.title": "Manage Workers", + "workerVoucher.GroupWorkerManagePanel.workerFilter": "Search", + "workerVoucher.GroupWorkerManagePanel.availableWorkers": "All Workers", + "workerVoucher.GroupWorkerManagePanel.chosenWorkers": "Workers in Group", + "workerVoucher.GroupWorkerManagePanel.tooltip.removeAll": "Remove All Workers", + "workerVoucher.GroupWorkerManagePanel.tooltip.addAllFiltered": "Add All Filtered Workers" }