diff --git a/src/actions.js b/src/actions.js index 5538f01..1eea91e 100644 --- a/src/actions.js +++ b/src/actions.js @@ -22,6 +22,7 @@ const ENROLLMENT_SUMMARY_FULL_PROJECTION = () => [ 'numberOfIndividualsAssignedToProgramme', 'numberOfIndividualsNotAssignedToProgramme', 'numberOfIndividualsAssignedToSelectedProgramme', + 'numberOfIndividualsToUpload', ]; export function fetchWorkflows() { @@ -72,6 +73,13 @@ const GROUP_HISTORY_FULL_PROJECTION = GROUP_FULL_PROJECTION.filter( (item) => item !== 'head {firstName, lastName}', ); +const UPLOAD_HISTORY_FULL_PROJECTION = () => [ + 'id', + 'uuid', + 'workflow', + 'dataUpload {uuid, dateCreated, dateUpdated, sourceName, sourceType, status, error }', +]; + export function fetchIndividualEnrollmentSummary(params) { const payload = formatQuery( 'individualEnrollmentSummary', @@ -116,6 +124,11 @@ export function fetchGroupHistory(params) { return graphql(payload, ACTION_TYPE.SEARCH_GROUP_HISTORY); } +export function fetchUploadHistory(params) { + const payload = formatPageQueryWithCount('individualDataUploadHistory', params, UPLOAD_HISTORY_FULL_PROJECTION()); + return graphql(payload, ACTION_TYPE.GET_INDIVIDUAL_UPLOAD_HISTORY); +} + export function deleteIndividual(individual, clientMutationLabel) { const individualUuids = `ids: ["${individual?.id}"]`; const mutation = formatMutation('deleteIndividual', individualUuids, clientMutationLabel); diff --git a/src/components/CollapsableErrorList.js b/src/components/CollapsableErrorList.js new file mode 100644 index 0000000..4309079 --- /dev/null +++ b/src/components/CollapsableErrorList.js @@ -0,0 +1,64 @@ +import React, { useState } from 'react'; +import { injectIntl } from 'react-intl'; +import ExpandLess from '@material-ui/icons/ExpandLess'; +import ExpandMore from '@material-ui/icons/ExpandMore'; +import { + formatMessage, +} from '@openimis/fe-core'; +import { + ListItem, + ListItemText, + Collapse, +} from '@material-ui/core'; +import { withTheme, withStyles } from '@material-ui/core/styles'; + +const styles = (theme) => ({ + item: theme.paper.item, +}); + +function CollapsableErrorList({ + intl, + errors, +}) { + const [isExpanded, setIsExpanded] = useState(false); + + const handleOpen = () => { + setIsExpanded(!isExpanded); + }; + + if (!errors || !Object.keys(errors).length) { + return ( + + + + ); + } + + return ( + <> + + + {isExpanded ? : } + + + { JSON.stringify(errors) } + + + ); +} + +export default injectIntl( + withTheme( + withStyles(styles)(CollapsableErrorList), + ), +); diff --git a/src/components/dialogs/AdvancedCriteriaForm.js b/src/components/dialogs/AdvancedCriteriaForm.js index 213b4cd..55a0722 100644 --- a/src/components/dialogs/AdvancedCriteriaForm.js +++ b/src/components/dialogs/AdvancedCriteriaForm.js @@ -264,7 +264,7 @@ function AdvancedCriteriaForm({ - + {formatMessage(intl, 'individual', 'individual.enrollment.totalNumberOfIndividuals')} @@ -274,7 +274,7 @@ function AdvancedCriteriaForm({ - + {formatMessage(intl, 'individual', 'individual.enrollment.numberOfSelectedIndividuals')} @@ -284,7 +284,7 @@ function AdvancedCriteriaForm({ - + {formatMessage(intl, 'individual', 'individual.enrollment.numberOfIndividualsAssignedToProgramme')} @@ -294,7 +294,7 @@ function AdvancedCriteriaForm({ - + {formatMessage(intl, 'individual', 'individual.enrollment.numberOfIndividualsNotAssignedToProgramme')} @@ -304,7 +304,7 @@ function AdvancedCriteriaForm({ - + {/* eslint-disable-next-line max-len */} @@ -315,6 +315,17 @@ function AdvancedCriteriaForm({ + + + + {/* eslint-disable-next-line max-len */} + {formatMessage(intl, 'individual', 'individual.enrollment.numberOfIndividualsToBeUploaded')} + + + {enrollmentSummary.numberOfIndividualsToUpload} + + + diff --git a/src/components/dialogs/IndividualsHistoryUploadDialog.js b/src/components/dialogs/IndividualsHistoryUploadDialog.js new file mode 100644 index 0000000..79dffc9 --- /dev/null +++ b/src/components/dialogs/IndividualsHistoryUploadDialog.js @@ -0,0 +1,248 @@ +import React, { useEffect, useState } from 'react'; +import { injectIntl } from 'react-intl'; +import Button from '@material-ui/core/Button'; +import Dialog from '@material-ui/core/Dialog'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import { + formatMessage, + formatDateFromISO, + ProgressOrError, + withModulesManager, +} from '@openimis/fe-core'; +import { + TableHead, + TableBody, + Table, + TableCell, + TableRow, + TableFooter, + TableContainer, + Paper, + MenuItem, +} from '@material-ui/core'; +import { withTheme, withStyles } from '@material-ui/core/styles'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import CollapsableErrorList from '../CollapsableErrorList'; +import { fetchUploadHistory } from '../../actions'; +import { downloadInvalidItems } from '../../utils'; +import { UPLOAD_STATUS } from '../../constants'; + +const styles = (theme) => ({ + item: theme.paper.item, +}); + +function IndividualsUploadHistoryDialog({ + modulesManager, + intl, + classes, + fetchUploadHistory, + history, + fetchedHistory, + fetchingHistory, +}) { + const [isOpen, setIsOpen] = useState(false); + const [records, setRecords] = useState([]); + + const handleOpen = () => { + setIsOpen(true); + }; + + const handleClose = () => { + setIsOpen(false); + }; + + const downloadInvalidItemsFromUpload = (uploadId) => { + downloadInvalidItems(uploadId); + }; + + useEffect(() => { + if (isOpen) { + const params = []; + fetchUploadHistory(params); + } + }, [isOpen]); + + useEffect(() => { + setRecords(history); + }, [fetchedHistory]); + + return ( + <> + + {formatMessage(intl, 'individual', 'individual.upload.uploadHistoryTable.buttonLabel')} + + + + {formatMessage(intl, 'individual', 'individual.upload.uploadHistoryTable.label')} + + +
+ + + + + + + {formatMessage( + intl, + 'individual', + 'individual.upload.uploadHistoryTable.workflow', + )} + + + {formatMessage( + intl, + 'individual', + 'individual.upload.uploadHistoryTable.dateCreated', + )} + + + {formatMessage( + intl, + 'individual', + 'individual.upload.uploadHistoryTable.sourceType', + )} + + + {formatMessage( + intl, + 'individual', + 'individual.upload.uploadHistoryTable.sourceName', + )} + + + {formatMessage( + intl, + 'individual', + 'individual.upload.uploadHistoryTable.status', + )} + + + {formatMessage( + intl, + 'individual', + 'individual.upload.uploadHistoryTable.error', + )} + + + + + + + {records.map((item) => ( + + + { item.workflow } + + + { formatDateFromISO(modulesManager, intl, item.dataUpload.dateCreated) } + + + { item.dataUpload.sourceType} + + + { item.dataUpload.sourceName} + + + { item.dataUpload.status} + + + + + + {[ + UPLOAD_STATUS.WAITING_FOR_VERIFICATION, + UPLOAD_STATUS.PARTIAL_SUCCESS].includes(item.dataUpload.status) && ( + + )} + + + ))} + + +
+
+
+
+ +
+
+ +
+
+
+
+ + ); +} + +const mapStateToProps = (state) => ({ + rights: !!state.core && !!state.core.user && !!state.core.user.i_user ? state.core.user.i_user.rights : [], + confirmed: state.core.confirmed, + history: state.individual.individualDataUploadHistory, + fetchedHistory: state.individual.fetchedIndividualDataUploadHistory, + fetchingHistory: state.individual.fetchingIndividualDataUploadHistory, +}); + +const mapDispatchToProps = (dispatch) => bindActionCreators({ + fetchUploadHistory, +}, dispatch); + +export default injectIntl( + withModulesManager(withTheme( + withStyles(styles)( + connect(mapStateToProps, mapDispatchToProps)(IndividualsUploadHistoryDialog), + ), + )), +); diff --git a/src/components/dialogs/IndividualsUploadDialog.js b/src/components/dialogs/IndividualsUploadDialog.js index ca2f320..8751db2 100644 --- a/src/components/dialogs/IndividualsUploadDialog.js +++ b/src/components/dialogs/IndividualsUploadDialog.js @@ -17,6 +17,7 @@ import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import WorkflowsPicker from '../../pickers/WorkflowsPicker'; import { fetchWorkflows } from '../../actions'; +import IndividualsHistoryUploadDialog from './IndividualsHistoryUploadDialog'; const styles = (theme) => ({ item: theme.paper.item, @@ -105,6 +106,7 @@ function IndividualsUploadDialog({ > {formatMessage(intl, 'individual', 'individual.upload.buttonLabel')} + ({ + ...data, + id: decodeId(data.id), + dataUpload: { ...data.dataUpload, error: JSON.parse(data.dataUpload.error) }, + })) || [], + individualDataUploadHistoryPageInfo: pageInfo(action.payload.data.individualDataUploadHistory), + errorIndividualDataUploadHistory: formatGraphQLError(action.payload), + }; + case ERROR(ACTION_TYPE.GET_INDIVIDUAL_UPLOAD_HISTORY): + return { + ...state, + fetchingIndividualDataUploadHistory: false, + errorIndividualDataUploadHistory: formatServerError(action.payload), + }; case REQUEST(ACTION_TYPE.MUTATION): return dispatchMutationReq(state, action); case ERROR(ACTION_TYPE.MUTATION): diff --git a/src/translations/en.json b/src/translations/en.json index f48d6b2..87190ef 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -35,7 +35,17 @@ "buttonLabel": "UPLOAD", "label": "Upload Individuals", "workflowPicker": "Workflow", - "cancel": "Cancel" + "cancel": "Cancel", + "uploadHistoryTable": { + "workflow": "Workflow", + "dateCreated": "Date Created", + "sourceType": "Source Type", + "sourceName": "Source Name", + "status": "Status", + "error": "Error", + "buttonLabel": "UPLOAD HISTORY", + "label": "Upload History Table" + } }, "enrollment": { "buttonLabel": "ENROLLMENT", @@ -52,9 +62,10 @@ "mutationLabel": "Enrollment has been confirmed", "numberOfSelectedIndividuals": "Total Number Of Selected Individuals", "totalNumberOfIndividuals": "Total Number of Individuals", - "numberOfIndividualsAssignedToProgramme": "Number of Individuals Assigned To Any Programme", - "numberOfIndividualsNotAssignedToProgramme": "Number of Individuals Unassigned to Any Program", - "numberOfIndividualsAssignedToSelectedProgramme": "Number Of Individuals Assigned to Selected Programme", + "numberOfIndividualsAssignedToProgramme": "Number of Individuals Already Assigned To Any Programme", + "numberOfIndividualsNotAssignedToProgramme": "Number of Individuals Without Assignment to Program", + "numberOfIndividualsAssignedToSelectedProgramme": "Number Of Individuals Already Assigned to Selected Programme", + "numberOfIndividualsToBeUploaded": "Number Of Individuals to be Uploaded", "confirmMessageDialog": "Are you sure you want to confirm the enrollment of the selected individuals into the {benefitPlanName} Programme?" }, "saveButton.tooltip.enabled": "Save changes", diff --git a/src/utils.js b/src/utils.js index 626d6a3..a98243e 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,3 +1,5 @@ +import { baseApiUrl } from '@openimis/fe-core'; + export function isBase64Encoded(str) { // Base64 encoded strings can only contain characters from [A-Za-z0-9+/=] const base64RegExp = /^[A-Za-z0-9+/=]+$/; @@ -7,3 +9,22 @@ export function isBase64Encoded(str) { export function isEmptyObject(obj) { return Object.keys(obj).length === 0; } + +export function downloadInvalidItems(uploadId) { + const url = new URL( + `${window.location.origin}${baseApiUrl}/individual/download_invalid_items/?upload_id=${uploadId}`, + ); + fetch(url) + .then((response) => response.blob()) + .then((blob) => { + const link = document.createElement('a'); + link.href = URL.createObjectURL(blob); + link.download = 'individuals_invalid_items.csv'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }) + .catch((error) => { + console.error('Export failed, reason: ', error); + }); +}