From 8e4eb0cf1ffaeae01bae223746bdc3df70450f81 Mon Sep 17 00:00:00 2001 From: Senad Uka Date: Mon, 3 Jun 2019 18:04:42 +0200 Subject: [PATCH] Make lock charges calculation functional for happy path --- client/package.json | 1 + .../UploadDLockData/components/FileUpload.js | 59 ++++++ .../components/UploadResults.js | 86 +++++++++ client/src/scenes/UploadDLockData/index.js | 49 ++--- client/src/store/actions/doorLockActions.js | 23 ++- client/src/store/reducers/doorLockReducers.js | 7 +- client/src/utilities/api.js | 5 + client/yarn.lock | 9 +- constants/constants.js | 30 +++ controllers/doorLock.js | 54 +++++- helpers/api.js | 12 ++ .../20190529103954-create-door-lock-events.js | 33 ++++ ...90530140559-create-booking-reservations.js | 30 +++ .../20190531154129-door-lock-incidents.js | 40 ++++ models/bookingReservation.js | 15 ++ models/doorLockEvent.js | 20 ++ package-lock.json | 177 ++++++++++++++++-- package.json | 6 +- routes/index.js | 2 +- services/doorLock.js | 150 +++++++++++++++ services/officeRnD/bookings.js | 40 ++++ services/officeRnD/members.js | 33 ++++ 22 files changed, 820 insertions(+), 61 deletions(-) create mode 100644 client/src/scenes/UploadDLockData/components/FileUpload.js create mode 100644 client/src/scenes/UploadDLockData/components/UploadResults.js create mode 100644 client/src/utilities/api.js create mode 100644 constants/constants.js create mode 100644 helpers/api.js create mode 100644 migrations/20190529103954-create-door-lock-events.js create mode 100644 migrations/20190530140559-create-booking-reservations.js create mode 100644 migrations/20190531154129-door-lock-incidents.js create mode 100644 models/bookingReservation.js create mode 100644 models/doorLockEvent.js create mode 100644 services/doorLock.js create mode 100644 services/officeRnD/bookings.js create mode 100644 services/officeRnD/members.js diff --git a/client/package.json b/client/package.json index a44f884..c87178e 100644 --- a/client/package.json +++ b/client/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "axios": "^0.18.0", "react": "^16.8.6", "react-dom": "^16.8.6", "react-redux": "^7.0.3", diff --git a/client/src/scenes/UploadDLockData/components/FileUpload.js b/client/src/scenes/UploadDLockData/components/FileUpload.js new file mode 100644 index 0000000..b185805 --- /dev/null +++ b/client/src/scenes/UploadDLockData/components/FileUpload.js @@ -0,0 +1,59 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import {Form} from "semantic-ui-react"; + +import { uploadDoorLockData } from "../../../store/actions"; + +class FileUpload extends Component { + constructor(props) { + super(props); + + this.state = { + file: null, + }; + + this.onFileChange = this.onFileChange.bind(this); + this.onUploadClick = this.onUploadClick.bind(this); + } + + onFileChange(event) { + const file = event.target.files[0]; + this.setState({file}); + }; + + onUploadClick() { + const { uploadDoorLockData } = this.props; + const { file } = this.state; + + if (file) { + uploadDoorLockData(file); + } + }; + + render() { + const { pending } = this.props; + return ( +
+ + Upload +
+ ); + } +} + +const mapStateToProps = (state) => ({ + pending: state.doorLockData.pending, +}); + +const mapDispatchToProps = (dispatch) => ({ + uploadDoorLockData: (doorLockDataFile) => uploadDoorLockData(dispatch, doorLockDataFile) +}); + +export default connect(mapStateToProps, mapDispatchToProps)(FileUpload); diff --git a/client/src/scenes/UploadDLockData/components/UploadResults.js b/client/src/scenes/UploadDLockData/components/UploadResults.js new file mode 100644 index 0000000..6a6bbfa --- /dev/null +++ b/client/src/scenes/UploadDLockData/components/UploadResults.js @@ -0,0 +1,86 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { Loader, Message, Tab, Label, Menu } from 'semantic-ui-react'; + +class UploadResults extends Component { + render(){ + const {pending, result, error} = this.props; + + const parsedEntries = result && result.parsedData ? result.parsedData : []; + const errorEntries = result && result.parserErrors ? result.parserErrors : []; + const unknownMembers = result && result.unknownMembers ? result.unknownMembers : []; + + const renderParsedEntriesTab = () => { + return ( +
+
+ +

{parsedEntries.length} entries successfully parsed

+
+
+ ); + }; + + const renderErrorTabResults = (results) => { + return ( +
+
+ { + results.map((entry, index) => { + return ( +
+
+ + {entry.error} +

{JSON.stringify(entry.details)}

+

File : {entry.file}

+
+
+ ); + }) + } +
+ ); + }; + + const parsedEntriesTabTitle = (Parsed Entries); + const errorEntriesTabTitle = (Error Entries); + const unknownMembersTabTitle = (Unknown Members); + + const panes = [ + {menuItem: parsedEntriesTabTitle, render: renderParsedEntriesTab}, + {menuItem: errorEntriesTabTitle, render: () => renderErrorTabResults(errorEntries)}, + {menuItem: unknownMembersTabTitle, render: () => renderErrorTabResults(unknownMembers)} + ]; + + + return ( +
+ { + pending && + } +
+ { + !pending && !error && result && + + } + { + !pending && error && + + Upload Failed +

{error.data}

+
+ } +
+ ) + } +} + +const mapStateToProps = (state) => ({ + pending: state.doorLockData.pending, + result: state.doorLockData.result, + error: state.doorLockData.error, + +}); + +export default connect(mapStateToProps)(UploadResults); diff --git a/client/src/scenes/UploadDLockData/index.js b/client/src/scenes/UploadDLockData/index.js index b75da54..b2e4e52 100644 --- a/client/src/scenes/UploadDLockData/index.js +++ b/client/src/scenes/UploadDLockData/index.js @@ -1,39 +1,22 @@ -import React, { Component } from 'react'; -import { connect } from 'react-redux'; - +import React from 'react'; import { Container, Form } from "semantic-ui-react"; import MainMenu from '../../components/MainMenu'; -import { uploadDoorLockData } from "../../store/actions"; +import FileUpload from './components/FileUpload'; +import UploadResults from './components/UploadResults'; -class UploadDLockData extends Component { - - render () { - return ( - - -

DLock Data

-
-
- - Upload - -
- ); - } +function UploadDLockData() { + return ( + + +

DLock Data

+
+
+ + + +
+ ); } -const mapStateToProps = (state) => ({ - -}); - -const mapDispatchToProps = (dispatch) => ({ - uploadDoorLockData: () => uploadDoorLockData(dispatch), -}); - -export default connect(mapStateToProps, mapDispatchToProps)(UploadDLockData); +export default UploadDLockData; diff --git a/client/src/store/actions/doorLockActions.js b/client/src/store/actions/doorLockActions.js index 9aa82ea..00273f4 100644 --- a/client/src/store/actions/doorLockActions.js +++ b/client/src/store/actions/doorLockActions.js @@ -4,14 +4,21 @@ import { UPLOAD_DOOR_LOCK_DATA_FAILED } from "../constants"; -export const uploadDoorLockData = (dispatch) => { +import API from '../../utilities/api'; + +export const uploadDoorLockData = (dispatch, doorLockDataFile) => { + const formData = new FormData(); + formData.append('doorLockDataFile', doorLockDataFile); + const additionalConfig = { + headers: {'content-type': 'multipart/form-data'} + }; + dispatch({type: UPLOAD_DOOR_LOCK_DATA_PENDING}); - fetch('/api/doorLockData') - .then(response => response.json()) - .then(data => { - dispatch({type: UPLOAD_DOOR_LOCK_DATA_SUCCESS, payload: data}) - }) - .catch(err => { - dispatch({type: UPLOAD_DOOR_LOCK_DATA_FAILED, payload: err}) + API.post('doorLock/upload', formData, additionalConfig) + .then(response => { + dispatch({type: UPLOAD_DOOR_LOCK_DATA_SUCCESS, payload: response.data}) }) + .catch(error => { + dispatch({type: UPLOAD_DOOR_LOCK_DATA_FAILED, payload: error.response}) + }); }; diff --git a/client/src/store/reducers/doorLockReducers.js b/client/src/store/reducers/doorLockReducers.js index 759e4fd..97158d3 100644 --- a/client/src/store/reducers/doorLockReducers.js +++ b/client/src/store/reducers/doorLockReducers.js @@ -6,8 +6,8 @@ import { const initialState = { pending: false, - result: {}, - error: '', + result: null, + error: null, }; export const doorLockData = (state, action) => { @@ -18,15 +18,18 @@ export const doorLockData = (state, action) => { case UPLOAD_DOOR_LOCK_DATA_PENDING: return Object.assign({}, state, { pending: true, + error: null, }); case UPLOAD_DOOR_LOCK_DATA_SUCCESS: return Object.assign({}, state, { pending: false, result: action.payload, + error: null, }); case UPLOAD_DOOR_LOCK_DATA_FAILED: return Object.assign({}, state, { pending: false, + result: {}, error: action.payload, }); default: diff --git a/client/src/utilities/api.js b/client/src/utilities/api.js new file mode 100644 index 0000000..264adff --- /dev/null +++ b/client/src/utilities/api.js @@ -0,0 +1,5 @@ +import axios from 'axios'; + +export default axios.create({ + baseURL: '/api/' +}); diff --git a/client/yarn.lock b/client/yarn.lock index 2f7cadb..8413b54 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -1597,6 +1597,13 @@ aws4@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" +axios@^0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.18.0.tgz#32d53e4851efdc0a11993b6cd000789d70c05102" + dependencies: + follow-redirects "^1.3.0" + is-buffer "^1.1.5" + axobject-query@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.0.2.tgz#ea187abe5b9002b377f925d8bf7d1c561adf38f9" @@ -3547,7 +3554,7 @@ flush-write-stream@^1.0.0: inherits "^2.0.3" readable-stream "^2.3.6" -follow-redirects@^1.0.0: +follow-redirects@^1.0.0, follow-redirects@^1.3.0: version "1.7.0" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.7.0.tgz#489ebc198dc0e7f64167bd23b03c4c19b5784c76" dependencies: diff --git a/constants/constants.js b/constants/constants.js new file mode 100644 index 0000000..db8cdc9 --- /dev/null +++ b/constants/constants.js @@ -0,0 +1,30 @@ +const USER_ENTRY_EVENT = 'User Entry'; +const ENABLE_PASSAGE_MODE = 'Enable Passage Mode by Group 2'; +const DISABLE_PASSAGE_MODE = 'Disable Passage Mode by Group 2'; + +const USER_LOCKED_DOOR = 'locked'; +const USER_UNLOCKED_DOOR = 'unlocked'; + +const VALID_CSV_HEADERS = ['Date', 'Time', 'User No', 'Name', 'Event']; + +const csvParserErrors = { + INVALID_HEADERS: 'Invalid headers', + INVALID_ENTRY_EXPECTED_USER: 'Invalid entry type. Expected user entry type', + INVALID_ENTRY_EXPECTED_PASSAGE_MODE: 'Invalid entry type. Expected enable/disable passage mode following user entry', + UNKNOWN_MEMBER: 'Member is not registered in OfficeRnD system', +}; + +const officeRnDAPIErrors = { + FAILED_TO_FETCH_MEMBERS: 'Failed to fetch members', +}; + +module.exports = { + VALID_CSV_HEADERS, + USER_ENTRY_EVENT, + ENABLE_PASSAGE_MODE, + DISABLE_PASSAGE_MODE, + USER_LOCKED_DOOR, + USER_UNLOCKED_DOOR, + csvParserErrors, + officeRnDAPIErrors, +}; diff --git a/controllers/doorLock.js b/controllers/doorLock.js index 8376c64..3e57137 100644 --- a/controllers/doorLock.js +++ b/controllers/doorLock.js @@ -1,7 +1,59 @@ 'use strict'; +const { parseDoorLockDataFile, writeDoorLockEvent } = require("../services/doorLock"); +const { fetchAllBookings, writeBookingReservation } = require('../services/officeRnD/bookings'); +const { officeRnDAPIErrors } = require('../constants/constants'); + +const IncomingForm = require('formidable').IncomingForm; + const uploadDoorLockData = (req, res) => { - res.json({status: 'ok'}); + const form = new IncomingForm(); + const parsingResults = []; + + form.on('file', (field, file) => { + if (file && file.type === 'text/csv') { + parsingResults.push(parseDoorLockDataFile(file)); + } + }); + + form.on('end', () => { + Promise.all(parsingResults) + .then((parserResults) => { + const parsedData = []; + const parserErrors = []; + const unknownMembers = []; + + parserResults.forEach((parserResult) => { + parsedData.push(...parserResult.parsedData); + parserErrors.push(...parserResult.errors); + unknownMembers.push(...parserResult.unknownMembers); + }); + + res.json({ + parsedData, + parserErrors, + unknownMembers + }); + + fetchAllBookings() + .then((bookingEntries) => { + bookingEntries.forEach((bookingEntry) => writeBookingReservation(bookingEntry)); + }) + .catch((error) => { + console.log('===> ERROR'); + console.log(error); + }); + + parsedData.forEach((entry) => { + writeDoorLockEvent(entry); + }); + }) + .catch(() => { + res.status(500).send(officeRnDAPIErrors.FAILED_TO_FETCH_MEMBERS); + }); + }); + + form.parse(req); }; module.exports = { diff --git a/helpers/api.js b/helpers/api.js new file mode 100644 index 0000000..441105d --- /dev/null +++ b/helpers/api.js @@ -0,0 +1,12 @@ +const axios = require('axios'); + +const officeRnDToken = process.env.OFFICE_RnD_TOKEN; + +const API = axios.create({ + baseURL: 'https://app.officernd.com/api/v1/organizations/sima-space-test-environment', + headers: {'Authorization': `Bearer ${officeRnDToken}`} +}); + +module.exports = { + API, +}; diff --git a/migrations/20190529103954-create-door-lock-events.js b/migrations/20190529103954-create-door-lock-events.js new file mode 100644 index 0000000..4355e87 --- /dev/null +++ b/migrations/20190529103954-create-door-lock-events.js @@ -0,0 +1,33 @@ +'use strict'; + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.createTable('doorLockEvents', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + memberName: Sequelize.TEXT, + memberNumber: Sequelize.INTEGER, + memberId: Sequelize.TEXT, + event: { + type: Sequelize.ENUM, + values: ['locked', 'unlocked'] + }, + timestamp: Sequelize.DATE, + createdAt: { + allowNull: false, + type: Sequelize.DATE + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE + } + }); + }, + down: (queryInterface, Sequelize) => { + return queryInterface.dropTable('doorLockEvents'); + } +}; diff --git a/migrations/20190530140559-create-booking-reservations.js b/migrations/20190530140559-create-booking-reservations.js new file mode 100644 index 0000000..6c970bb --- /dev/null +++ b/migrations/20190530140559-create-booking-reservations.js @@ -0,0 +1,30 @@ +'use strict'; + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.createTable('bookingReservations', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + reservationId: Sequelize.TEXT, + memberId: Sequelize.TEXT, + resource: Sequelize.TEXT, + start: Sequelize.DATE, + end: Sequelize.DATE, + createdAt: { + allowNull: false, + type: Sequelize.DATE + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE + } + }); + }, + down: (queryInterface, Sequelize) => { + return queryInterface.dropTable('bookingReservations'); + } +}; diff --git a/migrations/20190531154129-door-lock-incidents.js b/migrations/20190531154129-door-lock-incidents.js new file mode 100644 index 0000000..5dced0f --- /dev/null +++ b/migrations/20190531154129-door-lock-incidents.js @@ -0,0 +1,40 @@ +'use strict'; + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.createTable('doorLockIncidents', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + reservationId: Sequelize.TEXT, + memberId: Sequelize.TEXT, + resource: Sequelize.TEXT, + bookingStart: Sequelize.DATE, + bookingEnd: Sequelize.DATE, + doorLockEventTimestamp: Sequelize.DATE, + doorLockEventType: { + type: Sequelize.ENUM, + values: ['locked', 'unlocked'] + }, + chargeType: { + type: Sequelize.ENUM, + values: ['unlocked', 'unscheduled'] + }, + chargeFee: Sequelize.FLOAT, + createdAt: { + allowNull: false, + type: Sequelize.DATE + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE + } + }); + }, + down: (queryInterface, Sequelize) => { + return queryInterface.dropTable('doorLockIncidents'); + } +}; diff --git a/models/bookingReservation.js b/models/bookingReservation.js new file mode 100644 index 0000000..3a10d3e --- /dev/null +++ b/models/bookingReservation.js @@ -0,0 +1,15 @@ +'use strict'; + +module.exports = (sequelize, DataTypes) => { + const bookingReservation = sequelize.define('bookingReservation', { + reservationId: DataTypes.TEXT, + memberId: DataTypes.TEXT, + resource: DataTypes.TEXT, + start: DataTypes.DATE, + end: DataTypes.DATE, + }, {}); + bookingReservation.associate = function(models) { + // associations can be defined here + }; + return bookingReservation; +}; diff --git a/models/doorLockEvent.js b/models/doorLockEvent.js new file mode 100644 index 0000000..e0a06e1 --- /dev/null +++ b/models/doorLockEvent.js @@ -0,0 +1,20 @@ +'use strict'; + +const { USER_LOCKED_DOOR, USER_UNLOCKED_DOOR } = require('../constants/constants'); + +module.exports = (sequelize, DataTypes) => { + const doorLockEvent = sequelize.define('doorLockEvent', { + memberName: DataTypes.TEXT, + memberNumber: DataTypes.INTEGER, + memberId: DataTypes.TEXT, + event: { + type: DataTypes.ENUM, + values: [USER_LOCKED_DOOR, USER_UNLOCKED_DOOR] + }, + timestamp: DataTypes.DATE, + }, {}); + doorLockEvent.associate = function(models) { + // associations can be defined here + }; + return doorLockEvent; +}; diff --git a/package-lock.json b/package-lock.json index f454197..85b4ea6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -119,6 +119,15 @@ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", "dev": true }, + "axios": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.0.tgz", + "integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=", + "requires": { + "follow-redirects": "^1.3.0", + "is-buffer": "^1.1.5" + } + }, "babel-runtime": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", @@ -292,6 +301,30 @@ } } }, + "buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "requires": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" + }, + "buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=" + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, "buffer-writer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", @@ -542,8 +575,7 @@ "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "create-error-class": { "version": "3.0.2", @@ -571,6 +603,56 @@ "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", "dev": true }, + "csv-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/csv-parser/-/csv-parser-2.3.0.tgz", + "integrity": "sha512-yfYRZ9P9LNKuRK0lEFf40Be4HcFxe1XgxWL/QmlkakSE8SHcWbOGFZA/u7YpfGX/hVbRUdbnO5xO8XuYxrcBtA==", + "requires": { + "buffer-alloc": "^1.1.0", + "buffer-from": "^1.0.0", + "execa": "^1.0.0", + "generate-function": "^1.0.1", + "generate-object-property": "^1.0.0", + "minimist": "^1.2.0", + "ndjson": "^1.4.0" + }, + "dependencies": { + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } + } + } + }, "d": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", @@ -1045,12 +1127,35 @@ "locate-path": "^3.0.0" } }, + "follow-redirects": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.7.0.tgz", + "integrity": "sha512-m/pZQy4Gj287eNy94nivy5wchN3Kp+Q5WgUPNy5lJSZ3sgkVKSYV/ZChMAQVIgx1SqfZ2zBZtPA2YlXIWxxJOQ==", + "requires": { + "debug": "^3.2.6" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + } + } + }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", "dev": true }, + "formidable": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz", + "integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==" + }, "forwarded": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", @@ -1633,6 +1738,19 @@ } } }, + "generate-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-1.1.0.tgz", + "integrity": "sha1-VMIbCAGSsW2Yd3ecW7gWZudyNl8=" + }, + "generate-object-property": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", + "requires": { + "is-property": "^1.0.0" + } + }, "get-caller-file": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", @@ -1864,8 +1982,7 @@ "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, "is-ci": { "version": "1.2.1", @@ -2006,6 +2123,11 @@ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=" + }, "is-redirect": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", @@ -2032,8 +2154,7 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isexe": { "version": "2.0.0", @@ -2069,6 +2190,11 @@ } } }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, "jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -2264,8 +2390,7 @@ "minimist": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" }, "mixin-deep": { "version": "1.3.1", @@ -2347,6 +2472,17 @@ "to-regex": "^3.0.1" } }, + "ndjson": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/ndjson/-/ndjson-1.5.0.tgz", + "integrity": "sha1-rmA7NrE0vOw0e0UkIrC/mNWDLsg=", + "requires": { + "json-stringify-safe": "^5.0.1", + "minimist": "^1.2.0", + "split2": "^2.1.0", + "through2": "^2.0.3" + } + }, "negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", @@ -2750,8 +2886,7 @@ "process-nextick-args": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" }, "proto-list": { "version": "1.2.4", @@ -2824,7 +2959,6 @@ "version": "2.3.6", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -3287,6 +3421,14 @@ "extend-shallow": "^3.0.0" } }, + "split2": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-2.2.0.tgz", + "integrity": "sha512-RAb22TG39LhI31MbreBgIuKiIKhVsawfTgEGqKHTK87aG+ul/PB8Sqoi3I7kVdRWiCfrKxK3uo4/YUkpNvhPbw==", + "requires": { + "through2": "^2.0.2" + } + }, "static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", @@ -3326,7 +3468,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "requires": { "safe-buffer": "~5.1.0" } @@ -3373,6 +3514,15 @@ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, "timed-out": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", @@ -3641,8 +3791,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "utils-merge": { "version": "1.0.1", diff --git a/package.json b/package.json index 2aeaae4..b8e611e 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "docker-build": "docker build -t simaspace .", "docker-start": "docker run -e POSTGRES_USER=docker -e POSTGRES_PASSWORD=docker -e POSTGRES_DB=CrmIntegration --name pg_simaspace -d -p 5432:5432 simaspace", "docker-stop": "docker stop pg_simaspace", - "setup": "npm run install-server && npm run install-client && npm run docker-build && npm run docker-start && sleep 2 && npm run migrate", + "setup": "npm run install-server && npm run install-client && npm run docker-build && npm run docker-start && sleep 5 && npm run migrate", "migrate": "npx sequelize db:migrate", "start-server": "nodemon server.js", "start-client": "cd client && yarn start", @@ -21,9 +21,13 @@ "node": "11.12.x" }, "dependencies": { + "axios": "^0.18.0", + "csv-parser": "^2.3.0", "dotenv": "^8.0.0", "express": "^4.17.0", "express-basic-auth": "^1.2.0", + "formidable": "^1.2.1", + "moment": "^2.24.0", "pg": "^7.11.0", "sequelize": "^5.8.6", "sequelize-cli": "^5.4.0" diff --git a/routes/index.js b/routes/index.js index f5a4487..f5b4e2c 100644 --- a/routes/index.js +++ b/routes/index.js @@ -7,6 +7,6 @@ const express = require('express'); const router = express.Router(); router.get('/', apiStatusCheck); -router.get('/doorLockData', uploadDoorLockData); +router.post('/doorLock/upload', uploadDoorLockData); module.exports = router; diff --git a/services/doorLock.js b/services/doorLock.js new file mode 100644 index 0000000..497ae66 --- /dev/null +++ b/services/doorLock.js @@ -0,0 +1,150 @@ +'use strict'; + +const db = require('../models/index'); +const fs = require('fs'); +const csv = require('csv-parser'); +const moment = require('moment'); + +const { + USER_ENTRY_EVENT, + ENABLE_PASSAGE_MODE, + DISABLE_PASSAGE_MODE, + USER_UNLOCKED_DOOR, + USER_LOCKED_DOOR, + VALID_CSV_HEADERS, + csvParserErrors, +} = require('../constants/constants'); + +const { fetchAllMembers, findMember } = require('../services/officeRnD/members'); + + +const parseDoorLockDataFile = (file) => { + return new Promise ((resolve, reject) => { + const results = []; + const errors = []; + const unknownMembers = []; + let isValidFile = true; + + fetchAllMembers() + .then(() => { + fs.createReadStream(file.path) + .pipe(csv({ + mapHeaders: ({ header, index }) => header.trim().toLowerCase(), + mapValues: ({ header, index, value }) => value.trim() + })) + .on('headers', (headers) => { + + const sortedValidHeadersArray = VALID_CSV_HEADERS.concat().sort(); + const sortedParsedHeadersArray = headers.map(header => header.trim()).sort(); + + let validHeaders = true; + if (sortedParsedHeadersArray.length !== sortedValidHeadersArray.length) { + validHeaders = false; + }else { + for (let i = 0; i < sortedValidHeadersArray.length; i++){ + validHeaders = validHeaders + && (sortedValidHeadersArray[i] === sortedParsedHeadersArray[i]); + } + } + + if (!validHeaders){ + isValidFile = false; + errors.push({ + error: csvParserErrors.INVALID_HEADERS, + details: `Expected headers : ${JSON.stringify(VALID_CSV_HEADERS)}`, + file: file.name, + }); + } + }) + .on('data', (data) => { + if (!isValidFile) { + return; + } + + const eventType = data.event.trim(); + if ([USER_ENTRY_EVENT, ENABLE_PASSAGE_MODE, DISABLE_PASSAGE_MODE].includes(eventType)){ + results.push(data); + } + }) + .on('end', () => { + const parsedData = []; + let i = 0; + while (i < results.length){ + //Verify pair + //First entry type should be user entry and second should be enable / disable passage + const firstEntry = results[i]; + const secondEntry = results[i+1]; + + if (firstEntry && (firstEntry.event === USER_ENTRY_EVENT)){ + const memberObject = findMember(firstEntry.name); + + if (!memberObject){ + //Check if member is already labeled as unknown + const unknownMember = unknownMembers.find((member) => member.details === firstEntry.name); + if (!unknownMember){ + unknownMembers.push({ + error: csvParserErrors.UNKNOWN_MEMBER, + details: firstEntry.name, + file: file.name, + }); + } + } + + if (secondEntry && (secondEntry.event === ENABLE_PASSAGE_MODE || secondEntry.event === DISABLE_PASSAGE_MODE)){ + const event = (secondEntry.event === ENABLE_PASSAGE_MODE) ? USER_UNLOCKED_DOOR : USER_LOCKED_DOOR; + const dateTimeString = `${firstEntry.date} ${firstEntry.time}`; + const timestamp = moment.utc(dateTimeString, 'MM/DD/YY HH:mm:ss A').toISOString(); + + //Verify that member is registered in OfficeRnD system + if (memberObject){ + const entryData = { + memberName: firstEntry.name, + memberNumber: firstEntry['user no'], + memberId: memberObject.memberId, + timestamp, + event, + }; + + parsedData.push(entryData); + } + i+=2; + } else { + errors.push({ + error: csvParserErrors.INVALID_ENTRY_EXPECTED_PASSAGE_MODE, + details: firstEntry, + file: file.name, + }); + i+=1; + } + } else { + errors.push({ + error: csvParserErrors.INVALID_ENTRY_EXPECTED_USER, + details: firstEntry, + file: file.name, + }); + i+=1; + } + } + resolve({ + parsedData, + unknownMembers, + errors + }); + }); + }) + .catch((error) => { + reject(error); + }); + }); +}; + +const writeDoorLockEvent = (entry) => { + db.doorLockEvent.findOrCreate({where: {...entry}, defaults: {...entry}}) + .then() + .catch(); +}; + +module.exports = { + parseDoorLockDataFile, + writeDoorLockEvent, +}; diff --git a/services/officeRnD/bookings.js b/services/officeRnD/bookings.js new file mode 100644 index 0000000..ad834bd --- /dev/null +++ b/services/officeRnD/bookings.js @@ -0,0 +1,40 @@ +'use strict'; + +const db = require('../../models/index'); + +const { API } = require('../../helpers/api'); + +const fetchAllBookings = () => { + return new Promise((resolve, reject) => { + API.get('/bookings') + .then((result) => { + const cleanedBookingReservations = []; + const bookingData = result && result.data ? result.data : []; + + bookingData.forEach((fullBookingEntry) => { + cleanedBookingReservations.push({ + reservationId: fullBookingEntry['_id'], + memberId: fullBookingEntry.member, + resource: fullBookingEntry.resourceId, + start: fullBookingEntry.start.dateTime, + end: fullBookingEntry.end.dateTime, + }); + }); + resolve(cleanedBookingReservations); + }) + .catch((error) => { + reject(error); + }); + }); +}; + +const writeBookingReservation = (bookingReservation) => { + db.bookingReservation.findOrCreate({where: {...bookingReservation}, defaults: {...bookingReservation}}) + .then() + .catch(); +}; + +module.exports = { + fetchAllBookings, + writeBookingReservation, +}; diff --git a/services/officeRnD/members.js b/services/officeRnD/members.js new file mode 100644 index 0000000..e45b776 --- /dev/null +++ b/services/officeRnD/members.js @@ -0,0 +1,33 @@ +'use strict'; + +const { API } = require('../../helpers/api'); + +const membersList = []; + +const fetchAllMembers = () => { + return new Promise((resolve, reject) => { + API.get('/members') + .then((result) => { + const members = result.data || []; + members.forEach((member) => { + membersList.push({ + name: member.name, + memberId: member['_id'], + }); + }); + resolve(); + }) + .catch((error) => { + reject(error); + }); + }); +}; + +const findMember = (memberName) => { + return membersList.find((member) => member.name === memberName); +}; + +module.exports = { + fetchAllMembers, + findMember, +};