From 43c4214a23ab22d0c11b71ab8f415b6f8177a041 Mon Sep 17 00:00:00 2001 From: Senad Uka Date: Tue, 26 Nov 2019 14:00:18 +0100 Subject: [PATCH] Bump period to 7 minutes / silent feature --- .../components/SingleIncidentsTable.js | 383 +++++++++++------- .../components/MemberIncidentsTables/index.js | 8 +- client/src/components/SelectTable/index.js | 111 +++++ client/src/scenes/IncidentsReport/index.js | 2 +- .../SpecificMemberIncidentsReport/index.js | 8 +- .../src/store/actions/integrationActions.js | 15 + constants/constants.js | 2 + controllers/integration.js | 46 +++ environment.env | 1 + ...ted-column-for-unlocked-incidents-table.js | 14 + ...-column-for-unscheduled-incidents-table.js | 14 + models/unlockedIncident.js | 1 + models/unscheduledIncident.js | 1 + routes/index.js | 2 + services/doorLock/doorLock.js | 8 + services/integration/bookingChangeCharges.js | 10 + services/integration/doorLockCharges.js | 103 ++++- services/integration/invoiceIntegration.js | 34 +- services/integration/reports.js | 8 +- services/officeRnD/fees.js | 2 +- 20 files changed, 592 insertions(+), 181 deletions(-) create mode 100644 client/src/components/SelectTable/index.js create mode 100644 migrations/20191120144357-add-deleted-column-for-unlocked-incidents-table.js create mode 100644 migrations/20191120144439-add-deleted-column-for-unscheduled-incidents-table.js diff --git a/client/src/components/MemberIncidentsTables/components/SingleIncidentsTable.js b/client/src/components/MemberIncidentsTables/components/SingleIncidentsTable.js index 7219cc1..c4c2221 100644 --- a/client/src/components/MemberIncidentsTables/components/SingleIncidentsTable.js +++ b/client/src/components/MemberIncidentsTables/components/SingleIncidentsTable.js @@ -1,9 +1,11 @@ -import React from 'react'; -import { Loader } from 'semantic-ui-react'; -import ReactTable from 'react-table'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { Loader, Button } from 'semantic-ui-react'; import 'react-table/react-table.css'; import { NavLink } from 'react-router-dom'; +import SelectTable from "../../SelectTable"; + import {incidentsReportHeaderTitles} from '../../../constants/constants'; import { incidentTableTypes, @@ -13,155 +15,244 @@ import { UNSCHEDULED_INCIDENT_BEFORE_RESERVATION, UNSCHEDULED_INCIDENT_STANDALONE } from '../../../constants/enums'; import { doorLockRelatedWithReservationIncidentHeaders, standaloneDoorLockIncidentHeaders, bookingChangeIncidentHeaders } from '../../../constants/constants'; +import { deleteIncidents } from "../../../store/actions"; +class SingleIncidentsTable extends Component { + state = { + selectedUnlockedIncidentIds: [], + selectedUnscheduledIncidentIds: [], + selectedBookingChangeIncidentIds: [] + }; -const SingleIncidentsTable = props => { - const { - loading, - title, - openMemberSummaryOnMemberClick, - hideMemberName, - tableType - } = props; - const incidents = props.incidents ? props.incidents : []; - const columns = []; + onSelectChange = (selectedIncidents) => { + const newSelectedUnlockedIncidentIds = []; + const newSelectedUnscheduledIncidentIds = []; + const newSelectedBookingChangeIncidentIds = []; - if (incidents && incidents.length > 0){ - let tableHeaders; - switch (tableType) { - case incidentTableTypes.INCIDENTS_RELATED_TO_RESERVATIONS: - tableHeaders = doorLockRelatedWithReservationIncidentHeaders; - break; - case incidentTableTypes.STANDALONE_INCIDENTS: - tableHeaders = standaloneDoorLockIncidentHeaders; - break; - case incidentTableTypes.BOOKING_CHANGE_INCIDENTS: - tableHeaders = bookingChangeIncidentHeaders; - break; - default: - break; - } + selectedIncidents.forEach(incident => { + const incidentDetails = incident.split('-'); + // incident is described as : select-incidentType-incidentId + if (Array.isArray(incidentDetails) && incidentDetails.length > 2){ + const incidentType = parseInt(incidentDetails[1]); + const incidentId = parseInt(incidentDetails[2]); - tableHeaders.forEach((header) => { - const columnTitle = incidentsReportHeaderTitles[header]; - - let showColumn = true; - if (header === 'memberName' && hideMemberName){ - showColumn = false; - } - - if (columnTitle && showColumn){ - 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 = `/specific-member-incidents-report/${memberId}`; - cellValue = props.value; - break; - case 'resourceName': - if (props.row['_original'].resourceName){ - cellValue = props.row['_original'].resourceName || '---'; - }else{ - const oldResourceName = props.row['_original'].oldResourceName || '---'; - const newResourceName = props.row['_original'].newResourceName || '---'; - if (oldResourceName !== newResourceName){ - cellValue = `${oldResourceName}\n${newResourceName}`; - }else{ - cellValue = oldResourceName; - } - } - break; - case 'reservation': - const bookingStart = props.row['_original'].bookingStart; - const bookingEnd = props.row['_original'].bookingEnd; - cellValue = `${bookingStart}\n${bookingEnd}`; - break; - case 'doorLockTimestamps': - const unlockTimestamp = props.row['_original'].unlockTimestamp; - const lockTimestamp = props.row['_original'].lockTimestamp; - cellValue = `${unlockTimestamp ? unlockTimestamp : '---'}\n${lockTimestamp ? lockTimestamp : '---'}`; - 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_RELATED_WITH_RESERVATION: - case UNLOCKED_INCIDENT_STANDALONE: - cellValue = `${incidentLevelDescriptions[incidentLevel]}`; - break; - case UNSCHEDULED_INCIDENT_BEFORE_RESERVATION: - case UNSCHEDULED_INCIDENT_AFTER_RESERVATION: - case UNSCHEDULED_INCIDENT_STANDALONE: - cellValue = `${timeIntervalsToCharge} x 5 min`; - break; - default: - cellValue = ''; - break; - } - break; - case 'totalChargeFee': - const totalFee = (props.row['_original'].incidentPrice || props.value) || 0; - const totalFeeFormatted = parseFloat(totalFee).toFixed(2); - cellValue = `$ ${totalFeeFormatted}`; - columnContentsAlignment = columnAlignments.right; - break; - case 'oldReservation': - const oldBookingStart = props.row['_original'].oldBookingStart; - const oldBookingEnd = props.row['_original'].oldBookingEnd; - cellValue = `${oldBookingStart}\n${oldBookingEnd}`; - break; - case 'newReservation': - const newBookingStart = props.row['_original'].newBookingStart || '---'; - const newBookingEnd = props.row['_original'].newBookingEnd || '---'; - cellValue = `${newBookingStart}\n${newBookingEnd}`; - break; - default: - cellValue = props.value; - } - - if (openMemberSummaryOnMemberClick && urlValue){ - return {cellValue} - }else{ - return
{cellValue}
- } - } - }); + switch (incidentType) { + case 2: + case 5: + newSelectedUnlockedIncidentIds.push(incidentId); + break; + case 3: + case 4: + case 6: + newSelectedUnscheduledIncidentIds.push(incidentId); + break; + case 7: + case 8: + case 9: + newSelectedBookingChangeIncidentIds.push(incidentId); + break; + default: + break; + } } }); - } - return ( -
-

{title}

- - { - !loading && incidents && - + this.setState({ + selectedUnlockedIncidentIds: newSelectedUnlockedIncidentIds, + selectedUnscheduledIncidentIds: newSelectedUnscheduledIncidentIds, + selectedBookingChangeIncidentIds: newSelectedBookingChangeIncidentIds + }); + }; + + deleteSelectedFees = () => { + const { dateRange, deleteIncidentsById, memberId } = this.props; + const { selectedUnlockedIncidentIds, selectedUnscheduledIncidentIds, selectedBookingChangeIncidentIds } = this.state; + + const incidentsToDelete = { + unlockedIncidentIds: selectedUnlockedIncidentIds, + unscheduledIncidentIds: selectedUnscheduledIncidentIds, + bookingChangeIncidentIds: selectedBookingChangeIncidentIds + }; + + deleteIncidentsById(dateRange, incidentsToDelete, memberId); + this.setState({ + selectedUnlockedIncidentIds: [], + selectedUnscheduledIncidentIds: [], + selectedBookingChangeIncidentIds: [] + }); + }; + + render(){ + const { + loading, + title, + openMemberSummaryOnMemberClick, + hideMemberName, + tableType + } = this.props; + + const { + selectedUnlockedIncidentIds, + selectedUnscheduledIncidentIds, + selectedBookingChangeIncidentIds + } = this.state; + + const totalSelected = selectedUnlockedIncidentIds.length + selectedUnscheduledIncidentIds.length + selectedBookingChangeIncidentIds.length; + const numberOfSelectedText = totalSelected > 0 ? ` (${totalSelected})` : ''; + + const incidents = this.props.incidents ? this.props.incidents : []; + incidents.forEach(incident => { + incident.id = `${incident.incidentType}-${incident.incidentId}`; + }); + const columns = []; + + if (incidents && incidents.length > 0){ + let tableHeaders; + switch (tableType) { + case incidentTableTypes.INCIDENTS_RELATED_TO_RESERVATIONS: + tableHeaders = doorLockRelatedWithReservationIncidentHeaders; + break; + case incidentTableTypes.STANDALONE_INCIDENTS: + tableHeaders = standaloneDoorLockIncidentHeaders; + break; + case incidentTableTypes.BOOKING_CHANGE_INCIDENTS: + tableHeaders = bookingChangeIncidentHeaders; + break; + default: + break; } -
- ); -}; -export default SingleIncidentsTable; + tableHeaders.forEach((header) => { + const columnTitle = incidentsReportHeaderTitles[header]; + + let showColumn = true; + if (header === 'memberName' && hideMemberName){ + showColumn = false; + } + + if (columnTitle && showColumn){ + 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 = `/specific-member-incidents-report/${memberId}`; + cellValue = props.value; + break; + case 'resourceName': + if (props.row['_original'].resourceName){ + cellValue = props.row['_original'].resourceName || '---'; + }else{ + const oldResourceName = props.row['_original'].oldResourceName || '---'; + const newResourceName = props.row['_original'].newResourceName || '---'; + if (oldResourceName !== newResourceName){ + cellValue = `${oldResourceName}\n${newResourceName}`; + }else{ + cellValue = oldResourceName; + } + } + break; + case 'reservation': + const bookingStart = props.row['_original'].bookingStart; + const bookingEnd = props.row['_original'].bookingEnd; + cellValue = `${bookingStart}\n${bookingEnd}`; + break; + case 'doorLockTimestamps': + const unlockTimestamp = props.row['_original'].unlockTimestamp; + const lockTimestamp = props.row['_original'].lockTimestamp; + cellValue = `${unlockTimestamp ? unlockTimestamp : '---'}\n${lockTimestamp ? lockTimestamp : '---'}`; + 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_RELATED_WITH_RESERVATION: + case UNLOCKED_INCIDENT_STANDALONE: + cellValue = `${incidentLevelDescriptions[incidentLevel]}`; + break; + case UNSCHEDULED_INCIDENT_BEFORE_RESERVATION: + case UNSCHEDULED_INCIDENT_AFTER_RESERVATION: + case UNSCHEDULED_INCIDENT_STANDALONE: + cellValue = `${timeIntervalsToCharge} x 5 min`; + break; + default: + cellValue = ''; + break; + } + break; + case 'totalChargeFee': + const totalFee = (props.row['_original'].incidentPrice || props.value) || 0; + const totalFeeFormatted = parseFloat(totalFee).toFixed(2); + cellValue = `$ ${totalFeeFormatted}`; + columnContentsAlignment = columnAlignments.right; + break; + case 'oldReservation': + const oldBookingStart = props.row['_original'].oldBookingStart; + const oldBookingEnd = props.row['_original'].oldBookingEnd; + cellValue = `${oldBookingStart}\n${oldBookingEnd}`; + break; + case 'newReservation': + const newBookingStart = props.row['_original'].newBookingStart || '---'; + const newBookingEnd = props.row['_original'].newBookingEnd || '---'; + cellValue = `${newBookingStart}\n${newBookingEnd}`; + break; + default: + cellValue = props.value; + } + + if (openMemberSummaryOnMemberClick && urlValue){ + return {cellValue} + }else{ + return
{cellValue}
+ } + } + }); + } + }); + } + + return ( +
+

{title}

+ + { + + } +

+ { + !loading && incidents && + + } +
+ ); + } +} + +const mapDispatchToProps = (dispatch) => ({ + deleteIncidentsById: (dateRange, incidentsToDelete, memberId) => deleteIncidents(dispatch, {dateRange, incidentsToDelete, memberId}) +}); + +export default connect(null, mapDispatchToProps)(SingleIncidentsTable); diff --git a/client/src/components/MemberIncidentsTables/index.js b/client/src/components/MemberIncidentsTables/index.js index 45ec9d1..061870e 100644 --- a/client/src/components/MemberIncidentsTables/index.js +++ b/client/src/components/MemberIncidentsTables/index.js @@ -8,7 +8,7 @@ import { } from '../../constants/enums'; export default function MemberIncidentsTables (props) { - const { pendingIncidents, incidents, hideMemberName } = props; + const { pendingIncidents, incidents, hideMemberName, dateRange, memberId } = props; const incidentsRelatedToReservations = []; const standaloneIncidents = []; @@ -46,6 +46,8 @@ export default function MemberIncidentsTables (props) { openMemberSummaryOnMemberClick hideMemberName={hideMemberName} tableType={incidentTableTypes.INCIDENTS_RELATED_TO_RESERVATIONS} + dateRange={dateRange} + memberId={memberId} /> ); @@ -56,6 +58,8 @@ export default function MemberIncidentsTables (props) { openMemberSummaryOnMemberClick hideMemberName={hideMemberName} tableType={incidentTableTypes.STANDALONE_INCIDENTS} + dateRange={dateRange} + memberId={memberId} /> ); @@ -66,6 +70,8 @@ export default function MemberIncidentsTables (props) { openMemberSummaryOnMemberClick hideMemberName={hideMemberName} tableType={incidentTableTypes.BOOKING_CHANGE_INCIDENTS} + dateRange={dateRange} + memberId={memberId} /> ); diff --git a/client/src/components/SelectTable/index.js b/client/src/components/SelectTable/index.js new file mode 100644 index 0000000..f91653a --- /dev/null +++ b/client/src/components/SelectTable/index.js @@ -0,0 +1,111 @@ +import React, { Component } from 'react'; +import Table from 'react-table'; +import SelectTableHOC from 'react-table/lib/hoc/selectTable'; + +const SelectTableElement = SelectTableHOC(Table); + +class SelectTable extends Component { + static defaultProps = { + keyField: "id" + }; + + /** + * Toggle a single checkbox for select table + */ + toggleSelection = (key, shift, row) => { + // start off with the existing state + let selection = [...this.state.selection]; + const keyIndex = selection.indexOf(key); + + // check to see if the key exists + if (keyIndex >= 0) { + // it does exist so we will remove it using destructing + selection = [ + ...selection.slice(0, keyIndex), + ...selection.slice(keyIndex + 1) + ]; + } else { + // it does not exist so add it + selection.push(key); + } + // update the state + this.props.onSelectChange(selection); + this.setState({ selection }); + }; + + /** + * Toggle all checkboxes for select table + */ + toggleAll = () => { + const { keyField } = this.props; + const selectAll = !this.state.selectAll; + const selection = []; + + if (selectAll) { + // we need to get at the internals of ReactTable + const wrappedInstance = this.checkboxTable.getWrappedInstance(); + // the 'sortedData' property contains the currently accessible records based on the filter and sort + const currentRecords = wrappedInstance.getResolvedState().sortedData; + // we just push all the IDs onto the selection array + currentRecords.forEach(item => { + selection.push(`select-${item._original[keyField]}`); + }); + } + this.props.onSelectChange(selection); + this.setState({ selectAll, selection }); + }; + + /** + * Whether or not a row is selected for select table + */ + isSelected = key => { + return this.state.selection.includes(`select-${key}`); + }; + + rowFn = (state, rowInfo, column, instance) => { + const { selection } = this.state; + + return { + onClick: (e, handleOriginal) => { + // console.log("It was in this row:", rowInfo); + + // IMPORTANT! React-Table uses onClick internally to trigger + // events like expanding SubComponents and pivots. + // By default a custom 'onClick' handler will override this functionality. + // If you want to fire the original onClick handler, call the + // 'handleOriginal' function. + if (handleOriginal) { + handleOriginal(); + } + }, + style: { + background: + rowInfo && + selection.includes(`select-${rowInfo.original.id}`) && + "lightgreen" + } + }; + }; + + state = { + selectAll: false, + selection: [] + }; + + render() { + return ( + (this.checkboxTable = r)} + toggleSelection={this.toggleSelection} + selectAll={this.state.selectAll} + selectType="checkbox" + toggleAll={this.toggleAll} + isSelected={this.isSelected} + getTrProps={this.rowFn} + /> + ); + } +} + +export default SelectTable; diff --git a/client/src/scenes/IncidentsReport/index.js b/client/src/scenes/IncidentsReport/index.js index 891f87a..987fb5d 100644 --- a/client/src/scenes/IncidentsReport/index.js +++ b/client/src/scenes/IncidentsReport/index.js @@ -47,7 +47,7 @@ class IncidentsReport extends Component {



- + ); } diff --git a/client/src/scenes/SpecificMemberIncidentsReport/index.js b/client/src/scenes/SpecificMemberIncidentsReport/index.js index 5e1c0fb..d192645 100644 --- a/client/src/scenes/SpecificMemberIncidentsReport/index.js +++ b/client/src/scenes/SpecificMemberIncidentsReport/index.js @@ -83,7 +83,13 @@ class SpecificMemberIncidentsReport extends Component { - + diff --git a/client/src/store/actions/integrationActions.js b/client/src/store/actions/integrationActions.js index bef2eb7..f285e29 100644 --- a/client/src/store/actions/integrationActions.js +++ b/client/src/store/actions/integrationActions.js @@ -88,6 +88,21 @@ export const fetchIncidents = (dispatch, dateRange) => { }); }; +export const deleteIncidents = (dispatch, deleteData) => { + const pendingAction = deleteData.memberId ? FETCH_MEMBER_INCIDENTS_PENDING : FETCH_INCIDENTS_PENDING; + const successAction = deleteData.memberId ? FETCH_MEMBER_INCIDENTS_SUCCESS : FETCH_INCIDENTS_SUCCESS; + const failedAction = deleteData.memberId ? FETCH_MEMBER_INCIDENTS_FAILED : FETCH_INCIDENTS_FAILED; + + dispatch({type: pendingAction}); + API.delete(`/integration/fees`, { data: deleteData }) + .then(response => { + dispatch({type: successAction, payload: response.data}); + }) + .catch(error => { + dispatch({type: failedAction, payload: error.response}); + }); +}; + export const fetchMembersList = (dispatch) => { dispatch({type: FETCH_MEMBERS_PENDING}); API.get('officeRnD/membersList') diff --git a/constants/constants.js b/constants/constants.js index 6ee5812..6291394 100644 --- a/constants/constants.js +++ b/constants/constants.js @@ -109,6 +109,7 @@ const UI_TIMEZONE = process.env.UI_TIMEZONE || 'America/Los_Angeles'; const DEFAULT_DATE_FORMAT = 'YYYY-MM-DD'; const MAX_BACK_TO_BACK_DIFFERENCE = parseInt(process.env.MAX_BACK_TO_BACK_DIFFERENCE) || 0; +const UNSCHEDULED_USE_INITIAL_TIME_SEGMENT_LENGTH = parseInt(process.env.UNSCHEDULED_USE_INITIAL_TIME_SEGMENT_LENGTH) || 7; const UNSCHEDULED_TIME_RESOLUTION = parseInt(process.env.UNSCHEDULED_USE_TIME_RESOLUTION) || 5; const UNSCHEDULED_CHARGE_PRICE = parseFloat(process.env.UNSCHEDULED_USE_CHARGE_PRICE) || 5; @@ -154,6 +155,7 @@ module.exports = { UI_TIMEZONE, DEFAULT_DATE_FORMAT, MAX_BACK_TO_BACK_DIFFERENCE, + UNSCHEDULED_USE_INITIAL_TIME_SEGMENT_LENGTH, UNSCHEDULED_TIME_RESOLUTION, UNSCHEDULED_CHARGE_PRICE, BOOKING_CHANGE_PERCENTAGE_CHARGE, diff --git a/controllers/integration.js b/controllers/integration.js index be3ffed..e9c2d8b 100644 --- a/controllers/integration.js +++ b/controllers/integration.js @@ -9,6 +9,8 @@ const { deleteFeesFromORD, addFeesToORD } = require('../services/officeRnD/fees' const { reformatMembershipsName } = require('../services/officeRnD/memberships'); const { checkBookingChanges } = require('../services/integration/checkBookingChange'); const { checkIfProcessing } = require('../services/integration/processingStatus'); +const { deleteUnlockedIncidentsById, deleteUnscheduledIncidentsById } = require('../services/integration/doorLockCharges'); +const { deleteBookingChangeIncidentsById } = require('../services/integration/bookingChangeCharges'); const { UI_TIMEZONE, DEFAULT_DATE_FORMAT, ALLOW_SENDING_FEES, integrationServiceErrors } = require('../constants/constants'); @@ -175,6 +177,49 @@ const addFees = (req, res) => { } }; +const deleteFees= (req, res) => { + const deleteData = req.body; + const dateRange = deleteData.dateRange ? deleteData.dateRange : null; + const incidents = deleteData.incidentsToDelete ? deleteData.incidentsToDelete : null; + const memberId = deleteData.memberId ? deleteData.memberId : null; + + const unlockedIncidentIds = incidents.unlockedIncidentIds ? incidents.unlockedIncidentIds : []; + const unscheduledIncidentIds = incidents.unscheduledIncidentIds ? incidents.unscheduledIncidentIds : []; + const bookingChangeIncidentIds = incidents.bookingChangeIncidentIds ? incidents.bookingChangeIncidentIds : []; + + req.params.startDate = dateRange.startDate ? dateRange.startDate : null; + req.params.endDate = dateRange.endDate ? dateRange.endDate : null; + + if (Array.isArray(unlockedIncidentIds) && Array.isArray(unscheduledIncidentIds) && Array.isArray(bookingChangeIncidentIds)){ + const asyncDeleteActions = [ + deleteUnlockedIncidentsById(unlockedIncidentIds), + deleteUnscheduledIncidentsById(unscheduledIncidentIds), + deleteBookingChangeIncidentsById(bookingChangeIncidentIds) + ]; + + Promise.all(asyncDeleteActions) + .then(() => { + if (memberId){ + req.params.memberId = memberId; + getMemberIncidents(req, res); + }else{ + getAllIncidentsController(req, res); + } + }) + .catch((error) => { + console.log('Error deleting incidents : ', error); + res.status(500).send(); + }); + }else{ + if (memberId){ + req.params.memberId = memberId; + getMemberIncidents(req, res); + }else{ + getAllIncidentsController(req, res); + } + } +}; + const checkProcessingStatus = (req, res) => { checkIfProcessing() .then((processing) => { @@ -227,4 +272,5 @@ module.exports = { addFees, checkProcessingStatus, getPracticeSummaryReport, + deleteFees }; diff --git a/environment.env b/environment.env index f2fe6a3..6f77418 100644 --- a/environment.env +++ b/environment.env @@ -8,6 +8,7 @@ EARLIEST_UNLOCK=Time in minutes UI_TIMEZONE=Timezone for user interface | https://en.wikipedia.org/wiki/List_of_tz_database_time_zones | example : America/Los_Angeles +UNSCHEDULED_USE_INITIAL_TIME_SEGMENT_LENGTH=Time in minutes when first unscheduled use charge will be applied UNSCHEDULED_USE_TIME_RESOLUTION=Time in minutes UNSCHEDULED_USE_CHARGE_PRICE=Charge price diff --git a/migrations/20191120144357-add-deleted-column-for-unlocked-incidents-table.js b/migrations/20191120144357-add-deleted-column-for-unlocked-incidents-table.js new file mode 100644 index 0000000..a3bcb91 --- /dev/null +++ b/migrations/20191120144357-add-deleted-column-for-unlocked-incidents-table.js @@ -0,0 +1,14 @@ +'use strict'; + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.addColumn('unlockedIncidents', 'deleted', { + type: Sequelize.BOOLEAN, + defaultValue: false, + }); + }, + + down: (queryInterface, Sequelize) => { + return queryInterface.removeColumn('unlockedIncidents', 'deleted'); + } +}; diff --git a/migrations/20191120144439-add-deleted-column-for-unscheduled-incidents-table.js b/migrations/20191120144439-add-deleted-column-for-unscheduled-incidents-table.js new file mode 100644 index 0000000..52b9681 --- /dev/null +++ b/migrations/20191120144439-add-deleted-column-for-unscheduled-incidents-table.js @@ -0,0 +1,14 @@ +'use strict'; + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.addColumn('unscheduledIncidents', 'deleted', { + type: Sequelize.BOOLEAN, + defaultValue: false, + }); + }, + + down: (queryInterface, Sequelize) => { + return queryInterface.removeColumn('unscheduledIncidents', 'deleted'); + } +}; diff --git a/models/unlockedIncident.js b/models/unlockedIncident.js index d77c126..dd0ebdf 100644 --- a/models/unlockedIncident.js +++ b/models/unlockedIncident.js @@ -22,6 +22,7 @@ module.exports = (sequelize, DataTypes) => { }, incidentLevelPrice: DataTypes.FLOAT, unlockTimestamp: DataTypes.DATE, + deleted: DataTypes.BOOLEAN }, {}); unlockedIncident.associate = function(models) { // associations can be defined here diff --git a/models/unscheduledIncident.js b/models/unscheduledIncident.js index c24ce79..04ac39f 100644 --- a/models/unscheduledIncident.js +++ b/models/unscheduledIncident.js @@ -19,6 +19,7 @@ module.exports = (sequelize, DataTypes) => { totalChargeFee: DataTypes.FLOAT, unlockTimestamp: DataTypes.DATE, lockTimestamp: DataTypes.DATE, + deleted: DataTypes.BOOLEAN }, {}); unscheduledIncident.associate = function(models) { // associations can be defined here diff --git a/routes/index.js b/routes/index.js index c8d30fe..e35c18d 100644 --- a/routes/index.js +++ b/routes/index.js @@ -13,6 +13,7 @@ const { addFees, checkProcessingStatus, getPracticeSummaryReport, + deleteFees } = require('../controllers/integration'); const { calculateDoorLockCharges } = require('../services/integration/doorLockCharges'); @@ -34,6 +35,7 @@ router.get('/integration/report/allIncidents/:startDate/:endDate', getAllInciden router.get('/officeRnD/membersList', fetchMembersList); router.post('/integration/addFees', addFees); +router.delete('/integration/fees', deleteFees); router.get('/integration/processing', checkProcessingStatus); diff --git a/services/doorLock/doorLock.js b/services/doorLock/doorLock.js index 92c69bc..6ba62e7 100644 --- a/services/doorLock/doorLock.js +++ b/services/doorLock/doorLock.js @@ -205,6 +205,14 @@ const getUnlockEntryForReservation = (reservation, previousReservation) => { const toTimestamp = reservation.end; + // if (reservation.memberId === '5ce785af422bdd00967fb781') { + // console.log('======================='); + // console.log('\tStart : ', moment.tz(reservation.start, reservation.timezone).format('DD.MM, HH:mm')); + // console.log('\tEnd : ', moment.tz(reservation.end, reservation.timezone).format('DD.MM, HH:mm')); + // console.log('\t----------------------------------'); + // console.log('\tFrom time : ', fromTimestamp); + // console.log('\tTo time : ', toTimestamp); + // } const filters = { memberId, diff --git a/services/integration/bookingChangeCharges.js b/services/integration/bookingChangeCharges.js index 9412561..597a632 100644 --- a/services/integration/bookingChangeCharges.js +++ b/services/integration/bookingChangeCharges.js @@ -274,6 +274,15 @@ const deleteBookingChangeIncidents = (incidents) => { return Promise.all(asyncActions); }; +const deleteBookingChangeIncidentsById = (incidentIds) => { + const filters = { + id: { + [Op.in]: incidentIds + } + }; + return db.bookingChangeIncident.update({deleted: true},{where: filters}); +}; + module.exports = { getChargedCanceledReservations, getIncidentsFromChanges, @@ -281,4 +290,5 @@ module.exports = { getShorteningIncidentsForReservationId, getReservationsIncidentsForRemoval, deleteBookingChangeIncidents, + deleteBookingChangeIncidentsById }; diff --git a/services/integration/doorLockCharges.js b/services/integration/doorLockCharges.js index 582ba28..8e5885b 100644 --- a/services/integration/doorLockCharges.js +++ b/services/integration/doorLockCharges.js @@ -2,6 +2,7 @@ const moment = require('moment-timezone'); const db = require('../../models/index'); +const Op = require('sequelize').Op; const { doorLockEvents, @@ -9,6 +10,7 @@ const { unlockedIncidentLevelsPrices, UNSCHEDULED_CHARGE_PRICE, MAX_BACK_TO_BACK_DIFFERENCE, + UNSCHEDULED_USE_INITIAL_TIME_SEGMENT_LENGTH, UNSCHEDULED_TIME_RESOLUTION, UI_TIMEZONE } = require('../../constants/constants'); @@ -18,7 +20,8 @@ const { getAllFinishedBookings, getFirstPreviousBooking, getFirstNextBooking } = const getSortedIncidentsForMember = (memberId) => { const attributes = ['bookingStart', 'incidentLevel', 'incidentLevelPrice', 'unlockTimestamp']; const filters = { - memberId + memberId, + deleted: false }; const order = [['unlockTimestamp', 'DESC']]; @@ -59,6 +62,7 @@ const insertUnscheduledIncidents = (incidents) => { bookingEnd: end, unlockTimestamp, lockTimestamp, + deleted: false }, defaults: {...incidentForDB}, })); @@ -67,6 +71,15 @@ const insertUnscheduledIncidents = (incidents) => { return Promise.all(asyncJobs); }; +const deleteUnscheduledIncidentsById = (incidentIds) => { + const filters = { + id: { + [Op.in]: incidentIds + } + }; + return db.unscheduledIncident.update({deleted: true},{where: filters}); +}; + const insertUnlockedIncidents = (incidents) => { const asyncJobs = []; incidents.forEach((incident) => { @@ -93,6 +106,7 @@ const insertUnlockedIncidents = (incidents) => { bookingEnd: end, unlockTimestamp, incidentLevel, + deleted: false }, defaults: {...incidentForDB}, })); @@ -101,6 +115,15 @@ const insertUnlockedIncidents = (incidents) => { return Promise.all(asyncJobs); }; +const deleteUnlockedIncidentsById = (incidentIds) => { + const filters = { + id: { + [Op.in]: incidentIds + } + }; + return db.unlockedIncident.update({deleted: true},{where: filters}); +}; + const setUnlockedIncidentsLevel = (incidents) => { return new Promise ((resolve, reject) => { const sortingFunction = (incidentA, incidentB) => { @@ -230,7 +253,11 @@ const analyseReservation = (reservation) => { const unlockTime = moment.utc(unlockEntry.timestamp); timeDifferenceFromUnlockEntry = currentReservationStart.diff(unlockTime, 'minutes'); } - const timeIntervalsToChargeBefore = Math.floor(timeDifferenceFromUnlockEntry / UNSCHEDULED_TIME_RESOLUTION); + + let timeIntervalsToChargeBefore = Math.floor(timeDifferenceFromUnlockEntry / UNSCHEDULED_TIME_RESOLUTION); + if (timeDifferenceFromUnlockEntry < UNSCHEDULED_USE_INITIAL_TIME_SEGMENT_LENGTH){ + timeIntervalsToChargeBefore = 0; + } const totalChargeFeeBefore = timeIntervalsToChargeBefore * UNSCHEDULED_CHARGE_PRICE; const chargeBefore = totalChargeFeeBefore > 0; @@ -239,10 +266,54 @@ const analyseReservation = (reservation) => { const lockTime = moment.utc(lockEntry.timestamp); timeDifferenceFromLockEntry = lockTime.diff(currentReservationEnd, 'minutes'); } - const timeIntervalsToChargeAfter = Math.floor(timeDifferenceFromLockEntry / UNSCHEDULED_TIME_RESOLUTION); + let timeIntervalsToChargeAfter = Math.floor(timeDifferenceFromLockEntry / UNSCHEDULED_TIME_RESOLUTION); + if (timeDifferenceFromLockEntry < UNSCHEDULED_USE_INITIAL_TIME_SEGMENT_LENGTH){ + timeIntervalsToChargeAfter = 0; + } const totalChargeFeeAfter = timeIntervalsToChargeAfter * UNSCHEDULED_CHARGE_PRICE; const chargeAfter = totalChargeFeeAfter > 0; + // if (reservation.memberId === '5ce785af422bdd00967fb781') { + // console.log('\r\n\r\n==== ANALYSE RESERVATION ==== '); + // console.log('\tStart : ', moment.tz(reservation.start, reservation.timezone).format('DD.MM, HH:mm')); + // console.log('\tEnd : ', moment.tz(reservation.end, reservation.timezone).format('DD.MM, HH:mm')); + // console.log('\t----------------------------------'); + // console.log('\tFirst previous reservation : '); + // if (previousReservation) { + // console.log('\t\tStart : ', moment.tz(previousReservation.start, previousReservation.timezone).format('DD.MM, HH:mm')); + // console.log('\t\tEnd : ', moment.tz(previousReservation.end, previousReservation.timezone).format('DD.MM, HH:mm')); + // } else { + // console.log('\t\tNO PREVIOUS RESERVATION'); + // } + // + // console.log('\tFirst next reservation : '); + // if (nextReservation) { + // console.log('\t\tStart : ', moment.tz(nextReservation.start, nextReservation.timezone).format('DD.MM, HH:mm')); + // console.log('\t\tEnd : ', moment.tz(nextReservation.end, nextReservation.timezone).format('DD.MM, HH:mm')); + // } else { + // console.log('\t\tNO NEXT RESERVATION'); + // } + // console.log('\t----------------------------------'); + // if (unlockEntry) { + // console.log('\tUnlock entry : ', moment.tz(unlockEntry.timestamp, reservation.timezone).format('DD.MM, HH:mm')); + // } else { + // console.log('\tUnlock entry : NO UNLOCK ENTRY'); + // } + // if (lockEntry) { + // console.log('\tLock entry : ', moment.tz(lockEntry.timestamp, reservation.timezone).format('DD.MM, HH:mm')); + // } else { + // console.log('\tLock entry : NO LOCK ENTRY'); + // } + // + // console.log('\t----------------------------------'); + // console.log('\tTime before : '); + // console.log('\t\tOriginal : ', timeDifferenceFromUnlockEntry); + // console.log('\t\tModified : ', timeIntervalsToChargeBefore); + // console.log('\tTime after : '); + // console.log('\t\tOriginal : ', timeDifferenceFromLockEntry); + // console.log('\t\tModified : ', timeIntervalsToChargeAfter); + // } + const result = { currentReservation: reservation, previousReservation, @@ -423,7 +494,12 @@ const getIncidentData = (reservation) => { .catch((error) => reject(error))); // 1. Check if member entered before reservation start time + // console.log('\r\n\r\nChecking if member entered before reservation start time :'); + // console.log('\tunlockEntry : ', unlockEntry && unlockEntry.timestamp ? unlockEntry.timestamp : 'NO UNLOCK ENTRY'); + // console.log('\tCharge before : ', chargeBefore); + // console.log('\tThere is prev. res : ', previousReservationIsBackToBack); if (unlockEntry && chargeBefore && !previousReservationIsBackToBack) { + // console.log('\tIncident : YES'); incidents.push({ incidentType: incidentType.UNSCHEDULED_INCIDENT_BEFORE_RESERVATION, reservation, @@ -435,10 +511,17 @@ const getIncidentData = (reservation) => { timeIntervalsToCharge: timeIntervalsToChargeBefore, totalChargeFee: totalChargeFeeBefore, }); + }else{ + // console.log('\tIncident : NO'); } // 2. Check if member left after reservation end time + // console.log('\r\n\r\nChecking if member left after reservation end time :'); + // console.log('\tlockEntry : ', lockEntry && lockEntry.timestamp ? lockEntry.timestamp : 'NO LOCK ENTRY'); + // console.log('\tCharge after : ', chargeAfter); + // console.log('\tThere is res after : ', nextReservationIsBackToBack); if (lockEntry && chargeAfter && !nextReservationIsBackToBack) { + // console.log('\tIncident : YES'); incidents.push({ incidentType: incidentType.UNSCHEDULED_INCIDENT_AFTER_RESERVATION, reservation, @@ -450,9 +533,16 @@ const getIncidentData = (reservation) => { timeIntervalsToCharge: timeIntervalsToChargeAfter, totalChargeFee: totalChargeFeeAfter, }); + }else{ + // console.log('\tIncident : NO'); } // 3. Check if member forgot to lock the door + // console.log('\r\n\r\nChecking if member left unlocked door :'); + // console.log('\tunlockEntry : ', unlockEntry && unlockEntry.timestamp ? unlockEntry.timestamp : 'NO UNLOCK ENTRY'); + // console.log('\tlockEntry : ', lockEntry && lockEntry.timestamp ? lockEntry.timestamp : 'NO LOCK ENTRY'); + // console.log('\tThere is res before: ', previousReservationIsBackToBack); + // console.log('\tThere is res after : ', nextReservationIsBackToBack); if (!lockEntry && !nextReservationIsBackToBack){ const emptyReservation = { reservationId: null, @@ -461,7 +551,7 @@ const getIncidentData = (reservation) => { }; if (unlockEntry){ - + // console.log('\tIncident : YES [#1]'); incidents.push({ incidentType: incidentType.UNLOCKED_INCIDENT_RELATED_WITH_RESERVATION, unlockTimestamp: unlockEntry.timestamp, @@ -484,6 +574,7 @@ const getIncidentData = (reservation) => { getLastEntryForReservation(reservation) .then((lastEntry) => { if (lastEntry && lastEntry.event === doorLockEvents.USER_UNLOCKED){ + // console.log('\tIncident : YES [#2]'); incidents.push({ incidentType: incidentType.UNLOCKED_INCIDENT_RELATED_WITH_RESERVATION, unlockTimestamp: lastEntry.timestamp, @@ -573,5 +664,7 @@ const calculateDoorLockCharges = () => { }; module.exports = { - calculateDoorLockCharges + calculateDoorLockCharges, + deleteUnlockedIncidentsById, + deleteUnscheduledIncidentsById }; diff --git a/services/integration/invoiceIntegration.js b/services/integration/invoiceIntegration.js index 5e08211..7101224 100644 --- a/services/integration/invoiceIntegration.js +++ b/services/integration/invoiceIntegration.js @@ -65,7 +65,7 @@ const createFeeFromIncident = (incident) => { switch (incidentTypeNumber) { case incidentType.UNLOCKED_INCIDENT_RELATED_WITH_RESERVATION: - spacing = ' '; + spacing = ' '; roomExplanation = resourceName || 'Unknown'; dateExplanation = bookingStartMoment.clone().startOf('day').format('MMM DD'); bookingTimeExplanation = `[${bookingStartMoment.clone().format('HH:mm')} to ${bookingEndMoment.clone().format('HH:mm')}]`; @@ -78,7 +78,7 @@ const createFeeFromIncident = (incident) => { quantity = 1.00; break; case incidentType.UNSCHEDULED_INCIDENT_BEFORE_RESERVATION: - spacing = ' '; + spacing = ' '; roomExplanation = resourceName || 'Unknown'; dateExplanation = bookingStartMoment.clone().startOf('day').format('MMM DD'); bookingTimeExplanation = `[${bookingStartMoment.clone().format('HH:mm')} to ${bookingEndMoment.clone().format('HH:mm')}]`; @@ -91,7 +91,7 @@ const createFeeFromIncident = (incident) => { quantity = +timeIntervalsToCharge.toFixed(2); break; case incidentType.UNSCHEDULED_INCIDENT_AFTER_RESERVATION: - spacing = ' '; + spacing = ' '; roomExplanation = resourceName || 'Unknown'; dateExplanation = bookingStartMoment.clone().startOf('day').format('MMM DD'); bookingTimeExplanation = `[${bookingStartMoment.clone().format('HH:mm')} to ${bookingEndMoment.clone().format('HH:mm')}]`; @@ -104,7 +104,7 @@ const createFeeFromIncident = (incident) => { quantity = +timeIntervalsToCharge.toFixed(2); break; case incidentType.UNLOCKED_INCIDENT_STANDALONE: - spacing = ' '; + spacing = ' '; roomExplanation = resourceName || 'Unknown'; dateExplanation = unlockMoment.clone().startOf('day').format('MMM DD'); bookingTimeExplanation = `[${unlockMoment.clone().format('HH:mm')} to ${lockMoment.clone().format('HH:mm')}]`; @@ -117,7 +117,7 @@ const createFeeFromIncident = (incident) => { quantity = 1.00; break; case incidentType.UNSCHEDULED_INCIDENT_STANDALONE: - spacing = ' '; + spacing = ' '; roomExplanation = resourceName || 'Unknown'; dateExplanation = unlockMoment.clone().startOf('day').format('MMM DD'); bookingTimeExplanation = `[${unlockMoment.clone().format('HH:mm')} to ${lockMoment.clone().format('HH:mm')}]`; @@ -129,7 +129,7 @@ const createFeeFromIncident = (incident) => { quantity = +timeIntervalsToCharge.toFixed(2); break; case incidentType.BOOKING_MOVED_TO_ANOTHER_DAY: - spacing = ' '; + spacing = ' '; // if (oldResourceName !== newResourceName){ // roomExplanation = `${oldResourceName} -> ${newResourceName}`; // }else{ @@ -158,25 +158,11 @@ const createFeeFromIncident = (incident) => { roomExplanation = newResourceName || 'Unknown'; // dateExplanation = `${oldBookingStartMoment.clone().format('ddd, MMM DD')}`; - const oldBookingDuration = oldBookingEndMoment.diff(oldBookingStartMoment, "minutes", false); - const durationInHours = Math.floor(oldBookingDuration / 60); - const durationInMinutes = Math.floor(oldBookingDuration % 60); - let durationAsText = ''; - if (durationInHours !== 0){ - durationAsText += durationInHours + ' hour'; - if (durationInHours === 1){ - durationAsText += ' '; - }else{ - durationAsText += 's '; - } - } - durationAsText += durationInMinutes + ' minute'; - if (durationInMinutes > 1){ - durationAsText += 's'; - } + + const originalBookingExplanation = `${oldBookingStartMoment.clone().format('HH:mm')} to ${oldBookingEndMoment.clone().format('HH:mm')}`; dateExplanation = `${newBookingStartMoment.clone().format('MMM DD')}`; bookingTimeExplanation = `[${newBookingStartMoment.clone().format('HH:mm')} to ${newBookingEndMoment.clone().format('HH:mm')}]`; - incidentTimeExplanation = `reservation shortened from ${durationAsText} on : ${incidentTimestampMoment.clone().format('MMM DD, HH:mm')}`; + incidentTimeExplanation = `reservation shortened from [${originalBookingExplanation}] on [${incidentTimestampMoment.clone().format('MMM DD, HH:mm')}]`; incidentExplanation = `${incidentTimeExplanation}`; date = incidentTimestampMoment.clone().startOf('day').format(); @@ -185,7 +171,7 @@ const createFeeFromIncident = (incident) => { quantity = 1.00; break; case incidentType.BOOKING_CANCELED_LATE: - spacing = ' '; + spacing = ' '; roomExplanation = oldResourceName || 'Unknown'; // dateExplanation = `${oldBookingStartMoment.clone().format('ddd, MMM DD')}`; dateExplanation = `${oldBookingStartMoment.clone().format('MMM DD')}`; diff --git a/services/integration/reports.js b/services/integration/reports.js index 3717112..c605c5c 100644 --- a/services/integration/reports.js +++ b/services/integration/reports.js @@ -18,7 +18,9 @@ const { getChargedCanceledReservations } = require('../integration/bookingChange const getUnlockedIncidents = (startDate, endDate, memberIds) => { const attributes = ['id', 'reservationId', 'memberId', 'resourceId', 'bookingStart', 'bookingEnd', 'unlockTimestamp', 'incidentLevel', 'incidentLevelPrice']; - const filters = {}; + const filters = { + deleted: false + }; if (startDate && endDate) { const bookingStartCondition = { @@ -76,7 +78,9 @@ const getUnscheduledIncidents = (startDate, endDate, memberIds) => { 'totalChargeFee' ]; - const filters = {}; + const filters = { + deleted: false + }; if (startDate && endDate) { const bookingStartCondition = { diff --git a/services/officeRnD/fees.js b/services/officeRnD/fees.js index 8833839..57c0105 100644 --- a/services/officeRnD/fees.js +++ b/services/officeRnD/fees.js @@ -63,7 +63,7 @@ const deleteFeesFromORD = (dateRange, memberIds) => { } }); - API.delete('fees', { data: feeIdsToRemove }) + API.delete('fees/?silent', { data: feeIdsToRemove }) .then(() => { resolve(feesToSkip); })