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

View File

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

View File

@@ -2,6 +2,7 @@ import React from 'react';
import { Loader } from 'semantic-ui-react';
import ReactTable from 'react-table';
import 'react-table/react-table.css';
import { NavLink } from 'react-router-dom';
import {incidentsReportHeaderTitles} from '../../constants/menuItems';
import {
@@ -13,7 +14,7 @@ import {
const MemberIncidentsTable = props => {
const { loading, title } = props;
const { loading, title, openMemberSummaryOnMemberClick } = props;
const incidents = props.incidents ? props.incidents : [];
const columns = [];
@@ -34,9 +35,15 @@ const MemberIncidentsTable = props => {
Header: incidentsReportHeaderTitles[header],
accessor: header,
Cell: props => {
let cellValue;
let cellValue = '';
let urlValue = undefined;
switch (props.column.id) {
case 'memberName':
const memberId = props.row['_original'].memberId;
urlValue = `/practice-summary-report/${memberId}`;
cellValue = props.value;
break;
case 'incidentType':
cellValue = incidentDescriptions[props.value];
break;
@@ -68,7 +75,17 @@ const MemberIncidentsTable = props => {
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 = [
{
id: 'home',
showInMenu: true,
title: 'Home',
url: '/',
component: Home,
},
{
id: 'practiceSummaryReport',
showInMenu: true,
title: 'Practice Summary Report',
url: '/practice-summary-report',
component: PracticeSummaryReport,
},
{
id: 'practiceSummaryReportWithMemberId',
showInMenu: false,
url: '/practice-summary-report/:memberId',
component: PracticeSummaryReport,
},
{
id: 'report',
showInMenu: true,
title: 'Incidents Report',
url: '/incidents-report',
component: IncidentsReport,
},
{
id: 'uploadDLockData',
showInMenu: true,
title: 'DLock',
url: '/dlock',
component: UploadDLockData,

View File

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

View File

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

View File

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

View File

@@ -27,7 +27,13 @@ const uploadDoorLockData = (req, res) => {
parserResults.forEach((parserResult) => {
parsedData.push(...parserResult.parsedData);
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 = [];

View File

@@ -3,10 +3,11 @@
const db = require('../../models');
const fs = require('fs');
const csv = require('csv-parser');
const moment = require('moment/moment');
const moment = require('moment-timezone');
const Op = require('sequelize').Op;
const {
UI_TIMEZONE,
USER_ENTRY_EVENT,
ENABLE_PASSAGE_MODE,
DISABLE_PASSAGE_MODE,
@@ -38,7 +39,7 @@ const parseDoorLockDataFile = (file) => {
return new Promise ((resolve, reject) => {
const results = [];
const errors = [];
const unknownMembers = [];
const unknownMembersToReport = [];
let isValidFile = true;
const prefetchDataJobs = [getMappingsFromDatabase(), fetchAllMembers()];
@@ -48,6 +49,11 @@ const parseDoorLockDataFile = (file) => {
const mappings = result[0];
const allMembers = result[1];
const membersMap = {};
const unknownMembersMap = {};
allMembers.forEach((member) => membersMap[member.name] = member);
const mappingFromFileName = extractMappingFromFileName(file.name);
const mappingObject = checkIfMappingExsists(mappingFromFileName, mappings);
if (!mappingObject){
@@ -104,13 +110,15 @@ const parseDoorLockDataFile = (file) => {
const secondEntry = results[i+1];
if (firstEntry && (firstEntry.event === USER_ENTRY_EVENT)){
const memberObject = allMembers.find(member => member.name === firstEntry.name);
const memberObject = membersMap[firstEntry.name];
if (!memberObject){
//Check if member is already labeled as unknown
const unknownMember = unknownMembers.find((member) => member.details === firstEntry.name);
const unknownMember = unknownMembersMap[firstEntry.name];
if (!unknownMember){
unknownMembers.push({
unknownMembersMap[firstEntry.name] = firstEntry.name;
unknownMembersToReport.push({
error: csvParserErrors.UNKNOWN_MEMBER,
details: firstEntry.name,
file: file.name,
@@ -123,7 +131,7 @@ const parseDoorLockDataFile = (file) => {
doorLockEvents.USER_UNLOCKED : doorLockEvents.USER_LOCKED;
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
if (memberObject){
@@ -159,7 +167,7 @@ const parseDoorLockDataFile = (file) => {
}
resolve({
parsedData,
unknownMembers,
unknownMembers: unknownMembersToReport,
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 asyncJobs = [];
incidents.forEach((incident) => {
@@ -333,8 +179,8 @@ const getIncidentData = (reservation) => {
if (nextReservation){
// 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
const firstReservationEnd = moment(reservation.end);
const secondReservationStart = moment(nextReservation.start);
const firstReservationEnd = moment.utc(reservation.end);
const secondReservationStart = moment.utc(nextReservation.start);
const timeDifference = Math.abs(secondReservationStart.diff(firstReservationEnd, 'minutes'));
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 chargePrice = parseFloat(process.env.UNSCHEDULED_USE_CHARGE_FEE) || 5;
const reservationEndTime = moment(reservation.end);
const lockedTime = moment(lockEntry.timestamp);
const reservationEndTime = moment.utc(reservation.end);
const lockedTime = moment.utc(lockEntry.timestamp);
const timeDifference = Math.abs(reservationEndTime.diff(lockedTime, 'minutes'));
const timeIntervalsToCharge = Math.floor(timeDifference / timeResolution);
@@ -391,8 +237,8 @@ const getIncidentData = (reservation) => {
getFirstPreviousBooking(reservation)
.then((previousReservation) => {
if (previousReservation){
const previousReservationEnd = moment(previousReservation.end);
const currentReservationStart = moment(reservation.start);
const previousReservationEnd = moment.utc(previousReservation.end);
const currentReservationStart = moment.utc(reservation.start);
const timeDifference = Math.abs(currentReservationStart.diff(previousReservationEnd, 'minutes'));
const maxBackToBackDifference = parseInt(process.env.MAX_BACK_TO_BACK_DIFFERENCE) || 0;

View File

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