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 && (
+
+ )}
+
+ );
}
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 && (
@@ -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 && (
+
+ )}
+
);
}
@@ -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 && (