435 lines
17 KiB
JavaScript
435 lines
17 KiB
JavaScript
'use strict';
|
|
|
|
const db = require('../../models');
|
|
const fs = require('fs');
|
|
const csv = require('csv-parser');
|
|
const moment = require('moment-timezone');
|
|
const Op = require('sequelize').Op;
|
|
|
|
const {
|
|
UI_TIMEZONE,
|
|
USER_ENTRY_EVENT,
|
|
ENABLE_PASSAGE_MODE,
|
|
DISABLE_PASSAGE_MODE,
|
|
VALID_CSV_HEADERS,
|
|
doorLockEvents,
|
|
csvParserErrors,
|
|
TRIM_DLOCK_NAMES_LENGTH
|
|
} = require('../../constants/constants');
|
|
|
|
const { fetchAllMembers } = require('../officeRnD/members');
|
|
const { getMappingsFromDatabase } = require('../officeRnD/resources');
|
|
const { getFirstReservationInBlock } = require('../officeRnD/bookings');
|
|
|
|
const extractMappingFromFileName = (fileName) => {
|
|
const contentBetweenBracketsRegex = /\[(.*?)\]/;
|
|
const rawContent = fileName.match(contentBetweenBracketsRegex)[1];
|
|
const mappingContent = rawContent.split('-').map(word => word.trim());
|
|
return {
|
|
officeSlug: mappingContent[0],
|
|
resourceSlug: mappingContent[1],
|
|
}
|
|
};
|
|
|
|
const checkIfMappingExsists = (mappingFromFileName, mappings) => {
|
|
const { officeSlug, resourceSlug } = mappingFromFileName;
|
|
|
|
return mappings.find(mapping => (mapping.officeSlug === officeSlug) && (mapping.resourceSlug === resourceSlug));
|
|
};
|
|
|
|
const parseDoorLockDataFile = (file) => {
|
|
return new Promise ((resolve, reject) => {
|
|
const results = [];
|
|
const errors = [];
|
|
const unknownMembersToReport = [];
|
|
let isValidFile = true;
|
|
|
|
const prefetchDataJobs = [getMappingsFromDatabase(), fetchAllMembers()];
|
|
|
|
Promise.all(prefetchDataJobs)
|
|
.then(result => {
|
|
const mappings = result[0];
|
|
const allMembers = result[1];
|
|
|
|
const membersMap = {};
|
|
const unknownMembersMap = {};
|
|
|
|
allMembers.forEach((member) => {
|
|
const limitedMemberName = member.name ? member.name.substring(0, TRIM_DLOCK_NAMES_LENGTH) || undefined : undefined;
|
|
if (limitedMemberName){
|
|
membersMap[limitedMemberName] = member;
|
|
}
|
|
});
|
|
|
|
const mappingFromFileName = extractMappingFromFileName(file.name);
|
|
const mappingObject = checkIfMappingExsists(mappingFromFileName, mappings);
|
|
if (!mappingObject){
|
|
reject('Error ! File contains unknown location');
|
|
return;
|
|
}
|
|
|
|
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];
|
|
|
|
const trimmedName = firstEntry && firstEntry.name ? firstEntry.name.substring(0, TRIM_DLOCK_NAMES_LENGTH) || undefined : undefined;
|
|
|
|
if (firstEntry && trimmedName && (firstEntry.event === USER_ENTRY_EVENT)){
|
|
const memberObject = membersMap[trimmedName];
|
|
|
|
if (!memberObject){
|
|
//Check if member is already labeled as unknown
|
|
const unknownMember = unknownMembersMap[trimmedName];
|
|
|
|
if (!unknownMember){
|
|
unknownMembersMap[trimmedName] = trimmedName;
|
|
unknownMembersToReport.push({
|
|
error: csvParserErrors.UNKNOWN_MEMBER,
|
|
details: trimmedName,
|
|
file: file.name,
|
|
});
|
|
}
|
|
}
|
|
|
|
if (secondEntry && (secondEntry.event === ENABLE_PASSAGE_MODE || secondEntry.event === DISABLE_PASSAGE_MODE)){
|
|
const event = (secondEntry.event === ENABLE_PASSAGE_MODE) ?
|
|
doorLockEvents.USER_UNLOCKED : doorLockEvents.USER_LOCKED;
|
|
|
|
const dateTimeString = `${firstEntry.date} ${firstEntry.time}`;
|
|
const timestamp = moment.tz(dateTimeString, 'MM/DD/YY HH:mm:ss A', UI_TIMEZONE).tz('UTC').toISOString();
|
|
|
|
//Verify that member is registered in OfficeRnD system
|
|
if (memberObject){
|
|
const entryData = {
|
|
memberName: trimmedName,
|
|
memberNumber: firstEntry['user no'],
|
|
memberId: memberObject.memberId,
|
|
timestamp,
|
|
event,
|
|
resourceId: mappingObject.resourceId,
|
|
|
|
};
|
|
|
|
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: unknownMembersToReport,
|
|
errors
|
|
});
|
|
});
|
|
|
|
})
|
|
.catch(error => {
|
|
reject(error);
|
|
});
|
|
});
|
|
};
|
|
|
|
const writeDoorLockEvent = (entry) => {
|
|
return db.doorLockEvent.findOrCreate({where: {...entry}, defaults: {...entry}});
|
|
};
|
|
|
|
const getUnlockEntryForReservation = (reservation, previousReservation) => {
|
|
return new Promise((resolve, reject) => {
|
|
const { memberId, resourceId } = reservation;
|
|
|
|
const previousReservationEndMoment = previousReservation && previousReservation.end ?
|
|
moment.utc(previousReservation.end) : null;
|
|
const reservationStartMoment = moment.utc(reservation.start);
|
|
|
|
const fromTimestamp = previousReservationEndMoment && previousReservationEndMoment.tz(UI_TIMEZONE).isSame(reservationStartMoment.tz(UI_TIMEZONE), 'day') ?
|
|
previousReservation.end : reservationStartMoment.tz(UI_TIMEZONE).startOf('day').toISOString();
|
|
|
|
const toTimestamp = reservation.end;
|
|
|
|
// if (reservation.memberId === '5ce785af422bdd00967fb781') {
|
|
// console.log('=======================');
|
|
// console.log('\tStart : ', moment.tz(reservation.start, reservation.timezone).format('DD.MM, HH:mm'));
|
|
// console.log('\tEnd : ', moment.tz(reservation.end, reservation.timezone).format('DD.MM, HH:mm'));
|
|
// console.log('\t----------------------------------');
|
|
// console.log('\tFrom time : ', fromTimestamp);
|
|
// console.log('\tTo time : ', toTimestamp);
|
|
// }
|
|
|
|
const filters = {
|
|
memberId,
|
|
timestamp: {
|
|
[Op.and]: [
|
|
{[Op.gt]: fromTimestamp},
|
|
{[Op.lte]: toTimestamp}
|
|
]
|
|
},
|
|
resourceId,
|
|
};
|
|
|
|
const order = [['timestamp', 'DESC']];
|
|
|
|
db.doorLockEvent.findAll({
|
|
where: filters,
|
|
order,
|
|
})
|
|
.then((entries) => {
|
|
let candidateUnlockEntry = null;
|
|
let pairedLockEntry = null;
|
|
let eventFound = false;
|
|
|
|
const entriesBeforeReservationStart = entries.filter((entry) => moment.utc(entry.timestamp).isBefore(reservation.start));
|
|
|
|
entriesBeforeReservationStart.forEach((entry) => {
|
|
if (!eventFound) {
|
|
if (entry.event === doorLockEvents.USER_UNLOCKED) {
|
|
if (pairedLockEntry) {
|
|
pairedLockEntry = null;
|
|
candidateUnlockEntry = null;
|
|
} else {
|
|
candidateUnlockEntry = entry;
|
|
eventFound = true;
|
|
}
|
|
}
|
|
if (entry.event === doorLockEvents.USER_LOCKED){
|
|
pairedLockEntry = entry;
|
|
}
|
|
}
|
|
});
|
|
|
|
if (eventFound){
|
|
resolve(candidateUnlockEntry);
|
|
} else {
|
|
candidateUnlockEntry = null;
|
|
const numberOfEntriesLeft = entries.length - entriesBeforeReservationStart.length;
|
|
const entriesAfterReservationStart = entries.slice(0, numberOfEntriesLeft);
|
|
|
|
entriesAfterReservationStart.forEach((entry) => {
|
|
if (!eventFound) {
|
|
if (entry.event === doorLockEvents.USER_UNLOCKED) {
|
|
eventFound = true;
|
|
candidateUnlockEntry = entry;
|
|
}
|
|
}
|
|
});
|
|
|
|
resolve(candidateUnlockEntry);
|
|
}
|
|
})
|
|
.catch((error) => reject(error));
|
|
});
|
|
};
|
|
|
|
const getLockEntryForReservation = (reservation, nextReservation) => {
|
|
return new Promise((resolve, reject) => {
|
|
const { memberId, resourceId } = reservation;
|
|
|
|
const attributes = ['memberName', 'event', 'timestamp'];
|
|
|
|
const nextReservationStartMoment = nextReservation && nextReservation.start ?
|
|
moment.utc(nextReservation.start) : null;
|
|
const reservationStartMoment = moment.utc(reservation.start);
|
|
|
|
const fromTimestamp = reservation.start;
|
|
const toTimestamp = nextReservationStartMoment && nextReservationStartMoment.tz(UI_TIMEZONE).isSame(reservationStartMoment.tz(UI_TIMEZONE), 'day') ?
|
|
nextReservation.start : reservationStartMoment.tz(UI_TIMEZONE).endOf('day').toISOString();
|
|
|
|
const filters = {
|
|
memberId,
|
|
timestamp: {
|
|
[Op.and]: [
|
|
{[Op.gt]: fromTimestamp},
|
|
{[Op.lte]: toTimestamp}
|
|
]
|
|
},
|
|
resourceId,
|
|
};
|
|
|
|
const order = [['timestamp', 'ASC']];
|
|
|
|
db.doorLockEvent.findAll({
|
|
attributes,
|
|
where: filters,
|
|
order,
|
|
})
|
|
.then((entries) => {
|
|
let candidateLockEntry = null;
|
|
let pairedUnlockEntry = null;
|
|
let eventFound = false;
|
|
|
|
const entriesAfterReservationEnd = entries.filter((entry) => moment.utc(entry.timestamp).isAfter(reservation.end));
|
|
|
|
entriesAfterReservationEnd.forEach((entry) => {
|
|
if (!eventFound) {
|
|
if (entry.event === doorLockEvents.USER_LOCKED) {
|
|
if (pairedUnlockEntry) {
|
|
pairedUnlockEntry = null;
|
|
candidateLockEntry = null;
|
|
} else {
|
|
candidateLockEntry = entry;
|
|
eventFound = true;
|
|
}
|
|
}
|
|
if (entry.event === doorLockEvents.USER_UNLOCKED){
|
|
pairedUnlockEntry = entry;
|
|
}
|
|
}
|
|
});
|
|
|
|
if (eventFound){
|
|
resolve(candidateLockEntry);
|
|
} else {
|
|
candidateLockEntry = null;
|
|
const numberOfEntriesLeft = entries.length - entriesAfterReservationEnd.length;
|
|
const entriesBeforeReservationEnd = entries.slice(0, numberOfEntriesLeft);
|
|
|
|
entriesBeforeReservationEnd.reverse().forEach((entry) => {
|
|
if (!eventFound) {
|
|
if (entry.event === doorLockEvents.USER_LOCKED) {
|
|
eventFound = true;
|
|
candidateLockEntry = entry;
|
|
}
|
|
}
|
|
});
|
|
|
|
resolve(candidateLockEntry);
|
|
}
|
|
})
|
|
.catch((error) => reject(error));
|
|
});
|
|
};
|
|
|
|
const getEntriesBetween = (fromTimestamp, toTimestamp, resourceId) => {
|
|
return new Promise((resolve, reject) => {
|
|
if (!fromTimestamp || !toTimestamp || !resourceId){
|
|
resolve([]);
|
|
}else {
|
|
const andTimestampFilters = [];
|
|
|
|
if (fromTimestamp){
|
|
andTimestampFilters.push({[Op.gt]: fromTimestamp});
|
|
}
|
|
if (toTimestamp){
|
|
andTimestampFilters.push({[Op.lt]: toTimestamp});
|
|
}
|
|
|
|
const filters = {
|
|
resourceId,
|
|
timestamp: {
|
|
[Op.and]: andTimestampFilters,
|
|
},
|
|
};
|
|
const order = [['timestamp', 'ASC']];
|
|
|
|
db.doorLockEvent.findAll({where: filters, order})
|
|
.then((results) => resolve(results))
|
|
.catch((error) => reject(error));
|
|
}
|
|
});
|
|
};
|
|
|
|
const getLastEntryForReservation = (reservation) => {
|
|
return new Promise ((resolve, reject) => {
|
|
getFirstReservationInBlock(reservation)
|
|
.then((firstReservationInBlock) => {
|
|
const { memberId, resourceId } = reservation;
|
|
let fromTimestamp = reservation.start;
|
|
const toTimestamp = reservation.end;
|
|
if (firstReservationInBlock){
|
|
fromTimestamp = firstReservationInBlock.start;
|
|
}
|
|
|
|
const filters = {
|
|
memberId,
|
|
resourceId,
|
|
timestamp: {
|
|
[Op.and]: [
|
|
{[Op.gte]: fromTimestamp},
|
|
{[Op.lte]: toTimestamp}
|
|
]
|
|
},
|
|
};
|
|
|
|
const order = [['timestamp', 'DESC']];
|
|
|
|
db.doorLockEvent.findAll({where: filters, order})
|
|
.then((entries) => {
|
|
if (entries && entries.length > 0){
|
|
resolve(entries[0]);
|
|
} else {
|
|
resolve (undefined);
|
|
}
|
|
})
|
|
.catch((error) => reject(error));
|
|
})
|
|
.catch((error) => reject(error));
|
|
});
|
|
};
|
|
|
|
module.exports = {
|
|
parseDoorLockDataFile,
|
|
writeDoorLockEvent,
|
|
getUnlockEntryForReservation,
|
|
getLockEntryForReservation,
|
|
getEntriesBetween,
|
|
getLastEntryForReservation,
|
|
};
|