From 2693b3b80fe7f0219eadb91a7ca418dc2fd17e0b Mon Sep 17 00:00:00 2001 From: olewandowski1 Date: Tue, 29 Oct 2024 09:04:42 +0100 Subject: [PATCH 1/3] OM-325: add assignment progress tracker --- src/components/VoucherAssignmentForm.js | 17 +++--- .../VoucherAssignmentProgressTracker.js | 56 +++++++++++++++++++ src/pickers/WorkerMultiplePicker.js | 1 + src/translations/en.json | 4 +- 4 files changed, 70 insertions(+), 8 deletions(-) create mode 100644 src/components/VoucherAssignmentProgressTracker.js diff --git a/src/components/VoucherAssignmentForm.js b/src/components/VoucherAssignmentForm.js index e169960..cb30ed4 100644 --- a/src/components/VoucherAssignmentForm.js +++ b/src/components/VoucherAssignmentForm.js @@ -1,25 +1,26 @@ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { - Divider, Grid, Paper, Typography, Button, Tooltip, + Button, Divider, Grid, Paper, Tooltip, Typography, } from '@material-ui/core'; -import { makeStyles } from '@material-ui/styles'; import AssignmentIndIcon from '@material-ui/icons/AssignmentInd'; +import { makeStyles } from '@material-ui/styles'; import { + InfoButton, coreAlert, - useModulesManager, - useTranslations, - journalize, historyPush, + journalize, useHistory, - InfoButton, + useModulesManager, + useTranslations, } from '@openimis/fe-core'; import { assignVouchers, voucherAssignmentValidation } from '../actions'; import { MODULE_NAME, REF_ROUTE_WORKER_VOUCHERS, USER_ECONOMIC_UNIT_STORAGE_KEY } from '../constants'; import AssignmentVoucherForm from './AssignmentVoucherForm'; import VoucherAssignmentConfirmModal from './VoucherAssignmentConfirmModal'; +import VoucherAssignmentProgressTracker from './VoucherAssignmentProgressTracker'; export const useStyles = makeStyles((theme) => ({ paper: { ...theme.paper.paper, margin: '10px 0 0 0' }, @@ -168,6 +169,8 @@ function VoucherAssignmentForm() { + + ({ + container: { + display: 'flex', + alignItems: 'center', + justifyContent: 'start', + gap: '4px', + padding: theme.spacing(1), + }, +})); + +function VoucherAssignmentProgressTracker({ voucherAssignment }) { + const prevVoucherAssignment = useRef(); + const classes = useStyles(); + const { formatMessage } = useTranslations(MODULE_NAME); + const [isSaving, setIsSaving] = useState(false); + + useEffect(() => { + if (!_.isEqual(voucherAssignment, prevVoucherAssignment.current)) { + setIsSaving(true); + prevVoucherAssignment.current = voucherAssignment; + + // TODO: After BE integration, call the API to save the voucher assignment + setTimeout(() => { + setIsSaving(false); + }, 1500); + } + }, [voucherAssignment]); + + return ( + + {isSaving ? ( + <> + + {formatMessage('VoucherAssignmentProgressTracker.saving')} + + ) : ( + <> + + {formatMessage('VoucherAssignmentProgressTracker.upToDate')} + + )} + + ); +} + +export default VoucherAssignmentProgressTracker; diff --git a/src/pickers/WorkerMultiplePicker.js b/src/pickers/WorkerMultiplePicker.js index bc49422..2809c39 100644 --- a/src/pickers/WorkerMultiplePicker.js +++ b/src/pickers/WorkerMultiplePicker.js @@ -136,6 +136,7 @@ function WorkerMultiplePicker({ flexDirection: 'column', gap: '8px', alignItems: 'end', + marginTop: '8px', }} > Date: Wed, 30 Oct 2024 15:58:21 +0100 Subject: [PATCH 2/3] OM-325: sync with be --- src/actions.js | 98 ++++++++++++++++--- .../VoucherAcquirementGenericVoucher.js | 2 +- .../VoucherAcquirementSpecificWorker.js | 2 +- src/components/VoucherAssignmentForm.js | 63 +++++++----- .../VoucherAssignmentProgressTracker.js | 45 ++++++--- src/constants.js | 4 + src/reducer.js | 2 + src/translations/en.json | 5 +- 8 files changed, 168 insertions(+), 53 deletions(-) diff --git a/src/actions.js b/src/actions.js index 8b8162f..631a1ee 100644 --- a/src/actions.js +++ b/src/actions.js @@ -11,7 +11,7 @@ import { ACTION_TYPE } from './reducer'; import { CLEAR, ERROR, REQUEST, SUCCESS, } from './utils/action-type'; -import { EMPTY_STRING } from './constants'; +import { DRAFT_FORM_TYPE, EMPTY_STRING } from './constants'; const WORKER_VOUCHER_PROJECTION = (modulesManager) => [ 'id', @@ -56,12 +56,16 @@ export const GROUP_PROJECTION = (modulesManager, withWorkers = true) => [ `policyholder ${modulesManager.getProjection('policyHolder.PolicyHolderPicker.projection')}`, `groupWorkers { totalCount - ${withWorkers ? `edges { + ${ + withWorkers + ? `edges { node { isDeleted, insuree ${modulesManager.getProjection('insuree.InsureePicker.projection')}, } - }` : ''} + }` + : '' +} }`, ]; @@ -336,9 +340,7 @@ export async function fetchAllPages(dispatch, query, variables, categories) { while (hasNextPage) { try { // eslint-disable-next-line no-await-in-loop - const response = await dispatch( - graphqlWithVariables(query, { ...variables, after }), - ); + const response = await dispatch(graphqlWithVariables(query, { ...variables, after })); const data = response?.payload?.data || {}; const pageInfos = categories.map((category) => processCategoryData(category, data, allData)); @@ -414,12 +416,11 @@ export async function fetchAllAvailableWorkers(dispatch, economicUnitCode, dateR } } `; - const response = await fetchAllPages( - dispatch, - query, - { economicUnitCode, dateRange }, - ['allAvailableWorkers', 'previousWorkers', 'previousDayWorkers'], - ); + const response = await fetchAllPages(dispatch, query, { economicUnitCode, dateRange }, [ + 'allAvailableWorkers', + 'previousWorkers', + 'previousDayWorkers', + ]); return response; } @@ -679,3 +680,76 @@ export function fetchPublicVoucherDetails(voucherUuid) { { voucherUuid }, ); } + +export function fetchVoucherDraftForm(modulesManager, economicUnitCode) { + return graphqlWithVariables( + ` + query getCurrentDraft($economicUnitCode: String) { + voucherFormDraft(policyholder_Code: $economicUnitCode) { + edges { + node { + policyholder ${modulesManager.getProjection('policyHolder.PolicyHolderPicker.projection')} + workers ${modulesManager.getProjection('insuree.InsureePicker.projection')} + dateRanges { startDate endDate } + } + } + } + } + `, + { economicUnitCode }, + ); +} + +export function createOrUpdateVoucherDraftForm( + voucherAssignment, + clientMutationLabel, + typeOfForm = DRAFT_FORM_TYPE.ASSIGNMENT, +) { + const { employer, workers, dateRanges } = voucherAssignment; + + const mutationInput = ` + ${`typeOfForm: "${typeOfForm}"`} + ${`economicUnitCode: "${employer.code}"`} + ${workers ? `workers: [${workers.map((worker) => `${decodeId(worker.id)}`).join(', ')}]` : 'workers: []'} + ${dateRanges ? `dateRanges: ${formatGraphQLDateRanges(dateRanges)}` : 'dateRanges: []'} + `; + + const mutation = formatMutation('createOrUpdateVoucherDraftForm', mutationInput, clientMutationLabel); + const requestedDateTime = new Date(); + + return graphql( + mutation.payload, + [ + REQUEST(ACTION_TYPE.MUTATION), + SUCCESS(ACTION_TYPE.CREATE_OR_UPDATE_ASSIGNMENT_DRAFT), + ERROR(ACTION_TYPE.MUTATION), + ], + { + actionType: ACTION_TYPE.CREATE_OR_UPDATE_ASSIGNMENT_DRAFT, + clientMutationId: mutation.clientMutationId, + clientMutationLabel, + requestedDateTime, + }, + ); +} + +export function deleteVoucherDraftForm(economicUnit, clientMutationLabel, typeOfForm = DRAFT_FORM_TYPE.ASSIGNMENT) { + const mutationInput = ` + ${`economicUnitCode: "${economicUnit.code}"`} + ${`typeOfForm: "${typeOfForm}"`} + `; + + const mutation = formatMutation('deleteVoucherDraftForm', mutationInput, clientMutationLabel); + const requestedDateTime = new Date(); + + return graphql( + mutation.payload, + [REQUEST(ACTION_TYPE.MUTATION), SUCCESS(ACTION_TYPE.DELETE_ASSIGNMENT_DRAFT), ERROR(ACTION_TYPE.MUTATION)], + { + actionType: ACTION_TYPE.DELETE_ASSIGNMENT_DRAFT, + clientMutationId: mutation.clientMutationId, + clientMutationLabel, + requestedDateTime, + }, + ); +} diff --git a/src/components/VoucherAcquirementGenericVoucher.js b/src/components/VoucherAcquirementGenericVoucher.js index 3f374a0..1c3c35a 100644 --- a/src/components/VoucherAcquirementGenericVoucher.js +++ b/src/components/VoucherAcquirementGenericVoucher.js @@ -100,7 +100,7 @@ function VoucherAcquirementGenericVoucher() { historyPush(modulesManager, history, REF_ROUTE_BILL, [billId]); dispatch( coreAlert( - formatMessage('menu.voucherAcquirementSuccess'), + formatMessage('menu.voucherAcquirement'), formatMessageWithValues('workerVoucher.VoucherAcquirementForm.genericVoucherConfirmation', { quantity: voucherAcquirement?.quantity, }), diff --git a/src/components/VoucherAcquirementSpecificWorker.js b/src/components/VoucherAcquirementSpecificWorker.js index 00ac740..1a06ac6 100644 --- a/src/components/VoucherAcquirementSpecificWorker.js +++ b/src/components/VoucherAcquirementSpecificWorker.js @@ -110,7 +110,7 @@ function VoucherAcquirementSpecificWorker() { historyPush(modulesManager, history, REF_ROUTE_BILL, [billId]); dispatch( coreAlert( - formatMessage('menu.voucherAcquirementSuccess'), + formatMessage('menu.voucherAcquirement'), formatMessage('workerVoucher.VoucherAcquirementForm.specificVoucherConfirmation'), ), ); diff --git a/src/components/VoucherAssignmentForm.js b/src/components/VoucherAssignmentForm.js index cb30ed4..7beb520 100644 --- a/src/components/VoucherAssignmentForm.js +++ b/src/components/VoucherAssignmentForm.js @@ -1,5 +1,6 @@ -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useState, useRef } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import _ from 'lodash'; import { Button, Divider, Grid, Paper, Tooltip, Typography, @@ -11,13 +12,15 @@ import { InfoButton, coreAlert, historyPush, - journalize, useHistory, useModulesManager, useTranslations, + parseData, } from '@openimis/fe-core'; -import { assignVouchers, voucherAssignmentValidation } from '../actions'; -import { MODULE_NAME, REF_ROUTE_WORKER_VOUCHERS, USER_ECONOMIC_UNIT_STORAGE_KEY } from '../constants'; +import { + assignVouchers, deleteVoucherDraftForm, fetchVoucherDraftForm, voucherAssignmentValidation, +} from '../actions'; +import { MODULE_NAME, REF_ROUTE_WORKER_VOUCHERS } from '../constants'; import AssignmentVoucherForm from './AssignmentVoucherForm'; import VoucherAssignmentConfirmModal from './VoucherAssignmentConfirmModal'; import VoucherAssignmentProgressTracker from './VoucherAssignmentProgressTracker'; @@ -47,7 +50,6 @@ export const useStyles = makeStyles((theme) => ({ })); function VoucherAssignmentForm() { - const prevSubmittingMutationRef = useRef(); const modulesManager = useModulesManager(); const dispatch = useDispatch(); const classes = useStyles(); @@ -58,8 +60,8 @@ function VoucherAssignmentForm() { const [assignmentSummaryLoading, setAssignmentSummaryLoading] = useState(false); const [isAssignmentLoading, setIsAssignmentLoading] = useState(false); const [isConfirmationModalOpen, setIsConfirmationModalOpen] = useState(false); - const { mutation, submittingMutation } = useSelector((state) => state.workerVoucher); const { economicUnit } = useSelector((state) => state.policyHolder); + const prevEconomicUnitRef = useRef(); const assignmentBlocked = (voucherAssignment) => !voucherAssignment?.workers?.length || !voucherAssignment?.dateRanges?.length; @@ -94,6 +96,7 @@ function VoucherAssignmentForm() { 'Assign Vouchers', ), ); + await dispatch(deleteVoucherDraftForm(economicUnit, 'Delete Voucher Draft')); historyPush(modulesManager, history, REF_ROUTE_WORKER_VOUCHERS); dispatch( coreAlert( @@ -110,26 +113,38 @@ function VoucherAssignmentForm() { setIsConfirmationModalOpen((prevState) => !prevState); }; - useEffect(() => { - if (prevSubmittingMutationRef.current && !submittingMutation) { - dispatch(journalize(mutation)); + useEffect(async () => { + if (_.isEqual(economicUnit, prevEconomicUnitRef.current)) { + return; } - }, [submittingMutation]); - - useEffect(() => { - prevSubmittingMutationRef.current = submittingMutation; - }); - - useEffect(() => { - const storedUserEconomicUnit = localStorage.getItem(USER_ECONOMIC_UNIT_STORAGE_KEY); - if (storedUserEconomicUnit) { - const userEconomicUnit = JSON.parse(storedUserEconomicUnit); - setVoucherAssignment((prevState) => ({ - ...prevState, - employer: userEconomicUnit, - workers: [], - dateRanges: [], + + try { + const response = await dispatch(fetchVoucherDraftForm(modulesManager, economicUnit.code)); + + if (response.error) { + // eslint-disable-next-line no-console + console.error(`[ERROR]: Error while fetching voucher draft form. ${response.error}`); + } + + const voucherFormDraft = parseData(response.payload.data.voucherFormDraft)?.[0]; + + if (!voucherFormDraft) { + setVoucherAssignment(() => ({ + employer: economicUnit, + workers: [], + dateRanges: [], + })); + return; + } + + setVoucherAssignment(() => ({ + employer: voucherFormDraft.policyholder, + workers: voucherFormDraft.workers, + dateRanges: voucherFormDraft.dateRanges, })); + } catch (error) { + // eslint-disable-next-line no-console + console.error(`[ERROR]: Error during assignment init. ${error}`); } }, [setVoucherAssignment, economicUnit]); diff --git a/src/components/VoucherAssignmentProgressTracker.js b/src/components/VoucherAssignmentProgressTracker.js index 05e9460..e854a15 100644 --- a/src/components/VoucherAssignmentProgressTracker.js +++ b/src/components/VoucherAssignmentProgressTracker.js @@ -1,12 +1,17 @@ -import React, { useState, useEffect, useRef } from 'react'; -import { makeStyles } from '@material-ui/styles'; +import React, { + useEffect, useRef, useState, useCallback, +} from 'react'; +import { useDispatch } from 'react-redux'; import _ from 'lodash'; +import _debounce from 'lodash/debounce'; -import { Grid, CircularProgress, Typography } from '@material-ui/core'; +import { CircularProgress, Grid, Typography } from '@material-ui/core'; import CheckCircleIcon from '@material-ui/icons/CheckCircle'; +import { makeStyles } from '@material-ui/styles'; -import { useTranslations } from '@openimis/fe-core'; -import { MODULE_NAME } from '../constants'; +import { useToast, useTranslations } from '@openimis/fe-core'; +import { createOrUpdateVoucherDraftForm } from '../actions'; +import { DEFAULT_DEBOUNCE_TIME, MODULE_NAME } from '../constants'; const useStyles = makeStyles((theme) => ({ container: { @@ -20,21 +25,37 @@ const useStyles = makeStyles((theme) => ({ function VoucherAssignmentProgressTracker({ voucherAssignment }) { const prevVoucherAssignment = useRef(); + const dispatch = useDispatch(); const classes = useStyles(); + const { showError } = useToast(); const { formatMessage } = useTranslations(MODULE_NAME); const [isSaving, setIsSaving] = useState(false); + const saveAssignmentDraft = useCallback( + _debounce(async (voucherAssignment) => { + try { + await dispatch(createOrUpdateVoucherDraftForm(voucherAssignment, 'Save Assignment Draft')); + } catch (error) { + showError('[ERROR]: Error while saving the progress.'); + // eslint-disable-next-line no-console + console.error(error); + } finally { + setIsSaving(false); + } + }, DEFAULT_DEBOUNCE_TIME * 2), + [dispatch, showError], + ); + useEffect(() => { - if (!_.isEqual(voucherAssignment, prevVoucherAssignment.current)) { + if ( + !_.isEqual(voucherAssignment, prevVoucherAssignment.current) + && !_.isEmpty(voucherAssignment) + ) { setIsSaving(true); prevVoucherAssignment.current = voucherAssignment; - - // TODO: After BE integration, call the API to save the voucher assignment - setTimeout(() => { - setIsSaving(false); - }, 1500); + saveAssignmentDraft(voucherAssignment); } - }, [voucherAssignment]); + }, [voucherAssignment, saveAssignmentDraft]); return ( diff --git a/src/constants.js b/src/constants.js index 73b275a..ded1284 100644 --- a/src/constants.js +++ b/src/constants.js @@ -112,3 +112,7 @@ export const UPLOAD_STAGE = { FILE_UPLOAD: 'FILE_UPLOAD', WORKER_UPLOAD: 'WORKER_UPLOAD', }; + +export const DRAFT_FORM_TYPE = { + ASSIGNMENT: 'ASSIGNMENT', +}; diff --git a/src/reducer.js b/src/reducer.js index f7dc043..3ef4e6f 100644 --- a/src/reducer.js +++ b/src/reducer.js @@ -38,6 +38,8 @@ export const ACTION_TYPE = { DELETE_GROUP: 'WORKER_VOUCHER_DELETE_GROUP', CREATE_GROUP: 'WORKER_VOUCHER_CREATE_GROUP', UPDATE_GROUP: 'WORKER_VOUCHER_UPDATE_GROUP', + CREATE_OR_UPDATE_ASSIGNMENT_DRAFT: 'WORKER_VOUCHER_CREATE_OR_UPDATE_ASSIGNMENT_DRAFT', + DELETE_ASSIGNMENT_DRAFT: 'WORKER_VOUCHER_DELETE_ASSIGNMENT_DRAFT', }; const STORE_STATE = { diff --git a/src/translations/en.json b/src/translations/en.json index 34f979f..93d9e6a 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -3,7 +3,6 @@ "workerVoucher.menu.workersList": "Workers", "workerVoucher.menu.groupList": "Groups", "workerVoucher.menu.voucherAcquirement": "Voucher Acquirement", - "workerVoucher.menu.voucherAcquirementSuccess": "Voucher Acquirement Success", "workerVoucher.menu.voucherAssignment": "Voucher Assignment", "workerVoucher.menu.voucherAssignmentSuccess": "Voucher Assignment Success", "workerVoucher.menu.priceManagement": "Voucher Price Management", @@ -55,8 +54,8 @@ "workerVoucher.WorkerDetailsPage.title": "{chfId} Worker Details Page", "workerVoucher.VoucherDetailsPanel.subtitle": "Voucher's General Information", "workerVoucher.VoucherAcquirementForm.subtitle": "Select voucher acquirement method", - "workerVoucher.VoucherAcquirementForm.genericVoucherConfirmation": "You have successfully acquired {quantity} voucher(s). The vouchers will be ready for assignment once the payment is confirmed. The payment gateway has opened in a new tab. If you accidentally closed it, please click „Pay with MPay” from the Bill view to reopen it.", - "workerVoucher.VoucherAcquirementForm.specificVoucherConfirmation": "You have successfully acquired vouchers for your selected workers. The vouchers will be ready for assignment once the payment is confirmed. The payment gateway has opened in a new tab. If you accidentally closed it, please click „Pay with MPay” from the Bill view to reopen it.", + "workerVoucher.VoucherAcquirementForm.genericVoucherConfirmation": "The last step is to complete the payment. The payment gateway has opened in a new tab. If you accidentally closed it, please click „Pay with MPay” from the Bill view to reopen it.", + "workerVoucher.VoucherAcquirementForm.specificVoucherConfirmation": "The last step is to complete the payment. The payment gateway has opened in a new tab. If you accidentally closed it, please click „Pay with MPay” from the Bill view to reopen it.", "workerVoucher.VoucherAssignmentForm.assignmentConfirmation": "You have successfully assigned voucher(s) to the selected worker(s).", "workerVoucher.acquirement.method": "Acquirement Method", "workerVoucher.acquirement.method.GENERIC_VOUCHER": "Non-Personal Voucher", From dd7f3c38df66da23c8dbbb8dbbea24f870b0604b Mon Sep 17 00:00:00 2001 From: olewandowski1 Date: Wed, 30 Oct 2024 16:07:26 +0100 Subject: [PATCH 3/3] OM-3325: fix sonar --- src/actions.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/actions.js b/src/actions.js index 631a1ee..7bf2e62 100644 --- a/src/actions.js +++ b/src/actions.js @@ -706,11 +706,12 @@ export function createOrUpdateVoucherDraftForm( typeOfForm = DRAFT_FORM_TYPE.ASSIGNMENT, ) { const { employer, workers, dateRanges } = voucherAssignment; + const workerIds = workers ? workers.map((worker) => decodeId(worker.id)).join(', ') : EMPTY_STRING; const mutationInput = ` - ${`typeOfForm: "${typeOfForm}"`} - ${`economicUnitCode: "${employer.code}"`} - ${workers ? `workers: [${workers.map((worker) => `${decodeId(worker.id)}`).join(', ')}]` : 'workers: []'} + typeOfForm: "${typeOfForm}" + economicUnitCode: "${employer.code}" + workers: [${workerIds}] ${dateRanges ? `dateRanges: ${formatGraphQLDateRanges(dateRanges)}` : 'dateRanges: []'} `;