21 Commits

Author SHA1 Message Date
Bilal Catic
8f740eb8c5 generate member practice summary report without booking change checks 2020-03-09 17:18:38 +01:00
Bilal Catic
1451e0daaf Merge branch 'stop-adding-non-existing-bookings' into 'master'
Stop adding non existing bookings

See merge request saburly/psihologija!83
2020-01-15 18:31:17 +00:00
Bilal Catic
06601f272c do not add reservations to the db for tentative and free bookings 2020-01-15 19:29:57 +01:00
Bilal Catic
aa771bc7bd do not create booking reservation for non existing reservation in ORD with exceptions 2020-01-15 18:48:55 +01:00
Bilal Catic
45258334c1 do not create booking reservation for non existing reservation in ORD 2020-01-15 04:27:16 +01:00
Bilal Catic
55086fb54b handle unknown members, offices and rooms 2020-01-13 12:42:20 +01:00
Bilal Catic
459e6d15ef Merge branch 'fix-bug-with-clean-slate-unlocked-incidents' into 'master'
stop inserting and changing deleted unlock incidents

See merge request saburly/psihologija!82
2020-01-09 22:31:54 +00:00
Bilal Catic
62dc399a1a stop inserting and changing deleted unlock incidents 2020-01-09 23:23:28 +01:00
Bilal Catic
280e8af748 Merge branch 'allow-incident-price-modification-frontend' into 'master'
Allow incident price modification frontend+backend

See merge request saburly/psihologija!81
2020-01-09 17:13:04 +00:00
Bilal Catic
cf0b2793ec remove obsolete stylesheet file 2020-01-09 03:47:17 +01:00
Bilal Catic
e03bf7609e fix paging bug 2020-01-09 03:41:16 +01:00
Bilal Catic
a5bec0f7f3 remove logs on update request 2020-01-09 02:30:22 +01:00
Bilal Catic
aafd046063 show rounded decimals for default values; handle zero as normal input 2020-01-09 02:26:35 +01:00
Bilal Catic
1a21e91796 implement backend methods to update incident fees 2020-01-09 01:33:19 +01:00
Bilal Catic
ff8a836fed add action for sending update data to backend 2020-01-09 01:29:06 +01:00
Bilal Catic
c78a0e4138 allow fees edit; track changes; focus last changed input field 2020-01-08 19:09:35 +01:00
Bilal Catic
3a10e56eeb wait for door lock charges calculation 2020-01-06 22:33:26 +01:00
Bilal Catic
7db5c314ce Merge branch 'fix-undetected-unschedule-use-in-brake-between-bookings' into 'master'
Fix undetected unschedule use in brake between bookings

See merge request saburly/psihologija!80
2019-12-26 01:34:15 +00:00
Bilal Catic
b2450faa0c add special check for unscheduled use in brake between reservations 2019-12-26 02:31:40 +01:00
Bilal Catic
87d7193bb9 add method for finding last reservation in a block 2019-12-26 02:30:03 +01:00
Bilal Catic
c1f3f42368 match unlock entry before first reservation in a block 2019-12-25 16:41:05 +01:00
9 changed files with 531 additions and 111 deletions

View File

