From 3deee25814855d7b61a8a5eb78148076bbe4efd5 Mon Sep 17 00:00:00 2001 From: sniedzielski <52816247+sniedzielski@users.noreply.github.com> Date: Mon, 17 Jun 2024 12:52:02 +0200 Subject: [PATCH] CM-930: added frontend improvements in terms of performance, digital payment support (#52) --- src/actions.js | 16 +- .../payroll/PayrollSearcherApproved.js | 2 +- .../payroll/PayrollSearcherPending.js | 159 +++++++++++++ .../payroll/PayrollSearcherReconciled.js | 2 +- src/components/payroll/PayrollTab.js | 3 +- .../PaymentApproveForPaymentSummary.js | 32 ++- .../PaymentReconciliationSummaryDialog.js | 19 +- .../dialogs/PayrollPendingPayrollSummary.js | 211 ++++++++++++++++++ src/index.js | 10 + src/pages/payroll/PendingPayrollsPage.js | 37 +++ src/translations/en.json | 1 + 11 files changed, 471 insertions(+), 21 deletions(-) create mode 100644 src/components/payroll/PayrollSearcherPending.js create mode 100644 src/components/payroll/dialogs/PayrollPendingPayrollSummary.js create mode 100644 src/pages/payroll/PendingPayrollsPage.js diff --git a/src/actions.js b/src/actions.js index 605effd..3d88225 100644 --- a/src/actions.js +++ b/src/actions.js @@ -74,6 +74,20 @@ const PAYROLL_PROJECTION = (modulesManager) => [ 'isDeleted', ]; +const PAYROLL_SEARCHER_PROJECTION = (modulesManager) => [ + 'id', + 'name', + 'paymentMethod', + 'paymentPlan { code, id, name, benefitPlan }', + `paymentPoint { ${PAYMENT_POINT_PROJECTION(modulesManager).join(' ')} }`, + 'paymentCycle { code, startDate, endDate }', + 'jsonExt', + 'status', + 'dateValidFrom', + 'dateValidTo', + 'isDeleted', +]; + const CSV_RECONCILIATION_PROJECTION = () => [ 'fileName', 'status', @@ -186,7 +200,7 @@ export function updatePaymentPoint(paymentPoint, clientMutationLabel) { } export function fetchPayrolls(modulesManager, params) { - const payload = formatPageQueryWithCount('payroll', params, PAYROLL_PROJECTION(modulesManager)); + const payload = formatPageQueryWithCount('payroll', params, PAYROLL_SEARCHER_PROJECTION(modulesManager)); return graphql(payload, ACTION_TYPE.SEARCH_PAYROLLS); } diff --git a/src/components/payroll/PayrollSearcherApproved.js b/src/components/payroll/PayrollSearcherApproved.js index 73f9fc5..83005a1 100644 --- a/src/components/payroll/PayrollSearcherApproved.js +++ b/src/components/payroll/PayrollSearcherApproved.js @@ -103,7 +103,7 @@ function PayrollSearcherApproved({ (payroll) => ( ), ]; diff --git a/src/components/payroll/PayrollSearcherPending.js b/src/components/payroll/PayrollSearcherPending.js new file mode 100644 index 0000000..af08cb6 --- /dev/null +++ b/src/components/payroll/PayrollSearcherPending.js @@ -0,0 +1,159 @@ +import React, { useRef, useEffect } from 'react'; +import { connect, useSelector } from 'react-redux'; +import { bindActionCreators } from 'redux'; + +import { + Searcher, + useHistory, + useModulesManager, + useTranslations, + coreConfirm, + clearConfirm, + journalize, +} from '@openimis/fe-core'; +import PayrollFilter from './PayrollFilter'; +import { + DEFAULT_PAGE_SIZE, MODULE_NAME, PAYROLL_PAYROLL_ROUTE, RIGHT_PAYROLL_SEARCH, ROWS_PER_PAGE_OPTIONS, PAYROLL_STATUS, +} from '../../constants'; +import { fetchPayrolls } from '../../actions'; +import PayrollReconciliationFilesDialog from './dialogs/PayrollReconciliationFilesDialog'; +import PayrollPendingPayrollSummary from './dialogs/PayrollPendingPayrollSummary'; + +function PayrollSearcherPending({ + fetchingPayrolls, + fetchedPayrolls, + errorPayrolls, + payrolls, + pageInfo, + totalCount, + fetchPayrolls, + submittingMutation, + mutation, + classes, +}) { + const history = useHistory(); + const modulesManager = useModulesManager(); + const { formatMessageWithValues } = useTranslations(MODULE_NAME, modulesManager); + const rights = useSelector((store) => store.core.user.i_user.rights ?? []); + + const prevSubmittingMutationRef = useRef(); + + useEffect(() => { + if (prevSubmittingMutationRef.current && !submittingMutation) { + journalize(mutation); + } + }, [submittingMutation]); + + useEffect(() => { + prevSubmittingMutationRef.current = submittingMutation; + }); + + const headers = () => [ + 'payroll.name', + 'payroll.paymentPlan', + 'payroll.paymentPoint', + 'payroll.status', + 'payroll.paymentMethod', + 'emptyLabel', + ]; + + const sorts = () => [ + ['name', true], + ['paymentPlan', true], + ['paymentPoint', true], + ['status', true], + ['paymentMethod', true], + ]; + + const defaultFilters = () => ({ + isDeleted: { + value: false, + filter: 'isDeleted: false', + }, + status: { + value: PAYROLL_STATUS.PENDING_APPROVAL, + filter: `status: ${PAYROLL_STATUS.PENDING_APPROVAL}`, + }, + }); + + const fetch = (params) => fetchPayrolls(modulesManager, params); + + const rowIdentifier = (payroll) => payroll.id; + + const openPayroll = (payroll) => rights.includes(RIGHT_PAYROLL_SEARCH) && history.push( + `/${modulesManager.getRef(PAYROLL_PAYROLL_ROUTE)}/${payroll?.id}`, + ); + + const itemFormatters = () => [ + (payroll) => payroll.name, + (payroll) => (payroll.benefitPlan + ? `${payroll.benefitPlan.code} ${payroll.benefitPlan.name}` : ''), + (payroll) => (payroll.paymentPoint + ? `${payroll.paymentPoint.name}` : ''), + (payroll) => (payroll.status + ? `${payroll.status}` : ''), + (payroll) => (payroll.paymentMethod + ? `${payroll.paymentMethod}` : ''), + (payroll) => ( + + ), + (payroll) => ( + + ), + ]; + + const onDoubleClick = (payroll) => openPayroll(payroll); + + const payrollFilter = ({ filters, onChangeFilters }) => ( + + ); + + return ( + + ); +} + +const mapStateToProps = (state) => ({ + fetchingPayrolls: state.payroll.fetchingPayrolls, + fetchedPayrolls: state.payroll.fetchedPayrolls, + errorPayrolls: state.payroll.errorPayrolls, + payrolls: state.payroll.payrolls, + pageInfo: state.payroll.payrollsPageInfo, + totalCount: state.payroll.payrollsTotalCount, + confirmed: state.core.confirmed, + submittingMutation: state.payroll.submittingMutation, + mutation: state.payroll.mutation, +}); + +const mapDispatchToProps = (dispatch) => bindActionCreators({ + fetchPayrolls, + journalize, + clearConfirm, + coreConfirm, +}, dispatch); + +export default connect(mapStateToProps, mapDispatchToProps)(PayrollSearcherPending); diff --git a/src/components/payroll/PayrollSearcherReconciled.js b/src/components/payroll/PayrollSearcherReconciled.js index e9c6c04..6fc3f05 100644 --- a/src/components/payroll/PayrollSearcherReconciled.js +++ b/src/components/payroll/PayrollSearcherReconciled.js @@ -103,7 +103,7 @@ function PayrollSearcherReconciled({ (payroll) => ( ), ]; diff --git a/src/components/payroll/PayrollTab.js b/src/components/payroll/PayrollTab.js index d81d378..f9dd3e3 100644 --- a/src/components/payroll/PayrollTab.js +++ b/src/components/payroll/PayrollTab.js @@ -1,3 +1,4 @@ +/* eslint-disable max-len */ import React, { useState } from 'react'; import { Paper, Grid } from '@material-ui/core'; import { @@ -90,7 +91,7 @@ function PayrollTab({ {formatMessage('payroll.summary.download')} )} - {payrollUuid && payroll?.status === PAYROLL_STATUS.APPROVE_FOR_PAYMENT + {payrollUuid && payroll?.status === PAYROLL_STATUS.APPROVE_FOR_PAYMENT && payroll.paymentMethod === 'StrategyOfflinePayment' && ( { + if (payrollUuid) { + fetchPayroll(modulesManager, [`id: "${payrollUuid}"`]); + } setIsOpen(true); }; @@ -42,11 +49,10 @@ function PaymentApproveForPaymentDialog({ setIsOpen(false); }; - const modulesManager = useModulesManager(); const { formatMessage, formatMessageWithValues } = useTranslations(MODULE_NAME, modulesManager); useEffect(() => { - if (isOpen) { + if (isOpen && Object.keys(payroll).length > 0) { // Calculate total benefits and reconciled benefits const total = payroll.benefitConsumption.length; const selected = payroll.benefitConsumption.filter( @@ -80,16 +86,16 @@ function PaymentApproveForPaymentDialog({ const closePayrollCallback = () => { handleClose(); closePayroll( - payroll, - formatMessageWithValues('payroll.mutation.closeLabel', mutationLabel(payroll)), + payrollDetail, + formatMessageWithValues('payroll.mutation.closeLabel', mutationLabel(payrollDetail)), ); }; const rejectPayrollCallback = () => { handleClose(); rejectPayroll( - payroll, - formatMessageWithValues('payroll.mutation.closeLabel', mutationLabel(payroll)), + payrollDetail, + formatMessageWithValues('payroll.mutation.closeLabel', mutationLabel(payrollDetail)), ); }; @@ -126,7 +132,7 @@ function PaymentApproveForPaymentDialog({ marginTop: '10px', }} > - {formatMessageWithValues('payroll.reconciliationSummary', { payrollName: payroll.name })} + {formatMessageWithValues('payroll.reconciliationSummary', { payrollName: payrollDetail.name })} @@ -164,7 +170,7 @@ function PaymentApproveForPaymentDialog({ - + closePayrollCallback(payroll)} + onClick={() => closePayrollCallback(payrollDetail)} variant="contained" color="primary" disabled={selectedBeneficiaries === 0} @@ -191,7 +197,7 @@ function PaymentApproveForPaymentDialog({ {formatMessage('payroll.summary.approveAndClose')} downloadPayrollData(payroll.id, payroll.name)} + onClick={() => downloadPayrollData(payrollDetail.id, payrollDetail.name)} variant="contained" color="primary" style={{ @@ -202,7 +208,7 @@ function PaymentApproveForPaymentDialog({ {formatMessage('payroll.summary.download')} rejectPayrollCallback(payroll)} + onClick={() => rejectPayrollCallback(payrollDetail)} variant="contained" color="primary" style={{ @@ -237,11 +243,13 @@ function PaymentApproveForPaymentDialog({ const mapStateToProps = (state) => ({ rights: !!state.core && !!state.core.user && !!state.core.user.i_user ? state.core.user.i_user.rights : [], confirmed: state.core.confirmed, + payroll: state.payroll.payroll, }); const mapDispatchToProps = (dispatch) => bindActionCreators({ closePayroll, rejectPayroll, + fetchPayroll, }, dispatch); export default connect(mapStateToProps, mapDispatchToProps)(PaymentApproveForPaymentDialog); diff --git a/src/components/payroll/dialogs/PaymentReconciliationSummaryDialog.js b/src/components/payroll/dialogs/PaymentReconciliationSummaryDialog.js index 4934b17..d4521f1 100644 --- a/src/components/payroll/dialogs/PaymentReconciliationSummaryDialog.js +++ b/src/components/payroll/dialogs/PaymentReconciliationSummaryDialog.js @@ -26,13 +26,17 @@ import { import downloadPayroll from '../../../utils/export'; import BenefitConsumptionSearcherModal from '../BenefitConsumptionSearcherModal'; +import { fetchPayroll } from '../../../actions'; function PaymentReconcilationSummarytDialog({ classes, payroll, + payrollDetail, + fetchPayroll, }) { const history = useHistory(); const modulesManager = useModulesManager(); + const [payrollUuid] = useState(payrollDetail?.id ?? null); const { formatMessage, formatMessageWithValues } = useTranslations(MODULE_NAME, modulesManager); const [isOpen, setIsOpen] = useState(false); const [totalBeneficiaries, setTotalBeneficiaries] = useState(0); @@ -41,6 +45,9 @@ function PaymentReconcilationSummarytDialog({ const [totalReconciledBillAmount, setTotalReconciledBillAmount] = useState(0); const handleOpen = () => { + if (payrollUuid) { + fetchPayroll(modulesManager, [`id: "${payrollUuid}"`]); + } setIsOpen(true); }; @@ -50,12 +57,12 @@ function PaymentReconcilationSummarytDialog({ const handleCreatePaymentForFailedInvoice = () => { history.push( - `/${modulesManager.getRef(PAYROLL_PAYROLL_ROUTE)}/${payroll?.id}/${PAYROLL_FROM_FAILED_INVOICES_URL_PARAM}`, + `/${modulesManager.getRef(PAYROLL_PAYROLL_ROUTE)}/${payrollDetail?.id}/${PAYROLL_FROM_FAILED_INVOICES_URL_PARAM}`, ); }; useEffect(() => { - if (isOpen) { + if (isOpen && Object.keys(payroll).length > 0) { // Calculate total benefits and reconciled benefits const total = payroll.benefitConsumption.length; const selected = payroll.benefitConsumption.filter( @@ -119,7 +126,7 @@ function PaymentReconcilationSummarytDialog({ marginTop: '10px', }} > - {formatMessageWithValues('payroll.reconciliationSummary', { payrollName: payroll.name })} + {formatMessageWithValues('payroll.reconciliationSummary', { payrollName: payrollDetail.name })} @@ -157,7 +164,7 @@ function PaymentReconcilationSummarytDialog({ - + downloadPayrollData(payroll.id, payroll.name)} + onClick={() => downloadPayrollData(payrollDetail.id, payrollDetail.name)} variant="contained" color="primary" style={{ @@ -219,9 +226,11 @@ function PaymentReconcilationSummarytDialog({ const mapStateToProps = (state) => ({ rights: !!state.core && !!state.core.user && !!state.core.user.i_user ? state.core.user.i_user.rights : [], confirmed: state.core.confirmed, + payroll: state.payroll.payroll, }); const mapDispatchToProps = (dispatch) => bindActionCreators({ + fetchPayroll, }, dispatch); export default connect(mapStateToProps, mapDispatchToProps)(PaymentReconcilationSummarytDialog); diff --git a/src/components/payroll/dialogs/PayrollPendingPayrollSummary.js b/src/components/payroll/dialogs/PayrollPendingPayrollSummary.js new file mode 100644 index 0000000..c4949df --- /dev/null +++ b/src/components/payroll/dialogs/PayrollPendingPayrollSummary.js @@ -0,0 +1,211 @@ +/* eslint-disable max-len */ +import React, { useEffect, useState } from 'react'; +import Button from '@material-ui/core/Button'; +import Dialog from '@material-ui/core/Dialog'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import { + useModulesManager, + useTranslations, +} from '@openimis/fe-core'; +import { + Paper, + Grid, +} from '@material-ui/core'; +import Typography from '@material-ui/core/Typography'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import { MODULE_NAME, BENEFIT_CONSUMPTION_STATUS } from '../../../constants'; +import BenefitConsumptionSearcherModal from '../BenefitConsumptionSearcherModal'; +import downloadPayroll from '../../../utils/export'; +import { fetchPayroll } from '../../../actions'; + +function PaymentPendingPayrollPaymentDialog({ + classes, + payroll, + fetchPayroll, + payrollDetail, +}) { + const modulesManager = useModulesManager(); + const [payrollUuid] = useState(payrollDetail?.id ?? null); + const [isOpen, setIsOpen] = useState(false); + const [totalBeneficiaries, setTotalBeneficiaries] = useState(0); + const [selectedBeneficiaries, setSelectedBeneficiaries] = useState(0); + const [totalBillAmount, setTotalBillAmount] = useState(0); + const [totalReconciledBillAmount, setTotalReconciledBillAmount] = useState(0); + + const handleOpen = () => { + if (payrollUuid) { + fetchPayroll(modulesManager, [`id: "${payrollUuid}"`]); + } + setIsOpen(true); + }; + + const handleClose = () => { + setIsOpen(false); + }; + + const { formatMessage, formatMessageWithValues } = useTranslations(MODULE_NAME, modulesManager); + + useEffect(() => { + if (isOpen && Object.keys(payroll).length > 0) { + // Calculate total benefits and reconciled benefits + const total = payroll.benefitConsumption.length; + const selected = payroll.benefitConsumption.filter( + (benefit) => benefit.status === BENEFIT_CONSUMPTION_STATUS.RECONCILED, + ).length; + + setTotalBeneficiaries(total); + setSelectedBeneficiaries(selected); + + let totalAmount = 0; + let reconciledAmount = 0; + if (payroll && payroll.benefitConsumption) { + payroll.benefitConsumption.forEach((benefit) => { + if (benefit.benefitAttachment && benefit.benefitAttachment.length > 0) { + benefit.benefitAttachment.forEach((attachment) => { + if (attachment.bill && attachment.bill.amountTotal) { + totalAmount += parseFloat(attachment.bill.amountTotal); + if (benefit.status === BENEFIT_CONSUMPTION_STATUS.RECONCILED) { + reconciledAmount += parseFloat(attachment.bill.amountTotal); + } + } + }); + } + }); + } + setTotalBillAmount(totalAmount); + setTotalReconciledBillAmount(reconciledAmount); + } + }, [isOpen, payroll]); + + const downloadPayrollData = (payrollUuid, payrollName) => { + downloadPayroll(payrollUuid, payrollName); + }; + + return ( + <> + + {formatMessage('payroll.viewReconciliationSummary')} + + + + {formatMessageWithValues('payroll.reconciliationSummary', { payrollName: payrollDetail.name })} + + + + + + + {formatMessage('payroll.summary.selectedBeneficiaries')} + + + {formatMessageWithValues('payroll.summary.beneficiariesCount', { selectedBeneficiaries, totalBeneficiaries })} + + + + + + + {formatMessage('payroll.summary.totalAmountForInvoice')} + + + {totalBillAmount} + + + + + + + {formatMessage('payroll.summary.deliveredReconciliation')} + + + {totalReconciledBillAmount} + + + + + + + + + + + + downloadPayrollData(payrollDetail.id, payrollDetail.name)} + variant="contained" + color="primary" + style={{ + margin: '0 16px', + marginBottom: '15px', + }} + > + {formatMessage('payroll.summary.download')} + + + + + {formatMessage('payroll.summary.close')} + + + + + + > + ); +} + +const mapStateToProps = (state) => ({ + rights: !!state.core && !!state.core.user && !!state.core.user.i_user ? state.core.user.i_user.rights : [], + confirmed: state.core.confirmed, + payroll: state.payroll.payroll, +}); + +const mapDispatchToProps = (dispatch) => bindActionCreators({ + fetchPayroll, +}, dispatch); + +export default connect(mapStateToProps, mapDispatchToProps)(PaymentPendingPayrollPaymentDialog); diff --git a/src/index.js b/src/index.js index c01fed7..ad7f665 100644 --- a/src/index.js +++ b/src/index.js @@ -40,11 +40,13 @@ import { } from './components/payroll/PayrollTaskTabPanel'; import { PayrollDeleteTaskItemFormatters, PayrollDeleteTaskTableHeaders } from './components/tasks/PayrollDeleteTasks'; import { PayrollPaymentFilesTabLabel, PayrollPaymentFilesTabPanel } from './components/payroll/PayrollPaymentFilesTab'; +import PendingPayrollsPage from './pages/payroll/PendingPayrollsPage'; const ROUTE_PAYMENT_POINTS = 'paymentPoints'; const ROUTE_PAYMENT_POINT = 'paymentPoints/paymentPoint'; const ROUTE_PAYROLLS = 'payrolls'; const ROUTE_PAYROLLS_APPROVED = 'payrollsApproved'; +const ROUTE_PAYROLLS_PENDING = 'payrollsPending'; const ROUTE_PAYROLLS_RECONCILED = 'payrollsReconciled'; const ROUTE_PAYROLL = 'payrolls/payroll'; @@ -56,6 +58,7 @@ const DEFAULT_CONFIG = { { key: 'payroll.route.paymentPoint', ref: ROUTE_PAYMENT_POINT }, { key: 'payroll.route.payrolls', ref: ROUTE_PAYROLLS }, { key: 'payroll.route.payrollsApproved', ref: ROUTE_PAYROLLS_APPROVED }, + { key: 'payroll.route.payrollsPending', ref: ROUTE_PAYROLLS_PENDING }, { key: 'payroll.route.payrollsReconciled', ref: ROUTE_PAYROLLS_RECONCILED }, { key: 'payroll.route.payroll', ref: ROUTE_PAYROLL }, { key: 'payroll.PaymentPointPicker', ref: PaymentPointPicker }, @@ -68,6 +71,7 @@ const DEFAULT_CONFIG = { { path: `${ROUTE_PAYMENT_POINT}/:payment_point_uuid?`, component: PaymentPointPage }, { path: ROUTE_PAYROLLS, component: PayrollsPage }, { path: ROUTE_PAYROLLS_APPROVED, component: ApprovedPayrollsPage }, + { path: ROUTE_PAYROLLS_PENDING, component: PendingPayrollsPage }, { path: ROUTE_PAYROLLS_RECONCILED, component: ReconciledPayrollsPage }, { path: `${ROUTE_PAYROLL}/:payroll_uuid?/:createPayrollFromFailedInvoices?/:benefitPlanId?`, @@ -87,6 +91,12 @@ const DEFAULT_CONFIG = { route: `/${ROUTE_PAYROLLS}`, filter: (rights) => rights.includes(RIGHT_PAYROLL_SEARCH), }, + { + text: , + icon: , + route: `/${ROUTE_PAYROLLS_PENDING}`, + filter: (rights) => rights.includes(RIGHT_PAYROLL_SEARCH), + }, { text: , icon: , diff --git a/src/pages/payroll/PendingPayrollsPage.js b/src/pages/payroll/PendingPayrollsPage.js new file mode 100644 index 0000000..046035f --- /dev/null +++ b/src/pages/payroll/PendingPayrollsPage.js @@ -0,0 +1,37 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; + +import { makeStyles } from '@material-ui/styles'; + +import { + Helmet, + useModulesManager, + useTranslations, +} from '@openimis/fe-core'; +import { + MODULE_NAME, + RIGHT_PAYROLL_SEARCH, +} from '../../constants'; +import PayrollSearcherPending from '../../components/payroll/PayrollSearcherPending'; + +const useStyles = makeStyles((theme) => ({ + page: theme.page, + fab: theme.fab, +})); + +function PendingPayrollsPage() { + const modulesManager = useModulesManager(); + const classes = useStyles(); + const rights = useSelector((store) => store.core.user.i_user.rights ?? []); + const { formatMessage } = useTranslations(MODULE_NAME, modulesManager); + + return ( + + + {rights.includes(RIGHT_PAYROLL_SEARCH) + && } + + ); +} + +export default PendingPayrollsPage; diff --git a/src/translations/en.json b/src/translations/en.json index 32e93a9..384f523 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -124,6 +124,7 @@ "payroll.paymentData.upload.label": "Upload Payment Data", "payroll.route.payrollsApproved": "Approved Payrolls", + "payroll.route.payrollsPending": "Pending Payrolls", "payroll.route.payrollsReconciled": "Reconciled Payrolls", "payroll.tasks.rejected.title": "Rejected Payrolls", "payroll.PayrollTaskTab.label": "Tasks",