Fixed bookings and timezone problems

This commit is contained in:
Senad Uka
2019-06-25 15:44:46 +02:00
parent db1a3acb11
commit ffc8412d7c
11 changed files with 101 additions and 197 deletions

View File

@@ -73,17 +73,25 @@ class DateRangePicker extends Component {
render() { render() {
const { startDate, endDate, error, startDateLabel, endDateLabel } = this.state; const { startDate, endDate, error, startDateLabel, endDateLabel } = this.state;
const { inlineButton } = this.props;
const buttonLabel = this.props.buttonLabel || 'Save'; const buttonLabel = this.props.buttonLabel || 'Save';
const startDateValue = startDate.format(defaultDateFormat); const startDateValue = startDate.format(defaultDateFormat);
const endDateValue = endDate.format(defaultDateFormat); const endDateValue = endDate.format(defaultDateFormat);
const buttonRender = (
<Grid.Column width={inlineButton ? 1 : null}>
{ inlineButton && <label>{'\u00A0'}</label> }
<Form.Button onClick={this.onButtonClick.bind(this)}>{buttonLabel}</Form.Button>
</Grid.Column>
);
return ( return (
<Form> <Form>
<Grid columns="equal"> <Grid stackable columns={inlineButton ? null : 'equal'}>
<Grid.Row> <Grid.Row>
<Grid.Column> <Grid.Column width={inlineButton ? 7 : null}>
<label>{startDateLabel}</label> <label>{startDateLabel}</label>
<Form.Input <Form.Input
fluid fluid
@@ -93,7 +101,7 @@ class DateRangePicker extends Component {
onChange={this.onStartDateChange.bind(this)} onChange={this.onStartDateChange.bind(this)}
/> />
</Grid.Column> </Grid.Column>
<Grid.Column> <Grid.Column width={inlineButton ? 6 : null}>
<label>{endDateLabel}</label> <label>{endDateLabel}</label>
<Form.Input <Form.Input
fluid fluid
@@ -103,6 +111,9 @@ class DateRangePicker extends Component {
onChange={this.onEndDateChange.bind(this)} onChange={this.onEndDateChange.bind(this)}
/> />
</Grid.Column> </Grid.Column>
{
inlineButton && buttonRender
}
</Grid.Row> </Grid.Row>
{ error && { error &&
<Grid.Row> <Grid.Row>
@@ -114,11 +125,12 @@ class DateRangePicker extends Component {
</Grid.Column> </Grid.Column>
</Grid.Row> </Grid.Row>
} }
<Grid.Row> {
<Grid.Column> !inlineButton &&
<Form.Button onClick={this.onButtonClick.bind(this)}>{buttonLabel}</Form.Button> <Grid.Row>
</Grid.Column> {buttonRender}
</Grid.Row> </Grid.Row>
}
</Grid> </Grid>
</Form> </Form>
); );

View File

@@ -7,14 +7,19 @@ import { mainMenuItems } from '../../constants/menuItems';
const MainMenu = () => const MainMenu = () =>
(<Menu> (<Menu>
{ {
mainMenuItems.map(mainMenuItem => mainMenuItems.map(mainMenuItem => {
<Menu.Item key={mainMenuItem.id} if (!mainMenuItem.showInMenu){
as={NavLink} return null;
to={mainMenuItem.url} } else {
exact return (
> <Menu.Item key={mainMenuItem.id}
{mainMenuItem.title} as={NavLink}
</Menu.Item>) to={mainMenuItem.url}
exact
>
{mainMenuItem.title}
</Menu.Item>)
}})
} }
</Menu>); </Menu>);

View File

@@ -2,6 +2,7 @@ import React from 'react';
import { Loader } from 'semantic-ui-react'; import { Loader } from 'semantic-ui-react';
import ReactTable from 'react-table'; import ReactTable from 'react-table';
import 'react-table/react-table.css'; import 'react-table/react-table.css';
import { NavLink } from 'react-router-dom';
import {incidentsReportHeaderTitles} from '../../constants/menuItems'; import {incidentsReportHeaderTitles} from '../../constants/menuItems';
import { import {
@@ -13,7 +14,7 @@ import {
const MemberIncidentsTable = props => { const MemberIncidentsTable = props => {
const { loading, title } = props; const { loading, title, openMemberSummaryOnMemberClick } = props;
const incidents = props.incidents ? props.incidents : []; const incidents = props.incidents ? props.incidents : [];
const columns = []; const columns = [];
@@ -34,9 +35,15 @@ const MemberIncidentsTable = props => {
Header: incidentsReportHeaderTitles[header], Header: incidentsReportHeaderTitles[header],
accessor: header, accessor: header,
Cell: props => { Cell: props => {
let cellValue; let cellValue = '';
let urlValue = undefined;
switch (props.column.id) { switch (props.column.id) {
case 'memberName':
const memberId = props.row['_original'].memberId;
urlValue = `/practice-summary-report/${memberId}`;
cellValue = props.value;
break;
case 'incidentType': case 'incidentType':
cellValue = incidentDescriptions[props.value]; cellValue = incidentDescriptions[props.value];
break; break;
@@ -68,7 +75,17 @@ const MemberIncidentsTable = props => {
cellValue = props.value; cellValue = props.value;
} }
return <div style={{ textAlign: columnContentsAlignment }}>{cellValue}</div> if (openMemberSummaryOnMemberClick && urlValue){
return <NavLink to={urlValue}>{cellValue}</NavLink>
}else{
return <div style={{ textAlign: columnContentsAlignment }}>{cellValue}</div>
}
// return <NavLink to={urlValue}>
// <div>{cellValue}</div>
// </NavLink>
// return <div style={{ textAlign: columnContentsAlignment }}><a href={'www.gogole.com'} >{cellValue}</a></div>
} }
}); });
} }

View File

@@ -6,24 +6,34 @@ import PracticeSummaryReport from '../scenes/PracticeSummaryReport';
export const mainMenuItems = [ export const mainMenuItems = [
{ {
id: 'home', id: 'home',
showInMenu: true,
title: 'Home', title: 'Home',
url: '/', url: '/',
component: Home, component: Home,
}, },
{ {
id: 'practiceSummaryReport', id: 'practiceSummaryReport',
showInMenu: true,
title: 'Practice Summary Report', title: 'Practice Summary Report',
url: '/practice-summary-report', url: '/practice-summary-report',
component: PracticeSummaryReport, component: PracticeSummaryReport,
}, },
{
id: 'practiceSummaryReportWithMemberId',
showInMenu: false,
url: '/practice-summary-report/:memberId',
component: PracticeSummaryReport,
},
{ {
id: 'report', id: 'report',
showInMenu: true,
title: 'Incidents Report', title: 'Incidents Report',
url: '/incidents-report', url: '/incidents-report',
component: IncidentsReport, component: IncidentsReport,
}, },
{ {
id: 'uploadDLockData', id: 'uploadDLockData',
showInMenu: true,
title: 'DLock', title: 'DLock',
url: '/dlock', url: '/dlock',
component: UploadDLockData, component: UploadDLockData,

View File

@@ -24,7 +24,7 @@ class IncidentsReport extends Component {
<hr/> <hr/>
<DateRangePicker buttonLabel="Show report" onDatesUpdate={this.onDatesUpdate.bind(this)} /> <DateRangePicker buttonLabel="Show report" onDatesUpdate={this.onDatesUpdate.bind(this)} />
<br/> <br/>
<MemberIncidentsTable loading={pendingIncidents} incidents={incidents} /> <MemberIncidentsTable loading={pendingIncidents} incidents={incidents} openMemberSummaryOnMemberClick />
</Container> </Container>
); );
} }

View File

@@ -11,7 +11,6 @@ const MemberSummary = props => {
let totalUnlockedFees = 0; let totalUnlockedFees = 0;
incidents.forEach((incident) => { incidents.forEach((incident) => {
console.log(incident);
switch (incident.incidentType) { switch (incident.incidentType) {
case UNSCHEDULED_INCIDENT: case UNSCHEDULED_INCIDENT:
totalUnscheduledFees += parseFloat(incident.totalChargeFee); totalUnscheduledFees += parseFloat(incident.totalChargeFee);
@@ -19,6 +18,8 @@ const MemberSummary = props => {
case UNLOCKED_INCIDENT: case UNLOCKED_INCIDENT:
totalUnlockedFees += parseFloat(incident.incidentPrice); totalUnlockedFees += parseFloat(incident.incidentPrice);
break; break;
default:
break;
} }
}); });
@@ -65,5 +66,4 @@ const MemberSummary = props => {
); );
}; };
export default MemberSummary; export default MemberSummary;

View File

@@ -16,7 +16,7 @@ class PracticeSummaryReport extends Component {
this.state = { this.state = {
dateRange: null, dateRange: null,
memberId: null, memberId: props.match.params.memberId,
}; };
} }
@@ -52,7 +52,7 @@ class PracticeSummaryReport extends Component {
<MemberSelector onMemberSelect={this.onMemberSelectionUpdate.bind(this)} /> <MemberSelector onMemberSelect={this.onMemberSelectionUpdate.bind(this)} />
</Grid.Column> </Grid.Column>
<Grid.Column> <Grid.Column>
<DateRangePicker onDatesUpdate={this.onDateRangeUpdate.bind(this)}/> <DateRangePicker inlineButton onDatesUpdate={this.onDateRangeUpdate.bind(this)}/>
</Grid.Column> </Grid.Column>
</Grid.Row> </Grid.Row>
<Grid.Row> <Grid.Row>

View File

@@ -27,7 +27,13 @@ const uploadDoorLockData = (req, res) => {
parserResults.forEach((parserResult) => { parserResults.forEach((parserResult) => {
parsedData.push(...parserResult.parsedData); parsedData.push(...parserResult.parsedData);
parserErrors.push(...parserResult.errors); parserErrors.push(...parserResult.errors);
unknownMembers.push(...parserResult.unknownMembers);
parserResult.unknownMembers.forEach((newUnknownMember) => {
// Check if member is already labeled as unknown in different file
if (!unknownMembers.find((unknownMember) => unknownMember.details === newUnknownMember.details)){
unknownMembers.push(newUnknownMember);
}
});
}); });
const asyncWriteJobs = []; const asyncWriteJobs = [];

View File

@@ -3,10 +3,11 @@
const db = require('../../models'); const db = require('../../models');
const fs = require('fs'); const fs = require('fs');
const csv = require('csv-parser'); const csv = require('csv-parser');
const moment = require('moment/moment'); const moment = require('moment-timezone');
const Op = require('sequelize').Op; const Op = require('sequelize').Op;
const { const {
UI_TIMEZONE,
USER_ENTRY_EVENT, USER_ENTRY_EVENT,
ENABLE_PASSAGE_MODE, ENABLE_PASSAGE_MODE,
DISABLE_PASSAGE_MODE, DISABLE_PASSAGE_MODE,
@@ -38,7 +39,7 @@ const parseDoorLockDataFile = (file) => {
return new Promise ((resolve, reject) => { return new Promise ((resolve, reject) => {
const results = []; const results = [];
const errors = []; const errors = [];
const unknownMembers = []; const unknownMembersToReport = [];
let isValidFile = true; let isValidFile = true;
const prefetchDataJobs = [getMappingsFromDatabase(), fetchAllMembers()]; const prefetchDataJobs = [getMappingsFromDatabase(), fetchAllMembers()];
@@ -48,6 +49,11 @@ const parseDoorLockDataFile = (file) => {
const mappings = result[0]; const mappings = result[0];
const allMembers = result[1]; const allMembers = result[1];
const membersMap = {};
const unknownMembersMap = {};
allMembers.forEach((member) => membersMap[member.name] = member);
const mappingFromFileName = extractMappingFromFileName(file.name); const mappingFromFileName = extractMappingFromFileName(file.name);
const mappingObject = checkIfMappingExsists(mappingFromFileName, mappings); const mappingObject = checkIfMappingExsists(mappingFromFileName, mappings);
if (!mappingObject){ if (!mappingObject){
@@ -104,13 +110,15 @@ const parseDoorLockDataFile = (file) => {
const secondEntry = results[i+1]; const secondEntry = results[i+1];
if (firstEntry && (firstEntry.event === USER_ENTRY_EVENT)){ if (firstEntry && (firstEntry.event === USER_ENTRY_EVENT)){
const memberObject = allMembers.find(member => member.name === firstEntry.name); const memberObject = membersMap[firstEntry.name];
if (!memberObject){ if (!memberObject){
//Check if member is already labeled as unknown //Check if member is already labeled as unknown
const unknownMember = unknownMembers.find((member) => member.details === firstEntry.name); const unknownMember = unknownMembersMap[firstEntry.name];
if (!unknownMember){ if (!unknownMember){
unknownMembers.push({ unknownMembersMap[firstEntry.name] = firstEntry.name;
unknownMembersToReport.push({
error: csvParserErrors.UNKNOWN_MEMBER, error: csvParserErrors.UNKNOWN_MEMBER,
details: firstEntry.name, details: firstEntry.name,
file: file.name, file: file.name,
@@ -123,7 +131,7 @@ const parseDoorLockDataFile = (file) => {
doorLockEvents.USER_UNLOCKED : doorLockEvents.USER_LOCKED; doorLockEvents.USER_UNLOCKED : doorLockEvents.USER_LOCKED;
const dateTimeString = `${firstEntry.date} ${firstEntry.time}`; const dateTimeString = `${firstEntry.date} ${firstEntry.time}`;
const timestamp = moment.utc(dateTimeString, 'MM/DD/YY HH:mm:ss A').toISOString(); 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 //Verify that member is registered in OfficeRnD system
if (memberObject){ if (memberObject){
@@ -159,7 +167,7 @@ const parseDoorLockDataFile = (file) => {
} }
resolve({ resolve({
parsedData, parsedData,
unknownMembers, unknownMembers: unknownMembersToReport,
errors errors
}); });
}); });

View File

@@ -21,160 +21,6 @@ const getSortedIncidentsForMember = (memberId) => {
}) })
}; };
const createUnlockedIncident = (reservation) => {
return new Promise((resolve, reject) => {
const { reservationId, memberId, resourceId, start, end } = reservation;
getLastIncidentForMember(memberId)
.then(incidents => {
const lastIncident = incidents && incidents[0] ? incidents[0] : undefined;
const incident = {
reservationId,
memberId,
resourceId,
bookingStart: start,
bookingEnd: end,
incidentLevel: null,
incidentLevelPrice: null,
};
console.log('=> UNLOCKED INCIDENT');
console.log('\tMember : ', memberId);
console.log('\tStart : ', start);
console.log('\tEnd : ', end);
console.log('\tMore details : ');
/*
if (lastIncident){
const lastIncidentLevel = lastIncident.incidentLevel;
const lastIncidentBeginningOfTheMonth = moment(lastIncident.bookingStart).startOf('month');
const beginningOfTheMonth = moment.utc().startOf('month');
const timePassedFromLastIncident = Math.abs(beginningOfTheMonth.diff(lastIncidentBeginningOfTheMonth, 'months'));
if (timePassedFromLastIncident >= 6){
console.log('\t\t-> This is first incident for this member in last 6 months');
incident.incidentLevel = unlockedIncidentLevelsPrices.UNLOCKED_0.title;
incident.incidentLevelPrice = unlockedIncidentLevelsPrices.UNLOCKED_0.price;
} else {
console.log('\t\t-> This member had incident(s) in past 6 months !!!');
incident.incidentLevel = lastIncidentLevel;
incident.incidentLevelPrice = unlockedIncidentLevelsPrices[lastIncidentLevel].price;
}
console.log('\t\tLast incident details : ');
console.log('\t\tStart : ', lastIncident.bookingStart);
console.log('\t\tCalculated diff : ', timePassedFromLastIncident);
console.log('\t\t------------------');
console.log('\tNew incident level : ', incident.incidentLevel);
} else {
console.log('\t\tThis is first incident for this member, EVER !');
incident.incidentLevel = unlockedIncidentLevelsPrices.UNLOCKED_0.title;
incident.incidentLevelPrice = unlockedIncidentLevelsPrices.UNLOCKED_0.price;
}
*/
db.unlockedIncident.findOrCreate({
where: {
reservationId,
memberId,
resourceId,
bookingStart: start,
bookingEnd: end,
},
defaults: {
...incident
}
})
.then(()=>resolve())
.catch((error)=>reject(error));
})
.catch((error) => {
reject(error);
});
});
};
const createUnscheduledUseIncident = (reservation, doorLockEntry) => {
return new Promise((resolve, reject) => {
const timeResolution = parseInt(process.env.UNSCHEDULED_USE_TIME_RESOLUTION) || 5;
const chargePrice = parseFloat(process.env.UNSCHEDULED_USE_CHARGE_FEE) || 5;
const reservationEndTime = moment(reservation.end);
const lockedTime = moment(doorLockEntry.timestamp);
const timeDifference = Math.abs(reservationEndTime.diff(lockedTime, 'minutes'));
const timeIntervalsToCharge = Math.floor(timeDifference / timeResolution);
const totalChargeFee = timeIntervalsToCharge * chargePrice;
if (timeIntervalsToCharge > 0){
const incident = {
reservationId: reservation.reservationId,
memberId: reservation.memberId,
resourceId: reservation.resourceId,
bookingStart: reservation.start,
bookingEnd: reservation.end,
doorLockEventTimestamp: doorLockEntry.timestamp,
doorLockEventType: doorLockEntry.event,
chargePrice,
timeIntervalsToCharge,
totalChargeFee,
};
db.unscheduledIncident.findOrCreate({where: {...incident}, defaults: {...incident}})
.then(()=>resolve())
.catch((error)=>reject(error));
}else{
resolve();
}
});
};
const createDoorLockIncident = (reservation, doorLockEntry) => {
return new Promise((resolve, reject) => {
if (!doorLockEntry){
// Check if there is unlock entry for this reservation
getUnlockEntryForReservation(reservation)
.then((unlockEntry) => {
if (!unlockEntry){
// check if there is back-to-back booking before current one
getFirstPreviousBooking(reservation)
.then((previousReservation) => {
if (previousReservation){
const previousReservationEnd = moment(previousReservation.end);
const currentReservationStart = moment(reservation.start);
const timeDifference = Math.abs(currentReservationStart.diff(previousReservationEnd, 'minutes'));
const maxBackToBackDifference = parseInt(process.env.MAX_BACK_TO_BACK_DIFFERENCE) || 0;
if (timeDifference <= maxBackToBackDifference) {
createUnlockedIncident(reservation)
.then(() => resolve())
.catch((error) => reject(error));
}else{
resolve();
}
}else{
resolve();
}
})
.catch((error)=>reject(error));
}else {
createUnlockedIncident(reservation)
.then(()=>resolve())
.catch((error)=>reject(error));
}
})
.catch((error) => {
reject(error);
});
}else{
createUnscheduledUseIncident(reservation, doorLockEntry)
.then(()=>resolve())
.catch((error) => reject(error));
}
});
};
const insertUnscheduledIncidents = (incidents) => { const insertUnscheduledIncidents = (incidents) => {
const asyncJobs = []; const asyncJobs = [];
incidents.forEach((incident) => { incidents.forEach((incident) => {
@@ -333,8 +179,8 @@ const getIncidentData = (reservation) => {
if (nextReservation){ if (nextReservation){
// Check if next reservations is immediately after (back to back reservation) // Check if next reservations is immediately after (back to back reservation)
// If yes, then there is no need to check door lock entries related to this booking // If yes, then there is no need to check door lock entries related to this booking
const firstReservationEnd = moment(reservation.end); const firstReservationEnd = moment.utc(reservation.end);
const secondReservationStart = moment(nextReservation.start); const secondReservationStart = moment.utc(nextReservation.start);
const timeDifference = Math.abs(secondReservationStart.diff(firstReservationEnd, 'minutes')); const timeDifference = Math.abs(secondReservationStart.diff(firstReservationEnd, 'minutes'));
const maxBackToBackDifference = parseInt(process.env.MAX_BACK_TO_BACK_DIFFERENCE) || 0; const maxBackToBackDifference = parseInt(process.env.MAX_BACK_TO_BACK_DIFFERENCE) || 0;
@@ -355,8 +201,8 @@ const getIncidentData = (reservation) => {
const timeResolution = parseInt(process.env.UNSCHEDULED_USE_TIME_RESOLUTION) || 5 const timeResolution = parseInt(process.env.UNSCHEDULED_USE_TIME_RESOLUTION) || 5
const chargePrice = parseFloat(process.env.UNSCHEDULED_USE_CHARGE_FEE) || 5; const chargePrice = parseFloat(process.env.UNSCHEDULED_USE_CHARGE_FEE) || 5;
const reservationEndTime = moment(reservation.end); const reservationEndTime = moment.utc(reservation.end);
const lockedTime = moment(lockEntry.timestamp); const lockedTime = moment.utc(lockEntry.timestamp);
const timeDifference = Math.abs(reservationEndTime.diff(lockedTime, 'minutes')); const timeDifference = Math.abs(reservationEndTime.diff(lockedTime, 'minutes'));
const timeIntervalsToCharge = Math.floor(timeDifference / timeResolution); const timeIntervalsToCharge = Math.floor(timeDifference / timeResolution);
@@ -391,8 +237,8 @@ const getIncidentData = (reservation) => {
getFirstPreviousBooking(reservation) getFirstPreviousBooking(reservation)
.then((previousReservation) => { .then((previousReservation) => {
if (previousReservation){ if (previousReservation){
const previousReservationEnd = moment(previousReservation.end); const previousReservationEnd = moment.utc(previousReservation.end);
const currentReservationStart = moment(reservation.start); const currentReservationStart = moment.utc(reservation.start);
const timeDifference = Math.abs(currentReservationStart.diff(previousReservationEnd, 'minutes')); const timeDifference = Math.abs(currentReservationStart.diff(previousReservationEnd, 'minutes'));
const maxBackToBackDifference = parseInt(process.env.MAX_BACK_TO_BACK_DIFFERENCE) || 0; const maxBackToBackDifference = parseInt(process.env.MAX_BACK_TO_BACK_DIFFERENCE) || 0;

View File

@@ -41,7 +41,7 @@ const getAllFinishedBookings = () => {
const filters = { const filters = {
canceled: false, canceled: false,
end: { end: {
[Op.lt]: moment().toISOString() [Op.lt]: moment.utc().toISOString()
} }
}; };
@@ -56,8 +56,8 @@ const getAllFinishedBookings = () => {
const getFirstNextBooking = (reservation) => { const getFirstNextBooking = (reservation) => {
return new Promise ((resolve, reject) => { return new Promise ((resolve, reject) => {
const {resourceId, start, timezone} = reservation; const { resourceId, start } = reservation;
const endOfTheDay = moment.tz(start, timezone).endOf('Day').toISOString(); const endOfTheDay = moment.utc(start).endOf('Day').toISOString();
const attributes = ['reservationId', 'memberId', 'resourceId', 'start', 'end', 'timezone']; const attributes = ['reservationId', 'memberId', 'resourceId', 'start', 'end', 'timezone'];
const filters = { const filters = {
@@ -90,8 +90,8 @@ const getFirstNextBooking = (reservation) => {
const getFirstPreviousBooking = (reservation) => { const getFirstPreviousBooking = (reservation) => {
return new Promise ((resolve, reject) => { return new Promise ((resolve, reject) => {
const {resourceId, start, timezone} = reservation; const { resourceId, start } = reservation;
const startOfTheDay = moment.tz(start, timezone).startOf('Day').toISOString(); const startOfTheDay = moment.utc(start).startOf('Day').toISOString();
const attributes = ['reservationId', 'memberId', 'resourceId', 'start', 'end', 'timezone']; const attributes = ['reservationId', 'memberId', 'resourceId', 'start', 'end', 'timezone'];
const filters = { const filters = {