@@ -12,16 +12,21 @@ import {
incidentDescriptions,
incidentLevelDescriptions,
UNLOCKED_INCIDENT_RELATED_WITH_RESERVATION, UNLOCKED_INCIDENT_STANDALONE, UNSCHEDULED_INCIDENT_AFTER_RESERVATION,
UNSCHEDULED_INCIDENT_BEFORE_RESERVATION, UNSCHEDULED_INCIDENT_STANDALONE
UNSCHEDULED_INCIDENT_BEFORE_RESERVATION, UNSCHEDULED_INCIDENT_STANDALONE, BOOKING_CANCELED_LATE, BOOKING_MOVED_TO_ANOTHER_DAY,
BOOKING_SHORTENED
} from '../../../constants/enums';
import { doorLockRelatedWithReservationIncidentHeaders, standaloneDoorLockIncidentHeaders, bookingChangeIncidentHeaders } from '../../../constants/constants';
import { deleteIncidents } from "../../../store/actions";
import { deleteIncidents, updateIncidentFees } from "../../../store/actions";
class SingleIncidentsTable extends Component {
state = {
selectedUnlockedIncidentIds: [],
selectedUnscheduledIncidentIds: [],
selectedBookingChangeIncidentIds: []
selectedBookingChangeIncidentIds: [],
changedUnlockedIncidentIds: {},
changedUnscheduledIncidentIds: {},
changedBookingChangeIncidentIds: {},
inputIdToFocus: null,
};
onSelectChange = (selectedIncidents) => {
@@ -82,6 +87,24 @@ class SingleIncidentsTable extends Component {
});
};
updateChangedFees = () => {
const { dateRange, updateIncidentsById, memberId } = this.props;
const { changedUnlockedIncidentIds, changedUnscheduledIncidentIds, changedBookingChangeIncidentIds } = this.state;
const incidentFeesToUpdate = {
unlockedIncidentFees: changedUnlockedIncidentIds,
unscheduledIncidentFees: changedUnscheduledIncidentIds,
bookingChangeIncidentFees: changedBookingChangeIncidentIds
};
updateIncidentsById(dateRange, incidentFeesToUpdate, memberId);
this.setState({
changedUnlockedIncidentIds: {},
changedUnscheduledIncidentIds: {},
changedBookingChangeIncidentIds: {}
});
};
render(){
const {
loading,
@@ -94,12 +117,22 @@ class SingleIncidentsTable extends Component {
const {
selectedUnlockedIncidentIds,
selectedUnscheduledIncidentIds,
selectedBookingChangeIncidentIds
selectedBookingChangeIncidentIds,
changedUnlockedIncidentIds,
changedUnscheduledIncidentIds,
changedBookingChangeIncidentIds,
inputIdToFocus
} = this.state;
const totalSelected = selectedUnlockedIncidentIds.length + selectedUnscheduledIncidentIds.length + selectedBookingChangeIncidentIds.length;
const numberOfSelectedText = totalSelected > 0 ? ` (${totalSelected})` : '';
const totalChanged =
Object.keys(changedUnlockedIncidentIds).length +
Object.keys(changedUnscheduledIncidentIds).length +
Object.keys(changedBookingChangeIncidentIds).length;
const numberOfChangedText = totalChanged > 0 ? ` (${totalChanged})` : '';
const incidents = this.props.incidents ? this.props.incidents : [];
incidents.forEach(incident => {
incident.id = `${incident.incidentType}-${incident.incidentId}`;
@@ -122,6 +155,44 @@ class SingleIncidentsTable extends Component {
break;
}
const priceChangeHandler = (element) => {
if (element && element.target){
const {value: newValue, id: incidentDescriptionID} = element.target;
const newValueFloat = parseFloat(newValue);
const incidentData = incidentDescriptionID.split('-');
const incidentType = parseInt(incidentData[0]) || null;
const incidentID = parseInt(incidentData[1]) || null;
if ((newValueFloat || (newValueFloat === 0) || (isNaN(newValueFloat))) && incidentType && incidentID){
const changedUnlockedIncidentIdsCopy = Object.assign({}, changedUnlockedIncidentIds);
const changedUnscheduledIncidentIdsCopy = Object.assign({}, changedUnscheduledIncidentIds);
const changedBookingChangeIncidentIdsCopy = Object.assign({}, changedBookingChangeIncidentIds);
switch (incidentType) {
case UNLOCKED_INCIDENT_RELATED_WITH_RESERVATION:
case UNLOCKED_INCIDENT_STANDALONE:
changedUnlockedIncidentIdsCopy[incidentID] = newValueFloat;
this.setState({changedUnlockedIncidentIds: changedUnlockedIncidentIdsCopy, inputIdToFocus: incidentDescriptionID});
break;
case UNSCHEDULED_INCIDENT_BEFORE_RESERVATION:
case UNSCHEDULED_INCIDENT_AFTER_RESERVATION:
case UNSCHEDULED_INCIDENT_STANDALONE:
changedUnscheduledIncidentIdsCopy[incidentID] = newValueFloat;
this.setState({changedUnscheduledIncidentIds: changedUnscheduledIncidentIdsCopy, inputIdToFocus: incidentDescriptionID});
break;
case BOOKING_MOVED_TO_ANOTHER_DAY:
case BOOKING_SHORTENED:
case BOOKING_CANCELED_LATE:
changedBookingChangeIncidentIdsCopy[incidentID] = newValueFloat;
this.setState({changedBookingChangeIncidentIds: changedBookingChangeIncidentIdsCopy, inputIdToFocus: incidentDescriptionID});
break;
}
}
}
};
tableHeaders.forEach((header) => {
const columnTitle = incidentsReportHeaderTitles[header];
@@ -143,6 +214,10 @@ class SingleIncidentsTable extends Component {
Cell: props => {
let cellValue = '';
let urlValue = undefined;
let clickablePrice = undefined;
let priceAsNumber = undefined;
let priceInputID = undefined;
let priceFontColor = 'black';
switch (props.column.id) {
case 'memberName':
@@ -198,9 +273,44 @@ class SingleIncidentsTable extends Component {
}
break;
case 'totalChargeFee':
const totalFee = (props.row['_original'].incidentPrice || props.value) || 0;
const totalFeeFormatted = parseFloat(totalFee).toFixed(2);
cellValue = `$ ${totalFeeFormatted}`;
clickablePrice = true;
let totalFee = 0;
let changedIncidentsProxy;
switch (props.row['_original'].incidentType) {
case UNLOCKED_INCIDENT_STANDALONE:
case UNLOCKED_INCIDENT_RELATED_WITH_RESERVATION:
changedIncidentsProxy = changedUnlockedIncidentIds;
break;
case UNSCHEDULED_INCIDENT_STANDALONE:
case UNSCHEDULED_INCIDENT_BEFORE_RESERVATION:
case UNSCHEDULED_INCIDENT_AFTER_RESERVATION:
changedIncidentsProxy = changedUnscheduledIncidentIds;
break;
case BOOKING_CANCELED_LATE:
case BOOKING_MOVED_TO_ANOTHER_DAY:
case BOOKING_SHORTENED:
changedIncidentsProxy = changedBookingChangeIncidentIds;
break;
}
const changedFee = changedIncidentsProxy[props.row['_original'].incidentId];
if (typeof changedFee === 'number'){
// Not undefined, maybe 0 or NaN
if (isNaN(changedFee)){
totalFee = '';
}else{
totalFee = changedFee;
priceFontColor = 'red';
}
}else{
//Not defined, in this context means it is not changed
totalFee = parseFloat((props.row['_original'].incidentPrice || props.value) || 0).toFixed(2);
}
// const totalFeeFormatted = parseFloat(totalFee).toFixed(2);
priceAsNumber = totalFee;
priceInputID = `${props.row['_original'].incidentType || ''}-${props.row['_original'].incidentId || ''}`;
// cellValue = `$ ${totalFeeFormatted}`;
columnContentsAlignment = columnAlignments.right;
break;
case 'oldReservation':
@@ -217,10 +327,22 @@ class SingleIncidentsTable extends Component {
cellValue = props.value;
}
if (openMemberSummaryOnMemberClick && urlValue){
return <NavLink to={urlValue}>{cellValue}</NavLink>
if (clickablePrice){
return <p>$ <input
id={priceInputID}
style={{ color: priceFontColor }}
type={'number'}
onChange={priceChangeHandler}
value={priceAsNumber}
autoFocus={priceInputID === inputIdToFocus}
/>
</p>
}else{
return <div style={{ textAlign: columnContentsAlignment, whiteSpace: 'pre' }}>{cellValue}</div>
if (openMemberSummaryOnMemberClick && urlValue){
return <NavLink to={urlValue}>{cellValue}</NavLink>
}else{
return <div style={{ textAlign: columnContentsAlignment, whiteSpace: 'pre' }}>{cellValue}</div>
}
}
}
});
@@ -235,6 +357,9 @@ class SingleIncidentsTable extends Component {
{
<Button disabled={loading || totalSelected === 0} onClick={this.deleteSelectedFees}>{`Delete selected ${numberOfSelectedText}`}</Button>
}
{
<Button disabled={loading || totalChanged === 0} onClick={this.updateChangedFees}>{`Save changed ${numberOfChangedText}`}</Button>
}
<br/><br/>
{
!loading && incidents &&
@@ -252,7 +377,8 @@ class SingleIncidentsTable extends Component {
}
const mapDispatchToProps = (dispatch) => ({
deleteIncidentsById: (dateRange, incidentsToDelete, memberId) => deleteIncidents(dispatch, {dateRange, incidentsToDelete, memberId})
deleteIncidentsById: (dateRange, incidentsToDelete, memberId) => deleteIncidents(dispatch, {dateRange, incidentsToDelete, memberId}),
updateIncidentsById: (dateRange, updatedIncidentsData, memberId) => updateIncidentFees(dispatch, {dateRange, updatedIncidentsData, memberId})
});
export default connect(null, mapDispatchToProps)(SingleIncidentsTable);

View File

@@ -103,6 +103,21 @@ export const deleteIncidents = (dispatch, deleteData) => {
});
};
export const updateIncidentFees = (dispatch, updateData) => {
const pendingAction = updateData.memberId ? FETCH_MEMBER_INCIDENTS_PENDING : FETCH_INCIDENTS_PENDING;
const successAction = updateData.memberId ? FETCH_MEMBER_INCIDENTS_SUCCESS : FETCH_INCIDENTS_SUCCESS;
const failedAction = updateData.memberId ? FETCH_MEMBER_INCIDENTS_FAILED : FETCH_INCIDENTS_FAILED;
dispatch({type: pendingAction});
API.patch(`/integration/fees`, updateData)
.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')

View File

@@ -2,15 +2,25 @@
const moment = require('moment-timezone');
const { getMappingsFromDatabase, fetchOffices, fetchResources, saveNewMappingToDatabase, deleteMappingById, updateMappingById } = require('../services/officeRnD/resources');
const {
getMappingsFromDatabase,
fetchOffices,
fetchResources,
saveNewMappingToDatabase,
deleteMappingById,
updateMappingById } = require('../services/officeRnD/resources');
const { getAllIncidents, getMemberPracticeSummaryReport } = require('../services/integration/reports');
const { getMembersFeesForDateRange } = require('../services/integration/invoiceIntegration');
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 {
deleteUnlockedIncidentsById,
deleteUnscheduledIncidentsById,
updateUnscheduledIncidentsById,
updateUnlockedIncidentsById } = require('../services/integration/doorLockCharges');
const { deleteBookingChangeIncidentsById, updateBookingChangeIncidentsById } = require('../services/integration/bookingChangeCharges');
const { UI_TIMEZONE, DEFAULT_DATE_FORMAT, ALLOW_SENDING_FEES, integrationServiceErrors } = require('../constants/constants');
@@ -177,7 +187,7 @@ const addFees = (req, res) => {
}
};
const deleteFees= (req, res) => {
const deleteFees = (req, res) => {
const deleteData = req.body;
const dateRange = deleteData.dateRange ? deleteData.dateRange : null;
const incidents = deleteData.incidentsToDelete ? deleteData.incidentsToDelete : null;
@@ -220,6 +230,49 @@ const deleteFees= (req, res) => {
}
};
const updateFees = (req, res) => {
const updateData = req.body;
const dateRange = updateData.dateRange ? updateData.dateRange : null;
const incidents = updateData.updatedIncidentsData ? updateData.updatedIncidentsData : null;
const memberId = updateData.memberId ? updateData.memberId : null;
const unlockedIncidentFees = incidents.unlockedIncidentFees ? incidents.unlockedIncidentFees : {};
const unscheduledIncidentFees = incidents.unscheduledIncidentFees ? incidents.unscheduledIncidentFees : {};
const bookingChangeIncidentFees = incidents.bookingChangeIncidentFees ? incidents.bookingChangeIncidentFees : {};
req.params.startDate = dateRange.startDate ? dateRange.startDate : null;
req.params.endDate = dateRange.endDate ? dateRange.endDate : null;
if (unlockedIncidentFees && unscheduledIncidentFees && bookingChangeIncidentFees){
const asyncUpdateActions = [
updateUnlockedIncidentsById(unlockedIncidentFees),
updateUnscheduledIncidentsById(unscheduledIncidentFees),
updateBookingChangeIncidentsById(bookingChangeIncidentFees)
];
Promise.all(asyncUpdateActions)
.then(() => {
if (memberId){
req.params.memberId = memberId;
getMemberIncidents(req, res);
}else{
getAllIncidentsController(req, res);
}
})
.catch((error) => {
console.log('Error updating 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) => {
@@ -272,5 +325,6 @@ module.exports = {
addFees,
checkProcessingStatus,
getPracticeSummaryReport,
deleteFees
deleteFees,
updateFees
};

View File

@@ -13,7 +13,8 @@ const {
addFees,
checkProcessingStatus,
getPracticeSummaryReport,
deleteFees
deleteFees,
updateFees
} = require('../controllers/integration');
const { calculateDoorLockCharges } = require('../services/integration/doorLockCharges');
@@ -36,6 +37,7 @@ router.get('/officeRnD/membersList', fetchMembersList);
router.post('/integration/addFees', addFees);
router.delete('/integration/fees', deleteFees);
router.patch('/integration/fees', updateFees);
router.get('/integration/processing', checkProcessingStatus);
@@ -44,6 +46,14 @@ router.get('/integration/report/practiceSummary/:year', getPracticeSummaryReport
// temporary route, manually trigger door lock charge calculations
router.get('/calculate', (req, res) => { calculateDoorLockCharges(); res.send();});
router.get('/calculate', (req, res) => {
calculateDoorLockCharges()
.then(() => {
res.send('Done');
})
.catch((err) => {
res.send(`Error \r\n ${err}`);
});
});
module.exports = router;

View File

@@ -19,7 +19,7 @@ const {
const { fetchAllMembers } = require('../officeRnD/members');
const { getMappingsFromDatabase } = require('../officeRnD/resources');
const { getFirstReservationInBlock } = require('../officeRnD/bookings');
const { getFirstReservationInBlock, getFirstPreviousBooking } = require('../officeRnD/bookings');
const extractMappingFromFileName = (fileName) => {
const contentBetweenBracketsRegex = /\[(.*?)\]/;
@@ -196,12 +196,12 @@ const getUnlockEntryForReservation = (reservation, previousReservation) => {
return new Promise((resolve, reject) => {
const { memberId, resourceId } = reservation;
const previousReservationStartMoment = previousReservation && previousReservation.start ?
moment.utc(previousReservation.start) : null;
const previousReservationEndMoment = previousReservation && previousReservation.end ?
moment.utc(previousReservation.end) : null;
const reservationStartMoment = moment.utc(reservation.start);
const fromTimestamp = previousReservationStartMoment && previousReservationStartMoment.tz(UI_TIMEZONE).isSame(reservationStartMoment.tz(UI_TIMEZONE), 'day') ?
previousReservation.start : reservationStartMoment.tz(UI_TIMEZONE).startOf('day').toISOString();
const fromTimestamp = previousReservationEndMoment && previousReservationEndMoment.tz(UI_TIMEZONE).isSame(reservationStartMoment.tz(UI_TIMEZONE), 'day') ?
previousReservation.end : reservationStartMoment.tz(UI_TIMEZONE).startOf('day').toISOString();
const toTimestamp = reservation.end;
@@ -314,13 +314,13 @@ const getLockEntryForReservation = (reservation, nextReservation) => {
const attributes = ['memberName', 'event', 'timestamp'];
const nextReservationEndMoment = nextReservation && nextReservation.end ?
moment.utc(nextReservation.end) : null;
const nextReservationStartMoment = nextReservation && nextReservation.start ?
moment.utc(nextReservation.start) : null;
const reservationStartMoment = moment.utc(reservation.start);
const fromTimestamp = reservation.start;
const toTimestamp = nextReservationEndMoment && nextReservationEndMoment.tz(UI_TIMEZONE).isSame(reservationStartMoment.tz(UI_TIMEZONE), 'day') ?
nextReservation.end : reservationStartMoment.tz(UI_TIMEZONE).endOf('day').toISOString();
const toTimestamp = nextReservationStartMoment && nextReservationStartMoment.tz(UI_TIMEZONE).isSame(reservationStartMoment.tz(UI_TIMEZONE), 'day') ?
nextReservation.start : reservationStartMoment.tz(UI_TIMEZONE).endOf('day').toISOString();
const filters = {
memberId,
@@ -425,11 +425,25 @@ const getLastEntryForReservation = (reservation) => {
const order = [['timestamp', 'DESC']];
db.doorLockEvent.findAll({where: filters, order})
.then((entries) => {
.then(async(entries) => {
if (entries && entries.length > 0){
resolve(entries[0]);
} else {
resolve (undefined);
//No entry found in this block of reservations, now check if there is unlock entry for the first reservation in block, before reservation start time
try {
const firstPreviousBookingBeforeFirstInBlock = await getFirstPreviousBooking(firstReservationInBlock);
const unlockEntryForFirstInBlock = await getUnlockEntryForReservation(firstReservationInBlock, firstPreviousBookingBeforeFirstInBlock);
if (unlockEntryForFirstInBlock){
resolve(unlockEntryForFirstInBlock);
}else{
resolve(undefined);
}
}catch (e) {
reject(e);
}
}
})
.catch((error) => reject(error));

View File

@@ -283,6 +283,22 @@ const deleteBookingChangeIncidentsById = (incidentIds) => {
return db.bookingChangeIncident.update({deleted: true},{where: filters});
};
const updateBookingChangeIncidentsById = (incidentFees) => {
const incidentIds = Object.keys(incidentFees);
const asyncUpdateActions = [];
incidentIds.forEach((incidentId) => {
const newFeeCharge = parseFloat(incidentFees[incidentId]);
if (newFeeCharge || (newFeeCharge === 0)){
asyncUpdateActions.push(db.bookingChangeIncident.update({chargeFee: newFeeCharge}, {where: {id: incidentId}}));
}
});
return Promise.all(asyncUpdateActions);
};
module.exports = {
getChargedCanceledReservations,
getIncidentsFromChanges,
@@ -290,5 +306,6 @@ module.exports = {
getShorteningIncidentsForReservationId,
getReservationsIncidentsForRemoval,
deleteBookingChangeIncidents,
deleteBookingChangeIncidentsById
deleteBookingChangeIncidentsById,
updateBookingChangeIncidentsById
};

View File

@@ -15,7 +15,7 @@ const {
UI_TIMEZONE
} = require('../../constants/constants');
const { getUnlockEntryForReservation, getLockEntryForReservation, getEntriesBetween, getLastEntryForReservation } = require('../doorLock/doorLock');
const { getAllFinishedBookings, getFirstPreviousBooking, getFirstNextBooking } = require('../officeRnD/bookings');
const { getAllFinishedBookings, getFirstPreviousBooking, getFirstNextBooking, getLastReservationInBlock } = require('../officeRnD/bookings');
const getSortedIncidentsForMember = (memberId) => {
const attributes = ['bookingStart', 'incidentLevel', 'incidentLevelPrice', 'unlockTimestamp'];
@@ -80,6 +80,26 @@ const deleteUnscheduledIncidentsById = (incidentIds) => {
return db.unscheduledIncident.update({deleted: true},{where: filters});
};
const updateUnscheduledIncidentsById = (incidentFees) => {
const incidentIds = Object.keys(incidentFees);
const asyncUpdateActions = [];
incidentIds.forEach((incidentId) => {
const newFeeCharge = parseFloat(incidentFees[incidentId]);
if (newFeeCharge || (newFeeCharge === 0)){
asyncUpdateActions.push(db.unscheduledIncident.update({
totalChargeFee: newFeeCharge,
chargePrice: newFeeCharge,
timeIntervalsToCharge: 1
}, {where: {id: incidentId}}));
}
});
return Promise.all(asyncUpdateActions);
};
const insertUnlockedIncidents = (incidents) => {
const asyncJobs = [];
incidents.forEach((incident) => {
@@ -106,7 +126,6 @@ const insertUnlockedIncidents = (incidents) => {
bookingStart: start,
bookingEnd: end,
unlockTimestamp,
incidentLevel,
},
defaults: {...incidentForDB},
}));
@@ -124,6 +143,22 @@ const deleteUnlockedIncidentsById = (incidentIds) => {
return db.unlockedIncident.update({deleted: true},{where: filters});
};
const updateUnlockedIncidentsById = (incidentFees) => {
const incidentIds = Object.keys(incidentFees);
const asyncUpdateActions = [];
incidentIds.forEach((incidentId) => {
const newFeeCharge = parseFloat(incidentFees[incidentId]);
if (newFeeCharge || (newFeeCharge === 0)){
asyncUpdateActions.push(db.unlockedIncident.update({incidentLevelPrice: newFeeCharge}, {where: {id: incidentId}}));
}
});
return Promise.all(asyncUpdateActions);
};
const setUnlockedIncidentsLevel = (incidents) => {
return new Promise ((resolve, reject) => {
const sortingFunction = (incidentA, incidentB) => {
@@ -162,7 +197,7 @@ const setUnlockedIncidentsLevel = (incidents) => {
const incidentsWithLevel = [];
incidents.forEach((incident) => {
const memberLastIncident = membersLastIncident[incident.memberId];
const memberLastIncident = Object.assign({}, membersLastIncident[incident.memberId]);
const formattedIncident = {
reservation: incident.reservation,
@@ -368,10 +403,7 @@ const getIncidentData = (reservation) => {
const { resourceId } = currentReservation;
// const reservationMoment = moment.tz(currentReservation.start, currentReservation.timezone);
// if (currentReservation.memberId === '5ce785af422bdd00967fb781' &&
// reservationMoment.isAfter('2019-12-11 00:00:16+00') &&
// reservationMoment.isBefore('2019-12-12 23:59:59')
// ) {
// if (currentReservation.memberId === '5d240cd34a3efa00882d9526') {
// console.log('\r\n\r\n==== ANALYSE RESERVATION [GET INCIDENT DATA] ==== ');
// console.log('\tStart : ', reservationMoment.format('DD.MM, HH:mm'));
// console.log('\tEnd : ', moment.tz(currentReservation.end, currentReservation.timezone).format('DD.MM, HH:mm'));
@@ -477,7 +509,89 @@ const getIncidentData = (reservation) => {
// console.log('\tThere is res after : ', nextReservationIsBackToBack);
// }
let forgotToLockAsyncCheck;
let asyncUnscheduledUseTest = [];
if (!lockEntry && !nextReservationIsBackToBack){
// Special check
const checkForUnscheduledUseInBrakeBetweenReservations = async () => {
try {
if (nextReservation) {
const lastReservationInBlockAfterBrake = await getLastReservationInBlock(nextReservation);
const fromTimestamp = nextReservation.start;
const toTimestamp = lastReservationInBlockAfterBrake.end;
const entriesInBlock = await getEntriesBetween(fromTimestamp, toTimestamp, reservation.resourceId);
// const reservationMoment = moment.tz(reservation.start, reservation.timezone);
// if (reservation.memberId === '5d240cd34a3efa00882d9526') {
// console.log('\r\n\r\n==== ANALYSE RESERVATION [GET INCIDENT DATA] ==== ');
// console.log('\tStart : ', reservationMoment.format('DD.MM, HH:mm'));
// console.log('\tEnd : ', moment.tz(reservation.end, reservation.timezone).format('DD.MM, HH:mm'));
// console.log('\t----------------------------------');
// console.log('\tLast reservation in block after brake :');
// console.log('\t\tStart : ', moment.tz(lastReservationInBlockAfterBrake.start, UI_TIMEZONE).format('DD.MM, HH:mm'));
// console.log('\t\tEnd : ', moment.tz(lastReservationInBlockAfterBrake.end, UI_TIMEZONE).format('DD.MM, HH:mm'));
// console.log('\tSearch entries : ');
// console.log('\t\tFrom :', moment.tz(fromTimestamp, UI_TIMEZONE).format('DD.MM, HH:mm'));
// console.log('\t\tTo :', moment.tz(toTimestamp, UI_TIMEZONE).format('DD.MM, HH:mm'));
// console.log('\tFirst entry : ');
// }
const addStandaloneUnscheduledIncident = () => {
const unlockMoment = reservation.end ? moment.utc(reservation.end) : null;
const lockMoment = nextReservation.start ? moment.utc(nextReservation.start) : null;
if (unlockMoment && lockMoment) {
const timeDifference = lockMoment.diff(unlockMoment, 'minutes');
const timeIntervalsToCharge = Math.floor(timeDifference / UNSCHEDULED_TIME_RESOLUTION);
const totalChargeFee = timeIntervalsToCharge * UNSCHEDULED_CHARGE_PRICE;
if (timeIntervalsToCharge > 0) {
incidents.push({
incidentType: incidentType.UNSCHEDULED_INCIDENT_STANDALONE,
reservation: emptyReservation,
unlockTimestamp: reservation.end,
lockTimestamp: nextReservation.start,
memberId: reservation.memberId,
resourceId: reservation.resourceId,
chargePrice: UNSCHEDULED_CHARGE_PRICE,
timeIntervalsToCharge,
totalChargeFee,
});
}
}
};
if (Array.isArray(entriesInBlock) && entriesInBlock.length > 0) {
const firstEntry = entriesInBlock[0];
// console.log('\t\tTimestamp : ', moment.tz(firstEntry.timestamp, UI_TIMEZONE).format('DD.MM, HH:mm'));
// console.log('\t\tEvent : ', firstEntry.event);
if (firstEntry && firstEntry.event &&
firstEntry.memberId === reservation.memberId &&
firstEntry.event === doorLockEvents.USER_LOCKED) {
addStandaloneUnscheduledIncident();
}
} else {
const reservationAfterLastReservationInBlock = await getFirstNextBooking(nextReservation);
if (reservationAfterLastReservationInBlock) {
const lastReservationLockEntry = await getLockEntryForReservation(lastReservationInBlockAfterBrake, reservationAfterLastReservationInBlock);
if (lastReservationLockEntry) {
addStandaloneUnscheduledIncident();
}
}
}
}
}catch (e) {
console.log('ERROR while checking for unscheduled use in brake between reservations ');
console.log(e);
}
};
if (unlockEntry){
// if (currentReservation.memberId === '5ce785af422bdd00967fb781' && reservationMoment.isAfter('2019-12-01 00:00:16+00')) {
// console.log('\tIncident : YES [#1]');
@@ -489,6 +603,8 @@ const getIncidentData = (reservation) => {
resourceId: unlockEntry.resourceId,
reservation: reservation && reservation.dataValues ? reservation.dataValues : emptyReservation,
});
asyncUnscheduledUseTest.push(checkForUnscheduledUseInBrakeBetweenReservations());
} else {
// No lock entry, no unlock entry and no reservation after this one
// This is either :
@@ -504,17 +620,18 @@ const getIncidentData = (reservation) => {
forgotToLockAsyncCheck = getLastEntryForReservation(reservation);
forgotToLockAsyncCheck
.then((lastEntry) => {
if (lastEntry && lastEntry.event === doorLockEvents.USER_UNLOCKED){
// if (currentReservation.memberId === '5ce785af422bdd00967fb781' && reservationMoment.isAfter('2019-12-01 00:00:16+00')) {
// console.log('\tIncident : YES [#2]');
// }
incidents.push({
incidentType: incidentType.UNLOCKED_INCIDENT_RELATED_WITH_RESERVATION,
unlockTimestamp: lastEntry.timestamp,
memberId: lastEntry.memberId,
resourceId: lastEntry.resourceId,
reservation: reservation && reservation.dataValues ? reservation.dataValues : emptyReservation,
});
if (lastEntry) {
if (lastEntry && lastEntry.event === doorLockEvents.USER_UNLOCKED) {
incidents.push({
incidentType: incidentType.UNLOCKED_INCIDENT_RELATED_WITH_RESERVATION,
unlockTimestamp: lastEntry.timestamp,
memberId: lastEntry.memberId,
resourceId: lastEntry.resourceId,
reservation: reservation && reservation.dataValues ? reservation.dataValues : emptyReservation,
});
asyncUnscheduledUseTest.push(checkForUnscheduledUseInBrakeBetweenReservations());
}
}
})
.catch((error) => reject(error));
@@ -542,35 +659,36 @@ const getIncidentData = (reservation) => {
const previousReservationUnlockEntry = previousReservationResults.unlockEntry;
//Special check for bookings with break between
if (previousReservation &&
!previousReservationIsBackToBack &&
previousReservation.memberId === currentReservation.memberId &&
previousReservationUnlockEntry && !previousReservationLockEntry &&
!unlockEntry && lockEntry //current reservation unlock / lock entries
) {
const unlockMoment = previousReservation.end ? moment.utc(previousReservation.end) : null;
const lockMoment = currentReservation.start ? moment.utc(currentReservation.start) : null;
if (unlockMoment && lockMoment){
const timeDifference = lockMoment.diff(unlockMoment, 'minutes');
const timeIntervalsToCharge = Math.floor(timeDifference / UNSCHEDULED_TIME_RESOLUTION);
const totalChargeFee = timeIntervalsToCharge * UNSCHEDULED_CHARGE_PRICE;
if (timeIntervalsToCharge > 0) {
incidents.push({
incidentType: incidentType.UNSCHEDULED_INCIDENT_STANDALONE,
reservation: emptyReservation,
unlockTimestamp: previousReservation.end,
lockTimestamp: currentReservation.start,
memberId: currentReservation.memberId,
resourceId,
chargePrice: UNSCHEDULED_CHARGE_PRICE,
timeIntervalsToCharge,
totalChargeFee,
});
}
}
}
// Check this for duplication, previous special check in step 3. can catch this scenario
// if (previousReservation &&
// !previousReservationIsBackToBack &&
// previousReservation.memberId === currentReservation.memberId &&
// previousReservationUnlockEntry && !previousReservationLockEntry &&
// !unlockEntry && lockEntry //current reservation unlock / lock entries
// ) {
//
// const unlockMoment = previousReservation.end ? moment.utc(previousReservation.end) : null;
// const lockMoment = currentReservation.start ? moment.utc(currentReservation.start) : null;
//
// if (unlockMoment && lockMoment){
// const timeDifference = lockMoment.diff(unlockMoment, 'minutes');
// const timeIntervalsToCharge = Math.floor(timeDifference / UNSCHEDULED_TIME_RESOLUTION);
// const totalChargeFee = timeIntervalsToCharge * UNSCHEDULED_CHARGE_PRICE;
// if (timeIntervalsToCharge > 0) {
// incidents.push({
// incidentType: incidentType.UNSCHEDULED_INCIDENT_STANDALONE,
// reservation: emptyReservation,
// unlockTimestamp: previousReservation.end,
// lockTimestamp: currentReservation.start,
// memberId: currentReservation.memberId,
// resourceId,
// chargePrice: UNSCHEDULED_CHARGE_PRICE,
// timeIntervalsToCharge,
// totalChargeFee,
// });
// }
// }
// }
fromTimestamp = previousReservationLockEntry && previousReservationLockEntry.timestamp ?
previousReservationLockEntry.timestamp : previousReservation.end;
@@ -591,7 +709,7 @@ const getIncidentData = (reservation) => {
}
//Now wait for "forgotToLockAsyncCheck", and possible "getEntriesBetween" to finish
Promise.all([forgotToLockAsyncCheck, getEntriesAsyncCheck])
Promise.all([forgotToLockAsyncCheck, getEntriesAsyncCheck, asyncUnscheduledUseTest])
.then((asyncActionResults) => {
const entriesBetween = asyncActionResults[1];
if (Array.isArray(entriesBetween)){
@@ -767,5 +885,7 @@ const calculateDoorLockCharges = () => {
module.exports = {
calculateDoorLockCharges,
deleteUnlockedIncidentsById,
deleteUnscheduledIncidentsById
deleteUnscheduledIncidentsById,
updateUnlockedIncidentsById,
updateUnscheduledIncidentsById
};

View File

@@ -7,7 +7,6 @@ const Op = require('sequelize').Op;
const workbookCreator = require('excel4node');
const { checkBookingChanges } = require('./checkBookingChange');
const { incidentType, UI_TIMEZONE, DEFAULT_DATE_FORMAT, integrationServiceErrors } = require('../../constants/constants');
const { getAllBookingsForMembersInDateRange } = require('./bookings');
@@ -222,19 +221,29 @@ const getAllIncidents = (dateRange, memberIds) => {
unlockedIncidents.forEach((unlockedIncident) => {
const incidentTypeNumber = unlockedIncident.reservationId ?
incidentType.UNLOCKED_INCIDENT_RELATED_WITH_RESERVATION : incidentType.UNLOCKED_INCIDENT_STANDALONE;
const resourceObject = resourcesMap[unlockedIncident.resourceId];
const officeObject = resourceObject ? officesMap[resourceObject.officeId] : null;
const memberName = membersMap[unlockedIncident.memberId] ? membersMap[unlockedIncident.memberId].name : 'Unknown member';
const resourceName = resourceObject ? resourceObject.resourceName : 'Unknown room';
const officeId = resourceObject ? resourceObject.officeId : '';
const officeName = officeObject ? officeObject.officeName : 'Unknown office';
const officeSlug = officeObject ? officeObject.officeSlug : '-';
allIncidents.push({
incidentId: unlockedIncident.id,
memberId: unlockedIncident.memberId,
memberName: membersMap[unlockedIncident.memberId].name,
resourceName: resourcesMap[unlockedIncident.resourceId].resourceName,
officeId: resourcesMap[unlockedIncident.resourceId].officeId,
officeName: officesMap[resourcesMap[unlockedIncident.resourceId].officeId].officeName,
officeSlug: officesMap[resourcesMap[unlockedIncident.resourceId].officeId].officeSlug,
bookingStart: formatTime(unlockedIncident.bookingStart),
bookingEnd: formatTime(unlockedIncident.bookingEnd),
memberName,
resourceName,
officeId,
officeName,
officeSlug,
bookingStart: formatTime(unlockedIncident.bookingStart) || '-',
bookingEnd: formatTime(unlockedIncident.bookingEnd) || '-',
bookingStartRaw: unlockedIncident.bookingStart,
bookingEndRaw: unlockedIncident.bookingEnd,
unlockTimestamp: formatTime(unlockedIncident.unlockTimestamp),
unlockTimestamp: formatTime(unlockedIncident.unlockTimestamp) || '-',
unlockTimestampRaw: unlockedIncident.unlockTimestamp,
incidentType: incidentTypeNumber,
incidentLevel: unlockedIncident.incidentLevel,
@@ -253,20 +262,30 @@ const getAllIncidents = (dateRange, memberIds) => {
}else{
incidentTypeNumber = incidentType.UNSCHEDULED_INCIDENT_STANDALONE;
}
const resourceObject = resourcesMap[unscheduledIncident.resourceId];
const officeObject = resourceObject ? officesMap[resourceObject.officeId] : null;
const memberName = membersMap[unscheduledIncident.memberId] ? membersMap[unscheduledIncident.memberId].name : 'Unknown member';
const resourceName = resourceObject ? resourceObject.resourceName : 'Unknown room';
const officeId = resourceObject ? resourceObject.officeId : '';
const officeName = officeObject ? officeObject.officeName : 'Unknown office';
const officeSlug = officeObject ? officeObject.officeSlug : '-';
allIncidents.push({
incidentId: unscheduledIncident.id,
memberId: unscheduledIncident.memberId,
memberName: membersMap[unscheduledIncident.memberId].name,
resourceName: resourcesMap[unscheduledIncident.resourceId].resourceName,
officeId: resourcesMap[unscheduledIncident.resourceId].officeId,
officeName: officesMap[resourcesMap[unscheduledIncident.resourceId].officeId].officeName,
officeSlug: officesMap[resourcesMap[unscheduledIncident.resourceId].officeId].officeSlug,
bookingStart: formatTime(unscheduledIncident.bookingStart),
bookingEnd: formatTime(unscheduledIncident.bookingEnd),
memberName,
resourceName,
officeId,
officeName,
officeSlug,
bookingStart: formatTime(unscheduledIncident.bookingStart) || '-',
bookingEnd: formatTime(unscheduledIncident.bookingEnd) || '-',
bookingStartRaw: unscheduledIncident.bookingStart,
bookingEndRaw: unscheduledIncident.bookingEnd,
unlockTimestamp: formatTime(unscheduledIncident.unlockTimestamp),
lockTimestamp: formatTime(unscheduledIncident.lockTimestamp),
unlockTimestamp: formatTime(unscheduledIncident.unlockTimestamp) || '-',
lockTimestamp: formatTime(unscheduledIncident.lockTimestamp) || '-',
unlockTimestampRaw: unscheduledIncident.unlockTimestamp,
lockTimestampRaw: unscheduledIncident.lockTimestamp,
incidentType: incidentTypeNumber,
@@ -291,14 +310,14 @@ const getAllIncidents = (dateRange, memberIds) => {
deleted,
createdAt,
} = bookingChangeIncident;
const memberName = membersMap[memberId].name;
const memberName = membersMap[memberId] ? membersMap[memberId].name : 'Unknown member';
const oldResource = resourcesMap[oldResourceId];
const newResource = newResourceId ? resourcesMap[newResourceId] : null;
const oldResourceName = oldResource.resourceName;
const oldResourceName = oldResource ? oldResource.resourceName : 'Unknown room';
const newResourceName = newResource ? newResource.resourceName : null;
const officeId = oldResource.officeId;
const officeName = officesMap[officeId].officeName;
const officeSlug = officesMap[officeId].officeSlug;
const officeId = oldResource ? oldResource.officeId : '';
const officeName = officesMap[officeId] ? officesMap[officeId].officeName : 'Unknown office';
const officeSlug = officesMap[officeId] ? officesMap[officeId].officeSlug : '-';
allIncidents.push({
incidentId: id,
memberId,
@@ -308,10 +327,10 @@ const getAllIncidents = (dateRange, memberIds) => {
officeId,
officeName,
officeSlug,
oldBookingStart: formatTime(oldBookingStart),
oldBookingEnd: formatTime(oldBookingEnd),
newBookingStart: formatTime(newBookingStart),
newBookingEnd: formatTime(newBookingEnd),
oldBookingStart: formatTime(oldBookingStart) || '-',
oldBookingEnd: formatTime(oldBookingEnd) || '-',
newBookingStart: formatTime(newBookingStart) || '-',
newBookingEnd: formatTime(newBookingEnd) || '-',
oldBookingStartRaw: oldBookingStart,
oldBookingEndRaw: oldBookingEnd,
newBookingStartRaw: newBookingStart,
@@ -319,7 +338,7 @@ const getAllIncidents = (dateRange, memberIds) => {
incidentType,
totalChargeFee: chargeFee,
deleted,
incidentTimestamp: formatTime(createdAt),
incidentTimestamp: formatTime(createdAt) || '-',
incidentTimestampRaw: createdAt,
});
});
@@ -340,12 +359,12 @@ const getMemberPracticeSummaryReport = (year) => {
endDate,
};
const asyncJobs = [checkBookingChanges(), getAllBookingsForMembersInDateRange(dateRange), fetchAllMembers()];
const asyncJobs = [getAllBookingsForMembersInDateRange(dateRange), fetchAllMembers()];
Promise.all(asyncJobs)
.then((results) => {
const allBookings = results[1];
const allMembers = results[2];
const allBookings = results[0];
const allMembers = results[1];
const membersMap = {};
@@ -394,6 +413,7 @@ const getMemberPracticeSummaryReport = (year) => {
getChargedCanceledReservations(reservationIdsForAdditionalData)
.then((incidents) => {
console.log('Charged canceled reservations ...');
incidents.forEach((incident) => {
const {memberId, oldBookingStart, oldBookingEnd} = incident.get();

View File

@@ -22,6 +22,21 @@ const fetchAllBookings = () => {
return;
}
const fees = fullBookingEntry.fees ? fullBookingEntry.fees : [];
const canceled = fullBookingEntry.canceled ? fullBookingEntry.canceled : false;
const recurringReservation = fullBookingEntry.recurrence && fullBookingEntry.recurrence.rrule ?
fullBookingEntry.recurrence.rrule : null;
const tentative = fullBookingEntry.tentative ? fullBookingEntry.tentative : false;
const free = fullBookingEntry.free ? fullBookingEntry.free : false;
//Special case, canceled recurring reservation
//It has empty fees array (fees.length = 0) and no "canceled" field
//Normally, canceled reservation has canceled field
//Also, it has rrule !== null
//If it is tentative, it will have tentative = true, skip those (do not delete)
//If it is free, it will have free = true, skip those (do not delete)
if (fees.length === 0 && !canceled && recurringReservation && !tentative && !free){
bookingIdsToRemove.push(fullBookingEntry['_id']);
}
if (fees.length > 1){
// Recurring booking, let's create new booking
@@ -101,6 +116,8 @@ const fetchAllBookings = () => {
const fees = fullBookingEntry && fullBookingEntry.fees ? fullBookingEntry.fees : [];
const firstFee = fees.length > 0 && fees[0].fee ? fees[0].fee : undefined;
const hourlyRate = firstFee && firstFee.price ? firstFee.price : 0;
const free = fullBookingEntry.free ? fullBookingEntry.free : false;
const tentative = fullBookingEntry.tentative ? fullBookingEntry.tentative : false;
const startMoment = fullBookingEntry && fullBookingEntry.start && fullBookingEntry.start.dateTime ?
moment.utc(fullBookingEntry.start.dateTime) : null;
@@ -111,7 +128,7 @@ const fetchAllBookings = () => {
// console.log('End : ', endMoment.clone().tz(fullBookingEntry.timezone).format('DD.MM. HH:mm'), '[', endMoment.toISOString(), ']');
// console.log('Fees : ');
if (startMoment && endMoment){
if (startMoment && endMoment && !free && !tentative){
cleanedBookingReservations.push({
reservationId: fullBookingEntry['_id'],
memberId: fullBookingEntry.member,
@@ -244,6 +261,32 @@ const getFirstReservationInBlock = (reservation) => {
});
};
const getLastReservationInBlock = async (reservation) => {
const { resourceId, memberId, end } = reservation;
const toTimestamp = moment.utc(end).add(MAX_BACK_TO_BACK_DIFFERENCE).toISOString();
const fromTimestamp = end;
const filters = {
resourceId,
memberId,
start: {
[Op.and]: [
{[Op.gte]: fromTimestamp},
{[Op.lte]: toTimestamp}
]
}
};
const nextReservation = await db.bookingReservation.findOne({where: filters});
if (!nextReservation) {
return reservation;
} else {
return getLastReservationInBlock(nextReservation);
}
};
const writeBookingReservation = (bookingReservation) => {
const { reservationId, memberId, officeId, resourceId, start, end, timezone, canceled, hourlyRate } = bookingReservation;
const bookingReservationForDB = {
@@ -369,5 +412,6 @@ module.exports = {
getFirstNextBooking,
getFirstPreviousBooking,
getFirstReservationInBlock,
getLastReservationInBlock,
bulkWriteReservationsWithChangesTracking,
};