Fix for loading

This commit is contained in:
Senad Uka
2019-07-25 06:53:32 +02:00
parent a691ab94c7
commit 2db47e95e1
26 changed files with 838 additions and 400 deletions

View File

@@ -0,0 +1,53 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Button, Modal } from 'semantic-ui-react';
import { addFeesToOrd } from '../../store/actions';
class GenerateFeesInORDButton extends Component {
state = { open: false };
show = size => () => this.setState({ size, open: true });
close = () => this.setState({ open: false });
confirm = () => {
const { addFeesToOrd, dateRange, memberIds } = this.props;
if (dateRange){
addFeesToOrd(dateRange, memberIds);
}
this.close();
};
render() {
const { open, size } = this.state;
const { singleMember, disabled } = this.props;
const modalContent = singleMember ?
'This will remove all existing fees in ORD for selected member in selected date range and generate new fees based on shown incident tables. Do you want to continue ?':
'This will remove all existing fees in ORD for all members in selected date range and generate new fees based on shown incident tables. Do you want to continue ?';
return (
<div>
<Button disabled={disabled} onClick={this.show('tiny')}>Generate fees in ORD</Button>
<Modal size={size} open={open} onClose={this.close}>
<Modal.Header>Add fees to the ORD</Modal.Header>
<Modal.Content>
<p>{modalContent}</p>
</Modal.Content>
<Modal.Actions>
<Button negative onClick={this.close}>No</Button>
<Button positive icon='checkmark' onClick={this.confirm} labelPosition='right' content='Yes' />
</Modal.Actions>
</Modal>
</div>
);
}
}
const mapDispatchToProps = (dispatch) => ({
addFeesToOrd: (dateRange, memberIds) => addFeesToOrd(dispatch, dateRange, memberIds),
});
export default connect(null, mapDispatchToProps)(GenerateFeesInORDButton);

View File

@@ -1,111 +0,0 @@
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 {
incidentDescriptions,
incidentLevelDescriptions,
UNLOCKED_INCIDENT,
UNSCHEDULED_INCIDENT
} from '../../constants/enums';
const MemberIncidentsTable = props => {
const { loading, title, openMemberSummaryOnMemberClick } = props;
const incidents = props.incidents ? props.incidents : [];
const columns = [];
if (incidents && incidents.length > 0){
const incidentHeaders = Object.keys(incidentsReportHeaderTitles);
incidentHeaders.forEach((header) => {
const columnTitle = incidentsReportHeaderTitles[header];
if (columnTitle){
const columnAlignments = {
left: 'left',
right: 'right',
};
let columnContentsAlignment = columnAlignments.left;
columns.push({
Header: incidentsReportHeaderTitles[header],
accessor: header,
Cell: props => {
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;
case 'incidentLevel':
cellValue = incidentLevelDescriptions[props.value];
break;
case 'feeDescription':
const { incidentType, incidentLevel, timeIntervalsToCharge } = props.row['_original'];
switch (incidentType) {
case UNLOCKED_INCIDENT:
cellValue = `${incidentLevelDescriptions[incidentLevel]}`;
break;
case UNSCHEDULED_INCIDENT:
cellValue = `${timeIntervalsToCharge} x 5 min`;
break;
default:
cellValue = '';
break;
}
break;
case 'totalChargeFee':
const totalFee = props.value ? props.value : props.row['_original'].incidentPrice;
const totalFeeFormatted = parseFloat(totalFee).toFixed(2);
cellValue = `$ ${totalFeeFormatted}`;
columnContentsAlignment = columnAlignments.right;
break;
default:
cellValue = props.value;
}
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>
}
});
}
});
}
return (
<div>
<h4>{title}</h4>
<Loader active={loading} />
{
!loading && incidents &&
<ReactTable
data={incidents}
multiSort={false}
columns={columns}
/>
}
</div>
);
};
export default MemberIncidentsTable;

View File

