diff --git a/client/src/scenes/UploadDLockData/components/FileUpload.js b/client/src/scenes/UploadDLockData/components/FileUpload.js
index d0561ff..b185805 100644
--- a/client/src/scenes/UploadDLockData/components/FileUpload.js
+++ b/client/src/scenes/UploadDLockData/components/FileUpload.js
@@ -31,6 +31,7 @@ class FileUpload extends Component {
};
render() {
+ const { pending } = this.props;
return (
- Upload
+ Upload
);
}
}
+const mapStateToProps = (state) => ({
+ pending: state.doorLockData.pending,
+});
+
const mapDispatchToProps = (dispatch) => ({
uploadDoorLockData: (doorLockDataFile) => uploadDoorLockData(dispatch, doorLockDataFile)
});
-export default connect(null, mapDispatchToProps)(FileUpload);
+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 cc688a5..6a6bbfa 100644
--- a/client/src/scenes/UploadDLockData/components/UploadResults.js
+++ b/client/src/scenes/UploadDLockData/components/UploadResults.js
@@ -1,13 +1,58 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
-import { Loader, Message } from 'semantic-ui-react';
+import { Loader, Message, Tab, Label, Menu } from 'semantic-ui-react';
class UploadResults extends Component {
render(){
const {pending, result, error} = this.props;
- const parserErrors = result && result.parserErrors && result.parserErrors.length > 0 ? result.parserErrors : null;
- const parsedDataCount = result && result.parsedData && result.parsedData.length > 0 ? result.parsedData.length : null;
+ 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 (
@@ -16,36 +61,16 @@ class UploadResults extends Component {
}
{
- error &&
-
- Upload failed
- There was error uploading file
-
+ !pending && !error && result &&
+
}
-
{
- !error && parsedDataCount &&
-
- Upload complete
- {parsedDataCount} entries successfully inserted
- {parserErrors && Some entries could not be parsed. Details are shown below
}
+ !pending && error &&
+
+ Upload Failed
+ {error.data}
}
-
- {
- !error && parserErrors && parserErrors.map((parserError, index) => {
- return (
-
-
- {parserError.error}
- {JSON.stringify(parserError.details)}
- File : {parserError.file}
-
-
-
- );
- })
- }
)
}
diff --git a/client/src/store/reducers/doorLockReducers.js b/client/src/store/reducers/doorLockReducers.js
index 58cc7d0..97158d3 100644
--- a/client/src/store/reducers/doorLockReducers.js
+++ b/client/src/store/reducers/doorLockReducers.js
@@ -6,7 +6,7 @@ import {
const initialState = {
pending: false,
- result: {},
+ result: null,
error: null,
};
diff --git a/constants/constants.js b/constants/constants.js
index 4de18b5..77fc7f8 100644
--- a/constants/constants.js
+++ b/constants/constants.js
@@ -11,6 +11,11 @@ const csvParserErrors = {
UNKNOWN_COLUMN: 'Unknown column',
INVALID_ENTRY_EXPECTED_USER: 'Invalid entry type. Expected user entry type',
INVALID_ENTRY_EXPECTED_PASSAGE_MODE: 'Invalid entry type. Expected enable/disable passage mode',
+ UNKNOWN_MEMBER: 'Member is not registered in OfficeRnD system',
+};
+
+const officeRnDAPIErrors = {
+ FAILED_TO_FETCH_MEMBERS: 'Failed to fetch members',
};
module.exports = {
@@ -21,4 +26,5 @@ module.exports = {
USER_LOCKED_DOOR,
USER_UNLOCKED_DOOR,
csvParserErrors,
+ officeRnDAPIErrors,
};
diff --git a/controllers/doorLock.js b/controllers/doorLock.js
index 7925263..c7bdf5e 100644
--- a/controllers/doorLock.js
+++ b/controllers/doorLock.js
@@ -1,10 +1,10 @@
'use strict';
const { parseDoorLockDataFile, writeDoorLockEvent } = require("../services/doorLock");
+const { officeRnDAPIErrors } = require('../constants/constants');
const IncomingForm = require('formidable').IncomingForm;
-
const uploadDoorLockData = (req, res) => {
const form = new IncomingForm();
const parsingResults = [];
@@ -20,24 +20,26 @@ const uploadDoorLockData = (req, res) => {
.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
});
parsedData.forEach((entry) => {
writeDoorLockEvent(entry);
});
})
- .catch((error) => {
- console.log(error);
- res.json({result: 'error'});
+ .catch(() => {
+ res.status(500).send(officeRnDAPIErrors.FAILED_TO_FETCH_MEMBERS);
});
});
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
index 93990bd..6b8a452 100644
--- a/migrations/20190529103954-create-door-lock-events.js
+++ b/migrations/20190529103954-create-door-lock-events.js
@@ -9,8 +9,9 @@ module.exports = {
primaryKey: true,
type: Sequelize.INTEGER
},
- memberName: Sequelize.STRING,
+ memberName: Sequelize.TEXT,
memberNumber: Sequelize.INTEGER,
+ memberId: Sequelize.TEXT,
event: {
type: Sequelize.ENUM,
values: ['locked', 'unlocked']
diff --git a/models/DoorLockEvent.js b/models/DoorLockEvent.js
index f159dff..f07cc5a 100644
--- a/models/DoorLockEvent.js
+++ b/models/DoorLockEvent.js
@@ -4,8 +4,9 @@ const { USER_LOCKED_DOOR, USER_UNLOCKED_DOOR } = require('../constants/constants
module.exports = (sequelize, DataTypes) => {
const doorLockEvent = sequelize.define('doorLockEvent', {
- memberName: DataTypes.STRING,
+ memberName: DataTypes.TEXT,
memberNumber: DataTypes.INTEGER,
+ memberId: DataTypes.TEXT,
event: {
type: DataTypes.ENUM,
values: [USER_LOCKED_DOOR, USER_UNLOCKED_DOOR]
diff --git a/package-lock.json b/package-lock.json
index c2b51d6..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",
@@ -1118,6 +1127,24 @@
"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",
@@ -1955,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",
diff --git a/package.json b/package.json
index 7e736b1..7a8ddbb 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"node": "11.12.x"
},
"dependencies": {
+ "axios": "^0.18.0",
"csv-parser": "^2.3.0",
"dotenv": "^8.0.0",
"express": "^4.17.0",
diff --git a/services/doorLock.js b/services/doorLock.js
index c2552b7..30a7345 100644
--- a/services/doorLock.js
+++ b/services/doorLock.js
@@ -14,84 +14,110 @@ const {
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;
- fs.createReadStream(file.path)
- .pipe(csv({
- mapHeaders: ({ header, index }) => header.trim().toLowerCase(),
- mapValues: ({ header, index, value }) => value.trim()
- }))
- .on('headers', (headers) => {
- headers.forEach((header) => {
- if (!VALID_CSV_HEADERS.includes(header.trim())){
- isValidFile = false;
- console.log('INVALID HEADER');
- errors.push({
- error: csvParserErrors.UNKNOWN_COLUMN,
- details: header,
- file: file.name,
+ fetchAllMembers()
+ .then(() => {
+ fs.createReadStream(file.path)
+ .pipe(csv({
+ mapHeaders: ({ header, index }) => header.trim().toLowerCase(),
+ mapValues: ({ header, index, value }) => value.trim()
+ }))
+ .on('headers', (headers) => {
+ headers.forEach((header) => {
+ if (!VALID_CSV_HEADERS.includes(header.trim())){
+ isValidFile = false;
+ console.log('INVALID HEADER');
+ errors.push({
+ error: csvParserErrors.UNKNOWN_COLUMN,
+ details: header,
+ 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 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)){
- 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 entryData = {
- memberName: firstEntry.name,
- memberNumber: firstEntry['user no'],
- date: firstEntry.date,
- time: firstEntry.time,
- event,
- };
- parsedData.push(entryData);
- i+=2;
- } else {
- errors.push({
- error: csvParserErrors.INVALID_ENTRY_EXPECTED_PASSAGE_MODE,
- details: secondEntry || 'Last row in file',
- file: file.name,
- });
- i+=1;
+ })
+ .on('data', (data) => {
+ if (!isValidFile) {
+ return;
}
- } else {
- errors.push({
- error: csvParserErrors.INVALID_ENTRY_EXPECTED_USER,
- details: firstEntry,
- file: file.name,
+
+ 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 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)){
+ 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 memberObject = findMember(firstEntry.name);
+
+ //Verify that member is registered in OfficeRnD system
+ if (memberObject){
+ const entryData = {
+ memberName: firstEntry.name,
+ memberNumber: firstEntry['user no'],
+ memberId: memberObject.memberId,
+ date: firstEntry.date,
+ time: firstEntry.time,
+ event,
+ };
+
+ parsedData.push(entryData);
+ } else {
+ //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,
+ });
+ }
+ }
+ i+=2;
+ } else {
+ errors.push({
+ error: csvParserErrors.INVALID_ENTRY_EXPECTED_PASSAGE_MODE,
+ details: secondEntry || 'Last row in file',
+ 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
});
- i+=1;
- }
- }
- resolve({
- parsedData,
- errors
- });
+ });
+ })
+ .catch((error) => {
+ reject(error);
});
});
};
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,
+};