From 4574cacb771970347248db7cb18cbc8668a68fb7 Mon Sep 17 00:00:00 2001 From: sniedzielski <52816247+sniedzielski@users.noreply.github.com> Date: Mon, 22 May 2023 13:09:22 +0200 Subject: [PATCH 01/90] CM-24: Create frontend for "Individual" entity (#1) * CM-24: Scaffold frontend Individual module * CM-24: added Individuals and Individual pages with deletion * CM-24: possibility to edit Individual without validations * CM-24: added validation for json_ext and required fields * CM-24: updated readme * CM-24: added github workflows * CM-24: fixed code style in styles.js * CM-24: Adjustments after changes in individual model --- .babelrc | 12 ++ .eslintrc | 23 +++ .github/workflows/CI_and_build.yml | 44 ++++++ .github/workflows/npmpublish.yml | 51 ++++++ .gitignore | 24 +++ .priettierrc | 6 + README.md | 37 +++++ package.json | 41 +++++ rollup.config.js | 40 +++++ src/actions.js | 75 +++++++++ src/components/IndividualFilter.js | 74 +++++++++ src/components/IndividualHeadPanel.js | 99 ++++++++++++ src/components/IndividualSearcher.js | 213 ++++++++++++++++++++++++++ src/constants.js | 11 ++ src/index.js | 26 ++++ src/menus/BeneficiaryMainMenu.js | 31 ++++ src/pages/IndividualPage.js | 174 +++++++++++++++++++++ src/pages/IndividualsPage.js | 33 ++++ src/reducer.js | 106 +++++++++++++ src/translations/en.json | 41 +++++ src/util/action-type.js | 3 + src/util/json-validate.js | 8 + src/util/styles.js | 25 +++ 23 files changed, 1197 insertions(+) create mode 100644 .babelrc create mode 100644 .eslintrc create mode 100644 .github/workflows/CI_and_build.yml create mode 100644 .github/workflows/npmpublish.yml create mode 100644 .gitignore create mode 100644 .priettierrc create mode 100644 README.md create mode 100644 package.json create mode 100644 rollup.config.js create mode 100644 src/actions.js create mode 100644 src/components/IndividualFilter.js create mode 100644 src/components/IndividualHeadPanel.js create mode 100644 src/components/IndividualSearcher.js create mode 100644 src/constants.js create mode 100644 src/index.js create mode 100644 src/menus/BeneficiaryMainMenu.js create mode 100644 src/pages/IndividualPage.js create mode 100644 src/pages/IndividualsPage.js create mode 100644 src/reducer.js create mode 100644 src/translations/en.json create mode 100644 src/util/action-type.js create mode 100644 src/util/json-validate.js create mode 100644 src/util/styles.js diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..20c1db0 --- /dev/null +++ b/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + ["@babel/preset-env", { + "modules": false + }], + ["@babel/preset-react"] + ], + "plugins": [ + "@babel/plugin-transform-runtime", + "@babel/plugin-proposal-class-properties" + ] +} \ No newline at end of file diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..e74d212 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,23 @@ +{ + "parser": "babel-eslint", + "extends": [ + "standard", + "standard-react" + ], + "env": { + "es6": true + }, + "plugins": [ + "react" + ], + "parserOptions": { + "sourceType": "module" + }, + "rules": { + // don't force es6 functions to include space before paren + "space-before-function-paren": 0, + + // allow specifying true explicitly for boolean props + "react/jsx-boolean-value": 0 + } +} diff --git a/.github/workflows/CI_and_build.yml b/.github/workflows/CI_and_build.yml new file mode 100644 index 0000000..83603a2 --- /dev/null +++ b/.github/workflows/CI_and_build.yml @@ -0,0 +1,44 @@ +# This is a basic workflow to help you get started with Actions + +name: Build + +# Controls when the workflow will run +on: + # Triggers the workflow on push or pull request events but only for the main branch + pull_request: + branches: [ main, develop ] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [16.x] + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + # Runs a single command using the runners shell + - name: get dependences + run: | + node openimis-config.js openimis.json + - name: Install dependencies + run : yarn install + - name: build + run : yarn build + - uses: actions/upload-artifact@v2 + with: + name: frontend-node${{ matrix.node-version }}-${{github.run_number}}-${{github.sha}} + path: ./build/* diff --git a/.github/workflows/npmpublish.yml b/.github/workflows/npmpublish.yml new file mode 100644 index 0000000..61bbd22 --- /dev/null +++ b/.github/workflows/npmpublish.yml @@ -0,0 +1,51 @@ +# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created +# For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages + +name: Node.js Package + +on: + release: + types: [created] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: 12 + - run: yarn install + - run: yarn build + + publish-npm: + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: 12 + registry-url: https://registry.npmjs.org/ + scope: openimis + - run: yarn install + - run: yarn build + - run: npm publish --access public + env: + NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} + + publish-gpr: + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: 12 + registry-url: https://npm.pkg.github.com/ + - run: yarn install + - run: yarn build + - run: npm publish + env: + NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} + \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d3e2c0d --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ + +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +node_modules + +# builds +build +dist +.rpt2_cache + +# misc +.DS_Store +.env +.env.local +.env.development.local +.env.test.local +.env.production.local +.editorconfig + +npm-debug.log* +yarn-debug.log* +yarn-error.log* +yarn.lock diff --git a/.priettierrc b/.priettierrc new file mode 100644 index 0000000..0dc119f --- /dev/null +++ b/.priettierrc @@ -0,0 +1,6 @@ +{ + "trailingComma": "all", + "printWidth": 120, + "quoteProps": "preserve", + "arrowParens": "always" +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..01afd02 --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# openIMIS Frontend Individual module +This repository holds the files of the openIMIS Frontend Individual module. +It is dedicated to be bootstrap development of [openimis-fe_js](https://github.com/openimis/openimis-fe_js) modules, providing an empty (yet deployable) module. + +Please refer to [openimis-fe_js](https://github.com/openimis/openimis-fe_js) to see how to build and and deploy (in developement or server mode). + +The module is built with [rollup](https://rollupjs.org/). +In development mode, you can use `npm link` and `npm start` to continuously scan for changes and automatically update your development server. + +[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0) +[![Total alerts](https://img.shields.io/lgtm/alerts/g/openimis/openimis-fe-individual_js.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/openimis/openimis-fe-individual_js/alerts/) + +## Main Menu Contributions +* **Beneficiares and Households** (individual.mainMenu translation key) + + **Individuals** (individual.menu.individuals key), displayed if user has the right `159001` + +## Other Contributions +* `core.Router`: registering `individuals`, `individual`, routes in openIMIS client-side router + +## Available Contribution Points + +## Dispatched Redux Actions +* `INDIVIDUAL_INDIVIDUALS_{REQ|RESP|ERR}` fetching Individuals (as triggered by the searcher) +* `INDIVIDUAL_INDIVIDUAL_{REQ|RESP|ERR}` fetching chosen Individual +* `INDIVIDUAL_MUTATION_{REQ|ERR}`, sending a mutation +* `INDIVIDUAL_DELETE_INDIVIDUAL_RESP` receiving a result of delete Individual mutation +* `INDIVIDUAL_UPDATE_INDIVIDUAL_RESP` receiving a result of update Individual mutation + +## Other Modules Listened Redux Actions +None + +## Other Modules Redux State Bindings +* `state.core.user`, to access user info (rights,...) + +## Configurations Options +None diff --git a/package.json b/package.json new file mode 100644 index 0000000..0be595f --- /dev/null +++ b/package.json @@ -0,0 +1,41 @@ +{ + "name": "@openimis/fe-individual", + "version": "0.1.0", + "license": "AGPL-3.0-only", + "description": "openIMIS Frontend Individual module", + "repository": "openimis/openimis-fe-individual_js", + "main": "dist/index.js", + "module": "dist/index.es.js", + "engines": { + "node": ">=8", + "npm": ">=5" + }, + "scripts": { + "build": "rollup -c", + "start": "rollup -c -w" + }, + "peerDependency": { + "react-intl": "^5.8.1" + }, + "devDependencies": { + "@babel/cli": "^7.8.4", + "@babel/core": "^7.9.6", + "@babel/plugin-proposal-class-properties": "^7.8.3", + "@babel/plugin-transform-runtime": "^7.9.6", + "@babel/preset-env": "^7.9.6", + "@babel/preset-react": "^7.9.4", + "@babel/runtime": "^7.9.6", + "@rollup/plugin-babel": "^5.0.0", + "@rollup/plugin-commonjs": "^11.1.0", + "@rollup/plugin-json": "^4.0.3", + "@rollup/plugin-node-resolve": "^7.1.3", + "@rollup/plugin-url": "^5.0.0", + "rollup": "^2.10.0" + }, + "files": [ + "dist" + ], + "dependencies": { + "flat": "^5.0.2" + } +} \ No newline at end of file diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..e9c148c --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,40 @@ +import babel from '@rollup/plugin-babel' +import json from '@rollup/plugin-json' +import pkg from './package.json' + +export default { + input: 'src/index.js', + output: [ + { + file: pkg.module, + format: 'es', + sourcemap: true + }, + { + file: 'dist/index.js', + format: 'cjs', + sourcemap: true + } + ], + external: [ + /^@babel.*/, + /^@date-io\/.*/, + /^@material-ui\/.*/, + /^@openimis.*/, + "classnames", + "clsx", + "history", + /^lodash.*/, + "moment", + "prop-types", + /^react.*/, + /^redux.*/ + ], + plugins: [ + json(), + babel({ + exclude: 'node_modules/**', + babelHelpers: 'runtime' + }), + ] +} \ No newline at end of file diff --git a/src/actions.js b/src/actions.js new file mode 100644 index 0000000..c7acf56 --- /dev/null +++ b/src/actions.js @@ -0,0 +1,75 @@ +import { + decodeId, + graphql, + formatPageQuery, + formatPageQueryWithCount, + formatMutation, + formatGQLString +} from "@openimis/fe-core"; +import { ACTION_TYPE } from "./reducer"; +import { ERROR, REQUEST, SUCCESS } from "./util/action-type"; + +const INDIVIDUAL_FULL_PROJECTION = [ + "id", + "isDeleted", + "dateCreated", + "dateUpdated", + "firstName", + "lastName", + "dob", + "jsonExt" +]; + +export function fetchIndividuals(params) { + const payload = formatPageQueryWithCount("individual", params, INDIVIDUAL_FULL_PROJECTION); + return graphql(payload, ACTION_TYPE.SEARCH_INDIVIDUALS); +} + +export function fetchIndividual(params) { + const payload = formatPageQuery("individual", params, INDIVIDUAL_FULL_PROJECTION); + return graphql(payload, ACTION_TYPE.GET_INDIVIDUAL); +} + +export function deleteIndividual(individual, clientMutationLabel) { + const individualUuids = `uuids: ["${individual?.id}"]`; + const mutation = formatMutation("deleteIndividual", individualUuids, clientMutationLabel); + const requestedDateTime = new Date(); + return graphql( + mutation.payload, + [REQUEST(ACTION_TYPE.MUTATION), SUCCESS(ACTION_TYPE.DELETE_INDIVIDUAL), ERROR(ACTION_TYPE.MUTATION)], + { + actionType: ACTION_TYPE.DELETE_INDIVIDUAL, + clientMutationId: mutation.clientMutationId, + clientMutationLabel, + requestedDateTime, + }, + ); + } + +function dateTimeToDate(date) { + return date.split('T')[0]; +} + +function formatIndividualGQL(individual) { + return ` + ${!!individual.id ? `id: "${individual.id}"` : ""} + ${!!individual.firstName ? `firstName: "${formatGQLString(individual.firstName)}"` : ""} + ${!!individual.lastName ? `lastName: "${formatGQLString(individual.lastName)}"` : ""} + ${!!individual.jsonExt ? `jsonExt: ${JSON.stringify(individual.jsonExt)}` : ""} + ${!!individual.dob ? `dob: "${dateTimeToDate(individual.dob)}"` : ""}`; +} + +export function updateIndividual(individual, clientMutationLabel) { + const mutation = formatMutation("updateIndividual", formatIndividualGQL(individual), clientMutationLabel); + const requestedDateTime = new Date(); + return graphql( + mutation.payload, + [REQUEST(ACTION_TYPE.MUTATION), SUCCESS(ACTION_TYPE.UPDATE_INDIVIDUAL), ERROR(ACTION_TYPE.MUTATION)], + { + actionType: ACTION_TYPE.UPDATE_INDIVIDUAL, + clientMutationId: mutation.clientMutationId, + clientMutationLabel, + requestedDateTime, + }, + ); + } diff --git a/src/components/IndividualFilter.js b/src/components/IndividualFilter.js new file mode 100644 index 0000000..d41818d --- /dev/null +++ b/src/components/IndividualFilter.js @@ -0,0 +1,74 @@ +import React from "react"; +import { injectIntl } from "react-intl"; +import { TextInput, PublishedComponent } from "@openimis/fe-core"; +import { Grid } from "@material-ui/core"; +import { withTheme, withStyles } from "@material-ui/core/styles"; +import { CONTAINS_LOOKUP, DEFAULT_DEBOUNCE_TIME } from "../constants"; +import _debounce from "lodash/debounce"; +import { defaultFilterStyles } from "../util/styles"; + +const IndividualFilter = ({ intl, classes, filters, onChangeFilters }) => { + const debouncedOnChangeFilters = _debounce(onChangeFilters, DEFAULT_DEBOUNCE_TIME); + + const filterValue = (filterName) => filters?.[filterName]?.value; + + const onChangeStringFilter = + (filterName, lookup = null) => + (value) => { + lookup + ? debouncedOnChangeFilters([ + { + id: filterName, + value, + filter: `${filterName}_${lookup}: "${value}"`, + }, + ]) + : onChangeFilters([ + { + id: filterName, + value, + filter: `${filterName}: "${value}"`, + }, + ]); + }; + + return ( + + + + + + + + + + onChangeFilters([ + { + id: "dob", + value: v, + filter: `dob: "${v}"`, + }, + ]) + } + /> + + + ); +}; + +export default injectIntl(withTheme(withStyles(defaultFilterStyles)(IndividualFilter))); diff --git a/src/components/IndividualHeadPanel.js b/src/components/IndividualHeadPanel.js new file mode 100644 index 0000000..8b29b3a --- /dev/null +++ b/src/components/IndividualHeadPanel.js @@ -0,0 +1,99 @@ +import React, { Fragment } from "react"; +import { Grid, Divider, Typography } from "@material-ui/core"; +import { + withModulesManager, + FormPanel, + TextAreaInput, + TextInput, + FormattedMessage, + PublishedComponent, +} from "@openimis/fe-core"; +import { injectIntl } from "react-intl"; +import { withTheme, withStyles } from "@material-ui/core/styles"; +import { isJsonString } from "../util/json-validate"; + +const styles = theme => ({ + tableTitle: theme.table.title, + item: theme.paper.item, + fullHeight: { + height: "100%" + } +}); + +class IndividualHeadPanel extends FormPanel { + render() { + const { intl, edited, classes, mandatoryFieldsEmpty, setJsonExtValid } = this.props; + const individual = { ...edited }; + return ( + + + + + + + + + + + + + + {mandatoryFieldsEmpty && ( + +
+ +
+ +
+ )} + + + this.updateAttribute('firstName', v)} + value={individual?.firstName} + /> + + + this.updateAttribute('lastName', v)} + value={individual?.lastName} + /> + + + this.updateAttribute('dob', v)} + value={individual?.dob} + /> + + + this.updateAttribute('jsonExt', v)} + error={!isJsonString(individual?.jsonExt)} + /> + + +
+ ); + } +} + +export default withModulesManager(injectIntl(withTheme(withStyles(styles)(IndividualHeadPanel)))) diff --git a/src/components/IndividualSearcher.js b/src/components/IndividualSearcher.js new file mode 100644 index 0000000..a4a1aad --- /dev/null +++ b/src/components/IndividualSearcher.js @@ -0,0 +1,213 @@ +import React, { useState, useEffect, useRef } from "react"; +import { injectIntl } from "react-intl"; +import { + withModulesManager, + formatMessage, + formatMessageWithValues, + Searcher, + formatDateFromISO, + coreConfirm, + journalize, + withHistory, + historyPush, +} from "@openimis/fe-core"; +import { bindActionCreators } from "redux"; +import { connect } from "react-redux"; +import { fetchIndividuals, deleteIndividual } from "../actions"; +import { + DEFAULT_PAGE_SIZE, + ROWS_PER_PAGE_OPTIONS, + EMPTY_STRING, + RIGHT_INDIVIDUAL_UPDATE, + RIGHT_INDIVIDUAL_DELETE, +} from "../constants"; +import IndividualFilter from "./IndividualFilter"; +import { IconButton, Tooltip } from "@material-ui/core"; +import EditIcon from "@material-ui/icons/Edit"; +import DeleteIcon from "@material-ui/icons/Delete"; + +const IndividualSearcher = ({ + intl, + modulesManager, + history, + rights, + coreConfirm, + confirmed, + journalize, + submittingMutation, + mutation, + fetchIndividuals, + deleteIndividual, + fetchingIndividuals, + fetchedIndividuals, + errorIndividuals, + individuals, + individualsPageInfo, + individualsTotalCount, +}) => { + const [individualToDelete, setIndividualToDelete] = useState(null); + const [deletedIndividualUuids, setDeletedIndividualUuids] = useState([]); + const prevSubmittingMutationRef = useRef(); + + useEffect(() => individualToDelete && openDeleteIndividualConfirmDialog(), [individualToDelete]); + + useEffect(() => { + if (individualToDelete && confirmed) { + deleteIndividual( + individualToDelete, + formatMessageWithValues(intl, "individual", "individual.delete.mutationLabel", { + firstName: individualToDelete.firstName, + lastName: individualToDelete.lastName + }), + ); + setDeletedIndividualUuids([...deletedIndividualUuids, individualToDelete.id]); + } + individualToDelete && confirmed !== null && setIndividualToDelete(null); + }, [confirmed]); + + useEffect(() => { + prevSubmittingMutationRef.current && !submittingMutation && journalize(mutation); + }, [submittingMutation]); + + useEffect(() => { + prevSubmittingMutationRef.current = submittingMutation; + }); + + const openDeleteIndividualConfirmDialog = () => + coreConfirm( + formatMessageWithValues(intl, "individual", "individual.delete.confirm.title", { + firstName: individualToDelete.firstName, + lastName: individualToDelete.lastName + }), + formatMessage(intl, "individual", "individual.delete.confirm.message"), + ); + + const fetch = (params) => fetchIndividuals(params); + + const headers = () => { + const headers = [ + "individual.firstName", + "individual.lastName", + "individual.dob", + ]; + if (rights.includes(RIGHT_INDIVIDUAL_UPDATE)) { + headers.push("emptyLabel"); + } + return headers; + }; + + const itemFormatters = () => { + const formatters = [ + (individual) => individual.firstName, + (individual) => individual.lastName, + (individual) => + !!individual.dob ? formatDateFromISO(modulesManager, intl, individual.dob) : EMPTY_STRING, + ]; + if (rights.includes(RIGHT_INDIVIDUAL_UPDATE)) { + formatters.push((individual) => ( + + e.stopPropagation() && onDoubleClick(individual)} + disabled={deletedIndividualUuids.includes(individual.id)} + > + + + + )); + } + if (rights.includes(RIGHT_INDIVIDUAL_DELETE)) { + formatters.push((individual) => ( + + onDelete(individual)} + disabled={deletedIndividualUuids.includes(individual.id)} + > + + + + )); + } + return formatters; + }; + + const rowIdentifier = (individual) => individual.id; + + const sorts = () => [ + ["firstName", true], + ["lastName", true], + ["dob", true], + ]; + + const individualUpdatePageUrl = (individual) => modulesManager.getRef("individual.route.individual") + "/" + individual?.id; + + 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 isRowDisabled = (_, individual) => deletedIndividualUuids.includes(individual.id); + + const defaultFilters = () => ({ + isDeleted: { + value: false, + filter: "isDeleted: false", + }, + }); + + return ( + + ); +}; + +const mapStateToProps = (state) => ({ + fetchingIndividuals: state.individual.fetchingIndividuals, + fetchedIndividuals: state.individual.fetchedIndividuals, + errorIndividuals: state.individual.errorIndividuals, + individuals: state.individual.individuals, + individualsPageInfo: state.individual.individualsPageInfo, + individualsTotalCount: state.individual.individualsTotalCount, + confirmed: state.core.confirmed, + submittingMutation: state.individual.submittingMutation, + mutation: state.individual.mutation, +}); + +const mapDispatchToProps = (dispatch) => + bindActionCreators( + { + fetchIndividuals, + deleteIndividual, + coreConfirm, + journalize, + }, + dispatch, + ); + +export default withHistory( + withModulesManager(injectIntl(connect(mapStateToProps, mapDispatchToProps)(IndividualSearcher))), +); diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 0000000..74b4efa --- /dev/null +++ b/src/constants.js @@ -0,0 +1,11 @@ +export const BENEFICIARY_MAIN_MENU_CONTRIBUTION_KEY = "beneficiary.MainMenu"; +export const CONTAINS_LOOKUP = "Icontains"; +export const DEFAULT_DEBOUNCE_TIME = 500; +export const DEFAULT_PAGE_SIZE = 10; +export const EMPTY_STRING = ""; +export const ROWS_PER_PAGE_OPTIONS = [10, 20, 50, 100]; + +export const RIGHT_INDIVIDUAL_SEARCH = 159001; +export const RIGHT_INDIVIDUAL_CREATE = 159002; +export const RIGHT_INDIVIDUAL_UPDATE = 159003; +export const RIGHT_INDIVIDUAL_DELETE = 159004; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..1df9aea --- /dev/null +++ b/src/index.js @@ -0,0 +1,26 @@ +import messages_en from "./translations/en.json"; +import reducer from "./reducer"; +import flatten from "flat"; +import BeneficiaryMainMenu from "./menus/BeneficiaryMainMenu"; +import IndividualsPage from "./pages/IndividualsPage"; +import IndividualPage from "./pages/IndividualPage"; + +const ROUTE_INDIVIDUALS = "individuals"; +const ROUTE_INDIVIDUAL = "individuals/individual"; + +const DEFAULT_CONFIG = { + "translations": [{ key: "en", messages: flatten(messages_en) }], + "reducers": [{ key: "individual", reducer }], + "core.MainMenu": [BeneficiaryMainMenu], + "core.Router": [ + { path: ROUTE_INDIVIDUALS, component: IndividualsPage }, + { path: ROUTE_INDIVIDUAL + "/:individual_uuid?", component: IndividualPage }, + ], + "refs": [ + { key: "individual.route.individual", ref: ROUTE_INDIVIDUAL }, + ], +} + +export const IndividualModule = (cfg) => { + return { ...DEFAULT_CONFIG, ...cfg }; +} diff --git a/src/menus/BeneficiaryMainMenu.js b/src/menus/BeneficiaryMainMenu.js new file mode 100644 index 0000000..1ee83ee --- /dev/null +++ b/src/menus/BeneficiaryMainMenu.js @@ -0,0 +1,31 @@ +import React from "react"; +import { injectIntl } from "react-intl"; +import { connect } from "react-redux"; +import { Person } from "@material-ui/icons"; +import { formatMessage, MainMenuContribution, withModulesManager } from "@openimis/fe-core"; +import { BENEFICIARY_MAIN_MENU_CONTRIBUTION_KEY } from "../constants"; + +const BeneficiaryMainMenu = (props) => { + const entries = [ + { + text: formatMessage(props.intl, "individual", "menu.individuals"), + icon: , + route: "/individuals", + }, + ]; + entries.push( + ...props.modulesManager + .getContribs(BENEFICIARY_MAIN_MENU_CONTRIBUTION_KEY) + .filter((c) => !c.filter || c.filter(props.rights)), + ); + + return ( + + ); +}; + +const mapStateToProps = (state) => ({ + rights: !!state.core && !!state.core.user && !!state.core.user.i_user ? state.core.user.i_user.rights : [], +}); + +export default injectIntl(withModulesManager(connect(mapStateToProps)(BeneficiaryMainMenu))); diff --git a/src/pages/IndividualPage.js b/src/pages/IndividualPage.js new file mode 100644 index 0000000..e222a40 --- /dev/null +++ b/src/pages/IndividualPage.js @@ -0,0 +1,174 @@ +import React, { useState, useRef, useEffect } from "react"; +import { + Form, + Helmet, + withHistory, + formatMessage, + formatMessageWithValues, + coreConfirm, + journalize, +} from "@openimis/fe-core"; +import { injectIntl } from "react-intl"; +import { bindActionCreators } from "redux"; +import { connect } from "react-redux"; +import { withTheme, withStyles } from "@material-ui/core/styles"; +import { RIGHT_INDIVIDUAL_UPDATE } from "../constants"; +import { fetchIndividual, deleteIndividual, updateIndividual } from "../actions"; +import IndividualHeadPanel from "../components/IndividualHeadPanel"; +import DeleteIcon from "@material-ui/icons/Delete"; +import { ACTION_TYPE } from "../reducer"; +import { isJsonString } from "../util/json-validate"; + +const styles = (theme) => ({ + page: theme.page, +}); + +const IndividualPage = ({ + intl, + classes, + rights, + history, + individualUuid, + individual, + fetchIndividual, + deleteIndividual, + updateIndividual, + coreConfirm, + confirmed, + submittingMutation, + mutation, + journalize, +}) => { + const [editedIndividual, setEditedIndividual] = useState({}); + const [confirmedAction, setConfirmedAction] = useState(() => null); + const prevSubmittingMutationRef = useRef(); + + useEffect(() => { + if (!!individualUuid) { + fetchIndividual([`id: "${individualUuid}"`]); + } + }, [individualUuid]); + + useEffect(() => confirmed && confirmedAction(), [confirmed]); + + useEffect(() => { + if (prevSubmittingMutationRef.current && !submittingMutation) { + journalize(mutation); + mutation?.actionType === ACTION_TYPE.DELETE_INDIVIDUAL && back(); + } + }, [submittingMutation]); + + useEffect(() => { + prevSubmittingMutationRef.current = submittingMutation; + }); + + useEffect(() => setEditedIndividual(individual), [individual]); + + const back = () => history.goBack(); + + const titleParams = (individual) => ({ + firstName: individual?.firstName, + lastName: individual?.lastName + }); + + const isMandatoryFieldsEmpty = () => { + if (editedIndividual === undefined || editedIndividual === null){ + return false; + } + if ( + !!editedIndividual.firstName && + !!editedIndividual.lastName && + !!editedIndividual.dob + ) { + return false; + } + return true; + } + + const canSave = () => !isMandatoryFieldsEmpty() && isJsonString(editedIndividual?.jsonExt); + + const handleSave = () => { + updateIndividual( + editedIndividual, + formatMessageWithValues(intl, "individual", "individual.update.mutationLabel", { + firstName: individual?.firstName, + lastName: individual?.lastName + }), + ); + }; + + const deleteIndividualCallback = () => deleteIndividual( + individual, + formatMessageWithValues(intl, "individual", "individual.delete.mutationLabel", { + firstName: individual?.firstName, + lastName: individual?.lastName + }), + ); + + const openDeleteIndividualConfirmDialog = () => { + setConfirmedAction(() => deleteIndividualCallback); + coreConfirm( + formatMessageWithValues(intl, "individual", "individual.delete.confirm.title", { + firstName: individual?.firstName, + lastName: individual?.lastName + }), + formatMessage(intl, "individual", "individual.delete.confirm.message"), + ); + }; + + const actions = [ + !!individual && { + doIt: openDeleteIndividualConfirmDialog, + icon: , + tooltip: formatMessage(intl, "individual", "deleteButtonTooltip"), + }, + ]; + + return ( + rights.includes(RIGHT_INDIVIDUAL_UPDATE) && ( +
+ +
+
+ ) + ); +}; + +const mapStateToProps = (state, props) => ({ + rights: !!state.core && !!state.core.user && !!state.core.user.i_user ? state.core.user.i_user.rights : [], + individualUuid: props.match.params.individual_uuid, + confirmed: state.core.confirmed, + fetchingIndividuals: state.individual.fetchingIndividuals, + fetchedIndividuals: state.individual.fetchedIndividuals, + individual: state.individual.individual, + errorIndividual: state.individual.errorIndividual, + confirmed: state.core.confirmed, + submittingMutation: state.individual.submittingMutation, + mutation: state.individual.mutation, +}); + +const mapDispatchToProps = (dispatch) => { + return bindActionCreators({ fetchIndividual, deleteIndividual, updateIndividual, coreConfirm, journalize }, dispatch); +}; + +export default withHistory( + injectIntl(withTheme(withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(IndividualPage)))), +); diff --git a/src/pages/IndividualsPage.js b/src/pages/IndividualsPage.js new file mode 100644 index 0000000..65c6cb3 --- /dev/null +++ b/src/pages/IndividualsPage.js @@ -0,0 +1,33 @@ + +import React from "react"; +import { Helmet, withModulesManager, formatMessage } from "@openimis/fe-core"; +import { injectIntl } from "react-intl"; +import { withTheme, withStyles } from "@material-ui/core/styles"; +import { connect } from "react-redux"; +import { RIGHT_INDIVIDUAL_SEARCH } from "../constants"; +import IndividualSearcher from "../components/IndividualSearcher"; + + +const styles = (theme) => ({ + page: theme.page, + fab: theme.fab, +}); + +const IndividualsPage = (props) => { + const { intl, classes, rights } = props; + + return ( + rights.includes(RIGHT_INDIVIDUAL_SEARCH) && ( +
+ + +
+ ) + ); +}; + +const mapStateToProps = (state) => ({ + rights: !!state.core && !!state.core.user && !!state.core.user.i_user ? state.core.user.i_user.rights : [], +}); + +export default withModulesManager(injectIntl(withTheme(withStyles(styles)(connect(mapStateToProps)(IndividualsPage))))); \ No newline at end of file diff --git a/src/reducer.js b/src/reducer.js new file mode 100644 index 0000000..f0c38ba --- /dev/null +++ b/src/reducer.js @@ -0,0 +1,106 @@ +import { + formatServerError, + formatGraphQLError, + dispatchMutationReq, + dispatchMutationResp, + dispatchMutationErr, + parseData, + pageInfo, + decodeId, +} from "@openimis/fe-core"; +import { REQUEST, SUCCESS, ERROR } from "./util/action-type"; + +export const ACTION_TYPE = { + MUTATION: "INDIVIDUAL_MUTATION", + SEARCH_INDIVIDUALS: "INDIVIDUAL_INDIVIDUALS", + GET_INDIVIDUAL: "INDIVIDUAL_INDIVIDUAL", + DELETE_INDIVIDUAL: "INDIVIDUAL_DELETE_INDIVIDUAL", + UPDATE_INDIVIDUAL: "INDIVIDUAL_UPDATE_INDIVIDUAL" +}; + +function reducer( + state = { + submittingMutation: false, + mutation: {}, + fetchingIndividuals: false, + errorIndividuals: null, + fetchedIndividuals: false, + individuals: [], + individualsPageInfo: {}, + individualsTotalCount: 0, + fetchingIndividual: false, + errorIndividual: null, + fetchedIndividual: false, + individual: null + }, + action, +) { + switch (action.type) { + case REQUEST(ACTION_TYPE.SEARCH_INDIVIDUALS): + return { + ...state, + fetchingIndividuals: true, + fetchedIndividuals: false, + individuals: [], + individualsPageInfo: {}, + individualsTotalCount: 0, + errorIndividuals: null, + }; + case REQUEST(ACTION_TYPE.GET_INDIVIDUAL): + return { + ...state, + fetchingIndividual: true, + fetchedIndividual: false, + individual: null, + errorIndividual: null, + }; + case SUCCESS(ACTION_TYPE.SEARCH_INDIVIDUALS): + return { + ...state, + fetchingIndividuals: false, + fetchedIndividuals: true, + individuals: parseData(action.payload.data.individual)?.map((individual) => ({ + ...individual, + id: decodeId(individual.id), + })), + individualsPageInfo: pageInfo(action.payload.data.individual), + individualsTotalCount: !!action.payload.data.individual ? action.payload.data.individual.totalCount : null, + errorIndividuals: formatGraphQLError(action.payload), + }; + case SUCCESS(ACTION_TYPE.GET_INDIVIDUAL): + return { + ...state, + fetchingIndividual: false, + fetchedIndividual: true, + individual: parseData(action.payload.data.individual).map((individual) => ({ + ...individual, + id: decodeId(individual.id), + }))?.[0], + errorIndividual: null, + }; + case ERROR(ACTION_TYPE.SEARCH_INDIVIDUALS): + return { + ...state, + fetchingIndividuals: false, + errorIndividuals: formatServerError(action.payload), + }; + case ERROR(ACTION_TYPE.GET_INDIVIDUAL): + return { + ...state, + fetchingIndividual: false, + errorIndividual: formatServerError(action.payload), + }; + case REQUEST(ACTION_TYPE.MUTATION): + return dispatchMutationReq(state, action); + case ERROR(ACTION_TYPE.MUTATION): + return dispatchMutationErr(state, action); + case SUCCESS(ACTION_TYPE.DELETE_INDIVIDUAL): + return dispatchMutationResp(state, "deleteIndividual", action); + case SUCCESS(ACTION_TYPE.UPDATE_INDIVIDUAL): + return dispatchMutationResp(state, "updateIndividual", action); + default: + return state; + } +} + +export default reducer; diff --git a/src/translations/en.json b/src/translations/en.json new file mode 100644 index 0000000..ce818f1 --- /dev/null +++ b/src/translations/en.json @@ -0,0 +1,41 @@ +{ + "mainMenuBeneficiary": "Beneficiares and Households", + "emptyLabel": " ", + "any": "Any", + "editButtonTooltip": "Edit", + "deleteButtonTooltip": "Delete", + "dialog": { + "create": "Create", + "update": "Save", + "cancel": "Cancel" + }, + "menu": { + "individuals": "Individuals" + }, + "individual": { + "pageTitle": "Individual {firstName} {lastName}", + "headPanelTitle": "General Information", + "firstName": "First Name", + "lastName": "Last Name", + "dob": "Day of birth", + "json_ext": "Additional fields", + "mandatoryFieldsEmptyError": "* These fields are required", + "delete": { + "confirm": { + "title": "Delete {firstName} {lastName}?", + "message": "Deleting data does not mean erasing it from OpenIMIS database. The data will only be deactivated from the viewed list." + }, + "mutationLabel": "Delete Individual {firstName} {lastName}" + }, + "update": { + "label": "Update Individual", + "mutationLabel":"Update Individual {firstName} {lastName}" + }, + "saveButton.tooltip.enabled": "Save changes", + "saveButton.tooltip.disabled": "Please fill General Information fields first" + }, + "individuals": { + "pageTitle": "Individuals", + "searcherResultsTitle": "{individualsTotalCount} Individuals Found" + } +} \ No newline at end of file diff --git a/src/util/action-type.js b/src/util/action-type.js new file mode 100644 index 0000000..8a94938 --- /dev/null +++ b/src/util/action-type.js @@ -0,0 +1,3 @@ +export const REQUEST = actionTypeName => actionTypeName + "_REQ"; +export const SUCCESS = actionTypeName => actionTypeName + "_RESP"; +export const ERROR = actionTypeName => actionTypeName + "_ERR"; \ No newline at end of file diff --git a/src/util/json-validate.js b/src/util/json-validate.js new file mode 100644 index 0000000..d73eb1d --- /dev/null +++ b/src/util/json-validate.js @@ -0,0 +1,8 @@ +export const isJsonString = (string) => { + try { + JSON.parse(string); + } catch (e) { + return false; + } + return true; +}; diff --git a/src/util/styles.js b/src/util/styles.js new file mode 100644 index 0000000..a484a72 --- /dev/null +++ b/src/util/styles.js @@ -0,0 +1,25 @@ +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, +}); + \ No newline at end of file From 4d54009ccc31dc36b2c0f216356edcce08850c0e Mon Sep 17 00:00:00 2001 From: sniedzielski Date: Mon, 29 May 2023 10:31:30 +0200 Subject: [PATCH 02/90] hotfix: fix ci and cd --- package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 0be595f..a57dc8f 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,9 @@ }, "scripts": { "build": "rollup -c", - "start": "rollup -c -w" + "start": "rollup -c -w", + "format": "prettier src -w", + "prepare": "npm run build" }, "peerDependency": { "react-intl": "^5.8.1" From 410c8f7405cf13b5a24cfcbd7aa8622614b8647b Mon Sep 17 00:00:00 2001 From: sniedzielski Date: Tue, 30 May 2023 12:44:41 +0200 Subject: [PATCH 03/90] CM-24: remove additional field displaying on Individual page --- src/components/IndividualHeadPanel.js | 11 +---------- src/pages/IndividualPage.js | 2 +- src/translations/en.json | 1 - 3 files changed, 2 insertions(+), 12 deletions(-) diff --git a/src/components/IndividualHeadPanel.js b/src/components/IndividualHeadPanel.js index 8b29b3a..e84098b 100644 --- a/src/components/IndividualHeadPanel.js +++ b/src/components/IndividualHeadPanel.js @@ -22,7 +22,7 @@ const styles = theme => ({ class IndividualHeadPanel extends FormPanel { render() { - const { intl, edited, classes, mandatoryFieldsEmpty, setJsonExtValid } = this.props; + const { intl, edited, classes, mandatoryFieldsEmpty } = this.props; const individual = { ...edited }; return ( @@ -81,15 +81,6 @@ class IndividualHeadPanel extends FormPanel { value={individual?.dob} /> - - this.updateAttribute('jsonExt', v)} - error={!isJsonString(individual?.jsonExt)} - /> - ); diff --git a/src/pages/IndividualPage.js b/src/pages/IndividualPage.js index e222a40..028de4c 100644 --- a/src/pages/IndividualPage.js +++ b/src/pages/IndividualPage.js @@ -85,7 +85,7 @@ const IndividualPage = ({ return true; } - const canSave = () => !isMandatoryFieldsEmpty() && isJsonString(editedIndividual?.jsonExt); + const canSave = () => !isMandatoryFieldsEmpty(); const handleSave = () => { updateIndividual( diff --git a/src/translations/en.json b/src/translations/en.json index ce818f1..4393ac1 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -18,7 +18,6 @@ "firstName": "First Name", "lastName": "Last Name", "dob": "Day of birth", - "json_ext": "Additional fields", "mandatoryFieldsEmptyError": "* These fields are required", "delete": { "confirm": { From 127528b020d2a6f52dedfe1e9f0a64654c600c0c Mon Sep 17 00:00:00 2001 From: jdolkowski Date: Tue, 30 May 2023 14:02:21 +0200 Subject: [PATCH 04/90] CM-24: change uuids to ids --- src/actions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/actions.js b/src/actions.js index c7acf56..e080337 100644 --- a/src/actions.js +++ b/src/actions.js @@ -31,7 +31,7 @@ export function fetchIndividual(params) { } export function deleteIndividual(individual, clientMutationLabel) { - const individualUuids = `uuids: ["${individual?.id}"]`; + const individualUuids = `ids: ["${individual?.id}"]`; const mutation = formatMutation("deleteIndividual", individualUuids, clientMutationLabel); const requestedDateTime = new Date(); return graphql( From 293f685c16a3817f0534ae6d483931a0666cb92b Mon Sep 17 00:00:00 2001 From: Damian Borowiecki Date: Thu, 1 Jun 2023 15:41:39 +0200 Subject: [PATCH 05/90] CM-116: Added eslint and fixed workflow --- .eslintrc.json | 26 +++ ...CI_and_build.yml => CI_and_build copy.yml} | 16 +- .github/workflows/eslint_check.yml | 30 ++++ package.json | 8 +- src/actions.js | 105 ++++++----- src/components/IndividualFilter.js | 86 ++++----- src/components/IndividualHeadPanel.js | 166 +++++++++--------- src/components/IndividualSearcher.js | 120 ++++++------- src/constants.js | 6 +- src/index.js | 39 ++-- src/menus/BeneficiaryMainMenu.js | 30 ++-- src/pages/IndividualPage.js | 94 +++++----- src/pages/IndividualsPage.js | 24 ++- src/reducer.js | 27 +-- src/util/action-type.js | 6 +- src/util/json-validate.js | 4 +- src/util/styles.js | 9 +- 17 files changed, 435 insertions(+), 361 deletions(-) create mode 100644 .eslintrc.json rename .github/workflows/{CI_and_build.yml => CI_and_build copy.yml} (78%) create mode 100644 .github/workflows/eslint_check.yml diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..2317806 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,26 @@ +{ + "env": { + "browser": true, + "es2021": true + }, + "extends": [ + "plugin:react/recommended", + "airbnb" + ], + "overrides": [ + ], + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "plugins": [ + "react" + ], + "rules": { + "react/prop-types": "off", + "no-shadow": "off", // disabled due to use of bindActionCreators + "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], // disabled due to naming consistency with other modules + "import/no-unresolved": "off", // disable due to module architecture. For modules most references are marked as unresolved + "max-len": ["error", { "code": 120 }] + } +} diff --git a/.github/workflows/CI_and_build.yml b/.github/workflows/CI_and_build copy.yml similarity index 78% rename from .github/workflows/CI_and_build.yml rename to .github/workflows/CI_and_build copy.yml index 83603a2..fb19bf0 100644 --- a/.github/workflows/CI_and_build.yml +++ b/.github/workflows/CI_and_build copy.yml @@ -30,15 +30,15 @@ jobs: uses: actions/setup-node@v2 with: node-version: ${{ matrix.node-version }} - # Runs a single command using the runners shell - - name: get dependences - run: | - node openimis-config.js openimis.json - name: Install dependencies run : yarn install - name: build run : yarn build - - uses: actions/upload-artifact@v2 - with: - name: frontend-node${{ matrix.node-version }}-${{github.run_number}}-${{github.sha}} - path: ./build/* + - name: Check build status + run: | + if [ -d "dist" ]; then + echo "Build successful!" + else + echo "Build failed!" + exit 1 + fi diff --git a/.github/workflows/eslint_check.yml b/.github/workflows/eslint_check.yml new file mode 100644 index 0000000..c30cdac --- /dev/null +++ b/.github/workflows/eslint_check.yml @@ -0,0 +1,30 @@ +name: ESLint + +on: + push: + branches: + - main + - develop + pull_request: + branches: + - main + - develop + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup Node.js + uses: actions/setup-node@v2 + with: + node-version: 14 + + - name: Install dependencies + run: yarn install + + - name: Run ESLint + run: npx eslint src diff --git a/package.json b/package.json index a57dc8f..d06da9a 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,12 @@ "@rollup/plugin-json": "^4.0.3", "@rollup/plugin-node-resolve": "^7.1.3", "@rollup/plugin-url": "^5.0.0", + "eslint": "^7.32.0 || ^8.2.0", + "eslint-config-airbnb": "^19.0.4", + "eslint-plugin-import": "^2.25.3", + "eslint-plugin-jsx-a11y": "^6.5.1", + "eslint-plugin-react": "^7.28.0", + "eslint-plugin-react-hooks": "^4.3.0", "rollup": "^2.10.0" }, "files": [ @@ -40,4 +46,4 @@ "dependencies": { "flat": "^5.0.2" } -} \ No newline at end of file +} diff --git a/src/actions.js b/src/actions.js index e080337..f9ab087 100644 --- a/src/actions.js +++ b/src/actions.js @@ -1,50 +1,49 @@ -import { - decodeId, - graphql, - formatPageQuery, - formatPageQueryWithCount, - formatMutation, - formatGQLString -} from "@openimis/fe-core"; -import { ACTION_TYPE } from "./reducer"; -import { ERROR, REQUEST, SUCCESS } from "./util/action-type"; +import { + graphql, + formatPageQuery, + formatPageQueryWithCount, + formatMutation, + formatGQLString, +} from '@openimis/fe-core'; +import { ACTION_TYPE } from './reducer'; +import { ERROR, REQUEST, SUCCESS } from './util/action-type'; const INDIVIDUAL_FULL_PROJECTION = [ - "id", - "isDeleted", - "dateCreated", - "dateUpdated", - "firstName", - "lastName", - "dob", - "jsonExt" + 'id', + 'isDeleted', + 'dateCreated', + 'dateUpdated', + 'firstName', + 'lastName', + 'dob', + 'jsonExt', ]; export function fetchIndividuals(params) { - const payload = formatPageQueryWithCount("individual", params, INDIVIDUAL_FULL_PROJECTION); + const payload = formatPageQueryWithCount('individual', params, INDIVIDUAL_FULL_PROJECTION); return graphql(payload, ACTION_TYPE.SEARCH_INDIVIDUALS); } - + export function fetchIndividual(params) { - const payload = formatPageQuery("individual", params, INDIVIDUAL_FULL_PROJECTION); + const payload = formatPageQuery('individual', params, INDIVIDUAL_FULL_PROJECTION); return graphql(payload, ACTION_TYPE.GET_INDIVIDUAL); } export function deleteIndividual(individual, clientMutationLabel) { - const individualUuids = `ids: ["${individual?.id}"]`; - const mutation = formatMutation("deleteIndividual", individualUuids, clientMutationLabel); - const requestedDateTime = new Date(); - return graphql( - mutation.payload, - [REQUEST(ACTION_TYPE.MUTATION), SUCCESS(ACTION_TYPE.DELETE_INDIVIDUAL), ERROR(ACTION_TYPE.MUTATION)], - { - actionType: ACTION_TYPE.DELETE_INDIVIDUAL, - clientMutationId: mutation.clientMutationId, - clientMutationLabel, - requestedDateTime, - }, - ); - } + const individualUuids = `ids: ["${individual?.id}"]`; + const mutation = formatMutation('deleteIndividual', individualUuids, clientMutationLabel); + const requestedDateTime = new Date(); + return graphql( + mutation.payload, + [REQUEST(ACTION_TYPE.MUTATION), SUCCESS(ACTION_TYPE.DELETE_INDIVIDUAL), ERROR(ACTION_TYPE.MUTATION)], + { + actionType: ACTION_TYPE.DELETE_INDIVIDUAL, + clientMutationId: mutation.clientMutationId, + clientMutationLabel, + requestedDateTime, + }, + ); +} function dateTimeToDate(date) { return date.split('T')[0]; @@ -52,24 +51,24 @@ function dateTimeToDate(date) { function formatIndividualGQL(individual) { return ` - ${!!individual.id ? `id: "${individual.id}"` : ""} - ${!!individual.firstName ? `firstName: "${formatGQLString(individual.firstName)}"` : ""} - ${!!individual.lastName ? `lastName: "${formatGQLString(individual.lastName)}"` : ""} - ${!!individual.jsonExt ? `jsonExt: ${JSON.stringify(individual.jsonExt)}` : ""} - ${!!individual.dob ? `dob: "${dateTimeToDate(individual.dob)}"` : ""}`; + ${individual.id ? `id: "${individual.id}"` : ''} + ${individual.firstName ? `firstName: "${formatGQLString(individual.firstName)}"` : ''} + ${individual.lastName ? `lastName: "${formatGQLString(individual.lastName)}"` : ''} + ${individual.jsonExt ? `jsonExt: ${JSON.stringify(individual.jsonExt)}` : ''} + ${individual.dob ? `dob: "${dateTimeToDate(individual.dob)}"` : ''}`; } export function updateIndividual(individual, clientMutationLabel) { - const mutation = formatMutation("updateIndividual", formatIndividualGQL(individual), clientMutationLabel); - const requestedDateTime = new Date(); - return graphql( - mutation.payload, - [REQUEST(ACTION_TYPE.MUTATION), SUCCESS(ACTION_TYPE.UPDATE_INDIVIDUAL), ERROR(ACTION_TYPE.MUTATION)], - { - actionType: ACTION_TYPE.UPDATE_INDIVIDUAL, - clientMutationId: mutation.clientMutationId, - clientMutationLabel, - requestedDateTime, - }, - ); - } + const mutation = formatMutation('updateIndividual', formatIndividualGQL(individual), clientMutationLabel); + const requestedDateTime = new Date(); + return graphql( + mutation.payload, + [REQUEST(ACTION_TYPE.MUTATION), SUCCESS(ACTION_TYPE.UPDATE_INDIVIDUAL), ERROR(ACTION_TYPE.MUTATION)], + { + actionType: ACTION_TYPE.UPDATE_INDIVIDUAL, + clientMutationId: mutation.clientMutationId, + clientMutationLabel, + requestedDateTime, + }, + ); +} diff --git a/src/components/IndividualFilter.js b/src/components/IndividualFilter.js index d41818d..2376515 100644 --- a/src/components/IndividualFilter.js +++ b/src/components/IndividualFilter.js @@ -1,36 +1,38 @@ -import React from "react"; -import { injectIntl } from "react-intl"; -import { TextInput, PublishedComponent } from "@openimis/fe-core"; -import { Grid } from "@material-ui/core"; -import { withTheme, withStyles } from "@material-ui/core/styles"; -import { CONTAINS_LOOKUP, DEFAULT_DEBOUNCE_TIME } from "../constants"; -import _debounce from "lodash/debounce"; -import { defaultFilterStyles } from "../util/styles"; +import React from 'react'; +import { injectIntl } from 'react-intl'; +import { TextInput, PublishedComponent } from '@openimis/fe-core'; +import { Grid } from '@material-ui/core'; +import { withTheme, withStyles } from '@material-ui/core/styles'; +import _debounce from 'lodash/debounce'; +import { CONTAINS_LOOKUP, DEFAULT_DEBOUNCE_TIME } from '../constants'; +import { defaultFilterStyles } from '../util/styles'; -const IndividualFilter = ({ intl, classes, filters, onChangeFilters }) => { +function IndividualFilter({ + classes, filters, onChangeFilters, +}) { const debouncedOnChangeFilters = _debounce(onChangeFilters, DEFAULT_DEBOUNCE_TIME); const filterValue = (filterName) => filters?.[filterName]?.value; - const onChangeStringFilter = - (filterName, lookup = null) => - (value) => { - lookup - ? debouncedOnChangeFilters([ - { - id: filterName, - value, - filter: `${filterName}_${lookup}: "${value}"`, - }, - ]) - : onChangeFilters([ - { - id: filterName, - value, - filter: `${filterName}: "${value}"`, - }, - ]); - }; + 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 ( @@ -38,16 +40,16 @@ const IndividualFilter = ({ intl, classes, filters, onChangeFilters }) => { @@ -55,20 +57,18 @@ const IndividualFilter = ({ intl, classes, filters, onChangeFilters }) => { pubRef="core.DatePicker" module="individual" label="individual.dob" - value={filterValue("dob")} - onChange={(v) => - onChangeFilters([ - { - id: "dob", - value: v, - filter: `dob: "${v}"`, - }, - ]) - } + value={filterValue('dob')} + onChange={(v) => onChangeFilters([ + { + id: 'dob', + value: v, + filter: `dob: "${v}"`, + }, + ])} /> ); -}; +} export default injectIntl(withTheme(withStyles(defaultFilterStyles)(IndividualFilter))); diff --git a/src/components/IndividualHeadPanel.js b/src/components/IndividualHeadPanel.js index e84098b..eb4e9d2 100644 --- a/src/components/IndividualHeadPanel.js +++ b/src/components/IndividualHeadPanel.js @@ -1,90 +1,90 @@ -import React, { Fragment } from "react"; -import { Grid, Divider, Typography } from "@material-ui/core"; +import React, { Fragment } from 'react'; +import { Grid, Divider, Typography } from '@material-ui/core'; import { - withModulesManager, - FormPanel, - TextAreaInput, - TextInput, - FormattedMessage, - PublishedComponent, -} from "@openimis/fe-core"; -import { injectIntl } from "react-intl"; -import { withTheme, withStyles } from "@material-ui/core/styles"; -import { isJsonString } from "../util/json-validate"; + withModulesManager, + FormPanel, + TextInput, + FormattedMessage, + PublishedComponent, +} from '@openimis/fe-core'; +import { injectIntl } from 'react-intl'; +import { withTheme, withStyles } from '@material-ui/core/styles'; -const styles = theme => ({ - tableTitle: theme.table.title, - item: theme.paper.item, - fullHeight: { - height: "100%" - } +const styles = (theme) => ({ + tableTitle: theme.table.title, + item: theme.paper.item, + fullHeight: { + height: '100%', + }, }); class IndividualHeadPanel extends FormPanel { - render() { - const { intl, edited, classes, mandatoryFieldsEmpty } = this.props; - const individual = { ...edited }; - return ( - - - - - - - - - - - - - - {mandatoryFieldsEmpty && ( - -
- -
- -
- )} - - - this.updateAttribute('firstName', v)} - value={individual?.firstName} - /> - - - this.updateAttribute('lastName', v)} - value={individual?.lastName} - /> - - - this.updateAttribute('dob', v)} - value={individual?.dob} - /> - - -
- ); - } + render() { + const { + edited, classes, mandatoryFieldsEmpty, + } = this.props; + const individual = { ...edited }; + return ( + <> + + + + + + + + + + + + + {mandatoryFieldsEmpty && ( + <> +
+ +
+ + + )} + + + this.updateAttribute('firstName', v)} + value={individual?.firstName} + /> + + + this.updateAttribute('lastName', v)} + value={individual?.lastName} + /> + + + this.updateAttribute('dob', v)} + value={individual?.dob} + /> + + + + ); + } } -export default withModulesManager(injectIntl(withTheme(withStyles(styles)(IndividualHeadPanel)))) +export default withModulesManager(injectIntl(withTheme(withStyles(styles)(IndividualHeadPanel)))); diff --git a/src/components/IndividualSearcher.js b/src/components/IndividualSearcher.js index a4a1aad..586f5f5 100644 --- a/src/components/IndividualSearcher.js +++ b/src/components/IndividualSearcher.js @@ -1,5 +1,5 @@ -import React, { useState, useEffect, useRef } from "react"; -import { injectIntl } from "react-intl"; +import React, { useState, useEffect, useRef } from 'react'; +import { injectIntl } from 'react-intl'; import { withModulesManager, formatMessage, @@ -10,23 +10,23 @@ import { journalize, withHistory, historyPush, -} from "@openimis/fe-core"; -import { bindActionCreators } from "redux"; -import { connect } from "react-redux"; -import { fetchIndividuals, deleteIndividual } from "../actions"; +} from '@openimis/fe-core'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import { IconButton, Tooltip } from '@material-ui/core'; +import EditIcon from '@material-ui/icons/Edit'; +import DeleteIcon from '@material-ui/icons/Delete'; +import { fetchIndividuals, deleteIndividual } from '../actions'; import { DEFAULT_PAGE_SIZE, ROWS_PER_PAGE_OPTIONS, EMPTY_STRING, RIGHT_INDIVIDUAL_UPDATE, RIGHT_INDIVIDUAL_DELETE, -} from "../constants"; -import IndividualFilter from "./IndividualFilter"; -import { IconButton, Tooltip } from "@material-ui/core"; -import EditIcon from "@material-ui/icons/Edit"; -import DeleteIcon from "@material-ui/icons/Delete"; +} from '../constants'; +import IndividualFilter from './IndividualFilter'; -const IndividualSearcher = ({ +function IndividualSearcher({ intl, modulesManager, history, @@ -44,54 +44,67 @@ const IndividualSearcher = ({ individuals, individualsPageInfo, individualsTotalCount, -}) => { +}) { const [individualToDelete, setIndividualToDelete] = useState(null); const [deletedIndividualUuids, setDeletedIndividualUuids] = useState([]); const prevSubmittingMutationRef = useRef(); + function individualUpdatePageUrl(individual) { + return `${modulesManager.getRef('individual.route.individual')}/${individual?.id}`; + } + + const openDeleteIndividualConfirmDialog = () => coreConfirm( + formatMessageWithValues(intl, 'individual', 'individual.delete.confirm.title', { + firstName: individualToDelete.firstName, + lastName: individualToDelete.lastName, + }), + formatMessage(intl, 'individual', 'individual.delete.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); + useEffect(() => individualToDelete && openDeleteIndividualConfirmDialog(), [individualToDelete]); useEffect(() => { if (individualToDelete && confirmed) { deleteIndividual( individualToDelete, - formatMessageWithValues(intl, "individual", "individual.delete.mutationLabel", { + formatMessageWithValues(intl, 'individual', 'individual.delete.mutationLabel', { firstName: individualToDelete.firstName, - lastName: individualToDelete.lastName + lastName: individualToDelete.lastName, }), ); setDeletedIndividualUuids([...deletedIndividualUuids, individualToDelete.id]); } - individualToDelete && confirmed !== null && setIndividualToDelete(null); + if (individualToDelete && confirmed !== null) { + setIndividualToDelete(null); + } }, [confirmed]); useEffect(() => { - prevSubmittingMutationRef.current && !submittingMutation && journalize(mutation); + if (prevSubmittingMutationRef.current && !submittingMutation) { + journalize(mutation); + } }, [submittingMutation]); useEffect(() => { prevSubmittingMutationRef.current = submittingMutation; }); - const openDeleteIndividualConfirmDialog = () => - coreConfirm( - formatMessageWithValues(intl, "individual", "individual.delete.confirm.title", { - firstName: individualToDelete.firstName, - lastName: individualToDelete.lastName - }), - formatMessage(intl, "individual", "individual.delete.confirm.message"), - ); - const fetch = (params) => fetchIndividuals(params); const headers = () => { const headers = [ - "individual.firstName", - "individual.lastName", - "individual.dob", + 'individual.firstName', + 'individual.lastName', + 'individual.dob', ]; if (rights.includes(RIGHT_INDIVIDUAL_UPDATE)) { - headers.push("emptyLabel"); + headers.push('emptyLabel'); } return headers; }; @@ -100,12 +113,11 @@ const IndividualSearcher = ({ const formatters = [ (individual) => individual.firstName, (individual) => individual.lastName, - (individual) => - !!individual.dob ? formatDateFromISO(modulesManager, intl, individual.dob) : EMPTY_STRING, + (individual) => (individual.dob ? formatDateFromISO(modulesManager, intl, individual.dob) : EMPTY_STRING), ]; if (rights.includes(RIGHT_INDIVIDUAL_UPDATE)) { formatters.push((individual) => ( - + e.stopPropagation() && onDoubleClick(individual)} @@ -118,7 +130,7 @@ const IndividualSearcher = ({ } if (rights.includes(RIGHT_INDIVIDUAL_DELETE)) { formatters.push((individual) => ( - + onDelete(individual)} disabled={deletedIndividualUuids.includes(individual.id)} @@ -134,26 +146,17 @@ const IndividualSearcher = ({ const rowIdentifier = (individual) => individual.id; const sorts = () => [ - ["firstName", true], - ["lastName", true], - ["dob", true], + ['firstName', true], + ['lastName', true], + ['dob', true], ]; - const individualUpdatePageUrl = (individual) => modulesManager.getRef("individual.route.individual") + "/" + individual?.id; - - 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 isRowDisabled = (_, individual) => deletedIndividualUuids.includes(individual.id); const defaultFilters = () => ({ isDeleted: { value: false, - filter: "isDeleted: false", + filter: 'isDeleted: false', }, }); @@ -167,7 +170,7 @@ const IndividualSearcher = ({ fetchingItems={fetchingIndividuals} fetchedItems={fetchedIndividuals} errorItems={errorIndividuals} - tableTitle={formatMessageWithValues(intl, "individual", "individuals.searcherResultsTitle", { + tableTitle={formatMessageWithValues(intl, 'individual', 'individuals.searcherResultsTitle', { individualsTotalCount, })} headers={headers} @@ -183,7 +186,7 @@ const IndividualSearcher = ({ rowLocked={isRowDisabled} /> ); -}; +} const mapStateToProps = (state) => ({ fetchingIndividuals: state.individual.fetchingIndividuals, @@ -197,16 +200,15 @@ const mapStateToProps = (state) => ({ mutation: state.individual.mutation, }); -const mapDispatchToProps = (dispatch) => - bindActionCreators( - { - fetchIndividuals, - deleteIndividual, - coreConfirm, - journalize, - }, - dispatch, - ); +const mapDispatchToProps = (dispatch) => bindActionCreators( + { + fetchIndividuals, + deleteIndividual, + coreConfirm, + journalize, + }, + dispatch, +); export default withHistory( withModulesManager(injectIntl(connect(mapStateToProps, mapDispatchToProps)(IndividualSearcher))), diff --git a/src/constants.js b/src/constants.js index 74b4efa..3e59a32 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,8 +1,8 @@ -export const BENEFICIARY_MAIN_MENU_CONTRIBUTION_KEY = "beneficiary.MainMenu"; -export const CONTAINS_LOOKUP = "Icontains"; +export const BENEFICIARY_MAIN_MENU_CONTRIBUTION_KEY = 'beneficiary.MainMenu'; +export const CONTAINS_LOOKUP = 'Icontains'; export const DEFAULT_DEBOUNCE_TIME = 500; export const DEFAULT_PAGE_SIZE = 10; -export const EMPTY_STRING = ""; +export const EMPTY_STRING = ''; export const ROWS_PER_PAGE_OPTIONS = [10, 20, 50, 100]; export const RIGHT_INDIVIDUAL_SEARCH = 159001; diff --git a/src/index.js b/src/index.js index 1df9aea..06515c5 100644 --- a/src/index.js +++ b/src/index.js @@ -1,26 +1,27 @@ -import messages_en from "./translations/en.json"; -import reducer from "./reducer"; -import flatten from "flat"; -import BeneficiaryMainMenu from "./menus/BeneficiaryMainMenu"; -import IndividualsPage from "./pages/IndividualsPage"; -import IndividualPage from "./pages/IndividualPage"; +// Disable due to core architecture +/* eslint-disable camelcase */ +/* eslint-disable import/prefer-default-export */ +import flatten from 'flat'; +import messages_en from './translations/en.json'; +import reducer from './reducer'; +import BeneficiaryMainMenu from './menus/BeneficiaryMainMenu'; +import IndividualsPage from './pages/IndividualsPage'; +import IndividualPage from './pages/IndividualPage'; -const ROUTE_INDIVIDUALS = "individuals"; -const ROUTE_INDIVIDUAL = "individuals/individual"; +const ROUTE_INDIVIDUALS = 'individuals'; +const ROUTE_INDIVIDUAL = 'individuals/individual'; const DEFAULT_CONFIG = { - "translations": [{ key: "en", messages: flatten(messages_en) }], - "reducers": [{ key: "individual", reducer }], - "core.MainMenu": [BeneficiaryMainMenu], - "core.Router": [ + translations: [{ key: 'en', messages: flatten(messages_en) }], + reducers: [{ key: 'individual', reducer }], + 'core.MainMenu': [BeneficiaryMainMenu], + 'core.Router': [ { path: ROUTE_INDIVIDUALS, component: IndividualsPage }, - { path: ROUTE_INDIVIDUAL + "/:individual_uuid?", component: IndividualPage }, + { path: `${ROUTE_INDIVIDUAL}/:individual_uuid?`, component: IndividualPage }, ], - "refs": [ - { key: "individual.route.individual", ref: ROUTE_INDIVIDUAL }, + refs: [ + { key: 'individual.route.individual', ref: ROUTE_INDIVIDUAL }, ], -} +}; -export const IndividualModule = (cfg) => { - return { ...DEFAULT_CONFIG, ...cfg }; -} +export const IndividualModule = (cfg) => ({ ...DEFAULT_CONFIG, ...cfg }); diff --git a/src/menus/BeneficiaryMainMenu.js b/src/menus/BeneficiaryMainMenu.js index 1ee83ee..c146677 100644 --- a/src/menus/BeneficiaryMainMenu.js +++ b/src/menus/BeneficiaryMainMenu.js @@ -1,16 +1,20 @@ -import React from "react"; -import { injectIntl } from "react-intl"; -import { connect } from "react-redux"; -import { Person } from "@material-ui/icons"; -import { formatMessage, MainMenuContribution, withModulesManager } from "@openimis/fe-core"; -import { BENEFICIARY_MAIN_MENU_CONTRIBUTION_KEY } from "../constants"; +// Rules disabled due to core architecture +/* eslint-disable react/destructuring-assignment */ +/* eslint-disable react/jsx-props-no-spreading */ -const BeneficiaryMainMenu = (props) => { +import React from 'react'; +import { injectIntl } from 'react-intl'; +import { connect } from 'react-redux'; +import { Person } from '@material-ui/icons'; +import { formatMessage, MainMenuContribution, withModulesManager } from '@openimis/fe-core'; +import { BENEFICIARY_MAIN_MENU_CONTRIBUTION_KEY } from '../constants'; + +function BeneficiaryMainMenu(props) { const entries = [ { - text: formatMessage(props.intl, "individual", "menu.individuals"), + text: formatMessage(props.intl, 'individual', 'menu.individuals'), icon: , - route: "/individuals", + route: '/individuals', }, ]; entries.push( @@ -20,9 +24,13 @@ const BeneficiaryMainMenu = (props) => { ); return ( - + ); -}; +} const mapStateToProps = (state) => ({ rights: !!state.core && !!state.core.user && !!state.core.user.i_user ? state.core.user.i_user.rights : [], diff --git a/src/pages/IndividualPage.js b/src/pages/IndividualPage.js index 028de4c..d3e9623 100644 --- a/src/pages/IndividualPage.js +++ b/src/pages/IndividualPage.js @@ -1,4 +1,4 @@ -import React, { useState, useRef, useEffect } from "react"; +import React, { useState, useRef, useEffect } from 'react'; import { Form, Helmet, @@ -7,23 +7,22 @@ import { formatMessageWithValues, coreConfirm, journalize, -} from "@openimis/fe-core"; -import { injectIntl } from "react-intl"; -import { bindActionCreators } from "redux"; -import { connect } from "react-redux"; -import { withTheme, withStyles } from "@material-ui/core/styles"; -import { RIGHT_INDIVIDUAL_UPDATE } from "../constants"; -import { fetchIndividual, deleteIndividual, updateIndividual } from "../actions"; -import IndividualHeadPanel from "../components/IndividualHeadPanel"; -import DeleteIcon from "@material-ui/icons/Delete"; -import { ACTION_TYPE } from "../reducer"; -import { isJsonString } from "../util/json-validate"; +} from '@openimis/fe-core'; +import { injectIntl } from 'react-intl'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import { withTheme, withStyles } from '@material-ui/core/styles'; +import DeleteIcon from '@material-ui/icons/Delete'; +import { RIGHT_INDIVIDUAL_UPDATE } from '../constants'; +import { fetchIndividual, deleteIndividual, updateIndividual } from '../actions'; +import IndividualHeadPanel from '../components/IndividualHeadPanel'; +import { ACTION_TYPE } from '../reducer'; const styles = (theme) => ({ page: theme.page, }); -const IndividualPage = ({ +function IndividualPage({ intl, classes, rights, @@ -38,23 +37,27 @@ const IndividualPage = ({ submittingMutation, mutation, journalize, -}) => { +}) { const [editedIndividual, setEditedIndividual] = useState({}); const [confirmedAction, setConfirmedAction] = useState(() => null); const prevSubmittingMutationRef = useRef(); useEffect(() => { - if (!!individualUuid) { - fetchIndividual([`id: "${individualUuid}"`]); + if (individualUuid) { + fetchIndividual([`id: "${individualUuid}"`]); } }, [individualUuid]); useEffect(() => confirmed && confirmedAction(), [confirmed]); + const back = () => history.goBack(); + useEffect(() => { if (prevSubmittingMutationRef.current && !submittingMutation) { journalize(mutation); - mutation?.actionType === ACTION_TYPE.DELETE_INDIVIDUAL && back(); + if (mutation?.actionType === ACTION_TYPE.DELETE_INDIVIDUAL) { + back(); + } } }, [submittingMutation]); @@ -64,75 +67,73 @@ const IndividualPage = ({ useEffect(() => setEditedIndividual(individual), [individual]); - const back = () => history.goBack(); - - const titleParams = (individual) => ({ + const titleParams = (individual) => ({ firstName: individual?.firstName, - lastName: individual?.lastName + lastName: individual?.lastName, }); const isMandatoryFieldsEmpty = () => { - if (editedIndividual === undefined || editedIndividual === null){ - return false; + if (editedIndividual === undefined || editedIndividual === null) { + return false; } if ( - !!editedIndividual.firstName && - !!editedIndividual.lastName && - !!editedIndividual.dob + !!editedIndividual.firstName + && !!editedIndividual.lastName + && !!editedIndividual.dob ) { return false; } return true; - } + }; const canSave = () => !isMandatoryFieldsEmpty(); const handleSave = () => { updateIndividual( editedIndividual, - formatMessageWithValues(intl, "individual", "individual.update.mutationLabel", { + formatMessageWithValues(intl, 'individual', 'individual.update.mutationLabel', { firstName: individual?.firstName, - lastName: individual?.lastName + lastName: individual?.lastName, }), ); }; const deleteIndividualCallback = () => deleteIndividual( individual, - formatMessageWithValues(intl, "individual", "individual.delete.mutationLabel", { - firstName: individual?.firstName, - lastName: individual?.lastName + formatMessageWithValues(intl, 'individual', 'individual.delete.mutationLabel', { + firstName: individual?.firstName, + lastName: individual?.lastName, }), ); const openDeleteIndividualConfirmDialog = () => { setConfirmedAction(() => deleteIndividualCallback); coreConfirm( - formatMessageWithValues(intl, "individual", "individual.delete.confirm.title", { + formatMessageWithValues(intl, 'individual', 'individual.delete.confirm.title', { firstName: individual?.firstName, - lastName: individual?.lastName + lastName: individual?.lastName, }), - formatMessage(intl, "individual", "individual.delete.confirm.message"), + formatMessage(intl, 'individual', 'individual.delete.confirm.message'), ); }; const actions = [ !!individual && { - doIt: openDeleteIndividualConfirmDialog, - icon: , - tooltip: formatMessage(intl, "individual", "deleteButtonTooltip"), - }, + doIt: openDeleteIndividualConfirmDialog, + icon: , + tooltip: formatMessage(intl, 'individual', 'deleteButtonTooltip'), + }, ]; return ( rights.includes(RIGHT_INDIVIDUAL_UPDATE) && (
- +
) ); -}; +} const mapStateToProps = (state, props) => ({ rights: !!state.core && !!state.core.user && !!state.core.user.i_user ? state.core.user.i_user.rights : [], @@ -160,14 +161,13 @@ const mapStateToProps = (state, props) => ({ fetchedIndividuals: state.individual.fetchedIndividuals, individual: state.individual.individual, errorIndividual: state.individual.errorIndividual, - confirmed: state.core.confirmed, submittingMutation: state.individual.submittingMutation, mutation: state.individual.mutation, }); -const mapDispatchToProps = (dispatch) => { - return bindActionCreators({ fetchIndividual, deleteIndividual, updateIndividual, coreConfirm, journalize }, dispatch); -}; +const mapDispatchToProps = (dispatch) => bindActionCreators({ + fetchIndividual, deleteIndividual, updateIndividual, coreConfirm, journalize, +}, dispatch); export default withHistory( injectIntl(withTheme(withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(IndividualPage)))), diff --git a/src/pages/IndividualsPage.js b/src/pages/IndividualsPage.js index 65c6cb3..44d93c1 100644 --- a/src/pages/IndividualsPage.js +++ b/src/pages/IndividualsPage.js @@ -1,33 +1,31 @@ - -import React from "react"; -import { Helmet, withModulesManager, formatMessage } from "@openimis/fe-core"; -import { injectIntl } from "react-intl"; -import { withTheme, withStyles } from "@material-ui/core/styles"; -import { connect } from "react-redux"; -import { RIGHT_INDIVIDUAL_SEARCH } from "../constants"; -import IndividualSearcher from "../components/IndividualSearcher"; - +import React from 'react'; +import { Helmet, withModulesManager, formatMessage } from '@openimis/fe-core'; +import { injectIntl } from 'react-intl'; +import { withTheme, withStyles } from '@material-ui/core/styles'; +import { connect } from 'react-redux'; +import { RIGHT_INDIVIDUAL_SEARCH } from '../constants'; +import IndividualSearcher from '../components/IndividualSearcher'; const styles = (theme) => ({ page: theme.page, fab: theme.fab, }); -const IndividualsPage = (props) => { +function IndividualsPage(props) { const { intl, classes, rights } = props; return ( rights.includes(RIGHT_INDIVIDUAL_SEARCH) && (
- +
) ); -}; +} const mapStateToProps = (state) => ({ rights: !!state.core && !!state.core.user && !!state.core.user.i_user ? state.core.user.i_user.rights : [], }); -export default withModulesManager(injectIntl(withTheme(withStyles(styles)(connect(mapStateToProps)(IndividualsPage))))); \ No newline at end of file +export default withModulesManager(injectIntl(withTheme(withStyles(styles)(connect(mapStateToProps)(IndividualsPage))))); diff --git a/src/reducer.js b/src/reducer.js index f0c38ba..e6aa80a 100644 --- a/src/reducer.js +++ b/src/reducer.js @@ -1,4 +1,7 @@ -import { +// Disabled due to consistency with other modules +/* eslint-disable default-param-last */ + +import { formatServerError, formatGraphQLError, dispatchMutationReq, @@ -7,15 +10,15 @@ import { parseData, pageInfo, decodeId, -} from "@openimis/fe-core"; -import { REQUEST, SUCCESS, ERROR } from "./util/action-type"; +} from '@openimis/fe-core'; +import { REQUEST, SUCCESS, ERROR } from './util/action-type'; export const ACTION_TYPE = { - MUTATION: "INDIVIDUAL_MUTATION", - SEARCH_INDIVIDUALS: "INDIVIDUAL_INDIVIDUALS", - GET_INDIVIDUAL: "INDIVIDUAL_INDIVIDUAL", - DELETE_INDIVIDUAL: "INDIVIDUAL_DELETE_INDIVIDUAL", - UPDATE_INDIVIDUAL: "INDIVIDUAL_UPDATE_INDIVIDUAL" + MUTATION: 'INDIVIDUAL_MUTATION', + SEARCH_INDIVIDUALS: 'INDIVIDUAL_INDIVIDUALS', + GET_INDIVIDUAL: 'INDIVIDUAL_INDIVIDUAL', + DELETE_INDIVIDUAL: 'INDIVIDUAL_DELETE_INDIVIDUAL', + UPDATE_INDIVIDUAL: 'INDIVIDUAL_UPDATE_INDIVIDUAL', }; function reducer( @@ -31,7 +34,7 @@ function reducer( fetchingIndividual: false, errorIndividual: null, fetchedIndividual: false, - individual: null + individual: null, }, action, ) { @@ -64,7 +67,7 @@ function reducer( id: decodeId(individual.id), })), individualsPageInfo: pageInfo(action.payload.data.individual), - individualsTotalCount: !!action.payload.data.individual ? action.payload.data.individual.totalCount : null, + individualsTotalCount: action.payload.data.individual ? action.payload.data.individual.totalCount : null, errorIndividuals: formatGraphQLError(action.payload), }; case SUCCESS(ACTION_TYPE.GET_INDIVIDUAL): @@ -95,9 +98,9 @@ function reducer( case ERROR(ACTION_TYPE.MUTATION): return dispatchMutationErr(state, action); case SUCCESS(ACTION_TYPE.DELETE_INDIVIDUAL): - return dispatchMutationResp(state, "deleteIndividual", action); + return dispatchMutationResp(state, 'deleteIndividual', action); case SUCCESS(ACTION_TYPE.UPDATE_INDIVIDUAL): - return dispatchMutationResp(state, "updateIndividual", action); + return dispatchMutationResp(state, 'updateIndividual', action); default: return state; } diff --git a/src/util/action-type.js b/src/util/action-type.js index 8a94938..44404e2 100644 --- a/src/util/action-type.js +++ b/src/util/action-type.js @@ -1,3 +1,3 @@ -export const REQUEST = actionTypeName => actionTypeName + "_REQ"; -export const SUCCESS = actionTypeName => actionTypeName + "_RESP"; -export const ERROR = actionTypeName => actionTypeName + "_ERR"; \ No newline at end of file +export const REQUEST = (actionTypeName) => `${actionTypeName}_REQ`; +export const SUCCESS = (actionTypeName) => `${actionTypeName}_RESP`; +export const ERROR = (actionTypeName) => `${actionTypeName}_ERR`; diff --git a/src/util/json-validate.js b/src/util/json-validate.js index d73eb1d..d108ea8 100644 --- a/src/util/json-validate.js +++ b/src/util/json-validate.js @@ -1,4 +1,4 @@ -export const isJsonString = (string) => { +const isJsonString = (string) => { try { JSON.parse(string); } catch (e) { @@ -6,3 +6,5 @@ export const isJsonString = (string) => { } return true; }; + +export default isJsonString; diff --git a/src/util/styles.js b/src/util/styles.js index a484a72..8ebced6 100644 --- a/src/util/styles.js +++ b/src/util/styles.js @@ -1,7 +1,7 @@ export const defaultPageStyles = (theme) => ({ page: theme.page, }); - + export const defaultFilterStyles = (theme) => ({ form: { padding: 0, @@ -10,16 +10,15 @@ export const defaultFilterStyles = (theme) => ({ padding: theme.spacing(1), }, }); - + export const defaultHeadPanelStyles = (theme) => ({ tableTitle: theme.table.title, item: theme.paper.item, fullHeight: { - height: "100%", + height: '100%', }, }); - + export const defaultDialogStyles = (theme) => ({ item: theme.paper.item, }); - \ No newline at end of file From 0b1150e26dbfc1d0c76e334924d612d1e3ba9f33 Mon Sep 17 00:00:00 2001 From: olewandowski1 <109145288+olewandowski1@users.noreply.github.com> Date: Mon, 12 Jun 2023 11:14:54 +0200 Subject: [PATCH 06/90] CM-24: addressing issues and improving user experience of individuals page (#7) * CM-24: prevent erorr when doing actions on bp after the deletion * CM-24: fix resetting search params, add maxDate to the DOB picker * CM-24: system checks if any changes were made --- src/components/IndividualFilter.js | 6 ++++-- src/components/IndividualHeadPanel.js | 2 ++ src/components/IndividualSearcher.js | 4 ++++ src/pages/IndividualPage.js | 24 +++++++++++++++++++++--- 4 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/components/IndividualFilter.js b/src/components/IndividualFilter.js index 2376515..6a848e8 100644 --- a/src/components/IndividualFilter.js +++ b/src/components/IndividualFilter.js @@ -14,6 +14,8 @@ function IndividualFilter({ const filterValue = (filterName) => filters?.[filterName]?.value; + const filterTextFieldValue = (filterName) => filters?.[filterName]?.value ?? ''; + const onChangeStringFilter = (filterName, lookup = null) => (value) => { if (lookup) { debouncedOnChangeFilters([ @@ -40,7 +42,7 @@ function IndividualFilter({ @@ -48,7 +50,7 @@ function IndividualFilter({ diff --git a/src/components/IndividualHeadPanel.js b/src/components/IndividualHeadPanel.js index eb4e9d2..ca4937e 100644 --- a/src/components/IndividualHeadPanel.js +++ b/src/components/IndividualHeadPanel.js @@ -24,6 +24,7 @@ class IndividualHeadPanel extends FormPanel { edited, classes, mandatoryFieldsEmpty, } = this.props; const individual = { ...edited }; + const currentDate = new Date(); return ( <> @@ -79,6 +80,7 @@ class IndividualHeadPanel extends FormPanel { required onChange={(v) => this.updateAttribute('dob', v)} value={individual?.dob} + maxDate={currentDate} /> diff --git a/src/components/IndividualSearcher.js b/src/components/IndividualSearcher.js index 586f5f5..bc17017 100644 --- a/src/components/IndividualSearcher.js +++ b/src/components/IndividualSearcher.js @@ -7,6 +7,7 @@ import { Searcher, formatDateFromISO, coreConfirm, + clearConfirm, journalize, withHistory, historyPush, @@ -32,6 +33,7 @@ function IndividualSearcher({ history, rights, coreConfirm, + clearConfirm, confirmed, journalize, submittingMutation, @@ -83,6 +85,7 @@ function IndividualSearcher({ if (individualToDelete && confirmed !== null) { setIndividualToDelete(null); } + return () => confirmed && clearConfirm(false); }, [confirmed]); useEffect(() => { @@ -205,6 +208,7 @@ const mapDispatchToProps = (dispatch) => bindActionCreators( fetchIndividuals, deleteIndividual, coreConfirm, + clearConfirm, journalize, }, dispatch, diff --git a/src/pages/IndividualPage.js b/src/pages/IndividualPage.js index d3e9623..227b472 100644 --- a/src/pages/IndividualPage.js +++ b/src/pages/IndividualPage.js @@ -6,11 +6,13 @@ import { formatMessage, formatMessageWithValues, coreConfirm, + clearConfirm, journalize, } from '@openimis/fe-core'; import { injectIntl } from 'react-intl'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; +import _ from 'lodash'; import { withTheme, withStyles } from '@material-ui/core/styles'; import DeleteIcon from '@material-ui/icons/Delete'; import { RIGHT_INDIVIDUAL_UPDATE } from '../constants'; @@ -33,6 +35,7 @@ function IndividualPage({ deleteIndividual, updateIndividual, coreConfirm, + clearConfirm, confirmed, submittingMutation, mutation, @@ -48,7 +51,10 @@ function IndividualPage({ } }, [individualUuid]); - useEffect(() => confirmed && confirmedAction(), [confirmed]); + useEffect(() => { + if (confirmed) confirmedAction(); + return () => confirmed && clearConfirm(null); + }, [confirmed]); const back = () => history.goBack(); @@ -86,7 +92,14 @@ function IndividualPage({ return true; }; - const canSave = () => !isMandatoryFieldsEmpty(); + const doesIndividualChange = () => { + if (_.isEqual(individual, editedIndividual)) { + return false; + } + return true; + }; + + const canSave = () => !isMandatoryFieldsEmpty() && doesIndividualChange(); const handleSave = () => { updateIndividual( @@ -166,7 +179,12 @@ const mapStateToProps = (state, props) => ({ }); const mapDispatchToProps = (dispatch) => bindActionCreators({ - fetchIndividual, deleteIndividual, updateIndividual, coreConfirm, journalize, + fetchIndividual, + deleteIndividual, + updateIndividual, + coreConfirm, + clearConfirm, + journalize, }, dispatch); export default withHistory( From 0939cb5d36a259b7ed1aec343fcee21c0729272d Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 12 Jun 2023 13:51:17 +0200 Subject: [PATCH 07/90] CM-106: add benefit plans panel to individual (#8) * CM-106: add benefit plans panel to individual * CM-106: fix eslint * CM-106: fix eslint --- .../IndividualBenefitPlansActiveTab.js | 38 ++++++++++ .../IndividualBenefitPlansGraduatedTab.js | 41 +++++++++++ .../IndividualBenefitPlansListTab.js | 37 ++++++++++ .../IndividualBenefitPlansPotentialTab.js | 41 +++++++++++ .../IndividualBenefitPlansSuspendedTab.js | 38 ++++++++++ src/components/IndividualTabPanel.js | 69 +++++++++++++++++++ src/constants.js | 15 ++++ src/index.js | 34 +++++++++ src/pages/IndividualPage.js | 3 +- src/translations/en.json | 17 ++++- 10 files changed, 331 insertions(+), 2 deletions(-) create mode 100644 src/components/IndividualBenefitPlansActiveTab.js create mode 100644 src/components/IndividualBenefitPlansGraduatedTab.js create mode 100644 src/components/IndividualBenefitPlansListTab.js create mode 100644 src/components/IndividualBenefitPlansPotentialTab.js create mode 100644 src/components/IndividualBenefitPlansSuspendedTab.js create mode 100644 src/components/IndividualTabPanel.js diff --git a/src/components/IndividualBenefitPlansActiveTab.js b/src/components/IndividualBenefitPlansActiveTab.js new file mode 100644 index 0000000..aeb4299 --- /dev/null +++ b/src/components/IndividualBenefitPlansActiveTab.js @@ -0,0 +1,38 @@ +import React from 'react'; +import { Tab } from '@material-ui/core'; +import { formatMessage, PublishedComponent } from '@openimis/fe-core'; +import { BENEFICIARY_STATUS, INDIVIDUAL_BENEFIT_PLANS_ACTIVE_TAB_VALUE } from '../constants'; + +function IndividualBenefitPlansActiveTabLabel({ + intl, onChange, tabStyle, isSelected, +}) { + return ( + + ); +} + +function IndividualBenefitPlansActiveTabPanel({ value, rights, individual }) { + return ( + + + + ); +} + +export { IndividualBenefitPlansActiveTabLabel, IndividualBenefitPlansActiveTabPanel }; diff --git a/src/components/IndividualBenefitPlansGraduatedTab.js b/src/components/IndividualBenefitPlansGraduatedTab.js new file mode 100644 index 0000000..41fc961 --- /dev/null +++ b/src/components/IndividualBenefitPlansGraduatedTab.js @@ -0,0 +1,41 @@ +import React from 'react'; +import { Tab } from '@material-ui/core'; +import { formatMessage, PublishedComponent } from '@openimis/fe-core'; +import { + BENEFICIARY_STATUS, + INDIVIDUAL_BENEFIT_PLANS_GRADUATED_TAB_VALUE, +} from '../constants'; + +function IndividualBenefitPlansGraduatedTabLabel({ + intl, onChange, tabStyle, isSelected, +}) { + return ( + + ); +} + +function IndividualBenefitPlansGraduatedTabPanel({ value, rights, individual }) { + return ( + + + + ); +} + +export { IndividualBenefitPlansGraduatedTabLabel, IndividualBenefitPlansGraduatedTabPanel }; diff --git a/src/components/IndividualBenefitPlansListTab.js b/src/components/IndividualBenefitPlansListTab.js new file mode 100644 index 0000000..e757e76 --- /dev/null +++ b/src/components/IndividualBenefitPlansListTab.js @@ -0,0 +1,37 @@ +import React from 'react'; +import { Tab } from '@material-ui/core'; +import { formatMessage, PublishedComponent } from '@openimis/fe-core'; +import { INDIVIDUAL_BENEFIT_PLANS_LIST_TAB_VALUE } from '../constants'; + +function IndividualBenefitPlansListTabLabel({ + intl, onChange, tabStyle, isSelected, +}) { + return ( + + ); +} + +function IndividualBenefitPlansListTabPanel({ value, rights, individual }) { + return ( + + + + ); +} + +export { IndividualBenefitPlansListTabLabel, IndividualBenefitPlansListTabPanel }; diff --git a/src/components/IndividualBenefitPlansPotentialTab.js b/src/components/IndividualBenefitPlansPotentialTab.js new file mode 100644 index 0000000..91fc037 --- /dev/null +++ b/src/components/IndividualBenefitPlansPotentialTab.js @@ -0,0 +1,41 @@ +import React from 'react'; +import { Tab } from '@material-ui/core'; +import { formatMessage, PublishedComponent } from '@openimis/fe-core'; +import { + BENEFICIARY_STATUS, + INDIVIDUAL_BENEFIT_PLANS_POTENTIAL_TAB_VALUE, +} from '../constants'; + +function IndividualBenefitPlansPotentialTabLabel({ + intl, onChange, tabStyle, isSelected, +}) { + return ( + + ); +} + +function IndividualBenefitPlansPotentialTabPanel({ value, rights, individual }) { + return ( + + + + ); +} + +export { IndividualBenefitPlansPotentialTabLabel, IndividualBenefitPlansPotentialTabPanel }; diff --git a/src/components/IndividualBenefitPlansSuspendedTab.js b/src/components/IndividualBenefitPlansSuspendedTab.js new file mode 100644 index 0000000..05f98ee --- /dev/null +++ b/src/components/IndividualBenefitPlansSuspendedTab.js @@ -0,0 +1,38 @@ +import React from 'react'; +import { Tab } from '@material-ui/core'; +import { formatMessage, PublishedComponent } from '@openimis/fe-core'; +import { BENEFICIARY_STATUS, INDIVIDUAL_BENEFIT_PLANS_SUSPENDED_TAB_VALUE } from '../constants'; + +function IndividualBenefitPlansSuspendedTabLabel({ + intl, onChange, tabStyle, isSelected, +}) { + return ( + + ); +} + +function IndividualBenefitPlansSuspendedTabPanel({ value, rights, individual }) { + return ( + + + + ); +} + +export { IndividualBenefitPlansSuspendedTabLabel, IndividualBenefitPlansSuspendedTabPanel }; diff --git a/src/components/IndividualTabPanel.js b/src/components/IndividualTabPanel.js new file mode 100644 index 0000000..6d072fd --- /dev/null +++ b/src/components/IndividualTabPanel.js @@ -0,0 +1,69 @@ +import React, { useState } from 'react'; +import { Paper, Grid } from '@material-ui/core'; +import { Contributions } from '@openimis/fe-core'; +import { injectIntl } from 'react-intl'; +import { withTheme, withStyles } from '@material-ui/core/styles'; +import { + INDIVIDUAL_BENEFIT_PLANS_LIST_TAB_VALUE, + INDIVIDUAL_TABS_LABEL_CONTRIBUTION_KEY, + INDIVIDUAL_TABS_PANEL_CONTRIBUTION_KEY, +} from '../constants'; + +const styles = (theme) => ({ + paper: theme.paper.paper, + tableTitle: theme.table.title, + tabs: { + display: 'flex', + alignItems: 'center', + }, + selectedTab: { + borderBottom: '4px solid white', + }, + unselectedTab: { + borderBottom: '4px solid transparent', + }, + button: { + marginLeft: 'auto', + padding: theme.spacing(1), + fontSize: '0.875rem', + textTransform: 'none', + }, +}); + +function IndividualTabPanel({ + intl, rights, classes, individual, beneficiaryStatus, setConfirmedAction, +}) { + const [activeTab, setActiveTab] = useState(INDIVIDUAL_BENEFIT_PLANS_LIST_TAB_VALUE); + + const isSelected = (tab) => tab === activeTab; + + const tabStyle = (tab) => (isSelected(tab) ? classes.selectedTab : classes.unselectedTab); + + const handleChange = (_, tab) => setActiveTab(tab); + + return ( + + + + + + + ); +} + +export default injectIntl(withTheme(withStyles(styles)(IndividualTabPanel))); diff --git a/src/constants.js b/src/constants.js index 3e59a32..a8326d8 100644 --- a/src/constants.js +++ b/src/constants.js @@ -9,3 +9,18 @@ export const RIGHT_INDIVIDUAL_SEARCH = 159001; export const RIGHT_INDIVIDUAL_CREATE = 159002; export const RIGHT_INDIVIDUAL_UPDATE = 159003; export const RIGHT_INDIVIDUAL_DELETE = 159004; + +export const INDIVIDUAL_BENEFIT_PLANS_LIST_TAB_VALUE = 'individualBenefitPlansListTab'; +export const INDIVIDUAL_BENEFIT_PLANS_ACTIVE_TAB_VALUE = 'individualBenefitPlansActiveTab'; +export const INDIVIDUAL_BENEFIT_PLANS_POTENTIAL_TAB_VALUE = 'individualBenefitPlansPotentialTab'; +export const INDIVIDUAL_BENEFIT_PLANS_GRADUATED_TAB_VALUE = 'individualBenefitPlansGraduatedTab'; +export const INDIVIDUAL_BENEFIT_PLANS_SUSPENDED_TAB_VALUE = 'individualBenefitPlansSuspendedTab'; +export const INDIVIDUAL_TABS_LABEL_CONTRIBUTION_KEY = 'individual.TabPanel.label'; +export const INDIVIDUAL_TABS_PANEL_CONTRIBUTION_KEY = 'individual.TabPanel.panel'; + +export const BENEFICIARY_STATUS = { + POTENTIAL: 'POTENTIAL', + ACTIVE: 'ACTIVE', + GRADUATED: 'GRADUATED', + SUSPENDED: 'SUSPENDED', +}; diff --git a/src/index.js b/src/index.js index 06515c5..f566690 100644 --- a/src/index.js +++ b/src/index.js @@ -7,6 +7,26 @@ import reducer from './reducer'; import BeneficiaryMainMenu from './menus/BeneficiaryMainMenu'; import IndividualsPage from './pages/IndividualsPage'; import IndividualPage from './pages/IndividualPage'; +import { + IndividualBenefitPlansListTabLabel, + IndividualBenefitPlansListTabPanel, +} from './components/IndividualBenefitPlansListTab'; +import { + IndividualBenefitPlansActiveTabLabel, + IndividualBenefitPlansActiveTabPanel, +} from './components/IndividualBenefitPlansActiveTab'; +import { + IndividualBenefitPlansGraduatedTabLabel, + IndividualBenefitPlansGraduatedTabPanel, +} from './components/IndividualBenefitPlansGraduatedTab'; +import { + IndividualBenefitPlansPotentialTabLabel, + IndividualBenefitPlansPotentialTabPanel, +} from './components/IndividualBenefitPlansPotentialTab'; +import { + IndividualBenefitPlansSuspendedTabLabel, + IndividualBenefitPlansSuspendedTabPanel, +} from './components/IndividualBenefitPlansSuspendedTab'; const ROUTE_INDIVIDUALS = 'individuals'; const ROUTE_INDIVIDUAL = 'individuals/individual'; @@ -22,6 +42,20 @@ const DEFAULT_CONFIG = { refs: [ { key: 'individual.route.individual', ref: ROUTE_INDIVIDUAL }, ], + 'individual.TabPanel.label': [ + IndividualBenefitPlansListTabLabel, + IndividualBenefitPlansActiveTabLabel, + IndividualBenefitPlansGraduatedTabLabel, + IndividualBenefitPlansPotentialTabLabel, + IndividualBenefitPlansSuspendedTabLabel, + ], + 'individual.TabPanel.panel': [ + IndividualBenefitPlansListTabPanel, + IndividualBenefitPlansActiveTabPanel, + IndividualBenefitPlansGraduatedTabPanel, + IndividualBenefitPlansPotentialTabPanel, + IndividualBenefitPlansSuspendedTabPanel, + ], }; export const IndividualModule = (cfg) => ({ ...DEFAULT_CONFIG, ...cfg }); diff --git a/src/pages/IndividualPage.js b/src/pages/IndividualPage.js index 227b472..ffa9ede 100644 --- a/src/pages/IndividualPage.js +++ b/src/pages/IndividualPage.js @@ -18,6 +18,7 @@ import DeleteIcon from '@material-ui/icons/Delete'; import { RIGHT_INDIVIDUAL_UPDATE } from '../constants'; import { fetchIndividual, deleteIndividual, updateIndividual } from '../actions'; import IndividualHeadPanel from '../components/IndividualHeadPanel'; +import IndividualTabPanel from '../components/IndividualTabPanel'; import { ACTION_TYPE } from '../reducer'; const styles = (theme) => ({ @@ -155,7 +156,7 @@ function IndividualPage({ canSave={canSave} save={handleSave} HeadPanel={IndividualHeadPanel} - Panels={[]} + Panels={[IndividualTabPanel]} rights={rights} actions={actions} setConfirmedAction={setConfirmedAction} diff --git a/src/translations/en.json b/src/translations/en.json index 4393ac1..bbd1dc8 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -31,7 +31,22 @@ "mutationLabel":"Update Individual {firstName} {lastName}" }, "saveButton.tooltip.enabled": "Save changes", - "saveButton.tooltip.disabled": "Please fill General Information fields first" + "saveButton.tooltip.disabled": "Please fill General Information fields first", + "benefitPlansList": { + "label": "LIST" + }, + "benefitPlansActive": { + "label": "ACTIVE" + }, + "benefitPlansPotential": { + "label": "POTENTIAL" + }, + "benefitPlansGraduated": { + "label": "GRADUATED" + }, + "benefitPlansSuspended": { + "label": "SUSPENDED" + } }, "individuals": { "pageTitle": "Individuals", From 89bf417d2e92fe05b7942cafa17eadf725d7b81a Mon Sep 17 00:00:00 2001 From: Damian Borowiecki Date: Mon, 12 Jun 2023 16:25:25 +0200 Subject: [PATCH 08/90] Added coreMIS deployment --- .../workflows/core-mis-test-server-deploy.yml | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/core-mis-test-server-deploy.yml diff --git a/.github/workflows/core-mis-test-server-deploy.yml b/.github/workflows/core-mis-test-server-deploy.yml new file mode 100644 index 0000000..be43203 --- /dev/null +++ b/.github/workflows/core-mis-test-server-deploy.yml @@ -0,0 +1,32 @@ +name: CoreMIS Server Deployment +on: + push: + branches: + - develop + +jobs: + rebuild-test-server: + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v2 + + - name: Set up SSH + run: | + mkdir -p ~/.ssh + echo "${{ secrets.CORE_MIS_DEPLOYMENT_SSH_KEY }}" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + ssh-keyscan -H ${{ secrets.CORE_MIS_DEPLOYMENT_HOST }} >> ~/.ssh/known_hosts + env: + CORE_MIS_DEPLOYMENT_SSH_KEY: ${{ secrets.CORE_MIS_DEPLOYMENT_SSH_KEY }} + CORE_MIS_DEPLOYMENT_USER: ${{ secrets.CORE_MIS_DEPLOYMENT_USER }} + CORE_MIS_DEPLOYMENT_HOST: ${{ secrets.CORE_MIS_DEPLOYMENT_HOST }} + + - name: Run Docker Compose + run: | + ssh -o StrictHostKeyChecking=no -T ${{ secrets.CORE_MIS_DEPLOYMENT_USER }}@${{ secrets.CORE_MIS_DEPLOYMENT_HOST }} -p 1022 + ssh ${{ secrets.CORE_MIS_DEPLOYMENT_USER }}@${{ secrets.CORE_MIS_DEPLOYMENT_HOST }} -p 1022 << EOF + cd coreMIS/ + docker-compose build backend gateway && docker-compose up -d + EOF + \ No newline at end of file From 9cd49e58f23667d0d31dae5b3deaf2c787cea7ce Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 12 Jun 2023 17:30:22 +0200 Subject: [PATCH 09/90] CM-114: add groups display page (#6) * CM-114: fix eslint * CM-114: remove unused import --- rollup.config.js | 30 +++---- src/actions.js | 13 +++ src/components/GroupFilter.js | 67 ++++++++++++++++ src/components/GroupSearcher.js | 131 +++++++++++++++++++++++++++++++ src/constants.js | 4 + src/index.js | 6 ++ src/menus/BeneficiaryMainMenu.js | 7 +- src/pages/GroupPage.js | 0 src/pages/GroupsPage.js | 31 ++++++++ src/reducer.js | 36 +++++++++ src/translations/en.json | 12 ++- 11 files changed, 320 insertions(+), 17 deletions(-) create mode 100644 src/components/GroupFilter.js create mode 100644 src/components/GroupSearcher.js create mode 100644 src/pages/GroupPage.js create mode 100644 src/pages/GroupsPage.js diff --git a/rollup.config.js b/rollup.config.js index e9c148c..8c73fdd 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,6 +1,6 @@ -import babel from '@rollup/plugin-babel' -import json from '@rollup/plugin-json' -import pkg from './package.json' +import babel from '@rollup/plugin-babel'; +import json from '@rollup/plugin-json'; +import pkg from './package.json'; export default { input: 'src/index.js', @@ -8,33 +8,33 @@ export default { { file: pkg.module, format: 'es', - sourcemap: true + sourcemap: true, }, { file: 'dist/index.js', format: 'cjs', - sourcemap: true - } + sourcemap: true, + }, ], external: [ /^@babel.*/, /^@date-io\/.*/, /^@material-ui\/.*/, /^@openimis.*/, - "classnames", - "clsx", - "history", + 'classnames', + 'clsx', + 'history', /^lodash.*/, - "moment", - "prop-types", + 'moment', + 'prop-types', /^react.*/, - /^redux.*/ + /^redux.*/, ], plugins: [ json(), babel({ exclude: 'node_modules/**', - babelHelpers: 'runtime' + babelHelpers: 'runtime', }), - ] -} \ No newline at end of file + ], +}; diff --git a/src/actions.js b/src/actions.js index f9ab087..f88edbc 100644 --- a/src/actions.js +++ b/src/actions.js @@ -19,11 +19,24 @@ const INDIVIDUAL_FULL_PROJECTION = [ 'jsonExt', ]; +const GROUP_FULL_PROJECTION = [ + 'id', + 'isDeleted', + 'dateCreated', + 'dateUpdated', + 'jsonExt', +]; + export function fetchIndividuals(params) { const payload = formatPageQueryWithCount('individual', params, INDIVIDUAL_FULL_PROJECTION); return graphql(payload, ACTION_TYPE.SEARCH_INDIVIDUALS); } +export function fetchGroups(params) { + const payload = formatPageQueryWithCount('group', params, GROUP_FULL_PROJECTION); + return graphql(payload, ACTION_TYPE.SEARCH_GROUPS); +} + export function fetchIndividual(params) { const payload = formatPageQuery('individual', params, INDIVIDUAL_FULL_PROJECTION); return graphql(payload, ACTION_TYPE.GET_INDIVIDUAL); diff --git a/src/components/GroupFilter.js b/src/components/GroupFilter.js new file mode 100644 index 0000000..14f0a1e --- /dev/null +++ b/src/components/GroupFilter.js @@ -0,0 +1,67 @@ +import React from 'react'; +import { injectIntl } from 'react-intl'; +import { TextInput } from '@openimis/fe-core'; +import { Grid } from '@material-ui/core'; +import { withTheme, withStyles } from '@material-ui/core/styles'; +import _debounce from 'lodash/debounce'; +import { CONTAINS_LOOKUP, DEFAULT_DEBOUNCE_TIME } from '../constants'; +import { defaultFilterStyles } from '../util/styles'; + +function GroupFilter({ + classes, filters, onChangeFilters, +}) { + const debouncedOnChangeFilters = _debounce(onChangeFilters, DEFAULT_DEBOUNCE_TIME); + + const filterValue = (filterName) => filters?.[filterName]?.value; + + 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 injectIntl(withTheme(withStyles(defaultFilterStyles)(GroupFilter))); diff --git a/src/components/GroupSearcher.js b/src/components/GroupSearcher.js new file mode 100644 index 0000000..3f7a3b1 --- /dev/null +++ b/src/components/GroupSearcher.js @@ -0,0 +1,131 @@ +import React from 'react'; +import { injectIntl } from 'react-intl'; +import { + withModulesManager, + formatMessage, + formatMessageWithValues, + Searcher, + withHistory, + historyPush, +} from '@openimis/fe-core'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import { IconButton, Tooltip } from '@material-ui/core'; +import EditIcon from '@material-ui/icons/Edit'; +import { fetchGroups } from '../actions'; +import { + DEFAULT_PAGE_SIZE, + ROWS_PER_PAGE_OPTIONS, + RIGHT_GROUP_UPDATE, +} from '../constants'; +import GroupFilter from './GroupFilter'; + +function GroupSearcher({ + intl, + modulesManager, + history, + rights, + fetchGroups, + fetchingGroups, + fetchedGroups, + errorGroups, + groups, + groupsPageInfo, + groupsTotalCount, +}) { + function groupUpdatePageUrl(group) { + return `${modulesManager.getRef('individual.route.group')}/${group?.id}`; + } + + const onDoubleClick = (group, newTab = false) => rights.includes(RIGHT_GROUP_UPDATE) + && historyPush(modulesManager, history, 'individual.route.group', [group?.id], newTab); + + const fetch = (params) => fetchGroups(params); + + const headers = () => { + const headers = [ + 'group.id', + ]; + if (rights.includes(RIGHT_GROUP_UPDATE)) { + headers.push('emptyLabel'); + } + return headers; + }; + + const itemFormatters = () => { + const formatters = [ + (group) => group.id, + ]; + if (rights.includes(RIGHT_GROUP_UPDATE)) { + formatters.push((group) => ( + + e.stopPropagation() && onDoubleClick(group)} + > + + + + )); + } + return formatters; + }; + + const rowIdentifier = (group) => group.id; + + const sorts = () => [ + ['id', false], + ]; + + const defaultFilters = () => ({ + isDeleted: { + value: false, + filter: 'isDeleted: false', + }, + }); + + return ( + + ); +} + +const mapStateToProps = (state) => ({ + fetchingGroups: state.individual.fetchingGroups, + fetchedGroups: state.individual.fetchedGroups, + errorGroups: state.individual.errorGroups, + groups: state.individual.groups, + groupsPageInfo: state.individual.groupsPageInfo, + groupsTotalCount: state.groupsTotalCount, +}); + +const mapDispatchToProps = (dispatch) => bindActionCreators( + { + fetchGroups, + }, + dispatch, +); + +export default withHistory( + withModulesManager(injectIntl(connect(mapStateToProps, mapDispatchToProps)(GroupSearcher))), +); diff --git a/src/constants.js b/src/constants.js index a8326d8..a5b57af 100644 --- a/src/constants.js +++ b/src/constants.js @@ -9,6 +9,10 @@ export const RIGHT_INDIVIDUAL_SEARCH = 159001; export const RIGHT_INDIVIDUAL_CREATE = 159002; export const RIGHT_INDIVIDUAL_UPDATE = 159003; export const RIGHT_INDIVIDUAL_DELETE = 159004; +export const RIGHT_GROUP_SEARCH = 180001; +export const RIGHT_GROUP_CREATE = 180002; +export const RIGHT_GROUP_UPDATE = 180003; +export const RIGHT_GROUP_DELETE = 180004; export const INDIVIDUAL_BENEFIT_PLANS_LIST_TAB_VALUE = 'individualBenefitPlansListTab'; export const INDIVIDUAL_BENEFIT_PLANS_ACTIVE_TAB_VALUE = 'individualBenefitPlansActiveTab'; diff --git a/src/index.js b/src/index.js index f566690..ab1c660 100644 --- a/src/index.js +++ b/src/index.js @@ -27,9 +27,12 @@ import { IndividualBenefitPlansSuspendedTabLabel, IndividualBenefitPlansSuspendedTabPanel, } from './components/IndividualBenefitPlansSuspendedTab'; +import GroupsPage from './pages/GroupsPage'; const ROUTE_INDIVIDUALS = 'individuals'; const ROUTE_INDIVIDUAL = 'individuals/individual'; +const ROUTE_GROUPS = 'groups'; +// const ROUTE_GROUP = 'groups/group'; const DEFAULT_CONFIG = { translations: [{ key: 'en', messages: flatten(messages_en) }], @@ -37,10 +40,13 @@ const DEFAULT_CONFIG = { 'core.MainMenu': [BeneficiaryMainMenu], 'core.Router': [ { path: ROUTE_INDIVIDUALS, component: IndividualsPage }, + { path: ROUTE_GROUPS, component: GroupsPage }, { path: `${ROUTE_INDIVIDUAL}/:individual_uuid?`, component: IndividualPage }, + // { path: `${ROUTE_GROUP}/:group_uuid?`, component: GroupPage }, ], refs: [ { key: 'individual.route.individual', ref: ROUTE_INDIVIDUAL }, + // { key: 'individual.route.group', ref: ROUTE_GROUP }, ], 'individual.TabPanel.label': [ IndividualBenefitPlansListTabLabel, diff --git a/src/menus/BeneficiaryMainMenu.js b/src/menus/BeneficiaryMainMenu.js index c146677..5424b3a 100644 --- a/src/menus/BeneficiaryMainMenu.js +++ b/src/menus/BeneficiaryMainMenu.js @@ -5,7 +5,7 @@ import React from 'react'; import { injectIntl } from 'react-intl'; import { connect } from 'react-redux'; -import { Person } from '@material-ui/icons'; +import { Person, People } from '@material-ui/icons'; import { formatMessage, MainMenuContribution, withModulesManager } from '@openimis/fe-core'; import { BENEFICIARY_MAIN_MENU_CONTRIBUTION_KEY } from '../constants'; @@ -16,6 +16,11 @@ function BeneficiaryMainMenu(props) { icon: , route: '/individuals', }, + { + text: formatMessage(props.intl, 'individual', 'menu.groups'), + icon: , + route: '/groups', + }, ]; entries.push( ...props.modulesManager diff --git a/src/pages/GroupPage.js b/src/pages/GroupPage.js new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/GroupsPage.js b/src/pages/GroupsPage.js new file mode 100644 index 0000000..20bf949 --- /dev/null +++ b/src/pages/GroupsPage.js @@ -0,0 +1,31 @@ +import React from 'react'; +import { Helmet, withModulesManager, formatMessage } from '@openimis/fe-core'; +import { injectIntl } from 'react-intl'; +import { withTheme, withStyles } from '@material-ui/core/styles'; +import { connect } from 'react-redux'; +import { RIGHT_GROUP_SEARCH } from '../constants'; +import GroupSearcher from '../components/GroupSearcher'; + +const styles = (theme) => ({ + page: theme.page, + fab: theme.fab, +}); + +function GroupsPage(props) { + const { intl, classes, rights } = props; + + return ( + rights.includes(RIGHT_GROUP_SEARCH) && ( +
+ + +
+ ) + ); +} + +const mapStateToProps = (state) => ({ + rights: !!state.core && !!state.core.user && !!state.core.user.i_user ? state.core.user.i_user.rights : [], +}); + +export default withModulesManager(injectIntl(withTheme(withStyles(styles)(connect(mapStateToProps)(GroupsPage))))); diff --git a/src/reducer.js b/src/reducer.js index e6aa80a..a70d101 100644 --- a/src/reducer.js +++ b/src/reducer.js @@ -19,6 +19,7 @@ export const ACTION_TYPE = { GET_INDIVIDUAL: 'INDIVIDUAL_INDIVIDUAL', DELETE_INDIVIDUAL: 'INDIVIDUAL_DELETE_INDIVIDUAL', UPDATE_INDIVIDUAL: 'INDIVIDUAL_UPDATE_INDIVIDUAL', + SEARCH_GROUPS: 'GROUP_GROUPS', }; function reducer( @@ -35,6 +36,12 @@ function reducer( errorIndividual: null, fetchedIndividual: false, individual: null, + fetchingGroups: false, + errorGroups: null, + fetchedGroups: false, + groups: [], + groupsPageInfo: {}, + groupsTotalCount: 0, }, action, ) { @@ -49,6 +56,16 @@ function reducer( individualsTotalCount: 0, errorIndividuals: null, }; + case REQUEST(ACTION_TYPE.SEARCH_GROUPS): + return { + ...state, + fetchingGroups: true, + fetchedGroups: false, + groups: [], + groupsPageInfo: {}, + groupsTotalCount: 0, + errorGroups: null, + }; case REQUEST(ACTION_TYPE.GET_INDIVIDUAL): return { ...state, @@ -70,6 +87,19 @@ function reducer( individualsTotalCount: action.payload.data.individual ? action.payload.data.individual.totalCount : null, errorIndividuals: formatGraphQLError(action.payload), }; + case SUCCESS(ACTION_TYPE.SEARCH_GROUPS): + return { + ...state, + fetchingGroups: false, + fetchedGroups: true, + groups: parseData(action.payload.data.group)?.map((group) => ({ + ...group, + id: decodeId(group.id), + })), + groupsPageInfo: pageInfo(action.payload.data.individual), + groupsTotalCount: action.payload.data.group ? action.payload.data.group.totalCount : null, + errorGroups: formatGraphQLError(action.payload), + }; case SUCCESS(ACTION_TYPE.GET_INDIVIDUAL): return { ...state, @@ -87,6 +117,12 @@ function reducer( fetchingIndividuals: false, errorIndividuals: formatServerError(action.payload), }; + case ERROR(ACTION_TYPE.SEARCH_GROUPS): + return { + ...state, + fetchingGroups: false, + errorGroups: formatServerError(action.payload), + }; case ERROR(ACTION_TYPE.GET_INDIVIDUAL): return { ...state, diff --git a/src/translations/en.json b/src/translations/en.json index bbd1dc8..1210f97 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -10,7 +10,8 @@ "cancel": "Cancel" }, "menu": { - "individuals": "Individuals" + "individuals": "Individuals", + "groups": "Groups" }, "individual": { "pageTitle": "Individual {firstName} {lastName}", @@ -51,5 +52,14 @@ "individuals": { "pageTitle": "Individuals", "searcherResultsTitle": "{individualsTotalCount} Individuals Found" + }, + "groups": { + "pageTitle": "Groups", + "searcherResultsTitle": "{groupsTotalCount} Groups Found" + }, + "group": { + "id": "ID", + "individual.firstName": "First Name", + "individual.lastName": "Last Name" } } \ No newline at end of file From 6829e9a22c1ca23ac2de56748572eaad6f44c51c Mon Sep 17 00:00:00 2001 From: jdolkowski Date: Wed, 14 Jun 2023 15:22:29 +0200 Subject: [PATCH 10/90] CM-105: add export funcionality to groups --- src/actions.js | 8 +++ src/components/GroupSearcher.js | 114 +++++++++++++++++++++++++------- src/reducer.js | 30 +++++++++ 3 files changed, 127 insertions(+), 25 deletions(-) diff --git a/src/actions.js b/src/actions.js index f88edbc..e18272e 100644 --- a/src/actions.js +++ b/src/actions.js @@ -85,3 +85,11 @@ export function updateIndividual(individual, clientMutationLabel) { }, ); } + +export function downloadGroups(params) { + const payload = ` + { + groupsExport${!!params && params.length ? `(${params.join(',')})` : ''} + }`; + return graphql(payload, ACTION_TYPE.GROUP_EXPORT); +} diff --git a/src/components/GroupSearcher.js b/src/components/GroupSearcher.js index 3f7a3b1..e6f7329 100644 --- a/src/components/GroupSearcher.js +++ b/src/components/GroupSearcher.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { injectIntl } from 'react-intl'; import { withModulesManager, @@ -7,12 +7,17 @@ import { Searcher, withHistory, historyPush, + Dialog, + DialogActions, + DialogTitle, + downloadExport, + Button, } from '@openimis/fe-core'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import { IconButton, Tooltip } from '@material-ui/core'; import EditIcon from '@material-ui/icons/Edit'; -import { fetchGroups } from '../actions'; +import { downloadGroups, fetchGroups } from '../actions'; import { DEFAULT_PAGE_SIZE, ROWS_PER_PAGE_OPTIONS, @@ -32,6 +37,10 @@ function GroupSearcher({ groups, groupsPageInfo, groupsTotalCount, + downloadGroups, + groupsExport, + errorGroupsExport, + }) { function groupUpdatePageUrl(group) { return `${modulesManager.getRef('individual.route.group')}/${group?.id}`; @@ -84,30 +93,78 @@ function GroupSearcher({ }, }); - return ( - { + setFailedExport(true); + }, [errorGroupsExport]); + + useEffect(() => { + if (groupsExport) { + downloadExport(groupsExport, `${formatMessage(intl, 'socialProtection', 'export.filename')}.csv`)(); + } + }, [groupsExport]); + + const groupBeneficiaryFilter = (props) => ( + ); + + return ( +
+ + {failedExport && ( + + {errorGroupsExport} + + + + + )} +
+ ); } const mapStateToProps = (state) => ({ @@ -116,12 +173,19 @@ const mapStateToProps = (state) => ({ errorGroups: state.individual.errorGroups, groups: state.individual.groups, groupsPageInfo: state.individual.groupsPageInfo, - groupsTotalCount: state.groupsTotalCount, + groupsTotalCount: state.individual.groupsTotalCount, + selectedFilters: state.core.filtersCache.groupsFilterCache, + fetchingGroupsExport: state.individual.fetchingGroupsExport, + fetchedGroupsExport: state.individual.fetchedGroupsExport, + groupsExport: state.individual.groupsExport, + groupsExportPageInfo: state.individual.groupsExportPageInfo, + errorGroupsExport: state.individual.errorGroupsExport, }); const mapDispatchToProps = (dispatch) => bindActionCreators( { fetchGroups, + downloadGroups, }, dispatch, ); diff --git a/src/reducer.js b/src/reducer.js index a70d101..d89a2d1 100644 --- a/src/reducer.js +++ b/src/reducer.js @@ -20,6 +20,7 @@ export const ACTION_TYPE = { DELETE_INDIVIDUAL: 'INDIVIDUAL_DELETE_INDIVIDUAL', UPDATE_INDIVIDUAL: 'INDIVIDUAL_UPDATE_INDIVIDUAL', SEARCH_GROUPS: 'GROUP_GROUPS', + GROUP_EXPORT: 'GROUP_EXPORT', }; function reducer( @@ -42,6 +43,11 @@ function reducer( groups: [], groupsPageInfo: {}, groupsTotalCount: 0, + fetchingGroupBsExport: true, + fetchedGroupsExport: false, + groupsExport: null, + groupsExportPageInfo: {}, + errorGroupsExport: null, }, action, ) { @@ -129,6 +135,30 @@ function reducer( fetchingIndividual: false, errorIndividual: formatServerError(action.payload), }; + case REQUEST(ACTION_TYPE.GROUP_EXPORT): + return { + ...state, + fetchingGroupsExport: true, + fetchedGroupsExport: false, + groupsExport: null, + groupsExportPageInfo: {}, + errorGroupsExport: null, + }; + case SUCCESS(ACTION_TYPE.GROUP_EXPORT): + return { + ...state, + fetchingGroupsExport: false, + fetchedGroupsExport: true, + groupsExport: action.payload.data.groupsExport, + groupsExportPageInfo: pageInfo(action.payload.data.groupsExportPageInfo), + errorGroupsExport: formatGraphQLError(action.payload), + }; + case ERROR(ACTION_TYPE.GROUP_EXPORT): + return { + ...state, + fetchingGroupsExport: false, + errorGroupsExport: formatServerError(action.payload), + }; case REQUEST(ACTION_TYPE.MUTATION): return dispatchMutationReq(state, action); case ERROR(ACTION_TYPE.MUTATION): From 12ca3cda6378f3edb996179b18efd66c25f07a2c Mon Sep 17 00:00:00 2001 From: jdolkowski Date: Wed, 14 Jun 2023 20:36:36 +0200 Subject: [PATCH 11/90] CM-105: fix imports --- src/components/GroupSearcher.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/GroupSearcher.js b/src/components/GroupSearcher.js index e6f7329..025213e 100644 --- a/src/components/GroupSearcher.js +++ b/src/components/GroupSearcher.js @@ -7,15 +7,16 @@ import { Searcher, withHistory, historyPush, - Dialog, - DialogActions, - DialogTitle, downloadExport, - Button, } from '@openimis/fe-core'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; -import { IconButton, Tooltip } from '@material-ui/core'; +import { + IconButton, Tooltip, Button, + Dialog, + DialogActions, + DialogTitle, +} from '@material-ui/core'; import EditIcon from '@material-ui/icons/Edit'; import { downloadGroups, fetchGroups } from '../actions'; import { From 98c7b2e074c072045988307236e5474aa1ce06a3 Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 15 Jun 2023 09:53:34 +0200 Subject: [PATCH 12/90] CM-108: add group detail view (#10) * CM-106: add benefit plans panel to individual * CM-106: fix eslint * CM-106: fix eslint * CM-108: initial group detail view * CM-106: update view to mockup * CM-106: remove unused files * CM-108: add droup detail view * CM-108: update group detail view --- src/actions.js | 36 ++++ ...PlansListTab.js => BenefitPlansListTab.js} | 19 +- src/components/GroupHeadPanel.js | 70 +++++++ .../IndividualBenefitPlansActiveTab.js | 38 ---- .../IndividualBenefitPlansGraduatedTab.js | 41 ---- .../IndividualBenefitPlansPotentialTab.js | 41 ---- .../IndividualBenefitPlansSuspendedTab.js | 38 ---- src/components/IndividualSearcher.js | 22 ++- src/components/IndividualTabPanel.js | 12 +- src/components/IndividualsListTab.js | 46 +++++ src/constants.js | 8 +- src/index.js | 46 ++--- src/pages/GroupPage.js | 187 ++++++++++++++++++ src/reducer.js | 38 +++- src/translations/en.json | 13 +- 15 files changed, 437 insertions(+), 218 deletions(-) rename src/components/{IndividualBenefitPlansListTab.js => BenefitPlansListTab.js} (54%) create mode 100644 src/components/GroupHeadPanel.js delete mode 100644 src/components/IndividualBenefitPlansActiveTab.js delete mode 100644 src/components/IndividualBenefitPlansGraduatedTab.js delete mode 100644 src/components/IndividualBenefitPlansPotentialTab.js delete mode 100644 src/components/IndividualBenefitPlansSuspendedTab.js create mode 100644 src/components/IndividualsListTab.js diff --git a/src/actions.js b/src/actions.js index f88edbc..f21294b 100644 --- a/src/actions.js +++ b/src/actions.js @@ -42,6 +42,11 @@ export function fetchIndividual(params) { return graphql(payload, ACTION_TYPE.GET_INDIVIDUAL); } +export function fetchGroup(params) { + const payload = formatPageQuery('group', params, GROUP_FULL_PROJECTION); + return graphql(payload, ACTION_TYPE.GET_GROUP); +} + export function deleteIndividual(individual, clientMutationLabel) { const individualUuids = `ids: ["${individual?.id}"]`; const mutation = formatMutation('deleteIndividual', individualUuids, clientMutationLabel); @@ -58,6 +63,22 @@ export function deleteIndividual(individual, clientMutationLabel) { ); } +export function deleteGroup(group, clientMutationLabel) { + const groupUuids = `ids: ["${group?.id}"]`; + const mutation = formatMutation('deleteGroup', groupUuids, clientMutationLabel); + const requestedDateTime = new Date(); + return graphql( + mutation.payload, + [REQUEST(ACTION_TYPE.MUTATION), SUCCESS(ACTION_TYPE.DELETE_GROUP), ERROR(ACTION_TYPE.MUTATION)], + { + actionType: ACTION_TYPE.DELETE_GROUP, + clientMutationId: mutation.clientMutationId, + clientMutationLabel, + requestedDateTime, + }, + ); +} + function dateTimeToDate(date) { return date.split('T')[0]; } @@ -85,3 +106,18 @@ export function updateIndividual(individual, clientMutationLabel) { }, ); } + +export function updateGroup(group, clientMutationLabel) { + const mutation = formatMutation('updateGroup', formatIndividualGQL(group), clientMutationLabel); + const requestedDateTime = new Date(); + return graphql( + mutation.payload, + [REQUEST(ACTION_TYPE.MUTATION), SUCCESS(ACTION_TYPE.UPDATE_GROUP), ERROR(ACTION_TYPE.MUTATION)], + { + actionType: ACTION_TYPE.UPDATE_GROUP, + clientMutationId: mutation.clientMutationId, + clientMutationLabel, + requestedDateTime, + }, + ); +} diff --git a/src/components/IndividualBenefitPlansListTab.js b/src/components/BenefitPlansListTab.js similarity index 54% rename from src/components/IndividualBenefitPlansListTab.js rename to src/components/BenefitPlansListTab.js index e757e76..747eb10 100644 --- a/src/components/IndividualBenefitPlansListTab.js +++ b/src/components/BenefitPlansListTab.js @@ -1,37 +1,40 @@ import React from 'react'; import { Tab } from '@material-ui/core'; import { formatMessage, PublishedComponent } from '@openimis/fe-core'; -import { INDIVIDUAL_BENEFIT_PLANS_LIST_TAB_VALUE } from '../constants'; +import { BENEFIT_PLANS_LIST_TAB_VALUE } from '../constants'; -function IndividualBenefitPlansListTabLabel({ +function BenefitPlansListTabLabel({ intl, onChange, tabStyle, isSelected, }) { return ( ); } -function IndividualBenefitPlansListTabPanel({ value, rights, individual }) { +function BenefitPlansListTabPanel({ + value, rights, individual, group, +}) { return ( ); } -export { IndividualBenefitPlansListTabLabel, IndividualBenefitPlansListTabPanel }; +export { BenefitPlansListTabLabel, BenefitPlansListTabPanel }; diff --git a/src/components/GroupHeadPanel.js b/src/components/GroupHeadPanel.js new file mode 100644 index 0000000..ed469f5 --- /dev/null +++ b/src/components/GroupHeadPanel.js @@ -0,0 +1,70 @@ +import React, { Fragment } from 'react'; +import { Grid, Divider, Typography } from '@material-ui/core'; +import { + withModulesManager, + FormPanel, + TextInput, + FormattedMessage, +} from '@openimis/fe-core'; +import { injectIntl } from 'react-intl'; +import { withTheme, withStyles } from '@material-ui/core/styles'; + +const styles = (theme) => ({ + tableTitle: theme.table.title, + item: theme.paper.item, + fullHeight: { + height: '100%', + }, +}); + +class GroupHeadPanel extends FormPanel { + render() { + const { + edited, classes, mandatoryFieldsEmpty, + } = this.props; + const group = { ...edited }; + return ( + <> + + + + + + + + + + + + + {mandatoryFieldsEmpty && ( + <> +
+ +
+ + + )} + + + this.updateAttribute('id', v)} + value={group?.id} + /> + + + + ); + } +} + +export default withModulesManager(injectIntl(withTheme(withStyles(styles)(GroupHeadPanel)))); diff --git a/src/components/IndividualBenefitPlansActiveTab.js b/src/components/IndividualBenefitPlansActiveTab.js deleted file mode 100644 index aeb4299..0000000 --- a/src/components/IndividualBenefitPlansActiveTab.js +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import { Tab } from '@material-ui/core'; -import { formatMessage, PublishedComponent } from '@openimis/fe-core'; -import { BENEFICIARY_STATUS, INDIVIDUAL_BENEFIT_PLANS_ACTIVE_TAB_VALUE } from '../constants'; - -function IndividualBenefitPlansActiveTabLabel({ - intl, onChange, tabStyle, isSelected, -}) { - return ( - - ); -} - -function IndividualBenefitPlansActiveTabPanel({ value, rights, individual }) { - return ( - - - - ); -} - -export { IndividualBenefitPlansActiveTabLabel, IndividualBenefitPlansActiveTabPanel }; diff --git a/src/components/IndividualBenefitPlansGraduatedTab.js b/src/components/IndividualBenefitPlansGraduatedTab.js deleted file mode 100644 index 41fc961..0000000 --- a/src/components/IndividualBenefitPlansGraduatedTab.js +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react'; -import { Tab } from '@material-ui/core'; -import { formatMessage, PublishedComponent } from '@openimis/fe-core'; -import { - BENEFICIARY_STATUS, - INDIVIDUAL_BENEFIT_PLANS_GRADUATED_TAB_VALUE, -} from '../constants'; - -function IndividualBenefitPlansGraduatedTabLabel({ - intl, onChange, tabStyle, isSelected, -}) { - return ( - - ); -} - -function IndividualBenefitPlansGraduatedTabPanel({ value, rights, individual }) { - return ( - - - - ); -} - -export { IndividualBenefitPlansGraduatedTabLabel, IndividualBenefitPlansGraduatedTabPanel }; diff --git a/src/components/IndividualBenefitPlansPotentialTab.js b/src/components/IndividualBenefitPlansPotentialTab.js deleted file mode 100644 index 91fc037..0000000 --- a/src/components/IndividualBenefitPlansPotentialTab.js +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react'; -import { Tab } from '@material-ui/core'; -import { formatMessage, PublishedComponent } from '@openimis/fe-core'; -import { - BENEFICIARY_STATUS, - INDIVIDUAL_BENEFIT_PLANS_POTENTIAL_TAB_VALUE, -} from '../constants'; - -function IndividualBenefitPlansPotentialTabLabel({ - intl, onChange, tabStyle, isSelected, -}) { - return ( - - ); -} - -function IndividualBenefitPlansPotentialTabPanel({ value, rights, individual }) { - return ( - - - - ); -} - -export { IndividualBenefitPlansPotentialTabLabel, IndividualBenefitPlansPotentialTabPanel }; diff --git a/src/components/IndividualBenefitPlansSuspendedTab.js b/src/components/IndividualBenefitPlansSuspendedTab.js deleted file mode 100644 index 05f98ee..0000000 --- a/src/components/IndividualBenefitPlansSuspendedTab.js +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import { Tab } from '@material-ui/core'; -import { formatMessage, PublishedComponent } from '@openimis/fe-core'; -import { BENEFICIARY_STATUS, INDIVIDUAL_BENEFIT_PLANS_SUSPENDED_TAB_VALUE } from '../constants'; - -function IndividualBenefitPlansSuspendedTabLabel({ - intl, onChange, tabStyle, isSelected, -}) { - return ( - - ); -} - -function IndividualBenefitPlansSuspendedTabPanel({ value, rights, individual }) { - return ( - - - - ); -} - -export { IndividualBenefitPlansSuspendedTabLabel, IndividualBenefitPlansSuspendedTabPanel }; diff --git a/src/components/IndividualSearcher.js b/src/components/IndividualSearcher.js index bc17017..d4c7fe7 100644 --- a/src/components/IndividualSearcher.js +++ b/src/components/IndividualSearcher.js @@ -46,6 +46,7 @@ function IndividualSearcher({ individuals, individualsPageInfo, individualsTotalCount, + groupId, }) { const [individualToDelete, setIndividualToDelete] = useState(null); const [deletedIndividualUuids, setDeletedIndividualUuids] = useState([]); @@ -156,12 +157,21 @@ function IndividualSearcher({ const isRowDisabled = (_, individual) => deletedIndividualUuids.includes(individual.id); - const defaultFilters = () => ({ - isDeleted: { - value: false, - filter: 'isDeleted: false', - }, - }); + const defaultFilters = () => { + const filters = { + isDeleted: { + value: false, + filter: 'isDeleted: false', + }, + }; + if (groupId !== null && groupId !== undefined) { + filters.groupId = { + value: groupId, + filter: `groupId: "${groupId}"`, + }; + } + return filters; + }; return ( ({ @@ -31,9 +31,9 @@ const styles = (theme) => ({ }); function IndividualTabPanel({ - intl, rights, classes, individual, beneficiaryStatus, setConfirmedAction, + intl, rights, classes, individual, setConfirmedAction, group, }) { - const [activeTab, setActiveTab] = useState(INDIVIDUAL_BENEFIT_PLANS_LIST_TAB_VALUE); + const [activeTab, setActiveTab] = useState(individual ? BENEFIT_PLANS_LIST_TAB_VALUE : INDIVIDUALS_LIST_TAB_VALUE); const isSelected = (tab) => tab === activeTab; @@ -52,6 +52,8 @@ function IndividualTabPanel({ onChange={handleChange} isSelected={isSelected} tabStyle={tabStyle} + group={group} + individual={individual} /> diff --git a/src/components/IndividualsListTab.js b/src/components/IndividualsListTab.js new file mode 100644 index 0000000..a8f1de0 --- /dev/null +++ b/src/components/IndividualsListTab.js @@ -0,0 +1,46 @@ +import React from 'react'; +import { Tab } from '@material-ui/core'; +import { formatMessage, PublishedComponent } from '@openimis/fe-core'; +import { INDIVIDUALS_LIST_TAB_VALUE } from '../constants'; + +function IndividualsListTabLabel({ + intl, onChange, tabStyle, isSelected, individual, +}) { + if (individual) { + return null; + } + + return ( + + ); +} + +function IndividualsListTabPanel({ + value, rights, group, individual, +}) { + if (individual) { + return null; + } + return ( + + + + ); +} + +export { IndividualsListTabLabel, IndividualsListTabPanel }; diff --git a/src/constants.js b/src/constants.js index a5b57af..9beddfb 100644 --- a/src/constants.js +++ b/src/constants.js @@ -9,16 +9,14 @@ export const RIGHT_INDIVIDUAL_SEARCH = 159001; export const RIGHT_INDIVIDUAL_CREATE = 159002; export const RIGHT_INDIVIDUAL_UPDATE = 159003; export const RIGHT_INDIVIDUAL_DELETE = 159004; + export const RIGHT_GROUP_SEARCH = 180001; export const RIGHT_GROUP_CREATE = 180002; export const RIGHT_GROUP_UPDATE = 180003; export const RIGHT_GROUP_DELETE = 180004; -export const INDIVIDUAL_BENEFIT_PLANS_LIST_TAB_VALUE = 'individualBenefitPlansListTab'; -export const INDIVIDUAL_BENEFIT_PLANS_ACTIVE_TAB_VALUE = 'individualBenefitPlansActiveTab'; -export const INDIVIDUAL_BENEFIT_PLANS_POTENTIAL_TAB_VALUE = 'individualBenefitPlansPotentialTab'; -export const INDIVIDUAL_BENEFIT_PLANS_GRADUATED_TAB_VALUE = 'individualBenefitPlansGraduatedTab'; -export const INDIVIDUAL_BENEFIT_PLANS_SUSPENDED_TAB_VALUE = 'individualBenefitPlansSuspendedTab'; +export const BENEFIT_PLANS_LIST_TAB_VALUE = 'BenefitPlansListTab'; +export const INDIVIDUALS_LIST_TAB_VALUE = 'IndividualsListTab'; export const INDIVIDUAL_TABS_LABEL_CONTRIBUTION_KEY = 'individual.TabPanel.label'; export const INDIVIDUAL_TABS_PANEL_CONTRIBUTION_KEY = 'individual.TabPanel.panel'; diff --git a/src/index.js b/src/index.js index ab1c660..713beb9 100644 --- a/src/index.js +++ b/src/index.js @@ -8,31 +8,18 @@ import BeneficiaryMainMenu from './menus/BeneficiaryMainMenu'; import IndividualsPage from './pages/IndividualsPage'; import IndividualPage from './pages/IndividualPage'; import { - IndividualBenefitPlansListTabLabel, - IndividualBenefitPlansListTabPanel, -} from './components/IndividualBenefitPlansListTab'; -import { - IndividualBenefitPlansActiveTabLabel, - IndividualBenefitPlansActiveTabPanel, -} from './components/IndividualBenefitPlansActiveTab'; -import { - IndividualBenefitPlansGraduatedTabLabel, - IndividualBenefitPlansGraduatedTabPanel, -} from './components/IndividualBenefitPlansGraduatedTab'; -import { - IndividualBenefitPlansPotentialTabLabel, - IndividualBenefitPlansPotentialTabPanel, -} from './components/IndividualBenefitPlansPotentialTab'; -import { - IndividualBenefitPlansSuspendedTabLabel, - IndividualBenefitPlansSuspendedTabPanel, -} from './components/IndividualBenefitPlansSuspendedTab'; + BenefitPlansListTabLabel, + BenefitPlansListTabPanel, +} from './components/BenefitPlansListTab'; import GroupsPage from './pages/GroupsPage'; +import GroupPage from './pages/GroupPage'; +import { IndividualsListTabLabel, IndividualsListTabPanel } from './components/IndividualsListTab'; +import IndividualSearcher from './components/IndividualSearcher'; const ROUTE_INDIVIDUALS = 'individuals'; const ROUTE_INDIVIDUAL = 'individuals/individual'; const ROUTE_GROUPS = 'groups'; -// const ROUTE_GROUP = 'groups/group'; +const ROUTE_GROUP = 'groups/group'; const DEFAULT_CONFIG = { translations: [{ key: 'en', messages: flatten(messages_en) }], @@ -42,25 +29,20 @@ const DEFAULT_CONFIG = { { path: ROUTE_INDIVIDUALS, component: IndividualsPage }, { path: ROUTE_GROUPS, component: GroupsPage }, { path: `${ROUTE_INDIVIDUAL}/:individual_uuid?`, component: IndividualPage }, - // { path: `${ROUTE_GROUP}/:group_uuid?`, component: GroupPage }, + { path: `${ROUTE_GROUP}/:group_uuid?`, component: GroupPage }, ], refs: [ { key: 'individual.route.individual', ref: ROUTE_INDIVIDUAL }, - // { key: 'individual.route.group', ref: ROUTE_GROUP }, + { key: 'individual.route.group', ref: ROUTE_GROUP }, + { key: 'individual.IndividualSearcher', ref: IndividualSearcher }, ], 'individual.TabPanel.label': [ - IndividualBenefitPlansListTabLabel, - IndividualBenefitPlansActiveTabLabel, - IndividualBenefitPlansGraduatedTabLabel, - IndividualBenefitPlansPotentialTabLabel, - IndividualBenefitPlansSuspendedTabLabel, + IndividualsListTabLabel, + BenefitPlansListTabLabel, ], 'individual.TabPanel.panel': [ - IndividualBenefitPlansListTabPanel, - IndividualBenefitPlansActiveTabPanel, - IndividualBenefitPlansGraduatedTabPanel, - IndividualBenefitPlansPotentialTabPanel, - IndividualBenefitPlansSuspendedTabPanel, + IndividualsListTabPanel, + BenefitPlansListTabPanel, ], }; diff --git a/src/pages/GroupPage.js b/src/pages/GroupPage.js index e69de29..43a2946 100644 --- a/src/pages/GroupPage.js +++ b/src/pages/GroupPage.js @@ -0,0 +1,187 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { + Form, + Helmet, + withHistory, + formatMessage, + formatMessageWithValues, + coreConfirm, + clearConfirm, + journalize, +} from '@openimis/fe-core'; +import { injectIntl } from 'react-intl'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import _ from 'lodash'; +import { withTheme, withStyles } from '@material-ui/core/styles'; +import DeleteIcon from '@material-ui/icons/Delete'; +import { RIGHT_GROUP_UPDATE } from '../constants'; +import { fetchGroup, deleteGroup, updateGroup } from '../actions'; +import GroupHeadPanel from '../components/GroupHeadPanel'; +import IndividualTabPanel from '../components/IndividualTabPanel'; +import { ACTION_TYPE } from '../reducer'; + +const styles = (theme) => ({ + page: theme.page, +}); + +function GroupPage({ + intl, + classes, + rights, + history, + groupUuid, + group, + fetchGroup, + deleteGroup, + updateGroup, + coreConfirm, + clearConfirm, + confirmed, + submittingMutation, + mutation, + journalize, +}) { + const [editedGroup, setEditedGroup] = useState({}); + const [confirmedAction, setConfirmedAction] = useState(() => null); + const prevSubmittingMutationRef = useRef(); + + useEffect(() => { + if (groupUuid) { + fetchGroup([`id: "${groupUuid}"`]); + } + }, [groupUuid]); + + useEffect(() => { + if (confirmed) confirmedAction(); + return () => confirmed && clearConfirm(null); + }, [confirmed]); + + const back = () => history.goBack(); + + useEffect(() => { + if (prevSubmittingMutationRef.current && !submittingMutation) { + journalize(mutation); + if (mutation?.actionType === ACTION_TYPE.DELETE_GROUP) { + back(); + } + } + }, [submittingMutation]); + + useEffect(() => { + prevSubmittingMutationRef.current = submittingMutation; + }); + + useEffect(() => setEditedGroup(group), [group]); + + const titleParams = (group) => ({ + id: group?.id, + }); + + const isMandatoryFieldsEmpty = () => { + if (editedGroup === undefined || editedGroup === null) { + return false; + } + if ( + editedGroup.id + ) { + return false; + } + return true; + }; + + const doesGroupChange = () => { + if (_.isEqual(group, editedGroup)) { + return false; + } + return true; + }; + + const canSave = () => !isMandatoryFieldsEmpty() && doesGroupChange(); + + const handleSave = () => { + updateGroup( + editedGroup, + formatMessageWithValues(intl, 'individual', 'group.update.mutationLabel', { + id: group?.id, + }), + ); + }; + + const deleteGroupCallback = () => deleteGroup( + group, + formatMessageWithValues(intl, 'individual', 'group.delete.mutationLabel', { + id: group?.id, + }), + ); + + const openDeleteGroupConfirmDialog = () => { + setConfirmedAction(() => deleteGroupCallback); + coreConfirm( + formatMessageWithValues(intl, 'individual', 'group.delete.confirm.title', { + id: group?.id, + }), + formatMessage(intl, 'individual', 'group.delete.confirm.message'), + ); + }; + + const actions = [ + !!group && { + doIt: openDeleteGroupConfirmDialog, + icon: , + tooltip: formatMessage(intl, 'individual', 'deleteButtonTooltip'), + }, + ]; + + return ( + rights.includes(RIGHT_GROUP_UPDATE) && ( +
+ + +
+ ) + ); +} + +const mapStateToProps = (state, props) => ({ + rights: !!state.core && !!state.core.user && !!state.core.user.i_user ? state.core.user.i_user.rights : [], + groupUuid: props.match.params.group_uuid, + confirmed: state.core.confirmed, + fetchingGroups: state.individual.fetchingGroups, + fetchedGroups: state.individual.fetchedGroups, + group: state.individual.group, + errorGroup: state.individual.errorGroup, + submittingMutation: state.individual.submittingMutation, + mutation: state.individual.mutation, +}); + +const mapDispatchToProps = (dispatch) => bindActionCreators({ + fetchGroup, + deleteGroup, + updateGroup, + coreConfirm, + clearConfirm, + journalize, +}, dispatch); + +export default withHistory( + injectIntl(withTheme(withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(GroupPage)))), +); diff --git a/src/reducer.js b/src/reducer.js index a70d101..d452e92 100644 --- a/src/reducer.js +++ b/src/reducer.js @@ -16,10 +16,13 @@ import { REQUEST, SUCCESS, ERROR } from './util/action-type'; export const ACTION_TYPE = { MUTATION: 'INDIVIDUAL_MUTATION', SEARCH_INDIVIDUALS: 'INDIVIDUAL_INDIVIDUALS', + SEARCH_GROUPS: 'GROUP_GROUPS', GET_INDIVIDUAL: 'INDIVIDUAL_INDIVIDUAL', + GET_GROUP: 'GROUP_GROUP', DELETE_INDIVIDUAL: 'INDIVIDUAL_DELETE_INDIVIDUAL', + DELETE_GROUP: 'GROUP_DELETE_GROUP', UPDATE_INDIVIDUAL: 'INDIVIDUAL_UPDATE_INDIVIDUAL', - SEARCH_GROUPS: 'GROUP_GROUPS', + UPDATE_GROUP: 'GROUP_UPDATE_GROUP', }; function reducer( @@ -42,6 +45,10 @@ function reducer( groups: [], groupsPageInfo: {}, groupsTotalCount: 0, + fetchingGroup: false, + errorGroup: null, + fetchedGroup: false, + group: null, }, action, ) { @@ -74,6 +81,14 @@ function reducer( individual: null, errorIndividual: null, }; + case REQUEST(ACTION_TYPE.GET_GROUP): + return { + ...state, + fetchingGroup: true, + fetchedGroup: false, + group: null, + errorGroup: null, + }; case SUCCESS(ACTION_TYPE.SEARCH_INDIVIDUALS): return { ...state, @@ -111,6 +126,17 @@ function reducer( }))?.[0], errorIndividual: null, }; + case SUCCESS(ACTION_TYPE.GET_GROUP): + return { + ...state, + fetchingGroup: false, + fetchedIGroup: true, + group: parseData(action.payload.data.group).map((group) => ({ + ...group, + id: decodeId(group.id), + }))?.[0], + errorGroup: null, + }; case ERROR(ACTION_TYPE.SEARCH_INDIVIDUALS): return { ...state, @@ -129,6 +155,12 @@ function reducer( fetchingIndividual: false, errorIndividual: formatServerError(action.payload), }; + case ERROR(ACTION_TYPE.GET_GROUP): + return { + ...state, + fetchingGroup: false, + errorGroup: formatServerError(action.payload), + }; case REQUEST(ACTION_TYPE.MUTATION): return dispatchMutationReq(state, action); case ERROR(ACTION_TYPE.MUTATION): @@ -137,6 +169,10 @@ function reducer( return dispatchMutationResp(state, 'deleteIndividual', action); case SUCCESS(ACTION_TYPE.UPDATE_INDIVIDUAL): return dispatchMutationResp(state, 'updateIndividual', action); + case SUCCESS(ACTION_TYPE.DELETE_GROUP): + return dispatchMutationResp(state, 'deleteGroup', action); + case SUCCESS(ACTION_TYPE.UPDATE_GROUP): + return dispatchMutationResp(state, 'updateGroup', action); default: return state; } diff --git a/src/translations/en.json b/src/translations/en.json index 1210f97..f111d32 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -34,7 +34,7 @@ "saveButton.tooltip.enabled": "Save changes", "saveButton.tooltip.disabled": "Please fill General Information fields first", "benefitPlansList": { - "label": "LIST" + "label": "BENEFIT PLANS" }, "benefitPlansActive": { "label": "ACTIVE" @@ -47,7 +47,11 @@ }, "benefitPlansSuspended": { "label": "SUSPENDED" - } + }, + "individualsList": { + "label": "MEMBERS" + }, + "any": "ANY" }, "individuals": { "pageTitle": "Individuals", @@ -60,6 +64,9 @@ "group": { "id": "ID", "individual.firstName": "First Name", - "individual.lastName": "Last Name" + "individual.lastName": "Last Name", + "pageTitle": "Group {id}", + "headPanelTitle": "General information", + "mandatoryFieldsEmptyError": "* These fields are required" } } \ No newline at end of file From de5d2daf7dd8e4186cd7885bca2f489e3d2812b1 Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 15 Jun 2023 15:38:04 +0200 Subject: [PATCH 13/90] CM-108: address comments from last PR (#13) * CM-106: add benefit plans panel to individual * CM-106: fix eslint * CM-106: fix eslint * CM-108: initial group detail view * CM-106: update view to mockup * CM-106: remove unused files * CM-108: add droup detail view * CM-108: update group detail view --- src/pages/GroupPage.js | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/src/pages/GroupPage.js b/src/pages/GroupPage.js index 43a2946..8a73aca 100644 --- a/src/pages/GroupPage.js +++ b/src/pages/GroupPage.js @@ -78,24 +78,9 @@ function GroupPage({ id: group?.id, }); - const isMandatoryFieldsEmpty = () => { - if (editedGroup === undefined || editedGroup === null) { - return false; - } - if ( - editedGroup.id - ) { - return false; - } - return true; - }; + const isMandatoryFieldsEmpty = () => !editedGroup || !editedGroup.id; - const doesGroupChange = () => { - if (_.isEqual(group, editedGroup)) { - return false; - } - return true; - }; + const doesGroupChange = () => !_.isEqual(group, editedGroup); const canSave = () => !isMandatoryFieldsEmpty() && doesGroupChange(); From adbb1a143a76c66cd31ec8331121aef5ba9c2827 Mon Sep 17 00:00:00 2001 From: Jan Date: Fri, 16 Jun 2023 08:16:53 +0200 Subject: [PATCH 14/90] CM-121: address QA points (#15) * CM-29: add export funcionality to individuals * CM-121: address QA tasks * CM-121: fix eslint --- src/actions.js | 8 ++ src/components/GroupFilter.js | 42 +++-------- src/components/GroupHeadPanel.js | 1 + src/components/GroupSearcher.js | 93 ++++++++++++++++++++--- src/components/IndividualFilter.js | 4 +- src/components/IndividualSearcher.js | 109 ++++++++++++++++++++------- src/reducer.js | 34 ++++++++- src/translations/en.json | 22 +++++- 8 files changed, 240 insertions(+), 73 deletions(-) diff --git a/src/actions.js b/src/actions.js index 6ba9758..3777b55 100644 --- a/src/actions.js +++ b/src/actions.js @@ -129,3 +129,11 @@ export function downloadGroups(params) { }`; return graphql(payload, ACTION_TYPE.GROUP_EXPORT); } + +export function downloadIndividuals(params) { + const payload = ` + { + individualsExport${!!params && params.length ? `(${params.join(',')})` : ''} + }`; + return graphql(payload, ACTION_TYPE.INDIVIDUAL_EXPORT); +} diff --git a/src/components/GroupFilter.js b/src/components/GroupFilter.js index 14f0a1e..449b33d 100644 --- a/src/components/GroupFilter.js +++ b/src/components/GroupFilter.js @@ -4,7 +4,7 @@ import { TextInput } from '@openimis/fe-core'; import { Grid } from '@material-ui/core'; import { withTheme, withStyles } from '@material-ui/core/styles'; import _debounce from 'lodash/debounce'; -import { CONTAINS_LOOKUP, DEFAULT_DEBOUNCE_TIME } from '../constants'; +import { DEFAULT_DEBOUNCE_TIME, EMPTY_STRING } from '../constants'; import { defaultFilterStyles } from '../util/styles'; function GroupFilter({ @@ -12,43 +12,25 @@ function GroupFilter({ }) { 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}"`, - }, - ]); - } + const onChangeStringFilter = (filterName) => (value) => { + debouncedOnChangeFilters([ + { + id: filterName, + value, + filter: `${filterName}: "${value}"`, + }, + ]); }; return ( - - - @@ -56,7 +38,7 @@ function GroupFilter({ diff --git a/src/components/GroupHeadPanel.js b/src/components/GroupHeadPanel.js index ed469f5..e2ac19b 100644 --- a/src/components/GroupHeadPanel.js +++ b/src/components/GroupHeadPanel.js @@ -54,6 +54,7 @@ class GroupHeadPanel extends FormPanel { coreConfirm( + formatMessageWithValues(intl, 'individual', 'group.delete.confirm.title', { + id: groupToDelete.id, + }), + formatMessage(intl, 'individual', 'group.delete.confirm.message'), + ); + const onDoubleClick = (group, newTab = false) => rights.includes(RIGHT_GROUP_UPDATE) - && historyPush(modulesManager, history, 'individual.route.group', [group?.id], newTab); + && !deletedGroupUuids.includes(group.id) + && historyPush(modulesManager, history, 'individual.route.group', [group?.id], newTab); + + const onDelete = (group) => setGroupToDelete(group); + + useEffect(() => groupToDelete && openDeleteGroupConfirmDialog(), [groupToDelete]); + + useEffect(() => { + if (groupToDelete && confirmed) { + deleteGroup( + groupToDelete, + formatMessageWithValues(intl, 'individual', 'individual.delete.mutationLabel', { + id: groupToDelete.id, + }), + ); + setDeletedGroupUuids([...deletedGroupUuids, groupToDelete.id]); + } + if (groupToDelete && confirmed !== null) { + setGroupToDelete(null); + } + return () => confirmed && clearConfirm(false); + }, [confirmed]); + + useEffect(() => { + if (prevSubmittingMutationRef.current && !submittingMutation) { + journalize(mutation); + } + }, [submittingMutation]); + + useEffect(() => { + prevSubmittingMutationRef.current = submittingMutation; + }); const fetch = (params) => fetchGroups(params); @@ -78,6 +130,18 @@ function GroupSearcher({
)); } + if (rights.includes(RIGHT_GROUP_DELETE)) { + formatters.push((group) => ( + + onDelete(group)} + disabled={deletedGroupUuids.includes(group.id)} + > + + + + )); + } return formatters; }; @@ -87,6 +151,8 @@ function GroupSearcher({ ['id', false], ]; + const isRowDisabled = (_, group) => deletedGroupUuids.includes(group.id); + const defaultFilters = () => ({ isDeleted: { value: false, @@ -102,11 +168,11 @@ function GroupSearcher({ useEffect(() => { if (groupsExport) { - downloadExport(groupsExport, `${formatMessage(intl, 'socialProtection', 'export.filename')}.csv`)(); + downloadExport(groupsExport, `${formatMessage(intl, 'individual', 'export.filename')}.csv`)(); } }, [groupsExport]); - const groupBeneficiaryFilter = (props) => ( + const groupFilter = (props) => ( {failedExport && ( {errorGroupsExport} @@ -181,12 +249,19 @@ const mapStateToProps = (state) => ({ groupsExport: state.individual.groupsExport, groupsExportPageInfo: state.individual.groupsExportPageInfo, errorGroupsExport: state.individual.errorGroupsExport, + confirmed: state.core.confirmed, + submittingMutation: state.individual.submittingMutation, + mutation: state.individual.mutation, }); const mapDispatchToProps = (dispatch) => bindActionCreators( { fetchGroups, downloadGroups, + deleteGroup, + coreConfirm, + clearConfirm, + journalize, }, dispatch, ); diff --git a/src/components/IndividualFilter.js b/src/components/IndividualFilter.js index 6a848e8..a784a0c 100644 --- a/src/components/IndividualFilter.js +++ b/src/components/IndividualFilter.js @@ -4,7 +4,7 @@ import { TextInput, PublishedComponent } from '@openimis/fe-core'; import { Grid } from '@material-ui/core'; import { withTheme, withStyles } from '@material-ui/core/styles'; import _debounce from 'lodash/debounce'; -import { CONTAINS_LOOKUP, DEFAULT_DEBOUNCE_TIME } from '../constants'; +import { CONTAINS_LOOKUP, DEFAULT_DEBOUNCE_TIME, EMPTY_STRING } from '../constants'; import { defaultFilterStyles } from '../util/styles'; function IndividualFilter({ @@ -14,7 +14,7 @@ function IndividualFilter({ const filterValue = (filterName) => filters?.[filterName]?.value; - const filterTextFieldValue = (filterName) => filters?.[filterName]?.value ?? ''; + const filterTextFieldValue = (filterName) => filters?.[filterName]?.value ?? EMPTY_STRING; const onChangeStringFilter = (filterName, lookup = null) => (value) => { if (lookup) { diff --git a/src/components/IndividualSearcher.js b/src/components/IndividualSearcher.js index d4c7fe7..aa497c1 100644 --- a/src/components/IndividualSearcher.js +++ b/src/components/IndividualSearcher.js @@ -11,13 +11,19 @@ import { journalize, withHistory, historyPush, + downloadExport, } from '@openimis/fe-core'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; -import { IconButton, Tooltip } from '@material-ui/core'; +import { + IconButton, Tooltip, Button, + Dialog, + DialogActions, + DialogTitle, +} from '@material-ui/core'; import EditIcon from '@material-ui/icons/Edit'; import DeleteIcon from '@material-ui/icons/Delete'; -import { fetchIndividuals, deleteIndividual } from '../actions'; +import { fetchIndividuals, deleteIndividual, downloadIndividuals } from '../actions'; import { DEFAULT_PAGE_SIZE, ROWS_PER_PAGE_OPTIONS, @@ -47,6 +53,9 @@ function IndividualSearcher({ individualsPageInfo, individualsTotalCount, groupId, + downloadIndividuals, + individualsExport, + errorIndividualsExport, }) { const [individualToDelete, setIndividualToDelete] = useState(null); const [deletedIndividualUuids, setDeletedIndividualUuids] = useState([]); @@ -157,6 +166,18 @@ function IndividualSearcher({ const isRowDisabled = (_, individual) => deletedIndividualUuids.includes(individual.id); + const [failedExport, setFailedExport] = useState(false); + + useEffect(() => { + setFailedExport(true); + }, [errorIndividualsExport]); + + useEffect(() => { + if (individualsExport) { + downloadExport(individualsExport, `${formatMessage(intl, 'individual', 'export.filename')}.csv`)(); + } + }, [individualsExport]); + const defaultFilters = () => { const filters = { isDeleted: { @@ -174,30 +195,59 @@ function IndividualSearcher({ }; return ( - +
+ + {failedExport && ( + + {errorIndividualsExport} + + + + + )} +
); } @@ -211,12 +261,19 @@ const mapStateToProps = (state) => ({ confirmed: state.core.confirmed, submittingMutation: state.individual.submittingMutation, mutation: state.individual.mutation, + selectedFilters: state.core.filtersCache.individualsFilterCache, + fetchingIndividualsExport: state.individual.fetchingIndividualsExport, + fetchedIndividualsExport: state.individual.fetchedIndividualsExport, + individualsExport: state.individual.individualsExport, + individualsExportPageInfo: state.individual.individualsExportPageInfo, + errorIndividualsExport: state.individual.errorIndividualsExport, }); const mapDispatchToProps = (dispatch) => bindActionCreators( { fetchIndividuals, deleteIndividual, + downloadIndividuals, coreConfirm, clearConfirm, journalize, diff --git a/src/reducer.js b/src/reducer.js index 2ac3e3f..dfb2312 100644 --- a/src/reducer.js +++ b/src/reducer.js @@ -24,6 +24,7 @@ export const ACTION_TYPE = { UPDATE_INDIVIDUAL: 'INDIVIDUAL_UPDATE_INDIVIDUAL', UPDATE_GROUP: 'GROUP_UPDATE_GROUP', GROUP_EXPORT: 'GROUP_EXPORT', + INDIVIDUAL_EXPORT: 'INDIVIDUAL_EXPORT', }; function reducer( @@ -50,11 +51,16 @@ function reducer( errorGroup: null, fetchedGroup: false, group: null, - fetchingGroupBsExport: true, + fetchingGroupsExport: true, fetchedGroupsExport: false, groupsExport: null, groupsExportPageInfo: {}, errorGroupsExport: null, + fetchingIndividualsExport: true, + fetchedIndividualsExport: false, + individualsExport: null, + individualsExportPageInfo: {}, + errorIndividualsExport: null, }, action, ) { @@ -117,7 +123,7 @@ function reducer( ...group, id: decodeId(group.id), })), - groupsPageInfo: pageInfo(action.payload.data.individual), + groupsPageInfo: pageInfo(action.payload.data.group), groupsTotalCount: action.payload.data.group ? action.payload.data.group.totalCount : null, errorGroups: formatGraphQLError(action.payload), }; @@ -191,6 +197,30 @@ function reducer( fetchingGroupsExport: false, errorGroupsExport: formatServerError(action.payload), }; + case REQUEST(ACTION_TYPE.INDIVIDUAL_EXPORT): + return { + ...state, + fetchingIndividualsExport: true, + fetchedIndividualsExport: false, + individualsExport: null, + individualsExportPageInfo: {}, + errorIndividualsExport: null, + }; + case SUCCESS(ACTION_TYPE.INDIVIDUAL_EXPORT): + return { + ...state, + fetchingIndividualsExport: false, + fetchedIndividualsExport: true, + individualsExport: action.payload.data.individualsExport, + individualsExportPageInfo: pageInfo(action.payload.data.individualsExportPageInfo), + errorIndividualsExport: formatGraphQLError(action.payload), + }; + case ERROR(ACTION_TYPE.INDIVIDUAL_EXPORT): + return { + ...state, + fetchingIndividualsExport: false, + errorIndividualsExport: formatServerError(action.payload), + }; case REQUEST(ACTION_TYPE.MUTATION): return dispatchMutationReq(state, action); case ERROR(ACTION_TYPE.MUTATION): diff --git a/src/translations/en.json b/src/translations/en.json index f111d32..a8bbaa4 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -51,7 +51,8 @@ "individualsList": { "label": "MEMBERS" }, - "any": "ANY" + "any": "ANY", + "ok": "ok" }, "individuals": { "pageTitle": "Individuals", @@ -63,10 +64,23 @@ }, "group": { "id": "ID", - "individual.firstName": "First Name", - "individual.lastName": "Last Name", + "individual.firstName": "Individual First Name", + "individual.lastName": "Individual Last Name", "pageTitle": "Group {id}", "headPanelTitle": "General information", - "mandatoryFieldsEmptyError": "* These fields are required" + "mandatoryFieldsEmptyError": "* These fields are required", + "delete": { + "confirm": { + "title": "Delete {id}?", + "message": "Deleting data does not mean erasing it from OpenIMIS database. The data will only be deactivated from the viewed list." + }, + "mutationLabel": "Delete Group {id}" + } + }, + "export": { + "label": "DOWNLOAD", + "dateCreated": "Date Created", + "id": "id", + "filename": "filename" } } \ No newline at end of file From 278df44fa4f3389e7d0a78a108dc927be67ecd6a Mon Sep 17 00:00:00 2001 From: Jan Date: Fri, 16 Jun 2023 10:28:50 +0200 Subject: [PATCH 15/90] CM-29: add export funcionality to individuals (#14) From 38b545c066f1101a80c8c59c5095114d90a3ba8c Mon Sep 17 00:00:00 2001 From: olewandowski1 <109145288+olewandowski1@users.noreply.github.com> Date: Sat, 17 Jun 2023 12:25:48 +0200 Subject: [PATCH 16/90] CM-110: export necessary actions to create benefit package page for groups (#17) --- src/actions.js | 2 +- src/index.js | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/actions.js b/src/actions.js index 3777b55..afff115 100644 --- a/src/actions.js +++ b/src/actions.js @@ -133,7 +133,7 @@ export function downloadGroups(params) { export function downloadIndividuals(params) { const payload = ` { - individualsExport${!!params && params.length ? `(${params.join(',')})` : ''} + individualExport${!!params && params.length ? `(${params.join(',')})` : ''} }`; return graphql(payload, ACTION_TYPE.INDIVIDUAL_EXPORT); } diff --git a/src/index.js b/src/index.js index 713beb9..db0f6f5 100644 --- a/src/index.js +++ b/src/index.js @@ -15,6 +15,7 @@ import GroupsPage from './pages/GroupsPage'; import GroupPage from './pages/GroupPage'; import { IndividualsListTabLabel, IndividualsListTabPanel } from './components/IndividualsListTab'; import IndividualSearcher from './components/IndividualSearcher'; +import { downloadIndividuals, fetchIndividuals } from './actions'; const ROUTE_INDIVIDUALS = 'individuals'; const ROUTE_INDIVIDUAL = 'individuals/individual'; @@ -35,6 +36,8 @@ const DEFAULT_CONFIG = { { key: 'individual.route.individual', ref: ROUTE_INDIVIDUAL }, { key: 'individual.route.group', ref: ROUTE_GROUP }, { key: 'individual.IndividualSearcher', ref: IndividualSearcher }, + { key: 'individual.actions.fetchIndividuals', ref: fetchIndividuals }, + { key: 'individual.actions.downloadIndividuals', ref: downloadIndividuals }, ], 'individual.TabPanel.label': [ IndividualsListTabLabel, From 176de2dab6d880da082de26e1155a307ab2f9876 Mon Sep 17 00:00:00 2001 From: Jan Date: Sat, 17 Jun 2023 13:00:28 +0200 Subject: [PATCH 17/90] CM-29: fix export query names (#16) --- src/actions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/actions.js b/src/actions.js index afff115..ee1aadf 100644 --- a/src/actions.js +++ b/src/actions.js @@ -125,7 +125,7 @@ export function updateGroup(group, clientMutationLabel) { export function downloadGroups(params) { const payload = ` { - groupsExport${!!params && params.length ? `(${params.join(',')})` : ''} + groupExport${!!params && params.length ? `(${params.join(',')})` : ''} }`; return graphql(payload, ACTION_TYPE.GROUP_EXPORT); } From 9d181902ac0fb9159764eec508a891748c1f0773 Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 20 Jun 2023 09:14:28 +0200 Subject: [PATCH 18/90] CM-105: fix downloads (#19) * CM-105: fix exports * CM-105: update translations --- src/components/GroupSearcher.js | 28 ++++++++---------- src/components/IndividualSearcher.js | 24 +++++++-------- src/reducer.js | 44 ++++++++++++++-------------- src/translations/en.json | 8 ++++- 4 files changed, 53 insertions(+), 51 deletions(-) diff --git a/src/components/GroupSearcher.js b/src/components/GroupSearcher.js index 11afdd8..97c9b47 100644 --- a/src/components/GroupSearcher.js +++ b/src/components/GroupSearcher.js @@ -43,8 +43,8 @@ function GroupSearcher({ groupsPageInfo, groupsTotalCount, downloadGroups, - groupsExport, - errorGroupsExport, + groupExport, + errorGroupExport, deleteGroup, confirmed, submittingMutation, @@ -164,13 +164,13 @@ function GroupSearcher({ useEffect(() => { setFailedExport(true); - }, [errorGroupsExport]); + }, [errorGroupExport]); useEffect(() => { - if (groupsExport) { - downloadExport(groupsExport, `${formatMessage(intl, 'individual', 'export.filename')}.csv`)(); + if (groupExport) { + downloadExport(groupExport, `${formatMessage(intl, 'individual', 'export.filename.groups')}.csv`)(); } - }, [groupsExport]); + }, [groupExport]); const groupFilter = (props) => ( {failedExport && ( - {errorGroupsExport} + {errorGroupExport} + + + )} + + ); +} + +const mapStateToProps = (state) => ({ + fetchingGroupIndividuals: state.individual.fetchingGroupIndividuals, + fetchedGroupIndividuals: state.individual.fetchedGroupIndividuals, + errorGroupIndividuals: state.individual.errorGroupIndividuals, + groupIndividuals: state.individual.groupIndividuals, + groupIndividualsPageInfo: state.individual.groupIndividualsPageInfo, + groupIndividualsTotalCount: state.individual.groupIndividualsTotalCount, + confirmed: state.core.confirmed, + submittingMutation: state.individual.submittingMutation, + mutation: state.individual.mutation, + selectedFilters: state.core.filtersCache.groupIndividualsFilterCache, + fetchingGroupIndividualExport: state.individual.fetchingGroupIndividualExport, + fetchedGroupIndividualExport: state.individual.fetchedGroupIndividualExport, + groupIndividualExport: state.individual.groupIndividualExport, + groupIndividualExportPageInfo: state.individual.groupIndividualExportPageInfo, + errorGroupIndividualExport: state.individual.errorGroupIndividualExport, +}); + +const mapDispatchToProps = (dispatch) => bindActionCreators( + { + fetchGroupIndividuals, + updateGroupIndividual, + deleteGroupIndividual, + downloadGroupIndividuals, + coreConfirm, + clearConfirm, + journalize, + }, + dispatch, +); + +export default withHistory( + withModulesManager(injectIntl(connect(mapStateToProps, mapDispatchToProps)(GroupIndividualSearcher))), +); diff --git a/src/components/IndividualsListTab.js b/src/components/IndividualsListTab.js index a8f1de0..d364f89 100644 --- a/src/components/IndividualsListTab.js +++ b/src/components/IndividualsListTab.js @@ -37,7 +37,7 @@ function IndividualsListTabPanel({ ); diff --git a/src/constants.js b/src/constants.js index 9beddfb..bc97f4b 100644 --- a/src/constants.js +++ b/src/constants.js @@ -10,6 +10,11 @@ export const RIGHT_INDIVIDUAL_CREATE = 159002; export const RIGHT_INDIVIDUAL_UPDATE = 159003; export const RIGHT_INDIVIDUAL_DELETE = 159004; +export const RIGHT_GROUP_INDIVIDUAL_SEARCH = RIGHT_INDIVIDUAL_SEARCH; +export const RIGHT_GROUP_INDIVIDUAL_CREATE = RIGHT_INDIVIDUAL_CREATE; +export const RIGHT_GROUP_INDIVIDUAL_UPDATE = RIGHT_INDIVIDUAL_UPDATE; +export const RIGHT_GROUP_INDIVIDUAL_DELETE = RIGHT_INDIVIDUAL_DELETE; + export const RIGHT_GROUP_SEARCH = 180001; export const RIGHT_GROUP_CREATE = 180002; export const RIGHT_GROUP_UPDATE = 180003; @@ -26,3 +31,12 @@ export const BENEFICIARY_STATUS = { GRADUATED: 'GRADUATED', SUSPENDED: 'SUSPENDED', }; + +export const GROUP_INDIVIDUAL_ROLES = { + HEAD: 'HEAD', + RECIPIENT: 'RECIPIENT', +}; + +export const GROUP_INDIVIDUAL_ROLES_LIST = [ + GROUP_INDIVIDUAL_ROLES.HEAD, GROUP_INDIVIDUAL_ROLES.RECIPIENT, +]; diff --git a/src/index.js b/src/index.js index db0f6f5..909c5cc 100644 --- a/src/index.js +++ b/src/index.js @@ -14,11 +14,12 @@ import { import GroupsPage from './pages/GroupsPage'; import GroupPage from './pages/GroupPage'; import { IndividualsListTabLabel, IndividualsListTabPanel } from './components/IndividualsListTab'; -import IndividualSearcher from './components/IndividualSearcher'; +import GroupIndividualSearcher from './components/GroupIndividualSearcher'; import { downloadIndividuals, fetchIndividuals } from './actions'; const ROUTE_INDIVIDUALS = 'individuals'; const ROUTE_INDIVIDUAL = 'individuals/individual'; +const ROUTE_INDIVIDUAL_FROM_GROUP = 'groups/group/individuals/individual'; const ROUTE_GROUPS = 'groups'; const ROUTE_GROUP = 'groups/group'; @@ -30,12 +31,13 @@ const DEFAULT_CONFIG = { { path: ROUTE_INDIVIDUALS, component: IndividualsPage }, { path: ROUTE_GROUPS, component: GroupsPage }, { path: `${ROUTE_INDIVIDUAL}/:individual_uuid?`, component: IndividualPage }, + { path: `${ROUTE_INDIVIDUAL_FROM_GROUP}/:individual_uuid?`, component: IndividualPage }, { path: `${ROUTE_GROUP}/:group_uuid?`, component: GroupPage }, ], refs: [ { key: 'individual.route.individual', ref: ROUTE_INDIVIDUAL }, { key: 'individual.route.group', ref: ROUTE_GROUP }, - { key: 'individual.IndividualSearcher', ref: IndividualSearcher }, + { key: 'individual.GroupIndividualSearcher', ref: GroupIndividualSearcher }, { key: 'individual.actions.fetchIndividuals', ref: fetchIndividuals }, { key: 'individual.actions.downloadIndividuals', ref: downloadIndividuals }, ], diff --git a/src/pages/GroupPage.js b/src/pages/GroupPage.js index 8a73aca..9e2f3cc 100644 --- a/src/pages/GroupPage.js +++ b/src/pages/GroupPage.js @@ -53,7 +53,7 @@ function GroupPage({ }, [groupUuid]); useEffect(() => { - if (confirmed) confirmedAction(); + if (confirmed && confirmedAction) confirmedAction(); return () => confirmed && clearConfirm(null); }, [confirmed]); diff --git a/src/pages/IndividualPage.js b/src/pages/IndividualPage.js index ffa9ede..03dc141 100644 --- a/src/pages/IndividualPage.js +++ b/src/pages/IndividualPage.js @@ -53,7 +53,7 @@ function IndividualPage({ }, [individualUuid]); useEffect(() => { - if (confirmed) confirmedAction(); + if (confirmed && confirmedAction) confirmedAction(); return () => confirmed && clearConfirm(null); }, [confirmed]); diff --git a/src/pickers/GroupIndividualRolePicker.js b/src/pickers/GroupIndividualRolePicker.js new file mode 100644 index 0000000..f7b46f0 --- /dev/null +++ b/src/pickers/GroupIndividualRolePicker.js @@ -0,0 +1,27 @@ +import React from 'react'; +import { ConstantBasedPicker } from '@openimis/fe-core'; +import { + GROUP_INDIVIDUAL_ROLES_LIST, +} from '../constants'; + +function GroupIndividualRolePicker(props) { + const { + required, withNull, readOnly, onChange, value, nullLabel, withLabel, + } = props; + return ( + + ); +} + +export default GroupIndividualRolePicker; diff --git a/src/reducer.js b/src/reducer.js index 1eda6de..e37d74e 100644 --- a/src/reducer.js +++ b/src/reducer.js @@ -16,15 +16,19 @@ import { REQUEST, SUCCESS, ERROR } from './util/action-type'; export const ACTION_TYPE = { MUTATION: 'INDIVIDUAL_MUTATION', SEARCH_INDIVIDUALS: 'INDIVIDUAL_INDIVIDUALS', + SEARCH_GROUP_INDIVIDUALS: 'GROUP_INDIVIDUAL_GROUP_INDIVIDUALS', SEARCH_GROUPS: 'GROUP_GROUPS', GET_INDIVIDUAL: 'INDIVIDUAL_INDIVIDUAL', GET_GROUP: 'GROUP_GROUP', DELETE_INDIVIDUAL: 'INDIVIDUAL_DELETE_INDIVIDUAL', + DELETE_GROUP_INDIVIDUAL: 'GROUP_INDIVIDUAL_DELETE_GROUP_INDIVIDUAL', DELETE_GROUP: 'GROUP_DELETE_GROUP', UPDATE_INDIVIDUAL: 'INDIVIDUAL_UPDATE_INDIVIDUAL', + UPDATE_GROUP_INDIVIDUAL: 'GROUP_INDIVIDUAL_UPDATE_GROUP_INDIVIDUAL', UPDATE_GROUP: 'GROUP_UPDATE_GROUP', GROUP_EXPORT: 'GROUP_EXPORT', INDIVIDUAL_EXPORT: 'INDIVIDUAL_EXPORT', + GROUP_INDIVIDUAL_EXPORT: 'GROUP_INDIVIDUAL_EXPORT', }; function reducer( @@ -37,6 +41,12 @@ function reducer( individuals: [], individualsPageInfo: {}, individualsTotalCount: 0, + fetchingGroupIndividuals: false, + errorGroupIndividuals: null, + fetchedGroupIndividuals: false, + groupIndividuals: [], + groupIndividualsPageInfo: {}, + groupIndividualsTotalCount: 0, fetchingIndividual: false, errorIndividual: null, fetchedIndividual: false, @@ -61,6 +71,11 @@ function reducer( individualsExport: null, individualsExportPageInfo: {}, errorIndividualsExport: null, + fetchingGroupIndividualsExport: true, + fetchedGroupIndividualsExport: false, + groupIndividualsExport: null, + groupIndividualsExportPageInfo: {}, + errorGroupIndividualsExport: null, }, action, ) { @@ -75,6 +90,16 @@ function reducer( individualsTotalCount: 0, errorIndividuals: null, }; + case REQUEST(ACTION_TYPE.SEARCH_GROUP_INDIVIDUALS): + return { + ...state, + fetchingGroupIndividuals: true, + fetchedGroupIndividuals: false, + groupIndividuals: [], + groupIndividualsPageInfo: {}, + groupIndividualsTotalCount: 0, + errorGroupIndividuals: null, + }; case REQUEST(ACTION_TYPE.SEARCH_GROUPS): return { ...state, @@ -114,6 +139,36 @@ function reducer( individualsTotalCount: action.payload.data.individual ? action.payload.data.individual.totalCount : null, errorIndividuals: formatGraphQLError(action.payload), }; + case SUCCESS(ACTION_TYPE.SEARCH_GROUP_INDIVIDUALS): + return { + ...state, + fetchingGroupIndividuals: false, + fetchedGroupIndividuals: true, + groupIndividuals: parseData(action.payload.data.groupIndividual)?.map((groupIndividual) => { + const response = ({ + ...groupIndividual, + id: decodeId(groupIndividual.id), + }); + if (response?.individual?.id) { + response.individual = ({ + ...response.individual, + id: decodeId(response.individual.id), + }); + } + if (response?.group?.id) { + response.group = ({ + ...response.group, + id: decodeId(response.group.id), + }); + } + return response; + }), + groupIndividualsPageInfo: pageInfo(action.payload.data.groupIndividual), + groupIndividualsTotalCount: action.payload.data.groupIndividual + ? action.payload.data.groupIndividual.totalCount + : null, + errorGroupIndividuals: formatGraphQLError(action.payload), + }; case SUCCESS(ACTION_TYPE.SEARCH_GROUPS): return { ...state, @@ -155,6 +210,12 @@ function reducer( fetchingIndividuals: false, errorIndividuals: formatServerError(action.payload), }; + case ERROR(ACTION_TYPE.SEARCH_GROUP_INDIVIDUALS): + return { + ...state, + fetchingGroupIndividuals: false, + errorGroupIndividuals: formatServerError(action.payload), + }; case ERROR(ACTION_TYPE.SEARCH_GROUPS): return { ...state, @@ -206,6 +267,15 @@ function reducer( individualExportPageInfo: {}, errorIndividualExport: null, }; + case REQUEST(ACTION_TYPE.GROUP_INDIVIDUAL_EXPORT): + return { + ...state, + fetchingGroupIndividualExport: true, + fetchedGroupIndividualExport: false, + groupIndividualExport: null, + groupIndividualExportPageInfo: {}, + errorGroupIndividualExport: null, + }; case SUCCESS(ACTION_TYPE.INDIVIDUAL_EXPORT): return { ...state, @@ -215,12 +285,27 @@ function reducer( individualExportPageInfo: pageInfo(action.payload.data.individualExportPageInfo), errorIndividualExport: formatGraphQLError(action.payload), }; + case SUCCESS(ACTION_TYPE.GROUP_INDIVIDUAL_EXPORT): + return { + ...state, + fetchingGroupIndividualsExport: false, + fetchedGroupIndividualsExport: true, + groupIndividualExport: action.payload.data.groupIndividualExport, + groupIndividualExportPageInfo: pageInfo(action.payload.data.groupIndividualExportPageInfo), + errorGroupIndividualExport: formatGraphQLError(action.payload), + }; case ERROR(ACTION_TYPE.INDIVIDUAL_EXPORT): return { ...state, fetchingIndividualExport: false, errorIndividualExport: formatServerError(action.payload), }; + case ERROR(ACTION_TYPE.GROUP_INDIVIDUAL_EXPORT): + return { + ...state, + fetchingGroupIndividualExport: false, + errorGroupIndividualExport: formatServerError(action.payload), + }; case REQUEST(ACTION_TYPE.MUTATION): return dispatchMutationReq(state, action); case ERROR(ACTION_TYPE.MUTATION): @@ -229,6 +314,10 @@ function reducer( return dispatchMutationResp(state, 'deleteIndividual', action); case SUCCESS(ACTION_TYPE.UPDATE_INDIVIDUAL): return dispatchMutationResp(state, 'updateIndividual', action); + case SUCCESS(ACTION_TYPE.DELETE_GROUP_INDIVIDUAL): + return dispatchMutationResp(state, 'removeIndividualFromGroup', action); + case SUCCESS(ACTION_TYPE.UPDATE_GROUP_INDIVIDUAL): + return dispatchMutationResp(state, 'editIndividualInGroup', action); case SUCCESS(ACTION_TYPE.DELETE_GROUP): return dispatchMutationResp(state, 'deleteGroup', action); case SUCCESS(ACTION_TYPE.UPDATE_GROUP): diff --git a/src/translations/en.json b/src/translations/en.json index c3899db..09a875a 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -87,6 +87,19 @@ }, "firstName": "first_name", "lastName": "last_name", - "dob": "date_of_birth" + "dob": "date_of_birth", + "role": "role" + }, + "groupIndividual": { + "individual": { + "role": "Role" + }, + "update": { + "label": "Update Individual", + "mutationLabel":"Update Individual {firstName} {lastName}" + }, + "groupIndividualRolePicker.HEAD": "HEAD", + "groupIndividualRolePicker.RECIPIENT": "RECIPIENT", + "groupIndividualRolePicker": "Role" } } \ No newline at end of file From e3b35720fcf518a4f343f30002558fe52eb1c5b5 Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 6 Jul 2023 09:22:39 +0200 Subject: [PATCH 21/90] CM-48: fix QA issues (#21) * CM-48: add roles to individuals in groups * CM-48: remove console log * CM-39: fix issues on server --- src/components/GroupIndividualFilter.js | 2 +- src/components/GroupIndividualSearcher.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/GroupIndividualFilter.js b/src/components/GroupIndividualFilter.js index 8272cd4..4488909 100644 --- a/src/components/GroupIndividualFilter.js +++ b/src/components/GroupIndividualFilter.js @@ -79,7 +79,7 @@ function GroupIndividualFilter({ { id: 'role', value, - filter: `role: "${value}"`, + filter: `role: ${value}`, }, ])} /> diff --git a/src/components/GroupIndividualSearcher.js b/src/components/GroupIndividualSearcher.js index 9cc365a..8b90e5c 100644 --- a/src/components/GroupIndividualSearcher.js +++ b/src/components/GroupIndividualSearcher.js @@ -294,7 +294,6 @@ function GroupIndividualSearcher({ }} exportFieldLabel={formatMessage(intl, 'individual', 'export.label')} cacheFiltersKey="groupIndividualsFilterCache" - resetFiltersOnUnmount /> {failedExport && ( From fc68e0465cf77aca43e1c875df015bb2ed6ce1df Mon Sep 17 00:00:00 2001 From: olewandowski1 <109145288+olewandowski1@users.noreply.github.com> Date: Tue, 18 Jul 2023 16:22:03 +0200 Subject: [PATCH 22/90] CM-217: use searcher for individuals/groups (#22) --- src/components/BenefitPlansListTab.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/BenefitPlansListTab.js b/src/components/BenefitPlansListTab.js index 747eb10..e1672db 100644 --- a/src/components/BenefitPlansListTab.js +++ b/src/components/BenefitPlansListTab.js @@ -31,7 +31,7 @@ function BenefitPlansListTabPanel({ rights={rights} individualId={individual?.id} groupId={group?.id} - pubRef="socialProtection.BenefitPlanSearcher" + pubRef="socialProtection.BenefitPlanSearcherForEntities" /> ); From 9d927559dcae1bd3dd435eabb493630ef60ba949 Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 19 Jul 2023 15:26:36 +0200 Subject: [PATCH 23/90] CM-158: fix download and switching tabs (#23) * CM-158: fix download and switching tabs * CM-158: fix eslint --- src/actions.js | 22 +++++++++++++++- src/components/GroupIndividualSearcher.js | 9 ++++++- src/components/GroupSearcher.js | 7 ++++- src/components/IndividualSearcher.js | 7 ++++- src/index.js | 3 ++- src/reducer.js | 31 ++++++++++++++++++++++- src/util/action-type.js | 1 + 7 files changed, 74 insertions(+), 6 deletions(-) diff --git a/src/actions.js b/src/actions.js index 4584713..3a6fc92 100644 --- a/src/actions.js +++ b/src/actions.js @@ -6,7 +6,9 @@ import { formatGQLString, } from '@openimis/fe-core'; import { ACTION_TYPE } from './reducer'; -import { ERROR, REQUEST, SUCCESS } from './util/action-type'; +import { + CLEAR, ERROR, REQUEST, SUCCESS, +} from './util/action-type'; const INDIVIDUAL_FULL_PROJECTION = [ 'id', @@ -204,3 +206,21 @@ export function downloadGroupIndividuals(params) { }`; return graphql(payload, ACTION_TYPE.GROUP_INDIVIDUAL_EXPORT); } + +export const clearGroupIndividualExport = () => (dispatch) => { + dispatch({ + type: CLEAR(ACTION_TYPE.GROUP_INDIVIDUAL_EXPORT), + }); +}; + +export const clearIndividualExport = () => (dispatch) => { + dispatch({ + type: CLEAR(ACTION_TYPE.INDIVIDUAL_EXPORT), + }); +}; + +export const clearGroupExport = () => (dispatch) => { + dispatch({ + type: CLEAR(ACTION_TYPE.GROUP_EXPORT), + }); +}; diff --git a/src/components/GroupIndividualSearcher.js b/src/components/GroupIndividualSearcher.js index 8b90e5c..865b2df 100644 --- a/src/components/GroupIndividualSearcher.js +++ b/src/components/GroupIndividualSearcher.js @@ -24,7 +24,11 @@ import { import EditIcon from '@material-ui/icons/Edit'; import DeleteIcon from '@material-ui/icons/Delete'; import { - fetchGroupIndividuals, deleteGroupIndividual, downloadGroupIndividuals, updateGroupIndividual, + fetchGroupIndividuals, + deleteGroupIndividual, + clearGroupIndividualExport, + downloadGroupIndividuals, + updateGroupIndividual, } from '../actions'; import { DEFAULT_PAGE_SIZE, @@ -56,6 +60,7 @@ function GroupIndividualSearcher({ updateGroupIndividual, groupIndividualsPageInfo, groupIndividualsTotalCount, + clearGroupIndividualExport, groupId, downloadGroupIndividuals, groupIndividualExport, @@ -231,6 +236,7 @@ function GroupIndividualSearcher({ groupIndividualExport, `${formatMessage(intl, 'individual', 'export.filename.individuals')}.csv`, )(); + clearGroupIndividualExport(); } }, [groupIndividualExport]); @@ -332,6 +338,7 @@ const mapDispatchToProps = (dispatch) => bindActionCreators( fetchGroupIndividuals, updateGroupIndividual, deleteGroupIndividual, + clearGroupIndividualExport, downloadGroupIndividuals, coreConfirm, clearConfirm, diff --git a/src/components/GroupSearcher.js b/src/components/GroupSearcher.js index 50db49f..be9ad36 100644 --- a/src/components/GroupSearcher.js +++ b/src/components/GroupSearcher.js @@ -22,7 +22,9 @@ import { } from '@material-ui/core'; import EditIcon from '@material-ui/icons/Edit'; import DeleteIcon from '@material-ui/icons/Delete'; -import { deleteGroup, downloadGroups, fetchGroups } from '../actions'; +import { + deleteGroup, downloadGroups, fetchGroups, clearGroupExport, +} from '../actions'; import { DEFAULT_PAGE_SIZE, ROWS_PER_PAGE_OPTIONS, @@ -48,6 +50,7 @@ function GroupSearcher({ deleteGroup, confirmed, submittingMutation, + clearGroupExport, mutation, coreConfirm, clearConfirm, @@ -169,6 +172,7 @@ function GroupSearcher({ useEffect(() => { if (groupExport) { downloadExport(groupExport, `${formatMessage(intl, 'individual', 'export.filename.groups')}.csv`)(); + clearGroupExport(); } }, [groupExport]); @@ -255,6 +259,7 @@ const mapDispatchToProps = (dispatch) => bindActionCreators( { fetchGroups, downloadGroups, + clearGroupExport, deleteGroup, coreConfirm, clearConfirm, diff --git a/src/components/IndividualSearcher.js b/src/components/IndividualSearcher.js index 208cb6a..bf48ff9 100644 --- a/src/components/IndividualSearcher.js +++ b/src/components/IndividualSearcher.js @@ -23,7 +23,9 @@ import { } from '@material-ui/core'; import EditIcon from '@material-ui/icons/Edit'; import DeleteIcon from '@material-ui/icons/Delete'; -import { fetchIndividuals, deleteIndividual, downloadIndividuals } from '../actions'; +import { + fetchIndividuals, deleteIndividual, downloadIndividuals, clearIndividualExport, +} from '../actions'; import { DEFAULT_PAGE_SIZE, ROWS_PER_PAGE_OPTIONS, @@ -52,6 +54,7 @@ function IndividualSearcher({ individuals, individualsPageInfo, individualsTotalCount, + clearIndividualExport, groupId, downloadIndividuals, individualExport, @@ -175,6 +178,7 @@ function IndividualSearcher({ useEffect(() => { if (individualExport) { downloadExport(individualExport, `${formatMessage(intl, 'individual', 'export.filename.individuals')}.csv`)(); + clearIndividualExport(); } }, [individualExport]); @@ -275,6 +279,7 @@ const mapDispatchToProps = (dispatch) => bindActionCreators( fetchIndividuals, deleteIndividual, downloadIndividuals, + clearIndividualExport, coreConfirm, clearConfirm, journalize, diff --git a/src/index.js b/src/index.js index 909c5cc..357974a 100644 --- a/src/index.js +++ b/src/index.js @@ -15,7 +15,7 @@ import GroupsPage from './pages/GroupsPage'; import GroupPage from './pages/GroupPage'; import { IndividualsListTabLabel, IndividualsListTabPanel } from './components/IndividualsListTab'; import GroupIndividualSearcher from './components/GroupIndividualSearcher'; -import { downloadIndividuals, fetchIndividuals } from './actions'; +import { clearIndividualExport, downloadIndividuals, fetchIndividuals } from './actions'; const ROUTE_INDIVIDUALS = 'individuals'; const ROUTE_INDIVIDUAL = 'individuals/individual'; @@ -40,6 +40,7 @@ const DEFAULT_CONFIG = { { key: 'individual.GroupIndividualSearcher', ref: GroupIndividualSearcher }, { key: 'individual.actions.fetchIndividuals', ref: fetchIndividuals }, { key: 'individual.actions.downloadIndividuals', ref: downloadIndividuals }, + { key: 'individual.actions.clearIndividualExport', ref: clearIndividualExport }, ], 'individual.TabPanel.label': [ IndividualsListTabLabel, diff --git a/src/reducer.js b/src/reducer.js index e37d74e..49e39f3 100644 --- a/src/reducer.js +++ b/src/reducer.js @@ -11,7 +11,9 @@ import { pageInfo, decodeId, } from '@openimis/fe-core'; -import { REQUEST, SUCCESS, ERROR } from './util/action-type'; +import { + REQUEST, SUCCESS, ERROR, CLEAR, +} from './util/action-type'; export const ACTION_TYPE = { MUTATION: 'INDIVIDUAL_MUTATION', @@ -306,6 +308,33 @@ function reducer( fetchingGroupIndividualExport: false, errorGroupIndividualExport: formatServerError(action.payload), }; + case CLEAR(ACTION_TYPE.GROUP_EXPORT): + return { + ...state, + fetchingGroupExport: false, + fetchedGroupExport: false, + groupExport: null, + groupExportPageInfo: {}, + errorGroupExport: null, + }; + case CLEAR(ACTION_TYPE.GROUP_INDIVIDUAL_EXPORT): + return { + ...state, + fetchingGroupIndividualExport: false, + fetchedGroupIndividualExport: false, + groupIndividualExport: null, + groupIndividualExportPageInfo: {}, + errorGroupIndividualExport: null, + }; + case CLEAR(ACTION_TYPE.INDIVIDUAL_EXPORT): + return { + ...state, + fetchingIndividualExport: false, + fetchedIndividualExport: false, + individualExport: null, + individualExportPageInfo: {}, + errorIndividualExport: null, + }; case REQUEST(ACTION_TYPE.MUTATION): return dispatchMutationReq(state, action); case ERROR(ACTION_TYPE.MUTATION): diff --git a/src/util/action-type.js b/src/util/action-type.js index 44404e2..be3f6c7 100644 --- a/src/util/action-type.js +++ b/src/util/action-type.js @@ -1,3 +1,4 @@ export const REQUEST = (actionTypeName) => `${actionTypeName}_REQ`; export const SUCCESS = (actionTypeName) => `${actionTypeName}_RESP`; export const ERROR = (actionTypeName) => `${actionTypeName}_ERR`; +export const CLEAR = (actionTypeName) => `${actionTypeName}_CLEAR`; From 4bc4c278bf6301316a9aeb445bda8a97b2e39b20 Mon Sep 17 00:00:00 2001 From: olewandowski1 <109145288+olewandowski1@users.noreply.github.com> Date: Wed, 19 Jul 2023 17:16:14 +0200 Subject: [PATCH 24/90] CM-202: removal of BenefitPlansListTab, using it via contribution logic (#24) --- src/components/BenefitPlansListTab.js | 40 ------------------- src/components/GroupHeadPanel.js | 2 +- src/components/IndividualHeadPanel.js | 2 +- src/constants.js | 5 ++- src/contributions/getBenefitPlansListTab.js | 27 +++++++++++++ src/index.js | 15 ++++--- ...iaryMainMenu.js => IndividualsMainMenu.js} | 10 ++--- src/translations/en.json | 17 +------- 8 files changed, 48 insertions(+), 70 deletions(-) delete mode 100644 src/components/BenefitPlansListTab.js create mode 100644 src/contributions/getBenefitPlansListTab.js rename src/menus/{BeneficiaryMainMenu.js => IndividualsMainMenu.js} (80%) diff --git a/src/components/BenefitPlansListTab.js b/src/components/BenefitPlansListTab.js deleted file mode 100644 index e1672db..0000000 --- a/src/components/BenefitPlansListTab.js +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import { Tab } from '@material-ui/core'; -import { formatMessage, PublishedComponent } from '@openimis/fe-core'; -import { BENEFIT_PLANS_LIST_TAB_VALUE } from '../constants'; - -function BenefitPlansListTabLabel({ - intl, onChange, tabStyle, isSelected, -}) { - return ( - - ); -} - -function BenefitPlansListTabPanel({ - value, rights, individual, group, -}) { - return ( - - - - ); -} - -export { BenefitPlansListTabLabel, BenefitPlansListTabPanel }; diff --git a/src/components/GroupHeadPanel.js b/src/components/GroupHeadPanel.js index e2ac19b..fa6ef6d 100644 --- a/src/components/GroupHeadPanel.js +++ b/src/components/GroupHeadPanel.js @@ -1,4 +1,4 @@ -import React, { Fragment } from 'react'; +import React from 'react'; import { Grid, Divider, Typography } from '@material-ui/core'; import { withModulesManager, diff --git a/src/components/IndividualHeadPanel.js b/src/components/IndividualHeadPanel.js index ca4937e..638c191 100644 --- a/src/components/IndividualHeadPanel.js +++ b/src/components/IndividualHeadPanel.js @@ -1,4 +1,4 @@ -import React, { Fragment } from 'react'; +import React from 'react'; import { Grid, Divider, Typography } from '@material-ui/core'; import { withModulesManager, diff --git a/src/constants.js b/src/constants.js index bc97f4b..f6437fb 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,4 +1,4 @@ -export const BENEFICIARY_MAIN_MENU_CONTRIBUTION_KEY = 'beneficiary.MainMenu'; +export const INDIVIDUALS_MAIN_MENU_CONTRIBUTION_KEY = 'individuals.MainMenu'; export const CONTAINS_LOOKUP = 'Icontains'; export const DEFAULT_DEBOUNCE_TIME = 500; export const DEFAULT_PAGE_SIZE = 10; @@ -25,6 +25,9 @@ export const INDIVIDUALS_LIST_TAB_VALUE = 'IndividualsListTab'; export const INDIVIDUAL_TABS_LABEL_CONTRIBUTION_KEY = 'individual.TabPanel.label'; export const INDIVIDUAL_TABS_PANEL_CONTRIBUTION_KEY = 'individual.TabPanel.panel'; +export const BENEFIT_PLAN_TABS_LABEL_CONTRIBUTION_KEY = 'individual.BenefitPlansListTabLabel'; +export const BENEFIT_PLAN_TABS_PANEL_CONTRIBUTION_KEY = 'individual.BenefitPlansListTabPanel'; + export const BENEFICIARY_STATUS = { POTENTIAL: 'POTENTIAL', ACTIVE: 'ACTIVE', diff --git a/src/contributions/getBenefitPlansListTab.js b/src/contributions/getBenefitPlansListTab.js new file mode 100644 index 0000000..993080a --- /dev/null +++ b/src/contributions/getBenefitPlansListTab.js @@ -0,0 +1,27 @@ +/* eslint-disable react/jsx-props-no-spreading */ + +import React from 'react'; +import { Contributions } from '@openimis/fe-core'; +import { BENEFIT_PLAN_TABS_LABEL_CONTRIBUTION_KEY, BENEFIT_PLAN_TABS_PANEL_CONTRIBUTION_KEY } from '../constants'; + +function BenefitPlansListTabLabel(props) { + return ( + + ); +} + +function BenefitPlansListTabPanel(props) { + return ( + + ); +} + +const getBenefitPlansListTab = () => ({ BenefitPlansListTabLabel, BenefitPlansListTabPanel }); + +export default getBenefitPlansListTab; diff --git a/src/index.js b/src/index.js index 357974a..722b013 100644 --- a/src/index.js +++ b/src/index.js @@ -4,16 +4,13 @@ import flatten from 'flat'; import messages_en from './translations/en.json'; import reducer from './reducer'; -import BeneficiaryMainMenu from './menus/BeneficiaryMainMenu'; +import IndividualsMainMenu from './menus/IndividualsMainMenu'; import IndividualsPage from './pages/IndividualsPage'; import IndividualPage from './pages/IndividualPage'; -import { - BenefitPlansListTabLabel, - BenefitPlansListTabPanel, -} from './components/BenefitPlansListTab'; import GroupsPage from './pages/GroupsPage'; import GroupPage from './pages/GroupPage'; import { IndividualsListTabLabel, IndividualsListTabPanel } from './components/IndividualsListTab'; +import getBenefitPlansListTab from './contributions/getBenefitPlansListTab'; import GroupIndividualSearcher from './components/GroupIndividualSearcher'; import { clearIndividualExport, downloadIndividuals, fetchIndividuals } from './actions'; @@ -23,10 +20,14 @@ const ROUTE_INDIVIDUAL_FROM_GROUP = 'groups/group/individuals/individual'; const ROUTE_GROUPS = 'groups'; const ROUTE_GROUP = 'groups/group'; +const BENEFIT_PLAN_TABS_LABEL_REF_KEY = 'socialProtection.BenefitPlansListTabLabel'; +const BENEFIT_PLAN_TABS_PANEL_REF_KEY = 'socialProtection.BenefitPlansListTabPanel'; +const { BenefitPlansListTabLabel, BenefitPlansListTabPanel } = getBenefitPlansListTab(); + const DEFAULT_CONFIG = { translations: [{ key: 'en', messages: flatten(messages_en) }], reducers: [{ key: 'individual', reducer }], - 'core.MainMenu': [BeneficiaryMainMenu], + 'core.MainMenu': [IndividualsMainMenu], 'core.Router': [ { path: ROUTE_INDIVIDUALS, component: IndividualsPage }, { path: ROUTE_GROUPS, component: GroupsPage }, @@ -50,6 +51,8 @@ const DEFAULT_CONFIG = { IndividualsListTabPanel, BenefitPlansListTabPanel, ], + 'individual.BenefitPlansListTabLabel': [BENEFIT_PLAN_TABS_LABEL_REF_KEY], + 'individual.BenefitPlansListTabPanel': [BENEFIT_PLAN_TABS_PANEL_REF_KEY], }; export const IndividualModule = (cfg) => ({ ...DEFAULT_CONFIG, ...cfg }); diff --git a/src/menus/BeneficiaryMainMenu.js b/src/menus/IndividualsMainMenu.js similarity index 80% rename from src/menus/BeneficiaryMainMenu.js rename to src/menus/IndividualsMainMenu.js index 5424b3a..aafd952 100644 --- a/src/menus/BeneficiaryMainMenu.js +++ b/src/menus/IndividualsMainMenu.js @@ -7,9 +7,9 @@ import { injectIntl } from 'react-intl'; import { connect } from 'react-redux'; import { Person, People } from '@material-ui/icons'; import { formatMessage, MainMenuContribution, withModulesManager } from '@openimis/fe-core'; -import { BENEFICIARY_MAIN_MENU_CONTRIBUTION_KEY } from '../constants'; +import { INDIVIDUALS_MAIN_MENU_CONTRIBUTION_KEY } from '../constants'; -function BeneficiaryMainMenu(props) { +function IndividualsMainMenu(props) { const entries = [ { text: formatMessage(props.intl, 'individual', 'menu.individuals'), @@ -24,14 +24,14 @@ function BeneficiaryMainMenu(props) { ]; entries.push( ...props.modulesManager - .getContribs(BENEFICIARY_MAIN_MENU_CONTRIBUTION_KEY) + .getContribs(INDIVIDUALS_MAIN_MENU_CONTRIBUTION_KEY) .filter((c) => !c.filter || c.filter(props.rights)), ); return ( ); @@ -41,4 +41,4 @@ const mapStateToProps = (state) => ({ rights: !!state.core && !!state.core.user && !!state.core.user.i_user ? state.core.user.i_user.rights : [], }); -export default injectIntl(withModulesManager(connect(mapStateToProps)(BeneficiaryMainMenu))); +export default injectIntl(withModulesManager(connect(mapStateToProps)(IndividualsMainMenu))); diff --git a/src/translations/en.json b/src/translations/en.json index 09a875a..e9d3313 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1,5 +1,5 @@ { - "mainMenuBeneficiary": "Beneficiares and Households", + "mainMenuIndividuals": "Beneficiares and Households", "emptyLabel": " ", "any": "Any", "editButtonTooltip": "Edit", @@ -33,21 +33,6 @@ }, "saveButton.tooltip.enabled": "Save changes", "saveButton.tooltip.disabled": "Please fill General Information fields first", - "benefitPlansList": { - "label": "BENEFIT PLANS" - }, - "benefitPlansActive": { - "label": "ACTIVE" - }, - "benefitPlansPotential": { - "label": "POTENTIAL" - }, - "benefitPlansGraduated": { - "label": "GRADUATED" - }, - "benefitPlansSuspended": { - "label": "SUSPENDED" - }, "individualsList": { "label": "MEMBERS" }, From 82cb584022ede6e63e5b6bde5b23a23d7ad5eb0b Mon Sep 17 00:00:00 2001 From: olewandowski1 <109145288+olewandowski1@users.noreply.github.com> Date: Mon, 24 Jul 2023 11:14:39 +0200 Subject: [PATCH 25/90] CM-134: use id in mutation labels to avoid mutation errors (#25) --- src/components/GroupIndividualSearcher.js | 3 +-- src/components/IndividualSearcher.js | 3 +-- src/pages/IndividualPage.js | 6 ++---- src/translations/en.json | 6 +++--- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/components/GroupIndividualSearcher.js b/src/components/GroupIndividualSearcher.js index 865b2df..c7ce451 100644 --- a/src/components/GroupIndividualSearcher.js +++ b/src/components/GroupIndividualSearcher.js @@ -156,8 +156,7 @@ function GroupIndividualSearcher({ updateGroupIndividual( editedGroupIndividual, formatMessageWithValues(intl, 'individual', 'groupIndividual.update.mutationLabel', { - firstName: editedGroupIndividual.individual.firstName, - lastName: editedGroupIndividual.individual.lastName, + id: editedGroupIndividual?.individual?.id, }), ); } diff --git a/src/components/IndividualSearcher.js b/src/components/IndividualSearcher.js index bf48ff9..4fd3cc4 100644 --- a/src/components/IndividualSearcher.js +++ b/src/components/IndividualSearcher.js @@ -89,8 +89,7 @@ function IndividualSearcher({ deleteIndividual( individualToDelete, formatMessageWithValues(intl, 'individual', 'individual.delete.mutationLabel', { - firstName: individualToDelete.firstName, - lastName: individualToDelete.lastName, + id: individualToDelete?.id, }), ); setDeletedIndividualUuids([...deletedIndividualUuids, individualToDelete.id]); diff --git a/src/pages/IndividualPage.js b/src/pages/IndividualPage.js index 03dc141..7880a7a 100644 --- a/src/pages/IndividualPage.js +++ b/src/pages/IndividualPage.js @@ -106,8 +106,7 @@ function IndividualPage({ updateIndividual( editedIndividual, formatMessageWithValues(intl, 'individual', 'individual.update.mutationLabel', { - firstName: individual?.firstName, - lastName: individual?.lastName, + id: individual?.id, }), ); }; @@ -115,8 +114,7 @@ function IndividualPage({ const deleteIndividualCallback = () => deleteIndividual( individual, formatMessageWithValues(intl, 'individual', 'individual.delete.mutationLabel', { - firstName: individual?.firstName, - lastName: individual?.lastName, + id: individual?.id, }), ); diff --git a/src/translations/en.json b/src/translations/en.json index e9d3313..bb0a970 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -25,11 +25,11 @@ "title": "Delete {firstName} {lastName}?", "message": "Deleting data does not mean erasing it from OpenIMIS database. The data will only be deactivated from the viewed list." }, - "mutationLabel": "Delete Individual {firstName} {lastName}" + "mutationLabel": "Delete Individual {id}" }, "update": { "label": "Update Individual", - "mutationLabel":"Update Individual {firstName} {lastName}" + "mutationLabel":"Update Individual {id}" }, "saveButton.tooltip.enabled": "Save changes", "saveButton.tooltip.disabled": "Please fill General Information fields first", @@ -81,7 +81,7 @@ }, "update": { "label": "Update Individual", - "mutationLabel":"Update Individual {firstName} {lastName}" + "mutationLabel":"Update Individual {id}" }, "groupIndividualRolePicker.HEAD": "HEAD", "groupIndividualRolePicker.RECIPIENT": "RECIPIENT", From 6712ab63bee4810eebe68f7cfa6e10f14ab1a686 Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 26 Jul 2023 13:40:15 +0200 Subject: [PATCH 26/90] Update README.md --- README.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 01afd02..1da5540 100644 --- a/README.md +++ b/README.md @@ -14,19 +14,31 @@ In development mode, you can use `npm link` and `npm start` to continuously scan * **Beneficiares and Households** (individual.mainMenu translation key) **Individuals** (individual.menu.individuals key), displayed if user has the right `159001` + **Groups** (individual.menu.groups key), displayed if user has the right `159001` ## Other Contributions * `core.Router`: registering `individuals`, `individual`, routes in openIMIS client-side router ## Available Contribution Points +* `individual.TabPanel.label`, labels for tab panels displaying Individuals +* `individual.TabPanel.panel`, panels for tab panels displaying Individuals ## Dispatched Redux Actions -* `INDIVIDUAL_INDIVIDUALS_{REQ|RESP|ERR}` fetching Individuals (as triggered by the searcher) -* `INDIVIDUAL_INDIVIDUAL_{REQ|RESP|ERR}` fetching chosen Individual * `INDIVIDUAL_MUTATION_{REQ|ERR}`, sending a mutation * `INDIVIDUAL_DELETE_INDIVIDUAL_RESP` receiving a result of delete Individual mutation * `INDIVIDUAL_UPDATE_INDIVIDUAL_RESP` receiving a result of update Individual mutation - +* `GROUP_DELETE_GROUP_RESP` receiving a result of delete Group mutation +* `GROUP_UPDATE_GROUP_RESP` receiving a result of update Group mutation +* `GROUP_INDIVIDUAL_DELETE_GROUP_INDIVIDUAL_RESP` receiving a result of delete GroupIndividual mutation +* `GROUP_INDIVIDUAL_UPDATE_GROUP_INDIVIDUAL_RESP` receiving a result of update GroupIndividual mutation +* `INDIVIDUAL_INDIVIDUALS_{REQ|RESP|ERR}` fetching Individuals (as triggered by the searcher) +* `INDIVIDUAL_INDIVIDUAL_{REQ|RESP|ERR}` fetching chosen Individual +* `GROUP_GROUPS_{REQ|RESP|ERR}` fetching Groups (as triggered by th searcher) +* `GROUP_GROUP_{REQ|RESP|ERR}` fetching chosen Group +* `GROUP_INDIVIDUAL_GROUP_INDIVIDUALS_{REQ|RESP|ERR}` fetching GroupIndividuals (as triggered by th searcher) +* `INDIVIDUAL_EXPORT_{REQ|RESP|ERR}` export of Individuals +* `GROUP_EXPORT_{REQ|RESP|ERR}` export of Groups +* `GROUP_INDIVIDUAL_EXPORT_{REQ|RESP|ERR}` export of GroupIndividuals ## Other Modules Listened Redux Actions None From 9d068ab947565c0df2e65a5eac0544c564e8c351 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 2 Oct 2023 15:24:20 +0200 Subject: [PATCH 27/90] CM-295: display group head (#26) * CM-295: display group head * CM-295: fix eslint --------- Co-authored-by: Jan --- src/actions.js | 1 + src/components/GroupSearcher.js | 4 ++++ src/translations/en.json | 2 ++ 3 files changed, 7 insertions(+) diff --git a/src/actions.js b/src/actions.js index 3a6fc92..f43591c 100644 --- a/src/actions.js +++ b/src/actions.js @@ -35,6 +35,7 @@ const GROUP_INDIVIDUAL_FULL_PROJECTION = [ const GROUP_FULL_PROJECTION = [ 'id', 'isDeleted', + 'head {firstName, lastName}', 'dateCreated', 'dateUpdated', 'jsonExt', diff --git a/src/components/GroupSearcher.js b/src/components/GroupSearcher.js index be9ad36..0299ac6 100644 --- a/src/components/GroupSearcher.js +++ b/src/components/GroupSearcher.js @@ -110,6 +110,7 @@ function GroupSearcher({ const headers = () => { const headers = [ 'group.id', + 'group.head', ]; if (rights.includes(RIGHT_GROUP_UPDATE)) { headers.push('emptyLabel'); @@ -120,6 +121,9 @@ function GroupSearcher({ const itemFormatters = () => { const formatters = [ (group) => group.id, + (group) => (group?.head + ? `${group?.head?.firstName} ${group?.head?.lastName}` + : formatMessage(intl, 'group', 'noHeadSpecified')), ]; if (rights.includes(RIGHT_GROUP_UPDATE)) { formatters.push((group) => ( diff --git a/src/translations/en.json b/src/translations/en.json index bb0a970..4aa95da 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -49,6 +49,8 @@ }, "group": { "id": "ID", + "head": "Head", + "noHeadSpecified": "head not specified", "individual.firstName": "Individual First Name", "individual.lastName": "Individual Last Name", "pageTitle": "Group {id}", From 5e020bb30b0abd1871a17f3e3b3d367cd25946ba Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 3 Oct 2023 11:37:43 +0200 Subject: [PATCH 28/90] CM-333: filter individuals by group id (#27) Co-authored-by: Jan --- src/components/GroupIndividualFilter.js | 11 ++++-- src/components/GroupIndividualSearcher.js | 41 +++++++++++++---------- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/src/components/GroupIndividualFilter.js b/src/components/GroupIndividualFilter.js index 4488909..02362e3 100644 --- a/src/components/GroupIndividualFilter.js +++ b/src/components/GroupIndividualFilter.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } 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, + intl, classes, filters, onChangeFilters, groupId, }) { const debouncedOnChangeFilters = _debounce(onChangeFilters, DEFAULT_DEBOUNCE_TIME); @@ -37,6 +37,13 @@ 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 c7ce451..fbe9ce8 100644 --- a/src/components/GroupIndividualSearcher.js +++ b/src/components/GroupIndividualSearcher.js @@ -1,41 +1,38 @@ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { injectIntl } from 'react-intl'; import { - withModulesManager, + clearConfirm, + coreConfirm, + downloadExport, + formatDateFromISO, formatMessage, formatMessageWithValues, - Searcher, - formatDateFromISO, - coreConfirm, - clearConfirm, + historyPush, journalize, + Searcher, withHistory, - historyPush, - downloadExport, + withModulesManager, } from '@openimis/fe-core'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import { - IconButton, Tooltip, Button, - Dialog, - DialogActions, - DialogTitle, + Button, Dialog, DialogActions, DialogTitle, IconButton, Tooltip, } from '@material-ui/core'; import EditIcon from '@material-ui/icons/Edit'; import DeleteIcon from '@material-ui/icons/Delete'; import { - fetchGroupIndividuals, - deleteGroupIndividual, clearGroupIndividualExport, + deleteGroupIndividual, downloadGroupIndividuals, + fetchGroupIndividuals, updateGroupIndividual, } from '../actions'; import { DEFAULT_PAGE_SIZE, - ROWS_PER_PAGE_OPTIONS, EMPTY_STRING, - RIGHT_GROUP_INDIVIDUAL_UPDATE, RIGHT_GROUP_INDIVIDUAL_DELETE, + RIGHT_GROUP_INDIVIDUAL_UPDATE, + ROWS_PER_PAGE_OPTIONS, } from '../constants'; import GroupIndividualFilter from './GroupIndividualFilter'; import GroupIndividualRolePicker from '../pickers/GroupIndividualRolePicker'; @@ -255,11 +252,21 @@ function GroupIndividualSearcher({ return filters; }; + const groupBeneficiaryFilter = (props) => ( + + ); + return (
Date: Wed, 4 Oct 2023 11:01:22 +0200 Subject: [PATCH 29/90] CM-296: add custom filtering to individuals and groups (#28) * CM-296: add custom filtering to individuals and groups * CM-296: fix linter --------- Co-authored-by: Jan --- src/components/GroupSearcher.js | 15 ++++++++++++++- src/components/IndividualSearcher.js | 19 ++++++++++++++++++- src/constants.js | 4 ++++ src/util/searcher-utils.js | 23 +++++++++++++++++++++++ 4 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 src/util/searcher-utils.js diff --git a/src/components/GroupSearcher.js b/src/components/GroupSearcher.js index 0299ac6..8dfc805 100644 --- a/src/components/GroupSearcher.js +++ b/src/components/GroupSearcher.js @@ -28,9 +28,10 @@ import { import { DEFAULT_PAGE_SIZE, ROWS_PER_PAGE_OPTIONS, - RIGHT_GROUP_UPDATE, RIGHT_GROUP_DELETE, + RIGHT_GROUP_UPDATE, RIGHT_GROUP_DELETE, SOCIAL_PROTECTION_MODULE_NAME, BENEFIT_PLAN_LABEL, } from '../constants'; import GroupFilter from './GroupFilter'; +import { applyNumberCircle } from '../util/searcher-utils'; function GroupSearcher({ intl, @@ -55,9 +56,12 @@ function GroupSearcher({ coreConfirm, clearConfirm, journalize, + CLEARED_STATE_FILTER, }) { const [groupToDelete, setGroupToDelete] = useState(null); const [deletedGroupUuids, setDeletedGroupUuids] = useState([]); + const [appliedCustomFilters, setAppliedCustomFilters] = useState([CLEARED_STATE_FILTER]); + const [appliedFiltersRowStructure, setAppliedFiltersRowStructure] = useState([CLEARED_STATE_FILTER]); const prevSubmittingMutationRef = useRef(); function groupUpdatePageUrl(group) { @@ -224,6 +228,15 @@ function GroupSearcher({ exportFieldLabel={formatMessage(intl, 'individual', 'export.label')} cacheFiltersKey="groupsFilterCache" resetFiltersOnUnmount + isCustomFiltering + moduleName={SOCIAL_PROTECTION_MODULE_NAME} + objectType={BENEFIT_PLAN_LABEL} + additionalCustomFilterParams={{ type: 'GROUP' }} + appliedCustomFilters={appliedCustomFilters} + setAppliedCustomFilters={setAppliedCustomFilters} + appliedFiltersRowStructure={appliedFiltersRowStructure} + setAppliedFiltersRowStructure={setAppliedFiltersRowStructure} + applyNumberCircle={applyNumberCircle} rowDisabled={isRowDisabled} rowLocked={isRowDisabled} /> diff --git a/src/components/IndividualSearcher.js b/src/components/IndividualSearcher.js index 4fd3cc4..25f7227 100644 --- a/src/components/IndividualSearcher.js +++ b/src/components/IndividualSearcher.js @@ -12,6 +12,7 @@ import { withHistory, historyPush, downloadExport, + CLEARED_STATE_FILTER, } from '@openimis/fe-core'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; @@ -31,8 +32,9 @@ import { ROWS_PER_PAGE_OPTIONS, EMPTY_STRING, RIGHT_INDIVIDUAL_UPDATE, - RIGHT_INDIVIDUAL_DELETE, + RIGHT_INDIVIDUAL_DELETE, BENEFIT_PLAN_LABEL, SOCIAL_PROTECTION_MODULE_NAME, } from '../constants'; +import { applyNumberCircle } from '../util/searcher-utils'; import IndividualFilter from './IndividualFilter'; function IndividualSearcher({ @@ -61,6 +63,8 @@ function IndividualSearcher({ errorIndividualExport, }) { const [individualToDelete, setIndividualToDelete] = useState(null); + const [appliedCustomFilters, setAppliedCustomFilters] = useState([CLEARED_STATE_FILTER]); + const [appliedFiltersRowStructure, setAppliedFiltersRowStructure] = useState([CLEARED_STATE_FILTER]); const [deletedIndividualUuids, setDeletedIndividualUuids] = useState([]); const prevSubmittingMutationRef = useRef(); @@ -197,6 +201,10 @@ function IndividualSearcher({ return filters; }; + useEffect(() => { + // refresh when appliedCustomFilters is changed + }, [appliedCustomFilters]); + return (
( +
+ {number} +
+); From b7a75efa28f0146befdd5a9a2dc563ea09ef8506 Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 11 Oct 2023 08:52:32 +0200 Subject: [PATCH 30/90] add-sonar-ci: add sonar files (#29) Co-authored-by: Jan --- .github/workflows/ci.yaml | 23 +++++++++++++++++++++++ sonar-project.properties | 6 ++++++ 2 files changed, 29 insertions(+) create mode 100644 .github/workflows/ci.yaml create mode 100644 sonar-project.properties diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..da93b2d --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,23 @@ +name: Sonar CI pipeline +on: + push: + branches: + - main + - 'release/**' + - develop + - 'feature/**' + pull_request: + types: [opened, synchronize, reopened] +jobs: + sonarcloud: + name: SonarCloud + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: SonarCloud Scan + uses: SonarSource/sonarcloud-github-action@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..58c9dc7 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,6 @@ +sonar.projectKey=openimis_openimis-fe-individual_js +sonar.organization=openimis-1 +sonar.projectName=openimis-fe-individual_js + +sonar.sources=src +sonar.sourceEncoding=UTF-8 From 6619ae4760f1acc772e3a7df87a38404a13b4dd0 Mon Sep 17 00:00:00 2001 From: sniedzielski Date: Wed, 27 Dec 2023 17:12:00 +0100 Subject: [PATCH 31/90] CM-381: added changelog for individual --- src/actions.js | 5 + src/components/IndividualChangelogTab.js | 38 ++++++ src/components/IndividualHistorySearcher.js | 124 ++++++++++++++++++++ src/constants.js | 1 + src/index.js | 8 ++ src/reducer.js | 37 ++++++ src/translations/en.json | 20 +++- 7 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 src/components/IndividualChangelogTab.js create mode 100644 src/components/IndividualHistorySearcher.js diff --git a/src/actions.js b/src/actions.js index f43591c..4b8433f 100644 --- a/src/actions.js +++ b/src/actions.js @@ -61,6 +61,11 @@ export function fetchIndividual(params) { return graphql(payload, ACTION_TYPE.GET_INDIVIDUAL); } +export function fetchIndividualHistory(individualId) { + const payload = formatPageQueryWithCount('individualHistory', [`id: "${individualId}"`], INDIVIDUAL_FULL_PROJECTION); + return graphql(payload, ACTION_TYPE.SEARCH_INDIVIDUAL_HISTORY); +} + export function fetchGroup(params) { const payload = formatPageQuery('group', params, GROUP_FULL_PROJECTION); return graphql(payload, ACTION_TYPE.GET_GROUP); diff --git a/src/components/IndividualChangelogTab.js b/src/components/IndividualChangelogTab.js new file mode 100644 index 0000000..b2089c3 --- /dev/null +++ b/src/components/IndividualChangelogTab.js @@ -0,0 +1,38 @@ +import React from 'react'; +import { Tab } from '@material-ui/core'; +import { formatMessage, PublishedComponent } from '@openimis/fe-core'; +import { INDIVIDUAL_CHANGELOG_TAB_VALUE } from '../constants'; + +function IndividalChangelogTabLabel({ + intl, onChange, tabStyle, isSelected, +}) { + return ( + + ); +} + +function IndividalChangelogTabPanel({ + value, individual, +}) { + return ( + + + + ); +} + +export { IndividalChangelogTabLabel, IndividalChangelogTabPanel }; diff --git a/src/components/IndividualHistorySearcher.js b/src/components/IndividualHistorySearcher.js new file mode 100644 index 0000000..eb2d85f --- /dev/null +++ b/src/components/IndividualHistorySearcher.js @@ -0,0 +1,124 @@ +import React from 'react'; +import { injectIntl } from 'react-intl'; +import { + withModulesManager, + formatMessageWithValues, + Searcher, + formatDateFromISO, + withHistory, +} from '@openimis/fe-core'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import { fetchIndividualHistory } from '../actions'; +import { + DEFAULT_PAGE_SIZE, + ROWS_PER_PAGE_OPTIONS, + EMPTY_STRING, +} from '../constants'; + +function IndividualHistorySearcher({ + intl, + modulesManager, + individualHistory, + fetchIndividualHistory, + individualHistoryPageInfo, + fetchingIndividualHistory, + fetchedIndividualHistory, + errorIndividualHistory, + individualHistoryTotalCount, + individualId, +}) { + const fetch = () => fetchIndividualHistory(individualId); + + const headers = () => { + const headers = [ + 'individualHistory.firstName', + 'individualHistory.lastName', + 'individualHistory.dob', + 'individualHistory.jsonExt', + ]; + return headers; + }; + + const itemFormatters = () => { + const formatters = [ + (individualHistory) => individualHistory.firstName, + (individualHistory) => individualHistory.lastName, + (individualHistory) => (individualHistory.dob ? + formatDateFromISO(modulesManager, intl, individualHistory.dob) : EMPTY_STRING + ), + ]; + return formatters; + }; + + const rowIdentifier = (individualHistory) => individualHistory.id; + + const sorts = () => [ + ['firstName', true], + ['lastName', true], + ['dob', true], + ]; + + const defaultFilters = () => { + const filters = { + isDeleted: { + value: false, + filter: 'isDeleted: false', + }, + }; + if (individualId !== null && individualId !== undefined) { + filters.individualId = { + value: individualId, + filter: `id: "${individualId}"`, + }; + } + return filters; + }; + + return ( +
+ +
+ ); +} + +const mapStateToProps = (state) => ({ + fetchingIndividualHistory: state.individual.fetchingIndividualHistory, + fetchedIndividualHistory: state.individual.fetchedIndividualHistory, + errorIndividualHistory: state.individual.errorIndividualHistory, + individualHistory: state.individual.individualHistory, + individualHistoryPageInfo: state.individual.individualHistoryPageInfo, + individualHistoryTotalCount: state.individual.individualHistoryTotalCount, +}); + +const mapDispatchToProps = (dispatch) => bindActionCreators( + { + fetchIndividualHistory, + }, + dispatch, +); + +export default withHistory( + withModulesManager(injectIntl(connect(mapStateToProps, mapDispatchToProps)(IndividualHistorySearcher))), +); diff --git a/src/constants.js b/src/constants.js index 1c1b222..b1b8597 100644 --- a/src/constants.js +++ b/src/constants.js @@ -22,6 +22,7 @@ export const RIGHT_GROUP_DELETE = 180004; export const BENEFIT_PLANS_LIST_TAB_VALUE = 'BenefitPlansListTab'; export const INDIVIDUALS_LIST_TAB_VALUE = 'IndividualsListTab'; +export const INDIVIDUAL_CHANGELOG_TAB_VALUE = 'IndividualChangelogTab'; export const INDIVIDUAL_TABS_LABEL_CONTRIBUTION_KEY = 'individual.TabPanel.label'; export const INDIVIDUAL_TABS_PANEL_CONTRIBUTION_KEY = 'individual.TabPanel.panel'; diff --git a/src/index.js b/src/index.js index 722b013..0349385 100644 --- a/src/index.js +++ b/src/index.js @@ -10,9 +10,14 @@ import IndividualPage from './pages/IndividualPage'; import GroupsPage from './pages/GroupsPage'; import GroupPage from './pages/GroupPage'; import { IndividualsListTabLabel, IndividualsListTabPanel } from './components/IndividualsListTab'; +import { + IndividalChangelogTabLabel, + IndividalChangelogTabPanel, +} from './components/IndividualChangelogTab'; import getBenefitPlansListTab from './contributions/getBenefitPlansListTab'; import GroupIndividualSearcher from './components/GroupIndividualSearcher'; import { clearIndividualExport, downloadIndividuals, fetchIndividuals } from './actions'; +import IndividualHistorySearcher from './components/IndividualHistorySearcher'; const ROUTE_INDIVIDUALS = 'individuals'; const ROUTE_INDIVIDUAL = 'individuals/individual'; @@ -42,12 +47,15 @@ const DEFAULT_CONFIG = { { key: 'individual.actions.fetchIndividuals', ref: fetchIndividuals }, { key: 'individual.actions.downloadIndividuals', ref: downloadIndividuals }, { key: 'individual.actions.clearIndividualExport', ref: clearIndividualExport }, + { key: 'individual.IndividualHistorySearcher', ref: IndividualHistorySearcher }, ], 'individual.TabPanel.label': [ + IndividalChangelogTabLabel, IndividualsListTabLabel, BenefitPlansListTabLabel, ], 'individual.TabPanel.panel': [ + IndividalChangelogTabPanel, IndividualsListTabPanel, BenefitPlansListTabPanel, ], diff --git a/src/reducer.js b/src/reducer.js index 49e39f3..0c8dfc2 100644 --- a/src/reducer.js +++ b/src/reducer.js @@ -31,6 +31,7 @@ export const ACTION_TYPE = { GROUP_EXPORT: 'GROUP_EXPORT', INDIVIDUAL_EXPORT: 'INDIVIDUAL_EXPORT', GROUP_INDIVIDUAL_EXPORT: 'GROUP_INDIVIDUAL_EXPORT', + SEARCH_INDIVIDUAL_HISTORY: 'SEARCH_INDIVIDUAL_HISTORY', }; function reducer( @@ -78,6 +79,12 @@ function reducer( groupIndividualsExport: null, groupIndividualsExportPageInfo: {}, errorGroupIndividualsExport: null, + fetchingIndividualHistory: false, + errorIndividualHistory: null, + fetchedIndividualHistory: false, + individualHistory: [], + individualHistoryPageInfo: {}, + individualHistoryTotalCount: 0, }, action, ) { @@ -92,6 +99,16 @@ function reducer( individualsTotalCount: 0, errorIndividuals: null, }; + case REQUEST(ACTION_TYPE.SEARCH_INDIVIDUAL_HISTORY): + return { + ...state, + fetchingIndividualHistory: true, + fetchedIndividualHistory: false, + individualHistory: [], + individualHistoryPageInfo: {}, + individualHistoryTotalCount: 0, + errorIndividualHistory: null, + }; case REQUEST(ACTION_TYPE.SEARCH_GROUP_INDIVIDUALS): return { ...state, @@ -141,6 +158,20 @@ function reducer( individualsTotalCount: action.payload.data.individual ? action.payload.data.individual.totalCount : null, errorIndividuals: formatGraphQLError(action.payload), }; + case SUCCESS(ACTION_TYPE.SEARCH_INDIVIDUAL_HISTORY): + return { + ...state, + fetchingIndividualHistory: false, + fetchedIndividualHistory: true, + individualHistory: parseData(action.payload.data.individualHistory)?.map((individualHistory) => ({ + ...individualHistory, + id: decodeId(individualHistory.id), + })), + individualHistoryPageInfo: pageInfo(action.payload.data.individualHistory), + individualHistoryTotalCount: action.payload.data.individualHistory + ? action.payload.data.individualHistory.totalCount : null, + errorIndividualHistory: formatGraphQLError(action.payload), + }; case SUCCESS(ACTION_TYPE.SEARCH_GROUP_INDIVIDUALS): return { ...state, @@ -212,6 +243,12 @@ function reducer( fetchingIndividuals: false, errorIndividuals: formatServerError(action.payload), }; + case ERROR(ACTION_TYPE.SEARCH_INDIVIDUAL_HISTORY): + return { + ...state, + fetchingIndividualHistory: false, + errorIndividualHistory: formatServerError(action.payload), + }; case ERROR(ACTION_TYPE.SEARCH_GROUP_INDIVIDUALS): return { ...state, diff --git a/src/translations/en.json b/src/translations/en.json index 4aa95da..b2e6cd6 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -37,12 +37,30 @@ "label": "MEMBERS" }, "any": "ANY", - "ok": "ok" + "ok": "ok", + "individualChangelog.label": "Change Log" + }, + "individualHistory": { + "pageTitle": "Individual {firstName} {lastName}", + "headPanelTitle": "General Information", + "firstName": "First Name", + "lastName": "Last Name", + "dob": "Day of birth", + "individualsList": { + "label": "MEMBERS" + }, + "any": "ANY", + "ok": "ok", + "jsonExt": "Additional fields" }, "individuals": { "pageTitle": "Individuals", "searcherResultsTitle": "{individualsTotalCount} Individuals Found" }, + "individualHistoryList": { + "pageTitle": "Individuals History", + "searcherResultsTitle": "{individualHistoryTotalCount} Historical Records Found" + }, "groups": { "pageTitle": "Groups", "searcherResultsTitle": "{groupsTotalCount} Groups Found" From 78c612586d3f0bf02df928c0e820d80250157140 Mon Sep 17 00:00:00 2001 From: sniedzielski Date: Wed, 27 Dec 2023 17:54:10 +0100 Subject: [PATCH 32/90] CM-381: added filtering in changelog --- src/actions.js | 4 +- src/components/IndividualHistoryFilter.js | 76 +++++++++++++++++++++ src/components/IndividualHistorySearcher.js | 13 +++- 3 files changed, 90 insertions(+), 3 deletions(-) create mode 100644 src/components/IndividualHistoryFilter.js diff --git a/src/actions.js b/src/actions.js index 4b8433f..51bd6d4 100644 --- a/src/actions.js +++ b/src/actions.js @@ -61,8 +61,8 @@ export function fetchIndividual(params) { return graphql(payload, ACTION_TYPE.GET_INDIVIDUAL); } -export function fetchIndividualHistory(individualId) { - const payload = formatPageQueryWithCount('individualHistory', [`id: "${individualId}"`], INDIVIDUAL_FULL_PROJECTION); +export function fetchIndividualHistory(params) { + const payload = formatPageQueryWithCount('individualHistory', params, INDIVIDUAL_FULL_PROJECTION); return graphql(payload, ACTION_TYPE.SEARCH_INDIVIDUAL_HISTORY); } diff --git a/src/components/IndividualHistoryFilter.js b/src/components/IndividualHistoryFilter.js new file mode 100644 index 0000000..6f2683c --- /dev/null +++ b/src/components/IndividualHistoryFilter.js @@ -0,0 +1,76 @@ +import React from 'react'; +import { injectIntl } from 'react-intl'; +import { TextInput, PublishedComponent } from '@openimis/fe-core'; +import { Grid } 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 { defaultFilterStyles } from '../util/styles'; + +function IndividualHistoryFilter({ + 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: 'dob', + value: v, + filter: `dob: "${v}"`, + }, + ])} + /> + + + ); +} + +export default injectIntl(withTheme(withStyles(defaultFilterStyles)(IndividualHistoryFilter))); diff --git a/src/components/IndividualHistorySearcher.js b/src/components/IndividualHistorySearcher.js index eb2d85f..312dcfc 100644 --- a/src/components/IndividualHistorySearcher.js +++ b/src/components/IndividualHistorySearcher.js @@ -15,6 +15,7 @@ import { ROWS_PER_PAGE_OPTIONS, EMPTY_STRING, } from '../constants'; +import IndividualHistoryFilter from './IndividualHistoryFilter'; function IndividualHistorySearcher({ intl, @@ -28,7 +29,7 @@ function IndividualHistorySearcher({ individualHistoryTotalCount, individualId, }) { - const fetch = () => fetchIndividualHistory(individualId); + const fetch = (params) => fetchIndividualHistory(params); const headers = () => { const headers = [ @@ -75,10 +76,20 @@ function IndividualHistorySearcher({ return filters; }; + const individualHistoryFilter = (props) => ( + + ); + return (
Date: Thu, 28 Dec 2023 11:04:08 +0100 Subject: [PATCH 33/90] CM-380: add update individual task table (#31) Co-authored-by: Jan --- src/components/tasks/IndividualUpdateTasks.js | 16 ++++++++++++++++ src/index.js | 12 ++++++++++++ src/translations/en.json | 5 +++++ 3 files changed, 33 insertions(+) create mode 100644 src/components/tasks/IndividualUpdateTasks.js diff --git a/src/components/tasks/IndividualUpdateTasks.js b/src/components/tasks/IndividualUpdateTasks.js new file mode 100644 index 0000000..70bdc81 --- /dev/null +++ b/src/components/tasks/IndividualUpdateTasks.js @@ -0,0 +1,16 @@ +import React from 'react'; +import { FormattedMessage } from '@openimis/fe-core'; + +const IndividualUpdateTaskTableHeaders = () => [ + , + , + , +]; + +const IndividualUpdateTaskItemFormatters = () => [ + (individual) => individual?.first_name, + (individual) => individual?.last_name, + (individual) => individual?.dob, +]; + +export { IndividualUpdateTaskTableHeaders, IndividualUpdateTaskItemFormatters }; diff --git a/src/index.js b/src/index.js index 722b013..acb7984 100644 --- a/src/index.js +++ b/src/index.js @@ -2,6 +2,8 @@ /* eslint-disable camelcase */ /* eslint-disable import/prefer-default-export */ import flatten from 'flat'; +import { FormattedMessage } from '@openimis/fe-core'; +import React from 'react'; import messages_en from './translations/en.json'; import reducer from './reducer'; import IndividualsMainMenu from './menus/IndividualsMainMenu'; @@ -13,6 +15,10 @@ import { IndividualsListTabLabel, IndividualsListTabPanel } from './components/I import getBenefitPlansListTab from './contributions/getBenefitPlansListTab'; import GroupIndividualSearcher from './components/GroupIndividualSearcher'; import { clearIndividualExport, downloadIndividuals, fetchIndividuals } from './actions'; +import { + IndividualUpdateTaskItemFormatters, + IndividualUpdateTaskTableHeaders, +} from './components/tasks/IndividualUpdateTasks'; const ROUTE_INDIVIDUALS = 'individuals'; const ROUTE_INDIVIDUAL = 'individuals/individual'; @@ -53,6 +59,12 @@ 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, + taskSource: ['IndividualService'], + }], }; export const IndividualModule = (cfg) => ({ ...DEFAULT_CONFIG, ...cfg }); diff --git a/src/translations/en.json b/src/translations/en.json index 4aa95da..cd9f5e5 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -36,6 +36,11 @@ "individualsList": { "label": "MEMBERS" }, + "tasks": { + "update": { + "title": "Individual Update Tasks" + } + }, "any": "ANY", "ok": "ok" }, From efa319fedba9689834e3cf5fc5d69b2fbe7fa219 Mon Sep 17 00:00:00 2001 From: sniedzielski Date: Thu, 28 Dec 2023 12:45:22 +0100 Subject: [PATCH 34/90] CM-381: improving UI of changelog --- src/actions.js | 1 + src/components/IndividualHistorySearcher.js | 13 +++++++++++-- src/index.js | 4 ++-- src/translations/en.json | 2 ++ 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/actions.js b/src/actions.js index 51bd6d4..ca1f02e 100644 --- a/src/actions.js +++ b/src/actions.js @@ -19,6 +19,7 @@ const INDIVIDUAL_FULL_PROJECTION = [ 'lastName', 'dob', 'jsonExt', + 'version', ]; const GROUP_INDIVIDUAL_FULL_PROJECTION = [ diff --git a/src/components/IndividualHistorySearcher.js b/src/components/IndividualHistorySearcher.js index 312dcfc..81675b7 100644 --- a/src/components/IndividualHistorySearcher.js +++ b/src/components/IndividualHistorySearcher.js @@ -36,6 +36,8 @@ function IndividualHistorySearcher({ 'individualHistory.firstName', 'individualHistory.lastName', 'individualHistory.dob', + 'individualHistory.dateUpdated', + 'individualHistory.version', 'individualHistory.jsonExt', ]; return headers; @@ -45,9 +47,14 @@ function IndividualHistorySearcher({ const formatters = [ (individualHistory) => individualHistory.firstName, (individualHistory) => individualHistory.lastName, - (individualHistory) => (individualHistory.dob ? - formatDateFromISO(modulesManager, intl, individualHistory.dob) : EMPTY_STRING + (individualHistory) => (individualHistory.dob + ? formatDateFromISO(modulesManager, intl, individualHistory.dob) : EMPTY_STRING ), + (individualHistory) => (individualHistory.dateUpdated + ? formatDateFromISO(modulesManager, intl, individualHistory.dateUpdated) : EMPTY_STRING + ), + (individualHistory) => individualHistory.version, + (individualHistory) => individualHistory.jsonExt, ]; return formatters; }; @@ -58,6 +65,8 @@ function IndividualHistorySearcher({ ['firstName', true], ['lastName', true], ['dob', true], + ['dateUpdated', true], + ['version', true], ]; const defaultFilters = () => { diff --git a/src/index.js b/src/index.js index 0349385..bea270d 100644 --- a/src/index.js +++ b/src/index.js @@ -50,14 +50,14 @@ const DEFAULT_CONFIG = { { key: 'individual.IndividualHistorySearcher', ref: IndividualHistorySearcher }, ], 'individual.TabPanel.label': [ - IndividalChangelogTabLabel, IndividualsListTabLabel, BenefitPlansListTabLabel, + IndividalChangelogTabLabel, ], 'individual.TabPanel.panel': [ - IndividalChangelogTabPanel, IndividualsListTabPanel, BenefitPlansListTabPanel, + IndividalChangelogTabPanel, ], 'individual.BenefitPlansListTabLabel': [BENEFIT_PLAN_TABS_LABEL_REF_KEY], 'individual.BenefitPlansListTabPanel': [BENEFIT_PLAN_TABS_PANEL_REF_KEY], diff --git a/src/translations/en.json b/src/translations/en.json index b2e6cd6..04130f5 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -46,6 +46,8 @@ "firstName": "First Name", "lastName": "Last Name", "dob": "Day of birth", + "dateUpdated": "Date Updated", + "version": "Version", "individualsList": { "label": "MEMBERS" }, From a89928a215b68e4348ac41530a963fa5a9a7d45a Mon Sep 17 00:00:00 2001 From: Jan Date: Fri, 29 Dec 2023 12:56:18 +0100 Subject: [PATCH 35/90] CM-406: add change log fe for group (#33) Co-authored-by: Jan --- src/actions.js | 6 ++ src/components/GroupChangelogTab.js | 38 +++++++++ src/components/GroupHistoryFilter.js | 58 +++++++++++++ src/components/GroupHistorySearcher.js | 113 +++++++++++++++++++++++++ src/constants.js | 1 + src/index.js | 5 ++ src/reducer.js | 37 ++++++++ src/translations/en.json | 18 +++- 8 files changed, 275 insertions(+), 1 deletion(-) create mode 100644 src/components/GroupChangelogTab.js create mode 100644 src/components/GroupHistoryFilter.js create mode 100644 src/components/GroupHistorySearcher.js diff --git a/src/actions.js b/src/actions.js index f43591c..ed4457e 100644 --- a/src/actions.js +++ b/src/actions.js @@ -39,6 +39,7 @@ const GROUP_FULL_PROJECTION = [ 'dateCreated', 'dateUpdated', 'jsonExt', + 'version', ]; export function fetchIndividuals(params) { @@ -66,6 +67,11 @@ export function fetchGroup(params) { return graphql(payload, ACTION_TYPE.GET_GROUP); } +export function fetchGroupHistory(params) { + const payload = formatPageQueryWithCount('groupHistory', params, GROUP_FULL_PROJECTION); + return graphql(payload, ACTION_TYPE.SEARCH_GROUP_HISTORY); +} + export function deleteIndividual(individual, clientMutationLabel) { const individualUuids = `ids: ["${individual?.id}"]`; const mutation = formatMutation('deleteIndividual', individualUuids, clientMutationLabel); diff --git a/src/components/GroupChangelogTab.js b/src/components/GroupChangelogTab.js new file mode 100644 index 0000000..4a87817 --- /dev/null +++ b/src/components/GroupChangelogTab.js @@ -0,0 +1,38 @@ +import React from 'react'; +import { Tab } from '@material-ui/core'; +import { formatMessage, PublishedComponent } from '@openimis/fe-core'; +import { GROUP_CHANGELOG_TAB_VALUE } from '../constants'; + +function GroupChangelogTabLabel({ + intl, onChange, tabStyle, isSelected, +}) { + return ( + + ); +} + +function GroupChangelogTabPanel({ + value, group, +}) { + return ( + + + + ); +} + +export { GroupChangelogTabLabel, GroupChangelogTabPanel }; diff --git a/src/components/GroupHistoryFilter.js b/src/components/GroupHistoryFilter.js new file mode 100644 index 0000000..d814855 --- /dev/null +++ b/src/components/GroupHistoryFilter.js @@ -0,0 +1,58 @@ +import React from 'react'; +import { injectIntl } from 'react-intl'; +import { TextInput } from '@openimis/fe-core'; +import { Grid } 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 { defaultFilterStyles } from '../util/styles'; + +function GroupHistoryFilter({ + classes, filters, onChangeFilters, +}) { + 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 injectIntl(withTheme(withStyles(defaultFilterStyles)(GroupHistoryFilter))); diff --git a/src/components/GroupHistorySearcher.js b/src/components/GroupHistorySearcher.js new file mode 100644 index 0000000..9afe3e2 --- /dev/null +++ b/src/components/GroupHistorySearcher.js @@ -0,0 +1,113 @@ +import React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { injectIntl } from 'react-intl'; +import { + useModulesManager, + useTranslations, + Searcher, + withHistory, + withModulesManager, +} from '@openimis/fe-core'; +import { DEFAULT_PAGE_SIZE, EMPTY_STRING, ROWS_PER_PAGE_OPTIONS } from '../constants'; +import GroupHistoryFilter from './GroupHistoryFilter'; +import { fetchGroupHistory } from '../actions'; + +function GroupHistorySearcher({ + groupId, +}) { + const modulesManager = useModulesManager(); + const dispatch = useDispatch(); + const { formatDateFromISO, formatMessageWithValues } = useTranslations('individual', modulesManager); + + const fetchingGroupHistory = useSelector((state) => state.individual.fetchingGroupHistory); + const fetchedGroupHistory = useSelector((state) => state.individual.fetchedGroupHistory); + const errorGroupHistory = useSelector((state) => state.individual.errorGroupHistory); + const groupHistory = useSelector((state) => state.individual.groupHistory); + const groupHistoryPageInfo = useSelector((state) => state.individual.groupHistoryPageInfo); + const groupHistoryTotalCount = useSelector((state) => state.individual.groupHistoryTotalCount); + const fetch = (params) => dispatch(fetchGroupHistory(params)); + + const headers = () => [ + 'groupHistory.id', + 'groupHistory.head', + 'groupHistory.dateUpdated', + 'groupHistory.version', + 'groupHistory.jsonExt', + ]; + + const itemFormatters = () => [ + (groupHistory) => groupHistory.id, + (groupHistory) => groupHistory.head, + (groupHistory) => (groupHistory.dateUpdated + ? formatDateFromISO(groupHistory.dateUpdated) : EMPTY_STRING + ), + (groupHistory) => groupHistory.version, + (groupHistory) => groupHistory.jsonExt, + ]; + + const rowIdentifier = (groupHistory) => groupHistory.id; + + const sorts = () => [ + ['id', false], + ['head', true], + ['dateUpdated', true], + ['version', true], + ]; + + const defaultFilters = () => { + const filters = { + isDeleted: { + value: false, + filter: 'isDeleted: false', + }, + }; + if (groupId !== null && groupId !== undefined) { + filters.groupId = { + value: groupId, + filter: `id: "${groupId}"`, + }; + } + return filters; + }; + + const groupHistoryFilter = (props) => ( + + ); + + return ( +
+ +
+ ); +} + +export default withHistory( + withModulesManager(injectIntl((GroupHistorySearcher))), +); diff --git a/src/constants.js b/src/constants.js index 1c1b222..223655c 100644 --- a/src/constants.js +++ b/src/constants.js @@ -22,6 +22,7 @@ export const RIGHT_GROUP_DELETE = 180004; export const BENEFIT_PLANS_LIST_TAB_VALUE = 'BenefitPlansListTab'; export const INDIVIDUALS_LIST_TAB_VALUE = 'IndividualsListTab'; +export const GROUP_CHANGELOG_TAB_VALUE = 'GroupChangelogTab'; export const INDIVIDUAL_TABS_LABEL_CONTRIBUTION_KEY = 'individual.TabPanel.label'; export const INDIVIDUAL_TABS_PANEL_CONTRIBUTION_KEY = 'individual.TabPanel.panel'; diff --git a/src/index.js b/src/index.js index acb7984..95b5d23 100644 --- a/src/index.js +++ b/src/index.js @@ -19,6 +19,8 @@ import { IndividualUpdateTaskItemFormatters, IndividualUpdateTaskTableHeaders, } from './components/tasks/IndividualUpdateTasks'; +import GroupHistorySearcher from './components/GroupHistorySearcher'; +import { GroupChangelogTabLabel, GroupChangelogTabPanel } from './components/GroupChangelogTab'; const ROUTE_INDIVIDUALS = 'individuals'; const ROUTE_INDIVIDUAL = 'individuals/individual'; @@ -48,14 +50,17 @@ const DEFAULT_CONFIG = { { key: 'individual.actions.fetchIndividuals', ref: fetchIndividuals }, { key: 'individual.actions.downloadIndividuals', ref: downloadIndividuals }, { key: 'individual.actions.clearIndividualExport', ref: clearIndividualExport }, + { key: 'individual.GroupHistorySearcher', ref: GroupHistorySearcher }, ], 'individual.TabPanel.label': [ IndividualsListTabLabel, BenefitPlansListTabLabel, + GroupChangelogTabLabel, ], 'individual.TabPanel.panel': [ IndividualsListTabPanel, BenefitPlansListTabPanel, + GroupChangelogTabPanel, ], 'individual.BenefitPlansListTabLabel': [BENEFIT_PLAN_TABS_LABEL_REF_KEY], 'individual.BenefitPlansListTabPanel': [BENEFIT_PLAN_TABS_PANEL_REF_KEY], diff --git a/src/reducer.js b/src/reducer.js index 49e39f3..1895665 100644 --- a/src/reducer.js +++ b/src/reducer.js @@ -31,6 +31,7 @@ export const ACTION_TYPE = { GROUP_EXPORT: 'GROUP_EXPORT', INDIVIDUAL_EXPORT: 'INDIVIDUAL_EXPORT', GROUP_INDIVIDUAL_EXPORT: 'GROUP_INDIVIDUAL_EXPORT', + SEARCH_GROUP_HISTORY: 'SEARCH_GROUP_HISTORY', }; function reducer( @@ -78,6 +79,12 @@ function reducer( groupIndividualsExport: null, groupIndividualsExportPageInfo: {}, errorGroupIndividualsExport: null, + fetchingGroupHistory: false, + errorGroupHistory: null, + fetchedGroupHistory: false, + groupHistory: [], + groupHistoryPageInfo: {}, + groupHistoryTotalCount: 0, }, action, ) { @@ -128,6 +135,16 @@ function reducer( group: null, errorGroup: null, }; + case REQUEST(ACTION_TYPE.SEARCH_GROUP_HISTORY): + return { + ...state, + fetchingGroupHistory: true, + fetchedGroupHistory: false, + groupHistory: [], + groupHistoryPageInfo: {}, + groupHistoryTotalCount: 0, + errorGroupHistory: null, + }; case SUCCESS(ACTION_TYPE.SEARCH_INDIVIDUALS): return { ...state, @@ -184,6 +201,20 @@ function reducer( groupsTotalCount: action.payload.data.group ? action.payload.data.group.totalCount : null, errorGroups: formatGraphQLError(action.payload), }; + case SUCCESS(ACTION_TYPE.SEARCH_GROUP_HISTORY): + return { + ...state, + fetchingGroupHistory: false, + fetchedGroupHistory: true, + groupHistory: parseData(action.payload.data.groupHistory)?.map((groupHistory) => ({ + ...groupHistory, + id: decodeId(groupHistory.id), + })), + groupHistoryPageInfo: pageInfo(action.payload.data.groupHistory), + groupHistoryTotalCount: action.payload.data.groupHistory + ? action.payload.data.groupHistory.totalCount : null, + errorGroupHistory: formatGraphQLError(action.payload), + }; case SUCCESS(ACTION_TYPE.GET_INDIVIDUAL): return { ...state, @@ -224,6 +255,12 @@ function reducer( fetchingGroups: false, errorGroups: formatServerError(action.payload), }; + case ERROR(ACTION_TYPE.SEARCH_GROUP_HISTORY): + return { + ...state, + fetchingGroupHistory: false, + errorGroupHistory: formatServerError(action.payload), + }; case ERROR(ACTION_TYPE.GET_INDIVIDUAL): return { ...state, diff --git a/src/translations/en.json b/src/translations/en.json index cd9f5e5..d838903 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -69,6 +69,21 @@ "mutationLabel": "Delete Group {id}" } }, + "groupHistory": { + "pageTitle": "Group {id}", + "headPanelTitle": "General Information", + "id": "ID", + "head": "head", + "dateUpdated": "Date Updated", + "version": "Version", + "any": "ANY", + "ok": "ok", + "jsonExt": "Additional fields" + }, + "groupHistoryList": { + "pageTitle": "Groups History", + "searcherResultsTitle": "{groupHistoryTotalCount} Historical Records Found" + }, "export": { "label": "DOWNLOAD", "dateCreated": "Date Created", @@ -93,5 +108,6 @@ "groupIndividualRolePicker.HEAD": "HEAD", "groupIndividualRolePicker.RECIPIENT": "RECIPIENT", "groupIndividualRolePicker": "Role" - } + }, + "groupChangelog.label": "Change Log" } \ No newline at end of file From a220dadd05445b13fb01994d42904554997abc24 Mon Sep 17 00:00:00 2001 From: sniedzielski Date: Fri, 29 Dec 2023 13:38:25 +0100 Subject: [PATCH 36/90] CM-381: changed cache filter keys for history --- src/components/IndividualHistorySearcher.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/IndividualHistorySearcher.js b/src/components/IndividualHistorySearcher.js index 81675b7..ece2c1d 100644 --- a/src/components/IndividualHistorySearcher.js +++ b/src/components/IndividualHistorySearcher.js @@ -116,7 +116,7 @@ function IndividualHistorySearcher({ defaultOrderBy="lastName" rowIdentifier={rowIdentifier} defaultFilters={defaultFilters()} - cacheFiltersKey="individualsFilterCache" + cacheFiltersKey="individualHistoryFilterChache" resetFiltersOnUnmount />
From 040957456cc032a4b515044e3ed03d7fc068684f Mon Sep 17 00:00:00 2001 From: Jan Date: Sat, 30 Dec 2023 10:29:56 +0100 Subject: [PATCH 37/90] CM-406: add change log fe for group (#34) Co-authored-by: Jan --- src/components/GroupChangelogTab.js | 4 +++- src/components/GroupHistorySearcher.js | 4 ++-- src/components/IndividualChangelogTab.js | 4 +++- src/index.js | 2 ++ 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/components/GroupChangelogTab.js b/src/components/GroupChangelogTab.js index 4a87817..cf82bdf 100644 --- a/src/components/GroupChangelogTab.js +++ b/src/components/GroupChangelogTab.js @@ -4,8 +4,9 @@ import { formatMessage, PublishedComponent } from '@openimis/fe-core'; import { GROUP_CHANGELOG_TAB_VALUE } from '../constants'; function GroupChangelogTabLabel({ - intl, onChange, tabStyle, isSelected, + intl, onChange, tabStyle, isSelected, group, }) { + if (!group) return null; return ( [ (groupHistory) => groupHistory.id, - (groupHistory) => groupHistory.head, + (groupHistory) => `${groupHistory?.head?.firstName} ${groupHistory?.head?.lastName}`, (groupHistory) => (groupHistory.dateUpdated ? formatDateFromISO(groupHistory.dateUpdated) : EMPTY_STRING ), @@ -49,7 +49,7 @@ function GroupHistorySearcher({ const sorts = () => [ ['id', false], - ['head', true], + ['head', false], ['dateUpdated', true], ['version', true], ]; diff --git a/src/components/IndividualChangelogTab.js b/src/components/IndividualChangelogTab.js index b2089c3..e5a9fcf 100644 --- a/src/components/IndividualChangelogTab.js +++ b/src/components/IndividualChangelogTab.js @@ -4,8 +4,9 @@ import { formatMessage, PublishedComponent } from '@openimis/fe-core'; import { INDIVIDUAL_CHANGELOG_TAB_VALUE } from '../constants'; function IndividalChangelogTabLabel({ - intl, onChange, tabStyle, isSelected, + intl, onChange, tabStyle, isSelected, individual, }) { + if (!individual) return null; return ( Date: Wed, 3 Jan 2024 14:28:58 +0100 Subject: [PATCH 38/90] CM-406: get head from jsonExt (#35) * CM-406: add change log fe for group * CM-406: get head from jsonExt * CM-406: refetch on head change --------- Co-authored-by: Jan --- src/components/GroupHistorySearcher.js | 25 ++++++++++++++--------- src/components/GroupIndividualSearcher.js | 3 +++ src/translations/en.json | 3 ++- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/components/GroupHistorySearcher.js b/src/components/GroupHistorySearcher.js index 09866ba..510641e 100644 --- a/src/components/GroupHistorySearcher.js +++ b/src/components/GroupHistorySearcher.js @@ -32,24 +32,29 @@ function GroupHistorySearcher({ 'groupHistory.head', 'groupHistory.dateUpdated', 'groupHistory.version', - 'groupHistory.jsonExt', + 'groupHistory.members', ]; const itemFormatters = () => [ - (groupHistory) => groupHistory.id, - (groupHistory) => `${groupHistory?.head?.firstName} ${groupHistory?.head?.lastName}`, - (groupHistory) => (groupHistory.dateUpdated - ? formatDateFromISO(groupHistory.dateUpdated) : EMPTY_STRING - ), - (groupHistory) => groupHistory.version, - (groupHistory) => groupHistory.jsonExt, + (groupHistory) => groupHistory?.id || EMPTY_STRING, + (groupHistory) => { + const jsonExt = groupHistory?.jsonExt ? JSON.parse(groupHistory.jsonExt) : null; + return jsonExt?.head ?? EMPTY_STRING; + }, + (groupHistory) => (groupHistory?.dateUpdated + ? formatDateFromISO(groupHistory.dateUpdated) + : EMPTY_STRING), + (groupHistory) => groupHistory?.version || EMPTY_STRING, + (groupHistory) => { + const jsonExt = groupHistory?.jsonExt ? JSON.parse(groupHistory.jsonExt) : null; + return jsonExt?.members ?? EMPTY_STRING; + }, ]; const rowIdentifier = (groupHistory) => groupHistory.id; const sorts = () => [ - ['id', false], - ['head', false], + ['id', true], ['dateUpdated', true], ['version', true], ]; diff --git a/src/components/GroupIndividualSearcher.js b/src/components/GroupIndividualSearcher.js index fbe9ce8..1111671 100644 --- a/src/components/GroupIndividualSearcher.js +++ b/src/components/GroupIndividualSearcher.js @@ -67,6 +67,7 @@ function GroupIndividualSearcher({ const [deletedGroupIndividualUuids, setDeletedGroupIndividualUuids] = useState([]); const prevSubmittingMutationRef = useRef(); const [updatedGroupIndividuals, setUpdatedGroupIndividuals] = useState([]); + const [refetch, setRefetch] = useState(null); function groupIndividualUpdatePageUrl(groupIndividual) { return `${modulesManager.getRef('individual.route.individual')}/${groupIndividual.individual?.id}`; @@ -156,6 +157,7 @@ function GroupIndividualSearcher({ id: editedGroupIndividual?.individual?.id, }), ); + setRefetch(editedGroupIndividual?.individual?.id); } }; @@ -265,6 +267,7 @@ function GroupIndividualSearcher({ return (
Date: Thu, 4 Jan 2024 11:27:21 +0100 Subject: [PATCH 39/90] CM-359: allow to choose columns to be exported (#30) Co-authored-by: Jan --- src/components/IndividualSearcher.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/IndividualSearcher.js b/src/components/IndividualSearcher.js index 25f7227..7f0983c 100644 --- a/src/components/IndividualSearcher.js +++ b/src/components/IndividualSearcher.js @@ -255,6 +255,7 @@ function IndividualSearcher({ dob: formatMessage(intl, 'individual', 'export.dob'), }} exportFieldLabel={formatMessage(intl, 'individual', 'export.label')} + chooseExportableColumns cacheFiltersKey="individualsFilterCache" resetFiltersOnUnmount /> From 1ac979ed2ac87e57abc6fa528ccc6ee3a24fe75b Mon Sep 17 00:00:00 2001 From: Jan Date: Fri, 5 Jan 2024 09:45:41 +0100 Subject: [PATCH 40/90] CM-404: move individual to another group (#36) Co-authored-by: Jan --- src/components/GroupChangeDialog.js | 62 +++++++++++++++++++++++ src/components/GroupIndividualSearcher.js | 46 ++++++++++++++++- src/pickers/GroupPicker.js | 60 ++++++++++++++++++++++ src/translations/en.json | 18 ++++++- 4 files changed, 183 insertions(+), 3 deletions(-) create mode 100644 src/components/GroupChangeDialog.js create mode 100644 src/pickers/GroupPicker.js diff --git a/src/components/GroupChangeDialog.js b/src/components/GroupChangeDialog.js new file mode 100644 index 0000000..56df643 --- /dev/null +++ b/src/components/GroupChangeDialog.js @@ -0,0 +1,62 @@ +import React, { useState } from 'react'; +import { injectIntl } from 'react-intl'; + +import { withTheme, withStyles } from '@material-ui/core/styles'; +import { + Button, Dialog, DialogActions, DialogContent, DialogTitle, +} from '@material-ui/core'; +import { useTranslations, useModulesManager } from '@openimis/fe-core'; +import GroupPicker from '../pickers/GroupPicker'; + +const styles = (theme) => ({ + primaryButton: theme.dialog.primaryButton, + secondaryButton: theme.dialog.secondaryButton, +}); + +function GroupChangeDialog({ + classes, + confirmState, + onClose, + onConfirm, + groupIndividual, +}) { + const modulesManager = useModulesManager(); + const { formatMessage, formatMessageWithValues } = useTranslations('individual', modulesManager); + const [groupToBeChanged, setGroupToBeChanged] = useState(null); + + const handleConfirm = (groupToBeChanged) => { + onConfirm(groupToBeChanged); + onClose(); + }; + + return ( + + + {formatMessageWithValues('groupChangeDialog.confirmTitle', { + firstName: groupIndividual?.individual?.firstName, lastName: groupIndividual?.individual?.lastName, + })} + + + + + + + + + + ); +} + +export default injectIntl(withTheme(withStyles(styles)(GroupChangeDialog))); diff --git a/src/components/GroupIndividualSearcher.js b/src/components/GroupIndividualSearcher.js index 1111671..de8cbde 100644 --- a/src/components/GroupIndividualSearcher.js +++ b/src/components/GroupIndividualSearcher.js @@ -19,6 +19,7 @@ import { Button, Dialog, DialogActions, DialogTitle, IconButton, Tooltip, } from '@material-ui/core'; import EditIcon from '@material-ui/icons/Edit'; +import GroupIcon from '@material-ui/icons/Group'; import DeleteIcon from '@material-ui/icons/Delete'; import { clearGroupIndividualExport, @@ -29,13 +30,14 @@ import { } from '../actions'; import { DEFAULT_PAGE_SIZE, - EMPTY_STRING, + EMPTY_STRING, GROUP_INDIVIDUAL_ROLES, RIGHT_GROUP_INDIVIDUAL_DELETE, RIGHT_GROUP_INDIVIDUAL_UPDATE, ROWS_PER_PAGE_OPTIONS, } from '../constants'; import GroupIndividualFilter from './GroupIndividualFilter'; import GroupIndividualRolePicker from '../pickers/GroupIndividualRolePicker'; +import GroupChangeDialog from './GroupChangeDialog'; function GroupIndividualSearcher({ intl, @@ -68,6 +70,8 @@ function GroupIndividualSearcher({ const prevSubmittingMutationRef = useRef(); const [updatedGroupIndividuals, setUpdatedGroupIndividuals] = useState([]); const [refetch, setRefetch] = useState(null); + const [isChangeGroupModalOpen, setIsChangeGroupModalOpen] = useState(false); + const [groupIndividualToGroupChange, setGroupIndividualToGroupChange] = useState(null); function groupIndividualUpdatePageUrl(groupIndividual) { return `${modulesManager.getRef('individual.route.individual')}/${groupIndividual.individual?.id}`; @@ -124,6 +128,7 @@ function GroupIndividualSearcher({ 'individual.lastName', 'individual.dob', 'groupIndividual.individual.role', + 'emptyLabel', ]; if (rights.includes(RIGHT_GROUP_INDIVIDUAL_UPDATE)) { headers.push('emptyLabel'); @@ -161,6 +166,11 @@ function GroupIndividualSearcher({ } }; + const handleGroupChange = (groupIndividual) => { + setIsChangeGroupModalOpen(true); + setGroupIndividualToGroupChange(groupIndividual); + }; + const isRowUpdated = (groupIndividual) => ( updatedGroupIndividuals.some((item) => item.id === groupIndividual.id)); @@ -168,6 +178,22 @@ function GroupIndividualSearcher({ const isRowDisabled = (_, groupIndividual) => isRowDeleted(groupIndividual) || isRowUpdated(groupIndividual); + const onChangeGroupConfirm = (groupToBeChanged) => { + const updateIndividual = { + ...groupIndividualToGroupChange, + group: groupToBeChanged, + role: GROUP_INDIVIDUAL_ROLES.RECIPIENT, + }; + updateGroupIndividual( + updateIndividual, + formatMessageWithValues(intl, 'individual', 'individual.groupChange.confirm.message', { + individualId: updateIndividual?.individual?.id, + groupId: groupToBeChanged?.id, + }), + ); + setRefetch(groupToBeChanged?.id); + }; + const itemFormatters = () => { const formatters = [ (groupIndividual) => groupIndividual.individual.firstName, @@ -185,6 +211,18 @@ function GroupIndividualSearcher({ onChange={(role) => handleRoleOnChange(groupIndividual, role)} /> ) : groupIndividual.role), + (groupIndividual) => (rights.includes(RIGHT_GROUP_INDIVIDUAL_UPDATE) ? ( + ( + + handleGroupChange(groupIndividual)} + disabled={isRowDeleted(groupIndividual)} + > + + + + ) + ) : null), ]; if (rights.includes(RIGHT_GROUP_INDIVIDUAL_UPDATE)) { formatters.push((groupIndividual) => ( @@ -266,6 +304,12 @@ function GroupIndividualSearcher({ return (
+ setIsChangeGroupModalOpen(false)} + onConfirm={onChangeGroupConfirm} + groupIndividual={groupIndividualToGroupChange} + /> state.individual.fetchingGroups); + const fetchedGroups = useSelector((state) => state.individual.fetchedGroups); + const errorGroups = useSelector((state) => state.individual.errorGroups); + const groups = useSelector((state) => state.individual.groups); + const [group, setGroup] = useState(null); + + useEffect(() => { + if (!fetchingGroups && !fetchedGroups) { + dispatch(fetchGroups({})); + } + }, []); + + const groupLabel = (option) => option.id; + + const getGroupsWithoutCurrentGroup = (options) => options.filter( + (option) => option?.id !== groupIndividual?.group?.id, + ); + + const handleChange = (group) => { + onChange(group); + setGroup(group); + }; + + return ( + null} + /> + ); +} + +export default GroupPicker; diff --git a/src/translations/en.json b/src/translations/en.json index 965b594..cb8abc2 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -43,7 +43,12 @@ }, "any": "ANY", "ok": "ok", - "individualChangelog.label": "Change Log" + "individualChangelog.label": "Change Log", + "groupChange": { + "confirm": { + "message": "Individual {individualId} moved to Group {groupId}" + } + } }, "individualHistory": { "pageTitle": "Individual {firstName} {lastName}", @@ -130,5 +135,14 @@ "groupIndividualRolePicker.RECIPIENT": "RECIPIENT", "groupIndividualRolePicker": "Role" }, - "groupChangelog.label": "Change Log" + "groupChangelog.label": "Change Log", + "groupPicker": { + "label": "Select a group." + }, + "groupChangeDialog": { + "confirmTitle": "Move {firstName} {lastName} to a different group." + }, + "confirm": "Confirm", + "cancel": "Cancel", + "changeGroupButtonTooltip": "Move to another group." } \ No newline at end of file From 8cf0be19d3c1931b21ad69bc3768988e418d7187 Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 9 Jan 2024 09:10:27 +0100 Subject: [PATCH 41/90] CM-405: add tasks on frontend (#37) * CM-404: move individual to another group * CM-405: add individual id * CM-405: fix label in group tasks * CM-405: fix eslint --------- Co-authored-by: Jan --- .../tasks/GroupIndividualUpdateTasks.js | 16 ++++++++++++++++ src/index.js | 13 ++++++++++++- src/translations/en.json | 9 ++++++++- 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 src/components/tasks/GroupIndividualUpdateTasks.js diff --git a/src/components/tasks/GroupIndividualUpdateTasks.js b/src/components/tasks/GroupIndividualUpdateTasks.js new file mode 100644 index 0000000..4c92007 --- /dev/null +++ b/src/components/tasks/GroupIndividualUpdateTasks.js @@ -0,0 +1,16 @@ +import React from 'react'; +import { FormattedMessage } from '@openimis/fe-core'; + +const GroupIndividualUpdateTaskTableHeaders = () => [ + , + , + , +]; + +const GroupIndividualUpdateTaskItemFormatters = () => [ + (groupIndividual) => groupIndividual?.group ?? groupIndividual?.group_id, + (groupIndividual, jsonExt) => jsonExt?.individual_identity ?? groupIndividual?.individual, + (groupIndividual) => groupIndividual?.role, +]; + +export { GroupIndividualUpdateTaskTableHeaders, GroupIndividualUpdateTaskItemFormatters }; diff --git a/src/index.js b/src/index.js index 0f850da..e5fb3dc 100644 --- a/src/index.js +++ b/src/index.js @@ -26,6 +26,10 @@ import { } from './components/tasks/IndividualUpdateTasks'; import GroupHistorySearcher from './components/GroupHistorySearcher'; import { GroupChangelogTabLabel, GroupChangelogTabPanel } from './components/GroupChangelogTab'; +import { + GroupIndividualUpdateTaskItemFormatters, + GroupIndividualUpdateTaskTableHeaders, +} from './components/tasks/GroupIndividualUpdateTasks'; const ROUTE_INDIVIDUALS = 'individuals'; const ROUTE_INDIVIDUAL = 'individuals/individual'; @@ -77,7 +81,14 @@ const DEFAULT_CONFIG = { tableHeaders: IndividualUpdateTaskTableHeaders, itemFormatters: IndividualUpdateTaskItemFormatters, taskSource: ['IndividualService'], - }], + }, + { + text: , + tableHeaders: GroupIndividualUpdateTaskTableHeaders, + itemFormatters: GroupIndividualUpdateTaskItemFormatters, + taskSource: ['GroupIndividualService'], + }, + ], }; export const IndividualModule = (cfg) => ({ ...DEFAULT_CONFIG, ...cfg }); diff --git a/src/translations/en.json b/src/translations/en.json index cb8abc2..b1af43c 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -125,12 +125,19 @@ }, "groupIndividual": { "individual": { - "role": "Role" + "role": "Role", + "groupId": "Group ID", + "individualId": "Individual ID" }, "update": { "label": "Update Individual", "mutationLabel":"Update Individual {id}" }, + "tasks": { + "update": { + "title": "Group Update Tasks" + } + }, "groupIndividualRolePicker.HEAD": "HEAD", "groupIndividualRolePicker.RECIPIENT": "RECIPIENT", "groupIndividualRolePicker": "Role" From 35790d2426781d8424fc9fcbe61669fdf6d1e3e9 Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 9 Jan 2024 12:16:50 +0100 Subject: [PATCH 42/90] CM-408: display members in task (#38) * CM-404: move individual to another group * CM-405: add individual id * CM-405: fix label in group tasks * CM-405: fix eslint * CM-408: display members --------- Co-authored-by: Jan --- src/components/GroupHistorySearcher.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/GroupHistorySearcher.js b/src/components/GroupHistorySearcher.js index 510641e..ae923d0 100644 --- a/src/components/GroupHistorySearcher.js +++ b/src/components/GroupHistorySearcher.js @@ -47,7 +47,7 @@ function GroupHistorySearcher({ (groupHistory) => groupHistory?.version || EMPTY_STRING, (groupHistory) => { const jsonExt = groupHistory?.jsonExt ? JSON.parse(groupHistory.jsonExt) : null; - return jsonExt?.members ?? EMPTY_STRING; + return jsonExt?.members ? Object.values(jsonExt?.members).map((value) => `${value}, `) : EMPTY_STRING; }, ]; From 96c0932bc043be58ba895dcababe8ed78f3309d4 Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 10 Jan 2024 11:00:28 +0100 Subject: [PATCH 43/90] CM-406: add user updated (#39) * CM-406: add user updated * CM-406: fix eslint --------- Co-authored-by: Jan --- src/actions.js | 8 ++- src/components/GroupHistorySearcher.js | 4 +- src/components/IndividualHistorySearcher.js | 58 +++++++++------------ src/translations/en.json | 6 ++- 4 files changed, 38 insertions(+), 38 deletions(-) diff --git a/src/actions.js b/src/actions.js index 98aee13..08bc47f 100644 --- a/src/actions.js +++ b/src/actions.js @@ -20,6 +20,7 @@ const INDIVIDUAL_FULL_PROJECTION = [ 'dob', 'jsonExt', 'version', + 'userUpdated {username}', ]; const GROUP_INDIVIDUAL_FULL_PROJECTION = [ @@ -41,8 +42,13 @@ const GROUP_FULL_PROJECTION = [ 'dateUpdated', 'jsonExt', 'version', + 'userUpdated {username}', ]; +const GROUP_HISTORY_FULL_PROJECTION = GROUP_FULL_PROJECTION.filter( + (item) => item !== 'head {firstName, lastName}', +); + export function fetchIndividuals(params) { const payload = formatPageQueryWithCount('individual', params, INDIVIDUAL_FULL_PROJECTION); return graphql(payload, ACTION_TYPE.SEARCH_INDIVIDUALS); @@ -74,7 +80,7 @@ export function fetchGroup(params) { } export function fetchGroupHistory(params) { - const payload = formatPageQueryWithCount('groupHistory', params, GROUP_FULL_PROJECTION); + const payload = formatPageQueryWithCount('groupHistory', params, GROUP_HISTORY_FULL_PROJECTION); return graphql(payload, ACTION_TYPE.SEARCH_GROUP_HISTORY); } diff --git a/src/components/GroupHistorySearcher.js b/src/components/GroupHistorySearcher.js index ae923d0..61ebc18 100644 --- a/src/components/GroupHistorySearcher.js +++ b/src/components/GroupHistorySearcher.js @@ -28,15 +28,14 @@ function GroupHistorySearcher({ const fetch = (params) => dispatch(fetchGroupHistory(params)); const headers = () => [ - 'groupHistory.id', 'groupHistory.head', 'groupHistory.dateUpdated', 'groupHistory.version', 'groupHistory.members', + 'groupHistory.userUpdated', ]; const itemFormatters = () => [ - (groupHistory) => groupHistory?.id || EMPTY_STRING, (groupHistory) => { const jsonExt = groupHistory?.jsonExt ? JSON.parse(groupHistory.jsonExt) : null; return jsonExt?.head ?? EMPTY_STRING; @@ -49,6 +48,7 @@ function GroupHistorySearcher({ const jsonExt = groupHistory?.jsonExt ? JSON.parse(groupHistory.jsonExt) : null; return jsonExt?.members ? Object.values(jsonExt?.members).map((value) => `${value}, `) : EMPTY_STRING; }, + (groupHistory) => groupHistory?.userUpdated?.username, ]; const rowIdentifier = (groupHistory) => groupHistory.id; diff --git a/src/components/IndividualHistorySearcher.js b/src/components/IndividualHistorySearcher.js index ece2c1d..45ba79e 100644 --- a/src/components/IndividualHistorySearcher.js +++ b/src/components/IndividualHistorySearcher.js @@ -1,20 +1,16 @@ import React from 'react'; import { injectIntl } from 'react-intl'; import { - withModulesManager, + formatDateFromISO, formatMessageWithValues, Searcher, - formatDateFromISO, withHistory, + withModulesManager, } from '@openimis/fe-core'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import { fetchIndividualHistory } from '../actions'; -import { - DEFAULT_PAGE_SIZE, - ROWS_PER_PAGE_OPTIONS, - EMPTY_STRING, -} from '../constants'; +import { DEFAULT_PAGE_SIZE, EMPTY_STRING, ROWS_PER_PAGE_OPTIONS } from '../constants'; import IndividualHistoryFilter from './IndividualHistoryFilter'; function IndividualHistorySearcher({ @@ -31,33 +27,29 @@ function IndividualHistorySearcher({ }) { const fetch = (params) => fetchIndividualHistory(params); - const headers = () => { - const headers = [ - 'individualHistory.firstName', - 'individualHistory.lastName', - 'individualHistory.dob', - 'individualHistory.dateUpdated', - 'individualHistory.version', - 'individualHistory.jsonExt', - ]; - return headers; - }; + const headers = () => [ + 'individualHistory.firstName', + 'individualHistory.lastName', + 'individualHistory.dob', + 'individualHistory.dateUpdated', + 'individualHistory.version', + 'individualHistory.jsonExt', + 'individualHistory.userUpdated', + ]; - const itemFormatters = () => { - const formatters = [ - (individualHistory) => individualHistory.firstName, - (individualHistory) => individualHistory.lastName, - (individualHistory) => (individualHistory.dob - ? formatDateFromISO(modulesManager, intl, individualHistory.dob) : EMPTY_STRING - ), - (individualHistory) => (individualHistory.dateUpdated - ? formatDateFromISO(modulesManager, intl, individualHistory.dateUpdated) : EMPTY_STRING - ), - (individualHistory) => individualHistory.version, - (individualHistory) => individualHistory.jsonExt, - ]; - return formatters; - }; + const itemFormatters = () => [ + (individualHistory) => individualHistory.firstName, + (individualHistory) => individualHistory.lastName, + (individualHistory) => (individualHistory.dob + ? formatDateFromISO(modulesManager, intl, individualHistory.dob) : EMPTY_STRING + ), + (individualHistory) => (individualHistory.dateUpdated + ? formatDateFromISO(modulesManager, intl, individualHistory.dateUpdated) : EMPTY_STRING + ), + (individualHistory) => individualHistory.version, + (individualHistory) => individualHistory.jsonExt, + (individualHistory) => individualHistory?.userUpdated?.username, + ]; const rowIdentifier = (individualHistory) => individualHistory.id; diff --git a/src/translations/en.json b/src/translations/en.json index b1af43c..9414aa0 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -63,7 +63,8 @@ }, "any": "ANY", "ok": "ok", - "jsonExt": "Additional fields" + "jsonExt": "Additional fields", + "userUpdated": "User Updated" }, "individuals": { "pageTitle": "Individuals", @@ -104,7 +105,8 @@ "any": "ANY", "ok": "ok", "jsonExt": "Additional fields", - "members": "Members" + "members": "Members", + "userUpdated": "User Updated" }, "groupHistoryList": { "pageTitle": "Groups History", From 9e85dcec8a8e743c87c535da24c07ba21ec9c363 Mon Sep 17 00:00:00 2001 From: sniedzielski Date: Wed, 10 Jan 2024 16:01:40 +0100 Subject: [PATCH 44/90] CM-396: added Tasks tab for Individual and Group --- src/components/GroupTaskTab.js | 51 +++++++++++++++++++++++++++++ src/components/IndividualTaskTab.js | 51 +++++++++++++++++++++++++++++ src/constants.js | 5 +++ src/index.js | 12 +++++++ src/translations/en.json | 2 ++ 5 files changed, 121 insertions(+) create mode 100644 src/components/GroupTaskTab.js create mode 100644 src/components/IndividualTaskTab.js diff --git a/src/components/GroupTaskTab.js b/src/components/GroupTaskTab.js new file mode 100644 index 0000000..90e6ae8 --- /dev/null +++ b/src/components/GroupTaskTab.js @@ -0,0 +1,51 @@ +import React from 'react'; +import { Tab } from '@material-ui/core'; +import { + formatMessage, PublishedComponent, + useModulesManager, +} from '@openimis/fe-core'; +import { GROUP_TASK_TAB_VALUE, GROUP_LABEL, TASK_CONTRIBUTION_KEY } from '../constants'; + +function GroupTaskTabLabel({ + intl, onChange, tabStyle, isSelected, group, +}) { + if (!group) return null; + return ( + + ); +} + +function GroupTaskTabPanel({ + value, group, rights, classes, +}) { + if (!group) return null; + const modulesManager = useModulesManager(); + const contributions = modulesManager.getContribs(TASK_CONTRIBUTION_KEY); + if (contributions !== undefined) { + const filteredContribution = contributions.filter((contribution) => contribution?.taskCode === GROUP_LABEL)[0]; + return ( + + + + ); + } +} + +export { GroupTaskTabLabel, GroupTaskTabPanel }; diff --git a/src/components/IndividualTaskTab.js b/src/components/IndividualTaskTab.js new file mode 100644 index 0000000..f8017c6 --- /dev/null +++ b/src/components/IndividualTaskTab.js @@ -0,0 +1,51 @@ +import React from 'react'; +import { Tab } from '@material-ui/core'; +import { + formatMessage, PublishedComponent, + useModulesManager, +} from '@openimis/fe-core'; +import { INDIVIDUAL_LABEL, INDIVIDUAL_TASK_TAB_VALUE, TASK_CONTRIBUTION_KEY } from '../constants'; + +function IndividalTaskTabLabel({ + intl, onChange, tabStyle, isSelected, individual, +}) { + if (!individual) return null; + return ( + + ); +} + +function IndividalTaskTabPanel({ + value, individual, rights, classes, +}) { + if (!individual) return null; + const modulesManager = useModulesManager(); + const contributions = modulesManager.getContribs(TASK_CONTRIBUTION_KEY); + if (contributions !== undefined) { + const filteredContribution = contributions.filter((contribution) => contribution?.taskCode === INDIVIDUAL_LABEL)[0]; + return ( + + + + ); + } +} + +export { IndividalTaskTabLabel, IndividalTaskTabPanel }; diff --git a/src/constants.js b/src/constants.js index 7367724..eb3d1ee 100644 --- a/src/constants.js +++ b/src/constants.js @@ -23,12 +23,15 @@ export const RIGHT_GROUP_DELETE = 180004; export const BENEFIT_PLANS_LIST_TAB_VALUE = 'BenefitPlansListTab'; export const INDIVIDUALS_LIST_TAB_VALUE = 'IndividualsListTab'; export const INDIVIDUAL_CHANGELOG_TAB_VALUE = 'IndividualChangelogTab'; +export const INDIVIDUAL_TASK_TAB_VALUE = 'IndividualTaskTab'; export const GROUP_CHANGELOG_TAB_VALUE = 'GroupChangelogTab'; +export const GROUP_TASK_TAB_VALUE = 'GroupTaskTab'; export const INDIVIDUAL_TABS_LABEL_CONTRIBUTION_KEY = 'individual.TabPanel.label'; export const INDIVIDUAL_TABS_PANEL_CONTRIBUTION_KEY = 'individual.TabPanel.panel'; export const BENEFIT_PLAN_TABS_LABEL_CONTRIBUTION_KEY = 'individual.BenefitPlansListTabLabel'; export const BENEFIT_PLAN_TABS_PANEL_CONTRIBUTION_KEY = 'individual.BenefitPlansListTabPanel'; +export const TASK_CONTRIBUTION_KEY = 'tasksManagement.tasks'; export const BENEFICIARY_STATUS = { POTENTIAL: 'POTENTIAL', @@ -47,5 +50,7 @@ export const GROUP_INDIVIDUAL_ROLES_LIST = [ ]; export const BENEFIT_PLAN_LABEL = 'BenefitPlan'; +export const INDIVIDUAL_LABEL = 'Individual'; +export const GROUP_LABEL = 'Group'; export const SOCIAL_PROTECTION_MODULE_NAME = 'social_protection'; diff --git a/src/index.js b/src/index.js index e5fb3dc..53780e0 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,10 @@ import { IndividalChangelogTabLabel, IndividalChangelogTabPanel, } from './components/IndividualChangelogTab'; +import { + IndividalTaskTabLabel, + IndividalTaskTabPanel, +} from './components/IndividualTaskTab'; import getBenefitPlansListTab from './contributions/getBenefitPlansListTab'; import GroupIndividualSearcher from './components/GroupIndividualSearcher'; import { clearIndividualExport, downloadIndividuals, fetchIndividuals } from './actions'; @@ -26,10 +30,12 @@ import { } from './components/tasks/IndividualUpdateTasks'; import GroupHistorySearcher from './components/GroupHistorySearcher'; import { GroupChangelogTabLabel, GroupChangelogTabPanel } from './components/GroupChangelogTab'; +import { GroupTaskTabLabel, GroupTaskTabPanel } from './components/GroupTaskTab'; import { GroupIndividualUpdateTaskItemFormatters, GroupIndividualUpdateTaskTableHeaders, } from './components/tasks/GroupIndividualUpdateTasks'; +import { GROUP_LABEL, INDIVIDUAL_LABEL } from './constants'; const ROUTE_INDIVIDUALS = 'individuals'; const ROUTE_INDIVIDUAL = 'individuals/individual'; @@ -67,12 +73,16 @@ const DEFAULT_CONFIG = { BenefitPlansListTabLabel, IndividalChangelogTabLabel, GroupChangelogTabLabel, + GroupTaskTabLabel, + IndividalTaskTabLabel, ], 'individual.TabPanel.panel': [ IndividualsListTabPanel, BenefitPlansListTabPanel, GroupChangelogTabPanel, IndividalChangelogTabPanel, + GroupTaskTabPanel, + IndividalTaskTabPanel, ], 'individual.BenefitPlansListTabLabel': [BENEFIT_PLAN_TABS_LABEL_REF_KEY], 'individual.BenefitPlansListTabPanel': [BENEFIT_PLAN_TABS_PANEL_REF_KEY], @@ -81,12 +91,14 @@ const DEFAULT_CONFIG = { tableHeaders: IndividualUpdateTaskTableHeaders, itemFormatters: IndividualUpdateTaskItemFormatters, taskSource: ['IndividualService'], + taskCode: INDIVIDUAL_LABEL, }, { text: , tableHeaders: GroupIndividualUpdateTaskTableHeaders, itemFormatters: GroupIndividualUpdateTaskItemFormatters, taskSource: ['GroupIndividualService'], + taskCode: GROUP_LABEL, }, ], }; diff --git a/src/translations/en.json b/src/translations/en.json index 9414aa0..0fb76f5 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -44,6 +44,7 @@ "any": "ANY", "ok": "ok", "individualChangelog.label": "Change Log", + "individualTasks.label": "Tasks", "groupChange": { "confirm": { "message": "Individual {individualId} moved to Group {groupId}" @@ -145,6 +146,7 @@ "groupIndividualRolePicker": "Role" }, "groupChangelog.label": "Change Log", + "groupTasks.label": "Tasks", "groupPicker": { "label": "Select a group." }, From 64aba29e03f69a77fb091713eefd34ad0b2aae0a Mon Sep 17 00:00:00 2001 From: sniedzielski Date: Wed, 10 Jan 2024 16:39:01 +0100 Subject: [PATCH 45/90] CM-396: improving finding contribution --- src/components/GroupTaskTab.js | 40 ++++++++++++++++------------- src/components/IndividualTaskTab.js | 40 ++++++++++++++++------------- 2 files changed, 44 insertions(+), 36 deletions(-) diff --git a/src/components/GroupTaskTab.js b/src/components/GroupTaskTab.js index 90e6ae8..12b19c5 100644 --- a/src/components/GroupTaskTab.js +++ b/src/components/GroupTaskTab.js @@ -27,25 +27,29 @@ function GroupTaskTabPanel({ if (!group) return null; const modulesManager = useModulesManager(); const contributions = modulesManager.getContribs(TASK_CONTRIBUTION_KEY); - if (contributions !== undefined) { - const filteredContribution = contributions.filter((contribution) => contribution?.taskCode === GROUP_LABEL)[0]; - return ( - - - - ); + if (contributions === undefined) { + return null; + } + const filteredContribution = contributions.find((contribution) => contribution?.taskCode === GROUP_LABEL); + if (!filteredContribution) { + return null; } + return ( + + + + ); } export { GroupTaskTabLabel, GroupTaskTabPanel }; diff --git a/src/components/IndividualTaskTab.js b/src/components/IndividualTaskTab.js index f8017c6..df08426 100644 --- a/src/components/IndividualTaskTab.js +++ b/src/components/IndividualTaskTab.js @@ -27,25 +27,29 @@ function IndividalTaskTabPanel({ if (!individual) return null; const modulesManager = useModulesManager(); const contributions = modulesManager.getContribs(TASK_CONTRIBUTION_KEY); - if (contributions !== undefined) { - const filteredContribution = contributions.filter((contribution) => contribution?.taskCode === INDIVIDUAL_LABEL)[0]; - return ( - - - - ); + if (contributions === undefined) { + return null; + } + const filteredContribution = contributions.find((contribution) => contribution?.taskCode === INDIVIDUAL_LABEL); + if (!filteredContribution) { + return null; } + return ( + + + + ); } export { IndividalTaskTabLabel, IndividalTaskTabPanel }; From ac5c3f0a306f0ff63db9df636c64fd1e6d09b561 Mon Sep 17 00:00:00 2001 From: Jan Date: Fri, 12 Jan 2024 10:09:00 +0100 Subject: [PATCH 46/90] CM-406: add new filters for history (#42) Co-authored-by: Jan --- src/components/GroupHistoryFilter.js | 46 +++++++++++++++++++---- src/components/IndividualHistoryFilter.js | 38 +++++++++++++++++++ src/translations/en.json | 10 +++-- 3 files changed, 84 insertions(+), 10 deletions(-) diff --git a/src/components/GroupHistoryFilter.js b/src/components/GroupHistoryFilter.js index d814855..d07eedf 100644 --- a/src/components/GroupHistoryFilter.js +++ b/src/components/GroupHistoryFilter.js @@ -1,6 +1,6 @@ import React from 'react'; import { injectIntl } from 'react-intl'; -import { TextInput } from '@openimis/fe-core'; +import { TextInput, PublishedComponent } from '@openimis/fe-core'; import { Grid } from '@material-ui/core'; import { withTheme, withStyles } from '@material-ui/core/styles'; import _debounce from 'lodash/debounce'; @@ -13,6 +13,8 @@ function GroupHistoryFilter({ const debouncedOnChangeFilters = _debounce(onChangeFilters, DEFAULT_DEBOUNCE_TIME); const filterTextFieldValue = (filterName) => filters?.[filterName]?.value ?? EMPTY_STRING; + const filterValue = (filterName) => filters?.[filterName]?.value; + const onChangeStringFilter = (filterName, lookup = null) => (value) => { if (lookup) { debouncedOnChangeFilters([ @@ -38,17 +40,47 @@ function GroupHistoryFilter({ + + + onChangeFilters([ + { + id: 'dateUpdated_Gte', + value: v, + filter: `dateUpdated_Gte: "${v}T00:00:00.000Z"`, + }, + ])} + /> + + + onChangeFilters([ + { + id: 'dateUpdated_Lte', + value: v, + filter: `dateUpdated_Lte: "${v}T00:00:00.000Z"`, + }, + ])} /> diff --git a/src/components/IndividualHistoryFilter.js b/src/components/IndividualHistoryFilter.js index 6f2683c..0f56fc4 100644 --- a/src/components/IndividualHistoryFilter.js +++ b/src/components/IndividualHistoryFilter.js @@ -69,6 +69,44 @@ function IndividualHistoryFilter({ ])} /> + + onChangeFilters([ + { + id: 'dateUpdated_Gte', + value: v, + filter: `dateUpdated_Gte: "${v}T00:00:00.000Z"`, + }, + ])} + /> + + + onChangeFilters([ + { + id: 'dateUpdated_Lte', + value: v, + filter: `dateUpdated_Lte: "${v}T00:00:00.000Z"`, + }, + ])} + /> + + + + ); } diff --git a/src/translations/en.json b/src/translations/en.json index 9414aa0..0300dfb 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -64,7 +64,9 @@ "any": "ANY", "ok": "ok", "jsonExt": "Additional fields", - "userUpdated": "User Updated" + "userUpdated": "Changed by", + "dateUpdated_Gte": "Date updated after", + "dateUpdated_Lte": "Date updated before" }, "individuals": { "pageTitle": "Individuals", @@ -99,14 +101,16 @@ "pageTitle": "Group {id}", "headPanelTitle": "General Information", "id": "ID", - "head": "head", + "head": "Head", "dateUpdated": "Date Updated", "version": "Version", "any": "ANY", "ok": "ok", "jsonExt": "Additional fields", "members": "Members", - "userUpdated": "User Updated" + "userUpdated": "Changed by", + "dateUpdated_Gte": "Date updated after", + "dateUpdated_Lte": "Date updated before" }, "groupHistoryList": { "pageTitle": "Groups History", From 1659441ffdc21fa985132b6be6d4995329607ea9 Mon Sep 17 00:00:00 2001 From: Jan Date: Fri, 12 Jan 2024 10:09:29 +0100 Subject: [PATCH 47/90] =?UTF-8?q?CM-359:=20update=20individual=20searcher?= =?UTF-8?q?=20to=20support=20selection=20of=20fields=20fro=E2=80=A6=20(#41?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * CM-359: update individual searcher to support selection of fields from json_ext * CM-359: move fetching logic to social_protection module * CM-359: fix linter --------- Co-authored-by: Jan --- src/components/IndividualSearcher.js | 60 +++++++++++++++++++++------- src/constants.js | 4 ++ 2 files changed, 49 insertions(+), 15 deletions(-) diff --git a/src/components/IndividualSearcher.js b/src/components/IndividualSearcher.js index 7f0983c..fb35e86 100644 --- a/src/components/IndividualSearcher.js +++ b/src/components/IndividualSearcher.js @@ -15,7 +15,7 @@ import { CLEARED_STATE_FILTER, } from '@openimis/fe-core'; import { bindActionCreators } from 'redux'; -import { connect } from 'react-redux'; +import { connect, useDispatch } from 'react-redux'; import { IconButton, Tooltip, Button, Dialog, @@ -32,7 +32,11 @@ import { ROWS_PER_PAGE_OPTIONS, EMPTY_STRING, RIGHT_INDIVIDUAL_UPDATE, - RIGHT_INDIVIDUAL_DELETE, BENEFIT_PLAN_LABEL, SOCIAL_PROTECTION_MODULE_NAME, + RIGHT_INDIVIDUAL_DELETE, + BENEFIT_PLAN_LABEL, + SOCIAL_PROTECTION_MODULE_NAME, + RIGHT_SCHEMA_SEARCH, + FETCH_BENEFIT_PLAN_SCHEMA_FIELDS_REF, } from '../constants'; import { applyNumberCircle } from '../util/searcher-utils'; import IndividualFilter from './IndividualFilter'; @@ -61,13 +65,46 @@ function IndividualSearcher({ downloadIndividuals, individualExport, errorIndividualExport, + fieldsFromBfSchema, + fetchingFieldsFromBfSchema, + fetchedFieldsFromBfSchema, }) { + const dispatch = useDispatch(); const [individualToDelete, setIndividualToDelete] = useState(null); const [appliedCustomFilters, setAppliedCustomFilters] = useState([CLEARED_STATE_FILTER]); const [appliedFiltersRowStructure, setAppliedFiltersRowStructure] = useState([CLEARED_STATE_FILTER]); const [deletedIndividualUuids, setDeletedIndividualUuids] = useState([]); + const [exportFields, setExportFields] = useState([ + 'id', + 'first_name', + 'last_name', + 'dob', + ]); + const exportFieldsColumns = { + id: 'ID', + first_name: formatMessage(intl, 'individual', 'export.firstName'), + last_name: formatMessage(intl, 'individual', 'export.lastName'), + dob: formatMessage(intl, 'individual', 'export.dob'), + }; const prevSubmittingMutationRef = useRef(); + useEffect(() => { + const canFetchBenefitPlanSchemaFields = !fetchedFieldsFromBfSchema + && !fetchingFieldsFromBfSchema + && rights.includes(RIGHT_SCHEMA_SEARCH); + + if (canFetchBenefitPlanSchemaFields) { + const fetchBenefitPlanSchemaFields = modulesManager.getRef(FETCH_BENEFIT_PLAN_SCHEMA_FIELDS_REF); + if (fetchBenefitPlanSchemaFields) { + dispatch(fetchBenefitPlanSchemaFields(['bfType: INDIVIDUAL'])); + } + } + + if (!canFetchBenefitPlanSchemaFields) { + setExportFields([...exportFields, ...fieldsFromBfSchema]); + } + }, [fetchedFieldsFromBfSchema, fetchingFieldsFromBfSchema, rights, modulesManager]); + function individualUpdatePageUrl(individual) { return `${modulesManager.getRef('individual.route.individual')}/${individual?.id}`; } @@ -241,19 +278,8 @@ function IndividualSearcher({ appliedFiltersRowStructure={appliedFiltersRowStructure} setAppliedFiltersRowStructure={setAppliedFiltersRowStructure} applyNumberCircle={applyNumberCircle} - exportFields={[ - 'id', - 'first_name', - 'last_name', - 'dob', - 'json_ext', // Unfolded by backend and removed from csv - ]} - exportFieldsColumns={{ - id: 'ID', - first_name: formatMessage(intl, 'individual', 'export.firstName'), - last_name: formatMessage(intl, 'individual', 'export.lastName'), - dob: formatMessage(intl, 'individual', 'export.dob'), - }} + exportFields={exportFields} + exportFieldsColumns={exportFieldsColumns} exportFieldLabel={formatMessage(intl, 'individual', 'export.label')} chooseExportableColumns cacheFiltersKey="individualsFilterCache" @@ -289,6 +315,10 @@ const mapStateToProps = (state) => ({ individualExport: state.individual.individualExport, individualExportPageInfo: state.individual.individualExportPageInfo, errorIndividualExport: state.individual.errorIndividualExport, + fieldsFromBfSchema: state?.socialProtection?.fieldsFromBfSchema, + fetchingFieldsFromBfSchema: state?.socialProtection?.fetchingFieldsFromBfSchema, + fetchedFieldsFromBfSchema: state?.socialProtection?.fetchedFieldsFromBfSchema, + errorFieldsFromBfSchema: state?.socialProtection?.errorFieldsFromBfSchema, }); const mapDispatchToProps = (dispatch) => bindActionCreators( diff --git a/src/constants.js b/src/constants.js index 7367724..89491c3 100644 --- a/src/constants.js +++ b/src/constants.js @@ -20,6 +20,8 @@ export const RIGHT_GROUP_CREATE = 180002; export const RIGHT_GROUP_UPDATE = 180003; export const RIGHT_GROUP_DELETE = 180004; +export const RIGHT_SCHEMA_SEARCH = 171001; + export const BENEFIT_PLANS_LIST_TAB_VALUE = 'BenefitPlansListTab'; export const INDIVIDUALS_LIST_TAB_VALUE = 'IndividualsListTab'; export const INDIVIDUAL_CHANGELOG_TAB_VALUE = 'IndividualChangelogTab'; @@ -49,3 +51,5 @@ export const GROUP_INDIVIDUAL_ROLES_LIST = [ export const BENEFIT_PLAN_LABEL = 'BenefitPlan'; export const SOCIAL_PROTECTION_MODULE_NAME = 'social_protection'; + +export const FETCH_BENEFIT_PLAN_SCHEMA_FIELDS_REF = 'socialProtection.fetchBenefitPlanSchemaFields'; From 16d5809c2c8ca005afed10c314e6bf6b01007e91 Mon Sep 17 00:00:00 2001 From: Jan Date: Fri, 19 Jan 2024 13:34:23 +0100 Subject: [PATCH 48/90] CM-465: move individual to new group (#43) * CM-465: move individual to new group * CM-465: fix linter --------- Co-authored-by: Jan --- src/actions.js | 65 +++++++++++++++---- src/components/GroupChangeDialog.js | 27 ++++++-- src/components/GroupHeadPanel.js | 4 +- src/components/GroupIndividualSearcher.js | 19 ++++-- src/components/IndividualTabPanel.js | 6 +- src/components/IndividualsListTab.js | 4 +- src/components/tasks/GroupCreateTasks.js | 14 ++++ .../tasks/GroupIndividualUpdateTasks.js | 2 +- src/index.js | 8 +++ src/pages/GroupPage.js | 54 +++++++++++---- src/reducer.js | 34 +++++++++- src/translations/en.json | 14 +++- src/util/action-type.js | 1 + 13 files changed, 211 insertions(+), 41 deletions(-) create mode 100644 src/components/tasks/GroupCreateTasks.js diff --git a/src/actions.js b/src/actions.js index 08bc47f..a86bd5b 100644 --- a/src/actions.js +++ b/src/actions.js @@ -7,7 +7,7 @@ import { } from '@openimis/fe-core'; import { ACTION_TYPE } from './reducer'; import { - CLEAR, ERROR, REQUEST, SUCCESS, + CLEAR, ERROR, REQUEST, SET, SUCCESS, } from './util/action-type'; const INDIVIDUAL_FULL_PROJECTION = [ @@ -136,21 +136,27 @@ function dateTimeToDate(date) { return date.split('T')[0]; } +function formatGroupGQL(group, groupIndividualId = null) { + return ` + ${group?.id ? `id: "${group.id}"` : ''} + ${groupIndividualId ? `groupIndividualId: "${groupIndividualId}"` : ''}`; +} + function formatIndividualGQL(individual) { return ` - ${individual.id ? `id: "${individual.id}"` : ''} - ${individual.firstName ? `firstName: "${formatGQLString(individual.firstName)}"` : ''} - ${individual.lastName ? `lastName: "${formatGQLString(individual.lastName)}"` : ''} - ${individual.jsonExt ? `jsonExt: ${JSON.stringify(individual.jsonExt)}` : ''} - ${individual.dob ? `dob: "${dateTimeToDate(individual.dob)}"` : ''}`; + ${individual?.id ? `id: "${individual.id}"` : ''} + ${individual?.firstName ? `firstName: "${formatGQLString(individual.firstName)}"` : ''} + ${individual?.lastName ? `lastName: "${formatGQLString(individual.lastName)}"` : ''} + ${individual?.jsonExt ? `jsonExt: ${JSON.stringify(individual.jsonExt)}` : ''} + ${individual?.dob ? `dob: "${dateTimeToDate(individual.dob)}"` : ''}`; } function formatGroupIndividualGQL(groupIndividual) { return ` - ${groupIndividual.id ? `id: "${groupIndividual.id}"` : ''} - ${groupIndividual.role ? `role: ${groupIndividual.role}` : ''} - ${groupIndividual.individual.id ? `individualId: "${groupIndividual.individual.id}"` : ''} - ${groupIndividual.group.id ? `groupId: "${groupIndividual.group.id}"` : ''}`; + ${groupIndividual?.id ? `id: "${groupIndividual.id}"` : ''} + ${groupIndividual?.role ? `role: ${groupIndividual.role}` : ''} + ${groupIndividual?.individual.id ? `individualId: "${groupIndividual.individual.id}"` : ''} + ${groupIndividual?.group.id ? `groupId: "${groupIndividual.group.id}"` : ''}`; } export function updateIndividual(individual, clientMutationLabel) { @@ -187,8 +193,27 @@ export function updateGroupIndividual(groupIndividual, clientMutationLabel) { ); } +export function createGroupAndMoveIndividual(group, individualIds, clientMutationLabel) { + const mutation = formatMutation( + 'createGroupAndMoveIndividual', + formatGroupGQL(group, individualIds), + clientMutationLabel, + ); + const requestedDateTime = new Date(); + return graphql( + mutation.payload, + [REQUEST(ACTION_TYPE.MUTATION), SUCCESS(ACTION_TYPE.CREATE_GROUP_AND_MOVE_INDIVIDUAL), ERROR(ACTION_TYPE.MUTATION)], + { + actionType: ACTION_TYPE.CREATE_GROUP_AND_MOVE_INDIVIDUAL, + clientMutationId: mutation.clientMutationId, + clientMutationLabel, + requestedDateTime, + }, + ); +} + export function updateGroup(group, clientMutationLabel) { - const mutation = formatMutation('updateGroup', formatIndividualGQL(group), clientMutationLabel); + const mutation = formatMutation('updateGroup', formatGroupGQL(group), clientMutationLabel); const requestedDateTime = new Date(); return graphql( mutation.payload, @@ -226,6 +251,12 @@ export function downloadGroupIndividuals(params) { return graphql(payload, ACTION_TYPE.GROUP_INDIVIDUAL_EXPORT); } +export const setNewGroupIndividual = (groupIndividual) => (dispatch) => { + dispatch({ + type: SET(ACTION_TYPE.SET_GROUP_INDIVIDUAL), payload: groupIndividual, + }); +}; + export const clearGroupIndividualExport = () => (dispatch) => { dispatch({ type: CLEAR(ACTION_TYPE.GROUP_INDIVIDUAL_EXPORT), @@ -243,3 +274,15 @@ export const clearGroupExport = () => (dispatch) => { type: CLEAR(ACTION_TYPE.GROUP_EXPORT), }); }; + +export const clearGroup = () => (dispatch) => { + dispatch({ + type: CLEAR(ACTION_TYPE.GET_GROUP), + }); +}; + +export const clearGroupIndividuals = () => (dispatch) => { + dispatch({ + type: CLEAR(ACTION_TYPE.SEARCH_GROUP_INDIVIDUALS), + }); +}; diff --git a/src/components/GroupChangeDialog.js b/src/components/GroupChangeDialog.js index 56df643..24c8c71 100644 --- a/src/components/GroupChangeDialog.js +++ b/src/components/GroupChangeDialog.js @@ -1,12 +1,14 @@ import React, { useState } from 'react'; +import { useDispatch } from 'react-redux'; import { injectIntl } from 'react-intl'; import { withTheme, withStyles } from '@material-ui/core/styles'; import { Button, Dialog, DialogActions, DialogContent, DialogTitle, } from '@material-ui/core'; -import { useTranslations, useModulesManager } from '@openimis/fe-core'; +import { useTranslations, useModulesManager, useHistory } from '@openimis/fe-core'; import GroupPicker from '../pickers/GroupPicker'; +import { setNewGroupIndividual } from '../actions'; const styles = (theme) => ({ primaryButton: theme.dialog.primaryButton, @@ -19,16 +21,30 @@ function GroupChangeDialog({ onClose, onConfirm, groupIndividual, + setEditedGroupIndividual, }) { const modulesManager = useModulesManager(); + const history = useHistory(); + const dispatch = useDispatch(); const { formatMessage, formatMessageWithValues } = useTranslations('individual', modulesManager); const [groupToBeChanged, setGroupToBeChanged] = useState(null); - const handleConfirm = (groupToBeChanged) => { + const handleConfirm = () => { onConfirm(groupToBeChanged); onClose(); }; + const onMoveToNewGroup = () => { + history.push(`/${modulesManager.getRef('individual.route.group')}`); + onClose(); + dispatch(setNewGroupIndividual(groupIndividual)); + }; + + const onCancel = () => { + onClose(); + setEditedGroupIndividual(null); + }; + return ( @@ -43,15 +59,18 @@ function GroupChangeDialog({ /> + - diff --git a/src/components/GroupHeadPanel.js b/src/components/GroupHeadPanel.js index fa6ef6d..c171658 100644 --- a/src/components/GroupHeadPanel.js +++ b/src/components/GroupHeadPanel.js @@ -8,6 +8,7 @@ import { } from '@openimis/fe-core'; import { injectIntl } from 'react-intl'; import { withTheme, withStyles } from '@material-ui/core/styles'; +import { EMPTY_STRING } from '../constants'; const styles = (theme) => ({ tableTitle: theme.table.title, @@ -57,9 +58,8 @@ class GroupHeadPanel extends FormPanel { readOnly module="individual" label="group.id" - required onChange={(v) => this.updateAttribute('id', v)} - value={group?.id} + value={group?.id ?? EMPTY_STRING} /> diff --git a/src/components/GroupIndividualSearcher.js b/src/components/GroupIndividualSearcher.js index de8cbde..976b4d4 100644 --- a/src/components/GroupIndividualSearcher.js +++ b/src/components/GroupIndividualSearcher.js @@ -22,7 +22,7 @@ import EditIcon from '@material-ui/icons/Edit'; import GroupIcon from '@material-ui/icons/Group'; import DeleteIcon from '@material-ui/icons/Delete'; import { - clearGroupIndividualExport, + clearGroupIndividualExport, clearGroupIndividuals, deleteGroupIndividual, downloadGroupIndividuals, fetchGroupIndividuals, @@ -64,6 +64,10 @@ function GroupIndividualSearcher({ downloadGroupIndividuals, groupIndividualExport, errorGroupIndividualExport, + clearGroupIndividuals, + setEditedGroupIndividual, + editedGroupIndividual, + }) { const [groupIndividualToDelete, setGroupIndividualToDelete] = useState(null); const [deletedGroupIndividualUuids, setDeletedGroupIndividualUuids] = useState([]); @@ -71,7 +75,6 @@ function GroupIndividualSearcher({ const [updatedGroupIndividuals, setUpdatedGroupIndividuals] = useState([]); const [refetch, setRefetch] = useState(null); const [isChangeGroupModalOpen, setIsChangeGroupModalOpen] = useState(false); - const [groupIndividualToGroupChange, setGroupIndividualToGroupChange] = useState(null); function groupIndividualUpdatePageUrl(groupIndividual) { return `${modulesManager.getRef('individual.route.individual')}/${groupIndividual.individual?.id}`; @@ -120,7 +123,9 @@ function GroupIndividualSearcher({ prevSubmittingMutationRef.current = submittingMutation; }); - const fetch = (params) => fetchGroupIndividuals(params); + useEffect(() => () => (editedGroupIndividual ? clearGroupIndividuals() : null), [groupId]); + + const fetch = (params) => (groupId ? fetchGroupIndividuals(params) : null); const headers = () => { const headers = [ @@ -168,7 +173,7 @@ function GroupIndividualSearcher({ const handleGroupChange = (groupIndividual) => { setIsChangeGroupModalOpen(true); - setGroupIndividualToGroupChange(groupIndividual); + setEditedGroupIndividual(groupIndividual); }; const isRowUpdated = (groupIndividual) => ( @@ -180,7 +185,7 @@ function GroupIndividualSearcher({ const onChangeGroupConfirm = (groupToBeChanged) => { const updateIndividual = { - ...groupIndividualToGroupChange, + ...editedGroupIndividual, group: groupToBeChanged, role: GROUP_INDIVIDUAL_ROLES.RECIPIENT, }; @@ -308,7 +313,8 @@ function GroupIndividualSearcher({ confirmState={isChangeGroupModalOpen} onClose={() => setIsChangeGroupModalOpen(false)} onConfirm={onChangeGroupConfirm} - groupIndividual={groupIndividualToGroupChange} + groupIndividual={editedGroupIndividual} + setEditedGroupIndividual={setEditedGroupIndividual} /> bindActionCreators( deleteGroupIndividual, clearGroupIndividualExport, downloadGroupIndividuals, + clearGroupIndividuals, coreConfirm, clearConfirm, journalize, diff --git a/src/components/IndividualTabPanel.js b/src/components/IndividualTabPanel.js index c8d8439..5fc92fc 100644 --- a/src/components/IndividualTabPanel.js +++ b/src/components/IndividualTabPanel.js @@ -31,7 +31,7 @@ const styles = (theme) => ({ }); function IndividualTabPanel({ - intl, rights, classes, individual, setConfirmedAction, group, + intl, rights, classes, individual, setConfirmedAction, group, editedGroupIndividual, setEditedGroupIndividual, }) { const [activeTab, setActiveTab] = useState(individual ? BENEFIT_PLANS_LIST_TAB_VALUE : INDIVIDUALS_LIST_TAB_VALUE); @@ -54,6 +54,8 @@ function IndividualTabPanel({ tabStyle={tabStyle} group={group} individual={individual} + editedGroupIndividual={editedGroupIndividual} + setEditedGroupIndividual={setEditedGroupIndividual} /> ); diff --git a/src/components/IndividualsListTab.js b/src/components/IndividualsListTab.js index d364f89..433bcef 100644 --- a/src/components/IndividualsListTab.js +++ b/src/components/IndividualsListTab.js @@ -22,7 +22,7 @@ function IndividualsListTabLabel({ } function IndividualsListTabPanel({ - value, rights, group, individual, + value, rights, group, individual, editedGroupIndividual, setEditedGroupIndividual, }) { if (individual) { return null; @@ -37,6 +37,8 @@ function IndividualsListTabPanel({ diff --git a/src/components/tasks/GroupCreateTasks.js b/src/components/tasks/GroupCreateTasks.js new file mode 100644 index 0000000..5b28912 --- /dev/null +++ b/src/components/tasks/GroupCreateTasks.js @@ -0,0 +1,14 @@ +import React from 'react'; +import { FormattedMessage } from '@openimis/fe-core'; + +const GroupCreateTaskTableHeaders = () => [ + , + , +]; + +const GroupCreateTaskItemFormatters = () => [ + (group) => group?.id ?? 'NEW_GROUP', + (group, jsonExt) => jsonExt?.members ?? group?.group_individual_id, +]; + +export { GroupCreateTaskTableHeaders, GroupCreateTaskItemFormatters }; diff --git a/src/components/tasks/GroupIndividualUpdateTasks.js b/src/components/tasks/GroupIndividualUpdateTasks.js index 4c92007..0403915 100644 --- a/src/components/tasks/GroupIndividualUpdateTasks.js +++ b/src/components/tasks/GroupIndividualUpdateTasks.js @@ -9,7 +9,7 @@ const GroupIndividualUpdateTaskTableHeaders = () => [ const GroupIndividualUpdateTaskItemFormatters = () => [ (groupIndividual) => groupIndividual?.group ?? groupIndividual?.group_id, - (groupIndividual, jsonExt) => jsonExt?.individual_identity ?? groupIndividual?.individual, + (groupIndividual) => groupIndividual?.id, (groupIndividual) => groupIndividual?.role, ]; diff --git a/src/index.js b/src/index.js index 53780e0..17cbecd 100644 --- a/src/index.js +++ b/src/index.js @@ -36,6 +36,7 @@ import { GroupIndividualUpdateTaskTableHeaders, } from './components/tasks/GroupIndividualUpdateTasks'; import { GROUP_LABEL, INDIVIDUAL_LABEL } from './constants'; +import { GroupCreateTaskItemFormatters, GroupCreateTaskTableHeaders } from './components/tasks/GroupCreateTasks'; const ROUTE_INDIVIDUALS = 'individuals'; const ROUTE_INDIVIDUAL = 'individuals/individual'; @@ -100,6 +101,13 @@ const DEFAULT_CONFIG = { taskSource: ['GroupIndividualService'], taskCode: GROUP_LABEL, }, + { + text: , + tableHeaders: GroupCreateTaskTableHeaders, + itemFormatters: GroupCreateTaskItemFormatters, + taskSource: ['CreateGroupAndMoveIndividualService'], + taskCode: GROUP_LABEL, + }, ], }; diff --git a/src/pages/GroupPage.js b/src/pages/GroupPage.js index 9e2f3cc..a436f1b 100644 --- a/src/pages/GroupPage.js +++ b/src/pages/GroupPage.js @@ -15,14 +15,17 @@ import { connect } from 'react-redux'; import _ from 'lodash'; import { withTheme, withStyles } from '@material-ui/core/styles'; import DeleteIcon from '@material-ui/icons/Delete'; -import { RIGHT_GROUP_UPDATE } from '../constants'; -import { fetchGroup, deleteGroup, updateGroup } from '../actions'; +import { RIGHT_GROUP_CREATE, RIGHT_GROUP_SEARCH } from '../constants'; +import { + fetchGroup, deleteGroup, updateGroup, clearGroup, createGroupAndMoveIndividual, +} from '../actions'; import GroupHeadPanel from '../components/GroupHeadPanel'; import IndividualTabPanel from '../components/IndividualTabPanel'; import { ACTION_TYPE } from '../reducer'; const styles = (theme) => ({ page: theme.page, + lockedPage: theme.page.locked, }); function GroupPage({ @@ -41,15 +44,22 @@ function GroupPage({ submittingMutation, mutation, journalize, + clearGroup, + createGroupAndMoveIndividual, }) { const [editedGroup, setEditedGroup] = useState({}); + const [editedGroupIndividual, setEditedGroupIndividual] = useState(null); const [confirmedAction, setConfirmedAction] = useState(() => null); + const [readOnly, setReadOnly] = useState(null); const prevSubmittingMutationRef = useRef(); useEffect(() => { if (groupUuid) { fetchGroup([`id: "${groupUuid}"`]); } + return () => { + clearGroup(); + }; }, [groupUuid]); useEffect(() => { @@ -57,7 +67,10 @@ function GroupPage({ return () => confirmed && clearConfirm(null); }, [confirmed]); - const back = () => history.goBack(); + const back = () => { + setEditedGroupIndividual(null); + return history.goBack(); + }; useEffect(() => { if (prevSubmittingMutationRef.current && !submittingMutation) { @@ -85,12 +98,21 @@ function GroupPage({ const canSave = () => !isMandatoryFieldsEmpty() && doesGroupChange(); const handleSave = () => { - updateGroup( - editedGroup, - formatMessageWithValues(intl, 'individual', 'group.update.mutationLabel', { - id: group?.id, - }), - ); + setReadOnly(true); + if (editedGroup?.id) { + updateGroup( + editedGroup, + formatMessageWithValues(intl, 'individual', 'group.update.mutationLabel', { + id: group?.id, + }), + ); + } else if (editedGroupIndividual?.id) { + createGroupAndMoveIndividual( + editedGroup, + editedGroupIndividual.id, + formatMessageWithValues(intl, 'individual', 'group.createGroupAndMoveIndividual.mutationLabel'), + ); + } }; const deleteGroupCallback = () => deleteGroup( @@ -110,6 +132,8 @@ function GroupPage({ ); }; + const canAdd = () => rights.includes(RIGHT_GROUP_CREATE) && editedGroupIndividual && !readOnly; + const actions = [ !!group && { doIt: openDeleteGroupConfirmDialog, @@ -119,8 +143,8 @@ function GroupPage({ ]; return ( - rights.includes(RIGHT_GROUP_UPDATE) && ( -
+ rights.includes(RIGHT_GROUP_SEARCH) && ( +
) @@ -162,6 +190,8 @@ const mapDispatchToProps = (dispatch) => bindActionCreators({ fetchGroup, deleteGroup, updateGroup, + clearGroup, + createGroupAndMoveIndividual, coreConfirm, clearConfirm, journalize, diff --git a/src/reducer.js b/src/reducer.js index 067a49b..31c2869 100644 --- a/src/reducer.js +++ b/src/reducer.js @@ -12,7 +12,7 @@ import { decodeId, } from '@openimis/fe-core'; import { - REQUEST, SUCCESS, ERROR, CLEAR, + REQUEST, SUCCESS, ERROR, CLEAR, SET, } from './util/action-type'; export const ACTION_TYPE = { @@ -28,11 +28,13 @@ export const ACTION_TYPE = { UPDATE_INDIVIDUAL: 'INDIVIDUAL_UPDATE_INDIVIDUAL', UPDATE_GROUP_INDIVIDUAL: 'GROUP_INDIVIDUAL_UPDATE_GROUP_INDIVIDUAL', UPDATE_GROUP: 'GROUP_UPDATE_GROUP', + CREATE_GROUP_AND_MOVE_INDIVIDUAL: 'CREATE_GROUP_AND_MOVE_INDIVIDUAL', GROUP_EXPORT: 'GROUP_EXPORT', INDIVIDUAL_EXPORT: 'INDIVIDUAL_EXPORT', GROUP_INDIVIDUAL_EXPORT: 'GROUP_INDIVIDUAL_EXPORT', SEARCH_INDIVIDUAL_HISTORY: 'SEARCH_INDIVIDUAL_HISTORY', SEARCH_GROUP_HISTORY: 'SEARCH_GROUP_HISTORY', + SET_GROUP_INDIVIDUAL: 'SET_GROUP_INDIVIDUAL', }; function reducer( @@ -382,6 +384,24 @@ function reducer( fetchingGroupIndividualExport: false, errorGroupIndividualExport: formatServerError(action.payload), }; + case CLEAR(ACTION_TYPE.GET_GROUP): + return { + ...state, + fetchingGroup: false, + fetchedGroup: false, + group: null, + errorGroup: null, + }; + case CLEAR(ACTION_TYPE.SEARCH_GROUP_INDIVIDUALS): + return { + ...state, + fetchingGroupIndividuals: false, + fetchedGroupIndividuals: false, + groupIndividuals: [], + groupIndividualsPageInfo: {}, + groupIndividualsTotalCount: 0, + errorGroupIndividuals: null, + }; case CLEAR(ACTION_TYPE.GROUP_EXPORT): return { ...state, @@ -409,6 +429,16 @@ function reducer( individualExportPageInfo: {}, errorIndividualExport: null, }; + case SET(ACTION_TYPE.SET_GROUP_INDIVIDUAL): + return { + ...state, + fetchingGroupIndividuals: false, + fetchedGroupIndividuals: false, + groupIndividuals: [action?.payload], + groupIndividualsPageInfo: {}, + groupIndividualsTotalCount: 1, + errorGroupIndividuals: null, + }; case REQUEST(ACTION_TYPE.MUTATION): return dispatchMutationReq(state, action); case ERROR(ACTION_TYPE.MUTATION): @@ -425,6 +455,8 @@ function reducer( return dispatchMutationResp(state, 'deleteGroup', action); case SUCCESS(ACTION_TYPE.UPDATE_GROUP): return dispatchMutationResp(state, 'updateGroup', action); + case SUCCESS(ACTION_TYPE.CREATE_GROUP_AND_MOVE_INDIVIDUAL): + return dispatchMutationResp(state, 'createGroupAndMoveIndividual', action); default: return state; } diff --git a/src/translations/en.json b/src/translations/en.json index 62a8210..0140b4b 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -96,7 +96,16 @@ "message": "Deleting data does not mean erasing it from OpenIMIS database. The data will only be deactivated from the viewed list." }, "mutationLabel": "Delete Group {id}" - } + }, + "createGroupAndMoveIndividual": { + "mutationLabel": "Create group and move individual." + }, + "tasks" : { + "create": { + "title": "Group Create Tasks" + } + }, + "members": "Members" }, "groupHistory": { "pageTitle": "Group {id}", @@ -159,5 +168,6 @@ }, "confirm": "Confirm", "cancel": "Cancel", - "changeGroupButtonTooltip": "Move to another group." + "changeGroupButtonTooltip": "Move to another group.", + "moveToNewGroup": "New Group" } \ No newline at end of file diff --git a/src/util/action-type.js b/src/util/action-type.js index be3f6c7..fb01b28 100644 --- a/src/util/action-type.js +++ b/src/util/action-type.js @@ -2,3 +2,4 @@ export const REQUEST = (actionTypeName) => `${actionTypeName}_REQ`; export const SUCCESS = (actionTypeName) => `${actionTypeName}_RESP`; export const ERROR = (actionTypeName) => `${actionTypeName}_ERR`; export const CLEAR = (actionTypeName) => `${actionTypeName}_CLEAR`; +export const SET = (actionTypeName) => `${actionTypeName}_SET`; From 8871be04d689a2c30909ae59c64b5258a92ea4fa Mon Sep 17 00:00:00 2001 From: sniedzielski Date: Thu, 25 Jan 2024 15:49:28 +0100 Subject: [PATCH 49/90] hackaton --- src/actions.js | 15 ++ src/components/IndividualTabPanel.js | 4 + .../BenefitPlanBeneficiariesUploadDialog.js | 216 ++++++++++++++++++ src/pickers/WorkflowsPicker.js | 44 ++++ src/reducer.js | 31 +++ 5 files changed, 310 insertions(+) create mode 100644 src/components/dialogs/BenefitPlanBeneficiariesUploadDialog.js create mode 100644 src/pickers/WorkflowsPicker.js diff --git a/src/actions.js b/src/actions.js index 08bc47f..9949e2e 100644 --- a/src/actions.js +++ b/src/actions.js @@ -1,6 +1,7 @@ import { graphql, formatPageQuery, + formatQuery, formatPageQueryWithCount, formatMutation, formatGQLString, @@ -10,6 +11,20 @@ import { CLEAR, ERROR, REQUEST, SUCCESS, } from './util/action-type'; +const WORKFLOWS_FULL_PROJECTION = () => [ + 'name', + 'group', +]; + +export function fetchWorkflows() { + const payload = formatQuery( + 'workflow', + [], + WORKFLOWS_FULL_PROJECTION(), + ); + return graphql(payload, ACTION_TYPE.GET_WORKFLOWS); +} + const INDIVIDUAL_FULL_PROJECTION = [ 'id', 'isDeleted', diff --git a/src/components/IndividualTabPanel.js b/src/components/IndividualTabPanel.js index c8d8439..cc6acde 100644 --- a/src/components/IndividualTabPanel.js +++ b/src/components/IndividualTabPanel.js @@ -8,6 +8,7 @@ import { INDIVIDUAL_TABS_LABEL_CONTRIBUTION_KEY, INDIVIDUAL_TABS_PANEL_CONTRIBUTION_KEY, INDIVIDUALS_LIST_TAB_VALUE, } from '../constants'; +import BenefitPlanBeneficiariesUploadDialog from './dialogs/BenefitPlanBeneficiariesUploadDialog'; const styles = (theme) => ({ paper: theme.paper.paper, @@ -64,6 +65,9 @@ function IndividualTabPanel({ group={group} setConfirmedAction={setConfirmedAction} /> + ); } diff --git a/src/components/dialogs/BenefitPlanBeneficiariesUploadDialog.js b/src/components/dialogs/BenefitPlanBeneficiariesUploadDialog.js new file mode 100644 index 0000000..21b08f3 --- /dev/null +++ b/src/components/dialogs/BenefitPlanBeneficiariesUploadDialog.js @@ -0,0 +1,216 @@ +import React, { useEffect, useState } from 'react'; +import { Input, Grid } from '@material-ui/core'; +import { injectIntl } from 'react-intl'; +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 { + apiHeaders, + baseApiUrl, + formatMessage, +} from '@openimis/fe-core'; +import { withTheme, withStyles } from '@material-ui/core/styles'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import WorkflowsPicker from '../../pickers/WorkflowsPicker'; +import { fetchWorkflows } from '../../actions'; + +const styles = (theme) => ({ + item: theme.paper.item, +}); + +function BenefitPlanBeneficiariesUploadDialog({ + intl, + classes, + workflows, + fetchWorkflows, + benefitPlan, +}) { + const [isOpen, setIsOpen] = useState(false); + const [forms, setForms] = useState({}); + + const handleOpen = () => { + setIsOpen(true); + }; + + const handleClose = () => { + setForms({}); + setIsOpen(false); + }; + + useEffect(() => { + fetchWorkflows(); + }, []); + + const handleFieldChange = (formName, fieldName, value) => { + setForms({ + ...forms, + [formName]: { + ...(forms[formName] ?? {}), + [fieldName]: value, + }, + }); + }; + + const getFieldValue = () => forms?.workflows?.values?.workflow?.label ?? {}; + + const onSubmit = async (values) => { + const fileFormat = values.file.type; + const formData = new FormData(); + + formData.append('file', values.file); + + let urlImport; + if (fileFormat.includes('/csv')) { + formData.append('workflow_name', values.workflow.name); + formData.append('workflow_group', values.workflow.group); + urlImport = `${baseApiUrl}/individual/import_individuals/`; + } + + try { + const response = await fetch(urlImport, { + headers: apiHeaders, + body: formData, + method: 'POST', + credentials: 'same-origin', + }); + + await response.json(); + + if (response.status >= 400) { + handleClose(); + return; + } + handleClose(); + } catch (error) { + handleClose(); + } + }; + + return ( + <> + + + + + {formatMessage(intl, 'socialProtection', 'benefitPlan.benefitPlanBeneficiaries.upload.label')} + + +
+ + + + handleFieldChange('workflows', 'file', event.target.files[0])} + required + id="import-button" + inputProps={{ + accept: '.csv, application/csv, text/csv', + }} + type="file" + /> + + + handleFieldChange('workflows', 'workflow', value)} + value={() => getFieldValue()} + workflows={workflows} + required + /> + + + +
+
+ +
+
+ +
+
+ +
+
+
+ +
+ + ); +} + +const mapStateToProps = (state) => ({ + rights: !!state.core && !!state.core.user && !!state.core.user.i_user ? state.core.user.i_user.rights : [], + confirmed: state.core.confirmed, + workflows: state.socialProtection.workflows, +}); + +const mapDispatchToProps = (dispatch) => bindActionCreators({ + fetchWorkflows, +}, dispatch); + +export default injectIntl( + withTheme( + withStyles(styles)( + connect(mapStateToProps, mapDispatchToProps)(BenefitPlanBeneficiariesUploadDialog), + ), + ), +); diff --git a/src/pickers/WorkflowsPicker.js b/src/pickers/WorkflowsPicker.js new file mode 100644 index 0000000..43d80d6 --- /dev/null +++ b/src/pickers/WorkflowsPicker.js @@ -0,0 +1,44 @@ +import React, { useEffect } from 'react'; +import { SelectInput, formatMessage } from '@openimis/fe-core'; +import { injectIntl } from 'react-intl'; + +function WorkflowsPicker({ + intl, + value, + label, + onChange, + workflows, + readOnly = false, + withNull = false, + nullLabel = null, + withLabel = true, +}) { + const options = Array.isArray(workflows) && workflows !== undefined ? [ + ...workflows.map((workflows) => ({ + value: { name: workflows.name, group: workflows.group }, + label: workflows.name, + })), + ] : []; + + useEffect(() => { + if (withNull) { + options.unshift({ + value: null, + label: nullLabel || formatMessage(intl, 'bill', 'emptyLabel'), + }); + } + }, []); + + return ( + + ); +} + +export default injectIntl(WorkflowsPicker); diff --git a/src/reducer.js b/src/reducer.js index 067a49b..86da3c3 100644 --- a/src/reducer.js +++ b/src/reducer.js @@ -33,6 +33,7 @@ export const ACTION_TYPE = { GROUP_INDIVIDUAL_EXPORT: 'GROUP_INDIVIDUAL_EXPORT', SEARCH_INDIVIDUAL_HISTORY: 'SEARCH_INDIVIDUAL_HISTORY', SEARCH_GROUP_HISTORY: 'SEARCH_GROUP_HISTORY', + GET_WORKFLOWS: 'GET_WORKFLOWS', }; function reducer( @@ -92,6 +93,12 @@ function reducer( groupHistory: [], groupHistoryPageInfo: {}, groupHistoryTotalCount: 0, + fetchingWorkflows: true, + fetchedWorkflows: false, + workflows: [], + workflowsPageInfo: {}, + workflowsGroupBeneficiaries: null, + errorWorkflows: null, }, action, ) { @@ -409,6 +416,30 @@ function reducer( individualExportPageInfo: {}, errorIndividualExport: null, }; + case REQUEST(ACTION_TYPE.GET_WORKFLOWS): + return { + ...state, + fetchingWorkflows: true, + fetchedWorkflows: false, + workflows: [], + workflowsPageInfo: {}, + errorWorkflows: null, + }; + case SUCCESS(ACTION_TYPE.GET_WORKFLOWS): + return { + ...state, + fetchingWorkflows: false, + fetchedWorkflows: true, + workflows: action.payload.data.workflow || [], + workflowsPageInfo: pageInfo(action.payload.data.benefitPlan), + errorWorkflows: formatGraphQLError(action.payload), + }; + case ERROR(ACTION_TYPE.GET_WORKFLOWS): + return { + ...state, + fetchingWorkflows: false, + errorWorkflows: formatServerError(action.payload), + }; case REQUEST(ACTION_TYPE.MUTATION): return dispatchMutationReq(state, action); case ERROR(ACTION_TYPE.MUTATION): From 7f030b6452069c1e9c055097f91e112c558b25a1 Mon Sep 17 00:00:00 2001 From: sniedzielski Date: Thu, 25 Jan 2024 16:14:40 +0100 Subject: [PATCH 50/90] hackaton --- src/components/IndividualSearcher.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/IndividualSearcher.js b/src/components/IndividualSearcher.js index fb35e86..c464f75 100644 --- a/src/components/IndividualSearcher.js +++ b/src/components/IndividualSearcher.js @@ -40,6 +40,7 @@ import { } from '../constants'; import { applyNumberCircle } from '../util/searcher-utils'; import IndividualFilter from './IndividualFilter'; +import BenefitPlanBeneficiariesUploadDialog from './dialogs/BenefitPlanBeneficiariesUploadDialog'; function IndividualSearcher({ intl, @@ -292,6 +293,9 @@ function IndividualSearcher({ +
)} From ab90cb23bdcf71a60a6fba87dc9fab777c028565 Mon Sep 17 00:00:00 2001 From: sniedzielski Date: Thu, 25 Jan 2024 16:15:21 +0100 Subject: [PATCH 51/90] hackaton2 --- src/components/IndividualTabPanel.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/IndividualTabPanel.js b/src/components/IndividualTabPanel.js index cc6acde..60f6feb 100644 --- a/src/components/IndividualTabPanel.js +++ b/src/components/IndividualTabPanel.js @@ -65,9 +65,6 @@ function IndividualTabPanel({ group={group} setConfirmedAction={setConfirmedAction} /> - ); } From 9abbface0b2b8a7b66673ab14ed8bc9f51281f51 Mon Sep 17 00:00:00 2001 From: sniedzielski Date: Thu, 25 Jan 2024 16:20:51 +0100 Subject: [PATCH 52/90] hackaton3 --- src/components/IndividualSearcher.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/IndividualSearcher.js b/src/components/IndividualSearcher.js index c464f75..943d230 100644 --- a/src/components/IndividualSearcher.js +++ b/src/components/IndividualSearcher.js @@ -293,12 +293,12 @@ function IndividualSearcher({ -
)} + ); } From 0b10b2ba9c48710cc00bdd8b318dc0532655b768 Mon Sep 17 00:00:00 2001 From: sniedzielski Date: Mon, 29 Jan 2024 13:41:07 +0100 Subject: [PATCH 53/90] hackaton: refactor UI --- src/components/IndividualSearcher.js | 6 ++-- src/components/IndividualTabPanel.js | 1 - ...adDialog.js => IndividualsUploadDialog.js} | 29 +++++++------------ src/constants.js | 1 + src/index.js | 3 ++ src/translations/en.json | 6 ++++ 6 files changed, 22 insertions(+), 24 deletions(-) rename src/components/dialogs/{BenefitPlanBeneficiariesUploadDialog.js => IndividualsUploadDialog.js} (86%) diff --git a/src/components/IndividualSearcher.js b/src/components/IndividualSearcher.js index 943d230..3c1b353 100644 --- a/src/components/IndividualSearcher.js +++ b/src/components/IndividualSearcher.js @@ -37,10 +37,10 @@ import { SOCIAL_PROTECTION_MODULE_NAME, RIGHT_SCHEMA_SEARCH, FETCH_BENEFIT_PLAN_SCHEMA_FIELDS_REF, + INDIVIDUALS_UPLOAD_FORM_CONTRIBUTION_KEY, } from '../constants'; import { applyNumberCircle } from '../util/searcher-utils'; import IndividualFilter from './IndividualFilter'; -import BenefitPlanBeneficiariesUploadDialog from './dialogs/BenefitPlanBeneficiariesUploadDialog'; function IndividualSearcher({ intl, @@ -285,6 +285,7 @@ function IndividualSearcher({ chooseExportableColumns cacheFiltersKey="individualsFilterCache" resetFiltersOnUnmount + actionsContributionKey={INDIVIDUALS_UPLOAD_FORM_CONTRIBUTION_KEY} /> {failedExport && ( @@ -296,9 +297,6 @@ function IndividualSearcher({ )} - ); } diff --git a/src/components/IndividualTabPanel.js b/src/components/IndividualTabPanel.js index 60f6feb..c8d8439 100644 --- a/src/components/IndividualTabPanel.js +++ b/src/components/IndividualTabPanel.js @@ -8,7 +8,6 @@ import { INDIVIDUAL_TABS_LABEL_CONTRIBUTION_KEY, INDIVIDUAL_TABS_PANEL_CONTRIBUTION_KEY, INDIVIDUALS_LIST_TAB_VALUE, } from '../constants'; -import BenefitPlanBeneficiariesUploadDialog from './dialogs/BenefitPlanBeneficiariesUploadDialog'; const styles = (theme) => ({ paper: theme.paper.paper, diff --git a/src/components/dialogs/BenefitPlanBeneficiariesUploadDialog.js b/src/components/dialogs/IndividualsUploadDialog.js similarity index 86% rename from src/components/dialogs/BenefitPlanBeneficiariesUploadDialog.js rename to src/components/dialogs/IndividualsUploadDialog.js index 21b08f3..15701db 100644 --- a/src/components/dialogs/BenefitPlanBeneficiariesUploadDialog.js +++ b/src/components/dialogs/IndividualsUploadDialog.js @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { Input, Grid } from '@material-ui/core'; +import { Input, Grid, MenuItem } from '@material-ui/core'; import { injectIntl } from 'react-intl'; import Button from '@material-ui/core/Button'; import Dialog from '@material-ui/core/Dialog'; @@ -21,12 +21,10 @@ const styles = (theme) => ({ item: theme.paper.item, }); -function BenefitPlanBeneficiariesUploadDialog({ +function IndividualsUploadDialog({ intl, - classes, workflows, fetchWorkflows, - benefitPlan, }) { const [isOpen, setIsOpen] = useState(false); const [forms, setForms] = useState({}); @@ -91,18 +89,11 @@ function BenefitPlanBeneficiariesUploadDialog({ return ( <> - + {formatMessage(intl, 'individual', 'individual.upload.buttonLabel')} + - {formatMessage(intl, 'socialProtection', 'benefitPlan.benefitPlanBeneficiaries.upload.label')} + {formatMessage(intl, 'individual', 'individual.upload.label')}
handleFieldChange('workflows', 'workflow', value)} value={() => getFieldValue()} @@ -171,7 +162,7 @@ function BenefitPlanBeneficiariesUploadDialog({ marginBottom: '15px', }} > - Cancel + {formatMessage(intl, 'individual', 'individual.upload.cancel')}
@@ -186,7 +177,7 @@ function BenefitPlanBeneficiariesUploadDialog({ ) } > - {formatMessage(intl, 'socialProtection', 'benefitPlan.benefitPlanBeneficiaries.upload.label')} + {formatMessage(intl, 'individual', 'individual.upload.label')}
@@ -210,7 +201,7 @@ const mapDispatchToProps = (dispatch) => bindActionCreators({ export default injectIntl( withTheme( withStyles(styles)( - connect(mapStateToProps, mapDispatchToProps)(BenefitPlanBeneficiariesUploadDialog), + connect(mapStateToProps, mapDispatchToProps)(IndividualsUploadDialog), ), ), ); diff --git a/src/constants.js b/src/constants.js index 910b4c4..da3118b 100644 --- a/src/constants.js +++ b/src/constants.js @@ -58,3 +58,4 @@ export const GROUP_LABEL = 'Group'; export const SOCIAL_PROTECTION_MODULE_NAME = 'social_protection'; export const FETCH_BENEFIT_PLAN_SCHEMA_FIELDS_REF = 'socialProtection.fetchBenefitPlanSchemaFields'; +export const INDIVIDUALS_UPLOAD_FORM_CONTRIBUTION_KEY = 'individual.IndividualsUploadDialog'; diff --git a/src/index.js b/src/index.js index 53780e0..c651ea1 100644 --- a/src/index.js +++ b/src/index.js @@ -36,6 +36,7 @@ import { GroupIndividualUpdateTaskTableHeaders, } from './components/tasks/GroupIndividualUpdateTasks'; import { GROUP_LABEL, INDIVIDUAL_LABEL } from './constants'; +import IndividualsUploadDialog from './components/dialogs/IndividualsUploadDialog'; const ROUTE_INDIVIDUALS = 'individuals'; const ROUTE_INDIVIDUAL = 'individuals/individual'; @@ -67,7 +68,9 @@ const DEFAULT_CONFIG = { { key: 'individual.actions.clearIndividualExport', ref: clearIndividualExport }, { key: 'individual.IndividualHistorySearcher', ref: IndividualHistorySearcher }, { key: 'individual.GroupHistorySearcher', ref: GroupHistorySearcher }, + { key: 'individual.IndividualsUploadDialog', ref: IndividualsUploadDialog }, ], + 'individual.IndividualsUploadDialog': IndividualsUploadDialog, 'individual.TabPanel.label': [ IndividualsListTabLabel, BenefitPlansListTabLabel, diff --git a/src/translations/en.json b/src/translations/en.json index 62a8210..43af24e 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -31,6 +31,12 @@ "label": "Update Individual", "mutationLabel":"Update Individual {id}" }, + "upload": { + "buttonLabel": "UPLOAD", + "label": "Upload Individuals", + "workflowPicker": "Workflow", + "cancel": "Cancel" + }, "saveButton.tooltip.enabled": "Save changes", "saveButton.tooltip.disabled": "Please fill General Information fields first", "individualsList": { From 8e6408ac22218c005054ead9d2b7c11c81cb3e40 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 29 Jan 2024 17:38:34 +0100 Subject: [PATCH 54/90] CM-458: adjust group create task display (#44) Co-authored-by: Jan --- src/components/tasks/GroupCreateTasks.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/tasks/GroupCreateTasks.js b/src/components/tasks/GroupCreateTasks.js index 5b28912..c6e5363 100644 --- a/src/components/tasks/GroupCreateTasks.js +++ b/src/components/tasks/GroupCreateTasks.js @@ -7,8 +7,8 @@ const GroupCreateTaskTableHeaders = () => [ ]; const GroupCreateTaskItemFormatters = () => [ - (group) => group?.id ?? 'NEW_GROUP', - (group, jsonExt) => jsonExt?.members ?? group?.group_individual_id, + (group) => group?.id, + (group) => group?.group_individual_id, ]; export { GroupCreateTaskTableHeaders, GroupCreateTaskItemFormatters }; From 6856ed6803bda8cd66be04bbae80ecf6d882f2ff Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 8 Feb 2024 11:09:37 +0100 Subject: [PATCH 55/90] CM-501: adjust frontend to individual custom filters (#45) Co-authored-by: Jan --- src/components/GroupSearcher.js | 6 +++--- src/components/IndividualSearcher.js | 8 ++++---- src/constants.js | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/GroupSearcher.js b/src/components/GroupSearcher.js index 8dfc805..d557ee0 100644 --- a/src/components/GroupSearcher.js +++ b/src/components/GroupSearcher.js @@ -28,7 +28,7 @@ import { import { DEFAULT_PAGE_SIZE, ROWS_PER_PAGE_OPTIONS, - RIGHT_GROUP_UPDATE, RIGHT_GROUP_DELETE, SOCIAL_PROTECTION_MODULE_NAME, BENEFIT_PLAN_LABEL, + RIGHT_GROUP_UPDATE, RIGHT_GROUP_DELETE, INDIVIDUAL_MODULE_NAME, INDIVIDUAL_LABEL, } from '../constants'; import GroupFilter from './GroupFilter'; import { applyNumberCircle } from '../util/searcher-utils'; @@ -229,8 +229,8 @@ function GroupSearcher({ cacheFiltersKey="groupsFilterCache" resetFiltersOnUnmount isCustomFiltering - moduleName={SOCIAL_PROTECTION_MODULE_NAME} - objectType={BENEFIT_PLAN_LABEL} + moduleName={INDIVIDUAL_MODULE_NAME} + objectType={INDIVIDUAL_LABEL} additionalCustomFilterParams={{ type: 'GROUP' }} appliedCustomFilters={appliedCustomFilters} setAppliedCustomFilters={setAppliedCustomFilters} diff --git a/src/components/IndividualSearcher.js b/src/components/IndividualSearcher.js index fb35e86..1f52b03 100644 --- a/src/components/IndividualSearcher.js +++ b/src/components/IndividualSearcher.js @@ -33,10 +33,10 @@ import { EMPTY_STRING, RIGHT_INDIVIDUAL_UPDATE, RIGHT_INDIVIDUAL_DELETE, - BENEFIT_PLAN_LABEL, - SOCIAL_PROTECTION_MODULE_NAME, RIGHT_SCHEMA_SEARCH, FETCH_BENEFIT_PLAN_SCHEMA_FIELDS_REF, + INDIVIDUAL_MODULE_NAME, + INDIVIDUAL_LABEL, } from '../constants'; import { applyNumberCircle } from '../util/searcher-utils'; import IndividualFilter from './IndividualFilter'; @@ -270,8 +270,8 @@ function IndividualSearcher({ exportable exportFetch={downloadIndividuals} isCustomFiltering - moduleName={SOCIAL_PROTECTION_MODULE_NAME} - objectType={BENEFIT_PLAN_LABEL} + moduleName={INDIVIDUAL_MODULE_NAME} + objectType={INDIVIDUAL_LABEL} additionalCustomFilterParams={{ type: 'INDIVIDUAL' }} appliedCustomFilters={appliedCustomFilters} setAppliedCustomFilters={setAppliedCustomFilters} diff --git a/src/constants.js b/src/constants.js index 910b4c4..233a0fd 100644 --- a/src/constants.js +++ b/src/constants.js @@ -55,6 +55,6 @@ export const BENEFIT_PLAN_LABEL = 'BenefitPlan'; export const INDIVIDUAL_LABEL = 'Individual'; export const GROUP_LABEL = 'Group'; -export const SOCIAL_PROTECTION_MODULE_NAME = 'social_protection'; +export const INDIVIDUAL_MODULE_NAME = 'individual'; export const FETCH_BENEFIT_PLAN_SCHEMA_FIELDS_REF = 'socialProtection.fetchBenefitPlanSchemaFields'; From ab1a6693382ef420e3acda098516607e956a6b05 Mon Sep 17 00:00:00 2001 From: sniedzielski Date: Thu, 15 Feb 2024 13:11:16 +0100 Subject: [PATCH 56/90] prepare to merge from hackaton --- src/constants.js | 1 + src/index.js | 6 ++++++ src/pages/EnrollmentPage.js | 25 +++++++++++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 src/pages/EnrollmentPage.js diff --git a/src/constants.js b/src/constants.js index 233a0fd..3461d76 100644 --- a/src/constants.js +++ b/src/constants.js @@ -58,3 +58,4 @@ export const GROUP_LABEL = 'Group'; export const INDIVIDUAL_MODULE_NAME = 'individual'; export const FETCH_BENEFIT_PLAN_SCHEMA_FIELDS_REF = 'socialProtection.fetchBenefitPlanSchemaFields'; +export const INDIVIDUAL_ENROLMENT_DIALOG_CONTRIBUTION_KEY = 'individual.IndividualsEnrolmentDialog'; diff --git a/src/index.js b/src/index.js index 17cbecd..adaf051 100644 --- a/src/index.js +++ b/src/index.js @@ -9,6 +9,7 @@ import reducer from './reducer'; import IndividualsMainMenu from './menus/IndividualsMainMenu'; import IndividualsPage from './pages/IndividualsPage'; import IndividualPage from './pages/IndividualPage'; +import EnrollmentPage from './pages/EnrollmentPage'; import GroupsPage from './pages/GroupsPage'; import GroupPage from './pages/GroupPage'; import { IndividualsListTabLabel, IndividualsListTabPanel } from './components/IndividualsListTab'; @@ -37,12 +38,14 @@ import { } from './components/tasks/GroupIndividualUpdateTasks'; import { GROUP_LABEL, INDIVIDUAL_LABEL } from './constants'; import { GroupCreateTaskItemFormatters, GroupCreateTaskTableHeaders } from './components/tasks/GroupCreateTasks'; +import IndividualEnrollmentDialog from './components/dialogs/IndividualEnrollmentDialog'; const ROUTE_INDIVIDUALS = 'individuals'; const ROUTE_INDIVIDUAL = 'individuals/individual'; const ROUTE_INDIVIDUAL_FROM_GROUP = 'groups/group/individuals/individual'; const ROUTE_GROUPS = 'groups'; const ROUTE_GROUP = 'groups/group'; +const ROUTE_ENROLLMENT = 'enrollment'; const BENEFIT_PLAN_TABS_LABEL_REF_KEY = 'socialProtection.BenefitPlansListTabLabel'; const BENEFIT_PLAN_TABS_PANEL_REF_KEY = 'socialProtection.BenefitPlansListTabPanel'; @@ -55,6 +58,7 @@ const DEFAULT_CONFIG = { 'core.Router': [ { path: ROUTE_INDIVIDUALS, component: IndividualsPage }, { path: ROUTE_GROUPS, component: GroupsPage }, + { path: `${ROUTE_INDIVIDUAL}/${ROUTE_ENROLLMENT}?`, component: EnrollmentPage }, { path: `${ROUTE_INDIVIDUAL}/:individual_uuid?`, component: IndividualPage }, { path: `${ROUTE_INDIVIDUAL_FROM_GROUP}/:individual_uuid?`, component: IndividualPage }, { path: `${ROUTE_GROUP}/:group_uuid?`, component: GroupPage }, @@ -68,7 +72,9 @@ const DEFAULT_CONFIG = { { key: 'individual.actions.clearIndividualExport', ref: clearIndividualExport }, { key: 'individual.IndividualHistorySearcher', ref: IndividualHistorySearcher }, { key: 'individual.GroupHistorySearcher', ref: GroupHistorySearcher }, + { key: 'individual.IndividualsEnrolmentDialog', ref: IndividualEnrollmentDialog }, ], + 'individual.IndividualsEnrolmentDialog': IndividualEnrollmentDialog, 'individual.TabPanel.label': [ IndividualsListTabLabel, BenefitPlansListTabLabel, diff --git a/src/pages/EnrollmentPage.js b/src/pages/EnrollmentPage.js new file mode 100644 index 0000000..4fca989 --- /dev/null +++ b/src/pages/EnrollmentPage.js @@ -0,0 +1,25 @@ +/* eslint-disable max-len */ +import React, { useState } from 'react'; +import { + useModulesManager, + useTranslations, +} from '@openimis/fe-core'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; + +function EnrollmentPage() { + return ( + <> + + ); +} + +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)(EnrollmentPage); From 793380134f49789253d78bebcec042e8fb5043c2 Mon Sep 17 00:00:00 2001 From: sniedzielski Date: Thu, 15 Feb 2024 15:49:01 +0100 Subject: [PATCH 57/90] CM-711: added status for enrollment process --- src/components/EnrollmentHeadPanel.js | 10 +++++++++- src/pages/EnrollmentPage.js | 1 - 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/EnrollmentHeadPanel.js b/src/components/EnrollmentHeadPanel.js index 3b7d6a8..48275c6 100644 --- a/src/components/EnrollmentHeadPanel.js +++ b/src/components/EnrollmentHeadPanel.js @@ -70,7 +70,6 @@ class EnrollmentHeadPanel extends FormPanel { const { edited, classes, intl } = this.props; const enrollment = { ...edited }; const { appliedCustomFilters, appliedFiltersRowStructure } = this.state; - console.log(enrollment); return ( <> + + this.updateAttribute('status', status)} + value={enrollment?.status} + /> + ); diff --git a/src/pages/EnrollmentPage.js b/src/pages/EnrollmentPage.js index f21f229..a02f259 100644 --- a/src/pages/EnrollmentPage.js +++ b/src/pages/EnrollmentPage.js @@ -12,7 +12,6 @@ import { coreConfirm, clearConfirm, journalize, - decodeId, } from '@openimis/fe-core'; import EnrollmentHeadPanel from '../components/EnrollmentHeadPanel'; From d7efa242dcf0de4169f72aa8e04b6a5c1419b758 Mon Sep 17 00:00:00 2001 From: sniedzielski Date: Fri, 16 Feb 2024 14:38:01 +0100 Subject: [PATCH 58/90] CM-711: added summary of selection for enrollment --- src/actions.js | 16 ++ src/components/EnrollmentHeadPanel.js | 42 +-- .../dialogs/AdvancedCriteriaDialog.js | 253 +++++++++--------- .../dialogs/AdvancedCriteriaRowValue.js | 14 +- .../dialogs/IndividualsUploadDialog.js | 2 - src/reducer.js | 27 ++ src/translations/en.json | 6 +- 7 files changed, 205 insertions(+), 155 deletions(-) diff --git a/src/actions.js b/src/actions.js index ef2d637..d09cc1a 100644 --- a/src/actions.js +++ b/src/actions.js @@ -16,6 +16,13 @@ const WORKFLOWS_FULL_PROJECTION = () => [ 'group', ]; +const ENROLLMENT_SUMMARY_FULL_PROJECTION = () => [ + 'totalNumberOfIndividuals', + 'numberOfSelectedIndividuals', + 'numberOfIndividualsAssignedToProgramme', + 'numberOfIndividualsNotAssignedToProgramme', +]; + export function fetchWorkflows() { const payload = formatQuery( 'workflow', @@ -64,6 +71,15 @@ const GROUP_HISTORY_FULL_PROJECTION = GROUP_FULL_PROJECTION.filter( (item) => item !== 'head {firstName, lastName}', ); +export function fetchIndividualEnrollmentSummary(params) { + const payload = formatQuery( + 'individualEnrollmentSummary', + params, + ENROLLMENT_SUMMARY_FULL_PROJECTION(), + ); + return graphql(payload, ACTION_TYPE.ENROLLMENT_SUMMARY); +} + export function fetchIndividuals(params) { const payload = formatPageQueryWithCount('individual', params, INDIVIDUAL_FULL_PROJECTION); return graphql(payload, ACTION_TYPE.SEARCH_INDIVIDUALS); diff --git a/src/components/EnrollmentHeadPanel.js b/src/components/EnrollmentHeadPanel.js index 48275c6..2d8059f 100644 --- a/src/components/EnrollmentHeadPanel.js +++ b/src/components/EnrollmentHeadPanel.js @@ -1,8 +1,9 @@ +/* eslint-disable max-len */ /* eslint-disable camelcase */ -import React from 'react'; +import React, { Fragment } from 'react'; import { injectIntl } from 'react-intl'; -import { Grid } from '@material-ui/core'; +import { Grid, Divider } from '@material-ui/core'; import { withStyles, withTheme } from '@material-ui/core/styles'; import { @@ -72,19 +73,6 @@ class EnrollmentHeadPanel extends FormPanel { const { appliedCustomFilters, appliedFiltersRowStructure } = this.state; return ( <> - + + + <> +
+ Criteria +
+ + + + + +
); } diff --git a/src/components/dialogs/AdvancedCriteriaDialog.js b/src/components/dialogs/AdvancedCriteriaDialog.js index 0b4016c..ef5259e 100644 --- a/src/components/dialogs/AdvancedCriteriaDialog.js +++ b/src/components/dialogs/AdvancedCriteriaDialog.js @@ -1,10 +1,7 @@ import React, { useEffect, useState } from 'react'; import { injectIntl } from 'react-intl'; 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 { Grid, Paper } from '@material-ui/core'; import { decodeId, formatMessage, @@ -14,9 +11,11 @@ import { withTheme, withStyles } from '@material-ui/core/styles'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import AddCircle from '@material-ui/icons/Add'; +import Typography from '@material-ui/core/Typography'; import AdvancedCriteriaRowValue from './AdvancedCriteriaRowValue'; import { CLEARED_STATE_FILTER, INDIVIDUAL } from '../../constants'; import { isBase64Encoded, isEmptyObject } from '../../utils'; +import { fetchIndividualEnrollmentSummary } from '../../actions'; const styles = (theme) => ({ item: theme.paper.item, @@ -37,6 +36,11 @@ function AdvancedCriteriaDialog({ updateAttributes, getDefaultAppliedCustomFilters, additionalParams, + fetchIndividualEnrollmentSummary, + enrollmentSummary, + errorEnrollmentSummary, + fetchingEnrollmentSummary, + fetchedEnrollmentSummary, }) { const [isOpen, setIsOpen] = useState(false); const [currentFilter, setCurrentFilter] = useState({ @@ -59,26 +63,14 @@ function AdvancedCriteriaDialog({ }; const fetchFilters = (params) => { - console.log(params); fetchCustomFilter(params); }; - const handleOpen = () => { - setFilters(getDefaultAppliedCustomFilters()); - setIsOpen(true); - }; - const handleClose = () => { setCurrentFilter(CLEARED_STATE_FILTER); setIsOpen(false); }; - const handleRemoveFilter = () => { - setCurrentFilter(CLEARED_STATE_FILTER); - setAppliedFiltersRowStructure([CLEARED_STATE_FILTER]); - setFilters([CLEARED_STATE_FILTER]); - }; - const handleAddFilter = () => { setCurrentFilter(CLEARED_STATE_FILTER); setFilters([...filters, CLEARED_STATE_FILTER]); @@ -95,19 +87,33 @@ function AdvancedCriteriaDialog({ return updatedJsonExt; } + const handleRemoveFilter = () => { + setCurrentFilter(CLEARED_STATE_FILTER); + setAppliedFiltersRowStructure([CLEARED_STATE_FILTER]); + setFilters([CLEARED_STATE_FILTER]); + }; + const saveCriteria = () => { setAppliedFiltersRowStructure(filters); const outputFilters = JSON.stringify( filters.map(({ - filter, value, field, type, amount, + filter, value, field, type, }) => ({ - amount, custom_filter_condition: `${field}__${filter}__${type}=${value}`, })), ); const jsonExt = updateJsonExt(objectToSave.jsonExt, outputFilters); updateAttributes(jsonExt); setAppliedCustomFilters(outputFilters); + + // Parse the jsonExt string to extract advanced_criteria + const jsonData = JSON.parse(jsonExt); + const advancedCriteria = jsonData.advanced_criteria || []; + + // Extract custom_filter_condition values and construct customFilters array + const customFilters = advancedCriteria.map((criterion) => `"${criterion.custom_filter_condition}"`); + const params = [`customFilters: [${customFilters}]`]; + fetchIndividualEnrollmentSummary(params); handleClose(); }; @@ -136,119 +142,113 @@ function AdvancedCriteriaDialog({ return ( <> - - ( + + ))} +
- - {formatMessage(intl, 'paymentPlan', 'paymentPlan.advancedCriteria.button.AdvancedCriteria')} - - - {filters.map((filter, index) => ( - - ))} -
- - -
-
- + -
-
+
+
+
+ - -
-
- -
+ > + {formatMessage(intl, 'paymentPlan', 'paymentPlan.advancedCriteria.button.clearAllFilters')} + + +
+ +
+ + {fetchedEnrollmentSummary && ( +
+ + + + + {formatMessage(intl, 'individual', 'individual.enrollment.totalNumberOfIndividuals')} + + + {enrollmentSummary.totalNumberOfIndividuals} + + + + + + + {formatMessage(intl, 'individual', 'individual.enrollment.numberOfSelectedIndividuals')} + + + {enrollmentSummary.numberOfSelectedIndividuals} + + + + + + + {formatMessage(intl, 'individual', 'individual.enrollment.numberOfIndividualsAssignedToProgramme')} + + + {enrollmentSummary.numberOfIndividualsAssignedToProgramme} + + + + + + + {formatMessage(intl, 'individual', 'individual.enrollment.numberOfIndividualsNotAssignedToProgramme')} + + + {enrollmentSummary.numberOfIndividualsNotAssignedToProgramme} + + + + +
+ )} ); } @@ -260,10 +260,15 @@ const mapStateToProps = (state, props) => ({ errorCustomFilters: state.core.errorCustomFilters, fetchedCustomFilters: state.core.fetchedCustomFilters, customFilters: state.core.customFilters, + fetchingEnrollmentSummary: state.individual.fetchingEnrollmentSummary, + errorEnrollmentSummary: state.individual.errorEnrollmentSummary, + fetchedEnrollmentSummary: state.individual.fetchedEnrollmentSummary, + enrollmentSummary: state.individual.enrollmentSummary, }); const mapDispatchToProps = (dispatch) => bindActionCreators({ fetchCustomFilter, + fetchIndividualEnrollmentSummary, }, dispatch); export default injectIntl(withTheme(withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(AdvancedCriteriaDialog)))); diff --git a/src/components/dialogs/AdvancedCriteriaRowValue.js b/src/components/dialogs/AdvancedCriteriaRowValue.js index 93b8e13..bb9135e 100644 --- a/src/components/dialogs/AdvancedCriteriaRowValue.js +++ b/src/components/dialogs/AdvancedCriteriaRowValue.js @@ -40,7 +40,7 @@ function AdvancedCriteriaRowValue({ if (attribute === 'field') { updatedFilter = { ...{ - filter: '', value: '', type: value.type, amount: '', + filter: '', value: '', type: value.type, }, }; } @@ -159,18 +159,6 @@ function AdvancedCriteriaRowValue({ {renderInputBasedOnType(currentFilter.type)} ) : (<>) } - {currentFilter.field !== '' && currentFilter.filter !== '' && currentFilter.value !== '' ? ( - - - - ) : (<>) } ); } diff --git a/src/components/dialogs/IndividualsUploadDialog.js b/src/components/dialogs/IndividualsUploadDialog.js index 3828018..ca2f320 100644 --- a/src/components/dialogs/IndividualsUploadDialog.js +++ b/src/components/dialogs/IndividualsUploadDialog.js @@ -10,7 +10,6 @@ import { apiHeaders, baseApiUrl, useModulesManager, - useTranslations, formatMessage, } from '@openimis/fe-core'; import { withTheme, withStyles } from '@material-ui/core/styles'; @@ -18,7 +17,6 @@ import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import WorkflowsPicker from '../../pickers/WorkflowsPicker'; import { fetchWorkflows } from '../../actions'; -import { INDIVIDUAL_MODULE_NAME } from '../../constants'; const styles = (theme) => ({ item: theme.paper.item, diff --git a/src/reducer.js b/src/reducer.js index 5f51f7c..a12c037 100644 --- a/src/reducer.js +++ b/src/reducer.js @@ -36,6 +36,7 @@ export const ACTION_TYPE = { SEARCH_GROUP_HISTORY: 'SEARCH_GROUP_HISTORY', SET_GROUP_INDIVIDUAL: 'SET_GROUP_INDIVIDUAL', GET_WORKFLOWS: 'GET_WORKFLOWS', + ENROLLMENT_SUMMARY: 'ENROLLMENT_SUMMARY', }; function reducer( @@ -101,6 +102,10 @@ function reducer( workflowsPageInfo: {}, workflowsGroupBeneficiaries: null, errorWorkflows: null, + enrollmentSummary: [], + enrollmentSummaryError: null, + fetchingEnrollmentSummary: true, + fetchedEnrollmentSummary: false, }, action, ) { @@ -470,6 +475,28 @@ function reducer( fetchingWorkflows: false, errorWorkflows: formatServerError(action.payload), }; + case REQUEST(ACTION_TYPE.ENROLLMENT_SUMMARY): + return { + ...state, + fetchingEnrollmentSummary: true, + fetchedEnrollmentSummary: false, + enrollmentSummary: {}, + enrollmentSummaryError: null, + }; + case SUCCESS(ACTION_TYPE.ENROLLMENT_SUMMARY): + return { + ...state, + fetchingEnrollmentSummary: false, + fetchedEnrollmentSummary: true, + enrollmentSummary: action.payload.data.individualEnrollmentSummary, + enrollmentSummaryError: formatGraphQLError(action.payload), + }; + case ERROR(ACTION_TYPE.ENROLLMENT_SUMMARY): + return { + ...state, + fetchingEnrollmentSummary: false, + enrollmentSummaryError: formatServerError(action.payload), + }; case REQUEST(ACTION_TYPE.MUTATION): return dispatchMutationReq(state, action); case ERROR(ACTION_TYPE.MUTATION): diff --git a/src/translations/en.json b/src/translations/en.json index e43b27c..6dd25c4 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -40,7 +40,11 @@ "enrollment": { "buttonLabel": "ENROLLMENT", "label": "Enroll Individuals to the program", - "cancel": "Cancel" + "cancel": "Cancel", + "numberOfSelectedIndividuals": "Number Of Selected Individuals", + "totalNumberOfIndividuals": "Total Number of Individuals", + "numberOfIndividualsAssignedToProgramme": "Number of Individuals Assigned To Programme", + "numberOfIndividualsNotAssignedToProgramme": "Number of Individuals not Assigned to Programme" }, "saveButton.tooltip.enabled": "Save changes", "saveButton.tooltip.disabled": "Please fill General Information fields first", From 0f0a4c426cea1d7f80351c98a948459334319cfc Mon Sep 17 00:00:00 2001 From: sniedzielski Date: Fri, 16 Feb 2024 17:26:02 +0100 Subject: [PATCH 59/90] CM-711: renamed advanced filter file --- src/actions.js | 1 + src/components/EnrollmentHeadPanel.js | 4 ++-- ...teriaDialog.js => AdvancedCriteriaForm.js} | 20 ++++++++++++++++--- src/translations/en.json | 3 ++- 4 files changed, 22 insertions(+), 6 deletions(-) rename src/components/dialogs/{AdvancedCriteriaDialog.js => AdvancedCriteriaForm.js} (92%) diff --git a/src/actions.js b/src/actions.js index d09cc1a..e20ada4 100644 --- a/src/actions.js +++ b/src/actions.js @@ -21,6 +21,7 @@ const ENROLLMENT_SUMMARY_FULL_PROJECTION = () => [ 'numberOfSelectedIndividuals', 'numberOfIndividualsAssignedToProgramme', 'numberOfIndividualsNotAssignedToProgramme', + 'numberOfIndividualsAssignedToSelectedProgramme', ]; export function fetchWorkflows() { diff --git a/src/components/EnrollmentHeadPanel.js b/src/components/EnrollmentHeadPanel.js index 2d8059f..4d3e2bc 100644 --- a/src/components/EnrollmentHeadPanel.js +++ b/src/components/EnrollmentHeadPanel.js @@ -12,7 +12,7 @@ import { PublishedComponent, withModulesManager, } from '@openimis/fe-core'; -import AdvancedCriteriaDialog from './dialogs/AdvancedCriteriaDialog'; +import AdvancedCriteriaForm from './dialogs/AdvancedCriteriaForm'; import { CLEARED_STATE_FILTER } from '../constants'; const styles = (theme) => ({ @@ -102,7 +102,7 @@ class EnrollmentHeadPanel extends FormPanel { - ({ item: theme.paper.item, }); -function AdvancedCriteriaDialog({ +function AdvancedCriteriaForm({ intl, classes, object, @@ -112,7 +112,10 @@ function AdvancedCriteriaDialog({ // Extract custom_filter_condition values and construct customFilters array const customFilters = advancedCriteria.map((criterion) => `"${criterion.custom_filter_condition}"`); - const params = [`customFilters: [${customFilters}]`]; + const params = [ + `customFilters: [${customFilters}]`, + `benefitPlanId: "${decodeId(object.id)}"`, + ]; fetchIndividualEnrollmentSummary(params); handleClose(); }; @@ -198,6 +201,7 @@ function AdvancedCriteriaDialog({ variant="contained" color="primary" autoFocus + disabled={!object} > {formatMessage(intl, 'paymentPlan', 'paymentPlan.advancedCriteria.button.filter')} @@ -246,6 +250,16 @@ function AdvancedCriteriaDialog({ + + + + {formatMessage(intl, 'individual', 'individual.enrollment.numberOfIndividualsAssignedToSelectedProgramme')} + + + {enrollmentSummary.numberOfIndividualsAssignedToSelectedProgramme} + + + )} @@ -271,4 +285,4 @@ const mapDispatchToProps = (dispatch) => bindActionCreators({ fetchIndividualEnrollmentSummary, }, dispatch); -export default injectIntl(withTheme(withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(AdvancedCriteriaDialog)))); +export default injectIntl(withTheme(withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(AdvancedCriteriaForm)))); diff --git a/src/translations/en.json b/src/translations/en.json index 6dd25c4..e6a3030 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -44,7 +44,8 @@ "numberOfSelectedIndividuals": "Number Of Selected Individuals", "totalNumberOfIndividuals": "Total Number of Individuals", "numberOfIndividualsAssignedToProgramme": "Number of Individuals Assigned To Programme", - "numberOfIndividualsNotAssignedToProgramme": "Number of Individuals not Assigned to Programme" + "numberOfIndividualsNotAssignedToProgramme": "Number of Individuals not Assigned to Programme", + "numberOfIndividualsAssignedToSelectedProgramme": "Number Of Individuals Assigned to Selected Programme" }, "saveButton.tooltip.enabled": "Save changes", "saveButton.tooltip.disabled": "Please fill General Information fields first", From 1f6404079470436c197524314a774edf6e198e12 Mon Sep 17 00:00:00 2001 From: sniedzielski Date: Fri, 16 Feb 2024 17:54:05 +0100 Subject: [PATCH 60/90] CM-711L added button to finalize enrollment selection --- .../dialogs/AdvancedCriteriaForm.js | 28 ++++++++++++++++--- src/translations/en.json | 5 ++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/components/dialogs/AdvancedCriteriaForm.js b/src/components/dialogs/AdvancedCriteriaForm.js index 60f0403..f715ecf 100644 --- a/src/components/dialogs/AdvancedCriteriaForm.js +++ b/src/components/dialogs/AdvancedCriteriaForm.js @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; import { injectIntl } from 'react-intl'; import Button from '@material-ui/core/Button'; -import { Grid, Paper } from '@material-ui/core'; +import { Divider, Grid, Paper } from '@material-ui/core'; import { decodeId, formatMessage, @@ -176,7 +176,7 @@ function AdvancedCriteriaForm({ fontSize: '0.8rem', }} > - {formatMessage(intl, 'paymentPlan', 'paymentPlan.advancedCriteria.button.addFilters')} + {formatMessage(intl, 'individual', 'individual.enrollment.addFilters')}
@@ -188,7 +188,7 @@ function AdvancedCriteriaForm({ border: '0px', }} > - {formatMessage(intl, 'paymentPlan', 'paymentPlan.advancedCriteria.button.clearAllFilters')} + {formatMessage(intl, 'individual', 'individual.enrollment.clearAllFilters')}
- {formatMessage(intl, 'paymentPlan', 'paymentPlan.advancedCriteria.button.filter')} + {formatMessage(intl, 'individual', 'individual.enrollment.previewEnrollment')}
+ {fetchedEnrollmentSummary && (
+
+ {formatMessage(intl, 'individual', 'individual.enrollment.summary')} +
+ @@ -261,6 +266,21 @@ function AdvancedCriteriaForm({ + + + + + + +
)} diff --git a/src/translations/en.json b/src/translations/en.json index e6a3030..4ffabf9 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -41,6 +41,11 @@ "buttonLabel": "ENROLLMENT", "label": "Enroll Individuals to the program", "cancel": "Cancel", + "summary": "Preview Summary of Enrollment", + "addFilters": "Add Filters", + "clearAllFilters": "Clear All Filters", + "previewEnrollment": "Preview Enrollment Process", + "confirmEnrollment": "Confirm Enrollment Process", "numberOfSelectedIndividuals": "Number Of Selected Individuals", "totalNumberOfIndividuals": "Total Number of Individuals", "numberOfIndividualsAssignedToProgramme": "Number of Individuals Assigned To Programme", From 567c0313f1c8d2920bab0cee57c8af7f34d062a0 Mon Sep 17 00:00:00 2001 From: sniedzielski Date: Fri, 16 Feb 2024 21:01:49 +0100 Subject: [PATCH 61/90] CM-711: added mutation to confirm enrollment process --- src/actions.js | 23 ++++ .../dialogs/AdvancedCriteriaForm.js | 106 +++++++++++++----- .../dialogs/AdvancedCriteriaRowValue.js | 9 +- src/reducer.js | 1 + 4 files changed, 111 insertions(+), 28 deletions(-) diff --git a/src/actions.js b/src/actions.js index e20ada4..5538f01 100644 --- a/src/actions.js +++ b/src/actions.js @@ -191,6 +191,13 @@ function formatGroupIndividualGQL(groupIndividual) { ${groupIndividual?.group.id ? `groupId: "${groupIndividual.group.id}"` : ''}`; } +function formatConfirmEnrollmentGQL(params) { + return ` + ${params?.customFilters ? `customFilters: ${params.customFilters}` : ''} + ${params?.benefitPlanId ? `benefitPlanId: ${params.benefitPlanId}` : ''} + ${params?.status ? `status: ${params.status}` : ''}`; +} + export function updateIndividual(individual, clientMutationLabel) { const mutation = formatMutation('updateIndividual', formatIndividualGQL(individual), clientMutationLabel); const requestedDateTime = new Date(); @@ -206,6 +213,22 @@ export function updateIndividual(individual, clientMutationLabel) { ); } +export function confirmEnrollment(params, clientMutationLabel) { + // eslint-disable-next-line max-len + const mutation = formatMutation('confirmIndividualEnrollment', formatConfirmEnrollmentGQL(params), clientMutationLabel); + const requestedDateTime = new Date(); + return graphql( + mutation.payload, + [REQUEST(ACTION_TYPE.MUTATION), SUCCESS(ACTION_TYPE.CONFIRM_ENROLLMENT), ERROR(ACTION_TYPE.MUTATION)], + { + actionType: ACTION_TYPE.UPDATE_INDIVIDUAL, + clientMutationId: mutation.clientMutationId, + clientMutationLabel, + requestedDateTime, + }, + ); +} + export function updateGroupIndividual(groupIndividual, clientMutationLabel) { const mutation = formatMutation( 'editIndividualInGroup', diff --git a/src/components/dialogs/AdvancedCriteriaForm.js b/src/components/dialogs/AdvancedCriteriaForm.js index f715ecf..7e73b74 100644 --- a/src/components/dialogs/AdvancedCriteriaForm.js +++ b/src/components/dialogs/AdvancedCriteriaForm.js @@ -6,6 +6,9 @@ import { decodeId, formatMessage, fetchCustomFilter, + coreConfirm, + clearConfirm, + historyPush, } from '@openimis/fe-core'; import { withTheme, withStyles } from '@material-ui/core/styles'; import { connect } from 'react-redux'; @@ -15,7 +18,7 @@ import Typography from '@material-ui/core/Typography'; import AdvancedCriteriaRowValue from './AdvancedCriteriaRowValue'; import { CLEARED_STATE_FILTER, INDIVIDUAL } from '../../constants'; import { isBase64Encoded, isEmptyObject } from '../../utils'; -import { fetchIndividualEnrollmentSummary } from '../../actions'; +import { confirmEnrollment, fetchIndividualEnrollmentSummary } from '../../actions'; const styles = (theme) => ({ item: theme.paper.item, @@ -41,6 +44,10 @@ function AdvancedCriteriaForm({ errorEnrollmentSummary, fetchingEnrollmentSummary, fetchedEnrollmentSummary, + confirmEnrollment, + confirmed, + clearConfirm, + coreConfirm, }) { const [isOpen, setIsOpen] = useState(false); const [currentFilter, setCurrentFilter] = useState({ @@ -143,6 +150,41 @@ function AdvancedCriteriaForm({ useEffect(() => {}, [filters]); + const openConfirmEnrollmentDialog = () => { + coreConfirm( + 'Confirm', + formatMessage(intl, 'individual', 'individul.enrollment.confirmMessageDialog'), + ); + }; + + useEffect(() => { + if (confirmed) { + const outputFilters = JSON.stringify( + filters.map(({ + filter, value, field, type, + }) => ({ + custom_filter_condition: `${field}__${filter}__${type}=${value}`, + })), + ); + const jsonExt = updateJsonExt(objectToSave.jsonExt, outputFilters); + const jsonData = JSON.parse(jsonExt); + const advancedCriteria = jsonData.advanced_criteria || []; + + // Extract custom_filter_condition values and construct customFilters array + const customFilters = advancedCriteria.map((criterion) => `"${criterion.custom_filter_condition}"`); + const params = { + customFilters: `[${customFilters}]`, + benefitPlanId: `"${decodeId(object.id)}"`, + status: '"ACTIVE"', + }; + confirmEnrollment( + params, + 'Confirmed enrollment', + ); + } + return () => confirmed && clearConfirm(false); + }, [confirmed]); + return ( <> {filters.map((filter, index) => ( @@ -153,32 +195,38 @@ function AdvancedCriteriaForm({ index={index} filters={filters} setFilters={setFilters} + readOnly={confirmed} /> ))} -
- - -
+ + + + // eslint-disable-next-line react/jsx-no-useless-fragment + ) : (<>) }
@@ -201,7 +250,7 @@ function AdvancedCriteriaForm({ variant="contained" color="primary" autoFocus - disabled={!object} + disabled={!object || confirmed} > {formatMessage(intl, 'individual', 'individual.enrollment.previewEnrollment')} @@ -270,11 +319,11 @@ function AdvancedCriteriaForm({ @@ -303,6 +352,9 @@ const mapStateToProps = (state, props) => ({ const mapDispatchToProps = (dispatch) => bindActionCreators({ fetchCustomFilter, fetchIndividualEnrollmentSummary, + confirmEnrollment, + clearConfirm, + coreConfirm, }, dispatch); export default injectIntl(withTheme(withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(AdvancedCriteriaForm)))); diff --git a/src/components/dialogs/AdvancedCriteriaRowValue.js b/src/components/dialogs/AdvancedCriteriaRowValue.js index bb9135e..742c80e 100644 --- a/src/components/dialogs/AdvancedCriteriaRowValue.js +++ b/src/components/dialogs/AdvancedCriteriaRowValue.js @@ -33,6 +33,7 @@ function AdvancedCriteriaRowValue({ index, filters, setFilters, + readOnly, }) { const onAttributeChange = (attribute) => (value) => { let updatedFilter = { ...currentFilter }; @@ -80,6 +81,7 @@ function AdvancedCriteriaRowValue({ return ( ); @@ -88,6 +90,7 @@ function AdvancedCriteriaRowValue({ ); @@ -97,12 +100,14 @@ function AdvancedCriteriaRowValue({ return ( ); } return ( ); @@ -116,7 +121,7 @@ function AdvancedCriteriaRowValue({ className={classes.item} style={{ backgroundColor: '#DFEDEF' }} > - {filters.length > 0 ? ( + {filters.length > 0 && !readOnly ? (
{currentFilter.field !== '' ? ( @@ -151,6 +157,7 @@ function AdvancedCriteriaRowValue({ onChange={onAttributeChange('filter')} customFilters={customFilters} customFilterField={currentFilter.field} + readOnly={readOnly} /> ) : (<>) } diff --git a/src/reducer.js b/src/reducer.js index a12c037..741ea56 100644 --- a/src/reducer.js +++ b/src/reducer.js @@ -37,6 +37,7 @@ export const ACTION_TYPE = { SET_GROUP_INDIVIDUAL: 'SET_GROUP_INDIVIDUAL', GET_WORKFLOWS: 'GET_WORKFLOWS', ENROLLMENT_SUMMARY: 'ENROLLMENT_SUMMARY', + CONFIRM_ENROLLMENT: 'CONFIRM_ENROLLMENT', }; function reducer( From 3af9f259a39721a6013fc7fc44b83e5615abba7f Mon Sep 17 00:00:00 2001 From: sniedzielski Date: Mon, 19 Feb 2024 13:50:04 +0100 Subject: [PATCH 62/90] CM-711: final adjustments in frontend for this feature --- src/components/EnrollmentHeadPanel.js | 7 ++++-- .../dialogs/AdvancedCriteriaForm.js | 22 +++++++++++-------- .../dialogs/AdvancedCriteriaRowValue.js | 7 +++++- src/pages/EnrollmentPage.js | 14 ++++++------ src/translations/en.json | 14 +++++++----- 5 files changed, 40 insertions(+), 24 deletions(-) diff --git a/src/components/EnrollmentHeadPanel.js b/src/components/EnrollmentHeadPanel.js index 4d3e2bc..2baaf03 100644 --- a/src/components/EnrollmentHeadPanel.js +++ b/src/components/EnrollmentHeadPanel.js @@ -1,6 +1,6 @@ /* eslint-disable max-len */ /* eslint-disable camelcase */ -import React, { Fragment } from 'react'; +import React from 'react'; import { injectIntl } from 'react-intl'; import { Grid, Divider } from '@material-ui/core'; @@ -10,6 +10,7 @@ import { decodeId, FormPanel, PublishedComponent, + formatMessage, withModulesManager, } from '@openimis/fe-core'; import AdvancedCriteriaForm from './dialogs/AdvancedCriteriaForm'; @@ -68,6 +69,7 @@ class EnrollmentHeadPanel extends FormPanel { }; render() { + // eslint-disable-next-line no-unused-vars const { edited, classes, intl } = this.props; const enrollment = { ...edited }; const { appliedCustomFilters, appliedFiltersRowStructure } = this.state; @@ -88,6 +90,7 @@ class EnrollmentHeadPanel extends FormPanel { this.updateAttribute('status', status)} value={enrollment?.status} @@ -98,7 +101,7 @@ class EnrollmentHeadPanel extends FormPanel { <>
- Criteria + {formatMessage(intl, 'individual', 'individual.enrollment.criteria')}
diff --git a/src/components/dialogs/AdvancedCriteriaForm.js b/src/components/dialogs/AdvancedCriteriaForm.js index 7e73b74..5969dc6 100644 --- a/src/components/dialogs/AdvancedCriteriaForm.js +++ b/src/components/dialogs/AdvancedCriteriaForm.js @@ -1,3 +1,4 @@ +/* eslint-disable max-len */ import React, { useEffect, useState } from 'react'; import { injectIntl } from 'react-intl'; import Button from '@material-ui/core/Button'; @@ -5,10 +6,10 @@ import { Divider, Grid, Paper } from '@material-ui/core'; import { decodeId, formatMessage, + formatMessageWithValues, fetchCustomFilter, coreConfirm, clearConfirm, - historyPush, } from '@openimis/fe-core'; import { withTheme, withStyles } from '@material-ui/core/styles'; import { connect } from 'react-redux'; @@ -34,6 +35,7 @@ function AdvancedCriteriaForm({ moduleName, objectType, setAppliedCustomFilters, + // eslint-disable-next-line no-unused-vars appliedFiltersRowStructure, setAppliedFiltersRowStructure, updateAttributes, @@ -41,15 +43,13 @@ function AdvancedCriteriaForm({ additionalParams, fetchIndividualEnrollmentSummary, enrollmentSummary, - errorEnrollmentSummary, - fetchingEnrollmentSummary, fetchedEnrollmentSummary, confirmEnrollment, confirmed, clearConfirm, coreConfirm, }) { - const [isOpen, setIsOpen] = useState(false); + // eslint-disable-next-line no-unused-vars const [currentFilter, setCurrentFilter] = useState({ field: '', filter: '', type: '', value: '', amount: '', }); @@ -75,7 +75,6 @@ function AdvancedCriteriaForm({ const handleClose = () => { setCurrentFilter(CLEARED_STATE_FILTER); - setIsOpen(false); }; const handleAddFilter = () => { @@ -85,6 +84,7 @@ function AdvancedCriteriaForm({ function updateJsonExt(inputJsonExt, outputFilters) { const existingData = JSON.parse(inputJsonExt || '{}'); + // eslint-disable-next-line no-prototype-builtins if (!existingData.hasOwnProperty('advanced_criteria')) { existingData.advanced_criteria = []; } @@ -152,8 +152,8 @@ function AdvancedCriteriaForm({ const openConfirmEnrollmentDialog = () => { coreConfirm( - 'Confirm', - formatMessage(intl, 'individual', 'individul.enrollment.confirmMessageDialog'), + formatMessage(intl, 'individual', 'individual.enrollment.confirmTitle'), + formatMessageWithValues(intl, 'individual', 'individual.enrollment.confirmMessageDialog', { benefitPlanName: object.name }), ); }; @@ -175,7 +175,7 @@ function AdvancedCriteriaForm({ const params = { customFilters: `[${customFilters}]`, benefitPlanId: `"${decodeId(object.id)}"`, - status: '"ACTIVE"', + status: `"${objectToSave.status}"`, }; confirmEnrollment( params, @@ -307,6 +307,7 @@ function AdvancedCriteriaForm({ + {/* eslint-disable-next-line max-len */} {formatMessage(intl, 'individual', 'individual.enrollment.numberOfIndividualsAssignedToSelectedProgramme')} @@ -336,6 +337,7 @@ function AdvancedCriteriaForm({ ); } +// eslint-disable-next-line no-unused-vars const mapStateToProps = (state, props) => ({ rights: !!state.core && !!state.core.user && !!state.core.user.i_user ? state.core.user.i_user.rights : [], confirmed: state.core.confirmed, @@ -357,4 +359,6 @@ const mapDispatchToProps = (dispatch) => bindActionCreators({ coreConfirm, }, dispatch); -export default injectIntl(withTheme(withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(AdvancedCriteriaForm)))); +export default injectIntl( + withTheme(withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(AdvancedCriteriaForm))), +); diff --git a/src/components/dialogs/AdvancedCriteriaRowValue.js b/src/components/dialogs/AdvancedCriteriaRowValue.js index 742c80e..6d885af 100644 --- a/src/components/dialogs/AdvancedCriteriaRowValue.js +++ b/src/components/dialogs/AdvancedCriteriaRowValue.js @@ -1,4 +1,8 @@ -import React, { useEffect, useState } from 'react'; +/* eslint-disable react/jsx-props-no-spreading */ +/* eslint-disable react/jsx-no-useless-fragment */ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ +import React from 'react'; import { injectIntl } from 'react-intl'; import { PublishedComponent, @@ -25,6 +29,7 @@ const styles = (theme) => ({ }); function AdvancedCriteriaRowValue({ + // eslint-disable-next-line no-unused-vars intl, classes, customFilters, diff --git a/src/pages/EnrollmentPage.js b/src/pages/EnrollmentPage.js index a02f259..c06ba97 100644 --- a/src/pages/EnrollmentPage.js +++ b/src/pages/EnrollmentPage.js @@ -1,4 +1,4 @@ -import React, { useState, useRef, useEffect } from 'react'; +import React, { useState } from 'react'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; @@ -25,9 +25,9 @@ function EnrollmentPage({ const modulesManager = useModulesManager(); const classes = useStyles(); const history = useHistory(); - const { formatMessage, formatMessageWithValues } = useTranslations('individual', modulesManager); + const { formatMessage } = useTranslations('individual', modulesManager); - const [editedEnrollment, setEditedEnrollment] = useState({}); + const [editedEnrollment, setEditedEnrollment] = useState({ status: 'ACTIVE' }); const back = () => history.goBack(); @@ -37,15 +37,15 @@ function EnrollmentPage({
{}} - save={() => {}} + save={null} HeadPanel={EnrollmentHeadPanel} rights={rights} actions={actions} @@ -60,8 +60,8 @@ const mapDispatchToProps = (dispatch) => bindActionCreators({ journalize, }, dispatch); +// eslint-disable-next-line no-unused-vars const mapStateToProps = (state, props) => ({ - statePayrollUuid: props?.match?.params.payroll_uuid, rights: state.core?.user?.i_user?.rights ?? [], confirmed: state.core.confirmed, submittingMutation: state.payroll.submittingMutation, diff --git a/src/translations/en.json b/src/translations/en.json index 4ffabf9..73a51b1 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -39,18 +39,22 @@ }, "enrollment": { "buttonLabel": "ENROLLMENT", + "title": "Enrollment of Individuals to the Programme", "label": "Enroll Individuals to the program", "cancel": "Cancel", "summary": "Preview Summary of Enrollment", "addFilters": "Add Filters", + "criteria": "Enrollment Criteria", "clearAllFilters": "Clear All Filters", "previewEnrollment": "Preview Enrollment Process", - "confirmEnrollment": "Confirm Enrollment Process", - "numberOfSelectedIndividuals": "Number Of Selected Individuals", + "confirmEnrollment": "Confirm Enrollment Process", + "confirmTitle": "Confirm Enrollment Process", + "numberOfSelectedIndividuals": "Total Number Of Selected Individuals", "totalNumberOfIndividuals": "Total Number of Individuals", - "numberOfIndividualsAssignedToProgramme": "Number of Individuals Assigned To Programme", - "numberOfIndividualsNotAssignedToProgramme": "Number of Individuals not Assigned to Programme", - "numberOfIndividualsAssignedToSelectedProgramme": "Number Of Individuals Assigned to Selected Programme" + "numberOfIndividualsAssignedToProgramme": "Number of Individuals Assigned To Any Programme", + "numberOfIndividualsNotAssignedToProgramme": "Number of Individuals Unassigned to Any Program", + "numberOfIndividualsAssignedToSelectedProgramme": "Number Of Individuals Assigned to Selected Programme", + "confirmMessageDialog": "Are you sure you want to confirm the enrollment of the selected individuals into the {benefitPlanName} Programme?" }, "saveButton.tooltip.enabled": "Save changes", "saveButton.tooltip.disabled": "Please fill General Information fields first", From d7f205dfa0989762ff27f26e1e41366ef056c2ac Mon Sep 17 00:00:00 2001 From: sniedzielski Date: Mon, 19 Feb 2024 15:03:56 +0100 Subject: [PATCH 63/90] CM-711: fixing enrollment mutation log --- src/components/dialogs/AdvancedCriteriaForm.js | 2 +- src/translations/en.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/dialogs/AdvancedCriteriaForm.js b/src/components/dialogs/AdvancedCriteriaForm.js index 5969dc6..213b4cd 100644 --- a/src/components/dialogs/AdvancedCriteriaForm.js +++ b/src/components/dialogs/AdvancedCriteriaForm.js @@ -179,7 +179,7 @@ function AdvancedCriteriaForm({ }; confirmEnrollment( params, - 'Confirmed enrollment', + formatMessage(intl, 'individual', 'individual.enrollment.mutationLabel'), ); } return () => confirmed && clearConfirm(false); diff --git a/src/translations/en.json b/src/translations/en.json index 73a51b1..f48d6b2 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -48,7 +48,8 @@ "clearAllFilters": "Clear All Filters", "previewEnrollment": "Preview Enrollment Process", "confirmEnrollment": "Confirm Enrollment Process", - "confirmTitle": "Confirm Enrollment Process", + "confirmTitle": "Confirm Enrollment Process", + "mutationLabel": "Enrollment has been confirmed", "numberOfSelectedIndividuals": "Total Number Of Selected Individuals", "totalNumberOfIndividuals": "Total Number of Individuals", "numberOfIndividualsAssignedToProgramme": "Number of Individuals Assigned To Any Programme", From 0bfffae2f0ea2b0263d0ccd0afed20bd9ca381b8 Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 20 Feb 2024 10:07:32 +0100 Subject: [PATCH 64/90] CM-549: fix task tab in group page (#48) Co-authored-by: Jan --- src/components/GroupTaskTab.js | 4 ++-- src/components/IndividualTabPanel.js | 10 +++++++++- src/pages/GroupPage.js | 11 +++++++++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/components/GroupTaskTab.js b/src/components/GroupTaskTab.js index 12b19c5..c99361e 100644 --- a/src/components/GroupTaskTab.js +++ b/src/components/GroupTaskTab.js @@ -22,7 +22,7 @@ function GroupTaskTabLabel({ } function GroupTaskTabPanel({ - value, group, rights, classes, + value, group, rights, classes, groupIndividualIds, }) { if (!group) return null; const modulesManager = useModulesManager(); @@ -43,7 +43,7 @@ function GroupTaskTabPanel({ > ({ }); function IndividualTabPanel({ - intl, rights, classes, individual, setConfirmedAction, group, editedGroupIndividual, setEditedGroupIndividual, + intl, + rights, + classes, + individual, + setConfirmedAction, + group, editedGroupIndividual, + setEditedGroupIndividual, + groupIndividualIds, }) { const [activeTab, setActiveTab] = useState(individual ? BENEFIT_PLANS_LIST_TAB_VALUE : INDIVIDUALS_LIST_TAB_VALUE); @@ -64,6 +71,7 @@ function IndividualTabPanel({ value={activeTab} individual={individual} group={group} + groupIndividualIds={groupIndividualIds} setConfirmedAction={setConfirmedAction} editedGroupIndividual={editedGroupIndividual} setEditedGroupIndividual={setEditedGroupIndividual} diff --git a/src/pages/GroupPage.js b/src/pages/GroupPage.js index a436f1b..a658be6 100644 --- a/src/pages/GroupPage.js +++ b/src/pages/GroupPage.js @@ -46,10 +46,12 @@ function GroupPage({ journalize, clearGroup, createGroupAndMoveIndividual, + groupIndividuals, }) { const [editedGroup, setEditedGroup] = useState({}); const [editedGroupIndividual, setEditedGroupIndividual] = useState(null); const [confirmedAction, setConfirmedAction] = useState(() => null); + const [groupIndividualIds, setGroupIndividualIds] = useState([]); const [readOnly, setReadOnly] = useState(null); const prevSubmittingMutationRef = useRef(); @@ -62,6 +64,13 @@ function GroupPage({ }; }, [groupUuid]); + useEffect(() => { + if (groupIndividuals) { + const ids = groupIndividuals.map((groupIndividual) => groupIndividual.id); + setGroupIndividualIds(ids); + } + }, [groupIndividuals]); + useEffect(() => { if (confirmed && confirmedAction) confirmedAction(); return () => confirmed && clearConfirm(null); @@ -168,6 +177,7 @@ function GroupPage({ setEditedGroupIndividual={setEditedGroupIndividual} editedGroupIndividual={editedGroupIndividual} readOnly={readOnly} + groupIndividualIds={groupIndividualIds} />
) @@ -184,6 +194,7 @@ const mapStateToProps = (state, props) => ({ errorGroup: state.individual.errorGroup, submittingMutation: state.individual.submittingMutation, mutation: state.individual.mutation, + groupIndividuals: state?.individual?.groupIndividuals, }); const mapDispatchToProps = (dispatch) => bindActionCreators({ From b7faace22f6ffb6e232fd475a364eeb3be6be347 Mon Sep 17 00:00:00 2001 From: sniedzielski <52816247+sniedzielski@users.noreply.github.com> Date: Thu, 22 Feb 2024 13:01:46 +0100 Subject: [PATCH 65/90] CM-729: added individual upload history dialog (#49) * CM-729: added individual upload history dialog * CM-729: added adjustments to the frontend --- src/actions.js | 13 + src/components/CollapsableErrorList.js | 64 +++++ .../dialogs/AdvancedCriteriaForm.js | 21 +- .../dialogs/IndividualsHistoryUploadDialog.js | 248 ++++++++++++++++++ .../dialogs/IndividualsUploadDialog.js | 2 + src/constants.js | 9 + src/reducer.js | 35 +++ src/translations/en.json | 19 +- src/utils.js | 21 ++ 9 files changed, 423 insertions(+), 9 deletions(-) create mode 100644 src/components/CollapsableErrorList.js create mode 100644 src/components/dialogs/IndividualsHistoryUploadDialog.js diff --git a/src/actions.js b/src/actions.js index 5538f01..1eea91e 100644 --- a/src/actions.js +++ b/src/actions.js @@ -22,6 +22,7 @@ const ENROLLMENT_SUMMARY_FULL_PROJECTION = () => [ 'numberOfIndividualsAssignedToProgramme', 'numberOfIndividualsNotAssignedToProgramme', 'numberOfIndividualsAssignedToSelectedProgramme', + 'numberOfIndividualsToUpload', ]; export function fetchWorkflows() { @@ -72,6 +73,13 @@ const GROUP_HISTORY_FULL_PROJECTION = GROUP_FULL_PROJECTION.filter( (item) => item !== 'head {firstName, lastName}', ); +const UPLOAD_HISTORY_FULL_PROJECTION = () => [ + 'id', + 'uuid', + 'workflow', + 'dataUpload {uuid, dateCreated, dateUpdated, sourceName, sourceType, status, error }', +]; + export function fetchIndividualEnrollmentSummary(params) { const payload = formatQuery( 'individualEnrollmentSummary', @@ -116,6 +124,11 @@ export function fetchGroupHistory(params) { return graphql(payload, ACTION_TYPE.SEARCH_GROUP_HISTORY); } +export function fetchUploadHistory(params) { + const payload = formatPageQueryWithCount('individualDataUploadHistory', params, UPLOAD_HISTORY_FULL_PROJECTION()); + return graphql(payload, ACTION_TYPE.GET_INDIVIDUAL_UPLOAD_HISTORY); +} + export function deleteIndividual(individual, clientMutationLabel) { const individualUuids = `ids: ["${individual?.id}"]`; const mutation = formatMutation('deleteIndividual', individualUuids, clientMutationLabel); diff --git a/src/components/CollapsableErrorList.js b/src/components/CollapsableErrorList.js new file mode 100644 index 0000000..4309079 --- /dev/null +++ b/src/components/CollapsableErrorList.js @@ -0,0 +1,64 @@ +import React, { useState } from 'react'; +import { injectIntl } from 'react-intl'; +import ExpandLess from '@material-ui/icons/ExpandLess'; +import ExpandMore from '@material-ui/icons/ExpandMore'; +import { + formatMessage, +} from '@openimis/fe-core'; +import { + ListItem, + ListItemText, + Collapse, +} from '@material-ui/core'; +import { withTheme, withStyles } from '@material-ui/core/styles'; + +const styles = (theme) => ({ + item: theme.paper.item, +}); + +function CollapsableErrorList({ + intl, + errors, +}) { + const [isExpanded, setIsExpanded] = useState(false); + + const handleOpen = () => { + setIsExpanded(!isExpanded); + }; + + if (!errors || !Object.keys(errors).length) { + return ( + + + + ); + } + + return ( + <> + + + {isExpanded ? : } + + + { JSON.stringify(errors) } + + + ); +} + +export default injectIntl( + withTheme( + withStyles(styles)(CollapsableErrorList), + ), +); diff --git a/src/components/dialogs/AdvancedCriteriaForm.js b/src/components/dialogs/AdvancedCriteriaForm.js index 213b4cd..55a0722 100644 --- a/src/components/dialogs/AdvancedCriteriaForm.js +++ b/src/components/dialogs/AdvancedCriteriaForm.js @@ -264,7 +264,7 @@ function AdvancedCriteriaForm({
- + {formatMessage(intl, 'individual', 'individual.enrollment.totalNumberOfIndividuals')} @@ -274,7 +274,7 @@ function AdvancedCriteriaForm({ - + {formatMessage(intl, 'individual', 'individual.enrollment.numberOfSelectedIndividuals')} @@ -284,7 +284,7 @@ function AdvancedCriteriaForm({ - + {formatMessage(intl, 'individual', 'individual.enrollment.numberOfIndividualsAssignedToProgramme')} @@ -294,7 +294,7 @@ function AdvancedCriteriaForm({ - + {formatMessage(intl, 'individual', 'individual.enrollment.numberOfIndividualsNotAssignedToProgramme')} @@ -304,7 +304,7 @@ function AdvancedCriteriaForm({ - + {/* eslint-disable-next-line max-len */} @@ -315,6 +315,17 @@ function AdvancedCriteriaForm({ + + + + {/* eslint-disable-next-line max-len */} + {formatMessage(intl, 'individual', 'individual.enrollment.numberOfIndividualsToBeUploaded')} + + + {enrollmentSummary.numberOfIndividualsToUpload} + + + diff --git a/src/components/dialogs/IndividualsHistoryUploadDialog.js b/src/components/dialogs/IndividualsHistoryUploadDialog.js new file mode 100644 index 0000000..79dffc9 --- /dev/null +++ b/src/components/dialogs/IndividualsHistoryUploadDialog.js @@ -0,0 +1,248 @@ +import React, { useEffect, useState } from 'react'; +import { injectIntl } from 'react-intl'; +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 { + formatMessage, + formatDateFromISO, + ProgressOrError, + withModulesManager, +} from '@openimis/fe-core'; +import { + TableHead, + TableBody, + Table, + TableCell, + TableRow, + TableFooter, + TableContainer, + Paper, + MenuItem, +} from '@material-ui/core'; +import { withTheme, withStyles } from '@material-ui/core/styles'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import CollapsableErrorList from '../CollapsableErrorList'; +import { fetchUploadHistory } from '../../actions'; +import { downloadInvalidItems } from '../../utils'; +import { UPLOAD_STATUS } from '../../constants'; + +const styles = (theme) => ({ + item: theme.paper.item, +}); + +function IndividualsUploadHistoryDialog({ + modulesManager, + intl, + classes, + fetchUploadHistory, + history, + fetchedHistory, + fetchingHistory, +}) { + const [isOpen, setIsOpen] = useState(false); + const [records, setRecords] = useState([]); + + const handleOpen = () => { + setIsOpen(true); + }; + + const handleClose = () => { + setIsOpen(false); + }; + + const downloadInvalidItemsFromUpload = (uploadId) => { + downloadInvalidItems(uploadId); + }; + + useEffect(() => { + if (isOpen) { + const params = []; + fetchUploadHistory(params); + } + }, [isOpen]); + + useEffect(() => { + setRecords(history); + }, [fetchedHistory]); + + return ( + <> + + {formatMessage(intl, 'individual', 'individual.upload.uploadHistoryTable.buttonLabel')} + + + + {formatMessage(intl, 'individual', 'individual.upload.uploadHistoryTable.label')} + + +
+ + + + + + + {formatMessage( + intl, + 'individual', + 'individual.upload.uploadHistoryTable.workflow', + )} + + + {formatMessage( + intl, + 'individual', + 'individual.upload.uploadHistoryTable.dateCreated', + )} + + + {formatMessage( + intl, + 'individual', + 'individual.upload.uploadHistoryTable.sourceType', + )} + + + {formatMessage( + intl, + 'individual', + 'individual.upload.uploadHistoryTable.sourceName', + )} + + + {formatMessage( + intl, + 'individual', + 'individual.upload.uploadHistoryTable.status', + )} + + + {formatMessage( + intl, + 'individual', + 'individual.upload.uploadHistoryTable.error', + )} + + + + + + + {records.map((item) => ( + + + { item.workflow } + + + { formatDateFromISO(modulesManager, intl, item.dataUpload.dateCreated) } + + + { item.dataUpload.sourceType} + + + { item.dataUpload.sourceName} + + + { item.dataUpload.status} + + + + + + {[ + UPLOAD_STATUS.WAITING_FOR_VERIFICATION, + UPLOAD_STATUS.PARTIAL_SUCCESS].includes(item.dataUpload.status) && ( + + )} + + + ))} + + +
+
+
+
+ +
+
+ +
+
+
+
+ + ); +} + +const mapStateToProps = (state) => ({ + rights: !!state.core && !!state.core.user && !!state.core.user.i_user ? state.core.user.i_user.rights : [], + confirmed: state.core.confirmed, + history: state.individual.individualDataUploadHistory, + fetchedHistory: state.individual.fetchedIndividualDataUploadHistory, + fetchingHistory: state.individual.fetchingIndividualDataUploadHistory, +}); + +const mapDispatchToProps = (dispatch) => bindActionCreators({ + fetchUploadHistory, +}, dispatch); + +export default injectIntl( + withModulesManager(withTheme( + withStyles(styles)( + connect(mapStateToProps, mapDispatchToProps)(IndividualsUploadHistoryDialog), + ), + )), +); diff --git a/src/components/dialogs/IndividualsUploadDialog.js b/src/components/dialogs/IndividualsUploadDialog.js index ca2f320..8751db2 100644 --- a/src/components/dialogs/IndividualsUploadDialog.js +++ b/src/components/dialogs/IndividualsUploadDialog.js @@ -17,6 +17,7 @@ import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import WorkflowsPicker from '../../pickers/WorkflowsPicker'; import { fetchWorkflows } from '../../actions'; +import IndividualsHistoryUploadDialog from './IndividualsHistoryUploadDialog'; const styles = (theme) => ({ item: theme.paper.item, @@ -105,6 +106,7 @@ function IndividualsUploadDialog({ > {formatMessage(intl, 'individual', 'individual.upload.buttonLabel')} + ({ + ...data, + id: decodeId(data.id), + dataUpload: { ...data.dataUpload, error: JSON.parse(data.dataUpload.error) }, + })) || [], + individualDataUploadHistoryPageInfo: pageInfo(action.payload.data.individualDataUploadHistory), + errorIndividualDataUploadHistory: formatGraphQLError(action.payload), + }; + case ERROR(ACTION_TYPE.GET_INDIVIDUAL_UPLOAD_HISTORY): + return { + ...state, + fetchingIndividualDataUploadHistory: false, + errorIndividualDataUploadHistory: formatServerError(action.payload), + }; case REQUEST(ACTION_TYPE.MUTATION): return dispatchMutationReq(state, action); case ERROR(ACTION_TYPE.MUTATION): diff --git a/src/translations/en.json b/src/translations/en.json index f48d6b2..87190ef 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -35,7 +35,17 @@ "buttonLabel": "UPLOAD", "label": "Upload Individuals", "workflowPicker": "Workflow", - "cancel": "Cancel" + "cancel": "Cancel", + "uploadHistoryTable": { + "workflow": "Workflow", + "dateCreated": "Date Created", + "sourceType": "Source Type", + "sourceName": "Source Name", + "status": "Status", + "error": "Error", + "buttonLabel": "UPLOAD HISTORY", + "label": "Upload History Table" + } }, "enrollment": { "buttonLabel": "ENROLLMENT", @@ -52,9 +62,10 @@ "mutationLabel": "Enrollment has been confirmed", "numberOfSelectedIndividuals": "Total Number Of Selected Individuals", "totalNumberOfIndividuals": "Total Number of Individuals", - "numberOfIndividualsAssignedToProgramme": "Number of Individuals Assigned To Any Programme", - "numberOfIndividualsNotAssignedToProgramme": "Number of Individuals Unassigned to Any Program", - "numberOfIndividualsAssignedToSelectedProgramme": "Number Of Individuals Assigned to Selected Programme", + "numberOfIndividualsAssignedToProgramme": "Number of Individuals Already Assigned To Any Programme", + "numberOfIndividualsNotAssignedToProgramme": "Number of Individuals Without Assignment to Program", + "numberOfIndividualsAssignedToSelectedProgramme": "Number Of Individuals Already Assigned to Selected Programme", + "numberOfIndividualsToBeUploaded": "Number Of Individuals to be Uploaded", "confirmMessageDialog": "Are you sure you want to confirm the enrollment of the selected individuals into the {benefitPlanName} Programme?" }, "saveButton.tooltip.enabled": "Save changes", diff --git a/src/utils.js b/src/utils.js index 626d6a3..a98243e 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,3 +1,5 @@ +import { baseApiUrl } from '@openimis/fe-core'; + export function isBase64Encoded(str) { // Base64 encoded strings can only contain characters from [A-Za-z0-9+/=] const base64RegExp = /^[A-Za-z0-9+/=]+$/; @@ -7,3 +9,22 @@ export function isBase64Encoded(str) { export function isEmptyObject(obj) { return Object.keys(obj).length === 0; } + +export function downloadInvalidItems(uploadId) { + const url = new URL( + `${window.location.origin}${baseApiUrl}/individual/download_invalid_items/?upload_id=${uploadId}`, + ); + fetch(url) + .then((response) => response.blob()) + .then((blob) => { + const link = document.createElement('a'); + link.href = URL.createObjectURL(blob); + link.download = 'individuals_invalid_items.csv'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }) + .catch((error) => { + console.error('Export failed, reason: ', error); + }); +} From f9f3d6aa8c799efc28f2c562d30a3ae12e9fd0a9 Mon Sep 17 00:00:00 2001 From: sniedzielski <52816247+sniedzielski@users.noreply.github.com> Date: Tue, 27 Feb 2024 16:33:05 +0100 Subject: [PATCH 66/90] CM-729: added user upload and sort by date created DESC (#50) * CM-729: added individual upload history dialog * CM-729: added adjustments to the frontend * CM-729: added improvements for history dialog upload --- src/actions.js | 1 + .../dialogs/IndividualsHistoryUploadDialog.js | 18 +++++++++++++++--- src/translations/en.json | 3 ++- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/actions.js b/src/actions.js index 1eea91e..1410119 100644 --- a/src/actions.js +++ b/src/actions.js @@ -78,6 +78,7 @@ const UPLOAD_HISTORY_FULL_PROJECTION = () => [ 'uuid', 'workflow', 'dataUpload {uuid, dateCreated, dateUpdated, sourceName, sourceType, status, error }', + 'userCreated {username}', ]; export function fetchIndividualEnrollmentSummary(params) { diff --git a/src/components/dialogs/IndividualsHistoryUploadDialog.js b/src/components/dialogs/IndividualsHistoryUploadDialog.js index 79dffc9..262a73c 100644 --- a/src/components/dialogs/IndividualsHistoryUploadDialog.js +++ b/src/components/dialogs/IndividualsHistoryUploadDialog.js @@ -60,7 +60,9 @@ function IndividualsUploadHistoryDialog({ useEffect(() => { if (isOpen) { - const params = []; + const params = [ + 'orderBy: ["-dateCreated"]', + ]; fetchUploadHistory(params); } }, [isOpen]); @@ -85,8 +87,8 @@ function IndividualsUploadHistoryDialog({ top: '50%', left: '50%', transform: 'translate(-50%,-50%)', - width: '75%', - maxWidth: '75%', + width: '85%', + maxWidth: '85%', }, }} > @@ -141,6 +143,13 @@ function IndividualsUploadHistoryDialog({ 'individual.upload.uploadHistoryTable.status', )} + + {formatMessage( + intl, + 'individual', + 'individual.upload.uploadHistoryTable.userCreated', + )} + {formatMessage( intl, @@ -170,6 +179,9 @@ function IndividualsUploadHistoryDialog({ { item.dataUpload.status} + + {item.userCreated.username} + diff --git a/src/translations/en.json b/src/translations/en.json index 87190ef..a79d15b 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -44,7 +44,8 @@ "status": "Status", "error": "Error", "buttonLabel": "UPLOAD HISTORY", - "label": "Upload History Table" + "label": "Upload History Table", + "userCreated": "User Created" } }, "enrollment": { From d4df35742cb9a456c2abaa866690cad633e3a480 Mon Sep 17 00:00:00 2001 From: sniedzielski Date: Thu, 29 Feb 2024 17:02:04 +0100 Subject: [PATCH 67/90] CM-715: added benefits tab --- src/components/BenefitsTab.js | 44 +++++++++++++++++++++++++++++++++++ src/constants.js | 3 +++ src/index.js | 3 +++ src/translations/en.json | 3 +++ 4 files changed, 53 insertions(+) create mode 100644 src/components/BenefitsTab.js diff --git a/src/components/BenefitsTab.js b/src/components/BenefitsTab.js new file mode 100644 index 0000000..ac51352 --- /dev/null +++ b/src/components/BenefitsTab.js @@ -0,0 +1,44 @@ +import React from 'react'; +import { Tab } from '@material-ui/core'; +import { + formatMessage, PublishedComponent, +} from '@openimis/fe-core'; +import { BENEFITS_TAB_VALUE, BENEFITS_CONTRIBUTION_KEY } from '../constants'; + +function BenefitsTabLabel({ + intl, onChange, tabStyle, isSelected, individual, +}) { + if (!individual) return null; + return ( + + ); +} + +function BenefitsTabPanel({ + value, individual, rights, classes, +}) { + if (!individual) return null; + return ( + + + + ); +} + +export { BenefitsTabLabel, BenefitsTabPanel }; diff --git a/src/constants.js b/src/constants.js index fe598a1..13ff83b 100644 --- a/src/constants.js +++ b/src/constants.js @@ -28,12 +28,14 @@ export const INDIVIDUAL_CHANGELOG_TAB_VALUE = 'IndividualChangelogTab'; export const INDIVIDUAL_TASK_TAB_VALUE = 'IndividualTaskTab'; export const GROUP_CHANGELOG_TAB_VALUE = 'GroupChangelogTab'; export const GROUP_TASK_TAB_VALUE = 'GroupTaskTab'; +export const BENEFITS_TAB_VALUE = 'BenefitTaskTab'; export const INDIVIDUAL_TABS_LABEL_CONTRIBUTION_KEY = 'individual.TabPanel.label'; export const INDIVIDUAL_TABS_PANEL_CONTRIBUTION_KEY = 'individual.TabPanel.panel'; export const BENEFIT_PLAN_TABS_LABEL_CONTRIBUTION_KEY = 'individual.BenefitPlansListTabLabel'; export const BENEFIT_PLAN_TABS_PANEL_CONTRIBUTION_KEY = 'individual.BenefitPlansListTabPanel'; export const TASK_CONTRIBUTION_KEY = 'tasksManagement.tasks'; +export const BENEFITS_CONTRIBUTION_KEY = 'payroll.benefitConsumptionPayrollSearcher'; export const BENEFICIARY_STATUS = { POTENTIAL: 'POTENTIAL', @@ -54,6 +56,7 @@ export const GROUP_INDIVIDUAL_ROLES_LIST = [ export const BENEFIT_PLAN_LABEL = 'BenefitPlan'; export const INDIVIDUAL_LABEL = 'Individual'; export const GROUP_LABEL = 'Group'; +export const BENEFITS_LABEL = 'Benefits'; export const INDIVIDUAL_MODULE_NAME = 'individual'; diff --git a/src/index.js b/src/index.js index 27c95c9..9112400 100644 --- a/src/index.js +++ b/src/index.js @@ -39,6 +39,7 @@ import { import { GROUP_LABEL, INDIVIDUAL_LABEL } from './constants'; import { GroupCreateTaskItemFormatters, GroupCreateTaskTableHeaders } from './components/tasks/GroupCreateTasks'; import IndividualsUploadDialog from './components/dialogs/IndividualsUploadDialog'; +import { BenefitsTabLabel, BenefitsTabPanel } from './components/BenefitsTab'; const ROUTE_INDIVIDUALS = 'individuals'; const ROUTE_INDIVIDUAL = 'individuals/individual'; @@ -83,6 +84,7 @@ const DEFAULT_CONFIG = { GroupChangelogTabLabel, GroupTaskTabLabel, IndividalTaskTabLabel, + BenefitsTabLabel, ], 'individual.TabPanel.panel': [ IndividualsListTabPanel, @@ -91,6 +93,7 @@ const DEFAULT_CONFIG = { IndividalChangelogTabPanel, GroupTaskTabPanel, IndividalTaskTabPanel, + BenefitsTabPanel, ], 'individual.BenefitPlansListTabLabel': [BENEFIT_PLAN_TABS_LABEL_REF_KEY], 'individual.BenefitPlansListTabPanel': [BENEFIT_PLAN_TABS_PANEL_REF_KEY], diff --git a/src/translations/en.json b/src/translations/en.json index a79d15b..6cc59c3 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -20,6 +20,9 @@ "lastName": "Last Name", "dob": "Day of birth", "mandatoryFieldsEmptyError": "* These fields are required", + "benefits": { + "label": "Benefits" + }, "delete": { "confirm": { "title": "Delete {firstName} {lastName}?", From 98928b0d3b52cf413ef42f4dc35afa27e97c8bcd Mon Sep 17 00:00:00 2001 From: sniedzielski <52816247+sniedzielski@users.noreply.github.com> Date: Fri, 1 Mar 2024 16:27:57 +0100 Subject: [PATCH 68/90] CM-715: added benefits tab (#51) --- src/components/BenefitsTab.js | 44 +++++++++++++++++++++++++++++++++++ src/constants.js | 3 +++ src/index.js | 3 +++ src/translations/en.json | 3 +++ 4 files changed, 53 insertions(+) create mode 100644 src/components/BenefitsTab.js diff --git a/src/components/BenefitsTab.js b/src/components/BenefitsTab.js new file mode 100644 index 0000000..ac51352 --- /dev/null +++ b/src/components/BenefitsTab.js @@ -0,0 +1,44 @@ +import React from 'react'; +import { Tab } from '@material-ui/core'; +import { + formatMessage, PublishedComponent, +} from '@openimis/fe-core'; +import { BENEFITS_TAB_VALUE, BENEFITS_CONTRIBUTION_KEY } from '../constants'; + +function BenefitsTabLabel({ + intl, onChange, tabStyle, isSelected, individual, +}) { + if (!individual) return null; + return ( + + ); +} + +function BenefitsTabPanel({ + value, individual, rights, classes, +}) { + if (!individual) return null; + return ( + + + + ); +} + +export { BenefitsTabLabel, BenefitsTabPanel }; diff --git a/src/constants.js b/src/constants.js index fe598a1..13ff83b 100644 --- a/src/constants.js +++ b/src/constants.js @@ -28,12 +28,14 @@ export const INDIVIDUAL_CHANGELOG_TAB_VALUE = 'IndividualChangelogTab'; export const INDIVIDUAL_TASK_TAB_VALUE = 'IndividualTaskTab'; export const GROUP_CHANGELOG_TAB_VALUE = 'GroupChangelogTab'; export const GROUP_TASK_TAB_VALUE = 'GroupTaskTab'; +export const BENEFITS_TAB_VALUE = 'BenefitTaskTab'; export const INDIVIDUAL_TABS_LABEL_CONTRIBUTION_KEY = 'individual.TabPanel.label'; export const INDIVIDUAL_TABS_PANEL_CONTRIBUTION_KEY = 'individual.TabPanel.panel'; export const BENEFIT_PLAN_TABS_LABEL_CONTRIBUTION_KEY = 'individual.BenefitPlansListTabLabel'; export const BENEFIT_PLAN_TABS_PANEL_CONTRIBUTION_KEY = 'individual.BenefitPlansListTabPanel'; export const TASK_CONTRIBUTION_KEY = 'tasksManagement.tasks'; +export const BENEFITS_CONTRIBUTION_KEY = 'payroll.benefitConsumptionPayrollSearcher'; export const BENEFICIARY_STATUS = { POTENTIAL: 'POTENTIAL', @@ -54,6 +56,7 @@ export const GROUP_INDIVIDUAL_ROLES_LIST = [ export const BENEFIT_PLAN_LABEL = 'BenefitPlan'; export const INDIVIDUAL_LABEL = 'Individual'; export const GROUP_LABEL = 'Group'; +export const BENEFITS_LABEL = 'Benefits'; export const INDIVIDUAL_MODULE_NAME = 'individual'; diff --git a/src/index.js b/src/index.js index 27c95c9..9112400 100644 --- a/src/index.js +++ b/src/index.js @@ -39,6 +39,7 @@ import { import { GROUP_LABEL, INDIVIDUAL_LABEL } from './constants'; import { GroupCreateTaskItemFormatters, GroupCreateTaskTableHeaders } from './components/tasks/GroupCreateTasks'; import IndividualsUploadDialog from './components/dialogs/IndividualsUploadDialog'; +import { BenefitsTabLabel, BenefitsTabPanel } from './components/BenefitsTab'; const ROUTE_INDIVIDUALS = 'individuals'; const ROUTE_INDIVIDUAL = 'individuals/individual'; @@ -83,6 +84,7 @@ const DEFAULT_CONFIG = { GroupChangelogTabLabel, GroupTaskTabLabel, IndividalTaskTabLabel, + BenefitsTabLabel, ], 'individual.TabPanel.panel': [ IndividualsListTabPanel, @@ -91,6 +93,7 @@ const DEFAULT_CONFIG = { IndividalChangelogTabPanel, GroupTaskTabPanel, IndividalTaskTabPanel, + BenefitsTabPanel, ], 'individual.BenefitPlansListTabLabel': [BENEFIT_PLAN_TABS_LABEL_REF_KEY], 'individual.BenefitPlansListTabPanel': [BENEFIT_PLAN_TABS_PANEL_REF_KEY], diff --git a/src/translations/en.json b/src/translations/en.json index a79d15b..6cc59c3 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -20,6 +20,9 @@ "lastName": "Last Name", "dob": "Day of birth", "mandatoryFieldsEmptyError": "* These fields are required", + "benefits": { + "label": "Benefits" + }, "delete": { "confirm": { "title": "Delete {firstName} {lastName}?", From 8c0ba304a502f94ad9f9c2da5311bcafcb46cb98 Mon Sep 17 00:00:00 2001 From: sniedzielski Date: Mon, 4 Mar 2024 13:48:26 +0100 Subject: [PATCH 69/90] CM-738: added possibility to preview individual --- src/components/IndividualSearcher.js | 23 +++- .../dialogs/AdvancedCriteriaForm.js | 13 +- .../IndividualPreviewEnrollmentDialog.js | 120 ++++++++++++++++++ src/pages/IndividualsPage.js | 2 +- src/translations/en.json | 4 +- 5 files changed, 156 insertions(+), 6 deletions(-) create mode 100644 src/components/dialogs/IndividualPreviewEnrollmentDialog.js diff --git a/src/components/IndividualSearcher.js b/src/components/IndividualSearcher.js index d3ec6b7..76f15df 100644 --- a/src/components/IndividualSearcher.js +++ b/src/components/IndividualSearcher.js @@ -13,6 +13,7 @@ import { historyPush, downloadExport, CLEARED_STATE_FILTER, + decodeId, } from '@openimis/fe-core'; import { bindActionCreators } from 'redux'; import { connect, useDispatch } from 'react-redux'; @@ -69,6 +70,9 @@ function IndividualSearcher({ fieldsFromBfSchema, fetchingFieldsFromBfSchema, fetchedFieldsFromBfSchema, + isModalEnrollment, + advancedCriteria, + benefitPlanToEnroll, }) { const dispatch = useDispatch(); const [individualToDelete, setIndividualToDelete] = useState(null); @@ -172,7 +176,7 @@ function IndividualSearcher({ (individual) => individual.lastName, (individual) => (individual.dob ? formatDateFromISO(modulesManager, intl, individual.dob) : EMPTY_STRING), ]; - if (rights.includes(RIGHT_INDIVIDUAL_UPDATE)) { + if (rights.includes(RIGHT_INDIVIDUAL_UPDATE) && isModalEnrollment === false) { formatters.push((individual) => ( )); } - if (rights.includes(RIGHT_INDIVIDUAL_DELETE)) { + if (rights.includes(RIGHT_INDIVIDUAL_DELETE) && isModalEnrollment === false) { formatters.push((individual) => ( {failedExport && ( diff --git a/src/components/dialogs/AdvancedCriteriaForm.js b/src/components/dialogs/AdvancedCriteriaForm.js index 55a0722..9daba60 100644 --- a/src/components/dialogs/AdvancedCriteriaForm.js +++ b/src/components/dialogs/AdvancedCriteriaForm.js @@ -20,6 +20,7 @@ import AdvancedCriteriaRowValue from './AdvancedCriteriaRowValue'; import { CLEARED_STATE_FILTER, INDIVIDUAL } from '../../constants'; import { isBase64Encoded, isEmptyObject } from '../../utils'; import { confirmEnrollment, fetchIndividualEnrollmentSummary } from '../../actions'; +import IndividualPreviewEnrollmentDialog from './IndividualPreviewEnrollmentDialog'; const styles = (theme) => ({ item: theme.paper.item, @@ -48,12 +49,14 @@ function AdvancedCriteriaForm({ confirmed, clearConfirm, coreConfirm, + rights, }) { // eslint-disable-next-line no-unused-vars const [currentFilter, setCurrentFilter] = useState({ field: '', filter: '', type: '', value: '', amount: '', }); const [filters, setFilters] = useState(getDefaultAppliedCustomFilters()); + const [filtersToApply, setFiltersToApply] = useState(null); const createParams = (moduleName, objectTypeName, uuidOfObject = null, additionalParams = null) => { const params = [ @@ -119,6 +122,7 @@ function AdvancedCriteriaForm({ // Extract custom_filter_condition values and construct customFilters array const customFilters = advancedCriteria.map((criterion) => `"${criterion.custom_filter_condition}"`); + setFiltersToApply(customFilters); const params = [ `customFilters: [${customFilters}]`, `benefitPlanId: "${decodeId(object.id)}"`, @@ -172,6 +176,7 @@ function AdvancedCriteriaForm({ // Extract custom_filter_condition values and construct customFilters array const customFilters = advancedCriteria.map((criterion) => `"${criterion.custom_filter_condition}"`); + setFiltersToApply(customFilters); const params = { customFilters: `[${customFilters}]`, benefitPlanId: `"${decodeId(object.id)}"`, @@ -327,7 +332,7 @@ function AdvancedCriteriaForm({
- + + diff --git a/src/components/dialogs/IndividualPreviewEnrollmentDialog.js b/src/components/dialogs/IndividualPreviewEnrollmentDialog.js new file mode 100644 index 0000000..7b1d483 --- /dev/null +++ b/src/components/dialogs/IndividualPreviewEnrollmentDialog.js @@ -0,0 +1,120 @@ +/* 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 { INDIVIDUAL_MODULE_NAME } from '../../constants'; +import IndividualSearcher from '../IndividualSearcher'; + +function IndividualPreviewEnrollmentDialog({ + classes, + rights, + advancedCriteria, + benefitPlanToEnroll, +}) { + const [isOpen, setIsOpen] = useState(false); + + const handleOpen = () => { + setIsOpen(true); + }; + + const handleClose = () => { + setIsOpen(false); + }; + + const modulesManager = useModulesManager(); + const { formatMessage } = useTranslations(INDIVIDUAL_MODULE_NAME, modulesManager); + + return ( + <> + + + + {formatMessage('individual.enrollment.previewIndividuals')} + + +
+ +
+
+ +
+
+
+ +
+
+ +
+ + ); +} + +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)(IndividualPreviewEnrollmentDialog); diff --git a/src/pages/IndividualsPage.js b/src/pages/IndividualsPage.js index 44d93c1..aa6fd8f 100644 --- a/src/pages/IndividualsPage.js +++ b/src/pages/IndividualsPage.js @@ -18,7 +18,7 @@ function IndividualsPage(props) { rights.includes(RIGHT_INDIVIDUAL_SEARCH) && (
- +
) ); diff --git a/src/translations/en.json b/src/translations/en.json index 6cc59c3..96d2106 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -70,7 +70,9 @@ "numberOfIndividualsNotAssignedToProgramme": "Number of Individuals Without Assignment to Program", "numberOfIndividualsAssignedToSelectedProgramme": "Number Of Individuals Already Assigned to Selected Programme", "numberOfIndividualsToBeUploaded": "Number Of Individuals to be Uploaded", - "confirmMessageDialog": "Are you sure you want to confirm the enrollment of the selected individuals into the {benefitPlanName} Programme?" + "confirmMessageDialog": "Are you sure you want to confirm the enrollment of the selected individuals into the {benefitPlanName} Programme?", + "previewIndividuals": "Preview Individuals", + "close": "Close" }, "saveButton.tooltip.enabled": "Save changes", "saveButton.tooltip.disabled": "Please fill General Information fields first", From 8354ed2e40a4f063f0287f68ab4dd18cd6e957cd Mon Sep 17 00:00:00 2001 From: sniedzielski Date: Tue, 5 Mar 2024 16:14:31 +0100 Subject: [PATCH 70/90] CM-717: added group history tab --- src/actions.js | 16 +++ .../GroupIndividualHistoryFilter.js | 98 +++++++++++++++ .../GroupIndividualHistorySearcher.js | 115 ++++++++++++++++++ src/components/GroupIndividualHistoryTab.js | 39 ++++++ src/components/GroupTabPanel.js | 83 +++++++++++++ src/constants.js | 4 + src/index.js | 26 +++- src/pages/GroupPage.js | 4 +- src/reducer.js | 39 ++++++ src/translations/en.json | 14 +++ 10 files changed, 430 insertions(+), 8 deletions(-) create mode 100644 src/components/GroupIndividualHistoryFilter.js create mode 100644 src/components/GroupIndividualHistorySearcher.js create mode 100644 src/components/GroupIndividualHistoryTab.js create mode 100644 src/components/GroupTabPanel.js diff --git a/src/actions.js b/src/actions.js index 1410119..f2bc1ff 100644 --- a/src/actions.js +++ b/src/actions.js @@ -69,6 +69,17 @@ const GROUP_FULL_PROJECTION = [ 'userUpdated {username}', ]; +const GROUP_INDIVIDUAL_HISTORY_FULL_PROJECTION = [ + 'id', + 'individual {id, firstName, lastName, dob}', + 'group {id}', + 'role', + 'isDeleted', + 'dateCreated', + 'dateUpdated', + 'jsonExt', +]; + const GROUP_HISTORY_FULL_PROJECTION = GROUP_FULL_PROJECTION.filter( (item) => item !== 'head {firstName, lastName}', ); @@ -125,6 +136,11 @@ export function fetchGroupHistory(params) { return graphql(payload, ACTION_TYPE.SEARCH_GROUP_HISTORY); } +export function fetchGroupIndividualHistory(params) { + const payload = formatPageQueryWithCount('groupIndividualHistory', params, GROUP_INDIVIDUAL_HISTORY_FULL_PROJECTION); + return graphql(payload, ACTION_TYPE.SEARCH_GROUP_INDIVIDUAL_HISTORY); +} + export function fetchUploadHistory(params) { const payload = formatPageQueryWithCount('individualDataUploadHistory', params, UPLOAD_HISTORY_FULL_PROJECTION()); return graphql(payload, ACTION_TYPE.GET_INDIVIDUAL_UPLOAD_HISTORY); diff --git a/src/components/GroupIndividualHistoryFilter.js b/src/components/GroupIndividualHistoryFilter.js new file mode 100644 index 0000000..a16e5ba --- /dev/null +++ b/src/components/GroupIndividualHistoryFilter.js @@ -0,0 +1,98 @@ +import React, { useEffect } from 'react'; +import { injectIntl } from 'react-intl'; +import { TextInput, PublishedComponent, formatMessage } from '@openimis/fe-core'; +import { Grid } 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 { defaultFilterStyles } from '../util/styles'; +import GroupIndividualRolePicker from '../pickers/GroupIndividualRolePicker'; + +function GroupIndividualHistoryFilter({ + intl, classes, filters, onChangeFilters, groupId, +}) { + 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}"`, + }, + ]); + } + }; + + const handleGroupId = onChangeStringFilter('group_Id'); + useEffect(() => { + if (filters?.group_Id?.value !== groupId) { + handleGroupId(groupId); + } + }, [groupId]); + + return ( + + + + + + + + + onChangeFilters([ + { + id: 'individual_Dob', + value: v, + filter: `individual_Dob: "${v}"`, + }, + ])} + /> + + + onChangeFilters([ + { + id: 'role', + value, + filter: `role: ${value}`, + }, + ])} + /> + + + ); +} + +export default injectIntl(withTheme(withStyles(defaultFilterStyles)(GroupIndividualHistoryFilter))); diff --git a/src/components/GroupIndividualHistorySearcher.js b/src/components/GroupIndividualHistorySearcher.js new file mode 100644 index 0000000..e667a74 --- /dev/null +++ b/src/components/GroupIndividualHistorySearcher.js @@ -0,0 +1,115 @@ +import React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { injectIntl } from 'react-intl'; +import { + useModulesManager, + useTranslations, + Searcher, + withHistory, + withModulesManager, +} from '@openimis/fe-core'; +import { DEFAULT_PAGE_SIZE, EMPTY_STRING, ROWS_PER_PAGE_OPTIONS } from '../constants'; +import GroupIndividualHistoryFilter from './GroupIndividualHistoryFilter'; +import { fetchGroupIndividualHistory } from '../actions'; + +function GroupIndividualHistorySearcher({ + individualId, +}) { + const modulesManager = useModulesManager(); + const dispatch = useDispatch(); + const { formatDateFromISO, formatMessageWithValues } = useTranslations('individual', modulesManager); + + const fetchingGroupIndividualHistory = useSelector((state) => state.individual.fetchingGroupIndividualHistory); + const fetchedGroupIndividualHistory = useSelector((state) => state.individual.fetchedGroupIndividualHistory); + const errorGroupIndividualHistory = useSelector((state) => state.individual.errorGroupIndividualHistory); + const groupIndividualHistory = useSelector((state) => state.individual.groupIndividualHistory); + const groupIndividualHistoryPageInfo = useSelector((state) => state.individual.groupIndividualHistoryPageInfo); + const groupIndividualHistoryTotalCount = useSelector((state) => state.individual.groupIndividualHistoryTotalCount); + const fetch = (params) => (individualId ? dispatch(fetchGroupIndividualHistory(params)) : null); + + const headers = () => { + const headers = [ + 'individual.firstName', + 'individual.lastName', + 'individual.dob', + 'groupIndividual.individual.role', + 'emptyLabel', + ]; + return headers; + }; + + const itemFormatters = () => { + const formatters = [ + (groupIndividualHistory) => groupIndividualHistory.individual.firstName, + (groupIndividualHistory) => groupIndividualHistory.individual.lastName, + (groupIndividualHistory) => ( + groupIndividualHistory.individual.dob + ? formatDateFromISO(modulesManager, groupIndividualHistory.individual.dob) + : EMPTY_STRING + ), + (groupIndividualHistory) => groupIndividualHistory.role, + ]; + return formatters; + }; + + const rowIdentifier = (groupIndividualHistory) => groupIndividualHistory.id; + + const sorts = () => [ + ['id', true], + ['dateUpdated', true], + ['version', true], + ]; + + const defaultFilters = () => { + const filters = { + }; + if (individualId !== null && individualId !== undefined) { + filters.individual_Id = { + value: individualId, + filter: `individual_Id: "${individualId}"`, + }; + } + return filters; + }; + + const groupIndividualHistoryFilter = (props) => ( + + ); + + return ( +
+ +
+ ); +} + +export default withHistory( + withModulesManager(injectIntl((GroupIndividualHistorySearcher))), +); diff --git a/src/components/GroupIndividualHistoryTab.js b/src/components/GroupIndividualHistoryTab.js new file mode 100644 index 0000000..d1bdaea --- /dev/null +++ b/src/components/GroupIndividualHistoryTab.js @@ -0,0 +1,39 @@ +import React from 'react'; +import { Tab } from '@material-ui/core'; +import { formatMessage, PublishedComponent } from '@openimis/fe-core'; +import { GROUP_INDIVIDUAL_HISTORY_TAB_VALUE } from '../constants'; + +function GroupIndividualHistoryTabLabel({ + intl, onChange, tabStyle, isSelected, +}) { + return ( + + ); +} + +function GroupIndividualHistoryTabPanel({ + value, rights, individual, +}) { + return ( + + + + ); +} + +export { GroupIndividualHistoryTabLabel, GroupIndividualHistoryTabPanel }; diff --git a/src/components/GroupTabPanel.js b/src/components/GroupTabPanel.js new file mode 100644 index 0000000..ec992e9 --- /dev/null +++ b/src/components/GroupTabPanel.js @@ -0,0 +1,83 @@ +import React, { useState } from 'react'; +import { Paper, Grid } from '@material-ui/core'; +import { Contributions } from '@openimis/fe-core'; +import { injectIntl } from 'react-intl'; +import { withTheme, withStyles } from '@material-ui/core/styles'; +import { + BENEFIT_PLANS_LIST_TAB_VALUE, + GROUPS_TABS_LABEL_CONTRIBUTION_KEY, + GROUPS_TABS_PANEL_CONTRIBUTION_KEY, INDIVIDUALS_LIST_TAB_VALUE, +} from '../constants'; + +const styles = (theme) => ({ + paper: theme.paper.paper, + tableTitle: theme.table.title, + tabs: { + display: 'flex', + alignItems: 'center', + }, + selectedTab: { + borderBottom: '4px solid white', + }, + unselectedTab: { + borderBottom: '4px solid transparent', + }, + button: { + marginLeft: 'auto', + padding: theme.spacing(1), + fontSize: '0.875rem', + textTransform: 'none', + }, +}); + +function GroupTabPanel({ + intl, + rights, + classes, + individual, + setConfirmedAction, + group, editedGroupIndividual, + setEditedGroupIndividual, + groupIndividualIds, +}) { + const [activeTab, setActiveTab] = useState(individual ? BENEFIT_PLANS_LIST_TAB_VALUE : INDIVIDUALS_LIST_TAB_VALUE); + + const isSelected = (tab) => tab === activeTab; + + const tabStyle = (tab) => (isSelected(tab) ? classes.selectedTab : classes.unselectedTab); + + const handleChange = (_, tab) => setActiveTab(tab); + + return ( + + + + + + + ); +} + +export default injectIntl(withTheme(withStyles(styles)(GroupTabPanel))); diff --git a/src/constants.js b/src/constants.js index 13ff83b..487d2d2 100644 --- a/src/constants.js +++ b/src/constants.js @@ -27,11 +27,15 @@ export const INDIVIDUALS_LIST_TAB_VALUE = 'IndividualsListTab'; export const INDIVIDUAL_CHANGELOG_TAB_VALUE = 'IndividualChangelogTab'; export const INDIVIDUAL_TASK_TAB_VALUE = 'IndividualTaskTab'; export const GROUP_CHANGELOG_TAB_VALUE = 'GroupChangelogTab'; +export const GROUP_INDIVIDUAL_HISTORY_TAB_VALUE = 'GroupIndividualHistoryTab'; export const GROUP_TASK_TAB_VALUE = 'GroupTaskTab'; export const BENEFITS_TAB_VALUE = 'BenefitTaskTab'; export const INDIVIDUAL_TABS_LABEL_CONTRIBUTION_KEY = 'individual.TabPanel.label'; export const INDIVIDUAL_TABS_PANEL_CONTRIBUTION_KEY = 'individual.TabPanel.panel'; +export const GROUPS_TABS_LABEL_CONTRIBUTION_KEY = 'group.TabPanel.label'; +export const GROUPS_TABS_PANEL_CONTRIBUTION_KEY = 'group.TabPanel.panel'; + export const BENEFIT_PLAN_TABS_LABEL_CONTRIBUTION_KEY = 'individual.BenefitPlansListTabLabel'; export const BENEFIT_PLAN_TABS_PANEL_CONTRIBUTION_KEY = 'individual.BenefitPlansListTabPanel'; export const TASK_CONTRIBUTION_KEY = 'tasksManagement.tasks'; diff --git a/src/index.js b/src/index.js index 9112400..8b63641 100644 --- a/src/index.js +++ b/src/index.js @@ -40,6 +40,11 @@ import { GROUP_LABEL, INDIVIDUAL_LABEL } from './constants'; import { GroupCreateTaskItemFormatters, GroupCreateTaskTableHeaders } from './components/tasks/GroupCreateTasks'; import IndividualsUploadDialog from './components/dialogs/IndividualsUploadDialog'; import { BenefitsTabLabel, BenefitsTabPanel } from './components/BenefitsTab'; +import GroupIndividualHistorySearcher from './components/GroupIndividualHistorySearcher'; +import { + GroupIndividualHistoryTabLabel, + GroupIndividualHistoryTabPanel, +} from './components/GroupIndividualHistoryTab'; const ROUTE_INDIVIDUALS = 'individuals'; const ROUTE_INDIVIDUAL = 'individuals/individual'; @@ -75,25 +80,34 @@ const DEFAULT_CONFIG = { { key: 'individual.IndividualHistorySearcher', ref: IndividualHistorySearcher }, { key: 'individual.GroupHistorySearcher', ref: GroupHistorySearcher }, { key: 'individual.IndividualsUploadDialog', ref: IndividualsUploadDialog }, + { key: 'individual.GroupIndividualHistorySearcher', ref: GroupIndividualHistorySearcher }, ], 'individual.IndividualsUploadDialog': IndividualsUploadDialog, 'individual.TabPanel.label': [ - IndividualsListTabLabel, BenefitPlansListTabLabel, IndividalChangelogTabLabel, - GroupChangelogTabLabel, - GroupTaskTabLabel, IndividalTaskTabLabel, BenefitsTabLabel, + GroupIndividualHistoryTabLabel, ], 'individual.TabPanel.panel': [ - IndividualsListTabPanel, BenefitPlansListTabPanel, - GroupChangelogTabPanel, IndividalChangelogTabPanel, - GroupTaskTabPanel, IndividalTaskTabPanel, BenefitsTabPanel, + GroupIndividualHistoryTabPanel, + ], + 'group.TabPanel.label': [ + IndividualsListTabLabel, + BenefitPlansListTabLabel, + GroupChangelogTabLabel, + GroupTaskTabLabel, + ], + 'group.TabPanel.panel': [ + IndividualsListTabPanel, + BenefitPlansListTabPanel, + GroupChangelogTabPanel, + GroupTaskTabPanel, ], 'individual.BenefitPlansListTabLabel': [BENEFIT_PLAN_TABS_LABEL_REF_KEY], 'individual.BenefitPlansListTabPanel': [BENEFIT_PLAN_TABS_PANEL_REF_KEY], diff --git a/src/pages/GroupPage.js b/src/pages/GroupPage.js index a658be6..c9178b1 100644 --- a/src/pages/GroupPage.js +++ b/src/pages/GroupPage.js @@ -20,8 +20,8 @@ import { fetchGroup, deleteGroup, updateGroup, clearGroup, createGroupAndMoveIndividual, } from '../actions'; import GroupHeadPanel from '../components/GroupHeadPanel'; -import IndividualTabPanel from '../components/IndividualTabPanel'; import { ACTION_TYPE } from '../reducer'; +import GroupTabPanel from '../components/GroupTabPanel'; const styles = (theme) => ({ page: theme.page, @@ -168,7 +168,7 @@ function GroupPage({ canSave={canSave} save={groupUuid ? handleSave : null} HeadPanel={GroupHeadPanel} - Panels={[IndividualTabPanel]} + Panels={[GroupTabPanel]} rights={rights} actions={actions} setConfirmedAction={setConfirmedAction} diff --git a/src/reducer.js b/src/reducer.js index cae1739..304eee6 100644 --- a/src/reducer.js +++ b/src/reducer.js @@ -39,6 +39,7 @@ export const ACTION_TYPE = { ENROLLMENT_SUMMARY: 'ENROLLMENT_SUMMARY', CONFIRM_ENROLLMENT: 'CONFIRM_ENROLLMENT', GET_INDIVIDUAL_UPLOAD_HISTORY: 'GET_INDIVIDUAL_UPLOAD_HISTORY', + SEARCH_GROUP_INDIVIDUAL_HISTORY: 'SEARCH_GROUP_INDIVIDUAL_HISTORY', }; function reducer( @@ -114,6 +115,13 @@ function reducer( individualDataUploadHistory: [], individualDataUploadHistoryPageInfo: {}, errorIndividualDataUploadHistory: null, + + fetchingGroupIndividualHistory: false, + errorGroupIndividualHistory: null, + fetchedGroupIndividualHistory: false, + groupIndividualHistory: [], + groupIndividualHistoryPageInfo: {}, + groupIndividualHistoryTotalCount: 0, }, action, ) { @@ -533,6 +541,37 @@ function reducer( fetchingIndividualDataUploadHistory: false, errorIndividualDataUploadHistory: formatServerError(action.payload), }; + case REQUEST(ACTION_TYPE.SEARCH_GROUP_INDIVIDUAL_HISTORY): + return { + ...state, + fetchingGroupIndividualHistory: true, + fetchedGroupIndividualHistory: false, + groupIndividualHistory: null, + groupIndividualHistoryPageInfo: {}, + groupIndividualHistoryTotalCount: 0, + errorGroupIndividualHistory: null, + }; + case SUCCESS(ACTION_TYPE.SEARCH_GROUP_INDIVIDUAL_HISTORY): + return { + ...state, + fetchingGroupIndividualHistory: false, + fetchedGroupIndividualHistory: true, + // eslint-disable-next-line max-len + groupIndividualHistory: parseData(action.payload.data.groupIndividualHistory)?.map((groupIndividualHistory) => ({ + ...groupIndividualHistory, + id: decodeId(groupIndividualHistory.id), + })), + // eslint-disable-next-line max-len + groupIndividualHistoryTotalCount: action.payload.data.groupIndividualHistoryPageInfo ? action.payload.data.groupIndividualHistoryPageInfo.totalCount : null, + groupIndividualHistoryPageInfo: pageInfo(action.payload.data.groupIndividualHistoryPageInfo), + errorGroupIndividualHistory: formatGraphQLError(action.payload), + }; + case ERROR(ACTION_TYPE.SEARCH_GROUP_INDIVIDUAL_HISTORY): + return { + ...state, + fetchingGroupIndividualHistory: false, + errorGroupIndividualHistory: formatServerError(action.payload), + }; case REQUEST(ACTION_TYPE.MUTATION): return dispatchMutationReq(state, action); case ERROR(ACTION_TYPE.MUTATION): diff --git a/src/translations/en.json b/src/translations/en.json index 96d2106..b923e1d 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -201,6 +201,20 @@ "groupIndividualRolePicker.RECIPIENT": "RECIPIENT", "groupIndividualRolePicker": "Role" }, + "groupIndividualHistoryList": { + "searcherResultsTitle": "{groupIndividualHistoryTotalCount} Historical Records Found" + }, + "groupIndividualHistory": { + "individual": { + "role": "Role", + "groupId": "Group ID", + "individualId": "Individual ID" + }, + "label": "Group History", + "groupIndividualRolePicker.HEAD": "HEAD", + "groupIndividualRolePicker.RECIPIENT": "RECIPIENT", + "groupIndividualRolePicker": "Role" + }, "groupChangelog.label": "Change Log", "groupTasks.label": "Tasks", "groupPicker": { From cba0e82e81057dbde9881ce14a1a66cdcdf77bfc Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 6 Mar 2024 10:14:33 +0100 Subject: [PATCH 71/90] CM-737: make AppliedCriteriaRow public reference (#53) * CM-737: make AppliedCriteriaRow public reference * CM-737: get default criteria --------- Co-authored-by: Jan --- src/components/EnrollmentHeadPanel.js | 1 + src/components/dialogs/AdvancedCriteriaForm.js | 17 +++++++++++++++++ src/index.js | 2 ++ 3 files changed, 20 insertions(+) diff --git a/src/components/EnrollmentHeadPanel.js b/src/components/EnrollmentHeadPanel.js index 2baaf03..9db1b31 100644 --- a/src/components/EnrollmentHeadPanel.js +++ b/src/components/EnrollmentHeadPanel.js @@ -117,6 +117,7 @@ class EnrollmentHeadPanel extends FormPanel { updateAttributes={this.updateJsonExt} getDefaultAppliedCustomFilters={this.getDefaultAppliedCustomFilters} additionalParams={enrollment?.benefitPlan ? { benefitPlan: `${decodeId(enrollment.benefitPlan.id)}` } : null} + edited={this.props.edited} />
diff --git a/src/components/dialogs/AdvancedCriteriaForm.js b/src/components/dialogs/AdvancedCriteriaForm.js index 54f981c..984e0dd 100644 --- a/src/components/dialogs/AdvancedCriteriaForm.js +++ b/src/components/dialogs/AdvancedCriteriaForm.js @@ -50,6 +50,7 @@ function AdvancedCriteriaForm({ clearConfirm, coreConfirm, rights, + edited, }) { // eslint-disable-next-line no-unused-vars const [currentFilter, setCurrentFilter] = useState({ @@ -58,6 +59,22 @@ function AdvancedCriteriaForm({ const [filters, setFilters] = useState(getDefaultAppliedCustomFilters()); const [filtersToApply, setFiltersToApply] = useState(null); + const getBenefitPlanDefaultCriteria = () => { + const { jsonExt } = edited?.benefitPlan ?? {}; + try { + const jsonData = JSON.parse(jsonExt); + return jsonData.advanced_criteria || []; + } catch (error) { + return []; + } + }; + + useEffect(() => { + if (!getDefaultAppliedCustomFilters().length) { + setFilters(getBenefitPlanDefaultCriteria()); + } + }, [edited]); + const createParams = (moduleName, objectTypeName, uuidOfObject = null, additionalParams = null) => { const params = [ `moduleName: "${moduleName}"`, diff --git a/src/index.js b/src/index.js index 9112400..a9df257 100644 --- a/src/index.js +++ b/src/index.js @@ -40,6 +40,7 @@ import { GROUP_LABEL, INDIVIDUAL_LABEL } from './constants'; import { GroupCreateTaskItemFormatters, GroupCreateTaskTableHeaders } from './components/tasks/GroupCreateTasks'; import IndividualsUploadDialog from './components/dialogs/IndividualsUploadDialog'; import { BenefitsTabLabel, BenefitsTabPanel } from './components/BenefitsTab'; +import AdvancedCriteriaRowValue from './components/dialogs/AdvancedCriteriaRowValue'; const ROUTE_INDIVIDUALS = 'individuals'; const ROUTE_INDIVIDUAL = 'individuals/individual'; @@ -75,6 +76,7 @@ const DEFAULT_CONFIG = { { key: 'individual.IndividualHistorySearcher', ref: IndividualHistorySearcher }, { key: 'individual.GroupHistorySearcher', ref: GroupHistorySearcher }, { key: 'individual.IndividualsUploadDialog', ref: IndividualsUploadDialog }, + { key: 'individual.AdvancedCriteriaRowValue', ref: AdvancedCriteriaRowValue }, ], 'individual.IndividualsUploadDialog': IndividualsUploadDialog, 'individual.TabPanel.label': [ From 045586ef0fbf3eb8f99af50853cb2948b36cf3c7 Mon Sep 17 00:00:00 2001 From: sniedzielski Date: Wed, 6 Mar 2024 12:12:47 +0100 Subject: [PATCH 72/90] CM-717: added adjustments to the searcher of history group, added membership status --- src/actions.js | 1 + .../GroupIndividualHistoryFilter.js | 34 +++++-------------- .../GroupIndividualHistorySearcher.js | 23 +++++++------ src/reducer.js | 2 +- src/translations/en.json | 4 ++- 5 files changed, 26 insertions(+), 38 deletions(-) diff --git a/src/actions.js b/src/actions.js index f2bc1ff..4bad876 100644 --- a/src/actions.js +++ b/src/actions.js @@ -78,6 +78,7 @@ const GROUP_INDIVIDUAL_HISTORY_FULL_PROJECTION = [ 'dateCreated', 'dateUpdated', 'jsonExt', + 'version', ]; const GROUP_HISTORY_FULL_PROJECTION = GROUP_FULL_PROJECTION.filter( diff --git a/src/components/GroupIndividualHistoryFilter.js b/src/components/GroupIndividualHistoryFilter.js index a16e5ba..9b1d46f 100644 --- a/src/components/GroupIndividualHistoryFilter.js +++ b/src/components/GroupIndividualHistoryFilter.js @@ -7,6 +7,7 @@ import _debounce from 'lodash/debounce'; import { CONTAINS_LOOKUP, DEFAULT_DEBOUNCE_TIME, EMPTY_STRING } from '../constants'; import { defaultFilterStyles } from '../util/styles'; import GroupIndividualRolePicker from '../pickers/GroupIndividualRolePicker'; +import GroupPicker from '../pickers/GroupPicker'; function GroupIndividualHistoryFilter({ intl, classes, filters, onChangeFilters, groupId, @@ -47,32 +48,15 @@ function GroupIndividualHistoryFilter({ return ( - - - - - - - onChangeFilters([ + onChangeFilters([ { - id: 'individual_Dob', - value: v, - filter: `individual_Dob: "${v}"`, + id: 'group_Id', + value, + filter: `group_Id: "${value}"`, }, ])} /> diff --git a/src/components/GroupIndividualHistorySearcher.js b/src/components/GroupIndividualHistorySearcher.js index e667a74..c3fd415 100644 --- a/src/components/GroupIndividualHistorySearcher.js +++ b/src/components/GroupIndividualHistorySearcher.js @@ -2,6 +2,7 @@ import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { injectIntl } from 'react-intl'; import { + decodeId, useModulesManager, useTranslations, Searcher, @@ -29,25 +30,26 @@ function GroupIndividualHistorySearcher({ const headers = () => { const headers = [ - 'individual.firstName', - 'individual.lastName', - 'individual.dob', + 'group.id', + 'groupIndividual.dateJoined', 'groupIndividual.individual.role', - 'emptyLabel', + 'groupIndividual.statusOfMembership', ]; return headers; }; const itemFormatters = () => { const formatters = [ - (groupIndividualHistory) => groupIndividualHistory.individual.firstName, - (groupIndividualHistory) => groupIndividualHistory.individual.lastName, + (groupIndividualHistory) => decodeId(groupIndividualHistory.group.id), (groupIndividualHistory) => ( - groupIndividualHistory.individual.dob - ? formatDateFromISO(modulesManager, groupIndividualHistory.individual.dob) + groupIndividualHistory.dateCreated + ? formatDateFromISO(modulesManager, groupIndividualHistory.dateCreated) : EMPTY_STRING ), (groupIndividualHistory) => groupIndividualHistory.role, + (groupIndividualHistory) => ( + groupIndividualHistory.version === groupIndividualHistoryTotalCount ? 'Active' : 'Past' + ), ]; return formatters; }; @@ -56,8 +58,7 @@ function GroupIndividualHistorySearcher({ const sorts = () => [ ['id', true], - ['dateUpdated', true], - ['version', true], + ['dateCreated', true], ]; const defaultFilters = () => { @@ -100,7 +101,7 @@ function GroupIndividualHistorySearcher({ sorts={sorts} rowsPerPageOptions={ROWS_PER_PAGE_OPTIONS} defaultPageSize={DEFAULT_PAGE_SIZE} - defaultOrderBy="id" + defaultOrderBy="-version" rowIdentifier={rowIdentifier} defaultFilters={defaultFilters()} cacheFiltersKey="groupIndividualHistoryFilterCache" diff --git a/src/reducer.js b/src/reducer.js index 304eee6..291af1d 100644 --- a/src/reducer.js +++ b/src/reducer.js @@ -562,7 +562,7 @@ function reducer( id: decodeId(groupIndividualHistory.id), })), // eslint-disable-next-line max-len - groupIndividualHistoryTotalCount: action.payload.data.groupIndividualHistoryPageInfo ? action.payload.data.groupIndividualHistoryPageInfo.totalCount : null, + groupIndividualHistoryTotalCount: action.payload.data.groupIndividualHistory ? action.payload.data.groupIndividualHistory.totalCount : null, groupIndividualHistoryPageInfo: pageInfo(action.payload.data.groupIndividualHistoryPageInfo), errorGroupIndividualHistory: formatGraphQLError(action.payload), }; diff --git a/src/translations/en.json b/src/translations/en.json index b923e1d..08c7cab 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -199,7 +199,9 @@ }, "groupIndividualRolePicker.HEAD": "HEAD", "groupIndividualRolePicker.RECIPIENT": "RECIPIENT", - "groupIndividualRolePicker": "Role" + "groupIndividualRolePicker": "Role", + "dateJoined": "Joining Date", + "statusOfMembership": "Status Of Membership" }, "groupIndividualHistoryList": { "searcherResultsTitle": "{groupIndividualHistoryTotalCount} Historical Records Found" From 525775262b58327f9f0d94b6fe1345019730a572 Mon Sep 17 00:00:00 2001 From: sniedzielski Date: Wed, 6 Mar 2024 12:24:44 +0100 Subject: [PATCH 73/90] CM-717: fixed linter --- src/components/GroupIndividualHistoryFilter.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/GroupIndividualHistoryFilter.js b/src/components/GroupIndividualHistoryFilter.js index 9b1d46f..ff3cae2 100644 --- a/src/components/GroupIndividualHistoryFilter.js +++ b/src/components/GroupIndividualHistoryFilter.js @@ -1,10 +1,10 @@ import React, { useEffect } from 'react'; import { injectIntl } from 'react-intl'; -import { TextInput, PublishedComponent, formatMessage } from '@openimis/fe-core'; +import { formatMessage } from '@openimis/fe-core'; import { Grid } 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 { DEFAULT_DEBOUNCE_TIME } from '../constants'; import { defaultFilterStyles } from '../util/styles'; import GroupIndividualRolePicker from '../pickers/GroupIndividualRolePicker'; import GroupPicker from '../pickers/GroupPicker'; @@ -16,8 +16,6 @@ function GroupIndividualHistoryFilter({ const filterValue = (filterName) => filters?.[filterName]?.value; - const filterTextFieldValue = (filterName) => filters?.[filterName]?.value ?? EMPTY_STRING; - const onChangeStringFilter = (filterName, lookup = null) => (value) => { if (lookup) { debouncedOnChangeFilters([ From fc6a8cb70a4807f64796809e9dcd349f2bfecf77 Mon Sep 17 00:00:00 2001 From: sniedzielski <52816247+sniedzielski@users.noreply.github.com> Date: Thu, 7 Mar 2024 15:43:36 +0100 Subject: [PATCH 74/90] CM-317: fixing routing for individual in group individual list (#55) --- src/components/GroupIndividualSearcher.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/GroupIndividualSearcher.js b/src/components/GroupIndividualSearcher.js index 976b4d4..3aac596 100644 --- a/src/components/GroupIndividualSearcher.js +++ b/src/components/GroupIndividualSearcher.js @@ -90,7 +90,8 @@ function GroupIndividualSearcher({ const onDoubleClick = (groupIndividual, newTab = false) => rights.includes(RIGHT_GROUP_INDIVIDUAL_UPDATE) && !deletedGroupIndividualUuids.includes(groupIndividual.id) - && historyPush(modulesManager, history, 'individual.route.individual', [groupIndividual?.id], newTab); + // eslint-disable-next-line max-len + && historyPush(modulesManager, history, 'individual.route.individual', [groupIndividual?.individual?.id], newTab); const onDelete = (groupIndividual) => setGroupIndividualToDelete(groupIndividual); From ba7b9460cb4d35e3996d4bfc166c38bcd9805678 Mon Sep 17 00:00:00 2001 From: Jan Date: Fri, 8 Mar 2024 11:43:47 +0100 Subject: [PATCH 75/90] CM-700: download upload file (#56) Co-authored-by: Jan --- .../dialogs/IndividualsHistoryUploadDialog.js | 40 ++++++++++++++++--- .../dialogs/IndividualsUploadDialog.js | 16 ++++++-- src/translations/en.json | 6 ++- src/utils.js | 24 ++++++++--- 4 files changed, 69 insertions(+), 17 deletions(-) diff --git a/src/components/dialogs/IndividualsHistoryUploadDialog.js b/src/components/dialogs/IndividualsHistoryUploadDialog.js index 262a73c..4d90950 100644 --- a/src/components/dialogs/IndividualsHistoryUploadDialog.js +++ b/src/components/dialogs/IndividualsHistoryUploadDialog.js @@ -7,7 +7,7 @@ import DialogContent from '@material-ui/core/DialogContent'; import DialogTitle from '@material-ui/core/DialogTitle'; import { formatMessage, - formatDateFromISO, + formatDateTimeFromISO, ProgressOrError, withModulesManager, } from '@openimis/fe-core'; @@ -27,7 +27,7 @@ import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import CollapsableErrorList from '../CollapsableErrorList'; import { fetchUploadHistory } from '../../actions'; -import { downloadInvalidItems } from '../../utils'; +import {downloadIndividualUploadFile, downloadInvalidItems} from '../../utils'; import { UPLOAD_STATUS } from '../../constants'; const styles = (theme) => ({ @@ -58,6 +58,10 @@ function IndividualsUploadHistoryDialog({ downloadInvalidItems(uploadId); }; + const downloadFile = (filename) => { + downloadIndividualUploadFile(filename); + }; + useEffect(() => { if (isOpen) { const params = [ @@ -168,7 +172,7 @@ function IndividualsUploadHistoryDialog({ { item.workflow } - { formatDateFromISO(modulesManager, intl, item.dataUpload.dateCreated) } + { formatDateTimeFromISO(modulesManager, intl, item.dataUpload.dateCreated) } { item.dataUpload.sourceType} @@ -188,7 +192,8 @@ function IndividualsUploadHistoryDialog({ {[ UPLOAD_STATUS.WAITING_FOR_VERIFICATION, - UPLOAD_STATUS.PARTIAL_SUCCESS].includes(item.dataUpload.status) && ( + UPLOAD_STATUS.PARTIAL_SUCCESS, + ].includes(item.dataUpload.status) ? ( - )} + ) : ( +
// Render a blank placeholder + )} + + + ))} diff --git a/src/components/dialogs/IndividualsUploadDialog.js b/src/components/dialogs/IndividualsUploadDialog.js index 8751db2..684d3c8 100644 --- a/src/components/dialogs/IndividualsUploadDialog.js +++ b/src/components/dialogs/IndividualsUploadDialog.js @@ -11,6 +11,7 @@ import { baseApiUrl, useModulesManager, formatMessage, + coreAlert, } from '@openimis/fe-core'; import { withTheme, withStyles } from '@material-ui/core/styles'; import { connect } from 'react-redux'; @@ -18,6 +19,7 @@ import { bindActionCreators } from 'redux'; import WorkflowsPicker from '../../pickers/WorkflowsPicker'; import { fetchWorkflows } from '../../actions'; import IndividualsHistoryUploadDialog from './IndividualsHistoryUploadDialog'; +import { EMPTY_STRING } from '../../constants'; const styles = (theme) => ({ item: theme.paper.item, @@ -27,6 +29,7 @@ function IndividualsUploadDialog({ intl, workflows, fetchWorkflows, + coreAlert, }) { const modulesManager = useModulesManager(); const [isOpen, setIsOpen] = useState(false); @@ -78,13 +81,17 @@ function IndividualsUploadDialog({ credentials: 'same-origin', }); - await response.json(); - - if (response.status >= 400) { + if (response.ok) { handleClose(); return; } - handleClose(); + + const errorHeader = formatMessage(intl, 'individual', 'individual.upload.alert.header'); + const errorMessage = response.status === 409 + ? formatMessage(intl, 'individual', 'individual.upload.alert.sameFileName') + : EMPTY_STRING; + + coreAlert(errorHeader, errorMessage); } catch (error) { handleClose(); } @@ -209,6 +216,7 @@ const mapStateToProps = (state) => ({ const mapDispatchToProps = (dispatch) => bindActionCreators({ fetchWorkflows, + coreAlert, }, dispatch); export default injectIntl( diff --git a/src/translations/en.json b/src/translations/en.json index 08c7cab..d5cacd3 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -37,6 +37,8 @@ "upload": { "buttonLabel": "UPLOAD", "label": "Upload Individuals", + "alert.sameFileName": "Filename exists for given Benefit Plan. Please change the filename.", + "alert.header": "Error during file upload.", "workflowPicker": "Workflow", "cancel": "Cancel", "uploadHistoryTable": { @@ -48,7 +50,9 @@ "error": "Error", "buttonLabel": "UPLOAD HISTORY", "label": "Upload History Table", - "userCreated": "User Created" + "userCreated": "User Created", + "downloadInvalidItems": "Download Invalid Items", + "downloadUploadFile": "Download Upload File" } }, "enrollment": { diff --git a/src/utils.js b/src/utils.js index a98243e..1e92b76 100644 --- a/src/utils.js +++ b/src/utils.js @@ -10,21 +10,33 @@ export function isEmptyObject(obj) { return Object.keys(obj).length === 0; } -export function downloadInvalidItems(uploadId) { - const url = new URL( - `${window.location.origin}${baseApiUrl}/individual/download_invalid_items/?upload_id=${uploadId}`, - ); +function downloadFile(url, filename) { fetch(url) .then((response) => response.blob()) .then((blob) => { const link = document.createElement('a'); link.href = URL.createObjectURL(blob); - link.download = 'individuals_invalid_items.csv'; + link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); }) .catch((error) => { - console.error('Export failed, reason: ', error); + // eslint-disable-next-line no-console + console.error('Download failed, reason: ', error); }); } + +export function downloadInvalidItems(uploadId) { + const baseUrl = new URL(`${window.location.origin}${baseApiUrl}/individual/download_invalid_items/`); + const queryParams = new URLSearchParams({ upload_id: uploadId }); + const url = `${baseUrl}?${queryParams.toString()}`; + downloadFile(url, 'individuals_invalid_items.csv'); +} + +export function downloadIndividualUploadFile(filename) { + const baseUrl = `${window.location.origin}${baseApiUrl}/individual/download_individual_upload_file/`; + const queryParams = new URLSearchParams({ filename }); + const url = `${baseUrl}?${queryParams.toString()}`; + downloadFile(url, filename); +} From 9d9bf280a2cc0bed1773ac585fece531a3e049e4 Mon Sep 17 00:00:00 2001 From: Jan Date: Fri, 8 Mar 2024 16:28:31 +0100 Subject: [PATCH 76/90] CM-700: download upload file (#57) * CM-700: download upload file * CM-700: fix translation --------- Co-authored-by: Jan --- src/components/dialogs/IndividualsHistoryUploadDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/dialogs/IndividualsHistoryUploadDialog.js b/src/components/dialogs/IndividualsHistoryUploadDialog.js index 4d90950..b52d44b 100644 --- a/src/components/dialogs/IndividualsHistoryUploadDialog.js +++ b/src/components/dialogs/IndividualsHistoryUploadDialog.js @@ -226,7 +226,7 @@ function IndividualsUploadHistoryDialog({ {formatMessage( intl, 'individual', - 'individual.upload.uploadHistoryTable.uploadHistoryTable.downloadUploadFile', + 'individual.upload.uploadHistoryTable.downloadUploadFile', )} From 21020d283fff305b0fda0858cbbc7ef7f1fccb73 Mon Sep 17 00:00:00 2001 From: sniedzielski <52816247+sniedzielski@users.noreply.github.com> Date: Tue, 12 Mar 2024 10:57:58 +0100 Subject: [PATCH 77/90] CM-726: added changes in picker workflow (#58) * CM-726: added changes in picker workflow * CM-726: added changes in picker workflow 2 --- src/pickers/WorkflowsPicker.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/pickers/WorkflowsPicker.js b/src/pickers/WorkflowsPicker.js index 43d80d6..df65a5b 100644 --- a/src/pickers/WorkflowsPicker.js +++ b/src/pickers/WorkflowsPicker.js @@ -14,10 +14,12 @@ function WorkflowsPicker({ withLabel = true, }) { const options = Array.isArray(workflows) && workflows !== undefined ? [ - ...workflows.map((workflows) => ({ - value: { name: workflows.name, group: workflows.group }, - label: workflows.name, - })), + ...workflows + .filter((workflow) => workflow.group === 'individual' && !workflow.name.includes('Valid')) + .map((workflow) => ({ + value: { name: workflow.name, group: workflow.group }, + label: workflow.name, + })), ] : []; useEffect(() => { From 6e900faa620caae03794ced34244f004d690f1d1 Mon Sep 17 00:00:00 2001 From: sniedzielski <52816247+sniedzielski@users.noreply.github.com> Date: Tue, 12 Mar 2024 11:36:28 +0100 Subject: [PATCH 78/90] CM-726: Added filters for workflow (#59) * CM-726: added changes in picker workflow * CM-726: added changes in picker workflow 2 * CM-726: added adjustments related to backend gql for workflow --- src/actions.js | 2 +- src/pickers/WorkflowsPicker.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/actions.js b/src/actions.js index 4bad876..09b874a 100644 --- a/src/actions.js +++ b/src/actions.js @@ -28,7 +28,7 @@ const ENROLLMENT_SUMMARY_FULL_PROJECTION = () => [ export function fetchWorkflows() { const payload = formatQuery( 'workflow', - [], + ['group: "individual"'], WORKFLOWS_FULL_PROJECTION(), ); return graphql(payload, ACTION_TYPE.GET_WORKFLOWS); diff --git a/src/pickers/WorkflowsPicker.js b/src/pickers/WorkflowsPicker.js index df65a5b..6af9dea 100644 --- a/src/pickers/WorkflowsPicker.js +++ b/src/pickers/WorkflowsPicker.js @@ -15,7 +15,7 @@ function WorkflowsPicker({ }) { const options = Array.isArray(workflows) && workflows !== undefined ? [ ...workflows - .filter((workflow) => workflow.group === 'individual' && !workflow.name.includes('Valid')) + .filter((workflow) => !workflow.name.includes('Valid')) .map((workflow) => ({ value: { name: workflow.name, group: workflow.group }, label: workflow.name, From d9d36b6f6711a46f48a001e2238f99734afe2d66 Mon Sep 17 00:00:00 2001 From: sniedzielski <52816247+sniedzielski@users.noreply.github.com> Date: Mon, 18 Mar 2024 14:19:25 +0100 Subject: [PATCH 79/90] CM-778: added possibility to display extra fields of individual (#60) * CM-778: added possibility to display extra fields of individual * CM-778: removed console.log * CM-778: fixed commits * CM-778: added default filter in historical searcher --- src/components/IndividualHeadPanel.js | 6 + src/components/IndividualHistorySearcher.js | 2 +- .../dialogs/AdditionalFieldsDialog.js | 130 ++++++++++++++++++ src/translations/en.json | 5 + 4 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 src/components/dialogs/AdditionalFieldsDialog.js diff --git a/src/components/IndividualHeadPanel.js b/src/components/IndividualHeadPanel.js index 638c191..2fc08f6 100644 --- a/src/components/IndividualHeadPanel.js +++ b/src/components/IndividualHeadPanel.js @@ -9,6 +9,7 @@ import { } from '@openimis/fe-core'; import { injectIntl } from 'react-intl'; import { withTheme, withStyles } from '@material-ui/core/styles'; +import AdditionalFieldsDialog from './dialogs/AdditionalFieldsDialog'; const styles = (theme) => ({ tableTitle: theme.table.title, @@ -83,6 +84,11 @@ class IndividualHeadPanel extends FormPanel { maxDate={currentDate} /> + + + ); diff --git a/src/components/IndividualHistorySearcher.js b/src/components/IndividualHistorySearcher.js index 45ba79e..a7ac14e 100644 --- a/src/components/IndividualHistorySearcher.js +++ b/src/components/IndividualHistorySearcher.js @@ -105,7 +105,7 @@ function IndividualHistorySearcher({ sorts={sorts} rowsPerPageOptions={ROWS_PER_PAGE_OPTIONS} defaultPageSize={DEFAULT_PAGE_SIZE} - defaultOrderBy="lastName" + defaultOrderBy="-version" rowIdentifier={rowIdentifier} defaultFilters={defaultFilters()} cacheFiltersKey="individualHistoryFilterChache" diff --git a/src/components/dialogs/AdditionalFieldsDialog.js b/src/components/dialogs/AdditionalFieldsDialog.js new file mode 100644 index 0000000..9a78c7b --- /dev/null +++ b/src/components/dialogs/AdditionalFieldsDialog.js @@ -0,0 +1,130 @@ +import React, { useState } from 'react'; +import { Grid } from '@material-ui/core'; +import { injectIntl } from 'react-intl'; +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 { + formatMessage, + renderInputComponent, + createFieldsBasedOnJSON, +} from '@openimis/fe-core'; +import { withTheme, withStyles } from '@material-ui/core/styles'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import { INDIVIDUAL_MODULE_NAME } from '../../constants'; + +const styles = (theme) => ({ + item: theme.paper.item, +}); + +function AdditionalFieldsDialog({ + intl, + classes, + individualJsonExt, +}) { + if (!individualJsonExt) return null; + const [isOpen, setIsOpen] = useState(false); + + const handleOpen = () => { + setIsOpen(true); + }; + + const handleClose = () => { + setIsOpen(false); + }; + const jsonExtFields = createFieldsBasedOnJSON(individualJsonExt); + + return ( + <> + + + + + {formatMessage(intl, 'individual', 'individual.additonalFields.label')} + + +
+ + {jsonExtFields?.map((jsonExtField) => ( + + {renderInputComponent(INDIVIDUAL_MODULE_NAME, jsonExtField)} + + ))} + +
+
+ +
+
+ +
+
+
+ + +
+ + ); +} + +const mapStateToProps = (state) => ({ + rights: !!state.core && !!state.core.user && !!state.core.user.i_user ? state.core.user.i_user.rights : [], +}); + +const mapDispatchToProps = (dispatch) => bindActionCreators({ +}, dispatch); + +export default injectIntl( + withTheme( + withStyles(styles)( + connect(mapStateToProps, mapDispatchToProps)(AdditionalFieldsDialog), + ), + ), +); diff --git a/src/translations/en.json b/src/translations/en.json index d5cacd3..9b816fd 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -19,6 +19,11 @@ "firstName": "First Name", "lastName": "Last Name", "dob": "Day of birth", + "additonalFields": { + "label": "Additional Fields", + "showAdditionalFields": "Show Additional Fields", + "close": "Close" + }, "mandatoryFieldsEmptyError": "* These fields are required", "benefits": { "label": "Benefits" From af1edd273d1b0436821433ba0613fcdddec173eb Mon Sep 17 00:00:00 2001 From: sniedzielski <52816247+sniedzielski@users.noreply.github.com> Date: Mon, 18 Mar 2024 15:59:32 +0100 Subject: [PATCH 80/90] fixed max height of modal (#61) --- src/components/dialogs/AdditionalFieldsDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/dialogs/AdditionalFieldsDialog.js b/src/components/dialogs/AdditionalFieldsDialog.js index 9a78c7b..88aa305 100644 --- a/src/components/dialogs/AdditionalFieldsDialog.js +++ b/src/components/dialogs/AdditionalFieldsDialog.js @@ -58,7 +58,7 @@ function AdditionalFieldsDialog({ style: { width: 1200, maxWidth: 1200, - maxHeight: 1200, + maxHeight: 900, }, }} > From a7905c5e5fb9ca45a756ccfe6b6f3f5f1be03545 Mon Sep 17 00:00:00 2001 From: sniedzielski <52816247+sniedzielski@users.noreply.github.com> Date: Wed, 20 Mar 2024 16:35:01 +0100 Subject: [PATCH 81/90] hothfix: additional fields in historical searcher (#63) --- src/components/IndividualHistorySearcher.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/IndividualHistorySearcher.js b/src/components/IndividualHistorySearcher.js index a7ac14e..3adf725 100644 --- a/src/components/IndividualHistorySearcher.js +++ b/src/components/IndividualHistorySearcher.js @@ -12,6 +12,7 @@ import { connect } from 'react-redux'; import { fetchIndividualHistory } from '../actions'; import { DEFAULT_PAGE_SIZE, EMPTY_STRING, ROWS_PER_PAGE_OPTIONS } from '../constants'; import IndividualHistoryFilter from './IndividualHistoryFilter'; +import AdditionalFieldsDialog from './dialogs/AdditionalFieldsDialog'; function IndividualHistorySearcher({ intl, @@ -47,7 +48,11 @@ function IndividualHistorySearcher({ ? formatDateFromISO(modulesManager, intl, individualHistory.dateUpdated) : EMPTY_STRING ), (individualHistory) => individualHistory.version, - (individualHistory) => individualHistory.jsonExt, + (individualHistory) => ( + + ), (individualHistory) => individualHistory?.userUpdated?.username, ]; From 8ae54b25250b7cc777bd7b3c0b9307d0efdc4931 Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 21 Mar 2024 15:46:38 +0100 Subject: [PATCH 82/90] CM-847: fix droup id passing via prop (#64) * CM-847: fix droup id passing via prop * CM-847: fix eslint --------- Co-authored-by: Jan --- src/components/GroupIndividualSearcher.js | 33 +++++++++---------- src/components/GroupTabPanel.js | 3 ++ src/components/IndividualsListTab.js | 4 +-- .../dialogs/IndividualsHistoryUploadDialog.js | 2 +- src/pages/GroupPage.js | 1 + 5 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/components/GroupIndividualSearcher.js b/src/components/GroupIndividualSearcher.js index 3aac596..965f725 100644 --- a/src/components/GroupIndividualSearcher.js +++ b/src/components/GroupIndividualSearcher.js @@ -22,7 +22,8 @@ import EditIcon from '@material-ui/icons/Edit'; import GroupIcon from '@material-ui/icons/Group'; import DeleteIcon from '@material-ui/icons/Delete'; import { - clearGroupIndividualExport, clearGroupIndividuals, + clearGroupIndividualExport, + clearGroupIndividuals, deleteGroupIndividual, downloadGroupIndividuals, fetchGroupIndividuals, @@ -30,7 +31,8 @@ import { } from '../actions'; import { DEFAULT_PAGE_SIZE, - EMPTY_STRING, GROUP_INDIVIDUAL_ROLES, + EMPTY_STRING, + GROUP_INDIVIDUAL_ROLES, RIGHT_GROUP_INDIVIDUAL_DELETE, RIGHT_GROUP_INDIVIDUAL_UPDATE, ROWS_PER_PAGE_OPTIONS, @@ -126,7 +128,7 @@ function GroupIndividualSearcher({ useEffect(() => () => (editedGroupIndividual ? clearGroupIndividuals() : null), [groupId]); - const fetch = (params) => (groupId ? fetchGroupIndividuals(params) : null); + const fetch = (params) => fetchGroupIndividuals(params); const headers = () => { const headers = [ @@ -282,21 +284,16 @@ function GroupIndividualSearcher({ } }, [groupIndividualExport]); - const defaultFilters = () => { - const filters = { - isDeleted: { - value: false, - filter: 'isDeleted: false', - }, - }; - if (groupId !== null && groupId !== undefined) { - filters.groupId = { - value: groupId, - filter: `group_Id: "${groupId}"`, - }; - } - return filters; - }; + const defaultFilters = () => ({ + isDeleted: { + value: false, + filter: 'isDeleted: false', + }, + group_Id: { + value: groupId, + filter: `group_Id: "${groupId}"`, + }, + }); const groupBeneficiaryFilter = (props) => ( ({ diff --git a/src/pages/GroupPage.js b/src/pages/GroupPage.js index c9178b1..ce7a4a2 100644 --- a/src/pages/GroupPage.js +++ b/src/pages/GroupPage.js @@ -178,6 +178,7 @@ function GroupPage({ editedGroupIndividual={editedGroupIndividual} readOnly={readOnly} groupIndividualIds={groupIndividualIds} + groupId={groupUuid} />
) From ebcc8555c5ca1f1cec58f5cb9ed7d72decfda0f9 Mon Sep 17 00:00:00 2001 From: sniedzielski <52816247+sniedzielski@users.noreply.github.com> Date: Thu, 21 Mar 2024 17:19:58 +0100 Subject: [PATCH 83/90] CM-741: fixed height of history upload modal (#65) --- src/components/dialogs/IndividualsHistoryUploadDialog.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/dialogs/IndividualsHistoryUploadDialog.js b/src/components/dialogs/IndividualsHistoryUploadDialog.js index f33f7f4..2974c02 100644 --- a/src/components/dialogs/IndividualsHistoryUploadDialog.js +++ b/src/components/dialogs/IndividualsHistoryUploadDialog.js @@ -93,6 +93,7 @@ function IndividualsUploadHistoryDialog({ transform: 'translate(-50%,-50%)', width: '85%', maxWidth: '85%', + maxHeight: '75%', }, }} > From 419ae4ff90bd76e390a8b6dfdad13ff08006155c Mon Sep 17 00:00:00 2001 From: Jan Date: Fri, 22 Mar 2024 16:50:27 +0100 Subject: [PATCH 84/90] CM-742: update history upload frontend (#62) Co-authored-by: Jan --- src/translations/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/translations/en.json b/src/translations/en.json index 9b816fd..989ab31 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -52,7 +52,7 @@ "sourceType": "Source Type", "sourceName": "Source Name", "status": "Status", - "error": "Error", + "error": "Workflow Errors", "buttonLabel": "UPLOAD HISTORY", "label": "Upload History Table", "userCreated": "User Created", From 571637d5a8a4b379d8ab8116582b4b17d1cf2cae Mon Sep 17 00:00:00 2001 From: olewandowski1 Date: Tue, 26 Mar 2024 15:42:44 +0100 Subject: [PATCH 85/90] OP-1486: fix handling failed export --- src/components/GroupIndividualSearcher.js | 18 +++++++++++++----- src/components/GroupSearcher.js | 17 +++++++++++++---- src/components/IndividualSearcher.js | 17 +++++++++++++---- 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/src/components/GroupIndividualSearcher.js b/src/components/GroupIndividualSearcher.js index 3aac596..80c2376 100644 --- a/src/components/GroupIndividualSearcher.js +++ b/src/components/GroupIndividualSearcher.js @@ -16,7 +16,7 @@ import { import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import { - Button, Dialog, DialogActions, DialogTitle, IconButton, Tooltip, + Button, Dialog, DialogActions, DialogTitle, IconButton, Tooltip, DialogContent, } from '@material-ui/core'; import EditIcon from '@material-ui/icons/Edit'; import GroupIcon from '@material-ui/icons/Group'; @@ -269,7 +269,9 @@ function GroupIndividualSearcher({ const [failedExport, setFailedExport] = useState(false); useEffect(() => { - setFailedExport(true); + if (errorGroupIndividualExport) { + setFailedExport(true); + } }, [errorGroupIndividualExport]); useEffect(() => { @@ -280,6 +282,8 @@ function GroupIndividualSearcher({ )(); clearGroupIndividualExport(); } + + return setFailedExport(false); }, [groupIndividualExport]); const defaultFilters = () => { @@ -362,10 +366,14 @@ function GroupIndividualSearcher({ cacheFiltersKey="groupIndividualsFilterCache" /> {failedExport && ( - - {errorGroupIndividualExport} + + {errorGroupIndividualExport?.message} + + {`${errorGroupIndividualExport?.code}: `} + {errorGroupIndividualExport?.detail} + - diff --git a/src/components/GroupSearcher.js b/src/components/GroupSearcher.js index d557ee0..adf71e1 100644 --- a/src/components/GroupSearcher.js +++ b/src/components/GroupSearcher.js @@ -19,6 +19,7 @@ import { Dialog, DialogActions, DialogTitle, + DialogContent, } from '@material-ui/core'; import EditIcon from '@material-ui/icons/Edit'; import DeleteIcon from '@material-ui/icons/Delete'; @@ -174,7 +175,9 @@ function GroupSearcher({ const [failedExport, setFailedExport] = useState(false); useEffect(() => { - setFailedExport(true); + if (errorGroupExport) { + setFailedExport(true); + } }, [errorGroupExport]); useEffect(() => { @@ -182,6 +185,8 @@ function GroupSearcher({ downloadExport(groupExport, `${formatMessage(intl, 'individual', 'export.filename.groups')}.csv`)(); clearGroupExport(); } + + return setFailedExport(false); }, [groupExport]); const groupFilter = (props) => ( @@ -241,10 +246,14 @@ function GroupSearcher({ rowLocked={isRowDisabled} /> {failedExport && ( - - {errorGroupExport} + + {errorGroupExport?.message} + + {`${errorGroupExport?.code}: `} + {errorGroupExport?.detail} + - diff --git a/src/components/IndividualSearcher.js b/src/components/IndividualSearcher.js index 76f15df..ff92bcd 100644 --- a/src/components/IndividualSearcher.js +++ b/src/components/IndividualSearcher.js @@ -22,6 +22,7 @@ import { Dialog, DialogActions, DialogTitle, + DialogContent, } from '@material-ui/core'; import EditIcon from '@material-ui/icons/Edit'; import DeleteIcon from '@material-ui/icons/Delete'; @@ -217,7 +218,9 @@ function IndividualSearcher({ const [failedExport, setFailedExport] = useState(false); useEffect(() => { - setFailedExport(true); + if (errorIndividualExport) { + setFailedExport(true); + } }, [errorIndividualExport]); useEffect(() => { @@ -225,6 +228,8 @@ function IndividualSearcher({ downloadExport(individualExport, `${formatMessage(intl, 'individual', 'export.filename.individuals')}.csv`)(); clearIndividualExport(); } + + return setFailedExport(false); }, [individualExport]); const defaultFilters = () => { @@ -305,10 +310,14 @@ function IndividualSearcher({ } : { isCustomFiltering: false })} /> {failedExport && ( - - {errorIndividualExport} + + {errorIndividualExport?.message} + + {`${errorIndividualExport?.code}: `} + {errorIndividualExport?.detail} + - From 5115007bff05263dbd63796ccd408bc5eeb6b0ca Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 11 Apr 2024 12:42:03 +0200 Subject: [PATCH 86/90] CM-874: move individuals to social protection menu (#67) Co-authored-by: Jan --- src/constants.js | 1 - src/index.js | 23 ++++++++++++----- src/menus/IndividualsMainMenu.js | 44 -------------------------------- 3 files changed, 17 insertions(+), 51 deletions(-) delete mode 100644 src/menus/IndividualsMainMenu.js diff --git a/src/constants.js b/src/constants.js index 487d2d2..e1090f5 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,4 +1,3 @@ -export const INDIVIDUALS_MAIN_MENU_CONTRIBUTION_KEY = 'individuals.MainMenu'; export const CONTAINS_LOOKUP = 'Icontains'; export const DEFAULT_DEBOUNCE_TIME = 500; export const DEFAULT_PAGE_SIZE = 10; diff --git a/src/index.js b/src/index.js index 8260e65..fa6017f 100644 --- a/src/index.js +++ b/src/index.js @@ -4,9 +4,9 @@ import flatten from 'flat'; import { FormattedMessage } from '@openimis/fe-core'; import React from 'react'; +import { Person, People } from '@material-ui/icons'; import messages_en from './translations/en.json'; import reducer from './reducer'; -import IndividualsMainMenu from './menus/IndividualsMainMenu'; import IndividualsPage from './pages/IndividualsPage'; import IndividualPage from './pages/IndividualPage'; import EnrollmentPage from './pages/EnrollmentPage'; @@ -36,7 +36,7 @@ import { GroupIndividualUpdateTaskItemFormatters, GroupIndividualUpdateTaskTableHeaders, } from './components/tasks/GroupIndividualUpdateTasks'; -import { GROUP_LABEL, INDIVIDUAL_LABEL } from './constants'; +import {GROUP_LABEL, INDIVIDUAL_LABEL, INDIVIDUAL_MODULE_NAME} from './constants'; import { GroupCreateTaskItemFormatters, GroupCreateTaskTableHeaders } from './components/tasks/GroupCreateTasks'; import IndividualsUploadDialog from './components/dialogs/IndividualsUploadDialog'; import { BenefitsTabLabel, BenefitsTabPanel } from './components/BenefitsTab'; @@ -61,7 +61,6 @@ const { BenefitPlansListTabLabel, BenefitPlansListTabPanel } = getBenefitPlansLi const DEFAULT_CONFIG = { translations: [{ key: 'en', messages: flatten(messages_en) }], reducers: [{ key: 'individual', reducer }], - 'core.MainMenu': [IndividualsMainMenu], 'core.Router': [ { path: ROUTE_INDIVIDUALS, component: IndividualsPage }, { path: ROUTE_GROUPS, component: GroupsPage }, @@ -70,6 +69,18 @@ const DEFAULT_CONFIG = { { path: `${ROUTE_INDIVIDUAL_FROM_GROUP}/:individual_uuid?`, component: IndividualPage }, { path: `${ROUTE_GROUP}/:group_uuid?`, component: GroupPage }, ], + 'socialProtection.MainMenu': [ + { + text: , + icon: , + route: '/individuals', + }, + { + text: , + icon: , + route: '/groups', + }, + ], refs: [ { key: 'individual.route.individual', ref: ROUTE_INDIVIDUAL }, { key: 'individual.route.enrollment', ref: ROUTE_ENROLLMENT }, @@ -114,21 +125,21 @@ const DEFAULT_CONFIG = { 'individual.BenefitPlansListTabLabel': [BENEFIT_PLAN_TABS_LABEL_REF_KEY], 'individual.BenefitPlansListTabPanel': [BENEFIT_PLAN_TABS_PANEL_REF_KEY], 'tasksManagement.tasks': [{ - text: , + text: , tableHeaders: IndividualUpdateTaskTableHeaders, itemFormatters: IndividualUpdateTaskItemFormatters, taskSource: ['IndividualService'], taskCode: INDIVIDUAL_LABEL, }, { - text: , + text: , tableHeaders: GroupIndividualUpdateTaskTableHeaders, itemFormatters: GroupIndividualUpdateTaskItemFormatters, taskSource: ['GroupIndividualService'], taskCode: GROUP_LABEL, }, { - text: , + text: , tableHeaders: GroupCreateTaskTableHeaders, itemFormatters: GroupCreateTaskItemFormatters, taskSource: ['CreateGroupAndMoveIndividualService'], diff --git a/src/menus/IndividualsMainMenu.js b/src/menus/IndividualsMainMenu.js deleted file mode 100644 index aafd952..0000000 --- a/src/menus/IndividualsMainMenu.js +++ /dev/null @@ -1,44 +0,0 @@ -// Rules disabled due to core architecture -/* eslint-disable react/destructuring-assignment */ -/* eslint-disable react/jsx-props-no-spreading */ - -import React from 'react'; -import { injectIntl } from 'react-intl'; -import { connect } from 'react-redux'; -import { Person, People } from '@material-ui/icons'; -import { formatMessage, MainMenuContribution, withModulesManager } from '@openimis/fe-core'; -import { INDIVIDUALS_MAIN_MENU_CONTRIBUTION_KEY } from '../constants'; - -function IndividualsMainMenu(props) { - const entries = [ - { - text: formatMessage(props.intl, 'individual', 'menu.individuals'), - icon: , - route: '/individuals', - }, - { - text: formatMessage(props.intl, 'individual', 'menu.groups'), - icon: , - route: '/groups', - }, - ]; - entries.push( - ...props.modulesManager - .getContribs(INDIVIDUALS_MAIN_MENU_CONTRIBUTION_KEY) - .filter((c) => !c.filter || c.filter(props.rights)), - ); - - return ( - - ); -} - -const mapStateToProps = (state) => ({ - rights: !!state.core && !!state.core.user && !!state.core.user.i_user ? state.core.user.i_user.rights : [], -}); - -export default injectIntl(withModulesManager(connect(mapStateToProps)(IndividualsMainMenu))); From 38392ab100be599dc66f4d9110d4696e40b57229 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 15 Apr 2024 10:02:47 +0200 Subject: [PATCH 87/90] add-menu-filter: add menu filters (#68) Co-authored-by: Jan --- src/index.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/index.js b/src/index.js index fa6017f..9f1a15f 100644 --- a/src/index.js +++ b/src/index.js @@ -36,7 +36,13 @@ import { GroupIndividualUpdateTaskItemFormatters, GroupIndividualUpdateTaskTableHeaders, } from './components/tasks/GroupIndividualUpdateTasks'; -import {GROUP_LABEL, INDIVIDUAL_LABEL, INDIVIDUAL_MODULE_NAME} from './constants'; +import { + GROUP_LABEL, + INDIVIDUAL_LABEL, + INDIVIDUAL_MODULE_NAME, + RIGHT_GROUP_SEARCH, + RIGHT_INDIVIDUAL_SEARCH +} from './constants'; import { GroupCreateTaskItemFormatters, GroupCreateTaskTableHeaders } from './components/tasks/GroupCreateTasks'; import IndividualsUploadDialog from './components/dialogs/IndividualsUploadDialog'; import { BenefitsTabLabel, BenefitsTabPanel } from './components/BenefitsTab'; @@ -73,12 +79,14 @@ const DEFAULT_CONFIG = { { text: , icon: , - route: '/individuals', + route: `/${ROUTE_INDIVIDUALS}`, + filter: (rights) => rights.includes(RIGHT_INDIVIDUAL_SEARCH), }, { text: , icon: , - route: '/groups', + route: `/${ROUTE_GROUPS}`, + filter: (rights) => rights.includes(RIGHT_GROUP_SEARCH), }, ], refs: [ From c106a9c174b7e4cf3c848af0df8ecc8136974165 Mon Sep 17 00:00:00 2001 From: sniedzielski <52816247+sniedzielski@users.noreply.github.com> Date: Mon, 22 Apr 2024 16:49:16 +0200 Subject: [PATCH 88/90] CM-868: added Individual Picker (#69) * CM-868: added individual picker * CM-868: added possibility to filter by benefit plan --- src/constants.js | 1 + src/index.js | 2 + src/pickers/IndividualPicker.js | 152 ++++++++++++++++++++++++++++++++ 3 files changed, 155 insertions(+) create mode 100644 src/pickers/IndividualPicker.js diff --git a/src/constants.js b/src/constants.js index e1090f5..2e00542 100644 --- a/src/constants.js +++ b/src/constants.js @@ -87,3 +87,4 @@ export const UPLOAD_STATUS = { WAITING_FOR_VERIFICATION: 'WAITING_FOR_VERIFICATION', FAIL: 'FAIL', }; +export const INDIVIDUALS_QUANTITY_LIMIT = 15; diff --git a/src/index.js b/src/index.js index 9f1a15f..44c4aee 100644 --- a/src/index.js +++ b/src/index.js @@ -52,6 +52,7 @@ import { GroupIndividualHistoryTabPanel, } from './components/GroupIndividualHistoryTab'; import AdvancedCriteriaRowValue from './components/dialogs/AdvancedCriteriaRowValue'; +import IndividualPicker from './pickers/IndividualPicker'; const ROUTE_INDIVIDUALS = 'individuals'; const ROUTE_INDIVIDUAL = 'individuals/individual'; @@ -102,6 +103,7 @@ const DEFAULT_CONFIG = { { key: 'individual.IndividualsUploadDialog', ref: IndividualsUploadDialog }, { key: 'individual.GroupIndividualHistorySearcher', ref: GroupIndividualHistorySearcher }, { key: 'individual.AdvancedCriteriaRowValue', ref: AdvancedCriteriaRowValue }, + { key: 'individual.IndividualPicker', ref: IndividualPicker }, ], 'individual.IndividualsUploadDialog': IndividualsUploadDialog, 'individual.TabPanel.label': [ diff --git a/src/pickers/IndividualPicker.js b/src/pickers/IndividualPicker.js new file mode 100644 index 0000000..0e961e6 --- /dev/null +++ b/src/pickers/IndividualPicker.js @@ -0,0 +1,152 @@ +import React, { useState } from 'react'; +import { TextField, Tooltip } from '@material-ui/core'; + +import { + Autocomplete, useModulesManager, + useTranslations, useGraphqlQuery, + decodeId +} from '@openimis/fe-core'; +import { INDIVIDUALS_QUANTITY_LIMIT } from '../constants'; + +function IndividualPicker(props) { + const { + multiple, + required, + label, + nullLabel, + withLabel = false, + placeholder, + withPlaceholder = false, + readOnly, + value, + onChange, + filter, + filterSelectedOptions, + benefitPlan = null, + } = props; + + const modulesManager = useModulesManager(); + const [filters, setFilters] = useState({ isDeleted: false }); + const [currentString, setCurrentString] = useState(''); + const { formatMessage, formatMessageWithValues } = useTranslations('individual', modulesManager); + + if (benefitPlan) { + const decodedBenefitPlanId = decodeId(benefitPlan.id); + const { isLoading, data, error } = useGraphqlQuery( + ` + query IndividualPicker( + $decodedBenefitPlanId: String, $search: String, $first: Int, $isDeleted: Boolean + ) { + individual( + lastName_Icontains: $search, + first: $first, + isDeleted: $isDeleted, + ${decodedBenefitPlanId ? 'benefitPlanId: $decodedBenefitPlanId' : ''}, + ) { + edges { + node { + id,isDeleted,dateCreated,dateUpdated,firstName,lastName,dob,jsonExt,version,userUpdated {username} + } + } + }} + `, + { decodedBenefitPlanId }, + filters, + { skip: true }, + ); + const individuals = data?.individual?.edges.map((edge) => edge.node) ?? []; + const shouldShowTooltip = individuals?.length >= INDIVIDUALS_QUANTITY_LIMIT && !value && !currentString; + + return ( + `${option.firstName} ${option.lastName} ${option.dob}`} + onChange={(value) => onChange(value, value ? `${value.firstName} ${value.lastName} ${value.dob}` : null)} + setCurrentString={setCurrentString} + filterOptions={filter} + filterSelectedOptions={filterSelectedOptions} + onInputChange={(search) => setFilters({ search, isDeleted: false })} + renderInput={(inputProps) => ( + + + + )} + /> + ); + } + const { isLoading, data, error } = useGraphqlQuery( + ` + query IndividualPicker( + $search: String, $first: Int, $isDeleted: Boolean + ) { + individual( + lastName_Icontains: $search, + first: $first, + isDeleted: $isDeleted + ) { + edges { + node { + id,isDeleted,dateCreated,dateUpdated,firstName,lastName,dob,jsonExt,version,userUpdated {username} + } + } + }} + `, + filters, + { skip: true }, + ); + const individuals = data?.individual?.edges.map((edge) => edge.node) ?? []; + const shouldShowTooltip = individuals?.length >= INDIVIDUALS_QUANTITY_LIMIT && !value && !currentString; + + return ( + `${option.firstName} ${option.lastName} ${option.dob}`} + onChange={(value) => onChange(value, value ? `${value.firstName} ${value.lastName} ${value.dob}` : null)} + setCurrentString={setCurrentString} + filterOptions={filter} + filterSelectedOptions={filterSelectedOptions} + onInputChange={(search) => setFilters({ search, isDeleted: false })} + renderInput={(inputProps) => ( + + + + )} + /> + ); +} + +export default IndividualPicker; From 6af5f6932d7ab766a5ce6e0464728e68c2f9e4a3 Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 30 Apr 2024 09:30:23 +0200 Subject: [PATCH 89/90] 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", From 659c6c1df9da1fc3b5d40305b056d3d7b38d509a Mon Sep 17 00:00:00 2001 From: Patrick Delcroix Date: Tue, 30 Apr 2024 20:21:52 +0200 Subject: [PATCH 90/90] Update npmpublish.yml --- .github/workflows/npmpublish.yml | 85 ++++++++++++++++---------------- 1 file changed, 42 insertions(+), 43 deletions(-) diff --git a/.github/workflows/npmpublish.yml b/.github/workflows/npmpublish.yml index 61bbd22..5958bb0 100644 --- a/.github/workflows/npmpublish.yml +++ b/.github/workflows/npmpublish.yml @@ -1,51 +1,50 @@ # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages -name: Node.js Package +name: Publish Node.js Package on: release: - types: [created] + types: [published] + workflow_dispatch: + inputs: + node_version: + description: 'Node version to use' + required: true + default: '20' + registry_url: + description: 'NPM registry URL' + required: true + default: 'https://registry.npmjs.org/' + scope: + description: 'Scope for npm package' + required: false + default: 'openimis' + access: + description: 'Access level for npm package' + required: false + default: 'public' + tab: + required: true jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 - with: - node-version: 12 - - run: yarn install - - run: yarn build - - publish-npm: - needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 - with: - node-version: 12 - registry-url: https://registry.npmjs.org/ - scope: openimis - - run: yarn install - - run: yarn build - - run: npm publish --access public - env: - NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} - - publish-gpr: - needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 - with: - node-version: 12 - registry-url: https://npm.pkg.github.com/ - - run: yarn install - - run: yarn build - - run: npm publish - env: - NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} - \ No newline at end of file + call-npm-publish-workflow: + uses: openimis/openimis-fe_js/.github/workflows/module-npmpublish.yml@develop + with: + registry_url: 'https://registry.npmjs.org/' + node_version: ${{ github.event.inputs.node_version }} + access: ${{ github.event.inputs.access }} + scope: ${{ github.event.inputs.scope }} + tag: ${{ github.event.inputs.tag }} + secrets: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + call-gpr-publish-workflow: + uses: openimis/openimis-fe_js/.github/workflows/module-npmpublish.yml@develop + with: + registry_url: 'https://npm.pkg.github.com/' + node_version: ${{ github.event.inputs.node_version }} + access: ${{ github.event.inputs.access }} + scope: ${{ github.event.inputs.scope }} + tag: ${{ github.event.inputs.tag }} + secrets: + NPM_TOKEN: ${{ secrets.GITHUB_TOKEN }}