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,
+};