@@ -5,26 +5,50 @@ import { Container } from 'semantic-ui-react';
import MainMenu from '../../components/MainMenu'; import MainMenu from '../../components/MainMenu';
import DateRangePicker from '../../components/DateRangePicker'; import DateRangePicker from '../../components/DateRangePicker';
import MemberIncidentsTables from '../../components/MemberIncidentsTables'; import MemberIncidentsTables from '../../components/MemberIncidentsTables';
import GenerateFeesInORDButton from '../../components/GenerateFeesInORDButton';
import { fetchIncidents } from '../../store/actions'; import { fetchIncidents, addFeesToOrd } from '../../store/actions';
class IncidentsReport extends Component { class IncidentsReport extends Component {
onDatesUpdate(dateRange) { state = {dateRange: null};
onDatesUpdate = (dateRange) => {
const { fetchIncidents } = this.props; const { fetchIncidents } = this.props;
this.setState({dateRange});
fetchIncidents(dateRange); fetchIncidents(dateRange);
} };
render () { render () {
const { pendingIncidents, incidents } = this.props; const { pendingIncidents, incidents, pendingAddFeesStatus } = this.props;
const { dateRange } = this.state;
const loading = pendingIncidents || pendingAddFeesStatus;
const membersMap = {};
if (incidents && Array.isArray(incidents)) {
incidents.forEach((incident) => {
membersMap[incident.memberId] = true;
});
}
const memberIds = Object.keys(membersMap) || [];
return ( return (
<Container> <Container>
<MainMenu/> <MainMenu/>
<h3>Incidents Report</h3> <h3>Incidents Report</h3>
<hr/> <hr/>
<DateRangePicker buttonLabel="Show report" onDatesUpdate={this.onDatesUpdate.bind(this)} /> <DateRangePicker buttonLabel="Show report" onDatesUpdate={this.onDatesUpdate} inlineButton />
<br/> <br/>
<MemberIncidentsTables pendingIncidents={pendingIncidents} incidents={incidents} /> <GenerateFeesInORDButton
memberIds={memberIds}
disabled={loading}
dateRange={dateRange}
/>
<br/><br/>
<hr/>
<br/>
<MemberIncidentsTables pendingIncidents={loading} incidents={incidents} />
</Container> </Container>
); );
} }
@@ -33,10 +57,12 @@ class IncidentsReport extends Component {
const mapStateToProps = (state) => ({ const mapStateToProps = (state) => ({
pendingIncidents: state.incidentsReport.pending, pendingIncidents: state.incidentsReport.pending,
incidents: state.incidentsReport.result, incidents: state.incidentsReport.result,
pendingAddFeesStatus: state.addFeesStatus.pending,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
fetchIncidents: (dateRange) => fetchIncidents(dispatch, dateRange), fetchIncidents: (dateRange) => fetchIncidents(dispatch, dateRange),
addFeesToOrd: (dateRange, memberIds) => addFeesToOrd(dispatch, dateRange, memberIds),
}); });
export default connect(mapStateToProps, mapDispatchToProps)(IncidentsReport); export default connect(mapStateToProps, mapDispatchToProps)(IncidentsReport);

View File

@@ -1,12 +1,13 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import {Container, Grid} from 'semantic-ui-react'; import { Container, Grid } from 'semantic-ui-react';
import MainMenu from '../../components/MainMenu'; import MainMenu from '../../components/MainMenu';
import DateRangePicker from '../../components/DateRangePicker'; import DateRangePicker from '../../components/DateRangePicker';
import MemberSelector from './components/MemberSelector'; import MemberSelector from './components/MemberSelector';
import MemberSummary from './components/MemberSummary'; import MemberSummary from './components/MemberSummary';
import MemberIncidentsTables from '../../components/MemberIncidentsTables'; import MemberIncidentsTables from '../../components/MemberIncidentsTables';
import GenerateFeesInORDButton from '../../components/GenerateFeesInORDButton';
import { fetchMemberIncidents } from '../../store/actions'; import { fetchMemberIncidents } from '../../store/actions';
@@ -39,8 +40,12 @@ class PracticeSummaryReport extends Component {
} }
render () { render () {
const { memberIncidents, loading } = this.props; const { memberIncidents, loadingMemberIncidents, loadingAddFeesStatus } = this.props;
const { memberId } = this.state; const { memberId, dateRange } = this.state;
const loading = loadingAddFeesStatus || loadingMemberIncidents;
const addFeesButtonDisabled = !memberId || !dateRange || loading;
return ( return (
<Container> <Container>
@@ -64,6 +69,16 @@ class PracticeSummaryReport extends Component {
/> />
</Grid.Column> </Grid.Column>
</Grid.Row> </Grid.Row>
<Grid.Row>
<Grid.Column>
<GenerateFeesInORDButton
singleMember
disabled={addFeesButtonDisabled}
dateRange={dateRange}
memberIds={[memberId]}
/>
</Grid.Column>
</Grid.Row>
<Grid.Row/> <Grid.Row/>
<Grid.Row> <Grid.Row>
<Grid.Column> <Grid.Column>
@@ -78,7 +93,8 @@ class PracticeSummaryReport extends Component {
const mapStateToProps = (state) => ({ const mapStateToProps = (state) => ({
memberIncidents: state.memberIncidents.result, memberIncidents: state.memberIncidents.result,
loading: state.memberIncidents.pending, loadingMemberIncidents: state.memberIncidents.pending,
loadingAddFeesStatus: state.addFeesStatus.pending,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({

View File

@@ -14,6 +14,9 @@ import {
FETCH_MEMBER_INCIDENTS_PENDING, FETCH_MEMBER_INCIDENTS_PENDING,
FETCH_MEMBER_INCIDENTS_SUCCESS, FETCH_MEMBER_INCIDENTS_SUCCESS,
FETCH_MEMBER_INCIDENTS_FAILED, FETCH_MEMBER_INCIDENTS_FAILED,
ADD_FEES_TO_ORD_PENDING,
ADD_FEES_TO_ORD_SUCCESS,
ADD_FEES_TO_ORD_FAILED,
} from '../constants'; } from '../constants';
import API from '../../utilities/api'; import API from '../../utilities/api';
@@ -78,3 +81,17 @@ export const fetchMemberIncidents = (dispatch, memberId, dateRange) => {
dispatch({type: FETCH_MEMBER_INCIDENTS_FAILED, payload: error.response}); dispatch({type: FETCH_MEMBER_INCIDENTS_FAILED, payload: error.response});
}); });
}; };
export const addFeesToOrd = (dispatch, dateRange, memberIds) => {
dispatch({type: ADD_FEES_TO_ORD_PENDING});
API.post(`integration/addFees`, {
dateRange,
memberIds: memberIds || [],
})
.then(response => {
dispatch({type: ADD_FEES_TO_ORD_SUCCESS, payload: response.data});
})
.catch(error => {
dispatch({type: ADD_FEES_TO_ORD_FAILED, payload: error.response});
});
};

View File

@@ -21,3 +21,7 @@ export const FETCH_MEMBERS_FAILED = 'FETCH_MEMBERS_FAILED';
export const FETCH_MEMBER_INCIDENTS_PENDING = 'FETCH_MEMBER_INCIDENTS_PENDING'; export const FETCH_MEMBER_INCIDENTS_PENDING = 'FETCH_MEMBER_INCIDENTS_PENDING';
export const FETCH_MEMBER_INCIDENTS_SUCCESS = 'FETCH_MEMBER_INCIDENTS_SUCCESS'; export const FETCH_MEMBER_INCIDENTS_SUCCESS = 'FETCH_MEMBER_INCIDENTS_SUCCESS';
export const FETCH_MEMBER_INCIDENTS_FAILED = 'FETCH_MEMBER_INCIDENTS_FAILED'; export const FETCH_MEMBER_INCIDENTS_FAILED = 'FETCH_MEMBER_INCIDENTS_FAILED';
export const ADD_FEES_TO_ORD_PENDING = 'ADD_FEES_TO_ORD_PENDING';
export const ADD_FEES_TO_ORD_SUCCESS = 'ADD_FEES_TO_ORD_SUCCESS';
export const ADD_FEES_TO_ORD_FAILED = 'ADD_FEES_TO_ORD_FAILED';

View File

@@ -0,0 +1,38 @@
import {
ADD_FEES_TO_ORD_PENDING,
ADD_FEES_TO_ORD_SUCCESS,
ADD_FEES_TO_ORD_FAILED,
} from '../constants';
const initialState = {
pending: false,
result: null,
error: null,
};
export const addFeesStatus = (state, action) => {
state = state || initialState;
action = action || {};
switch(action.type){
case ADD_FEES_TO_ORD_PENDING:
return Object.assign({}, state, {
pending: true,
error: null,
});
case ADD_FEES_TO_ORD_SUCCESS:
return Object.assign({}, state, {
pending: false,
result: action.payload,
error: null,
});
case ADD_FEES_TO_ORD_FAILED:
return Object.assign({}, state, {
pending: false,
result: {},
error: action.payload,
});
default:
return state;
}
};

View File

@@ -6,6 +6,7 @@ import { addMapping } from './addMappingReducer';
import { incidentsReport } from './incidentsReportReducer'; import { incidentsReport } from './incidentsReportReducer';
import { membersList } from './membersListReducer'; import { membersList } from './membersListReducer';
import { memberIncidents} from './memberIncidentsReducer'; import { memberIncidents} from './memberIncidentsReducer';
import { addFeesStatus } from './addFeesToOrdReducer';
export const rootReducer = combineReducers({ export const rootReducer = combineReducers({
doorLockData, doorLockData,
@@ -14,5 +15,6 @@ export const rootReducer = combineReducers({
incidentsReport, incidentsReport,
membersList, membersList,
memberIncidents, memberIncidents,
addFeesStatus,
}); });

View File

@@ -1,3 +1,9 @@
const pool = {
max: parseInt(process.env.DB_POOL_MAX_CONNECTIONS) || 5,
acquire: parseInt(process.env.DB_POOL_ACQUIRE) || 60000,
evict: parseInt(process.env.DB_POOL_EVICT) || 1000,
};
module.exports = { module.exports = {
development: { development: {
username: 'docker', username: 'docker',
@@ -5,14 +11,17 @@ module.exports = {
database: 'CrmIntegration', database: 'CrmIntegration',
port: '5431', port: '5431',
dialect: 'postgres', dialect: 'postgres',
logging: parseInt(process.env.SEQUELIZE_LOGGING) ? console.log : false logging: parseInt(process.env.SEQUELIZE_LOGGING) ? console.log : false,
pool
}, },
test: { test: {
"use_env_variable": 'DATABASE_URL', "use_env_variable": 'DATABASE_URL',
logging: parseInt(process.env.SEQUELIZE_LOGGING) ? console.log : false logging: parseInt(process.env.SEQUELIZE_LOGGING) ? console.log : false,
pool
}, },
production: { production: {
"use_env_variable": 'DATABASE_URL', "use_env_variable": 'DATABASE_URL',
logging: parseInt(process.env.SEQUELIZE_LOGGING) ? console.log : false logging: parseInt(process.env.SEQUELIZE_LOGGING) ? console.log : false,
pool
} }
}; };

View File

@@ -1,16 +0,0 @@
{
"development": {
"username": "docker",
"password": "docker",
"database": "CrmIntegration",
"port": "5431",
"dialect": "postgres",
"logging": false
},
"test": {
"use_env_variable": "DATABASE_URL"
},
"production": {
"use_env_variable": "DATABASE_URL"
}
}

View File

@@ -13,32 +13,38 @@ const unlockedIncidentLevelsPrices = {
UNLOCKED_0: { UNLOCKED_0: {
id: 0, id: 0,
title: 'UNLOCKED_0', title: 'UNLOCKED_0',
price: parseInt(process.env.UNLOCK_0) || 0 price: parseInt(process.env.UNLOCK_0) || 0,
description: 'First month - warning',
}, },
UNLOCKED_1: { UNLOCKED_1: {
id: 1, id: 1,
title: 'UNLOCKED_1', title: 'UNLOCKED_1',
price: parseInt(process.env.UNLOCK_1) || 10 price: parseInt(process.env.UNLOCK_1) || 10,
description: 'Second month',
}, },
UNLOCKED_2: { UNLOCKED_2: {
id: 2, id: 2,
title: 'UNLOCKED_2', title: 'UNLOCKED_2',
price: parseInt(process.env.UNLOCK_2) || 20 price: parseInt(process.env.UNLOCK_2) || 20,
description: 'Third month',
}, },
UNLOCKED_3: { UNLOCKED_3: {
id: 3, id: 3,
title: 'UNLOCKED_3', title: 'UNLOCKED_3',
price: parseInt(process.env.UNLOCK_3) || 30 price: parseInt(process.env.UNLOCK_3) || 30,
description: 'Fourth month',
}, },
UNLOCKED_4: { UNLOCKED_4: {
id: 4, id: 4,
title: 'UNLOCKED_4', title: 'UNLOCKED_4',
price: parseInt(process.env.UNLOCK_4) || 40 price: parseInt(process.env.UNLOCK_4) || 40,
description: 'Fifth month',
}, },
UNLOCKED_5: { UNLOCKED_5: {
id: 5, id: 5,
title: 'UNLOCKED_5', title: 'UNLOCKED_5',
price: parseInt(process.env.UNLOCK_5) || 50 price: parseInt(process.env.UNLOCK_5) || 50,
description: 'Sixth month and onward',
} }
}; };
const csvParserErrors = { const csvParserErrors = {
@@ -50,9 +56,13 @@ const csvParserErrors = {
}; };
const officeRnDAPIErrors = { const officeRnDAPIErrors = {
FAILED_TO_FETCH_MEMBERS: 'Failed to fetch members', FAILED_TO_FETCH_MEMBERS: 'Failed to fetch members',
FAILED_TO_FETCH_BOOKINGS: 'Failed to fetch booking reservations' FAILED_TO_FETCH_BOOKINGS: 'Failed to fetch booking reservations',
FAILED_TO_FETCH_FEES: 'Failed to fetch existing fees',
FAILED_TO_DELETE_FEES: 'Failed to delete fees in ORD',
FAILED_TO_ADD_FEES: 'Failed to add fees in ORD',
}; };
const integrationServiceErrors = { const integrationServiceErrors = {
IMPORT_SUCCESSFUL_CALCULATION_FAILED: 'Import succeeded but fetching reservations and calculating charges failed',
FAILED_TO_SAVE_BOOKINGS: 'Failed to save booking reservations', FAILED_TO_SAVE_BOOKINGS: 'Failed to save booking reservations',
FAILED_TO_SAVE_DOOR_LOCK_ENTRIES: 'Failed to save door lock entries', FAILED_TO_SAVE_DOOR_LOCK_ENTRIES: 'Failed to save door lock entries',
FAILED_TO_SAVE_DATA_GENERIC: 'Failed to save data', FAILED_TO_SAVE_DATA_GENERIC: 'Failed to save data',
@@ -71,6 +81,16 @@ const incidentType = {
BOOKING_CANCELED_LATE: 9, BOOKING_CANCELED_LATE: 9,
}; };
const incidentTypeExplanations = {};
incidentTypeExplanations[incidentType.UNLOCKED_INCIDENT_RELATED_WITH_RESERVATION] = 'Door left unlocked';
incidentTypeExplanations[incidentType.UNLOCKED_INCIDENT_STANDALONE] = 'Door left unlocked';
incidentTypeExplanations[incidentType.UNSCHEDULED_INCIDENT_BEFORE_RESERVATION] = 'Room used before reservation started';
incidentTypeExplanations[incidentType.UNSCHEDULED_INCIDENT_AFTER_RESERVATION] = 'Room used after reservation started';
incidentTypeExplanations[incidentType.UNSCHEDULED_INCIDENT_STANDALONE] = 'Room used without reservation';
incidentTypeExplanations[incidentType.BOOKING_MOVED_TO_ANOTHER_DAY] = 'Reservation moved to another day';
incidentTypeExplanations[incidentType.BOOKING_SHORTENED] = 'Reservation shortened';
incidentTypeExplanations[incidentType.BOOKING_CANCELED_LATE] = 'A reservation canceled after the grace period';
const UI_TIMEZONE = process.env.UI_TIMEZONE || 'America/Los_Angeles'; const UI_TIMEZONE = process.env.UI_TIMEZONE || 'America/Los_Angeles';
const DEFAULT_DATE_FORMAT = 'YYYY-MM-DD'; const DEFAULT_DATE_FORMAT = 'YYYY-MM-DD';
@@ -94,6 +114,7 @@ module.exports = {
unlockedIncidentLevelsPrices, unlockedIncidentLevelsPrices,
integrationServiceErrors, integrationServiceErrors,
incidentType, incidentType,
incidentTypeExplanations,
UI_TIMEZONE, UI_TIMEZONE,
DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT,
MAX_BACK_TO_BACK_DIFFERENCE, MAX_BACK_TO_BACK_DIFFERENCE,

View File

@@ -4,6 +4,7 @@ const { parseDoorLockDataFile, writeDoorLockEvent } = require('../services/doorL
const { fetchAllBookings, writeBookingReservation } = require('../services/officeRnD/bookings'); const { fetchAllBookings, writeBookingReservation } = require('../services/officeRnD/bookings');
const { calculateDoorLockCharges } = require('../services/integration/doorLockCharges'); const { calculateDoorLockCharges } = require('../services/integration/doorLockCharges');
const { integrationServiceErrors } = require('../constants/constants'); const { integrationServiceErrors } = require('../constants/constants');
const { checkBookingChanges } = require('../services/integration/checkBookingChange');
const IncomingForm = require('formidable').IncomingForm; const IncomingForm = require('formidable').IncomingForm;
@@ -42,11 +43,25 @@ const uploadDoorLockData = (req, res) => {
Promise.all(asyncWriteJobs) Promise.all(asyncWriteJobs)
.then(() => { .then(() => {
res.json({ checkBookingChanges()
parsedData, .then(() => {
parserErrors, calculateDoorLockCharges()
unknownMembers .then(() => {
}); res.json({
parsedData,
parserErrors,
unknownMembers
});
})
.catch((error) => {
console.log('Error : ', error);
res.status(500).send(integrationServiceErrors.IMPORT_SUCCESSFUL_CALCULATION_FAILED);
});
})
.catch((error) => {
console.log('Error : ', error);
res.status(500).send(integrationServiceErrors.IMPORT_SUCCESSFUL_CALCULATION_FAILED);
});
}) })
.catch((error) => { .catch((error) => {
console.log(integrationServiceErrors.FAILED_TO_SAVE_DOOR_LOCK_ENTRIES); console.log(integrationServiceErrors.FAILED_TO_SAVE_DOOR_LOCK_ENTRIES);
@@ -54,7 +69,7 @@ const uploadDoorLockData = (req, res) => {
res.status(500).send(integrationServiceErrors.FAILED_TO_SAVE_DATA_GENERIC); res.status(500).send(integrationServiceErrors.FAILED_TO_SAVE_DATA_GENERIC);
}); });
fetchAllBookings() /*fetchAllBookings()
.then((bookingEntries) => { .then((bookingEntries) => {
const asyncJobs = []; const asyncJobs = [];
bookingEntries.forEach((bookingEntry) => asyncJobs.push(writeBookingReservation(bookingEntry))); bookingEntries.forEach((bookingEntry) => asyncJobs.push(writeBookingReservation(bookingEntry)));
@@ -62,13 +77,18 @@ const uploadDoorLockData = (req, res) => {
Promise.all(asyncJobs) Promise.all(asyncJobs)
.then(() => { .then(() => {
calculateDoorLockCharges(); calculateDoorLockCharges();
}); })
.catch((error) => {
console.log('Error updating booking reservations : ');
console.log(error);
})
}) })
.catch((error) => { .catch((error) => {
console.log(integrationServiceErrors.FAILED_TO_SAVE_BOOKINGS); console.log(integrationServiceErrors.FAILED_TO_SAVE_BOOKINGS);
console.log(error); console.log(error);
return; return;
}); });
*/
}) })
.catch((error) => { .catch((error) => {
res.status(500).send(error); res.status(500).send(error);

View File

@@ -2,6 +2,9 @@
const { getMappingsFromDatabase, fetchOffices, fetchResources, saveNewMappingToDatabase } = require('../services/officeRnD/resources'); const { getMappingsFromDatabase, fetchOffices, fetchResources, saveNewMappingToDatabase } = require('../services/officeRnD/resources');
const { getAllIncidents } = require('../services/integration/reports'); const { getAllIncidents } = require('../services/integration/reports');
const { getMembersFeesForDateRange } = require('../services/integration/invoiceIntegration');
const { deleteFeesFromORD, addFeesToORD } = require('../services/officeRnD/fees');
const { checkBookingChanges } = require('../services/integration/checkBookingChange');
const getKnownOfficeResourceMappings = (req, res) => { const getKnownOfficeResourceMappings = (req, res) => {
const dataToFetch = [getMappingsFromDatabase(), fetchOffices(), fetchResources() ]; const dataToFetch = [getMappingsFromDatabase(), fetchOffices(), fetchResources() ];
@@ -38,7 +41,7 @@ const getAllIncidentsController = (req, res) => {
endDate: req.params.endDate, endDate: req.params.endDate,
}; };
getAllIncidents(dateRange) getAllIncidents(dateRange, [])
.then((incidents) => { .then((incidents) => {
res.send(incidents); res.send(incidents);
}) })
@@ -55,7 +58,7 @@ const getMemberIncidents = (req, res) => {
endDate: req.params.endDate, endDate: req.params.endDate,
}; };
getAllIncidents(dateRange, memberId) getAllIncidents(dateRange, [memberId])
.then((incidents) => { .then((incidents) => {
res.send(incidents); res.send(incidents);
}) })
@@ -65,9 +68,53 @@ const getMemberIncidents = (req, res) => {
}); });
}; };
const addFees = (req, res) => {
const memberIds = req.body && req.body.memberIds ? req.body.memberIds : [];
const dateRange = req.body && req.body.dateRange ? req.body.dateRange : {startDate: null, endDate: null};
const { startDate, endDate } = dateRange;
if (startDate && endDate && Array.isArray(memberIds)){
checkBookingChanges()
.then(() => {
deleteFeesFromORD(dateRange, memberIds)
.then(() => {
// TODO: Change this to parallel execution
getMembersFeesForDateRange(dateRange, memberIds)
.then((allFees) => {
addFeesToORD(allFees)
.then((numberOfInsertedFees) => {
res.send({
status: 'ok',
numberOfInsertedFees
});
})
.catch((error) => {
console.log('Error adding fees');
res.status(500).send(error);
})
})
.catch((error) => {
console.log(error);
res.status(500).send(error);
})
})
.catch((error) => {
res.status(500).send(error);
});
})
.catch((error) => {
console.log('Error with updating booking reservations');
res.status(500).send(error);
})
}else{
res.status(400).send('Date range is missing');
}
};
module.exports = { module.exports = {
getKnownOfficeResourceMappings, getKnownOfficeResourceMappings,
addNewMapping, addNewMapping,
getAllIncidentsController, getAllIncidentsController,
getMemberIncidents, getMemberIncidents,
addFees,
}; };

View File

@@ -1,41 +1,6 @@
'use strict'; 'use strict';
require('dotenv').config(); require('dotenv').config();
const { fetchAllBookings, bulkWriteReservationsWithChangesTracking } = require('../services/officeRnD/bookings'); const { checkBookingChanges } = require('../services/integration/checkBookingChange');
const { chargeBookingChanges } = require('../services/integration/bookingChangeCharges'); checkBookingChanges().then(() => process.exit()).catch(() => process.exit());
const { bulkWriteChanges } = require('../services/integration/bookingChangeLog');
const checkBookingChanges = () => {
fetchAllBookings()
.then((reservations) => {
bulkWriteReservationsWithChangesTracking(reservations)
.then((changes) => {
bulkWriteChanges(changes)
.then(() => {
chargeBookingChanges(changes)
.then(() => {
process.exit();
})
.catch((error) => {
console.log('Error creating charges ', error);
process.exit();
});
})
.catch((error) => {
console.log('Error bulk write booking reservation change log :', error);
process.exit();
});
})
.catch((error) => {
console.log('Error bulk write booking reservations :', error);
process.exit();
});
})
.catch((error) => {
console.log('Error fetching bookings from ORD ', error);
process.exit();
});
};
checkBookingChanges();

View File

@@ -24,3 +24,8 @@ BOOKING_CHANGE_PERCENTAGE_CHARGE=Percentage of hourly reate to apply for cancell
ALLOWED_BOOKING_CANCELLATION_TIME=Time from creation (in minutes) in which cancellation is not charged ALLOWED_BOOKING_CANCELLATION_TIME=Time from creation (in minutes) in which cancellation is not charged
SEQUELIZE_LOGGING=0 - false, 1 - true (console logging) SEQUELIZE_LOGGING=0 - false, 1 - true (console logging)
#More about pool option : http://docs.sequelizejs.com/class/lib/sequelize.js~Sequelize.html
DB_POOL_MAX_CONNECTIONS=Maximum number of connection in pool (ex. 18)
DB_POOL_ACQUIRE=The maximum time, in milliseconds, that pool will try to get connection before throwing error (ex. 120000)
DB_POOL_EVICT=The time interval, in milliseconds, after which sequelize-pool will remove idle connections. (ex. 10000)

View File

@@ -2,8 +2,14 @@
const { apiStatusCheck } = require('../controllers/apiStatusCheck'); const { apiStatusCheck } = require('../controllers/apiStatusCheck');
const { uploadDoorLockData } = require('../controllers/doorLock'); const { uploadDoorLockData } = require('../controllers/doorLock');
const { getKnownOfficeResourceMappings, addNewMapping, getAllIncidentsController, getMemberIncidents } = require('../controllers/integration');
const { fetchMembersList } = require('../controllers/officeRnD'); const { fetchMembersList } = require('../controllers/officeRnD');
const {
getKnownOfficeResourceMappings,
addNewMapping,
getAllIncidentsController,
getMemberIncidents,
addFees
} = require('../controllers/integration');
const { calculateDoorLockCharges } = require('../services/integration/doorLockCharges'); const { calculateDoorLockCharges } = require('../services/integration/doorLockCharges');
@@ -21,6 +27,8 @@ router.get('/integration/report/allIncidents/:startDate/:endDate', getAllInciden
router.get('/officeRnD/membersList', fetchMembersList); router.get('/officeRnD/membersList', fetchMembersList);
router.post('/integration/addFees', addFees);
// temporary route, manually trigger door lock charge calculations // temporary route, manually trigger door lock charge calculations
router.get('/calculate', (req, res) => { calculateDoorLockCharges(); res.send();}); router.get('/calculate', (req, res) => { calculateDoorLockCharges(); res.send();});

View File

@@ -1,150 +0,0 @@
'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,
};

View File

@@ -97,7 +97,7 @@ const chargeBookingChanges = (changes) => {
incidents.push(incident); incidents.push(incident);
} }
}else{ }else{
const differenceFromCreation = reservationCreationTimestamp.diff(moment.utc(), 'minutes'); const differenceFromCreation = moment.utc().diff(reservationCreationTimestamp, 'minutes');
if (differenceFromCreation > ALLOWED_BOOKING_CANCELLATION_TIME){ if (differenceFromCreation > ALLOWED_BOOKING_CANCELLATION_TIME){
const chargeFee = 2 * reservationHourlyRate * oldReservationLength * BOOKING_CHANGE_PERCENTAGE_CHARGE / 100; const chargeFee = 2 * reservationHourlyRate * oldReservationLength * BOOKING_CHANGE_PERCENTAGE_CHARGE / 100;

View File

@@ -0,0 +1,53 @@
'use strict';
const Op = require('sequelize').Op;
const db = require('../../models/index');
const moment = require('moment-timezone');
const { DEFAULT_DATE_FORMAT, UI_TIMEZONE } = require('../../constants/constants');
const getActiveBookingsForMembersInDateRange = (dateRange, memberIds) => {
const startDate = moment.tz(dateRange.startDate, DEFAULT_DATE_FORMAT, UI_TIMEZONE).startOf('day');
const endDate = moment.tz(dateRange.endDate, DEFAULT_DATE_FORMAT, UI_TIMEZONE).endOf('day');
const attributes = [
'id',
'reservationId',
'memberId',
'officeId',
'resourceId',
'start',
'end',
'timezone',
'canceled',
'hourlyRate'
];
const filters = {
canceled: false,
};
if (startDate && endDate) {
filters.start = {
[Op.gte]: startDate.toISOString()
};
filters.end = {
[Op.lte]: endDate.toISOString(),
};
}
if (memberIds.length > 0){
filters.memberId = {
[Op.in]: memberIds
};
}
return db.bookingReservation.findAll({
attributes,
where: filters,
});
};
module.exports = {
getActiveBookingsForMembersInDateRange,
};

View File

@@ -0,0 +1,44 @@
'use strict';
const { fetchAllBookings, bulkWriteReservationsWithChangesTracking } = require('../officeRnD/bookings');
const { chargeBookingChanges } = require('./bookingChangeCharges');
const { bulkWriteChanges } = require('./bookingChangeLog');
const checkBookingChanges = () => {
return new Promise((resolve, reject) => {
fetchAllBookings()
.then((reservations) => {
bulkWriteReservationsWithChangesTracking(reservations)
.then((changes) => {
bulkWriteChanges(changes)
.then(() => {
chargeBookingChanges(changes)
.then(() => {
resolve(true);
})
.catch((error) => {
console.log('Error creating charges ', error);
reject(error);
});
})
.catch((error) => {
console.log('Error bulk write booking reservation change log :', error);
reject(error);
});
})
.catch((error) => {
console.log('Error bulk write booking reservations :', error);
reject(error);
});
})
.catch((error) => {
console.log('Error fetching bookings from ORD ', error);
reject(error);
});
});
};
module.exports = {
checkBookingChanges
};

View File

@@ -510,52 +510,66 @@ const getIncidentData = (reservation) => {
}; };
const calculateDoorLockCharges = () => { const calculateDoorLockCharges = () => {
getAllFinishedBookings() return new Promise((resolve, reject) => {
.then((reservations) => { getAllFinishedBookings()
const unlockedIncidents = []; .then((reservations) => {
const unscheduledIncidents = []; const unlockedIncidents = [];
const unscheduledIncidents = [];
const asyncCheckForIncidents = []; const asyncCheckForIncidents = [];
reservations.forEach((reservation) => { reservations.forEach((reservation) => {
asyncCheckForIncidents.push(getIncidentData(reservation)); asyncCheckForIncidents.push(getIncidentData(reservation));
}); });
Promise.all(asyncCheckForIncidents) Promise.all(asyncCheckForIncidents)
.then((allReservationsIncidents) => { .then((allReservationsIncidents) => {
allReservationsIncidents.forEach((reservationIncidents) => { allReservationsIncidents.forEach((reservationIncidents) => {
reservationIncidents.forEach((incident) => { reservationIncidents.forEach((incident) => {
if (incident.error) { if (incident.error) {
console.log('Error checking incident : ', incident.error); console.log('Error checking incident : ', incident.error);
} else if (incident.incidentType) { } else if (incident.incidentType) {
switch (incident.incidentType) { switch (incident.incidentType) {
case incidentType.UNLOCKED_INCIDENT_RELATED_WITH_RESERVATION: case incidentType.UNLOCKED_INCIDENT_RELATED_WITH_RESERVATION:
case incidentType.UNLOCKED_INCIDENT_STANDALONE: case incidentType.UNLOCKED_INCIDENT_STANDALONE:
unlockedIncidents.push(incident); unlockedIncidents.push(incident);
break; break;
case incidentType.UNSCHEDULED_INCIDENT_BEFORE_RESERVATION: case incidentType.UNSCHEDULED_INCIDENT_BEFORE_RESERVATION:
case incidentType.UNSCHEDULED_INCIDENT_AFTER_RESERVATION: case incidentType.UNSCHEDULED_INCIDENT_AFTER_RESERVATION:
case incidentType.UNSCHEDULED_INCIDENT_STANDALONE: case incidentType.UNSCHEDULED_INCIDENT_STANDALONE:
unscheduledIncidents.push(incident); unscheduledIncidents.push(incident);
break; break;
}
} }
} });
}); });
});
insertUnscheduledIncidents(unscheduledIncidents) setUnlockedIncidentsLevel(unlockedIncidents)
.catch((error) => console.log(error)); .then((completedUnlockedIncidents) => {
const insertIncidentsJobs = [insertUnscheduledIncidents(unscheduledIncidents), insertUnlockedIncidents(completedUnlockedIncidents)];
setUnlockedIncidentsLevel(unlockedIncidents) Promise.all(insertIncidentsJobs)
.then((completedUnlockedIncidents) => { .then(() => {
insertUnlockedIncidents(completedUnlockedIncidents) resolve(true);
.catch((error) => console.log(error)); })
}) .catch((error) => {
.catch((error) => console.log(error)); reject(error);
}) });
.catch((error) => console.log(error));
}) /*
.catch((error) => console.log(error)); insertUnscheduledIncidents(unscheduledIncidents)
.catch((error) => console.log(error));
insertUnlockedIncidents(completedUnlockedIncidents)
.catch((error) => console.log(error));
*/
})
.catch((error) => reject(error));
})
.catch((error) => reject(error));
})
.catch((error) => reject(error));
});
}; };
module.exports = { module.exports = {

View File

@@ -0,0 +1,257 @@
'use strict';
const moment = require('moment-timezone');
const { getAllIncidents } = require('./reports');
const { getActiveBookingsForMembersInDateRange } = require('./bookings');
const { DEFAULT_DATE_FORMAT, UI_TIMEZONE, incidentTypeExplanations, incidentType, unlockedIncidentLevelsPrices } = require('../../constants/constants');
const { getResourceMappings } = require('../officeRnD/resources');
const { fetchAllMembers } = require('../officeRnD/members');
const createFeeFromIncident = (incident) => {
const {
memberId,
officeId,
officeName,
resourceName,
oldResourceName,
newResourceName,
bookingStartRaw,
bookingEndRaw,
oldBookingStartRaw,
oldBookingEndRaw,
newBookingStartRaw,
newBookingEndRaw,
unlockTimestampRaw,
lockTimestampRaw,
incidentLevel,
timeIntervalsToCharge,
incidentPrice,
chargePrice,
totalChargeFee,
incidentTimestampRaw
} = incident;
const incidentTypeNumber = incident.incidentType;
const incidentExplanation = incidentTypeExplanations[incidentTypeNumber];
let date = '';
let price = 0;
let quantity = 0;
let priceExplanation = '';
let bookingTimeExplanation = '';
let incidentTimeExplanation = '';
let additionalIncidentExplanation = '';
let roomExplanation = '';
let dateExplanation = '';
const bookingStartMoment = moment.tz(bookingStartRaw, UI_TIMEZONE);
const bookingEndMoment = moment.tz(bookingEndRaw, UI_TIMEZONE);
const unlockMoment = moment.tz(unlockTimestampRaw, UI_TIMEZONE);
const lockMoment = moment.tz(lockTimestampRaw, UI_TIMEZONE);
const oldBookingStartMoment = moment.tz(oldBookingStartRaw, UI_TIMEZONE);
const oldBookingEndMoment = moment.tz(oldBookingEndRaw, UI_TIMEZONE);
const newBookingStartMoment = moment.tz(newBookingStartRaw, UI_TIMEZONE);
const newBookingEndMoment = moment.tz(newBookingEndRaw, UI_TIMEZONE);
const incidentTimestampMoment = moment.tz(incidentTimestampRaw, UI_TIMEZONE);
switch (incidentTypeNumber){
case incidentType.UNLOCKED_INCIDENT_RELATED_WITH_RESERVATION:
roomExplanation = resourceName;
dateExplanation = bookingStartMoment.clone().startOf('day').format('MMM DD, YYYY');
bookingTimeExplanation = `${bookingStartMoment.clone().format('HH:mm a')} - ${bookingEndMoment.clone().format('HH:mm a')}`;
incidentTimeExplanation = `UNLOCK : ${unlockMoment.clone().format('HH:mm a')}`;
unlockedIncidentLevelsPrices[incidentLevel].description
additionalIncidentExplanation = unlockedIncidentLevelsPrices[incidentLevel].description;
date = bookingStartMoment.clone().startOf('day').format();
price = incidentPrice;
quantity = 1;
priceExplanation = `$${price.toFixed(2)}, 1 x $${price.toFixed(2)}`;
break;
case incidentType.UNSCHEDULED_INCIDENT_BEFORE_RESERVATION:
roomExplanation = resourceName;
dateExplanation = bookingStartMoment.clone().startOf('day').format('MMM DD, YYYY');
bookingTimeExplanation = `${bookingStartMoment.clone().format('HH:mm a')} - ${bookingEndMoment.clone().format('HH:mm a')}`;
incidentTimeExplanation = `UNLOCK : ${unlockMoment.clone().format('HH:mm a')}`;
date = bookingStartMoment.clone().startOf('day').format();
price = chargePrice;
quantity = timeIntervalsToCharge;
priceExplanation = `$${totalChargeFee.toFixed(2)}, ${quantity} x $${price.toFixed(2)}`;
break;
case incidentType.UNSCHEDULED_INCIDENT_AFTER_RESERVATION:
roomExplanation = resourceName;
dateExplanation = bookingStartMoment.clone().startOf('day').format('MMM DD, YYYY');
bookingTimeExplanation = `${bookingStartMoment.clone().format('HH:mm a')} - ${bookingEndMoment.clone().format('HH:mm a')}`;
incidentTimeExplanation = `LOCK : ${lockMoment.clone().format('HH:mm a')}`;
date = bookingStartMoment.clone().startOf('day').format();
price = chargePrice;
quantity = timeIntervalsToCharge;
priceExplanation = `$${totalChargeFee.toFixed(2)}, ${quantity} x $${price.toFixed(2)}`;
break;
case incidentType.UNLOCKED_INCIDENT_STANDALONE:
roomExplanation = resourceName;
dateExplanation = unlockMoment.clone().startOf('day').format('MMM DD, YYYY');
bookingTimeExplanation = `NO RESERVATION`;
incidentTimeExplanation = `UNLOCK : ${unlockMoment.clone().format('HH:mm a')}`;
additionalIncidentExplanation = unlockedIncidentLevelsPrices[incidentLevel].description;
date = unlockMoment.clone().startOf('day').format();
price = incidentPrice;
quantity = 1;
priceExplanation = `$${price.toFixed(2)}, 1 x $${price.toFixed(2)}`;
break;
case incidentType.UNSCHEDULED_INCIDENT_STANDALONE:
roomExplanation = resourceName;
dateExplanation = unlockMoment.clone().startOf('day').format('MMM DD, YYYY');
bookingTimeExplanation = `NO RESERVATION`;
incidentTimeExplanation = `UNLOCK : ${unlockMoment.clone().format('HH:mm a')} LOCK : ${lockMoment.clone().format('HH:mm a')}`;
additionalIncidentExplanation = '';
date = unlockMoment.clone().startOf('day').format();
price = chargePrice;
quantity = timeIntervalsToCharge;
priceExplanation = `$${totalChargeFee.toFixed(2)}, ${quantity} x $${price.toFixed(2)}`;
break;
case incidentType.BOOKING_MOVED_TO_ANOTHER_DAY:
if (oldResourceName !== newResourceName){
roomExplanation = `${oldResourceName} -> ${newResourceName}`;
}else{
roomExplanation = oldResourceName;
}
dateExplanation = `${oldBookingStartMoment.clone().format('MMM DD, YYYY')} -> ${newBookingStartMoment.clone().format('MMM DD, YYYY')}`;
bookingTimeExplanation = `(${oldBookingStartMoment.clone().format('HH:mm a')} - ${oldBookingEndMoment.clone().format('HH:mm a')}) -> (${newBookingStartMoment.clone().format('HH:mm a')} - ${newBookingEndMoment.clone().format('HH:mm a')})`;
incidentTimeExplanation = `MOVED ON : ${incidentTimestampMoment.clone().format('MMM DD, YYYY')}`;
date = incidentTimestampMoment.clone().startOf('day').format();
price = totalChargeFee;
quantity = 1;
priceExplanation = `$${totalChargeFee.toFixed(2)}, 1 x $${price.toFixed(2)}`;
break;
case incidentType.BOOKING_SHORTENED:
if (oldResourceName !== newResourceName){
roomExplanation = `${oldResourceName} -> ${newResourceName}`;
}else{
roomExplanation = oldResourceName;
}
dateExplanation = `${oldBookingStartMoment.clone().format('MMM DD, YYYY')}`;
bookingTimeExplanation = `(${oldBookingStartMoment.clone().format('HH:mm a')} - ${oldBookingEndMoment.clone().format('HH:mm a')}) -> (${newBookingStartMoment.clone().format('HH:mm a')} - ${newBookingEndMoment.clone().format('HH:mm a')})`;
incidentTimeExplanation = `SHORTENED ON : ${incidentTimestampMoment.clone().format('MMM DD, YYYY')}`;
date = incidentTimestampMoment.clone().startOf('day').format();
price = totalChargeFee;
quantity = 1;
priceExplanation = `$${totalChargeFee.toFixed(2)}, 1 x $${price.toFixed(2)}`;
break;
case incidentType.BOOKING_CANCELED_LATE:
roomExplanation = oldResourceName;
dateExplanation = `${oldBookingStartMoment.clone().format('MMM DD, YYYY')}`;
bookingTimeExplanation = `${oldBookingStartMoment.clone().format('HH:mm a')} - ${oldBookingEndMoment.clone().format('HH:mm a')}`;
incidentTimeExplanation = `CANCELED ON : ${incidentTimestampMoment.clone().format('MMM DD, YYYY')}`;
date = incidentTimestampMoment.clone().startOf('day').format();
price = totalChargeFee;
quantity = 1;
priceExplanation = `$${totalChargeFee.toFixed(2)}, 1 x $${price.toFixed(2)}`;
break;
}
const formattedName = `[INCIDENT FEES] ${officeName}, ${roomExplanation}, ${dateExplanation}, ${bookingTimeExplanation}, ${incidentTimeExplanation}, ${incidentExplanation}, ${additionalIncidentExplanation} ${additionalIncidentExplanation !== '' ? ',' : ''} ${priceExplanation}`;
return {
name: formattedName,
price,
quantity,
date,
member: memberId,
team: null,
office: officeId,
isPersonal: false,
}
};
const createFeeFromBooking = (booking, resourceMappings) => {
const { officeId, resourceId, memberId, start, end, timezone, hourlyRate } = booking;
const { officesMap, resourcesMap } = resourceMappings;
const startMoment = moment.tz(start, DEFAULT_DATE_FORMAT, timezone);
const endMoment = moment.tz(end, DEFAULT_DATE_FORMAT, timezone);
const reservationLength = endMoment.diff(startMoment, 'hours', true);
const officeName = officesMap[officeId].officeName || 'Unknown';
const resourceName = resourcesMap[resourceId].resourceName || 'Unknown';
const totalCost = (hourlyRate*reservationLength).toFixed(2);
const formattedDate = startMoment.clone().startOf('day').format('MMM DD, YYYY');
const formattedStartTime = startMoment.format('HH:mm a');
const formattedEndTime = endMoment.format('HH:mm a');
const formattedName = `[BOOKING FEES] ${officeName}, ${resourceName}, $${totalCost}, ${reservationLength.toFixed(2)} x $${hourlyRate.toFixed(2)}, ${formattedDate} [${formattedStartTime} - ${formattedEndTime}]`;
return {
name: formattedName,
price: hourlyRate,
quantity: reservationLength,
date: startMoment.startOf('day').toISOString(),
member: memberId,
team: null,
office: officeId,
isPersonal: false,
}
};
const getMembersFeesForDateRange = (dateRange, memberIds) => {
return new Promise((resolve, reject) => {
const collectData = [getAllIncidents(dateRange, memberIds), getActiveBookingsForMembersInDateRange(dateRange, memberIds), getResourceMappings(), fetchAllMembers()];
Promise.all(collectData)
.then((result) => {
const allIncidents = result[0];
const allActiveBookings = result[1];
const resourceMappings = result[2];
const membersList = result[3];
const memberIdTeamMappings = {};
membersList.forEach((member) => {
memberIdTeamMappings[member.memberId] = member.teamId;
});
const allFees = [];
allIncidents.forEach((incident) => allFees.push(createFeeFromIncident(incident)));
allActiveBookings.forEach((booking) => allFees.push(createFeeFromBooking(booking, resourceMappings)));
allFees.forEach((fee) => {
fee.team = memberIdTeamMappings[fee.member] || null;
});
resolve(allFees);
})
.catch((error) => {
console.log(error);
reject(error);
});
});
};
module.exports = {
getMembersFeesForDateRange,
};

View File

@@ -10,7 +10,7 @@ const { incidentType, UI_TIMEZONE, DEFAULT_DATE_FORMAT, integrationServiceErrors
const { fetchAllMembers } = require('../officeRnD/members'); const { fetchAllMembers } = require('../officeRnD/members');
const { fetchOffices, fetchResources } = require('../officeRnD/resources'); const { fetchOffices, fetchResources } = require('../officeRnD/resources');
const getUnlockedIncidents = (startDate, endDate, memberId) => { const getUnlockedIncidents = (startDate, endDate, memberIds) => {
const attributes = ['id', 'reservationId', 'memberId', 'resourceId', 'bookingStart', 'bookingEnd', 'unlockTimestamp', 'incidentLevel', 'incidentLevelPrice']; const attributes = ['id', 'reservationId', 'memberId', 'resourceId', 'bookingStart', 'bookingEnd', 'unlockTimestamp', 'incidentLevel', 'incidentLevelPrice'];
const filters = {}; const filters = {};
@@ -41,8 +41,10 @@ const getUnlockedIncidents = (startDate, endDate, memberId) => {
Object.assign(filters, bookingStartOrUnlockTimestamp); Object.assign(filters, bookingStartOrUnlockTimestamp);
} }
if (memberId){ if (memberIds.length > 0){
filters.memberId = memberId; filters.memberId = {
[Op.in]: memberIds
};
} }
return db.unlockedIncident.findAll({ return db.unlockedIncident.findAll({
@@ -54,7 +56,7 @@ const getUnlockedIncidents = (startDate, endDate, memberId) => {
}); });
}; };
const getUnscheduledIncidents = (startDate, endDate, memberId) => { const getUnscheduledIncidents = (startDate, endDate, memberIds) => {
const attributes = [ const attributes = [
'id', 'id',
'reservationId', 'reservationId',
@@ -98,8 +100,10 @@ const getUnscheduledIncidents = (startDate, endDate, memberId) => {
} }
if (memberId){ if (memberIds.length > 0){
filters.memberId = memberId; filters.memberId = {
[Op.in]: memberIds
};
} }
return db.unscheduledIncident.findAll({ return db.unscheduledIncident.findAll({
@@ -111,7 +115,7 @@ const getUnscheduledIncidents = (startDate, endDate, memberId) => {
}); });
}; };
const getBookingChangeIncidents = (startDate, endDate, memberId) => { const getBookingChangeIncidents = (startDate, endDate, memberIds) => {
const attributes = [ const attributes = [
'id', 'id',
'reservationId', 'reservationId',
@@ -138,8 +142,10 @@ const getBookingChangeIncidents = (startDate, endDate, memberId) => {
} }
} }
if (memberId){ if (memberIds.length > 0){
filters.memberId = memberId; filters.memberId = {
[Op.in]: memberIds
};
} }
return db.bookingChangeIncident.findAll({ return db.bookingChangeIncident.findAll({
@@ -160,7 +166,7 @@ const formatTime = (timestamp) => {
} }
}; };
const getAllIncidents = (dateRange, memberId) => { const getAllIncidents = (dateRange, memberIds) => {
return new Promise ((resolve, reject) => { return new Promise ((resolve, reject) => {
let startDate, endDate; let startDate, endDate;
@@ -178,9 +184,9 @@ const getAllIncidents = (dateRange, memberId) => {
fetchAllMembers(), fetchAllMembers(),
fetchOffices(), fetchOffices(),
fetchResources(), fetchResources(),
getUnlockedIncidents(startDate, endDate, memberId), getUnlockedIncidents(startDate, endDate, memberIds),
getUnscheduledIncidents(startDate, endDate, memberId), getUnscheduledIncidents(startDate, endDate, memberIds),
getBookingChangeIncidents(startDate, endDate, memberId) getBookingChangeIncidents(startDate, endDate, memberIds)
]; ];
Promise.all(dataFetchJobs) Promise.all(dataFetchJobs)
@@ -210,10 +216,14 @@ const getAllIncidents = (dateRange, memberId) => {
memberId: unlockedIncident.memberId, memberId: unlockedIncident.memberId,
memberName: membersMap[unlockedIncident.memberId].name, memberName: membersMap[unlockedIncident.memberId].name,
resourceName: resourcesMap[unlockedIncident.resourceId].resourceName, resourceName: resourcesMap[unlockedIncident.resourceId].resourceName,
officeId: resourcesMap[unlockedIncident.resourceId].officeId,
officeName: officesMap[resourcesMap[unlockedIncident.resourceId].officeId].officeName, officeName: officesMap[resourcesMap[unlockedIncident.resourceId].officeId].officeName,
bookingStart: formatTime(unlockedIncident.bookingStart), bookingStart: formatTime(unlockedIncident.bookingStart),
bookingEnd: formatTime(unlockedIncident.bookingEnd), bookingEnd: formatTime(unlockedIncident.bookingEnd),
bookingStartRaw: unlockedIncident.bookingStart,
bookingEndRaw: unlockedIncident.bookingEnd,
unlockTimestamp: formatTime(unlockedIncident.unlockTimestamp), unlockTimestamp: formatTime(unlockedIncident.unlockTimestamp),
unlockTimestampRaw: unlockedIncident.unlockTimestamp,
incidentType: incidentTypeNumber, incidentType: incidentTypeNumber,
incidentLevel: unlockedIncident.incidentLevel, incidentLevel: unlockedIncident.incidentLevel,
incidentPrice: unlockedIncident.incidentLevelPrice, incidentPrice: unlockedIncident.incidentLevelPrice,
@@ -236,11 +246,16 @@ const getAllIncidents = (dateRange, memberId) => {
memberId: unscheduledIncident.memberId, memberId: unscheduledIncident.memberId,
memberName: membersMap[unscheduledIncident.memberId].name, memberName: membersMap[unscheduledIncident.memberId].name,
resourceName: resourcesMap[unscheduledIncident.resourceId].resourceName, resourceName: resourcesMap[unscheduledIncident.resourceId].resourceName,
officeId: resourcesMap[unscheduledIncident.resourceId].officeId,
officeName: officesMap[resourcesMap[unscheduledIncident.resourceId].officeId].officeName, officeName: officesMap[resourcesMap[unscheduledIncident.resourceId].officeId].officeName,
bookingStart: formatTime(unscheduledIncident.bookingStart), bookingStart: formatTime(unscheduledIncident.bookingStart),
bookingEnd: formatTime(unscheduledIncident.bookingEnd), bookingEnd: formatTime(unscheduledIncident.bookingEnd),
bookingStartRaw: unscheduledIncident.bookingStart,
bookingEndRaw: unscheduledIncident.bookingEnd,
unlockTimestamp: formatTime(unscheduledIncident.unlockTimestamp), unlockTimestamp: formatTime(unscheduledIncident.unlockTimestamp),
lockTimestamp: formatTime(unscheduledIncident.lockTimestamp), lockTimestamp: formatTime(unscheduledIncident.lockTimestamp),
unlockTimestampRaw: unscheduledIncident.unlockTimestamp,
lockTimestampRaw: unscheduledIncident.lockTimestamp,
incidentType: incidentTypeNumber, incidentType: incidentTypeNumber,
timeIntervalsToCharge: unscheduledIncident.timeIntervalsToCharge, timeIntervalsToCharge: unscheduledIncident.timeIntervalsToCharge,
chargePrice: unscheduledIncident.chargePrice, chargePrice: unscheduledIncident.chargePrice,
@@ -267,21 +282,28 @@ const getAllIncidents = (dateRange, memberId) => {
const newResource = newResourceId ? resourcesMap[newResourceId] : null; const newResource = newResourceId ? resourcesMap[newResourceId] : null;
const oldResourceName = oldResource.resourceName; const oldResourceName = oldResource.resourceName;
const newResourceName = newResource ? newResource.resourceName : null; const newResourceName = newResource ? newResource.resourceName : null;
const officeName = officesMap[oldResource.officeId].officeName; const officeId = oldResource.officeId;
const officeName = officesMap[officeId].officeName;
allIncidents.push({ allIncidents.push({
incidentId: id, incidentId: id,
memberId, memberId,
memberName, memberName,
oldResourceName, oldResourceName,
newResourceName, newResourceName,
officeId,
officeName, officeName,
oldBookingStart: formatTime(oldBookingStart), oldBookingStart: formatTime(oldBookingStart),
oldBookingEnd: formatTime(oldBookingEnd), oldBookingEnd: formatTime(oldBookingEnd),
newBookingStart: formatTime(newBookingStart), newBookingStart: formatTime(newBookingStart),
newBookingEnd: formatTime(newBookingEnd), newBookingEnd: formatTime(newBookingEnd),
oldBookingStartRaw: oldBookingStart,
oldBookingEndRaw: oldBookingEnd,
newBookingStartRaw: newBookingStart,
newBookingEndRaw: newBookingEnd,
incidentType, incidentType,
totalChargeFee: chargeFee, totalChargeFee: chargeFee,
incidentTimestamp: formatTime(createdAt), incidentTimestamp: formatTime(createdAt),
incidentTimestampRaw: createdAt,
}); });
}); });
@@ -292,7 +314,5 @@ const getAllIncidents = (dateRange, memberId) => {
}; };
module.exports = { module.exports = {
getUnlockedIncidents,
getUnscheduledIncidents,
getAllIncidents, getAllIncidents,
}; };

View File

@@ -0,0 +1,70 @@
'use strict';
const moment = require('moment-timezone');
const { API } = require('../../helpers/api');
const { officeRnDAPIErrors, DEFAULT_DATE_FORMAT } = require('../../constants/constants');
const deleteFeesFromORD = (dateRange, memberIds) => {
return new Promise((resolve, reject) => {
const startDate = moment.utc(dateRange.startDate, DEFAULT_DATE_FORMAT).startOf('day');
const endDate = moment.utc(dateRange.endDate, DEFAULT_DATE_FORMAT).endOf('day');
API.get('fees')
.then((response) => {
const fetchedFees = response.data ? response.data : [];
const memberIdsMap = {};
memberIds.forEach((memberId) => {
memberIdsMap[memberId] = true;
});
const deleteRequests = [];
const sendDeleteRequestPromise = (feeId) => {
return new Promise((resolve, reject) => {
API.delete(`fees/${feeId}`)
.then(() => resolve(true))
.catch(() => resolve(false));
});
};
fetchedFees.forEach((fee) => {
const { member, date } = fee;
const feeId = fee['_id'];
const isDateInDateRange = startDate.isSameOrBefore(date) && endDate.isSameOrAfter(date);
if (memberIdsMap[member] && isDateInDateRange) {
deleteRequests.push(sendDeleteRequestPromise(feeId));
}
});
Promise.all(deleteRequests)
.then(() => {
resolve(true);
})
.catch((error) => {
reject(error);
});
})
.catch((error) => {
console.log(error);
reject(officeRnDAPIErrors.FAILED_TO_FETCH_FEES);
});
});
};
const addFeesToORD = (allFees) => {
return new Promise((resolve, reject) => {
API.post('/fees', allFees)
.catch((error) => {
console.log('==== ERROR ====');
console.log(error);
});
resolve(allFees.length);
});
};
module.exports = {
deleteFeesFromORD,
addFeesToORD
};

View File

@@ -12,6 +12,7 @@ const fetchAllMembers = () => {
cleanedResult.push({ cleanedResult.push({
name: member.name, name: member.name,
memberId: member['_id'], memberId: member['_id'],
teamId: member.team,
}); });
}); });
cleanedResult.sort((member1, member2) => (member1.name > member2.name) ? 1 : -1 ); cleanedResult.sort((member1, member2) => (member1.name > member2.name) ? 1 : -1 );

View File

@@ -45,6 +45,30 @@ const fetchResources = () => {
}); });
}; };
const getResourceMappings = () => {
return new Promise((resolve, reject) => {
const fetchJobs = [fetchOffices(), fetchResources()];
Promise.all(fetchJobs)
.then((mappings) => {
const offices = mappings[0];
const resources = mappings[1];
const officesMap = {};
const resourcesMap = {};
offices.forEach((office) => officesMap[office.officeId] = office);
resources.forEach((resource) => resourcesMap[resource.resourceId] = resource);
resolve({
officesMap,
resourcesMap,
});
})
.catch((error) => reject(error));
});
};
const getMappingsFromDatabase = () => { const getMappingsFromDatabase = () => {
return db.officeResourceMapping.findAll(); return db.officeResourceMapping.findAll();
}; };
@@ -57,5 +81,6 @@ module.exports = {
getMappingsFromDatabase, getMappingsFromDatabase,
fetchOffices, fetchOffices,
fetchResources, fetchResources,
getResourceMappings,
saveNewMappingToDatabase, saveNewMappingToDatabase,
}; };