From 8053093c0a20cff33177dcf47b82791101ea9d61 Mon Sep 17 00:00:00 2001 From: Bilal Catic Date: Thu, 25 Jul 2019 14:40:33 +0200 Subject: [PATCH] prevent locking upload response; show processing status on upload screen --- .../UploadDLockData/components/FileUpload.js | 29 ++- .../components/UploadResults.js | 20 +- .../src/store/actions/integrationActions.js | 14 ++ client/src/store/constants.js | 4 + .../store/reducers/checkProcessingReducer.js | 38 ++++ client/src/store/reducers/index.js | 2 + constants/constants.js | 1 + controllers/doorLock.js | 186 ++++++++++-------- controllers/integration.js | 16 ++ .../20190725110004-add-processings-table.js | 26 +++ models/processing.js | 11 ++ routes/index.js | 5 +- services/integration/processingStatus.js | 46 +++++ 13 files changed, 312 insertions(+), 86 deletions(-) create mode 100644 client/src/store/reducers/checkProcessingReducer.js create mode 100644 migrations/20190725110004-add-processings-table.js create mode 100644 models/processing.js create mode 100644 services/integration/processingStatus.js diff --git a/client/src/scenes/UploadDLockData/components/FileUpload.js b/client/src/scenes/UploadDLockData/components/FileUpload.js index e0f9b23..30bd4f8 100644 --- a/client/src/scenes/UploadDLockData/components/FileUpload.js +++ b/client/src/scenes/UploadDLockData/components/FileUpload.js @@ -4,7 +4,7 @@ import { Form } from 'semantic-ui-react'; import UnknownMapping from './UnknownMapping'; -import { uploadDoorLockData, fetchMappings } from '../../../store/actions'; +import { uploadDoorLockData, fetchMappings, checkProcessing } from '../../../store/actions'; class FileUpload extends Component { constructor(props) { @@ -20,8 +20,9 @@ class FileUpload extends Component { } componentDidMount() { - const { fetchMappings } = this.props; + const { fetchMappings, checkProcessing } = this.props; fetchMappings(); + checkProcessing(); } componentWillReceiveProps(nextProps, nextContext) { @@ -36,6 +37,7 @@ class FileUpload extends Component { this.setState({unknownMappings: filteredUnknownMappings}); } + extractMappingFromFileName(fileName) { const contentBetweenBracketsRegex = /\[(.*?)\]/; const rawContent = fileName.match(contentBetweenBracketsRegex)[1]; @@ -52,7 +54,11 @@ class FileUpload extends Component { const { existingMappings } = mappings; - return existingMappings.find(mapping => (mapping.officeSlug === officeSlug) && (mapping.resourceSlug === resourceSlug)); + if (existingMappings && Array.isArray(existingMappings)){ + return existingMappings.find(mapping => (mapping.officeSlug === officeSlug) && (mapping.resourceSlug === resourceSlug)); + }else{ + return false; + } } onFileChange(event) { @@ -85,14 +91,24 @@ class FileUpload extends Component { } }; + refreshProcessingStatus = (event) => { + event.preventDefault(); + const { checkProcessing } = this.props; + checkProcessing(); + }; + render() { - const { pendingUpload } = this.props; + const { pendingUpload, pendingStatus, processingStatus } = this.props; const { unknownMappings, files } = this.state; - const uploadDisabled = pendingUpload || unknownMappings.length > 0 || !files; + const processing = processingStatus && processingStatus.processing ? processingStatus.processing : false; + + const uploadDisabled = pendingStatus || processing || pendingUpload || unknownMappings.length > 0 || !files; return (
+ {processing &&

Processing in progress. Please try again in a few minutes

} + {processing && Refresh} ({ pendingMappings: state.mappingsData.pending, mappings: state.mappingsData.result, addedMapping: state.addMapping.result, + pendingStatus: state.checkProcessing.pending, + processingStatus: state.checkProcessing.result, }); const mapDispatchToProps = (dispatch) => ({ uploadDoorLockData: (doorLockDataFiles) => uploadDoorLockData(dispatch, doorLockDataFiles), fetchMappings: () => fetchMappings(dispatch), + checkProcessing: () => checkProcessing(dispatch), }); export default connect(mapStateToProps, mapDispatchToProps)(FileUpload); diff --git a/client/src/scenes/UploadDLockData/components/UploadResults.js b/client/src/scenes/UploadDLockData/components/UploadResults.js index 6a6bbfa..c414a80 100644 --- a/client/src/scenes/UploadDLockData/components/UploadResults.js +++ b/client/src/scenes/UploadDLockData/components/UploadResults.js @@ -2,7 +2,20 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { Loader, Message, Tab, Label, Menu } from 'semantic-ui-react'; +import { checkProcessing } from '../../../store/actions'; + class UploadResults extends Component { + + componentDidUpdate(prevProps, prevState, snapshot) { + const previousPending = prevProps.pending ? prevProps.pending : false; + const newPending = this.props.pending ? this.props.pending : false; + + if (previousPending && !newPending){ + const { checkProcessing } = this.props; + checkProcessing(); + } + } + render(){ const {pending, result, error} = this.props; @@ -80,7 +93,10 @@ const mapStateToProps = (state) => ({ pending: state.doorLockData.pending, result: state.doorLockData.result, error: state.doorLockData.error, - }); -export default connect(mapStateToProps)(UploadResults); +const mapDispatchToProps = (dispatch) => ({ + checkProcessing: () => checkProcessing(dispatch), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(UploadResults); diff --git a/client/src/store/actions/integrationActions.js b/client/src/store/actions/integrationActions.js index 79a9068..3cb1ef1 100644 --- a/client/src/store/actions/integrationActions.js +++ b/client/src/store/actions/integrationActions.js @@ -17,6 +17,9 @@ import { ADD_FEES_TO_ORD_PENDING, ADD_FEES_TO_ORD_SUCCESS, ADD_FEES_TO_ORD_FAILED, + CHECK_PROCESSING_PENDING, + CHECK_PROCESSING_SUCCESS, + CHECK_PROCESSING_FAILED, } from '../constants'; import API from '../../utilities/api'; @@ -95,3 +98,14 @@ export const addFeesToOrd = (dispatch, dateRange, memberIds) => { dispatch({type: ADD_FEES_TO_ORD_FAILED, payload: error.response}); }); }; + +export const checkProcessing = (dispatch) => { + dispatch({type: CHECK_PROCESSING_PENDING}); + API.get('integration/processing') + .then(response => { + dispatch({type: CHECK_PROCESSING_SUCCESS, payload: response.data}); + }) + .catch(error => { + dispatch({type: CHECK_PROCESSING_FAILED, payload: error.response}); + }); +}; diff --git a/client/src/store/constants.js b/client/src/store/constants.js index 6d67aef..f2c950a 100644 --- a/client/src/store/constants.js +++ b/client/src/store/constants.js @@ -25,3 +25,7 @@ export const FETCH_MEMBER_INCIDENTS_FAILED = 'FETCH_MEMBER_INCIDENTS_FAILED'; export const ADD_FEES_TO_ORD_PENDING = 'ADD_FEES_TO_ORD_PENDING'; export const ADD_FEES_TO_ORD_SUCCESS = 'ADD_FEES_TO_ORD_SUCCESS'; export const ADD_FEES_TO_ORD_FAILED = 'ADD_FEES_TO_ORD_FAILED'; + +export const CHECK_PROCESSING_PENDING = 'CHECK_PROCESSING_PENDING'; +export const CHECK_PROCESSING_SUCCESS = 'CHECK_PROCESSING_SUCCESS'; +export const CHECK_PROCESSING_FAILED = 'CHECK_PROCESSING_FAILED'; diff --git a/client/src/store/reducers/checkProcessingReducer.js b/client/src/store/reducers/checkProcessingReducer.js new file mode 100644 index 0000000..ad649de --- /dev/null +++ b/client/src/store/reducers/checkProcessingReducer.js @@ -0,0 +1,38 @@ +import { + CHECK_PROCESSING_PENDING, + CHECK_PROCESSING_SUCCESS, + CHECK_PROCESSING_FAILED, +} from '../constants'; + +const initialState = { + pending: false, + result: null, + error: null, +}; + +export const checkProcessing = (state, action) => { + state = state || initialState; + action = action || {}; + + switch(action.type){ + case CHECK_PROCESSING_PENDING: + return Object.assign({}, state, { + pending: true, + error: null, + }); + case CHECK_PROCESSING_SUCCESS: + return Object.assign({}, state, { + pending: false, + result: action.payload, + error: null, + }); + case CHECK_PROCESSING_FAILED: + return Object.assign({}, state, { + pending: false, + result: {}, + error: action.payload, + }); + default: + return state; + } +}; diff --git a/client/src/store/reducers/index.js b/client/src/store/reducers/index.js index 3eb9e9f..27640e6 100644 --- a/client/src/store/reducers/index.js +++ b/client/src/store/reducers/index.js @@ -7,6 +7,7 @@ import { incidentsReport } from './incidentsReportReducer'; import { membersList } from './membersListReducer'; import { memberIncidents} from './memberIncidentsReducer'; import { addFeesStatus } from './addFeesToOrdReducer'; +import { checkProcessing } from './checkProcessingReducer'; export const rootReducer = combineReducers({ doorLockData, @@ -16,5 +17,6 @@ export const rootReducer = combineReducers({ membersList, memberIncidents, addFeesStatus, + checkProcessing, }); diff --git a/constants/constants.js b/constants/constants.js index 1d06ee2..95fdd47 100644 --- a/constants/constants.js +++ b/constants/constants.js @@ -62,6 +62,7 @@ const officeRnDAPIErrors = { FAILED_TO_ADD_FEES: 'Failed to add fees in ORD', }; const integrationServiceErrors = { + PROCESSING_TRY_AGAIN: 'Incident calculations are in progress. Please try again in a few minutes', IMPORT_SUCCESSFUL_CALCULATION_FAILED: 'Import succeeded but fetching reservations and calculating charges failed', FAILED_TO_SAVE_BOOKINGS: 'Failed to save booking reservations', FAILED_TO_SAVE_DOOR_LOCK_ENTRIES: 'Failed to save door lock entries', diff --git a/controllers/doorLock.js b/controllers/doorLock.js index a75cf2c..6861308 100644 --- a/controllers/doorLock.js +++ b/controllers/doorLock.js @@ -5,96 +5,126 @@ const { fetchAllBookings, writeBookingReservation } = require('../services/offic const { calculateDoorLockCharges } = require('../services/integration/doorLockCharges'); const { integrationServiceErrors } = require('../constants/constants'); const { checkBookingChanges } = require('../services/integration/checkBookingChange'); +const { checkIfProcessing, setStartProcessing, setDoneProcessing } = require('../services/integration/processingStatus'); const IncomingForm = require('formidable').IncomingForm; const uploadDoorLockData = (req, res) => { - const form = new IncomingForm(); - const fileParsers = []; + checkIfProcessing() + .then((processing) => { + if (processing){ + res.status(500).send(integrationServiceErrors.PROCESSING_TRY_AGAIN); + }else{ + const form = new IncomingForm(); + const fileParsers = []; - form.on('file', (field, file) => { - if (file && file.type === 'text/csv') { - fileParsers.push(parseDoorLockDataFile(file)); - } - }); - - form.on('end', () => { - Promise.all(fileParsers) - .then((parserResults) => { - const parsedData = []; - const parserErrors = []; - const unknownMembers = []; - - parserResults.forEach((parserResult) => { - parsedData.push(...parserResult.parsedData); - parserErrors.push(...parserResult.errors); - - parserResult.unknownMembers.forEach((newUnknownMember) => { - // Check if member is already labeled as unknown in different file - if (!unknownMembers.find((unknownMember) => unknownMember.details === newUnknownMember.details)){ - unknownMembers.push(newUnknownMember); - } - }); + form.on('file', (field, file) => { + if (file && file.type === 'text/csv') { + fileParsers.push(parseDoorLockDataFile(file)); + } }); - const asyncWriteJobs = []; + form.on('end', () => { + Promise.all(fileParsers) + .then((parserResults) => { + const parsedData = []; + const parserErrors = []; + const unknownMembers = []; - parsedData.forEach((entry) => asyncWriteJobs.push(writeDoorLockEvent(entry))); + parserResults.forEach((parserResult) => { + parsedData.push(...parserResult.parsedData); + parserErrors.push(...parserResult.errors); - Promise.all(asyncWriteJobs) - .then(() => { - checkBookingChanges() - .then(() => { - calculateDoorLockCharges() - .then(() => { - res.json({ - parsedData, - parserErrors, - unknownMembers - }); - }) - .catch((error) => { - console.log('Error : ', error); - res.status(500).send(integrationServiceErrors.IMPORT_SUCCESSFUL_CALCULATION_FAILED); - }); - }) - .catch((error) => { - console.log('Error : ', error); - res.status(500).send(integrationServiceErrors.IMPORT_SUCCESSFUL_CALCULATION_FAILED); + parserResult.unknownMembers.forEach((newUnknownMember) => { + // Check if member is already labeled as unknown in different file + if (!unknownMembers.find((unknownMember) => unknownMember.details === newUnknownMember.details)){ + unknownMembers.push(newUnknownMember); + } + }); }); - }) - .catch((error) => { - console.log(integrationServiceErrors.FAILED_TO_SAVE_DOOR_LOCK_ENTRIES); - console.log(error); - res.status(500).send(integrationServiceErrors.FAILED_TO_SAVE_DATA_GENERIC); - }); - /*fetchAllBookings() - .then((bookingEntries) => { - const asyncJobs = []; - bookingEntries.forEach((bookingEntry) => asyncJobs.push(writeBookingReservation(bookingEntry))); + const asyncWriteJobs = []; - Promise.all(asyncJobs) - .then(() => { - calculateDoorLockCharges(); - }) - .catch((error) => { - console.log('Error updating booking reservations : '); - console.log(error); - }) - }) - .catch((error) => { - console.log(integrationServiceErrors.FAILED_TO_SAVE_BOOKINGS); - console.log(error); - return; - }); - */ - }) - .catch((error) => { - res.status(500).send(error); - }); - }); - form.parse(req); + parsedData.forEach((entry) => asyncWriteJobs.push(writeDoorLockEvent(entry))); + + Promise.all(asyncWriteJobs) + .then(() => { + res.json({ + parsedData, + parserErrors, + unknownMembers + }); + + setStartProcessing() + .then(() => { + checkBookingChanges() + .then(() => { + calculateDoorLockCharges() + .then(() => { + setDoneProcessing() + .catch((error) => { + console.log('Error in process done indication'); + console.log(error); + }) + }) + .catch((error) => { + console.log('Error : ', error); + setDoneProcessing(); + // res.status(500).send(integrationServiceErrors.IMPORT_SUCCESSFUL_CALCULATION_FAILED); + }); + + }) + .catch((error) => { + console.log('Error : ', error); + setDoneProcessing(); + // res.status(500).send(integrationServiceErrors.IMPORT_SUCCESSFUL_CALCULATION_FAILED); + }); + }) + .catch((error) => { + console.log('Error in processing indicator'); + console.log(error); + setDoneProcessing(); + }); + }) + .catch((error) => { + console.log(integrationServiceErrors.FAILED_TO_SAVE_DOOR_LOCK_ENTRIES); + console.log(error); + res.status(500).send(integrationServiceErrors.FAILED_TO_SAVE_DATA_GENERIC); + }); + + /*fetchAllBookings() + .then((bookingEntries) => { + const asyncJobs = []; + bookingEntries.forEach((bookingEntry) => asyncJobs.push(writeBookingReservation(bookingEntry))); + + Promise.all(asyncJobs) + .then(() => { + calculateDoorLockCharges(); + }) + .catch((error) => { + console.log('Error updating booking reservations : '); + console.log(error); + }) + }) + .catch((error) => { + console.log(integrationServiceErrors.FAILED_TO_SAVE_BOOKINGS); + console.log(error); + return; + }); + */ + }) + .catch((error) => { + res.status(500).send(error); + }); + }); + form.parse(req); + } + }) + .catch((error) => { + console.log('Error while checking processing : '); + console.log(error); + res.status(500).send('') + }); }; module.exports = { diff --git a/controllers/integration.js b/controllers/integration.js index ffe2ef0..3c83bed 100644 --- a/controllers/integration.js +++ b/controllers/integration.js @@ -5,6 +5,7 @@ const { getAllIncidents } = require('../services/integration/reports'); const { getMembersFeesForDateRange } = require('../services/integration/invoiceIntegration'); const { deleteFeesFromORD, addFeesToORD } = require('../services/officeRnD/fees'); const { checkBookingChanges } = require('../services/integration/checkBookingChange'); +const { checkIfProcessing } = require('../services/integration/processingStatus'); const getKnownOfficeResourceMappings = (req, res) => { const dataToFetch = [getMappingsFromDatabase(), fetchOffices(), fetchResources() ]; @@ -111,10 +112,25 @@ const addFees = (req, res) => { } }; +const checkProcessingStatus = (req, res) => { + checkIfProcessing() + .then((processing) => { + res.send({ + processing + }) + }) + .catch((error) => { + console.log('Error checking if processing '); + console.log(error); + res.status(500).send(error); + }); +}; + module.exports = { getKnownOfficeResourceMappings, addNewMapping, getAllIncidentsController, getMemberIncidents, addFees, + checkProcessingStatus, }; diff --git a/migrations/20190725110004-add-processings-table.js b/migrations/20190725110004-add-processings-table.js new file mode 100644 index 0000000..06e3cd0 --- /dev/null +++ b/migrations/20190725110004-add-processings-table.js @@ -0,0 +1,26 @@ +'use strict'; + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.createTable('processings', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + processing: Sequelize.BOOLEAN, + createdAt: { + allowNull: false, + type: Sequelize.DATE + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE + } + }); + }, + down: (queryInterface, Sequelize) => { + return queryInterface.dropTable('processings'); + } +}; diff --git a/models/processing.js b/models/processing.js new file mode 100644 index 0000000..0163891 --- /dev/null +++ b/models/processing.js @@ -0,0 +1,11 @@ +'use strict'; + +module.exports = (sequelize, DataTypes) => { + const processing = sequelize.define('processing', { + processing: DataTypes.BOOLEAN, + }, {}); + processing.associate = function(models) { + // associations can be defined here + }; + return processing; +}; diff --git a/routes/index.js b/routes/index.js index 11efed8..46284bd 100644 --- a/routes/index.js +++ b/routes/index.js @@ -8,7 +8,8 @@ const { addNewMapping, getAllIncidentsController, getMemberIncidents, - addFees + addFees, + checkProcessingStatus, } = require('../controllers/integration'); const { calculateDoorLockCharges } = require('../services/integration/doorLockCharges'); @@ -29,6 +30,8 @@ router.get('/officeRnD/membersList', fetchMembersList); router.post('/integration/addFees', addFees); +router.get('/integration/processing', checkProcessingStatus); + // temporary route, manually trigger door lock charge calculations router.get('/calculate', (req, res) => { calculateDoorLockCharges(); res.send();}); diff --git a/services/integration/processingStatus.js b/services/integration/processingStatus.js new file mode 100644 index 0000000..6c7b5de --- /dev/null +++ b/services/integration/processingStatus.js @@ -0,0 +1,46 @@ +'use strict'; + +const db = require('../../models/index'); + +const setProcessingValue = (value) => { + return new Promise((resolve, reject) => { + const values = {processing: value}; + db.processing.update(values, {where:{}}) + .then(() => { + resolve(true); + }) + .catch((error) => reject(error)); + }); +}; + +const setStartProcessing = () => { + return setProcessingValue(true); +}; + +const setDoneProcessing = () => { + return setProcessingValue(false); +}; + +const checkIfProcessing = () => { + return new Promise((resolve, reject) => { + db.processing.findAll() + .then((results) => { + if (results && results.length > 0){ + resolve(results[0].getDataValue('processing')); + }else{ + db.processing.bulkCreate([{processing: false}]) + .then(() => { + resolve(false); + }) + .catch((error) => reject(error)); + } + }) + .catch((error) => reject(error)); + }); +}; + +module.exports = { + setStartProcessing, + setDoneProcessing, + checkIfProcessing +};